クラスを定義する
>>> 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__( )はインスタンス生成直後に呼び出される
- インスタンス生成時の引数が__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) 1989年11月25日
文字列中の%変換指定(%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 1989年11月25日
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() 1989年11月25日
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]
思ったよりも差がありました。