こんにちは。みやまえゆたかです。
導入
当社の社内ポータルサイトはSharePointで作られています。
各種申請書類やマニュアル、規定などへのリンクが集まっていて、 その中でも、新着情報が流れてくる「掲示板」のページは「更新がないか?」1日に1~2回は見に行くようにしています。
ただ、業務や会議がたてこんでいると「掲示板」を見ることを忘れ、重要な情報を見過ごしてしまうことがありました。
「なんで新着をスマホに通知してくれないんだ!!!」
更新が有るか無いかも分からないサイトを定期的に見る作業に疲れた私は、 「ポータルサイトをWebスクレイピングして、更新があったらSlackに通知する」機能を作りました。
処理は以下のようになっています。
ポータルサイトの「掲示板」を定期的にWebスクレイピングする
更新がないかチェックする
更新があったら、記事のタイトルと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に投稿する文章を作成する部分」は用途に合わせて編集が必要です。
結論
ちょっとしたアイデアとコーディングで、毎日社内ポータルサイトの更新を探す徘徊がなくなり、大変便利になりました。
それではまた次回!