Webページ監視
与えられたWebページ群を巡回監視し、変化があったら差分を表示するプログラムを作ってみよう。
以下のプログラム watch.py を適当な(空の)ディレクトリに置いて、./watch.py あるいは python3 watch.py などとして起動すると、プログラム最後のリスト targets で与えられたURLを巡回し、カレントディレクトリに(URLのMD5ハッシュ16進をファイル名とする)ファイルを作って保存する(文字コードはUTF-8に統一)。ファイルのタイムスタンプはHTTPのLast-Modifiedヘッダに書かれた日時に設定される(不明の場合は現在時刻)。次回、同様にして watch.py を起動すると、もしURLの指すページが変更されていれば、その差分の先頭100行を表示する。
実はこれはデバッグモードの動作である。もしLinuxサーバなどがあれば、プログラム中の DEBUG = True を DEBUG = False に書き換え、cronなどで定時動作させると、変更があればメールで自分宛に差分を送ってくれる。メールは自マシンのSMTPサーバ(Postfixなど)で無認証で送れるとする(追記:これができない場合のやりかたをメールを送るに書いた)。
#! /usr/bin/env python3
import os
import re
import time
import hashlib
import requests
from datetime import datetime
import difflib
import smtplib
from email.message import EmailMessage
DEBUG = True
os.environ['TZ'] = 'UTC'
time.tzset()
def generate_diff(old_content, new_content):
old_lines = old_content.splitlines(keepends=True)
new_lines = new_content.splitlines(keepends=True)
diff = difflib.unified_diff(old_lines, new_lines, fromfile='old', tofile='new', n=0)
return ''.join(list(diff)[:100]) # 差分の先頭100行を返す
def send_email(subject, body):
if DEBUG:
print(subject)
print(body)
return
msg = EmailMessage()
msg.set_content(body)
msg['Subject'] = subject
msg['From'] = "自分のメールアドレス"
msg['To'] = "自分のメールアドレス"
s = smtplib.SMTP('localhost')
s.send_message(msg)
s.quit()
def download_and_convert_to_utf8(url):
response = requests.get(url)
response.raise_for_status()
content_type = response.headers.get('Content-Type', '')
if 'charset=' in content_type:
charset = content_type.split('charset=')[-1]
else:
charset = 'utf-8'
m = re.search(r']*charset=["\']?(.+?)["\'>]',
response.text, re.IGNORECASE)
if m:
charset = m.group(1)
return response.content.decode(charset, errors='replace')
def download_if_newer_and_notify(url):
local_file_path = hashlib.md5(url.encode()).hexdigest()
if os.path.exists(local_file_path):
local_file_mtime = os.path.getmtime(local_file_path)
local_file_datetime = datetime.fromtimestamp(local_file_mtime)
else:
local_file_datetime = None
try:
response = requests.head(url)
response.raise_for_status()
last_modified_str = response.headers.get('Last-Modified')
if last_modified_str:
last_modified_datetime = datetime.strptime(last_modified_str,
'%a, %d %b %Y %H:%M:%S %Z')
else:
last_modified_datetime = datetime.utcnow()
if local_file_datetime is None or last_modified_datetime > local_file_datetime:
if os.path.exists(local_file_path):
with open(local_file_path, 'rb') as file:
old_content = file.read().decode()
else:
old_content = None
new_content = download_and_convert_to_utf8(url)
with open(local_file_path, 'wb') as f:
f.write(new_content.encode())
mtime = last_modified_datetime.timestamp()
os.utime(local_file_path, (mtime, mtime))
if old_content is not None:
diff = generate_diff(old_content, new_content)
if diff:
send_email("Updated: " + url, diff)
except requests.RequestException as e:
print("Error", url, e)
# 監視対象のURL
targets = [
'https://example.com',
'https://example.org/foo/bar.html',
'https://okumuralab.org/tex/mod/forum/view.php?id=2'
]
for url in targets:
download_if_newer_and_notify(url)
# time.sleep(10) # 必要に応じて設定
Webページの中には、アクセスごとにランダムにページの一部が書き換わるものがあって、厄介である。対応はできそうだが、まだ対応していない。
プログラミング上の注意として、os.environ['TZ'] = 'UTC' がないとUTC時刻を os.utime() でローカル時刻として設定してしまうので、ファイルのタイムスタンプがずれてしまう(time.tzset() は念のために入れた)。これはコマンドラインで ./watch.py の代わりに TZ=UTC ./watch.py と打つのと同じ効果を持つ。ls -l と TZ=UTC ls -l の表示を比べてみれば違いがわかる。
datetime.strptime(last_modified_str, '%a, %d %b %Y %H:%M:%S %Z') は "Tue, 02 Apr 2024 08:33:40 GMT" のような文字列を読んで datetime オブジェクトを返すが、タイムゾーン情報は正確に反映されない。HTTPのLast-ModifiedヘッダはGMTしか返さないので、これで十分であろう。