Pythonでウェブサイトから東方のセリフを抽出する

# この記事でやりたいこと

+ 東方作中のセリフをまとめているサイト[Radical Discovery](http://radical-d.extrem.ne.jp/index.html)からセリフをいい感じに抽出する。
    + いい感じとは、
        1. dialoguesディレクトリを作成する。
        2. その中に、dialogue_n.txt(nは整数)を作成する。
        3. そのファイルに、[キャラ名：セリフ]の形式で一文ずつ書き出す。
            + 話題の分かれ目は何もない行を挿入する。
            + こうすると他のプログラムからデータを扱いやすい。
    + です。

かなり雑に組まれているのでコード自体は汎用性は低いです。

# まずはソースから

```Python:vacuum_data.py
import requests
from bs4 import BeautifulSoup


def main_soup_from_url(url):
    response = requests.get(url)
    response.encoding = response.apparent_encoding
    main_soup = BeautifulSoup(response.text, 'html.parser')

    return main_soup


def get_urls(original_url):
    main_soup = main_soup_from_url(original_url)
    urls = []

    a_tag = main_soup.find_all('a')

    for a in a_tag:
        url = a.attrs['href']
        if url[:7] != 'http://' and url.find('toho/serihu/') == 0 and url != 'toho/serihu/serihu.htm':
            url = original_url + url
            urls.append(url)

    return urls


def rsplit_characters(text, rsp_chrs):
    for rsp_chr in rsp_chrs:
        text = text.rsplit(rsp_chr)[0]
    return text


def replace_characters(text,rep_chrs):
    for rep_chr in rep_chrs:
        text = text.replace(rep_chr[0],rep_chr[1])
    return text

def delete_characters(text, del_chrs):
    for del_chr in del_chrs:
        text = text.replace(del_chr, '')
    return text

def all_delete(text, all_dels):
    for all_del in all_dels:
        sta_n = text.find(all_del[0])
        end_n = text.find(all_del[1])
        while sta_n > -1 and end_n > -1:
            text = text[:sta_n] + text[end_n + 1:]
            sta_n = text.find(all_del[0])
            end_n = text.find(all_del[1])

    return text

def end_find(text, end_chrs):
    end_n = len(text)

    for end_chr in end_chrs:
        find_n = text.find(end_chr)
        if find_n > -1 and find_n < end_n:
            end_n = find_n

    if end_n < len(text):
        return end_n
    else:
        return -1

def match_characters(aim_chr,mat_chrs):
    for i in range(len(mat_chrs)):
        if aim_chr == mat_chrs[i]:
            return i
    
    return -1

def get_dialogue(url):
    main_soup = main_soup_from_url(url)

    td_tag = main_soup.find_all('td', class_='word')
    if len(td_tag) > 0:
        return get_dialogue_float(main_soup)
    else:
        return get_dialogue_int(main_soup)


def get_dialogue_int(main_soup):
    font_tag = main_soup.find_all('font')

    texts = []
    blank = True

    sep_chr = '：'
    del_chrs = ['\n', '　']
    sta_chrs = ['（']
    fin_chrs = ['）']
    end_chrs = '。！？〜～）♪'

    for value in font_tag:
        text = delete_characters(value.text, del_chrs)
        name_n = text.find('：')

        if(name_n > -1):
            name = text[:name_n]
            text = text[name_n + 1:]
            end_n = end_find(text, end_chrs)

            while end_n > -1:
                while end_n < len(text) and match_characters(text[end_n], end_chrs) > -1:
                    if match_characters(text[end_n], fin_chrs) > -1:
                        end_n += 1
                        if match_characters(text[0], sta_chrs) > -1:
                            break
                        else:
                            end_n += end_find(text[end_n:], end_chrs)
                    end_n += 1
                
                texts.append(name + sep_chr + text[:end_n])
                text = text[end_n:]
                end_n = end_find(text, end_chrs)

            if len(text) > 0:
                texts.append(name + sep_chr + text)

            blank = False
        elif(not blank):
            texts.append('')
            blank = True

    return texts


def get_dialogue_float(main_soup):
    td_tag = main_soup.find_all('td', class_='word')

    texts = []
    blank = True
    new_sentence = True

    sep_chr = '：'
    rep_chrs = [['\n\n','\t'],['〜','～']]
    del_chrs = ['\n', '\u3000', '\xa0', ' ', '　']
    all_dels = [['【','】']]
    sta_chrs = ['「','（']
    fin_chrs = ['」','）']
    end_chrs = ['。', '！', '？', '～', '♪'] + fin_chrs
    sentence_sta = 0

    for value in td_tag:
        text = value.text
        text = replace_characters(text, rep_chrs)
        text = delete_characters(text, del_chrs)
        text = all_delete(text, all_dels)

        if end_find(text,sta_chrs) > -1:
            end_n = end_find(text, end_chrs)

            while end_n > -1:
                name_n = end_find(text, sta_chrs)

                if name_n > 0 and (name_n < end_find(text, end_chrs) or new_sentence):
                    sentence_sta = match_characters(text[name_n],sta_chrs)

                    name = text[:name_n]

                    if name[0] == '\t':
                        texts.append('')
                        name = name.replace('\t','')
                    
                    text = text[name_n + 1:]
                    end_n = end_find(text, end_chrs)
                    new_sentence = False
                
                try:
                    while match_characters(text[end_n], end_chrs) > -1:
                        if text[end_n] == fin_chrs[sentence_sta]:
                            new_sentence = True

                            if match_characters(text[end_n - 1], end_chrs) > -1:
                                text = text.replace(text[end_n], '', 1)
                            else:
                                text = text.replace(text[end_n], '。', 1)
                                end_n += 1
                            
                            break
                            
                        end_n += 1
                except:
                    global urls,i
                    print('error:',urls[i])

                texts.append(name + sep_chr + text[:end_n])
                text = text[end_n:]
                end_n = end_find(text, end_chrs)

            if len(text) > 0:
                texts.append(name + sep_chr + text)

            blank = False
        if(not blank):
            texts.append('')
            blank = True

    return texts

# メインのコード

url = 'http://radical-d.extrem.ne.jp/'
urls = get_urls(url)

for i in range(len(urls)):
    texts = get_dialogue(urls[i])

    with open('dialogues/dialogue_' + str(i) + '.txt', 'w',encoding = 'utf-8') as file:
        print(urls[i])
        for text in texts:
            file.write(text + '\n')

    print('finish:',i,'/',len(urls))
```

# 内容解説

## 関数

+ get_urls(url)
    + [Radical Discovery](http://radical-d.extrem.ne.jp/index.html)上のaタグからセリフが入っているものだけを抽出する。
    + 旧作は入れてないです。

+ get_dialogue(url)
    + URLから会話を抽出する。
    + ここで問題なのが、セリフの表記方法で、
        + [キャラ名「セリフ」]
            + たまに[キャラ名（セリフ）]になる。
        + [キャラ名：セリフ]
    + が混在している。
        + 後から気づいたのですが、[新版のサイト](http://radical-d.extrem.ne.jp/touhou/)が公開されていて、そちらでは表記がちゃんと揃っています。ちゃんと下調べしてから実装していきましょう。
    + しかも、そのセリフがあるタグがfontタグかtdタグかも異なる。
    + なので、実際の処理はHTML全体でのtdタグの有無でget_dialogue_floatとget_dialogue_intに分けている。
        + これらにはURLではなく、main_soup_from_url(url)を使ってHTMLの構造を渡している。
        
+ main_soup_from_url(url)
    + URLからHTMLの構造を出力する。
        + requests.get → 文字コードを合わせる → BeautifulSoupという流れで取得できる。
     
+ get_dialogue_float(main_soup)
    + [キャラ名「セリフ」]から一文ずつ[キャラ名：セリフ]にしてまとめたテキストを出力する。
        + ここでいう一文は[。]や[！]が出てきたら終わるものを指します。
            + 元のセリフでは文が繋がっているものが多いので分けていく。
            + ！？など、終止符が一つで終わらない場合もあるのでwhileで終止符以降を1文字ずつ判断していく。
            + ただし、[」]で終わる場合は。がない場合があるのでちゃんと文末と判断して[。]をつけてあげる処理が必要。
        + 制御文字がたくさんあるので適切に排除していく。
            + \u3000や\xa0がある。何用かはよくわからない。
            + 話題の分け目の判断に必要な\n\nはいったん\tに変換しておき、\nを消してから\tを\nに変換する。
        + 一般的ではない伸ばし棒[〜(波ダッシュ)]を[～(全角チルダ)]に変換しておく。
            + Qiita上では同じに見えるが、違う文字コード。
            + 使用用途としては[〜(波ダッシュ)]が正しいが、キーボードで一発で打てないので、[～(全角チルダ)]を使う……
    + + 非整数番台の作品の一部に[キャラ名「セリフ」]の表記が見られたのでfloatと名付けた。
            
+ get_dialogue_int(main_soup)
    + [キャラ名：セリフ]から一文ずつ[キャラ名：セリフ]にしてまとめたテキストを出力する。
    + ここでの[～]は普通の[～]なのでスルーしてよい。
    + floatより幾分か処理がすっきりしている。実装も楽だった。
    + hrタグで対戦前後のセリフが分かれているが、これをデータに反映させるのは面倒なのでパスした。
+ それ以外の関数
    + 中身が簡素なので、どんな処理かはご自身で確かめてみてください……(説明が面倒になっただけ)

## 実行について

+ サーバへの負荷もありますし、時間がかかるのでお勧めできません。
+ [こちら(Github)](https://github.com/KitsuneDaro/ProgramMadeByPython/tree/main/NaturalLanguage)のdialogues.zipが実行後のデータを圧縮したものなので、何かに使いたい場合はこっちがおすすめです。

# まとめ

自然言語処理などで、大量のコーパス(テキスト)が必要になったときに、Webスクレイピングは非常に有効な手段です。
BeautifulSoupがあれば簡単にWebスクレイピングできるので、是非皆さんも試してみてね～！