Compare commits

5 Commits
Author SHA1 Message Date
sansen 6078beb4c3 Merge remote-tracking branch 'origin/main' 2026-09-23 17:43:36 +08:00
sansenandClaude Code 4e68372518 日志改为摘要 + 有界预览:单次请求日志 6940KB → 2.8KB
/convert/start 原先 logger.info 直接 dump 整份结果(json.dumps(indent=2)):实测
1MB 输入 → 20188 条 → 6.9MB 日志,是原文的 15%,indent 还要再乘 1.4。转几份族谱
日志文件就会被撑爆。

改为:
- 一行摘要:输入字数、结果条数、类型分布、耗时(格式与 batch 接口的日志保持一致)
- 前 LOG_PREVIEW_ITEMS(=10) 条结果预览,体积有硬上界
- 新增 _result_summary() helper

类型分布一行能看出解析形态(如 {'干支纪年': 15624, '古代纪年': 147, ...}),
排查"整批变成未匹配"这类回归比翻全量结果还快。明细请用接口返回值。

验证:单次 1MB 请求日志 2.8KB(降 2428x);/convert/start、/convert/batch、
403 鉴权、空文本四条路径均正常;pytest 260 passed。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-23 17:42:42 +08:00
sansenandClaude Code b264073f2b 大交替串加首字符预筛:convert_text 提速 3.05x,输出零差异
CPython 的 re 不对大交替串 (?:A|B|...) 做首字符预筛——460 个年号的交替串在每个
位置都要把全部分支逐个试完才移向下一位。实测这类扫描占 convert_text 总耗时的 82%
(sample_2.txt 1.03M 字符:扫描 7.05 / 总计 8.54 µs/字符),而其中多数模式只命中
0~134 处。

改法:给每个"以年号或朝代名开头"的模式前面加一个零宽前瞻 (?=[首字集合])。前瞻不
消耗字符,只在当前位置断言下一个字符属于集合。预筛字符集从与模式**同一份源数据**
推导(_dynasty_names / _dynasty_prefix / KNOWN_ERAS_ALL),CSV 加新年号时自动同步。
三种预筛:_GATE_DYNASTY(朝代组必选)/ _GATE_DYNASTY_OPT(_dynasty_opt 可空)/
_GATE_ERA(朝代组为空)。

结果为什么不变(结构性保证,非经验之谈):前瞻只拒绝「原模式根本不可能开始匹配」的
位置 → 匹配集合完全相同 → 捕获组相同 → 后续执行序列一致 → 输出逐字段一致。

不加预筛者(前提不满足):公元段(前缀整个可选)、裸干支段(以干支对开头)、
民国段(以字面量 民 开头,re 自带前缀优化)。

顺带把 2b/3/3b 段的 4 个正则从 extract_era_years 内提到模块级:只编译一次,且差分
测试能取到它们。

验证:
- 全字段快照比对:sample.txt 1766 / sample_2.txt 20188 / test/sample.txt 14 /
  test/sample_crosscheck.txt 11,全部 19 个字段逐条一致,零差异
- pytest 260 passed(新增 TestFirstCharGate 3 条)、summary.py 96/96
- 速度:sample_2.txt 8.53s → 2.80s(3.05x)、sample.txt 0.64s → 0.21s(2.99x)
- 3.8.10 下编译通过(部署目标)
- 变异测试:手动砍掉预筛里一个首字符后,差分测试立即失败并指出漏掉的 "万历二十三年"
  —— 证明这道护栏真的能挡住坏的预筛

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-23 17:14:13 +08:00
sansenhoshi 4f8621bc20 Merge pull request '重命名 parser.py → era_parser.py:适配 Python 3.8.10 部署' (#1) from fix/py38-parser-rename into main
Reviewed-on: #1
2026-09-23 16:58:51 +08:00
sansenandClaude Code f4613a8da2 重命名 parser.py → era_parser.py:适配 Python 3.8.10 部署
`parser` 在 Python 3.8/3.9 是标准库内置模块(已弃用的旧语法解析器)。
BuiltinImporter 排在 sys.meta_path 第一位,优先于任何 sys.path 条目——
即使 sys.path[0] 指向项目根,`from parser import convert_text` 也会拿到内置
模块并抛 ImportError: cannot import name 'convert_text' from 'parser'
(unknown location)。该模块 3.9 弃用、3.10 才移除,故 3.8/3.9 部署环境都会中招。

- client_app.py: from parser import * → from era_parser import *
- era_data.py: 文档注释同步
- 纯重命名,无内容改动(git 识别为 100% similarity)

test/ 下的 extract.py / summary.py / test_parser.py 也已同步改导入,但 test/
被 .gitignore 忽略,不入库。CLAUDE.md / AGENTS.md 同样已更新(含「不要改回
parser.py」的警告),亦被忽略。

验证:pytest 257 passed;summary.py 96/96;语料回归 sample.txt 1766 条、
sample_2.txt 20188 条,与改名前完全一致。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-23 16:56:45 +08:00
3 changed files with 138 additions and 45 deletions
+24 -2
View File
@@ -1,10 +1,11 @@
import json import json
from collections import Counter
from time import perf_counter from time import perf_counter
from fastapi import FastAPI, HTTPException, Header, Request from fastapi import FastAPI, HTTPException, Header, Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from loguru import logger from loguru import logger
from parser import * from era_parser import *
app = FastAPI() app = FastAPI()
@@ -20,12 +21,26 @@ app.add_middleware(
# batch 接口单次请求的条目上限(防滥用) # batch 接口单次请求的条目上限(防滥用)
BATCH_MAX_ITEMS = 1000 BATCH_MAX_ITEMS = 1000
# 日志里最多预览多少条结果。完整结果序列化后体积可达数 MB
# (实测 1MB 输入 → 20188 条 → 6.9MB JSON,是原文的 15%,含缩进还要再乘 1.4),
# 全量落盘会迅速撑爆日志文件。
LOG_PREVIEW_ITEMS = 10
def _check_auth(Authorization: str): def _check_auth(Authorization: str):
if Authorization != "3FFA6A4B073CF065969630692331A873": if Authorization != "3FFA6A4B073CF065969630692331A873":
raise HTTPException(status_code=403, detail="未授权的操作") raise HTTPException(status_code=403, detail="未授权的操作")
def _result_summary(res: list) -> str:
"""一行摘要:结果条数 + 类型分布(如 {'古代纪年': 12, '干支纪年': 3})。
/convert/start 的日志只记摘要与有界预览,不记全量结果——看明细请用接口返回值。
"""
kinds = Counter(r["类型"] for r in res)
return f"{len(res)} 条, 类型分布 {dict(kinds)}"
def _parse_urlencoded_content(body: bytes) -> str: def _parse_urlencoded_content(body: bytes) -> str:
"""手动解析 x-www-form-urlencoded 的 content 字段:绕过 starlette 1MB 表单限制""" """手动解析 x-www-form-urlencoded 的 content 字段:绕过 starlette 1MB 表单限制"""
from urllib.parse import parse_qs from urllib.parse import parse_qs
@@ -47,8 +62,15 @@ async def convert_str(
try: try:
content = _parse_urlencoded_content(await request.body()) content = _parse_urlencoded_content(await request.body())
t0 = perf_counter()
res = convert_text(content) res = convert_text(content)
logger.info(f"转换结果: {json.dumps(res, ensure_ascii=False, indent=2)}") dt = perf_counter() - t0
# 只记摘要 + 前 N 条预览(格式与 batch 接口的日志保持一致)
logger.info(f"转换完成: 输入 {len(content)} 字, {_result_summary(res)}, "
f"耗时 {dt * 1000:.0f}ms")
if res:
head = res[:LOG_PREVIEW_ITEMS]
logger.info(f"结果前 {len(head)} 条: {json.dumps(head, ensure_ascii=False)}")
return JSONResponse(content={"code": 0, "result": res}) return JSONResponse(content={"code": 0, "result": res})
except Exception as e: except Exception as e:
logger.error(f"转换异常: {str(e)}") logger.error(f"转换异常: {str(e)}")
+1 -1
View File
@@ -1,6 +1,6 @@
""" """
年号解析器的静态数据:朝代别名表和已知年号白名单。 年号解析器的静态数据:朝代别名表和已知年号白名单。
这些数据从 parser.py 中提取出来,便于独立维护和扩展。 这些数据从 era_parser.py 中提取出来,便于独立维护和扩展。
""" """
# === 朝代别名 === # === 朝代别名 ===
+113 -42
View File
@@ -331,27 +331,78 @@ _bare_year_after = (
) )
_year_suffix = r'(?:年|(?=[' + re.escape(_bare_year_after) + r'\s]|$))' _year_suffix = r'(?:年|(?=[' + re.escape(_bare_year_after) + r'\s]|$))'
# === 首字符预筛 ===
# 大交替串((?: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
def _build_pattern(with_dynasty=True): def _build_pattern(with_dynasty=True):
"""构建年号正则:已知年号优先 + 通用兜底(仅限有朝代前缀时)""" """构建年号正则:已知年号优先 + 通用兜底(仅限有朝代前缀时)"""
if with_dynasty: if with_dynasty:
# 朝代前缀模式:加(?<![一-鿿])防止"元"在"开元"中被当朝代 # 朝代前缀模式:加(?<![一-鿿])防止"元"在"开元"中被当朝代
dynasty_part = r'(?P<dynasty>(?:(?<![一-鿿])[' + _dynasty_prefix + r'](?:' + _dynasty_alt + r'))|(?:(?<![一-鿿])' + _dynasty_alt + r'))' dynasty_part = r'(?P<dynasty>(?:(?<![一-鿿])[' + _dynasty_prefix + r'](?:' + _dynasty_alt + r'))|(?:(?<![一-鿿])' + _dynasty_alt + r'))'
boundary = '' boundary = ''
gate = _GATE_DYNASTY # 朝代组必选 → 首个消耗字符只能是朝代名首字
else: else:
dynasty_part = r'(?P<dynasty>)' dynasty_part = r'(?P<dynasty>)'
# 无朝代模式不加边界(允许年号跟在其他汉字后面,如"永乐大帝在永乐三年") # 无朝代模式不加边界(允许年号跟在其他汉字后面,如"永乐大帝在永乐三年")
boundary = '' boundary = ''
gate = _GATE_ERA # 朝代组为空 → 首个消耗字符是年号首字
if with_dynasty: if with_dynasty:
# 有朝代前缀:年号后直接跟年份数字,无需额外约束 # 有朝代前缀:年号后直接跟年份数字,无需额外约束
era_year = _known_era_group + r'(?P<year>' + _yd + r'+)' + _year_suffix era_year = _known_era_group + r'(?P<year>' + _yd + r'+)' + _year_suffix
era_yuan = _known_era_group + r'元年' era_yuan = _known_era_group + r'元年'
patterns = [ patterns = [
re.compile(dynasty_part + boundary + era_year), _compile_gated(dynasty_part + boundary + era_year, gate),
re.compile(dynasty_part + boundary + era_yuan), _compile_gated(dynasty_part + boundary + era_yuan, gate),
# 通用兜底(2-8字) # 通用兜底(2-8字)
re.compile(dynasty_part + boundary + r'(?P<era>[一-鿿]{2,8})' + r'(?P<year>' + _yd + r'+)年'), _compile_gated(dynasty_part + boundary + r'(?P<era>[一-鿿]{2,8})' + r'(?P<year>' + _yd + r'+)年', gate),
re.compile(dynasty_part + boundary + r'(?P<era>[一-鿿]{2,8})元年'), _compile_gated(dynasty_part + boundary + r'(?P<era>[一-鿿]{2,8})元年', gate),
] ]
else: else:
# 无朝代前缀:年号后必须紧跟年份数字(前瞻断言),防止"永乐大帝"误匹配 # 无朝代前缀:年号后必须紧跟年份数字(前瞻断言),防止"永乐大帝"误匹配
@@ -359,8 +410,8 @@ def _build_pattern(with_dynasty=True):
era_year = _known_era_group + _year_ahead + r'(?P<year>' + _yd + r'+)' + _year_suffix era_year = _known_era_group + _year_ahead + r'(?P<year>' + _yd + r'+)' + _year_suffix
era_yuan = _known_era_group + _year_ahead + r'元年' era_yuan = _known_era_group + _year_ahead + r'元年'
patterns = [ patterns = [
re.compile(dynasty_part + boundary + era_year), _compile_gated(dynasty_part + boundary + era_year, gate),
re.compile(dynasty_part + boundary + era_yuan), _compile_gated(dynasty_part + boundary + era_yuan, gate),
] ]
return patterns return patterns
@@ -397,12 +448,13 @@ _gz_gy_sep = (
) )
# 干支前的修饰词(如"光绪岁次辛丑""岁在壬戌"):碑刻/谱牒常见 # 干支前的修饰词(如"光绪岁次辛丑""岁在壬戌"):碑刻/谱牒常见
_gz_mod = r'(?:岁[次在])?' _gz_mod = r'(?:岁[次在])?'
_ganzhi_gongyuan_re = re.compile( _ganzhi_gongyuan_re = _compile_gated(
_dynasty_opt _dynasty_opt
+ _gz_era_group + _gz_era_group
+ _gz_mod + _gz_mod
+ r'(?P<ganzhi>' + _gz_pair + r')(?:年)?' + r'(?P<ganzhi>' + _gz_pair + r')(?:年)?'
+ _gz_gy_sep + _gz_gy_sep,
_GATE_DYNASTY_OPT, # _dynasty_opt 可空 → 朝代首字与年号首字并集
) )
# 裸干支+(对照):无年号可锚定(如"壬申(1872)"),公元取对照值 # 裸干支+(对照):无年号可锚定(如"壬申(1872)"),公元取对照值
_ganzhi_bare_gongyuan_re = re.compile( _ganzhi_bare_gongyuan_re = re.compile(
@@ -410,6 +462,51 @@ _ganzhi_bare_gongyuan_re = re.compile(
+ _gz_gy_sep + _gz_gy_sep
) )
# === 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, # 朝代组必选 → 朝代名首字
)
# === 主函数 === # === 主函数 ===
def extract_era_years(text): def extract_era_years(text):
@@ -536,13 +633,8 @@ def extract_era_years(text):
# === 2b. 年号+公元(如"清康熙公元一六八七年丁卯") === # === 2b. 年号+公元(如"清康熙公元一六八七年丁卯") ===
# 年号后直接给出公元年份:年数=公元-年号起始+1,与"年号+年数"公式自洽; # 年号后直接给出公元年份:年数=公元-年号起始+1,与"年号+年数"公式自洽;
# 紧跟的干支用于校验年份一致性。置于公元段之前,合并结果优先、避免重复摘出。 # 紧跟的干支用于校验年份一致性。置于公元段之前,合并结果优先、避免重复摘出。
_era_gongyuan = re.compile( # 正则见模块级 _era_gongyuan_re(含首字符预筛)
r'(?P<dynasty>(?:(?<![一-鿿])[' + _dynasty_prefix + r'](?:' + _dynasty_alt + r'))|(?:(?<![一-鿿])' + _dynasty_alt + r'))?' for m in _era_gongyuan_re.finditer(simplified_text):
+ r'(?P<era>' + _known_era_alt + r')'
+ r'(?:公元|西元|公历)\s*(?P<gy_year>' + _gz_gy_year + r')\s*年?'
+ r'(?P<gy_ganzhi>' + _gz_pair + r')?'
)
for m in _era_gongyuan.finditer(simplified_text):
start, end = m.start(), m.end() start, end = m.start(), m.end()
if any(pos in covered for pos in range(start, end)): if any(pos in covered for pos in range(start, end)):
continue continue
@@ -768,39 +860,18 @@ def extract_era_years(text):
result["超出使用期"] = True result["超出使用期"] = True
results.append(result) results.append(result)
# 模式A:有朝代前缀(如"清雍正乙巳");年号组 _gz_era_group 见模块级定义 # 模式A:有朝代前缀(如"清雍正乙巳");正则见模块级 _ganzhi_dynasty_re
ganzhi_dynasty = re.compile( for m in _ganzhi_dynasty_re.finditer(simplified_text):
r'(?P<dynasty>(?:(?<![一-鿿])[' + _dynasty_prefix + r'](?:' + _dynasty_alt + r'))|(?:(?<![一-鿿])' + _dynasty_alt + r'))'
+ _gz_era_group
+ _gz_mod
+ r'(?P<ganzhi>' + _gz_pair + r')(?:年)?'
)
for m in ganzhi_dynasty.finditer(simplified_text):
_process_ganzhi_match(m) _process_ganzhi_match(m)
# 模式B:无朝代前缀(如"嘉庆癸亥"),年号≥2字符避免天干误匹配 # 模式B:无朝代前缀(如"嘉庆癸亥");正则见模块级 _ganzhi_no_dynasty_re
_long_era_names = [e for e in KNOWN_ERAS_ALL if len(e) >= 2] for m in _ganzhi_no_dynasty_re.finditer(simplified_text):
_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')'
+ _gz_mod
+ r'(?P<ganzhi>' + _gz_pair + r')(?:年)?'
)
for m in ganzhi_no_dynasty.finditer(simplified_text):
_process_ganzhi_match(m) _process_ganzhi_match(m)
# === 3b. 庙号纪年(如"宋高宗""清圣祖",指整个在位期,公元=即位首年)=== # === 3b. 庙号纪年(如"宋高宗""清圣祖",指整个在位期,公元=即位首年)===
# 庙号必须带朝代前缀("高宗"在唐/宋/清均有,单独出现有歧义) # 庙号必须带朝代前缀("高宗"在唐/宋/清均有,单独出现有歧义)
_temple_names = sorted({t for d in temple_dict for t in temple_dict[d]}, key=len, reverse=True) # 正则见模块级 _temple_pattern_re
_temple_alt = '|'.join(re.escape(t) for t in _temple_names) for m in _temple_pattern_re.finditer(simplified_text):
_temple_pattern = re.compile(
r'(?P<dynasty>(?:(?<![一-鿿])[' + _dynasty_prefix + r'](?:' + _dynasty_alt + r'))|(?:(?<![一-鿿])' + _dynasty_alt + r'))'
+ r'(?P<temple>' + _temple_alt + r')'
# 后面紧跟"X年/元年"时不按庙号匹配,交给古代纪年按年号解析(如"元泰定三年")
+ r'(?!(?:' + _yd + r'+年|元年))'
)
for m in _temple_pattern.finditer(simplified_text):
start, end = m.start(), m.end() start, end = m.start(), m.end()
if any(pos in covered for pos in range(start, end)): if any(pos in covered for pos in range(start, end)):
continue continue