LoginSignup
1
1

More than 1 year has passed since last update.

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

Posted at

この記事でやりたいこと

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

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

まずはソースから

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上のaタグからセリフが入っているものだけを抽出する。
    • 旧作は入れてないです。
  • get_dialogue(url)

    • URLから会話を抽出する。
    • ここで問題なのが、セリフの表記方法で、
      • [キャラ名「セリフ」]
        • たまに[キャラ名(セリフ)]になる。
      • [キャラ名:セリフ]
    • が混在している。
      • 後から気づいたのですが、新版のサイトが公開されていて、そちらでは表記がちゃんと揃っています。ちゃんと下調べしてから実装していきましょう。
    • しかも、そのセリフがあるタグが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)のdialogues.zipが実行後のデータを圧縮したものなので、何かに使いたい場合はこっちがおすすめです。

まとめ

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

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1