次の DEMO を見に行く
IT

Pythonで実現する商品価格の自動監視システムと通知のセットアップガイド スクレイピング Selenium

Dori

気になっている商品が安くなったら買いたい!
という目的のためのプログラムをPythonで書いてみました。

開発経緯

誰しもが欲しい商品があるけど価格が高いせいで諦めたとう経験はあると思います。

ですが1日に何回もサイトを開いて値段を確認するのは時間の無駄ですよね?

特に今回、このプログラムを開発しようと思ったのはNBAのジャージーを安く買いたいという理由が大きいです。

私はアメリカに住んでいるのですが、スポーツ用品最大手のDick’s Sporting Goodsでは選手が移籍して直ぐに在庫処分として大幅に値下げセールをする事で有名です。

具体的にはどの選手でも$120のジャージーが$30になって販売されます。

安すぎるのでいつもすぐ売り切れてしまうのですが、WEBサイトに張り付いて価格が落ちるのを待つわけにもいかないのでプログラムにやってもらう事にしました。

HTMLを理解して差し替えの必要がありますが、他のサイトでも応用が効くと思います。

ソースコード

今回はいくつかソースコードがあるので紹介します。

スクレイピングで行う場合

import discord
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
import asyncio

TOKEN = 'your_token_here'  # トークンをここに入れます
CHANNEL_ID = your_channel_id_here  # チャンネルIDを数値で入れます
URL = '' #監視したいサイトのURL

intents = discord.Intents.all()
client = discord.Client(intents=intents)
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument("--headless")
chrome_options.add_argument(f"user-agent={USER_AGENT}")

async def check_price():
    while True:  # 無限ループで定期的に価格をチェック
        with webdriver.Chrome(options=chrome_options) as driver:
            try:
                driver.get(URL)  # URLを文字列リテラルではなく変数として使用
                await asyncio.sleep(5)  # time.sleepではなくasyncio.sleepを使用

                element_visible = EC.visibility_of_element_located(
                    (By.CSS_SELECTOR, ".product-price-container .product-price.price-color.ng-star-inserted"))
                WebDriverWait(driver, 40).until(element_visible)  # タイムアウトを40秒に設定

                current_price_element = driver.find_element_by_css_selector(".product-price-container .product-price.price-color.ng-star-inserted")
                price = current_price_element.text.strip()
                
                channel = client.get_channel(CHANNEL_ID)
                await channel.send(f"The current price is: {price}")
                
            except TimeoutException:
                driver.save_screenshot('error_screenshot.png')
                print("Error in selenium: TimeoutException - Message: ")
            finally:
                await asyncio.sleep(1800)  # 1800秒(30分)待機

@client.event
async def on_ready():
    print("Bot is ready! Starting price check...")
    client.loop.create_task(check_price())  # バックグラウンドで価格チェックタスクを起動

client.run(TOKEN)

HTMLエレメントは監視したいサイトのものに変更してください。

30分毎に現在の価格をDiscordに通知してくれます。

必要に応じてDiscordをLINEやSlackに変更してください。

今回の監視対象サイトはプログラムのアクセスを拒否しており、このコードは使用する事ができませんでしたが他のサイトでは使える可能性があります。

Seleniumで行う場合

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

# Chromeのオプションを設定する
chrome_options = Options()
chrome_options.add_argument("--headless")  # ヘッドレスモードで実行

# カスタムユーザーエージェントの設定
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36"
chrome_options.add_argument(f"user-agent={USER_AGENT}")

# Chromedriverのパスを指定
service = Service('/opt/homebrew/bin/chromedriver')

# WebDriverインスタンスの生成
driver = webdriver.Chrome(service=service, options=chrome_options)

try:
    # URLにアクセス
    driver.get('your_url')
    
    # コンテンツがロードされるまで待機(必要な場合)
    # WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.CSS_SELECTOR, "some-element-selector")))

    # ページソースの取得
    html_source = driver.page_source

    # ファイルに保存
    with open('/your_path/selenium_source.txt', 'w', encoding='utf-8') as file:
        file.write(html_source)
    
    print("HTML source has been saved successfully")

except Exception as e:
    print(f"An error occurred: {e}")
finally:
    # ドライバーの終了
    driver.quit()

今度はSeleniumでサイトに行ってHTMLのソースをローカルに保存するサンプルコードです。

このコードもを実行したらCAPTCHAが表示されました…
また失敗です。

PythonでCAPTCHAを突破する方法はこちらにありますが今回は別の方法で試してみます。

Seleniumを使用して半手動で行う


from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
import time

# Chromeのオプションを設定する
chrome_options = Options()
# chrome_options.add_argument("--headless")  # ヘッドレスモードで実行したい場合はこの行をコメントアウト

# カスタムユーザーエージェントの設定
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36"
chrome_options.add_argument(f"user-agent={USER_AGENT}")

# Chromedriverのパスを指定
service = Service('/opt/homebrew/bin/chromedriver')

# WebDriverインスタンスの生成
driver = webdriver.Chrome(service=service, options=chrome_options)

try:
    # URLにアクセス
    driver.get('')
    
    # ここでユーザーが手動でCaptchaを解決するのを待つ
    input("Please solve the Captcha and then press Enter here...")

    # Captchaが解決された後のページソースの取得
    html_source = driver.page_source

    # ファイルに保存
    with open('/your_path/selenium_source.txt', 'w', encoding='utf-8') as file:
        file.write(html_source)
    
    print("HTML source has been saved successfully ")

except Exception as e:
    print(f"An error occurred: {e}")
finally:
    # ドライバーの終了
    driver.quit()

次はヘッドレスモードにしてユーザーが手動でCAPTCHAを突破した後にEnterを押して実行するコードです。

これで問題なくHTMLソースが保存されましたが、実質手動で行なっているので本来の目的は達成していません。

pyautoguiでHTMLソースを取得する方法

import pyautogui
import pyperclip
import time

# 新しいURLをクリップボードにコピーする
new_url = ''
pyperclip.copy(new_url)

# ブラウザのアドレスバーに焦点を合わせる
pyautogui.click(x=0, y=0)  # この座標は環境に合わせて調整

# 現在のURLを全選択する
pyautogui.hotkey('command', 'a')

# 少し待つ(コンピューターの速度によっては調整が必要かもしれません)
time.sleep(0.5)

# 選択したURLを削除する
pyautogui.press('backspace')

# クリップボードの内容(新しいURL)をペーストする
pyautogui.hotkey('command', 'v')

# Enterキーを押して新しいURLに移動する
time.sleep(3)
pyautogui.press('enter')
time.sleep(3)
pyautogui.press('enter')

# 少し待つ
time.sleep(5)

# x 881, y 297の座標に移動して「Command + Option + U」を実行する
pyautogui.click(x=0, y=0)
pyautogui.hotkey('command', 'option', 'u')
time.sleep(3)  # ソースページが開くのを待つ

# 「Command + A」で全て選択
pyautogui.hotkey('command', 'a')
time.sleep(4)

pyautogui.hotkey('command', 'c')
time.sleep(3)

# クリップボードからコピーした内容を取得
source_html = pyperclip.paste()

# Pythonでtxtファイルとしてコピーした内容をローカルに保存
with open('/your_path/html_source.html', 'w', encoding='utf-8') as file:
    file.write(source_html)

print('HTML source has been copied')


スクレイピングもSeleniumもダメならクライアントのマウスをPythonで動かすことにしました。

クリックに必要な座標は以下のコードで求められます。

from pynput.mouse import Listener

def on_move(x, y):
    print('Pointer moved to {0}'.format((x, y)))

# マウスリスナーを設定
with Listener(on_move=on_move) as listener:
    listener.join()

このスクリプトを実行すると、マウスを動かすたびにコンソールにその座標が表示されます。

スクリプトを停止するには、コンソールでCtrl+Cを押してください。

HTMLのソースコード自体は問題なく取得してローカルに保存できたのですが必要な価格の情報がありませんでした。

現代のWebアプリケーションでは、多くのデータがJavaScriptによって動的に生成されるため、ブラウザの「ページのソースを表示」機能を使っても見つけることができないことがあるようです。

またも失敗…

スクショを撮って送る方法

import pyautogui
import pyperclip
import time
from PIL import ImageGrab
import discord
import asyncio

async def take_screenshot_and_send():
    # 新しいURLをクリップボードにコピーする
    new_url = 'e'
    pyperclip.copy(new_url)

    # ブラウザのアドレスバーに焦点を合わせる
    pyautogui.click(x=753, y=89)  # この座標は環境に合わせて調整

    # 現在のURLを全選択する
    pyautogui.hotkey('command', 'a')

    # 少し待つ
    await asyncio.sleep(0.5)

    # 選択したURLを削除する
    pyautogui.press('backspace')

    # クリップボードの内容(新しいURL)をペーストする
    pyautogui.hotkey('command', 'v')

    # Enterキーを押して新しいURLに移動する
    await asyncio.sleep(5)
    pyautogui.press('enter')

    # 少し待つ
    await asyncio.sleep(10)

    # 座標を設定
    left_x =
    top_y =
    right_x =
    bottom_y =

    # スクリーンショットの範囲を指定
    bbox = (left_x, top_y, right_x, bottom_y)

    # スクリーンショットを撮影
    screenshot = ImageGrab.grab(bbox)

    # スクリーンショットを保存
    save_path = '/your_path/screenshot.png'
    screenshot.save(save_path)

    print(f"Screenshot saved to {save_path}")

    # Discordに送信
    await send_screenshot(save_path)

async def send_screenshot(save_path):
    await client.wait_until_ready()
    channel = client.get_channel(CHANNEL_ID)
    if channel:
        await channel.send(file=discord.File(save_path))
    else:
        print("Channel not found.")


# トークンとチャンネルIDを設定
TOKEN = ''
CHANNEL_ID =

intents = discord.Intents.all()
client = discord.Client(intents=intents)

@client.event
async def on_ready():
    print(f'Logged in as {client.user.name}')
    while True:
        await take_screenshot_and_send()
        await asyncio.sleep(3600)  # 1時間待つ

# ボットを実行
client.run(TOKEN)

今回試した方法はpyautoguiでWEBサイトを最新版に更新してPILでスクショを撮ったものをローカルで保存してそれをDiscordのbotに1時間毎に送ってくれるプログラムです。

今回はやっと上手く動いてくれました!

座標、URL、ID、Tokenはご自身のものに差し替えてご使用ください。

またWEBブラウザは既に開いている前提のコードですので開く作業も追加したい場合はご自身で追加してください。

このコードはローカルマシンのGUIを使ってスクリーンショットを撮るためのもので、クラウドサーバーやGUIがない環境では使用することができないので注意してください。

ずっとローカルのマシンを起動したままにするのとマウスも実行中は使えなくなるなど不便なコードではあります。

また値下げがあったら通知をするわけでは無いので定期的に確認する必要があります。

改善案としてはOCRでスクショの文字を読み取って元の価格よりも低い場合のみ指定したアプリに通知する方法です。

import pyautogui
import pyperclip
import time
from PIL import ImageGrab
import discord
import asyncio
from PIL import Image
import pytesseract

async def take_screenshot_and_send():
    # 新しいURLをクリップボードにコピーする
    new_url = 'モニターしたいサイトのURLを入れてください'
    pyperclip.copy(new_url)

    # ブラウザのアドレスバーに焦点を合わせる
    pyautogui.click(x=0, y=0)  # この座標は環境に合わせて調整

    # 現在のURLを全選択する
    pyautogui.hotkey('command', 'a')

    # 少し待つ
    await asyncio.sleep(0.5)

    # 選択したURLを削除する
    pyautogui.press('backspace')

    # クリップボードの内容(新しいURL)をペーストする
    pyautogui.hotkey('command', 'v')

    # Enterキーを押して新しいURLに移動する
    await asyncio.sleep(5)
    pyautogui.press('enter')

    # 少し待つ
    await asyncio.sleep(10)

    # 座標を設定
    left_x = 0
    top_y = 0
    right_x = 0
    bottom_y = 0

    # スクリーンショットの範囲を指定
    bbox = (left_x, top_y, right_x, bottom_y)

    # スクリーンショットを撮影
    screenshot = ImageGrab.grab(bbox)

    # スクリーンショットを保存
    save_path = '/Users/your_path/screenshot.png'
    screenshot.save(save_path)

    print(f"Screenshot saved to {save_path}")

    # Discordに送信
    # await send_screenshot(save_path)

    text = pytesseract.image_to_string(Image.open(save_path))

    # 結果を出力します
    print(text)

    price_change_message = "価格が変更になりました"

    # 変更がある可能性のある値段を設定
    word_looking = "0" 

    if word_looking not in text:
        await send_message(CHANNEL_ID, price_change_message)
        await send_screenshot(save_path)
    else:
        print('値段に変更はありません')

async def send_message(channel_id, message):
    channel = client.get_channel(channel_id)
    if channel:
        await channel.send(message)
    else:
        print("Channel not found.")

async def send_screenshot(save_path):
    await client.wait_until_ready()
    channel = client.get_channel(CHANNEL_ID)
    if channel:
        await channel.send(file=discord.File(save_path))
    else:
        print("Channel not found.")


# トークンとチャンネルIDを設定
TOKEN = ''
CHANNEL_ID = 

intents = discord.Intents.all()
client = discord.Client(intents=intents)

@client.event
async def on_ready():
    print(f'Logged in as {client.user.name}')
    while True:
        await take_screenshot_and_send()
        await asyncio.sleep(600)  # 10分待つ

# ボットを実行
client.run(TOKEN)

上記のコードは撮ったスクショをOCRで解析して価格に変更があった場合のみDiscordに通知を送る方法です。

思っていた通りに上手く動いてくれました。

まとめ

今回作成したコードはGUIに依存する、マウスが一時的に使用不可能になるなど色々不便な点はありますが目的は達成できたので良しとします。

Discordのbotをクライアントで運用する際はスリープモードを解除して電源を付けっぱなしにしないといけないのでPCへの負荷が高いのも難点です。

少し調べたところ、SeleniumからCAPTCHAの突破も出来ない事はないと思うので機会があったら挑戦してみようと思います。

Follow me!

ABOUT ME
Dori
Dori
アメリカ在住。 趣味のNBA観戦、Magic The Gathering、プログラミング、読書、英語学習やアメリカの生活について雑多な記事をブログで綴っています。
PAGE TOP
記事URLをコピーしました