メルカリを python スクレイピングして出品した商品をバンバン売りたい 

## 免責事項

ScrapeStorm-JP 様のコメントより、メルカリはスクレイピングを禁止しているため、本記事によるメルカリへのスクレイピングを行わないようお願いします。

なお、本記事はPython を利用した勉強のために残しておきますが、あくまでも勉強・利用把握のためだけに利用し、本ソースを利用してのスクレイピングは行わないでください。

利用する場合は、自己責任で行う必要があり、本記事に掲載された内容によって生じた損害等の一切の責任を負いかねますので、ご了承ください。

## きっかけ

みなさん、メルカリ使って不要になったものを売っていますか？
私も不要になった本などをメルカリで出品していますが、どれも古めの参考書・勉強本のため、なかなか売れません。。。

メルカリでは出品する際に、「売れやすい価格」を提案してくれます。
しかし高い金額に設定すると売れないし、逆に安くしすぎるとなんだか損をした気分になるしで苦悩しています。

私は出品物の値段を設定する前に、一度検索をかけて本当の相場はどれくらいなんだろうと調査しています。
(きっと自分だけじゃないはず。。。)

ただ、この作業がなかなか面倒くさく、何とか自動化できないかなと思っていました。
そこで python を使ってメルカリをスクレイピングし、いくらなら売れそうなのかを調査してみました！

※ ページの最後に`github`へのリンクがありますので、ぜひ遊んでみてください！

## 結果のイメージ図

実際にメルカリでスクレイピングを行い、グラフ化した物が以下になります。
こんな感じで、ある商品を売る際には大体 600 円前後に価格を設定すると良いのでは？という結果を得られました。
![mercari_histgram_リンガメタリカ-cd.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407150/78919978-ae41-6ae6-1eaa-5dd52cc5b7eb.jpeg)
以降では、メルカリでスクレイピングをする方法について記述します。

## 環境

- Windows 10 Pro
- Python 3.8.2
- Selenium 3.141.0
- ChromeDriver 80.0.3987.106

## 環境準備

### Python 仮想環境準備

自分のローカル環境が汚れるのが嫌だったので、自分は venv 仮想環境を作成しました。
なお、ローカルには Python 3.8.2 をインストール済みでパスも通してある状態です。

```python
python -m venv venv
```

上記コマンドで、実行したディレクトリ内に`venv`ディレクトリが作成されます。

### 仮想環境内での起動方法

`venv`ディレクトリがある場所で以下のコマンドを入力してください。

```cmd
venv\Scripts\activate
```

これでターミナルの頭に`(venv)`の文字がついていれば仮想環境内に入ることができました。

※ ちなみに、仮想環境から抜け出すには以下のコマンドを入力してください。

```cmd
deactivate
```

### python のモジュール準備

本プログラムを実行するためには以下のモジュールが必要です。
事前に入れておきます。

```cmd
pip install pandas matplotlib
```

### Selenium 環境準備

python の仮想環境内で pip インストールしました。

```cmd
pip install selenium
```

### Chrome Driver 環境準備

以下から、Selenium で利用する Chrome のドライバを用意します。
[ChromeDriver - WebDriver for Chrome](http://chromedriver.chromium.org/downloads)

ここでは、各自の Chrome のバージョンにあった物を選択します。
私の Chrome のバージョンは`80.0.3987.132`だったので、それに一番近い[ChromeDriver 80.0.3987.106](https://chromedriver.storage.googleapis.com/index.html?path=80.0.3987.106/)の windows 版を選択しました。
(本当は 64bit 版が良かったですが、32bit 版しかなかったので仕方なくそれを使いました。)

ちなみに、Chrome のバージョンは以下のようにして確認できます。
`Google Chrome の設定 -> Chrome について(左の三本線をクリックした一番下の項目)`

ダウンロードし、解凍したら`chromedriver.exe`を python ファイルと同じディレクトリに配置します。

## スクレイピング準備

### メルカリ検索用 URL の解析

メルカリで商品を検索する際の URL は以下のようになります。
例 1：「パソコン」で検索。

```cmd
https://www.mercari.com/jp/search/?keyword=パソコン
```

例 2：「パソコン」「中古」で検索。

```cmd
https://www.mercari.com/jp/search/?keyword=パソコン+中古
```

複数ワードで検索する際は、検索ワードとの間に`+`が付く模様。

### HTML 解析

メルカリのページへ移動し、デベロッパーツールを使って HTML のソースを確認してみます。
デベロッパーツールは Web ページ上で「F12」キーで表示できます。

#### 商品情報

以下に、メルカリで検索をかけた時に表示される商品一つの参考情報を明記します。
この情報からスクレイピングでほしい情報を把握します。

```html
<section class="items-box">
  <a href="https://item.mercari.com/jp/~~~~~~~~~~~~~~~~~~~~~~~~~~">
    <figure class="items-box-photo">
      <img
        class="lazyloaded"
        data-src="https://static.mercdn.net/c!/w=240/thumb/photos/~~~~~~~~~~~~"
        alt="パソコン"
        src="https://static.mercdn.cet/c!/w=240/thumb/photos/~~~~~~~~~~~~~~~~"
      />
      <figcaption>
        <div class="item-sold-out-badge">
          <div>SOLD</div>
        </div>
      </figcaption>
    </figure>
    <div class="itmes-box-body">
      <h3 class="items-box-name font-2">
        パソコン
      </h3>
      <div class="items-box-num">
        <div class="items-box-price font-5">¥19,800</div>
      </div>
    </div>
  </a>
</section>
```

#### 商品名

```html
<h3 class="items-box-name font-2">
  パソコン
</h3>
```

#### 値段

```html
<div class="items-box-price font-5">¥19,800</div>
```

#### 売却済み

売却済みの商品に関しては以下のタグが追加されています。

```html
<figcaption>
  <div class="item-sold-out-badge">
    <div>SOLD</div>
  </div>
</figcaption>
```

#### 次ページ遷移ボタン

スクレイピングを行っていく中で、次のページボタンの情報も把握する必要があったので記載します。

```html
<ul class="pager">
  <li class="pager-num">{ページ番号1,2,3,4,5など}</li>
  <li class="pager-next visible-pc">
    <ul>
      <li class="pager-cell">
        <a href="/jp/search/?page=~~~~~~~~~~~~~~~~~~~~~">
          <i class="icon-arrow-right"></i>
        </a>
      </li>
      <li class="pager-cell">{最後のページへ遷移するボタン}</li>
    </ul>
  </li>
</ul>
```

#### 次ページボタン

```html
<li class="pager-next visible-pc">
  <ul>
    <li class="pager-cell">
      <a href="/jp/search/?page=~~~~~~~~~~~~~~~~~~~~~">
        <i class="icon-arrow-right"></i>
      </a>
    </li>
  </ul>
</li>
```

## 実装の前に

### やりたかったこと

1. メルカリで検索ワードに対する売却済み商品の価格を取得する(`スクレイピング`)
1. スクレイピングしたデータを視覚的にわかりやすくする(`グラフ化`)
1. 大量にある検索ワードに対して、上記 1 と 2 をぐるぐる回す(`バッチ処理化`)

### 設計概要

上記を実装するために行ったことを、以下に記述します。
ソースコードはそれを実現するようにただゴリゴリ書くだけです。

1. メルカリで検索する商品リストを csv に上げておく
1. python で 1 の csv ファイルを読み込む
1. 読み込んだ検索ワードでスクレイピングを行う
1. もし検索結果が複数ページにわたる場合は全ページでスクレイピングを行う
1. スクレイピングが完了したら結果を別の csv ファイルに出力し保存する
1. 5 で作成した csv ファイルを元にグラフを作成する
1. グラフ作成用の csv ファイルとグラフ(.jpg)ファイルを保存する
1. 以降、商品リスト csv を全てスクレイピングするまで 1~7 をループする

## いざ実装

### ディレクトリ構成

```cmd
.
├── chromedriver.exe
├── mercari_search.csv
├── scraping_batch.py
└── venv
```

### 構成の説明

本ソースは大きく 3 つに分かれています。

#### search_mercari(search_words)

スクレイピングを行う関数です。
引数は検索ワードを入れます。

#### make_graph(search_words, except_words, max_price, bins)

スクレイピングした情報を元に、グラフを描画する関数です。
引数にはそれぞれ検索ワード、除外するワード、検索商品の最大値、グラフ幅を入れます。

#### read_csv()

事前に準備した検索用リストの csv ファイルを読み込みます。

### 実装

```scraping_batch.py
import pandas as pd
from selenium import webdriver
import matplotlib.pyplot as plt
import time
import csv
import os


def search_mercari(search_words):

    # 検索ワードをそのままディレクトリ名とするため、一時避難する
    org_search_words = search_words

    # 検索ワードが複数の場合、「+」で連結するよう整形する
    words = search_words.split("_")
    search_words = words[0]
    for i in range(1, len(words)):
        search_words = search_words + "+" + words[i]

    # メルカリで検索するためのURL
    url = "https://www.mercari.com/jp/search/?keyword=" + search_words

    # ブラウザを開く
    # 本pythonファイルと同じディレクトリにchromeriver.exeがある場合、
    # 引数空でも良い
    browser = webdriver.Chrome()

    # 起動時に時間がかかるため、5秒スリープ
    time.sleep(5)

    # 表示ページ
    page = 1
    # リストを作成
    columns = ["Name", "Price", "Sold", "Url"]
    # 配列名を指定する
    df = pd.DataFrame(columns=columns)

    # 実行
    try:
        while(True):
            # ブラウザで検索
            browser.get(url)
            # 商品ごとのHTMLを全取得
            posts = browser.find_elements_by_css_selector(".items-box")
            # 何ページ目を取得しているか表示
            print(str(page) + "ページ取得中")

            # 商品ごとに名前と値段、購入済みかどうか、URLを取得
            for post in posts:
                # 商品名
                title = post.find_element_by_css_selector(
                    "h3.items-box-name").text

                # 値段を取得
                price = post.find_element_by_css_selector(
                    ".items-box-price").text
                # 余計なものが取得されてしまうので削除
                price = price.replace("¥", "")
                price = price.replace(",", "")

                # 購入済みであれば1、未購入であれば0になるように設定
                sold = 0
                if (len(post.find_elements_by_css_selector(".item-sold-out-badge")) > 0):
                    sold = 1

                # 商品のURLを取得
                Url = post.find_element_by_css_selector(
                    "a").get_attribute("href")

                # スクレイピングした情報をリストに追加
                se = pd.Series([title, price, sold, Url], columns)
                df = df.append(se, columns)

            # ページ数をインクリメント
            page += 1
            # 次のページに進むためのURLを取得
            url = browser.find_element_by_css_selector(
                "li.pager-next .pager-cell a").get_attribute("href")
            print("Moving to next page ...")
    except:
        print("Next page is nothing.")

    # 最後に得たデータをCSVにして保存
    filename = "mercari_scraping_" + org_search_words + ".csv"
    df.to_csv(org_search_words + "/" + filename, encoding="utf-8-sig")
    print("Finish!")


def make_graph(search_words, except_words, max_price, bins):
    # CSV ファイルを開く
    df = pd.read_csv(search_words + "/" +
                     "mercari_scraping_" + search_words + ".csv")

    # "Name"に"except_words"が入っているものを除く
    if(len(except_words) != 0):
        exc_words = except_words.split("_")
        for i in range(len(exc_words)):
            df = df[df["Name"].str.contains(exc_words[i]) == False]
    else:
        pass

    # 購入済み(sold=1)の商品だけを表示
    dfSold = df[df["Sold"] == 1]

    # 価格(Price)が1500円以下の商品のみを表示
    dfSold = dfSold[dfSold["Price"] < max_price]

    # カラム名を指定「値段」「その値段での個数」「パーセント」の3つ
    columns = ["Price",  "Num", "Percent"]

    # 配列名を指定する
    all_num = len(dfSold)
    num = 0
    dfPercent = pd.DataFrame(columns=columns)

    for i in range(int(max_price/bins)):

        MIN = i * bins - 1
        MAX = (i + 1) * bins

        # MINとMAXの値の間にあるものだけをリストにして、len()を用いて個数を取得
        df0 = dfSold[dfSold["Price"] > MIN]
        df0 = df0[df0["Price"] < MAX]
        sold = len(df0)

        # 累積にしたいので、numに今回の個数を足していく
        num += sold

        # ここでパーセントを計算する
        percent = num / all_num * 100

        # 値段はMINとMAXの中央値とした
        price = (MIN + MAX + 1) / 2
        se = pd.Series([price, num, percent], columns)
        dfPercent = dfPercent.append(se, columns)

    # CSVに保存
    filename = "mercari_histgram_" + search_words + ".csv"
    dfPercent.to_csv(search_words + "/" + filename, encoding="utf-8-sig")

    # グラフの描画
    """
    :param kind: グラフの種類を指定
    :param y: y 軸の値を指定
    :param bins: グラフ幅を指定 
    :param alpha: グラフの透明度(0:透明 ~ 1:濃い)
    :param figsize: グラフの大きさを指定
    :param color: グラフの色
    :param secondary_y: 2 軸使用の指定(Trueの場合)
    """
    ax1 = dfSold.plot(kind="hist", y="Price", bins=25,
                      secondary_y=True, alpha=0.9)
    dfPercent.plot(kind="area", x="Price", y=[
        "Percent"], alpha=0.5, ax=ax1, figsize=(20, 10), color="k")
    plt.savefig(search_words + "/" + "mercari_histgram_" +
                search_words + ".jpg")


def read_csv():
    # メルカリ検索用リストのcsvファイルを読み込む
    with open("mercari_search.csv", encoding="utf-8") as f:

        # 検索ワード格納用の空リストを準備
        csv_lists = []
        # csvファイルの何行目を読み込むかを確認するためのカウンター
        counter = 0

        # csvファイルを1行ずつ読み込む
        reader = csv.reader(f)
        for row in reader:
            counter += 1
            csv_lists.append(row)
            try:
                # 検索ワードチェック
                # 空の場合、エラーメッセージを表示して終了する
                if(len(row[0]) == 0):
                    print("File Error: 検索ワードがありません-> " +
                          "mercari_search.csv " + str(counter) + "行目")
                    break
            except IndexError:
                # 行が空いている場合、エラーメッセージを表示して終了する
                print("File Error: CSVファイルに問題があります。行間を詰めるなどしてください。")
                break
            try:
                if(len(row[2]) == 0):
                    # グラフ描画時の最高値チェック
                    # 空の場合、エラーメッセージを表示して終了する
                    print("File Error: 金額が設定されていません-> " +
                          "mercari_search.csv " + str(counter) + "行目")
                    break
                else:
                    try:
                        int(row[2])
                    except ValueError:
                        # 値が数字出ない場合、エラーメッセージを表示して終了する
                        print("File Error: 金額には数字を入力してください-> " +
                              "mercari_search.csv " + str(counter) + "行目")
                        break
            except IndexError:
                # そもそも金額自体が書かれていない場合、エラーメッセージを表示して終了する。
                print("File Error: 金額が設定されていません-> " +
                      "mercari_search.csv " + str(counter) + "行目")
                break
            try:
                if(len(row[3]) == 0):
                    # グラフ描画時の最高値チェック
                    # 空の場合、エラーメッセージを表示して終了する
                    print("File Error: グラフ幅が設定されていません-> " +
                          "mercari_search.csv " + str(counter) + "行目")
                    break
                else:
                    try:
                        int(row[3])
                    except ValueError:
                        # 値が数字出ない場合、エラーメッセージを表示して終了する
                        print("File Error: グラフ幅には数字を入力してください->" +
                              "mercari_search.csv " + str(counter) + "行目")
                        break
            except IndexError:
                # そもそも金額自体が書かれていない場合、エラーメッセージを表示して終了する。
                print("File Error: グラフ幅が設定されていません-> " +
                      "mercari_search.csv " + str(counter) + "行目")
                break
        return csv_lists

# ------------------------------------------------------ #


# 0. メルカリ検索CSVファイルから読み取ったリストを格納する箱を用意
"""
検索用CSVファイルからリストを読み込む
:param csv_lists[i][0]: 検索ワード
:param csv_lists[i][1]: 検索結果から除外するワード
:param csv_lists[i][2]: グラフ表示する際の最高金額
:param csv_lists[i][3]: グラフ幅(bin)
"""
csv_lists = read_csv()

# バッチ処理
for i in range(len(csv_lists)):
    # 1. ディレクトリ作成
    os.mkdir(csv_lists[i][0])
    # 2. スクレイピング処理
    search_mercari(csv_lists[i][0])
    # 3. グラフ描画
    make_graph(csv_lists[i][0], csv_lists[i][1],
               int(csv_lists[i][2]), int(csv_lists[i][3]))

```

## 使い方

### 1. 検索ワードリストの準備

mercari_search.csv に検索したいワード、除外したいワード、最高金額、グラフ幅を入力します。

- 検索ワード(必須)：メルカリで検索したいワードを入力してください
  - 検索ワードが複数ある場合は`半角アンダーバー(_)`で接続してください
      - 例：ポケモン\_ゲーム
  - 検索ワードとの間にスペースは入れないよう注意してください(動作保証外です)。
- 除外ワード(任意)：グラフ描画時、そのワードがある商品を除外したい場合に入力してください
  - 除外ワードを入力しない場合は何も入れなくて結構です。
- 最高金額　(必須)：グラフ描画時の横軸となる最高金額を入力してください
  - 半角数字(整数)で入力してください。
- グラフ幅　(必須)：グラフ描画時のグラフ幅を入力してください
  - 半角数字(整数)で入力してください。

それぞれのワードを区切る際には csv ファイルですので、カンマ(,)で区切ります。

例：

```csv
時計,デジタル,10000,100
財布,牛,3000,100
ポケモン_ゲーム,カード_CD,3000,100
パソコン,,15000,500
```

### 2. スクレイピングの実行

本ソース(`scraping_batch.py`)ファイルと同じディレクトリに`chromedriver.exe`、`mercari_search.py`、があることを確認して、以下のコマンドを実行します。

```cmd
python scraping_batch.py
```

実行中は以下のようにスクレイピングしているページ数を表示させています。

```cmd
1ページ取得中
Moving to next page ...
2ページ取得中
Moving to next page ...
3ページ取得中
・・・
22ページ取得中
Moving to next page ...
23ページ取得中
Next page is nothing.
Finish!
```

### 3. 結果の確認

上記 2 で python を実行すると、検索ワードに応じたディレクトリが作成されます。
そのディレクトリ内に、メルカリでスクレイピングした結果としてグラフが作成されますので、結果を確認してください。

結果に納得いかない場合は、csv ファイルの構成を見直して再度スクレイピングを実行しましょう！

## 注意

python ファイルと同じディレクトリに、検索ワードと同じディレクトリが作成されます(python ファイル存在ディレクトリに大量にファイルが作成されるのを防ぐためです)。
すでに検索ワードと同じディレクトリが存在する場合や、検索用 csv(`mercari_search.csv`)に同じ検索ワードがある場合、ディレクトリを作成する処理(`os.mkdir()`)が正常に働かなくなり、スクレイピングが途中で止まってしまいます。
そのため、スクレイピングを開始する際は検索ワードと同じディレクトリが存在しないこと、csv ファイルに同じ検索ワードを入力しないように注意してください。

## 作成してみて

実際に、現在出品中の商品「リンガメタリカ」(知る人ぞ知る英単語帳)で、スクレイピングしてみました。
検索時に使用したワードとパラメータは以下です。
(リンガメタリカの単語帳本体を売りたいため、検索ワードに「CD」という文字が入っている物を除外するようにしています)
`リンガメタリカ,CD,1500,50`
上記のソースを回した結果、以下のようなグラフが作成されます。
![mercari_histgram_リンガメタリカ-cd.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407150/78919978-ae41-6ae6-1eaa-5dd52cc5b7eb.jpeg)
これを見てみると、

- 600 円程度でよく売れている
- 売れたものの約 80%が 800 円以下

という結果になっていました。
このグラフからは、もし「リンガメタリカ」を売りたければ 600 円程度が適正な価格であることがわかります。

### ちなみに

この記事を書いている時に「リンガメタリカ」を「目立った傷や汚れなし」で出品してみました。
その時にメルカリ側から提示された相場価格は「640 円」(売れやすい価格は 460 ～ 790 円)でした。

もしかしたらスクレイピングをして自分で確かめなくても、妥当な金額をメルカリ側は提示していたのかもしれませんね。。。

## 今後

今考えていることは以下の 5 つです。
今後ゆっくり時間があるときにでも行いたいなと思ってます。

- ほかのフリマサイトにも商品を出品しているので、上記と同様なスクレイピングを使用して、商品の売上情報を獲得してみたい。
- 商品の価値は発売してからの月日と、季節ごとによって変わると思うので、今度は時系列ごとや季節ごとの価格分類をしてみたい。
- 商品の状態によっても価格が変わるので、今度は「商品の状態」も要素としてスクレイピングしたい。
- ソースが冗長になってしまった部分もあるので、リファクタリングしたい(python に詳しい人、レビューしていただけたら幸いです！)。
- スクレイピングした情報を元に値段を設定してみて、実際に売り上げは上がったのかなどを調査したい。

今回はここまでとなります。
最後までお読みいただき、ありがとうございました。

## Github へのリンク

[kewpie134134/fleamarket_app_scraping](https://github.com/kewpie134134/fleamarket_app_scraping/tree/master)

## 参考

- [Python でメルカリから最安値をスクレイピングで取得する](https://qiita.com/kiyo-tomo/items/e2b83768dedae65179b1)
- [[Python]メルカリスクレイピング](https://note.com/itk170113/n/n3ed685ff0356)