【问题标题】:What am I doing wrong when building this twisted web scraper?构建这个扭曲的网络爬虫时我做错了什么?
【发布时间】:2012-11-27 11:26:38
【问题描述】:

你能以我扭曲的方式告诉我错误吗?很长一段时间以来,我一直在努力构建一个快速的网络爬虫。使用 Queue 构建一个传统的线程 scraper 是小菜一碟,到目前为止,速度更快。不过,我想比较扭曲! webscraper 的目标是递归地从图库中找到图像 () 链接,并连接到这些图像链接以抓取图像 () 和/或收集更多图像链接以供以后解析。代码如下所示。大多数函数都传递一个字典,因此我可以从概念上更深入地打包来自每个链接的所有信息。我尝试线程化否则会阻塞代码(parsePage 函数)并使用“异步代码”(或者我相信)来检索 html 页面、标题信息和图像。

到目前为止,我的主要问题是从我的 getLinkHTML 或 getImgHeader errback 中追踪到大量“用户超时导致连接失败”。我尝试过使用信号量来限制我建立的连接数量,甚至导致我的一些代码休眠无济于事,认为我正在淹没连接。我还认为问题可能来自 reactor.connectTCP,因为在运行 scraper 大约 30 秒后会产生超时错误,并且 connectTCP 有 30 秒的超时。但是,我将twisted模块的connectTCP代码修改为60s,运行后大约30秒仍然出现超时错误。当然,使用我的传统线程 scraperscraping 相同的网站可以正常工作,而且速度要快得多。

那么我做错了什么?此外,由于我是自学成才的,因此请随时对我的代码提出批评,并且我在整个代码中也有一些随机问题。任何建议都非常感谢!

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

【问题讨论】:

    标签: python web-scraping twisted


    【解决方案1】:

    首先,考虑根本不编写此代码。看看scrapy 作为满足您需求的解决方案。人们已经努力让它表现良好,如果确实需要改进,那么当你改进它时,社区中的每个人都会受益。

    接下来,不幸的是,您的代码清单中的缩进搞砸了,很难真正看到您的代码在做什么。希望以下内容有意义,但您应该尝试更正代码清单,使其准确反映您正在做的事情,并确保在以后的问题中仔细检查代码清单。

    就您的代码正在执行的操作导致其无法快速运行而言,这里有一些想法。

    程序中未完成的 HTTP 请求的数量没有限制。在不知道您实际解析的是什么 HTML 的情况下,我不知道这是否真的是一个问题,但如果您最终一次发出超过 20 或 30 个 HTTP 请求,您的网络很可能会过载。使用 TCP,这通常意味着连接设置不会成功(某些设置数据包会丢失,并且对它们的重试次数有限制)。由于您提到了很多连接超时错误,我怀疑这种情况正在发生。

    考虑程序的线程版本一次将发出多少个 HTTP 请求。 Twisted 版本是否可能发行更多?如果是这样,请尝试对此施加限制。 twisted.internet.defer.DeferredSemaphore 之类的东西可能是施加此限制的一种简单方法(尽管它远非 best 方式,因此如果它有帮助,那么您可能想开始寻找更好的方法来施加此限制 - 但是如果限制没有帮助,那么在更好的限制机制上投入大量精力是没有意义的)。

    接下来,通过将反应器线程池限制为最多 2 个线程,您将严重妨碍解析名称的能力。默认情况下,名称解析(即 DNS)是使用反应器线程池完成的。你有几个选择。我假设您有充分的理由希望将解析限制为两个并发线程。

    首先,您可以不理会反应器线程池,而是创建自己的线程池进行解析。见twisted.python.threads.ThreadPool。您可以将此其他线程池的最大值设置为 2 以获得您想要的解析行为,并且反应器可以随意使用任意数量的线程来进行名称解析。

    其次,您可以继续降低反应器线程池的大小,并将反应器配置为不使用线程进行名称解析。 twisted.names.client.createResolver 会给你一个名称解析器来做这件事,reactor.installResolver 让你告诉反应器使用它而不是它的默认值。

    【讨论】:

    • 感谢让-保罗的回复。我知道scrapy,但想用这个scrapy项目作为学习twisted以及与其他技术进行比较的一种方式。我对你所指的身份有点困惑。能给我举个例子吗?由于 python 使用标识作为定义代码“块”的一种方式,而不是像 C 中的括号,所以我不确定如何执行您的建议。关于网络过载,我认为情况并非如此,至少以任何明显的方式。
    • 另外,我知道连接可以处理的请求数量超过我提供的请求数量,因为我成功地使用传统的线程抓取器通过数百个线程一次运行数百个请求。有趣的是,主反应器循环本身是线程化的(?)。我一直认为这是一个单线程,并且通过调用'reactor.suggestThreadPool()'我正在修改反应器循环之外的线程数量,或者是扭曲本身在反应器循环之外生成额外的线程来服务DNS请求,因此制作“反应堆线程池”?
    • 通过使用 reactor.callInThread() 我打算在主反应器循环线程之外创建一个线程,而使用 reactor.callFromThread(),我希望将我的代码执行插入回主反应器循环/线程。这是正确的理解吗?我确实尝试将SuggestThreadPool扩展到〜30但没有任何成功:/。希望我在这里做错了一些明显的事情,但听起来我的代码太复杂而无法简要理解。我将尝试制作它的简化版本,同时保持主要的扭曲调用完好无损。再次感谢您的帮助!
    • 对于缩进,您列表中的代码不是合法的 Python,因为它错误地使用了缩进。我预计这是 stackoverflow 标记以及如何将代码复制/粘贴到浏览器中的问题。例如,parsePage 函数的第 1 行和第 2 行代码具有相同的缩进级别,即使第 1 行是函数定义并且需要以下行具有更大的缩进。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-09-11
    • 2021-05-26
    • 2010-12-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多