Notionに記事を機械的に移行しようとしたら大変だった話

Nature Engineering Blog祭り 7日目はCorporate ITのmaronyからお送りします。
簡単に自己紹介すると、Natureには今年の3月にJoinして、Corporate IT/情報マネジメント Roleという立場でいわゆる情シス的な業務をやっています。今までは総務やSREの方たちが片手間になんとかやっていた感じなので、純な情シスとしては1人目のメンバーです。最近はセキュリティ勉強会とかもしたりしてます。
今回は、KibelaからNotionに記事を機械的に移行しようとして大変だった話をします。(Notionのインポート仕様の話がメインです)

背景

NatureではもともとナレッジマネジメントツールとしてKibelaを使っていました。
2021年末ぐらいからNotionに移行することに決めたらしく、私がJoinした3月頭には新規記事は完全にNotionに作成するところまでは到達していました。*1
ただ、まだまだKibelaの方も現役で、以前作成したKibelaの記事をNotionから参照する形で記事が作られていることもある、という状態でした。
Kibelaはどうやら1ヶ月間まったくアクセスがなかったユーザーには課金しない仕組みにしてくれている*2ようだったのですが、NotionからKibela記事を参照する状態になっていると、月に1回しかKibelaを見てなくてもそのユーザー分はフルで課金されることになってしまうので、やっぱり移行しきっちゃったほうが良いかなということでクロージングのプロジェクトをはじめました。
とはいえ、その当時でもKibelaには約5,500件の記事が存在していたため、1個1個手作業で移行してもらうのだとすごい負担になります。というわけで、Corporate IT側である程度機械的に記事を移行して、それをチェック&必要な場所に移動してもらう、というやり方ですすめることにしました。

移行祭り

Kibelaからの出力

  • KibelaからはMarkdown形式で記事が一括エクスポートできたので、その点は楽ちんでした。
  • Kibelaの記事属性 (作成者等の情報や、記事に追加されたコメントなど) はMarkdown冒頭のコメントブロックにyaml形式で出力されていました。
  • 記事に直接貼り付けた添付ファイルも一緒にエクスポートされます。エクスポートされたMarkdown中では、その添付ファイルを参照する形式に自動で変換されています。
例1)
[image.png](100.png)

例2)
<img title='image.png' alt='image' src='../attachments/100.png' width="660" data-meta='{"width":660,"height":1148}'>
  • 元のMarkdownデータをそのままエクスポートするので、記事(作成者)によって結構表記揺れがありました
    • Markdown記法とHTML記法が混ざってる(添付ファイルの例などのように)
    • Kibelaはテーブルの記法も結構雑に書いても解釈してくれてました。また、後から気づいたのですがCSVコードもテーブル変換してくれてました。
(パターン1:普通のMarkdown記法)
|title|text |
|-----|-----|
| aaa | bbb |

(パターン2:省略版、これもテーブルとして解釈してくれてた)
title|text
-----|-----
aaa | bbb

(パターン3:codeブロック型CSVもテーブルとして解釈してた。※便宜上行頭にスペースを入れてます)
 ```{csv}
 title,text
 aaa,bbb
 ```

この時点では「なるほど、ちょっと表記揺れは怖いけど、まぁKibelaの記事属性周りとか添付ファイル周りをうまいことやれば結構さらっと機械移行できんじゃないかな」と思ってました。 結論から言うとめっちゃ大変だったし100%機械移行することはできませんでした

Notionインポート時の大誤算

添付ファイルに関するAPIがない

最初に躓いたのは添付ファイル関係でした。この記事作成時点(2022年6月)では、NotionにはファイルをアップロードするAPIがありません。また、添付ファイル込みのインポートにも対応していません。
結局、Google Driveにアップロードして、そのリンクを埋め込む、というやり方にしました。Google Drive周りはAPIが充実しているので、アップロードも参照用リンクの一括取得も楽ちんです。(実際にはファイルアップロードは手動でゴリ押しましたが、些事だよ些事!)
残念なのは、embed link形式はNotionが認識してくれないことですね。なので結局見栄えを整えようとすると、手動で差し替える必要があります。まぁよく使う記事なら手動で差し替えてね、という対応にすることにしました。
先駆者さん も同様の対応をしています(こちらはS3を使ってます)。この記事でもふれてますが、アップロードした資料の公開範囲を考慮してうちではGoogle Driveを採用しています。この記事はいろいろ参考にさせてもらいました。

mdファイル自体をインポートするAPIもない

次に躓いたのは、mdファイルを一括取り込みするAPIがないということです。
ページ作成のAPI自体はあるのですが、Notionは要素を1行ずつブロック形式で解釈する仕様になっているので、APIでページを作るときはコンテンツを1行ごとにブロックとして定義して挿入する、という手法を取る必要があります。つまり、Markdownファイルを1行ずつ解釈して適切なブロックを判断してそのブロックを挿入するためのbody dataを作成して……といったようなコンバータが必要になります。 いやその対応は辛いわ。
ということでここは諦めることにしました。手動でMarkdownファイルをインポートするやり方でも、一応複数ファイルまとめて読み込ませることはできる(今のページの子ページとして読み込まれる)ので、これで頑張りました *3

ブロック単位で解釈されるので、改行やリスト記法などが死ぬ

ここまででも十分苦しみましたが、更にNotionのインポート仕様に苦しめられることになります。
上でも書きましたが、Notionでは、コンテンツが1つの塊ごとに1つのブロックと言う形で表現されています。 文章やリストだと、改行によってブロックが区切れて、次のブロックが生まれるわけです。

問題はここからです。Notionは、Markdownを読み取る際、(ヘッダーやテーブル、コードブロックなどを除き) 前後に空行がある場合にブロックを区切る (逆に言うと、 次に空行が出てくるまでは同じブロックと認識する)という仕様があるようなのです。 つまり、こういったMarkdownファイルをインポートすると

こういったコンテンツは、
こういうリストでまとめたい
- リストA
  - リストA-1
  - リストA-2
    - リストA-2-1

Notionではこのように1つのブロック = 1行のテキストとして解釈されちゃいます。

こういったコンテンツは、こういうリストでまとめたい- リストA  - リストA-1  - リストA-2    - リストA-2-1

もし想定通りに読み込ませたい場合は、↓のように適切に空行を入れて上げる必要があります。

こういったコンテンツは、     ←1行のテキストブロックになる

こういうリストでまとめたい     ←1行のテキストブロックになる

- リストA              ←この固まりはリストとして認識される
  - リストA-1
  - リストA-2
    - リストA-2-1

この仕様によって、改行されていた文章は1文になり、リストやチェックリスト、クウォート表示などは無事死亡し、テーブルは崩れ、半泣きになりながら機械移行スクリプトを修正することになりました。

区切り線がコメントブロック扱いになってしまう

区切り線は「---」で表現できますが、Notionの場合は文章と同様に前後に空行が必要になります。なので、

---      ←区切り線とみなされる

ここは区切り線の中のコンテンツです

---      ←区切り線とみなされる

次のトピック

↑みたいなMarkdownは正しく2つの区切り線として読み込まれるのですが、↓みたいなMarkdownだと2つの「---」でくくられたエリアがコメントブロックとみなされてまるっと無視されてしまいます。

---      ←区切り線のつもり、だけど直後に空行がないのでコメントブロック扱いされる
ここは区切り線の中のコンテンツです

---      ←区切り線のつもり、だけどコメントブロックの終わりとみなされる

取り消し線の仕様が微妙に違う

細かいところですが、Markdownの取り消し線って "~" 1個でくくればOKな場合があります。(Kibelaはそうでした)
一方、Notionは厳密に "~~" のようにチルダ2個でくくってあげる必要があります。この仕様の違いにより微妙に取り消し線が反映されないことがありました。(これはどっちかというとKibelaが柔軟すぎたがゆえの弊害かも)

コードブロック内の改行が死ぬ

コードブロックはMarkdownだと以下のように表現されますね。

 ```
 - aaa
   - aaa-1
   - aaa-2
 - bbb
 ```

↑はちゃんと読み込まれる記法です。(何も言語を指定してないと、plain textとして読み込まれます)

一方、言語を指定しているコードブロックの場合、ブロック内の改行が死ぬことがあります。 例えば、こんな感じでyamlを指定してしまっていると

 ```yaml
 - aaa
   - aaa-1
   - aaa-2
 - bbb
 ```

Notionに読み込んだときは、こうなってしまいます↓(コードブロックとしては解釈されるけど、改行が死ぬ)

確認した限り、plaintext, shellを指定していた場合はセーフで、それ以外はだいたいアウトでした。(yaml, json, html, Javascript等では確認)

シンプルテーブルでインポートできない

これは見栄えを気にする人にとってはかなり微妙な仕様かもしれません。
Notionではシンプルテーブルを読み込むと、自動的にデータベースに変換するという仕様があります。
そしてこれはMarkdownをどう編集しようが回避不可です。 対処法としては、データベースとして生成されちゃったテーブルを右クリックして、「Turn into simple table」でシンプルテーブルに再変換することです。

ゴリ押しスクリプティング機械移行

紹介したように、いろいろと落とし穴があったので、最終的に機械移行としては以下のような形になりました。

  • Kibelaから全記事/添付ファイルをダウンロードする
  • Kibelaの添付ファイルを全件Google Driveにアップロードして、その共有用URLを全部取得する
  • KibelaMarkdownファイルをスクリプトでコネコネする
    • アップロードした添付ファイルと同名のファイル名を参照するリンクブロックが存在していたら、Google Driveのファイルのリンクに置き換える
    • テキストやリスト、区切り線については、1行ごとに改行を加えることでちゃんと想定通り読み込まれるようにする。
      • ただし一部例外があって、クウォート表示を示す「>」については改行挟んで連続させちゃうとNotionが絶対インポートエラーを吐くのでここは例外にする。
    • 取り消し線記号はNotion仕様に修正
  • コネコネしたMarkdownファイルを、手動で頑張って読み込ませる
  • 読み込ませたページ一式を一括ダウンロードしてリンクを取得し、旧Kibelaのページのリンク、属性情報などと一緒にNotion版ページのリンクを載せたデータベースを作成
    • ちなみに何回かページをアップし直してまして、データベースのNotionのリンクだけ一括で更新したいな、と思ったことがあるのですが、今だとCSVを読み込ませる方法だとUpdateが出来なくて(タイトルが同じでも別のレコードができちゃう)、ぐぬぬとデータベース自体を作り直ししてました。
  • メンバーに通知して、必要なページをNotionのページ/データベース配下に移動したり、中身を修正したりしてもらう。

(実際には、アップしてメンバーに移行を試してもらって、いろいろ崩れてるぞという指摘を受けて、調査して、スクリプトを修正して……みたいなルートをたどってます)

これでも結局、codeブロック形式のテーブルだったり、画像埋め込みだったり、移行しきれないところとかは出てきちゃったので、最終的には人の力が必要になっちゃいましたが。

最後に

さんざんNotionのインポート仕様について文句があるみたいな書き方をしましたが、 Notionはとても優秀で、非常に便利かつ快適に使えるツールだと思います
他のツール(特にMarkdown系)から移行するときに注意すべき点を知っておくと、今後Notionを使っていきたいたいという方にとっては便利かなーということでこのトピックで記事にしてみました。
というわけで7日目の記事でした。明日はハードエンジニアの神園さんの記事になります。

また、Natureではエンジニアを積極採用中です。

herp.careers

カジュアル面談も歓迎なので、ぜひお申し込みください。

herp.careers

Natureのミッション、サービス、組織や文化、福利厚生が気になる、という方はCulture Deckを公開しているので、こちらをご覧ください。

speakerdeck.com

*1:このあたりのエピソードは、昨日の記事で、SREかつNotion導入推進大臣の黒田さんが書いてくださってるので、まだの方はぜひ読んでみてください!

*2:利用がなかったユーザー分の料金をポイントと言う形で付与して、翌月以降の請求から相殺してくれます

*3:といっても100件同時読み込みとかさせると途中で結構エラー落ちしてたので、手放しにはやれなかったのですが。。