社内ポータル徘徊にさようなら!Webスクレイピングで更新自動通知

こんにちは。みやまえゆたかです。 ​

導入

​ 当社の社内ポータルサイトはSharePointで作られています。 ​

各種申請書類やマニュアル、規定などへのリンクが集まっていて、 ​ その中でも、新着情報が流れてくる「掲示板」のページは「更新がないか?」1日に1~2回は見に行くようにしています。 ​

ただ、業務や会議がたてこんでいると「掲示板」を見ることを忘れ、重要な情報を見過ごしてしまうことがありました。

「なんで新着をスマホに通知してくれないんだ!!!」

​ 更新が有るか無いかも分からないサイトを定期的に見る作業に疲れた私は、 ​ 「ポータルサイトをWebスクレイピングして、更新があったらSlackに通知する」機能を作りました。 ​

処理は以下のようになっています。

  1. ポータルサイトの「掲示板」を定期的にWebスクレイピングする

  2. 更新がないかチェックする

  3. 更新があったら、記事のタイトルとURLをSlackにPostする ​

これで、社内のSharePointポータルを定期的に見に行く必要がなくなりました!

​ ​

具体的説明

​ 機能の実装にあたって、「SharePointのHTMLを取得できない」という問題の解決に苦労しました。 ​

Webスクレイピング自体は特殊なことはしていないので、 本記事においてはWebスクレイピングに関する説明は省略し、問題解決の一部始終を記します。 ​

HTMLを取得できない

​ Pythonのスクレイピングプログラムを作成して、実行したところエラーが発生。 何らかの要因でHTMLを取得できていないようでした。 ​

原因を調査するために、curlコマンドでHTMLを取得できるか確認してみました。 ​

$ curl -s http://xxx/xxx.aspx
401 UNAUTHORIZED:

​ HTTPステータスコードの401(Unauthorized:認証が必要)が返ってきていいます。 ​

「なるほど。。。」 当然、このままだといくらやってもダメで、認証を通さないとHTMLを取得できないことがわかりました。 ​

特殊な認証がかかっている?

​ 認証の種類によって実装方法が異なるため、今度は-vオプションをつけて実行します。

-vをつけると、HTTPのリクエスト/レスポンスHeaderを確認でき、 WWW-Authenticate:という部分にどんな認証をしようしているかが表示されます。 ​ ​

$ curl -v http://xxx/xxx.aspx
#レスポンス一部抜粋
< HTTP/1.1 401 Unauthorized
< WWW-Authenticate: NTLM
< X-Powered-By: ASP.NET

​ 結果を確認すると、NTLM認証というプロトコルが使われていることが分かりました。 ​

NTLM認証について調べると、SharePointでは一般にNTMLを使用して認証するそうです。へー。 ​ Microsoft ) SharePoint Server でユーザー認証方法を計画する ​ ​

curlでNTLM認証をする方法

curlでNTLM認証を通す場合場合は ​ curl --ntlm --user ユーザー名:パスワード URLと記述する必要があります。 ​

実際にコマンドを打ってみると、 ​

$ curl --ntlm --user xxxx:yyyy http://xxx/xxx.aspx
HTTP/1.1 200 OK
<!DOCTYPE html>...........

​ 200(OK:成功)のステータスコードと、メッセージボディ(HTML)が返ってきました。 ​

NTLM認証を通せばエラーは解消しHTMLを取得できることがこれでやっと分かりました。 ​

PythonでNTLM認証をする方法

​ さて、NTLMで認証することでHTMLが取得できることは分かりましたが、今度はPythonでNTLM認証をする必要があります。 ​

Python3でNTLM認証を通す方法を調べたところ、python-ntlmというライブラリを見つけました。 ​ ​ ライブラリは

pip install python-ntlm3

で追加することができます。

Webスクレイピング サンプルコード

実行環境

  • python3.7.2

サンプルコード

sample.py

import urllib.request

from bs4 import BeautifulSoup
from ntlm3.HTTPNtlmAuthHandler import HTTPNtlmAuthHandler


def scraping(url, user, password):
    http_password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
    http_password_mgr.add_password(None, url, user, password)
    auth_ntlm = HTTPNtlmAuthHandler(http_password_mgr)

    opener = urllib.request.build_opener(auth_ntlm)
    urllib.request.install_opener(opener)

    response = urllib.request.urlopen(url)
    return BeautifulSoup(response, 'lxml')


def scraping_runner():
    user = '認証のID'
    password = '認証のパスワード'
    url = 'http://xxx.com/xxx/xxx.aspx'
    return scraping(url, user, password)


if __name__ == '__main__':
    print(scraping_runner())

これでNTLM認証のサイトでも、スクレイピングできるようになります。 ​

コード全体

​ 最後に、今回の成果物です。 ​ scraping.py

import csv
import os
import sys
import urllib.request

import pandas as pd
import slackweb
from bs4 import BeautifulSoup
from ntlm3.HTTPNtlmAuthHandler import HTTPNtlmAuthHandler


def has_csv(file_path):
    return os.path.exists(file_path) and os.path.getsize(file_path) != 0


def create_csv(file_path):
    with open(file_path, 'w', newline='', encoding='utf_8') as file:
        file.write('タイトル,url,更新日')


def read_csv(file_path):
    if not os.path.exists(file_path):
        raise Exception('ファイルありません')
    if os.path.getsize(file_path) == 0:
        raise Exception('ファイルの中身が空です')

    csv_list = pd.read_csv(file_path, header=None).values.tolist()
    return csv_list


def output_csv(portal_result, file_path):
    with open(file_path, 'w', newline='', encoding='utf_8') as file:
        writer = csv.writer(file)
        for row in portal_result:
            writer.writerow(row)


def scraping(url, user, password):
    http_password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
    http_password_mgr.add_password(None, url, user, password)

    auth_ntlm = HTTPNtlmAuthHandler(http_password_mgr)
    opener = urllib.request.build_opener(auth_ntlm)
    response = opener.open(url)
    soup = BeautifulSoup(response, 'lxml')

    # (ダミー)HTMLから必要なタグ(タイトル、リンク先URL)の部分だけ抜き取ります
    # サイトによって、HTMLの構造が違うのでカスタマイズが必要です
    portal_result = []
    for dfwp_item in soup.find_all(class_='dfwp-item'):
        portal_result.append(
            [dfwp_item.find_all('p')[1].text,
             dfwp_item.find_all('p')[1].a.get('href'),
             dfwp_item.find_all('p')[0].text])
    return portal_result


def list_diff(past_portal_result, portal_result):
    # 前回と今回のスクレイピング結果を比較して、新しいものだけリストで返す
    return_list = []
    for tmp in portal_result:
        if tmp not in past_portal_result:
            return_list.append(tmp)
    return return_list


def send_to_slack(diff_list, slack_api_url, host_url):
    text = ''
    for tmp in diff_list:
        text += tmp[2] + '\n<' + \
                host_url + tmp[1] + '|・' + tmp[0] + '>\n'

    slack = slackweb.Slack(url=slack_api_url)
    slack.notify(text=text)


def main():
    args = sys.argv
    if not len(args) == 4:
        raise Exception('引数を3つ(SlackWebhookURL,UserID,Password)指定してください')
    slack_api_url = args[1]
    user = args[2]
    password = args[3]

    # 前回のスクレイピング結果を取得する
    log_file_path = 'log.csv'
    if not has_csv():
        create_csv(log_file_path)
    create_csv(log_file_path)
    past_portal_result = read_csv(log_file_path)

    # Webスクレイピングする
    host_url = 'http://xxx.com/'
    file_url = 'xxx/xxx.aspx'
    portal_result = scraping(host_url + file_url, user, password)

    update_list = list_diff(past_portal_result, portal_result)
    output_csv(portal_result, log_file_path)
    if update_list:
        send_to_slack(update_list, slack_api_url, host_url)


if __name__ == '__main__':
    main()
    sys.exit()

​ 実際には、 ​

$ python scraping.py [SlackWebHooKのURL] [user-id] [password]

のコマンドで1時間おきに実行していて、更新があればSlackに更新が通知されます。

​ ※Slackに通知を送るにはSlack WebHookURLが必要です。 ​

※「HTMLから必要な項目を抜き出す部分」や「Slackに投稿する文章を作成する部分」は用途に合わせて編集が必要です。 ​ ​

結論

ちょっとしたアイデアとコーディングで、毎日社内ポータルサイトの更新を探す徘徊がなくなり、大変便利になりました。

それではまた次回!

ASKUL Engineering BLOG

2021 © ASKUL Corporation. All rights reserved.