美文网首页大数据 爬虫Python AI SqlPython 爬虫框架
Python 爬虫框架Scrapy入门 官方手册翻译版

Python 爬虫框架Scrapy入门 官方手册翻译版

作者: 别摸我蒙哥 | 来源:发表于2019-10-11 15:35 被阅读0次

    将以 'quotes.toscrape.com' 网站作为爬取的对象。

    在这个教程中将围绕如下内容展开:

    1. 创建一个新的 Scrapy 项目
    2. 编写一个 spider 去爬网站,提取数据
    3. 使用命令行导出抓取数据
    4. 修改爬虫递归下一个链接
    5. 使用 spider 属性

    创建项目

    进入目标项目文件夹,执行以下代码:

    scrapy startproject tutorial
    

    这会创建一个 tutorial 路径,包含以下内容

    turorial/
        scrapy.cfg        # 部署配置的文件
        
        tutorial/         # 项目的 Python 模块,import 导入的代码在模块中
            __init__.py
            
            items.py      # 项目的 items(条目)自定义文件
            
            middlewares.py # 项目的中间件文件
            
            pipelines.py   # 项目的管道文件
            
            settings.py    # 配置文件
            
            spiders/       # 后期放 spider 的文件
                __init__.py
    

    第一个项目

    自定义的 Spider 类用于从网站(一组网站)中抓取数据,它们必须为 scrapy.Spider 子类,初始化的请求为必选项,可选项包括对下一页的相关处理、从解析下载的页面内容提取数据的细节。

    我们的第一个 Spider 保存在 quotes_spider.py 文件中,在 tutorial/spiders 路径下:

    import scrapy
    
    
    class QuotesSpider(scrapy.Spider):
        name = "quotes"
    
        def start_requests(self):
            urls = [
                'http://quotes.toscrape.com/page/1/',
                'http://quotes.toscrape.com/page/2/',
            ]
            for url in urls:
                yield scrapy.Request(url=url, callback=self.parse)
    
        def parse(self, response):
            page = response.url.split("/")[-2]
            filename = 'quotes-%s.html' % page
            with open(filename, 'wb') as f:
                f.write(response.body)
            self.log('Saved file %s' % filename)
    

    可以看出来,我们定义的 Spider 是 scrapy.Spider 的子类,并且定义了一些属性和方法:

    • name:唯一识别名,在当前项目中必须唯一。
    • start_requests():必须返回请求的迭代(可以是请求列表或生成器函数),Spider 从这里开始爬。
    • parse(): 每个请求发起后调用,用于处理响应。response 参数是 TextResponse 的实例,它包含了页面内容,内容随后会被处理。parse() 方法实现许多功能,包括解析响应,提取爬取的数据并解析为字典,寻找下一个爬取的 URL 并对此链接发起新的请求。

    如何执行我们的 Spider

    为了使 Spider 生效,需要进入项目顶层路径,并执行如下命令:

    scrapy crawl quotes
    

    这个命令以 quotes 名称作为参数执行,并且会对 qotes.toscrape.com 域名的网站发起请求。

    执行命令后,可以发现在当前路径下,有两个新文件: quotes-1.html quotes-2.html

    刚才发生了什么

    Scrapy 会规划 Spider 中 start_requests 返回的 scrapy.Request。一旦接受到每个请求的响应,就会实例化 Response 对象,将响应实例作为回调方法的参数(在这个例子中,是 parse 方法),回调与请求 Request 关联。

    start_requests 方法的简写

    我们可以仅仅定义 start_urls 类属性,它是 URL 的列表组合,用于替代 start_requests() 中通过生成 scrapy.Reuqest 对象的做法。定义了 start_urls 后,会使用默认的 start_requests() 来创建 Spider 中的初始化请求。
    于是代码可以改成:

    import scrapy
    
    
    class QuotesSpider(scrapy.Spider):
        name = "quotes"
        start_urls = [
            'http://quotes.toscrape.com/page/1/',
            'http://quotes.toscrape.com/page/2/',
        ]
    
        def parse(self, response):
            page = response.url.split("/")[-2]
            filename = 'quotes-%s.html' % page
            with open(filename, 'wb') as f:
                f.write(response.body)
    

    变化在于 start_urls 替代了 start_requests() 方法,只提供 URL 列表,其余的初始化请求操作交给父类中默认的 start_requests() 方法。

    parse() 方法会被在每个 URL 被请求时调用,即使我们没有明确的告诉 Scrapy 这么去做,这是因为 parse() 是 Scrapy 默认自动调用的方法。

    提取数据

    使用 Scrapy 提取数据最好的调试方式:Scrapy shell,执行:

    scrapy shell 'http://quotes.toscrape.com/page/1'
    

    使用 shell,你可以尝试从响应中选择元素,以 CSS 选择器为例:

    In [4]: response.css('title')
    Out[4]: [<Selector xpath='descendant-or-self::title' data='<title>Quotes to Scrape</title>'>]
    

    返回的结果 SelectorList,类列表对象,它扩展了list功能,可以理解为 Selector 的列表。这个列表对象被 XML/HTML 元素包裹,允许你执行进一步的查询和更细微力度的选择。

    提取标签文本

    比如,从刚才的 title 对象中提取文本,可以这么做:

    >>> response.css('title::text').getall()
    ['Quotes to Scrape']
    

    这里有两点要说明:

    1. 在 CSS 查询中添加了 '::text',这意味着只提取 <title> 中的文本。如果定义 '::text',会返回整个 title 元素内容(包括标签):
    >>> response.css('title').getall()
    ['<title>Quotes to Scrape</title>']
    
    1. 调用 '.getall()',它返回一个列表:这意味着选择器可能返回一个或多个结果;如果你只需要第一个结果,可以使用 '.get()',也可以通过 python 代码先选择第一个结果:
    response.css("title::text")[0].get()
    

    但是,如果直接对 SelectorList 实例使用 get() 方法可以避免 'IndexError' 错误,它会处理成 None 返回,所以推荐直接使用 '.get()'

    正则匹配

    除了使用 'getall()' 和 'get()' 方法,可以使用 're()' 方法来提取数据

    浏览器查看响应结果

    为了更好的使用合适的 CSS 选择器,可以使用 view(response) 来查看响应。它会在浏览器中显示响应内容,你可以使用浏览器开发者工具来检测 HTML 并选择合适的选择器。

    XPath 介绍

    除了 CSS,Scrapy 选择器还支持使用 XPath 表达式:

    >>> response.xpath('//title')
    [<Selector xpath='//title' data='<title>Quotes to Scrape</title>'>]
    >>> response.xpath('//title/text()').get()
    'Quotes to Scrape'
    

    XPath 表达式非常强大,是 Scrapy 选择器的基石。实际上,CSS选择器最终会被转换成 XPath。

    尽管 XPath 表达式没有 CSS 活跃,但是它更有用,因为除了导航定位文件结构,它还能获取文本内容。
    使用 XPath,你可以选择这样的内容:选择一个包含 "Next Page" 的链接。
    这使得 XPath 足以胜任爬虫的任务,我们鼓励你去学习 XPath ,即使你已经了解了如何构建 CSS 选择器。

    更多 XPath 使用方法可以查看 http://zvon.org/comp/r/tut-XPath_1.htmlhttp://plasmasturm.org/log/xpath101/

    提取 quotes 和 authors

    现在你已经知道了一点选择和提取数据的方法,继续完善代码。

    >>> for quote in response.css("div.quote"):
    ...     text = quote.css("span.text::text").get()
    ...     author = quote.css("small.author::text").get()
    ...     tags = quote.css("div.tags a.tag::text").getall()
    ...     print(dict(text=text, author=author, tags=tags))
    {'tags': ['change', 'deep-thoughts', 'thinking', 'world'], 'author': 'Albert Einstein', 'text': '“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”'}
    {'tags': ['abilities', 'choices'], 'author': 'J.K. Rowling', 'text': '“It is our choices, Harry, that show what we truly are, far more than our abilities.”'}
        ... a few more of these, omitted for brevity
    >>>
    

    使用 spider 提取数据

    上面都是在 shell 中调试,在返回 spider 中。目前 Spider 没有提取任何数据,只是保存了整个 HTML 文件到本地。
    接下来整合提取数据的逻辑到 spider 中。

    一个 Scrapy spider 可以生成许多包含已提取数据的字典。
    为做到这个目的,我们使用 yield 关键字来构建生成器:

    import scrapy
    
    
    class QuotesSpider(scrapy.Spider):
        name = "quotes"
        start_urls = [
            'http://quotes.toscrape.com/page/1/',
            'http://quotes.toscrape.com/page/2/',
        ]
    
        def parse(self, response):
            for quote in response.css('div.quote'):
                yield {
                    'text': quote.css('span.text::text').get(),
                    'author': quote.css('small.author::text').get(),
                    'tags': quote.css('div.tags a.tag::text').getall(),
                }
    

    对比之前的 spider 代码,在这里主要发生变化的是 yield 子句。是一个调试的三个选择器,我们获取了每个 quote 的指定内容。

    存储爬取的数据

    最简单的存储方式是使用 'Feed exports',命令如下:

    scrapy crawl -o quotes.json
    

    这会生成 quotes.json 文件,包含爬取的项目,以 JSON 序列化。

    由于历史原因,Scrapy 使用附加的方式而非替代原文件内容生成文件。也就是说,如果你执行两次存储名命令,会再添加内容到文件中。由于 JSON 格式问题,存储两次,将破坏 JSON 的格式。

    其他格式 JSON Lines

    scrapy crawl -o quotes.jl
    

    'JSON Lines' 是一种流类型结构,可以轻易的添加新内容到文件中,而不用担心执行了两次破坏文件格式。
    因为每个记录都以单独的一行记录。
    JL 当可以搭配工具,如 'JQ' 来辅助执行这样的命令。

    项目管道 Item Pipeline

    在小项目中,上面提到才这些内容都足够了。然而,如果你想执行更加复杂的爬取,你可以写一个 'tiem pipleline'

    「下一页」链接

    除了仅仅抓取第一页和第二页的内容,你想要网页中所有的内容也可以。

    在上面已经了解到如何提取页面上的链接,接下来看看如何获取并进入下一页链接

    提取下一页标签

    首先观察一下页面中下一页内容

    <ul class="pager">
        <li class="next">
            <a href="/page/2/">Next <span aria-hidden="true">&rarr;</span></a>
        </li>
    </ul>
    

    需要提取的内容是 href 的属性值,它告诉我们下一页的相对地址

    >>> response.css('li.next a::attr(href)').get()
    '/page/2/'
    

    >>> response.css('li.next a').attrib['href']
    '/page/2'
    

    那么如何递归所有的下一页链接呢,spider 内容可以这样改动:

    import scrapy
    
    
    class QuotesSpider(scrapy.Spider):
        name = "quotes"
        start_urls = [
            'http://quotes.toscrape.com/page/1/',
        ]
    
        def parse(self, response):
            for quote in response.css('div.quote'):
                yield {
                    'text': quote.css('span.text::text').get(),
                    'author': quote.css('small.author::text').get(),
                    'tags': quote.css('div.tags a.tag::text').getall(),
                }
    
            next_page = response.css('li.next a::attr(href)').get()
            if next_page is not None:
                next_page = response.urljoin(next_page)
                yield scrapy.Request(next_page, callback=self.parse)
    

    这里的关键在于:一、判断是否有下一页,二、如果有,如何拼接 URL 递归请求访问和解析新页面内容。

    现在,已经提取了数据,parse() 中拼接绝对路径的 URL 使用了 urljoin() 方法,并且生成一个新的请求到下一页;它将自身注册作为回调,以处理下一页的数据,并且持续爬取新页面。

    总结一下 Scrapy 的下一页获取机制:如果你在回调函数中生成了一个请求,Scrapy 会适配已发送的请求,并在请求完成注册一个要执行的回调方法。

    基于这个机制,你可以构建复杂的爬虫自定义点击超链接,也可以根据访问的页面不同自定义提取规则。

    在这个例子中,我们创建了一个循环,一直进入下一页界面直到最后一页。这个功能非常适合爬取博客、论坛或者拥有分页的网站。

    一个简单的方式创建请求

    我们可以使用 response.follow 来请求下一个页面

    ...
            next_page = response.css('li.next a::attr(href)').get()
            if next_page is not None:
                yield response.follow(next_page, callback=self.parse)
    

    将上面例子的最后两行改成 response.follow() 请求网站,不同于 scrapy.Request,response.follow 提供相对路径请求,这意味着你不需要再添加 urljoin 拼接 URL。
    记住,response.follow 返回的还是请求实例,所以需要 yield 生成这个请求。

    由于 <a> 标签是一个链接,所以 response.follow 自动使用 href 属性,所以代码还可以简写为:

            for a in response.css('li.next a'):
                yield response.follow(a, callback=self.parse)
    

    更多的例子和模型

    这里继续将回调和更多操作的内容,这次爬取作者信息

    import scrapy
    
    
    class AuthorSpider(scrapy.Spider):
        name = 'author'
    
        start_urls = ['http://quotes.toscrape.com/']
    
        def parse(self, response):
            # follow links to author pages
            for href in response.css('.author + a::attr(href)'):
                yield response.follow(href, self.parse_author)
    
            # follow pagination links
            for href in response.css('li.next a::attr(href)'):
                yield response.follow(href, self.parse)
    
        def parse_author(self, response):
            def extract_with_css(query):
                return response.css(query).get(default='').strip()
    
            yield {
                'name': extract_with_css('h3.author-title::text'),
                'birthdate': extract_with_css('.author-born-date::text'),
                'bio': extract_with_css('.author-description::text'),
            }
    

    这个 spider 开始于主页面,为所有的作家页面调用 parse_author 回调,分页链接的 parse 回调。

    parse_author 回调定义了一个方法从 CSS 查询中提取和清理数据,再产生作者资料的字典。

    在这个 spider 中展示的另一个有趣的事情:即使有许多 quotes 对应一个 author,我们也不用担心会多次访问同一个 author 界面。
    默认情况下,Scrapy 过滤重复URL请求,避免因访问服务器过多而产生问题。
    可以在设置中配置参数 DUPFILTER_CLASS

    使用 spider 参数

    提供命令行参数给 spider,添加 '-a' 选项即可:

    scrapy crawl quotes -o quotes-humor.json -a tag=humor
    

    这些参数会传递给 Spider 类的 '__init__' 方法,并作为 spider 实例的默认属性。

    在这个例子中,提供给 tag 属性的值可以使用 self.tag 获取。
    添加 tag 选项后,spider 抓取的内容被限制在特定的标签中:

    import scrapy
    
    
    class QuotesSpider(scrapy.Spider):
        name = "quotes"
    
        def start_requests(self):
            url = 'http://quotes.toscrape.com/'
            tag = getattr(self, 'tag', None)
            if tag is not None:
                url = url + 'tag/' + tag
            yield scrapy.Request(url, self.parse)
    
        def parse(self, response):
            for quote in response.css('div.quote'):
                yield {
                    'text': quote.css('span.text::text').get(),
                    'author': quote.css('small.author::text').get(),
                }
    
            next_page = response.css('li.next a::attr(href)').get()
            if next_page is not None:
                yield response.follow(next_page, self.parse)
    

    在这里,添加了 humor 标签/tag 的 spider 访问的 URL 变成了 'http://quotes.toscrape.com/tag/humor'

    [1] http://docs.scrapy.org/en/latest/

    相关文章

      网友评论

        本文标题:Python 爬虫框架Scrapy入门 官方手册翻译版

        本文链接:https://www.haomeiwen.com/subject/wmztmctx.html