From 86e7ef29716ab1a998bede2aa248c2c31c03ee87 Mon Sep 17 00:00:00 2001 From: sansen Date: Fri, 14 Aug 2026 15:45:07 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=9A=E7=BB=B4=E5=BA=A6=E6=97=A5=E6=9C=9F?= =?UTF-8?q?=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- parser.py | 153 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 147 insertions(+), 6 deletions(-) diff --git a/parser.py b/parser.py index 36e8e5a..b6fe700 100644 --- a/parser.py +++ b/parser.py @@ -178,6 +178,13 @@ def gregorian_to_ganzhi(year: int) -> str: _gz_pair = '[' + _tiangan + '][' + _dizhi + ']' +# 干支纪年中的年号组(已知年号按长度降序;单字年号加前瞻(?![地支]), +# 防止单字年号"丙"吞掉天干"丙午"中的"丙") +_gz_era_group = r'(?P' + '|'.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闰)?(?P[正冬腊〇零一二三四五六七八九十廿卄卅卌]{1,3})月' + r'(?:(?P[初〇零一二三四五六七八九十廿卄卅卌]{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(?:(?[〇零○O一二三四五六七八九\d0-9]{3,4})\s*年?[))]' +_ganzhi_gongyuan_re = re.compile( + _dynasty_opt + + _gz_era_group + + r'(?P' + _gz_pair + r')(?:年)?' + + r'(?P' + _md_body + r')?' + + r'(?:' + _md_verb + r')?' + + _gz_gy_paren + + r'(?P' + _md_body + r')?' +) +# 裸干支+(对照):无年号可锚定(如"壬申(1872)"),公元取对照值 +_ganzhi_bare_gongyuan_re = re.compile( + r'(?P' + _gz_pair + r')(?:年)?' + + r'(?P' + _md_body + r')?' + + r'(?:' + _md_verb + r')?' + + _gz_gy_paren + + r'(?P' + _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一二三四五六七八九\d0-9]{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' + '|'.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(?:(?' + _gz_pair + r')(?:年)?' + # 紧跟的农历月日并入同一单位(如"清雍正乙巳十一月十二午时生") + + r'(?P' + _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)' + r'(?P' + _long_era_alt + r')' + r'(?P' + _gz_pair + r')(?:年)?' + + r'(?P' + _md_body + r')?' ) for m in ganzhi_no_dynasty.finditer(simplified_text): _process_ganzhi_match(m)