1

我正在使用 iTextSharp 从 pdf 中提取数据。我偶然发现了以下问题,如下图所示:

我创建了一个示例 excel 文件来说明。这是它的样子: 在此处输入图像描述

我使用许多可用的免费在线转换器之一将其转换为 pdf,它生成的 pdf 看起来像(当我生成 pdf 时,我没有将样式应用于 excel): 在此处输入图像描述

现在,iTextSharp用于从 pdf 中提取数据,将以下字符串作为提取的数据返回给我:

在此处输入图像描述

如您所见,包裹的单元格数据生成新行,其中每个包裹的数据由一个空格分隔。

问题:现在如何识别给定的包装数据属于哪一列?如果只iTextSharp保留与列一样多的空格...

在我的示例中 - 我如何确定111属于哪一列?


更新1:

每当一个字段有多个单词(即,包含空格)时,就会出现类似的问题。例如,考虑上面示例的第一行:

说它看起来像

---A---  ---B---  ---C---  ---D---
aaaaaaa    bb b     cccc      

iText 将再次为这个生成提取:

aaaaaaa bb b cccc

这里有同样的问题,必须确定每列的边界。


更新 2: 我正在使用的真实 pdf 文件的示例: 在此处输入图像描述 这就是 pdf 数据的样子。

4

3 回答 3

7

除了 Chris 的通用答案,iText(Sharp) 内容解析的一些背景......

iText(Sharp) 为/中的内容提取提供了一个框架。该框架读取页面内容,跟踪当前图形状态,并将有关内容片段的信息转发给或/或用户(即)提供。特别是它不会结构解释为该信息。namespace iTextSharp.text.pdf.parserpackage com.itextpdf.text.pdf.parserIExtRenderListenerIRenderListenerExtRenderListenerRenderListener

该渲染侦听器可以是文本提取策略 ( ITextExtractionStrategy/ TextExtractionStrategy),即主要设计用于提取纯文本流而没有格式或布局信息的特殊渲染侦听器。对于这种特殊情况,iText(Sharp) 还提供了两个示例实现,theSimpleTextExtractionStrategyLocationTextExtractionStrategy.

对于您的任务,您需要一个更复杂的渲染侦听器,它要么

  • 导出带有坐标的文本(Chris在他的一个答案中提供了一个扩展LocationTextExtractionStrategy,它可以额外提供文本块的位置和边界框),允许您在附加代码中分析表格结构;或者
  • 对表格数据本身进行分析。

我没有后一种变体的示例,因为一般识别和解析表本身就是一个完整的项目。您可能想从Tabula项目中寻找灵感;这个项目非常擅长表格提取任务。

PS:如果您对尝试从内容的纯字符串表示中提取结构化内容感到更加自在,但仍试图反映原始布局,您可以尝试类似此答案中提出的内容,这是工作的变体,LocationTextExtractionStrategy类似于pdftotext -layout工具;那里只显示要应用于的更改LocationTextExtractionStrategy

PPS:从非常具体的 PDF 表中提取数据可能会容易得多;例如,看看这个答案,它表明在一些 PDF 分析之后,创建给定表的特定方式可能会产生一个简单的自定义渲染侦听器来提取表数据。这对于具有跨越许多页面的表格的单个 PDF 是有意义的,例如在该答案的情况下,或者如果您有许多由同一软件创建的相同的 PDF,这可能是有意义的。

这就是为什么我在对您的问题的评论中要求提供具有代表性的示例文件


关于您的评论

仍然使用上面的 pdf 示例,从头开始实现 ITextExtractionStrategy 和扩展 LocationExtractionStrategy,我看到每个 RenderText 在以下块中调用:Fi、el、d、A、Fi、el、d... 等等在。这可以改变吗?

您作为单独调用获得的文本块RenderText不会因意外或 iText 的某些随机决定而分离。它们是页面内容中单独绘制的字符串!

在您的示例中,“Fi”、“el”、“d”和“A”有不同RenderText的调用,因为内容流包含首先绘制“Fi”,然后是“el”,然后是“d”,然后是“一种”。

起初这可能听起来很奇怪。造成这种撕裂的一个常见原因是 PDF 不使用字体中的字距调整信息。因此,要应用字距调整,PDF 生成软件必须在字符之间插入微小的向前或向后跳转,这些字符之间的距离应该比没有字距调整的距离更远或更近。因此,单词经常在字距调整对之间被撕裂。

所以这是不能改变的,你会得到这些碎片,文本提取策略的工作就是把它们放在一起。

顺便说一句,还有一些更糟糕的 PDF,一些 PDF 生成器分别定位每个字形,最重要的是这些生成器主要构建 GUI,但可以作为一个功能自动将 GUI 画布导出为 PDF。

我希望在进入“添加我自己的实现”的领域时,我可以控制如何确定什么是文本“块”。

你可以……嗯,你必须决定哪些传入的部分属于一起,哪些不属于。例如,具有相同 y 坐标的字形是否形成单行?或者它们是否在恰好位于彼此相邻的不同列中形成单独的行。

所以是的,您决定将哪些字形解释为单个单词或单个表格单元格的内容,但您的输入由实际 PDF 内容流中使用的字形组组成。

不仅如此,在界面的任何方法中,我都无法“发现”它如何/在何处处理非文本数据/图像 - 所以我可以解决间距问题(不调用 RenderImage)

RenderImage将调用嵌入式位图图像、JPEG 等。如果您想了解矢量图形,您的策略还必须实现IExtRenderListenerwhich provides methodsModifyPath和.RenderPathClipPath

于 2016-01-01T09:55:43.047 回答
5

这不是一个真正的答案,但我需要一个地方来展示一些可能有助于你理解事物的东西。

从 Excel、Word、PowerPoint、HTML 或其他任何内容到 PDF 的第一次“转换”几乎总是会带来破坏性的变化。破坏性部分非常重要,它的发生是因为您从一个程序中获取数据,该程序对数据代表的内容有非常具体的了解(Excel),并且您正在将其转换为非常通用的通用格式(PDF)的绘图命令,只关心关于数据的样子,而不是数据本身。除非数据被“标记”(现在几乎从未如此),否则绘图命令没有上下文。没有段落,没有句子,没有列、行、表格等。字面上只是x,y在 处画出这个词a,b

其次,假设您的 Excel 文件具有以下数据,并且由于某种原因,在制作 PDF 时最后一列比其他列窄:

Column A | Column B | Column 
                      C
Data #1    Data #2    Data
                      #3

你和我都有上下文,所以我们知道第二行和第四实际上只是第一行和第三的延续。但由于 iText 在提取过程中没有任何上下文,它不会那样想,它会看到四行 text。事实上,由于它没有上下文,它甚至看不到columns,只有行本身。

第三,尽管您需要了解您不会在 PDF 中绘制空格,但这是一件非常小的事情。想象一下下面的三列表:

Column A | Column B | Column C
                      Yes

如果您从 PDF 中提取它,您将获得以下数据:

Column A | Column B | Column C
Yes

在 PDF 中,“是”一词将仅绘制在x您和我认为位于第三列下方的某个坐标处,并且它前面不会有一堆空格。

正如我在开始时所说,这不是一个答案,但希望它能向您解释您正在尝试解决的问题。如果您的 PDF 被标记,那么它将具有上下文,您可以在提取过程中使用该上下文。然而,上下文不是通用的,因此通常不只是一个神奇的“插入上下文”复选框。Excel 实际上确实有一个复选框(如果我没记错的话)可以在导出期间制作一个带标签的 PDF,它最终会使用类似 HTML 的表格标签创建一个带标签的 PDF。非常原始,但它会起作用。但是,由您来解析此上下文。

于 2015-12-31T21:15:00.930 回答
0

在这里留下一个用于提取数据的替代策略 - 它不能解决谁是处理/可以处理空间的问题,但通过指定要从中提取文本的几何区域,您可以对提取进行更多控制。取自这里

 public static System.util.RectangleJ GetRectangle(float distanceInPixelsFromLeft, float distanceInPixelsFromBottom, float width, float height)
    {
        return new System.util.RectangleJ(
            distanceInPixelsFromLeft,
            distanceInPixelsFromBottom,
            width,
            height);
    }

      public static void Strategy2()
    {
        // In this example, I'll declare a pageNumber integer variable to
        // only capture text from the page I'm interested in
        int pageNumber = 1;

        var text = new StringBuilder();

        List<Tuple<string, int>> result = new List<Tuple<string, int>>();

        // The PdfReader object implements IDisposable.Dispose, so you can
        // wrap it in the using keyword to automatically dispose of it

        using (var pdfReader = new PdfReader("D:/Example.pdf"))
        {
            float distanceInPixelsFromLeft = 20;
            //float distanceInPixelsFromBottom = 730;
            float width = 300;
            float height = 10;

            for (int i = 800; i >= 0; i -= 10)
            {
                var rect = GetRectangle(distanceInPixelsFromLeft, i, width, height);

                var filters = new RenderFilter[1];
                filters[0] = new RegionTextRenderFilter(rect);

                ITextExtractionStrategy strategy =
                    new FilteredTextRenderListener(
                        new LocationTextExtractionStrategy(),
                        filters);

                var currentText = PdfTextExtractor.GetTextFromPage(
                    pdfReader,
                    pageNumber,
                    strategy);

                currentText =
                    Encoding.UTF8.GetString(Encoding.Convert(
                        Encoding.Default,
                        Encoding.UTF8,
                        Encoding.Default.GetBytes(currentText)));

                //text.Append(currentText);
                result.Add(new Tuple<string, int>(currentText, currentText.Length));
            }
        }

        // You'll do something else with it, here I write it to a console window
        //Console.WriteLine(text.ToString());
        foreach (var line in result.Distinct().Where(r => !string.IsNullOrWhiteSpace(r.Item1)))
        {
            Console.WriteLine("Text: [{0}], Length: {1}", line.Item1, line.Item2);
        }
        //Console.WriteLine("", string.Join("\r\n", result.Distinct().Where(r => !string.IsNullOrWhiteSpace(r.Item1))));

输出:

在此处输入图像描述

PS.:我们仍然存在如何处理空格/非文本数据的问题。

于 2016-01-13T11:10:20.973 回答