Набросал неспешно работающий парсер RTF для PL/SQL, предназначенный для пооператорного (с помощью regexp'ов) чтения RTF документа. Для парсинга небольших файликов, без наличия графики внутри, вполне приемлем. У меня есть в отладке "быстрая" версия, но пока что я не готов ее выложить на всеобщее обозрение - есть нерешенные вопросы по анализу структуры rtf.
Функция rtf_parse$lab() работает согласно Rich Text Format (RTF) Specification. Version 1.9.1 (C) Microsoft. Данные получает, как я уже говорил, пооператорно, regexp'ом находя начало следующего управляющего слова. Управляющие слова, согласно спецификации имеют вид:
{, } - открывающая или закрывающая скобки групп
\* - признак необязательного исполнения управляющего слова [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;
Функция 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;
Комментариев нет:
Отправить комментарий