Работа с файлами ZIP/XML на примере перевода live-скриптов MATLAB в формат ngscipt¶
В этом примере мы покажем, как работать с файлами формата OPC (Open Packaging Convensions), то есть с ZIP-контейнерами, содержащими набор разных XML и других файлов. Этот формат встречается повсеместно. Например, форматы такого типа используются во всех приложениях Office (DOCX
, XLSX
и т.д.) и во многих инженерных пакетах (Autodesk, Simulink, Engee).
Мы преобразуем файл технических расчетов из формата mlx
в ngscript
– перенесем все текстовые и кодовые ячейки, иллюстрации, гиперссылки и формулы из одного документа в другой.
Введение¶
Для работы с популярными форматами вроде Office Open XML
обычно доступны готовые библиотеки (например, XLSX.jl
для электронных таблиц). Но часто нам нужно оперативно обработать формат файла, для которого готовых библиотек еще нет, либо в которых не учитываются нужные вам элементы синтаксиса документа. Представим себя в таком сценарии, когда нам нужно произвести эту обработку вручную. В качестве учебного примера мы разберем программу перекодирования из формата LiveScript пакета MATLAB в формат ngscript
.
Для сравнительно низкоуровневой работы с форматами этих файлов нам потребуются следующие библиотеки:
using EzXML, ZipFile, JSON, Base64
Если какая-нибудь из них еще не установлена, запустите следующую ячейку, предварительно убрав символ #
(раскомментировав строку).
#]add EzXML ZipFile JSON Base64
Эту установку достаточно выполнить однократно, но иногда можно перезапускать ради обновления версий библиотек.
Из чего состоит файл MLX¶
Формат файла Live Code использует технологию Open Packaging Conventions, которая является расширением формата файла zip. Код и отформатированное содержимое хранятся в XML-документе отличающимся от документа с использованием формата Office Open XML. Чтобы поработать с содержимым этих файлов, достаточно поменять расширение файла на *.zip
, а затем разархивировать его через контекстное меню файлового браузера Engee.
Изучим содержимое файла *.mlx
после распаковки. Нам будут нужны следующие файлы из архива:
document.xml
, где хранится вся текстовая информация документаdocument.xml.rels
– каталог дополнительных материалов, включаемых в документ (формулы, иллюстрации)
В папке media
собраны иллюстрации, которые вставлены в документ, а в папке mathml
– использованные формулы в формате MathML.
Загрузка и обработка MLX файлов¶
Ради упрощения повторного использования нашего кода (а также ради чистоты изложения), организуем его в форме набора функций.
Вот какие функции мы реализуем на этом этапе:
- получение списка файлов
mlx
, лежащих в каталоге, - распаковка архива и чтение нужных нам файлов,
- обработка файла ссылок на медиа-файлы,
- получение списка ячеек из XML файла,
- перевод ячейки из XML формата в формат JSON.
И одна вспомогательная функция для работы с иллюстрациями, встроенными в файл:
- получение MIME-информации об иллюстрациях в нужном формате (из названия вроде "image.png" делаем MIME-идентификатор "image/png")
Первым делом, получим список файлов mlx
в каталоге.
function get_list_of_files( base_folder )
# Сканируем нужный нам каталог (не рекурсивно, без изучения вложенных папок)
filenames = readdir( base_folder)
# Отфильтруем только файлы с расширением `.mlx`
list_of_files = [joinpath(base_folder,fname) for fname in filenames if endswith( fname, ".mlx")]
end;
Распакуем mlx
файл и сложим интересующее нас содержимое в список.
function get_mlx_content( mlx_full_filename )
# Откроем архив для чтения содержимого
mlx_reader = ZipFile.Reader( mlx_full_filename )
# Прочитаем файлы, которые нас будут интересовать
document_file = read( [f for f in mlx_reader.files if endswith(f.name, "document.xml")][1], String )
rels_files_list = [f for f in mlx_reader.files if endswith(f.name, "document.xml.rels")]
relations_file = length(rels_files_list) > 0 ? read( rels_files_list[1], String ) : nothing;
# В этом списке будут пары название-содержимое
media_files_list = Dict([ ("../" * f.name, base64encode( read(f, String) )) for f in mlx_reader.files if occursin("media/", f.name ) ])
# Закроем архив
close( mlx_reader )
return document_file, relations_file, media_files_list
end;
Функция для чтения реестра медиа-материалов (картинок и уравнений).
function read_rels_file( input_string )
rels_dict = Dict()
if !(input_string == nothing) && !(input_string == "")
rels_tree = EzXML.parsexml( input_string );
ns = namespace( rels_tree.root )
for Rel in findall( "w:Relationship", root(rels_tree), ["w"=>ns])
rels_dict[Rel["Id"]] = Rel["Target"]
end
end
return rels_dict
end;
Создание списка ячеек в нужном нам формате.
function file_to_cells_list( input_string )
tree = EzXML.parsexml( input_string );
ns = namespace( tree.root )
# В этом списке будут собраны пара значений для каждой ячейки (параграфа) исходного документа: их стиль и контент
parsed_mlx = []
body_node = findfirst( "w:body", root( tree ), ["w"=>ns] );
for p in findall( "w:p", body_node, ["w"=>ns] )
# Сохраним стиль параграфа отдельно – обычно этот узел встречается один раз внутри каждого параграфа
pStyle = ""
pPr_node = findfirst("w:pPr", p, ["w"=>ns])
if !isnothing( pPr_node )
# Не будем обрабатывать параграф, если он представляет собой разделитель секций
if !isnothing( findfirst("w:sectPr", pPr_node, ["w"=>ns]) ) continue; end;
# Обычный стиль параграфа
pStyle_node = findfirst("w:pStyle", pPr_node, ["w"=>ns]);
if !isnothing( pStyle_node ) pStyle = pStyle_node["w:val"]; end;
end;
# Теперь пройдемся по всем узлам типа run (фрагментам параграфа)
pContent = []
element_name = nothing;
for run in findall("w:*", p, ["w"=>ns])
#run_name = run.name
if run.name == "pPr" continue; end;
runProperty_node = findfirst("w:rPr", run, ["w"=>ns])
run_content = run.content;
if run.name == "customXml"
element_name = run["w:element"];
if element_name == "image"
imageNode = findfirst("w:customXmlPr", run, ["w"=>ns])
for attr in findall("w:attr", imageNode, ["w"=>ns])
if attr["w:name"] == "relationshipId"
run_content = attr["w:val"]; end
end
end
elseif run.name == "hyperlink" # Тип фрагмента w:hyperlink
hyperlink_target = "w:docLocation" in attributes(run) ? run["w:docLocation"] : nothing;
run_content = (run.content, hyperlink_target)
else
element_name = nothing;
end;
append!( pContent, [(run.name, element_name, runProperty_node, run_content)] );
end
# Добавим стиль и параграф в список ячеек
push!( parsed_mlx, (pStyle, pContent) )
end
return (parsed_mlx, ns)
end;
Создадим функцию, которая возвращает мета-информацию про иллюстрации для ее включения в итоговый документ.
function process_image_info( image_name )
image_description = ""
image_name = lowercase(image_name)
if endswith( image_name, ".png" ) image_description = "image/png"
elseif endswith( image_name, ".jpg" ) image_description = "image/jpeg"
elseif endswith( image_name, ".jpeg" ) image_description = "image/jpeg"
elseif endswith( image_name, ".gif" ) image_description = "image/gif"
elseif endswith( image_name, ".svg" ) image_description = "image/svg+xml"
else image_description = "image/unknown"; end;
image_base64_prefix = "data:" * image_description * ";base64,"
return image_description, image_base64_prefix
end;
Перевод ячейки из формата XML в формат JSON.
function xml_text_cell_to_plain_text( cell_info, ns, rels_dict, media_files_list )
cell_type, content = cell_info
attachments = []
# Иногда стиль ячейки задаст нам начало выводимой строки (в markdown)
if cell_type == "title" plain_text = "# ";
elseif cell_type == "heading" plain_text = "## ";
else plain_text = ""; end;
for (run_name, run_element_type, runProperty_node, run_content) in content
if run_name == "pPr" continue
elseif run_name == "customXml"
# Если фрагмент – математическое выражение
if run_element_type == "equation"
plain_text = plain_text * "\$" * run_content * "\$"
# Если фрагмент – иллюстрация
elseif run_element_type == "image"
image_name = split( rels_dict[run_content], "/")[end]
image_content = media_files_list[rels_dict[run_content]]
image_type, image_prefix = process_image_info( image_name )
image_base64_content = image_prefix * image_content
append!( attachments, [(image_name, image_type, image_base64_content)] )
plain_text = "![$image_name](attachment:$image_name)"
end
# Если фрагмент – гиперссылка
elseif run_name == "hyperlink"
(hlink_name, hlink_target) = run_content
if isnothing(hlink_target) plain_text = plain_text * hlink_name;
else plain_text = plain_text * "[" * hlink_name * "](" * hlink_target * ")"; end;
# Если фрагмент – кодовое выражение (моноширинный шрифт)
elseif !isnothing(runProperty_node) && !isnothing(findfirst("w:rFonts", runProperty_node, ["w"=>ns])) && findfirst("w:rFonts", runProperty_node, ["w"=>ns])["w:cs"] == "monospace"
plain_text = plain_text * "`" * run_content * "`";
else
# В ячейке просто текст
plain_text = plain_text * run_content;
end
end
return (cell_type, plain_text, attachments)
end;
Проверочная функция для изучения входных файлов¶
Можно проверить, сколько ячеек, медиафайлов и прочих единиц информации было прочитано из каждого входного .mlx
файла.
for mlx_filename in get_list_of_files( "$(@__DIR__)/input" )
(mlx_file, rels_file, media_list) = get_mlx_content( mlx_filename )
println( )
println( "* Файл ", mlx_filename, " содержит:" )
cell_list, ns = file_to_cells_list( mlx_file )
println( length([c for c in cell_list if c[1] != "code"]), " текстовых ячеек")
println( length([c for c in cell_list if c[1] == "code"]), " кодовых ячеек")
rels_dict = read_rels_file( rels_file )
if length(keys(rels_dict)) > 0
print( length(keys(rels_dict)), " отсылок к внешним файлам" );
println( " (из них ", length([trg for (ref,trg) in rels_dict if occursin("../media", trg)]), " на иллюстрации)")
end;
end
Создаем шаблон для итоговых файлов ngscript¶
Теперь нам нужно подготовить файл .ngscript
, в который мы поместим ячейки нашего документа. Формат скриптов Engee обеспечивает обратную совместимость, хотя изменения иногда случаются. Чтобы иметь достаточно свежий шаблон документа ngscript
, воспользуемся в качестве шаблона тем же документом, который сейчас открыт перед вами – скриптом mlx_to_ngscript_parser.ngscript
.
Загрузим типовый файл ngscript
и сделаем из него:
- шаблон документа,
- шаблон текстовой ячейки,
- шаблон кодовой ячейки,
которые мы будем дополнять информацией по мере обработки входных mlx
файлов.
doc_template = JSON.parsefile( "$(@__DIR__)/mlx_to_ngscript_parser.ngscript" );
code_cell_template = [c for c in doc_template["cells"] if c["cell_type"]=="code" ][1];
text_cell_template = [c for c in doc_template["cells"] if c["cell_type"]=="markdown" ][1];
text_cell_template["isParagraph"] = false; # Правка для создания "обычного" параграфа, а не заголовка
doc_template["cells"] = [];
Посмотрим, какие шаблоны мы получили:
doc_template
text_cell_template
code_cell_template
Этих трех шаблонов нам будет достаточно, чтобы сгененировать новый .ngscript
файл для каждого файла .mlx
в каталоге входных файлов.
Наполняем JSON шаблон¶
В заключительной функции этого скрипта мы сделаем следующие действия:
- Пройдемся по всем
mlx
файлам в каталоге - Обработаем содержимое каждого файла
- Для каждого, создадим шаблон скрипта
ngscript
- Поочередно добавим в него текстовые и кодовые ячейки, не забыв про графические вложения
Кроме этого мы создадим всё необходимое для того, чтобы скрипт на языке MATLAB
можно было запустить в окружении Engee:
- Мы добавим вызов нужной библиотеки в начале файла,
- Для всех ячеек, где вызывается вывод графиков (например,
plot
иscatter
), мы добавим команды сохранения графика в файловое хранилище и вывод его средствами библиотекиImages.jl
, чтобы график оказался в отчете.
for mlx_filename in get_list_of_files( "$(@__DIR__)/input" )
# Узнаем всё, что нам нужно, про очередной изучаемый файл mlx
(mlx_file, rels_file, media_list) = get_mlx_content( mlx_filename )
cell_list, ns = file_to_cells_list( mlx_file )
rels_dict = read_rels_file( rels_file )
# Создадим шаблон под новый документ и добавим новую ячейку с кодом инициализаици
ngscript_doc = deepcopy(doc_template);
new_cell = deepcopy( code_cell_template );
new_cell["source"][1] = "using MATLAB\nmat\"cd('\$(@__DIR__)')\"";
push!( ngscript_doc["cells"], new_cell ); # Добавим ячейку в документ
for cell in cell_list
cell_type, plain_text, attachments = xml_text_cell_to_plain_text( cell, ns, rels_dict, media_list )
plot_counter = 0
if cell_type == "code"
# Перенесем MATLAB-код в ячейку и добавим обрамление в виде префикса mat"""..."""
new_cell = deepcopy( code_cell_template )
new_cell["source"][1] = "mat\"\"\"\n" * plain_text * "\n\"\"\"";
# Если в ячейке содержится подстрока plot, добавим
# MATLAB-инструкции сохранение графика
# и Engee-инструкции для его оторбажения в отчете
if cell_type == "code" && occursin("plot", plain_text) || occursin("scatter", plain_text)
new_cell["source"][1] = "mat\"\"\"\n" * plain_text * "\nsaveas(gcf,'plot_$(plot_counter).png')\n\"\"\"\nusing Images; load(\"plot_$(plot_counter).png\")";
plot_counter = plot_counter + 1;
end;
push!( ngscript_doc["cells"], new_cell );
else
# Добавим текстовую ячейку
new_cell = deepcopy( text_cell_template )
new_cell["source"][1] = plain_text
# Добавим в ячейку вложения (attachments)
if length(attachments) != 0
for (image_name, image_type, image_base64_content) in attachments
new_cell["attachments"][image_name] = Dict()
new_cell["attachments"][image_name][image_type] = image_base64_content
end
end
push!( ngscript_doc["cells"], new_cell )
end
end
# Сохраним скрипт под новым названием
new_script_name = replace( mlx_filename, ".mlx" => ".ngscript" )
new_script_name = replace( new_script_name, "input" => "output")
# Предварительно выполним сериализацию и сохраним ngscript
stringdata = JSON.json( ngscript_doc )
open(new_script_name, "w") do f write(f, stringdata); end;
end
Заключение¶
В этой демонстрации мы преобразовали файл из формата MLX/OPC
(на основе ZIP/XML
) в формат ngscript
(на основе JSON
). Получился инструмент, преобразующий часть информации из набора документов LiveScript
среды MATLAB
в документы .ngscript
платформы Engee.
Из отдельных функий этого примера можно почерпнуть представления об открытии файлов, работе с данными в формате XML, формировании документов на основе JSON. А еще этот пример демонстрирует сильные возможности Engee по части организации технических вычислений, доступных в связке с инструментарием модельно-ориентированного проектирования.