はじめに
Webスクレイピングというもをやってみたく、折角ならwebアプリにしようといことで作ってみました。
自分のPC(Windows10)内部に環境を構築し、Pythonを起動させています。
実際に完成した(いや、まだ未完成)動画がこちらです。
機能紹介
- 検索キーワードでスクレイピングする
- 検索した日付のデータのみ表示する
- 検索キーワードでフィルタリングする
- 画面上のデータをダウンロードする
- 検索キーワードでデータを削除する
上記の機能を実装しました。
Python3のダウンロード
まずは環境構築です。今、自分の環境にPythonが入っているか確認してみましょう。
こちらの記事にあるようにコマンドプロンプトなどでバージョン情報が表示されれば、すでにPCにインストールされています。しかし、本記事ではバージョン3系を利用していますので、バージョンが古い場合は新しバージョンをダウンロードしましょう。
Pythonのダウンロードは公式ページから行います。
ダウンロード方法の詳細はこちら。
MySQLのインストール
bottleフレームワークダウンロード
Pythonのプレームワークであるbottleを準備します。
この記事にもありますがbottleはpipでンストールするよりもコピペでファイル作ったほうが初心者的には、余計なところで躓かずに済むかもしれません。
さて、pythonのフレームワークといってもさまざまなものがありますが、今回はWeb上の情報を取ってきて、DBに登録、ブラウザに表示するということがやりたかったので、一番無駄がなさそうなbottleを採用しました。
このフレームを利用することで簡単にWEBアプリケーション開発を行えるようになります。
フロントを作る
フロントではざっくり以下の技術を使いました。
SPAとはSingle Page Applicationのことで、 単一のWebページでアプリケーションを構成する設計構造の名称 のことです。
ページ遷移がなく動作の向上が期待できます。近年ではVue.jsなどのフレームワークを使って実装されることが多く、より高度なWEB表現が可能になるらしいです。
今回はSPAで実装をしたかったのでjQueryでAjaxを使って実装しました。
Ajaxとは 「Asynchronous JavaScript + XML 」の略称でサーバーとの非同期通信を可能にします。
ソースコード
top.html
<!DOCTYPE html>
<html lang="ja">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="icon" href="./static/img/ico/favicon.ico">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<title>Web Scraping with Python</title>
</head>
<body>
<!-- nav配置 -->
<nav class="navbar navbar-expand-md navbar-light shadow sticky-top" style="background-color: rgb(1, 1, 1);">
<div class="navbar-brand"><img src="./static/img/ico/images.png" width="70" height="60" alt="ロゴ"></div>
<div class="d-inline-block mr-5" style="font-style: italic;color: aliceblue;font-size:xx-large ;font-weight: 700;">
Web Scraping with Python
</div>
<div class= "m-2 mt-3">
<div class="row">
<div class="float-right col-md">
<div class="d-inline-block">
<div class="m-2 ml-5">
<button type="button" class="btn btn-success btn-lg shadow" data-toggle="modal" data-target="#exampleModal" style="font-weight: 600;">SCRAPING START</button>
</div>
</div>
<div class="d-inline-block">
<div class="m-2">
<div class="other" id="all">
<a href="#" class="btn btn-primary btn-lg shadow" role="button" style="font-weight: 600;">TODAY'S DATA</a>
</div>
</div>
</div>
<div class="d-inline-block">
<div class="m-2">
<div class="dropdown">
<select id="ddmenu" class="btn btn-warning btn-lg shadow" style="font-weight: 600;">
KEY WORDS
</select>
</div>
</div>
</div>
<div class="d-inline-block">
<div class="m-2">
<div class="csv">
<a href="#" class="btn btn-secondary btn-lg shadow" role="button" onclick="output();return false;">CSV DOWNLOAD</a>
</div>
</div>
</div>
<div class="d-inline-block">
<div class="m-2">
<button type="button" id="del" class="btn btn-danger btn-lg shadow" style="font-weight: 600;">DELETE</button> </div>
</div>
</div>
</div>
</div>
</div>
</nav>
<!-- データ配置 -->
<div class="container" >
<!-- データ配置 -->
<div class="display-area">
<div class="m-2 mt-3">
<table class="table">
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody id="table">
</tbody>
</table>
<div id="iimg"></div>
</div>
<!-- モーダル部分始まり -->
<div class="modal fade" id="delete" tabindex="-1" role="dialog" aria-labelledby="delete" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="delete">Notice</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">delete</div>
<div class="modal-footer">
<button type="button" id="godel" class="btn btn-secondary" data-dismiss="modal">OK</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="recipient-name" class="col-form-label"><b>Scraping key word</b></label>
<input type="text" class="form-control" id="key">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">CANCEL</button>
<button type="button" id="start" class="btn btn-primary" data-dismiss="modal">START</button>
</div>
</div>
</div>
</div>
</div>
<!-- モーダル部分終わり -->
</div>
</div>
<footer class="fixed-bottom" style="background-color: rgb(1, 1, 1);">
<div style="font-style: italic;color: aliceblue;font-size:larger;">© 2020 KEI inc. v2.0</div>
</footer>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.3.1.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
<!--独自js-->
<script src="/static/js/spa.js"></script>
</body>
</html>
spa.js
//グローバル変数
var items = [];
var tUrl = 'http://localhost:8085/';
var all = 'all';
var key = 'key';
var sflag = 0;
var savedata;
function today() {
var d = new Date();
var formatted = `${d.getFullYear()}-${(d.getMonth()+1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`;
return formatted;
}
function scraping(sendkey) {
$(function(){
var targetUrl = tUrl+'scraping';
console.log(sendkey);
var request = {
'sendkey': sendkey
};
$.ajax({
url: targetUrl,
type: 'POST',
contentType: 'application/JSON',
//dataType: 'JSON',
data : JSON.stringify(request),
scriptCharset: 'utf-8',
}).done(function(data){
console.log(data);
$('#table').empty();
$('#iimg').empty();
$('#table').append('<tr><td><div style="font-style: italic;color: #000000;font-size:xx-large ;font-weight: 700;">INFO</div></td><td><div style="font-style: italic;color: #FF3300;font-size:xx-large ;font-weight: 700;">FINISH</div></td></tr>');
var pastDate = null;
other(all,pastDate);
sflag = 0;
getPastDay();
}).fail(function(data, XMLHttpRequest, textStatus) {
console.log(data);
$('#table').empty();
$('#iimg').empty();
$('#table').append('<tr><td><div style="font-style: italic;color: #000000;font-size:xx-large ;font-weight: 700;">INFO</div></td><td><div style="font-style: italic;color: #FF3300;font-size:xx-large ;font-weight: 700;">FAILURE</div></td></tr>');
sflag = 0;
console.log("XMLHttpRequest : " + XMLHttpRequest.status);
console.log("textStatus : " + textStatus);
});
});
}
function other(other,pastDate) {
var targetUrl = tUrl+'other';
var date = null;
if (pastDate == null || pastDate == '') {
date = today();
} else {
date = pastDate;
}
if (date != null) {
if (other == key) {
var request = {
'date' : date,
'other': key
};
} else if(other == all){
var request = {
'date' : date,
'other': all
};
}
} else {
alert('不正な値');
var request = {
'date' : today(),
'other': all
};
}
$(function() {
$.ajax({
url: targetUrl,
type: 'POST',
contentType: 'application/JSON',
dataType: 'JSON',
data : JSON.stringify(request),
scriptCharset: 'utf-8',
}).done(function(data) {
if (data == null || data == '' || data[0] == '') {
$('#table').empty();
$('#iimg').empty();
$('#table').append('<tr><td><div style="font-style: italic;color: #000000;font-size:xx-large ;font-weight: 700;">INFO</div></td><td><div style="font-style: italic;color: #000000;font-size:xx-large ;font-weight: 700;">NO DATA</div></td></tr>');
} else {
show(data);
savedata = data;
}
}).fail(function(data, XMLHttpRequest, textStatus) {
console.log(data);
console.log("XMLHttpRequest : " + XMLHttpRequest.status);
console.log("textStatus : " + textStatus);
});
});
}
function getPastDay() {
$(function(){
var targetUrl = tUrl+'getPastDay';
$.ajax({
url: targetUrl,
type: 'POST',
contentType: 'application/JSON',
dataType: 'JSON',
data : null,
scriptCharset: 'utf-8',
}).done(function(data) {
if (data == null || data == '' || data[0] == '') {
$('#table').empty();
$('#iimg').empty();
$('#table').append('<tr><td><div style="font-style: italic;color: #000000;font-size:xx-large ;font-weight: 700;">INFO</div></td><td><div style="font-style: italic;color: #000000;font-size:xx-large ;font-weight: 700;">NO DATA</div></td></tr>');
} else {
$('#ddmenu').empty();
$("#ddmenu").append('<option value="">KEY WORDS</option>');
for (var i = 0; i < data.length; i++) {
$("#ddmenu").append('<option value="'+data[i].dt+'"style="font-weight: 600;" >'+data[i].dt+'</option>');
}
}
}).fail(function(data, XMLHttpRequest, textStatus) {
console.log(data);
console.log("XMLHttpRequest : " + XMLHttpRequest.status);
console.log("textStatus : " + textStatus);
});
});
}
function delwords() {
$(function(){
var targetUrl = tUrl+'getPastDay';
$.ajax({
url: targetUrl,
type: 'POST',
contentType: 'application/JSON',
dataType: 'JSON',
data : null,
scriptCharset: 'utf-8',
}).done(function(data) {
if (data == null || data == '' || data[0] == '') {
$('#table').empty();
$('#iimg').empty();
$('#table').append('<tr><td><div style="font-style: italic;color: #000000;font-size:xx-large ;font-weight: 700;">INFO</div></td><td><div style="font-style: italic;color: #000000;font-size:xx-large ;font-weight: 700;">NO DATA</div></td></tr>');
} else {
$('#table').empty();
for (var i = 0; i < data.length; i++) {
//console.log(data);
$('#table').append('<tr><td>'+data[i].dt+'</td><td><button type="button" id="'+data[i].dt+'" class="godel btn-danger" class="btn btn-primary">DELETE</button></td></tr>');
}
}
}).fail(function(data, XMLHttpRequest, textStatus) {
console.log(data);
console.log("XMLHttpRequest : " + XMLHttpRequest.status);
console.log("textStatus : " + textStatus);
});
});
}
function godelwords(sendkey) {
$(function(){
var targetUrl = tUrl+'del';
console.log(sendkey);
var request = {
'sendkey': sendkey
};
$.ajax({
url: targetUrl,
type: 'POST',
contentType: 'application/JSON',
//dataType: 'JSON',
data : JSON.stringify(request),
scriptCharset: 'utf-8',
}).done(function(data){
console.log(data);
$('#table').empty();
$('#iimg').empty();
$('#table').append('<tr><td><div style="font-style: italic;color: #000000;font-size:xx-large ;font-weight: 700;">INFO</div></td><td><div style="font-style: italic;color: #FF3300;font-size:xx-large ;font-weight: 700;">FINISH</div></td></tr>');
var pastDate = null;
other(all,pastDate);
sflag = 0;
getPastDay();
}).fail(function(data, XMLHttpRequest, textStatus) {
console.log(data);
$('#table').empty();
$('#iimg').empty();
$('#table').append('<tr><td><div style="font-style: italic;color: #000000;font-size:xx-large ;font-weight: 700;">INFO</div></td><td><div style="font-style: italic;color: #FF3300;font-size:xx-large ;font-weight: 700;">FAILURE</div></td></tr>');
sflag = 0;
console.log("XMLHttpRequest : " + XMLHttpRequest.status);
console.log("textStatus : " + textStatus);
});
});
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CSV download
//jsonをcsv文字列に編集する
function jsonToCsv(json, delimiter) {
var header = Object.keys(json[0]).join(delimiter) + "\n";
var body = json.map(function(d){
return Object.keys(d).map(function(key) {
return d[key];
}).join(delimiter);
}).join("\n");
return header + body;
}
//csv変換
function exportCSV(items, delimiter, filename) {
//文字列に変換する
var csv = jsonToCsv(items, delimiter);
//拡張子
var extention = delimiter==","?"csv":"tsv";
//出力ファイル名
var exportedFilenmae = (filename || 'export') + '.' + extention;
//文字化け対策
var bom = new Uint8Array([0xEF, 0xBB, 0xBF]);
//BLOBに変換
var blob = new Blob([bom,csv], { type: 'text/csv;charset=utf-8;' });
if (navigator.msSaveBlob) { // for IE 10+
navigator.msSaveBlob(blob, exportedFilenmae);
} else {
//anchorを生成してclickイベントを呼び出す。
var link = document.createElement("a");
if (link.download !== undefined) {
var url = URL.createObjectURL(blob);
link.setAttribute("href", url);
link.setAttribute("download", exportedFilenmae);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}
}
function output(){
console.log(savedata);
if (savedata == null || savedata == 0) {
alert("No data");
} else {
var filename = savedata[0].dt;;
exportCSV(savedata,',', filename);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
window.onload = function() {
//today
var pastDate = null;
other(all,pastDate);
getPastDay();
}
//scraping
$(function(){
$('#start').on('click',function(){
if (sflag == 0) {
sflag = 1;
sendkey= $("#key").val();
scraping(sendkey);
$('#table').empty();
$('#iimg').empty();
$('#table').append('<tr><td><div style="font-style: italic;color: #000000;font-size:xx-large ;font-weight: 700;">INFO</td><td><div style="font-style: italic;color: #0000FF;font-size:xx-large ;font-weight: 700;">RUNNING <img src="./static/img/ico/load.gif" width="30" height="30" /></div></td></tr>');
} else {
$('#table').empty();
$('#iimg').empty();
$('#table').append('<tr><td><div style="font-style: italic;color: #000000;font-size:xx-large ;font-weight: 700;">INFO</td><td><div style="font-style: italic;color: #0000FF;font-size:xx-large ;font-weight: 700;">RUNNING NOW ( PLEASE WAIT A MINUTE ) <img src="./static/img/ico/load.gif" width="30" height="30" /></div></td></tr>');
}
});
});
//TODAY
$(function() {
$('.other').on('click',function() {
var id = $(this).attr('id');
var pastDate = null;
if (id == all) {
other(all,pastDate);
}
});
});
//プルダウン選択時
$(function() {
$('#ddmenu').on('click',function() {
var pastDate = $("#ddmenu").val();
other(key,pastDate);
});
});
$(function() {
$('#del').on('click',function() {
delwords();
});
});
$(function() {
$(document).on('click','.godel',function() {
var sendkey = $(this).attr("id");
window.confirm("データは完全に削除されます。本当によろしいですか?");
console.log('sendkey');
console.log(sendkey);
godelwords(sendkey);
});
});
function show(data) {
$(function() {
$('#table').empty();
$('#iimg').empty();
for (var i = 0; i < data.length; i++) {
var dirDay = data[i].dt;
var dirNaeme = dirDay.replace( /-/g , "" );
dirNaeme = dirNaeme.substr(0,8);
var id = i+1;
$('#table').append('<tr><td>'+data[i].img_id+'</td><td><a href='+data[i].url+' target="_blank" style="font-size:large;">'+data[i].title+'</a></td><td> ('+data[i].dt+')</td></tr>');
}
});
return data;
}
バックエンドを作る
Pythonを使ってコーディングしていきます。
スクレイピングするためにseleniumを導入しましょう。
こちらの記事を参考にseleniumの導入を行いました。
ソースコード
main.py
# coding:utf-8
from bottle import request, route, get, post, hook, response, static_file, template, redirect, run
from selenium import webdriver
import mysql.connector
import datetime
import os.path
import random
import string
import random
import time
import json
import os
#status
PASTDAY = 'pastday'
ALL = 'all'
KEY = 'key'
DEL = 'del'
#ファイルパス
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
STATIC_DIR = os.path.join(BASE_DIR, 'static')
#JS
@route('/static/js/<filename:path>')
def send_static_js(filename):
return static_file(filename, root=f'{STATIC_DIR}/js')
#img
@route('/static/img/<filename:path>')
def send_static_img(filename):
return static_file(filename, root=f'{STATIC_DIR}/img')
#rootの場合
@route("/")
def index():
return template('top')
@post('/other')
def postOther():
#値取得
data = request.json
date = data['date']
qerytype = data['other']
url = dbconn(qerytype, date)
#ID NULLチェック
if isUrlCheck(url):
print('checkedUrl:')
#json作成
jsonUrl = makeJson(url)
print(type(jsonUrl))
return jsonUrl
else:
return postOther()
@post('/getPastDay')
def pastDay():
date = None
qerytype = PASTDAY
url = dbconn(qerytype, date)
#ID NULLチェック
if isUrlCheck(url):
print('checkedUrl:')
#json作成
jsonUrl = makeJson(url)
print(type(jsonUrl))
return jsonUrl
else:
return postOther()
@post('/scraping')
def startscraping():
#値取得
data = request.json
sendkey = data['sendkey']
scraping(sendkey)
@post('/del')
def delete():
#値取得
data = request.json
sendkey = data['sendkey']
print(sendkey)
qerytype = DEL
dbconn(qerytype, sendkey)
i = 0
def isUrlCheck(url):
if url == None:
print('チェックNG')
global i
i += 1
print('i')
print(i)
if i < 5:
return None
else:
return True
else:
print('チェックOK')
return True
def makeJson(url):
jsonUrl = jsonDumps(url)
return jsonUrl
def jsonDumps(url):
url = json.dumps(url)
return isTypeCheck(url)
def isTypeCheck(jsonUrl):
if type(jsonUrl) is str:
return jsonUrl
else:
jsonDumps(jsonUrl)
def dbconn(qerytype, date):
print("q")
print(qerytype)
print(date)
f = open('./conf/prop.json', 'r')
info = json.load(f)
f.close()
#DB設定
conn = mysql.connector.connect(
host = info['host'],
port = info['port'],
user = info['user'],
password = info['password'],
database = info['database'],
)
cur = conn.cursor(dictionary=True)
try:
#接続クエリ
if qerytype == ALL:
sql = "SELECT site_id,title,url,img_id,CAST(dt AS CHAR) as dt FROM scrapingInfo2 WHERE dt LIKE '"+date+'%'"' ORDER BY dt DESC"
elif qerytype == KEY:
sql = "SELECT site_id,title,url,img_id,CAST(dt AS CHAR) as dt FROM scrapingInfo2 WHERE img_id LIKE '"+date+"'ORDER BY dt DESC"
elif qerytype == PASTDAY:
sql = "SELECT DISTINCT img_id as dt FROM scrapingInfo2 ORDER BY dt DESC"
elif qerytype == DEL:
sql = "DELETE FROM scrapingInfo2 where img_id = '"+date+"'"
print(sql)
#クエリ発行
if qerytype == DEL:
cur.execute(sql)
conn.commit()
else:
cur.execute(sql)
cur.statement
url = cur.fetchall()
if url is not None:
return url
else:
return None
except:
print("DBエラーが発生しました")
return None
finally:
cur.close()
conn.close()
def scraping(sendkey):
print("scraping start")
print(sendkey)
driver = webdriver.Chrome(BASE_DIR+'./static/chromedriver.exe')
driver.get('https://www.google.com/')
search = driver.find_element_by_name('q')
search.send_keys(sendkey)
search.submit()
time.sleep(3)
i = 1
i_max = 5
try:
while i <= i_max:
class_group = driver.find_elements_by_class_name('r')
for elem in class_group:
title = elem.find_element_by_class_name('LC20lb').text
url = elem.find_element_by_tag_name('a').get_attribute('href')
#DB設定
f = open('./conf/prop.json', 'r')
info = json.load(f)
f.close()
conn = mysql.connector.connect(
host = info['host'],
port = info['port'],
user = info['user'],
password = info['password'],
database = info['database']
)
now = datetime.datetime.now()
dt = "{0:%Y-%m-%d %H:%M:%S}".format(now)
c = conn.cursor()
#データ登録
sql = "INSERT INTO scraping.scrapingInfo2(site_id,title,url,img_id,dt) VALUES (2,%s,%s,%s,%s)"
print(sql)
c.execute(sql, (title, url, sendkey, dt))
sql = 'SET @i := 0'
c.execute(sql)
sql = 'UPDATE `scraping`.`scrapingInfo2` SET id = (@i := @i +1);'
c.execute(sql)
conn.commit()
conn.close()
if driver.find_elements_by_id('pnnext') == []:
i = i_max + 1
else:
next_page = driver.find_element_by_id('pnnext').get_attribute('href')
driver.get(next_page)
i = i + 1
time.sleep(3)
except:
driver.quit()
print("DBエラーが発生しました")
finally:
# ブラウザを閉じる
driver.quit()
if __name__ == "__main__":
run(host='localhost', port=8085, reloader=True, debug=True)
長いですね。もっとシンプルに書けそうですが、google先生に頼りながら、なんとかやりたいことはできました。
時間があるときにコードをきれいにします。
main.pyを実行するとブラウザからlocalhostでアクセス可能になります。
コードに何が書いてあるかは察してください。
課題
- chromeのドライバーがアップデートされると動作しなくなる(自動的にChromeのバージョンを検出し、バージョンにあったドライバーをダウンロードする機能が必要)
- find_element_by_class_nameでHtmlタグ中の’LC20lb’をキーワードにタイトルを抽出していますが、うまく動作しない場合があります。(おそらく、一番初めのリンクが広告で始まる場合→まだデバックしていない)
- Delete機能にバグあり
追加したい機能
- 検索されたURL毎にお気に入り登録
- お気に入りしたURLのみをフィルターしcsv出力
コメント