見出し画像

RによるAmazonレビューの収集とテキスト分析

2022/6/19
スクレイピングが可能であることを確認しました。
スクレイピングプログラムを英文でも取得できるよう改善しました。

●はじめに

Amazonで買い物をする際、レビューを参考にしない方はいないでしょう。
高評価であっても低評価であっても、レビューは貴重なユーザーの意見であり、情報の宝庫です。しかし大量にレビューがあると全て目を通すのには時間がかかりますし、高評価も低評価も様々で情報の整理は難しいですよね。もっと良い感じに要約、数値化できれば...ということで、今回はRを使ってAmazonレビューの収集と分析をしました。
スクレイピング禁止規定についてはこちらのサイトと同様の立場ですが...しっかりウェイトタイムを入れてAmazonさんのサーバーのご迷惑にならないよう、ログインせず自己責任でゆっくりやりましょう...。
(そしてお世話になったら買いましょう。)

当方WindowsですのでUTF-8のhtmlをスクレイピングすると文字化けしてしまいます。そのためDocker上のUbuntuでRStudioとSeleniumを使用し、Selenium経由でhtmlを取得する方式で収集しました。

以下の環境で実施しています。

> devtools::session_info()
─ Session info ─────────────────────────────────────────────────────────────────────────────────────────────────
setting  value                       
version  R version 4.0.5 (2021-03-31)
os       Ubuntu 20.04.2 LTS          
system   x86_64, linux-gnu           
ui       RStudio                     
language (EN)                        
collate  ja_JP.UTF-8                 
ctype    ja_JP.UTF-8                 
tz       Etc/UTC                     
date     2021-05-18                  

●収集

まずライブラリを読み込んでRSeleniumでchromeを開きます。
RSeleniumのVignette通りにやるとエラーが起きてしまいました。
どうやら私の環境ではremoteServerAddr = "localhost"でエラーが起こる様子。
remoteServerAddrにIPv4アドレスを指定してやることでエラーが解消できました。

rm(list=ls())
gc();  gc();

if (!require("pacman")) install.packages("pacman")
pacman::p_load(tidyverse, 
              RSelenium, 
              rvest,
              stringi,
              XML,
              openxlsx)

ip = "***.**.***.*"

remDr = remoteDriver(
 remoteServerAddr = ip,  # ipconfigでIPv4アドレスを確認しておく
 port = 4444, 
 browserName = "chrome"
)
remDr$open()

収集のコードはこちらを参考にさせていただきました。
変更点として、以下のような小アレンジをしています。
・RSeleniumでhtmlを取得するためにremDr$getPageSourceしたものをread.html()
・テキストの連続したスペースや\nを削除
・レビュー星数を数値データに出来るよう数字のみを抽出
・後から確認できるようurl、商品名、ブランドを取得
・外国語のレビューに切り替わった場合にclassが異なりエラーとなる点に対応

…等々。

scrape_amazon <- function(ASIN, page_num){
 url <- paste0("https://www.amazon.co.jp/product-reviews/",ASIN,"/?pageNumber=",page_num)
 remDr$navigate(url)
 Sys.sleep(5)
 doc <- remDr$getPageSource() %>%
   unlist() %>%
   read_html()
 
 # Review Title
 # There are two types of titles: with links and without links, so both are combined in the list.
 doc %>% 
   html_nodes("[class='a-size-base a-link-normal review-title a-color-base review-title-content a-text-bold']") %>%
   html_text() %>% 
   stri_replace_all(regex = "\n| +| +", replacement = " ") %>% 
   stri_replace_all(regex = "^ +", replacement = "") -> review_title1
 doc %>% 
   html_nodes("[class='a-size-base review-title a-color-base review-title-content a-text-bold']") %>%
   html_text() %>% 
   stri_replace_all(regex = "\n| +| +", replacement = " ") %>% 
   stri_replace_all(regex = "^ +", replacement = "") -> review_title2
 review_title <- append(review_title1, review_title2)
 
 # Review Text
 doc %>% 
   html_nodes("[class='a-size-base review-text review-text-content']") %>%
   html_text() %>% 
   stri_replace_all(regex = "\n| +| +", replacement = " ") %>% 
   stri_replace_all(regex = "^ +", replacement = "")  -> review_text
 
 # Number of stars in review
 # There are two types of stars, so both are combined in the list.
 doc %>%
   html_nodes("[data-hook='review-star-rating']") %>%
   html_text() %>% 
   stri_replace_all(regex = "\n| +| +|5つ星のうち", replacement = " ") %>% 
   as.numeric() -> review_star1
 doc %>%
   html_nodes("[data-hook='cmps-review-star-rating']") %>%
   html_text() %>% 
   stri_replace_all(regex = "\n| +| +|5つ星のうち", replacement = " ") %>% 
   as.numeric -> review_star2
 review_star <- append(review_star1, review_star2)
 
 # product_name
 doc %>% 
   html_node('[class="a-size-large a-text-ellipsis"]') %>%
   html_text() -> product_name
 
 # supplier
 doc %>% 
   html_node('[class="a-size-base a-link-normal"]') %>%
   html_text() -> supplier
 
 # Return a tibble
 tibble(review_title,
        review_text,
        review_star,
        page = page_num,
        page_url = url,
        product_name = product_name,
        supplier = supplier) %>% return()
}

今回は定番基礎化粧品「ニベア」のAmazonレビューを見てみます。
こちらは上のサイトのコードをそのまま使用させていただきました。
よりAmazon様に人の手のぬくもりっぽさを感じていただくためにSys.sleepによるウェイトタイムをrunifでランダム化してますが特に意味はありません。

ASIN <- "B00YQTWQB2" # ASIN
page_range <- 1:69 # scrape pages 1 to 69

match_key <- tibble(n = page_range,
                   key = sample(page_range,length(page_range)))

lapply(page_range, function(i){
 j <- match_key[match_key$n==i,]$key
 
 message("Getting page ",i, " of ",length(page_range), "; Actual: page ",j) # Progress bar
 
 Sys.sleep(runif(1, 2.5, 3.5)) # 3-second break
 
 if((i %% 3) == 0){ # After every three scrapes... take another two second break
   message("Taking a break...") # message on your console
   Sys.sleep(runif(1, 2, 3)) # Take an additional 2-3 second break
 }
 scrape_amazon(ASIN = ASIN, page_num = j) # Scrape
}) %>% 
 bind_rows() -> x

 
 # save data
 write.csv(x, paste(Sys.Date(), "amazon", ASIN, "reviewutf.csv", sep="_"), fileEncoding = "utf-8")
 
 # For windows, write.xlsx from the openxlsx package can be used to save the file in a good way without line shift.
 write.xlsx(x, paste(Sys.Date(), "amazon", ASIN, "review.xlsx", sep="_"), asTable = FALSE, overwrite = FALSE)

画像1

うまく取れました。

●レビューをテキスト分析する

レビューデータの面白いところはレビューの星数によって気に入った方と不満だった方を分けて分析できるところです。商品の利点と欠点を洗い出して購入の参考にしたり、企業であれば販促データや改善点の考察に使用したりできます。

まずreview_starを数値データにし、テキスト分析用にレビュータイトルと内容を結合します。

# ☆5 data
x %>%
   mutate(review_star = as.numeric(review_star)) %>% # 関数でも数値化してますが念のため数値化
   mutate(text = paste(x$review_title, x$review_text)) %>% 
   filter(review_star == 5) %>% 
   write.csv("nivea_5star_amazon.csv", fileEncoding = "SHIFT-JIS")
  
# <☆3 data
x %>%
   mutate(review_star = as.numeric(review_star)) %>%
   mutate(text = paste(x$review_title, x$review_text)) %>% 
   filter(review_star <= 3) %>% 
   write.csv("nivea_under3star_amazon.csv", fileEncoding = "SHIFT-JIS")  
  


全てをそのまま分析しても良いのですが、レビューで知りたい情報は「良かった点」と「悪かった点」ではないでしょうか。
レビューデータは評価の星数によって気に入った方と不満だった方を分けて分析できるため、良かった点と悪かった点を容易に可視化・要約することができます。

以前の記事でも使用しましたRMeCabとwordcloudも便利ですが、今回はテキスト分析用ソフトのKH_coderを使って☆5(気に入った方)と☆3以下(不満だった方)をそれぞれテキスト分析してみます。

・☆5-好評な方のレビュー(444件)

共起ネットワークにより、どんな風に使われているか、何を目的として使われているかが分かります。
最も出現頻度が多いワード、用途は冬の乾燥対策・乾燥肌ケアに化粧水と合わせて使うということが読み取れます。
・たくさん入っていてコスパが良い
・お風呂上りに使用
・ネットで話題になっていたため購入した
・肌荒れが改善
・ハンドクリームとして使用
・全身に使用
...などなど、良かった点や用途、効果などの様々な情報が読み取れます。
気に入られている方には広く支持されている様子です。

画像2


・☆3以下ー不満な方のレビュー(119件)

評価が低い方のレビューを分析することで、どういった点に不満が多いのかを要約することができます。

・伸ばしにくい
・肌に合わない人はかゆみやニキビが出る(?)
・配送に問題があって蓋が凹んだ
・そもそも近所の薬局に売ってるのでAmazonだと割高

...など、悪かった点が読み取れます。
当然ですが、肌に合わない方もいらっしゃる様子です。
説明書きの通り、まずは目立たないところで試してから使用した方が良いでしょう。
配送や価格等、商品自体以外の事も入ってきますので注意が必要です。

画像3

●レビューのワードクラウドを描く

もっと資料で使えそうな見た目インパクトのある図を作りたい...ということで、以前の記事でも行ったワードクラウドを描いてみます。
今回は高村先生の単語感情極性表を使用して、ネガティブなワードとポジティブなワードで色分けをしてみます。

library(RMeCab)
# タイトルとテキストを結合
text <- x %>%
    mutate(text = paste(x$review_title, x$review_text))
dat$text <- dat$text %>%
    str_replace_all(pattern = '\\p{ASCII}',replacement = "")
dat_text <- dat$text %>%
    na.omit() %>%
    iconv(from = "UTF-8", to = "CP932") %>%  # windowsのみEncodeの変更が必要です。
    paste(collapse = "")
textfile <- tempfile()
write(dat_text, textfile)
unlink(textfile)
cloud <- cloud %>%
    select(everything(), FREQ = starts_with("file")) %>% # 4列目のfile****....という名前が長いためFREQへ変更
    arrange(desc(FREQ))
# 消したい不要なワードを設定
exclude_word = c("する","なる","やる","ある","いる","①","②","③","-","♪","NA","NANA","NANANA","NANANANA","#","#")
# 動詞と名詞で良い感じのやつを残す
cloud2 <- cloud %>%
    filter(grepl(pattern = "動詞|名詞", x = POS1) &
    !grepl(pattern = "助動詞|代名詞", x = POS1) &
    !grepl(pattern = "非自立|接尾|数|代名詞", x = POS2)
    ) %>%
    filter(!TERM %in% exclude_word)

# 単語感情極性表の読み込み
sowdic <- read.table("http://www.lr.pi.titech.ac.jp/~takamura/pubs/pn_ja.dic",sep=":",col.names=c("term","kana","pos","value"),colClasses=c("character","character","factor","numeric"),fileEncoding="Shift_JIS")
# 品詞ごとにvalueが異なる場合があるのでくっつけて平均値をとる
sowdic2 <- aggregate(value~term+pos,sowdic,mean)
sowdic2$value <- sowdic2$value + 0.4 # デフォルトだとネガティブ寄りになるのでvalueを色を塗り分けたいところで調整
word <- subset(cloud2,TERM %in% sowdic2$term)
word <- merge(word,sowdic2,by.x=c("TERM","POS1"),by.y=c("term","pos"))
word2 <- word %>%
    mutate(color = if_else(value > -0.1, "skyblue", "salmon"))  # 色を塗り分けたいところでvalueを調整 もっとif_elseを重ねて塗分けてもいいかも
# 描画
word2 %>%
select(TERM, FREQ) %>%
mutate(FREQ = ((FREQ-mean(FREQ))/(sd(FREQ)))+1) %>%  # 文字の大きさのバランスを調整
arrange(desc(FREQ)) %>%
slice(1:150) %>% # 描画する範囲を設定
wordcloud2(color = word2$color, minRotation = 0, maxRotation = 0, size = 1)

画像4

ネガティブを赤、ポジティブを青で塗分けてみました。
単語感情極性表のー0.5以下で機械的に塗分けているので不思議な塗分けになっているところもありますが、valueの塗分け点を調整したり塗分け色数を増やせばもっと良い感じになりそうです。

●ポジティブな言葉とネガティブな言葉で出現頻度を棒グラフにする

ポジティブな言葉とネガティブな言葉の出現頻度を棒グラフにしてみます。

# ポジティブな言葉出現頻度(Value > 0.3)
p_word <- word2 %>%
filter(value>0.3) %>%
aggregate(FREQ~TERM+POS1, ., FUN=sum) %>%
arrange(desc(FREQ)) %>%
mutate(TERM = fct_reorder(TERM, -FREQ, .desc=TRUE)) %>%
slice(1:30) %>%
ggplot() +
geom_bar(aes(TERM,FREQ), stat = "identity", fill = "steelblue") +
coord_flip() +
ggplot2::theme_minimal(base_size = 16, base_family = "TPM")+
theme(panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
axis.text.y = element_text(face = c('bold'),
size = 14,
color = "steelblue")) +
labs(title = "明るい語句の出現頻度",
subtitle = "",
caption = "",
y = "出現頻度",
x = "語句")

# ネガティブな言葉出現頻度(Value < 0.1)
n_word <- word2 %>%
filter(value < -0.1) %>%
aggregate(FREQ~TERM+POS1, ., FUN=sum) %>%
arrange(desc(FREQ)) %>%
mutate(TERM = fct_reorder(TERM, -FREQ, .desc=TRUE)) %>%
slice(1:30) %>%
ggplot() +
geom_bar(aes(TERM,FREQ), stat = "identity", fill = "salmon") +
coord_flip() +
ggplot2::theme_minimal(base_size = 16, base_family = "TPM")+
theme(panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
axis.text.y = element_text(face = c('bold'),
size = 14,
color = "salmon")) +
labs(title = "暗い語句の出現頻度",
subtitle = "",
caption = paste("Amazonレビュー:データ取得日", Sys.Date()),
y = "出現頻度",
x = "語句")

install.packages("gridExtra")
gridExtra::grid.arrange(p_word, n_word, nrow = 1)

画像5

明るい語句としては効果、会う、満足、安心等があり、確かな効果や満足感、安心感が高く評価されていることが分かります。
暗い語句としては乾燥、冬、荒れるなど、ユーザーの使用動機や不具合が現れていることが分かります。
ポジティブな言葉とネガティブな言葉の出現頻度を見ることで、ユーザーが商品にどんな良い印象を持っているか、悪い印象を持っているかを可視化・要約でき、商品購入や商品分析の参考にすることができそうです。

●まとめ

今回はAmazonレビューの収集・分析を実施しました。
何百件もレビューがあると全てに目を通すことはできませんが、本方法であれば情報の要約や詳細の解析は容易です。
気を付けないといけないのは、レビューを残す方は全体の一部であること、商品に対して良かれ悪かれ大きな思い入れがある方のみであることです。
多くの方はレビューを残さず購入し使用している、または☆評価のみでレビュー文章は投稿していません。このため極端な意見のみが抽出される可能性があります。
また商品によってはサクラがレビューを投稿している場合もあります。評価が極端に偏っている、レビュー文章の日本語が一様であったり不自然な場合には注意が必要です。

しかしユーザー目線としての商品選択上、企業目線としての研究開発・マーケティング上、ユーザーの声が貴重であり有用であることは変わりありません。

使える情報は有意義に使って、楽しくお買い物・調査しましょう!

●参考

・【規約】Amazonのスクレイピングは本当に禁止?規約を確認してみた https://teshi-learn.com/2020-08/amazon-scraping-regulation/
・RSelenium: Basics https://rpubs.com/johndharrison/RSelenium-Basics
・Vignette: Scraping Amazon Reviews in R https://martinctc.github.io/blog/vignette-scraping-amazon-reviews-in-r/
・Docker-composeによるRStudio環境の構築と管理 https://qiita.com/g-k/items/42751a1839fbea25045f
・RSelenium のすヽめ https://qiita.com/TsuchiyaYutaro/items/5e0933eda59d67d4b39e
・KH Coder https://khcoder.net/