Files
ancient_date_parser/parser.py
T

412 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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