Author Archive

Создаем Searchable PDF с помощью Tesseract OCR

Недавно на работе столкнулись с задачей разпознавания сканированных документов и поиска по ним.

Мною был рассмотрен движок распознавания текста с открытым исходным кодом Tesseract.

В данной статье будут рассмотрены основные моменты возможной реализации.

Предположим, что у нас есть многостраничный отсканированные документ в формате PDF, но нераспознанный.

И наша задача распознать текст с помощью OCR (Optical character recognition - Оптическое распознавание символов) и создать так называемый Searchable PDF.

Searchable PDF - это PDF, в котором поверх изображения размещается дополнительный слой, содержащий распознанный текст, причем на тех же позициях что и на изображении.

Для начала нужно установить необходимые программы.

  • imagemagick - набор программ (консольных утилит) для работы с множеством графических форматов;
  • tesseract-ocr - приложение оптического распознавания символов;
  • tesseract-ocr-all - все языковые пакеты (но можно установить только конкретные языковые пакеты)

У tesseract есть языковые пакеты для русского и казахских языков, что очень круто.

Также можно не устанавливать локально у себя tesseract, а запустить через docker.

Официального образа на hub.docker.com я не нашел, поэтому сделал свой.

Запустить контейнер с tesseract из образа naik85/tesseract можно так (пример для linux/unix):

docker run --rm -v "$(PWD)":/files -w /files -it naik85/tesseract bash
После старта контейнера откроется консоль bash, где можно будет выполнять команды. Также будут доступны ваши файлы из директории, где вы запустили команду docker run.

Первый этап

На первом этапе нужно извлечь изображения из PDF. Здесь есть два варианта либо преобразовать PDF в один файл TIFF, либо преобразовать в набор изображений.

TIFF - это многостраничный формат хранения растровых графических изображений.

Для конвертации в TIFF использовалась следующая команда:

convert -density 300 YOUR_FILE.pdf -depth 1 -strip -background white -alpha off YOUR_FILE.tiff

Для конвертации в PNG использовалась следующая команда:

convert -density 300 YOUR_FILE.pdf -depth 1 -strip -background white -alpha off YOUR_FILE.png

Параметры конвертации приведены для примера, их можно настроить под ваши требования.

После выполнения конвертации в PNG для каждой страницы будет создан отдельный файл изображения.

Например:

YOUR_FILE-0.png
YOUR_FILE-1.png
...
YOUR_FILE-N.png

Второй этап

К сожалению, у меня не получилось преобразовать документ TIFF в Searchable PDF через tesseract.

Была использована следующая команда:

tesseract YOUR_FILE.tiff searchable -l rus PDF

Выходила следующая ошибка:

Tesseract Open Source OCR Engine v4.1.1 with Leptonica
Error in pixReadFromTiffStream: failed to read tiffdata

Кто знает как решить проблему, пишите в комментариях.

Для нашей задачи постраничное деление на отдельные файлы (изображения) было даже предпочтительней (об этом ниже).

Конвертируем каждую страницу (файл png) в Searchable PDF:

tesseract YOUR_FILE-0.png searchable-0 -l rus+kaz+eng pdf
tesseract YOUR_FILE-1.png searchable-1 -l rus+kaz+eng pdf
...
tesseract YOUR_FILE-N.png searchable-2 -l rus+kaz+eng pdf

На выходе получаем файлы:

searchable-0.pdf
searchable-1.pdf
...
searchable-N.pdf

Очень крутая фишка, что можно разпозначать несколько языков, перечислив их через символ '+': rus+kaz+eng.

Команда распознавания и извлечения текста

tesseract YOUR_FILE-0.png -l rus+kaz+eng YOUR_FILE-0
tesseract YOUR_FILE-1.png -l rus+kaz+eng YOUR_FILE-1
...
tesseract YOUR_FILE-N.png -l rus+kaz+eng YOUR_FILE-N

В результате будут созданы текстовые файлы:

YOUR_FILE-0.txt
YOUR_FILE-1.txt
...
YOUR_FILE-N.txt

Распознавая отдельно каждую страницу, мы можем организовать постраничный поиск и при необходимости показывать пользователю только нужные страницы.

Склеив тексты страниц и положив их в поисковый движок, получим подокументый поиск.

Если нужен целый Searchable PDF, то можно его склеивать из отдельных страниц.

p.s. Деление на отдельные страницы затратно получается, но, думаю, зато это более гибко.

Транзакции в debug-режиме

Транзакции в Hibernate работают не всегда прозрачно.

Если для отладки запросов в Hibernate мы используем show-sql: true в application.yml, то для включения логирования транзации нужно переключить уровень логирования на DEBUG для определенного класса.

Следующая настройка для Hibernate 5 и системы логирования SLF4J.

В файл logback-spring.xml (или logback.xml) добавьте следующую строку:

<logger name="org.hibernate.engine.transaction.internal.TransactionImpl"
 level="DEBUG"/>

После рестарта приложения в логах должны появится следующие строки:

1) Начало транзакции

...TransactionImpl         : begin

2) Коммит транзакции

...TransactionImpl         : committing

3) Откат транзакции

...TransactionImpl         : rolling back

Во сколько раз Base64 увеличивает размер в байтах

Base64 — стандарт кодирования байтов при помощи только 64 символов (A-Z, a-z и 0-9 (62 знака) и 2 дополнительных символа, зависящих от системы реализации).

Одним байтом можно закодировать 256 значений (28 бит), в то время как Base64 только 64 (26 бит).

Из этого следует соотношение 28 + 28 + 28 = 26 + 26 + 26 + 26, т.е. каждые 3 исходных байта кодируются в 4 байта.

Можно вывести формулу:

[Кол-во байт в Base64] = ([Исходное кол-во байт] / 3) * 4
или
[Кол-во байт в Base64] = 4 * [Исходное кол-во байт] / 3
или
[Кол-во байт в Base64] = 4/3 * [Исходное кол-во байт]

Например: Файл размером 1500 байт в Base64 будет занимать 2000 байт (т.е. на 1/3 больше).

Рассмотрим обратную задачу: есть Base64-строка и нужно понять сколько это будет байтов при раскодировке. Из формул выше можно выразить исходное кол-во байт:

[Исходное кол-во байт] = 3/4 * [Кол-во байт в Base64] 
или
[Исходное кол-во байт] = 0.75 * [Кол-во байт в Base64] 

Например: Base64-строка размером 2000 байт, декодируется в 1500 байт (т.е. на 1/4 меньше);

Преобразование в сетевой порядок расположения байт (Java)

HtoN (Host to Net) - узловой порядок в сетевой.

Функция htons преобразует узловой порядок расположения байтов в сетевой порядок расположения байтов. В short помещаются все возможные порты от 0 до 65535 (фактически от -32768 до 32767, т.к. в Java знаковый short):

static short htons(short val) {
    return (short) ((val & 0xff) << 8 
            | (val & 0xff00) >>> 8); // >>> - беззнаковый сдвиг
}

Функция htonl() преобразует узловой порядок расположения байтов в сетевой порядок расположения байтов. В int помещаются все возможные адреса IPv4 от 0.0.0.0 до 255.255.255.255 (фактически от -2 147 483 648 до 2 147 483 647, т.к. в Java знаковый int):

static int htonl(int val) {
    return (val & 0xff) << 24
            | (val & 0xff00) << 8
            | (val & 0xff0000) >> 8
            | (val & 0xff000000) >>> 24; // >>> - беззнаковый сдвиг
}
TAG: ,

Замер времени bash-скриптом

Замерить выполнение какого-либо процесса можно из кода самого процесса, но иногда это не получается сделать.

И на помощь приходит внешний замер времени с помощью bash-скрипта.

Сперва запоминаем время запуска (в наносекундах), потом фиксируем время окончания (в наносекундах) и находим разность времени окончания и начала.

Для перевода продолжительности из наносекунд в миллисекунды нужно разделить на один миллион.

Создайте скрипт (timer.sh) со следующим содержимым:

start_time="$(date +%s%N)"
# >>> тут пропишите запуск вашего процесса <<<
end_time="$(date +%s%N)"
elapsed="$(($end_time-$start_time))"
elapsed="$(($elapsed / 1000000))"
echo "Total of $elapsed milliseconds elapsed for process"

Удаление временно ненужных файлов

В очередной раз у разработчика закончилось место на жестком диске и начинается головоломка: "Что почистить?"

В данной заметке предлагается почистить файлы сборок старых проектов. Ведь непонятно, когда понадобятся сборки и понадобятся ли вообще, а место они занимают много.

Внимание все операции нужно делать с осторожностью, чтобы не удалить ничего лишнего!

Рассмотрим очистку места на примере удаления директорий build и node_modules.

Для начала просканируем директории на наличие build и node_modules:

find . -name build -type d -maxdepth 2
find . -name node_modules -type d -maxdepth 2

maxdepth - максимальная глубина сканирования

Следующим шагом вычисляем размеры найденных директорий и анализируем:

find . -name build -type d -maxdepth 2 -exec du -sh {} \;
find . -name node_modules -type d -maxdepth 2 -exec du -sh {} \;

Далее генерируем скрипты удаления директорий (это еще не удаление, а echo):

find . -name build -type d -maxdepth 2 -exec echo rm -rf {} \;
find . -name node_modules -type d -maxdepth 2 -exec echo rm -rf {} \;

Внимание все операции нужно делать с осторожностью, чтобы не удалить ничего лишнего!

Теперь можно взять сгенерированные скрипты и выполнить их все или точечно.

Место свободно!

Hibernate Envers проблема с размером приращения последовательности

В проект трехлетней давности не получилось прикрутить Hibernate Envers, возникала ошибка при запуске приложения:

Caused by: org.hibernate.MappingException: 
The increment size of the [hibernate_sequence] sequence is set to [1] 
in the entity mapping while the associated database sequence increment size is [50].

На том проекте Envers не сильно нужен был, поэтому обошлись без него.

На новом проекте все-таки возникла острая необходимость в использовании Envers, пришлось потратить время, чтобы решить данную проблему.

В нашем проект используется не hibernate_sequence, а sequence_generator, но принцип тот же.

Проблема в том, что в базе данных у sequence шаг приращения 50 (в целях производительности), а в коде у какой-то сущности с данным sequence приращение равно 1. Эта сущность - revinfo.

Нужно переопределить RevInfo, добавив следующую сущность в ваш проект:

@Entity
@Table(name = "revinfo")
@RevisionEntity
public class CustomRevisionEntity {
 
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
    @SequenceGenerator(name = "sequenceGenerator")
    @RevisionNumber
    private long rev;
 
    @RevisionTimestamp
    private long timestamp;
 
    public long getRev() {
        return rev;
    }
 
    public void setRev(long rev) {
        this.rev = rev;
    }
 
    public long getTimestamp() {
        return timestamp;
    }
 
    public void setTimestamp(long timestamp) {
        this.timestamp = timestamp;
    }
}

Ключевое изменение - это строки:

 @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
 @SequenceGenerator(name = "sequenceGenerator")

Которые явно задают sequence, который будет использовать таблица revinfo. В нашем случае - это sequence_generator.

Если помогло, отпишитесь в комментах. 🙂

p.s. Дефолтная revinfo какая-то косячная.

Идентификатор ревизии почему-то integer, а не long.

Также не читабельна ревизия в виде long timestamp, но есть две альтернативы. Либо только дата, либо только время. Что еще неудобнее, а хотелось бы человекочитаемую дату со временем.

Совет: лучше переопределить revinfo, даже если у вас нет проблем с последовательностями.

Полное удаление метрик Prometheus

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

Удалять метрики можно через API, но в моем случае это почему-то не помогло, поэтому пришлось применить радикальный способ.

Способ заключается в удалении содержимого директории базы данных временных рядов /var/lib/prometheus/.

Расположение этой директории можно узнать с помощью команды:

sudo service prometheus status

Выполнив команду, вы увидите --storage.tsdb.path /var/lib/prometheus/ - путь к базе данных временных рядов.

Степени двойки

Список степени двойки.

Степени двойки позволяют оценить алгоритмы, работающие за логарифмическое время.

Степень двойки (2x) Значение
01
12
24
38
416
532
664
7128
8256
9512
101 024
112 048
124 096
138 192
1416 384
1532 768
1665 536
17131 072
18262 144
19524 288
201 048 576
212 097 152
224 194 304
238 388 608
2416 777 216
2533 554 432
2667 108 864
27134 217 728
28268 435 456
29536 870 912
301 073 741 824
312 147 483 648
324 294 967 296
338 589 934 592
3417 179 869 184
3534 359 738 368
3668 719 476 736
37137 438 953 472
38274 877 906 944
39549 755 813 888
401 099 511 627 776
412 199 023 255 552
424 398 046 511 104
438 796 093 022 208
4417 592 186 044 416
4535 184 372 088 832
4670 368 744 177 664
47140 737 488 355 328
48281 474 976 710 656
49562 949 953 421 312
501 125 899 906 842 624
512 251 799 813 685 248
524 503 599 627 370 496
539 007 199 254 740 992
5418 014 398 509 481 984
5536 028 797 018 963 968
5672 057 594 037 927 936
57144 115 188 075 855 872
58288 230 376 151 711 744
59576 460 752 303 423 488
601 152 921 504 606 846 976
612 305 843 009 213 693 952
624 611 686 018 427 387 904

Резервное копирование в Vertica

В Vertica мы храним аналитические данные, которые в принципе можно не бэкапировать, т.к. объем данных - не велик, и можно перезалить в любой момент. Но это касается лишь новых данных, есть часть данных из старых источников: Oracle, Excel и текстовых документов, которые не желательно перезаливать. Лучше восстановить данные из бэкапа, чем собирать данные из старых источников, которые, возможно, уже и не существуют или с ними что-то случилось.

Снимать бэкап нужно под пользователем dbadmin:

su dbadmin

Создайте директорию, в которую будет записана резервная копия:

mkdir /home/dbadmin/backups20190207

В Vertica создание резервной копии и восстановление из нее осуществуляется с помощью утилиты vbr (думаю, это означает Vertica Backup and Restore). Для vbr нужно создать конфигурационный файл, который используется как для создания копии, так и для восстановления из нее.

Примеры конфигурационных файлов можно посмотреть в каталоге:

/opt/vertica/share/vbr/example_configs

Создайте новый конфигурационный файл /opt/vertica/share/vbr/example_configs/backup_restore_your_database.ini со следующим содержимым:

[Mapping]
v_your_database_node0001 = []:/home/dbadmin/backups20190207
#v_your_database_node0002 = []:/home/dbadmin/backups20190207

[Misc]
tempDir = /tmp

[Database]
dbName = YOUR_DATABASE
dbUser = dbadmin
dbPassword = YOUR_PASSWORD
#dbPromptForPassword = True

Нужно перечислить все узлы вашего кластера: v_your_database_node0001, v_your_database_node0002 и т.д. В нашем случае кластера нет, поэтому одна нода.

В dbName, dbUser и dbPassword укажите параметры подключения к вашей базе данных.

Пароль можно не прописывать, а каждый раз запрашивать, расскомментировав строку: dbPromptForPassword = True.

Возможные проблемы

В данной инструкции явно прописана учетка и пароль, т.к. без этого возникала проблема подключения:
Unable to log vbr invocations. Error SQL command "select log_vbr_invocations('Full Backup Task', '/tmp/vbr_2020-01-04-115520.log', 'SQL6F52TLDAFPCZ9YZEOH83P9YYKL9HW', 'Fail');" failed: vsql: FATAL 3781:  Invalid username or password
Error: SQL command "select name,catalogpath from v_internal.vs_nodes;" failed: vsql: FATAL 3781:  Invalid username or password
Backup FAILED.

Инициализация директории

Перед непосредственным созданием бэкапа нужно проинициализировать указанную в конфиге директорию:

/opt/vertica/bin/vbr -t init -c /opt/vertica/share/vbr/example_configs/backup_restore_your_database.ini

Создание резервной копии

После инициализации директории выполните команду:
/opt/vertica/bin/vbr -t backup -c /opt/vertica/share/vbr/example_configs/backup_restore_your_database.ini

Восстановление из резервной копии

Для восстановления из резервной копии выполните команду:
/opt/vertica/bin/vbr -t restore -c /opt/vertica/share/vbr/example_configs/backup_restore_your_database.ini