ChatGPT で Python プログラミング ケーススタディ： markdown を json に変換

ChatGPT のおかげで、ちょっとしたPythonプログラミングならチャチャっと作れるようになってしまいましたね。今回はそのケーススタディとして、 __「markdown を json に変換」__ するためのPythonコードを書いてみようと思います。この記事は __「データを得る」__ と __「ChatGPT で Python プログラミング」__ の２部構成になっています。どちらも、実際にChatGPTで行ったやりとりを載せています。

# データを得る



---

### **user**:

漫画「ジョジョの奇妙の冒険」に登場するスタンドを階層的に分類してください。

---

### **assistant**:

`ジョジョの奇妙の冒険`に登場する`スタンド`を階層的に分類するのは非常に大きな作業となりますが、一部を簡単に分類してみます。以下の分類は`スタンド`のユニークな特性や能力に基づいています。

> 1. 物理攻撃型スタンド
>     - 『Star Platinum』（空条承太郎）
>     - 『Magician's Red』（アヴドゥル）
>     - 『Silver Chariot』（ポルナレフ）
> 
> 2. 遠隔操作型スタンド
>     - 『Hermit Purple』（ジョセフ・ジョースター）
>     - 『Hierophant Green』（花京院典明）
> 
> 3. 補助・制御型スタンド
>     - 『Heaven's Door』（空条徐倫）
> 
> 4. 生物操作型スタンド
>     - 『Gold Experience』（空条ジョルノ）
> 
> 5. 罠設置型スタンド
>     - 『Killer Queen』（吉良吉影）
> 
> 6. 領域型スタンド
>     - 『The World』（DIO）

それぞれのカテゴリは戦闘スタイル、能力、影響範囲などを考慮に入れて分類されています。しかし、`スタンド`の能力は非常に多様で、多くのスタンドは複数のカテゴリにまたがる可能性があります。したがって、この分類はあくまで一例であり、全てをカバーしているわけではありません。

また、対応するキャラクター名も併記しています。


---

### **user**:

得られた木構造をMarkdown形式で記述してください。

---

### **assistant**:

もちろんです。以下に該当するMarkdown形式の木構造を記述します：

```markdown
- スタンド
  - 物理攻撃型スタンド
    - Star Platinum (空条承太郎)
    - Magician's Red (アヴドゥル)
    - Silver Chariot (ポルナレフ)
  - 遠隔操作型スタンド
    - Hermit Purple (ジョセフ・ジョースター)
    - Hierophant Green (花京院典明)
  - 補助・制御型スタンド
    - Heaven's Door (空条徐倫)
  - 生物操作型スタンド
    - Gold Experience (空条ジョルノ)
  - 罠設置型スタンド
    - Killer Queen (吉良吉影)
  - 領域型スタンド
    - The World (DIO)
```

この形式は、インデントを使用して階層構造を表現しています。`-`はリストのアイテムを表し、スペースまたはタブによるインデントが子アイテムを表します。これにより、スタンドのカテゴリに応じた分類が視覚的に表現されます。

---

### **user**:

得られた木構造をJSON形式で記述してください。

---

### **assistant**:

了解しました。以下にJSON形式での木構造を記述します：

```json
{
  "スタンド": {
    "物理攻撃型スタンド": {
      "Star Platinum": "空条承太郎",
      "Magician's Red": "アヴドゥル",
      "Silver Chariot": "ポルナレフ"
    },
    "遠隔操作型スタンド": {
      "Hermit Purple": "ジョセフ·ジョースター",
      "Hierophant Green": "花京院典明"
    },
    "補助・制御型スタンド": {
      "Heaven's Door": "空条徐倫"
    },
    "生物操作型スタンド": {
      "Gold Experience": "空条ジョルノ"
    },
    "罠設置型スタンド": {
      "Killer Queen": "吉良吉影"
    },
    "領域型スタンド": {
      "The World": "DIO"
    }
  }
}
```

JSON形式では、`{}`はオブジェクトを表し、`:`はそのプロパティと値を表します。この形式により、スタンドの名前とその所有者の名前を紐付けることができます。


---

# ChatGPT で Python プログラミング




---

### **user**:

markdown 形式で表された木構造データを json 形式に変換する pythonコードを教えてください。

---

### **assistant**:

Pythonのライブラリである`markdown`と`json`と`beautifulsoup` を使用することで、Markdown形式で書かれたデータからHTMLを生成し、そのHTMLを解析することで木構造を抽出し、最後にその木構造をJSON形式に変換することができます。

以下にそのようなPythonスクリプトのコードを示します。

```python
import json
import markdown
from bs4 import BeautifulSoup

# MarkdownをHTMLに変換する
def markdown_to_html(md):
    html = markdown.markdown(md)
    return html

# HTMLを木構造のデータに変換する
def html_to_tree(html):
    soup = BeautifulSoup(html, 'html.parser')
    return soup

# 木構造のデータをJSONに変換する
def tree_to_json(tree):
    return json.dumps(str(tree), indent=4)

# Markdownデータを取得する
md_data = """
# My Document
## Section
### Subsection
- item 1
- item 2
"""
# MarkdownをHTMLに変換する
html_data = markdown_to_html(md_data)

# HTMLを木構造のデータに変換する
tree_data = html_to_tree(html_data)

# 木構造のデータをJSONに変換する
json_data = tree_to_json(tree_data)

# JSONを出力する
print(json_data)
```

このスクリプトは、MarkdownからHTMLを生成し、そのHTMLを解析してBeautifulSoupの木構造（タグのネスト構造）を生成し、最後にそのネスト構造をJSON形式に変換します。ただし、このJSONはHTMLのネストしたタグ構造を表しますが、Markdownの特定の要素（例えばヘッダーやリストアイテム）への直接的なマッピングではありません。これは、Markdownが直接的には木構造を持たないためです。そのため、より高度なMarkdown解析が必要な場合は、専用のMarkdown解析ライブラリを検討することをお勧めします。

---

### **user**:

ありがとうございます。いただいたコードを変更して、入力ファイル名と出力ファイル名を指定すれば、markdown形式のファイルを入力して、json形式ファイルを出力するようにしてください。

---

### **assistant**:

指定した入力ファイルからMarkdownデータを読み取り、それを解析してJSONデータを生成し、指定した出力ファイルにそのJSONデータを書き込むスクリプトを以下に示します。

```python
import json
import markdown
from bs4 import BeautifulSoup

# ファイルからMarkdownを読み込む
def read_markdown_file(file_path):
    with open(file_path, 'r') as file:
        return file.read()

# ファイルにJSONを書き込む
def write_json_file(file_path, data):
    with open(file_path, 'w') as file:
        file.write(data)
        
# MarkdownをHTMLに変換する
def markdown_to_html(md):
    return markdown.markdown(md)

# HTMLを木構造のデータに変換する
def html_to_tree(html):
    return BeautifulSoup(html, 'html.parser')

# 木構造のデータをJSONに変換する
def tree_to_json(tree):
    return json.dumps(str(tree), indent=4)

# 入力ファイル名と出力ファイル名を指定する
input_file = 'path_to_your_input.md'
output_file = 'path_to_your_output.json'

# ファイルからMarkdownを読み込む
md_data = read_markdown_file(input_file)

# MarkdownをHTMLに変換する
html_data = markdown_to_html(md_data)

# HTMLを木構造のデータに変換する
tree_data = html_to_tree(html_data)

# 木構造のデータをJSONに変換する
json_data = tree_to_json(tree_data)

# ファイルにJSONを書き込む
write_json_file(output_file, json_data)
```

このスクリプトを使用すれば、Markdownファイルを読み込んでその内容をJSONファイルに書き出すことが可能です。ただし、上記で示したように、このスクリプトはHTMLのネストしたタグ構造をJSONに変換しますが、Markdownの特定の要素（例えばヘッダーやリストアイテム）への直接的なマッピングではありません。また、上記のスクリプトは、入力ファイルと出力ファイルが同じディレクトリに存在することを前提としています。違うディレクトリに存在する場合は、適切なファイルパスを指定してください。

---

### **user**:

ありがとうございます。コードを少しだけ変更して、入力ファイルと出力ファイルをコマンドラインで指定できるようにしてください。

---

### **assistant**:

Pythonの組み込みモジュールである `argparse` を使用して、コマンドラインから入力ファイルと出力ファイルを指定できるように改修します。

以下にそのようなPythonスクリプトのコードを示します。

```python
import argparse
import json
import markdown
from bs4 import BeautifulSoup

# ファイルからMarkdownを読み込む
def read_markdown_file(file_path):
    with open(file_path, 'r') as file:
        return file.read()

# ファイルにJSONを書き込む
def write_json_file(file_path, data):
    with open(file_path, 'w') as file:
        file.write(data)
        
# MarkdownをHTMLに変換する
def markdown_to_html(md):
    return markdown.markdown(md)

# HTMLを木構造のデータに変換する
def html_to_tree(html):
    return BeautifulSoup(html, 'html.parser')

# 木構造のデータをJSONに変換する
def tree_to_json(tree):
    return json.dumps(str(tree), indent=4)

def main():
    # コマンドライン引数を解析する
    parser = argparse.ArgumentParser(description='Convert markdown to JSON.')
    parser.add_argument('input_file', type=str, help='Input markdown file path.')
    parser.add_argument('output_file', type=str, help='Output JSON file path.')
    args = parser.parse_args()

    # ファイルからMarkdownを読み込む
    md_data = read_markdown_file(args.input_file)

    # MarkdownをHTMLに変換する
    html_data = markdown_to_html(md_data)

    # HTMLを木構造のデータに変換する
    tree_data = html_to_tree(html_data)

    # 木構造のデータをJSONに変換する
    json_data = tree_to_json(tree_data)

    # ファイルにJSONを書き込む
    write_json_file(args.output_file, json_data)

if __name__ == "__main__":
    main()
```

エンドユーザーは、コマンドラインから以下のように実行します：

```bash
python your_script.py input.md output.json
```

ここで `your_script.py` はスクリプトの名称、`input.md` は入力となるMarkdownファイルのパス、`output.json` は出力となるJSONファイルのパスです。

---

### **user**:

上記のコードは日本語を正しく扱えないようですが日本語を扱えるように変更してください。

---

### **assistant**:

日本語を含むファイルを開く場合、エンコーディングを "utf-8" に設定することで、日本語を正しく読み書きできます。

また、JSONのダンプ時にもensure_asciiオプションをFalseに設定することで、JSON出力でもUnicodeエスケープせずに日本語をそのまま出力できます。

以下が修正版のコードです:


```python
import argparse
import json
import markdown
from bs4 import BeautifulSoup

# ファイルからMarkdownを読み込む
def read_markdown_file(file_path):
    with open(file_path, 'r', encoding='utf-8') as file:
        return file.read()

# ファイルにJSONを書き込む
def write_json_file(file_path, data):
    with open(file_path, 'w', encoding='utf-8') as file:
        file.write(data)
        
# MarkdownをHTMLに変換する
def markdown_to_html(md):
    return markdown.markdown(md)

# HTMLを木構造のデータに変換する
def html_to_tree(html):
    return BeautifulSoup(html, 'html.parser')

# 木構造のデータをJSONに変換する
def tree_to_json(tree):
    return json.dumps(str(tree), indent=4, ensure_ascii=False)

def main():
    # コマンドライン引数の解析
    parser = argparse.ArgumentParser(description='Convert markdown to JSON.')
    parser.add_argument('input_file', type=str, help='Input markdown file path.')
    parser.add_argument('output_file', type=str, help='Output JSON file path.')
    args = parser.parse_args()

    # ファイルからMarkdownを読み込む
    md_data = read_markdown_file(args.input_file)

    # MarkdownをHTMLに変換する
    html_data = markdown_to_html(md_data)

    # HTMLを木構造のデータに変換する
    tree_data = html_to_tree(html_data)

    # 木構造のデータをJSONに変換する
    json_data = tree_to_json(tree_data)

    # ファイルにJSONを書き込む
    write_json_file(args.output_file, json_data)

if __name__ == "__main__":
    main()
```

これで、日本語を含むMarkdownファイルを正しく扱い、その内容を日本語をそのまま出力するJSONファイルに変換できます。

---

### **user**:

上記のコードを少し変更して、HTMLではないjson形式ファイルを出力するようにしてください。

---

### **assistant**:

Pythonのライブラリである`json`を使用することに加え、HTMLタグを解析し深さ優先探索で木構造を作成するライブラリである`beautifulsoup4`とそのタグを扱うために`html5lib`も使用します。

以下にそのようなPythonスクリプトのコードを示します。

```python
import argparse
import json
import markdown
from bs4 import BeautifulSoup

# ファイルからMarkdownを読み込む
def read_markdown_file(file_path):
    with open(file_path, 'r', encoding='utf-8') as file:
        return file.read()

# ファイルにJSONを書き込む
def write_json_file(file_path, data):
    with open(file_path, 'w', encoding='utf-8') as file:
        json.dump(data, file, ensure_ascii=False, indent=4)
        
# MarkdownをHTMLに変換する
def markdown_to_html(md):
    return markdown.markdown(md)

# BeautifulSoupオブジェクトから再帰的に辞書を作成する
def tag_to_dict(tag):
    if len(tag.contents) == 0:
        return tag.string
    else:
        return {c.name: tag_to_dict(c) for c in tag.contents if c.name is not None}

def main():
    # コマンドライン引数の解析
    parser = argparse.ArgumentParser(description='Convert markdown to JSON.')
    parser.add_argument('input_file', type=str, help='Input markdown file path.')
    parser.add_argument('output_file', type=str, help='Output JSON file path.')
    args = parser.parse_args()

    # ファイルからMarkdownを読み込む
    md_data = read_markdown_file(args.input_file)

    # MarkdownをHTMLに変換する
    html_data = markdown_to_html(md_data)

    # BeautifulSoupオブジェクトを作成
    soup = BeautifulSoup(html_data, 'html.parser')

    # BeautifulSoupオブジェクトから辞書を作成
    tree = tag_to_dict(soup.body)

    # ファイルにJSONを書き込む
    write_json_file(args.output_file, tree)

if __name__ == "__main__":
    main()
```

このスクリプトは、MarkdownからHTMLを生成し、そのHTMLを解析してBeautifulSoupの木構造（タグのネスト構造）を生成し、最後にそのネスト構造をJSON形式のネストした辞書に変換します。

このスクリプトをランして確認すると、生成されるJSONデータは、HTMLの木構造を表すPythonの辞書形式になります。+

---

### **user**:

上記のコードを変更して、次のような形のjsonファイルを出力するようにしてください。{
  "スタンド": {
    "物理攻撃型スタンド": {
      "Star Platinum": "空条承太郎",
      "Magician's Red": "アヴドゥル",
      "Silver Chariot": "ポルナレフ"
    },
    "遠隔操作型スタンド": {
      "Hermit Purple": "ジョセフ·ジョースター",
      "Hierophant Green": "花京院典明"
    },
    "補助・制御型スタンド": {
      "Heaven's Door": "空条徐倫"
    },
    "生物操作型スタンド": {
      "Gold Experience": "空条ジョルノ"
    },
    "罠設置型スタンド": {
      "Killer Queen": "吉良吉影"
    },
    "領域型スタンド": {
      "The World": "DIO"
    }
  }
}


---

### **assistant**:

こちらの内容を整理するとMarkdownフォーマットは以下のようになります：

```
# スタンド  
## 物理攻撃型スタンド  
- Star Platinum: 空条承太郎  
- Magician's Red: アヴドゥル  
- Silver Chariot：ポルナレフ  
## 遠隔操作型スタンド  
- Hermit Purple: ジョセフ·ジョースター  
- Hierophant Green: 花京院典明  
## 補助・制御型スタンド  
- Heaven's Door: 空条徐倫  
## 生物操作型スタンド  
- Gold Experience: 空条ジョルノ  
## 罠設置型スタンド  
- Killer Queen: 吉良吉影  
## 領域型スタンド  
- The World: DIO  
```

この情報を用いてPythonのコードを以下のように修正します。

```python
import argparse
import json
import markdown
from bs4 import BeautifulSoup

def read_markdown_file(file_path):
    with open(file_path, 'r', encoding='utf-8') as file:
        return file.read()

def write_json_file(file_path, data):
    with open(file_path, 'w', encoding='utf-8') as file:
        json.dump(data, file, ensure_ascii=False, indent=4)

def markdown_to_json(md):
    html = markdown.markdown(md)
    soup = BeautifulSoup(html, 'html.parser')
    contents = soup.body.contents
    return contents_to_dict(contents)

def contents_to_dict(contents):
    result = {}
    key = None
    for content in contents:
        if content.name == 'h1':
            key = content.text.strip()
            result[key] = {}
        elif content.name == 'h2':
            key = content.text.strip()
            result[next(iter(result))][key] = {}
        elif content.name == 'ul':
            for li in content.find_all('li'):
                stand, user = li.text.split(':')
                result[next(iter(result))][next(reversed(result[next(iter(result))]))][stand.strip()] = user.strip()
    return result

def main():
    parser = argparse.ArgumentParser(description='Convert markdown to JSON.')
    parser.add_argument('input_file', type=str, help='Input markdown file path.')
    parser.add_argument('output_file', type=str, help='Output JSON file path.')
    args = parser.parse_args()

    md_data = read_markdown_file(args.input_file)
    json_data = markdown_to_json(md_data)
    write_json_file(args.output_file, json_data)

if __name__ == "__main__":
    main()
```

コマンドラインから以下のように実行します：

```bash
python your_script.py input.md output.json
```

このスクリプトは指定したMarkdown形式の内容を解析し、指定された形式に変換してJSONファイルへ書き出すことができます。出力されるJSONの構造は元のMarkdownの階層構造に対応させるため、Markdownの内容が大きく変わる場合、コードの修正が必要になります。

---

### **user**:

上記のコードを変更して、次のような形式のmarkdownを入力できるようにしてください。
- スタンド
  - 物理攻撃型スタンド
    - Star Platinum (空条承太郎)
    - Magician's Red (アヴドゥル)
    - Silver Chariot (ポルナレフ)
  - 遠隔操作型スタンド
    - Hermit Purple (ジョセフ・ジョースター)
    - Hierophant Green (花京院典明)
  - 補助・制御型スタンド
    - Heaven's Door (空条徐倫)
  - 生物操作型スタンド
    - Gold Experience (空条ジョルノ)
  - 罠設置型スタンド
    - Killer Queen (吉良吉影)
  - 領域型スタンド
    - The World (DIO)


---

### **assistant**:

リスト形式のマークダウンを解析するには、以下のように再帰的な演算を使用することができます。

以下のコードは、マークダウンのリスト形式を適切な形で解析し、指定された形式のJSONを生成します：

```python
import argparse
import json
import markdown
from bs4 import BeautifulSoup

def read_markdown_file(file_path):
    with open(file_path, 'r', encoding='utf-8') as file:
        return file.read()

def write_json_file(file_path, data):
    with open(file_path, 'w', encoding='utf-8') as file:
        json.dump(data, file, ensure_ascii=False, indent=4)

def markdown_to_json(md):
    html = markdown.markdown(md)
    soup = BeautifulSoup(html, 'html.parser')
    contents = soup.body.contents
    return contents_to_dict(contents)

def contents_to_dict(contents):
    result = {}
    key = None
    for content in contents:
        if content.name == 'ul':
            result = parse_ul(content)
    return result

def parse_ul(ul):
    result = {}
    for li in ul.find_all('li', recursive=False):
        key, sub_ul = parse_li(li)
        if sub_ul is not None:
            result[key] = parse_ul(sub_ul)
        else:
            stand, user = key.split('(', 1)
            user = user.rstrip(')')
            result[stand.strip()] = user.strip()
    return result

def parse_li(li):
    key = None
    sub_ul = None
    for child in li.children:
        if child.name is None:
            key = child
        elif child.name == 'ul':
            sub_ul = child
    return key, sub_ul

def main():
    parser = argparse.ArgumentParser(description='Convert markdown to JSON.')
    parser.add_argument('input_file', type=str, help='Input markdown file path.')
    parser.add_argument('output_file', type=str, help='Output JSON file path.')
    args = parser.parse_args()

    md_data = read_markdown_file(args.input_file)
    json_data = markdown_to_json(md_data)
    write_json_file(args.output_file, json_data)

if __name__ == "__main__":
    main()
```

このスクリプトでは、BeautifulSoupのパーサがマークダウンから生成したHTMLの`<ul>`要素（リスト）と`<li>`要素（リストアイテム）を解析します。リストアイテムがさらに下位のリストを含む場合、`<li>`要素の中に`<ul>`要素が存在します。そのため、スクリプトは再帰的な処理を使用して全てのリストとリストアイテムを適切に解析します。リストアイテムのテキストがスタンド名とスタンド使い者の名前に分割可能な場合（つまり、`（）`で囲まれた部分が存在する場合）、それらを解析して適切なキーと値のペアを作成します。

---

# 以上です

以上、ChatGPTとのやりとりだけでコードを生成しました。