"""中文大写金额与阿拉伯金额互转(发票大小写金额一致性校验用)。 支持 "壹仟陆佰伍拾元整"、"壹佰贰拾叁元肆角伍分"、"拾元整" 等常见发票大写写法; 同时兼容 "¥1,234.50" / "1234.50元" 等阿拉伯写法的清洗。 解析失败返回 None,由调用方降级为"待确认",不做硬性驳回。 """ from __future__ import annotations import re from typing import Optional _DIGITS = { "零": 0, "壹": 1, "贰": 2, "叁": 3, "肆": 4, "伍": 5, "陆": 6, "柒": 7, "捌": 8, "玖": 9, } _SMALL_UNITS = {"拾": 10, "佰": 100, "仟": 1000} _GROUP_UNITS = {"万": 10**4, "亿": 10**8} _ALL_KEYS = r"零壹贰叁肆伍陆柒捌玖拾佰仟万亿元圆角分整" _TRAILING_CHARS = re.compile(r"[人民币¥¥\s,,元圆整]|人民币$") def clean_amount_text(text: str) -> str: """清洗阿拉伯金额文本,返回可 float() 的字符串(失败返回原文本去掉常见缀符)。""" cleaned = _TRAILING_CHARS.sub("", (text or "").strip()) return cleaned def parse_cny_upper(text: str) -> Optional[float]: """解析中文大写金额为数值;无法解析时返回 None。""" if not text: return None raw = (text or "").strip() raw = raw.replace("人民币", "").replace("¥", "").replace("¥", "").strip() raw = re.sub(r"[\s,,]", "", raw) if not raw: return None if re.search(r"[0-9]", raw): return None # 纯阿拉伯数字写法不属于大写金额 if not re.fullmatch(rf"[{_ALL_KEYS}]+", raw): return None int_part, frac_part = raw, "" for sep in ("元", "圆"): if sep in raw: int_part, frac_part = raw.split(sep, 1) break try: yuan = _parse_integer_section(int_part) if int_part else 0.0 except ValueError: return None jiao = fen = 0 m_jiao = re.search(r"([零壹贰叁肆伍陆柒捌玖])角", frac_part) if m_jiao: jiao = _DIGITS[m_jiao.group(1)] m_fen = re.search(r"([零壹贰叁肆伍陆柒捌玖])分", frac_part) if m_fen: fen = _DIGITS[m_fen.group(1)] return round(yuan + jiao / 10 + fen / 100, 2) def _parse_integer_section(section: str) -> float: """解析整数段(元之前的部分),如 壹仟陆佰伍拾 / 拾贰万零壹佰。""" if not section: return 0.0 if section == "零": return 0.0 total = 0.0 group_value = 0 number = 0 for ch in section: if ch in _DIGITS: number = _DIGITS[ch] elif ch in _SMALL_UNITS: unit = _SMALL_UNITS[ch] group_value += (number if number else 1) * unit number = 0 elif ch in _GROUP_UNITS: unit = _GROUP_UNITS[ch] group_value = (group_value + number) * unit total += group_value group_value = 0 number = 0 else: raise ValueError(f"非法大写字符: {ch}") total += group_value + number if total <= 0: raise ValueError("大写金额解析为 0") return total