1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Jupyter Notebook → PDF 変換方法の比較表 + 変換スクリプト例 (Playwright 使用)

Last updated at Posted at 2025-09-17

[2025-10-05 追記] 変換スクリプトをリファクタリングしました。
[2025-10-17 追記] 変換スクリプトにクラス名の更新漏れがあり修正しました。

Jupyter Notebook → PDF 変換方法の比較表をまとめました (以下)。誤りがありましたらご指摘いただけますと幸いです。

  • 印刷プレビュー経由で PDF に保存する方法 (表の 1 番上) は追加ソフトは不要ですが、PDF からコピペしたコードを実行できません (往々にして PDF からのコピー時に空白が崩れるのでインデントを手動で復元する必要がある)。また、Web ページを印刷するときにブラウザの印刷設定を変えると影響を受けます。
    • なお、これでもよいのでコマンド実行したいという場合は、jupyter のコマンドで HTML に変換した後 ブラウザのヘッドレスモードで PDF に変換できます。1
  • 表の 2 番目以降の方法であれば、それらのデメリットを (ある程度) 克服できます。
    • が、追加ソフト (外部バイナリ) が必要になります。
    • また、やはり空白は崩れるので、空白記号に置換する運用回避になります。
    • さらに、LaTeX 経由の方法は (Jupyter 内蔵の方法だが) HTML をレンダリングしないため、pandas.DataFrame をそのまま OR Styler で表示している場合は相性が悪いです。

image.png

この記事の最後に HTML 経由 PDF 変換スクリプト例を示します。

【補足】HTML オブジェクト出力について

例えば Pandas Styler でデータフレーム内の負の数を赤くしたり、最大値を強調したりすることがあるかもしれませんが、これは TeX 経由では PDF 変換できません。

下図の左側は後述のスクリプトで HTML 経由で PDF 化したノートブック、右側は jupyter nbconvert --to pdf Untitled.ipynb コマンドで (TeX 経由で) PDF 化したノートブックです。左側ではデータフレームが表示できていますが、右側は表示できていません。

image.png

つまり、TeX 経由の PDF 変換では display(HTML(...)) などといった HTML としてレンダリングされる要素の変換には対応しておらず、表示できません (なお、 df.styledisplay(HTML(...)) としなくてもセル末尾でこれを返せばノートブック上にレンダリングされますが、結局 HTML としてレンダリングされるのでどのみち表示できません)。

【おまけ】HTML 経由 PDF 変換スクリプト (Playwright 使用)

Jupyter Notebook を HTML 経由で PDF に変換するスクリプトの例です。ブラウザ操作ライブラリは Playwright を使用しています。

  • Playwright の pdf メソッドのドキュメントは以下です。余白 margin はデフォルトでゼロになっていて以下では変更していませんが好きに変更することもできます (はず)。
    Page | Playwright Python
nb2pdf.py
"""
Jupyter Notebook を PDF に変換します。以下のように実行してください。

使用例:
    python nb2pdf.py aaa/bbb/ccc.ipynb
    python nb2pdf.py aaa/bbb/ccc.ipynb --header  # ファイル冒頭に「関係者外秘」と印字します。
"""
from playwright.sync_api import sync_playwright
from bs4 import BeautifulSoup
from pathlib import Path
import subprocess
import re
import argparse


class NotebookPDFConverter:
    """ Jupyter Notebook を HTML 経由で PDF に変換するクラス

    Note:
        aaa/bbb/ccc.ipynb を aaa/bbb/tmp.html を経由して aaa/bbb/ccc.pdf にします。
        中間ファイル・出力ファイルは上書きするので、必要があれば上書き防止処理を追加してください。
        中間 HTML への加工処理 [2a] [2b] [2c] は不要なら削除して構いません。
    """
    @classmethod
    def _repl(cls, match):
        return '\n' + '\u2423' * len(match.group(1))

    @classmethod
    def replace_whitespaces_in_pre(cls, soup, pre):
        # ある pre タグ内の行頭の空白を空白文字に置換する
        for child in pre.children:
            if child.name or ('\n' not in child):
                continue
            # 改行を含む文字列のとき改行後の空白を同じ数の U+2423 Open Box に置換 (ライトグレーで)
            tag_open_box = soup.new_tag('span', style='color: lightgray;')
            tag_open_box.string = str(re.sub(r'\n( +)', NotebookPDFConverter._repl, child))
            child.replace_with(tag_open_box)

    @classmethod
    def replace_whitespaces(cls, soup, cells):
        # セル内の行頭の空白を空白文字に置換する
        for cell in cells:
            pres = cell.find_all('pre')
            for pre in pres:
                NotebookPDFConverter.replace_whitespaces_in_pre(soup, pre)

    def __init__(self, ipynb_path, print_header):
        html_path = ipynb_path.parent / 'tmp.html'
        pdf_path = ipynb_path.with_suffix('.pdf')

        # [1] まずノートブックを HTML に変換し Beautiful Soup でパースする
        subprocess.run(['jupyter', 'nbconvert', '--to', 'html', '--output', html_path, ipynb_path])
        soup = BeautifulSoup(html_path.read_text(encoding='utf8'), 'html.parser')

        # [2a] インプットセル内の行頭の空白を空白文字に置換する (PDF 上のコードがコピペできなくなるため)
        cells = soup.find_all('div', class_='jp-Cell-inputWrapper')
        NotebookPDFConverter.replace_whitespaces(soup, cells)

        # [2b] head タグ末尾に独自スタイルを挿入する
        tag_custom_style = soup.new_tag('style', type='text/css')
        tag_custom_style.string = 'body {margin: 0 !important; padding: 0 !important;}\n' \
            + 'div.jp-MarkdownOutput {font-family: "Rounded Mplus 1c"; line-height: 1.2;}\n' \
            + 'pre {font-family: "JetBrains Mono" !important;}\n'
        soup.head.append(tag_custom_style)

        # [2c] main タグ先頭に「関係者外秘」と印字する
        if print_header:
            soup.find('main').insert(0, '関係者外秘')

        # [3] HTML を PDF として保存する
        with sync_playwright() as p:
            browser = p.chromium.launch()
            page = browser.new_page()
            page.set_content(str(soup))
            page.pdf(path=pdf_path, format='A4', print_background=True, scale=0.9)
            browser.close()


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('ipynb_path', type=str)
    parser.add_argument('--header', action='store_true')
    args = parser.parse_args()
    NotebookPDFConverter(Path(args.ipynb_path).resolve(), args.header)
[参考] 当初のスクリプト (これでも動くがハードコーディング & メンテナンス性が悪い)

上図の左側の、HTML 経由 PDF を生成したときに使用したスクリプトは以下です。コード内かどうかの判定は適当にやっています (Beautiful Soup をつかった方がメンテ性のよいスクリプトになるとは思います)。

script.py
from playwright.sync_api import sync_playwright
import subprocess
import re

def repl(match):
    spaces = len(match.group(1))
    return '\u2423' * spaces  # 行頭の空白と同じ個数の空白記号を返す

# ---------- (1) HTML 変換 ----------
subprocess.run(
    ['jupyter', 'nbconvert', '--to', 'html', 'Untitled.ipynb'],
    check=True
)

# ---------- (2) HTML 読み込み兼書き換え ----------
code_flag = False
html = ''
with open('Untitled.html', encoding='utf8') as ifile:
    for line in ifile:
        # コード内かどうかの判定
        if '<div class="highlight hl-ipython3"><pre>' in line:
            code_flag = True
        if code_flag and '</pre></div>' in line:
            code_flag = False

        # head が終わる直前に独自スタイルを挿入
        if '</head>' in line:
            line += (
                '<style type="text/css">\n'
                'div.jp-MarkdownOutput {\n'
                '  font-family: "Rounded Mplus 1c";\n'
                '  line-height: 1.2;\n'
                '}\n'
                'body {\n'
                '  margin: 0 !important;\n'
                '  padding: 0 !important;\n'
                '}\n'
                '</style>\n'
            )

        # コード内なら行頭の空白を空白記号に置換
        if code_flag:
            line = re.sub(r'^( +)', repl, line)
        html += line

# デバッグ用
# with open('out.html', mode='w', encoding='utf8') as ofile:
#     ofile.write(html)


# ---------- (3) 読み込んだ HTML を PDF として保存 ----------
with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.set_content(html)
    page.pdf(
        path='Untitled.pdf',
        format='A4',
        print_background=True,
        scale=0.9,
    )
    browser.close()
  1. 手元の Windows 上の Git Bash + Chrome ではコマンド "/c/Program Files/Google/Chrome/Application/chrome.exe" --headless --print-to-pdf=/c/Users/user_name/output.pdf --no-pdf-header-footer file:///C:/Users/user_name/source.html で HTML を PDF 化できました。ただ、PDF 出力時の用紙サイズや倍率はコマンドラインでは制御できないようで、HTML の段階で CSS に記述する必要があるようです (私はやっていないのでわかりません)。 参考: https://stackoverflow.com/questions/46077392/additional-options-in-chrome-headless-print-to-pdf

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?