Парсер OOXML (docx, xlsx, pptx) на Ruby: наши ошибки и находки
Мы выложили парсер OOXML форматов на Ruby в open-source. Он доступен на GitHub’е и RubyGems.org, бесплатен и распространяется под лицензией AGPLv3. Всё как у модненьких Ruby-разработчиков.
Не секрет, что наш парсер не первый парсер OOXML на Ruby. Мы могли бы взять продукт сторонних разработчиков, но решили не брать. У тех решений, которые нам удалось найти, есть ряд проблем:
а) они давно заброшены разработчиками;
б) они поддерживают только базовую функциональность;
в) они, как правило, распространяются как три отдельные библиотеки. Зачастую парсер docx и парсер xlsx делали разные люди, поэтому их интерфейсы могут кардинально отличаться. Согласитесь, это неудобно.
Мы писали его под себя и свои задачи (тестирование редакторов документов), но потом поняли, что, возможно, он может помочь и другим Ruby-разработчикам, потому что он:
а) активно развивается;
б) поддерживает всю функциональность наших редакторов, а это очень много. Вот тут можно почитать;
в) называется OOXML парсер, так как работает и с docx, и xlsx, и pptx.
Отдельно остановимся на пункте б) — функциональность. Реализованы ли у нас все возможные фичи стандарта? Не-а. Стандарт ECMA-376 это четыре тома и в сумме over 9000 страниц (нет). На самом деле, около 7 тысяч. Можно выдыхать.
В общем, сами понимаете: реализовано у нас не всё. Но есть всё самое необходимое и более того: распознаются параграфы, таблицы, автофигуры. Есть поддержка таких комплексных вещей как
- Цветовые схемы;
- Стили параграфов и таблиц;
- Встроенные диаграммы;
- Свойства автофигур;
- Колонки;
- Списки.
Спойлер — Зачем нам вообще нужны парсеры?
Он появился на свет в отделе тестирования.
С самого начала автоматизированного тестирования у нас был принят единый концепт функциональных тестов.
Возьмем простейший тест:
1. Создаем новый документ.
2. Печатаем текст и выставляем у него свойство Bold.
3. Проверяем, что Bold выставлен.
Редактор ONLYOFFICE написан на Canvas, то есть, текст в документе представляет собой картинку. Проверифицировать толщину шрифта по картинке чрезвычайно сложно. А ведь применить Bold можно к любому шрифту!
В некоторых шрифтах (таких как Arial Black) Bold может вообще визуально никак не проявиться. Согласитесь, что сравнивать картинки imagemagick-ом — не самый оптимальный вариант.
Поэтому шаг верификации теста был выделен в отдельный пункт, а именно:
4. Скачиваем полученный файл в формате docx и проверяем, что у текста выставлен параметр Bold.
Таких параметров сотни. При этом ни одно из существующих решений не поддерживало ничего, кроме простейшего выхватывания текста, таблиц и еще пары вещей. Так мы и решили создавать свою библиотеку.
Постойте, спросите вы, вы же разрабатываете редактор документов, который умеет открывать все эти форматы на редактирование! Почему бы не использовать уже готовое решение из редактора и верифицировать тесты через него?
Почему нет?
1. В серверной части редакторов парсер написан на C++, а весь процесс автоматизированного тестирования построен на Ruby. С ходу было не совсем понятно, как это всё завязать друг с другом.
2. Сейчас у нас есть версия для Linux (и она основная), но в момент начала интеграции всей инфраструктуры для тестирования серверная часть документов поддерживала в качестве платформы только Windows. При этом в тестировании мы всегда использовали Ubuntu и производные. Чтобы склеить вот это всё, пришлось бы придумывать хитрые схемы.
3. А можно ли вообще серверный парсер считать эталоном? Верифицировать результаты работы продукта, используя сам продукт? Сомнительная идея.
Если вы когда-либо пытались заархивировать docx-файл, то могли заметить, что степень сжатия очень мала. Почему так? Всё просто: ooxml-файлы — это всего лишь заархивированный набор xml-файлов. Их структура достаточно тривиальна.
Для примера создадим простой файл с приветствием в нашем редакторе ONLYOFFICE и скачаем его в docx. Затем разархивируем как zip файл и посмотрим, где же хранится интересное нам мясцо этого документа.
Мы увидим такую структуру:
#tree
├── [Content_Types].xml
├── docProps
│ ├── app.xml
│ └── core.xml
├── _rels
└── word
├── document.xml
├── fontTable.xml
├── _rels
│ └── document.xml.rels
├── settings.xml
├── styles.xml
├── theme
│ ├── _rels
│ │ └── theme1.xml.rels
│ └── theme1.xml
└── webSettings.xml
Начинаем копаться во внутренностях. По порядку.
[Content_Types].xml — список mime-типов в документе. Холодно.
app.xml — метадата документа, приложение-создатель, статистика. Уже тепло, информация интересная, пригодится.
core.xml — метадата о последних модификациях.
document.xml — Ohh, that’s a bingo. В этом файле и прячется контент нашего документа, рассмотрим его позднее.
fontTable.xml — таблица шрифтов в документе. Пригодится.
document.xml.rels — список всех файлов в архиве, этот список будет нам очень полезен для комплексных документов, с картинками и графиками.
settings.xml — из названия понятно, что там хранятся разнообразные параметры документа, такие как дефолтный зум, разделители чисел и прочее.
styles.xml, theme1.xml и theme1.xml.rels — очень громоздкие, очень детальные файлы, содержащие параметры стилей и тем документа. Возможность понимать эти документы — одна из ключевых особенностей продукта.
webSettings.xml — настройка касаемо web-версии документа. Не самая популярная функциональность для docx, опустим.
Итак, оказалось, что в простом документе интересен именно word/document.xml.
Простая xml. Благо с парсингом xml на Ruby проблем никаких нет. Берем Nokogiri и получаем DOM-дерево. Ну, а дальше уже дело техники, почитаем стандарт (если не лень, документ же очень большой), или же просто старым добрым реверс-инжинирингом поймем, где в документе запрятан нужный параметр.
В начале работы мы допустили ряд ошибок, которые по мере возрастания осознанности сами поправили. Две самые значимые ошибки описаны ниже — благо они в прошлом, и нам уже не стыдно. Надеемся, наш опыт поможет другим не пробежаться по тем же граблям.
Огромные файлы
Итак, у нас есть задача обработать три разных формата документов. Как же мы организуем код для этого? Конечно, три файла по 4000 строчек кода (на самом деле, даже 4 файла по 4000 строчек кода, потому что были еще и общие методы для форматов).
Решение проблемы заняло больше всего времени. Пришлось приводить всё это хозяйство в аккуратный вид (хотя до сих пор иногда всплывает файлик на 300 строчек), выделять методы в аккуратные классы и т.д. Сейчас у нас более 200 файлов исходников вместо четырех. Править баги стало легче.
Отсутствие тестов
Логика была такая: мы пишем парсер, чтобы тестировать наш основной продукт ONLYOFFICE Document Server, зачем нам тестировать сам парсер?
НЕТ. НЕТ. НЕТ!!!
Сцена из жизни:
— Надо бы поправить вот тут кое-что, у нас цвет фигуры неправильно определяется.
— Да, сейчас, опечатка там была, одну букву исправил, закоммитил.
Итог:
Всё упало. Парсер, редактор, курс доллара, шалтай-болтай, самооценка.
А всего лишь надо было создать папочку `spec`, положить туда пару сотен файлов, проверить кучу параметров, чтобы спать ночами спокойно и знать, что тот коммит, который ты сделал перед уходом с работы, не сломает верификацию той опции, которая выставляется в меню 3-его уровня вложенности. Как мы это называем «в третьей звезде налево».
Но мы не только косячили. Здравые мысли у нас тоже были. Самые классные из них:
Использование RuboCop
RuboCop — это статический анализатор кода для Ruby, и мы его любим. Очень-очень. И всегда прислушиваемся к его мнению. Он помогает держать код в тонусе, не допускать глупых ошибок и строго следить, чтобы код не стал грязнее и хуже после очередного коммита (благодаря интеграции через overcommit).
Его работа выглядит так: после тяжелого рабочего дня ты забыл, что переменные в Ruby принято называть с маленькой буквы и пытаешься закоммитить код вида
— path_to_zip_file = copy_file_and_rename_to_zip (path_to_file)
+ ZIP_file = copy_file_and_rename_to_zip (path_to_file)
В этом случае произойдет ошибка:
Analyze with RuboCop…[RuboCop] FAILED
Errors on modified lines:
ooxml_parser/lib/ooxml_parser/common_parser/parser.rb:8:7: E: dynamic constant assignment
Закоммитить этот код без дополнительных манипуляций (`SKIP=RuboCop git commit -av`) не получится. Это отличная защита от дурака.
Ориентация на open-source проекты
Практически с самого начала разработки парсера мы ориентировались на другие open-source проекты. Хотя мы не были уверены, что наш код будет выложен в open source, но всегда были к этому готовы. Когда поступила команда «Выкладывайте», мы просто нажали кнопочку «make public» в GitHub’e и всё, никаких дополнительных причесываний и прочего.
В этом большая заслуга того же RuboCop: мы часто подглядывали в их код, думая как лучше организовать ту или иную тему, например Changelog, структуру гема. Кроме того, вся разработка, коммиты, история изменений и прочего изначально велись на английском.
Использование базы документов
При тестировании парсеров нам пригодились наши предыдущие наработки — крупная база со всякими странными, огромными и непонятными файлами трех форматов.
Когда-то давным-давно, на ранней стадии разработки редакторов ONLYOFFICE мы собрали эти файлы на просторах интернета — на них проверялся рендеринг сложных и нестандартных документов. Спустя несколько лет по этой же базе документов прогнался парсер. В результате нашлось достаточно много проблем разного уровня сложности и, потратив пару недель на их устранение, мы получили отличный продукт.
Итак, все доступно, берете, добавляете в своё Ruby-приложение, парсите docx, строите по ним статистику, анализируете, как работает ваша бухгалтерия по xlsx файлам, узнаете, какой мемасик спрятал ваш PM на презентации продукта в четвертом слайде. И все это бесплатно.
А еще можете находить проблемные файлы и создавать issue на GitHub’e, будем это разруливать. Можете даже править сами и слать Pull Requests.