PDF 在合同、报告、说明书和业务归档中非常常见,但一旦需要批量检索、抽取字段或整理表格,手工复制几乎不可持续。对 C# 开发者来说,关键不只是“能不能读出 PDF”,而是如何在保留代码可维护性的前提下,稳定完成文本与表格提取。本文按“环境准备、文本提取、表格识别、工程化注意事项”四个层次展开,并结合 Spire.PDF for .NET 的示例代码,帮助你判断它适合处理哪些任务、又该如何规避常见问题。
先把环境搭起来:安装库并验证 PDF 能否正常加载
PDF 不是普通的纯文本文件,它本质上是一种页面描述格式,内部可能包含文本、字体、图像、矢量对象以及复杂的布局信息。因此,实际开发中通常不会直接去解析底层二进制结构,而是通过成熟的 PDF 处理库来完成读取和提取。
本文示例选用 Spire.PDF for .NET。它提供了创建、编辑、转换和解析 PDF 的 API,适合在 C# 项目中快速落地常见文档处理需求。
NuGet 安装命令
在 Visual Studio 项目中,可通过 NuGet 包管理器安装:
Install-Package Spire.PDF
安装完成后,就可以在代码中引用相关命名空间并开始处理 PDF 文件。
先做最小验证:加载文档并读取页数
无论后续要提取文本还是识别表格,第一步都应该先确认文件能够被正常加载。下面这段代码演示了如何打开 PDF 并获取总页数:
using Spire.Pdf;
using System;
namespace PdfParserTutorial
{
class Program
{
static void Main(string[] args)
{
// 假设您的PDF文件位于项目根目录下的"Data"文件夹中
string pdfFilePath = "Data/Sample.pdf";
// 创建一个PdfDocument实例
PdfDocument doc = new PdfDocument();
try
{
// 从文件加载PDF文档
doc.LoadFromFile(pdfFilePath);
// 获取并打印PDF文档的总页数
Console.WriteLine($"PDF文档 '{pdfFilePath}' 已成功加载。");
Console.WriteLine($"总页数: {doc.Pages.Count}");
}
catch (Exception ex)
{
Console.WriteLine($"加载PDF文档时发生错误: {ex.Message}");
}
finally
{
// 释放资源
doc.Close();
}
Console.ReadKey();
}
}
}
这里涉及几个最基础、但也是后续高频使用的对象和方法:
PdfDocument是操作 PDF 的核心类。LoadFromFile()用于从指定路径加载文件。doc.Pages.Count可以获取文档总页数。doc.Close()用于释放文件句柄和相关资源,避免内存占用持续增长。
文本怎么提:整页提取与按区域提取各适合什么场景
在 PDF 自动化处理中,文本提取通常是最常见的需求。比如做关键字检索、报告归档、字段抽取或数据迁移时,首先都要把页面里的文字稳定拿出来。

不过,PDF 文本并不总是线性排列的。它可能被分散在不同对象层里,也可能受到字体嵌入、编码方式或布局结构的影响。因此,实际处理时通常要区分两种思路:一种是提取整页文本,另一种是针对特定区域做定向抽取。
提取所有页面的纯文本
如果目标是把整份 PDF 转成可搜索、可保存的文本文件,可以直接遍历每一页并逐页提取:
using Spire.Pdf;
using Spire.Pdf.Texts;
using System;
using System.IO;
using System.Text;
namespace PdfParserTutorial
{
class Program
{
static void Main(string[] args)
{
string pdfFilePath = "Data/Sample.pdf";
PdfDocument doc = new PdfDocument();
doc.LoadFromFile(pdfFilePath);
StringBuilder allText = new StringBuilder();
// 遍历文档中的每一页
for (int i = 0; i < doc.Pages.Count; i++)
{
PdfPageBase page = doc.Pages[i];
// 创建PdfTextExtractor实例
PdfTextExtractor extractor = new PdfTextExtractor(page);
// 设置提取选项,例如是否保留空格
PdfTextExtractOptions options = new PdfTextExtractOptions();
options.IsExtractAllText = true; // 设置为true可以提取所有文本,包括隐藏文本
// 提取当前页的文本内容
string pageText = extractor.ExtractText(options);
allText.AppendLine($"--- 第 {i + 1} 页文本内容 ---");
allText.AppendLine(pageText);
allText.AppendLine();
}
// 将所有文本内容写入文件或打印到控制台
File.WriteAllText("ExtractedText.txt", allText.ToString());
Console.WriteLine("所有文本内容已提取并保存到 ExtractedText.txt");
doc.Close();
Console.ReadKey();
}
}
}
这段代码的适用点很明确:
- 用
PdfTextExtractor从指定页面提取文本。 - 通过
PdfTextExtractOptions控制提取行为。 IsExtractAllText = true可尝试提取更多文本内容,包括可能被遮挡或处于特殊层级的文字。
如果你的目标是做全文检索、建立文本索引或保存为审计记录,这种“全页扫描”的方式通常最直接。
只提取指定区域文本,适合表单和固定版式文档
有些业务并不关心整页内容,只想拿到固定位置的一块文字。比如发票上的金额区域、表单中的姓名字段,或者报告首页的摘要框。这时,按矩形区域提取会更高效,也更容易减少无关文本干扰。
using Spire.Pdf;
using Spire.Pdf.Texts;
using System;
using System.Drawing; // 引入System.Drawing命名空间处理RectangleF
namespace PdfParserTutorial
{
class Program
{
static void Main(string[] args)
{
string pdfFilePath = "Data/Sample.pdf";
PdfDocument doc = new PdfDocument();
doc.LoadFromFile(pdfFilePath);
// 获取第一页
PdfPageBase page = doc.Pages[0];
// 定义一个要提取文本的矩形区域 (x, y, width, height)
// 这里的坐标和尺寸单位通常是PDF点(1/72英寸),原点在左下角或左上角,具体取决于PDF的设置。
// 假设我们想从页面左上角开始的某个区域提取文本
RectangleF rect = new RectangleF(50, 50, 300, 100);
PdfTextExtractor extractor = new PdfTextExtractor(page);
string regionText = extractor.ExtractText(rect);
Console.WriteLine($"--- 从指定区域提取的文本 ---");
Console.WriteLine($"区域 ({rect.X}, {rect.Y}, {rect.Width}, {rect.Height}) 的文本:");
Console.WriteLine(regionText);
doc.Close();
Console.ReadKey();
}
}
}
这里的关键在于 RectangleF:
- 它定义了要提取的页面区域。
extractor.ExtractText(rect)只返回该矩形范围内的文本。
需要注意的是,区域提取高度依赖版式稳定性。如果同一类 PDF 的模板经常变化,坐标方案就要额外维护;如果版式固定,它反而是效率很高的字段抽取手段。
表格如何识别:从页面元素中还原结构化数据
相比纯文本提取,表格解析通常更难。原因在于 PDF 并没有真正意义上的“表格对象”,很多表格只是由文本、线条和矩形拼出来的视觉结构。程序要把这些页面元素重新理解成“行、列、单元格”,本身就是一次结构还原。

Spire.PDF 提供了专门的表格识别能力,适合把页面里的表格内容提取为可继续处理的数据结构。
识别页面中的表格并输出内容
下面的示例展示了如何识别 PDF 页面中的表格,并逐行逐列读取单元格文本:
using Spire.Pdf;
using Spire.Pdf.Tables;
using System;
using System.Text;
namespace PdfParserTutorial
{
class Program
{
static void Main(string[] args)
{
string pdfFilePath = "Data/TableSample.pdf"; // 假设有一个包含表格的PDF文件
PdfDocument doc = new PdfDocument();
doc.LoadFromFile(pdfFilePath);
// 获取第一页
PdfPageBase page = doc.Pages[0];
// 创建PdfTableExtractor实例
PdfTableExtractor extractor = new PdfTableExtractor(page);
// 识别并提取所有表格
PdfTable[] tables = extractor.ExtractTable(true); // true表示尝试识别所有表格
if (tables != null && tables.Length > 0)
{
Console.WriteLine($"在页面 {1} 中找到 {tables.Length} 个表格。");
for (int i = 0; i < tables.Length; i++)
{
PdfTable table = tables[i];
Console.WriteLine($"--- 表格 {i + 1} 内容 ---");
// 遍历表格的行和列
for (int r = 0; r < table.RowCount; r++)
{
StringBuilder rowData = new StringBuilder();
for (int c = 0; c < table.ColumnCount; c++)
{
// 获取单元格内容
string cellText = table.GetText(r, c);
rowData.Append($"{cellText,-20}"); // 格式化输出,确保对齐
}
Console.WriteLine(rowData.ToString());
}
Console.WriteLine();
}
}
else
{
Console.WriteLine("未在页面中找到表格。");
}
doc.Close();
Console.ReadKey();
}
}
}
这段代码里,和表格识别直接相关的核心点有三个:
PdfTableExtractor用于页面表格识别。ExtractTable(true)会尝试提取当前页面中的所有表格。PdfTable提供RowCount、ColumnCount和GetText(row, column),便于把结果继续转存到数据库、CSV 或业务对象中。
输出结果大致会是什么样
假设 TableSample.pdf 中包含如下简单表格:
| Header 1 | Header 2 | Header 3 |
|---|---|---|
| Data A1 | Data B1 | Data C1 |
| Data A2 | Data B2 | Data C2 |
上述代码的控制台输出可能如下:
在页面 1 中找到 1 个表格。
--- 表格 1 内容 ---
Header 1 Header 2 Header 3
Data A1 Data B1 Data C1
Data A2 Data B2 Data C2
如果你的需求是“把 PDF 里的表格导出来再继续算”,这一层结构化提取通常就是关键起点。
真正落地时要注意什么:性能、异常处理与资源释放
示例代码能跑通,只说明功能路径可行;要放进实际项目,还要考虑稳定性和处理成本。尤其是批量处理大文件、面对损坏 PDF 或格式不规范文档时,工程层面的细节会直接影响可用性。

大文件处理时,先盯住性能和内存
- 按需处理页面: 如果只需要某几页内容,就尽量避免无差别扫描整份文档。
- 及时释放资源: 完成操作后调用
doc.Close(),或者根据项目风格改写为更清晰的资源管理方式,减少文件句柄和内存占用。 - 批量任务可考虑并行: 当需要处理大量 PDF 文件时,多线程或并行任务能缩短总耗时,但前提是先评估机器资源与库本身的并发行为。
面对损坏、缺失或异常格式文件,错误处理不能省
真实环境里的 PDF 文件并不总是干净标准:可能路径错误、文件损坏、加密、结构异常,或者来源系统生成得并不规范。这类情况如果没有分层处理,批处理任务很容易中断。
using Spire.Pdf;
using System;
using System.IO;
namespace PdfParserTutorial
{
class Program
{
static void Main(string[] args)
{
string pdfFilePath = "Data/CorruptedSample.pdf"; // 假设这是一个损坏或不存在的PDF
PdfDocument doc = new PdfDocument();
try
{
doc.LoadFromFile(pdfFilePath);
Console.WriteLine($"PDF文档 '{pdfFilePath}' 已成功加载。总页数: {doc.Pages.Count}");
}
catch (FileNotFoundException)
{
Console.WriteLine($"错误: PDF文件 '{pdfFilePath}' 未找到。请检查文件路径。");
}
catch (PdfException ex)
{
Console.WriteLine($"PDF解析错误: {ex.Message}");
// 记录更详细的错误信息到日志
// Logger.LogError($"PDF解析失败: {ex.Message}", ex);
}
catch (Exception ex)
{
Console.WriteLine($"发生未知错误: {ex.Message}");
}
finally
{
doc.Close(); // 确保即使发生错误也尝试关闭文档
}
Console.ReadKey();
}
}
}
这段示例的意义不在于“多写几个 catch”,而在于把错误分层:
FileNotFoundException用来处理路径或文件缺失问题。PdfException更适合承接 PDF 解析阶段的专属异常。finally中关闭文档,可以确保无论成功还是失败,都尽量把资源释放干净。
选择提取方案时,可以这样判断
- 如果目标是检索全文、归档文本,优先用整页文本提取。
- 如果文档模板固定,只关心少数字段,优先考虑坐标区域提取。
- 如果核心数据以表格形式呈现,就直接走表格识别流程,并准备好对识别结果做二次清洗。
总结
对 C# 开发者来说,解析 PDF 的关键并不是自己处理复杂底层结构,而是借助成熟库,把“加载文档、提取文本、识别表格、处理异常”这些环节串成一条可复用的流程。以 Spire.PDF for .NET 为例,整页文本提取适合检索和归档,区域提取适合固定模板字段抽取,表格识别则适合把页面内容转回结构化数据。
如果你的项目已经遇到批量 PDF 处理需求,这套方法基本可以作为起步框架。下一步真正决定效果的,往往不是 API 会不会用,而是文档模板是否稳定、异常样本是否充分,以及你是否为性能和容错留出了工程空间。









