【Python】スクレイピングでヤフオクの自動延長なし商品を抽出する

Pythonはなんやかんやで好きな泉（[@izumin_0401](https://twitter.com/izumin_0401)）です。

普段せどりをするんですが、ヤフオクのスクレイピングをしたい場面があったのでツールを作りました。

:::note info
スクレイピングする際はサーバに迷惑をかけないようにしましょう。
:::

# ブログ記事はこちら

https://traveler0401.com/python-auction-yahoo-tool/

# ヤフオクの自動延長なし商品とは？
ヤフオクは基本的にオークション形式なので、落札時間直前に入札が入ると自動で時間が延長されます。

時間が延長されれば価格が競り上がるので出品者としてはありがたいわけですが、たまに自動延長の設定を忘れているユーザがいます（つまり、商品の価格が競り上がらず、時間が来たら終了する）。

こういった商品を狙うことで安く商品を仕入れられます。

# ツールを作った理由
自動延長なしの商品かどうかは、各商品の詳細ページを見ないと分かりません。

![自動延長.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565129/91bd127e-f4a8-1979-dea8-ce4984989a08.png)
↑のように、各商品の詳細ページに自動延長「あり」「なし」の記載があります。

自動延長の確認を手動でやるのは超非効率なのでツールを作りました。

# ツールについて
## ソースコード
```python
import csv
import logging
import os
import random
import time
import tkinter as tk
from functools import wraps
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By

options = Options()
# options.add_argument("--headless")
driver = webdriver.Chrome(options=options)
driver.implicitly_wait(10)

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s",
    datefmt="%Y/%m/%d %H:%M:%S"
)
logger = logging.getLogger(__name__)

def sleep():
    time.sleep(random.uniform(2, 5))

def sleep_after_execution():
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            result = func(*args, **kwargs)
            sleep()
            return result
        return wrapper
    return decorator

def delete_csv_file():
    if(os.path.isfile("disappointing.csv")):
        os.remove("disappointing.csv")

    if(os.path.isfile("automatic_extension.csv")):
        os.remove("automatic_extension.csv")

@sleep_after_execution()
def open_site():
    driver.get("https://auctions.yahoo.co.jp/")

@sleep_after_execution()
def delete_ad():
    try:
        driver.find_element(By.CSS_SELECTOR, "div > a.Close-sc-uncojt.UslsY").click()
    except:
        pass

@sleep_after_execution()
def search(keyword):
    driver.find_element(By.CSS_SELECTOR, "div > input").send_keys(keyword)
    driver.find_element(By.ID, "acHdSchBtn").click()

@sleep_after_execution()
def filter_price(min, max):
    if min is not None:
        driver.find_element(By.XPATH, "//input[@name='min']").send_keys(min)
    
    if max is not None:
        driver.find_element(By.XPATH, "//input[@name='max']").send_keys(max)

    driver.find_element(By.CSS_SELECTOR, "form > input.Button.u-marginT10").click()

@sleep_after_execution()
def select_auction_mode():
    driver.find_element(By.CSS_SELECTOR, "div.Tab > ul > li:nth-child(2) > a").click()

@sleep_after_execution()
def sort():
    driver.find_element(By.CSS_SELECTOR, "li:nth-child(1) > div > button").click()
    driver.find_element(By.CSS_SELECTOR, "li:nth-child(1) > ul > li:nth-child(2) > a").click()

@sleep_after_execution()
def select_item_number():
    driver.find_element(By.CSS_SELECTOR, "li:nth-child(2) > div > button").click()
    driver.find_element(By.CSS_SELECTOR, "li:nth-child(2) > div > ul > li:nth-child(2) > a").click()

def get_hrefs_per_page():
    hrefs = []

    hrefs_elem = driver.find_elements(By.CSS_SELECTOR, "div.Product__detail > h3 > a")
    for elem in hrefs_elem:
        href = elem.get_attribute("href")
        hrefs.append(href)

    return hrefs

def get_hrefs():
    hrefs = []

    while True:
        try:
            hrefs.extend(get_hrefs_per_page())
            driver.find_element(By.CSS_SELECTOR, "li.Pager__list.Pager__list--next > a").click()
            sleep()
        except:
            break

    return hrefs

def write_csv(file_name, href):
    with open(file_name, mode="a", newline="", encoding="utf-8") as file:
        writer = csv.writer(file)
        writer.writerow([href])

def create_csv_file(hrefs):
    for href in hrefs:
        driver.get(href)

        try:
            remaining_time_unit = driver.find_element(By.CSS_SELECTOR, "span.Count__detail > span").text
        except:
            continue
            
        if remaining_time_unit == "日":
            break
        
        title = driver.find_element(By.CSS_SELECTOR, "h1").text
        if len(title) < 30:
            write_csv("disappointing.csv", href)

        automatic_extension = driver.find_element(By.CSS_SELECTOR, "tr:nth-child(5) > td").text
        if automatic_extension == "なし":
            write_csv("automatic_extension.csv", href)

        sleep()

def open_url(file_name):
    with open(file_name, newline='') as csv_file:
        urls = csv.reader(csv_file)

        for url in urls:
            driver.execute_script("window.open('');")
            driver.switch_to.window(driver.window_handles[-1])
            driver.get(url[0])

def main(keywords, min_price, max_price):
    try:
        logger.info("処理開始")

        delete_csv_file()

        open_site()
        delete_ad()

        for keyword in keywords:
            logger.info(f"キーワード「{keyword}」を処理中...")

            search(keyword)
            filter_price(min_price, max_price)
            select_auction_mode()
            sort()
            select_item_number()

            hrefs = get_hrefs()
            create_csv_file(hrefs)
        
        open_url("disappointing.csv")
        open_url("automatic_extension.csv")

        logger.info("処理終了")
    except Exception as e:
        logger.error(f"予期せぬエラーが発生しました: {e}")

if __name__ == "__main__":
    root = tk.Tk()
    root.geometry(f"{root.winfo_screenwidth()}x{root.winfo_screenheight()}")
    root.title("自動延長なし商品抽出ツール")

    tk.Label(root, text="検索ワード").grid(row=0, column=0, padx=10, pady=5)
    keyword_input = tk.Text(root)
    keyword_input.grid(row=0, column=1, padx=10, pady=5)

    tk.Label(root, text="最小金額").grid(row=1, column=0, padx=10, pady=5)
    min_price_input = tk.Entry(root)
    min_price_input.grid(row=1, column=1, padx=10, pady=5)

    tk.Label(root, text="最大金額").grid(row=2, column=0, padx=10, pady=5)
    max_price_input = tk.Entry(root)
    max_price_input.grid(row=2, column=1, padx=10, pady=5)

    button = tk.Button(root, text="実行", command=lambda: main(keyword_input.get("1.0", tk.END).strip().split("\n"), min_price_input.get(), max_price_input.get()))
    button.grid(row=3, columnspan=2, pady=10)

    root.mainloop()
```

## 実行方法
```shell
$ python3 yahoo_auction.py 
```
普通にpythonを実行すればOKです。

# ツール解説
基本的に以下のような感じです。
- GUIで「検索ワード」「最小金額」「最大金額」の入力ができる。
- 入力した検索条件を元に、当日の商品情報を抽出する。
- 自動延長なし商品の情報は「automatic_extension.csv」に出力される。
- おまけ機能として、タイトルが短い（30文字以下）商品も抽出。「disappointing.csv」に出力される。

あとは、適当に改良してみてください。

# まとめ
結構作るの大変だったけど、意外と役に立つツールできたから嬉ピヨ。

ではまた。

# 最後に
暇つぶしに[Twitter](https://twitter.com/izumin_0401)と[ブログ](https://traveler0401.com/)もやってるので見てね。