Web上でスクレイピングし、CSVにエクスポートできるツールを作ります
- メルカリでほしい商品名を検索欄に入れる
- メルカリのデータを取得
- CSVでダウンロードできる
みたいなものです。とりあえずローカルから動かしているので、まだディプロイしていません。次に本番環境で動かしてみようとは思いますが、あまりメルカリさんに迷惑をおかけすると申し訳ないので、起動時間も一時的にしておきます。
#Flask— DAI (@never_be_a_pm) December 9, 2017
メルカリで商品情報を取得するフロントエンド部分を作成。取得したいキーワードを入れると、バックエンドでメルカリから自動でデータを取得し、CSV形式にエクスポートしてくれる仕組み。ディプロイできたら、スクレイピングの汎用ツールできる。 pic.twitter.com/MECQr238lX
環境
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
で、検索キーワードを入力、
でPOSTリクエストが実行され、app.pyに送られ、そのキーワードからスクレイピングが始まります。
<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上の環境構築の問題やらでしんどい汗