🤖

PythonとSeleniumでAmazon自動購入botを作る

2022/10/19に公開約12,400字

はじめに

プロコンが品薄で全く買えません…😭
こちらの入荷速報アプリによると、特にAmazonではほぼ毎日入荷されているようなのですが、通知直後であってもカートに入れることすらままならない状況が続いています。

そこで、PythonとSleniumを使い簡単な自動購入プログラムを作ってみたいと思います。
なお、自分はプログラミング初学者であり素人同然ですので、内容についてはご容赦ください。

環境構築

自分の環境は以下の通りです。他の環境については分からないので各々で調べてください。

  • OS:macOS
  • Python:Miniforge環境(Anaconda系の1つ)
  • ブラウザ:Google Chrome
  • エディタ:VSCode

Pythonのインストール

何はともあれ、まずはPythonがないと始まりません。
インストール方法は色々とありますが、仮想環境の使い勝手などから個人的にはMiniforgeの利用をおすすめします。(詳しく知りたい方はこちらなど)

インストール方法

ターミナルを開き、次のコマンドでMiniforgeをインストールします。

brew install --cask miniforge

完了後、次のコマンドで初期設定をします。

conda init "$(basename "${SHELL}")"

ターミナルを開き直して(base)と文頭に表示されていれば正しくインストールできています。
試しにconda -Vと入力しバージョンを確認してみましょう。

❯❯❯ conda -V            
conda 22.9.0

なお(base)というのはconda環境をインストールするとデフォルトで立ち上がる環境ですが、これが鬱陶しい方は次のコマンドでオフにできます。

conda config --set auto_activate_base false

仮想環境の作成

Miniforgeをインストールした最大の理由がこの仮想環境です。仮想環境を利用することで異なるバージョンのPython環境を用意できたり、トラブル時に環境ごと消去して新たに作り直すことができます。

基本的なコマンドは以下の通りです。

仮想環境の作成
conda create -n 仮想環境名
仮想環境の一覧を表示
conda env list
仮想環境を有効化
conda activate 仮想環境名
仮想環境を無効化
conda deactivate
仮想環境の削除
conda remove -n 仮想環境名 --all

今回は次のコマンドで仮想環境の作成と同時にPythonをインストールします。

conda create -n myenv python

myenvは仮想環境の名前。
python=3.7とすればインストールするバージョンを指定できます。

Seleniumのインストール

Seleniumとはウェブブラウザをプログラムによって制御するためのツールです。
今回はGoogle Chromeを操作するために使用します。

先ほど作った仮想環境を有効化します。

conda activate myenv

condaコマンドを用いてSeleniumをインストールします。
バージョンを指定しなければSelenium4がインストールされます。

conda install selenium

seleniumがブラウザを操作するために必要なwebdriverをインストールします。
(webdriver-managerをインストールすることで適切なwebdriverを自動で選択してくれる)

conda install webdriver-manager

また、たまにModuleNotFoundError: No module named 'packaging'とエラーが出ることがあるのでpackagingも入れておきます。

conda install packaging

Google Chromeのインストール

公式からインストールするか、以下のコマンドを実行。

brew install --cask google-chrome

VSCodeのインストール

公式からインストールするか、以下のコマンドを実行。

brew install --cask visual-studio-code

最低限の拡張機能として以下のものを入れます。(左タブの拡張機能から検索)

  • Japanese Language Pack for Visual Studio Code
  • Python(Microsoftのもの)

VSCode自体の設定は各々調べるなりお好みでどうぞ。

使い方

左タブのエクスプローラーから「フォルダーを開く」をクリック、workspaceなど適当な名前をつけて作業用のフォルダを作って開きます。フォルダー名の右側のアイコン「新しいファイル」をクリックし、program1.pyなど適当なPythonファイルを作成、それにプログラムを書いていきます。
実行は右上の三角「新しいPythonファイル」をクリックして実行します。

試しに次のようなプログラムを実行して動作確認をしてみてください。

サンプル
print("Hello World")

もし実行中のプログラムをキャンセルしたいときは、下のターミナルタブでCtrlCを入力します。

さて、これでようやくプログラムを動かせる環境が整いました。
次は実際にプログラムを書いてみます。

自動購入プログラムの作成

Amazonの前提知識

Amazonで商品を買うときは普通、次のような手順を踏むかと思います。

商品ページにアクセス
  ↓
カートに入れる
  ↓
レジへ進む
  ↓
お届け先・支払い方法の設定
  ↓
注文を確定

プロコンなどの人気商品の場合、商品入荷時にいかに素早くこの購入処理をこなせるかが勝負の鍵です。
予めログインしてお届け先や支払い方法を登録しておくのは当然ですが、もう一つポイントがあります。
それがカートへ戻すボタンの活用です。

カートへ戻すは「あとで買う」に入った商品をカートに戻すためのボタンで、カートに入れるボタンよりも先に表示されやすいです。つまりこれを使うことで購入確率を高めることができます。

このような理由から、プログラムは次の2つに分けて作成したいと思います。

  1. 「あとで買う」にプロコンを入れる
  2. 「カートに戻す」ボタンからプロコンを購入する

1. 「あとで買う」に商品を入れる

Amazonではカートに入っている商品の在庫が無くなれば、自動的に「あとで買う」に移動されます。
プロコンなどの人気商品の場合は入荷されても一瞬で在庫切れになってしまいますので、一度でも正常にカートに入れることができれば、その後は自動的に「あとで買う」に移動されます。

なのでプログラムは、「商品が入荷されていればカートに入れる」という内容で十分なはずです。
では実際に書いてみます。

program1.py
# Selenium4
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service as ChromeService
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
from webdriver_manager.chrome import ChromeDriverManager
from time import sleep

# ドライバの自動インストール
service = ChromeService(executable_path=ChromeDriverManager().install())
# ドライバの起動
driver = webdriver.Chrome(service=service)
# 要素検索時のデフォルト待機時間(最大)
driver.implicitly_wait(0.5)
# 明示的な待機時間(最大)
wait = WebDriverWait(driver, 5)


####################
# 設定する項目
####################
# Amazonのログイン用メールアドレス
email = "メールアドレス"
# Amazonのログイン用パスワード
password = "パスワード"
# 商品ページのURL(Amazon公式販売のリンクを指定、一例としてプロコンのURL)
url = "https://www.amazon.co.jp/dp/B01NCX3W3O?_encoding=UTF8&m=AN1VRQENFRJN5&th=1&linkCode=sl1&tag=iiii1-22&linkId=01154c662ccaef939e284587bc5af761&language=ja_JP&ref_=as_li_ss_tl"


####################
# ログイン処理
####################
# Amazonのログインページへ遷移
driver.get("https://www.amazon.co.jp/ap/signin?openid.pape.max_auth_age=0&openid.return_to=https%3A%2F%2Fwww.amazon.co.jp%2Fref%3Dnav_signin&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.assoc_handle=jpflex&openid.mode=checkid_setup&openid.claimed_id=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0&")
# キー入力とクリック処理(クリック可能になるまで待機してクリック)
driver.find_element(By.ID, "ap_email").send_keys(email)
wait.until(EC.element_to_be_clickable((By.ID, "continue"))).click()
driver.find_element(By.ID, "ap_password").send_keys(password)
wait.until(EC.element_to_be_clickable((By.ID, "signInSubmit"))).click()
# 読み込み終わるまで待機
sleep(5)


####################
# 商品の監視
####################
# 商品ページへ移動
driver.get(url)

# 監視処理のループ
while True:
    try:
        # カートに入れるボタンがあるか確認
        if driver.find_elements(By.ID, "add-to-cart-button"):
            # あればクリックできるまで待機してクリック
            wait.until(EC.element_to_be_clickable((By.ID, "add-to-cart-button"))).click()
            # レジへ進むボタンが見つかればカートに追加出来たとみなしてループを抜ける
            if driver.find_elements(By.ID, "sc-buy-box-ptc-button"):
                break
        # なければ例外処理へ進む
        else:
            raise Exception
    except:
        # 入荷なし、もしくは何らかのエラーが発生した場合
        # 10秒待機して商品ページに移動
        sleep(10)
        driver.get(url)

print("処理を完了しました!")

# ブラウザの終了
driver.quit()

最初の辺りはおまじないとして、使用している関数の説明は以下の通りです。

関数の説明
# 指定のURLを読み込む
driver.get("URL")

# 要素を検索する
driver.find_element(By.要素の種類, "要素の名前")

# 要素をクリックする
driver.find_element(By.要素の種類, "要素の名前").click()

# 要素にキー入力を送る
driver.find_element(By.要素の種類, "要素の名前").send_keys("入力したい文字")

# 要素がクリック可能になるまで待機する(最大5秒)
WebDriverWait(driver,5).until(EC.element_to_be_clickable((By.要素の種類, "要素の名前"))).click()

# 次のように書いても良い
wait = WebDriverWait(driver, 5)
wait.until(EC.element_to_be_clickable((By.要素の種類, "要素の名前"))).click()

# 単純に10秒待機する
sleep(10)

「要素の種類」「要素の名前」というのはブラウザから確認できます。
例えばSafariであれば、「カートに入れる」ボタンを右クリックして「要素の詳細を表示」を選択すると以下のように種類がIDで名前がadd-to-cart-buttonであると分かります。

要素を確認する例

また例外処理でページをリロードする方法としてdriver.refresh()を使う方法もありますが、このコマンドではAmazon側で何らかのエラー画面に飛ばされてしまうとそのページをリロード続けてしまうため、あえてdriver.get()で商品ページを指定しています。

2. 「カートに戻す」ボタンからプロコンを購入する

さて、無事に「あとで買う」に商品を入れることができたでしょうか。
次は遂に、実際の購入処理まで含めたプログラムを書いてみたいと思います。

program2.py
# selenium 4
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service as ChromeService
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
from webdriver_manager.chrome import ChromeDriverManager
from time import sleep

# ドライバの自動インストール
service = ChromeService(executable_path=ChromeDriverManager().install())
# ドライバの起動
driver = webdriver.Chrome(service=service)
# 要素検索時のデフォルト待機時間(最大)
driver.implicitly_wait(0.5)
# 明示的な待機時間(最大)
wait = WebDriverWait(driver, 5)


####################
# 設定する項目
####################
# Amazonのログイン用メールアドレス
email = "メールアドレス"
# Amazonのログイン用パスワード
password = "パスワード"
# 商品価格のXPATH
price_xpath = "" # 例://*[@id="hogehoge"]/div[4]/div/div[2]/ul/div/p[1]/span
# 商品をカートへ戻すボタンのNAME
movetocart_NAME = "" # 例:submit.move-to-cart.hogehoge
# 入荷確認用の適正価格
correct_price = "" # 例:¥7,678


####################
# ログイン処理
####################
# Amazonのログインページへ遷移
driver.get("https://www.amazon.co.jp/ap/signin?openid.pape.max_auth_age=0&openid.return_to=https%3A%2F%2Fwww.amazon.co.jp%2Fref%3Dnav_signin&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.assoc_handle=jpflex&openid.mode=checkid_setup&openid.claimed_id=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0&")
# キー入力とクリック処理(クリック可能になるまで待機してクリック)
driver.find_element(By.ID, "ap_email").send_keys(email)
wait.until(EC.element_to_be_clickable((By.ID, "continue"))).click()
driver.find_element(By.ID, "ap_password").send_keys(password)
wait.until(EC.element_to_be_clickable((By.ID, "signInSubmit"))).click()
# 読み込み終わるまで待機
sleep(5)


####################
# 商品の監視と購入処理
####################
# カート画面へ移動
driver.get("https://www.amazon.co.jp/gp/cart/view.html?ref_=nav_cart")

# レジへ進むボタン
checkout_ID = "sc-buy-box-ptc-button"
# お届け先や支払い方法などの確認ボタン
confirm_ID = "orderSummaryPrimaryActionBtn"
# 次へ進むボタン
continue_ID = "continue-bottom"
# 注文確定ボタン
order_ID = "submitOrderButtonId"


# 監視と購入処理のループ
while True:
    try:
        # 価格の初期化
        price = 0
        # 商品の価格を取得
        if driver.find_elements(By.XPATH, price_xpath):
            price = driver.find_element(By.XPATH, price_xpath).text

        # 商品が入荷しているとき
        if price == correct_price:
            # 試行回数のカウンタ
            count = 0
            # 最大で100回繰り返す
            while count < 100:
                # カートに戻すボタンがあるか確認
                if driver.find_elements(By.NAME, movetocart_NAME):
                    wait.until(EC.element_to_be_clickable((By.NAME, movetocart_NAME))).click()
                # レジへ進むボタンがあるか確認
                if driver.find_elements(By.ID, checkout_ID):
                    wait.until(EC.element_to_be_clickable((By.ID, checkout_ID))).click()
                # お届け先、支払い方法などの確認ボタンがあるか確認
                if driver.find_elements(By.ID, confirm_ID):
                    wait.until(EC.element_to_be_clickable((By.ID, confirm_ID))).click()
                # 次へ進むボタンがあるか確認
                if driver.find_elements(By.ID, continue_ID):
                    wait.until(EC.element_to_be_clickable((By.ID, continue_ID))).click()
                # 注文確定ボタンがあるか確認
                if driver.find_elements(By.ID, order_ID):
                    wait.until(EC.element_to_be_clickable((By.ID, order_ID))).click()
                    # 内ループを抜ける
                    break
                sleep(2.5)
                driver.refresh()
                count += 1
            # 100回を超えたときは例外処理に移行
            else:
                print("試行回数が100を超えたためカートに戻ります")
                raise Exception
            # 注文確定をクリックできたとき外ループを抜ける
            break
        # 入荷していないときは例外処理へ進む
        else:
            raise Exception
    # 入荷なし、もしくは何らかのエラーが発生したとき
    except:
        # 10秒待機してカートへ移動
        sleep(10)
        driver.get("https://www.amazon.co.jp/gp/cart/view.html?ref_=nav_cart")

print("処理を完了しました!")

# ブラウザの終了
driver.quit()

単純なプログラムですが一応概要を説明すると、まずカート画面で「あとで買う」に入れた商品の価格を監視、指定した価格と一致すれば購入処理(内ループ)に移ります。購入が成功すればプログラムは終了、購入処理の回数が100回を超えた場合は失敗したとしてカート画面へ戻り、再度監視を続けます。

二重のループにしている理由について、Amazonでは購入時のエラー画面でページをリロードすると先へ進めることがあるためループ処理を分けています。またif文を多用したお見苦しいコードになっている理由は、単純にAmazonのエラー画面の種類が多すぎて網羅するのが面倒だったからです…。
要するに、どんな場面であっても常に各ボタンの存在を探すという力技で解決しようとしました。その為、毎回全部のボタンを探す時間が大きなロスとなっています。(もっと良いやり方があるはず…)

その他として内ループは外ループより繰り返しの間隔を短くしています。要素検索のデフォルト待機時間が0.5秒でボタンの数が5つなので2.5秒、sleep(2.5)と合わせて5秒ごとというつもりですが、bot判定的にはだいぶ怪しいと思うので不安な方は待機時間を伸ばすことをおすすめします。(もちろん時間を増やすだけ購入確率は落ちる)

XPATHの設定などは基本的にIDと同じでブラウザの検証機能から確認・コピーして下さい。
またプログラムの改変は自由ですので、各々で使いやすいように適宜変更してもらって結構です。

おわりに

簡単なプログラムですが目的のものは作れたかなと思います。(まだ買えたことないけど…)
それにしても、こんなもの使わずに買える世の中になってほしいなあ〜

Discussion

ログインするとコメントできます