You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

ASP.NET Core 9 MVC中如何提取PDF中带空格千分位的完整数字(基于PdfPig或替代免费工具)

ASP.NET Core 9 MVC中如何提取PDF中带空格千分位的完整数字(基于PdfPig或替代免费工具)

我来帮你搞定这个问题!你遇到的是PDF文本提取里非常常见的坑——带空格千分位的数字被拆成了两个独立的单词,导致后续的金额提取、数据处理都很麻烦。下面我会先教你怎么修改现有PdfPig代码解决这个问题,再给你推荐其他靠谱的免费工具方案。


一、修改现有PdfPig代码,合并同一行内的数字块

你的现有代码已经实现了按行排序单词的逻辑,现在只需要在同一行内增加判断:把属于同一个数字的拆分单词合并成完整字符串。

核心思路是:

  1. 先把所有单词按行分组(用灵活的间距阈值替代固定行高,适配不同PDF)
  2. 对每一行的单词,按左坐标排序保证阅读顺序
  3. 遍历每行单词,检查相邻单词是否是数字的拆分部分(比如“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的排版

三、额外小贴士

  1. 阈值调整:不管用哪种工具,rowTolerancewordGap的阈值都要根据目标PDF的字体大小、排版调整
  2. 极端情况处理:如果遇到带货币符号的数字(比如“1 000.23 EUR”),可以在合并逻辑里加入货币符号的判断,避免误合并
  3. PdfPig进阶方案:如果上面的方法还不够,可以用page.GetLetters()提取单个字符,自己完全控制分组和合并逻辑,但工作量会大很多

内容来源于stack exchange

火山引擎 最新活动