增加批量接口,完善日期解析
This commit is contained in:
+71
-4
@@ -1,5 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
from fastapi import FastAPI, HTTPException, Header, Form, Request
|
from time import perf_counter
|
||||||
|
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
|
||||||
@@ -16,21 +17,36 @@ app.add_middleware(
|
|||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# batch 接口单次请求的条目上限(防滥用)
|
||||||
|
BATCH_MAX_ITEMS = 1000
|
||||||
|
|
||||||
|
|
||||||
|
def _check_auth(Authorization: str):
|
||||||
|
if Authorization != "3FFA6A4B073CF065969630692331A873":
|
||||||
|
raise HTTPException(status_code=403, detail="未授权的操作")
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_urlencoded_content(body: bytes) -> str:
|
||||||
|
"""手动解析 x-www-form-urlencoded 的 content 字段:绕过 starlette 1MB 表单限制"""
|
||||||
|
from urllib.parse import parse_qs
|
||||||
|
params = parse_qs(body.decode("utf-8"), keep_blank_values=True)
|
||||||
|
return params.get("content", [""])[0]
|
||||||
|
|
||||||
|
|
||||||
@app.post("/convert/start")
|
@app.post("/convert/start")
|
||||||
async def convert_str(
|
async def convert_str(
|
||||||
request: Request,
|
request: Request,
|
||||||
content: str = Form(...), # ✅ 改为接收 x-www-form-urlencoded 参数
|
|
||||||
Authorization: str = Header(None)
|
Authorization: str = Header(None)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
带调试信息的转换接口 (支持 x-www-form-urlencoded)
|
带调试信息的转换接口 (支持 x-www-form-urlencoded)
|
||||||
|
手动解析表单体:绕过 starlette 1MB 表单限制(族谱全文可达数 MB)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if Authorization != "3FFA6A4B073CF065969630692331A873":
|
_check_auth(Authorization)
|
||||||
raise HTTPException(status_code=403, detail="未授权的操作")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
content = _parse_urlencoded_content(await request.body())
|
||||||
res = convert_text(content)
|
res = convert_text(content)
|
||||||
logger.info(f"转换结果: {json.dumps(res, ensure_ascii=False, indent=2)}")
|
logger.info(f"转换结果: {json.dumps(res, ensure_ascii=False, indent=2)}")
|
||||||
return JSONResponse(content={"code": 0, "result": res})
|
return JSONResponse(content={"code": 0, "result": res})
|
||||||
@@ -42,6 +58,57 @@ async def convert_str(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/convert/batch")
|
||||||
|
async def convert_batch(
|
||||||
|
request: Request,
|
||||||
|
Authorization: str = Header(None)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
批量转换接口:短文本多任务场景,一次请求处理 N 条 content。
|
||||||
|
请求: {"items": [{"id": "row1", "content": "..."}, ...]}(application/json)
|
||||||
|
响应: {"code": 0, "result": [{"id": "row1", "result": [...]}, ...]}
|
||||||
|
- id 透传用于前端回填,缺省按数组顺序编号
|
||||||
|
- 单条解析失败不影响其他条目(该条返回 error 字段)
|
||||||
|
- 每条 content 独立解析,位置语义与单接口一致(从 0 起)
|
||||||
|
"""
|
||||||
|
_check_auth(Authorization)
|
||||||
|
|
||||||
|
# 请求体解析与入参校验失败 → 400(客户端错误)
|
||||||
|
try:
|
||||||
|
payload = json.loads((await request.body()).decode("utf-8"))
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"batch 请求体解析失败: {str(e)}")
|
||||||
|
raise HTTPException(status_code=400, detail="请求体必须是合法 JSON")
|
||||||
|
items = payload.get("items") if isinstance(payload, dict) else None
|
||||||
|
if not isinstance(items, list) or not items:
|
||||||
|
raise HTTPException(status_code=400, detail="items 必须是非空数组")
|
||||||
|
if len(items) > BATCH_MAX_ITEMS:
|
||||||
|
raise HTTPException(status_code=400, detail=f"items 超过上限 {BATCH_MAX_ITEMS} 条")
|
||||||
|
|
||||||
|
try:
|
||||||
|
t0 = perf_counter()
|
||||||
|
results = []
|
||||||
|
for i, item in enumerate(items):
|
||||||
|
item_id = item.get("id") if isinstance(item, dict) else None
|
||||||
|
content = item.get("content", "") if isinstance(item, dict) else ""
|
||||||
|
if item_id is None:
|
||||||
|
item_id = str(i)
|
||||||
|
try:
|
||||||
|
results.append({"id": item_id, "result": convert_text(content)})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"batch 条目 {item_id} 解析异常: {str(e)}")
|
||||||
|
results.append({"id": item_id, "error": str(e)})
|
||||||
|
dt = perf_counter() - t0
|
||||||
|
logger.info(f"批量转换: {len(results)} 条, 耗时 {dt * 1000:.0f}ms")
|
||||||
|
return JSONResponse(content={"code": 0, "result": results})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"批量转换异常: {str(e)}")
|
||||||
|
return JSONResponse(
|
||||||
|
content={"code": 500, "result": f"批量转换服务异常: {str(e)}"},
|
||||||
|
status_code=500
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
|
|||||||
+10
@@ -47,6 +47,16 @@ TEMPLE_ALIASES = {
|
|||||||
"始皇帝": "始皇", # CSV 用"始皇帝",正文常用"秦始皇"
|
"始皇帝": "始皇", # CSV 用"始皇帝",正文常用"秦始皇"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# === 年号别名(正文异写/避讳/简称 → CSV 规范写法) ===
|
||||||
|
# 匹配后统一归一为规范年号再查表;别名本身同时加入白名单(KNOWN_ERAS_ALL)
|
||||||
|
ERA_ALIASES = {
|
||||||
|
"崇正": "崇祯", # 避"祯"字讳(清代文献常见)
|
||||||
|
"宏治": "弘治", # 避"弘"字讳(避乾隆帝弘历名)
|
||||||
|
"宣得": "宣德", # 同音误写(明宣德,族谱常见)
|
||||||
|
"大得": "大德", # 同音误写(元大德)
|
||||||
|
"靖国": "建中靖国", # 简称(北宋建中靖国,元年即 1101)
|
||||||
|
}
|
||||||
|
|
||||||
# === 常见年号白名单(优先匹配,避免正则贪心误匹配) ===
|
# === 常见年号白名单(优先匹配,避免正则贪心误匹配) ===
|
||||||
KNOWN_ERAS = {
|
KNOWN_ERAS = {
|
||||||
# 秦汉
|
# 秦汉
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import re
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
from opencc import OpenCC
|
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')
|
cc = OpenCC('t2s')
|
||||||
|
|
||||||
@@ -119,13 +119,18 @@ for _, row in era_df.iterrows():
|
|||||||
if span is not None:
|
if span is not None:
|
||||||
era_span.setdefault(dynasty, {}).setdefault(era, span)
|
era_span.setdefault(dynasty, {}).setdefault(era, span)
|
||||||
|
|
||||||
# 有效年号白名单 = 手工精选 ∪ CSV 年号(自动补全:白名单不应小于数据表)。
|
# 有效年号白名单 = 手工精选 ∪ CSV 年号 ∪ 年号别名键(自动补全:白名单不应小于数据表)。
|
||||||
# 排除:民国(有专门匹配段)、带括号的年号(如"(后元)",防止与东汉"中元"等歧义)
|
# 排除:民国(有专门匹配段)、带括号的年号(如"(后元)",防止与东汉"中元"等歧义)
|
||||||
_csv_eras = {
|
_csv_eras = {
|
||||||
e for e in era_df["年号"]
|
e for e in era_df["年号"]
|
||||||
if e and e != '民国' and '(' not in e and '(' not in e and 2 <= len(e) <= 8
|
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)
|
_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)十一月十四生") ===
|
# === 2c. 干支+(公元对照)(如"清同治壬申(1872)十一月十四生") ===
|
||||||
# 干支纪年旁直接给出公元年份:合并为一条结果、对照校验干支、紧跟的月日并入字段。
|
# 干支纪年旁直接给出公元年份:合并为一条结果、对照校验干支。
|
||||||
# 置于公元段之前(见 extract_era_years),避免"(1872)"被单独摘出成公元纪年。
|
# 除括号外还支持:"公元"标记(清同治壬申公元1872年)、横线(壬申-1872)、
|
||||||
|
# 无分隔连写(壬申1872年)。其余内容(如"十一月十四")不并入原文。
|
||||||
|
# 置于公元段之前(见 extract_era_years),避免对照年份被单独摘出成公元纪年。
|
||||||
_dynasty_opt = (r'(?P<dynasty>(?:(?<![一-鿿])[' + _dynasty_prefix + r'](?:' + _dynasty_alt + r'))'
|
_dynasty_opt = (r'(?P<dynasty>(?:(?<![一-鿿])[' + _dynasty_prefix + r'](?:' + _dynasty_alt + r'))'
|
||||||
r'|(?:(?<![一-鿿])' + _dynasty_alt + r'))?')
|
r'|(?:(?<![一-鿿])' + _dynasty_alt + r'))?')
|
||||||
# 括号中的公元年份:阿拉伯/全角/中文数字 3-4 位,可带"年"字
|
# 公元对照年份:阿拉伯/全角/中文数字 3-4 位,可带"年"字
|
||||||
_gz_gy_paren = r'[((]\s*(?P<gy_year>[〇零○O一二三四五六七八九\d0-9]{3,4})\s*年?[))]'
|
_gz_gy_year = r'[〇零○O一二三四五六七八九\d0-9]{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(
|
_ganzhi_gongyuan_re = re.compile(
|
||||||
_dynasty_opt
|
_dynasty_opt
|
||||||
+ _gz_era_group
|
+ _gz_era_group
|
||||||
|
+ _gz_mod
|
||||||
+ r'(?P<ganzhi>' + _gz_pair + r')(?:年)?'
|
+ r'(?P<ganzhi>' + _gz_pair + r')(?:年)?'
|
||||||
+ r'(?P<md1>' + _md_body + r')?'
|
+ _gz_gy_sep
|
||||||
+ r'(?:' + _md_verb + r')?'
|
|
||||||
+ _gz_gy_paren
|
|
||||||
+ r'(?P<md2>' + _md_body + r')?'
|
|
||||||
)
|
)
|
||||||
# 裸干支+(对照):无年号可锚定(如"壬申(1872)"),公元取对照值
|
# 裸干支+(对照):无年号可锚定(如"壬申(1872)"),公元取对照值
|
||||||
_ganzhi_bare_gongyuan_re = re.compile(
|
_ganzhi_bare_gongyuan_re = re.compile(
|
||||||
r'(?P<ganzhi>' + _gz_pair + r')(?:年)?'
|
r'(?P<ganzhi>' + _gz_pair + r')(?:年)?'
|
||||||
+ r'(?P<md1>' + _md_body + r')?'
|
+ _gz_gy_sep
|
||||||
+ r'(?:' + _md_verb + r')?'
|
|
||||||
+ _gz_gy_paren
|
|
||||||
+ r'(?P<md2>' + _md_body + r')?'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -342,28 +317,66 @@ def extract_era_years(text):
|
|||||||
# 覆盖集:先匹配的段落占据位置,后续段落跳过重叠(防止同一文本被多段重复摘出)
|
# 覆盖集:先匹配的段落占据位置,后续段落跳过重叠(防止同一文本被多段重复摘出)
|
||||||
covered = set()
|
covered = set()
|
||||||
|
|
||||||
|
# 民国纪年的公元对照仅支持括号形式("民国三十八年(1949)"):
|
||||||
|
# 民国X年已是确定年份,逗号/横线/连写分隔多为并列句(如"民国三十八年,公元二〇二四年"是两个日期),不合并
|
||||||
|
_mg_gy_paren = r'(?:[((][ \t]*(?P<gy_year>' + _gz_gy_year + r')[ \t]*年?[))])?'
|
||||||
|
|
||||||
# === 1. 民国 ===
|
# === 1. 民国 ===
|
||||||
minguo_pattern = re.compile(r'民[国國]\s*([〇零一二三四五六七八九十百廿卄卅卌元\d0-9壹貳贰叁肆伍陸陆柒捌玖拾]+)\s*年')
|
minguo_pattern = re.compile(
|
||||||
|
r'民[国國]\s*([〇零一二三四五六七八九十百廿卄卅卌元\d0-9壹貳贰叁肆伍陸陆柒捌玖拾]+)\s*年'
|
||||||
|
+ _mg_gy_paren
|
||||||
|
)
|
||||||
for m in minguo_pattern.finditer(simplified_text):
|
for m in minguo_pattern.finditer(simplified_text):
|
||||||
year_text = m.group(1)
|
year_text = m.group(1)
|
||||||
year_num = chinese_to_num(year_text)
|
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()
|
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))
|
covered.update(range(start, end))
|
||||||
raw_fan = original_text[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,
|
"原文": raw_fan,
|
||||||
"原文(简体)": m.group(0),
|
"原文(简体)": simplified_text[start:end],
|
||||||
"朝代": "民国",
|
"朝代": "民国",
|
||||||
"年号": "民国纪年",
|
"年号": "民国纪年",
|
||||||
"年数": year_num,
|
"年数": year_num,
|
||||||
"公元": 1911 + year_num,
|
"公元": gregorian,
|
||||||
"公元中文": f"{arabic_to_chinese_year(1911 + year_num)}年",
|
"公元中文": f"{arabic_to_chinese_year(gregorian)}年",
|
||||||
"位置": {"起始": start, "结束": end},
|
"位置": {"起始": 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. 民国+干支纪年(如"民国丁卯""民国甲戌")
|
# 1b. 民国+干支纪年(如"民国丁卯""民国甲戌"),支持(公元对照)合并
|
||||||
minguo_gz = re.compile(r'民[国國]\s*(?P<ganzhi>' + _gz_pair + r')')
|
minguo_gz = re.compile(
|
||||||
|
r'民[国國]\s*(?P<ganzhi>' + _gz_pair + r')(?:年)?'
|
||||||
|
+ _mg_gy_paren
|
||||||
|
)
|
||||||
for m in minguo_gz.finditer(simplified_text):
|
for m in minguo_gz.finditer(simplified_text):
|
||||||
ganzhi = m.group("ganzhi")
|
ganzhi = m.group("ganzhi")
|
||||||
gz_offset = _ganzhi_to_offset.get(ganzhi, -1)
|
gz_offset = _ganzhi_to_offset.get(ganzhi, -1)
|
||||||
@@ -372,22 +385,34 @@ def extract_era_years(text):
|
|||||||
# 民国元年=1912, base_gz = (1912-4)%60 = 8 (己酉)
|
# 民国元年=1912, base_gz = (1912-4)%60 = 8 (己酉)
|
||||||
base_gz = (1912 - 4) % 60
|
base_gz = (1912 - 4) % 60
|
||||||
year_in_era = ((gz_offset - base_gz) % 60) + 1
|
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()
|
start, end = m.start(), m.end()
|
||||||
covered.update(range(start, end))
|
covered.update(range(start, end))
|
||||||
raw_fan = original_text[start:end]
|
raw_fan = original_text[start:end]
|
||||||
results.append({
|
result = {
|
||||||
"原文": raw_fan,
|
"原文": raw_fan,
|
||||||
"原文(简体)": m.group(0),
|
"原文(简体)": m.group(0),
|
||||||
"朝代": "民国",
|
"朝代": "民国",
|
||||||
"年号": "民国纪年",
|
"年号": "民国纪年",
|
||||||
"年数": year_in_era,
|
"年数": year_in_era,
|
||||||
"干支": ganzhi,
|
"干支": ganzhi,
|
||||||
"公元": gregorian,
|
"公元": gy_year if gy_year is not None else computed,
|
||||||
"公元中文": f"{arabic_to_chinese_year(gregorian)}年",
|
"公元中文": f"{arabic_to_chinese_year(gy_year if gy_year is not None else computed)}年",
|
||||||
"位置": {"起始": start, "结束": end},
|
"位置": {"起始": start, "结束": end},
|
||||||
"类型": "民国纪年(干支)",
|
"类型": "民国纪年(干支)",
|
||||||
})
|
}
|
||||||
|
if gy_year is not None and gy_year != computed:
|
||||||
|
result["干支不符"] = True
|
||||||
|
result["干支推算"] = computed
|
||||||
|
results.append(result)
|
||||||
|
|
||||||
# === 2b. 年号+公元(如"清康熙公元一六八七年丁卯") ===
|
# === 2b. 年号+公元(如"清康熙公元一六八七年丁卯") ===
|
||||||
# 年号后直接给出公元年份:年数=公元-年号起始+1,与"年号+年数"公式自洽;
|
# 年号后直接给出公元年份:年数=公元-年号起始+1,与"年号+年数"公式自洽;
|
||||||
@@ -395,14 +420,14 @@ def extract_era_years(text):
|
|||||||
_era_gongyuan = re.compile(
|
_era_gongyuan = re.compile(
|
||||||
r'(?P<dynasty>(?:(?<![一-鿿])[' + _dynasty_prefix + r'](?:' + _dynasty_alt + r'))|(?:(?<![一-鿿])' + _dynasty_alt + r'))?'
|
r'(?P<dynasty>(?:(?<![一-鿿])[' + _dynasty_prefix + r'](?:' + _dynasty_alt + r'))|(?:(?<![一-鿿])' + _dynasty_alt + r'))?'
|
||||||
+ r'(?P<era>' + _known_era_alt + r')'
|
+ r'(?P<era>' + _known_era_alt + r')'
|
||||||
+ r'公元\s*(?P<gy_year>[〇零○O一二三四五六七八九\d0-9]{3,4})\s*年?'
|
+ r'(?:公元|西元|公历)\s*(?P<gy_year>[〇零○O一二三四五六七八九\d0-9]{3,4})\s*年?'
|
||||||
+ r'(?P<gy_ganzhi>' + _gz_pair + r')?'
|
+ r'(?P<gy_ganzhi>' + _gz_pair + r')?'
|
||||||
)
|
)
|
||||||
for m in _era_gongyuan.finditer(simplified_text):
|
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
|
||||||
era = cc.convert(m.group("era"))
|
era = _norm_era(m.group("era"))
|
||||||
year_num = chinese_to_num(m.group("gy_year"))
|
year_num = chinese_to_num(m.group("gy_year"))
|
||||||
if not (1000 <= year_num <= 2100):
|
if not (1000 <= year_num <= 2100):
|
||||||
continue
|
continue
|
||||||
@@ -458,9 +483,14 @@ def extract_era_years(text):
|
|||||||
def _process_ganzhi_gongyuan(m):
|
def _process_ganzhi_gongyuan(m):
|
||||||
raw_dynasty = m.group("dynasty") if "dynasty" in m.groupdict() else ""
|
raw_dynasty = m.group("dynasty") if "dynasty" in m.groupdict() else ""
|
||||||
dynasty = normalize_dynasty_name(raw_dynasty or "")
|
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")
|
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):
|
if ganzhi not in _ganzhi_to_offset or not (1000 <= gy_year <= 2100):
|
||||||
return
|
return
|
||||||
start, end = m.start(), m.end()
|
start, end = m.start(), m.end()
|
||||||
@@ -512,7 +542,6 @@ def extract_era_years(text):
|
|||||||
elif gregorian_to_ganzhi(gy_year) != ganzhi:
|
elif gregorian_to_ganzhi(gy_year) != ganzhi:
|
||||||
# 裸干支+对照:无年号可锚定,直接用公元年份的干支校验(同公元段约定)
|
# 裸干支+对照:无年号可锚定,直接用公元年份的干支校验(同公元段约定)
|
||||||
result["干支不符"] = True
|
result["干支不符"] = True
|
||||||
_attach_month_day(result, m)
|
|
||||||
results.append(result)
|
results.append(result)
|
||||||
|
|
||||||
for m in _ganzhi_gongyuan_re.finditer(simplified_text):
|
for m in _ganzhi_gongyuan_re.finditer(simplified_text):
|
||||||
@@ -522,13 +551,13 @@ def extract_era_years(text):
|
|||||||
|
|
||||||
# === 2. 公元(允许无"年"字)===
|
# === 2. 公元(允许无"年"字)===
|
||||||
gongyuan_pattern = re.compile(
|
gongyuan_pattern = re.compile(
|
||||||
r'(公元)?\s*([〇零○O一二三四五六七八九\d0-9]{3,4})\s*(?:(.*?))?\s*(年)?'
|
r'(?:公元|西元|公历)?\s*([〇零○O一二三四五六七八九\d0-9]{3,4})\s*(?:(.*?))?\s*(年)?'
|
||||||
)
|
)
|
||||||
for m in gongyuan_pattern.finditer(simplified_text):
|
for m in gongyuan_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
|
||||||
year_text = m.group(2)
|
year_text = m.group(1)
|
||||||
year_num = chinese_to_num(year_text)
|
year_num = chinese_to_num(year_text)
|
||||||
if not (1000 <= year_num <= 2100):
|
if not (1000 <= year_num <= 2100):
|
||||||
continue
|
continue
|
||||||
@@ -567,7 +596,7 @@ def extract_era_years(text):
|
|||||||
"""处理干支纪年匹配"""
|
"""处理干支纪年匹配"""
|
||||||
raw_dynasty = m.group("dynasty") if "dynasty" in m.groupdict() else ""
|
raw_dynasty = m.group("dynasty") if "dynasty" in m.groupdict() else ""
|
||||||
dynasty = normalize_dynasty_name(raw_dynasty or "")
|
dynasty = normalize_dynasty_name(raw_dynasty or "")
|
||||||
era = cc.convert(m.group("era"))
|
era = _norm_era(m.group("era"))
|
||||||
ganzhi = m.group("ganzhi")
|
ganzhi = m.group("ganzhi")
|
||||||
if ganzhi not in _ganzhi_to_offset:
|
if ganzhi not in _ganzhi_to_offset:
|
||||||
return
|
return
|
||||||
@@ -622,16 +651,14 @@ def extract_era_years(text):
|
|||||||
span = era_span.get(dynasty, {}).get(era)
|
span = era_span.get(dynasty, {}).get(era)
|
||||||
if span is not None and gregorian > base + span - 1:
|
if span is not None and gregorian > base + span - 1:
|
||||||
result["超出使用期"] = True
|
result["超出使用期"] = True
|
||||||
_attach_month_day(result, m)
|
|
||||||
results.append(result)
|
results.append(result)
|
||||||
|
|
||||||
# 模式A:有朝代前缀(如"清雍正乙巳");年号组 _gz_era_group 见模块级定义
|
# 模式A:有朝代前缀(如"清雍正乙巳");年号组 _gz_era_group 见模块级定义
|
||||||
ganzhi_dynasty = re.compile(
|
ganzhi_dynasty = re.compile(
|
||||||
r'(?P<dynasty>(?:(?<![一-鿿])[' + _dynasty_prefix + r'](?:' + _dynasty_alt + r'))|(?:(?<![一-鿿])' + _dynasty_alt + r'))'
|
r'(?P<dynasty>(?:(?<![一-鿿])[' + _dynasty_prefix + r'](?:' + _dynasty_alt + r'))|(?:(?<![一-鿿])' + _dynasty_alt + r'))'
|
||||||
+ _gz_era_group
|
+ _gz_era_group
|
||||||
|
+ _gz_mod
|
||||||
+ r'(?P<ganzhi>' + _gz_pair + r')(?:年)?'
|
+ r'(?P<ganzhi>' + _gz_pair + r')(?:年)?'
|
||||||
# 紧跟的农历月日并入同一单位(如"清雍正乙巳十一月十二午时生")
|
|
||||||
+ r'(?P<md>' + _md_body + r')?'
|
|
||||||
)
|
)
|
||||||
for m in ganzhi_dynasty.finditer(simplified_text):
|
for m in ganzhi_dynasty.finditer(simplified_text):
|
||||||
_process_ganzhi_match(m)
|
_process_ganzhi_match(m)
|
||||||
@@ -642,8 +669,8 @@ def extract_era_years(text):
|
|||||||
ganzhi_no_dynasty = re.compile(
|
ganzhi_no_dynasty = re.compile(
|
||||||
r'(?P<dynasty>)'
|
r'(?P<dynasty>)'
|
||||||
+ r'(?P<era>' + _long_era_alt + r')'
|
+ r'(?P<era>' + _long_era_alt + r')'
|
||||||
|
+ _gz_mod
|
||||||
+ r'(?P<ganzhi>' + _gz_pair + r')(?:年)?'
|
+ r'(?P<ganzhi>' + _gz_pair + r')(?:年)?'
|
||||||
+ r'(?P<md>' + _md_body + r')?'
|
|
||||||
)
|
)
|
||||||
for m in ganzhi_no_dynasty.finditer(simplified_text):
|
for m in ganzhi_no_dynasty.finditer(simplified_text):
|
||||||
_process_ganzhi_match(m)
|
_process_ganzhi_match(m)
|
||||||
@@ -697,7 +724,7 @@ def extract_era_years(text):
|
|||||||
"""处理一个年号正则匹配结果"""
|
"""处理一个年号正则匹配结果"""
|
||||||
raw_dynasty = m.group("dynasty") if "dynasty" in m.groupdict() else ""
|
raw_dynasty = m.group("dynasty") if "dynasty" in m.groupdict() else ""
|
||||||
dynasty = normalize_dynasty_name(raw_dynasty or "")
|
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 '零一二三四五六七八九十廿卄卅卌':
|
if len(era) == 1 and era in '零一二三四五六七八九十廿卄卅卌':
|
||||||
@@ -738,13 +765,21 @@ def extract_era_years(text):
|
|||||||
break
|
break
|
||||||
|
|
||||||
start, end = m.start(), m.end()
|
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]
|
raw_fan = original_text[start:end]
|
||||||
covered.update(range(start, end))
|
covered.update(range(start, end))
|
||||||
if base:
|
if base:
|
||||||
gregorian = base + year_num - 1
|
gregorian = base + year_num - 1
|
||||||
result = {
|
result = {
|
||||||
"原文": raw_fan,
|
"原文": raw_fan,
|
||||||
"原文(简体)": m.group(0),
|
"原文(简体)": m.group(0) + (ganzhi or ''),
|
||||||
"朝代": dynasty,
|
"朝代": dynasty,
|
||||||
"年号": era,
|
"年号": era,
|
||||||
"年数": year_num,
|
"年数": year_num,
|
||||||
@@ -753,6 +788,10 @@ def extract_era_years(text):
|
|||||||
"位置": {"起始": start, "结束": end},
|
"位置": {"起始": start, "结束": end},
|
||||||
"类型": "古代纪年",
|
"类型": "古代纪年",
|
||||||
}
|
}
|
||||||
|
if ganzhi:
|
||||||
|
result["干支"] = ganzhi
|
||||||
|
if gregorian_to_ganzhi(gregorian) != ganzhi:
|
||||||
|
result["干支不符"] = True
|
||||||
if len(multi) > 1:
|
if len(multi) > 1:
|
||||||
result["多候选"] = True
|
result["多候选"] = True
|
||||||
result["候选"] = [list(c) for c in multi]
|
result["候选"] = [list(c) for c in multi]
|
||||||
|
|||||||
Reference in New Issue
Block a user