Текст любой ценой: PPT

    Некоторое время назад мы с вами обсуждали получение чистого текста из различных форматов данных: будь то PDF или DOC. В одном из обсуждений был высказано предположение, что при парсинге презентаций PowerPoint я заработаю геморрой или другую страшную болезнь мягкой точки. Что ж, волей судеб мне пришлось доставать текст и из этого «сладенького» формата. Скажу честно, геморрой заработать не удалось, а вот класс для парсинга презентаций вышел.

    НеМного о формате PPT


    Как и DOC, PPT является надстройкой над форматом WCBFF (структурированного бинарного файлового формата), о котором вы можете прочесть в статье о MS Word, написанной мною ранее. Отмечу лишь то, что за время тестирования я нашёл несколько ошибок (а кроме того, повидал кучу «битых» файлов, из которых приходилось получать текст) в старых реализация WCBFF и DOC, поэтому советую обновить свои исходники тем, кто использует мои наработки.

    Так, мы отвлеклись. Продолжим беседу о PowerPoint. В отличие от DOC, мы будем работать не с «файлами» WordDocument и 1Table, как раньше, а со специфичными для презентаций: Current User и PowerPoint Document. Наличие обоих «файлов» в «файловой системе» CBF-файла обязательно для презентаций. По ним же можно определить, что перед нами презентация, в случае ошибочного расширения.

    Итак, чтобы начать получать данные из PPT стоит прочесть небольшой «файл»-запись (или «файл», состоящий из одной записи CurrentUserAtom) Current User. Эта запись содержит техническую информацию о том, кто редактировал этот файл в последний раз, но это не самое важное. В этом блоке есть информация о смещении к первой записи UserEditAtom, речь о которой пойдёт чуть ниже.

    Сейчас же я расскажу, как нужно читать записи в PPT. Любая запись в презентации содержит специальный заголовок rh, который содержит техническую информацию о ней. Для этого достаточно прочесть первые 8 байт любой записи. Первое слово обычно не содержит нужной информации, а вот следующие 6 байт нам потребуются. WORD по смещению 2 (rh.recType) идентифицирует тип записи, по которому можно узнать, что делать с записью дальше. Long по смещению 4 (recLen) — длина записи без учёта заголовка в восемь байт. Такой способ записи достаточно удобен и позволяет избежать многих ошибок при разборе файла презентации

    Что дальше? Вернёмся к записи UserEditAtom. Эта запись находится уже в PowerPoint Document. В последствии работать мы будем только с этим «файлом». С помощью прочтения этой и связанных с ней записей мы должны построить такую дивную штуку, как массив смещений PersistDirectory, с помощью которого мы будем искать главную структуру документа PowerPoint — DocumentContainer. Для этого мы должны прочитать текущую запись UserEditAtom, найти в ней смещение offsetPersistDirectory к текущей «live» версии PersistDirectory и смещение offsetLastEdit к следующей записи UserEditAtom. Так продолжим получать смещения, пока не наткнёмся на нули в DWORD'е offsetLastEdit.

    После получения всех смещений offsetPersistDirectory мы должны создать эту самую PersistDirectory. Идём по смещения в обратном порядке и читаем записи PersistDirectoryAtom. Они содержат в себе массив записей PersistDirectoryEntry. Каждая из коих содержит номер persistId первого entry и их количество cPersist в текущей записи. После этой информации идёт массив смещений к объектам PersistDirectory. Это самый важный массив, по которому мы будем находить ссылки на все объекты презентации.

    Теперь вернёмся к последнему прочитанному UserEditAtom и найдём там поле docPersistIdRef. Это номер самого важного объекта DocumentContainer в PersistDirectory. Прочитаем его. В нём хранится вагон и маленькая тележка информации о текущей презентации: колонтитулы, заметки к слайдам и главное — запись SlideListWithTextContainer, содержащая всякое разное о слайдах.

    Нас будут интересовать всего три типа записей, что хранятся в этом главном блоке: TextCharsAtom, TextBytesAtom и SlidePersistAtom. С первыми два всё несложно: это unicode-текст на слайде и обычный ANSI, соответственно. Другое дело, когда вместо текста получаем ссылку на слайд SlidePersistAtom. По ней мы должны прочитать объект Drawing, который (sic!) не является объектом PPT. Да-да, внутри слайда в этом случае внедрён объект MS Drawing, с достаточно неприятной структуру вложенных записей.

    Когда я в первый раз узнал об этом факте, скажу честно, я расстроился. Ещё 600 с гаком страниц документации. Но ODRAW, как оказалось, построен на тех же rh-заголовках с теми же recType'ами, что и PPT. Это позволило облегчить себе задачу и чуть-чуть смухлевать с помощью поиска в Drawing-объекте всё тех же атомов TextCharsAtom и TextBytesAtom по их recType'ам.

    Реализация


    Код с комментариями вы можете получить на GitHub'е. Пока ещё немножко сыро, но я думаю, что в ближайшее время найду все подводные камни. Основные ошибки выскакивают именно из-за не совсем правильно (?) прочтения PersistDirectory. Если у кого есть уточнения, то я с удовольствием их выслушаю.

    Литература




    Текст любой ценой


    Поделиться публикацией

    Похожие публикации

    Комментарии 31
      +1
      Спасибо, очень полезная серия статей, боюсь показать очевидным и невероятным, но неужели следующая статья будет про xls?
        0
        Пока, увы, не требуется. Кроме того, я хоть и не разбирался с XLS вообще, но боюсь там будут серьёзные проблемы с формулами, что сделает реализацию достаточно дикой. Могу ошибаться.
          0
          А вообще, есть желание переписать свои наработки (WCBFF и иже с ним, и PDF) на Python'е, который сейчас изучаю. Есть мнение, что код получится меньше и понятней.
            0
            Есть одна правда — заключается в том, что для xls есть вплоне вменяемая библиотека — как для забивания, так и для потрошения…
            http://www.codeplex.com/PHPExcel/. Использовал её для генерации прайсов.
              0
              XLSX only. Поправьте меня, если я не прав.
                +1
                Нет, почему же?
                «Read different file formats into your spreadsheet object

                * Excel 2007 (spreadsheetML)
                * BIFF5 (Excel 5.0 / Excel 95), BIFF8 (Excel 97 and higher)
                * PHPExcel Serialized Spreadsheet
                * Excel 2003 XML format
                * Symbolic Link (SYLK)
                * CSV (Comma Separated Values)»
                  –1
                  Признаю свою ошибку, мельком лишь пробежался.
                    0
                    Да ничего, просто я её юзал для конкретных задач:)
                      0
                      Да ничего, просто я её юзал для конкретных задач:)
                    0
                    PHPExcel умеет как читать, так и писать xls разных версий
                +1
                PHPExcelReader умеет парсить XLS файлы:
                sourceforge.net/projects/phpexcelreader/
                0
                XLS хорошо поддерживается в Apache POI
                0
                А я вот делаю импортёр XLS. В пятницу показал первые цифры. Из целой кучи команд реализовал только две — NUMBER и MULRK. Завтра буду исправлять эту недоработку :)
                  0
                  Молодец, я рад, что есть люди, которые страдают занимаются подобными задачами, несмотря на кажущуюся их начальную сложность.
                    0
                    иногда проще купить готовый компонент в систему, чем тратить время(которое обычно — деньги) на создание собственного.

                    для XLS есть хороший продукт: XLSParserPro
                      0
                      XLSX only. Поправьте меня, если я не прав.
                        0
                        не прав, так как данной библиотекой приходилось пользоваться года 3-4 назад — тогда xlsx мы не видели :)
                          0
                          Ой, простите не Вам. Выше про PHPExcel. Миль пардон )
                            0
                            да. только что перепроверил — правильное название: Php Excel Parser Pro
                    +1
                    Есть мнение что для всех перечисленных форматов файлов (кроме, наверное, pdf) можно использовать open office в headless режиме, как это и сделали парни из Alfresco. Понятно, что это куда более громоздкое решение, но, как говорится, 30 гектар леса разом и под корень.
                      0
                      Вы даже не представляете в каких стеснённых условиях у меня идёт разработка: save_mode, max_execution_time = 30, выключенные shell_exec и иже с ним. Но заказчик не хочет менять хостинг. Что ж работаем на том, что есть… Ясно дело, что можно использовать стороннее, отлаженное, классное. Но иногда не получается, хотя очень хочется.
                      0
                      Большое спасибо, пригодится не только для PHP.
                        0
                        Не понятно — как выстраивается последовательность того же текста?
                        на слайдах он может быть абсолютно не упорядочен.
                        Не получится ли на выходе (в некоторых случаях) текст не поддающийся анализу?
                          0
                          Это ещё предстоит выяснить. В худшем случае, придётся ещё и положение текста читать на странице и исходя из этого делать какие-либо предположения. Как уже говорил, скрипт ещё сыроват — в процессе доработки.
                          0
                          В свое время то же интересовался чтением из Майкросовтовских форматов.
                          Нашел класс ExcelReader и ExcelWriter, которые по-моему находятся в pear

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

                          Самое читаемое