HTTPのHEAD
メソッドを用いるとサーバからファイルの中身をダウンロードすることなく応答ヘッダのみが取得できるのだが、ここではHTTP/HTTPSの低レベル処理を行うモジュールを用いてPythonでHEAD
メソッドの要求を送信してLast-Modified
ヘッダを参照することで内容をダウンロードせずに最終更新日時のみを取得する操作を扱う。
使い方
接続オブジェクトの作成
- Python 3では
http.client
- Python 2では
httplib
の中に
- HTTPSサーバの場合は
HTTPSConnection
- HTTPサーバの場合は
HTTPConnection
というクラスがあり、まずはホスト名もしくはIPアドレスを引数にしてこのクラスのオブジェクトを作成する。:[数字]
を末尾に付けてポート番号を指定することもできる(指定しない場合は各プロトコルの既定のポート番号となる)。
# Python 3の場合
import http.client
c = http.client.HTTPConnection ('www.example.com')
このオブジェクト作成の段階でポート番号部分に数字以外のような無効な文字列を指定すると例外が発生する。
Python 3とPython 2の両方で動くプログラムを書きたい場合はimport
文を工夫することで対応できる。詳しくは後述の使用例を参照。
http://
で始まるような形式のURL文字列は(URLスキームやパス名も含むため)そのまま渡すことはできず、別途urlparse()
関数などにより文字列処理を行って要素を分割する必要がある。後述の使用例の1つでは手動で処理を行っているが、用途によってはurlparse()
を使う必要はないかもしれない。
- Python 3では
urllib.parse
内のurlparse()
- Python 2では
urlparse
内のurlparse()
リクエストの送信
このオブジェクトのメンバ関数request()
を
- 1番目の引数: 文字列
HEAD
- 2番目の引数: 対象ファイルのドキュメントルートからのパス名(
/
から始まる形)
これらを指定して呼び出すことで要求をサーバに送信する。
c.request ('HEAD', '/')
追加の要求ヘッダを付ける場合はheaders
という名前の辞書型のキーワード引数を渡す。
c.request ('HEAD', '/', headers = {'[header name]' : '[value]'})
接続に失敗した場合は例外が発生し、大きすぎるポート番号などが指定された場合もここで例外が発生する。
指定したファイルが見つからないことなどによる失敗の際には例外は発生しない。
応答オブジェクトの取得
同オブジェクトのメンバ関数getresponse()
により、応答の内容を含んだオブジェクトが得られる。
res = c.getresponse ()
最終更新日時の取得
応答オブジェクトのメンバ関数getheader()
の引数に文字列Last-Modified
を指定して呼び出すことで、サーバから返された最終更新日時の文字列が戻り値として得られる。また、メンバstatus
にはHTTPのステータスコード(200
や404
など)が整数値として入り、200
のときにのみLast-Modified
を表示するようにコードを記述することもできる。
last_modified = res.getheader ('Last-Modified')
if res.status == 200:
# 表示処理...
なお、301
(永続的なリダイレクト)や302
などが返された場合、Last-Modified
の代わりにLocation
を渡した戻り値として転送先のURLが得られ、これを用いてこれまでの流れと同様にして再度200
が得られるまで処理を行う。
使用例
いずれの使用例もリダイレクトには対応しているが、これが循環してしまうような場合は考慮していない。
指定URLのステータスコードと最終更新日時を表示
この例ではURL文字列の解析処理を手動で行っている。
get_http_last_modified.py
#! /usr/bin/python
# Get "Last-Modified" header from HTTP/HTTPS server
from __future__ import print_function
import sys
def get_http_last_modified (url):
path = host = None
if url.startswith ('https://'):
# HTTPS URL
try:
from http.client import HTTPSConnection as Connection # Python 3
except:
from httplib import HTTPSConnection as Connection # Python 2
# Parse URL manually
# Examples:
# https://www.example.com -> host='www.example.com', path='/'
# https://www.example.com/ -> host='www.example.com', path='/'
# https://www.example.com/dir/file.ext -> host='www.example.com', path='/dir/file.ext'
host_and_path = url[8:]
try:
idx = host_and_path.index ('/')
host = host_and_path[:idx]
path = host_and_path[idx:]
except:
host = host_and_path
path = '/'
elif url.startswith ('http://'):
# HTTP URL
try:
from http.client import HTTPConnection as Connection
except:
from httplib import HTTPConnection as Connection
host_and_path = url[7:]
try:
idx = host_and_path.index ('/')
host = host_and_path[:idx]
path = host_and_path[idx:]
except:
host = host_and_path
path = '/'
else:
# Not HTTP/HTTPS
sys.exit ('Error: URL "{0}" is invalid.'.format (url))
try:
c = Connection (host)
c.request ('HEAD', path)
res = c.getresponse ()
except Exception as e:
sys.exit ('Error: Failed to get header: "{0}"'.format (e))
print ('URL: {0}\nStatus: {1}'.format (url, res.status))
if res.status == 200:
return res.getheader ('Last-Modified')
elif res.status == 301 or res.status == 302 or res.status == 303 or res.status == 307 or res.status == 308:
return get_http_last_modified (res.getheader ('Location'))
else:
return None
if __name__ == '__main__':
if len (sys.argv) != 2:
sys.exit ('USAGE: {0} [URL]'.format (__file__))
url = sys.argv[1]
last_modified = get_http_last_modified (url)
if last_modified:
print ('Last-Modified: {0}'.format (last_modified))
URLと過去に取得した最終更新文字列を指定して現在のサーバ上の最終更新日時と一致しているかをチェックする
この例ではURL文字列の解析処理にurlparse()
を使用している。この使用例はインターネット上で公開されているソフトウェアの最新バージョンを常に取得可能なURLのファイルについて変更があった際にそれを知るなどの使い方ができるかもしれない。
compare_http_last_modified.py
#! /usr/bin/python
# "Last-Modified" comparing tool
from __future__ import print_function
try:
from urllib.parse import urlparse
except:
from urlparse import urlparse
import sys
def get_http_last_modified (url):
o = urlparse (url)
if o.scheme == 'https':
try:
from http.client import HTTPSConnection as Connection # Python 3
except:
from httplib import HTTPSConnection as Connection # Python 2
elif o.scheme == 'http':
try:
from http.client import HTTPConnection as Connection
except:
from httplib import HTTPConnection as Connection
else:
sys.exit ('Error: URL "{0}" is invalid.'.format (url))
path = '{0}{1}{2}'.format (o.path, '?' if o.query != '' else '', o.query)
try:
c = Connection (o.netloc)
c.request ('HEAD', path)
res = c.getresponse ()
except Exception as e:
sys.exit ('Error: Failed to get header: "{0}"'.format (e))
if res.status == 200:
return res.getheader ('Last-Modified')
elif res.status == 301 or res.status == 302 or res.status == 303 or res.status == 307 or res.status == 308:
return get_http_last_modified (res.getheader ('Location'))
else:
return None
if __name__ == '__main__':
if len (sys.argv) != 3:
sys.exit ('USAGE: {0} [URL] [EXPECTED_LAST_MODIFIED]'.format (__file__))
url, expected_last_modified = sys.argv[1:3]
last_modified = get_http_last_modified (url)
print ('URL: {0}'.format (url))
if not last_modified:
sys.exit ('Status is not 200.')
elif last_modified != expected_last_modified:
sys.exit ('Last-Modified is changed:\n Expected: {0}\n Current: {1}'.format (expected_last_modified, last_modified))
else:
print ('Last-Modified is unchanged: {0}'.format (last_modified))
同様のチェックをIf-Modified-Sinceヘッダの送信により行うもの
リクエスト時の引数によりIf-Modified-Since
ヘッダを付けて要求を送信し
304
が得られれば変更なし200
が得られれば変更あり
と判断するもの。サーバによっては対応していない(変更がなくても200
を返す)ようだ。
compare_http_last_modified-ims.py
#! /usr/bin/python
# "Last-Modified" comparing tool ('If-Modified-Since' version)
from __future__ import print_function
try:
from urllib.parse import urlparse
except:
from urlparse import urlparse
import sys
def compare_http_last_modified (url, expected_last_modified):
o = urlparse (url)
if o.scheme == 'https':
try:
from http.client import HTTPSConnection as Connection # Python 3
except:
from httplib import HTTPSConnection as Connection # Python 2
elif o.scheme == 'http':
try:
from http.client import HTTPConnection as Connection
except:
from httplib import HTTPConnection as Connection
else:
sys.exit ('Error: URL "{0}" is invalid.'.format (url))
path = '{0}{1}{2}'.format (o.path, '?' if o.query != '' else '', o.query)
try:
c = Connection (o.netloc)
# Send 'If-Modified-Since' header
c.request ('HEAD', path, headers = {'If-Modified-Since' : expected_last_modified})
res = c.getresponse ()
except Exception as e:
sys.exit ('Error: Failed to get header: "{0}"'.format (e))
if res.status == 304 or res.status == 200:
return (res.status, res.getheader ('Last-Modified'))
elif res.status == 301 or res.status == 302 or res.status == 303 or res.status == 307 or res.status == 308:
return compare_http_last_modified (res.getheader ('Location'), expected_last_modified)
else:
return (res.status, None)
if __name__ == '__main__':
if len (sys.argv) != 3:
sys.exit ('USAGE: {0} [URL] [EXPECTED_LAST_MODIFIED]'.format (__file__))
url, expected_last_modified = sys.argv[1:3]
status, last_modified = compare_http_last_modified (url, expected_last_modified)
print ('URL: {0}'.format (url))
if status == 304:
print ('Last-Modified is unchanged: {0}'.format (expected_last_modified))
elif status == 200:
if last_modified:
sys.exit ('Last-Modified is changed:\n Expected: {0}\n Current: {1}'.format (expected_last_modified, last_modified))
else:
sys.exit ('Last-Modified is unknown')
else:
sys.exit ('Got unknown status {0}.'.format (status))
- Python 2.7.14, 3.6.3