スクレイピングした文字列を ChatGPT で簡単にフォーマットする

コンサート検索の Web アプリを個人開発しています。
アプリ紹介記事は[こちら](https://qiita.com/ogatango/items/851e11bda3cf87210c53)

https://qiita.com/ogatango/items/851e11bda3cf87210c53

アプリは[こちら](https://concertmap.page/)

https://concertmap.page/

# ChatGPT を用いたスクレイピングの改善

以前 [Natural Language を用いたスクレイピングの改善](https://qiita.com/ogatango/items/7a9e70e9c6551152fca3) という記事を書きました。
その記事では Natural Language を用いればスクレイピングした文字列からコンサートの情報をうまく抽出することができるという仮説に基づき、少ないデータ数で学習してみたらなんとなくうまくいきそうという結論を出しました。
そして後半でデータを大量に用意して結果を検証すると宣言していました。

しかし学習データや検証用のデータを用意する面倒くさかったり、仕事が忙しかったり、splatoon3 にハマっていたことが重なりしばらく放置していました。楽しみにしていただいた方はすみません。

そうこうしているうちに ChatGPT3.5 の API が公開され、世の中に様々な面白いプロダクトが誕生しているのを見て、なにかできないかと考えているときに思いつきました。Natural Language で学習しなくてもいい感じに文字列を抽出できるのでは？と。
ということでこの記事では ChatGPT を用いてスクレイピングした文字列をいい感じに抽出してフォーマットした話を書きます。

## 目的

スクレイピングで雑に取得した文字列から、演奏者名やプログラムを抽出するような手間のかかる実装を省力化すること。

### 経緯

演奏者名やプログラムはサイトによって表記ゆれが大きく、同一サイトですら表記揺れするため正規表現で正しいデータを取得することが困難です。
そこで自然言語処理の AI を使うことで正規表現を書かなくても良くなると考えました。

Natural Language は学習用データの用意の手間で挫折してしまいましたが、ChatGPT であれば学習させる必要がなく、より簡単に実装できると考えました。

## 前提

### 実装手順

実装する前に手順を考えます。特に AI を用いたプログラムはその精度によって AI を採用すべきかどうかを考慮する必要があります。以下の手順を考えました。

1. いくつかのデータを用いて、うまく答えを返してくれる指示を作る。
1. より多くのデータを用いて、答えがうまく出るパターンとでないパターンを識別する。
1. 答えがうまく出ないパターンでも対応できる指示をつくる。
1. うまくできないものは諦める。うまくできるものが多ければ ChatGPT を採用し、そうでなければ採用せず手動で正規表現などを駆使して文字列を抽出する。

### 使用データ

今回の記事では次の入力データでの結果を紹介します。scraping でざっくりとコンサート全体のテキストを持ってきたものになります。改行を削除しているため横長になってしまい恐縮です。

```python
エフゲニー・キーシン　ピアノ・リサイタル 2021年11月6日(土) 15:00 開演 [ピアノ]エフゲニー・キーシン J.S.バッハ（タウジヒ編）：トッカータとフーガ ニ短調 BWV.565  モーツァルト：アダージョ ロ短調 K.540  ベートーヴェン：ピアノ･ソナタ 第31番 変イ長調 op.110   ショパン：マズルカ第5番 変ロ長調 op.7-1  ショパン：マズルカ第14番 ト短調 op.24-1  ショパン：マズルカ第15番 ハ長調 op.24-2  ショパン：マズルカ第18番 ハ短調 op.30-1  ショパン：マズルカ第19番 ロ短調 op.30-2  ショパン：マズルカ第24番 ハ長調 op.33-3  ショパン：マズルカ第25番 ロ短調 op.33-4  ショパン：アンダンテ･スピアナートと華麗なる大ポロネーズ op.22   会場 ザ・シンフォニーホール   料金   														A 21,000円　B 17,000円　C 13,000円　D 9,000円                                                        ※未就学のお子さまのご入場はお断りさせていただきます。                                                       お問合わせ ＡＢＣチケットインフォメーション
```

### 使用プログラム

とりあえず検証ということで、雑にプログラムを作ります。
command 部分に ChatGPT への指示を、 text の部分に今回の使用データのテキストを入れ、結果を print します。

```python
response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=[
            {
                "role": "system",
                "content": command,
            },
            {
                "role": "user",
                "content": text,
            },
        ],
)

print(response["choices"][0]["message"]["content"])
```

### 抽出するテキストの種類

今回の使用データから次のようなテキストを抽出することを目指します。

- コンサートのプログラム、つまり作曲家名と曲目名の組み合わせ一覧
- 演奏者名と使用する楽器の一覧
- チケット料金の一覧
- コンサートのジャンル
- コンサートの演奏形態
- コンサートのタイトル

## テキストを抽出する指示

### コンサートプログラムの抽出

次のような CSV のフォーマットで抽出することを目指します。

```
作曲家名,曲名
J.S.バッハ（タウジヒ編）,トッカータとフーガ ニ短調 BWV.565
モーツァルト,アダージョ ロ短調 K.540
ベートーヴェン,ピアノ･ソナタ 第 31 番 変イ長調 op.110
ショパン,マズルカ第 5 番 変ロ長調 op.7-1
ショパン,マズルカ第 14 番 ト短調 op.24-1
ショパン,マズルカ第 15 番 ハ長調 op.24-2
ショパン,マズルカ第 18 番 ハ短調 op.30-1
ショパン,マズルカ第 19 番 ロ短調 op.30-2
ショパン,マズルカ第 24 番 ハ長調 op.33-3
ショパン,マズルカ第 25 番 ロ短調 op.33-4
ショパン,アンダンテ･スピアナートと華麗なる大ポロネーズ op.22
```

まずは素直に ChatGPT に指示を出してみましょう。

#### 指示 1

作曲者と曲名の組み合わせを返してください。

#### 結果

```
エフゲニー・キーシンのピアノ・リサイタルにおける曲目は以下の通りです。
- J.S.バッハ（タウジヒ編）：トッカータとフーガ ニ短調 BWV.565
- モーツァルト：アダージョ ロ短調 K.540
- ベートーヴェン：ピアノ･ソナタ 第31番 変イ長調 op.110
- ショパン：マズルカ第5番 変ロ長調 op.7-1
- ショパン：マズルカ第14番 ト短調 op.24-1
- ショパン：マズルカ第15番 ハ長調 op.24-2
- ショパン：マズルカ第18番 ハ短調 op.30-1
- ショパン：マズルカ第19番 ロ短調 op.30-2
- ショパン：マズルカ第24番 ハ長調 op.33-3
- ショパン：マズルカ第25番 ロ短調 op.33-4
- ショパン：アンダンテ･スピアナートと華麗なる大ポロネーズ op.22
```

さっそくいい感じの結果がでました！
正規表現で四苦八苦したり、Natural Language を用いてやるために学習用データを少ないとはいえ用意していた身からすると、こんな簡単に抽出できてしまうことに驚きがあります。あの手間は一体なんだったのか・・・。

すでに ChatGPT に感動していますが、今回は CSV 形式にしたいので指示を追加します。

#### 指示 2

作曲者と曲名の組み合わせを返してください。作曲家名と曲名の間は , で区切ってください。

#### 結果

```
J.S.バッハ（タウジヒ編）：トッカータとフーガ ニ短調 BWV.565, モーツァルト：アダージョ ロ短調 K.540, ベートーヴェン：ピアノ･ソナタ 第31番 変イ長調 op.110, ショパン：マズルカ第5番 変ロ長調 op.7-1, ショパン：マズルカ第14番 ト短調 op.24-1, ショパン：マズルカ第15番 ハ長調 op.24-2, ショパン：マズルカ第18番 ハ短調 op.30-1, ショパン：マズルカ第19番 ロ短調 op.30-2, ショパン：マズルカ第24番 ハ長調 op.33-3, ショパン：マズルカ第25番 ロ短調 op.33-4, ショパン：アンダンテ･スピアナートと華麗なる大ポロネーズ op.22。
```

作曲家名と曲名の組の間を , で区切ったようです。惜しい。いっそ CSV 形式にすることを明示してみます。ついでにヘッダーもつけてみましょう。

#### 指示 3

作曲者と曲名の組み合わせを返してください。CSV 形式でヘッダーを「作曲家名」、「曲名」として返してください。

#### 結果

```
作曲家名,曲名
J.S.バッハ（タウジヒ編）,トッカータとフーガ ニ短調 BWV.565
モーツァルト,アダージョ ロ短調 K.540
ベートーヴェン,ピアノ･ソナタ 第31番 変イ長調 op.110
ショパン,マズルカ第5番 変ロ長調 op.7-1
ショパン,マズルカ第14番 ト短調 op.24-1
ショパン,マズルカ第15番 ハ長調 op.24-2
ショパン,マズルカ第18番 ハ短調 op.30-1
ショパン,マズルカ第19番 ロ短調 op.30-2
ショパン,マズルカ第24番 ハ長調 op.33-3
ショパン,マズルカ第25番 ロ短調 op.33-4
ショパン,アンダンテ･スピアナートと華麗なる大ポロネーズ op.22
```

うまくいきました。
今回のタスクでうまい指示を出すには次の 2 段階で考えれば良さそうです。

1. 抽出方法を指示する
2. フォーマット方法を指示する

### 料金の抽出

次は料金を取得してみます。
次のような json 形式でテキストが返ってくることを期待しています。

```json
{
  "A": 21000,
  "B": 17000,
  "C": 13000,
  "D": 9000
}
```

それではとりあえず雑に指示を出してみましょう。

#### 指示 1

料金とその種別を JSON 形式で返してください。

#### 結果

````text
以下が、エフゲニー・キーシンのピアノ・リサイタルの料金と種別の情報です。

```json
{
    "A": 21000,
    "B": 17000,
    "C": 13000,
    "D": 9000
}
```

また、未就学のお子さまはご入場いただけません。お問合わせは、ＡＢＣチケットインフォメーションまでお願いします。
````

料金についての情報を正しく抽出できています。

「未就学のお子さまはご入場いただけません。」という点は用途次第では必要な情報かもしれません。ただ今回は不必要な情報なため、もう少しうまく指示を出してみます。

#### 指示 2

料金とその種別を JSON 形式で返してください。JSON 以外の情報は削除してください。

#### 結果

```json
{
  "price": {
    "A": 21000,
    "B": 17000,
    "C": 13000,
    "D": 9000
  }
}
```

料金が英訳されてしまいました。このままでも十分使えそうですが、 price というキーを無くして期待どおりにしたいところです。

#### 指示 3

料金を JSON 形式で返してください。JSON 以外の情報は削除してください。JSON の階層は 1 にしてください。

#### 結果

```json
{
  "料金": {
    "A": "21,000円",
    "B": "17,000円",
    "C": "13,000円",
    "D": "9,000円"
  }
}
```

階層がついている上に料金の単位までついてしまいました。

もしや階層は０とするのが正しいのではないかと思い次の指示にしてみました。

#### 指示 4

料金を JSON 形式で返してください。JSON 以外の情報は削除してください。JSON の階層は 0 にしてください。

#### 結果

```json
{
  "料金": {
    "A": "21,000円",
    "B": "17,000円",
    "C": "13,000円",
    "D": "9,000円"
  }
}
```

階層の数字は関係なかったようです。

#### 指示 5

料金を JSON 形式で返してください。JSON 以外の情報は削除してください。JSON はネストしないでください。

#### 結果

```json
{
  "料金": [
    {
      "席種": "A",
      "価格": 21000
    },
    {
      "席種": "B",
      "価格": 17000
    },
    {
      "席種": "C",
      "価格": 13000
    },
    {
      "席種": "D",
      "価格": 9000
    }
  ]
}
```

期待する結果からさらに遠ざかってしまいました。

#### 指示 6

期待する値を出す指示を思いつかないので次のようにしてみました。

料金を JSON 形式で返してください。JSON 以外の情報は削除してください。キーは「price」としてください。料金の value は単位をつけない数値型にしてください。

#### 結果

```json
{
  "price": {
    "A": 21000,
    "B": 17000,
    "C": 13000,
    "D": 9000
  }
}
```

1 階層余計ですがこれで固定されたフォーマットでデータを取得できそうです。

ここまでの例で抽出に関しては非常にうまく処理してくれていることがわかります。
一方でフォーマットに関しては指示にコツがいるようです。
JSON フォーマットを得るコツを知っている方やアイデアがある方はぜひコメントください。

にしてもこんな手間で簡単に取得できてしまうなんてすごいですね。料金をいい感じに取得するために文字列の削除や正規表現を駆使したオレオレプログラムを作っていた時間が虚しくなります。

### 演奏者名の取得

演奏者名はコンサートプログラムの取得とほとんど変わらないため省略します。

### 開催日時の取得

次のような指示で取得できました。

#### 指示

開催日を取得してください。フォーマットは 「0000-00-00」 としてください。また開演時間を取得してください。フォーマットは「00:00」としてください。その 2 つを半角スペースで結合した値を返してください。

#### 結果

```json
2021-11-06 15:00
```

フォーマットの指定はいくつか試しましたが上記のような指定でうまくいきそうです。

### コンサートのジャンルの取得

ジャンルは事前に用意した次のなかのいずれかに当てはまればその中から返します。

- classic
- game
- jazz
- anime
- movie
- pops
- ballet
- musical

次のようにするとうまくいきました。

#### 指示

コンサートのジャンルを返してください。次のジャンルから選択し、当てはまるものがなければ False を返してください。classic,game,jazz,anime,movie,pops,ballet,musical

#### 結果

```
classic
```

コンサートの演奏形態も同じようなやり方でできるため省略します。

## 今回の検証でわかったこと

### ChatGPT は抽出タスクが得意

今回の検証でスクレイピングで雑に取得した文字列から目当ての文字列を取得することに成功しています。
ChatGPT は文字列を与えられたときに、その中の特定の文字列を抽出するタスクが得意な印象を受けました。
今まで特定の文字列を抽出するときは正規表現などを駆使してやっとの思いでプログラムを作っていました。そして作ったとしても想定外のフォーマットが来てエラーを吐いて気が狂いそうになりました。今後はそんな苦労をせずとも ChatGPT がうまく抽出してくれそうです。

ChatGPT は誤った情報を返してくることが多いため、仕事として使いにくいと思っていましたが、今回のような抽出タスクで多少の間違いがあってもよいものには使えそうです。

### ChatGPT で特定のフォーマットで出力するにはコツがいる

JSON や CSV、時刻のフォーマットで出力させてみましたが、 JSON に関しては思ったとおりに出力させることができませんでした。また解釈の余地が大きい指示を出してしまうと思った通りに出してくれないことがありました。
このあたりのコツについては今後まとめてくれる方々がいると期待しています。

## 次回

今回は限られたデータでうまくデータを出力する指示を検証しました。
次の実装手順として、多くのデータでもうまくデータを出力実現できるか試してみます（記事にするかどうかは微妙ですが）。

これがうまくいけばもう学習データを用意しなくてもよくなります。
そういう単調な作業が苦手なこともあって個人開発がストップしていたので、これを機に個人開発を再開しようと思いました。

こちらの記事に続きます

https://qiita.com/ogatango/items/3ef5e7e87f66ed971411