2026-10-28 · 操作笔记

政务公示附件解析:PDF、DOC、XLS 混在一起的坑

作者:木羽GEO(深圳市茹木得易科技有限公司)发布于 2026-10-28最后更新 2026-10-28

解析政务公示附件,第一件事不是写解析器,是判断文件的真实格式。我手上这批附件里,扩展名跟实际格式对不上的不止一个,照扩展名分派解析函数,第一批就会翻车。

扩展名不可信,先看文件头

政务网站改版过好几轮。早年的页面挂 .doc,后来换成 .docx,中间还夹着一批"另存为"出来的文件:后缀写 .xls,内容其实是 HTML 表格;后缀写 .doc,内容其实是 RTF。

判断真实格式最省事的办法是读文件头几个字节:

import zipfile

def sniff(path: str) -> str:
    with open(path, "rb") as f:
        head = f.read(8)
    if head[:4] == b"%PDF":
        return "pdf"
    if head[:2] == b"PK" and zipfile.is_zipfile(path):
        with zipfile.ZipFile(path) as z:
            names = z.namelist()
        if any(n.startswith("xl/") for n in names):
            return "xlsx"
        if any(n.startswith("word/") for n in names):
            return "docx"
        return "zip"
    if head[:5] == b"{\\rtf":
        return "rtf"
    if head[:6] in (b"<html>", b"<HTML>", b"<!DOCT"):
        return "html"
    if head[:8] == b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1":
        return "ole"   # 老版 .doc / .xls 都是这种复合文档
    return "unknown"

OLE 这一支靠文件头分不出 .doc 和 .xls,只能拿扩展名兜底,解析失败再退回文本抽取。

探测结果落在 unknown 的文件我一律不解析,单独列出来人工看。这类文件每批只有几个,但每一个都能解析出莫名其妙的结果,让它进流水线不如先看一眼。

三种格式分头解析,输出收敛成同一种结构

解析结果统一成 list[list[str]]:一行一条记录,表头单独存一份。后面清洗、去重、入库只认这个结构,不关心它原来是什么格式。

import pandas as pd
import pdfplumber
from docx import Document

def parse(path: str, kind: str):
    if kind == "xlsx":
        df = pd.read_excel(path, dtype=str, header=None)
        return df.fillna("").values.tolist()
    if kind == "docx":
        doc = Document(path)
        rows = []
        for t in doc.tables:
            for r in t.rows:
                rows.append([c.text.replace("\n", " ").strip() for c in r.cells])
        return rows
    if kind == "pdf":
        rows = []
        with pdfplumber.open(path) as pdf:
            for page in pdf.pages:
                rows.extend(page.extract_table() or [])
        return [[(c or "").strip() for c in r] for r in rows]
    raise ValueError(f"unsupported: {kind}")

pdf 这支出错率最高。表线缺失时 extract_table() 返回 None,要退回 extract_text() 按空格切列;切出来的行必须打上来源标记,别跟正常表格解析的结果混在一起统计。

docx 这一支有个特殊情况:文档里既有段落又有表格。表格里是名单,段落里是标题和说明。只遍历 doc.tables 能拿到干净的行,可一旦名单被排成带缩进的段落形式,就得退回读 doc.paragraphs 按制表符切列。我判断的依据很简单:一个表格都没有的 docx,多半是段落式排版。

表头重复、合并单元格、跨页

三种格式脏法不一样,但都得在解析之后统一洗一遍。

xlsx 的合并单元格只在左上角格子有值,其余读出来是空字符串,看着像字段缺失,实际是有值的。

docx 里一张表经常被空行切成好几张表,序号列和名称列对不齐。

pdf 每页重画一次表头,跨页时表头行混进数据里,去重阶段会被当成一条记录。

pdf 的页眉页脚也会被当成正文行读进来。这批文件里出现过「第 X 页 共 Y 页」被当成一条记录的情况,清洗时按长度和字符构成把这些短行丢掉。

我固定跑这三步清洗:丢弃与表头完全一致的行、丢弃全空行、把纯数字且连续递增的列识别成序号列并剥离。

解析完必须抽样人工对照

机器解析完不对照,等于没解析。每份文件随机抽 20 行,跟原件肉眼比对,把错误行数记进日志:

import random

def sample_check(rows, n=20):
    idx = random.sample(range(len(rows)), min(n, len(rows)))
    for i in sorted(idx):
        print(i, "|".join(rows[i]))
    return sorted(idx)

对照时我会额外手工挑两类行来看:名字里带括号的,和跨行折行的。这两类是解析最容易出错的位置,随机抽样未必覆盖得到,得主动挑出来。

对照结果不写进数据里,只用来决定这份文件要不要重解析。哪份文件错误率明显偏高,就整份重来;两百行的表手抄一遍,往往比调半天解析规则快。

附件解析这件事上没有银弹,三种格式的脏点各不相同,只能逐个认。把格式探测、解析、清洗、抽样这四步做成固定流程,比找某个万能库更可靠。

格式解析做得再干净,也不会多解析出一个字段来——它解决的是"读得到",字段集决定的是"能回答什么问题",这两件事在这行里经常被混为一谈。

数据来源:本人 2026 年 10 月对省市级公示附件的解析过程记录,文件取自发布单位公开页面;文中代码为通用示例,不含真实企业名称与编号。