wgetでYouTube等から動画を落とす
Rubyに戻ってみました。selfを書かなくて済むのはいいんですが、Pythonに慣れてきたせいか、endを書くのが少し面倒になってます。後、インスタンスごとにメソッドの振る舞いを変えられるのはいいです。新しくクラスを作らなくて済みますし。
#!ruby require 'net/http' require 'cgi' require 'kconv' save_dir = 'c:/My Documents' interval = 3 module Video class Error < StandardError; end class InvalidUrlError < Error; end VideoDetail = Struct.new(:id, :dl_url, :title, :ext, :encoding) def get_detail(url) @@videos.each do |video| begin return video.get_detail(url) rescue InvalidUrlError raise if video === @@videos.last end end end module_function :get_detail class Video def initialize(url_re, dl_url_re, title_re=nil, api_url=nil, ext=".flv", encoding=Kconv::UTF8) @url_re = url_re @dl_url_re = dl_url_re @title_re = title_re @api_url = api_url @ext = ext @encoding = encoding end def get_detail(url) id = extract_video_id(url) if @api_url video_url = @api_url % id else video_url = url end content = get_content(video_url) params = extract_dl_url_params(content) dl_url = build_dl_url(params) title = extract_title(content) VideoDetail.new(id, dl_url, title, @ext, @encoding) end private def extract_video_id(url) @url_re.match(url).to_a[1] or raise InvalidUrlError.new('invalid video url') end def get_content(url) begin res = Net::HTTP.get_response(URI.parse(url)) raise unless Net::HTTPSuccess === res res.body rescue raise Error.new('unable to download video page') end end def extract_dl_url_params(content) @dl_url_re.match(content).to_a[1..-1] or raise Error.new('unable to extract download url params') end def build_dl_url(params) params.first end def extract_title(content) @title_re.match(content).to_a[1] if @title_re end end @@videos = [] youtube = Video.new( %r!\Ahttp://(?:\w+\.)youtube\.com/watch\?v=([\w-]+)!, %r!watch_fullscreen\?.*?video_id=([\w-]+).*?&t=([\w-]+)!, %r!<title>YouTube - ([^<>]*)</title>! ) def youtube.build_dl_url(params) "http://www.youtube.com/get_video?video_id=%s&t=%s" % params end @@videos << youtube veoh = Video.new( %r!\Ahttp://www\.veoh\.com/videos/(\w+)!, %r!fullPreviewHashPath="([^"]+)"!, %r!title="([^"]*)"\s+dateAdded=!, 'http://www.veoh.com/rest/video/%s/details' ) @@videos << veoh dailymotion = Video.new( %r!http://www.dailymotion\.com/.*?/video/([\w/-]+)!, %r!(http%3A%2F%2Fwww\.dailymotion\.com%2Fget%2F\d{2}%2F320x240%2Fflv%2F\d+\.flv%3Fkey%3D\w+)!, %r!<h1 class="nav with_uptitle">([^<>]*)</h1>! ) def dailymotion.build_dl_url(params) CGI.unescape(params.first) end @@videos << dailymotion amebavision = Video.new( %r!http://vision\.ameba\.jp/watch\.do.*?\?movie=(\d+)!, %r!<imageUrlLarge>([^<>]+)</imageUrlLarge>!, %r!<item>\s*<title>([^<>]*)</title>!, "http://vision.ameba.jp/api/get/detailMovie.do?movie=%s" ) def amebavision.build_dl_url(params) flv_url = params.first flv_url['//vi'] = '//vm' flv_url['/jpg/'] = '/flv/' flv_url['_4.jpg'] = '.flv' flv_url end @@videos << amebavision yourfilehost = Video.new( %r!http://(?:www\.)?yourfilehost\.com/media\.php\?cat=video&file=([\w.-]+)\.!, %r!videoembed_id=([\w%.-]+)&! ) def yourfilehost.build_dl_url(params) CGI.unescape(params.first) end @@videos << yourfilehost end ARGV.each do |url| begin video = Video.get_detail(url) rescue => e puts "Error: #{e} (#{url})" next end if video.title filename = video.title.kconv(Kconv::SJIS, video.encoding) else filename = video.id end filepath = File.join(save_dir, filename << video.ext) system('wget', '-O', filepath, "--referer=#{url}", video.dl_url) sleep interval end
wgetでYouTubeから動画を落とす
wgetでYouTube、Veoh、Dailymotion、AmebaVision、YourfilehostからFLVファイルを落とします。ただし、ログインしないと見れない動画は落とせません。
#!python #encoding=utf-8 import os import sys import re import urllib import urllib2 import time save_dir = r"C:\My Documents" interval = 3 class Video: video_url_re = None api_url_format = None flv_url_re = None title_re = None server_encoding = "utf-8" def __init__(self, video_url): self.id = self._extract_id(video_url) if self.api_url_format: api_url = self.api_url_format % self.id else: api_url = video_url content = self._get_content(api_url) self.flv_url = self._extract_flv_url(content) self.title = self._extract_title(content) def _extract_id(self, video_url): match = self.video_url_re.match(video_url) if match: return match.group(1) else: raise ValueError("invalid video url") def _get_content(self, url): try: response = urllib2.urlopen(url) return response.read() except urllib2.URLError: raise RuntimeError("unable to download video page") def _extract_flv_url(self, content): match = self.flv_url_re.search(content) if match is None: raise RuntimeError("unable to extract flv url") return self._build_flv_url(match) def _build_flv_url(self, match): return match.group(1) def _extract_title(self, content): if self.title_re: match = self.title_re.search(content) if match: return match.group(1).decode(self.server_encoding, "ignore") class YouTube(Video): video_url_re = re.compile(r"http://(?:\w+\.)?youtube\.com/watch\?v=([\w-]+)") flv_url_re = re.compile(r"watch_fullscreen\?.*?video_id=([^&]+)&.*?t=([^&]+)&") title_re = re.compile(r"<title>YouTube - ([^<>]*)</title>") def _build_flv_url(self, match): return "http://www.youtube.com/get_video?video_id=%s&t=%s" % match.group(1,2) class Veoh(Video): video_url_re = re.compile(r"http://www\.veoh\.com/videos/(\w+)") api_url_format = "http://www.veoh.com/rest/video/%s/details" flv_url_re = re.compile(r'fullPreviewHashPath="([^"]+)"') title_re = re.compile(r'title="([^"]*)"\s+dateAdded=') class Dailymotion(Video): video_url_re = re.compile(r"http://www.dailymotion\.com/.*?/video/([\w/-]+)") flv_url_re = re.compile("http%3A%2F%2F\w+\.dailymotion\.com%2Fget%2F\d{2}%2F320x240%2Fflv%2F\d+\.flv%3Fkey%3D[a-z0-9]+") title_re = re.compile(r'<h1 class="nav with_uptitle">([^<>]*)</h1>') def _build_flv_url(self, match): return urllib.unquote(match.group(0)) class AmevaVision(Video): video_url_re = re.compile(r"http://vision\.ameba\.jp/watch\.do.*?\?movie=(\d+)") api_url_format = "http://vision.ameba.jp/api/get/detailMovie.do?movie=%s" flv_url_re = re.compile(r"<imageUrlLarge>([^<>]+)</imageUrlLarge>") title_re = re.compile(r"<item>\s*<title>([^<>]*)</title>") def _build_flv_url(self, match): flv_url = match.group(1).replace("//vi", "//vm") flv_url = flv_url.replace("/jpg/", "/flv/") flv_url = flv_url.replace("_4.jpg", ".flv") return flv_url class Yourfilehost(Video): video_url_re = re.compile(r"http://(?:www\.)?yourfilehost\.com/media\.php\?cat=video&file=([\w.-]+)\.") flv_url_re =re.compile(r"videoembed_id=([\w%.-]+)&") def _build_flv_url(self, match): return urllib.unquote(match.group(1)) klass = [YouTube, Veoh, Dailymotion, AmevaVision, Yourfilehost] def get_video(url): for k in klass: try: return k(url) except ValueError: if k == klass[-1]: raise invalid_chr_re = re.compile(u'[\/:*?"<>|]') for url in sys.argv[1:]: try: video = get_video(url) filename = (video.title or video.id) + ".flv" filename = invalid_chr_re.sub(" ", filename) filepath = os.path.join(save_dir, filename) command = "wget -O '%s' --referer='%s' '%s'" % (filepath, url, video.flv_url) os.system(command) time.sleep(interval) except (ValueError, RuntimeError), e: print "Error: %s [%s]" % (e, url)
テキストの編集
文字列の挿入
a | カーソル位置の後に文字列を追加 |
A | 現在行の末尾に文字列を追加 |
i | カーソル位置の前に文字列を追加 |
I | 現在行の最初の非空白文字の前に文字列を追加 |
o | 現在行の下に文字列を追加 |
O | 現在行を下に1行ずらし、空いた行に文字列を追加 |
文字列の削除
N x | カーソル位置とその後ろのN文字を削除 |
N X | カーソル位置の前のN文字を削除 |
N dd | N行削除 |
N D | カーソル位置からN-1行下の行末までを削除 |
文字列のコピーと貼り付け
N yy | N行をコピー |
N p | カーソル位置の後にN回貼り付け |
N P | カーソル位置の前にN回貼り付け |
N ]p | インデントを現在行に合わせ、カーソル位置の後に貼り付け |
N [p | インデントを現在行に合わせ、カーソル位置の前に貼り付け |
文字列の変更
N r ? | N文字を?に変更 |
R | |
N cc | N行を変更 |
N S | N行を変更 |
N s | N文字を変更 |
N ~ | N文字を大文字/小文字を入れ替える |
N g~~ | N行の大文字/小文字を入れ替える |
N guu | N行を小文字にする |
N gUU | N行を大文字にする |
その他
N . | 直前のコマンドをN回繰り返す |
N u | 最後のN回分の処理を取り消す(アンドゥ) |
N Ctrl-R | 最後のN回分のアンドゥを取り消す |
N >> | N行分インデントを追加 |
N << | N行分インデントを解除 |
N J | 現在行を含め、N行を空白文字で連結 |
N gJ | 現在行を含め、N行を空白文字を付けずに連結 |
演算コマンドと移動コマンド/テキストオブジェクト
d | 削除 |
c | 変更 |
y | コピー |
g~ | 大文字/小文字を入れ替える |
gu | 小文字にする |
gU | 大文字にする |
これらの演算コマンドは、移動コマンドやテキストオブジェクトと組み合わせて使うことができます。以下のコマンドのdの部分をyで置き換えればコピーに、cで置き換えれば変更になります。
N dl | N文字を削除 |
N dd | N行を削除 |
N d $ | カーソル位置からN-1行下の行末までを削除 |
d 0 | カーソル位置から行頭までを削除 |
d ^ | カーソル位置から先頭の空白までを削除 |
N dgg | 現在行からN行目までを削除 |
N dG | 現在行からN行目までを削除(デフォルトは最後の行まで) |
N dw dW | N個目の単語とその後ろの空白までを削除 |
N de dE | N個目の単語の末尾までを削除 |
N df? | カーソル位置から右側にあるN番目の?のまでを削除 |
N dt? | カーソル位置から右側にあるN番目の?の1文字前までを削除 |
diw | カーソル上にある単語を削除 |
daw | カーソル上にある単語とその後ろの空白を削除 |
daw | カーソル上にある単語とその後ろの空白文字を削除 |
dib di( | ( )の中の文字列削除 |
dab da( | ( )とその中の文字列を削除 |
diB di{ | { }の中の文字列を削除 |
daB da{ | { }とその中の文字列を削除 |
di[ | [ ]の中の文字列を削除 |
da[ | [ ]とその中の文字列を削除 |
di" di' | 引用符の中の文字列を削除 |
da" di' | 引用符とその中の文字列を削除 |
カーソルの移動
上下左右にN桁移動
N l | N桁右に移動 |
N h | N桁左に移動 |
N k | N桁上に移動 |
N j | N桁下に移動 |
N gk | 見た目の行でN桁上に移動 |
N gj | 見た目の行でN桁下に移動 |
行を折り返す設定にしている場合、長い行は以下のように表示されます。
5 XXX Perhaps there should be a slimmed version that doesn't contain all those backwards compatible?
これは本当は1行ですが、見た目では2行です。
行頭・行末への移動
N $ | 現在位置からN-1行下の行末に移動 |
0 | 行の先頭に移動 |
^ | 行の先頭の非空白文字に移動 |
N g$ | 見た目の行で現在位置からN-1行下の行末に移動 |
N g0 | 見た目の行の先頭に移動 |
N g^ | 見た目の行の先頭の非空白文字に移動 |
行単位での移動
N gg | N行目に移動(デフォルトは1行目) |
N G | N行目に移動(デフォルトは最後の行) |
N % | ファイルのN%の位置に移動(Nは省略不可) |
H | 画面上の最初の行に移動 |
M | 画面上の真中の行に移動 |
L | 画面上の最後の行に移動 |
単語単位での移動
N w | N個先の単語の先頭に移動 |
N b | N個前の単語の先頭に移動(カーソル位置が単語の先頭でなければn-1個前) |
N e | N個先の単語の末尾に移動(カーソル位置が単語の末尾でなければn-1個先) |
N ge | N個前の単語の末尾に移動 |
w 2w 2b b 2b --->---------------> <----------<--<-------- URL encoded queries should be treated as blank strings --->------->---------> <--------<----- e e 2e 2ge ge
大文字にした場合は、空白で区切られたものを単語として扱います。
ge w e <- ---> ---> This is-a line, with special/separated/words (and some more). <----- ---------------------> -----> gE W E
これらのコマンドは行をまたいで移動します。
特定の文字への移動
N f ? | 右方向にあるN個目の?に移動 |
N t ? | 右方向にあるN個目の?の左側に移動 |
N F ? | 左方向にあるN個目の?に移動 |
N T ? | 左方向にあるN個目の?の右側に移動 |
N ; | 直前のf,t,F,TをN回繰り返す |
N , | 直前のf,t,F,Tを逆方向にN回繰り返す |
2fe ; 3; ----->----------------->-----------------> 3fo 2Ft ---------------------> <-------------- The method may not store state in the Codec instance. --------------------> <------------- 3to 2Tt
行をまたいでの移動はできません。また、3foの後の3;は3foを3回繰り返すのではなく、foを3回繰り返します。
対応する括弧への移動
%を使います。
--------------------------------> len(tuple([(1, 2), (3, 4), (5, 6)])) <-----------------------
スクロール
N Ctrl-E | 下にN行スクロール(デフォルトは1) |
N Ctrl-D | 下にN行スクロール(デフォルトは画面の半分) |
N Ctrl-F | 下にNページ分スクロール |
N Ctrl-Y | 上にN行スクロール(デフォルトは1) |
N Ctrl-U | 上にN行スクロール(デフォルトは画面の半分) |
N Ctrl-B | 上にNページ文スクロール |
zz | カーソルが画面の真ん中に来るようにする |
ニコニコから動画を落とすスクリプト改
ニコニコ動画ダウンローダ - odz bufferを参考にして、以前作ったニコニコから動画を落とすスクリプトを少しばかり修正しました。
- デフォルトでCookieを"nico-cookie.txt"から読み込む
- -cオプションでブラウザ(Mozilla系)のCookieファイルを指定
- ログイン処理を行わないので、ブラウザの方でログインしてから使用
- スクリプト使用後もブラウザの方のログイン状態を維持
- プログレスバーにhttp://pypi.python.org/pypi/progressbarを使用
関数を使ってまとめたので、前よりは見やすくなったと思います。複数の動画を落とせるようにしようと思ったのですが、ブラウザからアクセスした時に「短時間での連続アクセスはご遠慮ください」と言われたので止めておきました。後、22時から2時ぐらいまでダウンロードの制限がかかっているようです。FLVファイルを落とす際にhttplib.BadStatusLineという例外が発生するので調べてみたら、それが原因でした。
#!python # encoding=utf-8 import os import sys import re import urllib import urllib2 import cookielib import httplib import cgi from optparse import OptionParser from progressbar import * # 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>') # Constants for progress bar PBAR_WIDGETS = [Percentage(), " ", Bar(marker="=",left="[",right="]"), " ", ETA(), " ", FileTransferSpeed()] COOKIE_FILE = "nico-cookie.txt" class LoginError(Exception): pass def login(mail, password): for i in range(2): if mail is None: mail = raw_input("mail: ") if password is None: password = raw_input("pass: ") query = {"mail":mail, "password":password} query = urllib.urlencode(query) try: response = urllib2.urlopen(LOGIN_URL, query) if response.headers["x-niconico-authflag"] == "1": return True else: mail = password = None except urllib2.URLError: pass return False def get_flv_url(video_id): response = urllib2.urlopen(API_URL + video_id) try: content = response.read() flv_url = cgi.parse_qs(content)["url"][0] return flv_url except KeyError: if content == "closed=1&done=true": raise LoginError else: raise ValueError def get_video_title(video_url): response = urllib2.urlopen(video_url) match = VIDEO_TITLE_RE.search(response.read()) if match: video_title = match.group(1).decode("utf-8", "ignore") video_title = re.compile(u'[\/:*?"<>|]').sub(" ", video_title) return video_title def download(flv_url, filepath): try: file = None response = urllib2.urlopen(flv_url) try: file = open(filepath, "wb") except IOError: raise RuntimeError("unable to open the file for writing") total = response.headers["content-length"] downloaded = 0 pbar = ProgressBar(widgets=PBAR_WIDGETS, maxval=int(total)).start() while True: data = response.read(4096) if not data: break file.write(data) downloaded += len(data) pbar.update(downloaded) pbar.finish() except (urllib2.URLError, IOError): raise RuntimeError("unable to download video data") finally: if file: file.close() # Create the command line options parser and parse command line usage = "usage: %prog [options] http://www.nicovideo.jp/watch/{video_id}" 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 cookies from FILE of the Mozilla/Netscape 'cookie.txt' format"), parser.add_option("-d", dest="save_dir", metavar="PREFIX", help="save a file to PREFIX/ (default current directory)") parser.add_option("-n", dest="no_title", action="store_true", help="save the file under the name {ID}.flv (default {title}.flv)") (options, args) = parser.parse_args() # Check arguments 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 a directory" % options.save_dir) # Configure urllib2 to use cookies cj = cookielib.MozillaCookieJar() try: cj.load(options.cookie_file or COOKIE_FILE) except: pass opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj)) urllib2.install_opener(opener) # Prepare downloading video try: try: flv_url = get_flv_url(video_id) except ValueError, e: sys.exit("Error: invalid video ID") except LoginError: # Exit if use the browser's cookies file if options.cookie_file: sys.exit("Error: not logged in") # Login and retry if login(options.mail, options.password): cj.save(COOKIE_FILE) flv_url = get_flv_url(video_id) else: sys.exit("Error: unable to login") video_title = get_video_title(video_url) except urllib2.URLError, e: sys.exit("Error: unable to prepare downloading a video") # Download video filename = video_title or video_id if options.no_title: filename = video_id filepath = os.path.join(options.save_dir or ".", filename + ".flv") print video_title try: download(flv_url, filepath) except (IOError, RuntimeError), e: sys.exit("Error: %s" % e) except httplib.BadStatusLine: sys.exit("Error: download seems to be limited")