Записки жертвы высоких технологий

    Здравствуйте! Мой блог посвящен в большей своей части моей профессиональной деятельности на поприще использования технологий ORACLE для разработки баз данных OLTP и OLAP хранилищ данных. В заметках я периодически размещаю разнообразные SQL, PL/SQL и Java скрипты написанные мной и не только мной, ссылки на интересные источники в сети либо другую полезную информацию, которая каким либо образом касается моей работы с замечательными продуктами компании ORACLE.
    Вы можете связаться со мной по контактному емейлу, если у вас есть какие-либо вопросы, связанные с разработкой баз данных на основе продуктов ORACLE, буду рад помочь вам, если это будет в моих силах.

6 авг. 2013 г.

Парсинг кода RTF документов в PL/SQL

Набросал неспешно работающий парсер RTF для PL/SQL, предназначенный для пооператорного (с помощью regexp'ов) чтения RTF документа. Для парсинга небольших файликов, без наличия графики внутри, вполне приемлем. У меня есть в отладке "быстрая" версия, но пока что я не готов ее выложить на всеобщее обозрение - есть нерешенные вопросы по анализу структуры rtf.

Функция rtf_parse$lab() работает согласно Rich Text Format (RTF) Specification. Version 1.9.1 (C) Microsoft. Данные получает, как я уже говорил, пооператорно, regexp'ом находя начало следующего управляющего слова. Управляющие слова, согласно спецификации имеют вид:

({|}|[\*]\cw[params])[[<space>]PCDATA]
где
    {, } - открывающая или закрывающая скобки групп
    \* - признак необязательного исполнения управляющего слова [MANDATORY]
    \cw - управляющее слово [a-zA-Z], длиной до 32 символов [CW]
    params - параметр [0-9], длиной до 10 символов [PARAMS]
    <space> - пробельный разделитель \s+, может быть не пробелом, а переходом на новую строку
    PCDATA - кодированные данные в hexadecimal формате (см. спецификацию), длина неограниченна. При чтении требуется декодирование, при записи - кодирование данных согласно спецификации.[PCDATA]

Константы ошибок PARSER$RTF.ERR_* - задайте самостоятельно, я их взял из пакета с рабочей "быстрой" версией парсера.

Основной минус этого пакета разбора - невысокая скорость, связанная с чтением данных из CLOB-объекта и повсеместным использованием функций regexp_*, крайне упрощающих написание кода, но создающих проблемы с ростом времени разбора. Для написания "быстрой" версии парсера я использовал загрузку RTF-кода в память с разбиением на строки по 32кБт и считывание порциями по 64байта для анализа разделителей команд.

create or replace function rtf_parse$lab(p_rtf in clob) return number 
is
    -- позиционные параметры        
    l_pos_start number :=null; -- начало лексемы
    l_pos_end number := null; -- конец лексемы
    l_pos_current number := 1; -- текущая позиция в тексте RTF
    l_pos_data_separator number := 0; -- позиция разделителя управляющего слова и данных
    l_pos_cw_params number := 0; -- позиция параметров в имени команды
    -- параметры хранения данных
    l_chunk_clob clob; -- данные в CLOB (если длина более 32000)
    l_chunk_char varchar2(32000); -- данные в CHAR (если длина менее 32000)
    l_cnunk_pcdata_type number (1); -- тип data (константы c_PCDATA_TYPE_*)
    l_chunk_cw varchar2(64); -- управляющее слово
    l_chunk_cw_params varchar2(32); -- параметры управляющего слова
    l_chunk_mandatory number(1); -- флаг обязательного управляющего слова
    c_PCDATA_TYPE_CLOB constant number := 1; -- константа - данные в CLOB
    c_PCDATA_TYPE_CHAR constant number := 0; -- константа - данные в CHAR
begin
    -- проверяем входные параметры
    if p_rtf is null or dbms_lob.getlength(p_rtf) = 0 then return PARSER$RTF.ERR_INVALID_ARGUMENT; end if;
    if substr(p_rtf, 1, 5) != '{\rtf' then return PARSER$RTF.ERR_INVALID_FORMAT; end if;
    -- определяем структуры хранения данных
    dbms_lob.createtemporary(l_chunk_clob, true, dbms_lob.session);
    loop
        l_pos_current  := regexp_instr(srcstr => p_rtf, pattern => '({)|(})|(\\[a-zA-Z])|(\\\*\\[a-zA-Z])',position => l_pos_current, occurrence => 1, returnparam => 0);
        -- если достигнут конец строки - выходим  
        if nvl(l_pos_current,0) = 0 then return PARSER$RTF.ERR_NOERROR_EOF; end if;
        -- иначе устанавливаем текущую финишную границу лексемы
        l_pos_end := l_pos_current;
        -- если установлена стартовая граница лексемы, начинаем разбор
        if l_pos_start is not null then
            -- [CW, PCDATA]
            if l_pos_end - l_pos_start > 32000 then
                l_chunk_clob := substr(p_rtf, l_pos_start, l_pos_end - l_pos_start);
                l_pos_data_separator := regexp_instr(srcstr => l_chunk_clob, pattern =>  '{|}|(\s)+', position => 1, occurrence => 1, returnparam => 0);
                if l_pos_data_separator > 0 then 
                    l_chunk_cw := substr(l_chunk_clob, 1, l_pos_data_separator);
                    l_chunk_clob := substr(l_chunk_clob, l_pos_data_separator+1);
                else
                    l_chunk_cw := l_chunk_clob;
                    l_chunk_clob := null;
                end if;
                l_cnunk_pcdata_type := c_PCDATA_TYPE_CLOB;
            else
                l_chunk_char := substr(p_rtf, l_pos_start, l_pos_end - l_pos_start);
                l_pos_data_separator := regexp_instr(srcstr => l_chunk_char, pattern => '{|}|(\s)+', position => 1, occurrence => 1, returnparam => 0);
                if l_pos_data_separator > 0 then 
                    l_chunk_cw := substr(l_chunk_char, 1, l_pos_data_separator);
                    l_chunk_char := substr(l_chunk_char, l_pos_data_separator+1);
                else
                    l_chunk_cw := l_chunk_char;
                    l_chunk_char := null;
                end if;
                l_cnunk_pcdata_type := c_PCDATA_TYPE_CHAR;
            end if;
            -- [PARAMS] тут у нас есть l_chunk_cw и l_chunk_char/l_chunk_clob
            l_pos_cw_params := regexp_instr(srcstr => l_chunk_cw, pattern => '[0-1]+', position => 1, occurrence => 1, returnparam => 0);
            if nvl(l_pos_cw_params,0) > 0 then
                l_chunk_cw_params := substr(l_chunk_cw, l_pos_cw_params);
                l_chunk_cw := substr(l_chunk_cw, 1, l_pos_cw_params);
            else
                l_chunk_cw_params := null;
            end if;
            -- [MANDATORY]
            if substr(l_chunk_cw, 1, 3) = '\*\' then
                l_chunk_mandatory := 0;            
                l_chunk_cw := substr(l_chunk_cw, 4);            -- убираем \*\ начала оператора
            elsif substr(l_chunk_cw, 1, 1) = '\' then
                l_chunk_mandatory := 1;            
                l_chunk_cw := substr(l_chunk_cw, 2);            -- убираем \ начала оператора
            else
                l_chunk_mandatory := 1;
            end if;

            -- тут мы имеем [MANDATORY], [CW], [PARAMS], [PCDATA], можно выводить на экран
            dbms_output.put_line('>: cw='|| rpad(l_chunk_cw, 32, ' ') || chr(9) || 
                ' prm='|| nvl(l_chunk_cw_params, '<NO PARAMS>') || chr(9) || 
                ' cwtyp=' || case when l_chunk_mandatory = 1 then 'MANDATORY' else 'OPTIONAL' end || chr(9) || 
                ' datatyp=' || case when l_cnunk_pcdata_type = c_PCDATA_TYPE_CHAR then 'CHAR' else 'CLOB' end || chr(9) || 
                ' data=' || case when l_cnunk_pcdata_type = c_PCDATA_TYPE_CHAR then nvl(l_chunk_char,'<NO DATA>') else nvl(substr(l_chunk_clob,1,1024) || '<...next ' || dbms_lob.getlength(l_chunk_clob)|| ' symbols...>','<NO DATA>') end);
        end if;
        -- иначе устанавливаем текущую стартовую границу лексемы
        l_pos_start := l_pos_end;
        -- вычисляем необходимое смещение, согласно полученной инфе на стеке
        if substr(p_rtf, l_pos_current, 3) ='\*\' then --/*/[a-zA-Z]
            l_pos_current := l_pos_current+3;
        elsif substr(p_rtf, l_pos_current, 1) ='\' then --\[a-zA-Z]
            l_pos_current := l_pos_current + 1;
        elsif substr(p_rtf, l_pos_current, 1) ='}' then --}
            l_pos_current := l_pos_current+1;
        elsif substr(p_rtf, l_pos_current, 1) ='{' then --{
            l_pos_current := l_pos_current+1; 
        else -- вообще сюда попасть никак не должно
            return PARSER$RTF.ERR_INTERNAL_ERROR;
        end if;

    end loop;
    return PARSER$RTF.ERR_INTERNAL_ERROR;
end rtf_parse$lab;

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