🔰 AWSとPythonで超名店鰻屋さんの口コミを分析してみた【Amazon Comprehend】

AWSのアカウントを取得したので、初心者にも分かりやすそうだった「Amazon Comprehend」を使用してみました！

**参考にさせていただいたQiita記事**

[・【初心者でも使える】AWSが提供する文書解析サービス「Amazon Comprehend」が日本語対応したので触ってみた！
@JJ33 さま](https://qiita.com/JJ33/items/eaa51a0f954889d04f4c/)
[・【Python】🍜ラーメンガチ勢によるガチ勢のための食べログスクレイピング🍜
@toshiyuki_tsutsui さま](https://qiita.com/toshiyuki_tsutsui/items/f143946944a428ed105b/)

**Amazon Comprehendとは**
>Amazon Comprehend は、機械学習を使用してテキスト内でインサイトや関係性を検出する自然言語処理 (NLP) サービスです。機械学習の経験は必要ありません。

[Amazon Comprehend（テキストのインサイトや関係性を検出）| AWS より](https://aws.amazon.com/jp/comprehend/)

##はじめに

###目的

食べログの評価が4.0以上である、超名店鰻屋さんの口コミを分析する。

<font color="DimGray">なぜ鰻屋さんにしたかというと、鰻が好きだからです笑
名店とされる鰻屋さんってあこがれますよね…！</font>

###方法
**口コミの取得**
Python：Beautiful Soupライブラリ
**口コミの分析**
AWS：Amazon Comprehend

##口コミの取得

###手順
PythonのBeautiful Soupライブラリ（HTMLやXMLから指定したデータを抽出するためのWEBスクレイピング用ライブラリ）を使用し、食べログの口コミを取得します。

###ソースコード
```python

import requests
from bs4 import BeautifulSoup
import re
import pandas as pd
import time

class Tabelog:
    
    def __init__(self, base_url, test_mode=False, p_ward='東京都内', begin_page=1, end_page=30):

        # 変数宣言
        self.store_id = ''
        self.store_id_num = 0
        self.store_name = ''
        self.score = 0
        self.ward = p_ward
        self.review_cnt = 0
        self.review = ''
        self.columns = ['store_id', 'store_name', 'score', 'ward', 'review_cnt', 'review']
        self.df = pd.DataFrame(columns=self.columns)
        self.__regexcomp = re.compile(r'\n|\s') # \nは改行、\sは空白

        page_num = begin_page # 店舗一覧ページ番号

        if test_mode:
            list_url = base_url + str(page_num) +  '/?Srt=D&SrtT=rt&sort_mode=1' #食べログの点数ランキングでソートする際に必要な処理
            self.scrape_list(list_url, mode=test_mode)
        else:
            while True:
                list_url = base_url + str(page_num) +  '/?Srt=D&SrtT=rt&sort_mode=1' #食べログの点数ランキングでソートする際に必要な処理
                if self.scrape_list(list_url, mode=test_mode) != True:
                    break

                # INパラメータまでのページ数データを取得する
                if page_num >= end_page:
                    break
                page_num += 1
        return

    def scrape_list(self, list_url, mode):
        """
        店舗一覧ページのパーシング
        """
        r = requests.get(list_url)
        if r.status_code != requests.codes.ok:
            return False

        soup = BeautifulSoup(r.content, 'html.parser')
        soup_a_list = soup.find_all('a', class_='list-rst__rst-name-target') # 店名一覧

        if len(soup_a_list) == 0:
            return False

        if mode:
            for soup_a in soup_a_list[:2]:
                item_url = soup_a.get('href') # 店の個別ページURLを取得
                self.store_id_num += 1
                self.scrape_item(item_url, mode)
        else:
            for soup_a in soup_a_list:
                item_url = soup_a.get('href') # 店の個別ページURLを取得
                self.store_id_num += 1
                self.scrape_item(item_url, mode)

        return True

    def scrape_item(self, item_url, mode):
        """
        個別店舗情報ページのパーシング
        """
        start = time.time()

        r = requests.get(item_url)
        if r.status_code != requests.codes.ok:
            print(f'error:not found{ item_url }')
            return

        soup = BeautifulSoup(r.content, 'html.parser')

        # 店舗名称取得
        store_name_tag = soup.find('h2', class_='display-name')
        store_name = store_name_tag.span.string
        print('{}→店名：{}'.format(self.store_id_num, store_name.strip()), end='')
        self.store_name = store_name.strip()

        # うなぎ屋以外の店舗は除外
        store_head = soup.find('div', class_='rdheader-subinfo') # 店舗情報のヘッダー枠データ取得
        store_head_list = store_head.find_all('dl')
        store_head_list = store_head_list[1].find_all('span')
        #print('ターゲット：', store_head_list[0].text)

        if store_head_list[0].text not in {'うなぎ','鰻'}:
            print('うなぎのお店ではないので処理対象外')
            self.store_id_num -= 1
            return

        # 評価点数取得
        rating_score_tag = soup.find('b', class_='c-rating__val')
        rating_score = rating_score_tag.span.string
        print('  評価点数：{}点'.format(rating_score), end='')
        self.score = rating_score

        # 評価点数が存在しない店舗は除外
        if rating_score == '-':
            print('  評価がないため処理対象外')
            self.store_id_num -= 1
            return
       # 評価が4.0未満店舗は除外
        if float(rating_score) < 4.0:
            print('  食べログ評価が4.0未満のため処理対象外')
            self.store_id_num -= 1
            return

        # レビュー一覧URL取得
        #<a class="mainnavi" href="https://tabelog.com/tokyo/A1304/A130401/13143442/dtlrvwlst/"><span>口コミ</span><span class="rstdtl-navi__total-count"><em>60</em></span></a>
        review_tag_id = soup.find('li', id="rdnavi-review")
        review_tag = review_tag_id.a.get('href')

        # レビュー件数取得
        print('  レビュー件数：{}'.format(review_tag_id.find('span', class_='rstdtl-navi__total-count').em.string), end='')
        self.review_cnt = review_tag_id.find('span', class_='rstdtl-navi__total-count').em.string

        # レビュー一覧ページ番号
        page_num = 1 #1ページ*20 = 20レビュー 。この数字を変えて取得するレビュー数を調整。

        # レビュー一覧ページから個別レビューページを読み込み、パーシング
        # 店舗の全レビューを取得すると、食べログの評価ごとにデータ件数の濃淡が発生してしまうため、
        # 取得するレビュー数は１ページ分としている（件数としては１ページ*20=２0レビュー）
        while True:
            review_url = review_tag + 'COND-0/smp1/?lc=0&rvw_part=all&PG=' + str(page_num)
            #print('\t口コミ一覧リンク：{}'.format(review_url))
            print(' . ' , end='') #LOG
            if self.scrape_review(review_url) != True:
                break
            if page_num >= 1:
                break
            page_num += 1

        process_time = time.time() - start
        print('  取得時間：{}'.format(process_time))

        return

    def scrape_review(self, review_url):
        """
        レビュー一覧ページのパーシング
        """
        r = requests.get(review_url)
        if r.status_code != requests.codes.ok:
            print(f'error:not found{ review_url }')
            return False

        # 各個人の口コミページ詳細へのリンクを取得する
        #<div class="rvw-item js-rvw-item-clickable-area" data-detail-url="/tokyo/A1304/A130401/13141542/dtlrvwlst/B408082636/?use_type=0&amp;smp=1">
        #</div>
        soup = BeautifulSoup(r.content, 'html.parser')
        review_url_list = soup.find_all('div', class_='rvw-item') # 口コミ詳細ページURL一覧

        if len(review_url_list) == 0:
            return False

        for url in review_url_list:
            review_detail_url = 'https://tabelog.com' + url.get('data-detail-url')
            #print('\t口コミURL：', review_detail_url)

            # 口コミのテキストを取得
            self.get_review_text(review_detail_url)

        return True

    def get_review_text(self, review_detail_url):
        """
        口コミ詳細ページをパーシング
        """
        r = requests.get(review_detail_url)
        if r.status_code != requests.codes.ok:
            print(f'error:not found{ review_detail_url }')
            return

        # ２回以上来訪してコメントしているユーザは最新の1件のみを採用
        soup = BeautifulSoup(r.content, 'html.parser')
        review = soup.find_all('div', class_='rvw-item__rvw-comment')#reviewが含まれているタグの中身をすべて取得
        if len(review) == 0:
            review = ''
        else:
            review = review[0].p.text.strip() # strip()は改行コードを除外する関数

        #print('\t\t口コミテキスト：', review)
        self.review = review

        # データフレームの生成
        self.make_df()
        return

    def make_df(self):
        self.store_id = str(self.store_id_num).zfill(8) #0パディング
        se = pd.Series([self.store_id, self.store_name, self.score, self.ward, self.review_cnt, self.review], self.columns) # 行を作成
        self.df = self.df.append(se, self.columns) # データフレームに行を追加
        return

       
unagi_review = Tabelog(base_url="https://tabelog.com/tokyo/rstLst/unagi/",test_mode=False, p_ward='東京都内')



#出力結果をCSVに保存
unagi_review.df.to_csv("output/unagi_reveiw.csv")
```

###口コミが取得された鰻屋さん
・うなぎ魚政
・かぶと
・活鰻の店 つぐみ庵

###取得した口コミの一部（うなぎ魚政）
![抽出した口コミ_うなぎ魚政.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/921847/c21486d7-26d5-b582-5c34-1838d5983f33.png)
<font color="DimGray">（文章以外の不要な情報は削除しました。）</font>

##口コミの分析（うなぎ魚政）
###手順
**①Amazon Comprehendサービスにアクセス**

**②サイドバーから「Real-time analysis」を選択**

**③「input text」に口コミを入力→「Analyze」を押下**
![3_「input text」に口コミを入力→「Analyze」を押下_うなぎ魚政.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/921847/a884b569-deed-3701-d791-4c22011986fd.png)
<font color="DimGray">（「テキストデータは5000バイト未満」という制限があるため、オーバーする分は削除し、貼り付けました。）</font>

**④結果が表示される**
![4_結果_うなぎ魚政.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/921847/a7543df4-f517-acfc-bec2-af73007982bd.png)

##口コミの分析結果（うなぎ魚政）
うなぎ魚政さんを例に、口コミの分析結果を紹介します。

###・Key phrases（キーフレーズ抽出）

>キーフレーズ抽出 API は、キーフレーズまたは会話のポイント、およびそれがキーフレーズであることを裏付ける信頼性スコアを返します。

![キーフレーズ抽出2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/921847/fe4e9bd6-d201-0a45-5acb-9378a4cffbfd.png)

###・Sentiment（感情分析）

>感情分析 API は、テキストの全体的な感情 (肯定的、否定的、中立的、または混在) を返します。

![感情分析2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/921847/d2d4a387-460d-cd5e-8d0d-e5216070da1d.png)

**Neutral：中立的
Positive：肯定的
Negative：否定的
Mixed：混在**

<font color="DimGray">肯定的のスコアが0.99ってすごいですね！さすがです…。</font>

###・Entities（エンティティ認識）

>エンティティ認識 API は、提供されたテキストに基づいて自動的に分類される、名前付きエンティティ (「人」、「場所」、「位置」など) を返します。

![エンティティ認識2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/921847/6858cb10-a286-d059-81ad-d630c5cf28b7.png)

<font color="DimGray">「坂東太郎」というのは、養殖うなぎのブランド名だそうです。
人の名前として認識されていますね笑</font>

###・Language（言語検出）

>言語検出 API は、100 を超える言語で書かれたテキストを自動的に識別し、主要言語と、言語が主要であることを裏付ける信頼性スコアを返します。

![言語検出2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/921847/727e73de-6505-dbc1-d4f0-2d2aaa4ec72b.png)

<font color="DimGray">もちろんJapaneseですね。</font>

##分析結果の比較（キーフレーズ抽出）

口コミ分析結果を、キーフレーズ抽出を例に比較します。

###・うなぎ魚政
![うなぎ魚政_キーフレーズ抽出.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/921847/4ebd731e-9563-9f97-a5d8-e2753294a91e.png)

###・かぶと
![かぶと_キーフレーズ抽出.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/921847/0a8ad671-a5d7-d5c9-7a64-61421fd93745.png)

###・活鰻の店 つぐみ庵
![活鰻の店つぐみ庵__キーフレーズ抽出.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/921847/d0e0e92c-d559-3474-7d29-10810e50609d.png)

##最後に
今回使用した「Real-time analysis」では、「テキストデータは5000バイト未満」という制限がありました。
次は、「Analysis jobs」（ファイルを指定して文章を分析できる）を使用し、長い文章の分析に挑戦してみます！

<font color="DimGray">本当は今回も「Analysis jobs」でファイルを指定した分析を行いたかったのですが、
**Amazon S3に取得したCSVファイルをアップロードし、Amazon Comprehendで参照→実行**した結果のファイル内容がよく分からず、断念しました…。（恐らく失敗）
次は、（きちんと結果がでるように）「Analysis jobs」を使用し、長い文章の分析に挑戦してみます…！</font>

<font color="DimGray">また、不手際も多かったのですが、初心者ゆえご容赦いただけますと幸いです:pray:</font>