четверг, 10 апреля 2014 г.

Обработка текстов. Генераторы, фильтры, трансформаторы и "SAX на коленке"

Вводная.

Пусть есть множество форматов текстов.

Например:

RTF
DOC
DOCX
HTML
TXT

И пусть стоит задача сделать преобразование из одного формата в другой.

Как это сделать?

Можно конечно написать набор преобразований типа:
RTF -> DOC
RTF -> HTML
RTF -> DOCX
HTML -> RTF
HTML -> DOC

ну и так далее...

Только тут вот какая штука - в итоге получаем "Це из Эн по Ка".

Нездорово как-то...

Что делать?

У меня есть ОТВЕТ продиктованный моей МНОГОЛЕТНЕЙ практикой обработки документов.

Надо для начала ввести ОДИН так называемый "канонический формат", который включает в себя всё многообразие "вариантов использования".

Т.е. этот "канонический формат" должен уметь "вмещать в себя" все остальные форматы.

Одним из кандидатов на "каноничность" является XML (точнее XML-подобные структуры), в силу его "расширяемости".

Ну или NSAttributedString.

Подробности чуть позже.

А пока - опишу идею.

Если мы выбрали "канонический формат", то дальше мы делаем вот что:

Мы делаем набор "читателей" и набор "писателей".

Точнее сначала мы "стандартизуем" интерфейс "генератора" типа:

type
 IGenerator = interface
  procedure AddAttribute(aName: String; aValue: String);
  procedure StartStructure(aName: String);
  procedure StartArray(aName: String);
  procedure Finish;
 end;//IGenerator

"Мою" реализацию можно посмотреть тут - https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/RealWork/K2/k2Interfaces.pas и тут - https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/RealWork/K2/k2Prim.pas

Она выглядит так:

 Ik2TagGenerator = interface(IUnknown)
  {* Генератор тегов }
   ['{694DAEA5-80F3-4E12-9CCF-2B9950479734}']
   procedure pm_SetCharsInLine(aValue: Integer);
   function pm_GetCurrentStartLevel: Integer;
   function pm_GetNextGenerator: Ik2TagGenerator;
   procedure pm_SetNextGenerator(const aValue: Ik2TagGenerator);
   function pm_GetContext: Ik2Op;
   function Get_CurrentVersion: Integer;
   procedure Set_CurrentVersion(aValue: Integer);
   procedure AddStringAtom(TagID: Integer;
    const Value: AnsiString;
    aCodePage: Integer = CP_ANSI); overload; 
     {* добавить строковый атом. }
   procedure AddPCharLenAtom(TagID: Integer;
    const Value: Tl3WString);
     {* добавить строковый атом. }
   procedure AddObjectAtom(TagID: Integer;
    Value: TObject;
    Shared: Boolean = true);
   procedure AddStreamAtom(TagID: Integer;
    aStream: TStream);
     {* добавить атом из потока. }
   procedure AddTransparentAtom(TagID: Integer);
     {* добавить "прозрачный" атом. }
   procedure AddIntegerAtom(TagID: Integer;
    Value: Integer);
     {* добавить целочисленный атом. }
   procedure AddBoolAtom(TagID: Integer;
    Value: Boolean);
   procedure AddAtom(AtomIndex: Integer;
    TK: Tk2TypeKind;
    const Value);
     {* добавить атом. }
   procedure AddStringAtom(TagID: Integer;
    Value: Tl3PrimString); overload; 
   procedure AddAtomEx(AtomIndex: Integer;
    const Value: Tk2Variant);
   procedure Start;
     {* начать генерацию. }
   procedure StartChild(TypeID: Integer);
     {* начать дочерний объект тега. }
   procedure StartDefaultChild;
     {* начать дочерний объект тега с типом по-умолчанию. }
   procedure StartTag(TagID: Integer);
     {* начать вложеный тег. }
   procedure Finish(NeedUndo: Boolean = false);
     {* закрыть скобку этапа генерации. }
   procedure Rollback(CheckBrackets: Boolean = false);
     {* откатить все открытые "скобки". }
   function Pixel2Char(Pixel: Integer): Integer;
   function Char2Pixel(Ch: Integer): Integer;
   procedure AddStringAtomClone(TagID: Integer;
    Value: Tl3CustomString);
   procedure AddInt64Atom(aTagID: Integer;
    aValue: Int64);
     {* Добавляет 64-битный атом }
   property CharsInLine: Integer
     write pm_SetCharsInLine;
   property CurrentStartLevel: Integer
     read pm_GetCurrentStartLevel;
   property NextGenerator: Ik2TagGenerator
     read pm_GetNextGenerator
     write pm_SetNextGenerator;
     {* следующий генератор в цепочке. }
   property Context: Ik2Op
     read pm_GetContext;
     {* Контекст генерации }
   property CurrentVersion: Integer
     read Get_CurrentVersion
     write Set_CurrentVersion;
     {* Текущая версия формата }
 end;//Ik2TagGenerator

И тогда получаем примерно такую картину:

Входной файл -> Читатель -> Писатель (реализующий интерфейс IGenerator) -> Выходной файл.

Что мы имеем?

Читатель читает входной файл из конкретного формата и "преобразует" его к "каноническому формату".

Писатель получает "канонический формат" и преобразует его к конкретному выходному формату.

Т.е. получаем набор читателей:

TRTFReader
TDOCReader
TDOCXReader
THTMLReader
TTXTReader

И набор писателей:

TRTFWriter
TDOCWriter
TDOCXWriter
THTMLWriter
TTXTWriter

Примеры можно посмотреть тут:

https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/RealWork/EVD/evdReader.pas
https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/RealWork/EVD/evdWriter.pas

https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/RealWork/EVD/evdXMLReader.pas
https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/RealWork/EVD/evdXMLWriter.pas

https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/RealWork/dd/ddHTMLReader.pas
https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/RealWork/dd/ddHTMLWriter.pas

https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/RealWork/dd/ddRTFReader.pas
https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/RealWork/dd/ddRTFWriter.pas

ну и так далее...

Таким образом мы получаем уже не "Це из Эн по Ка", а N - Reader'ов и N Writer'ов. Со стандартизованным входом и выходом.

Любой Reader можно прикрутить к любому Writer'у.

Можно даже сделать нуль-преобразование:

TRTFReader -> TRTFWriter

или

THTMLReader -> THTMLWriter

и таком образом протестировать ПОЛНОТУ преобразования "формата самого в себя".

Далее вот как разовьём тему.

Предположим нам не только надо преобразовывать форматы, но и трансформировать их.

Ну например выкидывать какие-то данные или добавлять новые.

Тогда картинка получается следующая:

Входной файл -> Читатель -> Фильтр1 -> Фильтр2 -> Фильтр3 -> Трансформатор1 -> Трансформатор2 -> Трансформатор3 -> Фильтр4 -> Фильтр5 -> Фильтр6 -> Трансформатор4 -> Трансформатор5 -> Трансформатор6 -> Писатель -> Выходной файл

Что Фильтр, что Трансформатор - опять же поддерживают интерфейс IGenerator.

Примеры:

https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/RealWork/EVD/evdHyperlinkEliminator.pas

https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/RealWork/EVD/evdAllDocumentSubsEliminator.pas

https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/RealWork/EVD/evdCommentFilter.pas

https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/RealWork/EVD/evdCommentFilter.pas

Т.е. мы можем не только ПРЕОБРАЗОВЫВАТЬ форматы, но и ПРЕОБРАЗОВЫВАТЬ данные.

Простейший пример:

Входной файл -> TRTFReader -> Фильтр выкидывающий пустые параграфы -> TRTFWriter -> Выходной файл

Или:

Входной файл -> TRTFReader -> Фильтр выкидывающий гиперссылки -> TRTFWriter -> Выходной файл

Или:

Входной файл -> TRTFReader ->  Фильтр выкидывающий гиперссылки ->Фильтр выкидывающий пустые параграфы -> TRTFWriter -> Выходной файл

Мысль понятна?

Ещё на ту же тему:

http://18delphi.blogspot.ru/2013/04/blog-post_10.html
http://18delphi.blogspot.ru/2013/04/blog-post_8517.html
http://18delphi.blogspot.ru/2013/11/coretext.html
http://18delphi.blogspot.ru/2013/10/coretext_30.html
http://18delphi.blogspot.ru/2013/10/offtopic-evd-coretext.html
http://18delphi.blogspot.ru/2013/10/coretext-glyphrunnerdelegate.html
http://18delphi.blogspot.ru/2013/10/coretext-nsattributedstring.html

Аналогичным образом строится работа с редактором:

Чтение:

Входной файл -> Читатель -> Буфер документ (как разновидность генератора) -> Редактор

Запись:

Буфер документа (принадлежащий редактору) -> Писатель -> Выходной файл

Пример буфера(ов) тут:

https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/RealWork/K2/k2DocumentGenerator.pas

https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/RealWork/K2/k2DocumentBuffer.pas

https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/RealWork/K2/k2InPlaceGenerator.pas

Мысль понятна?

Тема вообще интересна?

Продолжать?

P.S. Ну и на закуску - ForkGenerator - https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/RealWork/K2/k2ForkGenerator.pas

Ну я думаю, что понятно что он делает:
                                                     
                                          Писатель1 -> Выходной файл1
                                         /
Входной файл -> Читатель -> ForkGenerator 
                                         \
                                          Писатель2 -> Выходной файл2

Мысль понятна?

P.P.S. Написали тут:

"Ну просто идея уж больно "естественная"
правда возникает она после 2-3 перекодировщика
с поправкой на 3-4 итерацию, при первой кажется проще написать прямой"

Комментариев нет:

Отправить комментарий