beautifulsoup4でWebスクレイピングしてブログ更新お知らせbotを作ってみた

こんにちは。
急にWebスクレイピングをやってみたくなったので、ギークのホームページで試してみました。
ギークのホームページでは、urlwatchというオープンソースを使って固定ページの変更監視をしています。
こちらは指定したURLからHTMLをダウンロードし、前回取得分との差分を出力してくれる便利ツールです。今回はWebスクレイピングを利用して、ブログが更新されたら「タイトル」、「書いた人」、「記事へのリンク」をSlackのチャンネルを通知するスクリプトを作成しました。
使用したライブラリはBeautifulSoup4です。

目次

環境

・CentOS Linux release 7.5.1804
・python 3.4

コード

実際のコードを見ていきましょう。

import datetime
import pytz
import certifi
import urllib.request
import requests
import json
from bs4 import BeautifulSoup
# urlopen error [SSL: CERTIFICATE_VERIFY_FAILED]を回避
import ssl
ssl._create_default_https_context = ssl._create_unverified_context

# 1時間以内に投稿された記事を取得
def check_update(url):
    now = datetime.datetime.now(pytz.timezone('Asia/Tokyo'))
    target_date = now - datetime.timedelta(hours=1)
    html = urllib.request.urlopen(url)
    soup = BeautifulSoup(html, "html.parser")
    # 記事のリスト要素を取得
    posts = soup.find(class_="blog_list").find_all("li", class_="clearfix")
    articles = []

    for post in posts:
        title = post.h3.a.string
        url = post.h3.a['href']
        author = post.find(class_="author").span.string
        # 日付をチェックするために記事の中へ
        article_html = urllib.request.urlopen(url)
        soup = BeautifulSoup(article_html, "html.parser")
        post_date_str = soup.find('meta', attrs={'property': 'article:published_time'}).get('content')
        post_date = datetime.datetime.strptime(post_date_str, "%Y-%m-%dT%H:%M:%S%fZ")
        post_date = pytz.utc.localize(post_date).astimezone(pytz.timezone("Asia/Tokyo"))
        # 日付チェック
        if post_date < target_date:
            break

        article_dict = {
            "title": title,
            "url": url,
            "author": author
        }
        articles.append(article_dict)

    return articles

# slackへ通知を行う
def send_to_slack(webhook_url, articles):
    # textを整形
    text = ":muscle:ギークブログが更新されたよ:punch::punch::punch:\n\n"
    for article in articles:
        text += "*" + article["title"] + "*\n*"
        text += "書いた人: " + article["author"] + "*\n"
        text += article["url"] + "\n\n"

    username = "ギークブログ更新お知らせbot"
    requests.post(webhook_url, data = json.dumps({
        'text': text,
        'username': username,
        'icon_emoji': ':snowman:',
        'link_names': 1,
    }))
    return

def main():
    url = "https://www.geekfeed.co.jp/geekblog"
    articles = check_update(url)

    if len(articles) != 0:
        webhook_url = "https://hooks.slack.com/services/TXXXXXXX9/BXXXXXXXR/4XXXXXXXXXXXXXXXs"
        send_to_slack(webhook_url, articles)

if __name__ == "__main__":
    main()

コードレビュー

check_update

geekblogのトップページをurl引数として、1時間以内に投稿された記事リストを取得します。

時刻に関して(14、29~31行目)

時刻計算に関して、pytonのdatetimeクラスには”naive”と”aware”の2つのオブジェクトがあるので注意が必要です。
awareはタイムゾーンの情報などを持っており、他のawareオブジェクトとの比較が可能です。
コードでいうと、33行目のような比較ができます。
一方naiveオブジェクトはそういった情報を持たないため、awareとの比較はできません。

今回は、スクレイピングで取得した時刻にタイムゾーン計算が含まれていたので、awareオブジェクトを採用しました。

要素の取得に関して

19行目では、beautifulsoup4のfindメソッドとfind_allメソッドを組み合わせています。

findは指定した検索条件にあった最初の要素を返します。
find_allは指定した検索条件にあったすべての要素を返します。

記事のリストを取得するため、”clearfix”クラスで検索をかけたかったのですが、
記事以外の要素にも使用されていたため、検索範囲を絞るためにfindを使っています。

metaタグも取得可能です。
記事の公開日時がmetaタグに含まれていたため、以下のコードで取得しています。

"""
<meta property="article:published_time" content="2019-09-17T10:39:11Z">
"""
post_date_str = soup.find('meta', attrs={'property': 'article:published_time'}).get('content')

リターンオブジェクトに関して(20、33~43行目)

articlesという空の配列を作成しておき、その中に対象記事の情報を持つdictオブジェクトを格納します。
記事は最新のものから順に取得していくので、時間条件に外れた時点でbreakで記事取得処理を終了します。

ファンクションはarticlesを返します。
記事が1件もない場合はそのまま空の配列を返します。

send_to_slack

Slackのwebhook urlとarticlesオブジェクトを引数に、Slackへメッセージを送信します。

記事の情報をtextに整形して、単純にPOSTを行っているだけです。
送信したときはこんな感じになります。

main

urlとwebhook_urlは可変の変数なのでmain内で定義していますが、
実際はこのブログのURLでしかこのスクリプトは正常動作しないので定数でも良かったですね。。

check_updateファンクションの後、空の配列が帰ってきた場合には何もせずに終了しています。

おわりに

Webスクレイピングはとても簡単に行うことができることがわかりました。
インターネット上に情報が溢れているので、それらを蓄積してデータを活用できると思うとワクワクしますね。
ですが、問題点もあります。

・スクレイピングを許可していないコンテンツに気をつけなければいけない
・ページ内のどの要素を取得するか個別に設定が必要
・上記を踏まえた上でぱっと思いつく有効活用方法がわからない

似たような作りのサイトであっても、要素が少し違うだけで設定を変えなければなりません。
今回の記事のスクリプトのように、自社内のRPAとして使うのはお試しでは入りやすいのかなと思います。

この記事が気に入ったら
いいね ! しよう

Twitter で

【採用情報】一緒に働く仲間を募集しています

採用情報
ページトップへ