修整部分问题,归纳年号别名到单独文件

This commit is contained in:
sansen
2026-07-23 17:14:26 +08:00
parent 2a1db6624b
commit 5564a0ce77
5 changed files with 1137 additions and 0 deletions
+411
View File
@@ -0,0 +1,411 @@
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<era>' + _known_era_alt + r')'
_yd = '[' + _year_digits + ']'
def _build_pattern(with_dynasty=True):
"""构建年号正则:已知年号优先 + 通用兜底(仅限有朝代前缀时)"""
if with_dynasty:
# 朝代前缀模式:加(?<![一-鿿])防止"元"在"开元"中被当朝代
dynasty_part = r'(?P<dynasty>(?:(?<![一-鿿])[' + _dynasty_prefix + r'](?:' + _dynasty_alt + r'))|(?:(?<![一-鿿])' + _dynasty_alt + r'))'
boundary = ''
else:
dynasty_part = r'(?P<dynasty>)'
# 无朝代模式不加边界(允许年号跟在其他汉字后面,如"永乐大帝在永乐三年")
boundary = ''
if with_dynasty:
# 有朝代前缀:年号后直接跟年份数字,无需额外约束
era_year = _known_era_group + r'(?P<year>' + _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<era>[一-鿿]{2,8})' + r'(?P<year>' + _yd + r'+)年'),
re.compile(dynasty_part + boundary + r'(?P<era>[一-鿿]{2,8})元年'),
]
else:
# 无朝代前缀:年号后必须紧跟年份数字(前瞻断言),防止"永乐大帝"误匹配
_year_ahead = r'(?=' + _yd + r'+年|元年)'
era_year = _known_era_group + _year_ahead + r'(?P<year>' + _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<ganzhi>' + _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<era>' + '|'.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<dynasty>(?:(?<![一-鿿])[' + _dynasty_prefix + r'](?:' + _dynasty_alt + r'))|(?:(?<![一-鿿])' + _dynasty_alt + r'))'
+ _gz_era_group
+ r'(?P<ganzhi>' + _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<dynasty>)'
+ r'(?P<era>' + _long_era_alt + r')'
+ r'(?P<ganzhi>' + _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