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
>>>