Julia. Генераторы отчётов и документации

  • Tutorial


Одной из актуальных проблем во все времена, является проблема подготовки отчётов. Поскольку Julia — язык, пользователи которого непосредственно связаны с задачами анализа данных, подготовки статей и красивых презентаций с результатами расчётов и отчётов, то эту тему просто нельзя обойти мимо.


Изначально эта статья планировалась набор рецептов для генерации отчётов, однако рядом с отчётами находится тема документирования, с которой у генераторов отчётов много пересечений. Поэтому сюда включены средства по критерию возможности внедрения выполняемого кода на Julia в шаблон с некоторой разметкой. Наконец, отметим, что в обзор вошли генераторы отчётов как реализованные на самой Julia, так и средства, написанные на других языках программирования. Ну и, естественно, не остались без внимания некоторые ключевые моменты самого языка Julia, без которых может быть не ясно, в каких случаях и какие средства стоит использовать.


Jupyter notebook


Этот инструмент, пожалуй, следует отнести к наиболее популярным у тех, кто занимается анализом данных. Благодаря возможности подключения различных вычислительных ядер, он популярен у исследователей и математиков, привыкших к своим специфическим языкам программирования, одним из которых является Julia. Соответствующие модули для языка Julia реализованы для Jupyter Notebook полностью. И именно поэтому Notebook здесь упоминается.
Процесс установки Jupyter Notebook не сложен. Порядок см. https://github.com/JuliaLang/IJulia.jl Если же Jupyter Notebook уже установлен, то необходимо лишь установить пакет Ijulia и зарегистрировать соответствующее вычислительное ядро.


Поскольку продукт Jupyter Notebook достаточно известен, чтобы не расписывать его подробно, упомянем лишь пару моментов. Блокнот (будем использовать блокнотную терминологию) в Jupyter Notebook состоит из блоков, каждый из которых может содержать либо код, либо разметку в различных её видах (например Markdown). Результат обработки — это либо визуализация разметки (текст, формулы и пр.), либо результат выполнения последней операции. Если в конце строки с кодом поставлена точка с запятой, результат не будет выведен на экран.


Примеры. Блокнот до выполнения приводится на следующем рисунке:



Результат его выполнения приводится на следующем рисунке



Блокнот содержит графику и некоторый текст. Отметим, что для вывода матрицы возможно использование типа DataFrame, для которого результат отображается в виде html-таблицы с явными границами и скроллером, если он нужен.


Jupyter notebook может экспортировать текущий блокнот в файл в формате html. При наличии установленных средств преобразования, может преобразовать и в pdf.


Для построения отчётов по некоторому регламенту, можно использовать модуль nbconvert и следующую команду, вызываемую в фоновом режиме по расписанию:
jupyter nbconvert --to html --execute julia_filename.ipynb


При выполнении длительных вычислений целесообразно добавить опцию с указанием таймаута — --ExecutePreprocessor.timeout=180


В текущей директории появится html-отчёт, сформированный из этого файла. Опция --execute здесь означает принудительный запуск пересчёта.


Полный набор опция модуля nbconvert см.
https://nbconvert.readthedocs.io/en/latest/usage.html


Результат преобразования в html практически полностью соответствует предыдущему рисунку, за исключением того, что в нём отсутствует панели меню и кнопок.


Jupytext


Достаточно интересная утилита, которая позволяет конвертировать уже созданные ранее ipynb-заметки в текст Markdown или код на Julia.


Ранее рассмотренный пример мы можем преобразовать с помощью команды
jupytext --to julia julia_filename.ipynb


В результате чего получим файл julia_filename.jl с кодом на Julia и специальную разметку в форме комментариев.


# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .jl
#       format_name: light
#       format_version: '1.3'
#       jupytext_version: 0.8.6
#   kernelspec:
#     display_name: Julia 1.0.3
#     language: julia
#     name: julia-1.0
# ---

# # Report example

using Plots, DataFrames

# ### Drawing
# Good time to show some plot

plot(rand(5,5), linewidth=2, title="My Plot", size = (500, 200))

# ## Some computational results

rand(2, 3)

DataFrame(rand(2, 3))

Разделители блоков заметки — просто двойной перевод строки.


Обратное преобразование мы можем сделать при помощи команды:
jupytext --to notebook julia_filename.jl


В итоге, будет сгенерирован ipynb-файл, который, в свою очередь, можно обработать и преобразоваить в pdf или html.


См. подробнее https://github.com/mwouts/jupytext


Общий недостаток jupytext и jupyter notebook — «красивость» отчёта ограничена возможностями этих инструментов.


Самостоятельная генерация HTML


Если по какой-то причине мы считаем, что Jupyter Notebook является слишком тяжелым продуктом, требующим установки многочисленных сторонних пакетов, не нужных для работы Julia, или недостаточно гибким для построения нужной нам формы отчета, то альтернативным способом является генерация html-страницы вручную. Однако здесь придётся немного погрузиться в особенности формирования изображений.


Для Julia типичным способом вывода чего-либо в поток вывода является использование функции Base.write, а для декорирования — Base.show(io, mime, x). Причем, для различных запрошенных mime-способов вывода могут быть различные варианты отображения. Например, DataFrame при выводе как текст отображается таблицей с псевдографикой.


julia> show(stdout, MIME"text/plain"(), DataFrame(rand(3, 2)))
3×2 DataFrame
│ Row │ x1       │ x2        │
│     │ Float64  │ Float64   │
├─────┼──────────┼───────────┤
│ 1   │ 0.321698 │ 0.939474  │
│ 2   │ 0.933878 │ 0.0745969 │
│ 3   │ 0.497315 │ 0.0167594 │

Если же, mime указан как text/html, то итогом является HTML-разметка.


julia> show(stdout, MIME"text/html"(), DataFrame(rand(3, 2)))
<table class="data-frame">
  <thead>
    <tr><th></th><th>x1</th><th>x2</th></tr>
    <tr><th></th><th>Float64</th><th>Float64</th></tr>
  </thead>
  <tbody><p>3 rows × 2 columns</p>
    <tr><th>1</th><td>0.640151</td><td>0.219299</td></tr>
    <tr><th>2</th><td>0.463402</td><td>0.764952</td></tr>
    <tr><th>3</th><td>0.806543</td><td>0.300902</td></tr>
  </tbody>
</table>

То есть, используя методы функции show, определенные для соответствующего типа данных (третий аргумент) и соответствующего формата вывода, можно обеспечить формирование файла в любом требуемом формате данных.


Сложнее обстоит ситуация с изображениями. Если нам необходимо сформировать один единственный файл html, то изображение должно быть внедрено в код страницы.


Рассмотрим пример, в котором это реализовано. Вывод в файл будем выполнять функцией Base.write, для которой определим соответствующие методы. Итак, код:


#!/usr/bin/env julia

using Plots
using Base64
using DataFrames

# сформируем изображение и запомним его в переменной
p = plot(rand(5,5), linewidth=2,
         title="My Plot", size = (500, 200))

# распечатаем тип, чтобы видеть, кто формирует изображение
@show typeof(p)  # => typeof(p) = Plots.Plot{Plots.GRBackend}

# Определим три абстрактных типа, чтобы сделать 3 разных 
#     метода функции преобразования изображений
abstract type Png end
abstract type Svg end
abstract type Svg2 end

# Функция Base.write используется для записи в поток
# Определим свои методы этой функции с разными типами

# Первый вариант — выводим растровое изображение, перекодировав
#  его в Base64-формат.
#  Используем HTML разметку  img src="data:image/png;base64,..."
function Base.write(file::IO, ::Type{Png}, p::Plots.Plot)
  local io = IOBuffer()
  local iob64_encode = Base64EncodePipe(io);
  show(iob64_encode, MIME"image/png"(), p)
  close(iob64_encode);

  write(file,
        string("<img src=\"data:image/png;base64, ",
               String(take!(io)),
               "\" alt=\"fig.png\"/>\n"))
end

# Два метода для вывода Svg
function Base.write(file::IO, ::Type{Svg}, p::Plots.Plot)
  local io = IOBuffer()
  show(io, MIME"image/svg+xml"(), p)
  write(file,
        replace(String(take!(io)),
                r"<\?xml.*\?>" => ""  ))
end

# выводим в поток XML-документ без изменений, содержащий SVG
Base.write(file::IO, ::Type{Svg2}, p::Plots.Plot) =
    show(file, MIME"image/svg+xml"(), p)

# Определим метод функции для DataFrame
Base.write(file::IO, df::DataFrame) =
    show(file, MIME"text/html"(), df)

# Пишем файл out.html простейший каркас HTML
open("out.html", "w") do file
  write(file, """
<!DOCTYPE html>
<html>
  <head><title>Test report</title></head>
  <body>
   <h1>Test html</h1>
  """)

  write(file, Png, p)
  write(file, "<br/>")
  write(file, Svg, p)
  write(file, "<br/>")
  write(file, Svg2, p)

  write(file, DataFrame(rand(2, 3)))

  write(file, """
  </body>
</html>
  """)
end

Для формирования изображений по умолчанию используется движок Plots.GRBackend, который может выполнить растровый или векторный вывод изображения. В зависимости от того, какой тип указан в аргументе mime функции show, формируется соответствующий результат. MIME"image/png"() формирует изображение в формате png. MIME"image/svg+xml"() приводит к генерации svg-изображения. Однако во втором случае, следует обратить внимание на то, что формируется полностью самостоятельный xml-документ, который может быть записан как отдельный файл. В то же время, наша цель — вставить изображение в HTML страницу, что в HTML5 можно сделать путём простой вставки SVG-разметки. Именно поэтому в методе Base.write(file::IO, ::Type{Svg}, p::Plots.Plot) вырезается xml-заголовок, который, иначе, будет нарушать структуру HTML-документа. Хотя, большинство браузеров, способно корректно показывать изображение даже в этом случае.


Касаемо метода для растровых изображений Base.write(file::IO, ::Type{Png}, p::Plots.Plot), особенностью внедрения здесь является то, что бинарные данные мы можем вставить внутрь HTML только в Base64-формате. Делаем это при помощи конструкции<img src="data:image/png;base64,"/>. А для перекодирования используем Base64EncodePipe.


Метод Base.write(file::IO, df::DataFrame) обеспечивает вывод в формате html-таблицы объекта DataFrame.


Итоговая страница выглядит следующим образом:



На изображении все три картинки выглядят примерно одинаково, однако помним, что одно из них вставлено некорректно с точки зрения HTML (лишний xml-заголовок). Одно является растровым, значит не может быть увеличено без потери детализации. И только одно из них вставлено как правильный svg-фрагмент внутри разметки HTML. И оно же может быть легко масштабировано без потери деталей.


Естественно, страница получилась очень простой. Но любые визуальные улучшения возможны при помощи CSS.


Такой способ формирования отчётов полезен, например, тогда, когда количество выводимых таблиц определяется реальными данными, а не шаблоном. Например, необходимо выполнить группировку данных по какому-то полю. И по каждой группе сформировать отдельные блоки. Поскольку при формировании страницы результат определяется количеством вызовов Base.write, то очевидно, что нет никаких проблем обернуть нужный блок в цикл, сделать вывод зависимым от данных и пр.


Пример кода:


using DataFrames

# Химические элементы и их аггрегатное состояние
ptable = DataFrame(
    Symbol = ["H",  "He", "C",    "O",  "Fe"  ],
    Room   = [:Gas, :Gas, :Solid, :Gas, :Solid]
)

res = groupby(ptable, [:Room])

# А теперь выведем группы раздельно
open("out2.html", "w") do f
  for df in (groupby(ptable, [:Room]))
    write(f, "<h2>$(df[1, :Room])</h2>\n")
    show(f, MIME"text/html"(), DataFrame(df))
    write(f, "\n")
  end
end

Результат работ этого скрипта — фрагмент HTML странички.



Обратите внимание на то, что всё, что не требует декорирования/преобразования формата, выводится напрямую через функцию Base.write. В то же время, всё, что требует преобразования, выводится через Base.show.


Weave.jl


Weave — это генератор научных отчётов, который реализован на Julia. Использует идеи генераторов Pweave, Knitr, rmarkdown, Sweave. Основной его задачей декларируется экспорт исходной разметки на любом из предлагаемых языков (Noweb, Markdown, скриптовый формат) в разметку LaTex, Pandoc, Github markdown, MultiMarkdown, Asciidoc, reStructuredText форматы. И, даже в IJulia Notebooks и обратно. В части последнего он похож на Jupytext.


То есть, Weave — средство, позволяющее писать шаблоны, содержащие Julia-код, на различных языках разметки, а на выходе иметь разметку на другом языке (но уже с результатами выполнения Julia-кода). И это очень полезный инструмент именно для научных сотрудников. Например, можно подготовить статью на Latex, которая будет иметь вставки на Julia с автоматическим вычислением результата и его подстановкой. Weave сгенерирует файл для финальной статьи.


Существует поддержка редактора Atom при помощи соответствующего плагина https://atom.io/packages/language-weave. Это позволяет выполнять разрабатывать и отлаживать внедренные в разметку скрипты на Julia, после чего сгенерировать целевой файл.


Основной принцип в Weave, как уже было упомянуто, это разбор шаблона, содержащего разметку с текстом (формулами и пр) и вставки кода на Julia. Результат выполнения кода может быть выведен в итоговый отчёт. Вывод текста, кода, вывод результатов, вывод графиков — всё это может быть индивидуально настроено.


Для обработки шаблонов необходимо запустить внешний скрипт, который соберёт всё в единый документ и преобразует в нужный выходной формат. То есть, шаблоны отдельно, обработчики отдельно.


Пример такого скрипта обработки:


# Обработать файлы и сгенерировать отчёты:
# Markdown
weave("w_example.jmd",
      doctype="pandoc"
      out_path=:pwd)

# HTML
weave("w_example.jmd",
      out_path=:pwd,
      doctype = "md2html")

# pdf
weave("w_example.jmd",
      out_path=:pwd,
      doctype = "md2pdf")

jmd в именах файлов — Julia Markdown.


Возьмём тот же пример, который мы использовали в предыдущих средствах. Однако вставим ему заголовок со сведениями об авторе, которые понимает Weave.


---
title : Intro to Weave.jl with Plots
author : Anonymous
date : 06th Feb 2019
---

# Intro

## Plot
` ``{julia;}
using Plots, DataFrames

plot(rand(5,5), linewidth=2, title="My Plot", size = (500, 200))
` ``

## Some computational results
` ``julia
rand(2, 3)
` ``

` ``julia
DataFrame(rand(2, 3))
` ``

Этот фрагмент, будучи преобразованным в pdf, выглядит примерно следующим образом:



Шрифты и оформление — хорошо узнаваемы для пользователей Latex.


Для каждого фрагмента внедрённого кода можно определить то, каким образом этот код будет обработан, и что будет выведено в итоге.


Например:


  • echo = true — код будет выведен в отчёт
  • eval = true — результат выполнения кода будет выведен в отчёт
  • label — добавить метку. Если используется Latex, то это будет использовано как fig:label
  • fig_width, fig_height — размеры изображения
  • и пр.

О форматах noweb и script, а также подробнее об этом инструменте см. http://weavejl.mpastell.com/stable/


Literate.jl


Авторы этого пакета на вопрос, почему Literate, отсылают к парадигме Literate Programming Дональда Кнутта. Задачей этого инструмента является генерация документов на основе кода Julia, содежащего комментарии в формате markdown. В отличии от предыдущего рассмотренного инструмента Weave, документы с результатами выполнения он сделать не может. Однако инструмент легковесен и, в первую очередь, ориентирован на документирование кода. Например, помощь в написании красивых примеров, которые могут быть размещены на любой markdown-платформе. Часто используется в цепочке других инструментов документирования, например вместе с Documenter.jl.


Возможно три варианта выходного формата — markdown, notebook и script (чистый Julia код). Ни в одном из них выполнение внедрённого кода проводиться не будет.


Пример исходного файла с комментариями Markdown (после первого символа # ):


#!/usr/bin/env julia

using Literate
Literate.markdown(@__FILE__, pwd()) # documenter=true

# # Intro

# ## Plot
using Plots, DataFrames

plot(rand(5,5), linewidth=2, title="My Plot", size = (500, 200))

# ## Some computational results
rand(2, 3)

DataFrame(rand(2, 3))

Результатом его работы будет Markdown-документ и директивы для Documenter, если их генерация не была явно отключена.


` ``@meta
EditURL = "https://github.com/TRAVIS_REPO_SLUG/blob/master/"
` ``

` ``@example literate_example
#!/usr/bin/env julia

using Literate
Literate.markdown(@__FILE__, pwd(), documenter=true)
` ``

# Intro

## Plot

` ``@example literate_example
using Plots, DataFrames

plot(rand(5,5), linewidth=2, title="My Plot", size = (500, 200))
` ``

## Some computational results

` ``@example literate_example
rand(2, 3)

DataFrame(rand(2, 3))
` ``

*This page was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).*

Вставки кода внутри markdown здесь намеренно поставлены с пробелом между первым и последующими апострофами, чтобы не испортились при публикации статьи.


См. подробнее https://fredrikekre.github.io/Literate.jl/stable/


Documenter.jl


Генератор документации. Его основное назначение — формирование пригодной для чтения документации пакетов, написанных на Julia. Documenter преобразует в html или pdf как примеры с Markdown-разметкой и внедрённым Julia-кодом, так и исходные файлы модулей, извлекая Julia-docstrings (собственные коментарии Julia).


Пример типового оформления документации:



В этой статье мы не будем подробно останавливаться на принципах документирования, поскольку, по-хорошему, это надо делать в рамках отдельной статьи, посвященной разработке модулей. Однако некоторые моменты Documenter мы здесь рассмотрим.


Прежде всего, стоит обратить внимание на то, что экран разбит на две части — левая сторона содержит интерактивное оглавление. Правая сторона — собственно, текст документации.


Типовая структура директорий с примерами и документацией выглядит следующим образом:


    docs/
        src/
        make.jl

    src/
        Example.jl
    ...

Директория docs/src — это markdown документация. А примеры находятся где-то в общей директории исходных кодов src.


Ключевой файл для Docuementer – docs/make.jl. Содержимое этого файла для самого Documenter:


using Documenter, DocumenterTools

makedocs(
    modules = [Documenter, DocumenterTools],
    format = Documenter.HTML(
        # Use clean URLs, unless built as a "local" build
        prettyurls = !("local" in ARGS),
        canonical = "https://juliadocs.github.io/Documenter.jl/stable/",
    ),
    clean = false,
    assets = ["assets/favicon.ico"],
    sitename = "Documenter.jl",
    authors = "Michael Hatherly, Morten Piibeleht, and contributors.",
    analytics = "UA-89508993-1",
    linkcheck = !("skiplinks" in ARGS),
    pages = [
        "Home" => "index.md",
        "Manual" => Any[
            "Guide" => "man/guide.md",
            "man/examples.md",
            "man/syntax.md",
            "man/doctests.md",
            "man/latex.md",
            hide("man/hosting.md", [
                "man/hosting/walkthrough.md"
            ]),
            "man/other-formats.md",
        ],
        "Library" => Any[
            "Public" => "lib/public.md",
            hide("Internals" => "lib/internals.md", Any[
                "lib/internals/anchors.md",
                "lib/internals/builder.md",
                "lib/internals/cross-references.md",
                "lib/internals/docchecks.md",
                "lib/internals/docsystem.md",
                "lib/internals/doctests.md",
                "lib/internals/documenter.md",
                "lib/internals/documentertools.md",
                "lib/internals/documents.md",
                "lib/internals/dom.md",
                "lib/internals/expanders.md",
                "lib/internals/mdflatten.md",
                "lib/internals/selectors.md",
                "lib/internals/textdiff.md",
                "lib/internals/utilities.md",
                "lib/internals/writers.md",
            ])
        ],
        "contributing.md",
    ],
)

deploydocs(
    repo = "github.com/JuliaDocs/Documenter.jl.git",
    target = "build",
)

Как видим, ключевыми методами здесь являются makedocs и deploydocs, которые определяют структуру будущей документации и место для её размещения. makedocs обеспечивает формирование markdown-разметки со всех указанных файлов, что включает в себя как выполнение внедрённого кода, так и извлечение docstrings-комментариев.


Documenter поддерживает ряд директив для вставки кода. Их формат — ```@something


  • @docs, @autodocs — ссылки на docstrings документацию, извлеченную из Julia-файлов.
  • @ref, @meta, @index, @contents — ссылки, указания индекстых страниц и пр.
  • @example, @repl, @eval — режимы выполнения внедрённого кода на Julia.
  • ...

Наличие директив @example, @repl, @eval, по сути, и определило то, включать ли Documenter в это обзор или нет. Причём упомянутый ранее Literate.jl, может автоматически сгенерировать такую разметку, что и было продемонстрировано ранее. То есть, принципиальных ограничений к использованию генератора документации как генератора отчётов здесь нет.


Подробнее о Documenter.jl см. https://juliadocs.github.io/Documenter.jl/stable/


Заключение


Несмотря на молодость языка Julia, уже разработанные для него пакеты и инструменты, позволяют говорить о полноценном использовании в высоконагруженных сервисах, а не только о реализации пробных проектов. Как видим, возможность генерировать различные документы и отчёты, включающие результаты выполнения кода как в текстовом, так и в графическом виде уже обеспечена. Причём, в зависимости от сложности отчёта, мы можем выбирать между простотой создания шаблона и гибкостью генерации отчётов.


В статье не рассмотрен генератор Flax из состава пакета Genie.jl. Genie.jl — это попытка реализовать Julia on Rails, а Flax — своеобразный аналог eRubis со вставками кода на Julia. Однако Flax не предоставляется в виде отдельного пакета, а Genie не включена в основной репозиторий пакетов, поэтому он и не вошел в этот обзор.


Отдельно хотелось бы упомянуть пакеты Makie.jl и Luxor.jl, обеспечиющие формирование сложных векторных визуализаций. Результат их работы может также быть использован как часть отчётов, но об этом также следует писать отдедльную статью.


Ссылки


  • +20
  • 5,8k
  • 2
Поделиться публикацией

Комментарии 2

    +1
    Спасибо, очень интересно! Соxраню себе эту статью, и в следующий раз, когда мне надо будет прочитать данные из разных мест и сгенерить по ним какой-то отчет — попробую сделать это на Julia.

    Я так понимаю, что Julia может использоваться как полноценный скриптовый язык, вместо bash или shell?
      +1
      Julia может использоваться как полноценный скриптовый язык. Но подключение библиотек у неё — довольно длительная процедура.

      Если нужна скорость, то на Julia лучше писать сетевые сервисы, которые вообще не выгружаются из памяти и делают что-либо по запросу.

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое