クラスを定義する

>>> class Point:                   # クラス定義の開始
...     format = "%s [X:%s Y:%s]"  # クラス変数
...
...     def __init__(self, x, y):  # メソッドの定義
...         self.str = "Point"     # インスタンス変数
...         self.set(x, y)
...
...     def set(self, x, y):
...         self.__x = int(x)      # プライベートな属性
...         self.__y = int(y)
...
...     def show(self):
...         print Point.format % (self.str, self.__x, self.__y)
... # クラス定義の終了(ここでクラスオブジェクトが生成される)
>>>
>>> p1 = Point(2, 6)  # インスタンスの生成(クラスオブジェクトの呼び出し)
>>> p2 = Point(1,9)
>>>
>>> p1.show()         # メソッドの呼び出し
Point [x=2 y=6]
>>> p2.show()
Point [x=1 y=9]
>>>
>>> p1.str = "座標"   # インスタンス変数へ代入
>>> p1.show()
座標 [x=2 y=6]
>>> p2.show()         # 別のインスタンスには影響なし
Point [x=1 y=9]
>>>
>>> Point.format = "%s [x=%s y=%s]" # クラス変数へ代入
>>> p1.show()         # 全てのインスタンスに影響あり
座標 [x=2 y=6]
>>> p2.show()
Point [x=1 y=9]
>>>
>>> p1._Point__x      # プライベートな属性の参照
2
  • クラス定義の開始はclass クラス名:
  • クラス内で関数を定義すると、メソッドになる
  • メソッドの第一引数は、self(そのメソッドのインスタンス
  • メソッドを呼び出す際には、インスタンスを渡す必要はない
  • __init__( )はインスタンス生成直後に呼び出される
  • インスタンスの属性(データまたはメソッド)はインスタンス名.属性名で参照する
    • メソッド内から参照する場合は、self.属性名
  • クラス変数とはクラスオブジェクトの属性、クラス名.属性名で参照する
  • インスタンスの属性をプライベートにするには、名前を__で始まり__で終わらないものにする
    • プライベートな属性は、インスタンス名.__クラス名_属性名で参照する

ニコニコから動画を落とすスクリプト

勉強がてら、ニコニコから動画をダウンロードするスクリプトを書いてみました。一応、コメントとヘルプメッセージは英語にしてみましたが、正直、そこが一番自信がないです。というかプログラムそのものよりも、どういうメッセージを表示するかの方が悩みました。日本語としても英語としてもたいした文じゃありませんが。

使い方

python nico-download.py -m メールアドレス -p パスワード
-c Cookieを保存するファイル - d ファイルを保存するディレクトリ
http://www.nicovideo.jp/watch/{video_id}
$ python nico-download.py -m xxxxxx -p xxxxxx -c cookie.txt
-d "C:\My Documents" http://www.nicovideo.jp/watch/sm1462977
Directory: C:\My Documents
Filename : 麻雀 天鳳 東風 上級卓 その3.flv
Downloading video data ...
|=============================================| 28999/28999K (100%) 

ソースコード

#!python
# encoding=utf-8

# Download a flv file from http://www.nicovideo.jp/watch/{video ID}

import os
import sys
import re
import urllib
import urllib2
import cookielib
from optparse import OptionParser

# Constants for niconico
video_url_format = "http://www.nicovideo.jp/watch/{video_id}"
login_url = "https://secure.nicovideo.jp/secure/login"
api_url = "http://www.nicovideo.jp/api/getflv?v="
video_url_re = re.compile(r"http://www\.nicovideo\.jp/watch/(sm\d+)")
video_title_re = re.compile(r'<h1><a class="video" [^<>]*>(.+)</a></h1>')
video_params_re = re.compile(r"([^&]+)=([^&]*)")

# Constants for progress bar
progress_bar_width = 45
progress_bar_chr = "="

kb = 1024
block_size = kb * 10

# Convert byte into KB
def to_k(byte):
	return byte / kb

def is_login(response):
    if response.info().getheader("x-niconico-authflag") == "1":
        return True
    else:
        return False

def login(mail, password):
    if mail is None: mail = raw_input("mail: ")
    if password is None: password = raw_input("pass: ")
    try:
        query = {"mail":mail, "password":password}
        query = urllib.urlencode(query)
        response = urllib2.urlopen(login_url, query)
        if is_login(response):
            if options.cookie_file:
                cookie_jar.save(options.cookie_file)
        else:
            sys.exit("Error: unable to login (email address or password is invalid)")
    except urllib2.URLError, e:
        url_error("Error: unable to login")

def show_progress_bar(pbar_chr, pbar_width, total, total_width, downloaded):
    pbar_chrs = pbar_chr * (pbar_width * downloaded / total)
    percentage = 100 * downloaded / total
    print "\r|%-*s| %*d/%dK (%3d%%)" % \
        (pbar_width, pbar_chrs, total_width, downloaded, total, percentage),
    sys.stdout.flush()

# Handle urllib2.URLError
def url_error(message, error):
    if isinstance(error, urllib2.HTTPError):
        message += " (%s %s)" % (error.code, error.msg)
    sys.exit(message)


# Create the command line options parser and parse command line
usage = "usage: %%prog [options] %s" % video_url_format
parser = OptionParser(usage)
parser.add_option("-m", dest="mail",
                  help="specify the email address")
parser.add_option("-p", dest="password",
                  metavar="PASS",
                  help="specify the password")
parser.add_option("-c", dest="cookie_file",
                  metavar="FILE",
                  help="load the login cookie from FILE and save it to FILE")
parser.add_option("-d", dest="save_dir",
                  metavar="PREFIX",
                  help="save the file to PREFIX/ (default current directory)")
(options, args) = parser.parse_args()

# Extract the video ID from the video url string
if not args:
    parser.print_help()
    sys.exit(1)
video_url = args[0]
match = video_url_re.match(video_url)
if match is None:
    sys.exit("Error: video url not in this format: %s" % video_url_format)
video_id = match.group(1)

# Check whether the directory specified by -d option exists
if options.save_dir:
    if not os.path.isdir(options.save_dir):
        sys.exit("Error: %s is not directory" % options.save_dir)

# Configure urllib2 to use cookies
cookie_jar = cookielib.LWPCookieJar()
try:
    cookie_jar.load(options.cookie_file)
except:
    pass

cookie_handler = urllib2.HTTPCookieProcessor(cookie_jar)
opener = urllib2.build_opener(cookie_handler)
urllib2.install_opener(opener)

# Get the cookie from the video page
try:
    response = urllib2.urlopen(video_url)
    if not is_login(response): # Login and retry if not logged in
        login(options.mail, options.password)
        response = urllib2.urlopen(video_url)
except urllib2.URLError, e:
    url_error("Error: unable to download the video page", e)

# Exract the video tilte
video_title = None
html_data = response.read()
match = video_title_re.search(html_data)
if match:
    video_title = match.group(1).decode("utf-8", "ignore")
    # Convert characters that cannot be used for a filename into space
    video_title = re.compile('[\/:*?"<>|]').sub(" ", video_title)

# Extract the url of the flv file from the api page
try:
    response = urllib2.urlopen(api_url + video_id)
except urllib2.URLError, e:
    url_error("Error: unable to download the api page", e)
else:
    html_data = response.read()
    params = dict(video_params_re.findall(html_data))
    if "url" in params:
        flv_url = urllib.unquote(params["url"])
    else:
        sys.exit("Error: unable to extract the url of the file file")

# Open {video_id}.flv or {video_title}.flv for writing
save_dir = options.save_dir or "."
filename = (video_title or video_id) + ".flv"
video_file = None
for i in range(2):
    try:
        filepath = os.path.join(save_dir, filename)
        video_file = open(filepath, "wb")
    except (OSError, IOError):
        if filename.startswith(video_id):
            sys.exit("Error: unable to open %s for writing" %\
                    os.path.abspath(filepath))
        else:
            filename = video_id + ".flv"

# Download video data
print "Directory: " + os.path.abspath(save_dir)
print "Filename : " + filename
print "Downloading video data ..."
sys.stdout.flush()
try:
    response = urllib2.urlopen(flv_url)
    total = response.info().getheader("content-length")
    total = to_k(int(total))
    total_width = len(str(total))
    downloaded = 0
    while True:
        video_data = response.read(block_size)
        if not video_data: break
        video_file.write(video_data)
        downloaded += to_k(len(video_data))
        show_progress_bar(progress_bar_chr, progress_bar_width,
            total, total_width, downloaded)
except urllib2.URLError, e:
    url_error("\nError: unable to download video data", e)
finally:
    video_file.close()

フォーマット文字列のフィールド幅

>>> n = 1000
>>> format = "%%%dd" % len(str(n))
>>> format % 10
'  10'

この書き方だと%が多くてわかりにくいと思っていたところ、

>>> n = 1000
>>> size = len(str(n))
>>> format = "%*d"
>>> format % (size, 10) # フィールド幅、値の順
'  10'

こういう風に書けるということを知りました。

コマンドラインオプションの解析

from optparse import OptionParser, OptionValueError
import os

# スクリプトの使用方法を表す文字列
# デフォルト値は"Usage: %prog [options]"
# "usage: "で始まらないと自動的に"usage :"が追加される
# %progはスクリプト名で置換
usage = "usage: %prog [options] keyword"

# OptionPraserのインスタンスを生成
parser = OptionParser(usage)


# オプションの追加
# action オプションが見つかった場合に行う処理
# type   オプションの型
# dest   引数の保存先
#        省略時は長いオプション名を使用
#        それも省略なら短いオプション名
# default オプションのデフォルト値
#         省略した場合のデフォルト値はNone
# metavar ヘルプ表示で使われる仮引数
#         省略時は保存先の名前を大文字にして使用
parser.add_option(
    "-f", "--file",  # どちらか一つは必ず必要
    action="store",  # 引数を保存(デフォルト値)
    type="string",   # 引数をそのまま文字列として保存(デフォルト値)
    dest="log_file",
    help="log file"
)
parser.add_option(
    "-l", "--line",
    type="int",      # 引数をint( )で変換して保存
    metavar="N",
    default=10,
    help="display search result up to N"
)
parser.add_option(
    "-s", 
    type="choice",   # 引数がchoicesに含まれていなければエラー
    choices=["Yahoo", "Google", "Wikipedia"],
    default="Yahoo",
    metavar="SEARCH ENGINE",
    help="choose search engine form Yahoo, Google, Wikipedia"
)
parser.add_option(
    "-x",
    action="append_const", # constで指定した定数を追加
    const="X",
    dest="hoge",
    default=[],            # この場合はリストでなければエラー
                           # デフォルト値に[]を指定しなくても、オプションが
                           # 見つかった時点で保存先は[]で初期化される
    help="meaningless option"
)
parser.add_option(
    "-y",
    action="append_const",
    const="Y",
    dest="hoge",
    help="meaningless option"
)
parser.add_option(
    "-d", "--debug",
     action="store_true", # Trueを保存
                          # store_falseならFalseを保存
     default=False,
     help="debug"
)
def check_directory(option, opt_str, value, parser):
    if os.path.isdir(value):
        parser.values.dir = value # オプションの引数を保存
    else:
        raise OptionValueError("option -dir: %s is not directory" % value)
parser.add_option(
    "--dir",
    action="callback",    # callbackで指定した関数を呼び出す
    callback=check_directory,
    type="string",        # 引数を取るなら必須
    default="./",
    dest="dir",
    help="direcotry to hogehoge"
)

# コマンドラインの解析
# options 全てのオプションの値が入ったオブジェクト
# args    コマンドライン解析後に残った引数
(options, args) = parser.parse_args()

# エラー処理
if not args:
    # エラーメッセージを表示し、プログラムを終了
    parser.error("requires keyword")
    #parser.print_help() # ヘルプメッセージを表示
    #exit()              # プログラムを終了

# オプションを参照
# dest="file"ならoptions.fileで参照
# destが省略されていれば長いオプション名で参照
# それも省略されていれば短いオプション名で参照
print "options.log_file =", options.log_file
print "options.line =", options.line
print "options.s =", options.s
print "options.hoge =", options.hoge
print "options.debug =", options.debug
print "options.dir =", options.dir
print "keyword =", args[0]
$ test.py -h
Usage: test.py [options] keyword

Options:
  -h, --help            show this help message and exit
  -f LOG_FILE, --file=LOG_FILE
                        log file
  -l N, --line=N        display search result up to N
  -s SEARCH ENGINE      choose search engine form Yahoo, Google, Wikipedia
  -x                    meaningless option
  -y                    meaningless option
  -d, --debug           debug
  --dir=DIR             direcotry to hogehoge

$ test.py python
options.log_file = None
options.line = 10
options.s = Yahoo
options.hoge = []
options.debug = False
options.dir = ./
keyword = python

$ test.py python -f hoge.txt -l 20 -s Google -x -y -d --dir hoge
options.log_file = hoge.txt
options.line = 20
options.s = Google
options.hoge = ['X', 'Y']
options.debug = True
options.dir = hoge
keyword = python

$ test.py 
Usage: test.py [options] keyword

test.py: error: requires keyword

$ test.py python -f
Usage: test.py [options] keyword

test.py: error: -f option requires an argument

$ test.py python -l 20x
Usage: test.py [options] keyword

test.py: error: option -l: invalid integer value: '20x'

$ test.py python -s Goo
Usage: test.py [options] keyword

test.py: error: option -s: invalid choice: 'Goo'
 (choose from 'Yahoo', 'Google', 'Wikipedia')

$ test.py python --dir hogehoge
Usage: test.py [options] keyword

test.py: error: option -dir: hogehoge is not directory

詳しくはoptparserのマニュアルを参照してください。

urllib2でCookieを使う

urllib2モジュールでCookieを使ったWebへのアクセスは以下のように行います。

import urllib2, cookielib
cj = cookielib.CookieJar()              # Cookieを格納するオブジェクト
cjhdr = urllib2.HTTPCookieProcessor(cj) # Cookie管理を行うオブジェクト
opener = urllib2.build_opener(cjhdr)    # OpenDirectorオブジェクトを返す
r = opener.open(url)

urllib2.build_opener( )はurllib2.BaseHandlerクラスまたはそのサブクラスのインスタンス(ハンドラオブジェクト)を引数に取ります。OpenDirectorオブジェクトは複数のハンドラを経由してリクエストの送信及びレスポンスの受信を行います。


実際にCookieの管理ができているかを確かめるために、ニコニコから動画を落としてみます。

>>> import urllib, urllib2, cookielib, re
>>>
>>> cj = cookielib.CookieJar()
>>> opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
>>>
>>> # ログインCookieを取得
... r = opener.open("https://secure.nicovideo.jp/secure/login",
...     "mail=%s&password=%s" % (mail, password))
>>>
>>> # FLVファイルのURLを取得
... r = opener.open("http://www.nicovideo.jp/api/getflv?v=" + flv_id)
>>> params = re.compile("([^&]+)=([^&]*)").findall(r.read())
>>> params = dict(params)
>>> flv_url = urllib.unquote(params["url"])
>>>
>>> # 動画ページのCookieを取得(FLVファイルのダウンロードに必要)
... r = opener.open("http://www.nicovideo.jp/watch/" + flv_id)
>>>
>>> # ファイルをダウンロード
... f = open(flv_id + ".flv", "wb")
>>> r = opener.open(flv_url)
>>> f.write(r.read())
>>> f.close()

きちんと落とせました。

urllib2モジュール

Getリクエス

>>> import urllib2
>>> r = urllib2.urlopen("http://www.yahoo.co.jp")

Postリクエス

>>> import urllib
>>> query = {"name":name, "password":password} # 送信するデータ
>>> query = urllib.urlencode(query )           # URLエンコード
>>> r = urllib2.urlopen("http://www.hatena.ne.jp/login", query)

レスポンスオブジェクトのメソッド

>>> r = urllib2.urlopen("http://d.hatena.ne.jp/yumimue/edit")
>>> r.code, r.msg         # レスポンスコードとメッセージ
(200, 'OK')
>>> r.geturl()            # 取得したリソースのURLを返す
'http://d.hatena.ne.jp/yumimue/'
>>> r.info()              # ヘッダオブジェクトを返す
<httplib.HTTPMessage instance at 0x00DD7A80>
>>> r.geturl()            # 取得したデータのURLを返す
'http://d.hatena.ne.jp/yumimue/'
>>> r.info()              # ヘッダオブジェクトを返す
<httplib.HTTPMessage instance at 0x00DD7A80>
>>>
>>> body = r.read()       # データを全て読み込む
>>> r.read(50)            # 50バイトのデータを読み込む
''
>>> r.readline()          # 1行分のデータを読み込む
''
>>> r.readlines()         # 各行をリストにして読み込む
[]

geturl( )はリダイレクト先のURLが知りたい場合に使います。また、Fileオブジェクトと違いファイルポインタを戻すことはできませんので、一度読み込んだデータを再び読み込むことはできません。上記のようにread( )の後にreadline( )やreadlines( )を呼び出しても無意味です。

レスポンスヘッダを取り出す

>>> r = urllib2.urlopen("http://www.hatena.ne.jp/login",
...      urllib.urlencode({"name":name, "password":password}))
>>> headers = r.info()
>>> headers.getheaders("set-cookie")    # リストで取得
['rk=b1ab4aa8f986f75d6398e92d836e; domain=.hatena.ne.jp; path=/']
>>> headers.getheader("content-length") # 文字列で取得
'20227'
>>> headers.getheader("hoge", "0")  # ヘッダがなければデフォルト値を返す
'0'

ヘッダの値は文字列なので、デフォルト値も文字列にしておいた方がよいと思います。

リクエストヘッダを追加する

urllib.urlopen( )に文字列ではなく、Requestオブジェクトを渡します。

>>> req = urllib2.Request("http://d.hatena.ne.jp/yumimue/edit")
>>> req.add_header("Cookie", cookie)
>>> r = urllib2.urlopen(req)
>>> r.geturl()
'http://d.hatena.ne.jp/yumimue/edit'
>>>
>>> req = urllib2.Request("http://d.hatena.ne.jp/yumimue/edit",
...                        headers={"Cookie":cookie})
>>> r = urllib2.urlopen(req)
>>> r.geturl()
'http://d.hatena.ne.jp/yumimue/edit'

ヘッダをリダイレクト先に送信したくない場合は、add_unredirected_header( )を使います。

例外

>>> urls = ("http://ww.yahoo.co.jp",
...         "http://www.yahoo.co.jp/i.htm",
...         "http://www.yahoo.co.jp/index.html")
>>> for url in urls:
...     try:
...         r = urllib2.urlopen(url)
...         r.code, r.msg
...     except urllib2.HTTPError, e:
...         e.code, e.msg
...     except urllib2.URLError, e: # サーバに接続できない場合に発生
...         e
...
URLError(gaierror(11001, 'getaddrinfo failed'),)
(404, 'Not Found')
(200, 'OK')

HTTPErrorはURLErrorのサブクラスです。また、HTTPErrorクラスの例外オブジェクトはレスポンスオブジェクトと同じように扱うことができます。

文字列のフォーマット

>>> mail = "hoge@hoge.com"
>>> password = "hogehoge"
>>> print "mail=%s&password=%s" % (mail, password)
mail=hoge@hoge.com&password=hogehoge
>>>
>>> year = 1989
>>> month = 11
>>> day = 25
>>> print "%d年%d月%d日" % (year, month, day)
19891125

文字列中の%変換指定(%sや%d)は引数で指定したタプルの要素で、順番に置き換えられます。書式によって右詰にしたり左詰にしたり、足りない部分を0で埋めたりといったことができます。詳しくはここを見るとよいです。


引数に辞書を渡すこともできますが、フォーマット文字列の書式が少し違います。

>>> query = {"mail":"hoge@hoge.com", "password":"hogehoge"}
>>> print "mail=%(mail)s&password=%(password)s" % query
mail=hoge@hoge.com&password=hogehoge
>>>
>>> date = {"year":1989, "month":11, "day":25}
>>> print "%(year)d年%(month)d月%(day)d日" % date
19891125


vars( )は現在参照できる変数名とその値を辞書にして返します。

>>> mail = "hoge@hoge.mail"
>>> password = "hogehoge"
>>> print "mail=%(mail)s&password=%(password)s" % vars()
mail=hoge@hoge.mail&password=hogehoge
>>>
>>> year = 1984
>>> year = 1989
>>> month = 11
>>> day = 25
>>> print "%(year)d年%(month)d月%(day)d日" % vars()
19891125

vars( )が返す辞書には必要ない変数も含まれます。当然、タプルを渡す場合と比べると遅いです。

from timeit import Timer

setup = """
mail = "hoge@hoge.com"
password = "hogehoge"
"""

stmt1 = "'mail=%s&password=%s' % (mail, password)"
stmt2 = "'mail=%(mail)s&password=%(password)s' % vars()"

print Timer(stmt1, setup).repeat(3, 100000)
print Timer(stmt2, setup).repeat(3, 100000)
[1.0013032400811277, 0.88645803650748412, 0.91006805343703379]
[2.3239804555892656, 2.3187373237902076, 2.3770830888885168]

思ったよりも差がありました。