映画レビュー・評価値のスクレイピング １

# 概要

映画のレビューや評価値をPythonのBeautifulSoupライブラリを利用して取得する方法を紹介します．推薦システムを練習するためのデータ収集やレビューと評価値の分析の練習に利用できると思います．最終的に，下図のような表形式のデータセットの取得が目的となります．
![table.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3725285/f3c56c72-4319-e424-89c5-efdaaab718ca.jpeg)


【紹介項目】

* 映画.comを利用して対象となる映画のユーザーレビューや評価値を取得する方法

:::note
利用するライブラリのインストール
pip install beautifulsoup4
pip install requests
pip install lxml
:::

BeautifuSoupの基本的な使い方は解説しません．[公式のサイト](
https://www.crummy.com/software/BeautifulSoup/)や様々な解説ページ，スクレイピングの書籍を参考にしてください．

# 1. Webページのデータを取得
オードリーの永遠の名作である<font color="blue">「ローマの休日」</font>を具体例としてユーザーレビューや評価値を抽出していきます．

https://eiga.com/movie/50969/

:::note
取得URL情報
https://eiga.com/movie/50969/review/
:::

最初に，スクレイピングしたいWebページの情報を取得してみます．html解析に，lxmlを利用しました．User-AgentとURLを適切に設定して，requestsとBeautifulSoupを使いURLの情報を読みこみます．

~~~python: 「ローマの休日」スクレイピング
import requests
from bs4 import BeautifulSoup

url = "https://eiga.com/movie/50969/review/" # ローマの休日
headers = {"User-Agent": "Mozilla/5.0"}

response = requests.get(url, headers=headers)
soup = BeautifulSoup(response.text, "lxml")
~~~


soupを表示すると，Webブラウザーでページのソースを表示させたときと同様，１行の長いテキストになります．



~~~: soupの表示１
<!DOCTYPE html> <html lang="ja"> <head> <meta http-equiv="x-ua-compatible" content="ie=edge"/>
~~~
わかりにくいので，コードを整形してくれるエディターか，オンラインのHTML Formatterを利用して見た目を綺麗にしてみます．


~~~: soupの表示２
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta content="ie=edge" http-equiv="x-ua-compatible" />
    <meta content="width=1060" name="viewport" />
    <meta charset="utf-8" />
    <title>ローマの休日のレビュー・感想・評価 - 映画.com</title>
    <meta content="ローマの休日の映画レビュー・感想・評価一覧。映画レビュー全191件。評価4.4。みんなの映画を見た感想・評価を投稿。" name="description" />
    <meta content="ローマの休日,レビュー,感想,評価,映画" name="keywords" />
    <meta content="summary" name="twitter:card" />
    <meta content="@eigacom" name="twitter:site" />
    <meta content="ja_JP" property="og:locale" />
    <meta content="ローマの休日のレビュー・感想・評価 - 映画.com" property="og:title" />
   
    ~~~ 中略 ~~~
    
    <script src="//www.googleadservices.com/pagead/conversion.js"></script> <noscript> <div style="display:inline;"> <img alt="" src="//googleads.g.doubleclick.net/pagead/viewthroughconversion/833009905/?guid=ON&amp;script=0" style="width:1px;height:1px;border:0 none;"/> 
    </div></noscript> 
    </body>
</html>
~~~

この長いテキストファイルを眺めて，必要な情報の特徴や抽出の条件を見つけ出します．



# 2. 必要な部分を抽出
## 2.1 情報の確認
Webページから，分析に利用したい情報を探します．
![review.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3725285/91fabfdb-2e14-596d-e9cc-f9d7a5d96f5b.jpeg)
四角で囲われている部分が基本的な情報です．リンクなども含めて９種類に注目しました．

:::note
【基本情報】
1. ユーザー名
1. ユーザーID：アイコンや名前に紐付いている番号で画像では非表示
1. 評価値
1. 日付
1. 投稿手段
1. レビューのタイトル
1. レビューのURL：レビュータイトルに紐づくURLで画像では非表示
1. レビュー
1. 鑑賞方法
:::

この記事では，次の４種類を取得してみたいと思います．ユーザー名は同一なのですが，ユーザーIDが異なる状況があったので，ユーザーIDを使います．

:::note
【**抽出情報**】
1. ユーザーID：アイコンや名前に紐付いている番号
1. 評価値
1. レビューのタイトル
1. レビュー
:::

## 2.2 HTMLの確認
取得したHTMLのテキストを眺めて，必要な情報がどのような形で配置されているのかを判断します．少し長めですが，２人分を掲載しました．
~~~html: 取得したテキスト (soup) の抜粋
...前略...
<div class="user-review pro" data-review-user="498613">
    <div class="user-review-inner">
        <h2 class="review-title"> <span class="rating-star val45">4.5</span><a href="/movie/50969/review/02316391/">最後に語られる"フレンドシップ(友情)"の深い意味</a> </h2>
        <div class="review-data">
            <div class="user"> <a class="user-name" href="/user/498613/" rel="nofollow">清藤秀人</a><span class="user-name-title">さん</span> <a class="follow-btn hidden" data-google-interstitial="false" data-method="post" data-remote="true" href="/user/498613/follow/" rel="nofollow">フォロー</a> </div>
            <div class="time">2020年4月26日</div>
            <div class="post-device">PCから投稿</div>
            <div class="watch-methods">鑑賞方法：CS/BS/ケーブル</div>
            <div class="edit"> <a href="/movie/50969/review/form/" rel="nofollow">編集</a> </div>
        </div>
        <div class="pulldown violation-report margin-top24" data-review-user="498613"> <label class="icon more"> <input type="checkbox"/> <ul> <li><span class="icon flag"></span><a href="/help/contact/?report_target=2316391&amp;report_type=eiga_review">このレビューを報告する</a></li> </ul> </label> </div>
        <div class="txt-block">
            <p class="short">ヨーロッパ歴訪の過密スケジュールに辟易した某国王女アンが、...</p>
        </div>
        <div class="review-reaction">
            <div class="comment"> <a class="icon fukidashi" data-google-interstitial="false" data-remote="true" href="/movie/50969/review/02316391/comment/" rel="nofollow">コメントする</a> <span>（<strong>1</strong>件）</span> </div>
            <div class="empathy"> <a class="icon heart" data-google-interstitial="false" data-method="post" data-remote="true" href="/movie/50969/review/02316391/vote/" id="review_2316391_vote" rel="nofollow">共感した！</a> <span>（<strong id="review_2316391_votecnt">19</strong>件）</span> </div>
        </div>
        <div class="review-bookmarks">
            <ul class="bookmarks">
                <li> <a class="twitter-share-button twitter-share-button-rendered twitter-tweet-button" data-count="horizontal" data-counturl="https://eiga.com/movie/50969/review/02316391/" data-lang="ja" data-text="清藤秀人さんの映画「ローマの休日」レビュー（感想・評価）をシェア　☆4.5 #映画" data-url="https://eiga.com/movie/50969/review/02316391/" href="https://twitter.com/share" rel="nofollow">Xでつぶやく</a> </li>
                <li class="facebook"><a href="http://www.facebook.com/share.php?u=https%3A%2F%2Feiga.com%2Fmovie%2F50969%2Freview%2F02316391%2F" rel="nofollow" target="_blank">シェア</a></li>
            </ul>
        </div>
    </div>
    <div class="review-user">
        <a href="/user/498613/" rel="nofollow"><img alt="清藤秀人" class="img-circle" height="68" loading="lazy" src="https://eiga.k-img.com/dbimages/profile/498613/photo_1475737154.jpg" width="68" /></a>
    </div>
</div>


<div class="user-review" data-review-user="927905">
    <div class="user-review-inner">
        <h2 class="review-title"> <span class="rating-star val45">4.5</span><a href="/movie/50969/review/03736125/">名作と呼ばれるに相応しい！！</a> </h2>
        <div class="review-data">
            <div class="user"> <a class="user-name" href="/user/927905/" rel="nofollow">とも</a><span class="user-name-title">さん</span> <a class="follow-btn hidden" data-google-interstitial="false" data-method="post" data-remote="true" href="/user/927905/follow/" rel="nofollow">フォロー</a> </div>
            <div class="time">2024年4月20日</div>
            <div class="post-device">iPhoneアプリから投稿</div>
            <div class="watch-methods">鑑賞方法：VOD</div>
            <div class="edit"> <a href="/movie/50969/review/form/" rel="nofollow">編集</a> </div>
        </div>
        <div class="pulldown violation-report margin-top24" data-review-user="927905"> <label class="icon more"> <input type="checkbox"/> <ul> <li><span class="icon flag"></span><a href="/help/contact/?report_target=3736125&amp;report_type=eiga_review">このレビューを報告する</a></li> </ul> </label> </div>
        <div class="txt-block">
            <p class="short">初っ端の、｢thank you♡｣と一人一人にご挨拶をするシーンで...</p>
        </div>
        <div class="review-reaction">
            <div class="comment"> <a class="icon fukidashi" data-google-interstitial="false" data-remote="true" href="/movie/50969/review/03736125/comment/" rel="nofollow">コメントする</a> <span>（0件）</span> </div>
            <div class="empathy"> <a class="icon heart" data-google-interstitial="false" data-method="post" data-remote="true" href="/movie/50969/review/03736125/vote/" id="review_3736125_vote" rel="nofollow">共感した！</a> <span>（<strong id="review_3736125_votecnt">2</strong>件）</span> </div>
        </div>
        <div class="review-bookmarks">
            <ul class="bookmarks">
                <li> <a class="twitter-share-button twitter-share-button-rendered twitter-tweet-button" data-count="horizontal" data-counturl="https://eiga.com/movie/50969/review/03736125/" data-lang="ja" data-text="ともさんの映画「ローマの休日」レビュー（感想・評価）をシェア　☆4.5 #映画" data-url="https://eiga.com/movie/50969/review/03736125/" href="https://twitter.com/share" rel="nofollow">Xでつぶやく</a> </li>
                <li class="facebook"><a href="http://www.facebook.com/share.php?u=https%3A%2F%2Feiga.com%2Fmovie%2F50969%2Freview%2F03736125%2F" rel="nofollow" target="_blank">シェア</a></li>
            </ul>
        </div>
    </div>
    <div class="review-user">
        <a href="/user/927905/" rel="nofollow"><img alt="とも" class="img-circle" height="68" loading="lazy" src="https://eiga.k-img.com/gid/img/user/22609/798fde25131b05d7/160.jpg?1665368911" width="68" /></a>
    </div>
</div>

<div class="user-review" data-review-user="927905">
    <div class="user-review-inner">
...後略...
~~~
\<div class="user-review"...>がレビューの分割点になっていることがわかります．classには，user-reviewとuser-review proがあるようです．proはプロのレビューワーという意味なのでしょうか？user-review proには，空白があるので，user-reviewでうまく特徴を捕まえられそうです．

おおまかな方針は次の２点になります．
1. \<div class="user-review"...\>で始まるまとまりを，順番に取得する．
2. それぞれについて，ユーザーID，評価値などの部分を探す．

1番目については，divタグ，class属性の値がuser-reviewで探して，リストにすれば良いので，

:::note
review_elements = soup.find_all('div', class_='user-review')
:::

と記述できます．

2番目については，取得したリストの要素各々について，タグや属性を利用して抽出対象を探していきます．以下では，review_elementsリストの代表的要素をreview_elementと表記します．

### ユーザーID
ユーザーIDの部分は，HTMLの確認で見てわかるように，
~~~
<div class="user-review" data-review-user="111などのユーザーID">
~~~
という形をしています．ユーザーIDは，data-review-userの値であることがわかります．review_elementのdata-review-userの値なので，

:::note
user_id = review_element['data-review-user']
:::
によって値を取得できます．```[　]```を利用して値を知ることができます．

### 評価値
評価値を表す部分は，
~~~
<span class="rating-star val45">4.5</span>
~~~
となっています．評価値は，spanタグ，class属性の値がrating-starのテキスト部分に相当することがわかります．review_elementの中身をspan, rating-starで探して，テキスト部分を抽出すれば良いので，

:::note
rating_element = review_element.find('span', class_='rating-star')
rating = rating_element.text
:::

と書くことができます．1行目の```find```で検索，2行目の```.text```でテキスト部分の抽出を表しています．


### レビューのタイトル
レビュータイトルを表す部分は，若干面倒になります．
~~~
<h2 class="review-title">
<span class="rating-star val45">4.5</span>
<a href="/movie/50969/review/03736125/">名作と呼ばれるに相応しい！！</a>
</h2>
~~~

レビューのタイトルは，h2タグ，class属性の値がreview-titleとなるテキスト部分に相当することがわかります．しかし，テキスト部分をすべて抽出すると，評価値も合わせて抽出されるので，数値部分を取り除く作業が必要となります．他の方法としては，h2タグ，review-titleの部分を探した後に，\<a href="/movie/50969/review/03736125/">名作と呼ばれるに相応しい！！\</a> を利用して，テキスト部分のみを抽出しても良さそうです．評価値をすでに，ratingとして抽出してあるので，これを利用して，数値部分を削除する方法で試したものが下記となります．

:::note
title_element = review_element.find('h2', class_='review-title')
title = title_element.text.replace(rating, '').strip()
:::


### レビュー
いよいよ本題？で重要な部分です．レビュー本体は，
~~~
<p class="short">ヨーロッパ歴訪の過密スケジュールに辟易した某国王女アンが、...</p>
~~~
という形をしています．レビューは，pタグ，class属性の値がshortのテキスト部分に相当することがわかります．

pタグ，short属性でテキスト部分を抽出すれば良いので，

:::note
review_text_element = review_element.find('p', class_='short')
review = review_text_element.text.strip() 
:::
と書くことができます．


### レビュー（ネタバレレビューの扱い）
レビュー本文について，ネタバレの扱いだとWebページ上表示されないようになっています．ネタバレ内容を含むレビューは，pタグ，class属性の値がhiddenとして表記されています．

:::note
hidden_review_text_element = review_element.find('p', class_='hidden')
review = hidden_review_text_element.text.strip() 
:::
としてレビューを取得することができます．


## 2.3 プログラム
これまでの一連の流れをまとめると，次のようなコードとなります．評価値やレビュー，レビュータイトルが存在しないこともあるので，その場合は，Noneとなるように調整しています．time.sleepで小休止も追加しました．appendの部分をprintに変更すれば，取得しているデータを表示させることができます．

~~~python: 「ローマの休日」レビュー取得
import requests
from bs4 import BeautifulSoup
import time

url = "https://eiga.com/movie/50969/review/" # ローマの休日
headers = {"User-Agent": "Mozilla/5.0"}

response = requests.get(url, headers=headers)
soup = BeautifulSoup(response.text, "lxml")

# 全てのレビュー要素を取得する
# review_elementsはリストになる
review_elements = soup.find_all('div', class_='user-review')

# 空のリストを用意
# このリストに取得した値を追加していきます
user_ids = []   # ユーザーID
ratings = []    # 評価値
titles = []     # レビューのタイトル
reviews = []    # レビュー

# 各レビューの情報を取得する
for review_element in review_elements:
    # ユーザーID
    user_id = review_element['data-review-user']
    
    # 評価の数値を取得する
    rating_element = review_element.find('span', class_='rating-star')
    rating = rating_element.text if rating_element else None

    # レビュータイトルを取得する
    title_element = review_element.find('h2', class_='review-title')
    title = title_element.text.replace(rating, '').strip() if title_element else None

    # レビュー本文を取得する
    review_text_element = review_element.find('p', class_='short')
    # ネタバレを含むレビューを入れる場合
    hidden_review_text_element = review_element.find('p', class_='hidden')
    if review_text_element:
        review = review_text_element.text.strip() 
    elif hidden_review_text_element:
        review = hidden_review_text_element.text.strip() 
    else:
        review = None

        
    # 取得した情報をリストに追加
    user_ids.append(user_id)
    ratings.append(rating)
    titles.append(title)
    reviews.append(review)
    
    time.sleep(1)
~~~

# 3. 次のページへ
レビューの件数が多いと，下図のように１ページには収まりきらず次のページが発生します．
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3725285/d05d07bd-f8b6-b71f-660b-721ad08d9162.png" width=60% alt="page">
力技で次のページのURLを探し出して，スクレイピングしてもよいのですが，今回は実際に次のページをクリックしてURLの特徴に注目して手動でしのぎます[^1]．

:::note
1ページ目
https://eiga.com/movie/50969/review/

2ページ目
https://eiga.com/movie/50969/review/all/2/

3ページ目
https://eiga.com/movie/50969/review/all/3/
...
10ページ目
https://eiga.com/movie/50969/review/all/10/
:::

２ページ目以降は，/all/ページ番号/という形を追加するという規則になっていることがわかります[^2]．全てのページについて，2.3のプログラムを動かすことで評価値やレビューの一覧を取得できることがわかります．１ページ最大20件のレビューが掲載されているので，全レビュー件数を20で割り算してページ数を求め，URLのリストを事前に作成してしまいます．全レビュー数が191件で各ページ20件のレビューなので，10ページまでレビューのページが作られます．本質的な内容からそれるので，今回は手動でリストを作成しましょう:sweat_smile:

~~~python: ページ追記とURLリスト
base_url = "https://eiga.com/movie/50969/review/" # ローマの休日"
url_list = [base_url]

# /all/2 から/all/10 までを追記
for i in range(2, 11):
    url_list.append(base_url+"all/"+str(i))
~~~
このような形で，url_listに取得したいレビューのページのリストを作成しておきます．


# 4. CSVファイルで保存
pandasのデータフレームに変換後，CSVファイルとして保存します．一旦，2.3のプログラムに戻ります．取得した情報をリストへ追加していく作業まで終わりました．リストをpandasのデータフレームに変換して，CSVファイルとして保存すれば作業は終わりとなります．

~~~python: CSVファイルへ
    # 取得した情報をリストに追加
    user_ids.append(user_id)
    ratings.append(rating)
    titles.append(title)
    reviews.append(review)
    time.sleep(1)
    
#----ここから新しい内容----

# リストをPandasのDataFrameに変換
df = pd.DataFrame({
    'user_id'  : user_ids,
    'rating': ratings,
    'title': titles,
    'review': reviews
})

# csvファイルとして保存
df.to_csv("RomanHoliday.csv", index=False)   
~~~
保存したCSVファイルを表示させたものが次の図となります．次の図はjupyter labでCSVファイルを開いた状態です．
![table.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3725285/f3c56c72-4319-e424-89c5-efdaaab718ca.jpeg)

# 5. プログラム全体
最後に2.3のプログラムに，次のページ，CSVファイル保存を追加したプログラムを掲載して終わりにします．

~~~python: 「ローマの休日」レビュー取得
import requests
from bs4 import BeautifulSoup
import time
import pandas as pd


filename = "./RomanHoliday.csv"  # 保存するCSVファイル名
base_url = "https://eiga.com/movie/50969/review/" # ローマの休日

url_list = [base_url]
headers = {"User-Agent": "Mozilla/5.0"}

# 次のページのURL
# /all/2 から/all/10 までを追記
for i in range(2, 11):
    url_list.append(base_url+"all/"+str(i))


# 空のリストを用意
user_ids = []   # ユーザーID
ratings = []    # 評価値
titles = []     # レビューのタイトル
reviews = []    # レビュー

# 各URLについてレビューなどを取得    
for url in url_list:    
    response = requests.get(url, headers=headers)
    soup = BeautifulSoup(response.text, "lxml")

    # 全てのレビュー要素を取得する
    review_elements = soup.find_all('div', class_='user-review')


    # 各レビューの情報を取得する
    for review_element in review_elements:
        # ユーザーID
        user_id = review_element['data-review-user']
        
        # 評価の数値を取得する
        rating_element = review_element.find('span', class_='rating-star')
        rating = rating_element.text if rating_element else None
    
        # レビュータイトルを取得する
        title_element = review_element.find('h2', class_='review-title')
        title = title_element.text.replace(rating, '').strip() if title_element else None
    
        # レビュー本文を取得する
        review_text_element = review_element.find('p', class_='short')
        hidden_review_text_element = review_element.find('p', class_='hidden')  # ネタバレを含むレビューを入れる場合
        if review_text_element:
            review = review_text_element.text.strip() 
        elif hidden_review_text_element:
            review = hidden_review_text_element.text.strip() 
        else:
            review = None


        # 取得した情報をリストに追加
        user_ids.append(user_id)
        ratings.append(rating)
        titles.append(title)
        reviews.append(review)

    print(f"{url}まで終了")
    time.sleep(2)


#---------------------------------------------------------------------
# csvファイルとして保存
# リストをPandasのDataFrameに変換
df = pd.DataFrame({
    'user_id'  : user_ids,
    'rating': ratings,
    'title': titles,
    'review': reviews,
})

df.to_csv(filename, index=False)
~~~


# その他
スマートフォンのアプリレビューを取得する記事を紹介したことがあるので，拙稿ですが参考にしていていただけると幸いです（硬すぎ）．

https://qiita.com/AzukiImo/items/c1ce508faac6c75aef67

# 注
[^1]: 映画レビュー・評価値のスクレイピング２ではこのあたりも自動化する予定です．
[^2]: １ページ目だけは，/all/1/とならない点に注意が必要です．