👩‍💻

#43 Kibelaの記事をBacklogのWikiへ移行する〜移行のための調査と準備

2024/09/04に公開

はじめに

自社でこれまで作成してきたKibelaの記事をBacklogのWikiページに移行させたい、という要請を受けたため、まとめて記事を移行させるスクリプトをpythonで作成してみました。
こちらではその備忘録として、移行までの調査内容や実際に作成したコードについて、3部に分けてまとめていきたいと思います。
記事の第1部にあたる今回は、実際にスクリプトを作成するまでの、移行のための調査内容を中心に取り上げます。
本題材ではBacklogやKibelaから公開されているAPIをいくつか活用させていただいているので、初学者の方のご参考になれば幸いです。


なお、今回の題材はこれまでに投稿してきたブログの内容も関わっています。以前取り上げた箇所について、本記事では深掘りしないので、気になった方はそちらも合わせて一読いただけると良いかと思います。

本記事に関連するブログ記事

KibelaからBacklogへ移行する際の条件

移行するにあたり、以下の項目を満たすことを条件としました。

  • Kibelaでカテゴリごとにまとめられている記事について、Backlogへ移行後も何らかの形でグループとしてまとめられること
  • Kibelaの記事タイトルが、BacklogのWikiタイトルと同じである、またはタイトルから同じ記事が連想できること
  • Kibelaの記事中に挿入されている画像について、Backlogへ移行後も同様に挿入された状態で閲覧できること

Backlog-Wikiのツリー表示を利用する

Wikiページで手軽に記事をグループ分けをする方法として、Wikiのツリー表示があります。
Wikiタイトルに「/(スラッシュ)」を入れることで、Wiki一覧で階層化した表示が可能となります。
今回は、Kibelaでのカテゴリを第一階層、Kibelaの記事タイトルを第二階層としたWikiタイトルとすることで、移行後のグループ化を実現させることにしました。
Wikiタイトル = 「Kibela上のカテゴリ名 + "/" + Kibelaの記事タイトル」
ただし、移行対象のKibelaの記事タイトル中に既に「/」が使用されているものがいくつか発見されました。
意図せぬ階層化が発生してしまうため、kibelaの記事タイトル中の「/(半角スラッシュ)」を「/(全角スラッシュ)」に置換する必要がありそうです。
対応①:引用するKibela記事タイトルの「/(半角スラッシュ)」を「/(全角スラッシュ)」に置換

Kibelaの記事をエクスポートする

実際にkibelaの記事をエクスポートして、どのような形でエクスポートされるのか確認してみました。
オーナー権限を持つユーザーであれば、記事のエクポートが可能です。
参考としてリンクに示したページでは、エクスポートの仕様についての説明がありますが、中でも今回の試みにあたりネックになると思われる部分が以下の3点です。

  • Zipファイル内にはattachmentsという記事の添付ファイルが含まれるフォルダ、notesという記事が含まれるフォルダがあります。
  • notes以下にあるフォルダはKibela上で作成されたフォルダ名に相当します。
  • フォルダ名やファイル名として利用できない文字(?等)はアンダーバーに変更されます。
    引用:記事のエクスポート機能

これらに加え、エクスポートしたnotesフォルダ(Kibelaの記事が含まれるフォルダ)を確認すると、次のことがわかりました。

  • ファイル名の先頭には「[半角数字] + "-" 」が自動的に付与されている(例:501-猫に小判.mdなど)
  • ファイル名からでは、記事カテゴリの判別が困難
  • ファイルの中身の上部に、Kibela記事情報(記事IDや作成者、更新日など)が記載されている

以上のことから、エクスポートしたnotesフォルダをそのままWikiタイトルやWiki本文に利用するのは不適、という結論に至りました。

KibelaのWeb APIからjsonファイルを作る

Kibelaのアカウントを持っている方はKibela Web APIを実際にコンソール画面で試してみることができます。
参考:KibelaのWeb APIについて


Kibela Web APIはGraphQLとして提供されているため、コンソール画面からクエリを実行することで必要なデータを取得することが可能となっています。
そこで、今回は以下のクエリをコンソール画面から実行し、移行に必要なカテゴリ名、記事タイトル、記事内容をjson形式で取得することにしました。

query {
  currentUser {
    account
    realName
  }

  budget { cost }
    group (id: "R3JvdXAvMQ") {
    name,
    folders(first: 200) {
      nodes {
        id
        name,
        notes (first: 200) {
          totalCount,
          nodes {
            id
            title
            content
          }
                    }
      }
    }
  }
}

取得した内容をexport.jsonとしてjsonファイルで保存します。
これでWikiタイトルとWiki本文の引用元が用意できました。
参考までに、取得したjson例です。[nodes][id][name]がWikiタイトル(第一階層部分)、[title]がWikiタイトル(第二階層部分)、[content]がWiki本文に引用する項目です。

{
  "data": {
    "budget": {
      "cost": "xxxx"
    },
    "group": {
      "name": "NXTED",
      "folder": {
        "nodes": [
          {
            "id": 1,
            "name": "動物のことわざ",
            "notes": {
              "totalCount": 2,
              "nodes": [
                {
                  "id": "[ID]",
                  "title": "猫に小判 / 豚に真珠",
                  "content": "## はじめに \n動物のことわざです。  \n以下の画像を参照ください。<img title='スクリーンショット 2022-10-17 123250.png' alt='スクリーンショット 2022-10-17 123250' src='/attachments/d7e3b97b-90dd-4ad3-ab667-11ky7A3r' width="330" data-meta='{"width":330,"height":254}'>,"height":254}'>,"height":254}'>牛に経文も類諺"  
                },
                {
                  "id": "[ID]",
                  "title": "クラゲの風向かい",
                  "content": "抵抗しても無駄なこと"
                }
              ]
            }
          },
          {
            "id": 2,
            "name": "植物のことわざ",
            "notes": {
              "totalCount": 1,
              "nodes": [
                {
                  "id": "[ID]",
                  "title": "3/30 飢えに臨みて苗を植える",
                  "content": "手遅れです。\n<img title='スクリーンショット 2022-10-17 123250.png' alt='スクリーンショット 2022-10-17 123250' src='/attachments/d7e3b97b-90dd-4ad3-a846-14f8681ayy6' width="330" data-meta='{"width":330,"height":254}'>"
                }
              ]
            }
          }
        ]
      }
    }
  }
}

画像を挿入する<img>タグを置換

<img>タグ中のsrc='[ファイルパス]'にて、本文中に挿入させる画像を指定することができますが、実はエクスポートしたnotesフォルダの記事とexport.jsonのcontentでは、<img>タグのsrcの内容が異なります。
そのため、export.json内のcontentをそのままBacklogのWiki本文として引用しても、任意の画像を本文中に反映させることができません。

# エクスポートしたnotes記事の内容にある<img>タグ例
<img title='スクリーンショット 2022-10-17 123250.png' alt='スクリーンショット 2022-10-17 123250' src='../attachments/3617.png' width="330" data-meta='{"width":330,"height":254}'>  
  
# export.jsonのcontentにある<img>タグ例  
<img title='スクリーンショット 2022-10-17 123250.png' alt='スクリーンショット 2022-10-17 123250' src='/attachments/d7e3b97b-90dd-4ad3-a846-14f8681ae1d2' width="330" data-meta='{"width":330,"height":254}'> 
  

画像として参照したいのは、Kibelaからエクスポートしたattachmentsという記事の添付ファイルが含まれるフォルダです。
このフォルダにあるファイルは.png形式で、同じくKibelaからエクスポートしたnotesフォルダの<img>タグのsrcと一致します。
つまり、attachmentsフォルダには上の例にある「3617.png」というファイル名で対象の画像が保存されていることになります。
notesフォルダの<img>タグのsrcから対象のファイル名を取得する必要がありそうです。
対応②:notesフォルダの<img>タグのsrcから対象のファイル名を取得


更にもうひとつ、BacklogではMarkdown記法を利用して画像を表示させる際に使用するルールがあるため、<img>タグを![image]([ファイルパス])に置き換えなければなりません。
対応③:<img>タグを![image]([ファイルパス])に置き換える

対応②の実装方法を確認

対応①③については過去ブログで紹介した内容と方法としてはほぼ変わりありません。そのため、今回は解説を割愛させていただきます。
今後の第2、3部で全体のコードについて解説する機会があるので、本題材にあたりどのように活用しているのか、改めてご確認いただければと思います。

項目 関連ブログ記事
対応① ネストのあるjsonから文字列を抽出して置換したい
対応③ 正規表現でマッチした文字列を順に置換したい


ここでは「対応②:notesフォルダの<img>タグのsrcから対象のファイル名を取得」について、簡単にではありますが解説させていただきます。

notesフォルダ内を検索するため、export.jsonから取得した[title]を変換

前提として、「対応①:引用するKibela記事タイトルの「/(半角スラッシュ)」を「/(全角スラッシュ)」に置換」の処理過程にて、変数titleにexport.jsonから取得した[title]が格納されているとします。
title = export.jsonから取得した[title]


notesフォルダ以下では、「フォルダ名やファイル名として利用できない文字(?等)はアンダーバーに変更」されるため、re.subメソッドで対象の記号をアンダーバーに置換します。
re.subメソッドは文字列を正規表現で置換する場合に用いるメソッドでした。

reTitle = re.sub(r"[ .:*>/]", "_", title)  

notesフォルダを変数reTitleの値で検索し、一致するファイル名を取得します。

BASE_FOLDER = Path("notes")  
for p in BASE_FOLDER.glob("*" + reTitle + ".md"):  

条件を満たすパスの文字列を要素とするリストを返す.glob()関数では「*」「?」「[]」のワイルドカードの使用が可能です。

ワイルドカード 説明
* 0字以上の任意の文字列
? 任意の1字
[] 特定の1字 (例:[abc] = a,b,cのいずれか1字)

取得したファイル名からファイルの中身を行ごとに分割したリストとして取得し、.joinメソッドでひとつの文字列として連結します。

# 空のリストと文字列の変数をそれぞれ用意する  
lists = []  
string = ''  
  
BASE_FOLDER = Path("notes")  
for p in BASE_FOLDER.glob("*" + reTitle + ".md"):  
    with open(r'./{0}' .format(p), encoding='utf-8') as f: # 読み込みモードでファイルを開く  
    lists = f.readlines() # 行ごとに分割したリストとして取得  
string = ''.join(lists) # .joinメソッドでひとつの文字列として連結  

取得したファイル名「p」をopen()メソッドで開くため、.format()メソッドを利用しています。
これは以下のように説明されています。

format()メソッドの使い方

以下のように文字列の中に、波かっこ{}で囲まれた「置換フィールド」を埋め込み、format(
)メソッドの引数で{}の部分を置換します。

>>> line = "{0}さんの身長は{1}cm、体重は{2}kgです。".format("山田", 190, 105.3)  
>>> print(line) 山田さんの身長は190cm、体重は105.3kgです。  

引用:https://gammasoft.jp/blog/python-string-format/

これで変数stringにreTitleをファイル名に含むファイルの内容が格納されました。
よって、「対応③:<img>タグを![image]([ファイルパス])に置き換える」で置換に使う画像ファイル名をここから取得することができますね。

おわりに

今回は、Kibelaの記事をBacklogのWikiへ移行する記事の第1部として、移行のための調査と準備、そして.globメソッドによるファイルの検索について取り上げました。
以前ブログで取り上げた内容についての詳細は割愛させていただいたので、わかりにくい部分もあったかと思います。


次回はBacklogが公開しているAPIを中心にまとめていきたいと思います。
長くなりましたが、最後まで閲覧いただきありがとうございます。


参考:

Discussion