2026-07-23 17:14:26 +08:00
|
|
|
|
import re
|
|
|
|
|
|
|
|
|
|
|
|
import pandas as pd
|
|
|
|
|
|
from opencc import OpenCC
|
|
|
|
|
|
|
2026-08-14 17:06:10 +08:00
|
|
|
|
from era_data import dynasty_alias, KNOWN_ERAS, VARIANT_MAP, TEMPLE_ALIASES, ERA_ALIASES
|
2026-07-23 17:14:26 +08:00
|
|
|
|
|
|
|
|
|
|
cc = OpenCC('t2s')
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-07 16:12:59 +08:00
|
|
|
|
def normalize_chars(text):
|
|
|
|
|
|
"""OpenCC 之外的异体字/俗字归一化。文本与 CSV 两侧必须走同一归一化,保证一致。"""
|
|
|
|
|
|
for variant, canonical in VARIANT_MAP.items():
|
|
|
|
|
|
text = text.replace(variant, canonical)
|
|
|
|
|
|
return text
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-23 17:14:26 +08:00
|
|
|
|
# === 中文数字转阿拉伯数字 ===
|
|
|
|
|
|
def chinese_to_num(text):
|
2026-08-07 16:12:59 +08:00
|
|
|
|
text = text.replace("卅", "三十").replace("卌", "四十").replace("拾", "十")
|
2026-07-23 17:14:26 +08:00
|
|
|
|
if text in ["元", "元年"]:
|
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
|
|
chinese_numerals = {
|
|
|
|
|
|
'零': 0, '〇': 0, '○': 0,
|
|
|
|
|
|
'一': 1, '二': 2, '三': 3, '四': 4, '五': 5, '六': 6, '七': 7, '八': 8, '九': 9,
|
|
|
|
|
|
'十': 10, '百': 100, '千': 1000,
|
2026-08-07 16:12:59 +08:00
|
|
|
|
'壹': 1, '貳': 2, '贰': 2, '叁': 3, '肆': 4, '伍': 5, '陸': 6, '陆': 6, '柒': 7, '捌': 8, '玖': 9, '拾': 10,
|
2026-07-23 17:14:26 +08:00
|
|
|
|
'卄': 20, '廿': 20, '卅': 30, '卌': 40,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if text in chinese_numerals:
|
|
|
|
|
|
return chinese_numerals[text]
|
|
|
|
|
|
|
2026-08-07 16:12:59 +08:00
|
|
|
|
if '千' in text:
|
|
|
|
|
|
parts = text.split('千')
|
|
|
|
|
|
total = chinese_to_num(parts[0]) * 1000 if parts[0] else 1000
|
|
|
|
|
|
if len(parts) > 1 and parts[1]:
|
|
|
|
|
|
total += chinese_to_num(parts[1])
|
|
|
|
|
|
return total
|
|
|
|
|
|
|
|
|
|
|
|
if '百' in text:
|
|
|
|
|
|
parts = text.split('百')
|
|
|
|
|
|
total = chinese_to_num(parts[0]) * 100 if parts[0] else 100
|
|
|
|
|
|
if len(parts) > 1 and parts[1]:
|
|
|
|
|
|
total += chinese_to_num(parts[1])
|
|
|
|
|
|
return total
|
|
|
|
|
|
|
2026-07-23 17:14:26 +08:00
|
|
|
|
if '十' in text:
|
|
|
|
|
|
parts = text.split('十')
|
|
|
|
|
|
total = 0
|
|
|
|
|
|
if parts[0] == '':
|
|
|
|
|
|
total += 10
|
|
|
|
|
|
else:
|
2026-08-07 16:12:59 +08:00
|
|
|
|
total += chinese_to_num(parts[0]) * 10
|
2026-07-23 17:14:26 +08:00
|
|
|
|
if len(parts) > 1 and parts[1]:
|
2026-08-07 16:12:59 +08:00
|
|
|
|
total += chinese_to_num(parts[1])
|
2026-07-23 17:14:26 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# === 年号表 ===
|
2026-08-07 16:12:59 +08:00
|
|
|
|
def _cc(s):
|
|
|
|
|
|
"""CSV 单元格归一化:空值→'',其余走 OpenCC 繁转简 + 异体字归一"""
|
|
|
|
|
|
if pd.isna(s):
|
|
|
|
|
|
return ''
|
|
|
|
|
|
return normalize_chars(cc.convert(str(s)))
|
|
|
|
|
|
|
2026-07-23 17:14:26 +08:00
|
|
|
|
era_df = pd.read_csv("中国年号.csv")
|
2026-08-07 16:12:59 +08:00
|
|
|
|
era_df["年号"] = era_df["年号"].map(_cc)
|
|
|
|
|
|
era_df["所属朝代"] = era_df["所属朝代"].map(_cc)
|
|
|
|
|
|
era_df["名号"] = era_df["名号"].map(_cc)
|
|
|
|
|
|
|
|
|
|
|
|
def _safe_span(v):
|
|
|
|
|
|
"""CSV 使用年数列(可能为空或非数字)"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
return int(float(v))
|
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
|
return None
|
2026-07-23 17:14:26 +08:00
|
|
|
|
|
|
|
|
|
|
era_dict = {}
|
2026-08-07 16:12:59 +08:00
|
|
|
|
# 年号使用年数:用于干支/年数结果的使用期校验(如庆元仅6年,期内无庚子)
|
|
|
|
|
|
era_span = {}
|
2026-07-23 17:14:26 +08:00
|
|
|
|
for _, row in era_df.iterrows():
|
|
|
|
|
|
dynasty, era, start = row["所属朝代"], row["年号"], int(row["公元纪年"])
|
2026-08-07 16:12:59 +08:00
|
|
|
|
if not era:
|
|
|
|
|
|
continue
|
2026-07-23 17:14:26 +08:00
|
|
|
|
era_dict.setdefault(dynasty, {})[era] = start
|
2026-08-07 16:12:59 +08:00
|
|
|
|
span = _safe_span(row["使用年数"])
|
|
|
|
|
|
if span is not None:
|
|
|
|
|
|
era_span.setdefault(dynasty, {}).setdefault(era, span)
|
|
|
|
|
|
|
2026-08-14 17:06:10 +08:00
|
|
|
|
# 有效年号白名单 = 手工精选 ∪ CSV 年号 ∪ 年号别名键(自动补全:白名单不应小于数据表)。
|
2026-08-07 16:12:59 +08:00
|
|
|
|
# 排除:民国(有专门匹配段)、带括号的年号(如"(后元)",防止与东汉"中元"等歧义)
|
|
|
|
|
|
_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
|
|
|
|
|
|
}
|
2026-08-14 17:06:10 +08:00
|
|
|
|
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)
|
2026-08-07 16:12:59 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# === 庙号表 ===
|
|
|
|
|
|
# 名号列(如"高宗""太宗")表示整个在位期,公元取该名号下最早的年份(即位首年)。
|
|
|
|
|
|
# "代宗(景帝)" 等带括号的写法拆成两种叫法都收录;只收祖/宗/帝/王/后/皇结尾(或帝开头)的名号,
|
|
|
|
|
|
# 排除"拖雷""孺子婴"等非庙号条目。
|
|
|
|
|
|
def _is_temple_name(name):
|
|
|
|
|
|
return len(name) >= 2 and (name.startswith('帝') or name.endswith(('祖', '宗', '帝', '王', '后', '皇')))
|
|
|
|
|
|
|
|
|
|
|
|
temple_dict = {}
|
|
|
|
|
|
# 庙号在位年数(该名号下各年号使用年数之和):用于名号+年数结果的使用期校验
|
|
|
|
|
|
temple_span = {}
|
|
|
|
|
|
for _, row in era_df.iterrows():
|
|
|
|
|
|
name, dynasty, start = row["名号"], row["所属朝代"], int(row["公元纪年"])
|
|
|
|
|
|
if not name or not dynasty:
|
|
|
|
|
|
continue
|
|
|
|
|
|
for part in re.split(r'[()()]', name):
|
|
|
|
|
|
part = part.strip()
|
|
|
|
|
|
if not part or not _is_temple_name(part):
|
|
|
|
|
|
continue
|
|
|
|
|
|
part = TEMPLE_ALIASES.get(part, part)
|
|
|
|
|
|
if not _is_temple_name(part):
|
|
|
|
|
|
continue
|
|
|
|
|
|
temple_dict.setdefault(dynasty, {}).setdefault(part, start)
|
|
|
|
|
|
span = _safe_span(row["使用年数"])
|
|
|
|
|
|
if span is not None:
|
|
|
|
|
|
d = temple_span.setdefault(dynasty, {})
|
|
|
|
|
|
d[part] = d.get(part, 0) + span
|
2026-07-23 17:14:26 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# === 干支纪年 ===
|
|
|
|
|
|
_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 + ']'
|
|
|
|
|
|
|
2026-08-14 15:45:07 +08:00
|
|
|
|
# 干支纪年中的年号组(已知年号按长度降序;单字年号加前瞻(?![地支]),
|
|
|
|
|
|
# 防止单字年号"丙"吞掉天干"丙午"中的"丙")
|
|
|
|
|
|
_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')'
|
|
|
|
|
|
|
2026-07-23 17:14:26 +08:00
|
|
|
|
# === 数字转中文 ===
|
|
|
|
|
|
def arabic_to_chinese_year(num: int) -> str:
|
2026-09-20 17:24:56 +08:00
|
|
|
|
"""公元年份转中文(逐位体):1662 → 一六六二,-140 → -一四〇。
|
2026-09-21 13:48:55 +08:00
|
|
|
|
公元年份读作逐位数字,故不适用计数体规则(见 arabic_to_chinese_count)。
|
|
|
|
|
|
返回值不带"年"单位——字段"公元中文"与"年数中文"一样只给数字写法,单位由前端拼接。"""
|
2026-07-23 17:14:26 +08:00
|
|
|
|
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))
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-20 17:24:56 +08:00
|
|
|
|
# === 年数(纪年计数)转中文 ===
|
2026-09-21 12:03:55 +08:00
|
|
|
|
# 规则(纪年计数体,用于"年数中文"字段;只返回数字写法本身,单位"年"由前端自行拼接):
|
2026-09-20 17:24:56 +08:00
|
|
|
|
# 1. 首位 1 → "元"(元年):民国元年、康熙元年、建元元年。
|
|
|
|
|
|
# 2. 10-19 → 十、十一…十九(不写"一十")。
|
|
|
|
|
|
# 3. 20-49 且十位即最高位 → 廿/卅/卌 缩写:20→廿、21→廿一、26→廿六、35→卅五、40→卌、49→卌九。
|
|
|
|
|
|
# 更高位用全称(120→一百二十,不写"一百廿")。
|
|
|
|
|
|
# 4. 50 以上用全称:五十、六十一、九十九、一百、二百五十、一千二百三十四。
|
|
|
|
|
|
# 5. 空位补"〇"(不用"零"):105→一百〇五、1005→一千〇五、1050→一千〇五十;
|
|
|
|
|
|
# 连续空位只补一个〇(10005→一万〇五)。
|
|
|
|
|
|
# 6. 万/亿 分级同普通话:一万、十万、一百万、一千〇五十万。
|
|
|
|
|
|
_CN_UNITS = '〇一二三四五六七八九'
|
|
|
|
|
|
_CN_TENS_ABBR = {2: '廿', 3: '卅', 4: '卌'}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _cn_under_10000(n: int, leading: bool) -> str:
|
|
|
|
|
|
"""1 ≤ n ≤ 9999 的计数体中文;leading=True 表示该段是整个数的最高位段"""
|
|
|
|
|
|
digits = [n // 1000, n % 1000 // 100, n % 100 // 10, n % 10]
|
|
|
|
|
|
names = ['千', '百', '十', '']
|
|
|
|
|
|
out = []
|
|
|
|
|
|
zero_pending = False
|
|
|
|
|
|
for idx, (d, name) in enumerate(zip(digits, names)):
|
|
|
|
|
|
if d == 0:
|
|
|
|
|
|
if out:
|
|
|
|
|
|
zero_pending = True
|
|
|
|
|
|
continue
|
|
|
|
|
|
if zero_pending:
|
|
|
|
|
|
out.append('〇')
|
|
|
|
|
|
zero_pending = False
|
|
|
|
|
|
if name == '十' and leading and idx == 2 and not out and d < 5:
|
|
|
|
|
|
# 最高位就是十位:10-19 → 十…十九(不写"一十");20-49 → 廿/卅/卌 缩写
|
|
|
|
|
|
out.append('十' if d == 1 else _CN_TENS_ABBR[d])
|
|
|
|
|
|
else:
|
|
|
|
|
|
out.append(_CN_UNITS[d] + name)
|
|
|
|
|
|
return ''.join(out)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def arabic_to_chinese_count(num: int) -> str:
|
|
|
|
|
|
"""年数(纪年计数)转中文:1→元、20→廿、26→廿六、105→一百〇五、1050→一千〇五十。
|
|
|
|
|
|
与 arabic_to_chinese_year 的逐位体不同,本函数按计数体读法书写。"""
|
|
|
|
|
|
if num < 0:
|
|
|
|
|
|
return '-' + arabic_to_chinese_count(-num)
|
|
|
|
|
|
if num == 0:
|
|
|
|
|
|
return '〇'
|
|
|
|
|
|
if num == 1:
|
|
|
|
|
|
return '元'
|
|
|
|
|
|
if num < 10000:
|
|
|
|
|
|
return _cn_under_10000(num, leading=True)
|
|
|
|
|
|
if num < 100000000:
|
|
|
|
|
|
high, low = divmod(num, 10000)
|
|
|
|
|
|
out = _cn_under_10000(high, leading=True) + '万'
|
|
|
|
|
|
else:
|
|
|
|
|
|
high, low = divmod(num, 100000000)
|
|
|
|
|
|
out = arabic_to_chinese_count(high) + '亿'
|
|
|
|
|
|
if low:
|
|
|
|
|
|
if low < 1000: # 低位段最高位低于"千"→ 中间有空位,补一个〇
|
|
|
|
|
|
out += '〇'
|
|
|
|
|
|
out += _cn_under_10000(low, leading=False)
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-21 14:30:47 +08:00
|
|
|
|
# === 返回字段全量化 ===
|
|
|
|
|
|
# 每条结果都返回同一套键(顺序固定),无内容的字段给空值:
|
|
|
|
|
|
# 字符串 ""、数值 None、布尔 False、列表 []——前端/后端按对象固定字段接收参数时不会"找不到键"。
|
|
|
|
|
|
_RESULT_FIELDS = (
|
|
|
|
|
|
"原文", "原文(简体)", "类型",
|
|
|
|
|
|
"朝代", "年号", "庙号",
|
|
|
|
|
|
"年数", "年数中文", "干支",
|
|
|
|
|
|
"公元", "公元中文",
|
|
|
|
|
|
"位置",
|
|
|
|
|
|
"干支不符", "干支推算", "超出使用期", "多候选", "候选",
|
|
|
|
|
|
)
|
|
|
|
|
|
_RESULT_DEFAULTS = {
|
|
|
|
|
|
"原文": "", "原文(简体)": "", "类型": "",
|
|
|
|
|
|
"朝代": "", "年号": "", "庙号": "",
|
|
|
|
|
|
"年数": None, "年数中文": "", "干支": "",
|
|
|
|
|
|
"公元": None, "公元中文": "",
|
|
|
|
|
|
"位置": None,
|
|
|
|
|
|
"干支不符": False, "干支推算": None, "超出使用期": False, "多候选": False,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _finalize_result(result: dict) -> dict:
|
|
|
|
|
|
"""按 _RESULT_FIELDS 固定顺序输出全字段(缺的键补空值;列表字段各自独立拷贝)"""
|
2026-09-20 17:24:56 +08:00
|
|
|
|
out = {}
|
2026-09-21 14:30:47 +08:00
|
|
|
|
for key in _RESULT_FIELDS:
|
|
|
|
|
|
if key == "候选":
|
|
|
|
|
|
out[key] = list(result.get("候选") or [])
|
|
|
|
|
|
else:
|
|
|
|
|
|
out[key] = result.get(key, _RESULT_DEFAULTS[key])
|
2026-09-20 17:24:56 +08:00
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-23 17:14:26 +08:00
|
|
|
|
# === 古代年号正则 ===
|
|
|
|
|
|
# 从 era_dict 构建朝代匹配组(按长度降序排列,优先匹配长朝代名如"北宋"而非"北")
|
|
|
|
|
|
# 额外添加常见但不在CSV中的朝代简称(如"汉")
|
2026-08-07 16:12:59 +08:00
|
|
|
|
# 合并 temple_dict 的键:周/秦等年号列全空的朝代也必须有匹配能力
|
2026-07-23 17:14:26 +08:00
|
|
|
|
_extra_dynasty_names = ['汉', '晋', '宋', '周']
|
|
|
|
|
|
_dynasty_names = sorted(
|
2026-08-07 16:12:59 +08:00
|
|
|
|
list(era_dict.keys()) + list(temple_dict.keys())
|
|
|
|
|
|
+ [d for d in _extra_dynasty_names if d not in era_dict and d not in temple_dict],
|
2026-07-23 17:14:26 +08:00
|
|
|
|
key=len, reverse=True
|
|
|
|
|
|
)
|
|
|
|
|
|
_dynasty_alt = '|'.join(re.escape(d) for d in _dynasty_names)
|
|
|
|
|
|
# 常见朝代前缀(大、皇等),用于匹配"大唐""大清"等
|
|
|
|
|
|
_dynasty_prefix = '大皇前后胡'
|
2026-08-07 16:12:59 +08:00
|
|
|
|
# 非元年数字字符(排除元,避免歧义:建元元年中第一个元是年号的一部分)。
|
|
|
|
|
|
# 含阿拉伯/全角数字("康熙20年""乾隆20年")与大写数字("道光贰拾年")。
|
|
|
|
|
|
_year_digits = '〇零一二三四五六七八九十廿卄卅卌' + '01234567890123456789' + '壹貳贰叁肆伍陸陆柒捌玖拾'
|
2026-07-23 17:14:26 +08:00
|
|
|
|
|
|
|
|
|
|
# 已知年号白名单见 era_data.py
|
|
|
|
|
|
|
|
|
|
|
|
# 预编译已知年号正则片段(按长度降序,优先匹配长年号如"太平兴国"而非"太平")
|
2026-08-07 16:12:59 +08:00
|
|
|
|
_known_era_alt = '|'.join(sorted(KNOWN_ERAS_ALL, key=len, reverse=True))
|
2026-07-23 17:14:26 +08:00
|
|
|
|
_known_era_group = r'(?P<era>' + _known_era_alt + r')'
|
|
|
|
|
|
_yd = '[' + _year_digits + ']'
|
2026-09-21 14:44:31 +08:00
|
|
|
|
# 年份后缀:"年"字可省(碑谱/契约常见:"民国二十六""万历三十六",也支持后接干支
|
2026-09-21 14:57:31 +08:00
|
|
|
|
# "光绪二十六庚子")。省"年"时用**白名单前瞻**兜住误匹配——数字后只有跟下列内容才算年份:
|
|
|
|
|
|
# ① 族谱常用动词:生卒殁殇死终葬娶适嫁亡逝享寿嗣立
|
|
|
|
|
|
# ② 标点/括号/引号/空白/行尾:,。、;:,.;:!?!?""''()()[]【】{}《》<> 及 \s、$
|
|
|
|
|
|
# ③ 天干(甲乙丙丁戊己庚辛壬癸):后面就是干支,由各段自行并入并校验
|
|
|
|
|
|
# 于是这些都不再当年份:康熙六月(月)、万历三大征(大)、康熙四十二卷(卷/42卷)、
|
|
|
|
|
|
# 张景福三子(子/排行)、永明五十二都(都/地名)、万历二十万(万)、光绪二十六日(日)、
|
|
|
|
|
|
# 康熙二十天(天/20天)。白名单也天然挡住正则回溯:回退到更短数字串时其后仍是数字,不在表内。
|
|
|
|
|
|
_bare_year_after = (
|
|
|
|
|
|
'生卒殁殤殇死终終葬娶适適嫁亡逝享寿壽嗣立'
|
|
|
|
|
|
+ ',。、;:,.;:!?!?“”"\'‘’()()[]【】{}《》<>'
|
|
|
|
|
|
+ _tiangan
|
|
|
|
|
|
)
|
|
|
|
|
|
_year_suffix = r'(?:年|(?=[' + re.escape(_bare_year_after) + r'\s]|$))'
|
2026-07-23 17:14:26 +08:00
|
|
|
|
|
2026-09-23 17:14:13 +08:00
|
|
|
|
|
|
|
|
|
|
# === 首字符预筛 ===
|
|
|
|
|
|
# 大交替串((?:A|B|...) 里塞了几百个年号/朝代名)在 CPython 的 re 里没有首字符预筛:
|
|
|
|
|
|
# 引擎在每个位置都要把全部分支逐个试完才移向下一位。实测 1MB 文本上这类扫描占
|
|
|
|
|
|
# convert_text 总耗时的 82%,而其中多数模式只命中 0~134 处。
|
|
|
|
|
|
#
|
|
|
|
|
|
# 做法:在模式开头加一个**零宽前瞻** (?=[首字集合])。前瞻不消耗字符,只在当前位置断言
|
|
|
|
|
|
# "下一个字符属于该集合",断言失败就放弃这个起点。
|
|
|
|
|
|
#
|
|
|
|
|
|
# 结果为什么不变(这是结构性保证,不是经验之谈):集合从与模式**同一份源数据**推导,
|
|
|
|
|
|
# 覆盖了模式第一个被消耗字符的所有来源,因此只可能拒绝「原模式根本不可能开始匹配」的
|
|
|
|
|
|
# 位置 → 匹配集合完全相同 → 捕获组相同 → extract_era_years 后续执行序列一致 → 输出
|
|
|
|
|
|
# 逐字段一致。集合随 CSV/白名单变化自动同步(都从 _dynasty_names / KNOWN_ERAS_ALL 推导)。
|
|
|
|
|
|
#
|
|
|
|
|
|
# 适用前提(新增模式时必须逐条核对):
|
|
|
|
|
|
# ① 模式至少消耗 1 个字符、不能匹配空串——否则前瞻会挡掉零宽匹配;
|
|
|
|
|
|
# ② 第一个被消耗的字符只可能来自朝代名或年号名。
|
|
|
|
|
|
# 故以下模式**不加**预筛:
|
|
|
|
|
|
# - 公元段 (?:公元|西元|公历)?...:前缀整个可选,不满足前提①;
|
|
|
|
|
|
# - 裸干支段:以干支对开头,不满足前提②;
|
|
|
|
|
|
# - 民国段:以字面量 民 开头,re 自带前缀优化,加了也无收益。
|
|
|
|
|
|
# 三种预筛对应"朝代组"的三种写法:
|
|
|
|
|
|
# _GATE_DYNASTY 朝代组必选((?P<dynasty>(?:...|...)) 后无 `?`)
|
|
|
|
|
|
# _GATE_DYNASTY_OPT 朝代组可选(用 _dynasty_opt,见 2b/2c 段)
|
|
|
|
|
|
# _GATE_ERA 朝代组为空((?P<dynasty>) 后直接贴年号)
|
|
|
|
|
|
def _first_char_gate(names):
|
|
|
|
|
|
"""从名称集合推导首字符预筛前瞻。字符类里 ]、反斜杠、^、- 四个字符需转义。"""
|
|
|
|
|
|
chars = {n[0] for n in names if n}
|
|
|
|
|
|
body = ''.join('\\' + c if c in ']\\^-' else c for c in sorted(chars))
|
|
|
|
|
|
return '(?=[' + body + '])'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_dynasty_first = list(_dynasty_names) + list(_dynasty_prefix)
|
|
|
|
|
|
_GATE_DYNASTY = _first_char_gate(_dynasty_first)
|
|
|
|
|
|
_GATE_ERA = _first_char_gate(KNOWN_ERAS_ALL)
|
|
|
|
|
|
_GATE_DYNASTY_OPT = _first_char_gate(_dynasty_first + list(KNOWN_ERAS_ALL))
|
|
|
|
|
|
|
|
|
|
|
|
# 差分测试用:登记 (预筛, 未加预筛的模式串)。test_parser.py 逐个比对两者在语料上的
|
|
|
|
|
|
# 匹配序列(含捕获组),防止预筛与模式脱节——例如将来改了模式首部却忘了改预筛。
|
|
|
|
|
|
_GATED_PATTERNS = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _compile_gated(pattern, gate):
|
|
|
|
|
|
"""编译加过首字符预筛的模式,并登记 (预筛, 原串) 供差分测试比对。"""
|
|
|
|
|
|
compiled = re.compile(gate + pattern)
|
|
|
|
|
|
_GATED_PATTERNS.append((compiled, pattern, gate))
|
|
|
|
|
|
return compiled
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-23 17:14:26 +08:00
|
|
|
|
def _build_pattern(with_dynasty=True):
|
|
|
|
|
|
"""构建年号正则:已知年号优先 + 通用兜底(仅限有朝代前缀时)"""
|
|
|
|
|
|
if with_dynasty:
|
|
|
|
|
|
# 朝代前缀模式:加(?<![一-鿿])防止"元"在"开元"中被当朝代
|
|
|
|
|
|
dynasty_part = r'(?P<dynasty>(?:(?<![一-鿿])[' + _dynasty_prefix + r'](?:' + _dynasty_alt + r'))|(?:(?<![一-鿿])' + _dynasty_alt + r'))'
|
|
|
|
|
|
boundary = ''
|
2026-09-23 17:14:13 +08:00
|
|
|
|
gate = _GATE_DYNASTY # 朝代组必选 → 首个消耗字符只能是朝代名首字
|
2026-07-23 17:14:26 +08:00
|
|
|
|
else:
|
|
|
|
|
|
dynasty_part = r'(?P<dynasty>)'
|
|
|
|
|
|
# 无朝代模式不加边界(允许年号跟在其他汉字后面,如"永乐大帝在永乐三年")
|
|
|
|
|
|
boundary = ''
|
2026-09-23 17:14:13 +08:00
|
|
|
|
gate = _GATE_ERA # 朝代组为空 → 首个消耗字符是年号首字
|
2026-07-23 17:14:26 +08:00
|
|
|
|
|
|
|
|
|
|
if with_dynasty:
|
|
|
|
|
|
# 有朝代前缀:年号后直接跟年份数字,无需额外约束
|
2026-09-21 14:37:14 +08:00
|
|
|
|
era_year = _known_era_group + r'(?P<year>' + _yd + r'+)' + _year_suffix
|
2026-07-23 17:14:26 +08:00
|
|
|
|
era_yuan = _known_era_group + r'元年'
|
|
|
|
|
|
patterns = [
|
2026-09-23 17:14:13 +08:00
|
|
|
|
_compile_gated(dynasty_part + boundary + era_year, gate),
|
|
|
|
|
|
_compile_gated(dynasty_part + boundary + era_yuan, gate),
|
2026-07-23 17:14:26 +08:00
|
|
|
|
# 通用兜底(2-8字)
|
2026-09-23 17:14:13 +08:00
|
|
|
|
_compile_gated(dynasty_part + boundary + r'(?P<era>[一-鿿]{2,8})' + r'(?P<year>' + _yd + r'+)年', gate),
|
|
|
|
|
|
_compile_gated(dynasty_part + boundary + r'(?P<era>[一-鿿]{2,8})元年', gate),
|
2026-07-23 17:14:26 +08:00
|
|
|
|
]
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 无朝代前缀:年号后必须紧跟年份数字(前瞻断言),防止"永乐大帝"误匹配
|
2026-09-21 14:37:14 +08:00
|
|
|
|
_year_ahead = r'(?=' + _yd + r'+' + _year_suffix + r'|元年)'
|
|
|
|
|
|
era_year = _known_era_group + _year_ahead + r'(?P<year>' + _yd + r'+)' + _year_suffix
|
2026-07-23 17:14:26 +08:00
|
|
|
|
era_yuan = _known_era_group + _year_ahead + r'元年'
|
|
|
|
|
|
patterns = [
|
2026-09-23 17:14:13 +08:00
|
|
|
|
_compile_gated(dynasty_part + boundary + era_year, gate),
|
|
|
|
|
|
_compile_gated(dynasty_part + boundary + era_yuan, gate),
|
2026-07-23 17:14:26 +08:00
|
|
|
|
]
|
|
|
|
|
|
return patterns
|
|
|
|
|
|
|
|
|
|
|
|
# 无朝代模式只匹配已知年号(避免误匹配如"国"被当年号)
|
|
|
|
|
|
_patterns_no_dynasty = _build_pattern(with_dynasty=False)
|
|
|
|
|
|
# 有朝代模式:已知年号 + 通用兜底
|
|
|
|
|
|
_patterns_dynasty = _build_pattern(with_dynasty=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 15:45:07 +08:00
|
|
|
|
# === 2c. 干支+(公元对照)(如"清同治壬申(1872)十一月十四生") ===
|
2026-08-14 17:06:10 +08:00
|
|
|
|
# 干支纪年旁直接给出公元年份:合并为一条结果、对照校验干支。
|
|
|
|
|
|
# 除括号外还支持:"公元"标记(清同治壬申公元1872年)、横线(壬申-1872)、
|
|
|
|
|
|
# 无分隔连写(壬申1872年)。其余内容(如"十一月十四")不并入原文。
|
|
|
|
|
|
# 置于公元段之前(见 extract_era_years),避免对照年份被单独摘出成公元纪年。
|
2026-08-14 15:45:07 +08:00
|
|
|
|
_dynasty_opt = (r'(?P<dynasty>(?:(?<![一-鿿])[' + _dynasty_prefix + r'](?:' + _dynasty_alt + r'))'
|
|
|
|
|
|
r'|(?:(?<![一-鿿])' + _dynasty_alt + r'))?')
|
2026-09-21 14:57:31 +08:00
|
|
|
|
# 公元对照年份:阿拉伯/全角/中文数字 3-4 位,可带"年"字。
|
|
|
|
|
|
# 两端加数字边界:防止从更长数字串里截出 3-4 位当公元(数据里的标注 ID "{SPZ146356}"
|
|
|
|
|
|
# 会截出 1463、电话 13800138000 会截出 1380、编号 1987654 会截出 1987)。
|
|
|
|
|
|
_gy_edge = '0-90-9〇零一二三四五六七八九'
|
|
|
|
|
|
_gz_gy_year = (r'(?<![' + _gy_edge + r'])' + r'[〇零○O一二三四五六七八九\d0-9]{3,4}'
|
|
|
|
|
|
+ r'(?![' + _gy_edge + r'])')
|
2026-08-14 17:06:10 +08:00
|
|
|
|
# 干支与对照年份之间的连接方式(按优先级):
|
|
|
|
|
|
# 括号(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'(?:岁[次在])?'
|
2026-09-23 17:14:13 +08:00
|
|
|
|
_ganzhi_gongyuan_re = _compile_gated(
|
2026-08-14 15:45:07 +08:00
|
|
|
|
_dynasty_opt
|
|
|
|
|
|
+ _gz_era_group
|
2026-08-14 17:06:10 +08:00
|
|
|
|
+ _gz_mod
|
2026-08-14 15:45:07 +08:00
|
|
|
|
+ r'(?P<ganzhi>' + _gz_pair + r')(?:年)?'
|
2026-09-23 17:14:13 +08:00
|
|
|
|
+ _gz_gy_sep,
|
|
|
|
|
|
_GATE_DYNASTY_OPT, # _dynasty_opt 可空 → 朝代首字与年号首字并集
|
2026-08-14 15:45:07 +08:00
|
|
|
|
)
|
|
|
|
|
|
# 裸干支+(对照):无年号可锚定(如"壬申(1872)"),公元取对照值
|
|
|
|
|
|
_ganzhi_bare_gongyuan_re = re.compile(
|
|
|
|
|
|
r'(?P<ganzhi>' + _gz_pair + r')(?:年)?'
|
2026-08-14 17:06:10 +08:00
|
|
|
|
+ _gz_gy_sep
|
2026-08-14 15:45:07 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-09-23 17:14:13 +08:00
|
|
|
|
# === 2b / 3 / 3b 段的正则 ===
|
|
|
|
|
|
# 这些原先在 extract_era_years 里每次调用都重新编译、且无法被差分测试取到;
|
|
|
|
|
|
# 提到模块级后只编译一次,并统一登记进 _GATED_PATTERNS。
|
|
|
|
|
|
# 2b. 年号+公元(如"清康熙公元一六八七年丁卯"):年号后直接给出公元年份,
|
|
|
|
|
|
# 年数 = 公元 - 年号起始 + 1,紧跟的干支用于校验年份一致性。
|
|
|
|
|
|
_era_gongyuan_re = _compile_gated(
|
|
|
|
|
|
r'(?P<dynasty>(?:(?<![一-鿿])[' + _dynasty_prefix + r'](?:' + _dynasty_alt + r'))|(?:(?<![一-鿿])' + _dynasty_alt + r'))?'
|
|
|
|
|
|
+ r'(?P<era>' + _known_era_alt + r')'
|
|
|
|
|
|
+ r'(?:公元|西元|公历)\s*(?P<gy_year>' + _gz_gy_year + r')\s*年?'
|
|
|
|
|
|
+ r'(?P<gy_ganzhi>' + _gz_pair + r')?',
|
|
|
|
|
|
_GATE_DYNASTY_OPT, # 朝代前缀可空 → 并集
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 3-A. 有朝代前缀的干支纪年(如"清雍正乙巳");年号组 _gz_era_group 见模块级定义
|
|
|
|
|
|
_ganzhi_dynasty_re = _compile_gated(
|
|
|
|
|
|
r'(?P<dynasty>(?:(?<![一-鿿])[' + _dynasty_prefix + r'](?:' + _dynasty_alt + r'))|(?:(?<![一-鿿])' + _dynasty_alt + r'))'
|
|
|
|
|
|
+ _gz_era_group
|
|
|
|
|
|
+ _gz_mod
|
|
|
|
|
|
+ r'(?P<ganzhi>' + _gz_pair + r')(?:年)?',
|
|
|
|
|
|
_GATE_DYNASTY, # 朝代组必选 → 朝代名首字
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 3-B. 无朝代前缀的干支纪年(如"嘉庆癸亥"),年号≥2字符避免天干误匹配
|
|
|
|
|
|
_long_era_names = [e for e in KNOWN_ERAS_ALL if len(e) >= 2]
|
|
|
|
|
|
_long_era_alt = '|'.join(sorted(_long_era_names, key=len, reverse=True))
|
|
|
|
|
|
_ganzhi_no_dynasty_re = _compile_gated(
|
|
|
|
|
|
r'(?P<dynasty>)'
|
|
|
|
|
|
+ r'(?P<era>' + _long_era_alt + r')'
|
|
|
|
|
|
+ _gz_mod
|
|
|
|
|
|
+ r'(?P<ganzhi>' + _gz_pair + r')(?:年)?',
|
|
|
|
|
|
_GATE_ERA, # 朝代组为空 → 年号首字
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 3b. 庙号纪年(如"宋高宗""清圣祖",指整个在位期,公元=即位首年)
|
|
|
|
|
|
# 庙号必须带朝代前缀("高宗"在唐/宋/清均有,单独出现有歧义)
|
|
|
|
|
|
_temple_names = sorted({t for d in temple_dict for t in temple_dict[d]}, key=len, reverse=True)
|
|
|
|
|
|
_temple_alt = '|'.join(re.escape(t) for t in _temple_names)
|
|
|
|
|
|
_temple_pattern_re = _compile_gated(
|
|
|
|
|
|
r'(?P<dynasty>(?:(?<![一-鿿])[' + _dynasty_prefix + r'](?:' + _dynasty_alt + r'))|(?:(?<![一-鿿])' + _dynasty_alt + r'))'
|
|
|
|
|
|
+ r'(?P<temple>' + _temple_alt + r')'
|
|
|
|
|
|
# 后面紧跟"X年/元年"时不按庙号匹配,交给古代纪年按年号解析(如"元泰定三年")
|
|
|
|
|
|
+ r'(?!(?:' + _yd + r'+年|元年))',
|
|
|
|
|
|
_GATE_DYNASTY, # 朝代组必选 → 朝代名首字
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-08-14 15:45:07 +08:00
|
|
|
|
|
2026-07-23 17:14:26 +08:00
|
|
|
|
# === 主函数 ===
|
|
|
|
|
|
def extract_era_years(text):
|
2026-08-07 18:12:19 +08:00
|
|
|
|
# 标注符号([]、【】)不参与匹配,但保留在文本中——位置偏移以原始文本为准
|
|
|
|
|
|
# (含换行/空格/标注),仅在收尾时把标注符号从"原文"字段剔除
|
2026-07-23 17:14:26 +08:00
|
|
|
|
original_text = text
|
2026-08-07 18:31:56 +08:00
|
|
|
|
# 码点→UTF-16码元 映射:JS 前端字符串索引按 UTF-16 计,补充平面字符
|
|
|
|
|
|
# (如 𫓧 U+2B4E7)在 Python 算 1 个码点、在 JS 算 2 个码元,会导致其后位置漂移。
|
|
|
|
|
|
# 返回的"位置"统一用 UTF-16 码元,保证前端 text.slice(start, end) == 原文。
|
|
|
|
|
|
_utf16_index = []
|
|
|
|
|
|
_utf16_pos = 0
|
|
|
|
|
|
for ch in text:
|
|
|
|
|
|
_utf16_index.append(_utf16_pos)
|
|
|
|
|
|
_utf16_pos += 2 if ord(ch) > 0xFFFF else 1
|
|
|
|
|
|
_utf16_index.append(_utf16_pos)
|
2026-08-07 16:12:59 +08:00
|
|
|
|
simplified_text = normalize_chars(cc.convert(text))
|
2026-07-23 17:14:26 +08:00
|
|
|
|
results = []
|
2026-08-07 16:12:59 +08:00
|
|
|
|
# 覆盖集:先匹配的段落占据位置,后续段落跳过重叠(防止同一文本被多段重复摘出)
|
|
|
|
|
|
covered = set()
|
2026-07-23 17:14:26 +08:00
|
|
|
|
|
2026-08-14 17:06:10 +08:00
|
|
|
|
# 民国纪年的公元对照仅支持括号形式("民国三十八年(1949)"):
|
|
|
|
|
|
# 民国X年已是确定年份,逗号/横线/连写分隔多为并列句(如"民国三十八年,公元二〇二四年"是两个日期),不合并
|
|
|
|
|
|
_mg_gy_paren = r'(?:[((][ \t]*(?P<gy_year>' + _gz_gy_year + r')[ \t]*年?[))])?'
|
|
|
|
|
|
|
2026-07-23 17:14:26 +08:00
|
|
|
|
# === 1. 民国 ===
|
2026-08-14 17:06:10 +08:00
|
|
|
|
minguo_pattern = re.compile(
|
2026-09-21 14:37:14 +08:00
|
|
|
|
r'民[国國]\s*([〇零一二三四五六七八九十百廿卄卅卌元\d0-9壹貳贰叁肆伍陸陆柒捌玖拾]+)\s*'
|
|
|
|
|
|
+ _year_suffix
|
2026-08-14 17:06:10 +08:00
|
|
|
|
+ _mg_gy_paren
|
|
|
|
|
|
)
|
2026-07-23 17:14:26 +08:00
|
|
|
|
for m in minguo_pattern.finditer(simplified_text):
|
|
|
|
|
|
year_text = m.group(1)
|
|
|
|
|
|
year_num = chinese_to_num(year_text)
|
2026-08-14 17:06:10 +08:00
|
|
|
|
# 公元对照(如"民国三十八年(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
|
2026-09-21 14:57:31 +08:00
|
|
|
|
gregorian = gy_year if gy_year is not None else 1911 + year_num
|
|
|
|
|
|
# 年数上界:不存在"民国2000年"(→3911)、"民国146356"(→148267)这类写法,
|
|
|
|
|
|
# 换算出的公元超出支持窗口(1000–2100)直接不认——与公元段同一窗口约定
|
|
|
|
|
|
if not (1000 <= gregorian <= 2100):
|
|
|
|
|
|
continue
|
2026-07-23 17:14:26 +08:00
|
|
|
|
start, end = m.start(), m.end()
|
2026-08-14 17:06:10 +08:00
|
|
|
|
# 尾随干支校验(如"民国三十八年己丑"):并入原文同一单位(同公元段逻辑)
|
|
|
|
|
|
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
|
2026-08-07 16:12:59 +08:00
|
|
|
|
covered.update(range(start, end))
|
2026-07-23 17:14:26 +08:00
|
|
|
|
raw_fan = original_text[start:end]
|
2026-08-14 17:06:10 +08:00
|
|
|
|
result = {
|
2026-07-23 17:14:26 +08:00
|
|
|
|
"原文": raw_fan,
|
2026-08-14 17:06:10 +08:00
|
|
|
|
"原文(简体)": simplified_text[start:end],
|
2026-07-23 17:14:26 +08:00
|
|
|
|
"朝代": "民国",
|
|
|
|
|
|
"年号": "民国纪年",
|
|
|
|
|
|
"年数": year_num,
|
2026-08-14 17:06:10 +08:00
|
|
|
|
"公元": gregorian,
|
2026-07-23 17:14:26 +08:00
|
|
|
|
"位置": {"起始": start, "结束": end},
|
|
|
|
|
|
"类型": "民国纪年",
|
2026-08-14 17:06:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
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)
|
2026-07-23 17:14:26 +08:00
|
|
|
|
|
2026-08-14 17:06:10 +08:00
|
|
|
|
# 1b. 民国+干支纪年(如"民国丁卯""民国甲戌"),支持(公元对照)合并
|
|
|
|
|
|
minguo_gz = re.compile(
|
|
|
|
|
|
r'民[国國]\s*(?P<ganzhi>' + _gz_pair + r')(?:年)?'
|
|
|
|
|
|
+ _mg_gy_paren
|
|
|
|
|
|
)
|
2026-07-23 17:14:26 +08:00
|
|
|
|
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
|
2026-08-14 17:06:10 +08:00
|
|
|
|
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
|
2026-09-21 14:57:31 +08:00
|
|
|
|
minguo_gy = gy_year if gy_year is not None else computed
|
|
|
|
|
|
if not (1000 <= minguo_gy <= 2100):
|
|
|
|
|
|
continue
|
2026-07-23 17:14:26 +08:00
|
|
|
|
start, end = m.start(), m.end()
|
2026-08-07 16:12:59 +08:00
|
|
|
|
covered.update(range(start, end))
|
2026-07-23 17:14:26 +08:00
|
|
|
|
raw_fan = original_text[start:end]
|
2026-08-14 17:06:10 +08:00
|
|
|
|
result = {
|
2026-07-23 17:14:26 +08:00
|
|
|
|
"原文": raw_fan,
|
|
|
|
|
|
"原文(简体)": m.group(0),
|
|
|
|
|
|
"朝代": "民国",
|
|
|
|
|
|
"年号": "民国纪年",
|
|
|
|
|
|
"年数": year_in_era,
|
|
|
|
|
|
"干支": ganzhi,
|
2026-08-14 17:06:10 +08:00
|
|
|
|
"公元": gy_year if gy_year is not None else computed,
|
2026-07-23 17:14:26 +08:00
|
|
|
|
"位置": {"起始": start, "结束": end},
|
|
|
|
|
|
"类型": "民国纪年(干支)",
|
2026-08-14 17:06:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
if gy_year is not None and gy_year != computed:
|
|
|
|
|
|
result["干支不符"] = True
|
|
|
|
|
|
result["干支推算"] = computed
|
|
|
|
|
|
results.append(result)
|
2026-07-23 17:14:26 +08:00
|
|
|
|
|
2026-08-07 16:12:59 +08:00
|
|
|
|
# === 2b. 年号+公元(如"清康熙公元一六八七年丁卯") ===
|
|
|
|
|
|
# 年号后直接给出公元年份:年数=公元-年号起始+1,与"年号+年数"公式自洽;
|
|
|
|
|
|
# 紧跟的干支用于校验年份一致性。置于公元段之前,合并结果优先、避免重复摘出。
|
2026-09-23 17:14:13 +08:00
|
|
|
|
# 正则见模块级 _era_gongyuan_re(含首字符预筛)
|
|
|
|
|
|
for m in _era_gongyuan_re.finditer(simplified_text):
|
2026-08-07 16:12:59 +08:00
|
|
|
|
start, end = m.start(), m.end()
|
|
|
|
|
|
if any(pos in covered for pos in range(start, end)):
|
|
|
|
|
|
continue
|
2026-08-14 17:06:10 +08:00
|
|
|
|
era = _norm_era(m.group("era"))
|
2026-08-07 16:12:59 +08:00
|
|
|
|
year_num = chinese_to_num(m.group("gy_year"))
|
|
|
|
|
|
if not (1000 <= year_num <= 2100):
|
|
|
|
|
|
continue
|
|
|
|
|
|
dynasty = normalize_dynasty_name(m.group("dynasty") or "")
|
|
|
|
|
|
base = None
|
|
|
|
|
|
multi = []
|
|
|
|
|
|
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 base is None:
|
|
|
|
|
|
for d, eras in era_dict.items():
|
|
|
|
|
|
if era in eras:
|
|
|
|
|
|
multi.append((d, eras[era]))
|
|
|
|
|
|
if multi:
|
|
|
|
|
|
dynasty, base = multi[0]
|
|
|
|
|
|
if base is None:
|
|
|
|
|
|
continue
|
|
|
|
|
|
covered.update(range(start, end))
|
|
|
|
|
|
year_in_era = year_num - base + 1
|
|
|
|
|
|
ganzhi = m.group("gy_ganzhi")
|
|
|
|
|
|
result = {
|
|
|
|
|
|
"原文": original_text[start:end],
|
|
|
|
|
|
"原文(简体)": m.group(0),
|
|
|
|
|
|
"朝代": dynasty,
|
|
|
|
|
|
"年号": era,
|
|
|
|
|
|
"年数": year_in_era,
|
|
|
|
|
|
"公元": year_num,
|
|
|
|
|
|
"位置": {"起始": start, "结束": end},
|
|
|
|
|
|
"类型": "年号+公元",
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(multi) > 1:
|
|
|
|
|
|
result["多候选"] = True
|
|
|
|
|
|
result["候选"] = [list(c) for c in multi]
|
|
|
|
|
|
# 公元超出年号使用期(如"康熙公元1950年")→ 标注,值保留
|
|
|
|
|
|
span = era_span.get(dynasty, {}).get(era)
|
|
|
|
|
|
if span is not None and year_num > base + span - 1:
|
|
|
|
|
|
result["超出使用期"] = True
|
|
|
|
|
|
# 紧跟的干支与公元年份不符(如"一六八九年丁卯",1689应为己巳)→ 标注
|
|
|
|
|
|
if ganzhi:
|
|
|
|
|
|
result["干支"] = ganzhi
|
|
|
|
|
|
if gregorian_to_ganzhi(year_num) != ganzhi:
|
|
|
|
|
|
result["干支不符"] = True
|
|
|
|
|
|
results.append(result)
|
|
|
|
|
|
|
2026-08-14 15:45:07 +08:00
|
|
|
|
# === 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 "")
|
2026-08-14 17:06:10 +08:00
|
|
|
|
era = _norm_era(m.group("era")) if "era" in m.groupdict() and m.group("era") else None
|
2026-08-14 15:45:07 +08:00
|
|
|
|
ganzhi = m.group("ganzhi")
|
2026-08-14 17:06:10 +08:00
|
|
|
|
# 对照年份可能来自四种连接方式之一(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)
|
|
|
|
|
|
)
|
2026-08-14 15:45:07 +08:00
|
|
|
|
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,
|
|
|
|
|
|
"位置": {"起始": 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
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
2026-07-23 17:14:26 +08:00
|
|
|
|
# === 2. 公元(允许无"年"字)===
|
|
|
|
|
|
gongyuan_pattern = re.compile(
|
2026-09-21 14:57:31 +08:00
|
|
|
|
r'(?:公元|西元|公历)?\s*(' + _gz_gy_year + r')\s*(?:(.*?))?\s*(年)?'
|
2026-07-23 17:14:26 +08:00
|
|
|
|
)
|
|
|
|
|
|
for m in gongyuan_pattern.finditer(simplified_text):
|
2026-08-07 16:12:59 +08:00
|
|
|
|
start, end = m.start(), m.end()
|
|
|
|
|
|
if any(pos in covered for pos in range(start, end)):
|
|
|
|
|
|
continue
|
2026-08-14 17:06:10 +08:00
|
|
|
|
year_text = m.group(1)
|
2026-07-23 17:14:26 +08:00
|
|
|
|
year_num = chinese_to_num(year_text)
|
|
|
|
|
|
if not (1000 <= year_num <= 2100):
|
|
|
|
|
|
continue
|
2026-08-07 18:02:57 +08:00
|
|
|
|
# 公元年后紧跟的干支(如"公元一九六〇年庚子"):无年号可锚定时,
|
2026-08-07 18:12:19 +08:00
|
|
|
|
# 干支就是年份的干支,紧邻时并入同一日期单位——保证 位置[start:end]==原文,
|
|
|
|
|
|
# 前端可按范围整体替换;隔标注符号时(【公元一九六〇年】庚子)干支只摘入字段校验
|
2026-08-07 18:02:57 +08:00
|
|
|
|
ganzhi = simplified_text[end:end + 2]
|
|
|
|
|
|
if ganzhi in _ganzhi_to_offset:
|
|
|
|
|
|
end += 2
|
2026-08-07 18:12:19 +08:00
|
|
|
|
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
|
2026-08-07 16:12:59 +08:00
|
|
|
|
covered.update(range(start, end))
|
2026-07-23 17:14:26 +08:00
|
|
|
|
raw_fan = original_text[start:end]
|
|
|
|
|
|
era_type = "公元纪年(中华人民共和国)" if year_num >= 1949 else "公元纪年(近代/其他)"
|
2026-08-07 17:56:01 +08:00
|
|
|
|
result = {
|
2026-07-23 17:14:26 +08:00
|
|
|
|
"原文": raw_fan,
|
2026-08-07 18:02:57 +08:00
|
|
|
|
"原文(简体)": simplified_text[start:end],
|
2026-07-23 17:14:26 +08:00
|
|
|
|
"类型": era_type,
|
|
|
|
|
|
"公元": year_num,
|
|
|
|
|
|
"位置": {"起始": start, "结束": end},
|
2026-08-07 17:56:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
if ganzhi in _ganzhi_to_offset:
|
|
|
|
|
|
result["干支"] = ganzhi
|
|
|
|
|
|
if gregorian_to_ganzhi(year_num) != ganzhi:
|
|
|
|
|
|
result["干支不符"] = True
|
|
|
|
|
|
results.append(result)
|
2026-07-23 17:14:26 +08:00
|
|
|
|
|
|
|
|
|
|
# === 3. 干支纪年(如"清雍正乙巳十一月十二午时生")===
|
|
|
|
|
|
def _process_ganzhi_match(m):
|
|
|
|
|
|
"""处理干支纪年匹配"""
|
|
|
|
|
|
raw_dynasty = m.group("dynasty") if "dynasty" in m.groupdict() else ""
|
|
|
|
|
|
dynasty = normalize_dynasty_name(raw_dynasty or "")
|
2026-08-14 17:06:10 +08:00
|
|
|
|
era = _norm_era(m.group("era"))
|
2026-07-23 17:14:26 +08:00
|
|
|
|
ganzhi = m.group("ganzhi")
|
|
|
|
|
|
if ganzhi not in _ganzhi_to_offset:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 查找朝代和基准年
|
|
|
|
|
|
base = None
|
2026-08-07 16:12:59 +08:00
|
|
|
|
multi = []
|
2026-07-23 17:14:26 +08:00
|
|
|
|
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:
|
2026-08-07 16:12:59 +08:00
|
|
|
|
# 无朝代前缀:收集全部候选(如"大定"=北周581/金1161),取首个但标注歧义
|
2026-07-23 17:14:26 +08:00
|
|
|
|
for d, eras in era_dict.items():
|
|
|
|
|
|
if era in eras:
|
2026-08-07 16:12:59 +08:00
|
|
|
|
multi.append((d, eras[era]))
|
|
|
|
|
|
if multi:
|
|
|
|
|
|
dynasty, base = multi[0]
|
2026-07-23 17:14:26 +08:00
|
|
|
|
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]
|
2026-08-07 16:12:59 +08:00
|
|
|
|
result = {
|
2026-07-23 17:14:26 +08:00
|
|
|
|
"原文": raw_fan,
|
|
|
|
|
|
"原文(简体)": m.group(0),
|
|
|
|
|
|
"朝代": dynasty,
|
|
|
|
|
|
"年号": era,
|
|
|
|
|
|
"年数": year_in_era,
|
|
|
|
|
|
"干支": ganzhi,
|
|
|
|
|
|
"公元": gregorian,
|
|
|
|
|
|
"位置": {"起始": start, "结束": end},
|
|
|
|
|
|
"类型": "干支纪年",
|
2026-08-07 16:12:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
if len(multi) > 1:
|
|
|
|
|
|
result["多候选"] = True
|
|
|
|
|
|
result["候选"] = [list(c) for c in multi]
|
|
|
|
|
|
# 使用期校验:换算结果超出年号实际使用年数(如"庆元庚子"→1240,庆元仅1195–1200)。
|
|
|
|
|
|
# 值保留,仅标注,供人工判断(碑刻原数据常与年号实际期间不符)。
|
|
|
|
|
|
span = era_span.get(dynasty, {}).get(era)
|
|
|
|
|
|
if span is not None and gregorian > base + span - 1:
|
|
|
|
|
|
result["超出使用期"] = True
|
|
|
|
|
|
results.append(result)
|
2026-07-23 17:14:26 +08:00
|
|
|
|
|
2026-09-23 17:14:13 +08:00
|
|
|
|
# 模式A:有朝代前缀(如"清雍正乙巳");正则见模块级 _ganzhi_dynasty_re
|
|
|
|
|
|
for m in _ganzhi_dynasty_re.finditer(simplified_text):
|
2026-07-23 17:14:26 +08:00
|
|
|
|
_process_ganzhi_match(m)
|
|
|
|
|
|
|
2026-09-23 17:14:13 +08:00
|
|
|
|
# 模式B:无朝代前缀(如"嘉庆癸亥");正则见模块级 _ganzhi_no_dynasty_re
|
|
|
|
|
|
for m in _ganzhi_no_dynasty_re.finditer(simplified_text):
|
2026-07-23 17:14:26 +08:00
|
|
|
|
_process_ganzhi_match(m)
|
|
|
|
|
|
|
2026-08-07 16:12:59 +08:00
|
|
|
|
# === 3b. 庙号纪年(如"宋高宗""清圣祖",指整个在位期,公元=即位首年)===
|
|
|
|
|
|
# 庙号必须带朝代前缀("高宗"在唐/宋/清均有,单独出现有歧义)
|
2026-09-23 17:14:13 +08:00
|
|
|
|
# 正则见模块级 _temple_pattern_re
|
|
|
|
|
|
for m in _temple_pattern_re.finditer(simplified_text):
|
2026-08-07 16:12:59 +08:00
|
|
|
|
start, end = m.start(), m.end()
|
|
|
|
|
|
if any(pos in covered for pos in range(start, end)):
|
|
|
|
|
|
continue
|
|
|
|
|
|
dynasty = normalize_dynasty_name(m.group("dynasty") or "")
|
|
|
|
|
|
temple = m.group("temple")
|
|
|
|
|
|
base = None
|
|
|
|
|
|
if dynasty:
|
|
|
|
|
|
# 先精确匹配朝代,再模糊匹配(如"宋"→"南宋")
|
|
|
|
|
|
for d in temple_dict:
|
|
|
|
|
|
if d == dynasty and temple in temple_dict[d]:
|
|
|
|
|
|
dynasty, base = d, temple_dict[d][temple]
|
|
|
|
|
|
break
|
|
|
|
|
|
if base is None:
|
|
|
|
|
|
for d in temple_dict:
|
|
|
|
|
|
if (dynasty in d or d in dynasty) and temple in temple_dict[d]:
|
|
|
|
|
|
dynasty, base = d, temple_dict[d][temple]
|
|
|
|
|
|
break
|
|
|
|
|
|
if base is None:
|
|
|
|
|
|
continue
|
|
|
|
|
|
covered.update(range(start, end))
|
|
|
|
|
|
raw_fan = original_text[start:end]
|
|
|
|
|
|
results.append({
|
|
|
|
|
|
"原文": raw_fan,
|
|
|
|
|
|
"原文(简体)": m.group(0),
|
|
|
|
|
|
"朝代": dynasty,
|
|
|
|
|
|
"庙号": temple,
|
|
|
|
|
|
"公元": base,
|
|
|
|
|
|
"位置": {"起始": start, "结束": end},
|
|
|
|
|
|
"类型": "庙号纪年",
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-07-23 17:14:26 +08:00
|
|
|
|
# === 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 "")
|
2026-08-14 17:06:10 +08:00
|
|
|
|
era = _norm_era(m.group("era"))
|
2026-07-23 17:14:26 +08:00
|
|
|
|
|
|
|
|
|
|
# 过滤误匹配:年号是单个数字字符(如"民国二十六年"中的"二")
|
|
|
|
|
|
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)
|
2026-09-21 14:57:31 +08:00
|
|
|
|
# 年数上界:年号纪年不可能出现"洪武146356"这类(那是被当年份的数字 ID/编号),
|
|
|
|
|
|
# 年数 > 999 一律不认——否则会算出 147723 这种荒诞公元
|
|
|
|
|
|
if not (1 <= year_num <= 999):
|
|
|
|
|
|
return
|
2026-07-23 17:14:26 +08:00
|
|
|
|
base = None
|
2026-08-07 16:12:59 +08:00
|
|
|
|
multi = []
|
2026-07-23 17:14:26 +08:00
|
|
|
|
|
|
|
|
|
|
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:
|
2026-08-07 16:12:59 +08:00
|
|
|
|
# 无朝代前缀:收集全部候选(如"建元"=西汉-140/东晋343/南齐479),取首个但标注歧义
|
2026-07-23 17:14:26 +08:00
|
|
|
|
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]
|
2026-08-07 16:12:59 +08:00
|
|
|
|
multi = candidates
|
|
|
|
|
|
if base is None:
|
|
|
|
|
|
# 名号+年数兜底(如"汉惠帝三年"→即位首年-194 + 3 - 1,指皇帝在位第N年)
|
|
|
|
|
|
if dynasty:
|
|
|
|
|
|
for d, temples in temple_dict.items():
|
|
|
|
|
|
if (dynasty in d or d in dynasty) and era in temples:
|
|
|
|
|
|
base = temples[era]
|
|
|
|
|
|
dynasty = d
|
|
|
|
|
|
break
|
2026-07-23 17:14:26 +08:00
|
|
|
|
|
|
|
|
|
|
start, end = m.start(), m.end()
|
2026-08-14 17:06:10 +08:00
|
|
|
|
# 尾随干支校验(如"光绪二十六年庚子"):并入原文同一单位(同公元段逻辑)
|
|
|
|
|
|
ganzhi = None
|
|
|
|
|
|
if base:
|
|
|
|
|
|
ganzhi = simplified_text[end:end + 2]
|
|
|
|
|
|
if ganzhi in _ganzhi_to_offset:
|
|
|
|
|
|
end += 2
|
|
|
|
|
|
else:
|
|
|
|
|
|
ganzhi = None
|
2026-07-23 17:14:26 +08:00
|
|
|
|
raw_fan = original_text[start:end]
|
|
|
|
|
|
covered.update(range(start, end))
|
|
|
|
|
|
if base:
|
|
|
|
|
|
gregorian = base + year_num - 1
|
2026-08-07 16:12:59 +08:00
|
|
|
|
result = {
|
2026-07-23 17:14:26 +08:00
|
|
|
|
"原文": raw_fan,
|
2026-08-14 17:06:10 +08:00
|
|
|
|
"原文(简体)": m.group(0) + (ganzhi or ''),
|
2026-07-23 17:14:26 +08:00
|
|
|
|
"朝代": dynasty,
|
|
|
|
|
|
"年号": era,
|
|
|
|
|
|
"年数": year_num,
|
|
|
|
|
|
"公元": gregorian,
|
|
|
|
|
|
"位置": {"起始": start, "结束": end},
|
|
|
|
|
|
"类型": "古代纪年",
|
2026-08-07 16:12:59 +08:00
|
|
|
|
}
|
2026-08-14 17:06:10 +08:00
|
|
|
|
if ganzhi:
|
|
|
|
|
|
result["干支"] = ganzhi
|
|
|
|
|
|
if gregorian_to_ganzhi(gregorian) != ganzhi:
|
|
|
|
|
|
result["干支不符"] = True
|
2026-08-07 16:12:59 +08:00
|
|
|
|
if len(multi) > 1:
|
|
|
|
|
|
result["多候选"] = True
|
|
|
|
|
|
result["候选"] = [list(c) for c in multi]
|
|
|
|
|
|
# 使用期校验:年数结果超出年号使用期(或名号在位年数)时标注,值保留
|
|
|
|
|
|
span = era_span.get(dynasty, {}).get(era)
|
|
|
|
|
|
if span is None:
|
|
|
|
|
|
span = temple_span.get(dynasty, {}).get(era)
|
|
|
|
|
|
if span is not None and gregorian > base + span - 1:
|
|
|
|
|
|
result["超出使用期"] = True
|
|
|
|
|
|
results.append(result)
|
2026-07-23 17:14:26 +08:00
|
|
|
|
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["公元"]))
|
|
|
|
|
|
|
2026-09-21 14:30:47 +08:00
|
|
|
|
# === 收尾 ===
|
|
|
|
|
|
# 1) 派生字段:公元中文(公元年份逐位读法)、年数中文(廿/卅/卌 计数读法)——都只给数字写法,不带"年"单位。
|
|
|
|
|
|
# 在这里统一派生,任何新增的产生"公元"/"年数"的分支都会自动带上;
|
|
|
|
|
|
# 公元纪年、庙号纪年没有年数 → 年数中文 = "";未匹配年号没有公元 → 公元中文 = ""。
|
|
|
|
|
|
# 2) 原文剔除标注符号;位置换算为 UTF-16 码元(JS 前端索引)。
|
|
|
|
|
|
# 3) 字段全量化:_finalize_result 按固定顺序补齐所有键(缺内容给空值)。
|
2026-09-20 17:24:56 +08:00
|
|
|
|
for i, result in enumerate(results):
|
2026-08-07 18:12:19 +08:00
|
|
|
|
start, end = result["位置"]["起始"], result["位置"]["结束"]
|
2026-09-21 14:30:47 +08:00
|
|
|
|
result["年数中文"] = (
|
|
|
|
|
|
arabic_to_chinese_count(result["年数"]) if result.get("年数") is not None else ""
|
|
|
|
|
|
)
|
|
|
|
|
|
result["公元中文"] = (
|
|
|
|
|
|
arabic_to_chinese_year(result["公元"]) if result.get("公元") is not None else ""
|
|
|
|
|
|
)
|
2026-08-07 18:31:56 +08:00
|
|
|
|
result["位置"] = {"起始": _utf16_index[start], "结束": _utf16_index[end]}
|
2026-08-07 18:12:19 +08:00
|
|
|
|
result["原文"] = ''.join(ch for ch in original_text[start:end] if ch not in '[]【】')
|
|
|
|
|
|
result["原文(简体)"] = ''.join(ch for ch in simplified_text[start:end] if ch not in '[]【】')
|
2026-09-21 14:30:47 +08:00
|
|
|
|
results[i] = _finalize_result(result)
|
2026-08-07 18:12:19 +08:00
|
|
|
|
|
2026-07-23 17:14:26 +08:00
|
|
|
|
return results
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def convert_text(text):
|
|
|
|
|
|
results = extract_era_years(text)
|
|
|
|
|
|
return results
|