ASP.NET Core 9 MVC中如何提取PDF中带空格千分位的完整数字(基于PdfPig或替代免费工具)
ASP.NET Core 9 MVC中如何提取PDF中带空格千分位的完整数字(基于PdfPig或替代免费工具)
我来帮你搞定这个问题!你遇到的是PDF文本提取里非常常见的坑——带空格千分位的数字被拆成了两个独立的单词,导致后续的金额提取、数据处理都很麻烦。下面我会先教你怎么修改现有PdfPig代码解决这个问题,再给你推荐其他靠谱的免费工具方案。
一、修改现有PdfPig代码,合并同一行内的数字块
你的现有代码已经实现了按行排序单词的逻辑,现在只需要在同一行内增加判断:把属于同一个数字的拆分单词合并成完整字符串。
核心思路是:
- 先把所有单词按行分组(用灵活的间距阈值替代固定行高,适配不同PDF)
- 对每一行的单词,按左坐标排序保证阅读顺序
- 遍历每行单词,检查相邻单词是否是数字的拆分部分(比如“1”和“000.23”),符合条件就合并
修改后的完整GetWordsInReadingOrder方法
public static void GetWordsInReadingOrder(Page page, StringBuilder builder) { List<Word> words = page.GetWords().ToList(); if (!words.Any()) return; // 按行(垂直坐标)和左坐标排序单词 var sortedWords = words.OrderBy(r => r.BoundingBox.Bottom).ThenBy(r => r.BoundingBox.Left).ToList(); // 按行分组:用动态阈值判断是否属于同一行 const double rowTolerance = 5; // 可根据PDF字体大小调整,比如字号12的PDF用5-8 var lineGroups = new List<List<Word>>(); var currentLine = new List<Word> { sortedWords[0] }; double currentLineBottom = sortedWords[0].BoundingBox.Bottom; foreach (var word in sortedWords.Skip(1)) { if (Math.Abs(word.BoundingBox.Bottom - currentLineBottom) <= rowTolerance) { currentLine.Add(word); } else { lineGroups.Add(currentLine); currentLine = new List<Word> { word }; currentLineBottom = word.BoundingBox.Bottom; } } lineGroups.Add(currentLine); // 处理每一行:合并数字块,再拼接成文本 foreach (var line in lineGroups.OrderByDescending(g => g.First().BoundingBox.Bottom)) // 按从上到下的阅读顺序 { var sortedLineWords = line.OrderBy(w => w.BoundingBox.Left).ToList(); var mergedLineParts = new List<string>(); for (int i = 0; i < sortedLineWords.Count; i++) { var currentWord = sortedLineWords[i].Text; // 检查是否可以和下一个单词合并为完整数字 if (i < sortedLineWords.Count - 1) { var nextWord = sortedLineWords[i + 1].Text; var currentRight = sortedLineWords[i].BoundingBox.Right; var nextLeft = sortedLineWords[i + 1].BoundingBox.Left; var wordGap = nextLeft - currentRight; // 判断条件:两个都是数字/数字部分,且间距极小(属于同一数字的拆分) bool isCurrentNumeric = IsNumericPart(currentWord); bool isNextNumeric = IsNumericPart(nextWord); bool isTinyGap = wordGap < 3; // 正常单词间距远大于这个值,可调整 if (isCurrentNumeric && isNextNumeric && isTinyGap) { // 合并两个单词,跳过下一个单词 mergedLineParts.Add($"{currentWord}{nextWord}"); i++; // 跳过下一个,因为已经合并完成 continue; } } // 不满足合并条件,直接添加当前单词 mergedLineParts.Add(currentWord); } // 拼接当前行的文本,添加到StringBuilder builder.AppendLine(string.Join(" ", mergedLineParts)); } } // 辅助方法:判断字符串是否是数字的一部分(纯数字、带小数点的数字) private static bool IsNumericPart(string text) { return double.TryParse(text, out _) || Regex.IsMatch(text, @"^\d+$|^\.\d+$|^\d+\.\d*$"); }
关键说明
- 优化了行分组逻辑,用
rowTolerance动态判断行边界,比固定行高更适配不同PDF IsNumericPart方法覆盖了整数、小数的各种数字部分场景wordGap阈值用来区分“同一数字的拆分块”和“正常单词间距”,确保不会误合并
二、使用替代免费工具:iText 7 Community Edition
如果觉得修改PdfPig代码太繁琐,可以试试iText 7 Community Edition——免费开源(遵守AGPL协议即可),它的文本提取策略扩展性更强,能更轻松处理这种数字合并场景。
步骤1:安装NuGet包
在项目中安装以下包:
Install-Package iText7.Core Install-Package iText7.BouncyCastle
步骤2:编写提取代码
async Task<string> ExtractTextWithiTextAsync(MemoryStream pdfStream) { pdfStream.Position = 0; string extractedText = string.Empty; using var pdfReader = new PdfReader(pdfStream); using var pdfDocument = new PdfDocument(pdfReader); // 使用自定义的文本提取策略,自动合并数字块 var extractionStrategy = new NumberMergingTextExtractionStrategy(); for (int pageNum = 1; pageNum <= pdfDocument.GetNumberOfPages(); pageNum++) { var page = pdfDocument.GetPage(pageNum); var processor = new PdfCanvasProcessor(extractionStrategy); processor.ProcessPageContent(page); } extractedText = extractionStrategy.GetResultantText(); return extractedText; } // 自定义文本提取策略:合并相邻的数字块 public class NumberMergingTextExtractionStrategy : LocationTextExtractionStrategy { protected override void RenderText(TextRenderInfo renderInfo) { string currentText = renderInfo.GetText(); TextChunk lastChunk = this.latestChunk; if (lastChunk != null) { double distance = lastChunk.GetLocation().GetEndLocation().DistanceFrom(renderInfo.GetStartLocation()); bool isLastNumeric = IsNumericPart(lastChunk.Text); bool isCurrentNumeric = IsNumericPart(currentText); // 满足条件:都是数字部分,且间距极小,合并到上一个Chunk if (isLastNumeric && isCurrentNumeric && distance < 2) { // 用反射修改Chunk的文本(iText的TextChunk内部text字段是非公开的) var textField = typeof(TextChunk).GetField("text", BindingFlags.NonPublic | BindingFlags.Instance); textField?.SetValue(lastChunk, lastChunk.Text + currentText); return; } } // 不满足合并条件,正常添加新文本 base.RenderText(renderInfo); } private bool IsNumericPart(string text) { return double.TryParse(text, out _) || Regex.IsMatch(text, @"^\d+$|^\.\d+$|^\d+\.\d*$"); } }
关键说明
- 自定义策略继承自iText的默认文本提取策略,重写
RenderText方法实现数字合并 - 底层文本解析逻辑更稳定,适合处理复杂排版的PDF
- 同样可通过调整距离阈值适配不同PDF的排版
三、额外小贴士
- 阈值调整:不管用哪种工具,
rowTolerance和wordGap的阈值都要根据目标PDF的字体大小、排版调整 - 极端情况处理:如果遇到带货币符号的数字(比如“1 000.23 EUR”),可以在合并逻辑里加入货币符号的判断,避免误合并
- PdfPig进阶方案:如果上面的方法还不够,可以用
page.GetLetters()提取单个字符,自己完全控制分组和合并逻辑,但工作量会大很多
内容来源于stack exchange




