URLイテレータ
指定されたホストに含まれる、そのホスト内ファイルへのリンクを抽出するイテレータクラスを書きました。
URLの探索はキューを使った幅優先探索で、必要に応じて探索ページをフェッチして、URLを抽出してバッファリングします。よいところは、実際に探索ページのテキストが必要になるまでフェッチを遅延するところ。ホスト内のページが多い場合、抽出したURLをリストで返すと巨大なリストが作成され、メモリを圧迫し、時間がかかります。今回の実装では、nextメソッドを持つiteratableなオブジェクトとして実装しで必要な分だけURLを取得するので、メモリと時間を節約できます。
このクラスはiteratableなので、当然、forステートメントを使って全ての要素に対して操作をすることができます。下のように
iterator = UrlIterator("localhost") for url in iterator: print url
今回定義したクラスは、この様な操作によって、ページのフェッチが短時間に頻繁に起こらないよう、前回のフェッチから4秒以上経過していない場合は、それまで待つ仕様です。
デバッグが不十分かも。
#! /bin/env python # -*- coding: utf-8 -*- # UrlIterator.py import sys import re import urllib import time from Queue import Queue from urlparse import urlparse class UrlIterator: # 幅優先探索で指定されたホストに含まれるURLを返すイテレータ urlPattern = re.compile(r"http://[-_.!~*'()a-zA-Z0-9;/?:@&=+$,%#]+") # 参考URL: http://www.din.or.jp/~ohzaki/perl.htm#httpURL aHrefPattern = re.compile(r""".*?<a href="([^"]+?)">.*?</a>.*""") waitTime = 4.0 # ページのフェッチあたりに待機する秒数 def __init__(self, host): self.host = host self.q = Queue() home = "http://"+host+"/" self.q.put(home) self.visited = [home] self.urlBuffer = [] self.preGet = 0 def __str__(self): return "<UrlIterator host:%s>" % self.host def wait_by_need(self): # 必要に応じて待つ if time.time() - self.preGet < UrlIterator.waitTime: t = UrlIterator.waitTime - (time.time() - self.preGet) print "[UrlIterator]: wait %f second(s)" % t time.sleep(t) def filter_url(self, reqUrl): # ページをフェッチして正規表現でURLを抽出して返す # ページのフェッチ try: res = urllib.urlopen(reqUrl).read() except Exception, e: raise e # 正規表現でurlをフィルター lst = UrlIterator.urlPattern.findall(res) hrefs = UrlIterator.aHrefPattern.findall(res) # 正規化っぽいこと for link in hrefs: if not re.match("^http:.*", link) is None: # http:// ..で始まるlink lst.append(link) elif not re.match("^/.*", link) is None: # /.. で始まる link lst.append("http://" + self.host + link) elif link[0:2] == "./": lst.append(reqUrl + "/" + link[2:]) elif link[0:3] == "../": s = reqUrl[:reqUrl.rfind("/")].rfind("/") x = reqUrl[:s] + "/" + link[3:] lst.append(x) else: lst.append(reqUrl[:reqUrl.rfind("/")] + "/" + link) return lst def fillUrl(self): reqUrl = self.q.get() print "Search",reqUrl # debug self.wait_by_need() # ファイルに含まれるurlを抽出 lst = self.filter_url(reqUrl) # 落とすurlの条件 # - 異なるホストのurl # - 既に訪ねたurl # - 重複したurl .. 未実装 h = self.host v = self.visited lst = [url for url in lst if urlparse(url)[1]==h and (not url in v)] #print "added",lst # debug self.urlBuffer = lst self.visited += lst #print "urlBuffer:",self.urlBuffer for url in lst: path = urlparse(url)[2] ext = path[path.rfind("."):] if ext == ".html" or ext == ".php" or ext == path[-1]: # htmlかphpか拡張子無しなら探索キューに追加 #print "push -> ",url # debug self.q.put(url) self.preGet = time.time() def __iter__(self): return self def next(self): if len(self.urlBuffer) == 0: if self.q.empty(): raise StopIteration else: while len(self.urlBuffer) == 0 and not self.q.empty(): self.fillUrl() if len(self.urlBuffer) == 0: raise StopIteration url = self.urlBuffer[0] del(self.urlBuffer[0]) return url def listUrl(hostname, n=-1): # hostnameで指定されたホストに含まれるhostnameのホスト内のURLを抽出して表示する iterator = UrlIterator(hostname) if n==-1: for url in iterator: print url else: for idx in xrange(n): try: print iterator.next() except StopIteration: return idx def main(): if sys.argv.__len__() != 2: print "usage: %s host" % __file__ sys.exit() host = sys.argv[1] #listUrl(host) print listUrl(host,200) if __name__ == '__main__': main()
実行例)はてなのトップからはじめの20個のリンクを取得する
$ python UrlIterator.py d.hatena.ne.jp Search http://d.hatena.ne.jp/ http://d.hatena.ne.jp/mobile http://d.hatena.ne.jp/hatenadiary/20080828/1219925223 http://d.hatena.ne.jp/guri_2/20080830/1220076569 http://d.hatena.ne.jp/guri_2/ http://d.hatena.ne.jp/tikani_nemuru_M/20080830/1220032774 http://d.hatena.ne.jp/tikani_nemuru_M/ http://d.hatena.ne.jp/elegantlycruel/20080830/1220073618 http://d.hatena.ne.jp/elegantlycruel/ http://d.hatena.ne.jp/aureliano/20080828/1219913596 http://d.hatena.ne.jp/aureliano/ http://d.hatena.ne.jp/komachimania/20080830 http://d.hatena.ne.jp/komachimania/ http://d.hatena.ne.jp/elegantlycruel/20080829/1219986023 http://d.hatena.ne.jp/elegantlycruel/ http://d.hatena.ne.jp/heartless00/20080829/1219998324 http://d.hatena.ne.jp/heartless00/ http://d.hatena.ne.jp/soorce/20080830#p1 http://d.hatena.ne.jp/soorce/ http://d.hatena.ne.jp/crow_henmi/20080830#1220100510 http://d.hatena.ne.jp/crow_henmi/
実行例)はてなのトップからはじめの200個のリンクを取得する
$ python Python 2.5.1 (r251:54863, May 18 2007, 16:56:43) [GCC 3.4.4 (cygming special, gdc 0.12, using dmd 0.125)] on cygwin Type "help", "copyright", "credits" or "license" for more information. >>> from UrlIterator import UrlIterator >>> iterator = UrlIterator("d.hatena.ne.jp") >>> for i in range(200): ... print iterator.next() ... Search http://d.hatena.ne.jp/ http://d.hatena.ne.jp/mobile (略) http://d.hatena.ne.jp/hatenadiary/ Search http://d.hatena.ne.jp/mobile [UrlIterator]: wait 3.996000 second(s) http://d.hatena.ne.jp/images/no_profile_icon.gif (略) http://d.hatena.ne.jp/keywordlistmobile Search http://d.hatena.ne.jp/hatenadiary/20080828/1219925223 [UrlIterator]: wait 3.998000 second(s) http://d.hatena.ne.jp/hatenadiary/rss (略) http://d.hatena.ne.jp/sa_to_e/20080828/1219924649 >>>