iMind Developers Blog

iMind開発者ブログ

PythonでWebスクレイピング

概要

仕事柄Webスクレイピングのコードを書くことがよくあるので、普段使っているコードをまとめておく。

バージョン情報

  • beautifulsoup4==4.7.1
  • requests==2.21.0
  • chardet==3.0.4
  • reppy==0.4.12

導入

$ pip install beautifulsoup4 requests chardet reppy html5lib lxml

reppyによるロボットの確認

スクレイピングで実行するスクリプトも内容によってはロボットになる為、行儀よくチェック処理を入れておくと良いかもしれない。

import requests
from reppy.cache import RobotsCache

robots_cache = RobotsCache()

class RobotsException(requests.RequestException):
    ''' robots.txt not allowed. '''

def check_robots(url, user_agent=requests.utils.default_user_agent()):
    if not robots_cache.allowed(url, user_agent):
        raise RobotsException(url)

リクエスト

タイムアウト時間を設定したり、ユーザーエージェントを設定したり、テキスト以外は収集対象外にしたり、取得するデータサイズを制限したりしつつhttp requestで指定URLのデータを取得する。

import requests

# タイムアウト時間の設定
TIMEOUT_SEC = 10

# user agentの設定
DEFAULT_USER_AGENT = requests.utils.default_user_agent()

# サイズが大きい場合は途中で打ち切る処理を入れる
MAX_CONTENT_LENGTH = 512 * 1024 # 512KB

def get(url,
    user_agent=DEFAULT_USER_AGENT, \
     max_content_len=MAX_CONTENT_LENGTH, \
    timeout_sec=TIMEOUT_SEC):
    '''
    urlにリクエストしてbyte配列を返す
    '''
    # ロボット可チェック
    check_robots(url)

    # ヘッダの設定
    headers = {'User-Agent': user_agent}

    with requests.Session() as sess:
        # リクエスト
        resp = sess.get(url, stream=True, headers=headers, timeout=TIMEOUT_SEC, allow_redirects=True)

        # 画像とかpdfは取らないようにcontent-typeがtextのもののみ選ぶ処理
        if not 'content-type' in resp.headers or not 'text' in resp.headers['content-type']:
            raise requests.exceptions.InvalidHeader('no text header', url, resp.headers)

        # max_content_lenで指定した値を超えるまで読込み
        chunks = []
        byte_len = 0
        for chunk in resp.iter_lines():
            byte_len += len(chunk)
            chunks.append(chunk)
            if byte_len > max_content_len:
                break
        content = b"".join(chunks)

    return resp, content

BeautifulSoupによるパース

chardetで文字コードを推測してBeautifulSoupに渡す。chardetしなくてもだいたい正しく返してくれるけど、信頼性重視の富豪的なやり方で。パーサーも比較的重いhtml5libを指定。

import chardet
from bs4 import BeautifulSoup

resp, content = get('https://blog.imind.jp/')
enc = chardet.detect(content[:1024*100])['encoding']
soup = BeautifulSoup(content, features='html5lib', from_encoding=enc)

指定タグの収集

h1タグの属性とテキストを出力する。

for h1 in soup.select('h1'):
    print(h1.attrs, h1.text)

実行結果

{'id': 'title'} iMind Developers Blog 
{'class': ['entry-title']}  AirflowのCheckOperatorでDBの値チェック 
{'class': ['entry-title']}  Python+OpenCVで画像をリサイズして保存する 
{'class': ['entry-title']}  Tensorflow2.0 alphaのBEGINNER TUTORIALSを読む - その2 

(以下略)

指定タグ+ID

h1タグでidにtitleが指定されている要素を取得する。

for h1 in soup.select('h1#title'):
    print(h1.attrs, h1.text)

実行結果

{'id': 'title'} iMind Developers Blog 

指定タグ+指定クラス

h1タグでclassにentry-titleが指定されている要素を取得する。

for h1 in soup.select('h1.entry-title'):
    print(h1.attrs, h1.text)

実行結果

{'class': ['entry-title']}  AirflowのCheckOperatorでDBの値チェック 
{'class': ['entry-title']}  Python+OpenCVで画像をリサイズして保存する 
{'class': ['entry-title']}  Tensorflow2.0 alphaのBEGINNER TUTORIALSを読む - その2 

(以下略)

指定タグ+指定属性

divタグで属性にstyleが指定されている要素を取得する。

for elem in soup.select('div[style]'):
    print(elem.attrs, elem.text)

実行結果

{'id': 'sp-suggest', 'style': 'display: none;'} スマートフォン用の表示で見る
{'id': 'globalheader-container', 'data-brand': 'hatenablog', 'style': 'display: none'}  

(以下略)

指定タグ+指定属性+指定value

divタグで属性にstyleが指定されていてその値がdisplay: noneの要素を取得する。

for elem in soup.select('div[style="display: none;"]'):
    print(elem.attrs, elem.text)

実行結果

{'id': 'sp-suggest', 'style': 'display: none;'} スマートフォン用の表示で見る
{'id': 'hidden-subscribe-button', 'style': 'display: none;'}     読者です 読者をやめる   読者になる 読者になる         

指定タグ+指定属性+指定value前方一致

divタグで属性にstyleが指定されていてその値がdisplayで始まる要素を取得する。

for elem in soup.select('div[style^="display"]'):
    print(elem.attrs, elem.text)

実行結果

{'id': 'sp-suggest', 'style': 'display: none;'} スマートフォン用の表示で見る
{'id': 'globalheader-container', 'data-brand': 'hatenablog', 'style': 'display: none'}     

指定タグ+指定属性+指定value後方一致

divタグで属性にstyleが指定されていてその値がnoneで終わる要素を取得する。

for elem in soup.select('div[style$="none"]'):
    print(elem.attrs, elem.text)

実行結果

{'id': 'globalheader-container', 'data-brand': 'hatenablog', 'style': 'display: none'}  
{'class': ['hatena-star-metadata'], 'style': 'display: none'}  AirflowのCheckOperatorでDBの値チェック 

指定タグ+指定属性+指定value部分一致

divタグで属性にstyleが指定されていてその値がz-indexを含む要素を取得する

for elem in soup.select('div[style*="z-index"]'):
    print(elem.attrs)

実行結果

{'class': ['quote-stock-panel'], 'id': 'quote-stock-message-box', 'style': 'position: absolute; z-index: 3000'}
{'class': ['error-box'], 'id': 'unstockable-quote-message-box', 'style': 'display: none; position: absolute; z-index: 3000;'}

要素A配下の要素Bを取得する

h1タグでclassにentry-titleが指定された要素の子要素にいるaタグを取得する。

for h1 in soup.select('h1.entry-title a'):
    print(h1.attrs, h1.text)

実行結果

{'class': ['entry-title-link'], 'href': 'https://blog.imind.jp/entry/2019/05/09/000249'} AirflowのCheckOperatorでDBの値チェック
{'class': ['entry-title-link'], 'href': 'https://blog.imind.jp/entry/2019/05/06/224816'} Python+OpenCVで画像をリサイズして保存する

(以下略)

anchorのhrefを取得する

aタグのhrefを取得する。

for elem in soup.select('a'):
    print(elem.get('href'), elem.text)

実行結果

# スマートフォン用の表示で見る
https://blog.imind.jp/ iMind Developers Blog 

(以下略)

リンクの相対パスを解決する

スクレイピングするURLは相対パスで書かれていることもあるので、urljoinで解決する。

1つ目の引数にリクエストしているURL、2つ目の引数に相対パスを記述すると、URLが取得される。

from urllib.parse import urljoin
urljoin('http://blog.imind.jp/foo/', './test.txt')
    #=> http://blog.imind.jp/foo/test.txt

絶対パスで書かれているURLに対して実行しても正しく結果が返る。

urljoin('http://blog.imind.jp/foo/', 'http://example.com/bar/test.txt')
    #=> http://example.com/bar/test.txt

改定履歴

Author: Masato Watanabe, Date: 2019-05-10, 記事投稿