1. 合同批量处理为什么总在“人肉翻页”里打转企业法务、财务、审计三个部门只要提到“历史合同归档”基本都会露出同一种表情。几百份 Word 合同堆在共享盘里格式五花八门有的甲乙方写在正文段落有的塞进表格有的金额用中文大写、有的用阿拉伯数字还有的合同编号藏在页眉。你要做的不是“读一遍”而是把合同编号、签约主体、金额、身份证号、手机号这些字段抽出来做成一张能筛选、能对账、能审计的台账。问题在于手工摘录的出错率极高。我见过一份 300 份合同的归档任务三个人做了两周最后核对时发现金额错位 17 处、甲乙方张冠李戴 9 处。更麻烦的是合规合同里带身份证号、银行账号、手机号一旦台账在部门间流转敏感信息就跟着扩散。所以真正要解决的不是“能不能读 Word”而是“能不能在本地、批量、可复现地把字段抽出来并且输出前就脱敏”。这篇就围绕这个场景用 Cursor 辅助写一套 Python 脚本基于 python-docx 读取 .docx 合同遍历段落和表格用正则表达式定位甲乙方、金额、身份证号、手机号、合同编号输出前做掩码脱敏最后落成 Excel 台账。脚本骨架可以直接复制正则规则可以按你的合同模板改验证动作也一并给出。适合法务运营、财务共享中心、审计岗以及想用 Cursor 提效但不想把合同传到第三方云解析 API 的同学。2. 前置准备TaoToken 与本地环境怎么配写脚本之前先把两件事定下来模型调用通道和本地依赖。Cursor 本身是编辑器它负责帮你补全和改写代码真正跑批量解析的是本地 Python 进程。如果你在 Cursor 里接的是云端模型做代码补全建议把 API 入口统一到 TaoToken这样模型对话、Coding Plan、API Keys 都在一个控制台里管理切换模型不用改一堆环境变量。TaoToken 的定位是模型调用聚合入口官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 。它不替代 Cursor也不替代你的 Python 运行时只是把模型请求收敛到一个可管理的 Key 上。对于合同解析这种“代码要反复调、正则要反复试”的任务用 Coding Plan 做长期编码辅助会比每次临时开对话更省心入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。本地环境按下面配组件版本建议作用Python3.10.x运行时正则和 docx 库兼容性稳python-docx0.8.11解析 .docx 的 OOXML 结构支持表格遍历pandas2.1.0结构化字段并导出 Excelopenpyxl3.1.2pandas 写 xlsx 的引擎Cursor最新版辅助写正则、补全函数、生成测试用例安装命令用 uv 或 pip 都行我习惯 uvuv add python-docx0.8.11 pandas2.1.0 openpyxl3.1.2如果你还没配模型 Key先去控制台建一个地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 然后在 API Keys 页面生成密钥入口 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 里面写了 base_url 和鉴权头的写法。配好之后Cursor 里做代码补全、让模型帮你解释一段正则都走这个通道。注意合同文件本身不要上传到任何云端解析服务。本文方案全程本地读取模型只用来辅助写代码不接触合同内容。3. 可复制配置解析脚本骨架与正则规则这一节是核心。脚本分四块生成模拟合同方便你无样本也能跑通、文本抽取段落表格、正则匹配、脱敏输出。先给完整骨架再拆正则规则。3.1 文本抽取段落和表格都要遍历python-docx 读 .docx 时doc.paragraphs只给正文段落表格内容在doc.tables里。很多合同的甲乙方、税号、电话恰恰在表格单元格中所以必须两边都抓拼成一个文本块再跑正则。# -*- coding: utf-8 -*- 文件名: contract_parser.py 描述: 解析 Word 合同抽取敏感字段并脱敏输出 Excel 台账 import os import re from docx import Document import pandas as pd def extract_all_text(file_path): 遍历段落与表格返回拼接后的全文 doc Document(file_path) lines [] # 1. 正文段落 for para in doc.paragraphs: text para.text.strip() if text: lines.append(text) # 2. 表格逐行逐单元格 for table in doc.tables: for row in table.rows: for cell in row.cells: cell_text cell.text.strip() if cell_text: lines.append(cell_text) return \n.join(lines)这里有个细节合并单元格在 python-docx 里会被重复读取同一个 cell 对象可能出现在多个 row 中。如果你发现税号被抽了两次可以在拼接前用set去重或者按cell._tc的 id 判重。我一般先不去重让正则用findall取第一个够用。3.2 正则规则配置把字段规则集中管理正则不要散落在代码里集中成一个字典改模板时只动这一块。下面这套规则覆盖合同编号、签约日期、甲乙方、金额、身份证号、手机号、税号。PATTERNS { # 合同编号HT-2026-668899 / 合同编号ABC123 contract_code: r合同编号[:\s]*([A-Za-z0-9\-]{6,30}), # 签约日期2026年06月22日 / 2026-06-22 sign_date: r签约(?:时间|日期)[:\s]*(\d{4}[年\-/]\d{1,2}[月\-/]\d{1,2}日?), # 甲方主体甲方杭州某某科技有限公司 party_a: r甲方(?:委托方)?[:\s]*([^\n\r,。;]{4,40}), # 乙方主体 party_b: r乙方(?:服务方)?[:\s]*([^\n\r,。;]{4,40}), # 金额人民币 500000.00 元 / 总额为 500,000.00 amount: r(?:总额为|合同金额|费用总额|服务费总额)[:\s]*人民币[\s]*([\d,]\.?\d*), # 身份证号18 位末位可能是 X id_card: r\b(\d{17}[\dXx])\b, # 手机号1 开头 11 位 phone: r\b(1[3-9]\d{9})\b, # 纳税人识别号15-20 位字母数字 tax_id: r纳税人识别号[:\s]*([A-Za-z0-9]{15,20}), }金额那条正则里我加了[\d,]因为很多合同写500,000.00带千分位逗号。抽出来之后要replace(,, )再转 float否则float(500,000.00)会报错。3.3 脱敏函数输出前先掩码脱敏要在写 Excel 之前做不能先落盘再处理。掩码规则按字段类型分def mask_value(value, kind): 按字段类型做掩码脱敏 if not value or value Unknown: return value if kind company: # 杭州艾维科技有限公司 - 杭州**有限公司 if len(value) 6: return value[:2] ** value[-4:] return value[:2] ** if kind phone: # 13858888888 - 138****8888 if len(value) 11: return value[:3] **** value[-4:] return value if kind id_card: # 保留前 6 后 4 if len(value) 15: return value[:6] ******** value[-4:] return value if kind tax_id: # 91330106MA2H12345X - 9133**********45X if len(value) 15: return value[:4] ********** value[-3:] return value return value身份证号脱敏保留前 6 位地区码和后 4 位中间 8 位打码这是常见的合规做法。手机号保留前 3 后 4。公司名保留前 2 后 4中间打码既能对账又不暴露全称。3.4 主流程批量扫描目录并导出def parse_contract(file_path): 解析单份合同返回字段字典 text extract_all_text(file_path) result {key: Unknown for key in PATTERNS} for key, pattern in PATTERNS.items(): match re.search(pattern, text) if match: result[key] match.group(1).strip() # 金额去掉千分位逗号 if result[amount] ! Unknown: result[amount] result[amount].replace(,, ) return result def run_batch(input_dir, output_xlsx): 批量解析目录下所有 docx脱敏后写 Excel files [ f for f in os.listdir(input_dir) if f.lower().endswith(.docx) and not f.startswith(~$) ] if not files: print([Warn] 目录下没有可解析的 .docx 文件) return records [] for filename in files: path os.path.join(input_dir, filename) print(f[Parsing] {filename}) try: raw parse_contract(path) except Exception as e: print(f[Error] {filename} 解析失败: {e}) continue records.append({ 文件名: filename, 合同编号: raw[contract_code], 签约日期: raw[sign_date], 合同金额: float(raw[amount]) if raw[amount] ! Unknown else 0.0, 甲方主体: mask_value(raw[party_a], company), 乙方主体: mask_value(raw[party_b], company), 甲方税号: mask_value(raw[tax_id], tax_id), 手机号: mask_value(raw[phone], phone), 身份证号: mask_value(raw[id_card], id_card), }) df pd.DataFrame(records) df.to_excel(output_xlsx, indexFalse, sheet_name合同台账) print(f[Done] 台账已导出: {output_xlsx}) if __name__ __main__: run_batch(./contracts_input, ./contracts_input/audit_summary.xlsx)~$开头的文件是 Word 打开时生成的临时锁文件必须排除否则会抛PackageNotFoundError。4. 验证请求跑通一份模拟合同看结果没有现成合同样本时先用 python-docx 生成一份带表格的模拟合同验证整条链路。from docx import Document from docx.shared import Pt def make_mock_contract(path): doc Document() doc.add_paragraph(技术开发与咨询服务合同) doc.add_paragraph(合同编号HT-2026-668899) doc.add_paragraph(签约时间2026年06月22日) doc.add_paragraph( 本合同项下研发服务费总额为人民币 500,000.00 元。 ) table doc.add_table(rows4, cols2) table.style Table Grid table.cell(0, 0).text 甲方杭州艾维科技有限公司 table.cell(0, 1).text 乙方贵州某某信息技术有限公司 table.cell(1, 0).text 纳税人识别号91330106MA2H12345X table.cell(1, 1).text 纳税人识别号91520382MA6D67890Y table.cell(2, 0).text 联系电话13858888888 table.cell(2, 1).text 联系电话18908519999 table.cell(3, 0).text 身份证号330106199001011234 table.cell(3, 1).text 身份证号520382199202024567 doc.save(path) print(f[Init] 模拟合同已生成: {path}) if __name__ __main__: os.makedirs(./contracts_input, exist_okTrue) make_mock_contract(./contracts_input/mock_001.docx) run_batch(./contracts_input, ./contracts_input/audit_summary.xlsx)运行命令uv run python contract_parser.py预期输出[Init] 模拟合同已生成: ./contracts_input/mock_001.docx [Parsing] mock_001.docx [Done] 台账已导出: ./contracts_input/audit_summary.xlsx打开audit_summary.xlsx你应该看到字段原始值脱敏后合同编号HT-2026-668899不脱敏合同金额500000.00500000.0甲方主体杭州艾维科技有限公司杭州**有限公司甲方税号91330106MA2H12345X9133**********45X手机号13858888888138****8888身份证号330106199001011234330106********1234如果金额列显示为 0.0说明金额正则没匹配上检查合同里金额的写法是不是“总额为人民币”之外的表述把新表述加进PATTERNS[amount]的(?:...)分组里。5. 本篇常见错排查5.1 PackageNotFoundError文件不是真正的 docx报错长这样docx.opc.exceptions.PackageNotFoundError: Package not found at contract.docx原因通常是两种路径写错或者文件其实是 .doc 旧格式被强行改了后缀。python-docx 只认 OOXML 的 zip 结构.doc 是二进制 OLE 格式读不了。解决办法是用 LibreOffice 批量转libreoffice --headless --convert-to docx *.doc转完再跑脚本。另外在批量循环里用 try/except 包住单文件解析避免一份坏文件让整批中断。5.2 表格内容抽不到只遍历了 paragraphs如果你发现甲乙方、税号全是 Unknown但正文里的合同编号能抽到基本是表格没遍历。检查extract_all_text里有没有doc.tables那段。还有一种情况是合同用了文本框text boxpython-docx 默认读不到文本框里的内容需要走 XML 层解析w:txbxContent这个场景较少遇到再单独处理。5.3 正则匹配到多余内容甲乙方抓到整段甲方[:\s]*([^\n\r,。;]{4,40})里我限制了终止字符如果合同写“甲方杭州某某科技有限公司以下简称甲方”会把括号也抓进去。可以在匹配后做一次清洗value re.split(r[(], result[party_a])[0].strip()把括号前的内容截出来。Cursor 里可以让模型帮你根据实际合同样本调这条正则把样本贴进去让它生成更精确的字符集。5.4 金额转 float 报错float(500,000.00)会抛ValueError。前面已经用replace(,, )处理了但如果你合同里金额带“元”字或空格还要再 strip。稳妥写法amount_str re.sub(r[^\d.], , raw[amount]) amount float(amount_str) if amount_str else 0.05.5 脱敏后无法对账脱敏是为了防泄露但财务对账时需要能区分不同主体。公司名保留前 2 后 4 通常够区分如果两家公司前 2 后 4 相同可以改成保留前 3 后 3或者额外加一列哈希值如hashlib.md5(全称.encode()).hexdigest()[:8]用哈希做唯一标识既不可逆又能对账。6. 把脚本接进你的工作流脚本跑通只是第一步。实际落地时我建议把PATTERNS抽成独立的patterns.py不同合同模板配不同规则文件主流程不变。批量任务用concurrent.futures.ThreadPoolExecutor并发解析IO 密集场景能快 3 到 5 倍但注意 Excel 写入要加锁或最后统一写。如果你在 Cursor 里继续迭代这套脚本比如加 OCR 处理扫描件、加银行账号抽取、加合同到期提醒可以把模型调用统一走 TaoToken 的 Coding Plan长期编码场景下比零散开对话更连贯入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。需要临时验证某段正则或让模型解释报错用模型对话页面就行地址 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。接入细节和鉴权头写法看文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content Key 在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 管理。最后提醒一句生成的台账 xlsx 落盘后记得设只读权限或者直接写进带 RBAC 的数据库。脱敏解决的是“看到什么”权限解决的是“谁能看”两层都要有。