仮想と現実の真ん中あたり

主に舞台探訪とか聖地巡礼と呼ばれる記録をつづるブログ

舞台探訪作品の略称一覧をPythonで収集してみる

1.舞台探訪作品の名称一覧を収集する

 昨今の状況で外を出歩けずにいるのですが、無為に過ごすのも何なので、プログラムを作ってみることにしました。
 そこで思いついたのが、舞台探訪作品(=舞台探訪されている作品。正しくは「被舞台探訪作品」と言うべきか?)の名称一覧を収集するプログラムです。
 舞台探訪作品の情報が集積されているサイトと言えば、真っ先に挙げられるのが『舞台探訪アーカイブ Wiki* 』です。管理者のおおいしげん先生が前身の『舞台探訪アーカイブ』時代から丹念に収集を続けられており、Web上の舞台探訪活動の記録情報としては日本随一の情報量を誇ります。
 その『舞台探訪アーカイブ Wiki*』を利用させていただくことにして、Pythonでいわゆる「Webスクレイピング」するプログラムを作り、舞台探訪作品の一覧を出力してみることにします。
 プログラムの流れは下記のようになります。

【作品名収集プログラムのフロー】

  1. 必要なモジュールをインポートする
    • URLを解釈するための、urllib.parse
    • URLからHTMLを取得するための、requests
    • 取得したHTMLを解釈するための、Beautiful Soup
      (最近はScrapyが流行りらしいのですが、手元の参考書に載っていたのがコレだったので)
    • データを保存するための、pandas
      (今回はpandasまで使う必要が無いのですが、後々便利なので)
  2. 作品名一覧を収録するためのデータフレームをpandasで作成
  3. 舞台探訪アーカイブ Wiki*内の『作品名50音順一覧』のURLを「あ行」からurllibで解釈
  4. requests.get()でHTMLを取得
  5. 取得したHTMLをBeautiful Soupに入れて作品名リストを抽出
  6. 作品名をデータフレームに追加
  7. 作品名の数だけ5.~6.を実行
  8. 作品名リストの最後まで行ったら、3.に戻って次の50音順を解釈
  9. 以下、50音順に3.~8.まで繰り返し
  10. 収集したデータフレームを.csvファイル(TanbouSakuhin.csv)に書き出し

 作成したコードは次の通りです。(名前は"Tanbou Sakuhin Mei Collector"。ベタだ…)

# -*- coding: utf-8 -*-
#################################################################################
#                       TSMCollector                                            #
#   機能:舞台探訪作品名収集プログラム                                         #
#   履歴:Ver.1.00: 2020/04/04 舞台探訪アーカイブ Wiki*からWebスクレイピングで    #
#                              探訪作品名を収集してファイル出力。              #
#                                                                               #
#                       2020 USO9000 All rights reserved.                       #
#################################################################################
import urllib.parse
import requests, bs4
import pandas as pd

lst_coloumn = ['作品名']
url_index_tuple = ('あ', 'か', 'さ', 'た', 'な', 'は', 'ま', 'や', 'ら', 'わ')
url_post = 'https://wikiwiki.jp/legwork/'

#DBを作成
df = pd.DataFrame(columns = lst_coloumn)

for url_index in url_index_tuple:
    #舞台探訪アーカイブ Wiki*へのURLを50音順に作成
    url = '作品名50音順一覧(' + url_index + '行)'
    url = url_post + urllib.parse.quote(url)

    #舞台探訪アーカイブからページデータをダウンロード
    res = requests.get(url)
    res.raise_for_status()

    #ページデータをBeautiful Soupに入れて解析準備
    page = bs4.BeautifulSoup(res.text, 'lxml')

    #作品名のHTMLリストを抽出
    item_list = page.select('.list1 li a')  #class="list1"の<li>かつ<a>の要素(=作品名)を抽出
    item_num = len(item_list)

    #DFにデータを追加
    for i in range(item_num):
        #HTMLから作品名を抽出
        item_name = item_list[i].getText()
        #念のため末尾の空白文字を削除
        item_name = item_name.rstrip()

        if not (item_name == '[添付]'):   #例外を除く
            #DFへ追加
            new_coloumn = pd.DataFrame(data=[[str(item_list[i].getText())]], columns = lst_coloumn)
            df = df.append(new_coloumn, ignore_index=True)

#DBファイル書き出し
df.to_csv('TanbouSakuhin.csv', encoding='utf_8_sig')

 ※このプログラムはフリーウェアです。個人的利用の限りにおいて利用,複製,改変は自由です。

 このプログラムを実行すると、下記のようなCSVファイル(TanbouSakuhin.csv)が出力されます。

,作品名
0,ああっ女神さまっ
1,R.O.D
2,あいうら
3,I/O
4,A・Iが止まらない!
…
1796,ONE ~輝く季節へ~
1797,1/2 summer
1798,ワンパンマン
1799,One Room
1800,ヲタクに恋は難しい

 短いプログラムのわりに複雑な処理が出来るところに、Pythonのありがたさを感じますね。

2.舞台探訪作品の略称を追加する

 さて、探訪作品名は収集出来ました。しかし、ファンの間では一般的に作品は略称(例:『ガールズ&パンツァー』⇒略称『ガルパン』)でやりとりされますよね? そのため、作品名と略称の対応表が欲しくなります。(「他にだれが欲しがるのか?」というツッコミは置いといて)
 そこで、上記で作った作品名のデータを使って、Wikipediaから略称を調べて追記するプログラムを作ってみることにします。
 (舞台探訪作品に限定しなければ、最初からWikipediaから作品名と略称を抽出する方が近道でしょう。ただ、それだと作品数が膨大になるし、自分の場合は舞台探訪作品だけで十分だったので『舞台探訪 Wiki』に載っている作品のみを検索することにしたわけです)

 しかし、Wikipediaは不特定多数で編集されているため、『舞台探訪 Wiki
』に比べてフォーマットの統一性が劣ります。そのため、機械的な検索だけでは成功率を100%にするのは難しく、結果を人の手で修正する必要が出てきます。
 この課題を解決するため、下記のような機械と人手を組み合わせたワークフローで対応することにします。

【略称収集のワークフロー 】

  1. まず、上記で説明したプログラムで舞台探訪作品名の一覧を作成する
  2. 追加するプログラムで1.の舞台探訪作品名をWikipediaに投げて略称を調べ、ファイルに追記する
  3. 出来たファイルを手作業で編集して、おかしな略称を修正する
  4. Wikipediaが更新した場合のために)実行2回目以降のプログラムは、ファイルに略称が無い作品名のみを検索する
  5. (『舞台探訪 Wiki』が更新した場合のために)実行2回目以降のプログラムは、『舞台探訪 Wiki』にあってファイルに無い作品(新規作品)を抽出し、Wikipediaで略称を調べる
  6. プログラムの最後で新規作品名を追記してファイルを更新する

 このワークフローを実現するコードは、以下のようになります。
 ※先のプログラムに加えて、ファイル操作のための os, shutilモジュール、正規表現のための reモジュールを追加しています。

# -*- coding: utf-8 -*-
#################################################################################
#                       TSMCollector                                            #
#   機能:舞台探訪作品名収集プログラム                                         #
#   履歴:Ver.1.00: 2020/04/04 舞台探訪アーカイブ Wiki*からWebスクレイピングで    #
#                              探訪作品名を収集してファイル出力。              #
#         Ver.2.00: 2020/04/11 舞台探訪アーカイブ Wiki*から収集した作品名に、   #
#                              Wikipediaから略称を追加してファイル出力。        #
#                                                                               #
#                       2020 USO9000 All rights reserved.                       #
#################################################################################
import os
import shutil
import re
import urllib.parse
import requests, bs4
import pandas as pd

#略称検索結果格納オブジェクト
class clSearchResult():
    def __init__(self, ):
        self.match = 0
        self.name = ''

#舞台探訪アーカイブ Wiki*の作品名リストを取得
def dfGetButaiTanbou():
    lst_coloumn = ['作品名', '略称']
    url_index_tuple = ('あ', 'か', 'さ', 'た', 'な', 'は', 'ま', 'や', 'ら', 'わ')
    url_post = 'https://wikiwiki.jp/legwork/'

    #DFを作成
    df = pd.DataFrame(columns = lst_coloumn)

    for url_index in url_index_tuple:
        #舞台探訪アーカイブ Wiki*へのURLを50音順に作成
        url = '作品名50音順一覧(' + url_index + '行)'
        url = url_post + urllib.parse.quote(url)

        #舞台探訪アーカイブからページデータをダウンロード
        res = requests.get(url)
        res.raise_for_status()

        #ページデータをBeautiful Soupに入れて解析準備
        page = bs4.BeautifulSoup(res.text, 'lxml')

        #作品名のHTMLリストを抽出
        item_list = page.select('.list1 li a')  #class="list1"の<li>かつ<a>の要素(=作品名)を抽出
        item_num = len(item_list)

        #DFにデータを追加
        for i in range(item_num):
            #HTMLから作品名を抽出
            item_name = item_list[i].getText()
            #後でCSVにする都合上、','を全角に変換
            item_name = item_name.replace(',', ',')
            #念のため末尾の空白文字を削除
            item_name = item_name.rstrip()

            if not (item_name == '[添付]'):   #例外を除く
                #DFへ追加
                new_coloumn = pd.DataFrame(data=[[str(item_list[i].getText()), None]], columns = lst_coloumn)
                df = df.append(new_coloumn, ignore_index=True)

    return (df)

#略称検索
def GetShortName(list, word, left, right):
        name_flag = 0
        for item in list:
            text_part = item.getText()  #候補のテキスト部分を抽出
            m = re.search(r'{0}'.format(word), text_part)    #略称部分を正規表現で検索
            if bool(m): #略称を発見した場合
                target = m.group()
                m = re.search(r'{0}'.format(left), target)    #『,「等の位置を検索
                start = m.start()
                m = re.search(r'{0}'.format(right), target)    #』,」等の位置を検索
                end = m.end()
                short_name = target[start+1:end-1]
                name_flag = 1
                break

        result = clSearchResult()

        result.match = name_flag
        if name_flag:
            result.name = short_name
        
        return result

#Wikipediaから作品名の略称を取得
def GetWikipedia(name):
    url_post = 'https://ja.wikipedia.org/wiki/'
    url = url_post + urllib.parse.quote(name)

    #Wikipediaからページデータをダウンロード
    res = requests.get(url)

    #失敗した場合、小文字記号で再トライ(Wikipediaは基本的にこの記述のようだ)
    if not bool(res):
        name = name.translate(str.maketrans({'!': '!', '?': '?', '&': '&', ',': ','}))
        url = url_post + urllib.parse.quote(name)
        #Wikipediaからページデータをダウンロード
        res = requests.get(url)

    short_name = ''
    if bool(res):
        #ページデータをBeautiful Soupに入れて解析準備
        page = bs4.BeautifulSoup(res.text, 'lxml')

        #作品名のHTMLを抽出
        item_list = page.select('p')  #<p>の要素を抽出

        result = GetShortName(item_list, '(略称|通称|略して).*?(『|「).+?(』|」)','『|「','』|」')
        if not result.match:
            result = GetShortName(item_list, '(略称|通称).*?:.+?)',':',')')

        if result.match:
            short_name = result.name  #略称を発見出来た場合
        else:
            short_name = '-'  #略称を発見出来なかった場合

    return (short_name)    #略称を返す

#Wikipediaから作品名の略称を取得して更新(数十分かかる)
def UpdateShortName(df):
    #DFに調べてない略称があるなら更新
    s_df = df['略称']
    if s_df.isnull().any():
        #DFメンテ
        coloum_num = len(df)
        for index in range(coloum_num):
            if pd.isnull(df.at[index, '略称']):
                short_name = GetWikipedia(df.at[index, '作品名'])
                if short_name == '-':   #略称が見つからなかった
                    df.at[index, '略称'] = '-'
                elif not bool(short_name):  #Wikipedia不明
                    df.at[index, '略称'] = ''
                else:   #略称発見
                    df.at[index, '略称'] = short_name                   

            exec_rate = '{:3d}%'.format(int((index + 1) * 100 / coloum_num)) + ' {:4d}'.format(index)
            print(exec_rate) #実行率表示

    return (df)    #DFを返す

#メイン関数
if __name__ == '__main__':
    file_name = 'TanbouSakuhin.csv'
    file_name_dif = 'TanbouSakuhin_dif.csv'
    file_name_back = 'TanbouSakuhin_back.csv'
    lst_coloumn = ['作品名', '略称'] 

    #舞台探訪アーカイブ Wiki*の作品名リストを取得
    new_df = dfGetButaiTanbou()

    if not os.path.exists(file_name):   #DBファイルが無いなら書き出し
        #Wikipediaから未取得の略称を取得して更新
        new_df = UpdateShortName(new_df)
        #DF保存
        new_df.to_csv(file_name, encoding='utf_8_sig')

    else:   #既にDFファイルがあるなら、新規作品の略称をwikipediaから検索して更新
        #DFファイルを読み込み
        shutil.copy(file_name, file_name_back)    #DBファイルをバックアップ
        df = pd.read_csv(file_name, index_col=0, dtype=str)

        #手動更新した場合向けにindexを振り直した後、Wikipediaから未取得の略称を取得して更新
        df = df.reset_index(drop=True)
        df = UpdateShortName(df)
        df.to_csv(file_name, encoding='utf_8_sig')  #保存

        #差分を抽出
        dif_df = new_df[~new_df.作品名.isin(df.作品名)]
        print('新規作品数は ' + str(len(dif_df)))
        if len(dif_df) > 0:
            dif_df.to_csv(file_name_dif, encoding='utf_8_sig')  #確認用に差分を保存

            #差分を結合
            df = pd.concat([df, dif_df])
            df = df.reset_index(drop=True)

            #Wikipediaから未取得の略称を取得して更新
            df = UpdateShortName(df)
            #DF保存
            df.to_csv(file_name, encoding='utf_8_sig')

 ※このプログラムはフリーウェアです。個人的利用の限りにおいて利用,複製,改変は自由です。

 このプログラムを実行すると、下記のようなCSVファイル(TanbouSakuhin.csv)が出力されます。
 (略称欄で、"-"は略称が見つからなかった事を、"(空欄)"はURLが見つからなかったことを、それぞれ示す)

,作品名,略称
0,ああっ女神さまっ,-
1,R.O.D,-
2,あいうら,-
3,I/O,-
4,A・Iが止まらない!,AI止ま
5,アイカツ!,フォトカツ!
…
1796,ONE ~輝く季節へ~,-
1797,1/2 summer,-
1798,ワンパンマン,-
1799,One Room,-
1800,ヲタクに恋は難しい,ヲタ恋

 ※『アイカツ!』の略称が『フォトカツ!』になっているのは誤認識。
 判別結果をまとめて見てみると、以下のようになりました。

結果 件数 割合
総件数 1,801件 100%
Wikipediaに略称あり 382件 21%
Wikipediaに略称なし 1,096件 61%
WikipediaのURLなし 323件 18%

 これはプログラムが判別した値ですが、後で全1,801件中の約100件の略称を手で修正する必要がありました。正解率としては約95%です。まぁまぁ実用的なレベルと言えるでしょう。
 しかしながら、機械的な検索のため、どうしても変なノイズは拾ってしまいます。例えば、

15,アイドルマスター シャイニーカラーズ,シャニマス[2]

 ↑のように注釈([2])を拾うのはまだ良い方で、

439,逆境無頼カイジ,ざわ‥ざわ‥
555,ゴールデンカムイ,北鎮部隊
606,咲 -Saki-,まこメシ
1580,みにとじ,結城友奈は勇者である
1706,ラブライブ! サンシャイン!!,マルヨン

 ↑あたりになると「どうしてこんなのを拾った!?」という感じです。(いや、『カイジ』は合ってるかも?)
 ともあれ、PythonによるWebスクレイピングを試してみるには面白い題材でした。

 最後になりましたが、このプログラムは、『舞台探訪アーカイブ Wiki* 』が無ければ出来ませんでした。管理者のおおいしげん先生に感謝いたします。

3.参考資料

この記事を書くにあたり、下記のサイト,本を参考にさせていただきました。ありがとうございました。
* 舞台探訪アーカイブ Wiki*
* Wikipedia
* O'Reilly Japan - 入門 Python 3
* O'Reilly Japan - 退屈なことはPythonにやらせよう
* O'Reilly Japan - Pythonによるデータ分析入門 第2版
* kazetof - Qiita
* Pythonに関する情報 | note.nkmk.me
* その他、Pythonに関する有用な記事をWebに上げていただいている皆様