毎日テキストマイニング

180日間、毎日テキストマイニングをするブログです

2018/8/18【56日目】Amazoレビューで口コミ分析をしてみる

『仕事に使えるクチコミ分析[テキストマイニング統計学マーケティングに活用する]』を拝読いたしました。

f:id:rimt:20180819083054j:plain

本書の中でAmazonのレビューを使って、クチコミ分析を行う、という解説が面白かったので、Pythonで実践していきたいと思います。

今回のターゲット

今回はAK48のシングル全53曲を見てみたいと思います。 リストは次の通りです。

取得するためのコードを作る

いつも通りテキストの取得にはScrapyを使いたいと思います。 Scrapyの使い方はこちら。

dailytextmining.hatenablog.com

シェル上で取得できるテキストを確認していきます。 まずはレビューのテキストから取得していきます。

text = response.xpath('//*[@class="a-size-base ○○"]').extract()

元のAmazonのソースがこんな感じなので、テキストだけを取得すると、改行を挟むごとにリストの要素が増えてしまうようです。

元のテキスト。

“中西里菜が、好きだったら買うべき。彼女の”
<br>
“過去を知ることができる良い作品。”

取得するとこんな感じになります。

['中西里菜が、好きだったら買うべき。彼女の',
 '過去を知ることができる良い作品。',

要素がバラバラになってしまい、これだと使いづらそうなので、タグごと取得してみます。

text = response.xpath('//span[@class="a-size-base ○○"]').extract()
text
'[\'"<span data-hook=review-body class=a-size-base ○○t>中西里菜が、好きだったら買うべき。彼女の<br>過去を知ることができる良い作品。</span>"'

これを47日目で学んだBeautifull Soupでタグを取り除けば良さそうです。

dailytextmining.hatenablog.com

from bs4 import BeautifulSoup

bt = "<span data-hook=review-body class=a-size-base ○○>中西里菜が、好きだったら買うべき。彼女の<br>過去を知ることができる良い作品。</span>"
example = BeautifulSoup(bt)
print(example.get_text())

こんな感じで綺麗にテキストだけ取得できます。

中西里菜が、好きだったら買うべき。彼女の過去を知ることができる良い作品。

レビューのタイトルは簡単に取得できます。

text = response.xpath('//*[@class="a-size-base a-link-normal ○○"]/text()').extract()

実行結果。

'会いたかった!', '人それぞれ。', '神曲の1つ'

星の数はそのまま取得して、あとで処置したいと思います。

text = response.xpath('//*[@class="a-icon a-icon-star ○○"]//span/text()').extract()

こんな感じです。

'5つ星のうち5.0'

どの曲かわからなくなると困るので、曲のタイトルも取得します。

text = response.xpath('//*[@class="a-size-large ○○"]//a/text()').extract()
[‘会いたかった']

自動的に次のページに行くにはこちらですね。

review_pages = response.xpath('//*[@class="a-○○"]//a/@href').extract()

これで、とりあえず欲しいデータは取得できそうです。

実装

spiderに落とし込んでいきます。 その前にデータベースでテーブルを次のように作っておきます。

create database akb_amazon;
use akb_amazon;

create table songs(
id INT AUTO_INCREMENT NOT NULL PRIMARY KEY,
song_title varchar(60) NOT NULL,
star varchar(60) NOT NULL,
review_title varchar(60) UNIQUE NOT NULL,
review text,
datesp DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

spiderはほぼ42日目と同じです。

dailytextmining.hatenablog.com

上記のxpath追記して実行してみます。

$ scrapy crawl amazon_akb

実行結果。

id,song_title,star,review_title,review,datesp
1,['会いたかった'],"['5つ星のうち5.0', '5つ星のうち5.0', '5つ星のうち5.0', '5つ星のうち5.0', '5つ星のうち","['買うべき。', '会いたかった です...(⌒-⌒) / ニコニコ...', 'この曲が一番好きです', '子供のプ","['<span data-hook="review-body"(以下、省略)

Oh, 全部のデータが1つのリストに入っているかなり使えなさそうなデータができました。 ちょっとpandasで読み込んでみます。

    id,song_title,star,review_title,review,datesp
0   1,['会いたかった'],"['5つ星のうち5.0', '5つ星のうち5.0', '5つ星の...
1   2,['会いたかった'],"['5つ星のうち5.0', '5つ星のうち5.0', '5つ星の...
2   3,['会いたかった'],"['5つ星のうち5.0', '5つ星のうち5.0']","['元...

分析に全く使えなさそうですね。

とは言え、テキストは取得できていますので、方法としてはリストとして取得した要素を1つずつitemに入れれば良さそうです。 レビュータイトル・星の数・レビューテキストは全て同じ要素数のはずなので、49日目で学んだforの書き方が使えそうです。

for cluster in range(0,20):

これですね。これを参考に次のように書き直します。

        song_title = response.xpath('//*[@class="a-size-large ○○"]//a/text()').extract()
        star = response.xpath('//i[@data-hook="review-star-rating"]//text()').extract()
        review_title = response.xpath('//a[@class="a-size-base ○○"]/text()').extract()
        review = response.xpath('//span[@class="a-size-○○"]').extract()

        for i in range(0, len(review_title)):
            item['song_title'] = str(song_title)
            item['star'] = star[i]
            item['review_title'] = review_title[i]
            item['review'] = review[i]

            items.append(item)
            yield item

まず、テキスト要素をリストに代入し、[i]の場所だけをループにしてitemに代入します。 それで次のページに行く前にDBに保存する必要がありますので、forの中にsql文を入れればできるはずです。結局こんな感じになりました。

    def parse(self, response):
        sel = Selector(response)
        items = []
        item = AmazonItem()

        song_title = response.xpath('//*[@class="a-size-large○○"]//a/text()').extract()
        star = response.xpath('//i[@data-hook="review-star-rating"]//text()').extract()
        review_title = response.xpath('//a[@class="a-size-base ○○"]/text()').extract()
        review = response.xpath('//span[@class="a-size-base ○○"]').extract()

        for i in range(0, len(review_title)):
            item['song_title'] = str(song_title)
            item['star'] = star[i]
            item['review_title'] = review_title[i]
            item['review'] = review[i]

            items.append(item)
            yield item
            connect = {
                'user': ‘xxxxx’,
                'password': ‘xxxxxx’,
                'host': ‘xxxxxxxx’,
                'database': 'akb_amazon',
            }

            db=mysql.connector.connect(**connect)
            crsr = db.cursor()

            db=mysql.connector.connect(**connect)
            crsr = db.cursor()
            sql =  (
            "INSERT IGNORE INTO songs (song_title, star, review_title, review)"
            "VALUES (%s, %s, %s, %s)"
            )
            data = (item['song_title'], item['star'], item['review_title'], item['review'])
            crsr.execute('SET NAMES utf8mb4')
            crsr.execute("SET CHARACTER SET utf8mb4")
            crsr.execute("SET character_set_connection=utf8mb4")
            crsr.execute(sql, data)
            db.commit()
            crsr.close()

        review_pages = response.xpath('//*[@class="a-○○"]//a/@href').extract() #response.css('li.next a::attr(href)').extract_first()
        for review_page in review_pages:
            if review_page is not None:
                review_page = response.urljoin(review_page)
                yield scrapy.Request(review_page, callback=self.parse)

これで、DBにテキストをきれいに保存できました。DBから出力したcsvをpandasで読み込むとこんな感じになりました。

 id  song_title  star    review_title    review  datesp
0   66  ['49th Single「#好きなんだ」【Type D】通常盤']  5つ星のうち4.0 良かったです。   <span data-hook=review-body" class="a-size-bas...    2018-08-18 01:36:12
1   67  ['49th Single「#好きなんだ」【Type D】通常盤']  5つ星のうち5.0 このシングルも良いですね。 <span data-hook=review-body" class="a-size-bas...    2018-08-18 01:36:12

これで、分析ができそうです。 明日はレビューを集めて分析をしたいと思います。

今日の結果

今日のAKBメンバーによる呟きは67件でした。 f:id:rimt:20180819083842p:plain

{'楽しい': 6, '嬉しい': 5, '可愛い': 4, 'ない': 3, 'すごい': 3, 'よい': 2, '幅広い': 2, '面白い': 2, 'いい': 2, 'やばい': 2, '素晴らしい': 1, 'くい': 1, 'いたい': 1, '良い': 1, '近い': 1, '早い': 1, '甘い': 1, '大きい': 1})
'アクシュカイ': 12, '公演': 11, '日': 10, 'さん': 10, 'ちゃん': 9, '中': 8, '方': 8, 'ん': 7, '月': 7, '楽しい': 6, '笑': 6, '人': 6, 'お願い': 6, 'センチメンタルトレイン': 6, '受付': 6, '写真': 6, '皆さん': 6, 'よう': 6,
'する': 35, 'なる': 18, 'いる': 15, 'くださる': 13, 'アクシュカイ': 12, 'てる': 12, '公演': 11, '日': 10, 'さん': 10, 'ちゃん': 9, '中': 8, '方': 8, '来る': 8, 'ん': 7, '月': 7, 'くる': 7, 'くれる': 7, '会う': 7, 'みる': 7,