概要
カミナシはこれまでドキュメント管理ツールにScrapboxを使っていました。 手軽にかけ、リアルタイムに複数人で編集できるのでチームで議論しやすかったです。一方でドキュメント管理ツール以外に使ってるツールが増えたり、ストックしたい情報が探しにくくなっていたので、全社的にNotionを利用することにしました。
今回はドキュメントツールの移行に伴ってScrapboxにあったページを全てNotionにコピーした流れを紹介していきます。
ScrapboxのデータをMarkdownに整形する
Scrapboxはプロジェクトの設定ページからJSONファイルとしてエクスポートできます。 JSONファイルは1行ずつ配列の要素になっています。
{ "name": "kaminashi", "displayName": "kaminashi", "exported": 1616557445, "pages": [ { "title": "2020/03/02 議事録", "created": 1556528208, "updated": 1556528208, "id": "XXXXXXXX", "lines": [ "2020/03/02 議事録", "", "", "メモ", " カミナシ", "[** 見出し]", "[リンク https://example.com]" } }
Pythonを使ってMarkdownになおす時はJSONファイルを辞書に直して1行ずつ正規表現で直しました。 変換する関数は以下になります。
def scrapbox_to_markdown(text: str) -> str: text = text.replace("\u3000", "\t") replace_patterns = [ {'name': 'heading1', 'scrapbox': r'\t*\[#*\*{3,}#* (.+)]', 'markdown': r'# \1'}, {'name': 'heading2', 'scrapbox': r'\t*\[#*\*{2}#* (.+)]', 'markdown': r'## \1'}, {'name': 'heading3', 'scrapbox': r'\t*\[#*\*{1}#* (.+)]', 'markdown': r'### \1'}, {'name': 'link', 'scrapbox': r'\[([^ ]*)[\t| ](http.+)\]', 'markdown': r'[\1](\2)'}, {'name': 'list', 'scrapbox': r'^\t(\t*)(.*)', 'markdown': r'\1- \2'}, {'name': 'italic', 'scrapbox': r'\[\/ ([^\]]+)]', 'markdown': r'_\1_'}, {'name': 'strike', 'scrapbox': r'\[- ([^\]]+)]', 'markdown': r'~~\1~~'} ] for replace_pattern in replace_patterns: text = re.sub( replace_pattern['scrapbox'], replace_pattern['markdown'], text ) return text
見出しはこれから説明するライブラリの都合上3つまでにしています。 リストの部分は出力によってタブだったり空白になっているので一括でタブに変換しています。
Notionライブラリ
Notionは公式APIがありますが、まだプライベートβ版のため内部APIを使用することにしました。 Notionへのアップロードには2つのライブラリを使用しています。
notion-py
はページを作成したり、ブロックを追加したりできます。
md2notion
はMarkdownファイルや文字列からNotionのブロックを作成できます。
※注意
2021年3月25日現在は notion-py
を使ってAPIを実行すると以下のエラーがでます。
[ERROR] HTTPError: Invalid input.
すでにGItHubのIssueにも起票されていますが、Limitのパラメータを変更することで解消できます。
notion/client.py
data = { "query": search, "parentId": parent_id, - "limit": 10000, + "limit": 100,
ページの作成
事前にデータベースを作成して、データベース内のページとして作成していきます。
from notion.client import NotionClient notion_client = NotionClient(token_v2=NOTION_TOKEN) collection_view = notion_client.get_collection_view(COLLECTION_VIEW_URL) new_page = collection_view.collection.add_row() new_page.title = 'Scrapboxのタイトル'
COLLECTION_VIEW_URL
はデータベースのURLです。URLはページIDとビューIDをもっています。
https://www.notion.so/{pageId}?v={viewId}
Collection Viewからページを作成し、タイトルやプロパティを変更できます。
Markdownをページにコピー
Markdownのテキストに変換した文字列をNotionのページにブロック形式で保存します。
from md2notion.upload import convert, uploadBlock markdown_text = parse_scrapbox_lines(scrapbox_page_lines) notion_block = convert(markdown_text) for blockDescriptor in notion_block: uploadBlock(blockDescriptor, new_page, '')
ブロックの生成は md2notion
の convert
関数を使います。
convert
関数はブロックの配列を生成するので、一個づつuploadBlock
関数を使ってアップロードします。
アップロードするときに先程生成したページを引数に渡すと、ページの内部にブロックがコピーされていきます。
並列実行
これらのブロック生成処理はある程度時間がかかるため、一つの関数にまとめて並列処理を行いました。
from concurrent.futures import ProcessPoolExecutor, wait with ProcessPoolExecutor(max_workers=10) as executor: futures = [executor.submit(upload_markdown_block, scrapbox_page) for scrapbox_page in scrapbox_pages] done, not_done = wait(futures) [future.result() for future in futures] def upload_markdown_block(scrapbox_page): # 1ページ移行する関数
感想
ScrapboxからNotionに移行するスクリプトをPythonで作成しましたがScrapboxの記法が独特で変換するのが大変でした。
特に今回実装した正規表現ではGyazoの画像や内部リンクを移行できていないです。 内部リンクは一度全て移行した後にリンク先のページを検索して更新する必要があります。