用python写网络爬虫


因 7 中国主信出版集团多房哩明社 / 写 [澳] Richard Lawson 著 李斌 译 人民邮电出版社 北京 图书在版编目(CI P)数据 用Python写网络爬虫/(澳大利亚)理查德·劳森 (Richard Lawson) 著; 李斌译. 一北京:人民邮电出 版社, 2016.9 ISBN 978-7-115-43179一0 I. ①用…II . ①理…②李…III. ①软件工具一程 序设计N. ①TP311. 56 中国版本图书馆CIP数据核宇(2016)第177976号 版权声明 Cop严ight © 20 15 Packt Publishing. First published in the English language under the title Web Scraping with Python. All Rights Reserved. 本书由英国Packt Publishing公司授权人民邮电出版社出版。 未经出版者书面许可,对本书的任何部分不 得 以任何方式或任何手段复制和传播。 版权所有, 侵权必究。 ’ 著 [澳] Richard Lawson 译 李斌 责任编辑 傅道坤 责任印制 焦志炜 ’人民邮电出版社出版发行 北京市丰台区成寿寺路II号 邮编 100164 电子邮件 3 l 5@ptpress.com.cn 网址 http://www.ptpress.com.cn 三河市海波印务有限公司印刷 ’ 开本: 800x!OOO 1/16 印张: 10.75 字数z 148千字 印数z 1-3000册 著作权合同登记号 2016年9月第l版 2016年9月河北第1次印刷 图字:01-20 16-3962号 定价: 45.00元 读者服务热线: (010) 81055410 印装质量热线:(010) 81055316 反盗版热线:(010) 81055315 内容提要 本书讲解 了如何使用P川lOil来编写网络爬虫程序, 内容包括网络爬虫简 介, 从页面中抓取数据的三种方法, 提取缓存中的数据, 使用多个线程和进 程来进行并发抓取, 如何抓取动态页面中的内容, 与表单进行交互, 处理页 面中的验证码问题, 以及使用 Sca rpy和Portia 来进行数据抓取, 并在最后使 用本书介绍的数据抓取技术对几个真实的网站进行了抓取, 旨在帮助读者活 学活用书中介绍的技术。 本书适合有一定Python 编程经验, 而且对爬虫技术感兴趣的读者阅读 。 关于作者 Richard Lawson 来自澳大利亚 , 毕业于墨尔本大学计算机科学专业 。 毕 业后 , 他创办了一家专注于网络爬虫的公司,为超过 50个国家的业务提供远 程工作。 他精通于世界语 , 可以使用汉语和韩语对话,并且积极投身于开源 软件。 他目前在牛津大学攻读研究生学位,并利用业余时间研发自主无人机。 我要感谢Timothy Baldwin 教授将我引入这个令人兴奋的领域, 以及 本书编写时在巴黎招待我的ηiara 可 Douc 。 关于审稿人 Martin Bur咄是 一 名常驻纽 约的数据 记者 , 其工 作 是为华尔街日报绘 制 交互式图表。他在新墨西哥州立大学获得了新闻学和信息系统专业的学士学 位, 然后在纽约城市大学新闻学研究院获得了新闻学专业硕士学位。 我要感谢我的妻子Lisa鼓 励 我协助本书的创作 ,我的叔叔Michael 耐心解答我的编程问题, 以及我的父亲Richard激发了我对新闻学和写 作的热爱。 William Sankey是一位数据 专业人士,也是一位业余开发人员,生活在马 里兰州科利奇帕克市。 他于2012年毕业于约翰·霍普金斯大学, 获得了公共 政策硕士学位,专业方向为定量分析。他目前在L & M政策研究有限责任公司 担任健康服务研究员, 从事与 美国医疗保险和医疗补助服务中心 (C MS)相 关的项目。 这些项目包括责任医疗机构评估以及精神病院住院患者预付费系 统监测。 我要感谢我深爱的妻子Julia和顽皮的小猫Ruby,给予我全部的爱和 支持。 关于审稿人 Ayush Tiwari是一名Python开发者,本科就读于印度理工学院罗克分校。 他自2013年起工作于印度理工学院罗克分校信息管理小组,并活跃于网络开 发领域。 对他而言,审阅本书是一个非常棒的经历。他不仅 是一名审稿人, 也是一名狂热的网络爬虫学习者。他向所有Python爱好者推荐本书, 以便享 受爬虫的益处。 他热衷于Python网 络爬虫, 曾参与 体育直播订阅、通用P川lOil电子商务 网络爬虫(在Miranj)等相 关项目。 他还使用 Django 应用开发了就业门户,帮助改善印度理工学院罗克分校 的就业流程。 除了后端开发之外,他还喜欢使用诸如Nu mPy、 SciPy等Python库进行 科学计算和 数据分析,目前他从事计算流体力学领域的研究。你可以在GitHub 上访 问到他的项目,他的用户名是 ti waria yus h。 他喜欢徒步穿越喜马拉雅山谷,每年会参加多次徒步行走活动。 此外,他 还喜欢弹吉 他。 他的成就还包括参加国际知名的 Su per 30 小组, 并在其中成 为排名保持者。他在高中时, 还参加了国际奥林匹克数学竞赛。 我的家庭成员(我的姐姐Aditi、 我的父母以及Anand先生)、 我在 VI和IMG的朋友以及我的教授都为我提供了很大的帮助。我要感谢他们 所有人对我的 支持。 最后,感谢尊敬的作者和Packt出版社团队出版了这些非常好的技术 书籍。 我要对他们在编写这些书籍时的所有辛苦工作表示赞赏。 2 ..a.&. ....1』. 刷昌 互联网 包含了迄今为止最有用的数据集,并且大部分可以免费公开访问。 但是, 这些数据 难以复用。它们被嵌入在网 站的结构和样式当中, 需要抽取 出来才能使用。从网页中抽取数据的过程又被称为网络爬虫。随着越来越多 的信息被发布到网络上, 网络爬虫也变得越来越有用。 本书内容 第1章, 网络爬虫简介, 介绍了网络爬虫, 并讲解了爬取网站 的方法。 第2章, 数据抓 取,展示了如何从网页中抽取数据。 第3章, 下载缓存,学习了如何通过缓存结果避免重复下载的问题。 第4章, 并发下载, 通过并行下载加速数据抓取。 第5章, 动态内容,展示了如何从动态网 站中抽取数据。 第6章, 表单交互,展示了如何与 表单进行交互,从而访 问你需要的数据。 第7章, 验证码处理,阐述了如何访问被验证码图像保护的数据。 第8章, Scrapy,学习了如何使用流行的高级框架 Scrapy。 第9章,总结, 对我们介绍的这些网 络爬虫技术进行总结。 前言 阅读本书的前提 本书中所有的代码都己经在Python2.7 环境中进行过测试, 并且可以从 http: //bi tb uck et.org/wswp/code 下载到 这些 源 代码。 理 想情况 下, 本书未来的版本会将示例 代码移植到Python3 当中 。不过, 现在依赖的很多 库(比如 Sc ra py/Twi sted、 Mech anize 和Ghost ) 还 只支 持Python2 。为了帮 助 阐明爬取 示例 , 我 们创建 了一 个 示例 网 站, 其网址为 http: //exampl e. webscr api ng . co m 。 由于 该网 站限制 了 下载 内容的速度, 因此 如 果你 希 望 自行搭建示例网 站,可以从http ://bi tbuck et. org/wswp/places获取 网站源代码和安装说明。 我们决定为本书中使用的大部分示例搭建一个定制网站,而不是抓取活跃 网站, 这样我们就对环境拥有了完全控制。 这种方式为我们提供了稳定性, 因为活跃网 站要比书中的定制网 站更新更加频繁, 并且当你 尝试运行爬虫示 例时, 代码可能已经无法工作。 另外, 定制网 站允许我们自定义示例, 用于 阐释特定技巧并避免其他干扰。 最后, 活跃网 站可能并不欢迎我们使用它作 为学习网络爬虫的对象, 并且可能会尝试封禁我们的爬虫。 使用我们自己定 制的网 站可以规避这些风险, 不过在这些例子中学到的技巧确实也可以应用 到这些活跃网 站当中。 本书读者 阅读本书需要有一定的编程经验,并且不适用于绝对的初学者。在实践中, 我们将会首先实现我们自己的网络爬虫技术版本, 然后才会介绍现有的流行 模块, 这样可以让你更好地理解这些技术是如何工作的。 本书中的这些示例 将假 设你已经拥有Python语言以及 使用pip 安装模块的能力。如果你想复习 一下这些知识, 有一本非常好的免费在线书籍可以使用, 其作者为 Mark 2 前言 Pi lgr im,书籍网址是http: //www. diveint op ython. net 。这本书也是我 初学Python时所使用的资源。 此外,这些例子还假 设你己经了解网页是如何使用HTML 进行构建并通 过JavaScript 更新的知识。 关于HTT P、 css、AJAX、 WebK it 以及M ongoDB 的既有知识也很有用, 不过它们不是必需的,这些技术会在需要使用时进行 介绍。上述很多主 题的详细参考资料可以从 ht tp: //www. w3schools. com 获取到。 3 目录 第1章网络爬虫简介 1 .1 网络爬虫何时有用........... .... ........ 1. 2 网络爬虫是否合法 …… ……………… ………… ………………… …… …… ……… · 2 1.3 背景调研…………………………………………………………………………………··3 1.3.1 检查 robots.txt….......…...............................….............................3 1 . 3. 2 检 查网 站 地图 ......................... …......................…........................ 4 1.3.3 估算网 站 大小..........….......…………………………………………………5 1 . 3. 4 识 别网 站 所用技术 ………… …… ……… ……… ……… ………… …… …· 7 1 . 3.5 寻 找网 站所有者…. ......… ·· ……… ……… ………… ………… ………… ·· 7 1.4 编写第一个网络爬虫…..........................................….........…·················8 1.4.1 下载网页….........................…..........….........................................9 1.4.2 网站 地图 爬虫….........................…...................…······················12 1.4.3 ID遍历爬虫·············································································13 1.4.4 链接爬虫………………………………………………………………………··15 1.5 本章小结…··………………………………………………………………………………n 第2章数据抓取 23 2.1 分析网页…….................................….........................….....................…·23 2.2 三种网页抓取方法......................................…..................................…·26 2. 2. 1 正则 表达 式…………………… ……… ……… …… …… …… …… ……… …·26 目录 2.2.2 B eau tifu l Soup ·········…........................................……················28 2.2.3 Lxml·………………………………………………………………·················30 2.2.4 性 能对比.........................….........…................….........……·········32 2.2.5 结论.........……..............................................................……········35 2. 2.6 为链接 爬 虫添加 抓取 回调…........................ ......... ...................35 2.3 本章小结… .......… .......…………………………………………………………………38 第3章下载缓存 39 3.1 为链接爬虫添加缓存支持…·····················…·…....................................39 3.2 磁盘缓存...............................................................................................42 3.2.1 实现.........………………………………………………………………………·44 3.2.2 缓存测试..............……………………………………………………………46 3.2.3 节省磁盘空间…….......................….........…................……········ 46 3.2.4 清理过期数据…........................................................................47 3.2.5 缺点………………………………………………….......…........….........…· 48 3.3 数据库缓存...........................................................................................” 3.3.1 NoSQL是什么 ..............….......….......................……........…······50 3.3.2 安装M ongoDB …...................…….....................…····················50 3.3.3 MongoDB概述.......….........….................…...........................…50 3.3.4 MongoDB 缓存实现.................................…...............….......…52 3.3.5 压缩...............….........................................................................” 3.3.6 缓存测试…·”…....................…..........…........................…···········54 3.4 本章小结….........…............... ………………………………………………………妇 第4章并发下载 57 4.1 100万个网页.................…................................…..............…................57 4.2 串行爬虫.........................................…..................…................…........…60 4.3 多线程爬虫...................................…..................….........…...............…··60 2 目录 4.3.1 线程和进程如何工作...............................................................“ 4.3.2 实现........................................…........................…..................…61 4.3.3 多进程爬虫…............................................................................63 4.4 性能….........................…..........................….................….........……........67 4.5 本章小结 …··….......…......................….......…................….......................68 第5章动态内容 69 5.1 动态网页示例...............................................……..................................69 5.2 对动态网页进行逆向工程..............…·….........…............................……72 5.3 渲染动态网页.............................….......................….............................77 5 .3 .1 PyQ t 还是 PySid e……………… ……………………… …………………… 78 5 .3 .2 执行 J av aSc ript ···· ······ …. ...... …………………………………… ……….. 7g 5 .3 .3 使用W eb K it 与 网 站 交互 ....... …. ........ ....... …. ....................... … 80 5.3.4 Selenium ................................................................................... 85 5.4 本章小结.......…..........……………………………………………………...............gg 第6章表单交E 89 6.1 登录表单….........…....................................................….........................90 6. 2 支持内容更新的 登录脚 本扩展 …........ ........ ........ .. ….............. ........ ….. 97 6.3 使用M ec ha nize模块实现自动 化表单处理....... .. …....... …............ .... 100 6.4 本章小结 ………………………………………………………………………………·102 第7章验证码处理 103 7.1 注册账号…··……………………………………………………………………………·103 7.2 光学字符识别 ………… ………………………………………… …………………· … 106 7.3 处理复杂验证码….........…..........…................….......................…........ 111 7.3.1 使用验证码处理服务..............................…................….........112 7. 3. 2 9kw入门…………… …………………………………… ……… …………… 112 3 目录 7.3.3 与 注册功 能集成…..............……··············································119 7.4 本章小结 ..............…........…··………………………………………………………120 第 8章 Sc rapy 121 8.1 安装.............................…..................................……........................…··121 8.2 启动项目.........................…..........................................….......…··········122 8.2 . 1 定 义模型.........… ………………………………………………… ………… 12 3 8. 2. 2 创建爬 虫........……… …………………………………………………······· 12 4 8. 2. 3 使用shell命令 抓 取 …...........… …………………………… …………· 12 8 8.2.4 检查结果..............….......…………………………………………………129 8.2.5 中断与恢复爬虫”…...............……..............…··························132 8.3 使用P ort ia 编写可视化爬虫 ................. ............……··· ························ 13 3 8.3.1 安装…..............................….............…·····································133 8.3.2 标注……………………………………………………………………………··136 8. 3. 3 优化爬 虫…··……………………………………… ……… …………………· 13 8 8.3.4 检查结果…......................………………………………………………··140 8.4 使用 Scra pely实现自动 化抓取.........… .......... ................ ........ ........… 14 1 8.5 本章小结…........................…..........…........…......................…··············142 第9章 总结 14 3 句300OOAU’I句3 휌, AιTA丛TA件,、dp气d F飞d,、d 4·EA唱’EA唱BEA--EA唱-EA --EA唱··A 擎23· 索…站千…搜业网M… … 结 km M Hu h- 句J ,,町 鸣叫UUP马章AUh吭吭AU宝本咱,“ 咱3AaT ZJ 0707 070707 4 本章中, 我们将会介绍如下主题: · 网络爬虫领域简介: · 解释合法性质疑: · 对目标网 站进行背景调研1 ·逐步完善一个高级网 络爬虫。 1.1 网络爬虫何时有用 第1章 网络爬虫简介 假设我有一个鞋店, 并且想要及时了解竞争对手的价格。 我可以每天 访问他们的网 站 , 与我店铺中鞋子的价格进行对比。但是, 如果我店铺中 的鞋类品种繁多,或是希望能够更加频繁地查看价格变化的话, 就需要花 费大量的时间,甚至难以实现。再举一个例 子, 我看中了一双鞋, 想等它 促销时再购买。我可能需要每天访问这家鞋店的网 站 来查看这双鞋是否降 价, 也许需要等待几个月的时间, 我才能如愿盼到这双鞋促销。 上述这 两个重复性的手工流程,都可以利用本书介绍的网络爬虫技术实现自动化 处理。 第 1 章 网络爬虫简介 理想状态下, 网络爬虫并不是必须品,每个网 站 都应该提供API, 以结构 化的格式共享它们的数据。 然而现实情况中, 虽然一些网 站 已经提供了这种 API,但是它们通常会限制可以抓取的数据,以及访问这些数据的频率。另外, 对于网 站 的开发者而言,维护前端界面比维护后端AP I接口优先级更高。总 之, 我们不能仅仅 依赖于API 去访 问我们所需的在线数据, 而是应该学习一 些网 络爬虫技术的相 关知识。 1.2 网络爬虫是否合法 网络爬虫目前还处于早期的蛮荒阶段,“允许哪些行为” 这种基本秩序还 处于建设之中。 从目前的实践来看, 如果抓取数据的行为用于个人使用, 则 不存在问题:而如果数据用于转载, 那么 抓取的数据类型就非常关键了。 世界各地法院的一些案件 可以帮助我们确定哪些网 络爬虫行为是允许 的。 在Feist Publications, Inc. 起诉Rural Tel写phone Service Co. 的案件中, 美 国联邦最高法院裁定抓 取并转载真实数据 (比如,电话清 单〉是允许的。 而 在澳大利亚, Telstra Corporation Limited 起诉Phone Directories Company Pty Ltd 这一类 似案件中, 则裁定只有拥有明确作者的数据, 才可以获得版权。 此外, 在欧盟的ofir.dk起诉home.dk一案中, 最终裁定定期抓取和深度链接 是允许的。 这些案件 告诉我们,当抓 取的数据是现实生活中的真实数据(比如,营业 地址、 电话清 单) 时, 是允许转载的。 但是, 如果是原创数据 (比如,意见 和评论), 通常就会受到版权限制, 而不能转载。 无论如何,当你抓 取某个网站 的数据时,请记住自己是该网站 的访 客,应 当约束自己的抓 取行为, 否则他们可能会封禁你的 IP ,甚至采取更进一步的 法律行动。 这就要求下载请求的速度需要限定在一个合理值之内, 并且还需 要设定一个专属的用户代理来标识自己。 在下面的小节中我们将会对 这些实 践进行具体介绍。 2 1.3 背景调 研 关于上述几个法律案件的更多信息可以参考下述地址: • http://caselaw.lp.findlaw.com/scripts/ getcase. pl?court=US&vol=499&invol=340 £· http://www.austlii.edu.au/au/cases/cth/ FCA/2010/44.html • http://www.bvhd.dk/uploads/tx mocarticles /S og Handelsrettens afg relse i Ofir-sagen.pdf 1.3 背景调研 在深入讨论爬取一个网 站 之前,我们首先需要对目标站点的规模和结构进 行一定程度的了解。 网站 自身的robots.txt 和Sit emap 文件都可以为我 们提供一定的帮助,此外还有一些能提供更详细信息的外部工 具,比如Google 搜索 和 WHO IS。 1.3.1 检查robots.txt 大多数网站 都会定义robots.txt文件, 这样可以让爬虫了解爬取该网站 时存在哪些限制。 这些限制虽然仅仅 作为建议给出,但是良好的网络公民都应 当遵守 这些限制。 在爬取之前, 检查robots.txt文件这一宝贵资源可以最小 化爬虫被 封禁的可能,而且还能 发现 和网 站结构相关的线 索 。关于 robot s. tx t 协议的更多信息可以参见 ht tp ://www. robotstxt.org 。 下面的代码是我 们的示 例文件 robots.txt 中的内容, 可以访问http ://examp le. webscraping. co m/robots. txt 获取。 r e -- w a r 户Ud a B 叮4· ·㌏ 卡M nn ·· oew -- qo ta 1le c- -4 era ses s· 工 #UD 3 第 1 章 网络爬虫简介 # section 2 User-agent : * Crawl-delay: 5 Disallow : /trap # section 3 Sitemap : http://example.webscraping. com/sitemap .xml 在sect ion 1 中, robots. txt 文件禁止用户代理为BadCrawler 的 爬虫爬取该网站 , 不过这种写法可能无法起 到应有的作用, 因为恶意爬虫根 本不会遵从robots. txt 的要 求。本章后面的一个例子将会展 示如何让爬虫 自动遵守robots. txt 的要 求。 sect ion 2规定,无论使用哪种用户代理, 都应该在两次下载请求之 间给出5秒的抓取延迟, 我们需要遵从该建议以避免服务器过载。 这里还有 一个/trap 链接,用于封禁那些爬取了不允许链接的恶意爬虫。如果你访问 了这个链接, 服务器就会封禁你的 IP 一分钟! 一个真实的网站可能会对你 的 IP 封禁更长时间,甚至是 永久封禁。 不过如果这样设置 的话, 我们就无 法继续这个例子了。 sect ion 3定义了一个Si temap 文件, 我们将在下一节中了解如何检 查该文件。 1.3.2 检查网 站地图 网站提供的Sitemap 文件(即网站 地图) 可以帮助爬虫定位网站 最新的 内容, 而 无须爬取每 一个网页。 如 果 想要了解更多信 息 , 可 以从 http: //www. sitemaps. or g/protocol.html 获取网 站 地图标准的定 义。 下面是 在robots. txt 文件中发现的Sitemap 文件的内容。 http ://example . webscraping. com/view/Afghanistan-1 http ://example . webscraping. com/view/Aland-Islands-2 4 1.3 背景调研 http ://example . webscraping. com/view/Albania-3 网站地图提供了所有网页的链接, 我们会在后面的小 节中使用这些信息, 用于创建我们的第一个爬虫。 虽然Sitemap文件提供了一种爬取网站的有效 方式, 但是我们仍需对其谨慎处理, 因为该文件经常存在缺失、 过期或 不完 整的问题。 1.3.3 估算网站大小 目标网站的大小会影响我们如何进行爬取。 如果是像我们的示例站点这样 只有几百个URL的网站 , 效率并没有那么 重要:但如果是拥有 数百万个网页 的站 点, 使用串行下载可能需要持续数月才能完成, 这时就需要使用第 4 章 中介绍的分布式下载来解决了。 估算网站 大小 的 一个简便方法是检查G oo gle爬 虫的 结果,因 为G oo gle 很 可 能已经爬 取 过我们感兴趣的网 站 。 我们可以通 过Goo gl e 搜 索的site 关 键 词 过 滤域名 结果, 从而获 取 该 信 息 。 我 们可以从 http://www. google. com/ad va口ced search 了解到该 接口 及其他高 级搜 索参数的用法。 图 1.1 所示为使用site 关键词对我们的示例网 站进行搜索的结果, 即在 Google中搜索 site: ex am ple. webscra pin g. com o 从图 1.1 中可以看出,此时Google估算该网站拥有 202 个网 页, 这和实 际情况差不多。 不过对于更大型的网站, 我们 会发现Google 的估算并 不十 分准确。 在域名后面添加URL 路径, 可以对结果进行过滤, 仅显示网站的某些部 分。 图 1.2 所示为搜索 site: exam ple. webscra ping. com/ view 的结果。 该搜索 条件会限制Google只搜索国家页面。 5 第 1 章 网络爬虫简介 Aboul202re田JltS伺45蕃emnds) AF • Example web scrap�ng website 辄酬阳.we民町,商ping.oomlcor世in回I/AF• Exam阱嗣b쿩ing \¥盹脚.Arri目,闻geria · Angela · senin · Sotswana ·刷刷na Fa回·6田undl C副陌roon ca阳Verde ·C田11ra1 Arri国n R邱刷lc·Ch副. NA-εxa『nple web scraP.ing webs挂e ex翻F瑞e E翼샃pie web缸万冒pir咱w雹』画捶. IJ。曲Arr咀e拖a An窜山l国 Anti凯iaar晴B酣buda A阳ba Sahaπ、a&·Sa『1胁ad。S•B毫撼IZI>”Bl>伺11Ud握’哥。nal「·a, Saint Eus恒屈JS and . NE-E姐mple �eb scraping website e阻mpl且W创JSCI'胃病’g.oomllso/捋E• Nalklnal Fl咽’M回:1,267,000 S블惜먳阳elli剧,问pula如n: 10,676,27专lso: NE. Country: IJ胁,.Cai就醋’Nlam町C曲睛酣吐AF. Tlcl: J班Curr田,cy Code: XOF. NG - Example web sαaping website e刷脚.wet刷刷ng.comlisol闸, National Fl啕:滽:923,768 squa阳闹。metres. Pcψul硝or应154,00口,000.1聪:NG. C咀Jntry: N精制a巳apl国E Alli!由a con剧团提:AF. Tld: .ng. currency C时居:NGN. 图1.1 About 117明ults (0.52 seam峭 Example web部raping website 就ami归”W确scraping.α:m1/view/Guemse·沪倪, Natio捕’F陆g:A,回:76squ町e kllom钝国. Population: 65,22苗,i四:GG. Cou晴y. G佳emse于“Capital: St Peter pॹCon伽ent EU. Tld: .gg. Currency Code: GBP. Example web scraping website 回am树e.w唾民臼撞倒ng.comfv梅w/Je陀曹y-113 ... Nat阳nal F句:阳明:116呵U脚扭llornet阳.P句“la὚;阂,612. lso: JE. Country: Jerse沪 Capital: Saini Hel田.Conti阴阳t EU.T陆:.je. Currency Code: GBP. PK - Example �eb scraping website e泪m阱.webscra酬g.comlv恒w/P副stan-1邸, National Fl句:A陌a:8部,S铅squa用kffome悦s. Population: 184,404,791.国:PK. C由mtry: Pak陆;tan. Cap幅:1时ama缸ad.Cont加剧1l:AS“lld: .pk Currency Code ... Example web scrapinQ website - WebScraping.com example.w盼scra阪ng.com/view/Ma阳䭧134 ... Na悔自at Flag; Area: 329,750 squa阳kllometJ髓,Popi!翩。n: 28,27 4, 729精I盹:MY“ C皿miry: Ma姐ysia. Ca竽:Kuala L四npur. Con11nent AS. Tld: .my. C町rency町 图1.2 这种 附加的过滤条件非常有用,因为在理想情况下,你只希望爬取网站 中 包含有用数据的部分,而不是爬取网站 的每个页面。 6 1.3 背景调研 1.3.4 识别网 站所用技术 构建网站所使用的技术类型也会对我们如何爬取产生影响。 有一个十 分有 用的工具可以检查网站 构建的技术类型̍builtwith 模块。 该模块的安装 方法如下。 pip install builtwith 该模块将URL 作为参数,下载该URL并对其进行分析,然后返回该网站 使用的技术。 下面是使用该模块的一个例子。 》> imp。rt builtwith 》> builtwith .parse (’http://example .webscraping .c。m’} {u’j avascript-framew,。rks ’:[u ’ jQl且ery ’ , u’M。dernizr’ , u’jQl且ery UI ’ ] I u’pr。gramming-languages’ : [u ’Pyth。n ’ ] I u’web-framew。rks ’: [u ’ Web2py’ , u’Twitter B。。tstrap ’ ] , u ’web-servers ’: [u ’Nginx ’ ]} 从上 面 的返回 结果中可以看 出 , 示 例网 站使用了 Pyth on的Web2 py 框架 , 另外还使用了 一些通用的JavaScript 库,因此该网站的内容很有可能是嵌入 在 HTML中的 , 相 对而言比 较容易抓取。 而如果改用 A ng ula rJS构建该网 站 ,此 时的网 站 内容就很 可 能是动 态 加载的 。 另外, 如果网站 使用了 ASP. NE T , 那 么 在爬取网页时, 就必须要用到会话管理和表单提交了。 对于这些更加复杂 的情况, 我们会在第5 章和第6 章中进行介绍。 1.3.5 寻找网站所有者 对于一些网站 , 我们可能会关心其所有者是谁。 比如, 我们已知网站的所 有者会封禁网 络爬虫, 那么我们最好把下载速度控制得更加保守一些。 为了 找到网 站的所有者,我们可以使用WHOIS协议查询域名的注册者是谁。Python 中有一个针对该协议的封 装库, 其文档地址为 https: // pypi. python. or g/ py pi/ python-whois , 我们可以通过 pi p进行安装。 7 第l章 网络爬虫简介 pip install pyth。n-wh。is 下面是使用该模块对 appspot.com 这个域名进行WHOIS 查询时的返回 结果。 》> imp。rt wh。is 》> print whois.wh。is (’appsp。t.com’} 、四.e_servers" : [ ] , ”NSl .G。。GLE .COM” , ”NS2 .G。。GLE .COM” , ”NS3 .G。。GLE . COM” , ”NS4 .GࠂGLE . C。M” , ”ns4 . q。。qle .com”, ”ns2 . q。。qle .com” , ”nsl . q。。qle .com” , ”ns3 . q。。qle . c。m” ”。rq” : ”G。。qle Inc.”, ”e啤ails”: [ ”abusecomplaints@markm。nit。r . com” , ”dns-ad皿in@q。。gle .com” 从结果中可以看出该域名归属于 Google, 实际上也确实如此。 该域名是 用于GoogleApp En g ine服务的 。 当 我们爬 取 该域名时就需要十 分小 心 , 因 为 Google经常会阻断网络爬虫, 尽管实际上其自身就是一个网络爬虫业务。 1.4 编写第一个网络爬虫 为了抓 取网站 ,我们首先需要下载包含有感兴趣数据的网页,该过程一般 被称为爬取(crawling)。 爬取 一个网站有很多种方法, 而选用哪种方法更加 合适, 则取决于目标网站的结构。 本章中, 首先会探讨如何安全地下载网页, 然后会介绍如下3 种爬取网站的常见方法: 8 · 爬取网站地图1 · 遍历每个网页的数据库ID; · 跟踪网页链接。 1.4.1 下载网页 1.4 编写第一个网络爬虫 要想爬取网页,我们首先需要将其下载下来。下面的示例脚本使用Py thon 的ur llib 2 模块下载URL。 import urllib2 def download (url) : return urllib2 .url。pen (url) . read () 当传入URL参数时, 该函数将会下载网页并返回其HTML。 不过, 这个 代码 片段存在一个问题, 即当下载网页时, 我们可能会遇到一些无法控制的 错误, 比如请求的页面可能不存 在。 此时,ur lli b2 会抛出异常,然后退出 脚本。 安全起 见, 下面再给出一个更健壮的版 本, 可以捕获这些异常。 import urllib2 def download (url) : print ’ D。wnl。ading : ’, url try : html = urllib2 . urlopen (url) . read () except urllib2 .URLError as e: print ’Downl。ad err。r: ’, e.reas。n html = None return html 现在, 当出现下载错误时, 该函数能够捕获到异常, 然后返回No ne。 1. 重试下载 下载时遇到的错误经常是临时性的, 比如服务器 过载时返回的 503 Ser vic e Un availab le 错误 。 对 于此 类错 误, 我们可以 尝 试重 新 下载, 因 为这个服务器 问题现 在可能己解决。 不过, 我们不需要对所有错误都尝试重 9 第1章 网络爬虫简介 新下载。 如果服务器返回的是404 Not Found 这种错误, 则说明该网页目 前并不存 在, 再次尝试 同样的请求一般 也不会出现不同的 结果。 互联网工程任务组(Internet En gineeri ng Tas k Forc e)定义了Hπ? 错误的 完 整列表, 详情可参考https: //tools. ie tf. or g/h tml/rfc 72 31# section - 6。从该文档中,我们可以了解到4xx 错误发生在请求存在问题时, 而5xx 错误则发生在服务端存在问题时。 所以, 我们只需要确保download 函数在 发生Sxx 错误时重试下载即可。 下面是支持重 试下载功能的新版 本 代码。 def download (url, num retries=2): print ’ D。wnloading: ’, url try: html = urllib2 . urlopen (url) .read () except urllib2 .URLError as e: print ’Download error: ’, e.reason html = None if num retries > 0: if hasattr (e, ’code ’) and 500 <= e.code < 600: # recursively retry Sxx HTTP errors return download (url, num retries-1) return html 现在, 当 download函数遇到Sxx 错误码时, 将会递归调用函数自身进 行重试。 此外, 该函数还增加了一个参数, 用于设定重试下载的次数, 其默认 值为两次。 我们在这里限制 网页下载的尝试次数, 是因为服务器错误可能 暂时 还没有解决。想要测试该函数, 可以尝试下载http: //httpstat. us/500, 该网址会始终 返回500 错误码。 》> d。wnl。ad (’http : //httpstat.us/500 ’} Downl。ading : http://httpstat.us/500 D。wnload err。r : Internal Server Err。r D。wnl。ading : http://httpstat.us/500 Download err。r : Internal Server Err。z D。wnl。ading : http://httpstat .us/500 D。wnload err。r : Internal Server Err。r 10 1.4 编写第一个网络爬虫 从上面的返回结果可以看出, down load 函数的行为和预期一致, 先尝 试下载网 页, 在接收到500 错误后, 又进行了两次重试才放弃。 2. 设置用户代理 默认情况下, ur llib2 使用 Python-ur llib/ 2. 7 作为用户代理下载 网 页内容, 其中 2. 7是 Python 的版 本号。 如果能使用可辨识的用户代理则 更好, 这样可以避免我们的网络爬虫碰到一些问题。 此外, 也许是因为曾经 历过质量不佳的Python网 络爬虫造成的服务器 过载,一些网 站 还会封禁这个 默认的用户代理 。 比如, 在使用 P同lOil默认用户代理 的情况下,访问 http://www.meetup.com /, 目 前会返回如图 1.3 所示的访问拒绝提示。 Access denied η1e owner of this website (www.meetup.com) has banned your access based on your browser's signature ( 175413467,仿Rlae4-ua48). • Ray ID: 1754l34676eft>ae4 • Tim臼饱mp: Mon.06心ct-14 18:55:48 GMT • Yo咀r IP address: 83.27.128.162 • Reques撞ed URL: www.mee灿p.com/• E.π·or reference number: I 0 IO • Server ID: FL 33F7 • User-Agent:时也on-urllib/2.7 图1.3 因此,为了下载更加可靠, 我们需要控制用户代理的设定。 下面的代码对 download函数进行了修改, 设定了一个默认的用户代理“wsw p”( 即Web Scraping with Python 的首字母缩写)。 def d。wnload (url, user agent= ’wswp’, num retries=2) : print ’ Downloading: ’, url headers = {’User-agent ’: user agent } request = urllib2 . Request (url, headers=headers) try: html = urllib2 . urlopen (request) • read () except urllib2 . URLError as e: print ’Download error: ’, e.reason html = None 11 第l章 网络爬虫简介 if num retries > 0: if hasattr (e, ’code ’) and 500 <= e.code < 600: # retry 5XX HTTP errors return downl。ad (url, user agent, num retries-1) return html 现在 ,我们拥有了一个灵活的下载函数, 可以在 后续示例中得到复用。 该 函数能够捕获异常、 重试下载并设置 用户代理。 1.4.2 网站地图爬虫 在 第一个简单的爬虫中, 我们将使用示例网站 robots. tx t文件中发现 的网站地图来下载所有网页。 为了解析网站 地图, 我们将会使用一个简单的 正则表达式, 从< loc >标签中提取出URL。 而在 下一章中, 我们将会介绍一 种更加健壮的解析方法̍css选择器。 下面是该示例爬虫的代码。 def crawl sitemap (url) : # download the sitemap file sitemap = downl。ad (url) # extract the sitemap links links = re . findall (’ <loc> ( . *?) ]+href= [”\ ’] (.丰?) [”\ ’]’, re. IGNORECASE) # list of all links from the webpage retur口 webpage regex.findall(html) 要运行这段代码, 只需要调 用 link crawler函数, 并传入两个参数: 要爬取的网站 URL和用于跟踪链接的正则表达式。对于示例网站 , 我们想要 爬取的是国家列表索 引页和国家页面。 其中, 索 引页链接格式如下。 • ht tp: I I ex ampl e. webs crap in g. com/ index/ 1 • ht tp: I I ex ample. webs crap ing. com/土口dex/2 国家页链接格式如下。 • ht tp: I /examp le. webscrap ing. com/ view/A fghanist an-1 • ht tp: I /examp le. webs crap ing. com/ view/A land-I sla口ds- 2 因此, 我们可以用/(index[ view)/这个简单的正则表达式来匹配这两 类网页。 当爬虫使用这些输入参数运行时会发生什么呢?你会发现我们得到 了 如下的下载错误。 》> link crawler (’http:/ /example.webscraping.com’, ’I (index I view) ’) Downloading: http://example.webscraping.com Downloading: /index/1 Traceback (most recent call last) : ValueError: unknown url type : Ii口dex/1 16 1. 4 编写第一个 网络爬虫 可以看出, 问题出在下载/index/1时, 该链接只有网页的路径部分, 而没有协议和服务器部分, 也就是说这是一个相对链接。 由于浏览器知道你 正在浏览哪个网页, 所以在浏览器浏览时, 相对链接是能够正常工作的。 但 是 , ur lli b 2是无法获 知 上下文 的。为了让ur llib 2 能 够定位 网页, 我们 需要将链接转换为绝对链接的形式, 以便包含定位网页的所有细节。 如你所 愿,Py thon中确实有用来实现这 一功能的模块, 该模块称为urlparse。 下 面 是li nk crawl er 的改进版本, 使用了 url parse 模 块来 创 建绝对 路 径。 import urlparse def link_crawler (seed url, link regex) : ”””Crawl from the given seed URL following links matched by link regex ) ( P 1』O lp r- ue -u de e- -u e eqA SU - rL e㜍 uw =q品 a-r e14 C UW ea= ur q企cl -r leu wl aE 工 rh cw html = download (url) for link in get links (html) : if re .match (link_regex, link) : li口k = urlparse.urljoin ( seed url, link) crawl_queue .append (link) 当你 运行这段 代码时,会发现虽然网页下载 没有出现错误,但 是同样 的地 点总是会被不断下载到。 这是因为这些地点相互之间存在链接。比如, 澳大 利 亚链接到了南极洲, 而南极洲也存在到 澳 大利亚的 链接 , 此 时爬虫 就会在 它们之间 不断循环 下去。 要想避免 重复 爬 取相 同 的 链接 , 我们 需 要记录哪些 链接己经被 爬 取过。 下 面是修改后的 li nk craw ler函 数, 己具备存储己 发 现URL的功能,可以避免重复下载。 def link crawler (seed url, link regex) : crawl queue = [ seed url] # keep track which URL’s have seen before ) 14 m ←L )h (( ps 01’ 咱K )p l口 e- ZE 工 ue u14 eu ( - ue dt q· ·uae -e q4o qJ lu -- we 14nn auwW E工 zq- ao c- zdk f、 14c n tw =E l ea= 14 sr 14 cl mr =r to eu hf 口14 e- l eh sw 17 第l章 网络爬虫简介 # check if link matches expected regex if re . match(link regex, link) : # form absolute link link = urlparse. urljoin(seed_url, link) # check if have already seen this link if link not i口 seen: seen. add(link) crawl queue . append(link) 当运行该脚本时,它会爬取所有地点, 并且能够如 期停止。 最终, 我们得 到了一 个可用的爬虫! 高级功能 现在,让 我们为链接爬虫添加一些功能,使其在爬取其他网站 时更加有用。 解析robo ts.t xt 首先, 我们需要解析robots. txt 文件, 以避免下载禁止爬取的URL。 使用Python自带 的robotparser 模块, 就可以轻松完成这项工作, 如下面 的代码所示。 》> imp。rt z。b。tparser 》> rp = r。b。tparser . Rob。tFileParser () 》> rp . set_url ('http : //ex四1ple . webscraping . com/rob。ts .txt ’) 》> rp .read() 》> url = ’http://example . webscraping .com ’ 》> user_agent = ’BadCrawler ’ 》> rp . can_fetch (use�二agent , url) False 》> user_agent = ’G。。dCrawler ’ 》> rp . can_fetch (user_agent, url) True robotparser 模块首先加载robots.txt 文件,然后通过can_fetch () 函 数 确 定指定的 用户代理是否允许访 问网 页 。 在本 例 中 , 当用户代理设 置 为 ’B adCraw ler ’时 ,robotp arser 模块会返 回 结果表明无 法获 取网页, 这 和 示例 网 站 robots. txt 的 定 义一 样。 18 1.4 编写第一个网络爬虫 为了将该功能集成到爬虫中, 我们需要在cra wl 循环中添加该检查。 while crawl queue : url = crawl_queue . p。p () # check url passes robots.txt restrictions if rp . can_fetch (user_agent, url): else: print ’Bl。cked by robots.txt: ’, url 支持代理 有时我们需要使用代理访 问某个网 站 。 比如,Net fli x 屏蔽了美国以外的 大多数国家。使用urllib2支持代理并没有想象中那么容易(可以尝试使用 更友好的 Python HTTP模块re quests来实现该 功能, 其文档地址为 ht tp ://docs.p ython -reque sts.org /)。 下面是使用urllib2支持代 理的代码。 proxy = opener = urllib2 .build_opener () pr。xy params = { urlparse. urlparse (url) .scheme : proxy} 。pener. add handler (urllib2 . ProxyHandler (pr。xy_params)) response = opener.。pen ( request) 下面是集成了该功能的新版本d ow口l oad函数。 def download (url, user_agent= ’ wswp’, proxy=None, num_retries=2) : print ’Downloading:’, url headers = {’User-agent ’: user_agent} request = urllib2 . Request (url, headers=headers) opener = urllib2 . build一。pener () if proxy: try: proxy params = { urlparse.urlparse (url) .scheme : proxy} opener. add handler (urllib2 . Pr。xyHandler (proxy params) ) html = opener. 。pen (request) . read () except urllib2 . URLError as e: print ’Downl。ad error: ’, e.reason 19 第 1 章 网络爬虫简介 html = None if num retries > 0: if hasattr(e , ’code ’) and 500 <= e.code < 600: # retry SXX HTTP errors html = download(url, user agent, proxy, num retries-1) return html 下载F民速 如果我们爬取网站的速度过快,就会面临被封禁或是造成服务器过载的风 险。 为了降低这些风险, 我们可以在两次下载之间添加延时, 从而对爬虫限 速。 下面是实现了该功能的类的代码。 class Throttle: ”””Add a delay between downloads to the same domain def ̿init̿ (self, delay) : # amount of delay between downloads for each domain self.delay = delay # timestamp of when a domain was last accessed self.domains = {} def wait(self, url) : domain = urlparse.urlparse(url) .netl。c last_accessed = self. d。mains . get(domain) if self.delay > 0 and last accessed is n。t None : sleep secs = self.delay - (datetime .datetime . now() - last accessed) .seconds if sleep secs > 0: # domain has been accessed recently # so need to sleep time .sleep(sleep secs) # update the last accessed time self. d。mains [ d。main] = datetime .datetime . now() T hrot tl e 类记录了 每个域 名上 次访 问 的时间 ,如果当前时间距离上 次 访问时间小于指定延 时, 则执行睡眠操作。 我们可以在每 次下载之前调 用 Thr ott le 对 爬虫 进行限 速。 20 1.4 编写第一个 网络爬虫 thr。ttle = Throttle (delay) throttle.wait (url) result = download (url, headers, proxy=proxy, num retries=num retries) 避免爬虫陷阱 目前,我们的爬虫会跟踪所有之前没有访问过的链接 。但是,一些网 站会 动态生成页面内容, 这样就会出现无限多的网页。 比如, 网 站 有一个在线日 历功能, 提供了可以访问下个月和下一年的链接, 那么 下个月的页面中同样 会包含访问 再下个月的链接, 这样页面就会无止境地链接下去。 这种情况被 称为爬虫陷阱。 想要避免陷入爬虫陷阱,一个简单的方法是记录到达当前网 页经过了 多少 个链接, 也就是深度。 当到达最大深度时, 爬虫就不再向队列中添加该网页 中的链接了。 要实现这一功能, 我们需要修改 se en 变量。 该变量原先只记 录访问过的网页链接, 现在 修改为一个字典, 增加了页面深度的记录。 def link crawler ( ... , max depth=2): max depth = 2 seen = {} .. .. 、ns tk pn e- - 1』 d l l一 rxn ua -l rLm 口电k e= n e- -- l sl h =t r p。 he f td p ef di if link not in seen: ) k 14n ·工 +14 ( hd tn pe ep dp a =- e ЍU 『ke nu iq l一 『L 14 nw ea er sc 现在有了这一功能,我们就有信心爬虫最终一定能够完成。如果想要禁用 该 功能, 只需将 ma x_de p th 设为一 个 负 数 即可 , 此时 当前深度永远不会与 之相等。 最终版本 这个 高级链接 爬 虫 的 完 整 源 代 码 可 以在 ht tp s: //bitbucke t. org/ wswp /code/src/t ip/cha pte r01 /link_ cra wler3 .p y 下载得到 。 要测 21 第1 章 网络爬虫简介 试这段代码,我们可以将用户代理设置 为 BadCrawler, 也就是本章前文所 述的被robots.txt 屏蔽了的那个用户代理。 从下面的运行结果中可以看出, 爬虫果然被屏 蔽了, 代码启动后马上就会结束。 》> seed_url = ’http: //ex四iple . webscraping . c。m/index' 》> link_regex = ’/(indexlview) ’ 》> link_crawler ( seed_url , link_regex , user_agent= ’ BadCrawler ’} Blocked by robots .txt : http ://example . webscraping .com/ 现在,让我们使用默认的用户代理, 并将最大深度设置为1 , 这样只有主 页上的链接才会被下载。 》> link」crawler(seed_url , link_regex , max_depth=l) D。wnl。ading : http ://example . webscraping .com//index Downl。ading : http://example . webscraping .c。m/index/l Downl。ading : http://example . webscraping . com/view/Antigua-and-Barbuda-10 D。wnl。ading : http : //exa皿1ple . webs craping . c。m/view/Antarctica-9 D。wnloading : http://example . webscraping .c。皿/view/Anguilla-8 Downloading : http://example . webscraping . com/view/Ang。la-7 Downloading : http ://example . webscraping . com/view/Andorra-6 D。wnloading : http : //ex皿1ple . webscraping . c。m/view/阳.erican-S础。a-5 D。wnloading : http ://ex四ple . w,由scraping . com/view/Algeria-4 Downloading: http://example . webscraping . com/view/Albania-3 D。wnl。ading: http ://examp le . webscraping . com/view/Aland-工slands -2 D。wnl。ading: http://example . webscraping . c。m/view/Afghanistan-1 和预期一样, 爬虫在下载完国家列表的第一页之后就停止了。 1.5 本章小结 本章介绍了网络爬虫,然后开发了一个能够在后续章节中复用的成熟爬 虫。 此外,我们还介绍了一些外部工具和模 块的使用方法, 用于了解网 站 、 用户代理、 网站地图、 爬取延时以及各种爬取策略。 下一章 中,我们将讨论如何从己爬取到的网页中获取数据。 22 第2章 数据抓取 在上一章中 ,我们构建了一个爬虫, 可以通过跟踪链接的方式下载我们所 需的网页。 虽然这个例子很有意思 , 却不够实用, 因为爬虫在下载网页之后 又将 结果丢弃掉了。 现在, 我们需要让这个爬虫从每个网页中抽取一些数据, 然后实现某些事情, 这种做法也被称为抓取(scraping) 。 首先, 我们会介绍一个叫做Firebug Lite 的浏 览器扩展, 用于检查网页内 容, 如果你有一些网络开发背景的话, 可能己经对该扩展十分熟悉了。 然后, 我们会介绍三种抽取网 页数据 的方法, 分别是正则表达式 、 Beauti削 Soup和 lxml 。最后, 我们将对比这三种数据抓取 方法 。 2.1 分析网 页 想要了解一个网页的结构如何, 可以使用查看源代码的方法。 在大多数浏 览器中, 都可以在页面上右键单击选择 View page source 选工页, 获取网页的 源代码, 如图 2.1 所示。 我们可以在 H⬍的下述代码中找到我们感兴趣 的数据 。
Hon'.e 3号J「ci飞 l Lc;i.:ir1 • National Flag: ꇍ Area: 244,820 square kilorne甘es Population: 62,348.447 lso GB Country: Unit叫 Back Capital: London ForNa•d Continent: EU Reload Tld .uk Currency C创e GBP Save as .. Currency Name Pound Print Phone: 44 Translate to Englisl1 Postal Code Fo『mat: @##@@t@ #@@!@#@! View pag Postal Code Reg鰂'(([.舟苟明勾i {2}\d{2}[A Z]! .�, 'I '¥- ns因ct w1ll1 Firebug Lile Z)\d[A'Z阴阳 ’ (GIROAA)四 ”JSONView • Languages: en-GB,cy-GI � user-Agent Switcher Neigh也ours: IE Edit 图2.1 Inspect element 、 对于浏览器解析而言 ,缺失空白符和格式并无大碍, 但在我们 阅读时则会造 成一 定困难 。 要想更好地理解该表格, 我们将使用 Firebug Lite 扩展。 该扩展适 用于所有浏览器,我们可以通过 https ://getfirebug.com/firebuglite 页面获取到该扩展。 如果愿意的话, Firefox 用户可以安装完整版的 Firebug 24 2.1 分析网页 扩展, 不过Lite 版本己经包含了 我们在本章和第 6 章中所用 到的功能。 Firebug Lite 安装完成后, 可以右键单击我们在抓取中感兴趣的网页部分, 然后在菜单 中选择Ins p ect with Firebug Lite ,如图2.2 所示 。 National Flag: Area: Population. lso: Country: Capital: Continent: Tld: 留罢 、、J HU MM fJ AU MX w m Ha E e H B F R O L民e 盯 !cil-- U叶 阳 nu17k u sa斗 -WH 34 6 K 问Mm d m山 I MF Bd HHU B 川m u nd 缸GU U E .llk Save as. Cu厅ency Code: GBP Currency Name: Pound Phone: Languages: Neighbou「B Edit Prmt... Translate to English View page soume Vi四'/page info en-GB,cy-G Bj 、I User-Agent Switche「 IE Inspect element 图2. 2 此时, 浏览器就会打开如图2.3 所示的 Firebug 面板, 并显示选中元素周 围的 HTML层次结构。 如图2.3 所示, 当选择国 家面积这一属性时, 我们可以从 Firebug 面板中 清晰地看到, 该值包含在 class 为 w2p fw 的<td>元素中, 而<td>元素又是 ID 为 pl aces area ro w 的<tr>元素的子元素。 现在 , 我们就获取 到需要 抓取的面积数据 的所有信息了。 25 第2章数据抓取 拼 Inspect cons脚1可m Script DOM 百飞际aa> 臼 <body data -f<.>edly- mini;" yes"> 臼<div class="navbar navb盯- inve rse”〉 曰<div class="container、 日<section id=可nain" class=阴阳in row与 日<div class=· span旦、 国<form action=”#” enctype=飞ultipart/ fonn­ data” mεthod=’'post”〉 曰<table> 曰<tbody> 图2.3 2.2 三种网 页抓取方法 现在我们已经了解了该网页的结构 , 下面将要介绍三种抓取其中数据的方 法。 首先是正则表达式, 然后是流行的 BeautifulSoup模块, 最后是强大 的lxml 模块。 2.2.1 正则表达式 如果你对正则表达式还不熟 悉, 或是需要一些提示时禽, 可以查阅 https ://docs .pytho日 .org/2/howto/regex . html 获得完整 介绍。 当我们使用 正则表达式抓取面积数据时, 首先需要尝试匹配<td>元素中 的内容, 如下所示。 》〉 工mport re 》> url = ’http://example.webscrapi口g . com/view/Un工ted -Kingdom 239’ 》> html = download (url) >>> re . findall (’<td class=”w2p f w”> ( . *?) 标签。 要想分离出面积属性, 我们可以只选择其中的第二个元素, 如下所示。 》> re . findall (’<td class=”w2p f w”> (.•?) </td> ’, html) [l] ’244,820 square kil。metres ’ 虽然现在可以使用这个方案, 但是如果网页发生变化, 该方案很可能就 会失效。 比如表格发生了变化, 去除了第二行中的 国土面积数据。 如果我 们只在现在抓取数据, 就可以忽略这种未来可能发生的变化。 但是, 如果 我们希望未来还能再次抓取该数据, 就需要给出 更加健壮的解决方案, 从 而尽可能避免这种布局变化所带来的影响。 想要该正则表达式更加健壮 , 我们可以将其父元素< tr>也加入进来。 由 于该元素具有 ID 属性, 所以应 该是唯一的。 》> re . findall (’<tr id=”places area row”><td class=”w2p fl” ><label for=”places area” id=”places area label”>Area :
  • Area
  • Population
  • 同样地, lxml 也可以正确解析属性两侧缺失 的引 号, 并闭合标签 , 不过 该 模块没有额外添加 <ht ml > 和<body>标 签 。 30 2.2 三种网页抓取方法 解析完输入 内容之后, 进入选择元素的步骤, 此时 lxml 有几种不同的方 法, 比如 XPath 选择器和类似 Beautiful Soup的find ()方法。 不过, 在本例 和后续示例中, 我们将会使用 css 选择器, 因为它更加简沽, 并且能够在第 5章解析动态内容时得以复用。 此外, 一些拥有 jQuery选择器相关经验的读 者也会对其更加熟悉。 下面是使用 lxml 的css 选择器抽取面积数据的示例代码 。 》> tree = lxml.html.fromstring(html) 》> td = tree .cssselect (’tr#places_area_r。w > td . w2p_fw ’) [ O ] 》> area = td.text content() 》> print area 244, 820 square kilometres css 选择器 的 关键代码行 己被加粗显示。 该行代码首先会找到 ID 为 place s area r ow 的表格行 元素 ,然后选择 class 为 w2p f w 的表格数据 子标签。 css 选择器 css 选择器表示选择元素所使用的模式。 下面是一些常用的选择器示例。 选择所有标签: * 选择<a>标签:a 选择所有 class=”link”的 元 素 :.link 选择 clas s=”li口k”的 <a>标签:a.link 选择 id= "home” 的 <a>标签:aJfhome 选择父元 素为<a>标签的所有<span>子标签:a> span 选择<a>标签内部的所有<span>标签:aspan 选择 title 属性为”Home” 的所有<a>标签:a[ title=Home ] [� W3 C 己提 出 CSS3 规 范, 其网址为 http : I /www. w3. org / TR I /2011/RE C-cs s3-selecto rs-2 0110 92 9 /。 E Lxml 已经实现了大部分 CSS3 属性, 其不支持的功能可 以参见 https ://pythonhosted.org/cssselect/#supported- selectors。 31 第2章 数据抓取 需要注意的是, 以ml 在内部实现中, 实际上是将 css选择器转换为等价 的 XPath 选择器。 2.2.4 性能对比 要想更好地对本章 中介绍的三种抓取方法评估取舍, 我们需要对其相对效 率进行对比。 一般情况下, 爬虫会抽取网页中的多个字段。 因此, 为了让对 比更加真实, 我们将为本章中的每个爬虫都实现一个扩展版本, 用于抽取国 家网页中的每个可用 数据。 首先, 我们需要回到 Firebug 中, 检查国家页面其 他特征的格式, 如图2.4 所示。 lnsped Console l__!:!!:匹J 1CSS Script DOM 曰 <table> 曰 <tb同ldy> 田<t「 id=“ places_national_ flag̿row" > 囡<t「 id=』'places area_「·ow"> 因<t「 id="places_popullatio11_row与 因<tr id=” places_country _raw刨〉 田《t「 id=” pla仨es ca伊工tal̿row副〉 囚<tr id=” places_co咽ti凹E阳t一「ow" 囚<t「 id=” places一tld一rn制" ' 囚<t「 i.d=‘l pl曰ces_rnr「E盯cy_cocie一『口w” 〉 曰<t「 id=’l places_ rnr扫ency_问吕匹e一『ow" 〉 [±} 图2.4 � ܌, 从Firebug 的显示中可以看出, 表格中 的每一行都拥有一个 以 places 起始且以 row 结束的 ID。 而在这些行中包含的国家数据, 其格式都和上面 的例子相同。 下面是使用上述信息抽取所有可用国家数据的 实现代码。 32 2.2 三种 网 页抓取方法 FIELDS = (’area’, ’population ’, ’i s。 ’, ’country ’, ’capital ’, ’continent ’, ’tld’ , ’currency code ’, ’currency name ’, ’phone ', ’postal_code_format ',’postal_code_regex’,’ languages ’, ’ ne ighbo urs ’) import re def re scraper (html) : resu lts = { ) for field in FIELDS : results [field ] = re . search (’< tr id=”places_毛 s̿ro w”> .•? <td class=”w2p fw”> < • *? l tr#places 宅S row > td. w2p fw’ 毛 field) [0] .text c。ntent () return results 抓取结果 现在, 我们已经完成了 所有爬虫的代码实现, 接下来将通过如下代码片段, 测试这三种方法的相对性能。 import time NUM_ITERATIONS = 1000 # number of times t。 test each scraper html = d。wnload (’http:/ /example . webscraping.com/places/view/ United-Kingd。m-239’) 33 第 2 章数据抓取 for name , scraper in [(’Regular expressions ’, re_scraper) , (’BeautifulSoup’, bs_scraper) , (’Lxml’, lxml scraper) ]: # rec。rd start time 。f scrape start = time . time () for i in range (NUM ITERATIONS): if scraper == re scraper: re . purge () result = scraper (html) # check scraped result is as expected assert (result [’area’] == ’244,820 square kilometres ’) # record end time of scrape and output the t。tal end = time . time () pri口t ’屯s: 宅.2f seconds ’ 毛 (name , end - start) 在这段代码中, 每个爬虫都会执行1000 次, 每次执行都会检查抓取结果 是否正确, 然后打印总用时。 这里使用的 download 函数依然是上一章中定 义的那个函数。 请注意, 我们在加粗的代码行中调用 了 re .purge ()方法。 默认情况下, 正则表达式模块会缓存搜索结果,为了与其他爬虫的对比更加 公平, 我们需要使用该方法清除缓存 。 下面是在我的电脑中运行该脚本的结果 。 $ python perf。rmance .py Regular expressi。ns : 5.50 seconds BeautifulS。up : 42 .84 sec。nds Lxml : 7.06 sec。nds 由于硬件条件的区别, 不同电脑的执行结果也会存在一定差异。 不过, 每 种方法之间的相对差异应当是相当的。 从结果中可以看出, 在抓取我们的示 例网页时, Beautiful Soup 比其他两种方法慢了超过 6倍之多。 实际上这一结 果是符合预期的, 因 为 lxml 和正则表达式模块都是 C语言编写的 ,而 BeautifulSoup 则是纯 Python 编写的。 一个有趣的事实是, lxml 表现得 和正则表达式差不多好。 由于 lxml 在搜索元素之前, 必须将输入解析为 内 部格式, 因此会产生额外的开销。 而当抓取同一网页的多个特征时, 这种初 始化解析产生的开销就会降低, lxml 也就更具竞争力。 这真是一个令人惊叹 的模块 ! 34 2.2 三种网页抓取方法 2.2.5 结论 表2.1 总结了每种抓取方法的优缺点。 表2.1 抓取方法 性能 使用难度 安装难度 正则表达式 快 困难 简单(内 置模块) Beautiful Soup ↑曼 简单 简单(纯P归hon) Lxml 快 简单 相对困难 如果你 的爬虫瓶颈是下载网页, 而不是抽取数据的话,那么使用较慢的方 法(如 Beauti ful Soup ) 也不成 问题。 如果只需抓 取少量数据 , 并且想要避免 额外依赖 的话, 那么 正 则表达式可 能更加 适合 。 不过, 通常情况下, lxml 是 抓取数据的最好选择, 这是因为该方法既快速又健壮, 而正则表达式和 Beauti ful Soup只在 某些特定场景下有用。 2.2.6 为链接爬虫添加抓取回调 前面我们已经了解了如何抓取国家数据, 接下来我们需要将其集成到上 一章的链接爬虫当中。 要想复用这段爬虫代码抓取其他网站, 我们需要添 加一个 callback 参数处理抓取行为。 callback 是一个函数, 在发生某 个特定事件之后会调用该函数 (在本例中, 会在网 页下载完成后调用 ) 。该 抓取 callback 函数包含 url 和 html 两个参数, 并且可以返回一个待爬 取的 URL 列表。 下面是其实现代码, 可以看出在 Python 中实现该功能非 常简单。 def link_crawler ( ... , scrape_callback=None): links = [] if scrape_callback : links . extend (scrape_callback (url , ht皿1) 。r [ ] ) 35 第 2 章数据抓取 在上面的代码片段中,我们加粗显示了新增加的抓取 callback 函数代码。 如果想要获取该版本链接爬虫的完整代码, 可以访问 https ://bitbucket . org/wswp/code/src/tip/chapter02/link crawler.py。 现在, 我们只 需对传入的 scrape callback 函数定制化处理, 就能使 用该爬虫抓取其他网站了。 下面对 lxml 抓取示例的代码进行了修改, 使其 能够在 callback 函数中使用 。 def scrape callback (url, html) : if re . search (’/view/ ' , url) : tree = lxml .html . fromstring (html ) row = [tree . cssselect (’table > tr#places 告s row > td.w2p fw ’ 屯 field) [ 0] . text content () for field in FIELDS] print url, row 上面这个 callback 函数会去抓取国家数据, 然后将其显示出来。 不过 通常情况下, 在抓取网站时, 我们更希望能够复用这些数据, 因此下面我们 对其功能进行扩展, 把得到的结果数据保存到 csv 表格中, 其代码如下所示。 import csv class ScrapeCallback: def init (self) : self. writer = csv. writer {。pen (’countries . csv’, ’w’)) self. fields = (’area’, ’population ’, 『iso’, ’country’, ’capital’, ’continent’, ’tld’ , ’currency_code ’, ’ currency_name ’,’ phone ’,’ postal_code_format ’, ’postal code regex ’, ’languages ’, ’neighbours ' ) self.writer. writerow (self. fields) def call (self, url, html) : if re . search (’/view/ ’ ,url) : tree = lxml .html . fr。mstring (html) row = [] for field in self.fields : row . append (tree . cssselect (’table > tr#places {} row > 36 td .w2p fw ’. format (field) ) [ 0 J • text content () ) self .writer . writerow (row) 2.2 三种网页抓取方法 为了实现该 callback,我们使用 了回调类, 而不再是回调函数, 以便保 持csv 中 writer 属性的状态。 csv 的 writer 属性在构造方法中进行了实 例化处理, 然后在 call 方法中执行了多次写操作。 请注意, call 是一个特殊方法, 在对象作为函数被调用时会调用该方法, 这也是链接爬虫中 cache callback 的调用方法。也就是说, scrape callback (url, html) 和调用 scrape callback. call (url, html )是等价的。 如果想要了 解更多有关 Python 特殊类方法的知识, 可以参考 https ://do cs .pytho n. org/2/reference/datamo del.html#special-metho d-names 。 下面是向链接爬虫传入回调的代码写法。 link crawler (’http : //example .webscraping.com/ ’, ’/(工ndex lview) ’, max depth=-1, scrape callback=ScrapeCallback () ) 现在, 当我们运行这个使用 了 callback 的爬虫时, 程序就会将结果写 入一个 csv 文件中, 我们可以使用类似 Excel 或者 LibreOffice 的应用查看该 文件, 如图 2.5 所示。 图2.5 37 第 2 章数据抓取 成功了 ! 我们完成了 第一个可以工作的数据抓取爬虫。 2.3 本章小结 在本章中, 我们介绍了几种抓取网页数据的方法。 正则表达式在一次性数据 抓取中非常有用, 此外还可以避免解析整个网页带来的开销 : BeautifulSoup 提供了 更高层次的接口, 同时还能避免过多麻烦 的依赖。 不过, 通常情况下, lxml 是我们的最佳选择, 因为它速度更快, 功能更加丰富, 因此在接下来的例 子中我们将会使用 lxml 模块进行数据抓取。 下一章, 我们会介绍缓存技术, 这样就能把网页保存下来, 只在爬虫第一 次运行时才会下载网页。 38 第3章 下载缓存 在上一章中, 我们学习了如何从己爬取到的网页中抓取数据, 以及将抓 取结果保存到表格中。 如果我们还想抓取另外一个字段, 比如国旗图片的 URL, 那么又该怎么做呢 ? 要想抓取这些新增的字段, 我们需要重新下载整 个网站。 对于我们这个小型的示例 网站而言, 这可能不算特别大的问题。 但 是, 对于那些拥有数百万个网页的网站而言, 重新爬取可能需要耗费几个星 期的时间。 因此, 本章提出了对已爬取网 页进行缓存的方案, 可以让每个网 页只下载一次。 3.1 为链接爬虫添加缓存支持 要想支持缓存, 我们需要修改第 1章中编写的 download 函数, 使其在 URL 下载之前进行缓存检查。 另外, 我们还需要把限速功能移至函数内部, 只有在真正发生下载时才会触发限速, 而在加载缓存时不会触发。 为了避免 每次下载都要传入多个参数, 我们借此机会将 download 函数重构为一个类, 这样参数只需在构造方法中设置一次, 就能在后续下载时多次复用 。下面是 支持了缓存功能的代码实现。 class Downloader: def ̿init_ (self, delay=S, 第 3 章下载缓存 user_agent= ’wswp’, proxies=None, num retries=!, cache=N。ne): self.throttle = Throttle (delay) self.user_agent = user_agent self. proxies =proxies self. num retries = num retries self. cache = cache def call (self, url) : result = None if self. cache : try: result = self.cache [ url] except KeyError: t url is not available in cache pass else: if self.num retries > 0 and \ 500 <= result [’code ’ l < 600: t server error so ignore result from cache t and re-download result = N。ne if result is N。ne : # result was not loaded from cache # so still need to download self. throttle .wait (url) proxy = random .choice (self. proxies) if self.proxies else None headers = {’User-agent ’: self. user_agent} result = self. download (url, headers, pr。xy, self. num retries) if self. cache : # save result to cache self. cache [ url] = result return result [’html’] def download (self, url, headers, proxy, num_retries, data=None) : return {’html’: html , ’c。de’: code } 40 3.1 为链接爬虫添加缓存支持 下载类 的 完整源码可 以从 https ://b itbuc ke t . org/ l wswp/ code/src/ tip/ chapte r03/dowpl oader .py I 获取。 _J 前面代码 中的 Downl oad 类有一个 比较有 意思的部分, 那就是 cal l 特殊方法 , 在该方法 中我们实现了 下载前检查缓存的功能 。 该方法首先会检 查缓存是否 已经定义。 如果 已经定义 , 则检查之前是否 已经缓存了该 URL 。 如果该 URL 己被缓存 , 则检查之前的下载中 是否遇到 了 服务端错误。 最后 , 如果也没有发生过服务端错误, 则表明 该缓存结果可用 。如果上述检查 中的 任何一项 失败 , 都需要正常下载该 URL , 然后将得到 的结果添加 到缓存中 。 这里 的 download 方法和之前的 download 函数基本一样, 只是在返回下载 的HTML 时额外返回了 HTTP 状态码, 以便在缓存中存储错误码。 当然, 如 果你只 需要一个简 单的下载 功能, 而不需要 限速或缓存 的 话, 可以直接调用 该方法, 这样就不会通过 call 方法调用了。 而对于 cache 类, 我们可以通过调用 result = cache [url] 从 cache 中加载数据, 并通过 cache [url] =result 向 cache 中保存结果。 大家 应该很熟悉这种便捷的接口写法, 因为这也是 ηthon 内建字典数据类型的使 用方式。 为了支持该接口 ,我们的 cache 类需要定义_getitem一()和 setitem () 这两个特殊 的 类方法 。 此外, 为 了支持缓存功能, 链接爬虫的代码也需要进行一些微调,包括添 加 cache 参数、 移除限速以及将 download 函数替换为新的类等, 如下面的 代码所示。 def link crawler ( ... , cache=None): crawl_queue = [seed_url] seen = { seed url: 0 } num urls = 0 rp = get_rob。ts(seed_url) D = Downloader (delay=delay, user_agent=user_agent, proxies=proxies, num retries=num_retries, cache=cache) 41 第 3 章下载缓存 while crawl queue : url = crawl_queue . pop () depth = seen [url] # check url passes robots .txt restrictions if rp . can_fetch (user_agent, url) : html = D (url) links = [] 到目前为止, 这个网络爬虫的基本架构已经准备好了, 下面就要开始构建 实际的缓存了。 3.2 磁盘缓存 要想缓存下载结果,我们先来尝试最容易想到的方案, 将下载到的网页存 储到文件系统中。为了实现该功能, 我们需要将 URL 安全地映射为跨平台的 文件名。 表3.1 所示为几大主流文件系统的限制。 表3.1 操作系统 文件系统 非法文件名字符 文件名最大长度 Linux Ext3尼xt4 /和\0 255 字节 os x HFS Plus :和\0 255 个UTF-16编码单元 Windows NTFS 飞、 /、 ?、:、气”、〉、 〈和| 255 个字符 为了保证在不同文件系统中, 我们的文件路径都是安全的, 就需要限制其 只能包含数字、 字母和基本符号, 并将其他字符替换为下划线,其实现代码 如下所示。 》> import re 》> url = ’http://example. webscraping. c。m/default/view/ Australia- 1’ 》> re . sub (’[《/0-9a-zA-Z\一., ; ]’, ” , url) ’http一//example.webscraping . com/default/view/Australia-1 ’ 42 3.2 磁盘缓存 此外, 文件名及其父 目录的长度需要限制在 255 个字符以内 ( 实现代码如 下 ) ,以满足表 3.1 中给出的长度 限制。 》> filename = ’/’ .join ( segment [ :255] for segment in filename.split (’/’)) 还有一种边界情况需要考虑, 那就是 URL路径可能会以斜杠 (/) 结尾, 此时斜杠后面的空字符串 就会成为一个非法的文件名 。但是, 如果移除这个 斜杠, 使用其父字符串作为文件名 , 又会造成无法保存其他 URL 的问题。 考 虑下面这两个 URL: • http : I I example . web scraping. com/ index/ • http: I /example . webs craping. com/index/1 如果我们希望这两个 URL 都能保存下来, 就需要以 index 作为 目录名, 以1作为子路径。 对于像第一个 URL 路径这样以斜杠结尾的情况, 这里使用 的解决方案是添加 index. html 作为其文件名。 同样地, 当URL 路径为空 时也进行相同的操作。 为了解析 URL, 我们需要使用 urlparse . urlspli t () 函数, 将URL分割成几个部分。 》> import urlparse 》> compo口ents = urlparse. urlsplit (’http:/ /example . webscraping.com/index/’) 》> print components SplitResult ( scheme=’ http’, netloc= ’ example . webscraping.com’, path= ’/index/ ’, query= ”, fragment= ”) 》> print components.path ’/index/’ 该函数提供了 解析和处理 URL 的便捷接口。 下面是使用该模块对上述边 界情况添加 index. html 的示例代码。 》> path = compo口ents . path 》> if not path: 》> path = ’/index. html ’ 》> elif path . endswith(’/’): 43 第 3 章下载缓存 》> path += ’index .html ' 》> filename = compo口e口t s.n etloc + path + compone nts .query 》> fi lename ’example . webscraping. com/index/index .html ’ 3.2. 1 实现 上一 节中, 我们介绍 了创建基于磁盘 的缓存时需要考虑 的文件 系统限制, 包括允许使用 哪些字符 、文件名 长 度限制, 以及确保文 件和 目录的 创建位置 不同。 把URL 到文件名 的这些映射逻辑结合起 来, 就形成 了 磁盘缓存 的主要 部分。 下面是 DiskCache 类的初始实现代码。 import os import re import urlparse class DiskCache : def init (self, cache dir= ’ cache ’): self.cache dir = cache dir self.max length =max le口gth def url to path (self, url) : ”””Create file system path for this URL ’ ’ ’ , ’ components = urlparse. urlsplit (url) # append index. html to empty paths path = compone口ts.path if not path : path = ’/index .html ’ elif path. endswith(’/’): path += ’index .html ’ filename = components.netloc + path + compone口ts.query # replace invalid characters filename = re . sub (’ [ " /0- 9a- zA- Z\一. ,; ]’, ”, filename ) # restrict max imum number of characters filename = ’/’. join ( segme口t [ :255] for segment in filename . split ( ’/’)) return os .path.join (self .cache_di r, filename ) 44 3.2 磁盘缓存 在上面的代码中, 构造方法传入了一个用于设定缓存位置的参数,然后在 u rl to path 方法 中应用 了前面讨论 的 文件名 限制 。 现在 , 我们还缺少根 据文件名存取数据的方法, 下面的代码实现了 这两个缺失的方法。 e h ec --ea ke e-K -l s p· - D ࠍES OE pa m- -l c def _getitern_(self, url) : ”””Load data from disk for this URL path = self.url_to_path ( url) if 。s.path .exists (path) : with 。pen (path, ’rb’) as fp : return pickle .load (fp) else: # URL has n。t yet been cached raise KeyError (url + ’ does not exist ’ ) def setitern (self, url, result) : ”””Save data to disk for this url path = self.url_to_path (url) folder = os .path . dirnarne (path) if n。t os .path.exists (folder) : os.rnakedirs (folder) with open (path, ’wb ’) as fp : fp . write (pickle. durnps (result) ) 在 一 setitern () 一 中, 我们使用 u r l _t o _p a th () 方法将 URL 映射为 安全文件名 , 在必要情况下还需要创建父 目录。 这里使用的 pickle 模块会 把输入转化为字符串 , 然后保存到磁盘中 。 而在_ ge tite m ()方法中 , 首 先将 URL 映射 为 安全文件 名。 然后 , 如果文件存在 , 则加载其内容, 并执行 反序 列化, 恢复其原 始数据类型 : 如果文件不存在 , 则说明缓存中还没有该 URL 的数据 ,此时会抛出 KeyError 异常。 45 第 3 章下载缓存 3.2.2 缓存测试 现在 , 我们 通过 向爬虫传递 ca che 回调 , 来检验 D iskCa che 类。 该类 的完整源代码可 以从 https ://bitbucke t. org/wswp/ code/src/tip/ chap te r03 /disk ca che .py 获取 。 我们 可以通过执行如 下脚本 , 使用 链 接爬虫测试磁盘缓存。 $ time python disk_cache .py D。wnloading : http://example . webscraping.com Downloading : http://example . webscraping . com/view/Afghanistan-1 D。wnloading : http:// exm晤le . webscraping . com/view/Zil曲쬌we-252 23m38 . 289s 第一次执行该命令时, 由于缓存为空, 因此网页会被正常下载。 但当我们 第二次执行该脚本时, 网页加载 自缓存中, 爬虫应该更快完成执行, 其执行 结果如下所示。 $ time pyth。n disk_cache .py Om0 .186s 和上面的预期一样, 爬取操作很快就完成了 。当缓存为空时, 我的计算机 中的爬虫下载耗时超过 23 分钟1 而在第二次全部使用缓存时,该耗时只有0.186 秒(比第一次爬取快了超过 7000 倍。。由于硬件的差异, 在不同的计算机中 的准确执行时间也会有所区别。不过毋庸置疑的是, 磁盘缓存速度更快。 3.2.3 节省磁盘空间 为了最小化缓存所需的磁盘空间,我们可以对下载得到的 HTML 文件进 行压缩处理。 处理的 实现方法很简单, 只需在保存到磁盘之前使用 z lib 压 缩序列化字符串即可, 如下面的代码所示。 fp . write (zlib.c。mpress (pickle. dumps (result) )) 46 3.2 磁盘缓存 而从磁盘加载后解压的代码如下所示。 return pickle . loads (zlib . decompress (fp.read())) 压缩完所有网页之后, 缓存大小从 4.4MB下降到 2.3MB, 而在我的计算 机上爬取缓存示例网站的时间是 0.212 秒, 和未压缩时的 0.186 秒相比只是略 有增加。 当然, 如果你的项 目 对速度十分敏感 的话, 也可以禁用压缩功能。 3.2.4 清理过期数据 当前版本的磁盘缓存使用键值对的形式在磁盘上保存缓存,未来无论何时 请求都会返回结果。对于缓存网页而言, 该功能可能不太理想, 因为网页内 容随时都有可能发生变化, 存储在缓存中的数据存在过期风险。 本节中, 我 们将为缓存数据添加过期时间, 以便爬虫知道何时需要重新下载网页。 在缓 存网页时支持存储时间戳的功能也很简单,如下面的代码所示。 from datetime import datetime , timedelta class DiskCacke : def _init一 (self, ... , expires=timedelta (days=30) ): self. expires = expires def getitem (self, url) : ”””L。ad data from disk f。z this URL with open (path, ’rb’) as fp : result, timestamp = pickle. loads (zlib.dec。mpress (fp.read())) if self.has expired(timestamp) : raise KeyError (url +’has expired ’) return result else: it URL has not yet been cached raise KeyError (url + ’ does not exist ’) def setitem (self, url, result) : 47 第 3 章下载缓存 ”””Save data to disk for this url times tamp = datetime . utcnow ( ) data = pickle . dumps ((result, timestamp) ) with open (path, ’wb ’ ) as fp : fp . write (zlib . compress (data) ) def has expired (self, timestamp) : ”””Return whether this timestamp has expired return datetime .utcnow () > timestamp + self .expires 在构造方法中, 我们使用 timedelta 对象将默认过期时间设置为 30 天 。 然后, 在_set一方法中, 把当前时间戳保存到序列化数据中: 而在_get一 方法中, 对比当前时间和缓存时间, 检查是否过期。 为了测试过期时间功能, 我们可以将其缩短为 5秒, 如下所示。 》> cache = DiskCache ( expires=timedelta (sec。nds=S)) 》> url = ’http://example . webscraping .com ’ 》> result = {’html’: ’...’} 》> cache [url] = result 》> cache [url ] {’html ’: ’...’} 》> import time ; time.sleep (S) 》> cache [url] Traceback (most recent call last) : KeyError: ’http://example . webscraping.com has expired’ 和预期一样, 缓存结果最初是可用的, 经过5秒的睡眠之后, 再次调用同 -URL,则会抛出 KeyError 异常, 也就是说缓存下载失效了。 3.2.5 缺点 基于磁盘的缓存系统比较容易实现, 无须安装其他模块, 并且在文件管理 器中就能查看结果。 但是, 该方法存在一个缺点, 即受制于本地文件系统的 限制。 本章早些时候, 为了将 URL 映射为安全文件名 ,我们应用了多种限制, 48 3.3 数据库缓存 然而这又会引发另一个问题, 那就是一些 URL 会被映射为 相同 的文件名。 比 如, 在对如 下几个 URL 进行字符替换之后就会得到相 同的文件名 。 • http ://exarn ple . com/ ?a+ b • http : I I example . corn/ ? a *b • http: I I example . corn/ ?a=b • http ://exarn ple . com/ ?a ! b 这就意味着 , 如果其中一个 URL 生成 了缓存, 其他 3个U RL也会被认 为 已经生成缓存, 因为它们映射到 了同一 个文件名。 另外, 如果一些长 URL 只 在255 个字符之后存在 区别 , 截断后 的版本也会被 映 射为相 同 的文件名。 这 个问题非常重要 , 因 为U虹 的最大长度并没有 明确 限 制。 尽管在实 践中 U 肚 很少会超过 2000 个字 符, 并且早 期版本 的 IE 浏览器也不支持超过 2083 个字 符的 U虹 。 避免这些 限制 的一种解 决方案是使用 URL 的哈希值作 为文件名 。尽管该 方法可 以带来一定 改善 , 但是最终还是会面 临许多文件系统具有 的一个关键 问题, 那就是每个卷和每个 目录下的文件数量是有 限 制的。 如果缓存存储在 FAT 32 文件系统 中, 每个 目 录的最大文件数是 6553 5。 该限制可 以通过将 缓存 分割到 不同 目 录来避免 , 但是文件系 统可存储 的文件总数也是有限制的。 我 使用 的 ext 4 分区 目前支持略多于 1500 万个文件 , 而一个大型 网站往往拥有 超过 1 亿个 网页。 很遗憾 , Di s kCa che 方法想要通用 的话存在太多 限制 。要 想避免这些 问题, 我们需要把多 个缓存 网页合并到一个文件中 , 并使用 类似 B+树 的算法进行索 引 。我们并不会 自 己实现这 种算法 , 而是在下一节 中介绍 己实现这类算法 的数据库 。 3.3 数据库 缓存 为了避免磁盘缓存方案的 己知 限 制, 下面我们会在现有数据库系 统之上创 49 第 3 章下载缓存 建缓存。 爬取时, 我们可能需要缓存大量数据, 但又无须任何复杂的连接操 作, 因此我们将选用 NoSQL 数据库, 这种数据库比传统的关系型数据库更易 于扩展。 在本节中, 我们将会选用 目前非常流行的 MongoDB 作为缓存数据库。 3.3.1 NoSQL 是什么 NoSQL 全称为 Not Only SQL, 是一种相对较新的数据库设计方式。 传统 的关系模型使用 的是固定模式, 并将数据分割到各个表中 。然而, 对于大数 据集的情况 , 数据量太大使其难以存放在单一服务器中, 此时就需要扩展到 多台服务器。 不过, 关系模型对于这种扩展的支持并不够好, 因为在查询多 个表时, 数据可能在不同的服务器中 。相反, NoSQL 数据库通常是无模式的, 从设计之初就考虑了跨服务器无缝分片的问题。 在 NoSQL 中, 有多种方式可 以实现该 目标, 分别是列数据存储 ( 如 HBase) 、键值对存储 ( 如Redis)、 面 向文档 的数据库 ( 如 MongoDB ) 以及图形数据库 (如Neo4j ) 。 3.3.2 安装 MangoDB MongoDB 可以从 https ://www . mo口godb .org/downloads 下载得 到。 然后, 我们需要使用 如下命令额外安装其 Python 封装库。 pip install pymong'。 要想检测安装是否成功,可以使用如下命令在本地启动MongoDB。 $ mong。d -dbpa位l . 然后, 在 Python 中, 使用 MongoDB 的默认端口尝试连接 MongoDB。 》> from pymongo import MongoClient 》> client = MongoClient (’localhost ’, 2 7017 ) 3.3.3 MongoDB 概述 下面是通过 MongoDB 存取数据的示例代码。 50 3.3 数据库缓存 》> url = ’http:/ /example. webscraping. com/view/United-Kingdom-239’ 》> html = ’...’ 》> db = client . cache 》> db . webpage .insert ({’url’: url, ’html’ : html )) Obj ectid (’5518c0644e0c87444cl2a577’) 》> db . webpage .find one (url=url) { u’一id’: Obj ectid (’5518c0644e0c87444cl2a577 ’ ), u’html ’ :u’...’, u’url ’ :u’http://example. webscraping . com/view/United-Kingdom-239’ } 上面的例子存在一个问题, 那就是如果我们对相同的URL 插入另一条不同 的文档时, MongoDB 会欣然接受并执行这次插入操作,其执行过程如下所示。 》> db . webpage .insert ({’url’: url, ’html’: html ) ) 》> db . webpage .find (url=url) .count () 2 此时, 同一 URL下出现了多条记录, 但我们只关心最新存储的那条数据。 为了避免重复, 我们将 ID 设置为 URL,并执行 upsert 操作。 该操作表示 当记录存在时更新记录, 否则插入新记录, 其代码如下所示。 》> db . webpage . update ({’_id’: url), {’♀ set’ : {’html’: html }}, upsert=True) 》> db . webpage . find一0口e ( {’ id’: url } ) { u’_id' : u’http://example. webscraping. com/view/ United Kingdom-2 39’, u’html’: u’ ...’} 现在, 当我们尝试向同一 URL 插入记录时, 将会更新其内容, 而不是创 建冗余的数据 , 如下面的代码所示。 》> new html = ’<html> 图5.1 如果我们右键单击结果部分,使用 Firebug 查看元素 (参见第2章〉, 可 以发现结果被存储在 ID 为“result” 的 div元素中, 如图 5.2所示。 让我们 尝试使用 lxml 模块抽取这些结果,这里用到的知识在第2章和第 3章的 Dow川oader 类中都已经介绍过了。 》〉 工mport lxml.html 》> from downloader 工mport Downloader 》> D = Downloader ( ) 》〉 html = D(’http : //example.webscrap工口g.com/search 『) 》> tree = lxml.html.fromstring (html) 》> tree.cssselect (’div#results a’ ) [ ] 70 Name: IA Page size: 1 0 Sea「ch 윎 Afghanistan E僵 Aland Islands II Albania Console L叮�J CSS Script DO咽 曰<div class="containe「”〉 Algeria Andαra 回<header class="mastheade「 row"" id=“head'e r"" 曰<section id=”main'‘ class="main rm• "> 曰<div class=气pan!2"> 因<fOrll"l" 曰<div 工d="results与 曰<ta'ble> 曰<tbody,. 巳<t� 巳 «td> 日<div> 5.1 动态网页示例 国国 ’‘ ..,.iTl王u�irmr;:霄,虽lihr.1o1I:毫缸,..,,...,_
    自 南<t� 图5.2 这个示例爬虫在抽取结果时失败了。检查网页源代码可以帮助我们 了解抽 取操作为什么会失败 。 在源代码中, 可 以发现我们准备抓取的 div 元素实际 上是空的, 如下所示。
    71 第 5 章动态内容 而 Firebug显示给我们的却是网页的当前状态, 也就是使用 JavaScript 动 态加载完搜索结果之后的网页。 下一节中, 我们将使用 Firebug的另一个功能 来了解这些结果是如何加载的。 什么是 AJAX AJAX 是指异步 JavaScript 和 XML ( Asynchronous JavaScript and X孔1L ) ,于2 005 年引入, 描述 了 一 种跨浏 览器动 态生成 Web应 用内容的功 能。 更重要的是 , XML HttpRequ est 㘀 这个 最 初뭯 为ActiveX 实 现 的 JavaScript 对象, 目前已 经得到 大多 数呼 叫器时该技术九许 叫“叫到础服务器的时 请求并获得响应, 也就是说 Web 应用就可以传输和接收数据。 而传统的客户 端与服务端交互方式则是刷新整个网页, 这种方式 的用 户二比较差,并匨只需传输少量数据时栠成带宽浪费。 Google 的 Gmail 和 Google 地图是动态We b应用的早期 实 验者 , 也对 AJAX 成为主流起到 了重要帮助。 5.2 对动态网 页进行逆向工程 到目前为止, 我们抓取网页数据使用的都是第 2章中介绍的方法。 但是, 该方法在本章的示例网页中无法正常运行, 因为该网页中的数据是使用 JavaScript 动态加载的。 要想抓取该数据, 我们需要了解网页是如何加载该数 据的, 该过程也被称为逆向工程。 继续上一节的例子, 在 Firebug 中单击 Console 选项卡,然后执行一次搜索, 我们将会看到产生了一个 AJAX 请求, 如图 5.3 所示。 这个 AJAX 数据不仅可以在搜索网页时访问到, 也可以直接下载, 如下 面的代码所示。 >>> html = D(’http:/ /example. webscraping .com/ajax/ 72 5.2 对动态网页进行逆向工程 search .json?page=O&page size=lO&search term=a ’) Name: r� Pa!ge s i:ze: 10 ’ Sea「ch Afghanistan 纣 Aland Islands - Albania Algena C! American Andorra - A叩la 疆. A吨ui阳 � lnsped Cle却 �r HTML css Script DOM 国国国 曰 CET 闭� aocfsearc:h.json?'&se.arch_term=a& page�size剖10&,pa�e:;;O 2on OK 晶 Parnms Headetrs R导sponse JSO'N { .. 「eco「ds '": [{回 P「erty_linlk. " :啊<Cll iv> Afgha,nist:al"I</ 画></div>” , ··仨ount:1ry " : “ Afg1hanista 阳 ”, H id锢’ : 1261}. {"p「et ty_li.nk”: ”<div>
    二 Ȁ一 - 》>· 图5.3 AJAX 响应返回的数据是JSON 格式的,因此我们可以使用 Python 的 j son 模块将其解析成一个字典,其代码如下所示。 73 第5章 动态内容 》> import j s。n 》> json . loads (html ) { u’error ’ :u”, u’num pages ’: 22, u’records ’: [ { u’country’: u’Afghanistan ’ , u’id’ : 1261, u’pretty_link’: u’<div>= ajax[’口um pages ’]: break open (’countries .txt’, ’w’) .write (’\n’ .join ( sorted ( countries))) 这个 AJAX 接口 提供的抽取国家信息的方法, 比第2 章中介绍的抓取 方法更简单。 这其实是一个日常经验: 依赖于AJAX 的网 站 虽然乍看起 来 更加复杂 ,但是其结构促 使数据和表现层 分离,因此我们在抽取数据时会 更加容易 。 5.2.1 边界情况 前面的 AJAX 搜索 脚本非常简单, 不过我们还可以利用一些边界情况 使 其进一步简化。 目前, 我们是针对每 个字母 执行查询操作的, 也就是说我们 需要26 次 单独 的查询, 并且这些查询结果又 有很多重复。 理想情况下, 我们 可以使用一次搜索 查询就能匹配所有结果。 接下来, 我们将尝试使用不同字 符来测试这种想法是否可行。 如果 将搜索 条件置为空,其结果如下。 》> url = ’http:/ /example . webscraping.com/ajax/ search .json?page=O&page_size=lO&search term= ’ 》> json . loads (D (url ) ) [’n um page s ’] 。 很不幸,这种方法并没有奏 效,我们没有得到返回结果。下面我们再来尝 试’ 申’ 是否能够匹配所有结果。 》> json . loads (D(url +’* ’)) [’num_pages ’ ] 。 75 第 5 章动态内容 依然没有奏效。 现在我们再来尝试下 ’·’, 这是正则表达式里用于匹配所 有字符的元字符。 》> json . loads (D(url +’· ’)) [’num_pages’] 26 这次尝试成功了, 看来服务端是通过正则表达式进行匹配的。 因此, 现在 可以把依次搜索每个字符替换成只对点号搜索一次了。 此外, 你可能已经注意到在 AJAX 的URL中有一个用于设定每个页面显 示国家数量的参数。 搜索界面中包含 4、10、 2。 这几种选工页 ,其中默认值为 10。 因此, 提高每个页面的显示数量到最大值 , 可以使下载次数减半。 》> url = ’http://example .webscraping .com/ajax/ search.json?page=O&page size=20 &search_term= .’ 》> json . loads (D(url) ) [『num pages ’] 13 那么, 要是使用 比网页界面选择框支持的每页国 家数更高的数值又会怎 样呢? 》> url = ’http:/ /example . webscraping.com/ajax/ search.json?page=O&page_size=lOOO&search_term= .’ 》> j son . loads (D (url) ) [’num pages ’ ] 1 显然, 服务端并没有检查该参数是否与界面允许的选项值相匹配,而是直 接在一个页面中返回了所有结果。许多 Web 应用不会在 AJAX 后端检查这一 参数, 因为它们认为请求只会来 自 Web 界面。 现在, 我们手工修改了这个 URL, 使其能够在一次请求中 下载得到所有 国家的数据。 进一步简化之后, 抓取所有国家信息的实现代码如下。 FIELDS = ( ' area ’,’population ’ , ’iso’,’ country’,’ capital ’,’ continent ’, ’tld’,' currency_ code ’, ’ currency name ’,’ phone ’,’ postal_code_format ’ , ’postal_c。de_regex’,’ languages ’,’ neighbours ’) 76 5.3 渲染动态网页 writer = csv. writer (open (’countries .csv’ , ’w’)) writer. writerow (FIELDS) html = D('http://example . webscraping.com/ajax/ search.json?page=O&page size=lOOO&search term=. ’) ajax = json . loads (html) for record in ajax[’ records ’]: row = [ record [ field] for field in FIELDS] writer. writerow (row) 5.3 渲染动态网页 对于搜索网页这个例子,我们可以很容易地对其运行过程实施逆向工程。 但 是, 一些网站非常复杂, 即使使用类似 Firebug这样的工具也很难理解。比如, 一个网站使用 Google Web Toolkit ( GWT) 开发, 那么它产生的 JavaScript 代码 是机器生成的压缩版。生成的JavaScript代码虽然可以使用类似JSbeautifier 的工具进行还原, 但是其产生的结果过于元长, 而且原始的变量名也 已经丢 失, 这就会造成其结果难以处理。 尽管经过足够的努力, 任何网站都可以被 逆向工程, 但我们可以使用浏览器渲染引 擎避免这些工作,这种渲染引擎是 浏览器在显示网页时解析 HTML、 应用 css 样式并执行 JavaScri严 语句的部 分。 在本节中, 我们将使用 WebKit 渲染引擎, 通过 Qt 框架可以获得该引擎 的一个便捷 Python 接口。 可 什么是 Web.Kit? WebKit 的代码源 于1998 年的 KHTML 项目, 当时它是 Konqueror 浏览器的渲染引擎。 2001 年, 苹果公司将该代码衍 生为 WebKit,并应用于 Safari 浏览器。 Google 在 Chrome 27 之前的版本也使用了WebKit 内 核, 直到 2013 年转向利用 WebKit 开发的 Blink 内核。 Opera 在 2003 年到 2012 年使用的 是其 内部的 Presto 渲染引擎 ,之后切换到 WebKit,但是不久 又跟随Chrome 转向 Blink. 其他主流渲染引擎还包括 IE 使用 的 Trident 和 Firefox 的 Gecko. 77 第5章 动态内容 5.3.1 PyQt 还是 PySide Qt 框架有两种可以使用的 Python 库, 分别是 PyQt 和 PySide 。 PyQt 最初于 1998 年发布, 但在用于商业项 目 时需要购买许可。 由于该原因, 开发 Qt 的公司 ( 原先是诺基亚, 现在是 Digia) 后来在 2009 年开发了另一个 Python 库 PySide ,并且使用 了更加宽松 的 LGPL 许可。 虽然这两个库有少许区别,但是本章中的例 子在两个库中都能够正常工 作。 下面的代码片段用于导入已安装的任何一种 Qt 库。 try: from PySide . QtGui import * from PySide . QtCore import * from PySide . QtWebKit imp。rt * except ImportErr。r: from PyQt4 . QtGui imp。rt * from PyQt4 . QtCore import * from PyQt4 . QtWebKit import * 在这段代码中 ,如果 PySide 不可用 , 则会抛出 ImportError 异 常, 然后导入 PyQt 模块 。 如果 PyQt 模块也不可用, 则会抛出另-个 工 mp ortE rror 异 常, 然后退 出脚本。 qt-proje ct . org/wiki/Setting_up_PySide 和 http://pyqt .sourceforge .net/Docs/ PyQt4/ installation . html. 5.3.2 执行 JavaScript 为了确认 WebKit 能够执行 JavaScript,我们可以使用位于 http://example . webscraping.com/dynamic 上的这个简单示例。 78 5.3 渲染动态网页 该网页只是使用 JavaScript 在 div 元素中写入了 Hello World。 下面是 其源代码。
    document . getElementByid(”result ”) • innerText = ’Hello World ’; 使用传统方法下载原始 HTML 并解析结果时, 得到的 div 元素为空值, 如下所示。 》> url = ’http://example. webscraping. com/dynamic’ >>> html = D(url) 》> tree = lxml .html . fromstring (html) >>> tree .cssselect (’#result ’) [ 0 J • text content () 下面是使用 WebKit 的初始版本代码, 当然还需事先导入上一节提到的 PyQt 或 PySide 模块。 》> app = QApplication ( [ J) >>> webview = QWebView () 》> loop = QEventLoop () >>> webview.loadFinished.co口nect (loop .quit) 》> webview.load (QUrl (url) ) >>> loop .exec () 》> html = webview. page () .mainframe () . toHtml () 》> tree = lxml .html . fromstring (html) 》> tree .cssselect (’#result ’) [OJ . text content () ’Hello World’ 因为这里有很多新知识, 所 以下面我们会逐行分析这段代码。 · 第一行初始化了 QApplication 对象, 在其他 Qt 对象完成初始化之 前, Qt 框架需要先创建该对象 。 79 第 5 章动态内容 ·接下来, 创建 QWebView 对象, 该对象是 Web 文档的容器 。 ·创建 QEve ntLoop 对 象, 该对象用 于创建本地事件循环 。 • QWebView 对象的 loadFinished 回调连接了 QEventLoop 的 qui t 方法, 从而可以在网页加载完成之后停止事件循环。 然后, 将 要加载的 URL 传给 QWebViewo PyQt 需要将该 U肚 字符串封装在 QUrl 对象当 中, 而对于PySide 来说则是可选工页 。 ·由于 QWebView 的加载方法是异步的, 因此执行过程会在网页加载 时立即传入下一行。 但我们又希望等待网页加载完成, 因此需要在事 件循环启动时调用 loop .exec ()。 ·网页加载完成后, 事件循环退出, 执行过程移到下一行, 对加载得到 的网页所产生的 HTML 进行数据抽取。 · 从最后一行可以看出, 我们成功执行了 JavaScript, div 元素果然抽 取 出了 Hello Worldo 这里使用的类和方法在 C++的 Qt 框架网站中都有详细的文档, 其网址为 http ://qt-project . org/doc/qt-4.8/。虽然 PyQt 和 PySide 都有其 自身的文档, 但是原始 C++版本的描述和格式更加详尽, 一般的 Python 开发 者可以用它替代。 5.3.3 使用 WebKit 与 网站交互 我们 用于测试 的搜索 网页需要用户修改 后提交搜索 表单 , 然后单击页面链 接。 而前面介绍 的浏览器渲染 引擎只 能执行 JavaS cri p t ,然后访 问生成的 HTML。要想抓取搜索页面, 我们还需要对浏览器渲染引 擎进行扩展, 使其 支持交互功 能。 幸运 的是, Q t 包含 了 一个非常棒 的 API ,可以选择和操纵 HTML 元素 , 使交互操作变得简单 。 对于之前的 AJAX 搜索示例,下面给出另一个实现版本。 该版本己经将 搜索条件设为 ’ ·’, 每页显示数量设为 ’1000 ’, 这样只需一次请求就能获取 80 到全部结果。 app = QApplicati。n ( []) webview = QWebView () 工。op = QEventLoop () webview.loadFinished. connect (loop.quit) 5.3 渲染动态网页 webview .load (QUrl (’http://exarnple.webscraping .com/search ’)) loop . exec () webview . show () frame = webview. page () .rnainFrarne () frarne .findFirstElement (’#search term ’). setAttribute (’value ’, ’.’} frarne .findFirstElement (’#page_size opti。n : checked’). setPlainText (’1000’ ) frame . findFirstElernent (’#search ’). evaluateJavaScript (’ this . click () ’) app .exec一 () 最开始几行和之前的 Hello World 示例一样, 初始化了 一些用于渲染 网页的 Qt 对象。之后,调用 QWebView GUI 的 show ()方法来显示渲染窗 口 , 这可以方便调试。 然后, 创建了一个指代框架的变量, 可以让后面几行代码更短。 QWebFrame 类有很多与网页交互的有用方法。 接下来的两行使用 css 模式在框 架中定位元素, 然后设置搜索参数。 而后表单使用 evaluateJavaScript () 方法进行提交, 模拟点击事件。 该方法非常实用, 因为它允许我们插入任何 想要的 JavaScript 代码, 包括直接调用网页中定义的 JavaScript 方法。 最后一 行进入应用的事件循环, 此时我们可以对表单操作进行复查。如果没有使用 该方法, 脚本将会直接结束。 图5.4 所示为脚本运行时的显示界面。 1. 等待结果 实现 WebKit 爬虫的最后一部分是抓取搜索结果, 而这又是最难的一部分, 因为我们难以预估完成 AJAX 事件以及准备好国家数据的时间。 有三种方法 可以处理这一问题, 分别是z ·等待一定时间, 期望 AJAX 事件能够在此时刻之前完成: 81 第5章 动态内容 ·重写Qt的网络管理器, 跟踪 URL 请求的完成?间; ·轮询网页, 等待特定内容出现。 Exam1ple web scra1ping website n门vnu nu - AM ’ι p3 鼻M ‘, mm F 袋“,m M川n「 jl Sea rch 盟Z 灿ncJ Isla咄 Albania ·"' Algeria 图 5.4 第一种方案最容易实现, 不过效率也最低 , 因为一旦设置 了安全的超时时 间, 就会使大多数请求浪费大量不必要的时间 。而且, 当网络速度比平常慢 ?, 固定的超时时间会出现请求失败的情况。 第二种方案虽然更加高效, 但 是如果延时出现在客户 端而不是服务端时‘, 则无法使用。 比如, 已经完成下 载, 但是需要再单击一个按钮才会显示内 容这种情况,延时就出现在客户端。 第三种方案尽管存在一个小缺 点, 即会在检查 内 容是否加载完成时浪费 CPU 周期, 但是该方案更加可靠且易于实现。 下面是使用第三种方案的 实现代码 。 82 》> elements = None 》> while not elements: app . processEvents () 5.3 渲染动态网页 elements = frame .findAllElements (’#results a’) 》> countries = [e. toPlainText () .strip () for e in elements] 》> print countries [u’ Afghanistan ’, u’Aland Islands ’, ... , u’Zambia ’, u’z imbabwe ’] 如上实现中 , 代码不断循环 ,直到国家 链接 出现在 re sults 这个 di v元 素中。每次循环, 都会调用 app . proces sEve口ts (), 用于给 Qt事件循环 执行任务的时间, 比如响应点击事件和更新 GUI。 2. 渲染类 为了提升这些功能后续的易用性, 下面会把使用到的方法封装到一个类 中, 其源代码可以从 http s ://hitbucket . org/wswp/code/src/tip/ chapter05 /browser render.py 获取。 import time class BrowserRender (QWebView) : def init (self, show=True): self. app = QApplication (sys . argv) QWebView. init (self) if show: self.show () # show the browser def download (self, url, timeout=60) : ”””Wait for download to complete and return result ””” loop = QEventLoop () timer = QTimer () timer.setSingleShot (True) timer. timeout . connect (loop . quit) self.loadFinished. connect (l。op . quit) self. load (QUrl (url) ) timer .start ( timeout * 1000) loop .exec () # delay here until download finished 83 第5章 动态内容 if timer.isActive () : # downloaded successfully timer. stop () return self.html () else: # timed out print ’Request timed out : ’+ url def html (self): ”””Shortcut to return the current HTML””” return self.page () . mainFrame () . toHtml () def find (self, pattern) : ”””Find all elements that match the pattern””” return self .page () .mainFrame () . findAllElements (pattern ) def attr (self, patter口 , name , value) : ”””Set attribute for matching elements””” for e in self. find (pattern) : e.setAttribute (name, value) def text (self, pattern , value) : ”””Set attribute for matching elements ””” for e in self.find (pattern) : e.setPlainText (value) def click (self, pattern) : ”””Click matching elements ””” for e in self. find (pattern) : e . evaluateJavaScript ( ”this . click () ”) def wait load (self, pattern, timeout=60) : ”””Wait until pattern is found and return matches ””” deadline = time . time () + timeout while time .time () 国<td c\ass= "w2p_fw"'’ 副司「 id="subllit rec ord r剧与 需/tbody’ Iii �div styte="disptay: none; 飞 〈且nput name=悦 next ” type·四”hidden•’ value• 相l"i'>
    >> opener = urllib2 . build opener (processor) 》> url = ’http://example . webscraping.com ' 》> html = ope口er.ope口 (url) .read() 要检查 session 是否加载成功, 这次我们无法再依靠登录跳转了。 这时我 们需要抓取产生的 HTML ,检查是否存在登录用户标签。 如果得到的结果是 Logi口, 则说明 session 没能正确加载。 如果出现这种情况, 你就需要确认一 下Firefox 中是否 已经成功登录示例网站。 图6.2 所示为 Firebug 中显示的用户 标签结构。 Cons。le I HT阳L I CSS Seri11t DOM 自《div claso�"nash" s�yle�" d1 sp1 ay : none; ”〉 8
    llJ
    图6.2 Firebug 中显示该标签位于 ID 为 “navbar” 的<ul>标签中, 我们可以使用 第2章中介绍的 lxml 库抽取其中的信息。 》> tree = lxml .html . fromstring (htm工 ) 》> tree .cssselect (’ul#navbar li a’) [OJ .text conte口t () Wel come Test account 本节中的代码非常复杂, 而且只支持从 Firefox 浏览器中加载 session。如 果你想支持其他浏览器的 cookie,可以使用 browsercookie 模块。 该模块 可以通过 pip install browsercookie 命令进行安装, 其文档地址为 https ://pypi .python . org/pypi/browsercookie。 96 6.2 支持内容更新的登录脚本扩展 6.2 支持内 容更新的登录脚本扩展 自动化登录成功运行后,我们还可以继续扩展该脚本,使其与网 站进行交 互 ,更新国家数据。 本节中使用 的代码可以从 https ://bitbucket . org/wswp/code/src/tip/chapter06/edit.py 获取。 如图 6.3 所示, 每个国家页面底部均有一个 Edit 链接。 I Horr仓 Se:i rch 川 | National Flag: � !:dilll‘Z哩 Area: 244,820 square kilometres Population: 62,348,447 lso: GB Country: United Kingdom Capital: Londor11 Continent: EU Tld: .uk Cu厅e门cy Code: GBP Currency Name: Pound Phone: 44 Postal Code Format: @# 古f@ @ I@ ## #@@!@@##@@!@@## #@@!@#@ #@@!@@#@ #@@IGI ROAA Postal C口de Regex: A(([A-Z]ld{2}队-Z]{2})1([A Z]\d{3}(A-Z]{2})!([A­ ZJ{2队d(2}(A.-Z]{2))i([A Z](2孙d{3}[A-Z]{2}Jl([A­ ZJ\d[A司\d(A句。})l([A司{2}\d[A-Z]\d[A之] Languages: Neighbou厄: Edit {2))!(GIROAA))$ en-GB,cy-GB,gd IE 图6.3 在登录情况下, 该链接会指向另一个页面, 在该页面中所有国家属性都可 以进行编辑, 如图 6.4所示。 97 第 6 章表单交互 Na摇onal Fl吨: Area: Population: lso: Country: Cap1饱I: Con四nent: τId: Currency Code: Currency Name: Phone: 噩噩 244820.00 6234制47 GB 1 Un忧回 Kingdom ! London : Eυ I GBP Pound 44 Postal Code Format: : 伊咆@|@棉 #@@晦@#啕@I@@༃咆@ ! 向幽I Code R咿x: r ��([A-ZJ可{2HA-ZK2J>1u凡响{3页A-写{2})j([A-Z]{:可 Langu匈es: , en-GB,cy-GB,gd Neigl田hours: , IE .Update 图 6.4 这里我们编写一个脚本, 每次运行时, 都会使该国家的人 口 数量加 1。 首 先是复用 parse form ()函数, 抽取国家人 口数量的当前值。 》> import login 》> COUNTRY URL = ’http:/ /example.webscraping.com/edit/ United-Ki口gdom-239’ 》> opener = login .login cookies () 》> country html = ope口er.open (COUNTRY URL) • read () 98 》> data = parse_form ( country_html) 》> ppri口t .pprint (data) 6.2 支持内容更新的登录脚本扩展 {’ formkey ’: ’4cf0294d ea71-4cd8-ae2a-43d4ca0d46dd’ , ’formname’ : ’places/5402840151359488’, ’area ’ :’244820.00’, 'capital’: ’Londo口 ’, ’continent’ : ’EU’, ’ country ’: ’ United Kingdom ’, ’currency code 『: ’GBP’, ’cur re口cy 口ame’ : ’Pound’, ’id ’: ’5402840151359488’, ’iso’ :’GB ’, ’languages ’ :’en-GB,cy-GB,gd’ , ’ neighbours ’: ’IE’, ’phone ’: ’44 ’ , ’population’ : ’62348447', ’postal code format ’: ’ @Jt Jt@@I自## Jt@@ I 日日# #自 由I@@H Jt @@ I @Jt@ # 自由 l @@Jt日 Jt@@ I GIROAA’ , ’postal code regex ’: ’"(([A- Z)\\d { 2 } [A-Z] { 2}) I ( [A-Z ) \ \d{ 3 } [A Z ]{2}) I ( [A-Z ]{2 } \ \d{ 2} [A- Z ) { 2 }) I ( [A-Z ] { 2 } \ \d{ 3 ) [A-Z l { 2}) I ( [A-Z l \ \d [A-Z l \ \d [A-Z l { 2 } ) I ( [A-Z l { 2} \ \ d [A-Z)\\d [A-Z]{ 2}) I (G工ROAA) )$’, ’ tld’ : ’ .uk’ } 然后为人口数量加 1, 并将更新提交到服务器端。 》> data [’population 『) = int (data [’populati。口 ’ l) + 1 》> encoded data = urllib . urlencode (data) 》> request = urll工b2 . Request (COUNTRY URL, encoded data) 》> response = opener.open ( request) 当我们再次回到国家页 时, 可以看到人口 数量己经增长到 62,348,448,如 图6.5所示。 National Flag: Area: Population: ‘’”团 结副噩⫛ 244,820 square kilometres 62,348,448 图6.5 99 第 6 章表单交互 读者可以对任何字段随意进行修改和测试, 因为网站所用 的数据库每个小时 都会将国家数据恢复为初始值, 以保证数据正常。本节中使用 的代码可以从 https ://bitbucket . org/wswp/code/src/tip/chapter06/edit .py 获取。 需要注意的是, 严格来说, 本例并不算是网络爬虫, 而是广义上的 网络机器人。 不过, 这里使用的表单技术可以应用于抓取时的复杂表单交 互当中。 6.3 使用 Mechanize 模块实现自动化表单处理 尽管我们的例子现在已经可以 正常运行,但是可以发现每个表单都需要大 量的工作和测试。 我们可以使用 Mechanize 模块减轻这方面的工作, 该模块 提供了 与表单交互的高级接 口 。 Mechanize 可以通过 pip 命令进行安装。 pip install mechanize 下面是使用 Mechanize 实现前面的人口数量增长示例的代码。 >》 import mechanize 》> br 二 mechanize. Browser () 》> br . ope口 (LOGIN URL) 》> br . select form (nr=O) 》> br [’email ’] = LOGIN EMAIL 》> br [’password' ] = LOGIN_PASSWORD 》> respo口se = br. submit () >》 br . open (COUNTRY_ URL) 》> br. select form (nr=O) >》 br [’populatio口 ’] = str (int (br ['population’l) + 1) 》> br . submit () 这段代码比之前的例子要简单得多, 因 为我们不再需要管理 cookie,而且 访问表单输入框也更加容易。该脚本首先创建一个 Mechanize 浏览器对象, 然后定位到登录 URL, 选择登录表单。 我们可以直接向浏览器对象传递名称 100 6.3 使用 Mechanize模块实现自 动化表单处理 和值, 来设置选定表单的输入框内容。 调试时, 我们可以直接调用 br. form, 获取提交之前的表单状态, 如下面 的代码所示。 》> print br. form =Login) (readonly) > =) (readonly) > [。>pti。ns] [arqs ] Available c。mmands : bench Run quick bench皿ark test check Check spider c。ntracts crawl Run a spider 本章中 我们将会使用如下几个命令。 • startproj ect:创建一个新项 目 : • genspider: 根据模板生成一个新爬虫: • crawl: 执行爬虫: • shell: 启动交互式抓取控制台 。 …………… II ] doc .scrapy .org/en/latest/topics/ cornmands . html. 8.2 启动项目 安装好 Scrapy 以后, 我们可以运行 startproject 命令生成该项 目的默 认结构。 具体步骤为: 打开终端进入想要存储 Scrapy 项目的目录, 然后运行 scrapy startproject : DNS l。。kup failed: address ’ www .example . webscraping .c。m ’ not f。und : [Errn。 -5] No address ass。ciated with h。stna皿.e . 和 预期一样, 默认的 爬虫代码运行失败 了 ,这是因为 http ://www . example .web scraping .com 并不存在①。 此外 , 你还会注 意到命令 中有一 个 - s LOG LEVE L =E RROR 标 记 , 这是一个 Scrapy 设置 , 等同于在 settings . py 文件中定义 LOG_LEVEL = ’ ERROR ’。 默认情况下, Scrapy 会在终端上输出所有 日志信息, 而这里是将 日志级别提升至 只显示错误 信息。 ① 译者注: 在本书翻译时, 该域名 己经配置了 DNS 解析, 此时返回结果应该为空, 而不是错误信息。 126 8.2 启动项目 下面的代码更正了爬虫 的起始 URL, 并且设定了要爬取的 网页。 start urls = [ 'http://example . webscraping .com/ ’ ] rules = ( Rule ( LinkExtractor (allow= ’/index/’) , foll。w=True), Rule ( LinkExtractor (allow= ’ /view/ ’) , callback= ’parse item ’ ) 第一条规则爬取索引页并跟踪其中的链接, 而第二条规则爬取国家页面并 将下载响应传给 callback 函数用 于抓取。 下面让我们把日志级别设为 DEBUG 以显示所有信息, 来看下爬虫是如何运行的。 $ scrapy crawl c。untry -s LOG_LEVEL=DEBUG [c。untry] DEBUG : Crawled (200) [country] o:固UG: Crawled (200) <1钮T http://example . w曲scraping .com/扭曲x/1> [country] DEBUG : Filtered duplicate request: - no more duplicates will be sh。wn (see DUPEFILTE飞DEBUG t。 sh。w all duplicates) [c。untry] DEBUG : Crawled (200) [c。untry] DEBUG : Crawled (200) [c。untry] DEBUG : Crawled (200) 输出的日志信息显示, 索引页和 国家页都可以正确爬取, 并且 已经过滤了 重复链接。 但是, 我们还会发现爬虫浪费了很多 资源来爬取每个网页上的登 录和注册表单链接, 因为它们也匹配 rules 里的正则表达式。 前面命令中的 登录 URL 以 口ext=%2Findex%2Fl 结尾 ,也就是 next=/i口dex/l 经过 URL 编码后的结果, 其目的是让服务器端获取用户登录后的跳转地址。要想 避免爬取这些 URL, 我们可以使用规则的 de口y 参数, 该参数同样需要一个 正则表达式, 用于匹配所有不想爬取的 U肚。 下面对之前的代码进行了修改, 通过避免 URL 包含/user/来防止爬取用户登录和注册表单。 127 第 8 章 Scrapy rules = ( Rule ( LinkExtractor (allow= ’ /index/ ’, deny= '/user/ ’), follow=True), Rule ( LinkExtractor (allow= ’ /view/ ’, deny= ’/user/ ’), callback= ’parse item ’) 想要进一步了 解如何使用 该类, 可 以参考其文档 , 网址为 http://doc .scrapy .org/en/latest/topics/ l inkextractors . html. 8.2.3 使用 shell 命令抓取 现在 Scrapy 已经可以爬取国家页面了, 下面还需要定义要抓取哪些数据。 为了帮助测试如何从网页中抽取数据, Scrapy 提供了 一个很方便的命令̍ sr时工,可以下载 URL 并在 P严hon 解释器中给出结果状态。 下面是爬取某 个示例国家时的结果。 $ scrapy shell http ://example . webscraping . com/view/United-Kingdom-239 [ s] Available Scrapy 。,bjects : [s] crawler [s] item {} [s] request <钮T http://example . webscraping .com/view/United-Kingdom-239> [s] resp。nse <200 http://example . w由scraping . com/view/United-Kingdom-239> [s] settings [s] spider [s] Useful sh。rtcuts : [s] shelp () Shell help (print this help) [s] fetch ( re缍。r_url) Fetch request (。z URL) and update local 。bjects [s] view ( resp。nse) View resp。nse in a browser 现在我们可以查询这些对象, 检查哪些数据可以使用 。 In [l] : response .url ’http://example . webscraping. com/view/United-Kingdom-239’ 工n [2] : respo口se .status 200 128 8.2 启动项目 Scrapy 使用 lxml 抓取数据,所以我们仍然可以使用第 2章中用过的 css 选择器。 In [3) : response .css (’tr#places country row td. w2p fw:: text ’) [ {’name' : [u ’ Afghanistan '], ’populati。n' : [u ’29,121, 286’]} "C [scrapy] INFO: Received SIGINT , shutting down gracefully . Send again t。 force [c。untry] INF1。 : Cl。sing spider (shutd1。wn) [country] DEBUG : Crawled (200) (referer : http : //exai.呼le . w'由scraping . com/ ) [c。untry] DEBUG : Scraped fr。m <200 http://example . webscraping . c。m/view/Antigua-and-Barbuda-10> {’name' : [u’ Antigua and Barbuda ’1, ’p。1pulati。n ’: [u’86,754 ’ ] } [c。untry] DEBUG : Crawled (200) (referer: http://example . webscraping .com/ ) [c。untry] DEBUG : Scraped from <200 http://example . webscraping .com/view/Antarctica-9> {’name ’ :[u ’ Antarctica ’1,’ p。1pulation ’:[u’。’]} [c。untry] INF℃ : Spider closed (shutdown) 从上述执行过程可以看出, 我们使用Ac CCtrl+C) 发送终止信号, 然后 爬虫又完成了几个条 目 的处理之后才终止。 想要 Scrapy 保存爬虫状态,就必 须等待它正常结束, 而不能经受不住诱惑再次按下 C创+C 强行立即终止 ! 现 在, 爬虫状态保存在 crawls/country 目录中, 之后可以运行同样的命令 恢复爬虫运行。 132 8.3 使用 Portia 编写可视化爬虫 $ scrapy crawl country -s L。G_LEVEL=DEBUG -s J。,BDIR=crawls/c。untry [c。untry] INFi。 : Resu皿ing crawl (12 requests scheduled) [c。untry] DEBUG : Crawled (200) (referer : http://example . webscraping .com/ ) [c。untry] DEBUG : Scraped frଇm <200 http://example . webscraping . c。,m/view/Anguilla-8> {’na皿e’ : [u ’Anguilla ’ 1, ’p。pulati。n’ :[u’13,254 ’ ]} [c。untry] DEBUG : Crawled (200) (referer : http://example . webscraping .c。,m/ ) [country] DEBUG : Scraped fr。m <200 http://example . webscraping .c。m/view/Ang。la-7> {’name’ : [u ’Ang。la ’],’population’ : [u’13,068 ,161’]} 此时, 爬虫从刚才暂停的地方恢复运行, 和正常启动一样继续进行爬取。 该功能对于我们的示例 网 站而言用处不大,因为要下载的页面数量非常小。 不过, 对于那些需要爬取几个月的大型网站而言, 能够暂停和恢复爬虫就非 常方便了。 需要注意的是, 有一些边界情况在这里没有覆盖,可能会在恢复爬取时产生 问题, 比如 cookie 过期等, 此类问题可以从Scrapy的官方文档中进行详细 了解, 其网址为 http://doc .scrapy.org/en/latest/topics/ jobs .html。 8.3 使用 Portia 编写可视化爬虫 Portia 是一款基于 Scrapy 开发的开源工具,该工具可以通过点击要抓取的 网页部分来创建爬虫, 这样就比手工创建 css 选择器的方式更加方便。 8.3.1 安装 Portia 是一款非常强大的工具, 为了实现其功能需要依赖很多外部库。 由 于该工具相对较新, 因此下面会稍微介绍一下它的安装步骤。如果未来该工 133 第 8 章 Scrapy 具的安装步骤有所简化, 可 以从其最新文档中获取安装方法, 网址为 https ://github .com/scrapinghub/portia#running-portia。 推荐安装方式的第一步是使用 virtualenv 创建一个虚拟Py由on 环境 。 这里我们将该环境命名为 portia examp l e ,当然你也可以将其替换成其他 任何名称。 $ pip install virtualenv $ virtualenv portia二ex缸nple --n。-site-packages $ source p。rtia_example/bin/activate (p。rtia_example)$ cd p。rti8:二example 为什么使用 virtualenv? 假如你的项 目 使用早期版本的 工xml 进行开发,而新版本的 工xml 引入了一些向后不兼容的变史 , 会影响你项 目的正常运 行。 但是, 其他一些项目 准备依赖新版本的 lxml。 如果你 的 项目使用系统 中安装的 lxml,那么 当lxml 更新为 支持其他£ 项目的新版本时 你的项目将会运行失败 Ian Bicking 的 virtua工env 提供 了 一个聪明的办法来解决这 一问题, 那就是复制系统Python 可执行文件及其依赖到一个本 地目录, 创建隔离 的on环境。这样就九许项 目在本地安装 特定的 同咄on 库版本, 独立于外部系统。 详细信息可以查看其 文档, 网址为 https ://virtualenv.pypa.io。 然后,在virtualenv 中安装 Portia 及其依赖。 (p。rtia_ex四ple) $ git cl。ne https ://github . com/scrapinghub/p。rtia (p。rtia_exa皿ple) $ cd p。rtia (p。rti8:二example) $ pip install -r requirements .txt (p。rtia_example)$ pip install -e ./slyb。t Portia 目前处于活跃开发期, 因此在你阅读本书时其接口可能 已经发生变 化。 如果你想使用和本书相同 的版本进行开发, 可以运行如下 git 命令。 134 8.3 使用 Portia 编写可视化爬虫 (p。rtia example)$ git checkout 8210941 如果你还没有安装 git, 可以直接下载 Portia 的最新版 , 其网址为 https ://github . com/scrapinghub/portia/archive /master. zip 。 安装完成后, 可以进入 slyd 目 录运行服务器端来启动 Portia。 (portia example)$ cd slyd (portia_example) $ twistd -n s工yd 如果安装成功, 就可以在浏览器中访问到 Portia 工具, 网址为 http:// localhost:9001/static/main .html。 图8.1 所示为其初始屏幕。 F唱”nisP c:I 。阳’”吗副 图8.1 如果你在安装过程中遇到了问题, 可以查看 Portia 的问题页, 网址为 https ://github . com/scrapinghub/portia/issues,也许其他人已 经经历过相同的问题并且找到了 解决方案。 135 第 8 章Scrapy 8.3.2 标注 在 Portia 的启动页 , 有一个用于输入待抓取网站 URL 的文本框, 比如 http://example . webscrapir可 .com。 输入后, Portia 会在其主面板加载 该网页, 如图 8.2 所示。 Example web scraping website B重建M醉԰向 ㉨hl•" [example . webscraping .com] DEBUG : Scraped from <200 http ://ex四ple . w由scraping .c。m/view/Antarctica-9> {’_template’ :’ 9300cdc044d4b75151044340118ccf4efd976922 ’ , ’ _type’ : u ’ default ’ , u’name ’ :[u ’ Antarctica ’ l I u’p。pul ati。n ’ :[u’ 。’] , ’url’ : ’http://example.webscraping . c。m/view/Antarctica-9 ’ } [example . webscraping .com] DEBUG : Retrying (failed 1 times ): 500 Internal Server Error 这和 “ 优化放置” 小节中遇到的问题是一样的 ,因为Portia 生成的项 目使 用了默认的 Scrapy爬取设置, 导致下载速度过快。 我们仍然可以在设置文件 中修改这些设置 ( 文件位于 da ta/pro j ects /new pro ject /spiders / sett ings . py ) 。不过, 为了演 示一些新方法 , 这次我们改为使用命令 行进 行设置。 (p。rtia_exa呻le)$ p。rtiacrawl portia/slyd/data/projects/newyroject 部呻le . w由scraping . com -s CONαm圆T_R昭JESTS」血气民阳N=l -s DC删LO皿'._DELAY=S [example . webscraping .com] DEBUG : Crawled (200) [ex缸nple .webscraping .com] DEBUG : Crawled (200) 当运行这个放慢速度的爬虫时, 就可以 避免被封禁的问题了。不过, 接下来同样也会遇到下载非必要网页 ( 比如登录和注册页〉这个降低效率 的问题。 默认情况下, Portia 生成的爬虫会爬取给定域名的所有 URL。 要 想只爬取特定 URL, 可以配置右边栏面板中的 Crawling 选项卡, 如图 8.6 所示。 Cor,1.figure follow and exclude patterns .,. 回 民部pect nofollow Follow links that match Ulis P'llrtt程ms 0 Ni智商j 由 圈 圈 llnd辑对 I N叫low pattern Exclude lln'ks that match this patterns 0 ,’1岳母r/ Im |圈I N刷 刷elude pa时 5蛋 白11e·rlay lblock时 !Ink翠 移 图 8.6 这里,我们添加/川dex/和/view/作为爬虫跟踪模式, 并且将/user/ 作为排除模式, 这些都和之前 Scrapy 项目中的用法相似。 如果勾选了底部的 Overlay blocked links 复选框 , Portia 就会把跟踪链接高亮 为绿色, 排除链接 高亮为红色, 如图 8.7 所示。 139 第 8章 Scrapy S网Dlt•ur叼幽팆旬玛翩· Example web scraping website 制削喃’‘ @ 臣。 髟盟 阳,""ଂ .. 锢, 。 p。脚,, 恤h 帆布‘俐’馆的.... ......". 0 。。 因 时呻帕,阳’ …恤...�,,洒。 (囚 a 臼。恻,., .. 础�Rallt. 0 图8.7 8.3.4 检查 结果 现在就可以执行 Portia 生成的爬虫了, 另外和之前一样, 我们使用一output 选项指定输出的 csv 文件 。 {p。rtia_example) $ portiacrawl portia/slyd/data/projects/new_pr。ject example . webscraping .c。m --。utput=countries .csv -s CONCURRENT_REQUESTS_ PER DOMAIN=l -s DOWNLOAD DELAY=S 当运行如上命令时, 该爬虫将会产生和手工创建的 Scrapy 版本相同的 输出。 Portia 是一个非常方便 的与 Scrapy 配合的工具。 对于简单的网站, 使用 Portia 开发爬虫通常速度更快。 相反, 对于复杂的 网站 (比如依赖JavaS cript 的界面〉, 则可以选择使用 Python 直接开发 Scrapy 爬 虫。 140 8.4 使用 Scrapely实现自动化抓取 8.4 使用 Scrapely 实现自 动化抓取 为了抓取标注域, Portia 使用 了 Scrapely 库, 这是一款独立于 Portia 之外 的非常有用 的开源工具, 该工具可 以从 https :// github .co m/ s crapy/ scrape ly 获取 。 Scrapely 使用 训 练数据建立 从网页中抓取哪些 内容的模 型, 并在以后抓取相同结构的其他网页时应用该模型。 下面是该工具的运 行示例。 (portia_example)$ pyth。n 》> fr。m scrapely imp。rt Scraper 》> s = Scraper () 》> train_url = ’http://example . webscraping . cଌm/view/Afghanistan-1’ 》> s.train (train_url , {’name’ : ’Afghanistan ’, ’populati。n ’:’29,121,286’ } } 》> test_url = ’http://ex四ple . webscraping . c。,m/view/United-Kingdom-239 ’ 》> s.scrape (test_url) [ {u’na皿e’ : [u ’ United Kingdom ’ ] I U’populati。n ’: [u ’62,348, 447 ’ ]}] 首先, 将我们想要从 Afghanistan 网页中抓取的数据传给 Scrapely, 本 例中是国家名称和人口数量。 然后, 在另一个国家页上应用该模型, 可 以看 出 Scrapely使用该模型返回了正确的国家名 称和人口数量。 这一工作流允许我们无须知晓网页结构, 只把所需内容抽取出来作为训练 案例, 就可以抓取网 页。 如果网页内容是静态的, 在布局发生改变时, 这种 方法就会非常有用 。例如一个新闻网站, 己发表文章的文本一般不会发生变 化, 但是其布局可能会更新。这种情况下, Scrapely可以使用相同的数据重新 训练, 针对新的网站结构生成模型。 在测试 Scrapely 时, 此处使用的示例网页具有良好的结构, 每个数据类型 的标签和属性都是独立的 ,因此 Scrapely可以正确地训练模型。 但是, 对于 更加复杂的网页, Scrapely 可能在定位内容时失败, 因此在其文档中会警告你 应当 “ 谨慎训练”。 也许今后会有更加健壮的 自 动化爬虫库发布, 不过现在仍 需了解使用第 2章中介绍的技术直接抓取网站的方法。 141 第 8 章 Scrapy 8.5 本章小结 本章首先介绍了网络爬虫框架 Scrapy, 该框架拥有很多能够改善抓取网站 效率的高级功能。 然后介绍了 Portia,它提供了生成 Scrapy 爬虫的可视化界 面。 最后我们试用 了Scrapely , Portia 正是使用该库根据给定模型 自动化抓取 网页的。 下一章中, 我们将应用前面学到的这些技巧来抓取现实世界中的网站。 142 第9章 总结 截止到 目前, 本书介绍的爬虫技术都应用于一个定制网站, 这样可以帮助 我们更加专注于学习特定技巧。 而在本章中, 我们将分析几个真实网站, 来 看看这些技巧是如何应用的。首先我们使用 Google 演示一个真实的搜索表单, 然后是依赖JavaScript 的网站 Facebook, 接下来是典型的在线商店 Gap,最后 是拥有地图接口 的宝马官网。 由于这些都是活跃的网站, 因此读者在阅读本 书时这些网 站存在已经发生变更的风险。 不过这样也好, 因为这些例子的目 的是为了向你展示如何应用前面所学的技术, 而不是展示如何抓取指定网站。 当你选择运行某个示例时, 首先需要检查网站结构在示例编写后是否发生过 改变, 以及当前该网站的条款与条件是否禁止了爬虫。 9.1 Google 搜索引擎 根据第4章中Alexa 的数据 , google.com 是全世界最流行的网站之一, 而 且非常方便的是, 该网站结构简单, 易于抓取。 图 9.1 所示为 Google 搜索主页使用 Firebug加载查看表单元素时的 界面。 第9章 总结 也 是P ℰ Google 国际化版本 Google 可能会根据你的地理位直跳转到指定国家的版本。要想 无论处于世界任何地方, 都能使用 一致的 Google 搜索, 那么 可以使用 Google 的国际化英文版本, 其网址为 http ://www . google . com/口er。 其中, ncr 表示没有国家跳转 (no country redirect )。 Google .. 创始脚画’“ 阳血,碰咱也比例 .0 、, … …U惑: ...... 町毗 e鹏徽阳酣锵 m 一 一一 一 地� …一 lidor. c."11也盔,•tt,t' arli-””据国,. ..ρ”’‘民,,常有伽1 “”。”,· t�··o.俨 鹏飞”“”“,. ,.崎·‘?” ”“阳们ρ㯥䐁 .....叫“1 .. •• r-oL…㍙”.加 .喝'"’”•H每伊..  lHi;•、“" ld•"lffi!;俨》 . ‘"" “M>•飞警f'V'O' " 4” ζ 'ଇ t:•�'榈树 ‘沁悦,崎• I�"' U字”路,γ, 起唱““0$$•"1"t1t输惧、 :.,4jy 创'”’紟阜r l:!l•'"始知、耐创饲"'到··佑LO翅’在甘正由ι峙。“2悉’ “"'' 楠 ’相 ��价 “…1,伪E 4月 4” d•毒”‘mt= 四伏& • la梅’dirJ-..· t吨W 辈U但·‘’将l l) sltii> 伊 叭叭酬⏆且’都Z e、 他 帽” ''",梢””志 (I NJlb <" ” .. 庐”。. 川.yt�·气M‘ ...,,,,’ 施’‘,' di阳"hr"• 也 ,.,,,,,.... ,.,., ... ....帽Uct• IU""'ltt•> . 制” 插d惯也d* Hlh . .,'�“地”细细").ft' _.,. 飞:�-;二;M,.« i;:精盼陆楠精制崛制脚情耀糊钳制静蛐峨精酬’盼悔怖阳帽蝉噩噩 辩阙娟懒畅销镶懈掰楠精树栋佛协制耐椭精明”-归曾于?””啊圈”F阴晴”咽理回哥”’帽”’- 图9.1 可以看到, 搜索查询存储在输入参数 q 当中, 然后表单提交到 action 属性设定的/search 路径。 我们可以通过将 test 作为搜索条件提交给表单对 其进行测试, 此时会跳转到类似 https ://www.google . com/search?q= test&oq=test&es sm=93&ie=UTF-8 的URL 中。 确切的 URL 取决于你 的浏览器和地理位置。 此外, 还需要注意的是, 如果开启 了 Google 实时,那 么搜索结果会使用 AJAX 执行动态加载, 而不再需要提交表单。 虽然 URL 中 包含了很多参数, 但是只有用于查询的参数 q是必需的。 当URL 为 https ://www.google . com/search?q=test 时- , 也能产生相 同的结果, 如图 9.2 所示。 144 9.1 Google 搜索引擎 Google lost W国b N•嘀嘀 Aµ庐· 阳”‘F宫 、!.'idt'!:温 .... ’”- S."11d、 lt1ol” At>-.l1n 2日00的砂,叫出’:0 20 冉回碍’} Speedle•Lnel by Ookl• • The Glolγal Broadband Speed T esl "°""·'"""'"'''""" - s阻。P且L•回 · l!-SICIW田n IUt 'lllNH!-0!18四” 。\005e B!n面9 ”四”’因意 ct go呵ଇ'"幅lly d"pe响。 191"191$ 町咽,回 阶叠 W宵” “四町㋹皇剧”明..,. .....飞1l••t1aooai国h My F警e纠"'但 凶。倒'" """"' """""" Create T esls for Orga.n zal!onal Training and Certificatior hl켂"叫w le•t.""'" -THtcom prଂ剧’•comi>l•恼,ltl\\o'3:电 掣制曲,、阳,....... ɟrie tnt, ,回mMଂ咱 .. ㌌ㅬ...ほ 印制ali1ilc9lltltil:a11011 树啕暗暗 恫uo 102« 阳噜脑11" Peraonal”v Test · 同umanMetrics ,,wwJarmrn’,,, ...四αmt!识萨悦呻lyprJs2.t<叩 · 俨制唰’嗣同 JH.\ 阳'"' �• C J""9 “对ι6现"'均咽,!’阳 院领呻...踶 沪剧恀 阳nm৶ typo 归'"''"田、 QUF"11α""ṧ Tested •W• twod ooml . ···�吧。 .”’υ 7且15 Tw巾d h在.... ",飞'm酬咔700011.rid晴" '民削 7回坷RC ,,...刑1田칽 γ回 8回回o1Com阳'""嗣阳句’去阴雪 ?扭扭 ” Test cricket - Wikipedia, the tre e encyclopedia 凹•oki阳币、.orgl州M汀国Z <.ricJ< el . 消但呜冶tf'言 ’To副口缸”’唱’钮’" "阳.... 剑何贸ma’ ”E句阳阴创 Mci;r.:1 Tll函’ m.il:l:ll'w幡 nm p�iyM l>o":I'"'号。”’'"ἂଇ' r唰V-J..ntu\ vtJ !i\:;.t”痾 ’To.t山㌌,. 肝由制呻唰 ., ..., 图9.2 4蕆 搜索结果的结构可以使用 Firebug 来检查,如图9.3 所示。 ;J;.� '"酬a c. .. �,乓 ”TMLJ吟飞品和 即鹅 。 \千 二附 芝~… .m…-… > "'d•"sror猇”, � V , ........ . 4‘ol> 0 !., , , , , , , , , ] 到目前为止, 我们已经下载得到了 Google 的搜索结果, 并且使用 lxrnl 抽取出其中的链接。 在图 9.3 中, 我们发现链接中的真实网站 URL 之后还包 含了一串附加参数, 这些参数将用于跟踪点击。下面是第一个链接。 》> link = results [O] .get(’href’) 》> link ’/url?q=http ://www.speedtest.net/ &sa=U&ei=nmgqVbgCw&ved=OCB&usg=ACA_cA ' 这里 我们 需 要的内容是 http :// www. s peedt est . 口 et / ,可以使用 urlp arse 模块从查询字符 串中将其解析 出 来。 》> import urlparse 》> qs = urlparse . urlparse (link) .query 》> urlparse.parse_qs(qs) {’q ’: [’http://www .speedtest.net/’ ], ’ei’: [’nmgqVbgCw ’ ], ’ s a' : [’U’ ] , ’usg ’ : [ ’ACA cA '] , 冒ve d’ : [’OC B’ ]} 》> urlpa rse .pa rse qs (qs) .ge t(’q’ , [] ) [’http://www .speedtest.net/ ’ ] 该查询字符串解析方法可以用于抽取所有链接。 》> links = [ l 》> for result in results: 146 9.1 Google 搜索引擎 link = result. get ( 'href’) qs = urlparse. urlparse (link) .query links . extend (urlparse .parse qs (qs) .get(’q ’, [])) 》> links [’http://www.speedtest.net/’, ’https ://www.test. com/ ’, ’http://www.tested. com/ ’, ’http://www .speakeasy.net/speedtest/ ’, ’http:/ /www . humanmetrics . com/cgi win/jtypes2 .asp’, ’ http : //en.wikipedia . org/wiki/Test_cricket ’, ’https ://html5test. com/ ’, ’http://www.16pers。nalities. com/free-personality-test ’, ’https ://www.google . com/webmasters/tools/mobile-friendly/ ’, ’http://speedtest. comcast .net/ ’] 成功了! 从 Google 搜索中得到 的链接 已经被成功抓取出来了。 该示例的 完整源码可以从 http s ://bitbucket .org/wswp/code/src/tip/ chapter09/google .py 获取。 抓取 Google 搜索结果时会碰到的一个难点是,如果你的 IP 出现可疑行为, 比如下载速度过快, 则会出现验证码图像, 如图 9.4 所示。 To continue萝 please type the characters below: M伊拉 I 些些到 About 伽幅 如醉 αIr sysl随ns have d倒四恒d unusual tra胃ic 青田n 归U『 αllTlpU饱『 network. TI苦恼 page d晤E恼to see If It's really you sending ti回 阻qu眉ts and not a r蚀。t WhV did this ABDOQO? 图9.4 147 第9章总结 我们可以使用第 7章中介绍的技术来解决验证码图 像这一问题, 不过更好 的方法是降低下载速度, 或者在必须高速下载时使用代理, 以避免被 Google 怀疑。 9.2 Facebook 目前, 从月活用户数维度来看, Facebook 是世界上最大的社交网络之一, 因此其用户 数据非常有价值。 9.2.1 网站 图9.5 所示为 Packt 出版社的 Facebook 页面, 其网址为 https ://www . facebook.com/PacktPub。 PEOPLE 4,763 ilk出 ABOUT Packt Publish!吨 provides books. eBoaks, video tutorials, •nd a• llcles for IT developers, adm,,,lstrotors, and u白白 htlp•//www.PacktPub cam/ POSTS T。 PAGE • Vladlm’r Chrlstl•n Strnkelj 飞! ’时叭1u q n"m ” H• thethrough Amaz。n.c。”、 sεg Mocala, and th昭 um-friendly tutorial mak时 ,t s;mpler sbll 噩噩E噩ꙩ噩盟军� [����] 注目�lc��v; 凯�A� FREE LEARNING - HELP YOURSELF I PACKT Books PaeloklP•bli•hl吨S阳 ''ଂ ''"'"肝�ଂ·�·... 抱由驯 噎贯Z苦而'哩辛苦糟糟俨泪咱吨叮唁电汽-阳市τ- 国 !吼 ::.. • , , 岳 阳’四 图9.6 。 如W由 当我们与移动端网站进行交互, 并使用 Firebug 查看时, 会发现该界面使 用了和之前相似的结构用于处理 AJAX 事件, 因此该方法实际上无法简化抓 取。 虽然这些 AJAX 事件可以被逆向工程, 但是不同类型的 Facebook 页面使 用了不同的 AJAX 调用, 而且依据我的过往经验, Facebook 经常会变更这些 调用的结构, 所以抓取这些页面需要持续维护。 因此, 如第 5章所述, 除非 性能十分重要 , 否则最好使用浏 览器渲染引 擎执行 JavaScript 事件, 然后访问 生成的 HTML 页面。 下面的代码片段使用 Selenium自动化登录 Facebook, 并跳转到给定页面 的URL。 from selen工um 工mport webdriver def facebook ( username , password, url): driver = webdriver. Firefox () drive r .get ('https ://www .facebook .com’) driver.find_e l ement_by id ( 『 ernai工’).send keys ( usernarne ) driver. find element by id (’pass’) .send keys (password) driver. find element by id (’login form ’) • subrn工t () driver. implicitly wait (30) 149 第 9 章总结 # wait until the search box is available, # which means have successfully logged in search = driver .find_elernent_by_id (’q’) # now logged in so can go to the page of interest driver .get (url) # add code to scrape data of interest here 然后, 可以调用该函数加载你感兴趣的Facebook 页面, 并抓取生成的 HTML 页面。 9.2.2 API 如第 1 章所述, 抓取网站是在其数据没有给出结构化格式时的最末之选。 而Facebook 提供了 一些数据的 API,因此我们需要在抓取之前首先检查一下 Facebook 提供的这些访问是否 己经满足需求。 下面是使用 Facebook 的图形 API 从 Packt 出版社页面中抽取数据的代码示例 。 》> import json, pprint 》> html = D(’http:/ /graph .facebook.com/PacktPub ’) 》> pprint . pprint (json.loads (htrnl) ) { u’about ’: u’Packt Publishing provide s b。oks, eBooks, video tutorials, and articles f。r IT devel。pers, adrninistrat。rs, and users. ’, u’category’ :u’ Product/service ’, u’founded' : u’2004’, u’id ’: u’204603129458 ’, u’likes ’: 4817, u’link ’: u ’ https : //www . facebook. com/PacktPub ’ , u’mission ’: u’We help the world put software to work in new ways, through the delivery 。f effective learning and information services to IT professionals .’, u'narne’: u’Packt Publishing ’, u’talking_about_count ’: 55, u『user口arne’: u’Packt Pub’, u’website’: u’http://www .PacktPub.com’ } 该 API 调用以 JSON 格式返回数据 , 我们可以使用 j son 模块将其解析为 Python 的 diet 类型。 然后, 我们可以从中抽取一些有用的特征, 比如公司 名、 详细信息以及网站等。 150 9.3 Gap 图形API 还提供了很多访问用户数据的其他调用 , 其文档可以从 Facebook 的开发者页面中获取, 网址为 https ://developers .facebook. com/ docs/graph-api。不过, 这些 API 调用多数是设计给与 己授权的 Facebook 用户交互的 Facebook 应用的, 因此在抽取他人数据时没有太大用途。 要想得 到更加详细的信息, 比如用户 日志, 仍然需要爬虫。 9.3 Gap Gap 拥有一个结构化良好的网 站 ,通过 Sitemap 可以帮助网络爬虫定位 其最新的内容。 如果我们使用第 1 章中学到 的技术调研该网站, 则会发现在 http://www.gap .com/robots . txt 这一网址下的 robots.txt 文件中 包含了网站地图 的链接。 Sitemap : http://www.gap .com/products/sitemap_index. xml 下面是链接的 Sitemap 文件中 的内容。 http ://www .gap . com/products/sitemap_l .xml 2015-03-03 http ://www .gap . c。m/products/sitemap一2 .xml 2015-03-03 如 上所示, Sitem ap 链接 中 的内容 仅仅是索 引, 其中又包含 了其他 Sitem ap 文件 的链接 。 其他 的这些Si temap 文件 中则包含 了 数千种产 品类 目的链接, 比如 http ://www .gap.com/products/blue-long-sleeve­ shirts- for -men .jsp ,如图 9.7 所示 。 151 第9章总结 · ·-面口I ©1 阳自由时ING "" "" '"o••• ov•• S>o SM1 '5怜-t-BMW i find 阳rtners BMW partner in your area. Find and manage your pe陌onal BMW pa此neι You c阳 蜡arch BMW dealer al一回 stme up lo throe BMW pa民ne.- found In Y""' My BMW ac<»-SS unless y阳 画re a 盼91.rorcd u皿r a MyWi陪h U引 L Sew四 㐍 S制 图9.9 因 使用 Firebug, 我们会发现搜索触发了如下 AJAX 请求。 https ://c2b-serv工ces . bmw .com/c2b localsearch/services/api/v3/ clients/BMWDIGITAL DLO/DE/ po工s?country=DE &category=BM&maxRe sults=99&la口guage=e口& lat=52 . 507537768880056&1口g=l3 . 4252 696357 01511 这里, maxResults 参数被设为 99。不过, 我们可以使用第 1章中介绍 的技术增大该参数的值, 以便在一次请求中下载所有经销商的地点。 下面是 将 maxResults 的值增加到 1000 时的输出结果 。 》> url = ’https ://c2b-serv工ces .bmw .com/ c2b-localsearch/services/ap工/v3/clients/BMWDIGITAL DLO/DE/ pois?country=DE&category=BM&maxResults=毛d&language=en& lat=52 . 507537768880056&lng= l3. 425269635701511' 》> jsonp = D(url 宅l000) 》> jsonp ’callback ({”status": { }) 『 才54 9.4 宝马 AJAX 请求提供了 JSONP 格式的数据,其中JSONP 是指填充模式的 JSON ( JSON with padding ) 。这里 的填充通常是指要 调用 的函数, 而 函数 的参数则 为纯 JSON 数据, 在本例中调用 的是 callback 函数。 要想使用 Python 的 json 模块解析该数据, 首 先需要将填充部分截取掉 。 》> import json 》> pure_j son = jsonp [jsonp . index (’(’) + 1: jsonp . rindex(’) ’)] 》> dealers = json . loads (pure json) 》> dealers .keys () [u’ status ’, u’count ’, u’translation ’ , u ’ data ’, u ’metadata ’ ] 》> dealers [’ count ’] 731 现在, 我们 已经将德国所有的宝马经销商加载到 JSON 对象中, 可以看出 目前总共有 731 个经销商。 下面是第一个经销商的数据 。 》> dealers [’data ’ ][’pois’] [OJ { u’attributes ’: {u’ businessTypeCodes ’:[u’NO’,u『PR’ ], u'distributionBranches ’: [u’T’, u’F’, u’G’] , u ’ distributionCode ’ : u 'NL’, u’distributi。nPartnerid’ : u’00081’, u’fax’ : u’+49 (30) 20099-211 0’, u’h。mepage’ : u’http://bmw-partner.bmw .de/ niederlassung-berlin-weissensee ’, u ’mail ’: u ’nl .berlin@bmw . de ’, u’outlet Id’: u’3’, u『outletTypes’: [u’ FU ’ ], u’ph。ne ’: u’+49 (30) 20099 0’, u’requestServices ’: [u’RFO ’, u’RID’, u’TOA ’ ], u’services ’: [] }, u’category’ : u’BMW ’, u『city’: u ’ Berlin ’, u’country’ : u’Germany ’, u’countryCode’: u’DE’, u’dist’ :6.65291036632401, u『key’: u’00081 3’, u’lat ’: 52 . 562568863415, u’lng ’ :13.463589476607, u’name’ : u’BMW AG Niederlassung Berlin Filiale Wei \xdfensee ’, u’postalCode’ : u’13088’, u’street’ : u’Gehringstr. 20’} 155 第 9 章总结 现在可以保存我们感兴趣的数据 了。下面的代码片段将经销商的名称和经 纬度写入一个电子表格当中。 with open ( 'bmw . csv’ , ’w’) as fp : writer = csv. writer (fp) writer. writer。w ([’ Name ’, ’ Latitude ’, ’ Longitude ’ ]) for dealer in dealers [’data ’][’pois ’]: name = dealer [’name’J .encode ( ’utf-8 ’ ) lat, lng = dealer [’lat ’ ] , dealer [’lng ’ ] writer. writerow ( [name , lat, lng]) 运行该示例后, 得到的 bmw.csv 表格中 的内容类似如下所示。 Name , Latitude, Longitude BMW AG Niederlassung Berlin Filiale Wei.Bensee,5 2.562568863415, 13.463589476607 Autohaus Graubaum GmbH, 52. 452892 5,1 3.521265 Autohaus Reier GmbH & Co. KG, 52. 56473, 1 3.32521 从宝马官网抓取数据的完整源代码可以从 https ://b itbucket . org/ wswp/code/src/tip/chapter09 /bmw .py 获取 。 회 翻译外文内容 你可能已经注意到宝马的第一个截图 (见图 9.8 ) 是德文的 , 而第二个截图 (见图 9.9 ) 是英文的。 这是因为第二个截图中 的文本使用 了Google 翻译的浏览器扩展进行了翻译。 当尝试 了解如何在外文网站中定位时, 这是一个非常有用 的技术. 宝 马官网在经过翻译后, 仍然可以 正常运行。 不过还是要当心 Google 翻译可能会破坏一些 网站的正常运行,比如依赖原始值 的表单 , 其中的 下拉菜单内 容被翻译时就会出现问题。 在 Chrome 中,Google 翻译可以通过安装 Google Translate 扩展获得; 在Firefox 中, 可 以安装 Google T ranslator 插件; 而在 IE 中, 则可以安装 Google Toolbar. 此外 , 还可以 使用 http://t ranslate .google .com 进行 翻译, 不过这样通常会打断原有功能, 因 为要从 Google 的网 站中获取相关内容。 156 9 .5 本章小结 9.5 本章 小结 本章分析 了 几个著名 网站 , 并演示 了如何在其中应用本书中介绍过的技 术。 我们在抓取 G oogle 结果页 时使用了 css 选择 器, 在 抓取 Fac e book 页 面 时测试 了浏览器 渲染引 擎和 API , 在爬取 Gap时使用 了 Sitemap ,在从地 图 中抓取所有宝 马经销商 时利用 了AJ AX 调用 。 现在 , 你可 以运用 本书 中介绍的技 术来抓取包含有你感兴趣数据 的网站 了。 希望你能够和 我一样享受这其中 的力量 ! 157 构建辑程爬虫来并行爬取页面: �= n �邮电出版社 鳞. A步虹医 • www . e川I. com.en 面� ISBN 978-7-115-431 79-口 注册奋礼 , 提交勘误送手只分 新书抢鲜, 电子书罔步发售 投稿!&馈邮箱 contact@epubit.com.cn 新浪微博 @人邮异步社区 nu- nu- 7f- ω一元 5一∞ 41-R U 『··工 4 币一 价 川一定 RU一cu一 分类建议: 计算机/程序设计/ 即thon 人民邮电出版社网址: www.ptpress.com.cn
    还剩170页未读

    继续阅读

    下载pdf到电脑,查找使用更方便

    pdf的实际排版效果,会与网站的显示效果略有不同!!

    需要 10 金币 [ 分享pdf获得金币 ] 10 人已下载

    下载pdf

    pdf贡献者

    DLI315

    贡献于2017-01-15

    下载需要 10 金币 [金币充值 ]
    亲,您也可以通过 分享原创pdf 来获得金币奖励!