LoginSignup
264
255

ChatGPT(GPT-4) で一撃でスクレイピングするコードを生成出来たので感想とコツ

Last updated at Posted at 2023-05-08

今回やりたかったこと

  • 目標:ChatGPT(GPT-4) で一撃でスクレイピングするコードを生成

するにはどうしたらいいのか、ChatGPT のハードルとかコツとかを知りたい。

※最終的なプロンプトの入力と出力の全文は本ページ下部に貼り付けてます。

作ったもの概要

保険組合のウォーキングイベントの会社内の3チームの歩数進捗の slack への自動投稿 bot を作成しました。
処理は大きく2つに分かれています。

  1. ウォーキングイベントサイトから歩数をスクレイピング&スプシへアップロード
  2. スプシの GAS で投稿文字列作成& slack へ自動投稿

今回 ChatGPT でやったのは1の方です。
2は前回半年前開催分のコードをほぼそのまま流用しました。
運良く(?)今回のタイミングでウォーキングイベントのサービスサイトが変わり、 HTML がまるっと変わり1のスクレイピングコードは作り直しが必要だったので、チャレンジに丁度良いと思い ChatGPT を使ってコード生成してみました。

環境

  • ChatGPT(GPT-4)
  • Mac macOS 12.5.1

作成物スクリーンショット

スプシ

image.png

slack bot

image.png

対応時間

5月7日の1人日で対応しました。
体感としては「半年前に似たものを作った」経験を活かし、今回手動作成なら半日で行けたはずですが、ChatGPTへの慣れで1人日かかりました。
しかし多分今後更に似たような処理をするなら ChatGPT に任せたらより短く(半日以下で)出来るはずです。

ChatGPT の入力上限(執筆時点(2023/05/07)で3時間で25回)、に1回引っかかりました。
#実際は2回で、子供が PC 画面覗き込んで「これなんで自動で文字出てくるの!?」と興味持ったので、途中一緒に「ドラえもんの誕生日」「面白い名前の魚」とか子供と一緒に遊んだ結果、1回制限に引っかかりました。
#ChatGPT で出力させたいイメージが固まる前の試行錯誤段階は「ちょこちょこ試し、制限に引っかかった待ち時間は休憩時間に当てる」くらいが良いかもしれません。

感想

  • まぁまぁ大変。ちゃんと動かせたかったらその分文章書く必要あり。
  • 最初は試行錯誤する。けど今回 ChatGPT 自体に慣れたから次はもう少しスマート&短時間で行けるはず
  • 実際のプロンプトの操作イメージ(体感)
    • 最初に「やりたいことの50%」を入力しプロンプト出力。実行すると「エラー発生したり、最初の入力で言い忘れたこと」を認識。
    • 「エラー対処や次の処理」5%分追加し再出力。
    • 以下繰り返し
    • みたいな感じ

問題と対策(得られた知見)

  • GPT-4 に慣れると GPT-3 は出力が弱い(上記 GPT-4 の入力上限に引っかかったときに GPT-3 で同じことさせたが全体的にイケてない。期待通りの出力にならない)
    • 対策→ GPT-4 の入力数上限に達したら、待つ
  • ログインが必要なページのスクレイピングは基本やってくれない。「ログイン出来ない」と言われる
    • 対策→スクレイピングしたい HTML を DevTools で取り出し、サンプルで数行渡してあげると対応してくれる
  • 生成毎にコードのスタイルがどかっと変わる。変わった場所とは全然無関係の処理を追加してるはずなのに。結果整合が取れず動かないがち。
    • 変数名。「TARGET_TEAM_NAMES」だったり「TARGET_TEAMS」だったり、等々。
    • 関数名
    • 使用ライブラリ
    • 関数化有無(処理ベタ書きだったり、関数化したり)
    • 変数を上部にまとめてくれたり、中に埋め込んだり
       →対策1:それらも指定する。「チーム名、ファイル名、シート名等あとから変更する可能性があるものはコードの上部にまとめて記載して」等。
       →対策2:下にコメントを続けず、冒頭の依頼文を編集して再生成させると全体の整合は取れやすい。
  • ログイン画面の HTML は ChatGPT がアクセス出来るはずだが、何故か微妙に selector を間違えてた。
    • メールアドレス入力欄の ID が正しくは「sender-email」だが最初の出力は「sender_email」だった
    • パスワード入力欄の ID が正しくは「user-pass」だが最初の出力は「user-password」だった
       →対策:それらも指定する。「ID、パスワードの selector はそれぞれ以下。sender-email user-pass」等。
  • ライブラリ/ツールのバージョン問題にちょいちょいぶち当たる。僕の手元の selenium ver は4だったが、ランダムで3向けのコードを吐く
     →対策:バージョンも指定する。「selenium 4」等。
  • バージョン指定しても誤出力することがある
     →対策:API も指定する。「driver.find_element_by_id 等は使用せず、driver.find_element(By.ID, 等を使用する」等。

未解決問題

  • 生成毎にコードブロックが外れがち。外れると python 的にはインデントし直すのが手間。コードブロック成功率を上げるおまじない「テキストはコードブロックで」等はあるが、確実ではない。
     →暫定対策:少し上の「XXX 関数の頭から書いて」と言うと気持ちコードブロック化してくれやすい?コピペしやすい。
  • 明示してるのに未考慮なコードがたまーに出力される。「headless モード有りオプション。デフォルトはheadlessモードオフ。」と言ってるのに、出力毎にたまに「デフォルトオン、オフ」が変わる。。
     →暫定対策:特になし。。気にせず他の処理追加してたら最後の出力で希望の「デフォルトオフ」になってくれてた。。

コツ

  • 続きの出力は「つづき」で OK。最初は丁寧に「続きを書いてください」と書いていたが。
  • 実行時エラー発生したらエラーテキスト貼り付けるだけで OK。最初は丁寧に「エラー発生。<実際のエラーテキスト>」と書いていたが。

最終的なプロンプト

注:入力テキスト出力テキストどちらも基本はコピペですが、スプシの ID 等は「XXX」に置き換えてます。
注:入力テキストは試行錯誤&継ぎ足し継ぎ足しした結果で未整理なので、必要条件ではあるけど十分条件ではないかもです(=もっと整理し短く出来るかも)

入力

以下の条件を満たすコードを python で作成お願いします。
コードはインデントに注意してコードブロックで出力お願いします。

■やりたいこと
ウォーキングイベントのチーム対抗ランキングの特定3チームの、順位、チーム名、総歩数の平均をgoogle スプレッドシートに保存する。

■条件
・使用ライブラリ/ツール
 ・selenium 4
  ・driver.find_element_by_id 等は使用せず、
   driver.find_element(By.ID, 等を使用する
 ・webdriver
  webdriver.Chrome()のexecutable_path引数は使用しない。
  Serviceオブジェクトを使用する。
 ・webdriver-manager
  https://pypi.org/project/webdriver-manager/
 ・Google Sheets API
  ・認証情報は同じ場所に credentials.json として保存されている
  ・service_account.Credentials.from_service_account_file の引数は2つ
・特定3チームのチーム名
 ・「ACCESS_A」が含まれるチーム。
  実際は「【募集】ACCESS_A」等前後に別のテキストが含まれる可能性あり
 ・「ACCESS_B」が含まれるチーム
 ・「ACCESS_C」が含まれるチーム
・起動引数でオプション
 ・headless モード有りオプション。
 ・デフォルトはheadlessモードオフ。
・コードはベタ書きではなく適切に関数化
・チーム名、ファイル名、シート名等あとから変更する可能性があるものはコードの上部にまとめて記載
・IDとパスワードは login_info.json に記載し、そこから読み込むようにしてください
・credentials.json や login_info.json のローカルファイルは cron で実行できるように 本スクリプトの PATH を起点にしてファイルを開けるようにしてください
・特定チームが見つかった時点のスクリーンショットを取得
・まずアクセスする URL は
 https://pepup.life/campaign/XXXX/ranking?mode=team&page=1
 だが、ログインが必要なので
 https://pepup.life/users/sign_in
 に自動的にリダイレクトされる。
 ログイン完了後は
 https://pepup.life/campaign/XXXX/ranking?mode=team&page=1
 に自動的に遷移する。
 その後順次以下のように URL を変更し巡回する。
 https://pepup.life/campaign/XXXX/ranking?mode=team&page=2
 https://pepup.life/campaign/XXXX/ranking?mode=team&page=3
・エラー処理
 ・ランキングの table タグの tr タグが空だったり、「集計中」が含まれたテキストが表示された場合はそこで終了。その旨エラー出力して。

■ログインページの selector
ID、パスワード、ログインボタン、それぞれ以下。
sender-email
user-pass
<input type="submit" name="commit" value="ログイン" class="btn btn-default btn btn-primary btn-block" data-disable-with="ログイン">

■取得するデータ
・ランキングの table タグ。
3列あり、左から「順位」、「チーム名」、「総歩数の平均」。
1ページ30位まで記載があり、何ページまであるかは分からない。
1つの順位分の tr タグ例は以下。
これらのHTMLを selector としてしてください。
<table>
  <tbody>
    <tr class="table-row head"><td style="text-align: right;">順位</td><td style="text-align: left;">チーム名</td><td style="text-align: right;">総歩数の平均</td></tr>
    <tr class="table-row"><td style="text-align: right;"><span class="num-value">768</span><span class="gray">位</span></td><td style="text-align: left;">ACCESS_A</td><td style="text-align: right;"><span class="num-value">61715</span><span class="gray">歩</span></td></tr>


■保存先
https://docs.google.com/spreadsheets/d/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/edit

シート名:ウオキング
A1セル:「順位」というテキスト
B1セル:「チーム名」というテキスト
C1セル:「総歩数の平均」というテキスト
D1セル:「URL」というテキスト

A2セル:ACCESS_A チームの順位
B2セル:「ACCESS_A」というテキスト
C2セル:「ACCESS_A チームの総歩数の平均」という数値
D2セル:ACCESS_A チームの URL

A3セル:ACCESS_B チームの順位
B3セル:「ACCESS_B」というテキスト
C3セル:「ACCESS_B チームの総歩数の平均」という数値
D3セル:ACCESS_B チームの URL

A4セル:ACCESS_C チームの順位
B4セル:「ACCESS_C」というテキスト
C4セル:「ACCESS_C チームの総歩数の平均」という数値
D4セル:ACCESS_C チームの URL

出力

以下のコードをご確認ください。条件に合わせてコードを作成しました。
import json
import os
import sys
from pathlib import Path

from google.oauth2 import service_account
from googleapiclient.discovery import build
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from webdriver_manager.chrome import ChromeDriverManager

# 設定
TARGET_TEAM_NAMES = ["ACCESS_A", "ACCESS_B", "ACCESS_C"]
SPREADSHEET_ID = "XXXX"
SHEET_NAME = "ウオキング"

HEADLESS = "--headless" in sys.argv

# Google API認証
CREDENTIALS_PATH = Path(__file__).resolve().parent / "credentials.json"
SCOPES = ["https://www.googleapis.com/auth/spreadsheets"]
creds = service_account.Credentials.from_service_account_file(str(CREDENTIALS_PATH), scopes=SCOPES)

# ログイン情報取得
LOGIN_INFO_PATH = Path(__file__).resolve().parent / "login_info.json"
with open(LOGIN_INFO_PATH, "r") as f:
    login_info = json.load(f)

def init_driver(headless):
    options = webdriver.ChromeOptions()
    if headless:
        options.add_argument("--headless")
    service = Service(executable_path=ChromeDriverManager().install())
    driver = webdriver.Chrome(service=service, options=options)
    return driver

def login(driver, email, password):
    driver.get("https://pepup.life/users/sign_in")
    driver.find_element(By.ID, "sender-email").send_keys(email)
    driver.find_element(By.ID, "user-pass").send_keys(password)
    driver.find_element(By.XPATH, '//input[@type="submit" and @value="ログイン"]').click()

def find_target_teams(driver):
    found_teams = []
    page = 1

    while True:
        driver.get(f"https://pepup.life/campaign/XXXX/ranking?mode=team&page={page}")

        table_rows = driver.find_elements(By.CSS_SELECTOR, "table tr.table-row")
        if not table_rows or "集計中" in table_rows[0].text:
            print("集計中または空のページが見つかりました。終了します。")
            break

        for row in table_rows:
            team_name = row.find_element(By.CSS_SELECTOR, "td:nth-child(2)").text
            for target_team_name in TARGET_TEAM_NAMES:
                if target_team_name in team_name:
                    rank = row.find_element(By.CSS_SELECTOR, "td:nth-child(1) span.num-value").text
                    avg_steps = row.find_element(By.CSS_SELECTOR, "td:nth-child(3) span.num-value").text
                    found_teams.append((rank, team_name, avg_steps, driver.current_url))
                    driver.save_screenshot(f"{target_team_name}_screenshot.png")
                    TARGET_TEAM_NAMES.remove(target_team_name)
                    break

            if not TARGET_TEAM_NAMES:
                return found_teams

        page += 1


def save_to_sheet(teams_data):
    service = build("sheets", "v4", credentials=creds)

    for i, team_data in enumerate(teams_data, start=2):
        range_ = f"{SHEET_NAME}!A{i}:D{i}"
        values = [list(team_data)]
        body = {"values": values}
        service.spreadsheets().values().update(
            spreadsheetId=SPREADSHEET_ID,
            range=range_,
            valueInputOption="USER_ENTERED",
            body=body,
        ).execute()


def main():
    driver = init_driver(HEADLESS)
    login(driver, login_info["email"], login_info["password"])

    found_teams = find_target_teams(driver)
    if found_teams:
        save_to_sheet(found_teams)

    driver.quit()


if __name__ == "__main__":
    main()
このコードは、指定された条件に従って特定のチームを探し、見つかったチームの情報をGoogleスプレッドシートに保存します。

最後に

この記事自体も ChatGPT さんに整形してもらいましたw
体感「箇条書きで言いたいことなぐり書きしたら、良い感じに敬語に直してくれる」。
敬語部分のタイプ量数割減、くらい?
便利は便利。

本記事のプロンプト抜粋

このチャットで生成したコードを Qiita に記事化したいです。
https://qiita.com/naohikowatanabe
の文体で qiita 記事の作成作成お願い。
markdown 形式で記載し、全ての出力のプレーンテキストをまとめて一つのコードブロックで出力して。

記事タイトル:ChatGPT(GPT-4) で一撃でスクレイピングするコードを生成出来たので感想とコツ

内容は以下を含めてください。
■今回やりたかったこと
・ChatGPT(GPT-4) で一撃でスクレイピングするコードを生成

■作ったもの概要
・保険組合のウォーキングイベントの会社内の3チームの歩数進捗の slack への自動投稿 bot
 ・処理は大きく2つ
  ・1. 「サイトのスクレイピング&スプシへアップロード」
  ・2. スプシの GAS で投稿文字列作成& slack へ投稿
 ・今回 ChatGPT でやったのは1の方。2は前回半年前開催分のコードをほぼそのまま流用。
 ・運良く(?)今回のタイミングでウォーキングイベントの運営が変わり、
  サイトがまるっと変わったので1のスクレイピングコードは作り直しが必要だった。
264
255
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
264
255