iTextSharp将包装的单元格内容提取到新行 - 您如何识别给定的包装数据片段所属的列?

时间:2015-12-30 14:24:37

标签: itextsharp pdf-extraction

我正在使用iTextSharp从pdfs中提取数据。 我偶然发现了以下问题所描述的问题:

我创建了一个示例excel文件来说明。这是它的样子: enter image description here

我将其转换为pdf,使用其中一个可用的免费在线转换器,生成一个类似的pdf(当我生成pdf时,我没有将样式应用于excel): enter image description here

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

enter image description here

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

问题:现在,如何识别给定数据包所属的列?如果只有iTextSharp保留了与列相同数量的空格......

在我的示例中 - 如何识别 111 属于哪个列?

更新1:

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

说它看起来像

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

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

aaaaaaa bb b cccc

此处存在相同问题,必须确定每列的边框

更新2: 我正在使用的真实pdf文件的示例: enter image description here 这就是pdf数据的样子。

3 个答案:

答案 0 :(得分:5)

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

iText(夏普)在namespace iTextSharp.text.pdf.parser / package com.itextpdf.text.pdf.parser中提供了用于内容提取的框架。此franework读取页面内容,跟踪当前图形状态,并将有关内容的信息转发给用户IExtRenderListenerIRenderListener / ExtRenderListenerRenderListener(即)提供。特别是它将结构解释为此信息。

此渲染侦听器可以是文本提取策略(ITextExtractionStrategy / TextExtractionStrategy),即一种特殊的渲染侦听器,主要用于提取纯文本流而无需格式化或布局信息< / em>的。对于这个特殊情况,iText(夏普)还提供了两个示例实现,SimpleTextExtractionStrategyLocationTextExtractionStrategy

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

  • 使用坐标导出文本(Chris in one of his answers提供了一个扩展的LocationTextExtractionStrategy,它还可以提供文本块的位置和边界框),允许您使用其他代码来分析表格结构;或
  • 对表格数据本身进行分析。

我没有后一种变体的例子,因为一般地识别和解析表本身就是一个整体项目。您可能希望查看Tabula项目的灵感;这个项目非常擅长表格提取的任务。

PS:如果您想尝试从内容的纯字符串表示中提取结构化内容,但仍尝试反映原始布局,那么您可能会尝试在this answer中提出的建议, LocationTextExtractionStrategy的变体与pdftotext -layout工具类似;只显示要应用于LocationTextExtractionStrategy的更改。

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

这就是我在您的问题评论中要求提供代表性样本文件的原因

关于您的意见

  

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

您作为单独的RenderText电话获得的文本块不会被偶然或iText的随机决定分开。它们是页面内容中单独绘制的字符串!

在您的样本&#34; Fi&#34;,&#34; el&#34;,&#34; d&#34;和&#34; A&#34;进入不同的RenderText调用,因为内容流包含第一个&#34; Fi&#34;绘制,然后&#34; el&#34;,然后&#34; d&#34;,然后&#34; A&#34;。

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

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

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

  

我希望进入&#34;添加我自己的实现&#34;我可以控制如何确定什么是&#34; chunk&#34;文本。

你可以......好吧,你必须决定哪些传入的作品属于哪些,哪些不属于哪些。例如。具有相同y坐标的字形是否形成一条线?或者它们在不同的列中形成单独的行,这些行恰好位于彼此旁边。

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

  

不仅如此,在任何界面的方法中,我都可以&#34;发现&#34;如何/在何处处理非文本数据/图像 - 所以我可以用间距问题进行调解(不调用RenderImage)

RenderImage将调用嵌入式位图图像,JPEG等。如果您想了解矢量图形,您的策略也必须实施IExtRenderListener提供方法ModifyPathRenderPathClipPath

答案 1 :(得分:4)

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

从Excel,Word,PowerPoint,HTML或其他任何内容到PDF的第一次“转换”几乎总是会发生破坏性更改。 破坏性部分非常重要,之所以发生这种情况,是因为您从一个程序中获取数据,该程序具有非常具体的数据所代表的知识(Excel)并且您将其转换为绘图命令采用非常通用的通用格式(PDF),只关心数据的外观,而不是数据本身。除非数据被“标记”(并且现在几乎从未如此),否则绘图命令没有上下文。没有段落,没有句子,没有列,行,表等等。字面上只有x,y 绘制这封信,而在{{ 1}}

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

a,b

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

第三,尽管你需要了解一件非常小的事情,但你并没有在PDF中绘制空格。想象一下下面的三列表:

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

如果您从PDF中提取了该数据,则会获得以下数据:

Column A | Column B | Column C
                      Yes

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

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

答案 2 :(得分:0)

在这里留下一个提取数据的替代策略 - 这不能解决处理/可以处理空间的问题,但通过指定要从中提取文本的几何区域,可以更好地控制提取。取自here

 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))));

输出:

enter image description here

PS:我们仍然面临着如何处理空格/非文本数据的问题。