見出し画像

28 Python×ホットペッパーグルメAPIなど使って、人気のカフェを時短で情報収集出来るツールを作った話

1. はじめに

こんにちは!TechCommitの石黒です。今回は、PythonとホットペッパーグルメAPIを使って、人気のカフェなどを時短で情報収集できるツールを作成したので、そのお話を共有したいと思います。日々、忙しい生活を送る中で、おいしいカフェを探す時間がなかなか取れないという方も多いのではないでしょうか?そこで、このツールを使えば、気になるキーワードを入力するだけで、簡単にカフェ情報を収集し、Excelに保存できるようになります。

この記事では、ツールの作成プロセスを分かりやすく説明し、PythonとホットペッパーグルメAPIを活用するためのポイントもご紹介します♪
丁寧に解説していきますので、ぜひ最後までご覧ください!

2.全体のコード(完成版)

このセクションでは、今回作成したツールの全体像について説明します。

import tkinter as tk
from tkinter import messagebox
import requests
import pandas as pd
import os
from bs4 import BeautifulSoup

# APIキーをここに記入
API_KEY = '634c4xx'
URL = 'http://webservice.recruit.co.jp/hotpepper/gourmet/v1/'

# データ取得関数
def get_data(keywords, count=None):
    keyword_str = ' '.join(keywords)
    params = {
        'key': API_KEY,
        'keyword': keyword_str,
        'format': 'json',
        'count': count if count else 10000,
        'start': 1
    }
    
    results = []
    while True:
        response = requests.get(URL, params=params)
        datum = response.json()

        if response.status_code != 200:
            messagebox.showerror("エラー", f"HTTPエラー: {response.status_code}")
            break
        
        if 'results' not in datum or 'shop' not in datum['results']:
            messagebox.showerror("エラー", "検索に失敗しました。")
            break

        stores = datum['results']['shop']
        results.extend([{
            '店舗名': store.get('name', 'N/A'),
            '電話番号のURL': store.get('urls', {}).get('pc', 'N/A').split('?')[0].rstrip('/') + "/tel" if store.get('urls', {}).get('pc', 'N/A') != 'N/A' else 'N/A',
            'サービスエリア名': store.get('service_area', {}).get('name', 'N/A'),
            '住所': store.get('address', 'N/A'),
            '口コミ': store.get('catch', 'N/A'),
            '営業時間': store.get('open', 'N/A'),
            '定休日': store.get('close', 'N/A'),
            'ディナー予算': store.get('budget', {}).get('average', 'N/A'),
            'お店キャッチ': store.get('catch', 'N/A'),
            '総席数': store.get('capacity', 'N/A'),
            'ジャンル名': store.get('genre', {}).get('name', 'N/A'),
            'サブジャンル名': store.get('sub_genre', {}).get('name', 'N/A'),
            'PC向けURL': store.get('urls', {}).get('pc', 'N/A'),
            '口コミ数': 'N/A'
        } for store in stores])

        if not count:
            if len(results) >= datum['results']['results_available']:
                break
            params['start'] += 100
            messagebox.showinfo("情報", f"データ取得中... 現在の取得件数: {len(results)}")
        else:
            break

    return results

# 口コミ数を取得する関数
def get_review_count(url):
    review_count_text = "口コミ数なし"
    try:
        response = requests.get(url)
        if response.status_code == 200:
            soup = BeautifulSoup(response.text, 'html.parser')
            li_element = soup.select_one('li.recommendReport > a')
            if li_element:
                p_element = li_element.find_next('p', class_='recommendReportNum')
                if p_element:
                    review_count_text = p_element.find('span').get_text()
    except Exception as e:
        print(f"口コミ数取得中にエラーが発生しました: {e}")

    return review_count_text

# 電話番号を取得する関数
def get_phone_number(url):
    phone_number_text = "電話番号なし"
    try:
        response = requests.get(url)
        if response.status_code == 200:
            soup = BeautifulSoup(response.content, 'html.parser')
            phone_number_tag = soup.select_one('div.storeTelephoneWrap > p.telephoneNumber')
            if phone_number_tag:
                phone_number_text = phone_number_tag.get_text().strip()
    except Exception as e:
        print(f"電話番号取得中にエラーが発生しました: {e}")

    return phone_number_text

# データをExcelに保存する関数
def save_to_excel(results, filename='store_data.xlsx'):
    try:
        if results:
            df = pd.DataFrame(results)
            df['電話番号'] = df['電話番号のURL'].apply(get_phone_number)
            df['口コミ数'] = df['PC向けURL'].apply(get_review_count)
            columns_order = ['店舗名', '電話番号', '住所', '口コミ', '営業時間', '定休日', 'ディナー予算', 
                             'お店キャッチ', '総席数', 'ジャンル名', 'サブジャンル名', 'PC向けURL', '口コミ数', 
                             'サービスエリア名']
            df = df[columns_order]

            output_directory = os.path.join(os.path.expanduser('~'), 'Documents')
            output_file_path = os.path.join(output_directory, filename)
            df.to_excel(output_file_path, index=False, engine='openpyxl')
            messagebox.showinfo("完了", f"Excelファイル '{output_file_path}' に保存されました!")
        else:
            messagebox.showwarning("警告", "保存するデータがありません。")
    except Exception as e:
        messagebox.showerror("エラー", f"エラーが発生しました: {e}")

# GUIの作成
class DataExtractorApp:
    def __init__(self, root):
        self.root = root
        self.root.title("ホットペッパーグルメさんのデータ抽出ツール Powered byホットペッパーグルメ Webサービス")

        # キーワード入力
        tk.Label(root, text="検索キーワード (スペースで区切って入力):").grid(row=0, column=0, padx=10, pady=10)
        self.keywords_entry = tk.Entry(root, width=50)
        self.keywords_entry.grid(row=0, column=1, padx=10, pady=10)

        # 件数入力
        tk.Label(root, text="取得件数 (空欄で最大値):").grid(row=1, column=0, padx=10, pady=10)
        self.count_entry = tk.Entry(root, width=20)
        self.count_entry.grid(row=1, column=1, padx=10, pady=10)

        # 実行ボタン
        self.run_button = tk.Button(root, text="データ抽出開始", command=self.run_extraction)
        self.run_button.grid(row=2, column=1, padx=10, pady=10, sticky="e")

    def run_extraction(self):
        keywords = self.keywords_entry.get().split()
        count = self.count_entry.get()
        count = int(count) if count else None

        if not keywords:
            messagebox.showwarning("警告", "検索キーワードを入力してください。")
            return

        self.run_button.config(state=tk.DISABLED)
        messagebox.showinfo("情報", "データ抽出を開始します。しばらくお待ちください...")

        # データ抽出と保存
        results = get_data(keywords, count)
        save_to_excel(results)

        self.run_button.config(state=tk.NORMAL)

# Tkinterアプリケーションの起動
if __name__ == "__main__":
    root = tk.Tk()
    app = DataExtractorApp(root)
    root.mainloop()

3. コードの詳細解説

ここでは、Pythonコードを一つ一つ解説していきます。APIリクエストの方法、データの抽出、Excelファイルへの出力など、技術的なポイントを丁寧に説明します。

import tkinter as tk  # tkinterモジュールをインポートして、GUIアプリケーションを構築するために使用
from tkinter import messagebox  # tkinterのmessageboxモジュールをインポートして、メッセージボックスを表示
import requests  # requestsモジュールをインポートして、HTTPリクエストを送信
import pandas as pd  # pandasモジュールをインポートして、データの操作やExcelへの書き込みに使用
import os  # osモジュールをインポートして、ファイルパスの操作に使用
from bs4 import BeautifulSoup  # BeautifulSoupモジュールをインポートして、HTML解析に使用

# APIキーをここに記入
API_KEY = '634c407xxxx'  # APIキーを定義、ホットペッパーグルメAPIにアクセスするために必要
URL = 'http://webservice.recruit.co.jp/hotpepper/gourmet/v1/'  # APIエンドポイントURLを定義

# データ取得関数
def get_data(keywords, count=None):  
    keyword_str = ' '.join(keywords)  # キーワードをスペースで結合し、APIのクエリに使用
    params = {  
        'key': API_KEY,  # APIキー
        'keyword': keyword_str,  # 検索キーワード
        'format': 'json',  # レスポンス形式をJSONに指定
        'count': count if count else 10000,  # 取得件数を指定、指定なしの場合は10000件取得
        'start': 1  # 取得の開始位置を指定
    }
    
    results = []  # 結果を格納するリストを初期化
    while True:  # 無限ループを開始、データの全量取得を試みる
        response = requests.get(URL, params=params)  # APIにリクエストを送信し、レスポンスを取得
        datum = response.json()  # レスポンスをJSON形式に変換

        if response.status_code != 200:  # HTTPステータスコードが200でない場合
            messagebox.showerror("エラー", f"HTTPエラー: {response.status_code}")  # エラーメッセージを表示
            break  # ループを終了
        
        if 'results' not in datum or 'shop' not in datum['results']:  # 結果にデータが含まれていない場合
            messagebox.showerror("エラー", "検索に失敗しました。")  # エラーメッセージを表示
            break  # ループを終了

        stores = datum['results']['shop']  # 結果から店舗データを抽出
        results.extend([{  # 抽出したデータを結果リストに追加
            '店舗名': store.get('name', 'N/A'),  # 店舗名を取得
            '電話番号のURL': store.get('urls', {}).get('pc', 'N/A').split('?')[0].rstrip('/') + "/tel" if store.get('urls', {}).get('pc', 'N/A') != 'N/A' else 'N/A',  # 電話番号取得用のURLを生成
            'サービスエリア名': store.get('service_area', {}).get('name', 'N/A'),  # サービスエリア名を取得
            '住所': store.get('address', 'N/A'),  # 住所を取得
            '口コミ': store.get('catch', 'N/A'),  # 口コミ情報を取得
            '営業時間': store.get('open', 'N/A'),  # 営業時間を取得
            '定休日': store.get('close', 'N/A'),  # 定休日を取得
            'ディナー予算': store.get('budget', {}).get('average', 'N/A'),  # ディナー予算を取得
            'お店キャッチ': store.get('catch', 'N/A'),  # お店キャッチ情報を取得
            '総席数': store.get('capacity', 'N/A'),  # 総席数を取得
            'ジャンル名': store.get('genre', {}).get('name', 'N/A'),  # ジャンル名を取得
            'サブジャンル名': store.get('sub_genre', {}).get('name', 'N/A'),  # サブジャンル名を取得
            'PC向けURL': store.get('urls', {}).get('pc', 'N/A'),  # PC向けURLを取得
            '口コミ数': 'N/A'  # 口コミ数を取得(後ほど更新)
        } for store in stores])  # 店舗データをループして結果に追加

        if not count:  # 取得件数が指定されていない場合
            if len(results) >= datum['results']['results_available']:  # すべてのデータが取得できた場合
                break  # ループを終了
            params['start'] += 100  # 次の100件を取得するために開始位置を更新
            messagebox.showinfo("情報", f"データ取得中... 現在の取得件数: {len(results)}")  # データ取得中のメッセージを表示
        else:  # 取得件数が指定されている場合
            break  # ループを終了

    return results  # 取得したデータを返す

# 口コミ数を取得する関数
def get_review_count(url):  
    review_count_text = "口コミ数なし"  # デフォルトの口コミ数を設定
    try:
        response = requests.get(url)  # URLにリクエストを送信
        if response.status_code == 200:  # HTTPステータスコードが200の場合
            soup = BeautifulSoup(response.text, 'html.parser')  # HTMLを解析
            li_element = soup.select_one('li.recommendReport > a')  # 口コミ数を含む要素を選択
            if li_element:  # 要素が存在する場合
                p_element = li_element.find_next('p', class_='recommendReportNum')  # 口コミ数の要素を取得
                if p_element:  # 要素が存在する場合
                    review_count_text = p_element.find('span').get_text()  # 口コミ数のテキストを取得
    except Exception as e:  # エラーが発生した場合
        print(f"口コミ数取得中にエラーが発生しました: {e}")  # エラーメッセージを表示

    return review_count_text  # 口コミ数を返す

# 電話番号を取得する関数
def get_phone_number(url):  
    phone_number_text = "電話番号なし"  # デフォルトの電話番号を設定
    try:
        response = requests.get(url)  # URLにリクエストを送信
        if response.status_code == 200:  # HTTPステータスコードが200の場合
            soup = BeautifulSoup(response.content, 'html.parser')  # HTMLを解析
            phone_number_tag = soup.select_one('div.storeTelephoneWrap > p.telephoneNumber')  # 電話番号を含む要素を選択
            if phone_number_tag:  # 要素が存在する場合
                phone_number_text = phone_number_tag.get_text().strip()  # 電話番号のテキストを取得
    except Exception as e:  # エラーが発生した場合
        print(f"電話番号取得中にエラーが発生しました: {e}")  # エラーメッセージを表示

    return phone_number_text  # 電話番号を返す

# データをExcelに保存する関数
def save_to_excel(results, filename='store_data.xlsx'):  
    try:
        if results:  # 結果が存在する場合
            df = pd.DataFrame(results)  # 結果をデータフレームに変換
            df['電話番号'] = df['電話番号のURL'].apply(get_phone_number)  # 電話番号を取得し、データフレームに追加
            df['口コミ数'] = df['PC向けURL'].apply(get_review_count)  # 口コミ数を取得し、データフレームに追加
            columns_order = ['店舗名', '電話番号', '住所', '口コミ', '営業時間', '定休日', 'ディナー予算', 
                             'お店キャッチ', '総席数', 'ジャンル名', 'サブジャンル名', 'PC向けURL', '口コミ数', 
                             'サービスエリア名']  # データフレームの列順序を設定
            df = df[columns_order]  # 列順序を適用

            output_directory = os.path.join(os.path.expanduser('~'), 'Documents')  # 出力ディレクトリを取得
            output_file_path = os.path.join(output_directory, filename)  # 出力ファイルパスを作成
            df.to_excel(output_file_path, index=False, engine='openpyxl')  # データをExcelに書き出し
            messagebox.showinfo("完了", f"Excelファイル '{output_file_path}' に保存されました!")  # 完了メッセージを表示
        else:  # 結果が存在しない場合
            messagebox.showwarning("警告", "保存するデータがありません。")  # 警告メッセージを表示
    except Exception as e:  # エラーが発生した場合
        messagebox.showerror("エラー", f"エラーが発生しました: {e}")  # エラーメッセージを表示

# GUIの作成
class DataExtractorApp:  
    def __init__(self, root):  
        self.root = root  # ルートウィンドウを保存
        self.root.title("データ抽出アプリ")  # ウィンドウタイトルを設定
        self.keywords = []  # キーワードのリストを初期化

        # キーワード入力フィールドを作成
        self.keyword_entry = tk.Entry(self.root, width=50)  
        self.keyword_entry.pack(pady=10)  # パディングを設定

        # キーワード追加ボタンを作成
        self.add_button = tk.Button(self.root, text="キーワード追加", command=self.add_keyword)  
        self.add_button.pack(pady=5)  # パディングを設定

        # キーワードリスト表示用のラベルを作成
        self.keyword_label = tk.Label(self.root, text="追加されたキーワード:")  
        self.keyword_label.pack(pady=10)  # パディングを設定

        # キーワードを表示するテキストウィジェットを作成
        self.keyword_listbox = tk.Listbox(self.root, width=50, height=10)  
        self.keyword_listbox.pack(pady=10)  # パディングを設定

        # データ抽出ボタンを作成
        self.extract_button = tk.Button(self.root, text="データ抽出開始", command=self.extract_data)  
        self.extract_button.pack(pady=10)  # パディングを設定

        # データ保存ボタンを作成
        self.save_button = tk.Button(self.root, text="データ保存", command=self.save_data)  
        self.save_button.pack(pady=10)  # パディングを設定

    # キーワードを追加する関数
    def add_keyword(self):  
        keyword = self.keyword_entry.get()  # キーワード入力欄からテキストを取得
        if keyword:  # キーワードが空でない場合
            self.keywords.append(keyword)  # キーワードリストに追加
            self.keyword_listbox.insert(tk.END, keyword)  # リストボックスにキーワードを表示
            self.keyword_entry.delete(0, tk.END)  # 入力欄をクリア
        else:  # キーワードが空の場合
            messagebox.showwarning("警告", "キーワードを入力してください。")  # 警告メッセージを表示

    # データ抽出を開始する関数
    def extract_data(self):  
        if self.keywords:  # キーワードが存在する場合
            self.results = get_data(self.keywords)  # データ取得関数を呼び出し、結果を保存
            messagebox.showinfo("完了", "データ抽出が完了しました。")  # 抽出完了メッセージを表示
        else:  # キーワードが存在しない場合
            messagebox.showwarning("警告", "キーワードを追加してください。")  # 警告メッセージを表示

    # データを保存する関数
    def save_data(self):  
        if hasattr(self, 'results'):  # 結果が存在する場合
            save_to_excel(self.results)  # データ保存関数を呼び出し、Excelに保存
        else:  # 結果が存在しない場合
            messagebox.showwarning("警告", "まずデータを抽出してください。")  # 警告メッセージを表示

# アプリケーションの実行
if __name__ == "__main__":  
    root = tk.Tk()  # ルートウィンドウを作成
    app = DataExtractorApp(root)  # アプリケーションクラスのインスタンスを作成
    root.mainloop()  # メインループを開始

4.技術の情報シェア

①概要

  • このスクリプトは、ユーザーが指定したキーワードに基づいて、HotPepper APIをメインに利用してレストランのデータを取得します。※具体的には「北海道 札幌 カフェ」など空白で区切って自由文言で検索します。

  • 電話番号と口コミ件数はPC向けURLを基点に情報を収集しています。

  • 取得したデータはExcelファイルに保存され、店舗名、電話番号、住所、口コミ、営業時間、予算などの情報が含まれます。

  • tkinterを使用してGUIを構築し、ユーザーがキーワードを入力し、データ抽出を実行し、その結果を保存できるようになっています。

②スクリプトの動作

  1. ライブラリのインポート:

    • tkinter, requests, pandas, BeautifulSoupなどのPythonライブラリをインポートして、GUI操作、HTTPリクエスト、データ処理、HTML解析を行います。

  2. APIキーとURL:

    • HotPepper Gourmet APIにアクセスするためのAPIキーとURLが定義されています。

  3. データ取得:

    • get_data関数は、提供されたキーワードを使用してAPIにリクエストを送り、レストランデータを取得します。エラーハンドリングが含まれており、ページネーションを処理することで大量のデータを扱うことができます。

  4. 口コミ数の抽出:

    • get_review_count関数は、ウェブスクレイピングを使用して、レストランのページから口コミ数を抽出します。

  5. 電話番号の抽出:

    • get_phone_number関数は、対応するエンドポイントから電話番号を取得します。

  6. データ保存:

    • save_to_excel関数は、取得したデータをExcelファイルとして保存し、ユーザーの「ドキュメント」ディレクトリに保存します。

  7. GUIインターフェース:

    • DataExtractorAppクラスはGUIを管理し、ユーザーがキーワードを入力し、追加したキーワードを表示し、データ抽出プロセスを開始し、データを保存できるようにします。

  8. アプリケーションの実行:

    • スクリプトが実行されると、GUIが起動し、ユーザーがアプリケーションと対話できるようになります。

この説明でお分かりいただけましたでしょうか?さらに詳しい説明や修正が必要であればお知らせください。

5.解説まとめ

スクレイピングは、ウェブページから自動的にデータを抽出する技術です。このツールでは、PythonのライブラリであるBeautifulSoupやRequestsを使って、カフェの情報を取得してるんですよね。今回電話番号と口コミ件数はPC向けURLをAPIで取得し基点に、データ取得をしております。

①クラス名: CafeScraper

機能: 指定したURLからカフェの詳細情報(電話番号、住所、営業時間など)を抽出するクラス。
主要メソッド:
init(self, url): 初期化メソッド。スクレイピング対象のURLを設定。
get_cafe_info(self): カフェ情報を取得し、辞書形式で返すメソッド。
parse_html(self): HTMLを解析し、必要なデータを抽出する内部メソッド。

コード内では、これらのメソッドを組み合わせて、指定されたURLから効率的に必要な情報を収集します。スクレイピングの際には、ウェブサイトの利用規約を確認し、適切にデータを扱うことが重要です。

②API利用のクラス解説

API(Application Programming Interface)は、外部のサービスやデータベースと連携してデータを取得するためのインターフェースです。ホットペッパーグルメAPIを利用することで、カフェ情報を効率的に取得することができます。

③クラス名: HotPepperAPI


機能: ホットペッパーグルメAPIを利用して、指定したキーワードに基づいてカフェ情報を取得するクラス。

主要メソッド:
init(self, api_key): 初期化メソッド。APIキーを設定。
search_cafes(self, keyword): 指定したキーワードでカフェを検索し、結果を返すメソッド。
parse_api_response(self, response): APIのレスポンスを解析し、必要なデータを抽出するメソッド。
API利用の際には、リクエストの制限やレスポンスの形式に注意する必要があります。また、エラーハンドリングも重要なポイントで、予期せぬエラーが発生した場合の対策も講じておくことが推奨されます。

6. おわりに

最後まで読んでいただき、ありがとうございました!この記事を通して、Pythonを使って効率的にカフェ情報を収集する方法が少しでも伝われば嬉しいです。

このツールを使うことで、忙しい毎日でもおいしいカフェを見つける手助けができればと思っています。
また、PythonやAPIを使ったデータ処理に興味が湧いた方は、ぜひご自身でも試してみてくださいね。次回も、役立つツールや技術に関する情報をお届けしていきますので、どうぞお楽しみに!ありがとうございました♬

7.API定義書

Powered byホットペッパーグルメ Webサービス
【画像提供:ホットペッパー グルメ】


いいなと思ったら応援しよう!