Python
スクレイピング
BeautifulSoup

Python Webスクレイピング実装例

前書き

コードをできるだけ書かず、手間のかかるフレームワークやミドルウェアを使わず、手軽さ重視で定期実行に対応した(cronで雑にまわせる)スクレイピングを実装する。
自然言語処理・機械学習の検証のため必要に迫られて実装したもの。似たような要件の方に少しでも役立てば、というところ。

概要

題材は何でもいいのですが、日刊工業新聞Webサイトからトップニュースのタイトルと本文(有料記事等の読めない部分は諦める)を抜き出してsqliteに記録します。
cronで雑に定期実行することを想定して、過去に収集済みの記事は重複して収集しないようにします。
なお、認証要ページや、javascriptで動的生成されるタイプのページには対応しません。

環境

  • Python3
    • BeautifulSoup

必須じゃないですが、Jupyter notebook環境だとDOMまわりのトライアンドエラーが捗ります。

実装

1
import requests
import re
import sqlite3
import hashlib

from time import sleep
from datetime import datetime
from bs4 import BeautifulSoup

domain = "https://www.nikkan.co.jp"
startpath = "/"
basesoup = BeautifulSoup(requests.get(domain + startpath).text, "lxml")

とりえあずhtmlを取ってきてBeautifulSoupに突っ込みます。
しっかりつくるならrequests関連は例外処理を入れておいた方がいいです。タイムアウト等でこけるので。

2
links = []
for a in basesoup.find_all("a", href=re.compile("^/articles/view/[0-9]+")):
    links.append(domain + a.get("href"))
links = list(set(links))

トップページ内のaタグを拾いにいきます。その際にhrefに正規表現で条件を掛け、記事URL以外のaタグを弾きます。

3
texts = []
for link in links:
    sleep(2)
    res = requests.get(link)
    res.encoding = res.apparent_encoding
    soup = BeautifulSoup(res.text, "lxml")
    title = "".join([x.text.strip() for x in soup.find_all("div", class_="ttl")])
    main = "".join([x.text.strip() for x in soup.find("div", class_="txt").find_all("p")])
    text = (title + " " + main).replace("\u3000"," ")
    texts.append(text)

拾ったリンク先URL各々に対してDOM解析し、今度はタイトルと本文を拾いにいきます。
このサイトの場合は、class=ttlのdiv要素にタイトル、class=txtの最初のdiv配下のp要素に本文が入っているので、それで検索を掛けます。
記事によっては複数要素が返ってくるので、joinでまとめて結合します。

4
dbname = "text.db"
dbcon = sqlite3.connect(dbname)
dbcur = dbcon.cursor()

for text in texts:
    insert = "INSERT INTO rawtext(id, source, time, rawtext) VALUES(?, ?, ?, ?)"
    id = hashlib.md5(text.encode("utf-8")).hexdigest()
    source = "nikkan"
    time = datetime.now().isoformat()    
    args = (id, source, time, text)    
    try:
        dbcur.execute(insert, args)
    except sqlite3.Error as e:
        print('sqlite3:', e.args[0])

dbcon.commit()
dbcon.close()

DBに突っ込みます。dbファイルとテーブル定義は事前に作っておいて下さい。
id列はUNIQUE制約を付けておきます。
こうすることで、タイトル+本文のハッシュ値が一致する記事はUNIQUE制約で勝手にはじいてくれるので、雑に定期実行してもデータが重複しません。あまり美しくはない気がしますが。
他のサイトからのデータも放り込むかもしれないのでsource列にデータソース名を入れておきます。あと取得日時も何かと使いそうなのでtime列に保存しておきます。


以上です。上記のスクリプトを収集したいサイト向けに適当に改修してcronで回せば概ね上手く動くんじゃないでしょうか。
今日日ヘッドレスブラウザ必須やろ、という気がしていたのですが、当方要件は堅めのニュースサイトが主だった為か、意外と必要に迫られませんでした。
きっとDBも、Read性能や分散化等を考えると、他に取るべき選択肢があるのでしょうが、中小規模レベルではsqliteの簡単さがやはり魅力です。別途分かち書き要ですがftsで全文検索できたり何気に高機能ですしね。

なお、相手サーバに不可を掛けすぎて怒られても責任は負えませんので、紳士的なクローリングを心がけましょう。