🖊️

LaTeXを用いた論文の執筆体験を最高にする

2023/12/03に公開

天久保 Advent Calendar 2023 の3日目の記事です。いまだに「天久保」が何なのかよくわかっていないですが、今年も書きます。

tl; dr

template repository が生えています。こちらへどうぞ。

https://github.com/Okabe-Junya/latex-project-template

背景

論文執筆の時期になりました。卒論や修論、国際会議に向けてなど多くの方が論文を書いていることでしょう。特に理工系の論文の場合、組版システム LaTeX を使っている方も多いことでしょう。
LaTeXはWordのような文書作成ソフトと比べて、数式の表現しやすさ、Bibliography(いわゆる bib)を用いた参考文献の管理のしやすさ、論文の体裁を自動的に統一してくれるなどの長所があげられます。その一方で環境構築が面倒である点や、処理系が複雑であること、デバッグの難しさや文法の複雑さなどの短所もあります。

別の問題として、特に卒論や修論においてはバージョン管理やバックアップをどのようにとるべきかという問題があります。この時期は、それに言及する大学教員の方のツイートがバズっていることも多いです。情報系の学生であれば、GitHubのようなサービスを用いることでこの問題を解決している方も多いと思います。ではすでにGitHubを使用していた場合、さらに論文執筆の体験を良くするためにはどうすればよいでしょうか。

この記事では、LaTeXを用いた論文執筆の体験をより良くするいくつかの方法を紹介します。学生を想定読者としていますが、研究者やエンジニアの方にも参考になるかもしれません。

要件

本題に入る前にあらためて要件を整理します。この記事では、「LaTeXを用いた論文の執筆体験が良い」ことを 以下の要件が満たされている こととします。[1]

  1. MUST: LaTeXの環境(日本語)を 簡単 かつ 再現可能 に構築できること
  2. MUST: 再現性のあるビルドによってPDFを生成できること
  3. MUST: 意識せずに *.tex*.bib*.sty のようなソースコードとそれらをコンパイルした結果のPDFの 両方 を適切にバックアップできること
  4. SHOULD: typoや文法ミスを自動的に検出、修正できること

特に4については言及していませんでしたが、これも重要な要素の一つでしょう。
ソフトウェア工学的な観点からリンターやフォーマッター、テストなどによってコードの品質を保つことは重要でしょう。論文執筆においても、文法ミスやtypoを自動的に検出することで、論文を構成する自然言語の品質を保つことができます。特に学生の多くは、指導教員からのフィードバックを受けることも多いでしょう。その際にtypoや機械的なミスを含めたまま提出するのはお世辞にも褒められたことではありません。しかし人間にミスはつきものであることもまた事実であり、このような問題はソースコードのそれらと同様、テクノロジーによって解決されるのが喜ばしいことです。

前置きが長くなりましたが、本題に入ります。

環境構築

ローカルへの環境構築であれば、以下のような記事が参考になると思います。

もちろんこれらの記事を参考にして自前で構築するのも良いですが、

  • 複数の[マシン, OS]を使っており、環境に応じてセットアップするのが手間
  • 手癖でPCを初期化する

ような属性を持っている人[2]からするとそれでも大変なものです。そこで、Dockerを用いて環境構築を行います。ありがたいことに、比較的軽量なLaTeX用dockerイメージが存在するためこれを利用します。

こちらの文章の中に

基本的に platex と uplatex の環境が入っている.
(中略)
latexmk も入っているので,適当に latexmkrc でも用意してやるといい感じになる.
(中略)
また, tlmgr も入っているので,足りないパッケージはインストールできるし, ベースとして新しいイメージを作るのもありだと思う.

と書いてあります。書いてある通り、latexmkrcを生やします。

latexmkrc
#!/usr/bin/perl

$pdf_mode = 3;

$latex = 'platex -halt-on-error -file-line-error -interaction=nonstopmode';
$latex_silent = 'platex -halt-on-error -file-line-error -interaction=batchmode';
$bibtex = 'upbibtex %O %S';
$dvipdf = 'dvipdfmx %O -o %D %S';
$makeindex = 'mendex %O -o %D %S';
$dvips = 'dvips %O -o %D %S';
$ps2pdf = 'ps2pdf %O %S %D';

# set build directory to ./build
$out_dir = 'build';

# delete temporary files
$clean_full_ext = 'synctex.gz synctex.gz(busy) run.xml tex.bak bbl bcf fdb_latexmk fls log nav snm vrb';

これでLaTeXのビルド環境は整ったのですが、このdockerイメージ

ビルドのたびに docker run ... みたいなコマンドを叩きたくないので、Makefileをさくっと錬成したくなります。以下のようなMakefileを用意すれば、make でビルドが走ってホストマシン上にPDFが生成されます。

Makefile
DEFAULT_GOAL := build


.PHONY: docker-build
docker-build:
	@echo "Building docker image..."
	@docker build -t tex-ja:latest .

.PHONY: build
build:
	@echo "Building..."
	@docker run --rm -v $$PWD:/workdir paperist/texlive-ja latexmk

.PHONY: clean
clean:
	@echo "Cleaning..."
	@rm -rf ./build
	@mkdir ./build && touch ./build/.gitkeep

このMakefileから察する方もいるかもしれませんが、Dockerのバインドマウントを用いてホストマシンにPDFを持ってきています。これはこれで良いのですが、viewerの設定などが面倒です。

どうせDockerを用いるのであれば、devcontainerを用いて、開発環境をまるっとコンテナに乗せたくなります[3]。簡単な設定は以下のようになります。

.devcontainer/devcontainer.json
{
    "name": "Latex Environment",
    "dockerFile": "Dockerfile",
    "workspaceFolder": "/workspace",
    "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached",
    "customizations": {
        "vscode": {
            "extensions": [
                "James-Yu.latex-workshop",
                "mhutchie.git-graph",
                "donjayamanne.githistory",
                "EditorConfig.EditorConfig",
                "ms-azuretools.vscode-docker"
            ]
        }
    }
}
.devcontainer/Dockerfile
FROM paperist/texlive-ja

ARG USERNAME=user-name-goes-here
ARG USER_UID=1000
ARG USER_GID=$USER_UID

# Create the user
RUN groupadd --gid $USER_GID $USERNAME \
    && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME

# Install packages
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    git \
    curl \
    wget \
    unzip \
    make \
    && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# Set the user
USER $USERNAME

エディタがVSCodeに制限されてしまう[4]問題はありますが、VSCodeをひらけば環境構築が完了しているのはかなりありがたいのでは無いでしょうか。

バックアップ

LaTeXプロジェクトのソース一式をGitHubによってすでに管理している方は、手元のマシン上と github.com 配下にソースが置かれているため、どちらか一方でデータが失われても復旧できます。しかし、それでも以下のような問題は残ります。

  • PDFのようなバイナリ形式のファイルはしばしばgit管理下に置かれないため、手元のマシン上にしか存在しない[5]
  • (極めて稀なケースですが)github.com で障害が発生している場合には、手元のマシン上にしか存在しない。cloneしていないマシンでは作業ができない[6]

2点目に関してはもはや当てつけかもしれませんが、このような状況が発生した時のために3重のバックアップを行いたくなります。特に、ソースコードからビルド済み成果物までを含めた一式を継続的にバックアップしておきたいものです。[7]

これを実現する最も単純な方法は、AWS S3やGoogle Cloud Storageなどのクラウドストレージにバックアップを取ることです。[8]もちろんこんなことを手作業でやりたいはずもないので、シュッとGitHub Actionsを用いて自動化します。GCPを用いた例を以下に示します。

.github/workflows/backup.yml
on:
  push:
    branches:
      - main

jobs:
  build:
    permissions:
      contents: 'read'
      id-token: 'write'

    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Pull Docker Image
        run: docker pull paperist/texlive-ja:latest

      - name: Build LaTeX
        run: make build

      - name: archive artifacts
        run: |
          mkdir -p artifacts
          cp <source-file> ./artifacts
          cp <pdf-file> ./artifacts

      - name: auth gcp
        uses: 'google-github-actions/auth@v1'
        with:
          workload_identity_provider: "projects/${{ secrets.PROJECT_ID }}/locations/global/workloadIdentityPools/${{ secrets.POOLNAME }}/providers/${{ secrets.PROVIDER_NAME }}"
          service_account: "${{ secrets.SERVICE_ACCOUNT_NAME }}@${{ secrets.PROJECT_NAME }}.iam.gserviceaccount.com"

      - name: Use gcloud CLI
        run: 'gcloud storage cp -r ./artifacts gs://<bucket-name>'

技術的な詳細は、以下のGitHub org内のドキュメントなどに委ねますが、この例ではサービスアカウントを用いてGitHub ActionsからGCPにアクセスしています。OIDCプロバイダの設定を少しゴニョゴニョする必要がありますが、特別に複雑なことをしているわけではありません。

これでmainブランチに変更が加えられると、ソース一式とビルド済み成果物がGCPにバックアップされます。これで、

  1. 手元のマシン
  2. github.com 配下
  3. storage.cloud.google.com 配下

の3重のバックアップが実現されました。かなり安心できるのではないでしょうか。もちろんGCPにバックアップするのはあくまで一例です。他のクラウドストレージやオンプレストレージ、その他にも例えば組織内でSlackを用いている場合にはSlackに投げるなど、様々な方法が考えられます。

[余談] GCPの選定理由

所属している研究室ではgoogle workspaceを用いており、さらに計算機資源としてGCPを用いています。[9]なので、組織内に向けたIAMの設定のしやすさ、請求の管理のしやすさなどからGCP(Google Cloud Storage)を用いています。

[余談2] SaaSを用いたバックアップの副次的なメリット

これらのサービス上にPDFを置く副次的なメリットとして、スマートフォン(やタブレット端末)からでもブラウザやアプリケーションを介してPDFを閲覧することができます。移動中に書いた文章の推敲を行うことが多い自分にとっては、地味にありがたいことです。

typoや文法ミスの検出

これは端的に言えば自然言語に対するリンターが欲しいわけです。と思って見てみると欲しいものがあるわけです。

https://textlint.github.io

textlintは、ルールやプラグインを独自に設定することで、自然言語の文法ミスやtypoを検出することができます。とはいえ、これらの設定を一から行うのは大変です。さらにマークダウンやLaTeXなどの文書フォーマットをパースするのも一苦労です。

ありがたいことに、これらの問題のほとんどは有志によってすでに解決されています。例えば、

  • textlint向けのLaTeXのパーサーは、textlint/textlint-plugin-latex2e が存在します
  • 設定項目に関してもいくつかのプリセットが有志によって提供されています

これらのプラグインやプリセットを使うことで、設定にかける時間を大幅に削減することができます。このような背景を踏まえた上で、configの一例を以下に示します。これは自分が実際に使っている設定です。

.textlintrc
{
  "plugins": [
    "latex2e"
  ],
  "rules": {
    "preset-ja-spacing": true,
    "preset-ja-technical-writing": {
      "ja-no-mixed-period": {
        "periodMark": "。"
      },
      "no-mix-dearu-desumasu": false,
      "max-kanji-continuous-len": false,
      "sentence-length": false
    },
    "preset-ja-engineering-paper": {
      "unify-kuten-and-touten": false
    },
    "preset-jtf-style": {
      "1.1.1.本文": false,
      "1.2.1.句点(。)と読点(、)": false,
      "1.2.2.ピリオド(.)とカンマ(,)": false,
      "4.1.3.ピリオド(.)、カンマ(,)": false
    }
  },
  "filters": {
    "comments": {
      "enablingComment": "textlint-enable",
      "disablingComment": [
        "textlint-disable",
        "TODO: "
      ]
    }
  }
}

あとは package.json に以下のように記述したうえで、npm ci && npm run lint などとすれば、良い感じにtextlintが走ってくれます。

package.json
{
  "scripts": {
    "lint": "npx textlint -f checkstyle *.tex"
  },
  "devDependencies": {
    "textlint": "^13.4.0",
    "textlint-filter-rule-comments": "^1.2.2",
    "textlint-plugin-latex2e": "^1.2.1",
    "textlint-rule-preset-ja-engineering-paper": "^1.0.4",
    "textlint-rule-preset-ja-spacing": "^2.3.0",
    "textlint-rule-preset-ja-technical-writing": "^8.0.0",
    "textlint-rule-prh": "^5.3.0",
    "textlint-rule-spellcheck-tech-word": "^5.0.0"
  }
}

ちなみにtextlintは各種エディタへの統合にも対応しています。詳しくは以下のページを参照してください。

https://textlint.github.io/docs/integrations.html

GitHub への統合

ここまでの流れから推測できる方も多いかもしれませんが、リンターであればCIで発火させた方が体験が良いわけです。適切にPRを作成している前提にはなりますが、例えば reviewdog を用いることでtextlintの結果をPRにコメントしてくれます。

.github/workflows/textlint.yml
on:
  pull_request:
    branches:
      - main
  workflow_dispatch:

jobs:
  reviewdog-check:
    name: reviewdog
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}

      - name: setup reviewdog
        uses: reviewdog/action-setup@v1
        with:
          reviewdog_version: latest

      - name: Set up node
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: install Dependencies
        run: npm ci

      - name: Execute textlint
        run: npx textlint -f checkstyle **/*.tex | reviewdog -f=checkstyle -name="textlint" -reporter="github-pr-review"
        env:
          REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}


reviewdogに怒られているの図

テンプレート

GitHubリポジトリのテンプレートを作成しました。ここで紹介した内容がまとまっています。

https://github.com/Okabe-Junya/latex-project-template

まとめ

この記事の中では、LaTeXを用いた論文の執筆体験を最高にするためのいくつかの方法を紹介しました。特に最初に定義した要件を達成するために以下の内容を紹介しました。

  • dockerを用いた環境構築
    • devcontainerを用いたVSCodeの環境構築
  • GitHub Actionsを用いたリンターの自動化
  • github.comとクラウドストレージを用いた3重のバックアップ

これですばらしい執筆体験が整いました。あとは論文を書くだけです。

脚注
  1. ここにあげた定義が全てではないことは言うまでもありません。本記事では触れませんが、例えば「IDEが適切に補完してくれる」「文献管理ツールとの統合によってbibファイルの生成が半自動化されている」なども執筆体験に関わりうるでしょう ↩︎

  2. 特に2つめの要件に当てはまる人がどれだけいるのかわかりませんが、自分はその一人です ↩︎

  3. LaTeXの環境構築からVSCodeの環境構築に話がすり替わっただけと言われればそれまでですが、少なくともVSCodeの環境構築(といってもほとんどインストールで事足りますが)の方が簡単であることは間違い無いでしょう ↩︎

  4. 集団開発の際などにこれはしばしば問題になりますが、この記事の中ではあまり問題ないでしょう ↩︎

  5. バージョン管理とはいえないですが、バイナリも全てgit管理下においてしまうと言うのもある意味では解決策でしょう。しかしVCSを用いている以上、そのような場合は後述するartifactsを用いた方法を採用すべきかもしれません ↩︎

  6. scpなどを用いれば良いと言われればそれまでです ↩︎

  7. github.com 配下にPDFを置きたいだけであれば、artifacts を用いるのが簡単です。actions/upload-artifact が用意されているので、ビルドしたPDFをこのreusableワークフローに投げ込むだけで目的が達成できます ↩︎

  8. もちろん手元に大規模(でなくても良い)ストレージを用意する方法も取れます。オンプレストレージ事情などには詳しくないのですが、MinIOやHadoop(HDFS)などが選択肢になってくるのでしょうか ↩︎

  9. もちろんオンプレマシンもありますが、最近はGCPが中心になっています ↩︎

Discussion