Извлечение текста из ячеек таблицы

У меня есть PDF. PDF содержит таблицу. Таблица содержит много ячеек (>100). Я знаю точное положение (x,y) и размерность (w,h) каждой ячейки таблицы.
Мне нужно извлечь текст из ячеек с помощью itextsharp. Используя PdfReaderContentParser + FilteredTextRenderListener (используя подобный код http://itextpdf.com/examples/iia.php?id=279), я могу извлечь текст, но мне нужно выполнить всю процедуру для каждой ячейки. В моем pdf много ячеек, и программе нужно слишком много времени для запуска. Есть ли способ извлечь текст из списка "прямоугольник"? Мне нужно знать текст каждого прямоугольника. Я ищу что-то вроде PDFTextStripperByArea от PdfBox (вы можете определить столько областей, сколько вам нужно, и получить текст, используя.getTextForRegion("region-name")).

2 ответа

Эта опция не сразу включена в дистрибутив iTextSharp, но ее легко реализовать. Далее я использую имена классов, интерфейсов и методов iText (Java), потому что я больше знаком с Java. Они должны легко переводиться в имена iTextSharp (C#).

Если вы используете LocationTextExtractionStrategyможете использовать его апостериори TextChunkFilter механизм вместо априори FilteredRenderListener Механизм, используемый в образце, с которым вы связаны Этот механизм был представлен в версии 5.3.3.

Для этого вы сначала анализируете весь контент страницы, используя LocationTextExtractionStrategy без всяких FilteredRenderListener фильтрация применена. Это заставляет объект стратегии собирать TextChunk объекты для всех текстовых объектов PDF на странице, содержащей связанный сегмент базовой линии.

Затем вы называете стратегию getResultantText перегрузка с TextChunkFilter аргумент (вместо обычной перегрузки без аргументов):

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

Поэтому метод accept фильтра для данной ячейки должен проверить, находится ли рассматриваемый фрагмент текста внутри вашей ячейки.

(Вместо отдельных экземпляров для каждой ячейки вы, конечно, можете также создать один экземпляр, параметры которого, то есть координаты ячейки, могут быть изменены между getResultantText вызовы.)

PS: как упомянуто ФП, это TextChunkFilter еще не был портирован на iTextSharp. Это не должно быть трудно сделать, хотя, только один маленький интерфейс и один метод, чтобы добавить к стратегии.

PPS: в комментарии sschuberth спросил

Вы тогда еще звоните PdfTextExtractor.getTextFromPage() когда используешь getResultantText()или это как-то заменяет этот вызов? Если да, то как вам указать страницу для извлечения?

На самом деле 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 цикл.)

Вот мое мнение о том, как извлечь текст из табличной структуры в PDF с помощью itextsharp. Он возвращает коллекцию строк, и каждая строка содержит коллекцию интерпретируемых столбцов. Это может сработать для вас при условии, что между одним столбцом и следующим есть разрыв, который больше, чем средняя ширина одного символа. Я также добавил опцию проверки обернутого текста в виртуальном столбце. Ваш пробег может отличаться.

   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 + ")";
                }
            }
        }
Другие вопросы по тегам