- PVSM.RU - https://www.pvsm.ru -
Доброе время суток!
Недавно столкнулся с задачей: научиться вытаскивать текст из PDF запоминая его позицию на странице. И, конечно же, в несложной поначалу задаче вылезли подводные камни. Как же в итоге получилось это решить? Ответ под катом.
PDF (Portable Document Format) — популярный межплатформенный формат документов, использующий язык PostScript. Основное его предназначение — корректное отображение на различных операционных системах и т. д.
Первой идеей было просто самому изобрести велосипед а именно, вскрыть pdf и выдернуть оттуда текст. И, попытавшись это сделать, я понял, что внутри pdf устроен не очень приятно и выявил несколько фактов, серьезно усложняющих задачу:
Потому желание парсить pdf самостоятельно пропало моментально.
p.s. от всего этого невольно вспомнилась цитата
Тем, кто любит колбасу и уважает закон, лучше не видеть, как делается то и другое
Затем, поигравшись с несколькими библиотеками (pdfminer, pdfbox), я решил остановиться на iText.
iText: библиотека на Java, предназначенная для работы с pdf (также есть версия на C#: iTextSharp). Начиная с версии 5.0.0 свободно распространяется по лицензии AGPL (обязывающая предоставлять пользователям возможность получения исходного кода), но также есть и коммерческая версия. Снабжена неплохой документацией. А тем, кто хочет ознакомиться с библиотекой по-лучше, советую книгу от создателя библиотеки «iText in Action».
Вот этот код неплохо извлекает текст из 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();
}
}
Конечно, для хорошего извлечения текста надо добавить всякие фишки для корректной обработки текста в нескольких колонках, обработки пробелов не заданных явно и т.д., но я не хочу в пределах этой статьи углубляться в такие детали.
И еще хотелось бы отметить, что это лишь малая часть возможностей библиотеки. При помощи нее можно создавать документы, добавлять текст и изображения в уже существующие (включая водяные знаки).
И ссылка на репозиторий [1] (ох уж этот AGPL)
Автор: FedyuninV
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/61863
Ссылки в тексте:
[1] репозиторий: https://github.com/FedyuninV/itext-fun
[2] Источник: http://habrahabr.ru/post/225647/
Нажмите здесь для печати.