尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

PDF表格提取实战:用Python从投资报告中建立可核查数据台账

发布时间:2026/9/19 11:30:04

资讯中心
01
ARTICLE

PDF表格提取实战:用Python从投资报告中建立可核查数据台账

PDF表格提取实战:用Python从投资报告中建立可核查数据台账
简介《2021年世界投资报告》是联合国贸发会议UNCTAD发布的年度旗舰研究面向全球投资者、政策制定者和国际发展领域研究者系统梳理新冠疫情冲击下全球外商直接投资FDI的现状与前景。报告聚焦全球FDI趋势、地区差异、行业分布以及与可持续发展目标SDGs相关的投资重点同时结合数字化和清洁技术等新兴领域为理解和把握后疫情时代的国际资本流动提供了数据支撑与政策参考。本资源为官方英文原版PDF共1个文件大小13.21MB内容完整、排版清晰适合作为国际贸易、金融投资、发展经济学等方向的学习研究材料。目前已有130人学习下载。通过阅读该报告读者可掌握2021年全球投资格局的关键信息理解不同经济体FDI表现的差异及背后动因并了解如何通过政策引导实现绿色、包容和可持续的复苏路径具有较强的实证参考价值。1. 一份《2021年世界投资报告》PDF 里最值得自动化处理的段落《2021年世界投资报告》PDF 不是一本普通电子书它由联合国贸发会议发布正文里被引用最多的往往是 FDI 总量、区域汇总、行业分布这些表格而不是叙述性章节。表格式数据经过排版后复制粘贴会丢掉单元格边界千分位符号和全角空格混在数值里跨页表的表头还会重复出现。做海外投资研究的人、给领导层做季度简报的人、维护企业准入台账的运维到了这一步都会卡住。纠缠的解决方式不是对着 PDF 手工录一遍而是把页面当作对象处理先识别文本层、矢量线和嵌入图再决定用哪套解析流程。下面这篇文章会给出可复现的 PDF 表格提取链路并包含一轮有严格来源关系的对账方式。2. 报告 PDF 的数据结构先看页面对象再选解析工具2.1 排版软件留下的三种页面对象《2021年世界投资报告》这类旗舰报告由排版软件生成页面上并不是一整张“图像”而是由独立对象组成的画布文本对象、矢量图形和嵌入图像。表格的框线通常是矢量路径文字是独立的文本块个别数据来源图则是压缩后的位图。直接读取文本层时表格的列关系已经消失直接抽取矢量线时又看不到数字内容。所以不能只用一个工具解决全部页面必须先判断页面结构。文本对象的数量、矢量路径数量、图像数量这三项能构成一份页面画像。文本层缺失的页面后续只能走 OCR路径数异常多的页面大概率藏着密排表格图像数量多但文字量正常则只需要跳过图像即可。2.2 四种 PDF 解析工具的适用范围对比针对报告类 PDF常用工具集中在下面四个分别对应不同环节。选型时建议参考我平时用的这张对照表。工具最适合的场景主要局限在流程中的角色PyMuPDFfitz检查页面结构、渲染页面快照不具备表格结构还原能力前置体检和 OCR 渲染pdfplumber抽取正文文本和字符坐标复杂合并单元格时会丢列文本层提取Camelot抽取有线表格和无线表格对细线、跨页表需要调参表格数据主力tabula-py简单的单栏表格依赖本机 Java 环境备选常见错误是一上来就对整个 PDF 跑 Camelot 或者 tabula既慢又容易把图形页误判成表格页。我习惯先用 PyMuPDF 做页面级别的元数据扫描把全部页面分成文本页、表格页、图形页三类再分别决定交给哪个环节。2.3 用 PyMuPDF 给整份报告做页面体检下面这段代码可以输出每一页的文本长度、图片数量和矢量路径数。路径数是一个很重要的信号排版越复杂路径越多。import fitz doc fitz.open(2021年世界投资报告.pdf) print(页数:, doc.page_count) for i in range(doc.page_count): page doc[i] text page.get_text(text).strip() imgs page.get_images(fullTrue) drawings page.get_drawings() print( i 1, 文本长度:, len(text), 图片:, len(imgs), 路径数:, len(drawings), )代码逻辑很直接get_text(text)返回该页全部文本块拼接后的字符串文本长度为 0 表示这一页没有文字层get_images(fullTrue)返回嵌入图像的引用注意它不判断图像是否铺满页面get_drawings()返回所有矢量路径路径数上千的页面通常包含复杂的表格线或版面装饰。拿到这个清单后把“文本长度接近 0 且图片数大于 0”的页面标注为扫描页把“路径数大于 200 且文本中含连续空白”的页面标注为候选表格页。这一步不需要精确识别只要能缩小后续处理范围即可。3. 用 Python 把报告里的 FDI 表格完整取出来3.1 环境准备与依赖安装表格抽取依赖 pdfplumber、PyMuPDF 和 Camelot。Camelot 在 Linux 和 macOS 上依赖 GhostscriptWindows 下需要确保gswin64c已加入 PATH。安装命令如下。pip install pdfplumber pymupdf camelot-py[base] tabula-py openpyxlcamelot-py[base]会一起装入 pandas 和 openpyxl 依赖openpyxl用于导出 Excel 文件。如果只需要读取表格而不导出可以省略tabula-py。3.2 用 pdfplumber 提取正文文本并确认表头位置根据第 2.3 节筛出的候选表格页先用 pdfplumber 读取这些页面的完整文本确认表头所在行。这个环节能让后续的表格区域定位更简单。import pdfplumber with pdfplumber.open(2021年世界投资报告.pdf) as pdf: for page_no in candidate_pages: page pdf.pages[page_no - 1] text page.extract_text( x_tolerance1, y_tolerance3, layoutFalse, ) print(f--- page {page_no} ---) print(text[:800])x_tolerance控制同一行文字允许的最大水平间距默认值是 3当表头字母间距被拉宽时可以调大到 5。y_tolerance控制垂直方向文字归行的容忍度默认值是 3。如果输出的行和页面上看到的不一致优先调这两个参数而不是直接改代码逻辑。3.3 用 Camelot 抽取表格lattice 和 stream 怎么选报告里多数表格带完整框线优先用lattice模式。Camelot 会根据页面上的直线生成单元格再把文字填入。使用前需要把候选页转换成字符串列表传给pages。import camelot pages_str ,.join([str(p) for p in candidate_pages]) tables camelot.read_pdf( 2021年世界投资报告.pdf, pagespages_str, flavorlattice, line_scale40, ) print(共发现表格:, tables.n) print(tables[0].df.head(5)) for i, t in enumerate(tables): t.to_excel(fwir_table_p{t.page}_{i}.xlsx)line_scale默认值是 40单位与页面坐标相关。排版使用极细线时把它调到 60 可以提升识别率但调得过高会把表格底部的注释线条误认为表格框线。输出后建议先打印tables.n如果识别出的表格数量远多于肉眼看到的说明页面有多处被误判。对于没有框线或者彩色底纹的表格改用stream模式。它不依赖直线而是通过空白区域推断列边界。tables camelot.read_pdf( 2021年世界投资报告.pdf, pages46, flavorstream, table_areas[60, 770, 540, 520], row_tol5, col_tol5, )table_areas接收 PDF 坐标格式是x1, y1, x2, y2原点在页面左上角。坐标可以先在 PyMuPDF 渲染出的页面快照上标定再回填到 Camelot。row_tol和col_tol分别控制行、列的合并阈值遇到行距不均匀时把row_tol调小遇到窄列被合并时把col_tol调小。3.4 表格列错位的三个征兆与补救方式实际抽取时表格结果经常不是一次正确。最常见的三类问题有征兆可查。征兆原因补救方式某列数值整体错位到相邻列细线没有被识别调大line_scale或切到stream表头在每页重复出现跨页表格自带续表头按页抽取后在数据合并阶段去重合并单元格导致大量空值Camelot 对跨行单元格还原有限用上一行非空值填充并补充标记列处理这三类问题时不要在一份表格上反复调整全局参数。先把出错页面单独提取确认是线宽问题、跨页问题还是合并单元格问题再针对性地处理单页。3.5 文本层缺失页面的 OCR 兜底文本长度接近 0 的扫描页需要渲染成图片后走 OCR 识别。渲染倍数直接决定识别质量2 倍缩放约等于 144 DPI适合多数数字表格页面字号很小可以提高到 3 倍。import fitz doc fitz.open(2021年世界投资报告.pdf) page doc[49] pix page.get_pixmap(matrixfitz.Matrix(2, 2), dpiNone) pix.save(page_50.png)Matrix(2, 2)在水平和垂直方向各放大两倍dpiNone表示不额外指定分辨率完全由矩阵决定输出尺寸。OCR 环节我通常直接用 PaddleOCR 的英文模型对数字、百分比和小数点的识别比通用模型更稳。OCR 结果仍然是文本流需要用pdfplumber或正则的方式做行列重组因此它只是文本层缺失时的兜底方案不必一开始就启用。4. 清洗和对账让 2021 世界投资报告的数字能经得起核对4.1 统一处理空格、全角字符与括号负值Camelot 导出后的单元格会出现1, 234、1 234、(1, 234)这类形态。其中1 234是用空格分隔的千分位写法(1, 234)是财务报表中常见的负值写法。统一清洗规则如下。import pandas as pd df pd.read_excel(wir_table_p42_0.xlsx, headerNone, dtypestr) df df.apply(lambda col: col.str.replace(r[ \u3000], , regexTrue)) df df.replace(r^—$|^–$|^$, pd.NA, regexTrue) df df.replace(r^\((.)\)$, r-\1, regexTrue)第一条规则同时删除普通空格和全角空格避免to_numeric因空格报错。第二条规则把正文中常见的短横线、长横线统一识别为缺失值。第三条规则把括号内容转换成带负号的字符串这样后续可以直接转成负数。对清洗后的数据类型确认一个常见问题不要把所有列全部转成数值。百分比行、金额行和“经济体现在”这样的文本行混在同一张表中一次性转换会丢失信息正确的做法是保留原始字符串只在需要计算的列中做类型转换。4.2 按列类型转换并处理百分比行百分比和绝对值混排是年度报告表格的基本特征。转换值之前先判断列里是否含%符号再做不同处理。def to_number(x): if pd.isna(x) or not isinstance(x, str): return pd.NA if % in x: return pd.to_numeric(x.replace(%, ), errorscoerce) / 100 return pd.to_numeric(x, errorscoerce) num_cols df.columns[3:] for col in num_cols: df[col] df[col].map(to_number)这里没有直接使用 Pandas 2.x 中已弃用的applymap而是对目标列做map每个单元格都会走一遍to_number。errorscoerce保证遇到无法解析的字符串时返回NaN而不是中断程序。通过判断%来区分百分比和绝对值的逻辑适用于大多数 FDI 表格。4.3 用总量行反向校验分项之和《2021年世界投资报告》区域汇总表里通常带有“总计”行校验时把分项加总与总计对比允许存在 1 个最小单位的四舍五入误差。下面以合计行为例。total df.loc[df.iloc[:, 0] 合计, 3:].iloc[0] partial df.loc[df.iloc[:, 0].isin(region_names), 3:].sum() diff (total - partial).abs() print(diff[diff 1])total取第一列为止“合计”的行region_names是你预先定义好的地区清单包括发达经济体、发展中经济体与转型期经济体。diff 1这一阈值的含义是允许四舍五入带来 1 个单位的偏差。如果偏差超过阈值第一件事不是改代码而是回 PDF 检查是否有“其他未分类经济体”这一行没有被提取到。4.4 指标口径不同导致的“对不上”怎么判断对不上不一定是提取错误也可能是口径不一致。报告中正文提到的“增长 21%”和表格里出现的“增长 12%”很可能一个是基于当前价格计算另一个基于不变价格计算。判断方式是先看表头下方的单位说明和脚注再检查表格的计量单位是“百万美元”还是“当前美元”。遇到这种问题时把明显由口径引起的差异记录成一个独立的字段而不是强行把两个数字对齐。最务实的方案是保留两张表各自的转换逻辑在最终输出时借助报告口径列标注差异来源。日常数据处理里最危险的做法是让合计行迁就分项和的差值——那会让所有下游分析的基础无法追溯。5. 把报告沉淀成可检索的 SQLite 台账和图表快照5.1 用页码加表序号的编码方式保存表格来源多年度报告对比时最怕数据来源丢失。我的做法是把清洗后的表格存入 SQLite并为每张表记录页码和抽取方式。表格中每一个单元格都保留原始字符串形态不提前转成数值。import sqlite3 conn sqlite3.connect(wir.db) conn.executescript( CREATE TABLE IF NOT EXISTS tables_meta ( table_id TEXT PRIMARY KEY, page_no INTEGER, flavor TEXT, title TEXT ); CREATE TABLE IF NOT EXISTS table_cells ( cell_id INTEGER PRIMARY KEY AUTOINCREMENT, table_id TEXT, row_idx INTEGER, col_idx INTEGER, value TEXT ); )table_id使用“页码-序号”的编码例如42-0代表第 42 页第一张表。这种编码比自增主键更直观将来拿到数据就能从原始 PDF 对应页面核对。table_cells表格直接保存清洗前的原始字符串读取查询时再做类型转换以此避免前期清洗错误被固化。5.2 从 SQLite 取数并自动复现趋势图数据入库后取数、画图的过程可以完全自动化。绘制 FDI 趋势图时按年份列取数并同时读取表头行来命名图例。import pandas as pd import matplotlib.pyplot as plt query SELECT row_idx, col_idx, value FROM table_cells WHERE table_id 42-0 cells pd.read_sql_query(query, conn) pivot cells.pivot(indexrow_idx, columnscol_idx, valuesvalue) years pivot.loc[0, 1:].tolist() for r in range(1, len(pivot)): values pd.to_numeric(pivot.loc[r, 1:], errorscoerce) plt.plot(years, values, markero) plt.title(Regional FDI Trends from 2021 World Investment Report) plt.show()pivot 部分把单元格从长表转换成形如原始表格的宽表。years从第 0 行的后续列中取出数值行从第 1 行开始遍历。errorscoerce保证某个单元格不合法时不会中断整条绘图流程。这样的脚本可以固化成定时任务当年底更新同一系列的 PDF 后替换文件重新执行就能得到一套新图。5.3 可信度检查的最后一个步骤每次跑完流程还应该输出一份数据快照源文件哈希值、抽取日期、表格行数、合计行与分项和的偏差。将这份快照与 SQLite 文件放在同一目录作为整个数据链条的证据。在后续任何环节对数字产生疑问时从快照中能直接定位到对应的 PDF 页码和表格序号。加入table_id编码时还有一个容易被忽略的细节多期报告合并入库前要在tables_meta中记录报告年份否则跨年对比时“42-0”这个编码会覆盖上一年的同页数据。把年份和页码一起拼进table_id例如2021-42-0能避免这一处暗坑。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。