使用将MATLAB live脚本转换为ngscipt格式的示例处理ZIP/XML文件
在这个例子中,我们将展示如何使用OPC格式文件([打开包装约定](https://en.wikipedia.org/wiki/Open_Packaging_Conventions )),即与包含一组不同的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文件格式使用Open Packaging Conventions技术,这是zip文件格式的扩展。 代码和格式化的内容存储在XML文档中,该文档与使用Office Open XML格式的文档不同。 要处理这些文件的内容,只需将文件扩展名更改为 *.zip,然后通过Engee文件浏览器的上下文菜单解压缩。
让我们检查文件的内容 *.mlx 开箱后。 我们将需要从存档以下文件:
document.xml,其中存储文档的所有文本信息document.xml.rels-文档中包含的其他材料目录(公式,插图)
在文件夹中 media 收集插图,这些插图被插入到文档中,并在文件夹中 mathml -使用[MathML]格式的公式(https://ru.wikipedia.org/wiki/MathML )。
上传和处理MLX文件
为了简化代码的重用(以及为了清晰的演示),我们以一组函数的形式组织它。
以下是我们在这个阶段正在实现的功能:
*获取文件列表 mlx 位于目录中,
*解压缩存档并读取我们需要的文件,
*处理媒体文件链接,
*从XML文件获取单元格列表,
*将单元格从XML格式转换为JSON格式。
和一个辅助功能,用于处理嵌入在文件中的插图。:
*以所需格式获取有关插图的MIME信息(我们从"image"这样的名称中创建MIME标识符"image/png"。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
这三个模板将足以让我们生成一个新的。 .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在组织技术计算方面的强大能力,以及面向模型的设计工具。