You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iTextSharp C#提取含多对齐单元格的PDF表格文本问题求助

方案一:扩展LocationTextExtractionStrategy保留单元格间距

默认策略会合并同一Y坐标的文本,忽略单元格间距,导致对齐不同的内容丢失。我们可以自定义策略,记录每个文本块的位置,按行分组后根据X坐标间距插入空格模拟单元格分隔:

public class TableTextExtractionStrategy : LocationTextExtractionStrategy
{
    private List<TextChunk> _textChunks = new List<TextChunk>();

    public override void RenderText(TextRenderInfo renderInfo)
    {
        base.RenderText(renderInfo);
        // 保存每个文本块的基线坐标与内容
        var baseline = renderInfo.GetBaseline();
        _textChunks.Add(new TextChunk(renderInfo.GetText(), baseline.GetStartPoint(), baseline.GetEndPoint()));
    }

    public override string GetResultantText()
    {
        // 按Y坐标(取整后)分组,模拟行;PDF Y轴向上,降序是从上到下
        var rowGroups = _textChunks.GroupBy(c => Math.Round(c.StartPoint.Y, 2))
                                   .OrderByDescending(g => g.Key);

        StringBuilder result = new StringBuilder();
        foreach (var row in rowGroups)
        {
            // 同一行内按X坐标从左到右排序
            var sortedChunks = row.OrderBy(c => c.StartPoint.X).ToList();
            float lastEndX = 0;

            foreach (var chunk in sortedChunks)
            {
                // 文本块间距超过阈值时,插入空格模拟单元格分隔
                if (lastEndX > 0 && chunk.StartPoint.X - lastEndX > 5)
                {
                    // 按间距比例插入空格,可根据实际PDF调整系数
                    result.Append(new string(' ', (int)(chunk.StartPoint.X - lastEndX) / 2));
                }
                result.Append(chunk.Text);
                lastEndX = chunk.EndPoint.X;
            }
            result.AppendLine();
        }
        return result.ToString();
    }

    // 辅助类存储文本块的位置与内容
    private class TextChunk
    {
        public string Text { get; }
        public Vector StartPoint { get; }
        public Vector EndPoint { get; }

        public TextChunk(string text, Vector start, Vector end)
        {
            Text = text;
            StartPoint = start;
            EndPoint = end;
        }
    }
}

使用时替换原有策略:

text = PDFTextExtractor.GetTextFromPage(reader, i, new TableTextExtractionStrategy());
方案二:检测行边框提取整行文本

如果表格有明确的水平边框线,可以先提取这些线条的Y坐标,划分行区域后逐区域提取文本,确保整行内容完整:

第一步:提取页面水平线条的Y坐标

private static List<float> GetHorizontalLineYCoordinates(PdfReader reader, int pageNum)
{
    List<float> lineYs = new List<float>();
    var pageDict = reader.GetPageN(pageNum);
    var resourcesDict = pageDict.GetAsDict(PdfName.RESOURCES);
    var xObjectDict = resourcesDict?.GetAsDict(PdfName.XOBJECT);

    // 处理表单XObject中的线条
    if (xObjectDict != null)
    {
        foreach (PdfName name in xObjectDict.Keys)
        {
            var obj = xObjectDict.Get(name);
            if (obj.IsIndirect())
            {
                var xObjDict = reader.GetPdfObject(obj.GetIndirectReference()) as PdfDictionary;
                if (xObjDict?.GetAsName(PdfName.SUBTYPE) == PdfName.FORM)
                {
                    ExtractLinesFromContent(reader, xObjDict, lineYs);
                }
            }
        }
    }

    // 处理页面直接内容中的线条
    ExtractLinesFromContent(reader, pageDict, lineYs);

    // 去重并按Y坐标降序排序(从上到下)
    return lineYs.Distinct().OrderByDescending(y => Math.Round(y, 2)).ToList();
}

private static void ExtractLinesFromContent(PdfReader reader, PdfDictionary dict, List<float> lineYs)
{
    var contentStream = dict.GetAsStream(PdfName.CONTENTS);
    if (contentStream == null) return;

    var parser = new PdfContentParser(reader);
    var tokens = new List<PdfObject>();
    parser.Parse(contentStream, tokens);

    for (int i = 0; i < tokens.Count; i++)
    {
        // 检测PDF线条绘制指令(L操作)
        if (tokens[i] is PdfName && ((PdfName)tokens[i]).Equals(PdfName.L))
        {
            // 线条坐标格式:x1 y1 x2 y2
            float y1 = ((PdfNumber)tokens[i - 3]).FloatValue;
            float y2 = ((PdfNumber)tokens[i - 1]).FloatValue;

            // 判定为水平线(y1与y2差值小于容差)
            if (Math.Abs(y1 - y2) < 1)
            {
                lineYs.Add(Math.Round(y1, 2));
            }
        }
    }
}

第二步:按行区域提取文本

List<float> lineYs = GetHorizontalLineYCoordinates(reader, i);
StringBuilder fullText = new StringBuilder();

// 无检测到线条时,回退到默认策略
if (lineYs.Count == 0)
{
    fullText.Append(PDFTextExtractor.GetTextFromPage(reader, i, new LocationTextExtractionStrategy()));
}
else
{
    // 添加上下边界,确保首尾行被覆盖
    var pageSize = reader.GetPageSize(i);
    lineYs.Insert(0, pageSize.Top);
    lineYs.Add(pageSize.Bottom);

    for (int j = 0; j < lineYs.Count - 1; j++)
    {
        float topY = lineYs[j];
        float bottomY = lineYs[j + 1];

        // 创建当前行的区域过滤器
        var rowRect = new Rectangle(0, bottomY, pageSize.Right, topY);
        var listener = new FilteredTextRenderListener(
            new LocationTextExtractionStrategy(),
            new RegionTextRenderFilter(rowRect)
        );

        PDFTextExtractor.GetTextFromPage(reader, i, listener);
        var rowText = listener.GetResultantText().Trim();
        if (!string.IsNullOrEmpty(rowText))
        {
            fullText.AppendLine(rowText);
        }
    }
}

text = fullText.ToString();

注意事项

  • 两个方案中的阈值(间距、线条容差)需要根据目标PDF的实际排版调整;
  • 无边框表格优先用方案一,有明确边框的表格用方案二提取更准确。

内容的提问来源于stack exchange,提问作者Jaidev Khatri

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 18:55:26