增加批量接口,完善日期解析

This commit is contained in:
sansen
2026-08-14 17:06:10 +08:00
parent 86e7ef2971
commit 2f3830bd5d
3 changed files with 203 additions and 87 deletions
+122 -83
View File
@@ -3,7 +3,7 @@ import re
import pandas as pd
from opencc import OpenCC
from era_data import dynasty_alias, KNOWN_ERAS, VARIANT_MAP, TEMPLE_ALIASES
from era_data import dynasty_alias, KNOWN_ERAS, VARIANT_MAP, TEMPLE_ALIASES, ERA_ALIASES
cc = OpenCC('t2s')
@@ -119,13 +119,18 @@ for _, row in era_df.iterrows():
if span is not None:
era_span.setdefault(dynasty, {}).setdefault(era, span)
# 有效年号白名单 = 手工精选 ∪ CSV 年号(自动补全:白名单不应小于数据表)。
# 有效年号白名单 = 手工精选 ∪ CSV 年号 年号别名键(自动补全:白名单不应小于数据表)。
# 排除:民国(有专门匹配段)、带括号的年号(如"(后元)",防止与东汉"中元"等歧义)
_csv_eras = {
e for e in era_df["年号"]
if e and e != '民国' and '(' not in e and '' not in e and 2 <= len(e) <= 8
}
KNOWN_ERAS_ALL = KNOWN_ERAS | _csv_eras
KNOWN_ERAS_ALL = KNOWN_ERAS | _csv_eras | set(ERA_ALIASES)
def _norm_era(era):
"""年号归一:OpenCC 繁转简后套年号别名(崇正→崇祯),返回规范写法"""
era = cc.convert(era)
return ERA_ALIASES.get(era, era)
# === 庙号表 ===
@@ -256,70 +261,40 @@ _patterns_no_dynasty = _build_pattern(with_dynasty=False)
_patterns_dynasty = _build_pattern(with_dynasty=True)
# === 农历月日(干支纪年后的"十一月十四""闰六月廿一日" ===
_month_special = {"": 1, "": 11, "": 12}
def _parse_month(text):
"""月文本转数字:正月=1、冬月=11、腊月=12,其余走中文数字(十一月→11)"""
return _month_special.get(text, chinese_to_num(text))
def _parse_day(text):
"""日文本转数字:剥""前缀(初八→8、初十→10),廿三→23"""
if text.startswith(""):
text = text[1:]
return chinese_to_num(text)
_month_day_re = re.compile(
r'(?P<leap>闰)?(?P<month>[正冬腊〇零一二三四五六七八九十廿卄卅卌]{1,3})月'
r'(?:(?P<day>[初〇零一二三四五六七八九十廿卄卅卌]{1,3})(?:日)?)?'
)
# 嵌入干支正则的可选月日片段(整体捕获,事后用 _month_day_re 解析)
_md_body = r'(?:闰)?[正冬腊〇零一二三四五六七八九十廿卄卅卌]{1,3}月' \
r'(?:[初〇零一二三四五六七八九十廿卄卅卌]{1,3}(?:日)?)?'
# 族谱常见的生死动词:月日与(公元对照)之间可能隔着"生/殁"等单字
_md_verb = r'[生死殁卒薨殓]'
def _attach_month_day(result, m):
"""把匹配中的月日组(md1/md2/md,括号前/后位置)并入结果字段"""
md = None
for name in ("md1", "md2", "md"):
if name in m.groupdict() and m.group(name):
md = m.group(name)
break
if not md:
return
mm = _month_day_re.match(md)
if not mm:
return
result[""] = _parse_month(mm.group("month"))
if mm.group("day"):
result[""] = _parse_day(mm.group("day"))
result["月日"] = md
# === 2c. 干支+(公元对照)(如"清同治壬申(1872)十一月十四生") ===
# 干支纪年旁直接给出公元年份:合并为一条结果、对照校验干支、紧跟的月日并入字段
# 置于公元段之前(见 extract_era_years),避免"1872"被单独摘出成公元纪年。
# 干支纪年旁直接给出公元年份:合并为一条结果、对照校验干支。
# 除括号外还支持:"公元"标记(清同治壬申公元1872年)、横线(壬申-1872)、
# 无分隔连写(壬申1872年)。其余内容(如"十一月十四")不并入原文。
# 置于公元段之前(见 extract_era_years),避免对照年份被单独摘出成公元纪年。
_dynasty_opt = (r'(?P<dynasty>(?:(?<![一-鿿])[' + _dynasty_prefix + r'](?:' + _dynasty_alt + r'))'
r'|(?:(?<![一-鿿])' + _dynasty_alt + r'))?')
# 括号中的公元年份:阿拉伯/全角/中文数字 3-4 位,可带"年"字
_gz_gy_paren = r'[(]\s*(?P<gy_year>[〇零○O一二三四五六七八九\d-]{3,4})\s*年?[)]'
# 公元对照年份:阿拉伯/全角/中文数字 3-4 位,可带"年"字
_gz_gy_year = r'[〇零○O一二三四五六七八九\d-]{3,4}'
# 干支与对照年份之间的连接方式(按优先级):
# 括号(1872)/ 公元标记(,公元1872年)/ 横线(-1872)/ 无分隔连写(壬申1872年)
# 空白只允许空格/制表符([ \t]*),不允许换行——行尾的 \n 不得被吞进原文
# 公元标记支持 公元/西元/公历 三种写法
_gz_gy_sep = (
r'(?:'
r'(?:[(][ \t]*(?P<gy_year>' + _gz_gy_year + r')[ \t]*年?[)])'
r'|(?:[,、]?[ \t]*(?:公元|西元|公历)[ \t]*(?P<gy_year2>' + _gz_gy_year + r')[ \t]*年?)'
r'|(?:[-—–-~][ \t]*(?P<gy_year3>' + _gz_gy_year + r')[ \t]*年?)'
r'|(?P<gy_year4>' + _gz_gy_year + r')[ \t]*年?'
r')'
)
# 干支前的修饰词(如"光绪岁次辛丑""岁在壬戌"):碑刻/谱牒常见
_gz_mod = r'(?:岁[次在])?'
_ganzhi_gongyuan_re = re.compile(
_dynasty_opt
+ _gz_era_group
+ _gz_mod
+ r'(?P<ganzhi>' + _gz_pair + r')(?:年)?'
+ r'(?P<md1>' + _md_body + r')?'
+ r'(?:' + _md_verb + r')?'
+ _gz_gy_paren
+ r'(?P<md2>' + _md_body + r')?'
+ _gz_gy_sep
)
# 裸干支+(对照):无年号可锚定(如"壬申(1872)"),公元取对照值
_ganzhi_bare_gongyuan_re = re.compile(
r'(?P<ganzhi>' + _gz_pair + r')(?:年)?'
+ r'(?P<md1>' + _md_body + r')?'
+ r'(?:' + _md_verb + r')?'
+ _gz_gy_paren
+ r'(?P<md2>' + _md_body + r')?'
+ _gz_gy_sep
)
@@ -342,28 +317,66 @@ def extract_era_years(text):
# 覆盖集:先匹配的段落占据位置,后续段落跳过重叠(防止同一文本被多段重复摘出)
covered = set()
# 民国纪年的公元对照仅支持括号形式("民国三十八年(1949)"):
# 民国X年已是确定年份,逗号/横线/连写分隔多为并列句(如"民国三十八年,公元二〇二四年"是两个日期),不合并
_mg_gy_paren = r'(?:[(][ \t]*(?P<gy_year>' + _gz_gy_year + r')[ \t]*年?[)])?'
# === 1. 民国 ===
minguo_pattern = re.compile(r'民[国國]\s*([〇零一二三四五六七八九十百廿卄卅卌元\d-9壹貳贰叁肆伍陸陆柒捌玖拾]+)\s*年')
minguo_pattern = re.compile(
r'民[国國]\s*([〇零一二三四五六七八九十百廿卄卅卌元\d-9壹貳贰叁肆伍陸陆柒捌玖拾]+)\s*年'
+ _mg_gy_paren
)
for m in minguo_pattern.finditer(simplified_text):
year_text = m.group(1)
year_num = chinese_to_num(year_text)
# 公元对照(如"民国三十八年(1949)"):取对照值为公元,不一致时标注
gy_year = None
for name in ("gy_year", "gy_year2", "gy_year3", "gy_year4"):
if name in m.groupdict() and m.group(name):
gy_year = chinese_to_num(m.group(name))
break
if gy_year is not None and not (1000 <= gy_year <= 2100):
continue
start, end = m.start(), m.end()
# 尾随干支校验(如"民国三十八年己丑"):并入原文同一单位(同公元段逻辑)
ganzhi = simplified_text[end:end + 2]
if ganzhi in _ganzhi_to_offset:
end += 2
else:
j = end
while j < len(simplified_text) and simplified_text[j] in '[]【】':
j += 1
ganzhi = simplified_text[j:j + 2]
if ganzhi not in _ganzhi_to_offset:
ganzhi = None
covered.update(range(start, end))
raw_fan = original_text[start:end]
results.append({
gregorian = gy_year if gy_year is not None else 1911 + year_num
result = {
"原文": raw_fan,
"原文(简体)": m.group(0),
"原文(简体)": simplified_text[start:end],
"朝代": "民国",
"年号": "民国纪年",
"年数": year_num,
"公元": 1911 + year_num,
"公元中文": f"{arabic_to_chinese_year(1911 + year_num)}",
"公元": gregorian,
"公元中文": f"{arabic_to_chinese_year(gregorian)}",
"位置": {"起始": start, "结束": end},
"类型": "民国纪年",
})
}
if ganzhi:
result["干支"] = ganzhi
if gregorian_to_ganzhi(gregorian) != ganzhi:
result["干支不符"] = True
if gy_year is not None and gy_year != 1911 + year_num:
result["干支不符"] = True
result["干支推算"] = 1911 + year_num
results.append(result)
# 1b. 民国+干支纪年(如"民国丁卯""民国甲戌"
minguo_gz = re.compile(r'民[国國]\s*(?P<ganzhi>' + _gz_pair + r')')
# 1b. 民国+干支纪年(如"民国丁卯""民国甲戌",支持(公元对照)合并
minguo_gz = re.compile(
r'民[国國]\s*(?P<ganzhi>' + _gz_pair + r')(?:年)?'
+ _mg_gy_paren
)
for m in minguo_gz.finditer(simplified_text):
ganzhi = m.group("ganzhi")
gz_offset = _ganzhi_to_offset.get(ganzhi, -1)
@@ -372,22 +385,34 @@ def extract_era_years(text):
# 民国元年=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
computed = 1912 + year_in_era - 1
# 公元对照(如"民国壬申(1932)"):取对照值为公元,不一致时标注
gy_year = None
for name in ("gy_year", "gy_year2", "gy_year3", "gy_year4"):
if name in m.groupdict() and m.group(name):
gy_year = chinese_to_num(m.group(name))
break
if gy_year is not None and not (1000 <= gy_year <= 2100):
continue
start, end = m.start(), m.end()
covered.update(range(start, end))
raw_fan = original_text[start:end]
results.append({
result = {
"原文": raw_fan,
"原文(简体)": m.group(0),
"朝代": "民国",
"年号": "民国纪年",
"年数": year_in_era,
"干支": ganzhi,
"公元": gregorian,
"公元中文": f"{arabic_to_chinese_year(gregorian)}",
"公元": gy_year if gy_year is not None else computed,
"公元中文": f"{arabic_to_chinese_year(gy_year if gy_year is not None else computed)}",
"位置": {"起始": start, "结束": end},
"类型": "民国纪年(干支)",
})
}
if gy_year is not None and gy_year != computed:
result["干支不符"] = True
result["干支推算"] = computed
results.append(result)
# === 2b. 年号+公元(如"清康熙公元一六八七年丁卯") ===
# 年号后直接给出公元年份:年数=公元-年号起始+1,与"年号+年数"公式自洽;
@@ -395,14 +420,14 @@ def extract_era_years(text):
_era_gongyuan = re.compile(
r'(?P<dynasty>(?:(?<![一-鿿])[' + _dynasty_prefix + r'](?:' + _dynasty_alt + r'))|(?:(?<![一-鿿])' + _dynasty_alt + r'))?'
+ r'(?P<era>' + _known_era_alt + r')'
+ r'公元\s*(?P<gy_year>[〇零○O一二三四五六七八九\d-]{3,4})\s*年?'
+ r'(?:公元|西元|公历)\s*(?P<gy_year>[〇零○O一二三四五六七八九\d-]{3,4})\s*年?'
+ r'(?P<gy_ganzhi>' + _gz_pair + r')?'
)
for m in _era_gongyuan.finditer(simplified_text):
start, end = m.start(), m.end()
if any(pos in covered for pos in range(start, end)):
continue
era = cc.convert(m.group("era"))
era = _norm_era(m.group("era"))
year_num = chinese_to_num(m.group("gy_year"))
if not (1000 <= year_num <= 2100):
continue
@@ -458,9 +483,14 @@ def extract_era_years(text):
def _process_ganzhi_gongyuan(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")) if "era" in m.groupdict() and m.group("era") else None
era = _norm_era(m.group("era")) if "era" in m.groupdict() and m.group("era") else None
ganzhi = m.group("ganzhi")
gy_year = chinese_to_num(m.group("gy_year"))
# 对照年份可能来自四种连接方式之一(gy_year/gy_year2/gy_year3/gy_year4
gy_year = next(
chinese_to_num(m.group(name))
for name in ("gy_year", "gy_year2", "gy_year3", "gy_year4")
if m.group(name)
)
if ganzhi not in _ganzhi_to_offset or not (1000 <= gy_year <= 2100):
return
start, end = m.start(), m.end()
@@ -512,7 +542,6 @@ def extract_era_years(text):
elif gregorian_to_ganzhi(gy_year) != ganzhi:
# 裸干支+对照:无年号可锚定,直接用公元年份的干支校验(同公元段约定)
result["干支不符"] = True
_attach_month_day(result, m)
results.append(result)
for m in _ganzhi_gongyuan_re.finditer(simplified_text):
@@ -522,13 +551,13 @@ def extract_era_years(text):
# === 2. 公元(允许无"年"字)===
gongyuan_pattern = re.compile(
r'(公元)?\s*([〇零○O一二三四五六七八九\d-]{3,4})\s*(?:.*?)?\s*(年)?'
r'(?:公元|西元|公历)?\s*([〇零○O一二三四五六七八九\d-]{3,4})\s*(?:.*?)?\s*(年)?'
)
for m in gongyuan_pattern.finditer(simplified_text):
start, end = m.start(), m.end()
if any(pos in covered for pos in range(start, end)):
continue
year_text = m.group(2)
year_text = m.group(1)
year_num = chinese_to_num(year_text)
if not (1000 <= year_num <= 2100):
continue
@@ -567,7 +596,7 @@ def extract_era_years(text):
"""处理干支纪年匹配"""
raw_dynasty = m.group("dynasty") if "dynasty" in m.groupdict() else ""
dynasty = normalize_dynasty_name(raw_dynasty or "")
era = cc.convert(m.group("era"))
era = _norm_era(m.group("era"))
ganzhi = m.group("ganzhi")
if ganzhi not in _ganzhi_to_offset:
return
@@ -622,16 +651,14 @@ def extract_era_years(text):
span = era_span.get(dynasty, {}).get(era)
if span is not None and gregorian > base + span - 1:
result["超出使用期"] = True
_attach_month_day(result, m)
results.append(result)
# 模式A:有朝代前缀(如"清雍正乙巳");年号组 _gz_era_group 见模块级定义
ganzhi_dynasty = re.compile(
r'(?P<dynasty>(?:(?<![一-鿿])[' + _dynasty_prefix + r'](?:' + _dynasty_alt + r'))|(?:(?<![一-鿿])' + _dynasty_alt + r'))'
+ _gz_era_group
+ _gz_mod
+ r'(?P<ganzhi>' + _gz_pair + r')(?:年)?'
# 紧跟的农历月日并入同一单位(如"清雍正乙巳十一月十二午时生")
+ r'(?P<md>' + _md_body + r')?'
)
for m in ganzhi_dynasty.finditer(simplified_text):
_process_ganzhi_match(m)
@@ -642,8 +669,8 @@ def extract_era_years(text):
ganzhi_no_dynasty = re.compile(
r'(?P<dynasty>)'
+ r'(?P<era>' + _long_era_alt + r')'
+ _gz_mod
+ r'(?P<ganzhi>' + _gz_pair + r')(?:年)?'
+ r'(?P<md>' + _md_body + r')?'
)
for m in ganzhi_no_dynasty.finditer(simplified_text):
_process_ganzhi_match(m)
@@ -697,7 +724,7 @@ def extract_era_years(text):
"""处理一个年号正则匹配结果"""
raw_dynasty = m.group("dynasty") if "dynasty" in m.groupdict() else ""
dynasty = normalize_dynasty_name(raw_dynasty or "")
era = cc.convert(m.group("era"))
era = _norm_era(m.group("era"))
# 过滤误匹配:年号是单个数字字符(如"民国二十六年"中的"二"
if len(era) == 1 and era in '零一二三四五六七八九十廿卄卅卌':
@@ -738,13 +765,21 @@ def extract_era_years(text):
break
start, end = m.start(), m.end()
# 尾随干支校验(如"光绪二十六年庚子"):并入原文同一单位(同公元段逻辑)
ganzhi = None
if base:
ganzhi = simplified_text[end:end + 2]
if ganzhi in _ganzhi_to_offset:
end += 2
else:
ganzhi = None
raw_fan = original_text[start:end]
covered.update(range(start, end))
if base:
gregorian = base + year_num - 1
result = {
"原文": raw_fan,
"原文(简体)": m.group(0),
"原文(简体)": m.group(0) + (ganzhi or ''),
"朝代": dynasty,
"年号": era,
"年数": year_num,
@@ -753,6 +788,10 @@ def extract_era_years(text):
"位置": {"起始": start, "结束": end},
"类型": "古代纪年",
}
if ganzhi:
result["干支"] = ganzhi
if gregorian_to_ganzhi(gregorian) != ganzhi:
result["干支不符"] = True
if len(multi) > 1:
result["多候选"] = True
result["候选"] = [list(c) for c in multi]