用WorkBuddy实现一键提取出口报关单

天璃雪

财会 Python WorkBuddy
# 用WorkBuddy实现一键提取出口报关单 ## 一、背景 外贸财务经常需要通过每份报关单的关键信息,收货人、合同号,以及每一项商品的型号、数量、单价、总价与发票进行逐一匹配,并将发票一一匹配上出口报关单的海关编号。 本文运用WorkBuddy,只告知了需要提取报关单对应内容的需求,很快就写好了 Python 脚本,主要使用PyPDF2+正则表达式,只要200 行代码即可自动完成全部提取工作。 本文内容较长,只需要代码的可以直接划到末尾~~ --- ## 二、PDF 结构分析 拿到 PDF 之后,第一步不是写代码,而是搞清楚文档内部到底是什么,页面构成、字段分布、排版陷阱。 ### 2.1 一份报关单 PDF 里到底有什么? 我给的报关单 PDF 并非只有一张报关单表格,而是由多份不同性质的文件合并而成。以最常见的 **5 页**结构为例: ``` 第 1 页 → 出口货物报关单(1/2)——含头部信息 + 部分商品项目 第 2 页 → 出口货物报关单(2/2)——续接商品项目,直到完税信息 第 3 页 → 通关无纸化出口放行通知书 ← 不需要 第 4 页 → 委托报关协议 ← 不需要 第 5 页 → 委托报关协议通用条款 ← 不需要 ``` 还有 **4 页**的版本(报关单 1 页 + 放行通知书 + 委托协议 + 通用条款)。 > **关键**:不同报关代理拼的 PDF,页数可能不同(4 或 5 页),但规律是稳定的——**只有前 1~2 页是报关单本体,后面全是无关附件**。 ### 2.2 报关单页面的文本布局 用 PyPDF2 把第 1 页文本提取出来(原始顺序,未整理),核心结构如下: ``` *5304XXXXXXXXXXXXXX* ← 海关编号 10000个 ← 商品 6 的数量(倒序排列!) 1.2千克 ← 商品 6 的净重 10000个25000个 ← 商品 5/6 的数量 ... 155000个1100个 ← 商品 2/1 的数量 1.9千克 ← 商品 1 的净重 1100个征免 境内货源地 ... ← 数量表尾部 + 表头行 中华人民共和国海关出口货物报关单 ← 正式标题 页码/页数: 1/2 (深圳湾关) 海关编号: 预录入编号: 5304XXXXXXXXXXXXXX ... ← 编号区 ... 出境关别(5345) XXXX公司境内发货人 ← 发货人 ... XX Limited境外收货人 ← 收货人(换行截断!) ... 合同协议号 ← 头部结束 8542319099 控制器用集成电路 ← 商品 1 起 ...XX牌|XX型非量产3.3820 3720.20美元中国台湾(TWN)... ... 8542319099 控制器用集成电路 ← 商品 2 ...牌|XX型|量产|中4.2030 651465.00美元... ``` ### 2.3 PDF 中问题 **文本顺序颠倒。** PDF 文本提取的顺序是从页面底部到顶部:商品数量、重量在最前面,而商品详情(型号、单价、总价)反而在后面。不能按行号顺序直接解析。 **属性分散在页面不同位置。** 每一件商品的信息并不聚在同一个"块"里:数量和重量被集中在页面顶部的数量表区域(按照 `N个 → M千克` 的配对从底向上排列),而型号、单价、总价则散落在页面下半部分的商品明细区。 ### 2.4 总结 PDF 结构 > 报关单 PDF文件包含: 1~2 页报关单本体(含头部 + 商品) + 1 页放行通知书 + 2 页委托协议。 > 商品信息被拆成两部分分散在两页的不同区域,数量/重量要反向配对,型号/单价/总价要兼容多种格式。 --- ## 三、输出样式 在动笔写代码之前,需要先定义清楚"我要输出什么",我只需要以下几个关键内容。 Excel 格式:**头部信息 + 商品明细合并在同一行,每个商品占一行**。 | 海关编号 | 境外收货人 | 出口日期 | 申报日期 | 合同协议号 | 项号 | 商品编号 | 型号 | 是否量产 | 数量 | 单位(重量) | 单价 | 总价(美元) | | -------- | ---------- | -------- | -------- | ---------- | ---- | -------- | ---- | -------- | ---- | ---------- | ---- | ---------- | 脚本处理完 Customs 目录下所有 PDF 后,所有数据合并到一个 Excel 中,无需手动复制粘贴。 --- ## 四、代码设计 整个脚本分为 **6 个模块**,每个解决一个独立问题。 下面是逐一拆解,说明为什么这样设计。 ### 4.1 环境兼容性:`sys.stdout` 编码 ```python if hasattr(sys.stdout, 'buffer'): sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') ``` PDF 中的中文(如"控制器用集成电路""香港")在某些 IDE(IDLE、Spyder)中打印时可能乱码。这行代码让命令行环境下强制 UTF-8 输出,同时用 `hasattr` 做防御——在 IDE 环境跳过,避免 `'StdOutputFile' object has no attribute 'buffer'` 报错。 ### 4.2 模块一:页面识别与文本提取 **设计思路**:不是机械地取前两页,而是**按内容特征判断每页的角色**。 ```python def is_customs_page(text): """判断是否为报关单页面""" if not text or not text.strip(): return False # 排除放行通知书——关键字"放行通知书""兹申明" if '放行通知书' in text or '兹申明' in text or '(签印)兹申明' in text: return False # 排除委托报关协议——关键字"委托报关协议""通用条款" if '委托报关协议' in text or '中国报关协会监制' in text or '通用条款' in text: return False return True ``` **为什么用关键字而不是硬编码页码?** 因为不同代理拼的 PDF,报关单可能在第 1 页、第 1+2 页。枚举关键字比猜页码更稳。 ```python def extract_customs_text(pdf_path): """提取所有报关单页→合并文本 + 逐页文本列表""" reader = PyPDF2.PdfReader(pdf_path) pages = [] for page in reader.pages: t = page.extract_text() or '' if is_customs_page(t): pages.append(t) return '\n'.join(pages), pages # 返回两个值:合并文本(给正则解析用) + 逐页文本(给数量/重量提取用) ``` **为什么返回两个值?** 商品型号、单价、总价是全文正则匹配,不需要分页;但数量/重量是逐页按行扫描提取的(详见 4.3),必须保留原文的行结构,不能合并。 ### 4.3 模块二:头部信息解析 **设计思路**:把多行文本拍平成一个字符串,然后用正则按"锚点"定位每个字段。 ```python def parse_header(text): flat = ' '.join(text.split()) # 把所有空白字符替换为单个空格 h = {} # 海关编号 = 预录入编号 = 18位数字 m = re.search(r'预录入编号[::\s]*(\d{18})', flat) h['海关编号'] = m.group(1) if m else '' # 境外收货人 = "运输方式(N)"之后、"境外收货人"之前的文本 m = re.search(r'运输方式\(\d+\)\s*(.+?)境外收货人', flat) h['境外收货人'] = m.group(1).strip() if m else '' # 出口日期 = 紧贴"出口日期"之前的8位数字 m = re.search(r'(\d{8})出口日期', flat) h['出口日期'] = m.group(1) if m else '' # 申报日期 = 紧贴"申报日期"之前的8位数字 m = re.search(r'(\d{8})申报日期', flat) h['申报日期'] = m.group(1) if m else '' # 合同协议号 = "贸易国(地区)(XXX)"之后、"合同协议号"之前的文本 m = re.search(r'贸易国(地区)\s*\(\w+\)\s*(.+?)合同协议号', flat) h['合同协议号'] = m.group(1).strip() if m else '' return h ``` **核心技巧**:不去解析 PDF 的二维表格坐标,用**固定标签("境外收货人""出口日期"等)作为"锚点"**,通过它前后的已知文本做边界定位。例如"境外收货人"前面一定有"运输方式(N)",这段文字本身就是收货人的名字。它在 PDF 文本中就是一个连续的 span,只是被换行打断了。 > `.+?`(非贪婪匹配),保证只取到"境外收货人"之前。 ### 4.4 模块三:数量/重量提取 这是整个脚本中逻辑最复杂的模块,因为它要处理 PDF 文本顺序和实际表格顺序的颠倒。 #### 原始文本长什么样(以第 1 页为例) ``` 10000个 ← 按页面底部→顶部的顺序排列! 1.2千克 ← 商品 6 的净重 10000个25000个 4.4千克 ← 商品 5 的净重 ... 155000个1100个 1.9千克 ← 商品 1 的净重 1100个征免 ... ← 尾部 + 表头 ``` 规律: 1. 每个商品的"数量(个)"和"净重(千克)"是**一对一配对**的 2. PDF 从页面底部向顶部提取,所以排列是**倒序**的(商品 1 在最下面) 3. 同一行可能有多个数量值(`10000个25000个` 表示两项商品的数量),取最后一个 #### 解析逻辑 ```python def extract_qty_weights(page_text): lines = page_text.split('\n') raw = [] for line in lines: s = line.strip() # ① 匹配纯重量行: 如 "1.9千克" kg_m = re.match(r'^([\d.]+)\s*千克$', s) if kg_m: raw.append(('w', kg_m.group(1))) continue # ② 匹配数量行: 如 "155000个1100个" → 取最后一个 "1100" if re.match(r'^[\d]+\s*个', s) and '千克' not in s \ and '征免' not in s and '境内' not in s: nums = re.findall(r'(\d+)\s*个', s) if nums: raw.append(('q', nums[-1])) continue # ③ 一对一配对(数量 + 重量) pairs = [] pending = None for typ, val in raw: if typ == 'q': pending = val elif typ == 'w' and pending: pairs.append((pending, val)) pending = None # ④ 反转:PDF是倒序,翻转回正向 return list(reversed(pairs)) ``` ### 4.5 模块四:商品项目解析:正则兼容 6 种格式变体 商品明细的写法因代理而异(见 2.3 节的格式对比表)。 #### 第一步:定位每个商品的起始位置 ```python # 找所有 "项号 + 商品编号" 起始块 starts = list(re.finditer(r'(?
0.0000 1 0 踩 关注作者 收藏 2026-07-01   阅读量: 1934

评论(0)


暂无数据

博客推荐