以 MATLAB 实时脚本翻译为 ngscipt 格式为例,处理 ZIP/XML 文件¶
在本示例中,我们将展示如何处理 OPC(Open Packaging Convensions)文件,即包含一组不同 XML 和其他文件的 ZIP 容器。这种格式随处可见。例如,所有 Office 应用程序(DOCX
,XLSX
等)和许多工程软件包(Autodesk, Simulink, Engee)都使用这种格式。
我们将把一个技术计算文件从mlx
转换为ngscript
格式--将所有文本和代码单元格、插图、超链接和公式从一个文档转移到另一个文档。
简介¶
在处理Office Open XML
等常用格式文件时,通常都有现成的库(如用于电子表格的XLSX.jl
)。但我们经常需要快速处理一种文件格式,而这种格式还没有现成的库,或者库中没有考虑到您需要的文档语法元素。让我们想象一下在这种情况下,我们需要手动执行这种处理。作为教程示例,我们将研究一个将 MATLAB 的 LiveScript 格式转码为ngscript
格式的程序。
为了对这些文件格式进行相对低级的处理,我们需要以下库:
Pkg.add(["ZipFile", "EzXML"])
using EzXML, ZipFile, JSON, Base64
如果尚未安装其中任何一个,请运行以下单元格,并删除#
符号(取消注释字符串)。
#]add EzXML ZipFile JSON Base64
只需安装一次即可,但有时为了更新库版本,可以重新启动。
MLX 文件的内容¶
Live Code 文件格式使用开放打包约定技术,是 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 = ""
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
这三个模板足以让我们为输入文件目录中的每个.mlx
文件生成一个新的.ngscript
文件。
填充 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
)。结果是一个工具,可将MATLAB
环境中的一组LiveScript
文档中的部分信息转换为Engee平台中的.ngscript
文档。
从本示例的各个功能中,您可以了解打开文件、处理 XML 数据和生成基于 JSON 的文档。本例还展示了Engee强大的技术计算能力,可与模型驱动设计工具结合使用。