😃

Quartoを使ってJupyterより綺麗なPythonコード入りドキュメントを作る

2022/12/31に公開

何かしらのデータを解析するなどする場合に、Jupyter系をいままでは利用してきました。
もちろん、Jupyterを個人で単なる実行環境と利用する分にはそこまで大きな問題はないと思います。

ただ、Jupyter系はMarkdownを書くのには流石にあまり向いていないこと、pdfなどの出力がイマイチ綺麗にいかないことから、「分析してそれをそのままアウトプットしたい」という時にやや不満が残る点がありました。

そこで調べてみたところ、Quartoというものが使えそうだったので、今回セッティングと少し使ってみようと思った次第です。

まずは、公式のGalleryを見てみてください。結構いい感じと思います。
https://quarto.org/docs/gallery/
また、出力HTMLをGithub PagesへPublishすることができるなど、そのあたりも活用できればかなり良さそうです。

インストール

セットアップ超概略

Python + VsCode編

quarto CLIのインストール

https://quarto.org/
上記サイトGetStartedから、環境にあったCLIをダウンロードしてインストールします。
続けて、GetStartedの手順にしたがって、VsCodeでの環境をセットアップします。

quartoのVsCode拡張のインストール

quartoで調べたら出てくるので追加します。

(quarto関係ないですが) Python環境の作成+VsCodeインタープリタ設定

今回はpipenvで環境を作ります(python3自体はある前提)
適当に使いそうなものをインストールしておきます。
quartoがバックエンドにjupyterを利用するらしく、jupyter自体はインストールする必要があります。

exportとtouchあたりはカレントディレクトリに.venvを作成するためのおまじないです。

export PIPENV_VENV_IN_PROJECT=1
touch Pipfile
pipenv --python 3
pipenv install pandas scipy matplotlib numpy jupyter beautifulsoup4 requests

インストールが終わったらVsCodeを一度閉じて再度開いておきます(pythonインタープリタの読み込みがうまくいかなかったりすることがあります)

上記設定をすると以下のようなファイル構成になっていると思います。

|-.venv/
|-Pipfile
|-Pipfile.locks

ところで、quartoがレンダリングに使うpythonは以下の仕組みで選択されるようです。

  1. vsCodeのpython拡張機能の、pythonPathの0番目を取得
  2. pythonPathの仮想環境をactivate
  3. activate後にpython or python3の名前で引けるpythonインタプリタが選択され、quartoのレンダリングに使用される。

要は、pipenvで作成した./venv配下のインタプリタを、VsCodeのpython拡張機能(ms-python)で選択するようにしてあげれば、quarto側は勝手にそれを使ってレンダリングをします。
この辺りドキュメントに記載が見つからなかったので、結構迷う人もいる気がしました。(私は結局ソースコードを読みました)

具体的には、workspaceの設定で.venv配下の環境を使うように設定してあげます。

{
    "python.defaultInterpreterPath": "${workspaceFolder}\\.venv\\Scripts\\python.exe",
}
確認したソースコード。長いので折りたたみ
export function pythonExecForCaps(
  caps?: JupyterCapabilities,
  binaryOnly = false,
) {
  if (caps?.pyLauncher) {
    return ["py"];
  } else if (isWindows()) {
    return [binaryOnly ? "python" : caps?.executable || "python"];
  } else {
    return [binaryOnly ? "python3" : caps?.executable || "python3"];
  }
}

https://github.com/quarto-dev/quarto-cli/blob/aeb98c47089177f6e25ef7e23a95ca4d6854350a/src/core/jupyter/exec.ts#L33

    const quarto = "quarto"; // binPath prepended to PATH so we don't need the full form
    const cmd: string[] = [
      this.quartoContext_.useCmd ? winShEscape(quarto) : shQuote(quarto),
      isShiny ? "serve" : "preview",
      shQuote(
        this.quartoContext_.useCmd
          ? target.fsPath
          : pathWithForwardSlashes(target.fsPath)
      ),
    ];

    // extra args for normal docs
    if (!isShiny) {
      if (!doc) {
        // project render
        cmd.push("--render", format || "all");
      } else if (format) {
        // doc render
        cmd.push("--to", format);
      }

      cmd.push("--no-browser");
      cmd.push("--no-watch-inputs");
    }

    const cmdText = this.quartoContext_.useCmd
      ? `cmd /C"${cmd.join(" ")}"`
      : cmd.join(" ");
    this.terminal_.show(true);
    // delay if required (e.g. to allow conda to initialized)
    // wait for up to 5 seconds (note that we can do this without
    // risk of undue delay b/c the state.isInteractedWith bit will
    // flip as soon as the environment has been activated)
    if (requiresTerminalDelay(this.previewEnv_)) {
      const kMaxSleep = 5000;
      const kInterval = 100;
      let totalSleep = 0;
      while (!this.terminal_.state.isInteractedWith && totalSleep < kMaxSleep) {
        await sleep(kInterval);
        totalSleep += kInterval;
      }
    }

    this.terminal_.sendText(cmdText, true);

https://github.com/quarto-dev/quarto/blob/cc2b967161c044cd48e63470c07351c5f14df614/apps/vscode/src/providers/preview/preview.ts#L380

export function requiresTerminalDelay(env?: PreviewEnv) {
  try {
    if (env?.QUARTO_PYTHON) {
      // look for virtualenv
      const binDir = dirname(env.QUARTO_PYTHON);
      const venvFiles = ["activate", "pyvenv.cfg", "../pyvenv.cfg"];
      if (
        venvFiles.map((file) => path.join(binDir, file)).some(fs.existsSync)
      ) {
        return true;
      }

      // look for conda env
      const args = [
        "-c",
        "import sys, os; print(os.path.exists(os.path.join(sys.prefix, 'conda-meta')))",
      ];
      const output = (
        child_process.execFileSync(shQuote(env.QUARTO_PYTHON), args, {
          encoding: "utf-8",
        }) as unknown as string
      ).trim();
      return output === "True";
    } else {
      return false;
    }
  } catch (err) {
    console.error(err);
    return false;
  }
}

export class PreviewEnvManager {
  constructor(
    outputSink: PreviewOutputSink,
    private readonly renderToken_: string
  ) {
    this.outputFile_ = outputSink.outputFile();
  }

  public async previewEnv(uri: Uri) {
    // get workspace for uri (if any)
    const workspaceFolder = workspace.getWorkspaceFolder(uri);

    // base env
    const env: PreviewEnv = {
      // eslint-disable-next-line @typescript-eslint/naming-convention
      QUARTO_LOG: this.outputFile_, QUARTO_RENDER_TOKEN: this.renderToken_,
    };
    // QUARTO_PYTHON
    const pyExtension = extensions.getExtension("ms-python.python");
    if (pyExtension) {
      if (!pyExtension.isActive) {
        await pyExtension.activate();
      }

      const execDetails = pyExtension.exports.settings.getExecutionDetails(
        workspaceFolder?.uri
      );
      if (Array.isArray(execDetails?.execCommand)) {
        env.QUARTO_PYTHON = execDetails.execCommand[0];
      }
    }

https://github.com/quarto-dev/quarto/blob/cc2b967161c044cd48e63470c07351c5f14df614/apps/vscode/src/providers/preview/preview-env.ts

quartoでの文章の作成

まずはquarto GetStartedに載っている例を見てみます。


---
title: "Quarto Basics"
format:
  html:
    code-fold: true
jupyter: python3
---

For a demonstration of a line plot on a polar axis, see @fig-polar.

```{python}
#| label: fig-polar
#| fig-cap: "A line plot on a polar axis"

import numpy as np
import matplotlib.pyplot as plt

r = np.arange(0, 2, 0.01)
theta = 2 * np.pi * r
fig, ax = plt.subplots(
  subplot_kw = {'projection': 'polar'} 
)
ax.plot(theta, r)
ax.set_rticks([0.5, 1, 1.5, 2])
ax.grid(True)
plt.show()
```

先頭の---で囲まれた部分がドキュメントレベルでの設定(以降は単にオプションと呼びます)になります。
それ以降が文章本体です。```{python}で開始した部分にpythonコードを埋め込むことができます。
jupyterとの差異は、labelやfig-capの指定によって、latexのような相互参照の指定、キャプションの指定ができるなど、主に組版っぽい機能が充実しています。

今回はquartoの形式である、qmdファイルから作成していますが、quartoではipynbファイルからの変換などもできるらしく、実行に優れるipynb(quartoはレンダリングの分実行の機敏性は落ちます)で分析の大枠を決めたのち、quartoで文章に落とし込んでいくといったこともできそうです。

以降は、quartoドキュメントから有用そうな設定や機能をひろいつつ、サンプル文章を拡充していきます。

quartoオプションの基本

quartoは実態としてはpandocの拡張のようなもので、pandocとほぼ同等の設定を引き継いでいるようです。
quartoのサンプルでは、htmlフォーマットを利用していますが、それ以外にもさまざまな出力形式がサポートされています。
https://quarto.org/docs/reference/

フォーマットとしては主に以下等があります。

  • HTML
  • PDF
  • OpenOffice
  • ePub
  • Presentations(Revealjs、Powerpoint,Beamer)

個人的に割と利用機会が多いのは、HTML、PDFあたりでしょうか。
このうち、HTMLについては、そこまで困ることはなく、ドキュメントを単純に参照すれば良いのですが、唯一PDFは詰まるポイントがあります。
PDFについては詰まりポイントがある関係で後述とし、まずはHTMLについて確認してみます。

HTMLオプション

主なオプション

主なオプションとしては以下です。一部Referenceにはあまりちゃんとした説明がなかったりするケースもあるので、Guideも合わせて確認する必要があります。

オプション名 概略
title,subtitle,author,date,abstract 説明不要と思います。
theme テーマ設定です。詳しくはこちら
css CSSファイルを指定して読み込めます。
anchor-sections true/falseで章タイトルにホバーしたときに章へのリンクが表示されるかを切り替えます。
smooth-scroll true/falseでスムーススクロールを切り替えます。
html-math-method 数式レンダリングエンジンが設定できます。
toc,toc-depth,toc-location,
number-sections 目次関連です。toc-locationはデフォルトrightですが、leftやbodyが選択できます。
code-fold trueとすると、コードを折りたたみにすることができます。trueでは初期状態で折り畳まれた状態ですが、showにすると、初期状態が表示された状態で折りたたみ可となります。デフォルトはfalse(折りたたみ無効)です。
code-overflow コードが横に長すぎる時の挙動です。scroll,wrapから選択します。
code-line-numbers true/falseでコードの横に番号表示するかを切り替えます。
code-block-border-left,code-block-bg コードブロックのスタイルです。hexで指定できます。
highlight-style シンタックスハイライトのスタイルです。
echo trueにするとpythonコード自体がドキュメントに含まれます。falseにすると実行結果のみが含まれます
bibliography 参考文献関連です。
lang 言語です。日本語はjaです。
include-before-body ヘッダ部分にファイルなどから埋め込むことができます。
citations-hover,footnotes-hover true/falseで切り替えます。参考文献や注釈部分にマウスをホバーすると参照先情報が表示されるようになります。

qmdファイルサンプルと出力例

qmdファイル
---
title: "Quarto Basics"
subtitle: "サブタイトルを長々とつけて、サブタイトルの折り返しが発生する場合の挙動を見てみようと思います"
author: MosaMosa
date: "2022/12/31"
abstract: "jupyternotebookに比べ、即時の実効性は劣るものの、TexやHTMLなど組版としての機能にはQuartoの優位性があります。特にipynbをpdfなどに出力すると改ページの挙動などがイマイチで悩んでいたのですが、そういった悩みは改善されそうです。"
format: html
anchor-sections: true
smooth-scroll: true
toc: true
toc-depth: 3
number-sections: true
code-fold: true
code-overflow: wrap
code-line-numbers: true
bibliography: ./references.bib
citations-hover: true
footnotes-hover: true
editor:
  render-on-save: true
jupyter: python3
---

#

##

###

#### toc-depth 3だと目次に表示されない見出し

quarto[@quarto]での日本語入力のテスト @fig-polar.
長い文章を入力した時に日本語だとうまく折り返し処理をしてくれないことがあるが、そういったことが起こらないかの確認のための文章。

```{python}
#| label: fig-polar
#| fig-cap: "A line plot on a polar axis"

import numpy as np
import matplotlib.pyplot as plt

r = np.arange(0, 2, 0.01)
theta = 2 * np.pi * r
fig, ax = plt.subplots(
  subplot_kw = {'projection': 'polar'} 
)
ax.plot(theta, r)
ax.set_rticks([0.5, 1, 1.5, 2])
ax.grid(True)
plt.show()
```


See also

https://quarto.org/docs/output-formats/html-basics.html
https://quarto.org/docs/reference/formats/html.html

PDFオプション

PDF出力について

pdf出力に使うquartoにてエンジンはさまざま選択できますが、このうちメインの一つとなるのはTex系列のエンジンと思います。(この辺りで察される方も多いと思いますが)、Tex、特に日本語を扱うTexはquartoというよりもTexの問題でちょっと大変なポイントがあります。

Texのインストール

quartoはpdf-engineオプションにて、Texを指定することができますが、そのためにはtexエンジンにPathが通っていることもしくは絶対パスでの指定が必要となります。
ちなみに、quarto公式ではtinyTexでのインストールが推奨されていますが、日本語Texでパッケージをケチると思わぬエラーを吐いて苦しむことが多いため、個人的にはTexLiveのフルインストールを推奨します。

TexLiveのfullインストール相当のインストールをしていれば、特に問題はありませんでしたが、最小構成でのインストールなどをしている場合はパッケージの不足が生じるかもしれません。

Texエンジンの選択

quartoではpdf-engineに、Tex系ではxelatex, pdflatex, lualatex, tectonic, latexmkを選択することができます。デフォルトはxelatexです。xelatexはutf-8での組版が可能なため、一見日本語使用が問題なく見えますが、実際は組版上の問題が生じるようです。参考

そのため、日本語を利用する場合はlualatexを利用することとしました。

lualatexでの日本語設定

lualatexで日本語を使用するための設定については、他により良い説明がありますので、細かい説明は割愛しますが、要はluatex-ja関連のパッケージを使うように設定してあげれば問題ありません。

quartoでは、オプションでheader部分にファイルから読み込んだ文字列を挿入する機能があるため、それを利用して上記パッケージを読み込むようにします。
具体的には以下のようなファイルを別途作成し、

% luatex_headerというファイルで作成
\usepackage{luatexja}
\usepackage{luatexja-fontspec}
\usepackage[hiragino-pro]{luatexja-preset}

quartoドキュメントの設定部分で、

include-in-header: 
  file: ./luatex_header 

として読み込んであげます。markdown中にTex記法を混在させるとややこしいことになってしまいますが、文章全般設定やフォント類に関わるパッケージはかなり自由に追加可能と思います。

そのほかトラブルシューティング

  • 途中で謎のエラーによりTexのコンパイルが通らなくなる事象に苦しみましたが、TexLiveのバージョンが低いことに起因していました。TexLiveの最新版をインストールし直すことで改善しました。
  • lualatexがずっと実行される(6回、7回、、、と続く。通常は3回)事象が発生しました。原因としては、文献への参照での参照先ラベルが.bibファイルに存在しなかったことによります。quarto規定のlatexmkの設定では最大実行回数に上限がない(か、上限がかなり高い)ようなので、latex-max-runsオプションを5程度に設定して抑制しました。

そのほか便利そうなpdfオプション

オプション名 概略
title,subtitle,author,date,abstract 説明不要と思います。
pdf-engine 上記の通り日本語の場合はluatexを利用しましょう
documentclass,pagesize お馴染みのやつです。Texでは\documentclass[a4paper]{jsarticle}なんて書くと思いますが、quartoでは別々のオプションになっています。
include-in-header 上記の通りパッケージの読み込みに使えます。
toc,toc-depth,number-sections 目次の生成関連です。number-sectionsをfalseにすると第四階層の見た目が悪いので、trueが良いかなと思っています。
code-block-bg コードブロックの背景色をhexで指定できます。
echo trueにするとpythonコード自体がドキュメントに含まれます。falseにすると実行結果のみが含まれます
fig-pos \figureでやる、htbpとかHとかの例のアレです。わからない人はHでいいんじゃないかなと思っています。
bibliography、cite-method、biblatexoptions 参考文献関連です。
latex-max-runs 上記の通りで最大実行回数を制限できます。
lang このlangをjaに設定するとTex内での参照がFigure→図、 Table→表など日本語になります。これを設定しただけで日本語対応するような代物ではないです。
highlight-style シンタックスハイライトのスタイルです。

qmdファイルサンプルと出力例

折り畳み内に記載したファイルを、.qmd形式で作成し、VsCode拡張を利用している場合はCtrl+Shift+Kでpdfが出力されます。
Markdown+Pythonの割にはかなり良い感じと思います。

qmdファイル
---
title: "Quarto Basics"
subtitle: "サブタイトルを長々とつけて、サブタイトルの折り返しが発生する場合の挙動を見てみようと思います"
author: MosaMosa
date: "2022/12/31"
abstract: "jupyternotebookに比べ、即時の実効性は劣るものの、TexやHTMLなど組版としての機能にはQuartoの優位性があります。特にipynbをpdfなどに出力すると改ページの挙動などがイマイチで悩んでいたのですが、そういった悩みは改善されそうです。"
format: pdf
pdf-engine: lualatex
documentclass: ltjsarticle
papersize: a4 
include-in-header: 
  file: ./luatex_header 
toc: true
toc-depth: 3
number-sections: true
code-block-bg: "#F6F6F6"
echo: true
fig-pos: "H"
bibliography: ./references.bib
cite-method: biblatex
biblatexoptions: style=numeric-comp
latex-max-runs: 5
lang: ja
highlight-style: github
editor:
  render-on-save: true
jupyter: python3
---

#

##

###

#### toc-depth 3だと目次に表示されない見出し

quarto[@quarto]での日本語入力のテスト @fig-polar.
長い文章を入力した時に日本語だとうまく折り返し処理をしてくれないことがあるが、そういったことが起こらないかの確認のための文章。

```{python}
#| label: fig-polar
#| fig-cap: "A line plot on a polar axis"

import numpy as np
import matplotlib.pyplot as plt

r = np.arange(0, 2, 0.01)
theta = 2 * np.pi * r
fig, ax = plt.subplots(
  subplot_kw = {'projection': 'polar'} 
)
ax.plot(theta, r)
ax.set_rticks([0.5, 1, 1.5, 2])
ax.grid(True)
plt.show()
```


See Also

https://quarto.org/docs/output-formats/pdf-basics.html
https://quarto.org/docs/output-formats/pdf-engine.html
https://quarto.org/docs/reference/formats/pdf.html

おわりに

上記で基本的なドキュメント作成はできるようになりました。
サポートされているMarkdown記法の一覧や、そのほか機能をみつつ使っていこうと思っています。

Discussion