Доброе время суток!
Недавно столкнулся с задачей: научиться вытаскивать текст из PDF запоминая его позицию на странице. И, конечно же, в несложной поначалу задаче вылезли подводные камни. Как же в итоге получилось это решить? Ответ под катом.
Немного о PDF формате
PDF (Portable Document Format) — популярный межплатформенный формат документов, использующий язык PostScript. Основное его предназначение — корректное отображение на различных операционных системах и т. д.
Первой идеей было просто самому изобрести велосипед а именно, вскрыть pdf и выдернуть оттуда текст. И, попытавшись это сделать, я понял, что внутри pdf устроен не очень приятно и выявил несколько фактов, серьезно усложняющих задачу:
- слова могут быть нелогично разбиты на части. Например отображение слова «алгоритмы» записано, грубо говоря, тремя частями: отобрази «алг» «орит» «мы»
- строчки в тексте и слова в строчках могут отображаться совсем не в том порядке, как мы привыкли читать
- в одних документах пробелы задаются явно (т.е. есть команды содержащие ' '), в других — они образуются при помощи того, что соседние слова отображаются друг от друга на некотором расстоянии
Потому желание парсить pdf самостоятельно пропало моментально.
p.s. от всего этого невольно вспомнилась цитата
Тем, кто любит колбасу и уважает закон, лучше не видеть, как делается то и другое
Затем, поигравшись с несколькими библиотеками (pdfminer, pdfbox), я решил остановиться на iText.
Немного про iText
iText: библиотека на Java, предназначенная для работы с pdf (также есть версия на C#: iTextSharp). Начиная с версии 5.0.0 свободно распространяется по лицензии AGPL (обязывающая предоставлять пользователям возможность получения исходного кода), но также есть и коммерческая версия. Снабжена неплохой документацией. А тем, кто хочет ознакомиться с библиотекой по-лучше, советую книгу от создателя библиотеки «iText in Action».
Простой способ вытащить текст из PDF
Вот этот код неплохо извлекает текст из PDF, но не предоставляет какой-либо информации, о его расположении в документе.
public class SimpleTextExtractor {
public static void main(String[] args) throws IOException {
// считаем, что программе передается один аргумент - имя файла
PdfReader reader = new PdfReader(args[0]);
// не забываем, что нумерация страниц в PDF начинается с единицы.
for (int i = 1; i <= reader.getNumberOfPages(); ++i) {
TextExtractionStrategy strategy = new SimpleTextExtractionStrategy();
String text = PdfTextExtractor.getTextFromPage(reader, i, strategy);
System.out.println(text);
}
// убираем за собой
reader.close();
}
}
А теперь разберемся во всем по порядку.
PdfReader — класс, читающий PDF. Умеет конструироваться не только от имени файла, но и от InputStream, Url или RandomAccessFileOrArray.
TextExtractionStrategy — интерфейс, определяющий стратегию извлечения текста. Подробнее о нем — ниже.
SimpleTextExtractionStrategy — класс, реализующий TextExtractionStrategy. Несмотря на название, очень неплохо вытаскивает текст из PDF (справляется с переменчивой структурой PDF, а именно, если сначала текст идет в двух колонках, а затем переключается на обычное написание во всю страницу.
PdfTextExtractor — статический класс, содержащий лишь 2 метода getTextFromPage с одной разницей — указываем мы явно стратегию извлечения текста или нет.
Вытаскиваем текст, запоминая координаты
Для этого нам нужно обратить внимание на интерфейс TextExtractionStrategy. А именно на эти две функции:
public void renderText(TextRenderInfo renderInfo)
— при вызове getTextFromPage эта функция вызывается при каждой команде, отображающей текст. В TextRenderInfo хранится вся необходимая информация: текст, шрифт, координаты.
public string GetResultantText()
— эта функция вызывается перед окончанием getTextFromPage и ее результат вернется пользователю.
В качестве образца, научимся простейшим образом вытаскивать пары вида <y-координата строки, текст строки> для каждой строки на странице.
Реализация интерфейса:
public class TextExtractionStrategyImpl implements TextExtractionStrategy {
private TreeMap<Float, TreeMap<Float, String>> textMap;
public TextExtractionStrategyImpl() {
// reverseOrder используется потому что координата y на странице идет снизу вверх
textMap = new TreeMap<Float, TreeMap<Float, String>>(Collections.reverseOrder());
}
@Override
public String getResultantText() {
StringBuilder stringBuilder = new StringBuilder();
// итерируемся по строкам
for (Map.Entry<Float, TreeMap<Float, String>> stringMap: textMap.entrySet()) {
// итерируемся по частям внутри строки
for (Map.Entry<Float, String> entry: stringMap.getValue().entrySet()) {
stringBuilder.append(entry.getValue());
}
stringBuilder.append('n');
}
return stringBuilder.toString();
}
@Override
public void beginTextBlock() {}
@Override
public void renderText(TextRenderInfo renderInfo) {
// вытаскиваем координаты
Float x = renderInfo.getBaseline().getStartPoint().get(Vector.I1);
Float y = renderInfo.getBaseline().getStartPoint().get(Vector.I2);
// если до этого мы не добавляли элементы из этой строчки файла.
if (!textMap.containsKey(y)) {
textMap.put(y, new TreeMap<Float, String>());
}
textMap.get(y).put(x, renderInfo.getText());
}
@Override
public void endTextBlock() {}
@Override
public void renderImage(ImageRenderInfo imageRenderInfo) {}
// метод для извлечения строчек с их y-координатой
ArrayList<Pair<Float, String>> getStringsWithCoordinates() {
ArrayList<Pair<Float, String>> result = new ArrayList<Pair<Float, String>>();
for (Map.Entry<Float, TreeMap<Float, String>> stringMap: textMap.entrySet()) {
StringBuilder stringBuilder = new StringBuilder();
for (Map.Entry<Float, String> entry: stringMap.getValue().entrySet()) {
stringBuilder.append(entry.getValue());
}
result.add(new Pair<Float, String>(stringMap.getKey(), stringBuilder.toString()));
}
return result;
}
}
А основной код выглядит так:
public class TextExtractor {
public static void main(String[] args) throws IOException {
PdfReader reader = new PdfReader(args[0]);
for (int i = 1; i <= reader.getNumberOfPages(); ++i) {
TextExtractionStrategyImpl strategy = new TextExtractionStrategyImpl();
// вызываем, чтобы наша реализация стратегия получила информацию о тексте на странице
PdfTextExtractor.getTextFromPage(reader, i, strategy);
System.out.println("Page : " + i);
for (Pair<Float, String> pair: strategy.getStringsWithCoordinates()) {
System.out.println(pair.getKey().toString() + " " + pair.getValue());
}
}
reader.close();
}
}
Примечания
Конечно, для хорошего извлечения текста надо добавить всякие фишки для корректной обработки текста в нескольких колонках, обработки пробелов не заданных явно и т.д., но я не хочу в пределах этой статьи углубляться в такие детали.
И еще хотелось бы отметить, что это лишь малая часть возможностей библиотеки. При помощи нее можно создавать документы, добавлять текст и изображения в уже существующие (включая водяные знаки).
И ссылка на репозиторий (ох уж этот AGPL)
Автор: FedyuninV