1

私のねじれた方法でエラーを教えてください。私はかなり長い間、twisted を使用して高速な Web スクレーパーを構築するのに苦労してきました。Queue を使用して従来のスレッド化されたスクレーパーを構築するのは簡単なことであり、これまでのところ、非常に高速です。それでもツイストを比べたい!Webscraper の目的は、ギャラリーから画像 () リンクを再帰的に見つけ、それらの画像リンクに接続して画像 () をスクレイピングしたり、後で解析するためにさらに画像リンクを収集したりすることです。コードを以下に示します。ほとんどの関数は辞書を渡すので、各リンクからのすべての情報をより概念的にパケット化できます。私は、ブロックしているコード (parsePage 関数) をスレッド化して、html ページ、ヘッダー情報、および画像を取得するために「非同期コード」を使用しようとしています (またはそう信じています)。

これまでの私の主な問題は、getLinkHTML または getImgHeader errback からトレースバックされた大量の「ユーザータイムアウトにより接続エラーが発生しました」ということでした。セマフォを使用して作成する接続の数を調整しようとしましたが、接続が殺到していると考えて、コードの一部を無駄にスリープ状態にすることさえありました。また、スクレーパーを実行してから約 30 秒後にタイムアウト エラーが生成され、connectTCP には 30 秒のタイムアウトがあるため、問題は reactor.connectTCP から発生する可能性があると考えました。ただし、connectTCP コードをツイスト モジュールから 60 秒に変更しましたが、実行後約 30 秒でタイムアウト エラーが発生しました。もちろん、同じサイトを従来のスレッドスクレーパーでスクレイピングします。正常に動作し、はるかに高速に動作します。

それで、私は何を間違っていますか?また、私は独学であり、コード全体にもランダムな質問があるため、一般的なコードについて自由に批評してください。どんなアドバイスでも大歓迎です!

from twisted.internet import defer
from twisted.internet import reactor
from twisted.web import client
from lxml import html
from StringIO import StringIO
from os import path
import re

start_url = "http://www.thesupermodelsgallery.com/"
directory = "/home/z0e/Pictures/Pix/Twisted"
min_img_size = 100000

#maximum <a> links to get from main gallery
max_gallery_links = 500

#maximum <a> links to get from subsequent gallery/pages
max_picture_links = 35

def parsePage(info):
         
    def linkFilter(link):
    #filter unwanted <a> links
    if link is not None:
        trade_match = re.search(r'&trade=', link)
        href_split = link.split('=')
        for i in range(len(href_split)):
            if 'www' in href_split[i] and i > 0:
                link = href_split[i]
        end_pattern = r'\.(com|com/|net|net/|pro|pro/)$'
        end_match = re.search(end_pattern, link)
        p_pattern = r'(.*)&p'
        p_match = re.search(p_pattern, link)
        if end_match or trade_match:
            return None
        elif p_match:
            link = p_match.group(1)
            return link
        else:
            return link
    else:
        return None
        
    # better to handle a link with 'None' value through TypeError
    # exception or through if else statements?  Compare linkFilter
    # vs. imgFilter functions
        
    def imgFilter(link):
    #filter <img> links to retain only .jpg
    try:
        jpg_match = re.search(r'.jpg', link)
        if jpg_match is not None:
            return link
        else:
            return None
    except TypeError:
        return None
        
    link_num = 0
    gallery_flag = None
    info['level'] += 1
    if info['page'] is '':
    return None
    # use lxml to parse and get document root
    tree = html.parse(StringIO(info['page']))
    root = tree.getroot()
    root.make_links_absolute(info['url'])
    # info['level'] = 1 corresponds to first recursive layer (i.e. main gallery page)
    # info['level'] > 1 will be all other <a> links from main gallery page
    if info['level'] == 1:
    link_cap = max_gallery_links
    gallery_flag = True
    else:
    link_cap = max_picture_links
    gallery_flag = False
    if info['level'] > 4:
    return None
    else:
    
    # get <img> links if page is not main gallery ('gallery_flag = False')
    # put <img> links back into main event loop to extract header information
    # to judge pictures by picture size (i.e. content-length)
    if not gallery_flag:
        for elem in root.iter('img'):
            # create copy of info so that dictionary no longer points to 
            # previous dictionary, but new dictionary for each link
            info = info.copy()
            info['url'] = imgFilter(elem.get('src'))
            if info['url'] is not None:
                reactor.callFromThread(getImgHeader, info) 
                
    # get <a> link and put work back into main event loop (i.e. w/ 
    # reactor.callFromThread...) to getPage and then parse, continuing the
    # cycle of linking        
    for elem in root.iter('a'):
        if link_num > link_cap:
            break
        else:
            img = elem.find('img')
            if img is not None:
                link_num += 1
                info = info.copy()
                info['url'] = linkFilter(elem.get('href'))
                if info['url'] is not None:
                    reactor.callFromThread(getLinkHTML, info)
                    
def getLinkHTML(info):
    # get html from <a> link and then send page to be parsed in a thread
    d = client.getPage(info['url'])
    d.addCallback(parseThread, info)
    d.addErrback(failure, "getLink Failure: " + info['url'])
    
def parseThread(page, info):
    print 'parsethread:', info['url']
    info['page'] = page
    reactor.callInThread(parsePage, info)

def getImgHeader(info):
    # get <img> header information to filter images by image size
    agent = client.Agent(reactor)
    d = agent.request('HEAD', info['url'], None, None)
    d.addCallback(getImg, info)
    d.addErrback(failure, "getImgHeader Failure: " + info['url'])

def getImg(img_header, info):
    # download image only if image is above a certain threshold size
    img_size = img_header.headers.getRawHeaders('Content-Length')  
    if int(img_size[0]) > min_img_size and img_size is not None:
    img_name = ''.join(map(urlToName, info['url']))
    client.downloadPage(info['url'], path.join(directory, img_name))
    else:
    img_header, link = None, None #Does this help garbage collecting?
    
def urlToName(char):
    #convert all unwanted characters to '-' from url and use as file name
    if char in '/\?|<>"':
    return '-'
    else:
    return char
    
def failure(error, url):
    print error
    print url

def main():
    info = dict()
    info['url'] = start_url
    info['level'] = 0
    
    reactor.callWhenRunning(getLinkHTML, info)    
    reactor.suggestThreadPoolSize(2)
    reactor.run()
    
if __name__ == "__main__":
    main()
4

1 に答える 1

2

まず、このコードをまったく書かないことを検討してください。あなたのニーズに対する解決策として、scrapyを見てみましょう。人々はすでにパフォーマンスを向上させるための努力を行っており、改善が必要な場合は、改善するとコミュニティの全員が恩恵を受けます.

次に、残念なことに、コード リストのインデントがめちゃくちゃになっていて、コードが何をしているのかを実際に確認するのが難しくなっています。以下が理にかなっていることを願っていますが、コード リストを修正して、実行していることを正確に反映するようにし、今後の質問でコード リストを再確認するようにしてください。

あなたのコードが高速化を妨げている限り、ここにいくつかのアイデアがあります。

プログラム内の未処理の HTTP 要求の数に制限はありません。実際に解析している HTML がわからないので、これが実際に問題になるかどうかはわかりませんが、一度に 20 または 30 を超える HTTP 要求を発行することになると、ネットワークが過負荷になる可能性が非常に高くなります。TCP では、これは多くの場合、接続のセットアップが成功しないことを意味します (特定のセットアップ パケットが失われ、再試行の回数に制限があります)。多くの接続タイムアウト エラーについて言及されているため、これが発生していると思われます。

スレッド バージョンのプログラムが一度に発行する HTTP 要求の数を考慮してください。Twisted バージョンは潜在的により多く発行しますか? もしそうなら、これに制限を課してみてください。この制限を課す簡単な方法は次のようなものかもしれません (ただし、これは最善のtwisted.internet.defer.DeferredSemaphore方法とはほど遠いため、それが役立つ場合は、この制限を課すためのより良い方法を検討することをお勧めします。ただし、制限が役に立たない場合は意味がありません)。より優れた制限メカニズムに多くの労力を投資します)。

次に、reactor スレッドプールを最大 2 つのスレッドに制限することで、名前を解決する能力が大幅に妨げられます。デフォルトでは、名前解決 (すなわち DNS) は、リアクター スレッド プールを使用して行われます。ここにはいくつかのオプションがあります。解析を2つの同時スレッドに制限したいという正当な理由があると思います。

まず、リアクター スレッドプールをそのままにして、解析用に独自のスレッド プールを作成できます。を参照してくださいtwisted.python.threads.ThreadPool。この他のスレッド プールの最大値を 2 に設定して、必要な解析動作を得ることができます。リアクタは、名前解決に必要な数のスレッドを自由に使用できます。

次に、reactor のスレッド プール サイズを引き続き小さくし、reactor が名前解決にスレッドを使用しないように構成することもできます。 twisted.names.client.createResolverはまさにそれを行う名前リゾルバを提供しreactor.installResolver、リアクタにデフォルトの代わりにそれを使用するように指示できます。

于 2012-12-10T13:00:56.997 に答える