3

我正在尝试将 pdf 转换为 csv 文件。pdf文件具有表格格式的数据,第一行作为标题。我已经达到了可以从单元格中提取文本、比较表格中文本的基线并检测换行符的水平,但我需要比较表格边框以检测表格的开头。我不知道如何检测和比较 PDF 中的行。谁能帮我?

谢谢!!!

4

2 回答 2

6

正如您所看到的(希望如此),PDF 没有表格的概念,只有放置在特定位置的文本和围绕它们绘制的线条。文本和行之间没有内在联系。理解这一点非常重要。

知道了这一点,如果所有单元格都有足够的填充,您可以寻找足够大的字符之间的间隙,例如 3 个或更多空格的宽度。如果单元格没有足够的间距,不幸的是这可能会破裂。

您还可以查看 PDF 中的每一行,并尝试找出代表“表格”行的内容。有关如何遍历页面上的每个标记以查看正在绘制的内容,请参阅此答案。

于 2013-04-02T15:32:19.353 回答
5

我也在寻找类似问题的答案,但不幸的是我没有找到,所以我自己做了。

像这样的PDF页面在此处输入图像描述

将输出为在此处输入图像描述

这是我制作的 dotnet 控制台应用程序的 github 链接。 https://github.com/Justabhi96/Detect_And_Extract_Table_From_Pdf

此应用程序检测 PDF 特定页面中的表格,并在控制台上以表格格式打印它们。这是我用来制作这个应用程序的代码。

首先,我使用扩展iTextSharp 的 iTextSharp.text.pdf.parser.LocationTextExtractionStrategy类的类从 PDF 中取出文本及其坐标。代码如下:

这是要存储带有坐标和文本的块的类。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace itextPdfTextCoordinates
{
    public class RectAndText
    {
        public iTextSharp.text.Rectangle Rect;
        public String Text;
        public RectAndText(iTextSharp.text.Rectangle rect, String text)
        {
            this.Rect = rect;
            this.Text = text;
        }

    }
}

这是扩展LocationTextExtractionStrategy类的类。

using iTextSharp.text.pdf.parser;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace itextPdfTextCoordinates
{
    public class MyLocationTextExtractionStrategy : LocationTextExtractionStrategy
    {
        public List<RectAndText> myPoints = new List<RectAndText>();

        //Automatically called for each chunk of text in the PDF
        public override void RenderText(TextRenderInfo renderInfo)
        {
            base.RenderText(renderInfo);

            //Get the bounding box for the chunk of text
            var bottomLeft = renderInfo.GetDescentLine().GetStartPoint();
            var topRight = renderInfo.GetAscentLine().GetEndPoint();

            //Create a rectangle from it
            var rect = new iTextSharp.text.Rectangle(
                                                    bottomLeft[Vector.I1],
                                                    bottomLeft[Vector.I2],
                                                    topRight[Vector.I1],
                                                    topRight[Vector.I2]
                                                    );

            //Add this to our main collection
            this.myPoints.Add(new RectAndText(rect, renderInfo.GetText()));
        }
    }
}

此类覆盖 LocationTextExtractionStrategy 类的RenderText方法,每次使用PdfTextExtractor.GetTextFromPage()方法从 PDF 页面提取块时都会调用该方法。

using itextPdfTextCoordinates;
using iTextSharp.text.pdf;
//Create an instance of our strategy
var t = new MyLocationTextExtractionStrategy();
var path = "F:\\sample-data.pdf";
//Parse page 1 of the document above
using (var r = new PdfReader(path))
{
    for (var i = 1; i <= r.NumberOfPages; i++)
    {
        // Calling this function adds all the chunks with their coordinates to the 
        // 'myPoints' variable of 'MyLocationTextExtractionStrategy' Class
        var ex = iTextSharp.text.pdf.parser.PdfTextExtractor.GetTextFromPage(r, i, t);
    }
}
//Here you can loop over the chunks of PDF
foreach(chunk in t.myPoints){
    Console.WriteLine("character {0} is at {1}*{2}",i.Text,i.Rect.Left,i.Rect.Top);
}

现在,为了检测表格的开始和结束,您可以使用从 PDF 中提取的块的坐标。 就像如果特定行没有表格,那么当前块的右坐标和下一个块的左坐标将没有跳转。但是有表格的线条将有至少 3 个点的坐标跳跃。

像具有表的行将具有块的坐标,如下所示:

当前块的
右坐标 -> 12.75pts 下一个块的左坐标 -> 20.30pts

因此,您可以进一步使用此逻辑来检测 PDF 中的表格。代码如下:

using itextPdfTextCoordinates;
using iTextSharp.text.pdf;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class LineUsingCoordinates
    {
        public static List<List<string>> getLineText(string path, int page, float[] coord)
        {
            //Create an instance of our strategy
            var t = new MyLocationTextExtractionStrategy();

            //Parse page 1 of the document above
            using (var r = new PdfReader(path))
            {
                // Calling this function adds all the chunks with their coordinates to the 
                // 'myPoints' variable of 'MyLocationTextExtractionStrategy' Class
                var ex = iTextSharp.text.pdf.parser.PdfTextExtractor.GetTextFromPage(r, page, t);
            }
            // List of columns in one line
            List<string> lineWord = new List<string>();
            // temporary list for working around appending the <List<List<string>>
            List<string> tempWord;
            // List of rows. rows are list of string
            List<List<string>> lineText = new List<List<string>>();
            // List consisting list of chunks related to each line
            List<List<RectAndText>> lineChunksList = new List<List<RectAndText>>();
            //List consisting the chunks for whole page;
            List<RectAndText> chunksList;
            // List consisting the list of Bottom coord of the lines present in the page 
            List<float> bottomPointList = new List<float>();

            //Getting List of Coordinates of Lines in the page no matter it's a table or not
            foreach (var i in t.myPoints)
            {
                Console.WriteLine("character {0} is at {1}*{2}", i.Text, i.Rect.Left, i.Rect.Top);
                // If the coords passed to the function is not null then process the part in the 
                // given coords of the page otherwise process the whole page
                if (coord != null)
                {
                    if (i.Rect.Left >= coord[0] &&
                        i.Rect.Bottom >= coord[1] &&
                        i.Rect.Right <= coord[2] &&
                        i.Rect.Top <= coord[3])
                    {
                        float bottom = i.Rect.Bottom;
                        if (bottomPointList.Count == 0)
                        {
                            bottomPointList.Add(bottom);
                        }
                        else if (Math.Abs(bottomPointList.Last() - bottom) > 3)
                        {
                            bottomPointList.Add(bottom);
                        }
                    }
                }
                // else process the whole page
                else
                {
                    float bottom = i.Rect.Bottom;
                    if (bottomPointList.Count == 0)
                    {
                        bottomPointList.Add(bottom);
                    }
                    else if (Math.Abs(bottomPointList.Last() - bottom) > 3)
                    {
                        bottomPointList.Add(bottom);
                    }
                }
            }

            // Sometimes the above List will be having some elements which are from the same line but are
            // having different coordinates due to some characters like " ",".",etc.
            // And these coordinates will be having the difference of at most 4 points between 
            // their bottom coordinates. 

            //so to remove those elements we create two new lists which we need to remove from the original list 

            //This list will be having the elements which are having different but a little difference in coordinates 
            List<float> removeList = new List<float>();
            // This list is having the elements which are having the same coordinates
            List<float> sameList = new List<float>();

            // Here we are adding the elements in those two lists to remove the elements
            // from the original list later
            for (var i = 0; i < bottomPointList.Count; i++)
            {
                var basePoint = bottomPointList[i];
                for (var j = i+1; j < bottomPointList.Count; j++)
                {
                    var comparePoint = bottomPointList[j];
                    //here we are getting the elements with same coordinates
                    if (Math.Abs(comparePoint - basePoint) == 0)
                    {
                        sameList.Add(comparePoint);
                    }
                    // here ae are getting the elements which are having different but the diference
                    // of less than 4 points
                    else if (Math.Abs(comparePoint - basePoint) < 4)
                    {
                        removeList.Add(comparePoint);
                    }
                }
            }

            // Here we are removing the matching elements of remove list from the original list 
            bottomPointList = bottomPointList.Where(item => !removeList.Contains(item)).ToList();

            //Here we are removing the first matching element of same list from the original list
            foreach (var r in sameList)
            {
                bottomPointList.Remove(r);
            }

            // Here we are getting the characters of the same line in a List 'chunkList'.
            foreach (var bottomPoint in bottomPointList)
            {
                chunksList = new List<RectAndText>();
                for (int i = 0; i < t.myPoints.Count; i++)
                {
                    // If the character is having same bottom coord then add it to chunkList
                    if (bottomPoint == t.myPoints[i].Rect.Bottom)
                    {
                        chunksList.Add(t.myPoints[i]);
                    }
                    // If character is having a difference of less than 3 in the bottom coord then also
                    // add it to chunkList because the coord of the next line will differ at least 10 points
                    // from the coord of current line
                    else if (Math.Abs(t.myPoints[i].Rect.Bottom - bottomPoint) < 3)
                    {
                        chunksList.Add(t.myPoints[i]);
                    }
                }
                // Here we are adding the chunkList related to each line
                lineChunksList.Add(chunksList);
            }
            bool sameLine = false;

            //Here we are looping through the lines consisting the chunks related to each line 
            foreach(var linechunk in lineChunksList)
            {
                var text = "";
                // Here we are looping through the chunks of the specific line to put the texts
                // that are having a cord jump in their left coordinates.
                // because only the line having table will be having the coord jumps in their 
                // left coord not the line having texts
                for (var i = 0; i< linechunk.Count-1; i++)
                {
                    // If the coord is having a jump of less than 3 points then it will be in the same
                    // column otherwise the next chunk belongs to different column
                    if (Math.Abs(linechunk[i].Rect.Right - linechunk[i + 1].Rect.Left) < 3)
                    {
                        if (i == linechunk.Count - 2)
                        {
                            text += linechunk[i].Text + linechunk[i+1].Text ;
                        }
                        else
                        {
                            text += linechunk[i].Text;
                        }
                    }
                    else
                    {
                        if (i == linechunk.Count - 2)
                        {
                            // add the text to the column and set the value of next column to ""
                            text += linechunk[i].Text;
                            // this is the list of columns in other word its the row
                            lineWord.Add(text);
                            text = "";
                            text += linechunk[i + 1].Text;
                            lineWord.Add(text);
                            text = "";
                        }
                        else
                        {
                            text += linechunk[i].Text;
                            lineWord.Add(text);
                            text = "";
                        }
                    }                        
                }
                if(text.Trim() != "")
                {
                    lineWord.Add(text);
                }
                // creating a temporary list of strings for the List<List<string>> manipulation
                tempWord = new List<string>();
                tempWord.AddRange(lineWord);
                // "lineText" is the type of List<List<string>>
                // this is our list of rows. and rows are List of strings
                // here we are adding the row to the list of rows
                lineText.Add(tempWord);
                lineWord.Clear();
            }

            return lineText;
        }
    }
}

您可以调用上述类的getLineText()方法并运行以下循环以在控制台上查看表结构中的输出。

    var testFile = "F:\\sample-data.pdf";
    float[] limitCoordinates = { 52, 671, 357, 728 };//{LowerLeftX,LowerLeftY,UpperRightX,UpperRightY}

    // This line gives the lists of rows consisting of one or more columns
    //if you pass the third parameter as null the it returns the content for whole page
    // but if you pass the coordinates then it returns the content for that coords only
    var lineText = LineUsingCoordinates.getLineText(testFile, 1, null);
    //var lineText = LineUsingCoordinates.getLineText(testFile, 1, limitCoordinates);
    // For detecting the table we are using the fact that the 'lineText' item which length is 
    // less than two is surely not the part of the table and the item which is having more than
    // 2 elements is the part of table
    foreach (var row in lineText)
    {
        if (row.Count > 1)
        {
            for (var col = 0; col < row.Count; col++)
            {
                string trimmedValue = row[col].Trim();
                if (trimmedValue != "")
                {
                    Console.Write("|" + trimmedValue + "|");
                }
            }
            Console.WriteLine("");
        }
    }
    Console.ReadLine();
于 2018-09-15T08:34:08.807 回答