Компания ABBYY создала хорошую программную оболочку для работы со словарями, однако не меньшим её вкладом в цифровую лексикографию стал побочный продукт разработки ABBYY Lingvo — язык словарной разметки DSL. Он давно уже вышел за границы Lingvo, стал самостоятельным стандартом и форматом для других словарных оболочек, в том числе одной из самых известных в своём роде — GoldenDict.
Но сама по себе компания ABBYY не достигла бы таких успехов без помощи многочисленной армии энтузиастов-лексикографов, маниакально год за годом оцифровывавших бумажные словари и конвертировавших словари цифровые — от миниатюрных специальных до огромных общего назначения.
Одна из самых известных и плодотворных групп давно уже работает на сайте forum.ru-board.com. Со временем там накопилась как обширнейшая коллекция словарей, так и основательнейшая база знаний и инструментов в помощь их создателям и редакторам. Было написано множество скриптов и программ, набор которых отражает историю и изменения популярности языков программирования, более или менее приспособленных для обработки текста. Тут и Perl с Python, и языки пакетных файлов для оболочек, и макросы MS Word и Excel, и компилируемые программы на языках общего назначения.
Однако до последнего времени один из языков почти не был представлен в данной сфере. Хотелось бы восполнить этот пробел и отдать должное стремительному росту мощности, функциональности и популярности языка JavaScript. Думается, он может оказать большую помощь современным программистам-лексикографам, особенно на границе сетевой и локальной лексикографии.
Создание локальной копии сетевого словаря проходит обычно несколько стадий: сохранение HTML-страничек при помощи программ вроде Teleport, очистка их от тегов при помощи регулярных выражений (в текстовых редакторах, макросами или скриптами), окончательная разметка их языком DSL. JavaScript в его Node.js варианте позволяет существенно сократить и облегчить этот путь, ведь этот язык родной для WEB и умеет оперировать сетевыми данными, не опускаясь до шаткого и изменчивого уровня кода и регулярных выражений, но работая на уровне элементов DOM.
Я попробую проиллюстрировать возможности языка и некоторых его библиотек на примере создания локальной копии одного из самых богатых и популярных толковых английских словарей, родившихся в сети: Urban Dictionary. Плоды недавних стараний можно оценить по этим раздачам на популярных трекерах:
rutracker.org/forum/viewtopic.php?t=5106848
nnm-club.me/forum/viewtopic.php?t=951668
kinozal.tv/details.php?id=1389116
Если же вы пока не планируете сохранять какой-то сетевой словарь, можете заглянуть сразу в третью часть статьи: там собраны примеры других частых задач при работе с электронными словарями, которые можно решить при помощи Node.js.
Впрочем, необходимо заметить, что программирование для меня всего лишь хобби. Это одновременно и предупреждение о непрофессиональности дальнейших примеров, и ободрение для тех, кто, как и я, имеет только гуманитарное образование.
Подразумевается, что читатель знает JavaScript в его чистом и прикладном вариантах и самостоятельно разобрался в основах Node.js. Если это не так, придётся начинать с азов или восполнять пробелы: JavaScript, DOM и Node.js.
Поскольку мы будем запускать скрипты только на своих компьютерах и при этом использовать новые возможности языка, рекомендуется установка последней версии Node.js.
I. Предварительный этап: получение списка адресов словарных статей
Возможны как минимум три алгоритма создания локальной копии сетевого словаря.
1. В самом худшем случае словарь не предоставляет никакого надёжного механизма для перебора всех статей. Тогда приходится анализировать шаблон адресов и подставлять в него подряд слова из какого-нибудь более-менее полного списка лексем данного языка (можно позаимствовать набор вокабул из самого большого оцифрованного толкового словаря), отбраковывая неудачные запросы.
2. Некоторые словари позволяют идти по цепочке от первой вокабулы до последней (через ссылку «следующее слово» или набор ссылок на ближайшие вокабулы). Это самый простой способ, но не самый наглядный: трудно будет предварительно оценить общее количество вокабул, а затем следить за прогрессом копирования. Поэтому, хотя тот же Urban Dictionary предоставляет эту возможность (на страничке каждого слова есть столбец ссылок на ближайшие предыдущие и последующие статьи), мы будем пользоваться третьим способом.
3. Если словарь имеет отдельный список ссылок на все словарные статьи, мы предварительно копируем весь набор этих ссылок в файл. Так мы получаем представление о предстоящем объёме запросов и возможность следить за процентом сделанного. Например, в Urban Dictionary по ссылкам типа www.urbandictionary.com/browse.php?character=A, www.urbandictionary.com/browse.php?character=A&page=2 и т.д. можно получить список адресов всех статей с вокабулами на указанную букву (типа www.urbandictionary.com/define.php?term=a, www.urbandictionary.com/define.php?term=a%5E_%5E и т.д.).
Итак, весь процесс сохранения словаря будет разделён на два этапа, за каждый будет отвечать отдельный скрипт.
Вот код первого, сохраняющего список ссылок на словарные статьи:
1. В начальной части скрипта мы загружаем необходимые библиотечные модули (или сразу необходимые методы из них, если они будут вызываться часто). Почти все модули встроенные, устанавливающиеся вместе с инсталляцией Node.js. Из внешних нам понадобится только модуль jsdom: сам по себе Node.js не умеет анализировать HTML-страницы и превращать их в DOM-дерево, и эту способность нам обеспечит упомянутый модуль (установка модулей проста, ведь вместе с Node.js инсталлируется менеджер модулей npm; просто откройте консоль, перейдите в папку с созданным скриптом и наберите npm install jsdom
, а затем дождитесь окончания скачивания и установки — сам модуль и необходимые ему подчинённые модули будут установлены в папку node_modules
, там их и будет искать наш скрипт).
После загрузки модулей скрипт определяет папку, в которую будет сохранять файлы (если пользователь не задал папку при запуске скрипта первым ключом командной строки, будет выбрана папка, в которой находится скрипт), и создаёт в ней три будущих документа: сам список адресов словарных статей; список обработанных страниц, с которых он будет брать эти адреса; отчёт о произошедших ошибках.
В завершение первой части создаются четыре служебные переменные, в которых будут храниться:
— массив английского алфавита (для поочерёдного добавления букв при создании URL страниц со ссылками; последним в массив добавляется символ *, отвечающий за список ссылок на вокабулы, начинающиеся со спецсимволов);
— предыдущий и текущий URL запросов (чтобы при ошибках определять, запрашиваем ли мы всё тот же злополучный адрес, или же ошибка касается нового адреса и должна быть внесена в отчёт);
— флаг пользовательского прерывания работы скрипта.
2. Во второй части мы устанавливаем обработчики для двух событий: для любого окончания работы скрипта (там мы закрываем все файлы и вызываем функцию, которая звуком привлекает внимание пользователя к любому важному событию) и для команды пользователя прервать работу программы (вызывается нажатием клавиш Ctrl+C
и переключает флаг прерывания, который проверяется перед каждым новым сетевым запросом).
3. В третьей части мы запускаем цикл запросов, в котором будем получать и сохранять списки адресов словарных статей. Часть делится на два логических блока.
а. Если файл отчёта об уже обработанных страницах пуст, значит, скрипт начинает работу с самого начала, а не после аварийного завершения или пользовательского прерывания. В этом случае мы выводим в окно консоли и в её заголовок первую букву алфавита, извлекаем её же из алфавитного массива и вызываем функцию получения страницы с построенным по шаблону URL.
б. Если файл не пустой, значит, скрипт уже работал. Нужно извлечь из файла последний обработанный адрес, чтобы сформировать запрос к следующему по очереди. Поскольку файл отчёта может быть большой, мы не будем загружать его в память целиком, но воспользуемся модулем, позволяющим прочитать файл строка за строкой (на всякий случай игнорируя пустые строки). Дойдя до конца, мы будем иметь нужный адрес в переменной. Проанализировав этот адрес, мы получим букву алфавита, которую последней обрабатывал скрипт, а также определим страницу списка адресов, следующую за сохранённой перед завершением работы программы. Исходя из этих данных, мы сократим алфавитный массив по нужную букву включительно, выведем в консоль новое начало обработки и вызовем функцию получения следующей страницы с шаблоном, учитывающим нужную букву и нужную страницу.
На этом процедурная часть скрипта завершается. Далее следуют три функции: одна служебная и две основные, вызывающие друг друга по очереди в цикле запросов.
4. Для звуковой сигнализации в служебной функции playAlert()
я выбрал консольный кросс-платформенный плеер из библиотеки ffmpeg (ключи запуска см. на сайте разработчиков), но можно воспользоваться любым другим плеером или понравившимся модулем генерации звука системными средствами. Звук тоже можно выбрать любой другой.
5. Функция getDoc(url)
отправляет запрос на получение очередной страницы со списком адресов словарных статей. Сперва она проверяет, не требовал ли пользователь прервать работу скрипта (скрипт работает несколько часов, поэтому может возникнуть необходимость в перерыве). Затем функция обновляет переменные прошлого и предстоящего запроса. Наконец, она даёт команду модулю jsdom запросить страницу, одновременно передавая соответствующему методу функцию, которую нужно будет вызвать с получением страницы.
В коде закомментированы две дополнительные возможности.
а. Если вы планируете запускать несколько скриптов параллельно для ускорения закачки, лучше делать это через анонимизирующий прокси-сервер. Я тестировал связку Fiddler + Tor (в небраузерном варианте из Expert Bundle) — хоть и не использовал её на всём протяжении работы скрипта, поскольку она одновременно замедляет скорость общения с сервером одного процесса, и мне не хотелось усложнять работу разделением задания на части для параллельных процессов. Пример реализации смотрите здесь.
Если вы всё же захотите распараллелить работу скрипта, вам нужно будет или указывать при запуске разные папки для выходных файлов, или запускать разные копии скрипта из разных папок. В этих папках уже должны находиться файлы отчёта об обработанных страницах, состоящие как минимум из одной строки, указывающей на адрес, непосредственно предшествующий задаваемой порции адресов.
б. Другая предосторожность от забанивания со стороны сервера — использовать задержки между запросами. Достаточно обернуть вызов метода в setTimeout и поэкспериментировать с размером пауз. Мой опыт показал, что серверам Urban Dictionary достаточно естественных пауз между запросами, никаких дополнительных перерывов вставлять не требуется.
6. Функцию processDoc(err, window)
вызывает модуль jsdom
, получив страницу или наткнувшись на ошибку, — отсюда два соответствующих аргумента функции.
Сначала функция проверяет аргумент err
: если он определён, значит, запрос был неудачным. В таком случае скрипт сигнализирует звуком, потом записывает сообщение в файл ошибок (если это первая ошибка с данным URL, а не очередная в цепочке повторных запросов), выводит информацию в окно и заголовок консоли и перезапускает запрос, обращаясь к функции getDoc(url)
с повтором аргумента.
Если же аргумент err
пуст, функция начинает анализировать полученный документ. Тут возможно несколько результатов и реакций на них.
а. На странице есть порция ссылок на словарные статьи. Тогда функция записывает список адресов этих ссылок в файл содержания словаря, записывает адрес текущей страницы в файл обработанных страниц, сообщает в консоль количество сохранённых ссылок и пытается найти URL следующей страницы с адресами. Если поиск успешен, программа выдаёт в консоль информацию о следующем запросе (букву и номер страницы) и отправляет его уже знакомой нам функции getDoc(url)
. Если поиск неудачен, программа проверяет алфавитный массив: если в нём остались буквы, она переходит к новой, если он пуст — завершает работу.
б. Если ссылок на странице нет, но адрес совпадает с запрошенным, значит, скорее всего, произошла ошибка не сервере (так бывает, например, когда сервер даёт ответ о временной недоступности). В таком случае скрипт повторяет запрос.
в. Если ссылок нет и адрес не совпадает с запрошенным, значит, произошло перенаправление. Такое возможно из-за одной особенности списка адресов словарных статей на Urban Dictionary: иногда предполагаемое количество страниц с этим списком на текущую букву бывает больше реального количества страниц, и при попытке запросить несуществующий номер страницы в конце буквенного блока сервер переадресует пользователя на главную страницу. В таком случае скрипт переходит к следующей букве, если алфавитный массив не пуст.
г. Если массив пуст, скрипт завершает работу.
В результате мы получаем файл с содержанием словаря. Два других файла имеют служебное значение, поэтому их можно удалить, ознакомившись, по необходимости, с происходившими ошибками.
II. Главный этап: получение текста словарных статей
Структура второго скрипта будет похожа, различия в основном будут происходить из существенного роста как времени работы (оно теперь будет измеряться не часами, а днями), так и сложности обработки страниц:
1. В первой части мы снова загружаем почти те же модули, затем проверяем уже два ключа запуска программы: первым задаётся пусть к папке с входящим файлом (в ней скрипт будет искать список ссылок на словарные статьи, сохранённый на предыдущем этапе), затем путь к папке для новых исходящих файлов. В обоих случаях, если ключи не заданы, используется папка самого скрипта. Скрипт проверяет наличие файла со ссылками — если его не окажется, программа завершит работу с соответствующим объявлением.
Далее мы определяем несколько переменных:
— регулярное выражение для форматирования больших чисел и число миллисекунд в часе — они будут регулярно использоваться в дальнейшем;
— контейнеры для постоянного или временного хранения данных (списка ссылок на словарные статьи, списка заголовков текущей статьи и списка её разделов — разных пользовательских интерпретаций вокабулы);
— уже знакомые нам переменные для предыдущего и предстоящего запросов;
— переменные для вычисления и вывода скорости работы скрипта;
— флаг пользовательского прерывания работы.
2. Вторая часть вводит уже описанные выше обработчики событий: завершения работы скрипта и команды пользователя прервать работу.
3. В третьей части мы проверяем по размеру основного выходного файла, первый ли это запуск программы или очередной после перерыва. Если работа только начинается, мы вписываем в файл будущего словаря метку BOM и начальные директивы формата DSL.
4. Четвёртая часть завершает процедурный раздел программы. В ней мы сначала считываем наш входной файл со списком ссылок на словарные статьи в контейнер, который будет определять дальнейшие запросы (он будет похож на алфавитный контейнер из первого скрипта — станет отправной точкой нашего цикла запросов). Затем, как и в предыдущем скрипте, мы проверяем файл отчёта об уже обработанных адресах: если в нём что-то есть, мы находим конечную строчку, в котором записана последняя успешно сохранённая статья словаря, сокращаем соответственно массив адресов для будущей обработки, запоминаем количество оставшейся работы и запускаем функцию, которая раз в час будет высчитывать скорость работы скрипта и приблизительно предсказывать время окончания работы (в гипотетическом случае её непрерывности). Затем выводим в консоль количество оставшихся адресов (это большое число, поэтому мы разделим его разряды пробелами для лучшей читаемости) и запускаем наш привычный цикл запроса и сохранения страниц. Если файл отчёта пуст, мы пропускаем его чтение, переходя сразу ко второй части перечисленных действий.
Если же пустым оказывается и входящий файл ссылок на словарные статьи, мы сообщаем об этом пользователю и завершаем работу программы до лучших времён.
Далее следуют функции: несколько мелких служебных и две основные, составляющие уже знакомые нам витки циклических запросов.
5. Функция playAlert()
ничем не отличается от одноимённой из первого скрипта.
6. Функцией secure(str, isHeadword)
мы будем регулярно пользоваться при сохранении словарных статей в файл DSL. У неё две задачи: перевести встречающиеся, как ни странно, в сетевом тексте управляющие символы (символы из начального блока ASCII) в условно читаемый вид, который не будет смущать компилятор DSL; и сократить слишком длинные слова из статей, выходящие за граничные требования формата DSL (заголовки будут сокращаться по другим правилам).
7. Функция setSpeedInfo()
будет работать параллельно основному течению программы. Раз в час она будет подменять информационную строку, в которой отображается скорость работы скрипта и оставшееся время (в начале работы в строке будут сплошные знаки вопроса, которые после первого часа заменятся на числа). Работа функции довольно прозрачна, следует лишь сделать два примечания: в переменной restMark
хранится количество оставшихся адресов на время предыдущего вычисления скорости; запуск звуковой сигнализации о пересчёте скорости совершается в этой функции асинхронно (то есть скрипт не ждёт конца звучания) — для этого вначале мы сохранили в переменной отдельный метод асинхронного запуска дочерних процессов.
8. Функция getDoc(url)
, отправляющая сетевой запрос, ничем не отличается от описанной в предыдущем разделе, включая закомментированные предосторожности от забанивания сервером и способы ускорения работы.
9. Функция processDoc(err, window)
, по сравнению с одноимённой из предыдущего скрипта, сохранит каркас, но будет существенно отличаться в части обработки и сохранения информации с полученной страницы — ведь нам придётся не просто записать набор ссылок, но проанализировать и преобразовать целый блок данных.
Впрочем, начало функции не претерпело изменений: мы всё так же проверяем аргумент err
и, если он определён, заносим информацию в файл отчёта об ошибках и перезапускаем неудавшийся запрос.
Если же пока ошибок нет, мы начинаем анализировать страницу. Возможны следующие повороты событий.
а. На странице находится ожидаемая словарная статья, адрес страницы не даёт оснований подозревать переадресацию.
В таком случае мы превращаем в массив все части словарной статьи, то есть все пользовательские интерпретации слова. Затем переходим к анализу каждого элемента.
Каждая пользовательская интерпретация состоит, как правило, из трёх основных подразделов: заголовка (он может равняться главному заголовку статьи или быть его вариантом с несущественными отклонениями), толкования и примеров (последняя часть факультативна).
Все заголовки мы будем накапливать в особом буфере (чтобы избежать добавления дубликатов, мы будем использовать новую для JavaScript структуру данных Set
, сохраняющую лишь уникальные элементы). Перед этим каждый заголовок мы будем прогонять через упомянутую функцию secure(str, isHeadword)
, затем создавать два варианта: заголовок для заголовочной части DSL и заголовок для помещения в самое начало раздела карточки DSL — ведь к этим областям предъявляются разные требования. В каждом варианте мы будем экранировать требуемые символы. Первый вариант перед помещением в буфер мы сократим согласно требованиям формата, если он будет слишком длинным.
Поскольку для извлечения текста из DOM-элементов модуль jsdom
пользуется свойством textContent
, имеющим ряд недостатков, мы дополнительно перестраховываемся от потери переводов строк, дополнительно вставляя их символьные варианты перед некоторыми тегами br
.
Затем мы последовательно обрабатываем части толкования и примеров перед сохранением их во временные переменные: удаляем пробельные символы в начале и конце строк, сокращаем повторяющиеся пробелы, экранируем требуемые спецсимволы, вставляем требуемые для тела карточки начальные отступы, перестраховываемся от потери пустых разделительных строк во время будущей компиляции DSL.
Закончив с основной частью, мы сохраняем в переменные служебную информацию: голоса за и против каждого толкования и время создания каждой интерпретации (удаляя избыточную для нас часть, появляющуюся в анонимных подразделах).
В конце мы объединяем все части в очередной элемент буфера, накапливающего части словарной статьи.
Затем мы проверяем, распространяется ли статья на несколько страниц. Если да, мы запрашиваем следующую страницу, чтобы повторить анализ и увеличить наши буферы заголовков и интерпретаций. Если статья занимает одну страницу (или если это последняя страница многостраничной статьи), мы записываем оба буфера в файл словаря, заносим адрес статьи в файл отчёта об успешно обработанных адресах, выводим информацию о количестве сохранённых интерпретаций и о скорости работы программы и очищаем буферы перед следующим витком цикла.
Далее, если массив адресов не пуст, мы запрашиваем следующую словарную статью. В противном случае программа завершает работу.
б. Ожидаемой статьи нет, но адрес не говорит о переадресации. Для этого могут быть как минимум две причины.
— Заголовок статьи остался на сервере или попал на него по ошибке — толкование было удалено или не было создано. В таком случае вместо толкования вставляется особый смайлик. Если программа находит его, она заносит соответствующее замечание в файл ошибок, выводит сообщение в консоль и переходит к следующему адресу (или же завершает работу, если массив адресов пуст).
— Если смайлика нет, возможна ошибка на сервере. В этом случае скрипт перезапускает запрос того же адреса.
в. Адрес страницы отличается от ожидаемого шаблона — произошла переадресация.
В этом случае программа сообщает об ошибке, требующей вмешательства пользователя (нужно понять причину переадресации: вас забанили на сервере, на сайте произошли изменения или случилось что-то другое, неучтённое предыдущим опытом), и завершает работу.
Если всё прошло успешно, в конце мы получаем готовый словарь на языке DSL. Служебные и промежуточные файлы после этого можно удалить (проанализировав файл ошибок, если вы не делали этого по ходу работы скрипта).
III. Дальнейшие операции с полученным словарём
В этой части я попробую привести примеры других задач, с которыми можно столкнуться при создании и обработке цифровых словарей, а также при конвертации их в разные форматы.
1. Смена кодировки
Компилятор ABBYY Lingvo требует кодировки UTF-16, а GoldenDict может работать и с кодировкой UTF-8 (которая для словарей на чистой латинице даёт в два раза меньший размер файла). Некоторые текстовые редакторы с разной скоростью обрабатывают большие файлы в этих двух кодировках — это тоже может стать причиной конвертации. Конечно, можно пересохранить файл в другой кодировке при помощи редактора, но это не всегда оказывается самым быстрым и удобным путём.
Node.js предоставляет простые способы конвертации больших файлов, быстрые и нетребовательные к памяти. Вот два примера для упомянутых кодировок (к имени нового файла перед расширением прибавляется метка кодировки):
2. Замены текста
Как и в предыдущем случае, для больших файлов данную операцию проще и экономнее производить скриптами, чем текстовыми редакторами. Особо ценной становится работа скриптов, если замены приходится ставить в зависимость от разных условий.
Приведу три примера с возрастающей сложностью.
а. Простая замена
Данный скрипт появился в ответ на просьбу одного из пользователей сделать разделы примеров из Urban Dictionary скрываемыми — то есть обернуть их в теги вторичного отображения. Написание скрипта и создание запрошенной версии словаря заняло несколько минут.
Для других простых случаев достаточно заменить в этом файле строчки с регулярными выражениями.
б. Удаление метки BOM из начала файла
Необходимость в таком действии возникла, когда я заметил, что PDF-принтер превращает BOM в пробел при создании PDF файла. Хотя бывают и другие случаи, когда метка BOM сбивает программы с толку (особенно в кодировке UTF-8).
в. Сложные замены
В следующем скрипте приводится пример замен, поставленных в зависимость от разных условий.
— Разные замены срабатывают в зависимости от принадлежности строчки к директивам DSL, к заголовку статьи или к её телу.
— Из директив убирается строчка с ключевым словом #ICON_FILE
, вставляемая порой при декомпиляции LSD-словарей (даже тогда, когда своей встроенной иконки у словаря нет).
— Слова из заголовков переводятся в нижний регистр, если их нет в массиве исключений. Такая необходимость иногда возникает, если все символы заголовков словаря даны в верхнем регистре. Массив можно наполнить именами собственными и аббревиатурами, можно хранить его в отдельном файле и загружать в начале работы скрипта.
— В тегах отступа из тела карточек число отступов увеличивается на единицу ([m1]
превращается в [m2]
и т.д.).
Можно использовать этот скрипт как шаблон для других замен — нужно лишь закомментировать невостребованные строки и подредактировать необходимые.
Во всех примерах этого подраздела кодировка входящего и исходящего файлов задана в коде (примеры других возможных кодировок закомментированы для возможной замены). Но можно задавать её и в ключе запуска скрипта, немного переделав код. Можно добавить и два ключа, если кодировка старого и нового файла должна будет различаться.
3. Подсчёт элементов
Следующим скриптом я считал количество элементов в готовом словаре Urban Dictionary, а именно число заголовков, карточек, интерпретаций внутри всех карточек и строк в файле.
4. Извлечение элементов
Следующим скриптом можно извлечь все заголовки из словаря DSL (например, для построения списка слов языка по самым большим словарям или для формирования списка исключений для описанного выше скрипта, переводящего заголовки в нижний регистр).
5. Проверка уникальности элементов
Для больших словарей лучше проверять уникальность заголовков до компиляции, чтобы исправить ошибки заранее и не повторять долгий процесс компилирования несколько раз. Следующий скрипт выводит результаты проверки в консоль, а при наличии дубликатов выводит их список в файл.
6. Конвертация DSL в текстовый формат
Читабельный текстовый вариант словаря, помимо самостоятельного значения, может стать основой для конвертации в форматы PDF и DjVu. Это обуславливает особенности выбранной структуры текстового файла, в частности его жёсткое форматирование — задание полей пробельными символами и однозначный перенос строк в границах абзаца.
Начало скрипта традиционное. Добавляются два новых модуля:
string-width — проверяет, нет ли в строке символов, занимающих по ширине место сразу двух знаков (как правило, это символы азиатских языков из группы CJK). Необходим для корректного подсчёта длины строки при разбиении абзаца на части заданного размера (для этого в конце файла определяется небольшая служебная функция hasCJK(str)
).
wordwrap: разбивает абзац на строчки с заданной шириной и возможным равным отступом с левого края.
Далее следует ряд служебных переменных, ответственных за увеличение читабельности текста, разбиение абзацев на подстроки, добавление основных и дополнительных отступов, удаление тегов DSL, отмены экранирования спецсимволов, определения границ карточек.
Потом, после регистрации уже знакомого нам обработчика завершения работы, скрипт ищет в папке словаря файл аннотации и записывает его содержимое в создаваемый текстовый файл (можно вставить проверку наличия аннотации, но, как правило, она всё же есть).
В конце концов, скрипт читает формат DSL строка за строкой, следя при этом за сменой карточек (за переходом от конца карточки к первому заголовку следующей) и сбрасывая при необходимости ширину отступов, и производит необходимые действия в зависимости он предмета анализа (заголовка или тела карточки):
— удаляет экранирование спецсимволов, теги DSL и «предохранители» от потери пустых строк;
— определяет необходимость добавления отступов (если попадаются теги [mN]), проверяет наличие «широких» символов (я в таких случаях грубо делил допустимую ширину строки пополам, но можно добавить более тонкую настройку в зависимости от количества таких символов), устанавливает текущую ширину строки и необходимость жёсткого переноса строк внутри слов (если попадаются слова, превышающие отведённое строке место) и соответственно форматирует абзац;
— записывает отформатированный абзац в текстовый файл.
Примеры получающегося текстового файла можно посмотреть на скриншотах в любой из упомянутых выше трекерных раздач.
7. Вставка в текстовый файл номеров страниц
Иногда программа, из которой приходится печатать в PDF, не имеет опции автоматической расстановки номеров страниц. Для таких случаев может пригодиться скрипт пагинации текстового файла.
Скрипт принимает два обязательных аргумента: путь к файлу и количество строк на страницу (которое он уменьшает на два, чтобы оставить место номеру страницы после пустой строки).
Для более удобного форматирования номеров мы загрузим модуль string, предоставляющий множество функций для работы со строчными данными. В частности, нам понадобится функция, помещающая строку посередине ряда пробелов заданной ширины, и функция, удаляющая правую часть этого ряда посте вставки. Таким образом мы сможем поместить номера посередине строки внизу страницы.
В процедурной части скрипт читает входящий файл строка за строкой, в нужных местах вставляет отформатированные номера страниц и записывает всё это в исходящий файл. В конце работы последний номер вставляется в самом низу завершающей страницы.
8. Разделение файла на части.
Тут мы приведём два примера разделений, зависящие от разных параметров.
а. Разделение по буквам алфавита
Иногда с большим словарём легче работать, если разделить его на алфавитные части. При этом, например, Adobe Acrobat умеет воспринимать все части как одно целое, если проиндексировать целиком их родительскую папку и потом осуществлять поиск по индексу всего её содержимого.
Перед началом работы скрипт создаёт дополнительный файл отчёта, в котором будет сохранять информацию о размере частей (о количестве строчек на файл с отдельной буквой). Это может представлять интерес как теоретический (статистика), так и практический (представление о времени последующей обработки частей). Впрочем, это факультативное усложнение.
Затем формируется массив с алфавитом (в конец помещается символ, с которого начинается первый заголовок последней части словаря, содержащей вокабулы со спецсимволами) и несколько служебных переменных.
Процедурная часть начинается с вызова функции newPart(chr)
, которая будет запускаться каждый раз, как нужно будет создавать файл для хранения очередной части. Эта функция:
— закрывает предыдущий файл (если это не начало работы);
— записывает количество строк в нём в файл отчёта;
— сбрасывает счётчик строк;
— создаёт новый файл с предстоящей буквой алфавита (проверьте, чтобы символ последней части словаря не был из числа запрещённых системой для употребления в именах файлов);
— вписывает BOM в начало новой части (в самую первую часть она перенесётся автоматически из целого словаря);
— выводит предстоящую букву алфавита в консоль и в файл отчёта;
— если массив алфавита не пуст, формирует регулярное выражение, которым будут проверяться строки входящего файла с целью поймать переход к новой алфавитной части.
После создания файла для первой части следует цикл чтения целого словаря: каждая строка проверяется упомянутым регулярным выражением, при необходимости создаётся новый файл; в текущий файл записывается текст очередной части, счётчик строк для отчётного файла увеличивается; в конце цикла записывается размер последней части и все файлы закрываются.
Этот скрипт может также пригодиться для разделения огромных файлов DSL, которые оказываются не по зубам компилятору ABBYY (например, в недавних версиях Lingvo при попытках компиляции словарей «Мультитран» из самого объёмного русско-английского направления компилятор вылетал с сообщением о нехватке памяти). В таком случае может понадобиться разделение словаря всего на две части, по средней букве алфавита (поэтому упомянутый выше алфавитный массив можно сократить до одной этой буквы).
б. Разделение по количеству страниц
Когда я пытался напечатать в PDF весь словарь целиком из текстового редактора для больших файлов, то наткнулся на ограничение: редактор отправлял на принтер не более 65000 страниц. Пришлось делить словарь на куски по 65000 страниц, печатать их по отдельности, а потом объединять части в Adobe Acrobat. Для этого и был написан следующий скрипт.
Скрипт напоминает предыдущий за некоторыми исключениями:
— скрипт принимает три обязательных аргумента при запуске: путь к целому словарю, количество строк на одну страницу, количество страниц на одну часть разделённого словаря;
— файл отчёта не создаётся, потому что размер частей будет относительно одинаковым;
— функция создания новой части упрощается (нет записи в файл отчёта и создания проверочного регулярного выражения), в название части вместо буквы алфавита вносится номер первой страницы в составе общей нумерации;
— цикл чтения/записи вызывает создание нового файла, основываясь не на первых буквах заголовков словарных частей, но на счётчиках строк и страниц.
Вот, пожалуй, и всё, чем я могу пока поделиться с создателями и редакторами электронных словарей, которые пожелают испытать возможности Node.js. Спасибо за время и внимание. Приношу извинения за все недостатки кода, связанные с непрофессиональным подходом. Удачи в цифровой лексикографии)
Автор: vmb