Spaces:
Runtime error
Runtime error
| # core_numeric.py | |
| # 纯逻辑函数:判定“数字一致性”,辅助行筛选/归一化等;供 app.py 调用。 | |
| import re, unicodedata | |
| from collections import Counter | |
| import pandas as pd | |
| import cn2an | |
| from number_parser import parse as en_words_parse | |
| # ===== 公共常量 ===== | |
| SUFFIX = "_numeric_tagged" | |
| CATEGORY_TITLES = { | |
| "inconsistency in target", "inconsistency in source", "tag mismatch", | |
| "numeric mismatch", "target same as source", | |
| } | |
| PROCESS_ONLY_CATEGORIES = {"numeric mismatch"} | |
| # ===== 正则与工具 ===== | |
| DIGIT_RE = re.compile(r'[-+]?\d+(?:[.,]\d+)?(?:[%%‰])?') | |
| TAG_RE = re.compile(r'<[^>]+>') | |
| FILE_SEG_RE = re.compile(r'^(.*?)(?:\s*\((\d+)\))\s*$') | |
| _WS = ("\u00A0", "\u2009", "\u2002", "\u2003", "\u2007", "\u202F") | |
| MONTH_MAP = {"january":"1","jan":"1","february":"2","feb":"2","march":"3","mar":"3", | |
| "april":"4","apr":"4","may":"5","june":"6","jun":"6","july":"7","jul":"7", | |
| "august":"8","aug":"8","september":"9","sept":"9","sep":"9", | |
| "october":"10","oct":"10","november":"11","nov":"11","december":"12","dec":"12"} | |
| MONTH_RE = re.compile(r'\b(' + '|'.join(sorted(MONTH_MAP.keys(), key=len, reverse=True)) + r')\b', re.I) | |
| DAY_RE = re.compile(r'\b(\d{1,2})(?:st|nd|rd|th)?\b', re.I) | |
| YEAR_RE = re.compile(r'\b(19|20)\d{2}\b') | |
| EN_ORDINAL_MAP = {"first":"1","second":"2","third":"3","fourth":"4","fifth":"5", | |
| "sixth":"6","seventh":"7","eighth":"8","ninth":"9","tenth":"10", | |
| "eleventh":"11","twelfth":"12","thirteenth":"13","fourteenth":"14", | |
| "fifteenth":"15","sixteenth":"16","seventeenth":"17","eighteenth":"18", | |
| "nineteenth":"19","twentieth":"20"} | |
| EN_ORDINAL_RE = re.compile(r'\b(' + '|'.join(EN_ORDINAL_MAP.keys()) + r')\b', re.I) | |
| DOUBLE_SEMANTIC_PAT = re.compile(r'(二重|二層|双重|双层|ダブル|이중)') | |
| VAR_TOKEN_PAT = re.compile(r'\b[A-Z]\d+\b') | |
| FULLWIDTH_TO_HALF = str.maketrans("0123456789", "0123456789") | |
| def clean_invis_spaces(s: str) -> str: | |
| if not isinstance(s, str): return s | |
| for ch in _WS: s = s.replace(ch, " ") | |
| return s | |
| def strip_tags(s: str) -> str: | |
| return TAG_RE.sub("", s) | |
| def normalize_text(s: str) -> str: | |
| if not isinstance(s, str): return "" | |
| s = unicodedata.normalize("NFKC", s).replace(",", ",").replace(".", ".").replace("%", "%") | |
| s = strip_tags(s) | |
| # 英文常见词数化 | |
| try: s = en_words_parse(s) | |
| except Exception: pass | |
| # 中文数字转阿拉伯 | |
| try: s = cn2an.transform(s, "cn2an") | |
| except Exception: pass | |
| return s | |
| def add_month_tokens_if_date_context(text: str, counter: Counter): | |
| if not text: return | |
| for m in MONTH_RE.finditer(text): | |
| month_num = MONTH_MAP[m.group(1).lower()] | |
| pre = text[max(0, m.start()-8): m.start()] | |
| post = text[m.end(): m.end()+8] | |
| if DAY_RE.search(pre) or DAY_RE.search(post) or YEAR_RE.search(post): | |
| counter.update([month_num]) | |
| def _to_ascii_num(s: str) -> str: return s.translate(FULLWIDTH_TO_HALF) | |
| def normalize_cjk_dates_in_counter(text: str, counter: Counter): | |
| # 粗略把全角月/日换成半角,以便计数统一 | |
| m = re.search(r'([0-9\d]{1,2})\s*月\s*([0-9\d]{1,2})\s*日', text) | |
| if m: | |
| mm = str(int(_to_ascii_num(m.group(1)))); dd = str(int(_to_ascii_num(m.group(2)))) | |
| if counter.get(mm, 0) == 0: counter.update([mm]) | |
| if counter.get(dd, 0) == 0: counter.update([dd]) | |
| def extract_numbers(s: str) -> Counter: | |
| s = re.sub(r'(\d)\s*[-–—~〜~]\s*(\d)', r'\1 \2', s) | |
| s = re.sub(r'(?<=\d)(?=[A-Za-z])', ' ', s) | |
| s = re.sub(r'(?<=[A-Za-z])(?=\d)', ' ', s) | |
| s = clean_invis_spaces(s) | |
| nums = [token.replace(",", "") for token in DIGIT_RE.findall(s) if token] | |
| counter = Counter(nums) | |
| add_month_tokens_if_date_context(s, counter) | |
| normalize_cjk_dates_in_counter(s, counter) | |
| return counter | |
| def counter_to_str(c: Counter) -> str: | |
| if not c: return "" | |
| items = sorted(c.items(), key=lambda kv: kv[0]) | |
| return ", ".join([f"{k}×{v}" if v>1 else k for k,v in items]) | |
| def balanced_mask_vars(src: str, tgt: str): | |
| src_set = set(VAR_TOKEN_PAT.findall(src)) | |
| tgt_set = set(VAR_TOKEN_PAT.findall(tgt)) | |
| if not src_set or src_set != tgt_set: return src, tgt, [] | |
| def _repl(m: "re.Match") -> str: return m.group(0)[0] + "§VAR§" | |
| return VAR_TOKEN_PAT.sub(_repl, src), VAR_TOKEN_PAT.sub(_repl, tgt), sorted(src_set) | |
| def classify_row(src: str, tgt: str): | |
| note = [] | |
| s, t, vars_hits = balanced_mask_vars(src or "", tgt or "") | |
| if vars_hits: note.append("MaskedVars(" + ",".join(vars_hits) + ")") | |
| s_norm, t_norm = normalize_text(s), normalize_text(t) | |
| ns, nt = extract_numbers(s_norm), extract_numbers(t_norm) | |
| src_s, tgt_s = counter_to_str(ns), counter_to_str(nt) | |
| if not ns and not nt: | |
| base = "NoNumbersBothSides" | |
| return (False, src_s, tgt_s, base if not note else base+";"+";".join(note)) | |
| if ns == nt: | |
| base = "NumbersEqualAfterNormalization" | |
| return (False, src_s, tgt_s, base if not note else base+";"+";".join(note)) | |
| base = "NumbersDiffer" | |
| return (True, src_s, tgt_s, base if not note else base+";"+";".join(note)) | |
| def _is_blank(x) -> bool: | |
| if x is None or (isinstance(x, float) and pd.isna(x)): return True | |
| s = str(x).strip().lower() | |
| return s in {"", "nan", "none", "null"} | |
| def is_category_header_row(a, c, d) -> bool: | |
| a_norm = clean_invis_spaces(str(a)).strip().lower() | |
| return (a_norm != "") and _is_blank(c) and _is_blank(d) | |
| def is_allowed_category_name(a) -> bool: | |
| a_norm = clean_invis_spaces(str(a)).strip().lower() | |
| return any(a_norm.startswith(cat) for cat in PROCESS_ONLY_CATEGORIES) | |
| def find_header_row_by_probe(get_cell, max_scan=60) -> int: | |
| """ | |
| 通过 get_cell(r, c) 探测第 C、D 列是否出现 'Source'/'Target'。 | |
| 适配 xlrd 行读取或 pandas.DataFrame.iat。 | |
| """ | |
| for r in range(max_scan): | |
| c = str(get_cell(r, 2) or "").strip().lower() | |
| d = str(get_cell(r, 3) or "").strip().lower() | |
| if c == "source" and d == "target": | |
| return r | |
| return -1 | |