使用Python和Scrapy进行递归爬取

12

我正在使用Scrapy爬取一个网站。该网站每页有15个列表,并带有下一页按钮。我遇到的问题是,在管道中解析完所有列表之前,我的下一个链接请求就被调用了。以下是我的爬虫代码:

class MySpider(CrawlSpider):
    name = 'mysite.com'
    allowed_domains = ['mysite.com']
    start_url = 'http://www.mysite.com/'

    def start_requests(self):
        return [Request(self.start_url, callback=self.parse_listings)]

    def parse_listings(self, response):
        hxs = HtmlXPathSelector(response)
        listings = hxs.select('...')

        for listing in listings:
            il = MySiteLoader(selector=listing)
            il.add_xpath('Title', '...')
            il.add_xpath('Link', '...')

            item = il.load_item()
            listing_url = listing.select('...').extract()

            if listing_url:
                yield Request(urlparse.urljoin(response.url, listing_url[0]),
                              meta={'item': item},
                              callback=self.parse_listing_details)

        next_page_url = hxs.select('descendant::div[@id="pagination"]/'
                                   'div[@class="next-link"]/a/@href').extract()
        if next_page_url:
            yield Request(urlparse.urljoin(response.url, next_page_url[0]),
                          callback=self.parse_listings)


    def parse_listing_details(self, response):
        hxs = HtmlXPathSelector(response)
        item = response.request.meta['item']
        details = hxs.select('...')
        il = MySiteLoader(selector=details, item=item)

        il.add_xpath('Posted_on_Date', '...')
        il.add_xpath('Description', '...')
        return il.load_item()

这几行代码有问题。就像我之前说过的,它们在爬虫完成当前页面之前被执行了。在网站的每个页面上,这会导致只有15个列表中的3个被发送到管道。

     if next_page_url:
            yield Request(urlparse.urljoin(response.url, next_page_url[0]),
                          callback=self.parse_listings)

这是我的第一个爬虫,可能是我设计上的缺陷,有更好的方法吗?


1
嗨。你的代码运行了吗?我想让蜘蛛爬到下一页,但似乎找不到任何关于它的教程。你的工作代码可能会有用。谢谢! - Victor
没有。我甚至联系了Scrapy的创建者,但他们没有提供帮助。 - imns
我刚用不同的关键词进行了搜索,找到了这个链接:http://abuhijleh.net/2011/02/13/guide-scrape-multi-pages-content-with-scrapy/ 希望对你有所帮助。我还没有写过自己的爬虫。如果我有任何进展,我会发布一些内容。 - Victor
2
@Victor,字体很好看。给你加个赞。 - KJW
7个回答

4
用Scrape(爬取)而不是Spider(蜘蛛)?
由于你的问题需要重复浏览一系列内容,采用 mechanize(http://wwwsearch.sourceforge.net/mechanize/)和beautifulsoup(http://www.crummy.com/software/BeautifulSoup/)而非一个未知大小的内容树。
下面是使用 mechanize 实例化一个浏览器的示例。同时,使用 br.follow_link(text="foo") 意味着与你示例中的xpath不同,连接将仍然被跟踪,无论祖先路径元素的结构如何。这意味着,如果他们更新他们的HTML,你的脚本会崩溃。一个更松散的耦合将为您节省一些维护工作。 下面是一个示例:
br = mechanize.Browser()
br.set_handle_equiv(True)
br.set_handle_redirect(True)
br.set_handle_referer(True)
br.set_handle_robots(False)
br.addheaders = [('User-agent', 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:9.0.1)Gecko/20100101 Firefox/9.0.1')]
br.addheaders = [('Accept-Language','en-US')]
br.addheaders = [('Accept-Encoding','gzip, deflate')]
cj = cookielib.LWPCookieJar()
br.set_cookiejar(cj)
br.open("http://amazon.com")
br.follow_link(text="Today's Deals")
print br.response().read()

此外,在“下一页15”的href中,可能会有一些指示分页的内容,例如&index=15。如果所有页面上的物品总数在第一页上可用,则:
soup = BeautifulSoup(br.response().read())
totalItems = soup.findAll(id="results-count-total")[0].text
startVar =  [x for x in range(int(totalItems)) if x % 15 == 0]

然后只需遍历startVar并创建URL,将startVar的值添加到URL中,用br.open()打开它并抓取数据。这样,您不必在页面上编程“查找”“下一页”链接并执行单击以进入下一页 - 您已经知道所有有效的URL。将代码驱动页面操作最小化为仅需要的数据将加快您的提取速度。


3

这可以通过以下两种方式来完成:

  1. 在类下定义一个 listing_url 列表。
  2. parse_listings() 内部定义 listing_url

唯一的区别在于措辞。另外,假设要获取五页的 listing_urls。因此也将 page=1 放在类下面。

parse_listings 方法中,仅需发起一次请求即可。将需要跟踪的所有数据都放入 meta 中。也就是说,只使用 parse_listings 来解析“首页”。

一旦到达行末,即可返回项目。此过程是顺序的。

class MySpider(CrawlSpider):
    name = 'mysite.com'
    allowed_domains = ['mysite.com']
    start_url = 'http://www.mysite.com/'

    listing_url = []
    page = 1

    def start_requests(self):
        return [Request(self.start_url, meta={'page': page}, callback=self.parse_listings)]

    def parse_listings(self, response):
        hxs = HtmlXPathSelector(response)
        listings = hxs.select('...')

        for listing in listings:
            il = MySiteLoader(selector=listing)
            il.add_xpath('Title', '...')
            il.add_xpath('Link', '...')

        items = il.load_item()

        # populate the listing_url with the scraped URLs
        self.listing_url.extend(listing.select('...').extract())

        next_page_url = hxs.select('descendant::div[@id="pagination"]/'
                                   'div[@class="next-link"]/a/@href').extract()

        # now that the front page is done, move on to the next listing_url.pop(0)
        # add the next_page_url to the meta data
        return Request(urlparse.urljoin(response.url, self.listing_url.pop(0)),
                            meta={'page': self.page, 'items': items, 'next_page_url': next_page_url},
                            callback=self.parse_listing_details)

    def parse_listing_details(self, response):
        hxs = HtmlXPathSelector(response)
        item = response.request.meta['item']
        details = hxs.select('...')
        il = MySiteLoader(selector=details, item=item)

        il.add_xpath('Posted_on_Date', '...')
        il.add_xpath('Description', '...')
        items = il.load_item()

        # check to see if you have any more listing_urls to parse and last page
        if self.listing_urls:
            return Request(urlparse.urljoin(response.url, self.listing_urls.pop(0)),
                            meta={'page': self.page, 'items': items, 'next_page_url': response.meta['next_page_url']},
                            callback=self.parse_listings_details)
        elif not self.listing_urls and response.meta['page'] != 5:
            # loop back for more URLs to crawl
            return Request(urlparse.urljoin(response.url, response.meta['next_page_url']),
                            meta={'page': self.page + 1, 'items': items},
                            callback=self.parse_listings)
        else:
            # reached the end of the pages to crawl, return data
            return il.load_item()

1
您可以根据需要多次生成请求或项目。
def parse_category(self, response):
    # Get links to other categories
    categories = hxs.select('.../@href').extract()

    # First, return CategoryItem
    yield l.load_item()

    for url in categories:
        # Than return request for parse category
        yield Request(url, self.parse_category)

我发现这里有 — https://groups.google.com/d/msg/scrapy-users/tHAAgnuIPR4/0ImtdyIoZKYJ


1

请参见下面的更新答案,在EDIT 2部分(更新于2017年10月6日)

你使用yield有什么特别的原因吗?Yield将返回一个生成器,当在其上调用.next()时,它将返回Request对象。

将你的yield语句更改为return语句,应该可以按预期工作。

这是一个生成器的示例:

In [1]: def foo(request):
   ...:     yield 1
   ...:     
   ...:     

In [2]: print foo(None)
<generator object foo at 0x10151c960>

In [3]: foo(None).next()
Out[3]: 1

编辑:

将您的def start_requests(self)函数更改为使用follow参数。

return [Request(self.start_url, callback=self.parse_listings, follow=True)]

编辑2:

自Scrapy v1.4.0发布于2017-05-18起,现在建议使用response.follow而不是直接创建scrapy.Request对象。

来自发行说明

有一个新的response.follow方法用于创建请求;现在它是Scrapy蜘蛛中创建请求的推荐方式。这种方法使编写正确的蜘蛛更容易;response.follow比直接创建scrapy.Request对象具有几个优点:

  • 它处理相对URL;
  • 它可以在非UTF8页面上正确处理非ASCII URL;
  • 除了绝对和相对URL外,它还支持选择器;对于元素,它还可以提取它们的href值。

因此,对于上面的OP,请将代码更改为:

    next_page_url = hxs.select('descendant::div[@id="pagination"]/'
                               'div[@class="next-link"]/a/@href').extract()
    if next_page_url:
        yield Request(urlparse.urljoin(response.url, next_page_url[0]),
                      callback=self.parse_listings)

to:

    next_page_url = hxs.select('descendant::div[@id="pagination"]/'
                               'div[@class="next-link"]/a/@href')
    if next_page_url is not None:
        yield response.follow(next_page_url, self.parse_listings)

1
因为如果我使用return,它只会抓取一个列表并停止。它不会遍历每个列表并创建请求。您看,我可以在显示所有15个列表的页面上获取一些信息,但是我必须去抓取该列表的单独页面以获取我需要的其余信息。yield非常好用,直到我想添加爬取"下一页"的功能。 - imns
2
看起来 follow 不像是 Request() 的一个参数,我收到了一个错误 got an unexpected keyword argument 'follow' - imns
我也遇到了“got an unexpected keyword argument 'follow'”的问题,你是怎么解决的? - SAN
@SanjanaHE 我刚刚看了一下这段代码,它相当老旧。我已经更新了我的答案以反映最新版本的Scrapy。希望这可以帮到你。 - Mahmoud Abdelkader

0
我刚刚在我的代码中解决了同样的问题。我使用了Python 2.7自带的SQLite3数据库来解决它:在第一次解析函数的传递中,每个你收集信息的项目都会被放入一个数据库表中,每个解析回调函数的实例都会将每个项目的数据添加到该项目的表和行中。保持一个实例计数器,以便最后一个回调解析程序知道它是最后一个,并从数据库或其他地方写入CSV文件。回调可以是递归的,在元数据中告诉它分派到哪个解析模式(当然也包括哪个项目)进行处理。对我来说非常好用。如果你有Python,就有SQLite3。这是我第一次发现scrapy在这方面的限制时发布的帖子:Is Scrapy's asynchronicity what is hindering my CSV results file from being created straightforwardly?

0

0

你可能需要考虑两件事情。

  1. 你正在爬取的网站可能会阻止你定义的用户代理。
  2. 尝试在你的爬虫中添加一个DOWNLOAD_DELAY。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接