Engee 文档
Notebook

以 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 格式的程序。

为了对这些文件格式进行相对低级的处理,我们需要以下库:

In [ ]:
Pkg.add(["ZipFile", "EzXML"])
In [ ]:
using EzXML, ZipFile, JSON, Base64

如果尚未安装其中任何一个,请运行以下单元格,并删除# 符号(取消注释字符串)。

In [ ]:
#]add EzXML ZipFile JSON Base64

只需安装一次即可,但有时为了更新库版本,可以重新启动。

MLX 文件的内容

Live Code 文件格式使用开放打包约定技术,是 zip 文件格式的扩展。代码和格式化内容存储在 XML 文档中,与使用 Office Open XML 格式的文档不同。要处理这些文件的内容,只需将文件扩展名更改为*.zip ,然后使用 Engee 文件浏览器的上下文菜单解压缩即可。

image.png

让我们检查一下*.mlx 文件解压缩后的内容。我们需要存档中的以下文件:

*document.xml ,其中存储了文件的所有文本信息 *document.xml.rels - 文件中包含的附加资料目录(公式、插图)

image.png

文件夹media 包含插入到文档中的插图,文件夹mathml 包含MathML 格式的所用公式。

加载和处理 MLX 文件

为了简化代码的重复使用(也是为了整洁),我们以函数集的形式组织代码。

以下是我们现阶段要实现的功能:

  • 获取目录中的文件列表mlx
  • 解压压缩包并读取我们需要的文件、
  • 处理媒体文件链接文件、
  • 从 XML 文件中获取单元格列表、
  • 将单元格从 XML 格式转换为 JSON 格式。

还有一个辅助函数,用于处理文件中嵌入的插图:

  • 以所需格式获取插图的 MIME 信息(从 "image.png "这样的名称中获取 MIME 标识 "image/png ")。

首先要做的是获取目录中mlx 文件的列表。

In [ ]:
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 文件,将感兴趣的内容放入列表。

In [ ]:
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;

读取媒体寄存器(图片和公式)的功能。

In [ ]:
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;

按照我们需要的格式创建单元格列表。

In [ ]:
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;

创建一个函数,返回插图的元信息,以便纳入最终文档。

In [ ]:
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 格式。

In [ ]:
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 文件中读取了多少单元格、媒体文件和其他信息。

In [ ]:
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
* Файл /user/prestart/mlx_to_ngscript_conversion/input/OverviewCreatingAndConcatenatingExample.mlx содержит:
30 текстовых ячеек
19 кодовых ячеек

* Файл /user/prestart/mlx_to_ngscript_conversion/input/nddemo.mlx содержит:
25 текстовых ячеек
13 кодовых ячеек
3 отсылок к внешним файлам (из них 3 на иллюстрации)

为 ngscript 摘要文件创建模板

现在,我们需要准备一个文件.ngscript ,将文档的单元格放入其中。虽然Engee脚本格式有时会发生变化,但它提供了向后兼容性。为了给文档ngscript 提供一个合理的最新模板,我们将使用当前打开的同一文档作为模板--脚本mlx_to_ngscript_parser.ngscript

让我们下载模板文件ngscript 并从中制作一个模板:

  • 文件模板、
  • 文本单元格模板
  • 代码单元格模板、

在处理输入的mlx 文件时,我们将补充相关信息。

In [ ]:
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"] = [];

让我们看看收到了哪些模板:

In [ ]:
doc_template
Out[0]:
Dict{String, Any} with 4 entries:
  "cells"          => Any[]
  "nbformat_minor" => 5
  "metadata"       => Dict{String, Any}("engee"=>Dict{String, Any}(), "language…
  "nbformat"       => 4
In [ ]:
text_cell_template
Out[0]:
Dict{String, Any} with 6 entries:
  "cell_type"   => "markdown"
  "isParagraph" => false
  "source"      => Any["# Работа с файлами файлов ZIP/XML на примере перевода l…
  "id"          => "6e8ad43b"
  "attachments" => Dict{String, Any}()
  "metadata"    => Dict{String, Any}("name"=>"Название секции", "engee"=>Dict{S…
In [ ]:
code_cell_template
Out[0]:
Dict{String, Any} with 6 entries:
  "outputs"         => Any[]
  "cell_type"       => "code"
  "source"          => Any["using EzXML, ZipFile, JSON, Base64"]
  "id"              => "582c09f7"
  "metadata"        => Dict{String, Any}("name"=>"Название секции", "engee"=>Di…
  "execution_count" => 0

这三个模板足以让我们为输入文件目录中的每个.mlx 文件生成一个新的.ngscript 文件。

填充 JSON 模板

在本脚本的最后一个函数中,我们将执行以下操作:

  • 浏览目录中的所有mlx 文件
  • 处理每个文件的内容
  • 为每个文件创建一个脚本模板ngscript
  • 逐个添加文本和代码单元格,同时不忘添加图形附件。

此外,我们还将创建MATLAB 脚本在Engee环境中运行所需的一切:

  • 我们将在文件开头添加对所需库的调用、
  • 对于所有调用图形输出的单元格(例如plotscatter ),我们将添加命令,将图形保存到文件存储区,并使用库Images.jl 输出,这样图形就会出现在报告中。
In [ ]:
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强大的技术计算能力,可与模型驱动设计工具结合使用。