Amazonのほしい物リストから値下げされている商品を通知するPythonプログラムを作ってみました。

もちろんcronで定時動作し、メール送信も可能なので、商品が値下げされたらすぐに購入できます。

プログラムの概要

フローチャート図

フローチャート図

ソースコード

#! /usr/bin/python3
import bs4,requests,time,os,sys,random,re
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
from selenium.webdriver.common.alert import Alert
from selenium.webdriver.common.keys import Keys
from selenium.webdriver import Firefox, FirefoxOptions

#メール送信関係
import smtplib
from email.mime.text import MIMEText
from email.header import Header

#メールの文字コード、送信元、パスワード、宛先の指定
mail_data_file          = open("./mail_data.txt")
raw_mail_data           = mail_data_file.readlines()
mail_data_file.close()

for i in range(len(raw_mail_data)):
    raw_mail_data[i] = re.sub(r"\n","",raw_mail_data[i])

charset         = raw_mail_data[0]
from_email      = raw_mail_data[1]
from_email_pass = raw_mail_data[2]
to_email        = raw_mail_data[3]

#ヘッドレスモードで起動させる。
options = FirefoxOptions()
options.add_argument('-headless')

fp = webdriver.FirefoxProfile("ここにプロファイルのパスを記入してください。")

browser = webdriver.Firefox(fp,options=options)
browser.get("https://www.amazon.co.jp")

TIMEOUT         = 10
WAIT_TIME       = 0.5 
ORIGINAL_URL    = "https://www.amazon.co.jp/exec/obidos/asin/"

#ほしい物リストのリンクを取得する
def get_item_list_link():

    link_list = []

    try:
        html_wait = WebDriverWait(browser,TIMEOUT)
        print("HTML取得中")
        html_elem = html_wait.until(expected_conditions.visibility_of_element_located((By.TAG_NAME, "html")))
        print("HTML取得完了")
    except:
        print("HTMLの取得に失敗しました")

    else:

        #===============アカウント&リストをクリック======================================================
        html_wait = WebDriverWait(browser,TIMEOUT)
        botton_elem = html_wait.until(expected_conditions.visibility_of_element_located((By.XPATH, "//a[@id='nav-link-accountList']")))
        y_pos = botton_elem.location["y"]
        browser.execute_script("window.scroll(0 , " + str(y_pos) + ");")
        time.sleep(WAIT_TIME)
        botton_elem.click()

        #================マイリストをクリック======================================================
        html_wait = WebDriverWait(browser,TIMEOUT)
        botton_elem = html_wait.until(expected_conditions.visibility_of_element_located((By.XPATH, "/html/body/div[1]/div[3]/div/div[5]/div[1]/div/div/ul/li[3]/span/a")))
        y_pos = botton_elem.location["y"]
        browser.execute_script("window.scroll(0 , " + str(y_pos) + ");")
        time.sleep(WAIT_TIME)
        botton_elem.click()

        #================マイリストのリンクを取得======================================================

        link_list_elems   = browser.find_elements_by_xpath("//*[@id='your-lists-nav']/nav/ul/li/span/a")

        for i in range(len(link_list_elems)):
            link_list.append(link_list_elems[i].get_attribute("href"))

    return link_list

#ほしい物リストのURLにアクセスして、値下げされている商品のリストを返却
def check_price_down(url):

    browser.get(url)
    down_item_list = []

    #HTMLの検出作業
    try:
        html_wait = WebDriverWait(browser,TIMEOUT)
        print("HTML取得中")
        html_elem = html_wait.until(expected_conditions.visibility_of_element_located((By.TAG_NAME, "html")))
        print("HTML取得完了")
        time.sleep(2)
    except:
        print("HTMLの取得に失敗しました")

    else:

        for i in range(100):
            html_wait = WebDriverWait(browser,TIMEOUT)
            botton_elem = html_wait.until(expected_conditions.visibility_of_element_located((By.XPATH, "//*[@id='rhf']")))
            y_pos = botton_elem.location["y"]
            browser.execute_script("window.scroll(0 , " + str(y_pos - 150) + ");")
            time.sleep(3)

            parsed_page     = bs4.BeautifulSoup(browser.page_source, "html.parser")
            end_of_list     = parsed_page.select("#endOfListMarker")
            if end_of_list:
                break

        #TIPS:パーサーがlxmlではほしいものリストの下側の商品が取得できないので注意
        parsed_page     = bs4.BeautifulSoup(browser.page_source, "html.parser")
        all_item_list   = parsed_page.select("#g-items > li > span > .a-section > div > .a-fixed-left-grid-inner > div > .a-fixed-right-grid > .a-fixed-right-grid-inner > div > .a-row > div")


        for i in range(len(all_item_list)):
            check_string = all_item_list[i].text
            if "下がりました" in check_string:
                down_item_list.append(all_item_list[i])

    return down_item_list

#値下げされている商品の情報を抜き取って、二次元リストに格納して返却
def parse_down_item_list(down_item_list):

    item_info_list = []

    if down_item_list != [] and type(down_item_list) == list:
        for i in range(len(down_item_list)):
            item_info = []
            parsed_page             = bs4.BeautifulSoup(str(down_item_list[i]), "lxml")

            item_title              = parsed_page.select("h3")
            for i in range(len(item_title)):
                item_info.append(item_title[i].text)

            discount_item_price     = parsed_page.select(".a-offscreen")
            for i in range(len(discount_item_price)):
                item_info.append(discount_item_price[i].text)

            discount_item_reason    = parsed_page.select(".itemPriceDrop")
            for i in range(len(discount_item_reason)):
                discount_string     = discount_item_reason[i].text
                discount_string     = re.sub("下がりました","下がりました。\n",discount_string)
                discount_string     = re.sub("でした","でした。",discount_string)
                item_info.append(discount_string)

            item_link_tag           = parsed_page.select("h3 > a")
            for i in range(len(item_link_tag)):
                link_string         = item_link_tag[i].get("href")
                asin                = re.search(r"/\w{10}/",link_string)
                link_string         = str(asin.group())
                asin                = re.sub("/","",link_string)
                link_string         = ORIGINAL_URL + asin
                item_info.append(link_string)

            item_info_list.append(item_info)

    #値下げされた商品の二次元リストを返却する
    return item_info_list

#Gmailに送信する
def send_mail(item_info_list):

    string = ""

    for i in range(len(item_info_list)):
        for j in range(len(item_info_list[i])):
            string = string + str(item_info_list[i][j]) + "\n"
        string = string + "\n"


    msg = MIMEText("値下げされた商品の価格取得が完了しました。\n\n" + string ,"plain",charset)
    msg["Subject"] = Header("Amazon価格取得完了".encode(charset),charset)

    smtp_obj =  smtplib.SMTP("smtp.gmail.com", 587)
    smtp_obj.ehlo()

    smtp_obj.starttls()

    smtp_obj.login(from_email , from_email_pass)
    smtp_obj.sendmail(from_email, to_email , msg.as_string())
    smtp_obj.quit()

    return True

if __name__ == "__main__":
    try:

        down_item_list = []
        
        link_list = get_item_list_link()
        print(link_list)

        for i in range(len(link_list)):
            down_item_list = down_item_list + check_price_down(link_list[i])
            time.sleep(3)

        #値下げされた商品のリストを再度パースさせる。
        send_mail(parse_down_item_list(down_item_list))

        browser.close()

    except KeyboardInterrupt:
        print("\nprogram was ended.\n")
        sys.exit()

このプログラムの解説

このAmazonの価格取得プログラムをcronで動かすには、3つやることがあります。

  • その1:mail_data.txtに必要事項を入力しておく
  • その2:FirefoxでAmazonにログインして、プロファイルのパスを指定する
  • その3:Seleniumのヘッドレスモードを有効にして、cronに設定する

その1:mail_data.txtに必要事項を入力しておく

まず、mail_data.txtに文字コード、送信元のgmailのメールアドレス、送信元のgmailに対応するGoogleアカウントのアプリパスワード、送信先のメールアドレスの4つを順番に記入していきます。

記入する順番を間違えたり、空行があれば動かないので注意してください。

Gmailのアプリパスワードは二段階認証を有効化した上で登録する必要があります。

mail_dataの記入事項

その2:FirefoxでAmazonにログインして、プロファイルのパスを指定する

Firefoxを使用してAmazonで事前にログインを行い、プロファイルのパスを指定しておく必要があります。

Linuxの場合、プロファイルのパスは/home/user/.mozilla/firefox/xxxxx.defaultなので、そちらをプログラムの冒頭で指定します。

その3:Seleniumのヘッドレスモードを有効にして、cronに設定する

Seleniumのヘッドレスモードを有効にして、cronに設定を施します。

ヘッドレスモードを有効にしておけば、cronの設定で環境変数DISPLAYを指定する必要はないので簡単です。

工夫したところ

lxmlではパースに失敗するので、デフォルトのhtml.parserを採用した

Amazonのほしい物リストをSeleniumでスクロールした後、BeautifulSoupを使用してHTMLの解析をしようとしたところ、どうしてもうまく読み取れませんでした。

そこで、HTMLパーサーをlxmlではなくデフォルトのhtml.parserを指定することで解決しました。

ほしい物リストの商品を読み込ませる

普通にSeleniumを使って、ほしい物リストにアクセスし、BeautifulSoupでパースしても、リストの上部だけしか読み取れません。

何故でしょうか?答えは簡単です。アマゾン側がスクロールしない限り続きのページを読み込まない仕様になっているからです。

だからほしい物リストの上部ページの部分だけ表示して終わるのではなく、最下部までスクロールをすることで読み込ませています。

ちなみに、1つのほしい物リストのスクロール回数は100回までです。無制限にスクロールしても良いと思いますが、場合によっては無限ループに陥ってしまう可能性もあるので、今回は避けました。

動作環境

  • Python3.6以上
  • Selenium、BeautifulSoup、lxml、requestsがインストール済み。

実際に動かしてみる

それでは実際に動かしてみましょう。

起動すると、以下のようにシェルにほしい物リストのURLが出力されるようになっています。後はひたすらそのURLにアクセスしてスクロール、アクセスしてスクロールを繰り返して商品の情報を取得します。

ほしい物リストのURLを取得

取得した商品の中から値下げされている商品だけを抜き出し、文字列に変換してメールで送信。以下の画像のように値下げされた商品のリストが手に入ります。

メールが送信される

もちろん、ヘッドレスモードを有効にしておけばcronでも動くので、任意のタイミングで価格のチェックが可能です。

メリットとデメリット

メリット

  • ほしい物リストの値下げを定時でチェックしてくれるので、後はほったらかしでOK
  • メールで通知してくれるので外出中でもOK
  • 後からほしい物リストに追加した商品にも対応

ほしい物リストの値下げを定時でチェックしてくれるので、後はほったらかしでOK

プロファイルの指定にメールパスワードの入力など、設定はやや面倒ではありますが、一度cronに登録してしまえば後はほったらかしにしていてもメールが届きます。

わざわざ貴重な時間を浪費してAmazonのほしい物リストを徘徊する必要はないので、とても快適です。

メールで通知してくれるので外出中でもOK

gmailが使える環境下であれば、外出中でPCが使えなくても問題はありません。何時でもどこでも通知してくれるのはとても便利。

不要になったらcronから削除すればいいだけですし。

後からほしい物リストに追加した商品にも対応

もちろん、ほしい物リストの商品のURLを事前に記録してアクセスする方式ではないので、後からほしい物リストに登録した商品にも対応しています。

cronに一度登録してしまえば、エンドユーザーはもう何もしなくても良いのです。

デメリット

  • 実装までに手間がかかる

実装までに手間がかかる

実装までに手間がかかります。Gmailの場合はアプリパスワードを取得してテキストファイルに書き込み、Firefoxのプロファイルのパスを指定してcronに設定しなければなりません。

おそらく、非エンジニアの方はここまでやらないでしょうね。

結論

以前はSeleniumを使ってアマゾンアソシエイトのコードを自動生成しましたが、今回はほしい物リストを調べてメールで送信することができました。

ここまでくるとAmazonの操作や作業、情報収集であれば何でもできそうですね。

関連記事

Amazonアソシエイトタグの自動生成プログラム

【内部リンク】Seleniumを使ってAmazonアソシエイトのリンクタグを画像つきでジェネレートしてみた

こちらはAmazonにアクセスしてアマゾンアソシエイトのリンク入りのHTMLタグを自動的に生成するプログラムです。

Amazonは動的サイトなのでSeleniumを使って画像のリンクを取得しています。

YouTubeURLのスクレイピング

【内部リンク】YouTubeの検索結果から動画のURLを抽出するPython関数

YouTubeもAmazonと同様に動的サイトで作られているので、Seleniumを使用してアクセスしています。

URLを標準出力するだけですが、youtube-dlを使えば...うわ何するやめろ