2
5

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

Posted at

概要

映画のレビューや評価値をPythonのBeautifulSoupライブラリを利用して取得する方法を紹介します.推薦システムを練習するためのデータ収集やレビューと評価値の分析の練習に利用できると思います.最終的に,下図のような表形式のデータセットの取得が目的となります.
table.jpg

【紹介項目】

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

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

BeautifuSoupの基本的な使い方は解説しません.公式のサイトや様々な解説ページ,スクレイピングの書籍を参考にしてください.

1. Webページのデータを取得

オードリーの永遠の名作である「ローマの休日」を具体例としてユーザーレビューや評価値を抽出していきます.

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

「ローマの休日」スクレイピング
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ブラウザーでページのソースを表示させたときと同様,1行の長いテキストになります.

soupの表示1
<!DOCTYPE html> <html lang="ja"> <head> <meta http-equiv="x-ua-compatible" content="ie=edge"/>

わかりにくいので,コードを整形してくれるエディターか,オンラインのHTML Formatterを利用して見た目を綺麗にしてみます.

soupの表示2
<!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
四角で囲われている部分が基本的な情報です.リンクなども含めて9種類に注目しました.

【基本情報】

  1. ユーザー名
  2. ユーザーID:アイコンや名前に紐付いている番号で画像では非表示
  3. 評価値
  4. 日付
  5. 投稿手段
  6. レビューのタイトル
  7. レビューのURL:レビュータイトルに紐づくURLで画像では非表示
  8. レビュー
  9. 鑑賞方法

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

抽出情報

  1. ユーザーID:アイコンや名前に紐付いている番号
  2. 評価値
  3. レビューのタイトル
  4. レビュー

2.2 HTMLの確認

取得したHTMLのテキストを眺めて,必要な情報がどのような形で配置されているのかを判断します.少し長めですが,2人分を掲載しました.

取得したテキスト (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でうまく特徴を捕まえられそうです.

おおまかな方針は次の2点になります.

  1. <div class="user-review"...>で始まるまとまりを,順番に取得する.
  2. それぞれについて,ユーザーID,評価値などの部分を探す.

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

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の値なので,

user_id = review_element['data-review-user']

によって値を取得できます.[ ]を利用して値を知ることができます.

評価値

評価値を表す部分は,

<span class="rating-star val45">4.5</span>

となっています.評価値は,spanタグ,class属性の値がrating-starのテキスト部分に相当することがわかります.review_elementの中身をspan, rating-starで探して,テキスト部分を抽出すれば良いので,

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として抽出してあるので,これを利用して,数値部分を削除する方法で試したものが下記となります.

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属性でテキスト部分を抽出すれば良いので,

review_text_element = review_element.find('p', class_='short')
review = review_text_element.text.strip()

と書くことができます.

レビュー(ネタバレレビューの扱い)

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

hidden_review_text_element = review_element.find('p', class_='hidden')
review = hidden_review_text_element.text.strip()

としてレビューを取得することができます.

2.3 プログラム

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

「ローマの休日」レビュー取得
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. 次のページへ

レビューの件数が多いと,下図のように1ページには収まりきらず次のページが発生します.
page
力技で次のページのURLを探し出して,スクレイピングしてもよいのですが,今回は実際に次のページをクリックしてURLの特徴に注目して手動でしのぎます1

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

ページ追記と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ファイルとして保存すれば作業は終わりとなります.

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

5. プログラム全体

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

「ローマの休日」レビュー取得
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)

その他

スマートフォンのアプリレビューを取得する記事を紹介したことがあるので,拙稿ですが参考にしていていただけると幸いです(硬すぎ).

  1. 映画レビュー・評価値のスクレイピング2ではこのあたりも自動化する予定です.

  2. 1ページ目だけは,/all/1/とならない点に注意が必要です.

2
5
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
2
5