【问题标题】:Text extraction from table cells从表格单元格中提取文本
【发布时间】:2013-11-17 20:36:21
【问题描述】:

我有一个 pdf。 pdf 包含一个表格。该表包含许多单元格 (>100)。我知道表格中每个单元格的确切位置 (x,y) 和尺寸 (w,h)。
我需要使用 itextsharp 从单元格中提取文本。使用 PdfReaderContentParser + FilteredTextRenderListener (使用像这样的代码 http://itextpdf.com/examples/iia.php?id=279 )我可以提取文本,但我需要为每个单元格运行整个过程。我的 pdf 有很多单元格,程序需要太多时间才能运行。有没有办法从“矩形”列表中提取文本?我需要知道每个矩形的文本。我正在寻找 PdfBox 的 PDFTextStripperByArea 之类的东西(您可以根据需要定义任意数量的区域,并使用 .getTextForRegion("region-name") 获取文本)。

【问题讨论】:

    标签: pdf itextsharp


    【解决方案1】:

    此选项不会立即包含在 iTextSharp 发行版中,但很容易实现。在下文中,我使用 iText (Java) 类、接口和方法名称,因为我更熟悉 Java。它们应该很容易翻译成 iTextSharp (C#) 名称。

    如果您使用LocationTextExtractionStrategy,您可以使用它的后验TextChunkFilter 机制,而不是您链接到的示例中使用的先验FilteredRenderListener 机制。该机制已在 5.3.3 版本中引入。

    为此,您首先使用LocationTextExtractionStrategy 解析整个页面内容,而不应用任何FilteredRenderListener 过滤。这使得策略对象为包含相关基线段的页面上的所有 PDF 文本对象收集 TextChunk 对象。

    然后您使用 TextChunkFilter 参数调用策略的 getResultantText 重载(而不是常规的无参数重载):

    public String getResultantText(TextChunkFilter chunkFilter)
    

    您为每个表格单元格使用不同的TextChunkFilter 实例来调用它。你必须实现这个过滤器接口,它并不难,因为它只定义了一个方法:

    public static interface TextChunkFilter
    {
        /**
         * @param textChunk the chunk to check
         * @return true if the chunk should be allowed
         */
        public boolean accept(TextChunk textChunk);
    }
    

    因此,给定单元格的过滤器的接受方法必须测试有问题的文本块是否在您的单元格内。

    (除了每个单元格的单独实例,您当然也可以创建一个实例,其参数(即单元格坐标)可以在 getResultantText 调用之间更改。)

    PS: 正如 OP 所说,这个 TextChunkFilter 尚未移植到 iTextSharp。不过,这样做应该不难,只需一个小界面和一种方法即可添加到策略中。

    PPS:在评论sschuberth

    在使用getResultantText() 时,您是否仍然调用PdfTextExtractor.getTextFromPage(),或者它会以某种方式取代该调用?如果是这样,您如何指定要提取到的页面?

    其实PdfTextExtractor.getTextFromPage()内部已经使用了无参数getResultantText()重载:

    public static String getTextFromPage(PdfReader reader, int pageNumber, TextExtractionStrategy strategy, Map<String, ContentOperator> additionalContentOperators) throws IOException
    {
        PdfReaderContentParser parser = new PdfReaderContentParser(reader);
        return parser.processContent(pageNumber, strategy, additionalContentOperators).getResultantText();
    }
    

    要使用TextChunkFilter,您可以简单地构建一个类似的便捷方法,例如

    public static String getTextFromPage(PdfReader reader, int pageNumber, LocationTextExtractionStrategy strategy, Map<String, ContentOperator> additionalContentOperators, TextChunkFilter chunkFilter) throws IOException
    {
        PdfReaderContentParser parser = new PdfReaderContentParser(reader);
        return parser.processContent(pageNumber, strategy, additionalContentOperators).getResultantText(chunkFilter);
    }
    

    但是,在当前的上下文中,我们只想解析页面内容一次并应用多个过滤器,每个单元格一个过滤器,我们可以将其概括为:

    public static List<String> getTextFromPage(PdfReader reader, int pageNumber, LocationTextExtractionStrategy strategy, Map<String, ContentOperator> additionalContentOperators, Iterable<TextChunkFilter> chunkFilters) throws IOException
    {
        PdfReaderContentParser parser = new PdfReaderContentParser(reader);
        parser.processContent(pageNumber, strategy, additionalContentOperators)
    
        List<String> result = new ArrayList<>();
        for (TextChunkFilter chunkFilter : chunkFilters)
        {
            result.add(strategy).getResultantText(chunkFilter);
        }
        return result;
    }
    

    (您可以通过使用 Java 8 集合流而不是老式的 for 循环来使这个看起来更漂亮。)

    【讨论】:

    • C# 版本的 LocationTextExtractionStrategy 没有 getResultantText(TextChunkFilter chunkFilter)。 :( 但你的想法很有趣。我会寻找一种不同的方法。
    • 哎呀,你是对的。有趣的是,该功能已在 iText / Java 的 5.3.4 版本(不是 JavaDoc cmets 中声称的 5.3.3)中公开可用,该版本已有一年多的历史。但是我觉得翻译成C#应该真的很容易,毕竟只有一个接口和一个方法……
    • 当您使用getResultantText() 时,您是否仍然调用PdfTextExtractor.getTextFromPage(),或者它会以某种方式取代该调用?如果是这样,您如何指定要提取到的页面?
    • @sschuberth 在答案中授予我的 PPS。
    • @mkl 谢谢!
    【解决方案2】:

    以下是我对如何使用 itextsharp 从 PDF 中的表格结构中提取文本的看法。它返回一个行集合,每行包含一个解释列的集合。这可能对您有用,前提是一列和下一列之间的间隙大于单个字符的平均宽度。我还添加了一个选项来检查虚拟列中的换行文本。您的里程可能会有所不同。

       using (PdfReader pdfReader = new PdfReader(stream))
            {
                for (int page = 1; page <= pdfReader.NumberOfPages; page++)
                {
    
                    TableExtractionStrategy tableExtractionStrategy = new TableExtractionStrategy();
                    string pageText = PdfTextExtractor.GetTextFromPage(pdfReader, page, tableExtractionStrategy);
                    var table = tableExtractionStrategy.GetTable();
    
                }
            }
    
    
    
            public class TableExtractionStrategy : LocationTextExtractionStrategy
            {
                public float NextCharacterThreshold { get; set; } = 1;
                public int NextLineLookAheadDepth { get; set; } = 500;
                public bool AccomodateWordWrapping { get; set; } = true;
    
                private List<TableTextChunk> Chunks { get; set; } = new List<TableTextChunk>();
    
                public override void RenderText(TextRenderInfo renderInfo)
                {
                    base.RenderText(renderInfo);
                    string text = renderInfo.GetText();
                    Vector bottomLeft = renderInfo.GetDescentLine().GetStartPoint();
                    Vector topRight = renderInfo.GetAscentLine().GetEndPoint();
                    Rectangle rectangle = new Rectangle(bottomLeft[Vector.I1], bottomLeft[Vector.I2], topRight[Vector.I1], topRight[Vector.I2]);
                    Chunks.Add(new TableTextChunk(rectangle, text));
                }
    
                public List<List<string>> GetTable()
                {
                    List<List<string>> lines = new List<List<string>>();
                    List<string> currentLine = new List<string>();
    
                    float? previousBottom = null;
                    float? previousRight = null;
    
                    StringBuilder currentString = new StringBuilder();
    
                    // iterate through all chunks and evaluate 
                    for (int i = 0; i < Chunks.Count; i++)
                    {
                        TableTextChunk chunk = Chunks[i];
    
                        // determine if we are processing the same row based on defined space between subsequent chunks
                        if (previousBottom.HasValue && previousBottom == chunk.Rectangle.Bottom)
                        {
                            if (chunk.Rectangle.Left - previousRight > 1)
                            {
                                currentLine.Add(currentString.ToString());
                                currentString.Clear();
                            }
                            currentString.Append(chunk.Text);
                            previousRight = chunk.Rectangle.Right;
                        }
                        else
                        {
                            // if we are processing a new line let's check to see if this could be word wrapping behavior
                            bool isNewLine = true;
                            if (AccomodateWordWrapping)
                            {
                                int readAheadDepth = Math.Min(i + NextLineLookAheadDepth, Chunks.Count);
                                if (previousBottom.HasValue)
                                    for (int j = i; j < readAheadDepth; j++)
                                    {
                                        if (previousBottom == Chunks[j].Rectangle.Bottom)
                                        {
                                            isNewLine = false;
                                            break;
                                        }
                                    }
                            }
    
                            // if the text was not word wrapped let's treat this as a new table row
                            if (isNewLine)
                            {
                                if (currentString.Length > 0)
                                    currentLine.Add(currentString.ToString());
                                currentString.Clear();
    
                                previousBottom = chunk.Rectangle.Bottom;
                                previousRight = chunk.Rectangle.Right;
                                currentString.Append(chunk.Text);
    
                                if (currentLine.Count > 0)
                                    lines.Add(currentLine);
    
                                currentLine = new List<string>();
                            }
                            else
                            {
                                if (chunk.Rectangle.Left - previousRight > 1)
                                {
                                    currentLine.Add(currentString.ToString());
                                    currentString.Clear();
                                }
                                currentString.Append(chunk.Text);
                                previousRight = chunk.Rectangle.Right;
    
                            }
                        }
                    }
    
                    return lines;
                }
    
                private struct TableTextChunk
                {
                    public Rectangle Rectangle;
                    public string Text;
    
                    public TableTextChunk(Rectangle rect, string text)
                    {
                        Rectangle = rect;
                        Text = text;
                    }
    
                    public override string ToString()
                    {
                        return Text + " (" + Rectangle.Left + ", " + Rectangle.Bottom + ")";
                    }
                }
            }
    

    【讨论】:

    • 这个策略做了很多假设(Shaun 已经提到了其中一些),所以它不适用于所有类型的带有表格的 PDF。但对于假设成立的 PDF,无需知道坐标即可提取表格内容。
    • 只是好奇 - 是否有使用 iText 进行更通用解析的参考?我搜索了 SE,但没有找到 - 非常想知道示例源是否有更好的东西。
    • 好吧,关于文本提取,iText 为您提供了一个框架,您可以在其中开发您选择的复杂提取策略。但是许多用户认为与 iText 捆绑在一起的两个示例策略就是全部。你会发现一些专门的提取策略和 POCs 用于更高级的东西,例如这里在堆栈溢出答案中,但不是像 tabula 这样的大免费解决方案。可能有一些基于 iText 的闭源文本提取器。
    • @Shaun 非常感谢这个答案这对我们帮助很大!!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-02-06
    • 1970-01-01
    • 1970-01-01
    • 2011-08-28
    相关资源
    最近更新 更多