2018/02/28

PythonでHTTP/HTTPSサーバからファイルの最終更新日時のみを取得

HTTPのHEADメソッドを用いるとサーバからファイルの中身をダウンロードすることなく応答ヘッダのみが取得できるのだが、ここではHTTP/HTTPSの低レベル処理を行うモジュールを用いてPythonでHEADメソッドの要求を送信してLast-Modifiedヘッダを参照することで内容をダウンロードせずに最終更新日時のみを取得する操作を扱う。

  1. 使い方
    1. 接続オブジェクトの作成
    2. リクエストの送信
    3. 応答オブジェクトの取得
    4. 最終更新日時の取得
  2. 使用例
    1. 指定URLのステータスコードと最終更新日時を表示
    2. URLと過去に取得した最終更新文字列を指定して現在のサーバ上の最終更新日時と一致しているかをチェックする
    3. 同様のチェックをIf-Modified-Sinceヘッダの送信により行うもの

使い方

接続オブジェクトの作成

  • 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のステータスコード(200404など)が整数値として入り、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