Appearance
Глава 5. Парсим словари
Мне удалось найти только два словаря египетских иероглифов. Оба они в pdf-формате:
- Dictionary of Ancient Egyptian Hieroglyphs
- Mark Vygus. Ancient Egyptian Hieroglyphic Dictionary, 2018
и еще сохранил в pdf-формате страничку из википедии List of Egyptian hieroglyphs. Эти файлы я положил в папку /resources в корне проекта.
Вначале я думал, что из pdf будет легко получить информацию с помощью консольных утилит вроде pdftotext. Но все опять оказалось сложнее чем казалось...
Пришлось написать приложение dictionary-parser
.
Добавление Node.js
приложения
В основе Nx
лежит система плагинов. Добавим плагин для Node.js
и запустим визард для создания нового приложения -
bash
$ npm i -D @nx/node
$ npx nx g @nx/node:application apps/dictionary-parser
NX Generating @nx/node:application
✔ Which linter would you like to use? · eslint
✔ Which unit test runner would you like to use? · none
✔ Which end-to-end test runner would you like to use? · none
✔ Which framework do you want to use? · none
Я не стал создавать e2e-тесты, а для юнит-тестов я хочу использовать vistest. Он работает значительно быстрее, чем jest
. Добавляем vitest
в проект, с помощью плагина vite -
bash
$ npm i -D @nx/vite
$ npx nx g vitest --project=dictionary-parser
И добавим общую библиотеку common
bash
$ npx nx g @nx/node:library libs/common
$ npx nx g vitest --project=common
Парсинг Pdf
Читать данные из pdf буду с помощью плагина pdf.js-extract.
bash
$ npm i pdf.js-extract arg
Получить данные просто, сложнее их интерпретировать - определить, где начало строки и где конец. Со словарем Вигуса все оказалось сравнительно легко, данные а нем расположены построчно. А вот с табличным видом пришлось повозиться. В результате я написал утилиту calculateBoundaries, которая определяет примерные границы столбцов в таблице. Примерные, потому что тесктовый блок в pdf может быть размещен произвольным образом и позиционирование в нём тоже может быть любым. Получив приблизительные границы колонок, я распарсил, наконец, и два оставшихся файла.
Потоки (Streams)
Хотя библиотека pdf.js-extract
не умеет работать с потоками, но их, однозначно, стоит использовать для дальнейшей обработки данных. Создаем PassThrough поток для чтения данных, далее используем pipeline для конвейерной обработки.
Формат JSON
плохо подходит для потоковой обработки сериализованных объектов. К счастью, есть формат NDJSON
, в котором каждая строка это json. Такой формат удобно читать и записывать в него по частям.
bash
$ npm i ndjson
$ npm i -D @types/ndjson
Аргументы командной строки
Далее, добавил передачу аргументы из командной строки. В Nx
передать аргументы в Node.js
приложение можно только в формате --args:
bash
$ nx serve my-app --args="param1=value1".
В npm аргументы для скрипта отделяются разделителем --.
bash
$ npm run app -- --param1=yes --param2=no
Мне показалось неудобным передавать параметры в стиле Nx
через --args
и я добавил скрипт runner
для маппинга параметров в формат Nx
. Для парсинга аргументов подключил минималистичную библиотеку arg.
bash
$ npm run parse-dictionary -- ancient --from=2 --to=5 --calculate-boundaries
Параметры командной строки
bash
PDF Dictionary Parser
Extracts information from PDF files of following types - ancient, vygus, heroes
Usage:
npm run parse-dictionary -- name [options]
or
npm run parse-dictionary -- name1 name2 [--debug]
Options:
--from <number> First page to process
--to <number> Last page to process
-d, --debug Enable debug mode
-b, --calculate-boundaries Only print columns boundaries
Examples:
# Process all the dictionaries
npm run parse-dictionary -- ancient vygus heroes
# Process 'ancient' dictionary from page 2 to 5 with debug
npm run parse-dictionary -- ancient --from=2 --to=5 --debug
npm run parse-dictionary -- ancient --from=2 --to=5 -d
# Calculates the ancient's boundaries from page 2 to 5
npm run parse-dictionary -- ancient --from=2 --to=5 --calculate-boundaries
npm run parse-dictionary -- ancient --from=2 --to=5 -b
Terminal UI
В Nx
, начиная с версии 21, добавился интерактивный UI. Мне удобнее работать в консоле по-старинке, поэтому отключаю его в скрипте через env
переменную NX_TUI
typescript
execSync(`nx serve dictionary-parser ${args}`, {
stdio: 'inherit',
env: {...process.env, 'NX_TUI': 'false'},
});
Показ прогресса выполнения
Чуть улучшим вывод в консоль - будем перезаписывать сообщения и добавим анимацию прогресса загрузки. Для этого подключим библиотеку log-update для анимации вывода на терминале и добавим цвета.
bash
npm i log-update cli-color
npm i -D @types/cli-color
Строки перезаписываются по ключу, для логирования файловых операций используем в качесве ключа имя файла. Для удобства вызова, обернул логирование в Proxy.
typescript
consoleProgress.fileName1.progress('stat reading file1...');
consoleProgress.fileName2.progress('stat reading file2...');
consoleProgress.fileName1.progress('continue reading file1...');
consoleProgress.fileName2.success('finished reading file2...');
consoleProgress.fileName1.success('finished reading file1...');
Пример анимации в терминале
Многопоточность (worker threads)
К сожалению, библиотека pdf.js-extract
не использует неблокирующие вызовы и при парсинге pdf она полностью блокирует выполнение другого кода, поэтому спиннер замерзает и файлы загружаются последовательно. Нужно это исправить.
Обычно, Node.js
выполняет JavaScript-код в однопоточном режиме. Но, начиная с версии v11.7.0 была добавлена официальная поддержка worker_threads. Это поволяет запускать потоки воркеров в изолированном контексте, с возможностью отправлять сообщения в главный процесс.