Engee 文档
Notebook

使用将MATLAB live脚本转换为ngscipt格式的示例处理ZIP/XML文件

在这个例子中,我们将展示如何使用OPC格式文件([打开包装约定](https://en.wikipedia.org/wiki/Open_Packaging_Conventions )),即与包含一组不同的XML和其他文件的ZIP容器。 这种格式随处可见。 例如,这些类型的格式用于所有Office应用程序(DOCX, XLSX 等。)和许多工程包(Autodesk,Simulink,Engee)。

我们将从格式转换技术计算文件 mlxngscript –我们会将所有文本和代码单元格,插图,超链接和公式从一个文档传输到另一个文档。

导言

用于处理流行的格式,如 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文件格式使用Open Packaging Conventions技术,这是zip文件格式的扩展。 代码和格式化的内容存储在XML文档中,该文档与使用Office Open XML格式的文档不同。 要处理这些文件的内容,只需将文件扩展名更改为 *.zip,然后通过Engee文件浏览器的上下文菜单解压缩。

image.png

让我们检查文件的内容 *.mlx 开箱后。 我们将需要从存档以下文件:

  • document.xml,其中存储文档的所有文本信息
  • document.xml.rels -文档中包含的其他材料目录(公式,插图)
image.png

在文件夹中 media 收集插图,这些插图被插入到文档中,并在文件夹中 mathml -使用[MathML]格式的公式(https://ru.wikipedia.org/wiki/MathML )。

上传和处理MLX文件

为了简化代码的重用(以及为了清晰的演示),我们以一组函数的形式组织它。

以下是我们在这个阶段正在实现的功能:

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

和一个辅助功能,用于处理嵌入在文件中的插图。:

*以所需格式获取有关插图的MIME信息(我们从"image"这样的名称中创建MIME标识符"image/png"。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

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

填写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). 结果是一个工具,它将部分信息从一组文档中转换出来 LiveScript 星期三 MATLAB 在文件中 .ngscript Engee平台。

从本示例的各个功能中,您可以了解有关打开文件、使用XML格式的数据以及基于JSON生成文档的想法。 这个例子还展示了Engee在组织技术计算方面的强大能力,以及面向模型的设计工具。