Что ж продолжим наши изыскания на предмет получения текста из различных форматов данных. Не так давно мы с вами научились вытаскивать текст из zipped-xml-based файлов (odt и docx), а также, в начале этой недели, из pdf. Сегодня мы продолжим с обещанным rtf.
Rich Text Format (он же rtf),вы могли бы подумать, достаточно забытый, хотя и не очень сложный формат представления текстовых данных. Что ж, относительно несложный для получения текста, но за свою историю: от своей первой версии до текущей 1.9.1 — он приобрёл под 300 страниц официально документации и огромное количество надстроек, которые в большей степени нам будут мешать при получении plain text'а. Попробуем их обойти...
Как уж повелось давайте заглянем вовнутрь rtf-файла и посмотрим, что там внутри:

Что мы видим?Я вижу наше любимое стихотворение «Парус». Мы видим изначально текстовый 8-битный формат данных. Это уже радует — когда в исходных данных текст, понимать, что происходит, гораздо проще. Теперь давайте разберёмся, как эти самые данные прочитать. Для этого я расскажу немного теории по теме.
Будем считать, что rtf состоит из управляющих слов, которые могут быть сгруппированы во вложенные множества. Управляющие слово начинается на обратный слэш (
Управляющие слово состоит из последовательности букв английского алфавита (от
Группированные множества определяют область действия управляющих слов. Таким образом, управляющие слова описанные внутри фигурных скобок работают только внутри них и всех дочерних подмножеств. Для того, чтобы правильно отработать какие слова имеют место сейчас — требуется вести стек управляющих слов. При открытии фигурной скобки создавать новый элемент-массив в стеке, в который сразу же добавлять данные предыдущего слоя стека, при закрытии скобки — удалять самый верхний слой.
Ещё стоит отметить, что некоторые управляющие слова могут быть закрыты с помощью добавления параметра ноль, а не создания новой подгруппы. Например, следующие варианты эквивалентны:
С устройством нового для нас формата мы познакомились, теперь зададимся вопросом, а где брать текст. Тут всё не так сложно, как может показаться — текст надо брать там, где текущая последовательность не идентифицируется, как управляющее слово. С парой исключений, естественно.
Во-первых, стоит отметить, что исходная кодировка rtf-файла — это ANSI, поэтому без всякий изысков сохранится только, английский текст. Нас же интересует, как минимум, русский текст, а ещё лучше Unicode, не так ли? Что правда, то правда — rtf хоть и старый формат, но сгодится на сохранение и того и другого.
Итак, в rtf'е есть возможность использования второй половины таблицы ASCII, та что от 128 и выше. С учётом текущей кодировки (выше управляющее слово
Ну и второй, более интересный вариант, это unicode-кодированные данные. Для них в формат включено лаконично короткое ключевое слово
Просто, да не очень. В rtf существует ещё одно ключевое слово
В связи с этим, большинство современных редакторов после unicode-управляющего слова ставят символ вопроса, как знак, что требуется показать вместо текущего символа. Но возможны и варианты, например:
Похоже, что накопленных нами данных будет достаточно, чтобы прочитать наши первые rtf-файлы. Поехали:
Что мы имеем в итоге? Данный код справится верно с большинством rtf-файлов, но есть несколько способов его улучшить. Во-первых, стоит добавить дополнительные отсечения на нетекстовые данные — у меня отсекаются только шрифты, цветовая палитра, тема оформления, бинарные данные, а также всё, что помечено, как «не читай меня, если не можешь» (
Что дальше? Дальше я бы хотел затронуть форматы электронных книг, такие как fb2, epub и подобные им. В связи с этим, я хотел бы обратиться за помощью к читателям: во-первых, какие ещё форматы электронных книг стоит посмотреть, а во-вторых, где можно найти побольше файлов, указанных вами форматов. Заранее спасибо :)
Ссылки:
Rich Text Format (он же rtf),
А что там внутри?
Как уж повелось давайте заглянем вовнутрь rtf-файла и посмотрим, что там внутри:

Что мы видим?
Будем считать, что rtf состоит из управляющих слов, которые могут быть сгруппированы во вложенные множества. Управляющие слово начинается на обратный слэш (
\
), группа обёрнута в фигурные скобки ({
и }
).Управляющие слово состоит из последовательности букв английского алфавита (от
a
до z
) и может быть завершено численным параметром (возможно отрицательным). Как вариант, слово может содержать один не цифро-буквенный ascii-символ. Всё, что не подпадает под эти правила, не является частью управляющего слова. Таким образом, последовательность вида \rtf1\ansi\ansicpg1251
без проблем делится на три слова rtf
с параметром 1 (major-версия формата), ansi
(текущая кодировка) и ansicpg
с параметром 1251 (текущая кодовая страница под номером 1251 — т.е. Windows-1251).Группированные множества определяют область действия управляющих слов. Таким образом, управляющие слова описанные внутри фигурных скобок работают только внутри них и всех дочерних подмножеств. Для того, чтобы правильно отработать какие слова имеют место сейчас — требуется вести стек управляющих слов. При открытии фигурной скобки создавать новый элемент-массив в стеке, в который сразу же добавлять данные предыдущего слоя стека, при закрытии скобки — удалять самый верхний слой.
Ещё стоит отметить, что некоторые управляющие слова могут быть закрыты с помощью добавления параметра ноль, а не создания новой подгруппы. Например, следующие варианты эквивалентны:
This is {\b bold} text
, This is \b bold \b0 text
= This is bold text
.Откуда брать текст?
С устройством нового для нас формата мы познакомились, теперь зададимся вопросом, а где брать текст. Тут всё не так сложно, как может показаться — текст надо брать там, где текущая последовательность не идентифицируется, как управляющее слово. С парой исключений, естественно.
Во-первых, стоит отметить, что исходная кодировка rtf-файла — это ANSI, поэтому без всякий изысков сохранится только, английский текст. Нас же интересует, как минимум, русский текст, а ещё лучше Unicode, не так ли? Что правда, то правда — rtf хоть и старый формат, но сгодится на сохранение и того и другого.
Итак, в rtf'е есть возможность использования второй половины таблицы ASCII, та что от 128 и выше. С учётом текущей кодировки (выше управляющее слово
\ansicpg
), конечно же. Для этого в RTF была введена последовательность вида \'hh
, где hh — это двоичный hex-код символа из таблицы ASCII.Ну и второй, более интересный вариант, это unicode-кодированные данные. Для них в формат включено лаконично короткое ключевое слово
\uABCD
с цифровым параметром ABCD. ABCD в данном случае код unicode-символа в десятичной системе счисления. Всё опять просто, как вы могли заметить. Просто, да не очень. В rtf существует ещё одно ключевое слово
\ucN
, которое тесно связано с Unicode. Дело в том, что формат RTF очень рьяно поддерживает совместимость со старыми устройствами, на которых возможно придётся открывать данный файл. Как вариант, подобное устройство (ну например компьютер с Windows 3.11 :) не сможет прочитать Unicode, что ему делать? Для этого после каждого unicode-символа, шифрованного ключевым словом \u
может быть указано от нуля до нескольких символов, которые должны быть отображены в случае, если rtf-viewer не способен отобразить или разобрать текущие данные (по документации, если просмотрщик не может отобразить верно данные, он должен их пропустить).В связи с этим, большинство современных редакторов после unicode-управляющего слова ставят символ вопроса, как знак, что требуется показать вместо текущего символа. Но возможны и варианты, например:
Lab\u915GValue
. Зададимся вопросом — сколько символов требуется отобразить, если нет возможности показать Unicode. Всё опять же не очень сложно — указанное выше ключевое слово \ucN
в качестве параметра N как раз и предоставляет это значение. Т.е. перед Unicode-данными обязательно появится что-то типа \uc1
, что скажет нам пропустить один символ после unicode'а.Давайте почитаем!
Похоже, что накопленных нами данных будет достаточно, чтобы прочитать наши первые rtf-файлы. Поехали:
Код с комментариями вы можете получить на GitHub'е.
- function rtf_isPlainText($s) {
- $failAt = array("*", "fonttbl", "colortbl", "datastore", "themedata");
- for ($i = 0; $i < count($failAt); $i++)
- if (!empty($s[$failAt[$i]])) return false;
- return true;
- }
- function rtf2text($filename) {
- $text = file_get_contents($filename);
- if (!strlen($text))
- return "";
- $document = "";
- $stack = array();
- $j = -1;
- for ($i = 0; $i < strlen($text); $i++) {
- $c = $text[$i];
- switch ($c) {
- case "\\":
- $nc = $text[$i + 1];
- if ($nc == '\\' && rtf_isPlainText($stack[$j])) $document .= '\\';
- elseif ($nc == '~' && rtf_isPlainText($stack[$j])) $document .= ' ';
- elseif ($nc == '_' && rtf_isPlainText($stack[$j])) $document .= '-';
- elseif ($nc == '*') $stack[$j]["*"] = true;
- elseif ($nc == "'") {
- $hex = substr($text, $i + 2, 2);
- if (rtf_isPlainText($stack[$j]))
- $document .= html_entity_decode("&#".hexdec($hex).";");
- $i += 2;
- } elseif ($nc >= 'a' && $nc <= 'z' || $nc >= 'A' && $nc <= 'Z') {
- $word = "";
- $param = null;
- for ($k = $i + 1, $m = 0; $k < strlen($text); $k++, $m++) {
- $nc = $text[$k];
- if ($nc >= 'a' && $nc <= 'z' || $nc >= 'A' && $nc <= 'Z') {
- if (empty($param))
- $word .= $nc;
- else
- break;
- } elseif ($nc >= '0' && $nc <= '9')
- $param .= $nc;
- elseif ($nc == '-') {
- if (empty($param))
- $param .= $nc;
- else
- break;
- } else
- break;
- }
- $i += $m - 1;
- $toText = "";
- switch (strtolower($word)) {
- case "u":
- $toText .= html_entity_decode("&#x".dechex($param).";");
- $ucDelta = @$stack[$j]["uc"];
- if ($ucDelta > 0)
- $i += $ucDelta;
- break;
- case "par": case "page": case "column": case "line": case "lbr":
- $toText .= "\n";
- break;
- case "emspace": case "enspace": case "qmspace":
- $toText .= " ";
- break;
- case "tab": $toText .= "\t"; break;
- case "chdate": $toText .= date("m.d.Y"); break;
- case "chdpl": $toText .= date("l, j F Y"); break;
- case "chdpa": $toText .= date("D, j M Y"); break;
- case "chtime": $toText .= date("H:i:s"); break;
- case "emdash": $toText .= html_entity_decode("—"); break;
- case "endash": $toText .= html_entity_decode("–"); break;
- case "bullet": $toText .= html_entity_decode("•"); break;
- case "lquote": $toText .= html_entity_decode("‘"); break;
- case "rquote": $toText .= html_entity_decode("’"); break;
- case "ldblquote": $toText .= html_entity_decode("«"); break;
- case "rdblquote": $toText .= html_entity_decode("»"); break;
- default:
- $stack[$j][strtolower($word)] = empty($param) ? true : $param;
- break;
- }
- if (rtf_isPlainText($stack[$j]))
- $document .= $toText;
- }
- $i++;
- break;
- case "{":
- array_push($stack, $stack[$j++]);
- break;
- case "}":
- array_pop($stack);
- $j--;
- break;
- case '\0': case '\r': case '\f': case '\n': break;
- default:
- if (rtf_isPlainText($stack[$j]))
- $document .= $c;
- break;
- }
- }
- return $document;
- }
Заключение
Что мы имеем в итоге? Данный код справится верно с большинством rtf-файлов, но есть несколько способов его улучшить. Во-первых, стоит добавить дополнительные отсечения на нетекстовые данные — у меня отсекаются только шрифты, цветовая палитра, тема оформления, бинарные данные, а также всё, что помечено, как «не читай меня, если не можешь» (
\*
). Во-вторых же, стоит ещё распарсить кодировку и кодовую страницу, для того чтобы вернее отобразить ключевые слова вида \'hh
.Что дальше? Дальше я бы хотел затронуть форматы электронных книг, такие как fb2, epub и подобные им. В связи с этим, я хотел бы обратиться за помощью к читателям: во-первых, какие ещё форматы электронных книг стоит посмотреть, а во-вторых, где можно найти побольше файлов, указанных вами форматов. Заранее спасибо :)
Ссылки: