IText: витягуємо текст з PDF

image
 
Добрий час доби, хабровчане!
 
Нещодавно зіткнувся з завданням: навчитися витягувати текст з 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)

Джерело: Хабрахабр
  • avatar
  • 0

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.