多维度日期处理

This commit is contained in:
sansen
2026-08-14 15:45:07 +08:00
parent 8b7bb37996
commit 86e7ef2971
+147 -6
View File
@@ -178,6 +178,13 @@ def gregorian_to_ganzhi(year: int) -> str:
_gz_pair = '[' + _tiangan + '][' + _dizhi + ']'
# 干支纪年中的年号组(已知年号按长度降序;单字年号加前瞻(?![地支]),
# 防止单字年号"丙"吞掉天干"丙午"中的"丙"
_gz_era_group = r'(?P<era>' + '|'.join(
(re.escape(e) + (r'(?![子丑寅卯辰巳午未申酉戌亥])' if len(e) == 1 else ''))
for e in sorted(KNOWN_ERAS_ALL, key=len, reverse=True)
) + r')'
# === 数字转中文 ===
def arabic_to_chinese_year(num: int) -> str:
digits = "〇一二三四五六七八九"
@@ -249,6 +256,73 @@ _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"被单独摘出成公元纪年。
_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*年?[)]'
_ganzhi_gongyuan_re = re.compile(
_dynasty_opt
+ _gz_era_group
+ 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')?'
)
# 裸干支+(对照):无年号可锚定(如"壬申(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')?'
)
# === 主函数 ===
def extract_era_years(text):
# 标注符号([]、【】)不参与匹配,但保留在文本中——位置偏移以原始文本为准
@@ -378,6 +452,74 @@ def extract_era_years(text):
result["干支不符"] = True
results.append(result)
# === 2c. 干支+(公元对照)(如"清同治壬申(1872)十一月十四生") ===
# 干支纪年旁直接给出公元对照:合并为一条结果,用对照校验干支,紧跟的月日并入字段。
# 置于公元段之前,避免"(1872)"被单独摘出成公元纪年(同 2b 的合并优先策略)。
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
ganzhi = m.group("ganzhi")
gy_year = chinese_to_num(m.group("gy_year"))
if ganzhi not in _ganzhi_to_offset or not (1000 <= gy_year <= 2100):
return
start, end = m.start(), m.end()
if any(pos in covered for pos in range(start, end)):
return
# 查找年号基准年(与干支纪年段同逻辑:先精确朝代,后全量候选取首个并标注歧义)
base = None
multi = []
if dynasty and era:
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 base is None and era:
for d, eras in era_dict.items():
if era in eras:
multi.append((d, eras[era]))
if multi:
dynasty, base = multi[0]
covered.update(range(start, end))
result = {
"原文": original_text[start:end],
"原文(简体)": m.group(0),
"干支": ganzhi,
"公元": gy_year,
"公元中文": f"{arabic_to_chinese_year(gy_year)}",
"位置": {"起始": start, "结束": end},
"类型": "干支纪年",
}
if base is not None:
result["朝代"] = dynasty
result["年号"] = era
# 干支60年循环推算(同干支纪年段公式);与对照不一致时标注,公元仍取对照值
base_gz = (base - 4) % 60
year_in_era = ((_ganzhi_to_offset[ganzhi] - base_gz) % 60) + 1
result["年数"] = year_in_era
computed = base + year_in_era - 1
if computed != gy_year:
result["干支不符"] = True
result["干支推算"] = computed
if len(multi) > 1:
result["多候选"] = True
result["候选"] = [list(c) for c in multi]
span = era_span.get(dynasty, {}).get(era)
if span is not None and gy_year > base + span - 1:
result["超出使用期"] = True
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):
_process_ganzhi_gongyuan(m)
for m in _ganzhi_bare_gongyuan_re.finditer(simplified_text):
_process_ganzhi_gongyuan(m)
# === 2. 公元(允许无"年"字)===
gongyuan_pattern = re.compile(
r'(公元)?\s*([〇零○O一二三四五六七八九\d-]{3,4})\s*(?:.*?)?\s*(年)?'
@@ -480,18 +622,16 @@ 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 = r'(?P<era>' + '|'.join(
(re.escape(e) + (r'(?![子丑寅卯辰巳午未申酉戌亥])' if len(e) == 1 else ''))
for e in sorted(KNOWN_ERAS_ALL, key=len, reverse=True)
) + r')'
# 模式A:有朝代前缀(如"清雍正乙巳");年号组 _gz_era_group 见模块级定义
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')(?:年)?'
# 紧跟的农历月日并入同一单位(如"清雍正乙巳十一月十二午时生")
+ r'(?P<md>' + _md_body + r')?'
)
for m in ganzhi_dynasty.finditer(simplified_text):
_process_ganzhi_match(m)
@@ -503,6 +643,7 @@ def extract_era_years(text):
r'(?P<dynasty>)'
+ r'(?P<era>' + _long_era_alt + r')'
+ 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)