ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PDF表格提取实践:以移动通信半年报为例的完整解析流程

PDF表格提取实践:以移动通信半年报为例的完整解析流程 简介《2022年H1中国移动通信消费市场研究报告》是一份面向通信行业从业者、政策研究者及企业决策者的深度分析资料聚焦上半年中国移动通信市场的整体态势与细分赛道变化。报告围绕市场规模与用户消费习惯、5G网络建设及应用场景、用户行为画像、AI与大数据等新兴技术影响、运营商竞争格局、政策法规及消费者信任度等核心议题展开既呈现5G用户增长、基站覆盖、数据流量消耗等量化数据也剖析物联网、远程医疗、智慧城市等融合应用案例并为下半年趋势预判提供了数据支撑。包内文件共1个为PDF格式文档压缩包大小约3.99MB便于下载后直接阅读或存档备查。目前已有119人学习浏览适合需要快速把握市场动向、撰写行业分析或制定业务策略的读者参考尤其对关注5G落地、用户运营和监管环境变化的人群更具实用价值。1. 打开之前它是一份报告打开之后它应该是一组数据打开之前它是一份报告打开之后它应该是一组数据。2022年H1中国移动通信消费市场研究报告.pdf 这类文件的常态是前面二十页讲行业走势后面大几十页全是运营商用户规模、流量消耗、ARPU 和收入明细表。做数据分析的人拿到手第一反应不是从头翻而是想怎么把里面的表格批量抠出来跑一遍同比环比。直接复制粘贴是灾难多级表头错位、跨页表头重复、竖排指标文字混进行列半小时只整理出三行能用的数。所以这份文件真正的工程价值不在阅读在解析链路是否可控。下面按我处理同类半年报的习惯把结构判断、工具选型、提取参数和最终归档一次讲透。2. 先判断报告是文本型还是扫描型再决定解析路线2.1 文本型与扫描型怎么判断先跑一遍字符数统计PDF 的解析难度不取决于文件大小取决于文字是不是真文字。真正由排版工具导出的报告页面里存的是字符对象带字体和编码信息解析库可以直接读出文本如果报告是打印后扫描再合成的每一页只是一张图片文本提取出来是空字符串。一份2022年H1中国移动通信消费市场研究报告.pdf往往不是单一类型券商版本多数是文本型而转发过几手的扫描版本会在中间夹着若干纯图页需要分开处理。判断方法不靠肉眼靠脚本。我的习惯是先统计每页的可见字符量低于阈值的那几页再单独抽查。import fitz # PyMuPDF pdf_path 2022年H1中国移动通信消费市场研究报告.pdf doc fitz.open(pdf_path) print(f总页数: {doc.page_count} 页) for i in range(doc.page_count): text doc[i].get_text(text).strip() if len(text) 50: # 只打印需要关注的页面减少刷屏 print(f第{i1:3}页: {len(text):5}字符 - 疑似扫描/图片页) doc.close()这段脚本用 PyMuPDF 打开报告逐页调用 get_text() 读取文字对象。len(text) 小于 50 的页面基本可以判定为没有文字层后续要么走 OCR要么整页跳过。需要注意 get_text(text) 返回的是纯文本不包含坐标信息如果后面要按区域裁剪可以换成 get_text(words)它返回每个词的坐标矩形这个特性在第四节的区域提取中会用到。提示如果 PDF 打开时需要输入口令解析库同样无法直接读取内容先用 pypdf 的 decrypt() 解开口令再继续处理。2.2 解析工具怎么选pdfplumber 主力PyMuPDF 辅助camelot 慎用确定类型后选工具。常见的 PDF 解析库各有侧重我按用在报告上的实际效果排了个表工具定位报告场景表现明显短板pdfplumber基于 pdfminer.six 的解析封装文本坐标、表格线识别、区域裁剪都能做提取表格最稳页数多时速度一般百页报告大约十几秒PyMuPDF (fitz)高性能后端批量读文本极快适合先做类型判断和页码定位表格提取能力弱基本要靠手写规则camelot视觉化表格识别对清晰横竖线的表格提取效果好遇到底纹、跨页和合并单元格容易崩pypdfPDF 基础操作合并、拆分、加解密、页序调整不做内容级提取PaddleOCROCR 引擎扫描页转文本中文支持好需要装模型CPU 下速度慢我的选型结论很固定文本型页面用 pdfplumber 提取文本和表格PyMuPDF 只用来做快速翻页和渲染图片camelot 看起来快但对报告类页面适应性差——市场报告里大量使用色块底纹、指标注释框、跨页长表camelot 的直线检测容易被底纹干扰。这里可以先记住结论第四节会展开讲对应的参数调整。另外直接把 PDF 转成 Word 再复制并不可取转 Word 只是把排版问题换成了另一种格式兼容问题单元格合并、竖排文本框照样错位并不会省事。2.3 锁定数据页在哪关键词扫描加目录页定位一份 90 多页的半年报不是每页都要进提取流程。我一般先跑一遍关键词定位把含移动电话用户DOUARPU业务收入5G套餐的页面筛出来再决定哪些页做精细提取。import pdfplumber keywords [移动电话用户, DOU, ARPU, 业务收入, 5G套餐] hits {} with pdfplumber.open(pdf_path) as pdf: for page in pdf.pages: text page.extract_text(x_tolerance1.5) or matched [kw for kw in keywords if kw in text] if matched: hits[page.page_number] matched for page_no, kws in hits.items(): print(f第{page_no:3}页: {, .join(kws)})这段脚本把关键词命中的页码和命中词打出来下一步直接对着页码做区域提取。x_tolerance1.5 的作用是让相邻字符更积极地合并成词避免排版稀疏的页面把移动电话拆成移 动 电 话。关键词列表要按报告的实际用词改比如有的报告写流量而不是DOU宁可多写几个词不要漏页。如果报告带书签目录pdfplumber 的 outline 属性可以直接读出章节标题和页码建立章节名 - 起始页的映射提取结果就能按章节切分后续归档到知识库时直接引用章节名而不是页码。3. 用 pdfplumber 把报告正文和表格一次性提取成 Markdown 和 CSV3.1 环境准备一个干净的解析环境报告解析依赖一堆 Python 包我习惯先在项目目录下建独立虚拟环境避免污染系统 Python。mkdir -p report_parser cd report_parser python3 -m venv .venv source .venv/bin/activate pip install pdfplumber pandaspdfplumber 会连带安装 pdfminer.six 和 Pillowpandas 用于把表格变成 DataFrame 再落盘。执行完成后用 python -c import pdfplumber; print(pdfplumber.version) 确认版本不同版本的 extract_tables 接口签名基本一致参数行为差别不大按下面写法都能跑。3.2 提取全文保留页码标记的 Markdown 文件文本提取是第一步。下面的脚本逐页提取文本写入一个带页码注释的 Markdown 文件from pathlib import Path import pdfplumber SRC Path(2022年H1中国移动通信消费市场研究报告.pdf) OUT Path(./output) OUT.mkdir(exist_okTrue) md_parts [] with pdfplumber.open(SRC) as pdf: for page in pdf.pages: # x_tolerance 控制横向字符合并距离 # y_tolerance 控制纵向行合并距离报告中行距普遍偏小 text page.extract_text(x_tolerance1.5, y_tolerance3) md_parts.append(f\n!-- page {page.page_number} --\n) md_parts.append(text if text else [本页无可提取文本]) print(f提取完成共 {pdf.page_count} 页输出到 output/report.md) Path(OUT / report.md).write_text(\n.join(md_parts), encodingutf-8)文本从 page.extract_text() 返回的是按页面内容流顺序拼接的字符串但阅读顺序严格说是按页面对象里的流顺序遇到多栏排版可能错乱。市场报告通常是单栏排版问题不大如果遇到双栏页可以改用 page.columns 或手动裁剪左右区域分别提取。参数说明x_tolerance1.5 是字符横向合并阈值单位是 PDF 的图形单位约 1/72 英寸。报告中如果字符间距被拉大——常见于数字对齐排版——默认值 3 会让同一词被拆断调低到 1.5 更稳定y_tolerance3 是行合并阈值值太大会把两行文字并成一行。这两个参数是提取质量的第一道关口抄代码时建议保留。3.3 提取表格从 PDF 原生表格到 DataFrame正文提取之后是重头戏。extract_tables() 返回的是页面表格列表每个表格又是一个由 cell 组成的二维列表。cell 本身可能是字符串、空字符串或 None——合并单元格在非起始位置就是 None需要先补空再进 DataFrame。import pandas as pd import pdfplumber from pathlib import Path SRC Path(2022年H1中国移动通信消费市场研究报告.pdf) OUT Path(./output) OUT.mkdir(exist_okTrue) all_rows [] with pdfplumber.open(SRC) as pdf: for page in pdf.pages: tables page.extract_tables() for table in tables: for row in table: cleaned [] for cell in row: if cell is None: cleaned.append() else: # 去掉单元格内部的换行保留为单行文本 cleaned.append(str(cell).replace(\n, )) all_rows.append(cleaned) # 当表格列数不一致时pandas 会自动用 NaN 补齐 df pd.DataFrame(all_rows) df df.dropna(howall).dropna(axiscolumns, howall) df.to_csv(OUT / report_tables.csv, indexFalse, encodingutf-8-sig) print(f共提取 {len(df)} 行表格数据已写入 output/report_tables.csv)这里有三个容易踩的细节。第一extract_tables() 返回的单元格类型不统一统一 str() 并替换内部换行否则 CSV 里的单元格会包含换行符影响后续导入数据库或 Excel。第二dropna(howall) 删掉全空行这类行通常来自表格线检测误差产生的空行dropna(axiscolumns, howall) 删掉全空列来源是跨页表格在一侧多画了一条竖线。第三CSV 编码用 utf-8-sig让 Excel 直接双击能正确识别中文避免乱码。提示如果报告里某个表格横跨两页pdfplumber 默认会把它们识别成两个独立的 Table 对象行会被拦腰截断。处理方案在第四节讲不要在 DataFrame 阶段手工拼接。3.4 字段名映射报告原文到结构化列名提取出来的 CSV 表头还是 PDF 里的原始表头比如移动电话用户数比上年末净增。入库前我习惯建一张映射表统一列名报告原文表述统一列名演示值说明移动电话用户数mobile_users16.78 亿户全国合计移动互联网接入流量mobile_internet_flow1246 亿GB上半年累计当月DOUdou_gb14.89 GB6月单月人均5G套餐用户数user_5g9.50 亿户三家运营商合计通信业务收入telecom_revenue8158 亿元上半年累计建映射表的价值是让后续所有同比环比脚本只认统一列名换下一份报告时只改映射表不改分析代码。演示值不是原报告数据只是说明字段该填什么格式、什么单位。4. 表格提取参数这次调线检测阈值、竖排文字与扫描页纠偏4.1 extract_tables() 的核心参数表pdfplumber 的表格识别本质是找线、找交点、定单元格这六个参数控制的都是同一件事怎么把线条和文字判定成单元格的边界。下面是半年报场景里我常用的参数组合默认值是 pdfplumber 的出厂设置参数默认值作用报告场景建议vertical_strategylines竖线来源lines 只认矢量线text 按文字边缘推断有表格线时保持 lines无边框表改 texthorizontal_strategylines横线来源同上同上line_scale10线检测缩放越小越敏感9过滤掉浅色辅助线snap_tolerance2线段端点吸附到同一坐标的容差3适配不齐的表头线text_tolerance2文字与线框距离容差3适配文字贴线较近的单元格x_tolerance3字符横向合并距离1.5数字密集表格具体到移动通信半年报我碰到最多的是这两种情况表格有完整直线但线的颜色很浅默认 line_scale10 会把浅色线识别成噪声这时候把 line_scale 调到 9线检测的敏感度提高浅色横竖线就能被收录另一种是多级表头上有贯穿整页的顶线顶线与单元格竖线的端点不在同一坐标需要把 snap_tolerance 从 2 提到 3端点吸附半径变大表格轮廓才能闭合。4.2 竖排文字怎么处理用 upright 字段过滤报告里的指标说明经常竖排书写比如页面右侧写单位万户字符方向是旋转过的。竖排文字会影响 extract_tables() 对行高的判断会直接把表格撑高。处理方式是先用 extract_words() 拿到每个词的属性筛掉竖排词再进入表格识别流程import pdfplumber with pdfplumber.open(pdf_path) as pdf: page pdf.pages[12] # 假设第13页表格有竖排干扰 words page.extract_words(x_tolerance1.5, y_tolerance3) vertical_words [w for w in words if not w[upright]] print(竖排文字示例:, [w[text] for w in vertical_words][:10]) table page.extract_table( vertical_strategytext, horizontal_strategylines, text_tolerance3, )pdfplumber 在 extract_words() 里给每个词算了一个 upright 布尔值旋转过的文字 upright 为 False。先把竖排词打印出来确认位置如果竖排内容集中在页面边缘就用 page.crop(bbox) 把那一侧裁掉再调 extract_table如果竖排在表格内部搅在一起基本可以判断这个表其实是文本框和表格的混排需要单独裁单元格区域逐块提取没有全局自动解法。4.3 扫描版页面先纠偏再 OCR再进表格流程部分半年报会在附录里夹扫描页典型的特征是底色发灰、文字边缘有锯齿、前面字符数统计中疑似扫描的那些页。直接对扫描页跑 OCR识别率受倾斜角影响很大歪 2 度以上表格线就断得一塌糊涂也就是检索里常说的PDF 歪斜校正纠偏、漂白加深清晰这类操作它们都要放在 OCR 之前做而不是之后。用 OpenCV 的 HoughLinesP 估计倾斜角再旋转是常见做法import cv2 import numpy as np # gray: 由 PyMuPDF 渲染页面后转成的灰度图 (W x H, uint8) binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1] lines cv2.HoughLinesP( binary, 1, np.pi / 180, threshold200, minLineLength300, maxLineGap20 ) angles [] if lines is not None: for line in lines: x1, y1, x2, y2 line[0] angle np.degrees(np.arctan2(y2 - y1, x2 - x1)) # 把角度归一化到 [-45, 45]避免横竖线角度漂移影响中位数 if angle 45: angle - 90 elif angle -45: angle 90 angles.append(angle) if angles: skew np.median(angles) (h, w) gray.shape[:2] matrix cv2.getRotationMatrix2D((w / 2, h / 2), skew, 1.0) corrected cv2.warpAffine(gray, matrix, (w, h), flagscv2.INTER_CUBIC, borderModecv2.BORDER_REPLICATE) else: print(未检测到足够直线保持原图)旋转完成后把 corrected 交给 OCR 引擎我常用 PaddleOCR 的 PP-OCRv4 中文模型出文本再走 3.2 的文本流程。注意 HoughLinesP 的 threshold 和 minLineLength 要按页面分辨率调300 DPI 渲染出来的图 threshold 用 200 基本够用如果线被识别成大量碎段把 minLineLength 调大到 500。4.4 三个高频坑跨页表、重复表头、合并单元格跨页表断行是市场报告最高发的解析事故。判断方法很简单连续两页都解析出 Table 对象且列数一致、首行不是表头就认为是同一个表的延续。处理方案是先用 2.3 的关键词定位找到这个表起始页然后用 page.crop() 只裁表格区域把连续页的表格区域提取结果拼接import pandas as pd # page1_df, page2_df 来自两个连续页的 extract_tables() merged pd.concat([page1_df, page2_df], ignore_indexTrue) # 重复表头的特征通常是第一列与上一行的第一列完全相同 merged merged[merged.iloc[:, 0] ! merged.iloc[:, 0].shift()]这个去重逻辑在连续两行恰好同值时会误删数据所以只适合跨页表接缝处的粗筛精细操作要配合页码标记逐段确认。合并单元格在 pdfplumber 里表现为 None 单元格跨行合并的单元格只在第一行有值后续行是 None处理时对每一列用 df.ffill() 把上一个非空值向下填充但只允许在同一个表格内部使用跨表前先 reset_index()。另外如果报告页序本身就是乱的先用 pypdf 按书签拆分重排再进解析流程跨页表格的拼接则留在 DataFrame 层做两者不要混在一起。提示这三个坑都不是靠换库能解决的。camelot 对跨页表格同样无能为力反而因为视觉检测对底色敏感更容易在浅色底纹处多切出一条假表。pdfplumber 至少还能靠坐标裁剪精确控制范围。5. 归档成可检索语料元数据、校验与 OnlyOffice 预览5.1 给提取结果加元数据头提取产物要让别人或半年后的自己能看懂必须带元数据。我在 report.md 开头插入一段 YAML front matter记录来源、解析时间和关键统计--- title: 2022年H1中国移动通信消费市场研究报告 region: CN period: 2022H1 source_file: 2022年H1中国移动通信消费市场研究报告.pdf pages: 96 tables: 23 parsed_at: 2025-01-08 parser: pdfplumber ---这段元数据放入目录后可以直接被很多静态站点生成器和知识库工具识别用于按 region、period 做过滤。配合 Alist 做文件管理时原始 PDF、report.md、report_tables.csv 三个文件放同一目录用 Docker 部署 OnlyOffice Document Server 后浏览器端通过 OnlyOffice 在线预览 PDF不依赖本机 PDF 阅读器团队其他人要抽查原始版式直接在网页打开就行这是我在团队内部落地过的归档方式。5.2 五条指令验证解析质量解析完成不等于解析正确。我的固定校验动作是# 1. 关键词命中检查带页码标记 grep -n 5G套餐 output/report.md | head -10 # 2. 表格规模检查 python3 -c import pandas as pd; dfpd.read_csv(output/report_tables.csv); print(df.shape)# 3. 抽样渲染5页做人工比对 import fitz src 2022年H1中国移动通信消费市场研究报告.pdf doc fitz.open(src) for i in [2, 30, 61]: pix doc[i].get_pixmap(dpi150) # 150 dpi 足够人工核对 pix.save(fcheck_page_{i1}.png) print(渲染完成人工对照 PDF 原页)实际落地时我还会写一个 check_ratio.py随机抽 5 页统计 extract_text() 的字符数与原始渲染图像的字符级差异误差超过 5% 就标记该页需要人工复核。这个方法不适合做全量自动化但作为抽样门槛很有效。遇到可疑页回到 4.1 调整参数后再针对该页单独跑一次提取而不是整本报告重跑能省下大量等待时间。本文还有配套的精品资源点击获取
返回列表