如何使用Scrapy爬取每个链接的所有内容?

11

我是一个 scrapy 的新手,我想从这个网站中提取每个广告的所有内容。因此,我尝试了以下方法:

from scrapy.spiders import Spider
from craigslist_sample.items import CraigslistSampleItem

from scrapy.selector import Selector
class MySpider(Spider):
    name = "craig"
    allowed_domains = ["craigslist.org"]
    start_urls = ["http://sfbay.craigslist.org/search/npo"]

    def parse(self, response):
        links = response.selector.xpath(".//*[@id='sortable-results']//ul//li//p")
        for link in links:
            content = link.xpath(".//*[@id='titletextonly']").extract()
            title = link.xpath("a/@href").extract()
            print(title,content)

物品:

# Define here the models for your scraped items

from scrapy.item import Item, Field

class CraigslistSampleItem(Item):
    title = Field()
    link = Field()

然而,当我运行爬虫时,什么也没有得到:

$ scrapy crawl --nolog craig
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]

因此,我的问题是:如何遍历每个网址,进入每个链接并爬取内容和标题?哪种方法是最好的?


你是如何想出 XPaths 的呢?.//*[@id='sortable-results']//ul//li//p 看起来还不错,应该能够得到页面上的 <p class="result-info">。但在这些 <p class="result-info"> 中,我看不到与 .//*[@id='titletextonly'] 匹配的内容。你可以使用 scrapy shell 测试你的 XPaths。 - paul trmbrth
Scrapy使用示例或XPath的示例?我认为https://docs.scrapy.org/en/latest/intro/tutorial.html#extracting-quotes-and-authors与您的用例非常相似。 - paul trmbrth
每个网站都是不同的,你需要的数据取决于你的用例。也许你想学习XPath,这篇博客文章可以作为一个很好的入门介绍。 - paul trmbrth
我会接受获得更多赞的问题,两个都很好。 - student
2个回答

14

要搭建一个基本的 scrapy 项目,您可以使用命令

scrapy startproject craig

然后添加蜘蛛和物品:

craig/spiders/spider.py

from scrapy import Spider
from craig.items import CraigslistSampleItem
from scrapy.linkextractors.lxmlhtml import LxmlLinkExtractor
from scrapy.selector import Selector
from scrapy import Request
import urlparse, re

class CraigSpider(Spider):
    name = "craig"
    start_url = "https://sfbay.craigslist.org/search/npo"

    def start_requests(self):

        yield Request(self.start_url, callback=self.parse_results_page)


    def parse_results_page(self, response):

        sel = Selector(response)

        # Browse paging.
        page_urls = sel.xpath(""".//span[@class='buttons']/a[@class='button next']/@href""").getall()

        for page_url in page_urls + [response.url]:
            page_url = urlparse.urljoin(self.start_url, page_url)

            # Yield a request for the next page of the list, with callback to this same function: self.parse_results_page().
            yield Request(page_url, callback=self.parse_results_page)

        # Browse items.
        item_urls = sel.xpath(""".//*[@id='sortable-results']//li//a/@href""").getall()

        for item_url in item_urls:
            item_url = urlparse.urljoin(self.start_url, item_url)

            # Yield a request for each item page, with callback self.parse_item().
            yield Request(item_url, callback=self.parse_item)


    def parse_item(self, response):

        sel = Selector(response)

        item = CraigslistSampleItem()

        item['title'] = sel.xpath('//*[@id="titletextonly"]').extract_first()
        item['body'] = sel.xpath('//*[@id="postingbody"]').extract_first()
        item['link'] = response.url

        yield item

craig/items.py

# -*- coding: utf-8 -*-

# Define here the models for your scraped items

from scrapy.item import Item, Field

class CraigslistSampleItem(Item):
    title = Field()
    body = Field()
    link = Field()

craig/settings.py

# -*- coding: utf-8 -*-

BOT_NAME = 'craig'

SPIDER_MODULES = ['craig.spiders']
NEWSPIDER_MODULE = 'craig.spiders'

ITEM_PIPELINES = {
   'craig.pipelines.CraigPipeline': 300,
}

craig/pipelines.py

from scrapy import signals
from scrapy.xlib.pydispatch import dispatcher
from scrapy.exporters import CsvItemExporter

class CraigPipeline(object):

    def __init__(self):
        dispatcher.connect(self.spider_opened, signals.spider_opened)
        dispatcher.connect(self.spider_closed, signals.spider_closed)
        self.files = {}

    def spider_opened(self, spider):
        file = open('%s_ads.csv' % spider.name, 'w+b')
        self.files[spider] = file
        self.exporter = CsvItemExporter(file)
        self.exporter.start_exporting()

    def spider_closed(self, spider):
        self.exporter.finish_exporting()
        file = self.files.pop(spider)
        file.close()

    def process_item(self, item, spider):
        self.exporter.export_item(item)
        return item
您可以通过运行 命令 来运行蜘蛛程序:
scrapy runspider craig/spiders/spider.py

从您的项目根目录开始。

它应该在您的项目根目录下创建一个craig_ads.csv文件。


谢谢你的帮助,伊万。确实,如果您能提供另一个通过管道完成它的示例,对社区将非常有帮助。您能给我们提供一个示例吗? - student
1
当然可以!您需要输出的格式是什么?CSV?JSON?还是Excel文件? - Ivan Chaer
假设我需要一个CSV文件。 - student
我也尝试了 scrapy crawl myspider -o items.csv,但文件是空的... 你有什么想法为什么会发生这种情况吗? - student
1
这是因为在parse_item()的结尾处我们没有使用yield返回结果(我们只是为了示例而打印了这些项)。我添加了yield,并且还添加了管道以导出CSV文件。请告诉我这是否有所帮助。 - Ivan Chaer
显示剩余4条评论

5

我正在努力回答你的问题。

首先,由于你使用了错误的XPath查询,所以得到了空白结果。根据XPath".//*[@id='sortable-results']//ul//li//p",你正确地定位了相关的<p>节点,但我不喜欢你的查询表达式。然而,我对你接下来的XPath表达式".//*[@id='titletextonly']""a/@href"一无所知,它们无法像你预期的那样定位链接和标题。也许你的意思是要定位标题的文本和标题的超链接。如果是这样,我相信你必须学习XPath,请从HTML DOM开始。

我确实想指导你如何进行XPath查询,因为有很多在线资源可用。我想提及Scrapy XPath选择器的一些特点:

  1. Scrapy XPath Selector是标准XPath查询的改进封装。
在标准的XPath查询中,它返回您查询的DOM节点数组。您可以打开浏览器的开发模式(F12),使用控制台命令$x(x_exp)进行测试。我强烈建议您通过这种方式测试XPath表达式。它将为您提供即时结果并节省大量时间。如果您有时间,请熟悉浏览器的Web开发工具,这将让您快速了解网页结构并找到您要查找的入口。
而Scrapy response.xpath(x_exp)返回与实际XPath查询对应的Selector对象数组,实际上是一个SelectorList对象。这意味着XPath结果由SelectorsList表示。SelectorSelectorList类都提供了一些有用的函数来操作结果:
  • extract,返回序列化文档节点列表(到Unicode字符串)
  • extract_first,返回标量,extract结果的first
  • re,返回列表,extract结果的re
  • re_first,返回标量,re结果的first
这些函数使你的编程更加方便。一个例子是,你可以直接在SelectorList对象上调用xpath函数。如果你之前尝试过lxml,你会发现这非常有用:如果你想在lxml中对前一个xpath结果调用xpath函数,你必须遍历前一个结果。另一个例子是,当你确定该列表中最多只有一个元素时,你可以使用extract_first来获取标量值,而不是使用列表索引方法(例如rlist[0]),否则当没有匹配元素时会导致索引超出范围异常。记住,在解析网页时总会有例外情况,要小心并且保持编程的健壮性。
2. 绝对XPath vs. 相对XPath 请记住,如果您嵌套XPathSelectors并使用以/开头的XPath,则该XPath将针对文档而不是相对于调用它的XPathSelector进行绝对查询。
当您执行操作node.xpath(x_expr)时,如果x_expr以/开头,则它是一个绝对查询,XPath将从根开始搜索;否则,如果x_expr以.开头,则它是一个相对查询。这也在标准2.5缩写语法中注明。
.选择上下文节点 .//para选择上下文节点的para元素后代 ..选择上下文节点的父级 ../@lang选择上下文节点的父级的lang属性
如何跟随下一页和结束以下。
对于你的应用程序,你可能需要遵循下一页。在这里,下一页节点很容易找到--有下一页按钮。但是,你还需要注意停止跟踪的时间。仔细查看URL查询参数以告诉你的应用程序的URL模式。在这里,为了确定何时停止跟随下一页,可以将当前项目范围与项目的总数进行比较。
新编辑
我有点困惑链接内容的含义。现在我明白了@学生想要爬取链接以提取AD内容。以下是解决方案。
4.发送请求并附加其解析器
正如你可能已经注意到的,我使用Scrapy的请求类来跟随下一页。实际上,Request类的强大之处不仅限于此--你可以通过设置参数回调为每个请求附加所需的解析函数。

回调函数 (callable) - 这个请求的响应(一旦下载完成)将作为其第一个参数被传递给该函数。有关详细信息,请参见下面的向回调函数传递其他数据。如果请求没有指定回调函数,则会使用爬虫的parse()方法。请注意,如果在处理过程中引发异常,则会调用errback。

在步骤3中,我发送下一页请求时没有设置callback,因为这些请求应该由默认的parse函数处理。现在来到了指定的广告页面,这是与前一个广告列表页面不同的页面。因此,我们需要定义一个新的页面解析器函数,比如说parse_ad,当我们发送每个广告页面请求时,将这个parse_ad函数附加到请求上。

让我们看看对我有效的修改后的示例代码:

items.py

# -*- coding: utf-8 -*-

# Define here the models for your scraped items
#
# See documentation in:
# http://doc.scrapy.org/en/latest/topics/items.html

import scrapy


class ScrapydemoItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    title = scrapy.Field()
    link = scrapy.Field()


class AdItem(scrapy.Item):
    title = scrapy.Field()
    description = scrapy.Field()

蜘蛛

# -*- coding: utf-8 -*-
from scrapy.spiders import Spider
from scrapy.http import Request
from scrapydemo.items import ScrapydemoItem
from scrapydemo.items import AdItem
try:
    from urllib.parse import urljoin
except ImportError:
    from urlparse import urljoin


class MySpider(Spider):
    name = "demo"
    allowed_domains = ["craigslist.org"]
    start_urls = ["http://sfbay.craigslist.org/search/npo"]

    def parse(self, response):
        # locate list of each item
        s_links = response.xpath("//*[@id='sortable-results']/ul/li")
        # locate next page and extract it
        next_page = response.xpath(
            '//a[@title="next page"]/@href').extract_first()
        next_page = urljoin(response.url, next_page)
        to = response.xpath(
            '//span[@class="rangeTo"]/text()').extract_first()
        total = response.xpath(
            '//span[@class="totalcount"]/text()').extract_first()
        # test end of following
        if int(to) < int(total):
            # important, send request of next page
            # default parsing function is 'parse'
            yield Request(next_page)

        for s_link in s_links:
            # locate and extract
            title = s_link.xpath("./p/a/text()").extract_first().strip()
            link = s_link.xpath("./p/a/@href").extract_first()
            link = urljoin(response.url, link)
            if title is None or link is None:
                print('Warning: no title or link found: %s', response.url)
            else:
                yield ScrapydemoItem(title=title, link=link)
                # important, send request of ad page
                # parsing function is 'parse_ad'
                yield Request(link, callback=self.parse_ad)

    def parse_ad(self, response):
        ad_title = response.xpath(
            '//span[@id="titletextonly"]/text()').extract_first().strip()
        ad_description = ''.join(response.xpath(
            '//section[@id="postingbody"]//text()').extract())
        if ad_title is not None and ad_description is not None:
            yield AdItem(title=ad_title, description=ad_description)
        else:
            print('Waring: no title or description found %s', response.url)

重点提示

输出的快照:

2016-11-10 21:25:14 [scrapy] DEBUG: Scraped from <200 http://sfbay.craigslist.org/eby/npo/5869108363.html>
{'description': '\n'
                '        \n'
                '            QR Code Link to This Post\n'
                '            \n'
                '        \n'
                'Agency History:\n' ........
 'title': 'Staff Accountant'}
2016-11-10 21:25:14 [scrapy] INFO: Dumping Scrapy stats:
{'downloader/request_bytes': 39259,
 'downloader/request_count': 117,
 'downloader/request_method_count/GET': 117,
 'downloader/response_bytes': 711320,
 'downloader/response_count': 117,
 'downloader/response_status_count/200': 117,
 'finish_reason': 'shutdown',
 'finish_time': datetime.datetime(2016, 11, 11, 2, 25, 14, 878628),
 'item_scraped_count': 314,
 'log_count/DEBUG': 432,
 'log_count/INFO': 8,
 'request_depth_max': 2,
 'response_received_count': 117,
 'scheduler/dequeued': 116,
 'scheduler/dequeued/memory': 116,
 'scheduler/enqueued': 203,
 'scheduler/enqueued/memory': 203,
 'start_time': datetime.datetime(2016, 11, 11, 2, 24, 59, 242456)}
2016-11-10 21:25:14 [scrapy] INFO: Spider closed (shutdown)

谢谢。希望这对你有所帮助,玩得开心。


1
谢谢您的帮助。我仍然不明白如何提取每个链接的内容。 - student
1
@学生,你所说的提取链接内容是什么意思?你想要访问那个链接并爬取一些内容吗? - rojeeer
1
@学生 好的,现在我明白了,您想获取广告页面。这并不难,只需发送您已经提取的URL的请求,并为这些请求添加新的解析器。我会修改我的代码。等一下。 - rojeeer
1
@学生,我更新了答案。请检查一下是否对你有效。 - rojeeer
1
当然。items.py非常简单。我已经为您添加了。 - rojeeer
显示剩余6条评论

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