試験運用中なLinux備忘録・旧記事

はてなダイアリーで公開していた2007年5月-2015年3月の記事を保存しています。

Pythonのurllibを用いたダウンロードで進行状況を表示、キャッシュをせずに直接ファイルに保存

以前urllibモジュールを使用してファイルのダウンロードを行うコードを書いたが、このときにはurlretrieve()関数やURLopener系オブジェクトのメンバ関数retrieve()を使用していた。
これはURLの文字列と保存先の場所を指定することでサーバからデータをダウンロードするのだが、途中の経過は全く見えない。また、キャッシュの仕組みもあり、一時ファイルが使用される形となっている。
このあたりの動作が好みでない場合、FancyURLopenerの子クラスを作成し、メンバ関数open()を使用して得られたオブジェクトに対して少しずつ読み込みを行うようにメンバ関数retrieve()を上書きすることで、直接出力ファイルに書き出しつつ、進行状況も確認できる。

コード例

#! /usr/bin/python
# -*- encoding: utf-8 -*-

import urllib
import time
import sys
import os

class MyURLopener(urllib.FancyURLopener):
  """
  進行状況を表示しながらURLをファイルに直接ダウンロードして
  タイムスタンプも処理する
  """
  version = "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)"
  def set_localfile_timestamp(self):
    """
    ダウンロードしたファイルの最終更新日時を
    サーバの「Last-Modified」ヘッダに合わせる
    """
    try:
      # 「Sun, 22 Jun 2008 12:34:56 GMT」の形式
      new_timestamp = time.mktime(time.strptime(self.mtime_remote, "%a, %d %b %Y %H:%M:%S GMT")) - time.timezone
    except ValueError:
      print >> sys.stderr, u'注意: URL "%s" の最終更新日時の解析に失敗しました'.encode("utf-8") % self.url
      return
    try:
      os.utime(self.filename, (new_timestamp, new_timestamp))  # 場所, (アクセス, 更新)
    except OSError:
      print >> sys.stderr, u'注意: ファイル "%s" のタイムスタンプ操作に失敗しました'.encode("utf-8") % localfile
  def retrieve(self, url, filename):
    """
    進行状況を表示しながらURLをファイルに直接ダウンロード
    データのキャッシュは行わない
    上書きされたメンバ関数
    """
    self.url = url
    self.filename = filename
    try:
      f_in = self.open(self.url)
    except IOError:
      print >> sys.stderr, u'エラー: URL "%s" が開けません'.encode("utf-8") % self.url
      sys.exit(1)

    # 長さとタイムスタンプをヘッダから取得
    self.mtime_remote = -1  # 「Last-Modified: 」ヘッダが無い場合の値とする
    length = -1             # 同様にヘッダの無い場合のための値とする5
    for l in str(f_in.info()).splitlines():
      if "Content-Length: " in l:
        length = float(l[len("Content-Length: "):])  # 都合によりfloat化
      elif "Last-Modified: " in l:
        self.mtime_remote = l[len("Last-Modified: "):]

    # 出力ファイルを開く
    try:
      f_out = open(self.filename, "wb")
    except IOError:
      print >> sys.stderr, u'エラー: ファイル "%s" に書き込めません'.encode("utf-8") % self.filename.encode("utf-8")
      sys.exit(1)
    # データを溜めながら少しずつファイルに書き出していく
    bufsiz = 4096
    downloaded = 0              # ダウンロード済みバイト数
    while True:
      data = f_in.read(bufsiz)  # データをバッファ分までダウンロード
      if not data:
        break
      f_out.write(data)         # 読んだ分だけファイルに書き込む
      downloaded += len(data)
      # ダウンロード済みサイズと進行状況(Content-Lengthと比べた割合)を表示
      if length != -1:
        print "%d Bytes downloaded (%.1f%%)" % (downloaded, (downloaded / length) * 100)
      else:  # 長さが不明
        print "%d Bytes downloaded" % downloaded
    f_in.close()                # (全てダウンロードしたら)接続を切断
    f_out.close()               # 出力ファイルを閉じる
    # Last-Modifiedヘッダがある場合にローカルファイルのタイムスタンプを変更
    if self.mtime_remote != -1:
      self.set_localfile_timestamp()

if __name__ == "__main__":
  # メイン処理
  if len(sys.argv) < 2:
    print u"使用法: %s [URL] [出力ファイル]".encode("utf-8") % os.path.basename(__file__)
    sys.exit(1)
  url = sys.argv[1]
  try:
    outfile = sys.argv[2]
  except IndexError:  # 2番目の引数が指定されていない場合
    # ファイル名を指定しない場合にはURLから名前を使用して
    # 現在のディレクトリに保存する
    name = os.path.basename(url)
    if name == "":  # 「/」の後ろの名前がない場合はその前の文字列を使用
      name = url[len("http://"):]
    outfile = os.path.join(os.getcwd(), name.replace("/", ""))
  opener = MyURLopener({})
  opener.retrieve(url, outfile)

(2008/6/24)変数urlが一部参照できない部分があるのを修正
(2009/1/13)Windowsでデータがファイル書き出し時に破損する不具合を修正(ファイルへの書き込みモードをバイナリモードに修正・えす氏に感謝)
(2009/2/23)変数fileをoutfileに修正
メンバ関数retrieve()を上書きしているが、元の関数とは引数や戻り値など、異なる部分がある。
この例では、ダウンロードのバッファに書き込むたびに端末に状況の表示を行うため、サイズの大きなファイルをダウンロードすると、文字列が大量に表示される(仕様)。
また、「urllibでダウンロードしたファイルのタイムスタンプをサーバ上の最終更新日時に合わせる」で行ったように、「Last-Modified」ヘッダがある場合にwgetと同様にファイルのタイムスタンプに反映させるようにしてある。

使用例

$ [スクリプトの場所] http://jp.youtube.com/player2.swf
4096 Bytes downloaded (10.7%)
8192 Bytes downloaded (21.4%)
12288 Bytes downloaded (32.2%)
16384 Bytes downloaded (42.9%)
20480 Bytes downloaded (53.6%)
24576 Bytes downloaded (64.3%)
28672 Bytes downloaded (75.1%)
32768 Bytes downloaded (85.8%)
36864 Bytes downloaded (96.5%)
38201 Bytes downloaded (100.0%)
$ ls -l player2.swf
-rw-r--r-- 1 [ユーザ名] [グループ名] 38201 2008-05-02 08:06 player2.swf

「Content-Length」ヘッダがない場合にはパーセンテージ表示を行わない。

$ [スクリプトの場所] http://www.google.co.jp/
4096 Bytes downloaded
8192 Bytes downloaded
12288 Bytes downloaded
14608 Bytes downloaded

引数のURLの後ろに出力ファイルの場所を指定すると、名前を変えて保存するようになっている。

$ [スクリプトの場所] http://www.google.co.jp/ google.html

関連記事:

参考URL: