統計学、機械学習などを使って身近な世界を分析したりするブログです

機械学習を使って自分に映画をおすすめしてみた 〜スクレイピング編〜

本編をご覧のみなさんこんにちは。本編をご覧になっていないみなさんもこんにちは。

こちらの記事ではスクレイピング編をお送り致します。

言語は慣れたPythonを選択しています。

映画レビューサイトは、利用規約やデータ表示の構造から、みんなのシネマレビューさんにお世話になることにしました。ありがとうございます。情報量も多くて素晴らしいサイトですね。

では、早速やっていきましょう。まずは必要なライブラリをインポートしておきます。どれも一般的なものですね。

#必要なライブラリをインポート
from bs4 import BeautifulSoup
import requests
import pandas as pd
from pandas import Series, DataFrame
import time

みんなのシネマレビューさんでは、映画情報(制作年、監督、キャストなど)、レビュアーリスト(ID、名前、レビュー数など)、各レビュアーのページ(性別、年齢、レビューなど)という構成でページが分かれています。

今回は、レコメンドのモデルにユーザーベースド協調フィルタリングを用いるため、映画情報は使いません。したがって、まずはレビュアーリストからデータを取得し、その後各レビュアーからプロフィールやレビュー内容を取得します。

レビュアーリストからは以下の通り、ID、名前、レビュー数、最終レビュー日を取っておきます。

#レビュアーのID、名前、レビュー数、最終投稿日を取得

#リストを用意
ID1_list = []
Name_list = []
nReview_list = []
lastReview_list = []

#URL(レビュアーリストの1ページ目)
reviewer_url = 'https://www.jtnews.jp/cgi-bin_o/revlist.cgi?PAGE_NO='

for i in range(1,134):
    reviewer_url_no = reviewer_url + str(i)

    #データ取得
    result = requests.get(reviewer_url_no)
    c = result.content

    #HTMLを元に、オブジェクトを作る
    soup = BeautifulSoup(c, "lxml")

    #リストの部分を切り出し
    summary = soup.find_all("td",{'valign':'TOP'})
    
    #2つのテーブルを抜き出し
    tables = summary[2].find_all('table',{'bgcolor':'#4499FF'})
    
    #レビュアーリストの方からtrを抜き出し
    trs = tables[1].find_all('tr')
    
    #ページ内のレビュアー情報をループして取得
    for i in range(2,len(trs)):
        ths = trs[i].find('th') #No
        tds = trs[i].find_all('td') #名前、レビュー数、最終レビュー日

        #Noを取得
        No = str(ths)
        No = No.replace('<th><font color="GREEN">','')
        No = No.replace('</font></th>','')
        Replace_str = '<td><a href="revper.cgi?&amp;REVPER_NO=' + No + '">' #名前部分から文字列を削除するために用意
        ID1_list.append(No)

        #名前を取得
        Name = str(tds[0])
        Name = Name.replace('</a>さん</td>','')
        Name = Name.replace(Replace_str, '')
        Name_list.append(Name)

        #レビュー数を取得
        nReview = str(tds[1])
        nReview = nReview.replace('<td>','')
        nReview = nReview.replace('</td>','')
        nReview_list.append(nReview)

        #最終レビュー日を取得
        lastReview = str(tds[2])
        lastReview = lastReview.replace('<td>','')
        lastReview = lastReview.replace('</td>','')
        lastReview_list.append(lastReview)
        
    time.sleep(3) #待機

次は、協調フィリタリングには使わないのですが、だいたいどんな人たちがレビューしているのか確認しておきたいので、プロフィールから年齢と性別を取ってきます。

ここで良いニュースと悪いニュースがあります。

悪いニュースは、年齢も性別も入力していない人、年齢だけ入力している人、性別だけ入力している人、どちらも入力している人の4パターンがあることです。単純にリストに加えて行くと、後々、先ほど作ったレビュアーリストとくっつけられなくなるので、調整が必要です。

良いニュースは、年齢も性別も選択式になっているため、「男性」「女性」「〜歳」でフォーマットが統一されているので、複雑な前処理が不要なことです。

以下のように、まずはgender、ageをそれぞれTrue(入力あり)、False(入力なし)で場合分けしておき、それにしたがってリストに追加するように調整しました。

#レビュアーの性別と年齢を取得

#リストを用意
gender_list = []
age_list = []

url = 'https://www.jtnews.jp/cgi-bin_o/revper.cgi?&REVPER_NO='

for ID in ID1_list:
    individual_first_url = url + str(ID)

    #データ取得
    result = requests.get(individual_first_url)
    c = result.content

    #HTMLを元に、オブジェクトを作る
    soup = BeautifulSoup(c, "lxml")

    #リストの部分を切り出し
    summary = soup.find_all("td",{'valign':'TOP'})

    #2つのテーブルを抜き出し
    tables = summary[2].find_all('table',{'bgcolor':'#4499FF'})

    #プロフィールの方からtrを抜き出し
    fonts = tables[0].find_all('font', {'color':'GREEN'})

    gender = False
    age = False

    for font in fonts:
        if '性別' in font:
            gender = True
        if '年齢' in font:
            age = True

    #プロフィールの方からtrを抜き出し
    trs = tables[0].find_all('tr', {'bgcolor':'#FFFFFF'})

    for tr in trs:
        tr = str(tr)
        if '性別' in tr:
            gender = tr.replace(
                '<tr bgcolor="#FFFFFF"><th align="LEFT"><font color="GREEN">性別</font></th><td>\r\n','')
            gender = gender.replace(
                '<tr bgcolor="#FFFFFF"><th align="LEFT"><font color="GREEN">性別</font></th><td>\n','')
            gender = gender.replace('</td></tr>','')
            gender_list.append(gender)
        if '年齢' in tr:
            age = tr.replace(
                '<tr bgcolor="#FFFFFF"><th align="LEFT"><font color="GREEN">年齢</font></th><td>\r\n','')
            age = age.replace('</td></tr>','')
            age_list.append(age)
        time.sleep(1) #待機

    if gender == False:
        gender_list.append('')
    if age == False:
        age_list.append('')

    time.sleep(8) #待機

はい、入りました。これは僕の場合18時間くらいかかりました。

ここまでで取得したリストをくっつけて、レビュアーリストを作っておきましょう。

ID1_list = Series(ID1_list)
Name_list = Series(Name_list)
nReview_list = Series(nReview_list)
lastReview_list = Series(lastReview_list)
gender_list = Series(gender_list)
age_list = Series(age_list)

movie_reviewer_df = pd.concat([ID1_list, Name_list, gender_list, age_list, nReview_list, lastReview_list],axis=1)

#カラム名
movie_reviewer_df.columns=['ID1','Name','Gender','Age','nReview','last_Review']

#csvファイルとして保存
movie_reviewer_df.to_csv('movie_reviewer.csv', sep = '\t',encoding='utf-16')

次はいよいよ大事なレビュー内容を拾っていきます。

先ほど取得したIDリストを利用するのですが、レビュアー数が多すぎるので、小分けに分割して少しずつデータを取得します。そうしないと、サイトに負荷をかけすぎてしまったり、途中でミスってやり直すことになってしまいます。

リストを分割するための関数を定義します。他にももっといいやり方があるかもしれません。

#リスト分割関数
def split_array(ar, n_group):
    for i_chunk in range(n_group):
        yield ar[i_chunk * len(ar) // n_group:(i_chunk + 1) * len(ar) // n_group]

ここでは10個に分割していますが、これは、途中でどの程度待機時間を入れるかとのバランスになりますので、待機時間を短くしたい場合は、もっと細かく分割した方がいいでしょう。

#小分けにクローリングするために分割
ID1_list_split = [list(r) for r in split_array(ID1_list, 10)]

さて、一番大事なパートです。

今回は、レビュー内容の中でもテキストは無視して、評価点(0〜10)を使います。ここで気をつけなければいけないのは、レビュアーによってレビュー数が大きく異なることです。

f:id:shokosaka:20171120053242p:plain

f:id:shokosaka:20171120053255p:plain

このように、レビュー数が多いとページが2段で表示されたり3段で表示されたりするので、これに対応する必要があります。

また、評価点も、空白になっている部分があります。

f:id:shokosaka:20171120053354p:plain

これには、新しい数字を見つけるまで前の数字を保持するような仕様にしなければいけません。

以下のように書くことで、上記2つの問題を解決できます。

#レビュー内容(レビュアーID、タイトル、ポイント、レビュー日付)を取得

#リストを用意
ID2_list = []
title_list = []
point_list = []
reviewDate_list = []

for ID in ID1_list_split[0]: #毎回変更
    url = 'https://www.jtnews.jp/cgi-bin_o/revper.cgi?&REVPER_NO='
    
    ID_first_url = url + str(ID)

    #データ取得
    result = requests.get(ID_first_url)
    c = result.content

    #HTMLを元に、オブジェクトを作る
    soup = BeautifulSoup(c, "lxml")

    #リストの部分を切り出し
    summary = soup.find_all("td",{'valign':'TOP'})

    #2つのテーブルを抜き出し
    tables = summary[2].find_all('table',{'bgcolor':'#4499FF'})

    #プロフィールの方からtrを抜き出し
    trs = tables[0].find_all('tr', {'bgcolor':'#FFFFFF'})
    
    #ページ数を取得
    trs = tables[2].find_all('tr', {'bgcolor':'#FFFFFF'})
    last_tr = int(len(trs)/3 - 1)
    
    if last_tr == 0:
        a_last_line = trs[last_tr].find_all('td')
    else:
        a_last_line = trs[last_tr].find_all('a')
    
    number_all_a = last_tr * 20 + len(a_last_line)

    ID_urls = []
    ID_urls.append(ID_first_url)

    for i in range(2,int(number_all_a)+1):
        ID_url = ID_first_url + '&PAGE_NO=' + str(i)
        ID_urls.append(ID_url)
    
    #レビュアーの各ページをループ
    for url in ID_urls:
        time.sleep(2) #待機
        #データ取得
        result = requests.get(url)
        c = result.content

        #HTMLを元に、オブジェクトを作る
        soup = BeautifulSoup(c, "lxml")

        #リストの部分を切り出し
        summary = soup.find_all("td",{'valign':'TOP'})

        #2つのテーブルを抜き出し
        tables = summary[2].find_all('table',{'bgcolor':'#4499FF'})

        #プロフィールの方からtrを抜き出し
        trs = tables[0].find_all('tr', {'bgcolor':'#FFFFFF'})

        #レビュー情報取得
        trs = tables[3].find_all('tr',{'bgcolor':'#FFFFFF'})

        latest_point = 0

        for i in range(len(trs)):
            ths = trs[i].find_all('th')
            tds = trs[i].find_all('td')
            
            #点数取得
            point = str(ths)
            point = point.replace('[<th style="text-align:center">','')
            point = point.replace('</th>]','')

            #ポイントが空白なら引き継ぎ、数字なら更新
            if point is not '':
                latest_point = point

            point_list.append(latest_point)

            #タイトル取得
            title = tds[0].a.attrs['title']

            title_list.append(title)

            #レビュー日付取得
            reviewDate = str(tds[1])
            reviewDate = reviewDate.replace('<td style="text-align:center">','')
            reviewDate = reviewDate.replace('</td>','')

            reviewDate_list.append(reviewDate)
            
            ID2_list.append(str(ID))

        time.sleep(2) #待機
    
    time.sleep(2) #待機

できましたね。はい、それではデータフレーム化してcsvとして出力しておきましょう。

ID2_list = Series(ID2_list)
title_list = Series(title_list)
point_list = Series(point_list)
reviewDate_list = Series(reviewDate_list)

movie_review_df = pd.concat([ID2_list, title_list, point_list, reviewDate_list],axis=1)

#カラム名
movie_review_df.columns=['ID2','Title','Point','Review_Date']

#csvファイルとして保存                ファイル名毎回変更
movie_review_df.to_csv('movie_review0.csv', sep = '\t',encoding='utf-16')

このデータを使って、次の記事では映画のレコメンドモデルを構築します。

www.analyze-world.com