8 июня 2016 в 11:05

Парсер 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.
Автор: @Marazmatik
ONLYOFFICE
рейтинг 70,33

Комментарии (6)

  • +4
    По хабражизни я молчун, но тут не выдержал: огромное человеческое «спасибо» вам за этот гем!
    У меня как раз задача в pet project появилась — парсить pptx и xlsx. А тут такой подарок :)

    Offtopic: почему, когда меняли аккаунты на Read&Comment, не показали плашку с текстом «Писать комменты на хабре трудно! Задумайтесь, а надо ли оно вам?». Или только я такой замороченный, что проверяю 15 раз написанное :D
    • 0

      Судя по упавшему качеству комментов — только вы. С другой стороны, у таких комментаторов карма сливается довольно быстро, и, думаю, хабрамир скоро вернется к равновесию.

  • 0
    Не подскажете, как лучше изменять ряд свойств по всему документу? Мне часто нужно редактировать чужие презентации для использования в онлайн курсах, обычно путь такой:
    1. Редактирование в MS Office 2007/2010: поправить якоря картинок, заменить отбивку табуляцией или пробелами на нормальную, убрать ручные переносы, заменить «ручные» нумерованные списки на нормальные, поправить цвета на менее вырвиглазные, а шрифты на стандартные и т.д.
    2. Редактирование в ОпенОфис: сжатие презентации, конвертация в flash (для показа) и в pdf/png для скачивания

    Что хотел бы автоматизировать, но лень учить бейсик для макросов:
    1. Убрать по всему эффект «тень» для текста во всём документе. Без макросов эффект в тексте на элементах, полученных через вставка -> надпись, меняется только вручную.
    2. Поменять шрифт и выравнивание для определённых элементов, например, для заголовков
    3. Поменять цвет шрифта, если цвет фона такой-то (например, светло-серый на голубом фоне сменить на светло-желтый)
    4. Проверить границы, не выходят ли они за границу слайда, убрать висящие строки.

    Можете подсказать, как это грамотнее сделать? Так, чтобы xml развернулся, а потом обратно свернулся, но уже красивым и правильным :)
    • +1
      На данный момент посоветовать ничего, кроме макросов не можем.
      Но в ближайшее время (пару месяцев) у наших разработчиков в планах релиз утилиты, под кодовым названием 'doc-builder', которая позволит интерпретировать любой docx и xlsx файл в скрипт на языке программирования, основанном на JavaScript, произвести в нем любые правки, нужные вам, и обратно забилдить полученный скрипт в docx\xlsx файл.

  • 0
    Извините, а есть подобное описание структуры архива, но для PPTX (PowerPoint)? Стоит задача вытащить с каждого слайда текст и картинки, для перевода во внутренний редактор программы.
    Спасибо!
    • 0

      Как такого описания структуры нет, но если вы пользуетесь IDE (той же RubyMine) можно просто свободно потыкать в дебаггере по дереву структуры. Или же посмотреть в папке spec/pptx_examples тесты и по ним понять.
      Конкретно в вашем случае для выдирания текста код будет примерно такой — просто each-ами перебираем все что есть в презентации:


      pptx = OoxmlParser::PptxParser.parse_pptx(file_to_parse)
      text_array = []
      pptx.slides.each do |current_slide|
        current_slide.elements.each do |current_element|
          current_element.text_body.paragraphs.each do |cur_paragraph|
            cur_paragraph.runs.each do |current_run|
              text_array << current_run.text
            end
          end
        end
      end

      Для картинок будет примерно аналогично, свяжитесь со мной в ПМ если не разберетесь.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка