2017-12-11

【Python】スクレイピングができるウェブサイトを作ってみた (開発環境編)

Web上でスクレイピングし、CSVにエクスポートできるツールを作ります




メルカリのAPI的なもの作ってみました。

  • メルカリでほしい商品名を検索欄に入れる
  • メルカリのデータを取得
  • CSVでダウンロードできる

みたいなものです。とりあえずローカルから動かしているので、まだディプロイしていません。次に本番環境で動かしてみようとは思いますが、あまりメルカリさんに迷惑をおかけすると申し訳ないので、起動時間も一時的にしておきます。



環境


Windows10
Windows Subsystem For Linux
Python3.5

実装

PythonのWebフレームワークFlaskを使って実装します。スクレイピングは例によってSeleniumです。今回はヘッドレスブラウザを使わず、Chroniumを利用してスクレイピングを行っています。

- Flask
- Selenium
- Chronium

アルゴリズム


- 起動時にapp.pyが実行される
- ルート(/)にアクセスされると、index.htmlを表示
- ルート上で検索クエリのpostリクエストを送る
- ルート上でpostリクエストが行われると、検索クエリが受け取られ、メルカリからスクレイピングを開始
- スクレイピングで取得したCSVをウェブページ上でダウンロードできる

ソースコード

やっつけです。全然きれいにしないまま貼ってます。いらないコードとかたくさん入っています。ファイル構造も適当です。

.
├── Procfile
├── __pycache__
│   ├── data.cpython-35.pyc
│   └── merucari.cpython-35.pyc
├── app.py
├── default.csv
├── ghostdriver.log
├── npm-debug.log
├── requirements.txt
├── runtime.txt
└── templates
    ├── _navbar.html
    ├── home.html
    └── layout.html

app.py(これがルーティングを規定している)

app.py

from flask import Flask, render_template, request, logging, Response
import sys
import os
from selenium import webdriver
import pandas

app = Flask(__name__)

@app.route("/", methods=["GET"])
def home():
    return render_template("home.html")

@app.route("/", methods=["POST"])
def post():
    query = request.form["query"]
    csv = exportCSV(query)
    with open("output.csv") as fp:
            csv = fp.read()
            return Response(
                csv,
                mimetype="text/csv",
                headers={"Content-disposition":
                         "attachment; filename=output.csv"})


def exportCSV(query):
    browser = webdriver.Chrome(executable_path='/mnt/c/workspace/pydev/chromedriver.exe') #ローカル
    #browser = webdriver.Chrome() #ローカル
    #browser = webdriver.PhantomJS()#heroku クロスドメインでなんか死んだ
    df = pandas.read_csv('default.csv', index_col=0)
    query = query
    browser.get("https://www.mercari.com/jp/search/?sort_order=price_desc&keyword={}&category_root=&brand_name=&brand_id=&size_group=&price_min=&price_max=".format(query))
    page = 1
    while True: #continue until getting the last page
        if len(browser.find_elements_by_css_selector("li.pager-next .pager-cell:nth-child(1) a")) > 0:
            print("######################page: {} ########################".format(page))
            print("Starting to get posts...")
            posts = browser.find_elements_by_css_selector(".items-box")
            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('¥', '')
                sold = 0
                if len(post.find_elements_by_css_selector(".item-sold-out-badge")) > 0:
                    sold = 1
                url = post.find_element_by_css_selector("a").get_attribute("href")
                se = pandas.Series([title, price, sold,url],['title','price','sold','url'])
                df = df.append(se, ignore_index=True)
            page+=1
            btn = browser.find_element_by_css_selector("li.pager-next .pager-cell:nth-child(1) a").get_attribute("href")
            print("next url:{}".format(btn))
            browser.get(btn)
            print("Moving to next page......")
        else:
            print("no pager exist anymore")
            break
    df.to_csv("output.csv")

if __name__ == '__main__':
    app.run(debug=True) # デバックしたときに、再ロードしなくても大丈夫になる
    #port = int(os.environ.get('PORT', 5000)) #本番環境
    #app.run(host='0.0.0.0', port=port, debug=True) #本番環境

home.html

 <input type="text" class="form-control" name="query" id="query" placeholder="ルンバ">

で、検索キーワードを入力、

  <button type="submit" class="btn btn-default">Export CSV</button>

でPOSTリクエストが実行され、app.pyに送られ、そのキーワードからスクレイピングが始まります。

home.html

{% extends 'layout.html' %}

{% block body %}
<div class="jumbotron">
  <h1 class="display-3">Mercari CSV Exporter</h1>
  <p class="lead">You can download mercari data by csv</p>
  <hr class="my-4">
  <p>By filling in item names, you can download mercari data</br>
    It may take few minutes to export data.
  </p>
</div>

<h1>Please Insert name of items you want to export</h1>

<form action="/" method="post">
  <div class="form-group">
    <label for="name">item name:</label>
    <input type="text" class="form-control" name="query" id="query" placeholder="ルンバ">
  </div>
  <button type="submit" class="btn btn-default">Export CSV</button>
</form>
{% endblock %}


_navbar.html(ナビバーの部分、homeの中にぶち込まれている)

_navbar.html

<nav class="navbar navbar-expand-md navbar-dark bg-dark">
  <a class="navbar-brand" href="#">Navbar</a>
  <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
  </button>

  <div class="collapse navbar-collapse" id="navbarsExampleDefault">
    <ul class="navbar-nav mr-auto">
      <li class="nav-item active">
        <a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="/about">About</a>
      </li>
      <li class="nav-item">
        <a class="nav-link disabled" href="#">Disabled</a>
      </li>
      <li class="nav-item dropdown">
        <a class="nav-link dropdown-toggle" href="http://example.com" id="dropdown01" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Dropdown</a>
        <div class="dropdown-menu" aria-labelledby="dropdown01">
          <a class="dropdown-item" href="#">Action</a>
          <a class="dropdown-item" href="#">Another action</a>
          <a class="dropdown-item" href="#">Something else here</a>
        </div>
      </li>
    </ul>
    <form class="form-inline my-2 my-lg-0">
      <input class="form-control mr-sm-2" type="text" placeholder="Search" aria-label="Search">
      <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
    </form>
  </div>
</nav>

layout.html

layout.html

<html>
<head>
    <meta charset ="utf-8">
    <title>MyFlaskApp</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
</head>
<body>
    {% include '_navbar.html'%}
    <div class="container">
        {% block body %}{% endblock %}
    </div>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js" integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js" integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ" crossorigin="anonymous"></script>
</body>
</html>


次は本番環境にディプロイしてみますが、かなり苦戦しているのでもう少し時間がかかりそうです。クロスドメインの問題やら、Heroku上の環境構築の問題やらでしんどい汗

注目の投稿

 PythonのTweepyを利用して、Twitter APIを利用している。 その中で、ハマったポイントをメモしておく。 まず、Searchに関して。 Twitter検索は、クライアントアプリ側では、全期間の検索が可能になっている。 一方で、APIを利用する際は、過去1週間しか...