Published Date : 2019年5月21日8:08


Deep Manzai Part 1 〜 後半戦 〜







前回の簡単なあらすじ

前回 はお笑いの台本を 書き起こしてくれているサイトへ行き、 Scrapyを使って台本のセリフを
取得しようとする途中までいきました。


続き

それでは前回のコードを 説明していきます。
Responsive image

その前に一つだけ、 前回の記事でHTML構造を 画像で表しましたが、
上の図のように、 このサイトでは新旧で 若干構造が変わっているため、
2つ3つ対応できるように コードを 書いていかなければなりません。

ただ、前回も似たような記事を 書いているので、 説明はコード上だけにします。

manzai_daihon.py

# -*- coding: utf-8 -*-

# scrapyのインポート
import scrapy

# items.pyの中にあるManzaiItemクラスを呼び出す。
from manzai_daihon.items import ManzaiItem

# スパイダー
class ManzaiDaihonSpider(scrapy.Spider):

    # scrapy list を実行した際、出てくる名前。
    # scrapy crawl 名前で この name と一致していないと怒られる。
    name = 'manzai_daihon'

    # このドメインからのみクロールを受けつける。
    allowed_domains = ['spamhameggbaconspam.com']

    # 次に出てくるparseメソッドはこのリストにあるURLから順次実行される。
    start_urls = ['http://spamhameggbaconspam.com/']

    # レスポンスを引数にしてスクレイピングの処理を開始するメソッド
    # デフォルトのコールバックでもある。
    def parse(self, response):

        # ブラウザの検証機能で調べたら、上限ページが103だったので、リミット103
        # インデックス番号は0から始まるので、0から102までの103ページ。
        # ページ数は増えたり減ったりするので、各自ブラウザで直接該当ページを調べて、上限値を設定する。
        for i in range(103):

            # 次のページのURLを設定する。
            # fはフォーマットのf、{}で囲んだ場所に変数や計算式などを入れたら文字列として表示される。
            next_num=f'page/{i+1}'

            # responseには現在クロール中のURLが入っており、urljoinメソッドを使い、次のページのURLを作成する。
            next_page=response.urljoin(next_num)

            # もし次のページが存在するなら、
            if next_page:

                # スクレイピングしたデータを格納するオブジェクトを作成する。
                item=ManzaiItem()

                # 次のページのURLへアクセスし、
                # callback を使うことによって次に書くget_linkメソッドを発動させる。
                # dont_filterをTrueにすると、は同じURLでも正常にクロールしてくれる。
                request=scrapy.Request(next_page,callback=self.get_link,dont_filter=True)

                # 次のメソッド内でもアイテムが活躍できるようにメタ情報を渡す。
                request.meta['item']=item

                ジェネレーターとしてリクエストを返す。
                yield request

    # 台本が書き起こされたページへ飛ぶためのメソッド。
    def get_link(self,response):

        # これでアイテムオブジェクトはこのメソッド内で認識される。
        item=response.meta['item']

        # XPATHを使い台本ページへのリンクが格納されているリストからをaタグをごっそりとる。
        a_s=response.xpath('//div[@id="list"]/a')

        # 取ってきたaタグから該当ページのURLを内包表記でリスト化する。
        hrefs=[a.xpath('@href').get() for a in a_s]

        # リンク先へ一つずつアクセスして台本情報を毟り取るよ!
        for href in hrefs:
            # リンク先URLが入っている確認したのち、
            if href:
                # あればget_articleをコールバックして台本の本文をスクレイピングする。
                request=scrapy.Request(href,callback=self.get_article,dont_filter=True)
                request.meta['item']=item
                yield request

    # データをスクレイピングする直接のメソッド。
    def get_article(self,response):

        item=response.meta['item']

        # タイトルを取得する。
        # 本来なら一つ目の要素にタイトルが入っているが、
        # 上の画像で説明した通り、このサイトは途中からHTMLの構造が変わるため、タイトルや記事のタグを判定していく。
        if response.xpath('//h2/text()')[0].get()!='\n    ':
            # h2タグで取れるタイトルに謎の改行とスペースがある場合があるので、それを避ける。
            # ここで、items.pyで定義したtitleに辞書のような形でタイトルのデータを格納していく。
            item['title']=response.xpath('//h2/text()')[0].get()
        else:
            # こちらは別のHTML構造のタイトルに対応してる。
            item['title']=response.xpath('//h1[@class="entry-title"]/text()').get().strip()

        # セリフを取得していく。
        # BokeとTukkomiに別れているので別々に取得。
        boke=response.xpath('//div[@class="boke"]/text()')
        tukkomi=response.xpath('//div[@class="tukkomi"]/text()')
        
        # articleに空辞書を作る
        item['article']={}

        # ここで古い記事対策。古い記事は全てpタグなので、上で使用した方法でクロールすると、空リストになっている。
        # ついでに漫才の台本かどうかも判定。
        if boke==[]:

            # 記事を囲っているエレメントを取得し、
            m_divs=response.xpath('//ins[@class="adsbygoogle"]')

            # テキストを格納する空のリストを作成し、
            m_text=[]

            # For文で1行ずつかすめ取っていく。
            for m_div in m_divs:

                # 分割したdivタグと同じ階層にあり、かつ後に出てくる兄弟ノードの集合pタグとして記事が格納されている。
                # 一つずつ記事が格納されているセクションか確認して、ちゃんとセリフが格納せれていれば、
                if m_div.xpath('following-sibling::p')!=[]:

                    # 予定通りテキストリストにアペンドしていく
                    m_text.append(m_div.xpath('following-sibling::p/text()'))

                    # 1行より多く格納できたかを判定し
                    if len(m_text)>1:

                        # もし1行より多いなら
                        for m in m_text[0]:

                            # articleに空辞書を作る
                            if item['article']=={}:

                                # 「manzai」のキーを作り、キーの値として空のリストを作る。
                                item['article']['manzai']=[]
                                
                                # そのリストに掠め取った台本のセリフを詰め込んでいく
                                # その際に全角スペースを半角スペースに置き換える
                                item['article']['manzai'].append(m.get().replace('\u3000',' '))
                            else:
                                # もうすでに付け足し中なら、通常通り進行。
                                item['article']['manzai'].append(m.get().replace('\u3000',' '))

                # 第二構造に対応
                else:

                    # tableタグにセリフが格納してあるので、上記のように掠め取る
                    m_tds=m_div.xpath('following-sibling::table//td/text()')
                    if m_tds!=[]:
                        item['article']['manzai']=[m_td.get() for m_td in m_tds]
        
        # 漫才、コントかピンネタかあるいは例外的な仕様か判定していく。
        else:
            # tukkomiがあれば漫才、コント仕様に辞書を作成していく。
            # 前処理で大変な思いをしなくて済むようある程度処理を進めながら整形していく。
            if tukkomi!=[]:
                item['article']['boke']=[b.get() for b in boke]
                item['article']['tukkomi']=[t.get() for t in tukkomi]

            # ここで、例外的に発生する特殊な事例、タイトルが「芸人の名前+タイトル」の場合に対応
            elif item['title']=='ある芸人1の台本' or item['title']=='ある芸人2の台本' or item['title']=='ある芸人3の台本':
                item['article']['boke']=[t.get() for t in tukkomi]
                item['article']['tukkomi']=[b.get() for b in boke]
            # こちらピンネタに対応
            else:
                print('pin')
                item['article']['pin']=[b.get() for b in boke]
        ここでジェネレーターとしてアイテムをparseメソッドにぶっ返す
        yield item
詳しいことはこの記事を参考に。

後は実行するだけです。
# 実行可能なスパイダーを表示。
scrapy list

--> manzai_daihon

# daihon.jsonをアウトプットとしてクローラーを実行。
scrapy crawl manzai_daihon -o daihon.json
以下のようなJSONファイルができます。
Responsive image


前処理と分析

1 データ収集(Data Scraping)
2 前処理(Preprocessing)
3 分析(Analyizing)
4 Build Neural Network
5 Word2Vecを使う
6 LSTMによる文章生成
続いて前処理と分析です。 と言っても 大したことは行いません。

ではColaboratory JSONファイルを上げて、 作業していきましょう。

Colaboratoryに関しては この記事を参考に。

形態素解析のSudachiの設定は、 この記事を参考に。
# まずグーグルドライブをマウント
from google.colab import drive
drive.mount('/content/drive')

"""
Go to this URL in a browser: https://accounts.google.com/o/oauth2/auth?client_id=hogehogehogehogehoge...................

Enter your authorization code:
[ここにコードを入力してエンターキー]
"""

# JSONファイルをアップロードした場所に移動。
# マイドライブ内に適当なフォルダを作ってそこにJSONファイルをおいてくだちぃ。
cd /content/drive/My Drive/data

# ライブラリインポート
import pandas as pd
import json
import numpy as np

# jsonファイルの読み込み。
with open('./daihon.json','r',encoding='utf-8') as f:
    daihon=json.load(f)

# ファイル構造を理解する。
# その1:辞書が一つずつ格納されているリスト
type(daihon)
# -> list
type(daihon[0])
# -> dict

# その2:辞書のキーは"title","article"
daihon[0].keys()
#-> dict_keys(['title', 'article', 'url'])

# その3:辞書の"article"には「Boke」(ピンなら全てボケ)「Tukkomi」など役割が先頭についたセリフのリストが入っている。
daihon[0]['article']

"""
['Boke: どうも四千頭身です。お願いします。', 
'Tukkomi: よろしくお願いします。', 
'Boke: 突然なんだけどさ俺結構ね正夢見んのよ。',
.....................,
.....................,
"""


# ここで管理しやすいようにデータフレームに直す。

# タイトルとセリフを分ける。
titles=[d['title'] for d in daihon]
# セリフは一旦改行でくっつくけて一つの文字列にする。
articles=['\n'.join(d['article']) for d in daihon]

# Numpy配列にし、転置で(2行、(台本の数)列)に直す。
rows=np.array([titles,articles]).T
# 列名を指定。
columns=['title','article']
# データフレームにする。
df=pd.DataFrame(rows,columns=columns)
JSONファイルのままより 二度手間だが、 データフレームに直せば、
以下のように全体が把握しやすくなる。
Responsive image


続いて定量的な解析。
# ilocで行固定、列番号指定で簡単に取り出せる
df.iloc[0,1]
"""
'Boke: どうも四千頭身です。お願いします。\n
Tukkomi: よろしくお願いします。\n
Boke: 突然なんだけどさ俺結構ね正夢見んのよ。\n
Tukkomi: そうなの?どんな夢見るの?\n
Boke: ちょっと前になるけどサッカーワールドカップあったじゃない。
................................
...............................,
"""

# locで列名を指定をしても同じ。
df.loc[0,'title']
"""
'四千頭身「正夢」'
"""

# applyメソッドで記事の情報や操作ができる
# 台本のセリフ量(文字数)
df.iloc[0].apply(len)
"""
title         8
article    2472
Name: 0, dtype: int64
"""

# とりあえず、余分な文字を排除して正しい計算ができるようにする。
df['article'].apply(lambda x: x.replace('\n','').replace('Boke: ','').replace('Tukkomi: ','').replace('mannaka',''))

Responsive image

# 全てのセリフ量(文字数)
split_char=lambda x: x.replace('\n','').replace('Boke: ','').replace('Tukkomi: ','').replace('mannaka','')
len(df['article'].apply(split_char).sum())
-> 役125万文字
# ちなみに、上記作業を行わず、そのまま文字数を計ると190万文字になる

# 何度も操作するのでSeries(NumpyArray一次元配列)にする。
scripts=df['article'].apply(split_char)

# 全体の文字数。
scripts.apply(len)

Responsive image

#さらに短縮
data=scripts.apply(len)

# 最大文字数のセリフ
np.max(data)
"""
-> 5999
"""

# 最小文字数のセリフ
np.min(data)
"""
-> 188
"""

# 平均
# 役1488文字
np.mean(data)
"""
-> 1488.7949940405244
"""

# 中央値
# 役1488文字
np.median(data)
"""
-> 1318.0
"""

# 標準偏差
np.std(data)
"""
-> 826.7900655918364
"""

# 分散
np.var(data)
"""
-> 683581.8125613531
"""

# ついでに統計データを計算する標準ライブラリStaticsで同じことをする
import statistics as stat 


stat.mean(data)	#平均
"""
1488.7949940405244
"""
stat.median(data)	#中央値
"""
1318
"""
stat.median_low(data)	#中央の2値の小さい方を求める
"""
1318
"""
stat.median_high(data)	#中央の2値の大きい方を求める
"""
1318
"""
stat.median_grouped(data)	#グループ化されたデータから中央値を求める
"""
1317.75
"""
stat.mode(data)	#最頻値
"""
640
"""
stat.pvariance(data)	#母標準偏差
"""
683581.8125613529
"""
stat.variance(data)	#標本標準偏差
"""
684397.5426479415
"""
stat.pstdev(data)	#母分散
"""
826.7900655918362
"""
stat.stdev(data)	#標本分散
"""
827.2832300052634
"""

次回へ続く

なんでこんな切りの悪い ところで、次回へ続くかって?

気付いて無いかもしれないが、 今日は火曜日さ!

まだ週の半分もいってないんだぜ? ははは!HAHAHA!はは...

それではまた次回。


See You Next Page !