书名:精通Python爬虫框架Scrapy
ISBN:978-7-115-47420-9
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
• 著 [美]迪米特里奥斯 考奇斯-劳卡斯 (Dimitrios Kouzis-Loukas)
译 李 斌
责任编辑 傅道坤
• 人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
• 读者服务热线:(010)81055410
反盗版热线:(010)81055315
Copyright © Packt Publishing 2016. First published in the English language under the title Learning Scrapy.
All Rights Reserved.
本书由英国Packt Publishing公司授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式或任何手段复制和传播。
版权所有,侵权必究。
Scrapy是使用Python开发的一个快速、高层次的屏幕抓取和Web抓取框架,用于抓Web站点并从页面中提取结构化的数据。本书以Scrapy 1.0版本为基础,讲解了Scrapy的基础知识,以及如何使用Python和三方API提取、整理数据,以满足自己的需求。
本书共11章,其内容涵盖了Scrapy基础知识,理解HTML和XPath,安装Scrapy并爬取一个网站,使用爬虫填充数据库并输出到移动应用中,爬虫的强大功能,将爬虫部署到Scrapinghub云服务器,Scrapy的配置与管理,Scrapy编程,管道秘诀,理解Scrapy性能,使用Scrapyd与实时分析进行分布式爬取。本书附录还提供了各种必备软件的安装与故障排除等内容。
本书适合软件开发人员、数据科学家,以及对自然语言处理和机器学习感兴趣的人阅读。
Dimitrios Kouzis-Loukas 作为一位顶级的软件开发人员,已经拥有超过15年的经验。同时,他还使用自己掌握的知识和技能,向广大读者讲授如何编写优秀的软件。
他学习并掌握了多门学科,包括数学、物理学以及微电子学。他对这些学科的透彻理解,提高了自身的标准,而不只是“实用的解决方案”。他知道真正的解决方案应当是像物理学规律一样确定,像ECC内存一样健壮,像数学一样通用。
Dimitrios目前正在使用最新的数据中心技术开发低延迟、高可用的分布式系统。他是语言无关论者,不过对Python、C++和Java略有偏好。他对开源软硬件有着坚定的信念,他希望他的贡献能够造福于各个社区和全人类。
Lazar Telebak 是一位自由的Web开发人员,专注于使用Python库/框架进行网络爬取和对网页进行索引。
他主要从事于处理自动化和网站爬取以及导出数据到不同格式(包括CSV、JSON、XML和TXT)和数据库(如MongoDB、SQLAlchemy和Postgres)的项目。
他还拥有前端技术和语言的经验,包括HTML、CSS、JS和jQuery。
让我来做一个大胆的猜测。下面的两个故事之一会和你的经历有些相似。
你与Scrapy的第一次相遇是在网上搜索类似“Web scraping Python”的内容时。你快速对其进行了浏览,然后想“这太复杂了吧……我只需要一些简单的东西。”接下来,你使用Requests库开发了一个Python脚本,并且挣扎于Beautiful Soup中,但最终还是完成了很酷的工作。它有些慢,所以你让它整夜运行。你重新启动了几次,忽略了一些不完整的链接和非英文字符,到早上的时候,大部分网站已经“骄傲地”存在你的硬盘中了。然而难过的是,不知什么原因,你不想再看到自己写的代码。当你下一次再想抓取某些东西时,则会直接前往scrapy.org,而这一次文档给了你很好的印象。现在你可以感受到Scrapy能够以优雅且轻松的方式解决了你面临的所有问题,甚至还考虑到了你没有想到的问题。你不会再回头了。
另一种情况是,你与Scrapy的第一次相遇是在进行网络爬取项目的研究时。你需要的是健壮、快速的企业级应用,而大部分花哨的一键式网络爬取工具无法满足需求。你希望它简单,但又有足够的灵活性,能够让你为不同源定制不同的行为,提供不同的输出类型,并且能够以自动化的形式保证24/7可靠运行。提供爬取服务的公司似乎太贵了,你觉得使用开源解决方案比固定供应商更加舒服。从一开始,Scrapy就像一个确定的赢家。
无论你是出于何种目的选择了本书,我都很高兴能够在这本专注于Scrapy的图书中遇到你。Scrapy是全世界爬虫专家的秘密。他们知道如何使用它以节省工作时间,提供出色的性能,并且使他们的主机费用达到最低限度。如果你没有太多经验,但是还想实现同样的结果,那么很不幸的是,Google并没有能够帮到你。网络上大多数Scrapy信息要么太简单低效,要么太复杂。对于那些想要了解如何充分利用Scrapy找到准确、易理解且组织良好的信息的人们来说,本书是非常有必要的。我希望本书能够帮助Scrapy社区进一步发展,并使其得以广泛应用。
第1章,Scrapy简介,介绍本书和Scrapy,可以让你对该框架及本书剩余部分有一个明确的期望。
第2章,理解HTML和XPath,旨在使爬虫初学者能够快速了解Web相关技术以及我们后续将会使用的技巧。
第3章,爬虫基础,介绍了如何安装Scrapy,并爬取一个网站。我们通过向你展示每一个行动背后的方法和思路,逐步开发该示例。学习完本章之后,你将能够爬取大部分简单的网站。
第4章,从Scrapy到移动应用,展示了如何使用我们的爬虫填充数据库并输出给移动应用。本章过后,你将清晰地认识到爬虫在市场方面所带来的好处。
第5章,迅速的爬虫技巧,展示了更强大的爬虫功能,包括登录、更快速地抓取、消费API以及爬取URL列表。
第6章,部署到Scrapinghub,展示了如何将爬虫部署到Scrapinghub的云服务器中,并享受其带来的可用性、易部署以及可控性等特性。
第7章,配置与管理,以组织良好的表现形式介绍了大量的Scrapy功能,这些功能可以通过Scrapy配置启用或调整。
第8章,Scrapy编程,通过展示如何使用底层的Twisted引擎和Scrapy架构对其功能的各个方面进行扩展,将我们的知识带入一个全新的水平。
第9章,管道秘诀,提供了许多示例,在这里我们修改了Scrapy的一些功能,在不会造成性能退化的情况下,将数据插入到数据库(比如MySQL、Elasticsearch及Redis)、接口API,以及遗留应用中。
第10章,理解Scrapy性能,将帮助我们理解Scrapy的时间是如何花费的,以及我们需要怎么做来提升其性能。
第11章,使用Scrapyd与实时分析进行分布式爬取,这是本书最后一章,展示了如何在多台服务器中使用Scrapyd实现横向扩展,以及如何将爬取得到的数据提供给Apache Spark服务器以执行数据流分析。
为了使本书代码和内容的受众尽可能广泛,我们付出了大量的努力。我们希望提供涉及多服务器和数据库的有趣示例,不过我们并不希望你必须完全了解如何创建它们。我们使用了一个称为Vagrant的伟大技术,用于在你的计算机中自动下载和创建一次性的多服务器环境。我们的Vagrant配置在Mac OS X和Windows上时使用了虚拟机,而在Linux上则是原生运行。
对于Windows和Mac OS X,你需要一个支持Intel或AMD虚拟化技术(VT-x或AMD-v)的64位计算机。大多数现代计算机都没有问题。对于大部分章节来说,你还需要专门为虚拟机准备1GB内存,不过在第9章和第11章中则需要2GB内存。附录A讲解了安装必要软件的所有细节。
Scrapy本身对硬件和软件的需求更加有限。如果你是一位有经验的读者,并且不想使用Vagrant,也可以根据第 3 章的内容在任何操作系统中安装Scrapy,即使其内存十分有限。
当你成功创建Vagrant环境后,无需网络连接,就可以运行本书几乎全部示例了(第4章和第6章的示例除外)。是的,你可以在航班上阅读本书了。
本书尝试着去适应广泛的读者群体。它可能适合如下人群:
就必备知识而言,阅读本书只需要用到很少的部分。在最开始的几章中,本书为那些几乎没有爬虫经验的读者提供了网络技术和爬虫的基础知识。Python易于阅读,对于有其他编程语言基本经验的任何读者来说,与爬虫相关的章节中给出的大部分代码都很易于理解。
坦率地说,我相信如果一个人在心中有一个项目,并且想使用Scrapy的话,他就能够修改本书中的示例代码,并在几个小时之内良好地运行起来,即使这个人之前没有爬虫、Scrapy或Python经验。
在本书的后半部分中,我们将变得更加依赖于Python,此时初学者可能希望在进一步研究之前,先让自己用几个星期的时间丰富Scrapy的基础经验。此时,更有经验的Python/Scrapy开发者将学习使用Twisted进行事件驱动的Python开发,以及非常有趣的Scrapy内部知识。在性能章节,一些数学知识可能会有用处,不过即使没有,大多数图表也能给我们清晰的感受。
欢迎来到你的Scrapy之旅。通过本书,我们旨在将你从一个只有很少经验甚至没有经验的Scrapy初学者,打造成拥有信心使用这个强大的框架从网络或者其他源爬取大数据集的Scrapy专家。本章将介绍Scrapy,并且告诉你一些可以用它实现的很棒的事情。
Scrapy是一个健壮的网络框架,它可以从各种数据源中抓取数据。作为一个普通的网络用户,你会发现自己经常需要从网站上获取数据,使用类似Excel的电子表格程序进行浏览(参见第3章),以便离线访问数据或者执行计算。而作为一个开发者,你需要经常整合多个数据源的数据,但又十分清楚获得和抽取数据的复杂性。无论难易,Scrapy都可以帮助你完成数据抽取的行动。
以健壮而又有效的方式抽取大量数据,Scrapy已经拥有了多年经验。使用Scrapy,你只需一个简单的设置,就能完成其他爬虫框架中需要很多类、插件和配置项才能完成的工作。快速浏览第7章,你就能体会到通过简单的几行配置,Scrapy可以实现多少功能。
从开发者的角度来说,你也会十分欣赏Scrapy的基于事件的架构(我们将在第8章和第9章中对其进行深入探讨)。它允许我们将数据清洗、格式化、装饰以及将这些数据存储到数据库中等操作级联起来,只要我们操作得当,性能降低就会很小。在本书中,你将学会怎样可以达到这一目的。从技术上讲,由于Scrapy是基于事件的,这就能够让我们在拥有上千个打开的连接时,可以通过平稳的操作拆分吞吐量的延迟。来看这样一个极端的例子,假设你需要从一个拥有汇总页的网站中抽取房源,其中每个汇总页包含100个房源。Scrapy可以非常轻松地在该网站中并行执行16个请求,假设完成一个请求平均需要花费1秒钟的时间,你可以每秒爬取16个页面。如果将其与每页的房源数相乘,可以得出每秒将产生1600个房源。想象一下,如果每个房源都必须在大规模并行云存储当中执行一次写入,每次写入平均需要耗费3秒钟的时间(非常差的主意)。为了支持每秒16个请求的吞吐量,就需要我们并行运行1600 × 3 = 4800次写入请求(你将在第9章中看到很多这样有趣的计算)。对于一个传统的多线程应用而言,则需要转变为4800个线程,无论是对你,还是对操作系统来说,这都会是一个非常糟糕的体验。而在Scrapy的世界中,只要操作系统没有问题,4800个并发请求就能够处理。此外,Scrapy的内存需求和你需要的房源数据量很接近,而对于多线程应用而言,则需要为每个线程增加与房源大小相比十分明显的开销。
简而言之,缓慢或不可预测的网站、数据库或远程API都不会对Scrapy的性能产生毁灭性的结果,因为你可以并行运行多个请求,并通过单一线程来管理它们。这意味着更低的主机托管费用,与其他应用的协作机会,以及相比于传统多线程应用而言更简单的代码(无同步需求)。
Scrapy已经拥有超过5年的历史了,成熟而又稳定。除了上一节中提到的性能优势外,还有下面这些能够让你爱上Scrapy的理由。
你可以在Scrapy中直接使用Beautiful Soup或lxml,不过Scrapy还提供了一种在lxml之上更高级的XPath(主要)接口——selectors。它能够更高效地处理残缺的HTML代码和混乱的编码。
Scrapy拥有一个充满活力的社区。只需要看看https://groups. google.com/ forum/#!forum/scrapy-users 上的邮件列表,以及Stack Overflow网站(http:// stackoverflow.com/questions/tagged/ scrapy)中的上千个问题就可以知道了。大部分问题都能够在几分钟内得到回应。更多社区资源可以从http://scrapy.org/ community/中获取到。
Scrapy要求以一种标准方式组织你的代码。你只需编写被称为爬虫和管道的少量Python模块,并且还会自动从引擎自身获取到未来的任何改进。如果你在网上搜索,可以发现有相当多专业人士拥有Scrapy经验。也就是说,你可以很容易地找到人来维护或扩展你的代码。无论是谁加入你的团队,都不需要漫长的学习曲线,来理解你的自定义爬虫中的特别之处。
如果你快速浏览发布日志(http://doc.scrapy.org/en/latest/ news.html),就会注意到无论是在功能上,还是在稳定性/bug修复上,Scrapy都在不断地成长。
在本书中,我们的目标是通过重点示例和真实数据集教你使用Scrapy。大部分章节将专注于爬取一个示例的房屋租赁网站。我们选择这个例子,是因为它能够代表大多数的网站爬取项目,既能让我们介绍感兴趣的变动,又不失简单。以该示例为主题,可以帮助我们聚焦于Scrapy,而不会分心。
我们将从只运行几百个页面的小爬虫开始,最终在第11章中使用几分钟的时间,将其扩展为能够处理5万个页面的分布式爬虫。在这个过程中,我们将向你介绍如何将Scrapy与MySQL、Redis和Elasticsearch等服务相连接,使用Google的地理编码API找到我们示例属性中的位置坐标,以及向Apache Spark提供数据用于预测最影响房价的关键词。
你需要做好阅读本书多次的准备。你可能需要从略读开始,先理解其架构。然后阅读一到两章,仔细学习、实验一段时间,再进入后面的章节。如果你觉得自己已经熟悉了某一章的内容,那么跳过这一章也无需担心。尤其是如果你已经了解HTML和XPath,那么就没有必要花费太多时间在第2章上面了。不用担心,对你来说本书还有很多需要学习的内容。一些章节,比如第8章,将参考书和教程的元素结合起来,深入编程概念。这就是一个例子,我们可能会阅读某一章几次,在这中间允许我们有几个星期的时间实践Scrapy。你在继续阅读后续的章节,比如以应用为主的第9章之前,不需要完美掌握第8章中的内容。阅读后续的内容,有助于你理解如何使用编程概念,如果你愿意的话,可以回过头来反复阅读几次。
为使本书既有趣,又对初学者友好,我们已经试图做了平衡。不过我们不会做的一件事情是,在本书中教授Python。对于这一主题,目前已经有了很多优秀的书籍,不过我更加建议的是以一种轻松的心态来学习。Python如此流行的一个理由是因为它比较简单、整洁,并且阅读起来更近似于英文。Scrapy是一个高级框架,无论是初学者还是专家,都需要学习。你可以将其称之为“Scrapy语言”。因此,我会推荐你通过材料来学习Python,如果你发觉自己对于Python的语法比较迷惑,那么可以通过一些Python的在线教程或Coursera等为Python初学者开设的免费在线课程予以补充。请放心,即使你不是Python专家,也能够成为一名优秀的Scrapy开发者。
对于大多数人来说,掌握一门像Scrapy这样很酷的技术所带来的好奇心和精神上的满足,足以激励我们。令人惊喜的是,在学习这个优秀框架的同时,我们还能享受到开发过程始于数据和社区,而不是代码所带来的好处。
为了开发现代化的高质量应用,我们需要真实的大数据集,如果可能的话,在开始动手写代码之前就应该进行这一步。现代化软件开发就是实时处理大量不完善数据,并从中提取出知识和有价值的情报。当我们开发软件并应用于大数据集时,一些小的错误和疏忽难以被检测出来,就有可能导致昂贵的错误决策。比如,在做人口统计学研究时,很容易发生仅仅是由于州名过长导致数据被默认丢弃,造成整个州的数据被忽视的错误。在开发阶段,甚至更早的设计探索阶段,通过细心抓取,并使用具有生产质量的真实世界大数据集,可以帮助我们发现和修复错误,做出明智的工程决策。
另外一个例子是,假设你想要设计Amazon风格的“如果你喜欢这个商品,也可能喜欢那个商品”的推荐系统。如果你能够在开始之前,先爬取并收集真实世界的数据集,就会很快意识到有关无效条目、停产商品、重复、无效字符以及偏态分布引起的性能瓶颈等问题。这些数据将会强迫你设计足够健壮的算法,无论是数千人购买过的商品,还是零销售量的新条目,都能够很好地处理。而孤立的软件开发,可能会在几个星期的开发之后,也要面对这些丑陋的真实世界数据。虽然这两种方法最终可能会收敛,但是为你提供进度预估承诺的能力以及软件的质量,都将随着项目进展而产生显著差别。从数据开始,能够带给我们更加愉悦并且可预测的软件开发体验。
对于初创公司而言,大规模真实数据的集甚至更加必要。你可能听说过“精益创业”,这是由Eric Ries创造的一个术语,用于描述类似技术初创公司这样极端不确定条件下的业务发展过程。该框架的一个关键概念是最小可行产品(Minimum Viable Product,MVP),这种产品只有有限的功能,可以被快速开发并向有限的客户发布,用于测试反响及验证业务假设。基于获得的反馈,初创公司可能会选择继续更进一步的投资,也可能是转向其他更有前景的方向。
在该过程中的某些方面,很容易忽视与数据紧密连接的问题,这正是Scrapy所能为我们做的部分。比如,当邀请潜在的客户尝试使用我们的手机应用时,作为开发者或企业主,会要求他们评判这些功能,想象应用在完成时看起来应该如何。对于这些并非专家的人而言,这里需要的想象有可能太多了。这个差距相当于一个应用只展示了“产品1”、“产品2”、“用户433”,而另一个应用提供了“三星 UN55J6200 55英寸电视机”、用户“Richard S”给出了五星好评以及能够让你直达产品详情页面(尽管事实上我们还没有写这个页面)的有效链接等诸多信息。人们很难客观判断一个MVP产品的功能性,除非使用了真实且令人兴奋的数据。
一些初创企业将数据作为事后考虑的原因之一是认为收集这些数据需要昂贵的代价。的确,我们通常需要开发表单及管理界面,并花费时间录入数据,但我们也可以在编写代码之前使用Scrapy爬取一些网站。在第4章中,你可以看到一旦拥有了数据,开发一个简单的手机应用会有多么容易。
当谈及表单时,让我们来看下它是如何影响产品增长的。想象一下,如果Google的创始人在创建其引擎的第一个版本时,包含了一个每名网站管理员都需要填写的表单,要求他们把网站中每一页的文字都复制粘贴过来。然后,他们需要接受许可协议,允许Google处理、存储和展示他们的内容,并剔除大部分广告利润。你能想象解释该想法并说服人们参与这一过程所需花费的时间和精力会有多大吗?即使市场非常渴望一个优秀的搜索引擎(事实正是如此),这个引擎也不会是Google,因为它的增长过于缓慢。即使是最复杂的算法,也不能弥补数据的缺失。Google使用网络爬虫技术,在页面间跳转链接,填充其庞大的数据库。网站管理员则不需要做任何事情。实际上,反而还需要一些努力才能阻止Google索引你的页面。
虽然Google使用表单的想法听起来有些荒谬,但是一个典型的网站需要用户填写多少表单呢?登录表单、新房源表单、结账表单,等等。这些表单中有多少会阻碍应用增长呢?如果你充分了解你的受众/客户,很可能已经拥有关于他们通常使用并且很可能已经有账号的其他网站的线索了。比如,一个开发者很可能拥有Stack Overflow和GitHub的账号。那么,在获得他们允许的情况下,你是否能够抓取这些站点,只需他们提供给你用户名,就能自动填充照片、简介和一小部分近期文章呢?你能否对他们最感兴趣的一些文章进行快速文本分析,并根据其调整网站的导航结构,以及建议的产品和服务呢?我希望你能够看到如何使用自动化数据抓取替代表单,从而更好地服务你的受众,增长网站规模。
抓取数据自然会让你发现并考虑与你付出相关的社区的关系。当你抓取一个数据源时,很自然地就会产生一些问题:我是否相信他们的数据?我是否相信获取数据的公司?我是否需要和他们沟通以获得更正式的合作?我和他们是竞争关系还是合作关系?从其他源获得这些数据会花费我多少钱?无论如何,这些商业风险都是存在的,不过抓取过程可以帮助我们尽早意识到这些风险,并制定出缓解策略。
你还会发现自己想知道能够为这些网站和社区带来的回馈是什么。如果你能够给他们带来免费的流量,他们应该会很高兴。另一方面,如果你的应用不能给你的数据源带来一些价值,那么你们的关系可能会很短暂,除非你与他们沟通,并找到合作的方式。通过从不同源获取数据,你需要准备好开发对现有生态系统更友好的产品,充分尊重已有的市场参与者,只有在值得努力时才可以去破坏当前的市场秩序。现有的参与者也可能会帮助你成长得更快,比如你有一个应用,使用两到三个不同生态系统的数据,每个生态系统有10万个用户,你的服务可能最终将这30万个用户以一种创造性的方式连接起来,从而使每个生态系统都获益。例如,你成立了一个初创公司,将摇滚乐与T恤印花社区关联起来,你的公司最终将成为两种生态系统的融合,你和相应的社区都将从中获益并得以成长。
当开发爬虫时,还有一些事情需要清楚。不负责任的网络爬虫会令人不悦,甚至在某些情况下是违法的。有两个非常重要的事情是避免类似拒绝服务(DoS)攻击的行为以及侵犯版权。
对于第一种情况,一个典型的访问者可能每几秒访问一个新的页面。而一个典型的网络爬虫则可能每秒下载数十个页面。这样就比典型用户产生的流量多出了10倍以上。这可能会使网站所有者非常不高兴。请使用流量限速将你产生的流量减少到可以接受的普通用户的水平。此外,还应该监控响应时间,如果发现响应时间增加了,就需要降低爬虫的强度。好消息是Scrapy对于这些功能都提供了开箱即用的实现(参见第7章)。
对于版权问题,显然你需要看一下你抓取的每个网站的版权声明,并确保你理解其允许做什么,不允许做什么。大多数网站都允许你处理其站点的信息,只要不以自己的名义重新发布即可。在你的请求中,有一个很好的User-Agent
字段,它可以让网站管理员知道你是谁,你用他们的数据做什么。Scrapy在制造请求时,默认使用BOT_NAME
参数作为User-Agent
。如果User-Agent
是一个URL或者能够指明你的应用名称,那么网站管理员可以通过访问你的站点,更多地了解你是如何使用他们的数据的。另一个非常重要的方面是,请允许任何网站管理员阻止你访问其网站的指定区域。对于基于Web标准的robots.txt
文件(参见http://www.google.com/robots.txt的文件示例),Scrapy提供了用于尊重网站管理员设置的功能(RobotsTxtMiddleware
)。最后,最好向网站管理员提供一些方法,让他们能说明不希望在你的爬虫中出现的东西。至少网站管理员必须能够很容易地找到和你交流及表达顾虑的方式。
最后,很容易误解Scrapy可以为你做什么,主要是因为数据抓取这个术语与其相关术语有些模糊,很多术语是交替使用的。我将尝试使这些方面更加清楚,以防止混淆,为你节省一些时间。
Scrapy不是Apache Nutch,也就是说,它不是一个通用的网络爬虫。如果Scrapy访问一个一无所知的网站,它将无法做出任何有意义的事情。Scrapy是用于提取结构化信息的,需要人工介入,设置合适的XPath或CSS表达式。而Apache Nutch则是获取通用页面并从中提取信息,比如关键字。它可能更适合于一些应用,但对另一些应用则又更不适合。
Scrapy不是Apache Solr、Elasticsearch或Lucene,换句话说,就是它与搜索引擎无关。Scrapy并不打算为你提供包含“Einstein”或其他单词的文档的参考。你可以使用Scrapy抽取数据,然后将其插入到Solr或Elasticsearch当中,我们会在第9章的开始部分讲解这一做法,不过这仅仅是使用Scrapy的一个方法,而不是嵌入在Scrapy内的功能。
最后,Scrapy不是类似MySQL、MongoDB或Redis的数据库。它既不存储数据,也不索引数据。它只用于抽取数据。即便如此,你可能会将Scrapy抽取得到的数据插入到数据库当中,而且它对很多数据库也都有所支持,能够让你的生活更加轻松。然而Scrapy终究不是一个数据库,其输出也可以很容易地更改为只是磁盘中的文件,甚至什么都不输出——虽然我不确定这有什么用。
本章介绍了Scrapy,给出了它能够帮你做什么的概述,并描述了我们认为的使用本书的正确方式。本章还提供了几种自动化数据抓取的方式,通过帮你快速开发能够与现有生态系统更好融合的高质量应用而获益。下一章将介绍HTML和XPath,这是两个非常重要的Web语言,我们在每个Scrapy项目中都将用到它们。
为了从网页中抽取信息,你必须对其结构有更多了解。我们将快速浏览HTML、HTML的树状表示,以及在网页上选取信息的一种方式XPath。
让我们花费一些时间来了解从用户在浏览器中输入URL(或者更常见的是,在其单击链接或书签时)到屏幕上显示出页面的过程。从本书的视角来看,该过程包含4个步骤,如图2.1所示。
图2.1
gumtree.com
)用于在网络上找到合适的服务器,而URL以及cookie等其他数据则构成了一个请求,用于发送到那台服务器当中。下面来看看这些步骤,以及它们所需的文档表示。这将有助于定位你想要抓取并编写程序获取的文本。
对于我们而言,URL分为两个主要部分。第一个部分通过域名系统(Domain Name System,DNS)帮助我们在网络上定位合适的服务器。比如,当在浏览器中发送https:// mail.google.com/mail/u/0/#inbox
时,将会创建一个对mail.google.com
的DNS请求,用于确定合适的服务器IP地址,如173.194.71.83
。从本质上来看,https:// mail.google.com/mail/u/0/#inbox
被翻译为https://173.194.71.83/mail/ u/0/#inbox
。
URL的剩余部分对于服务端理解请求是什么非常重要。它可能是一张图片、一个文档,或是需要触发某个动作的东西,比如向服务器发送邮件。
服务端读取URL,理解我们的请求是什么,然后回应一个HTML文档。该文档实质上就是一个文本文件,我们可以使用TextMate、Notepad、vi或Emacs打开它。和大多数文本文档不同,HTML文档具有由万维网联盟指定的格式。该规范当然已经超出了本书的范畴,不过还是让我们看一个简单的HTML页面。当访问http://example.com
时,可以在浏览器中选择View Page Source(查看页面源代码)以看到与其相关的HTML文件。在不同的浏览器中,具体的过程是不同的;在许多系统中,可以通过右键单击找到该选项,并且大部分浏览器在你按下Ctrl + U快捷键(或Mac系统中的Cmd + U)时可以显示源代码。
在一些页面中,该功能可能无法使用。此时,需要通过单击Chrome菜单,然后选择Tools | View Source才可以。
下面是http://example.com
目前的HTML源代码。
<!doctype html>
<html>
<head>
<title>Example Domain</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type"
content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width,
initial-scale=1" />
<style type="text/css"> body { background-color: ...
} }</style>
<body>
<div>
<h1>Example Domain</h1>
<p>This domain is established to be used for
illustrative examples examples in documents.
You may use this domain in examples without
prior coordination or asking for permission.</p>
<p><a href="http://www.iana.org/domains/example">
More information...</a></p>
</div>
</body>
</html>
我将这个HTML文档进行了格式化,使其更具可读性,而你看到的情况可能是所有文本在同一行中。在HTML中,空格和换行在大多数情况下是无关紧要的。
尖括号中间的文本(比如<html>
或<head>
)被称为标签。<html>
是起始标签,而</html>
是结束标签。这两种标签的唯一区别是/字符。这说明,标签是成对出现的。虽然一些网页对于结束标签的使用比较粗心(比如,为独立的段落使用单一的<p>
标签),但是浏览器有很好的容忍度,并且会尝试推测结束的</p>
标签应该在哪里。
<p>
和</p>
标签中的所有东西被称为HTML
元素。请注意,元素中可能还包括其他元素,比如示例中的<div>
元素,或是包含<a>
元素的第二个<p>
元素。
有些标签会更加复杂,比如<a href="http://www.iana.org/domains/example">
。含有URL的href
部分被称为属性。
最后,许多元素还包含文本,比如<h1>
元素中的"Example Domain"
。
对于我们来说,好消息是这些标签并不都是重要的。唯一可见的东西是body元素中的元素,即<body>
和</body>
标签之间的元素。<head>
部分对于指明诸如字符编码的元信息来说非常重要,不过Scrapy能够处理大部分此类问题,所以很多情况下不需要关注HTML页面的这个部分。
每个浏览器都有其自身复杂的内部数据结构,凭借它来渲染网页。DOM表示法具有跨平台、语言无关性等特点,并且被大多数浏览器所支持。
想要在Chrome中查看网页的树表示法,可以右键单击你感兴趣的元素,然后选择Inspect Element。如果该功能被禁用,你仍然可以通过单击Chrome菜单并选择Tools | Developer Tools来访问它,如图2.2所示。
图2.2
此时,你可以看到一些看起来和HTML表示非常相似但又不完全相同的东西。它就是HTML代码的树表示法。如果不管原始HTML文档是如何使用空格和换行符的话,它看起来几乎就是一样的。你可以单击每个元素,检查或调整属性等,同时可以在屏幕上观察这些变动有何影响。比如,当你双击某个文本,修改它,并按下回车键时,屏幕上的文本将会更新为这个新值。在右侧的Properties标签下,可以看到这个树表示法的属性,并且在底部可以看到一个类似面包屑的结构,它显示出了当前选择的元素在HTML元素层次结构中的确切位置,如图2.3所示。
图2.3
需要注意的一个重要事情是,HTML只是文本,而树表示法是浏览器内存里的对象,你可以通过编程的方式查看并操纵它,比如在Chrome中使用Developer Tools。
HTML文本表示和树表示并不包含任何像我们通常在屏幕上看到的那种漂亮视图。这实际上是HTML成功的原因之一。它应该是一个由人类阅读的文档,并且可以指定页面中的内容,而不是用于在屏幕中渲染的方式。这意味着选择HTML文档并使其更加好看是浏览器的责任,不管它是诸如Chrome的全功能浏览器、移动设备浏览器,甚至是诸如Lynx的纯文本浏览器。
也就是说,网络的发展促使Web开发者和用户对网页渲染的控制产生了巨大需求。CSS的创建就是为了对HTML元素如何渲染给予提示。不过,对于抓取而言,我们并不需要任何和CSS相关的东西。
那么,树表示法是如何映射到我们在屏幕上所看到的东西呢?答案就是框模型。正如DOM树元素可以包含其他元素或文本一样,默认情况下,当在屏幕上渲染时,每个元素的框表示同样也都包含其嵌入元素的框表示。从这种意义上说,我们在屏幕上所看到的是原始HTML文档的二维表示——树结构也以一种隐藏的方式作为该表示的一部分。比如,在图2.4中,我们可以看到3个DOM元素(一个<div>
和两个嵌入元素<h1>
和<p>
)是如何在浏览器和DOM中呈现的。
图2.4
如果你具有传统软件工程背景,并且不了解XPath相关知识的话,可能会担心为了访问HTML文档中的信息,你将需要做很多字符串匹配、在文档中搜索标签、处理特殊情况等工作,或是需要设法解析整个树表示法以获取你想抽取的东西。有一个好消息是这些工作都不是必需的。你可以通过一种称为XPath的语言选择并抽取元素、属性和文本,这种语言正是专门为此而设计的。
为了在Google Chrome浏览器中使用XPath,需要单击Developer Tools的Console标签,并使用$x
工具函数。比如,你可以尝试在http://example. com/
上使用$x('//h1')
。它将会把浏览器移动到<h1>
元素上,如图2.5所示。
图2.5
你在Chrome的Console标签中将会看到返回的是一个包含选定元素的JavaScript数组。如果将鼠标指针移动到这些属性上,被选取的元素将会在屏幕上高亮显示,这样就会十分方便。
文档的层次结构始于<html>
元素,可以使用元素名和斜线来选择文档中的元素。比如,下面是几种表达式从http://example.com
页面返回的结果。
$x('/html')
[ <html>...</html> ]
$x('/html/body')
[ <body>...</body> ]
$x('/html/body/div')
[ <div>...</div> ]
$x('/html/body/div/h1')
[ <h1>Example Domain</h1> ]
$x('/html/body/div/p')
[ <p>...</p>, <p>...</p> ]
$x('/html/body/div/p[1]')
[ <p>...</p> ]
$x('/html/body/div/p[2]')
[ <p>...</p> ]
需要注意的是,因为在这个特定页面中,<div>
下包含两个<p>
元素,因此html/body/div/p
会返回两个元素。可以使用p[1]
和p[2]
分别访问第一个和第二个元素。
另外还需要注意的是,从抓取的角度来说,文档标题可能是head
部分中我们唯一感兴趣的元素,该元素可以通过下面的表达式进行访问。
$x('//html/head/title')
[ <title>Example Domain</title> ]
对于大型文档,可能需要编写一个非常大的XPath表达式以访问指定元素。为了避免这一问题,可以使用//
语法,它可以让你取得某一特定类型的元素,而无需考虑其所在的层次结构。比如,//p
将会选择所有的p
元素,而//a
则会选择所有的链接。
$x('//p')
[ <p>...</p>, <p>...</p> ]
$x('//a')
[ <a href="http://www.iana.org/domains/example">More
information...</a> ]
同样,//a
语法也可以在层次结构中的任何地方使用。比如,要想找到div
元素下的所有链接,可以使用//div//a
。需要注意的是,只使用单斜线的//div/a
将会得到一个空数组,这是因为在example.com中,'div'元素的直接下级中并没有任何'a'元素:
$x('//div//a')
[ <a href="http://www.iana.org/domains/example">More
information...</a> ]
$x('//div/a')
[ ]
还可以选择属性。http://example.com/中的唯一属性是链接中的href
,可以使用符号@来访问该属性,如下面的代码所示。
$x('//a/@href')
[ href="http://www.iana.org/domains/example" ]
实际上,在Chrome的最新版本中,
@href
不再返回URL,而是返回一个空字符串。不过不用担心,你的XPath表达式仍然是正确的。
还可以通过使用text()
函数,只选取文本。
$x('//a/text()')
[ "More information..." ]
可以使用*符号来选择指定层级的所有元素。比如:
$x('//div/*')
[ <h1>Example Domain</h1>, <p>...</p>, <p>...</p> ]
你将会发现选择包含指定属性(比如@class
)或是属性为特定值的元素非常有用。可以使用更高级的谓词来选取元素,而不再是前面例子中使用过的p[1]
和p[2]
。比如,//a[@href]
可以用来选择包含href
属性的链接,而//a[@href="http://www.iana.org/domains/example"]
则是选择href
属性为特定值的链接。
更加有用的是,它还拥有找到href
属性中以一个特定子字符串起始或包含的能力。下面是几个例子。
$x('//a[@href]')
[ <a href="http://www.iana.org/domains/example">More information...</a> ]
$x('//a[@href="http://www.iana.org/domains/example"]')
[ <a href="http://www.iana.org/domains/example">More information...</a> ]
$x('//a[contains(@href, "iana")]')
[ <a href="http://www.iana.org/domains/example">More information...</a> ]
$x('//a[starts-with(@href, "http://www.")]')
[ <a href="http://www.iana.org/domains/example">More information...</a>]
$x('//a[not(contains(@href, "abc"))]')
[ <a href="http://www.iana.org/domains/example">More information...</a>]
XPath有很多像not()
、contains()
和starts-with()
这样的函数,你可以在在线文档 (http://www.w3schools.com/xsl/xsl_functions.asp
)中找到它们,不过即使不使用这些函数,你也可以走得很远。
现在,我还要再多说一点,大家可以在Scrapy命令行中使用同样的XPath表达式。要打开一个页面并访问Scrapy命令行,只需要输入如下命令:
scrapy shell http://example.com
在命令行中,可以访问很多在编写爬虫代码时经常需要用到的变量(参见下一章)。这其中最重要的就是响应,对于HTML文档来说就是HtmlResponse
类,该类可以让你通过xpath()
方法模拟Chrome中的$x
。下面是一些示例。
response.xpath('/html').extract()
[u'<html><head><title>...</body></html>']
response.xpath('/html/body/div/h1').extract()
[u'<h1>Example Domain</h1>']
response.xpath('/html/body/div/p').extract()
[u'<p>This domain ... permission.</p>', u'<p><a href="http://www. iana.org/domains/example">More information...</a></p>']
response.xpath('//html/head/title').extract()
[u'<title>Example Domain</title>']
response.xpath('//a').extract()
[u'<a href="http://www.iana.org/domains/example">More
information...</a>']
response.xpath('//a/@href').extract()
[u'http://www.iana.org/domains/example']
response.xpath('//a/text()').extract()
[u'More information...']
response.xpath('//a[starts-with(@href, "http://www.")]').extract()
[u'<a href="http://www.iana.org/domains/example">More
information...</a>']
这就意味着,你可以使用Chrome开发XPath表达式,然后在Scrapy爬虫中使用它们,正如我们在下一节中将要看到的那样。
Chrome通过向我们提供一些基本的XPath表达式,从而对开发者更加友好。从前文提到的检查元素开始:右键单击想要选取的元素,然后选择Inspect Element。该操作将会打开Developer Tools,并且在树表示法中高亮显示这个HTML元素。现在右键单击这里,在菜单中选择Copy XPath,此时XPath表达式将会被复制到剪贴板中。上述过程如图2.6所示。
图2.6
你可以和之前一样,在命令行中测试该表达式。
$x('/html/body/div/p[2]/a')
[ <a href="http://www.iana.org/domains/example">More
information...</a>]
id
为"firstHeading"
的h1
标签下span
中的text
。 id
为"toc
"的div
标签内的无序列表(ul
)中所有链接URL
。 class
属性包含"ltr"
以及class
属性包含"skin-vector"
的任意元素内所有标题元素(h1
)中的文本。这两个字符串可能在同一个class
中,也可能在不同的class
中。 class
属性值为"infobox
"的表格中第一张图片的URL。 class
属性以"reflist
"开头的div
标签中所有链接的URL。 References
"的元素之后的div
元素中所有链接的URL。 //img/@src
有一些XPath表达式,你将会经常遇到。让我们看一些目前在维基百科页面上的例子。维基百科拥有一套非常稳定的格式,所以我认为它们不会很快发生改变,不过改变终究还是会发生的。我们把如下这些表达式作为说明性示例。
//table[@class="infobox"]//img[1]/@src
//*[text()="References"]/../following-sibling::div//a
实际上,你将会经常在XPath表达式中使用到类。在这些情况下,需要记住由于一些被称为CSS的样式元素,你会经常看到HTML元素在其class
属性中拥有多个类。比如,在一个导航系统中,你会看到一些div标签的class
属性是"link
",而另一些是"link active
"。后者是当前激活的链接,因此会表现为可见或使用一种特殊的颜色(通过CSS)高亮表示。当抓取时,你通常会对包含有特定类的元素感兴趣,具体来说,就是前面例子中的"link
"和"link active
"。对于这种情况,XPath的contains()
函数可以让你选择包含有指定类的所有元素。
//div[starts-with(@class,"reflist")]//a/@href
请注意该表达式非常脆弱并且很容易无法使用,因为它对文档结构做了过多假设。
//h1[@id="firstHeading"]/span/text()
//div[@id="toc"]/ul//a/@href
//*[contains(@class,"ltr") and contains(@class,"skin-vector")]//h1//text()
抓取时经常会指向我们无法控制的服务器页面。这就意味着如果它们的HTML以某种方式发生变化后,就会使XPath表达式失效,我们将不得不回到爬虫当中进行修正。通常情况下,这不会花费很长时间,因为这些变化一般都很小。但是,这仍然是需要避免发生的情况。一些简单的规则可以帮助我们减少表达式失效的可能性。
Chrome经常会给你的表达式中包含大量常数,例如:
//*[@id="myid"]/div/div/div[1]/div[2]/div/div[1]/div[1]/a/img
这种方式非常脆弱,因为如果像广告块这样的东西在层次结构中的某个地方添加了一个额外的div
的话,这些数字最终将会指向不同的元素。本案例的解决方法是尽可能接近目标的img
标签,找到一个可以使用的包含id
或者class
属性的元素,如:
//div[@class="thumbnail"]/a/img
使用class
属性可以更加容易地精确定位元素,不过这些属性一般是用于通过CSS影响页面外观的,因此可能会由于网站布局的微小变更而产生变化。例如下面的class
:
//div[@class="thumbnail"]/a/img
一段时间后,可能会变成:
//div[@class="preview green"]/a/img
在前面的例子中,无论是"thumbnail
"还是"green
"都是我们所依赖类名的坏示例。虽然"thumbnail
"比"green
"确实更好一些,但是它们都不如"departure-time
"。前面两个类名是用于描述布局的,而"departure-time
"更加有意义,与div
标签中的内容相关。因此,在布局发生变化时,后者更可能保持有效。这可能也意味着该站的开发者非常清楚使用有意义并且一致的方式标注他们数据的好处。
通常情况下,id
属性是针对一个目标的最佳选择,因为该属性既有意义又与数据相关。部分原因是JavaScript以及外部链接锚一般选择id
属性以引用文档中的特定部分。例如,下面的XPath表达式非常健壮。
//*[@id="more_info"]//text()
例外情况是以编程方式生成的包含唯一标记的ID
。这种情况对于抓取毫无意义。比如:
//[@id="order-F4982322"]
尽管使用了id
,但上面的表达式仍然是一个非常差的XPath
表达式。需要记住的是,尽管ID应该是唯一的,但是你仍然会发现很多HTML
文档并没有满足这一要求。
由于标记的质量不断提高,现在可以更加容易地创建健壮的XPath表达式,来抽取HTML文档中的数据。在本章中,你学习了HTML文档和XPath表达式的基础知识。你可以看到如何使用Google的Chrome浏览器自动获取一些XPath表达式,并将其作为我们后续优化的起点。你同样还学到了如何通过审查HTML文档,直接创建这些表达式,以及辨别XPath表达式是否健壮。现在,我们准备好运用已经学到的所有知识,在第3章中使用Scrapy编写我们的前几个爬虫。