データ分析
2024/01/29
上野 桃香

じゃらんの口コミをスクレイピング&可視化してみた

じゃらんの口コミをスクレイピング&可視化してみた

はじめに

じゃらんは口コミのスクレイピングは許可を出しています。(2024/01/18時点)


なので練習がてらにスクレイピングをしてみましょう!


私はBeautifulSoupを用いてスクレイピングしました。雑なコードなのですみません。


Streamlitで可視化した記事はこちら↓


blog.since2020.jp/data_analysis/google-colabからstreamlitでじゃらんの口コミ分析結果を可視化/

じゃらんの口コミをスクレイピング



from bs4 import BeautifulSoup
import urllib
import pandas as pd
import requests

html = requests.get('https://www.jalan.net/yad382598/kuchikomi/?screenId=UWW3001&yadNo=382598&smlCd=136802&distCd=01&ccnt=lean-kuchikomi-link-2')
soup = BeautifulSoup(html.content,'html.parser')

url_list = ['https://www.jalan.net/yad382598/kuchikomi/?screenId=UWW3001&yadNo=382598&smlCd=136802&distCd=01&ccnt=lean-kuchikomi-link-2']

# 各 URL から取得したデータを格納するためのリスト
post_body_text = []
lead_text = []
plan_info_text = []
post_date_text = []
rate_list_text = []
hotel_name_text = []
integrated_rate_text = []
cat_table_text = []
c_label_text = []

for row in url_list:
row = str(row)
html = urllib.request.urlopen(row)
soup = BeautifulSoup(html, 'html.parser')

# 各要素を抽出
post_body = soup.find_all("p", class_='jlnpc-kuchikomiCassette__postBody')
lead = soup.find_all("p", class_='jlnpc-kuchikomiCassette__lead')
plan_info = soup.find_all("dl", class_='jlnpc-kuchikomiCassette__planInfoList')
post_date = soup.find_all("p", class_='jlnpc-kuchikomiCassette__postDate')
rate_list = soup.find_all("dl", class_='jlnpc-kuchikomiCassette__rateList')
hotel_name = soup.find_all("p", class_='jlnpc-styleguide-scope jlnpc-yado__subTitle')
rating = soup.find_all('span', class_='jlnpc-kuchikomi__point')
cat_table = soup.find_all("table", class_='jlnpc-kuchikomi__catTable')
c_label=soup.find_all("span", class_='c-label')

# テキストを抽出し、リストに格納
post_body_text = [element.get_text().strip() for element in post_body]
lead_text = [element.get_text().strip() for element in lead]
plan_info_text = [element.get_text().strip() for element in plan_info]
post_date_text = [element.get_text().strip() for element in post_date]
rate_list_text = [element.get_text().strip() for element in rate_list]
hotel_name_text = [element.get_text().strip() for element in hotel_name]
integrated_rate_text = [element.get_text().strip() for element in rating]
cat_table_text = [element.get_text().strip() for element in cat_table]
c_label_text = [element.get_text().strip() for element in c_label]

import re

sex = []
age = []
purpose = [] # 利用用途
room_type = [] # 部屋タイプ
meal_type = [] # 食事タイプ
post_date_text_v2 = [] #投稿日
periods = [] # 時期
plans = [] # プラン名
prices = [] # 価格帯
low_prices = [] # 価格帯
high_prices = [] # 価格帯
room_ratings = []
bath_ratings = []
breakfast_ratings = []
dinner_ratings = []
service_ratings = []
cleanliness_ratings = []
overall_room_rating = []
overall_bath_rating = []
overall_breakfast_rating = []
overall_dinner_rating = []
overall_service_rating = []
overall_cleanliness_rating = []

for i in range(0, len(c_label_text), 4): # 4つの要素ごとにループ
# 性別と年齢の抽出
sex_age = c_label_text[i].strip().split('/')
if len(sex_age) == 2:
sex.append(sex_age[0])
age.append(sex_age[1])
else:
sex.append(None)
age.append(None)

# 利用用途の抽出
purpose.append(c_label_text[i + 1].strip() if i + 1 < len(c_label_text) else None)

# 部屋タイプの抽出
room_type.append(c_label_text[i + 2].strip() if i + 2 < len(c_label_text) else None)

# 食事タイプの抽出
meal_type.append(c_label_text[i + 3].strip() if i + 3 < len(c_label_text) else None)

# 正規表現パターンの定義
date_pattern = r'\d{4}/\d{1,2}/\d{1,2}'

for text in post_date_text:
# 正規表現を使用して日付を抽出
match = re.search(date_pattern, text)
if match:
# 抽出した日付を新しいリストに追加
post_date_text_v2.append(match.group())

for item in plan_info_text:
if item.startswith('時期'):
periods.append(item.split('\n')[1].split('宿泊')[0]) # 時期の情報を追加
elif item.startswith('プラン'):
plans.append(item.split('\n')[1]) # プラン名の情報を追加
elif item.startswith('価格帯'):
prices.append(item.split('\n')[-1].split('円')[0]) # 価格帯の情報を追加
if '〜' in item.split('\n')[-1].split('円')[0]:
low_prices.append(item.split('\n')[-1].split('〜')[0])
high_prices.append(item.split('\n')[-1].split('円')[0].split('〜')[1])
elif '~' in item.split('\n')[-1].split('円')[0]:
low_prices.append(item.split('\n')[-1].split('~')[0])
high_prices.append(item.split('\n')[-1].split('円')[0].split('~')[1])

for item in rate_list_text:
lines = item.split('\n') # 各行に分割
for i in range(0, len(lines), 2): # 2つの要素ごとにループ(カテゴリー名と評価)
category = lines[i]
rating = lines[i + 1]
if '部屋' in category:
room_ratings.append(rating)
elif '風呂' in category:
bath_ratings.append(rating)
elif '料理(朝食)' in category:
breakfast_ratings.append(rating)
elif '料理(夕食)' in category:
dinner_ratings.append(rating)
elif '接客・サービス' in category:
service_ratings.append(rating)
elif '清潔感' in category:
cleanliness_ratings.append(rating)

for item in cat_table_text:
lines = item.split('\n')
# 各カテゴリの評価を抽出
overall_room_rating = lines[lines.index('部屋') + 1]
overall_bath_rating = lines[lines.index('風呂') + 1]
overall_breakfast_rating = lines[lines.index('料理(朝食)') + 1]
overall_dinner_rating = lines[lines.index('料理(夕食)') + 1]
overall_service_rating = lines[lines.index('接客・サービス') + 1]
overall_cleanliness_rating = lines[lines.index('清潔感') + 1]

# DataFrameを作成
data = pd.DataFrame({
'sex': sex,
'age': age,
'purpose': purpose,
'room_type': room_type,
'meal_type': meal_type,
'post_body': post_body_text,
'post_lead': lead_text,
'post_date': post_date_text_v2,
'periods': periods,
'plans' : plans,
'prices': prices,
'low_prices' : low_prices,
'high_prices' : high_prices,
'room_ratings' : room_ratings,
'bath_ratings' : bath_ratings,
'breakfast_ratings' : breakfast_ratings,
'dinner_ratings' : dinner_ratings,
'service_ratings' : service_ratings,
'cleanliness_ratings' : cleanliness_ratings
})


# ホテル名だけを抽出
hotel_names = [name.split('のクチコミ・評価')[0] for name in hotel_name_text]

# データセットのすべての行に同じホテル名を適用
data['ホテル名'] = hotel_names*len(data)
data['総合評価'] = integrated_rate_text*len(data)
# データセットに各カテゴリの評価の列を追加
data['総合部屋評価'] = overall_room_rating
data['総合風呂評価'] = overall_bath_rating
data['総合朝食評価'] = overall_breakfast_rating
data['総合夕食評価'] = overall_dinner_rating
data['総合接客・サービス評価'] = overall_service_rating
data['総合清潔感評価'] = overall_cleanliness_rating

これを行うことで口コミがデータに格納できました。


顧客属性ごとに口コミ評価の分布を可視化


# 'prices' ごとの性別割合を計算
prices_sex_distribution = data.groupby('prices')['sex'].value_counts(normalize=True).unstack().fillna(0)

# 横棒グラフで表示
prices_sex_distribution.plot(kind='barh', stacked=True, figsize=(10, 6))
plt.title('価格帯ごとの性別割合')
plt.xlabel('割合')
plt.ylabel('価格帯')
plt.legend(title='性別')
plt.show()

# 年齢ごとの性別割合を計算
age_sex_distribution = data.groupby('age')['sex'].value_counts(normalize=True).unstack().fillna(0)

# 横棒グラフで表示
age_sex_distribution.sort_index().plot(kind='barh', stacked=True, figsize=(10, 6))
plt.title('年齢ごとの性別割合')
plt.xlabel('割合')
plt.ylabel('年齢')
plt.legend(title='性別')
plt.show()

# 性別ごとの利用用途割合を計算
sex_purpose_distribution = data.groupby('sex')['purpose'].value_counts(normalize=True).unstack().fillna(0)

# 横棒グラフで表示
sex_purpose_distribution.plot(kind='barh', stacked=True, figsize=(10, 6))
plt.title('性別ごとの利用用途割合')
plt.xlabel('割合')
plt.ylabel('性別')
plt.legend(title='利用用途')
plt.show()

# 評価データが文字列の場合、数値型に変換
rating_columns = ['room_ratings', 'bath_ratings', 'breakfast_ratings', 'dinner_ratings', 'service_ratings', 'cleanliness_ratings']
for column in rating_columns:
data[column] = pd.to_numeric(data[column], errors='coerce')

# 性別ごとの評価の分布を箱ひげ図で表示
plt.figure(figsize=(12, 8))
data.boxplot(column=rating_columns, by='sex')
plt.xlabel('性別')
plt.ylabel('評価')
plt.suptitle('') # サブタイトルを削除
plt.show()

# 年齢ごとの評価の分布を箱ひげ図で表示
plt.figure(figsize=(12, 8))
data.boxplot(column=rating_columns, by='age')
plt.xlabel('年齢')
plt.ylabel('評価')
plt.suptitle('') # サブタイトルを削除
plt.show()

# 利用用途ごとの評価の分布を箱ひげ図で表示
plt.figure(figsize=(12, 8))
data.boxplot(column=rating_columns, by='purpose')
plt.xlabel('利用用途')
plt.ylabel('評価')
plt.suptitle('') # サブタイトルを削除
plt.show()

# 年齢と性別でグループ化し、箱ひげ図で表示
for column in rating_columns:
plt.figure(figsize=(12, 8))
data.boxplot(column=column, by=['age', 'sex'])
plt.title(f'{column} - 年齢×性別ごとの分布')
plt.suptitle('')
plt.xlabel('年齢 / 性別')
plt.ylabel('評価')
plt.xticks(rotation=45) # X軸のラベルを45度回転
plt.show()

# 年齢、性別、利用用途でグループ化し、箱ひげ図で表示
for column in rating_columns:
plt.figure(figsize=(12, 8))
data.boxplot(column=column, by=['age', 'sex', 'purpose'])
plt.title(f'{column} - 年齢×性別×利用用途ごとの分布')
plt.suptitle('')
plt.xlabel('年齢 / 性別 / 利用用途')
plt.ylabel('評価')
plt.xticks(rotation=45) # X軸のラベルを45度回転
plt.show()

# 'plans' ごとの評価の分布を箱ひげ図で表示
for column in rating_columns:
plt.figure(figsize=(12, 8))
data.boxplot(column=column, by='plans')
plt.title(f'{column} - プランごとの分布')
plt.suptitle('')
plt.xlabel('プラン')
plt.ylabel('評価')
plt.xticks(rotation=45) # X軸のラベルを45度回転
plt.show()

# 'prices' ごとの評価の分布を箱ひげ図で表示
for column in rating_columns:
plt.figure(figsize=(12, 8))
data.boxplot(column=column, by='prices')
plt.title(f'{column} - 価格帯ごとの分布')
plt.suptitle('')
plt.xlabel('価格帯')
plt.ylabel('評価')
plt.xticks(rotation=45) # X軸のラベルを45度回転
plt.show()

# 'prices' と性別ごとの評価の分布を箱ひげ図で表示
for column in rating_columns:
plt.figure(figsize=(12, 8))
data.boxplot(column=column, by=['prices', 'sex'])
plt.title(f'{column} - 価格帯×性別ごとの分布')
plt.suptitle('')
plt.xlabel('価格帯 / 性別')
plt.ylabel('評価')
plt.xticks(rotation=45) # X軸のラベルを45度回転
plt.show()

このコードを実行することで例えば以下のようなグラフが出力できます。


EDAを行う

どのようなデータが格納されているのか軽くEDAを行いました。



# 性別の割合を計算
sex_counts = data['sex'].value_counts()

# 円グラフを描画
plt.figure(figsize=(8, 6))
sex_counts.plot(kind='pie', autopct='%1.1f%%')
plt.title('性別の割合')
plt.ylabel('') # Y軸ラベルを非表示に
plt.show()

# 年齢の分布を計算
age_counts = data['age'].value_counts()

# 棒グラフを描画
plt.figure(figsize=(10, 6))
age_counts.plot(kind='bar')
plt.title('年齢の分布')
plt.xlabel('年齢')
plt.ylabel('人数')
plt.show()

purpose_counts = data['purpose'].value_counts()
plt.figure(figsize=(8, 6))
purpose_counts.plot(kind='pie', autopct='%1.1f%%')
plt.title('利用用途の割合')
plt.ylabel('')
plt.show()

room_type_counts = data['room_type'].value_counts()
plt.figure(figsize=(10, 6))
room_type_counts.plot(kind='bar')
plt.title('部屋タイプの分布')
plt.xlabel('部屋タイプ')
plt.ylabel('人数')
plt.show()

meal_type_counts = data['meal_type'].value_counts()
plt.figure(figsize=(10, 6))
meal_type_counts.plot(kind='bar')
plt.title('食事タイプの分布')
plt.xlabel('食事タイプ')
plt.ylabel('人数')
plt.show()

# 価格帯のデータを数値に変換する必要がある場合があります
# 例: data['low_prices'] = pd.to_numeric(data['low_prices'], errors='coerce')

plt.figure(figsize=(12, 6))
plt.plot(data['low_prices'], label='最低価格')
plt.plot(data['high_prices'], label='最高価格')
plt.title('価格帯の変動')
plt.xlabel('サンプル')
plt.ylabel('価格')
plt.legend()
plt.show()

# 箱ひげ図の描画

# 評価データが文字列の場合、数値型に変換
data['room_ratings'] = pd.to_numeric(data['room_ratings'], errors='coerce')
data['bath_ratings'] = pd.to_numeric(data['bath_ratings'], errors='coerce')
data['breakfast_ratings'] = pd.to_numeric(data['breakfast_ratings'], errors='coerce')
data['dinner_ratings'] = pd.to_numeric(data['dinner_ratings'], errors='coerce')
data['service_ratings'] = pd.to_numeric(data['service_ratings'], errors='coerce')
data['cleanliness_ratings'] = pd.to_numeric(data['cleanliness_ratings'], errors='coerce')

# 箱ひげ図の描画
plt.figure(figsize=(10, 6))
data[['room_ratings', 'bath_ratings', 'breakfast_ratings', 'dinner_ratings', 'service_ratings', 'cleanliness_ratings']].plot(kind='box')
plt.title('各評価の箱ひげ図')
plt.ylabel('評価')
plt.show()

# 年齢層ごとに利用用途の割合を計算
purpose_by_age = data.groupby('age')['purpose'].value_counts(normalize=True).unstack().fillna(0)

# 横棒グラフの描画
purpose_by_age.plot(kind='barh', stacked=True, figsize=(10, 6))
plt.title('年齢層ごとの利用用途の割合')
plt.xlabel('割合')
plt.ylabel('年齢層')
plt.legend(title='利用用途')
plt.show()

# 'post_date' カラムを日付型に変換
data['post_date'] = pd.to_datetime(data['post_date'])

# 日付ごとにレビュー数を集計
reviews_per_day = data['post_date'].value_counts().sort_index()

# 折れ線グラフの描画
plt.figure(figsize=(10, 6))
reviews_per_day.plot(kind='line')
plt.title('日付ごとのレビュー投稿数の推移')
plt.xlabel('日付')
plt.ylabel('レビュー数')
plt.grid(True)
plt.show()

# 週ごとにレビュー数を集計
# 'post_date' を週の始まり(月曜日)にマッピング
data['week'] = data['post_date'].dt.to_period('W').apply(lambda r: r.start_time)
reviews_per_week = data.groupby('week').size()

# 折れ線グラフの描画
plt.figure(figsize=(10, 6))
reviews_per_week.plot(kind='line', marker='o')
plt.title('週ごとのレビュー投稿数の推移')
plt.xlabel('週(月曜日開始)')
plt.ylabel('レビュー数')
plt.grid(True)
plt.show()

頻出単語を可視化

頻出単語を可視化してみます。しかし、このままだと分析にはあまり使えないと思うので抜き出す品詞やキーワードなどの設定はもう少し考える必要がありますね。



!pip install janome

from janome.tokenizer import Tokenizer
from collections import Counter

# Janomeの形態素解析器の初期化
tokenizer = Tokenizer()

# ストップワードの設定(独自に定義する必要があります)
stop_words = {'これ', 'は', 'です', 'が', '。', '、', 'し', 'の', '・'}

# ワードの頻出分析
words = []
for post in data['post_body']:
tokens = tokenizer.tokenize(post)
words.extend([token.surface for token in tokens if token.surface not in stop_words and token.part_of_speech.split(',')[0] in ['名詞','動詞', '形容詞']])

word_counts = Counter(words)

# 上位20単語を取得
top_words = word_counts.most_common(20)

# 単語とそのカウントを別々のリストに分ける
words, counts = zip(*top_words)

# 棒グラフの作成
plt.figure(figsize=(10, 8))
plt.bar(words, counts)
plt.xlabel('単語')
plt.ylabel('頻度')
plt.xticks(rotation=90) # X軸のラベルを90度回転して読みやすくする
plt.title('上位20位の頻出単語')
plt.show()

おわりに

別の記事でこれらの結果をGoogle ColaboratoryからStreamlitで可視化する方法をこちらの記事で解説していますので、よかったらご覧下さい。


blog.since2020.jp/data_analysis/google-colabからstreamlitでじゃらんの口コミ分析結果を可視化/

New call-to-action