Python提取PDF中上标格式小数金额并保留负号的稳健方案
问题描述
我用Python处理一批PDF文件,需从中提取金额数据写入Excel。PDF里的金额小数以逗号跟上标数字形式呈现(如310,⁷⁶⁹⁷、−25,³⁴),实际为普通货币金额而非指数,要转换成310.7697、-25.34。
当前使用工具:
- PyMuPDF(
fitz)读取PDF文本,必要时用Tesseract OCR兜底 - 正则表达式+字符串清洗提取金额
- openpyxl写入Excel
现存问题:
- 小数转换不稳定,比如
310,⁷⁶⁹⁷有时会变成310.0或3107697,而非预期的310.7697 - 负数金额偶尔丢失负号,比如
−25,³⁴变成正数25,34
预期结果:
解析后得到浮点值[310.7697, -25.34],写入Excel后显示为:
310,7697 + -25,3400 总计:285,4297
解决方案
针对核心问题,通过优化字符处理逻辑、正则匹配和错误修复,实现稳健的金额解析:
关键修复点
- 修复函数拼写错误(
amouts→amounts),避免执行报错 - 改用最后一个逗号/点作为小数分隔符,规避千分位分隔符干扰
- 提前捕获负号状态再移除所有负号字符,避免后续处理时丢失负号
- 补全全角、罕见上标字符映射,覆盖OCR可能识别的所有上标类型
- 预处理时统一移除空格,清理无效字符,确保数字提取准确
- 扩展正则的上标字符范围,确保金额部分完整捕获
修改后完整代码
import re def clean_and_convert_amount(amount_str: str) -> float: """ 清洗带小数上标的金额字符串(如'310,⁷⁶⁹⁷'、'−25,³⁴'),转换为浮点值,保留负号和小数精度 """ if not amount_str: return 0.0 raw = str(amount_str).strip() # 1. 处理负号:先标记是否为负数,再移除所有负号字符避免干扰后续处理 minus_chars = {'−', '–', '-'} is_negative = any(char in raw for char in minus_chars) raw = ''.join([c for c in raw if c not in minus_chars]) # 2. 上标数字映射:覆盖所有可能的上标字符 superscript_map = { # 基础上标数字 '⁰': '0', '¹': '1', '²': '2', '³': '3', '⁴': '4', '⁵': '5', '⁶': '6', '⁷': '7', '⁸': '8', '⁹': '9', # 替代编码上标 '\u00B9': '1', '\u00B2': '2', '\u00B3': '3', # 全角上标数字 '₀': '0', '₁': '1', '₂': '2', '₃': '3', '₄': '4', '₅': '5', '₆': '6', '₇': '7', '₈': '8', '₉': '9', # 罕见上标变体 'ⁱ': '1', '': '2', '': '3' } def normalize_char(c: str) -> str: if c in superscript_map: return superscript_map[c] if c.isdigit(): return c if c in {',', '.'}: return c return '' # 3. 预处理:清洗字符、移除空格 processed = ''.join([normalize_char(c) for c in raw]).replace(' ', '') # 4. 定位小数分隔符:取最后一个逗号/点,避免千分位干扰 comma_pos = processed.rfind(',') dot_pos = processed.rfind('.') sep_pos = -1 if comma_pos != -1 and dot_pos != -1: sep_pos = max(comma_pos, dot_pos) elif comma_pos != -1: sep_pos = comma_pos elif dot_pos != -1: sep_pos = dot_pos # 5. 分割整数和小数部分 if sep_pos == -1: integer_part = processed.strip() or '0' decimal_part = '0' else: integer_part = processed[:sep_pos].strip() or '0' decimal_part = processed[sep_pos+1:].strip() or '0' # 确保仅保留数字 integer_part = ''.join([c for c in integer_part if c.isdigit()]) or '0' decimal_part = ''.join([c for c in decimal_part if c.isdigit()]) or '0' # 6. 转换为浮点值 try: value = float(f"{integer_part}.{decimal_part}") except ValueError: return 0.0 return -value if is_negative else value def extract_amounts_from_period(text: str) -> list[float]: """ 从'din perioada ... în cuantum de <amount>'格式的句子中提取金额,排除包含'a fost acoperita suma de'的上下文 """ if not text: return [] # 优化正则:覆盖所有上标字符,确保负号捕获 pattern = re.compile( r"din\s+perioada\s+[^\n\r]+\s*(?:\n|\r\n)?\s*(?:în|in)\s+cuantum\s+de?\s*" r"([-−–]?\s*[0-9\s,.⁰¹²³⁴⁵⁶⁷⁸⁹\u00B9\u00B2\u00B3₀₁₂₃₄₅₆₇₈₉]+)", flags=re.IGNORECASE ) lower_text = text.lower() amounts = [] for match in pattern.finditer(text): amount_str = match.group(1) start_pos = match.start(1) # 跳过已覆盖金额的上下文 context_window = lower_text[max(0, start_pos - 150):start_pos] if "a fost acoperita suma de" in context_window or "a fost acoperită suma de" in context_window: continue amounts.append(clean_and_convert_amount(amount_str)) return amounts # 修复拼写错误 # 测试示例 sample_text = """ din perioada ianuarie-februarie 2024 în cuantum de 310,⁷⁶⁹⁷ din perioada martie-aprilie 2024 în cuantum de −25,³⁴ din perioada mai-iunie 2024 în cuantum de 1 068,²⁷⁵⁶ """ extracted = extract_amounts_from_period(sample_text) print(f"提取金额:{extracted}") # 输出:[310.7697, -25.34, 1068.2756] # 准备Excel写入内容 amounts_str = " + ".join(f"{v:.4f}".replace(".", ",") for v in extracted) total_str = f"{sum(extracted):.4f}".replace(".", ",") print(f"Excel显示字符串:{amounts_str}") print(f"总计:{total_str}")
内容的提问来源于stack exchange,提问作者Larisa Palimaru
相关产品推荐
相关产品推荐

