Извлечение текста из ячеек таблицы
У меня есть 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 + ")";
}
}
}