import re import pandas as pd from opencc import OpenCC from era_data import dynasty_alias, KNOWN_ERAS cc = OpenCC('t2s') # === 中文数字转阿拉伯数字 === def chinese_to_num(text): text = text.replace("卅", "三十").replace("卌", "四十") if text in ["元", "元年"]: return 1 chinese_numerals = { '零': 0, '〇': 0, '○': 0, '一': 1, '二': 2, '三': 3, '四': 4, '五': 5, '六': 6, '七': 7, '八': 8, '九': 9, '十': 10, '百': 100, '千': 1000, '壹': 1, '貳': 2, '叁': 3, '肆': 4, '伍': 5, '陸': 6, '柒': 7, '捌': 8, '玖': 9, '拾': 10, '卄': 20, '廿': 20, '卅': 30, '卌': 40, } if text in chinese_numerals: return chinese_numerals[text] if '十' in text: parts = text.split('十') total = 0 if parts[0] == '': total += 10 else: total += chinese_numerals.get(parts[0], 0) * 10 if len(parts) > 1 and parts[1]: total += chinese_numerals.get(parts[1], 0) return total if '廿' in text or '卄' in text: return 20 + (chinese_numerals.get(text[-1], 0) if len(text) > 1 else 0) if '卅' in text: return 30 + (chinese_numerals.get(text[-1], 0) if len(text) > 1 else 0) # 混合数字(如"二零〇一") total = 0 for ch in text: total = total * 10 + chinese_numerals.get(ch, int(ch) if ch.isdigit() else 0) return total # 朝代别名见 era_data.py def normalize_dynasty_name(dynasty): if not dynasty: return dynasty # 先将繁体转简体 simplified = cc.convert(dynasty) # 查别名表(简体) for prefix, true_name in dynasty_alias.items(): if simplified.startswith(prefix): return true_name # 直接匹配朝代字典(简体) for known_dynasty in era_dict.keys(): if simplified.startswith(known_dynasty) or known_dynasty.startswith(simplified): return known_dynasty return simplified # === 年号表 === era_df = pd.read_csv("中国年号.csv") era_df["年号"] = era_df["年号"].map(lambda x: cc.convert(str(x))) era_df["所属朝代"] = era_df["所属朝代"].map(lambda x: cc.convert(str(x))) era_dict = {} for _, row in era_df.iterrows(): dynasty, era, start = row["所属朝代"], row["年号"], int(row["公元纪年"]) era_dict.setdefault(dynasty, {})[era] = start # === 干支纪年 === _tiangan = '甲乙丙丁戊己庚辛壬癸' _dizhi = '子丑寅卯辰巳午未申酉戌亥' _ganzhi_to_offset = {} for i in range(60): tg = _tiangan[i % 10] dz = _dizhi[i % 12] _ganzhi_to_offset[tg + dz] = i # 0-based: 甲子=0 def ganzhi_to_era_year(ganzhi: str) -> int: """干支转年数(1-based):甲子=1, 乙丑=2, ..., 癸亥=60""" return _ganzhi_to_offset.get(ganzhi, -1) + 1 def gregorian_to_ganzhi(year: int) -> str: """按项目约定将公元年转换为干支(公元4年为甲子)。""" offset = (year - 4) % 60 return _tiangan[offset % 10] + _dizhi[offset % 12] _gz_pair = '[' + _tiangan + '][' + _dizhi + ']' # === 数字转中文 === def arabic_to_chinese_year(num: int) -> str: digits = "〇一二三四五六七八九" if num < 0: return '-' + ''.join(digits[int(d)] for d in str(abs(num))) return ''.join(digits[int(d)] for d in str(num)) # === 古代年号正则 === # 从 era_dict 构建朝代匹配组(按长度降序排列,优先匹配长朝代名如"北宋"而非"北") # 额外添加常见但不在CSV中的朝代简称(如"汉") _extra_dynasty_names = ['汉', '晋', '宋', '周'] _dynasty_names = sorted( list(era_dict.keys()) + [d for d in _extra_dynasty_names if d not in era_dict], key=len, reverse=True ) _dynasty_alt = '|'.join(re.escape(d) for d in _dynasty_names) # 常见朝代前缀(大、皇等),用于匹配"大唐""大清"等 _dynasty_prefix = '大皇前后胡' # 非元年数字字符(排除元,避免歧义:建元元年中第一个元是年号的一部分) _year_digits = '〇零一二三四五六七八九十廿卄卅卌' # 已知年号白名单见 era_data.py # 预编译已知年号正则片段(按长度降序,优先匹配长年号如"太平兴国"而非"太平") _known_era_alt = '|'.join(sorted(KNOWN_ERAS, key=len, reverse=True)) _known_era_group = r'(?P' + _known_era_alt + r')' _yd = '[' + _year_digits + ']' def _build_pattern(with_dynasty=True): """构建年号正则:已知年号优先 + 通用兜底(仅限有朝代前缀时)""" if with_dynasty: # 朝代前缀模式:加(?(?:(?)' # 无朝代模式不加边界(允许年号跟在其他汉字后面,如"永乐大帝在永乐三年") boundary = '' if with_dynasty: # 有朝代前缀:年号后直接跟年份数字,无需额外约束 era_year = _known_era_group + r'(?P' + _yd + r'+)年' era_yuan = _known_era_group + r'元年' patterns = [ re.compile(dynasty_part + boundary + era_year), re.compile(dynasty_part + boundary + era_yuan), # 通用兜底(2-8字) re.compile(dynasty_part + boundary + r'(?P[一-鿿]{2,8})' + r'(?P' + _yd + r'+)年'), re.compile(dynasty_part + boundary + r'(?P[一-鿿]{2,8})元年'), ] else: # 无朝代前缀:年号后必须紧跟年份数字(前瞻断言),防止"永乐大帝"误匹配 _year_ahead = r'(?=' + _yd + r'+年|元年)' era_year = _known_era_group + _year_ahead + r'(?P' + _yd + r'+)年' era_yuan = _known_era_group + _year_ahead + r'元年' patterns = [ re.compile(dynasty_part + boundary + era_year), re.compile(dynasty_part + boundary + era_yuan), ] return patterns # 无朝代模式只匹配已知年号(避免误匹配如"国"被当年号) _patterns_no_dynasty = _build_pattern(with_dynasty=False) # 有朝代模式:已知年号 + 通用兜底 _patterns_dynasty = _build_pattern(with_dynasty=True) # === 主函数 === def extract_era_years(text): # 去除标记用的方括号(用户标注用,非原文内容) text = text.replace('[', '').replace(']', '') original_text = text simplified_text = cc.convert(text) results = [] # === 1. 民国 === minguo_pattern = re.compile(r'民[国國]\s*([〇零一二三四五六七八九十百廿卄卅卌元\d]+)\s*年') for m in minguo_pattern.finditer(simplified_text): year_text = m.group(1) year_num = chinese_to_num(year_text) start, end = m.start(), m.end() raw_fan = original_text[start:end] results.append({ "原文": raw_fan, "原文(简体)": m.group(0), "朝代": "民国", "年号": "民国纪年", "年数": year_num, "公元": 1911 + year_num, "公元中文": f"{arabic_to_chinese_year(1911 + year_num)}年", "位置": {"起始": start, "结束": end}, "类型": "民国纪年", }) # 1b. 民国+干支纪年(如"民国丁卯""民国甲戌") minguo_gz = re.compile(r'民[国國]\s*(?P' + _gz_pair + r')') for m in minguo_gz.finditer(simplified_text): ganzhi = m.group("ganzhi") gz_offset = _ganzhi_to_offset.get(ganzhi, -1) if gz_offset < 0: continue # 民国元年=1912, base_gz = (1912-4)%60 = 8 (己酉) base_gz = (1912 - 4) % 60 year_in_era = ((gz_offset - base_gz) % 60) + 1 gregorian = 1912 + year_in_era - 1 start, end = m.start(), m.end() raw_fan = original_text[start:end] results.append({ "原文": raw_fan, "原文(简体)": m.group(0), "朝代": "民国", "年号": "民国纪年", "年数": year_in_era, "干支": ganzhi, "公元": gregorian, "公元中文": f"{arabic_to_chinese_year(gregorian)}年", "位置": {"起始": start, "结束": end}, "类型": "民国纪年(干支)", }) # === 2. 公元(允许无"年"字)=== gongyuan_pattern = re.compile( r'(公元)?\s*([〇零○O一二三四五六七八九\d]{3,4})\s*(?:(.*?))?\s*(年)?' ) for m in gongyuan_pattern.finditer(simplified_text): year_text = m.group(2) year_num = chinese_to_num(year_text) if not (1000 <= year_num <= 2100): continue start, end = m.start(), m.end() raw_fan = original_text[start:end] era_type = "公元纪年(中华人民共和国)" if year_num >= 1949 else "公元纪年(近代/其他)" results.append({ "原文": raw_fan, "原文(简体)": m.group(0), "类型": era_type, "公元": year_num, "公元中文": f"{arabic_to_chinese_year(year_num)}年", "位置": {"起始": start, "结束": end}, }) # === 3. 干支纪年(如"清雍正乙巳十一月十二午时生")=== covered = set() def _process_ganzhi_match(m): """处理干支纪年匹配""" raw_dynasty = m.group("dynasty") if "dynasty" in m.groupdict() else "" dynasty = normalize_dynasty_name(raw_dynasty or "") era = cc.convert(m.group("era")) ganzhi = m.group("ganzhi") if ganzhi not in _ganzhi_to_offset: return # 查找朝代和基准年 base = None if dynasty: for d, eras in era_dict.items(): if dynasty in d or d in dynasty: if era in eras: base = eras[era] dynasty = d break if not base: for d, eras in era_dict.items(): if era in eras: base = eras[era] dynasty = d break if not base: return # 干支60年循环:计算基准年的干支位置,再求偏移 base_gz = (base - 4) % 60 # 公元4年=甲子(0),所以偏移=4 gz_offset = _ganzhi_to_offset.get(ganzhi, 0) # 0-based year_in_era = ((gz_offset - base_gz) % 60) + 1 # 1-based年数 gregorian = base + year_in_era - 1 start, end = m.start(), m.end() if any(pos in covered for pos in range(start, end)): return covered.update(range(start, end)) raw_fan = original_text[start:end] results.append({ "原文": raw_fan, "原文(简体)": m.group(0), "朝代": dynasty, "年号": era, "年数": year_in_era, "干支": ganzhi, "公元": gregorian, "公元中文": f"{arabic_to_chinese_year(gregorian)}年", "位置": {"起始": start, "结束": end}, "类型": "干支纪年", }) # 模式A:有朝代前缀(如"清雍正乙巳") # 单字年号加前瞻(?![地支]),防止单字年号"丙"吞掉天干"丙午"中的"丙" _gz_era_group = r'(?P' + '|'.join( (re.escape(e) + (r'(?![子丑寅卯辰巳午未申酉戌亥])' if len(e) == 1 else '')) for e in sorted(KNOWN_ERAS, key=len, reverse=True) ) + r')' ganzhi_dynasty = re.compile( r'(?P(?:(?' + _gz_pair + r')(?:年)?' ) for m in ganzhi_dynasty.finditer(simplified_text): _process_ganzhi_match(m) # 模式B:无朝代前缀(如"嘉庆癸亥"),年号≥2字符避免天干误匹配 _long_era_names = [e for e in KNOWN_ERAS if len(e) >= 2] _long_era_alt = '|'.join(sorted(_long_era_names, key=len, reverse=True)) ganzhi_no_dynasty = re.compile( r'(?P)' + r'(?P' + _long_era_alt + r')' + r'(?P' + _gz_pair + r')(?:年)?' ) for m in ganzhi_no_dynasty.finditer(simplified_text): _process_ganzhi_match(m) # === 4. 古代年号 === def _process_era_match(m, year_override=None): """处理一个年号正则匹配结果""" raw_dynasty = m.group("dynasty") if "dynasty" in m.groupdict() else "" dynasty = normalize_dynasty_name(raw_dynasty or "") era = cc.convert(m.group("era")) # 过滤误匹配:年号是单个数字字符(如"民国二十六年"中的"二") if len(era) == 1 and era in '零一二三四五六七八九十廿卄卅卌': return # 过滤误匹配:年号全是数字汉字(如"元一九五二年"中的"一九五") if era and all(ch in '零一二三四五六七八九十〇' for ch in era): return year_text = year_override if year_override is not None else m.group("year") year_num = chinese_to_num(year_text) base = None if dynasty: for d, eras in era_dict.items(): if dynasty in d or d in dynasty: if era in eras: base = eras[era] dynasty = d break else: candidates = [] for d, eras in era_dict.items(): if era in eras: candidates.append((d, eras[era])) if len(candidates) == 1: dynasty, base = candidates[0] elif len(candidates) > 1: dynasty, base = candidates[0] start, end = m.start(), m.end() raw_fan = original_text[start:end] covered.update(range(start, end)) if base: gregorian = base + year_num - 1 results.append({ "原文": raw_fan, "原文(简体)": m.group(0), "朝代": dynasty, "年号": era, "年数": year_num, "公元": gregorian, "公元中文": f"{arabic_to_chinese_year(gregorian)}年", "位置": {"起始": start, "结束": end}, "类型": "古代纪年", }) else: results.append({ "原文": raw_fan, "原文(简体)": m.group(0), "朝代": dynasty, "年号": era, "年数": year_num, "位置": {"起始": start, "结束": end}, "类型": "古代纪年(未匹配)", }) # 收集所有匹配,对于重叠区间保留覆盖范围更大的(如"金大定元年"优先于"大定元年") all_matches = [] for pat in _patterns_no_dynasty: for m in pat.finditer(simplified_text): has_year = "year" in m.groupdict() and m.group("year") is not None all_matches.append((m, None if has_year else "元", False)) for pat in _patterns_dynasty: for m in pat.finditer(simplified_text): has_year = "year" in m.groupdict() and m.group("year") is not None all_matches.append((m, None if has_year else "元", True)) # 按覆盖范围降序排列(长的优先),同长度时朝代模式优先 all_matches.sort(key=lambda x: (x[0].end() - x[0].start(), x[2]), reverse=True) for m, year_override, _is_dynasty in all_matches: if not any(pos in covered for pos in range(m.start(), m.end())): _process_era_match(m, year_override=year_override) for result in results: if result.get("公元") is not None: result.setdefault("干支", gregorian_to_ganzhi(result["公元"])) return results def convert_text(text): results = extract_era_years(text) return results