スクレイピングをして中古商品の相場を調べた


普通に検索して表にまとめるのがだるかったのでプログラムを作りました。

今回対象としたWebサイト

  • メルカリ
  • ヤフオク

商品を検索するときに指定した条件

  • ヤフオク: 中古
  • メルカリ: 目立った傷や汚れなし
  • 売り切れ(落札済み)

外れ値の除外

商品の価格は条件が同じであれば正規分布に従うので、外れ値を求めることができます。
今回はスミルノフ=グラブスの検定を利用して外れ値を取得し、取得した外れ値をデータから除外しました。
高い正確性を求めていなかったので、有意水準は5パーセントとして計算しました。

使用したライブラリ

外部ライブラリ

Beautiful Soup 4
スクレイピングを行うためのライブラリ

outlier_utils
スミルノフ=グラブスの検定を行って外れ値を削除するためのライブラリ

pandas
難しい計算ができるライブラリ

Matplotlib
グラフを作成するためのライブラリ

requests
HTTP通信を行うためのライブラリ

標準ライブラリ

re
正規表現を扱うためのライブラリ

statistics
統計的な指標を計算するためのライブラリ

プログラム

scraping.py
# 外部ライブラリ
from bs4 import BeautifulSoup
from outliers import smirnov_grubbs as grubbs
import pandas as pd
import matplotlib.pyplot as plt
import requests

# 標準ライブラリ
import re
import statistics

# User-Agentは403エラーを防ぐために必ず書く
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' \
              ' (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36'
}

prices = []
urls_yahoo_auction = []
urls_mercari = []

#キーワードをリストkeywordsの要素として指定する(複数指定できます)
keywords = ['hoge', 'fuga', 'hogebook air', 'fugapad pro 12.9']

for keyword in keywords:
    keyword = keyword.replace(' ', '+')
    # istatus=0がすべて、istatus=1が未使用、istatus=2が中古を表す
    # n=20が20件、n=50が50件、n=100が100件表示することを表す
    url_yahoo_auction = 'https://auctions.yahoo.co.jp/closedsearch/' \
                        'closedsearch?p=' + keyword + \
                        '&istatus=2&n=100'

    urls_yahoo_auction.append(url_yahoo_auction)

    # item_condition_id%5B3%5D=1は目立った傷や汚れなし
    # item_condition_id%5B4%5D=1はやや傷や汚れあり
    # status_trading_sold_out=1は売り切れ
    # status_on_sale=1は販売中
    url_mercari = 'https://www.mercari.com/jp/search/?keyword=' + keyword + \
                  '&item_condition_id%5B3%5D=1&status_trading_sold_out=1'

    urls_mercari.append(url_mercari)

def get_contents(url, class_):
    response = requests.get(url, headers=headers)
    response.encoding = response.apparent_encoding

    soup = BeautifulSoup(response.text)
    return soup(class_= class_)

for url_mercari in urls_mercari:
    for content_price_class in get_contents(url_mercari,
                                            'items-box-price font-5'
                                            ):
        price = re.sub(r'<div class="items-box-price font-5">¥' + \
                       r'([0-9]+)?,?([0-9]+)</div>', r'\1\2',
                       str(content_price_class)
                       )
        prices.append(int(price))

for url_yahoo_auction in urls_yahoo_auction:
    for content_price_class in get_contents(url_yahoo_auction,
                                            'Product__priceValue'
                                            ):
        price = re.sub(r'<span class="Product__priceValue">([0-9]+)?,' \
                       r'?([0-9]+)円</span>', r'\1\2', str(content_price_class)
                       )
        prices.append(int(price))

data = pd.Series(prices)
tested_data = grubbs.test(data, alpha=0.05).values
prices = tested_data.tolist()
print('サンプルサイズ:', len(prices))

# 四捨五入して中央値を整数で求める
median = round(statistics.median(prices))
# 四捨五入して平均値を整数で求める
mean = round(statistics.mean(prices))

list_median = []
list_str_median = list(reversed(str(median)))

list_mean = []
list_str_mean = list(reversed(str(mean)))

# 分かりやすいように数値にカンマを付ける
def get_str_value(l, list_str):
    for i in range(0, len(list_str), 3):
        if len(list_str[i:i + 4]) == 4:
            l += list_str[i:i + 3] + [',']
        else:
            l += list_str[i:i + 3]

    return ''.join(reversed(l))

str_median = get_str_value(list_median, list_str_median)
str_mean = get_str_value(list_mean, list_str_mean)

print('中央値:', str_median + '円')
print('平均値:', str_mean + '円')

# 中古価格のヒストグラムを作り
plt.hist(prices)
# それを描画する
plt.show()

実装例

Surface Pro 7 Core i5 ROM 256GB RAM 8GBの2つのモデルを型番で検索してみました。
この場合はkeywords = ['PUV-00014', 'PUV-00027']と記述します。

このようにモデル名を指定すると比較的正確なデータを取得できます。