Scrapbox記法をPythonでMarkdownに変換する方法

2020/06/06

【Python】 作ってみた

t f B! P L

ScrapboxのバックアップをMarkdownで残したい

Scrapbox

オンライン上でサクサクとシンプルなメモがとれるツール、Scrapbox。


Scrapbox - A knowledge base built for infinite ideas

Edit seamlessly, together. Everything is set up so you can effortlessly explore ideas. You can edit pages in real time with a team or work asynchronously. Plus, loads of content types auto-embed or are supported like images, videos, maps, quick sketches, code, and math notation.

昨年に出会ってから、1年近くメモをためつづけ、気付けば1000ページを超えていました。



が、最近Notionを使いはじめ、「記事作成のためにはメモもMarkdwonで取っておいた方があとあと使いやすい」ということに気づき、Notionで日々のメモを取り始めました。


そうなると、今までのScrapboxでのメモもNotionにコピーしたくなります。
メモを検索するときも、2つのアプリで探すのは面倒なので、ひとつにまとめたいわけです。

Scrapboxのバックアップ


Scrapboxにはバックアップがあります。
JSON形式ですべてのページをエクスポートできます。


今回は、「JSON形式のScrapbox記法→Markdownへの変換」をPythonで書いてみた話です。

Pythonで変換

コード

早速コードの全体です。

1ページ用ごとに変換

関数部分

# タイトルからjsonの中身を検索
def search_from_title(title):
    for page in list_pages:
        if page["title"]==title:
            # print(page["lines"])
            return page["lines"]
        else:
            pass
# 変換のための関数
def image_(line):
    if "[https://gyazo.com/" in line:
        image = "![](https://i.gyazo.com/" + line.split("[https://gyazo.com/")[1].strip("]") + ".png)"
    else:
        image = line
    return image

def link_(line):
    if "https://youtu.be" in line or "https://www.youtube.com" in line:
        link_ = line
    elif "[http" in line:
        try:
            title = line.split(" ", 1)[1].strip("]")
            link = line.split(" ", 1)[0].strip("[")
            link_ = "[" + title + "](" + link + ")"
        except:
            # 例外のリンクはそのまま残す
            print(line)
            link_ = line
    else:
        link_ = line
    return link_

# 変換
def sb2md(page):
    list_md_ele = []
    for i in range(len(page)):
        list_ = []
        line = page[i]
        # スペース入りの行は飛ばす
        if len(line)!=0 and line[0]==" ":
            pass
        else:
            # 変換 lineを更新していく
            # 箇条書き
            line = line.replace('\t\t\t\t', '    - ')
            line = line.replace('\t\t\t', '  - ')
            line = line.replace('\t\t', '- ')
            # 見だし
            line = line.replace('\t', '## ')
            # 引用
            line = line.replace('>', '> ')
            # 画像
            line = image_(line)
            # リンク
            line = link_(line)
            list_.append(line)
            # コード
            if "code:" in line:
                list_.remove(line)
                line = '```' + line.split(":")[1]
                list_.append(line)
                j = 1
                while True:
                    code_ = page[i+j]
                    # コードの中身がスペースのみのとき
                    if code_==" ":
                        list_.append('```')
                        break 
                    elif len(code_)!=0 and code_[0]==" ":
                        code = code_.lstrip(" ")
                        list_.append(code)
                        j += 1
                    else:
                        list_.append('```')
                        break
            # tableはそのまま追加する
            if "table:" in line:
                list_.remove(line)
                j = 1
                while True:
                    table_ = page[i+j]
                    if len(table_)!=0 and table_[0]==" ":
                        code = table_.lstrip(" ")
                        list_.append(code)
                        j += 1
                    else:
                        break
        # list_の中身を追加する
        if len(list_)==1:
            list_md_ele.append(list_[0])
        else:
            for ele in list_:
                list_md_ele.append(ele)  
    # ページのタイトル
    title = list_md_ele[0]
    # 日付の下に1行空きを入れておく
    list_md_ele = list_md_ele[1:]
    list_md_ele.insert(1, '')

    return title, list_md_ele

# ファイルへ書き出し
def md2file(title , list_):
    title = title.replace("/", "")
    with open('markdown/{}.md'.format(title), 'w') as f:
        for d in list_:
            f.write("%s\n" % d)
    # print(title)

実行部分

import json

json_open = open('exportしたJSONファイル', 'r')
json_load = json.load(json_open)
list_pages = json_load["pages"]

page = search_from_title("ページのタイトル")
title , list_ = sb2md(page)
md2file(title , list_)

ページのタイトルをinputに、.md形式のファイルの出力まで、行えます。

JSONバックアップからすべてmdに変換

今回やりたかったのはこちら。

import time
import json

json_open = open('json/inaeva7691_20200602.json', 'r')
json_load = json.load(json_open)
list_pages = json_load["pages"]

# 開始時間
start = time.time()

for page in list_pages:
    print(page["title"])
    title , list_ = sb2md(page["lines"])
    md2file(title , list_)

# 終了時間
elapsed_time = time.time() - start
print ("elapsed_time:{0}".format(elapsed_time) + "[sec]")

参考として、変換にかかる時間を測りました。
1043ページで


elapsed_time:0.7751188278198242[sec]

でした。
実用上まったく問題ない早さだと思います。

対応している変更箇所

今回のコードで、対応している変換箇所についてです。
雑な実装なので、きちんとすべての記法には対応していません。tableなどは崩れてしまいます。

見た目よりもテキストの保存データとして、ある程度整形できたらいいや、の精神で作ったのでご了承ください🙏
(もっときれいに変換できるよ!という方はぜひ教えてもらえたらうれしいです)


  • リストの一つ目を「見だし2」に
  • リストの2つ目以降をリストに
  • 画像のリンク(gyazo)
  • リンク
  • 引用
  • コードブロック
  • テーブル(一部)
  • このあたりの変換ができます。
    個人的な仕様の変換があるので、適宜コードをいじって変換内容を変えてみて下さい。

    変換後の見た目の比較

    変換前

    変換後

    同じ内容のページで比較しています。

    完全にMarkdownにきれいに変換できるわけではありませんが、自分が満足するくらいのきれいさでOKです。

    その他、使えるかもしれないコード

    ファイルを100ページごとにフォルダに分ける

    バックアップするページが多いと、importの際にも管理が大変です。
    例えば、100ページごとにフォルダに分ける場合は、


    import os
    import glob
    import shutil
    
    # #markdownフォルダに移動
    os.chdir("markdown/")
    list_md = glob.glob("*.md")
    # 100個ずつのフォルダに分割
    n = 100
    list_md_100 = [list_md[idx:idx + n] for idx in range(0,len(list_md), n)]
    # 移動
    for i in range(len(list_md_100)):
        i_ = str(i)
        os.mkdir(i_)
        for md in list_md_100[i]:
            shutil.move(md, "{}/".format(i_))
    

    このような感じでPythonで分割できたりできます。

    他のツールで変換する場合

    別にPythonで1から作らなくても、すでにある他のツールで変換できたりします。

    ですが、大体が「1ページ」ごとの変換だったり、思ったように変換できなかったり…と、
    バックアップ全体を一括でmarkdownに変換」できるものがなかったので、自分で作ってみた次第です。

    いくつかツールを調べてみたので、紹介しておきます。

    オンライン上で変換

    どちらも、scrapbox記法で書いたテキストを貼り付けます。

    Online ScrapBox Converter

    Online ScrapBox Converter


    • scrapbox→markdown
    • markdown→scrapbox

    の両方ができました。

    scrapbox-markdown

    https://gyo.gitlab.io/scrapbox-markdown/
    React App

    Scrapboxのページから

    UserScriptなりをつかって、ページ内部から変換できます。

    sb2md.sbext.js

    Scrapbox -> Markdown (Scrapbox extension)
    Scrapboxの記事をMarkdownとしてコピーできるScrapbox拡張を書いた

    userscriptに追加すると、ページの詳細部分から変換できます。

    Scrapbox2Markdown

    Scrapboxページの文章をMarkdownに変換するBookmarkletを書いた - #daiizメモ

    ScrapboxコンテンツをMarkdownに変換するBookmarklet - daiiz

    ScrapboxコンテンツをMarkdownに変換するBookmarklet - Scrapboxカスタマイズコレクション


    ブックマークレットとして登録することで使えます。


    ブックマークからリンクの部分を、

    javascript:(function(){var m=function(b){b=b.replace(/&/g,"&amp;");b=b.replace(/</g,"&lt;");b=b.replace(/>/g,"&gt;");b=b.replace(/"/g,"&quot;");return b=b.replace(/'/g,"&#39;")},n=function(b){b=void 0===b?0:b;for(var a="",g=1;g<b;g++)a+="    ";return a},p=function(b){b=void 0===b?"":b;var a=document.createElement("div");a.innerHTML=b;b=a.querySelectorAll("strong");for(var g=0;g<b.length;g++){var d=b[g],e=+d.className.split("level-")[1],c=d.innerHTML;e=void 0===e?1:e;e=6-e;for(var f="",h=0;h<e;h++)f+="#";d.innerHTML=
    f+" "+c}return a.innerHTML},q=function(b){b=void 0===b?"":b;var a=document.createElement("div");a.innerHTML=b;b=a.querySelectorAll("a");for(var g=0;g<b.length;g++){var d=b[g],e=d.innerText.trim(),c=d.href;e="["+e+"]("+c+")";var f=d.querySelector("img");null!==f&&(e="[![Image]("+f.src+")]("+c+")");d.innerText=e}return a.innerText},f=document.querySelector(".lines"),r=f.querySelector(".line-title .text").innerText;f=f.querySelectorAll(".line");pageTexts=[];for(var l=1;l<f.length;l++){for(var c=f[l].querySelector(".text").cloneNode(!0),
    h=c.querySelectorAll("span.empty-char-index"),a=0;a<h.length;a++){var k=h[a];k.innerText=""}h=c.querySelectorAll("span.backquote");for(a=0;a<h.length;a++)k=h[a],k.innerText="`";a=c.innerHTML.replace(/<span>/g,"");a=a.replace(/<span.+?>/g,"").replace(/<\/span>/g,"");a=a.replace(/<br.+?>/g,"");a=a.replace(/\n/gi,"").replace(/\t/gi,"").trim();a=p(a);a=q(a);c=c.querySelector(".indent-mark");null!==c&&(k=+c.style.width.split("em")[0]/1.5*2,a=n(k)+"- "+a);null===c&&0<a.length&&"#"!==a[0]&&(a+="<br>");pageTexts.push(a)}(function(b,
    a){b=void 0===b?"Title":b;a=void 0===a?[]:a;for(var c="# "+b+"\n",d=0;d<a.length;d++)c+="\n"+a[d];a=window.open();a.document.open();a.document.write("<title>"+b+"</title>");a.document.write("<pre>");a.document.write(m(c));a.document.write("</pre>");a.document.close()})(r,pageTexts)})();
    

    として登録したらOKです。


    実行すると、別ページとして開かれます。

    sb2md

    hitode909/sb2md

    コマンドとして変換できるらしいのですが、どうもインストールが上手くいきませんでした。

    どこで使う?

    今回のコードはどこで使えるのでしょうか?

    大きく2つあると思います。

  • markdwon形式で保存しておく
  • 別のツールにmarkdownとしてimportするとき

  • 私は、Notionにインポートするために使ってみました。
    移行の様子などは次の記事にしようと思います。


    記事の感想をリアクションでお願いします!

    QooQ