Парсер 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.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.