メルカリのスクレイピングをやっていきましょう。
それもメルカリにばれることなくやります。
今回は、商品詳細ページのスクレイピングです。
商品詳細ページをスクレイピングすることにより、商品情報を抽出できます。
本記事の内容
- ここまでの流れ【メルカリのスクレイピング】
- 商品詳細ページのスクレイピング仕様
- 商品詳細ページから商品情報を抽出する
それでは、上記に沿って解説していきます。
ここまでの流れ【メルカリのスクレイピング】
今回は、商品詳細ページのスクレイピングがメインです。
商品情報を取得できれば、スクレイピングとしては完結となります。
完結と書きましたが、メルカリのスクレイピングは段階を踏んで説明しています。
そして、本記事でそのシリーズが完結します。
過去の同シリーズの記事を記載します。
第1弾
第2弾
第3弾
第1弾に関しては、スクレイピングという技術に関して説明しています。
特に、「【必須】Webスクレイピングに関する考え方」だけでは読んでほしいです。
スクレイピングをやって犯罪者になりたくないなら、是非とも読んでください。
同時に、スクレイピングを行うための環境に関しても説明しています。
第2弾と第3弾も必要ならば、読んでください。
今回の記事で出てくる商品IDリストは、それらの記事のサンプルコードをもとに取得しています。
もちろん、それはスクレイピングによってです。
また、スクレイピングの過程を学ぶ上でも第2弾と第3弾の記事は為になります。
スクレイピング初心者にとっては、有益な内容となるでしょう。
次に、商品詳細ページのスクレイピング仕様を説明します。
商品詳細ページのスクレイピング仕様
商品詳細ページをスクレイピングする仕様について説明していきます。
大きく分けて、以下の3つ。
- 商品詳細ページのURL作成
- 商品情報の抽出
- スクレイピングプログラムの起動
以下で説明します。
商品詳細ページのURL作成
第3弾のサンプルコードを実行すると、以下の結果を得ることができました。
['m50918799282', 'm73773136162', 'm38044269702', 'm64368393092', 'm62524801137', 'm53130284232', ~ 'm86730511132', 'm99189037585', 'm74713790806', 'm74530402541', 'm85223463414', 'm78013511733']
これらは、商品IDの一覧です。
360個の商品IDのうちの最初と最後を表示しています。
この商品IDを以下の「●」に設定することになります。
「https://www.mercari.com/jp/items/●/」
例えば、「m99189037585」を設定します。
「https://www.mercari.com/jp/items/m99189037585/」
これで商品詳細ページのURLが出来上がりです。
このURLにアクセスして、スクレイピングを行っていきます。
商品情報の抽出
以下が商品詳細ページの商品情報の表示部分です。
あと、説明文が「売り切れました」の下に表示されるぐらいです。
基本的には、各項目はそのまま取得します。
大分県を都道府県コードに変換することはしません。
カテゴリーとブランドに関しては、それぞれのコードを取得します。
タグを見れば、hrefからカテゴリーIDを取得できます。
ブランドも同じです。
そして、カテゴリーは複数個、ブランドは(最大)一個と想定します。
画像に関しては、先頭(初期表示)の画像を取得していきます。
取得すると言っても、商品IDから自動で画像のURLを作成できます。
画像のURLを一つピックアップ。
https://static.mercdn.net/item/detail/orig/photos/m99189037585_1.jpg
m99189037585_1.jpg
m99189037585_2.jpg
m99189037585_3.jpg
・・・
このように命名ルールがあるようです。
命名ルールがあるので、スクレイピングは不要に感じます。
ただ、画像枚数は情報として価値があるかもしれません。
画像枚数をスクレイピングするようにしましょう。
あと、商品の販売ステータスをどう扱うかですね。
これは状況によって変更する値です。
ちなみに、販売中は次のような表示ボタンとなります。
このような状況に応じて変わるデータは、スクレイピングを行う趣旨で考えます。
今回は、機械学習や統計などで利用する目的によりデータを収集しています。
別の言い方をすると、在庫があるかどうかのなど監視目的でスクレイピングをしていません。
その趣旨からすると、商品の販売ステータスは不要と言えます。
同じような理由で、出品者の評価もスクレイピングの対象外とします。
そもそも、出品者の評価は商品ではなく出品者に関連付いているデータです。
出品者を分析したいなら、出品者のページをスクレイピングすればよいのです。
スクレイピングプログラムの起動
ここで言いたいことは、どのようにしてスクレイピングのプログラムを動かすのかということです。
例えば、商品IDが手元に100個あったとします。
この100個をどのようにして処理していくのかということです。
考えられるのは、以下。
- 1スクリプトで1商品IDを処理:100回起動
- 1スクリプトで10商品IDを処理:10回起動
- 1スクリプトで100商品IDを処理:1回起動
個人的には、「1スクリプトで1商品IDを処理」を採用しています。
この形でプログラムを作成しておくと汎用性が高いからです。
小さく分割しておけば、使い勝手がよいとも言えます。
それにスクレイピングは、そもそもいつどこで処理が停止するのかわかりません。
スクレイピング先のサイトからアクセス禁止の処分を受けることもあります。
それ以前に、ネットワーク不調で動かないことも普通にあります。
そのようなことを考えても、細かく刻んで処理していく方が安全です。
よって、今回は「1スクリプトで1商品IDを処理」の考え方でスクレイピングを行っていきます。
商品詳細ページから商品情報を抽出する
商品詳細ページから商品情報を抽出するコードは、以下。
現時点(2021年2月6日)では元気に動いています。
サンプルコード
import sys import re import bs4 import traceback from selenium import webdriver from selenium.webdriver.chrome.options import Options # ドライバーのフルパス CHROMEDRIVER = "chromedriver.exeのパス" # ドライバー準備 def get_driver(): # ヘッドレスモードでブラウザを起動 options = Options() options.add_argument('--headless') # ブラウザーを起動 driver = webdriver.Chrome(CHROMEDRIVER, options=options) return driver # 対象ページのソース取得 def get_source_from_page(driver, page): try: # ターゲット driver.get(page) driver.implicitly_wait(10) # 見つからないときは、10秒まで待つ page_source = driver.page_source return page_source except Exception as e: print("Exception\n" + traceback.format_exc()) return None # ソースからスクレイピングする def get_data_from_source(src): # スクレイピングする soup = bs4.BeautifulSoup(src, features='lxml') try: info = {} info["name"] = None info["wording"] = None info["img_count"] = None info["seller"] = None info["category"] = None info["brand"] = None info["condition"] = None info["delivery_fee"] = None info["delivery_method"] = None info["delivery_area"] = None info["delivery_date"] = None info["price"] = None info["tax"] = None info["shipping_fee"] = None info["description"] = None # 商品のbox item_elem = soup.find(class_="item-box-container") # 商品名 name = item_elem.find(class_="item-name").text if name: info["name"] = name # wording wording = item_elem.find(class_="item-wording").text if wording: info["wording"] = wording # 画像枚数 img_elems = item_elem.find_all(class_="owl-dot") img_count = len(img_elems) info["img_count"] = img_count # 項目部分(出品者、カテゴリー、・・・) tr_elems = item_elem.find_all("tr") for elem in tr_elems: heading_name = elem.find("th").text if heading_name == "出品者": a_tag = elem.find("a") if a_tag: href = a_tag.attrs['href'] match = re.findall("\/jp\/u\/(.*)\/", href) if len(match) > 0: seller = match[0] info["seller"] = seller elif heading_name == "カテゴリー": category_list = [] a_tags = elem.find_all("a") for a_tag in a_tags: if a_tag: href = a_tag.attrs['href'] match = re.findall("\/jp\/category\/(.*)\/", href) if len(match) > 0: category_id = match[0] category_list.append(category_id) info["category"] = category_list elif heading_name == "ブランド": a_tag = elem.find("a") if a_tag: href = a_tag.attrs['href'] match = re.findall("\/jp\/brand\/(.*)\/", href) if len(match) > 0: brand = match[0] info["brand"] = brand elif heading_name == "商品の状態": td_text = elem.find("td").text info["condition"] = td_text elif heading_name == "配送料の負担": td_text = elem.find("td").text info["delivery_fee"] = td_text elif heading_name == "配送の方法": td_text = elem.find("td").text info["delivery_method"] = td_text elif heading_name == "配送元地域": td_text = elem.find("td").text info["delivery_area"] = td_text elif heading_name == "発送日の目安": td_text = elem.find("td").text info["delivery_date"] = td_text # 価格 price = item_elem.find(class_="item-price").text if price: # 数値のみ抽出 price = re.sub("\\D", "", price) info["price"] = price # 税金 tax = item_elem.find(class_="item-tax").text if tax: info["tax"] = tax # 送料 shipping_fee = item_elem.find(class_="item-shipping-fee").text if tax: info["shipping_fee"] = shipping_fee # 説明 description = item_elem.find(class_="item-description").text if tax: info["description"] = description return info except Exception as e: print("Exception\n" + traceback.format_exc()) return None if __name__ == "__main__": # 引数 args = sys.argv # 商品ID item_id = "m99189037585" if len(args) == 2: # 引数があれば、それを使う item_id = args[1] # 対象ページURL page = "https://www.mercari.com/jp/items/" + item_id + "/" # ブラウザのdriver取得 driver = get_driver() # ページのソース取得 source = get_source_from_page(driver, page) # ソースからデータ抽出 data = get_data_from_source(source) # データ保存 print(data) # 閉じる driver.quit()
基本的には、第2弾のプログラムとは同じような構造です。
Seleniumでアクセスして、Beautiful Soupでスクレイピングするという形です。
そのため、細かくプログラムの説明はしません。
「商品詳細ページのスクレイピング仕様」と過去記事を参考にしてください。
ただ、以下に関しては説明をしておきます。
- 引数対応
- class名をもとにしたスクレイピング
引数対応
「1スクリプトで1商品IDを処理」を採用したプログラムとなっています。
そのため、コマンドラインで入力した引数を利用するコードになります。
# 引数 args = sys.argv # 商品ID item_id = "m99189037585" if len(args) == 2: # 引数があれば、それを使う item_id = args[1]
上記の部分ですね。
引数がなければ、「m99189037585」をデフォルトで商品IDとします。
なお、コマンドラインで入力した引数ついては次の記事でまとめています。
Pythonで引数を利用するケースについてです。
class名をもとにしたスクレイピング
get_data_from_source関数を見れば、書いている意味がわかると思います。
第2弾、第3弾と読んだ方なら、次のように思うかもしれません。
「メルカリではclass名でスクレイピングするのは無理では?」
はい、確かにカテゴリー一覧(第2弾)では無理でした。
そのため、データ属性をもとにスクレイピングしています。
また、第3弾でもclass名に依存するの危険だと判断しました。
その結果、class名を使ったスクレイピングは避けています。
では、なぜ第4弾はclass名をもとにしたスクレイピングを行うのか?
これは、class名を見て判断しました。
「sc-bwzfXH etiVLC」
商品詳細ページでは、このような意味不明なclass名ではありません。
「item-name」のように意味を持たせたclass名となっています。
そのため、カテゴリー一覧のようにコロコロと変化するモノではないと推測できます。
つまり、商品ページではclass名が固定だと推測できるのです。
よって、商品詳細ページではclass名をもとにスクレイピングをしています。
実行結果
サンプルコードを実行した結果は、以下。
問題なく、実際の商品詳細ページの商品情報を抽出できていますね。
(※description(説明)に関しては、一部省略しています)
まとめ
念のため、以下も説明しておきます。
第3弾の記事内でも説明済みですが、念のため。
# データ保存 print(data)
この部分は、各自で自由に保存するコードを記載してください。
ファイルに保存するのもよし、データベースに登録するのもよしです。
個人的には、今回のようなデータの持ち方ならMongoDBに保存します。
category(カテゴリー)が複数あり、これを正規化したテーブルに格納するのは面倒です。
MongoDBなら、そのままリスト形式で登録できます。
Windowsの場合なら、次の記事でインストールを解説しています。