Python3网络爬虫开发实战 第2版 [2 ed.]
 9787115577092

Table of contents :
封面
序一
序二
前言
目录
第1章 爬虫基础
1.1 HTTP基本原理
1.2 Web网页基础
1.3 爬虫的基本原理
1.4 Session和Cookie
1.5代理的基本原理
1.6多线程与多进程的基本原理
第2章 基本库的使用
2.1 urllib的使用
2.2 requests的使用
2.3 正则表达式
2.4 httpx的使用
2.5 基础爬虫案例实战
第3章 网页数据的解析提取
3.1 XPath的使用
3.2 Beautiful Soup的使用
3.3 pyquery的使用
3.4 parsel的使用
第4章 数据的存储
4.1 TXT文本文件存储
4.2 JSON文件存储
4.3 CSV文件存储
4.4 MySQL存储
4.5 MongoDB 文档存储
4.6 Redis缓存存储
4.7 Elasticsearch搜索引擎存储
4.8 RabbitMQ的使用
第5章 Ajax数据爬取
5.1 什么是Ajax
5.2 Ajax分析方法
5.3 Ajax分析与爬取实战
第6章 异步爬虫
6.1 协程的基本原理
6.2 aiohttp的使用
6.3 aiohttp异步爬取实战
第7章 JavaScript动态渲染页面爬取
7.1 Selenium的使用
7.2 Splash的使用
7.3 Pyppeteer的使用
7.4 Playwright的使用
7.5 Selenium爬取实战
7.6 Pyppeteer爬取实战
7.7 CSS位置偏移反爬案例分析与爬取实战
7.8 字体反爬案例分析与爬取实战
第8章 验证码的识别
8.1 使用OCR技术识别图形验证码
8.2 使用OpenCV 识别滑动验证码的缺口
8.3 使用深度学习识别图形验证码
8.4 使用深度学习识别滑动验证码的缺口
8.5 使用打码平台识别验证码
8.6 手机验证码的自动化处理
第9章 代理的使用
9.1 代理的设置
9.2 代理池的维护
9.3 付费代理的使用
9.4 ADSL拨号代理的搭建方法
9.5 代理反爬案例爬取实战
第10章 模拟登录
10.1 模拟登录的基本原理
10.2 基于Session和Cookie的模拟登录爬取实战
10.3 基于JWT的模拟登录爬取实战
10.4 大规模账号池的搭建
第11章 JavaScript逆向爬虫
11.1 网站加密和混淆技术简介
11.2 浏览器调试常用技巧
11.3 JavaScript Hook的使用
11.4 无限debugger的原理与绕过
11.5 使用Python模拟执行JavaScript
11.6 使用Node.js模拟执行JavaScript
11.7 浏览器环境下JavaScript的模拟执行
11.8 AST技术简介
11.9 使用AST技术还原混淆代码
11.10 特殊混淆案例的还原
11.11 WebAssembly案例分析和爬取实战
11.12 JavaScript 逆向技巧总结
11.13 JavaScript逆向爬取实战
第12章 APP数据的爬取
12.1 Charles抓包工具的使用
12.2 mitmproxy抓包工具的使用
12.3 mitmdump实时抓包处理
12.4 Appium的使用
12.5 基于Appium的App爬取实战
12.6 Airtest的使用
12.7 基于Airtest的App爬取实战
12.8 手机群控爬取实战
12.9 云手机的使用
第13章 Android逆向
13.1 jadx的使用
13.2 JEB的使用
13.3 Xposed框架的使用
13.4 基于Xposed的爬取实战案例
13.5 Frida的使用
13.6 SSL Pining问题的解决方案
13.7 Android脱売技术简介与实战
13.8 利用IDA Pro静态分析和动态调试so文件
13.9 基于Frida-RPC模拟执行so文件
13.10 基于AndServer-RPC模拟执行so文件
13.11 基于unidbg 模拟执行so文件
第14章 页面智能解析
14.1 页面智能解析简介
14.2 详情页智能解析算法简介
14.3 详情页智能解析算法的实现
14.4 列表页智能解析算法简介
14.5 列表页智能解析算法的实现
14.6 如何智能分辨列表页和详情页
第15章 Scrapy框架的使用
15.1 Scrapy框架介绍
15.2 Scrapy入门
15.3 Selector的使用
15.4 Spider的使用
15.5 Downloader Middleware的使用
15.6 Spider Middleware的使用
15.7 Item Pipeline的使用
15.8 Extension的使用
15.9 Scrapy对接 Selenium
15.10 Scrapy 对接 Splash
15.11 Scrapy 对接 Pyppeteer
15.12 Scrapy 规则化爬虫
15.13 Scrapy实战
第16章 分布式爬虫
16.1 分布式爬虫理念
16.2 Scrapy-Redis原理和源码解析
16.3 基于Scrapy-Redis的分布式爬虫实现
16.4 基于Bloom Filter进行大规模去重
16.5 基于RabbitMQ的分布式爬虫
第17章 爬虫的管理和部署
17.1 Scrapyd和ScrapydAPI的使用
17.2 Scrapyd-Client的使用
17.3 Gerapy爬虫管理框架的使用
17.4 将Scrapy项目打包成Docker镜像
17.5 Docker Compose的使用
17.6 Kubernetes的使用
17.7 用Kubernetes 部署和管理 Scrapy爬虫
17.8 Scrapy分布式爬虫的数据统计方案
17.9 基于Prometheus和Grafana的分布式爬虫监控方案
附录 爬虫与法律

Citation preview

●~

/\ 图灵原创

|/ `

×\`

尸yt∩o∩之父Gujd◎γa∏R◎ssum推荐的爬虫书 第|版销量近〗ooooo册

■′



虫开发实战 △■



囤中国I信出版集

士 S



凸口≤℃∏·□■□』■■■■∏■·□□口口∏ˉ■·凸α∩』■■■■■『□■夕■





′` ≥



‖Ⅵ′



·】·$°·《●凸■□■=

‖|『|巴





图灵原创

`′忍「『|′

■司凸■



Python3网络爬虫开发实战/崔庆才著_2版 —北京:人民邮电出版社’ 202L1l(2022.1重印) (图灵原创) ISBN978-7—115-57709_2

I .oP…II.@崔…IⅡ@软件工具_程序设计 Ⅳ[email protected]

中国版本图书馆CIP数据核字(2021)第209191号

内容提要



本书介绍了如何利用Python3开发网络爬虫。本书为第2版’相比于第1版,为每个知识点的实战

项目配备了针对性的练习平台,避免了案例过期的问题·另外,主要增加了异步爬虫、JavaSc∏pt逆向、 App逆向、页面智能解析、深度学习识别验证码、Kubemetes运维及部署等知识点’同时也对各个爬虫知 识点涉及的请求、存储、解析、测试等工具进行了丰富和更新· 本书适合Python程序员阅读.



凸□



图书在版编目(c|尸)数据

◆著 崔庆才 责任编辑王军花

责任印制周异亮

◆人民邮电出版社出版发行 北京市丰台区成寿寺路11号 邮编 100l64 电子邮件[email protected] 网址https://wwwptpress。com.cn 北京天宇星印刷厂印刷 ◆开本: 787×l092

印张: 58 字数: l684千字

l/16

202l年1l月第2版 2022年l月北京第3次印刷 定价: l39。80元

读者服务热线:(010)8408q456叼009印装质量热线:(010)81055316 反盗版热线:(010)81055315

广告经营许可证:京东市监广登字20170147号 ‖





| 序

b

■■■‖■■■■■

l



p

今天我们所处的时代是信息化时代,是数据驱动的人工智能时代。我们参加各种行业会议和技术 会议时’常会听到《数字化”这个概念°事实上’各行各业中的数据都越来越数字化了。在PC时代, 微软和IBM将企业办公桌面数字化了,这使得企业信息能够互联互通。在移动互联网时代’苹果`腾 讯和阿里巴巴把社交通信`购物和支付等移动化和数字化了°到了人工智能、物联网时代,万物互联

■ 厂 卜

和物理世界的全面数字化使得人工智能可以基于这些数据产生优质的决策’从而对人类的生产生活产 生巨大价值。在这个以数据驱动为特征的时代’数据是最基础的°数据既可以通过研发产品获得,也 可以通过爬虫采集公开数据获得’因此爬虫技术在这个快速发展的时代就显得尤为重要,高端爬虫人 才的收人也在逐年提高°

| b.



由于数据所有权和使用权的模糊,数据采集行业存在—定的不确定性,技术交流的机会也不多’ 公开且有技术深度的书就更少了’崔庆才的这本《Python3网络爬虫开发实战(第2版)》是目前为止 市场上公开数据采集领域最好的图书之_。相比第l版’这一版对案例做了本地化处理’提供了自行 搭建的服务供读者练手’从而不需要依赖外部网站的接口。此外’这-版还增加了Android逆向的大 量技术细节’这部分内容符合当前主流爬虫技术的发展要求°异步爬虫技术也是现阶段非常主流的一 种技术’用于大规模数据爬取。

最后,从认知科学的角度来看’大家边学边练,把书中的案例全部自己动手认真过—遍’遇到

■」

问题多在读者交流群提问’同时多解答其他网友的问题,把教和学结合起来,-定能成为这个领域的

=■尸巳■■巴■■‖■『

翘楚°

梁斌penny’北京八友科技总经理,清华大学博士

‖『‖■■■■『【■■【■■‖β『■■■『‖‖‖|卜卜■尸}■■【〗「‖▲■尸‖|「『■■【■伊



| 序

■■■■■■

‖ —



|(

我的梦想是做活的人工智能,这里的“活”有多重含义,我认为其中很重要的一点是应该通过某 种方式让人工智能从人类每天增长的数据中快速汲取‘养料,,,以不断地更新`成长,扎根于今天的 人类世界。

这会产生很多有趣的问题’比如,人们以往在对话中采用的评论数据也许并不能在一天的时间里

再如’人能够很快地学会一个流行语,如“卷,’或‘‘内卷,,°面对_个陌生的新流行语’我们看 几个相关的例子’基本上就能根据语境猜测出它的含义’并能迅速模仿例子’甚至把它用在新语句里° 那么人工智能是否可以迅速地学会使用流行语呢?我们几经尝试之后发现的确可以,只需要爬取几十 条包含某个新词的句子,人工智能就能大致学会这个新词的用法’还能使用它把相似语境里的话改写 成新句子。

||

积累很多’尤其是那些不那么热门的话题’那有没有办法从自媒体每天快速产生的大量文章里获取数据’ 再用某种方式将它们自然地使用在对话中呢?这就好比让人工智能也每天刷微博`刷头条和刷朋友 圈’于是它就能知道今天的网络或者真实世界发生了什么’它也因此跟我们同步生活在了—个世界中° 我们分析公众号数据后,发现的确存在这样的可能°如在2017年8月,我们在包含“鹿哈”的文章 里挖掘出了“邮筒”这个词,原来是文章里有-张鹿哈和_个邮筒在上海外滩的合影,之后粉丝们纷 纷排队打卡这个幸运的邮筒。这些新消息恐怕是和人工智能聊天的粉丝们最想知道的’有了及时更新 的数据,我们就有可能做到让人工智能为我们推荐感兴趣的消息’它就像志同道合的好朋友_样°









这_切都要依赖数据,而爬虫是人工智能行业获取数据时最方便、最常用的_种手段°崔庆才在 这方面的积累和能力早已在第l版中有所体现’我知道他_直在繁忙的工作之余修改第l版’不断更新 其中的技术和代码°他在写《Pylhon3网络爬虫开发实战(第2版)》时表现出来的勤奋让他成为我朋 友圈里最‘卷”的那一个,这本书即将出版,他再次邀请我为书作序,我感到荣幸之至°之后我会第 _时间买这本书’并将这本书推荐给我的朋友和学生’无论是研究还是开发,这本书都非常有价值° 人工智能因为深度学习得到了近十年的大发展’而以BERT为代表的预训练模型又让深度学习变

果°这些进展都离不开优秀的爬虫程序°









毫不夸张地说,数据是人工智能之源°正当地爬取数据、谨慎地使用数据和严格地保护隐私会让 人工智能得到滋养,从而探索更多的可能°在数据方面,本书能为大家提供极大的帮助。

|{|

得有点不-样。之前的监督学习依赖大量人工来标注数据’这不得不付出昂贵的标注成本,同时还得 忍受标注带来的局限性和不自然°而今,预训练模型采用的几乎都是从互联网上爬取的海量数据,通 过构造半监督任务来训练模型,如BERT构造了-个隐藏某个词,然后用这个词周围的词来填空的任 务,这样不需要标注,通过构造出的监督信息就能训练出神经网络。我们在悟道.文澜项目中’爬取 了几千万个图像和周边文字,通过预训练得到了图像和文字的统_表示,达到了很好的跨模态检索效



















宋春华, 中国人民大学高领人工智能学院长聘副教授

































■■厂》‖|‖仙|||}■尸『||‖■【|‖‖′|坠▲|已■「『|巴◆=【■止■=云|||‖■尸|‖『}■厂‖|【■「■=尸■『‖【『卜『■=口}『■『[‖|||‖|}厨『【‖『

| 兰



月|』



您好’我是崔庆才°

首先’非常高兴我们能够因此书初次或再次相会。为什么会提到再次相会呢?因为这本书已经是 第2版了。如果您曾经阅读过第l版’那么请允许我再次对您的支持表示诚挚的感谢°

我从20l5年开始接触网络爬虫’当时爬虫还没有这么火’我觉得能够把想要的数据抓取下来是

一件非常有成就感的事情’而且可以顺便熟悉Python’_举两得。在学习期间,我将学到的内容做好 总结,并发表到我的博客上。随着发表的内容越来越多’博客的测览量也越来越高’很多读者对我的 博文给予了肯定的评价,这也给我的爬虫学习之路增添了很多动力°后来有-天’图灵的王编辑联系 了我,问我有没有意向写_本爬虫方面的书,我听到之后充满了欣喜和期待,这样既能把自己学过的 知识点做一个系统整理’又能跟广大的爬虫爱好者分享自己的学习经验’还能出版自己的作品’于是 我很快就答应了约稿°_开始我觉得写书并不是—件那么难的事’后来真正写了才发现其中包含的艰 辛°书相比博客’用词更严谨’而且逻辑需要更填密.很多细节必须考虑得非常周全°编写前前后后

花了近一年的时间’审稿和修改又用了将近半年的时间’_路走来甚是不易’不过最后看到书稿成型 我觉得这一切都是值得的°

本书第l版是在20l8年出版的,出版后受到了不少读者的支持和喜爱’真的非常感谢各位读者 的支持。有的读者还特地告诉我,他看了我的书之后找到了一份不错的爬虫工作’听到之后我真的非 常开心,因为我的一些知识和经验帮助到了他人。

之所以写第2版,_方面是技术总是在不断地发展和进步’爬虫技术也_样’它在爬虫和反爬虫 不断斗争的过程中持续演进着。现在的网页采取了各种防护措施’比如前端代码的压缩和混淆`API的

参数加密、WebDrlver的检测,因此要做到高效的数据爬取,需要我们懂-些JavaScript逆向分析技术°

||

=■尸□■■厂}‖『}‖|





与此同时’App的抓包防护`加壳保护`本地化、风控检测使得越来越多的APP数据难以爬取’所以 我们不得不了解-些App逆向相关的技术’比如Xposed、F∏da` IDAPro等工具的使用°近几年,深 度学习和人工智能发展得也是如火如茶’所以爬虫还可以和人工智能相结合,比如基于深度学习的验 证码识别`网页内容的智能解析和提取等技术°另外’—些大规模爬虫的管理和运维技术也在不断发 展’当前Kubemetes、Docker`PIometheus等云原生技术也非常火爆’基于Kubemetes等云原生技术 的爬虫管理和运维解决方案也已经很受青睬。然而’第l版几乎没有提及以上这些新兴技术°

另一方面’第l版引用了很多案例网站和服务,比如猫眼电影网站、淘宝网站`代理服务网站’ 几年过去’其中有些案例网站和服务早已经改版或者停止维护,这就导致第l版中的很多代码已经不 能正常运行了°这其实是一个很大的问题’因为代码运行不通会大大打击读者学习的积极性和降低他 们的成就感,而且还浪费不少时间。另外,即使爬虫代码及时更新了’我们也不知道这些案例网站和 服务什么时候会再次改版,这都是不可控的°为了彻底解决这个问题’我花了近半年的时间构建了一

]|

个爬虫案例平台(h杖ps:〃scrape.center)--包含几十个案例网站,包括服务端痘染(SSR)网站、单



页面应用(SPA)网站、各类反爬网站`验证码网站、模拟登录网站、各类App等,覆盖了现在爬虫 和反爬虫相关的大多数技术。整个平台都由我来维护,书中几乎所有的案例网站都来自这个平台,这



{}



←二口

=‖·

尝月

勺么



样就解决了页面改版或停止维护的问题°

相比第l版’本书第2版主要更新了如下内容° □绝大多数案例网站来自自建的案例平台,以后再也不用担心案例网站过期或改版的问题。

□删除了第l版中的第l章“环境安装” ,将配置环境的内容全部汇总并迁移到案例平台

(https://sempscmpecenter),然后在书中以外链的形式附上,以确保环境配置和安装说明相关 的内容能得到及时更新。

□增加了深度学习相关的内容’如图形验证码、滑动验证码的识别方案。 □丰富了模拟登录的内容’如增加了JWT模拟登录的介绍和实战、大规模账号池的优化°

□增加了JavaScnpt逆向,包括网站加密和混淆技术` JavaScript逆向调试技巧` JavaSc∏pt的各 种模拟执行方式、AST还原混淆代码`WebAssembly等相关技术。 □丰富了App自动化爬取技术’如介绍了新兴框架Al∏est、手机群控和云手机技术° □增加了Android逆向’如反编译`反汇编`Hook`脱壳、分析和模拟执行so文件等技术° □增加了页面智能解析’包括提取列表页、详情页内容的算法和分类算法°

|||

□增加了_些新的请求库`解析库、存储库等,如httpx` parsel`Elasticsearch等。 □增加了异步爬虫,如协程的基本原理、aiohttp的使用和爬取实战。 □增加了—些新兴自动化工具,如Pyppeteer、Playwnght°

| q

□丰富了Scrapy相关章节的内容’如Pyppeteer的对接、RabbltMQ的对接、Prometheus的对 接等。

□增加了基于Kubemetes、Docker、Prometheus、Grafana等云原生技术的爬虫管理和运维解决 方案°

由于工作`生活等各方面的原因,我的时间并不像写第l版时那么宽裕’所以第2版的编写进度 比较慢,利用的几乎都是下班和周末的时间,耗时将近两年°如今’第2版终于跟读者见面了!在编 写期间我也收到过很多读者的询问和鼓励’非常感谢各位读者的支持和耐心等待° 希望本书能够为您学习爬虫提供帮助°

本书内容 本书内容—共分为l7章’归纳如下。

第l章介绍了学习爬虫之前需要了解的基础知识’如HTTP`爬虫`代理、网页结构`多进程、 多线程等内容°对爬虫没有任何了解的读者’我建议好好了解这_章的知识°

第2章介绍了最基本的爬虫操作’爬虫通常是从这_步学起的°这-章介绍了最基本的请求库 (urlljb` requests` h仗px)和正则表达式的基本用法。学完这_章’就可以掌握最基本的爬虫技术了°

第3章介绍了网页解析库的基本用法’包括BeautjfUlSoup`XPath、pyquery、parsel的基本使用 方法’这些库可以使信息的提取更加方便`快捷,是爬虫必备的利器。

第4章介绍了数据存储的常见形式及存储操作’包括TXT文件、JSON文件`CSV文件的存储’ 以及关系型数据库MySQL和非关系型数据库MongoDB`Redis的基本存储操作’另外还介绍了 Elasticsearch搜索引擎存储、RabbjtMQ消息队列的用法。学完这_章’就可以灵活、方便地保存爬取 下来的数据°

第5章介绍了Ajax数据爬取的过程°_些网页数据可能是通过A|ax请求ApI接口的方式加载的, 用常规方法无法爬取,这_章介绍了Ajax分析和爬取实战案例°



←一≡口

||



3

第6章介绍了异步爬虫的相关知识,如支持更高并发的协程的基本原理` aiohttp库的使用和实战

案例·有了异步爬虫’爬虫的爬取效率将会大大提高°

第7章介绍了爬取动态喧染页面的相关内容°现在越来越多的网站内容是由JavaSc∏pt喧染得到的, 原始HTML文本可能不包含任何有效内容’同时喧染过程会涉及某些JavaScnpt加密算法’对此可以

使用Selenium` Splash、Pyppeteer、Playwright等工具模拟测览器来进行数据爬取。 第8章介绍了验证码的相关处理方法°验证码是网站反爬虫的重要措施’我们可以通过这_章了 解各类验证码的应对方案·包括图形验证码`滑动验证码`点选验证码`手机验证码,其中会涉及OCR` OpenCV、深度学习、打码平台的相关知识。

第9章介绍了代理的使用方法°限制IP的访问也是网站反爬虫的重要措施’使用代理可以有效解 决这个问题’我们可以使用代理来伪装爬虫的真实IP°通过这—章’我们能学习代理的使用方法’代 理池的维护方法’以及ADSL拨号代理的使用方法°

第l0章介绍了模拟登录爬取的方法。某些网站需要登录才可以看到需要的内容’这时就需要用 爬虫模拟登录网站再进行爬取了。这一章介绍了最基本的模拟登录方法,包括基于Session+Cookle的 ‖【■【■■■=■■■■■|■■〖■■「■■■尸【■■■尸■■‖』『‖■■■■■■■■Ⅲ■■『■‖|』‖■〖‖■ˉ尸

模拟登录和基于JWT的模拟登录°

第l1章介绍了JavaSc∏pt逆向的相关知识,包括网站的混淆技术、JavaScript逆向常用的调试和

Hook技术、JavaSc∏pt模拟执行的各个方案,接着介绍了AST技术来还原JavaSc前pt混淆代码,另外 也对WebAssembly技术进行了基本介绍°

第l2章介绍了App的爬取方法,包括基本的抓包软件(Charles、mitmproxy)如何使用,然后介 绍了利用mjtmdump对接Python脚本的方法进行实时抓取’以及使用Appium、Ai沈est模拟手机App的 操作进行数据爬取°

第13章介绍了Android逆向的相关知识’包括反编译工具jadx、JEB和常用的Hook框架Xposed` Fnda等工具的使用方法’另外还介绍了SSLPining`脱壳、反汇编、so文件模拟执行等技术°

第l4章介绍了页面智能解析相关的技术’比如新闻详情页面中标题`正文`作者等信息以及新 闻列表页面中标题、链接等信息的智能提取,另外还介绍了如何智能分辨详情页和列表页°有了页面 智能解析技术’在提取很多内容时就可以免去写规则的困扰°

第15章介绍了Scrapy爬虫框架及用法°Scrapy是目前使用最广泛的爬虫框架’这章介绍了它的 基本架构、原理及各个组件的使用方法’另外还介绍了Scrapy对接Selenium`Pyppeteer等的方法° 第16章介绍了分布式爬虫的基本原理及实现方法°为了提高爬取效率’分布式爬虫是必不可少 的’这章介绍了使用ScrapyˉRedis`RabbitMQ实现分布式爬虫的方法。

第l7章介绍了分布式爬虫的部署及管理方法。方便、快速地完成爬虫的分布式部署’可以节省 开发者大量的时间°这一章介绍了两种管理方案’_种是基于Scrapy`ScIapyd、Gerapy的方案’另 —种是基于Kubemetes、Docker、Prometheus`Grafana的方案。

致谢 感谢我的父母`领导、导师,没有你们创造的环境’我不可能完成本书的写作° 】■■■■■】■】■Ⅷ』】】】』Ⅲ■■■■■〗‖|‖』‖‖}||[|此‖‖|』□

感谢在我学习过程中与我探讨技术的各位朋友,特别感谢韦世东`陈佳林`周子棋、蔡晋、冯威、 文安哲、戴煌金、陈祥安`唐铁飞、张冶青崔弦毅`苟桃`时猛`步绍鹏`阮文龙`杨威`钟业弘、 方东旭先生在我写书过程中为我提供思路和建议。



←=口



4

感谢开源界的各位大牛编写了诸多如此强大又便捷的工具和框架°

感谢为本书撰写推荐语的各位老师,感谢你们对本书的支持和推荐。 感谢王军花、武丙欣编辑’在书稿的审核过程中给我提供了非常多的建议,没有你们的策划和敦

|隆{

促’我也难以顺利完成本书。 感谢为本书做出贡献的每_个人!



相关资源 本书中的所有代码都放在了GltHub上,详见https://githuhcom/Python3WebSpiderO,书中每个实 例对应的章节末也有说明°

|{|

由于本人水平有限’写作过程中难免存在_些错误和不足之处’恳请广大读者批评指正。如果发 现错误,可以将其提交到图灵社区本书主页,以使本书更加完善’非常感谢!

另外’本书还设有专门的读者交流群’可以搜索“进击的Coder”微信公众号获取,欢迎各位读 者加人!

|{

最后,我本人也会在‘‘进击的Coder,,和“崔庆才|静觅”两个公众号分别分享_些技术总结和 个人感悟’欢迎订阅,可以扫下方两个二维码关注。

进击的cme「

崔庆才|静觅

崔庆才 202l年9月

|} |{





「|}‖】||」■■■■■■■■■■司

o也可到图灵社区本书主页免费注册并下载源代码°

目 第↑章



爬虫基础………...……………………….…..…l

第6章异步爬虫..….……….….…..….…..…….... l9l

1l

HTTP基本原理……………。….…….….…….……l

6』协程的基本原理…ˉ……….…。…,….。……,.…. l9l

1.2

Web网页基础………….….…….……….………. l2

62

ajohttp的使用…………………………………….20]

l3

爬虫的基本原理………ˉ………………。……,·…l9

6,3

aiohttp异步爬取实战…………ˉ……ˉ……ˉˉ…207

l.4

Session和Cookie….…。……………。……………2]

l5

代理的基本原理.….………………….…………..24

16

多线程和多进程的基本原理,…………ˉˉ…,..26

基本库的使用.………………..…...……….29

第2章

第7章」avaSc『|pt动态演染页面爬取..…2l2 7l

Selenium的使用…………….…..………………2l2

7ˉ2

Splash的使用…。..。……。….。…….……ˉ.ˉ……..226

73

Pyppeteer的使用…………….………………….242

2」

urllib的使用………ˉ.…………,………………ˉ….29

7.4

Playwrjght的使用………………………………257

22

requests的使用….…………………………………47

75

Selenium爬取实战。,…ˉ………….…..……ˉ….269

2·3

正则表达式………….………………………………63

76Pyppeteer爬取实战。……,…………….……….276

2.4

httpx的使用.…….…….………….……。…。………73

7.7

2。5

基础爬虫案例实战………………………….……78

第3章 网页数据的解析提取………..………..…90

CSS位置偏移反爬案例分析与爬取 实战…。……………………………….………………282

78字体反爬案例分析与爬取实战….……ˉ….287

第8章验证码的识别..……….……………………293

3.1

XPath的使用…………,….…ˉ…………·…………90

32

BeautifUlSoup的使用…。…,……………………99

8。l

3.3

pyqueIy的使用。………,….…………………ˉ…. ll3

8.2使用OpenCV识别滑动验证码的缺口.….298

3.4

parsel的使用…………………·,……ˉ…ˉ………, l24

8。3使用深度学习识别图形验证码.…………。304

第4章;

数据的存储……….….….…………….……l28

使用OCR技术识别图形验证码………….293

8.4使用深度学习识别滑动验证码的缺口.….309 85使用打码平台识别验证码……ˉ……,…,ˉ…3l6

4。l

TXT文本文件存储………….…………………l28

4.2

JSON文件存储。。……………。……….…………130

43

CSV文件存储.…………….……,ˉ….………,.ˉ l34

44

MySQL存储.…….….……………………………l38

9.l

45

MongoDB文档存储…ˉ…。.………………..…l44

9.2代理池的维护…ˉ……………..…………………340

46

Redis缓存存储…….……………….……………l5l

93付费代理的使用………………….……….……35l

47

Elasticsearch搜索引擎存储…………………l59

94ADSL拨号代理的搭建方法…………·……357

48

RabbitMQ的使用…………………….…………l66

95代理反爬案例爬取实战……………,….。.…365

第5章[ 川ax数据爬取……….……………………. l74

第↑0章模拟登录………….…..……….…….…….373

8.6手机验证码的自动化处理ˉ……….…………324

第9章代理的使用….………………………..…….33l 代理的设置…….…….…………..……………….33l

模拟登录的基本原理.………….……………373

什么是Ajax,…..……….…,….…………………, l74

lOl

52

A)ax分析方法……………….…………….…·…176

l02基于Session和Cookie的模拟登录

5.3

Ajax分析与爬取实战………….…………….. l79

5.l

爬取实战………。.………………………….…。…376





l03基于JWT的模拟登录爬取实战………38l l0,4大规模账号池的搭建…..…….………….。…385

第↑↑章」avaSc「|pt逆向爬虫..………………397

第(4章页面智能解析…….….………….………700 l4。l

页面智能解析简介ˉ. .….…。…………ˉ……。700

l42详情页智能解析算法简介………….……ˉ.707

l4.3详情页智能解析算法的实现.…….……...7l4

ll.1

网站加密和混淆技术简介…………..…….397

l44列表页智能解析算法简介……ˉ…ˉ………。722

l12

柳|览器调试常用技巧….…。…………………4l3

l45列表页智能解析算法的实现…………….727

ll3 JavaScrjptHook的使用…。……….ˉ………。430

l46如何智能分辨列表页和详情页………….735

l14无限debugger的原理与绕过….…………440 ll5使用Python模拟执行JavaScript….……445

第↑5章Sc『apγ框架的使用.….………………739

l16使用Node.js模拟执行JavaScrlpt………45l

l5。l

Scrapy框架介绍…………….…………………739

ll.7测览器环境下JavaScrjpt的模拟执行…454

l5.2

Scrapy人门………………………….………….743

ll8AST技术简介…….……….……….…………460

ll9使用AST技术还原混淆代码……………472

ll.l0特殊混淆案例的还原…………………......480

ll。ll

WebAssembly案例分析和爬取实战., ,490

lll2

JavaScrjpt逆向技巧总结…………………498

ll』3

JavaScrlpt逆向爬取实战….……….….…505

l53

Selector的使用…….…….………..…………754

l5。4

Spider的使用.….……………….….,…。.….759

l5.5

DownloaderMiddleware的使用……..』…766

|5.6

SpjderMiddleware的使用…………………775

l5.7

ItemPjpell∏e的使用………。…….…………78l

l5.8

Extenslon的使用……….…….…….…………792

l5.9

Scrapy对接Selenium………………..…·.…795

第↑2章∧pp数据的爬取……….….….….……530

l5.l0

Scrapy对接Splash。…。……….……………,80l

Charles抓包工具的使用..…….…….…。…530

l5.l l

Scrapy对接Pyppeteer………。……….……806

l22mitmproxy抓包工具的使用…。 .….………538

]5·l2

Scrapy规则化爬虫………。….…………….8l3

l23mjtmdump实时抓包处理……、……………544

l513

Scrapy实战…….……………….…….………827

l2l

l2.4Appium的使用…….………、…………………55l

第↑6章分布式爬虫...……………………….……840

l25基于Appium的App爬取实战…………562

l6』分布式爬虫理念.……·………………………。840

l2.6

Airtest的使用ˉ……ˉ…,…………….…ˉ………568

l27基于Alrtest的App爬取实战……………585

l6.2

ScrapyˉRedis原理和源码解析ˉ。ˉ。…ˉ……842

l63基于ScrapyˉRedls的分布式爬虫

l2.8手机群控爬取实战ˉ…….……..…。…ˉ………59l l2·9云手机的使用…….……………………………594

实现….……………………………………………847

l6.4基干BloomFilter进行大规模去重……85l

第↑3章∧∩d『o|d逆向…..……………………603

l65基于RabbjtMQ的分布式爬虫……..……859

l3」 jadx的使用………………………………………603

第↑7章爬虫的管理和部署………………..….862

JEB的使用……………………….….………….6l5

l7」 Scrapyd和ScrapydAPI的使用………….862

l32

l33Xposed框架的使用………………….………624

l7.2

ScrapydˉCljent的使用……….…….……….867

l3.斗基于Xposed的爬取实战案例……………635

l7.3

Gerapy爬虫管理框架的使用…...……….869

l35Frida的使用………………………………·……643

l7.4将Scrapy项目打包成Docker镜像……873

l36SSLPining问题的解决方案……ˉ……,…650

l75DockerCompose的使用…….…′……….…878

l3.7Androld脱壳技术简介与实战..……..….657

l7.6

l38利用IDAPro静态分析和动态调试

l7.7用Kubemetes部署和管理Scrapy

Kubemetes的使用·…………·….……………880 爬虫……..…………………………………………888

so文件.………….……………………………….664

Scrapy分布式爬虫的数据统计方案.…899

l3.9基于FridaˉRPC模拟执行so文件.…ˉ…680

l78

l3.l0基于AndServerˉRPC模拟执行

l7.9基于Prometheus和Gra饱na的分布式

13』l

■■‖‖』】】■■■■‖』‖‖」■■】■■‖』■■』■■可‖‖|■■■】‖□』■■』■】□`■■■■‖γ‖|‖』■■■‖‖□‖□■■■』`』』』■■〗‖』■∏‖■■‖』‖||■■■∏|‖■]』■‖』‖□■

2

so文件。……………………………………….…685

爬虫监控方案,…….,.………,,…,……ˉ…ˉ….904

基于unidbg模拟执行so文件……...…692

附录爬虫与法律…….….…..…....…….…….….…9l7





■ 第]章

爬虫基础 」■ 在写爬虫之前,我们还需要了解一些基础知识,如HTTP原理`网页的基础知识`爬虫的基本原 【∏日】[■「|‖‖■■■■【「‖‖【■『‖■【口【厂『『【【■■】【【尸‖‖【■『‖}‖『广‖‖』■■■■■■〗『■】【厅【■『"【■■〗【□【■■【■【■′□■■■『【‖【巳■■『‖

理`Cookie的基本原理`多进程和多线程的基本原理等’了解这些内容有助于我们更好地理解和编写

网络爬虫相关的程序。

本章我们就对这些基础知识做—个简单的总结。

↑ˉ↑ 卜|丁丁尸基本原理 本节我们会详细了解HTTP的基本原理,了解从往测览器中输人URL到获取网页内容之间都发 生了什么°了解这些内容有助于我们进_步了解爬虫的基本原理。 ↑.0R|和0只L

我们先了解_下URI和URL°URI的全称为Unj{brmResou『ceIdenti∏er’即统_资源标志符;URL 的全称为UniversalResourceLocator’即统_资源定位符。它们是什么意思呢?举例来说,

ht印s://githuhcom/favlconjco既是_个URI’也是-个URL。即有favjconico这样_个图标资源我 们用上_行中的URI/URL指定了访问它的唯_方式’其中包括访问协议h卯s、访问路径(即根目录) 和资源名称。通过-个链接’便可以从互联网中找到某个资源’这个链接就是URI/URL° URL是URI的子集’也就是说每个URL都是URl,但并非 每个URI都是URL°那么,怎样的URI不是URL呢?除了URL’ URI还包括_个子类’叫作URN’其全称为UniversalResource

U∩‖ l



Name’即统一资源名称。URN只为资源命名而不指定如何定位 资源’例如um:isbn:045l450523指定了_本书的ISBN,可以唯 ≥凸四飞O

P↓^同=■巳■□■凹=■二·≡·=巳

■=====

■剧〃~■



□→厂亨→









.

_标识这本书’但没有指定到哪里获取这本书’这就是URN° 图lˉl URL`URN和URI关系图

URL`URN和URI的关系可以用图lˉl表示°

在目前的互联网中,URN使用得非常少’几乎所有的URI都是URL’所以对于_般的网页链接, 我们既可以称之为URL,也可以称之为URI’我个人习惯称URL°

但URL也不是随便写的,它也是需要遵循一定格式规范的’基本的组成格式如下: 5〔he‖e://[u5eI∩a∏e:p日55word0]∩o5t∩a|∏e[ :poIt][/pat∩][;par日爬ter5][?querγ][#十mg"e∩t]

其中’中括号包括的内容代表非必要部分’比如https://wwwbajdu.com这个URL’这里就只包含了 scheme和hostname两部分,没有port、path、parameters` query` fragment°这里我们分别介绍_下 几部分代表的含义和作用°

□scheme;协议°常用的协议有http`https`Rp等’另外schcme也被常称作protocol’二者都 代表协议的意思。

‖ 2

第l章爬虫基础

□usemame、password:用户名和密码°在某些情况下URL需要提供用户名和密码才能访问, □hostname:主机地址°可以是域名或IP地址’比如h叮s://wwwbaidu.com这个URL中的 hostname就是wwwbaiducom,这就是百度的二级域名°比如https://88.88这个URL中的 hosmame就是888.8,它是_个IP地址。

□pon:端口。这是服务器设定的服务端口’比如https://8.8.8ˉ8:l2345这个URL中的端口就是 ht甲s协议的默认端口是蚂3。所以h呻s://Wwwbajducom其实相当于ht印s://Wwwbaiducom:叫3,

』 | ‖

l2345°但是有些URL中没有端口信息,这是使用了默认的端口。http协议的默认端口是80’



』|」■■■■■■■■□■■■■=■■■■■‖‖‖‖』■■■■】■∏

这时候可以把用户名和密码放在host前面°比如https://ssr3.scrapecenter这个URL需要用户名 和密码才能访问’直接写为https://admm:[email protected]则可以直接访问。

而hnp://wwwbajducom其实相当于h忱p://wwwbaiducom:80°

□que『y:查询°用来查询某类资源,如果有多个查询’则用&隔开°query其实非常常见,比 如h仗ps:〃wwwbaiducom/s?wd司】ba&ie=utf8’其中的query部分就是wd=nba&ie=utf8’这里 指定了wd是nba, ie是utP8。由于query比刚才所说的parameters使用频率高很多’所以平 时我们见到的参数、GET请求参数、parameters、 params等称呼多数情况指代的也是query° 从严格意义上来说,应该用query来表示° □fTagment:片段°它是对资源描述的部分补充,可以理解为资源内部的书签。目前它有两个主 要的应用,-个是用作单页面路由,比如现代前端框架VUe、React都可以借助它来做路由管

理;另外-个是用作HTML锚点,用它可以控制一个页面打开时自动下滑滚动到某个特定的

||

面的内容°



|‖|‖{|

□path:路径°指的是网络资源在服务器中的指定地址,比如https://githuhcom/favlconico中的 path就是faviconico,指的是访问GitHub根目录下的faviconico° □paJametc蹈:参数。用来指定访问某个资源时的附加信息,比如https:〃8888:l2345/hello;user中的 user就是parameters°但是parameters现在用得很少,所以目前很多人会把该参数后面的query 部分称为参数,甚至把parameters和query混用。严格意义上来说’parameters是分号(;)后



位置°

以上我们简单了解了URL的基本概念和构成,后文我们会结合多个实战案例来帮助大家加深理解° 2.∩丁「尸和‖]ˉ「尸S

在爬虫中,我们抓取的页面通常是基于http或https协议的’因此这里首先了解一下这两个协议 的含义°

HTTP的全称是HypenextTransIerProtocol,中文名为超文本传输协议’其作用是把超文本数据从 网络传输到本地测览器,能够保证高效而准确地传输超文本文档°HTTP是由万维网协会(WOr‖dWide

WebCo∏sonjum)和Intemet工作小组IETF(IntemetEnginee∏ngTaskForce)合作制定的规范, 目前 被人们广泛使用的是HTTPl.l版本,当然,现在也有不少网站支持HTIP20° H∏P的发展历史见表lˉl° 表↑ˉ↑ ‖丁『p发展史 版



主要特点

l99l年

不涉及数据包传输,规定客户端利服务器之间的通信格 式’只能使用GET请求

没有作为正式的标准

HTTPl0

l996年

传输内容格式不限制,增加p0丁、p∧「〔‖` ‖[AD`0p丁IO‖5、

正式作为标准

0[L[『[命令

发展现状



||

产生时间

HT丁P0.9



』□□■■□』』】□〗』■·■■■日‖〗〖】■■■Ⅵ‖‖||划‖‖|||°‖』■』‖■

刚才我们了解了URL的基本构成,其支持的协议有很多’比如http` ht‖ps、 f↑p、 shp、smb等°





l.l

HTTP基本原理

3

(续) 版



HTTPll

产生时间

主要特点

l997年

持久连接(长连接)、节约带宽、HOST域、管道机制、

发展现状 正式作为标准并广泛使用

分块传输编码

HTTP2.0

20l5年

多路复用、服务器推送`头信息压缩、二进制协议等

逐渐覆盖市场

HTTPS的全称是HypenextTransferProtocoloverSecureSocketLayer’是以安全为目标的HTTp通 道,简单讲就是HTTP的安全版’即在HTTP下加人SSL层简称HTTPS。 HTTPS的安全基础是SSL’因此通过该协议传输的内容都是经过SSL加密的, SSL的主要作用 有以下两种°

□建立一个信息安全通道’保证数据传输的安全性。 □确认网站的真实性。凡是使用了HTTPS协议的网站,都可以通过单击测览器地址栏的锁头标 志来查看网站认证之后的真实信息’此外还可以通过CA机构颁发的安全签章来查询°

现在有越来越多的网站和App朝着HTrPS的方向发展,举例如下。 □苹果公司强制所有jOSApp在20l7年l月1日前全部改为使用HTTPS加密’否则App无法 在应用商店上架° □谷歌从20l7年l月推出的Chrome56开始’对未进行HTTPS加密的网址亮出风险提示’即

。|慧糕耀麓瞧蕊鲤黑嚣∏Ps请求迸行网络通信不满足条件胸域名 和协议无法正常请求。

HTTPS已然是大势所趋°

注:HTTP和HTTPS协议都属于计算机网络中的应用层协议’其下层是基于TCP协议实现的, TCP协议属于计算机网络中的传输层协议’包括建立连接时的三次握手和断开时的四次挥手等过程°

但本书主要讲的是网络爬虫相关知识,主要爬取的是HTTP/HTTPS协议相关的内容,因此这里就不 对TCP、IP等内容展开深人讲解了,感兴趣的读者可以搜索相关资料了解下,如《计算机网络》 《图解HTTP》等书°

3.∩丁『p请求过程 在测览器地址栏中输人一个URL,按下回车之

一曰 霞赡:鹏霸腮露上个慧需□← . 站服务器接收到请求后对其进行处理和解析,然后=≈却α笛.(晌应) · …国(请求)



返回对应的响应’接着传回测览器°由于响应里包

含页面的源代码等内容,所以测览器再对其进行解 析,便将网页呈现出来’流程如图1ˉ2所示°

客户迫



服劳器

图1ˉ2流程图

图1ˉ2中的客户端代表我们自己的电脑或手机测览器,服务器就是要访问的网站所在的服务器°

为了更直观地说明上述过程’这里用Chrome测览器开发者模式下的Network监听组件来做一下 演示°Network监听组件可以在访问当前请求的网页时’显示产生的所有网络请求和响应。 打开Chrome测览器’访问百度,这时候单击鼠标右键并选择“检查”菜单(或者直接按快捷键Fl2) 即可打开测览器的开发者工具,如图1ˉ3所示.



第l章爬虫基础

4

m…0凶■■■■■

甲巾

曳p





P

°■

…●



邱尸·■守

` Q.



▲巴巳上



Ⅱ马’叮



呵…

-二

………$……m

m



_

≤0=、克咖姨譬→争 ●≈…■■J…◆



邑…



…〖p

』=



…辑>『…了·…≥…7《

价4↑qⅢmpm■,露■■几几『…皿=■■$‘ ■ty佐口·止p1匈】…向〗甸"孕八…雅 尸冠■冯t…岭.d彦LA…αL【≈△ Tqp`F中·皿p1叮;『…5p .幻□/T●H?…夕 v划l■】凸◎蚀m…厂巳1韩■^.哲……『.

·毋▲

G呕色7』尸t.■√■乙厂喊回



■‖…T‘?T凹《 》

p刘…tl●■■O■γl岭

■恤№g”mj 』

■0Ⅻ』呼〖 厂em0【■■了 防1…吕 0铀F

n●』V 1·罕ˉ…h

叮印0.1D≡7■. β■ ■w 沾.np…〖唾『.=把山 p`0hγ巴■°9≡0蚀■■■■ [l●■■钉防≥0…印0■1p←丁■□

■№.唾Ⅵ蚀《$ 】】锣; …■撼蚌rt〗 $0单5

●■0t■↓Qˉ』叮p…口′d1U乒

·■0v』庐卢■口…℃代□〔…■巴.←t”■叼↑∏←“凶……obb→: lw↑∏←A$曲……0ob→:′01甘α

锈钙

■■『0i∩p占0凶m『

=”

』 ■

0■比1…7印 〗

一仓… …_盂丁



〃』驴〔绅占 b‖“■j

吵胆

●·私』vc啤09 ·■jvc啤0□ !.■岿…嘲 °.■岿…嚼 』四0义t凶雷Q ,四0义t凶圃0 ∩茅“$U立 0茅“$U或 Lβ…~7考吧哼′O』vˉ

◆<1v』0 ≤1v』0



《』『 且

怎″』呻酝



·



≥禽■ⅫMˉ恤忱^…寸 ↓‖绅●=0…唇1咏r】=汕…市们 ↓‖绅●=0…唇1咏r】=切…≈w■…J■1γ汾



………吨吨硒…呵呻吨 呵吨殉吨…呵

▲■■刁

bαLVf【●■z二■…■=7●…叼m·■≤′●』■ 贞/01沂

◆■Q』phdF…$lm■=●■皿…≡wˉ ◆Q·Ⅻn·F…【l“巳 ●■皿…….≥=■“Zb口 ≯=■′“b■

少『 阳 甩

■01V 』O已M1缚° c[mO′ ‖0告t←『…>』触凹申∏妇『中尖●■喇′●17° 芍°呻岭≡TT=□鹤….- ■….



啦■m…F 《

◆m「1吭■·■■′呸厅…



字●●

@《

伶嚏笛……粒…蹿…啼≈炮…呻…凹V…≡≈

×钒





幼沁山

●∏■G





□≡~-

乏吕

●□搅



●m气=p…

白皿

◆●● 十



《mu必口 西…L叮…广 ·ˉ/■』γ>

《mu必口 西…L叮…广 ·ˉ/■』γ>

瞄……Qh…

q』□皿 □k■

…{

}』■仁l坠$ .c■t』m<丽■…厂出尸c■Mm=G●9 …‘B可′·〗■ 己0』■仁l蚂0, .c■t』m<雨■…厂也尸佰曰M…t■….Dq′■〗■

巳1.…№《

…围吁翻q

卜裕起『m鞋√哇厂…P



…t电l蛔屯醉t●『O

p吨≈「妒t 〖”■v.t唾tJj…加mme产…J■唁7』pt尸

凸麓呼倒鳃】罢睡4…c….…罐h』km巫幽山…则且匹坞皿堑m的巫…』丛匹」“……‘u纠△△n…正刨…D率u》斗腆……, ■ˉ皿 】宁〗0ˉ凑√“6年己·





mdT·栖吨u·p吼〈



肺.『呵≈P

■,…′“mpc`



′i忱7陋R溶≥°讹锤c〃…丁呻但 $≈hpt■■2〃■笆△▲些■■0【″ 官==■0■d▲ .宁0′△T”忙』■=…〃【■J11■F■1■G0”<=仑L`

……………°≈…÷匡巳…●`=……

`00t啼tV阳弓P…↑

图lˉ3开发者T具界面

我们切换到Network面板,然后重新刷新网页,这时候就可以看到在Network面板下方出现了很

多个条目’其中—个条目就代表一次发送请求和接收响应的过程’如图lˉ4所示° ∩r安…例仗冗■

窑∩● +



x

日》

●畦y…

★力●i

m■‖羽■m咱哩G爪

m产

PZ

「 …妇

乙■…出……仪

■叫=

.闻



≈幼内………γ…

…『咕

■ =

{……

倒=

●0







……`腮△@Ⅻ钢蹿唾s呵…P……Ws ……

■」

『…



●o守α团睦Ⅶ叼田………· 企n

……0■……

琶…

~≈巴一凹~■ 曰…一少■ 琶……尸■ ■≡=Ⅵ…7瞳伊■ ■□…凸喊四?0亿硒 且—~

… … 咖 … 咖 …

≈Ⅵ0一 疤 ■■ 呵 吨 问 殉 心 吨 咆 呵

… 初

腔 旧

闻 吨



40哩

↑7m

独 睡 …0‖ …0↑

呻 内



$血也

}0呻



巳且吗

乙定 ■吨

0@蹿

田吨

=…0… 户………也徊电

■.肆心‖…熙泊 m ~…ˉ盯7■‖_…口℃汕 ■







汀D靶

田≈



Sp■■

↑■≈

aD嘘

↑S呻



a月也

00≈



a〈蝎

∏≈



y0也

Ⅶ蝉

a习唾

↑0酶

↑w四

■≈



…〗 …↑〗



西…口吧 =≈…… 尸=…佃U ■·≡戳■…闻

m 酗 亚 恤

….‖

…‖.‖ …M …M 呵 ” 旧 旧

厢 口℃ 户屯 ■℃

=顿妒≈≡ =≡



凹 咱





02P田

】y≈

河■沪‖…0Ⅷ0■父尸



恒 旧





“0咱

重吨



0a0四



Lg幽

加吨



0S仅巳

z≈



色…尸℃



■■■…恶『∩】∏·】{′》‖■■驴〗区Ⅵ〗』‖七‖Ⅵ口尸门§$▲『肾《Ⅸ■】βⅡ〗宁}

宁==

‖『‖】□Ⅱ【β≤■■■●咀·

…℃

牌=

×中

飞起…m



′′秘`久

化回…耐0幽=a0呻……∩■k…问■…产…≡→…≈ ↓…+…≈

图lˉ4Network面板

我们先观察第一个网络请求’即wwwbaiducom,其中各列的含义如下。 □第-列Name:请求的名称°-般会用URL的最后-部分内容作为名称。 □第二列Status:响应的状态码。这里显示为200,代表响应是正常的°通过状态码,我们可以 判断发送请求之后是否得到了正常的响应。







l.l

HTTP基本原理

5

[』尸「‖|】■■■■「

□第三列Protocol:请求的协议类型°这里h忱p/l.l代表HTTPl.l版本, h2代表HTTP20版本° □第四列】ype:请求的文档类型°这里为document,代表我们这次请求的是一个HTML文档, 内容是_些HTML代码°

□第五列Injtjator:请求源°用来标记请求是由哪个对象或进程发起的。

|‖巴■■『「■「‖|||‖■■「|■庐

□第六列Size:从服务器下载的文件或请求的资源大小。如果资源是从缓存中取得的,则该列会 显示fTomcache°

□第七列Tjme:从发起请求到获取响应所花的总时间° □第八列Waterfall:网络请求的可视化瀑布流。

我们单击这个条目’即可看到其更详细的信息’如图lˉ5所示。

| 「■‖■卜

蹦瓣;鳞》



导≡_

…m≡∩t{p■M障·比血白…



…噎座7

旦≡9…由

=■「卜巴尸似■■∏■■似}【卜

…G=●2■铡

勘~…

≡←1凹■2哩°凹·】日“3

°巴

…跨沉『1m电r鲍垂c……℃m

副≡≈ 守锤~

寸←…医==

回-≈

~1

≡…由囱

..→

哆…{

……w…卫”侣 …pr1v眺●

j豆…呵

≡…uv●

α种…`

…~咖印

导_…

囤…辖~≈ =.…!…00d诊0四尸

←…唾wm1?G企醉D…W吧

…≈rO曲驰【m11y〗铅8〗7叮 …■tp09帅l唾1】7:田■扣曲Y

呜→仁

……】o】

=0……尸 尸|

……;钾化沪√

.」………尸

≡■…j户…

一p

■■■尸【【■■‖|↓【■「『‖■〗‖◆「〖匡■【】「|巴■∏『『卜‖卜「■■尸|■=尸■■■「△尸、』■■■■∏|‖|巴■■■【■「巴■∏|‖‖一■■‖‖‖‖■■■■■■=■■■■■■尸〗■∩·■■似■■■「‖|‖■■■【■β

酋Q≈曳触~

吟≡………罗2型〕…巫泣7■……6Lm1无=酪靶;凹t№/;…1″·m心审匠■

凶响■……4p

…………■…m… 泽】…mDl酗∑工≡9吗问乙匿=?…■垂V

凹-

-…

.………尸

Ⅲ~【…v■b…

g ...…≡p

●=皆琴=≡

自■…≈彻

噎t凶口…1·叼u″1四灿m〗◆回o叼u■t1蜘′唾l;≈·■·l…/的M0……p…/…p▲/▲〗碱°■p酶u色●hm

=蝗α…p

∏′□…;…『…△·

阐■≈叼淤

≈~尸印,“↑…U怔

孕……尸

圃≡‰霄 结触~?-吟?…7…



…辱

=←m画0蜘;…9p■8≈■■0降拘β■·70〗■;≈.00m;钩.3 =审…

-…u津

…■=…1…3;m……】■7……呐拍〗∑】S…趴血山】晦砷【……■wjpT■01…≡匹…沙7■2…T■T

图lˉ5详细信息

首先是General部分’其中RequestURL为请求的URL,RequestMethod为请求的方法’StatusCode 为响应状态码’RemoteAddress为远程服务器的地址和端口’Refe∏erPolicy为Referrer判别策略。

继续往下可以看到ResponseHeaders和RequestHeaders’分别代表响应头和请求头°请求头中包 含许多请求信息’如测览器标识、Cookje`Host等信息,这些是请求的一部分’服务器会根据请求头 里的信息判断请求是否合法’进而做出对应的响应°响应头是响应的一部分,其中包含服务器的类型、 文档类型` 日期等信息’测览器在接收到响应后,会对其进行解析’进而呈现网页内容° 4请求

请求’英文为Rcquest’由客户端发往服务器,分为四部分内容:请求方法(RequcstMethod)、 请求的网址(RequestURL)、请求头(RequestHeaders)`请求体(RequestBody)°下面我们分别予以 介绍°

●请求方法

请求方法’用于标识请求客户端请求服务端的方式,常见的请求方法有两种:GET和POST°



第l章爬虫基础

在测览器中直接输人URL并回车,便发起了一个GET请求’请求的参数会直接包含到URL里°

]」||

6

例如’在百度搜索弓|擎中搜索Python就是一个GET请求,链接为h仇ps://wwwbajducom/s?wd=Python,

一个POST请求’其数据通常以表单的形式传输,而不会体现在URL中° GET和POST请求方法有如下区别°

□GET请求中的参数包含在URL里面,数据可以在URL中看到;而POST请求的URL不会包 含这些数据,数据都是通过表单形式传输的,会包含在请求体中。 □GET请求提交的数据最多只有l024字节’POST方式则没有限制°

登录时-般需要提交用户名和密码,其中密码是敏感信息,如果使用GET方式请求’密码就会 暴露在URL里面`造成密码泄露,所以这时候最好以POST方式发送。上传文件时’由于文件内容 比较大’因此也会选用POST方式°

我们平常遇到的绝大部分请求是GET或POST请求。其实除了这两个,还有_些请求方法’如 HEAD、PUT`DELETE`CONNECT`OPTIONS`TRACE等’我们简单将请求方法总结为表lˉ2° 表‖ˉ2请求方法 方







GET

请求页面,并返回页面内容

HEAD

类似于GET请求,只不过返回的响应中没有具体内容。用于获取报头

POST

大多用于提交表单或上传文件,数据包含在请求体中

PUT

用客户端传向服务器的数据取代指定文档中的内容 请求服务器删除指定的贞面

CONNECT

把服务器当作跳板’让服务器代替客户端访问其他网页

OPTIONS

允许客户端查看服务器的性能

TRACE

回显服务器收到的请求。主要用于测试或诊断

本表参考: http://wwwrunoohcom/htt枫httpˉmethodshtml。 ●请求的网址

请求的网址’它可以唯—确定客户端想请求的资源°关于URL的构成和各个部分的功能我们在 前文已经提及了,这里就不再赘述°

●请求头

请求头,用来说明服务器要使用的附加信息,比较重要的信息有Cookje、Referer`UscPAgent等。 下面简要说明一些常用的请求头信息°

□Accept:请求报头域,用于指定客户端可接受哪些类型的信息° □AcceptˉLanguage:用于指定客户端可接受的语言类型° □AcceptˉEncoding:用于指定客户端可接受的内容编码° □Host:用于指定请求资源的主机IP和端口号’其内容为请求URL的原始服务器或网关的位置° 从HTTPl.l版本开始,请求必须包含此内容。

□Cookie:也常用复数形式Cookles’这是网站为了辨别用户’进行会话跟踪而存储在用户本地 的数据°它的主要功能是维持当前访问会话°例如,输人用户名和密码成功登录某个网站后,

服务器会用会话保存登录状态信息,之后每次刷新或请求该站点的其他页面,都会发现处于登

勺日|{||』■〗』■可|』■曰|■■■·』司。■·□』‖|■□‖|』■■□■司』』■〗|■‖『■□】』■■■|■‖□]』■Ⅱ|』■‖|」』■Ⅲ■■

∏Fl′ETE

■Ⅲ■‖』■■■〗‖」■】∏|{‖■■■|」■■‖』■司‖□」ˉ■』□]■|』·∏|·|‖|‖{|■■∏〗{」勺|‘■」■】■Ⅵ‖』■■』=■■可‖|‖■■Ⅵ√‖

其中URL中包含了请求的query信息,这里的参数wd表示要搜寻的关键字。POST请求大多在提交 表单时发起。例如,对于_个登录表单,输人用户名和密码后,单击“登录,按钮’这时通常会发起

「β■■■■■■「|■『『)『『[■『)「|■「|■「|『‖「七【|匹·「[『∏|

l」 HTTP基本原理

7

录状态,这就是Cookje的功劳°Cookje里有信息标识了我们所对应的服务器的会话,每次测



览器在请求该站点的页面时,都会在请求头中加上Cookje并将其发送给服务器,服务器通过 Cookle识别出是我们自己’并且查出当前状态是登录状态’所以返回结果就是登录之后才能 看到的网页内容。

□Referer:用于标识请求是从哪个页面发过来的,服务器可以拿到这_信息并做相应的处理,如 做来源统计`防盗链处理等。

□UserˉAgent:简称UA,这是_个特殊的字符串头’可以使服务器识别客户端使用的操作系统 及版本、测览器及版本等信息°做爬虫时如果加上此信息,可以伪装为测览器;如果不加,很 可能会被识别出来°

□ContentˉType:也叫互联网媒体类型(IntemetMediaIype)或者MIME类型’在HTTP协议消

》『|△广「》|≥「‖

息头中’它用来表示具体请求中的媒体类型信息°例如’text/html代表HTML格式’image/gif

代表GIF图片’application(json代表JSON类型° 请求头是请求的重要组成部分’在写爬虫时,通常都需要设定请求头°

尸亡■■‖「|■尸|

●请求体

请求体,—般承载的内容是POST请求中的表单数据’对于GET请求’请求体为空。 例如’我登录GltHub时捕获到的请求和响应如图lˉ6所示。

辜ˉ三〕ˉ…氨唾伞净瓣瓣夺嚣嚼露狰钩必

!6■■

=电…

_□=一=_

;况;…肉…~……



乃-X…·律…a ■叁钓-

= 肛1;…Ⅵ…

v…琶…… $●【$Ⅻ



ˉ篡 互 ■



回■0…~

臼写0■~



■…田 ■…田

……

‖『●【■『■‖■

醛—

■-

…11葱■t』m/mu狰.99…/≈仿·…/■…0w◆;科·8 =匝t′…p呻u凹t……1◆■IU』 _Omp◆m,1m·●厂 『』■0网oQo峦Ⅷ0“◎2p■tβ韩.2 …ˉ 尸 …汀狮2闸·■0■0砷.0· 一

蘸……

‖■■■●】·■□』∏■●■■●□■』■■■■■〗■旦】】】·■■□●□〗=■■■□■

■■|()|》■■(‖(卜■厂|巴『〖『』匹『■「『‖匹■■『『

属ˉ茁…哮=揖蜀ˉ≡-≡ˉ≡翻薄宁藤≡=r趟…ˉ百醛ˉ『ˉ_ˉ………-ˉ罚亏 ●●哮官…=、…叮…●≡■绵…=^…≈…v ■-■■■一■=ˉ=

●………=1

=|星撬腮愚, 魏=…令.



j

u—F…

己_、. 』~ 、°!=~≡1

」…—…蛔^!髓撼瞄龋岛{箔mt……mc鳃mQˉ〗2ˉ圈》…`…血…,酶(…,M油…》c…拘, ·…

u_~ 出……

当…≈…m≈≡=……{Q………≈……啤α…

岂… 二…

岂… ■…

G… 」…

{-E呻皿 ;

…′

|~…』r…呻=蓟蝴γq…旧mⅢmS蚀m`g“c…犹酣j绅■…∏V巨▲Qm……炉

Ⅱ !…

自忌‖姆…如■榴…■龋‖叁雾…….

凶…j姆鹤~0……

图lˉ6详细信息

登录之前,需要先填写用户名和密码信息,登录时这些内容会以表单数据的形式提交给服务器’

此时需要注意RequestHeaders中指定ContentˉType为appljcation/xˉwwwˉfbrmˉurlencoded°只有这样设 置ContentˉD′pe’内容才会以表单数据的形式提交。另外’也可以将ContentˉIype设置为applicatjon0son 来提交JSON数据’或者设置为multipan/fbrmˉdata来上传文件° 表lˉ3列出了ContentˉType和POST提交数据方式的关系°



第1章爬虫基础

8

表‖ˉ3Co∩te∩tˉⅣpe和尸OS丁提交数据方式的关系 pOS丁提交数据的方式

Co∏te∩tˉⅣpe app‖ication/xˉwwwˉfb∏nˉurlencoded

表单数据

multipart/fb∏nˉdata

表单文件上传

app‖icatjon(json

序列化JSON数据

text/xml

XML数据

在爬虫中,构造POST请求需要使用正确的ContentˉType’并了解设置各种请求库的各个参数时 使用的都是哪种ContentˉType’如若不然可能会导致POST提交后无法得到正常响应。 5.晌应

●响应状态码

响应状态码,表示服务器的响应状态如200代表服务器正常响应、404代表页面未找到` 500代

表服务器内部发生错误。在爬虫中,我们可以根据状态码判断服务器的响应状态,如状态码为200’

证明成功返回数据’可以做进_步的处理,否则直接忽略。表lˉ4列出了常见的错误状态码及错误 原因°

表↑ˉ4常见的错误状态码及错误原因 说







继续

请求者应当继续提出请求。服务器已接收到请求的一部分,正在等待其余部分

切换协议

请求者已要求服务器切换协议’服务器已确认并准备切换

成功

服务器已成功处理了请求

已创建

请求成功并且服务器创建了新的资源

已接收

服务器已接收请求’但尚未处理

非授权信息

服务器已成功处理了请求,但返回的信息可能来自另一个源

无内容

服务器成功处理了请求,但没有返回任何内容

重置内容

服务器成功处理了请求’内容被重冒

部分内容

服务器成功处理了部分请求

多种选择

针对请求’服务器可执行多种操作

永久移动

请求的网页已永久移动到新位置,即永久重定向

302

临时移动

请求的网页暂时跳转到其他页面,即暂时重定向

303

查看其他位置

如果原来的请求是POST,重定向目标文档应该通过GET提取

3似

未修改

此次请求返回的网贞未经修改,继续使用上次的资源

305

使用代理

请求者应该使用代理访问该网页

307

临时重定向

临时从其他位置响应请求的资源

400

错误请求

服务器无法解析该请求

40l

未授权

请求没有进行身份验证或验证未通过

403

禁止访问

服务器拒绝此请求

未找到

服务器找不到请求的网页

方法禁用

服务器禁用了请求中指定的方法

‖佣

405

|]

" ‖α ‖" ∑Ⅷ ∑胆 ∑仍 ∑叫 ∑仍 ∑帕 ∑" ]川 ]

状态码

|司(』〗‖』』■■‖□‖||。■可■]」■■■■‖‖」|□口」‖‖□』□■‖〗‖」‖』■〗』】‖』■司

响应,即Response’由服务器返回给客户端’可以分为三部分:响应状态码(ResponseStamsCode)` 响应头(ResponseHeaders)和响应体(ResponseBody)°















|‖

厂|||巴■「β『『|▲伊『||丘■厂『「||匹●「||■∏‖|卜「

l .l

HTTP基本原理

9

(续)

状态码







||■尸『‖‖|■■「‖■【『|■『「||■■「巴■厂〔厂『’‖巨■「■厂匹尸}【■「}||▲尸‖||且■「』〗『■■「)■「■『||■『‖|已■尸|‖[■「’■■|■∏|^『■『『‖卜『□■『{■厂||‖■尸|

406

不接收

无法使用请求的内容响应请求的网页

407

需要代理授权

请求者需要使用代理授权

408



请求超时

服务器请求超时

409

冲突

服务器在完成请求时发生冲突

4l0

已删除

请求的资源已水久删除

4ll

需要有效长度

服务器不接收不含有效内容长度标头字段的请求

4l2

未满足前提条件

服务器未满足请求者在请求中设置的某一个前提条件

4l3

请求实体过大

请求实体过大,超出服务器的处理能力

4l4

请求URI过长

请求网址过长,服务器无法处理

4l5

不支持类型

请求格式不被请求页面支持

416

请求范围不符

页面无法提供请求的范围

4l7

未满足期望值

服务器未满足期望请求标头字段的要求

5"

服务器内部错误

服务器遇到错误’无法完成请求

50l

未实现

服务器不具备完成请求的能力

502

错误网关

服务器作为网关或代理,接收到上游服务器的无效响应

503

服务不可用

服务器目前无法使用

5佣

网关超时

服务器作为网关或代理,没有及时从上游服务器接收到请求

505

H∏P版本不支持

服务器不支持请求中使用的HTTP协议版本

●响应头

响应头,包含了服务器对请求的应答信息,如ContentˉType` Se「ver、SetˉCookie等°下面简要说 明-些常用的响应头信息° □Date:用于标识响应产生的时间°

□LastˉModjhed:用于指定资源的最后修改时间。

□ContentˉEncoding:用于指定响应内容的编码° □Server:包含服务器的信息’例如名称`版本号等°

□ContentˉType:文档类型,指定返回的数据是什么类型,如text/html代表返回HTML文档’ applicatjon/xjavascnpt代表返回JavaScript文件’imagc0peg代表返回图片° □SetˉCookie:设置Cookie。响应头中的SetˉCookie用于告诉测览器需要将此内容放在Cookie中’

卜|‖|》||■『‖||「||■■■【‖〖『‖|■「‖‖儿尸‖〖【·『‖‖▲厂‖【『『‖|[「

下次请求时将Cookie携带上°

□Expires:用于指定响应的过期时间,可以让代理服务器或测览器将加载的内容更新到缓存中°

| ::′访问相阔的内容时就可以直攘从缓存中加载达到降{哪务器负载缩…恫的 ●拘应体

响应体,这可以说是最关键的部分了’响应的正文数据都存在于响应体中,例如请求网页时’响 应体就是网页的HTML代码;请求_张图片时’响应体就是图片的二进制数据°我们做爬虫请求网页 时’要解析的内容就是响应体,如图lˉ7所示。



第l章爬虫基础



…减…』…t0

』瓣瑶南瞻『摇瓣

司||』】||』日|■可|(Ⅵ]‖

l0

■■■|‖‖‖



□(‖

勺鳃P妇…P…>《0…t…(‖《咕丁油…建…几…《』“·∏厂●f·■t山(/←(Q=[吧}◆》/】0M‖油$唾t‘ αt;■w凶由畦7宙…t$0mM1…‖~t;t■威/cm…w幻t;…γ《CUlO厂8…;恤c约7…8ww;} 々【……………义』…切↑=c■■口■订t面6』牢1叮g…】.≥

图1ˉ7响应体内容



」■■■]||√‖‖‖|‖』日

在测览器开发者工具中单击P爬view,就可以看到网页的源代码’也就是响应体的内容’这是爬 虫的解析目标°在做爬虫时’我们主要通过响应体得到网页的源代码、JSON数据等,然后从中提取 相应内容。 ‖□∏

‖|(

本节我们了解了HTrP的基本原理,大概了解了访问网页时产生的请求和响应过程。读者需要好 好掌握本节涉及的知识点,在后面分析网页请求时会经常用到这些内容° 6.‖∏丁P20



下面我们就来了解一下HTTP20相比HTrPl.l来说’做了哪些优化° ●二进制分帧层

HTTP20所有性能增强的核心就在于这个新的二进制分帧层。在HTTPlx中’不管是请求

(Request)还是响应(Response),它们都是用文本格式传输的’其头部(Headers)、实体(Body)之 起来更加高效°同时将请求和响应数据分割为更小的帧’并采用二进制编码。 所以这里就引人了几个新的概念。

(RequestHeadersFrame)和请求体/数据帧(RequestDataFrame)。 □消息:与逻辑请求或响应消息对应的完整的_系列帧°

在HTTP20中’同域名下的所有通信都可以在单个连接上完成’该连接可以承载任意数量的双 向数据流°数据流是用于承载双向消息的,每条消息都是—条逻辑HTTP消息(例如请求或响应),

‖||·

□数据流:-个虚拟通道’可以承载双向的消息’每个流都有—个唯_的整数ID来标识°

刊□□

□帧:只存在于HTTP20中的概念,是数据通信的最小单位。比如—个请求被分为了请求头帧

』■■】」‖|』■‖|』■■‖

间也是用文本换行符分隔开的。HTTP20对其做了优化,将文本格式修改为了二进制格式,使得解析



‖●]·」‖□■γ」‖||』■■‖|°‖=■■■‖出■■■■〗

有读者这时候可能会问,为什么不叫HTTPl.2而叫HTTP20呢?因为HTTP2.0内部实现了新 的二进制分帧层’没法与之前HTTPlx的服务器和客户端兼容’所以直接修改主版本号为20°

‖ | (

前面我们也提到了,HTTP协议从20l5年起发布了20版本’相比HTTPl.l来说’HTTP20变 得更快、更简单`更稳定°HTTP20在传输层做了很多优化,它的主要目标是通过支持完整的请求与 响应复用来减少延迟,并通过有效压缩HTTP请求头字段的方式将协议开销降至最低,同时增加对请 求优先级和服务器推送的支持’这些优化—笔勾销了HTTP1·l为做传输优化想出的-系列‘歪招”°



qq

它可以包含—个或多个帧°

简而言之’HTTP20将HTTP协议通信分解为二进制编码帧的交换’这些帧对应着特定数据流中 的消息’所有这些都在_个TCP连接内复用,这是HTTP20协议所有其他功能和性能优化的基础。







●多略复用

在HTTPlx中,如果客户端想发起多个并行请求以提升性能’则必须使用多个TCP连接’而且测





p

卜卜卜卜

l』HTTP基本原理

ll

览器为了控制资源,还会对单个域名有6~8个TCP连接请求的限制°但在HTTP20中’由于有了二进 厂↑

制分帧技术的加持’HTTP2.0不用再以TCP连接的方式去实现多路并行了,客户端和服务器可以将

P

HTTP消息分解为互不依赖的帧,然后交错发送,最后再在另_端把它们重新组装起来’达到以下效果°

∏‖‖■■…田

□并行交错地发送多个请求,请求之间互不影响。 □并行交错地发送多个响应,响应之间互不干扰°

□使用-个连接并行发送多个请求和响应°

■■尸■■尸

□不必再为绕过HTIPlx限制而做很多工作。

□消除不必要的延迟和提高现有网络容量的利用率’从而减少页面加载时间°

这样一来’整个数据传输性能就有了极大提升°

》 二尸「「■尸‖

□同域名只需要占用一个TCP连接,使用一个连接并行发送多个请求和响应’消除了多个TCP 连接带来的延时和内存消耗°

□并行交错地发送多个请求和响应’而且它们之间互不影响° ■』

【■尸『但■■『}■■

□在HTTP20中,每个请求都可以带_个3l位的优先值, 0表示最高优先级,数值越大优先级 越低°有了这个优先值’客户端和服务器就可以在处理不同的流时采取不同的策略了’以最优 的方式发送流、消息和帧°

「β

●况捏刷 流控制

流控制是一种阻止发送方向接收方发送大量数据的机制,以免超出后者的需求或处理能力°可以

■尸「‖|【位■面『■‖■尸「||||■尸‖|’||)|巳■「‖‖‖‖Ⅱ■■『}||[砂■β||||巴■■【□‖|巴ˉ■『「|}巴=■「》‖}‖』

理解为’接收方太繁忙了’来不及处理收到的消息了’但是发送方还在一直大量发送消息’这样就会 出现一些问题。比如,客户端请求了—个具有较高优先级的大型视频流’但是用户已经暂停观看视频 了’客户端现在希望暂停或限制从服务器的传输,以免提取和缓冲不必要的数据°再比如’-个代理 服务器可能具有较快的下游连接和较慢的上游连接’并且也希望调节下游连接传输数据的速度以匹配 上游连接的速度,从而控制其资源利用率等°

HTTP是基于TCP实现的,虽然TCP原生有流控制机制’但是由于HTTP20数据流在一个TCP连 接内复用’TCP流控制既不够精细,也无法提供必要的应用级API来调节各个数据流的传输° 为了解决这一问题,HTTP20提供了一组简单的构建块,这些构建块允许客户端和服务器实现它 们自己的数据流和连接级流控制°

□流控制具有方向性.每个接收方都可以根据自身需要选择为每个数据流和整个连接设置任意的 窗口大小°

□流控制的窗口大小是动态调整的°每个接收方都可以公布其初始连接和数据流流控制窗口(以 字节为单位),当发送方发出0∧『∧帧时窗口减小’在接收方发出‖I‖Dα‖0p0∧『[帧时窗口增大。 □流控制无法停用。建立HTTP20连接后,客户端将与服务器交换5[∏I‖C5帧,这会在两个方 向上设置流控制窗口°流控制窗口的默认值设为65535字节’但是接收方可以设置一个较大的 最大窗口大小(23|-l字节),并在接收到任意数据时通过发送‖I‖0α‖0P0A『[帧来维持这一大小° □由此可见’HTTP20提供了简单的构建块’实现了自定义策略来灵活地调节资源使用和分配 逻辑,同时提升了网页应用的实际性能和感知性能° ●服务端椎送

HTTP20新增的另—个强大的功能是:服务器可以对一个客户端请求发送多个响应°换句话说’ 除了对最初请求的响应外,服务器还可以向客户端推送额外资源’而无须客户端明确地请求。

如果某些资源客户端是一定会请求的,这时就可以采取服务端推送的技术’在客户端发起-次请 求后,提前给客户端推送必要的资源’这样就可以减少一点延迟时间。如图lˉ8所示’服务端接收至[

k

第l章爬虫基础

HTML相关的请求时可以主动把JS和CSS文件推送给客户端’而不需要等到客户端解析HTML时再

发送这些请求。

……■蹿ˉ ~些里二=』

二●

筑『Pa们〗:/p己geht『W‖(客户竭请求)

图lˉ8服务端推送

另外,主动推送也遵守同源策略’即服务器不能随便将第三方资源推送给客户端’而必须是经过 服务器和客户端双方确认才行,这样也能保证—定的安全性° ●HTTP2.0发展现状

HTTP2.0的普及是—件任重而道远的事情’一些主流的网站现在已经支持HTTP20了’主流测 览器现在都已经实现了对HTTP2.0的支持,但总体上’目前大部分网站依然以HTIPl.l为主°

■■||‖]』■∏|□【■]|」』■】‖|‖■■|■■■■

服务端可以主动推送’客户端也有权利选择是否接收°如果服务端推送的资源已经被测览器缓存 过’测览器可以通过发送RSTSTREAM帧来拒收。

(|·

5t爬己m丑/灭筛pt。j5(pu5‖p『o『w‖5e) 5t把己们0:/Stγ‖eC55(pu臼hp「O『WiSe)

‖日□·■■

{ ,瞬↑| …|,翻‖|撇|| 』删}

|』·||‖‖■‖‖」』‖

‖∏P2,0连接

】||□□】||』‖||」■‖||□口||口]|

l2

另外’一些编程语言的库还没有完全支持HTTP20,比如对于Python来说, hyper、hnpx等库已 经支持了HTTP20,但广泛使用的requests库依然只支持HTTPl.l。 ‖

7.总结

本节介绍了关于HTTP的_些基础知识,内容不少,需要好好掌握,这些知识对于后面我们编写



和理解网络爬虫有非常大的帮助° {

本节的内容多数为概念介绍’部分内容参考如下资料°



□《HTTP权威指南》_书。

gMDNWebDocs上关于HTTP的介绍°

□Google开发者文档中关于HTTP2的介绍°

|」}

□百度百科上HTTP相关的内容。

(|

□维基百科上HTTP相关的内容。





尸■

□FunDebug平台上的博客文章‘‘—文读懂HTTP/2及HTTP/3特性,,。 □知乎上的文章‘‘-文读懂HTTP/2特性”。

用测览器访问不同的网站时’呈现的页面各不相同’你有没有想过为何会这样呢?本节我们就来 了解—下网页的组成、结构和节点等内容° ↑.网页的组成

网页可以分为三大部分—HTML、CSS和JavaSc门pt°如果把网页比作_个人’那么HTML相

当于骨架` JavaScript相当于肌肉`CSS相当于皮肤,这三者结合起来才能形成_个完善的网页。下 面我们分别介绍一下这三部分的功能°

|‖《|

↑.2Web网页基础

l2web网页基础

l3

●HTML

HTML(HypeRextMarkupLanguage)中文翻译为超文本标记语言,但我们通常不会用中文翻译 来称呼它,_般就叫HTML。

HTML是一种用来描述网页的语言。网页包括文字、按钮`图片和视频等各种复杂的元素’其基

础架构就是HTML°网页通过不同类型的标签来表示不同类型的元素,如用1阳g标签表示图片`用 「|



γjdeo标签表示视频用p标签表示段落’这些标签之间的布局常由布局标签diγ嵌套组合而成’各 种标签通过不同的排列和嵌套形成最终的网页框架°

~■厂| |■■「β『「‖■尸

那HTML长什么样子呢?我们可以随意打开-个网站,比如淘宝网首页’然后单击鼠标右键选择

“检查元素,,菜单或者按Fl2,即可打开测览器开发者工具’接着切换到Elements面板,这时候呈现

的就是淘宝网首页对应的HTML’它包含了_系列标签测览器解析这些标签后,便会在网页中将它 们喧染成一个个节点’这便形成了我们平常看到的网页。比如在图lˉ9中可以看到_个输人框就对应

匹 二 ■ 『‖‖‖

~个i∩put标签可以用于输人文字° 啥一…

_…



→…

≡″

弓千

苛膨

→…

■●

→…

=们

←矽

一…

一…

乙广口巳『||坚■■「



★ ■ ●

■=二H

卜|‖

|匡■■厂|





‖_“曰■}

(盂j 互 、m唾 (瑟ˉm

·瞅. ■ 蕊

淘窜 …ao Ⅶ№凸O

≡■…●■心…m呻叫■■士响■而……■良申油抄Ⅸ …■…■心…m呻呻■■…≈而…m■■□私…m



田燃酗泊m cm…≈=…~…铸…=′… F~

i估

—蹿■茁■■■p啥=…串←■=△←■□■■←凸·■=罕√■■■咀■■¥ ≡一_=_■



ˉ . ^ …. .`≡…; ′ =″,

●0 o 】 翼

■厂}

Q·炉 吻№

·●加一c`■″…』…西…■~□…P宙 ■ 厂

T<』■【……=…萨





叮□止尸■==一

广仁‖‖|【△厂||)[■∏|||匹尸『■「[尸|}』‖■β「||■尸



ˉ 〃0…

…龟…≡们ˉ

二旦二二 户…函…=_审】户……≥盯J山w

■………=■=Ⅶ…■●11≈ <罕0澎ˉˉˉ了…”拧蹿Ⅱ浑■0………卢赤沪

.哼瞒t芦≈…≡铲■《■虫洒吨`……■切…lo妒但°w闺,

<…碰阐……≈″j·.≈`≡喊市

f蓖=《

■·啼←营mRm蜒灯■…泣≡→

.

. 。 岭.、… 。

_…屯《

= 霹2蹿|艘囚蚀』

<…~=■~m……■ ■呻0嘲.= 广=…沁……·…·—』■T护

.

`1 ˉ

‘|=〔M``匹·胎 “c……哇7加…代炉7』■=It沪←Um≡…f止……l`闺凹 .`

.

厂.·- ■d■n~

ˉ



ˉ

…ˉ≠。ˉ .



.

】扣=~≡■气■气■

0

r和■→………:’

输尸,·四`·!

′-彬

b=ˉ……w■=~≈=心…………■…一■=~■—…=……~蹿v古坠

.

图lˉ9淘宇网网页源码

不同标辖对应不同的功能,这些标签定义的节点相互嵌套和组合形成了复杂的层次关系’就形成

|}匹●

了网页的架构。 ●CSS

HTML定义了网页的架构,但是只有HTML的页面布局并不美观,有可能只是节点元素的简单 排列°为了让网页更好看一些’可以借助CSS来实现。 b

CSS’全称叫作CascadingStyleSheets’即层叠样式表°“层叠,,是指当HTML中引用了多个样 式文件,并且样式发生冲突时,测览器能够按照层叠顺序处理这些样式。“样式,,指的是网页中的文 字大小、颜色`元素间距`排列等格式。CSS是目前唯一的网页页面排版样式标准’有了它的帮助’ ‖

页面才会变得更为美观。



匹 凸 尸 }■「|

第l章爬虫基础

l4

在图lˉ9中’Styles面板呈现的就是-系列CSS样式,我们摘抄一段: #he己d-wmpper.5ˉp5ˉi51jte .5ˉpˉtop{



这就是一个CSS样式°大括号前面是-个CSS选择器’此选择器的意思是首先选中jd为

headˉ)‖rapper且c1a55为5ˉpsˉi51jte的节点’然后选中此节点内部的c1a55为5_p_top的节点。大括 号的内部就是_条条样式规则, po5jtjo∩指定了这个节点的布局方式为绝对布局, botto"指定节点的 下边距为40像素,"jdt∩指定了宽度为l0O%,表示占满父节点, height则指定了节点的高度°也就

{ | |

po5itio∩: 己b5o1l』tej bottoⅧ: 40pxj Width: 1毗j ∩eig∩t: 181px;

是说,我们将位置、宽度`高度等样式配置统-写成这样的形式’然后用大括号括起来’接着在开头

在网页中’一般会统_定义整个网页的样式规则’并写人CSS文件中(其后缀为css)。在HTML 中,只需要用11∩促标签即可引人写好的CSS文件’这样整个页面就会变得美观、优雅° ●Javascript

JavaScnpt简称JS’是一种脚本语言°HTML和CSS组合使用,提供给用户的只是—种静态信息, 缺乏交互性。我们在网页里还可能会看到_些交互和动画效果’如下载进度条`提示框`轮播图等, 这通常就是JavaSc∏pt的功劳。JavaSc∏pt的出现使得用户与信息之间不只是_种测览与显示的关系, 还实现了一种实时、动态、交互的页面功能°

JavaSc门pt通常也是以单独的文件形式加载的’后缀为js’在HTML中通过5〔rjpt标签即可引人’ 例如: <5〔ript5r〔="jql』erγˉ2。1.αj5"〉〈/s〔rjPt〉

·

综上所述’ HTML定义了网页的内容和结构,CSS描述了网页的样式, JavaScript定义了网页的 行为°

■ ■ ‖ β ` ∏ · 】 ■ □ 』 ■ 」 ‖ 』 ■ 司 』 · | ‖ ‖ 」 勺 口 |‖

加上CSS选择器’这就代表这个样式对CSS选择器选中的节点生效,节点就会根据此样式来展示了°

2.网页的结构

我们首先用例子来感受_下HTML的基本结构。新建_个文本文件,名称叫作teSthtml’内容如下: 〈!皿Ⅳp[∩t∏1〉

〈‖tⅦ1〉

〈∏论t己〔har5et="0丁「ˉ8"〉

<tit1e》丁hj5153Dem</t1t1e〉 〈/恫ead〉

〈bOdγ〉 〈djvjd=0′〔o∩ta1∩er"〉

〈diγ〔1a55=0『Wrapper"〉 <h2〔1a55="tjt1e啊〉‖e11O"Or1d〈/∩2〉

〈p〔1己S5="teXt">‖e11o’ tM5i5aParagmPh.〈/P> </diV〉

=■‖‖|』■〗‖』■■]』‖』■·|‖‖』■∏凸■】

〈head〉

</d1γ〉

〈/body〉 </htⅦ1〉

这就是—个最简单的HTML实例。开头用00〔Ⅳp[定义了文档类型’其次最外层是‖t"1标签’

代码最后有对应的结束标签表示闭合。ht∏1标签内部是he己d标签和body标签,分别代表网页头和网 页体’它们同样需要结束标签°‖ead标签内定义了_些对页面的配置和引用,上述代码中的〈"eta char5et="0「「ˉ8"〉指定了网页的编码为UTFˉ8°

tjt1e标签则定义了网页的标题’标题会显示在网页的选项卡中,不会显示在正文中。body标签



||



l5

内的内容是要在网页正文中显示的°d1γ标签定义了网页中的区块,此处区块的1d是〔o∩t己j∩er, jd是 一个非常常用的属性’其内容在网页中是唯_的,通过它可以获取这个区块°然后在此区块内又有个d1γ标签’它的〔1己55为"rapper,这也是_个非常常用的属性,经常与CSS配合使用来设定样式° 然后此区块内部又有-个‖2标签’代表—个二级标题;另外还有一个P标签,代表-个段落°若想 在网页中呈现某些内容’直接把内容写人h2标签和P标签中间即可,这两者也有各自的c1a55属性°



将代码保存后,双击该文件在测览器中打开,可以看到如图1ˉ10所示的内容°







■=■■■

■…■≈≡■一■口·

■一=■■●

_●



■≈

●丁·°

ˉ—

丁钞Gc西司}Ⅷ。哪幻…m叼佃叼………〕赢

…丸

×】士

· 臼『●……



【‖■‖‖▲『『[「‖‖β「‖止『|[「|儿尸|)[尸『|卜}|『‖| 》 「 | 卜 | 》 「 [■厂■}匹◆尸||=■『「||‖△∏『|

~尸『| 化■尸|)■尸|} 巳■| ■ 尸 |

| ‖ 份

} [ 户 但 『 『 | | ■ 『 『 ‖ ● |

←■乙

佣e‖◎W◎『‖d 蚀9甘归妇■四酶额

图1ˉl0运行结果

可以看到’选项卡上显示ThjsisaDemo字样,这是我们在∩ead标签中的tjt1e里定义的文字。

网页正文则是由body标签内部定义的各个元素生成的,可以看到这里显示了二级标题和段落° 这个实例便是网页的—般结构°_个网页的标准形式是∩tⅦ1标签内嵌套∩ead标签和body标签, ∩ead标签内定义网页的配置和引用, body标签内定义网页的正文。 3ˉ节点树及节点间的关系

在HTⅣⅡ中’所有标签定义的内容都是节点,这些节点构成一个HT皿节点树’也叫HT皿.mM树° 先来看_下什么是DOM°DOM是W3C(万维网联盟)的标准’英文全称是DocumentObject

Model’即文档对象模型°它定义了访问HTML和XML文档的标准。根据W3C的HTMLDOM标准’ HTML文档中的所有内容都是节点。 □整个网站文档是_个文档节点°

□每个∩t∏1标签对应一个根节点’即上例中的html标签,它属于_个根节点°

□节点内的文本是文本节点,比如a节点代表—个超链接,它内部的文本也被认为是—个文本节点° □每个节点的属性是属性节点,比如a节点有一个hre+属性’它就是一个属性节点。 □注释是注释节点,在HTML中有特殊的语法会被解析为注释,它也会对应_个节点°

点儿

档|节加 文 根 巾

|—|

因此,HTMLDOM将HTML文档视作树结构’这种结构被称为节点树,如图1ˉ‖l所示°

节β |

节点



—【

||

节点

〈tjt1e〉

属性 ∩re十



本 本文









本 本文

本题





文档 文



_|—_‖



〈be日d〉

文硼 题 删—|一—Ⅷ—|





文懒 接 —

|||



l.2Web网页基础

图lˉ11

节点树



l6



第l章爬虫基础 父节点

通过HTMLDOM’节点树中的所有节点

均可通过JavaSc∏pt访问’所有HTML节点元

根节点 〈∩tⅧ1〉 Ⅷ1〉 『

素均可被修改`创建或删除°

节点树中的节点彼此拥有层级关系°我们

■可‖』日

第一个子节点 (∩旧tChild)

节点 <‖ead〉

常用父(parent)`子(chj‖d)和兄弟(sibling)



等术语描述这些关系。父节点拥有子节点’同 级的子节点被称为兄弟节点°

戳“

在节点树中,顶端节点称为根(root)。除

了根节点之外’每个节点都有父节点,同时可

节点

拥有任意数量的子节点或兄弟节点。图lˉ12

<bodγ〉

图lˉl2节点树及树中节点间的关系

4.选择器 最 我们知道,网页由_个个节点组成’CSS选择器会为不同的节点设置不同的样式规则’那么怎样

( ‖ |

展示了节点树以及树中节点间的关系。

后‖

定位节点呢?

…轩

点 〔o∩ta1∩er’那么这个节点就可以表示为#co∩taj∩eI,其中以#开头代表选择1d,其后紧跟的是1d的

哩霉霹厂



{』 ■ ■ □ ■

在CSS中’使用CSS选择器来定位节点。例如’“网页的结构”一节的例子中djγ节点的jd为 叫节

名称°如果想选择〔1a55为Wmpper的节点,则可以使用.Wrapper,这里以.开头代表选择〔1a55,其

另外, CSS选择器还支持嵌套选择,利用空格把各个选择器分隔开便可以代表嵌套关系,如

」‖]■■‖

‖‖』司

」‖|』』■|

#Co∩ta1∩er ."raPperP代表先选择1d为〔O∩taj∩er的节点,然后选择其内部〔1aS5为"raPPer的 节点’再进_步选择该节点内部的p节点°要是各个选择器之间不加空格,则代表并列关系,如 d1γ#〔o∩taj∩er ."mpperp.text代表先选择jd为〔o∩ta1∩er的d1γ节点’然后选择其内部〔1a55为 "rapper的节点,再进_步选择这个节点内部的c1as5为text的p节点°这就是CSS选择器,其筛选

■■可』】‖‖‖‖{■■〗

直接用h2即可°这些是最常用的三种方式’分别是根据1d、C1a55`标签名选择,请牢记它们的写法°

一■司{

后紧跟的是〔1a55的名称。除了这两种’还有—种选择方式,就是根据标签名’例如想选择二级标题’



功能还是非常强大的°



我们可以在测览器中测试CSS选择器的效果,依然还是打开测览器的开发者工具,然后按快捷键

Ctr‖+F(如果你用的是Mac’则是Command+F),这时候左下角便会出现一个搜索框,如图lˉl3所示°

0



●, …

★力●

制●‖◎Wmd



…》甘m比●…酗爬

d

儡田 曰…m ·…●. s鳞…呻w°厂忌而聪…隘潞_ˉ=啤…占-… p乖玲·</…>

`….ctQ也围! ′●t…t◇■tγt·《

v炯1γ】d■铃c“t■1哇「芭≥



m″…mr0■∩■r

血∏【

<pC`■m■.t红0俺≥"euo0 tMD1■□■田0r■pb.≤/P

々mv>

;』■印0簿百0§≈■′

{|■■]|‖



|』■||」‖||■可』‖|

●<…γ≥



·|

≤∩t■1>

v<加〔【m■伍芍…广> 唾c1m■≡ot』t沁≈和uc№『u</∩2>

巾 : ×|

囊…,

<)吨而[胎Ⅷl≥

图lˉl3搜索节点

|| | |

|攀} | ||….

}| l.2Web网页基础



l7

这时候我们输人.t1t1e就是选中了〔1a55为tjt1e的节点’该节点会被选中并在网页中高亮显示’ 如图lˉl4所示°

b

×[





十+co文件{ ^』甄 当



_=

★白●

hm1

=~—■■



问″0◎认′o妈『



.砧弯…扫p叮呐.

仕 __——==—

匿□≈■m

b

◎■…跑=…尸■如丽 啼…Y=………

* ;

x

吨榨吐■u ≤∩饵l≥

p=</邑≈

β

■…γ> ●妇』v…emt●…广>



勺≤q1v〔边3B罕闪■…广> m哦$哉r旗轴呻l℃O申7M呼∩蜜

中clm■■40t啤t矽扣№u◎0 tM■1S●户『印…▲叼隘 √@…

喇竿myJO〖…j }

≈—

b

■……£多妨m…《

mγf

_

■■■■■■■■■■■■■■■



√…>

b

</∩t■I> =



≡●

…哟≡ 呵—

——-

●‖■0!^甲{



图lˉl4节点搜索结果

b

p

p

输人d1γ#〔o∩taj∩eI.Mapperp.text就逐层选中了1d为〔o∩ta1"er的节点中〔1a55为wmppeI的 节点中的P节点,如图lˉl5所示° ■■门■

_●

_★

+今·o文仰M』塑↑w醉∏℃7俩中α●西……伞‖巾碱

≡办

· 廖Ⅲ5霹5盂孟—ˉ百■ˉˉ

0

●。··

p

p

b



戳:ˉ

p

0

■0…耗械户u <M■I≥ ◆

p



0

i■″…钉↑

b

p 色挫牵



b

图1ˉl5节点搜索结果





CSS

选择器还有一些其他语法规则,具体如表lˉ5所示°



表‖ˉ5CSS选择器的其他语法规则 例

选择器



例子描述



·〔1a55

·i∩tro

选择C1a55=』0j∩trO!!的所有节点

#1d

#十1r5t『]a『γ}e

选择1d=!干irSt∩a们e"的所有节点



选择所有节点

b

仿











e1e‖e∩t

p

选择所有p节点





第l章爬虫基础

l8

(续) 例

选择器

例子描述



diγ’p

e1e贬∩te1e爬∩t

diγp

选择diγ节点内部的所有p节点

e1e框∩t〉e1e眶∩t

diγ〉p

选择父节点为djγ节点的所有p节点

e1e爬∩t十e1e爬∩t

djv+p

选择紧接在div节点之后的所有p节点

选择带有target属性的所有节点

[attribute]

[target]

[attribute=va1ue]

[target=b1a∩k]

选择taIget≡"b1a∩假"的所有节点

[tjt1e~=十1O眶r]

选择tjt1e属性包含单词十1OweI的所有节点

[attIibute~=γa1ue]

|||」

e1e‖记∩t’e1e∏论∩t

选择所有div节点和所有p节点

:1i∩代

a:1i∏低

选择所有未被访问的链接

:γjSjted

己:γi5jted

选择所有已被访问的链接

:己Ctjγe

日;a〔t1γe

选择活动链接

d

:hOγer

己:hoγer

选择鼠标指针位于其上的链接



:fO〔uS

i∩put:「oCuS

选择获得焦点的i∩put节点



: :+iIStˉ1etter

p: :「jrStˉ1ette【

选择每个p节点的首字母

: :十jr5tˉ1i∩e

p8 :十ir5tˉ1j∩e

选择每个p节点的首行

:fjr5tˉC∩j1d

p:十1r5tˉ〔hi1d

选择属于父节点的第一个子节点的所有p节点

: :be+ore

p: :be十ore

在每个p节点的内容之前插人内容

: 8a十teI

p: :a十teI

在每个p节点的内容之后插人内容

:1己∩g(1己∩guage)

p:1a∩8(jt)

选择带有以jt开头的13∩8属性值的所有p节点



e1e‖论∩t1≈e1e∩沦∩t2

p~u1

选择前面有p节点的所有u1节点



[attrjbUte^=γ己1ue]

a[Br〔^二“∩ttp5闪]

选择5r〔属性值以http5开头的所有a节点



[attribl」te$≡va1Ue]

a[5I〔$=".pdf"]

选择5rC属性值以.pd+结尾的所有a节点

勺’

0 q

a[SrC*=国ab〔闻]

选择5r〔属性值中包含ab〔子串的所有a节点

;十jI5tˉo十ˉtype

P:十jr5tˉo十ˉtype

选择属于对应父节点的首个p节点的所有p节点

81a5tˉo十ˉtype

p:1a5tˉO+ˉtγpe

选择属于对应父节点的最后_个p节点的所有p节点

;o∩1yˉo十ˉtype

p:o∩1yˉofˉtype

选择属于对应父节点的唯一p节点的所有p节点

;o∩1yˉ〔M1d

p:o∩1yˉch11d

选择属于对应父节点的唯_子节点的所有p节点

:∩thˉ〔∩j1d(∩)

p:∩t‖ˉC∩j1d(2)

选择属于对应父节点的第二个子节点的所有p节点

;∩t∩ˉ1aStˉ〔M1d(∩)

p:∩t∩ˉ1a5tˉ〔∩j1d(2)

同上,不过是从最后一个子节点开始计数

:∩t∩ˉofˉtype(∩)

p:∩thˉo+ˉtype(2)

选择属于对应父节点的第二个p节点的所有p节点

:∩t‖ˉ1a5tˉo+ˉtyPe(∩)

p:∩t‖ˉ1a5t_O千ˉtype(2)

同上,不过是从最后-个子节点开始计数

:1a5tˉCh11d

p;1a5tˉ〔hj1d

选择属于对应父节点的最后一个子节点的所有p节点

:IOOt

8root

选择文档的根节点

:eⅦptγ

p:eⅧpty

选择没有子节点的所有p节点(包括文本节点)

:taIget

#∩ew5:t己Iget

选择当前活动的#∩e"5节点

j∩put:e∩ab1ed

选择每个启用的j∩put节点

[己ttrjbl』te*=γa1ue]







0

;e∩ab1ed

( ( d





勺 ●

选择每个禁用的1∩put节点

1∩pl』t:disab1ed

:〔∩e〔ked

1∩pl」t:〔he〔ked

选择每个被选中的j∩put节点

:∩ot(5e1e〔tor)

:∏Ot

选择非p节点的所有节点

: :5e1e〔tio∩

8 85e1eCt1O∩

选择被用户选取的节点部分

‖』』■』口』■■■■■■■■■■■』■■引』‘‖』日|‖‖‖■■■■

8dj5ab1ed

卜「



仿‖「 |■■「「||■矽■「↓|





l.3爬虫的基本原理

l9

另外,还有一种比较常用的选择器XPath,这种选择方式后面会详细介绍° 5.总结

本节介绍了网页的结构和节点间的关系,了解了这些内容’我们才能有更加清晰的思路去解析和

■■尸|∏尸〖『『|‖■■尸■尸

提取网页内容。

本节部分内容参考如下资料°

□MDNWebDocs上关于HTTP、JavaSc∏pt的介绍。 □W3School上关于HTMLDOM节点、CSS选择器的介绍°

■「||β卜■厂)■伊‖|′匹■△■「}止尸|匹●「|■尸●■■厂

□维基百科上HTTP相关的介绍°

↑.3爬虫的基本原理 若是把互联网比作一张大网,爬虫(即网络爬虫)便是在网上爬行的蜘蛛°把网中的节点比作一 个个网页,那么蜘蛛爬到_个节点处就相当于爬虫访问了一个页面,获取了其信息°可以把网页与网 页之间的链接关系比作节点间的连线,蜘蛛通过一个节点后,顺着节点连线继续爬行,到达下-个节

点’意昧着爬虫可以通过网页之间的链接关系继续获取后续的网页,当整个网站涉及的页面全部被爬 虫访问到后,网站的数据就被抓取下来了° ↑.爬虫概述

简单点讲,爬虫就是获取网页并提取和保存信息的自动化程序’下面概要介绍一下° ●获取网页

爬虫的工作首先是获取网页’这里就是获取网页的源代码。源代码里包含网页的部分有用信息’

「‖′「

▲ 尸 ■ ∏ |

所以只要获取源代码’就可以从中提取想要的信息了。

l.l节讲了请求和响应的概念’向网站的服务器发送_个请求’服务器返回的响应体便是网页源

代码。所以最关键的部分是构造一个请求并发送给服务器’然后接收到响应并对其进行解析’这个流 程如何实现呢?总不能手动截取网页源码吧?

不用担心’Python提供了许多库,可以帮助我们实现这个流程’如url‖jb` requeSts等,我们可以 用这些库完成H∏P请求操作。除此之外,请求和响应都可以用类库提供的数据结构来表示,因此得 到响应之后只需要解析数据结构中的body部分’即可得到网页的源代码’这样我们便可以用程序来

『|{β|||〖■「|[尸‖‖‖巴■『‖儿■『‖|〖■【〖‖【■【【『【■「‖}■【■‖[■■【】【■[∩|■■■【【■【■■∏【巳■

实现获取网页的过程了° ●捉取信息

获取网页的源代码后’接下来就是分析源代码’从中提取我们想要的数据°首先’最通用的提取 方法是采用正则表达式’这是_个万能的方法,注意构造正则表达式的过程比较复杂且容易出错° 另外,由于网页结构具有_定的规则’所以还有一些库是根据网页节点属性、CSS选择器或XPath 来提取网页信息的,如BeautifUlSoup、pyqueIy、lxml等°使用这些库,可以高效地从源代码中提取 网页信息’如节点的属性`文本值等。

提取信息是爬虫非常重要的一个工作,它可以使杂乱的数据变得条理清晰’以便后续处理和分析 数据° ●保存数据

提取信息后,我们_般会将提取到的数据保存到某处以便后续使用°保存数据的形式多种多样, 可以简单保存为TXT文本或JSON文本’也可以保存到数据库’如MySQL和MongoDB等,还可保



第l章爬虫基础

20

存至远程服务器,如借助SFTP进行操作等。 ●自动化程序

自动化程序的意思是爬虫可以代替人来完成上述操作°我们当然可以手动提取网页中的信息, 但是当量特别大或者想快速获取大量数据的时候,肯定还是借助程序快。爬虫就是代替我们完成爬取 工作的自动化程序,它可以在爬取过程中进行各种异常处理、错误重试等操作’确保爬取持续高效地 运行°

2.能爬怎样的数据

网页中存在各种各样的信息,最常见的便是常规网页,这些网页对应着HTML代码’而最常抓取 的便是HTML源代码°

另外,可能有些网页返回的不是HTML代码,而是_个JSON字符串(其中API接口大多采用这 样的形式),这种格式的数据方便传输和解析°爬虫同样可以抓取这些数据’而且数据提取会更加方便° 网页中还包含各种二进制数据,如图片、视频和音频等°利用爬虫,我们可以将这些二进制数据 抓取下来,然后保存成对应的文件名。

除了上述数据’网页中还有各种扩展名文件’如CSS` JavaScript和配置文件等。这些文件其实 上述内容其实都有各自对应的URL,URL基于HTTP或HTTPS协议,只要是这种数据,爬虫都 可以抓取°

3.」aγaSc「‖pt渣染的页面 有时候,我们在用urllib或requests抓取网页时,得到的源代码和在测览器中实际看到的不-样°

这是-个非常常见的问题°现在有越来越多的网页是采用Ajax`前端模块化工具构建的’可能整 个网页都是由JavaSc∏pt喧染出来的,也就是说原始的HTML代码就是-个空壳’例如:

〈head〉

〈贬ta〔har5et=腾0∏ˉ8"〉

〈t1t1e〉丁‖i5i5己0e『m〈/t1t1e〉

〈d1γjd="〔o∩ta1∩er"〉 〈/djγ>

</body〉 〈5〔rjptSI〔=』!app.jS"〉〈/5Cr1pt〉 </ht刚1〉

这个实例中, body节点里面只有-个1d为〔o∩ta1∩er的节点’需要注意在body节点后引人了

||』■■■■□〗】■■■』■可■■』■■

〈/head〉

〈body〉



·〗‖‖

〈|皿Ⅳp[∩t"1〉

〈∩m1〉



」』■■■勺‖||‖|□印□■■■卜』■■‖‖‖

最普通’只要在测览器里面可以访问到,就可以抓取下来°

appjs’它负责整个网站的喧染。

appjs文件,便去请求这个文件°获取该文件后,执行其中的JavaSc∏pt代码’JavaSc∏pt会改变HTML 中的节点,向其中添加内容’最后得到完整的页面。

JavaSmpt文件,我们也就无法看到完整的页面内容。 这也解释了为什么有时我们得到的源代码和在测览器中看到的不~样°

对于这样的情况’我们可以分析源代码后台Ajax接口,也可使用Selenium`Splash、Pyppeteer`

□』■‖■二■■■{|{』■可‖

在用url‖jb或requests等库请求当前页面时’我们得到的只是HTML代码’它不会继续加载

‖|||



在测览器中打开这个页面时’首先会加载这个HTML内容’接着测览器会发现其中引人了-个







l.4

Sessjon和Cookie

2]

后面,我会详细介绍如何采集JavaSc面pt喧染出来的网页。 4总结

本节介绍了爬虫的_些基本原理’熟知这些原理可以使我们在后面编写爬虫时更加得心应手°

↑.4

Sess|o∩和Coo低|e

在测览网站的过程中’我们经常会遇到需要登录的情况,有些页面只有登录之后才可以访问°在 登录之后可以连续访问很多次网站’但是有时候过_段时间就需要重新登录。还有一些网站’在打开 测览器时就自动登录了,而且在很长时间内都不会失效,这又是什么情况?其实这里面涉及Session和 ‖〖■‖}‖坠■『|『「止■■厂丫卜巴尸||△尸|)止印|匹■「|仍『▲日「|尸}■厂}■厂||■广「‖匹■「■厂■■}|匹尸|‖■∏‖‖|■厅『「|■「「■「‖『}■【「‖}■■『』‖〖■『「『|》||【●「||||●「|■「卜

Cookje的相关知识’本节就来揭开它们的神秘面纱。 ↑.静态网页和动态网页

在开始揭秘之前,我们需要先了解-下静态网页和动态网页的概念°还是使用“网页的结构,,节的实例代码,内容如下: 〈!皿∏p[ht‖1〉

<‖t川1〉 〈∩ead〉

〈爬ta〔∩日r5et="0丁「ˉ8"〉

〈tjt1e〉「∩15j5a0e咖</tjt1e〉 〈/bead〉

〈mdy〉 <d1vid=圃〔o∩taj∩er0o〉

〈djγC1a5S="训mpper‖0〉 〈b2〔1a55=00tit1e"〉‖e11O‖or1d〈/h∑〉

<pC1a55二"teXt"〉‖e11O’ tM5i5apam8mp‖.〈/p〉 〈/djγ〉 〈/diγ〉

</body〉 </‖t∏1〉

这是最基本的HTML代码,我们将其保存为—个.html文件,并把这个文件放在某台具有固定公

网IP的主机上’在这台主机上安装Apache或Nginx等服务器,然后该主机就可以作为服务器了’其 他人可以通过访问服务器看到那个实例页面’这就搭建了_个最简单的网站。 这种网页的内容是由HTML代码编写的’文字、图片等内容均通过写好的HTML代码来指定’

这种页面叫作静态网页°静态网页加载速度快`编写简单,同时也存在很大的缺陷,如可维护性差` 不能根据URL灵活多变地显示内容等°如果我们想给静态网页的URL传人一个name参数’让其在 网页中显示出来’是无法做到的°

于是动态网页应运而生,它可以动态解析URL中参数的变化,关联数据库并动态呈现不同的页 面内容’非常灵活多变。我们现在看到的网站几乎都是动态网站,它们不再是一个简单的HTML页面,

可能是由JSP`PHP`Python等语言编写的,功能要比静态网页强大`丰富太多°此外,动态网站还 可以实现用户登录和注册的功能。

回到l.4节开头提到的问题’很多页面是需要登录之后才可以查看的°按照—般的逻辑’输人用 户名和密码登录网站’肯定是拿到了一种类似凭证的东西,有了这个凭证’才能保持登录状态’访问 那些登录之后才能看得到的页面。

这种神秘的凭证到底是什么呢?其实它就是Sessjon和Cookie共同产生的结果’下面我们来-探 究竟°

厂↑=

Playwnght这样的库来模拟JavaSc∏pt喧染。

■■勺‖

22

第l章爬虫基础

2ˉ无状态卜∏ˉ『P

在了解Session和Cookje之前,我们还需要了解H∏P的一个特点’叫作无状态。

HTIP的无状态是指H∏P协议对事务处理是没有记忆能力的,或者说服务器并不知道客户端处 于什么状态°客户端向服务器发送请求后,服务器解析此请求’然后返回对应的响应,服务器负责完 成这个过程,而且这个过程是完全独立的,服务器不会记录前后状态的变化,也就是缺少状态记录。 这意味着之后如果需要处理前面的信息,客户端就必须重传’导致需要额外传递_些重复请求’才能 获取后续响应’这种效果显然不是我们想要的°为了保持前后状态,肯定不能让客户端将前面的请求 全部重传一次’这太浪费资源了,对于需要用户登录的页面来说,更是棘手°

可以这样理解,Cookie里保存着登录的凭证,客户端在下次请求时只需要将其携带上’就不必重

q

0



■■]‖|』】■■∏‖

这时’两种用于保持HTTP连接状态的技术出现了,分别是Sessjon和Cookje°Session在服务端’ 也就是网站的服务器,用来保存用户的Sessjon信息;Cookje在客户端’也可以理解为在测览器端, 有了Cookie’测览器在下次访问相同网页时就会自动附带上它’并发送给服务器’服务器通过识别 Cookje鉴定出是哪个用户在访问,然后判断此用户是否处于登录状态’并返回对应的响应。



新输人用户名、密码等信息重新登录了。

因此在爬虫中,处理需要先登录才能访问的页面时’我们-般会直接将登录成功后获取的Cookie

3.SeSS‖o∩

Session,中文称之为会话,其本义是指有始有终的一系列动作、消息°例如打电话时,从拿起电 话拨号到挂断电话之间的一系列过程就可以称为一个Session°

而在Web中, Session对象用来存储特定用户Session所需的属性及配置信息。这样’当用户在应

用程序的页面之间跳转时,存储在Session对象中的变量将不会丢失,会在整个用户Session中一直存 在下去°当用户请求来自应用程序的页面时,如果该用户还没有Session,那么Web服务器将自动创 建一个Session对象°当Session过期或被放弃后,服务器将终止该Session。 4Coo戊|e

Cookje,指某些网站为了鉴别用户身份`进行Sessjon跟踪而存储在用户本地终端上的数据° ●Session维持

那么’怎样利用Cookje保持状态呢?在客户端第_次请求服务器时,服务器会返回一个响应头 中带有SetˉCookie字段的响应给客户端,这个字段用来标记用户。客户端测览器会把Cookje保存起 来’当下一次请求相同的网站时’把保存的Cookje放到请求头中-起提交给服务器。Cookie中携带

着SessjonID相关信息,服务器通过检查Cookie即可找到对应的Session,继而通过判断Session辨认

用户状态。如果Sessjon当前是有效的,就证明用户处于登录状态’此时服务器返回登录之后才可以 查看的网页内容,测览器再进行解析便可以看到了°

反之’如果传给服务器的Cookie是无效的,或者Session已经过期了’客户端将不能继续访问页 面’此时可能会收到错误的响应或者跳转到登录页面重新登录°

Cookie和Sessjon需要配合,-个在客户端,—个在服务端’二者共同协作,就实现了登录控制° ●冯性结构

接下来,我们看看Cookie都包含哪些内容。这里以知乎为例’在测览器开发者工具中打开

■]‖|(」』■|■日■]』□|·』日|』●可||■∏」』■∏|□■||』■|■」】‖‖□■‖■】」勺||』』可」·|‖|||因司」·〗]」■

好了,了解Session和Cookje的概念之后’再来详细剖析它们的原理°

「巴

放在请求头里面直接请求,而不重新模拟登录。



l4

Session和Cookie

23

Applicatjon选项卡’其中左侧有-部分叫Storage, Storage的最后—项即为Cookles,将其点开, 如图lˉl6所示。

陨q……"…γ…°…."…隧ˉ=s…■ °……s…, 鹰…唾…^=~ …

oo

■… 华…棉

i…. Zˉ印

■……

;…





酣簿

7… ;°~ №啡wαr…鲤旧eα」P…』Zu…尸m……乙山v』甲可

态砂!…■′跑≈…… Ⅷ .!髓}0碰!望ˉ}…箕 / 知‖7但唾∏鹤γ9佣迈…r↓ ‖铂; √ ,

{…m—岭…≡ˉ.!-…m{′·缅O吟℃拘……!泅!

.m

.7↑2『仕“

…o■伯全‘≈■∏





t0

!函

!佣田…%臼%群…γ铅田…%…·°.『…沪=.≈咆

M

:●

!汹}

} | !

p瓣…砷呻

. D{

』2

6↑

ˉ

!

炒■…圈… ■~

{:几c0 坠…

}.隧…""_啼…ˉ {…… 田……‖…↑…7叫…‖幻▲70呵……‖≈…

!′…………勤鲤…! / …U7ˉ■γ岭06:咀硕虹} “





!4四

!……E~……{……

;′

!…■函…Pm{■;

!



}@! √ !



■…唾

…mm腻』.…`

{■尸‖‖‖‖■【■厂△【【『■尸■■’■■【·■■■■□【厂‖【‖β『}ˉ■厂[□·▲β↓‖■『□【尸『『=_■『|||匹■尸‖|‖|■=})|||■「■■‖「|匡尸『‖』‖『|}■=【『|||)|△■『■「【■「|■■■■’}||【■■

{哮唾正

●……

■←…

.{…

}西… } ˉ哼

}回



●□呻

j -mm

{-■w ! 凹吨 { ˉ…

;

雕锑南!}』§蹿隐躺擂|′} {

~嚣≡ { 函■

r上



{鸣…≡…}………M→ …№……杜V…0刚…幻…7…...……….m !/

|0………

8↑0…唾‖四q7唾……9山≡=

』-…口』′ ;西协●■m鱼唾…{纺{ { { |

-

!幽…牛………施…

知0OU7ˉ3‖丁碉:?O咎…, 幻}

.卫乙■』甲T由

日/

!……

肉′ ;…

8‖…,……刀a血…■矽m□呻mˉˉ…癣…

.6`■……!但~Q……m…:=~ 刀p■▲…` !‘,酶…↑…m″酮啤`…涧■`ˉ·!←一 』50碎≈

图lˉl6

翻O岭↑070·锤Q7翻l尹 』 妇『

{嘲} }

扣↑·但ˉ0U…0们…T 『







■;

;′Ⅸ/ ;me■,mz0m…}妇{ {『 } ^ ˉ= 喷 ↑4{ 』′ !m…忽w……!心}|」

!



Cookje列表

可以看到’列表里有很多条目’其中每个条目都可以称为-个Cookle条目°Cookje具有如下几 个属性。

□Name: Cookie的名称°Cookie—且创建’名称便不可更改。

□Value: Cookie的值°如果值为Unicode字符,则需要为字符编码°如果值为二进制数据’则 需要使用BASE64编码。 □Domain:指定可以访问该Cookje的域名°例如设置Domajn为.zhlhucom,表示所有以

zhjhucom结尾的域名都可以访问该Cookje。

□Path:Cookie的使用路径°如果设置为/path/,则只有路径为/path/的页面才可以访问该Cookje。 如果设置为/,则本域名下的所有页面都可以访问该Cook1e°



□MaxˉAge:Cookie失效的时间’单位为秒’常和Expires_起使用’通过此属性可以计算出Cookje 的有效时间。MaxˉAge如果为正数,则表示Cookle在MaxˉAge秒之后失效;如果为负数’则 Cook1e在关闭测览器时失效,而且测览器不会以任何形式保存该Cookje° □Sjzc字段:Cookje的大小。

□HTTP字段:Cookje的∩ttpo∩1y属性。若此属性为true,则只有在HTTPHeadcE中才会带有 此Cookle的信息’而不能通过do〔uⅧe∏t.〔oo代je来访问此Cookje。

□SeCure:是否仅允许使用安全协议传输Cookie°安全协议有HTTPS和SSL等,使用这些协议 在网络上传输数据之前会先将数据加密。其默认值为falSe°

●会话Cookie和持久Cookie 从表面意思来看’会话Cookje就是把Cookie放在测览器内存里,关闭测览器之后’Cookie即失

效;持久Cookje则会把Cookje保存到客户端的硬盘中,下次还可以继续使用’用于长久保持用户的 登录状态°

严格来说,其实没有会话Cookje和持久Cookie之分’只是MaxˉAge或Expi爬s字段决定了Cookie 失效的时间° 因此,_些持久化登录的网站实际上就是把Cookje的有效时间和Session有效期设置得比较长,



第l章爬虫基础

24

下次客户端再访问页面时仍然携带之前的Cookje’就可以直接呈现登录状态。 5.常见误区

在谈论Session机制的时候’常会听到一种误解—只要关闭测览器, Session就消失了。可以想 象_下生活中的会员卡,除非顾客主动对店家提出销卡,否则店家是绝对不会轻易删除顾客资料的° 对Session来说,也_样’除非程序通知服务器删除一个Session’否则服务器会_直保留°例如程序 _般都是在我们做注销操作时才删除Session°

但是当我们关闭测览器时’测览器不会主动在关闭之前通知服务器自己将要被关闭’所以服务器 压根不会有机会知道测览器已经关闭°之所以会产生上面的误解’是因为大部分网站使用会话Cookie 来保存SessionID信息,而测览器关闭后Cookie就消失了,等测览器再次连接服务器时’也就无法找 到原来的Sesslon了。如果把服务器设置的Cookje保存到硬盘上,或者使用某种手段改写测览器发出 的HTTP请求头’把原来的Cookie发送给服务器’那么再次打开测览器时`仍然能够找到原来的 而且恰恰是由于关闭测览器不会导致Session被删除,因此需要服务器为Session设置一个失效时 间’当距离客户端上一次使用Session的时间超过这个失效时间时’服务器才可以认为客户端已经停 止了活动,并删除掉Session以节省存储空间°



■■司‖」』

SessionID,依|日保持登录状态°





6.总结

本节涉及—些专业名词解释,部分内容参考如下资料。 □百度百科上Session`Cookle相关的介绍。

||

好掌握。

■‖二■‖|

本节介绍了Sesslon和Cookie的基本概念’这对后文进行网络爬虫的开发有很大的帮助’需要好

□维基百科上HTTPCookie相关的介绍°

□“码迷”网站上的博客文章‘』SeSSion和几种状态保持方案理解’。

↑.5代理的基本原理

并返回-些错误信息,可以称这种情况为封IP°

既然服务器检测的是某个IP在单位时间内的请求次数’那么借助某种方式把我们的IP伪装一下,

{√|

在做爬虫的过程中经常会遇到一种情况’就是爬虫最初是正常运行、正常抓取数据的,_切看起 来都是那么美好’然而—杯茶的工夫就出现了错误’例如403Forbjdden’这时打开网页_看’可能会 看到“您的IP访问频率太高”这样的提示°出现这种现象是因为网站采取了一些反爬虫措施。例如服 务器会检测某个IP在单位时间内的请求次数,如果请求次数超过设定的阑值,就直接拒绝提供服务,

让服务器识别不出请求是由我们本机发起的,不就可以成功防止封IP了吗?

本原理,它是怎样实现伪装IP的呢?

↑.基本原理

代理实际上就是指代理服务器’英文叫作ProxyScrver’功能是代网络用户取得网络信息°形象 点说’代理是网络信息的中转站°当客户端正常请求_个网站时’是把请求发送给了web服务器’ Web服务器再把响应传回给客户端°设置代理服务器’就是在客户端和服务器之间搭建一座桥’此时

』■]|■∏

—种有效的伪装方式是使用代理,后面会详细说明代理的用法。在这之前,需要先了解代理的基



客户端并非直接向Web服务器发起请求’而是把请求发送给代理服务器’然后由代理服务器把请求发 送给Web服务器’Web服务器返回的响应也是由代理服务器转发给客户端的。这样客户端同样可以正 q

|』■】■』■■□

[【■了‖匹●『}‖|



l.5代理的基本原理

25

常访问网页,而且这个过程中Web服务器识别出的真实IP就不再是客户端的Ip了’成功实现了Ip伪

卜「

■尸「|■「

装’这就是代理的基本原理。

0

2.代理的作用

代理有什么作用呢?我们可以简单列举如下° □突破自身IP的访问限制’访问-些平时不能访问的站点°

匹▲■■「‖也■■「|‖|‖■「■■■「‖『『窗←■Ⅳ"■·『■■】■|…■=『匹ˉ■【『肌‖【■

□访问_些单位或团体的内部资源。比如’使用教育网内地址段的免费代理服务器’就可以下载 和上传对教育网开放的各类FTP,也可以查询、共享各类资料等°

□提高访问速度。通常,代理服务器会设置一个较大的硬盘缓冲区’当有外界的信息通过时’会 同时将其保存到自己的缓冲区中’当其他用户访问相同的信息时’直接从缓冲区中取出信息, 提高了访问速度°

□隐藏真实IP。上网者可以通过代理隐藏自己的IP’免受攻击°对于爬虫来说’使用代理就是 为了隐藏自身IP’防止自身的IP被封锁° 3.爬虫代理

对于爬虫来说’由于爬取速度过快,因此在爬取过程中可能会遇到同-个IP访问过于频繁的问 题’此时网站会让我们输人验证码登录或者直接封锁IP’这样会给爬取造成极大的不便°



0

0







) |



0





使用代理隐藏真实的IP’让服务器误以为是代理服务器在请求自己°这样在爬取过程中不断更换 代理’就可以避免IP被封锁’达到很好的爬取效果。 4.代理分类

对代理进行分类时’既可以根据协议’也可以根据代理的匿名程度’这两种分类方式分别总结如下。 ●根据协议区分

根据代理的协议’代理可以分为如下几类。



□尸丁尸代理服务器:主要用于访问FTP服务器’_般有上传、下载以及缓存功能’端口_般为2l`









2l2l等。

□什丁丁p代理服务器:主要用于访问网页’_般有内容过滤和缓存功能’端口一般为80、8080、 3l28等。

□SSL/ˉ『LS代理:主要用于访问加密网站,-般有SSL或TLS加密功能(最高支持l28位加密

强度)’端口-般为“3°

□只丁S尸代理:主要用于Realplayer访问Real流媒体服务器,-般有缓存功能’端口一般为554° □丁e|∩et代理:主要用于Iunet远程控制(黑客人侵计算机时常用于隐藏身份),端口-般为23°

□尸O尸3/SM丁p代理:主要用于以POP3/SMTP方式收发邮件’-般有缓存功能,端口一般为

b









ll0/25。

□SOC低S代理:只是单纯传递数据包,不关心具体协议和用法’所以速度快很多’-般有缓存 功能,端口-般为l080°SOCKS代理协议又分为SOCKS4和SOCKS5, SOCKS4协议只支持

TCP’ SOCKS5协议则支持TCP和UDP’还支持各种身份验证机制`服务器端域名解析等。

简单来说, SOCKS4能做到的SOCKS5都能做到,但SOCKS5能做到的SOCKS4不-定做 得到。





●根据匿名程度区分

根据代理的匿名程度’代理可以分为如下几类°



} p



26

第l章爬虫基础

□高度匿名代理:高度匿名代理会将数据包原封不动地转发’在服务端看来似乎真的是—个普通 客户端在访问’记录的IP则是代理服务器的IP°

□普通匿名代理:普通匿名代理会对数据包做一些改动,服务端可能会发现正在访问自己的是个 代理服务器,并且有_定概率去追查客户端的真实IP°这里代理服务器通常会加人的HTIP头 有HTTPVIA和HTTPXFORWARDEDFOR。

□透明代理:透明代理不但改动了数据包’还会告诉服务器客户端的真实IP°这种代理除了能 用缓存技术提高测览速度’用内容过滤提高安全性之外,并无其他显著作用,最常见的例子是 □间谍代理:间谍代理是由组织或个人创建的代理服务器’用于记录用户传输的数据,然后对记 录的数据进行研究、监控等°

■■‖‖■‖

内网中的硬件防火墙。

5.常见代理设置 常见的代理设置如下。

□对于网上的免费代理,最好使用高度匿名代理’可以在使用前把所有代理都抓取下来筛选一下 可用代理,也可以进_步维护—个代理池°

□使用付费代理服务。互联网上存在许多可以付费使用的代理商,质量要比免费代理好很多° □ADSL拨号,拨—次号换-次IP’稳定性高’也是-种比较有效的封锁解决方案° □蜂窝代理,即用4G或5G网卡等制作的代理°由于用蜂窝网络作为代理的情形较少,因此整







q

体被封锁的概率会较低’但搭建蜂窝代理的成本是较高的。 在后面’我们会详细介绍_些代理的使用方式。

本文介绍了代理的相关知识,这对后文我们进行-些反爬绕过的实现有很大的帮助’同时也为后 文的_些抓包操作打下了基础,需要好好理解°

本节涉及_些专业名词,部分内容参考如下资料° □维基百科上代理服务器相关的内容。 □百度百科上代理相关的内容。

↑.6多线程和多进程的基本原理 在_台计算机中’我们可以同时打开多个软件’例如同时测览网页、听音乐`打字等,这是再正 常不过的事情°但仔细想想,为什么计算机可以同时运行这么多软件呢?这就涉及计算机中的两个名 词:多进程和多线程°

同样,在编写爬虫程序的时候,为了提高爬取效率,我们可能会同时运行多个爬虫任务,其中同 样涉及多进程和多线程°

↑.多线程的含义 说起多线程,就不得不先说什么是线程°说起线程’又不得不先说什么是进程° 进程可以理解为一个可以独立运行的程序单位’例如打开—个测览器’就开启了一个测览器进程; 打开_个文本编辑器,就开启了~个文本编辑器进程。在_个进程中’可以同时处理很多事情’例如

在测览器进程中,可以在多个选项卡中打开多个页面,有的页面播放音乐,有的页面播放视频,有的

网页播放动画’这些任务可以同时运行,互不干扰。为什么能做到同时运行这么多任务呢?这便引出 了线程的概念’其实_个任务就对应_个线程。

`|}|

6总结























l.6

多线程和多进程的基本原理

27

进程就是线程的集合,进程是由一个或多个线程构成的’线程是操作系统进行运算调度的最小单 位’是进程中的最小运行单元°以上面说的测览器进程为例,其中的播放音乐就是_个线程’播放视 频也是-个线程。当然,测览器进程中还有很多其他线程在同时运行’这些线程并发或并行执行使得 整个测览器可以同时运行多个任务°

了解了线程的概念’多线程就很容易理解了°多线程就是-个进程中同时执行多个线程’上面的 测览器进程就是典型的多线程° 2.并发和并行

说到多进程和多线程’不得不再介绍两个名词_并发和并行°我们知道,在计算机中运行-个 程序,底层是通过处理器运行—条条指令来实现的。

处理器同一时刻只能执行一条指令’并发(concu∏ency)是指多个线程对应的多条指令被快速轮 换地执行°例如一个处理器,它先执行线程A的指令_段时间,再执行线程B的指令一段时间’然后 再切回线程A执行一段时间。处理器执行指令的速度和切换线程的速度都非常快’人完全感知不到计 算机在这个过程中还切换了多个线程的上下文’这使得多个线程从宏观上看起来是同时在运行°从微 观上看’处理器连续不断地在多个线程之间切换和执行,每个线程的执行都—定会占用这个处理器的 -个时间片段’因此同_时刻其实只有-个线程被执行。 并行(parallel)指同_时刻有多条指令在多个处理器上同时执行’这意味着并行必须依赖多个处 理器。不论是从宏观还是微观上看,多个线程都是在同_时刻-起执行的°

并行只能存在于多处理器系统中’因此如果计算机处理器只有-个核,就不可能实现并行。而并 发在单处理器和多处理器系统中都可以存在,因为仅靠_个核’就可以实现并发°

例如,系统处理器需要同时运行多个线程°如果系统处理器只有一个核,那它只能通过并发的方 式来运行这些线程°而如果系统处理器有多个核,那么在一个核执行—个线程的同时’另一个核可以 执行另_个线程,这样这两个线程就实现了并行执行。当然’其他线程也可能和另外的线程在同_个

核上执行,它们之间就是并发执行。具体的执行方式’取决于操作系统如何调度°

3.多线程适用场景 在_个程序的进程中’有_些操作是比较耗时或者需要等待的’例如等待数据库查询结果的返回`

等待网页的响应°这时如果使用单线程’处理器必须等这些操作完成之后才能继续执行其他操作’但 在这个等待的过程中,处理器明显可以去执行其他操作°如果使用多线程’处理器就可以在某个线程 处于等待态的时候,去执行其他线程’从而提高整体的执行效率。

很多情况和上述场景一样’线程在执行过程中需要等待°网络爬虫就是—个非常典型的例子’爬 虫在向服务器发起请求之后’有一段时间必须等待服务器返回响应,这种任务就属于IO密集型任务°

对于这种任务,如果我们启用多线程’那么处理器就可以在某个线程等待的时候去处理其他线程’从 而提高整体的爬取效率°

但并不是所有任务都属于IO密集型任务’还有_种任务叫作计算密集型任务,也可以称为CPU密 集型任务。顾名思义’就是任务的运行—直需要处理器的参与°假设我们开启了多线程’处理器从一 个计算密集型任务切换到另一个计算密集型任务,那么处理器将不会停下来,而是始终忙于计算’这 样并不会节省整体的时间’因为需要处理的任务的计算总量是不变的。此时要是线程数目过多,反而 还会在线程切换的过程中耗费更多时间使得整体效率变低°

综上所述’如果任务不全是计算密集型任务’就可以使用多线程来提高程序整体的执行效率。尤 其对于网络爬虫这种IO密集型任务,使用多线程能够大大提高程序整体的爬取效率°



| 28

第l章爬虫基础 4.多进程的含义

前文我们已经了解了进程的基本概念,进程(process)是具有一定独立功能的程序在某个数据集 合上的_次运行活动’是系统进行资源分配和调度的-个独立单位。

顾名思义’多进程就是同时运行多个进程。由于进程就是线程的集合,而且进程是由一个或多个 线程构成的’所以多进程意味着有大于等于进程数量的线程在同时运行。 5尸yt∩o∩中的多线程和多进程

Python中GIL的限制导致不论是在单核还是多核条件下,同一时刻都只能运行一个线程这使得 GIL全称为GlobalInterpreterLock’意思是全局解释器锁,其设计之初是出于对数据安全的考虑。 在Python多线程下’每个线程的执行方式分如下三步。 □获取GIL°

□执行对应线程的代码°

』■■■■■‖‖‖‖』■■■‖二■■可

Python多线程无法发挥多核并行的优势°

□释放GIL。

■■■‖||‖■■■

可见’某个线程要想执行’必须先拿到GIL°我们可以把GIL看作通行证’并且在一个Python进

程中’GIL只有一个°线程要是拿不到通行证,就不允许执行°这样会导致即使在多核条件下’_个 Python进程中的多个线程在同一时刻也只能执行-个。 受GIL影响的°也就是说’多进程能够更好地发挥多核优势°

从整体来看, Python的多进程比多线程更有优势°所以,如果条件允许的话,尽量用多进程°

|‖

值得注意的是’由于进程是系统进行资源分配和调度的-个独立单位’所以各进程之间的数据是 无法共享的’如多个进程无法共享一个全局变量’进程之间的数据共享需要由单独的机制来实现°



■■口‖当■■■■』日‖』■∏‖|‖||』●]

不过’对于爬虫这种IO密集型任务来说,多线程和多进程产生的影响差别并不大°但对于计算 密集型任务来说,由于GIL的存在’Python多线程的整体运行效率在多核情况下可能反而比单核更低。 而Python的多进程相比多线程,运行效率在多核情况下比单核会有成倍提升。

■■

而对于多进程来说’每个进程都有属于自己的GIL’所以在多核处理器下’多进程的运行是不会

二β‖■司

关于Python中多进程和多线程的具体用法’由于篇幅原因,这里不再展开介绍’请移步如下链 接进行学习。

| 』 ■ 〗 』 | 」 ■ 】 = ■ | | | ( 」 ■ ■ 』 ■ 习

□Python多线程的用法: https://setupscrapecenter/pythonˉthreadlng° □Python多进程的用法: https://semp.scrapecenter/pythonˉmultiprocess1ng° 6.总结

本节介绍了多线程`多进程的基本知识,如果我们可以把多线程、多进程运用到爬虫中的话,爬 虫的爬取效率将会大幅提升。



由于涉及一些专业名词’本节内容参考如下资料° (

□百度百科上多线程、多进程相关的内容。

□博客园网站上的‘‘多进程和多线程的概念,’文章°

■■■■■

□Python官方文档中threading相关的内容° □Python官方文档中multlprocessing相关的内容。

q



第2章

「2

基本库的使用

L

1_乡

学习爬虫’其基本的操作便是模拟测览器向服务器发出请求’那么我们需要从哪个地方做起呢? 请求需要我们自己构造吗?我们需要关心请求这个数据结构怎么实现吗?需要了解HTTP`TCP`IP层 的网络传输通信吗?需要知道服务器如何响应以及响应的原理吗?

可能你无从下手’不过不用担心,Python的强大之处就是提供了功能齐全的类库来帮助我们实现 这些需求°最基础的HTTP库有urlljb、Iequ≈ts、httpx等° 拿urllib这个库来说’有了它’我们只需要关心请求的链接是什么’需要传递的参数是什么’以 及如何设置可选的请求头,而无须深人到底层去了解到底是怎样传输和通信的°有了urllib库’只用 两行代码就可以完成_次请求和响应的处理过程’得到网页内容,是不是感觉方便极了?

接下来’就让我们从最基础的部分开始了解HTTP库的使用方法吧。

2↑ | u「|||b的使用 首先介绍一个Python库’叫作urllib,利用它就可以实现HTTP请求的发送,而且不需要关心HTTP 协议本身甚至更底层的实现,我们要做的是指定请求的URL`请求头`请求体等信息。此外urlljb还

可以把服务器返回的响应转化为Python对象’我们通过该对象便可以方便地获取响应的相关信息’如 响应状态码、响应头、响应体等°

麓蠢|鳃籍墨酗删腮溺个靡来…了w请…遥.…恤…,醚‖ib2 首先’我们了解一下urllib库的使用方法’它是Python内置的HTTP请求库’也就是说不需要额 外安装,可直接使用。urllib库包含如下4个模块°

□reque5t:这是最基本的HTTP请求模块’可以模拟请求的发送°就像在测览器里输人网址然 后按下回车一样,只需要给库方法传入URL以及额外的参数’就可以模拟实现发送请求的过 程了。

□error:异常处理模块°如果出现请求异常,那么我们可以捕获这些异常’然后进行重试或其 他操作以保证程序运行不会意外终止°

□Parse:_个工具模块°提供了许多URL的处理方法’例如拆分`解析、合并等° □robotpar5er:主要用来识别网站的robotstxt文件’然后判断哪些网站可以爬,哪些网站不可 以,它其实用得比较少。

‖发送请求

使用url‖ib库的reqUe5t模块’可以方便地发送请求并得到响应。我们先来看下它的具体用法°

30

第2章基本库的使用

● l』r1ope∩

ur11jb.reque5t模块提供了最基本的构造HTTP请求的方法’利用这个模块可以模拟测览器的请 求发起过程’同时它还具有处理授权验证(Authentication)、重定向(Redirection)、测览器Cookje以 及其他一些功能。

下面我们体会一下reque5t模块的强大之处°这里以Python官网为例,我们把这个网页抓取下来:

』·|●■|

i川POrtur11ib。Ieque5t

Ie5po∩5e≡ur11ib.requeSt.ur1ope∩(0∩ttp5://""wpγt‖o∩.org,) pIi∩t(re5po∩5巳read(〉.decode( 0ut十ˉ8,))

运行结果如图2ˉl所示。 -□■



‘ M牛x14耳-口「·e q∏eto∩◎汛e=,‖厕sGppIiCαti◎∩ˉ丁tleIⅧ蛔e侧 C◎∩七e田t=‖0/三tα七j〔/矾et厂o←1Co∩=M午x14马-p厂





mⅧoSed.p门g">嚏!=ˉ呐ite5hαpe~=>

C…OS

口∏eto门咖e="巾So”uCotiO∩ˉ丁ue〔O1O7翘Co∩te砒■"鹤673α5"≥<!←pγt∩O∩blueˉˉ> m归to∩α『‖e…颁硼SGpp[`〔otio∩ˉ∏oVbOtt◎∏ˉCO1o厂时〔O∩te∩t■蝉3673G5"≥ <七1t1e≥We1〔咖e七op肚∩o∩°o尸g≤/tit1e≥

m吧tα∩呻e霉“砂e5〔「1ptt咖°|〔α∩te∩t…顾丁摊o仔i〔io1 h创褪Of饰epy七加∩p厂og7αⅧ‖1∩ g

[·∩g0αge"≥

<爬to∩口耐e辱恤keywo伊d白” C◎∏te∏t=啊pyt们onpFog「□丽∏mg1α∩guαgeobjeC七Q厂1e∩ted e

W

b什eeoPe∩三o邯〔eso代woPelⅡ〔e矾sGdo〔!卿e∩t口t1md咖↑)1闻‘cα铡u∩lty">

q睡oP厂ope丁ty=,0og:type咖 〔◎∩te∩t=邮web5lte皿> <∩etαP厂oPe沪ty=,‖og:s1teˉ∩o阳e,, 〔o∏te∩t捧喻py七∩o∩、◎庐g呵>

m℃tαp『◎pe沪ty由,`Og:title碱Co∩te∩七="懈elc◎用etopy比Ono庐gh>

<币etoPmPe厂ty="◎g:des〔广iptim′, cc砒e∏t=0,『们eo仟icio1扣颐Gof恤epyt们o门p尸o 7…i∩口m∩α螺Qe00> g「…1∩gLα"gu回g

图2ˉl

运行结果

这里我们只用了两行代码’便完成了Python官网的抓取,输出了其网页的源代码。得到源代码 之后’我们想要的链接`图片地址`文本信息不就都可以提取出来了吗?

接下来,看看返回的响应到底是什么°利用type方法输出响应的类型: j∏portuI11ib·reque5t

re5po∩5e=0r11jb°reque5t.ur1ope∩( 0∩ttp5://刚w.Pytho∩.or80) Pr1∩t(tyPe(re5PO∩5e))

输出结果如下: 〈〔1a55 0http。〔1ie∩t°‖∏pRe5pO∩5e〉

可以看出’响应是_个‖丁丁PRe5po5∩e类型的对象,主要包含read` readj∩to、 8et∩eader、 get∩eader5、+j1e∩o等方法,以及"5g、γer51o门、 5tat仙5` rea5o∩、 debu81eγe1、 c1o5ed等属性°

得到响应之后,我们把它赋值给Ie5po∩5e变量,然后就可以调用上述那些方法利属性’得到返回 结果的-系列信息了°

例如’调用Iead方法可以得到响应的网页内容`调用5tatu5属性可以得到响应结果的状态码 (200代表请求成功’404代表网页未找到等)。

下面再通过一个实例来看看: mportur11ib.Ieque5t



||

21

urlljb的使用

3l

Ie5Po∩5e=ur11ib.Ieque5t.uI1ope∩(!https;//www·pyt们o∩.org‖) pri∩t(re5po∩5e.5tatu5) pr1∩t(re5po∩5e·getheader5()) prj∩t(Ie5po∩5e.8etheader(‖SeIver|))



运行结果如下: 匹

●■{〖■『『==■【∏||||`△■■■■『』『「[=■=[‖‖|■∏■二■口’}||[■■厂‖「|‖‖「》■厂’|■■■■「‖‖‖【【■=『|‖



200

[(05erγer0 ’ 』∩gi∩x{)》(0〔o∩te∩tˉ丁ype』’ ‖text/ht爪15〔haI5et=ut+ˉ8‖)’(‖X≡「ra们eˉ0ptio∩5′’ |0[‖γ0)’(,γi己{ 」{1‘1 ve8ur0)’( ‖γia! ’ 01.1γaI∩i5h!)’(0〔o∩te∩tˉle∩gt‖’`48775‖)’(|∧c〔eptˉRa∩ge50 ’ ‖b)/te50 )’(Date0 ’ ‖5u∩’ 15

"aI2O2O13:29:01C‖丁‖)’({γ1a! ’ `1.1γ日r∩i5∩0)’(!Age』’ 07O8|)’(‖〔o∩∩e〔tjo∩‖’ ‖〔1o5e,)’(0Xˉ5ervedˉBy{ ’ 〔acheˉb"iS120ˉ8‖I’〔a〔heˉtγo19943ˉⅣ0‖)’(0Xˉ〔a〔he,’ ‖‖1『’ ‖I丁‖)’(|Xˉ〔a〔heˉ‖jt5{ ’ ‖2」 518|〉’(‖Xˉ丁j们er ’ |51584278942.717942』γ5o’γ[00)’(‖γary|’ 』〔oo促ie‖〉』(』5trictˉ丁m∩5portˉ5ecurjtγ0 ’ 『‖axˉ3ge=63o7200oj 1∩C1ude5ubDoⅧai∩50)] ∩g1∩X

其中前两个输出分别是响应的状态码和响应的头信息;最后一个输出是调用getbeader方法,并传人 参数5erγer’获取了响应头中5erγer的值’结果是∩gj∩x’意思为服务器是用Nglnx搭建的° 利用最基本的ur1ope∩方法,已经可以完成对简单网页的GET请求抓取° 如果想给链接传递~些参数’又该怎么实现呢?首先看_下ur1ope∩方法的API: ur11jb·reque5t.ur1ope∩(ur1’data=‖o∩e’[tj爪eout’]*》〔a十i1e≡‖o∩e’〔apath≡‖o∩e’〔ade十au1t≡「己15e’co∩text≡‖o∩e)

可以发现’除了第_个参数用于传递URL之外’我们还可以传递其他内容’例如data(附加数 据)` tjⅦeOut(超时时间)等。

接下来就详细说明_下ur1ope∩方法中几个参数的用法。 ●data参数

data参数是可选的°在添加该参数时’需要使用bγte5方法将参数转化为字节流编码格式的内容,

即byte5类型°另外’如果传递了这个参数,那么它的请求方式就不再是GET,而是POST了。 下面用实例来看-h下: j∏portuI11jb°p己r5e jⅦpoItur11ib.reque5t

data≡bγte5(ur11jb.p己I5巳ur1e∩〔ode({0∩a们e0 : 0ger爬y0})’ e∩〔od1∩g=』ut十ˉ8‖) re5po∩5e≡uI11ib.reque5t.uI1ope∩(!‖ttp5://www.httpbi∩.org/Po5t0 》 data=data) pIj∩t(re5po∩5e.read().de〔ode(‖ut「ˉ80))

这里我们传递了一个参数∩a‖e,值是ger"ey’需要将它转码成byteS类型°转码时采用了byte5方法’ 该方法的第一个参数得是5tr(字符串)类型’因此用uI111b.par5e模块里的uI1e∩Code方法将字典 参数转化为字符串;第二个参数用于指定编码格式’这里指定为utf8°

此处我们请求的站点是wwwhttpbinorg,它可以提供HTTP请求测试°本次我们请求的URL为

https://wwwhttpbinorg/post’这个链接可以用来测试POST请求’能够输出请求的一些信息’其中就 包含我们传递的data参数° 上面实例的运行结果如下: {

"aIg5": {}’ "data": ""’

"十i1e5,0 8 {}’ "+Or们": {

"∩a∏e": "gemeγ" }’ "headeⅢ5": {

"∧〔〔eptˉ[∩cod1∩g"; "jde∩tjty"’ "〔O∩te∩tˉ[e∩gt∩": "11"’



2

](

00〔o∩te∩tˉ丁ypeo! : "app1iC己tio∩/xˉ…ˉ十or川ˉur1e∩〔oded凹’ ""o5t": "州。httpbi∩·org"′ "05erˉ∧ge∩t": 00pyt∩o∩ˉur111b/3·700’

} }

可以发现我们传递的参数出现在了+or们字段中’这表明是模拟表单提交,以POST方式传输数据°

■『|」■■]▲■】‖■■司』‖乙■■

"Xˉ蛔z∩ˉ『mceˉId": "Root=1ˉ5ed27e43ˉ9eee〕61十e〔88b7d3ce9be9db"

}’ "jso∩』! : ∩u11’ "origj∩o0 吕 "17。22α233.154"’ "ur1": "http5;//洲.∩ttpbi∩。org/po5t00

=■司‖』《‖‖‖』·γ』■‖‖‖√司

第2章基本库的使用

32

·tj"eout参数 q

FTP请求°

下面用实例来看—下: mportur11jb·request

respo∩5e= (』r11ib.reque5t。uI1ope∩(0http5://州.httpbi∩。org/get0 』 t加eout≡0·1)

pIj∩t(re5po∩5e.read())

运行结果可能如下: DuIi∩g∩日∩d1i∩go千theaboγeexcept1o∩’ a∩otherex〔eptio∩o〔〔urred:

「r己〔eba〔促 (∏℃5tre〔e∩t〔a111a5t):「j1e 』!/v日r/py/pyt∩o∩/ur11ibte5t.py"』 1j∩eq’ 1∩〈『∏odu1e〉 respo∩5e=ur11ib°reque5t.uI1ope∩(‖http5://"洲.httpbj∩。org/8et|’ t1们eo0t=O。1) ■





ur11jb。eIror.0R[[rror:〈uI1ope∩error 551.〔;1059: 丁heh日∩d5h己keoper日t1o∩tmedout〉

|』■]‖‖』■■‖‖■■』■司』■■□□■』·二■■□■Ⅵ■■

tjⅦeout参数用于设置超时时间’单位为秒,意思是如果请求超出了设置的这个时间,还没有得 到响应,就会抛出异常。如果不指定该参数,则会使用全局默认时间°这个参数支持HTTP`HTTPS`

这里我们设置超时时间为0.l秒。程序运行了0.l秒后’服务器依然没有响应’于是抛出了0【[[rIOr



因此可以通过设置这个超时时间’实现当_个网页长时间未响应时’就跳过对它的抓取。此外’



异常。该异常属于uI111b.error模块,错误原因是超时° 利用trye×〔ept语句也可以实现,相关代码如下: 1ⅧPort5o〔代et

i∩Portur11jb·Ieque5t 1们portqr11jb°error

Ie5po∩5e≡ur11jb·Ieque5t.ur1ope∩(』https://州·httpbj∩.or8/get0 ’ t1Ⅷeout=α1〉

ex〔eptur11ib。error。0日[[rroIa5e8

‖■■

try8

1千i51∩sta∩〔e(e.rea5o∩’ sO〔ket·t1爬out): pri∩t(‖∏例[α」丁,)

而报错的结论,最后打印输出了丫I‖[00丁°

「I‖[卯丁

通过设置tmeout参数实现超时处理’有时还是很有用的°

口‖一□可

按照常理来说, 0.l秒几乎不可能得到服务器响应,因此输出了丁I‖[00丁的提示。

』{‖

运行结果如下:

■日|日

这里我们请求了https:〃wwwh仗pbinoIg/get这个测试链接’设置超时时间为0.l秒’然后捕获到 0R[[rror这个异常,并判断异常类型是5o〔ket.t1"eout’意思是超时异常’因此得出确实是因为超时

·其他参数

□·】■■」■|」』■|■司

除了data参数和t1"eol」t参数, ur1ope∩方法还有〔o∩text参数,该参数必须是551.55[〔o∩text类

‖。』』■■Ⅲ

■■■【■■■‖∏■■●【β‖Ⅲ■『「『}『伊〖}}■■【‖‖|『「『止■「‖||■「||■『『『|■∏『‖|■「【■「‖|■尸|||■「|[【■■●「||卜■「||[■『‖

2.l

urllib的使用

33

型’用来指定SSL的设置°

此外,〔a+11e和capat‖这两个参数分别用来指定CA证书和其路径,这两个在请求HTTPS链接 时会有用。

〔ade十au1t参数现在已经弃用了,其默认值为「a15e。

至此’我们讲解了ur1Ope∩方法的用法’通过这个最基本的方法’就可以完成简单的请求和网页 抓取。 ●Request

利用ur1ope们方法可以发起最基本的请求,但它那几个简单的参数并不足以构建一个完整的请求° 如果需要往请求中加人‖eadeIS等信息’就得利用更强大的Reque5t类来构建请求了。 首先,我们用实例感受—下Reque5t类的用法: j‖portur11ib。Ieque5t

reque5t≡ur11ib.reque5t·Req0e5t(0bttp5://pytho∩·org‖) re5po∩5e≡u工11jb.reque5t·uI1ope∩(reqoe5t) pr1∏t(re5po∩5e.read().de〔ode(『ut十ˉ8"))

可以发现,我们依然是用ur1ope"方法来发送请求,只不过这次该方法的参数不再是URL,而是

_个Reque5t类型的对象°通过构造这个数据结构’一方面可以将请求独立成一个对象’另一方面可 更加丰富和灵活地配置参数。

下面我们看一下可以通过怎样的参数来构造Reque5t类’构造方法如下: c1as5uI111b.reql」e5t·Reque5t(ur1’ data=‖o∩e′ beader5={}’ · 。 ho5t≡‖o∩e’ or1g1∩-req一

仙∩γeIi十1日b1e=「己15e’‖ethod=‖o∩e)

第_个参数ur1用于请求URL’这是必传参数’其他的都是可选参数。

第二个参数data如果要传数据,必须传byte5类型的。如果数据是字典,可以先用ur111b.paI5e 模块里的ur1e∩code方法进行编码°

第三个参数beader5是一个字典’这就是请求头’我们在构造请求时’既可以通过beader5参数 直接构造此项’也可以通过调用请求实例的add‖eader方法添加°

添加请求头最常见的方法就是通过修改05erˉ∧ge∩t来伪装测览器°默认的05erˉAge∩t是

pytho∩ˉur111b,我们可以通过修改这个值来伪装测览器°例如要伪装火狐测览器’就可以把05erˉ∧ge∩t 设置为:

"ozi11日/5.O(Ⅺ1j 0; [1∩uxj686)6e〔低o/2OO71127「1Ie十ox/2.0.0.11

第四个参数or1gi∩ˉreqˉ‖ost指的是请求方的host名称或者IP地址°

第五个参数u∩γer1伍ab1e表示请求是否是无法验证的’默认取值是「a15e’意思是用户没有足够 的权限来接收这个请求的结果°例如,请求一个HTML文档中的图片’但是没有自动抓取图像的权限 这时u∩`′eIi+1ab1e的值就是丁rue°

第六个参数"et‖od是_个字符串,用来指示请求使用的方法’例如GET、POST和PUT等。 下面我们传人多个参数尝试构建Req0e5t类: 十roⅦur11jbj们portreque5t’ p日r5e

ur1= ‖∩ttp5://www·httpb1∩。or8/po5t‖ header5={

!05erˉAge∩t|: !‖oz111a/4。o(〔o们patib1ei "5I[5。5j ‖j∩dow5‖丁)0 ’



第2章基本库的使用 |‖o5t|: |…。httpbi∩。org



d1Ct={0∩a爬′; 0gemey|} d己ta=byte5(par5e。ur1e∩〔ode(d1〔t〉’ e∩〔odj∩g=|ut千ˉ8‖) req=req0est·Reque5t(ur1≡0r1’data=data’ 打eader5=‖eader5’爬t∩od=』P05「‖) re5po∩5e≡reque5t.uI1ope∩(req) prj∩t(re5po∩5e.re己d().de〔ode(‖utfˉ8!))

这里我们通过4个参数构造了一个Reque5t类’其中的ur1即请求URL’header5中指定了 05erˉ∧ge∩t和‖o5t’ data用ur1e∩〔ode方法和byte5方法把字典数据转成字节流格式°另外,指定了 请求方式为POST。



"arg5": {}’ "data": 0‖"’

0干j1e5": {}’ "十or们": { ∩己∏e :

gemey

}’ "header5": { "∧c〔eptˉ[∩〔odj∩g"; "ide∩t1ty"’

"〔O∩te∩tˉke∩gth"; ′011"’ "〔o∩te∩tˉ「ype": "己pp1jcat1o∩/xˉ瞅ˉ+omˉ0r1e∩coded"’ "‖o5t00 : "…。‖ttpb1∩·org,0’ "05erˉAge∩t": 0|‖ozi11a/4·o(〔o∏|p日tib1e; ‖5I[ 5°5j ‖i∩dow5N「)"’ "XˉMz∩ˉ丁r己〔eˉId00 ; "Root=1ˉSed27十77ˉ884+5o3a2己a676Od+7679千0500 }’ "j5o∩": ∩u11’ ‘|Or1gi∩厕: 闻17。22O。2〕3.154"’

"uI1"8 "http5;//州.∩ttpbi∩.org/po5t" }

观察结果可以发现,我们成功设置了data、∩eader5和‖‖et|]od° 通过addheader方法添加∩eader5的方式如下: req=Ieque5t。Request(0r1=0r1’ dat己≡data’爬thod=‖p05『』)

req.addˉhe己der(005erˉ∧ge∩t|’ `‖o乙j11a/』.o(〔o们p己tjb1ej "5I[5。5j ‖1∩do"5‖丁)|)

有了Reque5t类’我们就可以更加方便地构建请求,并实现请求的发送啦°

我们已经可以构建请求了’那么对于-些更高级的操作(例如Cookje处理、代理设置等),又该 怎么实现呢?

此时需要更强大的工具’于是‖a∩d1er登场了。简而言之’‖a∩d1er可以理解为各种处理器’有 专门处理登录验证的`处理Cookie的、处理代理设置的。利用这些‖a∩d1er,我们几乎可以实现HTTP 请求中所有的功能。

首先介绍_下ur111b.reque5t模块里的Ba5e‖a∩d1er类,这是其他所有‖a∩d1er类的父类°它提 供了最基本的方法’例如de十au1tˉope∩` protoco1-reque5t等° 会有各种‖a∩d1er子类继承8a5e‖a∩d1er类,接下来举几个子类的例子如下。 □‖∏p0e十au1t[rror‖a∩d1eI用于处理HTTP响应错误’所有错误都会抛出‖∏P[rror类型的异常。 □‖∏pRedire〔t‖a∩d1er用于处理重定向。

□‖丁「p〔ook1epro〔e55oI用于处理Cookle°

□proxγ‖a∩d1er用于设置代理,代理默认为空。

□‖丁「ppa55word‖gr用于管理密码’它维护着用户名密码的对照表°

■∏■司‘勺二■]■可{‖|』□】

●高级用法

■■』■■〗||(』』■■】||‖|」‖■】〗‖|』】■可|』■」■|□|■■』‖||||‖」■■‖|■■■《|■■‖|||□□|』■〗|‖||‖|』■|《‖〗出■」■■』□□□■

运行结果如下:

‖』‖』■】]|‖□』』】』·‖刘卜}|(】‖‖■可』】』』】‖‖‖■〗]||■■‖‖〗』Ⅵ

34

{ ‖

` ↓

( q

| .|







0



|{|

2.l

urllib的使用

35

□‖「丁p8a5iC∧utb‖a∩d1eI用于管理认证,如果一个链接在打开时需要认证,那么可以用这个类来 解决认证问题° △■厂「‖|■=尸|■【『

关于这些类如何使用’现在先不急着了解’后面会用实例演示°

■』■=■■

另-个比较重要的类是0pe∩er0irector’我们可以称之为Ope∩er。我们之前用过的ur1ope∩方法’ 实际上就是urllib库为我们提供的-个0pe∩er。

巴■■「‖■「|||▲■庐■■■

那么,为什么要引人0Pe∩er呢?因为需要实现更高级的功能°之前使用的Reque5t类和ur1OPe∩类 相当于类库已经封装好的极其常用的请求方法,利用这两个类可以完成基本的请求’但是现在我们 需要实现更高级的功能’就需要深人_层进行配置,使用更底层的实例来完成操作’所以这里就用

到了0pe∩er°

0pe∩er类可以提供ope∩方法,该方法返回的响应类型和ur1ope∩方法如出_辙。那么’0pe∩er类



和‖a∩d1er类有什么关系呢?简而言之就是’利用‖a∩d1er类来构建0pe∩er类°

■尸】‖■尸‖巴■■「卜‖■■尸卜=■■「『▲=■卜巴■『

下面用几个实例来看看‖a∩d1er类和0pe∩er类的用法。 ●验证

在访问某些网站时,例如https:〃ssr3.scrape.center,可能会弹出这样的认证窗口’如图2ˉ2所示° 置录 h№8:牌「s■α……怕「 ■■■





用户名 {

■—二-一 —■=

■■■==→==一

一一■■≈≈-剐户~≈电0坦≈●…≈午=啥■巴●■■

〉■厉卜

■码

厂—-

β



▲ ▲

一=●p

7≡…←早→ˉ



取词

}「『

■■厅‖〖·□▲■「|‖|卜

图2ˉ2认证窗口

遇到这种情况,就表示这个网站启用了基本身份认证,英文叫作HTTPBasicAccessAuthentication,

这是一种登录验证方式,允许网页测览器或其他客户端程序在请求网站时提供用户名和口令形式的身 份凭证。



那么爬虫如何请求这样的页面呢?借助‖∏pB35i〔∧uth‖a∏d1eI模块就可以完成,相关代码如下:

卜‖

千r蛔ur11jb.req眶5timort‖∏pP■■5加r啤巾it∩促「au1tRea1佃’}{∏pBa5j〔∧l』th胸∩d1eI’ buj1d-ope∩eI 十r咖u【Uib.errori卯ortⅦlf∏oI u5em…= 0白向i∩0

=■■『『

pa55mrd=`a曲i∩0 ur1= ‖http5://55Ⅲ3·5Cra医·〔e∩teI/‖

b



p≡盯『ppas5即Ⅲ鳃巾ith匹+au1tReam() p·addˉpa55mrd(№∩e’ ur1’ user∩…’ pa三5蜘rd) authha∩d1er=盯丁pBaBicAuth‖a∏d1er(p)

p

ope∩er=bui1d-卯e∩er(auth≡bam1er)



tIy;

l尸



P

p

re5u1t=ope∩er.ope∩(ur1) = ∩t用1=re5u1t·read().de〔ode(|ut于ˉ8!〉 pI1∩t(∩t∏1) ex〔ePt0Rk[rTora5e:

pri∩t(e.rea5o∩)

[2

「 卜



第2章基本库的使用

36

这里首先实例化了一个‖『丁P8a51c∧ut∩‖a∩d1er对象autbba∩d1eI,其参数是‖丁丁pp日55word‖grˉ

‖1t‖0e十au1tRea1用对象,它利用add—pa55word方法添加用户名和密码’这样就建立了一个用来处理验 证的‖a∩d1er类°

然后将刚建立的己ut∩ha∩d1er类当作参数传人bu11d-ope∩er方法’构建-个0Pe∩er’这个0Pe∩er 在发送请求时就相当于已经验证成功了°

●代理

做爬虫的时候,免不了要使用代理,如果要添加代理’可以这样做:

■■‖▲■∏|』·|‖『】■■习」‖|」■■■

最后利用0pe∩er类中的ope∩方法打开链接’即可完成验证。这里获取的结果就是验证成功后的 页面源码内容°

干roⅦur11ib。erroImport0RL[rror 千ro们ur111b。reque5t加portproxy"a∩d1er’ buj1dˉope∩er

proxyˉ帕∩d1er=proxy‖a∩d1eI({ 0∩ttp|; 0http://127。o·0。1:808o0 ’ ′http5′ : !∩ttPs://127·0·0·1:808O! }) ope∩er=buj1dˉope∩eI(proxy_∩a∩d1er) try:

re5po∩5e=ope∩er.ope∩(`http5://w朋·ba1du.〔o∩‖) prj∩t(re5po∩5e.read(〉°de〔ode(|utfˉ8』)〉 ex〔ept0【[[Irora5e: Pr1∩t(巳re35o∩)

这里需要我们事先在本地搭建—个HTTP代理,并让其运行在8080端口上°

上面使用了proxy‖a∩d1eI,其参数是一个字典’键名是协议类型(例如HTTP或者HTTPS等)` 键值是代理链接,可以添加多个代理。

然后利用这个‖a∩d1er和buj1dˉope∩er方法构建了-个0pe∩eI’之后发送请求即可°

q





●Cookie





处理Cookie需要用到相关的‖a∩d1eI°

我们先用实例来看看怎样获取网站的Cookle,相关代码如下: 1呻ort‖ttp.〔oo代1ej日r’ ur11ib。reque5t cookie=http.coo灶ej己r.〔ooR1e〕ar() h己∩d1er≡ur111b。req0e5t.‖丁丁p〔oo代1epro〔e55or(〔oo促ie) ope∩er=ur111b·Ieque5t.buj1dˉope∩er(‖a∩d1eI) re5po∩se=ope∩er·ope∩(0https://卿.b己1du.〔o爪)



十or1te‖i∩cook1e:

Prj∩t(1te川.∩a爬+"≡』』 +jte∏|°v己1Ue)

后利用bu11dˉope∩er方法构建0pe∩er’执行ope∩函数即可° 运行结果如下: 8AI山I0=∧09[6〔4[3875353189「8q〔6O〔〔9「D「〔B;「C=1 BID0p5ID=∧O9[6〔4[387535312「8M4628O〔6〔SO2

‖p5p55ID=31358145231325210883111O31253316O531271314633O823 p5丁‖=159O854698 80SγRⅧ=1O

BD卜Ⅷ[=1

可以看到’这里分别输出了每个Cookie条目的名称和值。

Ⅲ■』■]』■|《』■■∏」勺|‖‖」■∏』·‖·‖司』■■{(|纠■]|{』||』■】|{|日|』』】■■』

首先,必须声明-个〔ookje〕ar对象°然后需要利用‖∏p〔ook1epro〔e55or构建一个‖a∩d1er,最

厂卜『「■「

})‖

2」 urllib的使用

37

p

既然能输出,那么可不可以输出文件格式的内容呢?我们知道Cookie实际上也是以文本形式保 存的。因此答案当然是肯定的,这里通过下面的实例来看看: 加Portur11ib.reque5t′ http.〔oo代jejaI



十i1e∩日阳e= ‖〔OO促1e。tXt‖

p



〔oo促je=http.〔oo代jejaI.‖ozi11a〔ooRje〕ar(「i1e∩a们e) ha∩d1eI=ur11jb.Ie∩ue5t.‖丁『p〔oo代jeproce55or(〔oohje) ope∩er二ur111b.reql」e5t.bu11d-ope∩er(h日∩d1er) re5po∩5e≡ope∩er。ope∩(|http5://州.bajdu。〔o川0) coo促ie.53ve(1g∩ore-di5card≡丁Iue’ 1g∩ore-expjre5=丁rue)

这时需要将〔ooRje]ar换成‖ozi11a〔oo代1e〕ar,它会在生成文件时用到,是〔oo促1e〕ar的子类’

可以用来处理跟Cookie和文件相关的事件,例如读取和保存Cookie’可以将Cookie保存成Mozilla型 测览器的Cookie格式。

运行上面的实例之后’会发现生成了一个cookie.txt文件’该文件内容如下: #‖et5cape‖∏p〔ookje「j1e #∩ttp://〔ur↓。haxx.5e/r+c/〔oo促jeˉ5pec。∩t"1 #丁hj5j5age∩erated+i1e| Do∩otedjt.

〗·厂

。bajd0。〔oⅦ °bajdu。〔o∩ °baid0.co" 。bajdu·co们

丁【0[ 『R0[ 丁R0[ 「R0[

/ / / /

/ / / /

「∧[5[ 「A[5[ 「∧[5[ 「∧[5[

16Ⅱ2390755 3738338qo2 ‖p5pS5I0 3738338』02

8∧I0(」I0 o84A680川8o〔o[53[5B82∧「098「9178「:「C=1 8I00p5ID 08哗∧68D748o〔0[53471「∧632928o「∧58 31262143831325 2112731110〕159631673 31q6q3o82326350 p5Ⅷ 1S9o854754

州.b日idu.〔咖「∧[5[/ / 「∧[5[

805γ盯"

0

州°bajdu°co们「∧[5[/ / 「∧[5[

B0卜Ⅷ[

1

|)

另外’ [‖p〔oo促1e〕ar同样可以读取和保存Cookie’只是Cookie文件的保存格式和‖o2111a〔oo促1e〕ar

匹■『‖■|卜『】卜■厂‖匹■■「■=■厂‖凸口|口『‖[■「●「■》巴■■

不-样,它会保存成LWP(libwwwˉperl)格式°

要保存LWP格式的Cookie文件’可以在声明时就进行修改: 〔oo促je=∩ttp.coo低1ejar儿‖p〔oo氏1e〕ar(十j1e∩a们e)

此时生成的内容如下: #L‖p≡〔Oo低je5ˉ2。0

5etˉ〔oo低1e3: BAID0I0="1「3O[[D∧35〔7∧94320275「991〔A583A5$「C=1"; pat∩="/00j do们aj∩=00·ba1du°coⅧ"】 Path-5Pec; do‖a1∩doti exp1re5=0|2021ˉo5ˉ3O16:O6:39Z"j co『∏肥∩t=bd『 γer5io∩=O

5etˉ〔ookje3:8I叫P5I0=1「30[[0∧35〔7∧9033〔97〔『6245〔8〔383jpat‖="/"〗d咖aj∩=0‖°b己1du。〔o们"βpath=5pe〔jdo‖m∩dotj

expire5="2O88ˉ06ˉ1719:20:46Z"; γer5jo∩=O

5etˉ〔oo促ie3: ‖p5p55I0=316R6144O2112431o6931254315943O8413167331464317153O823j Pat们≡,0/"j

do"a1∩="°bajdu。c咖"; patb-5pe〔; do『『Ej∩dotj di5〔ardj γeI51o∩=0

Setˉ〔oo恨ie〕: p5刚ˉ159O854799j pat打≡厕/,』了do"|aj∩ˉ!』.bajdu.co∏』; pathˉ5pec; d咖aj∩dotj exPjre5ˉ腮2O88ˉo6ˉ17

19:2o:462"j γer5jo∩=0

5etˉ〔oo促je3: BD5γ盯‖=11j path="/‖′j do们aj∩="0州w°ba1du·〔o∩‖"; pathˉ5pecβ d15cardj γer51o∩=0 5etˉ〔oo促je〕: B0}删[=1’ path="/0『j d咖aj∩=""Ⅷ.bajdu·c咖"; path=5pe〔j dj5〔ardj γer5io∩=0

由此看来’不同格式的Cookje文件差异还是比较大的° . !』

那么’生成Cookje文件后’怎样从其中读取内容并加以利用呢? 下面我们以[‖p〔OOkje〕ar格式为例来看一下: jⅦportur11jb.reque5t′‖ttp.coo低iejar ■』

p

coo代je=∩ttp.〔oo代jej3r儿‖p〔ookje〕ar()

〔ooⅨje·1oad(,coo促ie.txt0 ’ 1g∩ore一di5card二『rue$ jg"ore=expjres=丁me) ha∩d1er≡ur11ib。reql』e5t。‖∏p〔oo代ieprocessor(cookje) ope∩er=ur11ib.reql」e5t.bui1d-ope∩er(ha∩d1er) respo∩5e=ope∩er.ope∩(!http5://"Ⅷ.bajdu.〔o‖‘) prj∩t〈Iespo∩5e.read().decode(‖ut千ˉ8,))

可以看到,泣里调用1oad方法来读取本地的Cookje文件’获取了Cookje的内容°这样做的前提

]|

是我们首先生成了[‖p〔ooRie〕ar格式的Cookie’并保存成了文件°读取Cookie之后’使用同样的方



法构建‖a∩d1er类和0pe∩er类即可完成操作°

■∏‖·』‖{‖』■■∏』■□‖』‖‖」●■■‖‖』‖□■■]

第2章基本库的使用

38







运行结果正常的话,会输出百度网页的源代码°







通过上面的方法’我们就可以设置绝大多数请求的功能°





2处理异常

我们已经了解了如何发送请求,但是在网络不好的情况下’如果出现了异常’该怎么办呢?这时 要是不处理这些异常,程序很可能会因为报错而终止运行’所以异常处理还是十分有必要的°

urllib库中的error模块定义了由reque5t模块产生的异常。当出现问题时, reque5t模块便会抛 出erroI模块中定义的异常° ●0R[[rrOr

■■■

下面用-个实例来看-下:

■■■‖{‖■■可

它具有_个属性rea5o∩,即返回错误的原因。

·

0R[[rror类来自urllib库的error模块’继承自05[∏or类’是error异常模块的基类,由reque5t 模块产生的异常都可以通过捕获这个类来处理°

十ro刚l』I11jbjⅧportIequest’ eIror try:

respo∩5e≡Ieque5t。ur1ope∩(!http5://〔ujqj∩gcai.〔o∩/404|)



ex〔epteIror.0肌[rrora5e:

pr1∩t(e.reaso∩〉



我们打开了_个不存在的页面’照理来说应该会报错,但是我们捕获了0R[[rIor这个异常’运行 结果如下: ‖ot「ou∩d

程序没有直接报错’而是输出了错误原因,这样可以避免程序异常终止,同时异常得到了有效 处理°

||





●|∏丁p[rror

‖∏P[rror是0R[[rror的子类,专门用来处理HTTP请求错误,例如认证请求失败等°它有如下3 个属性°





□Code:返回HITP状态码,例如4叫表示网页不存在, 500表示服务器内部错误等。

□rea5o∩:同父类—样,用于返回错误的原因° □们eader5:返回请求头。

下面我们用几个实例来看看: +I咖uI11ibj‖Portreque5t’e∏oⅢ try:

re5po∩se=Ieque5t.uI1ope∏(,∩ttp5://〔l』jqi∩g〔a1.〔o『∏/』O』‖) exCepte∏or.‖∏p[rroIa5e:

pri∩t(e。Iea5o∩’e。code’ e.header5’ 5ep=!\∩』)

404

5erγer: ∩gi∩x/1·1O。3 (0bU∩tU)

||{

‖ot「o0∩d

■Ⅷ‖|□

运行结果如下:



厂■}匡尸『|△『‖

2。l

|》卜



urllib的使用

39

Date: 5at’ 3O陀y202O16:O8:42叫丁 〔o∩te∩tˉ「γpe: text/htⅧ1i c‖己r5et=盯「ˉ8

丁ra∩5+erˉ[∩〔Odj∩8: 〔∩0∩促ed 〔o∩∩e〔tjO∩: C1O5e

} p

厂}「 ■■■∏■[‖△■尸



5etˉ〔oo代je; p‖p5[S5ID=kp1a1bOo3a0pc「688促t73g〔78oj pat‖=/ pmg们日: ∩oˉcac∩e γary: 〔oO恨1e

[2

[xPire58 ‖ed’ 11〕a∩198qO5:0O;OOCN丁

〔a〔heˉ〔o∩tro1: ∩oˉ〔己che’ 们u5tˉreγ日1jdate』 肌日xˉage=o [i∩悯: 〈http5://c0iqj∩gca1°〔m/wpˉj5o∩/〉j re1="‖ttp5://apj."。org/"

依然是打开同样的网址,这里捕获了‖丁丁p[rror异常’输出了rea5o∩、code和∩eader5属性。 因为0R[[IrOI是‖∏p[rrOr的父类,所以可以先选择捕获子类的错误’再捕获父类的错误,于是 卜述代码的更好写法如下: 千roⅧur11jbi∏portreque5t’error

尸|}●■‖|‖

try:

re5po∩5e=reque5t.ur1ope∩(0https://〔ujq1∩gcaj.co‖/4o4|) ex〔epterror.‖∏p[rIorase:

pri∩t(e.Ieaso∩’ e·〔ode’ e.∩eader5’ 5ep≡|\∩,)

°卜’》`■「‖|Ⅱ■■【■『『~■厂

excePterror·0R[[rrora5e:

pri∩t(e.Iea5o∩) e15e8

pr1∩t(‖Reque5t5u〔〔e55+u11y‖)

这样就可以做到先捕获‖∏p[rror’获取它的错误原因、状态码、请求头等信息°如果不是 ‖∏p[rror异常,就会捕获‖R[[rror异常’输出错误原因°最后,用e15e语句来处理正常的逻辑。这 是一个较好的异常处理写法°



▲■『‖|『世「卜||〖■匹■「|■■尸『

|}



有时候’rea5o∩属性返回的不一定是字符串’也可能是一个对象。再看下面的实例: i们portsoc代et mportur111b.reque5t 加portur11ib.error

trγ;

re5po∩se≡uI11jb.reque5t.ur1ope∩(|http5://哪.bajdu.〔o∏]‖’ ti爬out=O。01)

ex〔eptur11ib°erroI·0R[[rIoI己5e:

| !离溅{翻{}……; 这里我们直接设置超时时间来强制抛出t1‖eout异常。 运行结果如下: 〈C1a5505oCket°tmeOut0〉 丁I‖[α∏

可以发现’rea5o∩属性的结果是5oc恨et.t1Ⅶeout类。所以这里可以用15j∩sta∩ce方法来判断它 的类型’做出更详细的异常判断°

本节我们讲述了error模块的相关用法’通过合理地捕获异常可以做出更准确的异常判断,使程 序更加稳健。

3.解析链接

前面说过,urllib库里还提供了par5e模块,这个模块定义了处理URL的标准接口,例如实现URL 各部分的抽取、合并以及链接转换°它支持如下协议的URL处理: file` f↑p、gopher、hdl、http、https、

1map、mailto、∏∏ns`news、∏ntp、pIospero`rsync、rtsp` rtspu` shp` sip` sips` snews` svn` sv∏+ssh` telnet和wais°

‖{‖‖

40

第2章基本库的使用

下面我们将介绍par5e模块中的常用方法’看-下它的便捷之处° ●ur1par5e (

该方法可以实现URL的识别和分段,这里先用一个实例来看一下: q

十ro川ur11jb。p日I5empoIt l』r1par5e

re5u1t=uI1p己r5e(!∩ttp5;//0w0w.bajdu°〔oⅧ/i∩de×.htⅦ1ju5er?id≡5#co∏∏肥∩t0)

这里我们利用ur1par5e方法对—个URL进行了解析,然后输出了解析结果的类型以及结果本身。 运行结果如下: <C1己55 0ur11jb·par5e.par5eRe5u1t0〉

par5e【e5u1t(sche∩`e≡′http5|’ ∩et1o〔二Www.baidu。〔o∩‖’ path=|/j∩de×.htⅧ1‖’ p3raⅧ5≡|u5er‖’ query≡|jd=5|’ 十ra8们e∩t=!CO『∏们e∩t‖)



ˉ·■|●□可‖·●

pri∩t(type(re5u1t)) pr1∩t(re5u1t)

可以看到,解析结果是一个par5eRe5u1t类型的对象,包含6部分’分别是5〔∩e‖e`∩et1oc`path` para肌5、 querγ和千mgⅦe∩t。 再观察一下上述实例中的URL: q

bttps://www·baid0.〔oⅦ/1∩dex.ht川1;u5er?jd=5#coⅧe∩t

可以发现’叮1par5e方法在解析URL时有特定的分隔符°例如://前面的内容就是5〔he川e’代 表协议。第_个/符号前面便是∩et1o〔’即域名;后面是pat∩,即访问路径°分号j后面是pamⅦS, 代表参数°问号?后面是查询条件query,一般用作GET类型的URL。井号#后面是锚点+rag"e∩t’

‖|

用于直接定位页面内部的下拉位置°

q

于是可以得出-个标准的链接格式’具体如下: 5〔he"e://∩et1o〔/pathjpar日Ⅶ5?query#+ra8"e∩t

一个标准的URL都会符合这个规则,利用uI1par5e方法就可以将它拆分开来°





除了这种最基本的解析方式外’ ur1Par5e方法还有其他配置吗?接下来’看—下它的API用法: ur11ib.par5e.ur1par5e(ur15trj∩g′ 5cheⅧe=! 0 ’ a11ow=十r日gⅦe∩t5=『rue)

可以看到, ur1Par5e方法有3个参数。

将这个作为默认协议。我们用实例来看一下:

』■|、‖|』■■{』■可

□ur15trj∩g‘这是必填项,即待解析的URL° □5〔∩e"e:这是默认的协议(例如http或https等)。如果待解析的URL没有带协议信息’就会



+ro们0I111b.par5e1们portuI1parse

‖‖

re5u1t=ur1par5e(‖洲w。bajdu.〔o们/j∩dex.∩t们1ju5er〉id=5#〔o∏∏∏e∩t0 ’ 5〔∩e阳e≡0∩ttp5‖) Pr1∩t(re5u1t)

运行结果如下: par5eRe5u1t(5〔he爬=0http5‖’ ∩et1o〔=』 | ’ p己th=‖M川w.b日1du.co∏/1∩dex·ht『n1‖′par己们5≡u5er ’ query=‖1d≡5‖’

可以发现’这里提供的URL不包含最前面的协议信息’但是通过默认的5〔he"e参数’返回了

结果∩ttp5。 假设带上协议信息:

】‖

re5u1t=ur1par5e(‖http://www。ba1d‖.〔o∩/1∩dex·∩t∏》1ju5eI?jd=5#〔ome∩t0 ’ 5c∩e爬二‖http5})

』·』』■‖|·□∏」■

十rag眠∩t二0co咖e∩t0)

{|





||}

2.l

urlljb的使用

4l

则结果如下:

par5eRe5u1t(5〔he爬≡‖∩ttP‖’ ∩et1o〔≡M‖w.b己1du.〔o阳|’ pat∩≡‖/1∩dex。ht川1‖’ p日m阳s≡u5er ’ query=‖1d≡5‖’ 十mg田e∩t≡‖〔ome∩t‖)

可见’5〔he"e参数只有在URL中不包含协议信息的时候才生效。如果URL中有,就会返回解 『||■「‖‖巳尸||厂}‖■=「〖仆|【■「|‖·以∏‖匹口|《尸【‖|》|』卜‖·β‖炉「‖■「|仑厂‖■『卜●厂〖『‖八■「||‖》「伊′[‖"‖仿·‖|

析出的5〔∩e阳e°

□a11oⅣ十ragⅧe∩t5:是否忽略+rag‖e∩t。如果此项被设置为尸a15e,那么十mg‖e∩t部分就会被 忽略’它会被解析为path` para‖S或者query的—部分’而千mg们e∩t部分为空。 下面我们用实例来看—下: +roⅧur111b·par5e1Ⅶportur1parse

re5l」1t= (」I1par5e( 0http5://硼w.baidu.〔oⅦ/1∩dex.∩tⅧ1ju5er?1d=5#〔oⅦ∏e∩t0 ’ a11ow_+mg川e∩t5=「a15e) pr1∩t(re5u1t)

运行结果如下: par5eRe5u1t(5〔∩e爬≡|‖ttp5! ’∩et1oc=‖们州·baidu。〔o"’path=!/i∩dex.们t∏1‖’pamⅧ5=0u5er ’query=,jd=5#come∩t0 ’ 十ragⅦe∩t=|』)

假设URL中不包含pamⅧ5和query’我们再通过实例看—下: 千ro们ur11jb。par5e1Ⅶportur1par5e

re5u1t二ur1pa工5e(|http5://州。bajdu·〔om/1∩dex.‖m1#〔oⅣ‖『冶∩t‖’ a11ow-十r己g们e∩t5≡「己15e) pr1∩t(re5u1t)

运行结果如下: Par5eRe5‖1t〈5che阳e={‖ttp5|’ ∩et1o〔≡"糊.b己jd|」.〔o川’ path≡‖/1∩dex.∩t"1#〔o|↑‖γ冶∩t‖」 para肌5=』‖ ′ query=, ‖ ’ 十ragⅧe∩t≡` ,)

可以发现,此时十mg‖e∩t会被解析为path的_部分° 返回结果par5eRe5u1t实际上是一个元组’既可以用属性名获取其内容’也可以用索引来顺序获 取。实例如下: +ro阳ur11jb。par5e1爪portur1par5e

re5u1t≡ur1par5e(忙tp5://"ww。ba1du。〔o川/i∩dex.ht们1#〔o∏∏∏e∩t! ’ 日11o比+r己gⅧe∩t5=「a15e) pri∩t(re5u1t.s〔‖e‖↑e’ re5u1t[O]′ re5u1t.∩et1oc’ re5u1t[1]’ 5eP=‖\∩‖)

这里我们分别用属性名和索引获取了5〔∩e『『|e和∩et1oC’运行结果如下: ∩ttp5 http5 州°baidl」°〔oⅧ 咖bajdu.〔o们

可以发现’两种获取方式都可以成功获取’且结果是—致的。

■■『『■『||匹■∏「

●ur1‖∩Par5e

有了ur1par5e方法,相应就会有它的对立方法ur1u∩par5e’用于构造URL。这个方法接收的参 数是一个可迭代对象’其长度必须是6,否则会抛出参数数量不足或者过多的问题。先用-个实例看



}「 }▲■□『『‖‖△尸[||『

p

-下: 「ro们ur11jb°p己r5ei们poItur1u∩paI5e

d己ta≡ [ ‖‖ttp5‖ ’ ‖www·ba1du.〔咖』’ |i∩dex.∩t们1,’|u5er!’ ‖a=6′’ 0Come∩t‖ ] pr1∩t(ur1‖∩par5e(data))

这里参数data用了列表类型°当然,也可以用其他类型,例如元组或者特定的数据结构°

}|)

第2章基本库的使用

运行结果如下: http5://"Ⅶw。bajdu。co∏l/j∩dex。hm1ju5er?a=6#cα∏∏e∩t

这样我们就成功实现了URL的构造° ●ur15p1jt

这个方法和ur1par5e方法非常相似’只不过它不再单独解析p己m们5这一部分(pam∏5会合并到 p己tb中)’只返回5个结果°实例如下:

「 ■

fro们ur11ib.p己r5e1呻ortur15p1it

曰 | | ■

re5u1t=ur15p1it(!http5://www.b己id‖·co们/i∩dex。ht川1】u5er?id=5#〔o∏Ⅷe∩t!)



prj∩t(re5u1t)

二 ‖ ‖ ‖

运行结果如下:

| ·

5p11tRe5u1t(5〔be川e=0∩ttps0 』 ∩et1oc≡www°ba1du°〔o‖ ’ pat‖=‖/j∩de》(,∩t们1j(』seI ’ q0ery=!1d≡50 ’



千mg爬∩t≡0〔Ome∩t‖)

可以发现,返回结果是5p11t【e5u1t’这其实也是一个元组’既可以用属性名获取其值,也可以 用索引获取。实例如下:

』■呵|■』

+ro"ur11jb·par5ej‖portur1sp1jt

□引‖

re5u1t=ur1sp1it(|∩ttp5://…。b己jdu。〔o们/i∩dex·∩t们1;u5er?jd=5#〔ome∩t0 ) pIj∩t(re5u1t.5〔∩e眠′ re5u1t[O])

运行结果如下: http5http5

●ur1u∩5p1it

与ur1u∩par5e方法类似,这也是将链接各个部分组合成完整链接的方法’传人的参数也是一个可 迭代对象,例如列表、元组等,唯一区别是这里参数的长度必须为5。实例如下: +roⅧur111b·par5eiⅦportur1u∩5p1jt

data= [ 0们ttp5‖’W0‖w.bajdu.〔咖‖’ |j∩dex.ht们10 ’ !己=6‖」 ‖〔o∏u爬∩t0 ] pIj∩t(ur1u∩5p11t(dat己)〉

运行结果如下: http5://哪。ba1du°〔o∏‖/i∩dex·ht肌1?3=6#〔o∩】∏e∩t

●‖r1joi∏

ur1u∩p日r5e和ur1u∩5p1it方法都可以完成链接的合并’不过前提都是必须有特定长度的对象’链

‖』■司‖句■可|]』〗‖{·】■‖』|‖』』■■可‖‖』■』‖‖‖‖|』】●可|』■■]|」■可]|‖||」‖■■‖|||]』■·□‖《||·□

接的每一部分都要清晰分开° Q

除了这两种方法,还有-种生成链接的方法,是ur1joj∩°我们可以提供一个baseur1(基础链 接)作为该方法的第一个参数,将新的链接作为第二个参数°uI1joi∩方法会分析ba5euI1的5che爪e、 ∩et1oc和pat∩这3个内容,并对新链接缺失的部分进行补充’最后返回结果° 下面通过几个实例看一下: +ro∏↑ ur11jb.par5e1川portur1joj∩ pri∩t(ur1joj∩(‖∩ttp5://咖.b日idu.〔o∩! ’ !「∧0.ht刚1‖)〉 pri∩t((』r1joi∩(0http5;//州.ba1du.〔o们』’ ,http5://〔ujqj∩g〔a1.co"/「∧O.ht们1‖)) pr1∩t((』r1jo1∩(0http5://洲w.baid(」.〔o‖/3boutht∩1‖’ 』∩ttps://cujqj∩gcai.co们/PAQ.htⅧ10)) pri∩t(uI1joj∩(,∩ttp5://州.baidu·〔oⅧ/about.∩tⅧ1‖’ ‖∩ttp5://〔ujqi∩g〔己i·〔o‖/「AQ.∩t川1?que5tjo∩=20)〉 prj∩t(l』r1jo1∩(|https://硼.ba1du.〔o们?"d=abc|」 |http5://〔uiq1∩gca1°〔o∏‖/j∩de×。php|)) pIj∩t(ur1joj∩(0http5://哪.bajd0.〔o"0 ’ ‖》〔己tegorγ=2#〔o|{∏论∩t0))

二 ■ 」 日 ■ ■ ■ ■ 口 · ■ ■ ■ ■



42



「『‖【巴■■■【〗〗■【■■「『‖||快β『}『|■■■|||〖■「‖‖‖‖匡■厂止=■■|巴■「「|‖血Ⅳ『「||}●「‖‖『|巴尸【「||上=■|■尸

2.l

urllib的使用

43

Prj∩t(ur1jo1∩(0‖"‖w.bajdu.〔o‖0 ’ 『?category=2#〔o∏〗"e∩t0)) pri∩t(ur1joi∩(|www.b己idu.〔o脯〔ome∩t,’ ‖?category≡2!))

运行结果如下: http5://训ww.baidu·coⅦ/「∧O·ht∏1

http5://〔u1qj∩gcaj。coⅧ/「∧0。∩t"1 httP5://〔Uiq1∩g〔a1。CO川/「∧0·ht川1 httP5://〔U1qi∩g〔a1。〔OⅦ/『∧0.们tⅧ1?ql」e5t1O∩=2 http5://c‖jqi∩g〔日1。〔o∏/1∩dex·php http5://Ⅶ枷.b日idu°co∏?〔ategory≡2#co咖e∩t www。b己idu·〔o们?c日tegoIy≡2#come∩t w枷·b己idu·〔oⅧ?〔ategoIy=2

可以发现’ ba5eur1提供了三项内容: 5〔he"e` ∩et1o〔和pat‖。如果新的链接里不存在这三项, 就予以补充;如果存在’就使用新的链接里面的’ ba5eur1中的是不起作用的。

■=尸|●厂‖|△■■卜■=七尸卜|■■■【■〗『皿=■尸卜|‖■■「‖■『|}卜「}但■「|■「『‖|△■【‖‖·‖■■∏‖〖‖‖|■=「|‖■【■厂|匹■厂|

通过Ur1jo1∩方法,我们可以轻松实现链接的解析`拼合与生成° ●ur1e∏〔ode

这里我们再介绍~个常用的方法—ur1e∩〔ode’它在构造GET请求参数的时候非常有用,实例 如下: 十ro‖ur11jb。par5e1们poItur1e∩〔ode

p3ra们5≡{ 0

0

∩己『∏e :

8er∏ey J

|age|: 25 }

base l』I1= 《‖ttp5://w硼°baidu.〔o们?」 ur1=b己5eur1+ur1e∩code(pamⅦ5) prj∩t(0r1)

这里首先声明了_个字典paraⅦ5,用于将参数表示出来’然后调用ur1e∩〔ode方法将pamⅦ5序 列化为GET请求的参数°

运行结果如下: ∩ttp5://w刷·ba1du·co们?∩a‖e=gemeγ&a8e=25

可以看到’参数已经成功地由字典类型转化为GET请求参数°

uI1e∩〔ode方法非常常用。有时为了更加方便地构造参数’我们会事先用字典将参数表示出来, 然后将字典转化为URL的参数时,只需要调用该方法即可。

[■■|■■∏[□尸『■|}』■【∏「|}▲■『■卜「|

●par5e=qS

有了序列化’必然会有反序列化。利用par5e-q5方法’可以将一串GET请求参数转回字典’实 例如下: +IOⅧUr111b.P己r5ej川POrtPar5e=q5

query= ∩a爬=ger爬y8a8e=25! pI1∩t(par5eˉq5(query))

■『}止「[『|【『■‖‖卜「|■『‖Ⅱ【■·‖}【◆〗〗〗『Ⅱ【■■【

运行结果如下: {{∩a"e′: [0gemeγ』]’ ‖age‖ : [ 』25‖ ]}

可以看到’URL的参数成功转回为字典类型° ●parse=q51

par5eˉq51方法用于将参数转化为由元组组成的列表’实例如下:



b

[(‖∩aⅦe0 ’ !gemey0)’(‖日ge0 ’ 』25‖)]

可以看到,运行结果是一个列表,该列表中的每一个元素都是一个元组’元组的第_个内容是参 数名’第二个内容是参数值。

■■引』(‖』‖□‖‖■

运行结果如下:



querγ= ∩日∏记=8emeγ&age=25! pri∩t(p己r5e-q51(query))



千IO‖Ur11ib·Par5emPOrtPar5e-q51 `

』·口‖||]■■‖|司‖|‖■■|

第2章基本库的使用

44

●quote

代eγword= ‖壁纸0

ur1= ‖∩ttp5://刚w.b日jdu.coⅧ/5〉wd=| +ql」ote(促eyword)

|■■」■■∏□■可‖|■■■

十Io‖ur11jb。p己r5ej们portquote



该方法可以将内容转化为URL编码的格式°当URL中带有中文参数时’有可能导致乱码问题

此时用quote方法可以将中文字符转化为URL编码,实例如下:

pri∩t(uI1)

这里我们声明了—个中文的搜索文字’然后用quote方法对其进行URL编码’最后得到的结果 如下:

q



http5://www。b己1du·coⅧ/5?wd=%[5汕3油1%[7%BA%88



●u∩quote

0

有了quote方法,当然就有u∩quote方法,它可以进行URL解码’实例如下: +roⅧ l」r11jb。par5ej们portu∩quote

ur1= ‖http5://州·ba1du°〔o‖/5?Ⅶd=%[5蝴3%81%[7%8峨880 prj∩t(u∩quote(l」r1))

这里的ur1是上面得到的URL编码结果’利用u∩quote方法将其还原,结果如下: bttp5://w枷·ba1du。〔oⅦ/5?wd≡壁纸

可以看到’利用u∩quote方法可以方便地实现解码°

本节我们介绍了Par5e模块的_些常用URL处理方法°有了这些方法’我们可以方便地实现URL 的解析和构造,建议熟练掌握. 4.分析RObotS协议

利用urllib库的Iobotpar5er模块,可以分析网站的Robots协议。我们再来简单了解-下这个模 块的用法。

q

| Q











q

」 q q

d

●Robots协议

Robots协议也称作爬虫协议、机器人协议,全名为网络爬虫排除标准(RobotsExclusionProtocol)’ 用来告诉爬虫和搜索引擎哪些页面可以抓取`哪些不可以°它通常是_个叫作robots.txt的文本文件’ _般放在网站的根目录下。

搜索爬虫在访问_个站点时’首先会检查这个站点根目录下是否存在robots.txt文件,如果存在’ 就会根据其中定义的爬取范围来爬取°如果没有找到这个文件’搜索爬虫便会访问所有可直接访问的 页面°

下面我们看—个robots.txt的样例: 05erˉage∩t: *

d|



| ( (



4■

L

■■口□可‖

2.l

urllib的使用

45

0j5a11OW目 /

A11ow: /pub11c/

这限定了所有搜索爬虫只能爬取Public目录。将上述内容保存成robotStxt文件’放在网站的根目 录下,和网站的人口文件(例如indexphp`mdexhtml和indexjsp等)放在_起° 上面样例中的05eIˉage∩t描述了搜索爬虫的名称’这里将其设置为*,代表RobotS协议对所有爬 取爬虫都有效。例如,我们可以这样设置: 05erˉage∩t: 8aidu5pider

这代表设置的规则对百度爬虫是有效的°如果有多条05eIˉage∩t记录’则意味着有多个爬虫会受 到爬取限制’但至少需要指定一条°

0i5a11O"指定了不允许爬虫爬取的目录,上例设置为/,代表不允许爬取所有页面。

∧11ow_般不会单独使用’会和015a11ow一起用’用来排除某些限制°上例中我们设置为/pub1ic/’ 结合015a11o"的设置’表示所有页面都不允许爬取’但可以爬取public目录° 下面再来看几个例子。禁止所有爬虫访问所有目录的代码如下: 05erˉage∩t: * Di5己11o训: /

允许所有爬虫访问所有目录的代码如下: 05erˉage∩t; 木 0i5a11Ow:

另外’直接把robots.txt文件留空也是可以的° 禁止所有爬虫访问网站某些目录的代码如下: 0Serˉage∩t; * 0j5a11o问: /pr1γate/ 0j5a11OW8 /t川p/

只允许某-个爬虫访问所有目录的代码如下: 05erˉage∩t: ‖eb〔raW1eI 0i5日11oW:

05erˉage∩t: 木 DiSa11OⅣ: /

以上是robots.txt的一些常见写法° ●爬虫名称

大家可能会疑惑’爬虫名是从哪儿来的?为什么叫这个名?其实爬虫是有固定名字的’例如百度 的爬虫就叫作BalduSpjder°表2ˉl列出了_些常见搜索爬虫的名称及对应的网站。 表2ˉ↑ -些常见搜索爬虫的名称及其对应的网站 爬虫名称

网站名称

BaiduSpider

百度

Googlebot

谷歌

360Spidcr

360搜索

YOdaoBot

有道

1aarchiver

A|exa

Scooter

altavlsta

Bingbot

必应



●robotpar5er

了解Robots协议之后’就可以使用robotpar5er模块来解析robots.txt文件了°该模块提供了-个 类Robot「i1epar5er’它可以根据某网站的robots.m文件判断一个爬取爬虫是否有权限爬取这个网页°

该类用起来非常简单’只需要在构造方法里传人robots.txt文件的链接即可°首先看一下它的声明: ur11jb。robotpar5eI.βobot「i1epar5er(ur1=m)

当然’也可以不在声明时传人robots.txt文件的链接,就让其默认为空,最后再使用5et=ur1()方

||

第2章基本库的使用

46

法设置一下也可以.

下面列出了Robot「j1epar5er类的几个常用方法。

□5etur1:用来设置robotstxt文件的链接°如果在创建Robot「i1epar5er对象时传人了链接’ 就不需要使用这个方法设置了。 q

(|

□read:读取robots.txt文件并进行分析。注意’这个方法执行读取和分析操作’如果不调用这 个方法’接下来的判断都会为「a15e’所以-定记得调用这个方法°这个方法虽不会返回任何 内容,但是执行了读取操作°

□par5e:用来解析robots.txt文件’传人其中的参数是robo饵txt文件中某些行的内容’它会按照 robots.txt的语法规则来分析这些内容。

□〔a∩十et〔∩:该方法有两个参数,第—个是05erˉ∧ge∩t,第二个是要抓取的URL°返回结果是丁rue 或「a15e,表示05erˉ∧ge∩t指示的搜索引擎是否可以抓取这个URL°





□"tme:返回上次抓取和分析robots.txt文件的时间,这对于长时间分析和抓取robots.txt文件的

搜索爬虫很有必要’你可能需要定期检查以抓取最新的robotstxt文件。 □∏|odi+1ed:它同样对长时间分析和抓取的搜索爬虫很有帮助,可以将当前时间设置为上次抓取

q

和分析robots.txt文件的时间。

千ro∩0 ur111b°Iobotpar5erjⅧportRobot「i1epar5er

这里以百度为例’首先创建了_个Robot「11epar5er对象rp’然后通过5etuI1方法设置了

(∩‖‖‖

rp≡Robot「j1ep己r5eI() Ip.5et一ur1(‖∩ttp5://哪.ba1du.〔o{∏/robot5°txt‖) Ip。Ie日d() prj∩t(rp.〔a∩ˉ千etch(』Bajduspjder‖’ ‖∩ttp5://州.bajdu.〔o∩‖)) prj∩t(rp.〔a∩—+etc打(08a1du5p1der‖’ 』∩ttp5://州.baidu·〔o‖/ho账page/‖)) prj∩t(rp.ca∩—十et〔∩(0Coog1ebot|』 |http5://州·ba1du。〔o∏/∩oⅧepage/‖))

」■■‖■■‖□■∏

下面我们用实例来看_下:

robots.txt文件的链接°当然’要是不用5etUr1方法’可以在声明对象时直接用如下方法设置:

接着利用〔a∩十etC∩方法判断了网页是否可以被抓取。

丁rue

『rue

「a15e

可以看到,这里我们利用Baiduspjder可以抓取百度的首页以及homepage页面,但是Googlebot就 不能抓取homepage页面° 打开百度的robots.txt文件’可以看到如下信息: 05erˉa8e∩t: 8aidu5pider 0j5a11o‖; /ba1du

当■■□】■■■』■■■·司|二■■■■■■■‖』·{

运行结果如下:

‖ {

Ip≡Robot「j1ep己r5eI(‖http5://w朋·bajd0·〔咖/robots·txt0)



2.2

『|)|β



Dj5己11o佣

/5?

0iS己11O训

/u1j∩促?

0j5a11OW

/1j∩仪?

Djs己11ow

/ho∩记/∩ew5/d日ta/

requests的使用

47

DiSa11o旧 /bb 》 ■ )





l」serˉ己ge∩t: Coo81ebot 0iSa11ow: /baidu

0j5a11ow: /5?

0i5a11oW: /5bj+e∩/

「 | ′

0iSa11OW: /∩O∏冶page/ 0i5a11O": /〔pIO Disa11ow: /u1i∩k? Di5a11叫: /1i∩低?

0is己11ow8 /∩o爬/∩印5/data/

·「|■■|■∏「||巴厂||■【■■厂||匹■『[■■∩|

D15己11o切: /b‖

不难看出’百度的robotstxt文件没有限制Bajduspjder对百度homepage页面的抓取’限制了 Googlebot对homepage页面的抓取°

这里同样可以使用parse方法执行对robots.txt文件的读取和分析,实例如下: 十I咖ur11jb。reque5tiⅧportur1oPe∩ +roⅧur11ib·robotp己r5erj‖portpobot「11epar5er

Ip=【obot「j1epar5er()

Ip.par5e(ur1ope∩(0http5://‖‖wN.bajdu.coWrobots·t》(t,).Iead().de〔ode(|ut+ˉ8』〉.5p11t(|\∩`)) prj∩t(rp.〔a∩+et〔‖(|8ajdu5pideI‖’ !http5://0哪。baidu.〔o"0))

p【j∏t(rp.〔a∩千etch(|Bajdusp1der|’ ‖http5://‖w0w.baidu.〔o‖/ho爬pa8e/!)) pri∩t(rp.〔a∩+et〔∩(′Coog1ebot0 」 ,http5://0仙‖‖‖°baidu.〔咖/ho眶page/0))

运行结果是-样的: 丁rue 丁rue

「a15e

本节介绍了robotpar5er模块的基本用法和实例,利用此模块,我们可以方便地判断哪些页面能 抓取、哪些页面不能° 5.总结

本节内容比较多’我们介绍了ur‖lib库的reque5t、error、par5e` Iobotpar5eI模块的基本用法’ 这些是一些基础模块’有_些模块的实用性还是很强的’例如我们可以利用parse模块来进行URL的 各种处理’还是很方便的°

本节代码参见: https://github.com/Python3WebSpider/UrllibT℃st°

2.2 「equests的使用 2.1节我们了解了urllib库的基本用法’其中确实有不方便的地方,例如处理网页验证和Cookie时’ 需要写0pe∩er类和‖a∩d1er类来处理。另外实现POST`PUT等请求时的写法也不太方便° 为了更加方便地实现这些操作’产生了更为强大的库—requestS°有了它’Cookie、登录验证、 代理设置等操作都不是事儿°

接下来,让我们领略一下requestS库的强大之处吧° ↑.准备工作

在开始学习之前’请确保已经正确安装好requests库,如果尚未安装’可以使用pjp3来安装: piP3j∩5ta11Ieque5t5

厂〔

2



{ 第2章基本库的使用

更加详细的安装说明可以参考https://semp.scrapecenter/requests。 2.实例弓|入

urllib库中的ur1ope∩方法实际上是以GET方式请求网页’requests库中相应的方法就是get方法’ 是不是感觉表意更百接一些?下面通过实例来看_下: 1川portreque5t5

r=Ieq0e5t5。get(0http5://wⅧ.bajdu.〔oⅧ/! ) pri∩t(type(r)) prj∩t(r.5t3tu5-〔ode) prj∩t(type(r°text)) prj∩t(r.teXt[:100]) prj∩t(r.〔OO低ie5)

<c1己55 ‖reque5t5°『∏ode15·Re5po∩se〉 200

〈〔1a55 |5tr|〉 〈|肛ⅣP[∩tⅧ1〉

<!-5丁∧丁U50Ⅶˉˉ〉<ht‖1〉〈∩ead)〈们et日httpˉequjγ=〔o∩te∩tˉtγpe〔o∩te∩t=text/htⅧ1;ch己r5e 〈佣eque5t5〔ook1e〕ar[〈〔oo代jeB00RZ=27315十or 。ba1dl」.co‖/〉]〉

这里我们调用get方法实现了与ur1Ope∩方法相同的操作’返回_个【e5pO∩5e对象,并将其存放 在变量r中’然后分别输出了响应的类型、状态码’响应体的类型`内容,以及Cookie。

观察运行结果可以发现’返回的响应类型是req‖e5ts.Ⅶode15.Re5po∩5e’响应体的类型是字符串 5tr’Cookje的类型是【eq0e5t5〔oo代je〕aI。

使用get方法成功实现-个GET请求算不了什么’requests库更方便之处在于其他请求类型依然 可以用—句话完成’实例如下: iⅧportreque5t5

r=reque5t5·get(|http5://州.bttpbj∩。org/get‖) r=reque5t5.po5t(0http5://0‖川w°httpbj∩.org/po5t′)

r=reque5t5.put(‖http5://Ⅳ‖‖γ.httpb1∩.org/put‖) I=reque5t5.de1ete(|http5;//www.∩ttpbj∩.org/de1ete↑) r=reque5t5。pat〔∩〈0∩ttp5://www.bttpbj∩.org/p己t〔h『 )

其实这只是冰山-角’更多的还在后面° 3.G巨丁请求

HTTP中最常见的请求之—就是GET请求’首先来详细了解~下利用requests库构建GET请求 的方法。 ●基本实例

下面构建一个最简单的GET请求’请求的链接为https:〃wwwh仗pbinoIg/get’该网站会判断客户 端发起的是否为GET请求’如果是’那么它将返回相应的请求信息: j∏portIeque5t5

I=reque5t5.get(‖http5://‖γww°httpbj∩.org/get|) prj∩t(I。text)

运行结果如下:

」■‖‖·。□、|□∏]』』■‖|』勺]』』·‖勺||■■■Ⅵ‖』■】□】‖‖可‖』‖‖■■■日】‖■■』‖』■〗】Ⅵ』□|‖‖|·】」』〗■■

这里分别用po5t、put、de1ete等方法实现了POST、PUT、DELETE等请求。是不是比urllib库 简单太多了?

=■■〗凸■ˉ■|』■∏』■〗|』■■■■】‖‖‖{』■】]|·■■』■」■■■||』】■■〗‖|」】々|』】■■‖」■■{』‖‖乙■|||■=■||■■

运行结果如下:

|·』‖|』■可‖‖』■■■■(‖』■■‖』】‖]□可』·‖‖』■】‖|■·■

48

49



●■

]『」

厂(儿

β





Ⅲ 己

"headeI5": { "∧〔〔ept0|目 "*/*"』 ||∧c〔eptˉ[∩〔odj∩g"; 00gzip’ de十1ate"’ "‖O5t"; "洲W。httpb1∩.Org"’ "05erˉAge∩t": ||pytho∩ˉrequest5/2.22·O"’ "Xˉ枷Z∩-丁mCeˉId": "Root=1ˉ5e6e3日2eˉ6b1己28288d721C9e』25日』62a"

}’ "Or1gi∩": "17°20·2〕3.237"’ "ur1"; "http5://www.bttpb1∩。org/get||



可以发现’我们成功发起了GET请求’返回结果中包含请求头`URL、IP等信息。

那么’对于GET请求’如果要附加额外的信息’_般怎样添加呢?例如现在想添加两个参数∩a川e

和age’其中∩a"e是ger∏ey` age是25’于是URL就可以写成如下内容: http5;//州。httpbj∩。org/8et?∩a爬=8emey8age=Ⅱ5

要构造这个请求链接’是不是要直接写成这样呢? r=reque5t5.8et(‖∩ttp5://w洲·‖ttpb1∩.org/get?∩a们e=gemeγ8a8e=250 )

这样也可以’但是看起来有点不人性化哎?这些参数还需要我们手动去拼接’实现起来着实不 优雅°

_般情况下’我们利用para‖S参数就可以直接传递这种信息了’实例如下: 加portreque5t5

d日ta≡{ 0∩a『∩e0 ; ]ger爬y! ’ |a8e|: 25

I=req0e5t5.8et(‖http5://w‖州.httpbi∩。or8/8et|’ par己Ⅶ5≡data) prj∩t(r。text)

运行结果如下: { 00

arg5!0 : { "age": "25"』 ∩a爬: gerⅧey" 00

00



}’ "∩e日der5": {

"∧C〔ept": "*/*"』

"∧〔〔eptˉ[∩〔odi∩g": "gⅢip’ de十1ate"’ "‖O5t||: |0硼Whttpbi∩。Org"’

"05erˉ∧ge∩t"; "Pγt∩O∩ˉreql』e5tS/2°10·000 }」

"Orjgi∩": 00122°4·215°33"’

"‖r1": "∩ttp5://wM∩′·∩ttpbi∩·or8/get?age=228∩a们e=gemeγ"



上面我们把URL参数以字典的形式传给get方法的pamⅦ5参数’通过返回信息我们可以判断’ 请求的链接自动被构造成了https://wwwht印binorg/get?age=22&name=germey’这样我们就不用自己构 造URL了’非常方便。

另外’网页的返回类型虽然是5tr类型,但是它很特殊,是JSON格式的°所以’如果想直接解 析返回结果’得到_个JSON格式的数据’可以直接调用j5o∩方法°实例如下: mportreque5t5

r≡reque5t5.get(|∩ttp5://wb‖w巾ttpbi∩.or8/get{) prj∩t(type(r。teXt))

「旧~



2.2 requests的使用

50

第2章基本库的伎用 pri∩t(r.j5o∩()) pri∩t(type(r·j5o∩()))

运行结果如下: 〈C1a5505tI‖〉

{‖∩eader50 : {‖∧〔〔ept_【∩〔od1∩g! : ’gzip′ de十1ate′』 ‖∧〔〔ept』: ‖*/*‖ ′ !‖o5t! 8 0www.httpb1∩.oI80′ ‖05eIˉAge∩t! : Wtho∩ˉIeque5t5/2.10。0』}’|ur1′: 』∩ttp://咖.httpbi∩。org/get! ’ |arg5‖: {}’ 0or1g1∩′ 8 0182.33.248.131‖} 〈〔1a55 ‖dj〔t′>

可以发现’调用j5o∩方法可以将返回结果(JSON格式的字符串)转化为字典。

●抓取网页

上面的请求链接返回的是JSON格式的字符串’那么如果请求普通的网页,就肯定能获得相应的

内容了°我们以一个实例页面https://ssrl.scrap◎center/作为演示’往里面加人一点提取信息的逻辑’ 将代码完善成如下的样子: j们portreque5t5 i‖pOrtre

□■『‖·■』‖』■■Ⅱ∏|{■■」■〗‖』■‖|||■■可(

但需要注意的是,如果返回结果不是JSON格式’就会出现解析错误’抛出j5o∩.decoder. 〕5O‖0e〔ode[rroI异常°

■■■■■■[■【■■■■■【■■■■■■尸■Ⅲ■■■■■【■■■■■■卜■■■■■『■■■』■∏‖‖□■■|』】■■■]|』■



r=Ieq0e5t5.get( 0http5://55I1.5crape.〔e∩teI/!) patter∩=Ie。〔o刚pi1e({<h2.*?〉(.*P〉〈/h2〉‖’ re.5) tjt1e5=re.于1∩da11(p日tter∩’ I·text) Pri∩t(t1t1e5)

详细介绍,这里其只作为实例来配合讲解° 运行结果如下:

[|肖中兑的救赎ˉ『be5haw5ha∩kRedeⅧptio∩』’霸王别姬ˉ「are"e11‖y〔o∩cubj∩e0 ’ 0暴坦尼克号ˉ丁ita∩ic‖’ 0 罗马假日 ˉ RoⅦa∩‖o1jdaγ‖’ ,这个杀子不太冷ˉ t色o∩‖’ |魂断蓝桥ˉ‖日teI1oo8ridge‖’ 0唐伯虎点秋奋ˉ「1irti∩g 5cho1己r0 ′ ‖兽剧之王ˉ「∩eⅫ∩go+〔o朋edy` ’ |楚门的世界_「he『n」‖a∩5∩ow0 ’ 0活着ˉ丁o[1γe』]

我们发现’这里成功提取出了所有电影标题’只需—个最基本的抓取和提取流程就完成了。 ●抓取二迸制数据

在上面的例子中,我们抓取的是网站的_个页面’实际上它返回的是_个HTML文档。要是想抓 取图片`音频`视频等文件,应该怎么办呢?

图片、音频、视频这些文件本质上都是由二进制码组成的’由于有特定的保存格式和对应的解析 方式’我们才可以看到这些形形色色的多媒体。所以’要想抓取它们,就必须拿到它们的二进制数据° 下面以示例网站的站点图标为例来看—下: 加POrtreque5t5

r=Ieque5t5.get(‖‖ttp5:// 5〔rape.〔e∩teI/十aγi〔o∩.1〔o0) pr1∩t(r。text) pri∩t(r.CO∩te∩t)

这里抓取的内容是站点图标’也就是测览器中每—个标签上显示的小图标,如图2ˉ3所示° 询…

●;ˉ





ˉ

盂≡飘输哺尝=尝=≡_

一镭ˉ~铀

纷回『…"jc。(32×32)

.|

+}

×



÷令G

■Sc『a“.鳃∏te袱aγ沁◎例·『m,. , 尸



≡=≈

图2ˉ3标签上的站点图标



舔丙…



□‖‖』日‖■■司■■Ⅵ|』‖|二■‖‖‖|■■司|||」■■引|||』■■‖』‖|」□』■■‖|‖|凹■■|‖‖{‖』■■■Ⅷ{|』□■‖‖‖‖‖』■■■‖‖|||■■|』‖‖‖■■〗■·|』■■】■】□‖|■■口■Ⅱ□■□〗‖』■■■■Ⅵ

这个例子中,我们用最基础的正则表达式来匹配所有的标题内容°关于正则表达式’会在2.3节

22 requests的使用

5l

上述实例将会打印Re5po∩5e对象的两个属性’—个是text,另一个是〔o∩te∩t° teXt和r.Co∩te∩t的结果。 】…

………

●鳞镰

>≥

〖=■■■■●「「|匡=■『『β『‖『【■ ∏‖■■‖‖|凸■■∏|

℃一

〕互 行结果如图2ˉ4和图2ˉ5所示’分别是I

f j ,) ≥>广宙庐e网ue5tS.get(,http5:〃瓢巾pe°官即捶了/「αviCo凡让触叮)

>>F宙厂

≥>p下i∩t ≥>D下t∩t(厂。k曙X七) 噎(馋

〖■■口■「‖□『|‖■■『|

脓捣枷?∏冈Ⅳ7瓜冈Ⅳ?J[冈Ⅳ?”瓣”脓n冈孵?D冈狮川∏柳厕日滞了腮撇甄硒?n喇耽砌咖哟胁幽?抒瓣y汀 愉′斑月N′∏阅W蛔冈洲/川冈W小爵W′厕N臃′n阀瓣′∏冈拥m∏W枷只w聊舞Ⅳf′赋W′∩岗W′刃阴wf川甄厂贷蛔秘阅馒f河舞 W7Jl躺?E痴W?网毗?贝叫7∩∩洲?几禽Wγ川腑?∩冈Ⅷ职雕∩∩懈?川酣?贝撇?贝舜窗?巩潞榔?∩砌?厕肉柳川枷加阅鞭庇栅?犹舞 7Jl腑?R痴W?网毗?贝冈W7′0∩孵J‖禽W7川撇?∩冈Ⅷ职雕∩R洲?∏酣?贝闭嘴?贝舜窗?巩潞懈?∩啊?厕肉w?川枷加阅鞭腮嘶?犹

司』■■■=

W′川伺Ⅳ?列冈w劝说W?沉引栅7厕只W?网只资?贝厨孵扔阔盟?腺例"?Ⅳ枷劝倪门?n咖肋硼?脚只孵舰喇?贝喇7例蝴?厕阅鞭?倔撇?"珊 W?J『只"?川月孵∩冈W?∏月w了川硼?∏贝W′瓜倒狮川洲》Jl例"?∏只x鹅jt舞Z8几蛆即挽鳃陇痴鹏几呕B』冈z助砸砌呕触呕即斜 2B川呕勋只配次呕8腮只28∩∩乙8n冈聪Ⅺ躯枷乳∑勋∩酿庇呕助蛔即∩2砌倔其撵瓣狮?冈贯"?∩∩豫7拥撇?刃痴脚?况撇了闷衡 阔义慈 洲7刀阔瓣鳃职γ腻冈J1酮xZ低冈Ⅸ酗日限Ⅸ冰趾冰耶罚跟氮冰趾冈Ⅸ歇只眶N窝醒低毗歇阅诞2贝冰2蛔哩脆哪∑诞冰趾阀

揖R2 限顺胆倔日乳2漓耶2X只J1晦毗:掷伺x豫E只谢?′l啪7职狮厕只膊7川测m晚X枷蝴;蛔铲]Q即呛负…彝×y冈,《y例》《”

■■∏‖‖

〉《”,《y阅’喝”》‘yR〉《γ冈》喝y知《y阑》噶y骑》≤y珠’§y日》〈y询》‘”》《”》阉y触恿y院陶只p‘中冈沪]o贷R水闽躁′γ藏 W7只洲?佣兜狮啊"?几∩乙8瓜∩R2诞凤…月阎冈”b怜”冈职朋””酮职酮鸦凝酮疵禽”兜斑阂”讶氮R只””彝冈凤腮”冈

№俘【■】》『|

闽蛔例职职曰”厨酮蛔氮R只只”冤日只”篱°h宾湃蛔调蕊碱噬y冈X鱼填呕B腮W?贿Ⅷ阅獭?n洲?川毗m滩2m`渺只曰W” b咖同协b冈冈@h阂只bb冈∩bn月别b睡历冈bh历绸b心晓闪h俭冈贷防h脚例bb筑兜bb嚼冈bb具篱b脉阎阅h■狗篱b@伪踊心b只升@b月狗鸯扫瞬蔚…圆

巳■尸||

酮∩只册庙y例眨腻曰Z8川脚消?几∩W?贝R"?贝贫W甄呕勋职2酮对by日只脚冈彝bh彝毋臃键脚倒钥R腮……冈…铸……魏闻



恤幻凰『鹏翘月筑呻冈只呻偶JⅪ咖贝…例码呻属′…冈只咖冈职只””蛔bb”””》《”哩炽Z即负咖瓜撇浙阀尊?腮湖?冈瓣 ∑酗只呕抵窝磁$y冈判只说简Ob例贷药臼侧只只冈例∩窝月筑窥只铜jv瞄冈僻踌踢闸痴间舟冈曰罚掷阅厕虏凤贸儡傀痢阑氏舞测肉铡只舞筒舞舞屏仍冈间只抒冈融虏舞饥溺纂

}■■『‖『■■『|

冈窝尸”””冈”bb”∩贝职》《”N2蛔聪∩酬7Ⅳ日脓∏∩"?巩蝴?闪鼻鳃贝只陋髓…舞酮”心甲…冈””月G…T贫

t=∩间{s阀冈乙}∩施f∩吨f门饲乙f∏只Zf∏冈乏f∩躯f∩月Z↑∩日{9∩毗ˉo沁…y测岗”’酗舞田撂b”””》慢《”嫩悦赋脓阅 W?只喇?列酗?阅洲了凤冈Z8蛔呕x例眺y”闪侥月如协”m别冈同”月t、O砸≡凝叫8髓冈髓诞湃船筷阀06抵沁6低…促箕幌蛔 船Ⅸf‖0βⅨ冈q;低∩噎,”t=O贝”例只酗”◎哲只掷”冈∏》〈y冈Ⅸ2陡呕枷jl懈n枷?几寅懈?凤舶?胆2勋躯2槐吮触妙”瓣” °·倒屑酗”只”闽{h门叫8低∩江∏只丫蛔Rv8删洱[酗阀[m贯[财∩[m悦[酗阅[…]触盯钡R}j肪冈只″冈咖痴舱胁翔

只”∩》《y躯趾∩汕D同w?川啪?川R赃历毗?"呕勋毗2枫凤叫by蛔冈曰渭hb贷…判酮”呕勋…促们γ″倒哩鹏丫哑翻 【■■「

|》【■『「

图2ˉ4

r.teXt的运行结果 …

◆攀瓣



≥>>p沪i∩t(庐.CO"te∩t) b0\民硼\x硼\×01\×腮\X01\H硼\X硼\x鹏\xO1\又锄\×硼\x饿息\x姆\x锄m酗\x16\烫锄\…姆 1\H硼\X硼\x$忽\xO1\又锄\×硼\x饿息\x”\x馋\x锄\x16\x锄飞…x ”\x″\泌趴X棚0\×蜘\x61\x鹏\x锄\x硼`x硼\x鳃\又锄\x酗\xi趴翼 0(\x00\x硼\x鳃\×锄\x鳃\x″\×”\X硼\x岭\x$1\义锄\x鳃\x硼`烫瞬\其鳃\Ⅲ铡\娜\m慈\×穆 0\X腑\x1鳖\x锄\x瞄\N06\x1Z\×沁\x锄\x硼\又腮\×硼\X鳃\×蜘\x蹿\X腮\x″…?\刃噬憋m瘫罚

}卜|卜



?\xeb\x仟"?\xeb\又f舶7\×eb\xf郴?\义俘℃\又仟∏7\×eb\xf腑?\×酗\x乍腑?\xeh\xf侧?\x曲取f腑 〈\汉e ?\xe eb\xfⅧ?\又e ?\又e 『W?\x ?\xeb\xf捌?\又eb\x萨侧?\xeb\义『Ⅷ?\x唱b\x仔伪?\×Gb\义ffW?\xeD\又f棚?沤僧趴x炉们γ\x酶\x仟嚼 ’\xG ↑炉(x γ\XGb\x仟栅?\义eb\又f↑瓣7\xeb\×↑础?\xeb\x仟W?\xeb\x仟懈\N酗\X『俐?\又eb\x仟饼7\X蜘\xf钢 eb\×仟"?\义e ?\xG归\x仟W?\x№\义f栅?\xeb\又仟洲?\x名b\又f删了\义巴b\xffw\x酗\x?栅?\醒b\义仟w\x鲍\献瓣 Gb\x仔W?\xe ?\义eb\义仟淌?\又eb\义f峨?\xeb\x痒WF\xPh\x仔W?\xeb\x′fw\义枷\x「删了\x鲍\x『础?\x酶狱萨侧 eb\x仟馏?\又e b\x尸∏

?\xeb\x『佃’\X畦打\x仔w?\xPb\X『侧′\义eb\x仔W?\XGb\NfⅧ7m枷飞x?栅?\又警b\XffW?\x蜘\x萨捌 又eb\Xf eb\x仔咨’\X畦 b\尸仔w

7\xeb\又仟W?\又Pb\x「fW?\Xeb\×仟珊7\讽G膨\x『锨7\xeb\x汗慨\x锄\义f锄?\Neb\xf侧?\x酗\又f懈 xeb\又『 ?\xeo\x行雕\xGD\X仔W7\又eb\x″W?\x总b\X仟W′xeb\X仔孵?\x蜘\虱?删?\xeb\Xffw\xe簿\浙侧 o\x仟脚了\xGD\X

■厂‖‖|)‖■■『‖|止■口‖巴尸}|}|■厂|

?\xeb\xf↑W?\又eb\x汗愉′\xcb\x仟聪\又e豫\x仔巫\又eb\x「「鹏\x咖\蕊f陋9\xeb\汉汗鹏\x酶\x仟Ⅱ b\xf↑W?\又eb\x \风eb\ 8`xe扫\义仟巫\xeb\×仟m\xeb\踊『↑.酗\风eb\贝汗狙\Xeb\Xf亿β\x蜘\x仟酗\xeb\X仟Z8\又eb\x仟霞 扫\义仟巫\xeb\× b\又仔邓\X攫b\x 8\xPb\只f缸8\X攫b\X「↑狙\xPb\xf〃$\x僵b\xf「鳃\x蹭b\x仟鹏\×酶\x仟犯\xeb\x仟酶\又eb\x仟矗 XG B\Xeb\x汗鹏\义瞻口\×「f呻.`X哈b\洱『Ⅷγ\xGb xG俭\x B\Xeb\x汗鹏\义瞻口\×「f呻.`X哈b\洱『Ⅷγ\xGb\x『腑?\xeb\X「懈?\×eh\义f侧?\xeb\又ff"?\x鳃\x汗博

〉\x僚b\x仟洲?\xeb\又「《.x融\xCb\x仟鼠】\xeq\x仟ji\又瞪°\x仔泄\又“\xf『唾\×eα\x仟腿`x嗡α\x仔Ⅸ 〉\x僚b\x《栅?\xeb\又「《.x融\xCb\x仟鼠】\xeq 又e幽\x

2\XG刷\x汗N2\xeo\x『汛/\XP□\x√f耀\又eG`贝仟腮\xe口\x仔滩\民藏◎\x仟陀\Xe◎\x仟腿\Xe镶\Xf报 2\X窿α\X汗膛\xe◎\xf汛′\xP◎\x√『跟\虱eα 长z\氮 2`x喧α\x仟鼠2\X舷o\n仟讥之`×色◎\xw胺∑\x攫u 2`x它α\x仟陀\X瞪O\几仟靴\义eo\x「『陶\x渔幽\x′「汽Z\Xeα\X仟促2\×舷儡\x仔R2\Xeo\x仆睡\xe何拟仔熟 x7『 R∑\× 2\x尸α\凤f{J1\M?口\又f‘R:\Xeα\x仟X睁\X启b R\x腆α\义「{〕1\M》口\又f鹏R:\Xeα\x仟X睁\xeb\X仟W?\xeb\x「『讶′\x酗\x汗孵?\xe℃\xf↑W?\x呻u铲硼 &R:\Xeα\又 x仔 『R; ’义中G\xf什》\x忽e\x『「\又9+\x91\又例\x仔\X9α\x蜘\×「3\x↑∩N鳃m肋` °文PG\又 f什}\xee ’飞×e℃\x仟蹈\×eb\×「『R; 7\xeD\x仟N钞\×啥b\×『『Rβ ’xPG\xf《厂]\xee

汗\ x∩\x仟\×9b\xgb\x「〕.`x仔`x‘)b\X8b\x∩ 3.`x仟`x〔)b f`x〔) \X8b\x∩ x∩\x仟\x9h\x8b\x「〕·`x『f鸟X9b\X8b\x门\Ⅸ仟\Xgb\义gb\×f3\x汗\又驰\x肋\又千3\X卡f\x酗\x助\

杆\ 义「3\X仟\x%\x8b\x↑3\又{于邓9b\义8b\R「3 又伺\X仟\x獭b\x8b\x门\Xf「\汉9b\义8b\贝「3\又仟\xqb\x助\X门\x′^x驹\x助\X门\x仟\x鲍u酗\ 3\M「邓9b

Ⅲf3\x仟\义沁\x8b\×f3\Xff\其9b\x8b\×「3\x仔\X驹\又8b\又簿\x仟\x%\X此\×『3\义仟\×gb\呐憋\

图2ˉ5

r.〔o∩te∩t的运行结果

可以注意到’I.text中出现了乱码’r.〔o∩te∩t的前面带有_个b’代表这是bγte5类型的数据°

由于图片是二进制数据,所以前者在打印时会转化为5tr类型’也就是图片直接转化为字符串’理所 当然会出现乱码。

上面的运行结果我们并不能看懂’它实际上是图片的二进制数据°不过没关系,我们将刚才提取 到的信息保存下来就好了’代码如下: 1们portreque5ts

r≡request5·get(′http5://5〔mpe·ce∩ter/十aγ1〔o∩.jco|) w1tbope∩(|千av1co∏.j〔o,』Wb|)35「;

+."I1te(r。〔o∩te∩t)

,『



52

第2章基本库的使用

这里用了oPe∩方法’其第一个参数是文件名称,第二个参数代表以二进制写的形式打开文件’ 可以向文件里写人二进制数据°

上述代码运行结束之后’可以发现在文件夹中出现了名为



{avicon。ico的图标’如图2ˉ6所示。

这样,我们就把二进制数据成功保存成了~张图片,这个

图2ˉ6名为favicon.ico的图标

小图标被我们成功爬取下来了。

同样地’我们也可以用这种方法获取音频和视频文件° ●添加请求头

我们知道’在发起HTTP请求的时候,会有一个请求头RequestHeaders,那么怎么设置这个请求 头呢?

很简单,使用‖eader5参数就可以完成了。 在刚才的实例中’实际上是没有设置请求头信息的,这样的话,某些网站会发现这并不是_个由 正常测览器发起的请求’于是可能会返回异常结果’导致网页抓取失败。

要添加请求头信息’例如这里我们想添加_个05erˉ∧ge∩t字段’就可以这么写: 加POrtreqUe5t5

∩eadeI5={

U5erˉ∧ge∩t|: ‖付ozj11日/5。O(‖日cj∩to5hj I∩te1‖a〔05X10ˉ11ˉ4)∧PP1e‖ebNit/53736 (Ⅸ‖『‖l’ 11keCe〔代o) 〔hro爬/52°O.27』3°116Sa+ari/

537。36!

} r=reque5t5·get(‖http5://55r1.5craPe.〔e∩ter/0’ header5=header5〉 prj∩t(r.teXt)

当然,可以在这个∩eader5参数中添加任意其他字段信息° 4尸OS丁请求

前面我们了解了最基本的GET请求,另外_种比较常见的请求方式是POST。使用requests库实 现POST请求同样非常简单’实例如下: mportreq0e5t5

dat3={0∩a′∏e‖: ‖gemey|’ 0age! : ‖25|} I=reque5t5.post("bttp5://洲训.∩ttpbi∩·oⅢg/post‖』’ d日ta=dat日) prj∩t(r.text)

这里还是请求htms;//wwwhttpbin.oIg/post’该网站可以判断请求是否为POST方式’如果是’就 返回相关的请求信息° 运行结果如下: {

"arg5": {}’ "d日ta": ""’ "+i1e5": {}’ "「Om": { "age": "25"』 ∩a帐:

8emey

"∧〔〔eptˉ[∩codj∩g"; "g∑jp’ de千1己te"’ "〔o∩te∩tˉ[e∩8t∩": ||18"’ "〔o∩te∩tˉ丁ype": 00app1i〔日tjo∩/Xˉ№0Wˉ「Om|ˉur1e∩〔Oded|’



』■■■■〗』■】□□‖□■■】□】勺‖■〗‖∩□∏■』』■】】■■

}’ "header500 : { "A〔〔ept": "*/*』|’

22 requests的使用

53

||‖o5t||: "Ⅶww·httpbi∩·org"’ "05erˉ∧ge∩t": "pγt∩o∩ˉreque5ts/2。22.0"’ "Xˉ∧川z∩ˉ丁I日〔e-Id": !|Root=1ˉ5e6e3b52ˉo十36782e日98o+ce53c8〔6524|| }’ "j5o∩": ∩u11’ )



||origj∩": "17·2O·232.237"’ "l」r1||: "http5;//www·httpb1∩。org/post||



可以发现’我们成功获得了返回结果,其中+om部分就是提交的数据’这证明POST请求成功发 送了。

5ˉ晌应

请求发送后’自然会得到响应。在上面的实例中’我们使用text和〔O门te∩t获取了响应的内容。 此外’还有很多属性和方法可以用来获取其他信息’例如状态码`响应头`Cookje等。实例如下: j川portIeql』e5t5







r=Ieque5ts°get(|∩ttP5://55r1.5〔rape·〔e∩ter/0) prj∩t(tγpe(r.5t日tu5〔ode)’ r。5tatu5code) prj∩t(tγpe(r.∩eader5)′ r。们eader5) Pri∩t(tγPe(I。〔OO代ie5)’ r.〔OO促je5) pr1∩t(type(I.ur1)』 I。uI1) pIj∩t(type(r.hi5tory)》 r.bi5tory)

这里通过5tatu5〔ode属性得到状态码`通过beadeI5属性得到响应头`通过〔ook1e5属性得到

Cookje、通过ur1属性得到URL、通过‖j5torγ属性得到请求历史。并将得到的这些信息分别打印 出来° 运行结果如下:

|●「‖‖■ˉ■【【‖『■『「}【尸‖|‖[■厂||}卜【【『【■■‖‖|■『‖‖〖■【『〖■尸‖‖‖巴■「‖▲厅「‖‖

〈〔1a5s |i∩t『〉2OO

〈〔1a55 ‖req仙e5t5°5tIu〔tl』IeS.〔a5eI∩5e∩51t1γe0jCt′〉{|5erγer0 : 』∩g1∩x/1.17.8′’ !0ate|: 05at’ 30"己y2020 16:56840C‖丁|’ !〔o∩te∩tˉ「γpe! : |text/∩tⅦ1id]ar5et=ut千ˉ80 ’ `丁ra∩5「erˉ[∩〔od1∩g|: ‖c∩u∩促ed! ’ ′〔o∩∩e〔t1o∩‖ ; {代eepˉ311ve{ ’ ‖γary0 : !∧〔〔eptˉ[∩〔od1∩g! ’ ‖Xˉ「ra∏eˉ0ptio∩s0 : D[‖γ,’ |Xˉ〔o∩te∩tˉ『ypeˉ0ptio∩5 : ∩o5∩j仟` ’ 0

0

』5tI1〔t≡『ra∩sportˉ5e〔‖I1ty! : |"axˉage=157248oOj 1∩c1ude50bDo"a1∩5‖」 ′〔o∩te∩tˉ[∩codi∩g| : ‖gxip‖}

〈c1a5s reque5t5.coo{〈1e5.Reque5t5〔oo|〈je〕日r0〉〈Requests〔oo|(ie〕ar[]〉 〈〔1a55 |5tr′〉http5;//55r1·5Cmpe。Ce∩ter/ 〈〔1日55 ‖115t|〉[]

可以看到’‖eader5和〔oo代je5这两个属性得到的结果分别是〔己5eI∩5e∩sjt1γe01〔t和日eque5t5ˉ 〔OO促je〕ar对象°

由第l章我们知道’状态码是用来表示响应状态的’例如200代表我们得到的响应是没问题的, 上面例子输出的状态码正好也是200,所以我们可以通过判断这个数字知道爬虫爬取成功了°

requests库还提供了一个内置的状态码查询对象reque5t5.code5’用法实例如下: iⅧPOrtIeqUe5t5

r=reque5t5.get(0http5://55r1.s〔mpe.ce∩ter/‖〉

ex1t() j「∩otr.5tatu5code≡reque5t5.〔ode5.oke15eprj∩t(|Reque5t5u〔〔e55千u11y′)

这甲通过比较返回码和内置的表示成功的状态码’来保证请求是否得到了正常响应’如果是’就 输出请求成功的消息’否则程序终止运行’这里我们用reque5t5.code5.o旧得到的成功状态码是200。

这样我们就不需要再在程序里写状态码对应的数字了’用字符串表示状态码会显得更加直观° 当然,肯定不能只有O促这一个条件码° 下面列出了返回码和相应的查询条件:

54

第2章基本库的使用

#仿总性状态码

1卯: (0〔O∩ti∩0e! ’),

1O1: (‖5wjt〔‖i∩g-PrOtO〔O15‖’)’ 1O∑: (‖proce55j∩g|’)’ 1O3: (!〔∩eC长pOj∩t0 ,)’

122: (0uIi-too-1o∩g|’ !reque5t-ur1-too=1o∩g!)’ #戌功状态码

2OO: (‖o|(0 ′ ‖o阳y|’ 0a11o|〈0 ’ !a11-okay|’ !a11≡good|’ 0\\o/0 ’ ‖√|)’ 2O1; 〈!〔reated‖’)’ 202: (!ac〔epted’)’

2O3: (0∩o∩authorjtatjγei∩千o,’ !∩o∩authorit日tiγej∩+omatio∩‖)」

2O4: (!∩OˉCO∩te∩t』’)’ 2O5: (`re5et〔o∩te∩t,’‖reset`)’ 2O6: (0partia1co∩te∩t! ’ 0parti己10)’

2O7: (0∩u1ti5tatUS|’ !"u1tiP1e-St日tu5|’ 0∩U1ti5tat10 ’ |们u1tiP1e—Stati!)’ 208: (!a1ready-reported0 ’)’ 226: (0mused0 ’)’ #支定向状态码

3O0: (|们u1t1p1e-Choj〔eS! ’)’

3o1: (|∏℃γedˉpema∩e∩t1y0 ’ {∏]oγed‖’ `\\oˉ0)’ 3O2: ( 0千Ou∩d’)’

3O3: (05eeOt‖er‖’ ‖Other,)′ 3O4: (′∩otⅧd1十ied‖’)’ 305: (‖u5e-Proxy! ’)’ 3O6: (05wjt〔hˉproxy‖’)’

307: (0te∏‖porary-redjre〔t0 ’ 0te∏)porary-∏℃γed|’|te|∏poIary‘〉’

〕08: (0per"a∩e∩tredire〔t』’ ‖re5u爬i∩c咖p1ete0 ’ 0re5u爬|’)’ #丁∏e5e2tobere∏℃γed1∩3.O #客户辅铅误状态码

4oo: (0badrequest‖’b3d′)’ 4O1: (|u∩authOrjZed|’)’

402: (‖pay『∏e∩tˉrequired‖’ ,pay们e∩t0〉’

407: (!proxγ-autbe∩ticatio∩-requ1red|’ !proxγ-aut∩0 ’ |proxγ-authe∩ti〔atjo∩0)’

』08: (』req仙e5tt1爬out0 ’ 0t1爬o0t‖)’ 4O9: (!co∩+1j〔t0 ’)’

410: (‖gO∩e0’)’ 411: (『1e∩gt∩一required! ’)’ 412: (』preco∩d1tjo∩十己i1ed|’ !pre〔o∩djtio∩,)’ q13: (‖Ieque5te∏t1tyˉtoo-1arge‖’)’ 414: (!Ieque5t-urjˉtoo≡1arge! ’)’ 415: (‖u∩5‖pported爬d1aˉtype0 ’ |u∩5upported爬dia0 ’ 』爬di己-type『)’

416: 〈‖reque5tedˉra∩geˉ∩otˉ5atjs+1ab1e0 ’ !reque5tedˉm∩ge0 ’ 0ra∩geˉ∩otˉ5ati5+j日b1e!)’ 417: (‖expe〔tatio∏+ai1ed|’)’ 』』8: (‖jⅦ-a-teapot』’ 』te3pot|’ 0jˉa川ˉa—teapot0)’ 421: (‖‖MSdjIe〔tedˉreque5t|’)’ 422: (0u∩pro〔e553b1ee∩tity‖’ |u∩pro〔es5ab1e))’

||

403: (0+orbidde∩‖’)’ ↓04: (′∩Ot+Ou∩d0 ’ 』ˉoˉ!)’ ▲O5: (‖∏Pt∩od∩ota11o仍ed! ’ 』∩ota11o脆d|)′ 4O6: (0∩ota〔ceptab1e‖’)’

423: (‖1o〔Ⅶed0’)’

424: (‖十aj1edˉdepe∩de∩〔y0 ’ 0depe∩de∩〔y|)’ 4258 (|u∩ordered〔o11ectio∩0 ’ !u∏ordered‖)’

426; (0upgradeˉIeql』jred! ’| l」pgrade』)’ 428: (‖preco∩ditio∩-required|’ !preco∏djtjo∩‖)’ 』29: (』tooˉ阳∩yˉIeq0e5t5|’ ‖too—川己∩y0〉’ 4〕1: (0‖eader十ie1d5-tooˉ1aIge|’ 0十je1d5-too-1arge‖)’ “4: (‖∩O—re5po∩5e』’ 0∩o∩e|)’ 449: (!retryˉNjtb|’‖retry!)’ 45o: (』b1o〔促ed-by一"j∩dows-paIe∩ta1〔o∩tro150 ’ ‖pare∩ta1co∩tIo15‖)’ 451: (0u∩avai1己b1eˉ+orˉ1ega1ˉrea5o∩5! ’ |1ega1—rea5o∩s0)’ q99: (‖〔1je∩t〔1o5edˉIequeSt|’)’ #服务端铅误状态码 5卯: (0i∩ter∩己15ervereIroI』’ ‖5ervererIor』’ 』/o\\』′ 』X‖)’



} 2.2







} }

) |



requests的使用

55

5o1: (|∩ot一mp1e爬∩ted0 ’)’ 5O2: (bad-g己teway,’)’ 5o3: (05erγ1〔eu∩aγaj1ab1e』’ !u∩aγai1己b1e0)’ 504: (‖gatewayˉti爬out0 ’)’ 5o5: (|httpˉγer5jo∩-∩ot-5‖pported‖》 ‖httpˉγers1o∩0)’ 5O6; (0γaria∩ta15o-∩egotj3te5』’)’ 5O7: (,j∩5U仟iCie∩t5tor己ge! ’)’ 5O9: (ba∩d"jdt∩1jmtexceeded’ 0ba∩d"idt∩0)′ 510: (!∩otexte∩ded,’)’ 511: (‖∩et"or代a(』t‖e∩t1c日tio∩-requ1red0 ’ !∩et"or代aut∩′’ ‖∩et"or代a‖t∩e∩tjc自tio∩|)

例如想判断结果是不是404状态,就可以用reque5t5.code5.∩ot+ou∩d作为内置的状态码做比较°

p

6.高级用法

b

通过本节前面部分’我们已经了解了Iequests库的基本用法,如基本的GET、POST请求以及







冈e5po∩5e对象。本节我们再来了解—些rEqUests库的高级用法’如文件上传、Cookie设置、代理设置等。 ●丈件上传

我们知道使用requests库可以模拟提交-些数据°除此之外’要是有网站需要上传文件,也可以 用它来实现,非常简单,实例如下: mportreque5t5

「11e5={干j1e` : ope∩(干aγico∩·ico0’ !rb!)}

r=Ieque5t5.po5t(!http5;//洲·httpbi∩.or8/post0’千j1es≡+i1e5) pIj∩t(r.text)

在前_节,我们保存了_个文件1aviconjco,这次就用它来模拟文件上传的过程°需要注意’ favicon.ico需要和当前脚本保存在同一目录下。如果手头有其他文件,当然也可以上传这些文件’更 改下代码即可°

运行结果如下: {

·ar85词: {}’ 口data口: 口口’

.十j1e5.:{

冈「j1e.: 阅dat己:app1jcatjo∩/octetˉ5tIea团〗ba5e6q’∧MBMI…园 }’ αfOm口: {}’ .header5口;{ 口肛〔ept■: 圃本/*口’

■A〔〔eptˉ[∩〔odi∏g·: 口gzip’de千1ate■’ .〔o∩te∩tˉle∏gth阐: .666S口’

口〔o∩te∩tˉ「ype圆: 口m1tipart/十omˉdata; bou∩d己rγ=41fc691282〔〔894十8千佣ad己bb24「oS十b国’ .肋巴t口: w“.httpbi∏。org口’

·0seⅢˉAge∩t.自 ·Wtho∩ˉreque5t£/2.2】.0口’

口Xˉ知Z∩ˉ∏m〔eˉId口g 口Rmt=1ˉ5e锤〕C肋ˉ0S切7圃d3a922e3“793e千咽口

}’

.jso∩.: ∩u11’ 口orj8i∩α: ■16·20°232·237口’ ·uⅢ1口8 □httpS://….htt恤j∩·O吧/poSt° }

以上结果省略部分内容,上传文件后,网站会返回响应,响应中包含十i1e5字段和「om字段,而 十om字段是空的,这证明文件上传部分会单独用-个「j1e5字段来标识° ●Cookie设Ⅲ

前面我们使用Ⅲlljb库处理过Cookie’写法比较复杂,有了【℃quests库以后,获取和设置Cookje只 需一步即可完成。



||||

第2章基本库的使用

56

我们先用_个实例看一下获取Cookie的过程:

r=reque5t5.get(0们ttp5://www。ba1du.〔o们0 ) PIj∩t(r.COO长je5〉 +Or低ey′γ己1ue1∩r。COO|〈1eS.1te们5()§ pri∩t(《ey+ 0≡0 +γa1l」e)

运行结果如下:

〈Reque5t5〔oo挝e〕ar[〈〔oo}〈je8O0RZ=27315十oI 。bajdu.coⅧ/〉]〉 8"RZ=27315

这里我们首先调用coo代1e5属性’成功得到Cookje,可以发现它属于Reque5t〔oo底1e〕ar类型°然 后调用ite"5方法将Cookie转化为由元组组成的列表’遍历输出每_个Cookie条目的名称和值,实 现对Cookje的遍历解析°

当然,我们也可以直接用Cookle来维持登录状态°下面以GitHub为例说明_下’首先我们登录 GltHub’然后将请求头中的Cookie内容复制下来,如图2ˉ7所示。 =曰

令十智



==~



α萨癣『◇…

十一

一■…『

≥…■

§

|◎



■占…=≡雹~

P哩《d…$《■ ‖●酗e$M硒配Ⅸ吟cn

厂≡′…羔



…昂々

…÷□



←≡





≡r

■…彤c唾`

Ⅲ≈….

_□ ~_



甲 =h■■凸■-a凸≡…

亭舜葡尸 ■≡





≡…

■~

; . 酗食》●§



△+·[l

正x檄mB

嘱酶钥沈…y



乐鞠 镇罐锣铀珐.贾

‘ 磷

其亡

颧……臭j . 亩













■电幽△企刨哈唾=拘已严

凸尸

乙亡



啥=皿c…·Ⅱ·四…°l5…1『 表…&】·…】曹凹7…1】1‖=肛挞幻吟晃…6蜒…≥肌=』…″=m

……==

『■【【【『∏■■■■■■丙【■■■■=■忘潭+

厂 =

网=i…;………7……】tr啦=1『咆…1乒t1U』t庐〗; =…m』〗ˉ^″9■皿…■皿潭”……c…】 汰t……j…Y≡白…u【…1峭…0…07【S■J…)■Ⅲ……v『〗………=沁 V…c“饱印

Ⅺ御7睡巾凸 『wb…P

》gh砷↑←-〖m却呵唾……戚≡=!叭罢二△

←……萨Q…

呵【_……1………洒==呻m犯回吓『靳〗…=】…mnJ只7…J…y

?1螺…uγ…_卫≡…1佃呻】@Ⅻx仍…龟…回}………$……f汹7e砸 …叭……7ˉ…………「……论 ·则m〗 p〕铀憾■…枢m】m ……恼』…丁_沁雷冠1咖啊心…= p伊……-″…审=l…咖减γ…`〗≡……四ˉM叮哇0 w…`h………………一9啮Ⅱ…挝俩y…Ⅵ…】仲……m…丽■…

酗它尸£…■……韵〗…马翱l_▲丙…归》…〗…………t例… 0口▲←=■■■=_=…7-—……_……m】…守N】…】m=~匪曲~户=▲-… ■



















■ 干























占 丁





































西











































【 ■





碎…0…] 哈 【

倒=…=…

呻尸…rw对…』=…1c·≈』≈y■γ…〗心】m●】…门;吓m→■●T已…唾】



0■|凹〖v■「0hT【q】7订乙】F

≡贴伊盗司|P□■H祈歹5ⅥⅡF0 己吐刀T仓P■PJ■

∩面■P≡0丁q0…】~←b…l户竿…坠■=沾』

鲤「■捶硒ˉ心…Jt■mmr『…伊… 尸…

…、_→=

…p止r吐◎… Q←~■′喊T≈户●甲沁G山哥■■』F十▲■二≡吨私啡■=

图2ˉ7请求头中的Cookje内容

可以将图2ˉ7中框起来的这部分内容替换成你自己的Cookie’将其设置到请求头里面,然后发送 请求’实例如下: 1们POrtIeque5t5

‖e3der5= {

‖〔oo|〈1e0 : ‘ octo=C‖1。1。1849343058·15766o2081j-ga≡6∧1°2°9046o451.15 76602111j ‖o5tˉu5er5es51o∩5a‖e5jte≡∩b0v62代‖‖jp』‖5长yQ‖γZ208waeq 5们‖gx「∩「〔88r∩γ7gⅣOˉj deγi〔e1d=a7Ca73be0e8千1381d1e2ebb5349千9075j

l」5er5es5jo∩=∩b0γ62代‖‖jp▲‖5ⅨyQ‖γ22o8waeq5‖‖g×「∩「〔88r∩γ7g『γQw-j 1oggedˉ1∩≡ye5j dot〔oⅦu5er≡Cemeyj t∑=∧5ja咒2「5‖日∩8∩ai; h己5reCe∩ta〔tiγ1ty=1j ˉBat=1j -g‖-5e55=yO0I-5e551O∩i∩+O‖’ |05erˉ∧ge∩t0 : 0‖ozi11日/5.O (陀〔1∩to5∏】 I∩te1阳〔05X1O114) ∧pp1e‖ebRit/537.36 (R‖丁‖[’ 1让e“c代o)〔∩ro们e/53°α2785.1165a+ar1/537。360 」 }

r=Ieque5t5。get(|http5://gjt∩ub.coⅦ/』」 ∩eader5≡∩eadeI5) pr1∩t(r.text)

运行结果如图2ˉ8所示。

】■】]‖‖(√|』■』■■||』■‖∩‖‖|」■■』■|』□】‖』■■■可‖』‖‖』■】|■〗‖■∏』■】‖{■]{可|』』=■‖|』■』‖√‖」●]‖·‖□】』』曰|』■■』‖■■■】‖』‖〗■■〗|‖□‖■|‖』□】■

— … ◎

_呻…/、_

汪≡_…′…’→~

_~……

中℃帜沪

≈″ 日呻…



■角缉

#″

≡砖、…

田雨

“沽

… 慰 … ■瓣静



…础

蘸◆

鲤●

二:罢罕……/;

=…簇糟晶愚



慰 H趣≈……

当■■■□□‖』■■■】‖』·■|■■□]‖‖‖■■∏』‖|■■勺■■|‖』凹|」】】】□■可‖‖|』■■〗‖|‖‖』■■可」■‖‖」=■】■‖‖』】■■·

mPOrtreque5t5





22





● 蘸

requests的使用

57

—ˉ—-_—雨瑟-5=了一~……鹤…=………龋』

<s…萨yc1°s…砸ˉ四唾铲lmebt厕鲁li∩底textˉ彼叮~“萨kt缸tˉb°lG圃i啦肘ˉ「uIt.tm….助tt恤“mm怎|

<5…7y



C匈[●又t幻 Cαγte又t臼 “tp■″←〔lL〔k■w助■… …刷’cl iC贞0砷颧edo〔〔酗∩tc鄙te义tgWt仑助ePˉ〔…睡t:‖∑●7鲤≥ <1呻喧 <1呵喧l巴S■■·‖酣u七■厂口◎1t●蜘“ 豌“G″响●γ腆



询1dt卜钡酗创罐i胁t■哆驹舜巳庐厄■颧h钱p■;〃口γ哑″晕【。9tt…拙驼沪…仓

∏t0〔咖/M/8678瞒〗p罚…;γ叫臃≥

G∩



≤印α∩c1α巫■o°仁弱ˉ亡广un〔ote色鳃静t厂刨忱〔ct●ˉtO尸9et∩lˉ】≥6●碾γ√Zp枷诊



<”回"c1oZ∑■■d7呻…↑=C□厂et翱≥</雷“∏≥ ≤/已…铲y>



妇■宅αi1$~唾∩Mp7Gl四d〔1·邑∑■"SeleC吨∏u5elP〔…刚ˉ№巴萨iIte7口oPm-l吨eu…■闰Cα0tGXt=田许itch≡t Ⅷ·

t1GˉlαF凶它‖‖ S7追咱脚/如色…尸山□j已x叁唾CO问te几t儿t$t?t凹沪7e∩f-〔吻hext鳃e雨ey御> <dWc1O唾≈,』5ele■t比T〗u←硒d°I0,≥



<∩eα“广Cl□巴二●■“Iect№∩uˉ∏e“GF,0≥

≤母p·∩cm已$■5eleCt比∩Dˉ命1rle口1◎■■c啄仓extˉ$w〖t〔hˉtttleMy矾』t蜘>和0tCh鲍导…mC咖γtext@ ≤母p·刊cm已$■5eIeCt比∩Dˉ命irle■t◎■殉c啄仓extˉ$w〖t〔hˉtttleMy矾』tα>印0tCh“3…园c碱te沉t咱





■“∩> α3邑■BelQCt№『比】辑c〗◎se8u ≤b吐t◎∏clα蹈■笆SelQCk№∩u笋c!°£e8ut飞o" typQ口忌b凹tt娜"“t嘲≈r蛔钞!e空fo7●钟·cc幽』∩t≈写内1t嘱沁萨辫l ≤b吐t谷∏clα蹈■笆Sel·〔k№∩u笋c!°沁8uh飞o"咖t”e凹忌b凹tt娜w蜘t奸r蛔0!e空fo7●钟·cc幽』∩t≈T内1t嘱沁萨雪l盯

仿

创t□><■γg″1□【回bGl■■〔lO巴■″∏q闻〔】o噬■●◎仁tj〔纫↑m仑】co∏ˉx句γi·…迅m颐a01216蛾ve尸£〖呻●1·1胸们idt

惟圃1乙0°hei,ht▲阐16翱ml≈.t呵·><po它∩ftll 尸ule■喊ev■∩OOd垒伞巨"7Q8甘l3沼3。芯.1q8108l69.q8t≡3.P



53.75LA8ˉ1乌8[々ˉ5Z8≡7742吕t1.08ˉ1ˉ48i亿652l]ˉ75ˉlˉ7日1.481“七了.q88Z■/D</巴γ°</u』忙km≥ 3.75LA8ˉ1乌8止々.5Z8.774酶t1·08ˉ1°48t665■l3°75气3窄7吕1°481“仁了.488冰〃创巳v少</酗t…

</‖V僧往‘e庐>





图2ˉ8运行结果

p 》}匹尸

匹■「皿「巳∩| 巴■「【∩|

}尸||■■「】「 匡■『|△尸「|



可以发现’结果中包含了登录后才能包含的结果’其中有我的GjtHub用户名信息,你如果尝试 一下’同样可以得到你的用户信息°

得到这样类似的结果’说明用Cookie成功模拟了登录状态’这样就能爬取登录之后才能看到的 页面了。

当然’也可以通过coo《1e5参数来设置Cookie的信息’这里我们可以构造_个Reque5t5〔oo代je〕日r 对象’然后对刚才复制的Cookie进行处理以及赋值,实例如下: 1"portIeque5t5

coo长je5二 ‖ octo=C‖1.1°184934〕058°1576602081j-ga=CA1.2.9O460451.1576602111j

‖ostˉu5er5e551o∩5a∏论5jte二∩b0v62{〈‖‖jp4‖5M"γZ208waeq5川‖gx「∩「〔88I∩W8丁γ枷ˉj deγ1〔e1d≡a7c日73be促8十1a81d1e2ebb5349十9075j u5er5e55jo∩=∩bDγ62代‖‖j叫5Rγ删γZ2o8"aeq5刷gx「∩『〔88r∩γ7gⅣ咖-〗

1ogged一i∩=ye5j dotcmu5er≡Cer爪ey; tz=A5ia%2「5‖a∩g∩ajj ∩己5rece∩ta〔tiγ1ty=1j-gat二1j 二 jar=req0e5t5.〔oo|《ie5.Reque5t5〔oo{《1e〕aI() ∩eader5={

U5erˉ∧ge∩t! : 0‖ozj11a/5。0(‖a〔j∩tos∩j I∩te1"acO5X1Oˉ11-4)App1e"e冰jt/53736 (Ⅸ‖丁肌’ 1汕eCe〔低o) 〔hro"e/53°O.2785。1165a「己rj/537。36‖



+OI〔OO代1e1门〔oOR1e5.Sp11t(‖; 0):

仪ey’γa1ue=〔oo代1e.5p1jt(!≡|’ 1) j己I.5et(|(ey’ γ日1ue)

r≡Ieque5t5.get(`http5://gjt‖‖b.co川/‖’ coo促je5=jar’ header5≡∩eader5) pr1∩t(Lte×t)

这里我们首先新建了-个Reque5t〔oo促je〕ar对象’然后利用5p1it方法对复制下来的Cookie内 容做分割’接着利用5et方法设置好每个Cookje条目的键名和键值’最后通过调用requests库的get方 法并把RequestCookieJar对象通过〔oo促je5参数传递,最后即可获取登录后的页面° 测试后’发现同样可以正常登录° ●SeSsion维持

直接利用requests库中get或po5t方法的确可以做到模拟网页的请求,但这两种方法实际上相当 干不同的Session,或者说是用两个测览器打开了不同的页面。

设想这样—个场景第_个请求利用肥qUests库的po5t方法登录了某个网站,第二次想获取成功 登录后的自己的个人信息,于是又用了一次requests库的get方法去请求个人信息页面°

~=



第2章基本库的使用

58

这实际相当于打开了两个测览器’是两个完全独立的操作’对应两个完全不相关的Session’那么

有人可能说’在两次请求时设置-样的Cookie不就行了?可以,但这样做显得很烦琐,我们有 更简单的解决方法°

究其原因’解决这个问题的主要方法是维持同-个Session’也就是第二次请求的时候是打开-个 新的测览器选项卡而不是打开一个新的测览器。但是又不想每次都设置Cookie,该怎么办呢?这时候 出现了新的利器—-Session对象°

利用Session对象’我们可以方便地维护—个Session,而且不用担心Cookie的问题,它会自动帮 我们处理好°

我们先做_个小实验吧,如果沿用之前的写法,实例如下: mportreque5t5

||

能够成功获取个人信息吗?当然不能。

Ieque5t5。get(‖http5://….∩ttpbi∩.or8/cooMe5/5et/∩u∩|ber/12〕4567890 ) I=reque5t5.get(‖https://"Ⅷ.httpbj∩.org/coo促1e5!) pri∩t(r.te×t)

这里我们请求了_个测试网址https://wwwhttpbinorg/cookies/set/number/l23456789°请求这个网



址时’设置了—个Cookie条目,名称是number’内容是l23456789。随后又请求了https://wwwhttpbjn. 这样能成功获取设置的Cookle吗?试试看° 运行结果如下: {

"〔OOMe5": {} }

发现并不能°

我们再用刚才所说的Sessjon试试看: mportreql』e5t三

5=reque5tB。5e55jo∩() 5。get(!∩ttp5://…。‖ttpbi∏.org/〔oo代je5/5et/∩u∏巾er/123456789|) r≡5.get(,http5://哪.httpbj∩。or8/〔oo低je5!) pri∏t(r.text)

再看下运行结果: { .〔ookie5口: {·∩u仙er.: ·123456789.} }

可以看到Cookje被成功获取了!这下能体会到同一个Sessjon和不同Sessjon的区别了吧!

所以,利用Session可以做到模拟同_个会话而不用担心Cookie的问题,它通常在模拟登录成功

之后,进行下—步操作时用到。

Sessjon在平常用得非常广泛’可以用于模拟在一个测览器中打开同_站点的不同页面,第l0章 会专门来讲解这部分内容。 ●SSL证书验j正

现在很多网站要求使用HTIPS协议,但是有些网站可能并没有设置好HTTPS证书’或者网站的 HTTPS证书可能并不被CA机构认可,这时这些网站就可能出现SSL证书错误的提示。

|{

org/cookjes,以获取当前的Cookje信息°











2.2

requests的使用

59

例如这个实例网站: https://ssr2scrape.center/’如果用Chrome测览器打开它’则会提示“您的连 接不是私密连接’这样的错误’如图2ˉ9所示°



0 ·「』’■■||尸『『|■′‖广=■』(·‘·‖}●■「)■ˉ』「‖》匹尸|■「‖■■厂‖卜止■「|■「■「▲尸}‖|°口〗‖』■「卜□「β■【》『‖ˉ■伊{■■『‖|止■「仪■「囚}■「β||■厂|’■||■厂|【■「|卜‖■「‖■■∏【■【

「豆



-

您的连接不是私密淬椿 攻资宿可瓣会…从■钢α萨■……■m嫩“■《仍■8■码`迅…四唾妇用卡 值■)·了■谭伯 框7坦喊ˉ…Ⅶ八J丁啊L…四

} 9 瓣乓您■玻搏C帕田…蘸鸦■蜘安金儡护’H开皮起渔翻但炉 {



『=≈Q 念▲守←□←

}出组 j



图2ˉ9错误提示

我们可以在测览器中通过_些设置来忽略证书的验证°

但是如果想用requests库来请求这类网站’又会遇到什么问题呢?我们用代码试_下: j‖portreque5t5

re5po∩5e≡Ieque5t5.8et(|∩ttp5://55r2.s〔rape.〔e∩teI/《) pr1∩t(re5po∩se.5tatu5code)

运行结果如下: reque5t5。e×〔eptjo∩5.55l[rror:‖∏p5〔o∩∩ectio∩poo1(‖o5t≡055r2.5〔rape.ce∩ter′’port=443):‖己xretI1e5exceeded "jthur1: /(〔己u5edby55[[rror(55〔〔ertγeri+j〔atjo∩[rIor(1’`[55[: 〔[盯I「I叭『[γ[RI「γ「∧I[[0]〔eIti{j〔日te γeIj十y+ai1ed: u∩ab1eto8et1oca1i550eIcert1十j〔己te ( 551.〔:1056)|)))

可以看到,直接抛出了55[[rror错误,原因是我们请求的URL的证书是无效的°

那如果我们_定要爬取这个网站,应该怎么做呢?可以使用`′erjfy参数控制是否验证证书,如 果将此参数设置为「a15e’那么在请求时就不会再验证证书是否有效。如果不设置veri十y参数’其默 认值是『Iue,会自动验证° 于是我们改写代码如下: 1‖portreq‖e5t5

re5pO∩5e≡request5。get(!∩ttp5://$5r2。5〔mP巳〔e∩ter/! ’ γeri十y=「a15e) pri∩t(re5po∩se。5tatu5〔ode)

这样就能打印出请求成功的状态码了: /l」5r/1o〔己1/1ib/pyt∩o∩3.7/sjteˉpac代a8es/ur11ib3/〔o∩∩e〔tjo∩poo1·py:857: I∩5ecureReql」est‖ar∩1∩g: 0∩γeri千ied ‖∏P5reque5tisbe1∩g帕de。∧ddi∩g〔eItj十j〔日teγeri「i〔at1o∩j55tro∩g1γadγj5ed° 5ee$ http5://ur11jb3。 readt∩edo〔5°1o/e∩/1ate5t/adγa∩〔edˉusage°∩tⅧ1#551ˉⅣ日r∏i∩85 I∩5eCureReque5t‖ar∩1∩g) 2卯

不过我们发现其中报了_个警告’它建议我们给它指定证书°我们可以通过设置忽略警告的方式 来屏蔽这个警告: 加portreque5t5

十Io爪req‖est5.pa〔低age51∏portur11jb3

60

第2章基本库的使用 ur111b3°di5ab1e—"ar∩i门gs()

re5po∩5e≡reque5t5.8et(|http5://55I2.5cr己Pe。ce∩ter/0 ’ γeri于y=「a15e) prj∩t(re5PO∩5e.5tatU5〔Ode)

或者通过捕获警告到日志的方式忽略警告: mport1oggi∩g mportreque5t5

1og81∩g.〔aPture日ar∩j∩g5(『Iue)

Ie5po∩5e=reque5t5.get(‖∩ttP5://55I2·5craPe。〔e∩ter/0 ′ γeri十y=「a15e) prj∩t(re5po∩5e.5t己tu5code)

当然’我们也可以指定_个本地证书用作客户端证书’这可以是单个文件(包含密钥和证书)或 一个包含两个文件路径的元组: mpOrtreqUe5t5

re5po∩5e≡Ieque5t5°get({∩ttp5://5sr2.5〔rape.ce∩ter/‖’ ceIt=(|/P3th/5erγer.〔rt‖’ ‖/p日t们/5erγer.促eγ‖)) prj∩t(Ie5pO∩5e.5t己t05〔Ode)

当然,上面的代码是演示实例,我们需要有〔rt和促ey文件’并且指定它们的路径。另外注意, 本地私有证书的底eγ必须是解密状态,加密状态的代eγ是不支持的。 ●超时设置

在本机网络状况不好或者服务器网络响应太慢甚至无响应时,我们可能会等待特别久的时间才能 接收到响应,甚至到最后因为接收不到响应而报错°为了防止服务器不能及时响应,应该设置一个超 时时间’如果超过这个时间还没有得到响应就报错。这需要用到t1"eOut参数’其值是从发出请求 到服务器返回响应的时间。实例如下: 1爪portIeque5t5

r≡reql」e5t5.get(0https://w‖w‖°‖ttpbj∩°org/8et! ’ t加eout≡1) prj∩t(r.5tat‖5code)

通过这样的方式,我们可以将超时时间设置为l秒,意味着如果l秒内没有响应’就抛出异常° 实际上,请求分为两个阶段:连接(connect)和读取(read)°

上面设置的t1佩eOut是用作连接和读取的t1"eOut的总和° 如果要分别指定用作连接和读取的t1"eout’则可以传人_个元组: r=reque5t5。get(!http5://www.httpbi∩°org/get』’ t1川eo0t=(5’ 3O))

如果想永久等待’可以直接将t1ⅦeOut设置为‖O∩e’或者不设置直接留空’因为默认取值是‖O∩e° 这样的话,如果服务器还在运行’只是响应特别慢’那就慢慢等吧,它永远不会返回超时错误的。其 用法如下: r=Ieque5t5.get( 0∩ttp5://ww"。∩ttpbj∩。org/get0 ’ t1川eo(」t=‖o∩e〉

或直接不加参数: r=reque5t5°get(』∩ttp5://"ww.∩ttpbj∩.org/get‖)

·身份认证

2.l节我们讲到’在访问启用了基本身份认证的网站时(例如https://ssr3.scrape.center/),首先会 弹出_个认证窗口,如图2ˉl0所示°





2.2

requests的使用

6l

登景 ‖忧pS:〃SwaSc旧pe.C●∩蚀『

用户名 ■▲■



●■

■→■■▲●■甲宇●勺■■■●■



■码 ●▲■■●●

■■●

■●■■■◆■=■●■▲■■亏

图2^l0弹出的认证窗口



这个网站就是启用了基本身份认证’2.l节我们可以利用urlljb库来实现身份的校验,但实现起来

相对烦琐°那在requests库中怎么做呢?当然也有办法°

我们可以使用requests库自带的身份认证功能’通过aut∩参数即可设置’实例如下: mportreque5t5 十r咖reque5t5·authmport‖∏p8a51〔∧ut∩

r≡reque5t5°8et(』http5://55I3。5〔I日pe·〔e∩ter/0 ’ aut∩≡‖∏PBa5i〔∧uth(‖日d‖i∩0′ 0ad爪i∩‖)) prj∩t(I.5tatuscode〉

这个实例网站的用户名和密码都是admin’在这里我们可以直接设置°

如果用户名和密码正确’那么请求时就会自动认证成功;返回200状态码;如果认证失败,则返 回40l状态码°

当然’如果参数都传—个‖∏p8a5j〔∧uth类,就显得有点烦琐了’所以requests库提供了—个更 简单的写法’可以直接传一个元组,它会默认使用‖∏pBa51〔∧ut‖这个类来认证。 |β



所以上面的代码可以直接简写如下: 加portIeque5t5

I=request5·8et(0http5://55r3.5crape.ce∩ter/0 ’ a‖t们=( 0adm∩|’ 0ad汛i∩‖)) Prj∩t(r.5tatu5-〔ode)

此外’requests库还提供了其他认证方式’如OAuth认证’不过此时需要安装oauth包’安装命 令如下: pjp〕 j∩5ta11reqoe5t5-oauth1jb

使用OAuthl认证的示例方法如下: 加portreque5t5

十ro"reque5t5=oauth11bj∏port0∧uth1

uI1= 0http5://3pj.twjtter。〔咖/1.1/ac〔ou∩t/γer1千y-〔rede∩t1a15.j5o∩0 aut‖=0∧uth1(|γα」∩-App-旺γ‖’ 』γα」R_∧pp-5[〔R[『』’ !05[R0∧0丁‖丁0旺‖! ’US[【0A0了‖「0旺‖S[〔旺丁|) reque5t5.8et(ur1」 auth≡日Mt∩)

●代理设Ⅲ

某些网站在测试的时候请求几次,都能正常获取内容°但是—旦开始大规模爬取’面对大规模且 频繁的请求时’这些网站就可能弹出验证码’或者跳转到登录认证页面’更甚者可能会直接封禁客户 端的IP,导致在-定时间段内无法访问。

那么,为了防止这种情况发生’我们需要设置代理来解决这个问题,这时就需要用到prOxje5参 数°可以用这样的方式设置:





第2章基本库的使用

62

j呻ortreque5t5

Pro×ie5氢{ 0http|: 0http://1O.10。10.1O:1080|』 0http50 : |http://10.1O·1O.1O:108O0 ’ }

req0e5t5.get(0∩ttp5://….∩ttpbi∩.or8/get』’ proxie5=pro×je5)

当然’直接运行这个实例可能不行’因为这个代理可能是无效的’可以直接搜索寻找有效的代理 并替换试验-下°

若代理需要使用上文所述的身份认证,可以使用类似http:〃user:password@host:pon这样的语法来 设置代理,实例如下: 1呻OrtreqUe5t5



|‖

pIoxje5={|http5‖: ‖http://u5eI§p己55word01O.1O.10.1O;1O8O/0 ’} reque5t5.8et(0http58//咖0。∩ttpbi∩.org/get!」 proxje5=prox1es)

除了基本的HTTP代理外’requestS库还支持SOCKS协议的代理。

pip3j∩5ta11 "reque5t5[5o〔促s]"

0

|(

首先,需要安装socks这个库:

然后就可以使用SOCKS协议代理了’实例如下:

Pro×1e5={ 0∩ttp‖ : 05oc伐55://u5eI;pa55灿I峭)o5t:mrt{ ’ 0http50 : ′5ock55://u5er:pa55wor峪ho5t:port{ } reque5t5.8et(‖∩ttps;//州.httpbi∩.org/get|’ prox1e5=proxje5)

●PreparedRequest

我们当然可以直接使用requests库的8et和po5t方法发送请求’但有没有想过’这个请求在 requests内部是怎么实现的呢?

实际上, requests在发送请求的时候,是在内部构造了一个Reque5t对象,并给这个对象赋予了 各种参数,包括ur1、‖eader5、data等’然后直接把这个Reque5t对象发送出去’请求成功后会再得 到一个【e5Po∩5e对象,解析这个对象即可°

那么Reque5t对象是什么类型呢?实际上它就是preparedReque5t。 我们深人-下,不用get方法,直接构造-个preparedReque5t对象来试试,代码如下: +ro‖reql」est5i呻ortReque5t’5essjo∩

uⅢ1= !https://州.∩ttpbi∩·org/po5t’ data={|∩a爬0 : |ger∏论y|} header5={

0‖5erˉ∧8e∩t0 : !№Ⅲj11a/5.0(比〔j∩tos们j I∩te1付己〔05X1oˉ11-』)∧pp1e"eb长jt/5〕7.〕6(氏‖『川’ 1jkeCe〔ko) 〔hrα‖e/53°o.2785·1165a千己ri/537。360

} 5≡5e55jo∩〈)

■·‖||』■‖』】■可〗‖‖』勺]‖|■■||‖』|‖·‖‖|·】|‖』·‖‖|」‖‖‖』□】‖‖‖·】]‖《‖‖|」■■]]■〗‖‖」■∏||■∏』勺||』■

j呻ortreque5t5

req=Reql」e5t(!p05丫‖’ l」r1’ d己t日=d日ta’ 门ea“r5≡∩eader5) prepped=5.prepare-reque5t(req) r=5.5e∩d(Prepped〉 prj∩t(r.te×t)

□]』□』□‖』‖■

这里我们引人了Reque5t类,然后用ur1、data和∩eaders参数构造了—个Reque5t对象,这时需 要再调用5e551o∩类的prepareˉreque5t方法将其转换为一个preparedReque5t对象’再调用5e∩d方

司』









23正则表达式

63

法发送,运行结果如下:











「 p

"aIg5阐: {}’ "data": 00"’

"十j1e5": {}’ 』‖fOm": { ∩a眶:

厂|2 k

gemey

}’ "headerS"; { "∧〔〔ept": "*/*"’ 0o∧〔ceptˉ[∩〔odi∩g|↑: 00gzip’ de+1ate"’ "〔O∩te∩tˉle∩gth": 0011,0’ "〔o∩te∩tˉ「ype": "app1i〔atjo∩/×ˉ洲wˉ「omˉur1e∩coded"’ "‖o5t": "卿.∩ttpb1∩.org"’



"05erˉA8e∩t口: "‖ozi11a/5.0(‖a〔1∩tos∩; I∩te1∩a〔05X10114) ∧PP1e咋bⅨjt/S〕7.36(R‖了肌’ 1jkeCe〔代o)〔hro「∏e/5〕·O。2785·116S己+arj/537°36"’ 0!Xˉ顺z∩ˉ『mceˉId": "Root=1ˉ5e5bd6a9ˉ6513〔838千35bo6日o7516o6d8"

p



}’ ”j5o∩00 : ∩u11’

β「「巴β

"origj∩": "167。220·232.237"’ "ur1": "‖ttp://咖w。∩ttpbi∩。or8/po5t" }

可以看到,我们达到了与POST请求同样的效果°

b

) 卜 ) 卜 ) | β ‖

有了Reque5t这个对象’就可以将请求当作独立的对象来看待’这样在一些场景中我们可以直接 操作这个RequeSt对象’更灵活地实现请求的调度和各种操作。 7.总结

本节的requests库的基本用法就介绍到这里了’怎么样?有没有感觉它比urllib库使用起来更为 方便。本节内容需要好好掌握’后文我们会在实战中使用requests库完成—个网站的爬取,顺便巩固 requests库的相关知识° 本节代码参见: https://githuhcom/Python3WebSplder/RequestsTest。

■ ■ 尸

23正则表达式

■「|巴■尸▲■■『

在22节中’我们已经可以用requests库来获取网页的源代码’得到HTML代码°但我们真正想 要的数据是包含在HTML代码之中的,要怎样才能从HTML代码中获取想要的信息呢?正则表达式 就是其中一个有效的方法。

本节我们将了解-下正则表达式的相关用法°正则表达式是用来处理字符串的强大工具,它有自 己特定的语法结构’有了它’实现字符串的检索、替换、匹配验证都不在话下°

卜 卜 } | }

当然’对于爬虫来说’有了它’从HTML里提取想要的信息就非常方便了。 ↑.实例弓|入

说了这么多’可能我们对正则表达式到底是什么还是比较模糊’下面就用几个实例来看一下它的 用法°

打开开源中国提供的正则表达式测试工具ht印://tool.oschinaneUregex/,输人待匹配的文本’然后 选择常用的正则表达式’就可以得出相应的匹配结果了。例如’这里输人如下待匹配的文本。 ‖e11o』‖∏ypho∩e∩l」∏berj5O10ˉ86q321OOa∩deⅧ日i1j5〔q〔0cU1q1∩g〔己i。〔o∏)’

己∏d『∏yweb5itei5

http5://〔ujqi∩g〔ai·〔oⅧ ■尸‖卜|卜卜『【尸队|■厂‖[『『【广■〗『卜‖■厂

这段字符串中包含一个电话号码、一个Eˉmail地址利一个URL’接下来就尝试用正则表达式将 这些内容提取出来°

64

第2章基本库的使用

在网页右侧选择‘匹配Emall地址,’’就可以看到下方出现了文本中的Eˉmail’如阁2ˉll所示° 存线盂则表达式沮试



…呵……mm…V岭……泊…0…叼…■ ≡…

■簿翻鳖獭攫憋戒

腰″李节宇辙包括震…殉 ■

| ■■■|■口‖

窿但…鞠

霹睡撼禽行 匹…她址

露鳃a遮区…也ˉ挣.oˉ招,副…嚼旷…■…■n□□m‘叼…

窿罐衡越迅凰 …鸥阐

q

q

蟹蘸 鳞谢瓤酿翔L 窿鞠 憾瞧毋儡证q

窿醚犀^蹭=嚼》携武曰H

图2ˉll

提取Eˉmail地址

如果选择』匹配网址URL,’ ’可以看到下方出现了文本中的URL,如图2ˉl2所示° =

在线正喇表达式泪试 …叼…『…■00…m……妇—……叼…■ 一耐

■簿用魔瓣囊达壹 酸…

…节瓣……剧 〃

蓬贿遮露伶雹^.小咽峙

≡-

囱窒m■口皿*′阿…

醒霉徽抒

||

窿翌…鳃镶号饲

疆鲤鳖…

蹿知…

…1龋么

…●■

鳃…喇嫩涵●仍

隧翱V…

-

臃m讯蛔臀

酶中…罕坦男 窿鳃↑啦身份证号

…月=臼腮武日■

图2ˉl2匹配网址URL

是不是非常神奇?

于URL’开头是协议类型’然后是冒号加双斜线,最后是域名加路径。 对于URL来说’可以用下面的正则表达式匹配° [aˉZAˉZ]+://[^\5]*

用这个正则表达式去匹配_个字符串,如果这个字符串中包含类似URL的文本,那么这部分就 会被提取出来。



勺{(」■』■■』∏||」□□」□可‖』』‖‖‖』司|』|』】■

其实’这里就是用了正则表达式匹配,也就是用一定的规则将特定文本提取出来°例如, Eˉmail地 址的开头是-段字符串’然后是_个0符号,最后是某个域名’这是有特定的组成格式的。另外’对



正则表达式看上去虽然是乱糟糟的—团’但里面其实是有特定语法规则的°例如, aˉZ代表匹配

任意的小写字母’\5代表匹配任意的空白字符, *代表匹配前面的任意多个字符,那一长串正则表达

■勺‖‖』』勺』】』勺|·〗‖γ‖‖|■■■巴■■

式就是这么多匹配规则的组合。

「 | }

〗■日}「『■【『‖}■【‖|▲厂『[「■『}■【|||『「|[■∏『「|【■「}『|ˉ■「||}■厂|

2.3正则表达式

65

写好正则表达式后’就可以拿它去_个长字符串里匹配查找了。不论这个字符串里面有什么,只 要符合我们写的规则’统统可以找出来。对于网页来说,如果想找出网页源代码里有多少URL,只要 用匹配URL的正则表达式去匹配即可。

上面我们介绍了几个匹配规则’表2ˉ2列出了常用的一些匹配规则° 表2ˉ2常用的匹配规则 模







\W

匹配字母、数字及下划线

\‖

匹配不是字母`数字及下划线的字符

\5

匹配任意空白字符,等价于0t\n\r\0

\5

匹配任意非空字符

\d

匹配任意数字’等价于[0ˉ9]

\0

匹配任意非数字的字符

\∧

匹配字符串开头

\Z

匹配字符串结尾°如果存在换行,只匹配到换行前的结束字符串

\Z

匹配字符串结尾·如果存在换行,同时还会匹配换行符

\6

匹配最后匹配完成的位置

\∩

匹配_个换行符

\t

匹配一个制表符





匹配一行字符串的开头 匹配_行字符串的结尾

匹配任意字符,除了换行符,当Ie.DO『A[[标记被指定时,可以匹配包括换行符的任意字符 […]

用来表示一组字符’单独列出’例如[amk]用来匹配a、m或k

[^…]

匹配不在[]中的字符,例如匹配除了a、b、c之外的字符 匹配0个或多个表达式





匹配l个或多个表达式



匹配0个或l个前面的正则表达式定义的片段,非贪婪方式

{∩}

精确匹配″个前面的表达式

{∩」 Ⅶ}

匹配″到加次由前面正则表达式定义的片段’贪婪方式

a|b ()

匹配a或b

匹配括号内的表达式’也表示_个组

看完这个表之后’可能有点晕晕的吧’不用担心’后面我们会详细讲解一些常见规则的用法。 其实正则表达式并非Python独有’它也可以用在其他编程语言中°但是Python的re库提供了整

个正则表达式的实现’利用这个库’可以在Python中方便地使用正则表达式。用Python编写正则表 达式时几乎都会使用这个库’下面就来了解它的_些常用方法° 2。"atC‖

这里首先介绍第一个常用的匹配方法ˉ~ˉ们at〔∩’向它传人要匹配的字符串以及正则表达式’就 可以检测这个正则表达式是否和字符串相匹配。

∏atC‖方法会尝试从字符串的起始位置开始匹配正则表达式’如果匹配’就返回匹配成功的结果; 如果不匹配’就返回None°实例如下:



|}

第2章基本库的使用

66

〔o∩te∩t≡ 0‖e11o1234567‖oI1d「hj5j5日【egex0e∏o0 pIj∩t(1e∩(co∩te∩t)) re5(」1t=re.们at〔h(|^‖e11o\5\d\d\d\s\d{4}\5\"{1o}‖’〔o∩te∩t) pri∩t(Ie5u1t) pr1∩t(re5u1t.gIo0p()) pri∩t(re5u1t.5p己∩())

刘|||』■√』‖□】■‖』‖』■■】】‖□■‖|』■口

mportre

运行结果如下: q

41

(O’ 25)

这个实例首先声明了_个字符串’其中包含英文字母`空白字符`数字等°接着写了-个正则表 达式:

·■‖‖二=■∏』‖□■〗‖|』■可

<5re.SR["atc∩obje〔tj 5pa∩≡(O’ 25)’爬t〔h≡0‖e11o123q567"or1d丁m5!〉 ‖e11o1234S67"or1d丁hi5

^‖e11o\5\d\d\d\5\d{』}\5\w{10}

白字符,最后\W{10}则表示匹配l0个字母及下划线。我们注意到,这里其实并没有把目标字符串匹 配完’不过这样依然可以进行匹配,只是匹配结果短一点而已°

在"at〔‖方法中,第_个参数是传人了正则表达式’第二个参数是传人了要匹配的字符串° 将输出结果打印出来,可以看到结果是5R[‖atC∩对象’证明匹配成功°该对象包含两个方法: grouP方法可以输出匹配到的内容,结果是‖e11o1234567‖or1d「h15,这恰好是正则表达式按照规 则匹配的内容; 5Pa∩方法可以输出匹配的范围’结果是(O’25)’这是匹配到的结果字符串在原字符 串中的位置范围。

■司』■■ˉ■■‖则|』■□削■■』·可』■■』■司■■∏

用它来匹配声明的那个长字符串°开头的^表示匹配字符串的开头’也就是以‖e11O开头;然后 \5表示匹配空白字符,用来匹配目标字符串里‖e11o后面的空格; \d表示匹配数字, 3个\d用来匹 配123;紧接着的l个\5表示匹配空格;目标字符串的后面还有4567’我们其实依然可以用4个\d来 匹配’但是这么写比较烦琐’所以可以用\d后面跟{4}的形式代表匹配4次数字;后面又是l个空

通过上面的例子,我们基本了解了如何在Python中使用正则表达式来匹配-段文字° ●匹配目标

用Ⅷat〔∩方法可以实现匹配’如果想从字符串中提取一部分内容’该怎么办呢?就像上一节的实

可以使用括号()将想提取的子字符串括起来°()实际上标记了—个子表达式的开始和结束位置’

被标记的每个子表达式依次对应每个分组’调用gIoUp方法传人分组的索引即可获取提取结果°实例 如下: 1‖portre

』】|·■]刘‖叫{』』]‖■

例-样,从_段文本中提取出Eˉmail地址或电话号码°

0

〔o∩te∩t= |‖e11o1234567‖or1d丁hj515aRegex0e刚o re5u1t=re·‖at〔∩(』^‖e11o\5(\d+)\5‖or1d0 ’ 〔o∩te∩t) pr1∩t(re5u1t)

pri∩t(Iesu1t.group()) pr1∩t(re5u1t.group(1)) pIj∩t(re5u1t。5pa∩())



通过这个实例,我们把字符串中的123q567提取出来了’可以看到其中数字部分的正则表达式被() 括了起来°然后调用group(1)获取了匹配结果° 〈 sr巳5R[‖at〔bobje〔t】 spa∩≡(O’ 19)’∩at〔∩二‖‖e11o1234567‖or1d』〉

■■司■□]·|■■■‖‖·‖二■】|‖{划°·二■■

运行结果如下:

)卜) }



23正则表达式

67

} }

什e11o1234567‖or1d 1∑34567

■『卜〗户β}▲广β『‖′‖β【■|仆广‖『「·「些·『

(O’19)

可以看到’我们成功得到了1234567°这里用的是group(1),它与group()有所不同’后者会输 出完整的匹配结果’前者会输出第一个被()包围的匹配结果°假如正则表达式后面还有用()包围的 内容’那么可以依次用grOUP(2)` grOuP(3)等获取°

[_

●通用匹配

刚才我们写的正则表达式其实比较复杂’只要出现空白字符就需要写\5匹配,出现数字就需要 写\d匹配’这样的工作量非常大。其实完全没必要这么做’因为还有-个万能匹配可以用’就是.*°

其中.可以匹配任意字符(除换行符), *代表匹配前面的字符无限次,所以它们组合在一起就可以匹 配任意字符了°有了它’我们就不用挨个字符进行匹配了°

接着上面的例子,我们利用.*改写一下正则表达式: 1呻ortre

■≥「‖|‖‖‖‖‖’■■■厅‖『‖

〔o∩te∩t= ‖‖e11o123』567‖or1d丁M5j5a【e8ex0e‖℃| re5u1t≡re.爪at〔∩(|^‖e11o。*De帅$,’ co∩te∩t) pr1∩t(re501t) prj∏t(re501t.grOup()) pri∩t(re5u1t.5p己∩())

这里我们直接省略中间部分,全部用.*来代替’并在最后加一个结尾字符串°运行结果如下:

〖▲■尸「‖巴■■尸■厉■尸■『

〈 5re.5R[阳t〔hobjecti 5p己∩≡(0’ 』1)’Ⅷat〔h≡,‖e11o1234S67‖or1d丁h1sjsaRegex0eⅧo′〉 ‖e11o1234567‖or1d丁hj5j5aRegex0e∏℃ (O’41)

可以看到, gro‖p方法输出了匹配的全部字符串’也就是说我们写的正则表达式匹配到了目标字 符串的全部内容; 5pa∩方法输出(O’41)’这是整个字符串的长度° 因此’使用.*能够简化正则表达式的书写° ●贪婪与非贪婪

使用通用匹配.*匹配到的内容有时候并不是我们想要的结果°看下面的例子:

′ 尸『}『

j呻OItre

巴「「仍)圆氏

〔o∩te∩t=『}‖e11o1234567"or1d『hi5iSaRegexDem0 resu1t=Ie.厢t〔h(`^}{e.*(\d+).*贮硒$0’〔o∏te∩t) . prj∩t(re5l』1t) prj∏t(re5u1t。8roup(1))

这里我们依然想获取目标字符串中间的数字,所以正则表达式中间写的依然是(\d+)°而数字两 侧由于内容比较杂乱,所以想省略来写,于是都写成.*°最后,组成^‖e.*(\d+).*0e『∏o$,看样子没 什么问题°可我们看下运行结果:

[□『‖『

} [「

「}





|尸

广





〈5Ie。5旺由t〔hobje〔tj 5p己∩=(O’ 4O)’硒t〔h≡,‖e11o1234367№r1d『hj5i5aRegexDe0m,〉 7

奇怪的事情发生了,只得到了7这个数字,这是怎么回事?

这里涉及贪婪匹配和非贪婪匹配的问题°在贪婪匹配下, .*会匹配尽可能多的字符°正则表达式 中.*后面是\d+’也就是至少一个数字,而且没有指定具体几个数字’因此, .*会匹配尽可能多的字 符,这里就把123456都匹配了,只给\d+留下一个可满足条件的数字7’因此最后得到的内容就只有 数字7°

但这很明显会给我们带来很大的不便°有时候,匹配结果会莫名其妙少—部分内容°其实这里

第2章基本库的使用

68

只需要使用非贪婪匹配就好了。非贪婪匹配的写法是.*?’比通用匹配多了_个?’那么它可以起到 怎样的效果?我们再用实例看_下: i∏pOrtre

〔o∩te∩t= |‖e11o123』567‖or1d丁∩j515aRegexDe∏]o! re5(」1t≡re‘∏3tc∩(|^‖e。*?(\d+)。*0e们o$『 ’ 〔o∩te∩t) pr1∩t(re5u1t) prj∩t(re5u1t.group(1))

这里我们只是将第一个.*改成了.*?’贪婪匹配就转变为了非贪婪匹配°结果如下: 〈5Ie.5【["atchobje〔tj 5pa∩=(0’ 40)’ ∏‖at〔h=0‖e11o1234567‖or1d『hi5i5aRegex0e帅』〉 1234567

此时便可以成功获取1234567了°原因可想而知,贪婪匹配是匹配尽可能多的字符,非贪婪匹配 就是匹配尽可能少的字符°当.*?匹配到‖e11o后面的空白字符时,再往后的字符就是数字了’而\d+

恰好可以匹配’于是这里.*?就不再进行匹配了’而是交给\d+去匹配。最后.*?匹配了尽可能少的 字符’\d+的结果就是1234567°

所以说’在做匹配的时候’字符串中间尽量使用非贪婪匹配’也就是用.*?代替.*’以免出现匹 配结果缺失的情况。

但这里需要注意’如果匹配的结果在字符串结尾.*?有可能匹配不到任何内容了’因为它会匹 配尽可能少的字符°例如: 1们Portre

〔o∩te∩t= ‖http://weibo。〔o们/co∏Ⅻe∩t/旺ra〔‖‖

re5u1t1=re.爪at〔h(0打ttp.*?co咖e∩t/(·*?)‖ ’ co∩te∩t) re5u1t2=re.帕tch(0http.*?co咖e∩t/(。*)』′ 〔o∩te∩t) pr1∩t( 0re5u1t1‖ ’ re5u1t1.group(1〉) pr1∩t〈|re5u1t∑‖」 re5u1t2.group(1〉)

运行结果如下: re5u1t1

re5u1t2k[ra〔‖

可以观察到, .*?没有匹配到任何结果’而.*则是尽量多匹配内容’成功得到了匹配结果° ●修饰符

在正则表达式中’可以用-些可选标志修饰符来控制匹配的模式。修饰符被指定为一个可选的标 志。我们用实例来看—下: mPoItre co∩te∩t= ! 0 ‖‖e11o1234567‖or1d『bj5

15■【egeXDe咖 0

0



res(』1t=r巳∏]at〔‖(|^‖e.*?(\d+).*?De『∏o$|’ 〔o∩te∩t)

pri∩t(re5u1t.gIOup(1))

和上面的例子相仿’我们在字符串中加了换行符,正则表达式还是一样的’用来匹配其中的数字° 看-下运行结果:

5 『| |

厂■■■【■■■∏匡■■■

Attribute[rror丁ra〔eba〔|〈 ("ost【ece∩t〔a111a5t) 〈jpγt们o∩ˉj∩p‖tˉ18ˉ〔7d232b39645〉 j∩<|∏odu1e〉()



6res(」1t=r已‖日t〔h(|^‖e.*?(\d+).*?De′‖o$|’ co∩te∩t)

ˉ_〉7pIj∩t(re5u1t.group(1))





Attribute[rror: 刊o∩e『γpe| obje〔tha5∩oattI1bute |grol」p 』



| ‖

【【■巴■「‖

份「□‖■厂)》

23正则表达式

69

发现运行直接报错,也就是说正则表达式没有匹配到这个字符串,返回结果为‖o∩e,而我们又调 ■ 尸 『 卜 ˉ ■ 厂

用了group方法,导致∧ttrjbute[rIor。 那么,为什么加了一个换行符,就匹配不到了呢?这是因为匹配的内容是除换行符之外的任意字

乙■尸‖『●『

「 } 卜 份‖『{『■「||伍「‖‖

f

符,当遇到换行符时, .*?就不能匹配了’所以导致匹配失败°这里只需加—个修饰符re.5,即可修 正这个错误: re5u1t=re.Ⅷ日t〔们(0^‖e.*?(\d+).*?0e『∏o$0 」〔o∩te∩t’ re.5)

这个修饰符的作用是使匹配内容包括换行符在内的所有字符。此时运行结果如下: 123q567

这个r巳5在网页匹配中经常用到。因为HTML节点经常会有换行,加上它’就可以匹配节点与 节点之间的换行了°

另外’还有~些修饰符’在必要的情况下也可以使用’如表2ˉ3所示° 表2ˉ3修饰符



修饰符



re.I

使匹配对大小写不敏感

re.l

实现本地化识别(localeˉawa冗)匹配

Ie.‖

多行匹配,影‖问^和$

re.5

使匹配内容包括换行符在内的所有字符









卧■【【飞巴■『「|■■「

re°0

根据Unicode字符集解析字符。这个标志会影响\N、\‖、 \b和\8

re。X

该标志能够给予你更灵活的格式’以便将正则表达式书写得更易于理解

在网页匹配中’较为常用的有re.5和re.I°

■庐′『‖‖「巳■厂

●转义匹配

我们知道正则表达式定义了许多匹配模式,如.用于匹配除换行符以外的任意字符°但如果目标 字符串里面就包含.这个字符,那该怎么办呢?

旧|卜|卜

这时需要用到转义匹配,实例如下: i∏pOrtre



〔o∩te∩t≡ ‖(百度)硼w。bajdq·c咖!

re5u1t=re.Ⅶ己tch(‖\(百度\)…\.baid‖\.co们‖’ 〔o∩te∩t)



prj∩t(re5‖1t)

当在目标字符串中遇到用作正则匹配模式的特殊字符时’在此字符前面加反斜线\转义_下即可° |卜







例如\.就可以用来匹配.’运行结果如下: 〈5re.5旺胎tchobje〔tj 5p己∩=(o’ 17)’ 爪at〔h=!(百度)"M√.bajdu.co‖`〉

可以看到’这里成功匹配到了原字符串°

以上这些是写正则表达式时常用的几个知识点,熟练掌握它们对后面非常有帮助。 3.5e己rCh

前文提到过,‖at〔∩方法是从字符串的开头开始匹配的’意味着一旦开头不匹配’整个匹配就失 败了。我们看下面的例子: j"portre

〔o∩te∩t= ,[xtm5t1∩85‖e11o12M567‖or1d丁M515aRegex0e『‖o[》《tra re5u1t =re.们at〔∩(|‖e11o。*?(\d+).*?DeⅧ0 ’ 〔o∩te∩t) pr1∩t(re5u1t)

5tj∩850



第2章基本库的使用

这里的字符串以[×tm开头’正则表达式却以‖e11O开头,其实整个正则表达式是字符串的一部 分,但这样匹配是失败的°运行结果如下: ‖O∩e

因为"at〔‖方法在使用时需要考虑目标字符串开头的内容’因此在做匹配时并不方便。它更适合 检测某个字符串是否符合某个正则表达式的规则。

这里就有另外_个方法5ear〔b’它在匹配时会扫描整个字符串,然后返回第一个匹配成功的结果° 也就是说’正则表达式可以是字符串的-部分。在匹配时, 5ear〔‖方法会依次以每个字符作为开头扫

描字符串,直到找到第一个符合规则的字符串,然后返回匹配内容;如果扫描完还没有找到符合规则 的字符串.就返回‖O∩e°

我们把上面代码中的"at〔h方法修改成5ear〔‖’再看下运行结果:



0





〈 5re.5R[‖at〔∩obje〔tj 5pa∩≡(13」 53)’ ∏3t〔∏≡0‖e11o1234567‖or1d『∩i515日促e8exDe们o!> 1234567



这时就得到了匹配结果。

因此’为了匹配方便,尽量使用5ear〔h方法°

下面再用几个实例来看看5ear〔h方法的用法°

首先,这里有_段待匹配的HTML文本’接下来写几个正则表达式实例实现相应信息的提取: ht川1=‖′〈djγjd="5o∩g5ˉ1j5t"〉







<‖2〔1aS5="t1t1e"〉经典老砍〈/∩2〉

<p〔1a5S≡"j∩tIodu〔tio∩"〉 经其老歌列浪 〈/p〉

<U11d="1i5t"〔1a55="1j5tˉgIOUP00〉 <1jd3ta_γje"="2"〉一略上有你</1j〉 〈1jd3taˉVjeW=卿70o〉



<己href="/2。们p3" 5i∩ger="任贤齐"〉沧海一户笑</a〉 〈/1i〉



〈11dat己ˉγje"="4"〔1a5S≡"a〔tjγe0o〉

〈ahre+="/3.∏p3" Sj∩8er="齐撂‖0〉往亨随风</a〉 〈/1j〉

〈1jdataˉγi印=圃6傲〉〈abre十="/4川p3曰 sj∩geI=国beγo∩d0‖〉光辉岁月〈/a〉〈/1i〉 <1jd3taˉγj酗=■5同〉〈ahIe「=圃/5。们p〕. 5j∩ger=订陈总琳,0〉记亨本</a〉〈/1i〉

| √

〈1jd己taˉγi出=口5国〉

〈ahre十≡闻/6。们p3" 5i吧er=闻邓丽君阐〉但愿人长久〈/3〉 〈/1i〉 〈/u1〉 </diγ〉‖0

可以观察到, U1节点里有许多11节点,这些1i节点中有的包含a节点,有的不包含°a节点还 有-些相应的属性—超链接和歌手名°

首先,我们尝试提取〔1a5S为a〔tjγe的1j节点内部的超链接包含的歌手名和歌名,也就是说需 要提取第三个1j节点下a节点的5j∩geI属性和文本°

此时正则表达式可以以1j开头’然后寻找_个标志符a〔tiγe,中间的部分可以用.*?来匹配°

接下来,因为要提取51∩geI这个属性值,所以还需要写人51∩geI="(.*?)"’这里把需要提取的部分用 小括号括了起来,以便用grOup方法提取出来,小括号的两侧边界是双引号°然后还需要匹配a节点 的文本’此文本的左边界是〉’右边界是</a〉。然后目标内容依然用(.*?)来匹配,所以最后的正则 表达式就变成了: 〈1i.*?a〔tive.*?5i∩8eI≡阅(.*?)"〉(.*?)(/a〉

再调用5ear〔们方法,它会搜索整个HTML文本’找到符合上述正则表达式的第一个内容并返回。



《||」{‖〗‖』】·‖‖‖】‖】■Ⅲ叫甥割刚叼呵{』‖‖{刘』』』■



70

| 「「|卜



2.3正则表达式

7l

另外,由于代码中有换行,所以5ear〔∩方法的第=个参数需要传人re.5°于是整个匹配代码如下: Ie5u1t=re.5earC∩(‖〈1L*?a〔tiγe.*?5i∩ger="(.*〉)"〉(.*?)</a〉0 ’ ∩tⅧ1’ re.5) i+re5u1t:

Prj∩t(re5u1t.8roup(1)’ re5u1t.gIoup(2))

由于需要获取的歌手和歌名都已经用小括号包围所以可以用group方法获取。 运行结果如下: 齐篓往亨随风

|β



■尸『|■■尸■■『‖‖『|



可以看到,这正是C1a55为aCtjγe的1i节点内部的超链接包含的歌手名和歌名°

如果正则表达式不加a〔t1γe(也就是匹配不带〔1a55为aCtiγe的节点内容)’会怎样呢?我们将 正则表达式中的a〔tjγe去掉,代码改写如下: reSu1t=re·Se日r〔h(‖〈1j.*?Sj∩8eI="(.*?)同〉(°*?)</a〉』’ ∩tⅧ1’ re。5) i千Ie5l』1t:

pri∩t(re5u1t°gIoup(1)’ re5l』1t.group(2))

由于5ear〔∩方法会返回第—个符合条件的匹配目标’于是这里结果就变了:

■卜『『

任贤齐沧海一户烫

■厂△。『‖‖【■■「}|■■■「||′■『||尸『「|儿■「‖「■■「「卜‖『‖「||↑}■『|||仆■『‖|■尸‖『‖|■尸‖『}Ⅱ■『『〗‖■厂『巳■〗■■【■尸「

把aCtiγe标签去掉后’从字符串开头开始搜索’此时符合条件的节点就变成了第二个1j节点’ 后面的就不再匹配’所以运行结果就变成第二个11节点中的内容。

注意’在上面的两次匹配中, 5ear〔h方法的第三个参数都加了re.5,这使得.*?可以匹配换行’ 所以含有换行的1j节点被匹配到了°如果我们将其去掉’结果会是什么?去掉re.5的代码如下: re5u1t=re.5ear〔‖(|〈1j.*?5i∩ger=α(.*?)"〉(.*?)〈/a〉` ’ hm1) j十re5u1t:

prj∩t(re5u1t。8roup(1)’ resu1t.8roup(2))

运行结果如下: beγo∩d尤啤岁月

可以看到’结果变成了第四个1j节点的内容°这是因为第二个和第三个1j节点都包含换行符, 去掉re.5之后’ .*?已经不能匹配换行符,所以正则表达式不会匹配这两个11节点,而第四个11节 点中不包含换行符’可以成功匹配°

由于绝大部分HTML文本包含换行符’所以需要尽量加上re.5修饰符’以免出现匹配不到的问题。 4+i∩da11

介绍完了5ear〔们方法的用法,它可以返回与正则表达式相匹配的第_个字符串。如果想要获取 与正则表达式相匹配的所有字符串,该如何处理呢?这就要借助十1∩da11方法了。

还是用上面的HTML文本’如果想获取其中所有a节点的超链接、歌手和歌名.可以将5ear〔h方 法换成+1∩da11方法。其返回结果是列表类型’需要通过遍历来依次获取每组内容。代码如下: Ie5u1t5=re.「i∩da11(‖<11.*?∩Ie十=铡(。*?)".*?5j∩ger=圃(。*?)"〉(.*?)〈/a〉0 ′ ht们1’ re.5) prj∩t(re5u1t5) pIj∩t(tγPe(re5u1t5)) 十orre5u1t1∩resu1t5:

prj∩t(re5u1t)

pri∩t(re5q1t[0]’ re5u1t[1]’ re5u1t[2])

运行结果如下:

[(|/2。∩p3|’ ,任贤齐0 ’ |沧海一户更|)’(|/3."p3′’|齐泰,’ 0往本随风|)’(|/4Ⅻp30 ’beyo∩d′ ’ ,光辉岁 月 |)’(,/5."P3|’ | 陈憋琳`’‖记亨本|〉’(|/6.卯3|’`邓丽君』’ 》但愿人长久|)]

〈〔1a55 |1i5t|〉

(’/2.们P]|’ |任贤齐|’ |沧海一户笑 |)



第2章基本库的使用

72

/2·‖p3任贤齐沧海一户笑 (,/3.呻3『 ’ ‖ 齐桑』’ |往亨随风‖) /3·们p3齐纂往亨随风 (!/q。刚p3|’beyo∩d‖ ’ |光挥岁月 0) /q。们p〕beyo∩d光啤岁月 (|/5。Ⅶp30 ’ ! 除憋琳!’ 0记亨本!) /5。呻3陈总淋记亨本 (0/6.呻30 ’ 0邓丽君‖’ 0但愿人长久!) /6.Ⅶp3邓丽君但愿人长久

可以看到’返回的列表中的每个元素都是元组类型,我们用索引依次取出每个条目即可° 总结一下’如果只想获取匹配到的第_个字符串,可以用5earC们方法;如果需要提取多个内容, 可以用+j∩da11方法。 5。sub

除了使用正则表达式提取信息,有时候还需要借助它来修改文本。例如,想要把一串文本中的所

有数字都去掉’如果只用字符串的reP1a〔e方法’未免太烦琐了,这时可以借助sub方法°实例如下: j呻Ortre

冰yrojRjx[g

这里往5ub方法的第_个参数中传人\d+以匹配所有的数字’往第二个参数中传人把数字替换成 的字符串(如果去掉该参数’可以赋值为空)’第三个参数是原字符串。

||

运行结果如下:

』■‖‖■■■□Ⅵ』』‖乙■】□】】〗■·

〔o∩te∩t= |54a阳qyr5ojR5』1x5k2g! co∏te∩t=re·5ub(0\d+‖’ 0 ‖ ’ 〔o∩te∩t) pr1∩t(〔o∩te∩t)

在上面的HTML文本中’如果想获取所有11节点的歌名,直接用正则表达式来提取可能比较烦

pIi∩t(re5u1t[1])

运行结果如下: 一略上有你 沧海一户笑 往亨随风 尤啤岁月 记本本

但愚人长久

而此时借助5ub方法就比较简单了°可以先用5ub方法将a节点去掉,只留下文本,然后再利用 ∩t|‖l1≡re.5ub(‖<日.*》〉|〈/日〉‖’ 0 ‖ ′ ht‖1) pri∩t(ht盯1)

re5u1tS≡re.fj∩d己11(!〈11.*?〉(。*?)〈/1i〉0 ’ ∩t∩1′ re.5)

+OIreSu1ti∩re5u1tS:

pri∩t〈re5u1t。5tr1P())

■■·‖‖‖■■■■】■‖]□■■·‖』■〗□』■

十1∩da11提取就好了:

■Ⅷ‖||』]||』‖(|·】‖‖』■■『』γ‖■』』|』叮(

re5u1t5≡re.十j∩da11(,〈1j。*?〉\5*?(〈a·*?〉)?(\盼)(〈/a〉)?\5*?〈/1j〉』’ ht"1’ Ie.5) foIre5u1t1∩re5u1t5:

|‖{

琐。例如’写成这样:

运行结果如下: 〈djγid="5o∩85ˉ1i5t同) 〈∩2〔1aS5="tjt1e"〉经典老歌〈/h2> 〈p〔1a55="i∩tmd0ctjo∩"〉 经其老砍列农 </p〉

〈u1jd="1j5t"〔1a55="1i5tˉgroup厕〉

‖■■‖|」□■勺』■‖』·】■■■

}}}

24 httpx的使用

73

〈1idata-γ1e"="2"〉一略上有你〈/1i〉 〈Md己t己ˉγ1eW="7"〉 ■■尸〖■■『■■■■『|△■尸

沧海一户更





</11〉

<1idat己ˉγjew二"』00 C1a55=,0己〔tiγe"〉

往本随风 </11〉

〈1jdataˉv1ew="6"≥光辉岁月</1i> <1jdataˉγieW≡"5"〉记事本</11〉 〈1idataˉγj出≡|!5"〉 但愿人长久 (/11〉 </u1> </diγ〉

△■厂巴=■■【=尸卜)|止■『卜■■厂}「■■广



一略上有你 沧海一户更

往亨随风 尤辉岁月 记亨本 但愿人长久

可以看到’经过5ub方法处理后’a节点就没有了’然后通过千j∩da11方法直接提取即可°可以 发现,在适当的时候借助5ub方法’可以起到事半功倍的效果。

6〔mpi1e

卜|‖

前面所讲的方法都是用来处理字符串的方法’最后再介绍-下CO阳pi1e方法,这个方法可以将正 则字符串编译成正则表达式对象’以便在后面的匹配中复用°实例代码如下: iⅧpOrtre



)》‖

co∩te∩t1= 02o19ˉ12ˉ1512:Oo0

〔o∩te∩t2= 『2019ˉ12ˉ17n:55| 〔o∩te∩t3= 0∑O19ˉ12ˉ2213:210

■■尸■吁■■尸『卜▲■「‖■尸■‖|户

p己tter∩=re.〔oⅧpj1e(|\d{2}:\d{2}0) re5u1t1=re。sob(patteI∩’ 0 』’ 〔o∩te∩t1) re5‖1t2=Ie.5Ub〈PatteI∩」 ! 0 ’ 〔O∩te∩t2) resu1t3=re.s(』b(patter∩’ 0 0 ’ co∩te∩t3) pr1∩t(re5u1t1’ re5u1t2’ Ie5u1t3)

这个实例里有3个日期,我们想分别将这3个日期中的时间去掉’这时可以借助5ub方法°该方 法的第一个参数是正则表达式’但是这里没有必要重复写3个同样的正则表达式,此时就可以借助 〔O"pi1e方法将正则表达式编译成一个正则表达式对象’以便复用° 运行结果如下:

} } ) 尸

2O19ˉ12ˉ1S

2O19ˉ12ˉ17

2O19ˉ12ˉ22

另外’〔o∏p11e还可以传人修饰符’例如Ie.5等修饰符’这样在5earc∩` +i∩da11等方法中就不 需要额外传了°所以,可以说co"p11e方法是给正则表达式做了-层封装’以便我们更好地复用。

}卜

尸′

■■







7.总结

到此为止,正则表达式的基本用法就介绍完了’后面会通过具体的实例来巩固这些方法。 本节代码参见: https://githuhcom/Python3WebSpider/RegexTest。

24 httpx的使用 前面我们介绍了urllib库和requests库的使用’已经可以爬取绝大多数网站的数据’但对于某些 网站依然无能为力°什么情况?这些网站强制使用HTrP/20协议访问,这时urlllb和requests是无法 爬取数据的,因为它们只支持HTTP/l.l’不支持HTTP/20°那这种情况下应该怎么办呢?



第2章基本库的使用

74

还是有办法的’只需要使用一些支持HTTP/20的请求库就好了’目前来说’比较有代表性的是

hyper和httpx,后者使用起来更加方便,功能也更强大, requests已有的功能它几乎都支持。 本节我们介绍httpx的使用° ‖.示例

下面我们来看-个案例’https://spal6scrap巳cente∏就是强制使用HTTP/20访问的_个网站,用 测览器打开此网站,查看Netwo「k面板,可以看到Protocol一列都是h2,证明请求所用的协议是



HTTP/2.0’如图2ˉ13所示。 +

§

…~



★≡…

■—、



飞′=



■巴■■=尝

愿●一≈

田●=

』司』■■‖‖{■■■]〗·

—……—…

半…∏ˉ〔■…

f仓 旦丝



0…■■

…≈

…■■

声■

…■■

陪 ………

澄=P

…■





…m

Ⅵ…=

‖0…=







□·‖ 」‖」‖』=■■〗



…≈

■□■』■

■ }■…0…

◆←Ⅷ

儡…≡……

m

吐 田

m



汹m功~≈…■ 甄= 丝m吨…匈…■ 硒 口_…

;』………… 臼■≈…

iˉ』≡…?尸



=回

泊 陋



1



沽 睡

…乙

……←,





』u^:鹅..



0…

必…=回m■



… m

■_m





心 心



吧 m

m

… 一

`赡°■



凹△吗

k

四■『■■■

↑0■·■■■■■

‖p‖■

击′……—Ⅷ■≡

迪D烟· ‖四●



^沁

~p=■丁香啡幽劫盂= 击曙钓

室ˉ.

■■■!

…广c} ■

这个网站用requests是无法爬取的’不妨来尝试一下:

l」r1= 0http5://5pa16。5cmpe·〔e∩teI/0 re5po∩se≡reque5t5.8et(ur1) prj∩t(Ie5po∩5e.text)

丁ra〔ebaC代(m5tre〔e∩t〔a111a5t): ■

■日‖‖』■』‖‖□

运行结果如下:

(』■可■■‖《|

1呻ortIeque5t5

■■|‖■■可

图2ˉl3使用HTTP/20协议访问的网站

p





———-一—=-—■-=

■吨{

m月■

凰酿P1=空_=-旦 0}T一…上==音=…1 ==r□■~■▲■……00”■~ ≡ ˉ■■Q=■ ‖=γ0@□

」} ≡ .

7』≈

?·≤

=.二

| ˉ

门m0

酗■

叮帝=凸司

一…尘■

‖ts·■ Ⅷ,0■ 0.0∏Q!■

G凸,o 瓜△空





m

队`0‘■

骋■ 】』■

γIˉ

m



攫-

上」…=…?穗…

■…■



((□

c—≈



ˉ陋 … 』 n Ⅻ ‖… 1 甩-m ≡`1 沽

沁≈0

Ⅵ$■

==

』 ■ 〗 ‖ ] ■ ■ ■

憋0



rai5eRe‖∏oteDi5co∩∩ected("∩e们otee∩d〔1o5ed〔o∩∩eCt1o∩wit∩o|」t"

http.〔1je∩t·只e帅teDi5〔o∩∩e〔ted: Re∏℃tee∩d〔1o5ed〔o∩∩ect1o∩wjt∩o0trespo∩5e

〔o∩∩e〔tio∩倒jt∩outre5po∩5e』)))

可以看到,首先抛出的就是RemoteDisconneCted错误’请求失败°

可能有人认为这是没有设置请求头导致的’其实不是,真实原因是l℃quests这个库是使用HTIP/l.1 访问的目标网站,而目标网站会检测请求使用的协议是不是HTTP/20’如果不是就拒绝返回任何结果。



司 ‖ ‖ | ((‖‖|{

reque5t5.eXceptio∩5·proxy[rIor; ‖∏p5〔o∩∩e〔tjo∩poo1(‖o5t=‖spa16.5cmpe.〔e∩teI0 ’ port≡443): ‖axretrje5

e》(ceeded"jt‖ur1: /(〔3u5edbyproxy[Iror(‖〔己∩∩ot〔o∩∩e〔ttoproxy.!’ Re"oteOjsco∩∩ected(|Re『∏otee∩dc1o5ed



2安装

』■■■■司』‖□■《‖■■‖‖

ht‖px可以直接使用pip3工具安装’所需的Python版本是36及以上,安装命令如下:

‖■■可

2.4

httpx的使用

75

pip3i∩5ta11httpx



但这样安装完的httpx是不支持HTTP/2O的’如果想支持’可以这样安装: pjp3 i∩5ta11 !∩ttpX[http2]‖



这样就既安装了httpx,又安装了httpx对HTIP/20的支持模块° 3.基本使用

httpx和requests的很多APl存在相似之处,我们先看下最基本的GET请求的用法: 1‖∏pOrt们ttpX 『■尸||旧卜}『β伊广「)『·『|}■∏β‖『■【『『匹■【『}

re5po∩5e=httpx.get(|∩ttp5://….httpbi∩.org/get,〉 pIi∩t(re5po∩5e.5tatu5code) prj∩t(respo∩5e。beader5) pI1∩t(Ie5po∩se.text)

这里我们还是请求之前的测试网站,直接使用httpx的get方法即可,用法和requests里的_模— 样,将返回结果赋值为re5po∩5e变量’然后打印出它的5tatu5code` ∩eader5、text等属性’运行结 果如下: 20O

‖eader5({0date|:Ⅷo∩’17"ay202115:54:06刨『! ’ |〔o∩te∩tˉtype|: ‖己pp1i〔atio∩/jso∩』’ ‖〔o∩te∩tˉ1e∩gt∩0 : 03o50 ’ 0〔o∩∩e〔tjo∩0 ; 0戊eep-a1jγe0 ’ ‖5erγer, : !gu∩icor∩/19。9。0‖’ !a〔〔e55ˉco∩tro1ˉa11o"ˉorig1∩! ; !*0 』

a〔〔e55ˉco∩tro1ˉ己11owˉ〔rede∩ti己15! : |true,}) {

!!arg5": {}’ "∩eaderS": { 冈∧〔Cept": 囱*/*"’

阅∧c〔eptˉ[∩〔od1∩g0! : "gz1p’de「1ate"」 ■‖ost00 : "哪.httpbj∩.oIg"’ "05eIˉ∧8e∩t": |‖pytho∩-∩ttpx/0。18·1"’

00Xˉ蛔z∩ˉ「ra〔eˉId"3 "Root=1ˉ6O己2919eˉ7〔ab9Od911d813877e6e4e8』00

}’

"or1gj∩困: 曰203.184°131°36口』 厕ur1口: 侧‖ttp5://卿。httpbi∩°or8/8et圃 }

输出结果包含三项内容, 5tatu5〔ode属性对应状态码’为2OO; ∩eader5属性对应响应头’是个‖eader5对象,类似于一个字典; text属性对应响应体,可以看到其中的05erˉ∧ge∩t是 pytho∩ˉ‖ttpx/0.18.1,代表我们是用httpx请求的。 下面换一个05erˉ∧ge∩t再请求一次’代码改写如下: j∩凹rt∩ttpx

header5={

U5erˉ∧8e∩t,: 0№zj11a/5。O(Nacj∩to5∩; I∩te1腮〔05X10157)∧pp1e"ebⅨit/537。36 (阳丁肌’ 1i代e6e〔促o) 〔‖r"论/90。O.“30.9〕5a「arj/537.36|



respo∩5e=∩ttp)《.get(0∩ttp5://b删‖。httpbi∩。org/get』’ ∩eader5≡header5) Pri∩t(Ie5PO∩5e。teXt)

试里我们换了-个U5erˉ∧ge∏t重新请求,并将其赋值为header5变量’然后传递给∩eader5参数’ 运行结果如下: {

娜arg5口: {}’ "header5阿: {

00∧C〔ept": "*/*"』



"A〔〔eptˉ[∩〔odj∩8"; "g2jp’de十1ate阑’ "

"}{o5t": 0,枷°‖ttpbj∩.org’

"05eⅢˉ∧8e∩t": "0‖o2j11己/S。0(舶〔i∩to5h】 I∩te1阳〔05X10157)∧pp1e‖eb贝jt/5〕7.36(Ⅶ‖丁肌’1jke0e〔代o) 〔∩rα肥/9O.O·4030.935a伯Ij/537。36口’

■〗」■∏■●

"XˉA』∏z∩ˉ『ra〔eˉId": "Root二1ˉ60a293b9ˉ1042225日7377888M54d1+62" 飞

)’

00origi∩": "2O3。184°131·36"》 !o0I1": 0o∩ttps目//w洲。httpbi∩.org/get|| }

可以发现更换后的05erˉ∧ge∩t生效了。

回到本节开头提到的示例网站,我们试着用httpx请求—下这个网站,看看效果如何’代码如下:

』■■‖■■■】勺|■可』‖】|■司‖

第2章基本库的使用

76







1‖pOIthttpx 0

运行结采如下:

丫I3CebaC促 (Ⅷ5tre〔e∩t〔a111a5t): ■



‖|||·

re5po∩5e=httpx.get(0http5://5p日16.5〔rape.〔e门ter0 ) prj门t(respo∩5e.te其t)



rai5eRe∏‖oteProto〔o1[rroI("5g) ∩ttp〔ore·Remteproto〔o1[rroI; 5eIγerdj5〔o∩∩e〔tedⅦit门outse∩d1∩gaIe5Po∩5e. 丁‖eaboveex〔eptio∩wa5thedire〔t〔日u5eo千t∩e千o11owi∩ge×ceptjo∩: ■





r日jse们己ppedˉe×c(‖e55age)+IoⅧex〔http×Re∩oteprotoco1[rroI: 5erγerdj5〔o∩∩e〔tedwjt∩outse∩d1∩g日Ie5po∩5e.

』■可■‖■」■■可』·Ⅵ

可以看到’抛出了和使用requests请求时类似的错误,不是说好支持HTTP/20吗?其实’h叮x默 认是不会开启对HTTP/2.0的支持的,默认使用的是HTTP/l.l ,需要手动声明-下才能使用HTTP/2.0, 代码改写如下:

运行结果如下:



‖|‖

<|皿Ⅳp[ht们1〉〈ht|『‖11a∩g=e∩〉〈head〉〈『∏eta〔∩ar5et=ut十ˉ8〉〈Ⅷeta∩ttpˉequ1v=Xˉ0∧ˉ〔o刚p日tib1e 〔o∩te∩t="I[≡edge,0〉<爬t己∩a们e=γ1ewport〔o∩te∩t≡"w1dth=deviceˉwidth’1∩itja1ˉ5〔31e=1"〉〈"et己∩a∏e=re+errer 〔o∩te∩t=∩oˉre「erIer〉〈1j∩促re1=j〔o∩hre+二/+己vj〔o∩.1〔o〉<tjt1e〉5〔mpe | Boo促〈/tjt1e〉〈11∩代 hre+=/〔55/〔∩u∩恨ˉ5o522e84.e4e1d己e6.c55Ie1=pre「etc∩〉〈1j∏代hre「=/c55/〔们u∩促ˉ+52d396〔·耳千574d24。〔55 Ie1≡pIe千et〔h〉<11∩低∩re「=/j5/〔hu∩代ˉ5O522e幽·6b3e24a己.j5Ie1≡pIe+et〔‖〉<11∩k

□{■勺』(|■〖巴

1川pOrthttpx 〔1je∩t=http×.〔1ie∏t(http2=了n』e) re5po∩5e二〔11e∩t.get(0‖ttp5://5pa16.5〔Iape.〔e∩ter/0 ) Prj∩t(re5po∩5e.text)

hre十≡/j5/chu∩Ⅸˉ千52d396〔·千8于41620。j5re1≡pre千etch〉〈1i∩代hIe「≡/〔55/己pp·ea9d8o2a。〔55Ie1≡pre1oad

■勺可』□■

35=5tγ1e〉〈11∩促hre十=/j5/己pp.b93891e2.j5re1=pre1oada5=5〔ript>〈1j∩促hIe十=/js/〔hu∩|《ˉγe∩dor5°a02仟921。j5 re1=pIe1oada5=5〔r1pt〉〈1i∩|(∩re「≡/〔55/app°ea9d802a·〔55re1=5ty1e5heet〉</he己d〉〈body〉<∩o5〔rjpt〉〈5tro∩g〉‖e0re sorrγbutporta1doe5∩‖tworkproper1ywit∩out〕日γ己5crjpte∩ab1ed. p1ea5ee∩己b1ejtto

‖■‖』■]■■■■□■■■

co∩t1∏ue.〈/5tro∩g×/∩o5〔ript〉<djγ1d≡app〉</djγ〉〈5〔ript 5r〔=/j5/〔‖q∩促ˉγe∩dor5。ao2仟921·j5〉</5〔rjpt〉<5〔r1pt 5r〔=/j5/日pp.b93891e2.j5>〈/5〔ript〉〈/bodγ〉〈/∩t"1〉

这里我们声明了_个〔1je∩t对象,赋值为〔11e"t变量,同时显式地将http2参数设置为『rue, 这样便开启了对HTTP/20的支持,之后就会发现可以成功获取HTML代码了°这也就印证了这个示 例网站只能使用HTTP/20访问。

刚才我们也提到了, h肮px和requests有很多相似的API,上面实现的是GET请求’对于POST请

q

□可』■■

求` PUT请求和DELETE请求来说,实现方式是类似的; 1川pOrt∩ttpX

r=httpx°get(′们ttp5://"ww.∩ttpbi∩.org/8et0 ′ pamⅦ5≡{‖∩a∏e‖ : !gemeγ0}) I=∩ttpx.po5t(!http5;//Nww.∩ttpbj∩.org/po5t0 ’ dat日={`∩a们e《 : 0ger们ey‖}) r=httpx.put(』http5://|0ww。∩ttpb1∩。org/put‖)



r≡∩ttpx.de1ete(0∩ttp5;//www.∩ttpbj∩.org/de1ete0) r=httpx.pat〔h(0∩ttp5://www.∩ttpbi∩°oIg/patc∩‖)

基于得到的Re5pO∩5e对象,可以使用如下属性和方法获取想要的内容。

q

□5t日tu5〔ode:状态码°

□text:响应体的文本内容°

』』|■■■■‖日乙■∏



‖」□】|





2.4 httpx的使用

77

□co∩te∩t:响应体的二进制内容,当请求的目标是二进制数据(如图片)时,可以使用此属性 获取。

□∩eader5:响应头’是‖eader5对象’可以用像获取字典中的内容-样获取其中某个‖eader 的值。

□j5o∩:方法’可以调用此方法将文本结果转化为JSON对象° 除了这些, h忱px还有-些基本用法也和requests极其类似’这里就不再赘述了,可以参考官方文 档: h叮s:〃wwwpythonˉhttpx.org/qujckstart/° 4〔1ie∩t对象

httpx中有_些基本的API和requests中的非常相似’但也有_些API是不相似的,例如httpx中 有一个〔1je∩t对象,就可以和requests中的5e55jo∩对象类比学习。 下面我们介绍〔1ie∩t对象的使用。官方比较推荐的使用方式是"jtha5语句’示例如下: j呻orthttpx

"jthbttpx.〔1je∩t()a5〔1je∩t: Ie5po∩5e=〔1je∩t。8et(,bttp5;//栅.httpb1∩.org/get|) Prj∩t(re5po∩5e)

运行结果如下: (∩e5PO∩5e[2""]〉

这个用法等价于: j呻OrthttpX

〔1je∩t≡httpx。〔1je∩t() try:

re5po∩5e≡〔1je∩t.get(!‖ttp5://哪.‖ttpbi∩.or8/get‖) 「i∩a11y: 〔1ie∩t.〔1oSe()

两种方式的运行结果是—样的’只不过这里需要我们在最后显式地调用C1o5e方法来关闭〔1je∩t 对象°

另外,在声明〔1je∩t对象时可以指定一些参数’例如∩eader5’这样使用该对象发起的所有请求 都会默认带上这些参数配置,示例如下: 1呻Ort∩ttpX

ur1= |∩ttp://硒。httpbi∩.or8/∩e己der50

∩eader5={,U5erˉ吧e∩t! ; ’‖∏yˉapp/o.0.1!} "ith∩ttpx.〔1je∩t(header5=∩eader日) a5〔1ie∏t: r=〔1ie∩t.get(u】1〉

pri∩t(r.j5o∩()[′headeI5!][‖05erˉ帕e∩t|])

这里我们声明了一个header5变量’内容为05erˉ∧ge∩t属性’然后将此变量传递给∩eader5参数 初始化了_个〔1ie∩t对象,并赋值为C1je∩t变量,最后用C1ie∩t变量请求了测试网站’并打印返回 结果中的05erˉ∧ge∩t的内容: 呵ˉ己pp/0.0·1

可以看到, header5成功赋值了°

关于〔1je∩t对象的更多高级用法可以参考官方文档: https:〃www.pythonˉhttpxorg/advanced/。 5.支持卜|∏丁p/2.0

现在是要在客户端上开启对HTTP/2.0的支持,就像“基本使用,’小节所说的那样,同样是声明



第2章基本库的使用

78

〔11e∩t对象,然后将∩ttp2参数设置为丁rue’如果不设置’那么默认支持HTTP/l.l’即不开启对 HTTP/20的支持°

写法如下:

re5po∩se=〔1je∩t.get(!http5://www.httpbi∩。org/get|) pri∩t(re5po∩5e.text) pr1∩t(reSpO∩5e。http-veISjO∩〉

这里我们输出了re5po∩5e变量的http-`′ers1o∩属性’这是requests中不存在的属性’其结果可 能为:

|(

1∏pOrt∩ttpX 〔1ie∩t≡httpx。〔1je∩t(http2≡丁rue)

"‖『丁p/1·0"’ "‖丁丁p/1.1"’ "‖丁丁p/2".

这里输出的∩ttp-γer51o∩属性值是HTTP/2’代表使用了HTTP/20协议传输。

这得客户端和服务端都支持HTTP/20才行°如果客户端连接到仅支持HTTP/l.l的服务器’ 那么它也需妥改用HTTP/l。l。

6.支持异步请求

h肮px还支持异步客户端请求(即AsyncClient)’支持Python的async请求模式’写法如下:

a5y∩〔de「+et〔h(l』r1): a5y∩〔wjt∩http×.A5y∩c〔1je∩t(∩ttp2=丁Iue) 35c1ie∩t: re5po∩5e=aw己it〔1ie∩t.8et(ur1) Prj∩t(Ie5po∩5巳text) j十

∩a爬≡!

阳j∩

0 ;

a5y∩cio.8etˉeγe∩t—1oop().ru∩ˉu∩tj1ˉ〔o『∏p1ete(千et〔∩(!bttp5://硼°httpbi∩.oIg/get|))

关于异步请求, 目前仅了解_下即可’后面章节也会专门对异步请求进行讲解。大家也可以参考 官方文档: hhps://wwwpythonˉh忱px.oIg/async/° 7.总结

本节介绍了h仗px的基本用法’该库的API与requests的非常相似,简单易用,同时支持HTTP/20, 推荐大家使用°

本节代码参见: https://gjthuhcom/Python3WebSpideI/HttpxT℃st。

25基础爬虫案例实战 我们已经学习了多进程、requests、正则表达式的基本用法,但还没有完整地实现过—个爬取案例。 这一节’我们就来实现_个完整的网站爬虫,把前面学习的知识点串联起来,同时加深对这些知识点 的理解°

↑.准备工作

我们需要先做好如下准备工作。

□安装好Python3’最低为3.6版本’并能成功运行Python3程序° □了解Python多进程的基本原理°

|」■】|||」●」||‖·口·∏」■Ⅷ]』■刮《·‖■‖』■‖』□】·`□】|」■列■□|(‖■可■司」■ˉ』』□|』』■□|』■■‖」□√‖|‖

mpOrthttpX mportasy∩c1o

■■】■■』■||■■‖』■■‖|』』■勺

注意在客户端的httpx上启用对HTTP/2D的支持并不意味着请求和响应都将通过HTrP/20传输’



2.5基础爬虫案例实战

79

| {■「●『‖‖|△■了『『





□了解PythonHTTP请求库requests的基本用法° □了解正则表达式的用法和Python中正则表达式库re的基本用法° 以上内容在前面的章节中多有讲解’如果尚未准备好’建议先熟悉-下这些内容。



甘[}巴■‖匹尸『|『尸

2爬取目标



本节我们以-个基本的静态网站作为案例进行爬取,需要爬取的链接为https://ssr1scmpe.center/’ 这个网站里面包含—些电影信息,界面如图2ˉl4所示。 ● ·』■≈|=

.】◆







+啡◎





●■●‖

β|′|β

回s.…

膨琶琶………

9.5 由



偏J叫吁=■≈··` 、喻°尸

!

翱妒

.v, 。.0 .口





型=b浑.=垄

′ ■卜▲■『■■「■■■▲■『■■厂

歌鲜…

9.5 ●●寸●



问中刃m爪.沛●5h渺琳…回

g。5

■■■■





●●●

…‖ 0凹… ~■∏■

=↓‖{{■胀龟〖

『■『{|■∩‖■「■〗厂■■厂■■『△■『■尸‖■■尸卜巴=■■■■厂

凹坦旦克G.V…比

9镶5

●●●

寸G

Q

■′和七

案例网站的页面

图2-14

邑,其中每部电影都包含封面、名称、分类、上映时间` 网站首页展示了一个由多个电影组成的列表,其中每部电影都包含封面、名称、分类、上映时间`

评分等内容,同时列表页还支持翻页’单击相应的页码就能进人对应的新列表页°

如果我们点开其中一部电影,会进人该电影的详情页面,例如我们打开第一部电影《霸王别姬》, 会得到如图2ˉl5所示的页面° 黔偶` . "趟

■≈■





●◆■●0

回武′呻° ■王别擅ˉ尸…】陶…烟

9·5 ◆古合●■

■■0

呵切必止■

■■卜■∩

|m问介

■■∏|‖▲■「|‖卜

王刮罐 删』 _



臣w份=出0■迅=?=■…自伞八蜘.…任■堑●”● …酗们l■●●■『◆……伯p■.叼7』T闻巴■″向冈

■′■P≈宁■′ .…,

0…扮牟巴T■=疆■【m0 」T

■●出∏u●■■尸…●?仁:…ˉ■门∧…A七赁

F唾▲■0●份….T■F“人■乓御伊巴q“叭 qP至向■七…Tmmh■●酗■…蛔…■ 早.■`m』≈ym■宙 . □■二■‘■暑■》毋…… ■…■■蹿叮萨●牵=

> P41吧■■■●■‖ ●■c0●0p●0由Oβ孕々凸■■●白"

F■■V沪出p▲▲b0

您斟…嚣辫

古…沁础●·■

‖「■≥『‖『

|导澳

▲■尸□■■■厂「|■厂|‖『

=睁

图2ˉl5打开《霸王别姬》呈现的页面 ●■0





…中mⅪ′l∏劣钾



O

"霹

●‖■■





2 .

第2章基本库的使用

80

这个页面显示的内容更加丰富’包括剧情简介、导演、演员等信息° 我们本节要完成的目标有:

□利用requests爬取这个站点每一页的电影列表,顺着列表再爬取每个电影的详情页; □用正则表达式提取每部电影的名称、封面、类别`上映时间、评分、剧情简介等内容; □把以上爬取的内容保存为JSON文本文件; □使用多进程实现爬取的加速°

已经做好准备’也明确了目标’那我们现在就开始吧。

3.爬取列表页 第—步爬取肯定要从列表页人手,我们首先观察一下列表页的结构和翻页规则°在测览器中访问

hnps:〃ssrl.scrape。center/’然后打开测览器开发者工具,如图2ˉl6所示° b

×‖十

· ·‖困s…|… ◆÷G



●m『0………加…

郧☆

撂兰二目羔二二d黑罢』 …



ˉ

… …—



乃刽●



|『 .

{赘赋.陶』`.倾′c……





| ″暂 h ″ ;. = 臼田≈…。≡ˉ盂占…■矿瓦舜吕·ˉ乙……匡叼疆司←h…- ˉˉ=_←_— ●窗 @ i x …匹…≥ ≡… -山`斗……………、…~◎ |

重=≡≡≡贾贾≡≡|

;慧隐=

O■1■凹t◇…7…〔`m■寸●lˉ…■ 』●□■磕丁翱b≈臼/O』味

■…·呵妇《

△》

审。“丁由k→7尸■■Ⅷ』小=0…=庐 守●●』■…→7…■咽宅…t仿■1=西T

·』0←!由t→y?≈Q■『 《

8 8齿?O淀

庐′●■21

■…■S…凸

—==-__-

■■d』vmh■←γ它□叼【u■■←●1<●I●1■■l=凹·1■m`屯V?”【=】●`

ˉ

≡—ˉ

■0户∏吕=·

ˉ—-—_一} .>? {

T域毗v◆竹片7严寸帕dmL句山.●1…曹∑

0

P

可叼■吟付■『7哗四■矿色■>№1■■〖〃■■=‖0…Dˉ■U…0■』…△ˉ←▲

B≡…仇1■1铲c…■≥≡二 二『ˉ甘

…白亿■6邑■■■丘U

惋『山厂~呵』■』b啤E】

√砂 》

●叼l■凸t←←稻1凹c…■■q.叶●1…1●0屯●l=N●l芭〖=包种■l面P=■◆l幸0l←帕白·

°■l=■《

●口■■呛~y…四晒田…J=·■0Ⅱ』`坤c`■●≈■…■、

…ˉE∑■2啤涸

_钙

鳃山R■七ˉ7…四〖山●F′→w■卫醉■p●…u叶〔…1■√凶D

■P由『吕O毕■●`■铭=门; m七叼「…l●『目 (A田"』

√P

…厂7l…●仪…β 巳O1●7【二午印0m!

β姆』U●l≥卜了…山【心■F,.唾宇…=·■</■1吵 ◆d1V幻m→γ0■〗■【l≈←r…m↑●~=必』广

— 【…皿1唾』0 ■助;

尸每d』●山m动=7≈■帕〖…T…它皿萨■■J■汕≥ </■八加



守呵1●■?■宇0…】T鸥咀怎b≈S■,.●l<回【■Ⅱ≤0【≡才qe1七O1■钝乙■9●l<●l■=3吼≈Pt=二沪妒 争山t0←w唾四【m…■■盯>r■←b琶■h9°5≤″

0dv《

中“■≥衫=γ唾皿D心′户

■…=?∏fγ【≈…?

山”l叮:D…■r

h吗』yˉ虾≥十U回努呵岭.·【心7·fL≡ml…、O·刀.…≥U·l■mt·门■→·l曰…O。·$匹疗‖. —二==-●=



■■=

≡≡ 寸9. 〔『ˉ

ˉ 巳崇■ 卓■ 二:

耐……=^■≈凸…~?■…O企…ˉ ‖…「 ~≡ 二≡ ~ 二-f



蜘……△…

=→■=≡=

~0■D←≡全0

●一■n



■■早巴明≈必泻宰=

0尸D

乙闪q

可→写

●←函∏ 『

●●■■●令■■■

图2ˉl6列表页及其HTML

观察每_个电影信息区块对应的HTML以及进人到详情页的URL’可以发现每部电影对应的区 块都是_个div节点,这些节点的〔1己5S属性中都有e1ˉCaId这个值°每个列表页有l0个这样的div节

‖||

,G(…叼o .■l@旧p ·●{■凹1叼β ■l=■中『 忽·●1…「0 ··〖战≈M帕…寸《

7●…函≈7…鸥h门卜~/□1■』M1句[`m■D

√已』卜

●B钻=护≈一已■奎

囱凸E】U8■…

…妇=■mk…↑…代■亿;

::■↑@m

■<▲v山t■←y…皿t山■芦·0<■飞■l<■l由10●l迂●1■协…G1七●l~■1文■〖□√尸·

点,也就对应着l0部电影的信息。

接下来再分析_下是怎么从列表页进人详情页的’我们选中第-个电影的名称’看下结果’如



图2ˉl7所示。



2.5基础爬虫案例实战 ■







迅咱



_

斡涉吗—









≡≡



—回





●ˉ

■★力司●自

··■?鲤…疟…「′p吧叫‖

〈. 蚕》G

8l

同5口·p·

儡 镀下曾」`p 瓣A酗

中…n中m础′|γ`箩w ‖w…出上■



链…-怎;ˉ≡阳?§.

7

.′ 。; ×

≡-蔼盂尸—一甸_—』…

阅—亨 =

啥≡…睁-…~■

鲍■0…■■^

=_--一-

_

G…~守邑

『}β}}



「=_-_

p…=

vmv』▲知… ◆叼B■■◆←琶■拦≡硒尸…垮…7~尸』■■≥

ˉ·时γ℃《

·l‖ 》

●αv■←←∏m四屿…】 ●≤』■=7…■O兰甲…二巳=弓扩.



0■■ 眺■+●回

=?Q巳2D…,

°→≈《 ■『0且≥韩0B■『 l学01…飞团?0

8吕…■门

F烯』v…古p二僻扫仁l■…≈●‖…l●l≈l牵劝●t钢1…呵矿》

◆<hp■醉7程?筑巳四…·1<■叼10■≥《皿…寸.←. 二



叮=

■『

·■∏■Z…℃1=儿…D

■巳

傀≈广…盯?’l·…『

山”k叮:D…』

●<』■…T十Vp…四cl…■‖·1≡立

…=■=『 Ⅲ=P

·2m…

……≈■■…『 0…′ …■◆1“■=』·…/ …◇Ⅱ■M…四08酶』 …←堑阜晌β

宁山v…T碎O%〔{二二吉二●〖…【●1如l=狸●l屯l…■`面`≡■l…0=肆。

竹■…仔←…m◇们仁口空T坠』M丫〔…】 `…≈岭←7…』■w●TM≈且〃■.凹~=≡≡…奄·▲

0~…■业■c…苛…

′■…f■酗

气J卜



√“厂

Q巴1v凹喻>卞T画1仍C蝉F钞产白●`<Dl●l≤Ol鄂●1=四l■m→■l·ml年冷凹G1…睁吗吭≡

……■-

=…·…‘…鸿.‘…=

·厂→-= …6 1=g00



……布Bq=00…

0mv…≤.…■适皿←斗m…7…●>=… |■■■「[||口■■厂‖『‖『||■■『『‖‖|||△■厂‖|||坠■「|||巴厂『|「||丛■■■『『【【■厂}|

》匈扣……c…→幻皿标→些→



p叫v△>守=庐饲仁…●面<m门■>≥幻吟

●【…昏{…『

√0』P

…亩…0■励《·…↑

— 〔镭』■啼/

守吧加…少←″丛U佣e匹F袒Q‖导田`巴`…l←2q●1…l…●1≤@l■■901…1…‘●



≤山寸>←7…四〔山锣≈味o摊←t□>田产·p…



……

图2ˉl7选中《霸王别姬》的名称

可以看到这个名称实际上是_个h2节点’其内部的文字就是电影标题。h2节点的外面包含_个a 节点,这个a节点带有hre+属性’这就是一个超链接’其中hre+的值为/deta11/1’这是_个相对网 站的根URLht1ps://ssrl.scrapecenter/的路径’加上网站的根URL就构成了ht卯s://ssrl.scrapecente∏

detaiⅡl’也就是这部电影的详情页的URL°这样我们只需要提取这个∩re千属性就能构造出详情页的

[■尸‖|||}■■「‖|||■■「『||巴■厂||||)■厂『‖||止■∏「|上■『■『||

URL并接着爬取了。

接下来我们分析翻页的逻辑’拉到页面的最下方’可以看到分页页码’如图2ˉl8所示° 鼻罐

·· ■=》= +个G



嗽喻嚣

簿古■●

△审↑ --

=■■■唾凶 ■



■…□=■→

■=

.一

9°5 ●●●●Q

《翻醋…… }……

9嗡o ●◆◆●

0

~—=迂工三二二二二



9,o

驴…

● ● ◆ ●

·

…■恩 0 ‘ 0 ‘ ’ c · 岭 0

图2ˉl8分页页码

82

第2章基本库的使用

可以观察到这里_共有l00条数据,页码最多是l0° 我们单击第2页’如图2ˉl9所示。 } .·●阮≡三 ◆◆◎

∑l

●■0一-■

h●■●

回s。牢. 硒哪ˉv估啥乙

累里′民●

8,9 ◆台●●

O

=c愁Ⅶm &卓



少年…■■■迫.L比的∩

●●●

8.9 台●■●

■■、…■■…父J0扛西





…V0辑邱



羹■心贝.A……』栏

(■●●

8·8 ●

■′1■函





●●亡■



…l小〗D】■

0[

日山 -二二≡二二←

酶■这件′』叼ˉd…出…缸..愉

图2ˉl9第2页的内容

可以看到网页的URL变成了https://ssrl.scrape.centcⅣpage/2’相比根URL多了/page/2这部分内 容°网页的结构还是和原来—模-样’可以像第l页那样处理°

接着我们查看第3页、第4页等内容’可以发现_个规律’这些页面的URL最后分别为/page/]` /page/4。所以’/page后面跟的就是列表页的页码,当然第l页也是_样,我们在根URL后面加上

/Page/1也是能访问这页的,只不过网站做了一下处理,默认的页码是l,所以第一次显示的是第1页 内容°



d

q



‖ 0





好’分析到这里,逻辑基本清晰了。

q

于是我们要完成列表页的爬取,可以这么实现:

0

□遍历所有页码’构造l0页的索引页URL; □从每个索引页’分析提取出每个电影的详情页URL° 那么我们写代码来实现一下吧°

首先’需要先定义—些基础的变量’并引人一些必要的库,写法如下:





{ ˉ‖



mportreql』e5t5 j呻ort1oggi∩g mportre

「ro|∏0r11jb。parse1"portur1joi∩

8∧5[0R儿≡ ‖http5://55r1.5〔mpe·〔e∩ter0 「0「∧[p∧C[=10

这里我们引人了requestS库用来爬取页面` logging库用来输出信息` re库用来实现正则表达式解 析` ur1joj∩模块用来做URL的拼接°



』】]』』■■‖』■■‖||□日』■司||■■】‖‖|』』■|】』■】||‖‖·」■■〗‖」司□‖纽‖|』口』■可‖』

1ogg1∩g.basjC(o∩+1g(1eve1=1o881∩g.I‖「O’ 十Omat=′咒(aS〔tj爬)B ˉ兜(1eve1∩a爬)5:%(爬55age)5‖)





25基础爬虫案例实战

83

接着我们定义了日志输出级别和输出格式’以及8∧5[0R[为当前站点的根URL’丁0丁∧[p∧C[为 需要爬取的总页码数量∩

完成了这些工作,来实现_个页面爬取的方法吧,实现如下: de千5〔Iape一page(ur1): 1Oggi∩g.j∩十O(|5CraPi∩g%5…’ Ur1) tIy:

Ie5po∩5e=reque5t5.get(ur1) i千re5po∩se·5tatu5〔oOe==2OO: retur∩re5pO∩5e.text

1oggi∩g.erIoI(!get1∩γa1id5t己tus〔ode%5wM1e5cmpi∩g%5』’





| 「′



Ie5po"5e.5tatu5code’ u【1) ex〔eptreq0e5t5.Reque5t[x〔eptjo∩;

1oggj∩8.eIIor(‖erroro〔〔uIred"∩j1e5〔mpj∩g%5‖’ ur1’ exC1∩fO=『me)

考虑到不仅要爬取列表页’还要爬取详情页’所以这里我们定义了_个较通用的爬取页面的方法’

叫作5crapeˉpage’它接收一个参数ur1,返回页面的HTML代码。上面首先判断状态码是不是200’ 如果是’就直接返回页面的HTML代码;如果不是,则输出错误日志信息°另外这里实现了】equests的 异常处理如果出现了爬取异常’就输出对应的错误日志信息。我们将logging库中的error方法里 的excj∩+o参数设置为丁rue,可以打印出『mceba〔促错误堆栈信息°



好了’有了5〔rape-page方法之后’我们给这个方法传人—个ur1’如果情况正常’它就可以返 回页面的HTML代码了。

在S〔rapeˉp己ge方法的基础上,我们来定义列表页的爬取方法吧,实现如下: de十5cmpeˉj∩dex(page); j∩dex0I1=十,{8A5[-0只L}/pa8e/{page}| retur∩5Cmpeˉpage(i∩dexur1)

卜|

方法名称叫作5〔rape-j∩dex’这个实现就很简单了’这个方法会接收-个page参数,即列表页的 页码’我们在方法里面实现列表页的URL拼接,然后调用5〔Iape—page方法爬取即可’这样就能得到 列表页的HTML代码了°

获取了HTML代码之后,下一步就是解析列表页’并得到每部电影的详情页的URL’实现如下: de+paI5e-j∩dex(htⅦ1):

patteI∩=Ie.coⅧp11e(’〈己.*?∩ref≡闯(.*?)"。*?〔1己s5二"∩a|∏e"〉0) jt咖S=re.+j∩d311(patter∩’ htⅦ1) b



i十∏Otite|∏5:

Ietur∩ [] +orjte∏| j∩1te‖5:

detaj1‖r1=uI1joj∩(8∧5[0Rt’1t印)

1oggj∩g·i门fo(0getdet日j1ur1%5′’ det己j1ur1) yje1ddetaj1ur1

这里我们定义了p日r5e一j‖dex方法’它接收一个参数ht们1,即列表页的HTML代码°

在parSeˉj∩deX方法里,我们首先定义了一个提取标题超链接∩re于属性的正则表达式’内容为: <日。*?bre+=啸(。*?)".*?C1aS5=』』∩田肥阐〉

其中我们使用非贪婪通用匹配.*?来匹配任意字符,同时在‖re+属性的引号之间使用了分组匹

配(.*?)正则表达式’这样我们便能在匹配结果里面获取href的属性值了。正则表达式后面紧跟着 〔1a55≡"∩a阴e||’用来标示这个〈a〉节点是代表电影名称的节点°

现在有了正则表达式’那么怎么提取列表页所有的‖re十值呢?使用re库的十1∩da11方法就可以

了,第-个参数传人这个正则表达式构造的patter∩对象,第二个参数传人ht"1,这样十1∩da11方法 便会搜索‖t们1中所有能与该正则表达式相匹配的内容’之后把匹配到的结果返回,并赋值为jte川5°



如果1te‖5为空’那么可以直接返回空列表;如果iteⅧ5不为空’那么直接遍历处理即可。

遍历1te"5得到的1te"就是我们在上文所说的类似/deta11/1这样的结果°由于这并不是-个完

整的URL’所以需要借助ur1joj∩方法把8∧5[0R[和hre十拼接到一起’获得详情页的完整URL’得 到的结果就是类似https://ssr1.scrapecente∏detaM这样的完整URL’最后调用yie1d返回即可° 现在我们通过调用par5ei∩de×方法,往其中传人列表页的HTML代码,就可以获得该列表页中 所有电影的详情页URL了°

接下来我们对上面的方法串联调用_下’实现如下: de于们aj∩():

+orpage1∩ra∩ge(1’「O丁A儿p∧6[+1): i∩dex‖tⅦ1=5〔rapeˉi∩dex(p己ge) detai1ur15≡par5ei∩de×(i∩dex门t"1)

1oggi∩g.i∩于o(,det己i1ur15‰‖’ 1j5t(det3i1ur15〉) ∩a『爬=

j+

∏`a1∩



∩m∩()

这里我们定义了Ⅷ己1∩方法’以完成对上面所有方法的调用。‖a1∩方法中首先使用m∩ge方法遍

历了所有页码,得到的page就是1ˉ10;接着把page变量传给5cmpeˉ1∩de×方法’得到列表页的HTML; 把得到的HTML赋值为j∩dex∩t刚1变量°接下来将j∩dex∩tⅧ1变量传给paI5ej∩dex方法,得到列 表页所有电影的详情页URL,并赋值为detaj1ur15,结果是—个生成器,我们调用115t方法就可以 将其输出°

运行_下上面的代码’结果如下: 202OˉO3ˉO822:〕9:5O’5O5 ˉ I‖「0; 5〔r己p1∩g∩ttp5://55I1·5cmp巳〔e∩ter/page/1… 202OˉO〕-O822;39;S1’9q9ˉ I‖「0: getdetaj1ur1bttp5://55r1.5〔rape·ce∩ter/detaj1/1 2o2oˉo3ˉ0822弓39:51’950ˉ I‖「O: getdetaj1ur1http5://55r1ˉ5〔mpe°ce∩ter/det己i1/2 2020ˉO3ˉ0822:39;51,950ˉ I‖「0: getdet3i1ur1http5://5sr1·5cmpe·〔e∩ter/deta11/〕 2o20ˉo3ˉ0822目39:51』950ˉ I‖「0; getdetaj1‖r1‖ttp5『//55r1·5crape.ce∩ter/det己j1/4 2020ˉo〕ˉ0822:39:51’950ˉ I‖「α getdet己i1ur1http5;//55r1°5〔rape·ce∩ter/deta11/5 2O2Oˉ03ˉ0822:39:S1’95Oˉ I‖「0: getdetaj1ur1∩ttps;//55r1·5〔rap巳ce∩teI/detaj1/6 2o20ˉ03ˉ0822;39;51’950ˉ I‖「0; getdet日j1ur1∩ttp5://5sr1.5〔r3pe·〔e∩ter/detaj1/7 2o20ˉo3ˉ0822;39:51’950ˉ I‖「0; getdeta11ur1∩ttps://55r1。5cr己pe·〔e∩ter/det己11/8 2o2oˉo3ˉ08∑2:39:51’950ˉ I‖「0: getdet日i1ur1http5目//55r1·5〔rape·〔e∩ter/det己j1/9 2O20ˉo]ˉ0822:39:51’95Oˉ I"「O: 8etdeta11ur1∩ttp5://55r1°5cIape。〔e∩ter/detaj1/1O 2020ˉO3ˉO822:39:51’951 ˉ I‖「0: detai1ur15 [‖http5://5sr1.5〔mpe.ce∩ter/detaj1/10 ’ `∩ttp5://5sr1·5crape。〔e∩ter/detaj1/20 ’ ′∩ttD5;//55I1·5〔r己pe。ce∩ter/deta11/3‖’ 0https://55r1.5〔r日pe。〔e∩ter/detaj1/40 ’ |∩ttp5;//55r1°s〔mpe。〔e∩ter/deta11/5! ’ 0∩ttp5://5sI1。5〔mpe°〔e∩ter/detaj1/60 ’ 『http58//s5r1.5〔rape.〔e∩ter/detaj1/7』’ !http5://55r1。5〔rape。〔e∩ter/detaj1/80 ’ |http5://s5r1.5〔mpe·ce∩teI/detaj1/9‘ ’ 0http5://s5m.5〔mpece∩ter/detai1/1o` ] 2O2O-0〕ˉ0822:39:51’951 ˉ I‖「O; 5〔rapj∩g∩ttp5://55r1。5〔r己pe。〔e∩teI/p己ge/2… 202oˉ03ˉ0822:39:52′842 ˉ I‖「O: getdetaj1ur1http5;//55r1。5〔mpe°〔e∩ter/det日11/11 2020ˉ03ˉ0822;39目S2’842 ˉ I‖「α getdet己11ur1https://55r1.5〔rape·〔e∩ter/deta11/12 ●



p

输出内容比较多’泣里只贴了一部分°

可以看到’程序首先爬取了第l页列表页’然后得到了对应详情页的每个URL,接着再爬第2页、

第3页,一直到第l0页’依次输出了每一页的详情页URL°意味着我们成功获取了所有电影的详情 页URL。

4爬取详情页

已经可以成功获取所有详情页URL了’下-步当然就是解析详情页’并提取我们想要的信息了。 首先观察一下详情页的HTML代码,如图2ˉ20所示。

】‖』□■】可|』■】』‖』』|·〗{■■』■‖』■】】|」■】Ⅵ‖』■■‖‖|■■】|』■■』|‖■■|‖(■■《』■■‖|■■□=■可」■】纠‖」=■■{{|■=■·』■■·、■■■】■可=■■□■=■司|。‖二■ˉ■|■■〗■|‘』■

第2章基本库的使用

84



2.5基础爬虫案例实战







电 ■

·









回s厨…

_=



●—§

_导≡

85



己抖=



冲~

p





} ■■[「}‖‖‖巴尸尸■庐■巴∩■■「{‖|『[□■■■α■【■■『尸{‖‖■■■■■■|』巴尸{‖■■■『》△■■▲尸■尸■■尸|匹『■■■■’■■『■■■『■尸■∩{‖■=■■‖『▲∩β■●■′‖仍}|■■尸「↑巴■=厂■尸‖|■广「||

_琶画≡=…≡三忌.旦望≡』 .· 鲤●ˉ乞℃″→■■■■=…【0询…d…

q专

O<』U…=】专`■T些…m白沁 寸……●吵怯…心磋°■0…尸c■…℃l<……叮●l…←西飞′P‖≈~■■1ˉ· ~■/·≈D q′硅仓=

◆…呻巫≥叮ˉw靶1■厢广m…→乙……吼…南=y·l…雷≈叮吼ˉ-.→…′′

`′.=…≈ˉ…■

■=淘油《

—l ˉ←=、■→ 审 宁■

●》 .广■《 …野归∩2…『…∩呵; ≈哼…吕 D≈▲?空=…q



=●■■~…≡回◇J磅 ……忻…/√←户

■坦加∑m』

■●l=l电·〖

…●乙←F■诊1Ⅶ牌/∏=二ˉ



√■8唾

内∧w

酶′巳凹』〗■1 =乓·■且Tl们

←绣

!Fγ=.一0m1罕】《 ?…『■穴↓

7□』世……ˉ”的▲o■〖lm″′…■■但他尸h ·……←T…匹l…P■蚂堡J啥

=≈°°凹也



^…α$●■〗■·c~≈‖■匹…■俏‖ Pp●P″β ∩●l≈●d→凹《

√~岭′中■ 堂会巴=≡、

q√m←

v<凹■t■→7晒二Q=色lm小曰→■虹幻曰宁

』■=“宁厘 宁一

≡宁~G?■门】

_ ■■≤皿幻p凸冶~;

◆■lv■>=≡cB=二沪唁户 ●型#『◎←………



a尸口…◇≈=刷……●必………□=■·v■≡凸磕◆■…=白辑ˉ…▲≈·=≈山◆…严p上』←.·,ˉ→

…′啼…’…

图2ˉ20详情页的HTML代码

经过分析’我们想要提取的内容和对应的节点信息如下°

□封面:是一个jⅦg节点’其〔1a55属性为coγer° □名称:是-个h2节点,其内容是电影名称。

□类别:是5Pa∩节点’其内容是电影类别°5pa∩节点的外侧是butto∩节点,再外侧是〔1a55为 c日tegorje5的djγ节点。 □上映时间:是5Pa∩节点’其内容包含上映时间’外侧是〔1a55为1∩十o的d1γ节点°另外提取 结果中还多了“上映,,二字,我们可以用正则表达式把日期提取出来。

□评分:是—个P节点,其内容便是电影评分。P节点的c1a55属性为5core° □剧情简介:是_个P节点,其内容便是剧情简介,其外侧是〔1a55为dra∏a的d1v节点。 看着有点复杂吧’不用担心’正则表达式在手’我们都可以轻松搞定° 接着实现一下代码吧°

我们已经成功获取了详情页URL’下面当然是定义一个详情页的爬取方法了’实现如下: de「5cmpe-det日11(uI1): retur∩5〔rape-page(ur1)

这里定义了一个5〔rapeˉdetaj1方法’接收_个参数ur1’并通过调用5〔mpeˉpage方法获得网页

源代码。由于我们刚才已经实现了5〔rapeˉpage方法’所以这里不用再写一遍页面爬取的逻辑,直接 调用即可,做到了代码复用。

另外有人会说’这个5〔rapeˉdeta11方法里面只调用了5cmpeˉpage方法’而没有别的功能,那 爬取详情页直接用5〔mpeˉpage方法不就好了’还有必要再单独定义5crape-detai1方法吗?有必要, 单独定义一个5cmpeˉdeta11方法在逻辑上会显得更清晰,而且以后如果想对5〔rape-detaj1方法进行 改动’例如添加日志输出`增加预处理’都可以在5crapeˉdetaj1里实现,而不用改动5〔mpe—page方 法’灵活性会更好°

好了’详情页的爬取方法已经实现了,接着就是对详情页的解析了,实现如下:

第2章基本库的使用

de「p己Ⅲ5e-detaj1(ht∏1):

〔oγeI-p己ttem≡re.〔O∏m1e(!〔1a55=·iteⅦ.*?<j吧.*?5r〔≡.(。*?〉阐。*? c1355=口coγer闰〉|’ re·5)

∩a爬-patter∩霉re。〔α‖pi1e(0〈h2。*?)(.*?)</‖2〉0)

categorje5-patteI∩=re.cαmi1e(,〈butto∩.*?〔ategoIy·*?〈5pa∩〉(.*?〉 </5pa∩〉.*?</b0tto∩〉|’ re.5)

pub1iShed=atˉpatteI∩=re.Cαmi1e(!(\d{4}ˉ\d{2}ˉ\d{2})\s?上映0) dra∏E-p日tteI∩=re·co呻i1e(’〈diγ.*?dra丽a。*?〉.*?〈p.*?〉(。*?〉</p〉‖’ re.S)

5core-patter∏≡re.〔o‖m1e(,〈p.*?5〔oⅢe.*?〉(.*?)〈/p〉|’ r巳5〉

Coγer=Ie。5ear〔h(coγer-Patter∩’ htⅧ1).grouP〈1)。5trjP() i于re. 5ear〔h(〔oγer一patteⅢ∩’ ht‖1)e15e‖o∩e ∩a′∏e=Ie.5e己r〔‖(∩a眠ˉp己tter∩’‖m1).8roup(1).stIip() ifre. 5e3rch(∩己爬-p3ttem’∩t川1)e15e‖o∩e

categor1e5=re.十i∩da11(〔ategorje5ˉpatter∩’ htⅦ1) j十re. 「i∩da11(〔ategorie5一pattem’ htⅧ1)e1Se []

pub1j5hedat=re.5e己rch(pub1j5‖ed-己t-patter∩’ ht丽1).group(1) i千re. 5ear〔h(pub1j5hed-at-p己tter∩, hm1)e15e‖o∩e dra帕=Ie.se己r〔h(dm帕=p己tter∩′ 们m1).8rol』p(1).5tIip() j+Ie. 5ear〔∩(dra归ˉpatteI∩’ ht爪1)e1se‖o∩e 5〔ore≡f1o己t(Ie。searc∩(5〔o】e-patter∩’‖tⅧ1).gro0p(1).strjp()〉 1f re。5earc∩(5coreˉP3tter∩’∩t"1)e1se‖o∩e



retur∩{ ‖COγer0 : 〔oγerD 0∩a爬0 吕 ∏a爬D

|categorie5` ; 〔ategoI1e5D !pub1j5∩ed-at{ ; pub1i5hedat’ 0dra阳0 8 dra爬’ 05COre0 : 5CoIe



这里我们定义了parseˉdetaj1方法’用于解析详情页’它接收_个参数为∩t刚1’解析其中的内容’ 并以字典的形式返回结果°每个字段的解析情况如下所述°

□coγer:封面°其值是带有cover这个〔1a55的1Ⅶg节点的Sr〔属性的值所以5r〔的内容使用 (.*?)来表示即可’在mg节点的前面我们再加上_些用来区分位置的标识符’如1te"°由于 结果只有一个,因此写好正则表达式后用5ear〔∩方法提取即可。

□∩aⅧe:名称。其值是h2节点的文本值’因此可以直接在∩2标签的中间使用(.*?)表示。因为 结果只有一个,所以写好正则表达式后同样用5eaIC∩方法提取即可°

□categorje5:类别°我们注意到每个category的值都是butto∩节点里面5pa∩节点的值,所以

0』





□‖‖】■■】】【‖』勺‖|』●引』‖|||』■■‖‖]‖■可‖||□勺]|‖

写好表示butto∩节点的正则表达式后,直接在其内部5pa∩标签的中间使用(.*?)表示即可° 因为结果有多个,所以这里使用+i∩da11方法提取,结果是_个列表°

□pub1i5hedat:上映时间。由于每个上映时间信息都包含‘‘上映,’二字’日期又都是—个规整的 格式,所以对于上映时间的提取’我们直接使用标准年月日的正则表达式(\d{4}ˉ\d{2}ˉ\d{2}) 即可°因为结果只有-个’所以直接使用Sear〔h方法提取即可°

□draⅧa:直接提取c1a55为draⅧa的节点内部的p节点的文本即可’同样用5ear〔h方法提取° □5〔ore:直接提取c1a55为5〔ore的p节点的文本即可,由于提取结果是字符串,因此还需要 把它转成浮点数’即「1Oat类型°

上述字段都提取完毕之后,构造_个字典并返回。

这样’我们就成功完成了详情页的提取和分析。

最后,稍微改写一下阳i∩方法,增加对5crapeˉdetaj1方法和parse—deta11方法的调用,改写如下: de千阳j∩():

千orp己ge1∩ra∩8e(1’『0『∧lP∧C[+1): 1∩de又∩tⅦ1=5cr己peˉj∩dex(Page) detaj1~ur15=p己r5eˉ1∩dex(i∩dex∩t∏1)

■■‖』』■可】γ』■】||‖|』■|』|{』■‖』』■】_■】□】『■】||□]司‖‖‖■】□



86







2.5基础爬虫案例实战 +ordet己11ur1 j∩detai1ur15;

detaj1ht们1=5〔r己pedeta11(det日11-ur1) data=par5edetaj1(detaj1ˉ∩t们1〉 1ogg1∩g.i∩「o(‖8etdeta11d己ta%5! ’ data)





|「

87

这里我们首先遍历deta11ur15’获取了每个详情页的URL;然后依次调用了5crapeˉdetaj1和 par5e-detaj1方法;最后得到了每个详情页的提取结果’赋值为data并输出。



运行结果如下:





2O20ˉ03ˉ082〕;37:35’936ˉ I‖「0: 5〔mpi∩g∩ttp5://55r1.5〔rape·〔e∩teI/page/1… 2o2oˉo3ˉO823837:36’833 ˉ I‖「O: getdetai1ur1http5://55r1。s〔mpe。ce∩ter/detaj1/1 2o20ˉ03ˉO823:37:36’833 ˉ I‖「α5〔rapj∩ghttp5日//5sr1·5〔rape·〔e∩ter/detaj1/1… 2020ˉO3ˉO823:37;39′985 ˉ I‖「0: getdetai1d己ta{0〔over‖: |http5://po。爬jtua∩.∩et/mγje/

〔e4d日3eO3e655b5b88ed31b5〔d7896〔+62472.jpg叫64wˉ6“bˉ1eˉ1〔0 ’ |∩a爬! : ‖霸王别姬ˉ「arewe11帅〔o∩〔ubj∩e‖’ 0〔ate8orje50 ; [{剧价` ’ ‖爱悄′]’ ′pub1i5‖edat‖ : !1993ˉ07ˉ26‖ ′ !dr3阳|: |影片借一出《霸王别姬》的京戏, 伞扯出二个人之间一段随时代风云变幻的爱』|R付仇·段′』、樱(张平毅饰)与程蝶衣(张四荣饰)足一对打′』、一起长大







b

P



P



的师兄弟,两人一个演生’一个饰旦,一向配合天衣尤缝’尤其一出《霸王别姬》’史是谷淆京城’为此’两人约定 合演一紫于《霸王别姬》°但两人对戏剧与人生关系的理肝有本励不同,段′」、楼深知戏非人生,程蝶衣】‖是人戏不分° 段小楼在认为垃成家工业之时迎娶了名妓菊仙(巩俐饰)’姓仗程壕衣认定莉仙是可肚的第三者,仗段′]、楼做了叛徒, 自比’三人围绕一出《霸王别姬》生出的金,|R怕仇战开始随甘时代风云的变迁不所升级,终酿成悲剧·| ’ !5〔ore‖:9.S} 202oˉ03ˉo823:〕7;39’985 ˉ I‖「0: getdetaj1ur1∩ttp5://5sr1。5crape.〔e∩ter/det己11/2 2o2oˉo3ˉ0823:37:39’985 ˉ I‖「0: 5cIapi∩g‖ttp58//55r1.5crape·〔e∩ter/detai1/2… 202OˉO3ˉO82〕S7:41’061ˉ I‖「0: getdet己j1data{0〔over,: 0http5://p1.爬itua∩。∩et/们ovje/6bea9a十4524d+bd Ob668eaa7e』87〔3d十767253.jpgα64"ˉ6“hˉ1eˉ1〔|’ 』∩a爬|: 』这个杀于不太冷ˉ l白o∩|’ 』〔ategorie5,: [!乃||情! ’ 』功作|」|犯罪,]’|p‖b1i5hed己t,: |199qˉo9ˉ1』! 」 ′dra爬′; 『里品(让·岔诺伸)是名孤独的职业杀于’受人瓜佣° 一天’邻居家′」`姑娘马蒂尔捻(纳塔丽。波特员饰)耿开他的房门,矣求在他那里暂避杀舟之祸·原米邻居率的主人 足T方衅拳血的眼线,只因贪污了一′」、包豢品品迫总T(加里°奥捻£饰)杀客全家的遥罚。 马蒂尔捻得到里品的 留救,伞兑于难’并留在里品那里·里品圾小★孩仗枪’她教里品法丈,两人关系日趋亲宙’柯处肚洽。★减想甘去报仇, 反倒枝林’里品及时赶到’将★孩救回°混杂着哀怨怕仇的正祁之战渐次什级,史大的冲突在所难兑……!』!5〔oIe|月 9.5} 2o20≡0〕ˉ0823:37:』1’o62 ˉ I‖「0; getdetaj1uI1http5://55I1.5〔mpe.ce∩ter/detaj1/〕

p

由于内容较多’这里省略了后续内容。



至此,我们已经成功提取出了每部电影的基本信息’包括封面、名称、类别等°



5.保存数据

P





l





成功提取到详情页信息之后,下_步就要把数据保存起来了。由于到现在我们还没有学习数据库 的存储,所以临时先将数据保存成文本格式,这里我们可以_个条目定义_个JSON文本。 定义一个保存数据的方法如下: i∏Portj5o∩ 十rmosi∏甲oIt币a长edjI5 千r咖o5.p3thj…rtexj5t5 R[5讥丁5DIR≡ 0Ie5u1t50

exj5tS(R[5U∏5DIR)or陋kedjⅢ5(R[5U∏5DIR)





de千5aγedat己(d3ta):

∩a‖记≡巾t己.get(‖∩a贬0) . data—p3t∩≡十,{R[5l」∏SDIR}/{∩a贬}.j5o∩』

jso∩。d』呻(d己ta’ ope∩(dat己-path’ 0"|’e∩〔odi吧≡,ut「ˉ8!〉’ e∩5uIea5〔ii=「己15e’ 1∩de∩t=2)

p



p



p

卜 p

卜 b





这里我们首先定义保存数据的文件夹【[50∏50IR’然后判断这个文件夹是否存在,如果不存在 则创建一个。

接着,我们定义了保存数据的方法5aγedata,其中先是获取数据的∩a们e字段’即电影名称,将 其当作JSON文件的名称;然后构造JSON文件的路径,接着用j5o∩的duⅦP方法将数据保存成文本

格式。du"p方法设置有两个参数,一个是e∩5urea5cjj ’值为「a15e’可以保证中文字符在文件中 能以正常的中文文本呈现,而不是unicodc字符;另一个是i∩de∩t’值为2’设置了JSON数据的结



‖ 咐 ■ ·

第2章基本库的使用



88

| ‖

果有两行缩进,让JSON数据的格式显得更加美观°

接下来把"a1∩方法稍微改写一下就好了’改写如下:

|{

de+爪aj∩():

十orpage1∩ra∩ge(1」 丁0丁A[p∧C[+1): i∩dex∩tⅧ1=5crapeˉj∩dex(page) detaj1ur15≡par5e-j∩dex(i∩dex∩m1) +ordeta11ur1j∩detaj1UI15:

deta11∩t"1=s〔rape二detaj1(detai1ur1) data≡par5e—det311(det311∩t川1)

1ogg1∩g.i∩+o( 08etdet己j1dat己‰0 ′ d己t己〉 1ogg1∩g.i∩千o(05aγi∩gdatatojso∩+j1e‖) 53γed3ta(dat日) q

1oggI∩g.1∩十o(0data5aγed5u〔〔e5s十u11y|)

重新运行,我们看下输出结果: 2o2oˉo〕ˉ09o1:10;27’o94ˉ I‖「0目 5〔r己p1∩g∩ttps://55r1。5〔rape.〔e∩ter/Page/1…

2o2oˉ03ˉ0901:1o:28’019ˉ I‖「0; getdetai1uI1‖ttp5://55r1.5〔mpe·ce∩ter/detai1/1 202oˉO3ˉ0901;1o目28’o19ˉ I‖「0: 5crap1∩ghttp5;//55I1·5〔rape。〔e∩ter/deta11/1… 202oˉ03ˉ0901:1o:29’183 ˉ I‖「0: getdet311data{|coγer‖; 0∩ttp5://pO。川e1tua∩.∩et/Ⅶoγie/ce4d日3eo3e655b5b88

■司|■■■■‖

这就是加了对5aγed日ta方法调用的们日j∩方法,其中还加了—些日志信息°

ed31b5〔d7896〔于62472.jp8@464闪644h1e1c‖′ 0∩aⅧe0 : `霸王月||姬ˉ「are"e11‖y〔o∩〔ub1∩e|′ 0c己tegorie5` : [‖剧悄0 ’ |爱价{ ]’ 0pub115hedat‖ : `1993ˉ07ˉ260 ’ ‖dm‖a』: ‖影片借一出《霸王月||姬》的京戏,牵扯出二个人

之间一段随时代风云变幻的金恨』时仇°段小楼(张丰破饰)与程蝶衣(张国荣饰)是一对打小一起长大的师几弟’ 两人一个演生’一个饰旦,一向配合尺衣允缝’尤其一出《霸王别姬》 ,史是柞满京戏, 为此’两人约定合演一苹于 《霸王另||姬》 °但两人对戏剧与人生关系的理肝有本励不同’段′」`楼深知戏非人生,程蝶衣则是人戏不分°段小楼在 认为该成家立业之时迎娶了名妓菊仙(巩俐饰) ,致仗程蝶衣认定菊仙是可耻的第三者’仗段小楼做了叛役, 自此,

202Oˉ03ˉO901:10:29’288 ˉ I‖「0: d己ta5aved5uc〔e55千u11y

2o20ˉ03ˉo901:1o:29’288 ˉ I‖『0: getdeta11ur1打ttp5://55r1。5〔rape·〔e∏ter/deta11/∑ 2o20ˉo〕ˉo9o1;1o:29’288 ˉ I‖「0; 5〔rapj∩8∩ttp5://55r1,5〔rape。ce∩ter/deta11/2… 2o20ˉ03ˉo9o1;1α3o’25o_ I‖「0: getdet己j1data{0〔over‖: !http5://p1.川eitua∩.∩et/用oγje/6be日9a+452耳d于bd ob668eaa7e187〔3d「7672S3.jpg@464"644们 1e1〔|’ |∩a们e{ : 』这个杀于不太冷= [仑o∩` ′ ′〔ategorje5’; [‖剧|叶’ 幼作|’ ‖犯罪‖]’ ‖pub1j5‖edat0 : |199↓ˉ09ˉ14‖′ 0dra爪a0 : 0里品(让。岔诺饰)是名孤独的职业杀于,受人

通过运行结果可以发现’这里成功输出了

◆●● 〈 `∩ ■■≡■■→……

将数据存储到JSON文件的信息。

件,文件名就是电影名’如图2ˉ2l所示。

6.多进程加速

■√色嗡翁●

≯°

— ^m二=-→三



…尸沛诧 …狱咖饵

■≈=矾向A≈■●…

…■津河

尊霹簿…毯_翻蕊赣嚣滁沁蹿擞烹.襟般『喻嗡……》龄″爵 .

“ .

…■沦捣

蔫≡.■……

融罕j蓖婶露瓣…霹赣……f怎′.. j÷. `



慧≡.酗々巳四T纫卫寸于々…

钩●由询

龋鞋藤辩躇萨.p…禽←缮. |….≡挚. .》



…蘸谗呜

隅=-…



慨瞬√嚏慧铲

萝.蕊撼…娘镶

鞠=垮…慈‖…厂』〈簿鳞慧嚣=辨唾、』 ;…` 8≈鸭●千七…山=

…缸油拘

…缉=睁髓:盘.散

由于整个爬取是单进程的’而且只能逐条

爬取,因此速度稍微有点慢’那有没有方法对 整个爬取过程进行加速呢?

壤哺0~≡……■申一→…碑馋汹

—嚣懒瓣…

帘骤蹿鳃熟/

辨鞠哩…■望←ˉ…~…忠…二ˉi辑= 瞬ˉ霹融瓣虚蹿龋臻…蕾…厂即镭』… 魁ˉ獭嗡,撵癣鞠锻瞬…繁…敷;`… …蚁℃■

n≡扣宰.砷…=≡ 屿■吓℃ 前面我们讲了多进程的基本原理和使用 但,繁撵一邀…F娜缴蓖;繁…湃鞭-噬: =了′、`叮刨 方法’下面就来实践-下多进程爬取吧。

图2ˉ2l

本地的结果

』■||司‖|司■■‖|·∏|』■‖‖‖■■】‖℃■γ‖|」□』■‖■■■』句』』■□■]‖‖』勺‖』■●』■■可』□

JSON文件’每部电影数据都是一个JSON文

囊弓

吁………ˉ…■≈……w……

■7●…=…… ˉv…pγb≡

运行完毕之后,我们可以观察下本地的结 果’可以看到re5u1t5文件夹下多了l00个





q

·□■{■■』|‖|■■■|』■」

雇佣°一犬,邻居家小姑娘马蒂尔捻(纳塔丽.泣特艾饰)歌开他的房门,妥求在他那里暂避杀身之祸。原米邻居家 的主人是T方鲜朵姐的眼线’只囚贪污了一‘|、包豢品而迫慈警(加里,奥德Ⅲ饰)杀客全家的惩罚。马蒂尔捻仔到里 品的留救’伞兑于难,并留在里品那里°里昂教小★孩使枪,她教里品法丈’ 两人关爪日趋亲密’相处融洽°女孩 想甘去报仇,反倒械杯’里品及时赶到’将士孩救回.混杂封哀怨价仇的正邪之战渐次什级,史大的冲突在所难兑……‖’ 5coIe! : 9.5} 2o2o_03ˉo9o1:1o:3o’250ˉ I‖「0: 5己γj∩gdat己toj5o∩十11e 2O∑OˉO3ˉO9O1:1O;3O』25〕 ˉ I‖「0弓 d己t己5aved5ucce55十u11y



■■‖‖|二■}=■∏

三人围绕一出《霸王月||姬》生出的爱恨怕仇战开始随豺时代风云的变迁不断什级,终酿成悲剧° 0 」 ‖ScoIe0 : 9.5} 202Oˉ03ˉ0901:10;29』183 ˉ I‖「0: 5aγ1∩gdatatoj5o∩十j1e

2.5基础爬虫案例实战

89

由于一共有l0页详情页,且这l0页内容互不干扰,因此我们可以一页开一个进程来爬取。而且 因为这l0个列表页页码正好可以提前构造成-个列表’所以我们可以选用多进程里面的进程池poO1 来实现这个过程。

这里我们需要改写下川a1∩方法,实现如下: 加POrt川l」1t1PIO〔eS5i∩g de千∏aj∩(page): j∩dex∩tⅦ1=5crape—j∩dex(page) detaj1ur15=par5ej∩dex(j∩dex∩tⅧ1) 「ordeta11ur1i∩detaj1uI15:

deta11∩t们1=5〔mpeˉdetai1(detaj1ˉur1) data=par5edetaj1(detai1‖t川1) 1og8j∩g.j∩+o(0getdetaj1d己ta%5|’ d日ta)



| b

}|

「| 卜「

『}



1oggj∩g。j∩fo(!5av1∩gdatatoj5o∩d3ta』) 5avedata(dat日)

1ogg1∏g.1∩千o(‖data5avedsu〔ce55+u11y‖) j千

∩a『∏e

=≡

爪a1∩

0:

poo1=∩u1t1proce551∩g.poo1() pa8e5≡ra∩ge(1’ 丁0丁A[p∧C[+1) pooL帕p(爪3j∩’ pages) poo1.〔1O5e() poo1。joi∩()

我们首先给∏a1∩方法添加了—个参数page,用以表示列表页的页码°接着声明了—个进程池’ 并声明page5为所有需要遍历的页码,即1ˉ10。最后调用刚ap方法,其第一个参数就是需要被调用的 参数’第二个参数就是page5,即需要遍历的页码。 这样就会依次遍历page5中的内容’把1ˉ10这l0个页码分别传递给‖a1∩方法’并把每次的调用 分别变成一个进程’加人进程池中’进程池会根据当前运行环境来决定运行多少个进程°例如我的机 器的CPU有8个核,那么进程池的大小就会默认设置为8,这样会有8个进程并行运行°

■庐■

运行后的输出结果和之前类似’只是可以明显看到,多进程执行之后的爬取速度快了很多。可以

》‖『卜

清空之前的爬取数据’会发现数据依然可以被正常保存成JSON文件° 好了’到现在为止’我们就完成了全站电影数据的爬取,并实现了爬取数据的存储和优化。 7.总结

■』

本节用到的库有requests、multiprocessing` re、logging等,通过这个案例实战’我们把前面学习 一■■『△∏》『[■尸■‖》∏

到的知识都串联了起来’对于其中的一些实现方法,可以好好思考和体会,也希望这个案例能够让你 对爬虫的实现有更实际的了解。

本节代码参见: https://githuhcomPython3WebSpider/ScrapeSsrl。



巴■「‖『『「忆田「『卜ˉ尸 匹■厅『『卜‖巴■尸|)|▲■【〗「

)》 巴■■‖‖「‖『▲=■『‖|||》■■∏【【『『■■■■厂



·|■{

网页数据的解析提 上一章我们实现了一个最基本的爬虫,但提取页面信息时使用的是正则表达式’过程比较烦琐,而 且万一有地方写错了.可能会导致匹配失败,所以使用正则表达式提取页面信息多少还是有些不方便。 对于网页的节点来说,可以定义1d`〔1a55或其他属性,而且节点之间还有层次关系’在网页中 可以通过XPath或CSS选择器来定位一个或多个节点。那么’在解析页面时’利用XPath或CSS选 择器提取某个节点,然后调用相应方法获取该节点的正文内容或者属性,不就可以提取我们想要的任



意信息了吗?

在Python中,怎样实现上述操作呢?不用担心,相关的解析库非常多,其中比较强大的有lxml` BeautjlhlSoup、 pyquery、parsel等°本章就来介绍这几个解析库的用法°有了它们,我们就不用再为

3.↑

Xpath的使用 XPath的全称是XMLPathLanguage,即XML路径语言’用来在XML文档中查找信息°它虽然 所以在做爬虫时,我们完全可以使用XPath实现相应的信息抽取°本节我们就介绍一下XPath的

基本用法。

↑.xPat∩概览

XPath的选择功能十分强大’它提供了非常简洁明了的路径选择表达式。另外,它还提供了l00多 个内建函数,用于字符串、数值时间的匹配以及节点`序列的处理等°几乎所有我们想要定位的节 点,都可以用XPath选择°

XPath于l999年ll月l6日成为W3C标准’它被设计出来,供XSLI`XPomter以及其他XML解

2XPath常用规则 表3ˉ1列举了XPath的几个常用规则° 表3ˉ‖ xpat∩的常用规则 表达式





选取此节点的所有子节点

从当前节点选取直接子节点

//

从当前节点选取子孙节点 选取当前节点

●●

0

选取当前节点的父节点

选取属性

□一■‖』‖□』‖‖』γ·■■〗』·】■]

∩ode∩a爬 /

■■·|』■■‖】』■‖|」■口||‖‖‖创词]|‖■■乙■■‖|‖』』』■口□‖

析软件使用°

■可{||」■■‖』|||』■可‖‖||』■■‖』■‖‖|」■∏γ‖|‖】■■■‖‖‖‖』■门

最初是用来搜寻XML文档的,但同样适用于HTML文档的搜索°

∏|||】Ⅶ]』]□·】||■可|

正则表达式发愁’解析效率也会大大提高°

』■■∏』|■■■■■■□

『□■尸‖止■「|■■「||坠■『‖‖』『′■厂■『·庐■尸凸巴■■[·「■■「「■■「[尸坚=尸‖■(β△◆【‖‖■`~■厂■■■■尸巴尸▲■「△■「|■■厂卜=■「■■■厂■β■■厂■厂|■■尸尸■■「‖二■『【■=■■=厂◆=尸『△■■

3.l

XPath的使用

9]

这里列础了XPath的_个常用匝配规则,如下: //t1t1e[01a∩g二e∩g0 ]

它代表选择所有名称为tjt1e,同时属性1a∩g的值为e∩g的节点° 后面会通过Python的lxml库,利用XPath对HTML进行解析° 3.准备工作

使用lxm|库之前’首先要确保其已安装好°

可以使用pjp3来安装: p1p3i∩5ta111X田1

更详细的安装说明可以参考: https://setupscrape.center/lxml° 安装完成后’就可以进人接下来的学习了。

4.实例引入 下面通过实例感受_下使用XPath对网页进行解析的过程,相关代码如下: +ro川1x川1jⅧportetree teXt= { | 0 〈diγ〉

〈l』1〉 〈1j〔1a55≡"jteⅦˉo">〈己打re「="1j∩促1.‖t川1"〉fir5t ite‖〈/己)〈/1j〉

〈1j〔1日55=|!ite"ˉ1"〉〈a‖re十="11∩代2.htⅦ1||〉5e〔o∩dite们</3〉</1i〉

〈1iC1a55="iteⅦˉj∩a〔tiγe"〉〈a∩re+二"1j∩旧.htⅧ1"〉t∩iIdite‖</a〉〈/1j〉 <1i〔1a55="jteⅧˉ1"〉〈ahIe+="11∩R4·∩t刚1"〉+ourth1te们</日)〈/1i)

〈1jC1a55≡"iteⅦˉ0"〉〈ahre千="1j∩代ShtⅧ1"〉+1十t门jte刚</a〉 勺



〈/u」〉

〈/djγ〉 0 『

0

∩tⅦ1=etIee.‖「‖L(teXt)

re5u1t≡et〔ee.to5tri∩g(∩t"1〉 prj∩t(re5u1t.decode(0ut十ˉ8‖))

这里首先导人lxml库的etree模块’然后声明了_段HTML文本,接着调用‖『‖[类进行初始化, 这样就成功构造了一个XPath解析对象°此处需要注意一点,HTML文本中的最后一个1j节点是没 有闭合的’而etree模块可以自动修正HTML文本°

之后调用to5tr1∩g方法即可输出修正后的HTML代码’但是结果是bγte5类型。于是利用decode 方法将其转换成5tI类型,结果如下: <∩t∏1×bodγ〉〈diγ〉 勺

〈u』〉

<11〔1a55="ite‖ˉO"〉〈a∩re千="1j∩假1.∩t爪1"〉+ir5t iteⅧ〈/a〉〈/1j〉 <11〔1日55="1teⅧˉ1"〉〈a∩re+="11∩长2°ht阳1"〉se〔o∩djteⅦ〈/3〉〈/1j〉 <1jC1a55="ite们ˉj∩a〔tjVe"〉<日∩Ie十二"11∩旧。门t们1"〉tMmite川〈/a>〈/1i〉 〈1ic1日55="jte们ˉ1"〉〈a∩re+≡"1i∩M°htⅧ1| |〉十ourt‖iteⅧ〈/a×/11〉 <11〔1355="1te们ˉ0"〉〈ahre千="1i∩促5.ht‖1"〉+i什hjte们</a〉 〈/11〉〈/u1) )∩『}口~■「■ˉ|【■■「『‖‖【■〗】【■■■■■『止■■【■■■■■■【

〈/diγ〉

</body〉</hm1〉

可以看到,经过处理之后的11节点标签得以补全’并且自动添加了body` ∩t川1节点° 另外,也可以不声明’直接读取文本文件进行解析’实例如下: 千IO肌1X川1i川pOrtetIee

‖t们1=etree.par5e(|./te5t.∩t"10 ’ etree。‖丁‖[PaI5er(〉) 工e501t二etree.to5tI1∩g(‖t们1) pri∩t(re5u1t。de〔ode(||」t「ˉ8|))



】■〗』■ⅢⅢ■■■□■■□·□■■■■□□□□■■』】‖‖□■〗』■■〗〗〗□』】〗‖■可』】‖』■』

第3章网页数据的解析提取

92

其中testhtml的内容就是上面例子中的HTML代码’内容如下: ■



〈o1V〉

〈01〉

<1i〔1aS5≡00jte们ˉ000〉〈a∩re十="11∩促1°hm1"〉千ir5tjte"〈/a〉〈/1j〉 <1ic1a55="ite们ˉ1"〉〈a∩re十=凹11∩恨2.∩t们1"〉5e〔o∩dit副</a×/1i〉 〈1iC1己55="1teⅦˉi∩a〔tjγe"〉〈ahre十=0011∩仅3.ht们1"〉thirdite们〈/a×/1i〉 <1j〔1a55≡"1te∩ˉ1oo〉〈ahre+=m1j∩促4。ht们1"〉「ourthjte们</a〉〈/1i〉 <1jc1a55="jte汛ˉo"》<a∩re十="1i∩代S.ht们1"〉+i千t‖1te们〈/a〉 〈/u1〉 〈/diγ〉

〈u1〉

((

<1j〔1a55="jte阳ˉO"〉〈己∩re千="1i∩k1ht∏1"〉+1r5tite∩</a〉</1j〉 〈11〔1己55="1te∏ˉ1"〉〈己‖re千="11∩炮·ht们1"〉5eco∩dite们</日〉〈/11〉 <11〔1a55≡"1te们ˉ1∩a〔t1γe|0〉〈ahre十="11∩旧。∩m1"〉thjIdjteⅧ〈/a×/11〉 〈11〔1as5="jte们ˉ1"〉〈ahre十="11∩刚°hm10!〉十ourt∩1teⅧ</a〉〈/11〉 <1jC1a55=‖0jte爪ˉ0"〉<己h】e十="11∩k5htⅧ1o|〉千1+t∏1te吓/a〉

{(

<‖t"1〉<body>〈djv〉

〗□∏

这次的输出结果略有不同,多了一个D0〔『γp[声明’不过对解析无任何影响’结果如下: 〈!卯〔丁γP[ht∩1p0BLI〔 00ˉ//‖3〔//D丁0‖丁川4。0『m∩5itjo∩a1//[‖00 "http://‖w‖Ⅳ.w3。org/丁R/R[(ˉ∩tⅦ140/1oo5e·dtd00〉

〈/1i></u1〉

〈/djv〉〈/bodγ〉〈/∏t们1〉

5.所有节点

我们一般会用以//开头的XPath规则,来选取所有符合要求的节点。这里还是以第一个实例中

{ q

+rO们1X们1加portetree

∩m1=etreep日ISe(|./te5t。∩t阳1』’ etree.‖『肌par5er()) re5u1t=∩t爪1.xpath(|//*0) prj∩t(re5u1t)

运行结果如下:

[〈[1e们e∩thtⅦ1日tox10510d9〔8〉’<[1e爬∩tbodγatox1o510dao8〉’〈[1e爬∩tdjγat0x1o51oda48〉’〈[1e爬∩tu1at

这里使用*代表匹配所有节点’也就是获取整个HTML文本中的所有节点°从运行结果可以看到’ 返回形式是-个列表’其中每个元素是[1e∏e∩t类型’类型后面跟着节点的名称’如∩t‖1`body`d1γ、 ●

当然,此处匹配也可以指定节点名称。例如想获取所有11节点’实例如下: +roⅦ1X川1加pOrtetree ∩t‖1≡etree.par5e(‖°/te5t.ht们1,’ etree·‖丁‖[Par5er()) re5u1t=‖tⅦ1.xp日t门(0//1j’) prj∩t(re5u1t) prj∩t(re5u1t[o])

这里选取所有1j节点,可以使用//,然后直接加上节点名称,调用时使用Xpatb方法即可。

[〈[1e们e∩t1i3tox1o5849208〉’〈[1e『∏e∩t1i己tox1058』9248〉’〈[1e爬∩t1jatOx105849288〉’ 〈[1e们e∩t11at 0x1058492c8〉’〈[1e|‖e∩t11at0x1O5849308〉] 〈[1e爬∩t1iatOx1058492O8〉

可以直接用中括号加索引获取,如[0]。 6.子节点

通过/或//即可查找元素的子节点或子孙节点。假如现在想选择11节点的所有直接子节点a,



‖{‖|

可以看到,提取结果也是_个列表,其中每个元素都是[1e"e∩t类型。要是想取出其中一个对象’

d

‖』‖■■

运行结果如下:

√{|」■‖|‖□□‖‖勺Ⅲ·■■可|■|■■‖』‖

u1、1j、a等,所有节点都包含在了列表中°

=■■】‖‖|二■可■■□『‖

ox10510da88〉』<[1e爬∩t11atox10510da〔8〉’ 〈[1e爬∩ta日tox10510db48〉’<[1e『∏e∩t1i己tox1051odb88〉’〈[1eⅧe∩t a己tox10510db〔8〉’<[1eⅧe∩t1jatox10510d〔o8〉’ 〈[1e∏e∩t己atox10510dbo8〉’〈[1e爬∩t11at0x1051odc48〉’<〔1e们e∩t 己at0x10510dc88〉’〈[1e爬∩t11atOx1O510d〔〔8〉’〈[1e′∏e∩taatOx1051Odd08〉]

』■■■可‖』·可』■司|■■

的HTML文本为例,选取其中所有节点’实现代码如下:



‖|





3.l

XPath的伎用

93

则可以这样实现: +ro们1xⅦ1i们portetIee



o卜

0

∩t"1≡etree.par5e(! ./te5t.ht们1!’ etre巳‖「肌P3rser()) re5u1t≡ht爪1°xpath( !//1j/a0) prj∩t(re5u1t)

这里通过追加/a的方式,选择了所有11节点的所有直接子节点a。其中//1j用于选中所有11节 点’/a用于选中11节点的所有直接子节点a。

















β



0



运行结果如下: [〈[1e爬∩taat0x106ee8688〉’〈[1e『∏e∩ta日tOx106ee86C8〉’〈[1e|∩e∩taat0x1O6ee87O8〉′<[1eⅢe∩taatOx1O6ee8748〉’ 〈[1eⅧe∩taat0x106ee8788〉]

上面的/用于选取节点的直接子节点,如果要获取节点的所有子孙节点’可以使用//。例如’要 获取u1节点下的所有子孙节点a,可以这样实现; +rO『∏1m1mpOrtetree

∩t‖1≡etree.parse(0 ./te5t.打m1‖’ etree.‖丁肌paI5er()) re501t≡ht们1。xp3t∩(0//u1//a0) prj∩t(re5u1t)

运行结果是相同的。

但是如果这里用//u1/a’就无法获取任何结果了。因为/用于获取直接子节点’而u1节点下没

有直接的a子节点’只有11节点’所以无法获取任何匹配结果,代码如下: fro刚1x‖1i卯ortetIee



ht‖1=etree.paI5e(‖./test.闪m10 ’ etree.‖丁‖[par5er()) Ies‖1t=∩t川Lxpath(’//u1/3|) pIi∩t(re5u1t)



运行结果如下:

p

0

[]

‖卜

因此这里要注意/和//的区别’前者用于获取直接子节点’后者用于获取子孙节点°



7.父节点

p





{[











通过连续的/或//可以查找子节点或子孙节点’那么假如知道了子节点’怎样查找父节点呢? 这可以用..实现°

例如’首先选中‖re十属性为11∩代4.∩tⅧ1的a节点’然后获取其父节点’再获取父节点的〔1a55属 性’相关代码如下: +rO们1xⅧ1i‖pOItetIee

∩tⅦ1≡etree.par5e(|./te5t.‖tⅧ1|’ etree.‖『‖儿par5er()) re5u1t=ht∏1·xpath(』//a[助re千≡"11∩哎4°htⅧ10』]/。./0〔1a55`) Prj∩t(re5u1t)

运行结果如下: [0jte‖ˉ10 ]

检查-下结果发现,这正是我们获取的目标1j节点的C1a55属性°

也可以通过pare∩t;:获取父节点, .代码如下: 十ro冈1x们1加portetree

∩tⅧ1≡etree.p日r5e(}./te5t.∩t"1』’ etree.‖Ⅷ[par5er()) re5U1t=ht肌1。Xp己th(|//a[0hre+≡"1j∩k4.htⅧ1』′]/Pare∩t: :*/伙1a三5『) Prj∩t(re5u1t)



第3章网页数据的解析提取

94

8.属性匹配

在选取节点的时候,还可以使用0符号实现属性过滤。例如’要选取C1a55属性为ite"ˉ0的11节 点,可以这样实现: 千ro∏1x‖1mportetree

hm1=etreepar5e(0 ·/te5t.∩t们1! ’ etree.‖丁肌par5er()) reBu1t=‖t肌Lxp3th(』//11[@〔1asS≡"jte"ˉO"]{) pri∩t(re5l|1t)

可见’匹配结果正是两个’至于是不是正确的那两个,后面再验证°

||

用XPath中的text方法可以获取节点中的文本’接下来尝试获取前面11节点中的文本’相关代 码如下:

』□□

9.文本获取

‖|

〈[1e们e∩t1j己tOx10a399288〉’〈[1e∏论∩t1jatO×1Oa3992c8〉

‖|

件的1j节点有两个,所以结果应该返回两个元素°结果如下:

厂▲

这里通过加人[0〔1a55="1te"ˉO"]’限制了节点的〔1a55属性为1te"ˉO°HTML文本中符合这个条

千Io川1x阳1mportetIee

∩m1=etree.par5e(‖·/te5t·ht们1‖’ etIee·什丁肌par5er()) re5u1t=htⅧ1.xpath(,//1i[0〔1a55=‖|jte‖ˉ0』0]/text()0) pI1∩t(re5u1t)

运行结果如下: [ 0\∩

0]

奇怪的是’我们没有获取任何文本,只获取了_个换行符’这是为什么呢?因为xpat∩中teXt方 法的前面是/’而/的含义是选取直接子节点,很明显1i的直接子节点都是a节点,文本都是在a节 点内部的’所以这里匹配到的结果就是被修正的11节点内部的换行符,因为自动修正的1j节点的尾 标签换行了。

即选中的是这两个节点: <1i〔1日55="1te∩≡0"〉〈ahre十≡"11∩k1。ht们1"〉于iI5tjte们</a〉〈/11〉

〈1i〔1a55="ite们ˉO"〉〈己hre十="1j∩k5·∏t盯1"〉+1十t∩jte们〈/a〉 〈/1j〉

其中_个节点因为自动修正, 1j节点的尾标签在添加的时候换行了’所以提取文本得到的唯-结 果就是1i节点的尾标签和a节点的尾标签之间的换行符。

因此,如果想获取1j节点内部的文本,就有两种方式,一种是先选取a节点再获取文本,另_ 种是使用//°接下来,我们看下两种方式的区别。

先选取a节点,再获取文本的代码如下: 十rO"1X们1加pOItetree ht刚1=etree.par5e(! ./te5t°∩tⅦ1』′ etre巳‖丁月[Par5er()) re5u1t=ht"1.xp3t∩(‖//11[@〔1a5s≡"1temˉO"]/a/text()0) pr1∩t(re5u1t)

运行结果如下: [!千jr5titeⅦ‖′ 0十i千t∩jte们0 ]

可以看到’这里有两个返回值’内容都是〔1a55属性为jte们ˉ0的1j节点的文本,这也印证了前 面属性匹配的结果是正确的。

这种方式下’我们是逐层选取的’先选取11节点,然后利用/选取其直接子节点a,再选取节点



3.l

XPath的使用

95

a的文本,得到的两个结果恰好是符合我们预期的。

再来看—下使用//能够获取什么样的结果,代码如下: 「rOⅦ1XⅧ1 i刚pOrtetIee

∩t∏1=etree.parse(‖./te5t。htⅧ1′’ etre巳‖『肌par5er()) re501t=‖t川1.xPat‖(′//11[@〔1a55=001teⅦˉO"]//text()0) prj∩t(re5u1t)

运行结果如下: [‖+ir5tite∏』’ 0十j十thite们|’ !\∩

| ]

|■「『|[■『‖■『似■■厂|■厂‖‖卜

不出所料’这里的返回结果是三个°可想而知’这里选取的是所有子孙节点的文本,其中前两个 是11的子节点a内部的文本’另外-个是最后_个11节点内部的文本,即换行符°

■■厂■尸■』

由此’要想获取子孙节点内部的所有文本’可以直接使用//加text方法的方式,这样能够保证 获取最全面的文本信息’但是可能会夹杂—些换行符等特殊字符°如果想获取某些特定子孙节点下的 所有文本,则可以先选取特定的子孙节点’再调用te×t方法获取其内部的文本’这样可以保证获取 的结果是整洁的°

↑O属性获取

我们已经可以用text方法获取节点内部文本’那么节点属性该怎样获取呢?其实依然可以用@符 》|卜



号°例如,通过如下代码获取所有11节点下所有a节点的bre十属性: 千roⅧ1x∏l1j‖portetree

ht"1=etree.paI5e(‖./te5t.ht爪1|’ etree.什「‖[Par5eI()) re5u1t=‖t∏1.xpat∩(,//1i/3/0∩re十《〉

p ■■∏|■尸「■■尸β|=尸

pri∩t(Ie5u1t)

这里通过@hre+获取节点的‖re{属性°注意’此处和属性匹配的方法不同’属性匹配是用中括号 加属性名和值来限定某个属性,如[@‖re+="1j∩亿LhtⅧ1"]’此处的0‖re十是指获取节点的某个属性, 二者需要做好区分°

■ 尸 卜 巳 尸

运行结果如下:

[{11∩代1.bt∏1‖’ 01j∩代2·bt∩1‖’ |1j∩k3。ht们10 ’ ‖1j∩k4ht川10 ’ ‖11∩代5.ht们1|]

′∩∩■■■■厂‖广『■■「■β△尸》『▲■「

可以看到’我们成功获取了所有1j节点下a节点的∩re千属性’并以列表形式返回了它们。 .

↑↑.属性多值匹配

有时候’某些节点的某个属性可能有多个值’例如: 「ro们1x川1加portetree te×t= ‖ 0 !

<1i〔1己55≡"111jˉ「jI5t"〉〈a∩Ie+=,|11∩艇.∩t‖1|{〉+ir5tite们〈/a〉</11〉 ∩m1=etree.‖『‖[(text)

Ie5u1t=ht阳1.xpat‖(|//1j[0〔1a55≡"1j"]/a/text()‖)

|‖【■「■尸■『卜『『炉『‖‖‖卜

pri∩t(resu1t)

这里HTML文本中11节点的〔1a55属性就有两个值: 11和11ˉ十1r5t。此时如果还用之前的属性 匹配获取节点,就无法进行了,运行结果如下:

~■亏『‖』】『■■「|「‖

[]

这种情况需要用到〔O∏ta1∩5方法,于是代码可以改写如下: +rO"1X∏1 1ⅦpOrtetree teXt= |‖



|}

〈1iC1a55="1j1iˉ+1r5t||〉<a打ref="11∩k.向tⅧ1"〉千jr5t1te‖(/a×/1j〉

∩t们1≡etree.日丁川(text) re5[」1t=ht们Lxpat∩(0//1j[co∩t己j∩5(@〔1己55’ "1j")]/a/text()0 ) prj∩t〈re5u1t)

上面使用了〔o∩ta1"5方法,给其第—个参数传人属性名称,第二个参数传人属性值’只要传人的

属性包含传人的属性值,就可以完成匹配了。 此时运行结果如下: [‖+1I5t1te们』]

Co∩taj∩5方法经常在某个节点的某个属性有多个值时用到°

‖2.多属性∏配

我们还可能遇到_种情况,就是根据多个属性确定-个节点,这时需要同时匹配多个属性。运算 符a∩d用于连接多个属性,实例如下: +ro∏1×川1jⅦportetree teXt= ! 0 !

〈1jC1日55="1j1jˉ千jr5t| | ∩己∏e="jte们"〉《a∩re+="1j∩代.htⅦ1"〉千1r5t jte‖</a〉〈/1j〉

ht川1≡etree.什丁‖[(teXt)

re501t=ht们1°xpath(‖//11[〔o∩taj∩5(@〔1a55’ "1j") a∩d@∩a川e="iteⅧ"]/a/

text()0 )

pri∩t(re5u1t)

这里的11节点又增加了_个属性∩a"e。因此要确定1j节点’需要同时考察〔1a55和∩日"e属性’ -个条件是〔1a55属性里面包含11字符串,另_个条件是∩a们e属性为ite"字符串’这二者同时得到 满足,才是1j节点。 〔1a55和∩己Ⅶe属性需要用a∩d运算符相连,相连之后置于中括号内进行条件筛 选。运行结果如下: [‖+1I5t1teⅦ0 ]

这里的a∩d其实是XPath中的运算符°除了它,还有很多其他运算符,如or` 们od等’在此总结 为表3ˉ2°

表3ˉ2运算符及其介绍 描

运算符







返回值

or



age=19ora8e=2o

如果age是l9,则返回true。如果己8e是2l ’

己∩d



age〉193∩dage〈21

如果a8e是20,则返回true。如果age是l8, 则返回十a15e

ⅧOd

计算除法的余数

5 『∏od2

计算两个节点集

//book|//cd

则返回千日1Se

返回所有拥有book和cd元素的节点集



6+4

‖0

6 ˉ4

2



乘法

6*q

24

div

除法

8djV4

2

等于

age二19

如果己ge是l90则返回true°如果日ge是20’则返回伯15e

不等于

age!=19

如果age是l8’则返回tIue°如果age是l9’则返回千a15e

日≡

小于

age〈19

如果己ge是l8’则返回true°如果3ge是l9,

则返回千a15e

〈=

小于或等于

<=19

如果日ge是l9,则返回true°如果age是20,

则返回伯1Se

大于

日ge)19

如果age是20,则返回true°如果age是l9’则返回千a15e

大于或等于

age〉=19

如果己ge是‖9,则返回tIue·如果age是l8’则返回「a15e



>=



■■|‖‖‖·』●■】】‖‖‖√可·』■□·可





(|

|≡

■■】°■『■■|‖日‖■■‖

加法 减法



·|』·|』』■‖』■‖』||■】‖‖司」』|||■∏」··‖‖‖||勺 ■ ■ ■ ■ 可 」 日 | 』 ■ □ 』 ■ 〗 《 ■ ■ ■ 妇 ‖ 』 ■ ■ ■ ■ ■ · ■ □ ‖ 汕 □ ■ ■ 〗 □ ■ ■ ■ ■ ■ ] ■ ■ ■ ‖ 』 · 可 · = ■ ■ ■ 可 ■ 旦 ■ 】 」 ■ 』 ■ ■ ■

第3章网页数据的解析提取

96

■■







3.l

XPath的使用

97

↑3.按序选择

在选择节点时’某些属性可能同时匹配了多个节点’但我们只想要其中的某_个,如第二个或者 b ■厂卜|■厂|■■卜

最后一个’这时该怎么办呢?

可以使用往中括号中传人索引的方法获取特定次序的节点’实例如下: 十rα∏1×∩1mportetree

}「

te×t= | 『} 〗



<o1γ〉

〈u1〉

〈1i〔1a55="jte‖ˉO"〉<ahre「="1j∩惯1.‖t们1"〉十jr5t1te『n</a〉</1j〉

〈11〔1a55="1te‖ˉ1"〉〈3∩re千="11∩促2。‖t刚10!〉5e〔o∩djte∏‖</3〉</1i〉 〈1i〔1a55="1te们ˉj∩aCtjγe!』〉<ahre+="1i∩旧。htⅧ100〉th1rdite‖〈/a〉〈/11〉 〈1j〔1a55≡00jte们ˉ1"〉〈a∩re千="11∩代4.∩tⅦ100〉fOUrthjteⅦ〈/a〉〈/1i〉 〈11〔1己s5="jte"ˉO"〉〈a‖re+=‖|11∩代5。‖t‖1"〉十i+thjte爪</a〉



●「}■尸「「■「●厂■■|‖|■厂}卜||匹尸■尸■「)△ˉ■厂′■「》‖|

〈/u1〉

</div) 0

0

0

hm1≡etIee.盯"[(text)

re5u1t≡h↑‖1.xpatb(‖//1j[1]/3/text()|) pri∩t(re5(」1t) re5u1t=∩m1。xpat∩(|//11[1a5t()]/a/text()|) p工[∩t(re5(」1t)

re|iu1t≡bt们1.xpath(,//11[po5itjo∩()〈3]/a/text()′) pm∩t(Ie5u1t)

Ie』iu1t=ht们1.xPat‖(』//1j[1a5t()ˉ2]/a/text()‖) pri∩t(Ie5‖1t)

止述代码中’第一次选择时选取了第一个1j节点,往中括号中传人数字l即可实现°注意’这 里和写代码不同’序号以l开头’而非0。

第二次选择时,选取了最后-个11节点’在中括号中调用1a5t方法即可实现°

第三次选择时,选取了位置小于3的11节点,也就是位置序号为l和2的节点,得到的结果就 是前两个1i节点°

》「■『卜}◆‖|■『′卜‖β|■■「『|◆「‖「》|}}■「[【『尸|||「【}[■■『|止「「}[●}||【『‖〖【■●【『匹■□『【【『【

第四次选择时’选取了倒数第三个1i节点’在中括号中调用1a5t方法再减去2即可实现°因为 1a5t方法代表最后一个’在此基础上减2得到的就是倒数第三个° 运行结果如下: [ 0千1rSt1te叮] [』+j代hite∏]

[‖f1r5tite‖|’ ‖5e〔o∩dite"|] [0t‖1rd1te们′]

在这个实例中’我们使用了1a5t、po5jt1o∩等方法。XPath提供了l00多个方法,包括存取、数 值`字符串`逻辑、节点、序列等处理功能。 ↑4。节点轴选择

XPath提供了很多节点轴的选择方法’包括获取子元素、兄弟元素、父元素`祖先元素等,实例 如下: +Io"1x‖1mportetree te×t= 『 0 『 <diγ〉 司

〈u儿〉

〈1j 〔1日55="jte刚ˉO"〉〈日hre「=!01j∩代1·hm1"〉〈5pa∩〉十1r5t1te‖〈/5p日∩〉</a〉〈/1i〉 〈1j 〔1a55="jteⅧ-1"〉<ahre十="1j∩炮·hm1『0〉5eco∩djte"≤/a×/11〉 〈11 〔1a55="1te∩~i∩己〔t1γe"〉〈a‖re千≡阅1j∩陶。‖t们1"〉tMrd1teⅦ</a〉〈/11〉



第3章网页数据的解析提取

■■■

<1j〔1ass="ite阳ˉ1"〉<己hre于二001j∩尚4.∩t川1"〉千ourthjte们〈/己〉〈/1i〉

·〗|』■】〗□■●Ⅶ‖』■■■‖|□■‖」=■司

98

〈11c1a55=,0jte"ˉO"〉〈ahre千="1i∩低5·ht们1"〉+i代hjteⅦ〈/a〉 </u1〉 〈/djV〉

运行结果如下:

[〈[1e们e∩t∩tⅦ1at0x107941808〉’〈[1e爪e∩tbodyat0x1O79418〔8〉’<[1e爬∩tdivatOx1O79419O8〉’〈[1e爬∩tu1日t 0X107941948〉] [≤[1e们e∩tdiγat0x1079419O8〉] [′iteⅦˉ0|] [<[1e川e∩taatOx1079』18〔8〉] [〈[1e『∏e∩t 5p己∩at0x1o7941948〉] [〈[1e∏e∩t己at0X1079418C8〉]

[〈[1e"e∩t1iat0x1079』1948〉’〈[1e∩↑e∩t 1j己tOx1O79』1988〉’〈[1e爬∩t1jatOx1O79q19c8〉’〈[1副e∩t1iat Ox107941己08〉]

上述代码中第_次选择时,调用了a∩Ce5tOr轴’可以获取所有祖先节点°其后需要跟两个冒号, 然后是节点的选择器,这里我们直接使用*,表示匹配所有节点’因此相应返回结果是第-个11节点 的所有祖先节点,包括ht们1` body、djγ和u1°

第二次选择时,又加了限定条件’这次是在冒号后面加了d1γ’于是得到的结果就只有djγ这个 祖先节点了。

第三次选择时,调用了attr1bute轴,可以获取所有属性值’其后跟的选择器还是*’代表获取 节点的所有属性’返回值就是11节点的所有属性值。

第四次选择时,调用了〔∩11d轴’可以获取所有直接子节点。这里我们又加了限定条件,选取∩re「 属性为11∩代1.∩t∏1的a节点°

第五次选择时’调用了de5〔e∩da∩t轴,可以获取所有子孙节点°这里我们又加了限定条件——获 取5pa∩节点’所以返回结果只包含5pa∩节点’不包含日节点。

第六次选择时,调用了千o11o"1∩g轴,可以获取当前节点之后的所有节点°这里我们虽然使用的

·』】|」■]‖|」■』■|』】‖|」■】|‖(』■■Ⅵ■■|」■■勺{■■司口■司』■‖|』■‖||」=·|』■‖■■可{■∏|』■习‖■∏||■=■■■‖‖■】·‖||■■可‖|」】γ‖|』■∏|‖(』■||」=■司|‖」■γ』(

ht川1=etree.‖『川(te×t) re5u1t=∩t们Lxpath(0//1j[1]/a∩ce5toI: :*0) pr1∩t(reSu1t) Ie5u1t=ht们Lxpath(0//1j[1]/a∩〔e5tor: :d1γ‖) pri∩t(re5u1t) re5u1t=ht爪1.xp己t何(‖//11[1]/3ttrjbute: :*!) pri∩t(re5u1t) re5u1t=ht∏1.×p己t∩( 0//11[1]/Chi1d; :a[@侗re十="1j∩惯1.‖t∏1"]0) Pr1∏t(re5u1t) reSu1t二∩t刚1.xpat‖(!//1i[1]/de5ce∩d己∩t::5pa∩,〉 pri∩t(re5‖1t〉 resu1t =∩t肌1.xpat们(0//11[1]/千o11oⅦi∩g: :*[2]』) prj∩t(Ie5u1t) Ie5u1t≡∩t"1.xpat‖(‖//1j[1]/十o11ow1∩gˉ5jb1i∩g: :*‖) Pri∩t(Ie5u1t〉

是*匹配,但又加了索引选择,所以只获取了第二个后续节点°

第七次选择时’调用了+o11o"i∩gˉ51b11∩g轴’可以获取当前节点之后的所有同级节点°这里我 们使用*匹配,所以获取了所有的后续同级节点°

到现在为止,我们把可能用到的XPath选择器基本介绍完了。XPath功能非常强大,内置函数非 常多’熟练使用之后,可以大大提升提取HTML信息的效率°

本节代码参见: h忱ps://github.com/Python3WebSpjder/XPathTest°

‖||||‖

↑5.总结

『}

「矽|

『■【■『‖『【■『【『『■【【「|[厂|「{|■[『}[【■『「■■‖『|■「尸■「卜‖|[◆【‖‖|■■【‖|■■『||匹■『‖)[■「|}|■「「‖|■■「||■「||■【■∏[『|■=厅‖匹■‖||

BeautifUlSoup的使用

3.2

99

32Beaut|fu|Soup的使用 第2章介绍了正则表达式的相关用法,只是-旦正则表达式写得有问题’得到的结果就可能不是

我们想要的了。而且每一个网页都有_定的特殊结构和层级关系’很多节点都用jd或〔1a55作区分, 所以借助它们的结构和属性来提取不也可以吗?

本节我们就介绍—个强大的解析工具_BeautifhlSoup,其借助网页的结构和属性等特性来解析 网页。有了它,我们不需要写复杂的正则表达式,只需要简单的几个语句’就可以完成网页中某个元

{窒-

素的提取°

废话不多说’接下来就感受一下BeautlhllSoup的强大之处吧°

↑Beaut|『u|Soup的简介

简单来说’Beautih』lSoup是Python的一个HTML或XML的解析库,我们用它可以方便地从网 页中提取数据’其官方解释如下:

BeautifUlSoup提供—些简单的、Python式的函数来处理导航`搜索`修改分析树等功能°它是 -个工具箱’通过解析文档为用户提供需要抓取的数据,因为简单,所以无须很多代码就可以写出-

个完整的应用程序° BeautifUlSoup自动将输人文档转换为Unicode编码’将输出文档转换为utP8编 码。你不需要考虑编码方式,除非文档没有指定具体的编码方式,这时你仅仅需要说明一下原始编码 方式就可以了° BeautifUlSoup已成为和lxml、html5lib一样出色的Python解释器,为用户灵活提供 不同的解析策略或强劲的速度。

总而言之’利用BeautihllSoup可以省去很多烦琐的提取工作,提高解析网页的效率° 2ˉ解析器

实际上’BeautjfUlSoup在解析时是依赖解析器的’它除了支持Python标准库中的HTML解析器’ 还支持一些第三方解析器(例如lxml)°表3ˉ3列出了BeautifUlSoup支持的解析器。 表3-3 Beaut‖↑U‖Soup支持的解析器

Python标准库 LXMLHTML



使用方法

解析器

8ea‖ti十u15oup(阳rkup’ ′∩t们1.par5eⅢ!) Python的内置标准库、执行速 Beautj十u15oup(阳r灿p’ ′1x盯1|)







Python2·7.3或3.2.2前

度适中、文档容错能力强

的版本中文容错能力差

速度快、文档容错能力强

需要安装C语言库

解析器 LXMLXML

Be己uti十u15oup(阳Ⅲkup’ 0xⅦ1,)

解析器 hhnl5lib

速度快、唯一支持XML的解需要安装C语言库 析器

8eauti十u15oup(帕rkup’ °hm1S1ib!〉

提供最好的容错性、以测览器速度慢、不依赖外部扩 的方式解析文档、生成HTML5



格式的文档

通过表3ˉ3的对比可以看出,LXlⅡ解析器有解析HTML和XML的功能,而且速度快、容错能 力强,所以推荐使用它。

使用LXML解析器’只需在初始化Beautih』lSoup时’把第二个参数改为1x‖1即可: +ro∏‖b54mpoIt8eautj+u15ol』p

5oup=Be己l』ti十u15Oup(‖〈p〉‖e11O</p〉!b `1)《‖10) pri∩t(5oup.p。5tri∩g)

在后面,统—用这个解析器演示Beautifi』‖Soup的用法实例°

3.准备T作

BeautjfillSoup直接使用 在开始之前’请确保已经正确安装好BeautifUlSoup和lxml这两个库°Beautjf Pjp3安装即可’命令如下: p1p3 j∩5ta11be己utj十u15oup4

更加详细的安装说明可以参考: https://setupscrapecente1√beautjfUlsoup° 其安装方法见3.l节° 另外’我们使用的是lxml这个解析器’所以还需要额外安装lxml这个库,其安

』|‖」《|‖』□‖』‖‖‖可』‖{√』〗』】●Ⅵ‖|』■□‖|』】‖||□」|

第3章网页数据的解析提取

l00



以上两个库都安装完成后’就可以进行接下来的学习了°

4基本使用



下面首先通过实例看看BeautifUlSoup的基本用法:



们tⅧ1= """

<∩t川1〉<head〉〈tit1e〉丫‖eDor们ou5e5story〈/t1t1e〉〈/‖e日d〉 〈body〉

〈p〔1a55=′,t1t1e" ∩a爬≡‘odIo∩)ou5e0‖》≤b〉丁he0on∏ou5e55tory</b〉</p〉 <pc1a55="5tory"〉0∩〔eupo∩己tmet∩ereweret‖ree1itt1e515ter5j a∩dt∩eir∩a『∏e5were 〈己∩re千≡"bttp://exaⅧp1e·〔o"/e151e"〔1a55="Sj5ter" 1d=001j∩代1"〉〈!ˉˉ [15jeˉˉ〉</a〉’ 〈a‖re十="http://exa∏lp1e。〔o∩)/1aCje"〔1as5=005i5ter" jd="1i∩促2"〉〔acje〈/a〉日∩d 〈a∩Ie+≡"∩ttp://e×aⅧp1e。〔o川/ti11ie"〔1a55=!|515ter" id=001i∩旧"〉丁i11ie 〈/a〉j a∩dt∩ey1iγedatthebotto们o+日we11.</p〉 〈pc1己55=00story"〉…〈/p〉 『『 00

00

十ro们b5』j们port8e己ut1+u15oup 5Oup≡8e日uti十u15oup(∩tⅧ1’‖1X们1‖)

pr1∩t(5oup.prett1「y()) prj∩t(5oup。tit1e.5tr1∩g)

Q

运行结果如下: 小m1〉 〈打ead〉

〈tjt1e〉

「∩eDom℃u5e0S5tory 〈/t1t1e〉

〈/head〉

<body〉 〈pc1as5="tit1e" ∩己爬=卿dro『∏ou5e瞬〉 〈b〉

丁∩eDomou5e55tory 〈/b)

〈/p〉

〈p〔1a55=!`5tory"〉 0∩ceupo∩atmethereweret∩ree1itt1e5i5ter5j 日∩dt们eir∩a爬5were

<ac1日55="515ter" hre千≡0‘http;//exa肌p1e°〔o"/e1sje凹 jd="11∩k1"〉 〈!ˉˉ[151eˉˉ〉 〈/日〉 』

<己〔1a55="5i5ter|| hre+="∩ttp://e×a∏p1e。〔oⅧ/1日〔jeo0 id=||11∩抿2′0〉 [a〔ie </a〉

a∩d

〈a〔1a55="5j5ter" ‖Ie千="http://exa川p1e.c咖/tj11je" jd="1j∩k300) 「i111e 〈/a〉 》

a∩dt∩eγ1jγed己tthebotto们o十awe1L 〈/p〉

〈p〔1a55=!!5tory"〉 〈/p〉





3.2

BeautilUlSoup的使用

l0l

〈/body〉 〈/∩t川1〉

丁heDor「∏o‖5e!55torγ

这里首先声明一个变量∩tⅧ1,这是一个HTML字符串。但是需要注意的是,它并不是-个完整

的HTML字符串’因为body节点和∩t爪1节点都没有闭合°接着,我们将它当作第一个参数传给 8eautj千u15Oup对象’该对象的第二个参数为解析器的类型(这里使用1x"1)’此时就完成了 Be3u+u15oup对象的初始化。然后’将这个对象赋值给5oup变量°

之后就可以调用5oup的各个方法和属性解析这串HTML代码了。

首先’调用prett1千y方法。这个方法可以把要解析的字符串以标准的缩进格式输出°这里需要注 意的是’输出结果里包含bodγ和‖t川1节点’也就是说对于不标准的HTML字符串8eautj「u15oup’ 可以自动更正格式°这—步不是由prett1十γ方法完成的’而是在初始化8eauti十‖15oup的时候就完成了。

然后调用5oup.t1t1e.5tr1门g’这实际上是输出HTML中tjt1e节点的文本内容°所以’通过 5ouP.t1t1e选出HTML中的tjt1e节点’再调用5trj∩g属性就可以得到tjt1e节点里面的文本了°你 看’我们通过简单调用几个属性就完成了文本提取’是不是非常方便?

5.节点选择器

||





直接调用节点的名称即可选择节点’然后调用5tIj∩g属性就可以得到节点内的文本了。这种选 择方式速度非常快,当单个节点结构层次非常清晰时,可以选用这种方式来解析° 下面再用—个例子详细说明选择节点的方法: ‖tⅦ1= """

〈∩tⅦ1×head〉<t1t1e)丁∩eDomou5e05storγ〈/tit1e〉〈/head) 〈body)

〈pc1a55="t1t1e" ∩a川e≡"dIo们ou5e"〉〈b〉丁he0orⅧou5e‖55tory〈/b〉</p〉

〈Pc1a55="5tory"〉0∩〔eupo∩atmet∩ereweret‖ree1itt1e515ter5j a∩dt‖e1r∩己Ⅷe5were 〈己hre「="http://e×a川p1e。〔o们/e15je"〔1日55≡"si5ter" jd≡"11∩|〈1"〉〈!ˉˉ 〔15jeˉˉ〉〈/a〉’ <ahre「≡"∩ttp://exaⅧp1e。〔o们/1acje"〔1a55="5j5ter00 id≡0011∩低2"〉L己cje〈/a〉a∩d <a∩Ie十="‖ttp://exaⅧp1e·co"/t111je"〔1日55="515ter" 1d≡"11∩|〈3|!〉「111je〈/a〉】 a∩dt‖ey11vedatt们ebottoⅧo十awe11°</p〉 〈p〔1a55≡"5tory"〉…〈/p〉

十ro们b54j川port8eauti十u15oup 5oup=Beauti「u15Oup(∩t川1’‖1X刚1‖) pri∩t(SOup.t1t1e) prj∩t(type(5oup.tit1e)) prj∩t(5ouP.t1t1e·5tr1∩g) pri∩t(5oup·head) prj∩t(5oup.p)

运行结果: 〈tjt1e〉丁∩e0orⅧou5e|5 5tory〈/t1t1e〉 〈C1a55 |b54.e1e爬∩t.『ag!〉 「heDomou5e『s5torγ 〈head〉〈tjt1e〉丁∩emmo(』se055tory</tjt1e〉〈/head》

〈p〔1a55="tit1e00 ∩aⅦe≡"dro∩ou5e问><b〉丁heDorⅦou5e,55tory</b〉</p〉

这里依然使用刚才的HTML代码,首先打印出tit1e节点的选择结果,输出结果正是t1t1e节点 及里面的文字内容。接下来’输出tjt1e节点的类型’是b54.e1e‖e∩t.丁ag,这是BeautifUlSoup中— 个重要的数据结构,经过选择器选择的结果都是这种丁ag类型°「ag具有一些属性,例如5tr1∩g属性, 调用该属性可以得到节点的文本内容’所以类型的输出结果正是节点的文本内容°

输出文本内容后,又尝试选择了head节点’结果也是节点加其内部的所有内容°最后’选择了P 节点°不过这次情况比较特殊’因为结果是第一个p节点的内容’后面的几个p节点并没有选取到。 也就是说,当有多个节点时’这种选择方式只会选择到第一个匹配的节点’后面的其他节点都会忽略°



第3章网页数据的解析提取

6.提取信息

上面演示了通过调用5tr1∩g属性获取文本的值’那么如何获取节点名称?如何获取节点属性的 值呢?接下来我们就统一梳理_下信息的提取方式° ●获取名称

利用∩a们e属性可以获取节点的名称°还是以上面的文本为例,先选取tjt1e节点,再调用∩aⅧe属 性就可以得到节点名称: Pri∩t(soup.tit1e。∩a爬)

tit1e

||

运行结果:





●获取属性 ‖

一个节点可能有多个属性’例如1d和〔1aS5等,选择这个节点元素后’可以调用attr5获取其



所有属性: pI1∩t(5Oup.p.attr5) pri∩t(5Oup.p.attr5[ 0∩a爬‖])

运行结果: {‖c1a55‖: [ ‖tit1e! ]’ 0∩a爬‖ : ‖dro∏℃u5e0} drOⅧu5e

可以看到,调用attr5属性的返回结果是字典形式,包括所选择节点的所有属性和属性值。因此

要获取∩a川e属性’相当于从字典中获取某个键值,只需要用中括号加属性名就可以了°例如通过

■■■』■■〗‖■■司

attr5[|∩a"e|]获取∩a们e属性°

pIj∩t(5Oup.p[ 』∩a眶‖]) prj∩t(5Oup°p[ !C1a55』])

运行结果如下: dⅢ…uSe

[0tjt1e,]

这里需要注意,有的返回结果是字符串,有的返回结果是由字符串组成的列表°例如, ∩a‖∏e属

□■可』□司‖‖■■‖‖‖=■(』·□』□■』□‖|■=■Ⅵ

其实这种方式有点烦琐’还有一种更为简单的获取属性值的方式:不用写attr5,直接在节点元

素后面加中括号,然后传人属性名就可以了°样例如下:



性的值是唯-的’于是返回结果就是单个字符串°而对于〔1a55属性,_个节点元素可能包含多个 〔1a55,所以返回的就是列表°在实际处理过程中,我们要注意判断类型。 ●获取内容

这点在前面也提到过,可以利用5tri∩g属性获取节点元素包含的文本内容,例如用如下实例获 prj∩t(5oup。p·5tIj∩g)

运行结果如下: 『heDom℃u5e‖55torγ

再次注意_下’这里选取的p节点是第_个p节点,获取的文本也是第一个p节点里面的文本。 ●嵌套选择

在上面的例子中,我们知道所有返回结果都是bS4e1e肌e∩t.丁ag类型,Tag类型的对象同样可以继

·‖』日凸■】‖」■■`』‖`旬□■■■Ⅵ』■■】|‖|{|‖』□■■■。

取第_个p节点的文本:



可‖日

|‖‖



l02

■■●【■∏‖『『‖■■『仪β[卜β‖『》◆尸‖[□◆‖卜「|◆【〖|皿■『卜|||巴尸【『||■厂‖‖〗【尸『□匹『β巴尸●「‖匹『β

32BeautifUlSoup的使用

l03

续调用节点进行下一步的选择°例如,我们获取了∩ead节点’就可以继续调用head选取其内部的‖ead 节点; ∩t川1=

00

U0

00

〈htⅧ1〉〈‖ead〉〈t1t1e〉丁he0omou5e}55tory</tjt1e×/柯ead〉

<body〉

fro们b54mportBeautj「u15oup 5oup≡8eautj千u15oup(htⅦ1’ 01x刚10)

pr1∩t(5oup。head.tit1e) pri∩t(type(soup.head。tit1e)) prj∩t(so0p。head.tjt1e.5tri∩g)

运行结果如下:

|■「|止「|■「

<tjt1e〉『heDom℃u5e05。story〈/tit1e〉 〈C1aSSb5q.e1e爬∩t.『日g0〉 丁∩e0or帅use055tory

ˉ ‖ | } 山 「 | | · 「 | 凸 「 | | ■ | | ′ | 「 | | 卜 ‖ | ■「}[匠『|[’‖|β〗||■「|●}‖「协||■『|卜[{卜『『巳「‖【『卜‖■[【厂『■【『□【■【‖■■『‖『『『【◆【【■【『匹■【

|而选择的tit1e节点°第二行打印出了它的类 运行结果的第一行是调用head之后再调用tjt1e’而选择的tit1e节点°第二行打印 芭说’我们在丁ag类型的基础上再次选择,得到 型’可以看到’仍然是b54.e1e爬∩t.『ag类型°也就是说’我们在丁ag类型的基础上再次 那么就可以做嵌套选择了。 的结果依然是丁ag类型°既然每次返回的结果都相同,那么就可以做嵌套选择了。

也就是节点里的文本内容。 最后-行结果输出了tjt1e节点的Str1∩g属性’也就是节点里的文本内容。

7.关联选择 再以它为基准选 在做选择的过程中,有时不能一步就选到想要的节点’需要先选中某—个节点’再以 子节点、父节点、兄弟节点等,下面就介绍一下如何选择这些节点°

●子节点和子孙节点

选取节点之后,如果想要获取它的直接子节点,可以调用CO∩te∩t5属性,实例如下: ht刚1=口■回 <htⅦ1〉 〈∩ead〉

〈tit1e〉『hemr帅u臼e0s5toIW/tjt1e) 〈tit1e〉『hemr帅u臼e0s5toIγ</tjt1e) 〈/head〉

〈mdy)

〈p〔1a55=口5tory口〉

助〔eupo∩ati∏记there眶rethree1jtt1e5i5ter5β a∩dtheir∩a畦s腮re 〈ahre「=口http://eXa∩甲1e.Cm/e1Sie■〔1己55=口515teⅢ· id=佩1i∩代1·) 〈5p日∩〉[15ie</5pa∏〉 〈/a〉

〈ahre十=口bttP://exa卯1e.〔m/1a〔ie■〔1as5=回5j$ter口 jd=硕1i∩k2口)1a〔je</a〉 a∩d

〈ahⅢe千=口http目//exa呻1e.〔ml/ti11ie口〔1a55=口田i5ter■ id=口1j∩k3口)丁i11ie</a〉 a∩dthey1iγedatthebottmofa距11. </p>

〈PC1a55=口5torγ口〉…</p〉 ■■■

十rmb叫i咖rtBeauti十u15oup

SOup=Beal」ti「u15Oup(hm1’ ,1m10) pri∏t(SOup。p。〔o∩te∩t5)

运行结果如下:

[!\∩们ceupo∩atj爬there啮rethIee1itt1e5i5ter5βa∩dthe1r∩a爬5距Ie\∩!’ 〈a〔1a5s=■si5ter冈hre+=口∩ttp://exa呻1e。〔刚/e15je口 jd=口1j∩k1闻〉

〈5 pan〉[15ie〈/5p3∩〉

〈/日〉’ |\∩0’ <日c1己55=■5i5ter口∩re+="∩ttp://ex副印1e.〔m/1a〔je" jd=圆1j∩促2■)[3〔ie〈/a〉’ 0 \∏a∏d\∩‖’ 〈a〔1a55=圆5j5ter圃hIe十=口http://exa即1e.c咖/tj11ie厕 id宣n1j∩妇口〉丁i11je〈/a〉’`\∩a∩dthey1jγedatthemttmo十a脆11.\∩』]

[3

l04

第3章网页数据的解析提取

可以看到’返回结果是列表形式°p节点里既包含文本’又包含节点,这些内容会以列表形式统 -返回°

需要注意的是,列表中的每个元素都是p节点的直接子节点°像第一个a节点里面包含的5pa∩节 点’就相当于孙子节点’但是返回结果并没有把5pa∩节点单独选出来°所以说’〔O∩te∩tS属性得到 的结果是直接子节点组成的列表°

同样,我们可以调用Ch11dre∩属性得到相应的结果: 5oup=Be3uti+u15o‖p(∩m1’‖1x们10) pr1∩t(souP.P.〔hj1dre∩) +orj’ 〔hi1di∩e∩u爬Iate(5oup.p。〔hj1dIe∩): prj∩t(j’〔hj1d)

{|

千ro∩b54j们portBe己utj+u15oup

运行结果如下: 〈1j$tjtemtoIobjectatOx1o64十7dd8〉 O

g

0∩ceupo∩atmethereNeIethree1jtt1e515ter5j a∩dtheir∩a爬5眠re



1〈a〔1355="5j5ter"‖re千="∏ttp://exa朋p1e.〔咖/e15ie同 jd="1j∩促1"〉 〈5pa∩〉[1Sie〈/5pa∩〉 〈/a〉 2





3 〈日〔1己55="5jsteI"hre十二"http://exa川p1e·〔oⅧ/1a〔1e,, 1d=o01i∩代2"〉[acje</日〉



d

己∩d

q

5〈a〔1a55="sj5ter|0 hre千="∩ttp弓//exa们p1e.〔oⅦ/ti11je00 1d室"11∩代3"〉『i11je〈/a〉 6

a∩dthey1iγedatt∩ebotto们o+awe11。

还是同样的HTML文本’这里调用〔hj1dre∩属性来选择’返回结果是牛成器类型.然后’我们 用于Or循环输出了相应的内容。

如果要得到所有的子孙节点,则可以调用de5Ce∩da∩t5属性: +ro‖b541爪port8eautj十u15oup 5oup二8eauti+u15oup(∩m1’|1x"1‖) pri∩t(5oup.p.de5〔e∩d己∩t5) +orj’chi1d1∩e∩l』爬rate(5oup.p。de5〔e∩da∩t5): prj∩t(j’〔∩j1d)

运行结果如下:















| ●

〈ge∩eratorobje〔tde5ce∩da∩t5at0x1O65Oe678〉 0

O∩〔eupo∩atmethereweret∩ree1jtt1e5j5ter5j 己∩dthejr∩a爬5were







1<a〔1aS5=n515ter" ∩re+="http://eXa们p1e。〔O"/e1Sje" jd="1j∩促1"〉 〈5pa∩)[151e〈/5pa∩〉 〈/a〉





2

〕』日



〈5pa∩〉[15ie≤/5pa∩)

[15ie

4|







6

7〈a〔1己5s=p05j5teIo! ∩Ie十=00http://exa‖p1e·〔oⅦ/1ac1e00 jd=0011∩R200〉[a〔je〈/a〉 8laCje





9

a∩d ‖

‖{

q

■■【『巴尸『‖『「‖=■庐『‖卜「|巴■『‖》|■■【『‖「

32BeautjfUlSoup的使用

l05

10〈a〔1日55="sj5ter"hre于="∩ttp://ex3Ⅶp1e。〔o∏/tj11je" jd二"1j∩R3"〉丁111je</a〉

|}||

=■「■∏■■■「[β甘∏}||▲■尸‖[■『巴■「■「‖『邑尸止β「巳■「■■尸‖「|↑■■「卜■尸『「卜■歹「}■■∩}『■=尸|‖‖|{『↓卜‖『■尸|『仆『‖『|【■厂}匹■「|》「|=■■『』『|‖β■【口『

11丁i11ie 12

己∩dthey11γedatt∩ebottoⅦo+己we11·

你会发现,此时返回结果还是生成器。遍历输出~下可以看到’这次的输出结果中就包含了5pa∩ 节点,因为de5〔e∩da∩t5会递归查询所有子节点,得到所有的子孙节点。

3

●父节点和祖先节点

k

如果要获取某个节点元素的父节点’可以调用pare∩t属性: htⅧ1= "‖0 0| <‖t∏1〉 〈head〉

<t1t1e〉『he0orⅧou5e055torγ〈/tit1e〉 〈/head〉

〈body〉 <Pc1a55=005tory00〉

0∩〔euPo∩atmetheIeweret∩ree1jtt1e5jster5j 己∩dt∩eir∩a∏e5were 〈ahre十="bttP;//exa呻1e·co‖/e15je"〔1己s5="515ter" jd="1j∩浪1"〉 〈5p己∩〉[151e〈/5pa∩〉 〈/己〉

</p〉 <p〔1as5="5tory"〉…〈/p〉



000】 00

十ro"b54i∏por↑8eaut1+u15oup 5o0p=Beaut1千u15Oup(∩t们1’ !1xⅧ1‖) pri∩t(5Oup.a。paIe∩t)

运行结果如下: 〈P〔1a55="5tOry00〉

0∩〔eupo∩atmet∩ereweret∩ree1itt1e5i5ter5; a∩dt∩e1r∩a爬5were

<a〔1a55="5i5ter" ∩re千=圃bttp://exa"p1巳co们/e15je00 id=001i∩k1"〉 〈5pa∩〉[1sie〈/5Pa∩〉 </a〉

〈/p〉

这里我们选择的是第一个a节点的父节点元素。很明显, a节点的父节点是p节点’所以输出结 果便是p节点及其内部内容。

需要注意’这里输出的仅仅是日节点的直接父节点’而没有再向外寻找父节点的祖先节点°如果 想获取所有祖先节点’可以调用pare∩t5属性: ∩t∏1= """ 〈ht‖1〉

〈body〉

〈P〔1己5s="5tory00〉

<a∩re+="http;//eXa呻1e·〔咖/e15je"C1355=00Sj5ter" jd二"1j∩促100〉 〈5pa∩〉[15ie〈/5pa∩〉

〈/a〉

〈/p〉 ■0

0‖

00

+IoⅧbs啡i们port8eauti十u15oup

5〔up=8eauti十u15ouP(ht‖1’ ′1x∏1』) pIi∩t(tyPe(5ouP。己.pare∩t5))

p】i∩t(115t(e∩‖『∏erate(5oup.a.pare∩t5)))

运行结果如下; 〈〔1aS5 0ge∩emtor0〉 [(o’〈p〔1a55="5tory|‖〉

〈§‖ c1a55="5j5ter" ‖re十二"http://exaⅦp1e·〔o"/e15je" jd="1j∩恨1"〉

〈!|pa∩〉[15ie〈/5p日∩〉 〈/a〉



〈/p〉)’(1’<body〉 〈P〔1a55≡"5tOry"〉 〈日〔1a5s≡"5i5ter" hre千=0,∩ttP://e×a爪p1e·〔oⅦ/e15ie" jd=0,1j∩低1"〉 <5pa∩〉〔15ie</5pa∩〉 〈/a〉

〈/p〉



‖(

〈/body〉)’(2’<hm1〉 <body〉 <pc1己s5="story"〉 〈a〔1a5s="5i5ter"hIef=00http://exa∩p1e·〔m/e15je" id=,‘11∩促1"〉 <5p己∩〉[15ie</5p日∩>

||

第3章网页数据的解析提取

l06

〈/a〉

〈/p〉





〈/日〉



厂■■

</bodγ〉〈/ht们1>)’〈〕’ 〈hm1〉 <body〉 〈pc1a55="5tory"〉 〈a〔1a55="5i5ter00 hre十="http5//e×a们p1e。〔α∏/e15je|| id="1j∩低1"〉 〈Spa∩〉〔151e</5pa∩〉 〈/p〉



</body〉</ht‖1〉)]

可以发现’返回结果是生成器类型°这里用列表输出了其索引和内容’列表中的元素就是a节点 的祖先节点°



●兄弟节点



子节点和父节点的获取方式已经介绍完毕,如果要获取同级节点,也就是兄弟节点’又该怎么办

‖‖|

|」

呢?实例如下: ∩t‖1= """

<∩t‖1〉

</a〉

‖e11O a∩d

〈ahre十=曰http://exa呻1e.〔咖/tj11ie00 〔1a5S=口5j5teI口 id=■1j∩k]口〉「i11ie</日) a∩dtheγ1jγedatthebott咖o千awe11. ·</p〉

「I咖bsq1呻ort8e己Mtj+u15onp 5qup=8eautj十l」15oup(‖tⅧ1’ ,1×Ⅷr) prj∩t(『‖ext5jb1j∩g0 ’ 5oup.a·∩ext5jb1i∏g)

Prj∩t(!pIeγ5jb1j∩8!’ 5o卯。a.previo055jb1i∩g) PIj∏t(‖‖ext5jb1i∩g5! ’ 1iSt(e∩u贬rate(5oup.3.∩ext5ib1mg5))) Prj∏t(|preγ5ib1j∩g50’ 1ist(e∩u爬rate(soup.a。prevjous5jb1i∩gs)〉)

运行结果如下: №Xt5jb1j∩g ‖e11O

preγ5jb1j∩8 0∩ceupo∩atj眶t∩ere胀rethIee11tt1esi5teI5】 a∩dthejr∩a爬5NeIe

1a〔1e" id=徽11∩Ⅸ2阐〉la〔ie〈/a〉)’(2’ |\∩a∩d\∩`)’(3’<日〔1己55≡雨5i5teI"hIe「=

"http;//exa"p1e.c刚/tj11je00 id≡"1j∩捉3徽〉∏11ie</a))’(4’|\∩a∩dt‖ey1iγedatthebott咖o十3 Ne11。\∩0)]

preγ5jb1i∩8B [(o’ !\∩0meupo∩atj爬thereⅣeIet∩Iee1jtt1e5j5ter5j 己∩dtheiI∩日贬5

肥re\∩0)]

|{|

‖ext5ib1j∩g5 [(0’ 0\∩‖e11o\∩0)’(1’〈3〔1a55≡"5i5teI" hre十=顾http://exa们p1e.c咖/



‖‖词‖』勺‖』■可』‖{■■司‖|‖(』■|(』■可二■

■■∏



」|{

〈己‖re+="httP://ex己呻1e.〔咖/13〔je回〔1355=口5i5ter闪 jd=口1i∏假2■〉tacje</己〉

■■∏■■〗

〈bodγ〉 〈p〔1a5S=005tOry"〉 0∩〔e l』po∩ati∏论t∩eIe眶Iet∩Iee1itt1e5jsteI5j a∩dthe1I∩a爬5were <ahre千="http目//e×a‖∏p1e。〔咖/e15ie"〔1日55="5ister侧 jd二,,1j∩恨1"〉 〈5pa∩〉〔15je〈/5p己∩>

3.2

BeautilillSoup的使用

l07



『 [尸【【■『‖巴■厂『|■■■■「

可以看到’这里调用了4个属性。∩ext51b11∩g和preγjou5 5jb11∩8分别用于获取节点的下—个 和上~个兄弟节点, ∩ext5jb11∩g5和preγ1ou55jb1j∩g5则分别返回后面和前面的所有兄弟节点。 ●提取信息

广‖卜‖■

前面讲过关联元素节点的选择方法,如果想要获取它们的_些信息,例如文本`属性等,也可以 用同样的方法,实例如下: ht"1= ""|| 〈htⅧ1〉

也尸『■‖‖|‖·■■「

〈body〉 <pc1a55="5tory"〉 0∩〔eupo∩atj爪et∩eIe训erethree1jtt1e5j5ter5j a∩dt∩eir∩a川e5were

·』

<ahre+="∩ttp://exa川p1巳〔oWe151e" c1a55="5isteI" jd=|『1j∩k1"〉Bob</a〉〈a‖re十= ||http://exa们p1e°〔o爪/1a〔je" c1as5="5i5teI00 id="11∩|(2"〉[acje</a〉

■■「卜■尸队△■■巴『「■β「矽『△■=卜■厂

〈/p〉 0■

00

〗0

+ro们b54j肌port8eal」t1+u15oup 5OUP≡8eaUtj+U15OUP(‖t川1’|1m10) pr1∩t(Wext5jb1i∏g:‖) pri∩t(type(5oup.a.∩ext5jb1j∩g)) pri∩t(5oup.a.∩ext5jb1j∩g) pr1∩t(5oup.a.∩extsib11∏g·5tri∩g) pr1∩t(0pare∩t:‖) pri∩t(type(5Oup.己·p己re∩t5))

p【j∩t(1j5t(5oup。a.p日re∩ts)[0]〉 prj∩t(1jSt(SOup。a·p己re∩t5)[0].attr5[0〔1己55‖])

运行结果如下: ‖eXt5ib1j∩8: 〈〔1a55 ‖b54e1e们e∩t°丁ag!〉

〈ac1a55="5j5ter" ∩re+="http://exa们p1e。〔o∩/1a〔ie" id="1i∩长2"〉lacie〈/己〉 □



‖‖

[aC1e

paIe∩t;

〈〔1a55 !8e∩emtor!〉 〈pC1a55=00story,,〉

0∩ceupo∩atmetherewerethree1jtt1e5j5ter5; a∩dt∩ejr∩己川e5were

p

■巳■】■尸卜‖■■■『‖【『‖■尸‖□●旧

〈己〔1a55="5ister" hre「=圆http8//exa刚p1e。〔o∏‖/e151e" id="1j∩|(1"〉8ob〈/a〉<己〔1a55=!′5j5ter" bre千=o0∩ttp://exa"p1e.co们/1acje" id=0|1j∩炮"〉[a〔ie〈/3〉 </p〉 [|5tOry|]

如果返回结果是单个节点,那么可以直接调用5tIj∩g、attrS等属性获得其文本和属性;如果返 回结果是包含多个节点的生成器,则可以先将结果转为列表,再从中取出某个元素,之后调用5tri∏8` attr5等属性即可获取对应节点的文本和属性° 8.方法选择器

~■「■∏■β■

·尸′匹∩『)■■}}‖■「卜‖止■「卜卜『||■【■∏■「}|》●■『■■∏



前面讲的选择方法都是基于属性来选择的’这种方法虽然快,但是在进行比较复杂的选择时,会 变得比较烦琐,不够灵活°幸好,BeautifUlSoup还为我们提供了—些查询方法,例如+1∩da11和十1∩d 等,调用这些方法’然后传人相应的参数’就可以灵活查询了° ●千1∩d曰]]

「i∩da11,顾名思义就是查询所有符合条件的元素,可以给它传人一些属性或文本来得到符合条 件的元素’功能十分强大°它的API如下: 十j∩da11(∩aⅦe 」 attI5 ’ re〔urSjγe , teXt ’ **蜘arg5) ●∩a∩e

我们可以根据∩aⅧe参数来查询元素’下面用-个实例来感受一下:

第3章网页数据的解析提取

l08

htⅧ1=| |!

〈diγc1a55二"Pa∩e100〉 〈djγc1日55="pa∩e1ˉ∩eadi∩g"〉 〈h4〉‖e11O</∩4〉 乓/d1γ〉

〈diγ〔1己5S="Pa∩e1ˉbOdy"〉 〈u1C1己S5="1i5t" id="1j5t-1">

<1j〔1日S5=“e1eⅦe∩t00〉「OO</11≥ 〈1i〔1日5S≡"e1e"e∩t"〉6己r</1i〉

<1jC1己55≡"e1e们e∩t"〉〕ay</1i〉 〈/l』1〉

<1i〔1a55="e1eⅧe∩t"〉「oo</1j〉

〈1jC1a55≡||e1eⅧe∩t"〉8ar〈/1i〉 〈/u1〉

〈/diγ〉 </djV〉 ■

0

0

千roⅦb541‖port6eaut1+u15oup

5Oup=8e己utj千u15Oup(∩tⅧ1’ 01XⅦ1』) Pri∩t(5oup。十i∩d己11(∩a川e=|u1|〉) pr1∩t(type(5oup.伍∩da11(∩a|"e=|u1|〉[o]))

运行结果如下: [〈01〔1己5S≡"115t" jd="1i5tˉ1"〉

司·‖‖■■】】‖』·〗■■∏■|‖】〖■■■■可』』勺‖』■■■‖■■■】』■■□‖■■可

〈u1C1a55=001i5t1i5tˉ5们a11" id≡"1j5tˉ200〉

〈11〔1a55=′0e1e"e∩t"〉「OO〈/11〉 <1i〔1己55="e1e爬∩t"〉8己r</11〉

〈11〔1日55="e1印e∩t"〉〕ay〈/1j) 〈/u1〉’〈u1〔1a55="1j5t1j5t≡5‖a11" id="115tˉ2"〉 <11〔1a55≡"e1eⅦe∩t"〉「OO</1i〉 〈11C1aS5="e1eⅦe∩t"》8ar</1j〉

〈/u1〉]

〈〔1aS5 ‖b54。e1e『∏e∩t.丁ag|〉

这里我们调用了千1∩da11方法,向其中传人∩a‖e参数’其参数值为u1’意思是查询所有u1节

点°返回结果是列表类型’长度为2’列表中每个元素依然都是b54e1e"e∩t.「ag类型° 因为都是『己g类型,所以依然可以进行嵌套查询°下面这个实例还是以同样的文本为例,先奋询 所有u1节点’查出后再继续查询其内部的1j节点: 千Oru1j∩5Oup.十i∩d_a11(∩a们e≡0u1|): Pr1∩t(u1.十1∩da11(∩己们e=01j0))

运行结果如下:

[〈1j〔1a55="e1e「∏e∩t"〉「oO〈/1j〉’〈11〔1a55≡"e1e|∏e∩t"〉8ar〈/11〉’〈11c1a55="e1e眠∩t"〉〕aγ</1i〉] [〈1j〔1己55≡"e1e爬∩t"〉『oo〈/1i〉’〈1jc1a55≡口e1e爬∩t"〉Bar〈/1i》]



‖ 0



























返回结果是列表类型,列表中的每个元素依然是「ag类型。 接下来我们就可以遍历每个1j节点,并获取它的文本内容了。

运行结果如下:

[〈1ic1己55≡"e1印e∩t{|〉「oo〈/11〉’〈1j〔1a55≡"e1e‖∏e∩t"〉8己r〈/1j〉′〈1j〔1日s5="e1e|∏e∩t"〉〕aγ〈/1j〉] 尸

卜OO

8aI ~

」ay

8ar

(|■■‖|‖』■■■‖」■】■■{■‖□■Ⅷ】日■■■】·■

[〈11〔1a55≡"e1e们e∩t"〉「oo〈/1j〉’ <1j〔1a55≡"e1e川e∩t"〉8己r〈/11〉] 「OO

‖|■■‖{Ⅵ]』■■』叮』■』■】||」·‖凸■口‘■曰

千Oru1j∩5Oup。千i∩d-a11(∩a川e=‖u1|): pr1∏t(u1。千j∩d日11(∩a|∏e≡!1j|)) 十or1i1∩u1.+i∩d己11(∩a们e=!1j』): pr1∩t(1j。5trmg)

【∩■【■■【■『·[【=■「|》「[◆『■■β}|■■「β|△■「|『|「■厂|■■■|

32BeautifillSoup的使用

l09

●attrS

除了根据节点名查询,我们也可以传人-些属性进行查询’下面用-个实例感受-下: ht们1=0 ‖ 『

■■「

<djγ〔1a55≡闻pa∩e100〉 〈djγ〔1a55=喇p己∩e1ˉ打eadj∩g"〉

■■■】■

〈M〉‖e11O</h4〉 ≤/diγ〉

尸|》

<diγc1a5s室"p3∩e1ˉbody"〉

厂3

Ⅱ■■■■■■

〈u1〔1a5S≡"1j5t" jd="1j5tˉ1圃∩己爬="e1e∏论∩t50|〉 <1jC1aS5=0‖e1e∏记∩t"〉「oO</1i〉

〈1j〔1a5S="e1e爬∩t阅〉8ar〈/11〉

(1i〔1a55="e1e∏把∩t"〉〕ay〈/1i〉

p

〈/01〉



■厂‖‖「=■厂}‖巴尸}△》『■尸〖『巴■「

〈u1〔1a55="1j5t1j5tˉ5‖a11" 1d="115tˉ2"〉 〈11〔1a55="e1e们e∩t■〉「oo</11〉

〈1j〔1a55=00e1e们e∩t"〉8ar</1i〉 〈/u1〉 〈/djγ〉 </d1γ〉 0

0

0

+r咖b5』mportBe3l」↑i+u15oup

soup=8ea‖ti千u15oup(∩tⅦ1’|1m1‖) pr1∏t(5oup.+i∩da11(己ttrs.{|id, : |1i5tˉ1|}))

pri∩t(5Oup。十j∩da11(attr5≡{0∩副∏e』: ′e1e∏记∩t5|}))

运行结果如下: ■■厂)

[<u1〔1a55=闻1i5t" jd≡"1j5tˉ1. ∩a爬="e1e爬∩t5圃〉 〈1iC1王二=qe1e爬∩t|0〉「oo</11〉 〈1iC1a55="e1eⅦe∏t"〉8ar〈/1j〉

〈1iC1a55=αe1eⅧe∩t"〉]ay</1j〉 ■尸■■□尸β‖巳■‖□■■『|‖凸【β|▲■■■『

〈/u1〉]

[〈u1C1a55="1j5t|| jd="1i5tˉ1" ∩己爬=圆e1e∏记∩t5"〉 <1i〔1as5=we1e爬∩t"〉「Oo〈/11〉 〈1jC1a55=00e1e爬∏t")0ar</1i)

〈1iC1a55=曰e1e们e∩t|‖〉〕己y〈/11〉 〈/u1〉]

泣里杏询的时候’传人的是attr5参数,其属于字典类型°例如’要查询jd为115t_1的节点’ 就可以传人attr5={|jd|: 』1i5tˉ1,}作为查询条件’得到的结果是列表形式’列表中的内容就是符合 jd为1j5t≡1这一条件的所有节点。在上面的实例中,符合条件的元素个数是1,所以返回结果是长度 为l的列表。

p

对于一些常用的属性,例如id和C1a55等’我们可以不用attr5传递°例如’要查询1d为1j5tˉ1 的节点’可以直接传人jd这个参数°还是使用上面的文本’只不过换—种方式来查询:

■厂‖·■尸□坠■=|‖|■厂‖「■■尸‖包尸『)∩■『●「|仿■■「仇■「‖■■‖■β■■■「

+r咖b54mportBeautj十u15oup

5O‖p≡8eautjfu15Oup(∩m1’|1m1|) prj∩t(5oup.+j∩da11〈id≡』1i5t-1|)) prj∩t(so0p.千1∩da11(〔1as5=!e1e∩记∩t|)) 运行结果如下: [〈u1〔1己5S≡闻1j5t" jd="1jStˉ1闰〉 〈1j〔1a55■口e1e∏冶∩t闪〉POo〈/1j〉 〈1iC1a55■■e1e爬∩t")B己r〈/1j〉

〈1jC1aS5■"e1eⅧe∩t"〉〕己y</1i〉 〈/l」1〉]

[〈1i〔1a55≡阐e1e爬∩t"〉『oo</11〉′〈1j〔1日55□阅e1e爬∩t"〉8ar〈/1i〉’〈11〔1a55≡"e1e『∏e∩t"〉〕aγ〈/1i〉’〈11

〔1a55=!』e1咖e∩t"〉「oo〈/1j〉’〈1iC1a55■圆e1e刚e∩t"〉8ar</1i》]

这里盲接传人id=|115tˉ1|’就可以查询jd为1i5tˉ1的节点元素了°而对于C1a55来说,由于C1日55

在python里是一个关键字’所以后面需要加一个下划线’即c1a55=`e1e"e∩t』’返回结果依然是「ag对

||

第3章网页数据的解析提取

ll0

象组成的列表° ●text

text参数可以用来匹配节点的文本,其传人形式可以是字符串’也可以是正则表达式对象,实例

1呻Ortre ‖t∩1=‖ 0 ‖

〈djγ〔1a55="p己∩e1阐〉 〈diγc1己55=阅pa∩e1ˉbody00〉 〈a)‖e11O’ t∩iS15a1j∩k</a〉 〈a〉‖e11o’ t‖i5i5a1i∏假’too</a〉 </diγ〉 0





</djγ〉 0

‖■■‖]■】』■■■■■】‖‖」■]白{

如下:



0

十rO∏b5qmpOrt8eauti千u15Oup 5oup=Beautj「u15oup(btⅧ1’ 01xⅧ10〉

prj∩t(5ol」p.千j∩da11(text=re.c咖pj1e(‖1i∩低‖)))

运行结果如下: [ ‖‖e11O’ thi5j5a1j∩‖’ 0‖e11O’ t∩j5j531j∩k′ tOO』]

这里有两个a节点,其内部包含文本信息°这里在十1∩da11方法中传人text参数’该参数为IF 则表达式对象,返回结果是由所有与正则表达式相匹配的节点文本组成的列表。

q

q

0



‖|



·

●千i∩d

除了于1∩da11方法’还有+j∩d方法也可以查询符合条件的元素,只不过十1∩d方法返回的是单个 元素,也就是第一个匹配的元素,而十1∩da11会返回由所有匹配的元素组成的列表°实例如下:



ht∏1≡| 0 ‖

<djγc1as5="Pa∩e1"〉

q

〈djγ〔1a55=||pa∩e1ˉhe己di∩g"〉 〈M〉‖e11O〈/‖4〉

q

〈/d1V〉

〈djγc1日55=词pa∩e1ˉbody"〉 〈u1〔1a55=阑115t厕 jd="1j5tˉ1"〉

〈1i〔1a55="e1e‖e∩t00〉「oo</1j〉 <11〔1a55=阑e1e‖e∩t"〉B己r</11〉







<1j〔1a55="e1e刚e∩t"〉〕ay</1i〉 〈/u1〉

〈u1〔1己55="1j5t1j5tˉ5Ⅶ己110『 id="1j5tˉ2"〉 <1j〔1a55="e1e‖∏e∩t阂〉「oO〈/1i〉 ≤1j〔1a55=00e1e『∏e∩t0|〉8ar</1i〉 〈/u1〉



| q

〈/d1γ〉 0

</d1γ〉 ■

0

0



「rO‖∏b54加pOrt8eautj+u15Oup 5Oup=8eautj+l」15Oup(∩m1’↑1x∩1!) pri∩t(5o0p。千j∩d(∩a爬≡‖u1‖)) prj∏t(type(5oup。千j∩d(∩a爬=|u1|))) pri∩t(5oup.「j∩d(〔1己55≡‖1j5t|))

运行结果如下:

| q q





〈u1〔1a55=日1j5t" jd="1i5tˉ1"〉 〈1iC1己55≡00e1e爬∩t"〉『OO〈/1i〉 〈1j〔1a5s=0℃1e爬∩t"〉8ar〈/1j〉

‖ 勺

〈11〔1a55■00e1e爬∩t"〉〕ay〈/1j〉 </01〉

〈〔1a55 `b54。e1e爬∩t°『ag‖〉 〈u1C1aS5="1jSt" jd="115tˉ1"〉 〈11〔1a55="e1e∏记∩t"〉「oo〈/11〉 〈1jC1a5S="e1e眶∩t阐〉8己r</1i〉













【■■【■∏□■■『‖凸■∏『【「『■庐『|卜‖【β『|》「|八■『■【‖〖■「‖『|■■「卜α〗}匹庐厂卜`口■■「·『||[■厂『|『『〔广卜『‖「|[■厂|■【『|‖|●「‖|『[■■「|||止■厂『』『[■『‖|

3.2

BeautiihlSoup的使用

lll

〈1jC1a55="e1e‖‖e∩t"〉〕己y〈/1i〉 </u1〉

可以看到,返回结果不再是列表形式’而是第_个匹配的节点元素,类型依然是「ag类型° 另外还有许多查询方法,用法与介绍过的十1∩da11` +j∩d完全相同’区别在于查询范围不同,在 此做_下简单的说明°

□+1∩d-pare∩t5和「1∩d-pare∩t:前者返回所有祖先节点’后者返回直接父节点° □十i∩d∩eXt51b11∩g5和十1∩d川eXt51b1j∩g:前者返回后面的所有兄弟节点,后者返回后面第 —个兄弟节点°

□+i∩dˉprev1ou55ib11∩g5和十j∩dˉpIeγjo05s1b11∩g:前者返回前面的所有兄弟节点’后者返回 前面第一个兄弟节点°

□+1∩da11∩eXt和十1∩d∩eXt:前者返回节点后面所有符合条件的节点,后者返回后面第—个符 合条件的节点。

□+1∩da11ˉpreγ1ou5和十1∩d_prev1ou5:前者返回节点前面所有符合条件的节点’后者返回前面 第一个符合条件的节点°

9CSS选择器





BeautifUlSoup还提供了另外一种选择器—CSS选择器。如果你熟悉Web开发,那么肯定对CSS 选择器不陌牛^

使用CSS选择器,只需要调用5e1ect方法,传人相应的CSS选择器即可°我们用一个实例感受 一下: ∩tⅧ1=0 0 『

〈djγ〔1a5s二"pa∩e1"〉 〈diγ〔1a55="pa∩e1ˉ‖eadmg,!〉 〈∩4〉‖e11o</M〉 </djγ〉

〈djγ〔1a55=||pa∩e1ˉbody"〉 〈u1〔1aS5="1jSt|| jd="1jStˉ1"〉

〈1ic1a55=!|e1e‖∏e∩tⅫ〉「oo〈/1j〉 〈1jC1a55="e1e爬∩t"〉BaI〈/1j〉

〈1iC1a55="e1eⅦe∩t"〉〕ay</1i〉 〈/u1〉

〈u1〔1日S5=,『1i5t1j5tˉS‖a11" id="1j5tˉ200〉 〈1iC1a55≡"e1e‖∏e∏t"〉「OO〈/1i〉 〈1i〔1a55="e1e『∏e∩t"〉Bar〈/1i〉 〈/u1〉

〈/djγ〉 〈/djγ〉 0

0

0

千ro们b541川port8eauti十u15oup

5Oup=Bea‖ti+u15OMp(∩t们1’ !1X"1‖) pIj∩t(5o0p。5e1e〔t(‖ .pa∩e1 ·pa∩e1ˉ∩eadi∩g‖)) pri∩t(5oup.5e1ect(0(」11i0)) pri∩t(5oup°5e1e〔t(!#1i5tˉ2 .e1e|∏e∩t!)) prj∩t(type(5oup.5e1e〔t(!u10)[O]))

运行结果如下: [〈djγ〔1a55="pa∩e1ˉ∩eadi∩g"〉 〈∩4)什e11O〈/M〉

〈/div〉]

[<1jC1a55≡||e1e|∏e∩t"〉「OO</1i〉’〈11〔1a55≡||e1e『‖e∩t!‖》Ba工〈/1i〉’ <11C1a55="e1e们e∩t"〉〕ay〈/11〉′〈1i C1a55="e1eⅧe∩t||〉「oo〈/1j〉’〈11〔1a55雪"e1e阳e∩t"〉8ar〈/1j〉]

[<11〔1a55="e1e爬∩t"〉「oo</11〉’〈1j〔1a55≡,』e1e∏记∩t"〉8ar</1i〉] 〈C1a55 |b54·e1e爬∩t·『ag|〉



在最后—句中’我们打印输出了列表中元素的类型°可以看到’类型依然是Tag类型°

] ■ ∏ ‖ ■ ■

这里我们用了3次CSS选择器,返回结果均是由符合CSS选择器的节点组成的列表°例如’ 5e1ect(!u111』)表示选择所有u1节点下面的所有1i节点’结果便是所有1i节点组成的列表。

||

|□』■∏|‖』·]|』‖

第3章网页数据的解析提取

ll2

●嵌套选择

尸巴

5e1e〔t方法同样支持嵌套选择’例如先选择所有u1节点’再遍历每个u1节点’选择其1j节点, 实例如下:

运行结果如下:

[〈1jc1a55="e1e爬∩t"〉「oo〈/1j〉′〈11〔1己55="e1e「‖e∩t")Bar〈/1j〉’〈11〔1日5s="e1eⅧe∩t"〉〕aγ〈/1j〉] [<1i〔1a55≡"e1e眶∩t"〉「oo〈/1i〉’ <11〔1己55≡』』e1e|"e∩t"〉8日r〈/1i〉]

可以看到’正常输出了每个ul节点下所有ll节点组成的列表° ●获取属性

既然知道节点是「a8类型’于是获取属性依然可以使用原来的方法°还是基于上面的HTML文本’ 这甲尝试获取每个u1节点的1d属性: +roⅦbs4mport8eautj十u15oup

5oup二Beaut1「u15oup(ht刚1’ 01xⅦ1‖) 十Or (」1i∩5Oup。5e1e〔t(0u10): pIi∩t(u1[ !jd|]) pr1∩t(01.attr5[ 』jd‖ ])

1j5tˉ1 1j5tˉ1

q

||

运行结果如下:

凸·‖‖‖■■■□■〗□勺‖二■可』□■■■■』‖』■■■司』■■|」□□‖|‖』■

千rO川b54加pOrtBeaut1+u15Oup 5oup≡Be己utj千u15oup(ht‖1’ 』1xⅦ1‖) +oru1i∩5Oup.5e1e〔t(|u10): pIj∩t(u1.5e1eCt(01i‖))

115tˉ2

1j5tˉ2



可以看到’直接将属性名传人中括号和通过attr5属性获取属性值,都是可以成功获取属性的。 ●获取文本

要获取文本’当然也可以用前面所讲的5tr1∩g属性°除此之外’还有一个方法,就是getˉteXt’

q

q

实例如下: 十ro‖b541们port8eautj十u15oup 5Oup≡8e己αtj{015Oup(∩t们1」 ‖1xⅧ10) 十Or1j1∩5oup.5e1e〔t(!110〉;

0

prj∩t(℃et丁ext: 0 ’ 1i。get=text(〉) pIi∩t(′5trj∩g: ‖ ’ 1i.5tI1∩g)

运行结果如下:

5tr1∩g: 8ar Cet丁ext: 〕ay StⅢj∩g: ]ay 5tri∩8: 「OO 6et「ext$ Bar

Str1∩g; Bar

二者的实现效果完全_致,都可以获取节点的文本值∩

q

(|(|』|√

6et丁ext;「oo

』□□‖‖(」』|

5trj∩g吕 「oo Cet丁e×t: 8aI

「 ■ ■ ‖ ■

Cet丁ext:「oo





| |



P

‖0

0





0









3.3

pyquery的使用

ll3

↑0.总结

到此’ BeautifillSoup的介绍基本就结束了’最后做-下简单的总结。

□推荐使用LXML解析库’必要时使用∩t阳1.par5er° □节点选择器筛选功能弱,但是速度快。

□建议使用+1∩d、+1∩da11方法查询匹配的单个结果或者多个结果° □如果对CSS选择器熟悉’则可以使用5e1eCt选择法°

本节代码参见: h仗ps://gjthub.com/Python3WebSpjder/Beautih』lSoup爬st°

3ˉ3 pyque「y的使用 32节介绍了BeautifUlSoup的用法’这是—个非常强大的网页解析库,你是否觉得它的_些方法 用起来有点不适应?有没有觉得它的CSS选择器的功能没那么强大?

如果你对Web编程有所了解,如果你比较喜欢用CSS选择器’如果你对jQuery有所了解’那么 这里有-个更适合你的解析库—pyquery°

0

接下来’_起感受—下pyqueIy的强大之处。



0





0











↑.准备工作

同样’在本节开始之前请确保已经安装好了pyquery库,如没有安装,可以使用pip3安装: pjp3j∩5ta11pyquery

更加详细的安装说明可以参考: https://setup.scrapecente∏pyquery° 安装完成之后,我们便可以开始接下来的学习了° 2初始化

在用pyquery库解析HTML文本的时候’需要先将其初始化为一个pyQuery对象° 初始化方式有很多种’例如直接传人字符串`传人URL、传人文件名,等等°下面我们详细介绍 -下这些方式°



●字符串初始化



这种方式是直接把HTML的内容当作初始化参数,来初始化pγQuery对象°可以用一个实例来感







受一下: ∩t『n1≡ | 0 |

〈diγ〉

<o1〉

〈1j〔1a55="iteⅧˉ0"〉千iI5titeⅧ</1j〉

〈1i〔1a55=00ite‖ˉ1"〉<日hre千="1i∩代2°hm100〉5eco∩d1te‖〈/a〉〈/1i〉

〈11〔1a55="iteⅧˉO己〔tiγe"〉〈ahre+="11∩促3·∩t们1"〉<5p日∩c1a55=|bo1d"〉t∩iId1te‖</5p己∩〉</a〉〈/1j〉 〈1j〔1a55="ite川ˉ1a〔tjγe"〉<日∩re+="1j∩代4·htⅦ1"〉+ourt∩1teⅧ〈/a〉〈/1i〉 <11c1a5s="jte‖ˉO"〉〈ahre十="11∩促5.ht们1"〉于j于t‖ite阳〈/日〉</11〉 </u1〉 </djγ〉 0

0



+Io川pyquery1丽poItpyQ‖erya5pq doc=pq(∩t们1) pri∩t(do〔(‖1i‖))

这里首先引人pyQuerγ这个对象’取别名为pq。然后声明一个长HTML字符串’并将其当作参数 传递给pγQuery类’这样就成功完成了初始化°接着,将初始化的对象传人CSS选择器°在这个实例 中’我们传人1j节点’这样就可以选择所有的1i节点了°



第3章网页数据的解析提取

ll4

运行结果如下: <1j〔1己55="jte‖ˉ000〉千ir5t 1teⅧ〈/1i〉

〈1jc1a5S=00ite‖ˉ100〉〈a"re十="1i∩仪2.ht川1"〉5eco∩diteⅦ〈/a〉〈/1i)

〈1jc1a5s=00it印ˉ0actjγe00〉<a∩Ie十≡0011∩旧。htⅦ100〉<5pa∩〔1日55=!bo1d"〉t∩irdjte∏〈/5pa∩〉〈/a〉〈/1i〉 <11〔1a55="it印~1actiγe"〉<ahre十="1j∩刚°何tⅧ1"〉「ourthiteⅦ〈/a〉</1i〉 〈1ic1a55="ite∏ˉO"〉〈ahre十="1i∩促5.‖t们1"〉+i千t们jte‖</a〉</1i〉

]|‖

●URL初蛤化

初始化的参数除了能以字符串形式传递’还能是网页的URL,此时只需要指定pyQuery对象的参

』』

数为l」r1即可:

运行结果如下:

□■∏|≤勺‖|‖‖

千roⅦpyquerγ加portpyQuery己5pq

do〔 =pq(ur1=|http58//〔uiqi∩g〔a1.co们0) pri∩t(dO〔(|tit1e!)〉

<t1t1e〉静觅|崔庆才的个人站点〈/tit1e〉 』■‖|‖□Ⅵ]」■■■■|||

这样的话, pyQueIy对象会首先请求这个URL’然后用得到的HTML内容完成初始化’其实就相 当于把网页的源代码以字符串形式传递给pyQuery类’来完成初始化操作° 下面代码实现的功能是相同的:



千ro们pyquery1川portpyQuerya5囚 mportreque5t5

doc=pq(reque5t5°get(0http5://〔ujqj∩gcaL〔oⅧ|).text〉 pri∩t(dO〔(‖tit1e|〉)



●丈件初始化

除了上面两种,还可以传递本地的文件名’此时将参数指定为+j1e"a"e即可:

■■

+ro∏pyquery1刚portpyQuerya5pq



■·‖己■〗‖‖|』■■■{□

do〔=pq(十j1e∩a∏记=!de∏D∩t∏‖1!) pr1∩t(do〔(01j‖))

当然, i文里需要有-个本地HTML文件demohtml,其内容是待解析的HTh皿字符串°这样’

pyQ0ery对象会首先读取本地的文件内容,然后将文件内容以字符串的形式传递给pyQuery类进行初 始化°

以上3种初始化方式均可采用°当然’最常用的还是以字符串形式传递。

Q

」■可‖■』司‖」■』■■

3.基本CSS选择器

首先用—个实例感受一下pyqueⅣ库的CSS选择器的用法: ht们1二 | 0 , <djγid=00co∩t己1∩er』0〉

<u1〔1a55="1i5t"〉 〈11〔1a5S≡"1te刚ˉ000〉十jr5tjteⅧ〈/11〉 <1j〔1己55="jte们ˉ1"〉<日hre千二"1i∩促2°ht"1"〉5eco∩djte川〈/a〉〈/1i〉

<1i〔1a55二"jte们ˉoa〔tiγe")<3hIe+="11∩促3·∩t们1"〉〈5pa∩〔1己55="bo1d"〉thiIdite『『l</5pa∩)</己)〈/1i〉



<1iC1己S5≡"jte川-1a〔tjγeo0〉〈a∩re十="11∩k4。hm10o〉千ourt∩1teⅧ〈/a)〈/11〉

〈1i〔1日55=001te‖ˉO"〉<ahIe+="1i∩k5·ht爪10|〉+i千t∩1te们</a〉</1j〉 ■



〈/u」〉

0

</diγ〉 0

0

0

+【o∏‖ pyquery1ⅦportpyQuerya5pq do〔=pq(hm1) pr1∩t(do〔(,#〔o∩t日j∩er .1i5t11‖)) pri∩t(type(do〔(0#〔o∩ta1∩er .1i5t1j』)))



』■■‖|‖』一■口||』■∏

运行结果如下:



} ●厉‖■尸|卜巴■厂)|■■「|卜

|巴尸‖■尸

●}‖ △■厂β||■伊【β「□■「



P `■■【‖‖·|‖■■=■■■‖■·`√■■■■』坠■□■■〗【=■■·【·|‖【■■口「

》▲尸【◆尸| ■ ■ = 厂 | 卜 □ β 「







33 pyqueIy的使用

l15

〈1j〔1a55="jteⅦˉ0||〉千jr5t1t印〈/1i〉 〈1j〔1ass≡Nite∏]ˉ1因〉〈a∩re千="1j∩低2.打tⅧ100〉5e〔o∩djte阳〈/a>〈/11〉

〈11c1a55="jte"ˉ0actjγe"〉〈a∩re十="1i∩k3·∩m1")〈5pa∩〔1a55="bo1d"〉t∩ird1te爪〈/5p日∩〉〈/3〉</1j〉 〈1jc1aS5="jteⅧˉ1a〔tjγe00〉〈日hre千="1i∩k0.htⅦ1"〉fourth1te‖〈/a〉</1j〉 〈11C1己55="1te∏]ˉ000〉〈3们re+="1j∩k5.‖m100)于1什∩ite川〈/a>〈/1i〉

〈〔1日55

pγquery.pyqueryPyQuery〉

这里我们初始化pyQuerγ对象之后,传人了_个CSS选择器#〔o∩ta1∩er .1j5t11,它的意思是 先选取jd为〔o∩ta1∩er的节点,再选取其内部c1a55为115t的节点内部的所有11节点,然后打印输 出。从运行结果可以看到,我们成功获取了符合条件的节点°

最后,将符合条件的节点的类型打印输出’可以看到依然是pyQuerγ类型。 下面’我们直接遍历获取的节点’然后调用text方法,就可以百接获取节点的文本内容了’代 码如下: 千or1teⅦj∩do〔(|#co∩tai∩er .1i5t1j,).ite川5():

pri∩t(ite们.text())

运行结果如下: 千jr5tite‖

5e〔o∩dite‖ thirdite田

十ourthit印

+i什b1te∏

怎么样?我们泣里没有写正则表达式,直接通过选择器和text方法,就得到了想要提取的文本 信息’是不是方便多了?

下面我们再来详细了解_下pyquery库的用法,包括如何查找节点、遍历节点,并获取各种信息 等°掌握了这些,我们才能更高效地提取数据.

4.查找节点 下面是_些常用的杏询方法。 ●于节点

奋找子节点时’需要用到「j∩d方法’其参数是CSS选择器°这里我们还是以上面的HTML为例: +r咖pyqueIyj…rtpyq』ery35pq do〔=pq(∩t∏1) it咖5=dO〔(!.1iSt0) prj∩t(ty怔(jt咖s)) pIj∩t(jt印5) 1j5=itm5·「i∩d(°1i!) prj∏t(type(1j5)) prj∏t(1j5)

运行结果如下: <C1a55 !pγqueIy.pγqueⅣ。p抑爬Iy,) <u1〔13S5=·1i5t□>

〈1j〔1ass=■jt酗ˉo■〉十jⅢ5tjt印</1j〉

〈1j〔1as5=曰jt酗-1园〉<己hIe千≡口1m促2.hm1口〉5e〔o∩djt酮</a×/1i》

〈1i〔1a55=■jt咖ˉOa〔tiγe田〉〈a∩ref=口1j∩妇.htⅦ1口〉〈5p3∩〔1ass≡曰bo1d闻〉thjrdjt印〈/5Pa∏〉</a〉</1j〉 〈1j〔1a55=■jt咖ˉ1a〔tiγe回〉〈ah【e于=.1i∩灿.∩t们1口)千ourthjte们</a>〈/1j〉 〈1i〔1巫s=■it咖ˉo口〉(ahref=□1j∩促5·htⅦ1.>千j代hite∏〗〈/a×/1i〉 〈/u1)

〈〔1a55 ,pyq0ery。Pyquery。pyq』erγ|〉 〈1i〔1aS5="itmˉo"〉千irStit颐〈/1i〉

〈1j〔1a5s=■it酮ˉ1口〉〈ahre千=口1j∏k2.hm1■〉5e〔o∩djt印〈/己×/1i〉

〈1i〔1a55=■it印ˉ0a〔t1γe阐〉<ahre十=口1i∩妇.ht‖1口〉〈5pa∩〔1a55=曰bo1d闻〉third1teⅧ〈/5pa∩〉〈/a〉</11〉 <1j〔1己55=口it咖ˉ1己〔tjγe"〉〈a∩re千=口1j∩低4.∩m1曰〉句urthite们〈/a〉〈/1j〉 〈1i〔1a55=口jt印ˉ0口〉〈ahIe千=闻1j∩仪5·htⅧ1闻〉千j代hjte们</a)〈/1j》



第3章网页数据的解析提取

ll6

这里我们先通过.115t参数选取〔1a55为1j5t的节点。然后调用+i∩d方法,并给其传人CSS选 择器,选取其内部的1j节点,最后打印输出°可以发现’+1∩d方法会将所有符合条件的节点选择出 来’结果是pyQuery类型°

其实+j∩d方法的查找范围是节点的所有子孙节点°如果只想查找子节点’那么可以用〔∩j1dre∩ 方法: 115=iteⅦ5.C∩j1dre∩()

pri∩t(type(1j5)) pr1∩t〈1i5)

运行结果如下: 〈c1a55 |pyquery。pyquery。pyQuery|> <11C1a55="iteⅧˉO"〉fir5t jte阳</1i〉

〈1iC1a55="ite‖ˉ1厕〉〈ahre千="1j∩代2。ht们1′0)5e〔O∩djte‖〈/a〉〈/1j〉

〈1j〔1a55="1te∏|ˉoact1ve"×己∩re+=oo11∩|G·hm1"〉〈5p己∩〔1a55=||bo1d"〉t∩ird 1te阶</5pa∩></a≥〈/1j> <1ic13s5="it印ˉ1a〔t1γe"〉〈ahre十=001i∩低4.ht阳1"〉千ourt∩1teⅦ〈/a〉〈/11〉 <1i〔1a55="1te∏|ˉO"〉<a∩re于≡"1i∩促5·ht爪100〉伍什hite们〈/a〉〈/1j〉



如果要筛选所有子节点中符合条件的节点,例如想筛选出子节点中〔1a55为aCt1γe的节点’则可 以向〔h11dre"方法传人CSS选择器.a〔tiγe’代码如下: 115二jte∏5.〔∩11dre∩(0 .a〔t1γe|)

prj∩t(1iS)



( 』

运行结果如下:

||

〈1i〔1a55="iteⅦˉ0a〔tiγe||〉〈a∩re十="1i∩旧·∩tⅦ1"〉〈5pa∩〔1a5s=||bo1d">th1rdite们〈/5pa∩〉</a〉〈/1i> 〈1jc1a55="iteⅧˉ1a〔t1γe"〉<3hre千="11∩代4。∩t阳1"〉+ourt∩iteⅦ</a〉</1i)

可以看到’输出结果已经是筛选过的,只留下了〔1a55为aCtiγe的节点。



●父节点

■■■司‖‖』■】■■■■习·■

我们可以用pare∩t方法获取某个节点的父节点’下面用_个实例感受—下: hm1= || ′

〈djγ〔1a55≡"Wmp") 〈diγid=00〔o∩taj∩er"〉

<u1〔1a55="1j5t"〉 〈1jC1a55="jte川ˉ0"〉十ir5tit即</1i〉

|」°|」

〈1jc1a55≡"1teⅦˉ1"〉<a∩Ie+="11∩代2°hm1||〉5e〔o∩d1t即</a〉〈/11〉

d

<1ic1己55="jte们ˉ0actjve"〉<a∩re+="11∩旧°ht们1"〉<5pa∩〔1as5="bo1d"〉t∩irdite爪〈/5pa∩〉〈/a〉</1i〉 <1i〔1a55="1te们ˉ1日〔tiγe"〉〈ahre十="1i∩代4。∩t们1"〉+OUrt∩1te爪〈/a〉〈/1i〉 〈1j〔1a55="ite们ˉO"〉〈ahre十="1j∩低5。ht∏1Ⅲ〉十j+thjte爪</a〉</1j〉 </u1〉 〈/diγ〉 〈/diV〉 0

0

0

q

「Io们pyquery1川portpyQuerγ日5pq do〔≡pq(∩t"1) ite‖5=dO〔( ‖ .115t0) co∩t日j∩er≡ite‖s.pare∩t() pr1∩t(type(CO∩tai∩er)) pIi∩t(CO∩ta1∩er)

运行结果如下: <C1a55







pyquery.PyqueIy。pγQ0eIy〉



00

〈divid="co∩taj∩er〉

<u1C1a55=o!1j5t"〉

<1i〔1a55≡"jteⅧˉO"〉十ir5t jteⅦ〈/11〉

〈1j〔1日55≡"ite"ˉ1"〉<ahre十="1j∩低2·ht们1"〉5e〔o∩djte爪</a〉〈/1j〉

<1i〔1己55="ite∏↑ˉoa〔tjγe"〉<3hre千="1j∩旧。ht∏)1"〉〈5pa∏c1a55=0bo1d"〉thjrdjte∏)</5p己∩〉</a〉</1i〉

‖|{(

〈11C1a5S="iteⅦˉ1a〔tiγe"〉〈a‖re十≡"11∩促4·∩tⅦ1"〉千oUrth1te阳</a>〈/11〉

〈1j〔1a55="jte刚ˉO")<a∩re「="11∩伐5.ht川1"〉+1什h jteⅦ〈/a〉〈/11〉



‖~

33 pyqueIy的伎用 巴■■∏【■队队■■】『■【■尸>■

〈/u1〉 〈/div〉

这里我们首先用.115t选取c1a55为1i5t的节点,然后调用pare∩t方法得到其父节点,其类型 依然是pyQuerγ。 这里的父节点是指直接父节点’也就是说,pare∩t方法不会继续查找父节点的父节点,即祖先节点。



但是如果就想获取某个祖先节点,要怎么办呢?这时可以用pare∩t5方法: +ro阳pyq0ery加portpyQuery日spq do〔=pq(∩t"1) jte川5=dO〔(0 .1i5t‖) Pare∩t5≡jte爪5°Pare∩t5() prj∩t(type(pare∩t5〉) pIj∩t(pare∩t5)





ll7

运行结果如下: 〈c1a55 |pyquery.pyquerγ.pyQuery〉 〈diγ〔1aS5="Wrap"〉 ■0

〈diγjd二00〔o∩t己j∩eI〉

| “l艘i:渊…铲…e"《′』℃

〈1i〔1a55=00it咖ˉ10,〉〈ahre「=001i∩促2·ht们1">5e〔o∩dite∏</己〉〈/1j〉

〈11c1a55="1te‖ˉOa〔tiγe"〉〈己hre+="1j∩代3°‖tⅦ100〉〈5p日∩c1a5s=00bo1d"〉t∩1rd1teⅦ〈/5pa∩〉</a×/1i〉 〈1i〔1日55="jt印ˉ1actjve闻〉〈a∩re「="11∩灿.∩tⅦ1"〉+o‖rthjteⅧ〈/a〉</11〉 〈1j〔1己55="it印ˉ00|〉〈己hre+="1i∩低5.hm100〉十j千thite‖</a〉〈/1i〉 </u1〉 </djγ〉

〈/djγ〉 〈djγ1d="co∩taj∩eI"〉 <u1〔1a5S="11St"〉

〈1iC1a55="ite‖ˉ0"〉+jr5tjte"</1i〉

〈11〔1a55≡"jte阳~1"〉〈ahre十="1i∩促2.ht‖100〉5eco∩d1te川</a〉</1j〉

〈1i〔1己55="jte们ˉoa〔tiγe阅〉〈a‖re千="1j∩旧。ht∏1"×5pa∩〔1日55="bo1d"〉t∩irdjte‖</5pa∩〉〈/日〉〈/11〉 <1ic1a55="jt即ˉ1a〔tjγe00〉〈ahre十=001i∩代4°∩m1"〉十ourt∩jteⅦ〈/日〉〈/1j〉 〈1i〔1己55="ite↑γlˉo"〉<己hre+="1i∩代5°bt爪100〉千i千thite‖</a〉〈/1i〉 〈/‖1〉 〈/djγ〉

可以看到’输出结果有两个:一个是c1a55为wrap的节点’一个是1d为co∏tai∩er的节点°也 就是说, pare∩t5方法会返回所有祖先节点° ●

如果想要筛选某个祖先节点,可以向pare∩t5方法传人CSS选择器’这样就会返回祖先节点中符 合CSS选择器的节点: pare∏t=it印5.paIe∩t5(! ."mp|) pr1∩t(paIe∩t)

运行结果如下: <diVC1己55=00Wr日p"〉 〈djγ1d="co∩tai∩eI||〉 <u1〔1aS5=001i5t训〉

<1iC1a55="ite川ˉ0"〉+jr5t1te刚〈/1i〉

〈1i〔1a55=001te‖ˉ1α〉〈ahre+≡"11∩代2.htⅦ1曰〉5e〔o∩djte∏‖</a〉〈/1i〉

〈1j〔1a5s="ite爪ˉ0己ctiγe"〉〈ahre千="1i∩k3.‖t∩1"〉〈5pa∩〔1a55=00bo1d00〉t‖jrdjteⅦ</5pa∩)〈/3〉〈/1i〉 〈1i〔1日S5=00it印ˉ1aCtiγe00〉<己hre千="1j∩灿°ht‖100〉+Ourth1teⅦ〈/a〉〈/11〉 〈1i〔1a55="iteⅢˉO"〉<3hre+="1j∩《5°hm1"〉十j十thiteⅧ〈/a〉</1i〉 </u1〉 〈/diV〉

〈/djγ〉

可以看到’输出结果少了一个节点’只保留了C1a55为Wrap的节点°

「3 ~

第3章网页数据的解析提取

■ ] ‖

||

1l8



●兄弟节点

前面我们说明了子节点和父节点的用法’还有_种节点就是兄弟节点。获取兄弟节点可以使用 5jb1i∩g5方法°这里还是以上面的HTML文本为例; +ro‖∏pyqueryj们poItpyQqeⅢya5pq

doC≡pq(htⅦ1) 1j=do〔(』°1i5t .it印ˉo。a〔tiγe0) pri∩t〈11.5jb1j∩g5()〉



运行结果如下:

d

{|日

这里首先选择〔1a55为115t的节点内部的〔1a55为1te刚ˉO和active的节点,也就是第三个11节 点。那么’很明显,其兄弟节点有4个,就是第一个、第二个`第四个、第五个1j节点。 〈11C1aS5="iteⅦˉ100〉<己∩re千=0011∩Ⅸ2.‖t们10‖〉5eCo∩diteⅦ</己〉</1i〉

■司《|』■■

〈1i〔1a55="it印ˉo凰〉+jr5t jteⅦ</1i〉

〈1iC1a55二"iteⅧˉ1act1γe00〉〈ahre+="1i∩M°ht‖1"〉十ourthiteⅦ〈/3×/1i〉 〈1j〔1a5s二"jte们ˉO■〉〈3href=001i∩促5·htⅧ1"〉「i什bite们</a〉〈/1j〉

可以看到’结果正是我们刚才说的那4个节点° 点中挑选出符合条件的节点了:

这里我们筛选了C1a55为a〔tjve的节点’通过刚才的结果可以观察到’ 〔1a55为aCtjve的兄弟节 点只有第4个1i节点满足,所以结果应该只包含_个节点。 我们再看—下运行结果: 〈1i〔135s=■it印ˉ1actiγe曰〉〈ahre十=口11∩R4.hm1口>千ol』rthjte刚</a〉≤/1j〉

结果确实符合我们的预期°

型,并没有像Beautj血lSoup那样返回列表°

如果结果是单个节点,既可以直接打印输出,也可以直接转成字符串:

|‖|

可以观察到, pyquery库的选择结果可能是多个节点,也可能是单个节点,类型都是pyQuery类

勺‖·

5.遍历节点

□(‖(■■』‖■】‖司

+ro∩pyquery加portpyQ‖erya5pq do〔=pq(ht爪1) 1j=doc(0 .1jst 。1t印ˉ0。a〔t1γe0) pri∩t(1j.5jb1j∩g5(0 .a〔tiγe0))

‖」|」(

如果要筛选某个兄弟节点’依然可以向51b11∩g5方法传人CSS选择器’这样就能从所有兄弟节



fr咖pyquery1…rtp池』eⅣaBpq

do〔≡pq(hm1〉 1i=do〔(|.jt田_o.a〔tiγe,) prj∩t(1i) pm∏t(5tr(1i)〉





运行结果如下:

如果结果是多个节点,就需要遍历获取了。需要调用jteⅧ5方法:

「Or1ji∩1iS8

〖 | |

pIi∩t(1i’ type(1j))

叫{』■|■日旦■

fr咖pyquery1∏mItp灿』erya5Pq do〔≡pq(∩tⅦ1) 1j5=doC(,1i,)。jt酮S() pIj∩t(tyPe(1j5))

」日』』■■·‖‖|{|划』】■】‖』勺|‖‖

<1j〔1a55≡口it酮ˉoa〔tjγe.)〈a∩re+=.1j∩k3.hm1口)〈5pa∏〔1a5s=口bo1d回)thjrdit印</5p己∩》(/3)〈/1i) 〈1i〔1as5=凰it酮ˉ0a〔tiγe■〉<ahIe十=词1i∩妇.htⅧ1因〉<5pa∩〔1as5=口bo1d口)tMⅢdjte们〈/5pa∩)</a×/1i>



|‖「}}

33 pyquery的使用

ll9

这里把所有1i节点遍历了—遍°运行结果如下: 〈c1a55

ge∩er日tor|〉

〈1j〔1a55≡0oite爪ˉo"〉十ir5t jte‖</1i) 坠■『‖卜’「巴尸|‖》

〈〔1a55

pyquery.pyqoery。pyQuery,〉

〈1iC1a5S="jte∏ˉ1"〉〈abIe+="1i∩促2·ht∩1顾〉Se〔O∩d it即〈/a〉</1j〉

<〔1a55 |PyqueIγ°Pyq0ery.pγQuery)

<1ic1355≡闻ite‖ˉ0日ctiγe"〉〈ahre+="1j∩Ⅸ3。∩tⅧ1"〉〈5Pa∩C1a55="bo1d"〉t们irdjteⅧ〈/5pa∩〉〈/a〉〈/1i〉 <〔1a5s

pyquery.Pγquery.pγQuery〉

〈1jc135S="ite『『‖ˉ1actjve"〉<ahre+=001i∩M.∩t爪100〉千ourt∩jte∏l〈/己〉</1i〉

〈〔1a55

pyql』ery.pγql』ery.pyQuery,〉

〈1j〔1己5S=!|jteⅦˉO0′〉〈a∩re十=001j∩促5。ht‖1"〉+j千thjte们</a×/1i>

〈〔1a55 !pyqUeIy.PyqUery.pyQuery〉

可以发现’调用1te‖S方法后,会得到—个生成器’对其进行遍历’就可以逐个得到1j节点对象

了’它的类型也是pyQuery°还可以调用前面所说的方法对1j节点进行选择’例如继续查询子节点` 寻找某个祖先节点等’非常灵活° ●获取信息

提取到节点后’我们的最终目的当然是提取节点包含的信息了°比较重要的信息有两类,一是属 性、二是文本,下面分别进行说明° ●获取属性

提取到某个pyQuery类型的节点后’可以调用attr方法获取其属性: ‖t∩1= ‖ ‖ 0

〈djγ〔1a55=0Wrap"〉 『0

〈d1v1d="〔o∩taj∩er>

<u1〔1aS5="1i5t"〉

<1i〔1a55="1t咖ˉO")+ir5tjt印〈/1i〉

〈1j〔1a5s="jte∩-1"><a∩re千="1i∩低2.ht‖1"〉5e〔o∩dite‖〈/a〉〈/1j〉

<1i〔1a55="it咖ˉOa〔tiγe口〉〈ahre千="1j∩Ⅷ3.∩m1阅〉〈5pa∩〔1a55="bo1d0‖>thirdite们〈/5pa∏〉</a〉〈/1i〉

<1ic1a55="jt印ˉ1actiγeα〉〈a∩re千=口1i∩M.∩m1口〉+ourthite爪</a〉</1j〉 <1jC1aSS="it印ˉ0α〉〈abre十="1i∩低5°ht∏1圆〉千j什∩ite∏‖</a〉</1i〉 〈/u1〉

〈/diγ〉 </diγ〉 0

0



+r咖pyql』erγmportpyq』erya5pq doc≡pq(ht∏1) a=do〔(·.it印ˉ0。a〔tivea!) pri∏t(a’ type(a)) pIi∏t(a.attr(』hre十|))

运行结果如下:

〈ahre千=阐1i∩|G.ht∏1口〉〈spa∩〔1a5s=因bo1d圃〉tMrditeⅦ</5pa∩)</a〉〈〔1a55 ,pyqueIγ。pyq‖|erγ.pγq』ery〉 1i∏旧.∩t∏1

这胆首先选中C1a55为ite∏]ˉO和actjve的1i节点内的a节点’其类型是P)′Querγ类型。 然后调用attr方法°在这个方法中传人属性的名称,就可以得到对应的属性值了° 此外’也可以通过调用attr属性来获取属性值,用法如下: pri∩t(己.attr.∩re十)

结果如下: 1i∩k3.ht田1

两种方法的结果完全_样。

如果选中的是多个元素,这种情况下调用attr方法,会出现怎样的结果呢?我们用实例来测试 -下:



‖{‖□

第3章网页数据的解析提取

l20

({

a≡dO〔(|日|) pri∩t(a’ type(a))



prj∩t(a日ttr(‖hre千‖)) pIi∩t(a.attr.∩Ie十)

q

运行结果如下: ≤3hre十=| |1j∩|(2.hm1"〉5eco∩djte"〈/a≥〈ahre+="1j∩旧·∩t|∏1"〉<5pa∩〔1a55=『|bo1d"〉t‖im1te‖〈/5pa∩×/a〉<a ‖re十="1j∩长4。btⅦ1"〉+ourt们1te川〈/日><日hre千="11∩|〈5。∩t‖1"〉「i什∩iteⅦ</a〉〈〔1as5 pyquery.pyquery。pγQueIy0〉 11∩股2。ht川1



11∩代2·∩t们1

第—个节点的属性°

那么,这时如果想获取a节点的所有属性,就要用到前面所说的遍历了:

■叮‖口‖{』■■口‖‖·‖|』■】■勺

照理来说’我们选中的a节点应该有4个,所以打印结果也应该是4个。但是当我们调用attr方 法时’返回结果却只有第_个。这是因为’当返回结果包含多个节点时’调用attr方法’只会得到

司‖

运行结果如下:

』□■』■ⅥⅥ

十ro‖pyqueIγ1ⅦportpγQueIya5pq αo〔=pq(∩t爪1) a=do〔(‖a‖) 千OIiteⅦi∩a.jte川5(): pr1∩t(ite".attr(!hre于`))



1i∩促2.∩t∏1

11∩k3.ht刚1 1i∩k4.‖tⅢ1



1j∩k5。‖t‖1

因此’在获取属性时’可以观察返回的节点是—个还是多个’如果是多个, 贝||需要遍历才能依次 获取每个节点的属性°





●获取丈本



获取节点之后的另_个主要操作就是获取其内部的文本,此时可以调用text方法实现:



bt川1= ‖ | !



<d1V〔1a55=Wrap〉 00

〈d1v1d="〔o∩t己j∩er〉

0

<u1〔1a55="115t"> ■

<1i〔1巳‖55="jteⅧˉ0"〉十jI5t 1teⅦ〈/1j>

<1jc1己|55=||ite‖ˉOa〔t1γe"〉<己打re千≡"11∩旧。ht∏1"〉<5pa∩〔1己55="bo1d"〉t∩iId 1te∩〈/5p己∩〉〈/a〉</11〉 〈1jc16}55=||1te∩ˉ1a〔t1γe"〉<己卜re「="1j∩k4·ht"1"〉千ourt们1te‖</a〉≤/11〉 〈1j〔1『〕55="ite们ˉO"〉〈a∩re+="1j∩代5.ht川1|」)千j什h1te们</a〉〈/11〉 〈/u1〉

■■‖|』■‖』■|』■Ⅵ

〈1ic1〖‖s5=∏1te阳ˉ1"〉〈ahre「="1j∩艇2°‖t们1"〉5eco∩djte们〈/a〉</1j〉

〈/djγ〉

〈/d1γ〉· T

0

d



+ro"Pγquerγ1Ⅷp《)rtpyQueIy日5pq do〔=pq(ht"1) a≡do〔(|.jte"ˉ0.actjγea0 ) pr1∩t(a) DI1∩t(己.teXt())





运行结果如下:

这里首先选中a节点’然后调用text方法’就可以获取其内部的文本信息°此时teXt方法会忽 略节点内部包含的所有HTML,只返回纯文字内容。 要想获取节点内部的HTML文本’需要用‖t∏1方法:

■]〗{‖■■‖{|‖·■□】可纽』|』■‖

<a∩re千="1j∩代3°hm1"〉〈5Pa∩C1aS5≡|bO1d"〉t∩1Id1te‖〈/spa∩×/己〉 tMId1te们

( Q



|}| ■尸|卜‖

「 |

0 b

3。3

pyquery的使用

l2l



丁ro"pyquerγmport pγQuerya5Pq do〔=pq(hm1) 1i=doc( ‖ .jteⅧˉO。己ctjγe‖) prj∩t(1j) prj∩t(1i·ht‖1())

这里我们选中了第三个11节点’然后调用了∩t‖1方法’.返回结果应该是11节点内的所有HTML 文本。



}3

运行结果如下:

h

〈a∩re+="1j∩Ⅶ3。hm1"〉<5p己∩〔1a55="bo1d")t卜|jrd1te们〈/5P己∩〉〈/a〉

这里同样有一个问题’如果我们选中的是多个节点’那么teXt或ht川1方法会返回什么内容?不 妨用实例来看一下:



∩t"1= 0` 0 Ⅶ

00

<diγ〔1a55=Ⅶrap〉





00

〈d1γ1d="co∩taj∩er〉

〈u1C1a55="115t"》

〈1ic1a55="ite∏ˉ1■>〈a∩Ie+="1j∩长2·ht‖1"〉5eco∩djte川</《】〉〈/11〉

■ 厂 ° □ ‖

〈1i〔1a55="1te刚ˉ0日ct1γe"〉〈ahre千≡"1j∩旧.ht‖10,〉〈5pa∩仁1a55=00bo1d"〉t|`irditeⅦ</5pa∩〉</a×/1i〉

尸|

〈1j〔1a5s≡"ite∏ˉ13ctjγe")〈a∩re+="1i∩M。htⅧ1">+ol』rthjte∏〈/a〉〈/11〉 〈11C1日55="ite"ˉO"〉〈3‖正+="1i∩代5·‖m1"〉+i千th 1te川</f』×/11〉 〈/(』1〉



〈/diγ〉 〈/djv〉

β

+rO‖PγqUerγ1川POrtpyQqerγa5pq p

}|

『■■■『|■■厂|‖||◆尸‖『■=厂|



do〔=pq(ht川1) 1j=do〔(』11‖) pr1∩t(1j.btⅧ1()) Pri∩t(1i.text()) pr1∩t(tγpe(11.teXt())

运行结果如下: 〈ahre十="11∩k2°∩t刚1"〉5eco∩d1te爪≤/a〉

5e〔o∩dite∏tMrdjteⅧ+ourt∩jteⅧ千1+thite川 <〔1a55!5tr0〉

结果可能比较出乎意料,‖tⅦ1方法返回的是第一个1i节点内部的HTML文本’而teXt返回了所 有的1j节点内部的纯文本’各节点内容中间用_个空格分割开’即返回结果是_个字符串。 所以这个地方值得注意’如果得到的结果是多个节点’并且想获取所有节点的内部HTML文本’ 就需要遍历这些节点°而text方法不需要遍历即可获取’会对所有节点取文本之后合并成一个字符串° 6.节点操作

pyquery库提供了一系列方法对节点进行动态修改’例如为某个节点添加—个c1a55’移除某个节 点等,有时候这些操作会为提取信息带来极大的便利° 节点操作的方法太多’下面仅举几个典型的例子来说明其用法° ●addClass和removeClass

我们先用一个实例感受一下: ht阳1= 0‖

〈diγ〔1a55=『Wrap"〉 00

<djγjd="co∩t日j∩er〉 〈u1〔1a55≡"1j5t"〉

〈1j〔1a55=‖|jte‖ˉ0"〉十1r5t1te"〈/1j〉

〈1jc1a55="1teⅦˉ1| |〉<ahre千霹"1j∩代2°ht∏10| 》5eco∩d1te们〈/a〉〈/1j〉

〈11〔1a55="ite们ˉoactjγe"〉<a‖re十="11∩‖3°hm1||〉〈5pa∩〔1as5="bo1d"〉tbird1te"</5pa∩〉〈/a〉</11〉

0

第3章网页数据的解析提取

l22

〈1j 〈1i

c1己55="jte∏=1a〔tiγe冈><ahre十=!01i∩k』.∩m100〉+ourthjte川〈/日〉</1j〉 C1aS5="jt印ˉO"〉〈ahre「="1i∩k5.门t"1"〉千1什hjteⅦ〈/a〉</1j〉

</(』1〉

〈/diγ〉 </diV〉

+roⅧpyquerymportpγQuerya5Pq do〔=Pq(们tⅧ1) 1i=do〔(′°jte∏ˉO.a〔tiγe‖) pri∩t(1i) 11.re|∏oγe〔1日55(0日〔tiγe!) Prj∩t(1j) 1i.3dd〔1a55(0xtjVe!) pri门t(1j〉

首先选中了第三个11节点’然后调用re"o`′e〔1a55方法,将其中的a〔tiγe这个c1a55移除’然 后又调用add〔1a55方法,将这个〔1a55添加回来°每执行-次操作,就会打印一次当前1j节点的内容° 运行结果如下: 〈1i〔1a5s≡"jte刚ˉoa〔tjγe"〉<ahre十="1j∩促3.∩m1"〉〈5p日∩〔1己55="bo1d0〉thjm1te∩〈/5Pa∩〉</己〉</11〉 <1j〔1己55="jteⅦˉ0回〉≤。a们re+≡"1j∩旧.|]m1国)<5p日∩〔1a55="bo1d"〉thjrditeⅧ〈/sp3∩×/3〉</1i> 〈1i〔1a55=o0jte∏]ˉ0a〔tiγe0q〉〈ahre千=001j∩促3.hm1厕〉<5pa∩〔1a55=!bo1d"〉thjIdite‖〈/5pa∩〉</a〉〈/1i)

可以看到,一共输出了3次。第二次输出时, 1j节点的aCt1γe这个〔1a55已经不见了’第三次 这个〔1a55又有了。

所以说, add〔1a55和re川ove〔1己55方法可以动态改变节点的c1a55属性。 ●attr、teXt和html

当然,除了add〔1a§j5和Ie们oγe〔1a51i方法,也可以用attr方法对属性进行操作°此外,te×t和∩tⅦ1

方法可以用来改变节点内部的内容。实例如下: ht‖1= ‖‖

〈u1〔1aS5≡闻1i5t00〉

〈1ic1a5s="ite『∏ˉ0a〔tiγe"〉<ahre+=||1j∩代3.htⅧ10o〉〈5pa∩c1as5=铡bo1d00〉thjrdjte∏</5pa∩〉〈/己〉〈/11〉 〈/u1〉 0

0

0

「roⅧpyqueIyj呻oItpyQoerya5pq

1j.text(|Ch己∩8edjt印0) pri∩t(1i)

1j.ht川1(‖<5p己∩〉〔ha∩gediteⅦ</5p己∩〉|) Pri∩t(1j)

〈1ic1a5s≡闻it咖ˉoa〔tjγe■>〈ahIe十=m1j∩低3。∩tⅦ1回〉<5pa∩〔1as5="bo1d圆〉tmrditeⅦ</5pa∩〉〈/a〉〈/1j〉 <1ic1a55≡圆it铡ˉO3ctjγe口∩a爬=圆1i∩促因〉〈ahre+=口:[i∩代3。‖tⅦ1口〉〈5pa∩c1a55="bo1d■〉tMrdite"≤/5pa∩〉〈/己〉〈/11〉 <1i〔1a55≡冈it印ˉ0a〔tiγe园∩a『∏e="1j∩代口〉〔h己∩gedit印</1j) 〈1i〔1as5=卿jteT‖ˉoa〔tjγew∩日眠=呵1j∏代■×5p日∩〉〔ha∩gedjt印〈/5pa∩>〈/1i〉







|√●]‖‖』』]‖〗‖』■】‖||』】|」』■■■

可以发现,调用attr方法后, 11节点多了一个原本不存在的属性∩aⅦe,其值为1j∩k°接着调用 teXt方法’传人文本之后, 11节点内部的文本全被改为传人的字符串文本了°最后’调用bt‖1方法 传人HTML文本后’11节点内部又变为了传人的HTML文本。

·(」■

运行结果如下:

=■■‖|』■‖■■■

当前的1i节点。

司‖

这里我们首先选中1j节点’然后调用attr方法修改其属性,该方法的第_个参数为属性名,第 二个参数为属性值°接着’调用text和∩t"1方法改变1j节点内部的内容。每次操作后,都会打印出

司‖||‖□∏」』‖

do〔≡pq(∩t刚1) 1j≡do〔(! 。jte们ˉ0.a〔tjγe‖) pri∩t(1j) 1i.己ttr(|∩a贬0 ’ ‖1j∩代0) pri∩t(11)

「| 33 pyquery的使用





l23

所以说,如果attr方法只传人第-个参数,即属性名,则表示获取这个属性值;如果传人第二 个参数’则可以用来修改属性值°teXt和bt们1方法如果不传参数,表示的是获取节点内的纯文本和 HTML文本;如果传人参数,则表示进行赋值。



●remove

顾名思义’re‖ove方法的作用是移除’有时会为信息的提取带来非常大的便利°下面有~段HTML 文本:



■‖△‖

hm1=|| 0

〈djγC1己55=耐Wrap"〉 "e11O’‖Or1d 〈p〉「‖1515aP己IagI己ph。〈/D〉 〈/diγ〉

h





0



「r咖pyqUery1爪POrtpyQUerγa5pq do〔≡pq(∩m1) wr巳p=do〔(|.哑日P‖) pri∩t(WmP。teXt())

现在想提取文本中的‖e11o’ 文本中的‖e11o’‖or1d这个字符串,而不要p节点内部的字符串’该怎样操作呢? 试里盲接先尝试提取〔1a55为侧Iap的节点中的内容,看看是不是我们想要的。运行结果如下: ‖e11o’‖or1d「bj5i5apaIagrap∩

这个结果中包含着p节点的内容’也就是说text方法把所有纯文本全提取出来了°要想去掉p节

点内部的文本, 可以再把p节点内的文本提取一遍’然后从整个结果中移除这个子串,显然这个做法 比较烦琐° ■‖■∏‖|

这时re们Oγe方法就派上用场了’可以这么做:

■】‖〖■‖ |》卜‖【‖『『‖■‖‖『【■∏‖■

| ●尸■■■■



"mp.+j∩d(0P0).Ie帅γe() prj∩t("rap。text())

首先选中p节点,然后调用Ie们oγe方法将其移除,这时"rap内部就只剩下}{e11o’‖or1d这句话 了, 再利用teXt方法提取即可。

其 实还有很多操作节点的方法, 例如appe∩d` e‖pty和prepe∩d等。

7伪类选择器 CSS选择器之所以强大’还有-个很重要的原因,就是它支持多种多样的伪类选择器’例如选择

第_个节点`最后_个节点、奇偶数节点、包含某一文本的节点等.实例如下: ht∏1= 0` 0

<djγ〔1a5s=闪NmP00〉 〈diγ1d=00〔o∩t己1∩eI"〉

<u1〔13S5="1i5t"〉

<1i〔1日S5=,0jteⅦˉ0">千ir5t1te川</11〉 〈11〔1355="jteⅧˉ1")〈己‖re「≡"1i∩代2.ht川1"〉5eCO∩d1te们〈/己〉〈/1j〉

〈11〔1a55="ite川ˉo己〔tiγe"〉〈ahIe{="1j∩k3·ht刚1"≥〈5pa∩〔1a55="bo1d00〉t∩1rdjte川〈/5pa∩〉</a〉〈/11〉 <1i〔1a55="iteⅦˉ1a〔tjγe"〉〈ahre+="11∩促4·ht们1"〉+Ol」rthite"〈/a〉</1i〉

〈11c1as5="ite们ˉO"〉〈日hre千≡"1i∩代5.∩t"1"》+j代h1te‖</日〉〈/1j〉 〈/u1〉 </d1V〉

〈/d1γ〉

千ro丽pyqueIyi们portpγQuerγaspq

doc≡pq(ht爪1) P

11 =do〔(011:+jr5tˉ〔h11d|) DIi∩t(11) 11≡dO〔(|1j:1a5tˉ〔‖i1d|) pri∩t(1i)



| l26

第3章网页数据的解析提取

容提取出来° 运行结果如下: +ir5tite∏l



t‖1m1te‖

千j什h1teⅧ

re5u1t≡5e1e〔toI·xpat竹(0//1j[co∩t己i∩5(@c1己55’"jteⅦˉO")]//text()!).get() pI1∩t(re5u1t)

输出结果如下: +jr5t iteⅦ

这里我们使用//11[co∩ta1∩5(0〔1a55’"1teⅧˉO")]//text()选取了所有〔1as5包含1teⅦˉO的11节 点的文本内容°准确来说’返回结果5e1e〔toI[15t应该对应三个1j对象,而这里get方法仅仅返回

■‖』』勺《■‖】当■可□■〗可』■■凸□】{□■□‖

我们再看一个实例:

‖(

get方法的作用是从5e1e〔tor[15t里面提取第一个5e1e〔toI对象然后输出其中的结果°

了第一个1j对象的文本内容’因为它其实只会提取第_个5e1e〔tor对象的结果。

那有没有能够提取所有5e1e〔tOI对应内容的方法呢?有’那就是geta11方法。 所以如果要提取所有对应的11节点的文本内容,写法可以改写为如下内容: re5l」1t=5e1e〔toI.xpat∩(0//11[〔o∩tai∩5(伙1a55′"jteⅧˉo")]//te×t()』)。geta11() Pr1∩t(re5u1t)

这时候’我们得到的就是列表类型的结果’其中的每_项和5e1e〔tor对象是—一对应的。

因此,如果要提取5e1e〔tor[15t里面对应的结果,可以使用get或geta11方法’前者会获取第 -个5e1e〔tor对象里面的内容’后者会依次获取每个5e1eCtoI对象对应的结果°

另外在上述案例中,如果把XPat∩方法改写成C55方法’可以这么实现: re5u1t≡5e1e〔tor.c55(0 .iteⅧˉO*;:text‖).geta11() pIi∩t(Ie5u1t)

这里*用来提取所有子节点(包括纯文本节点)’提取文本需要再加上::teXt’最终的运行结果和 上面是—样的°

到这里’我们就简单了解了提取文本的方法°

‖{||(‖(|■|||‖(|

[干iI5tite们|’ 』thimite川‖’千1代∩ite"‖]



输出结果如下:

5.提取属性

刚才我们演示了HTML中的文本提取,直接在XPath中加人//text()即可,那提取属性怎么实 现呢?方式是类似的’也是直接在XPath或者CSS选择器中表示出来就可以了°

1

例如我们提取第三个11节点内部的a节点的hre十属性’写法如下:

re5u1t=5e1e〔tor。〔55(‖。ite肌ˉ0·a〔tjvea: :attr(hIe于)‖).8et() prj∩t(re501t)

re5u1t=5e1e〔tor·xPath(,//11[〔o∩taj∩s(0〔1己55’ !01te"ˉO,』)己∩d〔o∩taj∩5(@〔1a5s’ "a〔tjγe")]/a/0hre+‖).get() prj∩t(re5u1t)

这里我们实现了两种写法’分别用〔55和×patb方法实现。我们以同时包含ite‖ˉo和a〔t1γe两 个〔1a55为依据’来选取第三个1j节点,然后进-步选取了里面的a节点°对于CSS选择器’选取

‖‖‖‖‖』‖‖‖‖‖□■■■‖‖』‖‖‖』‖』‖‖』■〗可‖]‖‖■〗』

千ro"paI5e1iⅦport5e1e〔tor 5e1e〔tor二5e1e〔toI(text≡btⅧ1)





3。4

parsel的使用

l27

属性需要加::attr(),并传人对应的属性名称才可选取;对于XPath’直接用/0再加属性名称即可选 取。最后统一用get方法提取结果° 运行结果如下:

■■》‖『

1j∩旧。ht∏1 11∩促3°∩tⅧ1

■厂【‖‖|巳■「‖∩||

可以看到两种方法都正确提取到了对应的‖re十属性°

6正则提取

p



0



|′ p







p

除了常用的〔55和xpath方法’5e1e〔toI对象还提供了正则表达式提取方法,我们用—个实例来 了解下: +roⅦpar5e1mport5e1ector 5e1e〔tOr=5e1eCtOI(teXt=打t"1) Ie5u1t=5e1e〔tor°〔55(‖。1te∏ˉ0』〉·re(‖1i∩促.*0) prj∩t(Ie5u1t)

这里先用c55方法提取了所有c1a5s包含jte们ˉO的节点,然后使用re方法传人了1j∩促.*,用来 匹配包含1j∩促的所有结果。 运行结果如下:

[‖1j∩旧°ht‖1"〉〈5pa∩〔1a55="bo1d"〉thirdjte川〈/5pa∩〉≤/a×/1j〉‖’ ‖1j∩低5。htⅧ1"〉十1什h1te们</a〉≤/11〉0]

‖ ‖■厂|▲■「

可以看到’Ie方法在这里遍历了所有提取到的5e1e〔tor对象,然后根据传人的正则表达式,查 找出符合规则的节点源码并以列表形式返回°

匹庐’■■『■「‖臼卜■Ⅳ‖■「▲■■尸′尸γ■尸‖仆Ⅲ■■尸。巴广△『|■■·■厂「【■『||…■「

当然’如果在调用〔55方法时’已经提取了进_步的结果,例如提取了节点文本值,那么re方 法就只会针对节点文本值进行提取: 千ro|‖p己r5e1mpOrt5e1e〔tOr se1|9〔tor=5e1ector(text=‖tⅧ1)

re5{」1t=5e1e〔tOr.C55(0 .iteⅦˉO*: :teXt|).re(! 。*1te‖!) pIi∩t(re5u1t)

运行结果如下: [|+jI5t 1teⅦ‖’ ‖th1rd1te‖‖ ’ 0千1十thjte川0 ]

我们也可以利用re千1r5t方法来提取第—个符合规则的结果: 十ro们par5e11Ⅶport5e1e〔toI 5e1e〔tor=5e1e〔tor(text=‖tⅧ1)

resu1t≡5e1ectoI·〔55( ‖ .ite"ˉo‖).reˉ十jr5t(`〈5pa∩c1a55="bo1d"〉(·*?)</5pa∩〉0) pr1∩t(re5u1t)

这里调用了re+1I5t方法,提取的是被5pa∩标签包含的文本值’提取结果用小括号括起来表示 一个提取分组,最后输出的结果就是小括号包围的部分’运行结果如下: thiIdjte∏

通过这几个例子’我们知道了正则匹配的_些使用方法’Ie对应多个结果’re+1I5t对应单个结 果’在不同情况下可以选择合适的方法进行提取。

「′‖

7.总结

parsel库是_个融合了XPath、CSS选择器和正则表达式的提取库’功能强大又灵活’建议好好 学习一下,同时可以为之后学习Scrapy框架打下基础,有关parsel更多的用法可以参考其官方文档 https://parselreadthedocs.io/·

)}





本节代码参见: https://github.com/Python3WebSpider/ParselTCst。





第4章

」■『·



§ 吁

数据的存储 「」 用解析器解析出数据后’接下来就是存储数据了°数据的存储形式多种多样’其中最简单的_种

是将数据直接保存为文本文件’如TXT` 』SON、CSV等。还可以将数据保存到数据库中,如关系型

数据库MySQL’非关系型数据库MongoDB`Redls等°除了这两种’也可以直接把数据存储到一些 搜索引擎(如Elasticsearch)中,以便检索和查看° 本章我们就来了解一些基本的数据存储的操作。

4| 丁X丁文本文件存储 就是不利于检索。所以如果对检索和数据结构的要求不高,追求方便第_的话,就可以采用TXT文 本存储°

我们接下来看_下利用Python保存TXT文本文件的方法° |.本节目标

‖((‖‖

将数据保存为TXT文本的操作非常简单’而且TXT文本几乎兼容任何平台’但是这也有个缺点,

■司‖‖■Ⅵ』■|·■■■Ⅵ』可|』■■司‖■‖■■Ⅵ‖·]」■■γ』□〗■】『■可■■‖‖□■‖■□γ‖』闪』□∏■■‖■勺‖■■】‖·】■■■■‖



我们以电影实例网站https://ssrl.scrape.center/为例,爬取首页l0部电影的数据,然后将相关信息 存储为TXT文本格式°

〗‖门

2基本实例 实例代码如下:



1∏portreque5t5

+11e=ope∩(‖∏)oγ1e5.txt‖’ ,w0 ’ e∩〔odi∩g≡Ut十ˉ8,)



(‖‖

ur1= 0∩ttp5://55r1·5〔mpe·ce∩ter/0 ht"1=req0e5t5.get(uI1).text do〔≡pq(向tⅦ1) iteⅦs=do〔(‖·e1ˉ〔ard|〉.jteⅧ5()

■‖‖·司

+roⅦpγqueIy1ⅦportpyQuerya5pq 1‖portre

千Orite"1∩1te们5:

∩aⅧe=ite"·千1∩d(0a〉∩2!).text() +11e.wrjte(十‖名称: {∩a川e}\∩0 ) #类月||

〔ategor】e5= [ite∏.text()十oIjte"i∩1te".十1∩d(『·〔ategor1e5butto∩5pa∩0〉.iteⅧ5()] fi1e.wI1te(千|类别: {c己tegorje5}\∩‖)

#上映时间

pub1i5hedat=jte川.+1∩d(0 .1∩十o:co∩t3i∩s(上映)‖ ).text()

pub1j5hedat=re.5ear〔h(0(\d{4}ˉ\d{2}ˉ\d{2})0 ’ pub115hedat). group(1) \ i十pub1i5hedat己∩dI巳5ear〔‖( 0\d{4}ˉ\d{2}ˉ\d{2}‖』 pub115hedat)e15e‖o∩e

+j1e.wr1te(+』上映时间: {pub115hed己t}\∩0 ) #评分

二■■‖】』■■‖|‖丑■■可‖■‖■■■□■■■」』■■■■■Ⅵ

#电影名称

(|

} } }



『 p



p





「 p

4.l

TXT文本丈件存储

l29

5〔ore≡jteⅦ.千j∩d(`p.5core|).text() +11e.Wrjte(「』评分: {5〔Ore}\∩‖) □ fj1e°Wrjte(千0{`「≡" * 50}\∩0) 十i1e·〔1o5e()

这里的目的主要是演示文件的存储方式,因此省去了Ieque5t5异常处理部分°首先,用requests库 提取网站首页的HTML代码,然后利用pyqueIy解析库将电影的名称`类别、上映时间、评分信息提 取出来°

利用Python提供的ope∩方法打开_个文本文件’获取一个文件操作对象’这里赋值为+j1e’每 提取_部分信息,就利用千11e对象的Write方法将这部分信息写人文件°

全部提取完毕之后’调用C1O5e方法将十11e对象关闭,这样抓取的网站首页的内容就成功写人



文本中了°

运行程序’可以发现在本地生成了—个moviestxt文件,其内容如图4ˉl所示。 —一=可





隔印:■王别蝎ˉ「o「…u时〔O∩c山me 舆别: !0题伯0′ 0爱情0]



上蛔间: 1”3宅7=泌

评分: 9°5

f

_≡

名棉8这个杀手不太冷=L印∩

|「

炎别8 [0剧悄!, 0动作! 0 0…0] 上蛔闷:鸡酗≈砾M 评分:9·5

广

名仰8冈申兜…=∏淀5…恤呻……tm∩ 类别吕 [°剧澜", 0犯暇{】



上映时闽$ 1…≡吟10



汗分; g·5

名称8纂坦臆克号=∏1t巳MC



舆别: !b膘讶, 0囊仍0′‖灾道0] 上…谰: 1…“心〕



评分; g·5





名称2歹马假日=∩…∩肋u□■γ

矣别; [■侣』0■■0’ 0Ⅲ钥0 ] 上殃时闽; 195…汕

评分:9°5 -→~-■一

图4ˉl

mov|esˉtxt文件中的内容

电影信息的内容已经被保存成了文本形式° 可以看出,电影信息的内容已经被保仔叹J又平旭义°

回过头来看下本节重点需要了解的内容’即文本写人操作,其实就是Ope∩`"Ijte` C1O5e这三个 方法的用法°

Ope∩方法的第一个参数是要保存的目标文件名称;第二个参数代表数据以何种方式写人文本,此

处为"’表示以覆盖的方式写人;第三个参数指定了文件的编码为utf8。最后’写人完成后,还需要 调用C1O5e方法来关闭文件对象。 3.打开方式

在刚才的实例中, Ope∩方法的第二个参数设置成了"’这样在每次写人文本时都会清空源文件’ 然后将新的内容写人文件°"只是文件打开方式的一种’下面简要介绍一下其他几种。 □n以只读方式打开一个文件’意思是只能读取文件内容,而不能写人°这也是默认模式°

气ˉ[ |`|一1仕电|□砖专士士丁耳一个寸灶擂置田干打开一}井铜|立件.例如音频`图片`视频等。

rb:以二进制只读方式打开一个文件’通常用于打开二进制文件’例如音频`图片`视频等。

□□

r+:以读写方式打开_个文件’既可以读文件又可以写文件°

rb+:以二进制读写方式打开一个文件’同样既可以读又可以写,只不过读取和写人的都是二 进制数据。



』勺|‖』■■

l30

第4章数据的存储

新文件。

□"b:以二进制写人方式打开一个文件。如果该文件已存在,则将其覆盖。如果该文件不存在, 则创建新文件。

‖‖|(■■■‖旦■∏‖■■

□":以写人方式打开—个文件°如果该文件已存在’则将其覆盖。如果该文件不存在’则创建

□"+:以读写方式打开一个文件°如果该文件已存在,则将其覆盖°如果该文件不存在,则创建 新文件°

则创建新文件。

□日:以追加方式打开_个文件。如果该文件已存在’则文件指针将会放在文件结尾。也就是说, 新的内容将会被写到已有内容之后°如果该文件不存在’则创建新文件来写人。

□ab:以二进制追加方式打开_个文件。如果该文件已存在,则文件指针将会放在文件结尾。也

就是说,新的内容将会被写到已有内容之后°如果该文件不存在,则创建新文件来写人°

□a+:以读写方式打开_个文件°如果该文件已存在’则文件指针将会放在文件结尾。文件打开 时会是追加模式°如果该文件不存在’则创建新文件用于读写。

□日b+:以二进制追加方式打开一个文件。如果该文件已存在’则文件指针将会放在文件结尾° 如果该文件不存在’则创建新文件用于读写。

■■司■勺‖』‖□勺」‖』■司‖‖』口■∏‖‖|』·□■■■

□"b+:以二进制读写格式打开一个文件。如果该文件已存在’则将其覆盖°如果该文件不存在,

4.简化写法

文件写人还有_种简写方法’就是使用w1t∩a5语法°当"jth控制块结束时,文件会自动关闭, 这种保存方式可以简写如下: "ithope∩(!mvje5.txt0 ’W|’ e∩codj∩g=‖ut十ˉ8‖): 「i1e°"rite(「0启称: {∩a∏e}\∩0) 「11已wrjte(「』矣别: {categor1e5}\∩0) 十j1巳write(千|上映时间: {pub1j5hed己t}\∩′) +j1e·"rjte〈+0评分: {5〔ore}\∩0)

基本的数据存储方法° 5.总结

本节我们了解了基本TXT文件存储的实现方式,建议熟练掌握°

JSON’全称为JavaScnptObjectNotation,也就是JavaSc∏pt对象标记,通过对象和数组的组合来 表示数据’虽构造简洁但是结构化程度非常高,是一种轻量级的数据交换格式° ↑.对象和数组

在JavaScrlpt语言中,—切皆为对象’因此任何支持的数据类型都可以通过JSON表示’例如字 符串`数字`对象、数组等°其中对象和数组是比较特殊且常用的两种类型,下面简要介绍一下这 两者°

对象在JavaScript中是指用花括号{}包围起来的内容’数据结构是{Rey1: γa1ue1’ 长ey2: γa1ue2’…}这种键值对结构°在面向对象的语言中, 低eγ表示对象的属性`γa1ue表示属性对应的值,

( □ 』 ■■』】■∏】‖』』·■■可』■Ⅵ■■■■□曰■■■■□·」■■‖』】

本节我们就来了解如何利用Python将数据存储为JSON文件。

□司■■』■■‖■·]|』·‖」■■

42 」SON文件存储



‖」

本节代码参见: https://githuhcom/Python3WebSpjder/FjleStorage北st°



∏■‖□]‖■■

以上便是利用Python将结果保存为TXT文件的方法’这种方法简单易用`操作高效,是_种最

‖(‖□

意味着不需要再调用〔1O5e方法°

42

JSON丈件存储

l3l

前者可以使用整数和字符串表示,后者可以是任意类型°

数组在JavaScript中是指用方括号[]包围起来的内容’数据结构是[』,jaγa"’"jaγa5〔rjpt|`’ "γb』』’…]这种索引结构°在JavaScript中,数组是—种比较特殊的数据类型,因为它也可以像对象



那样使用键值对结构,但还是索引结构用得更多。同样’它的值可以是任意类型° 所以’—个JSON对象可以写为如下形式: {{





"∩己‖∏e": "8ob"’ "ge∩der": "∏a1e冈’ "bjrthday,0 吕 "1992ˉ1Oˉ18"

}’{

—q

"∩a爬": "5e1j∩a闪’

00ge∩deI阿: "+e爬1e0‘’ 00birt∩day": 001995ˉ1Oˉ1800 }]

由[]包围的内容相当于数组,数组中的每个元素都可以是任意类型’这个实例中的元素是对象, 由{}包围。

JSON可以由以上两种形式自由组合而成’能够嵌套无限次,并且结构清晰,是数据交换的极佳 实现方式。 2读取」SON

Python为我们提供了简单易用的JSON库’用来实现JSON文件的读写操作,我们可以调用』SON 库中的1oad5方法将JSON文本字符串转为JSON对象°实际上, JSON对象就是Python中列表和字 典的嵌套与组合°反过来,我们可以通过dⅧp5方法将JSON对象转为文本字符串°

例如,这里有一段JSON形式的字符串’是5tr类型’我们用Python将其转换为可操作的数据结 构,如列表或字典: iⅧportj5o∩ 5tr= 0 0 0

[{ "∩a『爬": "Bob"’

厕ge∩der": 闪帕1e"’ 00birthd己y"日 冈1992ˉ10ˉ18∏ }’{ 00∩a爬■: 困5e1i∩a呵’

口ge∩deI■目 冈+e佃a1e"D 口birt∩d日y■: 口1995ˉ10ˉ18回 }]

pri∩t(type(5tr)) data=j5o∩.1oad5(5tr) pIj∩t(data) prj∩t(tyPe(data〉)

运行结果如下: 〈C1己5505tI『〉

[{,∩a爬|: ′8ob!’ 』ge∩der|: 0吧1e!’『birthday0 : ,1992ˉ1oˉ180}’{,∩a′∏e|: ,5e1i∩a! ’ ,ge∩der』:干e们a1e{ ’ ′bjrthday, : ,1995ˉ1Oˉ18,}]

<〔13S5 01i5t0〉

这里使用1oads方法将字符串转为了JSON对象。由于最外层是中括号,所以最终的数据类型是 列表类型°

这样—来,我们就可以用索引获取对应的内容了°例如,要想获取第一个元素里的∩a∏e属性, 可以使用如下方式:





第4章数据的存储

dat己[0][』∩己爪e0 ]

data[o]get(‖∩a"e0 )

得到的结果都是: Bob

认值),实例如下:

运行结果如下: ‖o∩e

25

这里我们尝试获取年龄age’原字典中并不存在该键名’因此会默认返回‖o∩e。此时如果传人了 第二个参数’就会返回传人的这个值°

值得注意的是, JSON的数据需要用双引号包围起来’而不能使用单引号°例如使用如下形式, 就会出现错误: i"Portj5o∩ 5tr≡ , ! 『

[{ !∩己Ⅶe! : ,8ob! ’

!ge∩deI! ; 0"a1e|’ bjrthd己y‖: |1992ˉ1Oˉ18|

}]

d日ta=j5o∩.1oad5(5tr)

运行结果如下: j5o∩.decoder·〕50‖0e〔ode[IroI:〔xpect1∩gproperty∩a"ee∩〔1o5ed i∩doub1eq(」ote5: 11∩e3〔o1u"∩5 (〔har8)

这里出现了JSON解析错误的提示,其原因就是数据由单引号包围着°再次强调’请千万注意

JSON字符串的表示需要用双引号’否则1oad5方法会解析失败。

下面实现从JSON文本中读取内容.例如有一个datajson文本文件,其内容是刚才定义的JSON字 符串,我们可以先将文本文件中的内容读出’再利用1oads方法将之转化为JSON对象: j"portj5o∩

wjthope∩(,d日ta.j5o∩′′ e∩codi∩g=0ut+ˉ8|) 己5十j1e: 5tr=十i1巳re己d() data=j5o∩.1oad5(5tr) pri∩t(d日t己)

运行结果如下:

[{0∩a们e|: 08ob‖ ’ 0ge∩deI』: ,∏a1e‖ ,bjIthd己γ‖: ‖1992ˉ1Oˉ180}’{‖∩a刚e0 : ‖5e11∩a‖ ’ 08e∩deI0 : 干eⅧ己1e0 ’

`birt∩day` : |1995ˉ1Oˉ18‖}]

这里我们使用oPe∩方法读取文本文件,使用的是默认的读模式’编码指定为utf8,并文件操作 对象赋值为「11e°然后我们调用十11e对象的read方法读取了文本中的所有内容,赋值为5tI。接着 再调用1oad5方法解析JSON字符串’将其转化为JSON对象。

』■□|』■】勺』■司·■■□』■】|‖乙■‖■■■■』■∏』门‖|乙□可』■■■】』□‖』■■□二■■‖■■∏‖■可□‖■■‖·』■司‖■■■‖·‖·Ⅵ■■■‖口■■■|‖‖」■■‖□」■■』■`|」』■■

data[O].get(0age‖) dat己[O].get(0日ge,」 25)

■■√二■■

以中括号加0作为索引,可以得到第-个字典元素,再调用其键名即可得到相应的键值。获取键 值的方式有两种,_种是中括号加键名,另一种是利用get方法传人键名。这里推荐使用get方法. 这样即使键名不存在’也不会报错’而是会返回‖o∩e。另外’get方法还可以传人第二个参数(即默

、|」』』■】■】】■】〗】□】』■】■〖引|』■]』‖勺引』■|勺■■‖勺‖■■】‖`|=■

l32

其实上述实例有更简便的写法’可以直接使用1Oad方法传人文件操作对象,同样也可以将文本

‖‖





■『『卜■β ■尸‖■■尸‖巴‖‖囚■『[◆『′

4.2

JSON丈件存储

l33

转化为JSON对象,写法如下: mportj5o∩

d日ta=j5o∩.1o己d(ope∩( !dat己.j5o∩! ’ e∩〔od1∩g≡0ut千ˉ8‖ 〉) pri∩t(data) p

}}

注意这里使用的是1oad方法,而不是1oad5方法°前者的参数是—个文件操作对象’后者的参数 是—个JSON字符串。 这两种写法的运行结果是完全—样的°只不过1oad方法是将整个文件中的内容转化为JSON对

■∩‖广■■厂∩|‖‖匹□■尸『■■‖『|巴■∏『‖|凸▲尸『『‖『

氮Ⅱ|腮爵以更灵……化哪….耐方灌可以窿遥…下…嫡. 可以调用du"p5方法将JSON对象转化为字符串°例如,将上面例子的运行结果中的列表重新写 人文本: 1川portj5o∩

)|卜

data= [{ ′∩aⅧe0 ; ‖8Ob}』 ‖ge∩der! ; |帕1e|』 birthd己γ『 : 0199∑ˉ1Oˉ180 }]

卜卜广

Mt∩ope∩(0data。j5o∩』’ 0"』’ e∩codi∩g=‖ut+ˉ8!) 日5十i1e: 千i1e."Iite〈j5o∩.du们p5(dat己))

这里利用duⅦp5方法’将JSON对象转为了字符串’然后调用文件的"rite方法将字符串写人文 本’结果如图4ˉ2所示°



图4ˉ2将列表写人文本

另外’如果想保存JSON对象的缩进格式’可以再往du爪p5方法中添加—个参数1"de∩t’代表缩 进字符的个数。实例如下: withope∩(‖d3ta·j5o∩‖’W』) a5f11e: 十j1e.write(j5o∩.du们p5(data’ i∩de∩t=2))

此时写人结果如图4ˉ3所示。

p营— ˉ二ˉ

……





的唾帕】"印b■,

0q既吨广‘日 0$m1e$o0

哟p1穴呻厂; 凹】田3ˉ】陆】″ }

图4ˉ3将列表写人文本并保存缩进

能够看出’得到的内容自带缩进’格式更加清晰°

另外’如果JSON对象中包含中文字符’会怎么样呢?现在将之前JSON对象中的部分值改为中 文’并且依然用之前的方法将之写人文本:

」』】■■■■〗』

第4章数据的存储

l34



i爪pOrtj5O∩ …

data二 [{ ∏a∏‖e! : ‖王伟′’

!ge∩der′ ; 0男!’ bjrthday‖ : 01992-1Oˉ18‖ }]



门jtbope∩(‖data。j5o∩! ′ 0w|’ e∩〔odj∩8=0ut千ˉ8{) 己5+i1e: 千i1e.wrjte(j5o∩.du∏p5(data’ i∩de∩t=2))





』‖



密……≡ˉ

l||‖■■■■■‖‖ M∩…008 q』 \u738D\叫↑17闪Q

‖‖‖

写人结果如图4ˉ4所示°



00…0“产; 00 \uγ5〕γ 00’ mb1徒刷盯00: o01992=1队18凹 } ] ■

0

图4ˉ4包含中文字符的错误写人结果



可以看到,文本中的中文字符都变成了Unjcode字符,这显然不是我们想要的结果.

要想输出中文,还需要指定参数e∩5orea5C11为「a15e’以及规定文件输出的编码:



wjthope∩(0data.j5o∩0 ’W0 ’ e∩〔odj∩g=‖ut十ˉ80) a5十11e; 十i1e."r1te(j5o∩.duⅧp5(data’ j∩de∩t≡2’ e∩50Iea5cjj=「日15e))

‖|‖

此时的写人结果如图4ˉ5所示。





00∩…“: "王伟"O

闻…e产: "男咖0 00b1穴may《』: e01g92=1队1800

‖』|‖

—镶趣雨}

‖|{‖

图斗ˉ5包含中文字符的正确写人结果

能够发现,现在可以将JSON对象输出为中文了。

j5o∩.du呻(data’ ope∩(!data。j5o∩0 ’W‖ ’ e∩〔odi∩g≡0ut「ˉ8‖)’ j∩de∩t二2’ e∩5uIea5〔ij=「a15e)

这里第一个参数是JSON对象’第二个参数可以传人文件操作对象,其他的1∩de∩t、e∩5uIea5Cii 对象还是保持不变,运行结果是一样的° 4.总结

■■■|』‖‖||·■■可」■`□■■‖司』■■

类比1oad5与1oad方法, du"p5同样也有对应的du"p方法’它可以直接将JSON对象全部写人文 件中,因此上述写法也可以写为如下形式:



本节我们了解了用Python读写JSON文件的方法,这在后面进行数据解析时经常会用到,建议熟 练掌握°

43CSV文件存储 式存储表格数据°CSV文件是—个字符序列,可以由任意数目的记录组成’各条记录以某种换行符分

q

』■』·`|‖」·‖』

CSV,全称为CommaˉSeparatedValues,中文叫作逗号分隔值或字符分隔值’其文件以纯文本形

■司||』‖』《】■■‖|』□】■□」司

本节代码参见: https://gjthub.com/Python3WebSpjde【√FjleStomgeT℃st°

[ 广

日 ] ]



储 存 件

日γ ℃

〕 △



「 ■■『‖|卜‖血尸『‖|『△尸|

隔开°每条记录都由若干字段组成’字段间的分隔符是其他字符或字符串,最常见的是逗号或制表符° 不过所有记录都有完全相同的字段序列,相当于—个结构化表的纯文本形式°它比Excel文件更加简 洁’XLS文本是电子表格’包含文本、数值`公式和格式等内容,CSV中则不包含这些,就是以特定 字符作为分隔符的纯文本’结构简单清晰。所以,有时候使用CSV来存储数据是比较方便的°本节 我们就来讲解Python读取数据和将数据写人CSV文件的过程°

巴伊『‖巴β『

‖.写入

匹■尸〗■田甲

这里先看_个最简单的例子:

β■■■■匹■■■尸



加Port〔5γ

wit∩ope∩(|data·〔5γ‖’ 0"0)a5〔5v千i1e: 卜『

■「凹■■厂卜◆【‖■尸β丛■「

|‖疆撇‖骡鞠抒:6|{ |腮搬删{{|删!|;:删]l1]) 这里首先打开datacsv文件’然后指定打开的模式为"(即写人)’获得文件句柄,随后调用csv库 的Writer方法初始化写人对象,传人该句柄,然后调用wrjterOW方法传人每行的数据’这样便完成 了写人。

|▲■■‖‖|

=■尸|||

运行结束后’会生成一个名为datacsv的文件’此时数据就成功写人了。直接以文本形式打开’ 会显示如下内容:

仇 尸

jd’∩a爬’age

△◆尸

1呕1’‖让e’2O 1"o2’8ob’22 1OOO3’〕oId日∩’21

● 【 『 「 } ■ ■ 「■「

可以看到,写人CSV文件的文本默认以逗号分 隔每条记录’每调用_次Wr1terOw方法即可写人_

■■尸|

行数据。用Excel打开datacsv文件的结果如图4ˉ6

|一·镭…廊…孺·. 尚圃舷泻.ˉ · E…α∏′|

{鸦ˉ■0一蛔皿窜□j《?P箩霉磐 ′′…i亏匣霄 }…

■ 厂 卜匡■伊『份■■■‖β||匹伊

『d

所示。

:| ×√卢

°

A 一≡A兰ˉ→瓣舜霹缝颗蹿′…一】

∩■们诌

呻ˉ.

1α■1棚胀e

如果想修改列与列之间的分隔符,可以传人

ⅧⅢⅡF;… 1…′…∩

ˉ



’ˉ

m

晕斗≈ ,



0

亚 a

ˉ

de1jmter参数’其代码如下: i‖晒rt〔5γ

△ ■ 『

》=■叮■『》『

wit‖ope∩(《dat己。〔5γ|’『w’)a5〔s叶i1e: wTiter=〔5v.mjter(〔5γ十i1e’ de1jmter=‖`) Nriter°盯iteI叫([』jd』’ 0∏…’’ ,age,]) Nriter.NriteI叫([『1皿1|′ °"北e!’ 2O]) 盯jter.wTiter叫([,1咽2′’ ,8ob『’ 22])

卜『′~■厂

|谣删删}卜删;撇"洲1])

…■

图46用Excel打开data.csv文件

这里在初始化写人对象时,将空格传人了de11mter参数,此时输出结果中的列与列之间就是以 空格分隔了,内容如下: id∩a眶age 1咖1"jke2o

止■厂|

1咖】8ob22

卜}}

1咖3〕orda∩21





另外’我们也可以调用"riterow5方法同时写人多行,此时参数需要传人二维列表,例如: j↑‖port〔5γ

Wjthope∩(|data.〔5γ!’w』)a5〔5γ十i1e: writer=〔5γ.Wrjter(〔5γ十j1e)

l36

第4章数据的存储 "riter."ritero"([!jd0 ’ ‖∩a们e! ’ 0age,])

wIjter.wrjterow5([[01OOO1! 」刊j|(e|’ 2O]’[|1OOO2! 』 !Bob! 」 22]’[ 01OOO3! ’ !〕orda∩0 ’ 21]]〉

id’∩3『‖e’age 1OOO1’‖ike’20 1Ooo2’Bob’22 10OO3’〕OIda∩’21

但是_般情况下’爬虫爬取的都是结构化数据’我们-般会用字典表示这种数据。cSv库也提供 了字典的写人方式,实例如下: mpOrt〔5γ

1d’∩a们e』a8e

这样就把字典写人了CSV文件中。

如下: 加port〔5γ

"1thope∏(0data.〔5γ‖’ 』a‖)a5c5γ十11e: 十1e1d∩a川e5= [0jd|’ ‖∩a爪e‖ ’ 0age0 ] writer=c5vDj〔t‖rjter(C5γ十11e’「ie1d∩a∏e5≡+je1d∩a|∏e5) wI1teI.Ⅳr1terow({!1d‖: 01OO040 ′ !∩a「∏e! : |Dur日∩t0 ’ 0age! : 22})

这样再次执行这段代码,文件内容便会变成: id’∩a『∏e’age 1OOO1’‖让e’2O 10oo2’8ob’22 1"03’〕Ord己∩’21 1"04’00ra∩t’22

由结果可见’数据被追加写人到了文件中。

如果要写人中文内容’我们知道可能会遇到字符编码的问题,此时需要给ope∩参数指定编码格 式°例如’这里再写人—行包含中文的数据’代码改写如下: i们pOrt〔5V

q

‖·】|‖□■」□|‖|■■■‖口|‖旦勺』■‖||■■■‖门|■■‖{|』』■]』■‖||■■】〗‖■■∏』■‘{{‖|」□■||』·{|■■∏|‖■■|]|■日■尸

另外’如果想追加写人’可以修改文件的打开模式’即把Ope∩函数的第二个参数改成a,代码



□■■‖‖」■■司

1卯01’‖j代e’2O 1OOO2’8ob」22 10003’〕orda∩’21

●■‖』■■■‖日当■■句·』■■■

这里先定义了3个字段,用+1e1d∩a"e5表示’然后将其传给0i〔t‖r1ter方法以初始化-个字典 写人对象’并将对象赋给"r1ter变量°接着调用写人了对象的"ritehe己der方法先写人头信息’再 调用"r1tero"方法传人了相应字典。最终写人的结果和之前是完全相同的,内容如下:

□■‖■

WithOpe∩(‖d己ta.〔5v0 ’ !w0)a5〔5γ十i1e: 十1e1d∩a|∏e5=[1d‖’ ‖∩a爪e! ′ !age! ] wrjter=〔5γ.Oi〔t‖r1ter(c5v十i1e’ 十1e1d∩己们e5≡千ie1d∩a|∩es) WIjteLWrite打eader() "r1ter。"riterow({』jd‖: ‖1oo01‖ 」 ‖∩a川e』: |‖让e|」 ‖age0 : 20}) wrjter.writerow({!jd0 ; ‖1O002|’ 』∩日∏e‖ : |8ob0 》 0a8e‖ : 22}) "rjter.wrjtero"({』jd: ‖1OO03‖ ’ 』∩a|∏e‖ : 』〕orda∩‖’ |age′: 21})

d

· ] 』 】 ■ ■ 』 ■ 可 ‖ ‖ ■ 】 ‖ ● | | 」 ■ ■ 』 ■ ■ ‖ ‖ 】 ■ 司 」 ‖ 〈

输出结果是相同的,内容如下:

0



wjthope∩(‖data.〔5γ‖’ 0a, ’ e∩〔od1∩g=0ut千ˉ8,) 日5〔5γ十11e: 十ie1d∩a『∏e5≡ [ ‖jd’ 0∩日Ⅶe0 ’ 0age0] NIjter=〔5γ。Di〔t‖I1ter(〔5V十11e’ 十je1d∩a们e5≡十1e1d∩a|∏e5) "r1teI。"riterow({‖1d0 : {1OOO』0 ’ |∩a∏p, : ‖王伟|’ 』age0 : 22})

‖ | ●

另外’如果接触过pandas等库’可以调用0ata「ra"e对象的to〔5γ方法将数据写人CSV文件中。

■■∏||

这里要是没有给Ope∩函数指定编码’可能会发生编码错误°

』|



)‖

} ■■『尸■β■厂·〖■

43CSV丈件存储

l37

这种方法需要安装pandas库,安装命令为: pip3 i∩5ta11pa∩d己5

安装完成之后’我们便可以使用pandas库将数据保存为CSV文件’实例代码如下: mportpa∩da5己5pd P

d日ta二 [

{‖jd0 : 01ooo1‖ ’ 』∩aⅧe‖ : |‖让e! ’ |age‖ ; 20}’ {‖id‖ ; !1ooo20 』 |∩a爬‖ : ‖Bob|’ |age! : 22}’ {‖id0 : ‖1oo03|’ 0∩a们e‖ ; 0〕ord己∩0 ’ 0age|: 21}’

p

「′■



d+=pd.0at己「ra∏e(data〉 d+。to〔sγ(‖data.〔5γ0 ’ j∩de×=「a15e)

止■‖皿■『■巴尸

这里我们先定义了几条数据’每条数据都是一个字典,然后将其组合成一个列表’赋值为data。

紧接着我们使用pandas的0ata「ra"e类新建了一个0己ta「ra‖e对象,参数传人data’并把该对象赋值 为d十°最后我们调用d+的to〔5γ方法也可以将数据保存为CSV文件°

匹■「‖■∏‖|■■『|‖

2.读取 我们同样可以使用csv库来读取CSV文件.例如,将刚才写人的文件内容读取出来’相关代码如下: 加port〔5γ

"1thope∩(|dat己。〔5γ』’ 0r|》 e∩codi∩g≡0ut+ˉ8‖) a5〔5v十j1e:



} ]』■■■



reader=c5v.reader(〔5v+i1e) forrowj∩reader:

pr1∩t(ro们)

运行结果如下: [ 01d0 ’ |∩aⅧe,’ 0a8e,] [ ‖10OO1‖ ’ ‖‖i代e0 ′ 』200] [ 010oo2{ ’ 08ob』’ 』22|] [{10O03|》 ‖〕Orda∩』’ 021‖ ]

这里我们构造的是Reader对象’通过遍历输出了文件中每行的内容,每_行都是一个列表。注 意’如果CSV文件中包含中文,还需要指定文件编码。



另外,我们也可以使用pandas的read〔5γ方法将数据从CSV文件中读取出来,例如: mportpa∩da5a5pd

d「≡pd.readˉc5γ(!data。〔sγ‖) pr1∩t(d+)

运行结果如下: id

∩a"e 日ge

O

1OOO1

∩让e

2O

1

1OO02

Bob

22

2

1o0o3

〕ord3∩

21

这里的d十实际上是—个0ata「raⅧe对象,如果你对此比较熟悉’则可以直接使用它完成一些数 据的分析处理。

如果只想读取文件里面的数据,可以把d十再进一步转化为列表或者元组,实例代码如下: 1∩]portPa∩d己5a5pd

d+=pd°readˉ〔5v(‖data.〔5γ‖) data=d千.γ己1ue5.to1j5t() pr1∩t(dat己)



」】■司』■□司

第4章数据的存储

这里我们调用了d十的va1ue5属性’再调用to115t方法’即可将数据转化为列表形式’运行结果 如下:

[[1OOO1’{‖ike0 ’ 2O]’[1OOO2’‖Bob|’ 22]’[100O3’ !〕orda∩‖’ 21]]

另外’直接对d+进行逐行遍历’同样能得到列表类型的结果,代码如下: i"pOTtpa∩da5a5pd

d十=pd.re己d—〔5v(!data。〔sγ!)

|(」■■】』』』】‖〈|■】|‖□{」||』|(‖』■|‖|·‖‖|山



|38

+OIi∩de×’ rO"1∩d十.jterIOW5()8

pri∩t(rOⅦ.tO1i5t())

运行结果如下: [1O0O1’ !问j促e|」 2O] [1ooo2’ ′Bob0 ’ 22] [10o03’‖〕ord己∩0 ’ 21]





可以看到,我们同样获取了列表类型的结果° 司

3.总结

{ d

本节中,我们了解了CSV文件的写人利读取方式°这也是-种常用的数据存储方式’需要熟练 掌握。





本节代码参见: https:〃githuhcom/Python3WebSpideI/FileStorageT℃st。

44MySOL存储 关系型数据库是基于关系模型的数据库,而关系模型是通过二维表来保存的’所以关系型数据库 中数据的存储方式就是行列组成的表’每_列代表一个字段、每—行代表-条记录°表可以看作某个 实体的集合,实体之间存在的联系需要通过表与表之间的关联关系体现,例如主键和外键的关联关系° 由多个表组成的数据库,就是关系型数据库°

关系型数据库有多种’例如SQLite、MySQL`Omcle` SQLServer、DB2等’本节我们主要来了 解—下MySQL数据库的存储操作°

在Python2中,连接MySQL的库大多是MySQLdb,但是此库的官方并不支持Python3’所以这 里推荐使用的库是PyMySQL°

下面’我们就来讲解使用PyMySQL操作MySQL数据库的方法。 ↑.准备工作



‖ 叫

d ■

| √





q

□‖

』■‖|‖‖■可

除了安装好MySQL数据外’还需要安装好PyMySQL库,如尚未安装PyMySQL,可以使用pjp3

0

§■Ⅵ‖■■

在开始之前’请确保已经安装好了MySQL数据库并保证它能正常运行,安装方式可以参考: https:〃setupscmpe.center/mysql°



来安装: ‖

pip3i∩5taI1py∏V5q1





更详细的安装方式可以参考: https:〃setupscrape.cente∏pymysql° 二者都安装好了之后’我们就可以开始本节的学习了。

| q



2.连接数据库

首先尝试连接-下数据库°假设当前的MySQL运行在本地’用户名为root,密码为l23456’运行 端口为3306°这里利用PyMySQL先连接MySQL’然后创建_个新的数据库,叫作spiders,代码如下:











0





| 》

44MySQL存储



l39

jⅦportpWV5q1

db=Py们γ5q1.〔o∩∩e〔t(向o5t≡01o〔a1们o5t′′u5eI≡,root』’ pa5sword≡′12]456′’ port≡〕3O6) cur5or=db.〔uI5or() 〔uI5oI.exe〔ute(!5[[[〔丁γ[RSIO‖() |〉 data=〔ur5oI.+et〔ho∩e() pIj∩t(Dat己ba5eγer51O∩: ! ’ data) 〔ur5or.exe〔ute〈"〔旺∧丁[0∧丁∧8∧5[ 5pider5D〔「∧U∏〔什AR∧〔丁[R5[丁ut+8们b4") db。c1o5e()

运行结果如下: 0atab己5eγer5io∩: (08.O。19` ’〉

■■■■

这里通过PyMySQL的〔o∩∩e〔t方法声明了—个MySQL连接对象db’此时需要传人的第_个参 【■ 数是MySQL运行的∩o5t(即IP)’由于MySQL运行在本地’所以传人的是1o〔a1∩o5t’如果MySQL 在远程运行,则传人其公网IP地址°后续参数分别是u5er(用户名)` p日s5word(密码)和port(端 口’默认为3306)。

连接成功后,调用cur5or方法获得了MySQL的操作游标’利用游标可以执行SQL语句°这里 我们执行了两个SQL语句’直接调用exe〔ute方法即可执行°第—个SQL语句用于获得MySQL的 当前版本’然后调用十etcbo∩e方法就得到了第一条数据,即版本号°第二个SQL语句用于创建数据

库spiders’默认编码为UTFˉ8,由于该语句不是查询语句,所以执行后就成功创建了数据库spiders’ 可以利用这个数据库完成后续的操作。 3.创建表

一般来讲’创建数据库的操作执行_次就可以了。当然,也可以手动创建数据库°我们之后的操 作都在spjders数据库上完成°

接下来’新创建_个数据表students’此时执行创建表的SQL语句即可°这里指定3个字段,结 构如表4ˉl所示° 表4ˉ↑ 数据表stude∩ts 字段名









jd

学号

γ己rC‖己[

∩a∏e

姓名

V己I〔har ∩

十`

年龄

导】

age

创建该表的代码如下: jⅦportPy阳γ5q1 //创边数据犀后’在连接时需兵额外指定一个泰数db

db=pyⅦy5q1.〔o∩∩e〔t(∩o5t=|1o〔a1host′’ u5er=0root{ ’ pa55word≡|1234560 ’ port≡3306’ db≡05pideI5′)

〔ur5or二db.〔ur5or〈)

5q1≡ |〔R[∧『[丁A8L[ I「‖0『[XI5丁55tude∩t5 (idγ∧R〔‖∧旧(255)‖0丁‖0儿[’ ∩aⅦeγAR〔‖A日(255)‖O『‖[儿[’ ageI‖「‖0丁"0u’ p【I灿Rγ旺γ(1d))‖ Cur5or.exec‖te(5q1) db.〔1o5e()

运行之后’便创建了_个名为students的数据表°

当然,为了演示’这里只是指定了最简单的几个字段°实际上,在爬虫爬取的过程中’我们会根 据爬取结果设计特定的字段。 4.插入数据

下_步就是往数据库中插人数据了。例如,这里爬取到一个学生信息,学号为20l2000l`名字为







Bob`年龄为20,如何将这条数据插人数据库呢?实例代码如下: j川portPy‖y5q1 jd= 02O12OOO1| u5er= |8ob|

age≡20

db=pγⅦγsq1.〔o∩∩e〔t(‖o5t≡|1o〔a1‖o5t0 ’ u5er≡0root0 》 passwoId=|123456"’ poIt≡33O6’ db=05p1der5|)

』■】√】‖■■■】‖‖‖‖{■■Ⅵ‖|‖■曰··|」】■‖』■■]|·『|』■∏

第4章数据的存储

l40

〔0r5or=db.〔ur5or()

q

5q1= 0I‖S[盯I‖丁05tMde∩t5(jd′∩a们e’ age〉γa1ueS(%55’%55’%55)0 try:

〔urSor.eXe〔ute(5q1’(jd’ 0Ser」 己ge)〉

』■司‘‖■■

db·〔oⅧit() ex〔ept: db。ro11ba〔促() db.c1o5e()

这里首先构造了_个SQL语句’其值没有用如下字符串拼接的方式构造:



+ ’

+age+ 0 ) |

这样的写法烦琐且不直观,所以我们直接用格式化符%55来构造,有几个γa1ue就写几个%55° 我们只需要在execute方法的第—个参数传人该SQL语句’γa1ue值用统_的元组传过来就好了°这

‖ ( (

5q1= ‖I‖5[盯I‖「05t仙de∩t5(id』 ∩己们e’ age)γ31ue5(| +jd+ ‖ ’ ‖ 十∩a川e

样的写法既可以避免字符串拼接的麻烦又可以避免引号冲突问题°

之后值得注意的是’需要执行db对象的〔oⅦ‖jt方法才可以实现数据插人’这个方法才是真正将 语句提交到数据库执行的方法°对干数据插人、更新`删除操作,都需要调用该方法才能生效。

这里涉及事务的问题°事务机制能够确保数据的一致性’也就是一件事要么发生完整了’要么完

■日‖』■■凸勺□

接下来,我们加了一层异常处理°如果执行失败,则调用ro11b日c代执行数据回滚’相当于什么都 没有发生过。

全没有发生°例如插人一条数据,不会存在插人_半的情况—要么全部插人`要么都不插人,这就 性’具体如表4ˉ2所示° 表4ˉ2事务的4个属性 解





乙 = ■ ■ ∏ 々 』 ■ ‖ 」 ■ 司

是事务的原子性°事务还有其他3个属性—一致性`隔离性和持久性°这4个属性通常称为ACID特



原子性(atomicity)

事务是一个不可分割的工作单位,事务中包括的诸操作要么都做、要么都不做

一致性(consistency)

事务必须使数据库从_个一致性状态变到另一个_致性状态。-致性与原子性是密切相关的 q

隔离性(isolatjon)

持久性(durabillty)

_个事务的执行不能被其他事务于扰°即—个事务内部的操作及使用的数据对并发的其他事

务是隔离的,并发执行的各个事务之间不能互相干扰

持续性也称永久性(pelmanence),指一个事务一旦提交,它对数据库中数据做的改变就应该 是永久性的°接下来的其他操作或故障不应该对数据有任何影响

插人`更新和删除操作都是对数据库进行更改的操作,而更改操作都必须是-个事务,所以这些 操作的标准写法是:

{ {d ■











try:

〔0r5or°exe〔ute(5q1) db。〔Omit()



eX〔ept:

db.Io11ba〔R()

这样便可以保证数据的一致性°这里的〔oⅦmt和ro11baCk方法就为事务的实现提供了支持°

上面数据插人的操作是通过构造SQL语句实现的,但是很明显’这里有_个极其不方便的地方,

例如突然增加了性别字段ge∩deI’此时SQL语句就需要改成:













■■■【ˉ>■■■【「『△厅‖‖|‖『|■『‖‖■「△■厂‖伊|■『『『|■■ˉ「|)‖巴厂)|【|凸■厂■}{■■【·「■‖|』■「[■『|[尸[『「|[■■■卜|||◆「》||||■尸●『『凸■□『

44MySQL存储

l4l

I‖5[盯I‖「05tude∩t5(1d」 ∩3′∏e」 日ge」 ge∩der)γ31qe5(%55」%55’%55’%55)

相应的元组参数需要改成: (id’∩a眶」 己ge’ ge∩der)

这显然不是我们想要的。在很多情况下,我们要达到的效果是插人方法无须做改动,其作为通用 方法’只需要传人-个动态变化的字典就好了°例如,构造这样-个字典: { ‖jd0 : |2O12OOO1|’ 0∩己眠0 : ‖Bob‖ 』

‖age0 : 2O

然后, SQL语句会根据这个字典动态构造出来,元组也是,这样才是实现了通用的插人方法°于 是我们改写-下插人方法: data二{ 0id0 : 02o12ooo1! ’ ′∩a爬|: ‖8ob! ’

0age0 : 2O } tab1e= 05tude∩t5′

代ey5= ! ’ 0 。jo1∩(data.key5()) γ日1l』e5= , ’ 0 .jo1∩([!‰0] *1e∩(data))

5q1= |I‖5[盯I‖丁0{tab1e}({|(eγs})γA[0[5({γa1ue5})|.+6mat(tab1e=tab1e’ Ⅶey5≡|〈ey5’ γ己1ue5=γa1ue5) try;

i十〔ur5oI.exe〔ute(5q1’ tup1e(d日ta·γa1ue5())):

| 删棚…1}) exCept:

prj∩t(0「ai1ed0) db.ro11ba〔促() db.c1o5e()

这里我们传人的数据是字典’将其定义为了data变量,将students表定义为了变量tab1e°接下来, 构造一个动态的SQL语句°

首先,需要构造插人的字段: 1d、∩a爬和age°这里只要将data的键名拿过来,并用逗号分隔即可。

所以』’|.joj∩(data.促ey5())的结果就是jd’∩a爬’ age,然后,需要构造多个%5当作占位符,有几个 字段就构造几个。例如,这里有三个字段,就需要构造%5’%5’%5。这里先是定义了一个长度为l的数

组[|%s』],然后用乘法将其扩充为[』%5|’ !%5』’|‰|],再调用jo1∩方法,就变成了%5’%5’%5。最 后’利用字符串的+omlat方法将表名、字段名和占位符构造出来。于是SQL语句就动态构造出来了: I‖5[盯I‖『05tude∩t5(jd’ ∩a‖e’ age)γA山[5(%5’%5’%5)

最后’为exe〔ute方法的第一个参数传人5q1变量,第二个参数传人由data的键值构造的元组’ 就可以成功插人数据了°

如此一来,我们便实现了通过传人一个字典来插人数据的方法’不需要再去修改SQL语句和插 人操作。

5.更新数据

数据更新操作实际上也是执行SQL语句,最简单的方式就是先构造一个SQL语句,然后执行: 5q1= |0p0∧丁[ stude∩t55叮a8e=%55朋〔R[∩a爬≡%5| try:

cuIsor.exec0te(5q1’(25’ 』8ob‖)) db.〔α门jt()

ex〔ept;

db.ro11ba〔k() db.〔1o5e(〉



}|

第4章数据的存储

l42

这里同样用占位符的方式构造SQL,然后执行exe〔ute方法,传人元组形式的参数,同样执行 〔o咖jt方法执行操作°如果做的是简单的数据更新,完全可以使用此方法。

但是在实际的数据抓取过程中,大部分情况下需要插人数据,我们关心的是会不会出现重复数据’ 如果出现了’我们希望更新数据而不是重复保存_次。另外’就像前面所说的动态构造SQL的问题, 所以这里可以再实现_种去重的方法:如果数据存在,就更新数据;如果数据不存在,则插人数据。 另外,这种做法支持灵活的字典传值。实例代码如下: d日t己= { 0id0 : 02o12卯o10 ’ ,∩a∏记0 : ‖Bob! ’

0age0 吕 21 } tab1e= 05tude∩t5|

5q1+=upd己te tIy:

i十〔uI5or.exe〔ute(5q1’ tup1e(data.va1ue5())*2): pr1∩t(’5u〔〔e5S十u1!) db.〔o∏mt() ex〔ept目

这里构造的SQL语句其实是插人语句’但是我们在后面加了0‖00p[I〔∧「[倔[γ0p0∧「[。这行代 码的意思是如果主键已经存在,就执行更新操作。例如’我们传人的数据jd仍然是20l2000l’但是

]■■■日|‖■‖』■■■∏』‖‖】

pIi∩t(|「己j1ed0) db·ro11ba〔代() db.c1o5e()

■■‖‖』■■□■■■||‖』■■

5q1= 0I‖S[R丁I‖「0{tab1e}({低eys})γ∧L0[5({v己1ues})O‖D0p[Iα『[匪γl」p0A「[|.foI阳t(tab1e=tab1e’ 恨ey5=keyS’γ日1oe5≡γa1ue5) Update= ‖’ ‖ .joj∩([卿{代ey}≡%55".「omat(keγ=keγ)十or代eγj∩dat3])

| 曰■

促eys= ‖ ’ , ·joj∩(data.恨ey5()) γ31ue5= 0 ’ ‖ .jOj∩([|%5』] *1e∩(d己t日))

年龄有所变化,由20变成了2l’此时不会插人这条数据,而是直接更新id为20l2000l的数据°构

造出来的完整SQL语句是这样的: I‖S[盯I‖「05tude∩t5(jd’∩a"e’ a8e)γ∧[0[5(%5’%5’%5)0‖00plI〔∧「[促[γ0p0A丁[id≡%5’∩a‖∏e=%5’ age=%5

这里变成了6个‰。所以后面eXe〔ute方法的第二个参数元组就需要乘以2’使长度变成原来的 2倍°

如此-来’我们就可以实现主键不存在便插人数据,主键存在则更新数据的功能° 6.删除数据

删除操作相对简单,直接使用0[[[『[语句即可,只是需要指定要删除的目标表名和删除条件’ 而且仍然需要使用db的〔o"mt方法才能生效。实例代码如下: tab1e= 05tude∩t5『

〔o∩djtio∩二 0age〉20|

sq1= |0[[[丁[「R删{t己b1e}‖‖[【[ {〔o∩djt1o∩}0 .+omat(tab1e=t己b1e’〔o∩djtjo∩=〔o∩ditjo∩)

db·〔1o5e()

《■‖‖‖」■·■

因为删除条件多种多样’运算符有大于`小于`等于` [Ⅸ[等,条件连接符有∧‖0、0R等所以 不再继续构造复杂的判断条件°这里直接将条件当作字符串来传递’以实现删除操作°

|(』

tIy: 〔uI5or.exe〔ute(5q1) db。〔o厕jt() e×〔ept: db.ro11ba〔R()





44MySQL存储

l43

7.查询数据

说完插人`修改和删除等操作,还剩下_个非常重要的操作’就是查询。查询会用到5[[[〔丁语 句’实例代码如下: $q1≡ 05[L[〔『*「R0例5tude∩t5‖‖[R[age〉=2O!

『 尸

卜 p

p



try:

〔ur5oI.exe〔ute(5q1) pri∩t(‖〔ou∩t: ‖ ’ cur5or。ro"〔ou∩t) O∩e=CuI5Or.fetC‖O∩e()

|删自oge棚§}…1() pri∩t(‖Re5u1t5: 0 ’ re5u1tS) PI1∩t(0Re5u1t5丁γpe: ‖ ’ tγpe(Ie5u1t5)) 十OrrOWj∩Ie5u1t5:

pri∩t(IOw)









『 p











「 p







β





0













p

eXCept;

prj∩t(![rIor0)

运行结果如下: 〔Ou∩t: 4

0∩e: (02012o0o10 ’ ‖8ob‖ ’ 25)

Re5U1t5: ((‖201200110 ′ ‖肘arγ0’ 21)’(020120012‖’ ‖"jⅨe0 ’ 20)’(‖2O12OO13』’ 0〕3‖`eS|’ 22〉)

Re5u1t5「ype:〈〔1as5 |tuP1e|〉 (』2O12O011! ’ ’‖ary|’ 21) (|2O12O012‖’ |‖让e|’ 2O) (|2O12O013‖′ ‖〕己Ⅶe50 ’ 22)

这里我们构造了一个SQL语句’查询年龄为20及以上的学生,然后将其传给execute方法°注 意’这里不再需要db的com1t方法°接着’调用cuI5or的row〔ou∩t属性获取查询结果的条数’当 前实例中是4条。

然后我们调用了十et〔‖o∩e方法,这个方法可以获取结果的第_条数据返回结果以元组形式呈现’ 元组中元素的顺序跟字段_一对应’即第一个元素就是第—个字段1d`第二个元素就是第二个字段

∩aⅦe,以此类推°随后’我们又调用了十et〔∩a11方法’可以得到结果的所有数据。之后将其结果和类 型打印出来,是_个二重元组’其中每个元素都是_条记录’我们遍历这些元素并输出°

但是需要注意一个问题,这里结果显示的是3条数据而不是4条’+et〔∩a11方法不是获取所有数 据吗?这是因为它的内部实现有一个偏移指针’用来指向查询结果’偏移指针最开始指向第—条数据, 取_次数据之后’指针偏移到下一条数据,于是再取就会取到下-条数据。我们最初调用了一次 十etc‖o∩e方法’这样结果的偏移指针就指向下_条数据’「et〔∩a11方法返回的是从偏移指针指向的数 据-直到结束的所有数据’所以它获取的结果就只剩3个了°

此外’我们还可以用"∩j1e循环加十etCho∩e方法的组合来获取所有数据,而不是用+etcba11全 部获取出来°十etCha11会将结果以元组形式全部返回’如果数据量很大’那么占用的开销也会非常高。 因此’推荐使用如下方法逐条获取数据; 5q1= ‖S[[[〔『*「R删5tude∩ts‖‖[R[己ge〉=2O‖ tIy;

〔ur5OI.exe〔Ute(5q1)

prj∩t(!〔o‖∩t: ! ’〔ur5oI.row〔ou∩t)

|删1;撇or…o∩e() prj∩t(‖RO": ‖ ’ rO")

Iow=〔uI5or.千et〔ho∩e()

p

eX〔ept: b





卜0



pr1∩t(‖[rroI0 )

这样每循环一次,指针就会偏移一条数据,随用随取’简单高效。



|‖■』■γ●日■■■』‖|」■■|』‖‖■■■■‖■■四■可勺■】】‖司』司

l44

第4章数据的存储

8.总结

本节我们了解了如何使用PyMySQL操作MySQL数据库’以及一些SQL语句的构造方法,后面 会在实战案例中应用这些操作来存储数据°

本节代码参见: h仗ps:〃githuhcom/Python3WebSpide!7MySQLIest。

45Mo∩9oDB文档存储 NoSQL’全称为NotOnlySQL’意为不仅仅是SQL’泛指非关系型数据库。NoSQL是基于键值

q

■■口门|■■]司

对的’而且不需要经过SQL层的解析,数据之间没有锅合性,性能非常高。 非关系型数据库又可细分如下。

□键值存储数据库:代表有Redis`Ⅵldemon和OracleBDB等°

□列存储数据库:代表有Cassandra、HBase和Riak等°

』■■■』■■■司|」■可‖■‖|‖●‖二■■■可勺{■‖

□文档型数据库:代表有CouchDB和MongoDB等° □图形数据库:代表有Neo4J` lnfbGrjd和InhniteGraph等. 对于爬虫的数据存储来说,-条数据可能存在因某些字段提取失败而缺失的情况’而且数据可能 随时调整。另外,数据之间还存在嵌套关系°如果使用关系型数据库存储这些数据’_是需要提前建 表,二是如果数据存在嵌套关系’还需要进行序列化操作才可以存储’这非常不方便。如果使用非关 系型数据库’就可以避免这些麻烦,更简单`高效。

本节中’我们主要介绍MongoDB存储操作。

MongoDB是由Oˉ+语言编写的非关系型数据库’是—个基于分布式文件存储的开源数据库系统, 其内容的存储形式类似JSON对象°它的字段值可以包含其他文档`数组及文档数组,非常灵活°本 节我们就来看看Python3下MongoDB的存储操作°

』勺‖』□

↑.准备工作

在开始之前,请确保已经安装好了MongoDB并启动了其服务,安装方式可以参考: https://setup. scrape.center/mongodb。

除了安装好MongoDB数据库,我们还需要安装好Python的PyMongo库’如尚未安装,可以使

‖‖司」■■‖』■||■■』■■■■』■|』■可』■(

用pjP3来安装: p1p31∩5ta11pyⅧ∩8o

更详细的安装说明可以参考: https://setupscrape.center/pymongo° 安装好MongoDB数据库和PyMongo库之后,我们便可以开始本节的学习了° 2.连接Mo∩goDB

连接MongoDB时’需要使用PyMongo库里面的‖o∩go〔11e"t方法’_般而言’传人MongoDB的 IP及端口即可°‖o∩gO〔1je∩t方法的第~个参数为地址∩O5t’第二个参数为端口port(如果不传人此 参数’默认取值为270l7): mportpy咖∩8o

c1je∩t≡Py∏|o∩go."o∩go〔11e∩t(∩o5t≡‖1o〔日1∩O5t『 ’ port=27017)

这样就可以创建MongoDB的连接对象了。

另外,还可以直接给‖o∩8o〔11e∩t的第-个参数∩o5t传人MongoDB的连接字符串’它以"o∩godb

d

{ q







|0

4.5MongoDB丈档存储

l45

开头’例如: c1ie∩t=‖o∩go〔1ie∩t(0『∏o∩godb://1o〔a1∩o5t:27017/‖)

这可以达到同样的连接效果。

3.指定数据库

在MongoDB中’可以建立多个数据库’所以我们需要指定操作哪个数据库。这里我们以指定test 数据库为例来说明: db=〔1ie∩t.te5t

这里调用〔11e∩t的te5t属性即可返回test数据库°当然,也可以这样指定: db=〔11e∩t[‖te5t0]

这两种方式是等价的°

4.指定集合

MongoDB的每个数据库又都包含许多集合(collection)’这些集合类似于关系型数据库中的表° 下-步需要指定要操作哪些集合,这里指定-个集合’名称为smdents°与指定数据库类似,指 定集合也有两种方式; 〔o11e〔tio∩=db·5tude∩t5

或 co11ectjo∩≡db[′5tude∩t5』]

这样我们便声明了一个集合对象° 5.插入数据

接下来’便可以插人数据了°在students这个集合中,新建_条学生数据’这条数据以字典形式 表示: 5tude∩t≡ { !id0 ; ‖2O17O101|’ !∩己‖‖e0 : ‖〕oIda∩|’ !age0 8 2O』

!8e∩der! : 0Ⅷa1e‖ }

这里指定了学生的学号`姓名、年龄和性别°然后直接调用Co11eCt1O∩类的i∩5ert方法即可插 人数据’代码如下: re5u1t二co11ectjo∩.1∩5ert(5tude∩t)

prj∩t(re5u1t)

在MongoDB中,每条数据都有一个1d属性作为唯一标识°如果没有显式指明该属性,那么 MongoDB会自动产生_个0bjectId类型的jd属性, 1∩5ert方法会在执行后返回jd值。 运行结果如下: 5932a6861S〔26O6814〔91+3d

当然’也可以同时插人多条数据’只需要以列表形式传递即可,实例如下: 5tude∩t1≡ { 0id0 : ‖2017O1O1‖ ’ 0∩a|∏e‖ : 0]orda∩` ’

|日ge! : 20’

‖ge∩der‖ : |∏a1e! }



{ q

第4章数据的存储

l46

5tude∩t2= { 01d‖ : ‖2O17O2O20 ’ ∩日『∏e| : 0‖jke0 ’ 日ge0 自 21’ ge∩der! : 0们a1e0 }

re5u1t=co11ectjo∩.j∩sert([5tude∩t1’ 5tude∩t3]) prj∩t(Ie5u1t)





[0bje〔tId(‖5932a80115〔26O6己59e8a0480)’0bje〔tId(|S932己8o11S〔2606a59e8aO490)]

实际上,在PyMongo3.x版本中,官方已经不推荐使用1∩5ert方法了°当然’继续使用也没什么 问题。官方推荐使用的是j∩serto∩e和1∩5ertⅦa∩y方法,分别用来插人单条记录和多条记录’实例 代码如下: 5t0de∩t={ |jd|; 020170101‖ ’ ∩a们e0 : |〕orda∩0 」 age|: 2O’ ge∩deI』: 0盯a1e0

.

1



运行结果如下: 〈py咖∩go·re5u1ts.I∩5ertO∩eReSu1tobje〔tatOx1Od68b558〉

||

re5u1t≡〔o11ect1o∩.1∩5erto∩e(5tude∩t) pr1∩t(re5u1t) pI1∩t(Ie5u1t.j∩5erted1d)

·』|{』■■』·‖』■■】||·‖|□‖」』■■】|‖』日{||■‖

返回结果是对应的id的集合:



5932日bO「15〔26O6「0〔1〔+6〔5

(‖‖|

与1∩5ert方法不同,这次返回的是I∩sertO∩eRe5u1t对象’我们可以调用其1∩5erted1d属性获 取1d°

对于j∩5eItⅧa∩y方法,我们可以将数据以列表形式传递’实例代码如下: 5tude∩t1={

age]: 20’ ge∩der|: !∏a1e!

□□`□■

‖jd|: 02O1701010 』 ∩己『∏e0 ; ‖〕Orda∩! 』





∩a‖e0 : 0∩让e′’



age0 8 21’ ge∩der0 : !门a1e|



|‖|叫□日凸■}‖|■■■|』■■Ⅵ‖

5tude∩t2 ={ !jd‖ 目 `2017O202|’

Ie5q1t=〔o11e〔tjo∩.i∩sertˉ爬∩y([5tude∩t1’ 5tude∩t2])

运行结果如下: 〈py|∏o∩go·re5u1t5.I∩5ert脆∩yResu1tobjectatox1o1de己558〉

[0bje〔tId({5932日b十41S〔26O7083d3b2a〔‖)’ Obje〔tId(‖5932ab伺15〔2607o83d3b2ad,)]

该方法返回的是I∩5ert‖a∩yRe5u1t类型的对象’调用1∩5eIted1d5属性可以获取插人数据的jd

‖‖』■|‖」』■|·』■{』日

prj∩t(re5u1t) pr1∩t(Ie5u1t.i∩5ertedjd5〉

列表°



‖|













p



p





p



45MongoDB文档存储

l47

6.查询

插人数据后’我们可以利用伍∩do∩e或十j∩d方法进行查询’用前者查询得到的是单个结果’后 者则会返回一个生成器对象°实例代码如下: re5u1t=〔o11eCtio∩.十i∩d-O∩e({0∩己爬0 : ‘‖让e!}) pr1∩t(type(re5u1t)) pri∩t(re5u1t)

这里我们查询∩aⅧe值为‖让e的数据,运行结果如下: <〔1a5S 0dj〔t!〉

{0-id!:Obje〔tId(|5932a8O115c26O6a59e8a049‖)’ !id|: 02017O2O20 」 ,∩a川e0 : 0"ike0’|age‖: 21’ 0ge∏deI! : 。0Ⅶa1e0}

可以发现’结果是字典类型,它多了1d属性,这就是MongoDB在插人数据过程中自动添加的°

此外,我们也可以根据0bje〔tId来查询数据’此时需要使用bson库里面的object1d: +r咖b5o∩.obje〔t1dj‖port0bje〔tId

re5u1t=〔o11ectio∩.十1∩dˉo∩e({』ˉjd! : Obje〔tId(!593278c115〔26o2667ec6bae′)}) pr1∩t(re501t)

其查询结果依然是字典类型’具体如下:

{|-jd! ;0bje〔tId(|593刀8〔115〔2602667ec6bae|)’‖id′ : ‖2017o1o1』′ ‖∩a们e』: 0〕ord3∩!’‖age』: 2o, 0ge∩der: ,Ⅶa1e0} p

当然’如果查询结果不存在’则会返回‖O∩e°



若要查询多条数据’可以使用千i∩d方法。例如,查找age为20的数据’实例代码如下:





resu1t5二〔o11ectjo∩.十j∩d({0age0 : 2O}) pr1∩t(re5u1t5) 于orresu1tj∩re5u1t5:





p



P





pIi∩t(re501t)

运行结果如下: 〈pγⅧ∩go.〔ur5oI。O」r5orobje〔tat0x1032d5128〉

{‖—jd0 :ObjectId(,593刀8〔115c26o2667e〔6bae!)’|id! : |2o17o1010 ’ )∩3Ⅷe0 : 』〕oIda∩! ’ ‖己ge0 :20’ |ge∩der0 : !帕1e0} {′ˉjd‖:Obje〔tId(!59〕278c815〔2602678bb2b8d,)’`jd『 : 020170102|’ 』∩a"e『 : ,Reγi∩, 」 』a8e|: 2o’ 0ge∩der』: |"a1e|} {|-id|:0bjectId(』593278d815〔260269d7645a8‖)’,id『 : ’2o17o103! ’ |∩aⅦe0 : !}{日rde∩|’ !日ge‖$20’|ge∩der‖: ,|"a1e‖}

返回结果是〔ur5OI类型’相当于一个生成器’通过遍历能够获取所有的结果,其中每个结果都 是字典类型。

如果要查询age大于20的数据’则写法如下: re5u1t5≡〔o11ect1o∩°千i∩d({,age|8 {,$gt! : 20}})





拭里查询条件中的键值已经不是单纯的数字了’而是一个字典,其键名为比较符号$gt’意思是 大于;键值为20°

p

这里将比较符号归纳为表牛3。



表牛3比较符号















P

















$1t

小于

{!a8e『 : {!$1t,: 2o}}

$gt

大于

{|age’: {!$gt0 : 20}}

$1te

小于等于

{,age‖: {!$1te! : 20}}

$gte

大于等于

{0age0 8 {!$gte! : 20}}

$∩e

不等于

{‘a8e! : {0$∩e! : 〗o}}

$j∩

在范围内

{0age0 : {`$1∩‖: [20’ 23]}}

$∩j∩

不在范围内

{,age,; {|$∩j∩‖: [20’ 2〕]}}

‖』



另外’还可以进行正则匹配查询°例如,执行以下代码查询∩日"e以‖为开头的学生数据:

||

』{‖

第4章数据的存储

l48

re5U1t5=CO11e〔tjO∩.于j∩d({`∩驯e‖ ; {0$regeX! : 0^‖.*‖}})

回■]』·‖□■∏{己■∏

这里使用$regex来指定正则匹配’^‖.*代表以‖为开头的正则表达式° 下面将一些功能符号归类为表4ˉ4。 表4ˉ4功能符号 符











实例含义

$IegeX

匹配正则表达式

{!∩a"e0 : {|$regeX0 : 0^".*‖}}

∩aⅧe以‖为开头

$e×i5t5

属性是否存在

{ ‖∩a∏e0 : {『$exi5t5‖: 丁rue}}

存在∩3们e属性

$tγpe

类型判断

{!age‖: {!$tγpe‖ : 0i∩t0}}

age的类型为i∩t

$Ⅶod

数字模操作

{0己ge0 : {‖$‖od0 : [5’ O]}}



{‖$teXt0 : {』$5ear〔h|: !‖j|(e‖}}

$wbere

高级条件查询

{‖$w∩ere‖ : ‖obj.伯∩5-cou∩t=obj.+o11ow5〔ou∩t‖}

‖止e字符串

自身粉丝数等于关注数

‖曰□□·■

文本查询

‖($

$teXt

age模5余0 teXt类型的属性中包含

7.计数

〔ou∩t=Co11e〔tjo∩.+1∩d()·Co‖∩t() pri∩t(〔ou∩t)

〔ou∩t=〔o11e〔tjo∩.「j∩d({0age0 : 2O})。cou∩t() Pri∩t(cou∩t)

≡■■可‖|■■】

统计符合某个条件的数据有多少条’代码如下:

‖(

要统计查询结果包含多少条数据’可以调用COu∩t方法。例如统计所有数据条数,代码如下:

运行结果是_个数值,即符合条件的数据条数。 a排序

q







排序时,直接调用5OIt方法’并传人排序的字段及升降序标志即可°实例代码如下:







re5u1t5=〔o11e〔tjo∩.千i∩d(〉.5oIt(!∩a『∏e0 ’ pyⅧ∩go.∧5〔[‖DI‖6) prj∩t([re5(」1t[|∩a∏`e! ]「orre5u1ti∩re5(」1ts])

·





运行结果如下:







[‖‖己rde∩0 ’ !〕orda∩! ′ ‖倔ev1∩` ’ 0‖3r‖’ ,‖1促e0 ]



9.偏移

γ

这里我们调用pym∩go.∧5〔[‖0I‖C指定按升序排序°如果要降序排,可以传入py咖∩go.0[5〔[‖0I‖C。



在某些情况下’我们可能只想取某几个元素’这时可以利用5k1P方法偏移几个位置,例如偏移2’ 即忽略前两个元素’获取第三个及以后的元素: re5u1t5=co11e〔tjo∩.+i∩d().5ort( ‖∩a‖∏e‖ ’ py∏℃∩go.A5〔[‖0I‖C)·5点jp(2) pIj∩t([re5u1t[‖∩己们e0 ] +orre5(」1tj∩Ie5(」1t5])



运行结果如下:

re5u1t5=〔o11e〔t1o∩.千i∩d()·5ort(』∩a们e0′ pγ‖)o∩go。∧5〔[‖DI‖C).5恨1p(2)。11‖1t(2)

」|司

另外,还可以使用11们1t方法指定要获取的结果个数,实例代码如下:

{|

「 0贝eγj∩‖」刊ark‖’ 0‖让e‖]

pri∩t([re5u1t[ 0∏a们e‖ ] +orre5u1t 1∩re5(』1t5])

运行结果如下:

」■■‖‖‖‖』】■可



T■



【〗■■「·‖卜‖■【『|卜(●■′‖}【■「‖队|伯「[‖}尸「卜卜[■口匹■}●【|‖「任『|‖||『■‖‖【『■■尸||『『卜′》尸》■「■β|■「|■■「‖|||〃■「ˉ■·’■「』『β{‖|[『‖)

45MongoDB丈档存储

l49

[贝eγ1∩‖’ ‖‖ark‖ ]

如果不使用11mt方法加以限制,原本会返回三个结果,而加了限制后’会截取两个结果并返回。 值得注意的是’在数据库中数据量非常庞大的时候(例如千万`亿级别)’最好不要使用大偏移 量来查询数据,因为这样很可能导致内存溢出.此时可以使用类似如下操作来查询: 千ro‖b5o∩.obje〔tidi旧port0bjectId 〔o11ectjo∩.于j∩d({‖ 1d『 : {』$gt| : 0bjectId(‖593278〔815〔26o2678bb2b8d|)}})

这里需要记录好上次查询的jd。

↑0.更新

[』

对于数据更新,我们可以使用update方法,在其中指定更新的条件和更新后的数据即可°例如: 〔o∏d1t1o∩={0∩a雁! : 0贝eγj∩0} 5tude∩t=co11e〔tjo∩°千i∩do∩e(〔o∩d1t1o∩)

5tude∏t[ ‖age0 ] ≡25 re5u1t≡〔o11e〔tjo∩.update(〔o∩djtio∩’ 5tude∩t) pIj∩t(re5u1t)

这里我们更新的是∩aⅧe值为促eγ1∩的学生数据的age:首先指定查询条件,然后将数据查询出来’ 修改其age后调用upd日te方法将原条件和修改后的数据传人。 运行结果如下: {′o促! : 1’ ‖∩‖odi千1ed! : 1’ !∩| : 1’ |updated[xj5ti∩g! : 丁rue}

返回结果是字典形式, o代代表执行成功, ∩‖odj十1ed代表影响的数据条数。

另外’我们可以使用$5et操作符实现数据更新,代码如下: re5u1t二〔o11e〔tjo∩.update(〔o∩djt1o∩』{‖$5et‖ : 5tude∩t})

这样可以只更新5tude∩t字典内存在的字段。如果原先还有其他字段,则既不会更新’也不会删除° 而如果不用$5et,就会把之前的数据全部用5tude∩t字典替换;要是原本存在其他字段’会被删除。

另外’ update其实也是官方不推荐使用的方法°官方推荐使用单独的updateˉo∩e方法和 updateˉ‖a∩y方法来处理单条和多条数据更新过程’它们用法更加严格’第二个参数都需要使用$类 ■厂『■『「|

P

‖}



型操作符作为字典的键名’实例代码如下: co∩djtjo∩≡{′∩a们e‖ : ′贝eγi∩‖} 5tude∩t=〔o11ectio∩.+1∩do∩e(〔o∩ditio∩) 5tude∩t[|a8e‖] ≡26

re5u1t≡co11ect1o∩.update-o∩e(〔o∩djtio∩’{|$5et0 : 5t(』de∩t}) prj∩t(re5u1t) pr1∏t(re5u1tⅫatched〔ou∩t′ re5u1t.加di+1edcou∩t)

匹∩β广叼‖巳伊卜『■

这里调用的是updateˉo∩e方法’其第二个参数不能再直接传人修改后的字典’而是需要使用 {|$5et』: 5tude∩t}这种形式的数据°然后分别调用们at〔hed〔ou∩t和Ⅷod1千1ed〔ou∩t属性’可以获得 匹配的数据条数和影响的数据条数。 运行结果如下: 〈py们o∩go.re5u1t5.0pdateRe5u1tobjectatO×10d17b678〉 10







》||`伊|





可以发现updateˉo∩e方法的返回结果是0pdateRe5u1t类型。我们再看一个例子: 〔o∩djtio∩≡ {0age,: {′$8t|: 20}} re5u1t=co11e〔tjo∩·update—o∩e(〔o∩ditio∩’{|$i∩〔‖ ; {0age0 : 1}}) prj∩t(Ie5u1t)

pr1∩t(re5u1t。们atc‖ed〔ou∩t’ re5u1t.加dj十jed一〔ou∩t)

这里指定查询条件为age大于20,然后更新条件是{|$j∩C|: {』age|: 1}}’也就是对age加l,

l50

第4章数据的存储

因此执行updateˉo∩e方法之后’会对第—条符合查询条件的学生数据的age加l° 运行结果如下: <pγ‖o∩go.reSu1tS°0pdateRe5u1tobje〔tatOx1Ob887』c8〉 11

可以看到匹配条数为l条,影响条数也为l条°

但如果调用updateˉⅧa∩y方法,则会更新所有符合条件的数据’实例代码如下: pri∩t(re5u1t)

prj∩t(re5u1t.们日tC∩edcou∩t’ re5‖1t.『∏odj+iedcou∩t)

〖『■】』‖』□‖

cO∩ditjO∩= {,age』: {!$gt‖ : 2O}} Ie5u1t=co11e〔tio∩。update-Ⅷ己∩y(〔o∩ditio∩》{0$i∩〔』: {0age‖: 1}})

运行结果如下: <py‖∏o∩go.re501t5.0pdateRe5u1tobje〔tat0x1O〔6384〔8〉 3 ]

可以看到’这时匹配条数就不再为l条了,所有匹配到的数据都会被更新°

↑‖.删|除

删除操作比较简单,直接调用reⅧoγe方法并指定删除条件即可,之后符合条件的所有数据均会 被删除°实例代码如下:

d 〈







re5u1t≡co11e〔tio∩·re∏℃γe({0∩己|∏e! : 0Ⅸevj∩0}) pri∩t(Ie5u1t)

运行结果如下: {!O促0 : 1’ 0∩0 : 1}

另外,这里依然存在两个新的官方推荐方法—de1eteo∩e和de1ete-Ⅷa∩y°de1eteo∩e即删除 第一条符合条件的数据’de1ete-"a∩y即删除所有符合条件的数据。实例代码如下: re5u1t≡〔o11e〔tio∩.de1ete-o∩e({0∩a雁0 : 0Reγj∩!}) prj∩t(Ie5u1t) prj∩t〈Ie5u1t.de1eted〔ou∩t) re5u1t=co11e〔t1o∩.de1eteˉ阳∩y({0age‖: {0$1t0 : 25}}) pri∩t(Iesl」1t。de1eted〔ou∩t)

运行结果如下: 〈py∏mgo。re501t5.De1eteRe5u1tobje〔tatOx10e6ba4〔8〉 1

4

两个方法的返回结果都是De1eteRe5u1t类型’可以调用de1eted〔ou∩t属性获取删除的数据条数° ↑2ˉ其他操作









||







」 ■q





除了以上操作,PyMongo还提供了—些组合方法,例如十1∩do∩ea∩dde1ete、十j∩d-o∩Qa∩dˉrep1ace 和十1∩d—o∩eˉa∩dˉupdate’分别是查找后删除`替换和更新操作,用法与上述方法基本-致。

q

q

另外’还可以对索引进行操作,相关方法有〔reatei∩dex、createi∩dexe5和drop-1∩dex等° ↑a总结

本节讲解了使用PyMongo操作MongoDB进行数据增删改查的方法,后面我们会在实战案例中应

{ q





用这些操作完成数据存储°

■□‖』‖‖□■】Ⅵ」‖|·』∏|』■□】

本节代码参见: https://gjthuhcom/Python3WebSpider/MongoDB爬st°



「 p

46

Redis缓存存储

l5l

46 ∩ed|s缓存存储



Redis是_个基于内存的`高效的键值型非关系型数据库,存取效率极高’而且支持多种数据存



储结构,使用起来也非常简单。本节我们就来介绍_下Python的Redis操作’主要介绍redisˉpy这个 库的用法°





广



||

■b

↑.准备工作

在开始之前’请确保已经安装好了Redjs并能正常运行,安装方式可以参考:ht印s:〃setupscrape.centeⅣ redis。

除了安装好Redis数据库外’我们还需要安装好redisˉpy库’即用来操作Redis的Python包,可 以使用pjp3来安装:

p

p1P3 1∩5ta11redj5



更详细的安装说明可以参考: https://sempscrape.centcⅣredisˉpy。

『 p









安装好Redis数据库和redisˉpy库之后’我们便可以开始本节的学习了° 2Red‖s和St『‖ctRed‖s

redjsˉpy库提供Redj5和5tr1ctRedj5两个类,用来实现Redjs命令对应的操作° 5tI1〔tRed15类实现了绝大部分官方的Redis命令’参数也一-对应’例如5et方法就对应Redis命

令的5et方法。而Red15类是5tr1〔tRedj5类的子类,其主要功能是向后兼容旧版本库里的几个方法° 为了实现兼容,Redi5类对方法做了改写’例如将1reⅦ方法中va1ue和∩uⅧ参数的位置互换’这和Redis



命令行的命令参数是不—致的°

) b







b

} b

官方推荐使用5tr1CtRedj5类,所以本节我们也用5tr1CtRed15类的相关方法作为演示。

3.连接Red‖s ‘

我们先在本地安装好Redjs,并运行在6379端口,将密码设置为fbobared°可以用如下实例连接 Redis并测试: +I咖redi5mmrt5tIi〔tRedj5

redj5=5trjctRedi5(host≡!1o〔己1}》o5t,’ port=6379’ db=o’ pa55"o【d=』「oob己red) Iedis.5et(|∩a赃,’ !8ob0) prj∩t(rediS.get(!∩a爬|))



p

P









p

h

} b

这里我们传人了RedjS的地址、运行端口、使用的数据库和密码信息。在默认不传数据的情况下, 这4个参数分别为localhost、6379、0和None。然后声明了一个5trj〔tRed15对象,并调用对象的5et() 方法’设置了_个键值对°最后调用get方法获取了设置的键值并打印出来° 运行结果如下: b08ob0

这说明我们成功连接了Rcdls,并且可以执行5et和get操作了°

当然’还可以使用〔o∩∩e〔t1o∩poo1来连接Rcdis,实例代码如下: 十Imredj5j『∏portStri〔tRedi5’〔o∩∩e〔tjo∏poo1

poo1=〔o∩∩e〔tjo∩poo1(ho5t=01o〔己1∩o5t‖’ port≡6379,db=O’ pa55咖rd=干oob日red) redis=5trj〔tRed15(〔o∩∩ectjo∩ˉpoo1=poo1)

这样的连接效果是一样的°观察源码可以发现, 5trjct日edjs内其实就是用host和port等参数又 构造了_个〔o∩∩e〔tjo∩Poo1’所以直接将〔o∩∩ectio∩poo1当作参数传给5trj〔tRedj5也一样。

p

p







第4章数据的存储

l52

另外,〔o∩∩e〔tjo∩poo1还支持通过URL来构建连接。URL支持的格式有如下3种: redis://[:pa55wom]助o5t:port/db redi55://[:p己55"ord]0ho5t:port/db u∩ix://[:pa55"ord]0/path/to/5o〔促et.5oc仪Mb≡db

这3种URL分别表示创建RedisTCP连接`RedisTCP+SSL连接、RedisUNIXsocket连接°我们 只需要构造其中任意_种即可’其中pa55"ord部分如果有则可以写上’如果没有也可以省略°下面再 用URL连接演示_下: uI1= ‖red15://:十oobared@1oc己1ho5t:6379/0!

pOO1=〔O∩∩e〔tjO∩pOO1.千rO川ur1(ur1) redj5=5trictRedis(〔o∩∩e〔tjo∩ˉpoo1≡poo1)

4键操作

■】Ⅲ■』■□』■】·‖可』□■】勺‖‖|

这里我们使用的是第一种格式°首先,声明_个Redis连接字符串’然后调用千ro"0r1方法创建 〔o∩∩e〔tjo∩poo1,接着将其传给5tr1〔tRed15即可完成连接,所以使用URL的连接方式还是比较方便的。

表4ˉ5总结了键的一些判断和操作方法° 0

表4ˉ5键的-些判断和操作方法 作













×

·[





□]

(∩





参数说明



redj5.exi5t5(‖∩a"e0 )

是否存在∩a们e这丁rue 个键

de1ete(∩a们e)

删除一个键

∩a们e:键名

redj5.de1ete(0∩aⅧe‖)

type(∩3们e)

判断键类型

∩a∩‖e:键名

redi5.tγpe(』∩日Ⅶe0 )

删除∩a"e这个键



(|

判断一个键是否存∩aⅦe:键名 在

实例结果

实例说明



q

1

判断∩aⅦe这个键b|5tri∩g‖

((

的类型

获取所有符合规则p日tter∩:匹redjskeγ5( 0∩*』)

促eγ5(patter∩)

的键

获取随机的_个键

re∩aⅧe(5r〔’ d5t)

对键重命名

m∩do毗ey()





‖□ □α



°]

) (

5r〔:原键名Iedj5.re"己爬(|∩a"e|′

将∩a∏e重命名为丁rue ∩j〔促∩a|∏e

db5jZe()

获取当前数据库中









十≈

■]









□□

×

(∩





睁】

tt1(∩a爬)

『mγe(∩己"e’ db)

获取当前数据库中100 键的数目

设定键的过期时∩a"e:键名

redj5。exp1re(』∩a爬0 」 2)将∩a|∏e键的过期丫rue 时间设置为2秒

tme:秒数

获取键的过期时∩日爬:键名 间’单位为秒

redi5·tt1(|∩a帐‖)

将键移动到其他数∩a们e;键名

‖oγe(0∩日"e0 ’ 2)

获取∩a爬这个键1(l表示永 久不过期)

删除当前所选数据

db:以往的

将∩a眶键移动到2丁rue 号数据库

+1u5Mb()

库中的所有键 千1u5ha11()

删除所有数据库中

q

的过期时间

数据库代号 +1u5Mb()



删除当前所选数据「rue 库中的所有键

+1u5ha11()

的所有键

5.字符串操作







』|

Redis支持最基本的键值对存储’相关方法的总结如表4ˉ6所示。

删除所有数据库中丁rue 的所有键

』·‖』■■〗』■■Ⅵ』·』』■■】‖‖‖‖』□

据库

获取随机的_个键b0∩日‖e‖

d5t:新键名 0∩1〔k∩日爬』) 键的数目

间’单位为秒

获取所有以∩为开[b‖∩己|∏e‖] 头的键

‖』·■·‖‖‖|·‖|』■]』■‖(」■■|‖‖』■】‖

ra∩do毗ey()

配规则

■■‖|』】■■〗‖■■■■□■



丁■巴■〗■∏『■■□】■『■【【∏【广「》[巴『‖□[β「「■厂‖「匹尸|仁|■‖卜■■「》|■「》‖}[■‖卜■厂卜||)●「卜‖}■■尸‖|■■′卜|ββ‖『|■【「|{仅β|『|广「|[■「|卜‖■「|血■■『|[■【『‖□■■

46

Redis缓存存储

l53

表4ˉ6键值对存储的相关方法 方



5et(∩a"e》 va1ue)





参数说明





实例说明

实例结果

将数据库中指定键∩a们e:键名

red15.5et(‖∩aⅧe‖’

将∩a爬这个键对应丁rue

名对应的键值赋值γa1ue:值

08Ob』)

的键值赋值为8ob

redj5.get(‖∩3Ⅷe0)

返回∩己Ⅷe这个键b′Bob0

为字符串Va1ue

get(∩a‖e)

返回数据库中指定∩己‖e:键名

对应的键值

键名对应的键值

get5et(∩a们e’ va1ue)

将数据库中指定键∩a川e:键名

redi5。get5et(0∩aⅧe‖’

名对应的键值赋值γa1ue:新值 0‖止e0)

应的键值赋值为



‖让e,并返回上次

为字符串γ31l」e,并

返回上次的Va1ue

Ⅷget(|(ey5’ *arg5)

将∩ame这个键对b08ob!

的γ己1ue

返回由多个键名对促ey5:键名 应的γa1ue组成的序列

redi5.吧et([‖∩a"e‖’ ,∩i〔|〈门a|∏e‖ ])

返回「口肥和∩1C灯H肥[b0‖止e‖’ 对应的va1眶 b』‖业er0 ]

列表

如果不存在指定的∩a|『|e:键名

red15·5et∩x(|∩eW∩aⅧe‖’如果不存在∩eW∩a爬第一次的运

键值对,则更新 γa1Ue,否则保持不 变

|〕己|ne5『)

5eteX(∩日∏e’ t加e’

设置键名对应的键∩a们e:键名

redj5.5etex(0∩a"e‖’ 1’将∩己!∏e这个键的丁rue

γa1ue)

值为字符串类型的t1"e:有效

‖〕己爬5』)

5et∩X(∩a川e’γa1ue)

这个键名,则设置行结果是 相应键值对’对应丁rue,第二 键值为〕日‖e5 次的运行结 果是「a15e

值设置为〕aⅦe5,有 效期设置为l秒

va1ue,并指定此键期v31ue:值 值的有效期

5etra∩ge(∩a"e’ o仟5et」设置指定键名对应∩a们e:键名

v己1ue)

redj5.5et(‖∩aⅧe0 ’

的键值的子字符串O仟5et:偏移|‖e11O!)

将∩aⅧe这个键对11’修改后

应的键值赋值为的字符串长

量va10e:子red15。5etr己∩ge ‖e11o,并在该键值度 字符串 (|∩己"e《 ’ 6’ |"or1d{) 中j∩dex为6的位 置补充‖or1d

"5et("appi∩g)

|『‖5et∩)《(阳pp1∩g)

批量赋值

将∩a"e1赋值为『rue

典或关键字

,DUr己∩t0 ’ ‖∩a"e2‖ :

0um∩t, ∩aⅧe2赋值

参数

‖]a|∏e5‖})

为〕aⅧeS

指定键名均不存在Ⅷappi∩g:字redj5."5et∩x({,∩a耐e〕|:在∩a们e3和∩a们e4『rl」e 时,才批量赋值

1∩cr(∩aⅧe’ a∏)ou∩↑=1)

∏appj∩g:字redj5.川5et({|∩a"e1|:

典或关键字

05Ⅶjt们‖ 》 ‖∩a们e4! :

均不存在的情况

参数

0〔urrγ|})

下,才为二者赋值

对指定键名对应的"a们e:键名 redj5.i∩cr(0age! ’ 1) 将age对应的键值1,即修改后 键值做增值操作’ 己|∏Ou∩t:增加 默认增1°如果指的值 定键名不存在,则 创建_个,并将键

增加1°如果不存的值 在age这个键名, 则创建_个,并设 置键值为1

值设为a咖u∩t

de〔r(∩a们e′ a"ou∩t≡1)

对指定键名对应的∩a"e:键名 Ied15°decr(0age‖」 1) 将age对应的键值1,即修改后 键值做减值操作, a|])Ou∩t:减少 默认减1°如果键的值 不存在,则创建_

减1°如果不存在的值 日ge这个键名,则 创建一个’并设置 键值为1

个,并将键值设置 为amu∩t

appe∩d(促e)/’ γa1ue)

对指定键名对应的|(ey:键名 键值附加字符串 Va1ue

red15.appe∩d( 0∩i〔促∩3"e在键名∩jc长∩a‖e对13,即修改 |’ ,O促』)

应的键值后面追加后的字符串 字符串0N

长度

』■■□■■

(续) 方







参数说明

实例说明



返回指定键名对应∩日"e:键名 redj5.5ub5tr(`∩a们e,’ 的键值的子字符串 5tart:起始1’4)

实例结果

返回键名∩a们e对b0e11o0 应的键值的子字符

索引

串,截取键值字符

e∩d:终止索 引,默认为

串中索引为1~4的 字符

□|

5ub5tr(∩a们e’ 5tart’ e∩d=ˉ1)



· | √‖`|‖

第4章数据的存储

l54









1,表示截取

q

到末尾



获取指定键名对应促ey:键名 redi5.getra∩ge(0∩a们e,’返回键名∩a们e对b0e11o』 的键值中从5taIt 5t日rt:起始1’4) 应的键值的子字符 串·截取键值字符

到e∏d位置的子字索引

串中索引为1~4的 字符

6.列表操作

Redis提供了列表存储’列表内的元素可以重复,而且可以从两端存储’操作列表的方法见表牛7° 表4ˉ7列表的操作方法 方



rpu5b(∩a们e’ *γa1ue5)







参数说明

实例结果

实例说明



在键名为∩a眠的∩a们e:键名

redj5。rpush(,1i5t』’ 1’向键名为1j5t的3,即列表大

列表末尾添加值为v己1仙e5:值

2’ 3)

γa1ue的元素,可以

列表尾添加1、2、 小 3

传人多个γ己1ue

1pu5h(∩a刷e’ *γa1l』e5)

在键名为∩a爬的∩3爬:键名

Iedj5.1pu5∩(』1iSt,’O)向键名为1i5t的0,即列表大

列表头部添加值为v31ue5:值

列表头部添加0



γa1ue的元素,可以

传人多个va1ue

返回键名为∩a爬∩a爬:键名 的列表的长度

redj5。11e∩(|1iSt‖)

1ra∩ge(∩a爬’ 5tart’

返回键名为∩己贬∩己爬:键名

rediS。1m∏ge(`1j5t‖’

e∩d)

的列表中索引从5tart:起始1’3)

11e「](∩己爬)

返回键名为1jSt q 的列表的长度

返回索引从1到3 [b!〕|’ 对应的列表元素

b42‖ ’ b‖1,]

5tart到e∩d之间索弓| 的元素

e∩d:终止索 引

0

保留键名为115t 「rue

截取键名为∩己眶∩a怔:键名

e∩d)

的列表,保留索引St日rt:起始

的列表中索引从1

从Start到e∩d之索引

到3之间的元素

1tri川(』1jst|’ 1′ 3)

』□γ‖(』■■

1trjⅦ(∩a爬’ Start’

间的元素

·□」‖`■■〗‖{■■】·|□■】〗■■】‖闯‖‖|口■叮‖□□■□可』■■』■■』■■‖勺■■·乌巴司日■■■|(|』■■

e∩d:终止索 引

符串

■■(□■口

getra∩ge(低eγ’ 5tart’ e∩d)

e∩d:终止索 引

返回键名为∩a爬∩a眶:键名

redj5.1j∩dex(‖1j5t! ’

返回键名为1j5t b,2,

的列表中i∏dex位i∩dex:索引1)

的列表中索引为1

置的元素

的元素

15et(∩a爬’ j∩deX’

给键名为∩a贬的∩a贬:键名

γa10e)

列表中i∩dex位置i∩dex:索引5) 的元素赋值,如果位Ⅲ

j∩dex越界就报错va1ue:值

redi5.15et(!1j5t』’ 1’将键名为1j5t的『Iue

列表中索引为l的 位置赋值为5

■■‖』□‖‖』■Ⅶ』|‖‖』■山■‖■■■■‖『‖□■】』】■■

1j∩dex(∩a爬’ j∩dex)





}卜 任『■尸

46Redis缓存存储

l55

》‖■巴■厂β■■

(续) 方







参数说明



实例说明

实例结果

redi5。1re们(‖1i5t‖’ 2’删除键名为1i5t 2,即删除的

1re们(∩a"e’〔ou∩t’

删除键名为∩a爬∩3′∏e:键名

γ日1ue)

的列表中〔oU∩t个Cou∩t:删除3) 值为γa1ue的元素个数





的列表中的两个3 个数

Va1ue:值 ■■厂卜■■■■■尸

1pOp(∩a爬)

返回并删除键名为∩a∏e:键名



返回并删除键名为b!5! 1iSt的列表中的第

元素

一个元素

返回并删除键名为∩a们e:键名

rpOp(∩a‖e)

redi5.1POP(|1j5t0)

∩a"e的列表中的首

Iedj5.rPOP(‖1j5t|)

返回并删除键名为b‖2‖

∩a"e的列表中的尾

1j5t的列表中的最

元素

后-个元素

巳■『■■■尸β■■

b1pOp(key5’tj爬OUt≡0)返回并删除指定键key5:键名

redi5.b1POp(‖115t‖)

返回并删除键名为[b05‖]

名对应的列表中的序列

1i5t的列表中的第

首个元素°如果列tmeout:超

—个元素

表为空,则-直阻时等待时

司∏

塞等待

间’0表示_

■尸〖β|卜「巴『『■尸}》尸

直等待

brpOp(keγ5’ ti账OUt≡O)返回并删除键名为促ey5:键名 redi5.brpOp(|1i5t‖〉

返回并删除间名为[b‖2‖]

∩a‖e的列表中的尾序列

1i5t的列表中的最

元素。如果列表为tmeOut;超 空’则-直阻塞等时等待时

后_个元素



间’O表示一 直等待

■厂卜卜|

rpOp1pU5h(5I〔’ d5t)

返回并删除键名为5IC:源列表redj5.rPOP1P05h(‖115t 删除键名为1i5t b02』 SI〔的列表中的尾的键名



■■『〖β●■‖止■「■巳■=卜■厂‖■尸‖■■「

的列表的头部

的列表的头部’然

7.集合操作

Redis还提供了集合存储,集合中的元素都是不重复的,操作集合的方法见表4ˉ8° 表4ˉ8集合的操作方法 方







参数说明

向键名为∩己Ⅷe的∩a川e:键名

卜巴尸

集合中添加元素





redi5.5add(|t己g50 ’

实例说明

实例结果

向键名为t日g5的 3,即添加的

「}

va1ueS:值, 0Boo低0 ’ |丁ea‖’

集合中添加BOo恨、 数据个数

可以为多个

丁ea和〔O仟ee这3

0〔o仟ee|)

项内容

5re‖(∩a↑∏e’ *va1ue5〉 ‖●■■■■■尸

0

个元素’并将其添 加到键名为115t2 后返回

5add(∩3‖e’ *γ日1ue5)



的列表中的最后_

! ’ |11St2|)

元素’并将该元素d5t: 目标列 添加到键名为d5t表的键名

从键名为∩a∏e的 ∩a爬:键名 redj5.5re"(‖t己g5‖’ γ日1ue5:值, ,8ook|) 集合中删除元素

从键名为tag5的 1,即删除的 集合中删除8OO促

数据个数

可以为多个

5pOp(∩aⅦe)

随机返回并删除键 ∩aⅦe:键名

redj5。5pop(!tag50)

随机删除并返回键b‖丁ea‖

名为∩日∏e的集合

名为ta85的集合

中的_个元素

中的某元素



(续) 作





参数说明



5mγe(5rc′ d5t’ γa1ue)从键名为5rc的集src:源集合redi5.5硒ve(‖ta85‖’ 合中移除v己1ue,并dst: 目标集 0t日g520 ’ ‖〔o仟ee‖)

加到键名为ta852

redj5.S〔ard(‖t己g5{)

获取键名为tag5

的集合中的元素个

的集合中的元素个





3

□~

丁▲

丁‖

」 (

5i5∩e肌ber(∩a|∏e’ va1ue)测试刚e们beI是否∩a"e:键值redi5.51S∏记|∏ber(0tag50 判断8ook是否是 是键名为∩a们e的 ’ 08oo代』) 键名为tag5的集

5i∩ter(|《ey5’ *ar85〉

丁Iue

的集合中



返回键名为∩a|∩e ∩a‖e:键名

从键名为t己g5的 集合中删除元素 〔o仟ee’井将其添

将其添加到d5t对合 应的集合中 γ己1ue:元素

5〔己m(∩a"e)

实例结果

实例说明



集合中的元素

合中的元素

返回所有给定键名促eγ5:键名 red15.5i∩ter([ 0tag5, ’ 的集合的交集 序列 |tag52』])

返回键名为tag5 {b|〔o仟ee0} 的集合和键名为 tag52的集合的交 集

51∩ter5tore(de5t′ 代ey5’*arg5)

求多个集合的交代ey5:键名 redi5°51∩ter5tore 集,并将交集保存序列 (‖1∩tt己8‖’ [ ‖tag50 ’ 到键名为de5t的de5t:结果 0t日g52‖]〉

求键名为tag5的 集合利键名为 tag52的集合的交

集合

集,并将其保存为

集合

1

键名是1∩tta8的

5u∩jo∩(促ey5’ *己I85)

返回所有给定键名长ey5:键名 red1s,5u∩jo∩([ 0t己g5|’ 的集合的并集 序列 |tag52』])

返回键名为tag5的 {b0〔o仟ee0 ’ 集合和键名为tag52 b0Book0 ’ 的集合的并集

5U∩jO∩5tOre(de5t’ key5′ *arg5)

求多个集合的并促ey5:键名 Iedj5.50∩iO∩5tOre 集,并将并集保存序列 (|j∩ttag‖’ [ 0tag5‖ ’ 到键名为de5t的de5t:结果 ′ta8520 ]) 集合

求键名为t日g5的集 合和键名为tag52 的集合的并集,并

b0pe∩』} 〕

将其保存为键名是

集合

j∩tt己g的集合

5di仟(促ey5’*己rg5〉

返回所有给定键名key5:键名 的集合的差集 序列

redjs.5d1仟([ 0tag5‖ ’ 0tag52‖ ]〉

返回键名为t己g5的 集合和键名为tagS2

{b|8oo代0 ’ b0pe∩!}

的集合的差集

5d1仟5tore(de5t’ 促ey5’求多个集合的差低eγ5:键名 redj5.5dj仟store *arg5) 集,并将差集保存序列 (!j∩ttag{ ’ [ 』t己g5』’ 到键名为de5t的de5t:结果 |tag52』]) 集合

集合

求键名为tag5的

3

集合和键名为 tag52的集合的差 集,并将其保存为

键名是1∩ttag的 集合

5爬们ber5(∩a们e)

返回键名为∩己们e ∩ame:键名 的集合中的所有元 素

5m∩dⅧeⅦber(∩a|∏e)

随机返回键名为∩己刚e;键值 ∩a阳e的集合中的个元素’但不删除

redj5°5『∏e阳ber5(,tag5‖)返回键名为t己g5

{b,Pe∩0 」

的集合中的所有元 b‖8oo促『 》 素 b0〔o仟ee|}

red15.5ra∩d们e们ber(‖tag随机返回键名为 5|) t3g5的集合中的—

5ra∩dⅦeⅧber

■■■■]|』司叼|』■■〗』|□‖●‖|』■]」々|』■】』■‖|‖■■‖·』』■Ⅷ‖√‖|」■|』·]」■‖‖司■■」』■■■Ⅷ】]·|■]■〗‖●■■】】‖』勺□■■〗∏‖』■·□‖』〗】■」

集合

■可||司|=■‖』』■]‖』■]‖』勺‖‖乙■】】·‖』■司‖`』■司□|‖|■‖司‖』■】·‖‖■■Ⅷ□■■■·||」■可·『{』■∏●||■■·‖||』■·』·■



】‖‖』■■‖□●二■习‖|』‖‖·】‖‖』Ⅵ』■烟|‖勺|司‖|■

第4章数据的存储

l56

(∩a川e)

个元素

该元素

‖{

「卜

4.6

β[■厂『□■尸|‖|■尸





有序集合比集合多了_个分数字段’利用该字段可以对集合中的数据进行排序,操作有序集合的 方法总结见表4ˉ9° 表4ˉ9有序集合的操作方法 方



zadd(∩己∏|e′ 己rg5’

p

0

p





l57

8.有序集合操作

*促war85)



Redis缓存存储





参数说明





实例结果

实例说明

向键名为∩a"e的∩a‖|e:键名 redi5·Z日dd(0grade‖’ 向键名为gmde的2,即添加的 有序集合中添加元arg5:可变参1Oo′|Bob0 』 98’ ‖‖jke0)有序集合中添加元素个数 素·5〔ore字段用于数

8ob(对应5core为

排序’如果该元素 存在’则更新各元

100)、‖j长e(对应 5core为98)



素的顺序

zreⅧ(∩a‖e’ *v己1ue5)

删除键名为∏a爬的∩a|∏e:键名 redj5.zIe川(『grade‖’ 有序集合中的元素va1ue5:元素|‖让e0〉

z1∩crbγ(∩a们e’ γa1ue’ aⅦou∩t=1)

如果键名为∩a‖记的∩己"e:键名 有序集合中已经存va1ue:元素

redj5.z1∩〔rbγ(‖grade|’将键名为8rade的98.0,即修 08ob0 ’ ˉ2) 有序集合中8ob元改后的值

在元素v日1ue,则将己"ou∩t:增长

p

从键名为匪3de的有l ’即删除的 序集合中删除‖i|(e 元素个数

素的5〔oIe减2



该元素的5〔ore增的5〔ore值

P

加amu∩t;否则向该



集合中添加va1ue

元素,其5〔o【e的值 为日卯‖∩t

},



Zra∏|〈(∩a川e’ va1Ue)



redjS.Zra∩|〈(0gI己de|’ ,枷y|)

得到键名为gmde l 的有序集合中咖y 的排名

元素的排名,或名值



次(对各元素按照 5COre从小到大排

Ⅲ广|卜〖β『

p

返回键名为∩a爬的∩a∏!e:键名 有序集合中va1ueγa1ue:元素

序)

zreγm∩促(∩a"e’γa1ue) 返回键为∩a‖e的∩aⅧe:键名 redi5.zreγra∩促 有序集合中γa1ueγ日1ue:元素 (『grade,’ |刚y』)

『‖‖■

元素的倒数排名, 值

得到键名为grade2 的有序集合中∧"y 的倒数排名

或名次(对各元素 按照5Core从大到

司■■■■

小排序)

江evra∩ge(∩aⅧe’ 5taIt’返回键名为∩a|‖e ∩a们e:键名 e∩d’ wit∩5CoIe5=

「a15e)

b,〕aⅧe50]

但■【[■■「卜■■尸△■「

素(按照5COIe从引 大到小排序) with5core5: 是否带5〔ore

Ⅲra∩geby5〔o工e(∩a"e’ 返回键名为∩a雁的∩a∏e:键名 们1∩’‖日x’ 5tart=‖o∩e’ 有序集合中5〔orem∩:最低 ∩u‖=‖O∩e’

"jth5Core5=「日15e)

在给定区间的元素5COre

阳ax:最高 5COre

5t日rt:起始

}「



返回键名为grade [b08ob0 ’

的有序集合中的前b|例让e|’ 四名元素 b!川y|’

e∩d之间的所有元e∩d:结束索

■厂◆■尸



redj5·Zrevra∩ge

的有序集合中名次5tart:开始 (`gmde‖’ 0’ 3) 索引从Start到索引

索引 ∩u‖:个数 训jt∩S〔Ore5:

是否带5COre

red15.zIa∩gebγ5core

(|gr己de,’ 80’ 95)

返回键名为gIade [b‖Bob|’

的有序集合中5core b,‖i|《e『’ 在8O和95之间的b!A∏]y|」

元素

b|〕日们e5|]



■■纠司|■■]‖|■〗』司

第4章数据的存储

l58









有序集合中5coreⅧi∩:最低

redi5.z〔o0∩t(0gr己de,」

返回键名为gr己de4

80’ 95)

的有序集合中5core

元素个数

川ax:最高

获取键名为gmde 3

的有序集合中的元

的有序集合中元素

素个数

的个数

Ⅲre们ra∩gebyr日∩代(∩a『∏e’删除键名为∩a∏e ∩a『∏e;键名 Iedi5.Zre川ra∩gebym∩代 m∩’ 爪己×) 的有序集合中排名m∩:最低名 (0grade! ’ o’ 0) 在给定区间的元素次

删除键名为gr己de 1,即删除的 的有序集合中排名元素个数 第-的元素

Ⅶax:最高名 次

zrema∩geby5〔oIe (∩a↑∏e’ 们1∩′ 们ax)

删除键名为∩a爬的∩a川e:键名 有序集合中5〔orem∩:最低 在给定区间的元素

redi5·zre∏ra∩geby5coIe 删除键名为grade 1,即删除的

(‖grade! ’ 8O’ 9O)

的有序集合中5〔ore元素个数

5core

在80和90之间的

川ax:最高

元素

5COre

9.散列操作

Redis还提供了散列表这种数据结构’我们可以用∩a‖e指定一个散列表的名称’表内存储着多个 键值对’操作散列表的方法总结见表4-l0°

■甲司□■司■■·』勺■『‖|口■·■■】』=■司‖叫■司』』■■勺■■■□』■勺

·□





己 Ⅲ

□□

∩已

















·] 』α

返回键名为∩a∏记∩a∏‖e:键名



S〔Ore

江aId(∩a「∏e)



在80和95之间的

在给定区间的元素5core 数量

实例结果

日{‖

z〔ou∩t(∩a川e’m∩’|∏ax)返回键名为∩a爬的∩a刚e:键名

实例说明





参数说明

刘‖|■二■∏

(续)

表4ˉ‖0散列表的操作方法 法





参数说明





实例说明

实例结果

向键名为pr1〔e的1,即添加的 关系, C3ke的值为



5

Va1ue:映射 键值

h5et∩X(∩a|∏e’ 代ey’ va1ue)

如果键名为∩a爬∩a们e:散列表 ‖Set∩X(0priCe‖’ 的散列表中不存在键名 boo|(‖’ 6)

给定映射’则向其恨ey:映射键 中添加此映射



向键名为prjCe的1’即添加的 散列表中添加映射映射个数

关系, boo代的值为 6

(‖

γa1ue:映射 键值

hget(∩aⅧe’ key)

获取键名为pI1Ce 5 的散列表中键名 C冰e的值

返回键名为∩a|∏e∩a爬:散列表 redi5」]Ⅶ8et(|pr1〔e|, 的散列表中各个键键名 [|app1e|’ ‖Ora∩ge‖ ])

名对应的值

key5:键名序 列

‖‖|■■

返回键名为∩a爬∩a们e:散列表 redjs.hget(!prj〔e|’

的散列表中长ey对键名 0Ca促e0) 应的值 促ey:映射键 名

h吧et(∩a川e’ 促ey5’ *arg5)

闰‖乙●叮‖■司∏■■‖』‖■可‖■佃

散列表中添加映射映射个数

代ey:映射键

』■{叫□■



h5et(∩a们e’ 椭ey′γ己1oe)向键名为∩a∏e的∩a们e:散列表 ∏Set(!priCe0 ’ 0〔冰e‖’ 散列表中添加映射键名 5)



获取键名为pr1Ce [b0〕! ′ 的散列表中app1e b‖70] 和or己∩ge对应的值

■可||‖|●■‖|■Ⅵ』□■



47 Elasticsearch搜索引擎存储

l59

(续) 方





h‖5et(∩己∏论′ 们己ppj∩g)



参数说明





实例结果

实例说明

向键名为∩aⅧe的∩a‖∏e:散列表Iedj5.h∏5et(‖Prj〔e0 ’

向键名为pri〔e的「Iue

散列表中批量添加键名 {′b3∩己∩a‖ : 2’ 0pear‖ : 映射 "appi∩g:映6})

散列表中批量添加 映射

射字典

h1∩〔rby(∩己爪e’ 代ey’ a们Ou∩t=1)

将键名为∩aⅧe的∩a们e:散列表redi5.∩j∩Crby(‖prj〔e‖’将键名为pr1ce的6,修改后的 散列表中的映射键键名 0己pp1e0 ’ 3) 散列表中app1e的值 值增加a帅u∩t

代ey:映射键

键值增加3





q

a阳Ou∩t:增长

返回键名为∩a"e ∩己爪e:散列表redi5。hexi5t5(‖prj〔e`′返回键名为pri〔e丁rue 的散列表中是否存键名 `b己∩a∩日|) 在键名为促ey的映key:映射键

的散列表中是否存 在键名为ba∩a∩a



的映射



在键名为∩a『‖e的∩日们e:散列表Iedj5.门de1(!price0 ’ 散列表中,删除具键名 !ba∩a∩a!)

从键名为price的『rue 散列表中’删除键

有给定键名的映射代ey5:映射键

名为ba∩a∩a的映

名序列 h1e∩(∩日『∏e)

b代ey5(∩a∏)e)









γ





巳 们 日

口[



(∩

× ·

广≈弓

■■■凸

日∩

[■■■‖||卜‖』||{■【|【尸卜「但尸『 0

∩de1(∩a爬′ *代ey5)





获取键名为∩a∏e∩a∏e:散列表Ied1s.‖1e∩(0pr1〔e‖)

获取键名为pIj〔e 6

的散列表中有多少键名

的散列表中映射的

个映射

个数

协卜■「′【★『匹■■‖匹尸「‖「坚■厂[■「■厂|伊■凸『止伊『‖■尸》■「‖|巳■■「|■■「||『■尸|位厂|世尸

获取键名为∩a∩e ∩a川e:散列表redi5·h促ey5(|prjce|)

获取键名为pr1〔e [b|〔冰e‖’

的散列表中的所有键名 映射键名

的散列表中的所有bboo代|’ 映射键名

bba∩a∩a‖ ’ b0Pear0 ]

hva15(∩a"e)

h8eta11(∩己Ⅶe)

获取键名为∩己爬∩a们e:散列表redjs.∩γa15(0pri〔e0) 的散列表中的所有键名 映射键值

获取键名为prj〔e [b』5|’ 的散列表中的所有b|60 》 b02』’ 映射键值 b|6|]

获取键名为∩a爬∩a川e:散列表redi5.hgeta11(0pr1ce‖)获取键名为prjce {b|〔a促e‖ : 的散列表中的所有键名 映射键值对

的散列表中的所有b‖5《 ’ 映射键值对 b0boo代0 : b060 》

b0ora∩ge : b070 ’

b!Pe己r0 :

b|6!}

↑O总结

鉴于Redis的便捷性和高效性,后面我们会利用Redis实现很多架构’例如维护代理池`账号池` ADSL拨号代理池、ScrapyˉRedis分布式架构等,所以需要好好掌握针对Redis的操作。

47 巨|ast|csea「c∩搜索引擎存储 P

■尸‖|‖■庐「‖‖‖』【=■「|●■【■「

想查数据’就免不了搜索,而搜索离不开搜索引擎°百度`谷歌都是非常庞大、复杂的搜索引擎’ 它们几乎能够索引互联网上开放的所有网页和数据°然而对于我们自己的业务数据来说’没必要使用

l60

第4章数据的存储

这么复杂的技术°如果为了便于存储和检索’想要实现自己的搜索引擎’那么Elasticsearch就是不二 之选°这是—个全文搜索引擎’可以快速存储、搜索和分析海量数据°

0

所以’如果我们将爬取到的数据存储到Elasticsearch里面,检索时会非常方便° ‖.巨|ast|csea『c∩介绍

U

才可以’而且需要我们对信息检索有_定程度的理解。

为了解决这个问题’Elasticsearch诞生了。Elastjcsearch也是使用Java编写的’其内部使用Lucene 实现索引与搜索’但是它的目标是使全文检索变得简单’相当于Lucene的一层封装,它提供了_套 简单_致的RESTfUlAPI来帮助我们实现存储和检索°

所以Elastlcsearch仅仅就是_个简易版的Lucene封装吗?如果这么认为,那就大错特错了,

□一个分布式的实时文档存储库,每个字段都可以被索引与搜索;



|』■■·‖‖』』‖〗』■、

Elasticsearch不仅是Lucene,并且也不只是-个全文搜索引擎°它可以这样准确形容:

■■』‖||■】』】●】□‖】】■`‖□

那Lucene又是什么呢?Lucene可能是目前存在的(不论开源还是私有)拥有最先进`高性能和 全功能搜索引擎功能的库’但也仅仅只是-个库°要想用Lucene’我们需要编写Java并引用Lucene包

|』·‖

Elastjcsearch是_个开源的搜索引擎,建立在-个全文搜索引擎库ApacheLuceneTM的基础之上。

□一个分布式的实时分析搜索引擎; □能胜任上百个服务节点的扩展’并支持PB级别的结构化或者非结构化数据。

总之’Elasticsearch是一个非常强大的搜索弓|擎’维基百科、StackOver∩ow`GltHub都纷纷采用 它来实现搜索°Elasticsearch不仅提供强大的检索能力,也提供强大的存储能力°

2.匠|ast|csea「c∩相关概念

l





引 q

Elasticsearch中有几个基本概念,如节点`索引`文档等’下面分别说明_下。理解这些概念’



对熟悉Elasticsearch是非常有帮助的°

●节点和集群

Elasticsearch本质上是-个分布式数据库’允许多台服务器协同工作’每台服务器均可以运行多 个Elastlcseamh实例。











( 0

单个Elasticsearch实例称为_个节点(Node)’一组节点构成-个集群(Cluster)°

( Q



●索引

索引即index,Elastjcsearch会索引所有字段,经过处理后写人-个反向索引(lnve吭edindex)。查 找数据的时候,直接查找该索引。所以, Elasticsearch数据管理的顶层单位就叫作索引,其实相当于

MySQL`MongoDB等中数据库的概念。另外,值得注意的是,每个索引(即数据库)的名字必须小写。 ●丈档

索引里的单条记录称为文档(document)’许多条文档构成—个索引。







q







对同一个索引里面的文档’不要求有相同的结构(scheme),但是结构最好保持-致,因为这样 有利于提高搜索效率。

{ q



●类型





文档可以分组’例如weather这个索引里的文档,既可以按城市分组(北京和上海),也可以按气

候分组(晴天和雨天)。这种分组就叫作类型(Type)’它是虚拟的逻辑分组’用来过滤文档’类似MySQL 中的数据表、MongoDB中的集合°

q









} p 尸「‖卜

47 Elasticsearch搜索引擎存储

l6l

匹『「|匹■■‖■■『

不同类型的文档应该具有相似的结构°举例来说’1d字段不能在这个组中是字符串’在另一个组

中却变成了数值°这点与关系型数据库的表是不同的°应该把性质完全不同的数据(例如product5和

■尸’‖匹■■『

1Og5)存成两个索引,而不是把两个类型的数据存在_个索引里面(虽然可以做到)° 根据规划, Elastic6.x版只允许每个索引包含-个类型’ Elastic7.x版将会开始移除类型。 ●字段

■■『卜■∏‖■■■■∩‖■■似■尸■‖■■「■=■『‖‖■■■『■△∩‖■尸‖■■■【■■『》巴■『■‖二■『‖■

每个文档都类似_个JSON结构,包含许多字段’每个字段都有其对应的值多个字段组成了个文档’其实可以类比为MySQL数据表中的字段°

在Elastlcsearch中,文档归属于一种类型(Type),而这些类型存在于索引中。我们可以画一个简 单的对比图来类比Elasticsearch与传统的关系型数据库: RelationalDB—,Databases→Tables→Rows-〉Columns

Elastlcsearch→Indices→Types→Documents→Fields

以上就是Elasticsearch里面的—些基本概念’和关系型数据库进行对比更加有助于我们理解°

3.准备工作 在开始本节实际操作之前’请确保已经正确安装好了Elasticsearch’安装方式可以参考:

h忱ps://setupscrapecente门elasticsearch,安装完成之后确保它可以在本地9200端口上正常运行即可° Elasticsearch实际上提供了一系列RestfUlAPI来进行存取和查询操作,我们可以使用cuI1等命令

或者直接调用API来进行数据存储和修改操作’但总归来说不是很方便°所以这里我们直接介绍-个

专门用来对接Elasticsearch操作的Python库’名称也叫作Elasticsearch,使用pip3安装即可: pjP〕1∩5ta11e1a5t1c5e3Ich

更详细的安装方式可以参考: hnps://setup.scrape.centeⅣelasticsearchˉpy。 安装好了之后我们就可以开始本节的学习了° 4.创建索弓|

|◆『△■『尸『

我们先来看—下怎样创建_个索引,这里我们创建-个名为∩e"5的索引: 十rO"e1a5tj〔5ear〔∩mport【1a5ti〔Sear〔h

■■

‖‖卜|

re5u1t=e5.j∩djces。〔Ieate(j∩dex≡|∩ew5』’ ig∩ore≡40o) prj∩t(Ie5(』1t)

这里我们首先创建了-个Elasticsearch对象’并且没有设置任何参数’默认情况下它会连接本地 伊β=■尸

9200端口运行的Elasticsearch服务’我们也可以设置特定的连接信息’如: e5=[1a5ti〔5ear〔向(

∩Ⅱ■『『‖∩▲■厂■■「



q ■ ■ ■ ■ ■ ■ ■

eS=[1a已ti〔5eaI〔∩()



-

[‖∩ttp5://[useI∩a爪e:pa55"ord0]host∩aⅦe:port! ]’ Verj十y=〔ert5≡丁rue》 #是否验证55[证书

第一个参数我们可以构造特定格式的链接字符串并传人, ‖o5t∩a川e和port即Elasticsearch运行

的地址和端口’ u5er∩a‖e和pa55word是可选的,代表连接Elasticsearch需要的用户名和密码’另外 而且还有其他的参数设置’比如Ver1十y一〔ert5代表是否验证证书有效性。更多参数的设置可以参考 https://elastjcsearchˉpy.readthedocsjo/en/latest/apj。html#elastjcsearch°

■尸「|【》‖

声明Elastjcsearch对象之后’我们调用了e5的i∩dice5对象的〔reate方法传人了1∩dex的名 称’如果创建成功’会返回如下结果:



■巴



l62

第4章数据的存储

{』己C|(∩ow1edged: 『n』e’|5hard5aC|〈∩叫1e电ed|: 丁rue」‘j∩dex』: |∩酗5|}

可以看到’返回结果是JSON格式,其中ack∩ow1edged字段表示创建操作执行成功。 但这时如果我们再把代码执行一次,则会返回如下结果: {0error‖: {0Iootcau5e『 : [{`tγpe0 : |re5山I〔e己1readyˉex15tsex〔eptio∩‖’ ,rea5o∩|: 01∩de×

[∩e"5/州[γo乙oq『z【_q∩W』j4a3"] a1Ieadyex15t5』’ 』i∩dexu』jd,了 |h‖[γozoq丁zⅨ-q【γγ』j4己3"! ’ ,i∩dex|: |∩e"s』}]’

』type』: 0re5ourcea1ready_exist5exceptjo∩|’ 』rea5o∩0 : !j∏dex[∩印s/h什[γozoq『水-qRvγ4j归3w]a1re己dyexist50 ’

它提示创建失败,其中5tatu5状态码是400’表示错误原因是索引已经存在°

注意在这里的代码中’我们使用的1g∩ore参数为400,说明如果返回结果是400的话’就忽略这 个错误’不会报错’程序不会抛出异常°

假如我们不加1g∩ore这个参数: e5= [1a5t1〔5earC‖() reSu1t=e5.1∩di〔eS.〔Ieate(1∩dex=∩eWS‖)

pr1∩t(re5u1t)

再次执行就会报错了:

勺《·γ□」■■|』』‖』■■】●‖‖|■■‖〗Ⅻ■Ⅵ‖□■

0i∩dexu‖id0 : ‖h‖[γozoq『z长ˉqRγγ4j4a3W’ 0i∩dex0 : 0∩ew5!}’ ‖5tatu5‖ : q0O}

m15e‖∏p[X〔[p丁I删5.8et(5tatu5code’「r己∩sport[rror)(5tatu5code’ error爬55age’addjtjo∩a1j∩「o) e1日5tic5eaIc∩.exceptio∩5.Request[rror: 丁r己∩sport【rror(4OO’ ′re5o0r〔ea1ready-exi5t5exceptio∩,’ 』i∩dex [∩ew5/酬6yz2创8Q[ˉb十Ⅸ‖c5o丁∩"] a1readyexist5|)

这样程序的执行会出现问题。因此’我们需要擅用1g∩oIe参数’把_些意外情况排除,这样才 可以保证程序正常执行而不会中断°

{{



创建完之后’我们还可以设置-下索引的字段映射定义,具体可以参考: https:〃elastjcsearchˉpy. readthedocs.io/en/latest/api.html?#elasticsearchclientIndicesClientpuLmapping。 5.删除索引



十ro阳e1a5ti〔Sear〔∩j∏pOrt[1a5ti〔5ear〔h

e5≡[1己5t1〔5ear〔h()

resu1t=e5。1∩dj〔e5。de1ete(i∩de×≡∩刨5‖’ 18∩ore=[4OO’ 0O4]) prj∩t(re5u1t〉

这里也使用1g∩ore参数’来忽略因索引不存在而删除失败’导致程序中断的问题。 如果删除成功,会输出如下结果: {|ac代∩ow1edged! : 丁rl』e}

如果索引已经被删除,那么再执行删除’就会输出如下结果: {』error|: {|root〔au5e|: [{`type0 : 0i∩de×∩ot于ou∩dexceptjo∩|’ !rea5o∩` : |∩o5u〔hi∩dex [∩酮5]`’ re5ource.type』: 』j∩dexora1ja50 ’ !re5o‖rce.jd: ,∩印50 ’ 』j∏dex l』ujd0 : 0 ∩a !’ 』j∩dex|; {∩酣5|}]’ 0type! : |i∩dex∩ot十ou∩dex〔eptio∩』’ 0re己5o∩0 8 』∩o5|」c∩j∩dex [∩酗5]0 ’ ,resource.type′ : 0i∩dexora1ja5|’ re5our〔e·jd! : ‖∩e"5,’ 』i∩dexu0id0 : ‖ ∩a ‖ ’ ‖j∩dex0 : !∩ew5』}’ 05tat‖5‖ : 』04}

这个结果表明当前索引不存在,删除失败。返回的结果同样是JSON格式’状态码是404’但是

由于我们添加了1g∩ore参数,忽略了404状态码’因此程序正常执行,输出JSON结果’而不是抛出 异常°

|||

Elastjcsearch就像MongoDB-样,在插人数据的时候可以直接插人结构化字典数据’插人数据可 以调用create方法°例如’这里我们插人一条新闻数据:

||

6.插入数据

|』』■』』■‖|」·‖』■{」·|司』■】』』■|』■]‖‖·■‖■]|‖司|‖|■司|·日||」■■‖■■■■■』】□可

删除索引也是类似的,代码如下:



|‖^■=‖‖『》{‖==■「|■匹=「‖匹‖【●『巴■厂



【尸卜■β伯|巴尸)尸‖|但■「‖‖■■▲■■‖■■「‖△■■口‖●■■「卜■■厉■■■伊『‖■■Ⅷβ『▲■■{‖■■尸■■■「|巴尸



4.7

Elasticsearch搜索引擎存储

l63

千rO‖e1a5tjC5e己r〔∩j∏port [1a5tjc5ear〔h e5=[1aStjC5e3r〔h()

es·i∩dice5·〔reate(j∩dex=‖∩ew5‖’ jg∩ore=4oo) d己ta= { ‖tjt1e‖ : ‖束风破淮不贞韶华,奋斗什冬回梦尚考!’

!ur10 : |∩ttP://γiew°j∩e门5°qq。〔咖/a/[0|」2021Oq16OO7322O00 }

re5u1t≡e5.〔reate(i∩dex≡0∩ew5‖’ id=1’ body=d己t己) pri∩t(re5u1t)

这里我们首先声明了_条新闻数据,包括标题和链接,然后通过调用Create方法插人了这条数

据°在调用Create方法时’我们传人了4个参数,其中i∩dex代表索引名称` jd是数据的唯一标识`

body则代表文档的具体内容° 运行结果如下: {0 j∩dex》: 0∩ews|’ |-type|: !-do〔|》 |-id|8 |10 」 ! ver51o∩! : 1’ 0re5u1t|: 0〔re日ted0 ’ ! 5∩am50 : {!tota1|: 2’ 5U〔ce55十U1|: 1′千ai1ed|: 0}’ ′-5e几∩o′: 0’ |ˉpri阳ryˉtem‖ 肖 1}

结果中的re5u1t字段为created,代表数据插人成功°

另外,其实我们也可以使用1∩dex方法来插人数据°与〔reate不同的是’Create方法需要我们指

定1d字段来唯-标识-条数据’j∩dex方法则不需要,如果不指定jd,那么它会自动生成-个。调用 j∩deX方法的写法如下: e5.i∩dex(i∩dex≡!∩ew5’’ body=data)

Create方法内部其实也是调用了1门dex方法’是对1∩dex方法的封装。 7.更新数据

更新数据也非常简单’我们同样需要指定数据的jd和内容,调用update方法即可’代码如下: +Io∩]e1a5tj〔5ear〔bmport[1a5ticse3rc们

=『口∏尸|■■尸◆|[尸■‖‖△=尸β|=■‖‖|〖尸「β■尸|卜▲■‖|■■=

e5≡[1astj〔5e己rch() data={ ,tjt1e, ; |束风攻浪不贞韶华,谷斗什总■梦尚考0’

0ur10 吕 0∩ttp://γj酗.i∩酣5.qq·〔酮/a/[山2O21O416OO7322卯0 ’ 0date0 : 02O21ˉ07ˉO50



Ⅲe5u1t=e5.upd3te(1∩dex=,∩ew50’ body=data’ jd=1) pri∩t(Ie5u1t)

这里我们为数据增加了一个日期字段,然后调用了update方法’结果如下: {0ˉi∩dex,: !∩印B,’|-type,: |do〔|’ , id|: 01|’ ‘ γersio∩, : 2′ ,re5u1t0 : !updated′ ’ , 臼hard5,: {『tot3r: 2’ 』succe5s于u1,: 1′ 』+ai1ed′: o}’ 』_5e[∩o』: 1’ 』ˉprinazyˉtem』: 1}

可以看到,返回结果中的re5u1t字段为updated’表示更新成功°另外,我们还注意到一个字段 γer5jo∩,这代表更新后的版本号,其后数字2代表这是第二个版本。因为之前已经插人过一次数据, 所以第_次插人的数据是版本1,可以参见上例的运行结果’这次更新之后版本号就变成了2’以后 每更新一次’版本号都会加1°

‖(尸卜

另外,利用j∩dex方法同样可以完成更新操作’其写法如下: e5.i∩dex(j∩dex≡!∩副5′’ do〔ˉty酝=°po1itj〔50, body=data’ id=1)

可以看到, j∩dex方法能够代替我们完成插人数据和更新数据两个操作°如果数据不存在’就执 行插人操作,如果已经存在’则执行更新操作,非常方便°



□●厂‖卜『|▲■∏『

8ˉ删除数据

如果想删除_条数据,那么调用de1ete方法并指定需要删除的数据1d即可。其写法如下: 十roⅦe1a5t1〔5e己r〔hiⅦport[1a5tj〔5ear〔h eS=[1日5tiC5ear〔h()

re5u1t≡e5.de1ete(1∩dex=‖∩ew5‖’ 1d=1〉 pr1∩t(re5u1t〉

‖ | | ■ ■ 可 | ‖ ‖ ‖ ■ ■ ‖||‖·刽』〗』』‖■Ⅵ‖‖』|』勺‖』』‖·】|]引』叫(』∏{■■」|‖‖·

第4章数据的存储

l64

运行结果如下:

{‖ i∩dex0 : 0∩ew50 ’ ,-type0 : 』do〔0 ’ | 1d|: 01‖ ’ | γers1o∩|: 2’ 『Ie5u1t0 ; |de1eted’ ′ 5hard50 : {0tota1‖: 2’ |5ucce55十u1‖: 1’ |千己i1ed‖ : o}’ ‖-5eq=∩o‖: 3」 !=pr加aIy-ter‖』: 1}

9.查询数据

上面的几个操作都是非常简单的,普通的数据库如MongoDB就可以完成’看起来并没有什么了 不起。Elastlcsearch更特殊的地方在于其异常强大的检索功能°

对于中文来说’我们需要安装_个分词插件’这里使用的是elastjcsearchˉanalysisˉlk。我们用

Elastlcsearch的另一个命令行工具elasticsearchˉplugin来安装这个插件,这里安装的版本是7.l32’请 确保和Elastlcsearch的版本对应起来,命令如下: e1己5ti〔5ear〔hˉp1ug1∩1∩5t己11∩ttp5;//git∩ub。co川/们ed〔1/e1日st1〔5earc‖ˉa∩a1ys15ˉik/re1ea5e5/dow∩1oad/γ7.13.2/ e1a5tic5eaIchˉa∩a1y5j5ˉ1kˉ7·13·2·21P

请把这里的版本号替换成你的Elasticsearch版本号。

安装之后,重新启动Elastlcsearch就可以了’它会自动加载安装好的插件°

首先’我们重新新建—个索引并指定需要分词的字段,相应代码如下: +ro∏e1日5tj〔sear〔hj‖poIt[1a5tj〔5eaI〔‖

eS≡[1a5tj〔5ear〔h()

Ⅶapp1∩g≡{ 0proPertje50 : { |tjt1e! 目 {

0type′ : 『text0 ’ |a∩a1y∑er|: !jk们axwoId‖’ ‖5e日rC们a∩a1γ∑er| ; !jk∏己xwom` `



} }

e5.1∩dj〔e5·de1ete(1∩dex=‖∩ew5‖’ jg∩ore≡[4OO’ 4O4]) e5.i∩dj〔e5。〔reate(j∩dex二‖"ew5‖’ 1g∩ore=40O) Ie5‖1t≡e5.1∩d1ce5.put-∏]app1∩g(1∏dex=‖∩ew5‖’ body≡"app1∩g) prj∩t(Ie5u1t)

这里我们先将之前的索引删除’然后新建了_个索引’接着更新了它的"app1∩g信息。‖appj∩g信 息中指定了分词的字段,包括字段的类型type`分词器a∩a1yzer和搜索分词器5earc∩ˉa∩a1yzer。指 定搜索分词器sear〔∩—a∩a1γ2er为j代"axword表示使用我们刚才安装的中文分词插件,如果不指定’ 则会使用默认的英文分词器°

接下来’我们插人几条新数据: fro∏e1a5tic5e己r〔hmport[1a5tic5earc∩ e5≡[1日5ti〔5earC∩() data5= [ {

,t1t1e, : ‖高考结局大不同‖」

旦■‖□可■‖■■可‖··日‖』■■∏司|』■■‖司■■】‖{』■■·||』■‖■■■』■■■】‖勺Ⅲ■■■】□」·□〗‖·■」■■‖|■■‖』■|』■|□∏■■(刁‖当■司可』■‖■|司Ⅲ■』■】■勺‖|』】■|{‖|司|‖

可以看到,运行结果中的re5u1t字段为de1eted,代表删除成功; γer51o∩变成了3’又增加了l。



} b

△卜「|‖■『|卜‖}

47 }」 {

Elasticsearch搜索引擎存储

l65

!ur1|: |∩ttp5://促·Bj∩a。〔o肌〔∩/artic1e7571o646Ⅺ81〔345』7Moo1o111z9。∩t‖1|’ |tjt1e|:进入职业大沈牌时代’〃吃杏〃职业还吃杏冯? |’

□卜『)■尸 巴尸‖|‖「●「|口『|巴=■『■日‖■尸|卜‖ ■■∏[‖『|匹■尸■■■『卜



|uI10 : {∩ttp5含//∩ew.qq。co们/o们∩/2O21O828/2O210828A025账0O。bt∏1|’



」》





‖t1t1e『 ; |束风破浪不负韶华,奋斗计春圆梦岛考‖’ ,ur1! : 0∩ttp;//γjew.1∩ew5.qq.coⅧ/a/[0U2021O416O0732200|’

I》 『



‖tit1e‖ ; ‖他,活出了我们理想的样子|’ |ur1! 8 ‖http5://∩eⅣ·qq°co们/oⅦ∩/20210821/2O210821∧O2OIDOO。∩tⅧ1‖’



] 十Ordataj∩data5;

e5.1∩dex〈i∩dex≡‖∩ew50 ’ body=dat己)

这里我们指定了4条数据,它们都带有t1t1e和ur1字段然后通过1∩dex方法将它们插人 Elasticsearch中’索引名称为∩ew5。

接下来,我们根据关键词查询一下相关内容:

■「卜■■■∏▲伊〖■尸卜|■■「『巴尸[β|

re5u1t=e5°5eaIC‖(i∩dex=∩e"5,) pri∩t(Iesu1t)

运行结果如下:

匹■「β|



{‖too代|: 11′ 0tmedout0 :「a15e’ |5∩ard5‖ 8 {0tot日1‖: 1》 05ucce55十u1‖: 1』 0s代1pped|:0′ |+aj1ed! :o}’!hit5! : {‖tota1|;{0va1ue‖;4’ |re1atjo∩0 : |eq‖}’ 0川ax5coIe』: 1.o’ ‖h1t5‖ : [{‖ 1∩dex, : 』∩e"5|’ ‖-type|: ‖ do〔‖’ | 1d0 ; ‖jebp代‖58们ˉ8A∩yˉ7b0γp‖’ 』 5〔ore‖: 1·0’ 0 5our〔e‖: {‖tjt1e0 : |高考结局大不同』’ 0uI1』: ‖∩ttps://R·5i∩a.〔o‖l〔∩/ | artj〔1e75710646281〔34547340010111z9°ht爪1‖}}’{‖ˉi∩dex0 ; 0∩e"5‖’ 0-type : ′ doc0 」 ‖ id0 : |jubpk‖58川ˉ8∧∩yˉ 7∩0bz0 ’ ! 5core‖ ; 1.0’ ‖ 5ource, : {‖tit1e|: ‖进入职业大沈牌时代, “吃杏’’职业还吃奋吗? ‖ ’ 0ur10 : ‖∩ttp5://∩ew. qq。co爪/o‖|∩/20210828/2021O828∧O25[长0OhtⅧ1‖}}’{0 i∩dex』: 0∩e"5|』 ‖ˉtype 0 : ‖ doc0 ’ ‖ 1d‖ : ‖jˉbp促‖s8‖『‖ˉ8∧∩yˉ 7∩eZ‖‖ ′ 0 5〔ore‖: 1.0’ 『 5ource‖ : {)t1t1e‖: 0乘风玻浪不负侣华,奋斗什容圆梦高考! ’ |ur10 : ‖bttp://γiew.i∩eⅣ5. qq.〔o们/a/[0(」2O21O416OO7〕2200‖}}’{0 1∩dex0 : |∩ew5! ’ 0-type‖ ; 0 do〔0 」 |jd0 : |低Obp代‖58"-8∧∩γˉ7‖ea∩‖’ 0 5〔ore『 : 1.0’ 0 5our〔e0 : {|t1t1e‖: ‖他,活出了我们理想的样子0 』 0‖r1‖ : ‖‖ttp5://∩ew.qq。〔咖/o‖∩/2O2108Ⅱ1/2O21O821∧O20 I00O.ht们1‖}}]}}



另外’我们还可以进行全文检索’这才是体现Elastjcsearch搜索引擎特性的地方: +rO‖e1a5ti〔SearChjⅧpOrt[1a5ti〔Se己r〔b 1"POrtj5O∩ d51={



‖query0 : { 0∩at〔∩‖ : { 二卜」

‖tit1e‖ : 』高考圆梦‖

、卜』

`′』

●厂‖||■∏『卜|[■「|‖【尸||’■厂|卜|■『|β|巳卜『|卜『『「卜巴■『|‖

可以看到’泣里查询出了插人的4条数据。它们出现在hit5字段里面,其中tOta1字段标明了查 询的结果条目数, ∏axS〔Ore代表了最大匹配分数°

e5≡[1a5tiC5ear〔‖() re5u1t=e5.5earch(j∩dex=∩eⅣ5|’ body≡d51〉

pr1∩t(Ie5u1t)

这里我们使用Elasticsearch支持的DSL语句来进行查询’使用‖atCh指定全文检索,检索的字段 是t1t1e’内容是“高考圆梦”’搜索结果如下: {0too代‖ : 6」|tmedout0 :「a15e’ 0 5h日rd5‖: {‖tota1‖: 1’ ,5o〔〔e55+u10 : 1’ ‖5灶pped‖: O」 0十aj1ed0 : O}’ 』‖1t5‖ :

{|tota1『 ; {『γa10e|丁2』|Ie1at1o∩』: 』eq|}’ 』"ax5〔ore! ;1.7796917’ |bit5|; [{| 1∩dex|: |∩ew5|’ |—type|: | do〔|’

| 』 id』: ′jˉbp代‖5B阳ˉ8A∩yˉ7heZ‖|′ ‖ score|: 1。7796917’ 一sour〔e‖: {‖tjt1e』: ‖末风玻浪不负韶华’奋斗订春圆梦岛 考0 ′ |ur1‖ : ‖http://view·1∩e"5.qq.〔o『∏/a/[Dl」2O21叫16"732】O0!}}’{0 j∩dex』: ,∩ew5 ‖ ’ !-type 0 : , doc|’ 0 jd‖ :

p





l66

第4章数据的存储

0jebp低‖58们ˉB∧∩yˉ7h0γp‖》 0ˉ5core』: O。81O851M’|5ource‖ : {{tit1e|: 0高考结局大不同0 ’ ‖ur10 :

』http5://k。51∩a.coⅧ.〔∩/artic1e75710646281c3q54734OO1O111z9.htm1,}}]}}

从结果可以看到,匹配的结果有两条’第一条的分数为l.77969l7,第二条的分数为O8l085l34’

这是因为第一条匹配的数据中含有“高考”和“圆梦,,两个词’第二条匹配的数据中不包含“圆梦,,’ 但是包含‘高考”这个词,所以也被检索出来了’只是分数比较低。

因此可以看出’检索时会对对应的字段进行全文检索’结果还会按照检索关键词的相关性进行排 序’这就是—个基本的搜索引擎雏形°

另外, Elasticsearch还支持非常多的查询方式。这里就不再_一展开描述了,总之其功能非常强 大’详情可以参考官方文档:https://www.elasticco/guide/en/elasticsearch/reference/master/queryˉdslhtml° ↑0.总结

以上便是对Elasticsearch的基本介绍以及使用Python操作Elasticsearch的基本用法’但这些仅仅 是Elasticscarch的基本功能’它还有更多强大的功能等待着我们去探索° 本节代码参见: https://githuhcom/Python3WebSpider/ElastjcSearchT℃st°

只abb|tMO的使用 在爬取数据的过程中’可能需要一些进程间的通信机制,例如下面三个°

□一个进程负责构造爬取请求’另—个进程负责执行爬取请求。 □某个数据爬取进程执行完毕’通知另外一个负责数据处理的进程开始处理数据° □某个进程新建了一个爬取任务,通知另外-个负责数据爬取的进程开始爬取数据°

为了降低这些进程的鹊合度’需要—个类似消息队列的中间件来存储和转发消息’实现进程间的 通信°有了消息队列中间件之后’以上各机制中的两个进程就可以独立执行,它们之间的通信则由消 息队列实现。

□一个进程根据需要爬取的任务’构造请求对象并放人消息队列,另一个进程从队列中取出请求 对象并执行爬取。

□某个数据爬取进程执行完毕,就向梢息队列发送消息,当另一个负责数据处理的进程监听到这 类消息时’就开始处理数据°

□某个进程新建了_个爬取任务后,就向消息队列发送消息’当另_个负责数据爬取的进程监听 到这类消息时,就开始爬取数据°

那这个消息队列怎么实现呢?业界比较流行的实现有RabbitMQ、RockctMQ、Ka仕a等’其中 RabbitMQ作为一个开源、可靠、灵活的消息队列中间件备受青睬,本节我们也来了解_下它的用法° 注意我们在前几节了解了一些数据存储库的用法’它们几乎都用于持久化存储数据°本节介绍的 是一个消息队列中间件’它虽然主妥应用于数据消息通信,但由于它也具备存储信息的能 力,所以将其放在本章介绍。 ↑.Rabb‖tMO的介绍

RabbitMQ是使用Erlang语言开发的开源消息队列系统,基于AMQP协议实现。AMQP的全称是 AdvancedMessageQueuePTotocol’即高级消息队列协议,其主要特点有面向消息、队列、路由(包括 点对点和发布/订阅)、可靠性、安全性°

RabbitMQ最初起源于金融系统’用于在分布式系统中存储和转发消息’在易用性`扩展性、高

』■】‖{』■●■‖|』=■■‖|··|』】」】∩■■】]』■■‖||■·]·‖||■■』‖|』■〗■■||·司□‖|』·日{|』可|」∏』■|』勺|■]」·』】]‖■‖‖]□』■∏{|■■|】·|』□|||』■乙·可|□」■司■‖』』■□

4ˉ8

‖|□‖‖■厂|‖■■】‖‖『■∩

48

RabbitMQ的使用

l67

b



可用性等方面均表现不俗’具体特点有以下这些°

▲份【巳■口『‖■■

□可靠性(Reliability):RabbitMQ通过一些机制保证可靠性,如持久化`传输确认、发布确认°

□灵活的路由(Flexjb|eRouting):由Exchange将消息路由至消息队列。RabbltMQ已经提供了 _些内置的Exchange来实现典型的路由功能;对于较复杂的路由功能,则将多个Exchange绑 定在-起,或者通过插件机制实现自己的Exchange。 □消息集群(Cluste面ng):多个RabbjtMQ服务器可以组成_个集群’形成一个逻辑Broker。 □高可用(HighlyAvajlableQueues):消息队列可以在集群中的机器上镜像存储,使得队列在部

△■■『萨『

分节点出问题的情况下仍然可用°

-

‖ = 』

□多种协议支持(multjˉprotocol):RabbitMQ支持多种消息队列协议’例如STOMP`MQTT° {4 □多语言客户端(ManyCllents):RabbltMQ几乎支持所有常用语言,例如Java、NET`Ruby。 □管理界面(ManagementUI):RabbjtMQ提供了一个易用的用户界面’使得用户可以监控和管

日‖『|巴■尸■■

理消息Broker的多个方面。

□跟踪机制(Tracing):RabbitMQ提供了消息跟踪机制,如果消息异常,使用者就可以找出发 生了什么°

□插件机制(PluginSystem):RabbltMQ提供了许多插件,实现了多方面的扩展’用户也可以编



写自己的插件°

2准备工作 ▲ ■ 『 △ ■

在本节开始之前,请确保已经正确安装好了RabbitMQ,安装方式可以参考https:〃sempscIa庐.ceIm/

rabbitmq,需要确保其可以在本地正常运行°

除了安装RabbitMQ’还需要安装_个操作RabbitMQ的Python库,叫作pika,使用pip3工具安 装即可:

0

■尸‖卜|

pjp3i∩5ta11pjk3

更详细的安装说明可以参考https://setupscrapecente〃pika。 以上二者都安装好之后,开启本节的学习° 3.基本使用

p

首先,RabbjtMQ就是_个消息队列,我们要实现的进程间通信’从本质上讲是一个生产者ˉ消费

∩|

者模型,即一个进程作为生产者往消息队列放人消息,另一个进程作为消费者监听并处理消息队列中 的消息,主要有3个关键点需要关注。 □声明队列:通过指定_些参数,创建消息队列°

∏■

□生产内容:生产者根据队列的连接信息连接队列,往队列中放人消息° □消费内容:消费者根据队列的连接信息连接队列,从队列中取出消息° 下面我们先来声明_个队列,相关代码如下:

0

mpoItP让a α」[0[‖M[= ‖5〔mpe‖

旧广止尸卜‖~】‖▲■■■『■△尸尸『匹「》『

co∩∩ectio∩≡p让a.01ocki∩8〔o∩∩e〔tjo∩(p让a.〔o∏∩e〔tjo∩Pam∏记ter5(‖1oca1∩o5t‖)) 〔ha∏∩e1=〔o∏∩e〔tjo∩.ch日∩门e1()

〔ha∩∩e1.ql』eue-deC1are(queue≡α」[l』[‖朋[)

这里先连接RabbjtMQ服务,由于RabbitMQ运行在本地,因此直接使用localhost即可’将得到 的连接对象赋值为co∩"ect1o∩。然后声明了_个频道对象,即〔ha∩∩e1,利用它我们可以操作队列内消 息的生产和消费°之后我们调用〔ba∩∩e1的queueˉde〔1are方法声明了_个队列’队列名称叫作5〔rape°

』 ■ | · ■ 可

第4章数据的存储

下面我们尝试往队列中添加消息: ro‖tj∩g=代ey=仙[0[=‖酬[’

body≡0‖e11o‖oI1d!|〉

这里我们调用〔∩a∩∩e1的ba5icˉpub1i5‖方法往队列放人了消息,其中routj∩gˉRey是队列的名 称, body是放人的真实消息°

』|』■·■‖纠勺|■■」‖■

〔∩a∩∩e1.basjc-pub1i5h(exc∩日∩ge=|‖ ’

』·可‖·二_■|‖‖‖‖』■■|

l68

将以上代码写人一个名为producenpy的文件,即生产者°

其实也很简单°消费者用同样的方式连接到RabbitMQ服务,代码如下:

』■‖□■‖=▲

加pOrtpj阳

』■‖·〗

现在’前两点—声明队列和生产内容其实已经完成了,接下来就是消费内容了°

00[0[‖酬[= 05Crape『

〔O∩∩e〔t1O∩≡P1ka.81OC肛∩g〔O∩∩e〔tjO∩(P业己。〔O∩∩eCtjO∩p己ra‖eter5(『1O〔a1∩O5t|)) 〔ha∩∩e1二CO∩∩e〔tjO∩。〔ha∩∩e1()



〔∩己∩∩e1。queueˉdeC13re(que仙e=Q0[0[‖州[〉

然后从队列中获取数据,代码如下: de于ca11ba〔代(〔h’爬tbod’ propertie5’ body): prj∩t(千"6et{body}")



〔ha∩∩e1.ba5i〔〔o∩5u爬(que‖e=,5〔r3pe‖’

o∩-们e55age—ca11ba〔k=c己11bad〈) Cha∩∩e1.5tart〔O∩5Um∩g()

』□∏□二■■

己utoac促=丁rue』

这里我们调用c∩a∩∩e1的ba51c〔o∩5u∏e方法从队列中取出消息’实现了消费’同时指定回调方

息之后会自动通知消息队列当前消息已经被处理’可以移除这个消息°

最后’将以上述代码保存为consumerpy文件(消费者)并运行’它会监听s〔mpe队列的变动’

■□|■■□叮■■■‖□■■

法o∩-‖e55age-Ca11bac代的名称为Ca11ba〔k。另外,还将aUtoa〔k设置为了丁rue,代表消费者获取消

如果有消息进人’就获取并消费,回调〔a11baC促方法’打印输出结果°

现在运行producer.py文件’运行之后会连接刚才的队列,同时往该队列中放人-条消息,消息内



容为‖e11o‖oI1d!。

■■可‖■■

这时再返回consume∏py文件,可以发现输出结果如下: Cet‖e11o‖oI1d!

这说明生产者成功把消息放人了消息队列’然后消费者收到并输出了这条消息° Q

可以继续运行producerpy,每运行_次’生产者都会向队列中放人-个消息’消费者会收到该消 息并输出。

以上便是最基本的RabbjtMQ的用法° 4随用随取



上面的案例是基于RabbitMQ实现的最简单的生产者和消费者之间的通信,但如果把这种实现用 列的变化’_旦监听到队列中添加了消息’便要立马处理’它无法主动控制取用消息的时机。应用到



爬虫中,消费者其实就是执行爬取请求的进程’生产者往队列中放置请求对象,消费者从中获取请求

』】■‖』■司

在爬虫上是不太现实的’因为我们把消费者实现为了“订阅,,模式’也就是说’消费者会_直监听队

对象’然后执行这个请求(向服务器发起HTTP请求以获取响应)°但问题是,消费者是无法控制从 发起请求到获取响应所消耗的时间的’因为什么时候获取到响应内容取决于服务器响应时间的长短,



■■∏|』□』』■■

■■『■‖■

Ⅱ伊β『■■

)「

但卜「‖卜β■=■=厂|

|仁

0」

β

4.8

RabbjtMQ的使用

l69

所以这意味着消费者不_定能很快地将消息处理完。如果生产者往队列中放置过多的请求,消费者处 理不过来,那就会出现问题°因此消费者也应该有权控制取用消息的频率’这就是随用随取° 我们可以对前面的代码稍做改写,使生产者可以自行控制向队列放人请求对象的频率’消费者可 以根据自己的处理能力控制从队列中取出请求对象的频率°如果生产者的放置速度比消费者的获取速 度更快’那么队列中就缓存一些请求对象’反之队列有时候会处于闲置状态°



总的来说’消息队列起到了缓冲的作用’使生产者和消费者可以按照自己的节奏工作° p

好’下面先实现下刚才所述的随用随取机制’队列中的消息可以暂且先用字符串表示,后面再将 p

其更换为请求对象。



可以将生产者实现如下:



巴■`「■巴β△卜

1‖pOrtp让a

Q0[0[‖酬[≡ 05〔IaPe 〔O∩∩eCt1O∩=pi阳.81OC代1∩8(O∩∩e〔tiO∩( p1低a.〔O∩∩e〔tjO∩p己ra眠ter5(hO5t=|1O〔己1hO5t!)) Cha∩∩e1≡〔o∩∩e〔tiO∩.C∩a"∩e1() 〔ha∩∩e1.queuede〔1己re(queue≡q』[(j[‖酬[) W∩i1e丁rue:

但■『’′■尸‖■尸■■■■■》■■尸‖‖■◆

data=j∩put()

〔ha∩∩e1.b日5ic-pub1i5∩(ex〔∩己∩ge=‖ | ’ routj∩g=低ey=仙[0[=‖酬[’ body=data) pri∩t(十‖put{data}‖)

这里我们还是使用j∩put方法来获取生产者的数据’输人的内容就是字符串’输人之后该内容会 直接被放置到队列中’然后打印到控制台。 先运行—下生产者代码,然后回车输人几项内容: +OO put「OO



bar

「『

putbar

b日Z

putba2

B

这里我们输人了+oo、 bar、 b日∑三项内容’每次输人后控制台都会输出对应的结果°

‖》『|甘【「》仪

然后将消费者实现如下: j们pOrtp让a

■尸■■『’●■

Q0[0[‖酬[= 05〔r日pe

〔o∩∩e〔tio∩≡p让a.81o〔促1∩g〔o∩∩e〔tjo∩(

Pi|〈a.〔O∩∩e〔tjO∩para"eter5(∩O5t≡!1O〔a1hO5t|)) cha∩∩e1=co∩∩ectio∩.〔∩a∩∩e1()

■尸习卜〖巴『‖尸氏‖仪|}||▲『『|卜|》『|‖|}■■「}|广‖「

WM1e『rue:

i∩put() 们et∩od千m爬’∩e己der’ body=〔ha∩∩e1.ba5i〔-8et( que‖e≡α」[0[‖州[’ 日utoac|〈=『rue) j+body:

pri∩t(+℃et{body}』)

我们这里也是通过j∩put方法控制消费者何时获取下一个数据’获取方法是ba5jcˉget,这个方 法会返回一个元组’其中的bodγ就是真正的数据°



第4章数据的存储

l70

运行消费者代码,然后按几下回车’每次按回车后都可以看到控制台输出-个从消息队列中获取 的新数据: Cetb|千oo0 6etbbar| CetbbaZ`

这样就实现了消费者的随用随取。 5.优先级队列

刚才我们仅仅是了解了最基本的队列用法’RabbitMQ还有一些高级功能°例如’生产者发送的 消息具有优先级,队列会优先接收优先级高的消息’这要怎么实现呢? 其实很简单,只需要在声明队列的时候增加一个属性即可: 川AXpRI0RI丁γ=1OO

!Xˉ爪a×ˉprioIity‖: NM-pRI0RIW

})

这里在声明队列的时候’增加了一个名为xˉ肌axˉprjor1tγ的参数’用来指定最大优先级,这样整 个队列就能支持优先级了°

1卯ortpi阳 "∧XpRI0RIⅣ=1OO

卯[0[‖州[≡ 0S〔mpe

〔o∩∩e〔tio∩≡p让a.81o〔促j∩g〔o∩∩e〔tio∩( Pj促a.〔o∩∩e〔t1O∩Pam爬ter5(∩O5t≡!1oca1ho5t0)) 〔‖a∩∩e1=co∏∩ectjo∩.〔ha∩∩e1() 〔ha∩∩e1.queuede〔1aIe(queue=α」[0[‖州[’ argu爬∩t5={ ×ˉ吧XˉpriOrity!:灿XpRI0RI∏ }) Wbi1e丁Iue;

这里的优先级我们也可以手动输人,需要将输人的内容分为两部分,这两部分用空格隔开,运行 结果如下:



(』■■】|判』司|‖』‖勺刘{」可|』』∏·

dat己’ prjorjty≡j∩p0t().5p1jt() 〔ha∩∩e1.ba5icˉpub1i5∩(ex〔ha∩ge≡,‘’ routj∩g-key=α」[l」[-‖朋[’ propeItje5=pikaB35j〔pIopertie5( pIjomtγ=j∩t(prjorjtγ)’)’ body=data) pri∩t(「0Put{d己ta}’)

刘|□‖‖(」■]|』|『∩』‖{』·』』|』`』

下面改写一下生产者代码,在其向队列发送消息的时候指定propertie5参数为8a5i〔propert1e5 对象,在8a5j〔propertje5对象里通过priorjty参数指定对应消息的优先级’实现如下:

‖■■』■]‖』《、·可|』』叫】■]|‖‖■〖】□

C∩a∩∩e1·queueˉde〔1are(queue=山[0[‖叫[’ argu‖e∩t5={

「Oo』O Put「OO

bar】0 putbar

b亚50 putbaZ

然后重新运行消费者代码,并按几次回车’可以看到如下输出结果:

□』■·Ⅵ』·‖‖‖||」‖■□‖||』□〗‖】Ⅱ』=■]‖引|』勺《司/·】

这里我们输人了三次内容,第一次输人的是十ooqO’代表+oo这个消息的优先级是40;第二次输 人baI2o,代表baI这个消息的优先级是2o;第三次输人baz5o’代表baz这个消息的优先级是50°



| |



4.8

RabbitMQ的使用

l7l

6etbbaZ0

Cetb千oo‖ Cetb0baI,

从输出结果我们可以看到,消息按照优先级被取出来了。ba乙的优先级是最高的,所以被最先取 出来°bar的优先级是最低的’所以被最后取出来° 6.队列持久化

除了设置优先级,还可以将队列持久化存储,如果不设置持久化存储,那么数据在RabbitMQ重 启之后就没有了。

在声明队列时指定dumb1e为「rue’即可开启持久化存储’实现如下: Cha∩∩eLq0e0ede〔1are(q|」eue="[0[‖酬[’ argⅧe∩t5={‖xˉ刚axˉprjoIjty0 :ⅧXpRI0RIⅣ}’ durab1e二『rMe)

同时在添加消息的时候需要指定8a51cpropertje5对象的de1jγery-Ⅷde为2,实现如下: pIopert1e5=pj阳。8a51〔propertje5(prjomty=j∩t(pIiorjty)’ de11γery-Ⅷde=2)

所以’这时的生产者代码改写如下: 1‖Portpik己 ‖A】pRI0βI丁γ=1OO

α」[0[‖刚[≡ ||■厂’|‖|■■『●【‖||■厅■【「[尸「













5〔rape

〔o∩∩ectio∩二p1阳°B1o〔促j∩g〔o∩∩ectjo∩( p止己.〔o∩∩eCt1O∩para‖eter5(hO5t≡|1o〔a1hO5t』)) 〔‖日∩∩e1=〔o∩∩e〔tio∩。〔‖日∩∩e1()

Cha∩∩eLq‖』el』ede〔1己re(ql』el』e=仙[0[‖刚[’ argt』川e∩t5={ 0xˉ爬xˉprjoIity’:灿XpRI0RI丁γ }’ d0Iab1e=『rue) W∩i1e了rue:

dat己’ prjorjty=1∩pl」t().5p1jt(〉 〔ha∩∩eLb己5i〔pub115∩(ex〔ha∩ge≡Ⅷ』 routi∩g≡代ey=q」[l」[‖州[’ PrOPertie5≡P让a.8a51CprOPertie5( prjorjty=i∩t(priorjty)’ de1jγeIy=帅de=2’ )’ body=data) pri∩t(「!put{data}0 )

这样就可以持久化存储队列了。

7.实战

最后,我们将字符串消息改写成请求对象’这里需要借助requests库中的Reque5t类来表示一个 请求对象°

构造请求对象时’传人请求方法和请求URL即可,代码如下: reque5t≡reql』e5t5.〖eqUe5t(0C[丁|’ ur1)

这样就构造了_个GET请求,然后可以通过pickle工具进行序列化,最后发送到RabbitMQ中° 生产者代码实现如下: iⅧpoItpj促a i们portIeque5t5

1们portp1〔伐1e

[4

| 第4章数据的存储

l72

"MpRIO【∏γ=1O0

『0『∧L=1Oo



0{」[0[‖州[ ≡ ‖5〔rape-q0eue

〔o∩∩ectio∩≡p汕a.B1oc促j∩g〔o∩∩e〔tio∩〈 pi{〈a。〔O∩∩e〔tjo∩pam爬ter5(∩O5t=‖1O〔a1hO5t‖)) Cha∩∩e1=〔o∩∩e〔tjo∩.〔‖a∩∩e1()

〔们a∩∩eLqoeuedec1are(queue=QU[l」[‖州[’ dumb1e=丁rue)



十Orji∩m∩ge(1’丫0『∧L+1): l』】1=「|http5://55r1.5cI己pe.ce∩ter/deta11/{j}| reque5t=reque5t5.RequeSt(℃[丁0 」 0r1) cha∩∩e1.b己5ic_pqb1i5∩(ex〔帕∩ge=′0 ’ routj∩g=key=q」[0[‖州[』

propeItje5=p业a.8a5icpropertje5( de1jvery-∏me=2’ )’

运行这段生产者代码’就构造出了l00个请求对象并发送到了RabbitMQ中。 对于消费者,可以编写-个循环,让它不断地从队列中取出请求对象’取出一个就执行一次爬取











■■』■■■句』□·{■

body=pi〔叔1巳duⅧp5(request)) pr1∩t(十!putreqoe5to十{ur1}0)

] ]

任务,实现如下:

1Ⅶportreque5t5 ‖∧XpRI0RI丁γ=10O

仙[0[‖酬[= 5craPe-que0e

〔o∩∩eCtjO∩≡p让a.81O〔代i∩g〔O∩∩eCtjO∩( pj阳.〔o∩∩ectjo∩para们eter5(ho5t=‖1oca1∩o5t|)) 〔h己∩∩e1=〔o∩∩ectjo∩.〔‖a∩∩e1(〉 5e55iO∩=reque5t5.5e55io∩()

de+5〔raPe(reqUe5t): trγ:

re5po∩5e≡5e551o∩.5e∩d(reque5t.prepare()) pr1∩t(+!su〔〔e555cr日ped{re5po∩5e.ur1},) ex〔eptreque5t5°Request[x〔eptio∩8

pr1∩t(「‖erroro〔〔urred"}〕e∩5crap1∩g{req(」e5t·ur1}0) W们i1e丁me:

"ethod千r己们e’ ‖eader’ body≡〔ha∩∩e1·ba5j〔ˉget( queue=0(」[0[‖州[’ autoack=丁Iue) 1千bodγ; reque5t≡p1〔演1e.1o己d5(body)

prj∩t(+06et{Ieque5t}』) 5cmpe(reqqe5t)

这里消费者调用ba51c-get方法获取了消息,然后通过pickle工具把消息反序列化还原成—个 请求对象,之后使用5e551o∩的5e∩d方法执行该请求’爬取了数据,如果爬取成功就打印爬取成功的 消息° 运行结果如下: Cet<Reql」e5t [C[丁]〉 50〔〔e555〔rapedhttps://55r1·5〔rape·〔e∩ter/detai1/1 Cet〈Reque5t [C盯]〉 5uc〔e555〔rapedhttp5目//55r1·5〔mpe.ce∩ter/deta11/2 ●





Cet 〈Reque5t [C[丁]〉 5‖〔ce555〔rapedhttp5://55r1°5〔r3pe°〔e∩teI/detai1/1oo

||旬||」■】{|‖』·』]‖』□』‖||』■」日勺」■∏√叫■刘|·‖|』]』■‖】■」司|《|□|‖』】司||□]■|||』■‖‖】】■□〗】】』■‖]|

1‖pOrtpj阳 j∩pOrtp1〔代1e

} p



b伊



p



P







0

p







P

「 p

0

「 p













0

P

p







卜 0











4.8

RabbitMQ的使用

l73

可以看到,消费者依次取出了请求对象,然后成功完成了_个个爬取任务° 8.总结

本节介绍了RabbitMQ的基本使用方法’有了它’爬虫进程之间的通信就变得非常简单了°后文 我们还会基于RabbitMQ实现分布式爬取的实战’所以本节的内容需要好好掌握°

本节代码参见: htms://gjthub.com/Python3WebSpider/RabbitMQT℃st° 本节中的部分内容参考hhps://wwwrabbitmqcom/documentatjonhtml和https://pikareadthcdocs.io° 厂

4 h



第5章 第

|勾aX数据爬取 有时我们用requests抓取页面得到的结果,可能和在测览器中看到的不-样:在测览器中可以看 到正常显示的页面数据,而使用requests得到的结果中并没有这些数据。这是因为requests获取的都 是原始HTML文档’而测览器中的页面是JavaScrlpt处理数据后生成的结果,这些数据有多种来源: 可能是通过Ajax加载的’可能是包含在HTML文档中的,也可能是经过JavaScnpt和特定算法计算后

■】□】勺』‖■□□‖勺|■】‖勺〗‖■■‖]·■可|●|』』■〗‖』‖』■Ⅷ勺||■■`{日

生成的°

对于第_种来源’数据加载是—种异步加载方式,原始页面最初不会包含某些数据’当原始页面

□■·

加载完后’会再向服务器请求某个接口获取数据’然后数据才会经过处理从而呈现在网页上’这其实

■刁‖

是发送了一个Ajax请求。 按照Web的发展趋势来看’这种形式的页面越来越多。甚至网页的原始HTML文档不会包含任

何数据’数据都是通过Ajax统一加载后呈现出来的,这样使得Web开发可以做到前后端分离’减小 服务器直接喧染页面带来的压力。

所以如果遇到这样的页面’直接利用requests等库来抓取原始HTML文档,是无法获取有效数据 的,这时需要分析网页后台向接口发送的Ajax请求°如果可以用requests模拟Ajax请求’就可以成





功抓取页面数据了°

所以,本章我们的主要目的是了解什么是A|ax’以及如何分析和抓取A|ax请求°

5.↑

什么是川ax

部分网页内容的技术。

对于传统的网页,如果想更新其内容,就必须刷新整个页面,但有了Ajax’可以在页面不被全部 刷新的情况下更新。这个过程实际上是页面在后台与服务器进行了数据交互,获取数据之后’再利用 JavaScnpt改变网页,这样网页内容就会更新了。

可以到W3School上体验几个实例感受一下:http://wwww3schoolcomcn/ajax/瑚ax-xmlhttpreques〔 send.asp。

‖.实例引入

测览网页的时候,我们会发现很多网页都有·下滑查看更多”的选项°拿微博来说’以我的主页 (https://mweibo.cn/u/2830678474)为例,一直下滑’可以发现下滑几条微博之后,再向下就没有了’

转而会出现一个加载的动画不—会儿下方就继续出现了新的微博内容,这个过程其实就是Ajax加 载的过程’如图5ˉl所示。

口】■■‖□勺‖』■‖|·‖●]」·|■]』■□】【〗』〗勺||』■‖‖‖■勺‖‖』■■〗‖|■引||」■|口●]‖』■】■■】■】■】‖·门』‖||■■■

Ajax’全称为AsynchronousJavaScnptandXML’即异步的JavaSc∏pt和XML°它不是_门编程 语言’而是利用JavaSc∏pt在保证页面不被刷新`页面链接不改变的情况下与服务器交换数据并更新

■■|□曰■■|■■





} | 勺〗〗』〗』

5.l

■尸凸‖巴■‖‖△■厂

■厂|·β_■尸‖■尸

β)‖ ■尸『凸尸‖■■=卜■「 【 = 尸

■β卜‖■[「■■「|■任‖‖■β『■『■「β ■■「伍■匹■厅「·‖





l75

这慧麓器像瞬嚣辙龋@删婶沁. 中却多了新内容,也就是后面刷出来的新

微博。这就是通过Ajax获取新数据并呈现 的过程。

2.基本原理 初步了解了A|ax之后’我们接下来详 细了解它的基本原理°从发送Ajax请求到

网页更新的这个过程可以简单分为以下3 步_发送请求、解析内容`喧染网页°

s叹厕阿0th7 ≡…………丁·醚°断… 台丁■艳S

旦震……=…

…拓

…≈←■■■■■一■

…≈=…=… i了: :$磁

田闭肢

;^

世‖

;霜

下圃分别详细介绍—下这几个过程.辫:;…( ′蕊,打;伞;…播1瘫』;〉欺; ;” ●发送请求



什么是Ajax

图5ˉl

页面加载过程

我们知道JavaSc∏pt可以实现页面的各种交互功能,Ajax也不例外,它也是由JavaSc∏pt实现的, 实现代码如下: γ日IXⅦ1httpj 1千(W1∩dOw.Ⅻ[‖ttPReqUe5t){

xⅧ1httP=∩e倒Ⅻ[砒tpReque5t()j }e15e{//code+orI[6、 I[5

x∩1http≡"酗∧〔tjveX0bje〔t(""i〔ro5o+t.Ⅻ[‖丁丁p"〉j }

x‖1∩ttP.o∩re己dy5tatecba∩ge=+u∩〔tio∩(){ i+(X们1∩ttp。ready5tate≡q88x‖1http.5tatuS==20O){ dO〔Ⅷe∩t.8et[1eⅦe∩t8yId("叮DjV").1∩∩er‖『川≡X"1∩ttp·re5pO∩5e丁eXtj } }

x‖1∩ttP.oPe∩("p05丁’0’"/ajax/"’tn」e); X川1http。5e∩d();

这是JaVaSC∏pt对AjaX最底层的实现’实际上就是先新建_个X‖[‖ttpReqUe5t对象X‖1∩ttp,然 后调用O∩readγ5tateC∩a∩ge属性设置监听’最后调用Ope∩和5e∩d方法向某个链接(也就是服务器) 发送请求。前面用PythOn实现请求发送之后,可以得到响应结果’但这里的请求发送由JaVaSC∏pt完 成°由于设置了监听,所以当服务器返回响应时, o∩ready5tateCha∩ge对应的方法便会被触发,然后 在这个方法里面解析响应内容即可。



‖.



卜 卜 b



●解析内容

服务器返回响应之后, O∩ready5tate〔∩a∩ge属性对应的方法就被触发了,此时利用XⅦ1bttP的

re5po∩se丁ext属性便可得到响应内容。这类似于Python中利用requests向服务器发起请求’然后得到 响应的过程°返回内容可能是HTML’可能是JSON,接下来只需要在方法中用JavaSc∏pt进一步处 理即可°如果是JSON的话,可以进行解析和转化。 ●演染网页

JavaScnpt有改变网页内容的能力’因此解析完响应内容之后’就可以调用JavaScrlpt来基于解析 完的内容对网页进行下-步处理了。例如’通过docⅧe∩t.get[1eⅦe∩t8γId().1∩∩er‖丁‖[操作,可以更 改某个元素内的源代码’这样网页显示的内容就改变了°这种操作也被称作DOM操作,即对网页文 档进行操作’如更改、删除等。

上面“发送请求,,部分,代码里的do〔u∏记∩t.get[1臼肥∩tById("∏V01γ").1∩∏er‖丁‖[=x川1‖ttp.re5po∩5e『ext

便是将ID为‖y0iγ的节点内部的HTML代码更改为了服务器返回的内容,这样"y01γ元素内部便会



呈现服务器返回的新数据,对应的网页内容看上去就更新了°

我们观察到’网页更新的3个步骤其实都是由JavaScrjpt完成的’它完成了整个请求、解析和喧 染的过程°

再回想微博的下拉刷新,其实就是JavaScriPt向服务器发送了—个匀ax请求,然后获取新的微博 数据’对其做解析,并喧染在网页中°

因此我们知道,真实的网页数据其实是一次次向服务器发送Ajax请求得到的,要想抓取这些数 据’需要知道Ajax请求到底是怎么发送的、发往哪里`发了哪些参数°我们知道这些以后’不就可 以用Python模拟发送操作’并获取返回数据了吗? 3.总结

本节我们简单了解了Ajax请求的基本原理和带来的页面加载效果’下_节我们来介绍下怎么分 析Ajax请求°

这里还以之前的微博为例』我们知道下拉刷新的网页内容由Ajax加载而得’而且页面的链接没 有发生变化,那么应该到哪里去查看这些Ajax请求呢? ‖.分析案例

此处还需要借助测览器的开发者工具,下面以Chrome测览器为例来介绍°

首先,用Chrome测览器打开微博链接httPs://mwelbocn/u/2830678474’然后在页面中单击鼠标

|」■】●]|‖||■■】■‖||■■□■||‖■■■』‖』■·】‖|』■■■

5.2勾aX分析方法

』】■司□■Ⅱ□■●∏□〗】·』■□■□●』■可‖‖】]·]‖‖句■】』γ‖■■】』司|■■】〗□|■司】‖‖■■‖叮‖|■‖刘{]·〗勺‖{」ˉ■司』‖」日

第5章Ajax数据爬取

l76

右键’从弹出的快捷菜单中选择‘检查”选项’此时便会弹出开发者工具,如图5ˉ2所示°

蛤→

辗…

←芭=

…″=……… ←_■

些鱼申

…■向…=



▲『四

■炉

|港≡ ←( _丫Irl◆鲤…叼潍磁鲤垫蟹墅鲤…堕塑碉…雏 ` …

m■

…尸~、 心=w亩≥■=≡….ˉ

…■

_

仓.…稚$沁,

=■…一■~_牵≡≡≡==≈■≈~

申喇匈凸罐



翘愚:……裂… 氨壶≡≡≡一…_

蛔 龋

ˉ尸=→■■…尸芯缨…口p…=…碑丁=←早一

壁箭— 蠕…… 』0的宁=哼二r

=■■…=■…″■…===≡=≡≡



m



_…、

…瞧…熟…

m

哟■m镇



霸=阀国



…..





m

翘=^q倔

一■■



翻潞醚…西m‘…… ∑





ˉ 器…"



… =.





∏…i…▲



=■司|』∏

图5ˉ2开发者工具

■Ⅺ|‖」■|』■

官翻m≈…鸿狞 ;缉…彰…嘛





≡■】■』·|」·|||□■】‖·{|■■‖■」凹]』Ⅵ■‖‖■■■」|■‖







攘翘蹿……’…

●e必柠…霉、…叮…铲钨勾■……尸…盯…v …■

前面也提到过’这里展示的就是页面加载过程中,测览器与服务器之间发送请求和接收响应的所 有记录° d

信息°

从图5ˉ3的右侧可以观察这个Ajax请求的RequestHeaders`URL和ResponseHeaders等信息。其 中RequestHeaders中有—个信息为XˉRequestedˉWlth:XMLHttpRequest’这就标记了此请求是Ajax请



|{ {(

事实上,Ajax有其特殊的请求类型’叫作xhr。在图5ˉ3中,我们可以发现一个名称以getIndex开 头的请求,其Type就为xhr’意味着这就是一个Ajax请求°用鼠标单击这个请求’可以查看其详细

求’如图5ˉ4所示°

□』■



5.2勾ax分析方法 [矗田………

l77

=—可

…皆~…~=气



§



●o■守…m、…●绅句辆~…●巴…尸…… -

-

「■「||卜|》[『》|[■尸|》|■∏‖∩|止■【【‖}■厂|卜■「‖β{厂『「||〖=■「·|■「卜「■厂「||■「尸‖『||■厂}|匹尸『‖|■尸卜||■【}|■■【||【■【『‖|■「『『【■『『●}[卜「‖‖【■『■■【【■『‖·「ˉ’

v≡

■ˉ .□…●tt房『〃■·妇止°酗/呻〃…0■1唾了′wt【…忱…唾■{……γ凹γ也mm1赋丁哇】…拘m…JQ …二凸^ˉ→唾丁 = .{●咱α

■ˉ 知

ˉ

. ˉ全

山·l邻01m.沁$“J

……=呵●丁7冶7一厂… v~…====

α =…11汪 -萨纱 叼了西…=…m

≡…■l“铱四′》…】…”t≈t↑心 …■9础J■…〗7;na■叮 ………·≈皿·■.■垦oc■ ………■·】Q· 山t■圭=q…〗〗…l0』锤汀0■γ伊O迪……7…?…0鼻

兰=●≡= ■工m硒

囤:=…

【……p吟J>沁〗0】∏30】·]m』≈=;田…/】……·m山.C

n8…1y

……四1画yo≡m.沂.坤妇·渔皿qm —仇…

…≈【呻…呻 =…7■yQ ~砷o●°u ■…←……

≡呻um』酝′』…0 t…/p贮酗‘ p/◆

…~■■印0“◆m·0b厂

图5ˉ3-个Ajax请求的详细信息 v…=……

…·卯l16■tjD们/】蛔′t唾t/plO皿0 中/中 -孵加0“?ut它o D「

…=2∩≈α‘mβ…°9pm;硝°80j■;哟徽70m~Ⅷ〗≈·0p威β剧·3 —…凶C油

≡h…u腮

…=L…酌M7E52m厄哩……〗巫=…—【m施IZo皿肛w厂t皿例蜘Jb…川7≡心m 』7中jbl〔·ˉ尘●…t巳■鳃mγ…w…c7Dt…■;甄P…wm£辆矽I酗…w∩$γ1【JQQ可Ⅲq∑7f…1促】Z厢

唾〔…■…w河γm曲Ⅷ…t0·『…绳1旧tⅨ=沮】“…i瞳=…γ】n【<……≈…织 硒9『旧=…3;眨【…=…〗11…”;∩乒〖……■↑呻t凹陋c……亚…`u止“ 的T1…〗】哪I?1=…■7…率y“7…0烃=?=起】“γ…』[…1酚酶】1 =■·酝血□C∩

……●c油 ~ntt碑8//∩.唾…写C∩/o/2…丁凹γQ

…№Z止u四3,0《由〔mmT∩; 】∏mlmc“x19=迪=乙)…{…犹/日刃·泌(刘『Ⅷt0uh●“C№』〔h厂…/03°●. 卫弱°酒2Sm■『蚌蹈7·泌 叮ˉ ˉ 兰…m川tt啊≈gt v……………饵咏田…

…Um ˉ

;…7隅70

. °

□飞1嘘0528…7“74

图5ˉ4XˉRequestedˉWith:XMLH忱pRequest标记 随后单击一下 苛单击一下Preview’就能看到响应的内容’如图5ˉ5所示°这些内容是JSON格式的’这里 Chrome为我们自酉 为我们自动做了解析’单击左侧箭头即可展开和收起相应内容。 v-凸…←.=△≡.…唾 -—~…_-一

_

●—-一◇_←

v{吐g 〗0峦t■: 《呻■7I肪0●8《四8…7“7q′■仁…=〗 .■庆才‖■n●’do≈》》 v由t■8 《B沁了加7O台《必且…硒7Q0 ●C……8 ■…‖■x■Q』0■} ↑…■c…:■‖tt·O8/m●唾…q叼Ⅳ』…?…■…千…z泌…u北…啼】匹W1中1…泊沮7醉γ铅?■tu睡p 『Ou吐B…E凶械t脚8〃■p出…·娟∩/7印tM陀/…l…/…T皿皿……1汹lf峰……7郴T把?鳃tu吨c… ·尸~Q 酚Z1…1m【〃…厂m「o加…m鳃7码丁鸽h』0亡蟹≡二=■q吗1?』仁10油墅褥塑7泌7峪?四tu吨〔……9沁■

巳…了呐81

尸t…m◆O【《蛤l“呵mh〗 】, t…〗{《t1【L·0 闻m■ot电…【鳞·宛?1le凹oc如t■』厢F蛔『 甸2m幻唾骗沁冯沁n0》’■』》 vuS●厂】Mo8《m8…7”4α■c……『唾…‖m●□』 吨t■冗m『■批tp■〖′…·p』m绚·c〃●刮……代□j…■3l锄1…它呵3·』闻腆 〔m约少l凶」8f■`韵 m■●7→…』·mTp■$〃w■1·■』佃纯·〔呵〔7呻···0·…p…口“●/…渔1tm甲1h〗W】』』2砷D枷■甲↑o』叮 山6仁了印t…8■CU纯』…』.c面 怕u吨…∩t〗7U9 ↑◎u■尸: ↑■u● 忙u南辱宇c…t8】们g fOu≈山■;0a【唾 …丁8洁0

川〗沁塑7议7』 1』赃日 「·l已·

u…:7·ue ■厂m色:●

■t…80

Ⅳ口↑n匙串r`0■ml田8〃…唾■坤纯●呵c呵它αα翱·辑o四′…●l……l仙〗2·c宰「甲·j”■ p”711幻』7l吕口htt田目/们·叼…o呵山……柏…幽』……】必1√1仁1…泌率7泌γ铅「c●【u门 9宅……0■…‖■E■ 凸t·tM0e田旧…tg硒

●.

图5ˉ5响应的内容

这里的返回结果是我的个人信息,如妮称`简介`头像等’这也是喧染个人主 经过观察可以发现’这里的返回结果是找的个人信思,如呢榔`间介` j∏t搏‖々至||i文些数据之后再执行相应的喧染方法’整个页面就喧染出来了° 页所使用的数据。JavaScnpt接收到这些数据之后,再执行相应的喧染方法’

■·‖ 』·二■则

第5章匀ax数据爬取

l78

另外’也可以切换到Response选项卡’从中观察真实的返回数据,如图5ˉ6所示: x…钩…|…宙』…沛呻咱 ■







盯寸∩飞 ■ ∩

■产 □b ■一





尸■呻 …

≡b■≡』且 电

二≡■ ■ ■

≈=



→□ =~←

归』b四LL

■■ ■

~几式■

户产L



^尸 ~ ■心由…





■ □



气~

■ p

…i{{雹磺:16.dam卿;{喇u蛇厂Ⅱ"↑°厕:《.」欲;劝…7U474′鹤sc旧e几侧…·:.\幽M\妈…M蝎2“\u4·2s\"975叭u田c5.,撼pm↑n詹ˉma

接下来’切回第一个请求,观察一下它的Response是什么’如图5ˉ7所示° =闸田…蜘=▲=

……|.ˉˉˉ



ˉ∏

▲ ●

▲-…

§

×

●●■v匡■飞白…凶…硷~■■…≡典□……U

=—空≡了‖…0 》矗三m幻●蔬方芭呵≡ˉ≡-盂藩≡= 陌—丙— … {回售~…宙廖=砷■ 于

=闯…?丛t≡

困浮四滓画 .……0mP

■罗=雪P宦皇

■鹤厕…回……耐《…

乙剖蕊百巳.—_0 bd=v…。=…w鹤……』≡

|■■



√→

<…臼『…向… ≡■……-l= _函一四 叮m>

me「…D

亡F囤0峪■《 …0庐■O0 ■【0 0…铭b0

…m【凹们』0

伯四0Q…$『 …幻t 『《剿t″】■■齿′户吨…‖■…’妒》〗『■‖ 『 ‖ 《》0 ≈0 00 …均8 QⅦ仓鸿◆掷『□

凹`〖……h呵·印ut‖0●c》【0】 》;

把净…■白『≈m仍〗"《》‖ √m7…

…………0…P唾≈』…绅(●0《…它缉∩《了》{』?(0‖丁]》面丛mt【「】.…@〗…r■r{厂I●《』;『pl』01驴0

蠢骡≡割{墅镊:蹿/僧:;攫舅:豁刮{日踪拐疆孺典爵簇箩@



2箩α=●p_

■=0■冯唾…↓啥鲍■…瑟

≤1绅N「0恃〃旧·0』″缚…刘都蛔 <1蝉■「0恃〃旧·■』唾呻冠…幻cO…°呵…鹰四■■心‖护mγ皿…●>

→ <』U……£m扣…呻=

…『… ■…·萝…门●…■《 ■q

■可司■■·□●■■习

……尸

幽心……

…鳃;{鳃{F鳃滁』酶闯瞬赡涵带瞬↓!o磕闭:…m…八……|…. …鳃;蹿{F鳃滁』″伊《

ˉ■可·■□‖

0≈壶=

■守~

■u吐盯哇0w劈B…′心·… <u哇呵汪吭F劈B…′心·已炉·勺△…沁UU1·】p】/但■■/凹■/c●咽.“●■>

』口

剿蹿……叭丛…画

沁毯沏瓣馋泌撼酗抛碑泌蜀捶水泌猫潞趴辑鳃泌撼瓣翻浑瓣■蝇僻心“

|田严忽“

■■‖‖」∩|‖■■可』■‖‖■■■■■〗‖‖■|』』■】■□]‖

图5ˉ6真实的返回数据



·凶□

|‖

图5ˉ7第_个请求的Response

所以说,微博页面呈现给我们的真实数据并不是最原始的页面返回的,而是执行JavaScnpt后再 次向后台发送Ajax请求’测览器拿到服务器返回的数据后进一步谊染得到的。

||·‖〈|刘‖‖■】‖‖

这是最原始的链接https:〃mwelbQcn/u/2830678474返回的结果’其代码只有不到50行’结构也 非常简单’只是执行了-些JavaSc门pt语句°



纠·■

■■■



γ

宅】

●冷



—=



→≡_—一■亨



“ m

w

翻 田=~≡…………"o…ˉm

亩ˉ 囱





m

m





m ‖…_呵~0w…匀m



函=…空………………≡m





m



_…



呼 图空叼侧:气…■…洞‘………愈…=爵 呻

蚤…ˉ.

熊芝{

二_墅蜜捌| 重翌: 撇||



… ■_尘≡≡唾宁些一O…°.…

l.





蜜…庐望

琵ˉ器}| 蹦 山■饥…}

$熄卜;}}



』■■〗■‖|||■■|

图5ˉ8所有A)aX请求

望…… ≡窑… …



■□‖■■||」··‖■□]|

团…咋……………………ˉ留



Ⅵ。. |…△

7』吧■…

■·

‘=’……每℃…

■■

…_.≡. =

←一=■

q

纠■可‖■|



ˉ/→…





腮 唾

=-

一…

些二……

凸弱甲宁…=……山……测…ˉ留 当‖=£■

…恫

…=≡

←_



…一叼

…_…

′=…

…一■

…~…

园ˉ●

邑● lp■■ …

|〈

利用Chrome开发者工具的筛选功能能够筛选出所有Ajax请求°在请求的上方有_层筛选栏’直 接单击xHR,之后下方显示的所有请求便都是Ajax请求了,如图5ˉ8所示°

□·■』句』·】

2过滤请求

』■■■可



53Ajax分析与爬取实战

l79

接下来’不断向上滑动微博页面’可以看到页面底部有一条条新的微博被刷出’开发者工具下方 也出现了_个个新的Ajax请求,这样我们就可以捕获所有的Ajax请求了。

随意点开其中_个条目’都可以清楚地看到其RequestURL`RequestHeaders、ResponseHeaders` ResponseBody等内容’此时想要模拟Ajax请求的发送和数据的提取就非常简单了。 图5ˉ9展示的内容便是我的某—页微博的列表信息° →……

″~≡→

匡…

`——

……

…洒…

………

田一●

臆◆

●‖A↑γ

γ





图5ˉ9某一个AjaX请求的具体内容

到现在为止’我们已经可以得到Ajax请求的详细信息了,接下来只需要用程序模拟这些Ajax请 求’就可以轻松提取我们所需的信息°

5.3

川ax分析与爬取实战

本节我们会结合一个实际的案例,来看 _下AjaX分析和爬取页面的具体实现。

↑.准备工作

开始分析之前,需要做好如下准备工作°

□□□

■『|『}■「‖‖‖任「巴∩|■『『||■尸卜「|■尸‖『||■【【||●[[|~■厂β‖||卜卜■『|||》【}|卜[『|「尸【卜『‖β‖[尸|■‖‖『|伊『|Ⅲ|■「卜卜|}‖「●|■■『『|



—_…-≡



安装好Python3(最低为3.6版本), 并成功运行Python3程序° 了解PythonHTTP请求库【它quests的基本用法° 了解AjaX基础知识和分析Ajax的基本方法°

k内容在前面的章节中均有讲解,如尚未准备好’建议先熟悉_下这些内容° 以上内容在前面的章节中均有讲解’

2.爬取目标

本节我们以一个示例网站来试验一下Ajax的爬取,其链接为: https://spal.scrape.center/,该 示例网站的数据请求是通过Ajax完成的,页面的内容是通过JavaSc∏pt喧染出来的,页面如图5ˉ‖0 所示°

田出…0吵介吁

●.■● O







吁女 由呵●4

●…0哼…·…

曰5cmp.

攒w;评 蹿鼎甲

95 ●

耐翱闯拿■·〃『洞常嚼 |钾…汹:■

一-

‖■■

■■■●■≡■

■■●■■■●…■■

≡崔冗岂~■==

晒孵…

p5

" R

片慧…………

9

图5ˉl0示例网站的页面

大家看着这个贞面叮能觉得似曾相识,这个网站不是第2章也列举过吗?其实不是_个网站。两 个网站的后台实现逻辑和数据加载方式完全不同’只有最后呈现的样式是一样的° 这个网站同样支持翻页’可以单击页面最下方的贞码来切换到下一页,如图5ˉll所示。 髓鞠可■.岭坤■

●B■ ◆







▲★力●

■尊q~…芦…

■…□=

\滁

泻上钢孕师.凹‖蛔●∩d●d刨pm∩m■■叫o◎C●■网◎ 画■●■

9·↑ ■

■次聪Fp撬柠仲

=ˉ 巴

≡■■■∑;

瓣憨靴c卜艳』泊

"! | 古



围*夕Vn却伯

q

=`

蛔….■大Ⅳ…寸甲T鄙仕W





≈■·●

迁役的鸟.丁b●∏■m撇H9颤『d$ (■■

Mq ≥

■日

Ⅱ l↑【

9?

酗…0£H



世ˉ钒

十糯|艺琶艺二.摔口°

砌刊瞎;‖报鉴田 u

必 坚

■。唾· ‘ `尸廖 · 0

唇|5ˉll





‖]□‖』司|』·‖]□]′□、」日■■|]■』旬|▲·√」·]ˉ·]||日』』可·、‖|‖‖』‖|■■』纠■划■■∏|司」日□■□〗』』□‖司‖刘』`‖|·」口」□纠·‖□口‖刽|副口■‖』勺(|』』□■〗"‖ˉ日』日|」■■∏则|■

馋瑟滥…|……`”

|·】■】〗‖』■∏■‖‖‖·■‖Ⅵ』■]■】‖』□」■‖‖`℃■

第5章Ajax数据爬取

l80

切换到第2贞 勺

||‖{(|

单击每部电影进人对应的详情页,这些页面的结构也是完全_样的’图5ˉl2展示的是《迁徒的鸟》 的详情页。

|} 5.3 Ajax分析与爬取实战

l8l

}|}

昌曰…写…嫌q酶彝嘲蹿龋翻…~…….……·禽.

+◆|G

办●

回scmp· ■一0

■肆■□凹

′—…=≡…写=司

d

迁徒的岛ˉ丁h●γram硼0g臼『饱

1[P£Ul〕L【 鹏;…UR

B





合∩台价u

…″汕蹿0h蝉



出山…茁蠢mm〃………呻寝∩逻及睡 熟人囊文帕宁●咖…锤m·华蝉巴南个羡宇宁二千皂≡



F嗡酮m· 庐





·~田″盯罕qm{耪严~

■■「}■「



■■h±…?■■■■■

·』|】】γ匡‖门】〗

攀`…″爷…唾。份酗n舆…匈…止丁▲壤 ■■刀Ⅶ】β□〗〗息‖占任【

}『尸|『′已



,?

唾喻辫·道嘲·酗绵.■±P钝分诌

|溺髓蔼介

」攘

骂≡=骂::?望了-一→

.…蹿_

吐『·】《 阎 ‖ Ⅺ { 尽 』 住 〖 『 隙 『 ‖ 伏 ‖ 』 划 ’ 簿 周 攀 〗 阐 藤 Ⅸ 书 ■ ‖ ‖ 】

′巴『『陛卜『||β

}了;

一=~旧→r出…共

—ˉ.…≡鱼怠=

-

_≡

可…_

巴■∏■|=■厂|『

图5ˉl2电影的详情页页面

伊‖|

此时我们需要爬取的数据和第2章也是相同的,包括电影的名称`封面、类别、上映日期` 评 分`

剧情简介等信息。

仕『 『 ′

■■[仍‖「

本节我们需要完成的目标如下° □分析页面数据的加载逻辑。

「}

□用requests实现A|ax数据的爬取° □将每部电影的数据分别保存到MongoDB数据库°

由于本节主要讲解A|ax’所以数据存储和加速部分就不再展开详细实现了’主要是讲解Ajax分 析和爬取的实现。

「|『

坠『

▲尸[‖

好’现在就开始吧°

|巴【〗【「『巳尸‖『‖‖■■‖‖∏

3.初步探索

我们先尝试用之前的requests直接提取页面,看看会得到怎样的结果°用最简单的代码实现一下 requests获取网站首页源码的过程’代码如下:



1们portreque5t5

‖》[■口|}‖『■「}β■|‖‖》|卜【『「{■「‖‖‖【■■∏|

ur1二 0http5;//5pa1·5Cmpe.〔e∩teI/| ∩t‖1=req0e5t5.8et(uI1)·text prj∩t(htⅧ1)

运行结果如下:

<!D0〔∏p[∩t‖1〉<ht∏11日∩g=e∩〉<head>〈‖eta〔har5et=ut千ˉ8〉<‖eta‖ttpˉeq0iγ=Xˉ0∧ˉ〔o们patib1e〔o∩te∩t="I[=edge"〉 <‖eta∩a∩`e=γ1ewport〔o∩te∩t=""1dth=deγi〔eˉwjdth’i∩itja1-5ca1e=1"〉<1j∩代Ie1=ico∩∩re千=/+avjco∩.i〔o〉<tit1e〉 5cmpe |№γje〈/tit1e〉〈1j∩浪‖re于=/〔55/〔加∩|(ˉ7oo+7oe1.1126do9O.〔55re1=pre十etch〉<1i∩促hre千≡/〔55/〔∩u∩|(ˉd1db5eda. o仟76b36.c55re1≡pre十etch〉<11∩促∩re「=/j5/〔‖u∩{〈ˉ70o于7oe1.0S48e2b4.j5re1=pIe十et〔h)〈1j∩|〈∩re十=/js/d`u∩Ⅷˉ d1db5eda.b56』504d.j5re1≡pre十et〔∏〉〈11∩代hre千=/〔55/app.ea9d802日°c55re1二pre1oada5=5tγ1e×1j∩|(hre千≡/j5/app。 1435e〔d5.j5Ie1≡pre1oad己5=5cr1pt〉〈1i∩|(‖re十=/j5/c∩u∩kˉγe∩doI5。77da+991。〕5re1=pre1oada5=5cript〉〈1j∩促 ‖re+=/〔55/日pp.ea9d802a。〔5sre1=5ty1e5∩eet〉〈/‖ead〉〈body〉〈∩o5〔I1pt〉〈5tro∩g〉"e0re5orrybutport日1does∩|twoI|( proper1ywjthout]aγ日5〔rjpte∩ab1ed. p1easee∩ab1ejtto〔o∏tj∩ue°</5tro∩g〉</∩o5〔ript〉〈djvid=app×/diγ〉 〈5〔ript5r〔=/j5/〔hu∩|《.γe∩dor5.77da+991.js〉〈/5cript〉<script5rc≡/j5/app.1435ecd3。j5〉〈/5〔ript〉〈/body〉〈/htⅦ1〉

可以看到’爬取结果就只有这么_点HTML内容’而我们在测览器中打开这个网站’却能看到如 图5ˉl3所示的页面°



]|』』』‖‖』弓■||‖□‖√马〗|(

第5章Ajax数据爬取

l82

份霹念 ≈



|||

回sc霞… 敬



馋匿毯…』腮’…`“

95 山亡

, ■

·

|露瓣戳鹏霄…





■—一=





≡骂辩j缀瓣

ˉ



….

ˉ` ˉ

臼,



0

·{』

睡静…__,瓦 \\ N

`鞠

h

≈呵…

_~



G缓…… =

一=

ˉˉ一_一—ˉˉ

句\p~≡

¥ {

| 0



■■习■Ⅷ{{

图5=l3在测览器中打开示例网站呈现的页面

在HTML中,我们只能看到源码引用的_些JavaSc∏pt和CSS文件’并没有观察到任何电影数据 ‖

信息°

据爬取吧。

4爬取列表页

首先分析列表页的Ajax接口逻辑,打开测览器开发者工具’切换到NetwoIk面板,勾选pTeseⅣe Log并切换到XHR选项卡,如图5ˉl4所示。

■一-寇≡辩瓣 ■汾力蜀●!

回墅°p.

膨 坊王铡经 腮除P

■王别姬ˉ「乙阳w训MyC◎∏Cu刨佣● ●●

9.5 台◆☆古白

中……■w↑金尸 ↑靶m奎“上蚀

_

塞…≡ 台0△●邑 ■

触·●ˉ

磋…

田●≈

9·s ■■〃

0

≡勾

曹猛娜宣:铲…萄…气岛沁ˉ…窄…塞

@凸 b己≈

;|

ˉ=| .ˉ} | —

图5ˉl4列表页的Ajax接口

≈印

勺(|(‖·]」·】」」司‖』】■|」■|‖□|□□‖‖‖』]■|‖‖』■〗‖‖‖‖』■|||●凶|』‖叫|司‖‖|」』■∏‖』■■Ⅵ‖‖』】■‖]{」】】』∏」』■】‖‖』』■】■‖□〖卫■■‖□□‖■■‖』』』■■】|』』■■■Ⅲ

在52节,我们已经了解了Ajax分析的基本方法’下面一起分析-下Ajax接口的逻辑并实现数

引‖‖|」■■

口,再获取数据就好了。

■ 习 □ | | ·

遇到这样的情况’说明我们看到的整个页面都是JavaSc∏pt谊染得到的,测览器执行了HTML中 引用的JavaSc∏pt文件, JavaScrjpt通过调用一些数据加载和页面喧染方法,才最终呈现了图5=13展 示的结果°这些电影数据一般是通过Ajax加载的’JavaSc∏pt在后台调用Ajax数据接口,得到数据之 后’再对数据进行解析并喧染呈现出来,得到最终的页面°所以要想爬取这个页面’直接爬取Ajax接

53Ajax分析与爬取实战

183

接着重新刷新页面’再单击第2页`第3页、第4页的按钮’这时可以观察到不仅页面上的数据 发生了变化’开发者工具下方也监听到了几个Ajax请求’如图5ˉl5所示° 宁蹿令

守√!嗡







龄戚惫

酞 些土

■ˉ■

≡=

仓白●§



厂|.■■■

锦≈

『早融

……

用女■璃裔∧c』枷…儡0鳃t5沁叮

8·9

酶●●■

●亡◆■

…′馋触铸 …?辑瓣鱼



饯『锄任 《

黔阑↑

0 □■■ A

5

J 凶 v俞 势 h



凶…←ˉ 琶 一_

_—一{





卢翱圭趟锚今p`尹牡







-=-铲 窑 ■

…p 割吞



芭-沁 …=≡皂芒

瞳…=

….0 …=0

″$川 …川

m

…彦? …,?

m

…萨? …铲?



……~ .

钧v

本′…钨=—…洼 .

广



…↑b$

扫岭=…≡…… `

.

—≡—

馏疆…§趣台′已q呻粕ˉ .

….0 ″q.T

m

…γ …p



…v …↑

-~鹊 =≡-ˉ ˉ

巾一.^喳

ˉ~′′-△~=≡白▲

m′…….≡ ˉ^ ˉ `ˉ

m

=- 攫…凹

′坐些←一= -~ 凸

…咯



审′……=- .=≡ r拦丝嚼 . —

≈…

·_





ˉ.≡空 m

霉垒 .舍

?—…….

舍≡ˉ =一= …△

…碱3r….\二.ˉ佃0m‘…融d

0a豺瓣扩“碑`≡苫

4

≈翠{糕! |) 就口瑟|‖ ■砂 碑≈! ! 蕊鹅{一 “出

汹徊} 广

p



由y……

…■叫吼宰

』:

§嚎

白≈—二ˉ盅罕 ≡ 挚…=



▲→■▲=∏

河…

■岳_







4…

… 富吼. ˉ . 兰` 萨. 靶印腮~气 』 ,『

■谷





〗■

→占



啦恃≈屯

= …

…熙军_



□~



j *…呻全二亏

§

p≡≡. o=m匡-≡ 『淀℃0=一≡

·室=■





= § =…′,^哦′^ γj…、^

熬“哗

舔『`^`…汗…≡

……



…=~…一



跨轴广…

…晶独…





锤…

…_…

睁…



啥…

摆臂

…凰

血〖迅窗

翅^翁

猿◆



蹿, £

?

ˉ凹■三…鞍亭!汹- {|

图5ˉl5开发者工具监听到了几个Ajax请求

我们切换了4页,每次翻页也出现了对应的Ajax请求°可以点击查看其请求详情’观察请求URL、 参数和响应内容是怎样的,如图5ˉl6所示° △



Q■见

■~

0=声涸=~■

~■—

坠它 ˉ =二

葱…=~………… =≡

避…0~

感=…赋h钾〖〃…·SC…·西m7/呻〃…四?l■1t■】幽?0■就翱

剿………p

鳖惟?…怕 q~~ 幽…~ a



△=宗—

『~…颤 卢

:≈≡二 二弓宁硒■ ?



≈- = ^≡ 二ˉ酗旦亏远==沉0蛔



;=≈·t…t■矿酗■……■≤矿鲍汕





D

=■。

舷泊←…一 《《二b宁ˉ乏≡宁≡←==

i◆=售旧



『诌氯鳃占『:?:……· !—庐竿■…日访

{…~立四′m;轴.·,■;…。·‘E妒咖》融.7,》·:≈.o′m“。9 i=…』蜘

!……如忙………】≥础=m…≡】加T=■比二9 ‖ f……3▲唾…·…

;……吕′…1·“…·…〃…$

:~■碱翱际0…o……』睦灯p…um=8面叮

{ …◇-沁 《≈~… |抛m卤…尊 ,四晦′a『啤6w函■§沁!

图5ˉl6任一Ajax请求的详情

为https://spalscrape.ceIIte∏apj/movie/ 这里我点开了最后_个结果, 观察到其Ajax接口的请求URL为https:〃spal ?limit=l0&offSet=40’这里有两个参数:_个是1jⅧ1t’这里是l0;—个是o仟5et’这里是40°

l84

第5章Ajax数据爬取

观察多个A|ax接口的参数’我们可以总结出这么一个规律: 11mt-直为l0,正好对应每页l0条



数据; o仟5et在依次变大,页数每加l, o仟5et就加l0,因此其代表页面的数据偏移量°例如第2页 的o仟5et为l0就代表跳过l0条数据’返回从ll条数据开始的内容,再加上1j∏1t的限制’最终页 接着我们再观察一下响应内容,切换到Preview选项卡’结果如图5ˉl7所示。 凹

抿 ↓…询

p面℃w……hj四■

γ帕由叮

噶…

□可』‖·‖」□■‖|(』□■‖‖●|

面呈现的就是第ll条至第20条数据。

q

T{cαmt: 】■…》 C山例t8 1佣

丁r咕细It∑: [{1d; 4】0 ∩■呕8 鳞萤火之N■’■um名 瞬萤火奶吐^』,p■}, 《1d8 02o ∩…: ,o出姐o,p ■l1■9; ■全仔o=》o^l ■l』出8 ■■火●杜^凹

了cm…厂』邯8 【■m刨, "E协Ⅷ, ■扫0 ■讶叮] 0吕 叫■w 18 00爱佰凹 2吕 ‘0动■凹 3目 "■灯G

仁ov台「: 凹http巴:〃p〗·唾1tuH∩.∏et/mγ屿/dc弱f沁↑5?■%凹dh3山70M田1哪9”∑6了0汹°』…6q比“dh=1●=1c‘0 1d: q1 ■1"u【e: 49

∩…8 ■■火乏由■

…L1$回■t日 ■拘1〗=·9=〗7钓 ●厂印1m5; [■日本,o〗

□‖■■‖』■□」□■·】‖‖‖|■■Ⅵ

T0;{m; ▲1β…: 枷蟹火之盅加O ■u●■吕 必钮火唾^"°~》



∑cO≈】 8·8

尸1日 {10; 4】0…8 凹唾阎0 ■um! "土■·p■}

凹小噬于咖0 ■u■S8 ■△巴口『 凹…√"0■》 ■舔佃p■u●3: "三乃q"0■》 闻大话添邀>大圣耍矗陀0 ■l皿■自 闻凡〔M』贮$e0dγmeγP■忧〗胸←〔』n0e把1l■拽p4 魄新蛔γ呻”’ ■l1■■; ■…0「■Sm印fe1m口0■》 "触不可及■U ●l虹98 ‖0I∩t◎凹〔hGbIe9回0■} ,c钥G欲切’ ■um8 凹…p沁M2t00p←}

图5ˉl7 ‖问应内容

素都是一个字典°观察一下字典的内容,里面正好可以看到对应电影数据的字段’如∩a们e、a11a5、





〔oγer`〔ategorje5。对比_下测览器页面中的真实数据’会发现各项内容完全一致,而且这些数据已



』■■■』‖』■

可以看到,结果就是_些JSON数据’其中有_个Iesu1t5字段’是—个列表,列表中每_个元

■(

卜28 {泅; q3′ ∩…8 ◆3: {1日8“p ∩a唾8 p▲; {1d; q50 ∏…8 p5; 《蛔; 46p ∩…8 P6;{泌;q70…8 ◆7;{m8 』80 ∩=8

■■]■当■‖‖■■可

0; 00日本切









经非常结构化了’完全就是我们想要爬取的数据’真的是得来全不费工夫。











这样的话,我们只需要构造出所有页面的Ajax接口,就可以轻松获取所有列表页的数据了°











先定义_些准备工作’导人_些所需的库并定义一些配置,代码如下:



·









mpoItreque5t5 i川Port1oggj∩8

·













1ogg1∩g.b己5i〔〔o∩十jg〈1eve1≡1ogg1∩g.I‖「O’ +or阳t=|%(a5cti‖∏e)5 ˉ%(1eγe1∩a"e)5:%(川e5Sage)5‖)











I‖0[X0肌≡ !httP5://5Pa1.5〔IaPe.〔e∩ter/aP1/Ⅷoγie/?1imt={11们1t}8o仟5et={o仟5et}{





·





这里我们引人了requests和logging库,并定义了|ogglng的基本配置。接着定义了I‖0[X0R[’







这军把11"jt和o仟5et预留出来变成占位符’可以动态传人参数构造_个完整的列表页URL。





下面我们实现_下详情页的爬取°还是和原来_样’我们先定义_个通用的爬取方法,其代码



def5〔mpe-ap1(ur1): 1Oggi∩g.1∩+O(05Cmpj∩g‰…‖’ ur1) try:



retur∩re5Po∩5e·j5O∩() 1oggj∩g。error(‖geti∏γa1id5t己tu5〔ode%5w∩i1es〔rapi∩g%50 ’ respo∩5e.statu5code」 ur1)



re5po∩5e≡Ieq仙e5t5.get(ur1) j+re5po∩5e·5t3tu5〔ode=20O:

□■■司二■]‖■

如下:

e×〔ePtreqUe5t5°【eqUe5t[X〔eptjO∩:

‖ ‖ | {

1oggj∩g.eIroI(!erroro〔〔urredwhj1es〔rap1∩g‰|’ ur1’ exci∩+o=『rue)







5.3勾ax分析与爬取实战

l85

这里我们定义了_个5Crape_api方法’和之前不同的是`这个方法专门用来处理JSON接口。最 后的re5po∩5e调用的是j5o∩方法’它可以解析响应内容并将其转化成JSON字符串° 接着在这个基础之上’定义—个爬取列表页的方法,其代码如下:

} △■厂‖【■∏||β

[I‖I丁=10

「}

de+5〔r日peˉ1∩dex(p己8e): (」r1= I‖0[XU肌.千Omat(1i爪it=1I‖∏’ O仟5et=[I‖∏*(pageˉ 1)) retur∩5〔rapeˉap1(‖I1)

这里我们定义了_个scraPe-j∩dex方法’它接收-个参数page’该参数代表列表页的页码° 凹「■■尸||『‖◇广

5cmpe—j∩dex方法中’先构造了—个ur1’通过字符串的千oI川at方法,传人1i"1t和o仟5et的值° 这里1jⅧjt就直接使用了全局变量[I‖I丁的值; o仟5et则是动态计算的’计算方法是页码数减一再乘 以1j"1t,例如第l页的o仟5et就是0,第2页的o仟5et就是l0’以此类推。构造好uI1后’直接调

卜‖『

用5〔mpeˉap1方法并返回结果即可。

这样我们就完成了列表页的爬取,每次发送Ajax请求都会得到10部电影的数据信息。

■■》|『▲■『卜■尸『》

由于这时爬取到的数据已经是JSON类型了’所以无须像之前那样去解析HTML代码来提取数 据’爬到的数据已经是我们想要的结构化数据’因此解析这-步可以直接省略啦。 到此为止’我们能成功爬取列表页并提取电影列表信息了°

△●厂■‖匹′■‖=■厂

5.爬取详情页

虽然我们已经可以拿到每—页的电影数据’但是这些数据实际上还缺少_些我们想要的信息,如 剧情简介等信息’所以需要进_步进人详情页来获取这些内容°

「}

单击任意_部电影,如《教父》’进人其详情页’可以发现此时的页面URL已经变成了https://spal. scra

pe.center/detail/40,页面也成功展示了《教父》详情页的信息’如图5ˉl8所示。 钟` `

尸h贴茸→



→…



船…ˉ—

_=_



α



=≡ .毛≡丁

咀卫===万

≡乙…响—=…、



≡=



h



≡甲…

_

囊瓮冉●;



份』|||























……



睡…″……^

冒…≈



…嚣惫露幽=…m=~…………. 图5ˉl8 《教父》详情页的信息

驴◆伯



〗=≈

五*



》|卜匹伊「卜|‖■·『●【[■『『『〖■「|‖「[∩||β「■「|皿●|●厂|巴止伊〖卜几

o

睦毋≈辆…褥…≡=唾≡占=→竿→ˉ…牵砖`ˉ `

「亏-

另外’我们也可以观察到开发者工具中又出现了一个Ajax请求’其URL为https://spal.scrape

cente∏apj/movie/40/,通过Prevjew选项卡也能看到Ajax请求对应的响应信息,如图5ˉl9所示° 灭啥………b…Y…

宁《妇;仰0 ∩…; 时彼父孕D●u■■】 ■№…?■t饰尸日■》

pⅨm门9 [《∩■$ ■马■.■兰■闻p吨℃? .■托DO肘γ』t◎CO厂l…铲o」0《≡; 闻冈爪°蹿牙.mⅥ; 邑诅冗尔佣』仁∩●Gt〔°厂l…对p宇》’■] ■1加■Z 性№…沟t忙尸

U〔■t…7』m■ [●酶0 ■■■‖ ·吕 ■m 1$ ■■叮

厄oV■厂8 ■∏Et脚〖〃馋·■1t酗肘.唾t/■咱』■/n蜗c田7…↑】乃7磅9C瑰…m1弗蹿.』…644打←沁=1〔" ◆d』「6亡tO「■『 [《…吕 ■冗…·呻·m鸳0…M 守00{…0■….酪·阴…o=》

■∏|‖■]』■‖‖■■∏』□可‖』二·□‖|』■ ■■可□(

第5章勾ax数据爬取

l86

…8 ■们t〖p窃:〃pⅡ·呻1t…°畦t/m勺1“蛔……7鳃∩归……7“游3·j硒Ⅱ…1了偏二止←1〔切 …: ●…,辆·旧″

■ 『

兰宁≡§了己b·牵琶了5硅万酞··m■(驰·臼巫询)■■平冗问■■砍…■,■■F浊从碑违的■■′…酗巳许2叫牢…护悼,只田人们=■.因力盛诌丁■妈■m 』gg心

◆■℃【O■『 ‖■htt碎://脚·哇』t…甸砷t/…屿/33■●021酗仁p∏…■m…5田酌307晦】■·】……1碑h←1≈1C痢p→] …uS…■t8 ■泌】>●4≡】炉 =

7画k8 73

■「吨儿m●; ‖■m酗! 0自 创m 0〔o恤8 0°■

…t“■m〗 户狗2碑3■7丁16SB0『51·7弹”Z■

图5ˉl9Ajax请求对应的响应信息

稍加观察就可以发现’Ajax请求的URL后面有一个参数是可变的,这个参数是电影的1d,这 里是4O,对应《教父》这部电影°

如果我们想要获取1d为5O的电影’只需要把URL最后的参数改成50即可’即h忱ps:〃spal.scIape.

centeⅣap″movie/50/,请求这个新的URL便能获取jd为50的电影对应的数据了。 同样’响应结果也是结构化的JSON数据,其字段也非常规整’我们直接爬取即可。

‖■γ■■』』■‖·■■日■‖■∏□可■■■‖‖二■■■」■ ■‖■■司‖‖‖

■m凹帕〗 〗79

…8 户■父■

现在,详情页的数据提取逻辑分析完了,怎么和列表页关联起来呢?电影1d从哪里来呢?我们 回过头看看列表页的接口返回数据,如图5ˉ20所示° _严≡







四←●



≤一ˉ■◎膨°。……″…蹿…………》忿磕……■…口》m……均°口……… m≈

m■

■≡

正=

…■

理<

…■

喳=

四≈

唾≈

m≈

睡=

m≈ m≈

洒硒 硒硒

≡= 垂≡

正早= m

■≡

-=犁" 友管啼……… ≡■

…q●

[…t8 1■ 《睡

呻~

;



. “市

守…■lw吕 l《凶〗 Ⅱ′…Ⅲ 剿■王m加p■l…? ■p…l1时…呻1″儡…}·【坦9 Ⅱo…E ■这沁平不x乃纳°·1』●■; ■〖…回·■}o=〗

丁00 《迪目 1D噎己 ■■…’ ●um; ■P■=l1殉〔…呻…■o→ ·l凹0: ■护●…u时c…凶1≈■ vm牢m■0 [·酝· ■m啪】 ·吕 ■南

■‖‖■■(〗□

ˉ !·《c…t2 1■Q=》

…龄0…=ˉ乞当



匹m

■■■■〗■■

■‖



~"_

■0■

■■■‖叮|‖■■

砸…—…

………………回…………

百α田…m叼□……℃釉…v琵T

】: ■″

} ‖







!

to〗

蜜自垫.

睁u赵硕8 ■】…7ˉ死口

.雁?翔″叮.≡扫函! 〗:·…矿

□■Ⅵ



…·厂:.向c…:′俩.■jt…。■U…』■c刨囱…■5玲…M怎可70%<拓2‘γ2.』…尸4吼l·-1z=

scn定$ 9·9

.1岛 (四2 2,…》.■↑锰砰太片.′·l』“: .….,』



◆2:《皿; 】.≡: .冉巾…」·l…: .……m厕■…f1D偷白.ˉl

造详情页的Ajax请求的URL就好了。



可以看到,列表页原本的返回数据中就带有1d这个字段,所以只需要拿列表页结果中的jd来构

日 」

图5ˉ20列表页的接口返回数据

□■

} §撇僵;嚣磁满撇臆险」

·』■



0

二■■■ ‖■■■

0

5.3勾ax分析与爬取实战

l87

接着’我们就先定义_个详情页的爬取逻辑,代码如下: 0[「AI[UR[= ‖httP5://5Pa1。5〔r己pe.〔e∩ter/日pj/卯oγje/{jd}0 . de十5〔rape—detaj1(1d); Ur1≡0[丁∧I[0R[.「or们at(id=id) retur∩5〔rape—日pi(ur1)

这里定义了一个5〔I己Pe-detaj1方法’它接收_个参数1d°这里的实现也非常简单’先根据定义 好的0[丁∧I[0R[加jd构造一个真实的详情页Ajax请求的URL’再直接调用5〔mpeˉap1方法传人这 个ur1即可。

『0『A1p∧C[=1O

「旧巳

|■厂[‖「■『·『|■厂卜|■『}『「■■「′|■『庐|■■β||■■【[||▲【■|[厂■「}■=[}●』卜|‖【■「卜「卜■‖β■「|伊|「|卜『‖■『‖「【■「广厂|【■「|巴■【■「「快|》『●『β|■■「|■尸|但厂||}

最后,我们定义_个总的调用方法,对以上方法串联调用’代码如下:

de「‖a1∩():

+orpage1∩ra∩ge(1’ 丁0丁A[PM[+1): j∩dexdat3≡5Crapeˉi∩dex(page) 「oIjte∏1∩ 1∩dexd日ta。get(』re501t5『): jd=1te爪get(01d|) det日j1data=5Crape-detaj1(id) 1oggi∩g.1∩+o(‖deta11d己ta%5』’ detai1data) 1+

∩日阳e

=≡

川日1∩

肌ai∩()

我们定义了—个Ⅷaj∩方法,该方法首先遍历获取页码page’然后把Page当作参数传递给

5CmPeˉj∩dex方法’得到列表页的数据°接着遍历每个列表页的每个结果,获取每部电影的id。之后 把id当作参数传递给s〔mpeˉdetaj1方法来爬取每部电影的详情数据’并将此数据赋值为 deta11data,最后输出deta11dat3即可。 运行结果如下:

2020ˉO3ˉ1902;51:55’981 ˉ I‖「0; 5〔rap1∩gbttp5://5pa1·5〔rape°〔e∩teI/apj/咖γje/?11mt=1O8o仟5et≡O… 2020ˉ03ˉ19O2:51目56’446ˉ I‖「0: S〔mpi∩g‖ttp5;//5p日1·5CIape。〔e∩ter/apj/∏℃γie/1…

2O20ˉ03ˉ1902;51:56’638 ˉI‖「0: detaj1data{0jd0 : 1’ |∩a∏e| : !霸王别姬!’ ‖a1ja5|; ‖「are"e11"y〔o∩〔|』bj∩e‖’ |〔oveI! : ‖‖ttp5://p0.|∏e1tu己∩.∩et/Ⅷγie/ceqda3e03e655b5b88ed31b5cd7896〔+62472。jpg刨6』w6“h1e1〔‖’

|categorje5|: [『剧怕|’ ’瓮怕』]」`regio∩5! : [|中囚大陆|’ `中囚杏港‖]’`a〔tor5! : [{′∩a‖∏e′: |张囚荣|’ ‖ro1e』: ‖程蝶衣‖’…}’…]’|di】e〔tor5‖: [{!∩a爬{ : ‖陈凯歌‖’ ‖加age|: 『http5://pO.爬jtua∩。∩et/『∏ovie/ 8十937225205o095o67e0e8d58e十3d939156』07。jpgβ128N17o∩1e1c|}]’ 』5〔ore! : 9.5’ 』Ia∩代! : 1’ {爪1∩l」te′: 171’ ‖dIa‖a0 : 0影片借一出《霸王别姬》的京戏’牵扯出三个人之间一段随时代风云变幻的爱恨‖阶仇°阻小楼(张千汲饰) 与程蝶衣(张国荣饰)是一对打小一起长大的师兄弟,…‖′ ‖p∩oto50 t […]’ 0p0b1i5hedat‖ : 0199〕ˉ07ˉ26』’ !updatedat, : |2020ˉo3ˉo7『16:31;36.967843Z‖}

2020≡O3ˉ19Oi:51:56’640ˉ I‖「O: 5〔rapj∩g∩ttp5://5pa1.5crape·ce∩ter/api/|∏oγje/2… 2o20ˉo3ˉ19o2:51:56’813 ˉ I‖「0: detaj1data{|1d‖: 2’ 』∩己们e0 : |这个杀于不太冷,’ ‖a1ja50 : 0[仑o∩‖ ’ 0coγer』: |http5://p1。雁1tua∩.∩et/mv1e/6bea9a千耳5Ⅱ4d+bdob668eaa7e187c3d+767253.jpg叫6绷6纠‖1e1c′’ |〔ate8oIjes′:

[ 』瓜‖悄』’`动作|’ 』犯罪』]’ 』regjo∩5』: [ 』法囚』]’ 』actor5』: [{』∩a"e』: 』让.岔诺』了』roIb』了|某品[eo∩』’…}’

…]’‖d1re〔tor5‖: [{!∩a们e! : ‖吕允.贝松0 ’ ‖mage0 : ‖http5://pO.|∏e1tua∩.∩et/加`/je/oe7d67eM3bd3372a 71↓093e8MOo28d40496.jpgβ128w170∩1e1c0}]’|5core‖: 9.5’ ,m"|(! : 〕’ ‖∏j∩ute0 : 110’ ,drama0 : ,里品 (让.岔诺饰)足名孤独的职业杀于,受入屉佣.一犬,邻居家小姑娘马蒂尔捻(纳塔丽.波特艾饰)敬开他的房门’ 妄求在他那里暂避杀身之祸°…|’ ‖p∩oto5|: […]’ 』pub115hedat‖: 0199』ˉ09ˉ14|’ !updatedat|: !2O2OˉO3ˉO7丁16;31:43.826235Z!} ●





由于内容较多,这里省略了部分内容°

可以看到,整个爬取工作已经完成了’这里会依次爬取每一个列表页的Ajax接口’然后依次爬

取每部电影的详情页A)ax接口’并打印出每部电影的Ajax接口响应数据’而且都是JSON格式。至 此,所有电影的详情数据’我们都爬取到啦。

6保存数据

好’成功提取详情页信息之后’下一步就要把它们保存起来了°第5章我们学习了MongoDB的 ●「「‖■「〖[|●「|

相关操作’接下来我们就把数据保存到MongoDB吧°

保存之前’请确保自己有_个可以正常连接和使用的MongoDB数据库,这里我就以本地localhost 的MongoDB数据库为例来进行操作,其运行在270l7端口上,无用户名和密码。

将数据导人MongoDB需要用到PyMongo这个库°接下来我们把它们引人_下’同时定义一下 MongoDB的连接配置’实现方式如下: ‖O‖C0〔0‖‖[〔∏O‖5丁RI‖C= 『 ! ∏o∩godb://1oca1∩o5t:27O17!

|·】‖·|』‖■·〗』′』·‖』』Ⅵ‖‖〈|‖』∏‖‖□】』‖日‖〈]·

第5章Ajax数据爬取

l88

ⅧMO二08ˉ‖酬[≡了Ⅷvje5』

Ⅷ‖凹〔0[[[〔∏0‖‖∧册[= !∏℃γie5|



1们pOrtpγ『『}O∩8O

〔1je∩t=pym∩go.日o∩go〔1ie∩t(舶‖COˉ〔0‖‖[〔∏0‖5『RI‖C) db=C1ie∩t[』帅vje5|] 〔o11e〔tio∩=db[ 0∏‖oγ1e5! ]



( 0

这里我们先声明了几个变量’如下为对它们的介绍。

{ q



□‖0‖C0〔0‖‖[〔丁I0‖5『RI‖C:MongoDB的连接字符串’里面定义的是MongoDB的基本连接信 息’这里是host` pon’还可以定义用户名`密码等内容° □‖0‖C008‖州[:MongoDB数据库的名称° □"0‖C0〔0[[[〔丁I0‖‖∧‖[:MongoDB的集合名称。

然后用‖O∩gO〔11e∩t声明了_个连接对象〔11e∩t,并依次声明了存储数据的数据库和集合° 接下来,再实现一个将数据保存到MongoDB数据库的方法’实现代码如下: de十5aγed己ta(data):

〔o11e〔tio∩.updateˉo∩e({ ‖∩a"e0 : d3ta.get(|∩a∏e0) }’{ ‖$Set! : data

}』 up5ert=『n』e)

( q

|| 』|

| q





| d



这里我们定义了一个5aγedata方法,它接收-个参数data,也就是上_节提取的电影详情信



息。这个方法里面,我们调用了update—o∩e方法’其第一个参数是查询条件’即根据∩a"e进行查 询;第二个参数是data对象本身’就是所有的数据’这里我们用$5et操作符表示更新操作;第三个

影数据°

注意实际上电影可能有同名现象,但此处场景下的爬取数据没有同名情况, 当然这里更重要的是

实现MongoDB的去重操作。 好的,接下来稍微改写一下Ⅷa1∩方法就好了’改写后如下: de+Ⅶai∩(): +orpage1∩Ia∩ge(1’『0『∧[pAC[+1):

j∩dexdata=5cr日pe-i∩dex(p己ge) 十orjte"i∩i∩dexd己ta。get(‖re5u1t5|): jd=ite川.get(‖id0) det己11data=S〔mpedetaj1(id〉 1og8j∩g·i∩fo(‖detai1data%50 ’ detai1data) 5己vedata(detai1dat己) 1Oggi∩8.i∩+O(‖data5aVed5U〔Ce55+U11γ′)

其实就是增加了对5avedata方法的调用’并添加了一些日志信息°

■|」」』■‖||■〗」■‖‖|』■■■〗‖‖』■占■〗‖|‖勺|』】■∏』·■■∏■■』■‖|』■■可|■】‖‖|』■□|』■■勺‖』】■■Ⅵ』』■】■■

参数很关键,这里实际上是up5ert参数,如果把它设置为True,就可以实现存在即更新’不存在即 插人的功能,更新时会参照第_个参数设置的∩a"e字段’所以这样可以防止数据库中出现同名的电

}| |『 ■「■■『}|

▲尸■『『【巴■『|‖■■尸

β|「》



5.3

A|ax分析与爬取实战

l89

重新运行,我们来看—下输出结果: 202Oˉ03ˉ1902;51:06’323 ~ I‖「0: 5〔rapi∩g∩ttp5;//5p己1·5cmp巳ce∩ter/日pj/川ovie/?1i「mt=108o仟5et=O… 2020ˉ03ˉ19o2:518O6’“0ˉ I肝0; 5crapi∩ghttp5://5p日1.5crape·ce∩teI/apj/Ⅶoγje/1… 2o2oˉ03ˉ1902:51;06,551 ˉ I‖「O: det日i1data{01d! : 1’ 0∩a∩e‖ : 0霸王别姬‖’ |日1ia5! ; !「己rewe11阶y〔o∩〔〔』bi∩e‖’

〔over′: ‖bttps://po.川ejtua∩.∩et/川ovje/ce4d日3eo3e655b5b88ed31b5〔d7896〔「62472.jpg@464w644h1e1c‖’

|categor1es` : [ 』剧』阶』’ |爱』肘』]』 』reg1o∩5|:[中国大陆|’ ,中国杏港|]’ 』aCtor5,: [{|∩a"e|:丁张国聚|了|ro1e』: ‖程蝶衣|’ ‖i‖age|: |http5;//p0.Ⅶeitua∩.∩et/Ⅶoγje/5de69己492d〔bd3十4b01』5o3d4e95d46c28837.jpg@128"17o们

1e1c0}’…」{|∩aⅦe‖ : |方征{ ’ 』ro1e‖: ‖螺客』’ |加age』: |http5://p1.川eitua∩.∩et/"ovje/39687137b23b〔 9727b47十d24bd〔〔579b97618°jpg@128w170∩1e1〔|}]’ ‖director5‖ : [{‖门己∏e| : !陈凯歌0 ’ 0m己ge』: ‖bttp5://pO。

户|β

Ⅷejtu日∩.∩et/『mγ1e/8+9372252050095067e0e8d58e千3d9391564o7.jpg@128w17oh1e1c|}]’ ‖5core‖ : 9.5’ 0Ia∩代0 : 1’ !m∩ute|: 171’ !dra∏a′ : !影片借一出《霸王别姬》的京戏’牵扯出三个人之间一段随叶代风云变幻的金恨怕仇° 段小楼(张丰毅饰)与程蝶衣(张国荣饰)是一对打小一起长大的师兄弟,两人一个演生,一个饰旦,一向配合 犬衣尤缝,尤其一出《霸王】|{姬》 ,史是替满京城’为此,两人约定合演一紫于《霸工别姬》.但两人对戏剧与人生 关系的理肝有本厕不同’段小楼深知戏非人生,程蝶衣别是人戏不分。段′|`楼在认为该成家立业之时迎娶了名妓菊仙 (巩俐饰) ,致使程煤衣认定菊仙是可耻的第二者’使段小楼做了叛徒, 自此’三人围绕一出《霸王别姬》生出的

■■‖■■「》■■■「β◆

发′|R怕仇战开始随着时代风云的变迁不断什级’终矗成怨剧· ! ’ |P们OtO5|: [ !们ttP5://PO.爪eitUa∩.∩et/ⅦOγ1e/ 45be438368bb291e5o1d〔523o92十0a〔8193424°jpg@106w1o6∩1e1c|’…’ ‖http5://p0.‖eitu己∩.∩et/‖oγie/ 0d95∑1o7429db3o29b64b千4十25bd762661696.jpgβ1o6w1o6h1e1〔0 ]」 0pub1i5bedat‖ : 01993ˉo7ˉ260 ’ !updatedat{ |2O20ˉO3ˉO7丁16:31:36967843Z!} ˉO刁ˉ1q02:51:O6·58刁 ˉˉ I‖「0: d日ta5aγed5ucce55「u11γ 2020ˉo3ˉ1902:51:o6》583 I肿「0: d日ta5aγed5ucce55「u11y 2020ˉ03ˉ1902:51:06』583 ˉ I‖P0: scrapi∩g‖ttp5://5p己1·5〔rape.〔e∩ter/api/‖oγ1e/R…

可以看到,这里成功爬取到了数据’并且提示数据存储成功’没有任何报错信息。

接下来’我们使用Robo3T连接MongoDB数据库看下爬取结果°由于我使用的是本地的MongoDB’ 所以我直接在Robo3T里面输人localhost的连接信息即可’这里请替换成自己的MongoDB连接信息’ 如图5ˉ2l所示。

c·m·cm′`— wm÷ (

—_卢=§]

}必醒≈臀…m …_.==

摊剖γ》跳

Ⅲ.】上蹿■

.

蛔d憾髓;



审翱→上\丁∧工上=:… 印酮↑y‖◎stm谢四『t饿棚●栅s◎Da6e『γ毡Ⅶ卜沁簿tca【》瓣镰汝α‖附4‖pv6◎「 感o旧蚀肌确a颓e·

〉[|卜‘尸||卜■■『β■口『|∩「‖|}『

眨■思Ⅱ『■■■■■巳Ⅷ口】』‖』〗泪习‖川习‖】‖俱】■□旧〗习叼】··

嗡缀碉键“”例鞠臆鳃勤

「卜·「

「「



▲■■■β■■「卜丛■尸‖匹■『【■『‖■■尸

同样’由于输出内容较多’这里省略了部分内容°

■…酗_ …饥



伊『}β‖■■【‖『||■■■厂|||△田【〖‖‖▲■∏『□『■■尸□「【田【【『‖‖■■■「‖‖止■■■■‖▲■■■■■■【■■【■

图5ˉ2l 输人MongoDB连接信息

连接之后’我们便可以在movies这个数据库中movies这个集合下看到刚才爬取的数据了,如 图5ˉ22所示。

[5

l90



第5章Ajax数据爬取

望宁?窟黑’』… …| 』……′ =



《■…》

●■们』





凶…

!蹿…‖ 韶咐=

F尸P≡

≡二■三ˉ



《$…『

●●→ 出网

■侧

β

蹿

■==宁

m





●渺心



■…

■≡

丙仪F





●… ≥中

■片■■出《■吞…》■…





●~←

{●治…!

●■…



…饼鲸理

=″

~■

〗凹…

!z…l 幽

==

】p■ ●

●凹

≡=

←>

尸■



B■■旧》……=

{爵麓} |嚼麓} 腮麓} }爵徽}

●凹睡乌…←…

【潞些●】

仑凹倒≡…… h凹(0)……= 卜■网……= 卜凹仍←■≈→…7噎0

≥■阐



q

q



] d





■…

嚼 龋

0

▲■





《∏…》

◆■侧



合印

《0…l

v■—



~幻



‖ 』















「』二赠田 …

图5ˉ22爬取的数据存人了数据库

可以看到’数据就是以JSON格式存储的,一条数据对应_部电影的信息’各种嵌套关系也一目 了然,同时第三列还标识出了数据类型°











这样就证明我们的数据成功存储到MongoDB里了°

本节我们通过-个案例体会了Ajax分析和爬取的基本流程’希望大家通过本节能够更加熟悉 A)ax分析和爬取的实现。 另外’我们也观察到’Ajax接口返回的大部分是JsoN数据’所以可以在_定程度上避免数据提 取工作’这也减轻了一些工作量°

0

本节代码参见: https://glthub.com/Python3WebSpider/ScrapeSpal。

■■||·」■司‖℃·」』‖■口■』■∏||』■|」=■·□■■■

7.总结

■可‖‖||』■■■■‖||‖|」■Ⅷ‖‖|□■■||■‖||」{|』■可‖|‖‖||凶■■∏■■■■■‖可」■■‖‖‖‖‖‖■■■』』■

虫 爬

止少

巴汁

■■■■■■■■‖肛询剪抖匪"导巳日

·□■〖■∏‖〗|『·■■■『〗□『■■厂‖(‖●{■「|》『■【『》『■[■尸『|【〗β■尸‖‖‖△■尸

|第6 章

■■■『『『}β|■■「}卜Ⅱ■丁『|‖巴尸|‖Ⅷ}■尸‖卜

我们知道爬虫是IO密集型任务’例如使用requests库来爬取某个站点,当发出_个请求后’程 序必须等待网站返回响应’才能接着运行,而在等待响应的过程中,整个爬虫程序是-直在等待的’ 实际上没有做任何事情。对于这种情况,我们有没有优化方案呢?

当然有,本章我们就来了解_下异步爬虫的基本概念和实现°





6.↑协程的基本原理

■尸『△■■尸■》|■■■‖卜△β伊‖△=口‖△尸

要实现异步机制的爬虫’那自然和协程脱不了关系。 ↑.案例引入

在介绍协程之前,先来看-个案例网站,地址为https://wwwhttpbinorg/delay/5,访问这个链接需 要先等待五秒才能得到结果,这是因为服务器强制等待了5秒时间才返回响应°

平时我们测览网页的时候’绝大部分网页的响应速度还是很快的’如果写爬虫来爬取’那么从发 出请求到接收响应的时间不会很长’因此需要我们等待的时间并不多。

然而像上面这个网站,发出_次请求至少需要5秒才能得到响应,如果用requests库写爬虫来爬 取’那么每次都要等待5秒及以上才能拿到结果°

卜『}『巴■卜『↑■尸

下面来测试一下,我们用requests写一个遍历程序,直接遍历l00次案例网站,试试看有什么效 果’实现代码如下:

价‖●「■■’仍『●「卜|■「■

1川POrtreque5ts j‖port1oggi∩g 1们pOrttme

1og81∩g·b日51〔〔o∩千ig(1eγe1=1og81∩g.I‖「0》

+Omat≡‖咒(a臼Ctme)5 ˉ%(1eVe1∩a|0]e)5:%(‖e55a8e)5‖)

丁0『AL‖叫B[R=1OO

0R止≡ 0‖ttp5://硼N.httpbi∩°org/de1ay/5! 5tarttj『∏e=t1爬·t1Ⅷe()

+or

1∩ra∏8e(1’ 丁0丁∧〔‖‖‖B[【+1): 1Oggj∩g.i∩十O(‖5CraPi∩g%5′’ 0只[) re5PO∩5e=reque5t5.get(UR[)

e∩dtj∏e=tj『∏e.t加e()

) 》|『

} |)叁尸『▲仔





6



广



1oggi∩8.i∩+o(『tota1t1"e%55e〔o∩d5` ’ e"dˉtj‖eˉ 5tarttjⅦe)

这里我们直接用循环的方式构造了l00个请求’使用的是requests单线程’在爬取之前和爬取之 后分别记录了时间,最后输出了爬取l00个页面消耗的总时间。 运行结果如下:

2020ˉo8ˉo3o1:o1:36’781ˉ I‖「0: 5〔r己p1∩ghttp5://脚"°httpbj∩。org/de1ay/5 2020ˉ08ˉO301:01;43’41oˉ I‖「0: 5〔mpi∩g‖ttp5://州·httpb1∩。oI8/de1aγ/5

k

l92

第6章异步爬虫

202oˉo8ˉo301:01:50’029ˉ 2o2oˉ08ˉ03o1:o1:36’7o2 ˉ 2020ˉo8ˉO3o1;o2:03’345 ˉ 2o20ˉo8ˉo3o1目02:o9’958 ˉ 2o2oˉo8ˉo3o1:o2;16’5ooˉ 202oˉ08ˉo3o1目02目23’143 =

I‖「0: I‖「0: 1川0: I‖「0: 1‖「0; I‖「0:

5crapi∩g们ttp5://www·httpbj∩°oIg/de13y/5 5crapj∩g∩ttp5://www。httpbj∩.org/de1ay/5 5〔r日pi∩ghttp5://www,∩ttpbj∩°org/de1ay/5 5crapj∩ghttp5目//www·httpb1∩。org/de1己y/5 5crapj∩g∩ttp5://Ⅳww.httpbj∩·org/de1ay/5 5crapi∩g们ttp5目//w棚·httpbj∩。org/de1ay/5

2o20ˉo8ˉo3o1:12:19’867 ˉ I‖「0目 s〔rapj∩g们ttp5://www·bttpbi∩·org/de1ay/5 2020ˉo8ˉ0301;12:26’479ˉ I‖「0: 5〔rapi∩g∩ttp5://www.httpbi∩·org/de1ay/5 2020ˉ08ˉ0301:12:33’083 ˉ I‖「0: 5〔mpi∩g侗ttp5目//www°httpbj∩。org/de1ay/5 2020ˉo8ˉo301:12:39’758 ˉ 1‖「0; tota1ti‖∩e662.97644305229195e〔o∩d5

由于每个页面都至少要等待5秒才能加载出来’因此l00个页面至少要花费500秒时间’加上网



站本身的负载问题,总的爬取时间最终约为663秒,大约l1分钟。

■|』■

这在实际情况中是很常见的’有些网站本身加载速度就比较慢’稍慢的可能l~3秒’更慢的说不

定l0秒以上°如果我们就用requests单线程这么爬取,总耗时将会非常大°此时要是打开多线程或多 进程来爬取’其爬取速度确实会成倍提升.那么是否有更好的解决方案呢?

本节就来了解_下使用协程实现加速的方法’这种方法对IO密集型任务非常有效°如过将其应

0

用到网络爬虫中,那么爬取效率甚至可以提升成百倍。

2.基础知识

了解协程需要先了解一些基础概念’如阻塞和非阻塞`同步和异步、多进程和协程。

阻塞状态指程序未得到所需计算资源时被挂起的状态°程序在等待某个操作完成期间’自身无法 继续干别的事情’则称该程序在该操作上是阻塞的。

●非阻塞

程序在等待某操作的过程中’自身不被阻塞’可以继续干别的事情’则称该程序在该操作上是非 阻塞的°

非阻塞并不是在任何程序级别、任何情况下都存在的°仅当程序封装的级别可以囊括独立的子程 序单元时’程序才可能存在非阻塞状态°

非阻塞因阻塞的存在而存在,正因为阻塞导致程序运行的耗时增加与效率低下’我们才要把它变 成非阻塞的。

副√|』■】‖』‖|■司司‖|□司‖‖‖|』句‖‖·‖』·‖|‖■】·||·|■

执行上下文切换操作的核不可被利用°

‖‖

常见的阻塞形式有:网络I/O阻塞、磁盘I/O阻塞`用户输人阻塞等°阻塞是无处不在的’包括 在CPU切换上下文时,所有进程都无法真正干事情’它们也会被阻塞°在多核CPU的情况下’正在



||‖

●阻塞

●同步

例如在购物系统中更新商品库存时’需要用‘‘行锁”作为通信信号’强制让不同的更新请求排队 并按顺序执行’这里的更新库存操作就是同步的° 简言之,同步意味着有序° ●并步

为了完成某个任务’有时不同程序单元之间无须通信协调也能完成任务,此时不相关的程序单元

■■■■|■■■□=■|』=■■Ⅵ■可』·■■□‖■●{|□习■】司‖|』■司】】■

不同程序单元为了共同完成某个任务,在执行过程中需要靠某种通信方式保持协调—致’此时这 些程序单元是同步执行的。

■□【△■β〗【‖□巴■『『‖〗▲■「『|卜『卜『||‖【■『′「■厂|‖且■‖||‖~尸『

6.l

协程的基本原理

l93

之间可以是异步的°

例如’爬取下载网页。调度程序调用下载程序后,即可调度其他任务,无须与该下载任务保持通 信以协调行为。不同网页的下载、保存等操作都是无关的,也无须相互通知协调°这些异步操作的完 成时刻并不确定。

‖匹■尸‖[‖伊‖匹■厂‖■厂■厂|伊=砂β‖卜『卜■=’

简言之’异步意味着无序° ●多进程

多进程就是利用CPU的多核优势’在同_时间并行执行多个任务’可以大大提高执行效率° ●协程

协程’英文叫作coroutine,又称微线程、纤程,是-种运行在用户态的轻量级线程。

协程拥有自己的寄存器上下文和栈°协程在调度切换时’将寄存器上下文和栈保存到其他地方, ●厉■『△尸

等切回来的时候’再恢复先前保存的寄存器上下文和栈。因此’协程能保留上一次调用时的状态’即

所有局部状态的_个特定组合,每次过程重人’就相当于进人上—次调用的状态。

》『卜尸

协程本质上是个单进程’相对于多进程来说,它没有线程上下文切换的开销’没有原子操作锁定



及同步的开销,编程模型也非常简单°

■尸庐二巴厂■■■■■)似■■■『『〖广‖‖『匹■■|‖卜

我们可以使用协程来实现异步操作,例如在网络爬虫场景下’我们发出一个请求之后,需要等待 一定时间才能得到响应’但其实在这个等待过程中’程序可以干许多其他事情,等得到响应之后再切 换回来继续处理’这样可以充分利用CPU利其他资源,这就是协程的优势° 3.协程的用法

接下来’我们了解一下协程的实现。从Python〕4开始’Python中加人了协程的概念,但这个版 本的协程还是以生成器对象为基础°Python3.5中增加了a5γ∩〔` awa1t,使得协程的实现更为方便°

Python中使用协程最常用的库莫过于asyncio’所以本节会以它为基础来介绍协程的用法°

●》匹尸〉|〖‖β■尸

首先,需要了解下面几个概念°

□eγe∩tˉ1oop:事件循环’相当于-个无限循环’我们可以把一些函数注册到这个事件循环上’ 当满足发生条件的时候’就调用对应的处理方法。

■尸『仔世β『|■▲■厂‖△■尸日∏卜『

□coroutj∩e:中文翻译叫协程,在Python中常指代协程对象类型’我们可以将协程对象注册到 事件循环中,它会被事件循环调用°我们可以使用a5γ∩〔关键字来定义-个方法’这个方法在 调用时不会立即被执行’而是会返回_个协程对象。

□ta5代:任务,这是对协程对象的进一步封装’包含协程对象的各个状态。 □十uture:代表将来执行或者没有执行的任务的结果’实际上和ta5代没有本质区别°

另外’我们还需要了解a5γ∩〔、awa1t关键字’它们是从Python35才开始出现的’专门用于定义

『「‖

协程°其中’前者用来定义一个协程’后者用来挂起阻塞方法的执行°

□‖

||

4准备工作

在本节开始之前’请确保安装的Python版本为35及以上,如果版本是3.4及以下’则下方的案 例是不能运行的。具体的安装方法可以参考: https:〃sempscrape.center/python°

安装好合适的Python版本之后我们就可以开始本节的学习了° 5.定义协程 勺』

我们来定义_个协程’体验一下它和普通进程在实现上的不同之处’代码如下:

》 =■尸■『伊■■厂



第6章并步爬虫

iⅦporta5y∩cio

己5y∩〔de+exeC(」te(x): pri∩t(』‖Ⅷber: 0’ x) 〔orouti∩e=e×ecute(1)

pri∩t(‖〔OrOut1∩e: 0 ’ COrOutj∩e) pri∩t(‖∧什erCa11j∩gexe〔qte0) 1oop=日5y∩〔iO.get-eγe∩t-1OOp() 1OOp.n」∩一u∩ti1-CO们p1ete(COrOutj∩e) pr1∩t(0∧+terca11i∩g1oop|)

运行结果如下: 〔oIout1∩e: <〔oro‖tj∩eobjectexecute己tOx1OM〔十83O〉 ∧代er〔日11i∩geXe〔ute ‖0们ber吕 1

∧+ter〔a11i∩81OOp

首先’我们引人了asyncjo包,这样才可以使用a5y∩C和a"a1t关键字。然后使用a5y∩C定义了 -个eXeCute方法,该方法接收一个数字参数X’执行之后会打印这个数字。 随后我们直接调用了execute方法,然而这个方法并没有执行’而是返回了_个corout1∩e协程

对象°之后我们使用getˉeγe门t=1oop方法创建了一个事件循环1oop,并调用1oop对象的 ru∩ˉu∩tj1ˉco们p1ete方法将协程对象注册到了事件循环中,接着启动。最后’我们才看到execute方

』】}】】●∏||』■Ⅵ·]〗勺‖■】‖』·』■】‖|‖]■司|‖』■■Ⅵ』』■】】■Ⅵ‖』■·]』∏】■■√‖‖●』』■‖」勺‖‖·‖叫`固‘|」■』■|」{■《

194

法打印出了接收的数字°

才可以执行。

|」』■■■∏‖‖■■■]

可见, a5y∩〔定义的方法会变成—个无法直接执行的协程对象,必须将此对象注册到事件循环中 前面我们还提到了ta5促’它是对协程对象的进_步封装,比协程对象多了运行状态’例如Iu∩∩j∩g` 十i∩15hed等’我们可以利用这些状态获取协程对象的执行情况。

下所示: mpOrt日5γ∩〔1O

●||‖||■|‖司‘‖

在上面的例子中’当我们把协程对象corouti∩e传递给ru∩一q∩tj1ˉ〔o‖p1ete方法的时候’实际上 它进行了-个操作,就是将corouti∩e封装成ta5k对象°对此,我们也可以显式地进行声明,代码如

日5y∩Cde十exe〔ute(X): pri∩t(‖‖‖Ⅷber: 0 ’ x) retur∩X

pIi∩t(|∧代erCa11j∩geXeCute|) 1oop=己5y∩〔jo.get-eve∩t-1oop() ta5k=1OOp·Createt己5促(〔Oroutj∩e) pri∩t(0『aS代: ‖ ’ t己5偶) 1Oop.rl」∩u∩ti1≡〔oⅦp1ete(t日5k) prj∩t(!『a5代: 0 ’ ta5代)

pri∩t(0∧+ter〔己11j∩g1OOp!)

运行结果如下: 〔oro0tj∩e: <coroutj∩eobje〔texe〔uteat0×10e0千7830〉 A千teI〔己111∩8exe〔l』te 丁a5R: 〈丫a5促pe∩d1∩gcoro≡〈e×e〔ute() ru∩∩i∩8atde∏o.py:4〉〉 ‖u∩ber; 1

「a5Ⅶ: 〈丁a5促十j∩j5∩ed〔oro=〈exe〔ute()do∩e’ de+i∩edatdeⅧ.py:4〉resu1t≡1〉 ∧+ter〔a111∩g1oop

·]」■||』‖■】||日■』‖‖■■』■∏|」‖‖‖|‖‖|□]||』■■】‖‖‖|·■■】‖|‖|』□]□■

〔orouti∩e=e×ecute(1) pr1∩t〈|〔oroutj∩e: ’ ’ coroutj∩e)



6』协程的基本原理

l95



这里我们定义了1oop对象之后’紧接着调用了它的〔re己teta5促方法,将协程对象转化为ta5R对 象,随后打印输出-下,发现它处于pe∩dj∩g状态。然后将ta5促对象添加到事件循环中执行’并再次 打印出ta5恨对象’发现它的状态变成了十1∩j5hed’同时还可以看到其re5u1t变成了1’也就是我们



定义的exeCute方法的返回结果。



定义ta5促对象还有另外一种方式’就是直接调用aSynCiO包的e∩5ure十uture方法’返回结果也 是ta5长对象,这样的话我们就可以不借助1Oop对象°即使还没有声明1OOp’也可以提前定义好ta5代



} ‖



0

P

p b





b









「 卜





对象’这种方式的写法如下: 1爪portasy∩cio

35y∩Cde十eXeCute(X); prj∩t(0‖l』ⅦbeI: | ’ X) retur∩x

〔OrOuti∩e=exe〔ute(1)

p工j∩t(}〔Orouti∩e: ′ ’ Corout1∩e) prj∩t(′∧代er〔a11j∩gexe〔ute!)

t35k≡己sy∏〔io.e∩50re+utl』Ie(〔oroutj∩e) pr1∩t(|∏a5代: | ’ ta5促) 1oop=a5y∩〔jo。getˉeγe∩tˉ1oop() 1oop.n』∩u∩tj1-〔o呻1ete(ta5R) pr1∩t(丁a5代; 0’ taS促) prj∩t(0∧「terCa11i∩g1OOp!〉

运行结果如下: 〔oroutj∩e:〈〔orouti∩eobjectexecuteatOx10aa3383O) A于ter〔a111∩geXeC0te 「a5低: <丁a5代pe∩dj∏g〔oro≡<exe〔ute()ru∩∩j∩gatde咖.py:斗>〉 ‖uⅧber: 1

















} p











『as代: <丁a5k千j∩i5∩ed〔oro熏〈exe〔ute()do∩e’ de十j∩edatde∏npy:4)resu1t雪1〉 ∧代erCa11i∩g1oOp

可以发现’运行效果都是一样的° 6.绑定回调

我们也可以为某个ta5|(对象绑定_个回调方法°来看下面这个例子: i∏mrta5γ∩〔jO m仰rtreque5t5

a5γ∩〔de千reque5t(); uI1= 0httP5://哪.baidu·〔刚0 5tatu5=reqUe5t5.get(ur1) retuⅢ∩5tatu5

de↑〔a11ba〔k(ta5k):

prj∏t(!5tatu58 !’taS仪.reBu1t()) coroutj∩e=reque5t()

ta5仅=a5γ∩〔jo.e∏到re千utl』re(〔Omuti∩e) ta5促。adddo∩e〔a11ba〔【(〔a11ba〔k) prj∏t(’『ask: ,’ta毗)

1α|p=己sⅧ〔jo.get-eγe∏t≡1oop() 1α}p.Iu∩ l』∩ti1=〔呻1ete(ta5k) prj∩t(!丁a5k: ,’ t日5k)

这里我们定义了reque5t方法,在这个方法里请求了百度,并获取了其状态码’但是没有编写任

何prj∩t语句。随后我们定义了〔a11b己c代方法,这个方法接收一个参数,参数是ta5促对象,在这个 方法中调用Prj∩t方法打印出了ta5R对象的结果。这样就定义好了_个协程对象和一个回调方法。我

l96

第6章异步爬虫

们现在希望达到的效果是’当协程对象执行完毕之后,就去执行声明的〔a11baC仅方法°

那么两者怎样关联起来呢?很简单,只要调用adddo∩eca11bac代方法就行°我们将〔a11bac代方 法传递给封装好的ta5促对象,这样当ta5代执行完毕之后’就可以调用C己11baC促方法了°同时ta5低对 象还会作为参数传递给〔a11bac代方法’调用ta5促对象的re5u1t方法就可以获取返回结果了° 运行结果如下:

丁a5|《: 〈丁a5印e∩d1∩gcoro≡<Iequest()n」∩∩j∩gatde{∏o·pγ:5〉〔b≡[〔a11ba〔代()atde则o.py:11]〉 Statu5: 〈Re5po∩5e [20O]〉

丁a5|《: 〈丁己5|《 于j∩j5hed〔oro=〈reque5t()do∩e’ de十1∩edatde"o.py:5〉re5u1t=<佣e5po∩se [200]〉〉

实际上’即使不使用回调方法’在ta5代运行完毕之后’也可以直接调用re5u1t方法获取结果, 代码如下所示: 加porta5y∩c1o mportreque5t5

ret0r∩5tatu5

〔oro0tj∩e=reque5t()

ta5R≡己5γ∩c1o.e∩5ure十qture(〔oroutj∩e) prj∩t(丁aS长: ‖ ′ t己5低) 1oop≡日5y∩〔jo.get-eγe∩tˉ1oop() 1oOP.ru∩u∩tj1ˉCO"P1ete(ta5代) prj∩t({丁a5k; ‖ ’ ta5k) pr1∩t(!『a5促Re5u1t: 0 》 t日5促.Ie5u1t())

运行结果是-样的: 丁a5k: 〈「a5促pe∩dj∩g〔oro=〈reque5t〈) ru∩∩1∩gatde|↑)o·py:4〉〉 丁a5k: ≤丁a5低「1∩i5钉edcoro=αeque5t()do∩e’ de「i∩edatde们o.py:』〉re5u1t=〈Re5po∩5e [2o0]〉〉 丁a5kRe5u1t;〈Re5po∩5e [200]〉

■‖勺|』■』可‖■』】{‖‖〈口』||‖‖|∩』|』』勺‖|‖·■|」‖‖√

己5y∩〔de+reque5t(): ur1= !http5://删w。bajdl」·〔oⅦ| 5tatu5=reqUe5t5.8et(Ur1)

7.多任务协程

1Ⅷporta5y∩cjo 1∏portreque5t5

a5y∩Cde十Ieque5t(): ur1= !http5://硼w。bajdu·〔o∏ 5t己tu5=Ieque5t5。get(ur1) ret0r∩5t3t‖5

ta5代5= [a5y∩c1o.e∩5ure「uture(reque5t())千oI

1∩r日∩ge(5〉]

pr1∩t( !『a5低S: ‖ ’ t己S促5)

+ort己5痰j∩ta5代5:

pri∩t(0丁a5代【e5u1t: 』 ′ ta5低.re5u1t())

这里我们使用_个「or循环创建了5个ta5促,它们组成_个列表’然后把这个列表首先传递给

asyncio包的侧a1t方法’再将其注册到事件循环中,就可以发起5个任务了。最后,输出任务的执行 结果’具体如下:

■可■■‖‖‖‖|司]」■■纽·|二■(|』■可||·‖」可‖口日」』』

1oop=日5γ∩〔1o.get—eγe∩t_1oop() 1oop。ru∩u∩tj1ˉ〔o"p1ete(a5y∩〔1o.wa1t(ta5低5))

□】■】Ⅵ●||』■]|』■】Ⅲ■‖|」』■■■■■〗‖‖□·|』■日‖‖]』】』■可|」』■■■■‖|』■■■■■

在卜面的例子中’我们都只执行了_次请求,如果想执行多次请求,应该怎么办呢?可以定义-

个ta5k列表,然后使用asynclo包中的wajt方法执行。看下面的例子:



| }



p



P











0

6.l

协程的基本原理

l97

丁a5|《5: [〈丁a5{〈 Pe∩dj∩gcoro≡〈Ieque5t() ru∩∩j∩gatde|∏o.py:5〉〉’ ≤丁a5代pe∩di∩gcoro=<request() Iu∩∩1∩gat de‖↑o.Pγ:5〉〉’ <丁a5|(Pe∩dj∩gcoIo=〈reque5t〈)ru∩∩1∩gatde帅。py:5〉〉’〈「as|〈pe∩d1∩gcoro=〈req0e5t()ru∩∩j∩gat de‖)o.Py:5〉〉’〈丁a5代Pe∩di∩g〔oro=〈reque5t() ru∩∩1∩gatdeⅧo。py:5>〉]

丁a5代【e5U1t:〈Re5PO∩5e [2OO]〉 丁a5kRe5u1t:〈Re5po∩5e [2OO]〉 「日5促【eSl』1t: 〈Re5pO∩5e 「2OO]〉 丁a5浪Re5u1t:〈Re5PO∩Se [2OO]〉 「a5促Re5u1t:〈Re5po∩5e [2O0]>

可以看到, 5个任务被)|顷次执行,并得到了执行结果° 8.协程实现

前面说了好一通’又是a5y∩c关键字,又是corout1∩e,又是ta5长,又是ca11ba〔k的,似乎并没 有从中看出协程的优势,反而写法上更加奇怪和麻烦了?别急’上述案例只是为后面的使用作铺热. 接下来’我们正式看看协程在解决IO密集型任务方面到底有怎样的优势° 在前面的代码中,我们用_个网络请求作为例子’这本身就是_个耗时等待操作’因为在请求网 页之后需要等待页面响应并返回结果°耗时等待操作一般都是IO操作,例如文件读取`网络请求等° 协程在处理这种操作时是有很大优势的’当遇到需要等待的情况时’程序可以暂时挂起,转而执行其 他操作’从而避免因_直等待-个程序而耗费过多的时间,能够充分利用资源°

为了表现协程的优势,我们还是以本节开头介绍的网站https:〃wwwhttpbinoIg/delay/5为例’因为 该网站响应比较慢,所以可以通过爬取时间让大家直观感受到爬取速度的提升°

为了让大家更好地理解协程的正确使用方法,这甩先来看看大家使用协程时常犯的错误,后面再 给出正确的例子作为对比°

首先’还是拿之前的requests库进行网页请求,之后再重新使用上面的方法请求_遍: 1ⅧpOrt日5y∩C1O 1‖portreque5t5

G

广





P

p

i"Dorttj∏记 ‖

5t日rt=tjⅧetme()

a5y∩〔de千reque5t(): ‖r1= |http5://洲w。bttpbj∩.org/de1ay/5『 pri∩t(,‖a1tj∩g+or0 ’ ur1) re5po∩5e=req‖e5t5.get(ur1) pri∩t(℃etre5Po∩5e千rO‖’ uI1’ 0re5Po∩5e ’ re5Po∩5e)

ta5代s≡ [己5y∩〔jo°e∩5ure+utuIe(reque5t())十or 1oop≡a5y∩cio.get-eγe∩tˉ1oop() 1oop。Iu∩u∩tj1-〔o们p1ete(a5y∩cjo。"ajt(t己s代5))

1∩ra∩ge(1o)]



P









e∩d≡t加e.ti爬() pI1∩t(|〔o5tt加e: ’ e∩d ˉ 5tart)

泣里我们还是创建了l0个taSk’然后将taS代列表传给Wa1t方法并注册到事件循环中执行。 运行结果如下: ‖ait1∩g+or‖ttp5://w洲。‖ttpbj∩。oIg/de1日y/5

Cetre5po∩se千ro∏↑ http5://州.httpbj∩.org/de1ay/5re5po∩se〈Re5Po∩5e [2OO]〉 ‖ajti∩g+or∩ttp5://w刚°httpbi∩°org/de1ay/5







P







Cetre5po∩5e十Io们∩ttp5://州°∩ttpbi∩.org/de1己y/Sre5Po∩5e≤ResPo∩5e [2OO]〉 ‖aitj∩g「or∩ttp5://w硼·httpbj∩.or8/de1ay/5 6etrespo∩5e+ro『∏∩ttp5://州.∩ttpbj∩弯org/de1ay/5re5po∩se<pespo∩se [2O0]〉 ‖a1tj∩g十orhttp5;//www0httpbi∩·org/de1ay/5

Cetre5po∩5e千ro「∏∩ttp5;//州.httpbi∩.org/de1ay/5re5Po∩5e〈【e5Po∩5e [20o]〉 〔osttj爬: 66,64284q2oo13428



{}

l98

第6章并步爬虫

可以发现’这和正常的请求并没有什么区别’各个任务依然是顺次执行的’耗时66秒,平均个请求耗时66秒’说好的异步处理呢?

其实,要实现异步处理’先得有挂起操作’当一个任务需要等待IO结果的时候,可以挂起当前 任务’转而执行其他任务,这样才能充分利用好资源°上面的方法都是_本正经地串行执行下来,连 个挂起都没有’怎么可能实现异步?莫不是想太多了。

要实现异步,我们再了解-下a"ait关键字的用法,它可以将耗时等待的操作挂起让出控制权° 如果协程在执行的时候遇到aⅣajt,事件循环就会将本协程挂起’转而执行别的协程’直到其他协程 所以’我们可能会将代码中的reque5t方法改成如下这样: a5y∩〔de十reque5t(〉: ur1= 0http5://硼。们ttpbi∩°org/de1ay/5| pr1∩t(‖‖3jtj∩g十or0 」 ur1) Ie5po∩se≡a"aitreque5t5·8et(ur1) pri∩t(℃etre5po∩5e十ro∏’‖r1’ |re5m∩5e|’ re5Po∩se)

‖ajti∩g十orhttp5://州·httpbi∩°org/de1ay/5 "aiti∩g千orhttp58//w刚.httpbi∩。oIg/de1ay/5 ‖ajt1∩g+or∩ttp5://州°∩ttpbj∩.oI8/de1ay/S "aitj∩g「orhttps://础·httpb1∩.org/de1己γ/S

□|‖|‖』●】·】』‖■]‖‖

仅仅是在reque5t5前面加了_个关键字awa1t°然而此时执行代码’会得到如下报错信息:

■■■■【■■【■■■■■尸匹■【■尸卜卜‖』■‖』·】‖闪

挂起或执行完毕°



●●●

千uture:〈丁a5|〈「j∩i5hed〔oro=<reque5t()do∩e’de千1∩edatde|∏o.py:8>ex〔eptio∩≡丁ype[rror("obje〔tRe5po∩seca∩‖t beu5edj∩ 0a"日jt0 expre5sio∩")〉 丁m〔eba〔k(ⅧStreCe∩tCa111a5t): 「j1e ,0de『∏o·pγ阑’ 1j∩e11’ 1∩request re5po∩5e≡awa1tIeque5t5。get(ur1) 丁ype[rroI: obje〔tRespo∩seca∩|tbeu5edj∩ ‖3wait! expre551o∩

这次协程遇到a"a1t时确实挂起了’也等待了’但是最后却报出以上错误信息。这个错误的意思

是requests返回的Re5po∩5e对象不能和a"a1t_起使用,为什么呢?因为根据官方文档说明, await后 面的对象必须是如下格式之-:

□_个原生协程对象;

」■】∏‖‖‖■■■』】‖|』■■|‖|』·‖‘`■可‖‖司‖」■||』·Ⅵ‖|』‖』■‖《

「a5促ex〔ept1o∩wa5∩everretrjeγed

□_个由type5.〔oroutj∩e修饰的生成器,这个生成器可以返回协程对象; □由一个包含

a"ajt

方法的对象返回的一个迭代器°

有的读者可能已经发现,既然a"ajt后面可以跟—个协程对象,那么a5y∩〔把请求的方法改成协 程对象不就可以了吗?于是就代码被改写成如下的样子: i呻ortasy∩〔jo j呻ortreque5t5 i呻ortti‖论 Start=tj爬。tj眠()

a5y∩〔defget(ur1〉: retUr∩Ieque5tS.get(ur1)

‖`||』‖|‖‖‖|↑|」‖

aSy∩〔de「reql』e5t(); l』r1= !http5://….httpbi∩°oIg/de1ay/5| prj∩t(0‖ajtj∩gfor0’ 0r1) Ie5po∩Be≡aNaitget(ur1) prj∩t(0Cetre5po∩sefr咖|’ uI1’ 0re5po∩5e ’ respo∩5e)

■]‖】】□〗(』〗■可】□』』】■】‖|刁划∏‖||』■可|||』】■■〗‖‖』■□‖|

这里reqeusts返回的Re5po∩5e对象以上三种格式都不符合’因此报出了上面的错误°

||

卜|卜「卜}尸|‖尸「卜{厂【‖『厂『卜|■■卜|■}}●‖■■■尸|卜卜但厂仕「







6』协程的基本原理 tas促5= [己5y∩cjo°e∩5ure十uture(reque5t())+or

1oop=己5y∩cio.get-eγe∩t-1oop() 1oop。m∩‖∩tj1ˉ〔o呻1ete(a5y∩〔1o."ajt(t己5长5))

l99

j∩m∩ge(10)]

e∩d=tme。ti爬()

prj∩t(0〔osttme: 』′ e∩dˉ 5tart)

这里将请求页面的方法独立出来’并用a5y∩〔修饰’就得到了_个协程对象。运行—下看看: ‖己jt1∩g「oIhttp58//‖州。httpbj∩·or8/de1己y/S CetresPo∩5e「ro‖∏http5://州.∩ttpbi∩.org/de1ay/5re5po∩5e<Re5po∩5e[2OO]〉 ‖aitj∩g十orhttp58//州。httpbj∩.or8/de1ay/5 Cetre5po∩5e千ro『∏https://….∩ttpb1∩。oIg/de1ay/5respo∩5e<Re5po∩5e [2OO]〉 "ajtj∩g+orhttps://州°∩ttpbj∩.org/de1ay/5 ●





CetIe5po∩5e+r咖https;//…。们ttpbj∩.org/de1ay/5re5po∩5e〈∩e5po∩se [2OO]) ‖ajti∏g千or∩ttp5://…·∩ttpbj∩·org/de1ay/5 Cetre5po∩5e千r咖‖ttp5;//咖.httpbj∩.org/de1己y/5re5po∩5e<【e5po∩se [20O]〉 ‖ajt1∩g千or∩ttp5://w硼·‖ttpb1∩.org/de1ay/5 Cetre5po∩5e+ro‖↑ ∩ttp5://州。‖ttpbi∩.org/de1ay/5re5po∩5e<Re5po∏se [2卯]〉 〔o5tti‖吧8 6S°394437756259273

还是报错,协程还不是异步执行的,也就是说我们仅仅将涉及IO操作的代码封装到aSy∩〔修饰 的方法里是不可行的°只有使用支持异步操作的请求方式才可以实现真正的异步’这里aiohttp就派上 用场了°

9.使用a‖o∩hp ajohttp是-个支持异步请求的库’它和asyncio配合使用,可以使我们非常方便地实现异步请求 ≥

操作°

我们使用pip3安装即可: Pjp3i∩5ta11aiohttp

具体的安装方法可以参考: https://setupscrape.cente门aiohttp°

aiohttp的官方文档链接为https://ajohttpIeadthedocsjo/,它分为两部分,_部分是Client,_部分 是Server°

下面我们将ajoh卯投人使用,将代码改写成如下样子: j呻ort己5γ∩cjo j呻o∏aio∩ttp i呵】tti账

5t3It=ti贬.ti贬()

己Sy∩Cdefget(ur1)8 Be55iO∩=aiOhttp.〔1ie刑t5e55jo∩() re5po∩5e=majt5e55jo∩。get(ur1) 己腮jtⅢe5仰∏5e。teXt() a创ajt5e55io∩·〔1O5e() retur∩re5四∩5e

a5y∩〔de「requeSt(): ur1= 0∩ttp58//….bttpbj∩·org/de1ay/50 pⅢj∩t(|"aiti∩g十Or‖’ l』r1) reSpO∩5e=a切aitget(l』r1)

pⅢj∩t(,αtⅢe5po∏5e于r咖|’ uI1’ 0re£po∩Be,’ res仰∩5e)

ta5k5= [a5γ∩〔1o.e∩5ure千uture(reque5t())「or ●

1oop=己5y∏〔jo。get—eγe∩t-1oop()

1mp.ru∩u∩tj1ˉ〔o日p1ete(己5γ∩〔jo·wait(ta5低5)) ■■

e∩d=tj眶.tj贬()

prj∩t(!〔o5ttj爬: ’ e∩dˉ 5t日rt)

i∩ra∩ge(10)]



■(』

| 』 』 ■ | 」 □ 』 口

这里将请求库由requests改成了aioht‖p’利用aiohttp库里〔1ie∏t5e551o∩类的get方法进行请求’ 返回结果如下: ‖aitj∩g十or∩ttp5://哪·httpbi∩。org/de1ay/5 ‖己iti∩g十oIhttps://哪.httpb1∩°org/de1ay/5 "ajti.∩g于orhttp5://娜.∩ttpbi∩。org/de1己γ/5 ‖ajtj∩8十or∩ttps8//州。httpb1∩·org/de1aγ/5



6色{re5po∩5e+ro"|https://州.httpbj".org/de1ay/5re5po∩se〈〔1je∩tRe5po∩5e(http5://….∩ttpbi∩.org/de1ay/5)

·

[2O00Ⅸ]〉〈α"u1tiDi〔tproxy(|0ate‖: !5u∩’ O9∧ug2O2014:3O:22C盯|’ |〔o∩te∩tˉ丁ype! : |app1j〔atjo∩/jso∩ ′ !〔o∩te∩tˉ[e∩gth! : 0〕600 ’ 0〔o∏∩e〔tio∩0 g 0代eepˉa1iγe0 』 ‖Serγer|: !gu"icor∩/19·9。O0 ’ |A〔〔e55ˉ〔o∩tro1ˉ∧11o们ˉOrjgj∩, : |*, ′ |∧cce55ˉ〔o∩tro1ˉA11o训ˉ〔rede∩tja15‖ ; 0tIue0)〉

CetIe5po∩5e「Io『∏https://w0‖w.httpb1∩。org/de1ay/Sre5po∩se〈〔1je∩tpe5po∩se(https://哪.httpbj∩.org/de1ay/5)

[20O0氏]〉〈〔I"u1tiDictproxy(Date0 : |5u∩’ 09∧ug2O2OM:3O:22CN丁0 ’ ‖〔o∩te∩tˉ「ype0 : |app1ic日tio∩/j5o∩‖’ |〔o∩te∩tˉle∩gth|: 0〕6O0 ’ 0〔o∩∩e〔tjo∩0 8 ‖低eepˉa1jγe0 ’ !5erγer0 目 ‖gu∩i〔or∩/19·9.o! 』 ‖∧〔ce55ˉ〔o∩tro1ˉA11owˉOrig1∩|: 0*|’ 0∧〔ce55_〔o∩tro1ˉ∧11owˉ〔rede∩tja150 : 0true0)〉

d

□‖』



第6章并步爬虫

200

〔o5tt1∏e; 6.033240O79879761 q

成功了!我们发现这次请求的耗时直接由5l秒变成了6秒,耗费时间减少了非常多。

』■■·

这里我们使用了a"ajt,其后面跟着get方法°在执行10个协程的时候’如果遇到aWajt,就会 将当前协程挂起’转而执行其他协程直到其他协程也挂起或执行完毕,再执行下一个协程° 开始运行时’事件循环会运行第一个ta5代°对于第-个ta5促来说’当执行到第一个aWa1t跟

着的get方法时’它会被挂起’但这个get方法第_步的执行是非阻塞的’挂起之后会立马被唤醒’

‘』□■■司」□当■■‖■司‖·】

立即又进人执行’并创建了〔11e∩t5e551O∩对象。接着遇到第二个a"a1t,调用5e55jO∩.get请求方 法’然后就被挂起了°由于请求需要耗时很久’所以-直没有被唤醒,好在第-个ta5代被挂起了’ 那么接下来该怎么办呢?事件循环会寻找当前未被挂起的协程继续执行,于是转而去执行第二个 ta5Ⅸ’流程操作和第一个ta5k也是-样的,以此类推,直到执行第十个ta5k的5e551o∩.get方法 之后’全部的ta5代都被挂起了。所有ta5脆都已经处于挂起状态’那怎么办?只好等待了。5秒之 后’几个请求几乎同时有了响应’然后几个ta5k也被唤醒接着执行,并输出请求结果,最后总耗 时是6秒!

■』‖』‖■‖‖■』·

有人会说’在上面的例子中,发出网络请求后’接下来的5秒都是在等待,这5秒之内’CPU可 以处理的taS促数量远不止这些’既然这样的话’那么我们放l0个、20个`50个、l00个、l000个ta5促 一起执行’最后得到所有结果的耗时不都是差不多的吗?因为这些任务被挂起后都是—起等待的°

□■‖日‖』■■

怎么样?这就是异步操作的便捷之处’当遇到阻塞式操作时’ta5低被挂起程序接着去执行其他 ta5代’而不是傻傻地等着,这样可以充分利用CPU,而不必把时间浪费在等待IO上°

从理论上来说’确实是这样,不过有个前提’就是服务器即使在同—时刻接收无限次请求’依然

要能保证正常返回结果’也就是服务器应该无限抗压’另外还要忽略IO传输时延°满足了这两点, 现机制不同’可能某些服务器并不能承受那么高的并发量’因此响应速度也会减慢。 这里我们以百度为例,测试-下并发量分别为l` 3` 5、l0、…、 500时的耗时情况’代码如下: mporta5y∩〔io mpOrtajO∩ttp mpOrttj『爬

de+te5t(∩u刚ber): 5tart≡tme.t1∩e()

」‖|‖

a5y∩cde十get(ur1):

■ ■ √ 可 ■ □ ■ □ □ ■ ■ ■ ‖ 口 ■ 』 · ■ 可 · □ ■

确实可以做到无限个ta5k一起执行,并且在预想时间内得到结果。但由于不同服务器处理ta5促的实



‖』



62

}} 匹尸||卜‖■庐■|[■=厂







们 卜 》 ′■〖『‖』尸『【「||〖[■「■■》旦■「》『广、巴■■『||巴砂『|匹伊▲■厂

●尸『》■尸『‖[■『|‖||坠■厂|

■尸「■■∏□‖广「‖∩凸「●巨■∏



p

20l

5e551O∩≡ajOhttP·〔1je∩tSeSS1O∩() reSPO∩5e=a"ajt5e55iO∩.get(Ur1) awaitre5Po∩5e.text() 日Wait5e55jO∩.C1O5e(〉 retur∩re5po∩5e



0

ajohttp的使用

己5y∩〔de于reque5t(): ur1= 0http5;//洲w.ba1d0。〔oⅧ/0 aⅣajtget(ur1)

ta5|〈5= [己5y∩c1o。e∩5ure+uture(reque5t()) 十or

1oop=a5y∩cjo。get-eγe∩tˉ1oop() 1oop。ru∩u∩tj1=co∩p1ete〈asy∩〔1o。wait(ta5|《5)〉

j∩r己∩ge(∩uⅧber)]

e∩d=tme.t1爬() pri∩t(0‖Ⅷber: ! ’ ∩u们ber’‖〔o5tti脆8 ‖ ’ e∩d ˉ 5tart) ‖





千O r∩u们Der1∩ (

1’ 3」 5’ 10’ 15’ 3O’ 50’ 75’ 10O′ 2OO’ 500] te5t(∩Ⅷber)

运 行结果如下: ‖uⅦbeI: ‖Ⅷber: ‖0‖ber; ‖u们匪r8 ‖u‖ber; ‖ⅧbeI: ‖uⅦbeI: ‖‖‖ber: ‖u∏ber; ‖u们ber: ‖u‖ber:



1〔o5ttj『爬: o。05885505676269531 3〔o5tt1‖∏e: 0.05773782730102539 5〔o5tt1爬: 0.0576870qq14〕67676 1O〔o5tt1爬: 0.15174412727355957 15〔o5ttj‖e: 0°o96o3o95054626465 30〔o5ttj爬: 0.178431o34o8813477 50(o5ttme: o·374180o785064697 75〔o5tt1Ⅷe: o.2894289q9〕560791 1"〔o5tt1爬$ 0·61853814125O61o4 2卯〔o5tt1Ⅷe; 1.O89412927627S635 SoO〔o5tt1爬: 1.8213098049163818

6 k

可以看到’在服务器能够承受高并发的前提下,即使我们增加了并发量,其爬取速度也几乎不会 太受影响。 综上所述’使用了异步请求之后’我们几乎可以在相同时间内实现成百上千倍次的网络请求,把 这个运用在爬虫中,速度提升可谓非常可观。

↑0.总结

以上便是Python中协程的基本原理和用法’在接下来的62节中,我们会详细介绍alohttp库的 用法和爬取实战,实现快速`高并发的爬取°

本节代码参见: https://githuhcom/Python3WebSpideⅣAsyncT℃st°

6.2 a|o∩ttp的使用 在6」节’我们介绍了异步爬虫的基本原理和asyncio的基本用法’并且在最后简单提及了使用 ajohttp实现网页爬取的过程。本节我们介绍一下aiohttp的常见用法。 ↑.基本介绍

}卜【|

「‘■「}■|‖‖〖■■「『|■∏}}臼■■|止∏

前面介绍的asyncio模块,其内部实现了对TCP、UDP、SSL协议的异步操作,但是对于HTTP请 求来说’就需要用aiohttp实现了。

ajohttp是一个基于asyncio的异步HTTP网络模块,它既提供了服务端,又提供了客户端°其中’ 我们用服务端可以搭建_个支持异步处理的服务器,这个服务器就是用来处理请求并返回响应的’类

似于Django、Flask`TDmado等_些Web服务器°而客户端可以用来发起请求’类似于使用requcs临发 起一个HTTP请求然后获得响应,但requests发起的是同步的网络请求’aioht‖p则是异步的°



■■‖纠■‖』·

202

第6章异步爬虫

本节我们主要了解_下aiohttp客户端部分的用法。

我们来看一个基本的aioh仗p请求案例’代码如下: mpOItaiOhttp iⅧporta5y∩〔io

己5y∩〔de十千etc‖(5ess1o∩’ ur1): a5γ∩cNjth5e55io∩.get(ur1) a5re5po∩se:

retur∩a"aitrespo∩5e.text()’ re5po∩5e·5tatu5

a5y∩〔de十Ⅷ日j∩():

a5y∩〔Witha1Obttp°〔1je∩t5e55jO∩() a55eS5jO∩: ht∏1’ 5tatu5=己"日jtfet〔∩(5e55io∩」 !∩ttp5://cl」iq1∩8〔3i。coⅦ0) pri∩t(千!们tⅦ1: {们t‖1[:1o0]}…!) prj∩t(千|5tatu5: {5tat(』5}‖) j+

∩aⅧe

≡0

们己i∩

0:

1oop≡a5y∩〔io.get-eγe∩t-1oop() 1OOp°ru∩-u∩tj1-〔咖p1ete(阳aj∩())

这里使用aiohttp爬取了我的个人博客,获得了源码和响应状态码,并打印出来’运行结果如下: ‖m1: 〈|皿Ⅳp[‖丁肌≥ <‖t∏1〉

〈head〉

〈『‖eta〔har5et二同0「「ˉ8"〉

<∏论ta∩aⅧe="baiduˉt〔ˉγerj+j〔at1o∩"〔o∩te∩t=… 5t日t05: 2O0

由于网页源码过长,这里只截取了输出的_部分°可以看到’我们成功获取了网页的源代码及响 应状态码200’也就是完成了_次基本的HTTP请求’即我们成功使用aiohttp通过异步的方式完成了 网页爬取°当然,这个操作用之前讲的requests也可以做到°

可」■]』」勺‖」·]■〗‖」]■]‖』]司|削‖』■」刺|」■‖‖■司|口』刽甸·‘』■]‖`‖°■|」□|」●】】]||司|可||』·|」·`·

2基本实例

能够发现, aiohttp的请求方法的定义和之前有明显的区别’主要包括如下几点°

0

《|』」口||‖|■〗

□首先在导人库的时候,除了必须引人ajohttp这个库,还必须引人asyncio库。因为要实现异步 爬取,需要启动协程’而协程则需要借助于asyncio里面的事件循环才能执行°除了事件循环’ asyncjo里面也提供了很多基础的异步操作° □异步爬取方法的定义和之前有所不同,每个异步方法的前面都要统—加a5y∩C来修饰° □"1tha5语句前面同样需要加a5y∏c来修饰°在Python中, wjtha5语句用于声明_个上下文 管理器’能够帮我们自动分配和释放资源°而在异步方法中,"1t∩日5前面加上a5y∩〔代表声 明_个支持异步的上下文管理器°

值就是—个数值,因此前面不需要加己"a1t°所以’这里可以按照实际情况做处理’参考官方

事件循环’而事件循环需要使用asyncio库,然后调用ru∩-u∩ti1ˉ〔oⅦp1ete方法来运行°

注意在Python3.7及以后的版本中,我们可以使用asy∩cio.ru∩(∏aj∩())代替最后的启动操作,不 Python版本,依然显式声明了事件循环°



‖■||』■‖||∩{□《`□〗』

需妥显示声明事件循环’ru∩方法内部会自动启动一个事件循环。但这里为了兼容更多的



』·』』口|

文档说明,看看其对应的返回值是怎样的类型,然后决定加不加aWa1t就可以了° □最后,定义完爬取方法之后,实际上是"aj∩方法调用了+et〔∩方法°要运行的话,必须启用

‖司〈

□对于—些返回协程对象的操作,前面需要加a"ajt来修饰。例如re5po∩5e调用text方法’查 询APl可以发现,其返回的是协程对象,那么前面就要加a"ait;而对于状态码来说’其返回

62 aiohttp的使用

203

3.0RL参数设蕾

对于URL参数的设置,我们可以借助para∏5参数’传人_个字典即可,实例如下: 1ⅦpOrtajohttp 1‖porta5y∩〔1o a5y∩〔de十Ⅶaj∩():

p己raⅦ5={0∩己爬, : !ger『‖ey‖’ 』a8e|: 25}

日5γ∩〔wit‖ajo∩ttp.〔1ie∩t5e55io∩()a55e551o∩:

asy∩〔wjth5e55jo∩.get(!http5;//www.httpbi∩.org/get0 ’ para川s=para肌s) a5re5po∩5e: prj∩t(awajtre5po∩5e.text〈〉)

∩a爬

i十

==

爪a1∩

·

-

|日5γ∩〔1O.getˉeγe∩tˉ1OOp〈).ru∩u∩ti1_〔O"p1ete(们己i∩())

运行结果如下: { 00

3rg5"; { age00 ; ||2500’



00

"∩日爬": "gerⅧey"



}」 "∩eader5": { "∧〔Cept": ||*/*"』

"A〔〔eptˉ[∩codj∩g": "gzjp』 de千1ate"’ "‖o5t": "‖曲Ⅶ‖·httpbi∩。or8"’ "l」5erˉ∧ge∩t": "pyt∩o∩/3。7ajohttp/3°6.2"’ "】ˉA』∏z∩ˉ丁ra〔eˉId0‖: ′|Root=1ˉ5e85eed2ˉd2qo日〔90千4ddd十4ob4723e+o" }’ "or1g1∩!0 : "17.2O。255°122"’

"ur1": 』|bttp5://www。∩ttpbj∩°org/get?∩a‖∏e=gemey8age=25" }

这里可以看到’实际请求的URL为h忱ps://wwwhttpbinorg/get?name=germey&age=25’其中的参 数对应于pam∏5的内容。 4其他请求类型

ajohttp还支持其他请求类型,如POST、PUT、DELETE等’这些和requests的使用方式有点类 似,实例如下: 5e5sio∩.post(‖∩ttp://咖".∩ttpbi∩.org/post‖’ data≡b0d日ta)) 5e55io∩.put(|http://"硼.∩ttpbi∩。or8/Put0 ’ data=b‖d日ta0) 5e55jo∩。de1ete(|http://""w.httpb1∩.org/de1ete‖) 5e55iO∩°head(!httP://硼W.们ttPbi∩。Org/get‖) 5e5s1o∩.optjo∩5(!‖ttp://卿.‖ttpbj∩.or8/get0) 5e5s1o∩.patc‖(‖bttp://‖vww.httpbi∏.org/patch』’ data=b0d3t日『)

要使用这些方法’只需要把对应的方法和参数替换一下。

·

‖α



·

』α

∩a1∩



==

get一eγe∩t_1Oop(),n』∩u∩t11-〔o"p1ete(‖aj∩())



-

◎ 〔

∩a∩e

asy∩〔1O



1千



asy∩〔切1th5e551o∩.po5t(!∩ttp5://州.httpbi∩.oI8/po5t』’ data≡data)日5Ie5po∩5e pI1∩t(a"己1treSpO∩5e.teXt())

司」

a5y∩Cde+‖ai∩(): dat己={‖∩aⅧe0 : ‖gen∏ey』’ !己ge|: 25} 日5y∩〔"jthajo∩ttp.〔1je∩t5e55io∩()a55e55jo∩:

Ⅷ Ⅲ ◎







↑ ↑





°]

β·

·[



●■■→

Ⅲ ◎

1Ⅷporta5y∩〔1o

千‖



■■】▲



/×

己 〔

‖[

·]

·‖ ○□



勺」



旧·

我们可以用如下方式来实现:

丁γ







∩ ○

口[

对于POST表单提交’

广儿

的 中 头 求 请 的 应 对 其

5.尸OS丁请求



第6章并步爬虫

204

运行结果如下: { 0q

日rg5": {}’

"d己t日": ""’

"千i1eS"; {}’ "十or川"; { age": ,o25"’ ∩a爬": "gemey00 }’ "he己deI5": { "∧〔〔ept‖0 : 0『*/*"’ ‖

00

"〔o∩te∩tˉ[e∩gt∩0,: "18"’ 00

"‖o5t": ’0…·httpbi∩·Org 』

"0serˉ∧ge∩t": "pytho∩/3.7ajo‖ttp/3·6.2"’ "Xˉ枷z∩ˉ『mceˉId"; "Root=1ˉSe85「0b2ˉ9O17ea603a68d〔285e055艺d0"

}》 "j5o∩ : ∩011’ ,0origi∩": "17.2o°255.58"’ "ur100 : "http5://州。httpbi∩°org/po5t" 00

q





对于POSTJSON数据提交,其对应的请求头中的〔o∩te∩tˉ「ype为app1j〔at1o∩/j5o∩’我们只需 要将po5t方法里的data参数改成j5o∩即可,实例代码如下: a5y∩〔de+阳日1∩(): data= {{∩a『‖e! : ‖8emey 」 age0 ; 25} a5y∩〔wit‖a1ohttp.〔1ie∩t5e5s1o∩(〉 a55e551o∩: ∏

||

"〔o∩te∩tˉ丁ype": "app1i〔atio∩/xˉ哪ˉ+omˉ0r1e∩〔oded00’

当■Ⅵ■■乙■β■■日■可

"∧〔〔eptˉ[∩codi∩g`0 : "gz1p’ de+1ate"’

0

a5γ∩〔"it∩5e55jo∩.po5t(|bttp5://哪.httpbj∩.org/post‖’ j5o∩=data) a5re5po∩5e; prj∩t(awaitreSpo∩5e.te×t())

运行结果如下: { 0U

arg5": {}’ "d3t己": "{\"∩a∩e\』0 : \"gemey\"’ \"age\": 25}"’ "千i1e5": {}’ "千Om": {}’ "header5": { "∧〔〔ept00 : "*/*"’ "∧〔〔eptˉ[∩〔odi∩g": "gzjp’ de十1ate"’ "〔o∩te∩tˉ[e∩gth"8 "2900’ "〔O∩te∩tˉ「yPe"; "aPP11CatjO∩/j5O∩ 」 "‖o5t"; "咖·∩ttpb1∩.org ’ ′‖05erˉ∧ge∩t": ‖|pytho∩/3·7aio‖ttp/3。6。2"’ 00

00

"Xˉ柳z∩ˉ丁raceˉId": "Root=1ˉ5e85千O3eˉc91c9a2O〔79b978Odbed754O"

}’ 00j5o∩": { age": 25’ ∩日爬: ge加eγ" 『0

00

U0

00



′』

"or1gi∩": "17。20·25’°58"’ "ur1": !!http5目//…。∩ttpbj∩·org/po5t"





可以发现,其实现也和reque5t5非常像’不同的参数支持不同类型的请求内容° 6晌阿

对于响应来说,我们可以用如下方法分别获取其中的状态码、响应头`响应体`响应体二进制内 容`响应体JSON结果’实例代码如下: mpOrtajOhttp mpoIta5y∩〔jo



205

) (







△]



‖∩

尸↑



γ



臼 己

data= {0∩aⅧe0 : ‖ger们ey『 」 ,age‖ ; 25} a5y∩〔Wjtha1OhttP.〔1je∩t5e55jO∩〈)a55e55iO∩:

a5γ∩〔w1t∩se551o∩.po5t(‖‖ttps://www。httpbi∩.org/po5t` 」 data≡data) a5re5po∩5e Pr1∩t(‖5t3tu5: ’ Ie5PO∩5巳5t己tu5) pr1∩t(0们e日der5: ’ Ie5po∩5e.header5) pr1∩t(body: 0 ’ a"a1tIespo∩5e.te×t()) pri∩t(‖byte5: ‖ ’ a"ajtre5po∩5e.read()) Pr1∩t(!j5O∩: ’ aⅣaitre5PO∩5e.〕5O∩(〉)

∩日"e

j十

==

Ⅶ日1∩

:

己5y∩c1o.get—eγe∩t—1oop().ru∩u∩tj1-coⅧp1ete(Ⅶai∩())

运行结果如下: 』







·司】凸

°七

∩ ◎

□‖

′/

日 〔

□『

口]



0

÷」

5tatuS: 200

∩e日der5:〈〔I‖u1t1Dictproxγ(Date‖: 0丁hu’ O2∧pI2O2014:13:05C‖丁0 ’ "〔o「〕te∩tˉ丁ype |〔o∩te∩tˉle∩gt们! : |5O3|’ !〔o∩∩ect1o∩! : |keepˉa1jγe|’ !5erγer : gu∩jcoI∩/19.9.O! ’ {∧〔〔e55ˉ〔o∩tro1ˉ∧11owˉOr1gi∩0 : ‖*‖ ′ |∧cce55ˉ〔o∩tro1ˉ∧11o"ˉ〔Iede∩t1a15|: ‖trl」e′)〉

司」

『□【=■『『『|【■「}且厂〖【【■■=「||■■|‖|■【■「巳■尸β‖■■「仆阻厂|‖℃「■「【厂■「尸■厂‖「■「|‖■■|■尸卜任「|■▲「|ˉ■『’■「卜■【尸}■[尸凸『卜■■[尸■■『『■■‖仁

62 aiohttp的使用

0

body: { arg500 : {}’ "data||: ""』 "于j1e5": {}’ "干Or们": { 己ge": "25"’ ∩a爪e": ′|gerⅧeγ| 00

0】

00



)’

"header5":{ "ACCept": "*/*"』 "∧c〔eptˉ[∩〔odj∩g"; "gzjp』 de+1ate"’ 00〔o∩te∩tˉ[e∩gt∩"; ||18"’ "〔O∩te∩tˉ丁γpe": "app1iC日tiO∩/XˉWWWˉ十Omˉ皿1e∩COded"’ "‖o5t": "www·们ttpb1∩.org"’ "05erˉ∧ge∩t": "Pytho∩/3°7ajohttP/3·6·2||’ "X-Am∩-丁m〔eˉId『』; 00Root=1ˉ5e85十2千1ˉ+55326仟58o0b15886c8eo29"

)『』



仿『β『■『』【’▲■「卜『《■「|巴■’〖ˉ■厂巴‖仁|■『「|尸

}’ "j5o∩ : ∩u11’ "ori81∩": "17.2o。255。58"’ "ur100 : "http5;//www°httpbj∩.org/Po5t||

byte5:b’{\∩ "arg500 : {}’\∩ "d日ta‖{ : ""’\∩ "千j1e5": {}』\∩ 』干om": {\∩ "a8e": !』25"’\『) "∩aⅦe": "gemey"\∩ }′

\∩ ‖0header5"; {\∩ "A〔〔ept』』: "*/*』0’ \∩ "∧〔〔eptˉ[∩〔odj∩g||: "gz1p’de「1己te"’\∩ "〔o∩te∩tˉLe∩gth": ||18"’ \∩ "〔o∩te∩tˉ「ype": "日pp1i〔己tio∩/xˉwMγˉ千omˉuI1e∩〔oded"’\∩ "‖o5t": "洲w.∩ttpb1∩。org"’\∩ "0serˉ∧ge∩t||

"pytho∩/3.7aiohttp/3.6。2"’\∩ "Xˉ∧Ⅷ2∩ˉ丁mceˉId0: "Root=1ˉ5e83伍+1ˉ千55326仟580Ob15886〔8e0290!\∩ }′ \∩ "j5o∩!0 : ∩u11’ \∩ "origj∩": "17.2o.255.58"’\∩ "l」r1": "http5://w0州.∩ttpb1∩。org/post"\∩}\∩0 j5o∩g {,日rg5|: {}’ |dat己‖ :|‖ ’ !ˉ「j1e5‖ : {}’ 0+orⅧ』: {0age0 : ‖25‖ ’ 』∩a∩e』: 0gemeγ0}’`beader5‖: {!∧〔〔ept| 0*/*0 』 ‖∧c〔eptˉ[∩codj∩g0 : ‖gz1p’ de十1ate0 ’ !〔o∩te∩tˉ[e∩gth! : |18‖’〔o∩te∩tˉ丁ype :



app1j〔atjo∩/xˉ0Ⅷwˉ「omˉuI1e∩coded0 ’ 0‖ost‖ : |wvⅧ·‖ttpbi∩。org|’U5erˉAge∩t|: {pyt∩o∩/3·7日io∩ttp/3.6。2‖’

‖Xˉ枷z∩ˉ『mceˉId! : |∩oot=1ˉ5e85十2「1ˉ「55326仟58oOb15886〔8eO29|}’ ‖jso∩‖ : ‖o∩e’ 0orjgj∩‖ : 017。20.255.58‖’ !ur10 ; |http5://叫枷。httpb1∩.org/po5t0}

可以看到’这里有些字段前面需要加aWa1t’有些则不需要°其原则是,如果返回的是一个协程

对象(如a5y∩C修饰的方法),那么前面就要加awa1t,具体可以看aiohttp的API’其链接为:https://docs. aiohtmorg/en/stable/clientreferencehtml° 7.超时设冒

我们可以借助〔1ie∩t『iⅧeOut对象设置超时,例如要设置l秒的超时时间,可以这么实现: j川portajohttp 加pOrt日Sy∩〔iO

a5y∩〔de+∏a1∩(): t1‖∏eout=a1ohttp.〔1je∩t丁j‖eout〈tota1=1) .

己5y∩c"it‖日io门ttp.〔1ie∩t5e55jo∩(tmeout=ti爬o0t〉a55e55jo∩: a5y∩〔"1thse551o∩.get(!http5://Ⅶ洲.‖ttpbi∩·org/get0) a5re5po∩se:



6 k

第6章并步爬虫 ●



口[

)吕

口[







巳 巳 ∩

口○

巳 巳 Ⅲ



■■

△[

们aj∩



°[





== 0



β。

∩ Ⅲ

·]

∩a‖∏e



i「

0:

a5y∩Cio.get=eγe∩t-1ooP().ru∩~‖∩ti1ˉ〔咖P1ete(爪ai∩())

如果在l秒之内成功获取响应,那么运行结果如下: 之"

如果超时,则会抛出丁j爬o|」t[rror异常’其类型为a5y∩〔io.『i爬out[rror,我们进行异常捕获即可。 另外,声明〔11e∩t丁i"eo0t对象时还有其他参数,如co∩∩ect、5o〔|〈etco∩∩e〔t等,详细可以参考 官方文档: https://docs.aiohttp.oIB/en/stable/client-quickstart.hmIl扰imeouts° 8.并发限制

由于aiohttp可以支持非常高的并发量,如几万`十万、百万都是能做到的’但面对如此高的并发 量, 目标网站很可能无法在短时间内响应,而且有瞬间将目标网站爬挂掉的危险,这提示我们需要控 制-下爬取的并发量°

一般情况下,可以借助asyncio的5e"aphore来控制并发量’实例代码如下: j卯ort己Sy∩〔jo 加pOrtaiO∩ttp 〔删〔0RR[‖〔γ≡5

0∩[= 0http5://棚°baid〔』·〔o们,

5印ap∩oIe=a5y∩cjo.5e们aphoIe(〔0‖(0R【[‖〔γ) 5e551O∩=‖O∩e

a5y∩〔de十5〔mpe-己pj(); 日5y∩cw1th5e们aphore;

pri∩t(05〔rapi∩g0 ’ 0肌) a5y∩C"jt们5e55jo∩.get(0【[) a5re5po∩se awajt己5y∩〔io.51eep(1) retur∩a"a1tre5po∩5e。text(〉 aSy∩Cde十爪ai∩(): g1oba15e551o∩ 5e55io∩≡己jobttp.〔1je∩t5es5jo∩()

5cmPeˉj∩dexta5悯5= [a5y∩〔io。e∩5ure+utuIe(scrape-apj())十or

i∩r己∩ge(1O0O0)]

己waita5y∩〔io。8at∩er(*5crape-j∩dexta5促5) j卡∩a眠

== ‖

们己j∩

0 ;

a5y∩〔io。get-eγe∩t-1oop(〉.ru∩-u∩tj1ˉco们p1ete(‖Ei∩())

这里我们声明〔0‖〔0∩R[‖〔γ(代表爬取的最大并发量)为5’同时声明爬取的目标URL为百度°

接着,借助5e"ap∩ore创建了_个信号量对象’将其赋值为5e们aphore,这样就可以用它来控制最大并 发量了°怎么使用呢?这里我们把5e"ap‖ore直接放置在了对应的爬取方法里,使用a5y∩〔w1t∩语句 将seⅧaphore作为上下文对象即可。这样一来’信号量便可以控制进人爬取的最大协程数量,即我们 声明的〔0‖〔0RR[‖〔γ的值°

在"a1∩方法里’我们声明了l0000个ta5代,将其传递给gat‖er方法运行。倘若不加以限制’那 这l0000个ta5促会被同时执行,并发数量相当大°但有了信号量的控制之后,同时运行的ta5代数量

最大会被控制在5个’这样就能给aioh叮限制速度了。 9总结

本节我们了解了ajohttp的基本使用方法’更详细的内容还是推荐大家查阅官方文档,详见 https://docsaiohttporg/。

‖』■■Ⅱ〗‖〗■■‖‖|』□】‖』』■■■‖』‖■■‖|‖ ■ = ■ 可 ■ = ■ ■ ■ ‖ 』 】 可 』 ■ 】 〗 ‖ ‖ ` ● 】 』 ‖ 口 | ■ ■ ‖ ( 日 ‖ √ □ 曰 | 』 ■ □ ` 』 ■ 〗 』 ■ ■ □ · ■ 可 』 ■ ■ 勺 □ ‖ | ■ ■ 』 ■ | ■ 可 』 · | 』 · 」 口 | ■ 〗 ‖ | | ■ ■ Ⅵ · | { | ■ ■ 】 · □ ■ 】 』 ■ · 】 』 ■ | □ 】 ■ ■ ■ | 』 ■ 】 ‖ ‖ ■ 可 ■ ■ ] □ ∏ { 引 | ■ 】 | ‖

206





6.3

aiohttp异步爬取实战

207

本节代码参见: https://githuhcom/Python3WebSpjder/AsyncTest‘

▲厂β巴■厂卜『■ˉ尸『巴■|β▲■∏’∩’■巴尸‖■尸‖▲■

6.3 a|o∩ttp异步爬取实战 62节我们介绍了alohttp的基本用法本节我们完成异步爬虫的实战演练° ↑案例介绍

本次我们要爬取一个数据量相对大一点的网站,链接为htq〕s:〃spa5.scrapccenteⅣ,页面如图6ˉl所示。 °

送拘

■●0

回s。Dp· 蹿…

『≡

_



■■β‖■=尸》『■尸■■厅卜■■



鸯泉













……渔舟它″



唾 边



■庐■丛■『’▲■■「‖二尸『



…●■ 二■ =

}【

…Ⅲ

翱‖

孽 ′ ` 、 { .、′b H七伊



=』台率■h …



戳ˉ ■凸

‖ 尚|—

≡见~书

……

1





一=







……

:冤.



弓…蛔潭七……m,′……

ⅢqC酗匈■0

.蹿南层…馋啼■尸…■…

腮姆:″|专}二厂蕊藏 图6ˉl

—』

要爬取的网站页面

卜‖》

这是_个图书网站’整个网站包含数千本图书信息,网站数据是JavaSc门pt喧染而得的’数据可

以通过Ajax接口获取’并且接口没有设置任何反爬措施和加密参数。另外,由于这个网站之前的电

0

影案例数据量多—些,所以更加适合做异步爬取。 本节我们要完成如下目标:

■■尸巴■尸■■尸》■■『〖■■「∩■『厂

□使用aioh忱p爬取全站的图书数据; □将数据通过异步的方式保存到MongoDB中° 2准备工作

开始本节的探索之前’请确保你已经做好了如下准备工作:

□安装好了Python(最低为Python3.6版本’最好为3.7版本或以上),并能成功运行python程序;

|}

□了解了Ajax爬取的一些基本原理和模拟方法; □了解了异步爬虫的基本原理和asyncjo库的基本用法; □了解了aiohttp库的基本用法;

□安装并成功运行了MongoDB数据库,而且安装了异步爬虫库motor° ■ 厂 |

’■∏‖|‖▲■【尸■∏『尸卜|估■

关于最后一条’要实现MongoDB异步存储,离不开异步实现的MongoDB存储库motor’其安装 命令为: P1P31∏5ta11 |∏otoI

208

第6章异步爬虫

详细的安装方式可以参考: h仗ps://setup.scrape.center/motor° 做好如上准备工作之后’我们就可以开始数据的爬取了° 3.页面分析

第5章我们讲解了Ajax的基本分析方法’本节的案例站点和之前分析Ajax时用的案例站点结构 类似’都是列表页加详情页的结构,加载方式也都是Ajax,所以我们能轻松分析到如下信息。

□列表页的Ajax请求接口格式为https://spa5.scrape.center/api/book/?limlt=l8&o脆et={offSet}°其 中11"1t的值为每一页包含多少本书; o仟5et的值为每_页的偏移量’计算公式为o仟5et二

11Ⅷit*(Pageˉ 1),如第l页的o仟5et值为0’第2页o仟5et的值为l8’以此类推。 □在列表页Ajax接口返回的数据里’re5u1t5字段包含当前页里l8本图书的信息,其中每本书 的数据里都含有一个1d字段,这个jd就是图书本身的ID’可以用来进-步请求详情页°

□详情页的Ajax请求接口格式为https://spa5.scrape.centeⅣapl/book/{ld}。其中的jd即为详情页对 应图书的ID’可以从列表页Ajax接口的返回结果中获取此内容。

4.实现思路

其实,-个完善的异步爬虫应该能够充分利用资源进行全速爬取’其实现思路是维护_个动态变 化的爬取队列,每产生_个新的ta5促,就将其放人爬取队列中,有专门的爬虫消费者从此队列中获取 ta5促并执行,能做到在最大并发量的前提下充分利用等待时间进行额外的爬取处理。 但上面的实现思路整体较为烦琐’需要设计爬取队列、回调函数`消费者等机制,需要实现的功

能较多°由于我们刚刚接触aiohttp的基本用法’本节也主要是了解aiohttp的实战应用’因此这里稍 微将爬取案例网站的实现过程简化—下°

‖‖□ ■ | ■ ‖ ‖ 』 ■ 〗 ‖ ‖ { · 】 』 ■ ‖ ‖ ■ 】 ■ 〗 | ‖ ■ 司

如果你掌握了53节的内容’那么上面三点应该很容易分析出来°如果有难度’不妨先复习_下 之前的知识°





我们将爬取逻辑拆分成两部分’第一部分为爬取列表页,第二部分为爬取详情页°因为异步爬虫 的关键点在于并发执行,所以可以将爬取拆分为如下两个阶段°

□第-阶段是异步爬取所有列表页’我们可以将所有列表页的爬取任务集合在_起’并将其声明 为由ta5k组成的列表’进行异步爬取°

□第二阶段则是拿到上_步列表页的所有内容并解析,将所有图书的1d信息组合为所有详情页 的爬取任务集合’并将其声明为ta5促组成的列表’进行异步爬取,同时爬取结果也以异步方 式存储到MongoDB里面。

因为两个阶段在拆分之后需要串行执行,所以可能无法达到协程的最佳调度方式和资源利用情 况’但也差不了很多°这个实现思路比较简单清晰,代码实现起来也较为容易,能够帮我们快速了解 alohnp的基本用法。 5.基本配置



| o〈











( q

首先’先配置一些基本的变量并引人_些必需的库’代码如下:



j们pOrta5y∩〔io mPorta1o∩ttp iⅦpOIt1Oggj∩8

0|

1oggj∩g.ba51c〔o∩十18(1eγe1=1ogg1∩g.I‖「0’ 「OrⅦ日t=|%(a5〔t加e)S ˉ%(1eγe1∩a"e〉5:%(们e5Sage)50)

I‖0[X0R[= 0‖ttP5://5Pa5.5cmp巳〔e∩ter/ap1/boo代/?1j|mt=188o仟5et≡{o仟5et}|

0[「∧I[0Rl≡ |httP5;//5Pa5.5〔raPe.〔e∩ter/ap1/boo央/{1d}|

















6.3

aiohttp异步爬取实战

209

p∧6[ 5IZ[=18

二 〔0‖〔0RR[‖〔γ=5

这里我们导人了asyncio` ajohttp` loggjng这3个库,然后定义了loggjng的基本配置°接着定义 了URL`爬取页码数量p∧C[‖0‖B[R`并发量〔O‖〔0【R[‖〔γ等信息°

p









O

p ■厂|△■「仆|尸牌卜β。》|[∏·厂}付■「户■厂

匹■『伊》}‖『■「也『■】「卜巳尸





6.爬取列表页 第-阶段来爬取列表页,还是和之前一样,先定义_个通用的爬取方法,代码如下: 5eⅦ日p∩ore=a5y∩〔1o.5e阳phore(〔0‖〔0RR[‖〔γ) 5e551o∩=‖o∩e

a5γ∩〔de千s〔mPe_日Pj(ur1): a5y∩〔w1t∩5eⅦap∩oIe: trγ;

1ogg1∩g°1∩+o(05〔Iapj∩g%5|’ ur1) a5y∩CWjt∩5e551O∩.get(Ur1) a5re5PO∩5e: retur∩awaitre5Po∩5巳j5O∩() excepta1o‖ttp。〔1ie∩t[rror: 1ogg1∩g。erroI(0erIoro〔〔urredwM1e5〔rap1∩8%5‖’ ur1’ ex〔i∩十o=丁rue)

这里我们声明了一个信号量,用来控制最大并发数量。

接着’定义了5〔rape—apj方法,接收—个参数ur1°该方法首先使用a5γ∩C"1th语句引人信号量 作为上下文,接着调用5e551o"的get方法请求这个ur1’然后返回响应的JSON格式的结果°另外, 泣里还进行了异常处理捕获了〔1ie∩t[rrOI’如果出现错误’就会输出异常信息° 然后,爬取列表页,实现代码如下: a5γ∩〔de千5crapeˉj∩dex(page): ur1二 I‖0[xl」R[.+Omat(O仟5et≡pM[ 5IZ[ * (p己ge ˉ 1)) retl」r∩己wait5〔I日peˉ己pi(ur1)

△■「伊卜巴■■

‖‖[「■[■=「‖‖[■【【尸「

这里定义了5〔mpeˉ1∩dex方法用干爬取列表页’它接收一个参数page。随后构造了一个列表页的 URL’将其传给5cIape—ap1方法即可°这里注意,方法同样需要用a5y∩c修饰,调用的5crape-ap1方 法前面需要加aWajt,因为5〔mpe-ap1调用之后本身会返回一个协程对象°另外’由于5Crape-日p1的 返回结果就是JSON格式’因此这个结果已经是我们想要爬取的信息,不需要再额外解析了。 接下来我们定义"a1∩方法’将上面的方法串联起来调用,实现如下: i川portj5o∩

『■『『

旧日卜户「似■。

a5y∩Cde「∩ai∩(〉: 81oba15e551o∩ 5e55jo∏=aiohttp·〔1je∩t5es51o∩()

5〔rapeˉj∩dexta5Ⅶ5≡ [a5γ∩〔1o.e∩5uIe千uture(5〔rape—i∩dex(page)) +orpage1∩m∩ge(1’pM[‖删8[R+1)]

re5u1t5≡a"ajt日5y∩〔1o.g日t‖er(*5〔Iapeˉj∩de×ta5代5)

1og81∩g.1∩「o(‖re5u1ts%5|」 j5o∩°du们p5(re5u1ts」 e∩sureas〔ij≡「a15e’ j∩de∩t≡2)) 1十



∩a『‖e

≡,

们31∩

0 :

a5y∩〔1o。get—eγe∩t-1oop()。ru∩ˉu∩tj1ˉ〔o‖P1ete(川a1∩())

这里首先声明了5e551O∩对象,即最初声明的全局变量°这样的话,就不需要在各个方法里面都 ′









传递5es51o∩了,实现起来比较简单°

接着定义了5Cmpe_i∩dexˉta5代5,这就是用于爬取列表页的所有ta5k组成的列表。然后调用

asyncio的gat∩eI方法,并将ta5长列表传人其参数’将结果赋值为re5u1t5’它是由所有ta5促返回结 果组成的列表。

最后’调用"a1∩方法,使用事件循环启动该‖aj∩方法对应的协程即可。 运行结果如下:



第6章并步爬虫

2o】0ˉO4ˉO3O3:4S;54’692 ˉ 2o2oˉ04ˉo303:45:54’7o7 ˉ 2O20ˉ04ˉ0303:45;5q』707ˉ 2o2oˉo』ˉo3o3:45:54’7O8ˉ 202oˉo4ˉo3o3:45目54’708ˉ 2o2oˉo4ˉo3o3:45;56’431 ˉ 2o20ˉ0』ˉo3o3:45:56』q3S ˉ

I‖「0: I‖「0: I‖「0: I‖「O; I‖「O: I‖「0: I‖「0:

5crapi∩8∩ttp5://5pa5·5〔Iape。〔e∩teI/己P1/book/?1imt=188o仟set≡O scmpi∩g‖ttp5;//spa5.5〔rape°ce∩ter/apj/book/?1imt=188o仟5et≡18 5〔rapi∩gbttps://5pa5.s〔mpe°〔e∩teI/ap1/booⅨ/?1imt=188o仟5et=36 5crapj∩g∩ttp5://5pa3.5〔mpe°ce∩ter/aPi/boo代/?1mjt=18&o仟5et=54 5〔rap1∩8http5://5pa5·5〔rape·〔e∩ter/api/boo促/?1j‖jt=188o仟5et=72 5〔rap1∩ghttp5://5pa5·5cmpe.〔e∩ter/apj/boo代/?1imt≡18&o仟5et■90 s〔mpj∩g∩ttps://5p己5.5cr己pe°ce∩teI/apj/book/?1jmt=188o仟5et=1o8

可以看到,这里就开始异步爬取了,并发量是由我们控制的, 目前为5°当然’也可以进_步调 高这个数字’在网站能承受的情况下,爬取速度会进一步加快° 最后, Ie5u1t5就是爬取所有列表页得到的结果,接着就可以用它进行第二阶段的爬取了°

7ˉ爬取详情页

第二阶段是爬取详情页并保存数据°由于每个详情页分别对应_本书,每本书都需要一个ID作 为唯_标识’而这个ID又正好存在re5u1t5里面’所以下面我们需要将所有详情页的ID获取出来°

在们a1∩方法里增加re5u1t5的解析代码’实现如下: id5= [] 十orj∩dexdata1∩Ie5u1t5;

j十∩ot j∩de×dat己: co∩tj∩ue

for1te们j∩j∩dexd己ta.get(!re5u1t5‖): id5.appe∩d(ite∩get(‖id』))

■‖‖‖■■口‖|{‖‖|·■■‖{|||』■■□‖||』■■■=可‖‖■■司」■□■■■‖‖」‖』·■■】‖‖』《■■】】‖』勺■■■■Ⅵ■日‘」■■司|口=■■‖·』司‖■■司|■」■||‖■■勺

2l0

这样1d5就是所有书的1d了,然后我们用所有的1d构造所有详情页对应的ta5{〈,进行异步爬取 即可°

「ro川帅tor。『∏otor-己5y∩cioj川portA5y∩cI酬otor〔1je∩t

Ⅷ‖C0〔0‖‖[〔丁IO‖5『RI‖C≡Ⅷ∩godb://1oc日1∩o5t:27o17‖ 灿‖6O08‖∧‖[ =boo低5‖ 肥‖C0〔0[[[〔丁I0‖‖酬[ =bOO代5!

〔11e∩t=∧5y∩〔I侧otor〔1je∩t(‖O‖C0〔0‖‖[〔丁I0‖5丁RI‖6) db=〔1ie∩t[Ⅷ‖印08‖∧川[] 〔o11e〔t1o∩二db[湘‖C0〔0[[[〔丁I0‖‖酬[] a5y∩〔de千5aved己t己(d日ta):

1o8g1∩g·i∩千o(‖5aγ1∩gdat己%5』′ data) j十data:

retur∩己wajt〔o11e〔tio∩.uPdateo∩e({ !1d: data.get(!id‖) }’{ !$5et! ; data

}’ uP5ert≡丁Iue)

a5y∩〔de+5〔rapeˉdetai1(1d): ur1=0[丁AI[ (」趴.于Om日t(id=jd) d己ta=awajt 5〔rapeˉ日pj(ur1) 日"3it5aγedata(data)

这里定义了5〔raPeˉdeta11方法用于爬取详情页数据’并调用5a`′edata方法保存数据°5aγedata 方法可以将数据保存到MongoDB里面。

这里我们用到了支持异步的MongoDB存储库motor°motor的连接声明和pymongo是类似的, 保存数据的调用方法也基本—致’不过憋个都换成了异步方法°

接着在∏aj∩方法里面增加对5〔mpe—detai1方法的调用即可爬取详情页’实现如下: 5cr己Peˉdeta11ta5低5≡ [a5y∩〔1o·e∩5ure+utl」Ie(5〔r日pe-detaj1(jd))「or1d1∩id5] aw己itasy∩〔io。"ait(5crapeˉdetaj1ta5代5) aⅣ日1t5e55iO∩·〔1o5e()

■ ] 』 ■ ] □ □ 』 ‖ ‖ ‘ · | | ‖ ■ 刁 ‖ ‖ · ‖ ‖ ‖ 』 可 』 · ] { ■ ■ ■ ■ ∏ | 』 】 ■ ] 」 ‖ 」 句 ■ ■ | ■ ● 』 ■ ■ | | |

这甲再定义两个方法,用于爬取详情页和保存数据’实现如下:

6.3

aiohttp异步爬取实战

2ll

这里先声明了5〔rape-deta11ta5R5’这是由所有爬取详情页的ta5代组成的列表’接着调用了 asyncio的wait方法,并将声明的列表传人其中,调用执行此方法即可爬取详情页°当然’这里也可 以使用gat∩er方法,效果是-样的,只不过返回结果略有差异。全部执行完毕后’调用〔1o5e方法关 闭5e551o∩。

—些详情页的爬取过程如下: 2O2OˉOqˉO304:00:32』576 202Oˉ0』ˉ0〕O4:0O:32’576ˉ 2o2oˉo4ˉ03o4:0o:〕2’577 ˉ 2O20ˉO4ˉO3O4;OO:32’577 ˉ 20∑Oˉ0』ˉO3o4:0o;32’S78ˉ

I‖「0: 5〔rapi∩g∩ttp5://spa5。5crape.ce∩ter/ap1/book/2301475 I‖「0: 5cr己pj∩ghttp5://5pa5。5crape.ce∩ter/ap1/boo仪/2351866 I‖「0; 5〔mpi∩g∩ttp5;//5pa5°5crape·ce∩ter/日pi/book/2828384 I‖「O: scrapi∩g∩ttps://5p己5。5〔mpe°〔e∩ter/ap1/boo戊/〕O40352 I‖「α5cIapj∩8∩ttps://5pa5.5〔r己pe·ce∩ter/apj/boo长/3o7481o

202oˉoqˉ03o4:00:44’858ˉ I‖「0: 5aγi∏gdata{!jd: ‖〕o哗o352{ 》 0〔ome∩ts‖: [{‖1d‖ : !387952888‖ ’ ‖〔o∩te∩t|: {温蓉丈,什梅竹马种马的很有金≈‖}’…’{!id‖: 』20O53142530 ’ ‖co∩te∩t‖ : 』沈苛8泰央,丈比较短,千乎淡淡, 贴近生活,短丈的缺点不妇瓜|}]’ !∩a|∏e‖ : |那些风花寸月|’ 』日ut∩OI5|: [ 』\∩公子欢ˉ8|]’ |tm∩51atOr5,: []’

2o20ˉ04ˉ03o4:oo:“’859 ˉ I‖「0; 5crapj∩g∩ttps$//5pa5·5crape。〔e∩ter/apj/booR/2994915 ●●●

最后,我们观察到’爬取的数据都保存到MongoDB数据库里面了’如图6ˉ2所示° ●●-…↓…o↑|



=一

■……■………7 ‖0… 鳞缸◎Ⅻe〔t↓mf0b…)『i咽〈

j

尸■…■







一……



靴睁

了洒

卜■闭……0…磅) 《↑0…》

p凹凹…~…帕…



——】■■■



器麓}

P■口…

≥叼橱—

守回酗-占宁一

『w…》



冗=°

必泌

柏…》

……

_■仿

巴唾_■阻■

-

2塑iT6 l0…】

………—……—彰…

p■仍喝_V

{裙麓} {:斟

p凹口……一罗》

p凹画…「………0…的



q■■■■

唾吁…



_~ˉ_里

_ Ⅷ =

『℃…】 …

≡_…

ˉ白□~

知吨

玩面 … ■





旦≈|▲=.

酶…≡ 尸■…

.匠≡=』一—_—ˉ ˉ.↓§翱-ˉ一 ●』…_闷`(「嘻 . ◆■网……一《旧…》

……_…—……

…妈=臣逻4些】

叁·→骂乒

ˉ?…/ˉ` k . . ‘ j. ˉˉ. .f. ˉ .‘飞赢;彝..(矗』ˉ峨』 仆■田_{怕…》

“…三=~…≡…ˉ…≡~一≡…ˉ~

防■ _=凸

L■■□ =__

图6ˉ2爬取到的图书数据

至此,我们就使用ajohttp完成了对图书网站的异步爬取° 8.总结

本节我们通过一个实例讲解了aiOht印异步爬虫的具体实现°在学习过程中不难发现, 相比普通的 ■=「|■厂卜「‖‖=■■匡′)|■「伊‖「■■■

单线程爬虫来说’使用异步爬虫可以大大提高爬取效率,后面我们也会多多使用° 本节代码参见: https://gjthuhcom/Python3WebSpjdeI/ScmpeSpa5°

厂β[

|tag5‖: [‖公子欢甚0 ’ ‖耽类‖’ 』8[! ’ ′ ′」`说』’ ‖现代‖’ !校团0 ’ 0那些风花营月0 ]’|ur1‖: 0http5://boo代.douba∩。 〔o"/subje〔t/3o4o352/|’ 0jsb∩0 : |9789866685156|’‖cover‖ : !∩ttp5://1吧9.douba∩jo.〔m/vi酗/5‖bject/1/pub11〔/ 53O29724.j叼, ‖p日g旦∩凹怔r` :№∩e’ |pⅢi〔e` :№∩e’ ,5〔oIe0 : ’8.10 ’ |j∩trod0〔tio∩’: ` 』」 |〔日ta1og| ‖O∩e’ ‖p‖b1i5hedat! : ‖2oo8ˉ03ˉ26丁16:oo:ooZ0 ’ !updatedat! : 02020ˉ03ˉ21丁16:S9:39.584722Z|}

■巴崎

尸■尸「|■『|》■■『|》『巴■「}》△■尸「『■■厂『巴尸内■尸「·■厂》■■‖■■『》仆‖卜‖●尸『‖「卜∩■■□巴「|‖》‖■厂【「广‖■尸》■「’尸‖■『》任『△『■『△》『卜『卜■■「》■■『‖■尸『■尸卜『匡■「||日匹广『|「



■]■■〗{■】■■|‖勺■Ⅵ〈■■|

」avaSc『|pt动态渔 爬取

《■■‖‖‖】·』■■|‖|‖□】司·■』

第7 章

■■||」□‖■■司』·



不过JavaScrlpt动态喧染的页面不止Ajax一种。例如,有些页面的分页部分由JavaScrlpt生成, 而非原始HTML代码,这其中并不包含A)ax请求°再例如ECharts的官方实例(详见h枕p://echarts. bajducom/demohtml),其图形都是经过JavaScrlpt计算之后生成的°还有类似淘宝这种页面’即使是 Ajax获取的数据,其A|ax接口中也含有很多加密参数’使我们难以直接找出规律’也很难直接通过 分析Ajax爬取数据°

」■‖Ⅲ‖

在第5章中’我们了解了A|ax数据的分析和爬取方式这其实也是』avaScrlpt动态喧染页面的— 种情形,通过直接分析Ajax,使我们仍然可以借助requests或urlljb实现数据爬取°

{ ‖

为了解决这些问题我们可以直接模拟测览器运行,然后爬取数据’这样就可以实现在测览器中

看到的内容是什么样,爬取的源码就是什么样_所见即所爬°此时我们无须去管网页内部的JavaScript 使用什么算法喧染页面’也不用管网页后台的Ajax接口到底含有哪些参数°

7.↑

Se|e∩|um的使用 上面讲解了A|ax的分析方法,利用A|ax接口可以非常方便地爬取数据°只要能找到A)ax接口的

规律,就可以通过某些参数构造册对应的请求, 自然就能轻松爬取数据啦°

址h呻………的№催口就包蹦鳞莲谜趟…蜒″ 含_个to促e∩参数,如图7ˉl所示°

哩…c=·2凹体 ……101.32o7O.22】:$‘〕

由于请求Ajax接口时必须加上to促e∩参

凰.…,°q°臃st厂1…厂1gi∩钠c"=…s剖吐gm 图7ˉl

包含to促e∩参数的A)ax接口

方法通常有两种:一种是深挖其中的逻辑’把to代e∩参数的构造逻辑完全找出来,再用python代 码复现’构造Ajax请求;另一种是直接模拟测览器的运行,绕过这个过程因为在测览器里是可以 看到这个数据的’所以如果能把看到的数据直接爬取下来,当然就能获取对应的信息了°

第一种方法难度较高’我们先介绍第二种方法:模拟测览器的运行’爬取数据。由于使用的r具



||‖

Selenium是_个自动化测试T具’利用它可以驱动测览器完成特定的操作,例如点击`下拉等,



‖{

是Selenjum’因此先了解—下它的基本使用方法。

‖|

的构造逻辑’是难以直接模拟勾ax请求的°

厂■

数,因此如果不深人分析并找到to长e∩参数

‖‖‖ · Ⅵ ■ 甲 | | 】 ■ 】 ‖ ‖ ■ ‖ ■ ■

裔卿霉蹦腻"娜瓣髓≡篮舔漏羔磊赢

■ ■ ■ ■ ■ 司 | ‖ · 曰 』 口 ‖ ‖ 』 Ⅵ

Python提供了许多模拟测览器运行的库’例如Selenium、Splash` pyppe仗er`playwright等,可以 帮助我们实现所见即所爬,有了这些库,就不用再为如何爬取动态喧染的页面发愁了。





} b 匹■「|}β巴■『卜■『|}}·‖[■「

7.l

Selenium的使用

2l3

还可以获取测览器当前呈现的页面的源代码,做到所见即所爬,对于_些JavaSc∏pt动态喧染的页面 来说,这种爬取方式非常有效。下面我们就来感受_下Selenium的强大之处吧。

■尸‖‖■尸‖阶|凸卜■上尸『

↑.准备工作

本节以Chrome测览器为例讲解Selenium的用法°在开始之前’请确保已经正确安装好了Chrome 测览器’并配置好了ChromeDrjver。另外’还需要正确安装好Python的Se|enlum库° 安装方法可以参考h肮ps://setup.scrape.center/selenium’全部配置完成后’便可以开始本节的学习° 2.基本用法

首先大体看-下Selenium的功能°示例代码如下: ■「■巴尸■~厂‖△尸「

十IoⅧ5e1e∩ju们1ⅦDoItwebdrjγer 『

「Io‖se1e∩iu们·webdrjγer.〔o们∏‖o∩·by1『∏port8γ 「ro闸5e1e∩ju川°webdriγeI·〔o川∏‖o∩·keysjⅧport促eγ5 +ro爪5e1e∩ju们。webdrjγer·5uppoIt加portexpe〔ted〔o∩ditjo∩sa5[〔 十roⅧ5e1e∩1u川。webdrjver·support.Ⅶait加port‖ebDrjγer‖日it brow5er≡"ebdriγer.〔hroⅧe() trγ: ′

0

》|)仕



bIow5eLget(‖http5://』vw》v。bajdu.〔o"』) i∩put=brow5er.+1∩de1e|∏e∩t—byˉid(0Rw‖) i∩p0t。5e∩dˉ代ey5(』pytho∩』) 1∩put。5e∩d-低eys(长eγ5.[‖『[R)



7 匹

"己it=‖eb0Iiγer"a1t(bro"5er’ 1o)

wait.u∩ti1([〔.pre5e∩〔eo十e1e"e∩t1o〔ated((8y.I0’‖co∩te∩t1e+t,))) pr1∩t(bIow5er。〔urre∩tur1) pIi∩t(browseI.getˉ〔oome5()) pr1∩t(bIo"5er.page—5our〔e)

△■■‖■厂

+1∩a11y; brow5er.〔1o5e()

运行代码后’会自动弹出一个Chrome测览器。测览器会跳转到百度页面’然后在搜索框中输人

p

Python’ 就会跳转到搜索结果页,如图7ˉ2所示。

■■厂『‖■β『

卜|β尸

=宁磐=尸′m∩

9岔‖ `ˉ稚|

曰■■u皿●皿

p子

霄瓣

马—呻j(酗鞭痢酶懒獭瓣瓣鳞搬 ˉ→厂2、『

▲■=『‖■■■「‖「△■『■■■‖|=■尸|■『「‖『‖β叮『■β‖「‖『△■∏}||■■■『

m=二…唾……

m工■

●宙可吨蠢心;r丁…

尖子…0■■∏…■

啥…■………

瓣` 蛊罐蹿.:蹿

…凸■■的中…·■…尸…■

…〖■α压歪……

『沁……创m响≡…≡…忱坷 ………幽四叼…回哟………闻仇°. ……≈·o■….…

…习■四m脚

守攒★●◆

…n…

●★亡GQ

…■症营刨

橱及…子≤●…w■

…(贝■H■』厂≡凹没■请厂=胡Q吁仆■句闪 ■=…∩旗醛▲企空口厂



喀m,■=…m…于0m匀·臼砷, ………

列·回开发″文∏

◆击★O●

以…==…蛆

………0≡

甘…■山猫计它蝇m蹿…凶蟹旦■■垂 ≡≡』雨■曰…

m凸

】…‖……

m堑■豫中艾m、宙▲伯■■→■

V≈……■№…~…=`0…b梅■●… ……■≡……“x…唾

……一..■….… …∩…■[■…

》 卜

拍颧……辐0…、—针■■, 曰□m■…■画于……宁=m毕·函 =Ⅲ…=◇.…亭0n…

图7ˉ2在搜索框中输人Python

………

■■■卜■厂》』『|=■〗『

』||』

第7章JavaScript动态渔染页面爬取

8i∩put『=878r5v-5ug耳=87

[{05e〔ure|: 「a15e’ ‖va1ue0 : 』Bq9oB5[8「6「3〔04o2【515D22B〔DA1598,」 0do们aj∩‖: ! 。baidu。〔o叮’ 0pat∩|; !/! ’ ‖‖ttp0∩1y|: 「己1se’ ‖∩己们e| ; ‖800R2|’ !expiry0 : 1491688O71.707553}’{‖sec皿e0 :「a15e’ ‖γa1ue0 : ‖2247〕14412108417oo1』’ |do爪ai∩‖ : ‖ .ba1du·〔o∏‖’ 』path』: 0/‖’ |httpO∩1y0 ; 「a15e’‖∩3眠0 : 』‖p5p55I0‖}′ {‖5e〔ure! :「己15e’!γ己1ue! : ‖128838753813999932590ooIR2o3o3〔o2「‖I I0‖ ’ |do‖ai∩|: ‖ .www.b3jdu.〔m|’

|‖

‖ttp5://00ww.baidu°〔o∏|/5?ie≡ut+ˉ88千≡88r5γ=bp=o&r5v-idx=18t∩≡bajdu8wd=pytbo∩&Isγˉpq=〔94d0d+9o0oa7卫dO8rsγt= o7o99xγu∩1Z毗0b于6eQγyg〕』3I0丁『0O15「□γpg晒2γR[5706p1〕j‖2「%28〔Q8rq1己∩g=〔∩8r5γe∩ter=18rsv-5ug3≡68r5γˉ5‖g2=o

■■勺‖]·‖■■

此时控制台的输出结果如下,因为页面源代码过长,所以此处省略其内容:

‖ | {

2l4

』pat∩』: `/』’ 』httpO∩1y』: 「a15e’ 』∩a"e』胳 | b5i』厂』歇区ry了:〕491肋1675.酌顽}]

<!皿Wp[ht∏1〉〈!ˉˉ5丁∧丁050Ⅸˉˉ〉. . .〈/们↑们1〉

可以看到’我们得到的当前URL`Cookie内容和页面源代码都是测览器中的真实内容°

q

所以说’用Selenium驱动测览器加载网页,可以直接拿到JavaSc门pt喧染的结果’无须关心使用 的是什么加密系统。



下面详细了解_下Selenjum的用法。

3.初始化》刘览器对象

Selenlum支持的测览器非常多’既有Chrome` Flre{bx`Edge` Safa∏等电脑端的测览器,也有 「IoⅧ5e1e∩ju∏加portⅣebdr1γer broⅣ5eI=webdr1γer.(∩ro∏e() brow5eI≡webdriγer.「iIe+ox()

bro"5er="ebdIjveI.〔dge() bIowser≡webdr1γer.5a+arj()

这样就完成了测览器对象的初始化’并将其赋值给了bro"5er。接下来,我们要做的就是调用

bro"5er’执行其各个方法以模拟测览器的操作°

我们可以使用get方法请求网页,向其参数传人要请求网页的URL即可°例如,使用get方法访 问淘宝’并打印出淘宝页面的源代码,代码如下:

bro们5eI=脆bdrjγer.〔hro‖恰()

br咖5er.8et(‖http5://….taobao.〔o‖∏‘) prj∏t(bro切ser.pa8e—sour〔e) bro切ser.〔1o5e()

通过上面几行简单的代码’就可以驱动测览器并获取网页源码,可谓非常便捷°

5ˉ查找节点

●单个节点

|□|」‖ ‖‖

例如’想从淘宝页面中提取搜索框这个节点,首先就要观察这个页面的源代码,如图7ˉ3所示。



□〗|‖‖■■∏目■‖|■■『‖』●■日

Selenjum可以驱动测览器完成各种操作’比如填充表单、模拟点击等。例如,想要往某个输人框 中输人文字,总得知道这个输人框在哪儿吧?对此, Selenjum为我们提供了_系列用来查找节点的方 法’我们可以使用这些方法获取想要的节点,以便执行下_步的操作或者提取信息°



』Ⅵ』■司乙■■■』|■■司

运行这段代码后,弹出了Chrome测览器并且自动访问了淘宝,然后控制台输出了淘宝页面的源

代码,随后测览器关闭°

|‖(

+I咖5e1e∩jⅧmPort眠bdrjver

·可』』●]·■■■∏

4.访问页面

』■=■■□‖|』曰‖|《■回■』‖■■∏』■‖■■‖□|】=■

Android、BlackBe∏y等手机端的测览器°我们可以用如下方式初始化测览器对象:

一·■|·



『』‖〖尸‖|【∏「|■■「|||■「||}‖△尸『||‖■厂‖「仆[尸「|‖凸尸|

7.l

γaobaO亡Om



’■厂)■「)尸尸′■△■「|赊卜}■■「》■厂萨炉’■厂『卜》『’。户}似′卜|》~■■【’』■『`■『)●心伊`尸}△■·′●厂卜■厂’』■『伊■甲『卜△口』卜|【广■■厂●『|′■「|‖∩·■『|●尸|■■●β|}■尸

淘宝网砷

Selenium的使用

2l5

芦8币g

U』

H衣■…外钮……

图7ˉ3淘宝页面的源代码

可以发现’淘宝页面的jd属性值是q, ∩a州e属性值也是q°此外,还有许多其他属性,我们可以 用多种方式获取它们°例如, 十j∩de1eⅧe∩t_byˉ∩己爪e是根据∩aⅧe属性获取,{i门de1e们e∩tˉbyˉid是根 据jd属性获取’此外还有根据XPath`CSS选择器等的获取方式。

下面我们用代码实现_下:

厂7 b

十roⅦ5e1e∩iuⅦj∏port"ebdriγer

bIo"5eI="ebdriγer.〔∩r叼记() brow5er.get(0http5://州.taobao.co∏‖!) 1∩putˉ「ir5t≡brow5er。+i∏de1e删e∩tˉbyˉid(!q』) j∩put-5e〔o∩d=bI叫5er.十i∩de1e赡∏t≡by-〔55一5e1ector(0#q‖) j∩put-t∩iId=bIo们5eI。千j∩de1e∩mtˉbyˉxpat∩(!//*[0id≡同q"]|) pm∩t(1∩put-十jr5t’ j∩put≡5eco∩d’ i∏put-t‖iId) br叫5er.〔1o5e()

泣里我们使用3种方式获取输人框对应的节点,分别是根据id属性、CSS选择器和XPath获取。 代码的运行结果如下: 〈5e1e∩jⅧ.眠bdIjγer°re∏‖ote.踞be1e贬∩t.‖eb[1e∏论∩t (5e55jo∩=圃5e53d9e1〔8646e“〔1qc1〔2880d424a千阅’ e1e眶∩t≡回0.5649563O96161541ˉ1阐))

<se1e∩i咖.眶bdrjγer.Ie∩me。眶be1e‖论∏t."eb[1e∩记∩t (5es5jo∩≡.Se53d9e1〔8646e“〔14〔1〔2880d424a+口’ e1e∏记∏t=阑o。5649563o96161541-1曰)〉

〈5e1e∩ju‖‖.眶bdIiγer.re‖℃te.腿be1e∏mt.‖eb[1e∏把∩t (5e55io∩=碱5e53d9e1〔8646e“〔14〔1〔Ⅺ88Od424a「"’ e1e贬∩t≡闻0.5649563096161S』1ˉ1■)〉

可以看到, 3种方式的返回结果完全一致。j∩putˉ+irst. j∩put-5eco∩d和1∩put-tMrd都属于 ‖eb[1e们e∩t类型,是完全—致的°

获取单个节点可以使用十j∩de1e们e∩tˉby≡1d、「1∩de1e"论∩t-byˉ∩a『‖e、 十i∩de1e『记∩t=by-xpath、

十j∩de1e们e∩t-bγ≡1j∩代text 、「j∩de1e爬∩t-by一partja11i∩代text` +j∩de1e爬∩t-by-tag-∩a们e 、

十j∩de1e‖记∩t-by≡〔1a55∩a‖e、十j∩de1e∏]e∩t-by-〔555e1e〔tor,这些就是所有的方法· 除了上述方法, Selenjum还提供了通用方法十1∩dˉe1eⅦe∩t,使用这个方法需要传人两个参数:查

找方式和方式的取值。这个方法其实就是上述那些方法的通用函数版本,不过它的参数更加灵活°例

如十j∩de1e『∏e∩tˉbγ=jd(jd)就等价于十i∩de1eⅧe∩t(By.I0’ 1d)’两个方法得到的结果是完全一致的。 我们用代码实现-下: 十ro∏‖ 5e1e∩ju∏j呻ort脆bdrjγer 十Io∏‖ 5e1e∩1l』∏。脆bdrjγer·〔o‖u"∩。byjⅧport8y

2l6

第7章JavaScript动态渔染页逾爬取

bro"ser°get({‖ttp5://咖‖°t己ob己o.〔o∏‖‖) i∩put-千irst=brow5eI.千i∩de1e‖e∩t(Bγ.ID’ ‖q0 )



bro"5er="ebdrjγer。〔∩ro‖e()

pri∩t(i∩put-「irSt) brow5er.c1o5e()

千Io‖5e1e∩1u∩jⅦportwebdIjver

brow5eI≡"ebdI1γer。〔∩ro们e()

pri∩t(1i5) bro"5eI.〔1ose()

运行结果如下:

··□■∏‖‖■·』‖‖|■■

bro"5er°get(‖∩ttps://洲w.taobao.co∏‖〉 1j5≡brow5er.fi∩de1eⅧe∩t5—byˉc5s—5e1ector(|.5eIv1〔eˉbd1j0)



·∏』』■可β】■{||■■■{|■‖

例如’要查找淘宅页面左侧导航条的所有条目’就可以这样实现:

■‖■■‖‖】Ⅲ■{」■Ⅱ

如果查找的目标节点在网页中只有_个’那么用十j∩de1e"e∩t方法就完全可以实现。但如果目标 节点有多个,再用千1∩de1e"e∩t方法查找,就只能得到第一个节点了,此时需要用+1∩de1e‖e∩t5方 法才能找到所有满足条件的节点°注意’这个方法名称中的e1e‖e∩t后面多了-个5’注意区分。

■■可‖』

●多个节点

[〈5e1e∩ju们."ebdrjγer。re们ote."ebe1e爬∩t·刊eb[1e刚e∩t (5e551o∩=00〔2629o835d4457eb千7d96b千ab37』0d19"》 e1eⅧe∩t≡| |0。09221044O33125603ˉ1")〉’〈5e1e∩1u肌webdrjγeI.re‖]ote.webe1e刚e∩t.‖eb[1e『∏e∩t (5e551o∩="〔26290835d4457eb十7d96b千ab374od19"′ e1e"e∩t="0。o9221O440331256o3ˉ2")〉’ 〈5e1e∩1u川·webdr1γer.re‖ot巳webe1e『∏e∩t.‖eb[1eⅦe∩t (5e55io∩≡"c2629O835d4』57eb「7d96b千ab〕74Od19"』 e1e『∏e∩t=‖!O.09221oq4O33125603ˉ3"))…〈5e1e∩ju川webdr1γer.re『∏ote。webe1e们e∏t.‖eb[1e们e∩t

这里简化了输出结果’省略了中间部分。可以看到’得到的内容变成了列表类型,列表中的每个 节点都属于‖eb[1e"e∩t类型.

总结_下,如果使用{1∩de1e∏e∩t方法查找’只能得到匹配成功的第一个节点,这个节点是 于‖eb[1e们e∩t类型。

获取多个节点可以使用+1∩de1e"e∩t5-by一1d`+j∩de1e们e∩t5-by=∩a肌e`十1∩de1eⅧe∩t5-by-xpat∩` +j∩de1e川e∩t5-by-11∩代te×t、 千1∩de1eⅧe∩t5-by-part1a111∩促text、{j∩de1e们e门ts-by-tag≡∩a∏e、

「1∩de1e"e∩t5一bγ-〔1a55∩a"e` +i∩de1eⅦe∩t5-by-c555e1e〔toI’这些就是所有的方法。 同理’我们也可以直接使用+j∩de1e|『|e∩t5方法,这时可以这样写: 1j5≡bIo"5er°+i∩de1e爬∩t5(8y.〔555[L〔〔丁0R′|.5erv1〔eˉbd11‖)

0

·■·司‖』=吓‖乙■可·司●可勺‖·‖‖■■】]』司|■■」·』■凸■

‖eb[1e‖e∩t类型的°如果使用十j∩de1eⅦe∩t5方法,那么结果是列表类型的,列表中的每个节点都属



|‖』】■■』■□□〗

(5e55jo∩≡"〔26290835d』457eb+7d96b+日b374od19"’ e1e川e∩t="0。O9Ⅺ210q4o33125603ˉ16")〉]

得到的结果是完全-致的°

6.节点交万

Selenjum可以驱动测览器执行一些操作。比较常见的用法有:用5e∩dˉ促ey5方法输人文字,用〔1eaI





方法清空文字’用〔11Ck方法点击按钮°示例如下:

bro"5er=webdriγer.〔∩ro们e()

bro"5er。8et(‖bttp5://州.t日obao.〔o川0) 1∩pl」t=bIo"5er.十i∩de1e爬∩tˉbγˉ1d(0q! ) i∩put.5e∩dˉ代ey5(‖ipbo∩e‖) t加e.51eep(1)

∩|』■]|=■■』□■」〗■∏』■∏|||□■

+ro‖5e1e∩iuⅧ1‖portwebdriver mpOrttme

尸`■『

■【[‖||■『『「『『『|■厂}|『厂|

7·l

Selenium的使用

2l7

1∩put.C1eaI() j∩p0t.5e∩d-捉ey5(0jp己d』)

butto∩=browser.+j∩de1e"e∩t-byˉ〔1a55ˉ∩aⅦe(,bt∩ˉ5e日r〔∩!)

『 ∩ |

butto∩.c1j〔促()

^ 尸 』 ■ 厂 | | ● 。 ■ 厂 | ‖ ■「

这里首先驱动测览器打开淘宝’然后使用千1∩de1e"e∩t-byˉid方法获取输人框’再使用5e∩dˉ代eγ5 方法输人文字iPhone’等待_秒后用C1ear方法清空输人框’并再次调用5e∩d_代ey5方法输人文字 lPad,之后使用千1∩de1e川e∩tˉbγˉ〔1a55—∩a"e方法获取搜索按钮’最后调用C1j〔R方法实现搜索°

巴●「}‖

通过上面的方法’我们完成了几种常见的节点操作’更多操作可以参见官方文档的交互动作介绍: http://selenium-pythonreadthedocs.io/apihtml#moduleˉselenjum.webdriver·remote.webelement°



7.动作链 ■尸‖「》‖匡尸「‖『巳∩

在上面的实例中’交互操作都是针对某个节点执行的。例如,对于输人框,调用了它的输人文字 方法

5e∩d一代ey5和清空 文字方法〔1ear;对于搜索按钮’调用了它的点击方法C1i〔促°其实还有_些操



作’ 作, 它们没有特定的执行对象,比如鼠标拖曳`键盘按键等,这些操作需要用另_种方式执行, (行对象,比如鼠标拖曳`键盘按键等’这些操作需要用另_种方式执行,那就

}估『‖‖■尸

是动作链。

例如’可以这样实现拖曳节点的操作’将某个节点从_处拖曳至另_处:

伊 「|■「‖|巴■「似■◆「|\『β}■厂

[7Ⅲ

十ro们5e1e∩ju"1呻ortⅣebdrjveI +ro川5e1e∩jⅧ."ebdr1γermportA〔tjo∩〔∩a1∩5 browser=webdrjγer.O`ro"e()

u【1= |∩ttp://w洲。ru∩oob。〔o∏/try/try。p∩p汗i1e∩a∏e=jquerγu1ˉapiˉdroppab1e! b row5er.8et(ur1) brow5er。5w1t〔∩to.fm刚e(|1+r3爬Re5u1t0 )

sour〔e≡browSer·十1∩de1e「∏e∏tˉbyˉ〔55-5e1ector(0#drag8ab1e|)

t日rget≡bro训5er.十j∩de1e爬∩t-bγ→c55—5e1e〔tor(!#droppab1e!) 3〔tio∩s=A〔tjo∩〔∩日i∩5(browser)

伊‖‖『



a〔tio∩5.drag—a∩dˉdrop(source’ t日rget) a〔t1O∩5。per+om()

这里首先打开网页中的一个拖曳实例’然后依次选中要拖曳的节点和拖曳至的目标节点’接着声 ■



明一个A〔t1O门〔ha1∩5对象并赋值给aCt1O∩5变量’ 再后调用a〔t1o∩5变量的drag-a∩dˉdIop方法声明 协作’就完成了拖曳操作’拖曳前和拖曳后的页面 方法执行 动作’ 拖曳对象和拖曳目标,最后调用per+Or阳方法执市



如图7ˉ4和图7ˉ5所示°

‖ b≡…泻呛≡D▲=马

》…

谗触闭粥这鳖|

■↑$$▲『』■

鞭蛔『`



卜 「●「‖|‖‖匹■尸‖‖『【∩‖{『〖化【【‖□‖□『】‖‖▲■■‖‖』【β卜‖[■■『『

图7ˉ4拖曳前的页面

图7ˉ5拖曳后的页面

更多的动作链操作可以参考官方文档的介绍: htq〕://seleniumˉpythonreadthedocsjo/apihtml#moduleˉ selenium·webdriver.common。actionchains。

8.运行」avaSc『|pt

还有-些操作’Selenium没有提供API°例如下拉进度条’面对这种情况可以模拟运行JavaScrlpt’ 此时使用exeCute5〔r1pt方法即可实现’代码如下:

{′‖

第7章JavaScript动态演染页面爬取

于ro∩] 5e1e∩iu∏mport‖ebdIjγer

browser=webdIjver.〔hro们e()

bro倒5er.get(0https8//硼.z∩jhu.co"/exp1oIe!〉

brow5er.execute-5crjpt(|wj∩do".5cro11丁o(O’ do〔u爬∩t。body。5〔ro11川eight)‖) brow5er。exe〔ute_s〔ript(!a1ert(00「o8otto∏0)0)

| |





这里利用execute-5〔rjPt方法将进度条下拉到了最底部,然后就弹出了警告提示框。所以说有了 exe〔ute—5cript方法,那些没有被提供API的功能几乎都可以用运行JavaScript的方式实现。

□■可■·■■可‖

2l8



9ˉ获取节点信息

前面我们已经通过pageˉsource属性获取了网页的源代码,下面就可以使用解析库(如正则表达 不过,既然Selenium已经提供了选择节点的方法’返回的结果是‖eb[1eⅦe∩t类型,那么它肯定 也有相关的方法和属性用来直接提取节点信息,例如属性`文本值等。这样我们就不需要用通过解析

源代码提取信息了’非常方便°

勺、‖|■|二□Ⅷ‖』‖

式`BeautjfillSoup、pyqueO′等)从中提取信息了。

d

让我们-起看看怎样获取节点信息吧°

可以使用get_attribute方法获取节点的属性,但其前提是得先选中这个节点’示例如下: 千Io们5e1e∩ju川mportwebdriγer brow5er=webdIjver.〔hIo∏〗e() ur1≡ ,∩ttp5://5pa2.scrape。〔e∩ter/!

控制台的输出结果如下:

向get—attrjbute方法的参数传人想要获取的属性名,就可以得到该属性的值了。 ●获取丈本值

每个‖eb[1e爬∩t节点都有text属性,直接调用这个属性就可以得到节点内部的文本信息,相当 于pyqueIy中的text方法,示例如下: 「ro‖∏5e1e∩iu∏0 j呻ort眶bdriver bIow5er=眠bdrjver.〔hⅢm馆() ur1= !‖ttp5://Spa2.5〔mpe.〔e∩ter/0 brow5er.get(ur1) j∏put=bro切ser。千j∩de1e∏记∩t-bγˉ〔1a55ˉ∩a眠(‘1ogoˉtjt1e,) prj∏t(i∩put。text)

这里依然先打开示例页面,然后获取c1a55名称为1ogoˉtjt1e的节点,再将该节点内部的文本值 打印出来°

控制台的输出结果如下: 5Crape

q

■ˉ|(□■‖|』】■■】〗‖·■■‖‖‖』■】■■〗‖|对』■】||{■■』■||‖』□]」■|』□|‖』·■司口|■】‖]】■】】‖』】‖勺|』■■】■

http5://spa2。5〔rape。〔e∩ter/j吨/1ogo.a5O8a8千o·p∩g



《·

<5e1e∩juⅧ。"ebdrjγe【.re∏me.webe1e∏记∩t.胀b[1e∩记∩t (5e5sio∩≡赋7「47▲5d]5a1o』759239b53于68a6十27do啸’ e1e爬∩t≡"〔d7〔72皿ˉ492oˉ47ed-91〔5ˉeao66o1d〔509")〉



』]|‖■■■|』·‖

运行代码,它会驱动测览器打开示例页面’然后获取其中C1355名称为1ogoˉj们age的节点’最后 打印出这个节点的5rC属性。



■勺‖』■〗叼·

bro佣ser。get(ur1) 1ogo≡brow5er.+j∩de1e∏e∩t-by-〔1a55-∩a爬(‖1ogoˉm3ge0) prj∩t(1ogo) prj∩t(1ogo。get-attribute〈!sr〔‖))

□■司‖‖四■□‖■{‖』■γ

●获取属性

7.l

Selenium的使用

2l9

●获取lD、位置、标签名和大小

除了属性和文本值,‖eb[1e"e∩t节点还有一些其他属性’例如jd属性用于获取节点ID, 1o〔at1o∩ 属性用于获取节点在页面中的相对位置’tag-∩a‖e属性用于获取标签的名称’51Ze属性用于获取节点 的大小’也就是宽高’这些属性有时候还是很有用的°示例如下: 十Io们se1e∩1u们1们portwebdIiγer

brow5er≡闪ebdriγer.〔∩ro爪e() ur1二 0http5://5pa2。5〔rape.ce∩teI/‖ browser.get(l」r1) 1∏put=bIow5eI。+j∩de1e∏侣∩t=by-〔1a55ˉ∩a川e(01ogo-t1t1e!) Prj∩t(i∩Put.id) prj∩t(j∩p(」t。1O〔at1O∩) prj∩t(1∩put.tag-∩己爬) prj∩t(i∩put.51Ze)

这里首先获取〔1a55名称为1ogoˉtjt1e的节点,然后分别调用该节点的jd` 1o〔at1o∩、tagˉ∩a"e` 51∑e属性获取了对应的属性值° 卜||匹尸『们|||■『‖■『‖尸』■「【『厂【尸|『●【∩|ˉ尸‖卜「卜|》『||■尸|》|■「〖β‖●『「‖|■『|归『『■『「|卜[∏|‖【■『‖)卜‖|β■「广|‖





↑0.切换P『a丽e

|■

我们知道,网页中有_种节点叫作lfmme’也就是子Frame,相当于页面的子页面’它的结构和 外部网页的结构完全—致° 全—致°Selenium打开-个页面后,默认是在父Frame里操作’此时这个页面中如 果还 有子Frame’它是不能获取子Frame里的节点的,这时就需要使用5"1t〔hto.+I日"e方法切换 Fram e。示例如下

jⅧpOrtt加e 十r咖生1e∩1uⅦ1‖portwebdrjγeI 0

frOⅦ5e1e∩jU川。〔O『∏『℃∩。eX〔ePtjO∩51『∏POrt‖O5U〔h〔1e∏记「]t[XCePtiO∩ bro"ser≡webdrjver.〔hrα爬() ur1= |http://"洲.ru∩oob.〔o们/try/tIy。p∩p汗i1e∩a『‖e≡jquerγuiˉapjˉdIoppab1e!

bro"5er.8et(uI1) brow5er·5"jt〔∩to·+m爬(‖1「m爬∩e5u1t‖) t】y;

1ogo≡bro切5er.+j∩de1e‖mtˉby-〔1a55∩a爬(』1ogo0) exCept‖o5uCh[1e‖∏e∩t[xCept1O∩: prj∩t(!"[卯O)

bro佣5er·5佣jt〔h-to.pare∩t十m∏把() 1ogo≡bIo"5er°「i∩de1e∩吧∩tˉby-c1a55∩a「∏e(‖1ogo‖) prj∩t(1ogo) pri∏t(1Ogo。teXt)

这里还是以演示动作链操作时的网页为例,首先通过5witcbto.十ra∏论方法切换到子FI固me里’然后

尝试获取其中的1ogo节点(子FIan贮里并没有1ogo节点),如果找不到,就会抛出‖o5uch[1e∏记∩t[x〔ept1o∩ 异常’异常被捕捉后’会输出‖0[0CO°接着,切换回父Frame,重新获取1ogo节点’发现此时可以 成功获取了. 控制台的输出结果如下: ‖0L呵

〈5e1e∩juⅧ."ebdriveI.remte."ebe1e贮∩t。‖eb[1e们e∩t(se5sio∩="4bb8a〔o3〔ed4ecbde十e十o3仟dcoe4〔〔d"’ e1e爬∩t=!|o.13792611〕20464965ˉ2")〉 【0灿8·〔侧

所以,当页面中包含子Frame时’如果想获取子Frame中的节点’.需要先调用5wjtcbto.十raⅦe方 法切换到对应的Frame,再进行操作°

□·■|司■■

第7章JavaScript动态渣染页面爬里



] 勺 □

220

‖↑.延时等待

在Selenium中, get方法在网页框架加载结束后才会结束执行,如果我们尝试在get方法执行完 毕时获取网页源代码’其结果可能并不是测览器完全加载完成的页面’因为某些页面有额外的Ajax请 求,页面还会经由JavaScr1pt喧染。所以’在必要的时候’我们需要设置测览器延时等待一定的时 间,确保节点已经加载出来。 ‖

这里等待方式有两种:一种是隐式等待,_种是显式等待° ●隐式等待

段时间再查找DOM’默认的等待时间是0°示例如下: 千roⅦ5e1e∩1Ⅷ1Ⅶportwebdr1γer brow5er≡webdriγer〔hro∏0e()

brow5eI°i川p1i〔it1y=wa1t(1o) broⅣ5eI。get(!们ttp5://5pa2.5cmpe.〔e∩teI/0)

1∩P‖t=bIo"5er.十i∩de1e∏论∩t一by-〔1a55ˉ∩a川e(01ogoˉmage!) prj"t(j∩put〉

这里我们用1"p11C1t1γ_Wajt方法实现了隐式等待。 ●显式等待

隐式等待的效果其实并不好’因为我们只规定了一个固定时间,而页面的加载时间会受网络条件 影响°

还有—种更合适的等待方式——显式等待,这种方式会指定要查找的节点和最长等待时间°如果 在规定时间内加载出了要查找的节点,就返回这个节点;如果到了规定时间依然没有加载出点’就抛

■■】‖·』】■■Ⅷ·■‖‖』■]』■□■·(」』■】』可]|」·■■■』守』■可』■■可|』■□■■」口|■■]α□」』·〗■■

使用隐式等待执行测试时’如果Selenium没有在DOM中找到节点’将继续等待’在超出设定时 间后’抛出找不到节点的异常°换句话说’在查找节点而节点没有立即出现时’隐式等待会先等待_

出超时异常°示例如下:

brow5er≡webdIiγer°〔∩ro∩e()

bro"5er.get(‖http5://卿.taobao.co刚/‖) "a1t=‖eb0riγer"己1t(bro"5er’ 1O)

j∩put≡"a1t。u∩ti1([〔.pre5e∩ceo+e1e∏e∩t1oCated((8y.I0’ 0q‖)))

butto∩≡"ajt.u∩tj1([〔.e1eⅧe∩t二to二beˉc11c[ab1e((8γ.〔55ˉ5〔L[〔『0R’ 』 .bt∩ˉsear〔b′)))

pri∩t(j∩pl』t’ b0tto∩)

这里先传人了pre5e∩〔eo十e1e"e∩t1ocated这个条件’代表节点出现’其参数是节点的定位元组 (8y.I0’|q』)’表示节点ID为q的节点(即搜索框)。这样做达到的效果是如果节点ID为q的节点在 l0秒内成功加载出来了’就返回该节点;如果超过l0秒还没有加载出来,就抛出异常。

然后传入的等待条件是e1e"e∩ttobe〔11C怕b1e’代表按钮可点击,所以查找按钮时要查找CSS 选择器为.bt∩ˉ5ear〔∩的按钮’如果l0秒内它是可点击的’也就是按钮节点成功加载出来了’就返回

该节点;如果超过l0秒还是不可点击,也就是按钮节点没有加载出来’就抛出异常° 运行代码’在网速较佳的情况下是可以成功加载出节点的。

|{〈||

这里首先引人‖eb0r1γeI‖a1t对象,指定最长等待时间为l0’并赋值给Wa1t变量。然后调用"a1t 的u∩t11方法’传人等待条件。

司||』·‖■·|』·】■■】|』■】■和』‖·‖‖对』■·日|■·‖」]‖』■Ⅵ‖

fro们5e1e∩1‖∏mportwebdIjγer +ro阳5e1e∩iu们。webdrjγer。〔o咖o∩°by加portBy +ro川5e1e∩iuⅦ.ⅣebdI1γer。5l」pport.ujj们port‖eb0riγer‖a1t +ro‖5e1e∩ju川·川ebdr1γer·5upporti们poItexpe〔ted_〔o∩ditjo∩5a5[〔

「‖[庐『||▲■[||■「‖‖『|》〖=尸|止■「}■[尸「’匹■‖■「‖■「‖『尸‖仁{‖》′β「’●尸「|β』′■尸。‖‖β

’|[「广‖》‖

△尸‖户『■尸□仿‖■尸 伊‖□厂

′|■



卜) ■』■ ■}‖■尸|

‖| 凹 卜 ‖ 凸 ■ | ■ 」 ■ ■

【‖卜|





7.l

Selenium的使用

22l

控制台的输出结果如下:

〈5e1e∩ju∏."ebdriver.re阳ote.webe1e‖∏e∩t°‖eb[1e∏e∩t (5e55jo∩≡"o7dd2「bc2d5b1〔e4oe82b9754aba8+a8"’ e1e爬∩t≡"α5642646294074107ˉ1")〉

〈5e1e∩ju们."ebdriγer。re们ote.webe1e爬∩t.‖eb[1e"e∩t (5es51o∩="o7dd2+bc2d5b1〔eq0e8鱼b975』aba8+a8"’ e1e爬∩t="0.5642646294O74107ˉ2")〉

可以看到’成功输出了两个节点’都是‖eb[1e"e∩t类型的°

如果网络有问题’l0秒到了还是没有成功加载’就抛出「j‖eout[xcept1o∩异常’此时控制台的输 出结果如下: 「meout〔×〔eptjo∩『mcebac代 (∏℃5tre〔e∩t〔a111ast) 〈ipyt‖o∩ˉ1∩putˉ4ˉ+3d73973b223〉j∩〈∏mu1e〉(〉 7broN5er。get(0http5://哪。t己obao.〔oⅧ/0) 8wajt=‖eb0rjγer‖ait(bro"5er’ 1O)

ˉ_〉9i∩put="己it.u∩tj1([〔。pre5e∩ceo+e1e『∏e∩t1o〔日ted((8y.ID’ !q‖)))

除了我们介绍的这两个,等待条件其实还有很多,例如判断标题内容、判断某个节点内是否出现

了某文字等°表7ˉl列出了所有等待条件° 表7=↑等待条件 含

等待条件



t1t1ei5

标题是某内容

tit1eCO∩ta1∩5

标题包含某内容

pre5e∩〔eo十e1e爬∩t1ocated

节点出现’参数为节点的定位元组’如(ByJD’ ’P0)

γi5jb111ty-o+-e1e爬∩t1o〔ated

节点可见,参数为节点的定位元组

γi5ibj1jty←o+

可见,参数为节点对象

prese∩〔eo千a11e1e『∏e∩t51o〔ated

所有节点都出现

textˉto-be一pre5e∩ti∩e1e爬∩t

某个节点的文本值中包含某文字

texttobepre5e∩tj∩e1e爬"tγa1ue

某个节点值中包含某文字

千ra爬tobe己γaj1ab1ea∩d5"jtchtoit千Ia∏e

加载并切换

i∩γ15ibj11ty=o+-e1e爬∩t1ocated

节点不可见

e1e爬∩ttobec1j〔炮b1e

按钮可点击

5ta1e∩e55o+

判断—个节点是否仍在DOM树中’可知页面是否已经刷新

e1e‖e∩ttobe5e1ected

节点可选择’参数为节点对象

e1e爬∩t1oc己tedtobe5e1e〔ted

节点可选择,参数为节点的定位元组

e1e爬∩t5e1ectio∩5tatetobe

参数为节点对象以及状态,相等返回丁rue’否则返回「日15e

e1e爬∩t1ocated5e1e〔tio∩5tatetobe

参数为定位元组以及状态,相等返回丁rue,否则返回「a15e

a1ert-js-pre5e∩t

是否出现警告提示框

=--0

「-

更多等待条件的参数及用法介绍可以参考官方文档: http://seleniumˉpythonreadthedocsjo/apihtml #moduleˉselenjumwebdriveⅢsupport.expected-conditlons°

↑2前进和后退

平常使用测览器时’都有前进和后退功能’Selenium也可以完成这个操作’它使用十or"ard方法 实现前进’使用ba〔低方法实现后退。示例如下: 1呻Ortt1眠

「ro川5e1e∩i0Ⅶmportwebdr1γer brow5er=webdrjveI.〔hro爬()

第7章JavaScript动态演染页面爬取

brow5er.get(!∩ttps://Ⅶ‖w。bajdu.〔o『∏/0) bIow5er.get(!http58//….t己obao.co‖|/0) br叫5eI.get(|∩ttp5://….pytho∩.org/|) bro倒5er.ba〔k(〉 tme.51eeP(1) brow5er.于orwam() brow5er。c1o5e()

这里我们先连续访问了3个页面’然后调用ba〔促方法回到第2个页面’接着调用「orward方法又

■·■=■■■己■‖』■■】·勺‖』■■‖|』□■可■■■■■■■

222

前进到第3个页面。 ↑3.Coo低le

□□·‖·■〗|■□

使用Selenium,还可以方便地对Cookje进行操作’例如获取、添加`删除等。示例如下: ↑【o‖se1e∩jl」ⅦjⅧportwebdIiver

bro们5er=webdrjγer.〔hro∏]e()

bIow5er.get(!https://哪.zhihu。c咖/exp1ore|) prj∩t(bIo"5er°getˉ〔oo促je5()) bro"5er.add〔oome({‖∩a眶, : 』∩己|∩e』’ ‖do们己i∩』: ‖洲w.乙h1∩u·co|∏‖’ ‖γa1ue』: 』ger"ey0}) pIi∩t(bro"ser.get-〔oo促ie5())

(‖"(·|‖‖

brow5er°de1ete己11cookie5()

pri∩t(bIowser。get-〔ooMes())

这里我们先访问了知乎°知乎页面加载完成后,测览器其实已经牛成Cookie了°然后’调用测

览器对象的getˉ〔oo灶e5方法获取所有的Cookje。接着’添加~个Cookle,这里传人了—个字典’包 含∩a们e` doⅦa1∩和va1ue等键值。之后,再次获取所有的Cookie,会发现结果中多了一项,就是我们

■■■Ⅵ』□■

新加的Cookie。最后,调用de1etea11〔ooMe5方法删除所有的Cookie并再次获取’会发现此时结 果就为空了°

控制台的输出结果如下: q

刘‖■

[{!5e〔‖re』: 「a15e’ 0γa1ue‖ : !"‖阴OZ「‖5‖0A刚‖[y‖酗‖0惯5卯1kZ‖γ30丁kxγ2I0‖0γ=|14916O4O91| 236e3429oa6十407b+bb517888849ea5o9a〔366do"! ’ ‖do腮j∩0 : ! °zhj∩u.〔o|∏0 ’ 0path! : !/|’ |∩tt血1y! :「a1se’ ∩a爬8 01ˉ〔aP-id』’ 0exP1ry『 : 149』196O91.4O3418}’…]

[{05e〔ure,: 「己15e’|γa1ue‖ : !gemeγ|」 0do蛔1∩0 : ‖ 。www。zh1∩u.co∏‖‖ ’ 0path‖ : |/! ’ 0∩ttpO∩1y! : 「315e’ ∩a爬! ; ∩己眶!}’{05e〔uIe‖: 「己15e’ 0γ己1ue|: ‖徽NCⅧZ『‖5‖D∧呐"[y‖酗‖0代5卯1促ZNγ30Tkxγ2IO‖0γ=|14916O4O91| 2〕6e34290a6伺O7b+bb517888849ea509a〔〕66do" |’ 』do肌aj∩! : ‖ 。zhjhu.〔o‖‖’ ‖path‖ : 0/|’ |http0∩1y|:「a15e’ ∩驯e : ‖1_〔apˉ1d! ’ ,exp1Iy! : 1494196091·4o3』18}’…] []



■(日

通过以上方法操作Cookie还是非常方便的° ↑4选项卡管理

访问网页的时候,会开启_个个选项卡°在Selenlum中,我们也可以对选项卡做操作°示例如下: j呻Orttj爬 ‖

|‖

十ro∏‖ 5e1e∩j‖Ⅶmportwebdrjγer bro倒5er=webdIiver·〔打m∏e() bro"5er。get(,∩ttp5://….ba1du。〔o∏‖0) bro脚5er.exe〔ute5crjpt(M∩dow.ope∩()0) pI1∩t(brow5er.Ⅳi∩dowha∩d1e5)

browser.5"it〔bto."j∩do刊(bro"5er."1∩do倒ha∩d1e5[1]) bro闪5er.get(!http58//…。taobao.〔o∏]|)



tme.51eeP(1)



bro"5er。5"jt〔hto.Ni∩do"(br叫5er.M∩do倒ha∩d1e5[o]) brow5er.get(』https://pγtho∩.org!)

‖||

这里首先访问百度,然后调用e)《e〔ute5〔r1pt方法’向其参数传人Ⅳ1∩do".ope∩()这个JavaScript 语句’表示新开启-个选项卡。接着,我们想切换到这个新开的选项卡。w1∩dowha∩d1e5属性用于获取 当前开启的所有选项卡,返回值是选项卡的代号列表。要想切换选项卡’只需要调用SWjt〔∩tO.W1∩dOW





■|

7.l

Selenium的使用

223

方法即可,其中参数是目的选项卡的代号。这里我们将新开选项卡的代号传人’就切换到了第2个选 项卡’然后在这个选项卡下打开—个新页面’再重新调用5w1t〔∩to.w1∩dow方法切换回第l个选项卡° 控制台的输出结果如下: 『〔帅j∩dowˉ4{58e3日7ˉ7167ˉ』587ˉbed+_9〔d6c867+吧5! ’ 0〔即i∩dowˉ6eo5十076~6d77ˉ453日ˉa〕6〔ˉ〕2baa〔〔447d{‖] ■■■■■■』■■■■■■■□■□■■■ ‖|但■厉||■■「『■■厂■||止■=尸}|卜





↑5.异常处理

在使用Selenjum的过程中,难免会遇到—些异常’例如超时、节点未找到等,一旦出现此类异常’

程序便不会继续运行了。此时我们可以使用tryexcePt语句捕获各种异常° 首先’演示_下节点未找到的异常,示例如下: 十ro『∏5e1e∩ju‖mportⅦebdriγer brow5er=webdrjγer。〔hro爬()

bro"5er.get(|http5://洲w.baidu.coⅦ|〉 bro训5er。十i∩d-e1e们e∩t-byˉjd(|he11o‖)

这里首先打开百度页面,然后尝试选择一个并不存在的节点,就会遇到节点未找到的异常。 控制台的输出结果如下:

广[〗『|[◆「卜|卜‖仆

‖o5u〔∩[1e∏论∩t[x〔ept1o∩「m〔eba〔促(Ⅷ5tre〔e∩t〔日111日5t) 〈ipy↑∩o∩ˉi∩putˉ23ˉ978945848a1b〉i∩〈咖du1e〉() 3bIo"5eI="ebdI1veI。〔hm爬()

』bI叫5er。get(0∩ttp5;//硼.baidl』.co∏) ˉˉ≡ˉ〉SbIo脚5er·「i∩d—e1e爬∩t-by-id(‖∩e11o‖)

可以看到’这里抛出了‖o5u〔h[1eⅦe∩t[x〔ept1o∩异常’这通常表示节点未找到°为了防止程序遇 到异常而中断运行,我们需要捕获这些异常’示例如下:

卜卜

千r咖5e1e∩1uⅦmportNebdIjγer

↑ro∏5e1e∩1uⅧ。〔o∏】Ⅷ∩.ex〔eptjo∩5mport『j『∏eo[』t[x〔eptjo∩』‖o5u〔∩[1e们e∩t[x〔eptjo∩ 『厂伊但尸|卜{■尸■|■尸「■■■『‖尸【■尸



「‖β



brow5er=眶bdrjγer.〔∩Io‖记() try:

bro切5er.get(』∩ttp5://….bajdu.〔oⅦ0〉 except『meout[xceptio∩8

prj∩t(丁j眶0ut0) try:

bro"5eI.「j∩d-e1e|‖e∩tˉbyˉjd(|∩e11o0) eX〔ept‖OS‖Ch[1e『‖e∩t[XCept1O∏8 pIi∏t(‖‖o[1e爬∩t0) +j∩a11y: bro阅5er。〔1o5e()

这里我们使用tryexcept语句捕获各类异常.例如’对查找节点的方法+1∏de1e阳e∩tˉby≡id捕获 ‖o5uch[1e『∏e∩t[×ceptjo∩异常,这样一旦出现这样的错误,就会进行异常处理’程序也不会中断。 控制台的输出结果如下:

■ ■

‖o[1e‖记∩t

厂 △ ■

关于更多的异常类’可以参考官方文档: http://seleniumˉpython.readthedocsjo/apihtml#moduleˉ

『 ■

selenium.common°exceptjons。

厂 ■ ■

↑6反屏蔽

■ 尸 ·

现在有很多网站增加了对Selenium的检测’防止一些爬虫的恶意爬取’如果检测到有人使用

■ ‖

Selenium打开测览器’就直接屏蔽。

■ ■ 厂

在大多数情况下’检测的基本原理是检测当前测览器窗口下的wi∩dow.∩aγ18atoI对象中是否包含



卜 卜 [ 庐

「}

q

第7章JavaScript动态演染页面爬取

224

q

J|}

webdrjγer属性°因为在正常使用测览器时’这个属性应该是u∩de「j∩ed, .一旦使用了Selenjum,它就

会给w1∩dow.∩aγ1gator对象设置webdrjγeI属性°很多网站通过JavaScript语句判断是否存在 Webdr1γer属性,如果存在就直接屏蔽。

一个典型的案例网站h仗ps://antispiderl.scrapecenter/就是使用上述原理,检测是否存在Ⅳebdr1veI 属性,如果我们使用Selenium直接爬取该网站的数据’网站就会返回如图7ˉ6所示的页面. +





×



+寸G

回”…↑…℃



■邮…∩ˉ睡……



§ 翼

c∩…王安瓤■动盯……° ←=■-ˉ=■=■=p

→p■≈=■■■■舔■■

→=■=■==≡■■=■■=■=■尸







●峦●



■<■■

■々=

W●bd『『ve「庐◎『b!dd@"·









■■纽■‖□■可■」■■司‖』旬■司■

这时可能有人会说直接使用JavaSc∏pt语句把侧ebdr1γeI属性置空不就行了’例如调用 exe〔uteˉ5〔ript方法执行这行代码: 0bject·de+1∩epropertγ(∩avjg日tor’""ebdrjver"’{get: () =〉u∩de+j∩ed})

这行代码的确可以把webdr1γer属性置空,但exe〔ute-5〔rjpt方法是在页面加载完毕之后才调用 这行JavaSc∏pt语句的’太晚了’网站早在页面谊染之前就已经检测"ebdrjγeI属性了,所以上述方 法并不能达到预期的效果°

在Selenlum中’可以用CDP(即ChromeDevtoolsProtocol’Chrome开发工具协议)解决这个问

可°另外’还可以加人几个选项来隐藏‖eb0r1ver提示条和自动化扩展信息’代码实现如下: 十rOⅦ5e1e∩1uⅦ1‖POrtwebdIiγer

+roⅦ5e1e∩1u‖·webdriγeri∏Port〔∩ro‖∏e0ptjo∩5 optjo∩≡〔∩ro们e0ptio∩5()

optio∩。addˉexperi′∏e∩ta1-optio∩(‖exc1ude5"1tche5‖’ [ 0e∩ab1eˉa0to爪己tio∩0 ]) optio∩.日dd—experme∩ta1ˉopt1o∩(‖u5e∧uto川atio∩[xte∩5jo∩` ’ 「a15e) browseI≡"ebdIiγer.〔∩ro们e(optio∩5=optjo∩) bro"5er。exe〔uteˉcdp—c|M(|page.日dd5〔rjpt『o[γ日1u己te0∩‖e汕o〔u川e∏t|’ { })



■‖』■■■可■■■二■■」·可|』■百

题,利用它可以实现在每个页面刚加载的时候就执行JavaSc∏pt语句’将"ebdr1γer属性置空。这里 执行的CDP方法叫作page.add5〔rjpt丁o[γa1uate0∩‖ew0o〔u∏e∩t’将上面的JavaSc∏pt语句传人其中即

‖‖』■■■勺』■■■|』·■、‖』□司∏■■司°日■·■·■■■

图7ˉ6使用Selenium打开测览器的结果

5ouI〔e0 ; 00bject.de千j∩eproperty(∩aγjg日tor’""ebdrjγer"’{get: () ≡〉 u∩de千1∩ed})‖

brO"5er.get(′httP5://日∩t15P1der1.SCrape.Ce∩ter/0)

这样就能加载出整个页面了,如图7ˉ7所示。





7.l

●_·。·

■■…7↑.西….■欣猛





e

225





※ 罢=ˉ_

_

■百=寓

Selenium的使用

同5c『°p· —

膨 羹剩』舔

■王别姬ˉPa『eWe‖Myc◎们cub;∩● ●●

95 ✩



lp凹内nˉ中区吕忌′↑7〗分睁

`w3o′ˉ出上撅

呻Ap 一_

← ■■■Q●■■■■■■卢已■■ ■■■■■弓■』p『■ ■=■q≈伯▲■~■■■少=■

=■■■■■■甲~■…■=D



这个杀手不太冷ˉL白◎∏



●■●■●●

9·5 台●白∩☆

隆凹′`腮沸p 0酗、碉叫二酿

于●



N



围V

肖申克的救赎ˉ而O5∩己W曰‖‖a∏kR●d●mpt‖◎∏ ●●

95 ★合

仿

贝酿′↑▲k勿仁

[7

09耙佃令00上烫

M^和b0■^们0悦 垂 沪





图7ˉ7加载出了案例网站的整个页面 ■■「仁|°■尸‖

■厂卜▲■■·「■尸》|■「■■尸||卜巴■「

p

}▲■■▲尸|■■ ■■■「‖卜「仙■尸‖’

P 匹■庐〖‖‖||‖■■匡ˉ■■「|‖[■■尸| 0

在大多数时候’以上方法可以实现Selenium的反屏蔽°但也存在_些特殊网站会对‖eb0r1γer属 性设置更多的特征检测,这种情况下可能需要具体排查。 ↑7.无头模式

不知道大家是否观察到,上面的案例在运行时’总会弹出一个测览器窗口,虽然有助于观察页面 的爬取状况’但窗口弹来弹去有时也会造成一些干扰。

Chrome测览器从60版本起,已经开启了对无头模式的支持,即Headless°无头模式下’在网站

运行的时候不会弹出窗口,从而减少了干扰’同时还减少了-些资源(如图片)的加载’所以无头模 式也在-定程度上节省了资源加载的时间和网络带宽。

我们可以借助〔们ro们e0ptjo∩5对象开启Chrome测览器的无头模式’代码实现如下: 十rα『) se1e∩ju"1们portwebdriγeI +roⅧ5e1e∩ju‖.webdr1γermport〔∩ro∏陀0ptjo∩5

Opt1O∩=〔hro∩论0pt1O∩s() opt1o∩.日ddˉargu{∏e∩t(‖-∩ead1e550)

brcw5er=webdrjγer.〔∩ro‖e(optjo∩s=optjo∩) brc|Ⅳ5er。5etw1∩dow51乙e(1366’ 768)

br〔|w5er.get(!https了//www.ba1dl」.co叮) bro"5er.get-scree∩5∩otas千i1e(‖preγjew.p∩g‖)

这里利用〔hro"e0ptjo∩5对象的add—argu‖e∩t方法添加了一个参数-∩ead1e55’从而开启了无头 模式。在无头模式下’最好设置~下窗口的大小’因此这里调用了5etw1∩dow51ze方法。之后打开 页面,并调用get—5Cree∩Shota5十11e方法输出了页面截图。

运行这段代码后,会发现窗口不会再弹出来了’代码依然正常运行,最后输出的页面截图如图7ˉ8 所示°

「}

第7章JavaScript动态演染页面爬取 …■…■■■■■哩m■■…

唾i擞酿 O回|菌篱ˉ穆 |





图7ˉ8输出的页面截图

↑a总结

现在,我们大体了解了Selenium的常规用法。有了Selenium’处理JavaSc∏pt喧染的页面不再是

‖‖』■|■■■‖·■‖勺

这样我们就在无头模式下完成了页面的爬取和截图操作。

】Ⅱ■■】】】】』●Ⅷ】□∏□】】‖】■·〗{|】■‖||■]|』】■‖】』■·】■】刁』■』〖〗·]·』‖』勺|』〗‖‖|□‖』β勺|』■■‖|■■|』

226

难事, 75节我们会用一个实例演示利用Selenjum爬取网站的流程°

本节代码参见: https://gjthub.com/Python3WebSpjder/SeleniumTest°

Sp|ash的使用 Splash是一个JavaScript喧染服务’是一个含有HTTPAPI的轻量级测览器’它还对接了Python中

的TWisted库和QT库。利用它’同样可以爬取动态喧染的页面° ↑.功能介绍

■‖‖‖||·可|』□

7ˉ2



d

□异步处理多个网页的痘染过程; □获取喧染后页面的源代码或截图;

□通过关闭图片擅染或者使用Adblock规则的方式加快页面喧染的速度; □执行特定的JavaSc"pt脚本; □通过Lua脚本控制页面的谊染过程;

□获取页面痘染的详细过程并以HAR(HTTPArchive)的格式呈现出来。

接下来’我们一起了解Splash的具体用法。

请确保Splash已经正确安装好并可以在本地8050端口上正常运行°安装方法可以参考https:〃setup. scrape.centeI/splash°

3ˉ实例引入

首先’利用Splash提供的Web页面来测试其谊染过程°例如’在本机8050端口上运行Splash服 务,然后打开http;/川ocalhost:8050/,即可看到Splash的Web页面’如图7ˉ9所示°

在图7ˉ9中,右侧呈现的是-个喧染示例’可以看到其上方有一个输人框,默认显示文字是 http:〃googlecom’我们将其换成https:〃wwwbajducom测试—下’换完内容后单击Renderme!按钮, 开始喧染’结果如图7ˉl0所示。 b



□甲』‖」|』■■■‖』日||‖|□□■■」■■|』‖』■■■■】‖‖‖』■■□||■■■‖』■司』】

2.准备工作

∩|‖】■‖]■■]」‖■】{·■■■』■‖|‖』{‖‖□〗‖]‖』■■γ‖

利用Splash’可以实现如下功能:

龋嫩娜…』 餐o产ˉ



Splash的使用

227

”蕊藕撼瓣恿蹿j鞠蹦



≡ˉ二

■ ★

■ ●

SP}as↑wS.5 :涸蹿『0臼向n睦〃0p?『wⅪ睡γ嘲攫FγNm.晦』『hF}『魁唾√β,! ″汕§锣邮p押;←°冒ˉ『凹绎口Ⅲ |∏·吠TY‘w酮。萨°PM↑≯.叮ˉˉ魁萨鸿

Ⅱ■ 啤1■0…■■hoGr7●』

?憾i凹』剃T〗〔i『 ●

G■它田佑仁《■p1&刀柑8忽@《p锣辨°丛m》》 ●巴●●rc《■P』△#n§必●人电《 》0

pk. ..8.q…0〗□F·· .巴幻·』0吨渔挠



d『

h亿■1攀■p1龄辙h腐斟酝其‖》’

打· ■ □.》P· ·电P. ≈.日m0的..□凸T▲■白D心晶田口0^·宁 0…●市■■0.0qM付

脚9户■p1■■绅初四『》职

G

● :o‖ 目彤 ●『.尸 ·■』□.■□召□f 0<·已l■Ⅶ■■凹.硝…V≈ ● ·.行£67.毗淘寸呻. ■





·p广b

·pr■■乒□□·■电尸凹日■■F■P□ □



hr■$pm毋hp闸▲∏ 》户 》

□P=.bT口■= 0尸. P· 』.…0晒U赴『.单→.0■

i瓣型‖;仔∏函d邮mγ孰u鸥Ⅲ《x了mm』旧|唾唾ⅢF?噬[险p …mq函酗UⅡm

[………》Ⅱ…肄舜.}……]

图7ˉ9

Splash的Web贞曲

■■

●们●



熏辑】g;闯…陆…时 .十



Go…钙…电…=…申…加■N□唾呛■冒…甜习a码27讳茹…■●回u…品…·…愚|u.…冒′…o、拘…几白力●

…ˉ‖√……= …F■

s圃a‘…°…厕…≡些 =-■■二 南‖

■颠一心 ■西≡=…凶酶

■匈≡咖 ●卤~酗 ●ˉ■●





{疆

■画~=獭

●■—m ■叮—■ ■m一■

●;墨

一| ■■■■■■■■■■■■ ……≡≡

·硒



二 ˉˉ

■颤—m ●m—硒 ●m-…砌



午■叮

=≈ ~ =……—=

■…■■■

Ⅷ尸■■■■

●壹

●霉…塞

凸一L‖





■臼一吨

】—一__

【→_≡=_~—



7.2

Ⅱ血

勺▲■

图7ˉ10檀染结果

■『■一可声…廷·≡

』 ■ 司 | ■ ■

■■‖‖凸勺】■可

第7章JavaScript动态 渣染页面爬 取

228

千u∩ctjo∩"aj∩(5p1a5h’ arg5)

‖』|』‖■■

那么’这个过程由什么控制呢?我们返回首页’可以看到这样一段脚本: 尸

a55ert(5p1aS∩;go(args°ur1)) a55ert(sp1a5h:wajt(O5)) ret0r∩{ht肌1=5p1己5∩8∩t们1()’ p∩g≡Sp1a5h:p∩g()’ har=5p1a5hthar()’}

■■■‖‖□·■

喧染结果中包含痘染截图HAR加载统计数据和网页的源代码°Splash擅染了整个网页’包括 CSS、JavaSc∏pt的加载等,最终呈现的页面和在测览器中看到的完全一致°



e∩d

这个脚本是用Lua语言写的°即使不懂Lua语言的语法’也能大致看懂脚本的表面意思’首先调用

「■

至此’我们大体了解了Splash是通过Lua脚本控制页面的加载过程,加载过程完全模拟测览器,

■□Ⅵ‖

go方法加载页面,然后调用w己jt方法等待了_定时间’最后返回了页面的源代码`截图和HAR信息。



最后可返回各种格式的结果’如网页源码和截图等°

接下来’我们就了解一下Lua脚本的写法以及相关API的用法°



Splash能够通过Lua脚本执行_系列喧染操作’因此我们可以用它模拟Chrome、PhantomJS。

‖‖‖

4.Sp|as∩[ua脚本

先了解-下SplashLua脚本的人口和执行方式°

』‖□勺‖‖■·‖

●入口及返回位

来看一个基本实例:

■{‖

+u∩〔tiO∩∏日j∩(5P1a5‖’ arg5) 5p1a5b;go("http://州.ba1du.coⅧ") 5p1日5h:"ait(0.5) 1oca1t1t1e=5p135h:eva1j5("docu爬∩t.t1t1e") retur∩{tjt1e≡tjt1e} e∩d

将这段代码粘贴到图7ˉ9中的代码编辑区域’然后单击Renderme!按钮,返回结果如图7ˉll所示°

··际辑,国|=≈重百]+ b

G

@

蹿幽k詹c……□…≡肇★力●!

o

5p!aS‖γ3.5

……|………|■■■■

……呵mm…T它…

』|‖

÷

伯”厂●它■

…=… ●△●n▲0 凶酗≡下…刃口

■‖|‖

运行结果

·《|■

图7ˉll

可以看到,喧染结果中包含网页的标题°这里我们通过eva1j5方法传人了JavaSc∏pt脚本’而 doCuⅦe∩t.tjt1e返回的就是网页的标题’ eγa1j5方法执行完毕后将标题赋值给t1t1e变量,随后将其 返回°

‖|√

注意,我们在这里定义的方法叫‖aj∩。这个名称是固定的’Splash会默认调用这个方法。刚a1∩方 法的返回值既可以是字典形式,也可以是字符串形式’最后都会转化为Splash的‖TTP响应’例如:

』■、‖凸■

||}

72 SplaSh的使用

229

十u∩〔t1o∩∏a1∩(5P1a5∩) Ietur∩{he11o≡腻ⅣoI1d|"} e∩d

返回的是字典形式的内容。下面的代码: |β■「| 卜 尸 | 『 [ 尸 β ■ 『 『

十u∩〔t1o∩Ⅷ日j∩(5P1a5∩) retur∩ 0he11O‖

e∩d

返回的是字符串形式的内容° ●并步处理

『■尸『卜■■■『■■尸‖》|}Ⅳ「|)|′ 尸 | ■ ■ 尸 ‖ [ ■ ■ ′ 卜

sh支持异步处理但是并没有显式地指明回调方法’其回调的跳转是在内部完成的°示例如下照 Splash支持异步处理但是并没有显式地 +U∩CtiO∩"aj∩(5D1a5h. tiO∩川a1∩(5p1a5h’日m5) aIg5) 00

1o〔a1exa川p1e一‖r1s≡{"洲w.ba1duco""’| |www.taob己o.coⅧ"」 WWW.z∩jhl」.〔OⅦ"} 1o〔a1ur15=arg5°‖r15oIexaⅦp1e_ur15 1oCa1Ie5u1t5= {} 「or1∩dex’ ur1i∩1pa1r5(仙I15)do 1o〔日1o悯’ re35o∩=5p1a5∩:go(』』∩ttp://" . . (」r1) i+O促t∩e∩

5P1a5∩:wajt(2) re5u1t5[ur1] =5p1己5∩:p∩g() e∩d

厂■‖|■■「》||厂似■’■『「卜



e∩d retur∩re5u1t5

e∩d

运行这段代码后的返回结果是代码中3个网站的页面截图’如图7ˉl2所示° P司

◆ ‖■ 各凤

【慧…,…·轿.^ ∑ +

°

Go熔…kS…颇舱?……6a财na碑■`…刘≡泌闸…ot蕾瓣酗贝赵睡舔弘哭酷鸭和w馋愈…您…汹延碑口c·口h眶囱b·…肌2鞠…. ☆力●『

…’……h·愈…|….|阎…|

5p|a5h`′35……m。"……

■庐「『■尸|■β_尸》■尸|卜■『■厂|卜匹■『尸》「|■尸「|》△■「『■■‖(■『「『|■厂}|「[■厂

SP1■■hm■严·●·酌0■●t

…°m1如·c■〗…● (P励g0 】0$▲Ⅲ760) Ⅺmb凸寸Td ●■



■■吧●■



■Q

田 ■■■ ■甲■

■忙电…■■■·

凰.窒≡

■■■…■■■■

■←■

■■■T■■■

~p←

….cm…■c■0 【■沪 ■■■

■=

■=

■ □■■●

■…々·■.≈

■O

Hb

0 ←■耘



Ⅲ悉】ˉ § 』

…鸽 ■●●■ ■■■宁■

■ ◆

中甲…■



≡■ …=













矽 ■■



●哪

…■

■w

Eh止hM巴巴■R X靶g● (P钝g″ 】02dH760)…1酵□

图7ˉl2运行结果



第7章JavaScript动态演染页面爬取

230

代码中调用的wa1t方法类似于Python中的51eep方法’参数是等待的秒数。当Splash执行到此 方法时,会转而处理其他任务,在等待参数指定的时间后再回来继续处理。

试里值得注意的是· Lua脚本中的字符串拼接和Python中不同’它使用的是“..”操作符’而不是 “+”°如果有必要,可以简单了解_下L皿脚本的语法’详见http:/)Www.mnoohcom/lua/luaˉbasicˉsyImxh‖ml°

另外’这里设置了加载页面时的异常检测°gO方法会返回加载页面的结果状态’如果返回的状态 码是4×x或5XX,那么O促变量为空’就不会返回加载后的图片。

5. 5p1a5‖对象的属性

能够注意到,前面例子中‖a1∩方法的第一个参数是5P1a5h,这个对象非常重要’类似于Selenlum中 的‖eb0r1γer对象我们可以调用它的一些属性和方法来控制加载过程。接下来’先看5p1a5∩的属性。 ●arg5属性

0

该属性用于获取页面加载时配置的参数’例如请求URL°对于GET请求, arg5属性还可以用于

获取GET请求的参数;对干POST请求, argS属性还可以用于获取表单提交的数据°此外’SplaSh支



Ⅵ』‖

持将川a1∩方法的第二个参数直接设置为arg5’例如: 十0∩〔tiO∩Ⅶaj∩(5p1a5‖’ 己rgS) 1O〔a1uI1≡ar85°‖r1 e∩d

这里的第二个参数arg5就相当于5p1a5‖.a士g5属性,以上代码等价于: 十u∩〔tiO∏∏己i∩(5p1己S∩) 1oca1ur1≡5p1a5h·arg5.ur1 e∩d

■■』』〗||■·』】■|■■Ⅵ‖‖』可|』■■‖‖』□■一■■‖|」□■司

●j5ˉe∩ab1ed属性

这个属性是Splash执行JavaSc门pt代码的开关’将其设置为tme或+a15e可以控制是否执行 JavaSc∏pt代码,默认取true°例如:

‖』■∏」■』■|』』■‖‖」』■·可|‖|』■·|

「u∩ctio∏帕j∩(5p1a5打’ ar8s) 5p1a5h:go("http5://卿·baidu.〔o∏‖阔) Sp1玉h.js-e∩ab1ed≡「a15e 1oca1tit1e=5p1ash自eγa1js("docu爬∩t.tjt1e") Ietum{tjt1e=t1t1e} e∩d

这里我们将j5-e∩3b1ed设置为「a15e,代表禁止执行JavaSc前pt代码’然后重新调用eva1j5方法 执行了JavaSc∏pt代码,此时运行这段代码,就会抛出异常: { ■

error府: 4"D





酝55age口: ■[5tri∩g\口十u∩ctjo∩爬i∩(sp1a5h’ aIg5)\r…\铡]:d: l」∩k∩酗∩〕5eIror: ‖o∩e阐’

001j∩e∩u∏ber": 4’ eIror曰吕 阅l』∩代∩ow∩〕5eIror: ‖o∩e口》 ■

"5p1a5h爬t∩od": "eva1j5" }’ 口de5criptio∩闻: 口[rrorhappe∩edwhi1eexe〔l」tj∩gLl』a5〔rjpt耐 }

不过,我们_般不设置此属性’默认开启。 ●resOurcetj贩Out属性

此属性用于设置页面加载的超时时间,单位是秒。如果设置为0或∩i1(类似Python中的‖o∩e),

刮■、‘||』■■可』□|||』=■■□|』■可||||■=■■Ⅵ|■■‖′■曰■■叫Ⅷ刘■■■■∩、(|」■】口··]‖|』口■■□一

因type口: w5cript[rror词’ "j∩十O闻; {国tγpe■: 。〕5[R【0R回’ "js-erroI眶5sage闻: ∩u11’ 5our〔e圃: "[stri∩g\闻千u∩〔tjo∩帕1∩(sp1as∩’ arg5)\r…\喊]0』’

72 Splash的使用 代表不检测超时°示例如下: 于u∩〔tio∩Ⅶaj∩(5p1a5∩) 5P1a5h。re5OUr〔eti们eOl」t=0·1

a55ert(5P1a5h:go(|https://…°taobao.Co‖0 )) retur∩5p1a5h:p∩g() e∩d

这里将超时时间设置为了0.l秒°意味着如果在0.l秒内没有得到响应,就抛出异常: { 00

erIoI00 : 40o’

"type": "5cript[Iror"’ "i∩千O": { "errOr": "∩etWOr低5"’ 00type"8 "山∧[RR0R"’ 001j∩e∩u∏ber00吕 3,









23]

"5ouIce腻: "[5trj∩8\"千u∩〔tio∩ |∏ai∏(5p1日5b)\r…\腻]0!’ 爬55age||: "[uaerror: [5tri∩8\n十u∩ctio∩"ai∩(5p1a5h)\r…\"]:3: ∩et"or代5" ■

}’

"de5cr1ptio∩": "[Irorhappe∩edwhj1eexe〔uti「lg[u己5〔rjpt" }

此属性适合在页面加载速度较慢的情况下设置。如果超过某个时间后页面依然无响应’则直接抛 出异常并忽略。

●mageSˉe∩己b1ed属性 此属性用于设置是否加载图片,默认是加载。禁用该属性可以节省网络流量并提高页面的加载速

度,但是需要注意,这样可能会影响JavaScript喧染。因为禁用该属性之后,它的外层DOM节点的 高度会受影响,进而影响DOM节点的位置°当JavaScript对图片节点执行操作时’就会受到影响°

另外有_点值得注意’Sp‖ash会使用缓存。意味着即使禁用j们agesˉe∩ab1ed属性’一开始加载出 来的网页图片也会在重新加载页面后显示出来,这种情况下直接重启Splash即可°

禁用mage5≡e∩ab1ed属性的示例如下: 十l』∩〔tjO∩归j∩(5p1己5h’ aIg5) 5p1a5h.i阳ges-e∩ab1ed=伯1se a$5ert(sp1a5h:go(,‖ttps://….jd。co∏|!)) retuI∏{p吧=5P1a5h:P∏g()} e∩d

这样返回的页面截图不会带有任何图片,加载速度也会快很多。

●p1ugj∏5-e∩ab1ed属性

此属性用于控制是否开启测览器插件(如Flash插件),默认取十a15e’表示不开启。可以使用如 下代码开启/关闭p1l』gi∏5-e∩ab1ed: 5p1a臼h°p1u8j∩s一e∩己b1ed=tIue/十a15e

●5αoU-mSitim局性

此属性可以控制页面上下滚动或左右滚动,是_个比较常用的属性°示例如下: 千u∩〔tjo∩硒i∏(5p1a5h’ aIg5)

|翻(耀墓蹦(蝴·,…) e∩d

这样可以控制页面向下滚动啡00像素值,运行结果如图7ˉ13所示°



7 匹

厂■■■■■■

× 尸≡









★力

G● →■宰■■■■■=…≈

Spla5∩γ35…●硒8肛l

矿口■

″◎

→ ■

獭嚣 苯…a5‖ ←

』■■□·‖‖γ■■||■■■■〗‖|■‖■■

第7章JavaScrjpt动态演染页面爬取

232

Qr■▲呼

≈0「ce〔@d●

雁蒜F瑟]

巳P1■#h贮■湃n■●;酶jm亿

严钉■ I…庐{…’1OJ0■76□〗士■‖… 尸■

…臼龄

■■…≈

■■■ <份…

可 ■

n■丙■■

…弛口→

■-‖

■=←





■…■

■■





■■







声■

仰●

=■



出^簿



■●

呼审凸■口 铂■~

| (

■■‖ □



‖‖《

图7ˉl3设置5cIo11ˉpo5jtio∩属性后的运行结果 如果要让页面左右滚动’可以传人X参数,代码如下: 5p1a5∩.5〔ro11ˉpo5itjo∩≡{x=10qy=2OO}

6.5P1a5h对象的方法

●go方法

{|



除了前面介绍的属性’ 5p1a5∩对象还有如下方法。

该方法用于请求某个链接’可以模拟GET请求和POST请求,同时支持传人请求头、表单等数



〈{

据’其用法如下: o代’ Iea5o∩=5p1a5∩;go{ur1’ b日5e0r1≡∩11』 header5=∩j1」 httpˉ爬thod≡!℃[「0‖’ body=∩j1’ 十omdata=∩i1}

对其中各参数的说明如下°

□uI1:请求URL°

□ba5eur1:资源加载的相对路径,是可选参数’默认为空°

□httpˉ"ethod:请求方法’是可选参数’默认为C[丁’同时支持P05丁。 □body: ∩ttpˉⅧet∩od为p05「时的表单数据’使用的〔o∩te∩tˉtyPe为aPP11〔at1o∩/j5o∩,是可选

|‖

参数,默认为空。

』■■■||』‖』■■〗

□∩eader5:请求头’是可选参数’默认为空°

□十omdata: httpˉ"et‖od为p05丁时的表单数据’使用的〔o∩te∩tˉtype为app1j〔atio∩/xˉw"wˉ +omˉur1e∩〔oded’是可选参数,默认为空。



该方法的返回值是o促变量和rea5o∩变量的组合’如果oⅨ为空’代表页面加载出现了错误’rea5o∩ +u∩〔tjo∏Ⅶaj∩(5p135h’ arg5)

1o〔a1o长’ rea5o∩=5p1己5‖:go{碱‖ttp://哪.∩ttpbi∩.org/po5t撼’ httpˉ爬thod=赋p05『"」 body="∩a爬≡6emey"} j十O代t∩e∩

■■‖‖{」‖||‖||』■■可

中包含错误的原因’否则代表页面加载成功°示例如下:

retur∩5p1a5‖:ht"1() e∩d e∩d

」‖

这里我们模拟了-个POST请求’并传入了表单数据’如果页面加载成功,就返回页面的源代码°







| 「 } |

i 72 Splash的使用

233

运行结果如下:

〈∩t‖1〉〈∩ead〉〈/∩ead〉〈bodγ〉(pre5ty1e≡"wordˉwIap『 bre日|(ˉword』 whjteˉ5pace: pIeˉwrapj"〉{"arg5"; {}’ 『■「|卜止■「||『》『「‖′仿|}[β′尸||『|■厂′■■■■■「|广|卜‖仿「|β|巴「巴『|■β「卜‖)卜∏厂|β)巴尸‖卜任「β′●「β『凶【尸|}●■β|■尸『|β■■「‖》‖尸■『【厂〖■「『·■■‖||「厂》|[■■『‖||■

"data0!: ""′"「j1e5": {}’"fom": {"∩a爬":"Cemeγ!!}’"header5": {"∧〔cept";!』text/∩t川1’app1i〔at1o∩/ xhm1+x田1’aPP1j〔atjo∩/x们1jq=O.9’*/*iq=O·8"’"A〔〔eptˉ[∩codj∩g00 ;"gzjp’ de+1ate"』α∧cceptˉ[a∩guage":"e∩’中"’

||〔o∩∩ectjo∩0‖:||c1o5e"′"〔o"te∩tˉle∩gt∩":"11"’"〔o∩te∩tˉ丁γpe":"app11catjo∩/xˉw0州≡千om)ˉur1e∩〔oded"’"‖o5t|′: 删w.∩ttpbj∩.org′』’|!Origi∩":"∩u11"′"05erˉAge∩t":!0№zi11a/5.o(X11j [i∩ux×8664)∧pp1e‖eb倔it/6o2.1 (刚『川’ 1jkeCec代o) 5p1a5hγer5jo∩/9.O5a「己r1/6O2.1"}’"j5o∩": ∩u11′"origj∩";"6O.2O7.237.85"’"ur1": 00

"∩ttp://w枷°∩ttpbj∩·org/po5t" 】

『 』

〈/pre〉〈/body〉〈/∩t爪1〉

可以看到’成功实现了POST请求并发送了表单数据° ●"ait方法

此方法用于控制页面等待时间,其用法如下: o促」 rea5o∩=5p1己5∩;Ⅳait{ti『‖e’〔a∩〔e1o∩redjre〔t=千a15e’ ca∩〔e1o∩eIror≡true}

对其中各参数的说明如下°

□t1Ⅶe:等待的时间,单位为秒°

□〔a∩〔e1o∩red1rect:如果发牛了重定向就停止等待,并返回重定向结果’是可选参数’默认 为+日15e°

□〔a∩〔e1o∩error:如果页面加载错误就停止等待,是可选参数,默认为「a15e。

其返回值同样是O低变量和rea5O∩变量的组合。 我们用_个实例感受一下: 千0「ctio∩‖3j∩(5P1日5h) 5p1a5h:go("∩ttp5://州.taobao.〔o|∏") 5p1a5h:Wait(2) ret(」r∩{∩t爪1≡5P1己5h:ht∏1()} e∩d

执行如上代码,可以访问淘宝页面并等待2秒’随后返回页面源代码° ●jS十u∩c方法

此方法用于直接调用JavaScript定义的方法’但是需要用双中括号把调用的方法包起来,相当于 实现了从JavaSc∏pt方法到Lua脚本的转换°示例如下: +0∩〔tjo∩阳3j∩(5p1a5h’ aIg5)

1o〔a18etˉd1γˉcou∩t=5p1a5h:js十u∩〔([[+(』∩〔tio∩ (){ γarbody=do〔u爬∩t·bodyj

γardjv5=body.get[1e耐e∩t5By丁ag‖日爬(0djγ|〉; retUr∩djv5.1e∩8thj} ]])

5p1a5h:go("∩ttp5://洲w.bajdu.〔oⅧ") retur∩("丫hereare咒5DIγ5");十or阳t(getˉdiγ-cou∩t()) e∩d

这段代码的运行结果如下: 『hereare210Iγ5

这里我们先声明了一个JavaSc∏pt定义的方法get-d1γˉcou∩t’然后在页面加载成功后调用此方法 计算出了页面中d1γ节点的个数。

关于从JavaScnpt方法转换到Lua脚本的更多细节,可以参考官方文档: h仗ps://splashreadthedocs, io/en/stab‖e/scriptingˉrefhtml#splashjshmc°

第7章JavaScript动态演染页面爬取

234

·eVa1j5方法

此方法用于执行JavaSc∏pt代码并返回最后—条JavaScript语句的返回结果,其用法如下: Ie5u1t=5p1a5h:eγa1j5(j5)

例如,可以用下面的代码获取页面标题: 1o〔a1tjt1e=5p1己5h:eγa1j5("docu爬∩t.tit1e")

●ru∏j5方法

此方法用于执行JavaSc∏pt代码.它的功能与eγa1j5方法类似’但更偏向于执行某些动作或声明 某些方法。例如:



叫‖■‖·

于u∩〔tjo∩们aj∩(sp1a5h′ 3rg5) . 5p1a5h:go(0|打ttp5://0栅‖.bajdu.coⅢ") sp1己5h:ru∩j5("十oo=十u∩ctio∩(){Ietur∩ !b己r‖}") 1o〔a1re5(」1t≡5P1a5h;eγ己1j5(』干oo()0』) retur∩re5u1t e∩d

这里我们先用ru∩j5方法声明了-个JavaScnpt方法十oo,然后通过eγa1j5方法调用+oo方法得 ‖

到的结果。

运行结果如下: bar

可以看到,这里我们成功模拟了发送POST请求,并发送了表单数据。



●ht‖1方法

e∩d

|](

+‖∩Ctjo∩ |∏ai∩(5p1aS∩’ 己Ig5〉 5p1a5h:go(阔‖ttp5://….httpbj∩。oI8/get闻) IetuI∩5p1a5h巾t们1()

□ 〗 ■ ■ Ⅷ 』 · | □ ∏ 日

此方法用于获取页面的源代码’是一个非常简单且常用的方法,示例如下:

运行结果如下:

口A〔〔ept曰: 口text/htⅦ1』己pp1j〔atjo∩/xht爪1+×"1’app1jcatjo∩/咖1jq=0.9』*/*jq=o°8口’ "∧〔〔eptˉ[∩〔odj∩g": 口gzjpJ def1ate。’ ■∧〔Ceptˉla∩guage口: 闻e∩’*"’ 口〔O∩∩e〔tjo∩口: ■〔1o5e口D

|]

.headeI5口:{

‖ 』 ■ ‖ ■ ■ ■

<ht"1)〈he己d〉〈/head〉〈body〉〈pre5ty1e="mrdˉNrap: brea|〈ˉNordjNhiteˉspace: preˉ们mpj同〉{"3r85": {}’

回‖o5t"吕 闻….httpbi∏·or8口》

·05erˉ∧ge∩t词: ·№2i11a/5.0(X11β ti∩l」xx8664)∧pp1e"ebⅨit/602.1 (长‖『"[’ 1j仪e6e〔代o) 5p1a5hγersjo∩/ 9·05a「arj/6O2.1口

●p∏g方法 此方法用于获取PNG格式的页面截图,示例如下:

e∩d

□可■】·

「u∏〔tjO∩mj∩(5p1a5∩’argS) sp1己B∩:go(.∩ttp5;//…。taobao。〔m.) retuⅢ∩Sp1己5h:p∏8()

‖|』■·日||』』γ|

}’ ■origj∩.: □6o.2O7.237°8S口’ ■uⅢ1国8 .打ttps://榴.httpbj∩.org/get. } 〈/pre〉〈/body×/ht阳1>

●jpeg方法 此方法用于获取JPEG格式的页面截图,示例如下:

‖|



「「

『‖‖巴■『「‖|卜

7。2

Splash的使用

235

0

千u∩〔tjo∩‖a1∩(5p1a5∩’ arg5) 5P1as∩:go("∩ttp5;//www.t3obao.〔o们")



retuI∩5p1日Sh:jpeg() e∩d



『|

●h已r方法

此方法用于获取页面加载过程的描述信息’示例如下: 十u∩ctio∩‖己1∩(5p1a5h’ arg5) 5p1a5∩:8o("http5://州.ba1dl」.coⅦ")



卜■‖

retur∩sp1a5∩:har() e∩d

运行结果如图7ˉl4所示°

b

鸟三豆『舅

×[

…9ˉ5『

‖】



△■「■『‖止尸

÷十



● O

……~『匹…●″■~

忘云云.

》‖■■‖匹■尸∩尸

嘲凰…

●』= …

=〃…

●!

■……



;窟:兰

… 空▲q

■ ←d≈

出醛下o…

=““

2鲤匹

■“V…∏…砌m

0·6四

■面=什 .垒

卜 p

■卸队…咎

■|■■■■……

▲■『卜庄■尸

■面劈【=·k…啤10o6心

■■■■■■■呻.四海

■面■锤犹…牢2铀砷1S■叫 忠匹丁~=;2咖“ 2Dm

■■■■胆·… ■|■■■■■……0

■“丁==№3●F】■“ 3』m 破皿丁…←蛔恼】“■卫1m

■■■■■…… ■■■■■】ol.…

■唾丁-←…“α0挝■

p

Ⅱ萨9Ⅸ6

必面…■【≈忍切“ ■m邑<1…吉叼“

]■m 3蕾6m

田呵……7酗“

1】唾

■面……白酗蚀

L3m

■■■■■■■D77·咎≡ -B沂≡ ■■■■■■■■Ⅱ秆≡_ -Ⅱ混≡-4袒…

槛“丁…】蜘21洒皿】酮蹿』0】0刚 》■■尸『

u回丁…≈

2锄“



6o9∏■

■α丁…≈ r酗“M·5邓 ◆唾丁…钳…】锄蚀 了βⅫ

■■■■】苑鲍■



■■■■■■■p”…口

!

■面=▲m引=≡〕8锤唾鲍.了m

-…ˉˉ 〖

■■7●≡…■■S52锄酗

匡二哪

“Q0

■m丁…=…酗Ⅱ幽“"』凹



·≡ hd . -=…≡…一ˉ.蓖·怪≡ˉ

ˉ—亏了m顾卤— { ■■■■■■■q〗,“‘ -goα憾0

■面…舆≈碑“ △〗m ■匹丁趴……G枷“蜘1.〗则



】】捶=



●呸丁凶

∩■哩Ⅱ∑≈】刨呻

■~■ =

■ .=▲≡

队】m

□■■l麓嘿~

■^∩……由U■

图7ˉl4 ∩ar方法的运行结果

} 卜|血∩|‖

这张图里显示了百度页面加载过程中的每个请求记录的详情° ●ur1方法

此方法用于获取当前正在访问的URL,示例如下:

巴『【}亡■「

「|『「广‖■β‖‖『【■■■■■「『‖■尸‖〖■′



+u∩〔tjo∩爬1∩(5p1ash’ 己rg5) 5P1己5∩:8o(词http5://‖州.baidu.〔o∏!) retur∩5p1aSh:ur1() e∩d

运行结果如下; ∩ttp5://…·ba1du.〔咖/



β【「‖

■面……■=黔…■

‖ L呻

恕…

S2m

■C叮■】了■丁…耙沁别蜘“■4】醒 ▲

||

出唾丁…=…R奶碑

■=】‖|‖」·习|□」■■■‖‖』■门|‖』]■·■』|]‖·■|」】■】■■】■‖●】|】■]‖〗』|·|勺』口·■■

第7章JavaScript动态演染页面爬取

236

●setu5er且8e∩t方法 此方法用于设置测览器的05erˉ∧ge"t,示例如下: 十u∩〔tjO∩们己j∩(5p1a5h〉

5p1己5h:5etu5erˉage∩t(05p1a5h0)

5p1a5h:go(百∩ttp;//bw‖w.∩ttpbj∩。oIg/get") re↑ur∩5p1a5h:hm1()

e∩d

这里我们将测览器的05erˉ∧ge∩t属性设置为了5P1a5∩,运行结果如下: 〈‖tⅧ1〉<head〉</he日d×body〉<pre5ty1e="wordˉ"rap: bre冰ˉ"ordj whiteˉ5pa〔e: preˉ"r己pj"〉{"arg5": {}’ ‖‖he己der5": {

"∧〔cept!0 : 00text/htⅧ1’app1j〔atjo∩/xhtⅧ1+xⅦ1’app1j〔at1o∩/xⅧ1jq=o·9』*/*;q=o°8"’ "∧c〔eptˉ[∩〔odi∩g|0 : "gzip’ de+1己te"’ ∏

|0∧〔〔eptˉ[己∩g0age :

00



e∩,*"’ ■】

"〔o∩∩e〔tjo∩": 00〔1O5e ’

q

0U

"‖o5t": "棚w。bttpbi∩·org 』

"05erˉAge∩t": "5p1a5‖||

■■|■■】

}’ "oIigi∩": "60·207°237·85"’ "uI100 : "们ttp://州·‖ttpbi∩.org/get00 } </pre〉〈/body〉</ht∏1〉

已】』‖』■■]√Ⅵ□

可以看到’我们设置的05erˉ∧ge∩t属性值生效了° ●Se1eCt方法

d

于u∩〔t1O∩们aj∩(5p1a5h) 5p1a5h:go(闻∩ttp5://硼w。baidu。〔o阳/") 1∩put≡5p1a5h:5e1e〔t("毗"") 1∩p‖t:5e∩dtext(05p1a5‖`) 5p1a5‖:w日jt(3) retur∩5p1a5∩:p∩g()

q

试里我们首先访问百度官网’然后用5e1eCt方法选中搜索框’随后调用5e∩dteXt方法填写了文 本’最后返回网页截图°运行结果如图7ˉ15所示° 了■尸 ≈』∏



_隔

0

@

★内●§

》·o

印|鸣hv3°5

D。cⅦ啪∩切j℃"

■啦它HC…

鲤c乙g9





=一→=~=一一一=一

鞠』…~β 2…!mp…l响qm“)…一瞬-

■■■ 7

=■■

0

ˉ 图7ˉl5运行结果

■■』■】‖‖‖夕·‖|||」■■】■日|||」■■■■■■□■■‖■■■■■■



■{‖』■|」·】■|■】‖‖』·』■=‖·■■■|{二■三■]|·】■■‖|{■■勺□■‖|‖‖‖■司□□|』■|‖{』■■]‖|

e∩d



』■】‖」■

选择器°示例如下:

{{

该方法用于选中符合条件的第一个节点’如果有多个节点符合条件’则只返回-个,其参数是CSS

卜}

72 Splash的使用

237

可以看到,我们成功填写了输人框° ■■■■「‖{}『■■■「「{|卜世■■「‖■■口}

广

●se1eCta11方法 ■■■

此方法用于选中所有符合条件的节点’其参数是CSS选择器。示例如下:

△■『’Ⅱ■▲■『

旬∩CtjO∩们己j∩(5P1a5h) 1oca1treat≡req‖jIe(0treat|) a55eIt〈5P1a5∩:gO("∩ttP;//qUOte5。tO5〔r日Pe.CO川/")) a55ert(5p1ash:"己jt(o.5)) 1oca1text5=5p1a5∩:5e1ectˉa11(0 .qqote .text0) 1O〔a1re5u1t5={} 千oIj∩dex′textj∩jpajr5(text5)do resu1t5[j∩dex] =text·∩ode。i∩∩er‖丁肌 e∩d

retur∩treat.aS己rmy(re501t5)

p

e∩d ■■「||卜巴■厂「『『■■匹口

这里我们通过CSS选择器选中了节点的正文内容’然后遍历所有节点,获取了其中的文本° 运行结果如下:

■「「『

【尸『》‖■「■■尸『β●「》●『『■【‖皿■■队目β)■尸

5p1a5‖Re5po∩5e:∧Imy[10] 0: "“「bewor1da5we∩aγe〔re己tedjtj5己proces5o十ourtM∩代j∩g. It〔a∩∩otbecha∩8ed训jt∩out〔∩a∩8j∩gour thi∩R1∩8·”!|

1: "“It15our〔bojce5』‖己rry』 that5‖咖晌3twetrl』1yare’ 「arⅧIet‖日∩ourabi1jtie5·”" 2:叮‖ereareo∩1yt"o"aysto1iγeyol』r1i+e.0∩ei5a5t∩ougb∩oth1∩gj5己∏ira〔1e·『heotherj5a5t∩ough



everytM∩8i5amrac1e·”

3: "“『‖eper5o∩’bejtge∩t1e■a∩or1ady’咖o∩as∏otp1ea5urei∩已good∩oγe1’刚5tbei∩to1emb1γstupid°”" 4; "“IⅦper千ectjo∩1sbeauty』爬d∩e55jS8e∩i‖5a∩dit,5bettertobeab5o1ute1yr1d1〔u1ou5tha∩己bso1‖te1y borj∩g.”w

5: 00叮ry∩ottobeCo‖|ea阳∩o千5u〔Ce55. Ratherbecα‖ea∩a∩o+γa1ue.’’00 6: 曰“Itjsbettertobehated千or灿atyou己ret‖已∏tobe1oγed+orwhatyouaIe∩ot°’’" 7: "“I∩aγe∩ot+己j1ed.rγeju5t十ou∩d1O’O0ONay5that灿∩|t"or促.”"

88 圆“∧咖∏E∩151jkeate日ba8β you∩eγer低∩o"∩o们5tro∩8jti5u∩ti1jt‖si∩hotNater。”" 9: !0■Ad日y切jthout5u∩5M∩ej51ike’youk∩ow』 ∩ight·”00

可以发现,我们成功获取了10个节点的正文内容° ●咖u5eC11Ck方法 ■■■



β【■『|■[■厂■■『「‖·■日『》■「》‖巳■『巳■『『「■■广||■尸卜》β■↓止■『『

此方法用于模拟鼠标的点击操作’参数为坐标值X`γ°我们可以直接选中某个节点直接调用此方 法’示例如下: 十u∩〔tjo∩阳1∩(5p1日5h)

Sp1a5h:go("http5://Ⅶ州.b日jdu.co们/") j∩Put=5P1a5h:5e1e〔t(n批W) i∩put:5e∩dtext(05p1a5‖0) 5p1a5h:"己it(3) 5ub∏1t=5p1a5h:5e1eCt(|#Sq‖) 5u咖it:咖u5e〔1iC促()

5p1a5∩:Nait(5) retur∩5p1a5∩:p∩g() e∩d

·

这里我们首先选中页面的输人框’向其中输人文本5p1a5h’然后选中提交按钮’调用Ⅶou5eˉ〔1j〔促 方法提交查询,之后等待5秒’就会返回页面截图’如图7ˉl6所示。





第7章JavaScript动态演染页面爬取

=—

■=圭—=--

·`◇.『紊≡5五|

……×

÷◆◎







o

●=

…■■■■=■■■

迁#坞钾■哮m …

←→

==●·=●● _平

二■=哇0■□●-

—~

=■■■-≡→唾

≈_Q …一=·~

…=

_

■z△一 、~■… ≡ =≡…=

戴静==轰 —■

=-

』查≡≡冠

可以看到’我们成功获取了查询后的页面内容,模拟了百度的搜索操作°

至此, 5p1a5∩对象的常用方法介绍完毕’还有-些方法这里不—-介绍了,更加详细和权威的说 明可以参见官方文档https:〃splashreadthedocsjo/en/stable/sc∏ptjngˉrefh|ml,此页面介绍了5p1a5h对象的 所有方法。另外,还有针对页面元素的方法’见官方文档https:〃splashreadthedocsjo/en/stable/scnptingˉ elementˉoh|ecthUnl。

7.调用Sp|as‖提供的∧P|

前面我们介绍了SplashLua脚本的用法’但这些脚本是在Splash页面里测试运行的’如何才能利 用Splash喧染页面?Splash怎样才能和Python程序结合使用并爬取JavaScnpt喧染的页面? 其实’ Splash给我们提供了—些HTTPAPI,我们只需要请求这些API并传递相应的参数即可获 取页面喧染后的结果,下面我们学习这些API。 ●re∩der.‖t"1

此API用于获取JavaScrjpt喧染的页面的HTML代码’API地址是Splash的运行地址加上此API 的名称,例如ht印://localhost:8050/renderhtml,我们可以用curl工具测试_下: cur1∩ttp://1o〔a1∩o5t8805O/re∩der·∩tⅧ1?ur1=∩ttp5://www.bajdu.〔o∏

我们给此API传递了一个ur1参数,以指定喧染的URL,返回结果即为页面喧染后的源代码。

用Python实现的代码如下: i呻ortreque5t5 ur1= 0‖ttp://1o〔a1host;805o/re∩der.ht∩1?ur1=bttp5://硼w.baidu·〔o叮

respo∩5e=Ieque5t5.get(Ur1) pIi∩t(re5pO∩5e.text)

这样就可以成功输出百度页面喧染后的源代码了°

此API还有其他参数,例如Wait’用来指定等待秒数°如果要确保页面完全加载出来’就可以设

■‖·可」‖■〗|■〗‖‖』■〗‖‖■〗‖』·』□‖』·】‖■‖□‖‖·■』』■■■■《□□】』■∏|勺』□■■|■■□□■■】■】|□』■]|■·■』·」■可■■‖‖』■‖』■可□〗‖‖■∏■■|』■

图7ˉl6 ∩ol』5e〔11〔代方法的运行结果

{|||‖

<■=-■===

=蛔Ⅶ>==__—一—ˉ

~~=

口|凹■君=

■p1▲■■m…Dp·2 【…Q1≈〃 1m0∑7c·》 n≡伯『…

ˉ二=

必●l

鱼廷F■■

墅Q时mr…

5p‖a5们γ35…u种…

‖』日·□■■■‖‖■■■|〗■〗■■】‖‖】■∏||■|』■可)‖}′』■

238

置此参数,例如:



i们portrequest5 ur1= 0∩ttp://1o〔a1ho5t;805O/re∩deI.ht阳1?ur1=∩ttp5目//州。taobao.co恤a们p;wajt=50 Iespo∩5e≡reque5t5。get(ur1) pri∩t(respo∩se.text)



■‖匹■■〗‖‖■[‖‖止甘【「|■■『‖|丛『‖‖‖◆■■『‖‖▲■■「|》△厂|□■||仁|卜‖}‖『■「‖卜「■『‖卜●『|‖●■『‖|》『‖‖‖β

7.2

Splash的使用

239

增加等待时间后,得到响应的时间会相应变长,如这里我们等待大约5秒钟才能获取JavaScript喧 染后的淘宝页面源代码。

另外’此API还支持代理设置、图片加载设置、请求头设置和请求方法设置’具体的用法可以参 见官方文档hnps://splashreadthedocsjo/en/stable/aplhtml#renderˉhtml。 ●re∩der.p∩g



此API用于获取页面截图’其参数比re∩deI.ht刚1要多几个’例如W1dtb和∩ejg∩t用来控制截 图的宽和高,返回值是PNG格式图片的二进制数据°示例如下: 〔ur1∩ttp://1oca1ho5t:805O/re∩der.p∩8?ur1=http58//咖‖°t己obao.〔o油wajt=58width=1OOO8∩ejght≡7OO

这里我们通过设置W1dt∩和‖ejght参数’将页面截图的大小缩放为1000×700像素。

如果用Python实现,可以将返回的二进制数据保存为PNG格式的图片’代码如下: i‖poItreq仙e5t5

uI1= 0∩ttp://1o〔a1ho5t:8O50/Ie∩der.p∩g?ur1≡http5;//w釉√。jd.〔o油"日1t=58w1dt∩≡1ooo肋ejght≡700‖ Ie5po∩5e=reque5t5.get(ur1) "1thope∩(,taob日o.p∩g|’ |wb0) a5+: +."I1te(re5pO∩5e.〔O∩te∩t)



得到的图片如图7ˉl7所示。 v

!簿¥每. ″………■·| 识…

宁 宗厩

矽朵忧肛p∩咀台贝■辟闪的缄拐京京宦宅取…京取魔彝京寨■俘睡懂 …

硼/遥亡■′n码 电宙/办公

歉■′凉具′m′■■



~=

*啪■

巳邑】ˉ压…闻亡鸡露F



你值得拥有

●儡/″硒

★&′呻′生鲜′钓尸 绑′棍必盯花′农…

H∏呻/计●…

||

″′…/呻吧

0■■·■■

解=芭==司

隧-

蝉′十护饿油/■们

肉牡/速蚀/≈

攀蹿※

撬上…

■姨′女狡′■胰′闪衣

女■′馆■/沁/■■

』量猩羚啪【=7芍连

爪赚快 扫



葱&



″ …望■办任* 呻=叮蜘

嘲 ▲哼挚…



唾 不…■▲屯份



册汝

辑n

m



……■切■

攀四◎伍迅 卜



…垂

■屯′m′m′电于书

蛔′蛔′m′生后

瞥唾

■帘



泊砧

削酶



0

m/…/曰伊/呻

雹 〔■0■乎….

由仪′m′∑诀f二早

于●二

吉_-—

图7ˉl7用Python实现的运行结果

这样我们就成功获取了京东首页喧染完成后的页面截图,详细的参数设置可以参考官网文档 https://splash·readthedocs。io/en/stable/apihtml#renderˉpng° ●re∏der。jpeg

此APl和re∩der.p∩g类似,不过它返回的是JPEG格式图片的二进制数据° 另外,此API比re∩der.p∩g多一个参数qua1jty,该参数可以设置图片质量。 ●re∏der·har

此API用于获取页面加载的HAR数据,示例如下:

_





第7章JavaScript动态渔染页面爬取

〔ur1http;//1o〔a1host:8o5o/Ie∩deI.h3r?ur1=打ttp5://wⅧ.jd.co毗wajt二5

运行结果非常多,是-个JSON格式的数据’里面包含页面加载过程中的HAR数据,如图7ˉl8 所示° ●



·可』叮‖▲∏·□可』□■∏‖』

240

CuF1∩ttp//lo〔αl∩OSt8050/Fe耐e7恤厂\?u卜I\巾ttp£〃啼jd·c…it\■5

1 },

e砒沪1e5坷 「[ˉ巳p°蜘ˉp尸O〔PS已∩g已tGt警■

,°『Mn吨四,

吐α咸e卸ote『1腮

°‖□

{lo90,; {ve厂$!o们 喻12,,, C广GOto广"目 {∩咖e饵 ■SPlo绷, γe「瓢咖α. 国3°5}, 割b「α 叶5Q厂 {肌∏α眠缸吕 锄ebN1t "VG厂s酮伺。 ,’馏2.1』 ,℃哪审滩∩t . 闰py0t§`M∑,0t5u "20

艺107鹏『18鹏10刁166251 , 厂que舅t, {爬沛Od 6[γ ’ u尸l . 闻『Tttps//…·〕d· 〔ow”,『∩tt口γ 0∩ttpγw臣`Q∩ . 盯『P/』 , 鳃f◎OM“ []’ 函que广y3f尸t∩旷§□」 ""灌Ode厂5倒。[ 『凹L》 l1ke6eC低◎ l1 1(刚ⅧL′

№r1 l″50α11 [I∩uxX86斟)知pleNebmt/的2

bpohγP7bto俐1005of□广1田21} {,,『`蜘e` 曰虹〔ept因o

γ

α1ue . text/向t铜l‖口pplmtO叮 们↑.阳!伞x删l》◎pp t □t`◎沈′×闲1;Qm6。9输瓣/·;q嗡`8砸}], “熄口 de厂SSZe 1鸥0 bmγSZc 1 广e当p◎们Se {b“y5Ze饵: 125鲍9, 憾∩吐pγe尸SI酮口;

‖丫w1°1 , ,Coo代eS喻 〔] 吨αdP厂已`, [{∩o≈碱. 5e尸Ve赋 γG1ue闻: ∩gmx馆}, 【园 ∏O阳e“· №te喊, "v°lue 5u∩瞥鹏』q1 z0鳃18酗.10叫『}p {∩…闻. m砒印tˉ丁ype , γ◎lⅧ. 卿tex卜/∏bWl,〔厢厂弓e

ut‘8,0}膊 i∩α唾廊. 〔o「WVe<tlm鹏簇

v◎l呸00

代eepˉ□

lue ∩C 1we }, {,,∩o铜e,, ■γα 闰γα厂y , mlue ∩C〔cpt≡[∩〔m∩g} {∩…· Ⅲ《〖∩it油摊e厂「t庐5 钮e qO ∩Ome .. ■ ts〔厂ee门峨’ “vn1ue ”°f「0,}, {∩me 气`∩itˉ加「ke萨四gec咖f1g , .嘲vα1ue· "印}阶 {圃 p0v 5仙∏“J ∩α俩e嚼; "[xp1厂eSS Ⅸov口Iue ‘剑门0“J山驹21 18鹏18“了饵}, {卿∩…磷· 嘶md7e-(m tFol 0|vo1oe娜 mxo9e30} {°∩″吧 ′‘悠o∏te砒[∩〔od↑∩g"’ ·vU{ue饵· "臼Z1P·}o [■

∩匈阳. ·|3e伊饵, “vo]l」e则

■■〈■■‖‖二■|□□■

{,∩◎冗G .U田P 。 同0田P铲Age∩t理, v□ue

81.63}、{国∩α锨龟 . 酗X〔mtP∩↑丁ypeˉ0pti咖s,0, γαlue口 "m

3∩`『「”}, {‘『`哪e,, : "Xˉ又55p「O↑e(↑`Qw` | vαⅡue 1;晒a…饥oCk瓣}, {,0丫…闻 X≡PFo 赃0pt1O∩5"’ “vulue · §峰职I6【刊卵} {m辆冶 Aqe , ■γα1ue. “亚},{回∩…. □γ 1o , .v◎lue°,; .v∩ttp1.10RIˉαα』O】Nz"IX17(〕“[CRSf])’l7七tp/11αˉlmˉZˉ倒

P25(〕cs[c腿「])}. {.厕圃网嚼·. “∧‘c…〔…萨·}.A!1o喇ˉ0伊igiw』, ″晌1峰蹿:…》, {| ∩mG

甸丁〗朋1"gAl1吵0尸ig[∩

阔v@1uP{,

山卿}‖『门…闻: 刷Xˉ『7α适它“》 ■V回I峰国: 愤酗ˉ



162Sq21828615ˉ矽0Ⅱ99,2锄162黔2〗83829S叼0ˉ01ˉ102昭肆162黔n8§蹈酚^0=0ˉo1→1■

}’{"酮e哦; "酞Fi《tWu∩spOFt5e它wtty"′ 挪γoll」e愉旨 ”呵xˉα$@回蹿孵T], 簿嘲0t“t饵;{

图7ˉl8

re∏der.}]ar方法的运行结果

此API包含前面介绍的所有re∩der相关的APl的功能,返回值是JSON格式的数据’示例如下: cur1http://1o〔a1ho5t:8O5o/Ie∩deLj5o∩?ur1=http5://www。∩ttpbi∩.org

运行结果如下:

‖‖‖‖《】

●re∩der.j5o∩



{"tjt1e": "∩ttpbj∩(1): ‖丁丁p〔11e∩t丁e5tj∩g5erγ1ce"』"ur1": "∩ttp5://www.∩ttpbi∩.org/"’ 』|req{」e5ted0r1": "bttp5://"ww。httpbj∩.org/"’"geo川etry": [0’ 0’ 1024’ 768]} ‖

我们可以通过传人不同的参数控制返回结果°例如’传人∩t‖1=1’返回结果会增加页面源代码;传 人p"g=1’返回结果会增加PNG格式的页面截图;传人baI=1,返回结果会增加页面的HAR数据°例如:

‖司‖■□

可以看到’这里返回了JSON格式的请求数据。

cur1∩ttp;//1o〔a1∩o5t;8o50/Ie∩der.j5o∩?ur1=∩ttp5://www.∩ttpb1∩.org肋t‖1≡18haI=1

此外’还有其他参数可以设置’可以参考官方文档https://splash.readthedocs』o/en/stable/apjhtml# rendeEjson。 ●exeCute

□■■‖■■■〗■可』■司

这样返回的结果中便会包含页面源代码和HAR数据°

此API才是最为强大的API°之前介绍了很多关于SplashLua脚本的操作’用此API即可实现与

先实现_个最简单的脚本’直接返回数据: 于u∩ctio∩们a1∩(5P1a5h) retur∩ ‖he11O|

e∩d

‖‖|□■‖■■■·‖当■·可■■乙■□‖●·■

如果要实现_些交互操作,这些API还是心有余而力不足,就需要使用exe〔ute了°

己 ■ ■ ‖

要爬取一般的JavaSc∏pt喧染页面,使用前面的re∩derˉbt"1和Ie∩deI.p∩g等APl就足够了’但

■■』‖

Lua脚本的对接°

(可」

72 Splash的使用

24]



然后将此脚本转化为URL编码后的字符串,拼接到exe〔ute后面,示例如下:

〔ur1

httP://1oca№o5t:8O5O/execute?1ua5ource≡+u∩ctjo∩+∏l己i∩%285p1a5‖%29油0%OA++retur∩+%27he11o%27汕0油∧e∩d

运行结果如下: ∩e11o

泣里我们通过1ua5ource参数传递了转码后的Lua脚本’通过exe〔ute获取了脚本最终的执行结果。

我们更加关心的是如何用Python实现上述过程,如果用Python实现’那么代码如下: 1们portreque5t5 千ro阳‖r111b·par5e1爪portquote

10a= | 『 ‖

+l』∩〔t1O∩爪a1∩(5p1己5∩) retur∩ |∩e11o! e∩d 0

0

0

ur1= ‖bttp://1o〔a1ho5t:805O/exe〔ute?1ua5ouI〔e≡0 +quote(1qa) re5PO∩5e≡reque5t5°8et(ur1) pri∩t(Ie5po∩5巳text)

运行结果如下:



∩e11O

这里我们用Python中的三引号将Lua脚本括了起来,然后用urllibparse模块里的quote方法对脚 本迸行URL转码,之后构造了请求URL,并将其作为1ua 5our〔e参数传递’这样运行结果就会显示 Lua脚本执行后的结果°

我们再通过实例看_下: i∏portIeque5t5 +ro‖ur111b.p己r5e加portquote 1ua= ` | 0

+U∩〔tiO∩‖ai∩(5P1a5b’ arg5) 1oca1treat≡requiIe(00treat0!)

1oc31re5po∩se≡5p1a5b日http-get("bttp://0曲‖w.∩ttpbj∩.org/get") retur∩{hm1=treat.a55tri∩g(Iespo∩5e.body〉’ 皿1=re5po∩5e·0r1’ 5tat‖5=re5po∩5e.5tatu5 }

e∩d 〗

0



ur1≡ ,http://1oca1ho5t;8O5O/execute?1ua5ource二 +quote(1u己)

re{5po∩5e=reql』ests.get(ur1) pri∩t(re5po∩5e.text)

运行结果如下:

{"‖I1": "http://M‖w.∩ttpbj∩.org/get"’"st日tu5": 20O’ "∩t‖1": "{\∩\‖0args\00 : {}’ \∩\"header5\": \∩\腻‖o5t\": \!|w0‖w。httpbi∩。org\"’\∩\"05erˉAge∩t\": \0刊oz111日/5.0(X11’[i∩仙xx866↓)∧pp1e‖ebRit/602·1(N‖丁"lˉ》 11keCe〔低o) 5p1a5‖γeI5io∩/9.O5a+ari/6O2.1\"\∩}’ \∩\||or1gj∩\": \"60·207.237.85\"’ \∩\"uI1\"$

{\∩\!`∧cceptˉ[∩codi∩g\": \008zip’ de千1ate\"’ \∩\"Acceptˉ[日∩guage\": \"e∩’*\‖0’ \∩\"〔o∩∩e〔tio∩\": \"〔1o5e\"’ \"∩ttp://0ww‖。httpbi∩.org/get\"\∩}\∩!!}

可以看到’返回结果是JSON形式的’我们成功获取了请求URL`状态码和页面源代码°

如此一来’之前所讲的Lua脚本就都可以用此方式与Python对接了’所有网页的动态喧染、模 拟点击、表单提交、页面滑动、延时等待后的结果均可以自由控制获取细节’获取页面源代码和截图 也都不在话下°



| 242

第7章JavaScript动态演染页面爬取



到现在为止’我们可以利用Python和Splash爬取JavaSc∏pt喧染的页面了°除了Selenium’Splash 同样可以实现非常强大的喧染功能,同时它不需要测览器便可喧染’使用起来非常方便。 8.负载均衡配置

用Splash爬取页面时,如果爬取的数据量非常大’任务非常多’那么只用一个Splash服务就会

由于篇幅原因’请移步https:〃sempscrape.center/splashˉloadbalance查看具体的配置方法°

本节中’我们全面地了解了Splash的基本用法。有了Splash’可以将JavaScript动态痘染的操 作完全托管到—个服务器上’爬虫爬取的时候不需要再依赖Selenium等库,整个业务逻辑会更加轻 量级°

在71节,我们学习了Selenium的基本用法,其功能的确非常强大,但很多时候会发现它也有_

些不太方便的地方,例如配置环境时’需要先安装好相关测览器’例如Chrome、Firefbx等’然后到

官方网站下载对应的驱动。最重要的是’需要安装对应的PythonSelenium库’而且得看版本是否对应’

·可|]勺||』■■』■‖‖‖」·司‖司』■]]□

7.3 尸yppetee『的使用

』■‖□■】‖|□|■〗□□勺||■■可|‖□■Ⅵ‖

9ˉ总结

·(□·

使压力非常大,此时可以考虑搭建_个负载均衡器把压力分散到多个服务器上,相当于多台机器`多 个服务共同参与任务的处理’可以减小单个Splash服务的压力。

这确实不太方便°另外,如果要大规模部署Selenium,-些环境配置问题也是很头疼的°

本节,我们介绍Selenium的另-个替代品: Pyppeteer°

Puppeteer是Google基于Nodejs开发的_个工具,有了它,我们可以利用JavaScnpt控制Chrome 测览器的-些操作°当然’Puppeteer也可以应用于网络爬虫上’其APl极其完善’功能非常强大。

Pyppeteer又是什么呢?它其实是Puppeteer的Python版实现,但不是Google开发的’是由_位 来自日本的工程师依据Puppeteer的-些功能开发出来的非官方版本。

||

看割F平圃云= -〕 q

口■■‖

用户群体要小众得多。两款测览器“同根同源,’有着同样 图7ˉl9

Chromlum测览器和Chrome

测览器的logo

种颜色,如图7ˉl9所示°

■□■】■■■■■

总的来说,两款测览器的内核一样’实现方式也一样, 可以看作开发版和正式版,功能上没有太

□]』■司|』■|‖』■∏

包含很多新功能°但作为—款独立的测览器’Chromium的

大区别°

q

□□‖‖

有新功能都会先在Chromium上实现’待验证稳定后才移植 到Chrome上’因此Chromlum的版本更新频率更高,同时

的|ogo,只是配色不同’Chromjumlogo的颜色是不同深度 的蓝色,Chromelogo的颜色是蓝色、红色、黄色和绿色这4

□可』■{

Chromium是Google为了研发Chrome启动的项目’是 完全开源的°二者基于相同的源代码而构建’Chrome的所

划』■■‖{

Pyppeteer的背后实际上有—个类似于Chrome的测览器——Chromium’它执行一些动作,从而进 行网页喧染°首先’介绍—下Chromlum测览器和Chrome 行网页喧染°首先’介绍—下Chromlum测览器和Chrome测览器的渊源。

■■∏』■■勺‖|■■■‖■■∏‖』■■■■Ⅵ■∏

↑Pyppetee『介绍

』·|□勺

注意是Pyppeteer’不是Puppeteer’Puppeteer是基于Nodejs的’ Pyppeteer是基于Python的。



□『匹■尸



》|}

73

Pyppeteer的使用

243

■厉

尸 卜

Pyppeteer就是依赖Chromium测览器运行的。如果第一次运行Pyppeteer的时候’没有安装 Chromlum测览器,程序就会帮我们自动安装和配置好’免去了烦琐的环境配置等工作°另外’Pyppeteer 是基于Python的新特性async实现的’所以它的_些操作执行也支持异步方式,和Selenium相比效 率也提高了。



■【)伯

■|‖



下面我们就-起了解一下Pyppeteer的相关用法° 2安装

首先要解决的便是安装问题°由于Pyppeteer采用了Python的async机制,所以要求Python版本 为3.5及以上。

使用plp3工具安装Pyppeteer即可:

户『)

piP3j∩5ta11PyPPeteer

具体的安装过程可以参考https:〃setupscrape.cente门pyppe仗er°

■■■△■■『‖■■『

安装完成之后,便可以开始接下来的学习° 3.快速上手

我们测试_下基本的页面擅染操作,这里用网址https:〃spa2.scmpe.centeⅢ/做测试’如图7ˉ20所示°



O

--=≡→_一7≡■

+—一







……

+÷G

…■

)}厂

m丙寅一— ●亡

力却●!

回5cmp. 广『|卜=■厂

■{

惨 遍盅铡罐 、舔甜炉

■王别姬ˉ「●洒w●0‖岭c◎m凹b脑 ■■(■●

9°5



山古∩◆

….…■/0γ‖舔

『…弘四

伊『■

垫墓蕴=

‖‖

三琶茁…

9·5 台古●■●

m′↑?O呻



…■

匹■■「卜巴厂

腔霉 伊‖■■『□『·β但向■尸 △ 尸β ‖ |

「′』



问申克mHˉT恬勤…= ●●

9。5 ●仑●●■

■■′0●濒 0…凶妇

b

图7_20测试网站

这个网站在7』节已经分析过了,整个页面是用JavaSc∏pt喧染出来的,一些Ajax接口还带有加 密参数,所以没法直接使用Icquests爬取看到的数据’同时也不太好直接模拟AjaX来获取数据。

enjum爬取这个网站中数据的方式,原理就是模拟测览器的操作,直接用 在7.l节介绍的使用Selenjum爬取这个网站中数抵 器把页面谊染出来,然后直接获取喧染后的结果°基于同样的原理’Pyppeteer也可以做到。 测览器把页面喧染出来,

下面我们用Pyppetcer试试,代码可以写为如下这样: i叮crta5y∏〔1o

千rcⅦpyppeteeImport1al』∩〔h 十rc厕pyql』eⅢyi‖印ortpyα』erya5pq

a5)′∩〔de千硒i∩(): bI叫Ber≡aNajt1au∩Ch()



2“

第7章JavaScript动态演染页面爬取 page=awajtbrow5e工。∩ewpage() awa1tpage.goto(|http5://5p日2.5〔rap巳ce∩ter/‖〉 己wajtpage.w己1t「or5e1ector(|。ite阳 .∩a爬|) do〔=pq(awajtp3ge°co∩te∩t()) ∩a"e5= [1teⅧ.text() 十orjte们i∩doc(‖.1teⅦ .∩己"e‖)。jte刑5()] Prj∩t(‖‖aⅧe5: !’ ∏aⅦe5) aw己itbrowSer.〔1o5e()

a5y∩c1o。get—eve∩t—1oop().ru∩_u∩tj1—coⅦp1ete(∩ai∩())

运行结果如下: ‖3"es: [ ′霸王别姬ˉ「日rewe11‖γ〔o∩〔ubi∩e! ’ |这个杀于不太冷_[白o∩0 ’ 0肖中克的救赎ˉ丁∩e5‖3w5ha∩kRede阳Pt1o∩0 ’ |暴坦尼克号ˉ丁ita∩i〔|’ 0罗马假日 ˉRo∏a∩‖o1jd日γ‖’ ‖唐伯虎点秋杏ˉ「1jrt1∩gS〔们o1ar0 ’ 0乱世佳人ˉCo∩ewitb t∩e‖i∩d‖ ’ ‖粤剧之王ˉ『∩eⅨi∩go+〔o爬dy0 ’ 0楚门的世界ˉ丁∩e丁ruⅦa∩5打ow! ’ !狮于王ˉ丁he[1o∩Nj∩80 ]

先粗略看_下代码,大体意思是访问了测试网站,然后等待.jteⅧ.∩a们e节点加载出来’随后通过 的结果一样,我们成功模拟了页面的加载行为,然后提取了页面上所有电影的名称°

□调用1au∩〔h方法新建了—个8row5er对象’并赋值给bro"5eI变量°这一步相当于启动了测 览器°

□调用bro"5er的∩e"page方法’新建了≡个page对象’并赋值给page变量°相当于在测览器 中新建了—个选项卡’这时候虽然启动了_个新的选项卡’但是还未访问任何页面,测览器依 然是空白的°

□调用page的goto方法’相当于在测览器中输人goto方法的参数中的URL,之后测览器加载 对应的页面。

□调用page的"a1t尸or5e1ector方法,传人选择器,页面就会等待选择器对应的节点信息加载出 来,加载出来后,就立即返回’否则持续等待直到超时°如果顺利的话’页面会成功加载出来。

□页面加载完后’调用co∩te∩t方法’可以获取当前测览器页面的源代码,这就是JavaScnpt喧 染后的结果。

□进—步’用pyquery解析并提取页面上的电影名称,就得到最终结果了°

另外’其他—些方法(例如调用asyncio的get—eγe∩t—1oop等方法)的相关操作属于Python异步 编程async相关的内容’大家如果不熟悉,可以查看第6章的知识。 通过上面的代码’我们同样可以爬取JavaSc∏pt谊染的页面。怎么样?相比Selenium,这个代码 是不是更简洁易读’环境配置也更加方便。在这个过程中’我们没有配置Chrome测览器’没有配置

测览器驱动’免去了一些烦琐的步骤,却达到了和Selenium一样的效果’还实现了异步爬取°

』旧』{■‖‖』■■』□】】』日』■习」‖·‖」□‖』·〗〗』』■]」■勺‖‖』■■】■‖‖』■■∏‖■‖‖■■□□■‖||‖‖|』■』】‖‖』□』■■■■]||■』勺】‖||日|』·■‖‖』■』●‖√■可·■‖

那么’其中具体发生了什么?我们来逐行解析一下°

{|』

pyque!y从页面源码中提取电影的名称并输出,最后关闭Pyppeteer。运行结果和之前用Selenium实现

接下来,我们看另外_个例子: mpoIta5y∩c1o 十ro们pγppeteer加port1au∩cb

Wjdth’ he1g∩t=1366》 768

aSy∩〔de+∏m∩(): brow5er=日wajt1au∩〔∩() p日ge=awaitbrow5er.∩ewpage()

awa1tp3g巳5etγie"port({W1dt}‖0 : w1dt∩’ |们ejght,: heig‖t}) 3waitpa8e·goto(!‖ttp5://5pa2.5cmpe.〔e∏teI/‖) aW3itpage.Wa1t「OI5e1e〔tOr(‖.jte刚 .∏a川e! ) a"己1ta5y∩C1O.51eeP(2)

a"ajtpage·5〔ree∩5∩ot(patb=‖exa‖p1e·p∩g‖) d1Ⅶe∩51o∩5=awaitpage·eγa10ate(0 0 0() =){ retur∩{





73



245

Ⅳ1dth: docu川e∩t。doc[」∩e∩t[1eⅦe∩t.〔11e∩t‖1dth’

})





Pyppeteer的使用

he1g∩t: do〔u们e∩t·do〔u∏e∩t[1eⅦe∩t.C1ie∩t‖e1g∩t’

deγj〔eS〔a1e「己〔tOr: W1∩dOW·deγ1Cep1Xe1RatjO’

















| }

} p



PIj∩t(dme∩5jo∩S) awajtbrow5er.c1o5e()

a5γ∩〔1o.get≡eγe∩t—1oop()°ru∩—u∩t11-〔o川p1ete(们ai∩())

这里我们用到了几个新的方法’设置了页面窗口的大小`保存了页面截图、执行JavaScrjpt语句 并返回了对应的数据。其中,在5cree∩5hot方法里,通过path参数用于传人页面截图的保存路径, 另外还可以指定截图的保存格式tyPe`清晰度qua11ty`是否全屏「u11page和裁切〔1ip等参数。页 面截图的样例如图7ˉ2l所示。

固 5c「ape ——■



■王别姬ˉ尸a「●W●‖册yC◎∩cubj∩e ●●

●「「『|尸卜「卜|》『‖

鞠王洲避

中■…"中m渺′w『沁

糯肿p!

0钓甄γ.褪上妓

9°5 ★台古★●





:勘:品.民沦·

α←…“

=伯≈●=…韩

这个杀手不太冷ˉL6◎∩

9·5

■●■●■

台亡●亡白

洁■′00o分钟

■「[『′■「归■|‖皿◆∏‖β■『■广卜‖{‖■厂‖|β〖■■『||卜‖‖■厅‖‖●「凸『||■厂尸「|‖凸■「|『■『|}〖尸‖|||

γ…ˉ↑q上脏

■ 肖申克的救赎ˉ丁b●5们己w劝■∏仪R●d●厕pt『◎R ●●



9·5 台企◆◆■

m/↑心沁 ↑硒仍.『0上脏

图7ˉ2l

截图样例

可以看到’返回结果是JavaScript喧染后的页面,和我们在测览器中看到的结果一模-样。 我们还调用eγa1uate方法执行了一些JavaScIipt语句°这里给JavaScIjpt传人了一个函数’使用retur∩ 方法返回了页面的宽高`像素大小比率这三个值, 区回了页面的宽高`像素大小比率这三个值,最后得到的是—个JSON格式的对象 内容如下: {W1dt‖0 : 1366’ ‖∩e1g∩t‖ : 768’ 0device5ca1e「a〔tor0 : 1}

实例就先感受到这里’有太多功能还没提及。

总之’利用Pyppeteer可以控制测览器执行几乎所有想实现的操作和功能,利用它自由地控制爬 虫当然也不在话下°

了解了基本的实例后,再来梳理Pyppeteer的-些基本和常用操作°Pyppeteer几乎所有的功能都 能在其宫方文档的APIRefercnce里找到,文档链接是https://pyppcteergjthubjo/pyppetee∏referencehtml, 使用哪个方法就来这里查询即可’参数不必死记硬背,即用即查就好° 4.1au∏C‖方法

使用Pyppeteer的第一步便是启动测览器。启动测览器相当于点击桌面上的测览器图标,用 Pyppeteer

第7章JavaScript动态演染页面/膛里

Pyppeteer实现时’调用1au∩〔∩方法即可。

先来看下1au∩〔h方法的APl,链接为:https://pyppeteergithub.io/pyppeteer/re忙rencehtml#launche『, 该方法的定义如下:

pγppeteer.1au∩〔∩er·1au∩ch(optjo∩5: dj〔t≡‖o∩e』 **代warg5)→pyppeteer·brow5eI.BIow5eI

可以看到, 1au∩C∩方法处于launcher模块中,在声明中没有特别指定参数,返回值是browser模 块中的8ro"5er对象。另外’观察源码可以发现这是一个async修饰的方法,所以在调用的时候需

□1g∩ore‖丁「p5[rIor5 (boo1):是否忽略HTTPS的错误,默认是「a15e。 □∩ead1e55(boo1):是否启用无头模式,即无界面模式。如果deγtoo15参数是丁n」e,该参数就

一■■{』‖{』□·』‖」■可

接下来,看看1au∩〔∩方法的参数°

『匹

要使用awajt。

]』]‖』】〗』‖』〗□□]‖||■{』□γ』勺‖』‖‖」可

246

会被设置为「日15e,否则为「rue,即默认开启无界面模式°

□exe〔ut己b1epath(5tI):可执行文件的路径。指定该参数之后就不需要使用默认的Chromjum测

□‖a∩d1e5ICI‖丁(boo1):是否‖何应SIGINT信号,也就是是否可以使用Ctrl+C终止测览器程序, 默认是丁n」e°

□ha∩d1e5IC『[R‖(boo1):是否响应SIGTERM信号(_般是促j11命令)’默认是丁me° □ha"d1e5IC‖0p(boo1):是否响应SIGHUP信号,即挂起信号’例如终端退出操作’默认是「n」e°

□dⅧpjo(boo1):是否将Pyppeteer的输出内容传给pro〔e55.5tdout对象和proce55.5tderI对象’ 默认是「a15e°

□u5er0ata01r(5tr):用户数据文件夹’可以保留一些个性化配置和操作记录° □e∩γ(dj〔t):环境变量,可以传人字典形式的数据°

□deγtoo15(boo1):是否自动为每_个页面开启调试工具’默认是「a15e。如果将这个参数设置

·可□■|‖』■〗β·]‖」■〗■●〗{』■〗」』■‖■■〗」·{·〗

□ig∩oIeDe+au1t∧Ig5(boo1):是否忽略Pyppetee『的默认参数。如果使用这个参数,那么最好通 过aIg5设置_些参数’否则可能会出现—些意想不到的问题°这个参数相对比较危险’慎用°

□可‖可』|●』■口‖

览器了’可以指定为已有的Chrome或Chromium°

□51o"‖o (j∩t|「1oat):通过传人指定的时间,可以减缓Pyppeteer的一些模拟操作。 □3rg5 ([1St[5tr]):在执行过程中可以传人的额外参数°





| q





为「rue’那么head1e55参数就会无效’会被强制设置为「a15e。

□1og[eve1 (1∩t|5tr): 日志级别,默认和mot1oggeI对象的级别相同° □al」to〔1o5e(boo1):当—些命令执行完之后,是否自动关闭测览器’默认是『n」e° □1oop(a5y∩〔jo.∧b5tIa〔t[γe∩t[oop):事件循环对象° 好了,了解了这些参数之后,小试牛刀_下吧°

5。无头模式

首先,试用_下最常用的参数_∩ead1e55°如果将它设置为「Iue或者默认不设置,那么在启动 的时候是看不到任何界面的°如果把它设置为「a15e,那么在启动的时候就可以看到界面了°我们一 般会在调试的时候把它设置为「a15e,在生产环境中设置为「rue°下面先尝试_下关闭无头模式: i呻oIt己Sγ∩〔io fr刚pyppeteeIj∏port1a仙∏〔h

q

Q









0

( q

| U

asy∩〔de千阳i∩(): a"ajt1au∩〔h(he己d1e5S=「a15e) a"aitasy∩〔io.51eep(10o)





a5y∏c1o.get-eγe∏t-1oop().ru∩_u∏ti1_〔o呻1ete("a1∩())













<∏≡







73Pyppeteer的使用









247

运行这段代码之后在控制台看不到任何输出’但是会出现_个空白的Chromium界面,如图7ˉ22 所示。

灌r聪猛愿……

…=…—■-印出~■日… …当…″潍……ˉ四-印=…=轴■p… 锋_…=】_…—=岳=房垂~一■一

墨′ ˉ隘@!o伯…月:…k 轴h=…=≈■■

蕊豌 = 譬′ ·烹》蕊c狸b蹿噶蛀—ˉ嘎▲

-丰

磷蝇``

-—…………~………≡″…荤■=冷■琵■=…→哗■=唾撂……=~…←■唾-\

唾…罐= … 丹 __ˉ ˉ ; 幂…. .篱″者→ˉ=. .-~……一…ˉ—_冀)e

…盯正费翻寝硒试城件…瓣. c沁℃面盯正瓣翻寝硒试城件的螟瓣.

Ⅱ 菇 畏

翻 碍 ″"

×

]卜 呼



▲宁…=喂韶…~…!



·,襄 《

g h骂

β

p



0

p

「 p

p

图7ˉ22空白的Chromium界面



p

这就是一个光秃秃的测览器而已’看一下相关信息,如图7=23所示° p



关于c↑Ⅵ◎〖VU‖oⅧ

骡. , 爆:撵?辨



°霹

P



}uⅧ ≡田口口→b__……与■ ^油~=…_-



版本7γD·3“a0(开

(御位)

p 曲

■一—

一~■≡■—■←←睁pp■哮●砷尸■=Q●P唾叮≈≡p==■-■=≈≡户=→…~伊A≈盼…=嗜~0…曙芍…

一一审磅=m

~—

田 团

的每助





图7ˉ23相关信息

β











p











开发者内部版本号’将其当作开发版的Chmme测览器就好° 上面有Chromium测览器的logo,开发者内部版本亏,将具当作计友版田Chrome测见舔肌灶° 6调试模式

本节开启调试模式。例如’在写爬虫的时候会经常需要分析网页结构和网络请求,所以开启调试 工具还是很有必要的。可以将de`/too15参数设置为『rue’这样每开启一个界面,就会弹出—个调试 窗口,非常方便’示例如下: i『‖port己5y∩〔io ■

十r咖pyppeteermpOrt1au∏〔h a5y门〔de十佃i∩(): br叫5er=a阳jt1au∩〔h(deγtoo1s=丁rl』e)

page=砌己itbr叫5er.∩酗p己ge() awaitpage.8oto(!http5://…。baidu。〔m,) aNajta5y∩〔io.s1eep(1oo)

a5y∩〔io。8et-eve∩t-1oop().ru∩u∩ti1一〔m甲1ete(陋i∩())

刚才说过,如果deγtoo15参数设置为丁rue’无头模式就会关闭’界面始终会显现出来°这里我 们新建了一个页面,打开了百度,界面运行效果如图7ˉ24所示°

||

第7章JavaScript动态演染页面爬取

248

吕∩—ˉ

★● §

_ ×

…正贤冤鳃避jˉ『壤酣佛…ˉ

×



●■■





啡!…t″峡用炉 ■!…← … p…=~

曲0趟





v





◆…「酗…′06丁…≥ p句C…建叼锤了酝摊 ●妇』可≡广茹字≤/d』诊

刨』vcu靶杉岭c吨蝉≈c贿t■…广蛔江凹〔≤鲤…`·』■萨,…/0A‖D ■呸「mt≥

m…·=“知~■w吠…〗



“呸了邱t少

p■9w…≈〃庄F华t> ◆≤c丁…超′“「唾≥ ●纪c7如廷■/“…>

<c丁…k洒叫t■w〗…厂m饲wc■…佃泣■Ⅲ〃=…O■分1画ˉ ~





蛇亿≥



‖‖|□】勺|·|(|‖‖‖‖』√|‖{□γ‖

鸭鞭.■设}c…。6【v`簿{ 0》 Uv●0…· ↑O沁《 碑』《m8≈1吼』沁6 ;}

剧|.

0



午·』

●〗凹■

仙…岭

皿幻航



●◆

‖…】吕1.≈

~≡当

≡.≡…= 、r…西●Ⅶ ………





………]……



呐…























§

















≡…

穗 警

b ~

……啤

…0 0●出匈 k冯pu 妙`《

p……丛…d▲



□■‖凸■可

图7ˉ24界面运行效果

7.禁用提示条

怎么关闭呢?这时候就需要用到arg5参数了,禁用操作如下: bro"SeI二a"ait1au∩〔∩(head1e55≡「a15e’ aIg5≡[|ˉˉdj5ab1eˉi∩+obarS』])

ˉˉdj5ab1eˉi∩+obaI5。

8.防止检测

有人会说,刚刚只是把提示关闭了,有些网站还是能检测到‖ebdI1γeI属性°不妨拿之前的案例

网站ht‖ps:〃antispiderl·scrapecenter/验证一下: 1‖pOrta5y∩〔1O

+roⅧpyppeteer1们port1au∩〔h a5y∩〔de+Ⅶ日j∩():

bro"5er≡己Najt1au∩c∩(head1e55=「a15e′ arg5=[ˉˉd15ab1eˉ1∩十obar50 ]) page=a"a1tbIow5er.∩e"page() a"aitpag巳goto(‖http5;//a∩t15pider1.5〔rap巳ce∩ter/‖) a"ait己5y∩CjO.51eep(1OO)

a5y∩〔1o。get-eγe∩t_1oop().n」∩u∩tj1-co呻1ete(Ⅶa1∩())

果然被检测到了,如图7ˉ25所示°

·」』』·■】|·』·{‖■■■‖||』】■|」勺』‖|■司■■』·■〗‖|』■■口』‖』』■】』∏]』勺·‖‖■■□」·』■|{‖‖]|■〗‖||』□■】‖

这里不再写完整代码了’就是给1au∩Ch方法中的arg5参数传人liSt形式的数据’这里使用的是

‖‖■■■■■』勺

可以看到图7ˉ24的卜面有一条提示』℃hmme正受到自动测试软件的控制”’这个提示条有点烦人’

「卜|)

‖′

Pyppeteer的使用

7.3

249



■■■E…撇 卜 【 ■ 厂

撼]

壁f印·『峪w把



←◆G||■ 削荆【屁

肖 武

Ⅸ■

《敝 刮削罗纠■■■日§

■厂『伍β「|‖■■「■■■「『》β■尸’△尸

Webd『iγe『尸◎rbidde侧·

…舜Ⅶ磊



—……凸■…■

=≡】

图7ˉ25检测结果

这说明Pyppeteer开启Chromlum后,照样能被检测到‖ebdrjγer属性的存在。 ■尸‖β‖■■■厂

那么如何规避此问题呢?Pyppcteer的page对象有一个叫作eγa1uateO∩‖ew0ocuⅦe∩t的方法’意思是 在每次加载网页的时候执行某条语句,这里可以利用它执行隐藏‖ebdI1γer属性的命令’代码改写如下:

■■尸‖=■■『▲巴尸

1Ⅶporta5y∏c1o 于IoⅧpyppeteer1卯oIt1au∩〔h

a5y∩〔de十"a1∩(): bro"ser≡awa1t1au∩〔h(∩e己d1e55=「己15e’ arg5≡[|ˉˉd15己b1eˉ1∩十obar5|]〉 pa8e=a"aitbro"5er.∩ewpa8e()



■ 》 尸 『卜

a"aitp己ge.eγ己10ate0∩‖ewDo〔u∩e∏t(|0bject.de+j∩eproperty(∩aγ1gator’|Webdriγer"’{get:〈)≡〉u∩de十1∩ed}),)

aⅣaitpage.goto(|http5://a∩tj5pjder1.5crape。〔e∩ter/0) a"aita5y∩〔io·51eep(1O0)

a5γ∩cio.getˉeγe∩tˉ1oop().ru∩u∩tj1_co"p1ete(Ⅶaj∩(〉)

可以看到’整个页面成功加载出来了’绕过了对‖ebdr1γer属性的检测’如图7ˉ26所示° 9



×

+。

■■已▲

早#==~■■地■■…■●■钾■冲唾冲蚀▲军◆…■■凸←巾…■●o≈≈沪=古◆≈←诗…Q呜≈补让钨■◆◆铀伞宅

咱■早



■凸=即#呻船争唾馆铅铃←≈怜

学=

同Sc『ape 一≡_■■_■

霸王别姬ˉFa『eWe‖}My C◎∩cub肮e

窜蹬爵 ★★★★古

}}

嗡文谢磁俞m四 憾瓣』中国内地中圈誊港′↑7`分钟 b~… 『 {…了26上映 =.

■ 厂 ■■厂|■■

这个杀手不太冷ˉLd·∩

■`■



■^树∑

■·△强■

与早凸●●●■→=●◇=■◆≈褂轩韩吓◆中申

β■=尸■■厂‖}『■【■「



∑?鸿″鸟

固sc′印·}M。γ裕

←÷G(·

■「》口}|■【■=「 匹 β 『 但



·≡瞒坪

■■尸}‖■■△■「■『伊‖□■厂|‖

■■咎嗡…

◎■● 法国/↑↑O分钟 w9409~‖4上映

豌$茬

脚宁

图7ˉ26加载成功的页面

9恿 ★★★★喇



9.页面大′|`设蕾

在图7ˉ28中,还可以发现页面的显示bug,整个测览器的窗口要比显示内容的窗口大’这个情况 并非每个页面都会出现°

这时可以设置窗口大小,调用pa8e对象的5etγ1e"POrt方法即可,代码如下: 1Ⅶporta5y∩c1o

十Io阳pyppeteer1们port1au∩〔h W1dt侗’ hejg∩t=1366’768

己sy∩〔io。getˉeγe∩tˉ1oop()。ru∩u∩tj1ˉco"p1ete(们a1∩()〉

这甲我们同时设置了测览器窗口的宽高以及显示区域的宽高’让二者_致,最后发现页面显示正 常了,如图7ˉ27所示°

■■Ⅵ‖|■■■■‖』·■■■

brow5er=aw31t1au∩〔h(打ead1e55=「315e』 arg5=[|ˉˉdi5ab1e_i∩千obar5‖’ 「′ˉˉwj∩dowˉ5jze二{width}」{he1g∩t}0 ]) p己ge二己"己itbro"5er.∩e"page() aw己jtpage.5etγ1ewport({!Nidth‖: wjdth′ !hejght|: ∩e1ght}) 3w己itpage.eva1uateO∩№汕ocu爬∩t(0Obje〔t.de+j∩eproperty(∩日vig己tor’""ebdr1ver′!’{8et: ()=>u∩de+1∩ed})‖) awajtp日ge.goto(‖‖ttp5://a∩ti5pjdem。5〔rape.〔e∩ter/0) awajta5γ∩〔io.51eep(1o0)

■■勺□■‖

己5y∩〔de+川己j∩():

■■■日■■■■■■尸■■‖|」■|{钠』■■||‖〔|‖‖`■】■|』】】■】Ⅱ】』】■■■】】】]】|‖

第7章JavaScript动态遭染页面爬取

250



| q

●…去R▲…… ◆



国……『…妇

R

x





☆O

■=~≡

同5c了·p· ■王别姬.庐己帕w●‖ⅧyC◎∏cqb『瞄

m■

95



| | {

台●●●合

…`中…Ⅺ′0γ0”



?施m出上n

云/





厂【鹰◎"

| 这个杀手不太冷ˉ[O◎们

●■■■●●

95



★■☆白凸

…′0`O滤 『…『q上以



( α■



▲■

|…………… ●●

9。5 ●◆台●D

m′0幻纬

q■夕硒0^卜■

图7ˉ27正常显示的页面



| 寸

↑O.用户数据持久化

刚才我们看到,每次打开Pyppcteer的时候,都是一个新的空白的测览器°如果网页需要登录, 那么即使这次登录成功,下_次打开时也还是空白的,又得登录-次,这的确是_个问题°

以淘宝为例,很多时候在关闭测览器并再次打开时,它依然处于登录状态。这是因为淘宝的_些 关键Cookle已经保存到本地了,再次登录的时候可以直接读取并保持登录状态° 那么,这些信息保存在哪里呢?答案是用户目录下°其中不仅包含测览器的基本配置信息’还包 含一些Cache、Cookle等信息,如果我们能在测览器启动的时候读取这些信息,就可以恢复—些历史 记录甚至登录状态信息了°

0

















Pyppeteer的使用

7.3

25l

β‖

这也解决了_个问题:很多朋友每次在启动Selenlum或Pyppeteer的时候总是_个全新的测览器* 究其原因就是没有设置用户目录,如果设置了’每次打开时就不会是一个全新的测览器了’它∏J以恢

卜‖‖卜卜

复之前的历史记录’和很多网站的登录信息。 那么’怎么设置用户目录呢?很简单,在启动测览器的时候设置u5er0ata01r属性就好了°示例

卜『

如下: 1川pOrta5γ∩C1O 千IO们pγppeteeri们pOrt 1au∩Ch

a5y∩〔de千刚己i∩():

‖)『‖)β

bIo"5er二a"ait1己u∩〔h(head1e55=「a15e’ u5eID日ta0jr≡0 。/u5eIdat3‖’ 日rg5=[ ’ˉˉd15ab1eˉ1∩十obar50]) page=awaitbIowser.∩ewpa8e(〉 awajtpa8e·goto(0∩ttp5;//0‖ww.taobao.co"‖) aWa1t己5y∩〔iO·51eep(10O)

日5y∩〔1o.getˉeγe∩t_1oop().ru∩ˉu∩tj1ˉ〔o"p1ete(们日j∩())

■■■「■■■厂β■●【‖■尸

这里将useI0ata01r属性的值设置为了./u5erdata’即当前目录的userdata文件夹°首先运行下这段代码’然后登录_次淘宝,这时候可以观察到在当前运行的目录下又多了一个userdata文件夹’ 其结构如图7ˉ28所示。

■ 压Ⅵ二.÷吃里■翻尸F异…里玉旦飞已. .

》日∏



∩:辩蜘

广■Cm什睡t朗·γOcH前α》

o33马

份■

岭sG

v

尸■酗饱u忱

●■瞄唾出 ■『■『|】■■∏ 可

p







伊■mewp∩tj◎"p刨『cγp勒记蚀8●

蹿



鲤.2■

·………

的*节9

夕窝典

ˉ

■■

o3ˉ36

.

∏伸史

●■

ˉ

叉件央

o习:a0

仕■M值阶刨md

《》33凸

尸■α……

QPg?△

≥■团m

o334

■……… 仔■≈沁a…吨

o咎凸弓

蓝门夹 z悦B 9

D勤…m℃m代№ 口…

x…圃…文鸦

ˉ

灾钵奥

ˉ

文件突

■■■

宜件宙

]↑宇节0

■曰 文件史

o3目34

●■-.

文件突

呻弘 Q33£

!望…

■身

。 文∩央

酵码

■■●

03白36

2O牢苇 叮

■毋

mT匡

09宇Ⅷ 『

F身

o33e

9£.字Ⅵ 】



尸…抬t咖γSo@帕t



抄■蹿勘=

蛔幽

仿■剑№…『c●「傲额

O33冯

伯密

.

文件央

.

亥q轻汐

■q■

■尸『■■厂

‖ ■■尸卜‖止■厉 ■厂‖『『





图7ˉ28

userdata文件夹的结构

关于这个文件夹的具体介绍可以看官方的一些说明’例如hthps:〃chmm1um.googlesouI配com/ChIomlum/ src/十/master/docs/userdatadjr.md。

再次运行上面的代码’可以发现淘宝已经处于登录状态不需要再次登录了,这样就成功跳过了 登录的流程°当然,也可能由于时间太久,Cookje都过期了,还是需要登录。

~■■||||■∏||||坠■β||||▲■■■||■■∏β【【【■≥■■『■■匹■■厂『

以上便是1au∩〔h方法及其对应参数的配置. ↑↑.Brm5er

我们了解了1au∩〔h方法,它的返回值是一个8ro005er对象’即测览器对象’我们通常会将其赋值 给bm"5er变量(其实就是8ro"5er类的一个实例)°

|‖°(

{ 第7章JavaScript动态演染页面爬取

252

〔1己5Spyppeteer.brOW5er.8rOW5er(CO∩∩eCtiO∩; PγPPeteer。〔O∩∩eCt1O∏.〔O∩∩eCtiO∩’〔O∩te×tId5:U5t[5tr]’ 1g∩ore‖∏p5[rrors:boo1’5et0e+al」1tγie即ort:boo1’pro〔e55:0ptjo∩a1[5ubProces5.poPe∩]=‖o∩e’〔1o5e〔a11ba〔|(: 〔a11ab1e[[]’ ∧闪日itab1e[‖o∩e]] =‖o∩e’ **灿arg5)

」■可|』□』口■■■■‖』■]|‖■■

下面来看看8ro训5er类的定义:

从这里可以看到, Bro侧5er类的构造方法有很多参数,大多数情况下直接使用1au∩〔‖方法或 〔O∩∩e〔t方法构建测览器对象即可。 ■■■

brow5er作为8row5er类的实例, 自然有很多用于操作测览器的方法’下面我们选取_些比较有 用的方法介绍一下。

↑2.开启无痕模式

Cookle等内容’可以通过〔reateI∩cog∩1to8Iow5eI〔o∩text方法开启无痕模式,示例如下:

∏‖匹■■■匹■尸『▲

我们知道Chrome测览器有无痕模式’其好处就是环境比较干净,不与其他测览器示例共享Cache`



j∏pOrta5y∩〔1o 十rO爪pγppeteeI1们pOrt1au∩〔∩

{{|

Ⅲ■■·

widt∩’ ∩eight= 12oo’ 768

日5y∩〔de于‖a1∩(): brow5er≡aw己jt1己u∩〔h(head1e55=「a15e’

」‖

日rg5=[‖ˉˉdi5ab1eˉ1∩于obar5‖’ 十‖ˉˉM∩dowˉ5i∑e={"1dth}」{∩e1g∩t}0 ]) co∩text=aⅣaitbro"5er.〔reateI∩〔og∩jto8Iow5er〔o∩text() page≡己wajt〔O∩teXt.∩eWpa8e() a"aitpa8巳5etγjewport({‖w1dt‖0 : "1dt∩’ !heig∩t‖ : he1ght}) aw日jtpage.goto(0∩ttp5://洲w.b己jdu.〔o∩‖) a"ait己5y∩〔jo·51eep(1oo)

|‘■可|■√□‖‖

a5y∩〔1o.get—eγe∩tˉ1oop().n」∩u∩ti1ˉco们p1ete(∏ai∩())

这里调用就是bro"5er的〔reateI∩〔og∩jto8Iow5er〔o∩text方法’返回值是_个〔o∩text对象。利 用〔o∩text对象可以新建选项卡°

0

|儒.

翻§ F

■咨甜







∩『m升w闪包函挝」γ蜗咖挑

』■勺■□·‖已】‖』

运行这段代码后,我们发现测览器进人了无痕模式’界面如图7=29所示°

■…m…四■■■■≈mm■







唾|J



‖‖ {

殴0趟百度

』□可

』■■■■■‖‖』』·‖(||ˉ■■

二■〗■∏』■■‖■■〗』‖

图7ˉ29开启测览器的无痕模式

』‖|

7.3

Pyppeteer的使用

253

↑3.关闭

怎样关闭测览器就不多说了’使用的是c1o5e方法。很多时候会因为忘记关闭测览器而产生额外 开销’因此_定要记得在测览器使用完毕之后调用C1O5e方法’示例如下: mpOrta5y∩〔iO froⅧpyPpeteer1∏port1au∩〔h 十rO们PyqUerymPOItpγQUery35pq

}‖‖‖|‖[■|「■「|『『‖β|日尸卜「》∏

a5y∩〔de+∩m∩(): brow5er=己wajt1au∩〔b() Page≡aw己itbrow5er.∩ewpage()

aNajtp日ge.8oto(‖‖ttp5://spa2°5〔rape.〔e∩teI/|) aN日itbrow5er.〔1o5e()

a5y∩cjo.getˉeγe∩t-1oop().ru∩ˉu∩ti1-co∩p1ete(∏ai∩())

↑4。page

page即页面’对应—个网页、一个选项卡°

在前面的很多示例中,其实已经出现了page对象的身影,这里再详细看_下它的_些常见用法°

尸β■尸‖【尸||〖广|尸|卜『β■∏ˉ■■「■●|||口■】|『》户卜『‖=■厂『■■厂■ˉ尸■「‖匹■·■′■厂|}■■‖β‖|‖■■|■■|▲■■∩■=『||■『■|■【■||■■【■||止■■厂

●选择器

p日ge对象内置了很多用于选取节点的选择器方法’例如〕方法’给它传人一个选择器’就能返回 匹配到的第一个节点’等价于q0ery5e1e〔tor方法;又如〕〕方法’给它传人选择器’会返回符合选择 器的所有节点组成的列表,等价于query5e1ector∧11方法。

下面我们分别调用〕方法` querγ5e1ector方法` 〕〕方法和query5e1ector∧11方法’代码如下: mpOrta5y∩〔io 千r刚pyppeteermport1日‖∩ch +r咖pγquery加portpyQuerγa5pq

a5y∩〔de千们aj∩(): bro"5er=awajt1a‖∩c∩()

page≡awajtbIow5er.∩e"page()

a"a1tpage。goto(‖们ttp5://5pa2.5crape。〔e"ter/0) 己"ajtpage."ajt「or5e1ector(0 .ite们·∩a川e‖) j—Ie5l』1t1≡a"己jtp日ge.〕(! .ite们。∩a刚e!) jˉre5u1t2=abmtpa8e.query5e1e〔tor(|·jteⅦ .∩aⅧe!) jjˉre5u1t1≡awa1tpage.〕〕(‖.iteⅧ.∩a们e!)

jj-res01t2=日wajtpa8e.query5e1e〔torA11(′.jteⅧ 。∩a爬‖) pri∩t(』〕Re5u1t1: 0 ’ j-re5u1t1) prj∩t(‖〕Re501t2:|’ j-re5u1t卫) prj∩t(|〕]Re5u1t1; |》 jjˉre5u1t1) Pri∩t(0〕〕 ∩e5u1t2: ! ’ jj_re5l』1t2)

己脚a1tbIo"Ser·C1o5e()

a5y∩C1o.get-eγe∩t-1OOP()。ru∩u∩tj1一〔oⅧP1ete(∩`aj∩()) 运行结果如下:

〕Re5u1t1: 〈pyppeteer.e1e爬∩tha∩d1e.[1e‖e∩t‖a∩d1eobjectatOx1166+7ddO〉 〕Re5u1t2: <pyppeteer.e1e爬∩t‖a∩d1e.[1e们e∩t‖a∩d1eobje〔t日tox1166+07d0>

〕〕Re5u1t1; [〈pyppeteer·e1e减t‖a∩d1e.[1eⅧe∩t‖a∩d1eobjectat0x11677d+50〉’<pyppeteer,e1e爬∏t∩a∩d1e。

[1e爬∩t‖a∩d1eobjectat0x1167857d0〉′〈pyppeteeI.e1咖e∩tha∩d1巳【1e爬∩t‖a∩d1eobje〔tatOx116785110)’

(jjppete…1…∩t‖a"d1e.[1e|"e∩t‖a∩d1e0bjectat0x11679db10〉’〈pγppete…1e爬"t∩a∩d1e[1e爬∩t‖a∩d1° obje〔t己tox1』679dbdo>] 〕〕 Re501t28 [〈pyppeteer.e1e「∏e∩t∩日∩d1e.[1eⅧe∩t‖a∩d1eobje〔t己t0x116794十1O〉’ <pyppeteer.e1e爬∩t∩a∩d1e。 [1e爬∩t‖a∩d1eobjectat0x11679Jd10〉′〈pyppeteer。e1e∩侣∩tha∩d1e.[1e爬∩t‖a∩d1eobjectat0x116794+50〉’ (βjppeteeL…∩t‖a"d1e[1e爬∩t‖a∩d1eobj…tOx11679个69O〉』〈PγPP….e1e"e∩tha∩d1e[1e爬"t什a∩d1° object己t0x11679十75o〉]



254

| |

第7章JavaScript动态演染页面爬取

可以看到’〕方法和query5e1ector方法的返回结果都是和传人的选择器相匹配的单个节点’返



回值为[1e们e∩t‖a∩d1e对象。〕〕方法和query5e1ector∧11方法则都是返回了和选择器相匹配的节点组 成的列表,列表中的内容是[1e川e∩t‖日∩d1e对象。 ●选项卡操作

‖ 」

前面我们已经多次演示了新建选项卡的操作,使用的是∩eWp己ge方法。那么新建选项卡之后,怎 样获取和切换呢?先调用page5方法获取所有打开的页面,然后选择一个页面调用其br1∩g丁o「Io∩t方

川」』

法即可°下面来看_个例子: 1Ⅷport日5y∩〔io +IO∏‖ pyppeteer1爪pOrt1au∩〔∩





日5y∩Cdef∩aj∩(): bIow5er=awajt1己u∩〔h(head1e55=「a15e)

■‖■■□·可▲■∏

page≡a"ajtbrow5er.∩e"page() awaitp3ge.goto(‖http5://洲w.bajdu.〔o‖0) page=a"ajtbro旧5eL∩e"page() a"ajtpage.goto(‖http5://哪.bi∩g。〔o∏)!) page5≡aw己jtbrow自er。page5() pri∩t(0PageS: 0 ’ page5) page1=page5[1] a"aitp日ge1.br1∩g丁o「ro∩t() awajta5y∩〔jo.s1eep(10o〉



a5y∩c1o.get-eγe∩tˉ1oop().ru∩u∩tj1-co∩]p1ete(川aj∩())

这里先启动了Pyppeteer,然后调用∩ewpage方法新建了两个选项卡,并访问了两个网站° —定要有对应的方法来控制-个页面的加载`前进、后退`关闭和保存等行为,示例如下: 1Ⅷporta5y∩〔1o 「r咖pyppeteerj‖port1au∩ch +ro∏‖ pyquery1"portpγQuerya5pq a5y∩〔de+∏ai∩(); bro"5er=a"ajt1au∩〔‖(head1e55=「a15e)

pa8e=a"a1tbrowser。∩e"Page() a"a1tpag巳goto(!http5://dy∩a们j〔1.5cIap巳〔ujqj∩g〔己1.〔o们/‖) a"aitpa8e.goto(0http5://5pa2°5cr3pe.〔e∩ter/‖)

】·‖|』■■‖·‖|·Ⅷ‖■」·‖‖‖』α`‖‖‖□‖』□宅

●页面操作

‖口



#后退

a"ajtpage。go8己ck() #前进

awa1tpa8e.8o「oIward() #刷断

a"ajtpage.Ie1oad() #保存p0「

a"ajtpa8e·5cIee∩5hot() #设Ⅲ页凸盯肌

awajtp己ge.5et〔o∩te∩t(‖〈∩2〉‖e11o‖or1d〈/h2〉,) #设豆0Serˉ^ge∩t

.

a"ajtpage。5et05er∧ge∩t(‖pyt∩o∩‖) #设Ⅲ‖eader5

‖●‖||‖…■可|」■甲」‖』《』■〗|

a"ajtpage.pd十() #截图

a"a1tp日ge.5et[xtr日‖∏p‖e己der5(∩e己der5={}) #关闭

a5y∩〔1o.8et_eγe∩tˉ1oop().ru∩u∩ti1ˉ〔o川p1ete(Ⅶa1∩())

这里我们介绍了一些常用的控制页面的方法’除此以外’还设置了05erˉ∧ge∩t` ‖eader5°

‖|·

己"己1tPage。〔1o5e() awajtbrow5er。c1o5e()





73

PvDDeteer的使用 ypp

255

●点击 尸■厂||巳■尸「

PyPPeteer同样可以模拟点击’调用其〔11c代方法即可°以https://spa2scrape.centeI/为例,等其所 有节点都加载出来后,模拟邮件点击: mporta5y∩cio 千roⅧpyppeteeri∩port1日u∩〔h 「ro‖pyq0erγ1"portpγQuerya5pq







aSγ∩〔de千∏ai∩(): broN5er=a"a1t1au∩〔h(head1e5S≡「315e) page=己wa1tbrowser.∩ewpage()

|)

日N己jtpa8e.goto(‖们ttp5://5pa2.5〔rape·〔e∩ter/‖) aWaitp己8e.Ⅳa1t「Or5e1e〔tOI(! .jteⅦ °∩己爬!) 日wajtpa8e.〔1jc|〈( ‖ ·jte们 。∩a们e ’ optio∩s={ |bl」tto∩|: |rjg∩t‖’

■■∏

「}

’〔1j〔促〔Ou∩t|: 1’ #1或2



|de1ay|: 〕oo0’ #迁秒

}) awa1tbIow5eI·c1o5e()

a5γ∩〔1o.get_eγe∩tˉ1oop()。Iu∩u∩tj1-〔o盯p1ete(Ⅶ己1∩())

这里C1j〔k方法中的第一个参数就是选择器,即在哪里操作.第二个参数是几项配置’具体有如 下内容°

厂7 匹

□b‖tto∩:鼠标按钮’取值有1e什、‖1dd1e、 r1g‖t° □C1jc促〔ou∩t:点击次数’取值有l和2’表示单击和双击°

□de1ay:延迟点击。 ●输入丈本

Pyppeteer也可以输人文本,使用tγpe方法即可,示例如下: mport日5y∩〔1o

+ro们pyppeteer1们PoIt1aq∩〔h +ro们pyq‖eIγ1"poItpγQl』erγaspq



a5y∩〔de十"aj∩(): bro"5er=aw日jt1au∩〔∩(∩ead1e55霉「a15e)

p己ge≡a"aitbro"5er°∩e"page() a"a1tpage.goto(|∩ttp5://洲w.taob日o.〔o‖‖) #后退

awaitpage。tγpe(《#q』』 |ipad′) #关闭

a"ajta5y∩〔1o.51eep(1o) a"ajtbro"ser。〔1o5e()

a5|/∩〔1o.getˉeγe∩t1oop().r|」∩u∩ti1ˉ〔咖p1ete(刚日i∩())

这甲我们打开淘宝’给tγPe方法的第—个参数传人选择器’第二个参数传人要输人的文本内容’ Pyppetee『就可以帮我们完成输人了° ●

获取信息

page对象需要调用co∏te∩t方法获取源代码’〔oo代1e5对象调用〔oo促je5方法获取,示例如下: i们POrta5γ∩〔iO

十ro∩pγppeteeri们port1au∩〔∩ 千ro们pyquery1‖portpγQ0erγa5pq

a5y∩〔de十‖aj∩():

brow5er=a"ajt1a仙∩〔h(he日d1e55=「a15e)

page=a"己jtbroⅦ5eI.∩e"page()

a"aitP己ge.goto(,∩ttP5://5pa2.5〔rape.〔e∩ter/』)





第7章JavaScript动态渣染页面爬取

256

pri∩t(|‖Ⅷ[: | 》 awa1tpage.〔o∩te∩t()) pIi∩t(0〔oo长je58 ’ 日w日itpage.〔ookje5()) awa1tbro切5er.c1o5e()

●执行

Pyppeteer可以支持执行JavaScript语句,使用eva1l」ate方法即可°看之前的例子: 1‖poIt己5y∩〔1o

+rO们pyppeteermpOrt1己l』∩〔∩ Wjdt‖’ ∩ei8∩t=1366’ 768 a5y∩〔de+Ⅶai∩(): browser=a"ajt1au∩〔h() p己ge≡己"a1tbroN5er.∩eNPage(〉

a"aitPage.5etγje"port({W1dt∩0 : wjdth’ ‖hejght‖ : ∩ejght}) a"ajtpage·goto({http5://5pa2。5cr日pe。〔e∩teI/‖) awajtpa8e."ajt「or5e1ectoI(0 。jte" .∩a爬‖) 己w己ita5y∩〔jo.51eep(2) 日wajtpag已5〔ree∩5∩ot(path二!exa刚p1e.p∩g|) di爬∩51o∩5=a"a1tp3g巳eγa1uate(0 ! 0() =〉{ retl』r∩{ wjdth: docu爬∩t。docuⅧe∩t[1e爬∩t.〔1ie∩t‖jdtb』

heig们t: do〔u川e∩t.do〔u‖e∩t[1eⅦe∩t·〔1je∩t‖e1ght’ deγjce5〔a1e「a〔tor: wj∩dow°deγi〔epixe1Ratjo’ 、

) 〕, ‖ 0\





pri∩t(d加e∩51o∩s) a"a1tbro"5er·c1o5e() a5y∩〔1o。get-eve∩t-1oop(〉.ru∩—u∩tj1ˉco们p1ete(Ⅶaj∩()〉

这里我们调用eγa1uate方法执行了JavaScript语句,并获取了对应的结果。另外’Pyppeteer还有 expo5e「u∩〔tjo∩` eva1uate0∩‖ew0ocu们e∩t、 eγa1uate‖a∩d1e方法,可以了解_下° ●延时等待

在本节最开头的地方,我们演示了wa1t「or5e1ector的用法,它可以让页面等待某些符合条件的 节点加载出来再返回结果°这里我们给wa1t「oI5e1e〔tor传人一个CSS选择器’如果找到符合条件的 节点’就立马返回结果,否则等待直到超时°

除了"a1t「or5e1e〔tor外,还有很多其他的等待方法,具体如下°

□wa1t「or「u∩〔t1o∩:等待某个JavaSc∏pt方法执行完毕或返回结果。 □"a1t「or‖aγ1gat1o∩:等待页面跳转’如果没加载出来’就报错° □wajt「orReque5t:等待某个特定的请求发出。 □"ajt「OrRe5pO∩5e:等待某个特定请求对应的响应° □"a1t「OI:通用的等待方法。

』■Ⅵ‖‖‖』】■■】‖||】■■□《]‖·〗■‖|刘门‖』·‖]■‖‖‖』■`□ˉ■、|·■■‖」日β」叫|||《|‖|』■司||日‖□{』·】』·‖|■■」』□■』‖‘□■]|‖|、|‖』‘』口□·■、|■』■‖」■∏|〗■」■|』·|

a5y∩〔io.getˉeve∩t-1oop()·Iu∩u∩t11-〔o呻1ete(爪己1∩())

□"a1t「orXpat‖:等待符合XPath的节点加载出来°

通过各种等待方法’就可以控制页面的加载情况了° ↑5.总结

Pyppeteer还有其他很多功能’例如键盘事件、鼠标事件`对话框事件等,这里就不再_一赘述了°

更多内容可以参考官方文档h忱ps://miyakogjgithub.jo/pyppetee∏referencehtml的案例说明°

本节我们凭借_些小案例介绍了Pyppeteer的基本用法’7.6节将使用Pyppeteer完成一个爬取实例°

‖■■】■■■■

74Playwright的使用



) }





257

本节代码参见: https://gjthuhcoImPython3WebSpide【/PyppeteerT℃st°

尸|ayw「|g∩t的使用

7.4

Playwrjght是微软在2020年年初开源的新一代自动化测试工具’其功能和Selenium`Pyppeteer等 类似’都可以驱动测览器进行各种自动化操作°Playw∏ght对币面上的主流测览器都提供了支持’API 功能简洁又强大,虽然诞生比较晚’但是现在发展得非常火热°

↑P|ayw『|ght的特点 □Playw∏ght支持当前所有的主流测览器,包括Chrome和Edge(基于Chromium)`Firefbx`Safari (基于webKjt)’提供完善的自动化控制的API°

□Playw∏ght支持移动端页面测试,使用设备模拟技术’可以让我们在移动Web测览器中测试响



「 p

应式的web应用程序°

□Playwright支持所有测览器的无头模式和非无头模式的测试° □Playw面ght的安装和配置过程非常简单,安装过程中会自动安装对应的测览器和驱动’不需要



额外配置WebDriver等° 了API编写的复杂度°

卜 |

本节我们就来了解下Playw∏ght的使用方法。 2.安装

首先请确保Python的版本大于等于3.7°

| p













要安装Playw∏ght’可以直接使用pip3工具’命令如下: p1P3 i∩5ta11p1aγwr1ght

安装完成后需要进行_些初始化操作: p1aywrjghtj∩5ta11

这时Playwrigth会安装Chromjum、Firefbx和WebKjt测览器并配置-些驱动,我们不必关心具体 的配置过程’ Playwright会自动为我们配置好。 具体的安装说明可以参考h呻s://setupscrap@ccnter/playwright°

安装完成后’便可以使用Playwright启动Chromium、Firefbx或WebKjt测览器来进行自动化操 作了°

3.基本使用

playwnght支持两种编写模式,_种是和Pyppe饥er—样的异步模式’一种是和Selenlum一样的同 步模式,可以根据实际需要选择使用不同的模式° 先来看_个同步模式的例子: 千rO们p1ayWright。5y∩〔-aP1mPOrt5y∩〔-P13yWrig们t Mth5y∩〔-p1aywrjg∩t()a5p:

十orbrow5er_typej∩ [p.〔∩ro|muⅧ」 p.十ire+ox’ p.web代1t]: bro"5er=bro"ser—type。1au∩〔∩(head1e55=「a15e) page=bro"5er.∩e"~page()

pa8巳goto(|∩ttp5://|川ww.ba1du,co‖|)

P己8e.5〔ree∩5})ot(Path=十,5〔ree∩5hOtˉ{brOW5erˉtype。∩a们e}.p∩8|)

pri∩t(page.tit1e()) brow5er,〔1o5e()

厂 | 丛

□Playw∏ght提供和自动等待相关的API’在页面加载时会自动等待对应的节点加载’大大减小

7

第7章JavaScript动态渣染页面爬取

这里我们首先导人并直接调用了5ymˉp1a讥mg∩t方法’该方法的返回值是一个p1ay0mg∩t〔mto《t阳∩ager 对象,可以理解为一个测览器上下文管理器’我们将其赋值为p变量°然后依次调用p的chro们1u∏` +jre十ox和web代1t属性创建了Chromium、FirefOx以及Webkit测览器实例°接着用-个十or循环依次 执行了这3个测览器实例的1au∩〔h方法,同时设置head1e55参数为「己15e°

注意如果不才巴∩ead1e55参数设五为「a15e’就会以默认的无头模式启动测览器’我们将看不到任 何窗口°

在「or循环中, 1au∩〔∩方法返回的是一个Bro"5er对象,我们将其赋值为bro"5er变量.然后调用

bro"5er的∩e"←page方法新建了一个选项卡,返回值是一个page对象,将其赋值为page’这整个过程 其实和Pyppetecr非常类似°之后调用page的_系列API完成了各种自动化操作,调用goto方法加载 某个页面,这里访问的是百度首页;调用5〔ree∩5hot方法获取页面截图’往其参数中传人的文件名称

| 叮 巴 ■ ■ ■ ■ ■ ■ β ■ ■ ■ ■ = 尸 ■ ■ ■ = ■ ■ ■ 『 ■ 尸 ■ ■ ■ ■ ■ ■ ■ 口 ■ 】 】 〗 】 ■ ■ ■ Ⅵ 』 〗 · 』 ■ 由 』 □ 】 ■ □ 回 | ■ 司 ‖ ‖ | | ■ 』 ■ 可 』 】 ■ 』 ■ ‖ 』 ■ | 】 · 勺 』 ■ 】 ■ ■ ■ □ ‖ ■ 可 | | | 已 可

258

是截图自动保存后的图片名称’这里的名称中我们加人了brow5er-type的∩aⅧe属性’代表测览器的类 型’于是3次循环中5cIee∩5∩ot方法的结果分别是〔hromⅧ`十1re十ox和"eb低1t°另外’还调用了t1t1e q

并将返回的页面标题打印到控制台°最后’调用brow5er的c1ose方法关闭整个测览器,代码结束。 运行_下这段代码’可以看到有3个测览器依次启动’分别是Chmmium、FiIefbx和Webkjt测览器’ 启动后都是加载百度首页’页面加载完后,生成页面截图,然后把页面标题打印到控制台,就退出了° 此时’当前目录下会生成3个截图文件,图片都是百度首页,文件名中都带有对应测览器的名称, 如图7ˉ30所示.

[=刁

{互l

__





sc『ee∩s∩ot-

sc『ee"shot←

sc『ee∩sh◎t-

ch「om|um.p∩g

fi『efox.p∩g

web灯t.p∩g

图7ˉ30同步模式示例的运行结果

■■‖乙■】‖|」■司■妇■」●可‖|』■】』■』■〗■□Ⅷ□‖』】=■■‖‖{■■日■‘」■凸■司』{』■】■』■■■‖‖‖‖■·司·■■』■■■〗』■』日

方法’该方法会返回页面的标题,即HTML源代码中t1t1e节点中的文字,也就是选项卡上的文字,

控制台的运行结果如下:

可以发现’我们非常方便地启动了三种测览器,完成了自动化操作’并通过几个API就获取了页

面的截图和数据’整个过程速度非常快,这就是Playwright最为基本的用法。 当然,除了同步模式’Playwright还提供了支持异步模式的API,如果我们的项目里面使用了 aSyncio关键字,就应该使用异步模式,写法如下: jⅧporta5y∩〔io

+IoⅧp1aywrig∩t。己5y∩〔-apj加porta5y∩〔-p1己ywrig∩t a5γ∩Cde千帕j∩(): 己5y∩〔"jtba5y∩cˉP1ay"rjght() a5P: +orbrow5erˉtypei∩[p.chromu们’ p.+ire+ox’ p."ebkit]: broNSer≡a"aitbIowserˉtype·1au∩〔∩()

」■』|』■||‖』■■〗‖‖∏」■』·‖‖∏■】Ⅵ‖〗■■】』|·■·口‖|■□‖‖■可■司‖|」勺‖勺|‖』■■■■

百度一下,你就知道 百度一下,你就袭回道 百度一下,你就孜回道

7.4

Playwright的使用

259

P己ge≡3Na1tbro"5er。∩e"—page() 己"ajtPage.goto(!http5://洲w。baidu.〔o∏)

aw日1tP己ge.5〔ree∩5∩ot(Path=「‖5〔ree∩5hotˉ{browser—type.∩a∏e}。p∩g|) pri∩t(日"己itpag巳t1t1e())

aw己itbIow5er.c1o5e() a5y∩C1o.ru∩(ma1∩())

可以看到,写法和同步模式基本—样,只不过这里导人的是a5γ∩〔-p1ayWr1g∩t方法’不再是 5y∩〔ˉp1ayWr1g∩t方法’以及写法上添加了a5y∩C/aWait关键字’最后的运行效果和同步模式是—样的。 另外可以注意到’这个例子中使用了"jthaS语句’"1t‖用于管理上下文对象’可以返回_个上 下文管理器,即_个p1aywr1ght〔o∩text‖a∩ager对象’无论代码运行期间是否抛出异常’该对象都能 帮助我们自动分配并且释放Playw门ght的资源。 4代码生成

Playw∏ght还有_个强大的功能’是可以录制我们在测览器中的操作并自动生成代码,有了这个

功能’我们甚至一行代码都不用写。这个功能可以通过p1aywrjg∩t命令行调用〔odege∩实现’先来看 看〔odege∩命令都有什么参数’输人如下命令: P1aywrjght〔odege∩ˉˉ‖e1p

结果类似如下: 05age: ∩pxp1ay刊rig∩t〔odege∩[optjo∩5][l』r1] ope∩pagea∩d8e∩emte〔ode千oru5eIactjo∩5 0ptjO∩S: ■ ■ ■ ■ ‖ 『 ‖ ● 「 ▲ 旧 ‖ ‖ 伊 卜 }

‖ 卜 尸





β

》卜



ˉo’ ˉˉol』tput〈十j1e∩a爬〉 saγe5the8e∩erated5cIiptto日fi1e ˉˉtarget〈1己∩gUage〉 1日∩gu己getOu5e’O∩eO十jav日5〔rjPt’Pytho∩’Pyt∩o∩ˉa5y∩C’〔5h己rP(de十au1t; "Pyt∩O∩") ˉb’ˉˉbr呻ser〈br叫ser「ype〉br叫sertou5e’o∩eo千〔r’c∩r咖iⅧ’仟’十jrefox’w促’眶bRit(de+au1t: "chromu们") ˉˉ〔∩己∩∩e1<c∩3∩∩e1) 〔hromuⅦdi5tribl」tjo∩〔ha∩∩e1’"〔打ro∏记"’ 圃〔hrα肥-betaw’""sedgeˉdev口’ et〔 ˉˉ〔o1orˉ5〔he眠<5ch曰爬〉 e们(』1atepIe「erIedco1or5che‖记’ 呵1jght曰or"dar代" ˉˉdeγj〔e〈deγ1ce‖aⅧe) 印u1atedeγj〔e’十orexaⅦp1e mipho∩e11冈 ˉˉgeo1o〔atio∩<〔oordj∩ate5〉 5pe〔j十ygeo1o〔己tjo∩〔ooIdi∩己te5’ 十oIexa呻1e■37.819722’ˉ122.478611" ˉ-1oadˉ5tomge<千j1e∏a爬) 1oadco∩text5torage5tate+ro∏lthe+j1e’previous1ysaγed"ithˉˉ5aγeˉ5tomge ˉˉ1己∩8〈1己∩gu38e〉 5pecj「y1a∩8l」age/1o〔a1e’ foIex己呻1e□e∩ˉC8"

ˉˉproxyˉ5erγer〈proxy> 5pe〔j十yproxy5erγer’千oremm1e闻∩ttp://叮pIoxγ:3128口or"5oc促55://』WpIoxγ:808o‖° ˉˉ5aγeˉ5tom8e〈千i1e∏a雁〉 5aγeco『〗text5torage5tateatthee∩d’+or1ateru5e"jt∩ˉˉ1oadˉ5torage ˉˉtj碴zo∩e<ti眶zo∩e〉

ti爬zo∩etoem1己te’ 「orexa∏p1e ,0[urope/倪m论圃

-ˉtj眠out〈ti畦o仙t》 ˉˉU$e【ˉa8e∏t〈ua5tri∩g〉 一vie硒rtˉ5ize≤5ize〉

t1眶ol』t+orp1ayNrig∩ta〔tjo∩51∏m11j5e〔o∩d5(de+au1t8 "10凹o碱) 5pe〔j十yuSeIage∩t5tIj∩g 5pe〔j十ybr叫serγie即ort5izei∏pjxe1s’+ore×a呻1e.128o’720·

ˉh’ ˉˉhe1p

di5p1ayhe1p「or〔mma∩d

[x咖Ie5: $〔odege∩ $〔odege∩ˉˉtaIget=pytho∩

$〔odege∩ˉb啮bkjthttp5://exa呻1e。〔咖

可以看到结果中有几个选项’ˉo代表输出的代码文件的名称; —target代表使用的语言,默认是

pytho∩,代表会生成同步模式的操作代码,如果传人pytho∩ˉasy∩〔则会生成异步模式的代码; ˉb代 表使用的测览器,默认是chroⅧju‖。还有很多其他设置,例如ˉdeγice可以模拟使用手机测览器(如

■■尸[■■厂|▲■■|■「『□■厂■■尸卜■’■■■■∏‖但■『「





jPhone1l)’ˉ1a∩g代表设置测览器的语言, ˉtj眶out可以设置页面加载的超时时间° 了解了泣些用法后,我们来尝试启动一个FiIehx测览器,然后将操作结果输出到sc∏ptpy文件, 命令如下: p1aywrjg‖t〔ode8e∩ˉo5〔r1pt。pyˉb十jre千ox



|』』■∏』Ⅲ日』』■■■■■Ⅵ日]‖·|

260

第7章JavaScript动态渣染页面爬取



代码°我们可以在测览器中随意操作’例如打开百度,点击搜索框并输人∩ba’再点击搜索按钮’这 时的测览器窗口如图7ˉ3l所示° | ◇



圃 功……面

馋撇m叮亩

…仓‖

u■ |■■

跃■…q刀■■■m■■咖蔓】

唾i溢百窿

』■■‖□』■〗‖

运行代码后会弹出_个FirefOx测览器,同时右侧输出一个脚本窗口’实时显示当前操作对应的





』■‖‖司‖山]‖■二■】



商旗←下



iPⅧ 《=

≡…红

…■…■Q田 …

…哇冉…■ ‖

… 堂◆E■

d

~乙…平■下■ ……■■ ………■■■

『…●咎工■田沽●冗■■■

马…

■■£■

印■m扮…Q

x…冲q

●″,『…h

体`撇…翻手…拍■…谷输静渺钢竹尸印迢幌呐协

图7ˉ3l

运行结果

|■■∏』■□■■■■司■』■■可

舵丁e启……硒pm●审



‖■〗口·〗

∏乃■P

巴田

f≡丁『r舔]

可以看到,测览器中会高亮显示我们正在操作的页面节点’同时显示对应的选择器字符串



●亩●

●Re呵dD!≥

俩w啦勺ht‖∩…c七◎『

‖‖

′b,

<p沁呵≥

γ

≡■

1「厂咖plαyWFig忙.Sy∩C-opi.『雨雨sy拘七≡pih肉卜id`t 2

3de「厂u∏《pl□yW「1ght): 4 b广呻5e「■ployW厂i蜘七°「i广efOx.1αmc∩(∩em1es5■「αl$G) 5

[o∩teXt=b尸呻5e「.∩鄙=CO∩teXt()

6

7

8

俞帅即『峪wpαge

pOge■〔mte吐,∩ew=pogeO

9

10

■6OtohttpB://…ˉbα1du·〔蛔/

11

四ge°goto(口http田5//…Dm血°c…′!)

1之

13

M

存〔M〔仪 1∏put[∩…■俩“w]

poge。〔I【Ch〔』,t∩p凹t[∩α锤■\圃嗣\同]阑)

15

16

】7

F∩llmput[∩…■咖wd威]

poge.「iu〔.i∏put[∩呻e■\.咽\〃]喻’ 同∩bα『,)

18

19

自pFGS5[∩t·F

20 21 22

肚wi饰mge.expGCtˉ∩αVi9αt1O∩(u「1■■竹ttp3『//砷.bα1dL』°〔咖/B71e叫七「司鹏「汪路『。5v ∏ithp□ge.eXpe硅ˉmVi卯ti酮◎: p□ge.p尸e5S(脚i『酗』t[∩…渔\.wd\瞬]`, 0 回[∩te厂圃)

z3 Ⅲ4

#-ˉ=ˉ、-…^.=ˉˉˉˉˉˉ一ˉˉ

H5 Z6

CO∩text.〔1oSe◎ b尸咖巴e7.〔1◎seO

』■』‖□|■】□■□∏』■』■■□‖』】■了‖■可■■■日』■■‖可|』□□』□■』■■乙■可」‖■■■

1∩put[∩a"e="wd||],右侧的代码窗口如图7ˉ32所示。

27

Z8∏lth5y∩c~p1叮W厂tg打t◎αSp1αyW「tght: 「u∏(p1□ⅦFtght)

图7ˉ32代码窗口



〗〈‖

∑9















74Playwright的使用

26l

在操作测览器的过程中’该窗口中的代码会跟着实时变化’现在这里已经生成了刚冈||_系列操作 对应的代码,例如: pag巳千111("j∩put[∩a们e=\‖Wd\"]0』’ 0』∩ba")

这行代码就对应在搜索框中输人∩ba的操作。所有操作完毕之后,关闭测览器’Playwnght会生 成—个scriptpy文件,内容如下: 「ro用p13ywrjg∩t·5y∩〔≡apij川port5y∩〔-p1aywright de「n」∩(p1ay"Ijght): brow5eI=p1aywr1ght.+jIe+o×.1a0∩c∩(咐ead1e55=「己15e) 〔o∩text≡bro"5er°∩ew〔o∩text() #打开所页面

pa8e≡〔o∩text.∩eⅦˉpage() #访问http5://咖w.bajdu·〔o刚/

page.goto(捌http5://洲".bajd‖.co||‖/") #点击枚索框

page。c1jc低("i∩put[∩a∏e≡\"m\"]")

P



| 「









#往枝索框中输入丈字

pag巳+j11(』‖i∩put[∩a们e=\"wd\"]"’ "∩b3") #点击枚索按枉

wjthpage.expe〔t-∩aγjg日tjo∩(); page。c1ic促("text=百度一下,』) 〔o∩text.〔1o5e() bro"5er.〔1o5e〈)

withsγ∩〔-p1ayNrjg∩t() sγ∩〔-p1ayNrjg∩t() a5p1aywr1ght: a5p1aywI1ght; ru∩(p1aywrjght)

可以看到这里牛成的代码和我们之前写的示例代码几平差不多’而且也是可以运行的,运行之后 会看到它在复现我们刚才所做的操作°

P





所以,有了代码生成功能’只通过简单的可视化点击操作就能生成代码,可谓非常方便!

| }

另外这里有一个值得注意的点’仔细观察一下生成的代码’和前面例子不同的是’这里的∩ew—page 方法并不是直接通过brow5er调用的,而是通过〔o∩text’这个〔o∩text又是由browser调用 ∩ew〔o∩text方法生成的。有朋友可能会问,这个co∩text究竟是做什么的呢?

) ) }

其实, co∩text变量是_个8row5er〔o∩text对象,这是—个类似隐身模式的独立上下文环境’其运 行资源是单独隔离的。在-些自动化测试过程中,我们可以为每个测试用例单独创建_个8r咖5er〔o∩text





对象’这样能够保证各个测试用例互不干扰’具体的API可以参考https://playwrightdev/python/docs/apV classˉbmwsα℃ontext。

5.支持移动端测览器

Playwnght的另—个特色就是支持模拟移动端测览器,例如模拟打开iPhonel2ProMax上的Safm 测览器。 示例代码如下: 千ro们p1aywr1g∩t。sy∩c-api加port5y∩〔-p1aywrig∩t

w1th5γ∩〔ˉp1ayNr1g∩t() a5p;

1p∩o∩eˉ12ˉproˉⅢax=p·deγ1〔es[|jpho∩e12pro阳x!] bro刊5eI=p.眶b促1t.1au∩〔h(he日d1e55≡「日15e)

〔o∩text二br叫ser.∩ew〔o∩text(



第7章

JavaScript动态演染页面爬取

**1p‖o∩e=12-prO=ⅦaX’ 1oca1e=!zhˉ〔‖0

page=co∩text.∩ew-page(〉

p3ge.goto(0∩ttp5://`州w.w∩ati5呵brob‖5er。〔o∏/|) page."己jt_+orˉ1oad—5tate(5t3te=0∩etwoI代jd1e{) pa8e。5cree∩5‖ot(p己th=bro"5erˉipbo∩e.p∩g|) broⅣseI.〔1o5e()

这里我们先用p1ay"r1g∩t〔o∩text‖a∩ager对象的deγ1ce5属性指定了-台移动设备,传人的参数 是移动设备的型号,例如iPhonel2ProMax,当然也可以传人其他内容,例如iPhone8` Plxel2等°

】|』■■‖|‖□■■■〗〗』‖司|‖‖』』■‖||」·可|』』■●■】〗‖』□‖】】』■■■〗』‖□■■〗】〗叮■{‖]‖·

262

前面我们已经了解了8Iow5er〔o∏text对象,它也可以用来模拟移动端测览器,初始化_些移动设 备信息、语言权限位置等内容°这里我们就创建了一个移动端8ro"5er〔o∩text对象,最后把返回 的8row5er〔o∩text对象赋值给〔o∩text变量°

接着’我们调用co∩text的∩ew-page方法创建了一个新的选项卡’然后跳转到_个用于获取测览 器信息的网站,调用wajt{or1o己d5tate方法等待页面的某个状态完成,这里我们传人的5tate是 ∩etwor促id1e’也就是网络空闲状态°因为在页面初始化和数据加载过程中,肯定有网络请求伴随产生, 所以加载过程肯定不算∩et"or促jd1e状态,意昧着这里传人∩etwor促id1e可以标识当前页面初始化和 数据加载完成的状态°加载完成后,我们调用5〔ree∩5∩ot方法获取了当前的页面截图’最后关闭了测 运行一下代码’可以发现弹出了_个移动版测览器’然后加载



……



御…嗡…5 咆







—个移动端测览器。

α



测览器信息是iPhone上的Safm测览器,也就是说我们成功模拟了

……

输出的截图也是测览器中显示的结果’可以看到’这里显示的

…… …怠

@W佣钦}sM归陋wse「c◎嗣酌妇…

…沁

出了对应的页面’如图7ˉ3]所示。

q .….…豆』宅狸雪YB舅写渍 西 ∏>2磊:Ⅷqo砖…萝凹.军愿西c@

Sa↑a『|o∩阳acOS =丑

这样我们就成功模拟了移动端测览器并做了—些设置,其操作

0

■″N肘滁]!V卞

(·」日‖|||」■■凸■■】』■□■日

鲍↑a『门耳◎『`『oS↑4q …尸浑

API和PC版测览器是完全一样的。

″…





→产

6.选择器



D…→厂″~

沁↑ 丫

U…0=究』牢′唾Fp,铱 一=■

不知道大家有没有注意,前面的〔11〔促和「j11等方法都有_





-兰

d沪 =



个字符串类型的参数’这些字符串有的符合CSS选择器的语法,

宙■恳』咖 5

有的以teXt=开头,似乎不大有规律,那么它们到底支持怎样的匹 配规则呢?下面就一起来了解_下°

的规则,例如直接根据文本内容筛选、根据节点层级结构筛选等°



一幻

β…丝



} ˉ…w由凰Row雹鹰wssm…

』□□】■』

我们可以把传人的字符串称为E‖cmcntSelector,除了它已经支

持的CSS选择器`XPath, Playwnght还为它扩展了一些方便好用

妇→…●乌龋南≡0■ 口

■帧

!

|{

览器°

0

…………………铂……■

0晌凹阿」『…切…8…h唾『№…… …■……m拘■…m…=

…吨m… ●



图7ˉ33当前的页面截图



●文本选择





文本选择支持直接使用text=这样的语法进行筛选’示例如下:





page.〔1j〔k(.te×t=[ogi∩画)



CSS选择器在3.3节就介绍过,例如根据id或者c1a55筛选:

■ ■ 二 ■ ■ ‖ 」 ■ ■

●CSS选择器



这代表选择并点击文本内容是[ogi∩的节点。

page.〔11c代(喊butto∩慰) page。〔1jc低(!0#∩avˉbar .〔o∩ta〔tˉu5ˉjte↑∏同)

〗‖■■』■■■■■|」|』■■』□‖



■【■∏■【■■■■【■■■『〖■『}‖尸卜■【卜「|■■「Ⅱ■「》■|Ⅷ|’}「『‖β‖膜■厂卜|■「【■■「‖尸|‖’仁|阻′■尸|》|广‖卜卜卜|’凸■「口|■厂|∩|》『卜任}卜尸|)『【■「‖〗尸|卜[·厂》「▲■「仿「■■『‖|

7.4

Playwright的使用

263

根据特定的节点属性筛选: Page.C1j〔促("[dataˉte5t≡1ogi∩ˉbutto∩]憾) page·〔11〔长("[ariaˉ1abe1=05jg∩i∩, ]")

●CSS选择器+丈本伍

可以使用CSS选择器结合文本值的方式进行筛选,比较常用的方法是∩日5ˉtext和text’前者代

表节点中包含指定的字符串’后者代表节点中的文本值和指定的字符串完全匹配’示例如下: page。〔1jC促("己rti〔1e:ha5ˉte×t(0p1aywright!)!0〉 page.〔1jc促("#∩aγˉbar :text(|〔o∩ta〔tu5』)")

第一行代码就是选择文本值中包含p1ayWr1ght字符串的art1C1e节点,第二行代码是选择1d为 ∩aγˉbar的节点中文本值为〔o∩ta〔tu5的节点°

●CSS选择器+节点关系

CSS选择器还可以结合节点关系来筛选节点’例如使用‖a5指定另外—个选择器’示例如下: p38e。〔1jc促(氮.it印ˉdes〔rjptio∏:∩a5(.ite∏ˉpro『‖oˉba∩∩er)”)

这里选择的就是〔1a55为jte∏]ˉde5〔r1pt1o∩的节点,且该节点还要包含c1a55为jte∏‖ˉpr"℃ˉba∩"er 的子节点°

另外还可以结合-些相对位置关系’例如使用rjg∩tˉo+指定位于某个节点右侧的节点’示例如下: page.〔1ic促("i∩p0t:rjg∩tˉo十(:text(U5er∩己‖e』))闻)

这里选择的就是一个1∩put节点’并且该节点要位于文本值为05er∩a爬的节点的右侧° ●XPath

当然,XPath也是支持的,不过Xpat∩这个关键字需要我们自行指定,示例如下: p3ge.〔1j〔Ⅶ(口xpat‖≡//b0tto∩闽)

这里在开头指定“xpath=字符串,,,代表这个字符串是—个XPath表达式。 更多关于选择器的用法和最佳实践’可以参考官方文档ht印s:〃playwri绑dcv/盯d】on/…s/sel唾蛔B° 7.常用操作方法

上面我们了解了测览器的初始化设置和基本的操作实例,下面再介绍—些常用的操作方法。例如 C1iCk《点击》,十j11(输人)等,这些方法都属于p3ge对象,所以所有的方法都可以从page对象的API 文档查找,文档地址是https:〃PlaywTightdev/Python/docs/ap″c‖assˉpage° 下面介绍几个常见操作方法的用法。 ●卒件监听

Pa8e对象提供一个o∩方法,用来监听页面中发生的各个事件,例如close、console、load`【记quest、 【℃SpOnSe等°

这里我们监听!℃sponse事件,在每次网络请求得到响应的时候会触发这个事件’我们可以设置回 调方法来获取响应中的全部信息,示例如下: 千Imp1ayNright.5y∩〔一apijmrt5y∏〔-p1ayNright de十o∩-ⅢeBpo『`5e(re5po∩5e):

pIi∏t(十05tatue{re5po∩5e.5t己tu5}: {respo∩5e.ur1},)

"jth5y∩〔ˉp1ay刊rj8∩t()a5p:

bro们5er=p.〔hIo∏|iu∏.13u∩c‖(‖ead1e55=「己15e)



| |



第7章JavaScript动态渣染页面爬取

264

■□

page=br叫5eI.∩ewˉPage() page.o∩( ′re5po∩5e ’ o∩-re5po∏5e) page.goto(|http5://5pa6。5〔rape.〔e∩ter/|) pag巳"ait千or1o己d5tate(|∩etwor代1d1e!)

‖·‖



browser.c1o5e()

我们在创建page对象后’就开始监听response事件’同时将回调方法设置为o∩ˉre5po∩5e, o∩ˉre5po∩5e接收—个参数,然后输出响应中的状态码和链接。 运行上述代码后,可以看到控制台输出如下结果:

5t日tue2o0: 们ttps://5p己6.5〔r己pe.ce∩ter/j5/app.5e千0d454.j5 5tatue2oo: 肘ttp5://spa6.5crape.ce∩ter/j5/c∩u∩恨ˉve∩doI5.77da+991.〕5 5t己tue2oo: ∩ttp5://spa6.5〔r日pe.〔e∩teI/c55/〔hu∩代ˉ19c920f8。2己6496eO°〔55 5tatue2oo: https://5p己6。5crape.ce∩ter/〔55/〔hu∩低ˉ19c92o+8.2a6q96eo·c55 5tatoe2oo: ∩ttp5://5pa6.5cmpe.ce∩ter/j5/chu∩低ˉ19c920+8。〔3a1129d.j5 5tatue2OO: ‖ttp5://5pa6。5〔mpe。ce∩ter/i阳g/1ogo。a5O8a8十O·p∩g 5tatue2oo; ∩ttp5://5pa6。s〔rape。〔e∩ter/十o∩t5/e1e‖e∩tˉjco∩5·535877十5·wo仟 5tatue301: http5://5pa6.5〔rape.〔e∩ter/apj/mγie?1mit=108o仟5et≡O8to促e∩=‖C咖付z「∩‖C[z盯「i付z〕促卯[OZ丁Q1γj0z 盯〔2删Mγ丁I1γ冰OZ删3∩5wx‖jIyO丁[4‖『[5 5t己tueⅪ0O: http5://5pa6·5〔rape.〔e∩ter/apj/|∩oγje/?11川it=1O&o仟set=O8toke∩=‖呻z『∩肌〔2盯「j∩z〕低"[OZ丁01γj0z ‖丁〔2叫Mγ丁I1γ2koZ删3"5wx‖jIγO丁[州丁[5



■Ⅵ‖』■】〗□日■■可臼■『』■‖』■』日·■·]‖■勺‖□

5tat仙e200: 打ttps吕//5p日6.5cr己pe·ce∩ter/ 5tatue20o: https://5pa6.5cmpe°ce∩ter/〔s5/日pp·ea9d8O2a.c55

5t己t0e20o: ∩ttp5://p0。‖eitua∩.∩et/|∏oγ1e/da6466o十82b98〔d〔1b8a38o4e696o9eo411o8.jpg0064训ˉ6“h-1eˉ1〔 5tatue20o: ‖ttps://po.爬itua∩.∩et/刚vie/283292171619〔d千d5b24oc8+do93+1eb∑55670.jpg刨64w644∏1e1〔 5tatue2oo: http5://p1.『∏ejtu己∩.∩et/加γie/b6o7千b己7513e7「15eab17oaa〔1e14ood878112.jpg刨64w6』4‖1e1〔

注意这里省咯了部分重复的内容。

内容是—一对应的° 嚼蜒专瓣

§. 孕

0奄.谊、●

」·γ」■ 可』』□Ⅵ■Ⅷ

▲■●§

年向

≈‖○

·』『』司

…″咐螺帖 `



同5c『ap° ■王别姬ˉ「a「●W诅‖ⅧyC◎∩mb帅●

9·5

●●

(当■■可‖』■■■■



◆古台●●

…地.中■S∑′∏?甸切 〗…′泌L砧

守α





F凹 ~

□~≈■…=≈≡·冗Q 9

.

▲▲←▲▲



…田

. ˉ . .. . . . .. .

■0

■ ." ·. . ˉ . .

登巴…m°●…■≈■哇m≈叼≈…荤.t〕■ˉ号弓% 二 9 。 亡=ˉ.岛■ .ˉ』



…≈

■■■■■■■

-

■≈

,吨≈

vm■

]■电

0山≈

—≡

.≈≈

…■

=■

呻■

≡气



=≡

=二

≡益

三二≡≡

牵砧



§

怠|

…→



」■】‖■■■‖‖|』■]‖{■·|当■■

●●

9.5

~=~≡β_

|□●】■■可‖‖』■■‖‖|」■■■■

|二琶茁业‘.,

…匡…



』 ■ 』 ■ 日

可以发现’这个输出结果其实正好对应测览器Network面板中的所有请求和响应’和图7ˉ34里的



■`‖』‖

』 ■ 】 ‖ ■ ■ 勺 ↑ 』 『

图7ˉ34测览器的Network面板

■■■】■】‖|』』■】●■日」‖‖·司









7.4

Playwright的使用

265

我们之前分析过这个网站,其真实的数据都是Ajax加载的,同时Ajax请求中还带有加密参数’ 不好轻易获取°但有了o∩re5po∩5e方法,如果我们想截获Ajax请求,岂不是就非常容易了?改写_ 下这里的判定条件,输出对应的JSON结果,代码如下: 千ro∏P1aywrjght。Sy∩〔=api1Ⅶport5y∩c≡p1aywIig∩t de千o∩-re5po∩se(respo∩5e):

j十 0/日pi/Ⅷγje/0 j∩resPo∩5e°ur1a∩drespo∩5e·5t日tu5≡2OO: pri∩t(re5po∩5e.j5o∩()) "1thSy∩C-p1ayWrjght()aSp: bro"5eI=p.〔hro∩nUⅧ.1au∩Ch(打ead1e55霉「a15e) pa8e=brow5er.∩e比page() page.o∩(0re5po∩se‖’ o∩ˉre5po∩5e) P日ge.goto(|https因//5pa6·scr日pe.〔e∩ter/0) Pa8e."ait+or1oad5tate(‖∩et"o工促1d1e‖) brow5er。c1o5e()

控制台的输出结果如下: {|〔ou∩t|; 1O0’ 』re5u1t5` : [{0id|: 1’ !∩aⅦe0 ;霸王别姬|’ 0a1ja5|: ‖「are"e11‖y〔o∩〔ub1∩e』’ 』〔over : |"ttP5://P0·川ejtua∩。∩et/‖j|oγie/ce4d日3e03e655b5b88ed31b5cd7896〔十62472·jpg以64"6“h1e1〔|’ 0c己tegor1e5! : . [!剧怕‖’ !爱悄|]’ ,Pub1i5‖edat0 : |1993ˉ07ˉ26』’ |m∩ute‖: 171’ |5〔ore|: 9.5’ 』regjo∩5‖ ; [ 』中国大陆‖’ |中圆杏港|]}′ ●

p



|P‖b1j5hedat! :‖o∩e’ "m∩ute‖: 1O3’ |5〔ore, :9°0’ 』regio∩5』: [ 0吴国!]}’{‖jd: 1O’ ‖∩a∏e0 ;`狮于王0 ’ ‖a1ja5‖ : 「|7 丁he[jo∩Ⅸi∩8|’|〔OVer|: ‖httP5://Po.爬itua∩。∩et/|∏oγie/27b76+e6c+3903+3d74963十7o786oo1eM38』06。jp8刚64w64q|v 1e1c|’ |c日tegorje5|; [`动画‖′ 0砍斧|’ ‖∏险‖]’ ‖P仙b115∩edat! : |1995ˉO7ˉ15』’ |m∩ute|: 89’ ,5〔ore0 : 9.0’ ,reg1o∩50 ; [|矣国』]}]} L

简直是得来全不费工夫’我们通过o∩re5po∩5e方法拦截了Ajax请求,直接拿到了响应结果’即 使这个Ajax请求中有加密参数,也不用担心’因为我们截获的是最后的响应结果’这让数据爬取变 方便太多了°

其他的事件监听,试里就不再一_介绍了’可以查阅官方文档° ●获取页面源代码



获取页面源代码的过程其实很简单,直接调用page对象的〔O∩te∩t方法就行,用法如下: 「ro‖p1aywrig∩t.5y∩〔-apii"port5y∩〔-p1日ywrigbt wit∩sy∩〔-p1ayNrjght() a5P: bro"5er=p.chro∏nⅧ.1au∩〔‖(he己d1es5=「315e) p己ge=brow5er.∩ew=p日ge()

pa8e.goto()‖ttps://5pa6·5crape.〔e∩ter/0) p己ge.wajtˉ+or-1oadˉ5tate(|∩e↑wor代id1e|〉 htⅧ1≡page.〔o∩te∩t() pri∩t(ht∏1) bro"Ser。〔1o5e()

运行结果就是页面源代码°获取了页面源代码之后,借助_些解析工具就可以提取想要的信息了° ●页面点击

实现页面点击的方法’我们已经不陌生了’就是C1i〔代方法’这里详细介绍一下这个方法如何使 用° 〔1jC促方法的API定义如下: p日ge.c1j〔k(se1ector’**灿ar85)

可以看到,必须传人的参数是5e1e〔tor,其他参数都是可选的°5e1ector代表选择器,用来匹配 想要点击的节点’如果有多个节点和传人的选择器相匹配’那么只使用第-个节点° 其他一些比较重要的参数如下。



□C1jCkCOu∩t:点击次数’默认为1。

□ti们eout:等待找到要点击的节点的超时时间(单位为秒),默认是30°

□po5jtjo∩:需要传人一个字典,带有x属性和y属性,代表点击位置相对节点左上角的偏移量。 □+oI〔e:即使按钮设置了不可点击,也要强制点击,默认是「a15e° 〔1iC长方法的内部执行逻辑如下°

■■】|‖]己■■■·可『』】■可■□】■■■■=■■|□■■■=

第7章JavaScript动态渣染页面爬取

266

□找到与5e1ector匹配的节点,如果没有找到,就一直等待直到超时’超时时间由t1们eout参 数设置°

□检查匹配到的节点是否存在可操作性’等待检查结果’如果某个按钮设置了不可点击,就等该 按钮变成可点击的时候再去点击,除非通过+orCe参数设置了跳过可操作性检查的步骤,才会



强制点击。

‖□

□如果有需要,就滚动—下页面,使需要点击的节点呈现出来°

□调用page对象的‖ou5e方法,点击节点的中心位置’如果指定了po51t1o∩参数’就点击参数 指定的位置° {

具体的参数设置可以参考官方文档ht印s:〃Playwrjghtdev/python/docs/apj/C‖assˉpage/#pageclickselectop kWargs。 勺

●丈本输入

文本输人对应的方法是{111’其API定义如下: Page.「i11(5e1e〔tor’γa1l』e’ **kwar85)

·■‖γ■■Ⅵ‖■■

这个方法有两个必传参数,第一个也是5e1ector’依然代表选择器;第二个是γa1ue,代表输人

的文本内容;还可以通过tj们eOut参数指定查找对应节点的最长等待时间° ●获取节点属性

除了操作节点本身,我们还可以获取节点的属性,方法是get-attrjbute,其API定义如下:



page.get-attrjbute(se1e〔to】’∩a爬′**刚argβ〉

可以通过t1爬Out参数指定查找对应节点的最长等待时间。示例如下: 于r叼p1ayNIjght。5γ∩〔-apimport5y∏〔-p1aymjg∩t

br础5er。〔1o5e()

这里我们调用了get-attrjbute方法,传人的5e1e〔toI参数值是a.∩a|‖e’代表查找C1a55为∩a爬 的己节点, ∩a们e参数值传人了hre十,代表获取超链接的内容,输出结果如下: /detai1/ZⅧZ‖〔‖OZⅫX№〕Od"[jⅫO1‖3〔X〔ⅣⅧ5Ota趴5酬∩5Z11tb"1爬}阴q[5「p∏∧tb"IX

可以看到获取了对应节点的hre「属性,但只有—条结果,这是因为如果传人的选择器匹配了多 个节点’就只会用第_个°那么怎样获取所有匹配到的节点呢?





■■司■=■■‖|‖■■■■|■■Ⅷ』●■]

问jt∩5y∩〔ˉp1ayNrjght()a5p: br酮5er=p.c向rmju∏。1己u∩〔∩(head1e55≡「315e) page二br酗5er°∩e"ˉpage() pa8e.8oto(』https://5pa6。5cmpe.〔e∩ter/°) page.Nait-+oI-1oad_5tate(0∩etmrkid1e,) hre「=pa8e。get_己ttⅢjbute(,3.∩己匪『’ 0hre十!) pri∩t(hIef)

■∏』日■■『■■■■■‖闯

这个方法有两个必传参数’第_个还是5e1e〔tor;第二个是∩a"e’代表要获取的属性的名称;还



●茨取多个节点 □勺]门

使用queryˉ5e1ectora11方法可以获取所有节点’它会返回节点列表,通过遍历得到其中的单个



‖‖





74Playwright的伎用

267



■■厂『■厂■=尸■’▲■尸■■■厂|△■■≡

节点后’可以接着调用上面介绍的针对单个节点的方法完成_些操作和获取属性,示例如下: 十ro阳p1aγwrjght·sy∩〔-api1「‖port5y∩〔-p1aⅦrjgbt "jt∩5y∩〔ˉp1日Ⅶrjght() a5p: brow5er≡p.〔∩Io∏nu们.1au∩〔h(‖ead1e5s≡「a15e) p己ge=bro们5er.∩ew≡page(〉 pa8e.goto(‖"ttps://5pa6.s〔rape。〔e∩teI/!) Page.wajt千or1oad5tate(‖∩etwoIkjd1e|) e1e爬∩t5=pag巳queryˉ5e1ector日11(|a.∩a∏e‖) 十oIe1e『爬∩ti∩e1e川e∩t5:

■■‖■尸

pIi∩t(e1e『∏e∩t.getˉattIibute(』hre十0)) prj∩t(e1e爬∩t.textco∩te∩t()) bIo"5er.〔1o5e()

膛■‖

这里通过queryˉ5e1e〔toIa11方法获取了所有匹配到的节点’每个节点各对应一个[1eⅦe∩t‖a∩d1e 对象,可以调用[1e"e∩t‖a∩d1e对象的getˉattr1bUte方法获取节点属性,也可以通过text〔o∩te∩t方



法获取节点文本。



运行结果如下:

■尸‖■=■■■=尸

/detai1/Z‖γz‖〔‖oZXγx肌〕od‖[jⅫ01‖3cxcⅣv‖5ota代∧50↑|h5Z21tb‖1『∏e‖剁q[5「p∏∧tb‖I× 霸王月||姬ˉ「aIe眶11‖y〔o∩cubj∩e /det己j1/Z刚z‖〔‖oZXγx‖C〕od‖[j巩〔o1阳〔x〔Ⅳγ‖5ota趴5酬∩5Z21tb什1爬什"q[5「p[丁∧tb‖Iy 这个杀于不太冷- l色o∩

/detaj1/Z‖γz‖〔‖oZXγxⅧ]0d‖[jⅫo1‖3〔x〔Ⅳγ‖50ta促∧5训‖5Z21tb‖1爬删q{5「p[丁∧tb‖Iz 肖中允的救赎ˉ『he5∩awsha∩促日ede呻tjo∩

/deta11/Z刚z‖〔‖0ZXγxⅧ〕0d‖[jⅫ01‖3〔xcⅣv‖50t己灿50什内5Z21tb‖1爬朋qL5「p∏∧tb‖I0 纂坦尼兑号ˉ丁jt日∩1C

} ■◆■‖▲■■

/detaj1/Z川z‖〔‖OZXV×‖C]0d‖[j巩〔O1‖3cx〔Ⅳv‖5Ota趴5训∩5Z21tb‖1雁Ⅷql5「pl丁∧tb‖I1 罗马假日 ˉRo阳∩‖o11d日y /detaj1/Z‖γz‖〔‖0ZXγx肌〕0d‖[jⅨ〔01‖3C×〔Ⅳγ‖5OtaR∧5洲∩SZ21tb‖1爬ⅧqL5「p[丁∧tb"I2 唐伯庇点秋脊ˉ「1irt1∩g5Cbo1ar /det日11/Z‖γz‖〔‖0ZXγx肌〕0d‖[jⅦ〔o1‖3cx〔Ⅳv‖50t冰∧50}{h5Z21tb‖1爬删qL5「pl『∧tb‖I3 乱世佳人ˉ6o∩ewit∩t∩e‖j∩d

/deta11/Z‖γ∑‖〔‖oZXγx‖C〕0d‖[jⅫ01‖3cx〔Ⅳv‖5ota灿S00{∩5ZⅪ1tb‖1爬Ⅷq[5「pl『Atb‖I4 甚剧之王_「hem∩go千〔o‖记dy

『「}β



/detaj1/Z‖γz‖〔‖0ZXγx‖C〕0d‖[jⅨ〔o1‖3cxcⅣγ‖50takA50|{∩5Z21tb‖1爬删q[5「pL「Atb‖I5 楚门的世界ˉ「‖e『r0们己∩5∩ow

/deta11/Z州z‖〔‖0ZⅫx‖C〕od‖[jⅨ〔01‖〕〔x〔Ⅳγ‖50t冰∧50↑|们5Z21tb‖1眶Ⅷqk5「p[TAtb"Ix灿≡ 狮于王ˉ丁he[io∩Ri∩g

●获取单个节点

■ ■ ■ 尸 ■ 匹 ∏ 『 ′

获取单个节点也有特定的方法’就是queryˉ5e1ector’如果传人的选择器匹配到多个节点,那它 只会返回第_个’示例如下: +ro∏] p1aywrjg∩t°5y∩〔-ap1mportsy∩〔-p1aywI1g∩t with5γ∩〔-P1日yNrjg‖t()aSP:

P

‖ ‖ 》



brow5er=p。〔∩rom帅.1au∩cb(he日d1e55=「a15e) pa8e≡br酬5er。∩e"-page() page.goto(0∩ttp5://5pa6.5crape.ce∩ter/|) page。"ait+or1oadstate(|∩et即水id1e,) e1e∏论∩t二p己8e.query_5e1ector(`a。∩a『∏e|) pri∩t(e1e爬∩t。get-attribute(|hre十0〉) pri∩t(e1e爬∩t.text〔o∩te∩t())



bro"5eI。〔1o5e()

〉|

运行结果如下:

}广[『‖

/detaj1/Z川z‖〔‖oZxγx肥〕od‖[j倔〔01‖3〔x〔Ⅳv‖5ota灿5叫h5Z21tb‖1爬删q[5「p口∧tb‖Ix 霸王别姬ˉ「己reWe11‖γ〔O∩Cubj∩e

匹■■「‖『‖‖|炉}|■■【■■【『|匹∩[’}』「亡【■

可以看到这里只输出了第一个节点的信息.



‖』●■□』□·

第7章JavaScript动态演染页面爬取

268

●网络劫持

再介绍-个实用的方法——Ioute,利用这个方法可以实现网络劫持和修改操作’例如修改reque5t 的属性,修改响应结果等。来看-个实例: +ro‖p1aγNright.5y∩〔一ap1i"port5γ∩〔=p1aywr1gbt 1ⅦpOrtre

wjt∩5y∩〔-p1aywright() a5p; browser=p°chroml」川.1au∩〔打(∩ead1e5s=「a1se) page=bro"5er.∩e"ˉp3ge(〉 de+〔a∩〔e1-reque5t(route’ reque5t); route.aboIt()

page.route(re.〔o‖pj1e(r"(\.p∩g)|(\·jpg)")’〔a∩ce1ˉreque5t〉 p己ge.goto("http5://5pa6。5〔rape°〔e∩ter/") p日g已wa1t-千or-1o日d_5tate(‖∩etwor低1d1e‖) pag巳5cree∩s∩ot(pat∩=!∩oˉpjcture.p∩g‖) bIo"5eI.〔1o5e()

(\.jpg)代表所有包含.p∩g或.jpg的链接’遇到这样的请求’会回调ca∩ce1ˉreque5t方法做处理。 ca∩〔e1ˉreque5t方法接收两个参数,一个是route’代表一个〔日11日b1e【oute对象;另一个是reque5t, 代表Reque5t对象°这里我们直接调用〔a11ab1eRoute对象的abort方法’取消了这次请求,导致最终 的结果是取消全部图片的加载。

观察下运行结果’如图7ˉ35所示,可以看到图片全都加载失败了° | ·口急『■…|… —区





肆=……_了—-—=…=←~重了r百惑亩了

令嗡。〔—



5.Dp●

.=

r■王别姬ˉ尸己「ew●‖ⅧyC◎们cuM∏● ●■

9·5 ∩台∩白■

↓ 中Ⅲ呐`中■■Ⅺ′↑7↑分w γw№′ˉ把上浊

而=≡二 这个杀手不太冷ˉLG◎佣

●●●●■

9·5 凸★台合★

珐■′‖↑O分w

|‖」可】‖|』〗』|勺‖]|引」`」』‖(』■·Ⅷ】|‖√‖‖|』∩|』]」‖|(■口‖‖』||(·可』□】‖』』

这里我们调用了route方法,第_个参数通过正则表达式传人了URL路径’这里的(\.p∩g) |

‖….0么上殴

■h 二

9·5 ◆亡凸凸白

贞■′tQ2分仰 ‖…↑0上照 笆

图7ˉ35图片全部加载失败

也许有人会说这个设置看起来没什么用啊?其实是有用的’图片资源都是二进制文件,我们在爬 取过程中可能并不想关心具体的二进制文件内容’而只关心图片的URL是什么’所以测览器中是否 把图片加载出来就不重要了,如此设置可以提高整个页面的加载速度’提高爬取效率。

另外’利用这个功能’还可以对_些响应内容进行修改’例如直接将响应结果修改为自定义的文 本内容°

■■■■■■『■■■■■【■■■尸■■■■■『【〗■‖』■■‖||√‖]‖』』●■□】【■■·□‖■■●·〗】』‖』‖』〗■■●■

肖申克的救赎ˉ丁∩●5h■w已h已∏h∩ed●mPtl◎∩ ●‖■■



75

Selenium爬取实战

269

这里首先定义-个HTML文本文件’命名为custom-response.html’内容如下: 〈!欧Ⅳp[htⅦ1〉

〈∩t川1〉 <‖ead>

〈tjt1e〉"日〔代Re5po∩5e〈/tit1e〉 〈/∩ead〉

〈body〉 〈∩1〉"a〔kRespo∩5e</h1〉 〈/body〉 </‖t‖1〉

代码编写如下: |「尸|卜【β(『『【[β|卜|卜|仁「》『}》「}|「卜|厂|『■∩「■『「卜「仁‖「仁|■|卜卜『|■庐}}}|【‖[}β′|巴’『|七

千ro‖∏p1ayNrj8ht.5y∩c-api1呻oIt5y∩〔-p1日ywright

W1th5y∩〔-p1ayNrjght() a5p: brow5er≡p.c∩ro∩nⅧ.1au∩〔h(‖ead1e55≡「己15e) page≡brow5er.∩ewˉp己ge() de千‖`odj十y-re5po∩5e(Io‖te’ reque5t): rO0te.十u1fi11(path≡"./〔l』5tO|∏ˉre5pO∏5e.ht|∏1』』)

page.route(!/0 ’ "od1十γ-resPo∩5e) page.8oto("http5://5pa6.5〔rape.ce∩ter/") bro"5er.c1o5e()

「7

这里我们使用〔a11ab1eRoute对象的千u1fi11方法指定了一个本地文件’就是刚才我们定义的 HTML文件,运行结果如图7ˉ36所示。

i■■

回……

旗卜

●|

图7ˉ36修改响应结果后的代码运行结果

可以看到,响应结果已经被我们修改了’URL依然不变’但结果已经变成我们修改后的HTML代 码°所以通过route方法’我们可以灵活地控制请求和响应的内容’从而在某些场景下达成某些目的° a总结

本节介绍了Playwright的基本用法’其API强大又易于使用’同时具备很多Selenium`Pyppeteer 不具备的更好用的API,是新-代爬取JavaScript喧染页面的利器。 本节代码参见: https://gjthuhcom/Python3WebSpider/PlaywrightT℃st°

7.5

Se|e∩|um爬取实战

在7.1节’我们学习了Selenium的基本用法’本节结合-个实际案例体会-下Selenium的适用场 景以及使用方法。

↑.准备工作

请先确保已经做好了如下准备工作。

h

第7章JavaScript动态渣染页面爬取

270

□安装好ChIDme测览器并正确配置了ChromeDrjver。

□安装好Python (至少为36版本)并能成功运行Python程序。 □安装好Selenium相关的包并能成功用Selenium打开Chrome测览器. 这些步骤在7.l节都有提及,可以参考相关内容° 准备工作都做好后,便可以开始实战练习了° 2爬取目标

本节还是用电影网站h忱ps://spa2.scrape.centeⅣ做示例,首页如图7ˉ37所示° 贞





■仓



…●



□□句≤







舶蹿曝;γ撼●

趣瑶

撼j鹤潍:聪;

办岛●!

回scmp.

涨Tγoj患 牺A甲

■王别姬ˉ萨■征w●‖』佃yc◎∩cub阳●

9 5

●●

G





‖|‖



中m■唾■′V∏w■

?硒7锄上炉

〈」

_ ■

簿钞牵凸=1柠:γ£|岂 {

·■|』《、‖

这个杀手不太冷ˉL6◎门

9·5

●●■●

●●台台

…β?绵分p Q钾■钾0q上●

闪申克的救肇ˉ丁h●Sh■w刮Dm悦∩●4●而ptj◎∩



95

|‖

●●

宙台●古●

…八恤分伪

D≈《印咕k■

图7ˉ37示例网站的页面

酶霸▲四切……·





勺条

.蹿.,

斟 趣蹿● 臼★冉蜀●『

e

●呀=铂……=■亡些■妇之Z炉J…fY丫▲哇罕-它≡…=ˉˉ ^■~=D≡=犁7=

回scr°p. ■王别姬.尸■…刨‖肌yC◎∩mb枷o ●●

9怎

』‖|‖

+◆○

≈□■‖‖|』司|」■司』日|』·

乍一看’页面和之前没什么区别。接下来我们仔细观察每部电影的URL和A|ax请求APL例如 点击《霸王别姬》’观察URL的变化,如图7ˉ38所示°



白仑古台仓

……Ⅺ∏w0” 】…泌』龟

B片●~出《■贾m》钝Fn,p些些罩守.八之■ˉˉ…贝盂■

′』`ˉˉ■伏大…■■∧=个唾0 曰个■■’ˉ睁…夹p和# 元∏=出『■干■0 .Ⅲ■■■Ⅸ沮′力■,■人唾m■■平 0■子…》.僵谰入叮凶二哼∧生又E蹿卒■不回·■′」心囊

丁■■啪`m妇]喻■■…uⅢ砷■■…三■锭朋 ← P□L铂▲■■●■心■·↓●00◆蹿●】■馋c▲■■·8w ■●■■■●△D■斡0■·m梆私··0■●Q■夕●■k·已

′』铀了佰诌0■此0宝人空=出《■歹…》匪出的…■召



凶碑人庄.…衣归S尸Ⅻ不b·m唾叭父宦■=m宝时嘘

■~■==



■mm仇■■.』叼(hm■]与m砍{巴■●饵》G=叶打

0■■

‖|

|呻■介

…代贝且的史迁不■鸦●阳■°■

…■=△■……净…■←

|导演

□‖



图7ˉ38

电影《霸王别姬》主页





』|



7.5

Selenjum爬取实战



可以看到,电影详情页的URL和首页的不一样。在图2ˉl3中’URL里的detajl后面直接跟的是ld, 是l、2`3等数字’但是这里变成了-个长字符串,看着是由Base64编码而得,也就是说详情页的URL 中包含加密参数’所以我们无法直接根据规律构造详情贞的URL°

然后,依次点击列表页的第l页到第l0页’观察Ajax请求,如图7ˉ39所示。

p





$仕●圈凹…富…



十◆护



·|



&☆

●9…“r…亿田汕酶j〖颠↓锤严娥

p

■●

叫|_ˉ

ˉ■

密】■■■←■叮■■啤凸■蝉■……■■■■

_…



_硒



_≡





P

=_…

__一…ˉ



≡”

……

_ˉ__



●…… 』……



P

p

27l

-~→=ˉ~

「 p

以…啼…奄…≡



犁.=^…-__ˉ—- ◆≡



团==-垂.=……′…赋……………………割….…m…^翻嚷啦mwm…哮『愿……国褪鳃 p

/董i苦-ˉ

F

b



窒星蹬鼠{嚣愚γ驴…

p 〔

.!…0≡‘Vr画ˉ ^气耳≡≡.…曲『轴·00碎0抽α●p公吐和·Vo脚5抽.■·斌;“p3

型净



≡…0』m

盈=……咆…m窒鳃?塑屿…………,颜…腿洞…m“〃…瞬………’ =…口=~□一皂■p 队……↑…←洁鳃E丫″↓wW=蚊…〗〃…2·肛…Q…t叮′…■

β





L

b

由幽s/m

…心■■蜘t幻α…〗■…蛇学o ■【Ⅳ…回;…→’…凹[m…■‖V妇严w

丛雪哪≠=…廓w阿阐m

……~门 …~…v

鳃β↑渺…必.7谭′●尼咱h……擒

[≡=二己 ˉ ˉ= . 阳■冗

图7ˉ39Ajax请求















p

b



「 b

b



p

可以看到’这里接口的参数多了_个token字段’而且每次请求的token都不同,这个字段看着 同样是由Base64编码而得°更棘手的_点是,API具有时效性’意味着把Ajax接口内URL复制下来’ 短期内是可以访问的,但过段时间就访问不了了’会直接返回40l状态码°

之前我们可以直接用requests构造A〕ax请求,但现在A|ax请求接口中带有token’而且还是可变 的。我们不知道token的生成逻辑’就没法直接构造Ajax请求来爬取数据°怎么办呢?先分析出token 的生成逻辑’再模拟A|ax请求’是_个办法,可这个办法相对较难°此时我们可以用Selenlum绕过 这个阶段’直接获取JavaSc前pt最终痘染完成的页面源代码再从中提取数据即可° 之后我们要完成如下工作°

□通过Selenium遍历列表页,获取每部电影的详情页URL。

□通过Selenium根据上_步获取的详情页URL爬取每部电影的详情页。 □从详情页中提取每部电影的名称、类别、分数、简介、封面等内容。 3.爬取列表页

先做—系列初始化工作: 十Io们5e1e∩1uⅧj们portwebdriγeI

十ro∩5e1e∩1‖Ⅷ。〔o『∏『旧∩.ex〔ept1o∩5 j『∏port『i『∏eout[xCePtjO∩ b

「 p

0

「 p

P

+roⅦ5e1e∩1u们·webdr1γer,〔o『γ∏℃∩。by加port8y

+ro们5e1e∩1l』".webdr1γer·5upportjⅦportexpected=〔o∩djtio∩5a5[〔 +ro阳5e1e∩1uⅧ.webdr1γer.5oppoIt·wajtmport"eb0r1γe巾ajt

第7章JavaScript动态演染页面爬取

272

1们port1ogg1∩8

1og81∩g。bas1〔〔o∩+ig(1eγe1≡1oggi∩g.I‖「0’ fom日t=0%(a5Ctme)5 ˉ%(1eVe1∩a∏e)5:咒(『∏e55age)S‖) I‖D[X0【[≡ ‖http5://5pa2.5〔r己pe.〔e∩ter/page/{Page}! 丁I"[α∏=1O

『0丁∧[pM[ =1O bro"ser=webdr1γer°〔hro∏‖e() wait二"eb0rjver‖ajt(bIow5er’∏‖[α」丁〉

这里首先导人了_些必要的Se‖enium包,包括webdriver`WebDriverWait等’后面我们会使用这 些包爬取页面和设置延迟等待等°然后定义了日志配置和几个变量,这和之前几节的内容类似。接着

使用〔hro"e类生成了-个"ebdriγer对象,并赋值为bro侧5er变量°我们可以通过bro"5er调用 Selenium的一些API来对测览器进行一系列操作,如截图、点击、下拉等°最后,我们声明了-个

‖eb0I1γer‖a1t对象,利用它可以配置页面加载的最长等待时间。

能够观察到,列表页的URL还是有一定规律的,例如第_页的URL是https://spa2.scrapecenter/ page/l ’最后的数字就是页码’所以可以直接构造出每_页的URL。

■‖|‖■、』■可·司

下面我们观察—下列表页,然后爬取其中的数据。

那么,怎么判断_个列表页是否加载成功呢?很简单,当页面上出现了我们想要的内容时’就代

表加载成功了°这里可以使用Selenium的隐式判断条件’例如每部电影的信息区块的CSS选择器 #j∩deX .jte们,如图7ˉ40所示° ●【● +

回≈…『… ○



x



△…2酝……



食■坷●!





===匹…,p■■碱≡



匡丁



盈白空…▲一—乙

"…



0

{」



醚…

Q



叼呕



宝q

. 『瓢





-

●γ

瘁呻巳Q0p户

.譬. °…』

…l…蛔w怜扣 ●…=导斤≡←≤

p… ◆=r…≈……` 7叼态■1庐户可T



§

×

蹿…………】 =

t呻.m$+◎圃

ˉ0-哺『0沉γm《 》

·1T哮‖●t卜←…c6』《

旧….鹏■》囱灿`cnβ』〗

■』α∩【 〗…



蜘…日…j 嘛 ■

.Q■Y{

·<」v…,…固…u…`…Ⅱ`.…皿…w刁…~… b四』U●忙←■<■…OqmB■叶·●l■四旧』〖申>↑ 1□■记沟厂毛「→厂~′O」净 》吧1r囱0←←…必■【■#少℃1←c■冈八…压■ 1■尸■■■T刘. ′~√m沪

剿蔼;严6`岛鳃导; ::}鞘{戳盅。′《 坤心『■『PQq…0◆p■宇■f

尸勇唾,….i』

‖●0哟■《 _锗

顷≡诗…F■α导l



卜叼加…>←…山【…■·■●B辽■■』■■>t t=刁.′~叼′m吵

◆叫汕″>v…■巴m″口●l…门1…昏0 1←蛔广=■『 ˉfP■口/山吵 □/■助尸

助′≈『『◆1陀mM□LP…句】 睡切…=‖■〃〗 . ′田行【

二β~晓/·』西

●m77…U◆咀缉5 ●`4十0■=R■□

靶』诊

尸阐』■幻t铲庐■和@佰℃0F^■l≡宙、■′m>

l

·/口■诊

_ 1硒口』t呻【卜.】■‖

■妒『

△』Q1忱

呵7…T■叮坤≡《

电厂.≡

ˉ: .



图7ˉ40电影的信息区块

直接使用γ15jb111ty≡o「—a11—e1eⅧe∩t5ˉ1o〔ated判断条件加上CSS选择器的内容,即可判断页面 有没有加载成功’配合‖eb0river‖ajt的超时配置’就可以实现10秒的页面加载监听°如果l0秒之 如下:

0

‖|| | { 』

内,我们配置的条件得到满足,就代表页面加载成功’否则抛出「1川eout[x〔eptio∩异常°实现代码

|」‖‖」勺』□‖。‖』■□〗』{

山∏F↓可』 ■【“占J

……■…QJ】日d…=仿q叮卸mqu.P々』■■■g咏n

………轻凸…m…≡00■…●….ˉ

」■■‖‖‖‖Ⅵ■■■司当●∏

p叼』■鲍≥…【哇″■l≤…皿印←0 1…r刨-^■√●』沪 p■此“↑护伊…勋em乙w户O飞<D呵』m←T 1G哉宁刘垒广Ⅷ■√m吵

::●代7

≈°←~匡△■;】

■巾沁=h叮;…『…■t『





‖||



D刨』v…>V=田巳边■≥吼艺硒【t■■酝臣叮二F~咖』々凸

↓≤』γc…●驴…=威y…■m均■

』‖ |{

_…











|巨

吊臼函薄m…





| 卜厂|『慎[}■『『止「「伊|『|伯|『[尸皿『巴『卜[厂}|■■厂『「’}协『‖【「『》|‖卜「卜)‖止β「卜●}‖卜‖|■■『(■



7.5

Selenium爬取实战

273

de十5CIapeˉpage(ur1’〔O∩djt1O∩’ 1O〔atOI): 1ogg1∩8。1∩十o(|5〔mp]∩g‰0 ′ ur1) try:

brow5eT.get(ur1) 倒己jt.u∩t11(〔O∩d1tjO∏(1O〔3tOr)) ex〔ept丁加eout[x〔eptio∩;

1oggi∩g.error(|erroIo〔〔urred"hj1e5〔r己Pj∩g%50 ’ ur1’ ex〔 1∩于o=丁rue〉 de十s〔mpeˉj∩dex(page): ur1= I‖D〔X0RL千om|at(page=page) 5crape—page(ur1’〔o∩dit1o∩=[〔。γj5ibi1ity-o+ˉa11—e1e爬∩t51o〔ated’ 1o〔日toI=(8y。C55-5[[[〔丫OR’ 0#i∩dex .jte‖‖))

这里我们定义了两个方法。

第一个方法5〔rape-page依然是—个通用的爬取方法’可以对任意URL进行爬取`状态监听以及 异常处理,接收ur1、 〔o∩d1tjo∩` 1o〔ator三个参数: uI1就是要爬取的页面的URL;〔o∩d1tjo∩是页面

加载成功的判断条件,可以是expe〔tedˉ〔o∩d1tjo∩5中的某_项’如γ15jb111t义o十—a11≡e1e∏隐∩t51o〔ated、 γis1bi11tyˉo+—e1eⅧe门t1o〔ated等; 1ocator是定位器,是一个元组’通过配置查询条件和参数来获 取-个或多个节点,如(By.〔55ˉ5[[[〔『0R’』#j∩dex.1te‖|)代表通过CSS选择器查找#j∩dex.jteⅧ来 获取列表页所有的电影信息节点。另外’我们在爬取过程中添加了超时检测,如果到规定时间(这里 为l0秒)还没有加载出对应的节点,就抛出「1Ⅷeout[x〔ept1o∩异常并输出错误日志°

第二个方法5〔rapeˉ1∩dex则是爬取列表页的方法’接收一个参数p日ge’通过调用5crapeˉpage方 法并传人co∩d1t1o∩参数和1ocator参数,完成对列表页的爬取°这里的〔o∩ditjo∩我们传入的是

γ15ib111tyˉo+ˉa11ˉe1e川e∩t51ocated,代表所有节点都加载出来才算成功° 注意’这里爬取页面时,不需要返回任何结果,因为执行完5cmpeˉj∩dex方法后,页面正好处于 加载完成状态,利用bIow5er对象即可进行进一步的信息提取°

现在已经可以加载出列表页了’下-步当然就是解析列表页’从中提取详情页的URL。这里定义 一个解析列表页的方法,具体如下: +r咖ur11ib.par5emportur1joi∩

de十parse—i∩dex():

e1e爬∩t5=bro"5er.+j∩de1e爬∩t5-by-〔s5—se1ector(‖#j∩dex .jte川 .∩aⅧe‖) 十ore1e爬∩t1∩e1e爬∩t5了 href=e1e‖e∩t.getˉattribute(‖∩re+!)

γie1dur1jo1∩(I‖D[Xˉ0R[’ hre十)

我们通过+1∩de1e川e∩t5ˉbyˉ〔55ˉ5e1e〔tor方法直接从列表页中提取了所有电影节点’接着遍历这 些节点’通过getˉattr1bute方法提取了详情页的∩re十属性值,再用ur1joi∩方法合并成—个完整的URL。 最后,我们用一个"a1∩方法把上面所有的方法串联起来,实现如下: de十"aj∩(): try:

+orpage1∩m∩ge(1’『0『A[p∧C[+1): scrape-i∩dex(p己8e) deta11ur15≡par5eˉj∩dex()

1oggi∩g.1∩十o(‖detai15ur15%50 ’ 11st(detaj1_ur15)) +j∩a11y: bro"5er.〔1o5e()

这里我们遍历了所有页码’依次爬取了每一个列表页并提取出详情页的URL° 运行结果如下: 2O20ˉO3ˉ2912:O3;09’896 - I‖「0; 5〔raPj∩ghttP5://5Pa2°5〔mpe·〔e∩ter/p己ge/1

2o2o匡o3ˉ2912:o3:1〕’724ˉ I‖「0: det日115ur1s [`https://5Pa2.scmPe.〔e∩teI/detaj1/



|‖√|‖日

JavaScript动态演染页面爬取

第7章

274

Z‖γz‖〔‖oZXγ×‖C〕od"[jⅨ(o1‖〕cx〔Ⅳγ‖50tak∧5O↑{h5Z21tb刊1爬‖∩q[5印[『∧tbMx0 ,

;∩{tps://5pa2.5crap巳〔e∩ter/detai1/Z‖γ2‖〔‖OZXγxⅧ〕Od‖[j氏〔O1‖3cxcⅣv‖5Ota代∧5卯b5Z21tb}{1ⅧeⅧqL5「pl『∧tb刊I5′′

0http5://5p日2·5〔rape。ce∩ter/det己i1/ZNγz肌‖0ZXγx陋〕Od‖[jⅨ〔01‖3cxcⅣγ‖5Ota趴5叫hSZ21t洲1∏记00↑qL5「p∏∧tbMx趴=0 ]

202oˉo3ˉ2912吕o〕:13’724ˉ I‖「α5〔rapj∩ghttps://5pa2.5cmpe·〔e∩ter/page/2

由于输出内容较多这里省略了部分内容。

观察结果可以发现,我们己经成功提取到详情页那_个个不规则的URL了! 4.爬取详情页

既然已经成功拿到详情页的URL了,接下来就进_步爬取详情页并提取对应的信息吧。

基于同样的逻辑’这里也可以加一个判断条件,如果电影名称加载出来,就代表详情页加载成功。

实现时,调用scrape-page方法即可,代码如下:



日』|□

def5cIapeˉdet己j1(ur1): 5〔rapeˉpage(ur1′ co∩djtio∩=[〔.γi5jbi1ity-o+-e1e爬∩tˉ1o〔ated」 1O〔atOr=(0y.「∧Cˉ‖州[』 |h2!))

这里的判定条件〔o∩djtjo∩传人的是γ151b111tyˉo+-e1e爪e∩t1o〔ated’即单个元素出现即可。 1o〔ator传人的是(8y.丁∧Cˉ‖州[’|h2|)’即h2这个节点,也就是电影名称对应的节点,如图74l所示° 0



◎}

-r|

●=们p图■c呻·‖灿… 牛



| ■

=一=

的★■司●§

·…a…呻m…凶呕w丫…哟【x…″……_ —≡T=一司玩



=司

同5cr…

q

r|\→.蘸嚣”|



中…·铂●■′w0咖

γ酗07悬●上以

‖■■

■片■=出《■王獭》mn,■乙幽三十人之叶…↑咀云Ⅲ

撤F‖

;鳃白蹿≈…割= ,≈



;

×

…~■

莎m′…+d髓







·…t°d《γmγˉ~ˉ…_~≈…

■叫1U珍.…中p ◆…←·宁□+…T■≥q●l=…1牛卢=『、■√·1U产

々●…《

■■加…0←炉尸…01<6哗阶·≥t≈

■…上〔匹5』

≈甲№…k■』 〗…【…70■tj

■■8守“0≯V忿打…〖1…■广●l=百▲



『;沁… ●…空▲二←0门…但四←■●1≈l●l…【=凹●l…l钮0…■丁■

心『

″「…t冗vi◇』睹t

口β■叼『□】…P 啼鳃『 0□≈′

T■▲b…≡■早汀m咱屯‖二二步●l……f~■…∩ ■!甲→

哮…l…■厅』O·… ■m~!……』·驴=f ■βp』步凹u…■■了华;

Q蛆〗80M0炉峙■1…… v酗γ幻0◆←…·匀■烟巳●~1…G{■…≥ 〗己出代rO

■勺山.』户M…【●『 丙■螟壮f■!·J

●咀』●…=乙…←户O《勾‖●l…卜扒吐…l啦的●1≤l~=′… v吧山山…∩…〖l昂el<●l●1也l…●‖→】←泌叭…‖←谚户 吁■…厅0Ⅶ…厢≈卫=P至』Ⅻ……栏=~9■~…w″←△ ˉ=飞

0 ■

【心p■o■…■t■P、■

l幽≡=●西BⅦ…酝=l…M″◆

瘁“

~二—_.瓣 △′≥ 巾…·lv…◆个「T1∑↑■G坤■●■…「…弓≈叹〃皿P ●气d↓■囱它→辊酗0■【m■厂…必↑旷…■1汀◎

…审hb…◎…砧…兰二守…

●《



』■∏〗■■■|」‖』■Ⅵ|■]司‖凸】■‖」‖||」■■■■】□』】〗□·□日】□

…—叭

(…宙)■-咖 ■…●凸●凸≈■=…….^申≈●々 =尸→…』~-——-■

…碍……



…_…=←……

霸王别雄

』■■{可』‖』

{■们蔼介

■→′D【』〗

■up ‖ 』…P(w 蝉07.m「w●t″P0…



炉越上v凹岭←↑冗=□田古『D●·■洁+■』■m■沪乙』■℃■■ ●5=』T→皿■『 ………=≈■…▲…■=00……4鳃=—由=^▲=■西…… =吉=ˉ Y-当= 已三~.

…r…f呵妇→〖

□ ●

阁7ˉ斗l

电影名称对应的节点们2 q

如果执行了5〔rapeˉdeta11方法,没有抛出「1"eout[xcept1o∩异常,就表示页面加载成功了°下 de+paI5edetaj1(): ur1≡brow5eI。〔urre∩tur1

∩a"e=brow5er.十1∩de1e帐∩t-by-tagˉ∩a爬(!h卫!).text

{‖■■』■■●|』】‖■‖

面定义-个解析详情页的方法来提取我们想要的信息°实现如下:

〔ategor1e5= [e1e爬∩t·text十ore1e们e∩t i∩bro"5er.+i∩de1e爬∩t5ˉbyˉc5sˉ5e1e〔tor(0·〔ategorie5butto∩

■‖|』■·】γ』‖‖叫‖|勺』引』●】·】

5pa∩,)] 〔oγer≡brow5er.+j∩de1eⅧe∩tˉbγ-c55-5e1ector(0 .〔oγer》).get-attribute(‖5I〔0) 5core二bIow5er.十1∩de1eⅦe∩t-by-〔1己5s∩a们e(‖5〔ore‖).text dr己Ⅶa=bro"5er.十1∩de1e们e∩t—by-c55ˉ5e1ector(‖ .draⅦ己p|).text

γ ∑





战 实







●■■■





]巳 吕



’ γ

▲_尸】□『‖‖匹■~■厂卜|卜△■■「‖‖‖【■■尸[′



retur∩{ ‖ur10 : 灿r1’ 0∩a∏吧0 : ∩a∏论D 0〔ategorje5|:〔ate8or1es』 〔oγer : coγer’

05〔Ore0 : 5〔Ore’ ′dm‖a0 : dm"a



这里定义了一个p日r5eˉdeta11方法,提取了详情页的URL和电影的名称、类别、封面、分数和

|》

简介等内容,提取细节如下° □URL:直接调用8row5er对象的c0rre∩tur1属性即可获取当前页面的URL。

b

} } }

|防|}「〖■厂|卜■「『》‖尸「卜



□名称:提取‖2节点内部的文本即可获取电影名称°这里我们使用伍∩de1eⅦe∩t_byˉt日gˉ∩a"e方 法并传人∩2’提取到了指定名称对应的节点’然后调用text属性提取了节点内部的文本,即 电影名称°

□类别:为了方便’这里通过CSS选择器提取电影类别,对应的CSS选择器为.〔ategor1e5butto∩

5pa∩°可以使用十j∩de1e们e∩tsˉby-c55-5e1ector方法提取CSS选择器对应的多个类别节点, 然后遍历这些节点,调用节点的text属性获取节点内部的文本。

□封面:可以使用CSS选择器.coγer直接获取封面对应的节点°但是由于封面的URL对应的

是5rC这个属性,所以这里使用get-attribute方法并传人5rC来提取。 □分数:对应的CSS选择器为.s〔ore°依然可以用上面的方式来提取分数’但是这里换了一个

方法’叫作于1∩de1eⅦe∩t=by-C1a55ˉ∩a阳e’这个方法可以使用C1a55的名称提取节点’能达到 同样的效果,不过这里传人的参数就是〔1a55的名称5〔Ore而不是.5COre了°提取节点后, 再调用text属性提取节点文本即可°

□|简介;对应的CSS选择器为.dra"aP,直接获取简介对应的节点,然后调用te烈t属性提取文 本即可°

最后’把所有结果构造成—个字典并返回°

接下来’在|∏ai∩方法中添加对这两个方法的调用,实现如下: de十帕j∏();

| try;forpage1"ra∩ge(1』m肌_pAC[+1); 5〔rape=j∏deX(page) detaj1-ur1s=p己rse-i∏dex()

+ordetai1Mr1i∩1j5t(det己j1ˉuI15):

1og8mg。j∩fo(°getdetai1 l』r1咒50’detai1ur1) s〔mpeˉdetaj1(detai1-uⅢ1) detai1ˉdata=parseˉdetai1() 1吧8j∩g.mfo(,detai1dat己咒s,’detai1d己ta) 十i∩己11y吕 br阻seⅢ.〔1o5e()

这样爬取完列表页之后,就可以依次爬取详情页来提取每部电影的具体信息了° 202OˉO3ˉ2912:2q:1O’723ˉI‖「O: 5〔mPj∩ghttP5://sPa2°scmPe°〔e∩ter/P己ge/1

202oˉo3ˉ2912:24:16’997ˉI‖田: getdetaj1uⅢ1http5://5pa2。5〔rape。〔e∩ter/detai1/Z‖γz№阳ZXγ×胚]o刨[j Ⅸ〔o川3〔x〔ⅣⅧ印taM”‖]5Z21t邯1唾烟k5「p∏At出Ix

2o20ˉo3ˉ2912:24:16’997ˉ I‖「0: scrapi吧http5://5pa2。5〔r■pe.〔e∏ter/detai1/Ⅻγz旺沏ZXγx肛]o邮[jⅨ〔01 ‖3〔X〔『W‖印ta趴Sα‖‖5Z21t棚1困钢[5「p[丁At出Ix

2o20ˉ03-2912:24:19’289-I‖「0: detaj1data{,ur1,: ’http5://spa2。5〔mpe。〔e∏teI/det3j1/Ⅻγz肌肋Ⅸγx肋]od

‖[j贝〔O1‖3〔xcⅣγ‖50t■趴5"h5Z21t洲1∏记仙l5「plⅧt即1x,’ 0∏a爬! : ,筋王别姬ˉ「are们e11帅〔o∩〔ubj∩e!’ !〔ategorie50 $ [倒份!’!伐竹0]’ 0〔oγeⅢ0 ; !https://凹.贬jtua∏.∩et/咖γje/〔e4da]eO3e6S5bSb88ed〕1b5〔d7896 〔十62472。jpg0q640L6“hˉ1e=1〔,’ !5〔ore,: ,9。5°’ 0drm己‘: !彤片份一出《疽王别烃》的京戏,伞扯出三个人之间

一段随叶代风云变幻的父恨份仇°段′J、咎(张半哎饰)与租煤衣(张■荣饰)足一对打′』、一起长大的斤几弟,两人 一个演生,一个饰旦,一向肛含天衣几违’尤其一出《E王别迁》,史足吞沟京成’为此,两人约定合济一求于《罚 王n|姬》。但两人对戏剧与人生关爪的理肝有本Ⅲ不周,段′‖、秘潭知戏非人生,程垛衣川是人戏不分.段′』、楼在认为



第7章

JavaScript动态演 染页面爬取

‖3〔x〔Ⅳγ‖SOt冰∧50‖h5Ⅲ21tb‖1Ⅷe川朴q[5「Pt『∧tb‖Iy

202oˉ03_2912:2』:19’291ˉ I‖「0: 5cIapi∩ghttp5://5pa2.5crape.ce∩ter/detaj1/Z刚z‖〔‖oZXγxⅧ〕od‖[j巩〔o1‖3〔x〔Ⅳv ‖Sota趴50}|∩5Z21tb‖1『∏e‖‖q[5「p[丁∧tb"Iγ

2o卫0ˉ03ˉ2912:24;21’524ˉ I‖「O: det己j1data{!ur10 : ,∩ttp5://5p日2.5cIape°〔e∩ter/deta11/Z‖γ2砸‖oZXγx肌〕0d

‖[jⅨ〔O1‖3〔xcⅣγ‖50taM5O‖h5Z21tb‖1陋朋q[5「pl『Atb‖Iy|’ 0∩a『∏e! : 0这个杀于不太冷ˉ L色o∩"」 |categorje5|:

[ 』剧怕|’ 0动作‖’ |犯罪‖]’ !〔over|: ‖http5://p1.|‖e1tua∩.∩et/|胆γ1e/6bea9a+452qd十bdOb668ea日7e187c3d十767253.jpg

刨6』w6“h1e1〔! ’ ‖5〔ore‖ : 』9.5! ’ ′dm『∏a| : 0里晶(让.岔诺饰)足名孤独的职业杀于,受人瓜佣·一犬’邻 居家′」`姑娘马蒂尔德(纳塔丽,波特员饰)哦开他的房门’要求在他那里暂避杀身之祸·原米邻居家的主人足T方缉 豢蛆的眼线’只因贪污了一小包#品而迫恶警(加里.奥他Ⅲ饰)杀客全家的惩罚。 马蒂尔捻得到里品的留救’伞 免于难,并留在里品那里°里品教′」、士孩使枪,她教里品法丈,两人关系日趋亲密’相处融洽° 女孩想豺去报仇, 反倒枚杯,里品及叶赶至‖|,将女孩救回°混杂封哀怨情仇的正邪之战渐次升级,史大的冲突在所难兑……|}

这样我们即得到了详情页的数据° 5.数据存储

最后,像之前那样添加_个存储数据的方法。为了方便’这里还是将数据保存为JSON文件’实 现代码如下: +ro‖os1川poItⅦa长edjr5 +roⅦO5·pat∩mporteXj5t5 R[50[丁50IR= 0re5u1t5|

ex15t5(R[50[『5DIR)oIⅦa代ed1rs〈R[50L『50IR) de千5aγedata(data):

∩a川e=data.get( ′∩a"e‖) dataˉpat∩≡f‖{R[50∏50IR}/{∩a爬}.j5o∩| j5o∩.duⅧp(data」 ope∩(data—pat∩’W’ e∩〔od1∩g=|ut+ˉ80 )’ e∩5urea5cU=「a15e’ j∩de∩t=2)

这里的原理和实现方式与25节是完全相同的’不再赘述。 最后在川aj∩方法中添加对5aγedata方法的调用即可。

6.设置无头模式 如果觉得爬取过程中弹出测览器会造成干扰,可以开启Chrome的无头模式’这样不仅解决了干

扰问题,爬取速度也会得到进_步提升。只需要对代码做如下修改即可开启无头模式: optjo∩5二"ebdI1ver.〔hro川e0ptio∩5() optjo∩5.日dd—argu|∏e∩t(0ˉˉ‖ead1e55!) brow5er≡"ebdr1γeI.〔们IoⅧe(opt1o∩5=opt1o∩5)

这里通过〔hro‖e0∩t1o∩5对象添加了一head1e55参数,然后用〔hro"e0ptjo∩5对ChIome进行了初 始化。之后重新运行代码’Chrome测览器就不会弹出来了,爬取结果也和之前完全-样° 7.总结

本节’我们通过一个案例了解了Selenium的适用场景’并实现了页面爬取’相信能让大家进_步 掌握Selenium的使用方法°

本节代码参见: https://gjthuhcom/Python3WebSpjder/ScrapeSpa2。

7.6

尸yppetee『爬取实战 在7.3节,我们了解了Pyppeteer的基本用法’和Selenlum相比’它确实有很多方便之处。

本节我们就使用PyPPeteer改写75节的爬取实现’来体会Pyppeteer和Selenium之间的不同,同 时加强对Pyppeteer的理解和掌握。

■]‖ˉ·」』■■□■□】■■】】■■■』■‖□■●】‖|』‖●】‖】□∏|·】‖‖』·|●‖』■`■口‖。■|‖|」‖」■]』□■]|··』·旬‖‖□■{」●|」门·]』∩·■可‖』■]{刮□·』□■‖』日(|】■Ⅵ』]|‖·□■{|‖□】||

该成京立业之时迎娶了名妓菊仙(巩俐饰) ’致使程蝶农认定菊仙是可耻的累三者,使段′」、楼做了叛徒, 自此,三 人围绕一出《霸王别姬》生出的金恨悄仇战开始随豺时代风云的变迁不断什级,终酿成悲剧。 0} 2020ˉo3ˉ2912:24;19’291 ˉ I‖「0g getdetai1ur1‖ttp5://5p日2.5〔Ⅲape.〔e∩ter/detai1/Z‖γ乙M‖0ⅢXγxⅧ〕od‖[jⅫ01

‖■·勺」】‖』=■■』■‖」|‖|乙■巴

276

战 实 γ γ ∑



取 爬

巳 已













7.6

↑.爬取目标

本节要爬取的目标和75节的一样,还是电影网站https;//spa2scrape.center/° 2.本节工作

本节要完成的工作也和75节的~样°

□遍历每-页列表页’获取每部电影详情页的URL。

■=厂■口■『「|卜巴■■「■庐『▲■■■△■‖巳=■∏『「=■尸‖■■■【|【尸■■厉|>△◆厂〖■厂▲尸‖}|△■厂△■尸巴■厂‖}◆『‖|■『囚●[【▲■■》’|■「‖凹_■厅°|‖|■■『巴◆『「|■■‖【■『「‖‖□■尸『‖



□爬取每部电影的详情页’提取电影的名称`评分`类别`封面、简介等信息° □将爬取的数据保存为JSON文件°

3.准备工作

在开始之前’需要做好如下准备工作°

□安装好Python (最低版本为36),并能成功运行Python程序° □安装好Pyppeteer并能成功运行示例。 其他的测览器`驱动配置此处就不需要了’这也是比Selenium更方便的地方° 4.爬取列表页

依然是先做-些准备工作: j∏port1ogg1∩g 1oggi∩g.ba51〔〔o∩千j8(1eγe1≡1oggj∩8.I‖「0’ 「or‖at≡|%(日5〔t1|∏e)5 ˉ兜(1eγe1∩a∩e)s:%(Ⅶe55age)s』)

I‖0[X0RL≡ |bttp5://5pa2.5〔mpe.〔e∩ter/page/{page}‖ 丁I‖[00丁=10 丁0丁∧Lp∧C[=1O

‖I‖刚‖ID丁‖’"I‖刚‖[IC‖丁=1366’ 768 ‖[∧DL[55=「a1Se

这里的大多数配置和75节是—样的’也导人了一些必要的包,定义了日志配置和几个变量’不 过这甲还额外定义了测览器窗口的宽和高’此处是l366×768,大家也可以随意指定适合自己屏幕的

宽高。另外’这里定义了-个变量‖[∧D[[55’用来指定是否启用Pyppeteer的无头模式,如果其值为 「a15e’那么在启动Pyppeteer的时候会弹出一个Chromium测览器窗口。

接着,我们再定义-个初始化Pyppe忱er的方法’其中包括启动Pyppeteer、新建一个页面选项卡 和设置窗口大小等操作。代码实现如下: +IoⅧpyppeteerjⅧport1au∩〔∩ brow5er’ t己b=‖o∩e’ ‖o∏e

a5y∩〔de「 i∩jt(): g1oba1 bro仍5er’ tab brOW5er I=awajt1au∩c∩(head1e55=‖[A0[[55’ arg5=[ !ˉˉd15ab1eˉ1∩+obaI5! ’ +0ˉˉWj∩dOW-5jZe={‖I‖"‖‖I0丁‖}』{‖I‖0侧刊[IC‖丁}|]) tab= awaitbrow5er·∩ewpa8e() aⅣajt

tab。5etγiewport({Wjdth! : ‖I‖00‖‖I0丁日’ ‖‖ejg∩t0 : ‖I‖DO"‖[I6‖丁})

这里先声明了brow5er变量和tab变量’前者代表Pyppeteer所用的测览器对象,后者代表新建的 页面选项卡。这两项都被设置为了全局变量’能够方便其他方法调用。

然后定义了_个1∩it方法,该方法中调用了.Pyppeteer的1au∩ch方法’并且给head1e55参数传

人‖[∧0[[55’将Pyppeteer设置为非无头模式’还通过args参数指定了隐藏提示条和设置了测览器窗 口的宽高。





第7章JavaScript动态渔染页面爬取

278

接下来’我们像之前_样,定义_个通用的爬取方法:



十IO∏) pγppeteer·errOr5mpOrt丁i贮Out[rIOI



己5y∩〔de十5〔mpe_p己ge(ur1′ 5e1e〔tor): 1og8j∩g.1∏十o(!5〔Iapi∩g%5! ’ uI1〉 trγ:

a"aittab。goto(l」r1) awaittab.wajt「orSe1ector(5e1e〔toI’optio∩5≡{



0tj贬Oot『 : 丁I‖[α∏*10OO )) 乙J

eX〔ept『1『眶Out[rrOr:

1og8i∩g。error(0erroIo〔〔urred"hi1e5〔Iap1∩g%5! ’ ur1’ e)《ci∩十o=「n』e)

使用goto方法调用此URL即可访问对应页面;另一个是5e1ectoI,即等待擅染出的节点对应的CSS 选择器。此外,我们调用了"ajt「or5e1ectoI方法,传人5e1ectoI,并通过optio∩5指定了最长等待 时间°

运行时,会首先访问传入的URL对应的页面,然后等待某个和选择器匹配的节点加载出来,最 长等待l0秒。如果‖0秒内加载出来’就接着往下执行’否则抛出丁1"eout[rror异常,并输出错误日志° 下面实现爬取列表页的方法:

这里定义了_个5〔rape—1∩dex方法’它接收参数page,代表要爬取的页面的页码。方法中’我们 首先通过I‖0[X0R[构造出了列表页的URL’然后调用5〔mpeˉpage方法并将构造出的URL传人其中’ 同时传人选择器°

这里我们传入的选择器是.1te" .∩a"e’是列表页中电影的名称’意味着电影名称加载出来就代 表页面加载成功了,如图7ˉ42所示° b

·■■」■‖』]叫‖口口|■■|』】‖」■■』·】】■Ⅷ〗]■可』■引‖』□】●■‖‖

a5y∩cde「5〔rape-j∩de×(Pa8e); ur1=I‖0[X0R[。十omat(p己ge=page) a"己itS〔rape-page(Or1’ 0 .ite「∏ .∏a爬‖)



‖■□』‖■■∏|·‖■]|」■可』■■‖』■□』■】】■γ』■司

这里定义了_个5〔rapeˉpage方法,它接收两个参数:一个是uI1’代表要爬取的页面的URL,

●|

■★白可●

回s.ap。

……==ˉ—_—ˉ可 膨 ■√= 囊鳃、梅 锨隙p

9.5 ●白●●台

…n·中■甘尼′↑∏分钟 0…Cγˉ孙上蚀



■‖

中§

x

.

…≡……≡…≡口

霹蔫≡冒≡直—令.°

M樱●≈



●■伪…■一c1≡≈尸T‖≡百T

…=…

■…色…尸●B邑…~

F审=■t扒≡

…医……

电↑一

唾→凹■巳1 …

旦÷■●≈

◆钳』■兰一『已=龟乙℃…【●‖<●‖=】0■1咖1舒…钮占● 7组乙●0ˉr…面寸`…【R■≥t-□

—『≡≡一_………

v■凹…一q矿0=它l=■



■山勺●T≥℃-岭→…T



·》■

叮<m巴■●….今 p也T画≥T■】→【?=■→●‖≡=…m酝

』■】■■■||当■

■■■■l ˉ

_≡

…~

=…

…≡

噎…

=……~′—

…—…割≡…防

=…

譬≈呈≡骂=

卜●1U壬△= ≡呈一≈]亡…●乌■1乙1●0…k=N■lm0四巴■1…1=●…←■≈m→

-=-=-= d

=←→▲…■00

◆≤】■●∏一c…T巨~…、■`′■D◆=

=〃樊尸≡





‘■占…■→1四《

屯≈口……~●≡^●匈~$·■匈~…≡≡■…-凸≡=哟▲●…■p0=

■……∏叮t=

雨—钻=

图7斗2加载成功的页面 (



||『‖||尸

△伊「‖■尸』》●『|■厂『〖■尸■■‖△尸



76Pyppeteer爬取实战

279

我们再定义_个解析列表页的方法’用来提取每部电影的详情页URL,方法定义如下: 己5y∩cde+parBe~j∩dex():

Ietur∩awaitt3b.querySe1ector∧11〔γa1(0 .iteⅦ .∩a们e! ’ 0∩ode5=〉∩ode5."己p(∩ode=〉∩ode。hre+)!)

这里我们调用了query5e1ector∧11[γa1方法,它接收两个参数:_个是5e1e〔tor’代表选择器; 另一个是Page「u∩〔t1o∩,代表要执行的JavaSc∏pt方法°这个方法的作用是找出和选择器匹配的节点, 然后根据page「u∩〔t1o门定义的逻辑从这些节点中抽取出对应的结果并返回°

我们给参数5e1e〔tor传人了电影名称。由于和选择器相匹配的节点有多个,所以给page「u∩ctjo∩ 参数输人的JavaSmpt方法就是∩ode5’其返回值是调用‖ap方法得到∩ode,然后调用∩ode的hre+属

可』

性得到的超链接°这样’ querγ5e1ector∧11[γa1的返回结果就是当前列表页中所有电影的详情页URL 组成的列表。

巴■尸‖△∏■

接下来,我们串联调用刚实现的几个方法’代码如下: mporta5y∩〔1o

●‖■尸

己5y∩〔de十∏ai∩(): aWa1t1∩1t() try:

十orpage1∩m∩ge(1’『0丁∧儿P∧C[+1): aWajt5〔mpe-i∩dex(page) detaj1ur15=awajtparse-i∩dex(〉 1oggi∩g°1∩十o(|detaj1ur15‰0 ’ det3j1ur1s) 十1∩a11γ: 己"a1tbro"5er.c1o5e()





》卜‖

i十

∩a爬≡‖ Ⅷai∩

0:

35y∩〔jo.get-eγe∩t—1oop()。Iu∩ˉu∏t11ˉ〔o『∏p1ete(爬i∩()〉

■尸‖巴■厂

这里定义了‖ai∩方法’其中首先调用j∩1t方法’然后遍历所有页码,调用5〔mpeˉ1∩dex方法爬

取了每-页列表页,接着调用par5e=j∩dex方法’从列表页中提取了详情页的每个URL’最后输出° 运行结果如下:

巴■▲■■『■尸【■『『■尸▲β■|■【■『

2o2oˉ叫ˉ0813;5小28’879ˉ I‖「0吕 scmpi∏8http5://5pa2·5cmpe°〔e∩ter/p己ge/1 2o2Oˉ0qˉ0813:54:31’』11 ˉ I‖「O: det己i1ur15 [!https://5p己2.5〔r3pe.〔e∩ter/det己11/Z‖γz‖〔‖oZⅫx肋〕od"[jⅦ〔01 ‖3c×cWⅧ印t己趴5侧们5z21tb|‖1雁恤Ls「p∏∧tb‖Ix|’…’

|∩ttp5://5pa2·5〔mpe.〔e∏ter/detaj1/Z‖γ∑顺阳ZXγx阳]Od"[jⅦ〔01‖3〔x〔Ⅳγ‖5Ota趴5侧h5Z21tb‖1∏把朋q15「P叮∧t阳I5,』 ,∩ttps://5pa2.5〔rape.〔e∩ter/detaj1/酣γz№‖0ZXγ“〕0删[jⅨ〔01‖3〔x〔ⅣⅧ5Ota灿5α|h5Z21t州1‖圃晌l5「P∏∧t州I棚=!] 2020ˉ凹ˉ0813:54:31’411ˉI‖『0: 5〔mpj∩ghttp5://5p己∑。5〔mpe·〔e∩teⅢ/pa8e/2

由于内容较多,这里省略了部分内容°可以看到’每_次的返回结果都是从当前列表页中提取出 的所有详情页URL组成的列表。下一步就可以凭着这些URL爬取详情页了° 5.爬取详情页

现在要爬取每-个详情页’先定义一个爬取详情页的方法,代码如下:



a5y∏〔de十5〔Ⅲape≡detai1(uⅢ1): a"ajt5〔rape-p己ge〈ur1’ !h2,)

这个方法非常简单,直接调用5Crape=page方法,传人详情页URL和选择器即可,这里的选择 器我们直接传人了h2,代表电影名称。运行顺利的话,Pyppeteer已经成功加载出详情页了’如图7ˉ43 ■』』

巴■■『巴■■■「〔=■≡【尸血■「【『『





所示。





第7章JavaScript动态演染页面/爬取

280

」司■]‖」■]‖{■■司|||』□』■■■

回sc『… ‖匡二二≡]函m怕N

9.5 ◆亡●●●

….铂■由‖『『Ow v嗣m.狮上政

‖‖



酚★力蓟●

‖■■

{■佰■介 ■片■=出t■爹臼迅〗鳃Ⅷ宙.●■出三十入之叶…潭

-逛■■旧酗耻0n键¥履邯》愿m夜(狂■甲询》分闪Ⅶ ■0

ˉ~ .一……=…

iB田≡= —



←…



;…憾雄化蘸



:琴″h=

心↑ x

噎…L=…~耳



…‖….■p

●…【刨;喊·‖

●≡乓◎9弘卢■√…『鲤>



◆…乙叮串

●·山v…二 .二~色l■●■℃卜亩】4=~≡赵●■



酝…▲mi〗

。●+■《 …U心……=勺净…□

F■k■■←7TⅢ兰由吟◆尸==↑↓.■四宁■■殴▲ 守叼仰…厅07酗p凹■四●=Ql≤讨P 8↑■勺「■

=…触■″【●…『

m《

■妇汕≈≥村…G■≡-诬…`■0≤■卜必●!吨●l≡』

四四叮F矾…卢 ■割m′ ■…占

■m■…幻■∏=【l二二←ˉ·1<■门』O≡f■…

■●一 7山■【四P■●…≡畸 守图】●●?≡…≈f呻尸幻…●‖=净 G吕……

■■■■■

■私…■……●·~ …《∩心■■A=舌 ●o产, …o田叫《』……f华$ ■帜●叼▲…t…β

心?唾0…0n■↓

●山甘…→7∏Ⅻ■ⅧU田p◇·●q→帕1■b=Pp·l=C■1<■申■0≤叭司~冒←■〃O】■′



■蕾m0…∏…■【凶咎巧砷甘你●`Q`…‖■“●…l→=■叭妇l→1r

●<■田…●胡铲■0…=≡□谷_-←→一!■…-…『』■…1·f■≠=…←巾巴 =吉■■…P=兰■0■=~ …≡≡旦刃…l…1叮E

$‖喊蝇



〖●』≈‖ 〖…』{↓



·…■画甩…田l■■甲◇■四7矿 ■空→

●#

●0四』 ″睡

●:≡■=『…(

…=0fyl●≈r

…南~_『▲审凸一Va◆…~≡—■■=ˉ……←≈尸…=_

□‖·〗

p■曲■==≈Ⅷi■■·产山…≈□扣l产

醇惩…0≈●8M

■0

√●

●Ud‖v●←7n■审『■■7m宁『啤宇~√■D

图7气43加载成功的详情页

‖」

下_步就是提取详情页里的信息。定义_个提取详情信息的方法: 日5γ∩〔de十paI5eˉdetai1(): ur1二t3b.ur1

∩ode.j∩∩er「ext)0)

·司‖■可

cover=己w己jttab.querySe1e〔tor[va1( ‖ .coγer0 ’ |∩ode=〉∩ode.5r〔‖) 5core二awajttab.query5e1e〔toI[γa1(0 .5core! ’ 0∩ode=〉∩ode.j∩∩er丁ext0) dra‖∏a≡日w日itt3b.querγ5e1e〔toI[va1(0 .dra川ap‖’ ‖∩ode=〉∩ode.1∩∩er丁ext0)

‖‖

∩a川e≡a"aittab.query5e1e〔toI[va1( 0∩2, ’ |∩ode=〉∩ode.j∩∩eI「e〉(t′) 〔ategor1es=a训己itt己b.querγ5e1e〔tor∧11[γa1( ! .categor1e5butto∩5p己∩! ′ `∩ode5=〉∩ode5Ⅷap(∩ode=〉

retur∏{ !0r1|: l」I1’ ∩a∏e : ∩己"e』

〔Oγer ; 〔OVeI’

5〔oIe :

5〔ore’

0dr日Ⅷa|: dr己川a



这里我们定义了paI5eˉdeta11方法’提取了URL`名称`类别`封面、分数`简介等内容° □URL:直接调用tab对象的ur1属性即可获取当前页面的URL°

□名称:由于名称只涉及-个节点’因此我们调用的是query5e1e〔tor[γa1方法°给这个方法传 人的第_个参数值是∩2,代表根据电影名称提取对应的节点;对于第二个参数Page「U∩CtiO∩’ 这里调用了∩ode的1∩∩er「ext属性,提取了文本值’即电影名称。

□类别:类别有多个,因此调用querγ5e1e〔tor∧11[va1方法°其对应的CSS选择器是.categorje5 butto∩5pa∩’可以选中多个类别节点;第二个参数page「u∩ct1o∩和之前提取详情页URL时类 似,使用∩ode5方法,然后调用"ap方法提取∩ode的j∩∩eI丁ext就得到了所有的电影类别。 □封面:同样’可以使用CSS选择器.〔oγer直接获取封面对应的节点,不同之处是封面的URL 对应5rC属性,所以这里提取5IC属性。

□分数:使用CSS选择器.5coIe直接获取分数对应的节点’然后调用∩ode的j∩∩er丁ext属性’

提取文本值。

□简介:使用CSS选择器.dIa"ap直接获取简介对应的节点,然后调用∩ode的1∩∩er「ext属性, 提取文本值°

‖°(‖□】∏‖』‖‖」‖■口Ⅱ日‖《』□‖‖‖』·引』∏‖■■』』‖可‖·勺·‖

‖〔己tegor1e5! :〔ategories’

□■』]』



76Pyppeteer爬取实战





28l

最后’将提取结果汇总成_个字典并返回°

接下来’在"a1∩方法里添加对5〔rape_detaj1方法和paI5edeta11方法的调用。川日1∩方法改写如下: 35γ∩〔de千Ⅷ己i∩(): a佣3it 1∩1t() trγ: 夕

+orpagei∩ra∩ge(1’丫0丁∧[PM[+1): a"ait 5Cmpeˉi∩dex(page) detaj1uI15=aw己1tparSej∩dex() +ordeta11ur1i∩detaj1ur15:

aWa1t 5〔rapeˉdet己j1(det311ur1) detai1data=己w日jtp3I5edet己11() 1oggi∩g.i∩+o(|d3ta%5』’ detai1data)

「j∩311y: awa1tbrow5er.c1o5e()

重新运行,结果如下: 2O20ˉO4-0814:12;39’564ˉ I‖「0: 5cIapj∩g‖ttp5://5pa2°5〔I己p巳〔e∩ter/pa8e/1 2O20ˉ04ˉ081q:12;42’935 ˉ I‖「0: s〔rapi∩g∩ttp5://5pa2.5crape.〔e∩teI/deta11/Z‖γz‖〔‖0ZXγxⅧ〕0d‖[jⅨ〔01‖3〔x〔 Ⅳγ‖5ot冰A50}{∩5Z21tb‖1『∏eⅧql5「p[丁∧tb‖Ix 202OˉOd~081』:12:45」781 ˉ I‖「0: d己t己{0uI10 : |∩ttp5://5p日2.5〔mpe。〔e∩ter/deta11/Z‖γZ‖〔‖0ZXγxⅧ〕Od‖[jN〔O1 ‖3〔×〔Ⅳγ‖5ot冰∧50‖∩5I21tb‖1『∏e朋q[5「p[丁∧tb‖Ix0 ’ !∩a『∏e| : [霸王月||姬ˉ「arewe11"γ〔o∩〔‖bi∩e|’ !〔ategor1e5‖ :

[ 0剧悄|’ 』发怕‖]′ 0〔oγer0 : ‖‖ttp5://pO。Ⅷeitua∩.∩et/帅vje/ce4da3eO3e655b5b88ed31b5cd7896c+62472.jpg刚6qw 6叫h1e1〔{ ’ !5〔ore0 ; |9.5! ’ !dra阳! ; |删片借一出《霸王月||姬》的京戏,伞扯出三个人之间一段随时代风云变

幻的聂』|【悄仇°段小楼(张丰毅饰)与程蝶衣(张国荣饰〉是一对打小一起长大的师兄弟,两人一个演生,一个饰

旦,一向配合犬衣无缝,尤其一出《霸王月|」姬》 ,史是吞满京城, 为此,两人约定合演一絮子《霸王别姬》。但两人 对戏乃||与人生关系的理解有本盾不同’段′」`楼深我口戏非人生’程蝶衣则是人戏不分°段′|`楼在认为该成家立业之时 迎娶了名妓菊仙(巩俐饰) ’玫使租蝶衣认定菊仙是可耻的第三者’使段′』、楼做了叛徒’ 自此’三人围绕一出《霸 王】||姬》生出的爱恨悄仇战开始随巷时代风云的变迁不断什级,终酿成悲剧°|}

2O20ˉ04ˉ081q;12;q5’782 ˉ I‖「0: 5〔Iap1∩g∩ttp5://sp己2.scrape.〔e∩ter/detai1/Z刚∑‖〔‖0ZXγxⅧ〕0d‖[j偶〔o1‖3c×c Ⅳγ‖50t日《AS叫h5Z21tb"1爬‖"q[5「p∏∧tb‖Iy

可以看到’这甩首先爬取列表页’然后提取详情页URL,接着爬取详情页’提取出我们想要的电 影信息’-个详情页爬完再接着爬取下_个°这样所有详情页就都被我们爬取下来了° 6.数据存储

和75节一样,这里也定义_个数据存储方法°为了方便’还是将爬取下来的数据保存为JSON文 件,实现如下: j朋portj5o∩ fro『『‖ o5mport刚冰edjr5 +ro"o5。p己thi们portexi5ts R[50[『SDIR≡ |re5u1t5‖

exi5t5(R[5U∏501R) or"3kedjr5(R[50[丁5DI【)

aSy∩Cde「5aγedata(dat己): ∩a‖e=dat己.get(0∩a爬‖) dataˉpath=+0{R[50[丁5DIR}/{∩aⅧe}.j5o∩

j5o∩.d0∩p(data’ ope∩(dat己一patb’ ‖"0 ’ e∩〔odj∩8=|ut千ˉ8‖)’ e∩5uIea5〔ii=「a15e’ 1∩de∩t≡2)

泣军的实现原理和之前完全相同’但由于Pyppeteer是异步调用’所以需要在5aγeˉdata方法的前 面加上a5γ∩〔关键字°

最后’在们aj∩方法里添加对5日γedata方法的调用。 7.问题排查

在代码运行过程中’可能会由于Pyppeteer本身实现方面出问题,因此在连续运行20秒之后控制 台输出如下错误内容:

pγppeteer。error5.‖et"oI促[rror: protoco1[rroI (冈u∩tme.eva1uate): Se551o∩c1o5ed. ‖ost11|《e1ythePage∩己§ bee∩C1o5ed

厅-

第7章JavaScrjpt动态渔染页面爬取

其原因是Pyppcteer内部使用了WebSocket’如果WebSocket客户端发送ping信号20秒之后仍未 收到pong应答,就会中断连接°

问题的解决方法和详情描述见https://github.com/mlyakogl/pyppeteer/jssues/l78’此时我们可以通过 修改Pyppetecr源代码来解决这个问题’对应的代码修改见https://github.com/miyakogi/pyppeteer/ pul‖/l60/files,即给〔o∩∩e〔t方法添加pj∩gˉj∩terγa1=‖o∩e和pi∩g-t1们eout=‖o∩e这两个参数° 另外,也可以复写_下〔o∩∩e〔t方法的实现’其解决方案同样可以在ht印s://githuhcom/miyakogj/

pyppeteer/pull/l60中找到’例如patc∩-pyppeteer的定义。 8.无头模式

最后,如果代码能稳定运行了,可以将其改为无头模式,只要将‖[∧0[[55参数值修改为丁rue即可: ‖[∧D儿[55=『rue

这样在运行的时候就不会弹出测览器窗口了° 9总结

本节我们通过实例讲解了使用Pyppeteer爬取_个完整网站的过程’相信大家会进—步掌握 Pyppeteer的使用°

本节代码参见: https:〃githuhcom/Python3WebSpide∏ScrapeSpa2。

7.7CSS位置偏移反爬案例分析与爬取实战 我们学习了Selenium、Pyppeteer等工具体会了它们的强大,但千万别以为这些工具就是万能的, 不容易爬取的数据依然存在,例如网页利用CSS控制文字的偏移位置’或者通过—些特殊的方式隐藏 关键信息’都有可能对数据爬取造成干扰°

本节先了解CSS位置偏移反爬虫的_些解决方案°

↑.案例

先介绍一个案例网址htq〕s:〃antjspider3.scrapecente∏’贞面如图7ˉ44所示° ●

■≡,≡ -——_-





=Q 0■■■





图7ˉ“书籍网站

酞出…ˉˉ_■■妻





Q

蛆≡_■■=



…瘫…浑





_ˉ 激ˉ—幸

0

矗包匹吁·虫





′″唱≈=ˉ



|』{|{=一∏{嚼感"嚼一_~

■』←

=■}



ˉ≡—喻蹈二



■乙●尊…

■=删=~_睫_斗川…■≡ˉ

同≈…





谷●

■■】‖]』‖|二■】‖」』□‖』■■□■□‖』■‖』‖‖□■|‖||』·■■‖□勺】』■■■‖』日』】■≡■■』」‖』■■】『‖·■■■■‖■〗■可】■]‖■二■■〗‖‖‖·■■可·二■可|■|△■司邹■■]■■】‖■■Ⅵ』■■·■=可

282

7.7

CSS位置偏移反爬案例分析与爬取实战

283

乍-看似乎也没什么特别之处,但如果真用Se|enlum等工具爬取和提取数据坑就立马显现出来 了’不妨试_试。

先尝试用Selenlum获取首页的页面源代码’并解析每个标题的内容: 十ro们5e1e∩1Ⅶ1川poItwebdrjver 十roⅦpyqueryiⅦportpyQuerya5pq fro阳5e1e∩1u们·"ebdr1ver。〔o∏〗∏]o∩。by加poIt8y +r咖5e1e∩iu∏°webdriγer。5upportmportexpectedco∩ditjo∩5a5[〔 十mⅦ5e1e∩iu们.Ⅶebdr1ver。5upport·wa1t加port‖ebDr1γer‖ajt







「 b

brow5eI=训ebdriγer.〔们ro『∏e() brow5er.8et(‖http5://a∩tj5pider3.5〔mpe.≤e∩ter/|) ‖ebDriver‖ajt(bro"Ser’ 10) 。u∩t11([〔.pre5e∩〔eo「311e1e爬∩ts1ocated((8γ。〔555[l[〔丁0R’|°jte‖,))) bt们1=bro训5eI。page_5our〔e do〔=Pq(‖t们1) ∩a‖e5=do〔(′.jte厕 .∩a"e‖) +or∩a|∏ei∩∩己Ⅶe5·1te‖5(): pri∩t(∩aⅦe.text()) brow5eI.〔1o5e()







这里我们使用Selenium打开Chrome测览器’然后使用‖eb0rjγer‖a1t对象的u∩t11方法指定了

等待加载的内容,确保首页上每本书的信息都可以加载出来°之后输出页面源代码’使用pyqueD′将 标题中的纯文本解析出来,_切看起来似乎非常正常对不对?













0













0



『 p

然而运行结果是这样的: ‖O∩der

风滑白家 结忆上册下宠 (法终老易) 的 为己 (册全) 士知二

那些年, 我们一起追的女孩 全二 ( ) 城倾我册非 些那儿朝亨明 的我书′怠和笑你 一第王小波卷集全 汗动然心 龙枪腾平史(全3册) 枪三册传全 (奇龙) 黎街明之 认示其理启学决回|u及 银河帝国2:基地与帝国 ; 帝基银国河地 肝材′」、ˉ丈四下级学教语午全 越界宫论(第3卷)

结果中很多标题的文字顺序是乱的,例如《明朝那些事儿》对应的输出结果是“些那儿朝事明’,’ 这是怎么回事?



2.排查



我们去测览器里面研究—下源代码’如图7ˉ45所示。





p

















0

∏{

f良田=…… ≈■■■

■■坠巴

=司……

喇回

■年阐月

钱…买忘书

王小沮叁簿■=■

■七夕

王‘超廷h■垫m■力

□■〗」||当_■■|□日{』■■』‖‖‖』■■■‖|‖·可■■■■■∏‖|‖‖』■■=■|·‖‖





面 页



■■』『

曹一





β

●■■■







α γ



γ

β ∑

尺…











…γ=…

PT…∏…丁…山■←~=U叮尸可…≈哟

v≤1γ·■t≥忻7门●汀●0c[■■$订庐·■斌■厂t…■1=…> ;8出7◎7e

■<1V0■k>v=7门▲刀■fc℃Z●也缚G【→“l●I■〔□卜2△鹏汐

‖■■可‖■■□‖凸■Ⅵ



<O障∩“t●←什=77】■刀●

■缄`0…铲;

$w`…卸uTt: 1…6■



■9加∏四t●=泞←γ7凹刀·

■立■cm广?

忍tvl萨凰℃ft!…βm



≤6p●札由t●=γ←7↑】●77■

9工Pc们Ⅳ■

Str炉■【·?t:蜘xβ■】

雹5m『l“r●≈→γ↑】D刀·

…”酗广Ⅸ

■w〖″播`·◆t;如又广≥



么■p印血t●=砂=7?1●77·

O7…广◆

々■p●n血t■=吵=γ〔1●77O

■■■c沁厂’ 0wl″■〖Q7〔:呻况;·】

■了γl←凹1G?t;n严5牟〗

·』

_

乙■■|』口司■■】可

叮/低3H </●≥

图7ˉ45书籍网站的首页源代码

的标题内容乱序就不足为怪了。

源代码中的文字本身是乱的’那为什么在网页上看到的标题是正确的?这是因为网页本身利用 CSS控制了文字的偏移位置,什么意思呢?观察下源代码:

■‖‖」■■司‖|

〈旧dataˉγ-7十1a77ef="" C1a55="阳ˉbˉ5∏∩日川e"〉

5ty1e=||1e+t; 5ty1e=||1e十t; 5tγ1e≡"1e什: 5tγ1e二| |1e什: 5tγ1e="1e什: 5ty1e=| 『1e什:

‖门|《到‖■』||』□■」‖‖■

<5pa∩d日taˉγˉ7十1a77e十="|| 〔1a55≡||c∩aI|| <5pa∩dat己ˉγˉ7十1己77e「≡"" 〔1日55=00〔h己I|| 〈5p3∩dat己ˉvˉ7千1a77e十="| | c1a55="c∩日r" 〈5pa∩dat己ˉvˉ7{1a77e十=00"〔1a55="〔har" 〈5pa∩dataˉγˉ7+1日77e+二"|『 〔1a5s="〔∩ar" 〈5p己∩dat己ˉγˉ7于1a77e千=||" c1a5s=||〔∩ar"



』■]』可』·Ⅵ|■■■■]

可以发现,_个字对应-个5Pa∩节点,这个节点本身的顺序就是乱的’所以用pyquery提取出来

16px")朝</5pa∩〉 64px"〉亨</5pa∩〉 48px"〉些〈/5pa∩〉 opx"〉明〈/5pa∩〉 32px| |〉那〈/5pa∩〉 8opx"〉儿</5pa∩〉

〈/∩〕〉

可以发现每个5pa∩节点都有—个5tγ1e属性,表示CSS样式’1e+t的取值各不相同°另外’ 在测览器中观察_下每个5pa∏节点的完整样式’如图7ˉ46所示· …←



0

」■■】|」‖|』■∏‖‖‖||■■勺‖

的偏移位置了’例如1e十t:0px代表不偏移; 1e千t: 16px代表从左边算起向右偏移l6像素,于是节点

|{

可以看到’5pa∩节点还有两个额外的样式’是d15p1aγ: 1∩11∩eˉb1o〔低和po51t1o∩: ab5o1ute’ 后者比较重要’代表绝对定位’设置这个样式后,就可以通过修改1e「t的值控制5pa∩节点在页面中

‖||‖

图7ˉ46 5pa∩节点的完整样式



CSS位置偏移反爬案例分析与爬取实战

7.7

285

就到了右边。 ■尸[|‖‖广‖|■尸『|}■■[『‖|【■『‖||■■厂『‖‖|卜巴■「‖·『『(尸■尸「厂|伊〗■‖》【|■■尸|[尸|●厂||卜「

源代码中’ “明’,字的偏移是0’ “朝”字的偏移是l6像素,“那”字的偏移是32像素,以此类推, 最终标题的视觉效果就变成了“明朝那些事儿”° 3.爬取

了解了基本原理后’我们就可以有的放矢了°这里只需要获取每个5Pa∩节点的5ty1e属性’提取 出偏移值,然后排序就可以得到最终结果了。 先实现基本的提取方法: 「IoⅦ5e1e∩ju∏mportwebdr1ver +ro『『| pyq0erγi‖portpyQ0ery日5pq +r咖5e1e∩10Ⅷ.webdriγer·co∏Ⅶo∩.byi呻oItBy 十r咖5e1e∩juⅦ°"ebdr1γer·5upportmportexpectedco∩d1t1o∩5a5[〔 十r刚5e1e∩ju阳°webdrjveI。5upport·w己jtmport‖ebDrjγer"3it 1们portre

de十par5e∩a『∏e(∩a|∏ehtⅧ1): 〔haI5二∩a们e∩tⅧ1(|.C|]ar|) jte"5= [] +OrChari∩〔haI5·ite们5():

「7

1te阳5.appe∩d({ ‖text, : 〔har.text().5triP()’



|1e+t‖ : j∩t(re。5e3r〔h〈』(\d+)px0 ’ c∩ar.attr(05tγ1e‖))°gro(」p(1))

})

1te们5=5orted(1te"5』|(eγ=1a们bd3x: x[|1e代0 ]’ reγer5e=「a1se) retur∩ 0 ‖ .jo1∩([1teⅧ.get(‖text‖)十or1te们i∩ite阳5]) brow5er=训ebdriγer·〔∩ro"e()

bro训5eI.get( 0∩ttp5://a∩ti5pjder3.s〔r己pe.〔e∩teI/!) ‖ebDrjver‖a1t(bro"5er’ 1o)

.u∩tj1([(。pre5e∩ceo十a11e1e爬∩t51ocated(〈By。〔555[[[〔丁O侗’ , 。ite∏]‖)))

ht"1=brow5er°page=5ol」rce

doc=pq(bt∏1) ∩a"e5=dOC(|.iteⅧ ·∩a‖e′)

.

十or∩a『∏eht『∏1j∩∩aⅧe5。jte『∏5():

∩a们e=par5e∩己爬(∩日Ⅶeh加1) pr1∩t(∩己们e) ′

bro佣5er.〔1o5e()

这里我们定义了一个par5e∩a爪e方法’用来解析页面源代码得到最终的标题。 它接收一个参数∩a‖e‖t们1’就是标题的HTML文本,类似这样: 〈h3d己t3ˉγˉ7+1a77e千=""〔1a55="们ˉbˉ5肌∩a『∏e00〉

〈5pa∩dataˉγˉ7十1日77e「="" c1己5s二"char" 5ty1e="1e十t: 16px"〉朝〈/5Pa∩〉

〈sPa∩dataˉ`′ˉ7十1a77e+=』』" c1a5sˉ』』c‖ar』』 5ty1e=||1e什: 64p×卿〉本〈/5pa∩〉

<5pa∩d3ta-γˉ7十1a77e千=""c1a55="〔∩ar" 5ty1e="1e千t8 〈5pa∩dataˉγˉ7千1a77e十=""〔1a55="〔∩ar" 5ty1e="1e什: 〈5pa∩dataˉγ-7十1a77e+=`|"c1a55="c门aI" 5ty1e="1e+t: 〈5pa∩dat己ˉvˉ7+1a77e十=",0 〔1a55="char" 5ty1e="1e+t:

48px"〉些〈/5pa∩> 0Px"〉明</5Pa∩〉 32px"〉那〈/5Pa∩〉 8OPx"〉儿</5Pa∩>

</h3〉

在par5e∩aⅦe方法中’我们首先选取.C|]aI节点,将其赋值为〔∩ar5变量,然后遍历〔∩ar5变 量’其中每个条目各自对应一个5Pa∩节点’其内容类似于: 〈5pa∩dataˉγˉ7千1a77e十=""〔1日55="Char" 5ty1e=‖|1e十t: 16Px||〉朝</5P日∩〉

在遍历的过程中,我们提取了5pa∩节点的文本内容作为字典的text属性’还提取了5ty1e属性 的内容’例如这里提取的是16pX’并用正则表达式提取了其中的数值,这里是16’将其赋值为字典的 1e千t属性。

遍历结束后’1te‖5的结果类似下面这样:



第7章JavaScript动态演染页面爬取

[{!text! : 0朝』′ !1e代0 ; 16}’{{teXt! : !本0 ′ |1e什‖: 6q}’{‖text|: ,些|’ ‖1e十t|: 48}’{‖te×t0 : |明! ’ |1e什0 : 0}’{|teXt! : 0那』’ 01e什|: 〕2}’{!text! : ‖儿!’ 01e什! : 80}]

面对这样的结果’怎么排序呢?直接调用5oIted方法就行,它有两个参数,一个是Rey’用来指 定根据什么排序’这里我们直接使用1aⅧbda表达式提取5pa∩节点的1e+t属性,所以最终结果是根据 1e+t的值排序而得;另一个参数是Ieγer5e’用来指定排序方式,此处将其设置为「a15e,表示从小到 大排序。

排序完的1te们5变成了这样:

最后将其中的teXt值提取出来并拼接,就得到了最终结果。

‖|

[{‖teXt|: 0明‖’ !1e十t! : 0}’{0teXt‖; 0朝‖’ 01e什‖; 16}’{‖teXt‖: 0那0 ’ ‖1e什0 : 32}’{!teXt0 : 0些0 ’ 01e十t|: 48}’{‖te×t‖ 8 ‖本|’ |1e代‖ ; 64}′{|text! : 0儿0 ’ 01e+t0 : 8O}]

|」□■‖·■|■司|·∏|·‖(|||·■||」■■」Ⅵ‖〗|回·|"■□|·■可|】·

286

℃‖|‖·■■

代码的运行结果如下:

|·■·】Ⅲ‖乙·|』司||·Ⅲ■‖日■】纽‖二■司』■

汾白家风 法老的宠忆终结禹(上下册) 士为少回己(企二册) 邪些年,我们一起迫的★孩 非我倾咸(企三册) 明朝那些亨儿 我和你的笑息书 王小波全某累一卷 仟然心动 龙枪传叶(企三册〉 纂明之街

认知心理学及其启示

银河帝国:基地 ′」、学教材全肝ˉ四年级语丈下

再继续排查’会发现有些标题节点内部没有分为一个个5pa∩节点’这些标题内部的文字本身就 有序’如图7ˉ47所示° h

瓣仿



粤 鹤

《 俐

£肆写

圃 !田≈■m

愿』啥

而白肛风

■例′汪===

往→△ …垮 = -~-■→■→ 子●←→◆■■丁■

</砂 填/01吵 9 吕■?r●厂

</q』v>

◆<回』V曲t>←了『№γγ●寸c1a9炉■mttm防t=…●l=『″≥ 目『比↑o淀

■匈』U“t←γ=7?1■77●『仁1“5■嘶,●l辛c●1Q1≤o1=2凸白≥ v



屯“t凸■γ→7↑1□w●?≤l▲69·d0■uT№丁■凹`咽· 〕· p■吐1牢′严 咱/●』U> 台;●7tQ「 #■』●=

图7ˉ47本身就有序的标题

■‖|■」■∏|‖■]■■」】■||』』■|■■]{』■■∏‖·司』=】』■{{■]□|】】〗】】■■】】』■■】□』■■■】‖〗■■■】‖‖·●』】』】■■】■■■■■〗』■】■】』』■■■】

等等’似乎少了几个标题内容中间为什么会出现空余?

‖|』 》■‖||匹■匡【尸『●厂卜|旧‖「■■■「●价



7.8字体反爬案例分析与爬取实战

经过观察和推测,不难发现内部没有5p日∩节点的∩3标题节点都带有—个额外的 经过观察和推测,不难发现内部没有5p日∩节点的∩3标题节点都带有 "ho1e的〔1a55属性,其余标题节点的内部则都分为了-个个5Pa∩节点°

287

取值为「‖a川e

搞清楚问题所在’接下来稍微加判断即可,改写解析方法:

‖「卜■■■

de十Par5e∩a|∏e(∩己『∏eht川1): 们a5"ho1e≡∩3Ⅶe∩t川1(! .who1e!) j十ha5who1e;

retuI∩∩a∩}e们t川1.teXt() e1Se;

■「)■β‖‖卜‖凸

〔harS=∩a川ehtⅦ1(0 .〔帕r!) iteⅧ5= [] 十Or〔hari∩〔‖ar5.iteⅦS(): jte"5.aPpe∩d({ 0text0 : 〔har.text().5trjp()’

‖1e什‖: i∩t(re.5e日r〔h(』(\d+)px0 ’ 〔∩3I·attI(|5ty1e‖))。grouP(1〉) })

ite"5=5orted(1te们5’|(ey=1日爪bdax: x[‖1e什‖]’ rever5e=「a15e) r巳tuI∩| | .joj∩([1te‖.get(|text′)「orite川1∩1te"5]) =■尸■△尸‖■■

运行结果如下: "o∩der

=』

■尸■■厂‖●■

尸》■■■·尸‖■■『‖止■∏伊β■卜「■■■β『|■几

汾白家风 法老的宠忆终结篇(上下册) 士为4回己(全二册) 那些年,我们一起迫的女孩 非我倾威(全三册) 明朝那些亨儿 我和你的笑忘书 王′』、波全纂累一卷 仟然心动 龙枪编午史(全3册) 龙枪传计(企三册) 黎明之街 认知心理学及其启示 银河帝国2:基地与帝国 银汀帝国;基地 小学杖材全肝ˉ四年级语丈下 越界愈论(某3卷)

我们成功爬取了书籍网站上每本书的名称! 4.总结

本节分析的是一个特殊案例’通过这个案例可以知道,有时候我们使用Selenium爬取的内容并不 -定和亲眼所见的完全符合,所以还需要小心。

本节代码参见: https://githuhcom/Python3WebSplder/ScrapeAntispider3°

卜「口厨

7β|字体反爬案例分析与爬取实战 本节再分析一个反爬案例’该案例将真实的数据隐藏到字体文件里,使我们即使获取了页面源代

●尸|■■厂卜△■【『



码’也没法直接提取数据的真实值° ↑.案例介绍

案例网站为h忱ps://antisplder4.scrape.center/’打开之后看着和之前的电影网站没什么不同。我们按 照7.7节类似的分析逻辑来爬取_些信息’例如电影标题、类别、评分等’代码实现如下: +ro们5e1e∩ju∩加portwebdriγer +ro|γl pyq0e】y加portpyQuerya5pq 十ro『∏5e1e∩1l』∏。webdr1γer·〔o『Ⅶo∩。by加port8y

十ro们5e1e∩ju川。webdr1γeI.5upport加poItexpected-co∩d1tjo∩5as[〔

F

7 】■■■■■■■■■■

|】

第7章JavaScript动态演染页面爬取

』□】‖|划

288

+roⅦ5e1e∩iⅧ。webdriver°5‖pport·wajtmport‖ebDrjγeI‖己1t

brow5er≡webdIiγer〔hro们e()





brow5eI.get(0https://a∩t15pider』。5〔raPe.ce∩ter/0〉



‖eb0riγeI‖ait(brow5er’ 1O)



,u∩t11([〔.pre5e∩ceo十己11e1e『∏e∩t51o〔己ted((日y°〔555[[[〔了0R」 ‖ .ite∏0 ))) ∩t∏1=brow5er。pageˉ5ource do〔二pq(ht们1) jteⅦ5=do〔(』.1te‖!) 「orjteⅧi∩1te川5。iteⅦ5(): ∩a们e≡ite川(‖ .∩己们e0 ).text()



c日tegorie5≡ [o.text() 于oro1∩ite们(‖.〔日tegoI]e5butto∩0).iteⅦ5()] 5COre≡ite们(0 。S〔Ore0).te×t()

这里先用Selenjum打开案例网站’等待所有电影加载出来,然后获取页面源代码,并通过pyquery

((‖{归

pri∩t(+|∩日Ⅶe: {∩a‖]e}〔ategorje5: {〔ategorie5} 5core: {5core}! 〉 brO"5er.〔1O5e()

提取和解析每-个电影的信息’得到名称`类别和评分’之后输出,运行结果如下:

‖■日

∩a们e:霸王别姬ˉ「arewe11‖y〔o∩cubj∩ec3tegor1e5: 「乃||』时0 」 ‖发0册‖] 5core: ∩a"e:这个杀子不太冷ˉ〔白o∩〔己te8or1e5: [‖剧情‖’ 0动作0 ’ {犯罪‖] 5〔oIe: ∩a"e: 肖中兑的救赎ˉ「he5h日wsh己∩促Rede『∏ptio∩〔日tegoI1es: [ !剧怕|’ 0犯罪|] 5〔oIe; ∩己‖e:泰坦尼兑号_「1t己∩1〔〔ategorie5: [ 0剧悄! ’ 0金』附! ’ 0灾难0 ] 5〔ore: ∩a们e: 罗马假日 ˉ Ro们a∩‖o11daγCategorje5: [剧悄0 ′ !兽剧0 ’ ‖爱怕‖] 5〔ore: ∩a川e:唐伯戊点秋杏ˉ「11rti∩g5c∩o1日I〔日tegoI1e5: [ !甚剧! 』 0爱情|」 |古装|] 5〔ore: ∩a"e;乱世佳人ˉCo∩ew1t们the‖j∩d〔ategoIie5: [ 0剧悄0 ’ {爱价‖’ ‖历史‖’ ‖战争Ⅲ ] 5〔ore: ∩己|7|e:兽剧之王ˉ丁∩e贝1∩go十〔o|∏edγcategoIje5: [|剧怕‖’ ‖兽剧‖’ ‖金价‖] 5core: ∩己∏]e:楚门的世界ˉ『he丁ru‖a∩5how〔ategoI1e5: [ 0剧悄|」料幻0 ] 5〔oIe; ∩a∩e:狮于王ˉ『∩elio∩民j∩g〔ategorje58 [ 0动画‖’ ′歌舞0 ’ 0冒险,] 5〔ore:

勺‖‖日

很奇怪’结果中的5〔ore字段不包含任何信息’这是怎么回事?经过仔细观察’发现评分对应的



源代码并不包含数字信息’如图7ˉ48所示° 嘴





固>…‖…

x





Q★力珊●§

◎ ·■…审A酶…呻m‘



·■|■■■‖□●∏

回5c『.p° …O=↑脓“沉6↑

辨膨

■王别姬ˉ「a「●w●||册yC◎"cub∏∩e ●●

穗剪靴姐

中田闯瓣中■■源′070甘协

啊队甲

0旁】O7聪上以

亡r●台●

……



?『饵7…

◆吧皿咖●口广……屯…`℃1=■;叭ml=〗·■0●鹤l七Ⅳ由0■∏° ●<』叮■0≥v≈『&8t…●…招m■≥d凹…∩ˉ . 叮■—

攫f



………P…1……→ . .









× 进

;…饥·+4唾

Ol=t°■吓Ⅶ《 }

●■』■【℃0◆/●H=耻== 守、■】v缸●·≠牺】必铂巳…■℃l=了■ 〗占钩呵●

β△d1v山1』≈←『…0um巷≡●l<■l●`=四`■泌◆1□切‖●■P心●l冗■1←O●1≈t□■蕾协=吻1v尸 ■√■』■●0←订…J…0■…广●●l●切札■〖=田‖宅0●‖<D沾…■●b<·0=…】■‖→l→峙~句′●0吟 ■

.lk蹿°$【●丁Q恤切……~!《 唾…P■行…】 w■0』m』~2 …t■…』了■;

…彰kJ…00Q0吧汕;】



·…■m《

酶‘…‘凸瓤〗』

≈饱H…t0心』ˉ『…0?宰吨佃?

■/…



P●……T……矽

0●≈l囱《

O迅献…F…吐″0■ 』色■』…■$ˉ^匀Q=

=些■→弓四i1

≈叼』q广■牢…『…■I〗

√匡



T 、=…→■…壬

p《

Ⅲ”″镭『哑y卯″F

山″i叮↑O!″} 尝空l『出l■■≤?…Ⅲ I■』 …△剧〔“锗】 …△←●!“镭】 J≈ ≈旧8制■h吟尔叮《…『 ≈旧$串‘■h吟尔叮《…□ …伶.吻U≈;←′ …拧吻凹…亏←′

■■厂

●◆″■ˉ广种…′卢 ′●』仿 0】■0T7

…≈9串…mh≈■呻■它旧…山凶…坷“■…△″“^…弘·……●轻韩α■…n山餐…必Q起m令………O矽炉n叭赶■幻.1■户.■曰【.t…导

图7ˉ48评分对应的源代码

5pa∩节点里就什么信息都没有,提取不出来自然也不足为奇了’那页面上的评分结果是怎么显示

《】■|』‖】·】』■]|·■■{|」■□』γ||」■■■□

其实也是CSS在作怪°

■■||」■■]‖』‖』】■■■Ⅵ‖■■|{■■□

卜凹“〔▲叮″…【l··≥^pg■1龟…酗.●■√皿 √…

出来的呢?

』·‖二■■■△■〗↓‖‖□■

冗~

……



……一







短啥





…_



田…

蹿霸…

| [■厂「卜|■■厂■『|』△尸「

【尸■仆卜■■「■尸‖■尸巴∏卜【■『β‖■■》伊■广↓『|■尸◆尸卜 巴 ■ 「 | } 卜 |

广

78字体反爬案例分析与爬取实战 2.案例分析

我们可以观察_下源代码,各个5pa∩节点的不同之处在于内部1节点的〔1aS5取值不太—样°可 以看到图7ˉ50中_共有3个5pa∩节点’对应的〔1a55取值分别是1co∩ˉ789` 1co∩ˉ981和i〔o∩ˉ5O4, 这和显示的9.5有什么关系呢? 接下来观察各个j节点的CSS样式,如图7ˉ49所示° </d』v≥

S…″p稗蛔四…

v≤d1vdat■→v→0907q▲C8αa§5=呵e1七□lG1=〔◎1=X马e1<O1=屁已=5el=C◎l审S>5e1=C◎1≈叼=▲.0>=—…,

v如d■t←γ=·907“C6Cha5$扫u0SC◎厂e叶t翘酝b=侗…S们0厘>

尸冶

7spa∏αBt■ˉγˉO9070▲〔8澎



‖?:撇蛾]



fl°………的瞬≥

v



翻蹿…_

<′】>

</$p■∩≥

p≤∑p●∩dat■=γ宅鳃7“ca>≈≤/S四∩> 『≤5凹∩dat白=γ…7“c8≥ T<1data=v=0兜7Q啡C8怎1aS■锤M1co∩』co0沪5O鼻雌>

8:比↑◎『e ≤/1≥

图7ˉ49

1节点

会发现1节点内部有_个::be+ore字段’在CSS中,该字段用于创建-个伪节点,即这个节点

和j节点或者5pa∩节点不-样° : :be十ore可以往特定的节点中插人内容,同时在CSS中使用co∩te∩t

』■■■■■

7 —

字段定义这个内容°我们在第一个i节点里看到了9这个数字,观察另外两个j节点’可以看到.和 5’ 3个内容组合起来就是9.5°

3.实战

那〔1a55取值和〔o∩te"t字段值的映射关系是怎么定义的?我们可以在测览器中追踪CSS源代 码,代码文件如图7ˉ50所示。 Stγ…cα询putm

匿γ颧Y↑L』Bt酮α白

礼剧γ◎"

「蛔



熊糕宦粤】

.1C◎卜7Sg:befO「e{ c◎∩t●∏t: 』09M;



『}

■尸β巴◆『■■『『尸「β广|◆巳■尸[『■■「β■■尸卜巴厂|■『



289

图7ˉ50

CSS源代码文件

进人文件后’可以看到整个CSS源代码都在—行放着,点击“{}”按钮格式化代码’如图7ˉ5l

巴厂■『「|}■■厂}|「仆‖』△■尸|}

所示° ……”

宁□m

E….………×



】0{cmte间tgo0900》o1co硅◎lO∩8证7◎「e《C即te∩t:0』:00},』仁O∏=9印1colo∩;befo厂e{CO|

v°■…0.蛔…呻vm p■…

卜■徊归

|●「}||■■「化尸)}■尸匹■■■『〖「·巴「‖‖■■『■■[|■■■【『【■■■『【『‖·■■【

尸n刺v咱 p皿尸



■儡…| 仆◎四·∏哟m`碱

p·pm酶■`J呵



腥 ‖



嗣囱`』·…`吨硒 图7ˉ5l

e……:呐

点击“{}”按钮

之后CSS源代码就被格式化了’如图7ˉ52所示°



第7章JavaScrjpt动态演染页面爬取

290

限缸

够…咀ˉ」

…尸……门

…蛔

sq篮二芒三





!



D



_

4Jw■=

v□呻

■丛≈p弓叮

1了唾

·【f`■…凸四……

凸■■●U≈UU■≈

co∩te∏t: 扫7■

17师1↓》 1γ船2a

P蜘酶0

17%3 ·』cm=281〗be↑o了e{

’■…

17酗 17“30}

b■们吨

cO∩te∏t; 0c80$

{嚼{ .lc°"ˉ7的:b·′.「·《

p■p

17“6

必◎油.∏喊缅.圃

◆°D0,…m

仁◎∏tG∩t: 凹9巴

|||

17酷9》 〗7·70

17071 ·1〔咖=colo":be?O了e{ 17·7∑

cO0`te∏t8 郎8帕

17073》 1707d 17·7Ⅲ ·1Cm=二四1CoI呻;be↑o

一乙Ⅳ…≈幻=_

■凸







…:吃

图7ˉ52格式化后的CSS源代码

叮以从中找出如下内容: .1co∩ˉ981:before{ 〔O∩te∩t: "°"

} .1co∩ˉ272:be十ore{ 〔o∩te∩t: "O"

0



.i〔o∩ˉ281:be十ore{ q

co∩te∩t; "8"



〔o∩te∩t: "9"



原来〔1a55对应的值就是_个个评分结果。这样我们就有底了,只需要解析对应的结果再做转换

即可°这里需要读取CSS文件并提取映射关系’这个CSS文件是h忱ps://antlspjde叫scrape.center/css/ app654ba59ecss’其部分内容如图7ˉ53所示.

可 ‖



回 令

令◎

|‖』口日日■■」‖二勺|‖』〗■口



。1co∩_789:be+ore{

×





巳■硒…Q缉鹤扩a吟礁…■…o“m6知≈$

q

冗●…r!dm■=7≈7q●●b900M…R6m■囤→c@1◎r1拆细》.匈诚皿…[d●之■=p=7●…0■]《■』户亿06OP丘『…儿哟←电牢80严).1…[d●m≈v=7Q●印p"】

《h●止g■亿600严〗■▲■浊0酌●尸5尸m4t▲m■【■X●色上●■》up●Q°1…●』≈庐【山c●≈卜7…O●M■儿协t60O尸》.1■呵凹 .】凹0=凹型●【硒■【·→=70●●凶O0】 《p叼1t』匈『…1凹■『】●丘t』9■严『c吨89严p沁1卯t′“瘁〗g■仁<m●■2n牌』G…型户亿q7晌』■〗■0掷4d}≈p《【mt=



((

心★力●!

f…1wmm1ro唾1■■七止@●夕杠』■〃■●■●≈■■■』Ⅲ}0■mt■B=●《2…=r回EV0●…←…》■】●m■』(··′f…c●/●1…cˉ1…■.g3507725·吨m)

■亿γ沁8■◎…1》…,[@1■●●由· ●1=』◎■■pi0lc1■●.≈止』唾M…』怜£mc←h必o■■·』m■四『雪≈■=m卫■2唾t-…a人闻8铱■y■c哗●》

『■1■■■·●■●1←1…芭]‘[巴1●■■^饵■1ˉ』辑咆≈】《£吨←Z■△1y8叭…仁■』…■M…江●…】程■…J【四色·旧巴〗卫■0…〔≈1ⅡZ■仁■…儿碘乙Ⅱ40O『【q2nt■ v·唇吗回仁0吵≈10t■它=t与…已:■…;1≡■』扣c:1p≈睦屿·1ˉ·1』尸0■■1…『“叼】叮0』■1…心1mnj。·lˉ1…Kc·ˉ◎

|‖

【●…k《争mf?●〗″■丁1《·.′【m亡■/●=t■mm▲审丁】刁】m凸.●仁Ⅲ》【…止《·c正■…□)『……■1吵R∏』003F…亿■d1叼1■y∏·■u七◎·〗『田】乞=



■函四Ⅱ…◎m《甸■…七0·\■■0句}·●】←』……<=…■■8…吼……8·\乙J》·●】■』…=】◎』1八…R■【≈●《…cmt日句`∏丛0·》辱●1=1cm← 人■≡七凶8m£◎m{◎回宅呻cE·\■凹】.》.●1自k…出…钩〗m…h{……■.\■蝉■》·●D1……r■Z●m《Qm…tD■\■“■·).●儿ˉA….

0

蜜?≡些?哇锤刊墅空色§巳P凹?》吕9l亏些…字,■■呼P{……c0.皿卸.〗型ˉ止…≡m【质…mt◎证!№Ⅱm·《…·m‘百`面■1·》。mˉ』…ˉ

‖】|」·||□可{|』■】||‖‖‖■■

户@忙←■七r1碎,■七r■《…七■亡F·\“m■》·●h■屿…=刨】k■亡■8■■m叫…■企o·`≈M□》·■1●1…←』□■≤rmk』远扣贷●《@…t●田它0w\mn】.》.■〗=…=

尸▲正【■■●Z●{□乙O■b■·`…但》.●1■』…≈●w1●8■£●r●{……▲8·`■…■》●●】也…=呵■■2●r●《…b■吧‘■Ⅵ●凹■》令●l=八…~

…1咏=画Ⅷ■←Fuu0■…●{函口t●m0·\面面□》令●≥L……1●←…m■……●《……0·`…】·》.●】■』…→碑L℃=

型↓婆…v{.宇c=c直F些p!?》.91宁▲…~…1吐↑■f°军·《…c凶m5`邮■口》,凶ˉ∏……】d<r皿■‘mmm《≡盂赢5`E6·T》′·1ˉ‖…=巨□■← c吨8助噬m●《@……0■\酗m·》·●1■沁m≈吨允■r=c■P0馋2m●《……cz□\…9·》·●k≡1…迪c→t●r0mⅡ。m《…亿mt!·`醉凶■}.●1每止….1e←

●x●…b<◎m《…■■色8●\■6■必》.凶=人●≡宇■■≈r@■…●{……●■·\■砖》◇●1■…亩匹■…≡●(@函…c■·`■6■·》·■1=』仁■→

些↓空子T唾匹9PTc警E:酵m:》:·1_』…=…wc1m£…【…t■仁!9\…》.·1ˉ久…二粒土垮m咸0…示《忘矗息;己Ⅷ氏丙.·1ˉ如…em丘ˉ ■硬皿】…凹m《卤u■田t$□\■6@°].●1■儿…■@h▲●…∏凶fm℃《……龟0户\■c】●》◇●1巡=…0酌…■《…0mm□\■$c4·》令●1~人…■d皿h= M沁f缸●《…t■c0·\奶cg·》·●1=1co■≤dmh〗凶f@妇{…t画电0p`…6·》■u→……电上碘c8mⅡ…■{@Q@七●■t0◇\郝蹿·》°●】ˉ』…←

…1邱f◎r●《…c■tdQ\碑r0·》.●1=』……】…≡·■…c■fm■《……8口\■幻1·》.●2≈…■户工uy■c1…:mf西●《……tt·\■6m嚼》.●1锈

』●■宅1…8■【…《……■屯■·`■GF3·》·●1=』…=…口yU函■°■《……O8·…碎■》.●】=…-O■■■°m《…0…t0·`■c丁7·》.●』=』…■G它厂d萄≡

〗8mr◎r●《…乙■t0旬\醉P0·》°●1二』@唾=m■m“8呻2◎■《……c0·`“”·》·n=跨■■萨洒u■k■2m●(……】.\■6m·》·●】毋∧…■ 11碘m儿吨0■Ⅲ…《mm■■c〖●`■门P■·》·巴≈八…→L1呻t=■■1■0■【□F●《…它mc0●`“症口》°●】■』…■■1■■…H酵2●■●《…它四巳〗·`■…■》·巴咀…■ 匹牢m110…“■《……t!.\■7】2颧》.●1尝儿…→●…mm2唾●《……允U●\m1〗■》.●1=堡…坤1〗0■mm《…tmc0·\■7l9.》.●l=β■≈ˉ

■■k咏m118■mF●《…t…t】■\■716.》.●1=1…●…P8■z唾吼……k8·\■拘■·》°u宇…=t…■■£◎m《…℃●■t8□山70·.》.●1=』…ˉ

b八■万L●p■2…■{●□屯…亡s·`■7G〗田)·●1ˉ4……皿●←…0■e@■●《…辑七0·`■$DD·》·●】ˉ…-■●汪●4m0呻G■●(…它…0°\■βDO.〗.●1-儿■m h●γ1■■唾■《……』□\■“2口)°●1二…=m】“m坤£唾●《…它四色U●\m■4憾》.●1=』…←…0m2…《“m●m0Q\■6■5触》·●1=』…←

■℃…〗…◎m{……w·Ⅶ0r■宫}◎鳃■几…■wm■=】0■【呵●《……巳D■\田庙口》·●儿●…吨…m…●(…c≈■■.`■∏0■.》·●L←1…≤丛匹

@1≈k0……《……tB·\■70】.】o●』■』由垫→1≈▲c』■8…Ⅲm叫…≡0·`■把4Q》·●1■蛇m也m吟』…七lm0迪2锤■{……屯8■\■709·》.■1■1c■■

mc1A西0肄≈T■{…七■c0.\■丁“口》.●1→』……l…唾』≈0……[~●0·`m9额》·●1=八…p…6m£●■■0…也m仁p勺\■709.》°●1←八…ˉ ■止““电r0…◎Z■《…■■t■·\■7恤■〗锣●1←m四=r1mcˉ□』企仁』t8…■唾叫…t■包〗口\m凶.》.●〗■…●tx响电1$≈mm《c■…屯0钨ⅦF“●》嗡●1←』…= cr…y∏m【■●{…●m8.`■7”■》·●上■1Om…儿【■z°m《……c0.`■70■●》·●1■-■18■mr●{…c●nc;.Ⅵ7O丁鱼》.●儿=人…=

■c…亡吨■>…●{@匈t…t】□\■7】0◇)°●1.1…■LC0贮£酝℃《……c■□\m】1.》·●1=』…=吐0谭Ⅲ酝■《…●●▲司t『口\■71■·》·●L→』…ru11~

图7ˉ53

CSS文件的部分内容

勺‖■‖(‖‖‖可‖□■‖□凸可|‖·‖』■□γ

…1唾■巳』□『■【■■{@唾起mt0◎`E了06.》.●1=儿…←1…m』≡』m…1m0■……广\m07p》·●』=』@呻口1…→0乙ˉ

‖ ●■「}

■■「}巴■厂

78字体反爬案例分析与爬取实战

29l

匹■厂甲】巴尸

我们可以试着用requests库读取结果’并通过正则表达式将映射关系提取出来,代码实现如下:

|【■『■巴『■》「匹β『)匹∩〖■厂‖『■β卜■尸卜■厉

1们Portre 1Ⅷportreque5t5

ur1= 0∩ttp5://a∩ti5pjder』,5〔r3pe.〔e∩ter/c55/app·65qba59e°〔55 re5PO∩5e=req0e5t5.ge↑(ur1) patter∩=Ie.〔o|∏p11e(0 .jco∩ˉ(·*?):be千ore\{co∩te∩t:‖0(.*?)御\}0) re5l」1t5=re.十j∩da11(patter∩’ respo∩se。te×t) 1co∩-川ap={jte∏[0]: jte∩[1] 千oIjte阳j∩re5l』1t5}

这里我们首先使用requests库提取了CSS文件的内容’然后使用正则表达式进行了文本匹配’表 达式写作.j〔o∩ˉ(.*?):be千ore\{〔o∩te∩t:|』(.*?)"\},这个表达式并没有考虑空格’因为CSS源代码本 身就在_行放着而日夫除了所有空格°

例如’对于如下CSS样式: .jco∩ˉ789:be十ore{co∩te∩t:"9"}

就会提取得到两个gro0p’第—个是789’第二个是9° ■』

这胆我们使用re里的+i∩da11方法进行了内容匹配,得到的结果如下:

卜}『『□‖|【■厂

●『卜■■‖‖‖

[…’(‖at! ’ ‖0‖)’(,∧‖’ |A0)’(』8』’ 』8!)′(|C’!C)’(D‖’D0)’(』[|’[!), (‖「0 ’ 0「′)’(℃! ’℃0)’(|‖! ’ ‖‖|)’(‖I0 ’ !I!)’(|〕, ’ !]‖)’(!促』’ ′‖)’(‖[0 ’ !l‖)’(‖‖’ !‖0〉’(』‖0 ’ ‖‖!)’(00! ’ ‖0!)’(‖p』’ ‖p‖ )’(‖α’ 0α)’(!【‖’ !R』)’(‖5『′ !5‖)’(丁0 ’ |丁,)’(!0』’U0)」(,γ0’ ‖γ0)’(W’w)’ (!X|’ |X0)’(0γ!’ ‖γ0)’(,Z! ′ |Z|)’(0bmC低et1e十t′’ ‖[‖)’(ba〔促51a5h|’ 0\\\\’)’(0braC促etright|’ ‖]0)’(0a5〔jj〔jICu"’ 0 ‖)’(!a!’』a|)′(,b』’ !b!)’(!C0 ’ !〔|)’(,d’0d‖)’(‖e0 ’ ‖e0)’(|「′’ 0十!)’(,g‖’‖g,)’(‖h‖’ ‖ |)’(|gmγe{ ’ `

■ ■ ■ ■ ■ ■ ■

7 . ■





,h,)’(′i!’ !j‖)’({j|’ 0j|)’(|‖’|低』)’(`1′’ !1』)’(|∩,’ |川!)’(`∩|’ ′∩|)’(|o』’ 』o|)′(|p|’ 0p0)’(』q"’ 0q′)’(0r`’|r』)’(|5|’ !5|)’(|t‖’ |t|)’(0U|’ 』U|)’(!γ0 ’ 『V|)’(W』’ 0"`)’(|X|’ |X|)’(0y』’ |γ0)’(『Z‖’

‖Z|)’(bra〔e1e什』’ !{!)’(!b3r‖’ 0「)’…]

这个结果是由很多二元组组成的列表。我们遍历这个列表’将其赋值成字典即可’最后j〔O∩ˉⅧap p

口〖卜)‖■□■「巴【尸ˉ『凸■■`ˉ■′■∩|厂’「■尸}’◆卜■尸|》『



就变成了如下这样: { ●





|at` : ,0|’ ‖∧, : 0∧0

08‖ : 08,’ ■





07890 自 090D ■





baI’ : | 0 ′’ ●







例如使用789索引,得到的结果就是9° 运行测试一下: pri∩t(j〔o∩-阳p[789‖]) PⅢi∩t(i〔o∩-阳p[04〕7‖])

运行结果: 9

3

和源代码保持—致。

所以’我们只需要修改一下提取逻辑即可,代码实现如下: 「ro们5e1e∩j咖iⅦportwebdIjγer +I咖pyqueryjⅧportpyQuerya5pq +r咖5e1e∩il』Ⅷ·webdIjγer·〔o∏】Ⅷ∩.byi呻o】t8y

千r咖5e1e∩j咖·NebdIiγer·supportj‖‖portexpe〔ted〔o∩djtjo∩5a5[〔

| 坠凸∏‖■}■■卜『‖



292

第7章JavaScript动态演染页面爬取

fro们5e1e∩1uⅦ·webdriver°5upport.w日jtj∩port‖eb0riγer"ajt 1Ⅶportre 1川portrequest5

ur1= |∩ttp5://a∩tispjdeI4。5cmpe.ce∩ter/c5s/己pp°654b日59e.c55 respo∩5e≡request5·get(ur1)

de十paI5e5〔Ore〈ite"): e1eⅦe∩t5=iteⅦ(0 .i〔O∩|) 1〔O∩γa1ue5= [] 十oIe1e|∏e∩ti∩e1eⅧe∩t5。jte川5(): 〔1a55∩3爬= (e1e川e∩t.attI(0C1a5S‖))



patter∩=re.〔o们p11e(』.jco∩ˉ(。*?):be十ore\{〔o∏te∩t:"(·*?),`\}‖) re5u1t5=re.+j∩da11(p己tteI∩’ re5pO∩5e.teXt) 1〔O∩ˉⅦap= {jte们[0]: 1teⅧ[1] +Orjte∏1∩IeSu1t5}

1〔o∩-代eγ=re.5earc∩(‖i〔o∩ˉ(\d+)! ’ c1a55∩己用e〉.group(1) ico∩γa1ue=1〔o∩ˉⅦap.get(j〔o∩-促ey) 1〔o∩γ日1ue5。appe∩d(1co∩γa1ue) IetuI∩ , 0 .joj∩(1co∩va1ue5) bro"5eI=webdI1γer·〔bIoⅧe()

brow5er.get(0http5://a∩tj5p1der4.5〔mpe.ce∩ter/‖) ‖eb0riγer‖a1t(brow5er′ 10) \

.0∩ti1([〔。pre5e∩〔eo+a11e1eⅧe∩t51o〔ated((8γ·〔555[[[〔丁O【’ ‖ .1te川‖)))

5core=p己r5e5〔ore(ite帆)

这里我们定义了par5e5core方法’它接收一个pyQUery对象jte‖’对应-个电影条目。首先提 取该1teⅦ中所有带有j〔o∩这个〔1a55的节点,然后遍历这些节点,从〔1a55属性里提取对应的1co∩代

』■】|‖□】·(』■■‖‖』』■』■】‖□□

pr1∩t(+』∩a川e: {∩日∏e}CategOr1e5: {CategOrje5}5〔Ore; {5〔Ore}‖) brOW5er.C1O5e()

□■可』问

c己tegor1e5二 [o.text() 十oIo1∩ite‖(0 .〔ategor1e5butto∩!)°jte∏5()]

‖|

∩t∏1≡brow5eI·page-5ource do〔=Pq(hm1) ite"5≡dO〔(,·1te"0) +Or1te∏j∩1te们5.ite们5(): ∩aⅦe=jte爪(‖ .∩a∏e』)。text()

号’例如1co∩ˉ789,提取的结果就是789’和我们刚构造的1〔o∩一∏ap是相对应的’将其赋值为1〔o∩-促ey° 使用i〔o∩—促ey从1〔o∩—"ap中查找对应的真实值’赋值为1co∩γa1ue。最后将j〔o∩γa1ue拼合成_个 字符串返回° 运行结果如下: ∩a爪e;霸王月|」姬ˉ『are"e11‖y〔o∩cubj∩e〔日tegoI1e5: [ ‖乃|」』肘0 ’ 0爱悄‖] 5〔ore: 9.5 ∩己们e:这个杀于不太冷ˉ[白o∩〔ategor1e5: [‖剧愉’动作|」 |犯罪! ] 5〔ore: 9.5 ∩a|↑|e: 肖中兑的救赎ˉ『∩e5∩日w5∩a∩代Rede们ptjo∩〔己tegor1e5: [ ,剧价‖’ ‖犯罪‖] 5〔ore: 9.5 ∩aⅧe:泰坦尼克号ˉ丁jta∩1ccategone5: [ ′剧』愉′’ |爱悄,′ 0灾难,] 5core: 9.5 ∩a∏e: 罗马假日 ˉ Ro川a∩‖o11daycategor1e5: [‖剧悄|’ ‖8剧‖’ ‖爱』阶‖ ] 5core: 9·5 ∩a们e:唐伯虎点秋杏ˉ「11It1∩g5cho1ar〔己tegor1e5: [|甚剧|’ 0发怕{ ’ ′古装0 ] 5core: 9.5 ∩a川e:乱世佳人ˉCo∩ewjt‖t∩eM∩dc己tegor1e5: [ }剧,叶’|金忻’‖历史‖’ ‖战争′] 5core: 9.5 ∩a"e:县剧之王ˉ 丁he肛∩go千〔o们edγ〔ategor1e5: [ 0剧′时0 ′ |各剧|’ |发价′] 5〔ore: 9.5 ∩a们e:楚门的世界ˉ丁∩e丁ru"日∩5how〔己tegor1e5: [ 0剧悄‖」`科幻0 ] 5〔oIe: 9.0 ∩己们e:狮子王ˉ 丁he[jo∩Rj∩g〔ategor1e5: [ 0动画‖’ }歌舞‖’ 』可险‖] 5〔oIe: 9。O





4.总结

本节介绍的也是一个特殊案例’通过这个案例我们知道’即使获取了关键的源代码,有些内容也

本节代码参见: https://github.com/Python3WebSpide∏ScrapeAntjsplder4°

||

还是提取不到,还是需要通过观察一些规律才能提取,平时遇到这种情况也应该多加小心°







■■■■



| 第8章{



验证码的识另‖



0





p

p p



各类网站采用了各种各样的措施反爬虫’其中一个便是验证码°随着技术的发展’验证码的花样 越来越多’由最初只是几个数字组合而成的简单图形’发展到加人了英文字母和混淆曲线,还有_些 网站使用中文字符验证码’这无疑使识别变得愈发困难° l2306验证码的出现使行为验证码开始发展,相信用过l2306的用户多少都为它的验证码头疼过,

p

b



需要识别文字,然后点击与文字描述相符的图片,只有所点的图片完全正确,才能通过验证°随着技

术的发展’这种交互式验证码越来越多,如滑动验证码需要将滑块拖动到指定位置才能完成验证’点 选验证码需要点击正确的图形或文字才能通过验证°

P

验证码变复杂的同时’爬虫的工作也变得越发艰难,有时候必须通过验证才可以访问页面°



本章统一讲解验证码的识别问题’涉及的验证码有图形验证码、滑动验证码`点选验证码和手机

P

P

厂 p

β

验证码等,这些验证码的识别方式和思路各有不同’有的直接使用图像处理库就能完成’有的则需要

借助深度学习技术完成,还有的要借助-些工具和平台完成°虽说技术各有不同,但了解这些验证码 的识别方式之后’我们就可以举—反三’使用类似的方法识别其他类型的验证码°

p



p



8.↑

使用OC只技术识别图形验证码 首先来看最简单的—种验证码-_图形验证码,这种

叠录



O

验证码最早出现’现在也依然很常见’一般由4位左右的



字母或者数字组成°





0















例如在案例网站h仗ps://captcha7.scrape.center/就可以

用户名

宙码

看到类似的验证码,如图8ˉl所示。

这类验证码整体比较规整,没有过多的干扰线和干扰

点,文字也没有大幅度的变形和旋转°对于这类验证码’ 可以使用OCR技术识别。

↑.OC只技术

验证码/

} aa玄b

回 图8ˉl 由字母或数字组成的图形验证码

OCR,即OpticalCharacterRecognition,中文叫作光学字符识别,是指使用电子设备(例如扫描 仪或数码相机)检查打印在纸上的字符’通过检查暗、亮的模式确定字符形状,然后使用字符识别方 法将形状转换成计算机文字。现在OCR已经广泛应用于生产生活中’如文档识别、证件识别、字幕 识别`文档检索等°当然’用来识别本节所述的图形验证码也没有问题°

本节中我们会以当前案例网站的验证码为例’讲解利用OCR识别图形验证码的流程’实现输人 验证码的图片’输出识别结果。

2.准备工作

在本节的学习过程中需要导人tesserocr库,这个库的安装相对来说没有那么简单’可以参考

「8 巴

■■·]』■∏‖□可■■

294

第8章验证码的识别

https://setup。scrape.center/tesse『ocr°

另外,还需要安装Selenium`Pillow`NumPy和re∏ying库’用来模拟登录`处理图像和重试操作°

■■】‖‖||司当■■‖‖

可以使用pjp3工具安装这些库: pip3j∩5ta115e1e∩j帅pj11ow∩uⅧpyIetry1∩8

·■Ⅵ』·

如果安装过程中遇到了问题,可以参考如下链接° 司

□Selenlum: htq〕s://setup.scrape.center/selenium □Pillow: ht!ps;//setup.scrape.centeI/pillow

·

』‖

□NumPy: https://setupscrape。center/numpy

□retryjng: https://setup.scmpe.centeⅣretrying q

安装好这陛库之后’就可以开始学习了°

3.保存验证码图片

d叁缚′

开案例网站’然后右击验证码图片,将其保存为captchapng文件即

■|』

为了便于操作,先将验证码的图片保存到本地°在测览器中打



可’示例如图8ˉ2所示°

图8≡2图片captchaPng

4.识别测试

q

现在新建—个项目’将验证码图片放到项目的根目录下’然后利用tesserocr库识别该验证码’代 码如下: 1刚portte55ero〔r

■■°·■■‖二■■

千ro爪pⅡ1∏port I∏己ge

1阳ge=I阳8e.ope∩〈‖〔apt〔∩a.p∩g0) re5u1t≡te55ero〔r。mage—to—text(jⅦ3ge) pri∏t(re5u1t)

这里先新建了—个图片对象.然后调用tesserocr里的j们ageˉtoˉtext方法将图片转换为了文本。 实现过程非常简单,运行结果如下:



d241

tesserocr还提供了_个更加方便的方法,可以直接将图片文件转化为字符串’代码如下:

d

‖‖』

j呻ortte55eIo〔r

pri∩t(teSserO〔r.+i1etOtext(‖Capt〔ba。p∩8′))

输出结果同样是d241° 可以看到,通过OCR技术便能成功识别图形验证码的内容。 5.处碑验证码

重新执行下面的代码做测试:



T

℃ . S 2d. ■

i呻ortte55ero〔r

「r咖pIlj{mort I帕ge j∏E8e=I阳ge。ope∩(|〔apt〔帕2.p∩g|) re5u1t≡te55erocr.mageˉtoˉtext(i帕ge)

输出结果如下:

||

ˉb32d

图8ˉ3图片captcha2.png

(|』‖

pIj∩t(re5u1t)



‖‖』』·‖‖■

换_个验证码,将其保存为captcha2.png文件,如图8ˉ3所示°

』{



8.1

使用OCR技术识别图形验证码

295

这次的识别结果和实际结果有偏差’多了一个‘‘ˉ”,这是因为图片里多余的点对识别造成了干扰° 对于这种情况’需要做一些额外的处理寸把干扰信息去掉。仔细观察可以发现,图片里那些造成干扰 的点’其颜色大多比文本的颜色更浅’因此可以通过颜色将造成干扰的点排除掉。 首先将保存的验证码图片转化为数组,看-下维度: i川poItte55ero〔r

+roⅦpI[mportI阳ge 加pOrt∩u们py日5∩p

i『∩age≡IⅦage.OPe∩(‖CaPtCha2.p∩g0) pri∩t(∩p.army(1们age)。5h己Pe) prj∏t(j阳ge。№de)

运行结果如下: (38’112’ 4) R6BA 卜|■「|『}■■尸′仍‖|卜|}止「[上■「|厂巳◆『卜旧【

从结果可以看出’这个图片其实是—个三维数组’38和112代表图片的高和宽’4则是每个像素 点的表示向量。为什么是4呢?因为最后_维是-个长度为4的数组’分别代表R(红色)`C(绿色)`

B(蓝色)`∧(透明度)’即一个像素点由4个数字表示°那为什么是R、C、8`∧,而不是尺` C、8或

其他呢?因为i则age.Ⅶode是R6B∧,即有透明通道的真彩色’运行结果的第二行也可以印证这一点。 Ⅶode属性定义了图片的类型和像素的位宽,_共有9种类型。

□1:像素用l位表示’Python中表示为「rue或「a15e,即二值化° □[:像素用8位表示’取值O~255’表示灰度图像,数字越小,颜色越黑°

□p:像素用8位表示’即调色板数据。

□R6B:像素用3×8位表示,即真彩色°

□RC8∧:像素用4×8位表示,即有透明通道的真彩色° □〔‖Ⅶ:像素用4×8位表示’即印刷四色模式。 □γ〔b〔r:像素用3×8位表示,即彩色视频格式°

□I:像素用32位整型表示°

□「:像素用32位浮点型表示。

为了方便处理,可以把RCB∧转为更简单的[’即把图片转化为灰度图像°往图片对象的co∩γert方 法中传人[即可’代码如下所示: mage=imge.〔o∩γert(!l,) j晒8e。5∩叫()

也可以往〔O∩vert方法中传人1,即把图片二值化处理,代码如下所示: mage=ma8e.〔o∩γeIt(,10) i‖Ege。5‖咖()

我们选择把图片转化为灰度图像,然后根据阔值删除图片中的干扰点,代码如下: 「ro‖‖ pI[ i即ort I阳ge j『印ort∩u∏WaS∩p

mage=I∏B8e.ope∏(!〔apt〔ha2。p∩g,) mage=ma8e.〔o∩vert(|[!) t∩re5bo1d=5O

army≡∩p.army(ma8e) army=∩p.灿ere(己rray〉thre5ho1d’255’0) mage=I阳ge.千Io|∏arIay(armγ.a5type(!uj∩t8,)〉 1阳ge。5bow()

这里先将变量tbre5∩o1d赋值为5O’它代表灰度的阂值。接着将图片转化为NumPy数组,利用



第8章验证码的识别

296

NumPy的WheIe方法对数组进行筛选和处理,其中指定将灰度大于阑值的图片的像素设置为255’表 示白色’否则设置为o’表示黑色°

最后看一下处理完的图片长什么样’如图8ˉ4所示。

可以看到原来图片中的干扰点已经不见了,整个图片变得黑白分明°此时重新识别验证码’代码 如下: i∩pOrtte55erOCr +ro们pI[ j‖port IⅦage iⅧport∩u刚py35∩p

mage=mage.co∩vert(!L0)

图8ˉ4处理完的验证码图片

thIe5ho1d=50

army=∩P.日rray(mage) arr日y≡∩p·"们ere(己rr己y〉thIes∩o1d’ 255’ o)

1‖age=I们日ge。千roⅦarr日y(aImy.a5type(!ui∩t80 )) prj∩t(te55ero〔r.i阳geˉto-text(j∩己ge))

运行结果如下: b32d

这次的结果是而确的°所以’针对_些有干扰的图片’可以做去噪处理这能提高验证码识别的 正确率。

6.识别实战

现在’我们可以尝试使用自动化的方式识别案例中的验证码’这里使用Selenjum完成这个操作, 代码如下: ]ⅦpOItt1『∏e

千Io‖5e1e‖1Ⅷ1呻oItwebdr1γer +Io阳joi∏portByte5I0 +ro∏pI[i∏portI‖age 千ro"Ietryj∩g1∏portretry 十ro川5e1e∩1u∏。webdriγer°5upport°Ⅳa1tmport‖eb0riγer‖ajt +Io∏5e1e∩juⅦ。webdrjγer.50pport加portexpected〔o∩djtjo∩5日5[〔 千ro们5e1e∩iu刚。webdrjver。co『∏『∏o∩·bγmportBy +ro川5e1e∩1u川.〔o∏‖『℃∩.ex〔eptio∩5mport丁1爬oot[xceptio∩

‖‖|‖□■■■‖‖】‖』】■〗■■■‖||』■】■□‖』‖

1川portre 1川portte55ero〔r



」 ■ ■ ‖ ‖ 】 】 ‖ ‖ 』 ■ 』 ‖ 』 ■ □ ‖ ● 〗 ‖ | □ ] ■ ∏ 〗 】 · □ 』 ■ ■ ∏ ‖ ‖ 勺 ‖ 】 】 ‖ 〗 ■ ∏ ‖ ■ ■ 勺 | ‖ 』 】 可 』 ° · 】 | ‖ 」 · { □ ‖

1"age= I∏a8e.Ope∩(‖〔apt〔ha2.p∩8‖)

℃32α

mport∏u∏Wa5∩P

Ietur∩1们age

@retIy(5topˉ们ax_atte∏lpt一∩uⅦber=1o’ retIy一o∩-re5u1t=1a川bdax: x1s「a15e) de+1ogi∩(): bro"5eI.get(‖http5://captcha7.5〔rape.ce∩ter/』) bIow5er。十j∏de1e‖∏e∩tˉby-〔55ˉ5e1e〔tor(′.u5eI∩a们ej∩put[type二"text"]!)。5e∩dˉ促eγ5(0日d‖1∩!) bro"ser.十i∩de1e爬∩t-by-c55ˉ5e1e〔tor(』.pa55"ordi∩put[type=赋pa55"oId圃]|)°5e∩dˉ促ey5(!adm∩‖) 〔apt〔‖a≡brow5er·十j∩de1e"e∩t-byˉ〔55ˉ5e1e〔tor(|#〔apt〔ha|) 1川age=IⅦage.ope∩(8yte5I0(〔aPtc∩a·5〔ree∩5打otˉa5-p∩8)) 1帕ge≡prepIo〔e55(j们age) CaptCha =tessero〔r·mage-toˉtext(m己ge) 〔aptCha ≡re.5ub(』[^∧ˉZaˉz0ˉ9] ` ’ ′ ‖ ’ 〔日ptc∩a) broN5er +1∩de1e眶∩t-by一c55一5e1ector(|.caPtc∩a i∩put[type="teXt"]‖〉.5e∩dˉ代ey5(〔日ptCha) brow5er 十i∩de1e爬∩tˉbyˉc55ˉ5e1ector(, .1ogj∩‖)〔1iCk()

‖ ■ ■ 」 ■ | | ‖ 」 ■ ■

de+preproce55(ma8e): 1们age=j们age.〔o∩γert(‖[‖) array=∩p.army(mage) arraγ=∩p.w∩ere(arraγ〉50’ 255’ 0) 1Ⅷage= IⅧ己ge。千ro∏l己rray(aIray.a5type(‖ui∩t8‖))





凶|‖』ˉ■』■Ⅲ

使用OCR技术识别图形验证码

297

trγ: ′

"eb0riγer‖a1t(bro倒5er′ 1O).u∩ti1([〔.pre5e∩〔eo+e1e用e∩t1o〔3ted((8γ.Xp∧丁}{’ 0//h2[〔o∩taj∩5( "昼录成功")]‖ ))) t1∏e.51eep(10) brow5er.〔1o5e()

Ietur∩「n」e

eXCept丁meout[X〔ept1O∩: retl」I∩「a15e

尸十 ·]

■口■【■==■■『‖‖皿=■■『β『‖【■■【『『‖|「■■『〗‖【‖=尸『‖‖‖|巴■厂『『}『■=■■「|||■■『|‖■=■■『『|■■■■「伊■「

8.l

∩a∩e

∏‖a1∩

≡=

brow5er

·

=webdriγer.〔∩r咖e()

卜■尸}‖广◆广‖■尸『■厂‖|■β卜■厂卜》}■■尸□′》=『『卜「■β‖β|■■■尸■匹庐『■【尸『■■尸▲■尸■田‖▲■「仕■■■「『■■■·尸●『‖■■■『|止■】■■「‖|血■【∩‖)「■■■【】■『血■■■「『□【尸□【■【【【

1o8i∩()

我们首先定义了-个pIeprO〔e55方法’用于对验证码图片做去噪处理’逻辑和前面是_样的° 接着定义了一个1og1∩方法’其执行逻辑是: (l)打开案例网站; (2)找到用户名输人框’输人用户名; (3)找到密码输人框’输人密码; (4)找到并截取验证码图片,转化为图片对象; (5)预处理验证码’去除噪声; (6)识别验证码,得到识别结果; (7)去除识别结果中的_些非字母字符和数字字符; (8)找到验证码输人框’输人验证码结果; (9)点击“登录”按钮;



(l0)等待“登录成功”的字样出现,如果出现就证明验证码识别正确’否则重复以上步骤重试°

其中我们用到了retO′ing来指定重试条件和重试次数’以保证在识别出错的情况下能够反复重试’ 增加整体的成功概率。 运行代码’会弹出测览器,我们按照以上流程输人相应内容,可能重试几次’就成功登录了网站° 测览器页面如图8ˉ5所示。 ◆瓮◆ ■

G◆G

固…‖……



■■_ˉ兰——

● 一_=—=

隧☆

●…了画…厉…叮

回5.·p· ~_

_

云录 砷书= ←=

….=

日H良b

甘赶■↑… ≈_—

o —

_

图8ˉ5测览器页面

■●l

登录成功的页面如图8ˉ6所示°



p◎



闻0÷

图≈审』……

● , `告

★●

旨古

●…’镭….……

固5画口p·

·■二



■司■』■可{』■可■■■●□■可|」■Ⅵ」』■■】‖‖|‖■=■■型■■

第8章验证码的识别

298

‖」

刘」‖



■录成功

@

·



图8ˉ6登录成功的页面

7.总结

■■‖』■|』■■

本节中我们了解了利用tcsserocr识别图片验证码的过程,并将其应用于实战案例,实现了模拟登 录°为了提高tesserocr的识别正确率’可以对验证码图片做去噪预处理°但利用tesserocr识别验证码

‖‖□‖■□

至此,我们已经能通过OCR技术成功识别图片验证码,并将其应用到模拟登录的实战中。

■□□门

的正确率整体并不高’下_节我们介绍其他方案。

本节代码见https://gjthuhcom/Python3WebSpide∏CracklmageCaptcha。参考资料见https://baikebaidu.

82使用Ope∩CV识别滑动验证码的缺口

|‖

随着互联网技术的发展,各种新型验证码层出不穷,最具有代表性的便是滑动验证码°本节中我们 首先了解滑动验证码的验证流程’然后介绍—个简易的利用图像处理技术识别滑动验证码缺口的方法°

■‖‖|」■■乙■■可‖|』‖』■司』■】■■』■■‖

com/jtem/OCR°

↑.滑动验证码

』■

说起滑动验证码’比较有代表性的服务商有极验`网易易盾等’验证码效果如图8ˉ7和图8ˉ8所示。

「△

<=舷

彰▲

辑浮爵蕊 {《 .二



r l

~|脖.





ˉ



■ˉ







□■

k』

.尸|







|||

尸西

霄块





厂q

}" l

酶引,七究sO哉

0



图8ˉ7极验的滑动验证码

户…”

滑块填充拼图

图8ˉ8网易易盾的滑动验证码

((』

拴捻攫缚ˉ瞳`



@◎o

82使用OpenCV识别滑动验证码的缺口

299

滑动验证码的下方通常有-个滑轨,上面带有类似‘拖动

滑块完成拼图,’字样的文字提示,我们需要向右拖曳滑轨上的 滑块’这时正上方的滑块会随它一起向右移动°验证码的右侧 有一个滑块缺口,我们将滑块恰好拖到这个缺口处,就算验证 成功了,图8ˉ7验证成功的效果如图8ˉ9所示°

0







h

p p









如果我们想用爬虫自动化完成这_流程,关键步骤有两个:



l‘·抄′

(l)识别目标缺口的位置;

(2)将滑块拖到缺口位置。

@◎●

蟹‘醒鳞

其中第(2)步的实现方式有很多,例如可以用Se‖enlum等自

动化工具模拟这个流程’验证并登录成功后获取对应的Cookie 图8ˉ9极验的滑动验证码验证成功 或IUken等信息,再进行后续操作’但这种方法的运行效率比较低°也可以直接逆向验证码背后的 JavaSc∏pt逻辑’将缺口信息直接传给』avaSc∏pt代码,执行获取类似“密钥”信息的操作,再利用获 取的“密钥”进行下一步操作°





注意出于某些安全方面的考虑’本书不会介绍第(2)步的具体操作’只会讲解第(l)步的技术问题。 p







P

本节的目标明确了—识别目标缺口的位置’即给定_张滑动验证码的图片,使用图像处理技术 识别出缺口的位置°

[·/

2基本原理

b

D

‖『











◆_



||=州Ⅶ■ˉ■了…

抄…

—曲$尸







出邮。`

—壬







幽雕剿



〗区■】闪‖〗∏山口‖『〖〃Ⅷ№阳『『】



图8ˉll所示。 捻旧|总’



本节中我们会介绍使用OpenCV技术识别缺口的方法’输人一张带有缺口的验证码图片’输出标 明缺口位置(-般是缺口左侧的横坐标)的图片。这里输人的图片如图8ˉl0所示°最后输出的图片如

图8ˉl0输人的带有缺口的验证码图片





」≥=[‘ 』 T

■= 厉

■ l

●1

v

图8ˉll



输出的标明缺口位置的图片





[ p







具体的步骤为:

(l)对验证码图片进行高斯模糊滤波处理’消除部分噪声干扰;

(2)利用边缘检测算法’通过调整相应阂值识别出验证码图片中滑块的边缘; (3)基于上-步得到的各个边缘轮廓信息,对比面积`位置`周长等特征,筛选出最可能的轮廓, 得到缺口位置。

3.准备工作

请确保已经安装好了pythonˉopencv库,安装方式如下: p1p〕1∩5ta11pyt∩o∩ˉope∩〔γ

如果安装出现问题’可以参考https://setupscmpe.cente门pythonˉopencv°



] 创

300

第8章验证码的识别

d

另外,建议提前准备_张滑动验证码图片’样例图片的下载地址是lmps://gthuhconγPyd】on3WebSpldeI/

CrackSljdeCaptcha/blob/cv/captchapng,当然也可以从https:〃captchal.scrape.center/上自行截取’得到 的图片如图8ˉl0所示°

4基础知识

先来了解—些OpenCV的基础方法’以便我们更好地搞懂整个原理° ●高斯滤波

√□||∩·‖|‖‖‖|」』‖

高斯滤波用来去除图片中的一些噪声,减少噪声干扰,其实就是把一张图片模糊化,为下-步的 边缘检测做好铺垫°

OpcnCV提供了_个用于实现高斯模糊的方法’叫作6a0551a∩81ur`其声明如下: de十Ca(』55j日∩B1l』I(5rc’ 促5ize’ 5jg们aX’ d5t=‖o∩e」 5jg∏aγ=‖o∩e』 border「ype≡‖o∩e)

|』(‖』‖‖‖{●

其中比较重要的参数如下° □5r〔:需要处理的图片°

□促5j∑e:高斯滤波处理所用的高斯内核大小’需要传人_个元组’包含x和γ两个元素° /\ □5ig"己X:高斯内核函数在X方向上的标准偏差°

□5jg爪aγ:高斯内核函数在γ方向上的标准偏差。若51gⅧaγ为o,就将它设为51gⅧaX;若51gⅧaX

ˉ





■■



de+〔a∩∩γ(加age’ t‖re5ho1d1’ tbre5ho1d2’ edge5=‖o∩e’ aperture51ze=‖o∩e’[2gradie∩t≡‖o∩e) 其中比较重要的参数如下°

□1‖age:需要处理的图片° □tbre5∩o1d1` thre5‖o1d2:两个阂值’分别是 /

核的大小。

□[2gmdje∩t:用于查找梯度幅度的等式。 通常来说’只需要设置t‖re5‖o1d1和thre5∩o1d2

的值即可’其数值大小需要视具体图片而定,这里可

以分别取为20O和45O°经过边缘检测算法的处理后’

会保留下_些比较明显的边缘信息,如图8ˉl3所示。

\/一’ ~/→

□ /.\ 」α°-—

图8ˉl3边缘检测后的效果

(|||

」■■]|||』■■】‖‖〗勺口||』』·■日||□』‖』■■■‖』】』■■■■【■■

□3pertur…;用于查找图片渐变的索贝尔内 } \



最小判定临界点和最大判定临界点°

』 ‖ 』 』 ||■■司|』』□∏|」□`』■‖||

也实现了算法,方法名就叫〔a∩∩γ,其声明如下:

□∏

ˉ°=



萨】■咱刀勺则





图8ˉl2高斯滤波处理后的效果

‖ | 叫 』 ‖ √ | 《 ‖ ‖



隘■酗》■



检测算法是〔a∩∩y’这是JohnF.Canny于1986年开发 出来的一个多级边缘检测算法,效果很不错°OpenCV

||

讣`





由于验证码图片里的目标缺口通常具有比较明显 的边缘’所以借助-些边缘检测算法,再加上调整阑 值是可以找出缺口位置的°目前应用比较广泛的边缘

■。Ⅺ■



●边缘检测

广■

滤波处理后’图片会变模糊’效果如图8ˉl2所示°

″ …

堂』』】】】|‖‖【Ⅷ

∏伊■■闰=

这里促51ze和51g‖aX是必传参数’对于图8ˉl0, 代5jZe可以取作(5’5)’ 51g刚ax可以取作0。经过高斯

√|』』|‖《〗』』∏‖■■〗|」

和51gⅧaγ都是O,就通过代5jZe计算出5ig"aX和5jg‖aγ°

82使用OpenCV识别滑动验证码的缺口

30]

·轮廓提取

进行边缘检测处理后,可以看到图片中会保留比较明显的边缘信息,下一步可以利用OpenCV技 术提取出这些边缘的轮廓’这需要用到+i∩d〔O∩tOur5方法,其声明如下: de++j∩d〔o∩tour5(j肌age’|∏ode′ Ⅷethod′〔o∩tour5≡‖o∩e’Memr〔hy≡‖o∩e’ o仟5et=‖o∩e)

其中比较重要的参数如下°

□i"age:需要处理的图片° □"ode:用于定义轮廓的检索模式’详情见OpenCV官方文档中对RetrievalModes的介绍°

°|鹏:用于定义轮廓的近侧方法详情见opencv富方文档中对c°n………M°d“ 这里’我们将Ⅷode设置为R[『R〔〔0‖p’将"et∩od设置为〔‖A1‖∧ppR0X5I‖p[[’具体的选择标准

可以参考OpenCV官方文档的介绍,这里不再展开讲解。

||

「「■【『|■「||■「|||●「{『[■「|乙■||■=■「■■「`『▲=■侦■ˉ已■‖|『■『||‖》)■■『■『卜▲■仍■「|‖}■「|>广|■尸|_■「}卜■『|●【[■『『卜||■「|凸|厂|}卜





●外接矩形

提取到边缘轮廓后’可以计算出轮廓的外接矩 形’以便我们根据面积和周长等参数判断提取到的

轮廓是不是目标缺口的轮廓°计算外接矩形使用的

方法是bou∩d1∩gRect, 其声明如下:









≡≡n′-哼

=九』≡弓 -

de+bou∩dj∩gRe〔t(array)

/.≤

这个方法只有-个参数,就是array,它可以



是-个灰度图或者2D点集’这里传人轮廓信息。

! ≥

i≈

经过对轮廓信息 和外接矩形做判断,可以得到类似 如图8ˉl4所示的效果。

8 ■■■■■■■■■■



图8ˉl4获取轮廓的外接矩形

●轮廓面积

从图8ˉl4可以看到,我们已经成功获取了各个轮廓的外接矩形’很明显有些不是我们想要的’

我们可以根据面积和周长等筛选缺口所在的位置,于是需要用到计算面积的方法CO∩tOur∧rea’其定 义 如下:

de千|c°∩t°q…(〔°∩t。ur』 。rie∩…o"e) 其中各参数的介绍如下° □〔O∩tOur:轮廓信息°

□orje∩ted:方向标识符’默认值为「a15e°若取丁rue’则该方法会返回一个带符号的面积值, 正负取决于轮廓的方向(顺时针还是逆时针)。若取「a15e,则面积值以绝对值形式返回° 返回值就是轮廓的面积° ●

轮廓周长

同理’周长也有对应的计算方法’叫作arC[e∏gt们,其定义如下: de+aIC儿e∩gt∩(Cqrγe′ 〔1O5ed)

其中各参数的介绍如下°

□□

CuIγe:轮廓信息°

〔1o5ed:轮廓是否封闭°

返回值就是轮廓的周长°



0

■=■』】|■■||■可

第8章验证码的识别

302

的具体实现。

‖‖

至此,我们介绍了_些OpenCV的内置方法’了解这些方法怎么用可以让我们更透彻地理解之后



5.缺口识别



』■《‖

现在,我们开始真正地实现缺口识别算法°

首先’定义实现高斯滤波`边缘检测和轮廓提取的3个方法: mport〔γ2



C∧055I∧‖8山R旺R‖[l5IZ[= (5」 5)







ˉI



〔∧‖‖γ丁‖R[5胆[01=2OO





q



de十getˉg日u55ja∩—b1urˉi阳ge(j们age):



retur∩〔v2.田U551a∩81ur(iⅦage’ C∧055I∧‖8山伺Ⅸ[R‖[l5IZ[’ C∧05S1A‖B[0R5IC趴X)



de十8etˉ〔o∩tour5(mage): co∩tou】5’ =cγ2。「i∏d〔o∩tour5(j‖age′〔v2.R[丁R〔〔删p’〔γⅪ·〔肌I‖AppR0X5I"p[[) retUI∩CO∩tOUr5

对3个方法的介绍如下。

」■可|‖叫■‖‖·」■■□‖·』■

de+get_〔a∩∩yˉ1阳8e(i爬ge): retuI∩〔γ2.〔a∩∩y(mage’〔∧‖‖γ「‖R[5‖0[01’〔∧‖‖γ「‖R[5}{0[02)

□getˉgau55ia∩—b1urˉjⅦage:传人待处理图片的信息’返回高斯滤波处理后的图片信息,促512e参

纠·}』■口

数定义为(5’ 5), 5jg刚aX参数定义为O。

□getˉca∩∩y-1‖age:传人待处理图片的信息,返回边缘检测处理后的图片信息’t∩re5ho1d1参

‖■·‖‖‖』《■可

数和thre5ho1d2参数分别定义为200和45o。

□get-co∩tour5:传人待处理图片的信息,返回提取得到的轮廓信息, "ode定义为R[「R〔〔侧P, 们ethod定义为〔‖∧I‖∧ppR0X5I‖p[[°

将原始的待识别的验证码图片命名为captchapng’接下来分别调用以上方法对此图片做处理: j阳ge-mw=〔γ2。mre日d(0captch己.p∩g‖) 1阳ge-hejg‖t’ 1阳ge=Nidt‖’ _=j川age-raw。s∩己pe

我们先读取原始图片,赋值为j"age—r日W’然后获取其宽高信息°接着调用getˉgau551a∩—b1ur-1Ⅷage 进行高斯滤波处理,将返回值赋值为1ⅧaRe屑au55ja∩b1ur。再将加a只e只au5s1a∩b1ur传人 方法进行高斯滤波处理,将返回值赋值为1Ⅷageˉgau55ja∩b1ur。再将j们ageˉgau5s1a∩b1ur传人 8etˉ〔a"∩yˉj"age方法进行边缘检测处理,并将返回值赋给j"age-〔a∩∩y°最后将1Ⅷa8eˉCa∩∩y传人 getˉCo∩tOur5方法得到各个边缘的轮廓信息,将返回值赋值为〔O∩tOur5°

』 ‖ ‖

j‖a8eˉgaU551a∩b1Ur=getˉgau55ja∩-b1ur~m己ge(j爬ge-mw) i阳geˉca∩∩y≡8etˉ〔a∩∩y—i"age(mage—g己u55ja∩b1ur) 〔o∩tour5=get-〔o∩tour5(mage-〔a∩∩y)



d



得到各个轮廓信息后,便需要根据这些轮廓的外接矩形的面积和周长筛选我们想要的结果了°第

—步需要确定怎么筛选’例如我们可以给面积设定—个范围’给周长设定一个范围’另外给缺口位置



也设定-个范围经过实际测量可以得出目标缺口的外接矩形的高度大约是验证码高度的0.25倍,宽

度大约是验证码宽度的0.l5倍°所以在允许误差是20%的情况下’可以根据验证码的宽高信息大约 计算出外接矩形的面积和周长的取值范围°同时,缺口位置(缺口左侧)有一个最小偏移量和_个最 大偏移量’这里的最小偏移量是验证码宽度的0.2倍,最大偏移量是验证码宽度的O85倍°将这些内

q



容综合起来’我们可以定义3个阑值方法: de十getˉ〔o∩touIare日thre5ho1d(j川age-"jdt‖’ i"己ge_∩eig∩t): 〔o∩tour己Ieam∩≡ (mageˉwjdt∩*o.15) * (i川a8eˉhe1ght*0。2S)*α8





retur∩〔o∩touIarea们i∩’〔o∩tour己rea∏a×







| 但●「》|

} 82使用OpenCV识别滑动验证码的缺口

■■「‖|■「′■■『|卜■∏||

de十get_ar〔ˉ1e∩gt∏—thre5∩o1d(加3geˉwidth」 j帕geˉheight):

ar〔≡1e∩gt们—∏i∩≡ ((加日ge-mdth*o。15) + (加己ge-hejg‖t *o.2S))*2 *o.8 arcˉ1e∩8t们ˉ"日x二 ((j∩]age—问jdt∩*α15) + (mage-heig‖t*o。25)) * 2*1.2 retur∩己r〔-1e∩gth-m∩’ aI〔-1e∩gth-们a×

「‖||卜「|||》△尸【|‖■■■『『‖‖尸 ■■

de+get-o仟5etthIe5∩o1d(m日8e-w1dt们): O仟5et爪j∩≡O·2*m日ge_们idth o仟5et们ax=α85*mage-田jdth retuI∩O仟5etⅧi∩’ O仟5etⅦ己X

对这3个方法的介绍如下。

□8etˉ〔o∩touIareatbre5‖o1d:定义目标轮廓的面积下限和面积上限,分别为〔o∩touraream∩

P

和〔O∩tOurareaⅦa×°

‖β‖

‖■‖



■ 厂 | | 「 侈厂

「|》『



303

□get-ar〔1e∩gt∩—thre5∩o1d:定义目标轮廓的周长下限和周长上限’分别为ar〔1e∩gthˉⅦ1∩和 ar〔1e∩gt∩-们ax。

□get工o仟5ett∩re5ho1d;定义缺口位置的偏移量下限和偏移量上限,分别为o仟Setm∩和 O仟5et川aX。

定义完方法’只需要遍历各个轮廓信息’根据限定条件进行筛选,即可得出目标轮廓的信息,实

现如下:

co∩tol』rarea‖1∩’ co∩touIare己∏ax≡get-co∩tourareathre5∩o1d(ma8e—Ⅳjdth’mageˉheig打t)

aI〔ˉ1e∩gthˉm∩’ 日r〔—1e∩gtb-∏a)( =getˉ己r〔—1e∩gthˉthre5ho1d(i阳ge-widt∩’加age-‖ejg∩t)

o仟setm∩’o仟set们日x≡getˉo仟5ett‖Ie5bo1d(mage-mdth)



o仟5et=‖o∩e 千or〔o∩tour1∩co∩tour5:

》(’ y’w’ h≡〔γ2.bol』∩dj∩g偶ect(co∩tour) i千〔o∩touIaream∩〈〔v2.〔O∩touI∧Iea(co∩tour)〈〔O∩tourarea"axa∩d

ar〔—1e∏gth-而i∩<〔v2.ar〔〔e∏gt∩(〔o∩tour’ 丁n」e)〈ar〔ˉ1e∩gt‖—∏{己xa∩d o仟5et∏i∩〈x〈o仟5etⅧax吕

cγ2.re〔ta∩g1e(mage—ra"了(x』 y)’(x+"’γ十∩)’(0’0’ 255)′ 2) o仟5et=x

cv2.jⅧrjte(0i爬geˉ1abe1.p∩g! ’ j∏己ge-ra"〉 prj∩t(‖o仟5et0 ’ o仟set)

这里我们首先调用getˉ〔o∩tourareathIe5∩o1d、getˉaI〔1e∩gthˉt∩re5ho1d和getˉO仟5ett‖res∩o1d

方法获取3个判断阂值’然后遍历〔o∩tOur5并根据这些阑值进行筛选,最终得到的X值就是目标缺口 位曾的偏移量,将其赋给o仟5et变量并打印出来·与此同时’我们调用re〔ta∩g1e方法对目标缺口的 外接矩形做了标注’将其保存为image-labelpng图片。 代码的运行结果如下: o仟5et163 一

同时输出的image-labelpng文件如图8ˉl5所示。

蘸■…

这样我们就成功提取出目标缺口的位置了,本节 的问题得以解决° 6.总结



‖ ˉ…. 器



本节中我们介绍了利用OpenCV技术识别滑动验 证码缺口的方法,其中涉及一些关键的图像处理和识

|″/^」r司参0!

别技术—高斯滤波、边缘检测`轮廓提取等算法°

我们也可以举一反三,将这些基本的技术应用到靴

[坠‖』k人寺二-ˉl义≡, 铲『可」△=凸兰而→‖√』央/‖→丛工=′00←甲≥、0…

的工作中,也会很有帮助° 类型的工作中,也会很有帮助°

图8ˉl5输出的image_labelpng文件



本节代码见https://glthuhcom/Python3WebSpide【/CrackSljdeCaptcha/tree/cv’注意这里是cv分支。 Q

■◎



■■]■■司|



』■‖‖‖|」■■】|』|」■∏‖||』■■■

304

第8章验证码的识别

8.3使用深度学习识别图形验证码

对基本的图形验证码的识别和对滑动验证码缺口的识别° 本节中我们先学习使用深度学习识别图形验证码的方法。 ↑.准备工作

{』■】‖〗〗】』■■】|■■」‖‖●』■‖

在8.l节和82节,我们了解了使用OCR技术和图像处理技术识别验证码的方法’但这些方法有 个共同的缺点,就是正确率不够高°从本节开始’我们来学习使用深度学习识别验证码的方法,包括

= ■ Ⅵ

pip〕1∩5t日11tor〔htor〔‖vjs1o∩

如果安装过程出现了问题,可以参考h忱ps://setupscrape.center/pytorch了解更详细的安装说明。

另外,由于本节需要使用深度学习训练一个识别图形验证码的模型,因此还需要准备—些训练数

据°训练数据又包含两部分’_部分是验证码图片,另—部分是验证码的真实标注°我们可以使用验 证码生成器自行生成_些验证码,这样就同时有了验证码图片和标注数据°生成验证码图片需要用到 —个叫作captcha的Python库’可以使用pip3工具安装这个库:

|〗】■■■‖||」引」■■■‖‖』‖』■]己■]‖‖□‖■|』■‖‖‖』司|□』■■〗‖』■℃』‖』■

由于本节所讲的内容涉及深度学习相关的知识,所以在开始之前’请确保已经正确安装好了—个 深度学习框架PyTbrch’可以通过pip3工具安装:

‖{|‖■■可

p1p3 1∩5t311〔aptC打a

』』■】‖』■■

= 另外由于本节涉及的知识点都和深度学习模型的构建`训练`验证和推理等过程相关联,同时伴 有数据集的准备过程导致代码量比较大;而我们的目标是训练出—个能够识别图形验证码的深度学 习模型为爬虫所用,并不是侧重学习深度学习的原理’因此本节我们不会从零开始编写一个深度学习 模型,建议大家直接下载代码跟着运行_遍即可,有兴趣的话可以深人研究其中的原理。

】]■√‖划

这部分代码见https:〃gjthub.com/Python3WebSpldeI/DeepLeaminglmageCaptcha’先复制下来: gjtc1o∩ehttp5://gjthub。〔o‖/pyt∩o∩3‖eb5pjder/0eep[ear∩j∩gI们age〔apt〔ha.g1t

运行完毕后,本地就会出现-个DeepLeam|nglmageCaptcha文件夹,证明复制成功°

』■]|

』■√‖|

2ˉ数据准备

别出这个验证码对应的文本内容°

那这些数据怎么准备呢?如果你稍微了解过深度学习相关的

』■

·』·

内容’相信并不会对数据标注这个词感到陌生,数据标注有相当_ 部分是需要人工参与的。假如我们有很多验证码图片,又不知道验 证码图片对应的文本内容是什么’就需要用到数据标注°说白了,

看-下验证码图片’然后把里面的文字记录下来,就相当于标注了

』』■■‖|』□■■

部分是图片数据,即—张张验证码图片,另—部分是标注数据,即验证码的内容是什么°有了这两部 分数据,就可以训练一个识别图形验证码的深度学习模型,模型在训练中不断调优的过程’就是逐渐 学会怎么识别_张验证码图片的过程。训练好模型后,向模型输人类似的验证码图片,模型便可以识

|』Ⅵ■可』■■

要训练-个深度学习模型’必不可少的就是训练数据°上面也提到了,训练数据分为两部分’-

p

图8ˉl6一张验证码图片

_条数据°例如这里有一张验证码图片,如图8ˉl6所示。

现在我们只是有这张图片,而没有图片里面内容对应的文本信息’这时候就需要标注°可以看到’ 咖位讥‖』屈运伺必派因厅,Ⅶ汉伺圈斤里回内吞对应的文本信息’ 图片里的内容是EHUK,把它记录下来’这张验证码图片的标洋就字 图片里的内容是EHUK,把它记录下来’这张验证码图片的标注就完成了°





■■]』』■■■·‖||』■]·Ⅵ

为了训练-个较好的用于识别图形验证码的深度学习模型’我们可能需要上万、上十万或者数百 万条训练数据°此时如果只有验证码图片而没有数据标注』就需要人工标注上万、上十万甚至数百万





「 p

8.3使用深度学习识别图形验证码

305

份『}|卜|■■=『=■||■「β|‖『■「}‖协『■|=尸·「「●「『厂‖|)|β}·俱′▲卜『卜但尸|‖|■「卜■■|尸|)「卜|『|■厂|》》|》卜‖■『|■■〖【■「|■■「|●[|[■『‖‖【厂|卜↑『|卜小

条数据,这是个非常枯燥且耗时的工作。

那有没有解决办法呢?办法自然是有的’我们反其道而行之就好了。具体是先随机生成标注数据’ 即随机生成—些4位的由字母和数字组合而成的数据,然后使用这些已经生成的标注数据生成验证码 图片,这样不就省去数据标注的过程了吗?有了这个方法’就不怕准备大量的训练数据了°

上述生成验证码的过程分为两步’第_步是生成随机的文本数据,第二步是根据生成的文本数据

生成验证码图片°打开项目中的generatepy文件,其中定义了两个方法ge∩erate—capt〔baˉtext和 ge∩emte-〔日pt〔‖a-textˉa∩d-i们age,分别用于完成这两步: 十ro∏〔apt〔h己.mage加portI『∏ageCaptcha +ro们pI[mport I‖age i们portr己∩doⅧ jⅦPOIt5ettj∩g

de十ge∩erate〔apt〔∩a=text(): 〔apt〔ha-teXt= [] +OIij∩ra∩ge(5ett1∩g°‖M-〔∧P丁〔肌): 〔=ra∩do‖.〔∩o1ce(5etti∩g.∧Uˉ〔‖∧R5[丫)

|re汕::呼‖捌自;删1{:lt) de十ge∩eIate-〔aptcha-textˉa∩d一j∏E8e(): mage≡I帕ge〔aptC‖己() 〔己ptc∩a-text=ge∩erate-captc∩a-text()

|删删硼铡鳃………)

这里ge∩eIateˉ〔apt〔们aˉteXt方法用于生成随机的文本数据’可以看到方法中先定义了_个执行



8 h

‖∧X〔AP丁〔‖∧次的「or循 环’每次循环都利用m∩do们.〔ho1〔e方法随机从∧[[〔‖∧∩5[丁里挑选一个字

符并放人〔apt〔‖a—teXt’ 最后将CaptC∩aˉtext中的字符拼接在_起° 其中‖∧X〔Ap丁〔‖∧和∧[[〔‖∧R5[「的定义在scttingPy文件里,相应代码如下 |30 ’’ 。』, 』』! ’’ ,5‖ ’ ‖6‖ ’ 』7,’ |8,’ ‖0‖8[p≡ [』O|’ 01』’ 02, ’ `3′ .5.’6’7’ ˉB ’ ‖90 9 ] 」

∧[p‖∧8[「= [ ‖∧| ’ 08‖ ’ 0〔|’ 00! ’ ‖[, 」 ‖「』’ ‖G′ !‖0 ’ !r’ 』〕` ’ {Ⅸ|』 ‖[0 ’w’ 0‖` ’ ‖0‖ ’ |p! ’ 0Q0 ’w’ 05』’ 0丁』’ ‖U0 ’ ‖γ‖ ’w’ 』X! ’ |γ‖ ’ |Z‖]

∧[[〔‖∧R5[T=‖0‖8[R+∧[p什∧8[丁



ˉ

可以看到‖∧〉〈〔∧p「〔‖∧的值是4’所以最后拼接而成的就是4位验证码°∧[[〔‖AR5[『的值是10个 阿拉伯数字和26个英文字母构成的列表,所以拼接而成的验证码文本就是4位数字和字母的组合° 接下来我们修改generatepy文件,先生成一批训练数据,建议生成l0万个验证码’按如下这样 修改〔ou∩t变量和Path变量: 〔Ou∩t=10OO0o

p日tb=5ettj∩g。丁R∧I‖-0A丁A5[丁PA丁‖

其中cou∩t就是验证码的个数,这里直接设定为1OOOOO, patb是验证码图片保存的路径’这在seningpy 文件中已经定义好了,有如下三个选择°

□丁R∧I‖0A『∧5〔「p∧「‖:训练集所在的路径’数据用于模型的训练。

□[γA[D∧「∧5[丁p∧丁‖:验证集所在的路径,一般在训练过程中或者训练完毕后用到’可用于验 证模型的训练效果°

□pR[0I〔丁0∧丁∧5[『p∧「‖:推理集,一般在训练完毕后用到’可用于模型推理和测试。 然后运行generatepy文件里的代码: pyt∩o∩3ge∩emte·pγ

输出结果类似如下这样:





■司‖山可]

306

第8章验证码的识别



5己γed1ooo : U儿1〔162o1o65』7.p∩g



这里生成了非常多的验证码数据’标注结果直接出现在文件名中’最终生成的训练集数据如图8ˉl7

■■■■■■‖■■■∏」■

5己γed1 : X∧PA16Ⅺo1o6547·p∩g 5己γed2 : 「6ZO162O106547·p∩g

所示。 ●●● 〈

朗e

〉…

疆营色°·萄 》Q q

鲤醚唾鳃峰舅醒圣醚j O丁5层〕62饥呻6Q0z瓜L】6200…0B陋申〗5…3泡7 ↑“儿L…mm2吨]·2m醒Q正刚室]m创… o酮

O…

巳…

缝鲤癣″醚寸『i

赣瓣

日‖

又…

a…

■翱

5蹿瘦

汕‖C4G2o0““2丛肘=】6m0凹6△ Ⅱ刽儿j6酌O3汹7 2丁w」砌的w972】WⅥ=〗“曲373四pLj·2o‖帕6▲8 a口叮 a呵 ◎… O·吨 沁碗 .… ‖

邑日‖』

鲤上…囊墨醒k锤童

鲤鲤i

mS刊」6m↑O66q□帕旧刨」53■9》35冲0」mO0侧β▲7唾2认]m◎徊“酗匪…0“御5γ加』02刨归“ 池…

G…

□网

□…



0…

】‖■■■

!哩i…鳃幽…聋憋! 》Q」Ⅵ早β620↑0酗γz肌■旧和…Q呻L归20刘…0Z■S妈沁吨“虾飘』·2m呻■叫VQ」O测0“5 a″g a户吧 a阻℃ ●p℃ ·■叼 ▲巳户叼 』

·□』β

图8ˉ17生成的训练集数据

利用同样的方法,可以生成验证集’用于验证模型的训练效果°还是修改〔Ou∩t变量和path变量: 4

〔Ou∩t=〕o0O

pat∩=5ett1∩g.[γ∧儿-0∧『∧5[丁_pA丫‖

验证集不需要像训练集那么大’〔ou∩t就修改为3OOO’pat∩修改为[γ∧L0∧「∧5[丁p∧「‖,然后重新

勺‖

运行generatepy文件里的代码: pytbo∩3ge∩erate。py

‖』‖

这样就生成了验证集数据,如图8ˉl8所示。 《

● ● ●



{ =但

二■∏||』●

|{



醒……蜒……唾≡……………

,吨

阑彰色°e. >Q

鹤……鲤…………………



……吨≡……………秘→…



昭◎

哩……蝉……唾………_…

鲤……峰…呵垫……锤…

………鲤……………诬… .酮

》…

°… 』

图8ˉl8生成的验证集数据

|勺‖」|

数据都准备好后,就可以开始训练模型了°





83使用深度学习识别图形验证码

307

γ P



3.模型训练

本节中我们使用的深度学习模型是一个基本的CNN模型’模型定义在modelpy文件里’定义 如下:











mpOrttOr〔们.∩∩a5∩∩ 加pOrt5ettj∩g 〔1aS5〔‖‖(∩∩.‖odu1e): de+ j∩it (5e1十): SUPer(〔‖‖’ 5e1+)。—j∩1t—() Se1十.1ayer1≡∩∩.5eqlle∏t1a1( ∩∩.〔o∏v2d(1’ 32’ Ⅷer∩e15ize=3’ paddj∩8=1)’ ∩∩.8at〔h‖om2d(3∑)’

∩∩DrOpOut(0·5)’ ∩∩.Re山()’ ∩∩."axpoo1∑d(2))

p







P



5e1+.1ayer2≡∩∩.5eql」e∩tja1( ∩∩·〔o∩γ∑d(32’ 64’ ker∩e151ze=3’ paddj∩g=1)’ ∩∩.8at〔∩‖om2d(6q)’ ∩∩。0ropout(0。5)’

p

P















∩∩.Rel0()’ ∩∏.∩axpoo12d(2))

5e1+.1ayer3=∩∩。5eque∏tja1( ∩∩.〔o∩γ2d(6q’ 64’ Rer∩e15jze=3’ paddi∩g=1)’ ∩∩。8at〔∩‖om2d(64)’ ∩∩.0ropol』t(O。5)》 ∩∩.【e山()’ ∩∩.陶xpoo12d(2)) 5e1千.+C≡∩∩,5eque∏tia1(

∩∩。[j∩eaI((5ettj∩g.I∩M[‖IDⅧ//8) * (5ettj∩g·IⅧC[‖[I阳『//8)*64’10叫)’

广

∩∩.0ropout(0.5)’

p

∩∩.Re山())

p

5e1「.r千〔≡∩∩.5eque∩tja1(

∩∩儿i∩e日r(1o∑4’ settj∩g."AX-叭p丁O{∧*5etti∩g.∧l[ˉ〔‖∧Rˉ5[丫=[[‖)’



















de++oINard(5e1十’ X): out≡5e1十.1ayer1(x) out≡5e1千.1aγeⅢ2(out) out=5e1十.1ayer3(out) out=out.vi锄(out。5ize(O)’ -1) Out≡5e1于。十〔(Ol』t) Ol」t=5e1十。r十〔(Out) retuI∩Out

b





{看到这里定义了三层·每层都是〔o∩γ2d(卷积)、 Bat〔‖‖or川2d(批标准化)、0ropout(随机 Bat〔‖‖or川2d(批标准化)、[ 可以看到这里定义了三层,每层都是〔o∩γ2d(卷积)、 失活)、【e儿0(激活函数)和阳xpoo12d(池化)的组合,经过这三层处理后,由—个全连接网络层输 出最终的结果,用于计算模型的最终损失。

p

模型的训练过程定义在nEjn.py文件中,整个训练逻辑是这样的:





r



仿



} p





(1)引人定义好的模型,即modelW文件,对模型进行初始化; (2)定义损失函数1o55; (3)定义优化器optjmzer;

(4)加载数据’_般包括训练集数据和验证集数据; (5)执行训练,这个过程包括反向求导、模型权重更新; (6)在执行完特定的训练步数后’验证和保存模型°



了解了基本的逻辑之后,就可以尝试用现有的数据训练一个深度学习模型了°怎么训练呢?运行 训练模型’即tmin.py文件即可: pγtho∩3traj∩。py

0





P

‖}





·ⅢⅢ=■■■■‖■】口二■■■■·Ⅺ`ˉ■■□·□□‖』|刑咱日|■|■□可」□□□ˉ■■■■■■■』■■■■】■‖]』句|||

第8章验证码的识别

308

训练过程的输出结果如下: epoc∩: 05tep$ 91o55: 0°2O04123O32O93O48 epo〔‖; 05tep: 191os5: o·15169423818588257 epoc∩: o5tep; 291o55吕 0。141016o2137088776

epo〔∩: 1O5tep: 1O91os5: o。o20418383o261237 epo〔h: 10step: 1191o55: O.O216972048472911 epo〔∩: 』05tep: 1291oss$ o·o21o623586177826



可以看到,训练过程中模型的损失在不断降低,说明模型在不断地学习和优化’同时每训练完一 个轮次之后都会执行_次模型验证°由于在训练模型时没有使用验证集数据’所以用验证集数据来验 证是可以得到模型的真实正确率的’是更为科学的° ■

0

另外在每次的验证过程中’还会保存最优的验证结果以及最优的模型’存为bestmodelpkl文件°

‖△



注意推荐使用GPU来训练模型’速度会比不用GPU快很多°关于女口何设置用GPU训练模型’可

|{





以参考PyT℃rch官方教程,这里不再赘述。



经过_段时间的训练,模型的损失趋近于0’训练的正确率在不断提升’验证的正确率能达到96%

以上,最后可以在本地看到—个bestmodelpkl文件’这便是我们想要的模型° 4.测试

现在我们来测试_下得到的模型’先在pR[0I〔丁D∧丁∧5[丁p∧「‖变量



对应的路径下生成几个验证码图片,生成过程前面讲过也实践过°这里

然后在predjctpy文件中加载上面得到的模型bestmodelpkl’关键

凸■‖‖‖■■

随意选取两个验证码图片放在那个路径下,如图8_]9所示。 (a)

代码如下: 征°◆



〔∩∩.1oad5t3tedj〔t(torch.1oad(|be5tˉ川odeLp代1|))

(b)



| 《

■■□曰‖□■司

运行predictpy文件:

&‘卢



并根据加载的模型对定义的CNN模型的权重进行初始化,整个模型 加载完毕后,就和刚才训练时_样强大’拥有识别图形验证码的能力。

?卸

图8ˉ19选取的验证码图片

pγtho∩3predj〔t·Pγ



可以看到输出结果如下: 「I0C I‖65

■■司

‖||』』的‖|||‖‖勺|

识别成功!这样我们就成功训练出了—个识别图形验证码的深度兰 习模型° 识别成功!这样我们就成功训练出了—个识别图形验证码的深度学 5.总结

| □ ■ | 』 ■ ■ 】 | | 』 司 ‖ ‖ | · ■ ■ 】 ■ | 』■』〗|』·】』‖]』(‖

本节介绍了利用深度学习模型识别图形验证码的整体流程’最终私 本节介绍了利用深度学习模型识别图形验证码的整体流程’最终我们成功训练出模型’并得到了 一个深度学习模型文件。往这个模型中输人一张图形验证码.它傅会刊 深度学习模型文件。往这个模型中输人一张图形验证码’它便会预测出其中的文本内容。 至此’我们还可以基于本节介绍的内容进行进_步的优化。

□由于本节各个环节使用的验证码都是由captcha库生成的’验证码风格也都是事先设定好的’

所以模型的识别正确率会比较高°但如果输人其他类型的验证码,例如文本形状`文本数量` 干扰线样式和本节的不同’模型识别的正确率可能并不理想。为了让模型能够识别更多的验证 码’可以多生成_些不同风格的验证码来训练模型,这样得到的模型会更加健壮。

二■●』■■

■ ■





84使用深度学习识别滑动验证码的缺口

309











□当前模型的预测过程是通过命令行执行的’这在实际使用时可能并不方便°可以考虑将预测过 程对接API服务器’例如对接Flask、Django、FastAPI等把预测过程实现为_个支持POST 请求的API’这个API可以接收~张验证码图片,并返回验证码的文本信息这样会使模型更 加方便易用。

本节代码见https://github.com/Python3WebSpideⅣDeepLeamingImageCaptcha。









k





p

8.4使用深度学习识别滑动验证码的缺口 上一节中我们使用深度学习完成了图形验证码的识别过程’正确率和使用OCR技术相比,高了

非常多。这时可能有朋友会说, 82节不是还介绍了一种使用图像处理技术识别滑动验证码缺口的方 法吗?深度学习可以用在这种场景下吗?

当然可以’本节中我们就来了解使用深度学习识别滑动验证码缺口的方法°

b



0

p

b





p



↑.准备工作

和83节一样’本节主要侧重于介绍利用深度学习模型识别滑动验证码缺口的过程,不会深人讲 解深度学习模型的算法。另外由于整个模型的实现较为复杂’故不会从零开始编写代码,而是倾向于 提前把代码下载下来进行实操练习。代码地址为https;//gjthuhcom/Python3WebSpider/DeepLeamjng

SlideCaptcha2’还是先把它复制下来: gjt〔1o∩e∩ttp5://git∩ub°〔o们/pytho∩3Neb5pider/Deep[ear∩1∩g51ide〔己pt〔∩a2.gjt

运行完毕后’本地就会出现_个DeepLeamingImageCaptcha2文件夹,证明复制成功。之后,切 换到DeepLeamjngImageCaptcha2文件夹,安装必要的依赖库:







pip3 1∩5ta11 ˉrrequire∏e∩t5。txt



0

运行完毕后,本项目所需的依赖库就全部安装好了。

p

b



2.目标检测

〖卜β△β■=■』∩口‖

识别滑动验证码的目标缺口问题’其实可以归结为目标检测问题°什么叫目标检测?这里简单介 绍-下。目标检测’顾名思义就是把想找的东西找出来’例如这里有_张包含狗的图片’我们想知道 狗和狗的舌头在哪儿’如图8ˉ20所示°

先找到图片中的狗和狗的舌头’再把它们框起来’就是目标检测。我们希望经过目标检测算法处 理后得到的图片如图8ˉ2l所示°







[ ■

尸 蝉 逆

腰 ■『 ≡尸ˉ

≤】 ′

狼■

β



q卜













」 『

ˉ

■儿



、 鱼咀`





}} 「 } } } }

》|



D ■

』″





图8ˉ20示例图片



=『

■司



0 臼

陵公】丛‖谷白



输矿』『、]`·蠕融刮·=





p 民=凶

| 『 }



茁…ˉ■





7≡

≈ˉ







』 扎旷』《 p



硼盅

0

p

『■P盼

j瓣



图8ˉ2l 目标检测处理后得到的图片

现在比较流行的目标检测算法有RˉCNN`FastRˉCNN、FasterRˉCNN、SSD、YOLO等’感兴趣 的话可以了解一下’当然就算不太了解对学习本节也不会有影响。

d

(日‖(

3l0

第8章验证码的识别

□OneStage:不需要产生候选框’直接将目标的定位和分类问题转化为回归问题’俗称‘看眼”,使用这种实现的算法有YOLO和SSD,这种方法虽然正确率不及TWoStage,但架构相

{|

目前目标检测算法主要有两种实现方法_-万一阶段式和两阶段式’英文叫作OncStage和TWoStage。

对简单,检测速度更快。

□TWoStage:算法首先生成—系列目标所在位置的候选框,再对这些框选出来的结果进行样本 FastRˉCNN、FasterRˉCNN等.这种方法架构相对复杂,但正确率高°

乙■■Ⅵ‖』勺‖

分类,即先找出来在哪儿’再分出来是啥,俗称“看两眼,,’使用这种实现的算法有RˉCNN`

这里我们选用YOLO算法实现对滑动验证码缺口的识别。YOLO的英文全称是YOuOnlyLook 兴趣的话可以搜_下相关资料。另外’可以了解一下YOLOVl~V3版本的不同和改进之处,这里列

‖|

Once’ 目前的最新版本是V5,应用比较广泛的版本是V3’算法的具体流程我们就不过多介绍了’感 几个参考链接°



3.数据准备

钮就会弹出一个滑动验证码’如图8ˉ22所示·

单独将图8ˉ23中框起来的区域保存下来,就收集了_张验证码图片。

图8ˉ22示例网站的滑动验证码

№…牢整恃

图8ˉ23要保存的区域

怎么保存那个区域呢?手工截图肯定不可靠,因为要收集的图片量非常大,这么做不仅费时费力’ 还不好准确地定位边界,会导致保存下来的图片有大有小°为了解决这个问题,可以简单写—个脚本 实现自动化裁切和保存,就是之前下载的代码仓库中的collect.py文件,其内容如下:



@◎o

{」

潞泪

‖|』∏』】‖‖』』□∩□∏||■■】‖°‖∩

o◎o……中m

q

‖‖=』

嚷-

|』■』□』■■当·■

明确了数据是什么’接下来就着手准备吧。第_步是收集验证码图片,第二步是标注图片中的缺

口位置并转为化想要的4位数字°举个例子,打开网站h叮s:〃captchal.sc『apecenter/,点击“登录”按

【′`"。=。■■『■■【■厂■

和83节_样,训练深度学习模型需要准备‖||练数据。这里的数据也分两部分’—部分是验证码 图片,另_部分是数据标注°和83节不_样的是’这次的数据标注不再是单纯的验证码文本,而是 缺口位置,缺口对应一个矩形框要表示矩形框,至少需要4个数据,如矩形左上角的点的横纵坐标 x、’加上矩形的宽高w、力°

叮 ■ 日 〈 」 ■ ■ 】 ‖ □ ‖ ■■·』■

□YOLOV3的论文: https://pjreddie.com/media/∏les/papers/YOLOv3.pdf° □介绍YOLOV3: h忱ps://zhuanlanzhlhu.com/p/34997279° □YOLOVl~V3版本的对比: https:〃www.cnblogs.com/makehle/p/yolov3.html°

) 8.4使用深度学习识别滑动验证码的缺口



3ll

)β似[

>『{‖|■■■‖匹『巴■「

「roⅦ5e1e∩iuⅧiⅦportⅣebdriγer fro‖∏5e1e∩j0们·webdr1γeI.〔o∏∏『℃∩·bymportBy +ro们5e1e∩iuⅦ。webdrjγer.5upport°ujmport‖ebDmver‖ajt 千ro爪5e1e∩juⅦ°"ebdriγer°5upport加poItexpectedco∩ditjo∩5a5 [〔 「ro们5e1e∩iu‖°co‖‖m∩。exceptjo∩5i‖port‖eb0r1γer[x〔eptio∩ iⅧpOrtti爬 千ro们1ogur0i呻oIt1ogger 〔α」‖丁=1咖

+OIjj∩Ia∩ge(1’〔"‖丁+1): try:

brow5er≡"ebdriγer.O]Iα爬() 训日jt=‖eb0rjγe瑚ait(br咖5er’ 1o)

bro切ser.get(‖∩ttp5;//capt〔‖a1.s〔rape.〔e∩ter/|) 甘■■「■■巳■厂|‖‖

butto∩≡"日jt.u∩ti1([〔.e1e∏记∩ttobe〔1i〔低ab1e(

(8y.〔555[[[〔『0β’ ‖ .e1ˉbutto∩!))) butto∩.〔1i〔促()

■■『|}■厂邑■厂|》|■■『‖[■尸|巴■■β‖卜‖

〔aptC怕="3jt.(』∩ti1( [〔.pre5e川〔eo+e1e爬∩t1o〔ated((8y。〔555[l[〔『0R’ 』 .8eete5t51i〔ebg。geete5tab5o1ute0 ))) tme。51eeP(5) 〔aptcha。5〔ree∩5‖ot(+|data/capt〔ha/mage5/〔己ptc‖a{j}.p∩g0) except"ebDIjγer[x〔eptjo∩a5e:

1ogger.error(+WebdrjveIerroroccu∏ed{e.Ⅷsg}0) 十i∩a11y: bIo切5er.〔1o5e()

■■=■■■=■■■■■■■■■■『吐■『■=====■=ˉ■■■■■■=■ˉ■■

代码中先是_个十or循环’循环次数为〔00‖「’每次循环都使用Selenjum启动一个测览器,然后 打开目标网站,模拟点击“登录”按钮的操作触发验证码弹出,然后截取验证码对应的节点’再调用 5〔ree∩5hot方法保存下来。



运行collectPy文件: pγtho∩3〔o11ect.py

运行完后’data/captcha/images/目录下就会出现很多验证码图片,例如图8ˉ24所示的这样。 @彰α

圃≡■…■_…■……

■…■≡■≡■…州

■彰色◇e唾

址 地 载 下

■=■…■≡■≡…辨窿

朗e

■…■_…■…■≡………

团…■≡■—…■……舜…

)…

■…■当■~凹_………吧

步·凹加

籍闷呻



■…Ⅷ_≡■≡■≡………

●●● 〈



‖(

‖ 第8章验证码的识别

3l2



安装完后可以直接在命令行运行:

□·■□■■|■■

1abe1I们g

这样就成功启动了labelImg工具’如图8ˉ25所示° 础丝

醛℃

!…哟



甲≈艘

p !

".…ˉ.

.典

■砒贮嘛

←企

…翘·帕议;唾刨|

翼嚣



--——--—二雨皇

‖』



^= }

睦压丝





磺豁 衫

肄辙…亏ˉ″矿亏ˉ_三锋岂:F…宁……ˉ~ P.绳 」b

………

图8ˉ25

启动了labelImg工具

点击左侧的OpenDlr按钮打开data/captcha/images/目录’然后点击左下角的CreateRectBox创建 target’然后点击OK按钮’如图8ˉ26所示°

… 军…

侈澎 G…陆





起也

◆…◆…酶

ch翱Q…■酗 巷攫郸黔吼由

崭瓣撼 ■





止出

飞…尸



.霹

)拘





Fb





‖呛‖



……















图8ˉ26将缺口所在的矩形框保存为target

|‖』‖■`

叮斟键■卫宁吵旧′…

咐■





…ˉ醒…弓





;…ˉ

笆 ■ ` 凰 霄 镭 舟 萨 研 曲 ″ 』 罚 Ⅷ





诌=…=…唾≡…■蹿嚼



=■≡厂《翻蹿抒僻乌

薛v…启`

№『— 酗 『

″酣…

·‖‖、』■□□□□‖·■□‖·司■Ⅵ‖

彤′歹

』‖』·■■二■■

_个标注框,可以将缺口所在的矩形框框起来’框完后labelImg会弹出填写保存名称的提示框填写

‖‖‖‖(‖‖‖·《‖·‖|

嚷$.镭!

‖‖■可

!

一点!

‖‖《





【∩‖‖

「 8.4伎用深度学习识别滑动验证码的缺口

3l3



~‖

这时会发现本地保存了—个xm‖文件,内容如下: 〈a∩∩OtatiO∩〉



≤十o1der〉1肌日ge5〈/十o1deI〉 〈千j1e∩3们e〉Capt〔ha-0。p∩g〈/于j1e∩a『∏e〉

「}

卜|

〈path〉data/〔日ptc∩a/m日ge5/〔apt〔h己-0°p∩g</path〉 〈SOurCe〉

〈datab己5e〉‖∩k∩ow∩</databa5e〉 </5OuICe〉 〈51Ze〉

〈Njdth〉52O</wjdth〉 β卜『|》▲尸‖●■■厂■『■尸仿|■厂■=■厂‖■

‖} =》‖|仆■=仁)卜尸β

卜·ˉ尸》■|》‖尸【■〖‖■尸■尸■■厂|「|‖【尸『巴■=■尸●■「|‖|~■■「||△■■



<hejg∩t〉32O〈/heig∩t〉 <dept‖〉3〈/depth〉 〈/5jZe〉

〈5e8爬∩ted〉o〈/5egⅦe∩ted〉 〈obje〔t〉 <∩a爬>target</∩a爬〉 〈pO5e〉0∩5peCj「ied〈/pO5e〉 〈tru∩〔ated〉O〈/tIu∩〔ated〉

〈d1仟iCl』1t〉0≤/dj仟i〔u1t〉 〈b∩dbox〉

(x∏j∩〉321≤/X∏i∩〉

<ym∩〉87</yⅧi∩〉 <xⅧaX〉407〈/X川aX〉

<Ⅶa×〉167〈/γ∩ax〉 〈/b∩dbox〉

〈/obje〔t〉 〈/日∩∩Ot己tiO∩〉

从中可以看到’51xe节点里有三个节点,分别是wjdth、he1g∩t和depth’代表原验证码图片的 宽度`高度和通道数°obje〔t节点下的b∩dbox节点包含标注的缺口位置’通过观察对比可以知道xm∩、 γm∩指的是左上角的坐标’ xⅦaX、ⅦaX指的是右下角的坐标。 我们可以使用下面的方法简单做_下数据处理: 加portx"1todjct mportj5o∩

de+p日r5eˉx‖1(十j1e): . x"1str≡ope∩(+j1e’ e"〔odj∩8≡‖ut+ˉ8!).read() data=x们1todi〔t.par5e(咖15tI) data≡j5o∩.1oad5(j5o∩.d0呻5(data)) a∩∩oatatio∩=data.get(0a∩∩ot己tjo∩|) 切jdth=j∩t(a∩∩oatat1o∩.get(|51ze0).get(|width!)) ∩ejght≡j∩t(a∩∩oatatjo∩.8et(!5jze!)°get(0hei8‖t0)) b∩dbox≡a∩∩oatatjo∩.get(|object0).8et(b∩dbox`) boxxm∩≡i∩t(b∩dbox.get(|xm∩‖)) boxx‖Ex≡1∩t(b∩dbox.get(‖x爬×0)) boxˉym∩= i∩t(b∩dbox。get(』ym门!)) box-y帕x=i∩t(b∩dbox.get(!yⅦax|)) box"jdth≡ (boxˉx川axˉbo×x们j∩)/wjdt∩ box一height≡ (boxˉyⅧaxˉboxˉym∩〉/∩eight retur∩boxxm∩/width』 boxˉy∏‖i∩/‖ejg∩t’ boxwidt∩/widt∩’ box-‖eight/heig∩t

这里定义了一个par5eX∏1方法’这个方法首先读取Xml文件’然后调用XmltOdiCt库里的Par5e方 法将XML字符串转换为JSON字符,之后依次读取验证码的宽高信息和缺口的位置信息,最后以元

组的形式返回我们想要的数据—缺口左十角的点的坐标和宽高的相对值°都标注好后’对每个xml 文件都调用一次此方法便可以生成想要的标注结果了。

我已经将对应的标注结果都处理好了’大家可以直接使用’结果的保存路径为data/captcha/labels’ 如图8ˉ27所示。



3l4

第8章验证码的识别 O彰Q

蹿潭的Qo彰

当■Ⅷ`|』■】■Ⅵ

=西一…■洒一…

酞顾

口瘁_…乙沁—…

【匝

ˉ囚……■逐…

撬…勤…

■≡…■|≡…

…撼…翻…

…叠…嗡=

朋$

嗡竖 团硼匠二志 c印哟a」▲m

■一

h

〔翅镭 …·℃=m.饭t

凹α印叫mn



C印tc恤ˉ】6iⅢ↑

c·p0Cm」7mt

匈…h■」aml

cGptC恤』9tⅢt

c■mc『田=2Oln

硒鞠廷豆登

h

ˉ丝≈. …c咆22颐

c■咋m23m

c·α怎m=20·m

c…恤=25.m

c■倾c帕=26曝tⅢt

…tc饰父7.tⅫ

图8ˉ27对所有xml文件的标注结果

其中每个txt文件各对应_张验证码图片的标注结果,文件内容类似这样: 0O·615384615384615qO·2750°16S967740.2417O968

第—位0代表标注目标的索引’由于我们只需要检测_个缺口,所以索引就是0;第二位和第= 位代表缺口左上角的点在验证码图片中所处的位置’例如06153846l53846l54代表从横向看’缺口左 上角的点大约位于验证码的615%处,这个值乘上验证码的宽度520得到320,表示左上角的点的偏 移量是320像素;第四位和第五位代表缺口的宽高和验证码图片的宽高的比,例如用第五位的 024l70968乘以验证码图片的高度320得到大约77,表示缺口的高度大约为77像素° 至此’数据准备阶段完成° 4.】‖练

为了达到更好的训练效果,还需要下载一些预训练模型’预训练的意思是已经有_个提前训练好 的基础模型了。直接使用预训练模型中的权重文件,就不用从零开始训练模型了,只需要基于之前的 模型进行微调即可’这样既节省训练时间,又能达到比较好的效果°

先下载预训练模型,YOLOV3模型才能有不错的训练效果。下载预训练模型的命令如下: ba5hpIep己Ie.Sh

注意在Windows环境下’请使用Bash命今行工具(如GitBash)运行此命今°

之后’就能下载YOLOV3模型的-些权重文件’包括yolov3wejghts和darh]et.weights。在正式 训练模型之前’需要使用这些权重文件初始化YOLOV3模型。

接下来就是训练了’还是推荐使用GPU来训练,运行如下命令: ba5htraj∩.5h

注意同样’在Windows环境下请使用Bash命令行工具(如GitBash)运行此命令°

在训练过程中,我们可以使用TmsorBoard观察loss和mAP的变化’运行如下命令: te∩5orboardˉˉ1ogdjr=‖1og5‖ ˉˉport=6佣6-‖o5tα0。o·o



}|}}

84使用深度学习识别滑动验证码的缺口

3l5 —□

注意请确保已经正确安装了本项目的所有依赖库’其中就包括ImsorBoard’安装成功之后便可以 使用te∩5orboard命今。

运行后,打开http://localhost:6006观察’ loss的变化和图8ˉ28类似°

h函〕

唾ˉm妒

匣】 唾…?



_=≈=-一■—≈=—←-—_~~≡■■

唾吐…

=丁.ˉ-

α8 oQ

o

0

o钮



0pZ





u肪





α″

l

闻■●…■咱■



↑|6

—→_一

■■■







『 ■













α睡 →」■■~







O9

o 寸



弓_

酞…





→=——

图8ˉ28





敛=



·□

■▲ 厂■

巴「[‖「■■『》■■「|》。▲尸「’|β卜■『』》■「厂■「ββ[尸′广}尸|卜■■「|口■尸『β‖『●·『·》|■■■尸■「β卜}■·「■■厂「·β「■■『‖|■厂巴尸匹■■|

mAP的变化和图8ˉ29类似°

lo四到们印田阳m”『m

【〗匡 □旦

loSs的变化

∩m怕d…uˉ≡

图8ˉ29mAP的变化

可以看到, lOSS最初非常高,之后下降到很低’正确率也逐渐接近l00%°

「s-

以下是模型训练过程中命令行中的一些输出结果: _ˉ[[poch99/10o’ Bat〔h27/29] ˉˉˉˉ +ˉ,==→=』=,=ˉ』==,==+=.=,==°=.==°=ˉ°==』=-ˉ+≡=-==°==-==≡=■■+-=°=→ˉ-°=』==ˉ==ˉ-十

|‖etr1〔5

|γ0k0Layer0 |γO[OLayer1 |γ0L0[ayer2|

+』=====°==』==』=』==+=』=°===°===ˉ==.===+■~-=°=°==←■■==■■+===→≡.=°==-°=°=°=ˉ=+

| gr1dˉ5jZe |14 | 1o55 | o.028268 |x | O.OO21O8 | O。0O4561 |y }" |0°OO1284 |‖ | 0.OO0594 |0.o197o0 | co∩+ |〔15 |O.O0OO22 |〔15己〔C | 10O.OO% | rec日1150 | 1.00OOO0 | re〔a1175 | 1.OO0OO0 | pre〔i5io∩ | 1·OOOOOO |〔o∩〔obj | O.994271 |〔o∩+_∩oobj | 0.00O126

| 28 | 0.o06o53 | 0.0O5267 | 0.002016 | O.0O4618 | O.0卯528 | o。033624 | 0。0"O01 | 1OO.O哪 | 1.O0O0O0 | 1.00OO00 |α8OO0OO |0·9992』9 | O.00O158

|56 | 0。o43745 |O.O08111 |0。OO9O47 | O。O002O7 | 0.0O0946 | 0。025』32 |O.0OOOO2 | 1O0.0叫 |1.0000OO | 1。000O00 | O。666667 | o.997762 | 0.OOO140

| | | | | | | | | |

| | |

+←==』=°==°==°====+==°==°==°===~=』=ˉ°=+=ˉ=°===°=°===■■==+===,=======,==°==+

「ota11o55o·118O663o343198776

||■■『‖||■■尸

这里显示了训练过程中各个指标的变化情况,如1o55、Ieca11、pre〔151o∩和co∩fˉObj,分别代表 损失(越小越好)、召回率(能识别出的结果占应该识别出的结果的比例’越高越好)、精确率(识别结 果中识别正确的概率’越高越好)和置信度(模型有把握识别对的概率’越高越好),可以作为参考° 5.测试

模型训练完毕后,会在checkpoints文件夹下生成一些pth文件’这些就是模型文件,和8.3节的 bcsLmodelpk‖文件原理_样’只是表示形式略有不同,我们可以直接使用这些模型做测试’生成标 注结果°



先在测试文件夹data/captcha/test下放人_些验证码图片,样例如图8ˉ30所示。 运行如下命令做测试: ba5‖dete〔t·5h

该命令会读取测试文件夹下的所有图片’并将处理后的结果输出到data/captcha/result文件夹’控 制台会输出_些验证码的识别结果。同时,在data/captcha/result文件夹下会生成标注结果’样例如图 8ˉ3l所示° 闪鸡







嚼 $ ″



弘辫

… 七拧









4







Ⅱ/l ˉ ~ 〔/露局ˉ{ ■\ 露. .≥ 1厂 ■儿//] 『 厂 `= i



←ˉ…』≡〔ˉ ˉ ˉˉ≥ˉ

= ˉ=广.←~ˉˉ… ≡

▲ ±=》

图8ˉ30样例图片

图8ˉ3l

甘~

带有标注结果的样例图片

可以看到,缺口被准确识别出来了°

实际上’dete〔t.5h命令的作用是运行detectpy文件,这个文件中的关键代码如下用 bbox=p己tche5.Re〔t己∩g1e((x1+box"/2’ y1+boxh/2)’boxw’ boxh』11∩ewidth=2’ edge〔o1oI=〔o1oI’ 千aceco1or="∩o∩e")

pri∩t(0bboN‖’ (x1′ γ1’ box—N’ box-∩)’‖o仟5et』』 x1)

这里的bbox就是指最终缺口的轮廓位置’x1指的是轮廓最左侧距离整个验证码最左侧的横向偏移量’

■【尸巴■■■■尸■■【■■■■■【■■曰■■〗■■』■‖』■■‖∩】■】‖】』■γ□■‖』■‖□‖■■|■·]·]·】‖』】■】】□●】口‖|■□可』‖』■〗』□■』●』』●』』]‖{□】‖■■]《』勺|』●‖〗·□】□〗|■』·|』■|

第8章验证码的识别

3l6

即O仟5et°通过这两个信息’就能得到缺口的位置了°

得到了目标缺口的位置’便可以进行—些模拟滑动的操作从而通过验证码的检测。 6.总结

到了—个深度学习模型文件°

往这个模型中输人~张滑动验证码图片,模型便会输出缺口的相关信息,包括偏移量`宽度等, 通过这些信息可以确定缺口所处的位置°

和83节一样’本节介绍的内容也可以做进_步优化’即把预测过程对接API服务器’例如对接

Flask、Django、FastAPI等’把预测过程实现为_个支持POST请求的API。 本节代码见https://githuhcom/Python3WebSpjder/DeepLeamingSlideCaptcha2°

85使用打码平台识另‖验证码 在前面四节’我们学习了几种识别验证码的方法,这些方法或多或少存在一些缺点’例如OCR、

OpenCV的识别正确率不高’深度学习的效果虽然还不错,但是训练和维护模型的流程相对复杂°那 有没有其他识别验证码的方法呢?



口ˉ□■司」」··、||」■】‖|■引|||臼||」■〗」|』■|』Ⅱ]』刁||」□(||」□‖

本节主要介绍使用深度学习识别滑动验证码缺口的整体流程’最终我们成功训练出了模型,并得

有’就是本节要讲的打码平台°利用打码平台可以轻松识别各种各样的验证码,图形验证码`滑

动验证码、点选验证码和逻辑推理验证码都不在话下,而且不需要懂任何算法,以及维护任何模型或



服务。打码平台提供了一系列API’只需要向API上传验证码图片’它便会返回对应的识别结果。 0

』∏‖‖■■■■■■■■

「}「卜





8.5使用打码乎台识别验证码

3l7

其实打码平台-般是半自动化的’也就是平台背后既有识别算法`模型的支持’也有人工打码的 支持。对于普通的由数字或字母构成的图形验证码’平台背后—般有深度学习模型作为支持,不仅识 别精度高而且速度快°对于_些较为复杂的、使用模型或算法难以实现或者难以达到较好效果的验证 码,会转到人工处理,打码人员通过平台提供的标注工具做标注,平台再通过API返回标注结果°

b



*疆■谴…、◆…脖

l

撅…, ˉ$

,

●蝉A\

茧; . 碱§ … 】吟. .耀跟魁二≡■腮蒙.……翻… 宦面雨叮=一

!≡

-凸

………焦…残洒》…Ⅱ ,二″毖霹吐置~三靠工叁 ˉ

ˉ工=

≡.皿包ˉg°.

熟甸

价豫镰嘘瓣嘴蘸撼陇簿瓣麓萝孽…

携麓密瓣·熟簿`露蹲`评蛾 娱氮贼攒瓤翻鳃戳撬热·艾搏寸膊攫 沪粉碰`榨麓p瞬霹爆鳞



·°



愈·



翻◎

广[∩‖『厂【厂|仁■∏「卜|广|仙尸[’「‖β‖■∏|》|心「|仁′‖|‖‖卜‖||卜■「‖|}■尸『『

怨′″亡;沁

| £…飞啡。β

鹏级鹰圈蹿分粪及识瞬瀑入系鳞

<迭年



▲■「‖『【「|凸口■【■厂‖|『■『〖〗『‖■「~■『●【>『‖‖■■■【~■■口『「■β□‖‖『匹■『坠=■■广

我个人比较推荐的_个平台是超级鹰’其官网为https://www.chaQ)jyingco∏γ,首页如图8ˉ32所示, 这个平台提供的服务种类非常广泛’可识别的验证码类型非常多;识别效果也很不错°

狡们承罐

锦_一酝

■■■■■

_弓

图8ˉ32超级鹰平台

超级鹰平台支持识别如下内容。

□英文数字:英文字母和数字混合而成的内容,最多识别20位° □中文汉字:最多识别7个汉字° □纯英文:最多识别l2个英文字母. □纯数字:最多识别ll位数字。

□任意特殊字符:不定长的汉字、英文和数字混合而成的内容’拼音首字母,计算题’成语混合, 集装箱号等。

□问答:例如问答题`选择题`复杂计算题°

□坐标多选:支持二选_、多选一`多选多等’通常返回选择结果的左上角的点的坐标。 如有变动’请以官网为准: https://wwwchaQjiyingcom/prjce.html。

本节中我们就来学习利用打码平台识别各类验证码的流程’涉及的验证码类型有图形验证码`点 选验证码、滑动验证码和问答验证码° ↑.准备工作

本节需要用到两个Python库—opencvˉpython和Pjllow’请确保已经正确安装,安装命令如下: p1p3j∩5ta11ope∩〔γˉpyt‖O∩p111OW

如果在安装过程中遇到问题’可以参考ht‖ps://setupscrape.center/opencvˉpython和https://setup



「8



3l8

第8章验证码的识别



scmpe静center/pillow。

另外请在超级鹰平台上注册一个账号,并购买-定的题分’具体的操作流程见官网或者联系官方 客服。

大家可以自行下载本节测试所用的验证码,地址为ht胚徊d】ub.哑mPyd〗on3叭bSpi妇/Ca碑haPlal加∏n’ 可以先复制下来: gjt〔1o∩ehttp5://github·co‖/pγtho∩3‖eb5p1der/〔apt〔hap1at+or肌git

复制之后,本地会出现一个CaptchaPlatfbrm文件夹’该文件夹内部存放的便是本节测试所需的验 证码图片°另外’文件夹中还有_个叫chaQjiyingpy的文件,这是基于官方SDK改写的’文件内容 如下: j呻ortreque5t5

十Io『‖∩a5h1ibmport刚d5

Se1+。u5er∩a爬=u5er∩a‖∏e

5e1+.pa55"ord="d5(pa55训ord.e∩〔ode(‖l』t+ˉ8|)).‖exdigest() 5e1「.5o什id=5o「t id

‖‖■可|‖司勺

i∩jt (5eM, 05er∩a爬’ pa55刚rd’ So什jd):

de+

■■‖■■■

c1a5s〔h日ojjyi∩g(obje〔t):

d

5e1「.ba5e-para"5={ 05o什jd|; 5e1+°5o千tjd』

} 5e1千.∩eadeIS={



u5er 目 5e1千°u5er∩a川e’

p日552‖ ; 5e1十°p日55word’



』05erˉ∧ge∩t0 : ‖‖ozj11a/4.O(〔o"patjb1ej "5I[ 8.0j ‖j∩do"5‖「5.1j 「ride∩t/4。0)! 』 } 夕

0■ 00"

m:图片字节

〔odetype:题目类型泰考http;//硼。〔haojiyi∩g.〔o们/pri〔eht∏1 00 『q

■0

pam"5={ 0〔odetype : 〔odetγpe’ } paIa们s.update(se1十。ba5e-paI日们s) +11e5= {!u5er+j1e0 : (‖〔c〔°jpg‖’ 1‖)} r=reque5t5.po5t(0∩ttp://up1oad·〔haoj1yi∩g.∩et/0p1oad/proce551∩g。php0 ’ data≡para们5’ 千j1e5≡+j1e5’ ∩eader5=5e1十.门eadeI5) retur∩I。j5o∩(〉 de十reporteIror(5e1十’ 1Ⅶjd); 00

0∩00

jⅧid:报错题目的图片I0 00

0■∏

p己r己们5={ 『jd0 :mjd’

纠■·■可|■|α■°■】』|·』可】■Ⅵ‖{·可□■Ⅵ{」■■■|‖■』■|||」=□|{‖|回■司

de+po5t_pi〔(5e1十’m」〔odetype):



header5=5e1十.∩eaderS)

retur∩r.j5o∩〈)

其中定义了_个〔∩aoj1y1∩g类,其构造方法接收三个参数° □u5ema"e:超级鹰账户的用户名°

个软件ID: 9l5502°







|‖‖

□pas5word:超级鹰账户的密码。 □5o+t—1d:软件ID’需要到超级鹰后台的‘软件ID”中获取,例如图8ˉ33这样’就牛成了_

」■■■■‖|』■■司|』‖||』■■■|

p日r己∏5.update(5e1卡.ba5e—para"5〉 r二reque5t5.po5t(‖http://up1oad°c‖aojiyi∩g.∩et/l」p1oad/Report[rror.p∩p0 ’ data=para们5’





]’ ]



‖‖卜

生咸一锦敏件ID

■●

软件【D:

…m1

9】…

软件旺γ8

软件 漠赂



软德律:



梅鳃

软件ID列寝

二■尸|‖『●厂‖‖●『

■■‖β‖■尸『『■∩|『‖β■厂卜巴■尸】′■尸

p



验 |



▲口



乎 码 打 用 吏 (



β



『‖【■=尸卜『|仔′「■「卜「■=

巴田■首页≥用户中心>软件【D

亏二……≈=…

总共】页1条数■刃1页每页显示20条[茵页][上-页][下-页][厄页]

图8ˉ33生成软件ID

这个类还实现了两个方法’PO5t=P1〔方法用于上传验证码并获取识别结果’ rePOrtˉerrOr方法用 于上报识别错误,识别错误时不扣题分’也就是不花钱°

以上内容都准备好后’开始识别验证码,首先是图形验证码。 2.图形验证码

本节中我们用图8ˉ34所示的图形验证码为例进行讲解,该图片被 图8ˉ34要识别的图形验证码 保存为captchal.png文件° 查阅价 格文档https://www.chaQjiying 这是一个由英文字母和数字组合而成的验证码,_共六位’查阅{

′,类型是“l006”.如图8ˉ35所示。 com/p∏cehtml’和这个验证码相符合的描述是“lˉ6位英文数字”,类 ■_

=… _

■■「 ■『■■□



_

—_

宜方单价■分》

=铂=■宁一——-

—_==斗

‖α]2〗5

惫犀斗喝位羹文戴宇

l四它

≡≡-—

~_

】o

‖髓锤嫩宇

『‖o]



~刁

β‖‖巴「

0叫

‖斗位英m宇

‖O

】m5

↑~骂髓唾游

{E

l咱位英文欲宇

]5

]≡7辕奠文数宇

!雨

‖兰8位癸文掀宇

‖…

γ≈凰位…戴宇

‖O]O

‖←》啦英文舷宇

·■■



〗Ol2

‖…狙位烫文做字

‖Om

‖=…英文″

∩ 巳

图8ˉ35价格文档

接着就可以编写实现代码: ∩

日=

ˉ]

◎ 日

γ″ °]

·]

』[



□∩ 广[





·『

·]

γ



冗巴

°〕

。]

□]





○ 日

』∩

Ⅲ○

尸十

‖| T0Ⅶ00

~■尸

05[删刚[、 p∧55舶R0、 5O「丁I0禽妄史改为自己的用户名、密码和软仟I0 ∏"h

■ ■ 厂 亡 ■ 尸

|‖■『尸|||【■■「||{「[■「‖【卜匹■『【▲■■■仔■■【■■■【

U5[R‖州[= 0` p∧55‖0R0= {| 50「丁I0= ‖ 0

〔∧p丁〔肌Ⅺ‖0= 01OO6!

「I[[‖州[ 二 !〔apt〔‖a1·p∩8

〔1je∩t≡〔∩aoj1y1∩g(05[刚州[’ p∧55刚R0’ 50「丁ˉI0)

Ie5u1t≡〔1ie∩t·po5tˉpj〔(ope∩(「I[[ˉ‖∧‖[’ 0rb|).read()’〔∧P「〔‖ARI‖0) pr1∩t(Ie5u1t)

…蹈豁匆



‖m7



|…

沾酌

户■尸|〖●卜=尸『■队■■庐巴尸

0

一…≡_=7————-_-

验证呻迎

验证码樊臼





第8章验证码的识别

这里首先利用05[R‖酗[、 p∧55‖ORD和50「丁I0三个信息初始化了—个〔∩aojiyj∩g对象,赋值为 〔1je∩t变量’然后调用c11e∩t的po5tˉp1〔方法上传了图8ˉ34的二进制内容’这里把po5t—pjc方法 的第二个参数设置为〔∧p丁〔‖∧促I‖0,即10O6° 返回结果如下: {|err∩o‖: O’ ‖err5tr! : 00R! ’ |pj〔-1d‖ : 0113841636O9492OOO1O0 」 0pi〔—5tr0 : !6叫』∩∩Ⅷ ’ ‖川d5|: !6十3+5Oe“7十bbOb13ab十828636096「94|}

可以看到,返回结果中的p1C-5tr字段出现_ 出点击下…■沿

°■撅

了正确的识别结果,识别成功!

点选验证码也是多种多样, l2306的验证码

就是非常典型的_种点选验证码。本节中我们用

图8ˉ36所示的点选验证码为例进行讲解,该图片

被保存为captcha2png文件°

靶翻毯■ ■丁|d



Q.『 `-

__=≡呻

查阅价格文档,比较符合这个验证码的描述 是“坐标多选,,’类型是“90O4”,会返回l4个

图8ˉ36要识别的点选验证码

坐标,如图8ˉ37所示°



e!O!

啤……阿麓蠢:男…绥……

!悬

鲤罐邻

9〗啤 Q… 9‖鸣



尘簿疹…■『凶争娜0m『』γ川螺涎鹤沁



~≈0y‖{埋涎鸿鸦“抖

-司

功≥泌

沁…

“各迅…个出似皿x‖M〗‖埋°诞{凰归

m餐n…a=愚个鳃酗↑,γ↑‖渔涎‖回0归 …

■抑谷迫∏唾E个…凶朝!γ‖隧涸坦UO仰■瞬{扫5″S



将“图形验证码”代码中的〔Ap『〔‖∧Ⅺ‖0改成9OO4,「I[[‖∧‖[改成〔apt〔∩a2.p∩g,然后重新运 行代码,得到的结果如下: {‖erI∩o0 : o’ 0err5tI0 : ‖0N0 ’ |p1〔-1d‖ ‖1138q16q4094920OO11‖ ’ ‖Pj〔ˉ5tI‖ : ‖ˉ|108’133|227’143′’ ‖|『U5| ‖526d232eb0∑己伯7ed8319OS1a48ed7e9!}

可以看到返回结果中的p1〔5tr字段变成了108’133|227’143,使用OpenCV技术在图8ˉ36上标 注出这个点: j呻Ort〔γ2

1Ⅶage≡cγ2.1mead(0〔aptc∩a2.p∩g|)

mage=〔v2.〔ir〔1e(i爬ge’(108) 133)」mdiU5=1O’〔o1or=(o’ 0′ 255)’ thi〔|(∩e55=ˉ1) 1吧ge≡〔γ2·〔1r〔1e(加age’(227’143)’mdiu5=1O’〔o1or=(0’ O’ 255)’ t∩ic代∩e55≡ˉ1) cγ2·i则Ijte(,〔aptc帕21己be1.p∩g‖′mage〉

运行结果如图8ˉ38所示°

■□]|』■■|||』■■||■引‖|■■

图8ˉ37价格文档

』可||』|』‖‖|(□‖{·`|]∏‖`」」|』■∏」‖《||√|□】』‖‖□判‖‖‖‖‖]|」】勺‖」Ⅵ·∏{』·|』日

3.点选验证码

●司‖』■∏』可||■■|||」句||日‖」■■〗日‖」■■■‖|■■■]‖凸勺■可』‖

320



8.5使用打码乎台识别验证码

32l

9■断









可以看到标注出来的正是第l张和第2张图片’没问题,验证成功!

另外’还有一些验证码也属于点选类型’例如指定点击物品的颜色的验证码,如图8ˉ39所示。

指定文字点击顺序的验证码,如图8ˉ40所示。 要求按照语序点击文字的验证码,如图8ˉ4l所示°

|刊

‖庐■「

阔按露爆依次点馈下圈文字』窿涵颧萨







≡■

h

西在下图依次点击;弘缮铀

秘~〕 匹]■

阴点击绿色拘品. {

■「□『)【=■尸卜●●『『》巴■厂‖■■尸『‖|β■「‖■「‖}。■尸「■『「■■『■∏|恤∩但■「卜匹=尸●■「■■「■厂》【匹■■■||【=■■■『‖|■■■「■■尸「|》■■『|■■「。■尸β「巴■}■「}『|巴■【『‖|■【|

图8ˉ38在captcha2,png上标注出返回坐标对应的点

=—==≡

-

图8ˉ40指定文字点击||顶序

图8ˉ39指定物品的颜色

图8ˉ4l

要求按照语序点击文字

4滑动验证码

我们再来验证滑块验证码,这里以图8ˉ42所示的图片为例进行讲解’这张图片被保存为captcha3 png文件。 |





寸 b

图8ˉ42要识别的滑动验证码

查阅价格文档’比较符合这个验证码的描述是“坐标选一”,类型是‘‘9l0l’,’如图8ˉ43所示°



第8章验证码的识别 ~

出…ⅢⅢⅢ乡…分■…上句o0o…力单位也吨y…铂







点■∏个……〗J0犀疵

鸳 鲍









】… γ









§~≡



9『锤

攀懈z≡浓…凰’

■■∏■■■∏| ‖■■||

|…

』■■‖』■■|■■‖(■■■可□■·

322

呻◆迎迁H】黑争…瓣魏γvγ‖哩归蛔烬

■·■‖



===~≡—

…▲

虫簿多迅…牛铂…Nγ矿》鸿v匿‖其aγ$ …

…多迅迅…+■酞鲍》U‖‖唾谰型泅‖叫凋玛沁



图8ˉ43价格文档

』‖`·】■司‖日■勺‖‖‖■■Ⅵ

……财‖…″瞬 _---~=二…

和“点选验证码,,类似’将“图形验证码,,代码中的〔∧p「〔‖∧【I‖0改成91O1, 「I[[‖∧‖[改成

{!err∩o‖: o」 0errstr‖ : !0R′ ’ 0p1〔-jd‖: 091384165509q920oO120 ’ !pj〔5tr! 8 !231’85‖ ’ |阳d50 : !ae9〔ba3a8bb十〔d9197551dda23aaO+d7{}

‖‖‖(‖

Capt〔∩a〕.p∩g,然后重新运行代码,得到的结果如下:

可以看到返回结果中的pj〔ˉ5tr字段变成了231’85,我们用印enCV技术在图842上标注出这个点: mportcγ2

‖{司







巳 已 巳



°]

β长



·∩



口[











j帕ge=〔γ2.j‖Iead(|〔3pt[ha3.p∩8!) 1爬ge=〔γ2.〔1I〔1e(j吨ge’(231》 85)’mdju5=10’ co1or=(0’ cγ2·j||Mrjte(0〔aptcha3-1abe1.p∩8』’mage)

返回的结果如图8ˉ“所示°

很遗憾’标注的点在缺口右侧的中间位置’这样不

方便我们判断。造成这个结果的原因在于平台的背后是 标注人员’标注人员拿到验证码图片后并不知道应该标

不足的前提下,标注结果自然不_定如我们所愿°

面对这种情况,一般应该怎么做?可以在图片上做 一些处理,例如添加自定义的文字’提醒标注人员哪里

是正确的位置。下面使用OpenCV技术在图8ˉ42上加 尸<ˉu舍Ⅷ门凹JuL-旦◎ 「‖且l区厂∏\′‖J℃』Ⅲ~v 〕义/『、『工国◎=斗≡≡上〃‖ _行字“请点击目标缺口的方卜角”:

图8ˉ“在captcha3.png上标注出返回坐标 对应的点

1"pOrt〔γ2

+roⅦpIL加portI‖己ge「o∩t’ 1‖己ge0ra"’mage 加port∩u川pya5∩p h∏pOrtjO

de千〔γ2addtext(mage’ text’ 1e十t’ top’ text〔o1oI二(255’ 0’ O)’ text51ze=2O): 1们己ge=I们日ge。+ro『∏array(〔γ2。〔γt〔o1or(mage」 cv2.〔0l0RBCR2RC8)) draw=I‖ageDraWDr日w(加age)

十o∩t≡I‖age「o∩t.truetype(05加5u∩.tt〔|’ text5jze’ e∩cod1∩8="utfˉ8")

dra闪.text((1e什’ top)』 text’ text〔o1or’ 十o∩t≡+o∩t) retur∩cγ2·〔γt〔o1or(∩p·a5array(ma8e)’〔γ2.〔0[0RR6B2BCR) 加age=〔γ2.mread(「I[[‖州[)

j阳ge≡cv2addtext(mage’′请点去目标缺口的左上角0 ′ j∩t(1‖日ge.5∩ape[1]/10)」i∩t(mage.5hape[0]/2)’(255’ re5u1t=c1ie∩t.po5t-pic(1o.8γte5I0(cγ2。1爬∩〔ode(‖。p∩g0 ’mage)[1]).getγa1ue()’〔∧P丫〔什∧RI‖0) pri∩t(re5u1t)

| | ||||

0’ 0)’ 4O)

〔11e∩t≡〔∩aojjyj∩g(05[R‖酬[’ pA55"OR0’ 5O「丁ˉI0)

』■■■‖‖』口■』·■■{』■■』■■=■Ⅷ|■■■`·■■■]□●勺■■■∏·

注哪里,例如标在目标缺口的左侧还是右侧,在信息量

8.5使用打码乎台识别验证码

323

这里我们定义了一个〔γ23ddteXt方法’由于直接添加中文会产生乱码’所以需要借助Pillow库’

并且依赖一个中文字体文件。添加文字后,图片如图8ˉ45所示。 重新运行代码,得到的结果如下: {0err∩o‖: o’ !erIstI‖ : ,0‖’ ‖p1〔-1d0 : |9138q183209q92ooo310 ’ |p1c≡5tr‖: ‖167’550 ’ 0Ⅷd50 : !12ae十197b545bb卡O5日c28c7797e5b己46!}

这时返回结果中的p1c—5tr字段变成了167’55’标注—下这个点’结果如图8ˉ46所示。 愤 "

‖●■■尸【‖‖■【■■■【【‖匹尸||』●[■「′|●『【■「【【■{●「‖》「[尸||●血■■『巴■[■厂)‖|■「卜●|■尸β‖‖卜‖尸‖●■■尸|‖|■■「||庄■[‖『|■尸′









■ 霹■鳞〔 `= 厦【



.



,|





℃瘪



●!

秘』…心霹ˉ 亨

图845添加提醒文字后的图片

睁憾





图8ˉ46标注新返回的坐标点

可以看到’这下标注的位置就正确了°所以在有必要的情况下’可以对图片稍做处理’以达到更 好的标注效果°

5ˉ问答验证码

再看_种验证码—问答验证码,样例图片如图8ˉ47所示’这张图片被保存为captcha4png°

酵渺罕《密 · 谬 ·霞鼠蕊′ ;‘嗡;·′`, ′. .矿. i Ⅷ

F

图8ˉ47要识别的问答验证码

[,并且在问题后的括号里有答案提示, 可以看到’验证码上有一个问题,并且在问题后的括号里有答案提示,问题中每个字的颜色、形

状和字与字之间距离各不相同’背景中还有—些干扰线°

对于这种验证码’如果想自动化完成识别,难度是比较大的°首先需要识别出图片上的文字,这 对正确率有很高的要求°在能正确提取所有文字并且问题相对简单的前提下,可以通过用爬虫模拟_ 些网络搜索操作获得结果。如果问题稍徽复杂一些或者在网络上搜索不到答案’可以通过—些自然语 言处理技术或者知识库获得答案。但总的来说’通过纯技术手段识别问答验证码的难度还是比较高的°

面对这样的验证码,比较合适的解决方案依然是打码平台,借助平台背后的人工力量完成识别° 同样,查阅一下超级鹰平台对此类验证码的支持情况,如图848所示。 -■→

■■■













ˉ密积·…=秽≈=一‘=■--…≡=耘奇玛≈==_一声…砷面酝叼

酞了



Ⅷ \ | 计m

≡撰…巴蹿撞…撰←

」 6四3 …





沪’£□



■方单价…)

咕诬…毡

唾睦型

】5

□…崇…`==…— 2$

… _~■=—







选毋…锣…]z3Q》

J

.

霞蘑

…■









05

…=坚ˉ二′.f一一`

■r=■■

『6

闷…·扫■回…



…~嘛

-甲≡≡

图8ˉ48价格文档







第8章验证码的识别

可以看到60叫类型是支持此类验证码的,我们把前面代码中的〔∧p丁〔肌长I‖0改成60O4’「I[[‖A卜|[

改成〔aptc∩a4.p∩g,然后重新运行代码,得到的结果如下: {|err∩o‖ 8 0’ ‖e工r5tr|: |0Ⅸ|』 ‖pic-1d‖ : 091386235909492ooo330 ’ 0pic-5tr『 : ′大象!′ ‖川d50 : 0〔〔2a9466〔15d于99o〔559bb7d千06e己c55‖}

返回结果中的piC-5tr字段是大象’回答正确。如此看来,打码平台着实非常强大。 6.总结

本节中我们总结了利用打码平台识别各种验证码的方法’图形验证码\点选验证码、滑动验证码和 问答验证码都不在话下’而且正确率也还不错’毕竟背后都是真实的人在操作’而且还有健壮的模型°

本节代码见https://githuhcom/Python3WebSpider/CaptchaPlatfbrm°

8.6手机验证码的自动化处理 前面我们了解了_些验证码的识别流程,这些验证码有_个共同的特点,就是通常只在PC上即可识

别通过,例如在PC上出现了一个图形验证码,那么在PC上直接识别就好了,所有流程都在PC上完成。 但还有_种验证码和这些验证码不同’就是手机验证码’如果在PC上出现了一个手机验证码’需要 先在PC上输人手机号,然后把短信验证码发到手机上,再在PC上输人收到的验证码’才能通过验证。 遇到这种情况’如何才能将识别流程自动化呢?

|.短信验证码的收发

通常而言’我们的自动化脚本运行在PC上,例如打开—个网页,然后模拟输人手机号,点击获 取验证码’接下来就需要输人验证码了°我们可以非常容易地把前三个流程自动化,但验证码是发送 到手机上了’怎么把它转给PC呢?

自动化验证码的整个收发流程’可以这么实现—当手机接收到_条短信时’自动将这条短信转 发至某处’例如转发至—台远程服务器或者直接发给PC’在PC上我们可以通过—些方法获取短信内 容并提取验证码,再自动化填充到输入的地方即可°

关键其实就是下面这两步:

■司‖』可』」|‖■■■‖||■习司‖■·|‖‖」■∏‖』《‖』■■司■■■∏‖·■■]|□■∏‖■■■■□|■】□】‖』《』■■‖‖勺‖|』‖|」么‖|‖·∏划]‖■可{『‖`|‖叫‖』·]‖』■■■Ⅵ削‖‖』当□】{‖(·】□■|β■

324

□监听手机接收到短信的事件;

□将短信内容转发至指定的位置。

这两步缺_不可,而且都需要在手机上完成°解决思路其实很简单’以Android手机为例’如果 有Android开发经验’这两个功能实现起来还是蛮简单的° =

注意这里我们仅仅简单介绍基本的思略’不会详细介绍具体的代码实现,感兴趣的话可以自行尝试° 首先如何监听手机接收到短信呢?在Android开发中,分为三个必要环节。

□注册读取短信的权限:在一个AndroldApp中’读取短信需要具备特定的权限’所以需要在 <l』5e5ˉpem‖i55io∩a∩droid:∩a川e="a∩dro1d.per川j551o∩.旺〔[Iγ[5‖5"〉〈/u5e5ˉperm5sio∩〉

这里我们在AndroidManlfest.xml文件中注册_个BIoad〔a5tRe〔e1ver’叫作5川5Recejγer:

□■□■■司□{』■■●■‖·■■‖‖△■可‖·■』■Ⅶ

□注册广播事件:Android有_个基本组件叫作8roadca5t尺e〔ejγeI,是广播接收者的意思’我们 可以用它监听来自系统的各种事件广播,例如系统电量不足的广播、系统来电的广播,那系统 接收到短信的广播自然也不在话下。这类似于注册-个监听器来监听系统接收到短信的事件。

(‖|

AndnodApp的AndroldManifestxml文件中将读取短信的权限配置好’例如:

■‖‖‖『巴■】



86手机验证码的自动化处理

325

〈re〔e1γera∩drojd;∩a阳e= .re〔eiγe°咖5Re〔ejγeI〉

〈j∩te∩tˉ十j1tera∩droid:prjorjty="999"〉 〈己〔tio∩a∩dIoid:∩aⅧe="a∩dro1d.proγjdeI°丁e1epho∩y。S"5旺〔[Iγ[D0|/〉 〈/i∩te∩t≡fi1ter〉

〈/reCeiγer〉

□实现短信广播的接收:这里就需要真正实现短信接收的逻辑了’只需要实现一个5"SReCejγer ■【【『『血尸■『「巴◆『「■■■■「『『■「|}β|■■「)‖■■∏「』【■『‖‖β|■■‖|‖

类’它继承Broad〔a5tRe〔e1veI类,然后实现其o∩Re〔e1ve方法即可’其中j∩te∩t参数里就

包含了我们想要的短信内容’实现如下: pub1j〔〔1a555爪5Re〔ejγeIe×te∩d58Ioad〔35tRe〔e1ver{ 刨γerride

pub1jcγo1do∩Recejγe(〔o∩text〔o∩text’ I∩te∩t 1∩te∩t){ 8u∩d1ebu∩d1e≡i∩te∩t。get〔xtra5(); 5们5价eSSageⅦ5g=∩u11; i千(∩l』11 !=bq∩d1e){ 0bje〔t[] 5Ⅷ50bj= (0bject[]) bu∩d1e.get("pdu5")j 于or (Objectobje〔t : 5‖50bj){ 们5g=5∏s爬s5age.create「ro们pd(」((bγte[])obje〔t)j lo8.e("短估号码』!’ 0!"+"5g·getOrigj∩ati∩g∧ddre55())j [Og.e("短仿内容|』’ "" +Ⅷ5g.getDi5p1aγ"e55age8ody())j [og。e(,0短信时间"’ ‖{ 』0 +Ⅷ5g°get『me5ta"p‖i11i5()); } } 四



如此_来’我们便实现了短信的接收。

收到短信之后,发送自然也很简单了’例如服务器提供-个API’请求该API即可实现数据的发 送’Androjd的一些HTTP请求库就可以实现这个逻辑’例如利用OkHttp构造ˉ个HTTP请求’这里 就不再赘述了。

不过总的来说,整个流程其实还需要花费_些开发成本’对于如此常用的功能,有没有现成的解 决方案呢?自然是有的。我们完全可以借助—些开源实现’这样就没必要重复造轮子了° 介绍-个开源软件SmsForwamer’中文叫作短信转发器’其GitHub地址为https:〃githuhcom/Pppscn/ SmsForwarder。它的基本架构如图8ˉ49所示。

m卡■

囚}回

v

…~回 …一议

| 唾m

=…

图8ˉ49

SmsForwarder的基本架构



』‖』■■■‖|·』·|』□

第8章验证码的识别

326

短信内容、接收时间等’然后将这些内容通过一定的规则转发出去,支持转发到邮箱`企业微信群 机器人、企业微信应用` T℃legram机器人和Webhook等°例如可以配置这样的转发规则,如图8ˉ50 所示。

又如当手机号符合-定规则时就把获取的内容转发到QQ邮箱; 当内容包含“报警”字样时就把 获取的内容转发到阿咀个`|[邮箱; 当内容开头是“测试’,时就把获取的内容发送给叫作TSMS的

Webhook。其中的QQ邮箱和阿里企业邮箱是我们已经配置好的发送方,都属于邮箱类型’TSMS也 是_种发送方,属于Webhook类型,发送方如图8ˉ5l所示°

圆邀飘qs`0os…翻Qo鳃疆 α蠢内窑■东东钓烫勤愈戴撇簿廓用

■■Ⅵ{‖』■■■可|□■可√■■·■司二■可日二■』‖』

囤拿…戳翻口打 巴金翘翱发翼…



酗醒吨

翻慧肉霉歼头凰翼谴键趣… 国塑陶s包禽顿口馋浚勤闺雹企遮癣熏

………………卵

瓤簿榴够‖O086摊值 }v 磷机 蟹转擞翻 豁人 并凰是短馆内宙包台欠蟹转擞翻

圈蕊鹏愚锡,翻嚼橱…凰‘

圈圈◎囤隧囤回

发送万

转发规则

■ 图8ˉ50配置的转发规则



■■ 图8ˉ5l

发送方

我们也可以点击图8ˉ51下方的“添加发送方,,按钮添加想要的发送方,可以选择对应的发送方 类型’例如这里添加邮箱类型的发送方,App会弹出“设置邮箱”的提示框,如图8ˉ52所示,我们可

同样,如果选择添加Webhook类型的发送方,App会弹出“设置Webhook”提示框,如图8ˉ53所 示’我们可以选择请求方式(POST或GET),设置WebSeⅣer的URL,设置Sec爬t°

」■可‖〗』‖』■·‖‖|■■■■{』司』■‖|』■■■‖‖』】■】■■】□』■】·{』‖可|■〗」□■■】

设置转发规则的页面如图8ˉ54所示,支持正则匹配规则和卡槽∏配规则。这里可以设置匹配的 卡槽、匹配的字段、匹配的模式,还可以填写正则表达式来设置匹配的值’图8ˉ54中就设置了尾号是 4566的手机号,执行_定的发送操作,收到的短信会发送到钉钉这个发送方°



』‖‖■■■‖‖」■■】γ{{‖』■口】|||」■】句]

以设置SMTP端口、发件账号`登录密码/授权码等内容°



』 ‖ ( |



■幽·出■P

8S

■■■■

α叫

·Ⅵ‖■可]|■■‖|』■■‖」‖■■‖‖|』Ⅵ]|(‖■《‖□〗■〗|{】□‖」』‖■■■{‖《』刘‖■‖|』Ⅵ』■司

SmsFo!warder的架构非常清晰,它可以监听收到短信的事件,获取短信的来源号码`接收卡槽、

…叫『绸"罕『■』





‖日亏瓣 ,| 刮.o凸窍.簿贪

氓■糜盅的卡硼

圆 设盅Web棍◎o揽

设用囱椰

■■「‖‖|■尸■「〗『■■尸【■「

○金瓤◎手机署○内口○多谊匹贬

酶农万或◎尸Os丫◎G盯 设■…蹿8悦如0…〃●h…γ砸闻 …@叮宜

发件眯署

铅宝哪…囊钱

°H

◎不迅

◎包盒

○开头

○结■

◎正则匹出

矗■廊正的蚀

诅…毗蠢变缉不计簿吨!

m密鸦/汉权码

○s删2

雪■冗腥的寨攫

发件耀仍髓

开虐ss止 Ⅷ泌

○S|川`

◎全韶

诅凶禽称

_一_

327

设Ⅲ转发规则

}颧| 设丘邮箱

s啊pmj



●■

且ZB



0

勺「



肉 已





0:$吕



‖‖■『‖【β卜|[■【『上『||·「|}■尸||β|△『 ●

8.6于机验证码的自动化处理

□》【■尸 ·卜‖◆『‖■》

.☆4鳃碱 收件地址

■■■■■

| 穗认 ‖ .







凹■汹童污 …锤盯



毛■

■□

图8ˉ53设置Webhook

■■■~

吼·

图8≡52设置邮箱

↓ ■ 气

‖卜■『『卜·∏『)》|·『∩■●◆坠□∩β「′■「

1墅■啦

图8ˉ54设置转发规则

2.实战演示 我们尝试使用Flask写一个API’实现如下: +r咖「1a5|(加POrt『1a5促’ req0e5t’ j5O∩i+y 十r咖1ogurui呻ort1oggeI

巴血■『

app≡「1a5k(_∩a爬-)

‖卜|》「|〖『》■■■ ■『| |》√『『‖‖坠『‖|

@己pp。IO[」te(|/5∏5 ’爬thod5=[!刚5丁|]) de+re〔eive(): 5‖臼〔o∩te∩t=Ieque5t.十om.8et(,〔o∩te∩t‖) 1ogge【.debug(f!re〔eiγed{5Ⅶ5〔o∩te∩t},) #肝析内容并将其保存到db或则 retur∩jso∩j+y(5tatu5=5u〔〔e5s,)

j+

∩a眠=0

∩ai∩

‖:

app.m∩(debug≡『me)

■■■「|八「「

代码很简单,先设置了一个路由,接收pOST请求,然后读取了Request表单的内容,其中co门te∩t 就是短信内容的详情,之后将其打印出来°

[■■|■【『

将上述代码保存为serve∏py,然后运行: Pytho∩35erγer·py

■■ ||■『‖‖·『[■

运行结果如下: *kbug加de: o∩

*Ru∩∩j∩8o∩http://127.0.0.1:5呕/(Pre55〔『Rl+〔toqujt) *Re5tarti∩g切ith5tat *kbuggerj5aCtiγe! *0ebuggeIpI‖: 269ˉ657ˉO53

为了方便测试,可以用N盯ok工具将该服务暴露到公网: ∩grO代∩ttp5O"





注意Ngn倔可以方便地将任何非公网的服务暴露到公网访问,并配置特定的临时二级域名,但一个域名 有时长限制’所以通常仅供测试使用°试用前请先安装Ngmk’具体可以参考ht印s:/)n囚okcom/° 运行之后’可以看到如下结果: 5e551O∩5tatu5

O∩1i∩e

5e55io∩[xP1Ie5

1hour’ 59m∩ute5

update己va11ab1e (γer5jo∏2.3。4o′〔tr1ˉ0toupd日te)

γer5iO∩

2.3.35

Regjo∩

U∩ited5tate5 (u5)

‖ebI∩ter+日ce 「orwardj∩g 「OrWardj∩g

‖ttp;//127·O。0°1:4040

〔o∩∩e〔t1o∩5

tt1

op∩

rt1

It5

p5O

p9O

9

0

O°0O

0°OO

O.0O

O.0O



http://1259539cb974∩gIo代·1oˉ〉 http://1o〔a1‖o5t:500O http5://12S9539〔b974。∩gro恨·ioˉ〉 http;//1o〔a1们ost:5OOo

接下来我们手机上打开SmsForder,添加一个Webhook类型的发送方,设置详情如图8ˉ55所示° 其中’我们把WebSeIver的URL直接设置成了刚才Ngrok提供的公网地址,注意要记得在URL的后 面加上sms。

接着我们添加一个转发规则’如图8ˉ56所示° ■



^√





^×…

『□■

″刃

『8a8锰α伐·

竿■`Gˉ翱■ G

铲蕊磷?

L

遥■…

m名仰W的…了eSt m名仰WebmOk丁eSt

○岔部○宇机q◎内客○多■匹■

■啼蓟o尸oS丫○G厄丁 ■呻蓟o尸oS丫○G厄丁

没迁邪

迅…哼目宙髓:bnp仪〃Bp.…响 …唾





○■

○不残

◎●古

·开头

○■启

○正肉匹捉

瞩 辑

↑]m〃γ259鳃9cb97▲ng『Ukl◎/S而S

蛔睡…儡

唾箕≡蚊鲤魔不讣… m…蚊坚魔不汁…

雪—_…=

焉试

′拯

!

.嚣汽 趣

Pb 捌

■霍曲

…?翘





。 〈

图8ˉ56设置转发规则

如下:

划 」 □ ] 』 ■ ■ ■ ‖ |』■]』■■|』■□{《‖〗■可|□■■■■』■】

添加完成后’我们可以尝试用另一台手机给当前运行此ApP的手机发送_个验证码信息,内容

‖ ■ ■ ‖ 《

这里我们设置了内容匹配规则’当匹配到以“测试”为开头的内容时’就将短信转发到Webhook 这个发送方’即发送到我们刚刚搭建的Flask服务器上。

』‖』■■■■司·‖■■司』■‖‖‖‖‖

…=

●■■/

凸冯帖黔〗冈

孟■脸

凸■

獭◎

~·△

咏较

■比Ⅶ卯

七′■

图8ˉ55设置Webhook

』‖』·

} |

■】司‖‖||■■』■■{]勺】‖‖■■丁|■Ⅲ■可|

◎S!仙2



○SM

嚼》’

◎金瓤

L

圈设丘w·h"… 圈设置W·bh。°服

斌瓣腮蹿鳃

没■转发规则 ……嗽澜

刽』‖』勺』■■〗】司‖■■‖‖{|·∏·』]{‖·‖·|』■■]·‖』‖‖■引‖|‖‖■■

可以看到Ngrok为我们配置了—个公网地址’例如访问ht印s:〃l259539cb974ngrokio就相当于访问 我们本地的http://localhost:5000服务’这样只需要在手机上配置这个地址就可以将数据发送到PC端了°

·■ 』‖■■司‖‖凹■■‖』●』■己‖{日】■■‖‖口

0pdate

‖‖■·■■〗】■Ⅷ】』□■■□∏』■■』□{』■】{』』■】□□』□】】叮』■■□‖】』』∩‖

第8章验证码的识别

328

巴■「}△■■■‖■■■‖■■『





8.6手机验证码的自动化处理

329

Ⅷ‖试验证码593722,一分钟有效°

这时可以发现刚才的Flask服务器接收到了这样的结果: re〔ejγed+8617××XXX×x×



Ⅷ‖试验证码593722’一分钟有效° 5I川2〔h1∩a0∩ico‖

0 ●尸||‖仆|巴■「△■尸■『■■「■■■『「『‖卜■尸



2021-O〕ˉ2718:』7;54 5‖ˉC986O

可以看到’发送给手机的验证码信息已经成功由手机发送到PC了,接着便可以对此信息进行解 析和处理,然后存人数据库或者消息队列°爬虫端监听到消息队列或者数据库有改动即可将收到的验 证码填写到爬取的目标网站上并进行一些模拟登录操作’这个过程就不再赘述了。 3.批量收发

以上介绍的内容针对的是只有-部手机的情况’如果有大量手机和手机卡’则可以实现手机的群 控处理’例如统一安装短信接收软件、统-配置相同的转发规则’从而接收和处理大量手机号的验证 码°图8ˉ57所示的就是—个群控系统。

■叮【尸‖■尸|『■

■■β卜′■厂【■■尸■‖■■厂‖■■『■『【尸|炉■厅『·■■「







卜|厂

图8ˉ57一个群控系统

4卡池`猫池

0

|‖

■尸‖【■「|}■厂△「■■●[■‖|〖■【「‖■『

p ■□[■■厂■



除了上面的方法,当然还有更专业的解决

方案’例如用专业的手机卡池、猫池,配以专

业的软件设备实现短信监听°例如图8ˉ58中的 设备支持插l28张SIM卡’这样可以同时监听 l28个手机号的验证码°

具体的技术这里不再阐述’可以自行查询 相关的设备供应商了解详情°

5.接码平台

卡池、猫池的解决方案成本还是比较高的,

而且这些方案其实已经不限于简单地接收短信

图8ˉ58支持插l28张SIM卡的设备

第8章验证码的识别

验证码了’就像手机群控系统一般会做手机群控爬虫’卡池也可以用来做4G/5G蜂窝代理’仅仅做短 信收发当然也可以,但未免有些浪费了。

|(|(‖』·

330

如果不想耗费过多成本,想实现短信验证码的自动化’还有一种方案就是接码平台,其基本思路 □平台会维护大量手机号,并可能开放一些API或者提供网页供我们调用来获取手机号和查看 □我们调用API或者爬取网页获取手机号,然后在对应的网站输人该手机号来获取验证码。 □通过调用API或者爬取网页获取对应手机号的短信内容’并交由爬虫处理°

由于对接码平台的管控比较严格’因此接码平台随时可能会不可用,请自行搜集对应的平台使用°

‖勺□‖]‖Ⅲ■司‖』□■‖

短信的内容°

‖ 日

如下。

6.总结 ‖

本节通过一个实战案例介绍了手机验证码的自动化处理流程,同时介绍了现在业界广泛采用的— 些用于收发验证码的工具°随着技术的发展’各种新的工具和技术会不断出现’合适又强大的工具会 让我们的爬虫开发过程如虎添翼°

·、‖`‖

■■|】■■■■可‖■■‖·■‖‖‖』■

‖‖

‖|』





■■|』■可■`‖‖』■■可





|||■■|■■■』■〗■可|』|■■■□■□

■■■■■■【■■【『『‖『「『〖『』[「‖「∩}【「『『■「「[■『[卜「|■『‖■}〖巳【「^山厂|【β|「■「‖卜|卜□「}■}’}巳尸|「「〔[■『旧「|『「|口『[■■「|‖|}【「|‖●′|卜■【′}坠·「|}·尸|[【′|}■陵`|·【「





代理的使用 / 在使用爬虫的过程中经常会遇到这样的情况’爬虫最初还可以正常运行,正常爬取数据’一切看 起来都是那么美好,然而-杯茶的工夫过去’就可能出现了错误,比如返回403Forbldden,这时打开 网页’可能会看到“您的IP访问频率太高”这样的提示,或者跳出一个验证码让我们识别’通过之后 才可以正常访问,但是过—会儿又会变成这样。

出现上述现象的原因是网站采取了-些反爬虫措施°例如服务器会检测某个lP在单位时间内的

请求次数’如果这个次数超过了指定的阂值,就直接拒绝服务’并返回_些错误信息’这种情况可以 称为封IP。这样,网站就成功把我们的爬虫封禁了°

既然服务器检测的是某个lP在单位时间的请求次数’那么借助某种方式把IP伪装起来’让服务 器识别不出是由我们本机发起的请求’不就可以成功防止封IP了吗?这时代理就派上用场了。本章会 详细介绍代理的基本知识以及各种代理的使用方式,包括代理的设置`代理池的维护`付费代理的使 用`ADSL拨号代理的搭建方法等内容’希望能够帮助爬虫脱离封IP的苦海。

9.| 代理的设置 我们在第2章和第7章介绍了很多请求库’例如urllib`requests`Selenlum`Pyppeteer、Playw∏ght 等,但是没有统一梳理过代理的设置方法’本节我们就针对这些库梳理—下代理的设置方法° ↑.准备工作

请先了解一下代理的基本原理,参考本书l.5节即可,这有助于更好地理解和学习本节内容°另

外,需要先获取_个可用代理代理就是IP地址和端口的组合’格式是〈iP〉:<port〉°如果代理需要 访问认证’则还需要额外的用户名和密码两个信息。 那怎么获取—个可用代理呢?

使用搜索引擎搜索“代理”关键字’会返回许多代理服务网站’网站上有很多免费代理或付费代

理,例如快代理的免费HTTP代理https://wwwkuaidaili.com/fTee/就提供了很多免费代理,但在大多数 情况下这些免费代理并不一定稳定’所以比较靠谱的方法是购买付费代理。付费代理的各大代理商家 都有套餐’数量不多’稳定可用’可以自行选购°

除了购买付费代理也可以在本机配置一些代理软件’具体的配置方法可以参考https://setup. scrape.center/proxyˉclient’运行代理软件后会在本机创建HTTP或SOCKS代理服务,所以代理地址一 般是l27.00.l:<poIt>这样的格式不同代理软件使用的端口可能不同° 我的本机上安装着_个代理软件’它会在7890端口上创建HTTP代理服务’在789l端口上创建

SOCKS代理服务,因此HTTP代理地址为l270.0.l:7890’SOCKS代理地址为l2700.]:789l’只要

|』

设置了这个代理’就可以成功将本机IP切换到代理软件连接的服务器的IP°在本章之后的示例里’ 我将使用这个代理软件演示设置方法’大家可以替换成自己的可用代理°

·■□‖■■』□■

第9章代理的使用

332

』′|■司|||■■■‖□‖■■·|□■■·】■】】■〗』■■■‖

设置代理后的测试网址是http://wwwh忱pbinorg/get,访问该链接可以得到请求相关的信息’返回 结果中的oI1g1∩字段就是客户端的IP,我们可以根据它判断代理是否设置成功’即是否成功伪装了IP° 接下来就看一下各个请求库是如何设置代理的吧° 2U「|||b的代理设置

先介绍最基础的urllib’代码如下: 千ro∏ l」r11ib.erroIj们portU砒[rroI

十ro∩{ ur111b。Ieque5t iⅦportpIoxγ付a∩d1er’ buj1d-ope∩er pIo×y= ‖127。0·O·1:789o!

pro×y-ha∩d1er≡pro×y付a∩d1er({ ‖http‖ : 『∩ttp://| +pIOXy’ !∩ttp5|: ’http://『 +prOXy 〕\ J′

ope∩er=b(」j1dˉope∩er(pIoxyˉha∩d1er) tIγ;

Ie5po∩5e≡ope∩er°ope∩(‖http5://www.‖ttpb1∩·oIg/get‖) prj∩t(re5po∩5巳read().decode( 0ut千ˉ8‖))

■■■|■■■■‖·』□、』■■□■■■日

eX〔ept0R[[rrOra5e:

Pr1∩t(e°rea5O∩)

这里需要借助proxy‖a∩d1er对象设置代理’参数是字典类型的数据’键名是协议类型’键值是代 理地址(注意,此处的代理地址前面需要加上协议’即‖ttp://或者http5://),当请求链接使用的是

HTTP协议时’使用http键名对应的代理地址’当请求链接使用的是HTTPS协议时,使用http5键 名对应的代理地址°这里我们把代理本身设置为使用HTTP协议’即代理地址前统一加‖ttp://°

创建完pro×γ‖a∩d1er对象之后’调用bu11d—ope∩eI方法并传人该对象,创建了一个0pe∩er对象, 赋值为ope∩er变量’这样相当于此对象已经设置好代理了。接着直接调用ope∩er变量的ope∩方法’ 运行结果如下: J

[ ■0

arg5": {}』



■』‖』可■」·‖

就访问到了目标链接。

"柯e日der5": {

"05eI-∧ge∩t": "pytho∩ˉur111b/3·7!!’ "Xˉ咖Ⅲ∩ˉ「I己ceˉId": "Noot二1ˉ6oe9己1b6ˉ0a20b8a6788“aOb2己b4e889"

}’

"Or1gj∩": ||210·17〕°1·204"’ "ur1"; "门ttp5://www·‖ttpbj∩.oIg/get"

』‖‖』‖

"∧cceptˉ[∩〔od1∩g||: "1de∩tjty"」 "什o5t": !』硼w。httpbj门·oIg"’



可以看到结果是JSON数据’其中有_个orjgj∩字段标明了客户端的IP。验证之后,此处的IP 如果遇到需要认证的代理.可以使用如下方法设置: 十Io川‖I111b·erroImport0R儿[rIor

十roⅧur11jb°Ieque5t1Ⅶportpro×γ扑a∩d1er’ buj1d-ope∩er proxy≡ u5er∩a肌e:pa55word0127。0.0·1:7890| proxy-帕∩d1er=proxy‖a∩d1er({ |}]ttp| : ‖∩ttP://| +PIoxγ’ |‖ttp5‖ : ,∩ttp;//! +pIOXγ 勺`



■■■』■』■司·』=■■■■]■■□‖己■■■□■

确实为代理IP’并不是真实IP。这样我们就成功设置好代理’并可以隐藏真实IP了。

})

ope∩er=bu11dope∩er(proxyˉh己∩d1eI) try:

A

re5po∩5e=ope∩eI。ope∩(|们ttp5://州.‖ttpbj∩.org/8et|) pri∩t(Ie5po∩5e·read().decode(!ut「ˉ80 ))

d





0|



| 巴■厂

9.l



代理的设置

333



ex〔ept0R[[rroI日se: Pr1∩t(e.rea5o∩)

■ ■ | | } = 尸

跟上面的代码相比,这里只是改变了prOXy变量的值’只需要在原来值的前面加人代理认证的用 户名和密码即可,其中u5er∩a∏e是用户名, pas5"ord是密码°例如用户名是fbo’密码是bar,那么

| |

代理地址就是+oo:[email protected]:7890°

■ ■ ∏ 》 ■ β

′ | β卜|



如果代理是SOCKS类型’那么可以用如下方式设置代理’注意需要在本机789l端口运行-个 SOCKS代理: iⅧpOrt5OC恨5 jⅦpOrt5OC代et

·

+ro阳uI11ibi‖portreque5t 于roⅧur11ib.error加poIt0R[〔rror

5oc代5.5etde十a01tˉproxy(5o〔|〈5·5叹Ⅸ55’ 0127。0。α1‖’ 7891) 5oc代et°5O〔促et≡5o〔k5·5o〔k5oC代et

尸[甲|巴尸』尸|)■「》|卜|卜『

try目

re5po∩5e=reque5t·ur1ope∩(‖http5://w洲.httpb1∩.org/get0) pri∩t(re5po∩se.read()·decode(0ut+ˉ80 )) except0R[[rror日5e: pri∩t(巳rea5O∩)

此处需要用到—个socks模块,可以执行如下命令安装:

》|■「『‖■■■「|尸|■厂′广「仁任‖}β■■『「|■▲尸‖||》巴■■『‖|卜◆【「|匹■■『『||』■■厅‖||■■尸‖|

P1p3 1∩5ta11pγ5o〔低5

运行成功后的结果和使用HTTP代理的结果—样: {

"arg5": {}′ "∩eader5徽: { "Acceptˉ[∩〔odi∩g": "jde∩tjty"’ "‖o5t00 : "咖w°httpbj∩·org00’

■ ■ ■ ■ ■ ■ ■ ■

9 ■ ■ ■ ■ ■ ■ ■

"05erˉAge∩t": "pyt∩o∩ˉur11jb/3·7"’ "xˉ蛔z∩ˉ丁mceˉId": 冈Root=1ˉ6oe9a1b6ˉoa20b836788“a0b2ab4e889"

}′

"or1gi∩"; |0210。173.1。204|0』 "ur1o0 : "https?//wⅧ‖·∩ttpbj∩·oIg/get" }

结果中的orjgj∩字段同样为客户端的IP’证明SOCKS代理设置成功° 3ˉ『equests的代理设置

对于requests来说’代理设置非常简单’只需要传人Proxje5参数即可°这里以我本机的代理为 例’看一下requests的HTTP代理设置,代码如下: i∏pOrtreqqe5t5

pro×y= ‖127。O.0.1:789o| Proxies={ 0∩ttp‖ : 0http;//’ +proxy’ 0http50 : !∩ttp://! +proxy’ } tIγ8 ′

respo∩5e=reque5ts。get(|http5://‖咐‖w.httpbj∩.org/get0 ’ Prox1e5=Proxie5) Prj∩t(Ie5po∩5e.text) ex〔eptreque5t5。e×ceptjo∩5·〔o∩∩e〔tio∩[Irora5e: pIj∩t(,[rrOI ’ e.己rg5)

运行结果如下: {

"己rg5": {}’ "∩eader5": {

"A〔〔ept": "*/*"’



,0∧〔〔eptˉ[∩〔od1∩g": ||gzjp’ de十1己te"’ "‖o5t": ’Www.httpbi∩.org"’

"05erˉAge∩t": "pγt∩o∩ˉreqUe5t5/2.22。0"』

"Xˉ蛔z∩ˉ丁mceˉId00 : 00Root二1ˉ5e8「358dˉ87913+68a192十b9千87日ao323"

}’

"origj∩"吕 "210。173.1·2O』"’ "uI1o0 : "http5;//w毗‖·‖ttpb1∩·org/8et00

|』Ⅷ■《‖‖|■∏『))|』■■勺‖}『||■■」=■(}√|』□■■■]|‖|■■】

第9章代理的使用

334



运行结果中的OIjg1∩字段若是代理服务器的lP,则证明代理已经设置成功。 如果代理需要认证,那么在代理地址的前面加上用户名和密码即可,写法如下: pIoxy= !u5er∩己"e:pa55Ⅶord@127。O.O。1:789O‖

大家在使用时,根据自己的情况替换u5er∩a‖e和Pa55word字段即可° 如果代理类型是SOCKS’可以使用如下方式设置代理: 1们POrtIeqUe5t5

pro×y= |127。0.0。1;7891! prox1eS={ 0http0 : 05o〔代s5://, +proxγ’ |∩ttP5‖ : |5O〔代55://‖ +PrOxy ]



try:

re5po∩5e≡req0e5t5°get(‖http5://www.bttpb1∩.org/get0 ′ pIox1e5=pIoxje5)

prj∩t(respo∩5e.te×t〉 ex〔eptIeque5t5.ex〔eptio∩5.〔o∩∩ectio∩[rrora5e: pri∩t(0【rror ’ e.aIg5)

这里我们需要额外安装一个包requests[socks]’相关命令如下: p1p31∩5t己11 "Ieq0e5t5[5oc|(5]"

运行结果和使用HTTP代理的结果完全相同: { 『0

aIg5": {}’ "∩e己der5": { "A〔〔ept同: "*/*"’ "∧c〔eptˉ[∩〔odj∩g": 『|g乙1p’ de+1己te"’ "‖o5t": "卿.httpbi∩.oIg ’ "O5erˉAge∩t"; ,Wt∩o∩ˉreque5t5/2·22.0"’ M

』■■」■■■』■■■Ⅵ‖|■■】■‖|‖|二■■■可·ˉ■■习|」口Ⅲ■■‖」·司从‖日。||■飞·出|』■·』纠■‖』ˉ■‖■·‖□司■■■』■■】■

和ur‖lib_样、当请求链接使用的是HTTP协议时’使用http键名对应的代理地址’当请求链接 使用的是HTTPS协议时,使用∩ttp5键名对应的代理地址’不过这里的代理同样统一使用HTTP协议。

"Xˉ∧m∩ˉ『ra〔eˉId": "Root=1ˉ5e8f364aˉ589d]〔千25o0fa十d47b5560十2"

}’ "Or1g1∩": "∑10°173。1.2O4"’ "uI1": "∩ttp5://www.httpb1∩°oIg/get" }

另外’还有_种设置SOCKS代理的方法’即使用socks模块’需要安装socks库, 这种设置方法

1‖portIeqqest5 1∏pOrt5O〔代5 1‖port5o〔促et

5o〔k5.5etde+au1t_pIoxy(5o〔代5.5叹氏55’ ‖127.o.o.1‖ 」 7891)

‖』({

如下:



5O〔ket.5OCRet=5OC代5。5OC代5OCket

try:

exceptreque5t5°ex〔eptio∩s.〔o∏∩ectjo∏[rroIa5e:

·司■■‖』■

re5po∩5e=reque5t5·get(|}|ttp5://www.httpbj∩.oIg/get0 ) pri∩t(re5po∩5e.text)

prj∩t([rror|’ e.aIg5) ‖





| 9.l

代理的设置

335

运行结果和上面是完全相同的。相比第一种方法,此方法属于全局设置。大家可以在不同情况下 [■『|‖■尸‖|||

p

| 坠 ■ ■ ■ 『 「 ‖ 旦 尸 「 ’ 『 ■ 【 = 尸

‖ 伊■『〉》卜



′■「



甩巳■厂》仕



选用不同的方法°

4h仗px的代理设置

h肮px的用法本身就与requests的非常相似,所以也是通过proxje5参数设置代理’不过也有不同, 就是PIox1e5参数的键名不能冉是们ttp或∩ttp5’而需要改为∩ttp://或https://° 设置HTTP代理的方式如下: jⅧpOrthttpX

proxy= 0127。o°0·1:789o0 pIOXie5二 { 0http;//0 ; !∩ttp://! +prOXy’ 0∩ttp5;//0 : !bttp://0 +proxy’ }

Wit∩httpX.〔1je∩t(prO〉《1e5=prOXje5〉己S〔11e∩t:

Ie5po∩5e=〔11e∩t.get(′http5://棚·∩ttpb1∩.org/get′) Prj∩t(Ie5PO∩5e.text)

对于需要认证的代理也是在代理地址的前面加上用户名和密码’在使用的时候替换u5er∩a∏e和 pa55word字段: proxγ= |u5er∩a|∏e;pa55woId@127。o。0。1:7890|

运行结果如下: { L

00



arg5": {}’

"header5"; {

′卜}卜)

~尸「△●

"∧〔〔ept"; "*/*"’

!|A〔ceptˉ[∏〔odj∩g"8 "gzjp’ de+1ate"’





"肋5t": "哪.httpb1∩。org’

"0serˉ∧ge∩t": "pytho∩ˉhttpx/O。18·1"’

"Xˉ枷z∩ˉ「ra〔eˉId"; "poot=1ˉ60e9a3e十ˉ5527仟632048q十8e46d39834"

` 」》

0『orjgj∩": "21O·173.1·2O4"’ "ur1": "http5://w‖‖w。httpb1∩。org/8et" }

对于SOCKS代理,需要安装httpxˉsocks[asyncjo]库,安装方法如下; ■=■△【尸厂‖|◆『

p1p3j∩sta11 』|httpxˉ5o〔k5[a5y∩c1o]"

与此同时’需要设置同步模式或异步模式°同步模式的设置方法如下: mpOItbttp×

■■■■厂|‖止■尸|||亡■尸

千roⅦ∩ttpx=5oCR5i‖port5y∩cproxy丁r己∩sport

tra∩5port=5y∩Cproxy丁ra∩5port.fro们ur1(』5o〔|(55://127.O.O。1:7891‖) wit∩httpx.〔11e∩t(tI己∏5port≡tm∩5port) a5c1je∩t;

Ie5po∩se=〔11e∩t.get(‖∩ttp5://‖‖ww。∩ttpbi∩。or8/get‖) pI1∩t(re5po∩S巳text)

》|》■面■|△β卜尸β



这里设置了一个丁m∩5port对象’并在其中配置了SOCKS代理的地址’同时在声明httpx的〔11e∩t 对象时将此对象传给tm∩5Port参数,运行结果和刚才一样° 异步模式的设置方法如下: 1‖pOrt∩ttpX 1阳pOIt3Sγ∩〔1O 0

+rO们∩ttPX≡5O〔比加POIt∧5y∩〔pIOXγ丁ra∩5POrt





aSy∩〔de十阳己1∩():

a5y∩〔withhttpx.∧sy∩〔〔1ie∩t(tm∩5port≡tr己∩5PoIt〉 a5〔1je∩t: respo∩5e≡awajtc1je∩t.get(|http5://硼Ⅶ.httpb1∩.org/get‖)

{{

tr日∩5POrt=∧5y∩〔prOXy丁ra∩5POrt。千rOⅧUr1( 5o〔k55://127,O·O.1:7891‖)

』■可‖|■■□■曰司‖■■』■

第9章代理的使用

336

prj∩t(re5pO∩5e.teXt) ∩3『∏e

i千

∏日i∩

‖:

和同步模式不同’此处我们用的丁ra∩5port对象是∧5y∩cproxy丁ra∩5port而不是5y∩cproxy『ra∩sport’

Selenium同样可以设置代理’这里以Chrome测览器为例介绍设置方法°对于无认证的代理’设 置方法如下:

‖‖

5.Se|e∏|u∏]的代理设置

·』

同时需要将〔1ie∩t对象更改为∧5y∩〔〔1je∩t对象,其他的和同步模式-样’运行结果也是_样的°

|‖

≡′

asy∩c1o.get←eve∩tˉ1oop().ru∩ˉu∩tj1-〔o"p1ete(们ai∩())



+ro∏Se1e∩iu们j∩lportⅣebdr1ver proxy≡ |127°0·O·1:789O‖

optjo∩5≡webdriγeI.〔hro∩e0ptjo∩5()

prj∩t(brow5eI·pa8e_5our〔e) bro"5er·〔1o5e()

运行结果如下: { 00

日rg5"; {}’ "he3der5": {

己pp1i〔atio∩/5ig∩edˉe×〔∩a∩ge【γ=b3jq=O.9"’ 』‖∧c〔eptˉ[∩〔odi∩g": "gz1p’ de「1ate,0’ 00∧〔〔eptˉ[己∩gu日ge": "zhˉ〔‖’zh『q=O。9"’ "‖o5t": "wM‖·httpbi∩.org′!」 ml」pgradeˉI∩5e〔ureˉReque5t5": ||1"」

〔hro∏冶/8O.O.〕987.149S3千ari/537°36"’ "Xˉ枷z∩ˉ丁ra〔eˉId||; "Root=1ˉ5e8+39〔dˉ60930o18205十d154a9a+39〔c||

}’ "Origj∩": "21O。173.1。204"’ 〕

"ur1": "http://…·httpbi∩·org/8et"



结果中的or1g1∩字段同样为客户端的IP’证明代理设署成功° 如果代理需要认证,则设置方法相对比较烦琐,具体如下: +roⅦ5e1e∩1uⅦ1呻ort眶bdr1γer

q



』叮■■■可■■‖』■■■√|‖门』■■■■』■Ⅵ‖|

"05erˉ∧ge∩t": "‖ozi11a/5.o(‖a〔i∩to5∩j I∩te1‖日c05X10154)∧pp1e‖e冰1t/537。〕6(N‖ˉ「‖[’11keCe〔ko)



‖{

"∧c〔ept"8 00text/ht们1’app1j〔atjo∩/x∩t"1+x"1』己pp11〔日t1o∩/xⅧ1iq=O·9’j们age/webP』1们age/aP∩g’*/*jq=O。8’

0

‖‖‖‖|

OptiO∩5.日dd—argU们e∩t(!ˉˉPrO》(yˉ5erver=httP://| +PrO)〈y) bro"5er="ebdr1γer.〔∩ro仍e(opt1o∩5二optjo∩5) brow5er.get(』http5://洲w.∩ttPbi∩。org/get‖)

十ro∏5e1e∩1Ⅷ。webdrjγer。〔hro们e。optjo∩5mportOptjo∩5

i川portzjp伍1e

ip= ‖127·O·0·10 port=7890

们a∩j十e5t-j5o∩= ′‖"00{"veI5jo∩":"1.o.o"’"川己∩j+e5tγer51o∩": 2」"∩a爬":"〔hro们epro×y"’"pemn551o∩5";

["proxy」』’"tab5"’"l」∩1imted5torage"’"stoIage"」"〈a11 〔」I15〉"’"webReq(」e5t"’0WebReque5tB1oc|〈1∩g"]』"ba〔|(grou∩d" {"s〔ript5′|; ["b己c代8rou∩d.j5||]



|‖‖

05er∩a川e= ‖+Oo|

pas5woId= 0b日I‖



〕 」



q

β】 0■ 0■

‖』■|■司』·』《、」|‖|□γ

|「 △伊』卜}■■

9l代理的设置

337

p



0‖

们Ode: "+1Xed5erγer5 ’

ru1e5: {

β『「●|

巴仔∏『卜旦■■『|

baC爬grou∩d↑S≡ """

γar〔o∩十jg={

5i∩g1eprOXy; {

飞 卜

5C们eⅦe: "∩ttp"’ 们o5t: "%(ip) 5』』’ POrt;腮(POrt) 5



] 『 J



0



■■「|‖‖=■「‖‖■■尸

chr○爬.pro×y.5ett1∩gs.5et({va1ue: co∩于ig′ 5〔ope: reg[』1ar"}’ 十u∩ctjo∩(){})】 0l

{u∩ctio∩Ca11b日c假「∩(detaj15){ retur∩{

Ⅷ门』

auth〔rede∩tja15: {l」5eⅢ∩a们e: "%(05eI∩a‖e) S"’ pa55"ord: "%(pa55"ord) 5" } 飞

‖}「卜匣 ●





o"e·web∩eque5t。o∩∧uthRequ1red.add[iste∩er( c己11ba〔|〈「∩’{ur15: C己11ba〔促「∩’{ur15: [" [||<a11ur15〉"]}’[|b1o〔{〈1∩g‖])

Ch IO∏‖e·WeDNeqUe5〔.o「l∩u〔『‖ 〔『] β‖‖■尸【厂『

%{‖jp! :: ip’ port0 : "0|"%{‖jp! iP’ !Port0

port’ 0 l』5er∩a∏e0 : u5er∩咖e’|pa55word‖: pa55word}

》「|似『|

‖■》 口 · 「 『 ′ 巴 尸

p10g1∩一+11e= prOXy-a0th-p1ugi∩.Z1p| Ⅳ1thZ1P十j1e.Z1P「i1e(P1ugj∩-十j1e」W‖) a52P:

zP.Ⅳr1te5tr(||川a∩1+e5t。jSO∩"’ 们a∩j十e5t-j5o∩) ∑p。wrjte5tr("bac|(gIo0∩d·j5"」 ba〔|〈gro‖∩d-j5)

OptiO∩5≡0ptio∩5()

optio∩5·add-arg(』爬∩t("_ˉ5tartˉ"axiⅦ1zed") optio∩5.addexte∩5jo∩(p1ugi∩-十j1e)

卜■|

browseI="ebdr1ver.〔hro侧e(optio∩5=optjo∩5)

bro"5eLget(0bttp5://wwv0.httpbi∩.org/get‖)



□′■■■『「『■「

prj∩t(bro"5er·page_5ource) bro"5eI。〔1o5e()

这里在本地创建了一个manifestjson配置文件利backgroundjs脚本来设置认证代理°运行代码后,

巳【「【「|■■『}‖●■‖|■尸}■■|{【■【‖【『■■■「|

本地会生成一个proxy-authˉpluginzip文件来保存当前的配置。

运行结果和上面一样’orjg1∩字段为客户端的IP’证明代理设置成功°

SOCKS代理的设置方式也比较简单’把对应的协议修改为5oCk55即可,如无密码认证的代理设 置方法为: 千ro"5e1e∩ju‖1‖portwebdr1γer

proxy= ‖127°0°o.1;78910

optio∩5≡ ‖vebdrjver.〔hm『旧Optio∩5()

optjo∩s.addˉargu川e门t(0ˉˉproxγˉ5erγer≡5o〔|〈55://! +pro》(γ) brow5eI=webdriver.〔hro爬(opt1o∩5≡optio∩5)

bro"5er.get(0‖ttp5;//‖"‖γ‖.‖ttpbj∩.org/get‖) pr1∩t(brow5eI.pageˉ5o仙r〔e) brow5er.c1o5e()

运行结果和上面—样°

6.a|oh仗p的代理设置

对于aiohttp’可以通过proxy参数直接设置代理°HTTP代理的设置方式如下: 1"port日5y∩cio 1∏pOrta1o‖ttp

proxγ= ‖‖ttp://127。o.0.1:78900







338

第9章代理的使用

a5y∏CdefⅧ己j∩(): aSy∩c"ithaiohttP.〔1ie∩t5e55jo∩() a55e551O∩: 日5y∩〔w1th5es5jo∩.8et(|http5://州·∩ttpb1∩.org/get|’ proxy=pIo×y) asreBpo∩5e pri∩t(日"ajtre5po∩5e.text())

j+

∩a‖旧



Ⅶ己1∩

0

a5γ∩cio。getˉeγe∩t-1oop()°ru∩u∩ti1ˉ〔o川p1ete(门ai∩(〉)

如果代理需要认证’就把代理地址修改_下:

对于SOCKS代理,需要安装-个支持库aiohttpˉsocks,安装命令如下: p1p] 1∩Sta11ajOhttpˉSO〔k5

可以借助这个库的proxy〔o∩∩ector方法来设置SOCKS代理,代码如下 1朋porta5y∩〔1o

mport日johttp

fro们aiohttp-5o〔Ⅶ5加portproxy〔o∩∩e〔tor

〔o∩∩e〔toI=proxy〔o∩∩e〔tor。十ro们ur1(05o〔促55://127·0°O.1:7891』) aSy∩〔de十们aj∩(): a臼y∩〔with31O∩ttP.〔1je∩t5e55iO∩(〔O∩∩eCtOI=〔O∩∩e〔tOr) 日55e551O∩:

asy∩〔"ith5e5s1o∩.get(』∩ttP5://哪√.httpbi∩·org/get‖) 日sre5pO∩5e: pr1∩t(a"a1tre5pO∩5e。text()) i十

∩己‖7沦

== 0

们■j∩

|‖‖』■〗‖‖■‖勺‖‖‖●∏〗』■■■可|」』□`‖则」《‖〗』●】刽|‖』】‖引

pro×y= !侗ttp://u5er∩a们e:pa55word@127。O·o。1:7890|

0

0:

a5y∩c1o。getˉeve∩tˉ1oop().ru∩ˉu∩ti1—〔o们p1ete(Ⅷaj∩())

运行结果依然和之前-样°

另外’aiohttpˉsocks库还支持设置SOCKS4代理`HTTP代理以及需要认证的代理’详情可以参 7尸yppetee『的代理设置

对于Pyppeteer’由于其默认使用的是类似Chrome的Chromjum测览器’因此设置代理的方法和 使用Chmme测览器的Selenjum—样’例如都是通过arg5参数设置HTTP代理的,代码如下: j呻Orta5y∩C1O

+Io‖pyppeteermport1au∩〔∩ pro×y= 0127.O·O°1;78900

己5y∩〔de十爪aj∩();

日wajtpage.goto(|http5;//wwwhttpbi∩。org/get‖) pri∩t(己w己itp日ge.〔o∩te∩t()) a"己itbro"ser.c1o5e()

i十

∩己∏e



们a1∩

‖。



a5y∩c1o。8et—eγe∩tˉ1oop〈)·ru∩u∩t11—〔oⅧP1ete("ai∩())

运行结果如下: { 00

arg5": {}’ 』0header5": {

"A〔〔ept": "te)(t/∏tⅧ1’app1j〔at1O∩/X∩t们1+X∩1’app11〔at1O∩/〉(们1jq≡0.9」i∏|age/WebP’i∏age/aP∩g』*/*jq=O。8"’ "∧cceptˉ[∩〔od1∩g"; "gz1p’ de+1ate’ br"’ "∧〔〔eptˉ[日∩gUage": "Zhˉ〔‖’2h】q=O·9"’

乙■■■引|■■■‖』]|□■‖·‖{||■■■可|』□■‖|』』■‖‖√|』■■■‖』〗』□‖】■‖‖‖〗Ⅵ〗』■‖||□]】勺||·】」■』

bIow5er=await1au∩〔b({↑aI85! : [‖ˉˉproxyˉ5eIγer=http://0 +pro》(y]’ ‖bead1e5s0 : 「a15e})

p己ge=a"ajtbro佣5er.∩e倒page()

引引|‖日‖‖|■〗‖{●』□」‖可||可‖|

考官方介绍。

9.l

代理的设置

339

||什o5t": "咖‖.httpb1∩.org"』 000pgr3deˉI∩5e〔ureˉReque5t5": "1"’

"05erˉ∧8e∩t": "№zj11a/S.o(阳〔j∩to5∩j I∩te1"a〔O5X1o—15=4)∧pp1e‖eb瓜jt/537.36 (阳丁‖[’ 1j|〈eCe〔|〈o) 〔∩ro佃e/69.0·M9』·O5a+己Ii/537°3600’ "Xˉ咖z∩ˉ『raceˉId|; "【oot=1ˉ5e8于442cˉ12b1ed7865b049"7267a66〔"

}』 "oI1gj∩"; "21O·173·1·2040`’ 0Ur1": "∩ttp5://州。打ttPbi∩·org/get" }

同样可以通过or1g1∩字段证明代理设置成功° SOCKS代理也一样,只需要将协议修改为soc促SS即可,代码如下: 1"poIta5y∩〔1o

十ro们pγppeteerjⅦport1au∩c们

pIoxy二 0127·0.0。1:7891|

3Sy∩〔de+∏a1∩():

brow5er=a"ajt1au∩〔}‖({0arg5』: [|-proxγˉ5erγer=soc|〈55://0 +pro)(y]’ 』bead1e55』: 「a15e})

page=a"aitbro田5er.∩ewp己ge()

a"ajtpa8e。goto(!∩ttp5;//№蛔.∩ttpbi∩.oIg/get,) prj∩t(3waitpage.CO∩te∩t()) a"aitbro切5er.〔1o占e()

j十

∩3∏`e

=, 刚a1∩

8

a5γ∩〔io.getˉeγe∩t_1oop().ru∩0∩ti1-co们p1ete(爪aj∩()) 运行结果也是一样的°

8.p|ayw『|ght的代理设置 ■■■■■■■■■

相对Selenium和Pyppeteer’Playwright的代理设置更加方便’因为其预留了一个proxγ参数’在 启动的时候就可以设置°



对于HT丁P代理来说,可以这样设置: 十I咖p1aγwrjght。5γ∩〔-ap1mport5γ∩〔-p1aywr1ght wit‖5y∩〔ˉp1aywIight()a5P: bIo仍5er=p。chr刚iuⅧ.1au∩〔h(proxγ≡{ 05erγeI0 怠 0http://127.0.o。1:78900 })

page=bⅢ叫5eI.∏团ˉpage()

page。goto(,http5;//b‖啪0.∩ttpbi∩.org/get‖〉 pri∩t(p己ge.co∩te∏t()) br叫5eI。〔1ose()

在调用1au∩〔∩方法的时候,可以传人prOxy参数,它是一个字典,其中有一个必填的字段叫作 5erγer,试里我们直接填人HTIP代理的地址即可° 运行结果如下: {

"己rg5口: {}’ ■∩eader5.; {

阑A〔〔ept■: 口text/们tⅦ1’app1i〔atio∩/xhm1+m1’app1icatio∏/m1βq=o°9』ma8e/aγi千’i阳ge/webp』mage/ap∏g’*/*; q=0.8’app1i〔atjo∩/5ig肥dˉexcha∩geW■b3;q■O.9口’

口A〔〔eptˉ[∩〔odi∏g口; ■gzip’ de千1atep br口’ 口ACCeptˉla∩gu昭e阑: ■Zhˉ〔‖’∑∩;q=0.9曰’ ■

口肋5t口: ■…·httpbi∏·org’

圃5e〔ˉ〔∩ˉ0己.: 口\口‖otAj8r己∏d\口W=\.99\.’\曰〔hrmim\口;γ=\口92\冈■’

005e〔ˉO]ˉ0aˉ№bi1e凹: "?O■’ "5e〔ˉ「et〔hˉDe5t": "do〔u贬∩t阐’

闪5e〔ˉ「et〔hˉ№de词: "∩aγi8己te口’ 闪

曰5e〔ˉ「et〔∩ˉ5jte伺: "∩O∩e ’



q

"5e〔ˉ「et〔hˉ05eI": "?1"’ "0pgradeˉI∩5e〔ureˉ【eque5t5": "1"’

"05erˉAge∩t00 : "问ozi11a/5·O(例a〔i∩to5们j I∩te1‖a〔05X10-15≡7)∧pP1e‖ebⅨjt/537.36(肘‖丁"[′ 1i|〈eCe〔ko) 〔hro∏e/92·o°4498·o5己十ari/537。36"’ 、

)’

1



对于SOCKS代理’设置方法也完全_样’只需要把5erγer字段的值换成SOCKS代理的地址即可:



司|·』·

"or1gi∩": |′210。173·1.∑O4"’ 0ouI1": "https://州°httpbi∩°org/get"



』叼‖|‖当■

"XˉA∏]2∩ˉ丁mceˉId": "Root=1ˉ60e99ee+ˉ4+a746a01a38日bd469ecb467| |

□ ■ ■ ‖ 』 ■ |

第9章代理的使用

340

千ro爪p1aywright·5y∩c-ap1mport5y∩〔≡p1aywr1ght

0

5erver 弓

0

5o〔促55://127°O·O°1:7891『

‖(□‖

Wj恤5y∩〔一p1己yWrig们t() a5p: brow5er=p.c们Iom咖。1己u∩ch(proxy≡{ })

′′

p己ge=brow5eI.∩ew一page() p日ge.goto(0∩ttp5://www·httpbj∩.org/get‖) pri∩t(pag巳〔o∩te∩t()) brow5er·c1o5e()

对于需要认证的代理’只需要在proxy参数中额外设置u5er∩a∏e和pa55woId字段即可’假设用 千ro‖p1aywrjg‖t。5y∩〔-apj1∏port5γ∩〔-p1aγwr1ght

}) page=brow5eI。∩e比page()

‖ ‖ {

05erγeI|: 0http://127·0·0·1:7890|’ u5er∩日‖]e0 ; |「oo! 」 p355word0 : 0b3r|

‖(‖

"jth5y∩〔ˉP1己yWrjg向t() 己5P: brow5eI=p.〔∩romu阳.1a0∩〔h(proxγ={

《‖ˉ■■

户名和密码分别是fbo和bar’则设胃方法如下;

‖|‖‖

运行结果和刚才也完全-样°



page.goto(0http5://|√ww.httpb1∩.org/get0) pr1∩t(p己ge.〔o∩te∩t()) brow5er.c1o5e(〉

本节我们总结了各个请求库的代理设置方法’这些方法大同小异,学会这些之后,以后再遇到封 IP问题’就可以轻松通过设置代理的方式解决°

本节代码见https://githuhcom/Python3WebSpider/ProxyTest°

9.2代理池的维护 我们在9.l节了解了给各个请求库设置代理的方法,如何实时高效地获取大量可用代理变成了新 的问题。

首先,互联网上有大量公开的免费代理当然我们也可以购买付费代理但无论是免费代理还是 付费代理,都不能保证是可用的’因为自己选用的IP′可能其他人也在用,爬取的还是同样的目标网 站,从而被封禁,或者代理服务器突然发生故障、网络繁忙°一旦选用的是_个不可用的代理,势必 就会影响爬虫的工作效率°所以要提前做筛选’删除掉不可用的代理’只保留可用代理°

那么怎么实现呢?这就需要借助~个叫代理池的东西了°本节就来介绍一下如何搭建—个高效易 用的代理池。



‖日·‖·」『■■‖■|·■』■∏|」■■刘』勺』■■■■可‖■月■∏‖口乙■■』●]□■‖‖‖』刮■■

9总结

‖(

}■尸}

} 92代理池的维护

34l

匹■■「}||}〖■厂||『■∩∏■■厂|‖似■■「似‖卜■■■■『|■■■■■■厅「β▲=尸似|■■■『■■■■「■■■∏『卜》『β●『『卜『『伊尸

↑.准备工作

存储代理池需要借助于Redis数据库’因此需要额外安装Redis数据库°整体来讲’本节需要的 环境如下°

□安装并成功运行和连接一个Redis数据库,它运行在本地或者远端服务器都可以’只要能正常

连接就行’安装方式可以参考https://setupscrape.center/redis □安装好一些必要的库’包括aloh肮p、requests、redisˉpy`pyqueIy`Flask、loguru等,安装命令 如下: pip31∩三ta11ajo们ttpreque5t5redi5pyquery十1日5代1oguru

2ˉ代理池的目标

我们需要实现下面几个目标来构建_个易用高效的代理池°

代理池分为4个基本模块:存储模块`获取模块、检测模块和接口模块°各模块的功能如下。

□存储模块:负责存储爬取下来的代理°首先要保证代理不重复’标识代理的可用情况’其次要 动态实时地处理每个代理,一种比较高效和方便的存储方式就是Redis的So冗edSet,即有序 集合。

□获取模块:负责定时在各大代理网站爬取代理°代理既可以是免费公开的’也可以是付费的’ 形式都是IP加端口。此模块尽量从不同来源爬取’并且尽量爬取高匿代理’爬取成功后将可 用代理存储到存储模块中。

可』』

□检测模块:负责定时检测存储模块中的代理是否可用°这里需要设置_个检测链接’最好是设

置为要爬取的那个网站’这样更具有针对性。对于—个通用型的代理,可以设置为百度等链接° 另外,需要标识每_个代理的状态’例如设置分数标识’l00分代表可用’分数越少代表越不





β

可用°经检测’如果代理可用’可以将分数标识立即设置为满分l00,也可以在原分数基础上



=■尸‖β『卜‖■■=|■β■■

加l;如果代理不可用’就将分数标识减l,当分数减到一定阑值后,直接从存储模块中删除 此代理°这样就可以标识代理的可用情况,在选用的时候也会更有针对性° □接口模块:.用API提供对外服务的接口°其实我们可以直接连接数据库来获取对应的数据’但 这样需要知道数据库的连接信息’并且要配置连接。比较安全和方便的方式是提供_个WebAPI 接口’访问这个接口即可拿到可用代理°另外’由于可用代理可能有多个’所以可以设置一个 随机返回某个可用代理的接口,这样就能保证每个可用代理都有机会被获取’实现负载均衡。

以上内容是设计代理池的_些基本思路。接下来,我们设计整体的架构,然后用代码实现代理池。 3.代理池的整体架构

|‖|『

■■■■■■

结合上文的描述,代理池的整体架构 如图9ˉl所示°

结合这张图’再简述一下4个模块的

由_蹦=o 获取攫块

存储攫块

检澜摸块

功能°

■■价}巳■尸}‖『卜〗『「=》『

□存储模块使用Redis的有序集合’ 负责代理的去重和状态标识,同时 它是中心模块和基础模块,用于将 其他模块串联起来°

□获取模块定时从代理网站爬取代 理’将爬取的代理传递给存储模 块’并保存到数据库°

| <烹》 接□攫块

图9ˉl 代理池的整体架构

广伊

}伊)



342

第9章代理的使用

□检测模块定时通过存储模块获取所有代理,并对代理进行检测’根据不同的检测结果对代理设 置不同的标识。

□接口模块通过webAPI提供服务接口’接口通过连接数据库并通过web形式返回可用的代理° 4.代理池的实现

接下来我们分别用代码实现代理池的4个模块。

注意完整的代码,代码量较大’因此本节我们不会详细编写’大家了解源码即可’源码地址为 https://githuhcom/Python3WebSpide∏ProxyPool° ●存储模块

存储模块使用Redjs的有序集合,集合中的每个元素都不重复’对于代理池’集合中的元素就是

代理,是IP地址和端口号的组合,如6O.2O7.2〕7.111:8888。另外,有序集合中的每个元素都有_个

分数字段’分数可以重复’既可以是浮点数’也可以是整数。集合会根据每个元素的分数对元素进行 排序’分数值小的元素排在前面’大的排在后面’这样就实现了有序°

具体到代理池,分数可以作为判断—个代理是否可用的标志: l00为最高分,代表最可用; 0为

勺‖』■可

最低分,代表最不可用°如果要获取可用代理’可以从代理池中随机获取分数最高的代理°注意这里 是随机’能够保证每个可用代理都有机会被调用。

分数的设置细节是新获取的代理的分数为l0’如果经测试是可用的,立即将分数置为l00°检测

□当检测到代理可用时,立即将分数置为100’这样能够保证所有可用代理都有更大的机会被获 取°你可能会问’为什么不将分数加l而是直接设为最高值l00呢?设想_下,有的代理是从 各大免费公开代理网站获取的’_个代理通常并没有那么稳定,可能平均每5次请求中有2次 成功, 3次失败,如果按照这种方式设置分数,那么这个代理几乎不可能获得高分数’意昧着

□将新获取的代理的分数设置为l0,如果它不可用’就把分数减l,减到0的话就删除;如果可 用’则把分数置为l00°由于很多代理是从免费网站获取的,所以新获取的代理无效的概率非 常大,可用的代理可能不足10%。这里将分数设置为l0,到弃用最多检测l0次,没有可用代



』|‖|{{

网络繁忙或者其他人用此代理请求得太过频繁°

{|

□当检测到代理不可用时,把分数减l’分数减至0后,删除代理°按此规则’要删除_个有效 代理,需要连续不断失败l0O次°也就是说’当使用_个可用代理尝试了l00次都失败后,才 将此代理删除,一旦有_次是成功的,就重新置回l00。尝试机会越多,这个代理被拯救回来 的机会就越多,这样不会使_个曾经的可用代理轻易被丢弃’因为代理不可用的原因很可能是



」|‖|』

即便它有时是可用的’但是因为筛选的依据是最高分’也几乎不可能被调用。如果想追求代理 调用的稳定性’就要使用上述方法,这种方法可确保分数最高的代理_定是最稳定可用的°所 以’这里我们采取“可用即设置l00”的方法,确保代理只要可用就有机会被调用°

■‖‖』《●‖」■·‖■■□■■

这只是_种解决方案,当然还可能有更合理的方案°之所以按此方案设置’有如下几个原因°

|}日

器会定时循环检测每个代理的可用情况,_且检测到可用的代理就立即将分数置为l00;如果检测 到某个代理不可用,就将其分数减l,分数减至0后’删除代理。

理的l00次那么多,可以适当减小开销°

上述设置思路不一定是最优的,但据个人实测’实用性还是比较强的°这里首先给出存储模块的

||{{|

源代码’见https:〃githuhcom/Python3WebSpjder/ProxyPooⅡtree/masteI/proxypooⅡstomges’建议直接对 照源代码阅读°



d

9.2代理池的维护

0



343

代码中’定义了—个类Red15〔1je∩t来操作Redis的有序集合’其中定义了_些方法来设置分数、 获取代理等°核心实现代码如下: ]川pOrtred15

}}

■尸||卜巴矽■‖}‖

+ro们proxypoo1°except1o∩5 1"poTtpoo1[们pty[x〔eptjo∩ 千roⅧpIoxypoo1.5c∩e们a5·proxγmportproxy +ro0ⅥproxypooL5ettj∩g1ⅧportR[DI5‖O5丁』 R[DI5PO盯’ R[DISp∧55‖ORD’ R[DI5旺γ’ p偶0Xγ5〔OR["∧X’ PR0Xγ5〔OR[付I‖’ pR0Xγ5〔0旺I‖IT

十roⅧra∩doⅦ1∏port〔ho1ce 十ro‖tyP1∩g1∩Port [j5t 广



0司



↑ro‖儿oglm」1∏port1ogger

+roⅧpro×γpoo1.uti15·proxyi们poIt j5γa1jd-pro×y’ 〔o∩γertˉproxγ-or-proxje5 ~■『∩△■

R[0I5αI[‖『γ[恨5I0‖ =redi5.

γer5jo∩

R[0I5αI[‖「γ[R5I删.5t日rt5Wjth(02. ‖ )

) ●●

■司〗√

十吗

〔 巳

□◎

∩ ·

〈○

■■□▲

口[

勺·〗▲

□α



广儿



^民

卜胆尸「)冈匹尸尸[【矽伊[「卜|■■「■卜【■β■■■■■||伪■尸「|□【■■〖‖匹β‖。卜「|〖■‖|)[β∩‖||[■「||■尸|||卜‖〖‖‖

de十

●]



曰 巳 己

■』

□‖}■尸′山尸『|■■厂’|■「也ˉ尸

ISR[0I5γ[R5I0‖2=

i∩jt (5e1+’ ho5t≡【[0I5刚5『’ port≡R[DI5p0【∏’ pa55mrd=旺DI5p∧55刚RD’**灿arg5): 5e1十.db=redi5.5tr1ctRedj5(ho5t=∩o5t’port≡port’pa55word=pa55"ord」decode_re5po∩5e5=丁me’**促w己rg5)

de十add(5e1+’ pIoxy: pIoxy’ s〔ore≡pR0Xγ5〔0【[I‖I丁) ˉ>i∩t: j十∩oti5γa1jd-proxy〈千,{pIoxy.ho5t}:{proxy。port}|): 1o8ger.j∩十o(+‖i∩γa1idproxγ{proxy}’ t∩ro"1t|) retur∩

i十∩ot5e1+。exj5ts(pIoxy): i「I5R[0I5γ[【5I"2:

retur∏5e1十.db.zadd(R[0I5Ⅶ[γ’ s〔ore’ pIoxy·5tri∩g()) retur∩5e1千.db。zadd(R[0I5Ⅸ[γ’{proxy.strj∩g(): 5〔ore}) de+ra∩do↑∏(5e1十)ˉ〉proxγ: #尝试获取录大伍的代理

Proxje5=5e1十.db。zra∩8eby5〔ore(R[DI5R[γ’ pR0Xγ5〔0R[灿X’ pR0Xγ5〔OR[趴X〉 j「1e∏(proxie5):

retl」I∩〔o∩γert-proxγ—oIˉproxie5(choi〔e(prox1e5)) #否则根据分数排序

proxie5≡5e1「.db。zrevm∩ge(R[DI5赃γ」 pROXγ5〔帜["I‖’ pR0Xγ玫0R[酗X) i千1e∏(proxieS): retum〔o∏γert-proxγˉoI-pIo×ie5(〔hoi〔e(pⅢoxie5)) #否】‖极错

raj5ePoo1[呻ty[x〔eptio∩

de十de〔Iease(5e1千’ pIoxy: proxy)ˉ〉j∏t: 5〔ore=5e1f。db.Z5〔ore(R[0I5长[γ’ pⅢoxy。5trj∩8()) #当前分数比pR0Xγ亚低["I‖大 i「5〔o【ea∩ds〔ore〉P日0Xγ亚职[∩I‖;

1o88eI.i∩「o(+|{proxy雷5tⅢmg()}〔uⅢre∩t5〔oIe{5〔oIe}’ de〔Iease10) j十I5R[0I5γ[RSI侧2:

retum5e1于·db.Xi∩〔rby(R[DI5贝[γ’ pIoxy.5tIj∩g()’ˉ1) 【etur∩5e1千.db。zi∩Crby(R[DI5Ⅸ[γ’ ˉ1’ proxy·5trj∩g()) #否】‖臼除代理 e15e8

1oggeI.1∩「o(于,{pⅢoxy.strj∏g()}c仙rIe∏tBcore{5〔ore}D remve。) retuI∩5e1「.db.zrm(R[OI5Ⅸ[γ’ pIoxy.5trj∏g())

de十exjst5(5e1十’ pIoxγ: PIoxy)ˉ》boo1: retl」m∏ot5e1于。db蕾∑s〔ore(R[DI5Ⅸ[γ’ proxy.5tri∩g()) j5‖o∏e de「爬x(5e1十’ proxy: proxy) _〉i∏t:

1oggeI。j∏「o(f0{pIoxy.strj吧()}j5γ己1jd’ 5etto{PR似γ亚帜["AX}0)

ifI5R[DI5γ[【5I酬2:

ⅢetuI∩5e1f.dbˉzadd(R[0I5N[γ’ PR毗γ哑[肌X’proxy·5tIi∏g(〉) Ietur∩5e1f.db.zadd(R[OI5Ⅸ[γ’{pIoxy。5t【i∏g《): pR似γ5〔帆[趴X})



de+co0∩t(5e1十)ˉ〉j∩t: ret(」r∩5e1千.db.zcard(R[DI5旺γ)

de+a11(5e1千) ˉ〉 Lj5t[proxy]:

retur∩〔o∩γert≡proxy一or-proxje5(5e1于.db.zra∩geby5core〈R[DI5旺γ’pR0Xγ5〔0R[‖I‖’pROXγ5〔ORO‖AX)〉

de十b3tc∩〈se1千’ 5tart’ e∩d)ˉ〉U5t[proxy]:

retur∩〔o∩vertˉproxyˉor—proxie5(5e1+.db.zrevr己∩ge(旺DI5N[γ’ 5tart’ e∩dˉ 1)) j「

∩3‖↑恰

二≡ 0

们aj∩

0 :

Co∩∩=Redj5〔1je∩t() re5u1t=co∩∩.m∩do们(〉 pr1∩t(Ie5u1t)

这里首先定义了-些常量,如pR0Xγ5〔0R[ⅧX、pR0Xγ5〔0R[‖I‖、 pR0Xγ5〔0R[ I‖I丁分别代表最 大分数`最小分数`初始分数; R[0I5‖O5丁` R[DI5p0R丁` R[0I5p∧55‖0RD代表Redis的连接信息,即IP

地址、端口和密码; R[DI5旺γ是有序集合的键名’我们可以通过它获取存储代理所使用的有序集合。 然后在RedjS〔11e∩t这个类中定义了一些用来对集合中的元素进行处理的方法’这些方法如下°



1∩jt方法用于初始化’其参数是Redis的连接信息,默认的连接信息已经定义为常量°我

们在

1∩1t

方法中初始化了5tr1〔tRed15类,建立了Redis连接。

□add方法用于往有序集合中添加代理并设置分数,分数默认取pR0Xγ5〔0R[ I‖I丁的值’也就是 l0’返回值是添加的结果。

□ra∩dO‖方法用于随机获取代理。首先获取所有分数为l00的代理’然后从中随机选择-个返

回°如果不存在l00分的代理’则按照排名,获取排在前100位的代理,然后从中随机选择— 个返回,否则抛出异常° □deCIea5e方法用于在代理检测无效时’将其分数减l° .

□eX15t5方法用于判断代理是否存在于集合中°

□"ax方法用于将代理的分数设置为pR0xγ5〔OR[‖M,即l00,在代理检测有效时用到。 □〔Ou∩t方法用于返回当前集合的元素个数。 □a11方法用于返回所有代理组成的列表’供检测使用°

定义好这些方法后,就可以在后续的模块中调用Redi5〔11e∩t类来连接和操作数据库。如果要获 取随机可用的代理’只需要调用m∩do们方法即可’得到的就是随机且可用的代理° ●获取模块

获取模块主要负责从各大网站爬取代理并将代理保存到存储模块’代码实现见h仗ps;〃gjthuhcom/ Python3WebSpideI√ProxyPoo‖tree/maste∏proxypoo‖crawle蹈。

这个模块的代码逻辑相对简单’例如可以定义_些爬取代理的方法,示例如下: +ro们proxypoo1。cmN1er5。b35empoIt8a5e〔ra"1er +roⅦproxγpooL5〔he阳5.proxymportpIo×y mPortIe

灿Xp∧C[ =5

B∧5[0R[

= 0http://硼w.ip3〕66.∩et/+ree/?stγpe=18p日ge={p己8e}|

〔1己55Ip3366〔mw1er(8a5e〔ra"1er): □0Ⅶ00

jp3366爬虫’∩ttp://w№‖。ip3366,∩et/ 00 00T0

ur15= [8∧5[0R[.千or"at(page≡1)十or11∩m∩ge(1’ 8)]

‖』□‖‖|』』』‖|■■‖‖|‖|‖刚■{‖‖‖』■』‖】·】‖■□】‖□‖』□|`‘□|』‖{|〗||』‖』】‖□■■曰‖划』Ⅵ|‖(‖□勺

第9章代理的使用

3“

92代理池的维护

345

de千par5e(5e1千’∩t们1):

ipˉaddre55=re°co{||pj1e(』〈tr〉\5*〈td〉(。*?)</td〉\5*〈td〉(。*?)〈/td〉‖) #\5*匹配空格’起剑换行作用

re-ipˉaddre55=1p~addre55.「i∩da11(htⅦ1) +oraddres5』 port1∩reˉ1p=addre55: proxy二proxy(ho5t=日ddre55°5trjp()’ port=j∩t(port.5trjp())) yje1dpro×y

这里定义了-个代理类Ip3366〔mw1er’用来爬取IP3366网站的公开代理,通过par5e方法解析 页面的源代码’然后构造_个个proxy对象并返回° 我们在其父类8a5e〔raW1er里定义了通用的页面爬取方法千et〔h,代码实现如下: ·

「ro爪retryi∩8mportretry mPortreque5t5 十ro"1oguruj呻ort1ogger

c1a558a5e〔ra"1er(obje〔t): ur1S≡ []

0retrγ(5top-们ax-atteⅦpt-∩mber=〕’ retry=o∩-re5u1t=1己∏bdax: x15‖o∩e) de千+et〔h(5e1十’ ur1’ **代war85〉: try:

re5PO∩5e≡reql』e5t5.get(Ur1’**促Warg5) j千re5po∩5e·5tatu5code==2O0: Ietur∩re5po∩5e.teXt exceptreq0e5t5。〔o∩∩ectio∩〔rror8 retuI∩

01O8ger·Cat〔b de「〔m训1(5e1十): +or l』I1i∏5e1+.uI15:

1ogger.j∩+o(+’「et〔∩j∩g{ur1}!) hm1≡5e1千.十et〔们(ur1)

+orproxy1∩5e1「.par5e(bt爪1):

1ogger。j∩千o(+‖千et〔hedpro×γ{proxy.stri∩g()}千ro∏l {ur1}0) γje1dprO×γ

如果要扩展一个代理类〔mw1er,只需要继承8a5e〔raw1er并实现par5e方法即可’扩展性较好° +etCh方法可以读取〔mW1er里定义的全局变量Ur15并对其中的页面进行爬取’ 〔mW1er再调用Par5e 方法解析页面即可°

这样,就可以让一个个〔raW1er从各个不同的代理网站爬取代理’最后统一将所有〔raW1eI汇总 起来’遍历调用即可。如何汇总呢?这里是通过检测代码,只要检测到8a5e〔mW1er的子类,就将其 算作一个有效的〔mw1er’可以直接遍历Python文件包’代码实现如下: j们portp悯gl』ti1 十r咖。ba5e1Ⅶport8a5e〔r日刊1er mporti∩5pe〔t C1a5Se5= []

+or1oader’∩a爬’ i5-p代gi∩p伐guti1.川日1促=pa〔炮ge5(~pat∩-): mdu1e=1oader.十i∏dmdu1e〈∩日爬)。1oad晒du1e(∩a爬) 千or∩a爬’γa1uej∩j∩5pe〔t。geme爪ber5(∩odu1e)§ g1ob315()[∩3爬] =v己1ue

j于j∩5peCt。i5〔1a5S(γa1Ue)己∩djSSub〔1a55(γa10e」 8a5e〔ra"1eI)己∩dγa1‖ej5∩Ot8aSe〔ra"1er: c1a5se5.己pPe∩d(γ己1ue)

a11



∧l[

=〔1a55e5

这里我们调用了"a1kˉpac阳ge5方法’遍历了整个crawlers模块下的类’并判断每个类是否为 8a5e〔r日W1er的子类’如果是就将其添加到C1日55e5中并返回。最后只要将遍历C1a55e5里面的类并依 次实例化,调用各自的〔ra"1方法即可完成代理的爬取和提取’代码实现见h仗ps://githuhcom/

■■■■■■■■

9 ‖■■■■■■■

0

q

= ■ ■ ∏ | 』 ■

第9章代理的使用

346

Python3WebSpider/P【oxyPool/b‖ob/master/proxypool/processors/getterpy。

我们已经成功获取了各个网站的代理,现在需要_个检测模块对所有代理进行多轮检测。如果检 测代理可用’就把其分数置为l00,检测不可用,就把分数减l’这样可以实时改变每个代理的可用 由于代理非常多,为了提高检测效率,这里使用异步请求库aiohttp来检测。

出来,那我们就需要先等待十几秒的时间,这期间程序不会继续往下执行,但完全可以去做其他的事 情,例如调度其他请求或者解析网页等°

如果服务器响应得比较快,那么使用requests和aloh仗p的效果差距就没那么大°可检测-个代理 一般需要十多秒甚至几十秒的时间`这时候使用alohttp库的优势就大大体现出来了,效率可能会提高

』■‖‖■■』■■|■□■Ⅶ‖‖|」·■■■

requestS是-个同步请求库,在使用requests发出一个请求后’程序需要等待网页加载完才能继续 执行。也就是网页加载的过程会导致我们的程序阻塞’如果服务器响应得非常慢’例如十几秒才加载

‖‖●∏‖√

情况°要获取有效代理时,从分数高的代理中选择即可°

=■|‖|』■|』■〗‖|』■■

●检测模块

q



几十倍不止°

■■』』勺·‖』勺■■

检测模块的实现示例如下:



■■‖■|』司

j们port日5y∩cjo 1刚pOrt3jo‖ttp 十ro‖1oguru1呻ort1ogger fro∏proxypoo1·5〔he阳5mportpro×y +roⅧproxypoo1.5torage5°redismport只edis〔1ie∩t +ro"pro×ypoo1.5etti∩gi∏‖port「[5『∏旺叫「’『[5丁8∧「〔‖’ ∏[5「0R[’『[5『γ∧[ID5「A丁0S

十ro们己1ohttpmpOrt〔1je∩tprOxy〔o∩∩e〔tjo∩[rror’5eIγer015co∩∩ected[rror’〔1ie∩t05[rror’〔1je∩t‖ttPproxγ[rroI 「ro∏‖a5y∩〔jo加port『1爬out[rror [X〔[p丁I删5= ( 〔1ie∩tpIo×y〔o∩∩e〔t1o∩[rror’ 〔o∩∩ectjo∩Re+u5ed[rror’ 丁meOut[IrOI’ 5erγer015〔o∩∩e〔ted[∏or’ 〔1je∩t05[rror』

q

』‖‖

〔1ie∩tNttppIoxy[rroI )

〔1as5丁e5ter(obje〔t): de「

q

□勺‖』

1∩jt (Se1于): 5eM.Iedi5=【edi5〔1ie∩t()

5e1「.1oop=a5γ∩ciαget-eγe∩tˉ1oop()

a5y∩〔de十te5t(5e1f’ proxy: Proxy): 己5y∏c切ithaiohttp。〔1je∩t5es51o∩(co∩∩e〔toI=ajobttp。『〔p〔o∩∩e〔tor(s51≡「315e))as5es5jo∏:

Q

tIγ:

e1Se:

{{

5e1十·Iedi5。‖Ex(proxy) 1o88eⅢ.debug(十0proxy{pIoxy。5tIi∩g()}j5va1jd’ 5et |"ax5〔ore0)

厂△■巳

i十re5po∩5e.5tatu5i∩『[5『ⅦlID5「A『U5:

■■■‖■■

1ogger.deb(』g(+|te5ti∩g{pIoxy。5tri∩g()}』〉 己5y∩〔切jth5e53jo∩.get(『[5「0Rl’ pIo×γ≡十!http://{proxy.5tIj∩g()}|’ tj爬out=『[5丁『I月[α∏’ a11№redjIe〔t5=『a1se)a5re5po∩5e:

‖勺〈

5e1十。Ied1s.de〔re己5e(proxy) 1oggeI。debl』g(+|pIoxy{proxy.strj∩g〈)}i5i∩γ己1id’ de〔rease5〔ore,) ex〔ept[X〔[p「I删5吕

01oggeⅢ.c己t〔h de+ru∩(5e1十):

{|

5e1f.redjs.de〔rease(proxy) 1ogger。debl』g(「°proxy{proxy.strj∩g()}j5j∩va1jd’ de〔rea5e5〔ore0)





■■■■■■■■■■■■■■□■■■口■。||■■‖心「|■「|||匹尸『↓【■厂「

9.2代理池的维护

347

1Ogger。1∩+O(|5tatj∩gte5ter…0) 〔ou∩t=5e1千.Iedjs.〔ou∩t()

1ogger.deb0g(千‖{〔o仙∩t}proxje5totest‖) 「orj1∩ra∩8e(0’〔ou∩t’『[5丁8∧「〔什): 5tart』 e∩d=j’们j∩(i+丁〔5「8A「〔‖’〔o0∩t) 1oggeLdeb‖g(十,te5tj∩gpIoxje5十ro∩↑{5tart}to{e「‖d} j∩dj〔e5,) prox1e5≡se1+.Ied15.bat〔h(5tart’ e∩d)



ta5代5≡ [5e1+.te5t(pIoxy)「orpro)《y1∩proxie5] 5e1于.1OOp.n』∩u∩tj1—〔OⅦp1ete(a5y∩〔1O.Wajt(ta5k5))

| △■■「『「■■■■「

j+

∩a∏e

== ,

们己i∩

‖:

te5teI=丁e5ter() te5teI.ru∩()

■■■‖[β■β「

这里定义了一个类「e5ter。首先在其构造方法中建立了一个Red15〔11e∩t对象’供类中的其他方

法使用。然后定义了—个te5t方法,用来检测单个代理的可用情况,参数就是被检测的代理。注意’

卜 》 》

te5t方法前面加了a5y∩〔关键词’代表这个方法是异步的°te5t方法的内部首先创建了aiohttp的 〔1je∩t5e551o∩对象’可以直接调用该对象的get方法来访问页面°

卜『‖■尸■尸

测试链接在这里被定义为常量丁[5『0R[,建议将其值设置为目标网站的地址’因为在爬取过程中’ 可能代理本身是可用的’而该代理的IP已经被目标网站封禁了°例如’某些代理可以正常访问百度等 页面’但知乎己经把它们封了,所以如果对知乎的某个页面有爬取需求,可以直接将『[5『0R[的值设

巴尸|‖|伊|巴尸)■■「『■『●

置为知乎这个页面的链接’当请求失败,代理被封后,代理的分数自然会减下来’等到失效时就不会 被获取了°

如果实现的是一个通用的代理池,则不需要专门设置丁[5丁0R[的取值’既可以将其设置为—个不 会封IP的网站’也可以设置为百度这类响应稳定的网站。

■「卜‖▲■「【【【■■尸

我们还定义了『[5『γ∧[ID5「∧Ⅶ5变量,这个变量的类型是列表’由正常的状态码构成,例如[2OO]° 当然’某些目标网站还可能会出现其他状态码’可以自行配置。程序在获取响应信息后需要判断其状 态,如果状态码在丁[5丁γ∧[ID5『A丁05列表里,就代表代理可用’需要调用Red15〔1je∩t对象的们ax方

巴■Ⅲ■「‖■β

法将该代理的分数置为l00’否则调用de〔rea5e方法将代理分数减l,如果出现异常,也同样将代理 分数减l。

另外’我们设置了批量测试的最大值「[5丁8∧「〔‖’意思是—批最多测试丁[5「B∧「〔‖个,这样可以

) ||巴尸「『■■尸皿『■「‖||■■【『{●■

避免在代理池过大时,一次性测试全部代理导致内存开销过大的问题°当然’也可以用信号量机制实 现并发控制。

te5t方法之后’定义了ru∩方法用于获取所有的代理列表,然后使用aiohttp分配任务,启动运 行°在不断运行的过程中’代理池中无效代理的分数会—直减l ’直至代理被删除’有效的代理则— 直保持l00分,供随时取用。

至此,测试模块的逻辑就完成了° ●接口模块 ■【卜巴■「}[■■|‖‖β‖尸||‖[厂■■「「卜广「。「[■厂|炉「卜』◆『‖尸『‖‖■「卜|

通过前面3个模块’我们已经可以实现代理的获取、检测和更新,Redis数据库会以有序集合的 形式存储各个代理及代理对应的分数’分数l00代表可用,分数越小代表越不可用° 但是我们怎样方便地获取可用代理呢?可以用Redj5〔11e∩t类直接连接Redls’然后调用ra∩doⅧ 方法。这样做没问题,效率很高’但也会有几个弊端°

□使用这个代理池需要知道Redis连接的用户名和密码信息,如果其他人使用’会很不安全° □如果代理池需要部署在远程服务器上运行’而远程服务器的Redjs只允许本地连接’那么就不 能通过远程直连Redis来获取代理。





使用Red15〔11e∩t来获取代理。

我们使用一个比较轻量级的库Flask来实现这个接口模块,实现示例如下: 千rO∏千1己5代mPOrt「1a5促’ g

十ro们pro×ypoo1.storage5.redj5mportRed15〔1ie∩t +IoⅧpro×ypoo1°5etti∩gmport∧pI }|O5丁』 ∧pIpOR丁’ ApI『‖R[∧D[D a11

≡ [‖app‖]

日pP≡「1a5k( ∩aⅧe )

de十8etˉ〔o∩∩(): j十∩otha5attr(g’‖red15,): g.Ied15=Redj5〔11e∩t() retur∩8.redi5 0app.rOute(!/』)



已■】‖‖|日】||』□口日||』■■】』■■■』■□■■∏‖··`{』·』■∩‖』』门』】■

□如果Redj5〔11e∩t或者数据库结构有更新’那么爬虫端必须同步这些更新,这样非常麻烦° 综上考虑’为了使代理池可以作为一个独立服务运行’我们最好增加一个接口模块,并以webAPI 的形式暴露可用代理°这样_来,获取代理只需要请求接口即可’以上的几个弊端也可以避免°

乙 ■ 司 | · ■ ■ ■ ] 』 ‖ 』 ■

□如果爬虫所在的主机没有连接Redjs模块,或者爬虫不是由Python语言编写的’我们就无法

|」■■〗‖】‖」‖乙■■〗‖

第9章代理的使用

348

de十1∩dex():

0日pp.rO‖te(』/r3∩dO∏‖〉 de十get-proxγ(): 〔O∩∩=getˉCO∩∏() retur∩〔o∏∩.ra∩do刚()stri∩g()

□‖|□□‖』』】」■■□』■

retur∩ !〈∩2〉‖e1co们etoproxypoo15γ5teⅧ</∩2〉|

0己pp.rOute(‖/〔Ou∩t』) de千get-〔ou∩t(): co∩∩=get一co∩∩() retur∩5tr(〔o∩∩。cou∩t()) j十

∩己Ⅷe

==

‖31∩

·

己pp.ru∩〈∩o5t=∧pI朋S丁’ port=∧pIp0盯’ threaded=∧pI阳R[∧D[0)

这里我们声明了_个「1a5促对象,以及定义了3个接口,分别用于获取首页、随机代理页和数量 页°运行代码后’ F|ask会启动_个Web服务’我们只需要访问对应的接口即可获取可用代理° ●调度模块

调度模块用于调用上面定义的4个模块,通过多进程的方式把它们运行起来,示例如下:

+rOⅦProxγPOo1。Pro〔e55or5。gettermPortCetter 十ro们Pro×ypOo1。Pro〔e55or5。te5termPort丁ester 十ro"proxypoo1·5ettj∩gi『∏port〔γα[C[∏[日’〔γα[丫[5『[R’∧pI‖5『’ ApI丁‖R[AD[0』 ∧pIp0R丁》[趴8[[5〔Rγ[【’

· ( ‖

mportt1l∏e 加pOrt刚1t1prOCe55j∩8 十r咖proxypoo1。proce55or5·5eIγer1们poIt己pp

〔肌8[[C[∏[日’[‖AB[【『[5『[R’ I5‖I‖刚5

千ro们1oguruj∏port1o8ger i十I5‖I‖刚5:

‖u1tiproce551∩g.「reeze-5upport()

C1a555Chedl』1er():

de十ru∩teSteI(5e1千’〔y〔1e≡〔γα[丫[5『[R):



■■‖」

te5ter=pIo〔e55’ getter-pro〔e55’seIγer一pro〔e55=‖o∩e’ ‖o∩e’ ‖o∩e

1+∩ot[‖∧B[[『[S丁[R: retur∩



||

1ogger°i"十o(‖te5ter∩ote∩ab1ed′ e叉1t0)

「 『 匹 ■ 『 ■ 『 | 》 | ■ 尸

92代理池的维护



349

| ■ ■ 「 ‖

te5ter=丁e5ter() 1oop=0

任 卜 「 ■

Nhj1e『Iue:

「 ‖

1ogger.debug(千0te5teI1oop{1oop}5t日rt…|)

■ 厂

te5ter.ru∩() 1OOp+=1 tj爬.S1eep(CyC1e)

位 厂 | ◆ β | ◆ 『

de千ru∩ˉgetter(5e1十’ cy〔1e=〔γ〔[[C[『丁[R):

‖ ‖

j+∩ot[趴8t[C[丁丁[【8

■ ■

1ogger.i∩+o(|8etter∩ote∩ab1ed’ exit‖)

「 ■

retur∏

尸 ‖

getter≡Cetter() 1oop=0

「 ■ 》 )

Whj1e丁n』e§

▲ ■

1ogger.debug(千0getter1oop{1oop}5t日It…|) getter.ru∩()

■ [ 厂 |

1oop+=1

[ 卜

tme.51eep(CyC1e)

■ 厂 『 ■

de+rl」∩5eIγer(5e1千):

厅 卜 》

j十∩Ot[肌81[5[Rγ[R:



1o肥er·1∩十o(|5erver∩ote∩ab1ed’ exit』)

▲ ■ ∏

retur∩



app.ru∩(ho5t二∧pI刚5丁’ port=ApIp0盯’t∩readed■ApI丁‖R[∧D[O)

卜 ■ 「 份 血

de十ru∩(5e1+):

β 「

81oba1te5teI-proce5sD 8etter-pro〔e55’ 5erγer-pro〔e55

| ‖

trγ:

■ 『 β

1ogger.i∩十o(|5t己rti∩gproxypoo1…|)



1十[‖AB[[『[5丁[R:

「 『

te5ter~pmce55=Ⅷ』1tipIo〔e55i∩g.pro〔e55(t日rget≡se1千.n」∩te5ter) 1ogger.j∩+o(十$5t己rtm8te5ter’ pid{te5ter-proce55。pjd}…0)

△ ■ ■ 卜

te5teI-proce55.start()

■ 尸 ′ ∏

i+[‖∧8[[C[∏[R:

■ |

getter=proCeS5=刚1tjproCe55j∩8·pIOCeS5(taIget雹5e1+.ru∩-getter) 1ogger.j∩+o(十|5t己rti∩88etter’ pid{getter-proces5·pid}…‖〉

‖ 尸 ■ β

getter_pro〔e55.5ta∏()

| 巳 ■ ■ 厂

i十[肌8t[5[Rγ[R;



5erγerˉproce5s=Ⅷ』1tiproCe5sj∩g.proce5s(taⅢget=se1「.ru∩5erγer) 1o8ger.j∩+o(+『5tartj∩85erγer’ pid{5eⅣerˉproce55。p1d}…‖)

■ 「 ) ‖ ▲

5erγerˉproce55°5tart()

■ 厂 凸 尸

te5ter-pro〔e55.joi∩() 8etterˉproces5·joi∩() 5erγerˉprocess.joi∩()

『 | 卜 | ■ 尸

except旧eyboardI∩tern』pt:

「 ■

1o8ger.i∩+o(,re〔eiγed戊eγboardi∏terrupt518∩己1,)

■ 『

te5terˉprO〔eS5镭temi∩ate() getter-proce55.temi∩ate() 5erγer=prOCe5S.temi∩己te()

| 〖 〖 ■ 【 『

十j∩a11y8

‖ ‖

te5ter-proce55.joi∩()

【 ■

getterˉpro〔e5s.joi∩() 5erγer≡proce55。joi∩()



1o88e【.1∩十o(千』teste【js{厕a1jγe0′ ifte5terˉpIoce5s.j5a1jve() e15e !`dead"}`) 1ogger.j∩千o(+`8etterj5{徽a1iγe|, 1十getter-pIo〔e5s·15a1iγe()e1se 『|de己d"}』) 1og8er.i∩千o(千!5erγer1s{闻a1iγe′′ i十5erγer-pro〔e55.15a1iγe() e15e ||dead"}`) 1o8ger.i∩+o(|pIoxytemi∩ated0)

i千

∩己爬≡′ ∏]a1∩

:

5〔∩edu1er=5〔hedu1er() 5C∩edu1er.n』∩()



∏‖‖■■】■‖□口■■尸|匹■■■■■■〔

350

第9章代理的使用

这个模块的启动人口是Iu∩方法’这个方法会判断模块的开关是否开启’如果开启’就新建一个 Process进程设置好启动目标’然后调用5tart方法运行该进程’对3个模块都如此操作’之后3个进 3个调度方法的结构也非常清晰。例如’ru∩te5ter方法用于调度检测模块,方法中首先声明_ 个『e5ter对象,然后进人死循环,不断地调用其ru∩方法’执行完-轮后就休眠_段时间’休眠结束 后再重新执行°这甲把休眠时间定义为一个常量’如20秒’即每隔20秒进行一次代理检测。

以上内容便是整个代理池的架构和各个模块对应的实现逻辑°

■司‖■■乙■可

5.运行 现在,我们将代码整合在一起’并运行’运行之后的输出结果如下: | proxypoo1.5torage5.Iedj5:de〔rea5e:73 ˉ 6o.186.1q6。193:9oo0〔0rre∩t |proxypoo1.pro〔e55or5.te5ter:te5t;52 ˉ proxy6O.186.146.193:9O0O15

‖(

5〔ore10.o’de〔rea5e1

2O2Oˉ04ˉ1302:52:O6.S17 | 0〔80C

d



最后’只需要调用5Chedu1er类的ru门方法即可启动整个代理池°

2o2oˉo4ˉ1302:52;o6.510 | I‖「0

0

‖(

程并行执行’互不干扰°

||

泣里首先定义了3个常量[‖∧8[[「[5丁[R、[‖∧8[[C[丁「[R和[‖∧B[[—5[Rγ[R’都是布尔类型,分别 表示测试模块`获取模块和接口模块的开关,如果都取丁rue,代表3个模块都开启了。

1∩γ己1id’ de〔Iea5e5core

2O旦0ˉ04ˉ1〕O2:52:O6.524 | I‖「0

| pIoxypoo1.5torage5.redj5:decrea5e:73 ˉ 6O.186.151.147;9OOO〔uIre∩t

5〔oIe1o·0’ de〔rea5e1

2O2Oˉ04ˉ13O2:52:O6.532 | 0[80C

| proxypoo1。pro〔e55or5.te5ter:te5t:52 ˉproxy60。186.151.1q7:90OO15

i∩γa1id’ de〔rea5e5〔ore

2O2oˉ0↓ˉ13o2:52:o7。159 | I‖「O

| proxypoo1.5torage5.redj5:们ax;96 ˉ 6o.191.11.246:3128i5va1jd′

2020ˉ04ˉ13O2:52:07。167 | 0[80C

| pIoxγpoo1.pro〔e55oIs。te5ter:test:46ˉ proxy60·191。11·246:3128j5

γa1id’ SetⅦaX5〔Ore

2O20ˉO4ˉ13O2:52:17。271 | I‖「0

| proxypoo1.5tomge5.redi5:de〔rea5e:73 ˉ 59.62。7.13O:9oo0〔urre∩t

s〔ore1O.0’de〔re日5e1

202OˉOqˉ1302:52:17。28O | D[8[」C 1∩γa1jd’ de〔re35eS〔oIe

2O2OˉO4ˉ1302:52817.288 | I‖「0

| proxypoo1。pro〔e55oI5.te5ter:te5t:52 ˉ pIoxy59.62。7.130:9OOO15 | proxypooL5torage5·redj5:de〔rea5e:7〕ˉ60.167.1O3。7』:113〕〔urIe∩t

5〔ore1O0’de〔rease1

2020ˉ0』ˉ13O2:52:17.295 | D[80C

| pIoxypoo1.proce55or5.te5ter:te5t:52 ˉ proxy6O.167.103.7』:113〕 j5

1∩va1id’ decrea5e5core

2020ˉ04ˉ13O2:52:17.302 | I‖「0

■■■‖{二■■‖‖乙■∏‖‖』■■■可‖』■=■‖‖|□■】可』■■‖|

SettO1OO

| proxypoo1.storage5.redj5:de〔rea5e:73 ˉ 6O·162.71·11〕:9OO0〔urre∩t

5〔Ore1O.O’de〔rea5e1



2020ˉ0qˉ13O2:52:17.309 | 0[B(」C j∩γa1id’ decIea5e5〔ore

| proxypoo1.pro〔e55or5。te5ter:te5t;52 ˉ proxy6O°162.71.113:9OOO15

以上是代理池的控制台输出,可以看到可用代理的分数被设置为l00’不可用代理分数被减l°

现在打开测览器,当前配置运行在5555端口’所以打开http://l27.O0.l:5555即可看到代理池系 统的首页,如图9ˉ2所示。

令 ·…



凶=

-—_…

魁琶一一



× ‘习





—=▲=一▲■

=~-_

We|c◎met◎尸『oxγ尸◎◎|Sγste阳

{22.72°3272:8O

图9ˉ3获取随机的可用代理

‖‖』γ{|‖■‖

图9ˉ2代理池系统的首页



‖‖|』■|」』』】|《】‖

…凸…

巳?”D。o↑旧656

■■∏|■■■

再打开http://l2700.l:5555/random,即可获取随机的可用代理’非常方便’这里获取_个如图9ˉ3 所示°



·■=『Ⅱ【||■■|『‖儿■伊Ⅱ||{}■∩『|坠■■「卜}■|■■■■‖β|■■「『■■■■■■■■■厂「|『■■■■■



93付费代理的使用

35l

获取代理的代码如下: 加portIeque5t5

pR0XγP卯[0Rl≡ ’∩ttp://1oca1ho5t8S555/r己∩do叮

de十getˉpIoxy〈): trγ; ′

re5po∩se= Ifeque5ts。get(pR0Xγp"L0R[) i十re5po∩5e。5tatu5〔ode==200: retur∩re5po∩5e·text ex〔ept〔o∩∩ectjo∩〔IIoI; retur∩‖O∩e

运行这段代码便可以获取一个随机口I用代理了’代理是字符串类型的数据°可以按照9.l节的方

■■尸|『匹■几尸|卜■厂卜『八■尸匹■厂|■■巳■「∩伊「}『■■■『■■尸■『「『▲■‖|旧■「‖「■尸庄■厂·■〗「‖▲β「|[■■尸}■『■尸【●「||■「

法设置此代理,例如为requests设置代理: 1川portreque5t5

proxy=get-pIoxy() proxies={ ‖‖ttp|; 0http://‖ +proxy’ ‖https0 : !们ttp5;//0 +proxy’





trγ:

Ie5po∩5e=Ieque5t5。get(0‖ttp://www.httpb1∩.oIg/8et0 ′ prox1e5=pIoxie5) prj∩t(re5pO∩Se.teXt) exceptrequests.ex〔eptjo∩5。〔o∩∩e〔t1o∩[rroIa5e:

pri∩t([IrOr 」 e.arg5)

有了代理池,即可从中取出代理使用’有效防止我们的[P被封° 6.总结

本节中我们学习了代理池的设计思路和实现方案,有了这个代理池’我们就可以实时获取_些可 用的代理了。相对之前的实战案例,整个代理池的代码量多了很多’逻辑复杂度也比较高’建议好好 理解和消化—下·

本节的代码见https://gjthuhcom/Python3WebSpjder/ProxyPool,代码库中还提供了基于Docker和 Kubemetes的运行和部署操作,可以帮助我们更快捷地运行代理池。

9.3付费代理的使用 前面两节我们讲解了代理的基本使用方法和免费代理池的搭建过程’但使用过程中其实还会存在 代理不稳定的情况’例如代理的失效速度快、运行速度慢°毕竟这些代理都是可以公开获取的,可能 有很多人在用’稳定性差也不足为奇了。

·尸『′厂′‖止■尸|卜’「|·尸|}■【「|「■【■〗【■‖『〔■■【『‖●■■『[「巴『

相对免费代理’付费代理的稳定性更高’所以如果想进_步提高代理的稳定性’可以考虑使用付 费代理。

↑.付费代理的分类

按照使用流程,可以大致将付费代理分为两类°

□一类是代理商提供代理提取接口的付费代理’我们可以通过接口获取这类代理组成的列表,这

类代理地址的IP和端口都是可见的,想用哪个就用哪个,灵活操控即可。这种代理_般会按 时间或者按量收费’比较有代表性的这类代理有快代理(htms://wwwkualdalli.com/)`芝麻代

理(h忱p://wwwzhimaruanjjancom/)和多贝云代理(http://www.dobelcn/)等°



‖|



第9章代理的使用

352



开放代理和独享代理’私密代理相对开放代理来说稳定性更高,相对独享代理来说价格更实惠,总体

性价比更高。

私密代理的介绍链接为https僵//wwwkuaidailicom/doc/producUdps/’官方简介内容是:私密代理是 我们自运营的高品质HTTP/SOCKS代理服务器, IP动态变化,仅对购买客户授权使用。每天可用IP超

l5万个’支持API接口和SDK。

||■■、‖|』□』■|||」旦■(γ』■||』■■‖|』■』□引

这里我以快代理为例演示通过接口获取代理并使用的方法’需要先到快代理的官方网站注册—个 账号并购买对应的套餐。由于我的目的仅仅是测试,因此我购买的是私密代理套餐’类似的套餐还有

月』|

2通过接口提取代理

■■‖】《‖

本节分别讲解这两类代理的使用方法°

■·‖』●』‖■■■

abuyuncom/)、快代理(h忱ps:〃wwwkuaidai|j.com/)和多贝云代理(http://wwwdobelcn/)等°



□另_类是代理商搭建了隧道代理的付费代理’我们可以直接把此类代理设置为固定的IP和端 口’无须进_步通过请求接口获取随机代理并设置°在这种情况下’我们只需要知道一个固定 的代理服务器地址即可’代理商会在背后进_步将我们发出的请求分发给不同的代理服务器并 做负载均衡,同时代理商会负责维护背后的整个代理池,因此开发者使用起来更加方便,但这 样就无法自由控制设置哪个代理IP了。比较有代表性的这类代理有阿布云代理(https://www.

私密代理提取接口的说明链接为https;//wwwkuaidallicom/doc/api/getdps/’接口地址为http://dps.

‖{

kdlapl.com/apⅡgetdps。

在调用私密代理提取接口时需要传人一些参数’参数的官方说明如表9ˉl所示。

51g∩-type

是否必填

是否否



参 orderid

S1g∩ature

参数说明

取值说明

订单号

有效的私密代理订单号

签名验证方式。目前支持51"p1e和∩川已〔5ba1

默认值: Si∩p1e

请求签名,用来验证此次请求的合法性°私

支持2种签名验证方式

密代理接口默认不需要验证签名,但在会员



是否

∩uⅧ

马前的UNIX时间戳(秒级),可记录发起

例如15575耳601O,如果其取值与当前时间相 差过大,会弓|起签名过期错误

此参数为必填项

提取数量

例如1O0

多个地区用英文逗号分隔,例如北京,上海

支持按量付费的订单 areaeX

口[

Ⅲ 己

°[

·]







排除某些地区的IP,支持按省/市排除,仅 支持按量付费的订单

多个地区用英文逗号分隔,例如北京’上海



筛选以特定数字开头的lP(多个lP段用英

例如120·52







‖| U



排除以特定数字开头的IP(多个IP段用英

例如12O.52.

文逗号分隔) pt





文逗号分隔) ip5tarteX





APl请求的时间51g∩-type取‖阳a〔5ha1时

筛选某些地区的IP’支持按省/市筛选,仅

己rea

‖‖

q

中心开启验证后,此参数为必填项

t加esta们p

‖二■□|‖|凸γ‖

表9ˉ‖ 调用私密代理提取接口时传入的参数



提取的代理IP的类型

1表示HTTP代理(默认), 2表示SOCKS 代理







=〕

+[





按稳定使用时长筛选【P,这个稳定使用时长

O:不筛选(默认)其他值意 自自定义时长

是从提取时算起,此参数只对‘‘均匀提取 ( l~50分钟) 30~60分钟版,’有效

q







353

(续) 参



参数说明

是否必填

否否否



9.3付费代理的使用

+1O〔

于〔itγ〔Ode +et

取值说明

提取结果包含地区信息

取值固定为1

提取结果包含地区编码

取值固定为1

提取结果包含此代理从提取时算起的可用

取值固定为1

时间(单位:秒) 否

dedup

取值固定为1

过滤今天提取过的IP’不带此参数代表不 过滤



千Or‖at

text表示文本格式(默认),j5o∩表示JSON

接口返回内容的格式

格式’×们1表示XML格式



5ep

1表示用“\r\∩,,分隔(默认)’ 2表示用

结果列表中,每个代理的分隔符

“\∩,,分隔’3表示用空格分隔’4表示用“|,, 】■【■■

分隔

可以看到’这里支持指定的内容还是比较多的’例如提取数量、地区`代理类型和过滤条件等°

接口可返回文本格式` JSON格式或XML格式的内容’对返回内容中字段的说明如表9ˉ2所示° 表9ˉ2返回内容中字段的说明 参







返回码°取值: O代表成功;非O代表失败

〔ode 们5g

错误信息

d己ta

包含接口返回的数据

data°pro×y-115t

返回的代理组成的列表

data°〔ou∩t

返回的代理数量

data°dedup-〔o0∩t

返回的不重复的代理数量(按量付费、包年包月集中提取订单专有)

dat己。order1e代〔ou∩t

订单提取余额(按量付费订单专有)

d己ta.today-1e十t=〔o(』∩t

今天提取余额(包年包月集中提取订单专有)



好,基本的请求参数和返回结果我们已经搞清楚了’下面实践看看。 产

购买相应的套餐之后’可以在订单页面找到对应的订单号’例如我的订单号是93726053059l661, 如图9ˉ4所示。 93》翻日…‖蹿↑

私Ⅸ代迁…分钟版 辙绎穗月

↑刚天

有傲

“天

忠中攫墩

m田卜‖0“

任眼掷取代豫发咸AP涵接″『仪叮

??鞭倪?:w

罐更歼组』p白餐蚁∏升组窖■订单测伯

图9ˉ4套餐的订单号

然后点击‘‘生成API链接”选项,即可跳转到快代理提供的提取页面,这里它已经准备好了操作 界面’通过点选就可以配置参数了。例如图9ˉ5,填好订单号’把提取数量设为l0’代理类型选h仗p/htms’ 其他选项均保持默认’最后点击底部的“生成链接”按钮°



■ ‖ 』 ■ ■ | 』 日 | · ‖ ■ 可 日 `|(

354

第9章代理的使用 生鲤砷z∧p蹿 ■…蛾8

一ˉ——_

跨黔



‖已硷…■■唾宙…■………芍

■印曰巴n…色哩=~m泽,…■…″■■α a西■”…,兹■…°m■…才醒白 ——_—



牢血勺

≈≈



■●■●



■■■■■■分、凸■℃…●

汀单Q伪 °眺7……90“↑ =

(门

0■■…创爪从■刃■m =■



V

→→≡■≈



塑U■■· 00

代n矣翻●…/…■回呵c…口■ ◎…′…迎…●代…口号

■·■■■■■|■■■■■日勺乙■司

矗磺烙武●文本◎…◎x励

笆泉分闭符●`m分■◎Ⅶ分们○室泪分●◎|分■◎■定义 「ˉ…ˉ{

L●■…=』

∧Pm■侍灿◇不刃■(吨0 ◎四疆0…)疤碑 ■歼更多m巾



‖日



图9ˉ5配置参数

■:靛霉2Ⅲ__—二≤—■ ÷今G‘已印$k…………时囊簿s护涵o0…?ss‖翻吼』獭司?锄″■]…铂7 ■和…一_~---一ˉ---→≈■…, 钨→■~■←



i

ˉ=-=—m啼-=≡巴←--一耳=琴→己==



■一≈≡争…争F■●ˉ←≈●=_■



■护

12』°61·』50□08〗6307 】17·6已。47°〗1】18801

1】4·99.109口2]7』2102S

114·220·5■·】1d■18025 59·62°■0·1971213γ6

q

叫‖|‖γ||』

之后会看到生成了一个API链接’这里我生成的API链接为ht印s:〃dpskdlapicom/api/genps/〉orderid= 93726053059l661&num=l0&p广l&sep=l,直接访问就可以看到代理列表了,一共是l0个,如图9ˉ6所示°

171△00·106◇】■00立2]14

1·0◎1G0◇47·1708】曰s81 皿巴·87.109o115819573



】〗0·106●136·6■【2120·

1□0△109■236.213Ⅱ19387





这些就是可用的代理了。为了防止代理被滥用,快代理设置了白名单机制: (1)需要设置IP白名单或用户名密码才能使用私密代理;

(2)Ip白名单和用户名密码最好二选-’如果是使用用户名密码访问的,请不要设置IP白名单°

可以根据其提示设置用户名密码访问或Ip白名单访问’这里我使用的是IP白名单,这个IP得是 我们对外开放的公网Ip’可以通过很多方式查询到,例如通过百度直接查询,如图9ˉ7所示。

■型噬四■



0° 铀员讯

喧…

`心■片

』珍知逆

□文灯

略砧吧

_

■H硒找呻员■碑↑唾…ˉ…个

丁韶●丁庄

P№汹击■

陋本机腰』↑2o2“"…北顶辆阳区穆渤 .:了=

{″入内垃

本…方注…凹■古住 ←-

蘑嚏≡下

泞采■岔坦ⅢB

■‖』‖|』■■∏|||』■司」·‖■■■■γ■■□■■

◎闰Ⅲ

』■■■‖‖‖|■■可‖二■■■□■|‖■■■■■∏■■■

图9ˉ6代理列表

凸=--——一=■■■

图9ˉ7通过百度查询IP \

‖‖

●■■「『·■■厂‖|‖■■「■■■厉■[■「‖[伊■『尸■■尸

| | 也●『[■■「■尸■■厉『|「[■位■「‖}■尸|

■厂■‖巴■『『}~■口二■「

卜| ■■厂【厂|■■厂||■「■■『}■「|【■厂■‖「 |‖||△■厂‖『△●【■『‖|′|=■■■|俱■尸’■·■’|‖′|■■■■■『′■■=「贮匹『‖|〖■■■「|||巴=尸『‖‖『『|[■尸口〖■



93付费代理的使用

355

此时我的IP地址为l2O2“.ll8.l34,把这个IP设置到快代理的后台’我就可以正常使用代理了° 当然’你需要找到你的IP’然后设置白名单°

以上流程完成后,我们用代码实现-下’测试图9ˉ6中的IP是否可用: mPortreque5ts

pR0Xγ∧pI= 0∩ttp;//dp5·促d1己pL〔o们/apj/getdps/?ordeIid=93726O53O591661&∩u∏=1O&pt=1&5eP=1! de十get-prox1e5(): re5po∩5e=reque5t5。get(pR0Xγ∧pI) retur∩re5po∩5e.te×t.Sp1jt(0\∩‖)

de十te5t-pIoxie5(): p【oxje5=get-proxie5() 于OrPrOXyj∩PIOxje5: proxy=proxy.strip(〉 pIj∩t(+|(」5i∩8proxy{proxy}|) try吕

re5po∩5e=reque5ts.get(|∩ttp://咖.httpbj∩·org/ip‖’ pIoxje5={ !http0 8 ,bttp://0 +Proxy’ }) pIj∩t(re5po∏Se.text) ex〔eptreque5t5.〔o∩∩ectjo∩[rror:

prj∩t(十,proxy{proxy} is i∏va1id,) j千

∩a爬

== 0

川a1∩

0 8

te5t』rOXje5()

这里首先声明了—个pR0Xγ∧pI,其值就是上文获取的用于提取代理的API链接,然后我们使用

get=proxje5方法请求了这个API,会返回由可用代理组成的列表’再利用5p1jt方法将列表里的代理 逐行分开°接着,我们在te5t-proxie5方法里面直接用requests的get方法请求了http:〃wwwhttpbm. org/ip’会返回发出请求的真实IP地址,代理我们是通过pIoxjes参数设置的°



注意,由于我们访问的是HTIP类型的页面,所以只需要把proxie5参数设置成以http为字典键 名的代理即可,无须再设置以们ttp5为字典键名的代理°另外,键值内容也是HTTP类型的代理’即 ‖ttp://加上代理。 最后直接打印出网站返回的结果.运行一下代码,输出结果类似如下这样: usi∩gproxy18o.113.8.2』1:21871 {

口oIi8j∏口8 口180·113°8·241闻 } u5i∩8proxy175.42.129.219:21〕5o {

曰omgi∩口: 口175·q2.129.219冈 } u5j∩gproxy182.〕8.205.29:1幽乃 {

口origj∏口: 口182.38°2oS.29口 } usmgpmxy49·68·111.S0:2鸥5 {

口orj8i∩口: 国49.68·111·5Oα }

可以看到,首先输出提取并使用的代理,然后输出请求http://Www.httpbjnoIg/ip的返回结果,会 发现代理和网站返回的真实IP完全—样°



这样就证明我们成功使用代理请求了网站并达到了伪装真实IP的目的° 3.使用隧道代理

上面我们介绍了利用接口提取代理的使用方法,可以发现这个过程其实相对烦琐’首先得请求接 口获取代理,然后选出想用的代理,再设置代理发出请求,那么有没有更方便的设置代理的方法呢? 有’这个方法在前面也提到过,就是隧道代理°

隧道代理相当于服务商在云端维护了-个代理池,客户端只需要设置-个固定的代理服务器’换 IP的流程由服务器来完成’让用户使用的流程更简单。用户无须更换IP’隧道代理会将请求转发给不 同的代理’可按需指定转发周期°

这里我们还是以快代理为例演示隧道代理的使用方法,首先需要购买隧道代理的套餐,链接为

https://wwwkuaidajli.com/doc/producUtps/,购买之后进人个人中心可以看到类似如图9ˉ8所示的页面。

|」■■■|』‖||■习‖{|■■】】‖』■||{■`」|■】』』】】●■〗』■■■』】‖■■■‖』、|‖‖』■〗‖』】■■】‖』■】■】□〗】】■

第9章代理的使用

356

会员中心

快T

…u

=凤

--

0■0G

…‖0

p叮■{∩■j

■助$α●邱

砷 一…凹=

‖{‖‖』■□■

口牵■【

铀↑鲍…`… ■`魄……酗

蛔=硒

…耻

陋~…

″呐

『v~……●次田求

m钉■

…愚…

■=

6户Ⅷ戊



戚毋ty■w佃 ●凸臼*吐…□

歼似抨宜●∩●■■■

Ⅲ何记矗

——=■

_

■■■户G″

″撇C

愿尸名W ……=●

‘;

胀凛攀/

…,

″脏■…

~—__

【■■『宦″穗瀑蠢沪●名轧…尼凤■$呐彝习“用蜘…隔h左$0蜘涵典芦出…颧ˉ钾户■■巴萨m由…

田取…` ≈

-≡—■

…企

■还代■

××

″…哼

■环m豺泽耗■●…押…■·碑……次…· ■■僧■貌甘●

巴…■凭■垂逊■■咏…■$盅c产1】6.■h掣.…作=■1g■1●…■巧‘也…■挛…了°

■君豺窗髓■≡蹿』

攫…无…鳃攫砷也迁田啊…m·

图9ˉ8隧道代理套餐

其中显示隧道host为tpsl36kdlapi.com’HTTP端口为l58l8, Socks端口为208l8’我们只需要 在请求目标网站的时候把代理设置为这个hoSt和这个端口的组合就好了°另外,使用隧道代理需要用 到用户名和密码’这两项也在图9ˉ8所示的页面里。

另外,这里同样需要设置白名单,按照前_节类似的流程设置即可。接下来我们根据后台提供的

一些信息测试_下隧道代理的设置,测试代码如下: 加portreque5t5

ur1≡ !http://Ⅳ洲.httpbi∩.oIg/jp| #代理估息

proxy-ho5t≡ |tp5136。kd1ap1·co‖’ proxy-port≡ ‖15818『 proxy-u5er∩a川e= ,t172605334226460 pro×y≡pa55word= |γ9〕〔q4tk!

pro×y=f‖http://{proxyˉu5er∩a们e}:{proxyˉpa55"ord}0{proxy—ho5t}:{proxyˉport}‖ Pro×1e5二 { 0http|: prOxy’ |http5! ; pro×y’

} re5po∩5e=request5.get(uI1’ proxje5=proxje5) pr1∩t(re5po∩S巳te其t)

这里首先声明了proxyˉho5t、 proxγ—port` proxyˉu5er∩a"e和proxγ-pa55"oId,分别是隧道代理 的host、端口`认证所需的用户名和密码。然后调用了requests的get方法’并传人prox1e5参数直

一■】■】||·〗●]|』□□·司‖‖】』■■■■■■〗】■‖』|□〗·■□‖|●Ⅵ■■‖{|·■』‖】』■■■】||』■‖|||□●■■」‖‖】■】‖‖』■■γ‖】〗■∏‖|‖‖■■可|||』■可」』·‖|』】■】』‖】■■』』■』■】Ⅵ

■…●

■「|「仍

| |‖■■『【『‖‖丛■■尸||■■「『「}■■「『■■『■「‖|·巴■尸|■■「‖‖

■■■■■尸|‖■『■尸|■■■尸卜∩匹尸「『【『仙β『卜|●‖■尸■■‖▲巳『『‖■厂

·■‖□厂Ⅲ

}■□■厂|‖■「′‖》■尸卜【尸|■■■■△尸『■■厂

「 |

‖‖■厂||

b

94ADSL拨号代理的搭建方法

357

接将代理设置为隧道代理的地址° 运行代码’看下效果: { "orjgi∩,0 : "117·92·21q°244" }

可以看到返回了客户端的IP,但校验之后发现这个IP并不是我们的真实IP,说明代理设置成 功了。

然后再同时运行几次,可以看到运行结果_直在变化’例如: {

"orig1∩": "1卫°246.92.129" } { "origj∩": "113.121.2O.52" }

这说明每次请求时的代理IP是随机变化的。 所以,我们现在只需要设置一个固定的隧道代理就可以实现在每次请求时自动切换IP了,使用 起来更加方便°

4.总结 本节讲了两种代理的设置方案’各有各的优势°

□通过接口提取代理:我们可以灵活地控制使用哪个代理,同时可以将代理对接到代理池中维护 起来’整体的使用灵活性更高°

□使用隧道代理:我们无须关心具体使用哪个代理,会更加省心° 两种方案各有利弊’可以根据具体的业务场景具体选择°

9.4∧DSL拨号代理的搭建方法 我们在9.2节尝试维护过-个代理池’从中可以挑选出许多可用的代理,但这些代理常常稳定性 不高`响应速度慢,而且大概率是公共代理’意味着同一时间可能有多个人使用’故被封的概率很大。 另外,这些代理的有效时间可能比较短,虽然代理池_直在筛选可用代理,但不免存在没有及时更新 状态的情况’这样有可能导致我们得到不可用的代理°

在93节’我们也了解了付费代理,其质量相对免费代理会好不少’的确算是—个相对不错的方 案,但本节要介绍的方案可以使我们既能不断更换代理’又可以保证代理的稳定性。

大家可能会在_些付费代理套餐中注意到这样_个套餐—_独享代理或私密代理,这种代理其实 是使用专用服务器搭建了代理服务’相对一般的付费代理来说’稳定性更好,速度也更快,同时IP可 以动态变化。这种代理的IP切换大多是基于ADSL拨号机制实现的,_台云主机每拨号一次就可以

换_个IP’同时云主机上搭建了代理服务’我们可以直接使用该云主机的HTTP代理来进行数据爬取° 本节就来讲解搭建_个ADSL拨号代理服务的方法° ↑.什么是∧oS[

ADSL的英文全称是AsymmemcDjgitalSubscnberLine’即非对称数字用户环路。它的上行带宽 和下行带宽不对称’采用频分复用技术把普通电话线分成了电话、上行和下行3个相对独立的信道, 从而避免了相互之间的干扰。



||



358

第9章代理的使用

ADSL通过拨号的方式上网,拨号时需要输人ADSL账号和密码’每拨号一次就更换一个IP°IP

分布在多个A段如果这些IP都能使用,意味着IP量级可达千万°如果我们将ADSL主机作为代理, 每隔_段时间云主机拨号换_个IP,就可以有效防止[P被封禁。另外,由于我们直接使用专有的云 主机搭建代理服务,所以代理的稳定性相对更好,响应速度也相对更快。 ■=

2.准备工作

ADSL代理云主机的服务商还是比较多的,个人推荐阿斯云和云立方,官网分别为ht‖ps:〃asiyun.cn/ 和https://www.yunljfangcn/°

-…_

-凸

由≡…

=-—二



开…期

芒■锚(■田可■咖沮)

壶■…酮

中卫电T韩四

=■兰■邑≡≡可

云■齿■名.7↑0〗…田 云主机…ˉ旧■■■■■■

……二……v钾埋… 网……0辑…] ……■…

…/饱■

审回●硒…‖m逛:…2

砸0如`t″m

醒0〃/‖28“‖8





●疆梗瓣

[铅锣][蹿庭尸●] }鞍…忿|

图9ˉ9查看云服务器的相关信息

司可(‖』□』■Ⅷ‖‖司‖‖■】」‖□∏·γ‖‖』■‖』■司|』』|■■□||■(」■‖‖‖■」□Ⅷ】〗■

如图9ˉ9所示。

』』■■

本节以阿斯云为例,我购买了一合电信型云服务器,同时安装了CcntOSLjnux系统的云主机。购 买成功后,可以在后台找到服务器的连接lP`端口、用户名`密码,以及拨号所用的用户名和密码’

」■■■可‖|||‖■■■■』〗】‖■』■■

均衡°

‖|||■■日|

在本节开始之前.需要先购买几台ADSL代理云主机,建议购买2台或以上°因为在云主机拨号 的一瞬间,服务器正在切换lP,所以拨号之后代理是不可用的状态’需要2台及以上云主机做负载

□ 司

使用的IP和端口是zhongwejdx0ljsq.bz:30042’用户名是root°在命令行下输人如下内容: 5shroot0xho∩醉jdx01.j5q。b∑ ˉp]卯啡2

输人连接密码,就可以连接到远程服务器了’如图9ˉl0所示。 ●0

…7?00…m2吕=



3酗乙 $B∩7O◎↑佰∑门m辆e1dx日1J∑QoZ←p5酗乙 h了0O◎↑隘∏m辆e1dx巳1

了们e◎D铅↑pP∏它1亡l[VG千h◎St ‘【Fho『呼e!α又B1]驹OZ〗ˉ…匀E(【〗锤451以19g]3…2) 〔◎ 0tOce已t◎o】1sheo

[(D5∧髓y千∩gG巾PM`t lb5汹【跪X毋了U0∩司厂w尸A5cV1"础8QⅡ[皿PWy【2J弘V似l…[XXb丁Ⅳ 八尸G丫O凹5凹吧γ◎{」伪oV〗tto色mtw悦』ec饰∩C〔七‖■g《yG凸『Yo/[「1胰ge广口尸∩t可)’yeS 和凑 ∩Q. peF吨∩e∩↑yo酮cd ’[Z门◎∩″ei咖钮』5q◎r7J…2,【1“牛51鹏,”.30O徘g l毛t◎fmO甜∩m巴t巴

『DCwⅢ尸o∩鳞心I◎x01 〕SQ恤·S四£5m厂d αb° 吗∩ 5扫∩」陷 Ⅲ 110133酗k〗↓「…1g22“2冒▲ 1 行鸡兰闺左氢酝永拨弓γp5 Z叁坯怎上淫俩瑰没冶赤魔雨淫

喂匙石襄莲栅沼功逃蔓

浸共密熄缴洒忍撅止曼绎遣魁幽丧

巳戚翻,髓父露蟹遵『;蛤琴c唇囊闺霓

受』号窥驭卓唁

图从拽蹬起讲产大臀户髓遍绝^直的抖记侵 忙

本字娥额膜〔e∩r◎5了6x6马 累粳!:欧认酝Ⅱ克导耀硬

园以p□pOe=5tα『.t巧如pOe-gtOp 鼠/分b∩J‘up”p0闸/sb1∩/〗「d酞γ飞9p酗 沼决号吸作 ■坷矗号鳞赛公厕Ip蔓婉

共军赞扫瞩民堂 塞星绘=碑壶灾蟹Sq"泌二方弓傀留2钥姆挽 『厂O口t泞7111Z□凭√巫2≈白 Oct哼7111Z臼5γ配2 ..』

图9ˉl0连接到远程服务器

■■|』·□∏|

3 尽

S

| ■ ■

([〔0弘)t◎仆0

』□■■『‖|‖·Ⅵ||』■司■■■|■■|||』■司‖

再找到远程管理面板→远程连接的用户名和密码’也就是SSH远程连接服务器的信息°例如我



94ADSL拨号代理的搭建方法 登录成功后,开始正式的学习° 3.测试拨号







云主机默认已经配置了拨号相关的信息’如宽带用户名和密码等,所以我们无须额外进行配置’ 只需要调用相应的拨号命令即可实现拨号和IP地址的切换° 可以输人如下拨号命令来拨号:





P



359

pppOe-Bt己rt

拨号命令成功运行’没有报错信息,耗时约几秒,结束之后整个主机就获得了-个有效的IP地

址°如果要停止拨号’可以输人如下命令:



pppOe≡5tOp



运行完该命令后,网络就会断开’之前的IP地址也会被释放°

所以,如果想切换IP,只需要先执行pppoeˉ5tOp,再执行pppOeˉ5tart即可°每次拨号前,可以 用j+〔o∩{ig命令查看主机的IP,如图9ˉ‖l所示。 .ˉ

■密赚

ˉ————‖■|■■}

—ˉ_——_—

『mt簿”002四y∑·m→

即p0:钝叼s玛驹5巡F.睡村『钾恤颇》瓤蜘Ⅱ獭’哪腮p,队凡杠α毋丁护喊u1钧z ppU: vt蛔巴劈施…席.…其Ⅳ『Umm?勘…1硒∏…Fg网JL0£巴风≥】P掘Ⅷu』吁逐匹 i‖0e七1“.45.』酪.测腮…冰25§·z55.2宪翁∑55

de£忱晒tiQ∩1皖.45.1帜.1

”p t又queue1呻3 (pOMt=tOˉpot∩tp7ot◎eOl》 赋四〔Ret5乙里坷七eS1帕“(姆·7低诅) Rx巳「7O广50 o「oppe00闸γ诌卜m∩S0什…0 丁X四〔Ret559 bytes2帕5〈2,4Rt8) 丫Xe尸7o7巴0 d厂吨pm0Ovew‖m田0 〔□沪河e厂0 cOnimo门s0 厂儿厂匹巳



「四te71112口巴y26Z~]j″poeˉ写t◎p;Pppoe→5t◎穴 Fmt邑711皿刨巴y26Z~]扩飞丁C匈↑1g

t们0 「!αg5玛163山p’8R0AD〔A5了J尉t」酗ING’灿l丁I〔∧S∏≥毗u15卿 1∩e七10.巫·“·羽∩色t…毗∑s5.25b°255.Ob广mdc仪st10.∑2.68255



i∏et6千e锄;:α16:81仟吕fe35:37知p厂e?t又Ie∏融S℃呻噎i◎0x20<1mk≥ et仍e尸08:16.81°35·37:鲍tXqueuele"1酗([饰e厂∏et) 肌p□〔低et品1眨351 Dyte邑1121∑327(10ˉ6川】8) RXe广厂◎尸S0o尸Opped125驹ovc下7u"儡0 「「α∏e0 了XpO〔ket531S25 byt“驹1酚10(19酞B) γXe伊厂◎庐5O咖oppe创0oγe广广u∩巳0 〔o广厂飞G「0 〔oll1鳞o∩S0 l o.∩αg三工73d」p,L"p弘〔促,R0村"I"6>锨u6甄36

1∩et 』27.0.0·1 ∩etm5戊25500·0

t∩et6 。 1

〗Qop

p厂e「iX〗e∏128

ScOpe100x19<№St

tXq仙Gue1e∩1酗(tO〔o] LOOpb◎〔k) f庐◎爬0

γ义p□〔kPtb0叮teb0(00B) 『义e广厂可FS0 □厂Opped0◎ve厂广lmS0 〔αF厂te76 〔ollˉS1OV?S6 p口p0·

儿卜

RXp◎〔■etS0 Dvte30(0.砂8) RXe厂7o∩∑0 α萨◎pped0 ◎γe庐7u∩S0

||

止■■厂|}【■「卜【■「|广■■『|囚■血■『|·「仿「▲■厂●=尸卜∩■[巴「|仕■【●■尸■尸|血_■‖巴■尸′■■■『■「}●[『△■厂|卜【■「■厂‖

注意不同云主机的拨号命今和停止命令可能不同’如云立方主机的拨号命今和停止命令为 ad51ˉ5tart和ad51ˉ5tOP’请以官方丈档的说明为准°

fIOqsq30b≤0p°p01"丁0pOI"『0Ru付MM°咽ARPp恤」t丁I〔A5『≥减uM92 】∩e〔1瞬宫4s·1腮1“∩et‖翘Sh乙53.【55·25日·255

图9ˉll

口e吐1们α七l◎∩1脆.牛s·1钢.】

拨号前后的IP变化

可以看到,执行了PPPOeˉ5tOP和pPPoeˉ5tart命令之后,通过i+cO∩+ig命令获取的网卡信息中

的IP地址就变了’代表我们成功实现了IP地址的切换°

那么要想将这台云主机设置为可以实时变化IP的代理服务器’主要得做这几件事情: □在云主机上运行代理服务软件,使之可以提供HTTP代理服务; □使云主机定时拨号’更换IP; □实时获取云主机的代理lP和端口信息。

审□ 四



第9章代理的使用

4设置代理服务器

当前云主机使用的是Lmux的CentOS系统’它是无法作为—个HTTP代理服务器使用的,因为 该云主机上目前并没有运行相关的代理软件。要想让它提供HTTP代理服务,需要安装并运行相关的 代理服务软件。

α 完β口

那什么软件能提供这种代理服务呢?目前业界比较流行的有Squid和TinyProxy’在云主机上配置 好它们后,它们会在特定端口上运行—个HTTP代理°知道了云主机当前的IP’我们就能使用Squid或 TjnyProxy提供的HTTP代理了。

|』■■勺□』』■■Ⅷ‖□】‖●■■‖‖‖』·』可■(||』■】■■∏‖■■■■□■】】■∏■■]〗』■∏‖

360



5udoyu川ˉyupd己te y0Ⅷˉy1∩5ta115q0id

运行完这两行命令后, Squid就安装成功了°

如果想启动Squid’可以运行如下命令: 5y5te们Ct15tart5quid

如果想配置开机自动启动,可以运行如下命令: 5y5te爪〔t1e∩日b1e5quid

成功启动Squid后,可以使用如下命令查看它当前的运行状态: 5y5teⅧ〔t15tatu55qujd

{」

结果如图9ˉl2所示’可以看到Squid已经成功运行了° ●



勺■■]|‖」‖·】』‖□■』■可||』Ⅲ■(』□】|‖』■』■】】‖勺』■可‖‖』■】■】‖‖』■|□{

这里以Squid为例演示_下配置过程°首先安装Squid’在CentOS系统上安装Squid的命令如下:

…●yT?↑Rmγ2sa·~



1=‖歇ˉDαe硒"Jm@了C们080.48=5.僧w pe厂k=刊喧r~u@…们·晒◎厂C价o8U.q6~b.曙Ⅱ′

pe尸l→plRp〔.∩Oα7C∩050.20Z6.M.e17



Sq0』idˉ巾1gr◎t1o∩ˉ已〔厂1Pt.x86→“7:35.乙0→17^e〗7=9.6 q

:毕‖

q

沪o◎t@7111血骂y∑62q矿sγste们Ctl 5tα矽ts日u{d 旦正

◎∏●

「mt@7111Z哎Sy262 ]尽Sy5t酮Ctle∏吨!e凸∩uid 7e◎feoSy∏↑1i∩k 『7叮/etC/£yste∏n/三ySte朋/厕』lti~u£e广tC厂get.w口∩ts/£细id.田e厂v1〔e /u$7/l1b/Sy巴t咖/■γ5七e呵巳quM.5ewt〔e.

沪m馋7111∑∩syH6∑、MSy三tQ酗号t1Stαtu55qujd 5qu1d·邑e广v1Ce





§Qut◎cα[向i∩gp了oxy

e亡 LO口□oo. Ⅱ“0刨(/u田广/1jb/5y巴te硒/导y5tew巳qu1d5e沪γ1〔e; e∩αb1cd;γ色咽O厂p「e二e



0iSαb1e仗)

ACtwe

幽〔r ` ′

J

凸1∩〔e日Z6E1O71】1〕Z2·03C5丁’8$αgO

№mpID。“539(>q凹id)

〔6roup; /叁y5te币.围l1屯e/5quld·田eFγ1喧巴 什46s39/仙S广/邑b1∏/巴qg1°ˉ「/e七[/5qu【d/5qut◎Co∩f

d

‖,

恰《陌41(Squ【d1) f/etC/SqMM/qqmd〔咖f

巴啡6SqZ(l四「11eooe砸∩)/γ叮/log/5qutd/αC〔e蚂.log 了+′了了"「妇

月11 】3:z2:03n11巳α三yz62∑γ∑te↑pd[1Ⅱ; S↑◎伊ti咽SQutdCG(h1Ⅳgp广◎xy…

月11 13。乙H:0371112o笆yZ6Z凸qu1α[耳6539〕· Squtdp□「e∩t8Ni11二tO『t1代id■





同111322合0371』亚α已γ之62$qut口[%臼39]: 5qm创po厂e∩t吕(鱼qmdˉ1)pmce£S0…□

月1113:z2;037【11Z□已y262巴y凸te刚di1]· 5t□Fteo5quld〔o〔∩mgp广Oxy.

q

1∩t. bo阳e[me5We『oee〖I1□5`Zed0 u“=l↑Ob0]呻i∏「凹lL 广o◎馋7111ao凸γ262^.]# 】茹z…]

」 图9ˉl2成功运行的Squid

Squld默认会运行在3l28端口’相当于在云主机的3l28端口启动了代理服务°接下来我们测试

一下Squld的代理效果,在该云主机上运行〔ur1命令,请求https://www』lttpbinorg,并使用在云主机 上配置的代理服务:

0









cur1 ˉxhttp://127.o·0.1:3128http5;//b州‖°httpb1∩。org/get

这里〔ur1的ˉX参数代表设置HTTP代理’由于现在是在云主机上运行,所以直接将代理设置

为了hnp://l270.0.l:3l28°运行完毕之后,再用1+co∩「1g命令查看下当前云主机的IP,结果如图9ˉl3 所示。

q q U





」 q









9.4ADSL拨号代理的搭建方法 ●△攀

36l

…臼7W『z■叮202°≡

[尸mtG7】〗12α5y26Z≈]F〔u厂l t恤衣 尸mt吁门】】gα5yZ6乙≈』F〔u厂I ˉ又∩吐p.〃渔7.001:3128 .又∩ttp.//1Z7.001:31翻 httpB;〃恍t恤j∏.◎「p/腮仓 7

1

△~■「|‖‖|■厂

憾例沪鳞·p: {}, ”枷e“詹Fs“:{

制∧迢〔ept.0

巾/D0v’

|亡■厂||

"m乌t|,; 翻狱tpO1∩。◎79哟, 嘲05e厂霉蛔e∩t■。 〔uF1/7290 , 呐X~枷r∏.丫厂o〔eˉ1d蓟.恫mt■16捉色8】be“2G3“67抖5768C钢d2bcαb曰

}9

′}

▲广「|‖‖

"◎庐igj"仰: 偶1脆4S.I腮1“°『0

`,u庐《输: w打ttp5吕〃∏ttpb1∩.O广g/听t′, } [F 『mt蟹7u12◎5γ正2~]么飞「C团『1O e七 t旧;

fmg二纠163d」0,BRO∧Ⅸ∧5『『∩0榔I腮0阳」L『IαS丁>赋o15锄 l∩e↑ 10.22.田测∏etmα弛酶5z552550

b厂m钧c@巴t 10乙z“.Z5S

i「V巴r6「e80弓·◎1681仟[email protected]α p陀「Ⅱx【e∩“$亿ope1d9叉2O≤M∩岭 )■「止尸

e恤e广侧;】6吕81.〕5375α t又queueIe∏】酗([恤e7腮t) 赋抄α〔代et5S11泅9 byte墨“1633338(4r11烦8) 灿e厂厂◎了已0巾…e°12610oγe厂广凹∏巴0什匈e0

丁Xpg〔出et5246130

byte∑2Z3田215〔n3毗B)

dFO印ed0Oγ它F沪u问$0 〔口厂广Ⅱ台「0 〔◎ut$1o向巴0

『Xe厂7o厂S0

[o;健αg5m73山p’[”0A〔Ⅸ0∩l』删I腮≥毗U655无

p ■■「‖|卜β『》【卜

1∩et』27.0.0.1

∏etm$k蹈5·o0.0

t∩et6 ∩et6 · ·1 .1 p下台「tX1e∩128 p下白 邑c卯e1α0X10≤仍◎母t≥ loop oop t又queuele∩1酗([o〔圆iL四pm〔氏》 t又queuel RX RXpo厄仅et£35 bγte已8662(8.▲Ri8) RX RXG厂尸OP田0咖Oppeo0 oγe广『u∩50 f「…0

∏xpO毗et已35 byt它已8662(8.qmB) Ⅶe广「o厂50 nmppcd0oγeP沪阐∩50 Cq「厂1e厂0

COlM窗tc侧£0

pp姆 p谰:∩吧s鸽鸿s<t』p”I‖丁网I‖『,R《j栅I掷6,刚∧Rp,Ⅻ叮Iα颤> 『

i『Tet 1%°4S.1钒°1“∩e七…文2S5.25§·255255

户■「‖|■■【β ■厂『‖|『尸■厂‖▲β『‖

■尸「旦ˉ■■■尸′‖【『『■■■∏

|■厂‖

■■■【【=尸‖「『▲■「

■「| ■厂|『 ■ ■ 【 『 ‖ } 【 ■ = ■ [ 『 | ■ 【 ■ ■ 「 尸|匹■‖「 }■『■■『巳■【■□‖‖‖■■■『·



碾tu1呜【

deStⅡ『℃tt创】1肠·▲5.1叭°1

图9=l3使用代理请求测试网站

可以看到测试网站的返回结果中的oI1g1∩字段里的IP和用j+〔o∏千1g获取的IP是-致的° 接下来,在自己的本机上(非云主机)运行如下命令测试代理的连通情况’这里的lP就需要更 换为云主机本身的IP了’从图9ˉl3可以看到云主机当前拨号的IP是l06.45.l04.l66’所以需要运行 如下命令: 〔ur1 ˉxhttp://1o6。q5。1o4166:3128∩ttp58//w洲°httpbj∩·org/8et

然而发现并没有输出对应的结果’代理连接失败。其实失败的原因在于Squid默认不开启“允许 外网访|可』,对此我们可以修改Squid的相关配置’例如修改当前代理的运行端口`允许连接的IP, 配置高匿代理等’这些都需要用到配置文件/etc/squid/squidconfb 要开启“允许公网访问”’最简单的方法就是将配置文件中的这行: httpˉ己C〔e55de∩ya11

修改为: ‖ttP-aC〔e55a11Owa11

意思是允许来自所有IP的请求连接本代理°另外还需要在配置文件的开头配置a〔1的部分添加: a〔1a115r〔0°0°O。O/0

然后’将Squid配置成高度匿名代理’这样目标网站就无法通过一些参数(如Xˉ「or"ardedˉ「or) 得知爬虫机本身的IP了’所以在配置文件中再添加如下内容: req0e5t-headerˉa〔Ce5Sγiade∩ya11

req0e5t=header=ac〔e55Xˉ「orwaIdedˉ「orde∩ya11

考虑到有些云主机厂商默认封禁了Squjd代理所在的3l28端口’因此建议更换一个端口,例如 3328’修改这行即可: http-port3128

将其中的3128修改为3328:



第9章代理的使用

‖ttp=port 3328

修改完这些配置信息后’保存配置文件’重新启动Squid代理: 5y5te川〔t1re5tartsqujd

此时重新在本机上(非云主机)运行刚才的〔ur1命令(将端口修改为3328): 〔ur1 ˉxhttp://106°45.10』.166:3328∩ttp5://州°∩ttpbj∩°org/get

返回结果如下: {

,U5erˉ∧ge∩t"8 "〔l」r1/7.64°1"’ "XˉA∏)z∩ˉ『mceˉId"8 "Root=1ˉ6oea8千〔Oˉo7O1b1494eq68Ob95889〔db1"

}’ 00or18j∩": 00106°45°104°166"’ "ur100 : "http5://瞅。httpbj∩°org/8et" }

现在就可以在本机上直接使用云主机的代理了!

5.动态获取|尸

知道拨号后的IP就可以使用代理了°

那怎么动态获取拨号主机的IP?怎么维护这些代理?又怎么保证获取的代理_定可用呢?还可 能出现下面的问题°

□如果我们只有_台拨号云主机’并且设置了定时拨号,那么在拨号的几秒内,这台云主机提供 □如果我们不使用定时拨号的方法,而是在爬虫端控制拨号云主机的拨号操作’那么爬虫端还需 要定义单独的逻辑来处理拨号和重连问题’这会带来额外的开销° 综合考虑下来,—个比较好的解决方案如下°

□为了不增加爬虫端的逻辑开销’无须爬虫端关心拨号云主机的拨号操作,它只要保证爬虫通过

某个接口获取的代理是可用的就行,即拨号云主机的代理维护逻辑和爬虫端毫不相关° □为了解决_台拨号云主机在拨号时代理不可用的问题,让多台云主机同时提供代理服务’可以

将不同云主机的拨号时段错开,当_台云主机正在拨号时,用其他云主机顶替它提供服务。 □为了更加方便地维护和使用代理,可以像92节介绍的代理池一样把这些云主机的代理统_维

护起来,把所有拨号云主机的代理统—存储到-个公共的Redls数据库中,可以使用Redis的

Hash存储方式,存好每台云主机和对应代理的映射关系°拨号云主机在拨号前会清空自己对 应的代理内容’拨号成功后再将更新代理’这样Redis数据库中的代理就-定是实时可用的了。 利用这种思路’我们要做到如下几点°

□配置_个可以公网访问的Redis数据库,每台云主机都将自己的代理存储到Redis数据库的对 应位置,由Redis数据库维护这些代理。

□申请多台拨号云主机’并按照上文所讲内容,在这些主机上配置好Squjd代理服务,为每台云 主机设置定时拨号来更换IP°

□每台云主机在拨号前先删除Redis数据库中原来的代理,拨号成功后测试一下代理的可用性’ 并将最新的代理更新到Redls数据库中。

引□||』□]‖』■■||』■||』·司||‖|{』□】||‖‖{〗‖‖||□‖||■∏||‖■】』』√‖』』】□】□■‖|』■■√‖』●』■〗■】】Ⅶ‖习』引‖□]‖|●□□

的代理服务是不可用的°

‖□·■■■日』■■■■勺】‖■■■°■司■·

我们现在已经可以执行命令让主机动态切换IP,也在主机上搭建好代理服务器了,接下来只需要

」■□■可」‖』■∏〖‖‖Ⅷ■■∏|■■】|』田』■‖‖』■』■‖‖|■]‖

"日IgS": {}」 "he3der5": { 00∧〔〔ept|0 : "*/*"’ 00‖o5t"目 阑州.∩ttPbj".org"』

‖{』■可‖||·∏{|」·‖||□□||门]』‖」勺〗‖』·】‖‖|用‖||Ⅱ司‖‖‖

362







9.4

ADSL拨号代理的搭建方法

363

接下来就实操—下吧。我们使用Python语言,先在云主机上安装Python库:

p

γu‖ˉyi∩5t己11pγt∩o∩3



关于自动拨号、连接Redls数据库`获取本机代理`设置Redls数据库的操作’我已经写好了_

b





β

卜 b





个Python包并发布到PyPi了,大家可以直接使用这个包完成如上操作,这个包叫作adslproxy’可以 在云主机上使用plp3T具安装: pjp3 i∩5ta11ad51proxγ

安装完毕后’可以使用export命令设置环境变量: exportR[DI5"05丁=〈【edjs数据库的地址〉

二 export日[0I5p∧55‖0RD≡〈Redj5数据库的密码〉

二 eXpOrt0IA[8A5‖=〈拨号脚本〉



eXPortαI[‖丁‖酬[=<云主机的唯一标识〉 exPort0IA〔〔γα[=〈拨号间隔〉

b

b





P









这里的R[0I5∩0S丁、 R[DI5P0盯、【[0I5p∧55‖0R0是远程Redis的连接信息’就不再赘述了° pR0Xγp0盯是云主机上代理服务的端口,我们已经设置为了3328°DI∧儿B∧5‖是拨号命令’即

pppoeˉ5topjpppoeˉ5tart’当然对于不同的云主机厂商来说’该脚本的内容也可能不同,以实际为准° 01∧[1「‖酗[是拨号云主机上的网卡名称,程序可以通过获取该网卡的信息来获取当前拨号主机的IP

地址’从之前的操作可以发现’网卡名称是pppo’当然这个名称也以实际为准。αI[川丁‖州[是云主 机的唯-标识’用来在Redis数据库中存储主机和代理的映射,因为我们有多台云主机’所以应该把

广



不同云主机的名称设置为不同的字符串’例如ad511、ad512等。这里我们的设置如图9ˉl4所示°

D

—≡--=二舍些….帝=-■亏…心沁T?嚣…2鲤8=

7尸

印镶◆

β

p



…卸un‖辑. 删雾了辑‖… ˉ ~滤. 。 纂" [沪“馋7n返“y∑锤引狰酗”饯雕m典 Sy廷b毯≈』砰e紧梦o沪雹汉匡赃4载■ y廷b毡≈」 =p嘛…3涵 [了獭》瓣7u退αSy直6Ⅺ钟〕锥鳖诞m砒R田I$Sy直6Ⅺ钟〕锥鳖诞m砒R田I$ˉ y直6Ⅺ钟] p嘛7霓 …酗55娜p渔pˉ 』="°; ·笋卓 0 [F酗罐m1哩◎翔药乙蹿]棋哪p◎代匪m∑ˉ 锄2钮咐]棋哪p◎代匪m∑… y2钮咐] p抖55N0 =p0盯固328 . [沁膨憾 [『℃◎t譬γn亚例”鳃2~]*e呻加tp汕xγ≡ 5y2唾~]*e呻加tp汕xγ~ y鳃2别 ”盯巴 翱262剖撅Gx四浓哑A↓-趴 s肥‖洁0ppme≡疆…§p卵oe肄Sm妒t0 5‖蒸0p [沪硒锯 [沪铀馋7皿1m期262剖撅Gx四浓哑A↓-隘 [徊o髓! I「川“聋,”曲』 [侨“酶7n亚“y酶2刨狱它呻wkpIAk-I 二y酶2刨狱它呻wkpIAk-I 刚“= 糙]懈exp″tα亚N丁-酗[酶v四suq [沪m菇臀7n12◎£y2钮≈]雾expo施α亚N丁 £y2钮≈]雾expo施α亚N丁 ˉ酗[零 〔γα【=1韶 怠烬锤≈]熟eX酗咸赃AL-〔 ≈]熟魁Xp嗡咸赃∧L- γα【=1 [F“憾7越血α怠烬62≈]熟魁X酗咸赃AL-〔



0





≈〕雾 [F@ot色7u亚G∑y26堑≈〕雾



图9ˉl4我们设置的环境变量



p

设胃好环境变量之后’就可以运行ad51proxy命令进行拨号了’命令如下:

p

日d51proxγ5e∩d



运行结果如下:









P









2021ˉO7ˉ1115:30:O].O62 | I‖「0 2O21ˉO7ˉ1115:30:03.063 | I‖「0 2o21ˉo7ˉ1115:3o:03.063 | I‖「0

| ads1proxγ.5e∩deI.5e∩deI:1oop:90ˉ5tartj∩gd1a1… |ad51proxy.5e∩der.5e∩der:m∩:99ˉDja15tarted’ reⅦoveproxγ | ad51proxy.5e∩der.5e∩deI:re∩oγe—proxγ:62 ˉ Re们oγi∩g日d511…

2021ˉO7ˉ1115:30:O5·373 | I‖「0 2021ˉo7ˉ1115:3o:15·552 | I‖「0 2o21ˉ07ˉ1115:]o:16·501 | I‖「0

| ad51proxγ.5eⅧder.5e∩der:ru∩:111ˉCet∩ewIP106.45.1O5。3〕 | ad51proxy.5e∩der.5e∩der;ru∩:120ˉγa1idproxy1o6°45.1o5。33:3328 | 己d51proxy.5e∩der·5e∩der:5etˉproxγ:82 ˉ 5u〔ce55+u11γ5etproxy

2021ˉo7ˉ1115:3o:04065 | I‖『0

| ad51proxy.se∩der°5e∩der$re『∏oγe—proxy:69ˉRe∏℃vedad5115u〔〔e55「u11y

106.45.1O5·33;3328

2o21ˉ07ˉ1115:33:36.678 | I‖「0 2021ˉo7ˉ1115》33:36.679 | I‖P0 2O21ˉ07ˉ1115:33:36.68o | I‖「0

2O21ˉ07ˉ1115:33:37.214 | I‖「0

2021ˉo7ˉ1115:〕3:38.617 | I‖「0

2021ˉ07ˉ1115:33;48。750 | I‖「0

| ad51pro×y。5e∩der。5e∩der:1oop:90ˉ 5t3rt1∩gdia1… | ads1proxy.se∩der.5e∩der:I|」∩:99 ˉ01a15tarted’ re『『|oγeproxγ |ad51proxγ.se∩der.5e∩der:Ie帅ve一proxγ:62 ˉ Re加γi∩gad511…

|ads1proxy.5e∩der.5e∩deI:工e爪oveˉproxy:69 ˉ Re们oγedad5115u〔ce55+u11y | ad51proxy.5e∩deI。se∩deI:r仙∩:111 ˉCet∩ewIp106·45.1o5.219

|ads1proxγ.5e∩der肆5e∩der:r|」∩;12oˉγa11dproxy1o6.45.1o5.219:3328



『 p







从中可以看到’因为在云主机拨号之后’当前代理就失效了’所以程序在拨号之前先尝试从Redls



广









||"(

第9章代理的使用

364

数据库中删除当前云主机的代理°然后开始执行拨号操作,拨号成功之后如果验证代理是可用的,再 将该代理存储到Redis数据库中。这样循环往复运行’就达到了定时更换IP的效果’同时Redls数据 库中存储的也是实时可用的代理。

最后’们可以购买多台拨号云主机,并都按前面那样配置,这样就有多个稳定且定时更新的代理 可用了,Redis数据库会实时更新各台云主机的代理’如图9ˉl5所示。

| (

V∧虱斤捶::“O萌…‖×

…田

】锄v扛eγ

■m妇辑

陋蹿V旧

……≡_=—跨~出

蛔四

-≈村』钞

≈t汀L

』】□』■■

Sj酿3} 丁n:=‖

…8…



】 ……-≡

o吨… 吼__—■-岛

…月…

↑伯川09?"‖72吕… ‘虚

2

…]

1醒.!3a!斟】]3:…8

3

…∩

}捣22↑.γ团52…

■山…7酗 …早≡亡←—_-—牵

q

弓a22.Ⅵ↑23?…

…4





≡←

…鸭们呻…≡

』■‖■·

Redjs数据库中存储的各个代理

图9ˉl5

·|



●…γ砷■

二.■

图9ˉl5显示的是4台ADSL拨号云主机经配置并运行后,Redis数据库中的内容’其中的代理都 是实时可用的。

{ d

6.使用代理

如何使用代理呢?在任意支持公网访问的云主机上连接刚才的Redis数据库并搭建—个API服务

即可°怎么搭建呢?同样可以使用adslproxy库’该库也提供API服务。

为了方便测试’我们在本机进行测试’安装好adslproxy库之后’设置REDIS相关的环境变量: exPoItR[0IS‖05丁=〈Redj5数据库的地址〉

二 exportR[DI5p∧55灿R0=〈Redj5数据库的密码〉

2020ˉo7ˉ1116:31:58·651| IN「0

|己d51proxy。5erγeI.5erγer:6erγe:68ˉ∧pI1j5te∩j∩go∩∩ttp://0.o.0。0:8425

可以看到API服务就运行在8425端口,我们打开测览器即可访问首页,如图9ˉl6所示°

其中最重要的就是r己∩do"接口,使用这个接口即可获取Redis数据库中的~个随机代理’如图 9ˉl7所示° 铲

窃 }@‖°Ca‖》OSt:8比25





× 可

÷今Go{◎c已|榆◎St:8425

№‖◎o∏】●t◎∧DS止尸r◎xγ∧尸『

…。……刨…

@|。c副h°s『:a425/『a"d。m

●. 书

×{+



÷÷Go|oca!host:8感25/『a门d◎贼 ↑O6.45.↑O533:3328

图9ˉl6访问代理服务的首页

图9ˉl7获取_个随机代理

经过测试’这个代理的可用性没有问题’这样爬虫就可以使用它爬取数据了°

最后’我们部署_下API服务,就可以像代理池_样使用这个ADSL代理服务了。每请求—次API 服务,就可以获取-个实时可用代理’在不同的时间段,这个代理会不同’不仅连接稳定’速度也快,

|||||

然后运行如下命令启动adslproxy:





「 })‖



9.5代理反爬案例爬取实战

365

实在是网络爬虫的最佳搭档。 7.总结

本节我们介绍了ADSL拨号代理的搭建过程。通过这种代理,我们可以无限次更换IP,而且线路

■ ■

非常稳定,爬虫的爬取效果也会好很多°

「 ■ ■ ■ ‖

本节代码见https://glthub.com/Python3WebSpider/AdslProxy°

『 『 ▲ ■ ■ 「

9.5代理反爬案例爬取实战

〗 『 ■ 厂 ‖

92节` 9.3节和9.4节我们了解了代理池的维护和付费代理的相关使用方法,通过这些方法可以

▲ 尸 『 |

获得不少可用的代理’方便我们在爬取数据的时候伪造IP,绕过_些通过IP实现反爬的网站°

止 尸 卜 ■

本章我们就分析-个实例,看_下如何使用代理池绕过某些网站的反爬机制。

■ 「 ) 》 「

↑.本节目标

| ● ■ ■

我们会以_个IP反爬网站为例进行这_次实战演练’该网站限制单个IP每5分钟最多访问l0次,

尸 ′ 『

访问次数超过l0,网站便会封锁该IP’并返回403状态码’l0分钟后才解除封锁°

匹 ∩ 广 》

) }

所以,要想在短时间内快速有效地爬取这个网站的所有数据’就得使用代理了°我们会先使用92

节讲解的代理池获取—些可用代理’再利用这些代理爬取数据,本节会介绍整个爬取流程的实现° 2准备工作

●尸|卜■■

首先需要准备并正常运行代理池。还需要安装好_些Python库—requests、redlsˉpy、 envlrons、 pyqueIy和loguru,安装命令如下:

》 「

p1p31∩5ta11reque5t5redj5e∩v1ro∩5pγquerγ1o8ur0





安装完毕后,就可以往下走了°

■ 『

3.爬取分析

β 卜

本节要爬取的网站是https://antispider5.scrape.centeI√,首页如图9ˉl8所示°





—…

= ●

……





恤五







……回









回加

■■

◎°口·

■L

×

一心



■彰…‖…

} -坷

】厂‖『『二仅

■王别姬→Fa田w●‖Myc°"Cub‖∩●

9·5

●●

宙☆亡仓 °

….中■■记』‖∏潞



】…T.必上议

「|「}|

}}

「「‖

》卜尸》|



这个杀手不太冷ˉL◎◎∏

●■●●

95 台台●古★

■■′0测舔 ●

0…ˉ↑0上h



| §

了■

肖申克的救赎ˉ仆●ShGw已h投∩找Re创●陋pt0◎∩

●●

9.5 ●★★台仇

mf0@Ow 】…‖0上炔

图9ˉl8爬取的目标网站



页面看上去和之前没什么不同’{日试里网站增加了IP反爬机制’限制单个IP每5分钟最多访问l0 次’超过l0次就封锁IP,并返回403状态码°例如我连续刷新l0次网页,页面就变成了下面这样’

| ● .● e

|{

如图9ˉl9所示° 403「◎『b闷de∩

×

+ ~尸吊

p尸 <

》p

P

0a∩t『sp‖Oe『5.sc「ape。ce∩te『

G

□`■■|●■Ⅵ|{|』·司|·■||』Ⅵ

第9章代理的使用

366

d

4O3庐o『b『dde∩



| 图9ˉl9连续刷新l0次后的页面

以确保请求失败的时候可以再次爬取,直到成功。

‖‖‖

由于我们无法预知某个代理是否能完成-次正常的爬取,因此可能请求成功也可能请求失败’失 败原因可能是网站封锁了该代理,或者代理本身失效了。为了保证正常爬取’我们需要添加重试机制,

0

』{

但如果此时切换一个网络环境,例如使用全局代理或者由WiˉFi切换到手机热点,总之让访问目 标网站所用的IP地址发生改变’就又可以看到页面正常显示了。也就是说’要想在短时间内爬取这个 网站的所有数据,得更换多个IP进行爬取,怎么更换呢?自然就是使用代理了°





·









如果有很多个请求都失败了’又该记录呢?_个简单的解决方案就是使用队列’当请求失败时,把对 应的请求加人队列里’等待下次被调度°队列的实现方式有很多’本节我们选用Redis实现’简单高效°



那怎么实现失败后的重试呢?至少要把失败的请求记录下来吧,那记录下来后又保存到哪里呢?







综上所述,本节实现了如下几个功能:





‖】

日日 日·‖

|勺」门|

·]·‖]‖

i∩it (Se1+’

」■■|』』‖

de+

』■Ⅵ‖‖|

〔1a5SRequeSt(Req0e5t‖OO促5‖1Xj∩):



先看看Reque5t对象的源代码:



等方法都是通过执行尺eque5t对象实现的°



我们可以采用继承requests库中的Requcst对象的方式实现这个数据结构。reques帕库中已经存在 Reque5t对象’它将请求作为_个整体对象去执行,得到响应后再返回°其实reques临库里的get、po5t



列获取出这个对象的时候直接执行就好了°



既然要用队列存储请求,就肯定要实现_个请求的数据结构’这个请求需要包含一些必要信息, 例如请求链接`请求头、请求方式和超时时|司。另外’对于_个请求,需要实现对应的方法来处理它 的响应,所以需要加-个回调函数〔a11ba〔代。如果_个请求的失败次数太多’就不会再重新请求了’ 所以还需要增加失败次数的记录°用这些内容组成—个完整的请求对象并放人队列等待被调度,从队



4构造请求对象



下面几节我们用代码实现一下这些功能°



□提取详情页的信息°



□构造Redls爬取队列’用队列存取请求; □实现异常处理’把失败的请求重新加人队列; □解析列表页的数据’将爬取详情页和下_页的请求加人队列;



爬thod=‖o∩e』 uI1=‖o∩e』 header5≡‖o∩e’ fj1e5=‖o∩e’ data=‖o∩e’

·(

| ‖



■■【■■■■‖‖■■【『|■■『『「匹■『『‖|■厂‖‖|■尸|}广|‖}△β‖■『‖「[■「日|『|■厂【『|■■「「|卜



9.5代理反爬案例爬取实战

367

par己|∏5=‖o∩e’ aUt∩=‖o∩e」〔Oo促1e5=‖O∩e’ hOO代5=‖O「}e’ ˉ|5O∩=‖O∩e) d己ta= [] j十dat己15‖o∩ee15edata 十i1es= [] j千+j1e515‖o∩ee15efj1e5 headeI5= {} i个he日der5i5‖o∩ee15e∩eader5

pamⅦ5={}j+p己ra阳5 js‖o∩ee1sePam们5 们oo代s≡{}i+肘oo促5 15‖o∩ee15e∩oo低5

5eMhoo低5=de十己l」1tbooR5(〉 十or (代’ v) j∩1ist(hook5.ite∏5()): 5e1十.regj5terhoo代(eγe∩t=R’ hook=γ) 5e1十.『爬thod=Ⅷet们od

5e1+·0r1=ur1 5e1千。‖eader5=headeI5 5e1+。伍1e5=「11e5

5e1十°d己ta=data

5e1十·j5o∩≡j5o∩ 5e1千.pam们5=pam"5 5e1千.auth=aut‖ 5e1+。COO代ie5=COO促ie5

这是requests库中尺eque5t对象的构造方法°其中已经包含了请求方式请求链接和请求头这几 个 属性’但和我们需要的相比还差几个°因此需要实现_个特定的数据结构’在原先【eque5t对象的

基础上加人上文额外所提的几个属性°这里需要继承【eque5t对象重新实现_个请求对象’将其定义

为‖oγjeReque5t’ 实现如下: 丁I"[α∏=1o

+rO‖reque5t5j爪Port伺eque5t

〔1日55‖oγjeReq0e5t(Reque5t): de千 j∩it (5e1十」 ur1’ 〔a11ba〔k’爬tbod=℃[『‖’ header5=‖o∩e’∩eed-proxy=「a15e’ +aj1tj爬≡O’ tj‖∏eOut=『I‖[0U丁): Reqoe5t· j∩it_〈5e1十’『∏etbod’ ur1’ ‖e日der5) 5e1十.〔a11ba〔决=Ca11baC促



5e1千.十aj1tme=于ai1t加e

Se1千·ti爬Out=tj爬Out

这里我们实现了‖oγjeReque5t类,代码文件保存为requestpy,在构造方法中先调用了Request类 的构造方法’然后加人了几个额外的参数’分别定义为ca11bac代` 十aj1t1‖e和t1"eout’代表回调函 数`失败次数和超时时间。

之后就可以将‖ov1eReque5t作为一个整体来执行,各个‖oγjeReque5t对象都是独立的,每个请 求都有自己的属性°例如’调用请求的〔a11baCⅦ属性就可以知道应该用什么方法处理这个请求的响应’ 调用+a11t1Ⅷe就可以知道这个请求失败了多少次’继而判断失败次数是否到达阔值,该不该丢弃这 个请求。这里我们采用了面向对象的_些思想°

5.实现请求队列

现在要构造请求队列’实现请求的存取。存取无非分为两个操作—放和取’所以这里利用Redis 的rpu5h方法和1Pop方法即可。

另外还需要注意’存取时不能直接使用Reque5t.对象°因为RediS数据库里存着的是字符串,所

以在存Reque5t对象之前要先把它序列化’取出来的时候要将其反序列化’这两个过程可以利用pickle 模块实现: 十ro们pj〔Ⅸ1ejⅧportdu"p5’ 1oad5 +ro们reque5t 1‖port‖eixi∩Reque5t 〔1a55Red15Que0e(): de「 1∩jt (5e1千):

5e1f。db≡5tIi〔tRedj5(∩o5t=R[0I5ˉ}{05丁’ poIt=R[DI5-p0盯’ pa55"ord=R[0I5P∧55‖0R0)



■‖

=■■‖‖■■可|■■]|■■■‘‖】‖』■■

第9章代理的使用

368

de+日dd(5e1十’ reque5t): j于i5j∩5ta∩〔e(req0e5t’‖ovieReque5t): retl』r∩5e1「.db.rPu5b(【[DI5旺γ′ doⅧp5(Ieque5t)) retuI∩「a1Se

工etur∩「a15e

de十e们pty(5e1十): retum5e1于.db.11e∩(R[DI5旺γ)≡0

这里实现了—个Red150ueue类’代码文件保存为dbpy’先在构造方法里初始化了一个

■■||二■■可‖`』』』·■■■可

de+pop(5e1千〉$ 1于5e1「.db.11e∩(日[0I5旺γ): retur∩1oads(5e1+°db.1pop(R[01S旺γ))

5tIi〔t【ed15对象°随后实现了add方法,方法中首先判断了请求对象的类型如果是‖oγ1eReque5t’

程序就会使用pickle模块的du们p5方法把它序列化,再调用Ip‖5h方法把它加人队列°pop方法则相反,



先调用1pop方法从队列取出请求’再使用pjckle模块的1oad5方法将其转为‖oγ1eReque5t对象。另 在调度的时候,我们只需要新建—个Redj5Queue对象然后调用add方法,传人‖e1x1∩【eque5t对象’

日‖」

外’eⅧpty方法会返回队列是否为空,通过判断队列长度是否为0即可知道。 即可将‖eix1∩Reque5t加人队列,调用pop方法,即可取出下_个№γ1e尺eque5t对象,非常简单易用。 6.修改代理池

现在要找_些可用代理’此处直接使用94节的代理池即可°根据9.4节的操作启动代理池,等

《』‖

待_定时间’可以观察到RedisHash表中多了一些l00分的可用代理,如图9ˉ20所示。 T本机:鲍O::仅ox…×

∏3.25523a2』9缚R田6

72

〗万.2印.『2a20:SO80

73

↑凹97.250Ba“

7哗

↑8OS7.250B788O

75

]80·97ˉ25O即:8O



‖刨.脚a↑35.↑3a6328↑

`“.O.『3a"7:… ?3?99ˉ9.2豌s8酗

79

5224鳃.2弘:鳃33

SO

66。↑62】22.2伞…O



“.57.↑鲍2驹:3‖28

82

■雷鄂V口□

e

…↑

◎「T 锣t…

@

9↑°2宜9ˉ222J6a532S‖



图9ˉ20生成的可用代理

代理接口设置为5555’因此访问http://l27.00.l:5555/random即可获取随机的可用代理’如图9ˉ2l 所示°

再定义一个用来获取可用代理的方法: p∩0Xγp卯[0Rl= |http://127·o·0。1:5555/Ia∩do‖, 「ro"1ogurumport1ogger





.@]27OD.↑:655刨「日∩GQ∏》

×



·当■∏■■〗纠·□』‖■■‖』■·‖‖

78

■”ete「叫

●胎亿adγa‖归

‖可

76

o…∩础

(‖‖

7‖

铀↑丁几

■四℃怕

陋》碱『陶

』{

…·副7j屿.!67S肥O

丁丁L:=]

卧咎§己·『

7O

S‖狙;82

…蛔灿烟Ⅷ灿灿灿Ⅷ仰Ⅷ帧仙灿

ZS曰8…睦 「呻v咽归

←啡◎o沤7O。0.↑:5555/『a砸Q耐 2↑a8a204。2“:3256

°·{』■∏|

1og8eLdebug(+』getproxy{re5po∩se.text}′) retur∩Ⅲe5pO∩5e·teXt

图9ˉ2l

获取的一个可用代理

√‖·

01ogger.Cat〔‖ de+get_proxy(): re5po∩5e=reque5t5.get(pROXγp"L0促[) j十Ie5Po∩5e。5tatu5code≡=2OO:

‖‖【《‖ q







9.5代理反爬案例爬取实战







369

这里有个小技巧’我们使用loguru日志库里的cat〔∩方法作为get~pIoxy方法的装饰器,这样可

以在请求代理池失败的时候输出具体的报错信息’同时又不会中断程序运行,也避免了编写tryex〔ept 语句的麻烦’使得代码看起来更简洁°

↑ 7.第-个请求





} }



_切准备工作都做好了’现在我们就可以构造第—个请求并放到队列里以供调度了°代码如下: 十rO川Ieql』e5t5j刚POrt5e55jO∩ +ro‖dbmportRedj50ueue U

十ro∏reque5t加port日ovje【eque5t 8∧5[0Rl= ,http5目//a∩tjsp1deI5。5cmpe·〔e∩ter/` ‖旧∧0[【5= {



U5erˉ∧ge∩t0 ; |№zj11a/5.o(‖a〔j∩tos∩j I∩te1问a〔05X10-12-3)∧pp1e‖eb长jt/5]7.36 (Ⅸ‖丁阶[’ 1让eCec代o〉 〔∩ro∏记/59·0。3o71·1155a千arj/537。36‖

} bF







〔1a555p1der(): 5e55jO∩二5e55iO∩() queue=Redi50ueue()

de+5tart(5e1千): 5e1十.5e55io∩.header5。upd3te〈‖[A0[【S) 5t己rtur1=8∧5[0肌



















| p









Ieq0e5t≡肋γieReque5t(uI1≡5tartur1’〔己11bac代=5e1十.par5e1∩dex) 5e1f.queue·add(reque5t)

泣里先定义了2个全局变量, B∧5[0R[代表目标网站的URL’‖[∧0[R5代表请求头。然后定义了

5pjder类,代码文件保存为spiderpy°



在5pjder类中,先初始化了5e551o∩对象和Red1s0ueue对象,分别用来执行请求和存储请求° 然后定义了5tart方法’该方法第一步全局更新了∩eader5,使得所有请求都能应用全局变量‖[∧0[R5;

9 k

第二步构造了-个起始URL’并将8∧5[0R[赋值给它;第三步用起始URL构造了—个‖ovjeReque5t

对象’回调函数是5pider类的paI5ej∩dex方法,也就是说当请求成功后就用par5ej"dex方法来处 理和解析返回结果;第四步调用了Red15Queue对象的add方法’用于将请求加人队列,以供调度° 8调度请求

把第_个请求加人队列之后’就可以开始调度执行了.首先从队列中取出这个请求’将它的结果 解析出来,生成新的请求加人队列,然后拿出新的请求,将结果解析,再新生成的请求加人队列,这 样循环执行,直到队列中没有请求’代表爬取结束°

我们在5pjdeI类中添加5Chedu1er方法’实现如下: 「ro们1oguIu1∏port1og8er γ∧kID5「∧】U5[5≡ [20O]

de十5Chedu1e(5e1f):

w∩i1e∩ot5e1「.queue.eⅧpty(); reqoe5t=5e1f·queue。PoP() 〔a11b日〔代=reque5t°〔a11ba〔k

1oggeI°debug(十|executi∩greque5t{reque5t·ur1}‖) re5po∩5e≡5e1+.reque5t(Iequest)

1ogger.debug(十‖re5po∩se5t己tu5{respo∩5e}o「{reque5t.ur1}0) i十∩otre5po∩5eor∩otre5po∩5e。5tatu5codei∩γ∧[ID5丁A「U5[5: 5e1千。erroI(reque5t) 〔o∩t1∩ue

re5u1t5=1j5t(ca11b己〔代(re5po∩5e〉) i十∩OtIe5u1t5:

se1「.error(req0e5t)









0」

370

第9章代理的使用



||

q.

〔o∩ti∩ue



+Orre5u1tj∩re5l』1tS8

i于j5i∩5t日∩Ce(Ⅲe5u1t’№γieReque5t):

1ogger.debl』g(千!ge∩er己ted∩ewreque5t{re5u1t}‖) 5e1+.queue°add(re5u1t) 1千1S1∩5ta∩〔e(re5u1t’ diCt):

1og8er.debug〈「05craped∩e"data{Ie5u1t}』)

5C∩edu1er方法的内部是一个wM1e循环,该循环的判断条件是队列不为空。当队列不为空时’

调用pop方法取出下_个请求,然后调用reque5t方法执行这个请求, reque5t方法的实现如下: 01ogger。catch defIequeSt(5e1f’ req‖e三t); proxy≡get卫roxy() 1ogger.debug(千‖getproxy{pⅢoxy}0) proxje5={ ‖http0 : 0http://| +proxy’ ‖http5! : 0∩ttp5://0 +proxy } i+proxye15e‖o∩e retuI∏se1+.ses5io∩·5e∩d(Ieque5t·prepaIe()’ ti爬ol」t≡reque5t。t1脆out’ proxie5=prOxje5)

reql」e5t方法中’首先则调用get-proxy方法获取代理,然后将代理赋值为prox1e5变量以备使用。 接着调用5e55io∩变量的5e∩d方法执行这个请求’这里调用prepare方法将请求转化为了prepared

Reque5t对象(这在本书22节有相关介绍)’具体的用法可以参考https://docspythonˉrequestsorg/en/ master/use∏advanced/#preparedˉrequests, t1阳eout属性是该请求的超时时间’ prox1e5属性就是刚才声 明的代理°最后返回5e∩d方法的执行结果°

执行req‖e5t方法之后会得到两种结果:一种是「a15e,即请求失败’连接错误;另-种是Re5PO∩5e 对象’即请求成功后服务器返回的结果’需要判断其中的状态码,如果状态码合法,就对返回结果进 行解析,否则将请求重新放入队列。

判断状态码合法后’对返回结果进行解析时会调用‖oγ1eReque5t类的回调函数°例如这里的回调 函数是par5e1∩deX,其实现如下: 卡roⅦpyql』eryj川portpyQ0eⅢya5pq 十ro{∏ur11ib.par5e1们portur1joj∩ de十parsei∩dex(5e1十′ re5po∩se): do〔=pq(re5po∩5e.text) #讶求坪怕页

ite‖5=do〔(! .jte们 .∩a们e』).ite爪5() +oriteⅧi∩ite们5:

detai1ur1≡ur1jo1∩(8∧5[0肌’1teⅧ.attr(!‖re千』)) reque5t=例oγjeReque5t( ur1≡det己i1ur1′〔a11ba〔低=5e1千.par5e-deta11) γ1e1dreque5t #计求下一页





q ■







·

q

0

{ q √











{ l













| d

∩e叉thre于=do〔(0 .∩ext!)。attr(!∩re+『) jf∩eXthre+:

∩extur1≡ur1jo1∩(8∧5[0R[’∩ext∩re千) reque5t≡‖OγieReque5t( ur1=∩extur1’〔日11b日〔R=5e1+.par5ej∩dex) γje1dreqqeSt

这里定义了_个生成器’它做了两件事:一件事是获取列表页中所有电影对应的详情页链接’另

一件事是先获取下-页的链接’再构造日ovjeReque5t对象’之后yield返回。

然后’5Ched‖』1e方法会对返回的结果进行遍历,利用j51∩5ta∩Ce方法判断返回结果是否为№Vj酿eque5t 对象,如果是’就将其重新加人队列。



■『【[「||止■厂‖β|■「卜■「■「‖世尸》』[■■厂△■■厂』′匹=■『|■▲■‖■尸[『|■【厂匹尸|●》|尸ˉ■「|》匡尸||ˉ□【「△■■「■。仕』||巴尸■■[【尸■「‖=■■■■「■【■[■「|’》■■厂

95代理反爬案例爬取实战

37l

至此,第一次循环运行结束°

这时"hj1e循环会继续执行°如果第一次请求成功’那么这时的队列里会新增爬取第一个列表页 中l0部电影对应的详情页的请求和爬取下一页的请求,即队列中又多了1l个新请求°程序会从队列 中获取下一个请求,然后重新调用reque5t方法获取其响应’再调用对应的回调函数对响应进行解析。

如果爬取的是详情页,那么回调方法就不一样了,是par5e=detai1方法,此方法的实现如下: j川Portre 十rOⅦPyqUerymPOrtpyQueIya5Pq

de+paIse-det己i1(5e1+’ re5po∩5e): doc≡pq(re5po∩5e.text) 〔oγer=do〔(!mg。coγer‖).attI(!sr〔’) ∩a爬=doc(0己)h2,).text() categorje5= [jteⅦ.text() 于orjte田j∩do〔(,。〔ategoriesbutto∩5pa门0).ite‖]5()] pub1i5‖edat=doc(|.i∩+o:〔o∩ta1∩5(上映)|).text() pub1j5hed己t二re.5e己rch(|(\d{4}ˉ\d{R}ˉ\d{2}〉!′ pub1j5hedˉ日t).group(1) i千p0b1j5∩edata∩dre。sear〔‖(』\d{4}ˉ\d{】}ˉ\d{2}‖’ p0b1i5们edˉ己t)e1se‖o∩e dI己阳=doc(|.dra们ap!).text() s〔ore=do〔(,p.5〔ore!)°text() 5coIe=十1oat(5〔ore) 1「5〔oree15e‖o∩e yje1d{ 0Coγer0 8 COγer』 0∩a爬‖ : ∩aⅦep

|〔ategorie5! : cate8orie5’ ,pub1ished-己t0 ; pub1i5bed己t’ 0dra爬‖: dm‖a’ 5core

:

score



这个方法解析了详情页的内容,提取出了电影的名称、类别`上映时间`简介和评分等信息,然 后将这些信息组合成一个字典返回°



之后程序会接着调用后续的请求,然后接着执行第三次循环、第四次循环’这样往复下去。每个 请求都有自己的回调函数’列表页解析完毕后’会继续生成后续请求,而详情页解析完毕后,会返回 结果,直到爬取完毕。 现在,整个调度就完成了°

最后,添加一个人口方法: de千n」∩(5e1+): 5e1十。5tart() 5e1于.5〔∩edu1e(〉 1十

∩a爬

== ,

归i∩

0:

5Pider≡5pider() 5pjder.ru∩()

ru∩方法中先调用5tart方法添加了第一个请求,然后调用5〔∩edu1e方法开始调度和爬取。 现在’对IP反爬网站的爬取就算完成了。 9.运行 部分运行结果如下: 2021ˉ02ˉ3102:28:55.227|0[BⅦ

| 〔oIe.5pider:5c∩edu1e:133ˉexecutj∩gIeque5t

∩ttpS;//a∩tj5pjder5 5Cmpe·〔e∩ter/ 2O21ˉ02ˉ31O2:28g55.232 | 0[B四 2O21=O2ˉ31O2:28:55·232 | D[BⅦ

2021ˉO2ˉ〕102:28:56。838|D[8(L

| 〔ore.5pjde【:getˉproxy:30ˉ8etproxy118°99.127.62:8o80 |〔ore.5pider8reque5t:102 ˉ8etproxy118.99.127。62:808o | 〔oIe.5pider:5c∩edu1e8135ˉre5po∩5e5t日tu52"o+



‖ttp5;//a∩tj5p1der5。5〔r日pe°〔e∩ter/

2O21ˉo2ˉ31O2:28:56。8』7 | D〔80C |〔ore°5pjder:5chedu1e:145 ˉ ge∩eIated∩ewreque5t http5吕//a∩tjspideI5.scr己pe·〔e∩ter/deta11/1 2o21ˉ02ˉ〕1o2:28:56.847 | D[8|」C |〔ore.5pjder:5chedu1e;145 ˉge∩erated∩ewreque5t https;//a∩tjspjdeI5.5crape°ce∩ter/det3i1/2 ■





「arewe11"y〔o∩〔ubj∩e0 ’ 0categoIie5|: [ !剧,阶′’ !爱′悄』]’|pub1i5hedat0 : ‖1993ˉo7ˉ26! ’ ‖dra川a0 : ‖影片 借ˉ-出《霸王月||姬》的京戏’伞扯出三个人之间一段随时代风云变幻的金′限』阶仇.段′」`楼(张丰破饰)与程蝶衣 (张国荣饰)是一对打′」、一起长大的师兄弟’ 两人一个演生’一个饰旦,一向配合犬衣元缝’…0 ’ !5〔ore! : 9.5}

q



||

2omˉo2_3102:28:56.85o | O[8(」C |core.spider:5〔hedu1e:145 ˉge∩erated∩ewreque5t http5://a∩ti5pjder5.5crape。ce∩ter/detai1/1o 2o21ˉO2ˉ3102:28856.85O | D[B0C | core.5pider:5chedu1e:145 ˉge∩er日ted∩e"reque5t ∩ttp58//a∩t15pider5。5〔r己pe.〔e∩ter/page/2 Ro21ˉo2_3102:28:56。85o | D[8[}C |〔ore.5pider:5chedu1e;133 ˉ exe〔uti∩greq|」e5t http5;//日∩ti5pjdeI5。5cmpe.〔e∩teI/detai1/1 2021ˉo2ˉ31o2:28:56.855|D[80C |〔ore.5pider:get一proxγ:3Oˉgetproxγ189.5.172。4」:3128 2021ˉo2ˉ3102:28:56.855 | 0[B06 | core.5pjder:req0e5t;102 ˉgetproxγ189.5.172.“:〕128 2O21ˉO2ˉ31o2:29:16.274 | 0[80C | 〔ore.5pjder:5〔hed|」1e:135 ˉre5po∩5e5tatu52OOof ∩ttp5://a∩tj5pider5·5CIape°Ce∏ter/detai1/1 2o21ˉO2ˉ3102:29:16.294 | D[Bl」6 | 〔ore.5pider:5c向edu1e:1』8ˉ 5〔I己ped∩ewdat日{!〔oγer0 : |‖ttp5://pα ‖e1t‖a∩.∩et/|∏oγje/ce4da3eo3e655b5b88ed31b5〔d7896〔f62472.jp8@46q"644h1e1〔‖’ |∩己爬‖ : ‖ ˉ霸王月‖|姬ˉ

叫‖」■■‖』■■】■‖]|■■∏|■Ⅶ〗』■〗〗□〗□■■】】‖‖‖■■

第9章代理的伎用

372







从结果可以看到,爬虫首先爬取了首页,也就是第一个列表页’爬取时通过getˉproxy方法获取 _个代理然后执行爬取’爬取成功,接着顺次产生了后续的ll个请求’即l0个爬取详情页的请求 和l个爬取下一页的请求°之后调度队列里的下一个请求’爬取第-个详情页,爬取时获取了一个新 的代理’爬取成功’输出了提取结果°然后接着往下执行,直到爬取结束°

{ q

{ ‖ q

d



↑O总结





本节中我们了解了利用代理池解决IP反爬问题的方法’实现过程中涉及一些队列的实现和调度

q



逻辑的实现’需要大家好好理解和消化°



本节代码见h叮s://githuhcom/Python3WebSpide门ScrapeAntispider5









‖』



‖ 叫





{ d q ■

| q



‖ (

d

d q 0







■■「|『「△■「卜■厂‖‖■尸】‖■仆『|■『‖|■∩‖‖●∏巴尸‖「■■尸

第]0章

|模拟登录 很多情况下,网站的_些数据需要登录才能查看’如果想要爬取这部分数据的话,就需要实现模 拟登录的一些机制°

0

『.

) p

模拟登录现在主要分为两种模式,_种是基于Session和Cookie的模拟登录’-种是基于JWT (JSONWebTbken)的模拟登录。

p

P



厂|‖卜 〖◆「》巴■「■■仗『■↑■『|●【■■‖|『■■■「·■■■『【◆『{‖|■尸■尸

p

尸‖【 ■■■■「▲■尸臼『■血■■厂 ■∏■「『【●『【尸}~■厂|》巴■「}【■■=■『问|||■尸||





对于第一种模式,我们已经学习过Sesslon和Cookle的用法°简单来说’打开网页后模拟登录, 服务器会返回带有5etˉ〔ook1e字段的响应头’客户端会生成对应的Cookje’其中保存着与SessjonID相 关的信息’之后发送给服务器的请求都会携带这个生成的Cookie°服务器接收到请求后,会根据Cookie 中保存的SessjonID找到对应的Session’同时校验Cookje里的相关信息,如果当前Session是有效的 并且校验成功,服务器就判断当前用户已经登录,返回所请求的页面信息°所以’这种模式的核心是 获取客户端登录后生成的Cookie°

对于第二种模式也是如此’现在有很多网站采取的开发模式是前后端分离式,所以使用JWT进 行登录校验越来越普遍°在请求数据时,服务器会校验请求中携带的JWT是否有效,如果有效,就 返回正常的数据°所以’这种模式其实就是获取JWT°

基于分析结果’我们可以手动在测览器里输人用户名和密码’再把Cookie或者JWT复制到代码

中来请求数据’但是这样做明显会增加人工工作量°实现爬虫的目的不就是自动化吗?所以我们要做 的就是用程序来完成这个过程,或者说用程序模拟登录。 本章我们将介绍模拟登录的相关内容。

↑0.↑

模拟登录的基本原理

很多情况下’-些网站的页面或资源需要先登录才能看到°例如GltHub的个人设置页面,如果 不登录就无法查看; l2306网站的提交订单页面,如果不登录就无法提交订单;在微博上写了-个新 内容,如果不登录也是无法发送的。

我们之前学习的案例都是爬取无须登录即可访问的网站,但是和上面例子类似的情况也非常多’ 那如果我们想用爬虫访问这些页面,例如用爬虫修改GitHub的个人设置,用爬虫提交购票订单,用

爬虫发微博,能做到吗?

答案是能,这时就需要用到一些模拟登录相关的技术°

↑.网站登录验证的实现 要实现模拟登录’首先得了解网站如何验证登录内容。

登录一般需要两个内容——用户名和密码’也有的网站是填写手机号获取验证码’或者微信扫码, 或者OAuth验证等,从根本上看’这些方式都是把—些可供认证的信息提交给服务器°



| 第l0章模拟登录

就拿用户名和密码来说’用户在_个网页表单里面输人这两个内容’然后在点击登录按钮的_瞬 间,测览器客户端会向服务器发送_个登录请求’这个请求里肯定包含刚输入的用户名和密码’这时

车了,这个票就是坐火车时的凭证°

那么问题来了,这个凭证是怎么生成的’服务器又是怎么校验的呢?答案其实在本章开头已经介 绍过了,-种是基于Sessjon和Cookie,-种是基于JWT. 2基于Sess|o∩和Coo恨|e

不同网站对于用户登录状态的实现可能是不同的,但Session和Cookje-定是相互配合工作的,



q

」■】】‖··|『‖日|·』■‖||」■■‖|‖■■』■■■司□■□」■〗■■■‖

服务器需要处理这些内容,然后返回给客户端一个类似凭证的东西’有了这个凭证,客户端再去访问 某些需要登录才能查看的页面时,服务器自然就会“放行”’并返回对应的内容或执行对应的操作° 形象点说’坐火车前,乘客要先用钱买票,有了票之后,让进站口查验_下,没问题就可以去候

■■●■|』‖|兰■■

374

0

下面梳理_下°

以上两种情况几乎能涵盖大部分这种模式的实现,具体的实现逻辑因服务器而异,但Session和 Cookie-定是要相互配合的°

| ] |

执行设置Cookje的操作’将那些类似凭证的信息保存到Cookie里,以后再访问网站时都携带 着Cookie,服务器拿着其中的信息进行校验’自然也能检测登录状态°

『当■■

□Cookje里可能只保存了SessionlD相关的信息,服务器能根据这个信息找到对应的Sessjon°当 用户登录后,服务器会在对应的Session里标记一个字段,代表用户已处于登录状态或者其他 (如角色`登录时间)。这样一来,用户每次访问网站的时候都带着Cookle’服务器每次都找到 对应的Session,然后看_下用户的状态是否为登录状态’再决定返回什么结果或执行什么操作° □Cookje里直接保存了某些凭证信息。例如用户发起登录请求,服务器校验通过后,返回给客 户端的响应头里面可能带有5etˉ〔oo代1e字段’.里面就包含着类似凭证的信息’这样客户端会

3.基于」W丁

的校验又存在—定问题,例如服务器需要维护登录用户的Sessjon信息’而且分布式部署也不方便, 不太适合前后端分离的项目,所以JWT技术应运而生° JWT的英文全称为JSONWeb∏ken’是为了在网络应用环境中传递声明而执行的一种基于JSON

的开放标准,实际上就是在每次登录时都通过一个TDken字段校验登录状态°JWT的声明一般用来在 身份提供者和服务提供者之间传递要认证的用户身份信息,以便从资源服务器获取资源,此外可以增

加-些业务逻辑必需的声明信息,总之TDken可以直接用于认证,也可以传递_些额外信息。



‖■□■Ⅵ|‖‖‖‖{』··]‖|」』■■】|‖■■】|||

Web开发技术—直在发展’近几年前后端分离的开发模式越来越火,传统的基于Session和Cookie

0

‖(

有了JWT,~些认证就不需要借助于Sessjon和Cookjc了’服务器也无须维护Sessjon信息’从 而减少了开销,只需要有_个校验JWT的功能就够了’同时还支持分布式部署和跨语言开发。

JWT-般是_个经过Base64编码技术加密的字符串’有自己的标准,格式类似下面这样:

其中有两个起分隔作用的“.”,因此可以把JWT看成_个三段式的加密字符串°这三部分分别

■‖β

ey〕0eX∧xIjoj"∏z‖〔I5I‖「sZzIjOi]hZC1pbiI5I∩R5〔〔I6I代pXγ〔I5I"「5ZyI6I代hⅧj02I∩o.ey]γ〔2γy5山10j[州ymγX‖1〔促5hb 刚iOi〕hZC1PbiI5I|W4〔〔I6盯01问jI4‖jco‖i“‖z〔0Ⅷ[▲田·p[gd∏「∧y73wa1「o∩["2zbxg』60th3d1∏o2‖【9iγzX38



是‖eader、 pay1oad和51g∩ature° □‖eader:声明了JWT的签名算法(如RSA、SHA256等)’还可能包含JWT编号或类型等数据°

□pay1oad:通常是_些业务需要但不敏感的信息(如UserID),另外还有很多默认字段’如JWT 签发者` JWT接受者、JWT过期时间等°



} ■■尸‖‖}■【匣‖■「‖■■‖囚●∏|||●「

β} △■「□‖■■■■「β伊





l0.l

模拟登录的基本原理

375

□51g∩ature:这就是一个签名’是利用密钥5ecIet对‖eader` Pay1oad的信息进行加密后形成 的’这个密钥保存在服务端,不会轻易泄露°如此_来,如果paγ1oad的信息被篡改,服务器 就能通过51g∩ature判断出这是非法请求,拒绝提供服务° 登录认证流程也很简单了,用户通过用户名和密码登录’然后服务器生成JWT字符串返回给客

户端’之后客户端每次请求都带着这个JWT’服务器会自动判断其有效情况,如果有效就返回对应的

数据°JWT的传递方式多种多样,可以放在请求头中,也可以放在URL里,甚至有的网站把它放在 Cookje里,但总而言之’把它传给服务器进行校验就可以了。

好’到此为止,我们就了解了网站登录验证的具体实现。

4.模拟登录



经过前面几节的学习,想必大家已经有模拟登录的思路了。下面我们同样基于两种模式来实现°



●基于Session和Cookie模拟登录

巴■△■「△伊}■丁『「|△■‖巴■=【■∏△■【二≥■】|■△■■『『|·『【=厂■『|[■『=■厂

如果要用爬虫实现基于Session和Cookie的模拟登录,最主要的是要维护好Cookje的信息,因

为爬虫相当于客户端的测览器’我们把测览器做的事情模拟好就行°接下来结合本书之前所讲的技术 总结—下如何用爬虫模拟登录。

□第_’如果已经在测览器中登录了自己的账号,那么可以直接把Cookjc复制给爬虫°这是最

省时省力的方式’相当于手动在测览器中登录°我们把Cookje放到爬虫代码里,爬虫每次请 求的时候都将其放到请求头中,可以说完全模拟了测览器的操作。之后服务器的动作和前面一 样’通过Cookje校验登录状态,如果校验没问题,就执行某些操作或返回某些内容。 □第二’如果想让爬虫完全自动化操作’那么可以直接使用爬虫模拟登录过程。大多数时候,登

卜【■∩·■尸||■■「[|■凸「|■『‖|‖■=■『‖‖》↓||

录过程其实就是一个POST请求。用爬虫把用户名、密码等信息提交给服务器’服务器返回的 响应头里面可能会有5etˉ〔OO促1e字段’我们只需要把这个字段里的内容保存下来就行了°所

墟嚣嚣麓孵灌墨默繁憋蹦嚣嘉麓盛瞧蕊田

言实现的,所以可能还得仔细分析其中的逻辑’尤其是用requests这样的请求库进行模拟登录 时,遇到的问题总是会比较多°

□第三,可以用-些简单的方式模拟登录,即实现登录过程的自动化°例如用Selenium、Pyppeteer

或Playw前ght驱动测览器模拟执行_些操作(如填写用户名和密码`提交表单等)°登录成功 后’通过Selenium或Pyppeteer获取当前测览器的Cookle并保存°同样之后就可以拿着Cookie 的内容发起请求’实现模拟登录。

以上介绍的就是一些常用的利用爬虫模拟登录的方案’核心是维护好客户端的Cookie信息°总 之’每次请求时都携带Cookie信息就能实现模拟登录了° ●基于JWT模拟登录

基于JWT的模拟登录思路也比较清晰’由于JWT的字符串就是用户访问的凭证,所以模拟登录 只需要做到下面几步°

(l)模拟登录操作°例如拿着用户名和密码信息请求登录接口’获取服务器返回的结果’这个结 果中通常包含JWT信息,将其保存下来即可。

(2)之后发送给服务器的请求均携带JWT°在JWT不过期的情况下’通常能正常访问和执行操作° 携带方式多种多样’因网站而异。

(3)如果JWT过期了,可能需要再次做第一步,重新获取JWT。 当然,模拟登录的过程肯定会带—些其他加密参数,需要根据实际情况具体分析。

第l0章模拟登录

5.账号池

如果爬虫要求爬取的数据量比较大或爬取速度比较快’网站又有单账号并发限制或者访问状态检 测等反爬虫手段’我们的账号可能就无法访问网站或者面临封号的风险° 这时一般怎么处理呢?可以分流,建立一个账号池,用多个账号随机访问网站或爬取数据,这样 能大幅提高爬虫的并发量’降低被封号的风险°例如准备l00个账号,然后这100个账号都模拟登录’

||||

376

并保存对应的Cookje或JWT,每次都随机从中选取-个来访问,账号多’所以每个账号被选取的概

尸■■■■

率就小’也就避免了单账号并发量过大的问题,从而降低封号风险。 6ˉ总结

本节中我们首先了解了基于Session和Cookie,以及基于JWT模拟登录的原理,接着初步了解了 两种方式的实现思路,最后初步介绍了—下账号池。

·‖·■

后面我们会通过几个实战案例实现上述两种模拟登录’为了更好地理解实战内容’建议好好学习 本节的知识°



〈 {

↑0.2基干Sess|o∩和Coo低|e的模拟登录爬取实战 本节我们通过实例讲解基于Session和Cookje模拟登录并爬取数据的流程° ↑.准备工作

□安装好requests库并掌握其基本用法’具体可以参考本书2.2节° □安装好Selenlum库并掌握其基本用法’具体可以参考本书7.l节° 2案例介绍

这里用到的案例网站是https://login2.scmpe.cente∏,访问这个网站,会打开-个登录页面’如图l0ˉl 所示。 _

肥.[口……∏二≡二

T←7…-=-

·

唾宁≡霉罢寥乏T≡霉二二.愚_宁γ~-==??霖瞬≡≡宁宁T?]苏爵芭~『 回5°■p。 ■

+◆◎|●…→……呼

■-ˉ-

譬 ■-=-==-……一…=▲= -一■—=涸~…ˉ≡-__-■

ˉ=一≡….~—————



||‖



』 刻

§

R矗

卜’

『 ″■

‖ 』 ‖

旷 m(



□■

『 0

-=一_■=干--—-声



~==-ˉ

1 图10ˉl

—奇~-

案例网站的登录页面

输人用户名和密码(都是admin),然后点击登录按钮°登录成功后,我们便可以看到_个熟悉的 页面’如图I0ˉ2所示。

‖‖」□`』·‖■Ⅷ‖|』‖』Ⅵ||·』■■|』■‖||』■‖』‖」■」』■■■‖||■】』‖‖』●■〗〗‖】■Ⅵ||■]|」‖`|』·|』Ⅲ可|』』■■■』‖‖■■□□‖□〗■】□〗]|·【』‖」■■■■〗■·■Ⅲ■∩□■】叫〗』可□|司」曰」■

需要先做好如下准备工作°





■ ■ 『 | ■ ■ 「



l0.2基于Session和Cookje的模拟登录爬取实战 丁

377

≡=

·◇·『■…|… 十◆O







键…

■垮钓≡…

同sc「…



冉司●

◆仓

刁=面

= =



)‖

■‖卜



■王别姬ˉ「a『●w●‖肌yC◎∏仁ub面∏● ●●

獭;

95 仑●●●★

中■硒m■■■Jw↑甘适 0…w器上矽

攀螺题撼

·

.→

_=

巳■叼卜

■■

这个赖手不太冷ˉLd◎『0

95

●●■●■

) 厂■[尸■■尸卜膛■■



★●台●亡

■■.γ旭滞 ↑锚O佛?d上■

‖■■ 厂田

_

J

肖申克的戴思.沛●5haw■h■∏kR●d●mpt‖◎∩



L`…●●

95

.,·、Ⅺ|

m′!■津

唾.

0锚够渺k蚀

0

. ^ .′

图l0ˉ2登录成功后的页面

■『‖匹■卜尸|β卜▲■「

这个网站是基于传统的MVC模式开发的’因此比较适合基于Session加Cookie的模式模拟登录° 3.模拟登录

对于这个网站’如果要模拟登录’需要先分析登录过程中发生了什么°打开开发者-〔具,重新执



粉瓣粉

■Ⅸ…0…■

潍:





+◆宿 ·…a…=

■贪

■部●『

←守

Ⅵ犯啦Ⅺ0…

回sc『…



儡秤「儒…|…… 伏王γ』.确

9°5

中■文●中■■■.`,`汾仲

.

儡趟=…=…~…~~= _





匹艺

| `

●古●●●



●e可α■~旬°〕磋…≈=· 虫 1

『≈Ⅸ



曰α

卒 ! 泛 o

.亏=■…●mnm呵…≈≈鸿山……言≈=…◎…~

酶■蛔≈…≈■≈…■

0m■

■~元…宅…“…司回■…→≡弓≡■…≈…■■…≈≡电…记

■=≡邑……… ■惩

≈Ⅱ

马■

》‖■『■■∏■【■■厂》▲■∏|△■■■『■■厂『■■尸■尸「■尸■厂|■■巴■『▲β■『

行登录操作,查看登录过程中产生的请求’如图l0ˉ3所示°

0

■】■‖□β亿卜■■「■■β

图l0ˉ3登录过程中产生的请求

从图l0ˉ3中可以看到’在登录的瞬间’测览器发起了一个POST请求’目标URL是https://login2.



SCrapeCenteⅣ‖ogin,并通过表单提交的方式向服务器提交了登录数据,其中包括u5er∩己刚e和Pa55WOrd

坠■∏巴■广「|》}

卜β

两个字段’返回的状态码是3o2’Re5po∩5e‖eader5的1ocat1o∩字段为根页面,同时Re5po∩se‖eader5 还包含5etˉ〔oo促ie字段’其中设置了5eSsio∩1d°

由此我们可以想到,要实现模拟登录,只需要模拟这个POST请求就好了°那我们用代码实现-

每次发出的请求默认都是独立且互不干扰的’例如第_次调用pO5t方法模拟登录了网站,紧接 次请求,因此常规的)帧序调用无法达到模拟登录的效果。下面用代码实现这个例子: jⅦportreque5t5 十ro『∏ur11jb.p3r5emportuI1joi∩

B∧5[UR[= ‖http5://1ogi∩2°5crape.ce∩ter/0 lmI‖UR1≡uI1joj∩(B∧5[URl’ 0/1ogi∩!)



p∧55肌R0= 0adm∩‖

re5po∩5e≡1ogj∩=reque5t5.po5t([mI‖0R[’ data≡{ u5er∩己『∏e『 : 05[Ⅷ刚[’

q

』■■γ‖』□∏□』■可

05[R‖酬[= !己dm∏0

{」{

着调用get方法请求了主页面。这两个请求是完全独立的,第_次请求获取的Cookie并不能传给第二

‖ 〈 (

下吧!

■】·】』■]■■|当□□可(‖|‖

第l0章模拟登录

378

pa55word|: p∧5S肋日D

})



re5po∩5ei∩dex=Ieque5t5。get(I‖0[X0R[) pr1∏t〈|【e5po∩5eStatu50 ’ re5po∩5e-j∩dex.statu5〔ode) prj∩t(0Re5po∩5e0肌‖’ re5po∩5e-i∩dex.ur1)

运行_下代码,结果如下: 佣e5PO∩5e5tatu5200

Re5po∩5e0肌bttp5://1ogi∩2·5〔rape。Ce∩ter/1ogj∩?∩ext=/Page/1

可以看到,最后打印的页面URL是登录页面的URL°这里也可以通过re5po∩5e-1∩dex的text属 性来看_下页面源码’这里就是登录页面的源码内容’由于内容较多’这里就不再输出对比了。

总之,这个现象说明我们并没有成功完成模拟登录,这就印证了按序调用requests的po5t`8et方 法是发出了两个请求,两次对应的Sessjo∏不是同_个,这里我们只是模拟了第_个Scssjon’并不影 响第二个Session的状态’因此模拟登录也就无效了°

那怎样才能实现正确的模拟登录呢?Session和Cookje的用法我们在本章开头就介绍了,模拟登

录的关键在于两次发出的请求的Cookie相同°因此这里可以把第_次模拟登录后的Cookjc保存下来’

‖‖‖‖ ‖{{』】■‖∏』□√司■可·』□■‖‖

以自动处理重定向’所以在最后把响应的URL打印出来’如果结果是I‖0[x0R[’就证明模拟登录成 功并成功获取了网站首页的内容’如果结果是[卯I‖0R[,就说明跳回了登录页面,模拟登录失败°

‖ 已

这里我们先定义了3个URL`用户名和密码,然后调用Ⅱ℃quests库的po5t方法请求了登录页面进行 模拟登录’紧接着调用get方法请求网站首页来获取页面内容’它能正常获取数据吗?由于requests可

在第二次请求的时候加上这个Cookie,代码改写如下: i∏POrtreqUe5t5

+ro∏uI111b.paI5ej刚portur1joi∩



05[R‖酬[= 『adⅦ1∩0 p∧55舶R0= 『adm∩0

u5er∩M记‖: 05[R‖州[’ Pa55wOm0 : p∧S5舶RD

|{」(‖

Ie5po∩se_1ogj∩=request5.po5t(‖ˉ卯I‖0肌’ data={

■■■■‖』□』■‖〗』‖

B∧5[0肌≡ 0http5://1ogi∩2.5cIape。〔e∩ter/0 [“I‖‖【[ 叁ur1joj∩(8∧5[0R[’ 0/1ogi∩』)

q

司|

『■■尸「|)■▲■∏|′ ˉ■■卜}

■尸「‖■「》卜|巴■■厂‖●『囚■尸匹尸



k

l02基于Session和Cookie的模拟登录爬取实战

379

}’ a11o训Iedire〔t5=「a15e)

coo促1es≡Ie5po∩5e-1ogj∩°cooMes pIi∩t(0〔oO灶e5』’〔OOkie5)

re5Po∩5e-i∩dex=reque5t5.get(I‖D[X0R[’ cooMe5=cookie5) PIi∩t(0Re5Po∩5eStatu50 」 re5po∩5e-i∩de×。statu5〔ode) pri∩t(!佣espo∩se0R[′’ respo∩5e-j∩dex.uI1)

仕■尸卜β‖『◆【■厂

由于I℃quests具有自动处理重定向的能力,所以在模拟登录的过程中要加上a11咖Iedjre〔t5参数并 将值设置为「a15e’使IcqUests不自动处理重定向°这里将登录之后服务器返回的响应内容赋值为 resPo∩5e一1o8i∩变量,然后调用re5po∩5e-1og1∩的cookies属性就可以获取了网站的Cookje信息°由于

Iequests自动帮我们解析了响应头中的5et_〔oo恨je字段并设置了Cookje,因此不需要我们再去手动解析°

接着,调用requests的get方法请求网站的首页°和之前不同’这里的get方法增加了一个参数 〔oo代1e5’传入的值是第—次模拟登录后获取的Cookie’这样第二次请求就携带上了第_次模拟登录

获取的Cookie信息’之后网站会根据里面的SessionID信息找到同-个Session,并校验出当前发出请 求的用户已经处于登录状态’然后返回正确的结果°

D

‖『卜β■‖》‖■■「口卜‖‖「■》『止尸

最后我们还是输出最终的URL,如果结果是I‖0[x0R[’就代表模拟登录成功并获取了有效数据’ 否则代表模拟登录失败°

运行结果如下:

〔oo长jes〈Req0e5t5〔ookie〕ar[〈〔oo代ie5e551o∩jd=p5∩u8ij69+01te〔d3wa5〔cγ∑〔6ud41t〔+or1ogj∩2.5〔mpe.〔e∏ter/〉]〉 Re5pO∩5e5tatu52OO

Re5po∩se0〖[∩ttps://1ogj∩2。5〔mpe。ce∏ter/page/1

返回的是I‖0[X0R[,这下没有问题了,模拟登录成功!此时还可以进_步输出re5po∩5e-j∏dex的 text属性,看—下数据是否获取成功°

但其实可以发现’这种实现方式比较烦琐,每次请求都需要处理并传递_次Cookje’有没有更简



〗『■■■厂■■∏『■■■尸}■■

便的方法呢?

有的’可以直接借助requests内置的5essjo∩对象帮我们自动处理Cookje,使用5e551o∩对象之



后’requests会自动保存每次请求后设置的Cookie’并在下次请求时携带上它’这样就变方便了°把 刚才的代码简化_下: mportrequest5

+ro‖uI111b.paI5e1‖portur1joi∩

■ 『



B∧5[0Rl≡ 0http5目//1ogi∩2·scrape·ce∩ter/‖

l卯I‖0Rl二u工1joi∩(B∧S[0【[’ !/1o81∩0) I‖D[X0RL叁ur1joi∩(B∧5[0R[」 ,/Page/1』)

‖ |

05[删AN[= |3dⅧi∩0 p∧55‖0R0= |adm∩0

■ 尸

5e55jO∩=reqUe5t5.5e55iO∩()



Ie5po∩5e一1ogi∩≡se5sjo∩.po5t(k卯I‖-0R[’ data≡{ !u5er∩a∏e0 : 05[R‖州〔’ 0pa55"ord! : p∧S5ⅧRD

「 ●■■尸|》『■卜巴■■■『巴尸





}) COO促ie5=5e5S1O∩.COO长1e5

pri∩t(|〔OO灶e50 ’ 〔OO|〈je5)

re5po∩5e1∩dex=5es5io∩·get(I‖D[X0RL)

pr1∩t(|Re5po∩5e5tatu5|’ re5po∩5ei∩dex.日t日t0scode) pI1∩t(|Re5po∩5e[」∩[0 ’ re5po∩5e1∩dex穆ur1)

可以看到’这里声明了—个5e55jo∩对象,然后每次发出请求的时候都直接调用5e55jo∩对象的

■=尸■厂}|【◆■「|卜『|匹■弓●■



第l0章模拟登录

po5t方法或get方法就好了’使我们无须再关心Cookie的处理和传递问题° 运行结果如下;

〔ook1e5〈Reql」e5t5〔oo代ie〕ar[〈〔oo|〈ie5e55jo∩id=55∩g代14i7e∩9γⅦ73bb36hxi十O5|〈1O|(13「or1ogj∩2.5〔rape.〔e∩ter/〉]〉 Re5po∩5e5tatu520O Re5po∩5e0【[∩ttp5://1ogi∩2·5cI己Pe·ce∩ter/page/1

和刚才的结果完全_样°因此建议大家使用5e55jo∩对象进行请求,这样实现起来会更加方便°

这个案例整体来讲其实比较简单’如果碰上复杂—点的网站,例如带有验证码、带有加密参数的

网站,直接用requests并不能很好地处理模拟登录’那登录不了,整个页面不就没法爬取了吗?有没 有其他方式来解决这个问题呢?当然有’例如可以使用Selenium模拟测览器的操作’进而实现模拟登

录,然后获取登录成功后的Cookie,再把获取的Cookie交由requests等爬取。 还是同样的页面,由Selenium实现模拟登录’后续的爬取则交给requests,相关代码如下:

白‖■

8A5[0Rl= 0‖ttp5://1ogj∩2.5〔rap巳〔e∩ter/’ 儿“I‖0R[=ur1joj∩(BA5[0RL’ !/1ogi∩!) I‖D[X0R1=uI1joi∩(8∧5[0Rlˉ’ ‖/page/1|)

q

日(‖

{ro‖uI111b.parSej∏portur1joj∩ 「Io腑5e1e∏1u"mportwebdriγer j‖portreque5ts j川pOrtt1Ⅷe

】‖·■■勺‖‖=■■|■□=□〗‖√‖|』勺‖』{■■』〗」□■日(』习(日纠《

380

05[R‖∧州[= 0adm∩‖

p∧55‖OR0= 0日d∏i∩|

tme.51eep(10)

‖□可‖司□□‖·□□‖】』·

bro"5eI="ebdriγeI.〔∩ro们e()

brow5eI.get(8∧5[0R[) brow5er·千i∩de1e爬∩tˉby-c55-5e1ector(′i∩pot[∩a们e="u5er∩a爪e,"]0).5e∩d-惯ey5(‖5[R‖酗[) bIo"5eI.「i∩de1e|∏e∩tˉby-c55-5e1ector(‖i∩put[∩a刚e≡"p己55word0|]0)·5e∩d_促eys(p∧5S肌RD) bro"5er.于1∩de1e∏e∩t_byˉ〔55-5e1ector(‖i∩put[type=‖』5ubmt"]‖).c1ic代() #从测览器对象中获取〔ooRie仿′鸟

#把〔OO促je信』憋枚入请求中 5e551o∩=reql」e5t5.5e551o∩() 于or〔ooMej∩〔oo代1es;

5e551o∏.〔oO代1e5.5et(〔OO促je[0∩a川e‖]’ COO化ie[ 0γa1Ue|])

re5po∩5e1∩dex=5e55io∩。get(I‖0[X0R[) pr1∩t(′日e5po∩5e5tatu50 ’ re5po∩5ei∩dex.5tatu5code) pm∩t(‖Re5PO∩5e0R[0 ) re5PO∩5ei∩dex.ur1)

‖』■■可■■■』■■‖·|』』■∏』■

〔ook1e5=brow5er.getˉ〔ookje5() prj∩t(0(oo促je50 ’ cooRie5) bro"5er°〔1o5e()

这里我们先使用Selenium打开Chrome测览器,然后访问登录页面,模拟输人用户名和密码’并 d

点击登录按钮°测览器会提示登录成功,并跳转到主页面°

之后’我们声明了_个5e551o∩对象’赋值给5e551o∩变量,然后遍历了刚才获取的所有Cookie

‖{{

这时,调用getˉcoo代je5方法便能获取当前测览器的所有Cookie信息’这就是模拟登录成功之后 的Cookje’用它就能访问其他数据了°

信息’将每个Cookle信息依次设置到5e55jo∩的〔ookje5属性上’随后拿这个5e55jo∩请求网站首页’ 就能够获取想要的信息了而不会跳转到登录页面° 运行结果如下:

日e5po∩5e5tatl」52O0

Re5po∩5e0【[‖ttp5://1ogi∩2°5〔r日Pe.ce∩ter/P3ge/1

乙■□·■』■|』·Ⅵ□■■□■

〔oo{〈1e5 [{‖do帕j∩! : ‖1ogi∩2.5crape.〔e∩ter0 ’ ‖expjry』: 1589O43753.553155′ ,∩ttp0∩1γ|: 丁rue’ 0∩aⅦe′: ′5e55io∩id|’ 0pat‖! : 0/! ’ 』5a川e51te0 『 ‖[ax,’ 05e〔ure! :「a1se’!va1ue0 : ‖rd3g7ttjqhγaz日γpx〕z31y0t∩ze81zur!}]



·‖

| l03基于JWT的模拟登录爬取实战

38l

可以看到,这里的模拟登录和获取Cookle信息后的爬取都成功了。因此当碰到难以模拟登录的 情况时’可以使用Selenium等模拟测览器的操作方式’使用它获取模拟登录后的Cookie’再用这个 Cookje爬取其他页面就好了°

这里也再_次巩固了对前面结论的认识’即对于基于Session和Cookje验证的网站,模拟登录酸

关键是获取Cookie°可以把这个Cookje保存下来或传递给其他程序继续使用,甚至可以持久化存健 或传输给其他终端使用°另外’为了提高Cookie的利用率和降低封号风险’可以搭建_个账号池实 现Cookie的随机取用°

4.总结

本节我们通过—个实例来演示了基于Session和Cookje模拟登录并爬取数据的过程’以后遇到这 种情形的时候可以用类似的思路解决°

本节代码见https:〃github.com/Python3WebSpjder/ScmpeLogin2°

↑0.3基干」W丁的模拟登录爬取实战 本节中我们通过实例讲解基于JWT模拟登录并爬取数据的流程° ↑.准备工作

请确保已经了解了JWT相关的知识’可以回顾l0.1节。另外还需要安装好requests库并了解其 基本的使用方法’可以回顾22节°

2.案例介绍

这里用到的案例网站是https:/川ogin3.scrape.center/’访问这个网站,同样会打开_个登录页面’ 如图l04所示。 巴

■=正厂

厦|个

●| ■N片

◆◆○ ℃■

仓■■●l



回s·四p·



■■ …一-~

=℃

″田 争啡←■一■■e-Q=■≈勾≈●≈■≈

酗(…乏二ˉ ˉ二



图l0斗案例网站的登录页面

用户名和密码依然都是admm’输入后点击登录按钮会跳转到首页’证明登录成功。 3.模拟登录

基于JWT的网站通常采用的是前后端分离式’前后端的数据传输依赖于Ajax’登录验证依赖于 JWT这个本身就是token的值’如果JWT经验证是有效的,服务器就会返回相应的数据。



第l0章模拟登录

382

和上-节—样,下面先打开开发者r具’重新执行登录操作’查看一下登求过程中产生的请求’ 如图l0ˉ5所不。

G

x



弓罐

厅■

●出■■k…‖… ◆

@

鳃俞冉鄙●】

●……一

| 句m■磁°需° 〔H『■恤…冗



圃年离独 『■仕违匹!望■

{■m■

甲…叮【.=

m年全的台■■

dYP■ ‖m■■芹■乓

■■D■它甲■

…■

扛●







完●~≡==~=~←= —

●●Tα■…■蜜…=≈…甘



Q

。…≡…●…缚≈m……≈鸿≈≈…唾■=…◎……

敏■

!≈≈…≈…■■…丙…≈…≈萨=〕≈≈…≈

二 亏r■

口≡=江 $…≡b■ ■… 和

h 二=宁

?=℃0…≈Y…扁



■■



p加■.钩







0……~………

■=……它=些苟庐…

∏=……蹿.… 且空…=一≡._ ■黑嚣呈…|(~-ˉ ■空m●1= -…ˉ~ 当赣呈吟…=…≈ 圃慧蹿=≈………

衡~一→●■α

~~渔 r.河 DmQ郸

……=…≈『 T-≡=…

=…·面…

-…1』■ ≡羽』 ≡哇=1■T≡qr 尸j≡ ……·凶≈l…r0n8■ym

…-■■斜…8…【=… …锣…

_~Ⅶ

~■

~凶

`三‖孕例

巨霹乙写=≡‘…… ■嚣墅= - ˉ~铺闹≡

_…回■ ◆…=向

古≡m图 ˉ当

=O



■ · ∏





儡锄一…” 图l0ˉ5登录过程中产生的请求

从图l0ˉ5可以看出,登录时的请求URL为hnps://login3.scmpe.center/apγlogin’是通过A)ax请求 的°请求体是JSON格式的数据’而不是表单数据’返回状态码为200°然后看_下返回结果是怎样 的’如图l0ˉ6所示° 口●·帕

Ⅺ咆γ

中… 〗 ●幢 四 …









……

……

●硒

……

…=…

………

≡……—…

=……雨…

_一一



→……

……≈…■ …

…芯睁……赋■…

■忘血患……乍=.0『≡

■豁』-一’. .: _:— 且翻;犁懂圃……,霹腐‘ 图l0ˉ6返回结果

从阁l0ˉ6可以看出,返回结果也是JSON格式的数据’包含一个to旧e∩字段’其内容为: ey〕oeXAi0j〕Rγ10il〔〕hbCcj0j]IUzI1‖i〕9.ey]1c2VγⅫ1促Ijox{○1〔2γy咖「tZ5I6I川「kb‖1uI1MZXh"Ijox‖jI10丁g1础A3[〔〕1b"「 pb〔I6I川「kb‖1u“胀b‖1u[爪‖γb5IsI"9ya‖d十a肝OIjox‖jI10丁Qx"∧3+O.γz0州γhyˉC"c|"o∩十Ⅻ丁〕Mq∩8b]o6he∩751b82d50j8

这和我们在l0.l节讲的_样,由‘』.”把整个字符串分为三段°那么有了这个』WT之后’怎么获 取后续的数据呢?我们翻_下页’观察下后续的请求内容,如图l0ˉ7所示。

l03基于JWT的模拟登录爬取实战 』

■产

睹…"





■兑













■●

飞呻

…Ⅷ 言■●宜Ⅲ干晶

…m…

呻夕伯

■n

■■

控■巴

哩●些邹

四■■巧而



Ⅷ座■■臼■宁

曰…· ;`■’△ =_—…

。 · ˉ

O吨 ●

§

N



~口

~…邹

—≡_…_

……≡_……

●■=

~一…≈

…_≡…●^

…………ˉ咎



…~°口…口

=……■





她★▲财●

【忘司熙U|碑亭窗戳墅f』o。:忠蹿…龋』叮严三ˉ .厕≡

………



● _…









户‖■厂〖■厂}‖■ˉ■■「卜【匹■『』『巴■■β『|■∏「|■■「|■β『}》■■‖|■■厂



● ● ●

383

叼≈

和≈

≡-_一

■≡=…=V…



==

≡≡≡

—≡…=

二=



p琶~棒■…●

国=

=■【皿■『…〗心0 0●■q归□■』■·●P■

R睦…=….坤^

—仗酗□●O‖早●p伊

`…0■■ ■■≈d □ ˉ-■.四0 ■p~…加-

鹏雾哼==→ˉ羔一 巳军电′..,症→、围… ■二尝…~、=…综 ■;鳃二…….…二?T唾■ˉ一厂≤、忌`ˉⅢ

…■●8D巳…V垮■■g_==·■四=∏p龄m…eQ=辱】■●『=啤≈· …lm■0·■P=二→

-■≈T≈肮…8…·■m■….5■奄矿…‖■0牢…□5尸□■ —啪

≈~… —可和

~…h■m ≡■1u■3°■≈酞≡·mO●‖≈田∏出=u贝〗峭■…M户?B〗·归冈…o涅m鲍■■〗【伪■≈J切·●.▲■刃m铀7凸7】j9 D■■

■蹿贺.萨≡.……

■……→…◇p尸◆

】=v呻■

二…p巍■·雨

≡u ~〗●

w≡尸■四一0e空←…

图l0ˉ7翻贞后的请求内容

从图l07可以看出,在后续发出的用于获取数据的Ajax请求中’请求头里多了一个∧ut∩or1zat1o∩字

段’其内容为jwt加上刚才图l06中to长e∩字段的内容’返回结果也是JSON格式的数据’如图l0ˉ8所示◎ ●●U】 ●■α■悼=,≈





@



-



0



藩T辈严〃 |亏=一~』■≡

|m牵β霹谱鹅

…~皇0罕女贝下葛

m…

■出

■戊

‖■■■■攫蜘

m

·…修 ′ `■〗 ‘

啤回逐 ˉ

赎■0■■臼■斗

, · =咽Ⅷ ● 巾

=唾

…■

…叼

=←…

—_…

…………

~≡…≈



∑■

=—≡…■



汀Q

p勺

≡………

a.§ ●●

■n= ˉ



■~~……啃 _

=≡怕

●q尸==8产ˉ p 已≡吕…

岂弓

·■…;〖●目M四R…■≡0古□…O

—吨

·…■■q■P■赋】净=·■=】

■骂淫…一.`_~

…m.唾…″啤铸扛·』■、.■.…日p.T 『扩…∏ . D’GPq″·回

哗=

薛·辟

■P0■■

■由喻■ ˉ弘铲

■—

贮9炔贤已…~……″ H.!瞩安ˉ…一… R零霉斜……=ˉ…. R9琴`!q固一 ■嘿蹿旦…=.鳃ˉ婶 ■浊T…ˉ ˉ :=≡ ∑…Ⅶ,酮 凶■二



!■■【■》■需…丁■·

··弓《四Ⅱ■≡U…古pm≈■8 0■Pl■』●…·■■私■‖◇■】

≤=

^. =

■●凹~0p蹿-=

■;‖《袒8…Q彰0■…0■0

O…0O°沪钞Ⅷ口和^…,饵■枷po 曰√…v…贸■‘mr旷↓ 牢m↑如力々懒砷〃…勺…出·它妒口D≈…咎◆tf椰…`炉螺仆鳃励心′庐、绸 凹片 乓…p赌

=$2… 0GB■●l

◆庐

■『目 Ⅱ■自■■■…p ■■■■日 ■团p■0

●…℃r■8 〗■■畔宜■·°曰■■●驴p…叮°■…F心■■w■V …「·≈■GmD″四L尸-一p·■■叮·甲甲▲℃7S已△河′…p…Q巩宁Pb 日·吁 【■· ■泌fR“卢

=吊■… …■号 =■≠

·0吝 《凹. ■…″·…8官口■=■o ;●■…■〗◆.0 ·0



-

■瀑拦呻皿

←回■



‖0

D☆白靶●

●…_

‖□■. ■■些巳O污·-0辛=昌≈■守p卓』

●l0 《■●U…□…合■』b■7■口…■.妒≡[α■p纂闽■●■■■m户 嚣●} ·GSt四吕和■…驴常0…●·m…O』!U诌西;◆管』

p》0 【Ⅱ■■……p…凸藩p■■9β l、畴′私呻■· 零…p…工备吩牟》刚山 ◇●8《帕8咽…喻=g-白伪…5护·蛔滁律0…■〗p丁j

p0°《皿令■…尸公…台■匣Q…D8PmlQ洱》 ·β·ˉ《l■?■…U=■四…0幽…弓卜酶】四`=●r;p口p

图l0ˉ8返回结果

硒≈

二■■■■Ⅶ■■■

第l0章模拟登录

384

拟登录的整个思路就变简单了’其实就是如下两个步骤: □模拟登录请求’带上必要的登录信息,获取返回的JWT;

□之后发送请求时,在请求头里面加上∧ut∩ori∑at1o∩字段,值就是JWT对应的内容°

|(

接下来我们用代码实现:

‖』β■□凸□∏

可以看出,返回结果正是网站首页显示的内容’这也是我们应该模拟爬取的内容°那么现在,模



mportIeque5t5 十roⅧl』r11jb.par5ei呻ortur1joi∩

8∧5[0【l= 0http5://1ogi∩3.5cmpe·ce∩ter/0 [卯I‖0R[≡ur1jo1∩(B∧5[-‖R[’|/api/1ogj∩!)

二 p∧55ⅧR0= 0adⅧi∩0

re5po∩5e-1ogi∩=Ieque5t5.Po5t(k"I‖0Rl’ j5o∩={ u5er∩a『爬‖ 8 05[R‖州[’



〗Ⅲ·Ⅵ‖‖·■■{‖■■可

05[R‖A‖[= 0adm∩0

0p己55word0 : p∧55灿【D

})

∩e己der5={

,∧uthori∑atjo∩|: 「,jwt{jwt}0



二■■■二■■·■■■■■■‖

data=re5po∩5e—1ogi∩.j5o∩() prj∩t(‖Re5po∩5e〕5O‖』’ dat己) jwt≡data。get(!toke∩|) pri∏t(‖〕‖丁0 ’ j毗)



re5po∩5e-i∩dex=reql」e5t5.get(I删[X0RL」 PamⅧ5={ 0O仟5et‖: O

‖(

|1jmt↑8 18’

}’ he己der5≡‖eader5)

pri∩t(0Re5po∩5e5tat‖s0 ’ Ie5po∩5ei∩dex.5t3tu5〔ode) prj∩t(0偶e5po∩5e0【儿! ’ re5po∩5ei∩dex.ur1) pm∩t(,伺e5po∩5e03t己! ′ re5po∩5ej∩dex°jso∩())

泣里我们同样先定义了登录接口和获取数据的接口,分别是[卯I‖0R[和I‖0[X0R[,接着调用

运行结果如下:







{‖id|: |302893160 ′ ‖∩a"e|: 0魏耳这样’还是甚欢你’篮原允生』’ !己‖t∩oI50 ; [ 0扫虫矗0]’ 0〔oγer : 0∩ttp5://i吧3.do0ba∩io.〔o们/γjew/5ubject/1/pub1ic/529875oo2.jpg’’ 05〔ore! :7.5』}]}

可以看到,这里成功输出了JWT的内容’同时获取了想要的数据,模拟登录成功!

■勺■■■■■】■■■■■‖日‖□|』■□可

Re5po∩5e]50‖{,to贺e∩|; !ey]oeⅫi01爪γ1Q1[〔〕‖bCciOj〕I02I1№〕9.ey〕1〔2γyX21代Ijoxl○1〔2γybⅧ「tZ5I6IⅧ「代州1uIj"i ZX∩wIjox‖「g3"c4‖z《x[〔〕1b‖「pb〔I6I刚「促b‖1uα「仪b‖1u[州vb5I5I昭ya‖d+a‖「OIjox‖『83咖1‖丁Rx+Q.10∩u3γMjaˉ8upb28 [8〔T0d5y‖[6jgp毗8poI〔pγ叫‖} 〕Ⅶey〕OeⅫ10i〕Ⅸγ1Qj[□∩bCc1Oi〕I0zI1‖1〕9。ey〕1〔2γyX21氏IjoxL□1c2γybⅦ「tZ5I6I"「促b"1uIjwjZX‖wIjoxN「g3"〔4‖zkx [□1附「pbα6I|∏「代州1呕「促州1u[∏‖γb5I5I叼y日嗣千日‖「OIjoxⅦg3酬1盯灶句.j0∩u3γ∩d1aˉ8upb281gm」d5y肌6jgp冰8pompⅥ叫 Re5PO∩5eSt己tu52OO Re5po∩5e0R[http5://1og1∩3。5crape.〔e∩ter/己pj/boo低/?1加it=18&o仟5et=o Re5po∩5e0ata{‘〔ou∩t‖: 92oo’ ‖【e5u1t5, : [{0id0 : ‖27135877』」 0∩aⅧe, : |校团市场:布局术米浦贷群’决战平轻人 市场‖’ ‖aut‖or5, : [0单兴华0 ’ ‖牛坪]’ 0〔over! : |http5://i吧9.douba∩io.〔o∏‖/γie"/5ubje〔t/1/pub1j〔/s29539805. jpg|’ ‖5〔ore! : !5。5|}’

』■』日{■■司』■■‖■■

递。接着获取并打印出了返回结果中包含的JWT°之后构造请求头,设置∧uthor1zatio门字段并传人 刚获取的JWT’这样就能成功获取数据了°



(‖

requests的po5t方法进行了模拟登录。由于这里提交的数据是JSON格式,所以使用j5o∩参数来传



4总结

(■』

本节我们通过_个实例成功实现了基于JWT模拟登录以及爬取数据的流程’以后如果遇到基于 JWT认证的网站,也可以通过类似的方式实现模拟登录。

‖(

|| ■尸||『止尸|■巴■|∩′■尸

l0.4大规模账号池的搭建

385

↑0.4大规模账号池的搭建 我们在10.l节已经提过账号池,要想降低账号被封的风险’同时还能实现大规模爬取’自然而然 想到的方法就是分流。在现在的场景中,分流是指将请求分摊到不同的账号上。我们利用分流可以达 成下面两个目标。

个账号访问网站的频率就降低,被封禁的概率也越低°

□如果单位时间内单个账号的请求量_定,同样是每次随机选取-个账号请求,那么账号越多, 单位时间内的总请求量就越大。

所以,利用分流的思想,可以在保证爬取规模的情况下降低单个账号被封的概率°如何实现这个

过程?如何维护多个账号的登录信息?这时就要用到账号池了°接下来看看账号池的搭建方法° ‖。案例介绍

我们本节所用的案例网站是https://antispjder6.scrape.center/,访问该网站,会自动打开登录页面, 如图10ˉ9所示° ●●● }围…0… +◆G



邑_—



止=





h

………4… ●





力旬●!

●■

回5°圃p· 司

旦录

…〔二;:其二r二:∑门 ≡≈宁●→■亡■■●●●●■

●→●◆■←●企仑●◇■◆■ ■▲=哈℃→



■■[ 、■■p■凸■●■■==守=■^→→≈■

==■■■=■■

■■■

●■■=

没∏B号?庄田



瓣^←



。§

|丛『『』【■厂|}■卜)|■■尸「}■尸||卜■「[■■「‖仿|「「》仿「巴》■厂》户||β「匹■「卜|仿『‖‖‖户}■匹『『【『【广日『·■尸||■■■)|匹『【‖『止■=‖|·‖卜『「}八『

□如果单位时间内所有账号的总请求量_定,每次都随机选取-个账号请求,那么账号越多’单



j

图10ˉ9案例网站的登录页面

用户名和密码还是填入admin’登录后的页面如图l0ˉl0所示。

此时如果多次在登录状态下刷新页面’刷新几次后就会发现’页面不再返回任何信息,只显示“斗03 Forbidden”’如图l0ˉll所示°

此页面对应的状态码是403,代表当前账号已经被封,无法获取有效内容°过一段时间后’这个 账号又可以正常访问该网站’但如果像之前一样多次刷新,会再次被封°虽然这个账号被封’但是新 开一个独立的窗口’使用另_个账号(如用户名和密码都为admin2的账户)登录,还是可以正常地 访问页面,如图l0ˉl2所示°



第l0章模拟登录

386 厂

@

‖引



×



G



●●●回≈■●』…吁 令

岭★

●m…·工…ˉ…『

■蛋J●§

≡—=--

同5c『… ■王别姬ˉP己『●we‖‖朋yC◎∏Cub帅●

●■

翁喇{噬 娜怨

·」』】■】‖‖‖」ˉ·|■∏』司||‖』‖|」·司』■]





●■■

95 由

古●亡

…冈■中■■■′0丁‖甘肿 弓叮3c′占分上琅



·●■中可0~◆占…■■出咀■和■

===乙■F←→唾

这个杀手不太冷.L的∩

95

■■■■●■

白★●白

硒‖〗00旬怕 ↑,0』忻ⅧⅡ■

95 白

贝■′?d2刀们 q瓣Q翱.Ⅶˉ告w

图l0-l0登录后的贞面 0

···【·…………ˉ · 伞 +



●|



●~=…

qo3尸◎巾『dde∩



●撬◆

因蜘……

x



● ~

G

●■■=………户…0≈

▲叼●

-

同S筐mp°



●■■ ■王别诅ˉ「■「ewe‖Myc◎∩cubi∩O ●●

憎喇撼

中田仰咱中田●■′wV分甘

瓣除婶

!”j好P 泌上铂

95 台

□ ■ ‖ 』 ■ ■ ■ ‖ | ‖ ■ ■ ] |





■■p■●●→■·■≡■■■●■p■m

===~白=



95 ●

魔■′‖协分们 0酗″●吗 1胺

肖申克的救赎ˉ丁h●5hawZha∩低R●d●丽ptj◎∩ ●●

95 岳

贝儡′可□’仕协 γ…宁的上■

|冬{ l0ˉl2换_个账号登录

=■司■·Ⅵ{■■‖』]』■||」■』■■』■‖



‖」■■■■|』■■■■■■■■】□‖‖」■■】·■‖

【匠◎·"△

这个杀手不太冷.L合。∩

●●●■●

| ■ ■ ■ ■ ■ | 』 ■ ■ ‖‖‖‖」』■■引』‖{||」■~司



』‖·□■‖』■■■』‖|||』■■■■〗□‖||』□司

图l0ˉl‖ 刷新儿次后的页面

(』■■ |司勺‖■]』』■】■可』‖』可|』』』■|·】』■|‖口』■〗』■

0芦b0

幽 肖申克的救赎ˉ帅e5们aw5h已∩伐ROd●mptj◎" ‖■■●

‖「|

| l04大规模账号池的搭建

387

所以说,如果有多个账号’在总请求量_定的情况下,可以将爬取请求分流到多个账号上’可以



降低封号的概率。 2.本节目标

本节的目标就是搭建一个账号池’例如在账号池内维护l00个账号信息以及对应的Cookie’并存 放到数据库中°每次爬取的时候,随机取用其中_个账号的Cookie° 这个账号池需要具备如下几个功能°

□需要保存能登录目标站点的账号和登录后的Cook1e信息° ■■尸‖■■「》■「【卜‖■■【■厂|)■·‖■厂『怪「△β尸■尸|《β侣》忙卜’『伊}也′广|■巴『〖■厂●广『卜住■‖「匹尸|伍「巴β}■『『〗‖|■尸‖匹∏■「伊■匹■『·『■尸卜『|[■「◆‖‖[■

□需要定时检测每个Cookie的有效性,如果检测到Cookje无效,就删除它并模拟登录生成新的 Cookje·

□还需要_个接口’即获取随机Cookje的接口°账号池在运行中’只需请求该接口,即可随机 获得_个Cookle并用其爬取数据°

由此可见,账号池需要有自动生成Cookie、定时检测Cookie、提供随机Cooklc这几个核心功能。

账号池有了,当然要使用它来爬取本章的案例网站’实现在不封任何_个账号的情况下高效完成全站 数据的爬取。

3.准备工作

请确保已经安装好Redjs数据库并使其能正常运行,安装方式可以参考h仗ps:〃setupscrapecenter/

redis°另外需要安装Python的redisˉpy` requests、 selelnium`∩ask` loguru和environs库’安装命令 如下: pjP31∩5ta11Iedj5Ieque5t55e1e∩i0Ⅶ+1a5R1ogurue∩v1Io∩5

4.账号池的架构

账号池的架构和代理池类似,也是分为4个

核心模块:存储模块、获取模块、检测模块和接 口模块’如图l0ˉ13所示。

厂厅—-

曲翻@ 获取筷块

4个模块的功能分别如下°

□存储模块负责存储每个账号的用户名、密 码,以及每个账号对应的Cookie信息,同 时需要提供一些实现存取操作的方法° □获取模块负责生成新的Cookie°该模块会

存储攫块

↑0 _

检呻炔

| 〈烹h

从存储模块中逐个拿取账号对应的用户

攫口顿块

名和密码’然后模拟登录目标页面,如果 登录成功’就把返回的Cookie交给存储模

图l0ˉl3账号池的4个核心模块

块存储°

□检测模块需要定时检测存储模块中的Cookie°这里需要设置_个检测链接’不同的网站的检 测链接不同°检测模块会逐个拿取账号对应的Cookje去请求检测链接’如果返回的状态是有 效的,就表示此Cookie没有失效’否则代表此Cookje失效,需要将其删除’然后等待获取模 块重新生成°

□接口模块需要用API提供对外服务的接口°由于可用的Cookje可能有多个’所以可以随机返 回Cookle的接口’用这样的方式保证每个Cookje都有被取到的可能。Cookie越多’每个Cookie

尸 } }

被取到的概率就越小’从而减少了被封号的风险。



‖‖

第l0章模拟登录

388

5.账号池架构的实现 首先,分别了解各个模块的实现原理。 ●存储模块

其实,需要存储的内容无非就是账号信息和Cookie信息。账号由用户名和密码两部分组成,我 们可以把它存成用户名和密码的映射°Cookie可以存成字符串,但是新的Cookie需要根据账号生成’

『卜~

…_





















赵…=

…=】【 】



{(

在生成的时候我们需要知道哪些账号已经生成了Cookie,哪些没有,所以要同时保存和Cookie对应 的用户名信息,其实也是用户名和Cookie的映射°加起来就是两组映射,我们自然而然能够想到Redis 的Hash,于是就建立两个Hash,结构分别如图l0ˉl4和图10ˉ15所示°

』□乙■司‖』‖■‖■■■|‖■■|

以上账号池的基本设计思路和第9章讲的代理池有相似之处’下_节我们用代码实现°

…n



_



= = …==…

】 巳 ’■日山

|=

…午









图10ˉ14Hash结构1 U■■

l■-=…≡ˉ鳞萝谨霹诞瞳j二么嚏蕊瞬髓…潍 ▲



■ 】

0 【 6





←~^乱-=←→^=→■≈白坪早≡吕≈←



≈□

=~■

=■

▲=≈

…m巴



……≡丛≡~一-≡



ˉ于箭=.



广=守■≡一ˉ ˉ

+…′m



…■酶…7………

1

! 『 』

●山…… _=一

‖ ]

ˉ≡

!{

-一哇-…←‖的≡-回

渺日山mv山·



■吕宇

■田

图10ˉ15

Hash结构2



两个Hash结构中的键名都是用户名,键值分别是密码和Cookje。需要泞意,账号池具有可扩展

把存Cookie信息的Hash名称设置为〔rede∩tja1:a∩tj5p1der6°如果要扩展微博的账号池,可以使用 a〔〔ou∩t:"ebo和〔rede∩tj己1:wejbo,这样比较方便。

接下来我们就创建一个存储模块类’用来提供_些Hash结构的基本操作’代码如下: j↑∏portm∩dα∩ i∏portIedi5 十r咖a〔〔ol』∩tpoo1。5etti∩gmport*

口■‖·』■■■司』■∏‖』·

可以对Hash结构的名称做二级分类,例如把存账号的Hash结构的名称设置为a〔〔ou∩t:a∩t15pider6’

‖‖■■

性’其中存储的一些账号和Cookje不_定适用于本案例’其他网站也可以对接此账号池’所以这里

0

c1己55RedjS〔1je∩t(obje〔t):

i∩jt (5e1十’ type’"ebsjte’ ho5t=R[DI5们5丁’ poIt=R[DI5pO盯’ pa55"oId=R[0I5pA55胆【D): 5e1+。db=redj5.5trjctRed15(host=ho5t’ port=port’ pa5sword=pa55"ord’ de〔ode-Ie5po∩5es=丁rue) 5e1于.type=type 5e1十。眶bSite=眶bSjte

de千∩3爬(Se1+)8 retuI∩+!{5e1十.type}:{5e1+。眶b5jte}‖

de十5et(5e1千’ u5er∩己厕e’ γ己1ue): retuⅢ∩5e1「。db.h5et(5e1千.∩吕爬()’ u5er∩a爬′γa1ue)

| |

defget(5e1+’ u5eI∩a眠): Ietu【∩5e1+.db.‖get(5e1十.∩a爬(), u5er∩a爬)

■闪』■■】■■□■凸■』■可■■‖□日‖·

de千

■‖·Ⅲ

} }卜

0



104大规模账号池的搭建

389

})

巴尸‖|∩}}■∏

de于de1ete(5e1十’ u5er∩a爬): retur∩5e1+.db.∩de1(5e1十。∩a眶()’ U5er∩a爬) de+cou∩t(Se1千): retur∩5e1f.db°h1e∩(5e1+.∩a爬())

de+ra∩doⅧ(5e1+): retumra∩do‖‖.chojce(5e1+。db。‖γa15(5e1十.∩a爬())) de+u5er∩a爬5(5e1+):

■尸`‖〗▲■■尸儿■■

retur∩5e1十.db.∩促ey5(Se1「.∩a爬(〉) de+己11(5e1+): retur∩5e1十.db.∩8eta11(5e1十。∩a‖e())

这里我们新建了-个Redj5〔1ie∩t类,其构造方法

j∩it

有type和web51te两个关键参数’

■’■厂

分别代表存储的内容类型和网站名称,是用来拼接Hash结构名称的两个字段°如果这个Hash是用来

‖【厂‖|}■匡尸β|仁■‖●「|卜卜|·『尸|)巳‖『卜′广卜『|【卜■β|■『『}卜庄■■}|[β‖「|△∩■‖‖■「

存储账号的’那么type是a〔cou∏t,web5jte是a∩tjspjder6;如果是用来存储Cookie的’那么type是 〔rede∩tja1’ web5jte是a∩t15pjder6°剩下几个参数代表Redis的连接信息,给这些参数传人值来初 始化一个5trjctRedj5对象’肄京Redis连接°

接下来的∩a‖e方法将type和"eb5jte拼接在了一起’组成Hash结构的名称°5et方法、get方 法和de1ete方法分别代表设置、获取和删除Hash结构中的某一个键值对,〔ou∩t方法用于获取Hash结 构的长度°

ra∩do们也是一个比较重要的方法,主要用于从Hash结构里随机选取一个Cookie并返回。每调用 一次ra∩d咖方法,就会获得一个随机的Cookje,将此方法与接口模块对接即可实现请求接口获取随 机Cookie。 ●获取模块

获取模块负责从存储模块中拿取各个账号信息并模拟登录,然后将登录成功后生成的Cookje保 存到存储模块中°相关代码如下: 「r咖己〔〔Ou∩tpoo1.eX〔eptiO∩5.i∩itj■甲ortI∏jt[X〔ept1o∩ +ro∏‖ ac〔ou∩tpoo1·5toIa8e5·redi5j呻ortRedi5〔1je∩t fr咖1吧uruj呻Ort1OggeI

c1吕55B己5e6e∩emtor(obje〔t): de「

1∩jt_(se1「’胎b5jte=‖o∩e〉: 5e1千.啮b5jte=web5ite i十∩ot5e1于.Neb5ite:

raj5eI∩jt[x〔eptio∩

5e1十.a〔cou∩t-operatoI.=Redi5〔1je∩t(tγpe≡ac〔ou∩t|’眠b5jte=5e1十.web5ite) se1十.crede∩tia1-oper3toI≡Redi5〔1je∩t(type=|〔rede∩tia1|’肥b5ite=se1十.脆b5ite) de十ge∩erate(5e1十’ u5er∩a爬’ pas5咖Id): rai5e‖otmp1e∏记∩ted[Iror de千i∩jt(5e1+〉: p己55

de十Iu∩(5e1「): 5e1千.i∩it()

1ogger.debl』g(`5tarttoru∩ge∩emtor』) +Oru5er∩a‖记’ paS5Nordj∩Se1+.a〔Cou∩t-operator·a11().jte"5(); j十5e1十。crede∩tja1-oper日tor.get(u5er∩己赡): 〔o∩tj∩ue

1ogger.debu8(+!5tarttoge∩eratecrede∩tj己1o+{u5er∩a爬}′) 5e1f.ge∩er己te(05er∩a爬’ pas5word)



↑0 匹

□』·』□‖■■∏‖

第l0章模拟登录

390

泣里新建了-个基类8a5eCe∏erator,在其构造方法中’初始化了两个Redj5〔1je∩t对象,分别是 a〔〔o0∩t一opeIator和〔rede∩tia1-operator·

| q

接着声明了ge∩rate方法和1∩jt方法,这两个方法目前都没有具体的实现。ge∩erate方法用于接 收用户名和密码,生成Cookie并返回,这里直接抛出了‖otI∏p1e删e∩ted[rror异常,因此子类必须实 现该方法’否则运行时会报错.j∩it方法则是在运行开始之前做一些准备T作’这里留空,子类可以 选择性复写°

最后就是最卞要的ru∩方法了,其主要逻辑是找出那些还没有对应Cookie信息的账号’然后逐个

调用8e∩emte方法获取Cookic°

对于本节的案例网站’我们可以直接实现ge∩emte方法来完成模拟登录,代码如下: 〔1a5s∧∩t15p1der6Ce∩erator(BaseCe∩emtor): defge∩eI己te(5e1「’ u5er∩aⅦe’ p己55"om): i千5e1+.〔rede∩tia1~operator.get(u5eI∩a们e): 1oggeI.debug(+‖〔rede∩tia1o「{u5ema|"e}exj5t5’ 5代jp‖)

{」

u5er∩a∩e : u5er∩aⅢe』

□□■■‖」‖‖■】

retuI∩

1ogj∩-ur1≡ !http5;//a∩ti5pideI6°s〔rape。〔e∩ter/1ogi∩0 5≡Iequest5.5es5io∩() 5.po5t(1ogj∩-ur1』 data={ pa5sword0 : pa55woId }) re5u1t= [] 「oⅢ〔oo仪jej∩5。〔oo代ie5:

pI1∩t(〔OO代ie。∩a爬’〔OO仅j巳γa1ue) res01t.appe∩d(+‖{coo促je.∩a|∏e}={〔ooki巳v己1ue}』) re5u1t= 0| ; 。joj∩(re5u1t) 1ogger.debug(千0get〔Iede∩tja1{Ie5u1t}0) 5e1{.crede∩tj己1ˉopeIator.5et(u5er∩a|∩e’ re5u1t)

●检测模块

我们现在可以利用获取模块生成Cookje了’但是免不了由于时间过长或者使用过于频繁等导致

Cookje失效°对于这样的Cookie’肯定不能把它继续保存在存储模块里°

此时检测模块闪亮晋场’它要做的就是检测出失效Cookje’然后将其从存储模块中删除。把失效 Cookie删除后,获取模块就会检测到与之对应的账号没有了Cookie信息’继而用此账号重新模拟登 录并获取新的Cookje,从而实现了此账号对应Cookie的更新°相关代码如下: 加poItreque5t5 +roⅦIeque5t5。ex〔ePt1o∩5i川poIt〔o∩∩e〔t1o∩[rroI 千ro∏a〔〔ou∩tpooLstorage5.redj5mport* 「rO们a〔〔Ou∩tpOO1。eX〔ept1O∩5·i∩iti川pOrtI∩1t[XCeptiO∩ 「ro∏1oguIumport1ogger

|·‖|(」·」门』‖〗』■■】‖』』■■■】‖|]■■』■‖』■]」】■■■|||』■|□■

运行这段代码,会遍历那些牟成Cookle的账号,然后模拟登录生成新的Cookie°



de十

i∩1t (5e1十′"eb51te≡‖o∩e): 5e1+·Web51te="eb5ite

j+∩ot5e1十·web51te:

ra1seI∩jt[x〔ept1o∩ 5e1十.3〔〔ou∩t≡opemtor二Redi5〔11e∩t(tγpe=a〔cou∩t‖’ 切eb5jte≡5e1+.web5ite)

5e1十。cre0e∩t1a1-oper己tor二Red15〔1je∩t(tγpe≡〔rede∩t1a1‖’web5jte≡5eM倒eb51te)

■∏|]《‖·■」|■】‖|日‖

c1a558ase丁e5ter(obje〔t):

de+te5t(5e1f’‖5eI∩a『∏e」〔rede∩tja1): m15e‖otI‖p1eⅧe∩ted[rIor

de十ru∩(Se1十): 〔Iede∩tja15≡5e1十.〔rede∩tia1-opemtor.a11()





′) 》)



■■■‖‖|匹■『‖△■尸△『Ⅱ尸匹■



l04大规模账号池的搭建

39l

+oru5er∩3们e′〔rede∩ti31j∩〔rede∩ti己15.jte∏)5(): 5e1+.te5t(l」5er∩a帐’〔Iede∩tia1)

为了实现通用性和可扩展性』我们定义了—个检测器父类,在其中声明_些通用组件。这个父类 叫作Ba5e丫e5ter,在其构造方法里指定了网站的名称"eb51te,并且同样初始化了两个Red15〔1je∩t对

象’分别是a〔〔ou∩tˉoperator和crede∩t1a1ˉopeIator。 然后最主要的方法就是ru∩了,其主要逻辑是遍历所有Cookje并逐个做测试,具体是利用

伊队}‖■■∏)巳【

〔rede∩tja1ˉopemtoI拿到所有的Cookie,然后调用te5t方法进行测试。这里的te5t方法同样抛出了 ‖otI们p1e‖e∩ted[rror异常’所以子类必须实现这个方法°下面我们再写_个子类继承这个8ase丁e5ter’ 并重写其te5t方法’代码如下: 〔1a5s∧∩ti5p1der6『e5ter(8ase『e5ter〉:

■尸‖〖ββ□β

de十

i∏jt (5e1十’ web51te=‖o∩e): 8日5e丫e5teI. 1∩1t (5e1+’ Web51te)

de+te5t(5e1「’ U5er∩a眠」〔rede∩ti日1): 1oggeI.j∩「o(+|te5tj∩8〔Iede∩tja1十oI{u5er∩a爬}‖) tIy: te5tur1=丁[5丁0R[趴p[5e1千。Web5jte] re5po∩5e=reque5t5.get(te5tur1′‖eader5={ 0〔oo促je0 : 〔rede∩t1a1

}′ ti爬out=5’ a11叫Iedire〔t5二「a15e) i+re5po∩5e.5tatu5〔ode==200;

1og8er.i∩千o(』〔rede∩ti己1i5va1id!)

β

e15e:

1ogger.j∩「o(‖〔rede∩tia1j5∩otva1jd’ de1etejt0) 5e1f.〔rede∩tia1ˉopeⅢ己tor。de1ete〈u5er∩a们e) e×〔ept〔o∩∩e〔tio∩[rro【8

1ogger。j∩十o(0te5t千ai1ed0) ‖■‖▲β



te5t方法的主要逻辑是拿到测试URL’然后获取Cookie进行模拟登录°如果返回的状态码是200’ 就证明Cookie有效’否则Cookie无效’将其对应的记录删除°

为了实现可配置化’我们将测试URL也定义成字典’代码如下: 『[5丁0肌灿P≡{

0a∩ti5pider60 : 0∩ttp5://a∩tispider6.5cr己pe。〔e∩ter/0 }



如果要扩展其他网站,可以统一添加在字典里。

‖巳∩旧‖『▲■尸『|■■「′‖|■厂【∩■■■■■「巴■=

0

●接口模块

如果获取模块和检测模块定时运行’就可以完成Cookie的实时检测和更新°此处的Cookje最终 是要为爬虫所用的,—个账号池可同时供多个爬虫使用,所以我们有必要定义—个Web接口,爬虫访 问此接口便可以获取随机的Cookie°我们采用Flask来实现接口的搭建,代码如下: j∏portj5o∩ 十r咖f1aS低i呻OIt「1aS促』 g

app=「1a5促(一∏蓟记—)

}[}





C[‖[R∧『0R趴p={

0a∏tjspjder60 : 0∧∏ti5pider6Ce∩emtor0 }

βapP.route(|/!) de+j∩deX(): retur∩ 0〈∩】〉‖e1〔o‖记to〔oo代jePoo15yste"〈/h2〉0

de十get-〔o∩∩(): 「oIweb5jtej∩C[‖[R∧『0偶趴P:

i千∏otha5attr(g’website)



第l0章模拟登录 Set日ttr(g’ 十‖{"eb5jte}-{Crede∩tja1}|’ Redi5〔11e∩t(〔rede∩tia1’ b‖eb5ite)) 5et己ttr(g’ 十!{web5ite}{a〔〔ou∩t}‖’ Redj5〔1je∩t〈accou∩t’web51te)) retl」r∩g

{||

392

@己pp.route(0/<web5jte〉/ra∩do∏) de十ra∩do们(web5ite);

resu1t=get己ttr(g」 十‖{"eb5ite}{〔Iede∩tia1}‖).ra∩do∏)() 1ogger.debug(+0get〔rede∩tja1{re5u1t}‖) retu】∩re5u1t

|‖

g=get-〔o∩∩()

泣里同样需要实现通用的配置以对接不同的站点,所以接口链接的第_个字段定义为网站名称’ ■

扩展其他站点’可以更改web5jte参数’例如/"e1bo/m∩do阳代表获取微博的随机Cookle。 ●调度模块

』■■■■‖■■■|

第二个字段定义为获取方法,例如/a∩tj5pider/ra∩do‖代表获取当前案例网站的随机Cookie’如果要

』■』■司

调度模块的作用是计前面4个模块配合运行,主要工作是驱动几个模块定时运行’同时各个模块 需要运行在不同进程k,相关代码如下:

{ 日

1∏pOrtt1爬

i"portⅦu1tjpro〔e55i∩g +roⅦ己〔〔oo∩tpooLpro〔e55or5。5erγer1‖portapp 十ro‖a〔〔ou∩tpoo1.pro〔e55or51Ⅷportge∩eratoIa5ge∩erators 十roⅦaccou∩tpoo1°proce55or51们portte5tera5tester5 千roⅦa〔〔ou∩tpoo1.setti∩g加port〔γ〔[[C[‖[RA丁0R’〔γ〔[[了[5「[R’ ApI‖05『》 ∧pI丁‖R[AD[0’ ∧pI pO盯’ [‖∧Bl【5[Rγ[R’[‖AB[[C[‖【R∧丫0R’[‖∧B[[丁[5丁〔R』 I5"I‖刚5』「[5「[R肌P’ C[‖[R∧『0R刚p

千ro刚1oguIumport1oggeI j「I5‖I‖四‖5:

te5ter-pIO〔e55』 8e∩emtOr=prO〔e55』 5erver一PrO〔e55=‖O∩e’ ‖O∩e’ ‖O∩e

〔1a555chedu1er(obje〔t): de于ru∩te5ter(5e1十′"eb5ite’〔yc1e=〔γα[「[S『[R): i十∩ot[‖∧8[[丁[5「[R:

1ogger·i∩千o(‖te5ter∩ote∩ab1ed’ exjt』) retur∩

te5ter≡getattr(te5ter5’「[5丁[β灿p[Web5jte])("eb5jte)

·■‖‖·■■∏‖司』■‖||』■■日〗‖■■■‖]|』Ⅵ||

爪u1tiproce5sj∩g.十reeze—5upport()

1oop=O W‖j1e丁me:

1ogger.debug(于0te5ter1oop{1oop}5tart…|) te5ter·m∩() 1OOp+=1 tme.s1eeP(〔y〔1e)

de千ru∩ˉge∩eIator(5e1+’website’ cy〔1e=〔γ〔[[C[‖[R∧『OR): i十∩Ot [‖∧81[C[‖[R∧『0R:

1ogger.j∩于o(’getter∩ote∩ab1ed’ exit‖ ) Ietur∩

ge∩emtor=get3ttr(ge∩erator5’C[‖[RA丁0R肌p[web5jte])(web5ite) ≈

』OOp=O W∩j1e丁rue:

1og8er.deb0g(+‖getter1oop{1oop} 5t己rt…‖) ge∩erator.n』∩() 1oOp+=1 t加e.51eep(cyc1e)

de千ru∩Serγer(5e1十’ ):

1ogger.i∩+o(05erγer∩ote∩ab1ed’ ex1t0) retur∩

己pp.ru∩(ho5t=ApI‖05丁’ port=∧pIp0盯’ threaded=∧pI丁‖R[AD[0)

【■尸■■■【■■■尸■■【■尸‖‖■■■■仅【■【〗尸■

1于∩Ot[‖A6[[5[Rγ[R:

‖巳■=『‖■凸『′‖‖』‖』巴〗「}■日『「■=尸)‖『|■『|‖‖「■■【■尸‖‖■尸|■■「■■■厂■■■□■尸β‖『=■|尸广■尸『「卜巴=■‖‖〖●『‖■■厂}■■厅‖●「‖电『【『●甘…尸‖●■■=■「~尸「巴■「=广|β■厉|‖{

l04大规模账号池的搭建



393

de千ru∩(5e1千’Web5ite):

g1oba1tester-proce55’ ge∩er日tor=pro〔e55’ 5erγer-pro〔ess tIy:

1og8er.j∩+o〈05tartj∩gaccou∩tpoo1…|) j+[‖∧B[[丁[5丁[【:

te5ter-proce55=∩u1tiproce55i∩g.pro〔e55(target=5e1十.ru∩te5ter》 arg5=("eb5ite’)) 1og8er·1∩千o(千‖start1∩gte5teI’ pid{te5ter-pro〔e55·pjd}…‖) te5ter一pro〔e55.5t日rt() j千[‖∧8[[□[‖[R∧「"8

ge∩erator~pro〔e55=‖u1tjproce55j∩g。Proce55(target=se1+.ru∩ˉge∩emtor’ ar8s=(web51te’)) 1ogger。j∩千o(千!5tartj∩g8etter’ pjd{ge∩er3tor-pro〔e55.pjd}…0) ge∩eratoLpro〔ess.start() i十[酗8[[ 5[Rγ[R:

5erver≡proce55=则1tjproce55j∩g.pIo〔e55(taIget=5e1+.Iu∩serγer’ aIg5=(Neb5jte’)) 1ogger·1∩千o(+,5tartj∩g5erγer’ pid{5erγerˉproce55。pjd}…|) 5erγer=prOCe5S.5t己rt() te5terˉproce55.joj∩() ge∩eratoIˉproce55.joi∩() 5erγerˉPrOCe55.jOi∩() exceptNeybo3rdI∩terrl』pt:

1o8ger.1∩+o(0re〔ejγed代eyboard1∩terrupt51g∩a1|) teSterˉprOCe55。temi∩ate() ge∩eratorˉprO〔e55·term∩ate() 5erγerˉpro〔ess.temi∩ate() 千1∩a11y:

testeI-pIoceS5.joi∩() ge∩eIatOrˉPIO〔e55.jOi∩() 5erγerˉpro〔e55.joi∩()

1ogger.j∩十o(十‖te5terj5{′′a1iγe" j十te5ter-proce55.i5a1iγe()e1se"dead"}‖) 1ogger.j∩+o(十,getteris{"a1jγe" i千ge∩er己torˉpro〔e55.j5a1iγe()e15e ′』dead,}|) 1ogger.i∩+o(十|5erγeIj5{‖!a1jve00 i+5eIγer=proce55.15a1jγe(〉e15e ,‖dead"}0) 1ogger.i∩+o(|accou∩tpoo1temi∩ated‖)

这里用到了两个重要的配冒’即产生模块类和测试模块类的字典配置’代码如下:



‖0

#产生模块类的字典配Ⅲ 6[‖[R∧丁0佣趴p={

■■■■■■■

|a∩tj5pider60 目 |A∩ti5pider6Ce门erator‖ } 夕

#刮试棋块类的字典配丑

丁[5丁[M∧p={ |a∩tj5pjder6‖; 0∧∩ti5pjder6丁ester0 }

这样的配置是为了方便动态扩展’键名为网站名称’键值为类名。如果要扩展其他站点,可以在 字典中添加’例如扩展微博的产生模块可以配置成这样: 6删[R∧『0R肌p={ 0weibo‖ : ‖‖eibo〔oo低1esCe∩emtor0 ’ ,日∩t15pider6` : ‖∧∩ti5pider6Ce∩emtoI′ }

在5〔∩edU1er类里对字典进行遍历’利用module的getattr方法获取对应的类’调用其人口ru∩方 法运行各个模块°同时,各个模块的多进程使用了multiprocessing中的pro〔es5类,调用其5tart方 法即可启动各个进程。

另外’各个模块还设有模块开关,可以在配置文件中自由设置此开关的开启和关闭,代码如下: 十rOⅧe∩γ]IO∩5mpOrt[∩γ e∩v≡ [∩v() e∩γ.reade∩γ() ]





γ





』·

∩ ●

∩∏ =七

丁 [ 「







[‖

[ 日

|[‖∧8l[-丁[5丁[R!’『rue)

】||■〖



第l0章模拟登录

[‖∧B[[0[‖[R∧「0R=e∩v.boo1(‖[‖A8[[6[‖[RA丁OR‖’『Iue) [‖A8l[5[Rγ[R=e∩γ.boo1( ![‖∧B儿[5[【γ[R0 ’ 丁rue)

设胃为丁rue代表开启模块,为「a15e则代表关闭模块’这里借助「environs库来实现测试。至此, 我们的配置就全部完成了°接下来将所有模块同时开启,启动调度器,命令如下所示: pγtho∩3ru∩°pya∩tisp1der6

控制台的输出结果如下: 202Oˉ1o-130O:34:27.3O8 | D[B06

| a〔cou∩tpoo1.5〔hedu1eI:n」∩te5teⅢ:31 ˉ te5ter1oopO5taIt…

*[∩γirO∩川e∩t: prOduCtiO∩

‖AR‖I‖C: 丁h15 i5adeγe1op|∏e∩t5erγer· Do∩otu5eitj∩aproductjo∩dep1oy阳e∩t. 05eaprod0〔tjo∩‖5CIserveri∩5tead。 *Debugmde: o仟 2o2oˉ1oˉ13oo:34:27.3o9 | 0田0C | a〔cou∩tpoo1.proce55or5.8e∩emtoⅢ:rl」∩;39 ˉ 5tartton」∩8e∩emtor 2O20ˉ10ˉ13OO:34827.309 | I‖「O | a〔〔ou∩tpoo1.pIo〔es5or5.te5ter:te5t:51 ˉte5t1∩8crede∩t1己1+or己dm∩ 2O2Oˉ1Oˉ130O:34:27°310 | 0[80C | a〔〔o‖∩tpoo1。proce5Sor5·ge∩er日tor:ru∩:41 ˉ 5tarttoge∩er3te 〔rede∩t1日1o十adm∩

| 3〔〔ou∩tpoo1。pro〔e55or5·ge∩emtor:ge∩erate:6〕 ˉ 〔rede∩t1a1o十

ad‖i∩eXj5t5』 5灶p

2O2Oˉ10ˉ1300:34:27.31O | 0[B|」C

| a〔〔ol」∩tpoo1.pro〔e55or5。8e∩erator:ru∩;41 ˉ 5tarttoge∩emte

〔rede∩ti己1o十己d∏i∩2

202Oˉ10ˉ13OO:34;27.31O | 0[B0C

| a〔〔ou∩tpooLpIoces5or5°ge∩erator:ge∩erate;63 ˉ〔rede∩t1a1o+

己d∏i∩2exiSt5’ 5mP

2O20ˉ1Oˉ13卯:34:27.31O | D[80C

|a〔〔ou∩tpooLpro〔e5sor5。8e∩erator:ru∩:41 ˉ 5tarttoge∩erate

crede∩t1a1o+adⅧ1∩3

*旧‖∩∩j∩go∩http://O.0.O.O:6789/(Pre55〔『R[+〔toq0jt) 2O2Oˉ1Oˉ13OO:34:32·073|I‖「O | ac〔ou∩tpoo1。pro〔e55or5·te5ter:te5t:58 ˉ 〔Iede∩t1己1j5γa1jd 202Oˉ1Oˉ13OO:34:32.073 | I‖P0 | a〔cou∩tpoo1.pro〔e55or5。te5ter:te5t:51 ˉte5ti∩g〔rede∩tja1千oradm∩2 2O2Oˉ10ˉ13OO:〕4:3∑.678 | 0[80C | a〔〔ou∩tpoo1°pro〔e55or5.ge∩eIatoI;ge∩erate:76ˉget〔rede∩tj日1 2O2O-10ˉ13OO:3q:〕2.68O|0[B{」C | 己〔cou∩tpoo1.pro〔e55or5。ge∩erator:ru∩841 ˉ 5tarttoge∩emte

■■·可‖二■■■可■■■■■』■■日■卫■‖‖■■Ⅷ■】‖]·』■|□■】■■】·】』■】二■∏|■曰司夕■∏●■■

*5eIγi∩g「1a5代己pp"acCou∩tpoo1.pIoces5or5.5erγer"(1azγ1oadi∩g)

202Oˉ10ˉ13OO:34:27·31O | 0[8[」C

{ | |

394

〔rede∩tia1O+ad∏i∩q

从控制台的输出内容可以看出’各个模块都正常启动,检测模块逐个测试Cookie’获取模块获取

尚未生成Cookie的账号,各个模块并行运行,互不干扰°

我们可以访问接口模块获取随机的Cookle,如图l0ˉl6所示。 | 0■



o]”ˉoˉ0.↑:句89扫∩t‖sp蛔·呵『a『×

+ +

÷÷G°↑27°0·0。↑:6789/乙∏tjSpj“阎『a∩o◎咖

图l0ˉl6访问接口模块

爬虫只需要请求该接口就可以获取随机的Cookie。 6.账号池的使用

我们先将账号池运行_段时间’让其模拟登录_些账号并维护起来。接着便可以使用账号池实现 全站数据的爬取了°这里我们使用ajohttp来实现’由于案例网站中每个电影详情页的URL是有一定 规律的’所以我们直接构造l00个详情页URL来进行爬取,整体代码实现如下: mporta5y∩cio i呻Ortajohttp

+ro∏0 pyqueryj『∏portpyQueIya5pq 千r咖1ogumi呕rt1ogger

|{

se唾‖o∩‖d≡巾↑e6tmW娇顾砒0kc徊创6◎8|c胆qp9m



l0.4大规模账号池的搭建

△β‖|仿



「 | 尸

「|



395

川MIO=1佣

〔0‖〔URR[‖〔γ=5

T∧RC[丁0肌= 0http5://a∩t15pideI6.5crape°ce∩ter! ∧〔〔00丁p"山0R[= !http://1o〔日1ho5t:6789/a∩t15pider6/ra∩do叮 5e∩↑aphore=a5y∩〔1o°5e门己p∩ore(〔0‖〔0R旺‖〔γ)

■厂|’‖β‖‖巴∩』■}尸△口》叮■∏

a5y∩〔de+par5eˉdeta11(htⅧ1): do〔≡pq(∩t"1) tit1e=do〔(0 .iteⅧh2‖).text() 〔日tegorie5= [jte".text(〉+orjte川i∩doc(! .jte∏ 。〔ategoI1e55Pa∩‖)。jte阳5()] 〔OVer=dO〔(0 .1te们 .〔OVer‖).attr(05r〔‖) SCOre=dO〔(0 .1te们 .S〔Ore0)。text() dIa川a=do〔(‖。1te们 .draⅧ3‖)·text()。5trjp() retur∩{ · !t1t1e0 ; tjt1e」 ↑categorje5|: categor1e5’ COγer :ˉ〔OVeIp 5〔ore ; 5〔oIe’

‖dm们a|; dm川a 》 P



a5γ∩cde十十et〔h〔rede∩tj日1(se55io∩): a5y∩〔"1th5e55jo∏.get(∧〔〔0{∏p"[0R[) a5Ie5po∩se: Ietur∩a"ajtre5po∩5e。text()

~』

·∏卜尸》●「β◆‖巴卜}‖》‖口■「■■■

a5γ∩Cde十5〔r己Pe—detaj1(5e5SiO∩’ Ur1): a5y∩〔wjth5eⅦaphore; CIede∩tja1=await十et〔∩〔rede∩tj日1(5e55jo∩) beader5={|cookje‖: crede门tja1}

1og8er.deb0g(十|5〔rape{uI1}u5j∩gcIede∩tja1{〔rede∩tia1}0) a5y∩c"jt‖5e55io∩.get(ur1’∩eader5≡headers)日5re5po∩5e: 肚"1=awajtre5po∩5e。text() data=a"aitpar5edetai1(ht"1) 1ogger。debug(千!data{data}‖) a5y∩〔de+们日i∩(): 5e551o∩=a1ohttp.〔1je∩t5e55io∩() ta5促5= [] +orii∩ra∩ge(1』‖AXI0+1); ur1=千0{『∧RC[丁0R[}/detaj1/{i}0

ta5|〈 ≡a5y∩cjo.e∩5ure+utuIe(5crape-deta11(5e55jo∩’ ur1〉) t日5促5°日ppe∩d(ta5惯) awa1ta5γ∏〔io。gatber(*ta5长S)



} ■厂■巴尸■尸‖▲■『■■■{‖心∩‖‖【■「■厂凸■尸β◆「■『『

i+

∩a‖e

== 0

‖E1∩

:

日5y∏〔iO.get-eγe∩t—1OOP()。IU∩U∩ti1-COⅦP1ete(∏`aj∩())

我们一共实现了4个方法°

□"aj∩:人口方法。这里我们构造了l00个详情页URL’然后调用asyncio的e∩5Ure+uture方

法将5〔rape—deta11方法初始化为一个个异步任务,再调用gat∩er方法使它们运行起来。 □5〔Iapeˉdeta11:爬取方法°主要用来爬取详情页的信息,这里的关键点就是在爬取之前先调 用十etchcrede∩t131方法获取一个Cookje’然后利用Cookie进行数据爬取°如果不这样做, 是爬取不到任何数据的,爬取完毕之后会调用parSe-deta11方法对页面数据进行解析。 □+et〔hcrede∩tja1:主要用来从账号池获取Cookje信息°这里我们将账号池的API为定义 ∧〔〔"‖丁p"[0R[,每请求_次,就可以获取-个Cookie°

| ■「■■『『ˉ■|■尸●■厂卜|}■「|[■「●「【■『



□par5e—detaj1:解析方法。主要用来解析爬取的详情页,提取想要的电影名称、类别`封面 评分和简介等信息°

另外,为了限制爬取速度’这里还引人了信号量’限制并发量为5。



第l0章模拟登录

』categor1e50 : [ 0乃||价’ 0动作! ’ ,犯罪!]’|〔oγer‖ : 0http5://p1."ejtua∩.∩et/|‖oγ1e/6bea9a千4524d千bdob668eaa 7e187〔3d「767∑53.jpg0464w6帜∩1e1〔|, 05〔ore0 ; |9。5‖’ !dra∏a0 : |乃|」′价周介\∩里品(让.岔诺饰)是名孤独的

职业杀于’…,史大的冲突在所难兄..ˉ.|}

202oˉ10ˉ14o0:51:52。129 | D[8[』C "ai∩ :5〔rapeˉdet己i1:39ˉ 5〔rapehttp5://日∩tj5pideI6.5〔rape·〔e∩ter/ detaj1/7u5j∩g〔rede∩tja15e55jo∩jd=dqdb+t1r+1j〔a9j81十5十〔∩〔y9∩γq〔1wb

● ■ 可 ■ 】 · 纠 〗 ■ 可 』 · ■ ■ ■ □ ‖ |

陪凸杰兑和这段父价长眠海底. |}

2O2Oˉ1oˉ1q0O:51:』9.127 | D[B(」C |-∏己j∩ :5cⅢ己pe-det己j1:39ˉ 5〔Iape∩ttp5://己∩tispider6.5crape.ce∩ter/ detaj1/6 l』51∩g〔Iede∩t1a15e551o∩jd=十1w38|(z3γo7d89们b1Ⅷy2〕5o〔ej|〈63qz 20∑oˉ10ˉ140o:S1:52.126 | 0[80C Ⅶaj∩ :5〔mpe-detai1:03 ˉ d己ta{0t1t1e0 : 0这个杀于不太冷ˉ [白o∩0 ’

■∏|‖|二■可|」·|{■‖列凸』

2o2oˉ1oˉ140o:51:4O·685 | 0[80C |—们ai∩ :5crape-detaj1:39ˉ 5crape∩ttp5://a∩tj5pider6.5〔mpe.ce∩ter/ det日j1/105j∩gcrede∩ti己15e55jo∩jd=ht4uwjb「1o28q叫o8j87ozdd‖mg促zcq∩ Ⅺo20ˉ1o_1qoo:518』0.695 | D[8(」C |一|∏日i∩ :5〔r日pe一detai1:39ˉ 5crapehttp5://a∩t15pider6.5〔rape.〔e∩teI/ det己j1/3u5i∩gcrede∩tia15e55jo∩jd=1帕wd4+pqw14]吧〔yjtgmyeprt+∩6uj 】02oˉ1oˉ14oo:51:40.695|D[B0C |_』E1∩ 85cr己pe-det己i1:〕9ˉ 5cmpehttp5://a∩t15pjdeI6.5〔mpe.〔e∩ter/ detai1/5 l』sj∩gcrede∩t1a15es5jo∩jd=∩b1e6t‖Wb千刚促O代c1ot16o81〔代8qp9们 202Oˉ1oˉ140o:51:』o.696|0[B06 |-∏`ai∩ :5〔rape=det己j1:39 ˉ 5〔rape∩ttp5://a∏tj5p1der6.5cmpe.〔e∩ter/ det己i1/2 l』5j∏gcrede∏ti日15e5sjo∩id=dhjaxb1zd8xqa+8p7wyqgγwb叫teueid 202oˉ1oˉ14Oo:51:』o.696 | D[B0C |-们aj∩ :5〔mpeˉdetaj1:39ˉ 5crape∏ttp5://己∩ti5p1der6.5crape.ce∩ter/ det己i1/405i∩8〔rede∩tia15e55jo∩jd=a3∩c冰qo52"7j1wqktdb己r4g190∩日y∩1 2o2oˉ1oˉ14oo:518』9.121 | D[B(」C Ⅶai∩ :5crape-det日j1843 ˉ data{0tit1e! : |暴坦尼兑号ˉ『jt日∩jc0 ’ ‖categorje5‖ : [!剧悄|′ 0爱怕|′ ‖灾难‖]’ 0cover‖ : 0http5://p1.爬itua∩.∩et/巾vje/b607+ba7513e7+1Seab170 己ac1e1q"d878112.jpgα6↓"6“h1e1c! ’ 05core|『 !9.5‖ ’ 0dm∏a! :·0瓜|」份简介\∩1912年q月15日,…让它

■ ■ ` 』 ■ ■ |

上述代码的运行结果如下:

』■』■■可‖|』■■■】|‖|■■■∏‖

396

可以看到此时的并发量被限制为了5,每次爬取都会获取—个Cookie’接着便会打印爬取的结果’ 7.总结

本节我们了解了账号池的基本作用、设计原理和基本实现,并通过一个案例结合账号池进行了数 据爬取,突破了单账号爬取频率的限制°

本节代码见https://githuhcom/Python3WebSpjde∏AccountPool°

■■■·‖■Ⅵ■司■·‖‖』■□·‖」‖°■□·■■司

不会再产生封号问题。

本节内容比较重要’需要好好掌握,后面我们会利用账号池和第9章所讲的代理池进行分布式大

‖|‖`■■■■■』■■]‖□■■□■』■‖|■■■

规模的爬取。



日‖ ■八

(‖‖

‖‖

‖■■‖ ‖



}『



》「‖[侩‖『『|卜 ■■『■‖■■‖〖广【伊[「‖||

第]T章

}′

」avaSc「|pt

卜‖

随着大数据时代的发展’各个公司的数据保护意识越来越强,大家都在想尽办法保护自家产品的

■卜■巳■}■■了△‖■『■■「卜炉「▲卜‖■厂|‖【β「|》|■尸

数据,不让它们轻易地被爬虫爬走°由于网页是提供信息和服务的重要载体’所以对网页上的信息进 行保护就成了_个至关重要的环节°

网页是运行在测览器端的’当我们测览_个网页时’其HTML代码、JavaSc门pt代码都会被下载 到测览器中执行°借助测览器的开发者工具’我们可以看到网页加载过程中所有网络请求的详细信息’

也能清楚地看到网站运行的HTML代码和JavaSc∏pt代码。这些代码里就包含了网站加载的全部逻辑’ 比如加载哪些资源,请求接口是如何构造的’页面是如何喧染的,等等°正因为代码是完全透明的’ 所以如果我们能研究明白其中的执行逻辑,就可以模拟各个网络请求’进行数据爬取了°

然而’事情没有想象得那么简单°随着前端技术的发展,前端代码的打包技术、混淆技术、加密

技术也层出不穷’借助于这些技术’各个公司可以在前端对JavaSc∏pt代码采取—定的保护’比如变 量名混淆`执行逻辑混淆`反调试、核心逻辑加密等’这些保护手段使得我们没法很轻易地找出

′})}

■■尸巴■「‖■=尸【尸「||止■∩■■尸■■厂『■■■■∏{}|『[尸·『‖

JavaSc∏pt代码中包含的执行逻辑°

在前几章的案例中’我们也试着爬取了各种形式的网站。其中有些网站的数据接口是没有任何验 证或加密参数的,我们可以轻松模拟并爬取其中的数据°但有的网站稍显复杂’网站的接口中增加了

_些加密参数’同时对JavaSc∏pt代码采取了上文所述的一些防护措施。当时我们没有尝试去破解’ 而是用类似Selenium等工具模拟测览器的执行方式,进行“所见即所得”的爬取。其实对于后者,我 们还有另外-种解决方案:逆向JavaSc∏pt代码,找出其中的加密逻辑’直接实现该加密逻辑进行爬 取°如果加密逻辑过于复杂’我们也可以找出_些关键人口,从而实现对加密逻辑的单独模拟执行和

数据爬取°这些方案的难度可能很大,比如关键人口很难寻找或者加密逻辑难以模拟’可是_旦成功 找到突破口’我们便不用借助Selenium等工具进行整页数据的谊染’爬取效率会大幅提高°

在本章中’我们首先会对JavaScnpt防护技术进行介绍’然后介绍一些常用的JavaSc∏pt逆向技 巧’包括测览器工具的使用、Hook技术、AST技术、特殊混淆技术的处理、WebAssembly技术的处 理°了解了这些技术’我们可以更从容地应对JavaSc∏pt防护技术。

↑↑.↑

网站加密和混淆技术简介

我们在爬取网站的时候,会遇到_些需要分析接口或URL信息的情况,这时会有各种各样类似 加密的情形。

□某个网站的URL带有-些看不太懂的长串加密参数,要抓取就必须懂得这些参数是怎么构造 的’否则我们连完整的URL都构造不出来,更不用说爬取了。

□在分析某个网站的Ajax接口时’可以看到接口的-些参数也是加密的,RequestHeaders里面 也可能带有一些加密参数,如果不知道这些参数的具体构造逻辑,就没法直接用程序来模拟这 些Ajax请求°



|们

第ll章JavaScript逆向爬虫

这些导致我们无法轻易根据JavaScript源代码找出某些接口的加密逻辑。

以上情况基本上是网站为了保护其数据而采取的_些措施,我们可以把它归类为两大类: □URL/API参数加密

↑.网站数据防护方案

当今是大数据时代’数据已经变得越来越重要了°网页和APP现在是主流的数据载体,如果其数 据的API没有设置任何保护措施,那么在爬虫工程师解决了一些基本的反爬(如封IP`验证码)问题 之后,数据还是可以被爬取到的°

●URL/API参数加密

|‖‖Ⅱ‖勺刘

有没有可能在URL/API层面或JavaSc∏pt层面也加上一层防护呢?答案是可以°

■■‖‖‖』〗』■■∏‖‖□■日‖■■』叼■】■司|‖『|□□』·

本节中,我们就来了解—下这两类技术的基本原理和一些常见的示例°知己知彼,百战不殆,了 解了这些技术的实现原理之后’我们就能更好地去逆向其中的逻辑’从而实现数据爬取°

{ ■■

□JavaSc∏pt压缩`混淆和加密

』■■〗』勺

□翻看网站的JavaSc∏pt源代码’可以发现很多压缩了或者看不太懂的字符.比如JavaScnpt文 件名被编码’文件的内容被压缩成几行,变量被修改成单个字符或者一些十六进制的字符……

||}

398

网站运营者首先想到的防护措施可能是对某些数据接口的参数进行加密’比如说给某些URL的

参数加上校验码,给一些ID信息编码’给某些API请求加上to促e∩` 51g∩等签名’这样这些请求发 送到服务器时’服务器会通过客户端发来的一些请求信息以及双方约定好的密钥等来对当前的请求进 行校验,只有校验通过,才返回对应数据结果。

比如说客户端和服务端约定—种接口校验逻辑,客户端在每次请求服务端接口的时候都会附带一

个51g∩参数’这个51g∩参数可能是由当前时|司信息、请求的URL、请求的数据、设备的ID`双方约 定好的密钥经过一些加密算法构造而成的,客户端会实现这个加密算法来构造51g∩,然后每次请求服 务器的时候附带上这个参数。服务端会根据约定好的算法和请求的数据对S1g∩进行校验’只有校验 通过,才返回对应的数据’否则拒绝响应°

当然’登录状态的校验也可以看作此类方案’比如一个API的调用必须传一个token’这个token 必须在用户登录之后才能获取,如果请求的时候不带该token,API就不会返回任何数据°

倘若没有这种措施’那么URL或者API接口基本上是完全可以公开访问的,这意味着任何人都 可以直接调用来获取数据’几平是零防护的状态’这样是非常危险的’而且数据也可以被轻易地被爬

虫爬取°因此’对URL/API参数进行加密和校验是非常有必要的°

●JavaScript压缩、混淆和加密 接口加密技术看起来的确是-个不错的解决方案,但单纯依靠它并不能很好地解决问题°为什 么呢?

对于网页来说’其逻辑是依赖于JavaSc∏pt来实现的°JavaScnpt有如下特点。 □JavaSc∏pt代码运行于客户端’也就是它必须在用户测览器端加载并运行° □JavaScnpt代码是公开透明的,也就是说测览器可以直接获取到正在运行的JavaSc∏pt的源码。

基于这两个原因, JavaScrlpt代码是不安全的’任何人都可以读、分析`复制`盗用甚至篡改 代码。

所以说’对于上述情形,客户端JavaScnpt对于某些加密的实现是很容易被找到或模拟的’了解

了加密逻辑后’模拟参数的构造和请求也就轻而易举了,所以如果JavaSc∏pt没有做任何层面的保护



』|





















| |





■尸|■■■∩[‖β|【■【‖■■■

} ■「卜厂 ■■厂△■尸‖‖■尸『■■厂

卜 卜尸‖■尸

β

‖卜

■■

■∏止尸=尸β■▲尸尸匹β【■尸「■■◆「‖‖‖伊△■『 □』

■匹≥『『■「‖■■■■■■『『■「■■「|■■■尸[『[△■『卜坚■■尸|}■■「△■‖|厂

} 『卜

p



ll」 网站加密和混淆技术简介

399

的话’接口加密技术基本上对数据起不到什么防护作用°

如果你不想让自己的数据被轻易获取,不想他人了解JavaScnpt逻辑的实现’或者想降低被不怀 好意的人甚至是黑客攻击的风险,那么就需要用到JavaSc∏pt压缩`混淆和加密技术了° 这里压缩`混淆和加密技术简述如下。

□代码压缩:去除JavaScrjpt代码中不必要的空格`换行等内容,使源码都压缩为几行内容’降 低代码的可读性’当然同时也能提高网站的加载速度。 □代码混淆:使用变量替换、字符串阵列化`控制流平坦化`多态变异、僵尸函数`调试保护等

手段,使代码变得难以阅读和分析’达到最终保护的目的°但这不影响代码的原有功能,是理

想`实用的JavaScript保护方案° □代码加密:可以通过某种手段将JavaSc∏pt代码进行加密’转成人无法阅读或者解析的代码’ 如借用WebAssembly技术’可以直接将JavaScnpt代码用C/Gˉ+实现’JavaScnpt调用其编译 后形成的文件来执行相应的功能°

下面我们对上面的技术分别予以介绍。 2.0只|ˉ/∧尸|参数加密

现在绝大多数网站的数据_般都是通过服务器提供的APl来获取的,网站或App可以请求某个数据 API获取到对应的数据’然后再把获取的数据展示出来。但有些数据是比较宝贵或私密的’这些数据肯 定需要_定层面上的保护。所以不同API的实现也就对应着不同的安全防护级别’我们这里来总结下° 为了提升接口的安全性’客户端会和服务端约定_种接口校验方式,一般来说会用到各种加密和 编码算法’如Base64、Hex编码,MD5`AES`DES、RSA等对称或非对称加密°

举个例子’比如说客户端和服务器双方约定—个5ig∩用作接口的签名校验’其生成逻辑是客户 端将URL路径进行MD5加密’然后拼接上URL的某个参数再进行Base“编码’最后得到一个字符

串51g∩’这个51g∩会通过RequestURL的某个参数或RequestHeaders发送给服务器。服务器接收到 请求后’对URL路径同样进行MD5加密,然后拼接上URL的某个参数’进行Base“编码’也会得

到一个5ig∩°接着比对生成的51g∩和客户端发来的51g∩是否一致,如果一致’就返回正确的结果’ 否则拒绝响应°这就是—个比较简单的接口参数加密的实现°如果有人想要调用这个接口的话’必须

厂!





定义好51g∩的生成逻辑,否则是无法正常调用接口的°

当然’上面的这个实现思路比较简单,这里还可以增加—些时间戳信息增加时效性判断’或增加 一些非对称加密进_步提高加密的复杂程度°但不管怎样’只要客户端和服务器约定好了加密和校验 逻辑’任何形式的加密算法都是可以的°

这里要实现接口参数加密,就需要用到_些加密算法,客户端和服务器肯定也都有对应的SDK实 现这些加密算法,如JavaScrjpt的cryptojs、Python的hashlib、Crypto’等等°

但还是如上文所说’如果是网页的话’客户端实现加密逻辑使用JavaScnpt的话’其源代码对用 户是完全可见的’如果没有对JavaScnpt做任何保护的话’很容易弄清楚客户端加密的流程°

因此’我们需要对JavaSc∏pt利用压缩`混淆等方式来对客户端的逻辑进行一定程度的保护° 3.」aγaSc『|pt压缩

这个非常简单’JavaSc∏pt压缩即去除JavaSc∏pt代码中不必要的空格`换行等内容或者把一些可 能公用的代码进行处理实现共享’最后输出的结果都压缩为几行内容,代码的可读性变得很差’同时 也能提高网站的加载速度。



第ll章JavaScript逆向爬虫

如果仅仅是去除空格`换行这样的压缩方式’其实几乎是没有任何防护作用的,因为这种压缩方

式仅仅是降低了代码的直接可读性。因为我们有_些格式化工具可以轻松将JavaScrlpt代码变得易读, 比如利用IDE`在线工具或Chrome测览器都能还原格式化的代码°

这里举一个最简单的』avaScript压缩示例。原来的JavaSc门pt代码是这样的: 十u∩〔tio∩e〔ho(5trj∩g∧’ 5trj∩gB){ 〔O∩5t∩a爪e= "Cem》ey"; 己1ert("he11O"+∩a"e);



压缩之后就变成这样子: 「u∩ctjo∩e〔ho(d’c){co∩5te="CerⅦey"j己1ert("‖e11o "+e)}j

曰‖··‖■■□‖‖■■』】■」■■|■■]‖■】|■‖』■‖】句]|』■■

400

可以看到,这里参数的名称都被简化了’代码中的空格也被去掉了,整个代码也被压缩成了-行, 代码的整体可读性降低了°

名带有_些不规则的字符串’同时文件内容可能只有几行’变量名都用—些简单字母表示。这其中就 包含JavaSc∏pt压缩技术’比如一些公共的库输出成bundle文件’一些调用逻辑压缩和转义成冗长的

|』■■勺‖』■可|·』■■】

目前主流的前端开发技术大多都会利用webpack、Rollup等工具进行打包°webpack、Rollup会对 源代码进行编译和压缩’输出几个打包好的JavaScrlpt文件’其中我们可以看到输出的JavaScnpt文件

几行代码’这些都属于JavaSc∏pt压缩°另外,其中也包含了一些很基础的JavaSc∏pt混淆技术’比如 把变量名`方法名替换成—些简单字符,降低代码的可读性°

4」avaSc『|pt混淆

.

厂■

{|||

但整体来说’JavaScnpt压缩技术只能在很小的程度上起到防护作用’要想真正提高防护效果’ 还得依靠JavaScnpt混淆和加密技术。

JavaScnpt混淆完全是在JavaScnpt上面进行的处理,它的目的就是使得JavaSc∏pt变得难以阅读 和分析’大大降低代码的可读性’是_种很实用的JavaSc∏pt保护方案。 JavaSc∏pt混淆技术主要有以下几种°

□变量名混淆:将带有含义的变量名`方法名`常量名随机变为无意义的类乱码字符串’降低代 码的可读性,如转成单个字符或十六进制字符串.

□字符串混淆:将字符串阵列化集中放置并可进行MD5或Base64加密存储’使代码中不出现 明文字符串,这样可以避免使用全局搜索字符串的方式定位到人口。 □对象键名替换:针对JavaScrlpt对象的属性进行加密转化’隐藏代码之间的调用关系° □控制流平坦化:打乱函数原有代码的执行流程及函数调用关系’使代码逻辑变得混乱无序。 □无用代码注入:随机在代码中插人不会被执行到的无用代码’进_步使代码看起来更加混乱。

□调试保护:基于调试器特性’对当前运行环境进行检验,加人一些debugger语句’使其在调 试模式下难以顺利执行JavaSc∏pt代码°

□多态变异:使JavaSc门pt代码每次被调用时’将代码自身立刻自动发生变异’变为与之前完全 不同的代码,即功能完全不变’只是代码形式变异’以此杜绝代码被动态分析和调试。 □域名锁定:使JavaScnpt代码只能在指定域名下执行°

总之’以上方案都是JavaSc∏pt混淆的实现方式’可以在不同程度上保护JavaScript代码。 在前端开发中’现在JavaSc∏pt混淆的主流实现是javasc∏ptˉobfUscator和terser这两个库°它们都

回■■■■■■■■■『广■■■尸■■尸■■尸巴■▲■■■■■『】■■【■‖

□代码自我保护:如果对JavaSc耐pt代码进行格式化,则无法执行’导致测览器假死° □特殊编码:将JavaScript完全编码为人不可读的代码’如表情符号`特殊表示内容,等等。





lll

网站加密和混淆技术简介

40l

能提供—些代码混淆功能,也都有对应的webpack和Rollup打包工具的插件。利用它们’我们可以非 常方便地实现页面的混淆’最终输出压缩和混淆后的JavaSc∏pt代码’使得JavaScrjpt代码的可读性大 大降低。

下面我们以javascriptˉobfUscator为例来介绍—些代码混淆的实现°了解了实现’那么我们自然就

||

对混淆的机理有了更加深刻的认识。

javascrjptˉobfilscator的宫方介绍内容如下: AfTeeandefficjentobfUscatorfbrJavaScript(includingES20l7)Makeyourcodeharderto

copyandpreventpeoplefromstealjngyourwork

它是支持ES8的免费`高效的JavaScript混淆库’可以使得JavaScript代码经过混淆后难以被复 制`盗用’混淆后的代码具有和原来的代码_模一样的功能° } 卜‖〖■『

怎么使用呢?首先’我们需要安装好Nodejsl2.x及以上版本’确保可以正常使用∩p肌命令’具 体的安装方式可以参考: https://setupscrape.center/nodqs。

接着新建一个文件夹’比如jsˉobfUscate’然后进人该文件夹’初始化工作空间: ∩pⅦj∩1t

这里会提示我们输人—些信息’然后创建packagejson文件’这就完成了项目初始化了° 接下来’我们来安装javascnptˉobfilscator这个库: 『}》【■∏〗卜|甘

∩p"1 ˉDjaγa5〔Iiptˉob+u5c日tor

稍等片刻,即可看到本地jsˉobfUscate文件夹下生成了_个nodemodules文件夹(如图llˉl所示)’

白||血》卜『|■β||β尸『|〖〗「|‖■『‖||卜■卜

里面就包含了」avasc∏ptˉobfUscator这个库’这就说明安装成功了。 ●●●



〉jB=山fuSc“

沁吨

芦…Q…窜j$凹

Bpm…叫mk.协α` √■∩…∏mu·S 》■@∩四咖 〕■回·砸■ˉ咖乙tch 》■■帕y磊四茁m 》■m询 》■印■『■比 〕■匝吟…瓤癣m 》■m0侮『ˉ0呵"



》■四盯七“Ⅶ∩

≡e 贮℃川…饰d



低蛔

s!出

泅■γ●t06萨53

275叮适9

」so佃

γ记w吐妈:63 Ⅵm叮狱?5;5S 陶d的拯悔:sS 7°dw·t?S芍3 γ碱■v醚掐蝎3 Vmw·『?663 …喊滴:63 Ym●γ引幅.53 γhq卸碱渺68

32ⅨB

」S酗

Vm■v□《‖6;63

…■γm谗63 γ四叮ml5!63 Y诅叮硕愉53 『“●γ●↑帽:53

》■碎m↓S

γ…·t馅:日s

》■『mtˉmmSh喊∩ 》■αγ献 》■■b酌O■● 〉■四切『ˉ∩□『∏● 》■f耻∩ct肋`~慨叼 ■m●ˉ0叼

而dw·t↑5:53 γ…碰掩:63



=》、夕

》■■vwˉ删粕『 》■■G◎…tˉ蜘以细 》■创叼~…x 》■印卧●tym

■徊mc∩

》Q

霉√啦.°臂

γ函的●t↑5;63

γ…碱怒53

ˉˉ



仁OEα

ˉ= 『@撼韧 □ˉ

F◎℃叮

…尸喻撼刨 ●·

仁@鲤罕

=…■ 尸c她田

… ●■

尸O…

侈喇够

■●

●←

●■

黑: 翻

!蕊 瞧

…γ■t05;63

仁Q划僻

Ⅶ…碱临:53

尸@妇α

了m●v●『↑5853

仁◎妇钮



图llˉl jsˉobfUscate文件夹 接下来’ 我们就可以编写代码来实现一个混淆样例了°比如’新建mainjs文件,其内容如下:



〔o∩5tcode= 1etx= 010 +1

〔o∩so1e.1og(|x‖’ x) 〔o∩5tOptiO∩5={ 〔OⅦpa〔t: 千a15e’ 〔o∩tro1「1叫「1atte∩i∩g }

true

〔o∩stob+(」5c己toI=req|」ire(‖jaγa5criptˉob「(」5〔ator‖) 仙∩ctjo∩obfu5cate(code’ optjo∩s){ retur∩ob于05cator。ob+u5〔ate(code’ optjo∩s).get0b十uscated〔ode() ] 》



〔o∩5o1e。1og(ob「u5cate(〔ode’ optio∩5))

』·』■■司』■■】{|■■|■■■(■■|』‖|■■】‖‖】■司‖』日」‖]|■■可{□‖□■可□】□(

第ll章JavaScript逆向爬虫

402

这里我们定义了两个变量:一个是〔ode,即需要被混淆的代码;另-个是混淆选项optjo∩5’是

一个Object°接下来’我们弓|人了javascrjptˉob血scator这个库,然后定义了一个方法,给其传人code 和optjo∩5来获取混淆后的代码,最后控制台输出混淆后的代码。



∩ode"a1∩.j已

输出结果如下:

q

γar ox53b千二 [`1o8‖]j (十u∩ctjo∩ (一0x1d84+e’ Ox3aedaO){ γar

0x1d8斗十e[!pu5h|](-ox1d84千e[‖shift{]());

0x4M1e5=

Ox4341e5 ˉ0xOj

O×b3622e= Ox53b「[O×4341e5];

retur∩

Oxb]622ej

}i 1etX= |10 +Ox1j

co∩So1e[ Oxq8O日(‖0xO』)](‖x‖」 x)j

看到了吧’那么简单的代码’被我们混淆成了这个样子’其实这里我们就是设定了“控制流平坦

化”选项°整体看来’代码的可读性大大降低了’JavaSc∏pt调试的难度也大大加大了°

注意由于这些例子中调用javascrjptˉobfUscator进行混淆的实现是一样的’所以下丈的示例只说明 code和optjo∩5变量的修改’完整代码请自行补全。 ●代码压缩

这里javasc∏ptˉobfUscator也提供了代码压缩的功能,使用其参数coⅦpact即可完成JavaSc门pt代 码的压缩,输出为一行内容。参数co"pact的默认值是true’如果定义为千a15e’则混淆后的代码会 分行显示°

示例如下: 1etX= 010 +1

CO∩5O1e.1Og({x|」 X)

』|』■∏‖■∏

〔o∩5t〔ode=`

q

■■可』■口■■‖‖二■■■■■‖』■可|■■‖■■■||当■∏』·■■■■可

好’那么我们来跟着javasc∏ptˉobfilscator走_遍’就能具体知道JavaSc∏pt混淆到底有多少方法了°

=■■□■∏■可‖】■|□■■臼■■■司■■】■司

}j 0X10a5日(++OX3aed己O)j }(0x5〕b+’ Ox172))j γ日r ox48oa=+u∩ctio∩(o叫3q1e5’ ox5923b4){



·■■∏|■』■■』■

Ox1O己5己≡「u∩Ct1o∩(Ox2十Oa5∑){ "hj1e (ˉˉO×2+Oa52){ }

γ己r

‖勺|

代码逻辑比较简单,我们来执行_下代码:

「卜卜}‖



p

ll.l

网站力口密和混淆技术简介

403

〔O∩5tOpt1O∩5={ 〔o们Pa〔t; 千315e }

这里我们先把代码压缩选项的参数〔o们pact设置为十a15e,运行结果如下 b

1etX= 01| +0x1;

co∩5o1e[|1og0 ](0×‖’ x)i

卜尸[

如果不设置〔OⅧpaCt或把CO∏‖pa〔t设置为tn』e,结果如下: var Ox151c≡[01og|];(十u∩〔tio∩(Ox1〔e384’ Ox2Oa7c7){v日r-Ox25千〔92=千u∩ct1o∏(-Ox188ae〔){咖i1e(ˉˉOx188己e〔)

{0风1ce384[』push』](ˉox1ce384[』5∩j千t』]())了}}jˉo)《25+c92(++ox2oa7c7)j}(0Ⅸ151〔′ox1b7))j`′ar-ox553e=「u∩ctjo"

(ox259219’ox201“5){0x259219≡Ox259219ˉox0;γar Ox56d72d==ox151c[←0x259219]】retur∩ ox56d72d;}j1etx=

’1|+O)〈1;co∩5o1e[0x553e(00x0|)](丁x!’x);

■■尸■■’‖仔■‖

可以看到,单行显示的时候’对变量名进行了进—步的混淆,这里变量的命名都变成了十六进制 形式的字符串’这是因为启用了一些默认压缩和混淆配置°总之’我们可以看到代码的可读性相比之 前大大降低了。 ●变量名混淆

变量名混淆可以通过在javasc∏ptˉobfhscator中配置ide∩ti十jer‖aⅦe5Ce∩emtor参数来实现°我们 通过这个参数可以控制变量名混淆的方式’如将其值设为∩exade〔j‖a1’则会将变量名替换为十六进

□们a∩g1ed:将变量名替换为普通的简写字符’如a、b` c等° 该参数的默认值为hexadeCj刚日1°

我们将该参数修改为Ⅷa∩g1ed来试一下: 〔o∩5t〔ode=`

1ethe11o= ‖10 +1

〔o∩5o1e.1og(‖he11o0 ′ he11o) 〔O∩5tOptjO∏s={



〔α∏pa〔t: true’ jde∩ti千jer‖a爬5Ce∩erator: |‖a∩g1ed }

运行结果如下: Ⅲ 日

×

γ





■■=





α

{〔 )

∩ ○

十巴

·]

·□

〔 ∩ 咽

var3≡[,∩e11o|]i(+u∩ctjo∩(〔’d){γa】

e=+u∩〔tio∩(+){"hj1e(ˉˉ十){c[』pu5h,](〔[′5hi于t,]())j}};e(什d)】}(a’0x9b)〉;γar e=a[c]jretur∩e;}β1ethe11o≡』10+0x1;〔o∏so1e[,1og{](b(!0xo!)’‖e11o)i

千□

△■尸||〗■■■似《■=■厂||『已■厂‖■■|‖|



□hexade〔j∏a1:将变量名替换为十六进制形式的字符串’如0xabc123°

(〔

》卜β‖■■口尸·|■『■「〖■’|■■}‖■■「|巴■厂‖●■尸}●「[■厂



制形式的字符串。该参数的取值如下°

可以看到’这里的变量名都变成了a、b等形式°

如果我们将jde∩tj千ier‖a"e56e∩erator修改为∩exadecj‖a1或者不设置’运行结果如下: γaⅢ ox』e98=[,1og0’|∩e11o0];(于u∩〔tjo∩(0x“6』de’Ox39de6c){γ3r

0xd卉dd己=于u∏〔tio∩(ˉOx6■95d5){灿i1e(≡ˉ0x6a95d5){一0×4q64de[,pu5h`](ˉoד6“e[05∩i什′](〉)j}}j=oxd仟dda(什

0x39de6〔);}(ˉOxqe98’ox〔8));γar_Ox53cb=十u∩〔tjo∩(ˉOx393bda’ 0x8So4e7){-ox393bda≡ox393bda≡0xo;vaI 0xq6ab8o霉ox‖e98[0x393bd己]βretumo叫6己b8o;}β1et

he11o=010盯x1jco∩5o1e[一Ox53cb(,OⅨO!)](-0x53cb(00x1』)’∩e11o);

可以看到,选用了Ⅷa∩g1ed,其代码体积会更小,但选用hexadeciⅦa1的可读性会更低° 另外,我们还可以通过设置ide∩ti于jer5pre+iX参数来控制混淆后的变量前缀,示例如下: 〔o∩St〔ode= ~

1et∩e11o= 010 +1

〔o∏5o1e.1o8(』he11o『’ he11o)

尸■

‖‖‖(

第ll章JavaScript逆向爬虫

404

〔O∩5tOPtiO∩5={ 1de∩tj+1er5pre千1x; 08er∏ey }

varger爬yˉ0x3dea=[01og‖’ !∩e11o|]j(千(」∩〔tjo∩(Ox3q8仟3’ Ox533Oe8){γarˉ0x1568b1=十u∩ctio∩(Oxq740d8){"hj1e

(ˉˉˉo×470od8){ox348仟3[』pu5h』](o》G48仟3[』5∏j+t』]())j}}; ox1S68b1(++ˉ0x5330e8)i}(gemeγ二0)《3dea’ox9耳))j

γaIgemey=ox30e4=十u∩〔tjo∩(oxne8+7c’ o×1o66a8){ox2e8千7c=0x2e8+7〔ˉo×0jγar o)《5166ba=gemey=ox3dea [ˉOx2e8十7c]jretur∩ 0x5166baj};1ethe11o=‖1!+0x1;co∩so1e[gem记yˉ0x30e4(‖0×O‖)](geI爬y—O)(3Oe4(00x10)’he11o);

可以看到’混淆后的变量前缀加上了我们自定义的字符串ger"eγ° 如下:

q

」||勺

另外, re∩a"eC1oba15这个参数还可以指定是否混淆全局变量和函数名称’默认值为十a15e°示例

旦 ■ ■ ‖ ‖ ■ ■ ‖ ‖ · ■ ‖ ‖ ■

运行结果如下:

〔O∩St〔Ode= 、

γar$≡+u∩CtiO∩(id){

retuI∩do〔u|∏e∩t。get[1e们e∩t8yId(jd)j }j

re∩a‖∏eC1oba1s: tIue



γar 0x↓86qbo二十u∩〔tio∩(o×5763be){retuI∩do〔u刚e∩t[‖get[1e爬∩t8yId‖](ˉox5763be);}j

可以看到,这里我们声明了一个全局变量$’在re∩a们eC1oba15设置为tn」e之后’$这个变量也

被替换了°如果后文用到了这个$对象,可能就会有找不到定义的错误,因此这个参数可能导致代码 执行不通。

如果我们不设置re∩a川eC1oba15或者将其设置为十a1Se,结果如下: γ己r Ox2393=[‖get[1e阳e∩t8yId‖];(「u∩〔t1o∩(Ox3十45a3’ O×583d十a){var 0x2〔日de2=千u∩ctio∩(ˉOx28479a){"∩i1e (ˉˉOX28479a){_OX3+45己3[ ′pUSh』](ˉOx3十45a3[ 05‖j+t|]())j}}j 0x2Cade2(++0XS83d十a)j}(Ox∑39a’0xe1))j



ret‖r∩ Ox531b8dj}iγar$≡+u∩〔tjo∩(Ox3d8723){Ietur∩do〔uⅧe∩t[ 0×3758({O×0`)](一Ox3d8723)j}j

可以看到,最后还是有$的声明,其全局名称没有被改变。 ●字符串混淆

字符串混淆,即将—个字符串声明放到_个数组里面’使之无法被直接搜到°这可以通过

5tr1∩g∧rmy参数来控制’默认为true°

此外,我们还可以通过rotate5trj∩g∧rmy参数来控制数组化后结果的元素顺序’默认为tIue° 还可以通过5tr1∩g∧rIay[∩〔od1∩g参数来控制数组的编码形式,默认不开启编码°如果将其设置为true或

ba5e64’则会使用Base64编码;如果设置为rCq’则使用RC4编码°另外,可以通过5tri∩g∧∏ay『∩re5∩o1d 来控制启用编码的概率’其范围为O到1’默认值为O.8。 示例如下: co∩5t〔ode≡ 、

■■·‖·Ⅵ』■■■司■■』〗』■Ⅵ■■〗刮‖■‖|』■■∏■■‖■■·|日‖|■■司」■可■■■■■■二■■■|■■■}‖『■■∏』■可」■■回

运行结果如下:

■■●■■可』■

co∩5toptjo∩5≡ {

γara= ‖he11owor1d|

】‖

|‖]●|■‖‖■]】■■■]‖■】|己』■■■■

〔O∩5tOPt1O∩5={ Stri∩g∧rray: true』 rotate5tri∩gArray; true’ 5tr1∩g∧rmy[∩〔odj∩g: trl』e’ // |b日5e64|或|r〔』0或千a15e 5tr1∩gArray『hre5ho1d: 1’ }

■■■■■■『》【■■■‖〖‖[尸■)||巳■厂||}■『『‖|■尸「卜■『‖

ll.l

网站力口密和混淆技术简介

405

运行结果如下:

var ox4215=[‖日Cγ5bC8gd29γbOQ≡! ]j(十q∩〔t1o∩(o×躯b千17’ 0x4〔348十){v日r o×328832≡十u∩〔tjo∩(o×355be1){Ⅳhi1e (ˉˉ0x355be1){0x42b+17[ ,pu5h0 ](0x42b十17[‖5门1千t0 ]())j}}; 0x328832(++ox4c3〃8+)j}(0x4215’ox1d己〉)j

尸卜■『『′·尸■尸『||》■■尸|卜『匹尸旧■尸|口β|卜》′口『》●「『’′■尸卜口「「|■′》|′■}ββ卜巴■「|■■「■「||》■『|)|[尸『「|匹尸(卜}巴尸「

γ3r 0x5191≡千u∩ctjo∩(0x3c+2ba’ ox1917d8){Ox3〔十∑ba≡ox3〔十2baˉox0;γ日r ox1千93+o≡ox4215[ 0x3〔十2ba]j 1十(0x5191[ ′lqbγ0‖′ ]=≡u∩de千i∩ed){〈十u∩〔tio∩(){γar ox5O96b2;try{v日rˉox282db1=「u∩ctio∩(‖retur∩\x2o (十u∩〔t1o∩()\x2O0+0{}.〔o∩5tIu〔tor(\x22retur∏\x2Ot‖j5\x22)(\x2O)|+0 )i ‖ )i 0x5O96b∑=Ox282db1()i}〔at〔h (Ox2己cb9〔){0x5O96b2=wi∩dowj}γ己r Ox388〔14=‖∧8〔0[「C‖I〕队"‖0p0R5丁0ⅦXγZab〔de十ghij代1们∩opqr5tuvwxγz

Oi23456789+/=0 j Ox5O96b2[|己tob0]||(Ox5O96b2[|atob|]=千u∩〔tio∩(O×4cc27〔){var Ox2a十4ae=5tr1∩g(-Ox4cc27〔〉

[ 』rep1a〔e』](/≡+$/′』 』 )j+or(γar ox214oob=0xo’0x3千4e2e’ o×5b19〕b’ 0x233381=oxo了ox3dcc十7≡』』 j o讽5b193b= ox2a+qae[‖〔har∧t‖](_0x233381++)广0x5b193b88(0x3于4e2e≡ox214oob油x4?ox〕榔e2e*0x40+0x5b193b: 0x5b193b’ ox214OOb++油x』)?0x3d〔c千7+≡5tri∩g[干ro「∏〔har〔ode0 ](0×仟8ox3千4e2e〉〉(ˉo×2*ox214oob80x6)):0xo){ox5b193b= 0x388〔14[0i∩dex0+‖](Ox5b193b)j}retur∩ 0x3‘cc于7j})j}()); 0x5191[ 』DuIuI丁|]=千u∩〔tio∩(ˉOx51888e)

{γar o×29801千≡atob(二0x51888e)War ox561e62=[]j+or(var ox5dd788=o×o’ ox1a8b73≡ox298o1千「1e∩gt‖』];

0x5dd788〈0x1a8b73j 0x5dd788++){0x561e62+=』%{+(‖0o‖+ox298o1+[0〔har〔ode∧t0](ox5dd788)[|to5tri∩g‖](ox1o 〉)[‖51i〔e0](ˉ0x2)j}retur∩decode0RI〔o川po∩e∩t(Ox561e62)j}j 0x5191[ 0‖go即d‖]={}j OxS191[|[qWD什|]=!|[]j}vaI 0x1741+o≡0x5191[|吧o8Rd‖][0x3c于2ba]jj千(0x1741十o=≡u∏de+i∩ed){0x1+93「o=ox5191[00uIur『|](0x1+93千o)j

0x5191[ 』"go8佣d』][0x3〔十2ba]=0x1千93千O;}e15e{ox1于93+0=ox17』1+0;}retur∩ 0x1+93+oj};γara≡o×5191(』Oxo』)了

可以看到,这里就把字符串进行了Base64编码,我们再也无法通过查找的方式找到字符串的位 置了°

如果将5tr1∩g∧∏ay设置为十a15e的话,.输出就是这样: var日=0he11o\x2o"or1d0 j

字符串就仍然是明文显示的,没有被编码。

另外,我们还可以使用u∩1〔ode[5cape5eque∩ce这个参数对字符串进行Unicode转码,使之更加 难以辨认’示例如下: 〔o∩st〔ode= `

γara= !he11OwOr1d|

〔O∩StOptjo∏5={ 〔O刚pa〔t: 十日15e’ l」∩i〔ode〔5〔日pe5eque∩ce: true

运行结果如下: γ日r 0x5〔0d= [』\X68\x65\x6〔\X6C\x6+\x20\X77\X6+\X72\X6C\X640 ]j (fu∩〔tjo∩ (0x54〔〔9〔’ 0x573〕b2){ var 0x十833c+=十u∩〔tio∩(Ox3cd8c6){

冈hj1e(-0x3Cd8C6){

ox54〔c9〔[ ‖pu5h‖ ](-ox54cc9〔[ 05hj十t0 ]())j }

}j ox「833〔千(十+o×57a3b2)j }(ON5〔Od’ 0x17d))j γar 0x28e8=fu∩〔tio∩(Ox3+d645’ Ox2C十Se7){

匹=■■尸‖′’=尸『}|||=■■~■『‖·′|‘■=■=∏||’止■『■■■口|匹■【■■「『

O×3fd645=

Ox3于d645 ˉ 0xO;

γ日I Ox298a20=-0x5〔Od[-Ox3「d645]】 retur∩

}i γara二

O×298a20j

0x28e8(‖Ox00);

可以看到,这里字符串被数字化和Unicode化’非常难以辨认°

在很多JavaScnpt逆向的过程中’_些关键的字符串可能会作为切人点来查找加密人口°用了这 种混淆之后,如果有人想通过全局搜索的方式搜索∩e11O这样的字符串找加密人口,也没法搜到了° ●代码自我保护

我们可以通过设置5e1十0e十e∩d1∩8参数来开启代码自我保护功能。开启之后’混淆后的JavaSc∏pt 会强制以-行形式显示°如果我们将混淆后的代码进行格式化或者重命名,该段代码将无法执行°



虫 爬 向 逆



°∏



α γ

^·





] 〗





β 斗‖

示例如下: 〔o∩5tcode= 、

〔o∩5o1e°1og(!们e11owor1d|〉 〔o∩5toptiO∏S={ 5e1+0e「e∩di∩8: true }

运行结果如下:

{0data0 :{,key0 : 0coo∏e0 ’ ‖γa1ue0 :|ti爬out』}’』5et〔oo促je‖:+u∩〔t1o∩(Ox2d52d5’ 0x16千eda’ 0x57〔ad干’ 0x56056十)

=勺‖‖划

var Ox26da=[ 』1og‖’ 0he11o\xRo"or1d‖]j(十u∩ctio∩(~Ox190327’ 0x57c2〔O){γ己r ox577762二+u∩〔tio∩(ˉ0x〔9dabb) {"hj1e(ˉˉ-ox〔9dabb){Ox190317[|pu5h‖](-Ox190327[ 05hi十t‖]());}}jγaI Ox35976e≡+u∩ctjo∩(){γaI 0x16b3十e≡

{ox56056+=ox56o56千||{}jγaI 0x5b6dc3=0x16十eda+‖=0+ox57〔ad「;v5r ox333〔ed=oxoj十or(γar 0x〕〕3〔ed≡oxo’ ox19ae36=0x2d52d5[!1e∩gth0]i-ox333〔ed〈ox19ae36j ox333〔ed++){γar ox4o9587≡Ox2d52d5[ 0x333ced]j

0x5b6d〔3+=‖j\x2o0+ox4o9587;var ox』a3oo6≡0x2d52d5[0x409587]j_Ox2d52d5[!pu5h|](-0xqa5oo6)j 0x19ae36=

cou∩ter0 )j}e1se{Ox16b]十e[|re"oγe〔oo促ie0 ]()j}}j 0x35976e(〉】}(Ox26da’Ox14O))jvaI Ox4391=十u∩〔t1o∩



□ 叮 · 、 当 ■ Ⅵ | ■

γar oxq673〔d=+u∩〔tjo∩(〉{γaIˉ0x』〔6〔5〔=∩e"日e8[xp(,\x5〔w+\x2o*\x5〔(\x5c)\x∑0*{\x5c"+\x2o*[\x27|\x22]· +[\x27|\×22]j?\x20*}』);retuI∩ Ox4C6〔S〔[,te5t,](0x16b3千e[‖reⅧγe〔oo瞩1e0 ][ ‖to5trj∩g, ]());}; 0x16b3+e [ 0update〔oo挝e0]=ox4673〔divar ox5baa80=mjγaI 0x1+a+19≡0x16b3+e[,update〔oo促ie‖]()ji+(| 0x1+己十19) {0x16b3十e[ 05et〔oo代1e‖]([ ‖*0 ]’0〔ou∩ter‖’Ox1)j}e15ej千(Ox1+a「19){ˉOx5baa8O=Ox16b3十e[ 0get〔oo低ie0](∩u11」

勺■

0×2dS2d5[1e∩8t∩‖]ji「(ox4aaoo6|≡!|[]){-o×5b6dc3+=‖≡0+ox4aa0o65}}o×56o56+[,coo促ie‖]≡o×5b6d〔3〗}’ Ie‖∏ove〔oo低je|:十u∩〔tjo∩(){retur∩0deγ0 ;}’0get〔oo促je‖:千u∩〔tjo∩(ox3Oc497’ O×51923d){0x3O〔497≡0x30〔』97|| 十u∩〔tjo∩(Oxqb7e18){retuI∩ 0x4b7e18j}jγar 0x557e06=0x3oc』97(∩e"Re8[xp(0(?:^|j\x∑0〉0+ox51923d[ ‖rep1ace‖ ] (/([.$?*|{}()[]\/+^])/g’0$1!〉+‖≡([^;]*)0)〉;γar ox817646=十u∩ctjo∩(0x千〕+ae7’ ox5d8208){ox「3+ae7(什ox5d8208);}j 0x817646(ˉ0x577762’ ox57〔2〔o)jIetur∩ ox557e06Mecode0RI〔oⅦpo∩e∩t(0x557e06[ox1]):u∩de+j∩edj}}j





(ˉox1b42d8’ 0x57ed〔8){ˉox1b42d8=ox1b42d8ˉoxojγar 0x2+beca=0x26da[ 0x1b4m8]jretur∩ 0x2千beca;}j

γaI 0x197926=千u∩〔t1o∩(){γaI-0x1O598十≡! ![];retuI∩「u∩〔tjo∩(Ox仟己3b3’ 0x7a40十9){γar Ox48e571= 0x1o598千?+u∩ctio∩(){j十(Ox7日』0+9){γar ox2194b5=ox7a4o+9[『app1y0 ](ˉOx仟a3b3’arguⅧe∩ts)i 0x7己40+9=∩u11j ret0r∩ ON2194b5i}}:十u∩〔tjo∩(){}j-Ox1O598千=![]jretur∩ Ox48eS71j};}()jvar 0x2〔6+d7≡0x197926(th15’ +u∩〔tio∩(〉{v日rˉ0x4828bb=+u∩〔tio∩(){retur∩,\x64\x65\x76‖;}′ˉ0x〕5〔3忱=+u∩〔tjo∩(){retur∩0\x77\x69\x6e\x6』\ x6「\x77‖ j}ivar ox456o7o=「u∩〔tjo∩(){var ox4576a4二∩e"Re8[xp(!\x5c\×77\x2b\x20\x2a\x5〔\x28\x5〔\x29\x20\ x2a\x7b\x5〔\×77\x2b\x2O\x2a\x5b\x27\x7〔\x22\x5d\x2e\x2b\x5b\x27\x7〔\x22\x5d\x3b\x3十\x2O\x2a\x7d0 )j

x4「\x660 ](|\x69』二≡ox2a6361)){ox〔388〔5(一0x58十dbq)i}}jvar Oxc388〔5=+u∩〔tjo∩(ox2o73d6){γaI 0x6bb49「= ~ˉox』〉〉ox1+0x仟蜘xojjf(ox2o73d6[ 0\x69\x6e\x6q\x65\x78\x』千\x66‖]((! ![]+‖)[0x3])!≡ˉox6bb49f){ox2d9a5o (ox2o73d6);}};j十(! ox啡56o7o()){i+(| ox3于de69()){ˉ0x2d9a5O(』\x69\x6e\x6』\uO435\x78\x』+\x660); }e15e{Ox2d9a50〈0\x69\x6e\x64\x65\x78\x4十\x660 )j}}e15e{Ox2d9a5O(0\x69\x6e\x64\uO435\x78\x4+\x66,)j

‖··

γar ox2d9己5o≡十u∩〔tio∩(o×58fdb4){γaI ox2日6361=≈ˉox1〉〉0x1十ox十+油xo】1+(ox58+db』[‖\x69\x6e\x64\x65\x78\

|{

Ietur∩! o叫576a4[0\x7q\x65\x73\x74‖](-ox』828bb[‖\x74\x6+\x53\x74\x72\x69\x6e\x67, ]())j};γar 0x3+de69= 千u∩〔tjo∩(){γaI oxabb6仙=∩ew【eg[Np(』\x28\x5〔\x5〔\x5b\x78\x7〔\x75\xSd\x28\×5〔\x77\x29\x7b\x32\x2〔\x34\ x7d\x29\x2b‖)jretur∩ 0xabb6伺[ 0\x74\x65\x73\x74‖](Ox〕5〔3b〔「\x7q\x6十\x53\x7』\x72\x69\x6e\x670 ]())j}j

}})j ox2〔6+d7()j〔o∩5o1e[0×4391(|ox0,)](ox4391(‖0x10))j

如果我们将上述代码放到控制台,它的执行结果和之前是一模_样的,没有任何问题° 如果我们将其进行格式化’然后贴到测览器控制台里面’测览器会直接卡死无法运行。这样如果 有人对代码进行了格式化’就无法正常对代码进行运行和调试’从而起到了保护作用°

| ■

环逻辑,这导致整个执行逻辑十分复杂、难读° 比如说这里有一段示例代码:



||

控制流平坦化其实就是将代码的执行逻辑混淆,使其变得复杂、难读°其基本思想是将一些逻辑 处理块都统一加上-个前驱逻辑块,每个逻辑块都由前驱逻辑块进行条件判断和分发,构成—个个闭

』|

●控制流乎坦化

q

Co∩5O1e.1Og(〔)j CO∩SO1e°1Og(a); co∩5o1e。1og(b)历

代码逻辑—目了然’依次在控制台输出了C、a` b三个变量的值°但如果把这段代码进行控制流 〔O∩5t5= "3|1|2口.5P1it("|")j 1etX≡0;

(‖‖(

平坦化处理’代码就会变成这样:





ll』 网站加密和混淆技术简介

卜卜

◆■||‖●厂|『「‖■||炉『‖〗「■■「|■■「}|■■「|『■■尸|‖『■尸 》·尸 p

407

"h11e (true){ 5训jt〔h (5[X++]){ 〔a5e"1":

co∩5o1e.1og(a)j CO∩ti∩uej 〔a5e 002":

〔o∩5O1巳1O8(b〉j 〔O∩ti∩uej 〔a5e"300:

〔o∩5o1e.1og(c)j CO∩ti∩uej

} bre冰j



可以看到,混淆后的代码首先声明了一个变量5,它的结果是一个列表,其实是["3"’"1"’"2"]’

然后下面通过5"jt〔h语句对5中的元素进行了判断,每个〔a5e都加上了各自的代码逻辑。通过这样 的处理,一些连续的执行逻辑就被打破了’代码被修改为_个5W1t〔h语句’原本我们可以一眼看出的 逻辑是控制台先输出C’然后才是日` b,但是现在我们必须结合5WjtCh的判断条件和对应Ca5e的内 容进行判断,我们很难再-眼看出每条语句的执行顺序了,这大大降低了代码的可读性°

在javasc∏ptˉobfUscator中,我们通过co∩tro1「1叫「1atte∩j∩g变量可以控制是否开启控制流平坦 化,示例如下:

‖β

〔O∩5toptjO∩5≡{ C咖paCt:「a15e’

广|■尸仁尸『‖‘卜「‖「●卜|

〔o∩tro1「1叫「1atte∩i∩g; true }

使用控制流平坦化可以使得执行逻辑更加复杂、难读’目前非常多的前端混淆都会加上这个选项。 但启用控制流平坦化之后,代码的执行时间会变长’最长达l.5倍之多°

另外’我们还能使用〔o∩tro1「1o倒「1atte∩i∩g丁hre5ho1d这个参数来控制比例’取值范围是0到l’ 默认值为075°如果将该参数设置为0,那相当于将co∩tIo1「1ow「1atte∩j∩g设置为十a15e’即不开启 控制流扁平化°



●元用代码注入





无用代码即不会被执行的代码或对上下文没有任何影响的代码,注人之后可以对现有的JavaSm队 代码的阅读形成干扰。我们可以使用dead〔odeI∩je〔tjo∩参数开启这个选项,其默认值为十a15e°



P

比如’这里有—段代码:



〔o『]5ta≡「u∩〔tjo∏(){

卜)|



〔o∩5o1e.1og(憾he11omr1d阑); };

〔o∩5tb≡「u∩ctio∩(){

co∏so1e.1吧(口∩j〔eto∏比etyou口); }; a(); b()〗

泣里声明了方法a和b,然后依次进行调用,分别输出两句话° 经过无用代码注人处理之后’代码就会变成类似这样: 〔o∩5t 0x16〔18d=+u∩〔tio∩(){ j「(!|[[]]){

〔o∩5o1e.1og(0!∩e11oNor1d蕊)i }e15e{ 〔o∩5o1e·1o8(撼th15啸)j



q





第ll章JavaScript逆向爬虫

408



」 〔O∩5O1e.1Og〈"15")j co∩5o1e。1og("dead")j co∩5o1e.1og(阐code")j }

}; 〔o∩5t

Ox1十7292=+u∩〔tio∩ (){

j于("×川γ2∩0d十y2‖".char∧t(4) !=5tri∩g。+ro『∏〔har〔ode(11O)){ 〔o∩5o1e.1og(圃thiS圃)j 〔o∩5o1e.1og("j5曰)j 〔o∩5o1巳1og(圃dead阐)j 〔o∩so1e.1og("〔ode0!)j



{|





}e15e{

co∩5o1e°1og(』0∩i〔eto"eetyou腻)j }j

Ox16〔18d(〉i

二Ox1千7292()j

可以看到’每个方法内部都增加了额外的1+…e15e语句’其中j十的判断条件还是一个表达式’ 其结果是true还是伯15e我们还不能一眼看出来’比如说0xlf7292这个方法,它的i千判断条件是: "x"v2∩0dfy2‖0|.char∧t(4) !≡≡5tr1∩g.十rα恤har〔ode(110)

在不等号前面其实是从字符串中取出指定位置的字符,不等号后面则调用了+ro‖〔har〔ode方法来

根据ASCII码转换得到-个字符,然后比较两个字符的结果是否是不_样的。前者经过推算’我们可 以知道结果是∩;但对于后者,多数情况下我们还得去查_下ASCII码表,才能知道其结果也是∩。 最后两个结果是相同的,整个表达式的结果是十a15e,所以1十后面跟的逻辑实际上就是不会被执行到 的无用代码’但这些代码对我们阅读代码起到了—定的干扰作用° 因此,这种混淆方式通过混人_些特殊的判断条件并加人_些不会被执行的代码,可以对代码起 到_定的混淆\干扰作用°

在javasc∏ptˉobh』scator中’我们可以通过dead〔odeI∩jectjo∩参数控制无用代码的注人’配置如下: 〔O∩5tOpt1O∩5={ CO∩‖paCt; 「a15e’ de3d〔odeI∩je〔tjo∩: tn」e }

另外,我们还可以通过设置dead〔odeI∩je〔t1o∩丁hre5∩o1d参数来控制无用代码注人的比例°该参 数的取值范围为0到l,默认值是O4°



‖‖‖□□■‖]‖·】■|‖□]||』■□□】|■·』·]||■■



q



」 q

d q

』 q q

q

0□

』|

{ {



0

●对象键名替换 ‖|

如果是_个对象’可以使用tra∩5+om旧bje〔t促ey5来对对象的键值进行替换’示例如下:



co∩st〔ode=

baⅢ: ‖te5t20



}i })()j 〔O∩StOpt1O∩5≡ { 〔o"p日〔t: +己15e’



tm∩5+or巾bje〔tRey5: tIue



输出结果如下:

q

q

|■■■■■■]】■■■■■】‖』■■】●】」·』■〗|=■‖】勺』■■■|』■〗‖■∏‖]■□■■引」■‖」■·】】■■

(千u∩Ct1O∩(){ γarobject={ 十OO: 0te5t1‖ ’ bar; {

}}「}「)

ll.l

网站加密和混淆技术简介

409

Ox7a5d≡ [

γar

0bar‖ ’ 0te5t2! 』 ‖te5t10

















]j (十u∩ctjo∩(ox59+ec5’ 0x2e4十a〔){ γar 0x231e7吕=+u∩〔tio∩(0x46+〕3e){ "M1e(ˉˉOx46十33e){ 0×59千e〔5[ ‖Pu5h‖ ](Ox59+ec5[‖5hi千t! ]())j l



}j Ox231e7a(++-ox2e4十aC); }(0x7a5d′ O×167))j γar 0x3bc4=千u∩〔tio∩(ox3o9ad3’ ox22d5ac){ Ox3O9ad3= 0x〕09己d3 ˉ0xOj

γar 0x3a034e= o×7a5d[≡0x309ad3]j



Ietur∩



} ‖

0x9+1十d1[ˉOx3b〔4(‖0x1!〉] ={}; 0X9千1十d1[ 0X3b〔4(‖Ox1′)][{baZ’] 三

「 p







p 卜





F









0X330Me;

}】 (千u∩〔tjO∩(){ γar 0×9十1+d1={}; 0x9「1+d1[|+oo!] = ox3bc4(!o×o|);

0X3b〔』(‖0X2|)j

}())j

可以看到,Ohject的变量名被替换为了特殊的变量’代码的可读性变差’这样我们就不好直接通 过变量名进行搜寻了’这也可以起到一定的防护作用。

●禁用控制台输出

我们可以使用d15ab1e〔o∩5o1e0utput来禁用掉〔o∩5o1e.1og输出功能’加大调试难度,示例如下: co∩5tCode=

co∩5o1e.1og(!∩e11o"or1d|) ≈

CO∩5tOpt1O∩S≡ { di5ab1e〔o∩5o1e0utput: tr0e ] J

运行结果如下:

vaI ox3a39=[ 0debug,’ 』j∩+o! ’ 0error,’ 0ex〔eptio∩{ ’{tm〔e!’!be11o\x2侧or1d!’!app1y』’‖{}.co∩5tru〔tor (\x2Iretur∩\x2ot∩i5\x22)(\x2o)|’‖〔o∩5o1e! ’ ,1o8′」War∩『];(「u∩ctio∩(ˉox2日157a’ 0x5d9d3b){γaIˉox488e2〔≡

↑↑

千u∩〔tio∩(ˉox5bcb73){Nhi1e(ˉˉox5b〔b73){≡ox2a157己[’pu5h0](-ox2己157a[ 』5M+t』]());}}j-0x488e2〔(++ˉox5d9d3b); `

}(-ox〕a39’0x1oe〉)βγar一ox5b仟=+u∩〔tjo∩(ˉo×43bd十〔’ 0x52e4c6){一o×43bd十c=ox43bd十cˉoxojγar 0xb67384=ox3a39 [ˉox↓3bd+c];retur∩ oxb6738』;};γar Ox349b01=+u∩〔t1o∩(){γar-ox1十48』b=|![]jretur∩+u∩ctio∩(0x5e+eod’ 0x33db62){γaI卫x2ob〔d2≡ox1千484b汗u∩ctjo∩(){j十(ˉox33db62){v日r-0x77o54〔=0x33db62[0x5b仟(『0xo』)] (ox5e+eod’argu爬∩t5)j 0xi3db62=∩u11jretur∩ ox77054ci}}:十u∩〔tio∩〈){}『ˉox1+484b=![]jretur∩ ox2obcd2;}j}()j γar Ox19千538=0x349bO1(t∩j5’+u∩ctio∩(){γa】-Ox7日b6e4=于u∩〔tio∩(){}】γar-O×157b仟jtry{γarˉ0x5e672〔≡「u∩ctio∩

(|ret仙r∩\x20(十0∩〔t1o∩(〉\x2O′+ˉOx5b仟(!ox1|〉+‖)】』)】卫x157b仟霉0x5e672c()j}〔日tc∩(-ox11028d){ˉOx157b仟=w1∩d叫;} i十(! 0x157b仟[ Ox5b仟(′Ox2`)]){-0x1S7b仟[=0x5b仟(』0x2|)]=「u∩〔tio∩(ˉOx7ab6e4){γarˉ0x5己8d9e={};ˉ0x5a8d9e [—ox5b仟(』0×3`)]≡-Ox7ab6e4; 0x5a8d9e[≡ox5b仟(`ox』|)]二ˉox7ab6e4j ox538d9e[ˉOx5b仟(|ox5,)]=ˉ0x7ab6eqj 0x5a8d9e[ˉ0x5b仟(』0x6|)]=ˉox7ab6eq;ox5a8d9e[ˉ0x5b仟(!ox7`)]=-0x7ab6eqj 0x5己8d9e[ˉox5b仟(,0×8』)]=ˉ ox7ab6e4; ox5a8d9e[ox5b仟(|0x9』)]唾〕x7ab6e4;retur∩ 0x5a8d9ei}(ˉ0x7ab6e4);}e15e{ˉO×157b仟[o×5b仟(`ox2』〉] [0x5b仟(,0x3!)]≡ˉox7ab6eqj 0x157b仟[ˉOx5b仟(0o×2’)][ˉox5b仟(0ox4『)]=-0x7ab6e4】 ox157b仟[ˉOx5b仟(0ox20)] [丁debug` ]=ˉox7己b6e4; 0x157b仟[ˉox5b仟(0ox2|〉][ˉox5b仟(!ox6,)]二ˉox7ab6eq; ox157b仟[0x5b仟(`ox2`)][ox5b仟 (‖ox7‖)]=0x7ab6e4β Ox157b仟[ˉox5b仟(|ox2|)][卫xSb仟(』0x8,)]=ˉox7ab6e』j ox157b仟[ 0×5b仟(′0x20)][ ox5b仟 (』ox9』)]≡二ox7ab6e4j}}); ox19千538();co"5o1e[ˉox5b仟(』O义3|)](_0x5b仟(』0xa`));

此时,我们如果执行这段代码’发现是没有任何输出的,这里实际上就是将〔o∩5o1e的一些功能 禁用了。 ●调试保护

我们知道’如果在JavaSc∏pt代码中加人debugger关键字,那么执行到该位置的时候,就会进人 断点调试模式。如果在代码多个位置都加人debugger关键字’或者定义某个逻辑来反复执行debugger’



第ll章JavaScript逆向爬虫

就会不断进人断点调试模式’原本的代码就无法||顷畅执行了°这个过程可以称为调试保护,即通过反 其效果类似于执行了如下代码:



|‖|‖

复执行debuggeI来使得原来的代码无法顺畅执行。

|( |{

4l0

setI∩terγ己1(() ≡){debu8gerj}’ 3OO0)

如果我们把这段代码粘贴到控制台,它就会反复执行debugger语句,进人断点调试模式,从而干 在javascrjptˉobhJscator中’我们可以使用debugprote〔tjo∩来启用调试保护机制’还可以使用 debugprote〔t1o∩I∩terγa1来启用无限调试(debug) ,使得代码在调试过程中不断进人断点模式,无 法顺畅执行°配置如下: 〔O∩5topt1O∩s≡{ debugprote〔tjo∩: tme’ debugprote〔tjo∩I∩terva1: tn』e’ }j



■■】」‖■■■∏』■■■{〗‖|■■‖‖■∏■|

扰正常的调试流程°



混淆后的代码会不断跳到debugger代码的位置,使得整个代码无法顺畅执行’对JavaSc∏pt代码 ●.域名锁定

我们还可以通过控制do川a1∩[o〔促来控制JavaSc门pt代码只能在特定域名下运行,这样就可以降低 代码被模拟或盗用的风险。 示例如下: 〔o∩5tcode= 、

〔O∩5tOptiO∩5二{ do们aj∩[o〔代台 [|〔ujqi∩g〔ai.〔o∏‖] }

这里我们使用do『|m∩[o〔|〈指定了_个域名cujqjngcal.com’也就是设置了一个域名白名单’混淆 γar 0x〕203≡[!app1y,’,retur∩\x20(fu∩ctjo∏()\x200 ’ 0{}.〔o∩5tru〔tor(\x22retur∩\x20thi5\x22)(\x20)0’』jt咖0 ’ !attribute‘’,γa1ue‖ ’{rep1己〔e|’。1e∩gth! ’|char〔ode∧t』’|1og!’ 』∩e11o\x2咖or1d0]5(「u∩ctjo∩(Ox2ed22〔’ Ox〕ad〕70)

{var Ox49d〔54=千u门〔tio∩(0x53a′86){咖j1e(ˉˉ0xS3a786){0x2ed22〔[‖pu5∩|](一0x2ed22〔[05hj+t‖]())j}};~Oxq9d〔54 (升0x3ad370);}(0x3203’ox155))War 0x5b38=+u∩ctio∩(0xd778ob’ Ox19〔o千2){oxd7780b二0xd778obˉox0;

ox2ed646[一0xSb38(,ox00)](o×d1千329’arg嘘∏t5)j 0x2ed6q6=∩u11;Ietur∩ ox〕35「63;}}:于u∩〔tio∩(){}; 0xS〔+798=![]iIetur∏ Ox56ab十j};}()βvar Ox67d〔c8=Ox485919(t‖i5’+l』∩〔tjo∩(){γar 0x276a31j

tW{γaI 0x5〔8be2=「u∩〔tio∩(ox5b38(『0x10)+0x5b38(,ox2|)+‘);』)】 ox276a〕1=0x5〔8be2()j}〔at〔h(_0x;f1c") {0x276a31="i∩d叫〗}vaI ox25佃0d≡「u∩〔tio∩(){retur∩{|促ey,:-0xSb38(!0x30)’0va10e0 : 0x5b38(|0xq!)’ |get∧ttribute! :「u∏〔tio∩(){+or(γaI 0x5Cc]〔7≡OxOβ Ox5〔〔3〔7〈0x3e8j0x5〔〔3〔7-){vaI Ox35b30b=0x5〔〔〕〔7〉OxOj





日‖{』

v3I Ox2d2f“=0x32O3[Oxd778Ob]5retum0x2d2于叫i}5γaI Ox4859I9=十0∩〔tio∩(){γ己r 0x5〔千798≡! ![]; retur∩于u∩〔tio∩(0xd1+a29’ 0x2ed6斗6){v己r Ox36ab「≡0x5c十798汗u∏〔tio∩(){j「(0x2ed646){vaI Ox〕3a+6〕=

‖|‖

后的代码结果如下:

□■‖·■」■‖‖‖《』■‖]』■=■

〔o∩5o1e.1og(!he11owor1d!)

‖ | ■ ■ ■ ■ ■ ■ ■ ■ 司 』 · ■ ■ ■ { ■ ■ □ ■

的调试形成一定的干扰°

5"it〔∩(-Ox〕5b30b){ca5e! ![];Ietumthis[OxSb〕8(,Qx3!)]+0 ,+t‖j5[ 0x5b38(‖0xS’)]+! ′+毗5〔○〔7;de+au1t:thi5

γar 0x2b71bdi∩ 0x276a31[_ox5cod己2]){j「(0x2b71bd[ox5b38(了ox7!)]=ox8腿0x2b71bd[0x5b38(0ox8′)](ox7)

==ox6e腿0x2b71bd[ 0x5b38(00×8,)](oxO)=ox6〔){0x5992〔a=0x2b71bdjbIe冰j}}子or(var Ox397十55i∩ 0x276a]1

[0x5c0d52][0x5992ca]){i「(0x397f55[01e∩gth!]≡Ox8880x397f55[ox5b38(′ox8‖)](ox7)=ox65腿o×397+55 [0x5b38(|ox8|)](oxo)=0x68){ox4obd39=0x397十55jbre己【j}}}j+(| ox5c0da∑||! ox276a31[_ox5〔oda2]〉{retuI∩j} γar 0x5「19be=0x276a31[ox5〔6da2][一0x19ad5d]jγaI 0x67《+76≡|! ox276331[0x5〔oda2][0x5992ca]腿0x276a31

[0x5〔0da2][Ox5992〔a][ 0xqObd39]jvaI 0xSe1b34=Ox5+19be| | 0叉674+76jj+(| Ox5e1b34){retur∩】}γ3r 0x59339』=

』』□|』■■||■|」■■■||□■■|·〗‖■■■·|』■|{」·|』』】』■■

γN〕‖p11],’,g,);var Ox5a94d2=0〔l』QLiqj〔I∩Rγkg〔「zd"〔pzRAaⅫi.h〔oxⅦ]Oγp『pⅧ〕‖p11|[OxSb38〈’Ox6!)] (ox3b375己’! 0)[!sp1jt,](,;‘)jvaI OxS〔Oda2jvar 0x19ad5djvaI o×5992〔ajγar ox4obd〕9j十oI(γ己r 0x5〔己d1i∩ 0x276a〕1){j十(0x5〔己d1[-Ox5b38(!0x7,)]==0x8腿0xB〔ad1[0x5b〕8(00x8,)](ox7〉=ox74腿0xS〔ad1[ox5b]8(』0x8』)] (0xS)≡≡0x65腿Ox5〔己d1[0x5b38(』0x8‖)](0X3)≡0X75腿Ox5Cad1[OX5b38(!OX8!)](OXO)≡Ox“){OX5〔Od己2=Ox5〔ad1i brea代j}}十or(γaIox2955』1∩0x276a31[0xS〔od己2]){j+(0x295S1[ox5b38(0Ox7′)]≡≡0x6腿0x29551[0xSb38(00x80)] (0x5)≡0x6e腿0x29551[ox5b38(‖ox8』)](0x0)=ox6』){ox19ad5d=0x29551jbre冰】}}if〈!(|~‖〉0x19ad5d)){「or(

| | 〕

[ˉOx§b38(|ox〕0)]+0 』+thi5[0x5b〕8(!0x5『)]j}}}(〉};}】γar 0x3b3乃a=∩ewReg【xp(′[Q1dM〔「z础pzR∧X"hx〕0γp『p

■】八■■】□『‖|△■■‖炉「「|【■厂ⅨⅧ‖|′β■∏|||〖■『‖||卜■尸β『|■「|‖止■「|』‖■巳「‖凸『■∏·■「户卜巴厂·■尸‖■■广∏β′·住》‖尸■■|)|广》》尸伊尸‖●【【『『尸|『|■尸▲尸●厂卜●∏‖’巴■‖′巴『



ll.l

网站加密和混淆技术简介

4ll

| []j「or(vaI ox耳79239=oxoj ox479239〈0x5a94d2[1e∩gt∩‖]j-0x479239什){var ox19己d5d≡ox5a94d卫[ 0x479239]】 γar

0x112〔24=ox5e1bM[ 01e∩gth|]ˉˉ0x19ad5d[{1e∩gt∩0]jvar 0x51731〔=ox5e1b34[ 』1∩dex0+‖](ox19ad5d’

0x112〔2耳)ivar ox173191≡0x51731c!==ˉox18&0xS17〕1〔≡=ox112c2』ij于(o×173191){j于(ox5e1b34[01e∩gt‖‖]==

0x19ad5d[ 0x5b38(0ox7‖)]|| ox19己d5d[|1∩dex0+‖](, 。‖)==o×0){O×593394=||[]j}}}1+(! 0x593394){dataj}e15e {retur∩j}0x254a0d()j})j Ox67d〔c8()jco∩5o1e[ Ox5b38(』Ox9‖)](0x5b38(0Oxa‖))i

这段代码就只能在指定域名cuiqingcai.com下运行,不能在其他网站运行。这样的话’如果_些 相关JavaSc∏pt代码被单独剥离出来’想在其他网站运行或者使用程序模拟运行的话’运行结果只有 失败’这样就可以有效降低代码被模拟或盗用的风险° ●特殊编码

另外’还有一些特殊的工具包(比如aaencode、jencode、jsmck等),它们可以对代码进行混淆 和编码°

示例如下: Var3=1

使用jsfUck工具的结果: [][(![]+[])[!+[]+!|[]+| ![]]十([]+{})[+!|[]]+(| ![]+[])[+|![]]+(! ![]+[])[+[]]][([]+{})[!+[]+! ![]+! ![]+! ![]+| ![]]+([]+{})[+! |[]]+([][[]]+[]〉[+! ![]]+(![]+[])[ !十[]+|![]+| | []]+(|![]+[])[+[]]+(! | []+[])[+||[]]+([][[]]+[ ])[+[]]+([]+{})[ !+[]+||[]+! ![]+! ![]+| ![]]+(! ![|+[])[+[]]+([]+{}〉[+| ![]]+(!|[]+[])[+! ![]]]([][(![]+[])[!+[

]+!| []+! ![]]+([]+{})[+!|[]]+(| ![]+[]〉[+||[]]+(! ![]+[])[+[]]][([]+{})[ |+[]+||[]+!|[]+||[]+| ![]]+([]+{})[+{ ![]]+([][[]]+[])[+! ![]]+

([]+{})[+! ![]]+(| ![]+[])[+| ![]]]((!|[]+[])[+| ![]]+([][[]]+[])[ !+[]+||[]+! ![]]+(! ![]+[]〉[+[]]+([][[]]+[])[ +[]]+(||[]+[])[+| ![]]+([][[]]+[])[+| ![]]+([]+{})[|+[]+| ![]+! ![]+||[]+!|[]+! ![]+|![]]+〈|[]+[])[!+[]+! ![]]+ ([]+{})[+| | []]+〈[]+{})[ !+[]+! |[]+| ![]+| [[]+!| []]+(+{}+[])[+!{[]]+(! ![]+[])[+[]]+([][[]]+[])[!+[]+! ![]+!| [ ]+| ![]十!|[]]+([]+{})[+! l[]]+([][[]]+[])[+!|[]])(!+[]+! ![]十! ![]+||[]+! | []))[ |+[]+| ![]+| ![]]+〈[][[]]+[])[|+ []+| | []+| ![]])(!+[]+||[]+! ![]+!| []+! |[])(([]+{})[+[]])[+[]]+(|+[]+| ![]+! ![]+[])+([][[]]+[])[|+[]+! ![]])+( []+{})[!+[]十! ![]+! ![]+! ![]+| ![]+| ![]+! ![]]+(+l ![]十[]))(!+[]+| ![]+! ![]+|{[]+! ![]+! l[]+! ![]+! ![])

使用aaencode工具的结果: 。o°/≡/| 川′) /≈』—上 /[| 』]jo=(.ˉ. ) ==3j C二(·O。〉二(.ˉ°)ˉ(。ˉ镭)j (。Ⅱ.〉≡(.e. )=(O^—^O〉/(O^-^O);(.

丑.)={·O. | : ` ’.@./ : ((。@./=3)+′`)[°e.] ’.ˉ·/ :(。°。/+ 0-|)[O^ˉ^Oˉ(.O.)] ’.卫./:((.ˉ。=3)+0-‖)[. ˉ。] }j (。Ⅱ。)[.O°] =((·●。/=3)+{-0)[C^—^o]j(·Ⅱ·) [‖〔‖ ] ≡ ((。Ⅱ·)+|ˉ!)[ (.ˉ.)+(.ˉ.)ˉ(.e。) ]j(.贝 。)[,O{ ] ≡((.Ⅱ。)+‖ ‖)[。e,]j(.O。)=(。Ⅱ. )[ 0C|]+(.Ⅱ°)[ ‖O‖ ]+(.●./+‖ `)[。e. ]+((·°·/≡3)+‖ˉ|)[。 ˉ. ]+((.Ⅱ。)+0_,)[(.ˉ。)+(.ˉ. )]+((,-·=3)+| |)[·O。]+((.ˉ°=3)+|ˉ『)[(·ˉ·) ˉ (.O。)]+(·Ⅱ.)[|C‖]+((. " -

Ⅱ。)+|ˉ|)[(.一.)+(,ˉ.)]+(°Ⅱ.)[|O0]+((.ˉ。=3)+| 』)[·e.]】(.Ⅱ. )[M]=(O^-^O)[. o° ][。 O.]j(. c·)≡((°

ˉ.≡3)+! ‖)[。S.]+(.Ⅱ°) ..Ⅱ·/+((.Ⅱ.)+』 ‖〉[(·ˉ.)+(翁ˉ。)]+((°ˉ·≡3)+』ˉ0)[O^—^Oˉ°S。]+((Ⅷˉ.=3)+0 ‖) 「O。]+(。·。/+0 』)[.e.]j (。ˉ.)+≡(.e.)j (.Ⅱ。)[, e.]=‖\\| j (。Ⅱ.)。.e./=(·∏。干.ˉ. )[o^ˉ^Oˉ(·e.)];(O 。ˉ.o)≡(.Q./+` `)[C^ ^O]】(.Ⅱ·)[.O。]≡‖\" 0 j(·Ⅱ.)[ ‖ˉ0 ]((.Ⅱ.)[ 0 ‖](. e。+(°Ⅱ。〉[·O。]+(°Ⅱ。)「E .]+(.e·)+((O^^o)+(O^^O))+((O^^O〉+(o^-^O))+ (.Ⅱ·〉[。 e. ]+(.O.)+(.ˉ.)+(·e。)+(。Ⅱ.)[· 旧.]+(.e

】■

.)+((o^ˉ^O)十(O^ˉ^O))+((O^ˉ^o) ˉ (·O.))+(.卫.)[. e. ]+(.ˉ.)+(C^ˉ^O)+(·Ⅱ.)[. e°]+〈·O. )+(·ˉ.)+(. O°)+(.Ⅱ.)[. e· ]+(.ˉ.)+ (〔^ˉ^o)+(.Ⅱ。)[° e.]+((.ˉ。)+(O^_^O))+ ((·ˉ·)+(.e. ))+(·Ⅱ。)[。 e.]+(.一.)+ (C^^O)+(。Ⅱ.)[. E° ]+((O^一^O)+(O^-^O))+(.O.)+(。Ⅱ.)[。 O·])(.O·))((·e.)+(。∏.)[。 e.]+((磷ˉ.)+(.e 。))+(°O·)+(°Ⅱ。)[°O. ])i

使用jjencode工具的结果:

$=≈[]j$={_:十+$’$$$$:(|[]+"")[$]》一$:++$’$$ :(![]+』『")[$]’ $ ;++$」$$$:({}+"")[$]’$$-$:($[$]+""〉[$]’ˉ$$: ++$’$$$ :(|』| 』』+`』|』)[$]’$—:++$’$—$:++$’$$ :({}+||』』〉[$]’$$ˉ:++耶$$:++$了$ :++$』$ $:++$}j$.$≡($.$=$+|』』|)[ $.$$]+($. $=$。$ [$。 $])+($.$$≡($.$+"")[$。-$]〉+((!$)+"")[$. $$]+($._≡$°$—[$.$$ˉ])+($·$≡(!""+』』")[$·-$] )+(Ⅱ._≡(! ,叮+』』")[『.—$〕)十$.$_[$.$ˉ$]+$.-+$.ˉ$+$.$j$.$$≡$.$+(||| 』,+』|{,)[$.ˉ$$]十$.—+$.ˉ+$.$十$`$$’$.$≡($.—)[ $.$ ][$。$ ];$.$($。$($.$$+"\""+"\\"+$。 $+$.$$+$·$$+$.$$+"\\"+$. $+$.$$+$。 $+"\\"+$.$ +$. +$。$$ +`』\\"+$。$ +$.

+"≡\\"+$.$ +$。

+$. $+"\’』")())();

可以看到,通过这些工具’原本非常简单的代码被转化为一些几乎完全不可读的代码’但实际上 运行效果还是相同的°这些混淆方式比较另类’看起来虽然没有什么头绪’但实际上找到规律是非常 好还原的,并没有真正达到强力混淆的效果。



■‖引■■■凶·|〗■■司

第ll章JavaScript逆向爬虫

4l2

以上便是对JavaScnpt混淆方式的介绍和总结°总的来说’经过混淆的JavaSc∏pt代码的可读性 大大降低’同时其防护效果也大大增强° q

5.Web∧ssemb|y

q

WebAssembly是—种可以使用非JavaScript编程语言编写代码并且能在测览器上运行的技术方 案,比如我们能将C/C++文件利用Emsc∏pten编译工具转成wasm格式的文件, JavaSc∏pt可以直接

(|』

过JavaScrjpt调用执行,从而起到二进制级别的防护作用。

』|`

随着技术的发展’WebAssembly逐渐流行起来°不同于JavaScnpt混淆技术’WebAssembly的基 本思路是将一些核心逻辑使用其他语言(如C/C+卜语言)来编写’并编译成类似字节码的文件’并通

调用该文件执行其中的方法。

化的执行环境。

比如’这就是一个基本的WebAssembly示例: 0061736d

010O0OO0

O1OcO260

O27+7+01

7十6O017千

O17「O3O3

02000107

1O02O〕61

6A64O000

O6737175

617265O0

O10a1302

O8OO2OOO

20O16aO+

Ob080020

OO20OO6〔

o千0b` .trm().5p11t(/[\5\r\∩]+/g)°们ap(5tr=〉par5eI∩t(5tI’ 16)) )).t∩e∩(|∏odu1e=〉{





})

‖勺

〔o∩5tj∩5ta∩〔e二∩ew‖ebA55e们b1y·I∩5ta∩〔e〈"odu1e) co∩5t{add’ 5ql』are}=j∩5ta∩〔e.export5 〔o∩5o1e·1og(02+4≡0 ’ add(2’ 4)) co∩5o1e.1og(‖3^2≡! ’ 5quare(3)) co∩5o1e.1o8(|(2+5)^2=|’ 5quare(add(2+5))〉

‖日‖{』‖‖□‖

‖ebA55eⅧb1y.co们p11e(∩ew0j∩t8∧rr日y(`

■■可‖|司|■=■勺‖』■■日‖Ⅵ

WebAssembly是经过编译器编译之后的字节码’可以从C/C{ˉ+编译而来’得到的字节码具有和 JavaSc∏pt相同的功能’运行速度更快,体积更小’而且在语法上完全脱离JavaSc∏pt’同时具有沙盒







确实无从知晓里面究竟定义了什么逻辑,但确实是可以执行的°我们将这段代码输人到测览器控制台



这里其实是利用WebAssembly定义了两个方法’分别是add和5quare,分别用于求和和开平方计 算°那这两个方法是在哪里声明的呢?其实它们被隐藏在01∩t8∧rmy里面。仅仅查看明文代码’我们



下’运行结果如下: ‖√‖

2+4=6

3^2=9

(2+5)^2=49

被轻易找出来了。

所以’很多网站越来越多地使用WebAssembly技术来保护_些核心逻辑不轻易被人识别或破解’

‖‖

由此可见,通过WebAssembly我们可以成功将核心逻辑“隐藏”起来’这样某些核心逻辑就不能





可以起到更好的防护效果°

6.总结

由于本节涉及一些专业名词’部分内容参考来源如下°

□javasc∏ptˉobh」scator官方GitHub仓库° □javasc∏ptˉobfUscator官网°



■■■司□·]』·■■■■Ⅶ■■‖』Ⅵ‖■】■■■√‖□】』■■

本节代码参见: https://gjthuhcom/Python3WebSpjdeI/JavaSc∏ptObfUscate°

‖|』

在本节中,我们介绍了接口加密技术和JavaSc∏pt的压缩`混淆技术’也初步了解了WebAssembly 技术。知己知彼方能百战不殆’了解了原理’我们才能更好地去实现JavaSc∏pt的逆向。

0

卜户|卜止■卢■厂||■厂



广

1l2测览器调试常用技巧

4l3

□阮_峰的“asmJs和Emsc∏pten人门教程”° □掘金上的‘‘JavaScnpt混淆安全加固”文章。

||.2测览器调试常用技巧

■尸仕■■「‖|‖|【■厂|■■厂‖|■■=■■■「‖‖■=■■厂‖伊『■■尸

在上一节中,我们了解了JavaScnpt的压缩`混淆等技术,现在越来越多的网站已经应用这些技 术对其数据接口进行保护°在做爬虫时’如果我们遇到了这种情况,可能就不得不硬着头皮去想方设 法找出其中隐含的关键逻辑了,这个过程可以称为JavaSc∏pt逆向° 既然我们要做JavaScnpt逆向’那少不了要用到测览器的开发者工具。因为网页是在测览器中加 载的,所以多数的调试过程也是在测览器中完成的° 工欲善其事,必先利其器°本节中’我们先来基于Chrome测览器介绍测览器开发者工具的使用°

但由于开发者工具的功能十分复杂’本节主要介绍对JavaSc∏pt逆向有_些帮助的功能,学会了这些’ 我们在做JavaSc∏pt逆向调试的过程中会更加得心应手。 本节中’我们以_个示例网站h仗ps://spa2.scrape.center/来做演示’用这个示例介绍测览器开发者

卜{■■■■彦

工具各个面板的用法°

↑.面板介绍

′仁}β

首先’我们用Chrome测览器打开示例网站,页面如图l1ˉ2所示。

|·,·『□…|…

■◆



岿』 ☆力割●§

叼=

≡…

一…

尸■

…今





=…



亏 盂





→ →





■■ =℃



……

一……

.》○

·

÷

膨 霸巫斟违 蹿‖萨

■王别姬ˉ「■旧w●‖Ⅷyc◎∩cub『∏O

9·5

●●

●仑白★白

….中■■泪/lγ0分W 0…γ.鳃上哦

_



兰丝瓣蹿鳃辈罕



—空

这个杀手不太冷ˉL“∩

9·5

●■●●●■

★★●★白

…′00O分甘 河乖

■=「β尸■■『尸·【■■■「卜}【尸■尸■『

回scmp·

↑…0a上烛

「}卜



肖申克的藏Ⅸ.沛e5haw■h■∩RR●dO加pt‖◎∩



—ˉ——ˉh 9。5

●●

★∩古★由

『{}}} } | 「 }

■日|■「

m′‖凹”





‖…‖0上n

图l1ˉ2示例网站页面

接下来,打开开发者工具,我们会看到类似图1lˉ3所示的结果°









虫 爬











▲副●

出《 ★∏

●却a2°哇旧赡.Ce∩te『

凸■]‖‖」·]■■||』■■‖

同5c「ap° ■

俏芝毯…||删,……





。|

×l+ ++◎

|■■■司|』‖



·∏

γ

α

∩己







] ]



斗 ] 斗



9.5 ★★★亡★

霸里刮姐 亨国肉憋`甲…翘′"`分w 》.瓣瓣』 |…孔2‘上织



-==

_

. .ˉ;h通翘~·. 『己田,函m穗c″…鲍Ⅲ…№…愈

L呻0…… p创α"心囱…mγ…L咖m四…



旷…V门片仓尸!抄

《举:=…肃…;炙墅≡

巾0■1…Pm凹协 卜垂■二4′佑≡

‘… k ≈… 、 ″ ˉ .ˉ

ˉ、.ˉ

-→..



ˉˉˉ…ˉ . ˉ .

↑■l…↑o∏wl●《 》

7叼』v』庐.喀R户

毛`

0













←◇→



.■l■洒《

b创』v…≡U.。γ生…厂0■α护■■l=…1″■≡芦>耳dt时> T■□』『G●t■=U4…巳但6』■■■0≡■..>

=d邯V…全…钒■■8铲巴1■…贴叮血吵鞭r鞠

■亡

Om△仓0酗0u,.…

由■』l儿o∏; 了cl■t』沁5

‖ 监

| ˉ…●缺→→妇缔←■磺尸■ m又≈】人【1n0『比吨…又〗

p咀』U6■k公=卜c凶田毋5〔6cm3$可℃1≈呵『户=々/d1卜

‖》

Q/01v净 ■/d1℃

UB巴扩…t$ry〔05睦0

〔山γ《

口m广』阮9「阮■/0£/mmk≈mm『Ⅲ口77dM锣】.皿·…/β■mpt>

』‖ ‖

v…P 尸…■F】pt■…/∩■■〔丁蛔【≥

αmpⅡ■γ』 D『“∧J

■】〔厂1pt】『萨白/{1f己m.■O7M▲〕0′]】≈…/乃c厂皿t> ·/…y争

J ≈仇■

●r



凸←■己二■导午■二己导≈=甲 ■■护←毛吼■四



f……山″^,^ £ . ˉ` ˉ

■…1>



0讨←心苛=凸





;

、←7…■±比亏白=■■ ■己=→▲电…诌■■≈▲二■ ●

■△

=■



■■

心…■→p ■么■■■尸

●凹



≈■



-



■= ■





:…p {c1·凹■■■1=止峙■】0 [c】=9■l=1《0十‖…°…●■●ˉf0习泌 《 …』t=宁卸r~…飞M叼8 ■∏{止l儿■巴“〗

ˉ函午中肚≥~…=QF■…Sb◆P

“`…【必5〗】

□』



如pp (

『mt·?■ny2 AU呻1「’№1v·t江■oA「1●l0$■∩■=Z曰厂1↑;

′ 〖 } _…

≡…

……



……

·…

ˉ钓°←

Q,



w=■■■ ■=



,……… 龟铀里 ·

p宁Ⅷ

■■







、▲□■

■勺

睁 q =厅7 、 □

■…宇凸巴△~■吟■■凸毛←■…抖p‖

,■

■占吵≡■≈

由→

~ .ˉ尸■



图l1ˉ3打开开发者工具

0

‖』

这里可以看到多个面板标签’如Elements、Console` Sources等,这就是开发者工具的_个个面

板’功能丰富而又强大°下面先对面板进行简单介绍°

0

□日eme∩ts:元素面板’用于查看或修改当前网页HTML节点的属性`CSS属性`监听事件等°

HTML和CSS都可以即时修改和即时显示°



□Co∩so|e:控制台面板’用于查看调试日志或异常信息°另外’我们还可以在控制台输人

呈现页面的性能分析结果°

□肌e丽o『y:内存面板’用于记录和分析页面占用内存的情况’如查看内存占用变化’查看 JavaSc∏pt对象和HTML节点的内存分配° □∧pp||cat|o∩:应用面板’用于记录网站加载的所有资源信息,如存储缓存、字体`图片等, 同时也可以对_些资源进行修改和删除。

■■■■可‖』●■□■■■‖■·■·』·■

□尸e巾「厕a∩ce:性能面板,用于记录和分析页面在运行时的所有活动’比如CPU占用情况`

司 』 ■

□Netwo巾:网络面板’用于查看页面加载过程中的各个网络请求’包括请求、响应等°

《〗』■■』·

JavaSc∏pt代码,方便调试。 □Sou「ces:源代码面板’用于查看页面的HTML文件源代码` JavaSc∏pt源代码`CSS源代码° 此外’还可以在此面板对JavaSc∏pt代码进行调试’比如添加和修改JavaSc∏pt断点’观察 JavaSc∏pt变量变化等°

□L|g∩thouse:审核面板,用于分析网络应用和网页,收集现代性能指标并提供对开发人员最佳

□●■■■

实践的意见°

了解了这些面板之后,我们来深人了解几个面板中对JavaSc∏pt调试很有帮助的功能° 2查看节点事件

□】■■■』《]□‖』=■■·‖··凸■■‖□■

前面介绍过,我们通过Elements面板可以审查页面的节点信息’可以查看当前页面的HTML源 代码及其在网页中对应的位置’查看某个条目的标题对应的页面源代码’如图llˉ4所示。

}■■巴『■‖尸

ll.2测览器调试常用技巧

4l5

b ■■厂■■=厂

‖■旨意蹿…■+ 回 5c『a尸e

酗古力萄●



÷÷· ■一…少哦

…=回门泌



0

9·5 古●白●★



一 ≥

…伞化翻

O….■叮u《

·■1v『D■→全■Ⅱ□±…泞

·■1v『D一■Ⅱ…… ●割№_6厂飞→心■儿~T ●划山……→6厂飞■●争℃儿叮■T 0【呵… ●≤』b…p望fB■仕垢℃l…l●1=【蜗●…田■1…【ˉ●飞…t≡■′mm ◎<B■■≥■←电fB…啦龄’■l…l●1=【咽●…田■l…〖~●飞…t=~白′m亦



≡=兰写Td〗

·…【

……t…Ⅱ年『=t…



U0m』U■≡…Ⅷ尸…知尸…■l~1●…帖袒·1…迪■绚●几…1←皿·【…k→1Sb

■D■嗜节■o0=■

》■■『■■■↑》

(镶蕊臂

O…酝◆+←0嘱‖■E■胡畸=归…蛔由

邹『如

~◆…



……

下■

凶■△…巳

…∏□囤

沁一啥·

… 加~

…=…°■

— ≈

■匡 <匡



↑唾



■■■「■■尸|■■=■‖■=伊|』■β『

亏亏住?且Ⅷ :?→?◎『诅 寸…山~■■▲口0萤■1…lq1…卜山■1…1=hd□● 寸…山一■■▲■0萤■1…lq1…卜山■1…1=h‘□O 守■』v…垂尸1=q孝叭…1仓■…凹—尸

汹喇■√■【碌;…》

幻婶

′mR…:幻畦

◆■加……≤巳m…■`也l●l宝●`ˉ私叭也l→〗●1走@1←…■1和l…●…J山v凸



8:■…

口′OiU◇

……●≈

…z…唾』』

■《

√·凹P γ■加P p……F】■々中呛`≤■■1t■≥t=凹…■…

■L份g 』…F1Ⅶ ″

◆也v■≡P〗■■冯≥■1<Ⅻ『■1t■≥t丛镭=p诌凸′m击 P■0T……■u0w已…门k四咏〃插……p

》 ▲’…?→u心『

■尸卜‖巳厂「β■「‖′》‖●■

b…嗣…………由●硒m●=比…≡色≈■口d≈nq十0■…■…≈…=……■Q

…o

■≈厂=p■吓…〖

《p仲7产…←制≈+

图l|ˉ4查看源代码

点击右侧的Styles选项卡,可以看到对应节点的CSS样式,我们可以自行在这里增删样式,实时 预览效果’这对网页开发十分有帮助°

在Computed选项卡中,可以看到当前节点的盒子模型’比如外边距`内边距等,还∏I以看到当 前节点最终计算出的CSS样式,如图llˉ5所示°

d

「「

■■尸止■「■■■尸|■■广巴■厂伊‖▲■尸匹■『







趣蹲蹋熟搏=盅蹄/…≡……琶f‘ Ⅸ 这′d』>

T<1v■G→7泌唾『沁Cl■p仔放,·广··1<@1·`画l=泌·1←西【=∏广凸●1却`←D●l■七O1■

!藤鲤■■

哇凶^℃

T<…防0≈泥…汹钉陀7■嗡『4←t▲1〗′…~……$…汕■r■西k击f……■些▲…y]



镰爵″^鞠↑…讥鞠〔m尸呻B岭′啤黔鲤瓣 咀/●产

p□1v“↑田▲v2…v劝c…步酝牢硒■铡翻≥=叼q1v沙 ●<1■≡=告=7】锤寸沁【Ⅱ里=…加7●■沁叼′·婶

p<加■■●≈=财…7劝r】凹■·…=阳m帕的>≠蛔1沙 ≤/0∏庐

仿妇』v山t汗…『必f1些□=△■l■≈1·h≤●l=N●l…1□啤巴●l=m1→9■l无●l→ qⅨ≥≥′·婶 88■仕●7

°●…■□●m甲



√●』诉

●亿●l●「

■门1萨

●厄u厂沁7

√■』沪

●叼1U■…y2…P渔博l鳃0潭q●l■m闷1t■≥Ⅲ 』Z■…港丁…,尸≥√dXU猫 》<』丫4至≡手7』捶7】Uc汹■·●臼■1→■【t■>tm=…了略…≈≤′●1卜

h叼1v由《←v■而=G蚂喧】″…嗡■1尤″口1t■←t四=…白千■…沪w<√山v> 伊割』U■二m→7…↑勤〔l酗酗钓O1画呵1f切>t…「寸→厂≈哩/mT、 ◆■1p由t←←72…T心仁〗″■■p●`盂■呵1■■≥t1■…●7≡队■</■』v≥

谷01$β凹γ 0 ↑凹t=0…`γ 卜0mt牢`刀■

◆V钾t→』吵t 洁』啪了

■广0心(帕0 Q90 9Ⅱ》 尸mt·7 bI味■

贞v懒1广0赊1沁t』c■·贝「凹{· Ⅱ 泌呻 〗锄

】峪■

■←≈0…U∩←bˉ

图llˉ5盒子模型

接下来’切换到右侧的EventLlsteners选项卡’这里可以显示各个节点当前已经绑定的事件’都 是JavaScript原生支持的,下面简单列举几个事件。 □〔们a∩ge:HTML元素改变时会触发的事件° □〔1jC促:用户点击HTML元素时会触发的事件°

□Ⅶou5eoγer:用户在一个HTML元素上移动鼠标时会触发的事件° □Ⅷou5eout:用户从一个HTML元素上移开鼠标时会触发的事件°

□keydow∩:用户按下键盘按键时会触发的事件。 □1oad:测览器完成页面加载时会触发的事件。

〗|」勺』可|』■■□】‖』日勺|||□■■|

第ll章JavaScript逆向爬虫

4l6

点’右侧EventLjsteners选项卡下会看到它绑定的事件° 回$回…】■…

●正● +寸G

β



■ ■ ■



衅☆力●!

g『■o●=尔▲砧项 亡w

羹髓′醉分钾 『…′唱上腆

■≈…切切〃0

凝!憾. 民田

巨…阿■四…”日…隧m仅

_

■瓣ˉ丁.『 , ‘ ˉ 啊

≈坤…回…mγ…远呼■m≈

心 § …″咆■■…R…芯儿≈m■

守d几γ山td封=跑QGc↑驹6l■pL 。●1七●1e1=Colˉ1·Gl<@l电抒mY←1】 b

v′·1γ回■《■→′2』Gm劝哼l捶囚,p·6∑■·t1m·→l矿、 宇尸gLγ血t←γ~沤纪c佃b〔l心m揭摊e1钧pmm●tm0 1侈ˉ蚀c☆07喇巩d′′

v◎例■闪·≈@"(酗硝℃匀

◎■〃比酗2四已酗 ◆cM吨

g■b●fO硒

×



◆≤铀t化醉Qγ声恤飞tmαi$ab1“. d』■啤】GO 〔诅●S`叁`忱∏巾陀γ羊■√buRlO∩窍 ◆m〗〔1碑■n曰·l■…『〃p弹

′Z…c1醉鳞·l=饵ginmm『》-tOtDl…100仕√Ⅷ碑何锣~ ■u匡`■22 ■0≈「…t』v■忌№1悲八』锚 ◆■1…‖≈0·宁尸…h二r…显二7’血P幽1^Ⅶ■113

■!一 .u〖lm2.p.…r.b】√u≥…■0

■■々

v唾p■雪=

<uc比邮…了尹3好/皿炉

p6缸…佣t印即·守£蜘魂.沁m@『乳.77d■锄m·》题E

≤u亡归$巳 ˉ∏…F吗戊/u沁

Pm刨$e够p

<【』【也Z£箍义…厂芯5噎八』≥

广…巴t■t0

<1』〔l■●$睁钨…广二扣◆≤′u″ 卜吨ucl醛$ el≈』[·∩m厂ebt讣酗1〔呻Ⅸt·1→1c…■「Go>~√/lD <1』cl●幽

低…「 ∏】·^√u≥

匀■l≥

…咱……`oG画U锤ˉ↑t哗p毕唾αMw、…呻卧…云盂D……囚砖…U厕互油■

图llˉ6选中切换到第2页的节点

这里有对应事件的代码位置’内容为一个』avaScrjpt文件名称〔∩u∩代ˉγe∩dor5.77da+991.j5,然后

紧跟_个冒号’接着跟r_个数字7。所以对应的事件处理函数是定义在〔hu∩代ˉγe∩dor5.77da千991.j5 这个文件的第7行.点击这个代码位置,便会自动跳转到Sources面板’打开对应的c∩u∩促ˉve∩dor5. 77da十99Lj5文件并跳转到对应的位置,如图llˉ7所示° 屋田印…m

C团……■吨呻…h…∏四……江γ…陋》 L…m



-_…凹

p………B ●_

●□柏p

0‖ <. 0

O

·°

×

沁o

2 D

翱…

0

国一].…

7【]》…S3》『fmct』皿』丁《■,t0的o』》《1「《Z7》{吨厂丙Ⅶ0

炉心出咱

8

b懈任

p

ˉ

2a

塑≡

宁…

1〗 1■

〗】t晌v·「帅驴!1》}′…\t{Ⅶ1呀亏●。■■1e〔t■αa唾l0C■u沁c恤?凹∩ct1卸(t》《G^■e 】d

些_

3】

20 】】

】川

阁llˉ7跳转到对应的代码位置

■γ‖』■‖‖□■司|‖‖|〗』■■‖』■■】■‖■■]‖』■叼



炉n…

p◎p『】恤口』斌





◆■甲

少◎”磺鱼R已

四α热z*割γ幽涸◎句J7…↑.亿x 1

勺◎…ˉ…….哼洒

§″″!

§

|」■■γ‖‖〗】■‖□〗〗·■■】】】‖‖●〗‖|」□、□〗|《口■」‖□刁‖|‖』·‖‖‖||』·』口■‖∏||□■』(‖‖‖‖`‖|』■』』■】‖』‖■

通常,我们会给按钮绑定一个点击事件,它的处理逻辑_般是由JavaSc前pt定义的,这样在我们 点击按钮的时候’对应的JavaSc∏pt代码便会执行°比如在图llˉ6中,我们选中切换到第2页的节

所以’利用好EventListeners’我们可以轻松找到各个节点绑定事件的处理方法所在的位置’帮 我们在JavaScrjpt逆向过程中找到_些突破口° 3.代码美化

刚才我们已经通过EventListeners找到了对应的事件处理方法所在的位置并成功跳转到了代码所 在的位置。

但是,这部分代码似乎被压缩过了,可读性很差’根本没法阅读’这时候应该怎么办呢? 不用担心’Sources面板提供了_个便捷好用的代码美化功能°点击代码面板左下角的格式化按 钮(如图llˉ8所示),代码就会变成如图llˉ9所示的样子。







■【巳■「‖|【ˉ■日|||尸卜)

l].2测览器调试常用技巧

4l7

上尸



屋田韵颧`龋

◎衍画佃锄D…悔!w·巾≈烟咆…?吕

仁p∩0m≈

…m嘲…归…》

_

P牢尸″宦『唾!】 ”

旧功凹冰ˉv咖山『B.了了m饱9‖.佃。×

;



■「『‖仿|}|△■庐‖■|■■{‖|■『|β『』『|》■「》【■「【’

_

〗 〗 2

◇□吨

.◎…2麓■彰Up律@颧袍

〕 〕

》■c唾



▲ 5 6

忙■『m馋

71‖ 71‖ )…S3》『↑u∩ct』o∩Jr《e0t’∩01》 )…93》『↑u∩ct』o∩J厂《e『tp∏01》.{』↑(乙厂》{va′ 矿薄Vn’…t0t≈°〃厂□p啤■千U陀ct1o∩.)

卜■‖7U

8 0

p■扫 【



识■■椒….





10 10

〗】

.!』

〗3

◇◎四J咆…】.憾

12 12

p◎pt『…um呵

1]tm距「1"严!1}},砷ce`8{γ■1仕e8e| ut肋距「1"严!1}},≈ce`8{γ■1仕e8e.5ele旧tG@La馋10c□11b■ck8↑仙∩ct』Q∩(t){e.5e`eC↑ 勺△

1Q

】5

泌 w 16

。°删mm呐

『万『……硒… 图llˉ8代码格式化按钮 闲田

匠↓o‘∏钳`鸽

G领…

’…"…·嘴丽创γ ^pp…m



p…P…邮Ⅵ



E咖卯

:

↑门 7o…↑0户·

【■尸「·伊「■′■『■『尸|)‖|●=「■【广|卜■尸阻口’广■■||■■『『■伊■「′【■厂|■ˉ|■「|

v■「O亡t·■pp1y(∏u1l′●『O哇∏↑$);

‖436 0q〕7

v◎…·…叮诚■

凹u!=o径0丁《●p 厂D ∩0 』} 》

0Q38

卜α…

“”



卜■恼也

四●{

’■响

蚂1 鞘】

γ●「Z『■■t唾!(厂e“№…厂(『e{1])…53); ?mCt】mJ了《●’ tp ∩, 』){ 』7 《Zr》{

蛔3

F■亿

v●了厂亡γ∏



F蹦… ◆◎四』呻硷几m p◎饥…h鲤0嚼0m



减u沛雨雨丽示

‖叫39

v□℃p

L胁加…

■N抑脚γ…它°7..列°俩柯Vγmtm× 创N虾毗异v顿d破H。7..列°扫柯wmtm×

p o止t;

嘲9 … 哩7

t=o·工…r闲0m疽M…{





』↑ (巳.t■呵它t=e·c0厂了·∩t陀厂味t ‖| ●.t』唾5t… 「etu「∩o·日卯ly(t∩160 O「0…tS)

Q“9



“50

q「.■G唾v·∩tL儿$t·∩e厂(●0 t0 ■e?{

“51

c印tur■巳 ∩β

口Z■Ave: 』 》 ; ∏)

“5∑

妈53l鳃蚂6 …]0】47

c…:?γa

图ll_9格式化后的代码

此时会新出现—个叫作chunkˉvendors.77da↑99ljs:1DImatted的选项卡’文件名后面加了fUrma仗ed 标识’代表这是被格式化的结果°我们会发现,原来代码在第7行’现在自动对应到了第“45行, 而且对应的代码位置会高亮显示,代码可读性大大增强!

这个功能在调试过程中经常用到’用好这个功能会给我们的JavaScrlpt调试过程带来极大的便利°



4断点调试

■『|【■‖『}|[■「}巴[『‖|‖】「|■‖『‖[■【「|∏■【厂「卜[■「■「|β|∩卜【【『【宁【厂|■

接下来,我们介绍_个非常重要的功能_断点调试。在调试代码的时候,我们可以在需要的位 置上打断点’当对应事件触发时,测览器就会自动停在断点的位置等待调试’此时我们可以选择单步 调试’在面板中观察调用栈、变量值’以更好地追踪对应位置的执行逻辑°

那么断点怎么打呢?我们接着以上面的例子来说。首先单击如图l1ˉl0所示的代码行号。 [己田……

…翱……α≈油∏■m…γ=…″



_

……■ _

v□呻 v°…=蹿芭

;

四咖mˉm…·V…p 》

“汕

“咒

m7跌●■t径![■任N

p■乙

…1 吗2

「…坤』「『·· tO ∩0 』》《 M(D) 《

p■…

…】

价■℃

w■…o

孔1r◆°凹…m

◆◎p0』…。喊

叫0

闺鳃

Q呛〗

QQ抛 “3

岭测 q■a3

4鹤6" 上m……0m

×

v四=

÷二「(冗{〗]〗…S】)〗

啥f=西

mr厂■Ⅶ

』“么





0宁拎@

p…





广■…

团 0‖^

·n口巾…西uⅦ.,↑尸柯咱m凶

v…

0 ●■w

『.?}塌熟慧凝鞋蹈↓}…… )

}『.°…潭t酗…矿|,′$,…《

■α……汀…Y扛恼…… 10 《G·∏■…t=e·酗丁闷何们●7叭0 ‖!●□t匹

…t仙■8■』

■DM沁』 l

p……

》 $ ∩》

户……■

》 ?m〔um贴《●0 t□ⅦD ↓》《

《八l‖盯l早m

……

T≡

~=凸§■ˉ 二 p1弘°t■■』…『』】 tα∩l …咖

加…■l……

》…0……

图1lˉl0单击代码行号



这时候行号处就出现了—个蓝色的箭头,这就证明断点已经添加好了’同时在右侧的B【eakpoints 选项卡下会出现我们添加的断点的列表。

由于我们知道这个断点是用来处理翻页按钮的点击事件的,所以可以在网页里面点击按钮试一 下,比如点击第2页的按钮,这时候就会发现断点被触发了’如图llˉll所示。

嘉霹;《 ˉ





』 抒…

…田击睡蹿

靶澎、禽…

一司

哮…嚼■■■



澜碘



『二

N

F



p■

铀°



p





≈.

m$ $ ` 』擎





·

F咱

『阅

T

耳芍



办●§

蛇★

●■脚∑°O叮如α磕∩【匈″9“‖



b

×{ 牛

回韵…‖…呵

●eG +

‖」·」||ˉ■引|□勺‖|」■■||‖■】‖||‖』】】」]‖」■可|‖|日||□■||‖

第l1章JavaScript逆向爬虫

4l8

滩′

厘=

q

b



→鳞. 』

▲ 们■

q





〔宦□≈硒屯

……碳空≈

尸…尸…冲



……h…n■∑ ………≡…



§

创m*ˉ晒…嗡7…‖扣柯∏…x



§

×



U抄俺↑ ?…≠@









} 》 γ0r【「江■t任l《7e“…丫(而[1』》≡S】);

“』1

↑咖〔t1吵J「《·o t0 ∩0 L) 《

“邦

U□↑印

4▲39

丁◎…西…C蓟洒 ●驻…

蚂Q4

b●帕叮

◆■…

●血◎·=………7′, ′脑∏……6

■ ◎臼t〗 『.

▲009



寸酗函=

吨「『TV∩

“4可

D■户

P枷■↑

17 〔2「){

々“∑

尸■↑仅m

●p刊≈匈ˉ

》◎四何哦m』邯

p◎pm汹h皿`‖咽0

甲凹c巳1



9q4二.

■2

◆■『…t】: ∩叼…ⅡD低u■唾γ甸t’■u“0 ( 0=

叫9



凹50

q「,四证瞻回tL1■tme「(●0 t,哇『《

“5〗

仁●:…■e旧Vmh{19丁m$f四9 【…oM畦nx日 9】7=

凹35

蚂已6 内创



p●凹1Ve8 』 }: ∩) 》 ↑u∏吐1m毗(●p t’ ∩O 1〕《 〈人-【0□r》q丁≡喳…0 4唾…r[e□ 【·■「■…7| ‖ t·∩‖

L■℃A列4α…嘛飞

q 0

卜t∩1■2叭·●l=…丁

C呻tD厂·: ∩f

4▲5Ⅺ …] …





寸…

鸿冤0— ˉ ˉ~r二二二←崇ˉˉ画7下■≡.=≡~

…陋

仆ClD■u「e(』丁) ◆〔l“o砧

炉〔1O■u门C切e)

q

庐6‖Ohm

■md归

v…h…





断点被触发

图l]ˉll



q

q

这时候我们可以看到页面中显示了一个叫作Pausedindebugger的提示,这说明测览器执行到刚 才我们设置断点的位置处就不再继续执行了,等待我们发号施令执行调试°



q

q

0

此时代码停在了第4446行’回调参数e就是对应的点击事件‖ou5e[γe∩t。在右侧的Scope面板 处’可以观察到各个变量的值’比如在[O〔a1域下有当前方法的局部变量’我们可以在这里看到 ‖ou5e[ve∩t的各个属性’如图llˉl2所示。





‖ G

鄂u…陶w“穴

P宙↑矾∏碰面佃■阿Y

∧……止嘲灿m四



§

_

四αu{毗ˉ呵…a77…‖扫

αm嘛≡γ■….7.·川尸枷`m■×

×

函 】仔^f ↑…贮o





』妈8 “39

“0· Q“』 凹2 6“3 8…



司40§

|…己 q“γ

郸8

芋燃…鳞



q々68 “6g

γE厂tOβ ∩O左《

凹72 Q△℃≈

0

№【t◎们乞: ·

〔O吮〔elBubDle8

q厂奉vo1d0 }

“β7

‖…出▲屿叶】雨n

butto∏8

=t(∩, 厂p 』厂刁 0「0x「’ t□〔O∩t画t)′

“60

Q△70

m比leZ】 t「ue

q「=t·e1■D 6「〔∩)』

刁q“

器涂谢』鹰。

■w亿eγg 7■【Se

′ 「况e.d■t■.o∩ | | {》β

绸65

4Q7】

■e8

γar∩■t,dm■触o∩ | |{》

叫63



抛砸t1◎"eo(ep t) { M( !Me·qat●°O∩) ‖| !Mt.“t■.o∏}]《



‖d58 “59



p$它t“`I用m/(′ O

马“2



(儿 | { Q「}.「…γ·值v巳∏tL』$tme「《e』 t.工「已ppe「| } t』 ∩)



』Qb』

其 印 仓

「‖m〔t1咖Q厂(e0 t『 ∩口 」){

q』50



p5v■b啡M3y凶◎`.ltemt@『‖:′v□IOeZ《』 ≥9etc■u忿e8 /〔)

4耳57





1分∩gth: 1

》: ∏》 》







COu唾; (.·· 〉

亿 巳

〔绅tu厂e且 ∩o 碑5S1吨; 』

4冯56



丁…





q了.■dd它γem[』5te瞻7{·′ t0 己G》《

汹5B|



寸幽≈■巴山

Q4田0 咽93 40弘



卜吟I

■此α……太翘…了…磅仆倒魄

书乱9 qaS1 “5j

TQ

@p■…◎∩…t



γ■「Z『■日t巴!《厂e凸…厂(瘫11]》…53‖』 『mCt』m〕了{O′ t, ∩’ 1》 { 1? (z厂){ v■丁「仁γ∩ p °zh5

γalSe

亡▲∩Ce1■ble: t7uG





c11e∏tX:028

c1沁∏W: 172

c厂eFte: eo’ 凹“■te日 eo 》0 fo肛t1o∩1°(e0 t》 { 4■丛■dJ←←▲午■·=h∏二



C…oSeq日 【「必e

c

·▲■□p‘▲■^全■





=hl



=≈仍

Ctf1№γ8 ↑●[巳● ◆cu『「印t『·f9●t8 ul。■l=…『



“?■ultp厂tve∩t“由 ↑■I$G 司

图llˉl2查看[oc己1域

| q



』 司



} ■■■□尸仆‖■■厂‖■『『

ll.2测览器调试常用技巧

4l9

另外,我们关注到有一个方法o,它在〕r方法下面,所以切换到〔1o5ure(〕r)域,可以查看它的

■「‖■

定义及其接收的参数,如图llˉl3所示°

………了皇~…、≡……`≡…}Ⅷ鸽: 诞 醚$峪锚脊 ?…;铃』翰F

巾 々Q溺 ■■『{‖|卜■■产

喇40 令0冯1 “々2

●悍婶~

卜 ■

v■丁∑「酝己t“Ⅱ《『e“■…厂(丁e!l】》<■53》』 f仙拥Ct1OpJ厂(c0 t0 ∩0 1》《

=穆…}^

;〔§锄…″(…` …Ⅷ

』√ 《Z「》{

▲冯q】

睡田『『左v∩

4△4今

》)



》■尸‖‖β【∩【β》△■卧

隅辱■1U●8 1 }】 ∩》

“52 必£3 蜗已

“鳃



tQ 《』‖! q厂》.矿…v酝UmtL1StmⅦ厂《■p t口皿厂…e「 } ↑ tq

↑OⅢct1蝇●Q《e′ t》《 k?《巡《e,由t■·m] ‖‖ !』《r·由t■们@卯》}{

Pp″t@t”色: 《鲍们9tr凹ct@7:列 ≥…聘『酚《◎~ˉ; ′『)

沁7∩mt·由m.·田 ‖‖ {》

, 丁■e.卤t●°“}|《》; qF罕t份e℃D G厂《∩》p

“61

…2 …3

,

厂: 9泌7·】30蜘…

=t(∩0 rF〕「’0厂’Ⅲ丁D t·c浙『t唾t)′

护C岭s‖吨

q了工vo测0

P〔1O蜘施《2…}

} γDfkO′m■{

抄61◎呻1

■尸『‖卜|卜)■「

鞭9

c『●钉t●8印汾

抖y0

吨囱t●8e□

“71

}; ↑0∩ct1mm《eO t》{

户△≈蝇

pF

p ■夕p→

■=←←



『′卢〃砸t】m止“Dt』…′}3 ∩′{5垣…5′}目 5cO碑5【▲】





“γ2

m叼th; 0

∩》

忱哮: 的旅U



…7 …

鳃`帕矿: 《α.·}

扣∩tt1m0丁《eo to ∏’ 1〗《

4436

“Zγ H邱酶 8鳃9

》臆狙r…e7目y《·) $f醉酶械$;(…)



… 叮“6



. .』”……

◆蜘◎…域巩巨Ⅷˉ唾加漏忽γ′枷y嘲…〈q马“

Ⅱ ■■t8

→■←≈→■=■q

0 ■

■pp▲==会当

毋=~■←=▲■0·

恤…





图11ˉl3查看〔1O5ure(〕r)域

》『『·巳■

我们可以看到’「M∏Ct1O∩[OCatiO∩又指向了方法o,点击之后便又可以跳到指定位置’用同样的 方式进行断点调试即可。

■厂|■▲ˉ■=卜■■尸|■■尸

在Scope面板还有多个域’这里就不再展开介绍了。总之’通过Scope面板’我们可以看到当前 执行环境下变量的值和方法的定义,知道当前代码究竟执行了怎样的逻辑°

接下来’切换到Watch面板’在这里可以自行添加想要查看的变量和方法。点击右上角的+按钮, 我们可以任意添加想要监听的对象’如图llˉ14所示°



p

■ ■ 「 ● 『

0

「 p



鳃 鳃 翻 龋

唾2 ≥面尸

严■Z』据旧 1 》 自"》

广prU蜘贮y碑8 《…店t『mm丁S∩



0…t』凹07《·●tG ■,八)《 》

《』↓{ q丁》.……tL■k…了《●勤 r.亚…r !‖ t〃∏》

?呻怎tj鳃蓟《e’t》[

1f《!MG.命t■硅m)!‖ !』《t·凹t■◇额》)《

耀

吨「■汰t.mtα酮{‖《》

唾】

α7立t·e℃o

p 『■■鹰呻■·m‖}《};



图1lˉ14Watch面板

|尸





t

比如这里我们比较关注O.app1y方法’于是点击添加o.app1y’这里就会把对应的方法定义呈现 出来,展开之后再点击「u∩〔t1O∩Lo〔己tjO∩定位其源码位置°

我们还可以切换到Console面板’输人任意的JavaScript代码,此时便会执行、输出对应的结果, 如图llˉl5所示°

{ | ‖ {‖

第ll章JavaScript逆向爬虫

420

侧…γ ^呼亨啊u啪m四·

"·tw°耐` 『、『怕…`·e

cα`°刨· s…os 叫°! 阿阶■ 阿阶■

|叫

僵烛"α"翘

限田

旧● t呻

^史三]

酗…v

》 ∑1β00;03.qMO

■‖(

令m:·0:03°46之/门《》{γ巳厂≈『p蛔台∩t5′1■∏.千∏5′』7《′A「.厂■y.』酗厂『凹y《』))″f口加∩t(』′∏ul【′□厂gU“∩fS’tp凹γ■O∩∩O∏o1e′■)′′◎厂《γ日厂 ′■1·5l】ce()′澎尸o<J°◎1e叼th′◎++)∩t(厂′oj,∩凹[1′G′t′卿γU∩∩■∩口【e尸)} 》 2】:曲:髓·1“o°app1γ

‘, 21:08;屿.195f日即ly(){[∩●t1沁cO“′J 》m:帕:10·7羽■吨…∩t5

哗212·8罩1O.761户A厂p…∩f$们凶田酶γ“t′ c■11“『 (…儿5…Ol『5…l·1rG围f◎厂′:刀 》皿0鳃82马.163■厂…mS[·〕

《. 21:帕:24.1蜘》肋U5哇γe∩t{』5丁厂U■tG□8 t向把′ 压c厂●G似『91y′ 5c厂雨γg282′cl』即蜘:820· 亡t1即tγ台 】巫〃=} 》

·‖‖ 』 ‖ 】 】 ■ ■ ‖ ‖ | 』 』 』 ■ 】 臼 ■ 】 ■ 』 ■ ■ ■ ■ ‖ 』 』 ■ ■ □ | | | | ■ ■ 」 ■ ■ ] ‖ | 口 ■ 〗 ■

图llˉl5

Console面板

如果我们想看看变量argu"e∩t5的第一个元素是什么,那么可以直接敲人argu"e∩t5[o],此时便 会输出对应的结果‖ou5e[ve∏t。只要在当前上下文能访问到的变量都可以直接引用并输出° 此时我们还可以选择单步调试,这里有3个重要的按钮,如图llˉl6所示° So`"℃鳃

9

付吼w砷

p臼徊∏m酶M·mαy

∩pp加at咖

;中

[}蜘↑lm″

i

×



E曰闸mˉvαnαw7da↑99↑。归



|o≈困mb砧·坤吐劝



…归_ˉ__“39

V■『z「正■t趾l(『e“门…e「《陀[】l) <■53); 他∩ct1m〕「《e’ t’ ∏〃 1》{

“帕 妈q1

》№贮h

1↑ 《Z「》《

“々2

Va厂r=丫∩

0q43



画0少呵中≠o

cm『`kˉvem°′a7…!.jS柯γ∏mt四×

vc酗忌』窒让

亏鞠嚣徽鞠幽!撒…………=

菱厂_



“Q9

C巳ptu『■: ∩’

“5】

} 8 ∩)

“53

◆x佣卿抛↑c们S吧a贞四『0tS



QQ“

十u厕ct1m0「(e, t′ ∏, 』){

“5β

′〃

■∩巳巧

7^≈^·0^■Q■^■●■ 弓尸◇^丙■尸′^

■ 【 ^户b



1? 《它.m呵et=e.cu厂「e∩t丁已「g宦t { { e"tm=

四$£』ve; 」

“62



d=■^■^=

0 0



| pD◎蛾颧■skpmm

绳0

■^0=■■0 4 0

p



」■…

‖■■■■□『‖□·‖

田ch《』呻辑γ钮}d◎『■·万d…『挺扛∏酗t囱鸿“6

qr.巳dd〔γe∩tL』Bte∩e厂《ep t’ ·e7《

“30

图llˉl6单步调试按钮

这3个按钮都可以做单步调试,但功能不同。



■■习‖■■■

□StepOverNextFunctionCall:逐语句执行。 □StepIntoNextFunctionCall:进人方法内部执行° □StepOutofCurrentFunction:跳出当前方法°

己可



尸e沛o『∏7a∩c● mce

‖etw◎冰

呼牛■□.一—一=-一_-.ˉˉ

■■~叮尸=刁

∧pp↓‖c旧 №p↓‖四tj◎∏

L!g批h◎use

c∩u『‖况ˉve∩d◎『S·7…↑·lS:『◎『mmea× C∩u『‖‖《ˉve∩d°『s·7…↑·lS:『◎′m巳



. ■β

←〕

γa厂Z『=己t“』 (厂e睦川Ⅷbe「(厂e[1]) 十u∩ct1o旧J「(e’ t’ ∩′ 1){ 1↑ (Z「){

■凸

“q0 4“1 “q2



} = <

4q39

va厂厂=丫∏ 0 O=t5

q443 444q

‖尸

同?



oD●bugg●「pa 户Watc们 vCa||StacⅦ

勺」』

Echu∏l《ˉγ印do泻.77申馋9‖jS

U=下●

Mo『γ〗o『y MO师oⅣ

□纠‖口

S◎u陀os



到了“47行,高亮的位置就变成了这一行’如图llˉl7所示。

」|



用得较多的是第_个’相当于逐行调试。比如,点击StepOverNextFunctionCall按钮’就运行

◆」【O。≡Ⅶap碑『

伊SC◎pe vB旧日点pO|∩tS 4q49 4』50 0▲51 dd乓7



q「°己dd恒γe∩t皿5te∩e『·(e′ t0 己e7{ C己ptu「e: ∩0 ∩■龟■TM尸■

田◎"u∩kˉγ刨创|

1↑ (e.ta厂g

b

图llˉl7点击StepOverNextFunctlonCall按钮 ‖

』』■

l12剑览器调试常用技巧

42l

5.观察调用栈

在调试的过程中,我们可能会跳到一个新的位置,比如点击几下StepOverNextFunctionCal]按 钮,可能会跳到_个叫作〔t的方法中’这时候我们也不知道发生了什么’如图llˉl8所示。 攀



固……‖…

·节

■■【‖}|■【『■■■辰‖■【「■尸↓=■『凸■尸



■「|}|▲■尸|卜|■=‖■尸‖‖·【凸「■『■尸【尸‖{■■厂 刁21S

◆△p灿呻啤M斌

Ot●?m值Z1枷(》 { ■t.t阿《α)分

i2i0 肖21,

砒……≈…7食锡勉声…

m墨$G`w…t《")

『‖

憨2沁

吨……弘7…一w钓



liig

O

『】2e

■t■?■

》■1必Ⅱ↑(配{‖ 内“eγm啸罕…tγ砷7喇$■tj“…巳门“|t

1221

ot=呜呻v】豹喇哮 0…r〗芦o?乙i狞一二辽色沁■唾(台″v~芭



…△…y…砖U=

门呵

……7‘←=毡『6

心……m瘁瞬

■^w~Q●沁1亡仓》

……了ˉ·.…?…

} 22J$

8 f吵陋t1呵(》 {

i撵6 k鞍y

呵丁…t(c↑, ·)



习】扮日



】蝉

cI$琅《 m厂∩t江1

鳃p】

0 0t囊傀噬蜘t加哇而厂(Ct〗

鳃狸

β代酌“c…假印啪《S了幽…《5Rm翱(饿》》;

鳃恿〕

m0凶沁啼《PtQ 《

邓$A 酚瓣

……◆γ催参=@》助 …″呻…又匆ˉ≡…p



跑砷≡n7…匿…锣飞狸

曰.O凹m

敏…电……bγ′ . .匡≡宁=

m……

触m…uγ″拿=■…



…审…秒7.献仔~争T购

C肋7…t咖咖m: !·

)》’ …■铀一古…左1邮h

知蜗→

……y…一?0

《胸垂獭薛…瞬鹤

私◎钡-…

c啸呻 枷

……γ………徽“4y

b…

图llˉl8跳到Ct方法中

究竟是怎么跳过来的呢?我们观察—下右侧的CallStack面板’就可以看到全部的调用过程了°

比如它的上—步是Ot方法’再上_步是pt方法’点击对应的位置也可以跳转到对应的代码位置,如 图llˉl9所示° …



…楷

—延→…≡



分腻



r ft(〔■’ t$ α…t『j蚀■》

22“!

『.百可

} 》〕’

m5g 魏S6;

龄《{{廖〈`t. !

Ⅺ酶7| 22Sα

!e蹈M划0Ⅶe7X

r2§9{

『etM而∩e

§

刊∑e

2∑6e

r2总1;

■■‖》『卜■『‖‖}■尸

γa「盯t=励ew滩『 ?u卯ct』oⅣγt(■){ gt(■’脉t》β

可、FT

希尸0^≈←『`

曰Ⅷm产…队γ穗.ˉ……0≈十?…

田"m…勘γ…恤0沪速学0.骏‖8

…型塑…讥抛哪…`…′ 创Ⅷ砷咐…、扒『、蝴W……

矾(…

配Ⅻ汝.哺…7…婶Ⅳ睡……

1v泊瞎『m冲

酝瘫唾…7……渔创割§‖749



…^…〃ˉ`宁…酞1…

队…

…→γ…….y·°,缅…?“↑6

…肆….γ………γ…



2潞6



汽时咒…



)》 ″



)磁…切南间…唾



2巫2

■二

}=…·《…呻呵

·

越鲍『





》 e!Se ∩隧 ∩“∩(t)

兄252’



v″…

e°CalMt》 》c■t 》cpt酿(c日》 {

泌5e

b 少



trY{

酝51{

×

份…

Ⅻ(e》 i↑ (e》

戮{

§

| ◆戴……鹤…

游{$t·pU盅打(《 ∑↑ {St·pu塞打(《?咖ct1o"《》{

Ⅺ2Q7$

2∑65!

!巾

翻沁^? ?学鞍碰, --■= `剿……豁 ; 中叶° 中…患…′pα 抽沪哟银…T=!≡

;:四《 桨恒

mqS

乃2“

2∑6q;

—~…≡

p

p

己t=!0 献=!0 } 》 ↑酿〔t ↑M刷〔k v v

醚dL 睡q2 2狸3 泌Qq

2泌3Ⅷ

……=

…)

滋匀0

响 ^

…缉。……



…”

—罐…





卜卜■■『尸卜■尸■βββ△■■尸。■尸■『■尸|■■■|◆厂■}■■卜

2z篱0

欣 ∩

图llˉl9



mH汝…a7………r?↑“



…….γ………邹′23γ0

∏f

敏…″…oy.诊瞬呵喇!唾雪f凹

CallStack面板

有时候调用栈是非常有用的’利用它我们可以回溯某个逻辑的执行流程,从而快速找到突破口。

厂 ’ ■



|{

虫 爬

向 逆



」■■司勺】】‖‖■■■】〗‖



●■■■

Ⅱ ◎

^己

γ







] ]





∑ ▲



6.恢复」avaSc『|pt执行

scrlptexecution按钮,如图llˉ20所示° S·Ufc醋 …

帆e∩》◎o…↓c钒l硕)

P御赋『∏郎`赡

N巳t№水

;* i

也墩Vtjm搏s

′ ■ 『 辜.河

ch仙∩k唇γ鄂d◎晤.7…‖.jS:{o;V丫mtm×

猩ch[Ⅳ水.U颤】oαa了7■…↑裤 } ↑

ˉˉ

-. . . .■

恫感…0…寸…够o…≡ 呼…

■℃■■



■■芦≈户叶弱≈净=E■

γa「∩;











2】d5

1? (S忆.puS"((fⅧ已t』oMl {

◆cl

try《 epca{l〔t》 》乙日t‘h《〔己》{

∏“∩《t)

℃fⅥⅪ水裙γα‖“『n了…脑W呻“『墅↑6

} αpt

哦帕箍0叔诊vw…已7,《呻∏m…:32鳃

酵Ⅶ鲤〖缚γm咖∏儡7.mm渤`忠酞22衍

鳃寻3 2∑弘



"M』ma帕

c抛Ⅻ*笋γ酞…∑嗡7…?◎『γV嘲§m,3蹿

215舅

)》,

p2蹿

∏旧.硒帅

侣0b{Ⅳ椒-呵m酗a7…『U?m喊t“:w匀9

;[{}},{l呵= !‘,

S檄t

硷tz椒P呵…魁憋γ…恼?γ阀盐≈《q↑酗狰

2237

图llˉ20 Resumesc∏ptexecutjon按钮

这时测览器会直接执行到下一个断点的位置,从而避免陷人无穷无尽的调试中°

当然,如果没有其他断点了,测览器就会恢复正常状态°比如这里我们就没有再设置其他断点了’ 测览器直接运行并加载了下一页的数据’同时页面恢复正常’如图llˉ2l所示° ■●凸曳■■

■盅■■■■凸

……_

■■『泊■

◆◆c

涉≈

—田麓》 - 二二《;蕊擞

|…『菌≡…

△…2ˉ苫°…因汹…孵Ⅺ

「″>皋皇_|干毫千导.干七干粤o捧闽b 千窃ˉ碍

9·↑

m■●●

古★宙

★d

日本′V泌分w 涸盯

豁‖,“2‖上映

■γ 阅



E

翼i墨 改



宙■〗」



迁徒的鸟ˉ丁he∏■ve‖『∏gBi7dS ■ 蛔◇….■趣·■n牙、m/”分w 酗‖琶〗2-02上缺



宅= 一





…予

…=

匡■

Ⅱ…

舞…‘ ,■,

4

°

,

≡ !° ’

】2q3

7mct1凹pR《亡,t}《

紧逆趣m

2卫□ 睡鳃

寸心…

≡一__一…=_←■

p…

1? {e》 ℃-

.,守≡

t可{

品伊气nⅦ◆0

Lh冲…7?f…酗叭↑了

图ll寅2l

■→

啦…

γ■r脉; 』f《或.脚曲《(『颧厄t1酶《‖{ --



→■=

▲…止

发鳃

∑涎』

≈.ˉ…~·ˉ

……叼■ …

. ˉ ˉ . . ” ˉ .一` .…= …叁ˉ 、 ˉ .

ˉ ′ ˉ `ˉ 豫萨^ 『 .

巳c恤■出≈■南己万…!扫虹Ⅷm…々“6 0千 『尸。寸■P介■寸=户 厂0§77≈↑丁■f…◆ ‖0 ■ ◆i≡旦

测览器恢复正常状态

7.∧|ax断点 上面我们介绍了—些DOM节点的监听器(Listener)’通过监听器我们可以手动设置断点并进行 调试°但其实针对这个例子,通过翻页的点击事件监听器是不太容易找到突破口的。

」■

沪知…

`、.巳●

ˉ●=…



p的贮



本; ×



2】句】

◆■顿咆

一芦……



■t● !●

舌■

m铅

巫刁】

宁■



创m淤呵…,7…『扛灿∏…诉



β■

●◎弓…夕≈=ˉ…甜=

.一

ˉ^

丁□呻

轩河

□△『β■塑记

EmⅢ申幻…7…尸

§

‖…出…

……妇" 冲 ←_■

≡圃韭

0嘲…犹陀恤∏凶面…画γ呻c…{…呻≈

■_

」■可|

广■=■

〔员田‖…唾m………日…

||』■■■□□|·‖‖‖二■勺|■■‖{|』=■可三■■‖□日□■■』勺』■●■|凹●司』ˉ■■】|‖闪°■■‖」‖]■■■■■`』■■■□』】| 』 ■ ■ ■■口‖‖〗■■】乙■■■』■■、{‖‖』=■∏』■∩□□】■■□‖‖‖

} e〔se

◎t

□□

tt(C己, 2a 切∩eⅢt丁上Ch崎)

F

陶颧T…飘铆“…0翰



鲤硼 2汕8 鳃▲9



喊罚贝k吧喇呵藩7…『αw蹬瞄m锤鹤

‖』□

1↑ (e》

∑251

已毛串n妒■



≡凹_产

■…

三酶0

己向■凸…=毛=

`0 · ≈…嚣= .…=

Tca↑‖S↑…仅

渔乌甸

2蹿2

■乾■





?uyl厄t1O∩βt(e’ t》{

之2$6

■ ■



p…咖

!

亚与3



佃D巴hM@s●『…由“

己t兰 l6

22冯〗

唾辊

宁 ■

…=哇伪净D■咱…

笋=-

x

=■伊『

座箩颧′… 酗48

掣i

』□‖』■■】】□□叮‖己■〗‖||

g

■■」‖』■‖‖』■〗‖

在调试过程中,如果想快速跳到下—个断点或者让JavaScript代码运行下去’可以点击Resume



ll2测览器调试常用技巧

423



接下来我们再介绍-个方法—Ajax断点’它可以在发生Ajax请求的时候触发断点°对于这个 例子’我们的目标其实就是找到Ajax请求的那—部分逻辑’找出加密参数是怎么构造的。可以想到’ 通过Ajax断点,使页面在获取数据的时候停下来,我们就可以顺着找到构造Ajax请求的逻辑了°

b

怎么设置呢?



我们把之前的断点全部取消,切换到Sou「ces面板下,然后展开XHR/fetchBreakpoints,这里就 可以设置A)ax断点,如图llˉ22所示° ……_-—

●0『



§

×



团 【0^? γ…拎●

厩丙盂丙『盂了司,…

p 仆

‖ =

!v≡ 砂§【

№= -

卜… §凸■■≡≡…′■【『∩●『『■帅川

■巳厂|}■尸【}『巴厂‖







——

-ˉ-—___ 涧…

出… +

啮…志



凸…-

|!



j

◆≈ˉ己已ˉ~亏r■

硼ˉ_

′刚·

!…壶●蛋=T〔\【…‘d !P…啥…

|《》 L■℃0刺…廓mv

…咖′

图1lˉ22展开XHR/fetchBreakpoints

F





要设置断点’就要先观察Ajax请求°和之前一样,我们点击翻页按钮2,在Network面板里面观 察Ajax请求是怎样的’请求的uRL如图llˉ23所示。







0

同" ‘

S

O



↑0



=→ˉ″ˉ…





◆≡ˉ一…



……

可ˉ

…缚 ………ˉ

_

●‖5 』O

;

× 宰







宇印

厅卜

□睡……□…… 巴

『m巾

0m丽

…面

…m

…吨





~_-七■

…←





一■=

…ˉ_















—≡■●■■=P占~^—≡==>~=-■=



_…。

…ˉ一





Ⅵ…

『≡



■·■■■Ⅲ■■一 …□

油□→·熔



】【‖∏■Ⅱ



…孕…

域…≈

≡□





…孕闽≠…





_≈

…α 亏≡ ˉ

田● 田●

】|

哩Ⅷ





纂0侧彼



雨=吨…

卜〖



…唾q理兰?≡△==…4.. —==—_=—

…G≡4≡ˉ-空…V…

…?哇…=哩△坦……晦

沁76↑…m0≡…‖..o

…“~】哇ˉˉ

■「尸尸■∏■】=■■■



叶=≡●2佣

…当宁ˉ空 -.≡ˉˉˉ

……三…墅0…↑。°°

→″.

■】●1冒32.7■≡工2f“3

……·tm丘t电「均』…m●Gˉomgm ■

Q~◆=-■=~→守=◇…≡一仁●●≡≈一←→

■=■

v~…

图l1ˉ23请求的URL



■厅‖卜

}}

p

可以看到,URL里面包含/apj/帅γje这样的内容,所以我们可以在刚才的XHR/允tchBrmkpoints 面板中添加拦截规则°点击+按钮,可以看到一行8rea促"he∩0R[〔o∩tai∩5:的提示’意思是当Ajax

请求的URL包含填写的内容时,会进人断点停止’这里可以填写/api/‖℃vie’如图‖lˉ24所示°











第ll章JavaScript逆向爬虫

424

●导●



回〗0^:

0…×

4 0



旧…】c7裙孵



馅◎ ◎岭

釉呻涵m炮咖Y…“m》 哟‖……

≈……



-1陌《D“w匪hm

惫<ht耐l [己∩◎≈e∩玉head>缅et6c们a厂§ t己∩9…∩…head>≤回eta c们ar.5et=ot↑=8p…ch

0

丁B…RmhW巴



№…】由 …

№↑四JS蛔



v圃‖≈盟让



№r≈… +

vx"雕℃h日『酗pα硒





O∩L…圃∩S

『|

髓糊: b

p四M…Ⅷm‖VtS





●G…‖ 脸t@…$

p匠丫e『0t1蛇te∏田B『●a占硒们》妇



少CS尸Ⅵ◎‖atj◎∩a巴ahm打Tts







图llˉ24填写/己p1/『‖ovje



这时候我们再点击翻页按钮3,触发第3页的A|ax请求°会发现点击之后页面走到断点停下来了, 如图llˉ25所示°



{ ■

回S嫩…‖…

q∏



+◆G

×



尚』

·名p■2■c…△c卸t●『/pG9●闷

■』 ☆办●





q



陋纪四恤……| ^



』 |! Ⅱ 飞





迁徒的鸟ˉ丁he∏己ve||}"9B{M堪



‖ √l









-_一-

壤翻.膊魄■`.40 0

韵帛 .

。。 .

.

§6谬7嘲瞄.;:0 ˉ+弹

_…一艘巫粥器卫爵鹃泌刀酗器皿鲍…弘器珊儡一… …

…扫 ≡鲍 …… … 、



…卫



………………闷……

…°●●

镭……

……刹四呵呵

跟尸

□矗

_▲

撼 …1…… …

●‖6

* §

×

0α^? 『 中.拎@

汀…↑.尸



o尸■

‖匈x腑叮鲤鲤

htrp雹;//蟹pa2.田c帽脾·ce门tef/ap』/凹γ1G7

11mf宜1…7?■et■2酗to赃∩酬w1γz酞叮「L

:凝°蹿



№…

U… vL◎C■1



Pc〗 ′(』 BG8酿D0rr…q酝Zt{『emγ5t■te; 1’ t1▲ 7: u∩Ge?meG



h∩古 {∩cc呻t8 0!●ppl1Cat1O∩/iso∩0 text/自→

酗…虹……卫Hm型【≡望唾Ⅱ唾p鳖,;!臃潞』′…′′ 萨…』∩ pt; 厂〈」 th儿5吕 u∩“f1睡@

u: 盯uu

γ8 ∩ul1

∩e讨→厂o『《"proc●E5.chdi「1S∩◎t巴吵pO「teG幽)}!『,四a毗蘸↑仁〔lOSu7e (e°expO了t5》

呻鲍,酗Ⅱm呜?7Q

四γ■…『vu

》〔lO宙u广e (b30d) ←●





0 p0

』」

图]lˉ25断点调试模式



■厂『‖‖■『【‖『▲■|「■■

‖‖

} ll2测览器调试常用技巧

425

格式化代码看_下’发现它停到了A|ax最后发送的那个时候,即底层的X‖[‖ttpReque5t的5e∩d 方法’可是似乎还是找不到Ajax请求是怎么构造的°前面我们讲过CallStack,通过它可以顺着找到

前序调用逻辑,所以顺着它_层层找,也可以找到构造勾ax请求的逻辑,最后会找到_个叫作 o∩「et〔h0ata的方法’如图llˉ26所示。

p

S◎ij『…Ne渺◎冰

‖·『m「γ

尸@『财7γn∩由

Lj吵甘m…

∧p似℃·tm

0

晒c↑NJ由ˉv酗口田己7…MS:脑Ⅵγm囱×







P



0















▲】687 41688 纲巨8q 』1690

e·广e巳po∩EG丁ype》

t「γ{ d.厂e乞pO∩SG丁γpe远P.「QSpo∩宅e了y

} c自↑c们《 》{ i↑ 《00

q】691

q】69? 叫693 A169d 感1895 ‘】$g6 点1697 靴1698 q】699 41700 41701 q1702

t



撒g!=……p.

"70"Ct1o∩』】 ≡=二typ僵o?色.O∩m隅∩lOOdp厂o

■锄酗四「■

凶0凹本=v■…凹.7…α汀m《m;4l6o6

●.鳃四↑巴

俄■触闻呻吨呵。7…脑∏m…“6!

pγD砸凹嗡沈m…哟

砸703~ 司】γ0A q1γ0S

』17拓

)》

灯w谰…n丫酗





赚腮:;}

脑V硒…啡ˉ”↑0

恐1707

}0



41708 q170g

心坐28 ↑‖ b8·38 ↑ⅧCti◎们《e0 t》《 e·e〗 e·e×po汰S= !0



c…≡…E7

叮唾…、2{田

巨n●圃$…扮

e抠唾…·7

0硒“巴…



▲≈■■△→



wmd四

e·w1tM厂e “∩t13l5“《d□切1t们〔「ede∩t1avc酣锐唾点

b ◆P

×

;

仆c1obaI

)}′

41686







▲1685



@↑6

№′^! T 吩. 腾@

·

叮顶碾 『j∩e6"==WpeO『u“『|Co∩t》αOSu「e (b50d)

砸5F尹 似68q



u吨d↑7田0…硒↑g

α…°吨

F=~=-□ ■≡=

图llˉ26找到o∩「et〔h0ata方法



F

‖(二击『『|‖【■『■『‖′|▲■■■【『|■■■「

接下来,切换到o∩「et〔h0ata方法并将代码格式化,可以看到如图llˉ27所示的调用方法° ……陶…佣

E口

≈婶V丁函啤…EY……↑……

T……0↑自鹤…挺

针mⅨ.`…。 ·.20体柏∏啊烟x

@↑6

‖尸△^? 『 牛·



_

巫8 159 160 161

th1S.$7凶te「·po●h(《 碑9e0 t

●… …二束…γ..≡ˉaw鸥

》)″ t们』■.o∩「et〔狗oat■(》

●°酗……巾…7 °…▲0硒

》p

◎……由=呵…已7 .颧……‖

o∩PetchD□t■: fm亡t1◎∩(){ U■「t■tME;

……串

!瞬!野翻擞潞:ˉ1)*〖m凰.um《

■…击≈=~■丁ˉ;…智垫

×

导·

1了d 175 1γ6

守幽…



173

Ⅸ…a<…>

t"1sβ:瓣?.严!油as.$·…s….u『l.愈"…′ 《 1蜘」t〗 tMg01加1t0

177 】γ8 179 18U 】81 1鳃 183 18《

×

■md扬

p61◎b·l

∩…; m上肿啪xP·gG伪O p■rD阳5;{ }

§

pαO田u「e(峙“》

mp呵呸hm0●: 7咖ct1m(t》{

1碰 163 涎8 i65 166 〗57 166 169 170 171 】7n



≠@

………7·.…■.3瑟

…q购k-…勒·7、 .:…鸿÷酗

o↑↑Sek吕 ap

……』…吵s {.厕v…□

to扛G∩8 e



=唾 …=↑…° ·砖‖鳞

0 s■e·陀sult5 爪

p ∩左e·cOmt;

toI●●m啊= !〗0 to田v坚■=$’ t.tOt■1=∩

止沁072喻…m田

……7沁

…罕…7……二===t2↑凹



立缄←…7.ˉ…=均0O



…~7…锤乞舍己 卜纱↑助

尸∩∩…

酝砷…7炳雨…

图ll-27调用方法

可以发现’这里可能使用a×1o5库发起了一个Ajax请求’还有1j们jt、o仟5et` to恨e∩这3个参 数,基本就能确定了,顺利找到了突破口!我们就不在此展开分析了,后文会有完整的分析实战。 因此在某些情况下’我们可以比较容易地通过Ajax断点找到分析的突破口,这是一个常见的寻 找JavaSC门pt逆向突破口的方法°

要取消断点也很简单’只需要在XHR/fetchBreakpojnts面板取消勾选即可,如图llˉ28所示°



■■■■勺]‖



第ll章JavaScript逆向爬虫

426

C《1《肋№tm厂仪[「厂o产0 eU ∩uu′ d))j }

№…窿



d·O∩tmeo‖t≡↑u∩Ct1m《) {

C《1(o0t血赡oMtO↑凹◆e·tmeOut+"氟Sexce“ed‘0β e0 d=∩Mu

0‖〔(酚少Sc◎碑 |抉c酗孰“市





i.1S5fmda了0B『酮Ze炬∩v(》)《 v己广田=∩(凹γ●自〔凹》 〔ooR 0 v■《e。田1thC『m印t1a1S | ‖ s(●°u『l》》墨e.又二「?

U“(MeoxS「70怕……】 =γ》

《fⅧCt1O∩《 e0 t》, 仕G柏ha』L赋■潭U l↑(爵感翱癣鲤·聪酬潍.孵臃鹏y殿望°;!

当●Ⅷ□□‖‖』■‖□『】』■】‖||』□‖

守日瘴…面m

d=∏u1I

■■■‖□■■】〗可|』▲■

×

沪W酗ct`

d·O∩e「「Q「=↑u∩〔t1O∩(){

犀■=t·toL…伯…)tL幻m■…爪S



少……↑B…渝a

))0

eomthc厂e“肪t1■1S睦《d·mt№厂eo·而t1n1S土 !0》O

d口≈S印腮ew阳■e.定9p四醋Ⅶ阵

}c.驴《49》{

t‖翻』g!=…sp…·『yp·』 -一 ˉ ˉˉ—…≡_ 一一^

1…-锤←_

-_

…●堂『】h

l」旧4‖7倔,c和泪l↑9

图llˉ28取消断点

■□■‖』■‖■■∏可‖司

…翻n;酌酝〗

41692

△晌9d`



4】668 41669 乃1670 q1671 q】672 41673 0】674 41w5 ▲1676 41677 q1678 4】6γ9 q1680 q1“1 4】602 ▲1683 』1″ 41685 q1686 01687 01“8 ▲168g ‘16” 41691









ji蕊7; .



L哪撇》α鲤

唾●

p窃脑∏Ⅷ…M蓟my…↑

‖JG柯7■…× c尚m仟↑…知饱′.″2‖c柯Wmm

a改写」aγaSc『|pt文件

我们知道,_个网页里面的JavaSc∏pt是从对应服务器上下载下来并在测览器执行的。有时候, 我们可能想要在调试的过程中对JavaSc∏pt做一些更改,比如说有以下需求°

叫■■‖口可

□发现JavaSc面pt文件中包含很多阻挠调试的代码或者无效代码、干扰代码,想要将其删除° □调试到某处,想要加_行〔o∩5o1e.1og输出-些内容,以便观察某个变量或方法在页面加载过 程中的调用情况°在某些情况下’这种方法比打断点调试更方便°

到对应的目标服务器°

』·‖

如图1lˉ29所示。



■■■司{

这时候我们可以试着在Sources面板中对JavaSc∏pt进行更改,但这种更改并不能长久生效,_ 且刷新页面,更改就全都没有了。比如我们在JavaScnpt文件中写人一行JavaScnpt代码,然后保存,

|‖|‖

□调试过程遇到某个局部变量或方法,想要把它赋值给"j∩do"对象以便全局可以访问或调用° □在调试的时候,得到的某个变量中可能包含一些关键的结果,想要加_些逻辑将这些结果转发

」■|(

■■■

‖‖■



{|

·ˉ·

尸β‖▲

图llˉ29在JavaSc而pt文件中写人一行JavaSc∏pt代码

这时候可以发现JavaSc∏pt文件名左侧上出现了—个警告标志’提示我们做的更改是不会保存的°

□司·』·||』■■□■司

这时候重新刷新页面’再看—下更改的这个文件,如图llˉ30所示°

』|』□■

ll .2测览器调试常用技巧 _

照田

曰钡`颧诚s…m℃

Sα〃…N·t…↑《

Pα阿唾…Mmmv

427

…—

L嘲柑m图

∧p掷碱蛔↑

■_=_

尸a9O……》

ECh《」毗~v■勺ma汀““↑。芦…ˉ?e778a码.扫× c↑"m■ˉ0驼9m把O↑2555鲤.隅

§

_

皿 —

〗(γ仙∏ct」O∩(e){fⅧct1O∏t《t》《↑O「(va「「‘o01=t[0]’C=t[1]pg=t[2]0l绚0f二[]51<1 0le「

丁□↑◎p

◆◎却□2…『…ˉ呵讥盯 pm唾B

p■脑‖■

`≥■Ⅳ7叼 宁■尸

龋钮潍鳞团…舷. ■创Nmˉ{比920侣nV蹈韶 ■c』"囚`仇ˉ4↑365"◎幻a碱 ■c帕m~v■Ⅺ。吧|77oa腮ˉ



v■田酿



■』

0





「 b





R

图llˉ30刷新贞面后的JavaScrjpt文件 有什么方法可以修改呢?其实有-些测览器插件可以实现’比如ReRes.在插件中’我们可以添

加自定义的JavaSc∏pt文件,并配置URL映射规则’这样测览器在加载某个在线JavaScnpt文件的时 候就可以将内容替换成自定义的JavaSc∏pt文件了。另外,还有_些代理服务器也可以实现,比如 Charles`Fjddler’借助它们可以在加载JavaSc∏pt文件时修改对应URL的响应内容,以实现对JavaScrlpt 文件的修改°

其实测览器的开发者工具已经原生支持这



个功能了’即测览器的Ove∏ldes功能,它在



Sources面板左侧’如图llˉ3l所示。

[良田

巴洒∏m旧

◎mm归

n

S。凹…Ne恤。∩《

″徊γ∏an■

∩∩凹vmγ



; E………汀…`鹏…`c7…膳×

叫雨|” ■O

1 (「Ⅷ〔t1O∏《e){f0∏ct』O∩t(t){fo『(γ己「「po’

+…b…脑…碉鳃

P









我们可以在OverrideS面板上选定—个本

地的文件夹,用于保存需要更改的JavaScript文 件,下面来实际操作_下。

广

b

0





首先’根据前面设置A|ax断点的方法,找 到对应的构造Ajax请求的位置,根据—些网页

‖…∏…

开发知识’我们可以大体判断出the∩后面的回



图llˉ3l Ovemdes功能

果,如图llˉ32所示。 …唾巴…k

■≡

_—_

尸田m而四……回γ…∩





L砸诚mm

●_

旧创m泣ˉ↑…m.0 』◎z—_











电】

b



P

||









》)0 》o

…°…铜 a……

愈毗嚼谣:?.铲遗(…僻…瓤…翻萨1·i喇■,《 u■1t〗 tm5·Mn1tp

B



O↑↑5et: ■’ t0咖: e

t.i°:d蔬.鳃『; 》

…邹…w…捧’ c帕∏水州咖…万…↑摔7

∩↑

………汀…`扛7 刨N■水w郡…迅w◎…0捍7

●呻…◎■雄ˉ呵…迅77…]辑;『a

)》



l勿w↑7ac酬四Tm瓣

“R 团Q些m

t°审◎γ1es二S0

t°tota1=∏

=持唾≈■

陶m″"m哟……m河…`摔`

…………=0…琶……吐‖耀

……当`…鳃^ .;婶…m‖鹤

… ’ S=e°「e$叭t£ }

……77…‖碎0

优蛔恒碑扛S.<m桓H■> αm浊啤恤…Ⅷ。汀…『烬:↑

α…油…

})

L

mm….鹤?′…`m副

O∩「■tc恤■t·吕 ↑四ct1m《》{ γa了t仁tM二6

;:灌搬滥;!{{t熊曝撼……媳″

179 180 181 〗82 183 18马 185 】86 187 188

×

创Nm嗡旧西已万…‖摔?a

} ●诬四铂

th1S·O∩『etc恤己t■(》

176 1汀]

■邹间白

}…m·

了-…=_-—_

163 】硝 16日 】“ 167 168 169 1γ0 17〗 17】 173 】γq 〗γ3

;

倒〗仔‘^! !…斡o

d``m欠ˉ0呻自腮….2恼脑∏m↑四腮冷

256S哟D

178

仿

|↑↑

调方法接收的参数a中就包含了Ajax请求的结 -…→ˉ

p

◎徊γ划epa锈巴蝉钒BW↑l↑! …竹m]α囤↑…

_—田-凸

妇℃∩γ∏m回

创NⅢ*它…a汀…‖挥了



刨m饰静呵…镭汀…1扫7

口≡

“0审…『m

尉匈…nT“7《甸沉仁》

图llˉ32 the∩语句





第ll章JavaScript逆向爬虫

428

q

我们打算在A|ax请求成功获得响应的时候,在控制台输出响应的结果,也就是通过〔o∩5o1e.1og



输出变量a。

再切回OveITides面板,点击+按钮,这时候测览器会提示我们选择一个本地文件夹’用于存储

=△

要替换的JavaSc∏pt文件。这里我选定了一个新建的文件夹ChromeOverTides’注意这时候可能会遇到 如图llˉ33所示的提示’如果没有问题,直接点击“允许”即可°

√‖

÷◆◎

×|+

=藏

凸Spa2Sc「已pe`c即te『/p已ge/3

|』■■】{

· ◆ } 回Sc『a·。|Movje



图llˉ33弹出提示

这时’在Ovemdes面板下就多了ChromeOvemdes文件夹’用于存储所有我们想要更改的JavaSc∏pt

||」·】■■则』」■■司」‖|』■』■

°…雹嚼…对′u…′…γ′c…°v…′…访闷权腮.…怎不盒泄露…….侧趣



文件’如图llˉ34所示°

◎°∩s。阳

Ne《Wα贸

Sou『℃雷

蹿■日■γ



……



止=

…小

阐■■

≡《 飞声■■-=



j

j ;



tm5.o∩「etchData《)

165

173 17冯

175

§ ′

176 177 178 179

186 181 182 183 18啡

b

page: t

}》p

16q





四厂a曰58 {

161 162 163

166 167 168 169 170 171 172



}’

-

O∏「etC∩Dat己; ↑u∩ct1O∩(》{ γ已厂t=t旭臼;

tms·lOad1∩g≡ !0;

v.『:二搬:搬:州{t髓隐;膘tatc."『l.1"d. th1s鳃;:;.{et(…$sto……u厂[.』"de腻,{ umt: th1$·umt’ o什Set: 己’ tORe∏:

e

} })°the∏(《十u"Ct1o『》(己》{ γ己「e=a·d己ta

U 5=e·厂e5ultS 0 "=e·COu∩tβ

t.1O日d1∩g二 !1‘

t·mv1eS=S’ t·tOt己1=∏

图llˉ34OverTides面板下出现ChromeOve∏ldes文件夹

我们可以看到’现在所在的JavaSc∏pt选项卡是〔hu∩旧ˉ19〔92O十8.012555a2.j5:+or阶atted’代码已

』‖‖|||‖‖』‖

j ;

。"p·蕊卿:it酚;删{t) { ∩己眶『 U0i∩dexPage00’

』】‖□‖凹□∏』□



[jg励℃u”

c们tⅡ欢弓]…2O↑8·…2.牌柯matm×》

—=-_■怕=◆■=_—-=

| ●;撰 日 160 ↑

知p‖m0o∩

Ⅷ●帕αy

p“◎『∏`aJ`CO

旧 印lmkˉ馏vd◎『a汀由怕9↑.}s

尸翱eα■而d韶淬

||

妇、 巨|α"即馅



经被格式化了°因为格式化后的代码是无法直接在测览器中修改的,所以为了方便,我们可以将格式

化后的文件复制到文本编辑器中’然后添加一行代码’修改如下: ●





}).the∩((千u∩〔tjO∩(日){ 〔o∩So1e.1og(0re5po∩5e ’ 日) //添加一行代玛 vare=a。data

门‖

t.1oadi∩g= |1’



|」

’ 5=e。Ie5u1t5 』 ∩=巳COu∩ti



接着把修改后的内容替换到原来的Jav蝎crlpt文件中°这里要注意切换到chunkˉl9c920f80l2555a2js 文件才能修改’直接替换JavaSc∏pt文件的所有内容即可,如图llˉ35所示。

‖』

』·」‖■∏|■■」』‖□|」□]』■■

厂『|匹尸『佳『}|[上■「【||‖■■■∏■日‖■■「|卜【=■『‖|

ll.2划览器调试常用技巧 冉

G



曰αⅥ·"捻

p巳geαα"deG

c°『`s。炮

Sαm唾

b

一 = ■巳学■,●苛沪西·■■□■=≈■O吁 =■

■■■=

v◎Sp日2Sc『日pe·c创]te『 步■cSS

p

v山lS

p 5=e·厂e5ult$ 0 ∏=e·COo∩t;

t·1oad1∩g= !1’ t·刚Oγ1e5二S『 t·tOt日1=∩

■吻u∩‖We∩α·『s v

187 188 169 1g0 191 1g2 193

77 oa『99

■四9e

■3



β I=o

0 C=(e(00C己9C00》′

:{:!粥}}|





Mu蔑e6t『1ctd0;

γa「5≡e《"C6b↑o0)

『 ∩=e°∏(5); ∩Q己



■■

20q



·[删d:?;M撕c罢(;!::;船·, "′ !1′ "u``′"72…b鳃』∩ul`);

) ]



》0

eb45; 千u门Ct1O∏(t』 a′ e){



195 196 1g7 198 19g 200 201 202 203



205 |



…田

{} u∩e2幅.c◎↓凹冈∩|



} 「

图llˉ35替换JavaSc前pt文件的所有内容

替换完毕之后保存’这时候再切换回Overrides面板’就可以发现成功生成了新的JavaScript文件’ 它用于替换原有的JavaSc∏pt文件,如图llˉ36所示° 漾茁.酗Ⅵ…妇 Ove丽oes Oγe丽oeS

S°u…AN欲w碱

p钮{。"γ妇厕c°

栅…v

!」g|`t↑℃…

App|『c雹"m

Eq◎们u毗ˉ↑9c920↑a0↑2555a2.牌×

§





■匠刷池怕Lmaloγe『丁灶eS



昌 色

=■■■=■Q=U

-





1og(u「e3po∩se|′ 日)

□□d臼ta

e·「e301tS

e·Cou∩t;

g= !1P =5G =∩

回 口





■[



( 巳



们· 〕 「









■■



〔 【

凹 〗 『

】Ⅵ■









=〗】】】】】】]]】]】]】】]】】]』】^丛〗

pa9e

cms由

●骗



} } }

】9耳





} )》

186 ■■





Co"SO1e·1og《,厂eSpo∩5e00 a》

184

b



吨∩拉心g韶

γa厂e=a■O己t己

185





№咋at蛔]

P敏如钢m…渺刨‖蛔γ

179 180 181 18Ⅺ 183

v□tOp



悦s…食

Echu∩}《ˉ|9c9它0怕。O炮55鲍2.lS·×



—=

p

429

图l1ˉ36生成了新的JavaScript文件

好,此时我们取消所有断点’然后刷新页面,就可以在控制台看到输出的响应结果了’如图llˉ37 所示。

正如我们所料’我们成功将变量a输出’其中的data字段就是Ajax的响应结果’证明改写 JavaSc门pt成功!而且刷新页面也不会丢失了。

我们还可以增加一些JaVaSCriPt逻辑’比如直接将变量a的结果通过ApI发送到远程服务器’并 通过服务器将数据保存下来,也就完成了直接拦截A|ax请求并保存数据的过程了。

修改JaVaSCrjpt文件有很多用途,此方案可以为我们进行JaVaSC∏pt逆向带来极大的便利°



』〈||·

第ll章JavaScript逆向爬虫

430

7‖



÷ ˉ〉G

`‖

×|+

●|

翱★}力可●

■gp日2$c吧p·co∩te「

曰Sc『ap· __兰≈≡冗—

霸王别姬ˉ「a『ewe|‖Myc◎∏cub肮e ■●

9.5 ★ ★ ★ ★ ★

中■内地`中田香泡/∏‖分钟

0钾汕γˉ泌上映

■◇■

×◎



i………坯 ·≥

仙…◆



…∏……mγ…1



田旦… 眼旧而

…辩_ˉ__…_…



‖…■‖

·



【 0〗812g】0·7门J●『止?Uf ′√0mmm8■々 $t▲r面距∏tF .印■’∩≈…w作】o…〃■『 《兰》·■》■ ≥cα‖7』g: [r『m■7◎…U●S↑: 《■》0wmp7O『的#$p“■G0 《=》G Y』≈n仙↑目 0· 罚Z『7〔oQ吐…g ·xmf≡刁…0m毕tG丁8 ′0=》 ◆凹《■2

[O吧∩t: 1四

■广●3p`∏β: ∧厂『w(】●) v·8

Ql』m: 蹿7O「…l{盯〔酗色山1吨■ 7cUt…『』■吕幻叼《2》

■■·‖二‖‖■■■■‖■■■||■■引‖‖‖|』■』■〗|·■‖‖]□°‖‖

·冒·『眉 …|…

08 睁m■ 12 ■■饲■

1G吨t仇〗 ■

伊…-5 『G{甘■l…: ∩厂厂叮(∑)0生D8“·″哩闪?; 0》 伯『[p■fm”O′′吕 ∩「「w

mγ■「: ■∩↑tp5;〃阳·串皿凸∩口旧Wmv』G/mQd■…汗田5鹤…J1b3Cdγw6仁↑0】q720』呻虫0●叫`←】Gˉ1c儡 ■№u↑e;

】7】

∩瘁日 ■■羊…m 扒巾M●呵=m8 ■】”〕■7≡】6“

U「闻』“乌; ∩7厂w《2) ·2 固…℃■

】〗 闻中■■记矾 q…`伪: 】

图l]ˉ37响应结果

9总结

在JavaSc∏pt逆向的时候,我们经常需要追踪某些方法的堆栈调用情况°但在很多情况下,_些 JavaSc∏pt变量或者方法名经过混淆之后是非常难以捕捉的°在上一节中,我们介绍了断点调试`调

|‖(

↑↑.3」avaSc『|pt‖ook的使用

』■|

会对后续JavaScript逆向分析打下坚实的基础’请大家好好研究°

』■∏|」■∏』■■‖|

本节总结了_些测览器开发者工具中对JavaSc∏pt逆向非常有帮助的功能’熟练掌握了这些功能

日‖||‖‖凸·■纠□∏‖■田〗■‖□■‖‖」〗

』d8 1

用栈查看等技巧,但仅仅凭借这些技巧还不足以应对多数JavaSc∏pt逆向。 本节中’我们再来介绍一个比较常用的JavaSc∏pt逆向技巧—Hook技术°

那么问题来了?怎样才能在测览器中方便地执行我们所期望执行的JavaSc∏pt代码呢?这里推荐-个 插件’叫作Tampermonkey。这个插件的功能非常强大,利用它我们几乎可以在网页中执行任何

||

要对JavaSchpt代码进行Hook操作’就需要额外在页面中执行一些有关Hook逻辑的自定义代码。

■■■□■』■■可

Hook技术又叫钩子技术,指在程序运行的过程中’对其中的某个方法进行重写’在原先的方法 前后加人我们自定义的代码°相当于在系统没有调用该函数之前,钩子程序就先捕获该消息’得到控 制权,这时钩子函数既可以加工处理(改变)该函数的执行行为,也可以强制结束消息的传递°



|」■可』】‖‖】■司

↑.‖ook技术

JavaSc∏pt代码’实现我们想要的功能°

下面我们就来介绍一下这个插件的使用方法’并结合_个实际案例’介绍这个插件在JavaScript 2丁a们pe「川o∩代ey







Thmpermonkey,中文也叫“油猴”,它是一款测览器插件’支持Chrome°利用它’我们可以在测



』■■|■■|』』■]

Hook中的用途°

















‖【广【●}′「■尸『||‖尸■『「‖|■「||止■『『|||[■「||■「「′■「|

■厂||[巴■『‖|‖▲≥『『|}「‖|■■。尸■「|}■■「‖|卜■■『‖卜■』『■尸|·【【■■「‖【■卜广‖|卜■「|凸|产||卜【防『‖[■[匡「‖「匹■『』「■■厂||《■「}|匹■■「【‖〖■「|‖卜}|‖儿西■■『】】■

43l

览器加载页面时自动执行某些JavaSc∏pt脚本°由于执行的是JavaScript,所以我们几乎可以在网页中 完成任何我们想实现的效果,如自动爬虫、自动修改页面、自动响应事件等°

其实’Tampermonkey的用途远远不止这些,只要我们想要的功能能用JavaScrjpt实现’ Tampermonkey就可以帮我们做到。比如’我们可以将Tampemonkey应用到JavaScrjpt逆向分析中, 去帮助我们更方便地分析—些JavaScnpt加密和混淆代码。

3.安装丁a们pe「爪o∩《ey 首先,我们需要安装Tampermonkey,这里我们使用的测览器是 Chrome。直接在Chrome应用商店或者THmpermonkey官网上下载并

[』 ,回· ;

安装即可°

安装完成之后’在Ch『ome测览器的右上角会出现THmpemonkey 的图标’这就代表安装成功了’如图llˉ38所示。

图llˉ38 Tampemonkey的图标

4获取脚本

Tampermonkey运行的是JavaSc门pt脚本’每个网站都能有对应的脚本运行,不同的脚本能完成不 同的功能.我们既可以自定义脚本’也可以用已经写好的很多脚本,毕竟有些轮子有了,我们就不需 要再去造了。

我们可以在https://greasyfbrkorg/zh≡CN/sc∏pts上找到—些非常实用的脚本,如全网视频去广告 百度云全网搜索等’大家可以体验_下。 5.脚本编写

除了使用别人已经写好的脚本外’我们也可以自己编写脚本来实 现想要的功能°编写脚本难不难呢?其实就是写JavaScrjpt代码’只

要懂—些JavaScnpt语法就好了。另外’我们需要遵循脚本的_些写 作规范,其中就包括-些参数的设置。



鞠☆田●

?p堑r了一≡}ˉ严 β没有遗狞中患涵卒

α获蛔唾凹本… 田露洒霸逆本…





下面我们就简单实现一个小脚本。首先’点击Tampemlonkey插

件图标,再点击“管理面板,,项(如图l1ˉ39所示),打开脚本管理页

?

G用尸舆…击■断

!



叠报锗8ug ·欢迎l酗 一…、卓…≡钨=监==势翻茫▲

面’如图llˉ40所示。

.



丝=二-…~→…珍磊

创建的’也包括从第三方网站下载和安装的。另外’这里也提供了编



』}

渤蟹理酗

这里显示了已经有的—些Tampemonkey脚本’既包括我们自行

!

o帮助|更新日忘 >

, `…

=一

ˉ

图llˉ39 ‘管理面板’项

辑、调试、删除等管理功能’方便我们对脚本进行管理°

|田…懒



加pe『们

』mB盯越 叫0酌』m 宁q堆毛

髓色

p

砷…^

Q 旬

‖ 2

…哦守



3

名■▲

瓜本

站甩

户~-…辟吁…

仑三二三

00↑



n‖

o

一….h、…

…甄啊 ~劈…决

目度问臼■冶下位孵 ●≈

■守



Ⅷ≈



捶宛田◎





□□…

■甲≡



_=百-—

…←●→●…● ≈←



JavaScriptHook的使用

ll.3

没x√实用工贝Ⅱ 箭蹿 ■蠢■俩

γ胸5々

6口



圃脑

固愈

一…牵_哲—≡~

0q92』 ■■



恿e臼

● ■T■●■

←仁≈■■q啥■■●



●≡▲

▲ □

↑S啊

园围图

■`

图l]ˉ40脚本管理页面

接下来,创建—个新脚本°点击左侧的+按钮,会显示如图 llˉ4l所示的页面。



|则‖」‖‖

第ll章JavaScript逆向爬虫

432

…V……硒硒硒







踊蹿 <断涅用户■本>



』剿飘圆

工 用 襄



… …

工翼

0■=气广尸马面F~丁

;司

文件纪Ⅷ逸瘴闪凹∏找鸦剧开世■

图llˉ4l

□■□□■■司□可』·』■■□∏‖□』■■‖□』·■■■二■‖■‖||』□■、■■

!

自;助

点击+按钮出现的页面

初始化的代码如下: //≡=05er5〔r1pt== //o∏a∏记 ‖eWl」5er5〔ript //0∩a‖eSp已Ce http://t日‖pemO「|戊ey。∩et/ //@γer5iO∩

O·1

//刨escrjpt1o∩ trγtot日代eoγeIthewor1d| //@日uthoI

■■】‖|■■】‖‖‖|』■∏||‖‖」■■‖‖」]‖〗』■司|‖|■■|‖|‖』■■

『尽9t

刃壁



田『a爪pe『『丫》o∩戊噬~

γOu

//枷己tch http5://洲w.t己『∏per∏D∩key.∩et/doα』川e∩tat1o∩.php?ext=d∩dg //牢ra∩t ∩O∩e //≡/05er5〔ript=

(+u∩〔tjo∩(){ u5e5tri〔t『 j



//γourcodehere°。。

})();



□0∩a们e:脚本的名称,就是在控制面板中显示的脚本名称°

■■『‖|』■■■□

下面简单介绍05er5〔r1pt‖eader的一些参数定义°

』|』

在上面这段代码里,最前面是-些注释’它们非常有用,这部分内容叫作05er5〔I1pt‖eadeI’我 们可以在里面配置~些脚本的信息’如名称`版本`描述`生效站点等。

□@∩a‖e5Pa〔e:脚本的命名空间° q

□0VerS1O∩:脚本的版本’主要是做版本更新时用°

日||

□0de5crjPt1o∩:脚本描述。 □0‖o"epage`O∩o"epage0R[、刨eb51te`05our〔e:作者主页,用于在Iampermonkey选项页面上 从脚本名称点击跳转°请注意,如果0∩a们e5pace标记以∩ttp://开头’此处也要一样°

』《Ⅵ‖‖‖‖‖】■■

□@author:作者°

□@1〔o∩`01〔o∩0【[、@de+au1t1co∩:低分辨率图标° □@i〔o∩64`0jco∩640R[: 64×64高分辨率图标°

例如: //0j∩〔1udehttp://硼w镶t己们pen∏o∩促eγ.∩et/* //0j∩〔1udehttp://*

‖』】‖■■■曰』■■■】|‖■■乙■别』■

□05uPport0R[:报告问题的网址。 □@1∩〔1ude:生效页面,可以配置多个,但注意这里并不支持URLHash°

■●□■』·□』·‖■■■■

□@update0R[:检查更新的网址’需要定义@veI51o∩° □0dow∩1oad0R[:更新下载脚本的网址,如果定义成∩o∩e’就不会检查更新°

」□乙□|=■司』■■

|) ▲■■■『}■■□‖}■■

ll。3

JavaScriptHook的使用

433

//@1门c1udehttp5://* //β1∩〔1ude*

尸 ′ 》

□@‖atCh:约等于0j∩C1ude标签,可以配置多个。 □@exC1ude:不生效页面,可配置多个,优先级高于0i∩C1ude和咖at〔∩°

□@requ1re:附加脚本网址’相当于引人外部的脚本,这些脚本会在自定义脚本执行之前执行’ 比如引人—些必需的库,如」Query等,这里可以支持配置多个0requ1re参数° ■厂『■■‖∩■厂‖巴■■

‖|‖厂卜■‖》}‖庐『住叼||厂伊「‖心β

仅『卜『炉仅》■户‖∩■》「■■【仁



β「β } ‖ 卜 尸 「 ∏】



例如: //@requ1re∩ttp5://〔ode.jql」ery.〔o"/jqueIyˉ2。1.4.m∩°j5 //@requirehttPs://code.jquery,〔o"/jqueryˉ2.1。3。m∩。j5#5阳256=23456…

//@requ1【ehttp5://〔ode。jquery。〔o"/jqqeIγˉ2。1.2.们i∩.j5枷d5=34567…’5∩a256=6789…

□0Ie5our〔e:预加载资源’可通过C‖-get【e5ol」r〔e0R[和C州_get【e5ource「ext读取。 □0〔o∩∩ect:允许被C‖x|『|1‖ttpRequest访问的域名’每行l个° □0n」∩ˉat:脚本注人的时刻’如页面刚加载时`某个事件发生后等° ■do〔u川e∩tˉ5taIt:尽可能地早执行此脚本°

■do〔u‖e∩tˉbody:DOM的body出现时执行。 ■docⅧe∩tˉe∩d: 00‖〔o∩te∩t[oaded事件发生时或发生后执行°

■do〔u"e∩tˉjd1e: 00‖〔o∩te∩t[oaded事件发生后执行’即DOM加载完成之后执行’这是默认 的选项°

■〔o∩textˉ川e∩u:如果在测览器上下文菜单(仅限桌面Chrome测览器)中点击该脚本’则会 注人该脚本°注意:如果使用此值’则忽略所有0j∩〔1ude和0exC1ude语句。

□0gra∩t:用于添加GM函数到白名单’相当于授权某些GM函数的使用权限。 例如: //@gra∩t6‖5etγa1ue //0gm∩tC"-getγa1ue //@gm∩tC‖5et〔1ipboam //卵m∩tu∩5a十e‖1∩dow //卵ra门twi∩do".〔1o5e //@gm∩twj∩doⅦ.+ocu5

如果没有定义过0gra∩t选项’Tampermonkey会猜测所需要的函数使用情况°



□0∩o+mⅧe5:此标记使脚本在主页面上运行,但不会在ihame上运行。

□0∩o〔oⅦpat:由干部分代码可能是为专门的测览器所写’通过此标记’Tampermonkey会知道脚 本可以运行的测览器° 例如: //0∩O〔O卯at〔‖rOⅦe

这样就指定了脚本只在Chrome测览器中运行。

除此之外, Tampermonkey还定义了_些API’使得我们可以方便地完成某个操作° □C‖-1og:将日志输出到控制台。 □C‖5etγa1ue:将参数内容保存到测览器存储中。

■■■『|‖■「『■「心『【尸|■「‖【■■

□C‖addγa1ue〔ha∩ge[jSte∩er:为某个变量添加监听,当这个变量的值改变时,就会触发回调° □C‖xⅧ1∩ttpReque5t:发起Ajax请求° □C‖dO"∩1Oad:下载某个文件到磁盘。

□C‖5et〔11pboard:将某个内容保存到粘贴板°

0







434

第1l章JavaScript逆向爬虫

此外,还有很多其他的API,大家可以到https://wwwtampermonkeyncUdocumentatjonphp查看。

在05er5〔ript‖eader下方’是JavaScnpt函数和调用的代码’其中』u5e5tr1〔t』标明代码使用 JavaScript的严格模式°在严格模式下,可以消除JavaScript语法的—些不合理、不严谨之处,减少_

0



些怪异行为,如不能直接使用未声明的变量’这样可以保证代码运行安全,同时提高编译器的效率’ 提高运行速度°在下方//γourcodehere…处就可以编写自己的代码了° 6.实战分析



个方法执行的位置,从而快速定位逆向人口。

登录

』■日||凸■、

下面我们通过_个简单的JavaSc∏pt逆向案例来 演示如何实现JavaScnpt的Hook操作,轻松找到某 阑尸名

d

忌码

‖(

接下来,我们来看—个简单的网站https:/川oginl.



scrape.center/’这个网站的结构非常简单’只有‘用 户名”文本框`‘密码”文本框和·登录”按钮’如

@

图llˉ42所示°但是不同的是,点击“登录,,按钮的 图llˉ42登录页面

密码,而是—个加密后的token°

输人用户名和密码(都为admin)’点击“登录’’按钮’观察网络请求的变化’结果如图ll43所示。

‖‖‖‖‖

时候,表单提交POST的内容并不是单纯的用户名和

‖|·‖

v冈…卜…硒U

■…1o91∩1■5C「ape·ce∩te厂 哺导p05γ

…Ⅷ/ 刻创冯…:们ttp5

((

…●睦巳ppMC■t九o"/j5◎∩, text/p1■1∩, */* 酗…↑心…酗睡gZ1p, de7Ⅱ■te0 b厂

■…咐……Z打=C川0K∏βQ钳缠g′e∩;q=9曝8′zh≡Ⅷ;q尝0.7pja〗中006,"tβq础p乙 ……日∏o→raC讥e ‖■

@函m几酗U由8 6耳

◎呻化‖ttp5甘//1o9m1.5c『己pe趣〔e『lte「

□■~■]√■■

它■…t.…: 己pp1』c日m◎∩/j5□∩;c们a「set刨汗^8 ◎”筑坤Ⅶm忘kmCt1d崇17879705↑d610■l=08324fαbf“bb∑屯q1m27C=M0000一17879705fd7f↑f

…酝∩O汇已亡雌

泊佃田:∩ttp多8/八09m1.£C『■pe·仁e∩te厂/

0唾.创》武…Q【6O四l它C∩厂◎■e』,;v=,『89′b0 M〔h厂o恫m爪0『『γ=『‖0900p 〗【『№t∧B厂a∩0葱』6γ=00g9(0



‖』』■可‖□

…劲屯谷丽硷逛月 70

■“抽剑卜“0仁e佩pty …锤励…?〔O『g

……→刨协: 5a侗e←or1g1∩

u…卞■″惋亡竹o乙人uD/300 (∏□仁止∩tOs打; 】∩teI№CQSx11→习=3)∧pp1巳比b№t/S37·〕6 (刚TWL′ukec巴Ck◎ T冈蛔…0户■扒●●◎

V……墅垮 0

v{tO仅ep; 0|eY]1C2γγb『UPt乙I6m厂点mlUI』W1〔旷正C30γC咽1OD贬C1pmJ9g,} to惯e∩8 ·‖●w1C2Vγm于[蹬I6InP《印l凹I1mcc赋C30v〔咽1mJhZ61pmJ9l↓

图llˉ43

网络请求



q

我们不需要关心‖阿应的结果和状态,主要看请求的内容就好了。

可以看到’点击』登录”按钮时’发起了一个POST请求’内容为;

| ■

确实’没有诸如u5er∩a们e和pa55"ord的内容,怎么模拟登录呢?

模拟登录的前提就是找到当前token生成的逻辑’那么问题来了,这个token和用户名、密码到 底是什么关系呢?我们怎么寻找其中的蛛丝马迹呢?

』■■■‖

{"to促e∩,0 : 』`ey〕1〔2γyb们「tZ5I6I"「比‖1uIj"i〔C「∑c]dv〔帕10j〕hZC1pbi〕9"}



| 创■

日刊

『|》

} ll.3

JavaScriptHook的使用

435

『尸‖‖

这里我们就可能思考了’本身输人的是用户名和密码’但提交的时候却变成了_个token’经过 观察并结合_些经验可以看出’token的内容非常像Base64编码。这就代表’网站可能首先将用户名 和密码混为—个新的字符串,然后经过了一次Base64编码,最后将其赋值为token来提交了。所以, 经过初步观察,我们可以得出这么多信息。

■『{‖|出■■厂‖|■尸

好’那就来验证—下吧!探究网站JavaScript代码里面是如何实现的° 首先’我们看_下网站的源码,打开Sources面板’看起来都是webpack打包之后的内容经过了 _些混淆’如图llˉ44所示。

‖伊 ∏

=■叮■■◆「『口‖日△■厂‖‖■■『‖}仕■仔■

卜‖卜}‖



似圆)卜|血尸‖』β

0

【■■「■「匹∩|

p

图1lˉ44

这么多混淆代码,总不能_点点扒着看吧?遇到这种情形,怎么去找token的生成位置呢?

解决方法其实有两种,-种就是前文所讲的Ajax断点’另—种就是Hook. ●Ajax断点

由于这个请求正好是Ajax请求’所以我们可以添加—个XHR断点来监听,把pOST的网址加到 断点上面°在Sources面板右侧添加—个XHR断点,匹配内容就填当前域名,如图11ˉ45所示。

这时候如果我们再次点击‘登录”按钮,发起_次Ajax请求’就可以进人断点了’然后再看堆 栈信息,就可以_步步找到编码的人口了°

●「‖匡■∏

卜‖『【『【■■■

!

…=. 凸→.亡≡…….→==≡创 . . . .

. 二 凸…障≡==

崖°,…·mm鼠西… http乌:/八吨1∩】·5■丁ape·C颐te〃

结果如图ll46所示。

`卜ˉ…ˉˉˉ ˉˉ . ˉ.一ˉ . , . 』垂

→→

6曰……~≡…ˉ…_

=三荆

蕊/翻=蜜 ●…配碑起…↑?…尸:‖3

№…晦



=..哇………ˉ→≡≈←-…~≡ˉ ˉ ˉ…

:键忍

vS…

l……ˉ呻…例…捶〗

…四…

血…LL=p… 尸

‖}



}比^§ ?宁贮@

再次点击‘登录”按钮’页面进人断点状态,停下来了,

…【‖…舞↑

vc刨‖≈…k

妇mⅧ…由站…打…酸产0

№f″… vx"…ChB……枷茁



Sources面板

…助成

ˉ;苞+;《

田U趾呵说am·酗]↑……困涸β 仕DoⅧB『……油豁

;b咙d幽菌商怠…ˉ…, _←一≡ ,…^ 炉巨γ田γtuSt臼唾BJea…Ⅳ↑芭

.

→—≡二ˉ==←…ˉ= ˉ. 、.…宁;….

’c唾塑堕l巳堕…_--ˉˉ一—

图llˉ45添加XHR断点

ˉ

……了↑…ˉ…0↑d摔↑



磊『‖…肄7



咖碰钳…们…·体了



…吹幻…们=挥7

m牟…m

申触西…h…插丁

卜~出….…°"…体03



……狮…碎7



…ˉ…h…律7

吐◎、血…申喊~m…汪∩…扛了

图l146页面进人断点状态

但实际上我们观察到’这里断点的栈顶 一步步找’最后可以找到人口其实在o∩5U咖jt方法那里°但实际上我们观察到’这里断点的栈Ⅲ

还包括了_些Promise相关的内容’而我们真正想找的是用户名和密码经过处理’再进行Base64编码







436

第ll章JavaScript逆向爬虫

的地方’这些请求的调用实际上和我们找寻的人口没有很大的关系°

另外,如果我们想找的人口位置并不伴随这一次Ajax请求,这个方法就没法用了。 下面我们再来看另一个方法—Hook。 ●Hook

第二种可以快速定位人口的方法,就是使用Tampermonkey自定义JavaScript,实现某个JavaSc门pt 方法的Hook°Hook哪里呢?很明显,HookBase64编码的位置就好了°

这里涉及_个小知识点: JavaSc∏pt里面的Base64编码是怎么实现的?



d

没错’就是btoa方法’在JavaSc∏pt中该方法用于将字符串编码成Base64字符串’因此我们来 Hookbtoa方法就好了°

这里我们新建一个Tampermonkcy脚本’其内容如下:

//枷a『∏e5pa〔e

∩ttp5;//1o8j∩1。5〔rape.〔e∩ter/

//酗er5iO∩

0。1

//0de5crjptio∩ ‖oo代8a5e64e∩〔ode十l」∏〔tio∩ //@己ut∩or Cer∩ey //枷日t〔∩ http5://1ogj∩1。5cr日pe。ce∩teI/ //βgm∩t ∩o∩e //≡/05er5crjpt== (千‖∩ctjo∩(){ |U5e5trjCt!

十l」∩ctio∩hoo促(object’ attr){ γar千u∩〔=objeCt[attr] objeCt[attr] =「u∩〔tio∩(〉{ 〔o∩5o1巳1og(0hoo低ed0 ’ obje〔t’ attr) v己rIet=十u∩〔。app1y(obje〔t’ arg0Ⅷe∩t5) debugger ret‖r∩ret

} } hook("i∩do"’btoa0 ) })()

首先’我们定义了一些UserSc∏ptHeader’包括0∩a"e和咖atch等。这里比较重要的就是0∩a‖e, 表示脚本名称;另外-个就是狮at〔∩,它代表脚本生效的网址°

接着’我们定义了hoo促方法,这里给其传人object和attr参数,意思就是Hookobje〔t对象的 attr参数°例如’如果我们想Hooka1ert方法,那就把obje〔t设置为wj∩dow,把attr设置为字符 串a1ert°这里我们想要HookBase64的编码方法,而在JavaSc∏pt中’Based64编码是用bto3方法 实现的’所以这里只需要HookⅣ1∩doⅣ对象的btoa方法就好了°

‖可■■■■‖』】■】■〗‖」『□■■■■{』□司Ⅵ』■』■Ⅷ‖]|』■■】■■司|■】■■』‖』■■』■|■』■可』■‖■司·■』■Ⅵ‖

// =二05eI5〔rjpt== //@∩3"e ‖oo低8ase64

那么’Hook是怎么实现的呢?我们来看一下, γar+u∩c=obje〔t[attr],相当于我们先把它赋 值为_个变量,即我们调用十u∩〔方法就可以实现和原来相同的功能。接着,我们直接改写这个方法

原来的方法。这样我们就可以保证前后方法的执行效果不受影响,之前这个方法该干啥还干啥° 但是和之前不同的是’现在我们自定义方法之后,可以在十u∩C方法执行前后加人自己的代码’

如通过〔o∩5o1e.1o8将信息输出到控制台,通过debugger进人断点等。在这个过程中,我们先临时保

』·】』』■』■■『

的定义,将obje〔t[attr]改写成一个新的方法°在新的方法中’通过千u∩〔.app1y方法又重新调用了



存下来「u∩〔方法,然后定义—个新的方法,接管程序控制权’在其中自定义我们想要的实现’同时

‖‖‖□·■|■■■■■■Ⅵ』Ⅵ□

在新的方法里面重新调回十u∏C方法,保证前后结果不受影响°所以,我们达到了在不影响原有方法 效果的前提下’实现在方法前后自定义的功能,这就是Hook的过程。















ll.3

JavaScriptHook的使用

437

最后,我们调用∩oo代方法,传人"j∩do"对象和btoa字符串,保存°

接下来刷新页面,这时我们可以看到这个脚本在当前页面生效了’T℃mpe∏nonkey插件面板提示 了已经启用。同时,在Sources面板下的Page选项卡中’可以观察到我们定义的JavaScriPt脚本被执 行了’如图llˉ47所示。

p ■

回…mQ‖t…

●锣●

n



@

●…↑·■色….c●h憾



邱☆砧向● √巳团鹰用

同5cr凰p·



■8匿G二二巳△

αFF…ˉ. 田乙…寸

.双-~ 》

氏用工只

登录

●拧

o依纯l更历日■‖◎o砌

肌尸名

‖■■■■■■

二□

q… _

fid」…■ ( …S■■…陶……∏■囱…γ……1些→ …■凶 …≈绢D p字…D

:

晌幽雄昼≡ 冗‖—ˉˉ■■…≡≡约h





§

×

` . =二■凸■≡≡

◇鲤呻

守◎…『‖·西…斡… ▲α四

p■… 卜■扫

0“·$t「1店t■

】习

出0… w°…■…

溅沁—烛

囱『…~占

}} ,豌隙蹦黑鞠翻{》《

翱耀询嫂露{硷瞬房鹤`隘)

l; 〗q

锋r

门m恼唾t

了■



2】 捻



”丫…(…0 0m”‖》 20》〗(》

洲‖…搬姚嚣鹏阻{龋{嚣.髓栅耀睬}浙} 》){))

…■割……『山

冶?·c二I■NV》



图llˉ47

Sources面板下的Page选项卡

输人用户名和密码’然后点击“登录”按钮’成功进人断点模式并停下来了’代码就卡在我们自

定义的debuggeI这行代码的位置’如图llˉ48所示° 厂

●a● ≈》○

回肆…』[…

×



O

■…l?·加7…叁酚"‖颧

■} ★咕禽● |…田…必^ 】∏0 斟

哲‖沁9 短

@ 佩鳃函睡m≈……““≈徊Ⅷ…■m砌………= →—■



§

×



图llˉ48断点模式



第ll章JavaScript逆向爬虫

438

四…”坯es



酗呻

民旧

田●

这时看_下控制台,如图llˉ49所示。这里也输出了wi∩dow对象和btoa方法’验证正确° №t和『仇

≈油『腮呸

啊砷V

…1帧甘`… 户

_了°瞅顾——ˉ ˉ

_—

刨|…v ●

22:42P“.211hoo伐ed>m′】咖■{m∏“w8皿∏“w〃 5e【f;m′T咖wβ“cU…tf巾〔…″f′∩…『 鹤卸′ 〖◎c凸t义m: 儿oC日t』o门′宇}btoa 》

‖‖|`

』■∏|‖|』■∏|‖

成功Hook住了’这说明JavaScript代码在执行过程中调用到了btoa方法。

图]l斗9控制台

■▲

另外再观察下Local面板,看看arguⅧe∩t5变量是怎样的,如图1lˉ5l所示°

‖ ( ‖

这样我们就顺利找到了Base64编码操作这个路口’然后看_下堆栈信息’已经不会出现Promise 相关的信息了,其中清晰地呈现了btoa方法逐层调用的过程’如图llˉ50所示°

▲~~

■S“田

vc■‖S0梦

置…

…悦←鲤7↑…G…‖0d捶『

郭c“△

αN∏*.唾7↑翻e·龋门↑?d孵‖

cm归伪~唾70…c…E↑Vα尸$



曰义触ˉ咆…们…挥7



…~…伺…碎7

「戍

……∩….脑7

图llˉ50

ˉ……………=≡…

vLO仁■[

…跳↑顿N0.…}…4↑:‖9

寸■「g…∩tS: ∩呵u■e∩tS(1》 08 凶{腆凹Ze「∏a酝腮:龄a归1‖的’铡pa=m「d翱;00己dm∩"》0, 〔e[【ee8 〔°.°)

le吨th; 1 p5γ鲍◎1(SmbO1.1te「己to厂)8 fγ臼[Oe巧〈」 pgGt〔己uee目 f{) p唾t〔■1!e它: ′《) p一p「□t坐: 0bje〔t 「et: 00ey〕1C2γymPtZ5I6I■P长蜘lD【蛔j〔0「】〔3门γC叫101Jh乙

图llˉ5l

CallStack面板

arguⅦe∩t5变量

可以说_目了然’argu"e∩t5就是指传给btoa方法的参数, Iet就是btoa方法返回的结果°可以 看到, argu们e∩t5就是u5er∩a"e和pa55word通过JSON序列化之后的字符串’经过Base64编码之后 得到的值恰好就是Ajax请求参数to促e∩的值° 结果几乎也明了了’我们还可以通过调用栈找到o∩5ub∏‖jt方法的处理源码: o∩5ubmt: +u∩〔tio∩(){ vare=〔.e∩〔ode(]5删.5tri∩8i0〈thi5.「or‖)); t‖jB.$http.post(己[00a赋].5tate.ur1.root’{ to《e∩吕 e

}).tbe∩((十l』∩〔tiO∩(e){ 〔o∩so1e。1o8(阐data阿’ e) })) }

仔细看看’e∩〔ode方法其实就是调用了btoa方法,就是_个Base64编码的过程,答案其实已经 很明了了°

当然,我们还可以进一步添加断点验证—下流程,比如在调用e∩Code方法的那行添加断点,如 图11ˉ52所示° x≯ _=■≈→→■≡凸■=■■■■

】々7 2“ 209 2斡

…:■四in】■D 屯t■: ?mCum《‖{ retum{ ↑O馋8 《

西1

u已em…;"up

252’



x53

p□$…「d: ∩uu



】弘

25S

》’

2茄

崖t…£8 {

m…1t:加o蚁…《》 {

乙7

霉 }).t…《《7u∩Ctjm〔e) {

26〕 26β

} 》》



265 266 267

2碑

〔O∩巴◎〔e.1四《■日■t■■p e〗

} 》 0 『宝u

图llˉ52添加断点

‖|』■】可{|』】■‖■Ⅵ·】■■■■Ⅵ』■■■】■■■〗】■■‖□可|』■∏|』』`]=■】■■■

TTkm: e

Z60q 261 262

』《·□】■]|乙■■■]|‖‖勺·」‖·]‖■』■】』∏‖】·∏‖‖』■]】□|』日·Ⅵ‖」■】{』■Ⅷ‖|■■‖{■■■】{‖』■∏|』■司‖|』■□‖‖』■·‖』·』□‖|』』■■·Ⅵ‖|■■■】■〗

◆…t…





JavaScriptHook的使用

439

添加断点之后,可以点击Rcsun贮scIiptex“utjon按钮恢复JavaScnpt的执行’跳过当前TCm碑】I℃nk盯 定义的断点位置,如图llˉ53所示°

■厂‖|■■尸|「‖|匹尸‖|「■巴■

‖|}

「|「|‖



ll。3

卜 ‖ ■

图11ˉ53 Resumescnptexecution按钮

卜●∏『|》‖抄[‖〖■β〗‖}β尸‖|卜卜|}》「■■

然后重新点击“登录”按钮’可以看到这时候代码就停在当前添加断点的位置’如图llˉ54所示°

申『币

c…











p



0







图l1ˉ54代码停在当前添加断点的位置

P





这时候可以在Watch面板下输人thj5.十oIⅧ,验证此处是否为在表单中输人的用户名和密码,如 图11ˉ55所示。















图11ˉ55WatCh面板





没问题’然后逐步调试。我们还可以观察到,下-步就跳到了我们Hook的位置’这说明调用了btoa 方法’如图llˉ56所示°可以看到,返回的结果正好就是token的值°



◎γ

1马 15 16

〖6I吵Ⅲ础1U【1■1〔G「zC驹γC曲1Oj〕↑

17 18

19 20 21 22



23 2q 25 26



p…

,B譬导手

h°°血‖mno四′ 0bms"》

}》(》 》}}})(勋■t°〔o∩te只t’ th●t.?己pp1y0

t∩■t°co∩5O1●》;

f中瓤…0a…◎…

((「|耀翻蛾喳噬;瞒"涨鳃搬F?鳃l:{:{麓{麓°艘珊滁{闺蜒。m凹m爆匈"`……蹿萨

x7

p而仙户=全凹…励粒

图llˉ56返回结果验证

验证到这里,已经非常清晰了,整体逻辑就是对登录表单的用户名和密码进行JSON序列化,然后

调用e∩〔ode(也就是bto日方法),并把e∩〔ode方法的结果赋值为toke∩发起登录的Ajax请求,逆向完成° 我们通过Tampermonkey自定义JavaSc∏pt脚本的方式’实现了某个方法调用的Hook,使得我们 能快速定位到加密人口的位置,非常方便。

以后如果观察出_些门道,可以多使用这种方法来尝试,如Hooke∩〔ode方法` de〔ode方法、 5tr1∩g1十y方法、1og方法、a1ert方法等,简单又高效° 7.总结

以上便是通过Iampermonkey实现简单Hook的基础操作°当然,这仅仅是一个常见的基础案例, 我们可以从中总结出_些Hook的基本门道。 由于本节涉及-些专有名词,部分内容参考如下。 □简书上的‘Hook技术”文章°

□Ihmpermonkey官网° □MDNWebDocs网站上Base64编码°

||.4无限debugge「的原理与绕过 在上_节的学习过程中’你可能注意到了一个知识点—debUgger关键字的作用。debugger是 JavaScrjpt中定义的_个专门用于断点调试的关键字’只要遇到它, JavaSc∏pt的执行便会在此处中断’ 进人调试模式°

有了debu8ger这个关键字’我们就可以非常方便地对JavaSc∏pt代码进行调试,比如使用 JavaSc∏ptHook时,我们可以加人debugger关键字,使其在关键的位置停下来’以便查找逆向突破口°

但有时候, debugger会被网站开发者利用’使其成为阻挠我们正常调试的拦路虎° 本节中,我们介绍_个案例来绕过无限debugge『。 ↑.案例介绍

我们先看_个案例,网址是https://antispjder8.scrapecenter/,打开这个网站,_般操作和之前的网 站没有什么不同°但是’—旦我们打开开发者工具’就发现它立即进人了断点模式’如图ll_57所示°

■■■可]|■■Ⅵ‖‖‖■■‖‖|■可‖】」 ■ Ⅵ 」 ‖ ‖ ■ ■ ■ 山 ■ 】 · ∩ ‖ 』 ■ | ‖ | ■ ■ 可 。 ‖ 】 ■ 、 」 】 ■ ■ ] 〗 』 】 · ] | ■ ‖ ‖ ‖ ‖ 』 ■ ] ‖ 』 □ ■ 】 』 ‖ · | ■ ] 削 ‖ √ ] □ 】 ] 』 ■ 】 ■ 】 { 』 □ 』 ■ ‖ ‖ ‖ 』 ■ ‖ 』 ‖ ‖ ■ 】 | ■ · ‖ 】 ■ 可 ■ ■ | 】 ‖ 】 · ‖ | | ‖ · 〗 】 』 | 」 ■ 】 | | 」 ■ 】 |

第ll章JavaScript逆向爬虫

440

Ⅲ■『■【■■■□‖‖■■【■「〖■■■『·〗·∏■=厂}|〖■■‖‖}‖■∏『|甘■■「}■■『

l1.4无限debugger的原理与绕过 …魁



■●

晶α

■▲叮

@

÷÷O ··"妇口d■Qpm°e,c…『

;嚼 ,.态



同…′鸭.





★击司●占

“…… ; ,翅|掣……| .. 凸









|}}|

■尸【『|}|‖■■『‖|■厂■『■『′■庐‖户■厂|【■「‖[厂|‖■尸卜■■厂》|■『}‖■尸■「■『‖》『′■「卜卜》■尸‖‖■「|尸■厂|‖■尸「■■「■∏}■尸卜■|‖卜》】‖『巴中「〔■【■『|■【尸

睡′蜜嘿…|卿,c…|".

{ \…



l■■



i囊



■≈卜

●●●回函≡卜凶…

44l

叫 三骚::



-

「 吼

龋田田…≈■由



中■洒‘p■削|冈『 `ˉ轴分摊 〗992□7翁Ⅱe上氓 “



sαⅪ……似……γ…叼mm牢

■ {} Lb泊0,≈勿m↑



—■一≈=审

-………

Q

;

×

甘md四

…呻0》…唱…泊

图l1ˉ57进人断点模式

我们既没有设置任何断点’也没有执行任何额外的脚本’

它就直接进人了断点模式°这时候我们可以点击Resumesc∏pt



×

6∩酗硒…m…雨泛\ 0

□_—_尸■→■…■

executjon(恢复脚本执行)按钮,尝试跳过这个断点继续执行,

:

同^肾 》ˉ绳吵@ _=

●晌恼

v9…■

如图l1ˉ58所示。

№…d志 =

…g

然而不管我们点击多少次按钮,它仍然-次次地进人断点

模式’无限循环下去,我们称这样的情况为无限debugger°

`vS印p●

可…■





罕≡丁

寸Lo@1 ◆th』5;切1】Ⅶ四

凸C1o$u厂e (B印7》 门1∩d如

h6LOb■l ◆四≈…k

怎么办呢?似乎无法正常添加断点调试了’有什么解决办

墅咖闽m■:↑

法吗?

办法当然是有的,本节中我们就来总结一下无限debugger



「|↑ 止

的应对方案° 图l1ˉ58尝试跳过断点

2实现原理

我们首先要做的是找到无限debugger的源头°在SouIces面板中可以看到’debugger关键字出现 在-个JavaScrjpt文件里,这时点击左下角的格式化按钮’如图llˉ59所示° 匠田≈西℃≈… ■=

……仇



§

摊o

■_

●□吨

口◎■…回…·…柯

≥≡-

◆■…



寸~

尸m…

№…肋

p■响 仆■尸



●…

_-—

0◆1@四I



pth1B吕N江■≈

卜·邯…回

p〔1邯o罐(s“7)

◆◎p{…=ˉ唾

O01…l _

■…o→ _—

●…

◆……



:戳蹿



仕…?0出……■

网恤涸`

径硷由Ⅶ…

c司…‖『m ’csp…m`o■坤°炳饵

图llˉ59点击Sources面板中的格式化按钮

』组‖|』■■《Ⅵ曰□‖

第ll章JavaScript逆向爬虫

442

■■可■‖‖叮■‖‖

格式化后的代码如图llˉ60所示’可以发现这里通过5etI∩terγa1循环’每秒执行l次debugger语句。

·]□



■■‖』‖‖

图llˉ60每秒执行l次deb0gger语句

了解了原理,下面我们就对症下药吧!

因为debugger其实就是对应的_个断点’它相当于用代码显式地声明了_个断点,要解除它’我 们只需要禁用这个断点就好了°

‖(|‖日‖

3.禁用断点

勺■‖■■二□‖□■■■

当然’还有很多类似的实现’比如无限+or循环`无限wb11e循环`无限递归调用等’它们都可 以实现这样的效果,原理大同小异°

首先’我们可以禁用所有断点。全局禁用开关位于Soum田面板的右上角,叫作Ⅸacnva脆忱园肛jn腿’ 点击它’会发现所有的断点变成了灰色’如图11ˉ62所示° ×≡←′醒国Ⅷ』|鲤



j§A邀…冀回o.ˉ′…ˉ O……国 p咐吻 E

v曰…m

■….0O7….尸柯∏m…2γ5 debugge厂;CO∩5o1e.I吨{"debⅦ9e产o) U…

×

O…凹m凶 —P≡≡_--

卜Wm回γ

=■ 寸LoC■1

‖ ◆tMS8Wj晒四

ptb巫:■』咽础

}≥〔IOSu「e《5因7)

卜ClOsu「e〔5“7)

◆6〗…l ■

§

J

T…

}T止o屯aI

■■■



|隋^—仓-γ.寸.『司o

■□

mm贿 ●■_



抄G1比■1

‖1∩d四

_=~=====≡

●…≈吐

v回…础

~_ˉ霹雨…徊硒…a75 防ⅨⅫa矗翻s ˉ亏c…;.|!…矗已

…·

-…. = .

O…汰t」6m忠励… ≡■_刁==~---



≡→~∑

全局禁用开关



■■

…钓】…彝≈∏…了5 =_=℃

公Ⅺ…〗a…眶 b

p…H由…啮 ◆…0…些码

0

p〔回l[远■啦B幽…眶

图1lˉ62禁用所有的断点

这时候我们再重新点击_下Resumesc∏ptexecution按钮,跳过当前断点,页面就不会再进人到 无限debugger的状态了。

但是这种全局禁用其实并不是一个好的方案’因为禁用之后我们也无法在其他位置增加断点进行

这时候我们可以选择禁用局部断点。取消刚才的Deactivatebreakpojnts模式,页面会重新进人无

■■‖』■】】』■■■■』■■‖』二■Ⅵ‖』勺

调试了,所有的断点都失效了°

门□■■】■]·司』·■』■】■]』Ⅶ□■』■

图1L6l

■…而m图

ˉ.…、~…. 、 ←

尸Ⅻ印…0凸…血

■∏凸■□■·■■·■(‖』勺■■可□Ⅲ■Ⅵ■■∏四■■■■·■■

如图11ˉ6l所示。

q

■=■尸‖■『‖

叮| ■『∩=■∏■■|尸口

} lL4无限debugger的原理与绕过

443

限debugger模式’我们尝试使用另_种方法来跳过这个无限debuggcr。

我们可能会想着去掉Breakpomts里勾选的断点’

』* 份←色…

卜△}β

心想这样不就禁用了吗?大家尝试取消勾选,如图l1ˉ63

●∩■h拦可田呼锋古 赶w= 咽

Ⅲ品 毛



△啡呛) ■■≈土≈可≈甲←■串吕~学…屯

F裙尸飞、…铀←泽热钞…

§

×

0 、、~■■哮■毛…

吁■≈吁甲

|炉^; T◆.;砖@″ 幻

oPau…◎∩b砧■kpo{『讹

所示。

△ 串 ■

仓■

飞 ≥ } ≥孕B℃





轧、 p叮■■串串

~ ■

■ ■田

≈~户



≡~

■钓





B、~…≡■…哦牛≈…L~内 ■



b

■ ■←Q=



巨℃■

…凸……片

;P…v 鞠妇世点钢^、…b萨总喀…`咽 辉噪…贴←`

■庐『‖『■■■「■〗’■■

然而,取消之后再继续点击Resumescr1ptexecut1on

………-

壳蜀电≡……垒…■…

冉.4钉谗^.§ 诞…姆搀黔铲营…龟瞄 」…^审ˉ^^

∑〕

按钮’它依然不断地停在有debugger关键字的地方,并 没有什么效果°

≈≈…←

玲→≡…v ′≡≈



∑丁LoCa1

‖■β「‖》|□巴β‖‖■■|》『卜尸「}『■■●■厂

其实’ Breakpoints只代表了我们手动添加的断点°

对于debuggeI关键字声明的断点’这里直接取消是没有

pth15吕 ‖1∩dOW

|pCIO臼u厂e (5“7》

{’旦1qb§1——_ˉˉ ˉ一-一≡←……=~…~-ˉ一卫』nq虫

用的°

图llˉ63取消勾选

这种情况下还有什么办法吗?

有的。我们可以先将当前Breakpoints里面的断点删除’然后在debugger语句所在的行的行号上 单击鼠标右键’此时会出现一个快捷菜单’如图l1ˉ64所示°

广「‖▲■■「|‖■卜



『|■§■厂‖‖『■‖卜’佯■『■尸卜°甘■■‖■■∏‖山巴■尸■尸|▲■■「|

图llˉ64在行号上单击鼠标右键出现的快捷菜单

这里会有一个Neverpausehere选项,意思是从不在此处暂停°选择这个选项’于是页面变成如 图llˉ65所示的样子。

[【■■『‖■冈|‖|‖■■∏『{■■■■厂



} 『巴厂「■厂『‖『β|>■【尸【■●『■■■■■■■■■

图llˉ65选择Neverpausehere选项后的页面



第ll章JavaScript逆向爬虫

当前断点显示为橙色,并且断点前面多了一个?符号,同时Breakpojnts也出现了刚才添加的断点 位置°这时再次点击Resumescriptexecutlon按钮’就可以发现我们不会再进人无限debugger模式了。

≈止里

p●《沁门硒…………mm

…α仪

Eo记叫

山浦m函

!『…≠● 副0杉△^! 『… q

….‖0‖睡8酮,侈…咽‖鲤…摔匈∏…“x …′{a…咽‖鲤…摔匈∏…“x

必】 363 3“



死5 犯6

户W咏Ch

碑th: ,o/“t●1I/巾■V■0 ∩…: 的血t■11o0p

vS帕髓p◎m3

C…me∩t弓 ↑u∏Ct』咖《)《 厂etu们p「m皿·□●1I(【∩.e(,0ch仙∩贞引1365铀C·0》’ ∏





Ⅵ■■



367 368 369 〕70 371 372

》 》

「屯·



ˉ 您呻蕊涌ˉ;蹿甄丽顽而石 m唾霸ˉ′霉霄I{丽瓦而丽u肉做ˉ‘136…』〕′"o…圃.·m…° 枷…T灼

■β

■■■

β.Sc°p·

}碱腮;"…

士= —嚣Ⅷ 3饰

…瞬岭‖6

|!瓣蘑…

辩辨‖

≡→U≈听?寸u"Cm哪《e0 〖】◎∏《e0 t『 ∏》 {》』

|户巨呵汕uBtαma…‖〕田

□旧3750C呻呵↑3

C田●…∩m

尸oSpmGm〗臼…四们■

图ll-66Addcondjtionalbreakpoint选项

这个模式更加高级,我们可以设置进人断点的条件’比如在调试过程中,期望某个变量的值大于 某个具体值的时候才停下来°但在本案例中,由于这里是无限循环,我们没有什么具体的变量可以作 为判定依据’因此可以直接写_个简单的表达式来控制°

选择Addconditjona|breakpoint选项’直接填人false即可’如图llˉ67所尔° P锁诡∏7涵沁●啊my

∧pp‖四m∩

k椰nmm

·□■〗(·‖

……川欲w棘 _

回 回《■“x)

…胆] O]m田9扣

a卯`0O!0红…,知柯∏mt“x

.c谰“颧≡7雨m丁厂T==ˉ=ˉ ˉ

沈Ⅺ

》 }’{

366 367

『仆∩

-

) 》

}]

◆·

转o



爬t0mp「叼1□e.■u([∩.e(0och仙∩k-41沁5韶〔")p ∏

■β

371 37∑ 373

?

vBm刨甲α『V笆

c…me们t目 ?U∩〔t1oM) (



ˉ告ˉˉ

仁咆d↑

pat们: 『αdet己」Tp 凶/由t■1l/〗Hey必p …:`

3田 369 370

0广^晋

厂etump吨站e·■u《[∩·e(00Chu"k出】无5O0C"》‘ ∩O…………

晌…咖

vSc◎pO

中S四俄 丁止oC□l 尸tm$: "1啊m

〕7△ 375

P〔10So爬(5印7)

上‖旧375; Com肘『切刨隧B盐po恤tv

户Gl◎b■l 节o田↓忌↑■碘

?已l$e

●御mM囤阁 376

37γ 378

379 380 38】

…?

px月∩/↑U〗℃hB唾…帧匈

》, 1臼)0 ∩印「°de↑3u1t({ 5tO「e; p』 「Oute「8证』 厂eme厂8 ↑u∩ct1m《e》《

u『℃3 3750 酗…『3

0四胁B『琳四buS 广@‖r山@‖1!·!硒U

卜匠γmtus恼可日吟“们涵 …由…筐咖 卜≈尸m巳山∏曰…∏帕

图llˉ67设置Conditionalbreakpoint为false

此时其效果就和选择Neverpausehere选项-样,重新点击Resumescnptexecution按钮,也不会 进人无限Debbugger循环了。 4替换文件

前文我们介绍过Overrides面板的用法,利用它我们可以将远程的JavaSc∏pt文件替换成本地的 JavaSc∏pt文件,这里我们依然可以使用这个方法来对文件进行替换,替换成什么呢?

很简单’我们只需要在新的文件里面把debu8ger这个关键字删除。

‖■日]口·可|△口||」■■】{■引』

363 … 365

』□】■]|■门■■】|(□■可|(‖」·{」□■‖□】|·‖|(」】■|□】勺|』□】‖|‖■、‖(纠■」□]』】门{|」■|《]司`|

当然,我们也可以选择另外一个选项AddconditjonalbIca幼oint’如图llˉ66所示。

·□、‖|(|■■■】■|·■·{■■‖|·■|』■|

4q4

》‖■β「‖■■尸

} ▲■尸‖||■



ll,5使用Python模拟执行JavaScript

445

我们将当前的JavaScnpt文件复制到文本编辑器中,删除或者直接注释掉debugger这个关键字’ 修改如下:

■■「|‖■「

5etI∩terva1((十U∩CtjO∩(){ //debuggeIi //可以立接删除比行或者注释此行

〔o∩5o1e.1og("debugger") }

} ■■■尸□份■『■【■厂▲■■■‖‖‖■■■‖〖■■■「‖『▲■尸●『■■『‖■尸匹■■「『『巴巴「

打开Sources面板下的Overrides面板,将修改后的完整JavaScript文件复制进去,修改的内容如 图l1ˉ68所示° _————

陨团幽励e∩熄



尸四e



◎γ韧「门es

Co"s。妇

Sα闽℃“

_

ˉ←



_^

么川otwm《

E仙■叫

§

pm·∏"mcs

佃臼‖"°叮 `起

Ap侧cs加∩

L蛔m↑℃仙S°

q□p口凹‖呢83S.|a× → ·■牵-士■…■→q ●口·■~●_■≡-≡厂℃…



》}; 》| 》

°……….·}珊|



』憾…{{k霉蜜蹦坷… ′

3蹿,

′ 』

382 383,

st◎「G吕 p0

「Oute『:■‘

! 381′

厂e∩de「: fu∩〔t1o∩《e》{ 定tu丁∩●《∑)



! 3哪

|巍 }嚣;聪}!嚣!洲‖ |巍| :螺;则艘i;耀}e{′ti|"}《}, }豁



卜‖·





『_

393.})〗

■●△!

》 。.exp。吭.=".p々。1m′logo.a508a8『0.mgⅧ 二

《》 u吨3斟】cO‖{』∏了∏↑

图llˉ68改后的JavaScript文件 ▲尸β=■尸‖■『■■尸『■砂『△■厂△■尸巴■尸■∏||巴■『~■『‖‖|巴■■■‖■■■■『■■■■■【‖■「『■=【■「|巴■「[仲■卜‖【■尸■「山■■[「■■■「‖|■■‖|匹■厂|‘上■∏〗‖匡■■■■■■■■■∏

替换完成之后’重新刷新网页,这时候发现不会进人无限debugger模式了。 注意如果此操作不熟悉’可以参考ll.2节的内容°

另外,我们不仅可以使用Charles`Fjddler等抓包工具进行替换’也可以使用测览器插件ReRes等 进行替换,还可以通过Playw∏ght等工具使用Requestlnterception进行替换。这三种方式达成的效果 是-致的’原理都是将在线加载的JavaSc门pt文件进行替换’最终消除无限debugger° 5.总结

本节讲解了无限debugger的绕过方案’包括禁用全局断点、条件断点、替换原始文件等’从这些 操作中我们也可以学习到一些JavaSc∏pt逆向的基本思路,建议好好掌握本节内容°

↑↑。5使用尸yt∩o∩模拟执行」aγaSc「|pt 前面我们了解了_些JavaSc∏pt逆向的调试技巧’通过_些方法’我们可以找到一些突破口,进 而找到关键的方法定义°

比如说’通过一些调试’我们发现加密参数to代e∩是由e∩〔rγpt方法产生的°如果里面的逻辑相 对简单的话’那么我们可以用Python完全重写-遍°但是现实情况往往不是这样的’一般来说,一些 加密相关的方法通常会引用一些相关标准库’比如说JavaSc∏pt就有一个广泛使用的库,叫作crypto0s’ 这个库实现了很多主流的加密算法’包括对称加密、非对称加密`字符编码等。比如对于AES加密, 通常我们需要输人待加密文本和加密密钥’实现如下: 〔o∩5t〔1Phertext≡(ryPto〕5.A[5·e∩〔rypt(‖e55age’|(ey〉。tO5trj∩g()〗

| 》

期] ˉ

…围

■』‖■‖】日





………



■呻时的

ˉ纠』Ⅱ—、

■γ‖【队‖■【□△

_虱





—}|||

『『『‖‖研■■■■■■■_■■■■■勺_』』‖『【份〖‖『『『【『β【【□Ⅱ傅巴愿■四■■■区■·三『『『鹤■

■一_





丙『‖什【‖●■■■】●沮勺间■■■日组■△■■■■世■■■已■■■■■司月日■=■■岗苟■召■芍■□】

…亏宅

匹△■值■匪■■『…

=四』

咕0留■●

乙‖‖′匹…司■∏ 凸■■■■■■■■■■■■■■】『□』‖勺『』·■■芍

■』

|.『

蓟ˉ

』■』■□】日

….m

…雪_ˉ .

o油惮』田 鸽凹m「 ■■ Q?宁子币







ˉ 叼 …

▲■■}■‖[■|匹■【β|卜|■■「■尸■【■}Ⅱ「【「||■■厂}|■ˉ■□■′坠ˉ■尸■■「|(·「)|』■尸『|〖■厂|‖∩‖「{■■[匹尸〖■■}卜卜』■β『[‖[∩‖|Ⅱ■‖●「|》【■『|■■■厂`ˉ■|‖ˉ■[β}|■【■[

:蔚虹趾宴土_夏瞬挝驯马耳[」}驻肆舜‘曰二

豺工男硼丁卿却测‘马姆JdUoSuAPf脏奉米露业马宴JdUoSpAEf聊牢卿裂号侧且耳丑细Sfoox3Kd

°s!OoxMd/J9]u9O◎dEJosdm鳃//:s匈‖ :肆零门胆融再葛咀胜臻磊阳膨燕

皿重。s『9pol`l吾阴裳斡酵羽γ↓‘露业旦耳]dpoSEAuf+-臻矗量墨强[」}驻‘{‘罕割+熏却臻矗上翱 门蛔‘霹业马宴]dUoSu∧Ff醚驯量墨鹃贩阳JdUoSB^uf旦瞬卧·阳JdUoSuAFf纱峰士出晋SIo9x∏Kd 5〔〕9X9∧d∏e]Su『[d『d

:土师刺首‘a韩辜令粤[d『d

出到[‖鞋。Sfo9x3Kd珊b〗荆阳出到川鞋亩熏·JdUoSP八P『坠瞬僻辫uoq】∧d出到量墨川驻‘申黔丰 驯工暑默.z 。米阳码

慧融再职币阳串母±墨皿砾组融出挺裂茧墨皿阳审毋±墨皿↓熏昭群晋鹏阳嚼量[」}畦甭硬‘门姐

。阳凹业晋印串母章墨‖《阳亩骡\↓鸳日舞‘阳猫莽旦晋冒剿阳雷狸毗遮茸串毋章墨皿↓璃‘审斜±晕‖!↓ —旦蝶奉茸可剁斗来鸳‘{‘告。冒割伞霖阳百瞩雨—上匹翘辛组阴剁斗出‘咖囤百骡VH凤`|/_晋璃 咖囤雪聪V日N69ˉ[[圈

…….m

△≡…

…庭↓』面‖

剿‖ “ ˉ

鞘| -

■| |割毛

| `ˉ参二二ˉ厉….…惠 ●■●GO



m

=ˉ宁~ˉ



‖●

°巫蛔

69ˉll圈"‘/J90mOmmOS.Lpds〃:s山0q晋聂鹅‘赐章阳∏′辟巢非钮斟阴辗鹏x丁畦↓一垦辛[」}堆宙熏 Y|巨财窜.↓

°辈耳艰溯阳JdUoSp^pf趾瞬附褂uoq认d出到糊上米孵[‖唯‘中斗立 乙知上卸孵业莆彰‖丢鄙JdUoSuAEf碌熏 尘雌僻爵爱直uo哑d阻鞋彭〗〖‘上却脏近营□JdUoSuAPf群扭切旦乙知裂哗暇幽7仆旦‘7〗【 °荆柔卵#-手

辈JdPoSuAuf雌旦串—业抵中uoⅥMd熙团‘醛—宣重手塞职曾鼓诽裂召透茸[‖瞻Ⅳ剿明#璃士躯

牢淑粤霹]d!JOSuAuf夺II瓷

9汁

【‖△■■【凸■‖|}|■■■「|■■尸□‖ 卜△■厂||■『‖|巳■ 尸『巴『‖「

ll.5使用Python模拟执行JavaScript

“7

1ⅦPortexe〔]5

prj∩t(exe〔j5·get().∩a们e)

匹=广■■‖ 似■■|

运行结果类似如下:

△■厂|‖

‖ode·j5 (V8〉

3.分析 接下来’我们就对这个网站稍作分析。打开Sources面板,我们可以非常轻易地找到加密字符串 的生成逻辑’如图llˉ70所示。

比约ht8 『】蝇“‖0

畦10∩t〗 Q79·叮腮8 『

》D

№(《



』■■』『

%卯鳃”的饥■归“仍铺们靶帕泌汕泌凹Ⅻ沮犯Ⅵ泊Ⅻ汕 〗【〗〗Ⅱ]』】〗Ⅱ』】Ⅱ】】】让』】』Ⅱ

·l8 ·…0’‖

叮●8 ↑断ct…()《 定to「∩《

pu沁厂5β key〗 0f〗pwγ凸Z∏『0a…】7LQ…r仁』… t



》0 …t…5‖{

供∏◎■CM■la沁『)《

}{

■『■厂′|■「|}■止■「|卜【●『‖【『|【伊「们=■【「‖山【_尸)‘‖■∩

]脱》

■「}|■|■■[‖■尸‖}■=■尸|巳■■卜||『■∏『■尸‖|·【尸匹■■■尸|』

如果你成功安装好PyExecJS库和NOdejs的话’其结果就是‖ode.j5(γ8)。当然,如果你安装的 是其他的JavaSc门pt运行环境,结果也会有所不同。

◇吐『0·p■丁■●(t倔1■.

撼‘阔畦『瞬{;翻.龋§′器删 胎』ghtpwe1ght》 pwe1ght》 ·由9…o■t



■∩厄wpt〔0$

gh 》`0№γ0 {

「·tu厂∏m仁『ypt“αtOSt7』呵(} 》 }

图llˉ70

Sources面板

首先,声明一个球员相关的列表,如:

β 广 · 「 『 ■ ■ 厂 } 巴 尸

co∩StP1ayer5= [ { ∩a们e月 ‖肌丈ˉ杜兰特‖’

j爬ge; |dura∩t·p∩g0 ’ bjrthday: 01988ˉ09ˉ29‖ ’ hejg‖t8 |208C∏’ wejgbt: !1O8°9Ⅻ0 }

[们

‖)■尸|‖』■『‖■「)}′ ■ ■ 尸 |

然后对于每-个球员,我们调用加密算法对其信息进行加密。我们可以添加断点看看’如图llˉ7l 所示° 1“|∩酣V畦(《

e1: ,Fapp》′

101!

■尸『『■■

「efu厂∩{ 搬| d…↑""ct1°"() {

10q{

plWe厂吕′

擦| 》)……·…假……』 }"↑ip尸?vs∑垦『da凹hJ雌]↑〔。aqγ职Z「尸1翻lt“

1枷{

醒t№d酗{

: 3S■■ _



God●: 〔印p〔o』5.…G°医〔00

1M!

113{

pDdm∏g; 〔〃pt◎〕5op□α.pkc£7 })

滁|

「et仙厂∏e∏C厂γpt●d·to5t厂1∩0() 》



》夕



□· ‖≈】 Ⅱ】

116‖

图l1ˉ71

添加断点



‖、』』■∏二□■|二■

虫 爬





γ

°∏





γ

α

∩●





] ]





β



可以看到’get『o促e∩方法的输人就是单个球员的信息’就是上述列表的_个元素对象’然后

t∩15.代ey就是—个固定的字符串°整个加密逻辑就是提取球员的名字`生日`身高`体重,接着先进 加密算法是怎么实现的呢?其实就是依赖了cryptojs库,使用CryptoJS对象来实现的。

那么’CryptoJS这个对象是哪里来的呢?总不能凭空产生吧?其实这个网站就是直接引用了 c】yptojs库,如图llˉ72所示。 臆田《 臼曰"印拘

O°痢s°佑



彪四α伐

Sm.·s

◆●Fα□∩…Ⅶ蛔■…□四■

尸■加耐…· O撇晦

v

刚钢γmγ

勺pk虱m

l绚狱mu酶 亏▲

∧由…《

■■|」】|』■■∏』■■可|□■■■

行Base64编码,然后进行DES加密,最后返回结果°



士』

-≈

q塑绥旦∩吗@Ⅺ恫」ScSS盯U‖‖·°h「啦°°c呢q‖囱‖怕s`…□脑圃。c点.d.。。№G□… x 卜…啼…唾■…m

…唾 粕…

_—

—___

丁↑『触吧

_

-

l !『鲤瑶瓣黑{γ…↑………`e.…「《………{|; .′o"….=

翻…7……,叮

≈》(t∩1β0 ↑呻〔t1o∩《)《 va「∩〃 t0 ■d 『0 』p Ⅳ0 γ『 O0 $O C0 ■p l′ d·田0 x’ bβ "0 Z0 ∧0 uU p0 =0 γ,γp 0

翘阻墨v…ˉα…v…

1γ(蔷……|ˉ……………“|………》 lt唾时O…f1框d■『击ty…7■1n…“门』咽唾°■2〔叮pt◎“(t■m…°■Z〔

!………" !.…『,…`隘,…l……《t.,…`…、 !t“钵↑‖mct1m门…【y“Ov了c呻』屈》

墨蜜…

t叮《 》〔■t吮(t)《》 「U№t儿mM》[

1『(I↓↓Ⅷ…厕.=w………… t叮{

"′!《;燃|鸳镍棚脚驴…《』""|

圈阑

;



…扫

"·枷{……(……)

‖|口‖

翻瞬:………

■■■』■司

阑醚c.…和

d

‖‖‖

`.…『詹(.c….′

国谣…



{|‖

陛[_

t∏「≈∩≈匠「「◎「(‖N■t』吐乞叮p[o…u1eCouId『mtbe仙Smto0erZeCu『‖ 》



γa「厂■0bject°c了它me | ‖ fu∩〔t』◎∏《t‖{

坐e●T

va厂G;

凹啦匈…m必“嚷↑S您州油V沦.

7■tu厂∩∏·p■totγ砷年tβ e汀∏卸∩p

∩·p「ot◎type■∩Mup

4.模拟调用

既然这样’我们要怎么模拟呢?下面我们来实现一下°

首先,我们要模拟的其实就是这个get丁o促e∩方法,输人球员相关信息’得到最终的加密字符串。 这里我们直接把低ey替换下,把get丁o促e∩方法稍微改写一下’具体如下:



因为这个方法的模拟执行需要CIyptoJS这个对象,如果我们直接调用这个方法’肯定会报 CryptoJS未定义的错误°

怎么办呢?我们只需要再模拟执行一下刚才看到的cryptojsminjs不就好了吗?

《‖』■■■』=■■■■‖□■■■■‖《||』■■』■|‖《‖‖■■■■‖|‖■■■||■■■{」·■』□■」□』■■Ⅵ‖』■〗日■■』·‖』■|‖·可‖」■■』』■■■

key’ { ‖ode; 〔rγpto〕5。∏℃de。[〔8’ paddi∩g: 〔rγpto〕5。pad。p代c57’ } )j retuI∩e∩〔rypted.to5trj∩g()j

‖日

「u∩〔t1o∩get∏o低e∩(p1己yer){ 1et低ey=〔rypto〕5·e∩〔.0t十8·par5e(0』fip「W5Z5丁da9』‖〕邯〕千loaqyq眶「「iⅧ[t|』)j co∩5t{ ∩a爬’ bjrt∩daγ’ ∩e1ght’ weight }=p1ayerj 1etbase64‖a爬=〔rγpto〕5。e∩c.8a5e64.5tri∩gi十y(〔rypto〕5.e∩c.0t+8·p3r5e(∩3爬))j 1ete∩〔Iypted=〔rypto〕5.0[5.e∩crypt( 、${b35e6耳‖a爬}${bjrthday}${heig∩t}${wejght}`’

■■■■·」■■|‖|■■】‖』■|(|■■司||

执行clγptojs库对应的这个JavaSc前pt文件之后,C!yptoJS就被注人测览器全局环境下’因此我 们就可以在别的方法里直接使用CIyptoJS对象里的方法了°

勺`‖司(‖】■∏

图llˉ72 cryptojs库对应的网络请求

■■■「}■■『}巴■β‖『`「仁『‖|■■「卜「

} ll5使用Python模拟执行JavaScript

“9

因此,我们需要模拟执行的内容就是以下两部分°

□模拟运行c【yptojs.minjs里面的JavaScnpt,用于声明C【yptoJS对象° □模拟运行get丁o促e∩方法的定义’用干声明get丁o代e∩方法。

匹尸||)卜『■「||

接下来’我们就把cryptojsminjs里面的代码和上面get丁o促e∩方法的代码复制_下,都粘贴到个JavaScrlpt文件里面,比如就叫作c1ypto」s°

巴■■「β口■尸》‖‖‖尸尸

接下来’我们就用PyExecJS模拟执行—下,代码如下: i‖poItexe〔j乌 i们poItj5o∩ ite"={ ,∩日Ⅶe‖ : ‖凯丈ˉ杜兰特‖’

b

■■「『任【■‖●伊■尸‖■■厂■■β~■『■‖‖ ■ 厂 |

P ∩ 『 卜

)卜 卜‖‖ 】』







|i∏己ge]: |dum∩t.p∩g‖』 0bjrthd己y‖: ′1988ˉ09ˉ29! ’ 0∩ejght0 : 02O8c阳|’ 0wej8ht‖ : !1O8°9ⅨG

+j1e≡ !〔Iypto.j50 ∩ode≡execj5.get() 〔tx=∩ode.coⅦpi1e(ope∩(「i1e).read())

j5≡千"get丁o贺e∩({jso∩.du刚ps(jte们′ e∩5uIea5cji≡「a15e)}〉" pIj∩t(j5〉 Ie5u1t=〔tx.eγa1(j5) pri∩t(re5u1t)

这里我们单独定义了_位球员的信息,并将其赋为1te"变量。然后使用eXeCj5的get方法获取 JavaSc门pt执行环境,赋值为∩ode° 接着’我们调用∩ode的〔o们p11e方法’这里给它传人刚才定义的cⅡyptojs文件的文本内容。co们Pj1e 方法会返回_个JavaScnpt的上下文对象’我们将其赋给〔tx°执行到这里,其实就可以理解为’〔tx对 象里面就执行过了cIyptojsmjnjs’CⅡyptoJS就声明好了,然后紧接着get『o促e∩方法的声明代码也被 执行,所以get『oke∩方法也定义好了’相当于完成了一些初始化工作。

接着,我们只需要定义我们想要执行的JavaSc∏pt代码。我们定义了-个j5变量’其实就是模拟 调用了get「o恨e∩方法并传人了球员信息°打印j5变量的值’内容如下:



们 k

8et丁o恨e∩({"∩a雁": "凯丈ˉ杜兰特"′ 』』1帕ge": "dum∩t.p∩g"’"bjrtMaγ": "1988ˉ09ˉ29"’ 』』hejght!』: "2O8c""’ 0Wejg∏t!0 : "1O8.9瓜6"})

其实这就是_个标准的JavaScnpt方法调用的写法而已°

接着.调用ctx对象的eγa1方法并传人j5变量’其实就是模拟执行这句JavaSc∏pt代码’照理 来说最终返回的就是加密字符串了。

0

然而,运行之后,我们可能看到这个报错:

)》’

}|

。■「》「|■■■■「■尸【『「■■【■巴尸『[■

exe〔j5.ˉex〔eptjo∩s.pIogra爪[Iror: Re千ere∩〔e[rIoI:〔rypto〕515∩otde+1∩ed

很奇怪’ CryptoJS未定义?我们明明执行过 cryptojsminjs里面的内容了呀?

(}u∩危7‘o∏ (『u∏℃丫6o0

‖』obje〔t加

(l ′p

) ′{ [

·γpeo

; 酣‖↑u∩Ct1O∩|‖

问题其实出在cIyptojsminjs’可以看到其中声 明了_个JavaScnpt的自执行方法如图llˉ73所示°

expo『t5

T (Ⅶodule.exp◎rt3=expo「tS~e(》) tγ

o↑de↑i∩e醋0e↑1∩e°a闸d

引血{j『】乙([]p e》

(t°〔『γptoJ5≡P()); })(

0

ˉ [川〔`16∏ (){

什么是自执行方法呢?就是声明了一个方法’ 然后紧接着调用执行。我们可以看下这个例子:

});

图1lˉ73 JavaScrjpt的自执行方法





!(+u∩〔tjo∩(a』 b){〔o∩5o1e.1o8(‖re5u1t,’ a’ b)})(1’ 2)



的参数就好了,比如传人1和2,执行结果如下:



这里我们先声明了-个十u∩〔tiO∩’它接收a和b两个参数,然后把内容输出出来’接着我们把这 个「u∩ctjo∩用小括号括起来°这其实就是一个方法,可以被直接调用’怎么调用呢?后面再跟上对应

■■■司■■·|■司‖■■■||』‖■口‖‖‖

第l1章JavaScrjpt逆向爬虫

450

re5u1t12 q

可以看到’这个自执行方法就被执行了。



同理, cIyptojs.mjnjs也符合这个格式’它接收t和e两个参数, t就是thj5’其实就是测览器 中的wi∩dow对象’e就是_个+u∩ctio∩(用于定义CryptoJS的核心内容)° 我们再来观察下cryptojsmjnjs开头的定义: |!object" ==typeo十export5

?de十i∩e([]’ e)

: (t.〔ryPto〕5=e()〉j

在NodeJs中’其实export5用来将一些对象的定义导出,这里"obje〔t』|=typeo十export5的结 果其实就是true,所以就执行了们odu1e.e×port5=export5=e()这段代码,这相当于把e()作为整 体导出’而这个e()其实就对应后面的整个十u∩〔t1O∩°+u∩Ct1O∩里面定义了加密相关的各个实现’其 实就指代整个加密算法库°

到t‖15对象上面,而t∩15就是测览器中的全局wj∩dO"对象,后面就可以直接用了。如果我们把代码 放在测览器中运行,那没有任何问题°

对象里面,所以后面我们再调用CIyptoJS就自然而然出现了未定义的错误了。

怎么办呢?其实很简单’直接声明_个CryptoJS变量’然后手动声明_下它的初始化不就好了 吗?所以我们可以把代码稍作修改,改成如下内容: γaI〔ryPto〕5j !(千u∩CtiO∩(t’e){

〔ryPto〕5≡e(); "obje〔t"≡typeo千export5 ? (晒du1巳export5≡export5=e()) : "十u∩〔tio∏" ==typeo十de十j∩e肥de千j∩e.a刚 7de+j∩e([]’ e) : (t.〔IyptO〕5=e())】 })(thi5’ +u∩CtjO∩ (){

』■|■■·■■〗■■■∏』■】|」■■]■■】

然而,我们使用的PyExecJS是依赖于-个Nodejs执行环境的,所以上述代码其实执行的是 Ⅷodu1e.export5二expoIt5=e()’这里面并没有声明CryptoJS对象’也没有把CryptoJS挂载到全局

0

■■■·■∏·口■■‖纠■‖』■■]」■‖

但是在测览器中,其结果就不一样了’测览器环境中并没有export5和de+j∩e这两个对象°所以, 上述代码在测览器中最后执行的就是t.〔rypto〕5=e()这段代码’其实这里就是把CryptoJS对象挂载



』■□‖‖」■】■可」■■■司』■■‖』■

? (加du1e.export5=export5=e()) : 00+u∩〔tio∩" ==tγpeo十de十i∩e腿de十1∩e°a们d







//°· .

这样我们再重新运行刚才的Python脚本,就可以得到执行结果: 这样我们就成功得到加密字符串了,和示例网站上显示的_模_样,这样我们就成功模拟

JavaSc∏pt的调用完成了某个加密算法的运行过程°

{ | |

gQ5+eq1山I〕贸An删9『Z【X/eXγIw恤j73b2〔jXⅦ6peZ3r田65Q5[2W■=



二■■』■■‖‖□·】■■

这里我们首先声明了_个CryptoJS变量,然后直接给Oypto」S变量赋值e(),这样就完成了 CIyptoJS的初始化°

‖‖

});

{|

q

■口『【=■∏■■■■【■■『■□‖『|巴■■『·【〗『■■「■■■仿|‖八■厂)|■■■■■『||‖‖β□■厉}卜广「■■■

‖)

∏』■■■■‖■ 巴■尸|■■■「



| ■『‖‖「∩「〖【伊》凸■‖{■「■■■■■〖尸·■【尸|■■尸

) ■『|}△■■{■『||△■■ ■』



ll.6使用Node.js模拟执行JavaScrjpt

451

5.总结

本节介绍了利用PyExecJS来模拟执行JavaScnpt的方法,结合-个案例来完成整个实现和问题排 查的过程。本节内容还是比较重要的’以后我们如果需要模拟执行JavaScnpt’就可以派得上用场° 本节代码参见: https://github.com/Python3WebSpider/ScrapeSpa7°

↑↑.6使用Node.]s模拟执行」aγaSc「|pt 在上_节中’我们了解了利用Python来模拟JavaSc∏pt调用的方法,使用的库是PyExecJS’其执 行环境我们选用的也是Nodejs,但有时候在调用过程中我们会发现这还是有不太方便的地方’而且可 能也会出现上-节提及的变量未定义的问题。有没有其他的解决思路呢?

我们模拟执行的是JavaSc∏pt’而且依赖的是Nodejs,为什么不直接用NodeJs来尝试JavaScnpt的 执行呢?其实原理上来说这种方案是完全可行的。

本节中,我们就来了解使用NodejS来执行JavaScnpt的方法° ↑.准备工作

本节中,我们需要使用Nodejs’请确保已经正确安装好了Nodejs,安装流程可以参考上一节的 说明°

安装完成之后’我们应该可以正常使用∩ode和∩p『∏两个命令’如不能使用,请检查Nodejs的安 装情况和环境变量的配置°

2.模拟执行

本节的案例和上-节完全_样’我们想要的其实还是计算出每位球星所对应的加密字符串。所以

整体思路其实还是加载Crypto库并执行get「oke∩方法,这里我们直接基于Nodejs来实现°

首先,还是把cIyptojs.mjnjs文件中的内容复制下来,新建一个c【yptojs文件并把内容粘贴进去° 然后新建一个mainjs文件,其内容如下: 〔o∩5t〔Ⅳpto〕5≡Ieql』ire(励./crypto圃)】

十u∩ctjo∩8et丁o攫e∩(p1aγer){

1etkey≡〔【ypto]S.e∩〔。0t十8.par5e(口十jp「刊5Z5『d己94h]‖Ⅸ〕代oaqyqNZ仟i响lt·〉;

〔o∩5t{ ∩a眶’bjIt∩day’ hejght’髓i8ht}=p1ayer;

1etb35e64付蓟论≡〔rypto〕5·e∩〔。Ba5e64.5trj∩gj十y(〔rypto]5。e∏〔.0t「8。p己r5e(∩a爬))〗

1ete∩crγpted≡〔Ⅲypto〕5。0[5.印cⅢypt( `${b己5e6州a爬}${birthday}${∩eig‖t}${眶jg∩t}、’ keyp { 『帕de:〔rypto〕5·晒de°[〔B’

paddj∩g: 〔Iypto]S.pad·pk〔57, } )§

retur∩e∩〔rγptedto5tri∩g()j }

〔O∩5tP1ayeI≡{ ∩a雁: ■凯丈ˉ杜兰特脚’ ■■

1帕ge8 "dum∩t.p门g ’ birtMay: ·1988ˉ09ˉ29"』 ‖eight: 口208〔∏口》 wejght: ■1O8.9Ⅸ6" }

co∩5o1e。1og(get丁o低e∩(p1ayer))

试里我们直接使用Node』s中的requ1re方法导人c【yptojs这个文件’然后将其赋值为CryptoJS对 ●

象’这样其实就完成了CryptoJS对象的初始化了,后面我们就可以正常使用OyptoJS对象了° 这时候读者可能会有疑惑:上_节中我们用的PyExecJS的底层也是用Nodejs模拟的呀?在上_ 节中,我们需要修改代码才能完成CryptoJS的初始化’那这次为什么什么都不用修改就能完成

』■■

CryptoJS的初始化呢?

』■■■二■】纠·‖□『纠■‖‖』■■

第ll章JavaScript逆向爬虫

452

继续回过头来观察cIyptOJS中最开头的定义:

□‖□■■■■可】■■司』】■可

|(十u∩Ct1O∩(t’e){ "obje〔t°≡typeo+export5 ? (加du1e。export5=export5=e()) : "+u∩〔tio∩" ==typeo+de+j∩e88de十1∩e·a[∏d ?de千j∩e([]’ e) : (t°〔rypto〕5≡e())j })(thi5′ 于u∩〔t1O∩ (){ //°. °

+u∩ctjo∩里面定义了加密相关的各个实现’其实就指代整个加密算法库°

方法来进行-些加密和编码操作了°

CIyptoJS初始化完成了’接下来get『o代e∩方法其实就是调用CIyptoJS里面的各个对象的方法, 实现了整个加密流程,整个逻辑和上一节是—样的。

最后’传人p1ayer对象,然后输出对应的加密字符串即可。 运行mamjs’命令如下: ∩ode盯aj∩.j5

得到的结果如下:

经过比对’结果和网站上的结果(如图llˉ74 所示)是_致的°



ˉ杜兰特 …和

3.搭建服务

↑阳9ⅨG

图llˉ74网站上的结果

|■】}■■』■』■‖|·]‖曰■】Ⅲ■■

但如果此时我们就想用Python来编写整个爬虫’怎么办呢?该怎么和NodeJs对接呢?很简单’直 接使用Nodejs来把刚才的算法暴露成—个HTIP服务就好了,这样的话Python直接调用Nodejs暴露的 HTTP服务’通过ReqUestBody传人对应的球员信息,然后加密字符串通过HTTP的Response返回即可。

』■■■■]当■■■■■‖』■〗■■|』■■∏

就可以了。 就口」』以「°

】‖

刽■■司‖‖|』■■∏■】】

现在我们其实已经能够使用Node』s完成整 个加密字符串的生成了,完全用Nodejs编写爬虫

』■■■‖■■■(■■

这样我们就成功通过Nodejs完成了整个 JavaScript的模拟过程°

|恿照鹏…咖….……………|

□■|‖■■|□■‖

Ⅲ1u渊q1例70e‖hd571‖1酬|ˉ℃oI2t「p"〔B帆ppoo〔Wqpt们1「瓜j「u9【1u‖o2wm」"

‖{‖』‖■■】ˉ■」·‖‖■‖‖ ■ ■ ■ ■ 可 ‖ ■ ‖ 』 ■ ■ · 〗

正是因为我们在Nodejs中有和export5配合的requ1re的调用并将结果赋值给〔rypto〕5变量’ 我们才完成了CryptoJS的初始化°因此’后面我们就能调用CryptoJS里面的DES` enc等各个对象的

铅 ■ 】 ■ ■

既然在cryptojs里面声明了这个导出’那么怎么导人呢?requ1re就是导人的意思’导人之后我 们把它赋值为了整个〔rypto〕5变量’其实它就代表整个CryptoJS加密算法库了°

■ ■ 』 ■ ■

通过上_节的说明我们知道’在Nodejs中定义了export5这个对象’它用来将一些对象的定义 导出°这里"object||=tγpeo十export5的结果其实就是true’所以就执行了们odu1e.export5≡export5 =e()这段代码’这样就相当于把e()作为整体导出了’而这个e()其实就对应这后面的整个+u∩〔t1O∩°

‖{■■■』■■■■■■

})j

】『■■■『「[■■厂‖卜|亡■|‖|「■尸|‖|巴■尸】■「「■■「|‖|□=尸巴■尸‖巴尸‖巴=■尸|■■‖|■■■‖口「

■■■■庐′■匹∏■■▲■『■■■尸|■■∏■厂■■●「‖尸■叮‖■■■■「|■「■■「‖矽〖β|伯■■■■··■■「■■■尸巴∏■■=尸「●厂 ■尸【■■‖■■「▲■厂●『



















ll.6使用Nodejs模拟执行JavaScript

453

那么HTTP服务用什么来实现呢?Nodejs中最流行的HTTP服务框架当属exp『ess了,所以这胆 我们就选用它来作为HTTP服务器°

首先安装express,在mainjs所在目录下运行如下命令: ∩咖1exPre55

然后改写majnJs为如下内容: 〔O∩5t〔ryPtO〕5=IeqUjIe("./〔ryPtO圆)j 〔o∩5texpre55=req‖jre(脚expre55圃)j 〔O∩5taPP=exPre55()j CO∩5tpOrt=3OOOj

日pp.u5e(expIe55。jso∩())i

千u∩ctio∩get丫o代e∩(p1ayeI){

1et代ey=〔rypto]5。e∩〔.0tf8.parse(!干ip「ⅣsZ5「d己94∩〕肌〕仇oaqyq∩Z「「jⅧ[t"); 〔o∩5t{∩a眶’ b1rthday’ ∩ejg∩t’wejght}=p1己yerj 1etba5e64‖a『∏e=〔rγpto〕5。e∩〔.8a5e64.stri∩gj十y(〔rypto〕5.e∩〔·0t千8。par5e(∩a爬))j 1ete∩〔rypted=〔Iγpto〕5.0[5。e∩〔rypt(

`${ba5e6』‖a爬}${birt∩day}${hei8∩t}${Wejg}]t}`’ Rey’ { ‖me8 〔rypto]5.加de.[〔8’ padd1∩g: 〔rypto〕5.pad°p代〔57, } )j retur∩e∩crypted.to5trj∩g()j }

app.po5t("/"’(req’ reS) m〉{ co∩5tdata=req·bodyj Ie5.5e∩d(get『oke∩(data))j })j

app.1i5te∩(port’(〉=〉{ 〔o∩5o1e.1og(`[xaⅦp1eapp1j5te∩j∩go∩port${port}! 、); });

这里我们就使用express编写了一个服务’它可以接收_个POST请求,RequestBody就是球员信 息’然后返回get『oke∩的计算结果作为Response的内容° 接下来’重新运行该脚本: ∩ode帕i∩.j5

这时候可以看到’express就在本地3000端口上运行了° 如果我们想用Python调用的话’直接使用requestS调用该API’然后传人对应的球员数据即可, 示例如下: i"Portreque5t5

data≡{ "∩a爬′|: 00凯丈ˉ杜兰特"’ 圆jⅧage冈: 口dum∩t。p∩g"′

0birthdayw: 口1988ˉO9ˉ29"’ 0|hejght碱; 闪2O8c们"’ 0Wejgbt阑g 面1O8.9瓜C" } uI1= 0bttp;//1O〔a1∩O5t;30OO0

re5po∩5e≡reque5t5,po5t(uI1’j5o∩=dat己〉 Pr1∩t(re5po∩5e.text)

运行结果如下: DC1u附q1"7Oe‖hd571‖15"}{0oI2t「p‖〔8叭pp00cγ「qpt‖1郎j「u9R10"o2w3献川

第ll章JavaScript逆向爬虫

Python进行后续的分析`处理操作了。 4总结

本节中’我们介绍了利用Nodejs进行JavaScnpt模拟的方法,并介绍了Nodejs和Python进行对 接的方式—通过express暴露HTTP服务°此种方案对于JavaScnpt的兼容性也会更好’对于模拟执 行JavaScript也会更加方便。

■■■‖]□∏』■■■习‖|(|』□Ⅱ|||』■■

这样我们就成功实现了Nodejs到Python调用的转换’这样爬取到数据之后,我们就可以使用

ˉ■|■‖■■■γ‖■■■■■

454

本节代码参见: https://githuhcom/Python3WebSpideI/ScrapeSpa7°

在前面两节中,我们了解了利用PyExecJS和Nodejs对JavaScnpt进行模拟执行的方法,但在某 些复杂的情况下可能还是有一定的局限性° ↑.分析

比如说:我们在测览器中找到了_个类似的加密算法,其生成逻辑如下: co∩5tto低e∩=e∩〔rypt(a’ b)

我们最终需要获取的就是to仪e∩这个变量究竟是什么。这个toke∩模拟出来了,就可以直接拿着 去构造请求进行数据爬取了。但这个to低e∩是由一个e∩crypt方法返回的’参数是a和b。对于参数a 和b’我们可能比较容易找到它们是怎么生成的’但是这个e∩〔rypt方法非常复杂’其内部又关联了 许多变量和对象,甚至方法内部的逻辑也进行了混淆等操作’向内追踪非常困难。 这时候如果我们要用Python和Nodejs来模拟整个调用过程’关键其实就两步: □把所有的依赖库都下载到本地;

□使用PyExecJS或Nodejs来加载依赖库并模拟调用e∩Crypt方法° 但在某些情况下可能存在_定的问题’我们分两个方面来进行探讨° ●环境差并

前面提到过’NodeJs中没有全局"j∩dow对象,取而代之的是81oba1对象。如果JavaScrjpt文件 中有任何引用wj∩do刊对象的方法,就没法在Nodejs环境中运行°我们需要做的就是把w1"do切对象改 写成g1oba1对象,或者把_些测览器中的对象用_其他方法代替。 ●依赖库查找

在上面的例子中, e∩〔rypt所依赖的全部逻辑和依赖库其实都已经加载到测览器。如果我们要在 其他环境中模拟执行’要从中完全剥离出e∩〔rypt所依赖的JavaScmpt库,肯定还需要费_些功夫。 _旦缺少了必备的依赖库’就会导致e门crγPt方法无法成功运行° 对于_些复杂的情况,为什么我们不直接用测览器作为执行环境来辅助逆向呢?

本节中,我们就来介绍_个借助测览器模拟辅助逆向的方法,可以实现任意位置的代码注人和修 改,同时可以实现全局和任意时刻调用’非常方便°

』■□■■‖{■■■■·■■司□■】■■■■■■‖|■■】■』‖■■■司·』■■]‖‖』■〗二■■‖』■】■]·」■■■■‖■■■γ』■■■可』■■■‖肌】■■■|』■可|■■」■■〗□■‖·■■■口{□■]司|』‖‖』■■■■可‖』■■■

↑↑.7测览器环境下」aγaSc『|pt的模拟执行

2准备工作

本节中’我们使用playw∏ght来实现测览器辅助逆向°首先,安装playwright’相关命令如下: pjp3j∩5t己11p1己ywright p13yNri8hti∩5ta11





可■■■■■Ⅲ



ll7测览器环境下JavaScript的模拟执行

455

运行如上两条命令之后’会安装playwrjght库’并安装Chromjum、Firefbx`WebKjt三个内核的

测览器供playwrlght直接使用。具体的安装方法可以参考; https://sempscrapecente门playwright。 3.案例介绍

β

本节中,我们要分析的目标站点是https://spa2.scrapecenteⅣ。可以看到’其Ajax请求参数带有— △■「‖‖·「匹■∏‖』■『匹■厅】『|〖■尸|·■尸‖‖■【■「》∩「■【■尸【■尸|

个toke∩’并且每次都会变化,如图llˉ75所示°

■臆寅~…宅■



雾T≡=~==字…罕●;

曰sc『…

碾田

倘墅樊原….||Myc…|".

固…■≈…斡…↓幻…≈……·南屯v ∩p…k…m≤……m

●Ovα曰品卧·印曰钠……■№甘m世℃ 甲冤, 品 ¥…≈



_——塑 | ■

■…

≡=||

U



_■_—

-

×

§

亭▲

严葛盲…亏孟呵攀岭兢《獭霹颧涸器缉黔些鳖…霍严譬…黔翘霉 _■—



p …≈

≡■

=言刁

!

厂……南■F…】…吧

^…p



雹淫…〃……… 酝…~

‖……岭丁



‘…厂→●m肛

[…=101.m白Ⅶ.】】】↑与q】

=■「■尸■■『

…嗜■q「此t由F幻h=【巾m宅丁1U』■ 守

■●■



于……啥==

|…匹『。…『·…,硒Ⅱm

【_…℃11味 ■■■■「|‖『匹■尸

=l→z邓5 =…峙宁M″→1m′】… …P冗0 1P…m〗】『β80弓w口

图llˉ75 Ajax请求参数 添加XHR断点并通过调用栈找到to促e∩的生成人口,如图1lˉ76所示。

■·=′…



@

+争×(Ⅲ≡=ˉ

Ⅲ百己ˉ可蚕)禽●



蚀□



己 『

■-白

〃√

■▲尸∩■尸『「■■厂|■■尸■■「■■『匹=■∏但尸

__|墅蜜贾铲…』…….

|…●……Ⅲ□

0

} β 「



u6c

QG亡■》…《M■丁]》‖也』■.“m≈.■Q■r

lⅡ丁■



】幻?

Ol…Ⅱ

1■p

pα8m赋t≈1《7…□[G吕 10 R匡 7吕=…

p酗≡】■■l…c…》…◇…巾匹



p哇馆1Ⅶ弓′p…3fP…吕∩

‖》[

1碑0

p8≡0加■

…《■t忙』$B

潞}

◆k百′″



i雕搜鳞

恤』巳Ⅲ哇01囤

●=

】γ3 1y0

〗门 〗沁 】汀 1沽 1涌 【铸 〗0】 ‖■ 】田

■0

凶8■n p3≈【`

b〔Ⅷ一≈《■…穴■}

Ⅷ■OQ8 thm々B■■◆ˉ ●…8 ■·

●〔u≡疵(…】

●钟户8 ●

◆G【空I



■j=—

》〗丁~《‖↑…M凹〖●〗《 m7G乙■…■

0 D■@…‖t■ ■■

【■



》川

《●

t· t·

】出

酗 醛 】m 咀

●c9′″



】$】′ 1■ 】田: 蹿 』舶 】“p 107;



}}}卜



酗;

1弘:



◆一

:=



…~刀≡严乙

…=~体?g

~……→

α………厅…,■p

『……●…■≡



……万…押γ

一…≡…向=江↑

图llˉ76通过XHR断点寻找人口



■■■】]|‖』■■■‖‖‖|■■Ⅵ‖■■〗□∩』

第l1章JavaScript逆向爬虫

456

可以发现’请求参数的to长e∩就是变量e’它的生成过程如下:

γara=(t∩i5.Pageˉ 1)*thi5.1imt’e=0bjeCt(i["日"])(t∩i5。$5toIe。5tate.ur1.1∩dex’ a)j

在此处添加断点调试一下,看看具体的变量值’如图llˉ77所示。

—…

……

。 ~



…●

…回

]…

◆了`=ˉ…

●◆

旧…

K=二—_—ˉˉ悍~



|…切…协ˉ龟|

| d

‖』■

』■■|

「‖】|

{引 d

经过对比,可以很容易发现,变量己其实就是请求数据的O仟5et’数据一页l0条’所以第一页o仟5et 就是0,第二页o仟5et就是l0’所以变量a就是0、l0’以此类推。t‖15.$5tore.5tate.ur1.1∩dex是一

个固定的字符串/aP1/帅vie’但是调用0bject(j["a"])方法之后,结果e也就是最终的to代e∩就得到了°

■■□】■∏‖|』■■■■‖□』■■■■■』勺‖』■■■】‖|】■』■∏■■可|』■■■‖』·

图l1ˉ77查看变量值

因此’我们可以断定0bjeCt(i["a』|])里面就是核心的加密逻辑,我们再把1["a|』]方法追踪—下’ 可以看到如图llˉ78所示的逻辑° ≡■



-

……≈恼油■…mv≡l…m■

}牢

△_



咱咖m已↑哑鲤……7摔钩硒蛔咖口出≡40……扭



^濒? 〗仙3d 1▲▲3 l…

1叫S 1“8

血唾凸!……Q挥…篱 注



-



--==ˉ

b8…《●γm0酶`″

=划t1`■=…叮p巳≡贬0…~蹄丁巴Dˉ儿 -7…8≤砷t●■仔叹喳leT



扩●tg…t盯■』‖砷1e】

恤1Z·「O饱】经些↑』… 汰m°仿=g■°…↑》硒

4mmd((N团D·t巳)·吠t丁…()/〗●3》句

「■…

1…

1467 M68

M铀

v■8 ′』O

■7仔…∩tβg ‖山…t』m8W…77α

晒『:绸:豁i5i戳魁↓i瞬『黑!;i豫鳃』!!°,`]′j。‘"《鲤.岭)|);

乍■ll■广8 [[x…t1m8 丁棒厂冗「F ←

7Q记0了∏C

l…t佣8·

》 e[■■尸‖ ■1

∩…2 0Oj0D

●″otow芦; {仁m●〔也〔m「8 /)

一P-p旧t口-『 /「)

[…『缀窝蓟"咐}「卜mMq|, 『(.酮b··』) }



[[尸凹砸r』……u■』 卜′〔丘…5′‖8珠…■『 p少丁·t坐3助』mt rh19·亿t@「e◎0t●te·u「l.1mGN1 啪/mL

)《·p (7咏t1m(『)《 了etu加t·…0巳C0立7o欣t』◎∏(》( v●厂e亡t□l迪·日l·c№1p帕…·■ t…()〗 γO厂e=t0l屿·■loc江1p帕…·知

≈tO「∩e.巳m巧pto「■e··Nt●阀〔《

■9

,…耀蹬撼耀黔胞`剃《.·{ 》

M70 M71

1△y2 M了3

A「「■y(G◆』8助』●ct

0

●日 峙哩区m]…■N0=1∑已 ___

v…m

》)O

□….00↑…2…沁…睁°

γ■『p江(tMUQ闽O→1》*tM尘 一_—

℃… ≈=咖

图llˉ78

j方法对应的逻辑

‖■■司·司划|」■■■‖』■■■■■】‖』』■』·`|」□∏|·∏·‖乙■■■■』‖』■□』■‖』■司|」■]」■■Ⅵ』■可」■■■■■

∩tβ‖1l〗 「『1】 =■呵…tβ 「q…b(t》;

1峪l 1必∑ M63 1… 1晒

~—

【』…toU■讥酶1钞

】451

】$6U

×

d

__二

{!捻°’

1△d0 泌47 1“■■ 】“9 M”

】▲52 】q鲤 1… 10巫g M巫 〗佃γ 10S8 M凸0a

§



贮^§ 丁◆Q仲●

}「





■厂|■△■厂‖‖》△■■厂■‖「

} 匹■■尸§■尸}尸■■「「}‖〖■「「 「 ‖『)|■尸|



ll.7测览器环境下JavaScrjpt的模拟执行

457

我们大致可以看到,这里又掺杂了时间` SHAl`Base64`列表等各种操作。要深人分析,还是 需要花费一些时间的°

现在’可以说核心方法已经找到了’参数我们也知道怎么构造了’就是方法内部比较复杂,但我 们想要的其实就是这个方法的运行结果,即最终的to代e∩° 这时候大家可能就产生了这样的疑问°

□怎么在不分析该方法逻辑的情况下拿到方法的运行结果呢?该方法完全可以看成黑盒° □要直接拿到方法的运行结果,就需要模拟调用了,怎么模拟调用呢? □这个方法并不是全局方法,所以没法直接调用’该怎么办呢? ■■已

其实是有方法的。

■·

叶…

屯■‖

』■

□模拟调用当然没有问题’问题是在哪里模拟调用°根据上文的分析,既然测览器中都已经把上 ( ◎





下文环境和依赖库都加载成功了’为何不直接用测览器呢? { □怎么模拟调用局部方法呢?很简单’只需要将局部方法挂载到全局"1∩dow对象上不就好了吗?

□怎么把局部方法挂载到全局w1∩do"对象上呢?最简单的方法就是直接改源码°

□既然已经在测览器中运行了’又怎么改源码呢?当然可以’比如利用playwright的Request Interception机制将想要替换的任意文件进行替换即可° 4.实战

首先’我们来实现0bje〔t(1[,』a"])的全局挂载’只需要将其赋值给w1∩do"对象的—个属性即可’ 属性名称任意,只要不和现有的属性冲突即可。 比如我们需要在代码: γara=(tM5·pageˉ 1)*thi5·1mit」 e=Obje〔t(j["a"])(t∩15.$5tore.5tat巳ur1·j"dex’ a);

△■厂|

下方添加如下用干挂载全局w1∩do"对象的代码:

■■厂‖|■■■厂

"j∩do".e∩crypt≡0bje〔t(i["a"])j

匹■■厂|■■■∏{‖||也■{{‖

比如,这里我们将0bject(j["a』』])挂载给"1∩do"对象的e∩cIypt属性°这样只要该行代码执行完 毕,我们调用wi∩do".e∩〔rypt方法就相当于调用了0bjeCt["a"]方法°



接着’我们将修改后的整个JavaSc∏pt代码文件保存到本地,并将其命名为chunk]s’如图llˉ79所示° F创N贝水庐

矿于

X ′云



尸■u洁·

■F

}′ })′

‖|





‖↑

.`『?凸~0it0『pⅢ. 。‖(); ↑

f〗

|△■【■‖「}||



wTˉ t

O

°1oad1∩9

{《》;

丫飞′ 己 ( .p已ge ) 幻 .u∏1t’ e 0b]eCt(1[汕己!0])( .$5tO『e·5t己te°u厂1.1∩αex? a);

w1∩doN.e∩〔「γpt

0bjett(1["a"]);

·$己x1O5

.$Store.5↑ate,l』厂l°mdex0 {

.q仁o`(

p己「己用58 { u∏jt弓

·lm1t∏

of『Set: 己0

to恨e∩: e0

},

})

■ ■■巳











.1『咖.0《『8‖,MikOM》{ 巴■■



ˉ0; ( 、 ){

b. 《

c|『『屯p 0!『‖. √日

瓦T5

图llˉ79 chunkjs文件

■【

第l1章JavaScript逆向爬虫

458

接下来’我们利用playwnght启动一个测览器’并使用RequestInterception将JavaScrlpt文件替换, 实现如下:



+ro‖p1aywright°sy∩〔-api i川port5y∩c-p1aγwrig打t



8A5[0RL= |httP5://sPa2°5〔raPe°〔e∏teI| 〔o∩te×t=5y∩〔-p1aywright(〉。5tart() bro切5er=co∩text.c∩ro|mu∏.1au∩〔∩()

page≡bro0v5er·∩e"ˉpage(〉 page.route( "/j5/c∩u∩Ⅶˉ1o192己oo.2』3〔b8b7.j5"’ 1aⅧbdaIoute: route.+u1+i11(p3t‖="./〔hu∩Ⅸ.j5") 〉

page.goto(B∧5[0RL)

这里首先使用playw∏ght创建一个Chromlum无头测览器’然后利用∩e"—page方法创建—个新的 页面’并定义了-个关键的路由: page.route( "/j5/〔加∩长ˉ1O192a".2』3〔b8b7°j5』0’ 1a∩)bdaroute: route.+l」1「j11(pat‖="./〔∩u∩促°j5肌) )

这里路由的第一个参数是原本加载的文件路径’比如原本加载的JavaSc∏pt路径为/j5/〔∩u∩低ˉ 10192aO0.243〔b8b7.j5’如图llˉ80所示。 呈







×



呻…雪…4≡■←画……→ □…≈墅铂

一--=≡

蛔m》≈…励…≈…硒

了…≈}



|■



h`

执=■亡 肉啃≈■晒…『吨…

一…



{m■ ~」=u尸

坐……=ˉ. 舒~ˉ=F………舒=矗砖堑砰ˉ…号缸…,jT、…=■已—一ˉ=→~ˉ=芦=学廷= ≡. . ._一=…………=≡=…霸…·………箭闰…-ˉˉ… 寸…叮汹

潭诗…

固m≈→…,…0尸 ‖凹●…出……尸

||‖『≡

[|99四弓垫翌′…… { ≡袁三j;:『

‖■…^氢↑m睡…押k…′拽

圣=曰



} ;云茵:袁:觅魁′·.…





『……9t71威≤了蛔▲≡…p电7约皿 0 O

‖O砷ˉ

0=……

{ ≈学=哼…

{≡授:蔚雍

!≡…响`』cm』囤′』…锤『lm

{……,1o≈v2·m1o:99:go■γ 凸

‖…■……】广

缉=总总雹黑龋鼠Ⅸu……

▲J■…加℃′皂Ⅵ四~』

图11ˉ80原JavaScript文件加载路径

|{

怎么模拟调用呢?很简单,只需要在playwTight环境中额外执行JavaScnpt代码即可’比如可以

‖{|‖

这样playwright加载6s/chunkˉl0192a"243cb8b7js文件的时候,其内容就会被替换为我们本地 保存的chunkjs文件°当执行之后, 0bje〔t(1[圃a"])也就被挂载给"j∩dov《对象的e∩〔rypt属性了,所 以调用切j∩do切.e∩crypt方法就相当于调用了0bject(j[圃a圈])方法了°

■ ■ 可 = ■ ■

第二个参数利用route的fu1十j11方法指定本地的文件,也就是我们修改后的文件chunkjs。

定义如下的方法:



(‖·

de十get-to促e∩(o仟5et)8 re5u1t≡page.eγ己1uate(…()=〉{ Ietur∏m∩d呻.e∩〔rγPt(饵%5口’ ■则5口) }…%(!/api/硒γje′’ o仟5et)) retuI∏re5u1t

『■∏巴■■尸■■尸~■■巴■■■■■巳■■■■■尸‖‖‖Ⅲ

这里我们声明了get-to代e∩方法,经过上文的分析,模拟执行方法需要传人两个参数,第一个参





|′

■∏|斟『巴■『「

}‖}}‖



l17划览器环境下JavaScript的模拟执行

459

数是固定值/己p1/"oγ1e’另一个参数是变值,所以将其当作参数传人。

在模拟执行的过程中,我们直接使用page对象的eγa1u己te方法’传人JavaScript字符串即可, 这个JavaSc∏pt字符串是一个方法,返回的就是wj∩do侧.e∩crypt方法的执行结果°最后将结果赋给 re5u1t变量,并返回。

到此为止,核心代码就说完了°最后,我们只需要完善_下逻辑,将上面的代码串联调用即可° 最终整理的代码如下: +ro们p1aγwIight·5y∩〔-ap1j‖port5y∩〔-p1aⅦr1gbt j们PoIttme 1川portreque5t5

8∧5[0Rl≡ 0∩ttP5://5Pa2。5CmPe·Ce∩ter‖ I‖0[X0肌=8∧5[Ⅶ[+ ‖/apj/‖mγje?1加it={1i川jt}8o仟set={o仟5et}8to代e∩={to低e∩}0 ‖∧Xp∧C[≡ 1O

儿I‖∏=10

■■「‖■■■■巴■厂‖■■「△β●『■■■【□β■■■尸■■=尸△尸■■庐■什■■■■■■●厂■■=巴尸卜似匹■■■

〔o∩text=5y∩〔ˉp1aywr1g∩t().5tart() bIowser=〔o∩text。chroⅢiⅧ.13u∩〔h() p日8e=brow5er.∩ew-page() page.Ioute( "/j5/〔hu∩促ˉ10192a00.243〔b8b7.j5"》 1a"bdaroute: ro0te.+u1「j11(pat∩≡"./c}]u∩促.js") ) page.goto(BA5[_0【[)

de十getˉto促e门(o仟5et): reSu1t≡pa8e.ev日1u日te(‖{() =〉{ retuI∩"i∏dow.e∩crypt(卿%5"’"%5") }‖ !‖%(0/ap1/们Oγie‖’ O仟5et)) retur∩reSu1t

+orj 1∩r3∩ge(刚Xp∧C[): o仟5et=j本 lI‖I『

to代e∩≡get-toke∩(o仟5et) 1∩dexur1=I‖D[X0肌.十omat(1mjt=lI‖I丁’ o仟5et=o仟5et’ to代e∩=to代e∏) re5po∩5e=reque5t5.get(j∩dex l』r1) pr1∩t(0re5po∩5e 」 re5po∩5e.j5o∩(〉)



这里我们遍历了l0页,然后构造了o仟5et变量’传给getˉtoke∩方法获取to代e∩即可,最终运 行结果如下: {|〔ou∩t|: 1oq!Ie5u1t5′ : [{|jd|: 1’ 』∩咖e! : !霸王别姬0 ’ 』a1jas0 : !「are眶11∩y〔o∩cubj∩e|’ !〔oγeI :

0http5://p0°川ejtua∩·∩et/Ⅷγje/〔e4da3e03e655b5b88ed31b5cd7896〔十62472‘jpg·464"6』4h1e1c‖」 !categorjes0 : [‖剧′时,’ 0瓮悄‖]’ ,pub1i5∩edat|; !1993ˉo7ˉ26‖ ’ ||∏i∩ute|: 171’ 05core‖ : 9.5’ 0regio∩5|: [ !中国大陆』’ {中囚杏港』]}’ ·





{!jd‖: 10’ ‖∩a『∏e0 : 0狮于王! 」 |a1jas0 : |『he[jo∏促j∩g0 ’ 0〔oγer|; |http5;//po。∏mtua∩.∩et/加γie/

})

27b76十e6〔十39o3f〕d74963千70786Oo1e1438』o6.jpg幽64"644h1e1c0 ’ !〔a↑egorie5|: [|动画‖’ ‖歌分0 ’ !冒险|]」 ,pub1i5∩edat‖ : 』1995ˉo7ˉ15‖’ !m∏ute‖ : 89’ !5〔ore『 : 9.o’ 0regio∩5! : [‖英国』]}]} ■





可以看到,每一页的数据就被成功爬取到了,简单方便。

仔卜

5ˉ总结

■尸「■尸■■[卜|■尸『卜》|巴尸「◆尸|匡■『△■■厅□■『■■■■「

本节中,我们介绍了在测览器环境中模拟执行JavaScnpt来辅助JavaSc∏pt逆向的方法’这会在 -定程度上减轻逆向的压力°熟练掌握此技能’我们可以少走很多弯路°



↑↑。8∧S丁技术简介

前面我们介绍了—些JavaScrjpt混淆的基本知识’可以看到混淆方式多种多样’比如字符串混淆` 变量名混淆对象键名替换、控制流平坦化等°当然,我们也学习了一些相关的调试技巧,比如Hook` 断点调试等°但是这些方法本质上其实还是在已经混淆的代码上进行的操作’所以代码的可读性依然 比较差。

有没有什么办法可以直接提高代码的可读性呢?比如说’字符串混淆了,我们想办法把它还原了; 对象键名替换了’我们想办法把它们重新组装好’控制流平坦化之后逻辑不直观了,我们想办法把它 还原成一个代码控制流。

到底应该怎么做呢?这就需要用到AST相关的知识了°本节中,我们就来了解AST相关的基础 知识’并介绍操作AST的相关方法。

↑.∧S丁介绍

首先’我们来了解什么是AST。AST的全称叫作AbstractSyntaxTree,中文翻译叫作抽象语法树° 如果你对编译原理有所了解的话’-段代码在执行之前,通常要经历这么三个步骤。 □词法分析:一段代码首先会被分解成一段段有意义的词法单元’比如说co∩5t∩a"e=℃emeγ|

这段代码,它就可以被拆解成四部分: 〔o∩5t` ∩a∏e` =` |Cemey|’每—个部分都具备_定的 含义。

□语法分析:接着编译器会尝试对_个个词法单元进行语法分析,将其转换为能代表程序语法结 构的数据结构°比如’co∩st就被分析为γar1ab1e0eC1aratio∩类型,代表变量声明的具体定 义; ∩aⅧe就被分析为Ide∩t1+1er类型,代表_个标识符°代码内容多了’这—个个词法就会 有依赖、嵌套等关系’因此表示语法结构的数据结构就构成了一个树状的结构,也就成了语法 树’即AST。

□指令生成:最后将AST转换为实际真正可执行的指令并执行即可°

AST是源代码的抽象语法结构的树状表示’树上的每个节点都表示源代码中的_种结构’这种数 据结构其实可以类别成一个大的JSON对象°前面我们也介绍过JSON对象’它可以包含列表、字典并 且层层嵌套因此它看起来就像—棵树’有树根`树干`树枝和树叶’无论多大,都是一棵完整的树° 在前端开发中’AST技术应用非常广泛,比如webpack打包工具的很多压缩和优化插件`Babel插 件、VUe和React的脚手架工具的底层等都运用了AST技术。有了AST’我们可以方便地对JavaScnpt 代码进行转换和改写’因此还原混淆后的JavaSc∏pt代码也就不在话下了。 接下来,我们通过_些实例了解AST的一些基本理念和操作° 2实例引入

首先,推荐_个AST在线解析的网站ht‖ps://astexplorerneU’我们先通过_个非常简单的实例来 感受下AST究竟是什么样子的°输人上述的示例代码: 〔o∩5t∩a∩`e=℃er『爬y`

这时候我们就可以看到在右侧就出现了—个树状结构’这就是AST,如图llˉ8l所示°

Ⅱ‖』ˉ■■Ⅷ】‖』■]』〗】●□】』】』■】‖‖」■司|‖」』■□■‖|』■】‖|」■口□‖‖司=】|‖ˉ■】■≈■■■‖‖」■■■】||』■■』勺』■□□·』■可||」··■■‖|』日■】|』□Ⅵ|口ˉ■占■】■』■』]■■|」■·□■

第ll章JavaScript逆向爬虫

460

′|》



} | | △ ■ 【 | } ▲ = 『 ‖ ■ ■ ■ ‖ |



AST技术简介

ll.8

‘—硷.●`^^顾啃. ←宇G

●…7=吨t

46l

×■ … 7

叮F



≡■



ˉ

ˉ`一亏唾=酶.ˉ ■★ 力●

山→~…

矗m■m1°mr圈s口j…仁巴鱼J…scr』吐Q》0bOh·1/p·r…◇m酝…f。m颐“mu】但? 】函n●亡∩…■ ·…y· ‖ ?b蹿 ‖ 」so什

P·…z『 p……1′口△r…ˉ7.15ˉ3 …



□Au…U■■"哗『W●《们“g■N旧eem哟kw$■间‖◎e℃C□tj□∏dam □Ⅷ“W砷炬鸭 →■◎

【…t1r岂■r

□:…确色a【凸啪厅

■γ…d

巴 ■ 尸 匹 ■ 「 }

■0▲冗· 6

啡8心

〗o

≈h【距”







0

◆吼丛扩t〗 『 0…



■勺

■■

PM■甲●=B

!

■幻■ h

□F



□■

Ⅶ…?』∏雨

巳■=弛1r丑″西≡q ·丁

0

‖≥『■尸『■■■尸■■尸■【■厂巴■■







|….…



ˉ蛔』:默!麓魁圃』″



慧l!……



◆彰∏ 『吕●■■■母·2…





E4‖啼u■些g[≡宁=

`伞气阻k2;·严→; ;·写≡sl拿= 写……●0



∏…■LⅦ瘫■ 口山…y●



-_酶盂r≡盂豆谅页盂了≡忘俱澎~弛…避刽…屯|…山|a』阳;田刀■s

◆▲■|■「■厂■【■厂卜‖八β■「|■Ⅲ■

尸|卜

}|



0

图1lˉ8l

博 }



b



AST

这就是-个层层嵌套的数据结构,可以看到它把代码的每_个部分都进行了拆分并分析出对应的

类型`位置和值°比如说’∩a刚e被解析成_个tγpe为Ide∩t1+1er的数据结构’ 5tart和e∩d分别代表 代码的起始和终止位置’"a们e属性代表该Ide∩t1十1er的名称。另外, CerⅦey这个字符串被解析成了

5trj∩g[jtem1类型的数据结构,它同样有5tart、e∩d等属性,同时还有eXtr己属性°eXtm属性还带 有子属性mWγa1ue,该子属性的值就是Cemey这个字符串。我们所看到的这些数据结构就构成了一 个层层嵌套的AST°

另外’在右上角,我们还看到一个Parser标识’其内容是0babe1/par5er°这是一个目前最流行

的JavaScnpt语法编译器Babel的Nodejs包’同时它也是主流前端开发技术中必不可少的~个包°它 内置了很多分析JavaSc∏pt代码的方法,可以实现JavaScnpt代码到AST的转换。更多的介绍可以参 考Babe‖的官网°

接下来,我们使用Babel来实现一下AST的解析、修改° 3.准备工作

由于本节内容需要用到Babel,而Babel是基于Nodejs的’所以这里需要先安装Nodejs’版本 推荐为l4x及以上’安装方法可以参考: https://setupscrape.center/nodejs°

安装好Nodejs之后’我们便可以使用∩pⅧ命令了°接着’我们还需要安装一个Babel的命令行 工具@babeUnode’安装命令如下: ∩p‖‖ 1∩5ta11ˉg0b日be1/∩ode

接下来’我们再初始化一个Node.js项目leamˉast,然后在leamˉast目录下运行初始化命令’具体 如下: ∩mj∩jt

∩p川i∩5ta11 ˉ0@b日be1/〔ore0babe1/c1i@b日be1/presetˉe∩γ

运行完毕之后,就会生成一个packagejson文件并在devDependencics中列出了刚刚安装的几个 Nodejs包。

第ll章JavaScript逆向爬虫

462

接着’我们需要在leamˉast目录下创建一个.babe1rc文件,其内容如下: {

"pre5et5": [ "@babe1/pIe5etˉe∩γ"



这样我们就完成了初始化操作。 4.节点类型

在刚才的示例中’我们看到不同的代码词法单元被解析成了不同的类型,所以这里先简单列举 Babel中所支持的_些类型°

‖u|∏er1〔l1tem1、 8jgI∩t[jtera1等类型’更确切地代表某_种字面量。 □Declaranons:声明,比如「u∩〔t1o∩0ec1aratjo∩和γar1ab1e0e〔1arat1o∩分别用于声明一个方 法和变量°

□Expresslons:表达式’它本身会返回一个计算结果,通常有两个作用:-个是放在赋值语句的 右边进行赋值,另外还可以作为方法的参数°比如[og1ca1[xpre551o∩`〔o∩djtjo∩a1[xpre55io∩`

∧rray[xpre55jo∩等分别代表逻辑运算表达式`三元运算表达式`数组表达式。另外’还有一

|||

□Llteral:中文可以理解为字面量,即简单的文字表示,比如3、"日b〔"、 ∩u11` tn」e这些都是基本 的字面表示。它又可以进—步分为Reg[xp[1tera1`‖l」11[jteIa1、5tr1∩g[1teIa1、8oo1ea∩[1tem1`

些特殊的表达式’如γ1e1d[xpre55jo∩`∧wajt[xpIe55io∩`『∩15[xpre551o∩° □Statements:语句’比如I十5tateⅦe∩t、5wjt〔h5tate‖e∩t` Brea促5tateⅧe∩t这些控制语句,还有

-些特殊的语句’比如0ebugger5tate们e∩t、81ock5tateⅦe∩t等。 □Identlfier:标识符,指代_些变量的名称’比如说上述例子中∩a∏e就是-个Identjfier°

□Classes:类’代表一个类的定义,包括〔1a55`〔1a558odγ、〔1a55‖et‖od`〔1己55PIoperty等具 体类型°

□Functions:方法声明’它_般代表「u∩〔t1o∩0e〔1arat1o∩或「u∩ct1o∩[xpre551o∩等具体类型° □Modules:模块’可以理解为一个Node」s模块,包括‖odu1e0eC1arat1o∩` ‖odU1e5pec1十1er等 具体类型。

□Program:程序,整个代码可以成为Program°

当然’除此之外还有很多类型’具体可以参考https://babe0sjo/docs/en/babelˉtypes。

5.@babe|/pa「se『的使用

」■■|‖‖■

@babel/parser是Babel中的JavaScript解析器,也是—个Nodejs包’它提供了-个重要的方法, 就是Par5e和Par5e[xpre55jo∩方法’前者支持解析_段JavaScript代码,后者则是尝试解析单个 JavaScript表达式并考虑了性能问题°_般来说,我们直接使用par5e方法就足够了。 对于parse方法来说’输人和输出如下° □输人:一段JavaSc∏pt代码° □输出:该段JavaScript代码对应的抽象语法树’即AST’它基于ESTree规范°

由于JavaSc∏pt代码中包含多种类型的表达’比如变量名`变量值、方法声明`控制语句`类声

明等。这里简单做下归类,具体可以参考: https:〃githuhcom/babel/babel/blob/masteⅣpackages/babelˉpaⅡFe门 asUspec.md。



‖ q

现在我们来测试~下°

新建一个JavaScnpt文件’将其保存为codes/codel.js’其内容如下:



ll.8

AST技术简介

463

〔o∩5ta=3j

1et5tr1∩g= | |he11o"『 千Or(1eti=0; 1〈aj j++){ 5tIj∩g+≡ wor1d"j } co∩5o1e.1og("5tri∩g"’ 5trj∩g)j 00

『「‖

■∏

} }

下面我们需要使用par5e方法将其转化为_个抽象语法树’即AST° 新建_个basicLjs文件’其内容如下: 00

■■尸‖■■户■厂

1爪port{ p己r5e}+ro阳"@babe1/par5er ; 1们pOrt十5千rOⅦ ’`+S"j

〔o∩5tcode≡+5.read「j1e5y∩c(』0〔ode5/code1.j5"’"ut十ˉ8")j 1et日5t=p己I5e(〔ode)】 Co∩5o1已1og(a5t);



接着,我们可以使用babe1ˉ∩ode运行:

‖‖口γ△日■■

babe1ˉ∩odeba51〔1.j5

运行结果如下: ‖ode{

type: !「11e0 』 5taIt: 0’

0

e∩d; 114’



1o〔8 5Our〔eLOC己tjO∩{

5tart: po51t1o∩{ 11∩e8 1’ co1Ⅷ∩: o}’ e∩d8 po5jtjo∩{ 1j∩e日 6’ co1uⅧ: 29}

0

p

}’ erIor5: []’

p



progIa∏: ‖ode{ type目 !prograⅧ ’

p



5tart8 0’

e∩d: 114’

P

1OC: 5O0r〔e[OCat1O∩{5t日rt; [pO5jt1O∩]’ e∩d: [pO5jtjO∩] }’

■■尸■■■尸~■尸|‖‖■■ ) ■ 尸 ■ 尸 ‖

5our〔e「yPe: ‖5〔riPt|’ i∩terpreter: ∩l』11’

body; [ [‖ode]’[‖ode]’[‖ode]’[‖ode] ]’

dire〔tjγe5: [] }’ 〔o∏∏爬∩t5: []



] 》



可以看到,整个AST的根节点就是-个‖ode,其type是「i1e’‖ 可以看到,整个AST的根节点就是-个‖ode,其type是「i1e’代表一个「11e类型的节点,其

■『‖‖|}巴■厂■β【尸| ||

中包括type、5tart、e∩d、1oc、progm"等属性°其中progm"也是—个‖ode’但它的type是prograⅧ’ 代表—个程序。同样’ pro8ra"也包括了一些属性’比如5tart` e∩d、1oc、i∩terpreter、 body等° 其中’body是最为重要的属性’是一个列表类型’列表中的每个元素也都是一个‖ode,但这些不同的 ‖ode其实也是不同的类型’它们的type多种多样’不过这里控制台并没有把其中的节点内容输出出来。

)》卜



′| 卜 叼 广 ■ 『 ∩ | 低

我们可以增加一行代码’再专门输出—下body的内容: 〔o∩so1e。1o8(a5t。progra川。body);

重新运行’可以发现这里又多输出了一些内容,具体如下: ‖ode{

tγpe: ‖γar1ab1e0e〔1ar己tio∩|’ 〕 」’

‖Ode{

tγpe: |γar1ab1e0e〔1ar日t1o∩|’ }’



(‖



||□

第l1章JavaScript逆向爬虫

464

‖ode{ type: 0「or5tate‖赔∩t0 ’ ●





j∩jt: ‖ode{ type: 0γariab1eDec1日mtjo∩0 ’

} }

由于内容过多’这里省略了_些内容。可以看到’我们直接通过35t.progra们.body即可将body获 取到。可以看到,刚才的四个‖ode的具体结构也被输出出来了。前两个‖ode都是γar1ab1e0ec1amt1o∩ 类型’这正好对应了前两行代码: 〔O∩St日=3;

1et5trj∩g= "∩e11o"j

这里我们分别声明了一个数字类型和字符串类型的变量,所以每句都被解析为γar1ab1e0e〔1aratio∩

接着’我们再继续观察下—个‖ode。它是「or5tate们e∩t类型,代表—个十or循环语句,对应的代 码如下: +Or(1et 1=Oi j<aj j++){ 5tri∩g+=倒咖r1d口j

{』‖‖叫

类型。每个γar1ab1e0e〔1aratjo∩都包含了-个de〔1arat1o∩s属性’其内部又是_个‖ode列表,其中 包含了具体的详情信息。

0

」划■●日」】‖■■■■■■Ⅵ』■■■可‖‖■■』·司■■]|‖·γ■■]‖□■■■■司』■■可

} }’ ‖ode{ type: ![XpIe55io∩5tate∏把∩t0 ’



‖|

}’ body; ‖◎de{ type吕 081oc代5tate『肥∩t‖’

|‖

}’ upd己te: ‖ode{ type; 00pdate[xpres5jo∩0 』

|||‖』■■

}′ te5t: ‖ode{ type: ‖8i∩ary[×pⅢes5jo∩0 ’





■司|■

十Or循环通常包括四个部分’+Or初始逻辑、判断逻辑、更新逻辑以及「Or循环区块的主循环执行 逻辑’所以对于一个「or5tate∏e∩t’它也自然有几个对应的属性表示这些内容,分别为j∩it` te5t` update和body°



对于1∩1t,即循环的初始逻辑,其代码如下:

它相当于_个变量声明,所以它又被解析为γar1ab1eDe〔1aratio∩类型’这和上文是-样的° i<日

它是一个逻辑表达式,被解析为8j∩arγ[xpre551o∩’代表逻辑运算°

尸】

1++

||

对于update’即更新逻辑,其代码如下:

二■■■■|叫|』句|‘』■】■■

对于te5t’即判断逻辑,其代码如下:

‖·

1etj=0j



』‖

ll.8

AST技术简介

465

它就是对i加1,也是-个表达式’被解析为0pdate[xpre551o∩类型° 对于body,它被-个大括号包围,其内容为: {

5tr1∩g+= 00wOr1d"; }

整个内容算作一个代码块’所以被解析为81oc促5tateⅧe∩t类型,其bodγ属性又是_个列表° 对于最后_行’代码如下: CO∩5O1e.1Og(‖Strj∩g! ’ 5tr1∩g)j

它被解析为[xPre55jo∩5tateⅧe∩t类型’expre551o∩的属性是〔a11[xpre551o∩。〔a11[xpre551o∩又

包含了ca11ee和argu阳e∩ts属性,对应的就是〔o∩5o1e对象的log方法的调用逻辑° 到现在为止,我们应该能弄明白这个基本过程了°

| b





仿

卜 p



0

p





p



parser会将代码根据逻辑区块进行划分’每个逻辑区块根据其作用都会归类成不同的类型,不同 的类型拥有不同的属性表示°同时代码和代码之间有嵌套关系’所以最终整个代码就会被解析成一个 层层嵌套的表示结果°

另外,个人还推荐使用上文提到的https://astexplorerneU网站来进行AST的解析和查看’它比代 码更加直观°

转化为AST之后’怎样再把AST转回JavaSc∏pt代码呢?要还原’我们可以借助于ge∩erate方法。

6.@babe|/ge∏e「ate的使用

@babe‖generate也是一个Node」s包,它提供了ge∩erate方法将AST还原成JavaScnpt代码’调 用如下: jⅦport{par5e}十roⅧ"0babe1/par5er0』j mportge∩emte十r咖"@b己be1/8e∩emtor"j 1∏port千5+ro们圃+5"j

b

〔o∩5tcode=「臼.Iead「i1e5y∩c(闻〔ode5/code1.j5』0’ ‖』|』t+ˉ8")j

p

1eta5t二par5e(〔ode)】 〔o∩5t{〔ode: output}=ge∩erate(ast)j co∩5o1e.1og(output)『



0

b

′ b



重新运行’可以得到如下结果: 〔O∩5ta=3j

1et5trj∩8= "he11o";

十Or(1etj≡O; i〈aj 1++〉{ 5tri∩g十= "wor1d"; }







| b

〔o∩5o1e。1o8("5tri∩g"’ 5tri∩8)『

这时候我们可以看到’利用ge∩erate方法’我们成功地把_个AST对象转化为代码°

到这里我们就清楚了,如果要把_段JavaSc而pt解析称AST对象,就用par5e方法。如果要把AST 对象还原成代码’就用ge∩emte方法。

P







} p

另外’ge∩erate方法还可以在第二个参数接收—些配置选项’第三个参数可以接收原代码作为输 出的参考’用法如下:

〔o∩5toutput二ge∩erate(a5t’{ /*opt1o∩5*/ }’〔ode)j

其中OptjO∩5可以是—些其他配置°这里列举_部分配置’具体如表llˉl所示°



} p







‖■■■■

第ll章JavaScript逆向爬虫

466





己uxi1iary〔om记∩t8e十ore 3uxi1jary〔o∏‖∏e∩tA代er





默认值





5tm∩g

在输出文件的开头添加块注释可选字符串

5tri∩g

在输出文件的末尾添加块注释可选字符串

reta1∩11∩eS

boo1ea∩

十a15e

尝试在输出代码中使用与源代码中相|司的行号

Ietaj∩「u∩ct1o∩p己Ie∩s

boo1e己∩

于a15e

保留表达式周围的括号

boo1ea∩

true

输出中是否应包含注释

CO‖γ|pa〔t

boo1ea∩或!auto|

Opt5°m∩j千ied

设置为true以避免添加空格进行格式化

m∩jfjed

boo1ea∩

十a15e

是否应该压缩后输出

〔O『∏『隐∩t5

Co∩5t{〔ode: oUtPut }≡ge∩er日te(己5t』{

运行结果如下:





‖(‖‖‖日

reta1∩[i∩e5; true’

})j co∩5o1e。1og(o0tput)



二■·■‖‖‖‖二■■·可

比如,如果我们想要和原代码维持相同的代码行,可以使用如下配置:

α〗||{({||‖】

表↑↑ˉ‖ Opt1O∩S部分配置

CO∩5ta=3;

这时候我们就可以看到’生成的代码中间没有再出现空行了,和原来的代码保持一致的格式。 7.@babe|/t「aγe「se的使用

那就是AST的遍历和修改。

遍历我们使用的是@babel/traverSe’它可以接收_个AST,利用tmver5e方法就可以遍历其中的 所有节点°在遍历方法中’我们便可以对每个节点进行对应的操作了°

我们先来感受~下遍历的基本实现°新建-个JavaScnpt文件’将其命名为basic2js,内容如下: 1阶port{par5e}十r刚|00b己be1/par5er"j 1呻ortge∩er日te十ro" 000babe1/ge∩emtor0|j 加pOrt千5千rO刚 00+S"j

〔o∩5t〔ode=「5.read「i1e5y∩〔("〔odes/code1°j5"’"ut十ˉ8")j

□』■■■■■■』『』□■』■■』■■■可■·■

前面我们了解了AST的解析,输人任意—段JavaSc∏pt代码’我们便可以分析出其AST。但是只 了解AST,我们并不能实现JavaScnpt代码的反混淆。下面我们还需要进—步了解另一个强大的功能’



□、■□∏||·司■■‖|■■■■■、口■■■

1etstri∩g≡ 00he11o阅j 十Or(1eti=Oj i<aj i++){ 5trj∩g+= "门oI1d"j } co∩5o1e.1og("5trj∩g!|’ 5tr1∩g)j

1et日5t≡p日rSe(COde)j

tmVer5e(a白t’{ e∩teI(path){ co∩5o1e。1og(path) }’ })j

|」|(

这里我们调用了tmγer5e方法,给第一个参数传人AST对象,给第二个参数定义了相关的处理 逻辑’这里声明了—个e∩ter方法,它接收path参数°这个e∩ter方法在每个节点被遍历到时都会被 调用,其中pat‖里面就包含了当前被遍历到的节点相关信息°这里我们先把pat∩输出出来’看看遍

历时能拿到什么信息°

babe1ˉ∩odeba51〔2.j5

|||||

运行如下代码:

卜}瓜

『 ‖ ‖ △ 庐 ∏ ■ 『 | ■ ∩ | } [ ■ 『 「 |



ll.8

AST技术简介

467

这时我们看到控制台输出了非常多的内容’调用很多次1Og 调用很多次1Og方法输出了对应的内容°每次输出都 代表一个path对象,我们拿其中_次输出结果看下,内容如下: ‖Odep己t‖{

■■

Dare∩t: ‖Ode{ type: 『γar1ab1e0ec1aratjo∩,』

0







β卜

匹尸‖』■■尸′|′

}’ ∩ub: u∩de千j∩ed’

〔o∩teXt5: [ 丁raγer5己1〔o∩text{ G

0



} 】

」’ 巾





■■尸卜■【田尸|‖■■

pare∩tpat∩: ‖odepat∩{ ●





户}

type: 0γaIjab1ekc1ar3t1o∩『 }’ 〔O∩teXt; 丫Iaγer5己1〔o∩text{ que‖e: [ [〔jrCu1aI] ]’ p日re∩tpat打: ‖odepat门{ ■





}’ ■日■》 凸■伊尸广△■厂■■■『■尸户◆

}’ 〔O∩tai∩er: [ ‖ode{ tγpe: 0γar1ab1e0e〔1amtor‖’ ■





} ]’ 1j5tRey: 0de〔1aratio们5 ’ 促ey: 0’ ∩ode: ‖ode{

type8 0γariab1e0e〔1aratoI‖’ ●●●

1d: ‖ode{

type; ‖Ide∩tj十ier0 ’ }’ j∩it: ‖ode{

| ■■『●■■〖■厂仆■〖■尸■β‖[■尸甘叼卜■■『|∩

type: 0‖u眠ri〔uter310’ ●●●

厂| L

↑↑

} }’

5〔Ope吕 5Cope{ uid: 1’

b1o〔R:№de{

type: 0「oIState∏论∩t0’ ●●■

}’

path: ‖Odepath{ ●●■

}’ ●■■

}’

type: ‖γarjab1e促〔1己mtoI‖ }

可以看到内容比较复杂,这里将不必要的内容省略了。首先,我们可以看到它的类型是‖odepath,

▲■■■厂||■■「卜||■■■尸|■■『「仁【■}}■『』■■■■■厂【■尸|■■

拥有pare∩t、co∩taj∩er` ∩ode、 5〔ope、type等多个属性。比如"ode属性是一个‖ode类型的对象,

和上文说的{‖ode是同—类型,它代表当前正在遍历的节点。比如,利用pare‖t也能获得—个‖ode类

型对象,它代表该节点的父节点°

所以,我们可以利用path.∩ode拿到当前对应的‖ode对象,利用path.pare∩t拿到当前‖ode对象 的父节点°



既然如此,我们便可以使用它来对‖ode进行一些处理。比如,我们可以把值变化一下’原来的 代码如下:

|||」=■口□■□】】■■■■】■■□‖■■■■■■■■■||』=■∏·■‖‖‖|□●

第ll章JavaScript逆向爬虫

468

CO∩5t己≡3j

1et5tri∩g= α‖e11o"; +or(1eti=0j j〈a; j++){ 5tri∩g+= "咖r1d"; } Co∩5O1e·1og("5tri∩g瞬’ Strj∩g);

〔O∩5ta=5β

1et5trj∩g= 阅M圃j 于Or〈1eti=O; 1<aj i++){ stri∩g+=国wor1d00’ }

〔o∩5o1e.1og("5tm∩g"’ 5trj∩g)j

我们可以实现这样的逻辑: mporttTaγer5e+r咖"0babe1/traver5e";

1们port{p日r5e}+ro|∏ ‖‖0babe1/par5er阅j 1呻ortge∩emte+ro‖]"0babe1/8e∩emtor"j 1"port十5于ro们"千5"j

co∩5tcode≡千s。re3d「j1e5y∩〔("code5/〔ode1.j5"’"ut+ˉ8")j 1eta5t≡par5e(COde)j traγer5e(a5t’{ e∩ter(pat∩){ 1et∩ode=path.∩odej j「(∩od巳type=≡"‖Ⅷerj〔[jtera1"腿∩ode.va1ue≡=3){ ∩ode。va10e=5j }



j+(∩ode.type=≡≡ "5tr1∩gLjter己1"88∩od巳va1ue=≡ "∩e11o"){ ∩Ode°γa10e= "M"j } }′ }〉j

CO∩5t{Code: Outp‖t}二ge∏erate(a5t’{ retaj∩[i∩e5: true’

})β CO∩5o1e.1Og(output)j

这里我们判断了∩ode的类型和值,然后将∩ode的γa1ue进行了替换,这样执行完毕traγer5e方 法之后, a5t就被更新完毕了。 运行结果如下: CO∏5ta=5j

1et5tri∩8= 00hi"j 千Or(1etj=o; i〈aj j++〉{ 5trj∩g+= "wor1d"j } 〔o∩5o1e.1og("5tr1∩g"’ 5tri∩g)j

可以看到’原始的JavaSc∏pt代码就被成功更改了!

另外’除了定义e∩ter方法外’我们还可以直接定义对应特定类型的解析方法,这样遇到此类型 的节点时’该方法就会被自动调用,用法类似如下: mporttraver5e十ro‖↑"0babe1/traγer5e"j

mport{par5e}+I咖』Pb己be1/par5er"i mportge∩emte千r咖"0babe1/ge∩emtor"; mpOrt+5千rOⅧ口+S";

·■‖』■■』■」』■■】‖‖‖』■■‖■■·二■‖‖|■■■■]』{』』·』■■国』·■√司·可|□·■叼』口」■■|||」□■■可||·■‖‖‖」□■●可{▲·■·』■■Ⅵ』■■‖|』■司■■‖□■□□·‖』〗‖·{·■■■■■■白■」』■■

我们要想利用修改AST的方式对如上代码进行修改,比如修改_下a变量和5trj∩g变量的值, 变成如下代码:





ll8AST技术简介

469

〔o∩st〔ode二「5°read「11e5y∩c(』0code5/code1·j5"′"l」t+ˉ8"); 1et日5t≡parse(code)j 「)卜|

| 『◆



traVer5e(35t’{ ‖(』川eriC[1tem1(path){ i十(path。∩Ode.γ日1ue≡=3){ path·∩ode·γ日1ue=5】 } }’ 5tIj∩gljtem1(path){ i十(path·∩ode.va1ue≡= "∩e11o喊){ p己t∩·∩ode.γa10e= "∩j00j } 引

』’

})〗

·■「‖‖【庐「|匹■■

运行结果是完全相同的,单独定义特定类型的解析方法会同得更有条理。 另外’我们可以再看下其他的操作方法°比如,删除某个∏ode,泣里可以试着删除最后-行代码

对应的节点’此时直接调用Ie"oγe方法即可,用法如下:

■尸}■■■『

1刚poIttraveI5e+ro‖↑l "@babe1/tr己γer5e!0; i∩port{p己r5e}十r刚"@babe1/p己r5er同j 1‖portge∩er己te千ro‖ !!@b日be1/ge∩erator‖!; ⅧpOrt「5千IO爪 00十5"j

■尸■■【■}β{■「卜广》■「』■厂卜■尸「【尸

〔o∩5t〔ode≡f5.Iead「11e5y∩c(‖0〔odes/code1.j5"’"ut十ˉ8")j 1eta5t≡par5e(code)j traγer5e(a5t’{ 〔a11[Xpre55jo∩(p己t‖){ 1et∩ode=path。∩ode; i+(

∩ode.〔己11ee.object.∩3爬≡="〔o∩5o1e00腿 ∩ode°〔311ee。propeIty。∩a爬=== 闪1o8" ){

patb。re『∏Oγe()j } }’ })】

co∩5t{〔ode: o0tput.}≡ge∩er己te(a5t’{



retaj∩[j∩e58 true’

});

■■「‖■二■厂

〔O∩5o1e.1og(outp0t)〗

这样我们就可以删除所有的co∩5o1e1og语句°



运行结果如下: ■二

〔O∩5ta=3j

) | 卜

1et5t【i∩g= ■he11O0|; 千OI(1etj=0】 i<aj i++){ 5tr1∩g+= ""Or1d"〗 }

}卜|} 「卜

上面说了简单的替换和删除,那么如果我们要插人一个节点’该怎么办呢?插人新节点时,需要 先声明-个节点,怎么声明呢?这时候就要用到type5了°

8.@babe|/types的使用

@babe‖types也是一个Nodejs包,它里面定义了各种各样的对象,我们可以方便地使用tγPe5声 明一个新的节点。



比如说,这里有这样—个代码: CO∩5t3=1】

■■β|■

我想增加-行代码’将原始的代码变成: CO∩5ta=1i

〔o∩5tb=a+1;











第ll章JavaScript逆向爬虫

470

尸■■

该怎么办呢?这时候我们可以借助tγpeS实现如下操作: j呻orttraγeI5e「ro『∏ 口@babe1/traγeI5e ; 1『mort { paI5e}千r咖"@babe1/paI5er圆i 1呻ortge∩eIate「r咖阐助abe1/ge∩emtor阅j 1呻ort*己5type5+r咖闻助日be1/type5"j



■■|■|■■可

〔o∩5t〔ode= 闪co∩5ta=1i"j

1et35t≡par5e(〔ode)i traγer5e(己5t’{ γariab1eDec1己I3tio∩(pat∩){

1eti∩jt=type5.bj∩ary[xpre551o∩( "



+ 』

typeS.jde∩ti+jer(.己,0)’ tyPe5。∩u"’erj〔[jtera1(1〉

)j



1etde〔1己r己tor=types.γarjab1e0e〔1ar己tor(type5。ide∩tjfjer(画b")’ j∩it)j 1etdec1ar己tio∩=type5.γ己rjab1eDe〔1aratio∩(阐co∩st"’[dec1ar己tor])i

{|{

』■■□■

pat∩.1∩5ertA+ter(de〔1aratio∩)j path.5toP()j }’ })j 〔o∩stoutput=ge∩er己te(己5t’{ ret日j∩[1∩e5: true’

}〉.COde; 〔o∩5O1e.1Og(Output)j

运行结果如下:

0

〔o∩5t己二1;〔o∩5tb=a+1j

这里我们成功使用AST完成了节点的插人,增加了_行代码° (日卜面的代码看起来似乎不知道怎么实现的, i∩1t、de〔1amtor、de〔1aratjo∩都是怎么来的呢?

不用担心’接下来我们详细剖析一下。首先’我们可以把最终想要变换的代码进行AST解析’结 果如图1l-82所示。

团m=甲

斤|

憨记.f瓣趣…∩●! 剧sT酝P1°…团smw·c吕色J区mscr…呐Qb…1/…征◆●壶…r…■d·f乙u1烂?磷篷ˉ ’…鹰『…L蜘鹰…尘么鱼 N沪 1…■巳●■lp ^___……0 、霄嗡≈ ◆钞◎

】-c■■■◆1月

=和Td→D≡O●T…】m《

●…i .…▲●b丛■■峪№『●ql■■ 宁·一ˉ■



≡=▲m

◆■~■《面·…

==……§→‖

0

■…·盐=?■■△·~

t…0 ■№r…冲……行■ ■▲■■ 《0

=宁郸

●=8 iG…h·…》 ■凹0…仰…0

…】≈…m1∏D■· =~〗p

…P

】O

●Ⅱ…D『m茁《凸…=■P=》 == ■矿 0

→鲤先『…■……≈◆傍〖 .…丁 ■酗~■ →▲≈P

岔4

~=工

◆…《F』■°≈》 O』″●E…江』“0…◇O【…廖…吕■e闺

…pt睦8·●· ◆…】…l坠$t…〗0■庐…■……r

| ■ 川 』 ‖ ■ ■ ■ 巳 】 〗 】 】 ■ | ‖ ■ α ‖ ‖ · ■ | ‖ ■ Ⅺ ‖ 』 ■ ■ 〗 ‖ 』 』 ■ □ ■ 】 { | ■ ■ | | ‖



{ |

…□ ●仆

》 》

■■叫° ■~宁·

…… ………ˉ…≡

h …画=句』…▲↓印烟;4□…

■■[■巳巴■■■■■■[‖■■厂Ⅱ■}

图llˉ82对代码进行AST解析

|| ll.8AST技术简介

47l

■尸■■■尸|卜「伊尸|■厂仪『

这时候我们就可以看到第二行代码的节点结构了,现在需要做的就是构造这个节点,需要从内而 外依次构造° 首先,看到整行代码对应的节点是γariab1e0ec1aratio∩°要生成γar1ab1eDe〔1amt1o∩,我们可

以借助tγpe5的varjab1e0e〔1arat1o∩方法’二者的差别仅仅是后者的开头字母是小写的。 API怎么用呢?这就需要查阅官方文档了°我们查到γar1ab1eDe〔1amt1o∏的用法如下:

厂|}||凸■■‖八■匡■=∩

t.γari己b1eDec1aratjo∩(代i∩d」 dec1amtio门5)

可以看到,构造它需要两个参数,具体如下。

□ki∩d:必需’可以是!』γar"|』|1et』』 | "co∩5t".

□de〔1日r日tjo∩5:必需’是∧rmy〈γar1ab1e0e〔1arator〉,即γ日r1ab1e0e〔1arator组成的列表°



卜卜「巴■

这里|(i∩d我们可以确定了’那么de〔1amtjo∩5怎么构造呢?

要构造de〔1aIatjo∩S,我们需要进_步构造γaI1ab1e0ec1arator,它也可以借助type5的

■‖》

γar1ab1e0eC1arator方法’用法如下: t.γ己rjab1e0e〔1aIator(id′ 1∩jt)

它需要1d和i∩1t两个参数。

□jd:必需’即1de∩t1「jer对象

卜}



□1∩1t: [xpre551o∩对象’默认为空°

因此’我们还需要构造1d和i∩1t。这里jd其实就是b了’我们可以借助于tγpe5的1de∩t1+jer

=尸■■『■‖『■■■厂》■■厂【尸

方法来构造°而对于j"jt,它是expre55jo∩’在AST中我们可以观察到它是Bj∩ary[xpre55io∩类型, 所以我们可以借助于type5的bj∩aIγ[xpre55io∩来构造。b1∩ary[xpre55jo∩的用法如下: t。bi∩ary[xpre5sio∩(operatoI’1e什’Iight)

它有三个参数’具体如下。

□opeIator:必需, "+"|,』ˉ" | "/" | ′』%" | ,』*" | "**"|"8"|"|"|!!)〉′』|">〉〉"|"〈<|||



)β 〗‖

p

0

0



p

′怜

‖■





』,^"|』|=="|"=="|"!=』』 | |` !=|||"1∩"|"1∩5ta∩〔eOf" | "〉,| | "<|||||〉=|||"<=阀° □1e十t:必需, [xpre5s1o∩,即opemtor左侧的表达式。 □r1g们t:必需,[xpres5jo∩,即operator右侧的表达式°

这里又需要三个参数,opemtor就是运算符’1e十t就是运算符左侧的内容, right是右侧的内容。 后面两个参数都需要是[xpre55jo∩,根据AST,这里的[xpre551o∩可以直接声明为Ide∩ti+ier和 ‖uⅧerj〔儿jteIa1,所以又可以分别用type5的1de∩ti+ier和∩uⅦer1c[1tera1创建°

这样梳理清楚后’我们从里到外将代码实现出来,一层_层构造,最后就声明了一个 γar1ab1eDe〔1amtio∩类型的节点°

最后,调用path的j∏5ert∧代er方法便可以成功将节点插人到path对应的节点°

这里关于tγpe5的更多方法,可以参考h』ps://babe0s.io/docs/en/babcl一types沁inaIyexpIEssjon,这里的 很多方法和节点类型都是对应的’利用方法便可以创建_个节点,具体的参数可以查看每个方法的文档° 9总结

膛尸‖|仁「

至此,我们就把Babel库中有关AST操作的方法都介绍完了,内容还不少,需要好好梳理和消化° 熟练应用如上方法之后’我们就可以灵活地对JavaScript代码进行处理和转换°进-步地’将其应用

可`到■

广‖▲

||||||||||』‖‖』】】』■■■■

■厂仁■■、‖β匡■厂■『

到JavaSc∏pt的反混淆中也是可以的。

在下—节中’我们就来了解AST如何进行混淆代码的还原。

本节代码参见: http://githuhcom/Python3WebSpjdeI√LeamAST°

|↑.9使用∧S丁技术还原混淆代码 在上一节中,我们介绍了AST相关的基本知识和基础的操作方法,本节中我们就来实际应用这些

方法来还原JavaSc∏pt混淆后的代码,即_些反混淆的实现°

由于JavaSc∏pt混淆方式多种多样,这里就介绍一些常见的反混淆方案,如表达式还原`字符串 还原`无用代码剔除、反控制流平坦化等。

↑.表达式还原

有时候’我们会看到有一些混淆的JavaScnpt代码其实就是把简单的东西复杂化,比如说-个布 尔常量true,被写成!![];_个数字,被转化为p日r5eI∩t加-些字符串的拼接。通过这些方式’些简单又直观的表达式就被复杂化了。 看下面的这几个例子,代码如下: CO∩5ta= ! ![]j 〔o∩5tb= "ab〔口 ≡= 0b〔d"j Co∩5t〔≡ (1〈<3〉| 2j 〔o∩5td=Par5eI∏t("5‖‖ + "O")j

■】]■■■■■{△尸』●‖‖■‖||」‖』■】{|』‖■■勺』‖‖‖‖|刮‖·|‖■■|■||·叮‖·■』』■=■』■■可|{」·■□‖‖。‖

第ll章JavaScript逆向爬虫

472

对于这种情况,有没有还原的方法呢?当然有,借助于AST’我们可以轻松实现°

怎么处理呢?我们将上述代码保存为code1js,根据上一节学习到的知识’可以编写如下还原代码:

mpoIt+5千ro∏〗 0干5呵;

co∩5t〔ode≡千5.read「j1e5y∩〔("〔ode1.j5"’ "ut+ˉ8")『 1et日5t≡par5e(code)i

』■■■可■■可‖·■■

j∩porttI3γer5e千ro『『‖"0babe1/tmγer5e j j川port{par5e}froⅧ"@b日be1/par5er"; mportge∩emte+ro∏``0b3be1/ge∩erator厕j mport*日5type5+ro们 !00babe1/types"j



|」‖】‖日』■■‖■

首先,在=的右佃‖,其实都是—些表达式的类型’比如说"abc"="bcd』』就是-个8j∩ary[xpre551o∩’ 它代表的是_个布尔类型的结果°

tIaγer5e(a5t」{

"0∩ary[xPre551o∩|81∩ary[xpre55jo∩|〔o∩d1tjo∩a1[xpre551o∩|〔己11[xpres51o∩|′ ( co∩5t{〔o∩+ide∩t’γa1ue}=path.eγ日1u己te()j }’ })j

〔o∩st{〔ode: output}=8e∩erate(a5t)j co∩5o1e.1og(output)j

这里我们使用tmver5e方法对AST对象进行遍历,使用|!0∩ary[xpre55jo∩|8j∩ary[xpre5sjo∩| 〔O∩d1t1O∩a1[Xpre55jO∩|〔a11[×pre551O∩』』作为对象的键名,分别用于处理一元表达式`布尔表达式`

|]

司引』■』】|』■‖‖

方法°在回调方法里面’我们调用了path的eγa1uate方法’该方法会对Path对象进行执行,计算所 得到的结果。其内部实现会返回_个co∩千1de∩t和va1ue字段表示置信度’如果认定结果是可信的, 那么〔o∩+ide∩t就是true’我们可以调用pat∩的rep1ace‖1th方法把执行的结果γa1ue进行替换,否



‖‖

条件表达式、调用表达式。如果AST对应的pat∩对象符合这几种表达式,就会执行我们定义的回调

0

(‖《{‖』|‖

jf(va1ue≡≡I∩于j∩1ty || γa1ue≡ˉI∩十j∩jty)retur∩; 〔o∩十1de∩t88pat‖.rep1a〔eMt们(type5·γ己1ue丫o‖ode(v日1ue)〉j

」■■■]

p己t‖ ) =〉{







》 |



ll.9使用AST技术还原混淆代码

473

匹■■}■■‖■『

则不替换°

运行结果如下:

■『|「|~■

〔O∩5t己立truej co∩5tb=+己15ej co∩5t〔左1O;

〔o∩5td=p3r5eI∩t("So");



所以,利用这个原理’我们可以实现对-些表达式的还原和计算’提高整个代码的可读性。 2.字符串还原









闪 片

我们先在ht‖ps:〃astexplorerneU里面把



■十了■9



巳屯习仑卜

卜β

吗?当然可以。

货 见

对于这种字符串’我们能用AST还原

]=『乙

找关键突破口’就搜不到了。



代码里面’我们想通过搜索字符串的方式寻

任◇ 应〖 厕 扭 亡 Ⅳ 蔽 僧 “ £ 立 庐 ← 』 " … 牵 硒 ‖么 冈工 ● 巴

…………跺∏’…

〖■厂‖■尸||■凸尸■■

了。如果这样的字符串被隐藏在JavaSc∏pt

正γ■▲『砧七γ〕『】

转换成UTFˉ8编码之后’其可读性大大降低

………砷……毛

其实这原本就是_个简单的字符串,被

扛 硅 口亡 ≈已 Ⅲ工·勒…让啡 …』滩正恤跳…`″七∏勤 $■亡°工七曰●工 壬《‘偶 °夕[∏迂+

凸工

co∩5t5tri∩g5≡ ["\x68\x6S\x6c\×6C\x6千"’ "\X77\X6「\X72\x6C\x6』"]j



△尸■■■■■尸『巳■『

码的数据,比如说这样的例子:

七亡窿巳】巳 ∏ +■

在1l.l节中,我们了解到, JavaScnpt被混淆后,有些字符串会被转化为Unjcode或者UTFˉ8编

……″ ′间 巴门 ◎臼 土α 」 】 ∑ ■ 吕 之 $ 【 □ 臼 帅 日 正 ∏ ′ ◎ 巳 划 工 义≥ 贺 旷 】羞 硅

■■■■尸匹■『β‖■∩尸}‖卜

可以看到’原本看起来不怎么直观的代码现在被还原得非常直观了。

J

□h●11◎■

ue8

ra

\』,\\x66\\x65\\x6C\\x6c\\x6r\盯"′

这行代码粘贴进去,结果如图l1ˉ83所示°

5trj∩g[1tem1类型’它们都有_个extra属



=S七r立n砰L』七er患l {

七y日e】 碘E七r1ngL」tSr己1■

性°eXtra属性里面有_个m"属性和

巳伯■r七∑

mWγa1ue属性,二者是不_样的, rawγa1ue

pnd8

↑ 厂川[

巴『‖●△■‖●尸■■

v巳1ue〗 0‘h仑11◎曰

可以看到,两个字符串都被识别成

冯1

63

△∩}卜

守■

的真实值已经被分析出来了°

+1唾2 {s蛔rt′ Ond′血』e回■陌●′《“∩t圣虹er灿p,G} e派cxa苫

因此,我们只需要将5tr1∩g[1tera1中





↓ β



β



\×







卫 了







β







丁 了













\ \ ■■













』α





鱼 寸

丫←

可,实现如下:





△尸■■「■尸二■【卜》上■「|

extm属性的raW值替换为mWγa1l」e的值即

j呻orttraγer5e「r咖000b己be1/tⅢaγe工5e ; mport{par5e}「r咖"0b3be1/par5er辙j mport8e∩er日te+ro‖"0babe1/ge∩emtorwj mport十5+ro们 0干5"j



Tawn1闷●$ ∏w◎厂γ门"

》 】

图llˉ83粘贴代码后的效果

伪『

co∩5t〔ode≡f5.read「i1e5y∩c("code∑.j5圃』"ut十ˉ8赋); 1eta5t=par5e(〔ode)j tmγer5e(日5t’{

5tri∩gLitera1({∩ode}){ jf(∩ode.e×tra腿/\\[ux]/8i.te5t(∩ode.extr己。m饵)){

p

》β|卜『

p

∩ode。e×tra·raw=∩ode.extm.r己wγa1uei } }’ })j

『「

co∩5t{〔ode: o(』tput}≡ge∩erate(a5t〉j 〔O∩SO1e.1og(Output)〗

|}△■=■■=’||~■厂‖■





| 第ll章JavaScript逆向爬虫

474

co∩5t5trj∩85= [he11o’wor1d];

如果我们把这个脚本应用于混杂了混淆字符串的JavaScnpt文件,那么其中的混淆字符串就可以 被还原出来。

这里还是拿ll.l节的样例来介绍,代码如下:

co∩5o1e.1o8("he11owor1d")j }e15e{

co∩so1e.1og("th15"〉j 〔O∩5O1e.1og("iS")j 〔o∩so1e.1og("dead厕); 〔o∩so1巳1og("code圃)j Ox1十7292=十u∩ctio∩(){

i十("x朋γ2∩0d于y2‖".char∧t(』〉 !≡=5tri∩g.十ro∏OaI〔ode(110)){ 〔o∩5O1e·1O8(阐thj5")j 〔o∩5o1e°1o8(|0j5")j 〔o∩5o1e°1og(闻dead||)i 〔o∩so1e.1og("〔ode阐)j }e15e{

〔o∩5o1e.1og(圃∩1〔eto赃etyou")j }

}j O×16〔18d()j Ox1十7292()5

这里首先声明了两个方法,最后分别调用’而且两个方法内部都有一些1fe15e语句。比如,第

一个j十语句的判定条件是!![[]],乍看起来并不能直观地看出它的真实值到底是多少’其实这里有 一个双重否定’后面紧跟一个二维数组{[]]°由于[[]]本身就是一个非空对象’加上双重否定之后结

果就是true。第二个i千语句的判定条件则是_个字符串的判断,前者|』X"γ2∩Od十γ2‖".〔‖ar∧t(4)其实 就是字符∩’ 5trj∩g.「ro∏汇har〔ode(110)就是把110这个ASCII码转换为字符’结果也是∩,而判定符 又是|=’所以整个表达式的结果就是十a15e°

所以说,第一个方法其实执行的是j十对应的区块, e15e对应的区块是不会被执行的。第二个方

法其实执行的是e15e对应的区块, j十对应的区块是不会被执行的。不会被执行到的代码其实是冗余 的’起到-些干扰作用,加大我们分析代码的难度°

对于这种情况’我们也可以使用AST来把_些僵尸代码去除。

首先’我们把上述代码贴到ht印s:〃astexplo爬me″分析_下°选中第_个方法里面的j「语句,如

图l1ˉ84所示’可以看到它对应的就是一个I十5tateⅧe∩t节点,它有type` 5tart` e∩d、 1o〔` te5t、 〔o∩5eque∏t、 a1ter∩3te这几个属性,其中te5t就是指1+判定语句,就是! ![[]]’ co∩seque"t就是j十 对应的代码区块, a1ter∩ate就是e15e对应的代码区块。

」可‖□ˉ□|‖]|』√)■〗‖{■勺叮{{|口ˉ·‖】■‖|』‖■∏‖|」■]|‖」■●||』·〗‖』■】』■】□可』■‖||‖』■∏|■』〗||■‖〗』』■】□■‖||■】■|日刽」■〗■‖‖■■】』■】‖■∏』■|』■■■■||·】』■

} }j co∩5t

□□‖凶■∏|‖』■■∏』‖』■]Ⅱ可

co∩st o×16c18d≡千u∩〔tio∩ (){ i十(|![[]]){

』□可(|{|‖■■】‖‖■□】闪‖』■|

在ll.l节中,我们还了解过其他的混淆方式,比如说为了使代码的可读性降低’混淆工具会给原 来的代码注人一些无用的代码’这些代码本身其实无法被执行。

■□■‖

3.无用代码剔除

□‖|‖】】□〗‖‖

这样我们就成功实现了混淆字符串的还原°



】|·

输出结果如下:



} 』■■「|『■■『■厂|‖『■||‖卜

ll.9使用AST技术还原混淆代码 =b◎dy5

475



=汪S七己七emen亡

缅口“{

七yPe: w工fStat蹿獭en七闪 s↑刁丁七g

end8

34

192

∩|

+1◎C: {s亡己r亡′ e〃d扩虹I曰刃曰mc′ 工◎●nt∑m●rⅣ句me》

■ ■ ■ 尸 『 『 ■ ■ 尸

+七es七8 Una工yExme$S止◎n{t″辑′鸵豫燕′@『】◎′ 『◎c′ ◎pek己乙◎r′ .。· 十2}

+C◎nsequen七: B1◎CkS七己七emen亡{仑″臼? s亡ar亡′

》》■『巴β广)

eJTα′ 』◎G′ 归《,αy′ ·. · +1}

+a1亡erna七eS B1GCkS七a七emen七{t″@鸳 st凸rt′ Gnd′ 义“′D◎创P′ 。·。 十i》 》 ]

α土re◎t工veSg [ ] 卜【、■

图llˉ84选中第一个方法里面的i「语句 ↑■■『

∩『■|

0

所以’这里我们可以实现如下还原代码: 00

‖『卜卜

mpOrt tr己γerse「roⅦ"@b日be1/tmγer5e j 1们pOrt {par5e}十Io∏ !!@babe1/parser0|; 1ⅦpOrt ge∩eIate十r咖"0babe1/ge∩emtor"j ]ⅧpOrt *a5type5十roⅦ"0b日be1/type5"j mport +5+rOⅦ 00十5厕】

〔o∩5t〔ode二+5.read「i1e5y∩〔("code3.js"’"ut+ˉ8")j 1eta5t≡par5e(code);

「β

traγeI5e(己St’{

|‖

■尸|■■■尸●□△■厂■■卜匹矽△■■‖□‖『巳■■『

I+5tate爬∩t(p日t∩){ 1et{〔o∩seque∩t’ a1ter∩ate}=patb.∩odej 1ette5tp日th≡P3th.get("test")『 co∩5teγa1‖ate『e5t=te5tpat∩.eγa1uate丁rut打y()j i千(eγa1u己te丁e5t≡=tme){ 1+(type5。i581ocR5t日te‖论∩t(co∩5eque∩t)){ 〔o∩5eque∩t=co∩seque∩t,body; }

pat∩.rep1a〔e"1t∩∩u1tjp1e(〔o∩5eque∩t)j }e15ej+(eγa1uate丁eSt≡≡+a15e){ j+(a1ter∩ate |=∩u11){ i十(type5。js81o〔|〈5tate∏|e∩t(己1ter∩ate)){ a1ter∩ate=日1ter∩日t巳body〗 }

pat∩.rep1aCe‖1th‖u1tip1e(a1ter∩ate)j }e15e{

pat∩.Iemγe()j } } }

})』 伊「卜‖■■厂卜卜‖‖丛■[β【■「‖卜巴■∏■■巴尸

co∩5t{code$ oqtput }=ge∩emte(己5t)j 〔o∩5o1e。1o8(outp‖t)i

这里我们定义了一个I+5tate『∏e∩t的处理方法:首先获取到path对应节点的〔o∩5eque∩t和a1ter∩ate

属性’然后拿到te5t属性对应的pat‖’赋值为te5tpat‖,接着调用te5tpat∩的eva1uate丁rut∩y方法’

eγa1uate丁rut‖γ方法可以返回对应pat∩的真值。比如说’对于第-个i+判定语句( ![[]],它的值是 m』e’那么eγa1uate丁n」t∩γ方法返回的结果就是tn』e°



0

|卜

b伪



||

第l1章JavaScript逆向爬虫

476



如果是true的话,应该怎么办呢?很简单,直接将整个pat∩替换成co∩seque∩t对应的节点就好 了°也就是说’对于第_个方法’原本是: i+(! | [[]]){ co∩5o1e.1og(碱he11o胆r1d")j }e15e{





」‖

〔O∩5O1e.1og("tbj5")j 〔O∩5o1e.1Og("i5徽)j 〔o∩5o1e.1og(闰dead")j co∩5o1e.1og〈"code词)j

直接替换成: 〔O∩5O1e。1Og〈"he11O"or1d")i

所以,原本不被执行到的代码就被完全删除了’同时i+和e15e语句也被删除了’最后只剩下可 以被执行到的代码°



j

最后的运行结果如下: 0

〔o∩5t 0x16〔18d=十u∩〔tjo∩ (){ 〔o∩5o1e·1og(′′he11o"or1d"); }j

U









〔o∩5t

0x1十7∑92≡+u∩ctjo∩ (){

〔o∩5o1e.1og("∩1ceto爬etyou")j }j

‖ q



0x16C18d()i Ox1于7292()j

可以看到’无用代码被剔除了,代码变得非常精简,可读性大大增强° 4.反控制流平坦化

另外’在ll.l节中,我们还看到-种混淆方式’叫作控制流平坦化’其实就是把原本正常执行的 逻辑顺序进行了混淆’通过_些i十e15e或者5"1tCh语句进行拆分’这导致我们不能很直观地看到各 个代码区块执行的)硕序。 还是拿之前的样例,代码如下: 〔O∩St5= ′03|1|2·.5P1jt("|")j 1etX=0j

"h11e(tme){ 5W1t〔h (5[X++]){ 〔a5e 00100 :

〔O∩5ta=1j 〔o∩t1∩l」ej 〔a5e 『0200 :

co∩5tb=〕j 〔O∩tj∩uej 〔a5e"3":

〔O∩5t〔=0j 〔O∩t1∩uej

} bre毗j



可以看到’这里首先定义了一个S变量,其中使用5p1it方法对字符串进行分割’结果其实就是 [′‖3"’ ||1|』’ "2"]’然后配合使用W∩j1e和5MtC∩语句’这里判定5[X++]变量’每执行-次循环,它

的结果就会变_次’三次循环分别就是3、1` 2’然后每次循环都匹配对应的〔a5e语句并执行不同的 语句°











ll.9使用AST技术还原混淆代码

477

所以说’代码真正的执行顺序其实是: Co∩5t〔=oj 〔O∩5ta=1j

〔o∩5tb=3i

而经过控制流平坦化之后,代码原本的执行顺序就被混淆了,我们-眼不能看出真正的执行顺序° 要进行代码的还原’我们就需要做如下处理°



□首先找到5W1t〔‖语句相关节点’拿到对应的节点对象’比如各个〔a5e语句对应的代码区块° □分析5"jtC‖语句判定条件5变量的对应的列表结果’比如将』』3|1|2!』.SP1it("|′′)转化为["3』』’ 』|1』|’ )|2||]°







p



□遍历5变量对应的列表,将其和各个〔a5e语句进行匹配’顺序得到对应的代码区块并保存° □用上-步得到的代码替换原来的代码即可°

注意上述思略虽然看起来是专门为当前示例代码设计的还原方案’但其实其对应的逻辑就是混淆 工具obfUscator的常用套略’都是先用一个类似b|a|〔这样的字符串’然后调用5p11t方法得 到一个列表’再使用5WjtC∩语匀来匹配列表的每一个元素并执行对应的代码°所以,上述解 决方案其实也可以算作较为通用的解决方案°

接下来’我们分析_下°首先,还是把上述代码粘贴到htlps://astexploremeU分析_下’"∩11e语 句就不再赘述了’它就是_个无限循环°我们看看5Wjt〔h语句的结构’如图1lˉ85所示° 哇≡』□



=贷画i+广



Sw1亡chS七atem巳n七 《

七γr》冶; "Sw止仁ChS亡a七emen七|‖ 吁

s栏肉节七弓

end:

58

226

+1◎C: {□亡毋r之′●nα′赵见On吕赡′ 工d●n亡工红erⅣame} ≈d土巴cr土xun巳n七自 M巴nb巴rExPreSSx◎n {

′亡yP巳8 "Me〖"berExpreSSiOn00 睁棋今

S朵剧7仁; 66 endS

72

+1◎C: {S亡ar亡′ en绒′红1endme′ ident1虹●r№mP)

+◎bje巳七2 工den七i过er{亡】Pe′ sc巳rt′ 臼厕α′ 晶°c′ fl凸me}

GomPu七ed2 true 审.

+谆r◎Per℃y: UPd巳七eExpresS工◎n{亡xp@′ St虚r亡′ e肋d′ 』◎锣′

…工涸亡◎r′ .°·榴} 》

·





+Sw止chcaS巳{亡Ⅺ睡′ E亡闺r亡′ O∏d′ l◎e′ e◎n宣eqc●n亡′ . 。. . +】》

+Sw1七ChC己Se{t…玄 禽亡ar亡′ 名m′m癣′ G◎n5equent′ . ° ˉ

{]

=C己S巳S台

越}

+Sw工七ChCaSe{亡』PPe′ 曰亡ar亡′ end′ 』◎c′ 官◎nseq[Je∩亡′ . ′ . +』} 》 酞

} 坠旦~…Y

图l1ˉ85 5WjtCh语句的结构

可以看到,它是一个5W1t〔‖5tateⅦe∩t节点,带有di5〔rim∩a∩t和CaSe5两个属性:前者就是判



厂心·■

478

第ll章JavaScript逆向爬虫



所以我们先尝试把可能用到的节点获取到,比如d1s〔r1∏1∩a∏t`ca5e5和d15cr1m∩a∩t的obje〔t property’相关代码如下:

■□■■

定条件’对应的就是5[X++];后者就是三个〔a5e语句’对应的是三个5W1t〔h〔a5e节点°

tmγer5e(a5t’{ . "hi1e5t己te∏记∩t(path){ 〔o∩st{∩ode’ 5cope}=p日th】 co∩5t{te5t’ body}=∩odej 1et5wjtc∩№de≡body.body[o]j 1et{diSCrmi∩a∩t’ Ca5e5}二Swit〔h‖Odej 1et{object’pIoperty}≡dj5〔rjm∩a∩tj }」 })j



」习

由于我们关注的是5wjtc∩的判定条件,所以这里进_步追踪下判定条件5[x++]°展开obje〔t’ 可以看到它就是_个Ide∏tj+1eI节点’如图llˉ86所示°

■]|」■■司



ˉdxSCr九m土nan七: Membe工ExPreSS工◎n 《

七yPe8 铂M…erExPr●SS立◎n” s七a了仁8 台nd:

β6

7∑

+1◎Cg {惠它£rc′eⅢα’红』巴闽锤翻G′ 』“D越虹er蛔鳃} -QbjeG七; 工den七」mer { 七yPe2 仰工den七if止er唾 s七烈x七Z

‖||□』■

end8

β

6嘱

67

n盈m@8

"S|』



c◎缸聘u七咎感: 仁rue

+p王◎Per七y· uPda七eExPress工°n{t帅●′s“r亡,●刃创



』◎c

口p●r■t酝q′ °·。镭》 }

图llˉ86展开obje〔t

先拿到这个节点的∩a们e属性,添加如下代码: 1etaIr‖己『∏e=object.∩aⅧej

这其实是一个数组,那么它原始的定义在哪里呢?其实在上面的声明语句里’就是CO∩5t5= getB1∩di∩g方法获取到它绑定的节点,添加如下代码:







|旦■可|■刁

|‖3|1|2||.5p11t(|』|")j 。那么我们知道了5’怎么拿到其原始定义呢?我们可以使用S〔oPe对象的

』■〗□可‖‖|乙■‖■]』■■习‘』■■■】‖{‖」■习

+1◎c: {臼t□r亡′ ●门α′鲤“n闽…′ 上d●n亡上鲤白rⅣn皿□}

1etbi∩dj∩g≡5〔ope.get8j∩di∩g(arr‖aⅧe)i

其实这个bj∩d1∩g就对应"3|1|2!|.5p11t(|||")j这段代码°

我们再选中这段代码’可以看到它是-个〔a11[xpre55io∩节点’如图11ˉ87所示°

这里我们怎么获取它的真实值呢?其实就是使用"3|1|2"调用5P11t方法即可°我们可以分别逐



层拿到对应的值’然后进行动态调用,添加如下的代码:

1etarmy「1ow=obje〔t.γa1ue[property.∩a们e](argu川e∩t)j

尸□∩~■■巴■■二】■■【■■【

1et{j∩jt}二bj∩di∩g.p己th.∩odej obje〔t≡1∩jt°〔a11eeobje〔t〗 property=j∩it·〔a11ee·propertyj 1etaIgⅧe∩t≡i∩jt°ar8u『‖e∩t5[0]°va1ue;





ll9使用AST技术还原混淆代码

479

P

司■■■■

■‖◆『□「||■「‖|‖■尸【份‖|[∩

△尸》匹尸|■∩‖}■【■厂『■■「



上面这几行代码其实就等同于调用了 ˉ工n让. ca1mxp……n t "3|1|2".5P1jt(|′ |") ,只不过这里面的值是

膛ype. 』℃盈X1E蕊p厉…工。n.

我们从节点里面动态获取的°所以’这里

s膛a重仁.m

e甄d. 28

array尸1O"的值就是["3』』’ "1』』’ ||2"]了° 后面怎么处理呢?我们只需要遍历这个

+1◎C: {霉超雄〃蟹阅鳃mⅪe∏a…′ 卫“n越鳃孽r脆瓜》@} =ca11●e:眶mherExpreSS工◎n {

窿yp.. ·Hemh。rxxpre…°愿|

列表’找出对应的〔a5e语句对应的代码即 可。由于遍历的执行是有||顶序的,所以最终

s窿.r亡: 】o

拿到的每个〔a5e对应的代码也是符合这个

。nd

+1°c圆 {s谴…′ 戳魁创′ f辽“…4酶赋应fj酝制‖睦厕·》

顺序的°

ˉ◎bje◎七? S七rin,L止七era1

因此’我们再添加如下遍历处理的代码: 1etre5u1t8odγ= []j arraγ「1ow.+or[a〔∩((1∩dex) ≡〉{

七yPe

S七ringL工七er己1闪

s西r七

10

$n◎“f

em吕 17

户 ‖ ■ ■ 「

1et5"itCh(a5e=〔己5e5.「j1ter((〔) =〉 〔·te5t.γa1ue==j∩deX)[0]j

+1°C: (窟mr它′ °回鲤1ena厕律′ 1“庇2r上e星№m·》

1et〔a5e8ody=sⅦjt〔h〔a5e.〔o∩5eque∩tj

+extra台 {牙awVme′raw}

1千(types.j5〔o∩tj∩ue5tate|∏e∩t(ca5e8odγ

仆▲尸|》■■『‖|卜)~尸「||户■厂「卜》『》‖》|‖》【尸『[‖■■‖}‖◆「}|「·『|‖‖卜》■【‖匹◆『■·『|■「}|

[〔a5eBody.1e∩gt们ˉ 1])){

va1ue: "3}1|2` 》

〔a5eBody·pop();

} });

23

c◎m斟呻“』 费ais窿

resu1t8ody≡re5u1t8ody·co∩〔at(ca5e8ody)i

=Pr°Per七y, Iden七土f1er { 七yPe8 "m色邀七立£工巳r饰

这里我们声明了一个re5u1t8odγ变量 用于保存匹配到的Ca5e对应的代码’同时还

S七溪rt: 18 end.

把Co门tmue语句移除了。

23

+X◎c官 {篇t截吐′ G门口宛忍唾口凸酿避 赎 嚣“舞趣鲤er恕a…}

n“ne8 曲sP1i七"

最后’re5u1t8ody里面就对应了三块 代码:



CO∩5tC=Oi 〔O∩5ta=1j

十■工gumen龟s8 [】兽L碑治n }

〔o∩5tb=3j

这样原本的代码||顶序就被我们还原出

图llˉ87 〔a11[xpre5s1o∩节点

来了。

最后’我们只需要把最外层path对象的代码替换成re5u1t8ody对应的代码即可’添加如下代码: patb·rep1aceMt‖"l」1tip1e(re5u1t8ody)】

最终整理—下,完整代码如下: 1‖porttraγer5e「ro0∏ ′`@babe1/tmγer5e"j i爪poIt{ parse}+ro""@babe1/par5er j 1阳portge∩eIate「roⅧ"@babe1/ge∩emtor!!; mport本a5types十ro‖"0b3be1/type5"j mpOrt于5+rO川"+5";

〔o∩5t〔ode二十5.read「i1e5y∩c(0!〔ode4.j5"’"ut+ˉ8")】 1eta5t=parse(〔ode); tr己γer5e(a5t’{ ‖∩i1e5t3te"冶∩t(pat∩){ 〔o∩5t{∩ode’ 5〔ope}=pat∩;

〔o∩5t{te5t’ bodγ} =∩odej 1et5w1tch‖ode≡body。body[o]j

1et{di5〔r1"1∩a∩t′〔a5e5 }=5W1t〔‖‖odej

1et{obje〔t’ property}=djs〔r1Ⅷ1∩a∩tj 1etarr‖己贬=object。∩a爬j

■□】■∏■■■|」■■■】■司■■门·可|

第ll章JavaScript逆向爬虫

480

1etbi∩di∩g=ScoPe。get8j∩dj∩g(日rr"a们e)j 1et{ i∩jt}=bi∩di∏8.pat∩°∩ode5 obje〔t= i∩jt.ca11ee.object; property=i∩it°〔a11ee°propertyj

1et己rgU|‖e∩t=i∩it.argⅧe∩t5[O].va1ue〗 1etaImy「1o"=object.γa1l』e[property.∩aⅦe](arg(』Ⅷe∩t)j 1etre5u1tBody= []5 aImy「1Ow.+Or[a〔h((i∩de×〉 =〉{ 1et5WitCh〔己Be≡〔日5e5.「i1ter〈(C) =〉C.teSt.γa1ue=j∩de×)[0]j





1etca5e8ody=5w1tch〔ase.co∩5eque∩t;

〔ase8ody.pop()j } re5u1tBody=re5u1t8ody.co∩〔己t(〔3se8ody)j })i

田| {

p己th.rep1ace‖ith‖u1tjp1e(re5u1tBody); }’ })j

』{

1十(type5.is〔o∩ti∩ue5tate爬∩t(〔a5e8ody[ca5eBody.1e∩gt门ˉ 1])){

| β■

〔o∩5t{code: o0tput}=ge∩erate(a5t)j 〔o∩5o1巳1o8(output)j

运行结果如下: cO∩5t5= "3|1|2".5P1it("|")j ■ ‖

CO∩5t己=1j



CO∩StC≡Oj



1etX=O;



可以看到,原本控制流平坦化的代码就被还原得清晰又简洁,而且代码的执行顺序也一目了然,

」|』

co∩5tb=3j

这样我们就实现了反控制流平坦化。

{‖

5.总结

在本节中,我们通过四个案例讲解了利用AST还原混淆代码的过程。案例虽然基础’但是其中的

本节代码参见: https://gjthuhcom/Python3WebSpideI/Deobmscate°

||.↑0特殊混淆案例的还原

||可勺|■■|叫

思路值得深人研究。有了AST的加持’很多混淆代码都有机会被还原得更加简洁、易读’从而能大大 降低我们逆向代码的难度°



除了基于javasc∏ptˉobfilscator的混淆,还有其他混淆方式,这里介绍几种有代表性的混淆方案(比 ↑.∧∧巨∩code的还原

AAEncodc是-种JavaSc∏pt代码混淆算法,利用它,我们可以将JavaSc∏pt代码转换成颜文字表

|』‖{

如AAEncode、JJEncode` JSFuck)的还原方法。

示的JavaSc门pt代码°



(〈

这里有一个示例网站h忱ps://utf8jp/publjc/aaencode.html》打开之后我们便可以看到如图llˉ88所 示的样例。

■■司‖则|」勾

‖‘‖田』■围



l

■■尸『·~■■『『△『卜匹■「|』「|『■尸‖「

ll.l0特殊混淆案例的还原

48l

aaef]comαem◎回■

仿『‖「||■『「【》■β『

aaenm由ˉ匠加odeaJv」aV臼sc∏ptpm■a肮℃」a匹吧睡Stγ伯0∏》ot{c◎陋《仁^》 〔哦e厂」臼γaSc『lptmu『ce。

, 膝

「…愈…》—__一

■■ ■……

「‖『【『■‖‖》

′刨.′·′ˉ『wˉ)/纠-L〃·″v.已/【0=0〕β…墅) ■ˉ■0β c鲸O.)吨→〕ˉ〈3);(且,》牢e.≥(Q∩ˉ∩O)/(吨铂);r厄.)叮O. ; ‖ˉ0 ’·四./ : (〈幻.′■3》◇oˉU〗r S°1 ,.ˉ·′ 】(●·′◆°=o)[″ˉ灼ˉ〔·°)] 』.∏./8〔(ˉ广=】)·°~o)r→.]]0 《.∏)〔eˉ]■〔(ˉ■ˉ′己)◇4←0)[妒=≈]j(.且.)[‖C·〕·《〔且.≥0-o)〔〔ˉ蛰》0〔=》re·)』『〔 q·》[°□嗡]▲((∏.>.ˉ,〕r●.];〔◎≡河·且·〕[·亡,玉[且.〕【‖·刨〕疥■.′◆‘←.〕rO.胀〔(凹°/=3)◆°ˉ·〕〔审.〕◇〔(且.)+.=.)[(→〕◆(气.Ⅺ◇〔rˉ嗡≈』)◆°ˉ·)[●.> 《〈ˉ△=3)◇划-U)〔〈ˉ°〕ˉ(O.〕]<∏·〕[°毡o】×(A.≥·→,〕rˉ.>〈ˉ4)〗◆(口.〕[o°0]×r二乙〕宁o→o)〔◎,〕j〈且营》〔o=p]域O^ˉ℃)〔6〕r◎】0(■.河(-.■]) ◆‖→‖)『e·]vr且·〕 .·且=/·〔r红》◇0ˉc)[(=)◆(=曝〗]汛〔兰=酚◇↑=′)o八←≈fO0]◇《(~.=B〕宁°ˉ°〕rO·}(■°′◆`←0)re·〕d(-.》←(◎·》;〔□)r6°>↑`\0 ; r且^)..°·′≤且.·.=°〕[弘~℃·C◎.)];《◎.=@汪■./◇o=,)[趴≡≈】6(且`)〔矿〕与?`●0;(且·)[0ˉ.]〔〔·且°)["=p]〔° ■°驱°且°)〔O舍j◆〈且·)〔∏°]◆(e°≥(←·≥〈 e·>(∏〕r6.]ofe°×〔〈=°〕宁ro·〕×〔.P>(□.〕〔8。〕砸白.>rˉP〕+“ˉ.)◆〔O,)>C且°〕〔■·〗砸e·>〔〔醉℃〕题·▲℃〕×〔〔铲幻》.rS.〕≥〔皿·〕

【‖

「■.】Q(●·×《《唾ˉ≈〕N@▲ˉ≈》>Cˉ,≥(∏.)厂■·】函rˉ.》◇re韧))o(α→牺>(∏·)厂0.】咐岂×《(″~知)ˉre°》>rⅡ.》厂5△…e°)◆〔.O·>(〖^ˉ七>r

α·》『【°]≤O>〈..×((□P)◆(·.)≥〔』.xⅡ°】0rO.)◆((碌.)◇(e.))◇(=·≥(』.〕rg.】≤O、)◇(〈岂)◇〔○·)≥〔-·≥(∏,)r已,】◆〔e嗡>《(~ˉ)◆

(e.)×(rˉ.)◆o^~∩O)×(且·》r6°]◆((ˉ.)◆(o、)>(ˉ.)◆〔q.》r0.〕Q〈→·)◇(钟ˉ凸◎≥(∏·)r8.】◇(e,≥〔e·》◇〔〔@^-∩o)ˉrO.))◇〈n.)r【.]啊e.)◆

『ˉ.〕+〔Oˉ》◆(∏.〕〔【·】巧e.≥〔《o^ˉ^o〕电o^卓∧O))◆〔〔萨=帕)=(◎▲AO))●(∏)〔■.】◆〈e·≥〔=ˉ×(e.)◇〔∏.)〔■.]舍(e.〕◆《〔°八∩@)ˉ〔eˉ))◆(伊-^O≥(

且.)〔8.】砸e.〕◆〔占)◆〔o\蛇)◇〔几.)〔0·]两e.义〔(α=≈)Ⅸo^~∩O)>〔″韵袍〕~〔e。))◆(八.〕r【,]O〔e.)◆(〔ˉ°)←(e.)〕◇(e.×〔,』,〕旨【。〕工e.× 〔〔吨≈〕◇(馁ˉ吨)〕◆〔萨→徊≥(且°〕r【°§◆re勺)+〔〔唾≈〕o〔O∩.≈〕≥c~°≥r且.)〔·‘】疼ˉP×〔〔醉~℃)ˉ〔●〕>〔且.》r【.]+(〔.p)◇(e·〕》+〔O·≥(且霄Jr ●·])re°)》〔°=.〕5

》』仙「

[鲤且i][Pe『丁γ}刨j冰‖

巳【■厂凹『

‖防■■「



图llˉ88颜文字表示的JavaSc∏pt代码 可以看到,_个最简单的HelloWorld就被转变成了很长的颜文字’代码被混淆得面目全非°

但实际上’混淆后的代码其实还是遵循了JavaScnpt语法的’只不过其中的一些变量被替换成了

『}

「|》

表情符的样子°

这里我们再看—个示例网站https://spall.scrapc.cente门’这是_个NBA球星网站,展示了球星的 _些数据,但与此同时’每个球星的信息面板上都对应了_串字符’我们把鼠标移动到面板上就可以

》伊β厂)圆尸》『|■β

看到’如图llˉ89所示: 唇Ⅷ | 。.



_=—=—



-= □

°

.…」

-_



『||●「■厂‖}‖[■【||〖【■「』『儿■■『‖Ⅱ厂●「|■■尸「||=厂







f

| |

嗣■·…■

酗.丈T兰0 ·…

衅…→= 日■户兰一

●■:=…

|邱覆 图llˉ89每个球星的信息面板上都对应了一串字符

实际上’这个字符包含了_定规律’其结果其实和这些球星的数据有关系° 接下来,我们来探究_下究竟是怎么回事°查看页面源码,如图llˉ90所示°

■‖‖‖‖‖

_≡……

■_

‖‖■

Ⅲ}藤 ■累| |B塞 | | m | ■嚣: | |必Ⅺ ■ 膨嚣 Ⅳ蔓| | |





尸■呵

p■归



■′…譬冀) 户◎…函m沁●…Ⅵ P°叹……V1

…恰

P■函

…一

勺◎…↑γ.m…≈…

四西吐尸

… 一

T□加



……乃





…_

▲■■

d…Ⅷ

.ˉm0……∑矿ˉ弓o丁『≈…`■万……………9.…ˉ弓……….…·…←ˉ ≤咕I=7由:g凹tte7…屿■>

m :蜘y■凹p`■沁丁°…埋≥

t◎>巴t●厂t■≥ ≤·斯g}°;I睬;}:艇凰娜』oγ蕊武蹿《v蹿『髓,辗蕊c蹿;黑 ≤e【=C■mg∩“…F加ve产ctoβ■■凶β[●γe了■≥ <1←…

≤■t=cOl 8印…幻〗·" 古°汗■et■■1■>

<′;潞艘…厨"叼′,·p[…■庐.c`·…陶』…·.≥ <I←cD1〗9…】】■ 8O什马et■凹∑■≥ 巾3Cm9与■■…■≥

《{ph据了·∩…》} </‖>

蛔瑟s:蹿蛾愈;《·′.『.恤』恤t卜′…D </p≥



《]

咆空卫密沁刀狙鹅驹现亚疆弘酶巫”犯”仰似唾心“侣

F……== G

唁坤m钝p

限田



第ll章JavaScript逆向爬虫

482

如罐.嚣撼铲;{叮·…峨t》村…诊

≤′>

</●I~c◎D </·1=… 刽·l=C□7d≥

</e1=tm1t… </el七o1> </el≡cD`D 47 </●l=厂呻 ≤/●l~厂… 钳 ≤/●l→怎Q卜 ≤/●l=【Q{> $9 匈el≈『… QFl≈『… 3·勺d卫吟 勺d卫吟

‖|司‖

▲6

g】≤′ ≤′

瓣:隐糕瓣′龋;?船′丽:′‘……,… 瓣:隐糕瓣′龋;?船′丽…′….=′…修

52≤! ≤! 53≤5 ≤5

弘<!==恤仰『rJ■vO酞7ipt-≥

53<SC了人ptS「甘‖ttpS8〃u呻g.c■/●1…∏t=uA/uh/』门m·j3w汪/Sc「』pt≥

琉…i,t潞瓤孵么翻g蕊瓣蹄0贸圈〈腮k1篇鳃;;7雕j撼酗腕脆戮嫩,^…9鲍哪条′翅氧,"s腐αp`′2….

57

‖《‖勺

爵辗船;膘:赐嘛瞬孵撼,…. 图llˉ90页面源码

可以看到’lndex页面引人了一些标准库VUe、ElementUI、Crypto等’正常情况下应该不会出现在 这里面。最后,我们发现页面还引人了一个JavaSc∏pt文件mainjs,下面观察该majnjs里面都有什么° 可以看到,这里面就是_整行颜文字,如图l1ˉ9l所示。 -



-…-—■

………啥 沉≡了

尸ˉ〃

…_当

…仙

ˉ…

_〗

□呻 ··…‖0≡…巳口灯

…面



……

壁……玲

_雨

≈P圭

…困

田田≈m℃

b



·厂『!二;∏—庐r二』「ˉ已声;盂而冗(ˉˉ. 》乖ˉ了门r『己《,矾苦『°^^°)/(o^玉』;《.∏问ˉ0·: |ˉⅢ ’,w;-《(.已/≈30 ·,



●■■

卜独吨 守心■



响样

E… p◎………o… p°山…匈Ⅶ

| malnjs文件

这其实就是用了AAEncode混淆°我们尝试点击左下角的格式化按钮,发现格式化也是无效的°

当然是有的,我们可以试着先观察_下代码的规律。从代码的前后两端人手’可以观察到开头基

■]』■■∏■■

那么这种混淆方式有的解吗?看也看不懂’格式化也无效°

‖‖

图llˉ9l

本上都是』●』/=/』 们/)/~』-L /,结尾基本上都是(』川)[,o』])(‖e|))(』 』)j°因为这段JavaScript 代码是可以运行的’那么它一定是符合JavaScnpt语法的°但最后是以—个括号结尾的,按照JavaSc∏pt 的语法’可以判定前面的整体是一个方法声明°就比如类似这样的代码:

如下: he11o"or1d

其实AAEncode的原理也是将前面的内容转化成一个方法声明’最后传人-个参数来调用执行’ 只不过最后传的参数是-个下划线而已。

{ q

‖‖

对于上面的例子,假如我们不知道(千u∩〔tjo∩(a){〔o∩5o1e.1og(|∩e11o! ’ a)})这个方法声明究竟

|■可‖|」■■

是怎么写的,可以将其输出到控制台上°下面看下运行效果’如图llˉ92所示。

·纠‖■■■司

前面是_个方法的声明,然后整个通过大括号括起来’最后再传人_个参数来调用’运行结果

‖‖

(fu∩ct1o∩(3){co∩5o1e.1og(|∩e11o0 ’ a)})(Wor1d』)j

·



ll.l0特殊混淆案例的还原

483

·~‘

陋●{ t。p

v° |∩忧α 7■

》01日06830.590(fu∩ct1o∏(a){co∩5o1e.1og( ‘‖eUo0′ a)}) 〈 01:06830°591/(日){co「)5o【e°【og〈0′》e【【o|′ 句)} 〉 |

■■●『|)■■『『|‖●▲∏「||》卜『》『‖尸巴尸|)■回◆『|卜■尸■厂『|



图1lˉ92运行效果

可以看到’这个方法的声明就被打印出来了。

对于AAEncode来说’我们也可以试着将最后的参数(』 』)去掉,将前面的代码输出到控制台’ 看下运行效果’如图1lˉ93所示°

{qO" 》◇(0-薄)◇ (U》◆『∏]『【『 ]心(,0· )◆《·←.》◆『■》◆「∩》『Z, ]◆《’0、)◆『≡》◆『占)+《‘∏)『■°]◆(剿0°)◆(『亭·》◆(··.》』+《.o》◇ (·Ⅱ)『a· 】◆《·O》◆(《.=ˉ】 ◆ (·O′)△

((O~∩O】◆(0^=户D)‖· 《.∏》『°6°‖◇『·』◆《.ˉP)◆《(·ˉP)◆〔o~、)》◆[w)r■|』◆((ˉ=, ) ◆ (□~^O》》◆ ((铲℃》 =『0,)》◆『Ⅱ》[.■.】◆(纽·》+ (仁^≈^·)+《O^←^●】寸[,Ⅱ》【· ■. 】◆《0. )◇((o^≥◎) ◆《妒=℃)》◆ (〔o▲、) =〖.0·])◆《wM■ˉ〗十『α》◆〔《,=》◆《7、》)O《.0.》◆《^Ⅸ》r■0】O《.U》◆((铲=^O》◆《√^@』》◆(c^二^◎》◆(°日)「巳0 】◎《.

·. 》◆(《厂、)◆(√=、》』◆(.≤.》◆《∏》「【.』◆{.O》◆(『ˉ°》◆《·0))十 《《.→〗 ◇《铲■m》)◆{.∏)『■’〗+『叮》◆〖°00 》◆(《铲、》=《.0)》+《bⅡ)『■°〗◆(.∏》◆{《铲^■》≡ (←6》)◆《″、》◆(.∏)[·■|〕◆(『→) ◆ (·0》》◆《《铲■臼U》十《了、)]+(·Ⅸ)『已翻l叫·U)◇ 《(●G今0〗 ◆(7^·)》◆ 〔C■=、)◆《.日)『【T◆《.·ˉ 〕◆{...)◇ (.叮)o{.Ⅲ〗『■.〗州.

6)◆ 《...}◆『ˉ. 】◆『∏)『■′l十《《°司ˉ》 ◆『0》』◆《(铲ˉ●◎)+《0^气▲@》)◆ (.∏)『【嗡〗◎『Ol◆ ((矿■^o}=(′叮)》◆(铲欠》◆(ˉ∏》「【· ]◆『0省》◆(《.ˉf》 ◆『·ˉ )》◇《7≈℃》◆ (∏)『■′l·《U》◆(·-鲁》◇ (◎^■白●)◆『日》『■‘l◆{、O》◆(m^=七) ◆(吭、)》◆{铲=、》◆(.∏)『【0 〗●《‖O^内、) ◇《矿尸、】)+《(·曰p》 ◆(0^■^0》》十《.问》『6·〗◆《·U》◆(《.=》 十 (o^=、)》◆《(·″°) ◆ 《°0°》)◆《∏)f【· l◆([o■p) ◇『00 》)◇《.0》◆《0∏》『q°】◇(《°=·》◇(广=^@〗》◇《O^■^0》+『∏‖‖.【ˉ‖◆《·0ˉ》◆ 《(铲=、)◆(″←^@))◇ 〔(O~七》一《.O‖》)◆ 『冈)『6·)◆《·U》+ (°→.》◆ (《.罕. 》 ◆(·0· 》)◆《∏》r■锣]◇《’O》◇ 《《●^≈△o》◇《铲~、)》÷『=.》◇《·∏》『■.]十《w)◆《(o▲7凸O》◆《铲←、》》+《『叁]牛(,0))o 《·【》r6°‖◆(.U》◆

↑《铲■、》◆(铲■、l》◆ ((铲乒O》 = (亨U》)◆《.日】『c】◆(.■)◇ ((.ˉ.) ◆ (°·』)◆(《矿、)◆(铲■^O》》◆『∏》《它. ]◆《ˉ=. )◇ (t▲亡、》◆ 《.∏〗『《.】◆《.O)◆『夕》Qw.P》◆(. 0. 》〕◆《.∏)『■°l◆『0` 》◆(《亩=.》 ◆ {.0.》〗◆《《吮≈)吼●∩←、)》◆〔·∏〕『Ⅸ. l◆(.叮》◇《、岁》+ 《O乞、》◇(∏)【.U.〕◆《.0)◎ 《(旷二·D》◆《O▲=、)》◆《《矿=、》=《、0ˉ‖》仆《·∏}

《0口】◆(.U)◆((0~. 》 ◆《矿已PD》》◇『0ˉ)◆(·Ⅲ》「■.〗◆《嗡叮)O 《(●△≡△U) ◆(O^=、))◆{G≡Pn》◆{ˉ∏)r6. l◆「U》◆ ((O▲ˉPD》十《矿=■O))◆《,迂》◆{、∏)『0°〗刹U》◆《,■. 》◆ (《,寸)◆ (ˉ■.》]◆《·∏)r■· ]◇(■)◆ (、=》+『.P》+《.∏》『【.]◆[《·≥) ◇『U)』十((铲≠o)◆《″=^◎》)◆『阎)『0巳.】◇《.0. l◆ (《旷→^O}◆(7=℃》)+『.. )◆《.日)「巳′l◇《、0.l◆

(《°岂) ◆{′叮)》◆(《.■. ) ◇ (厂会◎》》+(.∏)『【. 】◆《·0.》+ ((硕=^o} 钓(·0》》◇(0^蛋O}◆《.Ⅲ)『巴· 】◆『0` 〗◆ (‖o往、』◆(7白^◎》》◆(.二)◆《u′》「巳ˉ}Mˉ0ˉ )◆((o宅℃》十 (矿=、))◆ 〔(矿=、) ~《窜0. 〗》十 《副【》『巳聋!◆『O》◆(『判◆{ˉU))+《,0、)十『∏》『■. lG『U)◆{『ˉ.)十 (,0ˉ 》》+ ‖(硕=七‖ ◆〔●~、})◆ 《·∏》《.6. 』◆(.□》◇『·孕)◆ [『=. ‖ ◆ 《铲=℃]》◆(·门}『■·】叫『=. 〗 + (.0°》》◆《伊=户℃》◆(ˉ∏》『【≡]◆(『ˉ· 》 ◇ 〔°□》)◆(ˉ0.》◆《口∏)『巳. ]O‖·U》◆W^酋 〕◆ (o公-/D》》◆ 〔『ˉ· ‖ ◇《.0}》十《.【》『〔蛰]◆『U》◆ (《· 占》◆《铲国9℃])◆《〔.=‖) ◇『0. )』◆ (.∏》『@】◆『·.‖◆(『.尸》+《7≈、》}十(《°到◆ (.·》]+《.日)『【.l+({..尸》 0『■》》十「叮》◆ ‖·Ⅱ)『【.]◆《.0°》◇ ((O^=、》 =【°O))◆ 《.口》[·O譬 ‖》『□))

孕01吕凶;酷°31·′…〔 ) {

C…T尸助■丁=′障』0n好α囊印·′』…r0空西它·αU0□D1尸t…y;′】蛔←酶°’″』铀rgo2”四0°m1肿〔8′〗■呵印U′》o『∩=』0〃帝仔西

口′…′0j≡萨呵0’皿付…『?】呻=』2囱c0囤钟t【0硒四′o出』蚀t『"上



图llˉ93运行效果

可以看到’这个方法被解析出了“真正面目’’’当然这里还不太好观察°我们可以进_步将方法 转化为字符串’在后面加_个to5trj∩g方法的调用如图1lˉ94所示。

0〗》◆『∏]『【,』◆《◇O》◆『『→)◆『●》0◆《《吠欠0◆《…@》》◆r∏》『■】◆《U》◆‖..P》◆〔铲ˉ≈》O(·H》「■°l◆〔ˉ0〕O〔《●…》◆(吠←≈》》◇(《●生、》 ←『·)》◇『【》 「仔】靴,U》◆ ‖『=)◆(吭勺)》◆(·■)◆《订)『■舍]+『U№《(o企^o》州吠←≈〗〗+(旨=●@》0「∏》『■.〗◆(.·)◆ 【(吮、》÷『@龟命))◆『=》◆〔.∏》『■.】◆『●》◆《.3》◆ 《f=》◆《.叮》》◆(ˉ∏)『g舍]叫.O]◆『.p)◆【曝-.》◆《·Ⅲ】『■]0《《.≥〗◆{,●》》十《『吮、)州吮、))◆「日)「■. I+(.·, )◆『〖吭、)◆《@~、》〕◆《·=》◇r日》『0.』刊·U)◆

(『字》 ◇[ˉ··》》◆(『~伞)十《砍■、)〗◆《.∏〗『它】啡□〗◆〔《吭、〗=〔°·静》》◆ ‖广ˉO●》◆【。∏》〖.r』◆(·O》◆《〔O^≡■w◆《O△■≈))◆ 〖·→}◆(、∏)『8.》◆《.··〗牛【(铲℃》 ◇ 【铲白、))◇《《吠欠』→《.·)》◆『日》『c〗别.α)◆(《.占》◆『O》》◆《·U》◆『日)『【.】◆《ˉU》◆(『~龄)·(·U))4(《铲~、》◆《●…》》◆(.Ⅱ0「■°l◆『U)◆「3)◆(『由.0 ◆《吠■勺}》◆『∏}『t°】◆[『孕)◆《ˉ·)》◆(≤、》◆《◇■0『r】◆《(ˉ■)◆(ˉ■〗肿『U》◆〖.∏0『区〗+「叮》◆《《.→P』·《●^岂、》》◇《「ˉ=)◆《v〗)◆《Ⅷ)r巳.】◆『叮〗◆(《. 鸿

侣∩ Ⅱ



·



〖w)r■.】◆『·》◆『→]◆{〔.武‖◆(.U》》什《.Ⅲ0『【,〗◆《w》◆《《吭、》州伏乡O》0◆『≡)◆『日〗『c】◆『·)◆(《●●=七》州●·■≈》》◆〔《ˉˉ, ) ◇『U』》◆〔,∏》『U】◇〔、·》◆

((吠≈》◆《@~≈]》◆((厂=≈)←『O])◆(.∏》『■ˉ】吼·U〗+(《°ˉP》◆(.■》》◆(《吭、)◆(″=、》》◆〔.∏》『c,』◆《.-.)◆(G≡P●》◆《.Ⅲ》『区瞳]◆『U》◇0.ˉ鱼》◆〔《,·P》 ◇《°

■ 】

})匹「||卜|



o」呻

…丁….f谭t厂吨[》}》}》`∩`n》

》{

仆卜◆『 ■β『}仁| 仆

图11ˉ94添加to5tri∩g方法的调用

这时我们发现这个方法的声明被转化为字符串了’内容_目了然。 将代码整理后格式化_下,就能得到如下结果: 十u∩〔tjO∩a∩o∩y∏℃u5(){ Co∩5tp1ayer5= [ {



·』·∏|



第l1章JavaScript逆向爬虫

484

叫|‖

∩a『∏e: ||凯丈ˉ杜兰特"’

1帕ge: "dura∩t.p∩g00’ birthdaγ; "1988ˉ09ˉ29"’ height: 同2O8c∩"’ Ne1ght: "1O8·9ⅨC"’ }’ ●



{日|□





□‖

]『 ∩eWγl」e({ e1: "#己pp0’’ data: +u∩〔tjo∩(){ retuI∩{p1己yer5’ 促ey: !0∩〔Q7ywz〕γ[q口丁Ⅻ〔p「〕zXv8ju0咖p∩rZ∧r" }j }’ 们ethod5;{ get『o低e∩(p1aγer){ 1et低ey≡〔rypto]S.e∩c.0t千8.p己I5e(t∩j5。代ey)j





这里发现—个get「o促e∩方法,逻辑也十分清晰’就是将球员的名字、生日`身高`体重经过处理

之后再进行DES加密,加密密钥就是key’其值就是∩〔Q7ywz〕γ[qC丁丁x∩Cp「〕∑Xγ8ju0‖wp‖rZ∧r,DES加 密之后返回即可,具体逻辑可以自行验证·

以上就是AAEncode混淆的分析思路和解决方案° 2」」巨∩code的还原

JJEncode也是—种JavaSc∏pt代码混淆算法,其原理和AAEncode大同小异’利用它’我们可以

将JavaScript代码转换成颜文字表示的JavaSc∏pt代码° 这里有_个示例网站https://utP8jp/public〈ljencodehtml’打开之后我们便可以看到如图llˉ95所示 的样例° 启′v帕旷副Ⅳ」avaSc∏ptSou『℃曰:隆

摔§

___ˉ…锤.…

′=

鳃翻戳





■【酥〔飞I1Op】…广ipt但)

慧红 如

一』

■工■

.■)



■=

α.$=S]砸$.犁.Ⅱ=[』.一叮×$. 型]◆0°≡◇$·ˉ鸽.汾9.m;$夸咏5.

辩衅捶

慧".

『$]0么ˉ:#$0义四:(o◆··》四o凹



蹈唾



0→ ′ . ,~q瓣辑蹿j蕊霹嚏捶 】|ˉ|四‖肌α「m》B 更亨霹雨…

;特S◇凹2锌$,必-?〔小■.〕

叫」■|‖】■●□■】勺■可·γ‖‖‖‖|‖|‖||纠■■‖□』∏』■〗】】】·』■□□∏■日□■可||』』|」●■‖□]||」』□∏||」】■■γ』□■■■Ⅷ】■■■■‖‖|‖口』』‖』□■Ⅵ|

Co∩5t{ ∩al∏e’ b1rthday’ height’wejg∩t}=P1aγerj 1etba5e64‖己『∏e≡〔rypto〕5.e∩〔.Ba5e645tri∩gi千y( 〔Iypto〕5.e∩〔.0t+8。p3r5e(∩aⅧe) )】 1ete∩〔rypted=〔rypto〕5.0[5.e∩crypt( `${baSe64‖a爬}${birthday}${‖ejght}${"ej8∩t}`’ key’ {∏℃de:〔rypto〕5。‖ode.[〔8’ paddj∩g; 〔rypto]S.pad.p代〔57} )j retur∩e∩〔rypted。to5trj∏g()j }’ }」 });

·几[8·皿』≥■.■‖··◇q刨〕[$窜-5】≥o·=Ⅸl=□◇··) □◆.·)[5.←$ˉ〕+Z.5$S℃+■\\·令0糊一阳0·郸=+9.坐十$-◇■

$ˉ$=◆■``■赵°-‰0·$3→◆$·3$→$,$=5ˉ分■\\.+$`一』O$惑ˉ』~◆$,■皿◆$·皿~牛·叭鳞叫伞=尸$`$$=+$,垒◆·\\p◆8·=H8°$必O$-』◆仅叭口呜√-虫5

图llˉ95示例网站





ll.l0特殊混淆案例的还原



485

■日『||■『「‖‖|■尸|匹■∏‖||△巴||『

可以看到’这个代码中包含了很多$,看起来可读性也很差’但实际上它也遵循_定的JavaSc门pt 语法。

接下来,我们再看_个示例网站https://spal0scrape.cente『/’网站的表现形式和上~个例子完全祥,其源码是经过JJEncode混淆的,如图l‖ˉ96所示。 {…





≡呵







…··●



藤…ˉ…

…_…



……

砸…



口□吨

^ ~』

1=(];$巴L一g^$′$$$$〗《![]d…〗‖0]’=$;◆÷$’$■$=8(‖‖〗◆>■》【$‖0=儿8≈$′$=5$〗〔{》◆■■)【$I,郸≈$吕《$〔0】o尸■》[$]·多6B◇◇$0$$儿:《}…◆■■》($‖o坠t◇◇$口起6?什0p$上

宁◎…‖冗==…=



●印申

b■吨 v■尸

■∏四`腰

■『■■‖怕『}「 『 | 卜 尸 } ‖ 仁 | ‖ 皿 β



■●… 炉·……=" h°凹响腆…Ⅻ

图llˉ96网站源码

其实JJEncode混淆的解决方案和AAEncode差不多°因为最后可以看到同样也是有-个(),所

以我们同样也把最后的()去掉’粘贴到控制台中’如图llˉ97所示°



「}



‖》 卜△尸|■厂〗【■「匹■尸〗‖『‖||■■■厂|巴■

吁01:Ⅱ】;泌‘y饵f…■《 》{

Cm$〖p〖…f6叫{唾』.睡…7′…S 0曲颂r.…0′D1厂【…I『lD四垫”0′蝉』吵t『0酗■o△啤】吵【0°〗■·咖0】′{■≈』,……o′』…0°j…挚p购0▲』厂f…f△】…=』刀ˉ ”0’酗佛仕00…p0…励r!0〗国

D ‖

}|



图llˉ97控制台

运行结果也极其相近’可以看到这也是一个方法。

同样,通过添加to5tIj∩g方法的调用也可以将这个方法转化为字符串输出,如图llˉ98所示°



01`1‖016·0〗7





图llˉ98添加to5tri∩g方法的调用

使用同样的方法’我们也可以对代码进行格式化并还原’再看具体的加密流程就可以了°

■■■■■■

□日‖』□‖‖』】

486

第ll章JavaScript逆向爬虫

3.」S「uc促的还原

JSFuck也是一种特殊的混淆方案’是基于开源的JSFuck库来实现的’其样例可以参考http://www



』■可‖{』□】】■■|』‖‖|』■■‖

js血ck.com/’如图11ˉ99所示。

』■■

()+ []!

{||{

[I』 」JSFuck JSFuCk1s己neS◎七ericandeduCa七j◎na1pr◎gra∏m1ngS七y1eb己Sed◎n七he 己七◎m工cp己r七s◎fJ己vaS◎x立pt· 工七u曰es◎n1ys立xd工fferen七char巳c亡ers七◎ wr立七e扁∏dexecu亡eC◎de·

Ⅲ七d◎eSn◎仁dePend◎nahr◎w曰ex′ S◎y◎uCanevenrun工七◎nN◎de.jS· ‖

Usethef◎mnbe1◎w七◎c◎nver七y◎ur◎wnscr1p七. Uncheck 00eva1s◎urce" 七◎ ge七b■◎k己p1己jns七r土ng·

■■』■|

| |恿nc。d·」田…1s。urce田RunrnP…n七sc…

ˉ



·】■■|‖

[[】】+[])[+[]】+《【 l [】+[])[+』+[]l+([】[[]]+[]》[+l+[】]+《+[ ![]]+[][(l[]+[])

|』■■]■■司|

[]]]((』』[]+[])[+!+[]]+《 ! l[]+[])[』+[]+』+[]+!+[]]+( 』![]+[]》[+[]】+([】

■■司

[][( ![]+[]》[+[]]+(![]+[】》[ !+[]+l+[]]+( l[l+[ ])[+』+[]]+(! l[]+[])[+[]]] [([ ][(![]+[])[+[]】+( l[ ]+【])[l+[]+!+[】]+( 』[]+[]》[+』+[]]+(! ![]+[])[+ []]]+[])[ !+[]+!+[]+l+[]]+(!l []+[][(』[]+[])[+[]]+(』[]+[]》[』+[]+』+[]]+( ! []+[])[+!+[]]+( 〖 l[]+[])[+[]]]》[+!+[]+[+[]]]+([][[]]+{])[+!+[]】+(』[]+ [])[ 【+[]+!+[ ]+』+[]]+(』![]+[])[+[]]+(』![]+[]){+!+[]]+([】[[]]+I])[+[]]+ ([l[(』[]+[]》[+[]]+(』[]+[]》[!+[]+!+[]]+( ![]+[])[+』+[]]+( 』l []+[]》[+[]]]+ [])[l+[]+‖+[]+!+[]]+(』1[】+[])[+[]]+《!![]+『][《 ![]+[])[+[]]+(![]+[])【』+ [】+l+[]]+( ‖[]+[】)[+』+[]]+《 ! 』[]+[]》【+[]]]》【+l+[]+[+[]]]+( !1[]+[]》[+』+

当■司』■‘□」‖

己1er七(1) |凰1ert(1)

‖‖』■

图ll~99

JSFuck混淆案例

其中JSFuck官方也做了说明,它是基于如下几个等价变量实现的:

5tri∩g

≡) []+[] =) =) ≡〉 =〉

![] [][阐「i1ter"] [][口「i1teI懒][口co∩5tIu〔tor慰](〔"[ )() [][圃「i1teI闻][磕〔o∩5tru〔toI口](阐Ietur∏this闻)()



《《

通过如上变量的组合’再加上—些小括号处理优先级’就可以将任意JavaScrjpt代码转换为我们 所看到的混淆JavaSc∏pt代码°



月‖`一■司■曰

BOo1ea∩ 「u∩〔tiO∩ eva1 m∩do制

=〉 ![] =〉 ! ![] 二> [][[]] => +[![]] =〉 +[] ≡〉 +‖+[] ≡〉 !+[]+!+[] =) [+!+[]]+[+[]] ≡〉 [] ≡) +[]

《∩

十a1Se true u∩de+j∩ed ‖a‖ O 1 2 1o Army ‖颐ber

‖《‖

我们可以看到_段a1eIt(1)代码被转变为包含[]、() ` +、 !的JavaSc∏pt代码了°

但这次不像AAEncodc和JJEncode那样了’这次混淆代码需要稍微花点时间来解混淆°

□引

■■门]』■|{■■

我们再看-个示例网站https:〃spal2scrape.centeI/’这个网站和前面的网站相比,也是仅仅只有 mainjs不同,其内容是经过JSFuck混淆得到的’如图llˉl00所示。

‖|』··可

|||

翻镰警蕊瞬』.慈

砂°…呻…噶m∏

字已

p◎m…鹰m饥 丛■■「‖|‖|■∏|}‖[巴■■「

图llˉl00示例网站 |■β|

但是观察整个代码,发现最后的部分不再是_个小括号了’内容如下: …[+!+[]+[ !+[]+!+[]]])())

这样我们就没法像AAEncode和JJEncode 可以看到,这里最后的小括号后面还跟了一个小括号,这样我们就没法像AAEnc《 β【尸‖▲β}【β止β□

怎么办呢?我们可以稍微退-步,看一下最后的-个右括号匹配的左括号是哪个° 首先可以对代码进行格式化’此时可以借助Beautj侗er工具,如图1lˉl0l所示° 仔「■■尸△尸

=尸=≈~ =——

■=些■-=≡_

■ ■ ■ ■ 日

O∏}‖『led已γaSc『‖pt日e已utj∩e『M缸↑3》 ·}}

细钳m…■…℃呜………L噎m…巾畴咖

睡.| !……-, .

■硒和…■一畸令………■…~它■憾.

_壬



妨γ封7



■…凰…≡…?

●………■……



■……酮…■■7

■β■▲■『

曾露::蹿爵黑闺毖|

■【》「』β广}‖ 尸

〖■■■▲--…~…

◆◆◆+●■

●◆◆ˉ〖

●』◆

◆◆◆◆■】

·β十

巳■◆

◆◆◆●『口

+■↓十



◆◆ ◆◆◇ ◆◆●



·〗◆

◆◆◆● ‖ +仙◆ ◆◆◆◆ 〗 ◆ ◆ ● 川 ‖ ◆◆◆◆ ◆〖〗

◆◆◆』



●』◆

№ ◆◆●◆ ∏Ⅶ』

◆· ● 〖 ●【◆』【 ◇』Ⅶ ◆〗◆≡】ˉ‖】◆·』·〗

·‖◆=‖◆■■°‖°【◆·■

川ⅧⅦ汕《◆】‖◆ [ 《【《◆】

◆】◆】◆《◆"◇

■『◆●Ⅱ◆■∏■』■‖+■■

〗勺◆‖々◆◆◆■‖◆

+· ■《 ◆〗 ▲· 巳『 ·◆ ■ β Ⅱ◆ 〗■ □已 『巳 ◇】 『乙 ■』‖ ◆

令■$◆□Ⅱ◆ 〗 ◆◆ 『■ ◆

ˉ『◆『凸◆■』◇『巳』◆

◆·〗‖■■

十 ■■ 〗‖ 『‖ ·■ 』· ◆】 ■· 】■ ■◆ 《 □■ ·■ ■◆ ‖』

』◆■·◆◇】【◆▲』

【◆··◆■■◆■■◆■】

◆ 〗◆】〗『◆川◆ ●』□■‖·叮●←〗■■巳

◆● ◆● { ◆● β◆ ◆‖ ◆仙 ■◆】 ■】◆〗■◆ˉ■【■■◆已■ 〖 』 ‖ 』 划 Ⅻ ◆ˉ ■【 ◆ 【 ●‖ 】◆ ■ 》 ‖ 〗 』 △〗 》 』仙 ◆ ◆□Ⅱ◆□■‖β■◆ 『 占□ ■〗 凸◎ 》 〗 ◆ 〗 ◆ ◆ 仙◆" ◆□【 ◆‖◆【『【◆咖Ⅷ

『《 ◆』 ◆ ■ ■ ˉ □ · 『 ■■■··‖■□

口(‖◆‖◆◆■□‖◆ˉ〗

■‖◆

●』◆◆·『◆‖●□●‖◆■■■■

■□●Ⅱ◆■`◆·■·〗·〖◆●■●∏

十·』

◆·‖·『◆巾仔◆◆■■■【◆◆

◆川咖冰山冰Ⅶ脓咖№◆ ●』 ■◆ β◆ 』巳 ■【 ◆● 』』 ■】 〗 《『》 【 【■【 〖●】 『 【 □■◆》ˉ 【【

口 【 ◆ Ⅱ · · 』 · 』 ■ β ◆ 【 ■■■■◆ ◆仑■『甲·【■■■〗·』◆·‖■Ⅷ‖早

‖〗〗

◆ 〗●〗【 〗〖‖〔【◆"‖◆ ◆【 ‖《 ‖[ ‖《 〗』 ◆』 "『 ‖【 Ⅷ【 】 ◆』 ●巳‖+◆◆◆肌∏◆◆Ⅷ

[‖[【【【‖【』【【『【『『『『【『{‖

■■β|「‖『■■

呻吨蛔Ⅶ酗…涵洒呻油潭洒洒洒油洒沤汹砸汕啤

巴■∏【『‖‖■■■尸

■■厂‖‖「

□】·】·』·】◆〗寸°】凸〗◆〗〗ˉ〗·『〗◆】◆·】‖甲

◆◆●◆◆◆

●【 】令 ◆〖 ·} 】◆ 〗 〗【 【◆ 『个‖ 〖◆ [■〖 巳 □ ■ 十 『ˉ ‖◆ ◆◆◆ ◆◆■ ‖· Ⅱ◆ ◆‖ ◆ 川川ⅦⅧ川Ⅶ◆ 川川 Ⅶ◆ ⅦⅦ

◆◆口■·■■■凸日◆◆令□·巳◆

】】〗〗〗】】』】】】

巴 ● 厂

∩1P~■△…Ⅲ·0 · ·0 ■…T~=雨~〗0…■■ ■□■口

■』≡◇玛彝.0 尸·●■

■■■「|||▲■「〖■「’【■【■「

由于小括号和中括号特别多,肉眼非常难观察出其中的规律,这时候可以将格式化后的代码粘贴

到lDE里面,借助于IDE找到括号的呸配规律°

‖巴■■『▲∏〗‖【‖止■■■丁『|

标放在最后一个括号的位置,如图l]ˉl02所示°

Beautifier工具

图l1ˉl01

U

●≡…m饰士 当…7

! ≡.

! □a…田…蝴恤

■………■冰……7 ……面…·…灯…巾队…枕……↓…S…池●…0虎

隧』 食办封● 鱼斡 ■m型出妇丁,亿

++G

◎ +

□………∏

●攀出 ■『■■『

这里我们可以选用VSCode’新建一个JavaScript文件,如mainjs’将代码粘贴进去,然后将光



◆■切■ 守■尸 巴尸卜=■尸



` F彝■

尸钓悲=~

±~学· bⅨ 捍

487

ll』0特殊混淆案例的还原 =

]◇《‖[P◆[l)[◆〗◇『I〗◆《!0[l◆[‖M◆‖]]】)[◆!◆[]◆{◆[‖】]◇〔[l‖‖1]◆[

— -P=≡

p

那样,将最后的小括号去掉了°



第ll章JavaScript逆向爬虫

488

])[+↓+[]

||

|』□■□



[√[]]] + ([][

) ) )

$『■

] ] ] [

















■□‖』

[ ) ( ]



→■‖巳

↑‖』

二飞〗′

■■』毛

■■】■

≈〗‖■

=‖‖巴

勺』■』

■#

』『■可

] ] ] [



』|

图11ˉl02将光标放在最后一个括号的位置

可以发现,最后的-个括号被突出显示’同时在VSCode上方也会有另外_个高亮的位置提示它



对应的左括号的位置,如图llˉl03所示。

d







[ ) ] [



] [ (

] ] [ + [ ) ] [









] [ ( [ ] [ ( ) ( ) ] ] [



] [



) ] [



] [ (

] ‖

口□』『{□‖■■‖



[ ) ] [

] [







] ] ] [





] [







] {



[ ) ( ] ] ] [

[ ) ] [

■■仗■

] [



■『■已



q



』 ‖ {

图llˉl03对应的左括号

我们选择将两个括号之间的内容复制出来’粘贴到控制台’如图llˉl04所示° ■心……

《}

]》【+‖◆『]◆ 【十【》】l ◆ (【!‖]◆‖『〗‖◆[]l ◆ (‖[J ◆ ‖]》!◆l◆[l] ◆ 〔! l』 ◆ ‖{)[!+(j ◇ ↑+‖】I ◇《[!〔】] ◇ 『川 [!

』】[+‖G〔l◆ [◆{]】】 中 ‖[】[《!"◆ ‖』》[+l】l ◇ 《1[] ◇ I]M!◆l] ◆ !十[】】 ◆ (l《J◆ I‖)l◆!+[1‖ ◆ 『】↑‖』 ◆ []){◆『〗』』 ◆ {〗)‖【◆[』 ◆ !◆[〗 ◆ !◆l】‖ ◆ {川] ◇ 【】》[』◆ 【l◆ !◆【]◆ !◆『l‖](》【◆‖+[] + 1·{ll〗) + ‖]》『△〗◆[〗]) ◆{{】 ◆ [』)【(‖l] ◆ !l)『◇"l ◆《! ‖{] + l]‖《! ‖『 小 [])[◇‖}l ◆(!【]+{】》[‖◆『l ◆ 』◇【]』 + (!‖】 ◆ [])

(◇!◆l〗]◆《0‖l! ◇ ‖]][◆[]】l》[0!◆I} ◇ 『个[】]] ◆([】l 【I

] ◆ ‖》)【+!◆[『】◆(』『[] ◇ [〗)1◇[‖‖ 十 ([‖[(! ‖] ◆ |】》(◇[】1+ 〔‖【]+ 『』》〔!+《‖◆‖◆[}l + (!【] + l〗》‖+』◆{〗] ◇ (『|[〗 ◆ ‖‖》[◆[』]】 ◇ ‖〗》!l+[』+!◇[l ◆ !◆‖』‖

『l{‖}{ $ }{}{↓}{!训{{↑!↓]{↑|{!↓}|?{』}{『!↓「|{↑|↓l!↑》{↓}![『{!!′}↑!『}|↓↑!}}↓}{|『|{!!↓『}|!|}蹦}|;|}↑}{|{『!{}↓{余↑|{|{}}t}↓}↑↓籍}!↓!『{|{↓}!↑"

… 】

图llˉl04复制代码到控制台

那么剩下的代码是做什么的呢?我们把刚才复制出来的代码从原来的代码里面删除’然后再把剩 下的代码粘贴到控制台,如图llˉl05所示。

■』■∏」□可|」■】|』■]■引‖‖■■]|□□」■‖□=■]·■■■‖』■】】』■■

我们又看到熟悉的代码了’其类型就是—个字符串,这时候就已经成功了一半°

■■】■可■■■|■■■■‖』■]』■可〗■】■∏」】■

(》

·】;咽【乙g·6〗6

」■■||‖|■■■可■■■〗■Ⅵ‖』』■■■司

《卿懒m…

■●■

阳…■

×本





辆呻

幻● 银田

…J ≈





ll』0特殊混淆案例的还原

489

p





牢; ×

谭田函…门m………≈…≈酝…≈呵=叼≈■ _■

心…, ■№…

v●『≈

回@m



‖ ◆‖】》『◆0呛[》】 ◇ (!【]◆‖〗》[!+[I ◆ 2◆[I ◇ !◆0』』◆(!! l〗 ◆ |)l[◆[I]◆(0ll『◆"M◆!十[]‖ ◆ (『』{ ‖]

] ◇ 1])【◆『lI÷《‖I[〔『[‖ ◆《〕)‖◆j)l ◆ 『f[】 ◇ []〗『!◆[】 + ‖◆【』‖ ◇ 《0[l ◆ 『〗》『◆!◆l]] ◆ (『! ‖] ◇ ‖】)『◆『l]l◆『‖}[!+『‖ · 】◆[} ◆ ‖÷[]] ◆ (『)l】 ◇ [〗》‖◆‖』] ◆

(』!l』 ◆ 【‖(‖?‖】 ◆ ‖』}『◆〔J】 ◆ ( ‖『』 ◇ [l)[!◆‖〗 ◇ 0十【‖] ◆ (i0‖◆『》》l◇『+l〗‖ ◇ 《q‘【》◆ 『】』[◆〔]]J》【◇』巾‖〗 + !◆〔‖』]◆《‖‖{]+[‖》l◆‖◇‖】])《{!〗〔〗 ◆ [)》‖◇《◇ [》]◆ (!【‖] ◇ {]》『!◆‖』 ◆ !0『]十 l0(】〗◆(M[‖ ◆ 『I川寸ll!◆ 『【]【 {]

] ▲ ‖1》『0〔]》+(g!〔l◆[])[◆!◆[〕] ◆ ‖ !』『 『]

] +【〗〗{◇!◆【]』 ·《◇【M』』 ◆ [l‖(![] ◆ [1》[◆(】l ·《01J o ‖〗川0午【】 心 ↑◆[]』 ◇ 《l【』 ◆【』》[◇!◇‖J』◆ (〗』[『 ◆ !〗)‖◇‖』!』』〔◇g·‖l ◆[◆!守【}】〗 ◆ (! !『]◆ [】》〔‖◇[〗 宁 0十〔] ◆ 』十{‖】 ◇ (+『!+[‖ 々 !O[‖ ◆ !◆{l ◆ [+!◆『l]}》l《! !!] ◆ {l )【O‖】〗十(!![l ◇ 』》『【![】 十 |】〗{◆[】〗◆(‖‖》 ◇ 『‖》[!◆l』 ◇ !0l]} ◆ 『 !『l ◇ ‖】》!+!+[‖『 + (§! 【l 十 【])【◇{‖I】}[O!仑[]十 【十【》l』 中 ({】 ◇ {lM【[][《0[1 +【〕』{十l]〗 ◇(!‖l◆ ‖1)(‖◆‖l◆ 『◆‖]0 ◆ (‖{l 十 I]》‖◆!◇‖l‖ ◆{‖』Ⅱ]卞 l]》1仆『I]] ◇ ‖]》{l厂『】 ◇ !◆Il ◆ !◆‖]l + 《『‖『‖ ◇【〗[〔0『} +[〗j[◆[)]→{p!】 ◇ [↑》[0◆【l ◆ 『0[‖ ] ◇ 〔!I]+ ‖l}《◇0◆【]‖ ◆ (0![1牛 [l)【◆{]l〗》{◆‖◆{l◆[◆[】』‖ ◇(「M 『)

〗 ◇ {〗]{·!◇[lJ ◇ ‖![‖ ← 『〗》【!+[】 ◇ l◇[】 寸 ‖◆【〗】 ◆ (卯‖】 ◆ {])‖◆『】〗◆(!![‖ ◇ 【】M◆‖◆【‖】 ◆ ([】‖ !]

{′;(}l!}『{|}!↑||』!}{|}↓!;】|{|}↑}W|{{{{≡↑!|』|}↓↑{}$ {↑{||令令!{{|!』t}}}|!令}↑}{}${{{‖『j}}‖!↑!}{|f|↓}}}{伞k||↑!{↑』↓↑{{f』{↓|丁l}}{}}!令|]M龄[川. [〗 l + ‖』》l◆!◆『]]+ ( ![] 宁 ‖‖)i◆‖◆『】l ◆『〔◆『‖]【([]‖(![】 ◆ !〗》{◆!〗】 守 (〗[! ◆ !l》Ⅱl◆[1◆ ‖◆‖‖‖ ◇〔![】◆ 【』‖ !◆!◆『]]+ (:l‖】 ◇[〗0『◇[‖】』 ◆ ‖】)【§◇l] ◆ 9◆『] + !◆‖〗] ◆ (! !!] ◆ ll‖《!【] ◆ [I)[十[I‖ ◆ (!【l ◆【]》[‖◆[】◆ 【◆‖l】 ◆《!【]寸[】〗‖◆!◆『11 Q (Ml】 ◇ 【‖)i◆l}】}》[◆!◆!] Q [十『]】] ◆《1M l}

〗 →‖】)!●§◆[]] ◇ (0【‖ ◇ [l》{!心[』 十0◆‖l+!◆‖ll◆(!‖[】 + ‖l)‖◆‖】】◆ 《00‖I ◇ ‖‖)‖◇!◇[‖l ◆Ml[ !〗

}』↑!|』↓(『}{|j|(↓|}{{}』|『』|{}}『}{|,嗡|{}}!譬}』|↓|{』『}{${↑{{〕′!{{{↓』$』{{}↓·{『}}{;}{‖}↓|『|{{腮{{!礁镣[↓}}}}嗡↓|(『』{『!1『}撇↓′『|{』↓{{』「《{}』}『!↓

[+!◆【]】》 ◇ (!!『‖ ◇i』)〔!斗l] ● 『◆‖] ◆ !+[】]]〗《0◆[』 + !牛[] ◆ !+『〗◆ [!◆{] + 』0[ll)+(‖[‖ ◆ 《]M÷!+[l]◆‖!ll◆『】)l『十[‖ + !仆【‖]川』‖》 ·1邑";〗6o【虽咕呻屯「】∩国

图llˉ105控制台

可以看到是u∩de+j∩ed’相当于执行成功了,但是没有返回结果°

这时候我们又会想起前面的思路,返回值u∩de十i∩ed说明返回结果为空’而当前代码最后也带了

一个括号’代表执行当前方法’但刚才我们已经把方法的参数(也就是字符串)都已经删除了’也就 相当于没有传参数调用’返回值为u∩de千j∩ed也是有可能的。 既然是方法,那么我们可以尝试得到其方法本身试试°试着去掉最后的_对小括号,重新在控制 台运行’如图llˉl06所示°

屋田唾…二="…≈■≡u…镑;酷■

圈咎■.呛..



扫…●!.■户0≡=亏

■』●.≈

. ˉ .…..严.. ... . .. .户!.咯

』◆[‖》【十‖◆【】】 丰《0‖l ◆[}》『』◆[]◆ 0◇【】◆ !◇[‖〗◆‖!![】+『〗》{◆【』! ◆《0‖『】◆l‖0『◆!◆{』] ◆《!]【

. · °

ˉ.

8中

〔〗

{!;〔{‖』‖『{}{′『』!』‖}{{{」!『!}雕{{‖蕾翻|{‖{!嗜『‖!』|删{$}『!}|·嗡!{{{↓0;‖{}‖{.}↑!{}t{{{{l`『!{}腮{{|·参‖!{{}{雪』』|副『《!↑{}搬『↑|{}}}{!『{‖{}↑}赃 {〗1个《0【【〗◆『l)[!◆〖】仆 !0!』◆ !◆【』〗·《!ⅦI◇ [〗》‖O[]‖◆(!l【 [】

]◆‖】》【◆‖】l◆0!§[l◆【!川◆】◆‖〕‖▲《『】[ !】

{』『:}棚撇』!}硼{!』|{}!!』撇{{|{}{↓『{删};『{|》』!』{{糊!蹦『}!}{;{{{』』}{『{删{!{{|栅{渊{测!}『!!{|删 ‖◇『】】◇0胆【〗◆[‖【『g‖】◆{〗】‖◇【】〗◇(!‖〗◇‖lM!+『!◇ ‖◇"‖ ◆《‖『『·【『》‖◇!凸【』】 ◇《0![】◇【】〕『◆‖〗】〗〗0宁!◇‖』+j◇『Ⅷ◆(『】[ 【〗

I◆《lM◆!◆【】l0《0【】◇【0》[‖凸0◆!◇0〗◆且◇‖l‖ ◆《!![】 ◆『〗》[◆【】】守《!‖[】◆!〗ⅦQ!◆[]』 ◆ 『[l[ l』

{0;!{l{‖『{{{|『!‖↓‖}}{}』!『!!』》{↑}{|镭栅}露『|!!》删{$}『}{‖熟`{{{』!;〖{{》↓喻}『!{{$删l!j|{}{蛾{|徽铲!{{{{{4!《瞄』↓t』{腮『『‖{}}}{‖镰』"M". 『】 』『令{{{!$』『!!{』3!}{}《;『{』l!『i『{!{』『!!洲!↓{}}{{}!!↑』|{』『{{|譬僻《{}{‖漆蹦!{{}!;k{}].除!『{{{!影嗡(}{{‖$!}{}{『娥{蠢删{』·嘲!{}!『"‖!·"M“"…" l] 1◆【】》『+P◆【〗]◆《![】◇ 《‖]‖!◆"◆ ‖◆【【◆!◆l】‖ ●《‖‖[】+[〗》!仆【』〗p《!【【‖◆【』》『◆!◆『】』◆《[]《 !〗

{『↑!{〕↓‖↑{}{』b`!}}{{}↓!『』『!》{↑{{』静·‖{;{{令『|!』‖{』『{}${『}W《{{}!』芭!{}|雕!{{$删l‖『|{}}蝴{』.伞‖↓}懈!|↑!}『|↓↑』耀!(『』{』l』{`『『剿『!!』. 【+!◆〖〗0〗◆《M‖〗o【]》《】◆【】◆‖+『】 令 !◆i]l]M!◆l]◆!◇【] 0 ‖◆[l◇【‖◆i】◇ !◆』】〗〗0《0!〗◆‖l0『O】O〔‖l ◆《川】◇【l‖【〗◇!】+ l◆『]】M0 Ql:曲8Ⅶ.》刀f…【『》『f咱t』归c呻′》

图1lˉl06去掉最后的一对小括号,重新在控制台运行

这时候就可以看到其运行结果了°这是一个eγa1方法’是JavaSc∏pt中定义的原生方法’传人_ 段JavaScnpt字符串,利用eγa1就可以执行了° 比如: eγa1("〔o∩5o1e。1og(‖he11owor1d』)卿);

的执行结果就是: ∩e11OWOI1d

所以,第一部分的运行结果就是字符串’把它传给eVa1方法’自然就可以执行对应的逻辑了。 这样’JSFuck这种特殊混淆的神秘面纱也被我们揭开了°



『‖■】』

| 虫 爬 向 逆

β

·∏



γ





^·







‖ ]



‖ 斗’

U

4.总结



本节讲解了一些特殊混淆的还原方案’通过观察得到的规律,配合一定的JavaScript基础知识’ 问题便会迎刃而解°这些分析过程需要具备一定的JavaSc∏pt基础和经验,多加练习,以后再有类似 的案例我们也可以举一反三了°

↑↑.|↑ Web∧ssemb|y案例分析和爬取实战 WebAssembly是一种可以使用非JavaScript编程语言编写代码并且能在测览器上运行的技术 方案。

前面我们也简单介绍过了’借助Emscnpten编译工具,我们能将C/C++文件转成wasm格式的文 这样做的好处如下°

□一些核心逻辑(比如APl参数的加密逻辑)使用C/C++实现’这样这些逻辑就可以“隐藏” 在编译生成的wasm文件中,其逆向难度比JavaScnpt更大° □一些逻辑是基于C/Oˉ+编写的,有更高的执行效率’这使得以各种语言编写的代码都可以以 接近原生的速度在Web中运行°

技术常见的呈现形式’即原生代码被编译成了wasm后缀的文件’JavaScript通过调用wasm文件得到 对应的计算结果’然后配合其他JavaSc前pt代码实现页面数据的加载和页面的喧染° 本节中,我们就来通过_个集成WebAssembly的案例网站来认识下WebAssembly,并通过简易的 模拟技术来实现网站的爬取°

↑.案例介绍

下面我们来看—个案例,网址是https://spal4scrapecenter/,这个网站表面上和之前非常类似,但 是实际上其API的加密参数是通过WebAssembly实现的°

…。—

…业ˉ…一…

一_…_

~……—…

□硒



………

碾●…‖

田·晒

首先,我们还是像之前一样,加载首页,然后通过Network面板分析Ajax请求,如图llˉl07所示°

. j]{…



□04m……◎…≈ ↑…吨

‖‖咖硒

↑顶

‖…吨

∑一



F

■■

节b

』{ —≡

—即T_

=_=

x…U……Ⅳ……c…净



ˉ-

_一.~.



O…码… 吨7t户↑幽↑…↑理

……∩ttpS自//占“M.SC了●佐°〔印te「/己p〃mv1e/?umt=1…「7S●t…jg庐540…8

「!■巾弛↑哩?…↑≡

……6[丁



目蕊;←r′翌鳖驾鹏…… 枷?忙↑匪]…7唾

……■t厂1Ct=O厂1g1∏额e∩=C厂o55=o「1g1∏

沪枷?贮0哩↑…Ⅵ9

!,≈…■·…m `"·…m·

{瞬寄瞬魁′… :

c■…鸭戊ee…um

{……尘…27‘5 ! …m诽…印puc·t1o∩/j5O∩ 图llˉl07

Network面板

纠·】■】‖』‖』■■】■■‖||』■Ⅲ■■|■|乙■■日一■』‖】』叼』□‖{|日」■■|二■■]‖■■■■可‖|■■■■●■■■■】‖』■■‖|■□‖|■■可■■■·Ⅶ■·二■】■可|{」二■】■]■】‖■]■■■■■四■】|‖■‖

对于这种类型的网站,—般我们会看到网站会加载一些wasm后缀的文件,这就是WebAssembly

日】■■■·二■』■习」】■Ⅲ■■】□■‖』■■】■■

件’JavaSc门pt可以直接调用该文件执行其中的方法°

■■■日□Ⅱ▲凸『卜‖|‖世=■厂■■『





ll.l] WebAssemb|y案例分析和爬取实战

▲尸「

▲ ■ 厂

可以看到`这里就找到了第_页数据的Ajax请求°和之前的案例类似’1jmt` o仟set参数用来 控制分页’ 51g∩参数用来做校验,它的值是一个数字。通过观察后面几页的内容’我们发现51g∩的 值一直在变化°

卜∩} ‖ |

p

49l

因此,这里的关键就在于找到5jg∩值的生成逻辑’我们再模拟请求即可° 接下来,我们就进行一下逆向,先看看这个参数生成的逻辑在哪里吧。

这里我们还是设置—个Ajax断点’在Sources面板的XHR/fetchBreakpolnts这里添加一个断点’ 内容为/aP1/"ov1e’就是在请求加载数据的时候进人断点,如图llˉl08所示° 沁@

『〖磕§ ! 簿· 坠D

相乓



尸′Watch

l

抄Ca‖Stac促 『尸

◆Sco″

■尸‖|

》s蹿a|〈po}∏tS

【■民■厉‖■■■■『

vx"w↑etChB『Ea洪p◎{∏ts



B馆akW"α](』∩LoO∩↑a獭s ≡→■一~-=■■■—一→-≡

二三二二二二苫=二=回

}妇pγ『γ℃v℃ ~=~-≡

=些…琶一军了~…..

…→







■■)■『队匹尸『■尸’‖′~∩■‖卜β卜 ■■■∩『卜Ⅱ■

仍DOMB『●已№◎j们↑S 炉G|oba{uSte门e『S

●曰颧扰uSte"e『S『ea№°枷ts

图llˉl08添加断点

接下来’重新刷新页面’可以看到页面执行到断点的位置后停了下来’如图llˉl09所示。



;峰鳞—



′惧■斜

ˉ护G

。…螺嫂

,

斟萝赣了ˉ`

×瞬磊.

◆, ′ ;Ⅳ茧固Sc『m·|№响

…:瞬 ″…霓

泌』 ☆★●;

●apa↑4巳cmp●哮c颜le「



翻 s馆馏腮e ■_

■ 「 ■ β 广 ■ 「 ■ 「 ‖ ■

{ ↑↑ ■_—

赣田勘咖■咙习

c呻a·知 Sα剿…判臼…k Pm饱『洒啤啮咐哟,…『 i咖…呻 ! 鸥或闷噶樊划嚼`o娇秘…m仅 °』认喇w■咖恋。簿』磷呻柯wmt“x 乃 于■=—≡转曲千

p唾…Ⅶ



审筐呻 v△…γ冰露….…w

膀馋… 抄瓣勉γ帖

卜燃m渺 ,*p

▲β■

|斟`麓(…射翻辩= ’·″γ‖



‘i蛔~≡

『} 4酗, 42粕i



魄鞠5 唾喇s

; i瓤!

滥2

趣毗^§ ? ◆.糙翰 订tf脚÷〃50凸辨.$t了…龄“仍te广/$p〃助订Le7 狱f脚÷〃50凸辨.$t7…龄“仍te广/瓣〃助订Lep

gx·警∩萨Yγ舞…≈p°愉smpe 』d□獭龋雨迫FpOu巳E罕膛 ` 』

已…

马拍潞`

}cmch《O》《

17 (世j■·刺● 』…e◇r沮■“佃$G丁y泌》

Q…

t枷硒0



愿鞭4渺13

■匹°疽·1

啡U"mm硒丙=w障@√份.…1…7鲍呵■曙d·m懈v臼Vt鸣, bc8 ′{」

q蓖81刮·

■↑mc化…皿=则

q酌【S·

●·〔蚀c·l…“

q酗1γ

|【 |

瓣耀;峨《′{ ,:!瓣麓……………,.… ◆h: 《肛它印t; ■■印u尼诫m佃/y■…′ t醉t/pm1"p▲′研}

d骚《□°…撇

』沁〗B.

:《g》抛i『嗡潍~《″……』勘…獭《 ′鹰;…唾; ′′…′』 『…; ′》

d卫G1p

》 》》0

幻0R4 q2酗如

′凝唾≡=一—畔,瀑蝗幽÷凶驰弓戳旦…≡←≡琵=m 》

仰0蛔■?皿凹 ◆tt /〔』

面==一≡……=…睁|

|鳃;』 ‖瓣鹏

鳃·】γ《

|}

『 QzO聪《

q】0鹤『

}‘

嘘03d



u:酗1I

◆〔`O$u定《e口●m◎穴惫》



》[`O$U忙(b巴0o)

■驼3: ↑tmct1锄《eβ t》《 eqG又po咸凸■『0

》Gl吨ol

·::删…

b“■; 7晌ct』酗(■0 t° ∩》《

哟【m…∏鳞沁慰……"

vBr』R冈{尸勋7伯■)

一~

t恤Z: 凹E四匀↑m■‘ v巳“u

]》

}蕊卜鳃《潍黔《恐} · 4泌疆

* ! ×



q

.呵●7』…碱=飞洒7ˉ5雷言雨硕`(ˉ`……t.k…o……`Ⅺ叔窜…. _ =.—…..≡.≡…-.

』 Q酿11

= 0沁】】

4h儡,膊~

『‖β》『仿 司·





↓!白』tmo…帆a岭凸(d…减…飞』.`…O》』 』!血t试”匈愈皿`s“《刨刨赎…也雌. 』,》′ ‘酱》…愈 嗡输翻鳃磷 `…]……咱匠蟹蝉醒5q叫·O… `…』……咱‘鸳蝉蝉:剿°‘勤… ·m留^铲v,·》 ′v…

aⅡ锄6 aⅡ硼6

! a潞瓣;

p



罕~■』抒▲■■…i

u心h20邀c鳃岛m泪

.

"1呻





◆……£ `



嚼啊酶;↑0忿 心∩W哩……=≡

图llˉl09在断点处停止 图llˉl09



=■■|■

第ll章JavaScript逆向爬虫

492



o∩「etch0ata方法里面,如图llˉll0所示。



】ˉˉ=-

胜硷敷霓宁{嚷劈=… opⅨ…mm府“怕↑吨



h屯tp占员〃5陋观·$c丁ape·“砸宫F/甲〃盯oγ1e7 飞jm丈赵…ffSet=…轴‖】锚‖…“

尸■

←润≈…唾勺亏←←≡p~≈·■,…□■≡■°P■勺知壶■°.口■…‖■

睁. ^

>′….ˉ .

々□·□一甲■~字■■■■■■





卜 贴

T■p■

ˉ§





己』·尸

" 翻也 ! . .。

■锤脂峰执 乙

‖‖】

这里我们还是通过CallStack找到枚 Stack找到构造逻辑。经过简单的查找和推测,可以判断逻辑的人口在

L

=ˉ→唾…←首止



c测m奸γ宙滋◎7U瞬◎…αγVm…鸡2o痊S

e诊GxpO陋

c↑]m水叫αmα已c...◎『ma…;组髓8

e°ex四↑B

‖』|』

●(即◎『Ⅳ∏℃鸣

c撤mk■γ镊》鲍揖ˉe…沁∏γ?a灶●d『驼?6

…刘…°创蹿∩《…叫



. ˉ…

a『囤l…t

创`u吨〃■沮田Ec…:fαW】attm【366

Ⅸ↑御…ha<印『『)put函>

c柯O『淤咆】…ac,.△:脑『γmt柏◎:3跑

(囱`O∩ymou刨

cbl贮抵ˉv钡Ⅷ刨aC…扣∏γmlm?鳃9

` °同P墩枷m』撬

…匈t……胸:t

(am∩γ『∏α匈

Chu呻吧7】4….69o@钢窟↑)怠;『 刨"』油c7‖▲3c鲍。田唾b卸.鹏;!

S

(即o∩Ⅵ了muS)

chu毗芍7143c吨馏6…bfs↑ˉ{$;?

佃℃‖Ty『m呵

凶Ⅶ毗ˉ田↑勾3c或。69钾b妇[惦2‖

c

C恤吨冶7‖4“鲍铲69“b泊↑.鹏{‖

a

chM毗E7‖qSc酶。69“b跑f.隅身t

图llˉll0找到逻辑人口

点击o∩「etc∩0日ta方法’找到方法所在的JavaScript代码位置’如图llˉlll所示。 ….;嘛 “…建…擎``……p醚唾…颧$“簿哮℃ ≡-吞≡…~←砷…占^←………………罕………→一=≡.=ˉ≡≈…恕儒……. _…锣封蹄

鸥…翻……』 ;`喇瓣酿…僻.寸£…獭

←…铲……=」…ˉ≈ˉ、…~…γ…丽~≡=_—≡ …′ 田于…8{ 2”1 2岭2

}’

R…·

』)‘ th1■·o∏尾tChDat■《)

u■止t』 tMS·umt0 o抒3●t: ∩0

■蛇间吕 e

2】“『

γ■厂e■∩·d■ta

2I帕〗

〃 r哩e·厂■Zu1t5

2110『

p 』=··co‖nⅦ;

$om■mg渔 t□mvje巴庄「0!】0 )

211q

乙1屿 2116



2皿7; r1酶

司0■勺



Rpt◎它■1左1



》 0 ‖五I ‖刁 ,p p翼 p≈(e《码c■9C鳞)0

■『』0■●■■ ≈『』0■,写″`

图llˉlll

·

α汕宙ˉ……队宫…臼『啦啊合冲匀向赣

●o…泣

曰醚山镭吨摊α‘α『γ滩nm:$?蜘

……

………感…!峨…z‘…

渝……》铜 ■Ⅲ】wγ…nc←,,涸∏b乙?0■巴…

口浊好咖.γ钳…已律…;↑nm鲤0凸分wz

(■m晌℃哟

C们咖泳铃γ凹耐钢n℃` .沁0wm↑”$鹤

…e出∩钞凸

创N砷心了!…°·。!α7了】戳?mj??↑O]

(…Ⅶ硒叼

c才n打Ⅶ咯7《……‖颧W0蟹0皿0夕叫叫

S

))

=.……

◆佃m加V趣倒

a……

》).t№∩《《Ⅷ∩怎t1O你《们》《

R1飘々 【1亚 2】Ⅱ3

. 辑

【赋怎颧冯.<写◎∏〗pmm>



互w!

21鹏;

m16



恤屿麓灌?忿铲k蹦瓣……….…业…,《

m03j

.O了》x↑儡御…]

◆恍鲤E

p●右恤加.…臼■.■m、知c『γpt(∩β pa了$■】∩↑《m仓h.ro0B0〗0《《∩翻m记

21懈: Ⅺ1髓:

o内

吁F鲤≈■…

黑,摆翻恿?蹋:ˉⅡ)·……

翻| 2〗呢》

●←■…

献tp导【〃■碑鳃.9c獭pe·〔e仰t■〃apn■ov1e7 u血X尤酌n“臼?7盛m■瓣湿绚赔5△鞘·■s“ 、 辱 · 、…`=

”丁t习$h屿;

2…! 2硼9

221g

』=尸…

m“tc咖ta$ 7【mct义o∩(〗{

2鳃70

』▲2 ;* § ×

ˉˉ ·。

. ˉ



颧0 】pb^? ↑啡·拎@

创u爪贞必7γ4……′切γt`3t↑m县滤鳃

(刨℃顾Ⅵ◎u8)

c↑啦№们迢70▲强凶.P.允∏】逾t甄:?瓣↑

(mmVmOuS》

α酗毗噬70……ˉ脑γ∏唾t鲍:↑2鹤

@

cfⅦmⅨˉ67?玲…、°.′owγ憾1m:Y9髓



笛7酗戏户6了‖q…嚼°.赋VⅧ蜒t野召…

o∩「et〔h0at己方法的定义

和之前的案例类似, paIaⅧ5的参数有三个—11Ⅷit` o仟5et、5ig∩,这和Ajax请求_致。 提示当然’为了确保是一致的,你可以继续添力口断点进一步验证’这里不再赘述了。

γar∩= (tbi5。P日geˉ 1)*t∩iS.1jmt

’ e=t∩j5°$|∧|日5"。35们。e∩crypt(∩′ par5eI∩t(‖ath.ro0∩d((∩ew0ate)。get「j们e() /1e3)。to5trj∩g()))j

可以看到’它通过调用thjS.$"a5".35"对象的e∩〔rypt方法传人了∩和_个时间戳构造出来了°



■■■■`■勺

这里关键的参数就是51g∩了,可以看到它的值是用变量e表示的,而e的生成代码就在上面,如下:

■=■』■■‖■■■■|」■■]■■■』』】』■】||‖』■■‖■■习·■司』|』■】■]■■|■】■■|

2OgB!

。.

p己ge0 t



2岭3 】·p啡}

毯』 ˉ

叫 · 司



‖」□



WebAssembly案例分析和爬取实战

llll

493

接下来,我们进-步在此处调试_下’在2l00行添加断点,如图llˉll2所示°





20g0

p己「aⅦ5: {

2091 2092



page; t

})’

乙093

‖)‖

th1S.O"「etC向D已t已(》

2094 2095

}p

2096

O∩「etChDat日8 fO∩〔t1O∩()《 γ日『t=tM5〗

2097 20g8 2099

th15·1Oad』∩g= !0; γa「∏= 《th1S.page= .p己ge= 1)*th15·1m1t

■和己S肌口巳s■·@e∏c厂ypt《∩UDpa虑eI吭("已th.愚『



et《th15.$5tO「e.5tate°u「1.1∩dexp { tms·$ax1o5·9et《th15.$5to厂e°5tate°|

2101 2101|

pa「a懈; t∩is.umt′ o什5et: ∩′

■■厂|‖伊

耀| 21Oq}

51g"8 e

2105{

}).t《e"(|铡"…("){

瓣|

β

β■‖■β

图llˉll2在2l00行添加断点

重新刷新页面,可以发现页面运行到该断点的位置并停下来了’ 如图llˉll3所示。

■尸『■尸

}-_ˉ=… ÷钉G



一~≡--→

醚★ 醚★

●…0《″m●″●∩tw

力●§



巨…

△ ■ 厂

L慧

□「厂|卜∩ 门

▲2

…y……



_……ˉ



…………………………侧酗…………………

→…·』、

_≯…



…]…

=_一



…一……

■■尸■■「|▲■▲■尸·凸尸■■■『●尸

啼…●



§

×

-=

蜘H巾UγT…=镶悼

助凶″忿7M……几…其乃

聪沁°^0 了←产●

| …【■…中■

”厂≈8 { 》

…】■

淹α吁O:°mcM) 》

●→=←

『=

}· { 幻… l



怕≡

c



|■■厂‖■■厂

[ ;m…m…叫 【6■↑? tmβ·l…Tˉ

o抒…T∩p ●蛔8c 》

}》□t榔《W哪cM蛔《∩)《





户窍m』≡′■._… …魏呜T04→…一 ≈…



咖兰雪!g≡…一 ……咆7V丹学…≡…▲

←●. ←= …■…牛汕→| .一



~=幻γ凹= 一

!



虫=≡广~=m





龚毯雪ˉ→—户≡ 一

■·…蛔■7O tctO【●`■1

~=U『?~………





t.b;‘蜀.臣嚼;

β

…哑0…. .·~0宜

…w=m.

=←=徊γ唾’..~…

0

·「冉●·…1t■

…四……~■

@

嚼…

吧7G巾∏■■■◆■

空…0=…… = 钢四

b

■β「





■伊

}严

去兰 零←■’纠一

……△.一2▲



恤■【∏|

图llˉll3页面运行到该断点的位置并停下来

∩■■尸‖■■



这相当于JavaSc∏pt上下文处于o∩「etc∩0ata方法内部’所以现在我们可以访问方法内部的所有 变量’比如thj5` t‖j5.$Wa5们等°

接下来’我们就在Watch面板中添加一个变量t‖j5.$wa5‖’先看看它是什么对象’如图llˉll4

》卜卜

所示。

》|》}》【『【◆■【‖协|》

可以看到,这个t‖15.$W日5"对象里面又定义了很多对象和方法,其中就包括了a5们对象°因为代 码中又调用了a5∏对象的e∩〔rypt来产生5jg∩’所以我们进一步看看a5"对象` e∩〔Iγpt方法都是什



】··

第ll章JavaScript逆向爬虫

494



么°将图llˉll4中的a5"对象直接展开即可’如图l1ˉll5所示° 0>^?

? ◆.





泌o



■|

op■umdo【T恤mhp呻『t ■-

+◎

vW日↑c∩

■■■‖□∏[「|

◆th』眺己

◎: ≤"□t·γ已X1abIe≥

o孕己p∩W8 <∩ot■VaI1己D1e> o◎~w厂appe厂: ≤∩Qt巳γ巳naD1e>



γ∏自 <"otaVa1I■ble≥ vaS朋:

th1s°$w凸$励: objec↑

h『

p"趴P8吕 I冗t弘厂厂w{16777216)『0’ 0‖ 0p 00 00 0’ 0’ 0β≡

尸"趴pP“: 「`OaT64A厂厂●γ《20W152》[0p 0· 0’ 0』 0’ 0′ 0南 ≥‖团pl』8;毗∩t8A厂7w《16777216){0’ 0′ 0』 0p 0J 0’ 0′ = 仆"〔A刚162 Umt16Ar「eγ《6388608》[0l 0P 00 0’ 0p 0G 0o= ◆‖趴Pu32; 0mr32A厂『aγ《q19q3咖)[0′ 0′ 0o 0〃 0’ 0o 0’审 ≥as阳: {Ⅱ…o印目№侧o厂y0-1∩d1「eCt十u∏〔t1◎"t己b1e; 丁a… caueo∩u"8 t厂ue

◆喧Cau目 厂L〔t′∩′e′厂‘工) ◆〔W厂日p; /0(t′∏′e′厂)

ve∩C叮ptg /q() a「g』』唾∩tS8 ∩uu

‖|‖

P‖[∧p16; 】∩t16A厂丁aγ《83“608){0’ 0p 0〃 0p 0’ 0’ 0′ 0→ p甘〔∧P32』【∩[3趴「rw〔A1g▲3酗)[0q 0, 0p 0′ 0‘ 0’ 00 0= ◆付趴P『32目 「I啤t3趴丁『ay(419q30q)[0’ 0′ 0′ 0, 0’ 00 0≈

Ca【1e『8 ∩ulI

le∩gt"8 2 ∩a阳e5

0gq00

》-p『QtO~; f〈〉 ′〔尸U∏ct1o∩LOC己t工O∏JJ 卜〔′5Cope5′′8 5COpeS[0|■ 卜肌e励oⅣg "臼∏o『γ{} pSt己〔次∧u◎C: f]『)

[.h7…;0m, |

■■Ⅵ■■■□■■■可||』●可■■司

卜5t己C代Re5to「e: ′2()

仍BtaCk5己γe: ′〕()

≥p厂e1O己ded∧u61◎5: {}

pp「elo“edInSge58 {}

》 1∏d1「eCt「O∩〔t1O∩t己ble$ 了ab1e{}

l

沪 」Mt1己1亚e日 f0〈)

供「e己dy: p厂呻止占e{<↑uW』11eO≥; {=}} pm∩自 f∧t《∩′

C己ue0Ru∩: t「ue

p〔c巳u: /L〈t′门′e′厂′1)

尸£t■c趴uoC8 f()

‖■叫■■

图ll-ll斗变量t∩15。$Wa5Ⅶ

图llˉll5展开己5肌对象

这时候我们可以看到a5川对象里面又包含了几个对象和方法,比较重要的就是e∩Crypt方法了’ 其中它的[[「u∩〔t1o∩Lo〔at1o∩]]指向了另外-个位置,名称是ab728922:0xd9。因为我们就是想知道 这个方法内部究竟是什么逻辑,所以直接点击进人,如图llˉll6所示° …







▲2 ■tM£°$m”g帅〕酝t

~ 《…匹IO 洱

p■缅℃

臃嫂髓慰嚼|·嚣蹦i龋斗蜘…."

,……z……|,.…,·,,.α

,触僻酶;师……|…o懒!|o.o』°, 。, °, o′ o』

q

沪》记A仔32; Vl凹t皿〃厂■γ『d】9Q…》『0Q 0D 0‘ 00 ·′ 0仑





卜‖旧Ap庐6q8 「1唾【郸「砷《】硼y淄〗》[O′ 00 ·p 0′0囱 0,

α

O叮……

p凶吨

■r≈●

》心门

■嘻=巴

(7qmc$2t$仁h∧】l碾(;1凸)(exp◎穴,!3t诬Ml[pc■》(p●「申$吧门1巫)《「eSul↑

鹤↑!撰感呼`o

←些↑

●Ⅲ钦1

·只味5

0∏酗γ

0唾b

■ ●E尘它

△■…

0月“7

a叮…tD】 ∩uM

七D〖l●矿:m』ll

《『山吨和@仁…to7●(;2!)〔mm「t伺巳t凸ch∩e【to「e凹)《仰『■曲●丁0132》 1●ml.gGt$γ■门

0l@b■l.$Gt约`呻●lO





16叼0∩〖 2

《↑unc粹蜘亡执S钉0G (;3〗》 ‖●xp●穴■■mc心■ve』↑)〔「eZultj32》

0mα3

0

0o■

、曙…; ′W′

lcml·teQ$v0厂·

{蹿腮厕滩酗(0 》

…)

(园



M0EApU8: 0mt以f7w↓】6777216》 【●U 00 0′·c 00 O凸● p0慑∧P0』6『 ‖JDPt1钞什w《820$幽》[0夕pD 00 0O0『 ■p ··~

●峰^Pu328MmM■厂r□v′Q1叫靶』『O0 0’00 0』·□0『

山2ˉm山

【:蕊儡:蹄…‘ ■≥p

×

u…

!?势.$=1nq鳞!岂!钙t」m鳃匹峨M塑p?盛阐马耐』…ˉ′…t1哑tob ’…8:m幽向y(〗‘γ′72m)〔00 0, ·0 o′ 0, 0, o』··=

{;蹬{颧瑟b《翻l蓟ˉ恿i瑟i『!墨滞)

◆肛…

伞 §

‖仁°^! γ…铃@

四α问口巾钉↑‖3…』↑懈℃而■烟山79…?2x乃

刊…8 ■q■ 卜-尸rQt尘; ′『》

「〔尸凹m』O门儿@c●t』mJ′:■D?Ⅲ89血B出dq

p【〔sc″●■′』Z Smpe■[0〕

9【om1回鳞t句{oO□I0

◆…「γ8…了γ《》

■■司‖|■■‖‖■■■■司|』■可||■■■■□·可|‖」‖‖■■‖{‖□■宅



咆…_…

Sα…0怕W饭k≈栖心m哟叫γ……m, l…m≈

p0r&CM〖loC《 ′』′》

二 (,耀鞠gp:Ll当‘山e硒穴.…yp《碱》《”r…v△′p渔2)(p…$,o『业芭{盂矗盂}°!7i{′

■尝二

·■哩白

四2。cm田t〕

》≈1∩1un\』Z0?厂0《』

』32.〔O0↑】t 1唾巫

卜山? ≈哩

匈u■诅』炯0 tme 0CC●11日 ′上「to∩70′厂′』)

1mQ■d △

…〃■

卜C■厂呻; /0『?′■′●o厂′ ◆D≈1o蛔…1O巳; {》

图llˉl l6展开e∩〔rypt方法

可以看到,我们进人了—个似乎不是JavaSc∏pt代码的位置’文件名称叫作己b728922。通过寿侧 的Page,可以看到它在wasm路径下,代码跳转的位置可以看到e∩cIypt字样’其代码定义如下: (+u∩〔$e∩〔rypt (i4j)(e)《port "e∩〔rypt嚼)(p己ra厕$γarOi32)(p己mⅦ$v己r1j32〉(re5u1t j32)

气咱■■】|‖」■■■‖|■■

L…27…Ⅶ0



●ˉ』』咆1「唾t↑…t』mLt曲le8 丁蜘胎{》

1墅二@1v6 坦p■4针

钞磐· ·冗“】

^=≈

,球。c…; ′3『′

烯}瞬;飘~

·v…



1oca1.get$γaro 1o〔日1°get$γ己r1 j32.CO∩5t〕 1〕2°djγS

j32°aαd

『=■■}

|| ll』l

WebAssembly案例分析和爬取实战

495

132.〔o∩5t16358

132·add ‖■尸||「□■■厂■■『‖‖『‖

如果你了解汇编语言的话,会发现这有点汇编语言的味道°

这其实就是wasm文件’这里面的逻辑其实原本是用C++编写的,通过Emscripten转化为wasm文 件,就成了现在的这个样子°

卜|β

这时候我们可以找下Network请求’搜索wasm后缀的文件,如图llˉll7所示° 压田贮砷血

◎α…Sαm…

N碱懈αh

≈呵∏m℃·

0出m。″

App℃■m叼厕硒m



■■■『|■■尸

●owα田衙…憾唾巴〖】m№°缸怕αⅧ陋

v





"酵…m…{p…叔■∩……■ ˉ≡蔼□}粒°s鲍恤屿m叫日烬cSS吨M°°妇F。′谜o。cWS贼“,"愈凰‖。"可· ■-

吵些T〗

___≡≡—

N扫『滩 甜

x…B ■

撕酬……咯●

0『蹦臼t顿

『Ⅵ喊吧

G…m



口『■β『●■■『‖■∏「「〖什休『匹广■尸尸广■■厂『‖》「■尸}■尸■且∩

fˉ『悟…

`《

‖啸°矗口厂=ˉ

口№酬w膨waa"》 —



‘…[m=∩ttp92//■四M·与cT■陛°〔e『VtQ了/jZ″■鳃.略5■

……比艳味丁 ≈→■户…●2铡啄 =鲤≡123◆129·骗·M9吕“〕

R……■t『1Gt屯厂』9m≈…∩七mS■←o『1g肛` ■…韩■

…T■■…

……bγ【“

c…℃Ⅷ…=●l』γe

……: 232 ◎■….…己pp\』鳃t1◎∩/Qct●t=$t「eO■

图llˉll7搜索wasm后缀的文件

可以看到,这里就有一个wasm后缀的文件’其逻辑就是刚才看到的内容° 到了这里’wasm代码已经完全看不懂了,接下来怎么做呢?

有两种办法,一种是直接把wasm文件进行反编译,还原成Gˉ+代码’此种方法上手难度大’需

Q

}‖

||

■■「卜|■■■「■■『|尸=尸『△■β『『|凸尸■■■厅【〗匡■‖|●■■「■尸

要了解WebAssembly和逆向相关的知识;另外_种就是通过模拟执行的方式来直接得到加密结果° 本节中’我们主要来了解第二种方案。拿到wasm文件’然后通过Python模拟执行的方式调用 wasm文件,模拟调用它的e∩crypt方法’传人对应的参数即可。 2.模拟执行

首先’我们把wasm文件下载下来’地址为https://spal4scrape.center0s/Wasmwasm’将其保存为 Wasmwasm文件。

要使用Python模拟执行wasm’可以使用两个Python库,_个叫作pywasm’另一个叫作 wasmerˉpython,前者使用更加简单,后者功能更为强大°我们使用任意-个库都可以完成wasm文件 的模拟’下面我们来分别予以介绍° ●pywasm

这个库比较简单’其主要功能就是加载一个wasm文件,然后用Python执行° 安装命令如下: pjp3i∩5t己11pyWa5"

安装完成之后’我们可以用如下代码来加载wasm文件: 1刚portpywa5‖

n」∩t加e≡pywa5肌1oad(! ./‖a5∏.was们0) pri∩t(rl」∩t1『∏e)



(」

第ll章JavaScript逆向爬虫

这里我们调用了pywasm的1oad方法’直接将wasm文件的路径传人’实现了wasm文件的读取’ 输出结果如下: 〈pyw日sⅧ.Ru∩tmeobjectat0x7十bd880efd1o〉

可以看到’返回结果就是_个pywa5川尺u∩t1"e类型的对象。 有了这个Ru∩t1旧e对象之后’我们就可以调用它的exec方法来模拟执行Wasm里面的方法°

比如,在网页中我们可以看到它执行了e∩crypt方法,并传人了两个参数。我们也来试-下,要 我们可以将代码改写如下: mportPγw己5川 ru∩t1Ⅷe=Pyw己5们.1oad(! ./‖a5们。"己sⅧ|) re5u1t=ru∩t1们e.exe〔(0e∩crypt‖’ [1’ 2]) prj∩t(re5u1t)

这里我们调用了exe〔方法,第-个参数就是要调用的wasm中的方法名,这里我们传人字符串



』·Ⅲ■‖■·《叮』■||■日{·』■

模拟调用wasm的方法,只需要调用Ru∩t1‖e对象的exec方法并传人对应的方法名和参数内容即可。

|」』■■‖|」■■勺〗‖‖|□■|■■〗‖‖|己■]■』】■■‖‖(‖`□■】‖□〗‖

496

e∩〔rypt,第二个参数是-个列表’代表e∩〔rypt方法所接收的参数’如果是两个,那么列表长度就是 2’参数和列表的元素__对应即可° 运行结果如下: 16359

调用成功了!

通过分析逻辑’我们知道传人的参数其实一个是O仟5et’_个是时间戳° 其中后者的实现是这样的:

Par5eI∩t(‖ath.rou∩d((∩e"Date)·get丁j∏e()/1e3).toStrj∩g())

这是JavaSc∏pt中的实现’我们将其输出到控制台’可以看到运行结果如图llˉll8所示

■可』■Ⅵ‖』|』·】■Ⅵ‖‖|■■●□■可」』‖||■】■司‖」■■■

成功输出了结果’但是这似乎并不是我们想要的,因为这里传人的参数其实是我们自定义的°要 真正模拟网站的Ajax请求’就要用网站里面的真实参数。

》 16吕43840°825pa「5eI∩t(‖己th.厂ou∩d((∩ewD己te).get丁」"e() /1e3).to5t厂1∩g()) <, 16叫3840·834162115q621

图llˉll8运行结果

1川poIttj∏e

i∩t(t1"巳tj爬())

最终,我们可以将爬虫逻辑实现,具体如下: i们portpywa5Ⅷ i们porttj『∏e 加portrequest5

|||||

输出的其实就是-个时间戳’结果是数值类型’位数是l0位°使用Python实现同样的结果’可 以这样写:

‖{□

〉 |

8A5[0R[= ‖http5吕//5pa14。5〔mpe·〔e∩ter| 丁0丁∧[pAC[=1O Iu∩tme=pywa5肌.1oad(!·/‖a5爪."己5叮) 十oIi1∩m∩ge(ˉ『0丁Al一p∧C[):

■=凸

ll.ll

WebAssembly案例分析和爬取实战

497

o仟5et=1*1O

51g∩=ru∩ti们e.exe〔(,e∩crypt‖’ [o仟白et’ i∩t(tme。ti|∏e())])

ur1=+!{8∧5[0【[}/api/Ⅷovie/?11mt≡1O&o仟set≡{o仟set}85ig门={51g∩}!

re5po∩5e≡Ieque5ts·get(ur1)

pIj∩t(re5po∩5巳j5o∩())

这里我们先定义了「0『∧[p∧C[是l0’就是l0页,然后开始_个十or循环遍历’1就是0~9的数 字, o仟5et就是0` l0` 20、…` 90, 51g∩就利用刚才的实现,将参数转化为o仟5et变量和时间戳’ 最后构造URL请求即∏I° 运行结果如下:

{0〔ou∩t‖ : 100’ 0Ie5u1t5! :[{01d: 1’ 0∩a爬! : 0霸王别姬|’ |己1ja5‖ : !「己rewe11付y〔o∩cubj∩e,’ 0〔over! ; 』http5;//po |‖eitu己∩.∩et/帅γje/ce4d己3eo3e655b5b88ed31b5〔d7896c千62472.jpg凹64w6“h1e1〔0 ’ ‖〔己tegorie5|: [‖剧悄』’ 』爱悄0 ]』 0pub115hedat‖ : !1993ˉo7ˉ260 ’ 』川j∩ute‖ : 171’ ‖5core! : 9.5’ ′regjo∩5! : [‖中国大陆0 ’ ‖中国杏港‖]}’ ●



p

{0id|: 10’ ‖∩a‖e| : 0狮于王|’ 0己1ja5|: !丁he[io∩Rj∩g0 ’ 0〔over0 :| |]ttps://pO.‖e1tu己∩.∩et/们ovie/ 27b76+e6c十3903f3d74963「70786O01e1438406.jpg0464w640h1e1〔! 」 !〔ategorje50 : [‖动画|」 0歌舞‖′ 0冒险` ]’ |pub1i5∩ed日t0 : 01995ˉO7ˉ150 ’ 0m∩ute0 : 89, 05〔ore! : 9.o’ !re81o∩5! : [‖类国0 ]}]}

可以看到,Ajax请求被成功模拟了!成功爬取到了结果° ●wasmerˉpython p



除了使用Pywasm库’我们还可以使用另—个库wasmerˉpython来完成同样的操作°相比pywasm’ wasmerˉPython的功能更为强大,它提供了更为底层的API。如果遇到更为复杂的wasm调用情形,推 荐使用wasmerˉpython°

要安装wasmerˉpython这个库’依然使用pjp3即可,命令如下: P1P3j∩5ta11Ⅳa5『‖erˉpγt∩o∩

要读取wasm文件’我们需要先声明_个5tore对象’然后将wasm对象转化为‖odu1e对象,再 将其转化为I∩Sta∏〔e对象’写法类似如下: 十ro‖w日5「∏erj呻orte∩gi∩e’ 5toIe’№dq1e’ I∩5ta∩〔e 十Io∏wa5ⅦerˉcoⅧpj1er〔m∩e1i代mport〔oⅧpj1eI

5tore=5tOre(e∩g1∩e.〕I『(〔O‖pj1er)) 们odu1e="odu1e(store’ ope∩〈Wa5".wa5Ⅶ|’ 』Ib|).read()) i∩5ta∩〔e=I∩5t己∩ce(『∏odu1e)

re5u1t=j∩5ta∩〔e.exPOrt5.e∩〔ryPt(1’ 2) prj∩t(re5u1t)

这里我们还是调用了e∩CIyPt方法并传人了1和2两个参数’运行结果如下: 16359

运行结果和刚才是一致的’这说明此时调用成功了。 关于更多API的法,用可以参考官方文档: https://wasme∏o.glthub.io/wasmerˉpython/ap‖wasmer/。

根据刚才的逻辑,我们再实现一下完整的爬取逻辑’代码如下: 1川portrequest5 1∏pOrtti爬 1川pOItWW35川

千ro∏wa5Ⅷerj‖porte∩g1∩e’ 5tore’№du1e’ I∩sta∩〔e 千roⅦw己5们erˉ〔o"pj1ercra∩e1j什1呻oIt〔o∏‖pj1er

5tore=5tore(e∩g1∩e.〕I「(〔o∏‖p11eI))

川od(」1e≡‖odu1e(5toIe’ ope∩(|‖a5∏.wa5川! ’ |rb!).read()) 1门5t日∩Ce≡I∩5t3∩〔e(川OdU1e)

8A5[0R[≡ 0∩ttp5://5pa145〔mpe。〔e∩ter0



] |

第ll章JavaScript逆向爬虫

498

『0丁AtpAC[=1O



O什5et≡j*1O

sig∩=1∩st3∩ce.e)《poIts.e∩〔rypt(o仟5et’ j∩t(t加e。tme())) ur1=+0{B∧5[0Rl}/ap1/『∏oγie/?1imt≡1O8o仟5et={o仟5et}&5jg∩≡{5jg∩}0

运行结果也-样’泣里不再列出。这里我们也成功使用wasmerˉpython库完成了wasm的模拟执 行’并成功爬取到了数据° 3.总结

本节中’我们了解了WebAssembly的基本概念并分析了_个WebAssembly的示例并用Python模 拟执行wasm文件实现了数据爬取°

↑↑.↑2 」aγaSc『|pt逆向技巧总结

总的来说, JavaScnpt逆向可以分为三大部分:寻找人口、调试分析和模拟执行。下面我们来分 □寻找入口:这是非常关键的-步,逆向在大部分情况下就是找一些加密参数到底是怎么来的,

比如_个请求中to代e∩、5jg∩等参数到底是在哪里构造的,这个关键逻辑可能写在某个关键的 方法里面或者隐藏在某个关键变量里面°一个网站加载了很多JavaScript文件’那么怎么从这 么多JavaScnpt代码里面找到关键的位置,那就是一个关键问题。这就是寻找人口° □调试分析:找到人口之后’比如说我们可以定位到某个参数可能是在某个方法里面执行的了’

那么里面的逻辑究竟是怎样的,里面调用了多少加密算法’经过了多少变量赋值和转换等’这

些我们需要先把整体思路搞清楚’以便于我们后面进行模拟调用或者逻辑改写。在这个过程中’ 我们主要借助于测览器的调试工具进行断点调试分析,或者借助于一些反混淆工具进行代码的 反混淆等°

□模拟执行:经过调试分析之后,我们差不多已经搞清楚整个逻辑了,但我们的最终目的还是写 爬虫,怎么爬到数据才是根本,因此这里就需要对整个加密过程进行逻辑复写或者模拟执行’

以把整个加密流程模拟出来,比如输人是_些已知变量’调用之后我们就可以拿到-些tO促e∩ 内容’再用这个to促e∩来进行数据爬取即可°

本节中’我们就来对以上内容进行梳理。 ↑.寻找入口

首先’我们来看下怎么寻找人口,其中包括查看请求、搜索参数、分析发起调用`断点`Hook等 操作’下面我们来分别介绍_下。



』】■‖■引|‖□□■∏‖』■∏」■■]』■』可盯■丫』■】·‖|」·■■】||]』■】·〗|·□』■■`|红■□。」|‖』■Ⅵ‖‖■Ⅵ〗‖■∏□|■■]』]|』■】』■口■Ⅵ』■■

别介绍。

q

二■Ⅵ■■■‖|‖」·】尸匹‖‖」·■■

本节中,我们就对前面的知识点做_个串联和总结,总结出JavaSc∏pt逆向过程中常用的一个流 程,这个流程适用于大多数JavaSc∏pt逆向过程°大家熟练运用之后,可以在不同情况下运用不同的 技巧来进行JavaSc∏pt逆向操作。



《‖‖

前面我们已经学习了不少JavaScript逆向相关的知识,包括测览器调试`Hook`AST、无限debugger 的绕过以及JavaSc面pt的模拟调用等,这些知识点都比较松散,有时候大家学完了可能觉得没有形成 _个知识体系’或者说没有_个常规“套路”来应对~些JavaScrjpt逆向的处理流程°



‖‖‖|‖|司

本节代码参见: https://github.com/Python3WebSpider/ScrapeSpal4。

旦■‖|‖|■■■

re5po∩Se≡reque5t5.get(l」r1〉 pri∩t(re5po∩5e.〕5o∩()〉

■Ⅵ·γ■■】‖‖

Iu∩tj‖e≡pywa5瓜1oad(0 ./‖a5川wa5∏!) +orii∩ra∩ge(丁O丁∧lp∧6[):

■■【■「●『‖|凸■「 ■【「‖|

ll.l2 JavaScript逆向技巧总结

499

■■|〖『|■■厂‖‖)■「|

●查看请求

一般来说,我们都是先分析想要的数据到底是从哪里来的°比如说对于示例网站https://spa6.scrape

■「|

center/’我们口I以看到首页有一条条数据’如“霸王别姬,’、“这个杀手不太冷,,等,这些数据肯定是

■■}》↓〗「匹『■厂【‖「=尸■‖’‖【尸■■■「卜‖『■厂

某个请求返回的,那它究竟是从哪个请求里面来的呢?我们可以先尝试搜索下°

打开测览器开发者工具’打开Network面板,然后点击搜索按钮,比如这里我们就搜索‘霸王别 姬,,这四个字’如|冬| llˉll9所示° ◎ ==≡-~

÷



酝★

■巴

冉司●

同5c『ap·

}■■日

.ˉd£

口m…■

儡墅樊丘堕懒富!』正…逛_ˉ…~2量_ p■恤≈唾山呵v…沁] p■缸…唾山呵v…沁` l聊泡……k…狙

…m■馏w献 C■!……m■馏w献

■丁

U

f

×

卜■■「●■「▲尸□》

×●●—亏!百]圆尉…m■…坦9…户…鲤呵 ·吨c 麓



…二二]·°』…

………@,…辙`蹿…吨……。°°陷峙酗………圆…d·

l

!…………`…|…』…}…』…潭闺…画

,口 ˉ 了自 ;二ˉ 琶ˉ

;…■』℃,:《。妇,;‖|.m油.:Ⅷ宇■0广

缺■u●

出∩℃

砷仑喧咀

■ 「 ■ ■ 『|‖■●

■攀ⅢT?…o…们……”咖 医瓣鄂副演……’o……!蛔

腿 |″

呵 !…

墨爵麓螺爵…|………碑…晒



幽爵嚣嚣;豁s………fh





·.

-『鸿J硒…』№0人勺古口电

》[■厂 ■■■■‖■■△■‖▲■β『》》‖■■厂

里爵::蹦嘛:°。…徊…′…=…

ˉ妥



a7『酶≡勺

唾m

…刨

丁巾惦



………捏| 瓣穗盏 鳃……雪, 雕 ;嚼

』 `

碳…

…ˉ…万…】.■↑

↑06阻

…q

,……

…啪





霉……割 { j;瞧缉 | ‖

·………红』 雕爵黑

野ˉ





≈…

4…} 瞩 ! ’~■

…ˉ≡万….■}

{够椭B

牵m=■加…0壁〗

》p°l

{肛

旦》瓣阅翻蕊倒·…°"霹w…勒°=… B:耪漂副M剿….`′铡o…癣…蛔 ■爵蹿鳃…….`m啤w..Ⅳm

‖…▲恼

】y钾



『咽0NB

740硒

瞳 … …… | 磁 碑 ~ ; _鸟--I二—………空』塑〔烹乳ˉ 』 ≈…

8月归U…a旦吧………a↑l■……t印■ […:〗□γ■

【=■「‖|■■「

图llˉll9搜索“霸王别姬”

● = ■ 厂

此时可以看到对应的搜索结果,点击搜索到的结果’我们就叮以定位到对应的响应结果的位置’ 如图1lˉ120所示。 ■■







■—■



■■

■■卢●■■■■■



■■≈■

■●





×◆o。守α田~m.■……№…可热红



d廷琶鳃砂.…由∑磕寅哮……腮…幽亡α…°≈圃d……·

勇霹蹿≡-赚蘸≈ˉ ……

■ 尸 ■ β

§……mj°…‘印窒…≡…=/≡ˉ

ˉˉ

.≡-——-=■-—_=三_

宦≡≡芒==引-= 6…

■ β |



●■

■■

·

■■■「

一瘁……

■『》『

舅圈→

圈………

…m·.≈Ⅺ‖… 陋·=

◆■●



×中



【■『卜}■「[■■厂

■『

区田…m凶………蹿…旗……≡p~岳……血 昌

0…



}鳞照臆焉腻觅.圈魁瞬……………

■g守■~l1咐〔=吟!=·■…尸占口■tm■吕〃氏·■1↑0

盯…T▲归声.四《…『p…0

图1lˉ120定位到对应的响应结果的位置

找到对应的响应之后’我们也就可以顺便找到是哪个请求发起的了,如图1lˉl21所示。





■■■|』■■·‖■■■‖|■■■√』■可|■】]{■■

500

第l1章JavaScript逆向爬虫 ~—了T产广ˉ==丁—=一7

x

∩6…≈

p旧啪W

∩e却O∩蹿

{∏仇‖m叮

∏丽吨cmk‖eS



TGm·圃‖

∩●qu●●tU∩仁们ttp5!//sp己6°曰c『日pe.ce∩te『/■p上/陌ov1危/?1№1t=10巴o7fset≡·凸t◎ke∏剐互E喇W2袖0ODUZjⅦγz‖lγjI1γjd旧Z 0IZ"Ⅷm花γZ丁「啊0喇1哪jI弛QⅦ丁I0

∩响u“t朗…6[丁 蟹…■…●2帕0低 冈……:121·2g.5q·117:“3

■ ■ ■■己≡-_=≈→=_←≈一辑~~→一→≈一→■■→-=+≈恃■



‖‖{|‖|』

…帕行p酗甸: $t厂1ct=O厂191∩制】e∏七「oS占一o厂1g1∩ =击

尸∩●●…●…(‖引 卜∩呻…t…≈《↑司

Tαm可S晌gP■……

γ…sα师●

γ℃wU∩Lˉe硒oo函

0垃吐10

谷哇士O

[|……w…oouz』γ…γjnⅦ…酬…丫厦γ叮萨…‖…j…卿了m|| 图llˉl2l

对应请求

‖ · 《 ‖

比如’这里我们就顺利找到想要的数据所对应的请求位置了’可以看到这是—个GET请求’同 时还有一个toke∩参数’我们可以在后面继续分析。

_般来说’我们可以通过这种方法来尝试寻找最初的突破口。如果这个请求带有加密参数,就顺 着继续找下这个参数究竟是在哪里生成的。如果这个请求对应的参数甚至都没有什么加密参数’那么 这个请求都可以直接模拟爬取了°

0

·拽索参数

‖』·∏ ■【▲■尸巴

在上_步中,我们找到了最初的突破口’也就是关键请求是怎么发起的,带有什么加密参数°比

如,在上面的例子中’我们发现这里有一个关键的加密参数to促e∩’那这又是怎么构造出来的呢?

‖』』勺

-种简单有效的方法就是直接进行全局搜索。~般来说,参数名大多数情况下就是_个普通的字 符串,比如这里就叫作tORe∩,那么这个字符串肯定隐藏



〕B{°c腮酮∩eq

可以加冒号`空格`引号等来配合搜索°因为—般来说这

DQc惯副de

』■□□司』■可··可〗□Ⅵ纠·‖、」■■司■■司·□ˉ■■』■』门

曰| 嘲□×

一一ˉˉˉ

在某个JavaSc∏Pt文件里面’我们可以尝试进行搜索,也

四□曰■

个……_鳖符号—……}『呵以″=:= 索to代e∩、 to促e∩:` to代e∩ ` : "to促e∩":等°

印刨`融

在哪里搜索呢?我们可以直接利用测览器调试面板的

器尸

M°德tms 』 ˉ.==≡



搜索功能’如图llˉl22所示°



,|

这最—个资源搜索的入q比』呵以搜索下载下来"谰渺

JavaSc∏pt文件的内容,这里我们输人to代e∩来进行搜索’ 图llˉl22测览器调试面板的搜索功能

结果如图llˉl23所示°

硒●

尸—_



||

…; v ≡



刚S◎问 0]=w↑∩o咖『 ‖…‖Qs◎∩P0]=wi∩dow「webp“灿臼◎∏p0]‖O『α凰]Ⅶ℃hu∏长ˉ↑9c920↑8M5a↑9|负』∩αm《ˉOx斗99e7□0-0渔鲤4田,ˉ0x3刨▲800‖c6b↑`:「u∩c↑j◎∩仁OxS3

s…馒函曰pacα`tαⅦ已/chu毗ˉvmα◎『S77da↑990.』S

‖…Ⅻso∩P‖]=wt∏d◎w[…bp日c灿●o∩p‖]|Ⅷ』push|Ⅻ印Ⅲ‖《ˉv曰沮◎『s』]°{`O‖4b`仇』吨bm(~mb398∩~0x3↑e77f!ˉOx3‖bc7O》{h…s协ct|;v锄ˉ0x↑e5伸 6…4ad[Ox↑↑bd8a){v盯-0x4bO▲3d=NumbαL0x]咽8a[VαB沁∩0]『sp|‖↑|](′。)[咖0〗;‖仁m4m〃3d>=咖2)ˉOx‖↑bd8a『|w汉|∏]({‘mf◎熄OBate|:Ox5『‖』 25…eq8(-ON‖8‖87c!ˉOx27↑‖o8){OxO;}flmcm∩0渔5‖2c2仁Ox5αWa9){『utu「∩O闽“nm℃℃tyPe‖][!↑oSt『‖门g『l『ca』‖]LO×5d97巳9)[|j∩OexO「](0巨∏o『!)> 3]…陋!]=ftmctjo吐助↑O8odc){mu抽∩u‖!=助↑O8田c&a贝u‖!=砒↑08●dc[0c◎『]sbuct◎F]蹈Tu∩ct‖◎∩,==tγp印f=0x‖O8edc『c◎∩st『uct◎「]「|sB叮f

|‖

丁◎∏u∩次ˉγ臼旧α已°77d臼酗↑.尸



厂■=■「■■■■【■■■■巴

图llˉl23搜索结果 q





|} lll2 JavaScrjpt逆向技巧总结 ■■「}||△■厂匡■「|

这样我们就可以找到_些关键的位置点了,_共五个结果’结果不多,我们可以进_步点击并定 位到对应的JavaScnpt文件中’然后进-步进行分析。 ·分析发起调用

●■〗‖■「■『‖『■■■厂■■『□‖■■

上述的搜索是其中-种查找人口的方式,这是从源码级别上直接查找。当然’我们也可以通过其 他的思路来查找人口’比如可以查看发起调用的流程,怎么查看呢? 可以直接从Network请求里面的Inltiator查看当前请求构造的相关逻辑’如图llˉl24所示° 儡田由心m≈…Sα凰…N·tw田仇

P甄勉呵…佃刨∏磺γ…呻

L杠硼睡……幻吐…皿



●o守α■∩■…v·吨田雌a№…怕№↑0mtl比U

v慰士

f

□卜彻d··酗a(唾s囚乍蛔γX"∩dSCSS ‖碉肋odaF审"D。cWS…"‖徊闹■`m调《□陋b…o°c·。…◎田…d……



=—=_-=—

『凹硒

—-

m吨

m巾

…≈

}∩

=== | 胎←



晌~

睡囱

m=

壹 层『 |

咖m

′三=≡】≡选

■L』





V●

↑m≈

-—=-

■尸「■■巴尸

∩…唾灿

『庐

∩炯‖.‖

砷9

}咖

侣.硒

』…‖.↑

恼w



VwV酬Ⅵ|W归旬`…{”『

§M●v“pOmm叼. .

β ‖●「‖巴尸∩

刁……





■= 码■

■■■

■■■■■



戳 勤

hN胸tα

… ■………



区藤"………鲸

‖mm

吨`……■营γ7…‖.缸‖

o饥



l…m



h凹

回幻g·…·s蛔α叼

—阿

〗‖m≈

b≡■=

=≡_

鳃理h 吗

2

蛔砸

1

| …

p

50l

攒 6

尸…

啊〕ˉ?

Ⅺ门′袒…0

…`"

Ⅸ坷



斡b威

。侧`°《



图llˉl24从Initiator查看当前请求构造的相关逻辑

把光标对应到Initiator这-列,就会出现发起这个请求都经过了哪些调用’也就是调用发起方的 一步步执行流程’如图llˉl25所示°

「【巳β匹∩■■尸|■【伊■■■

右侧显示了_步步调用对应的源码的位置’我们可以顺次点进去找到对应的位置’比如这里第8层 调用里面有一个o∩「et〔hData方法°点击右侧的代码位置,就可以找到—些相关逻辑,如图llˉl26所示。 回曲《日太ˉ]9c92酮.。。.d嚎‖S:亿∏γmt“× I5〗

152 15〕 15q

□口

}|卜

0又516365,emo厂t■ 0N209“8·e×m厂tS

0x5dQb「f·「国哇9t 0×5“bf↑。≤c…ut田>

(0"◎"γ加u5) o∩PetC∩DBt■

』Ⅲ▲Ⅲ

□□自◎巳

》‖|白

尸…15O°t″∏(■5”C』

1且呈盟

158

0∩me0 8 ,』呻x…eop 0pa丁由$0;{ 0p■ge‘ : =0又14c92e

159 160

})p tMS[‖o∩『etc‖OSta,](〗日

165 166 167

168}

]Ⅲ』1

17·

171 172

▲ 尸 伊

Gx3?d088

{a∩o∩…u吞)

0X063gb1棚」凹ate 0x336bg5

■■■■

|}

0x】己695c.gGt 0x沁695c·m∩

O〔hⅫ∩k=γ巴∏dD[旦亿汀q□四91oi981

§·目

=■■▲尸卜|

OXd85「9?

f991ui£B1

鲤理皿

』几■e穴

}′ 0o∩「etCh0日t00『 ↑u∩ct』O∩(){ γa厂 0】4q“71■tM■】

th土M0`O酗』吨‖〗 = !0x·日 γ0厂 0m■d6■〔= 《th15I0p”e‖】 一0x1》巾th1S[‖1m』t0]

0→·x2酗↑↑d亡晌je〔t〔=0×mC0逛[`a,l‖(t们1■[0$StO「e|]I‖$tete,‖ |

翰隧!;;瓣!!{,…麓《喻约!……M喻…]《M』[′…』], ‘[jm1t,: t∏1臼[0u■1t‖】〃 『O77SGt0合 0∏3西mcβ

】7〕

0tO代e∩0 8

17耳

17S 1γ6

vp「一0X5…≡ ·■】睡哟8[0d■ta‖l

177 178

p夕ד马30〗定Dx5凹●66I↑「e501tS0]

′ ˉβx59↑612适

181』

182

183

图llˉl25调用发起方的_步步执行流程

0x鳃·秘6[`coo∩t‖];

0x446d71[‖1o“1n00] ■ !0其1’ 0汰4q6d71〔0■ov1e$‖] =←0x“▲3010 0x“6d71[0tot■1‘l =£焰97612;

180

●(hupk≈uG∩d□r盅.77d■↑g91△‖■:】

0x2b0?7d



})『0the‖‖〗(「u∩ct』O∩(夕x1唾b鲤){

179 』且己

↑↑ 匹



16〗

162 163

@P们u∏贸←…吐◎『5.77d■↑”1·js8】

巳●巳

0xQd726a



t∩1$[0$了o0te『0】[0po■h0]({

16g mu∩ted



0t厂a∩£↑e「0 日 0义10c蚀6[『■‖]’ 0o∏p硼●Ch日吨e0 日 「u∏〔t1◎∩《=·x1QCg2e)《

157

16d

1.‖S且1



°睡t‖odS0 :{

155 156

(■∩O∩…仙S》



0闲0∏t“0: ↑9∩ct1O∏《){ tM$[‘o‖「etch0at日,]()β }』

})β

图llˉl26相关逻辑

这里可以看到—些tO代e∩相关的逻辑调用过程了°

『|〉

●断点

另外’我们还可以通过_些断点来进行人口的查找’比如XHR断点`DOM断点、事件断点等°



502

第ll章JavaScript逆向爬虫

我们可以在开发者工具Sources面板里面添加设置’比如这里

我们就添加了XHR断点和全局Load事件断点’如图llˉl27

弓亚画…盂ˉ一…—__

鲤日旦墅塑工e塑国9:ˉ=≡ ._.—ˉ 寸DOM日【函…j诫s

所示°

.

■』荤

这样网页就可以在整个网页加载之后和发起Ajax请求的

__ˉ__=筋疆藏胃~ˉ~ --——~—一冷__一ˉ≡■

::饼b疆:…吨 ’□猛面赢 仑□印门γ碑

大的断点调试功能,我们也可以找到对应的人口。

,口°|晌.°…



{(

时候停下来,进人断点调试模式°也就是说,通过测览器强

p□cmm

候□o○0丹Muta前o∩ G□O●γ烟

●Hook

些代码搜索或者断点并不能很有效地找到对应的人口位置` 这时候就可以使用Hook了.

p◎O『鲍/口吨



,□G·。蛔°a`|°"

喝鹏… :8鹏 ’○"硼∏C名m〕

比如说,我们可以对_些常用的加密和编码算法`常用

的转换操作都进行_些Hook’比如说Base64编码`Cookie的

图]lˉl27添加XHR断点和全局

比较方便的Hook方式就是通过爬mporMonkey这个插件实现’使用它我们不仅可以方便地自定 义脚本执行的时间点’也可以引人一些额外的脚本来辅助Hook代码的编写’具体的实现流程可以参

■■Ⅵ‖||‖』■■■]『□‖|■]』■

Load事件断点

赋值JSON的序列化等°

‖』□』可‖‖‖』■勺‖』‖|己■·■{■■

Hook也是_个非常常用的查找人口的功能°有时候’—

考ll.3节的内容,这里不再赘述° ●其他

■||‖』‖=□

面内置的API实现_些数据拦截和过滤功能,也可以使用_些抓包软件对_些请求进行拦截和分析, 还可以使用一些第三方I具或测览器插件来辅助分析。

||(

以上便是一些常见的分析人口的方法,当然还有很多其他方法,比如使用PyPPeteer`PlayWnght里

2调试分析

找到对应的人口位置之后,接下来我们就需要进行调试分析了°在这个步骤中,我们通常需要进 |·

行_些格式化、断点调试、反混淆等操作来辅助整个流程的分析°

格式化这个流程是非常重要的,它可以大大增强代码的可读性,_般来说很多JavaSc∏pt代码都 是经过打包和压缩的°多数情况下’我们可以使用Sources面板下JavaSc∏Pt窗口左下角的格式化按钮

】〗日

渺■

…γ·●·→



3

6

71l》=53》】fmCt1O"j 〔e0t』∩0j){1f〔r了){va『 〗它γ∏0■t;t….卫m…庐7四Ct1m.‖ 8

‖‖

咆…_………………



■∏■■■‖■●

对代码进行格式化’如图llˉ128所示°

』∏{卧

●格式化

g’ 】0

尸◎凹m■h旧jMm 0◎p丁n…》Jm

〗1 12

1》}0…e12{γ己1啤ge°Ge1e〔te毗己beI·〔aumC戊:?u】ct1m《t》{e·田e1eC0 13t‖oUe丁j∏F!1》}0…● M

』5 17

】8

固…

■字

7c…了m5?畴3

图llˉl28格式化按钮

~≡←≡示产

…;『咆

‖■‖‖■‖‖■ ] ‖ ] 」 《 | √ | □ ∏ 』 ‖ 门 (

16



} ■■■◆『|■■厂●尸【‖‖‖▲■「■『



ll』2

JavaScript逆向技巧总结

503

另外’有一些网站的HTML和JavaScriⅨ是混杂在一起的,比如htms://spa8scIa征.cent刨’如图llˉl29 /

所示。

■厂‖■『|‖■■尸■厂‖卜‖【■『|■=尸

■■▲

仿『『巴尸

图llˉ129HTML和JavaScript混杂在-起

可以看到,这里JavaSc∏pt代码被压缩成—行’并且放在5cr1pt节点里面’这时候我们就需要手 动复制出来,然后用一些格式化工具进行格式化°可以搜索JavaSc∏ptBeautiher相关工具,比如

https://beauti低erjo/’然后把JavaSc∏pt代码粘贴进去’此时即可看到格式化之后的JavaSc∏pt代码,如

0

图llˉl30所示。 ■ 『 ‖

OnlineJavaScriptBeautiher(vⅡM则 印■啤酗醒勿…锤哄鲤…凹嚏鲤鹏q蹿鹅……鸥

β

β『■尸庐》口》‖伊们■∏■■‖■■■·■△■卜》■‖卜‖β

酗的〗心m■四…℃……恼■对…L硅m酶必…腑了… 巳耐唾…■酗Ⅷ睡■删℃…t……ym■■沮■″==蚀赵.

画≡_≡≡—司

■ ■ ■ ■ ■

↑↑

-

凸乓≡□

.



…ˉ ˉ

hr】



ˉ

ˉˉ.…ˉ ˉ

p卫·

0.·T

ˉ ˉˉ隆」=l…画|№≈创…′ˉ(画石≡`「≡

…■ °…………….

》′ 〕

邑0c≡■ 《



二_ 『

}『…]硷嫩…

\酝宦画匡



图11ˉl30格式化之后的JavaSc∏pt代码

} ■厂》’△■∏∩‖

另外,我们还可以选择一些格式化选项,比如缩进、换行等° ●断点调试

代码格式化之后,我们就可以进人正式的闺…程了,基本操作就是给想要调试的代码添加断点’

「)′)β『 D_





第ll章JavaScript逆向爬虫

504

■■可■■曰

同时在对应的面板里面观察对应变量的值°

』 ■ ■

如图llˉ13l所示’这里我们在第169行添加断点’然后逐行运行对应的代码,这时代码页面就会 出现对应变量的值,同时我们也可以在Watch面板上监听关注的变量。 ◆ ×

凸●仓力●§



‖…

·……

—…·^·



●_千

霹】+ |…访…0pα



=0^:1』Ⅷ

‖‖

「■伊■



…砷 片…





…■拽凸~翻

咆…_…心心■■°



嘴 】02 mJ

【■

D ■ttt皿…0td沁冗『『]

呻°

·…啤2ˉ■必?“JOmt〗…》 …问■

p■■℃《…吕 《》

在第l69行添加断点

■|Ⅵ‖

图llˉl31

q

0

D二…巳…l…t■丁汕…:《』

=…】【o…叼p〗■〖●lp

…{Q西mo〗p伞山mp

7■≡=一

辨ˉ



…1Ⅶl心8 0●l凹

·→…『■■■=0dc凶"t0】『

】”’ 也】

0

◆上cm“■$O

吨J→否■m…§0山屯$‖] 0■…●磅‖0厂“仰MB0》

D7



V们,`→0 ■√M↑些.…

●th〗6『6【■四m口】』 □HⅡ■〔皿

》M,t…0]《?…u■《ˉ■…』{

〗70

】网



已↓■宁呻≈′吕…· 旁√凸§\″{萨 T呸】亡▲乙:忱jmt ●■日f=0心″y伺√』 p=门轧≈8蚀』●m

{严镭

| }Ⅱ 絮 撼!

》]Q

tM■【0画倔…比D〗(〗〗

》p

■ 日

通过这样的方式,我们就可以对整个代码的执行流程有—个大致的了解°

●反混淆

□‖』·|司·

关于断点调试的使用’可以参考ll.2节和后续实战章节。

在某些情况下,我们还有可能遇到一些混淆方式’比如控制流扁平化、数组移位等°对于_些特

‖ 』

殊的混淆,我们可以尝试使用AST技术来对代码进行还原°

比如说,案例https://antispiderl0.scrape.centeⅣ就使用控制流扁平化的方式对代码进行混淆,如图 ·

llˉl32所示°

= ■ ■ ‖

蕊圈…

←=■

15∑

CO∩二t1窜b;

154

co∩gte与c()β wh」1e 《 ! ![]){

155

156

t厂γ{ Co∩马t f=

157

(p己『SeI∩t(l(0×6))/0x1) * (pa「seI∩t《1《0X10)〕/0x2) +

158 1田9

pa『£eI∩t(1(0Xa))/0x3+ =p巴「二eI∩t(1(0x7》)/0xq+

160

(pa「艺eI∩t《l(0×4》)/0×5) *(p己「吕eI∩t(1《0X1)) /0×6) 十

161

→pa『Se1∩t(1(0xe))/0x7+

162 163

缉鳃捌息……』

(=pa丁SeI∩t(1(0×12))/0x8》*(pa厂吕e【∩t(1(0×3))/0x9) +

16匀



工/

1f {↑=d) { b「e酣; }el$e{

165

166

167

e[00push00]《eI"5Mγt,!]());

168



169

170

171 172

173

}〔己tC们 〔0) { ●[帅pq9h00](e[0‖S∩1↑t"](》)j 》 }

17』})《a′

0x796ee);

wq

图l1ˉl32使用控制流扁平化的方式对代码进行混淆

{■】】··』』■司■Ⅵ当■司·凸□■』■」勺

膨臆『』ˉ

=pa厂Se【"t(1(0x0)) /6x·;

』 □ 可 ■ ■ ■ 』 ■ 司 当 □ 】 ■

圈鹏cα"′…赋 露蹦:……ˉ蛔帆"

《↑仙∏Ct1O∩ (C『 d){

153

■■

蹈mp悯9c。m′幽…u{′|!趴h·m…|

凸■

』■■□】■■可

□可』■□可|■■■

∏■■■■■■■ˉ■■■■□『尸|||它「|》「|卜‖||β}||卜『||■「||β|「β卜β}



‖ b

ll.l3

JavaScript逆向爬取实战

505

可以看到,这里有-个W∩j1e循环’循环内通过一些判断条件执行某些逻辑’有的逻辑放在了1+ 区块’有的逻辑放在了e15e区块’还有的逻辑放在了〔at〔∩区块,这就导致我们无法-下子了解这几 个区块的真正执行顺序°

对于此类混淆’为了更好地还原其真实执行逻辑,我们可以尝试使用AST进行还原,具体的实现 流程可以参考ll.9节的内容。

3.模拟执行 经过一系列调试,现在我们已经可以厘清其中的逻辑了’接下来就是_些调用执行的过程了°在

前面的章节中,我们已经讲过一些案例执行的流程了°

●Python改写或模拟执行

由于Python简单易用’同时也能够模拟调用执行JavaSc∏pt°如果整体逻辑不复杂的话’我们可 以尝试使用Python来把整个加密流程完整实现一遍°如果整体流程相对复杂,我们可以尝试使用 Python来模拟调用JavaScnpt的执行°具体的内容可以参考ll.5节°

β b

●JavaScript模拟执行+APl





p



} p



由于整个逻辑是JavaSc∏pt实现的’使用Python来执行JavaSc∏pt难免会有-些不太方便的地方° 而Nodejs天生就有对JavaScript的支持。为了更通用地实现JavaScript的模拟调用’我们可以用

cxpress来模拟调用JavaScript,同时将其暴露成一个API,从而实现跨语言的调用°具体内容可以参 考ll6节。 ●测览器模拟执行



仿



由于整个逻辑是运行在测览器里面的’我们当然也可以将测览器当作整个执行环境。比如使用

Selenium`PlayWright等来尝试执行-些JavaScript代码’得到_些返回结果°具体内容可以参考

b





ll.7节°

调用执行的方式有很多,不同情况下我们可以根据实现的难易程度来选择不同的方案。

p p

4.总结







本节中’我们对本章所学的知识进行了_些串联和总结’通过三大步骤—寻找人口、调试分析`

模拟执行来梳理JavaSc∏pt逆向过程中常用的技巧。另外’我希望大家能多结合实战案例对这些技巧 进行运用,熟能生巧。

p

) p



『 p





P





↑|.|3 」avaSc「|pt逆向爬取实战 前面我们学习了各种JavaSc∏pt逆向技巧’本节中我们综合应用之前学习到的知识点进行_次完 整的JavaScnpt逆向分析和爬取实战° ↑.案例介绍

本节的案例网站不仅在API参数有加密’而且前端JavaSc∏pt也带有压缩和混淆,其前端压缩打

包工具使用webpack,混淆工具使用javascriptˉobfUscator°分析该网站需要熟练掌握测览器的开发者 工具和_定的调试技巧,另外还需要用到_些Hook技术等辅助分析手段°

案例的地址为hnps://spa6.scIape.center/’页面首页如图llˉl33所示。初看之下’和之前的网站并 没有什么不同之处,但仔细观察可以发现其Ajax请求接口和每部电影的URL都包含了加密参数°

















506

第ll章JavaScrjpt逆向爬虫

了二

.

已☆ 内●§

{』|

同5c『…







霸王别姬ˉ庐a「ewe|‖MyC◎"cub『∩e

95

■●



酗阻内绝宇晒●Ⅲ′w↑甘■

0丹■

毛 ■P

□□·

AD0◎S八棚0

◇0山20了锡2▲工乐

q

= ■●

■●■





●■●■

■■■■

■■

●■

■■■



■■■D

F■尸■











■■■■■=步≈■■

■■■

这个杀手不太冷ˉ止白◎∩

》E

寇■o 〗TC分p ·哼叫牺汹2但 话



\叼

肖申克的救赎ˉ丁们e5b■WSha∏仪R●de丽俗tj°∩

95

●● ∏■′;铭和 V,铡屿瓣k痢

●d^】■





· □ ■



图llˉl33贝面首页

比如,我们点击任意_部电影’观察—下URL的变化’如图llˉl34所示°

同5c『.p. ■王别姬ˉ「a吧W●‖朋yC◎"Cubj∩e

95

■■●

《』■|||■■∏‖〗‖】■』■〗』』■■|‖」‖|』●』■■]||」■Ⅵ■〗‖‖|』●可□■口·‖‖‖‖』■可』]|‖』■」■]|||』‖尸‖Ⅷ‖||』■】』■]』■■

●■■●●

…℃.中■■■′!P‖分钟 □%』0′PO上殴

■■

|■愤■介 队涡■ˉˉ止〖■壬询E》们巾记.压创匡二占/`人靠巴=册…代戊云显吕P已

”出仇."′阳l牛凹协》岛■■孜·征沮Ⅷ矩》q…潭W哆广=鉴怯 王硒】·Ⅲ■■■囱贝°为戊闸人灼定酮≡■子《■耍蛔》· pd吊

八时n■曰人伎共Ⅸ岭乙臼本田不曰°险′P咀歪知田p人包睁,阻们残钱Ⅺ 田■夜认r吨G可卑仁■三白,■殷′卜啪了佰挝。E止三人辩≡忠

【■下喊》生…召哦俐仇凶开妇■■田代贝云的由迢灰低升拇‖傀■压 ………咀亚



】 … 碑 ≈ ■

_…………

人功不分·勋』峨谷认为队…w性之闪湿■了名蜀吗曲〔巩崩协′ 贮牛

9■°

|导Ⅷ

"I以看到详情贝的URL包含了一个长字符串,看上去像是Base“编码的内容°

接下来’看看Ajax的请求°我们从列表页的第l页到第l0页依次点_下,观察Ajax请求是怎样 的,如图llˉ‖35所不°

!jI以看到,Alax接[]的URL里多了一个to促e∩,而且在不同的页码’to促e∩是不一样的’它们同 样看似是Base64编码的字符串°



图llˉ134点击任意一部电影后URL的变化

■勺■■口■Ⅵ】■■Ⅵ|■■■|■」■||』■■■‖|‖·‖」

唾曰兄印·氏典诌个滩≈个宙呈° ≡唾●Ⅲ衣租ˉ尤■一泄『■



| ●茅■ +

x 」 +

回物…献也心 G

JavaScript逆向爬取实战

507

■■

β}

ll.l3



●…≈……≈种0



■●



动断蓝桥ˉW酞●r‖◎◎B『jdg●



9.5

●●●

● ■ ◆



■■′9凹估蚀 p

l0蛇仍w上砍

』β「『

{文田函■m 鹰α…S…蚀 肘毗…贞 P佃…m回呻…Y壁》 叼白m≈ ●o亏α■凭…冲吨田懒……wO询℃ Q 仑 £

:

x 巾

憾…om·丛…蝎“S呵…·『…吐wS啥…α汀`k……m……■≡

≈ =尸■尸·=尸■=∏▲尸■尸■【■「■‖■尸‖■尸■■■■尸尸··■■尸『【队■■∩佃■『卜卜■尸■●●【尸■尸|β■■■口▲■■■■●∏·|△尸{‖『丛■『『「■■■∏■『

『。》「} 广



洱…………7



γ……

|■鳃隐g…………瞬蹦鼠″恕嚣霹费麓′;应蛊蕊蕊撼………,………… ,}∏…硒理`…■■.

孕唾

]〖…丁C0

……侄V

『孕蕊隐…….…肚 ‖……●mm …b∏…袖诚m0g ■兰中T◎△=L=辽ˉ

……”·03◇酗o凹3:“2

……2Y7匹∏铅厂1·』砷>c砸9心「约皿

型′鳞

_—

图llˉl35

Ajax请求列表

另外,更困难的是’这个接口还有时效性。如果我们把Ajax接口的URL直接复制下来,短期内 是可以访问的,但是过段时间之后就无法访问了,会直接返回40l状态码°

我们再看一下列表页的返回结果’比如打开第_个请求’看看第一部电影数据的返回结果,如图 llˉl36所示° ×

N…e『s

P『酌怕W

∩魄……

‖∩"mα

∏『Nmg…N胸

中《1d: 10 沉司胚; 00■王别姬000 aI1己S: "下□厂…l1"γCo∏cubme"′■》

户aCtO「田: [{∩已赌: 邪张圈蒙000 厂o1e日 "穗嫌衣"′=}′{∩■酝8 M张丰p"0 丁Q\e: "段小根0o′≡}, {∏a■eP "巩俐o,, 厂o1e$ "蔼仙"′=] aM己s: ′!「a「…u利γco励cubme『l vC已t甸◎「1e5: [·0剧情b0’ 呻爱佃"】 0: ‖0枷‖0 】8 问Ⅲ恫‖0

Coγe「目 ,0http3日//p0.刷e1t‖己∩.∩et/mv1e/Ceqda3e03e655b5阳8ed〕1D5〔d7896〔f62472.jpg铝“■■6q马hˉ1e=1它," vd1厂ecto痞; [《∩曰臃: 00陈■欣『‖’=}]

》0: {∩■哇8 00陈瓤欣"0单}

d厂■腮8 00彤片旧-出《■王别姬》的京戏,牵扯出三个人之间-殿随时代凤云变幻的爱佃筋仇·段‖嚼(张丰假饰》与租矮衣(张国荣饰) 』 1d8 1 ■』∩仙te: 171

∩a畦: 00■下别撮"

◆phot◎S吕 ["∩ttpSg//曲.唾Ⅲtu■∩°∩et/mγ1●/马5beq38368bb291e501dc52〕的2f0aC8193420.jp嘘10鲍】髓h←1e=1C0‖’=] pobu5∩edat8 0$19g3=07=26" 「■Ⅶ■: 1

丁「eg1o们£8 [‘`中围内地"’ 00中田香港"l 0: 00中■内地00

18 咖中四香港的 ■cO「e〗 9·5

0pdqte0典t; 阿:020刑3ˉ07丁16:31;36‘g67843乙"

图llˉl36第一部电影数据的返回结果

这里看似是把第一部电影的返回结果全展开了’但是刚才我们观察到第一步电影的URL是 https://spa6scrape.center/detail/ZWYzNCN0ZXVxMGJ0dWEjKC0lN3cxcTVvNS0takA5OHh5Z2|tbHlme HMqLSFpLImbWIx’看起来是Base64编码,我们对它进行解码,结果为e+34#teuq0btua#(ˉ57w1q5o5-

『『

j098xyg1Ⅶ1γ+x5*ˉ!1ˉ0ˉ‖b1’看起来似乎还是毫无规律,这个解码后的结果又是怎么来的呢?返回结 果里也并不包含这个字符串’这又是怎么构造的呢?



508

第ll章JavaScript逆向爬虫

还有’这仅仅是某个详情页页面的URL’其真实数据是通过A}ax加载的’那么Ajax请求又是怎 样的呢?我们再观察下’如图llˉl37所示° 中

§

× 中

v十ˉ

·

…蜒:旧t…2〃….5C7…·…TO〃绅1/…蛔m…】N〔■泅沈NcⅣⅧ5●t●…】Z2lt酬…B『◎止叭tm叉/7t



…EJj翻【5…γ叼酗C…m巳m『lγj…j1晒心冲 …空←匪丁



…=^≡亏●2们邱 ……】幻pm7·卫7·n3:0凹



■■‖‖‖」·】叮‖‖』■‖』∩‖

卜唾…m…帕w碱……≈铂



畸T…■t尸1cr≤厂】0』炉出m<7口T■宇oF1日1∩

{{‖■

守……■

……

……bγte$ …唾丁p盯p p∧7α0m正丫Fp…p的丁T…

-汽……V…, .

-………皿ˉ ˉ

…呻■绑』□

ˉ `

{`刻′`“函…晒哩旦s咽v■唾t÷

…Q"江…句p■≈=■#…屿】j

图llˉl37A)ax请求列表

这里我们发现其A)ax接口除了包含刚才所说的URL中携带的字符串,又多了_个to代e∩’同样 也是类似Base64编码的内容。总结下来,这个网站就有如下特点:

□列表页的A|ax接口参数带有加密的to长e∏; □详情页的URL带有加密1d;

如果我们要想通过接口的形式进行爬取’必须把这些加密1d和toke∩构造出来才行’而且必须

由于是网页,所以其加密逻辑—定藏在前端代码里’但上—节我们也说了,前端为了保护其接口 加密逻辑不被轻易分析出来’会采取压缩`混淆等方式来加大分析的难度.下面我们就来看看这个网 站的源代码和JavaScnpt文件是怎样的。

首先看网站的源代码’我们在网站上点击鼠标右键,此时会弹出快捷菜单,然后点击‘查看源代 码,选项,可以看到结果如图llˉl38所示° 回S口…↑a…·

x回v≡≈…佩硒…四征x

q





ˉp G@v…谰~………>砂』

霸{. 冷`Y韩鼠^

敷☆ 办●§

. . 当、、′宇丫尸飞 』飞0`←‖ˉq1c皿1…■●n>■b●四>=七旦·h己r■●c■u亡壬=0…t■D℃七伊●…1v渔x≈Un=c…■乞土b1●c◎n七@n七■·工E■●dg●白><四t■n■■■vmw脚rt

耍回些醛写.¥』g哩T屯v』c●ˉ■1°恤′土皿诬αIˉ■c●1←】闰><u浊r●L■儿cα`hm击α…^…<c』c1e>购m严}№v1…/七ic1·≥<L1n找h〖·f·么°s自/°hunkˉ

L…旦…皿率鸟且乓·LT雪·2·七ch≥qmkhr·Ⅲ.四…k二2KZ№出z且五区∏刁Ⅲ〗巫mr·1·pmf·电亡h沪之1土圃欠hr·f./↑■/chu∩rˉ19C926fac〕a11∑9d.js pQpp粤m·哼c脑<1人@上ˉ°….上…二j■m1丽恒·mC.D<1血m盂●矗·/』口′扁肘…k≡△d▲c7·f0ˉ豁c2bl〕Oˉ↑日r。1回pr·f°t.h><11nk

hmf□么m婴马蜒哩2……■x■1■Pm】°oda白■·亿y1●<1inm`r·f匈】i■/△…窖mmd△昼·ˉ0qh·1■砰m°adap■臼°r蝉t><1儿nkh污·f曰/j5无硒匡

…Xn…凶上皿rc1■w·1c函a·.口crip七≥≤11nkhr·fm上四■/…ˉ…靶■o塑ˉ…口.mj郊cy1b·h…c>≤/h·…<i°qy>乏h°·c过pt=之·c臆mg>们··r·口·rzYmt 尸m■1…■n0cmrkPx…r1Ym它№u七J●v…rnp但m山1m. P1凹m●肋■b1●止它◎@…它』nM●.</■cr◎ng≥≤/n◎scr儿P仁≥<d1v1◎■●pp>≤/d人v><·cr1p七 ■rC■△■/巳hnnk=■■■土它■ˉ77d▲f99】ˉ《□≥</■cr止陡菏●c■』P七■r它●么↑■/…p台≈歪od▲■Gˉ寸■></●cr1P七…/鞠dy></‖亡m↓≥

图l lˉl38查看源代码

‖|‖■‖□■司|■]口|」■■刁|」】·∏司■■■]口|司』司口〗||』■■Ⅵ√‖』』■■■□■|□Ⅷ】』]』■■■■■





■ ■ ■ ■ 〗 』 ■ ∏ | | | ‖ ‖ ■ ■ ■ | 』 ■ 】 | | | ] ‖ 」 ■ ■ ‖ ‖ 《

到现在为止,我们知道了这个网站接口的加密情况’下一步就是去找这个加密实现逻辑°

』 】 』 ■ ■ |

_步步来。首先我们要构造出列表页Ajax接口的to代e∩参数’然后获取每部电影的数据信息,接着根 据数据信息构造出加密1d和加密toke∩。

||

□详情页的Ajax接口参数带有加密jd和加密to促e∩。

|《‖□■」‖·Ⅲ□『‖|』■】■口‖‖(〗`|□』∏‖|』■■』■司|‖可

}篱熟氮ˉ

团潜…m…颐"ˉ

’ 日β

战 实









●●□巳





■■■「||△尸‖||■「‖『■■

「 ■





γ



·



^●









〕 ] ] ]

} 内容如下:

〈|D0〔丁γP[∩t们1〉〈ht川11a∩g=e∩〉〈head〉〈∏let己〔h吕I5et=ut+ˉ8〉<∏etahttpˉequiγ=Xˉ0∧ˉ〔o‖p日tjb1eco∩te∩t≡00I[=edge"〉 <们eta∩a‖e=γiewpoIt〔o∩te∩t=||width=deγjceˉⅣidth’j∩jtja1ˉ5〔a1e=1")<11∩低re1=jco∩hre千=/十aγj〔o∩·i〔o〉〈tit1e〉 5〔Iape | ‖oγ1e〈/tjt1e〉〈1j∩k∩re+=/〔55/〔∩u∩促_19〔92o十8.2a6496eo.c55re1≡pre千etch〉〈1i∩长bre+=/c55/c们l」∩促ˉ

2十73b8+3。5b462e16。〔s5re1≡pre千et〔h〉〈1i∩代hre十≡/j5/chl」∩促ˉ19c92o+8°〔3日1129d。j5re1=pre「et〔h〉〈1i∩k们re千=/j5/ 〔bu∩促ˉ2+73b8「3.8「2十〔3〔d.j5re1≡pre「et〔h〉〈1i∩k∩re十=/j5/〔∩u∩促ˉ』de〔7e千O·e4c2b13o·j5re1二pre+et〔门〉<1i∩代hre十=/ 〔55/日pp.ea9d802a.〔55Ie1=pre1oad日s=5ty1e><1j∩促hre+≡/j5/app°5e千od454.]5Ie1=pre1oada5=5〔Iipt〉<1j∩代hre十≡/ j5/c们u∩促ˉve∩doI5°77da「991.〕5Ie1=pIe1oada5≡5crjpt〉<11∩贞∩re+=/cs5/app.ea9d8O2a.c55re1≡5tγ1e5∩eet〉</bead〉 <body〉〈∩o5〔ript〉〈5tro∩g〉‖e|re5orrybutpoIta1doe5∩0twoⅢ|(proper1ywit‖out]己γ己ScIipte∩日b1ed° p1ea5ee∩ab1e ittoco∩tj∩ue.</5tro∩g>〈/∩o5cript〉〈d1γ1d=app〉〈/djγ〉〈5cIipt5rc≡/j5/〔‖0∩|(ˉ`/e∩dor5.77da十991.j5〉</s〔r1pt〉 〈5cript5rc=/j5/app.5e干0d』54j5〉</5〔ript〉〈/body〉</∩t们1〉

『侣

这是_个典型的SPA(单页Web应用)页面’其JavaSc∏pt文件名带有编码字符、〔∩u∩促`γe∩dor5

等关键字’这就是经过webpack打包压缩后的源代码’目前主流的前端开发框架VUejs、Reactjs等的 输出结果都是类似这样的。

‖·

-

●攀砧回坐…‖山引● e

÷

-

-

-

蹿



日 Q古

■ ■…●c…c●n■

白●

同5c『圈p. …=



…唱



‖ ‖



『…… 己



姬 别





……

瓜…





【尸『△■■■尸》巴尸份凸■厂『∏■■■■■≥口

接下来,我们再看—下其JavaScrlpt代码是什么样子的°在开发者工具中打开Sources选项卡下 的Page选项卡’然后打开js文件夹,在这里我们能看到JavaScript的源代码’如图llˉl39所示°

∩亡 中

;

×

—~

忻‖瞬舍◆

……∏

D

§

宁口吨

E…由.‖….c9·‖‖…尸揖

团‘9^! ?←≠o

i 《汗m…『g…ck】…pp]≈…l·…℃凶■…,l‖0[】》lQ…h,】《【l’〔hmh=10cmO7■0M0 p恼 vc当9…

v·…画…≈"Ⅸ

嗣…

伊鞠…

’懈脑m

》‖‖β‖卜由「

卜瓣色吨

啦…

丁出捧 丁…

心呼5…剔尸

些—

丰感芳…创史考Q二.一≡ˉ■宇T0 谚由m…7m“回 h创Nm、蹿…江77… G们…》

—-=~



bⅫ″…… ◆…… ●…【≡

■◎蝉稻囱吨

>区呵1……m

o°p↑…回碱

’◎酷.P〕旧件

co″…门日

《} kh唾↑·…呵d… ■

图llˉl39 JavaScnpt的源代码

我们随便复制一些出来’看看是什么样子的,结果如下: ‖}}|『「



(wj∩do"[Webpac|(]5o∩p0 ]="j∩do0‖[!眶bp己〔k〕5o∩p‖]||[])[』pu5h』]([[‖c∩u∩|(ˉ19c92o十8, ]’{05a19! :千u∩ctjo∩(卫x3cb7〔3’ ox5〔b6ab’ ox5+S01o){}’!〔6b「‖:+l」∩〔tio∩(一0x1846十e’ 0x459〔o4’ o)(1仟8e3){}’|c日9〔‖;+0∩ct1o∩(px1952o1』0)(〔41ea d’ ox1b389〔〉{0u5e5tr1ct0War 0x468b』e=0x1b389〔(‖5a19‖)’ ox23245』=O)《1b389〔[|∩』](-0x468b4e); 0x232』Sq 「a‖]j}』′d504‖ :…’[0xd670a1[』-v!](-oxd670a1[|ˉ50 ](-0x2227b6)+0\x0a\x∑0\》(2o\×2o\》(2o\)(∑0\x2o\)《20\)《20\x20\ x20\xⅡ0\x20\x2O\x2O‖)])j})’Ox1)’ˉOx4e十533(|d1v0 》{|stati〔〔1a55, : ‖们ˉγˉ5‖\x20i∩十o‖}’[ˉ0》《4e千5〕3(|5pa∩! ’[-Oxd6

‖[ △尸‖■■∏匹尸 仙■■■■■■■『}■■厂‖|卜『|巴■厂‖巴■「

70a1[ 』ˉγ| ](ˉ0》(d67oa1[『_5|](_0x1〔〔7eb[『reg1o∩5’][,joj∩0](|、 |)))])’ˉ0x4e十533(|5pa∩|’[ˉ0)〈d670日1[|—γ』] (0\》(2o/\》(2o‖)])’ˉ0x』e+533(』spa∩‖ ’[—o》(d67oa1[0一v|](-0xd67o31[ 0-s‖ ](-0x1〔〔7eb[|m∩(』te‖])+0\x2o分钟‖)])])’ Ox4e+533(`diγ『 ’…’_ox』e十533(|e1ˉ〔o1|’{|attr5, :{|》(5! :ox5’|5叮:Ox5’|"d』:Ox4}}」[ˉOxqe+533(|p‖’{05t己t1c〔1a5 5‖ :‖5core\x20"ˉtˉ‖M\x2oⅧˉbˉ∩ˉ5们’}’[-Oxd67oa1[ !ˉv‖](ˉ0xd670a1[『_s0 ](ˉox1c〔7eb[ ‖5core0 ][ 0to「1)(ed!](0x1)))]) ’ 0xqe+533(|p‖’[ˉ0x4e十533( ‖e1ˉmte‖ ’{‖attr5‖:{』va1ue′: Ox1〔〔7eb[‖5〔ore0]/Ox2’‖di5ab1ed,| : 0 ’』肌ax|:OxS’ 0tex t≡〔o1or! : ‖#仟990O|}})]′0)(1)])]’0x1)]’0x1)j})′0》《1)]’Ox1)’→Ox4e十533(0e1ˉroW’[ˉO)(4e+S33(‖e1ˉ〔o1‖’{0attr50 :{ |5pa∩‖:Oxa’ 』o仟5et0 :0xb}}’[ Ox4e+533(‖div0 》{‖5tati〔〔1a55‖: !pag1∩atio∩\x2O"ˉvˉ1g‖}’[ˉOx4e十533(′e1ˉpagi∩ati o∩|’…;+u∩ctio∩(O》(347c29){ˉO》(d670a1[|page}]=-0x3』7〔29;}’|update:〔urIe∩tˉpa8e|:+u∩〔t1o∩(ˉOx79754e){ˉ0xd6 7oa1[|p己ge|]≡ˉo)《79754ej}}})]’0x1〉])]’0》(1)]’0)(1)j}’ˉox357eb〔=[]’ˉ0)《18b11a=0x1a3e6o(,7d92|)’ ox4369=0x1a3 e60(|3e22|)’…;γaI 0x498d+8=…[`the∩|](+u∩〔tio∩(0x59d60O){varˉ0x1249b〔=O)《59d6oO[`d3ta丁]’ˉ0)《10e亚4≡ 0x1249b〔[re5u1t5』]了ox47d41b=ox1249bc[』cou∩t|]》ˉo"531b38[|1oadi∩8]≡|0x1’ ox531b38[`′‖ovie5』]≡0x1oe〕24’



■]■∏‖|‖|』■■司|‖|』■■■门|‖|

5l0

第ll章JavaScript逆向爬虫

没错’我们就是要从这里找出to代e∩和1d的构造逻辑°

要完全分析出整个网站的加密逻辑还是有一定难度的,不过不用担心,本节中我们会_步步地讲

| | 」 ■ 】 『 { | | | ■ 口 二 ■ 可 | ‖

‖恩,就是这种感觉,可以看到_些变量是十六进制字符串,而且代码全被压缩了°

■ ■ 司 ■ ■ 可 ■ ■

ox531b38[』tota10 ]≡ˉox』7d41bj})j}}}’≡0x28192a=ox5十39bd’ 0x5+5978=(-0×1a3e6o(‖ca9c』)’ 0x1a3e6o(0eb』S‖)’ 0 x1日3e60(』2877{))’ˉ0×〕十ae81=Obje〔t(ˉ0x5十5978[0己‖])(ˉOx28192a’ Ox443d6e’ 0x357eb〔’!0x1’∩u11’|724e〔+3b‖’∩u11 )j 0x6十764〔[{de十au1t‖]≡0x3十ae81[ 』exPort5‖]j}’』ebq50 :fu∩ctjo∩(-O×1d3〔3c’ Ox52e11C’ 0x3+1276){0use 5tri〔t0 βvar 0x79“6〔=Ox3+皿76(0〔6b+』)’-0x219366=0x3千1276[|∩0 ](Ox79O46〔)j 0x219366[03‖]j}}])j

解逆向的思路、方法和技巧°如果你能跟着这个过程走完’相信还是会对整个JavaScript逆向分析过 程更加熟练°





2.寻找列表页∧|ax入口 我们就开始第-步,寻找人口吧!这里简单介绍两种寻找人口的方式: □全局搜索标志字符串

』】

□设置Ajax断点 ●全局搜索标志字符串

·可』口』曰{

-些关键的字符串通常会被作为寻找JavaSc∏pt混淆人口的依据’我们可以通过全局搜索的方式 来查找,然后根据搜索到的结果大体观察是否为我们想找的人口。



(』

重新打开列表页的Ajax接口’看一下请求的Ajax接口’如图llˉl40所示。 ×■



(●

心a★向●

同5c「ape

■■■■■■‖‖』■■司■■可

…=厂■■▲?●硒蚀

…←2〗』·0〗尸】“·〗“84q〕

佣…面p…已w“t电门0皿…电冗■5幻「19加 v…「弓

三 』

……正巴

……b泳“

…唾丁0陋7p睡№0的V】呻 ……■…■u沁

c啼…2705

|■圃硫曰雨雨■

……呻umt』αUj□醉

{沁/塑……缅阳/a回Mem℃佣

m蜘“T·uJmNm10;的0锤α丁

图llˉl40请求的Ajax接口

这里Ajax接口的URL为https://spa6scrapecenteⅣapj/movie/?ljmjt=l0&ofISet=0&token=M2ZjNzBi

■|」■●]』】‖』■□】』‖』』■■■■」■■|』‖』■■□】■■

YTg4M)k5OGFmMjVkOGU3NmNjODFlN2NjM〕ZjNDgxMT八xNSwxNjIzNDklMDAy’可以看到带有 11刚it` O仟5et`tO低e∩三个参数,关键就是找to代e∩’我们就全局搜索是否存在tO长e∩吧!点击开发者 工具右上角的“三个小竖点”选项卡,然后点击Search’如图l1ˉl41所示°

■■‖□∏|■□|尸〗』|■■司』』‖』∏|(■■■】||』■〗‖|』■■

』刀…叼『∩山…』1卸侦mAy ……面

JavaScript逆向爬取实战

ll.l3

5ll

瓣霄噶……ˉ匣二二二== ÷令◎

●■…霄吨……tα

乃●§



同sc「愚p· 蹄齿

磺"●

田●

匡江…■



鳖……到到』凶气型翻



0

俺墅蜒…||M′……—塑_ C饵…Sαn呛卸

0…α伐

■∩…VOhG巴α■■b℃◎四乃…m

◇广

…击~

…曲锣…●镭.… !

∩·中≈唾占∩t〔p5;//9…·sc陋pe°c的te厂/印』/mγle/?M■1t墨〗“o↑↑蘸曹!山略ton印喇】乙〗门28』Y



如o`胸

广

Mα●泊酶

9mc刮‖R

……噎…匝丁 —_

≈→些^=■z翱m

脚呻



……·惩`…堡〗1】·9】·1“『19●』44〕

…可…已tf』ctˉ◎「』βm韶沧刷ˉcm■仔=◎「驹亚

牵凸=

$汹屿■α加必

寸∩…呸℃●剁●…■

…………9 巳……

呵西唾mN乃哼门韩0 ………r…N】C闪…

A“……湃whe$

.

№…诀了』”5丁p…p"丁】呻

…抵砷‖凹唾…△蛙雪A ·=二二■=■二 . : 9‖.··

…k…uγe



c…协…呢】70S

……酗K骡.



·罕L

x飞乍

_-[豁…钟

ˉ—

ˉ汹

j∏』Ⅻ0细7…″j】“《〗mAγ

~…



×

;

α…幽…咖嗜



(m

0



DQ◎贞■“尸■曰□

× 片…钡■ ∩■弓…『妇■p饵咱● 钉m■m ‖预■甲 cOQh妇9

°°,°°…°·°α

…伊F= …负=



l矿"涎皿●.

仓 仕

v

:寻胸·da锤u厕∑酗颧跨cSs…汹…Fm`…Ws闯■獭…m梅■"斡…*■履…■

…=…}. …峪…e●· …~……

p

p■脑呐…似汹洒γ…m



守α

c…协.…■呻`1Catm∏/j□咖



马饵胆/5争9爬汀……“tD 】2J0m2皿1】0:瓣:·∑α丁

鲍′跑5…

-~—===■==巳

ˉ■≡■=ˉ■· ·



搜索功能

这样我们就能进人全局搜索模式,搜索to促e∩,可以看到的确搜索到了几个结果,如图llˉ142所示° 囊蘸●回约…!… ~

◆◆◎



_

▲…0……ˉc可…



嗜` ☆

冉●

_—

同5c圃p·

捣阻翱瞳『萨…e‖M……

9°5

=__~

履田函哺m

◎α…S…■…喇m宜



■ ■_

堕●—

v







.

□ ■■

□ .

□艺

§哟……×Ⅷ呛沁悔 _

←勺







巴}吨幽■u知□M…EcSs…删匈a′泣Ⅶ…ws胸…“m■"■口°c隅■c°…: 日蛔c檄■∩··…

酗》潭……d铡鹏′队B川B…宙尖′H…■ ∩……鳞 、°

=—

≈粒∩■……询αγ ^呻焰…叼泡m呵·

攀O守α田∩…Ⅶ鞠臃……灼.α池℃

×*

仆 ˉ ■ 厂 |『‘卜「■■尸′『’■彦·卜『■日坚β|■厂β△■似『|ˉ仅尸卜|侦■■’》「■厂卜厂勺■■尸广■卜已■β|)』■■卜

图l1ˉl4l



k…T和p…

._←-

≈ˉ_

°= 二

×

■■

m _■

◎o

………~幽岳…田率£…爵仁几…`航孕!酶.查=?:刘蕊".。 γ…亿≈砂棒酌……p触伊m洒αm太.↑…蛔?r5m叮hⅢrm儿山A…7α·ˉ…4`m酚们000‖出饼泊■耐m儿…9‖3。缸q睡↑酗.豌…↑咖‖…0.∩■…H ■咖∏太.m…万…‖归~°胡~.兰

m…T仍

`…亿m审卜咕吨……甲"Ⅷ…逝αN■巾Ⅷ…■1《,00物0巾■…N仰…~呻0OwLms‖咖7刚!u●●吨比《w■ˉ如硒M6■…‖比测嗡趣◎》,ˉ山↑曲02匪ˉm3‖比了α

6……m0…■ˉ四…廷恤■…血0……"僧…‖/…坝酗m…山叼ˉm?…mm0Ⅶ(m牺帕…·:畦‖↑…k割γ可.0哺7m7它■ˉm0…… 嚣.渺叫曰典Tm07c~…v…獭■…ˉ…0理……mm…〔………■℃Tc●Ⅲ札酞幽了凹F■…扑巳洒)≥体T;认贝m四0 .…ˉm20…·山冯□y6

3? 。.』℃q吨■…m0…】∏m■仙挡蚀?…巳■m!≈m0由…匈吨…牌Ⅵ】…0.■■■悔ˉm↑…□m哇叮P问a街皿ˉ啪『…酗画…oIo…″R.0又】

…伦….尸…5…由U柑■如2恒

图llˉl42搜索to促e∩的结果

观察一下,下面的两个结果口I能是我们想要的’点击第一个进人看看,此时定位到一个JavaScrjpt 文件’如图llˉl43所示°

芦 | | 》 | ~ 尸 「 ‖ ■ 口



』』||(■■司|■■‖

第ll章JavaScript逆向爬虫

5l2

囱陆…0…

■■可{』■■

◆Q◆ ÷…G



x

旨 ;虱★ ▲●

■B…·西…cm℃「

同5e蹈p·



≈帖……晒y=L……

呻响亡 眠田贮∏■m≈…助m■呻响亡

×

=_

P…尸…顿γ 渺

哪00

…霞…γ0…尸材 囤α血*霹↑…闰

§

斡o

敬 ■



■【勺唾坤已cM$◎呻‖】钳1旧山[c时…c■$咖pp∏↓‖]》【vmgh·M[|O〔‖mk-】9c0 pW眶们 1 《■1咽≈【勺瞎印己《

■□呻

■生■≈=■

,◎…■……恤

p■…

□Qr』 .

p■恼们



■…

.卢唾】…

v心尸 va钾■…内0■

b……“扫

■c′M*产『艇鲤吨.·啪‖|

■哟…

h咖呻叫…7即.·d函 ■◎"皿*扫n田■刀…

.X例颐…〗a…汹 户m肌…已

■0向叫

F…儿■呻…

◆◎凹.呵n酗↑.≈

■匠…uBmm……$

P◎p?镭何啦汹M斌 BcD否伴.小扭件

《}凹↑●↑·砷m獭4鲤

c跨穴彻

图llˉl43点击第一个结果,定位到—_个JavaScript文件

这时可以看到整个代码都是经过压缩的,只有_行.不好看,点击左卜角的{}按钮’格式化 JavaSc∏pt代码’格式化后的结果如图llˉ]44所示° ●●●

G

马 色☆ 内●§

x +

■■砷OBCm碎西…

回scr… 霸王别姬ˉPa『eWe‖仙yC◎"cubj∩e

|、 p

=~

罐田

…α

ˉ._

…=…≈负 睡…们 ≈蛔Ⅵ■画…mγ……m…



_

p■…

D■↑咆■ p■Ⅳ『旧

宁凸尸

■钳"闪咖-4…7m■q函 卜卜》

h口`凹肿呻…乱汀□呸 ■仙…

◎四∏哦m吨

′■

.′

§

×

≠@



1了9

》)

w6 【》′ 17日

} t油∩0】(?山〔【1m{£x〗酝…》 t∏·∩0](T哟〔t1四《ˉβR1涵…){ v■「■…约£Ⅸ1…【0凹t● v■「■…■£Ⅸ1…【°凹t●『l 0彝凹4纯1■ ·又≈『‖厂0bol(O0 』 0■m“4绅1■=·又5…『0厂0bo ’=枷90拓凹■ ,=枷珊拓亚■ 0x…l0t凶∩ 0x…‖0t凶∩10‖;

179

0…“71【‖\钞●d闻′] 0幽“y1[『\钞■d1叼′‖ ■ α "·贝1, 出0H1?

】8O



C哑≈=■

…鱼

20] 1辟 】8凸 1腮

》l

咖““n[‖t◎t●lul m““n[‖t◎t●lul ■

凶…

0鳃?6』2; 0鳃γ6』2;

T日……响

》 }

L

□≡

》 ’p~咖1bC■6『 ≈0x1bC867 二 中加2?b2鲍

10》 1咽

p=·独1…27

!89 】叫 〗91 1OJ





凸x+‖翱『…】6…憾

(=On】】d7“《0c■9cp》′

=…(le醉5□ 》p p的船= =G渔初“(』酶770 )〗 ≈m劝7C11 ■帖】eCt《=0mgk27[,■‖1》『←0x1江“『 p =OⅨ07卫2d′ ≈0 ∏ P…[= ’ =m刁b7C11 枷’由?优[0“7 枷2〔b3·c[p“7…[tt】 ■ Ox∑h了但】1(0守…7〖凸ul8 e炯止…口…■吁f…·

·尸↑.∏喊u■L哺

1O∏

》0 》d

·汪俘′}…

TM 1蝇

$蜘白5么; 『帅ct』咖(= ·GO▲5么; ?咖(t』oRLm〗76b12『 ■印1尘?81′=·N由?10↑)《 出u置03t「1〔t0; 负u白G5t「1〔t0;

〗略 29J

γa7£丑】7乃79亡

l鳃

夕x0■C3】5〖0●0 ] ;

2钠



v…

=·泌▲“71【0啦邯e刽】 =·泌▲“71【0啦UⅡe知】 ■m蜘四1 ■m蜘沁1’

101 〗a2 ■■

■……鹊尸

1汹

08

0→出≈c31■■

Ox中7m↑《0仁曲?‖)

0又“7】■↑[0∩,】(0x2ws疮)〗



2佃}‖ }‖》; 201

[硒咖0 刃0凶…s

■■」□`■■‘■■□】■】|』■】■]|〕】□]α

■◎……妇西





v□吨

…Ⅷ…扫| E…肌.‖…咖、…Ⅷ…狼 |创"月次ˉ!…a.。则捧彻向Ⅷ… 目|

、 ·

……酮γ 睁 —

。]■■】□‖||ˉ■口|』可|□■

●●● 回sc,mQ|…

」□‖||」』·∩·|纠·』ˉ■| ‖』■■】||』"々||·刽|则□■引』|曰■□■司削·||

尸知吨



』 | | | ■ ■ 】 |

霸王别姬ˉ尸a了ewe‖ⅧyC◎∏cubj∏e =~

α…『γ凸

| 」 ■ □■■ 划

图llˉl44格式化后的代码

可以看到’这里弹出来-个新的选项卡,其名称是“JavaSc∏pt文件名+:fU∏natted” ’代表格式 化后的代码结果°这里我们再次定位到tO低e∩观察一下°

]」

〕 ] 日

战 实

取 爬





。工



·

◆■■■





γ

穴已





■」

〕 ] ] ]

可以看到`这里有11Ⅷ1t`o仟5et`to代e∩°然后观察其他的逻辑’基本上能够确定这就是构造Ajax 请求的地方,如果不是的话’可以继续搜索其他文件观察下° 现在.我们就成功找到了混淆的人口’这是一个寻找人口的首选方法° ●设置AjaX断点

由于这里的字符串tO代e∩并没有被混淆,所以 忙

0‖^昔 T ◇。泌@

上面的方法是奏效的·之前我们也讲过’由于这种

修Watch

字符串非常容易成为寻找人口的依据’所以这样的

v◎aj|siac汽

№↑四‘』s.口

字符串也会被混淆成类似Unjcode、Base64`RC4等 的编码形式’这样我们就没法轻松搜索到了。

vSmp· №f…』s田o

另外,前面我们也介绍过XHR断点,利用该方

法我们可以方便地找到发起Ajax请求的_些人口 位置°

v日『Da椭p◎‖∩鹤 №婶…硼7灼 +

vⅫ∩憾印B……顺检

我们可以在Source$选项卡右侧Ⅺ!Metch 巨雨c…`·…] Breakpoints处添加—个断点°首先点击+号,此时

’D◎Ⅷ日妇°呻创∩t雹

就会让我们输人匹配的URL内容°由于A)ax接口的

庐α。balDst…s

形式是/ap1/‖oγ1e/?1jmt=1o…这样的格式’所以

萨匿ve∩tust啊巳旧倒kp°j滩

截取_段填进去就好了,这里填的就是/ap1/∏oγje,

图llˉl45添加断点

如图llˉl45所示;

添加完毕后,重新刷新页面,进人了断点模式,如图llˉl46所示° ●怕●: ×

◇≈…日…扫



点撼‖萨.户}点…h镭蹦蹦乙息:`】』辗;暂撬霹?γ; 』 ;



■●古由●§

0≡—







≡∏回 C『己pe

团 …琶



~…≡奎

§

x





§

广

—呻呻尸"…畸

砷…—……心■℃■…

………

囤由呻≈唾n…尸n…≡~C=匈P

q.曳硒G γ申芦●

;…[≡■m∏飘i鞭臼旷叭顽…睫‖〗L■m鹰q1『·pm呵…‘〗【o卯cm丁m`】叮mct1m(-m心n【呵umˉm…b7』:云墅岸三三琶ˉ №t卯吕〃…凸0■…●回0『』=【≈正p lm0■…柠p…·…………l

` ■ h

b■■[】0少心”7馒‖削皿咖玩「…‖〗■{哟〗阳「=蚀1c曲7c■“…刀〔陷》d→印5…〗跑■…0yγ[o□‖)《~·m…0O树 ■筐≈

p q

◆佃m雨

飞p

屯『.^.牢吨■目 P客写】碑g F.5÷

i2

…0-0.0乙●泞…可P山碑?完6几

■ ■ ■■

望…伊≈~·…b亨■甲q守叮.0. ·o

i4

~…扫●…〗

● ■ ■●

〗0

踏……

凸叶 1□

■几.尸■T■唾′.、?Pv.″·卜.

….…产≡■

M

r沁≈·■…↓T·.用丽.价.

lq

』0

仰…扣…

@…■ˉb≈…早.〃■…吕卜0

尸T

≡早

呜…■0…舌塑70■吵■0L.

2弓





趾蹿,瞳《熏薄7wj5岛…mj》…………x…睡圃mo…s· 愿

P日









…沈

《》u凹乃…■■勺肥T碱

…=饵呻

『比r山·q’郑早U咐庐h0·g缸目沪·

……气.;…尸ˉ G .气:T.小Fq.u■ =..·尸尸0 0 0勺 .

q·.刁■、″…b·→9·笆■·FG

.~°…..白…■.°.

图llˉl46断点模式

接下来,我们重新点击格式化按钮{}’格式化代码,看看断点在哪里’如图llˉ|47所示。这里 有一个字符5e∩d’我们可以初步猜测它相当于发送Ajax请求的—瞬间。

第ll章JavaScript逆向爬虫

5l4

■乙











×

常睦鳃



QQ仓力●§

凸■~ ==宁≡o≡

·G曙

G幻…询…跑……≡……婶… _气

p…$…乃

i

_

_缉…〗℃l摊……幽

《〔■牙7 《1■牙7

亏□…

0〗∩°■

△】囱 d四Ⅶ

K铲γ《 Uv《

.睡呵 p■■

糊 4,D 0

』〗U 』

□1Un Oγp

》 》

0

皿】0凸1…↑?■

,哩由 vc些←凸

·麓藤1…舅……叭….测》四

二膨lj贸卜蘸:{翘愈驴淄搬蛾{镭剧棚患||优了筐瓣爵!

凸〗α;y △1僻’ ■】0●

p◎…哄』Ⅷ件

1?M《{臆,ˉ温撬mⅫ·门印…,洒·0》 ({耀ˉ温闪罐mⅫ,′·甲…γ…·〗)

′……·=`宙o,ˉm4·乙m[`°呻…m·…厄q′·o,』‖ 6eˉoz屿e∏凰

鳃■早 d】0e

兰气@Ⅺ们伍■…

M《m〃…·

^^…y 田……

020毛■

O~→≈空 ●绝忿.鸭…7 °”V←盆蚀“

43仪0 0no且

…-=ˉ苯趣蹬撼……F:

≥唾…稗…0

』〗Q·》

0】庐凸 秘≈3 d且0■巳 qⅧ仙P

》 ]5

………≈啦■

杠…3凶

…<…≥

》 ;

≡……~ q

0Q+■

》·

d〗中O

0唾30? ↑顿c【』面L·…□=m…】《

≡-一

n2■■O≡[0…7t$口】■ 』幽; 马吕眨□ }p ■;m了 °江知·且 ↑…t呻…m冗】0尸~●,』…田) 《

▲〗悠§

…刀2[℃…「th,‖●■凶…】《.c…p》β

…础≈■

……幽?…

两一

→→…金『m0

……~7≡‖汀

》p

己】●铃

‖」

;雕

?宁铃@ 〗→

O■囤



……n°…GT…ol ←…必n,…G『…Q‖■=…码$1‖0「m睁……o】; ●…卫D1[0『m辟my时0》; ≈…■■唾盯k = }〔●t山(户】】=)[ }〔●t山(户】【…)

丛l■ 丛l■ OO

出…

少^分

)′…幻m●尸

≡m▲…n0≈O…了南0l0

0〗∩o■

U〗■·V ·〗■.v

V·…四…■■

×



■■

巴中

咽…山…w…斗尸…奋……‖…扔…~7…‖≡沉

…』↑……辆泌

……『沁

狠…空一.7≡泞$



图ll司l47格式化代码

逐层调用的过程’如图llˉl48所示∩ 词

e皱灯…凹m泊









· ‖











×



||

前面我们说过怎样回溯查找相关逻辑的方法°点击右侧的CallStack,这里记录了JavaSc∏pt方法

| 函℃m…墅……~…v… 唾

● 『



●·… 7◎ b■唾 ◆■呵

P■尸

■…馏 少◎G休,′』恬件

!

瞳m■亡=…7…?扣

窒=o≡圭Q仓…=

▲月△哩J】[‘w』t怔…mⅡ■!■·}轻《…蛔 ▲月□…】[d讨』t邯「…爪Ⅱ■l■·}巴《坪田 幻[0■B寸m丁P…〖山比0l● ?0p0)0 ……1[·……T…o】〗 …■…1[ o≈D…T…o】〗

←…m『O……凹Vy沁·]■■m

》mt毗『户』』V^〕《 【P《。』蹿0 0……塑】《‘厂●β倔 t●…≈吨0T=;

』O●1 0已■.』 q100

但p自》

】)p

0■庐$

…… ==←?…≡…



)』

q1忻Q

=7~唾

ˉ隅蹿|稻哪‖‘…

嫂三:二ˉ二二产

∩…≈… …厉蛊…….。…ˉ 二 ■里



『 》■



′呐〕‖0 ↑咖Cm响…d =●皿…c』《 ≥匈…{、■…穴】0‖ ● ‖0m; 》0 `江皿·8 √…tⅡ“儿h蜘门2′字·x…鳞′ˉ0心酗思a》(

~~早 啮一

←加蜘九M℃…r【6’)丛=·四…】‖pC澎n』6

0`比.『

乙■电●△

曰唾】……

mQ蛔E仍(←■0…r…穴0‖‖‖p

810·昂

0〗卜■

=磺□

G围……了 .…玲}…

鳃鳃↑.舞{憾摊〗裂』d蹿}!

■】庐i

d1·dQ 毗仅· 0‖田; 哑出]

守″=

′hm6↑』“°…【y…v=…趣』l砂α…

々Ⅻ泊

hn碎】〃…々mr…△cm【■〃坤叼…出『 1』■』r叮l≈Ⅳ和…t…m18助≡`…℃但 p恃



61●▲ 々2a=备 $沁§y

61衫p ▲8卜F

■…3』●=m蹿面=

↑『γ《 ↑『〗{

Q】止0

d〗0PD

×

p.^9 ! 心.芦@

……≡0…x

■p■

08Uo】 d出口g

四…

h

…凸γ四 巴…必

…牢曲

图llˉl48

堑≡

…★.↑…^Ⅶ心尸↑

鳞心‖…申0《…隅o7 女→≡了…~『丁『 -

『.,‘~喳咆

CallStack

当前指向的是一个名为a∩O∩y‖Ou5(也就是匿名)的调用’在它的下方显示了调用a∩O∩γ田Ou5的方 法’名字叫作0x516]65,然后在下_层就显示了调用Ox2O99d8方法的方法’以此类推。我们可以

继续找下去’注意观察类似tO恨e∩这样的信息,就能找到对应的位置了。最后,我找到了O∩「etC‖0日ta, 这个方法实现了to促e∩的构造逻辑’这样就成功找到to促e∩的参数构造位置了’如图llˉl49所示°

」□]|』勺』■〗‖|□]‖刻‖‖』

虚句

ˉ犁 …泊

| 『‖ 「 }

ll.l3 ■■■F≡『逊恿厅—_“ 爷 ×

JavaScript逆向爬取实战



5l5

~~—

●≡…障…

包☆ 毋·☆臼●『

…n…仍回

『| 「

「} @

;

×

吵口^0 ↑…体@



●≡另咖α…

■■『‖》 β 》 匹 ■ | ‖ 尸 | | ‖ ’ ‖ △ ■ 『

‖ⅧpQ『〃■睁·T亡…碟但舔q俭P′m1/■●廷7

D■』↑●】…Ⅳ■c…砍…函…》蹿…℃q■ p…

■四≡

龄巴』醚4〗{瑟费

◆~一J .~… ←四…… —丁,、~…

≡…… …←′叮一缚

≡学 =≡_7.-… 一 仁默≡一

噎~

当…

惹慧T二霉:



血二兰 ←T ″赣←…

p

to代e∩的参数构造位置

图llˉ149 p

「p

卜∩仿》广尸●■■‖‖●厂■尸户β■「卜

到现在为止’我们已经通过两个方法找到人口了°其实还有其他寻找人口的方式,比如Hook关 键函数,稍后我们会讲到。

3.寻找列表页加密逻辑 我们已经找到to代e∩的位置了’可以观察这个to促e∩对应的变量’它叫作0x2b4仟d,所以关键就

是要看看这个变量是哪里来的°

怎么找呢?添加断点就好了°

看-下这个变量是在哪里生成的’然后我们在对应的行添加断点°我们先取消刚才打的XHR断 点’如图l1_150所示°



】61

162

》》o

】G】

宙虹【仑=也D】《》; 》U o~O0=《叫〗《

1e0

165 1“

≡鞠鞠翻糕

〗07

》‖ ▲■厂△■≈凸■〗■‖『●■尸

加顶加加加加啊加加咀皿皿血咽唾晒

『·辨■□钙o】【 ·…00 o尸=08 《 《

…又0』p

B1妇1k· 9…t00蚀妇【oⅡ…·』o o@w≈t·8=p

■由■由巴0……00…们≈~?■

;:蹿『 0…·∑=…础

·=…↑倔■砷j锤t《辑血51c$2S[0●d)》《tmDL

》 》)[ o] ;

}》『 》





′ 『

■■尸|》『||

图1lˉ150取消XHR断点

‖【‖)|‖【止厂|》「卜|【『〔β‖『

这时我们就设置了一个新断点°由于只有一个断点,刷新网页后’我们会发现网页停在新的断点 上,如图ll_151所示°

] 『■↓伍

第ll章JavaScript逆向爬虫

5l6

心Q☆向●『 ″…铂…w盯§●≈

5靠『ape





.…

v◎………

:

昭□虱哗△…■汀…0尸

嚣 1凸7



p■■

〗5s

p汹户

】“

蹿嚣翰斋嚼蠕闺息司

【鲤

少◎任何小刃件

〗m

〗“

O…m…



■咆由

■=位0 ol~Gp

‖龄■≈0吕{

岭↓

邑…

]》′ 』’



×

0伊4^§ 『 命。 ≠@

…哄‖…a…α仁……虹

口闽∏办‖…吨望豺↑↑…彝

§

■≈≈…

°户尸0$…g锄■

P… v…

t∩皿l,0∏厂●七t№故■o‖0》〗

曰m硒‖…n…】‖…·户…]田



』=■可|」‖■■习|』■■■】■】‖』』■■』■]

愤田≈……鲍∏m…■………… 尸…←词

Q



0■0洱8bQ球↑6■鲍》mt《夕心】C鲤3[9■p!》《tb山归

勺乙……℃



□吨=°′…· 0………吧

X门 】力

O…h一

!·77≈t0; →·Ⅱ3… $t…00=出2熙70d

〗乃 1了』 】门

P僵呻进t■………





})

】而



=■

【汀 〗≈

酌】} ml

…T■↑…〕∏貌

…p成

图llˉl5l

网页停在新断点上

这时我们就可以观察正在运行的—些变量了,比如把鼠标放在各个变量上,可以看到变量的值和



·∏||||·司■■Ⅵ`·‖』■可

类型;把鼠标放在变量Ox51c425上,会有—个浮窗显示,如图llˉl52所示°





』■■·】‖|

170

!umt0 g tMS[011阑1t『1′ 『offset0 ; -0x3ad6日c′ 0to■e∩0;

0x∑M什d

图llˉl52将鼠标放在变量上,会有浮窗显示

另外’还可以在右侧的Watch面板中添加想要查看的变量,如这行代码的内容为: ’

0x2b4仟d≡0bje〔t(ˉOx51c425[|a′])(t∩j5[|$5tore, ][ ,5t己te|][|ur1|][|j∩dex|])i



」《|』‖{‖

可以发现’ Ox51〔425是一个对象,它具有属性a’其值是—个方法°t‖15[|$5toIe』][|5tate|] [』urr][|j∩dex|]的值其实就是/apj/Ⅷoγ1e,即A|ax请求URL的patb° ox2b4仟d就是调用前者的方 法传人/ap1/"Oγ1e得到的°

||

我们比较感兴趣的可能就是Ox51〔q25,还有th15里的$store属性°展开Watch面板’然后点击 +号,把想看的变量添加到Watch面板里面,如图llˉl53所示°

二·]】口‖·」Ⅵ‖□叮··‖■■』■‖‖●‖■

172 173



‖‖|■

5l7



■】·

—…

—它







……

×

≡…









砷喻

s敏a修e

′|卜



■◆



JavaScrjpt逆向爬取实战

ll.l3

G

炉啼

…■凸·

……■协……

—……心心■心



~肛…



厂■

▲● G【卜』















河口士m欧时■这曙酝郸



■「|「[■■「|■「}|■■『‖||●β}『『■尸■■||}伯■厂■【■厂■■广》【尸‖|》

∏伪

图llˉl53把想看的变量添加到Watch面板

下_步就是去寻找这个方法°我们可以把Watch面板

丁0x51Cq25; 0bje〔t =

■「|[■「卜「|}■尸∩

的0x51〔425展开,这里会显示的「u∩Ct1o∩[oCat1o∩就是这

寸a目 f-0呕f沤fg〈) argu碾∩t∑月 (.. .)

个函数的代码位置’如图llˉl54所示。

caue「8 (· . .)

1e‖gt‖: 0

点击进人’发现它仍然是未格式化的代码’于是再次

∩日俯e; 凹0x2↑7乙?9Ⅷ

∩p『◎t◎tγpei {COⅦ5t「ucto『: /}

●尸■尸■=尸{「■=■「|匹■■■■■■『|■■尸■■庐■■厂|[■厂||■■■

点击{}按钮格式化代码。

尸一prom-: 厂(}

,腥耀;『蹦绷[

这时我们就进人一个新的名字为0x〔9e475的方法里’

》_p『Ut◎-; 0bject

在这个方法中’应该就有tO促e∩的生成逻辑了°添加断点’

图llˉl54函数的代码位冒

然后点击面板右上角蓝色箭头状的Resumesc∏ptexecutlon 按钮,如图llˉl55所示。 ◇



●……





↑↑



■— 罕

×

_]

-≡

▲=●巴∑=T●←

■. 曲仓■●】

一~=哩亩芦甲-■

慧c了ap凸

』…■………











|}}}

■■■■【‖|||△=■=「}巴=■『凸■『

…}啤′肆糙…0

图ll=l55 Resumescnptexecutjon按钮



这时会发现我们单步执行到如图1lˉ155所示的这个位置了°接下来,我们不断进行单步调试,观 察_下这里面的执行逻辑和每—步调试的结果都有什么变化,如图11ˉ156所示°

】97丁

O;…t枷●11呻{◎

;

〗5厕D

四了9】醇M●{0…「∑d1;

O·■卵lw■呵●v●』1酌\=

1…

。α磕■广…了日 ■牺tm■上1№1巳≥

〗50】

γ∩; 乙响『av●1哇l″

1日m}ct』oM0x】“9790 =·Ⅲ】●泌1C′』x11cGm》(

龋雨F爵三票孺;;_二二二二:二望二.ˉˉ

=慧≡票:=ˉ

15w

一田创N门*ˉ…7m。…?m`婶帅∏mm;0翠 『 ■…

1…』…68『『O枕m亢g‖l 汀咖峪…《 0澎’●h虫1(4乙1h↑0〗·

m厂 (V■「=0Ⅲ帕醉↑↑■曲tMh『D…0](∩由0Ut二 —_=~=■—■=—

寸办↓耽灿B…凶

』5蛔{Ct10∩(■酗Q…阅『▲N33晦比』←·x沁…2)《 〗599如(夕∏“】3730■0x“↑b”D→m”〗u0》{ m2曲碑M03助O0)》〗

■■■α·‖』■□■■■■■■■纠■∏‖=■Ⅷ||■■{当·】||■■■凶·Ⅲ‖□司|



□u甩…·……·

图llˉl56单步调试,观察结果的变化

在每步的执行过程中’我们可以发现—些运行值会被打到代码的右舰‖并高亮表示’同时在Watch

(·司■‖』



第ll章JavaScript逆向爬虫

5l8

面板下还能看到每步的具体结果°

□传人的/apj/Ⅷoγ1e会构造_个初始化列表’将变量命名为Ox5bq「5]° □获取当前的时间戳’命名为Ox4810仟,调用push方法将其添加到Ox5b4+53变量代表的列表中°

□将0x829249进行Base64编码,命名为0x3ea52O’得到最后的toⅨe∩。

经过反复观察,以上逻辑可以比较轻松地总结出来了,其中有些变量可以实时查看’同时也可以 自己输人到控制台上进行反复验证。 现在加密逻辑我们就分析出来啦,基本思路就是:

□将/aPj/刚vje放到_个列表里; □在列表中加人当前时间戳; □将列表内容用逗号拼接;

□将拼接的结果进行SHA1编码;

□将编码的结果和时间戳再次拼接;

□将拼接后的结果进行BaSe64编码°

验证—下,如果逻辑没问题,我们就可以用Python来实现啦°

4.使用Pytho∩实现列表页的爬取 要用Python实现这个逻辑,我们需要借助两个库:—个是h己5h1jb,它提供了5ha1方法;另外一 个是ba5e64库’它提供了b64e∩〔ode方法对结果进行Base64编码°实现代码如下:

二 ■ ■ ■ | ■ ] ■ ∏ 」 』 ■ ■

i∏甲ort‖己5∩1ib j『卯orttj雁 mpoItb己5e6q 十Io∏‖typi∩gmportlist0∧∩y j『印ortIeque5t5

■■■■■■司■=■■■■‖』■】■·■■|■■■司■■■■司‖■□‖■■||‖|■■■‖□■、·】■■」■』■|二■|

□将0x5M十53变量用’拼接’然后进行SHAl编码,命名为0x32d914° □将ox32d91』(SHAl编码的结果)和ox481』仟(时间戳)用逗号拼接’命名为ox8292』9。

■〗』■』■|』■■■■

最后,我们总结出这个to低e∩的构造逻辑’如下°

司|」■∏



√‖‖Ⅵ|·]』■■

ll.l3

JavaScript逆向爬取实战

5l9

I‖0〔X0Rl= !httP5://5pa6.scr己pe.ce∩ter/api/∏oγie?11『mt≡{1jmt}8o仟5et={o仟5et}8to|(e∩={to|〈e∩}!

[I‖∏=1O

0「「S[丁=0

de+get—toke∩(arg5: [i5t[A∩y]): th∏e5ta们p=5tr(i∩t(ti|∏e.tme())) arg5.己ppe∩d(tme5taⅦp)

s1g∩≡ha5h11b·5∩a1({ 」』·joj∩(args).e∩〔ode(‖ut千ˉ8!)).hexd1ge5t() retur∩b己5e64.b64e∩〔ode(0 ’ 0 .joi∩([5ig∩’ tme5ta∩p]).e∩〔ode(|ut+ˉ8‖)).de〔ode(|ut千ˉ8‖) arg5= [ 0/api/川oγje‖ ] to促e∩=get=to长e∩(arg5=arg5〉 i∩dexur1=I‖D〔X0R[.+omat(1i∏it=[1‖I丁, o仟5et≡O「「5[丁』 to促e∩=to代e∩〉 re5po∩5e≡reque5ts.get(j∩dexur1)

prj∩t(!respo∩5e‖’ Ie5po∩5巳j5o∩())

我们根据上面的逻辑把加密流程实现出来了’这里我们先模拟爬取了第一页的内容°最后运行一 下,就可以得到最终的输出结果了°

5.寻找详情页加密|d入口 观察上-步的输出结果,把结果格式化,这里看看部分结果: { !COu∩t, ; 100’ 0re5u1t50 : [ { 0id‖ : 1’ |∩a|∏e0 8 |霸王别姬|』 0a1ia50 ; 0「aIewe11"y〔o∩〔ub1∩e‖’

‖coγeI0 :| |)ttp5://pO.|∏eitua∩°∩et/|∏oγje/〔e4da〕e03e655b5b88ed31b5cd7896〔十62」72。jPg0▲64"ˉ6“h-1e-1〔! ’ |〔ategorje5! : [

′}

|剧价0 ’ ‖金悄‖ ]’ !pub1j5hedat‖ : 01993ˉO7ˉ26|’ ‖∏i∩l』te| : 171’ {5core|8 9ˉ5’



!regiO∩5! : [ !中国大陆’’ ‖中国奋港! 』■■■■

}’

0↑↑ }

这里我们看到有个id是1’另外还有_些其他字段’如电影名称、封面、类别等,这里面_定有 某个信息是用来唯-区分某个电影的。

但是’当我们点击第—部电影的信息时,可以看到它跳转到了URL为ht印s:〃dynamjc6.sc!H庐.centc【/

detaⅣZWY洲CN0ZXVxMGJ0dWEjKC0lN3cxcTVvNS0takA5OIⅪ】5Z2ltbHlmeHMqLSFPLMtbWIx的页 面’可以看到这里的URL里面有一个加密jd为ZWYmCN0ZXVxMGJ0dWEjKC0lN3cxcTVvNS0takA

5OHh5Z2ltbHlmeHMqLSFpLIAtbWIx,它和电影的这些信息有什么关系呢? 如果你仔细观察’其实可以比较容易地找出规律来’但是这总归是观察出来的’如果遇到一些观 察不出规律的’那就很麻烦了°因此,还需要靠技巧去找到它真正的加密位置。这时候该怎么办呢? 分析_下,这个加密jd到底是怎么生成的°

点击详情页的时候’我们就可以看到它访问的URL里面就带上了ZWYzNCN0ZXVxMGJ0dWEj

KC0lN3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLI八tbWIx这个加密1d了°而且不同详情页的加密 id是不同的,这说明这个加密id的构造依赖于列表页Ajax的返回结果°因此,可以确定这个加密id 的生成发生在A)ax请求完成后或者点击详情页的—瞬间°





●…=o与

—…



一陆

■=『…

● 毛

同sc『…



7对1妒≡72…∑值…呜醉●《<□飞■l=忿@1■】DQl已亡@1门7「已m=r≥

◆甩』v…广勺≡m∩咖7贮[l……●l…呵皿■≥t 1$~…面啼′>

尸《

■l~

蹿….cT已曹1

ml“〗 1…7皿; t钩t幸山FQ【…;抄…

■■8‖『{m■=●l<●…蹲P ●<』γ缸t汗…↑№【1二2三■凹G1叶面护



T』…而



.≈

■β…』f→唾u蚀『

●叼山…D乍7…硒c凶p可瞬●`吨耻l·l…卜】0Ql和l电6电●1≤□l←咐·0电·1回山■≥≥/刨忱

泞…





……【■对Ⅵ…N

一约 叼碗广;…屈叮』

_=

》 ……·』Uo■l丰■皿兰≡≡·↑·0…

勺′■≥

°■l.七■ro《

b凶1■凹■●=寸■河≈7沁〔【仕m白幻…宇『…#~叮J0加> p<加■吵℃←p■“c『比C0·■碑已v=■1刀0●.~■/山沪

…幽≡…』〗

时fmp←7凹郸尸竿0

■≤』0山$任v=n…’山〖h6●鄙】凸…』∩0●■守徊』■> √dt协

m「由: ·毕b◆1四…09i

…吨7……

,甜76

陆〃t≈!p·1→』

◆■』■=.’…↑油【匹砂铲■l←t■to〗=切l=Ne1■ml=四=9■l=■lˉ…巳Gl回l≈■□拈←句/0哇

…~■叠

『■■∩P0

,=《0~净卜~…

√■呼

1…』『…pp刃;

叼■』诉



″dtm

.p.由……哇…^……唾…伞……蝉…m≡0$≡=俗巳…

……6』… ●

图llˉl57详情页链接的∩re+

由此可以确定,这个加密jd是在Ajax请求完成之后生成的’而且肯定也是由JavaScript生成的° 怎么去找A)ax完成之后的事件呢?是否应该去找Ajax完成之后的事件呢?

可以试试。在Sources面板的右侧,有-个EventListenerBreakpoints’这里有一个XHR的监听’ 包括发起时、成功后、发生错误时的_些监听’这里我们勾选上ready5tate〔ba∩ge事件’代表Ajax得 到响应时的事件,其他断点可以都删除’然后刷新页面看_下,如图llˉl58所示° Q匪●

_…



_ ▲ — ≈



…—

恤—

sC『ap●

_仓



■舀-—



■】■·■勺||‖·■‖(』|■=·■、||』·】‖」】Ⅷ】||」■〗‖||■■|||·司(|{■■|||纠■可‖|●』□叫」■』·」■」】□|』■·°■‘·、‖」·‖|』■』■`」■‖■】」』■‖|■■‖|‖」·』■■口■■■|□■

虫 爬





β

°∏





γ

∩□













β 日∑

为了进-步确定这发生在何时,我们查看页面源码,可以看到在没有点击之前’详情页链接的∩re十 里面就已经带有加密jd了,如图llˉl57所示°



●■■

—■

■=

0◆‘^9



q■

t学≠@

·豆壶~ˉ ″



…=.7!、≡已=

≈1 P=T



t=…0』】

≈ …Q

_… =-T-=?辑



啤‖…

≡ … … 函

一γ ·一≡

◆△旨, 0←0龙…〗》 卜==~■ …←V..′≡=



……

■≡-==

ˉ≡≈=…一.7. {=→…

ˉ…→豆一

… … …7 … …

金=7 」~≡

『罐总,了蹈h!,愈…′‖′

…庐… —

·6t■回ⅧtD旧…■l00tm吮『仕m·‖o

驯酗

;鳃?}:ˉ墨凛?,

q啤〗

△0斡i

…≈

0…o8≈凶m

4Ⅱ凹】

″=

~7{ 《砖…

…`0……?0…列

…户……泅稗捧] 坐≈…7′|=琶‖w

》5 堡

…w0』″…0≈值…0 ≈…》,

0l…

“…mp罕M〗

Q1■9

锋入

-—=-… ·"匡 ·

^一

LF≡▲……酗

~…■==… …咖



… ~■■==■~

图llˉl58刷新后效果

土……7 、牵乙『·

=『 ′`一0 …毗=▲V 、朝匡… ←■■

●■▲

■■



■←…■←撂O

| }



= … 0…

d蹿

【‖■■

p◎】仰,′』瓣



■们…



≈_……七心

≡幼…

D■尸

g涡 淬 ■…厂t■』‖●‘ …

ll」3

JavaScript逆向爬取实战

52l

可以看到此时就停在Ajax得到响应时的位置了。我们怎么知道这个id是如何加密的呢?可以

选择通过断点_步步调试下去,但这个过程非常烦琐,因为这里可能会逐渐用到页面UI喧染的-些 底层实现,甚至可能找着找着都不知道找到哪里去了°

怎么办呢?这里我们又可以用上前文介绍的用于快速定位的方法,那就是Hook’这里就不再展 开讲解原理了’具体可参见ll.3节° 那么’这里怎么用Hook的方式来找到加密jd的加密人口呢?

想—下,这个加密id是_个Base64编码的字符串’那么生成过程中想必就调用了JavaScnpt的 Base64编码的方法’这个方法名叫作btoa。当然’Base64也有其他的实现方式’比如利用cIyptojs库 实现’但可能底层调用的就不是btoa方法了°

现在’我们其实并不确定是不是通过调用btoa方法实现的Base64编码’那就先试试吧。 少

要实现Hook’关键在于将原来的方法改写’这里我们其实就是Hookbto己这个方法了’ btoa这 伊■尸‖‖|■■「‖||户}■【‖|卜|庐『||尸|■「「(「■「‖|》「■「|■[■「「■【■『『||■■「【厂|||但『「||·卜|||■「|■「|||‖■『「‖|沽|‖|■■『‖『‖■厂|■「■「|‖■卜|●‖■「『[■【尸|上尸》【■「}『◆【◆|{■■「卜‖[■∏【■■【》〗〖■■【■厅『■

个方法属于w1∩dow对象,这里百接改写w1∩dow对象的btoa方法即可°改写的逻辑如下: (「(』∩〔tjO∩(){ ,u5e5trj〔t|

十u∩ctjo∩hooⅨ(object’ attr){ γar「u∩〔二obje〔t[attI] object[己ttr] =+u∩〔tjo∩ (){ 〔o∩5o1e。1og(‖hooked’ obje〔t’ attr’ 日Igu们e∩t5) varret=十u∩〔.app1γ(object’己rgq爬∩t5) debugger

〔O∩5O1e.1Og(0re5u1t′’ ret) retur∩Iet

} } 们oo惯(wi∩dow’|bto己0) })()

这里我们定义了_个‖oo长方法’给其传人obje〔t和attr参数’意思就是Hookobject对象的attr 参数°例如’如果我们想Hooka1ert方法’那就把object设置为w1∩do侧’把attr设置为字符串a1ert° 这里我们想要HookBase64的编码方法’所以只需要Hookwj∩dow对象的bto日方法就好了。

∩ook方法的第_句var+u∩c=obje〔t[attr],相当于把它赋值为一个变量’我们调用十u∩〔方法 就可以实现和原来相同的功能°然后我们改写这个方法的定义’将其改成一个新的方法°在新的方法

中,通过千u∩c.app1y方法又重新调用了原来的方法°这样我们可以保证前后方法的执行效果不受影响 的前提下’在十u∩c方法执行的前后加人自己的代码’如使用〔o∩5o1e.1og将信息输出到控制台,通过

debugger进人断点等。在这个过程中’我们先临时保存+u∩〔方法’然后定义—个新方法来接管程序 控制权’在其中自定义我们想要的实现’同时在新方法里重新调回「u∩〔方法’保证前后的结果不受 影响°因此’我们达到了在不影响原有方法效果的前提下,可以实现在方法的前后实现自定义的功能’ 就是Hook的完整实现过程。

最后’我们调用hoo代方法’传人"1∩do"对象和btoa字符串即可。

怎么去注人这个代码呢?这里我们介绍3种注人方法。 □控制台注人

□重写JavaScnpt

□mmpemIonkey注人 ●控制台注入

对于我们这个场景’控制台注人其实就够了’我们先来介绍这个方法°这其实很简单’就是直接

在控制台输人这行代码并运行即可’如图llˉl59所示° ·镶鞍·…陪

"蹿瓣鳞…蹿` `″『

马ˉ α仓力@



辫陋■

.

、尸





_ .声

气γ

字ˉ■





^硒

=≡黔…≡

. `



叮邑咐

△………

七士亡

× ≡

,.

≈ ■气

蛰息!彰" ˉ



伊可









9





氓萨 力



【 V■



×中



← ,一 | 々 仑.

儡画韵叮也四……`…;;』 ′…j翰』 ……`… 、毛^笆龟钓 古 `

户怪百





-

k

$~

十』



宇/

‖=

泞 宅

_

蹿●唾

v●

烛…`v

萨…

7ˉ恤■』▲『『…k』“〔】《 叶●$【f几危tp

’q‖汇B儿□∩…{山》ect∏ ■七w》{

曲『↑…▲●b〕·吐[●tr「】 口

山】“M■tw』●0…Ⅷ乙《){ C…`■.]甸(p…m,0 ◎b』Gcc, ●tY厂4 乙『…℃〗 γ■「 厂■t■「骗c.印pM@b】■ckp ·『U嚼rO) …吠「

r…@l■0l印【∑「碑u〔tQ 』 f酞) 厂Gtu7∩「■亿

》 } 仍凹Mw↓…■ 》》『》

bTm□』

』 】〗81■……↑』…

执行完这段代码之后,相当于我们已经把w1∩dow的btoa方法改写了,取消前面打的所有断点’ 然后在控制台调用btOa方法试试,如: bto己(‖8emey‖)

回车之后,就可以看到它进人我们自定义的debugger的位置并停下了’如图llˉl60所示。 _—

·钻

炉■

讯·八▲

G





翱石霉而囱国噬ˉ

醚Q★

◆×●`~ ^

若巳「np怎

办●

』■·削日‘·‖·]|削]■{‖□』■可□■■‖』■■ˉ·‖』』■‖·】口

图llˉl59在控制台输人代码并运行

·引勺■□■■‖|勺·』」■]』{」』|· ‖〗‖』■】‖||』、·‖」■‖ 』‖‖■■‖|‖·』||‖《■可‖■|

第ll章JavaScnpt逆向爬虫

522

■】□口

‖』■■]口■】■』■』■■■·】·

| ■ 】 」 ■

学雪…→?…憾≥·罕寸→…掐………

………唾一___……

≠…—…………ˉ…………≡≡=………~≡

《》…q…泅

图llˉl60进人我们自定义的debugger的位置并停下

‖)





JavaScript逆向爬取实战

ll』3

523

我们把断点向下执行’然后点击Resumescrlptexecution按钮’就可以看到控制台也输出了 _些对应的结果,如被Hook的对象、Hook的属性`调用的参数、调用后的结果等,如图llˉl6l 所示° ■——

酿田

噎嘲辗翻`涵

豁《Ⅳ…N碱w◎咸

c钥诺o归 i

陋o ″

p锻扣『丁确副涵

瓤色《Tγ创γ

--_—_啊…~≡空当≡

v§°

知鞠l舱铰k婶$

ˉ≡可诌霹 默二

』隅崎

~→…屿乙=…肄…_=弄二寻≡哼滓==





l颠瓣蹿u幽



0蛔蜘截宜v

〔O∩s◎〖e。1吨《‘hoo戊ed0’ ◎bje乍t’ 己tt厂0 ■「gu碾门ts)



γar厂et=十Ⅷc·■pp1y《ohje℃t萨 a呵《獭总】Vt5) debug赡厂 cO∏S◎Ⅱe,mg( 0陀5uM00 厂et》 厂etM厂∩厂et





№oR(WmdOw′ 』bt◎a0》 })《) 《

b

13B37$18·9蹈u∩de↑』∩ed

》鸥837;们.9“bto己(0ge『们eγ‘》

鸡君37:铡"g77№o戊ed份恤Ⅳdo抒f】吼m“灯5m门咖衍′$e【厂』恤〃“门′“仁…门t;dock唾∩t’∩a“『 瞬卿′【oc己mm;{◎6at加n′=}bt咱

p

p

抄A徊四e∩rs!俐ge′雄γ,|′ c□l【鳃f 〔..·)′ 5’帧OI《…bo【·1fe厂·mr′『fJ

13γ378钡癣wghoO侦ed>恤∩o◎wf叫∩如W『她∏“w′ 5GⅣr比〃αo侈′如C凹晦′〗t『“〔U爬碰0 ∩a″g 睡咖’【Oc■t1”; 止o匣at加″′=}bt酮



仆A厂9【″门t5′秘g它‖沁γ"′ c6【【唾: ′..°》′ 5’酗◎〖《S″o【°1te心t@厂》f 厂′

卜』

咽:§了叫〗.

P



l3;3》碎1·







13:37〗41.





图llˉl6l















p



0







控制台输出的结果

我们通过Hook的方式改写了btoa方法’使其每次在调用的时候都能停到一个断点,同时还能输 出对应的结果。

接下来’怎么用Hook找到对应的加密1d的人口呢?

由于此时我们是在控制台直接输人的Hook代码’所以页面刷新就无效了。但我们这个网站是 SPA页面,点击详情页的时候页面是不会整个刷新的’因此这段代码依然生效°如果不是SPA页面’ 即每次访问都需要刷新页面的网站’那么这种注人方式就不生效了°

我们想要Hook列表页Ajax加载完成后的逻辑,对应的就是加密jd的Base64编码过程’怎样在 不刷新页面的情况下,再次复现这个操作呢?很简单’点击下_页就好了°

这时候点击第2页的按钮,可以看到它确实再次停到了Hook方法的debugger处°由于列表页的

β

Ajax和加密1d都带有Base64编码的操作’所以都能Hook到。接着,观察对应的Argu川e门t5或当前



网站的行为’或者观察栈信息,我们就能大体知道现在走到哪个位置了’从而进_步通过栈的调用信

b



息找到调用Base64编码的位置°

根据调用栈的信息,可以观察这些变量是在哪一层发生变化的。比如对于最后的这_层我们可 以很明显看到它执行了Base64编码,编码前的结果是:



e+34#teuqobtua#(~57Ⅳ1q5o5-j@98xyg加1y+x5*ˉ|1ˉ0ˉ们b1

p

编码后的结果是:



Ⅲ‖γⅢ‖〔‖02Xγx‖6]Od‖[jN〔O1‖3〔x〔Ⅳγ‖50taM50帅5Z21tb‖1『‖e‖‖qlS「p[「∧tb‖I×

b

如图llˉl62所示’这很明显。

p



P

) }





』■可■凸■Ⅵ■

虫 爬

向 逆











γ

^■











斗 日∑



#●…=田…

×





龋·…馋

』。磷惮

每…吼…

厂f

="龟



锄…撰砷.

嘴凸丙

嚼獭

×庭″蕊…

…·……

6

瓣 □万

…m…渺…

而』‖‖」 □■

■□

、.拇… …=…

…Ⅶ







……



……

……≡



§

×

渺^? 『罕芦锈 刃 w …



0

◆……

铡…尸

………

懦》了

…‖………y嗣n…↑揖掸↑↑ ≡…甜潍敏翻…亡蜘沁??

≈■望哪

……7呻硝…γ蹿摔0↑

倔↑c口二宁

图……y哟″啦0缉捧!↑

……挚私……《0…帖l 咖盛…拦队,……增嘲

r肘乓=心

碑臼由岭《…想绚?`≈艇`

哑■T凸≈Q

…〗“↑.≈…

纠■■‖ ‖ ‖ ■ ■ ■ ■ 司 ■ ■ ■ ■ ■

……

乃≡髓1`靡固 《凸…0

·】■|

蜘瘫≈…7^…ⅧT睡瞬r =……γ′ `…您?峪

m‖…肆…翻扭w蹿…`? ^加…… 触曙…≈′吗了瞬.…4凸…

………蛔…吼y. .恼…锤绸 …两…、γ…悟…

■■】』■

唾色q=丝争

□·■凶〗‖]

m4…『帜`

………γ…啥甫…

■■‖

面Ⅱ外Ⅱ《贾已}■‖·】巾△〕乃■Ⅱ



…···



…挣………吨户…雕`闸…

户◆

…″ ……_……心心唾心簿°·



… ……

心……j碗



‖日

图llˉl62编码后的结果

核心问题就来了’编码前的结果e十34#teuq0btua#(ˉ57w1q5o5ˉˉj098xγg1"1γ十x5*ˉ!1ˉ0ˉ"b1又是怎 么来的呢?我们展开栈的调用信息,_层层看这个字符串的变化情况。如果不变’就看下_层;如果

乙■Ⅵ|

变了,就停下来仔细看°最后,我们可以在第5层找到它的变化过程’如图llˉl63所示。







β

蛇巍☆冉●‖



…≡



廷≡尸

×











×≡

台=

蓬》《,撼慰输.堪

d

■广…闪子

℃■■→■■

守△

■■T■■F■

■ = ■ 】

寻:四飞嚼■孝

伊□丸Ⅱ

· 0掩“ 〖,

∑〈

悦唱.

■■■■∏‖‖■■■■■

~●』车

陆∏









割“



』 ■

尸 … …知 …』″蜘 ″…

四…牙…气…攫



蹿 』r』0 1rm 】Ⅱ抛 n』3 月∑汕



》(》p



0沁踞0 鞋泌0:g ?呻α』师Lm〕…’…铂2色c7■【, △呻● △呻●9t7ict『β

眩屿ˉ 泅唾 1〗鞠

g

-………袍 `^.………

=毗】】驰郭》[

…′…T蹿…,呵翰…≈≈…↑

锣皿凶7出

≈m】; =凸】站…《·酗do)『

◆刮m『……唾莎m……γ沁

丫叮

O Q

?咖忿‖

°

臼江№?≈鳃谰…?$a企2捶v

…0“《.笆 仲向←

》』 Q轮铂,已 心晌勺; ‖?碑GtA扛M曰蚌7】7腮0≈0…7qp遍■尼3团》《

……憋γ……?

(↑p伺《 (十“cmO∩k“4c7仙0严kc丁7OQ ←…蛔T』《

,

=……向噎…n7 .…巍l鳃

;

~Um…………姑机.…m…

尸m‖…辨…^知…V、‘‘…潞龄獭霹

}《铜狞}耀滥腰鹏纠严‘cwd`ˉm,·嚼…《`测膨》·》.ˉ帅…敷岿(·…" 》(吨§鹏鹤蹿;灌』·‖「°刷‖……《》《

”7…枷S独切●≈·Rx】汪{0ub‘M001@◎″屯T耗…0Mo画t…』I《』 ’£江】】213b■弊m幽‖U断C…●′0〗■巫g2■岭〔"韵tC硒ql《《



管心D…

…码

,,旷°辩望凝↑遥呼!腮{宗鳖瓣!{ˉ吨…‖{

珠H巾…γ·`…哩酶鳃围

《蛔*.…‰…b…骡骏倒

ˉ…沁联曲宁=…■丫. `翱…呵翻〗

q~Qx“佰5”■=·只yp7M6j‘0购″k【G`l

M℃0吟、…窥簿

雨蜘■浊嗓↑…?f…↑ 心…酗审`啼7‖≤输…Ⅺ孽

“ ·如▲田■□



~~丁一 =▲即 ■▲2c 眉y●1[·b,』G ·h』6归矽↓

y鼓′

…『

≡…》啮…卢▲…3γm萨.`…肉里…酗

■ …『γ■

钩…m纂阅串 ≈



唾占≤

O







穷~

图1lˉl63看看字符串的变化情况



ox5d十〔〔0是_个写死的字符串e「34#teuq0btua#(ˉ57w1q5o5ˉˉj098xyg1川1γ+x5*ˉ|1_oˉ们b’然后和 传人的0x2十O999拼接起来就形成了最后的字符串°



|·■{」』]√红□日|』∩』勺

1翱 1黎1 … 1泌3 li5办 1巫凸 1撼6

…嚼

……



蹿匡-—…」 〗2出7 妮铭

x

(』





◆凸

$巫】

【》们

§

嚣赐^? ?←≠鲜



0皿屿5“[o∩…11{吨■gY·】门回m】■l0鞠! 0盛神矽t■摊`酝『。M=·洱9咙马了‖》;

渔j7 12』U 21“



丁←●x揖n57h『·x0}… 狰………

少凸鸥■1『◇…b』t[…亡γ。‖〗

》p

》)》g





. -D凹01“1t●x7‖■ ●■2】05沤I

12】巴 凹沁

〗『幽



『斟α掸……

‖」■■■可|

;



·~四…=◇

幻—…心心心心心壤◎·

—……… 萨 ……吨偶……酣

@

‖{‖





』户‖|β『|‖《【‖八尸|







ll.l3 JavaScript逆向爬取实战

那0x2十o999又是怎么来的呢?再往下追一层’可以看到就是Ajax返回结果的单个电影信息的1d。

因此.这个加密逻辑就清楚了。其实非常简单’就是e+34#teuqObtua#(ˉ57"1q5o5ˉˉj@98xygj川1y十×5*ˉ !jˉOˉ川b1加上电影id’然后进行Base64编码即可。

到此,我们就成功用Hook的方式找到加密jd的生成逻辑了。

但是想想有什么不太科学的地方吗?刚才其实也说了,我们的Hook代码是在控制台手动输人的’ 一旦刷新页面就不生效了’这的确是个问题°而且它必须在页面加载完了才能注人,所以它并不能在

一开始就生效°

下面我们再介绍几种Hook注人方式。

‖‖

●重写JavaScript

曰e∏e∩ts

限田

借助Chrome测览器的Ovcrrides功能’我们可以

Cms。|e

「 ‖ ‖ 伪 [ ■ ■ △ 厂 ■ 尸

| page O`′e刷des 》

实现某些JavaScript文件的重写和保存°Overrides会在



本地生成-个JavaSc∏pt文件副本,以后每次刷新’都 会使用副本的内容°

s。u贮es

; 囤

固巨∩ab|eLoca{Oγe∏‖des●

}巍澈c脑…v°…?\_■■

这里我们需要切换到SourceS面板中的Overrides

选项卡’然后选择-个文件夹,比如这里我自定义了-

Sources面板中的OverTides选项卡

图llˉl64

个ChromeOve∏ides文件夹’如图l1ˉl64所示°

然后随便选一个JavaSc∏pt脚本,在后面贴上这段注人脚本’如图llˉl65所示° 闽



芍◆●匣山…l…







呵 =







_…

●………叮 ≡

■=’~■『■「▲■庐■尸■■厂■■=尸

■■■≡

—一



争◆◎





525

■; ●★白●!



回sc『…

儡」琶琶…|』啊c…∏″

9趣5 ★★古★★

…国

……

′〖

霸更肘碾牺大口.牢口口疆′w`分钟

瓣仆樱 『 !’”m趁鲤— 中

…蹿…▲………~…

§

×



;

旧…铲…呻国…抑们

牢唾…7…↑侣

Q……“扫



00≈』 i

G

芦@



〔}

p





||

[卜卜「|匹■■儿β‖‖「|■■尸

图llˉl65在脚本后面贴上注人脚本

保存文件’此时可能提示页面崩溃,但是不用担心’重新刷新页面就好了。可以发现,现在测 览器加载的JavaScript文件就是我们修改过后的了’文件名左侧会有一个圆点标识符’如图llˉl66 所示。





第ll章JavaScript逆向爬虫

526

飞 十

)×

F



■·向

●≈咆…·…■

固 5c泡pe



■尸

汾●

`锣辩萨

靛.;√!髓:簿哲 秽口’ .· · =≈

p

广 ↓





^? 鞠蝇^0

了 ◆ 牟.

x

产够

Ⅱ (0…【』m‖=加7】硅M7…↑h“→mD7】3刃{=哩$缅】0《?@r《v●「二0m】1硒.-咖“?"】G←m1G…→恤靶·0n‖●…-…

·□吨

】{↑mCQq邦『)《 O

…归

h■…



■■…

】仆





■◎≡=≡一

●闷

·…■t「』〔0□

S 已

-愚 卜…

T



p…

9

〗己

●……

n

G…畦一

12

n

〗d

】5



p严凸■0≈■乙



p…0=







…h〔N』m如0 `·tQ●0》



●……… ●……m…■

10 })《)



|』】|·】〗』』』〗】司』‖‖』·‖|·叫‖|』

p□琶 ■□… 卜□…

份□≈m D□……

·◎… 庐□…′…

贮◎≡ 伊□…

{》凹■0α…lγ3

……呵昌

步□〔四

图llˉl66修改后的JavaSc∏pt文件 同时我们还注意到, 目前直接进人断点模式’并且成功Hook到btoa方法°



引」

其实Overrjdes的功能非常有用’有了它’我们可以持久化保存任意修改的JavaScrlpt代码’想在 哪里改都可以了,甚至可以直接修改JavaScript的原始执行逻辑。 ●Tampermonkey注入



如果不想用Ove∏jdes的方式改写JavaSc∏pt来 注人’我们也可以使用前面介绍的Tampe∏nonkey插

曰咖`e∏ts

限田

c·∏so‖e

Sou冗es



户age

NetW·∏〈

÷芦弓 吴8

Ov邮qes



;

囤□app5e加4弘.js×

同∩甄…|…..°| 盅(…°"(…‘

件来注人,详细的使用方法可以参考ll.3节。

》u

开始之前请清除所有的断点,并且把刚才的

Oh吨『n由Vew|U蹈

3!(fu∏ct1o∩ (){ 4

Ovemdes功能关闭’以防对本方法产生干扰,如 图llˉl67所示。

『05e5t厂1〔t‖

5

fu∩ct儿O∏∩oO长(

6

γ己厂↑u∏C=

7,

ob]ect[己tt

8′ 9

co∏5o‖ γ己『厂〔

勺@

刮^仕■·∩尸

接下来,我们创建_个新的脚本试试。点击左

」■‖■■|||·(

图llˉl67关闭OveTTides功能

侧的“+”号’此时会显示如图llˉl68所示的页面°

|田腮…嗽

弘0切……吐

m…穿、唾m开m

^ 踪



=宁琶蒜T■; 洱



ˉ-尹和

兰…



~辫=……≡= P

2

p〃≈……



d〃●……l… 〃……=■/ 〃…O0

〃凶…械…?陆四函∏…』出γ…4■心论■m`口m…吨

〃…… 〃■唾■■



□…乱』{ ……;

//V四……≡

}刚

图llˉl68新建用户脚本

■●■■°『』■『$●仆□导■■

/′……叮加……m…

′〃●…V凶

■■■||■■|司|』■】■】】□□】■□■]‖■】』■∏‖■||·】』■】■■

=即媳附川舱州增饯

蓟■粉‖

‖·■■‖」‖』□■】口|{』■■■』|‖■■】‖|

寸■

囊顿矗用户■本>

| 0



0

ll .l3

JavaScript逆向爬取实战

527

我们可以将脚本改写为如下内容: //==05eI5〔riPt≡≡ ‖oo长B己5e64

//@∩a们e

//0∩己∩e5pa〔e

http5://5〔I己pe.〔e∩ter/

//αer5jo∩

0·1

//@de5〔riptio∩ ‖oo代8a5e64e∩〔ode十l」∩〔tio∩

「β

//@aut∩OI

Cer∏eγ

//O∏atc∩ //卵ra∩t

http5://5pa6·5〔r己p巳〔e∩teI/ ∩O∩e

//0ru∩ˉat

do〔uⅧe∩tˉ5t日rt

·

■ ■ 「

//≡=/U5er5Cript== (十u∩〔tio∩ (){

■■尸『‖|●■

‖‖5e5trjCt|

卜■=

「u∩ctjo∩∩ook(obje〔t’ attr){ γar十U∩〔=obje〔t[attr] co∩5o1e。1og(′「(』∩c0 ’ 「u∩〔) object[日ttI] =「(」∩〔tjo∩ (){ 〔o∩5o1e.1og(0hoo促ed’ obje〔t’ 己ttI) γ己rret =千u∩〔.app1y(obje〔t’ argu们e∩t5)

● 「 |

‖■匡■■◆■『「

debuggeI Ietur∩Iet

} }

广 》 卜

hoo低(wi∩do"’btoa0) })()

这时候启动脚本,重新刷新页面,可以发现可以成功Hookbtoa方法,如图llˉl69所示°接着’

β厂卜『

我们再顺着找调用逻辑即可° 印芦

0

∑ *

"|……

翻豆『·……

■『Q古砧力●:

●…x了……审

p



尸‖尸▲■厂■■尸■■■■

5c『ape

?

燃§



■『『■■■∏巴■『

…………,…

仲^↑ ?令渔

l…啤枷.雨止.忌γ团『…』Q《碑n> …出…■7厂…〗尸……ˉ丽L』田『6



§

×

{((c…■f’ 0●w∏Yp ‘…∩℃)→们』k∩ 《C■t≈l》 ‖[ ‖|…te=《户mdTk了1佰t·jt叮《?…●=… r…回t●)→佃浊∩kmt≈1》

佣 h

◆…



堡些二…

p仁■●→

【■〃 ∩m■〃/$cf…□…【■〃 0□』 艳■ ˉ宁0…曲屈创西 曲屈创西

·罕≈≈

臼口 0亡m『″/ 炳四0〃…兰“…0亡m『″/

’…



◆…睹=℃ 0u■t∏T7k〔t0

p…也…

巾〗“tD■ⅡkF)《 吨广↑…毛呛』●cT[●tl『l …℃·1叼【0↑四c,′↑…) 叼mtl●咏7〗·恼c[』m(》 { { ■j≈t′■tt『)

26》0‖》

v…[……血

◆□…

p◎…

【霞;囊§=吨!碌咖.…钩翻刊…睡q砷呻『狮’□… ^ˉ=

防·…

O□四阐油… ■凹【只屈… ●·… p◎●咱′… p◎●咱′由

m蚀0》

p□— ●◎…

·呛.c·m°佐】: 歹;》》洲]《血忙◎…·…·0…1yp沟8仑t…沁】;

伊「〖=尸■∩

《》 lm?`…`‖0

………吨p□『≡

图llˉl69成功Hookbto日方法

■■「[伊|

这样我们就成功通过Hook的方式找到加密jd的实现了。

『山■■尸=■『‖匹■|

6.寻找详情页川ax的toⅨe∩

|抄「》

现在我们已经找到详情页的加密1d了’但是还差一步,其A)ax请求也有一个to代e∩,如图llˉl70 所示°

『四cM邱…《

矫…………………』》…

』(

≈…β『

▲■「「二■■■β‖巴■厂

亚凹Ⅷ吗ⅧⅫⅫ加川加卫”和羽

p……m

二呈…=∏捶r

凶《哩…-茵呻〃〃…虐f幼…贞“·…↑…fpq′…d哇雾m应▲,谰J咖→■m…p·≡

}‖

b

厂— — ˉ

怜@

‖‖

■■|·】■|□』■■

第ll章JavaScript逆向爬虫

528

比α右■冉●§



限幻

匠烟碑m

c切…S°蝉℃彝

|」

同5c「ap.

!0碱呵禽

b窒避虑…|卿’

?复、.

≈锤硒…M■…y…m吨"甘m■

●Gwq□∩…唾■…◎…℃α此泊



,



x

口悯·°a配腮趣邮锤蝇cSS吨…o 尸m…陋删·渔刨创西口……沤口°…m』■固m蚀j…… ×陶西U





c

尸酗……■

■…γ白油■

c… 为

一-ˉ-

圈圈…掣m°…c…叭P旦蜜

庆==器=÷斡庄V

…旦■≡22]□111△而qⅡ9吕q03

团睁,…………删…}碌厕:罕T诞°°乎`浓狞… ‖ ∏二÷台←ˉ『bγt■ c■■亡古…长屈毛庐己l』沁

旨翻:隐…^…翘」:望蔗F蔼嚼默獭·颐



」·‖(

囚蹿篇………。…j野哼A呼′粱′……′雨… 圈鳃四o″M…唾…] 冒…警°TT巫

日(‖

圈腰…°….o鳃…Ⅶˉ……·烈吵 圈瞬::“……γ……| "……,t′』…………‘蛔

叫□

昌勘滞……Ⅷ」!息鞍鞘鳃肘:聪刮瓣筋删鳃蹦{努………………〃“

‖ 』■■■可||{』■■〗‖』‖

N=些

* § L■

_

讨期爵铡睬『睬愚|翻蘸:魁………………蛔 图llˉl70详情页的匀ax请求

可以发现其实这个tO促e"和详情页tO《e∩的构造逻辑是一样的°

7.使用尸ytho∩实现详情页爬取

■■]|■■』■可|凶■可|■■■

因为也是Ajax请求,我们可以通过上文提到的同样的方法对该to代e∩的生成逻辑进行分析’最终

现在’我们已经成功把详情页的加密1d和A]ax请求的to促e∩找出来了’下~步就是使用Python完 j冈port‖a5∩1ib mpOrttj爬 1‖portba5e64 0

1‖poItreque5t5

I‖0[X0R[= ,http5://5pa6.5crape·ce∩ter/api/|∏ovje?1j们it={11mt}&o仟5et={o仟5et}hto促e∩={toⅦe∩}‖ D[丁∧I[0尺l≡ |http5://5pa6.5crape.〔e∩ter/ap1/们oγ1e/{id}′to促e∩≡{to长e∩}| LI‖I丁=1O

0「「5[『=O

5[〔R[丁= 0e+34批euq0btua#(ˉ57w1q5o5-j@98xyg1川1γ「x5*ˉ|iˉ0_川b!

旦■司|」■‖巴■■·‖=■∏‖」■

日rg5= [‖/ap1/Ⅶoγje0 ] toke∩≡get_to代e∩(arg5=日rg5) i∩dexur1=I‖D[X0肌.十or们日t(11『∏1t=[I‖I丁」 o仟5et=0「尸5[「’ to代e∩=toⅨe∩) re5po∩5e≡reque5t5.get(j∩de×ur1) pr1∩t(,re5po∩5e ’ re5po∩5e.〕5o∩()〉

■■■曰||

de+getˉto低e∩(arg5: [ist[∧∩γ]): ti爬5ta"p=5tr(j∩t(ti‖e。t加e())) aIg5·刁ppe∩d(tme5taⅧp) s1g∩二∩己5‖1jb.5ha1〈0 ’ 0 .jo1∩(arg5).e∩code(0ut「ˉ8』)).们exd1ge5t() retur∩ba5e64.b64e∩code(0 ’ ′ 。joi∩([sjg∩′ t1∩)e5ta阳p]).e∩〔ode(|ut千ˉ8‖))。decode( 0utfˉ8")



』』■可|■■■‖‖‘·■■■■■日■■]司」■司

千rO们tγpi∩gmpOrt[iSt’ A∩y

‖■■】■‖||■■■可

成爬取°这里我只实现第-页的爬取,示例代码如下:

re5u1t=re5pO∩5e.〕5O∩()

〗』



} p

ll.l3 JavaScript逆向爬取实战 p

)■|

‖|

「 ■伊■‖‖■■厂‖}巴■■■厂



529

+Orite们i∩re5U1t[|re5U1t5‖]: 1d≡jte‖[ 01d‖ ] e∩〔ryPtˉjd=ba5e64.b64e∩〔ode((5[〔R[『+5tr(1d)〉.e∩〔ode(‖ut千ˉ8‖)).de〔ode(,ut+ˉ80) aI85≡ [+0/api/刚oγ1e/{e∩〔rypt-id}′] tO促e∩=get_tO代e∩(己rg5=arg5) deta11ur1≡0[丫∧I[0RL.于omat(id=e∩〔rypt≡id’ to促e∩=toke∩) Ie5po∩5e=reque5t5.get(detai1ur1) prj∩t(|re5po∩5e ’ re5po∩5e.j5o∩())

这里模拟了详情页的加密1d和to促e"的构造过程’然后请求了详情页的Ajax接口’这样我们就 可以爬取到详情页的内容了。 8.总结

尸■尸~■■『『■■|「▲■■■「

本节内容很多’_步步介绍了整个网站的JavaSc∏pt逆向过程,其中的技巧有:全局搜索查找人 口`代码格式化、设置A|ax断点、变量监听、断点设置和跳过、栈查看`Hook原理`Hook注人` Ovemdes功能` Tnmpermonkey插件、Python模拟实现°掌握了这些技巧,我们就能更加得心应手地 实现JavaScript逆向分析了。

本节代码参见: https://githuhcom/Python3WebSpideI/ScrapeSpa6°

■■

■ 尸 | } 卜 「

‖卜厂



厂‖■厂|■∩

【 ■ 价 ‖ | ‖ ■ ■ 『 ■ = ■



0 仙尸||‖■■

卜 ■

『 『 ‖卜 [尸β■尸‖尸叼凸■|厂β■厨》}■「』∩怔





■□】■∏‖=■口 q

∑ ‖

=早







∧pp数据的

截至目前’我们介绍的都是爬取网页数据相关的内容°但随着移动互联网的发展,越来越多的企

业不再提供网页端的服务,而是直接开发了App,更多更全的信息都是通过App展示的。

‖ 对 ■ ■ 可 ‖ { 』 ■ ■ ■ |

■【 "/夕





那我们可以爬取ApP的数据吗?当然可以°

大部分App使用的数据通信协议也是基于HTTP/HTTPS的,App内部—些页面交互和数据通信 App中的数据加载,我们将其类比于网页中的A]ax请求和数据喧染’其基本过程是App向服务器发 在网页中’我们可以借助测览器开发者工具中的Network面板看到网页中产生的所有网络请求和

从中找到_定的规律’就可以用程序直接构造请求来模拟API的请求’从而完成数据爬取°

和网页—样,App为了更好地保护数据不被爬取’其对应的API请求中也会出现加密参数’如果 我们因此找不到对应的规律,那么即使抓到了包,也不好直接构造请求完成数据爬取。这时就需要想

各种办法了、例如直接拦截所有请求的响应内容并实时处理`使用与网页中的Selenium类似的工具完

■可』司□■∏‖{』■勺、■■■■□

响应内容,然而App怎么办呢?要想拦截App中的网络请求,就得用到抓包工具了’例如Charles` Fiddler`mjtmp『oxy等,我们可以通过这些工具拦截App和API通信的请求内容和响应内容’如果能



·、‖■□』■■

起-个HTTP/HTTPS请求,然后接收并解析服务器的响应内容’之后将得到的数据呈现出来°

‖■·

的背后也都有对应的API来处理,例如某个页面呈现的数据几乎都来源于某个API。为了更好地理解

成‘』所见即所爬\直接HookApp中的关键方法来获取数据`直接逆向App找到其中的接口参数逻辑, 本章的介绍侧重于比较基础的App爬取技术’例如App数据包的抓取和App的自动化等技术, 会主要介绍Charles`mitmproxy、Appium和Ainest这些工具的使用方法,学会这些足以应对大多数App 的爬取°

当然只学会本章所讲的技术还不够’可能还是会抓包失败,或者在使用自动化工具爬取数据时碰

壁。换句话说’仅仅停留在表层是不够的’在某些情况下需要对ApP进行逆问来找到其核心逻辑以及 数据请求究竟是怎么实现的’这就涉及App的逆向、脱壳`模拟执行so文件等技术了,这些会在第l3 章单独讲解。

↑2↑

C∩a『|es抓包工具的使用









容,这和在测览器开发者I具的Network面板中看到网页产生的内容是_样的道理。

■‖

Char|es是—个网络抓包工具’我们可以用它抓取App运行过程中产生的所有请求内容和响应内

凸■刀」‖‖■■■■■■■』■■■■司‖』■弓·二■■

等等°







Charles` Fiddle『等都是非常强大的HTTP抓包软件’功能基本类似’本节我们选择Charles作为 主要的移动端抓包T具来分析App的数据包,以便之后爬取App的数据。 |.本节目标

q



本节我们会以-个电影示例App为例,利用Charles抓取这个App在运行过程中产生的网络数据



■■Ⅵ」■。」■■



l2.|

Char‖es抓包工具的使用

53l

包’然后查看具体的请求内容和响应内容。

同时’我们会使用Python改写抓取到的数据包中的请求’继而爬取App的数据° 2.准备工作

「 b

p

Charles运行在—个电脑上,运行的时候会在该电脑的8888端口开启_个代理服务,首先请确保 已经正确安装好Charles并开启了代理服务°

■厂‖「■■「‖●β||)|》匹■『||皿■尸『「

然后准备—部Android手机或模拟器(系统版本最好在7.0以下)’并让手机或模拟器的网络和 Charles所在电脑的网络处干同_个局域网下(可以让模拟器通过虚拟网络与电脑连接,也可以让手机 真机和电脑连接同_个WiˉFi)。 之后设置好Charles代理和CharlesCA证书,在Charles中开启SSL监听。整个配置过程可以参

考h忱ps://setupscrapeccnter/charles° 另外需在手机上安装示例App,安装包下载地址为h肮ps‘//appl .scrape.center/’访问即可下载.

卜卜

注意为了方便’本节后续的内容会统一使用‘‘子机,’指代‘‘ˉ手机真机.,或“模拟器,, 。



D



} b



3.抓包原理

设置手机代理为Charles的代理服务的地址’这样手机访问互联网的数据包就会先流经Charles,

再由Charles转发给真正的服务器;同样’服务器返回的数据包会先到达Charles’再由Charles转发

给手机°整个过程中的Charles相当于中间人’可以捕获所有数据包’意味着可以捕获所有请求内容



和响应内容。不仅如此,Charles还可以对请求内容和响应内容做修改。

广

P





p





4实战抓包 打开Charles’初始运行界面如图l2ˉl所示。 ●



α■№■4.Q.『=酌p审∩‖











■志s碎m比·



β

0 p



p



p















出细‖匝

P p



■ ■ ■ ■

■四■

图l2ˉl

Charles的初始运行界面

Charles会一直监听手机产生的数据包’捕获的数据包将显示在界面左侧’随着时间的推移’会捕 获越来越多的数据包’左侧列表里的内容也越来越多°











日 ( ‖

第l2章App数据的爬取

532

现在打开手机上下载的appl ,其界面如图l2ˉ2所示°

会发现Charles已经捕获了对应的数据包,其界面类似图l2ˉ3(注意—定要提前设置好Charles代



Q



……哪…………ˉ∑西…~…一ˉ·^

0

■■■」■■■】当■■■|{司」■

=~…

…≡…汹磅~……ˉ雷≡……………呻………

酶■

"…



…………



已 β° β◎§ · ’$ ’日 α息 钮幼 色· 嗓





萝ˉ………≡…………………………………………… 靠圈■函厦幽田团■囚

理以及CharlesCA证书,否则没有效果)。

酝】9:田 ■O÷ …凶…穴丁厢mm●o份z垂93:四



∩…G已…mm·“=m盆3gD巳

印…●回7…沁刀“.m瑟338o5



唾写==

巴羽吨

四9

刁m

c… v上9竹m~今Ⅶ■

盯■!

‖‖↑



」·‖

…酮T…凹20诊“.m企虫3a茧0【

”巾 `O0≈

产B



图l2ˉ2 appl的界面

图l2ˉ3打开appl后,Charles的界面

□(

在appl里不断上拉’Charles会捕获这个过程中产生的所有网络请求’图l2ˉ4中左侧的列表展示

Rm唾″… 丁军

』‖‖‖』

‖……

{ | 』

●∏……■严…c酮 ●‖……山攒y°审…闭m`c∩ ●…….…D…飞c腕 钞b呻〃…膨~团

干全

γ“

■吓比■…

·、…◎…

阴四吟●

…碑T…

…≡………

钟…≡……………………≡≡…·∏蛔

●凶

唱一ˉ…吼…≡…旭…一……=………

≡_~—……=雷一=~≡ˉ≡一…

●·

●∩…;仇巴=℃丛…

●…牺.…呻

m2.$0O00↑77『3凹△● ■”0闰…b…团β2‖7O.逛▲△$



/ 》W=轻些坦 γ… ∩●宁…=·…V…】D刨℃日口鲍2让a0刘2

∩…喧呵了加泊

凸■‖□·{■■■

了捕获的数据包。

2Om吧B■z22塑3知R

…pm卸…门丁坤20田=05.幻睡36:o2 ∩■”∩”…丁肺2m↑心S写弦2贮习s民02 匹二0m

220硒

oNs



cα■… ·

锤γ……TO固………▲凸■刨■…Ⅶ



图l24在appl里不断上拉后’Charles的界面

可以看到,左侧列表中有一个链接是h忱ps://appl.scrape.center’在appl里上拉的时候它会-直闪 动,这就表示当前appl发出的获取电影数据的请求被Charles捕获了。

纽‖『■□Ⅵ■〗己■‖

硒"■…凸9

下砒旷?

q

为了验证这个结论的正确性’点击上述链接下的_个条目’切换到Contents选项卡,查看其详情° 此时界面右侧会显示一些JSON数据,其中有—个resu1t5字段’该字段中每_个条目里的∩a阳e值都

‖√‖ 〕』

| b



Charles抓包工具的使用

l2.l

533

p



是-个电影的名称,这些名称与图l2ˉ2中的电影—-对应,如图l2ˉ5所示° 》

墨吨_

恕_…

…窖~

●一

●■▲

…攀_…

…_7乙_叼一_-

●●

唾V…恬…℃盯了p们ˉ『

{ 』…』…〃.….

…加呵■r…思●…

■犁

仿

■田







…·≡…

癣=



、_

p

≈…’≈…=…

√们…怕 匹『‖|

■沁峰g唾堡c ‖o



!≡K…∩h0 }≡ˉ-呻

}【←↑0▲际忙00

…·A·■0ok闷a0OO

[■尸

叶←m△≡它‖0 ′□?啥=垂·…切

▲■「

唯卢●■巳→℃ `史v切…凹■0…0o 戊锤←m▲噎怕

‖( 0

宙…8■‖呻p

浊,·≈……也 U7矿二 =7O产·.■弓9

{ ·…瞬$!《

捶7…=鲤△嗣烂抢 匹7…哩=鲤▲…铂抢



凹7=…田

酚7…’O■m仕7o

q…0…·

■■■尸

■0b…乓吕·穴『…``NT6…山…·,

:爵霹愈鳃帮钙;翻P鹰′…′呻……幽…酗出噶n…怕…』…勋妇p田Qˉ“~睡′

泌壹营.■君庐℃ 』〖壹钾●嗓岩耘℃

》●h…〃『==丛…=●望舍≡ 勘●h…〃『…=丛…=●望=

.■1止柏哩沪0列切8凸?=疆白0

Ⅶ■』

■●▲酗t■■『】】〗甘



b●…′…■ 二生ˉc■w

》●…■=■■宁≈罕"^… 》●…■=■■宁≈山·"户…

匣 ■

’●……●v加.…≈…名" ●……闰…坤呻……m 》●吨扣……吨m p●吨加……吨C∩

:镭蹿·?.{≈”,.中…崩.



』8隐憾慧爵露::豁}"《疆!熊嘉罚.……霉…………………『 ℃1…■O抄…N0

■∩

二cov·户L睡鸣§《腰0咱8鲤颠咱t《…』″…0“Zq·…$出≈】°2■《彰0沁729】·j…蝇吟∩静沁窜2e·.

!

5;翻溅Z÷!需舞·亏{,

■■「『凸『



■■m■t广】n0o





:谣潞↑↑…"

》.《掣…尸"。皿必由鹤酮…早.■人m氓αm4蛔QGm…圃…净m…礼■~



■泣官刁β



…蟹鳃麓繁‰[…『…~

卜||‖



~ _~ˉ

……

Ⅷ 磅田|

碑7…≈m■…n劳|…■≈『?汹酗■辆蚀■ 一■田

图l2ˉ5所捕获数据的具体内容

至此可以确定, https://appl.sc『ape.center对应的接口就是获取电影数据的接口。这样我们就成功 捕获了在上拉刷新过程中产生的请求内容和响应内容°

P

尸》▲■■∩』‖卜「■尸■β‖■

5.分析

现在分析一下捕获的请求内容和响应内容的详细信息。首先回到Overvlew选项卡,界面右侧的

上方显示了请求URL、响应状态码ResponseCode和请求方法Method等信息’如图l2ˉ6所示’这些 内容和在网页中用测览器开发者工具捕获的内容是类似的°

丛噎m●…‖0

■■■■

ppU←≈

7醒■?ˉ2

》C岂■曲如

丫La值cD"良RS∧W∏Nˉ∧压sˉ〗沁cCM.$N▲?唾

‖回吟凹…00

哆…五“…00



潞涸…鲍…↑0 止?■唾=“●邱恒10

〗…@由…Q

∩L尸‖

畦……它00

E≡啼℃

「「『

●●●●·●

『|【

亚……瘫q0 睁●▲…… 二~0≡

h呵『、0

ˉ

尸s●≈叮c嗣怔■m■

r

‖ ’兰唾函=一 些■坦



】…巴·



…….丁γ沁

…………



田……?

…≈p…0信…

C≈学▲●■宁≡

h……■口财翻……b哼T …Ⅳ…姆…c…酮

≈…←……四司田0Ba70垂■马g

….哇…

愉坠‖蚀′a?.‖刀弓■…· _

l 丁率

· r的

·

……

、消△牟坐P出■●

! 矽…

-可■蹿…≡

二一=.-一一~

…=g叮‖…2O2仁o巳””U●习:05 ∩…旧咽干m



∩● ˉ

3O∑lˉ0巳?●分P叫吕oS

门…t丁hⅧ沁刁00G掘…88:0e

日……丁h■沁洒■O巴m2止蛔宁0巴 T ~庐

吵写=…

□碧■尝

二…≡

}炉)

}|

刚印 …—_

bb…■一

=凸=

|响…翱…·…蝉………避m睦m





-



图l2ˉ6 Overview选项卡下的内容



■■■□■、‖■■』司Ⅵ

第l2章App数据的爬取

534



到JSONT℃xt选项卡即可看到响应体’并且响应体的内容已经被格式化° 酝

=■

》一

—…煌

—…■

≡感勘

—令■

_…●

_■=

_●



$"

…罩^锡^ 溯…′′四『ˉ…睦仁顿`

●b…Ⅳ…l≈■四宅创哲

…0呵甲西■■●…

■…妇

T~【■…●

■f…【氧叫由…泊



{‖{

峙h≡〗mDU由呻0…0…

■甲

A…·…g印



…铜比h…ˉ↑O.o

?≡00△…l0

T…0三…畦7q



…空巳0吓7O

…◇…慈wm9

缺=◆.圣△兰=00





咕c酵凸●■百1■0 ∩厂■@`↑■■g 【《

甘………00

d…=助凹碱三怕

□·】‖‖』】‖■■■‖‖〗‖|』■‖』‖{‖』■司‖‖||乙■‖■■

接下来点击Contents选项卡,查看请求内容和响应内容的详情’界面如图l2ˉ7所示。上半部分 显示的是请求的信息,下半部分显示的是响应的信息。切换到Headers选项卡即可看到请求头’切换

曲峨】 见U

喻…●〗-0

?…元凸咋0O

‖司

pO1t●U■; ←P″…‖【肘佐●■0▲d叮·

←…旦0=恤

口F…户】 ■b敌p■!〃沁·…0凹们.■碱′=p』●′佰函■■≥0〕必90哮…◆幻〗b0色哆V加6巧002】7〕.〗←≡…=汕~1■·0 雨哼●t吨●尸〗7,■占 【Um■p ■…]0 ■O啮k皿…←m72 ■1闪』弟′■〖0田0

啼↑≡m▲础岂0O

Oh…叫『■…谚比·…m『c酮

℃』∩仙t··0 1邓Q ∩$亿●7广? 0■■

$∩…〃试……』弓酮 ●…川哩≈∩……

^F叮10…■6 【■屯哲’尸中酣刀贝】·

p挝…勒…肘印■"…弓c°mc"

∩』苫■0n0

盼∩心/′畸…啤匈

户‖…■8 ●建↑趴…汾■U ·o■u扮0■了■1…■p

‖《

℃「…■? ●■硒=出【■干回》倔求坞●耻■三个八之■=又洒G晒宜祖击九■』咱『任■■●J》m仪‖…协》■←呵」′卜一 》O 《

●…』爬…wm.0彦‖而隐了。吧…`ca

■@汪丁■吕 ■●tt●■『〃910唾t‖mβ·喊′■v归′O味酚■749了▲Of…锋6出句■′P』h7ry“?■7】9].…←…~1·臣1r■∏

■巾m…=』啪■0 『·畸』杉m■0 ■田■′〗’

∏■■…■〗 0gd

凹向』…■■□卜…】O .p□「…■8 ■■■『让■句申■…·p卢=氓…‖…乌酶■(■=m■泣)H…门. ■*巴旭■■行m■ }′《 ■0广2

np

凹…白0■闪巾=『

0《■1几■广:■丁m凯■罚t蜘毗酗q总■魏】m伶0



■删…、 w囤`=忘—]总…瓤…[…撇喊}…

刚“飞

{ …|



畸…●…1…佰…悸h■□“凶≈0■口

CoIltents选项卡下的内容

由于这个请求是GET请求,因此还需要关心GET的参数信息’切换到QueryStrjng选项卡即可 查看’如图l2ˉ8所示° c…$舒则缚bs…】



鹤础噬]嘴浮ˉ , 傍列— ■瞬抓 刽

_宁

√●甲

U■「













。…吟『三…:]…啊…

尸 『 =

二兰」

一】

鱼二

■P惺酗『二呻总硒`室;o



聋啥00O…论 甘……蜂凹

!



雄≡…靡涸

垫寸咋……沁

≡…° l… !《

潭7…伊凹…■↑0

j

淫,……凸痴亡山

ˉ…≡

与f…0二, 〗■′

§ .′器『oi! !!

晌…·:-.

曲噎…灼



童…■牵m 色7…>凹▲№津m

! ::浮!:『蹿〉腮。:摇!膘》…′………,戮……7…….′…跑……铲,

●…沫m…≡…虱

●…≈……"

〗●…≈ˉ■L三…

〉G~叮.~…钾

》◆■…≈屯凸嫩■≈■c…m

〗●…譬严ˉ. ˉ .m

}T :殿『蹈』5!.『黔ˉ5翻『 ■凶≈n70 卫m■ 闰D$….:D9·

^趣温塑『萨呼◇=· .…‖,

l

} 》′.{亩, .·喻·ˉ蜀=呻n…=人mˉ=………键′…「………Ⅷm臼》口ˉ呵感叫 .』口ˉ8肛’

,

■_ˉ.:■■宁毋…净°



|擞夸:=宽:二二罢(二猫二ˉ 】’《

■』厂8〗D 彻…■8■■巾唁,

『 …

Mpm●庐‖≈m睁≡·℃护■

一-→≡」≈勾$`m眶 一由

SF『哺即…饥凸●凶◆…蜘…懒哟嫡…饲

」铀U出……0【 .…匝雪■… 瞬

图l2ˉ8

ˉ…

GET的参数信息

对于其他App’同样可以使用本节的分析方式°如果能直接分析出请求URL和参数的规律,就 可以直接模拟发出请求实现数据的批量抓取°

‖■】|』·`」■∏|」]■]‖』刁■|□■]‖」■□|■引|』■』■{」■司ˉ·、』‖‖』|■】■‖|」■·]』■■』勺|‖』■√]‖』□】‖·】■]』』】】■』』■‖司」』■‖」■■】■■』

图l2ˉ7

∩日日‖司



■p施1』Zk吼叭8汛和■曰14■′ 砷●』陋γ·口: ↓Ⅲp



Charles抓包工具的使用

l2.l 》|‖

} p

6.重发

Charles还有_个强大功能’是可以对捕获的请求内容加以修改并把修改后的请求发送出去。点击 界面右侧上方的修改按钮,左侧列表就会出现—个以编辑图标为开头的接口’代表我们正在修改此接 口对应的请求’如图l2ˉ9所示° ◆

…ˉ,…

●卜…〃山Y…℃∩《m7` ≡●…″·呵■…c…叮7 v■■口

■】『■■■「|「

泊… 吗…广o么陆≡‖0

. .ˉˉ.…^淌宅№



`″

|●

田=



535



…二ˉw翰叮)」

■壤3……·渔… …q

0

肋志

|0 二≡—

pK比们>0o凸m卜‖0

_一

0p『=…舔00

【栏台OQ旱回o

_

L凸



E……

凹7宛佐空n△…00 O了琶T诞哇00 』■跨m&醒…O

■■『『』‖‖【■『 尸}

雨启思·

【■厂卜巴■「|

防告△·△■…0 〃『跨醉旦?刑恬‖O

?冲=…旦巳础比00

伊←n里§列t户`o

闸『■一幼■L仁…

……′C…

………二甲

匹■「=■

旧』=

【◆【■尸

■怎o m·℃∏ …询四…0石G 〔 鸵.c仇 …叮曝…■ …√^…°加

■■■■=『||□尸‖′■=『||~■『■■「|■巴=■》仔 ’ β 尸 △ ■ 尸

…国弓 ≥弓 ≤占 ………

陋叮 昔=→

匪7…—

【=T



k凸户…剖●灼9

←′



图l2ˉ9修改捕获的请求

可以修改请求参数中的某个字段,例如这里将offSct字段的值由0修改为l0’如图l2ˉ10所示, 然后点击界面下方的Execute按钮即可发送修改后的请求°

匡■■ ■ 『

慰.

…■丛】-…〗◆

.



巴■■尸「■■■■厂▲■尸

[ 且『扎比哼]……唾 》●…·=… √印…■

.…≈



















……`矗~

」 愚妇嚼

≈|

■■■■■■冗贾m了 | 一_—≡一…■mL≈△_

■甲 ■甲 →■…

≈ˉˉ .

少跨~】0

=ˉ

ˉ

司】刊

巳【←哲二≡~·@

≡…

■--

■ ■

=-■占—■—_

n′牵…=■

—ˉ≡巳ˉˉ_■■



▲』-~

型←广= °←吗



↑2

■■■■■■_一-



.必=■ .- ˉ=

—-≡—--_-___一ˉ一≡≡≡=■

划~灭导ˉ ~一≡ -

坠一≡●@

‖□■‖ 『巴∏|』‖■■尸|||』■【『『|『「巴■■■

′污◎°◎心◎

【 ■ 尸 ■ ■ ■ 厂 | ▲ ■ 「『

狸←■== ←·O 埋←▲=≈山

些琶=0 ●←=O …- →面′r:m■■= =←≡-

≡←-←一

一≡

|二 }

—=■

, ~-



!

-=罕m …^~额

……

△尸[。厂||=■厂△尸「|



mt

尸~≈■k

~←~一一

仁司●

咬〗…标鱼-_宁…

图l2ˉ10将o匪et字段的值由0修改为10

可以发现左侧列表出现了对应的请求结果,点击Conten屿选项卡’查看响应内容’这次返回的是 第2个列表页中的电影信息,如图l2ˉll所示。



第l2章App数据的爬取

536

‖■





F -稗-≡=■

……们… 了■西≡.匈曰

wOt■

●运〃…■T=~= 兰岂

o∏■《

0o

■巾

0o

■… 9」声=?T 』奄l“≡■?@

回畦m…『O

"…p |哦刚酗婶{ ∏o哟

7带旁■F浩0O



笆…必8…田·

…冗智→旧

■■l凹D■G ■V『·「■G●“T℃□°

r?≡==…℃

≈c…P·8■毗t印■〃p【古■1R山∩曰屁?′…归…】c】乓“7Dd】b1■■嗽沁“〗凹〗△G“…】 】唾…』…甲№=〗c户p ·团钩8·了』″‖ 『…p 闰呻■′吗…『 『忽齿』‘

‘0←←=-0O

夕砖≡■=.. ■叫O 】砷■芦V0…‖0 吕f兔ˉˉ



■…11…≡●t■8 ■】…ˉ皿哦u付p

凸mm《广己u∏o ·眶@厂●■8●·0■

●拍…碱呻…■…… p色:≡:

--

●…飞■0酗『 ∩沁凶M∏■『 ‖[ ■妇■吕u□

?≡■●■~‖0

8

-~-~



鳃…闺吧唾‖O c$…~管=■宅?O

·≈凹…□ 0·西, ■酝叮 ■■】o

ˉ二二…

□q宁…t_→下’<……勺枷■■酵懂女于民■由■.浊≡…四■m■》…■Ⅲ铀人■出` γ臼■吓■

》p 《

●柿……≈p■幽.c匈

■m■Ⅱ血·

∑●伸…′』尸=…』=~…^=咖

■…■0 ●≡=·

●…悸巾m…吧cm也c∩

■■l丛■■■丸M●@Ppro ■…·【尔r0凹【〃户°■M…■凹T/…让●◎…■】】…·7▲7■0m…凶M■0“1d】O汪γ■〗……=“■■胆→【c●·

≡日=二 户二二二≈=二△≈

°■…『》■□8 l■…D■mp≈■r‖.

■…1儿…《●0■20】≥k1=m●□ m≈?广■凹】o ●“◎『苛·8 0·■p

■…M■户【 l□画′■牺●泞0 ■其疗p ■m★0‖〗O 吧.一口G ■【……户纽…》尸宁宁钎凶裔●==丁…=■侈戌早■…仰■呆n蹿丁皮●≡T呻m不叮■旗…‖n 》°{







钾γˉ

帅驹…



」■■



画阳T·,…0



.·Ⅱ沁簿.《 =A…皿8!已1‖m■0翻0

="…、丫…怎。 」酗…碑`

酬″:



□』厂8凹0 ■≡』■晤^仔o





…硒…!0和涸0

图l2ˉll

返回了第ll~20个电影的信息

有了重发功能,就可以方便地使用Charles做调试了’可以通过修改参数`接口等测试不同请求 的响应状态’从而知道哪些参数必要和哪些不必要’以及参数分别有什么规律,最后抽象出~个最简

| 「■■■■■■‖■■『■

单的接口供模拟使用°

7.修改晌应内容 除了修改请求内容’Charles还可以修改响应内容,例如将响应内容修改为本地或远程的某个文件, 这样就可以实现数据的修改和伪造了。

怎么实现呢?右击任意_个请求’可以看到出现的菜单中有MapRemote和MapLocal这两个选 项’通过这两个选项就可以将响应内容修改为远程或本地的文件’如图l2ˉl2所示°

以生成本地文件为例,怎么实现呢?可以先把当前的响应内容,也就是JSONT℃xt的内容复制下

来’保存成本地文件’文件名是datajson,如图l2ˉl3所示。 ●呵二二n=←一= ■甲 ■=



…0



■↑…昌四`~k :峦

H

…c呐凹凰

…c…剑^R宙p…

00cou∩t0, ; 1000 |,厂e白ult写o08 [{ 001d0{ : 1p

瑟;′■

…C……

…动加…w

,…←【 〗出Q 9猴≈!u·∏ 【《 ■唾8 】G 汹≡0叮

…FmM

P呵…?铀9囱` 4占…

…C= ?…

F?… …

∩●…『

赊…&…≈m

毋…m ■ml0邑

山=▲·▲

…日V

0|∩a们eM8 0■王别姬枷』

0‖a11a三00; 『0「a「酗ell‖γCo∩cUbme皿0 00Coγe「0‖ 目 ‖qhttp5;//p0·唾1tu■∩.∩et/刚oγ1e/

p马k酗生』 ■P 们■憾尸?卞

△0一 ˉ宁 °二· ∩啥四 ●…网 ……■,E≈ 些■=?←一宁" …跨白匀纫 ●……叫 …v……′ ●一 ●m■怜≈ …m…≡Q

■□曰t3.j£◎∩√

●●●

00



CeQGa3e03e655b5b“ed31b5Cd7896c762▲7Z°j…6』比“4∩ˉ1e→1C000 ,0c己tego『1e5‖0弓 ["囚饲", "Ⅲ悄"!′

■●…寸0〗 ■0c■秤f●

00pubu田们ed己tH8 0‖19g3=07→260o′

q喝…●0 ■■=8 口■

"m∩ute0‖8 171j 『0Bco「e00 : 9.5′

广『° ‖

诊【午0 】□



勺p…U锤

『|厂e91O∩5M: ["中画内地叫』 "中国香港!‘]

■■…∏■也 G【=0≈

‖$mt‖: 20 卿Ⅳ己∏e,$8 ‖0这个杀手不太冷0a’ Ma1止a3『0: ‖0L匈∩‖0′

.二;=≡·Y

■p的■√U●

尸…针

‖…

》, {

牟已●…0… ■柱】■~



G砷…●■β

}}.!….0

广■=α…



妨p″=≡T』窒…· □…翱L∩…T

●…0■Ⅲ

Ⅱ中●』…U可 …‘·



00coγG『侗8 "‖ttp58//p1.≈』tu己∩·∩et/mγ』e/

●叮0 】■

G=

「…p

6be己9己fq52qd↑bd0b668e臼己7e187c3d7767253.」p…6邻→6“h1e1cM =

‖『5Co广e‖0; 9.5D

,,厂eg1O∩5‖,: 【“法回$]

…出Lm

》,{

…L啪 c…靴m…

0o1dd‖ ; 3′

……α.

,0∩己爬"8 "肖申克的救赎"’



图l2ˉl2MapRemote和MapLocal选项

p

0Qpubushed己tM: o,1g9q=Og=1q‘‖0 ‖0们工∏l』te‖‖: 110’

… 咆c季… =壁崖二

■■■■■■■■■■■■■■■■

ˉ

■Catego「】e5$0: {00■饲0o’ 00动作q0′ 00犯罪0,』’

h

o〗au■S00 : M丁你e5何已凹5‖■∩kRed印pt1o∏『』′

0■←→ˉ .=→■0口 『q巴▲▲==.″■■=←4◆..■→ ==±p=■□°f●β

图l2ˉl3保存当前的响应内容







「户|卜|■尸|·‖|

l2.l

Charles抓包工具的使用

537

p 口■『|■「

然后修改其中的字段值’例如将第_个条目的∩a‖e值修改为“霸王别姬2”’并保存修改,如图l2ˉl4 所示° 攀data.jso∏

仆 |

●●● 『



■尸「巴β『

0‖cOu∩t|0 8 100’ 00厂e5u1ts"吕 [{ M1d,『目 1′ ↓‖∩a们eM: |‖嚼干喇懈z‖00

‖【巳■【||■伊|〗▲■『

|‖a11己二‖$; ,$「□厂酣eu问y〔OhCubi∩e0‖『 "coγe厂‖,〗 n0∩ttps8//p0.盯e1tu己∩·∩et/耐◎v1e/

ce4d己3e03e655b5b88ed31b5Cd7896〔「62q72·jpg融6q比6』△h1e1C"

蛔C己tego厂1e5即: [口c剧悄m′ 00爱悄"】『

= =



0』pub11S∩edat0$; |‖1g93=07→26‖U′ 0{佃工∩ute60: 171p |05c◎厂e〗』: g.50

00广eg1◎∩5,0; [“中国内地0`, ,0中圃誊港皿] },{

‖》匹■「》【尸|

!↓m‖$: 2′

"∩a爪e『`: 00这个杀手不太冷00′ t0己11as,‘台 00L色o"M′

0‖〔oγe厂』『: b』们ttP5://p1·"e立tOa∩·∩et/网v1e/

6be己9af4524d↑bd帅668ea■7e187C3d「767253°jpg触6邻→6“∩1e1C,0 -



0

β}

|0C己te9O厂1es‖, 目 ["圈悯M! 帅动作M′"犯罪"‖ 0 ‖opubu5‖ed己t0,: $0199q≡09=M‖『’

}卜

‖0刚1∏Ute!‖: 110』 0‖sCo厂e00 2 9.5p

‖』厂eg1◎∩s购目 [响法国‖0] }0 {

尸}|卜■=

‖0mⅧ0 3’

"∩已眠": "肖申克的救赎,『p

‖$己ua5o0 日 0『丁∩eS们已田臼h己∩ⅧRede呻t1o仇0!0 【0■→·q→=0l ■



0【巴▲←==· 6〃→^=≡出尘·‖凸=

==▲F==口●』←□

|【「‖卜‖‖》∏尸|■

图l2ˉl4修改响应内容中的字段值

再在MapLocal的配置中’选定datajson文件的保

|■「’‖‖|卜「■■「『也厂|『|●∩』巳「

懂碰tM∩钞沙‖吨 …

L





M”脚谰T]

………→=岭≈…≈磁铃中L咀审

∏…■睡

■■『||

P『°t°c°!;{蝉隅阑 ∩◎S∑: a即↑.Sc「却eC酗t·『

_

■ ■ ■ 「

p。咸;… path削匝郧而vH一≡—~_— …≡.←ˉˉ-=二≡≡



巴 ■ 「

Que『y肖 O行9Ot窒0引枷讨t≡q0 _

「 匹 ■ ■‖|

M印v° ˉ=

ˉ一

羚亩钝琶←冒撰 =_一ˉ=≡



F潍f、、 .″~^…

pa!向:…申 …p.l厕;田m碑■ˉ|≡睡墓: E…



侈5

蕊, 艇镶`m

这样’我们就把apPl的第_个请求—加载列表页 成功修改成本地文件datajson中的内容了。接下来重新 启动appl’界面如图l2ˉl6所示。

时土

遥榴,●…蟹■ |

、pa髓】■r■

镣■理组2

存路径,如图l2ˉl5所示°

簿■i◆

| 藤

匹■尸【『『‖‖ ■ ■ 「 ‖ 『

田C己Seˉ臼e0y琳!`′e 田C己SeˉSe『`5!t|`′e 》 ;…慧蹿= 瓷泞~

睁^′.~备△≡

丁◎(w印『「mγD”〖hm口jt可郡唾e创p狮够扣uⅧg熟咖d↑h0呻↑『whh■·嚼了□m印 刁■∏恤■∩o■■…↑师p碰∩b姑唯

圈爵霸爵 巴髓铲 涩爵霸…

谬s

95

9.5

窿::聪惺" 幽蔗蹦※ 留爵熊髓. 憋器氯翱 ■爵繁′ 图霞惫`∏"

95

95

q5

贱涣`诱史、缝酿

珍爵

擎0

9{》

c办理二.…



s-蹲





}卜 「〔

‖匹尸卜『▲』■』



习…. ˉ

图l2ˉl5配置MapLocal

图l2ˉl6重启appl后的界面

可以发现’第_个电影的名称变成了“霸王别姬2”’我们成功修改了响应内容。

这和ll.ll节内容的原理其实都是修改了HTTP响应的内容°在ll.ll节中’我们使用PlayW∏ght

修改了网页中要加载的一些数据文件’将它们映射为本地的文件’整个过程是依据Playw∏ght中设置 的规则完成的’此处是借助Charles完成°

凸尸|■「』▲■『『·■「[■「尸

| 第12章App数据的爬取

538

可以把前面所讲的‘重发”和这个过程称为中间人攻击,Charles就相当于中间人’它可以拦截请 求内容和响应内容,并对这些内容进行修改,而手机端完全无感。 8.模拟爬取

现在我们已经成功完成了抓包操作’appl发出的所有请求—目了然,请求URL就是h忱ps://appl.

scrape.center/apⅡmovie/,后面跟着两个请求参数o腿et和limjt°显然, ofISet就是偏移量’lim|t就是一 次的返回结果中包含的条目数量°例如ofISet为20, limit为l0,代表返回第2l~30条电影的信息。 另外’通过观察可以发现_共有100个电影’因此o脆et取0、l0、20`…、90’ lim|t则恒为l0°



| q







1∏portIeque5t5



B∧5[0Rl= ‖∩ttp5://己pp1.5cmpe.ce∩ter/api/『∏oγje?o仟5et={o仟set}81jmt=1o0



接下来我们用Python简单模拟_下请求’这里写法有-些从简’代码如下:

千Ori1∩r己∩ge(O’ 1O): O仟5et=1*10

uI1=8∧5[0R[.十Om|at(O仟5et≡O仟5et) data=reque5t5。get(ur1).j5o∩() pri∩t(‖data』」 data)

运行结果如下:







‖ q q

b‖







data{|〔ou∩t0 : 1oo, !re5u1t5‖ : [{01d! ; 1’ 0∩a们e0 〗 0霸王丹||姬` 》 0a1ia5』; 0「己Ⅲewe11‖y〔o∩〔ub1∩e|’ !〔oγer :

q

q

』http5://p0.们ejtua∩.∩et/|∏ovie/ce4da3eo3e655b5b88ed31b5〔d7896〔十62q72.jpg酗64"64』h1e1〔0 ’ ‖〔己te8oI1e50 : [‖ 剧惰』’ ‘爱悄‖]’ 』pub1j5hedat‖: 01993ˉ07ˉ26! ’| |m∩ute‖: 171’ 05〔ore0 : 9。5’ 0Ieg1o∩5, : [ ,中国大陆‖’ ′中国香 港|]}’{!jd‖ : 2’ ‖∩a雁‖ ; 0这个杀于不太冷0 ’ 』a1i己50 : ![白o∩!’ ‖coγer :…0pl」b115hedat0 : ‖1995ˉo7ˉ15! ’ 0们i∩ute0 89’ 05〔ore0 : 9.0’ ‖regjo∩5! : 「英国‖]}]} data{!〔ol」∩t‖ : 1o0’ ‖re5u1t5! : [{0jd|: 11’ ‖∩己爬‖ : ‖γ字仇杀队0 ’ ‖a11a50 : 0γ十orγe∩dett己‖’ ‖〔oγeI :

‖http5://p1。′∏ejtua∩.∩et/师γie/O6e〔3〔1〔6』79q2b1eqOb〔a84036O14e9490863.jpg@46q"644h1e1〔! ’ 』〔ategor1e50 : [ 0剧悄0 ’ {动作』」斜幻』’|惊惊"]’ 0pub1j5hed日t′: ‖2o05ˉ12ˉ11‖ 》 ‖‖n∩ute‖ : 132’ 05〔ore‖: 8.9’ 』regjo∩50「 : 矣国0 ’ |英国0 ’ 0雄国|]}’…0categoIje5|: [‖纪录片|]’ !pl』b115hedat! : |2O01ˉ12ˉ12,’ 0∏i∩ute|: 98’ 05core ;

9。1’ ′regio∩50 : [ 0法国‖ ′ !雄国0 ’ 』愈大利0 ’ 』西班牙! ’ !瑞士0]}]} ….



÷



data{0〔ou∩t|: 1oo’ `re5仙1t50 : [{|jd0 : 21’ ,∩a『Ⅵe0 : ‖黄金三镣客! ’ 』a1ja50 : ,I1buo∩o’ i1bIutto′ j1〔attiγo。 ’ 〔Oγer







可以看到,我们轻松模拟了每个请求,并爬取了服务器的响应内容°









由于apPl的接口没有任何加密措施,因此我们仅仅靠抓包以及观察数据包的规律就轻松模拟了







apPl的请求°







9总结



或者出现无法抓包的情况,在第l3章会详细讲解如何处理此类情形°

↑22mjt们p『oxγ抓包工具的使用

』||{

当然’本节所讲的案例是基于一种比较理想的情况,随着技术的发展,ApP接口往往会带有密钥



· | ‖

程序模拟请求实现批量抓取。



另外,知道了请求和响应的具体内容后,通过分析得到请求URL和参数的规律,之后就可以用



本节介绍了借助抓包工具Charlcs模拟App请求的过程°我们成功抓取了App发往网络的数据包, 还捕获了原始的响应数据,并通过修改原始请求和发送修改后的请求进行了接口测试°

mjtmpmxy是_个支持HTTP/HTIPS协议的抓包程序,和Fiddler`Char‖es有类似的功能’只不 过它以控制台的形式操作°

m!tmproxy还有两个关联组件°一个是mitmdump,这是mitmproxy的命令行接口,利用它我们可 以对接Python脚本,用Python实现监听后的处理°另_个是mitmweb’这是-个Web程序,通过它 我们可以清楚地观察到mjtmproxy捕获的请求°



l22mitmproxy抓包工具的使用

539

本节来了解_下mitmp『oxy的用法° |.们|t川p「oxy的功能 m1tmproxy有如下几项功能: □拦截HTTP/HTTPS的请求和响应; □保存并分析HTTP会话;

□模拟客户端发起请求,模拟服务端返回响应;

□利用反向代理将流量转发给指定服务器; □支持Mac系统和Linux系统上的透明代理;

□利用Python实时处理HTTP请求和响应° 整体来看,mitmproxy相当于_个命令行版本的Charles,同样可以捕获与修改请求内容和响应内 容。其实’相比Charles,mitmproxy最有优势的是它的关联组件mitmdump’mitmdump可以使用Python 脚本实时处理请求和响应’功能非常强大°



本节我们先了解mitmproxy的基本功能, l2.3节再来介绍mitmdump对接Python的实现° 尸[■=【『匹●巴【『【■■△■∏■‖『■■「‖‖‖几也卜「

2.准备工作

和Charles_样’mitmproxy运行之后会默认在当前电脑的8080端口开启_个代理服务’这个服 务实际上是一个HTTP/HTTPS的代理。

请确保已经正确安装好了mltmproxy’并且让手机和mitmproxy所在的电脑处于同—个局域网下。 然后配置好mitmproxyCA,具体的安装和配置方法见https://setupscrapecente∏mitmproxy°

|卜‖■『||【尸‖■『』‖{■■「||卜出厂似|▲β「|‖匹■厂『‖『||‖■尸「户

3.抓包原理

让手机和电脑处在同_个局域网下’将手机代理设置为mjtmproxy的代理服务的地址。这样手机 在访问互联网的时候’数据包就会先流经mitmproxy’再由mitmproxy把这些数据包转发给真正的服 务器;同样,服务器返回的数据包也会先到达mitmproxy,再由mitmproxy转发给手机。整个过程中 的mitmproxy相当于中间人’能够抓取所有请求和响应°

这个过程还可以对接mjtmdump’直接用Python脚本处理抓取的请求和响应的具体内容°例如得 到响应之后’直接解析其内容’然后存人数据库° 4.基本使用

■厂|

b嗽·

■■|■尸‖‖‖‖

首先需要运行mitmproxy,命令如下; |…

-亏…了翱鳃

爪jt‖proxy

运行之后当前电脑的8080端[胜k会运行

■ 尸 | 伍 ∏ | 份 [ 【 厂 β 卜 [ 尸 ‖ ‖ ■ ■ 【■厂●=『卜‖

一个代理服务。mitmproxy的页面如图l2ˉl7 所示’右下角是当前正在监听的端口°





接下来将手机和电脑连接在同_局域网 下,将代理设置为当前代理。先查看当前电 脑的IP地址,在Windows上查看的命令如下: jpco∩干jg

[0/0] [0/四

[·:岭S9 [·:龋0]

| β

在Linux和Mac上查看的命令如下:

●【‖

j+〔O∩+jg

图l2ˉl7启动mimlproxy的结果



引 ‖《|||□

■■·‖』·可■∏|

540

第l2章App数据的爬取

■勺‖]】勺】■■■‖|■■‖‖|」』■】‖||』叫‖司

输出结果如图l2ˉl8所示。 厂





D[AS丁’5酗R丁,仅0NNIM】,5〖NPL亡x9川UL‖』L凤3]>们tu1■咖 e∏1日∩□gs=8863<0p’BROA0〔A5丁,S毗盯,R0洲I"6’5mp1〔X,"l)[丁I〔A5丁>们tu15腮 N肿[kI0≥ ◎p七io例S≡q00<〔‖∧N"[kI0≥ op

67:1e;8b et们e『14:7创8d°:67:1e;8b et

i"et6「e8O::10e◎:30e1:88oα:C7C%e∩1pPe「ix1e∩64SeCuFedSc◎pe1 o:30e1:88oα:C7c涂∩1pPe「ix1e∩64SeCuFedSc◎peid0x7

1"

i∩e七19Z.168·31.102∩ebmsk0x仟仟仟09b「oαdCα5七192。168.31.25 1∏ .102∩ebmsk0x仟仟仟09b「oαdCα5七192。168.31.255 ∏d <p[R「0刚N00〗M0> ∩d6op七1o∏5≡Z01<p[R「0刚N00〗M0> 盯e C七 盯e饥□: □u七O5e1eC七

St Stαtus昌 αCt1γe

D〔∧5丁,5№R丁,R0‖‖I‖6’pRO例I5〔’5I川pL[x’卿t丁I〔A5丁>Ⅷtu15硼 e∩2; ∩2; 「lqgS 「lqgs=8963<Up’BRO∧D〔∧5丁,5№R丁,R0‖‖I‖6’pRO例I5〔’5I川pL[X’卿t丁I〔A5丁>

』·Ⅵ■■■□

啡’丁506,〔"∧N‖恒L一I0> ◎p七iO"S=460<『504,丁506,〔"∧N‖恒L一I0> ◎p et们e「82:86:e1】27音50:酚 et 27音50:酚

刚e血α: αut◎5e1eCt<ful1ˉdup1ex> Ct<ful1ˉdup1ex>

闸e

e 5t 5t◎t阴S; 1∩αCt1γe

0〔∧5丁’5灿R丁’R‖"MN6,pR咖15〔,5I川pl[X》咖k丁I〔A5丁> 盯七u15" e∏3: R3: 「1αgS 「1αgs=8g63<up’BROA0〔A5丁’5灿R丁’R‖"MN6,pR咖15〔,5I川pl[X〗咖k丁I〔A5丁> q,丁506’〔‖八N毗L≡I0≥ op七io门s辫60<丁50q,丁506,〔‖八N毗L≡I0≥ Op et et们e广8它:86:e1$27:50:01 27:50:01

Ⅷeoio: α0tOSe1e〔七<fu11ˉaup1ex≥ 〔七<fu11ˉaup1ex≥ St e StαtuS; 1∏αCt1Ve

b沪ioge08∩αg5≡8863<0p’BR0A0α5丁D5酗R丁’R0"N1M’SI啤L[X,Ⅷ[丁I〔∧5丁>丽tu 沪1◎geU8 Tlαg5≡已Ub」<UP BR0A0α5丁D5酗R丁’R0"N1M’SI啤L[X,Ⅷ[丁I〔∧5丁>丽tu15硼



◎口tio∩5≡63<RX〔5l」川.丁X〔5l舰.丁SO4.丁S06> Optio∩5=63<RX〔 l」川’丁X〔5腮’丁SO4,丁S06>

id0:0

0:0目0;0p尸io厂ity0∩eUoti田e0「w“e1αy0

』∏αxαge

0h◎MC∩t0p尸Oto$tp∏℃×α“F100tt耐e◎ut1Z硼

图l2ˉl8查看当前电脑的IP地址

当前电脑的IP地址形式_般是l0.*.*.*、 l72.l6*.*和l92.l68.*.*’例如图l2ˉl8中的l92.l683l.l02°

‖·( 」 { ‖ 』 ‖ (

et‖e『8z:86弓e1 且7:50;0O

〔◎"∩gu「αtio∩:

设置mtmproxy的代理时’设置为此地址即可°



■】|



』司

这个过程中的所有请求,例如在手机测览器上打开百度’期间产生的请求如图l2ˉl9所示°

』■■‖|‖』■』■■‖‖

设置成功后’在手机测览器上访问任意网页或者打开任意App时,mjtmproxy页面上都会呈现出 ●▲●

」■】

P1 十[ ”S

>>〔i[『ht〔pB.//h佩。b口dD。C《听|√加9.’『〔1舟R琴鹏C【 R允blt胞£可38喊85邻V!…68化ep空≡ 43b197闸S

0t ←乙硼t僵xt/hk俯『 『 59.11椭88 59.11椭882川s

S./ O1〔』‖』·L勺刺/∩b 征『httpS。//挪γαOhOl〔|‖』.(q!、l/∩bd厂,‘『1h〃^γ {:γ职∩I◎[毗([5滩uyOB0酗nxN旧INj臣…

γ0凹Co↑te『〕‖ i 376"s

凋0+ext/p『G1′?

αh止p色://SB2bu! ‖`0

P启f叭γq7 p0Q1卿y∩∩q

T

=估6]63609a14229295“『鹏…

20。3梗25弥S

哆吵

Z【 ‖]t十pB;//S52h◎"P 『目0`F′幻撇γ5]闽0M冈{丫枷{{ · (』 1】硼7鳃35,143阴徘86铅千吼 15.82k35咖S

.锄〗浙凰雕,° p;

5.// lα《』 鞘/闹0洲附 6叮http5.//SS0°bmdqw;/闹州雌! p"↓/8W肺q/让/刨w邱823蜘班》142∑9Z鳃池f皿

O 】 <瓣]Fqα哪笋/

〗8 1 11°36代395ⅧE 12 12.Z6代耳29耐5

/田儡嗡b //田儡嗡b



嚼咎划 ;辖划

`jW‘ ` 】〔〕《M

″Ⅲ

γ脱‖、n/

码期腑2287勤M丝9295班f∏L

13 13025h389∩]5

20。69k485阳s

′…』:

.》!》() d+γ奸∩□

』尸‖[6′〕3o511,143鹏令86巫伪…

1156kq69阳5

》 ‖ 刽‖

γ

y↑『凰q′

即j25365铡QM191了209&『‖VL

[*:80晌]

这也相当于我们之前在测览器开发者工具中监听得到的内容°图l2ˉl9中左下角显示的1/31代表 -共产生了3l个请求,箭头当前所指的请求是第l个°每个请求的开头都有—个C[丁或p05丁,这是

型(例如text/∩t们1代表网页文档、i"age/jpe8代表图片)’再后面是响应体的大小和响应时间°总之,

」■■]‖』■可

当前页面呈现了所有请求和响应的概览’我们可以通过这个页面观察所有的请求。

■ ■ 〗 』 ■ 』 」 ■

请求方法,后面紧接的内容是请求URL’URL之后是请求对应的响应状态码.其后是响应内容的类

」■】■‖‖{□■】■]‖』‖』·』■||‖』■■』■可

图l2ˉl9打开百度产生的所有请求



e [1/31]

纠 ■

/S$

】 ′6!丸‘『

」‖■■■

』《le′

∩ 十『》勤。//另$0b们

』 ■ ‖

/岛蓖1耀p 凶[ ↑!儿t『〕今//鳃1砷《{‖』 《, /、γ气参 【]↓ !/′ γ枷q/ ?/ 6币“M鳃寻,1398Sq72路f懒建肆

t『°

|』■■】‖

.ˉ 2船飞d↑↑卤GC 书吧C

■■||

队了 〗吱↑p吕.//SS】 oα山〔!占 ′院汕x5,1砷0I【Rk沁∩q/ t/u鼻6H0897黔8?1430』Q脆路f泽.闰 (

□■■可‖』‖』闪』■∏|」□■■■‖‖|」‖』□■■」‖■■

′0粉1硼αqG (〗

《i『丁http巴//鹏^b旧}咖(◎船/ 门迅『。!.c∏ 103γ5S卡

|」■可《』■

□』■可|』■可』‖』■■■■■■■■

』|』



■「

l2.2mitmproxy抓包工具的使用

54l

如果想查看某个请求的详情’可以通过上下键切换,选中这个请求后按下回车键,进人请求的详 厂

情页面’如图l2ˉ20所示。 β归



● ·′■ 口

■ 勺■ 儿比

·飞



■【≈

^Ⅶ





℃□】

=‖



》‖‖【Ⅱ■厂【【』■【■尸「■尸|

T

fl

L

+巴

鱼 臼

》 【







卜【■【『■■「|卜匹■尸||■■厂‖「■【◆『‖

F



吐■

『∏■

蹦 β 纱 嚣

’Ⅱ

■尸



}「



匹尸「》『ˉ■厂



▲当』≡=-_

图l2ˉ20请求的详情页面

从图l2ˉ20中’可以看到请求头的详细信息,例如‖o5t`〔oo代je等°图的上方有Reque5t`尺e5po∩5e

和0etaj1三个选项’当前是打开了Reque5t选项°按下Tab键切换至Re5po∩5e选项卡’查看请求对

■β

应的响应详情’如图l2ˉ2l所示° ==一=一-

●攀狞

--■=亩

… ≡-ˉ-ˉ—-—--~=w≡—撼

□‖● ‖●

■■即 ■■■比



厂≈

∩啪

七α



』 ∩Ⅷ {

尸↑



】 ■■‖■





■■



^…■



们「『「卜队″》∩‖户β■■尸



厂 · ■

↑2 b

‘《

】叫

}}

■厂■尸|■「‖『=■「



, .瞬哪]

图12ˉ21

响应的详情页面

卜【)

开始的内容是响应头的信息’下拉之后可以看到响应体的信息°针对当前请求’响应体就是网页 的源代码°此时再按Tab键切换到最后_个选项卡Detai1’即可看到当前请求的详细信息’如服务

▲■『‖■『『但〗

器的IP和端口、HTTP协议版本`客户端的IP和端口等,如图l2ˉ22所示°

厂◆「》「》

mjtmproxy还提供了命令行式的编辑功能’使我们可以重新编辑请求°按下E键即可进人编辑页 面’这时它会询问要编辑哪部分内容’选项有Coo长je5` querγ` ur1等’如图l2ˉ23所示°









第l2章App数据的爬取 厂

●』与■ 『io介0etαII Lo介 【」etαIlS 出

了■



亚旧

α

■【‖

β







。亡



·Ⅱ



■■白

〔α

仁‖

=飞



佰■

γ





ˉ仔



〉■

辽 巳

『 ◇



尸〔



厂`



尸令



户■

γ

「 巳



尸□

;

β





日·

■■

△个$

.}

图l2ˉ22当前请求的详细信息 __≡

●才◆

「1c士帅它□{lS



●巴

α。

品 〔

∩肌



…—pg「0--ˉˉ=〗

B :`』‘『 .$ 舌 □ □ ·

f

.

0

.■·

0 ■





● □ 〔P

〗■ 〃 ′}」pl..i · 0`



U





‖q哗



-■■■■≈■~■■■■■=-■==■=□=



■〃

孔■



|*:8份80』

图l2ˉ23

编辑请求

面’例如按下数字6键’ 按下要编辑内容对应的索引键即可进人编辑页面’例如按下数字6键进人编辑请求URL的参

数QueIyStrjngs的页面’如图l2ˉ24所示。我们可以按下D键删除当前的QuelyStnngs’然后按下A键 新增-行’输人新的Ⅸey和γa1ue。 =~__丁

口_——-_≡甲

●守■ Zi 〔βi↑.0!』 C『·γ ■



贝cy 旧》 门□ □

O

■。‖■勺】{■■|■可||」■□|‖|‖」』|」·】可‖||■■|々』■』‖‖』‖||司(|」司|·日(‖‖日』』‖]日』·||刘|■』‖■司`〈□||■■■』·`』■』■·`|』■】‖{」』■∏〗‖‖]·‖‖‖■Ⅵ□·』可■」■■■||

542



~●≠

巾△



「巴

]〖 《▲目阳80]

图l2ˉ24编辑QuelyStrings的页面

这里我们分别输人wd和‖8∧°之后按下Esc键和Q键返回请求的详情页面可以看到请求URL里 多了一个wd=NBA的参数,如图l2ˉ25所示°

■■■■■■■『■■■‖

543

接着按下E键和数字4键.进人编辑Path的页面,如图l2ˉ26所示。和上面流程—样,按下D键 和A键修改Path的内容°

这里我们将Path修改为5°之后按下Esc键和Q键返回请求的详情页面,可以看到请求URL变 成了https://mbaiducom/s?wd=NBA,访问这个页面,结果如图l2ˉ27所示’这和用百度搜索关键词NBA 的返回结果—样° 吕

■仕

】■

屯皿

勺卜





〗』

■ 【



■■

比■



▲■『

■巴



■■■■■■【□‖‖■■「‖||卜●「{{广「『‖‖)}卜「|∩‖〖『‖[矽『|■■『}■『『‖‖△■

l22mitmproxy抓包工具的使用



■■「匹『『〖◆止■匣



坠i撼≡睡



耳■碌

‖|■■■『|『||炉区二=■厂‘■=■厂

[篱"弘 圭部

视然

—m~下 | …醛ˉ 溉议

叠熟

砧霉

羹国职业篮球联赘

土`帆辗



0m

"202呕↑









■程

搏名

球队楞

琼员捐

官网



■■



飞■

赛程〉 £·目8鹏0

0S22肺失

哩:o0

图l2ˉ25更新了修改后的请求页面

攀舷宿

宁m{巳:强.巨

●■● ‖

』乡■

厂『





屯‖











■∩匹■『』『‖》卜「|}■}■厂[||巴「●「』『■尸■■■【■■虫匹二■『此■■■■∏广『〖尸任‖上厂·【『【广『■厂‖‖‖‖Ⅱ■尸‖||}|{■■□厉





0S赶3′今天

●热火

未开赛

γ趣囊

凹:30

●独行侠

琅膳赛

选快佃

鹏;"

攀屠纂

朱开峦

o豌尔特人

未开F

币源网 宜■金部蜒

『°目把棚‖|



‖|■■「[■匹■■「‖|||■「}

热议》

图l2ˉ27访问修改后的URL的结果 图l2ˉ27历|口』修叹′百田ORL助结未

图l2ˉ26编辑Path的页面

接下来’按下R键重新发送修改后的请求’可以看到请求旁边多了一个回旋箭头,如图l2ˉ28所示° 曲

α

■■·■



■■巴

■巴



『『■

训 ◎

∩=



[(2

十巴





巳←



∩托



咕β■』



0

′~

■■「||但『『》[●『||=■『‖【止■尸『‖【「卜【『[尸一■厂|}卜【■「【■厅■【【■『卜「■【伊【尸‖■厂·【【尸》〖■∏巳■【凹·「止尸|『

[0已●

〔●;808叫

[36/36]

图l2ˉ28重新发送修改后的请求



544



第l2章App数据的爬取

响应结果如图l2ˉ29所示,观察响应体’可以看到图l2_27所示页面的源代码° 川『

【 ■

■■■

■■■











^Ⅱ





●. ◆



R撒$『〗O∏qe

倪· :§飞雅

=■■ 0



□00

O广

互l 8严

』 【M

‖·目8080]



□ ] | 』 ● 】 ■ 】 ‖ 』 】 ■ ■ 】 ■ ‖ 』 ■ 』 ■ 』 』

[36/36J

图l2ˉ29响应结果

5.总结 』 ‖

本节介绍了mjtmproxy的简单用法,其基本功能和Charles是类似的,只是不像Charles那样有方

」 ‖

便操作的UI界面,不过快捷键使用熟练后’也是非常方便的。

』 勺 』

利用mitmp『oxy,我们可以观察手机上的所有请求’以及对请求进行修改并重新发送。Fiddler、



我们可以在scrjptpy脚本里写人如下内容: de十reque5t(十1o倒):

+1Ow.IeqUe5t·he日der5[‖[」5erˉAge∩t‖] = ‖例jt‖prOxy‖ pr1∩t(+1o"·reque5t。‖eader5)

其中定义了~个reque5t方法’参数为「1o"’这是一个‖丁丁p「1ow对象,调用其reque5t属性即可

□■」‖』』‖‖』】■□□‖』·■■∏】∏』■■●〗】』■‖司』』司‖』

这里使用ˉ5参数指定了本地脚本sc∏ptpy为处理脚本’用来处理抓取的数据,需要将其放置在 当前命令的执行目录下°



|」(|

mt∏duⅦpˉ55〔rjpt·py

「』□】■■■■‖〖‖』■■】

我们可以使用命令启动mitmdump:



↑.实例引入

·

正是由于mjtmdump可以对接Python脚本’因此我们在Python脚本中获得请求和响应的内容时’ 就可以顺便添加—些解析`存储数据的逻辑’这样就实现了数据的抓取和实时处理°

·

求和响应的处理逻辑即可。



mjtmdump是mi!mproxy的命令行接口,可以对接Python脚本处理请求和响应’这是相比Fiddler` Charles等工具更加方便的地方。有了它’我们不用再手动抓取和分析HTTP请求与响应,只要写好请



↑2.3们|tmdu阳p实时抓包处理



Charles也有这个功能’而且它们的UI界面操作起来更加方便°那么mitmproxy的优势何在?其实, m1tmproxy的强大之处体现在它的关联组件mitmdump上,有了mltmdump,我们便可以直接对接 Python脚本对请求和响应做处理°l2.3节我们就来学习mjtmdump的用法°







mitmdump实时抓包处理

l2.3

比宙.●山钮■

"36扩d匣

☆D

hl0pb|∩O′g

·

获取当前的请求对象°这里将当前请求对象的请求头0Serˉ∧ge∩t 修改成了‖jt"proxγ,然后打印出了所有请求头°

运行启动命令后’在手机端访问h仗p://wwwhttpbin.org/get.

J

1

"a厂g£“ : {》′ 武∩e已dG厂3●2 《

可以看到手机端显示的页面显示如图l2ˉ30所示。

"∧(〔ept占:

∏》■●尸



545

呵rext/hm1′己pp11cat1◎∏/xhtm宁xⅧ1『己pp11〔a[1α]

/x■1jq≡0°9’1酗8e/eV1「01砷8e/…,1旧age/己囚g′

同时电脑端的控制台输出如图l2ˉ3l所示的结果°

古/古jq=o°8‖印p11℃己t1αM518庇dˉ

e又c陋∩8ejv江b3;q■0,9臼‖ 耐∧仁乙eptˉ〔厕COd1∩g回8 ■gZ1p″ de+1ate翻0 ●∧C〔epr←L■∏g`』age司: ·e∩ˉ05′台∩■′

图l2ˉ30中的beader5实际上就是请求头’可以看到里面的

坷什o占t宙自 "村t[pb1∩,O厂go, 0

白p「Oxy-CO"∩ect1O『l飘: ←heepˉauγe勘′ 『 尸upg「■de-I∩Be〔u了eˉ∩equeSt£团: 凶沪|

「}

05erˉ∧ge"t是我们修改后的‖jt"proxy°图l2ˉ3l中的‖eader5也 是,其中05erˉ∧ge∩t的内容正是‖1tⅧprOXy°

内05e『ˉ∧ge盯t伪: .mt硼『oxy凹』 ■X空知m=丫『己c■ˉIdp: ■∏mt≡‖ˉ6由932「c= 4凹b了07〔44f56CCaO32●5↑45■

}′ 闻◎「1g1∩■: ·‖67.220°∑33.↑57■0

在这个案例中,我们通过只有3行代码的scrlptpy脚本完成

伺u「1伊: ■∩rtp:〃httpb1∩·O厂g/ger■ 》

了对请求的修改’输出结果可以呈现在电脑端的控制台上,调试

起来很方便。







·尸■■「

192.168.31·177:3q970; 〔Iie∩tCo∩∩eC七

尸【厂|■【■

‖eode厂5[(b!‖o5t| | b0恍七pbj∩.o厂g′)’(b,p「oxyˉ〔o∩盯ectio∏0》 b0促e印ˉ□l1ve,)’《b‘∧Cce pt-lα∏◎uOo0′· b℃∩-05’e∩,), (b!l』pgF◎deˉI∩5e〔u厂eˉReqoe已t5! 》 b‖1?)’(b『u∑e「ˉ蛔印t, , b0例it们p厂o×γ! ! , (b0几ccept0 $ b0text/∩t闸1·□pp1tcαt【m/劝t∏l+x田1’αpp11c口tim/ml;q 叼镶g’Ⅷoge/αV1「,mmge/Webp』imge/αp∩g,v/愈;q=0.8’αpp1im七to∏/5tg∏邮备exCm咽e;v亡b3 jq唾0。9|), (b’∧CCeptˉ〔∏c◎dt∩g` , b,gⅢip’αe「I□te『)] 19∑·168,3]°177:349q2: 9∑·168,3]°177:349q2: b七tp巴://Odα.boMu琶〔呵/u如17“tα蜗7蹿2s=i″酗…23 b七tp巴://Odα.boMu琶〔呵/u如17“tα蜗7蹿2s=i″酗…2se α下^ 「^

<< <≤ 夕ˉ





°

、/

/`、



占凸 .!△43b

http://‖ttPbi∩令o「g/ge亡

19乙l68·31.177:34968: 9乙l68·31.177:34968: ..

Z; 0 !..X弘1b

·口

eαde丁5『(b0‖o5t『 · 《b‖p『oxVˉ〔o∏∩ec七iO∩0 ,° b0代eepˉ□Mve0). (b盯0三e ‖eαde丁5[(b,‖◎5t『 , b0们仕Dbi∩.om,)· b!们仕Pbi∩.o了g,), (b0p沪o×yˉ〔o∏∩ectiO∩0 b、促eepˉ□Mve,), (b盯0三eP 「 ◎ ˉAge∩t0 ’ b|佃】t巾p7o×y『)’(b,A〔cept, , b0t∏℃ge/ovi于,i‖mge/凹ebp0imge/op∏g,i胸ge/■’嘲 又 /*;卜o8,), (b,Re「e7e丁, , b《肚tp://httpbi∩◎「g/get0)h (b0∩CCGptˉ印C◎m∩g0 ’b,gZ1p 口】

γ=







q叼.8,■∏6q毒0.7o)] ’ defmte『),(b`∧c〔eptˉk口∩guGge, ′ b{e∩ˉ05,e∏;q=0.9,劝ˉ〔‖;q忘0。8’■∏;咋0。7o)] ◎ 『←



■■■》卜

19乙168.31·177:34%88 它`

盯 打ttP://砒tpbi∩.o「g/fαγic◎∏.1C◎ 巳 仁 °亡

图l2ˉ3l

图l2ˉ30手机端显示的页面

电脑端控制台的输出结果 〖·





·



β

2日志输出



·佣

巴 巳 β』

mitmdump提供了专门的日志输出功能,可以设定以不同的颜色输出不同级别的结果。把scr!ptpy α

■』

卜 | )

` γ 号

脚本的内容修改成如下这样:



·





·

十rO们mt∏p【OXy1们pOrtCtX

∧ ℃



卜 △∏「| 匹尸■『‖|■‖‖‖}}巴■尸‖‖|||∩

de+reque5t(十1o"): 「1o"·reque5t.∩eader5[U5eIˉAge∩t0] ≡ ‖‖jt们proxγ 〔tx·1og·i∩十o(5tr(+1ow.requeSt.打eaders)) 〔tx.1Og。"ar∩(5tr(千1oW.reqUe5t.‖eadeI5)) 〔tx·1og.erroI(5tr(「1ow.Ieq(』e5t。he日der5))

这里调用了Ctx模块,它有—

个名为1Og的功能,调用不同的输 出方法可以输出不同颜色的结果,

尸卜仍『》}户|‖『|β[[[尸【「「

以便我们更直观`方便地调试(例 如’调用j∩「O方法输出的结果为 白色,调用War∩方法输出的结果 为黄色,调用errOr方法输出的内 容为红色)。上述代码的运行结果

●『



↑2 h



"eα口e厂s[(b!‖ost! , b↑打ttPbi∏.o厂g『), (b0p「oxyˉ〔o∩∩ectio∩′’ b‘促eep-o1ive,)p (b『〔□c" eˉ〔o∏t尸ol, , b,|mxˉ□ge→0,), (b『Ac〔GptLo∏guoge` · b,e∩-‖;,e∩,)·(h『0pg厂odeˉmsecuF eˉReque5ts0 ° ·,1,)’(b,05e「-∧ge"t, , b,川1t明白厂oxy,),〔b,∧C〔ept, o b0teX亡/‖饰1,□pP1i cαtio∩/xht川l+x闭1’◎pp1iCαt】◎∏/xⅧl;q=0.90i『"Oge/αvif,t∏皿ge/webPoi硒ge/O尸"go本/▲;q助. 8瞻◎PP1icαtjo们/sig∩edˉex〔hα门ge1v醒b3;α驹·9`), 瞻◎pp1icαtjo们/sig∩edˉex〔hα门ge1v醒b3;α驹·9`)》(b,∧ccept[∩怎odt∩gv (b,∧ccept七∩怎◎dt∩gv , b『gzip, def1吭e

‖)] )] "〔!啊吁『鹰鸟『( 〔!啊吁『鹰鸟『《. ., °}|‖ ‖ P(『》咆[『飞〗 (『》咆[『飞〗 |. 『

气龟《d0M`『 !

L罗 罗 ’. 0 ′ 『 $ˉ色■ ·色■ 飞 . `

’ . .! , , j` .i

?

《`″;:‘)! ( |蹦. ‖ 。.

.



.°?

· .‘ ,··

. :. . .、 、 『 ˉ『 : . . 0 『. .:



, ,?





√ 0 :.‘鸣0, . . ; .铲 · b" b□ :邮《‘{ !γ§P !v§P 〗. 『‖′「.咱旧§! 「.咱旧§‖ ’〗 矿

|0... ‘ 『

审 『

.



》· l邑

』『` , ? 此`

· . i、刁°o〗p:G』.z旧0. i、刁 ·〗p:G』.z旧0. 6尸巴0.铲』尸

0·【】V

. 幼巴挡b亏 0·宁=[.门pp0 ‖

』.. . 0…, . . .‖『』q .`.o0F‖ 0pU 内畔l0 :』p:恩h¥△曹·qc§】 . 0 . . . β圃日 田0;0 ‖0 咀口厅;宁. gqP· . i〗↑0 i〗↑0g ■

如图l2ˉ32所示°

图l2ˉ32使用1o8功能的运行结果

卜 旧’『■■



‖{

| ‖

■■||』】|■』

第l2章App数据的爬取

546

3.请求

我们来看看mjtmdump还有哪些常用的功能,先用_个实例感受一下: 「ro∏mt们proxy加port〔tx



‖』

de十request(千1o"): Iequest=十1ow.req0e5t j∩+o=〔tx·1og。1∩fo j∩fo(req0e5t.ur1) i∩十o(5tr〈reque5t.he己der5)) i∩千o(5tr(IeqMest.〔ookie5)) j∩千o(reque5t。host) 1∩「o(reql』e5t.眶t‖od〉 j∩千o(5tr(reque5t.port)) j∩千O(reqUe5t.5〔∩e们e〉

勺( {

这里的reque5t方法是mltmdump针对请求提供的处理接口。将scnptpy脚本修改为如上内容, 然后在手机上打开hnp://wwwhnpbinorg/get,即可看到电脑端的控制台输出了一系列请求。这里我们 找到第_个请求,控制台打印出了该请求的-些常见属性’如请求URL`请求头`请求Cookjes、请

■ ‖ 〗

求Host、请求方法、请求端口`请求协议等’结果如图l2ˉ33所示°

|』■‖‖■‖

我们可以修改其中的任意属性’就像最初修改请求头05erˉ∧ge∩t-样,直接赋值即可。例如’修 改请求URL: de+reque5t(「1o"): ‖r1= !http5://洲·ba1du·coⅦ‖ 千1ow。reque5t·ur1=uI1

姆锦om妇■

":‖6围沙0

刚pb‖∩.◎『g/get

勺‖(‖

这甲直接将请求URL修改成了百度,会发生什么事情呢?手机端会显示如图l2ˉ34所示的页面。

× 四四

」‖|









田 已

● ● ●

[函di∩gSC7ipt巫Fip七3°py p厂o义ySe「Ve7IR∑七e∩Ⅱ∩口αt肚tp://叮:8080 αt肚tp;//叮:8080 192.168,31017783335Ⅺg C1ie∏七Co∏∩e〔t 1ie∩七CO∏∩e〔t Ite门七■o∩∩eCt 192·168.31,177§35354: 〔Ite∏七■o∏∩ect

h七tp://httpbt∏.oF9/ge七 γe『 爬□口e厂£[(b‖什o戴, ’ b‖httpbi∩.◎Fg,)’(b,p广Oxyˉ〔o∏∩ectio∩『 , b,促eBp←°Mve『), (b,〔oc∩ e=[mt7ot,』 b,砸x-口ge副0),(b0Accept=Lo∩gUαge,’ b0e∩-0S,e∩|), (b0Upg庐αdeˉI∩5eCu「

/■≈q ·←■≈←≡~.~△■■==·

O ~■≡□

ˉ ˉ_ ——] ■ | 盲陛下| 」

eˉReque5ts, ’ b‖1,)o (b0U3e广ˉ的e∩七. , b!№乙i11α/5.0〔U∩u×; ∧∩d「otO11; g酗50础铡 ˉ698“)八PP【e"e冰it/537°36(Ⅻ『M1p l让e6ecko)5m≡u∩gBm们已e沪/14。0〔‖庐咖e/87.0.4 28O·1q1№b11e5口「o沪1/537.36,),(b0∧〔Cept, , b『text/∩饰1′Opplicαtio∩/x门恤1+咖1↑αp pli〔■tiwl/Ⅺ∏l6q竭。9,Umge/□Vi「,tmge/W它bp,i硒ge/αp∩g’■/■;q=0.8poppl1C□tio∩/51g∏e

0

上滑肋■Ⅲ多

dˉ酗C陋叼e;γ中3;…,9!), xCh□四e;γ=b3;…,9}),〔b!Ac〔Gptˉ[∩cod↑∩g! ’ b!g∑tp, de「lαte0)] №1七i0tCtγieW口 七i0tCtγieW口

日■户皮饿

h吐pbi∏.O厂g pbi∏.O厂g 征丁

鞠攫呻$诅………F翻警

80

礁琅…●们…$嘲

偶ttp p .168·31°177:3535∑己 6[『批tp;//∩ttpbi们。o7g/get 6[『什tt口;//∩tt口bi∩.o7q/αe 19Z.168°31°177:3535R:

图l2ˉ34手机端显示的页面

有意思的是’测览器最上方呈现的网址还是h忱p://wwwh仇pbinoIg,而页面已经变成了百度首页。

」■·司』

图l2ˉ33输出结果

我们用简单的脚本就成功修改了目标网站’意味着这种方式使修改和伪造请求变得轻而易举’这也是 中间人攻击°

范意识.

』■·■‖‖‖

注意通过这个实例我们也能知道’URL正确不代表页面内容也正确。我们需婆进一步提高安全防



日‖



「—



△■■■■田□巴■■尸■=

l23mitmdump实时抓包处理

547

了解了基本用法’很容易就能获取和修改请求内容,另外可以通过修改Cookie、添加代理等方式 规避反爬。

4. ‖阿应

广『□‖■Ⅲ〗Ⅱ〗【■■『↓卜■尸『}卜▲■【|}■■|

对于爬虫来说’更加关心的其实是响应内容,响应体才是要爬取的结果°和请求一样,mltmdump

针对响应也提供了对应的处理接口,就是re5po∩5e方法: 十roⅦmt川proxγ1‖port〔tx

将sc门ptpy脚本修改为如上内容,然后用手机访问h仗p://wwwhttpblno『g/get,电脑端控制台会输 出响应状态码、响应头、响应Cookie和响应体几个属性,其中最后一个就是网页的源代码°电脑端 控制台的输出结果如图12ˉ35所示° =--甲≡尸=≡亩丁■弱可=ˉ=厂丙≡声ˉ=尸

●咀′锄

92.168·31.1了7;玉81q目 b『 】 "ttp:/′∏t…1∩.o厂g′庐℃

撼憋聪}墓蕊搬磷撇鹅 <<沁P‖0§7硷



eαde厂5「(b!D口te, bvSot, 2Z蛔γ20乙116名跪;凹帆『『),(bo〔o∏te∩tˉ丁ype,, bu印PMC■

Olˉ∧11叫ˉ〔广ede向ttα15, ° b,t了仙e0)] 】亡F°lˉ∧11。"ˉ〔广ede∩ttα15,, b’tr仙e『)]

1tt0i仁tγte佛[]

辨`…M.瞬[] 围』

■■尸|‖■份【‖卜[β■|卜‖■『‖■『|°|卜●【『●『■∏Ⅱ厅「『巳∏卜【『「●『|△■卜『『■广【β

de千re5po∩5e(千1ow): Ie5po∩5e=十1ow。re5po∩5e 1∩十o=〔tx.1og.j∩十o j∩十o(5tr(respo∩5e.5tatu5〔ode)) 1∩十o(5tI(re5PO∩5e.∩eader5)) j∩+o(str(re5po∩5e.coome5)) 1∩十O(5tr(IeSpO∩5e.teXt))

时α下gS°.日 {}, 『′阶eode「5": {

”A<ce口t萨: ∩/x贼∏1+x刷l,oppl飞COt1O″x朋l己q幻·Ⅵp〗∏ °.贝cceptoˉ: ‘0text/∩七晌l·◎pD1iCptlo∩/x∩t∏1+xmI,αpp1tCOtiOn/Ⅻ‖l;q刑·9’i硒ge/αⅦ「0 ·,teX七/"UNk’”pl1C II〔□亡io∩/Stq∩edˉex〔们◎∩qe;γ■b35α△ag俐 ;q=0,8 》αppII〔□亡1o∩/S1g∩e◎ˉex〔门◎∩ge;γ西U卧5α巴 1∩旧ge/Webp‖1霞oge/°p吨0●/·;q咐·8 Z已p》 de「1◎te", 00A℃Ceptˉ巴门Coαj咱呵; "g乙tp》 d ∩=U5‖e∏!‖ ′ 00AC仁ept-L□∏guOge°{8 "e∩~U5‖G 腻〔αC‖Te←〔O∩↑厂◎1≈. °°∏蛔又ˉ□ge=0 ˉ□ge二9", ,『〔O◎kiG‖. 吕 ˉ.BD5γR丁问二叫20|,

"NO5t,.: 辨"tt口b1∩。◎Fg"’ 促eeOˉ□]1Uc,0 ,0p尸◎又y-〔o门可CCtIO∩,,: 0譬足eeO-□ ue5t5陋『 !01°0 0 ‘Upg了OdeˉI片5e〔M厂eˉReque5t已0· 1t o/5°0(L1声凹x‖坪`α尸eⅡd11; Ⅶbse厂→∧ge∩t, -∧ge∩t,:吕 陋蜘Z飞uo/5°0 Ⅸ`炊γ乙飞uo/5`0 (L1声凹x‖ 鸟^α尸eⅡd11; S酗S0N65M←698田)∧p@Ie‖e冰i S州S0‖65M←698田)Ap·1e‖e冰i

f◎ /5〕7.36(贝"了Mk, 5OF 鼠u∩g8厂◎★Se「/M·0〔们?″?e/8700舍纪80°MXlqobtle5口+ 贝"了Mk, 11唾〔e〔l〈o〕 l1ke〔e〔Ro〕5O同 庐飞/537ˉ36』,

图12ˉ35电脑端控制台的输出结果

我们通过re5po∩5e方法获取了每个请求对应的响应内容’接下来对响应信息进行提取和存储’就 成功完成爬取了°

5.实战准备

我们已经可以利用mitmdump对接Python脚本实时处理响应内容了’接下来看看能否将其应用于

App爬虫°先尝试将一个电影App的接口数据爬取下来,再将结果实时保存到MongoDB数据库中。 请确保已经正确安装好mim聊oxy和mitmdump,手机和电脑处于同—个局域网下,以及配置好 了mjtmp!oxy的CA证书,具体的配置流程可以参考htlps://sempscmpe.centeⅣmi‖mpIoxy°

本节使用的示例App的下载地址为hUps://app5.scrape.centc∏,请在手机上安装这个App° 6.抓取分析

首先获取一下当前页面的URL和返回内容,编写—个脚本: de十re5po∩5e(+1叫): pri∩t(f1ow。reque5t.ur1)

prj∩t(十1o".re5po∩Se.text)



第l2章App数据的爬取

548

这里只打印了请求URL和响应体这两个最关键的部分,

聪毕o蝎酗■ |

0‖:凹…r·

■嚷嚼圃" 圈剿麓… 团蕊熟翻. ■霖辊

将此脚本保存spiderpy文件。接下来启动mjtmdump:

,0 ,0 90 9‘

■男刀零潭口″

Ⅶ1t∏dⅧpˉ55pjder。pγ

打开手机上下载的app5’便可以看到电脑端控制台输出 了相应内容’接着不断下拉’可以看到手机屏幕上的电影数据 在_页_页加载,如图l2ˉ36所示。

观察—下电脑端控制台’可以看到输出了类似JSON数据 的结果,部分结果如图l2ˉ37所示°

,0

==翻饶`谢愉、奇幻



■爵锐 图疆霞 ■嚣慧..

,0 90 9"

巫髓凳敏…

90

囤费鞭隐 勇器繁…

,‘ ,‘

·

杜tpS:/″p5°二CFαpe.Ce∩te厂mpi脑γie?o仟set宏蜒]imt=1

0= <<



ttp已://αpp5,$e卜α@e“ce问te厂/口pl/丽◎Vlc/?pf「s儡t■3魁1[饥七=1鹏七O挝e∩喇良05川VγxZj[ http∑://印p5·甄跑pe′ce问te厂/口pl/丽◎Vle/?Q仟罪t密3魁1Ⅷ1七=1鹏七O挝e∩喇良05财γXⅢ][毗‖酗 鹏七O良e∩喇良05川】γXⅢ][… pⅧγ刑γ州『IzZ6队γ耐zRγ卫αt盯∩1∏g乙…γWx‖jIx‖z∧酗DA5…

{脚〔Ou∏t‘|810a"尸esu1tS0『 。[{"id"吕驹!"∏o赃『!: !『上帝之城","αIi□5『0 □5『‖ :; ′,〔idα6ede陇U5辅,"〔Oγ ′,〔idα6ede0哩u5割·咖〔o

eP":凹∩ttp5://p1.↓∏ettu◎∩。∩et/门oγ1e/b553d13f30100db731口b6仁“s氏8e52d94703.Jpg鳃田w ˉ604h一1eˉ1〔阑0"Cαteg◎厂ie己厕:[圃聪倘川,"犯辙‖{]’叫pubIi5批edˉ□t。『 ;"ul[’"阳i∩ute":130’·0已〔◎7 e赋:8.8′"厂e9iO∩■":["巴西"0"法圈"],"励『炯□恤:"巴西望的炔内卢的贫民窟》这鲤愚“上帝之 城碱, 更思魔鬼也舍叹恩名转身的地方.闽炮(亚历山大.罗撂里格斯饰)带揖我们到了这 里,他见证了灌里二十参年来娥践翼、贪蛰`麓仇野心防叛、掠夺所翼挟的翟乱生活以 及虽终导致的.=场灾难性的屈帮争斗°墨然从小就蜜鞠转干匪征闽东纬存,但胆′」`泊驭的性 铬与自我保护的本能卸使他ˉ直能平安凄日· 60年代棚.阿毛陶夹和阿果翅这里的“少年 三侠藏°在抢吨完罐馆之后,他们三人分通扬键, 阿夹瓣回上帝的怀抱, 满阀果和阿毛纷纷

付出了生命的代价· 70牢, 当年“少年三侠"手下的小弟小豆子(遭铬拉断.席尔瓦饰)■疆 自己的心狸手簿’不停地吞井别人的地凰成为了贫民区的“小攒主",生螺也从抢劫升级到 腾达《 了更为狠利的蘸品买樊’稠他一涵飞负腾达的还有腑尼〈p∩e1Mpe储樱森饰) ) ·班尼认识 ·

--——■

"!



1gz168.31177:361田巨 $

〉‖准爸归隐在遇别的晚台上, ‖ 〉 外破 了受阑的安遥丽卡(艾翱丝。布拉拽饰〉 他葱外破对头杀窘 掇仇, 0思揭之下, 解小霸王’,狼合人手结册尼獭仇,帮流之间的际杀耽此开始°此时的闷炮·机缘 巧台下哦为『杂志仕的见习碾影师,他的相机,照下的却屉孩子们持枪核弹的狰狞‖ 租甲溢 闽无休无止的仇杀 "],{°′id .31{"∩O爬期↓:"辩沪人,′’ 0僻α11αS,0 :‖′翘登凹Ⅷ,`℃OVG广卯:脚http二。//

p0.meltm∩°∩e七/硒γ1e/1牛3〕d81b19d116Z”dbb「0E◎呢肛3c19265682.jp鳞耳6q们-6“h~1eˉ1〔凰



,|,CotegO庐ieg|『 ?[口瓢悯厕]’ Publ飞写侗edˉ◎七闪: 0『2013ˉ12ˉ18!』’"m『‖Ute,|:1乙7,0『sco尸e":8.8’厕厂eg

图l2ˉ36不断加载的电影数据

图l2ˉ37控制台输出的部分结果

选取其中的—个URL观察_下’具体为https://app5.scrape.centeⅣapi/movie/?ofISet=30&|imlt= ]0&token=M2U5NjYxZjEwNmQyMDlkYmYyNTIzZGFkYmZkYzdiNThlYTgzOWQ0MywxNjIxNzAzM DA5%0A’可以看到除了ofTSet、limit参数’里面还带有_个token°我们把结果中的响应体复制下来, 并格式化处理,结果如图l2ˉ38所示° 厂

由data·jS◎∩

●●● {

‖0COU∩t』0 8 1000 "厂eSu1tS"8 [ {

001d凹8 30’

00∩己础00目 M上帝之喊,00 p0日11己5068 00〔1dadedepeus曲』 ‖0Coγe「00: "∩ttps日//p1。爪e1tua∩.∩et/mγ1e/

b553d13↑301O0αb731aD6C闷5668e52d94703.jpg@▲64比644∩一1e-1〔0‘0 00C□tegO「1e5凹: !"图恫"’"犯耶闪]c 00pub1工∩edat0‖8 ∩uu’ ‖0m∩uteⅣ8 1300

0『5Co厂ego 弓 8·8』

"厂eg1o∩s00吕 ["巴西0o, M法国00L

‖0d厂酗a愈0: 00巴西里约热内卢的贫民■,这里是·上帝之城″′Ⅲ是臣鬼也会叹且岳转身的地方·阿炮(亚历|』}大°罗穗里格斯饰)

带吞我们到了这里′他见证了这里二十多年采被残毋、贫婪`Ⅲ仇、野心、背叛、撩夺所裹挟的泡乱生活以及虽终导数的-场灾难性的黑帮 争斗·虽然从′」吼Ⅲ疆转于匪徒间求生俘,但胆′』`怕F的性格与自我保护的本″使他-■能平安度日° 60年代初’阿毛`阿央租阿呆矗 这里的″少年三侠″,在抢劫…馆之后.他们三人分沮扬■冈央疽回上帝的怀抱’而闽果和阿毛纷纷付出了生命的代价·70年‖ 当年“少 年三侠”手下的小弟小豆子(沮格拉斯.席尔瓦饰)m昔自己的心狠手赚′不仰地吞并别人的地盘’成为了贸民区的■小■王厕,生∏也从抢 劫升级到了更为贝利的毒品买耍『和他-起飞负冈达的还有班尼(P∩eu1pe。睹根森饰)·班尼认识了荧丽的安迪丽卡(艾瓤丝。布拉加 饰) ,准备归障,在送别的晚会上,他窟外破对头杀吝,悲瘴之下, 」′小窟王蟹集台人手给班尼报仇』帮派之间的房杀酗开始·此时的阿 炮, Ul绝巧台下成为了杂恋社的见习涡影师,他的泪机』照下的却是孩子们持枪核弹的狰狞’和帮派阎无体无止的仇杀· 0‖ ` 』』



‖‖1d↓0: 31′

`0∩己爬‖"8 m辩护人,00 k

Ma11aS↓‖ 8 0『岂重钮‖o′

:: 二

;

图l2ˉ38格式化处理后的响应体内容

‖‖



l23mitmdump实时抓包处理



549

p

通过对比可以发现’图l2ˉ38中的内容和app5里显示的电影数据完全一致,说明手 说明我 们成功截获了 响应体。 b

|》‖

有人可能会想,为什么这里不能像l2.l节那样直接用Python请求爬取数据呢?区 数据呢?因 为这次ApP的

接口带有一个加密参数token’仅凭借抓包根本没法知道它是如何生成的’而通过Pytl 而通过Python直接构造请 求来爬取数据需要模拟token的生成逻辑’所以不能°

■■■【■■■Ⅸ■■■『「

我们 直接拿到了响 好在我们已经可以直接通过mjtmdump抓取结果,相当于请求是App构造的,我|}

应结果,得来全不费功夫,也不需要再去构造请求了’直接把响应结果保存下来就好。

■■■

卜卜

7.数据抓取

现在我们稍微完善一下spjderPy脚本’增加_些过滤URL和处理响应结果的逻辑: 1『∩portj5o∩ 千ro们Ⅶimpro×yj川port〔tx

0

「}‖

『|‖

de千re5pO∩5e(+1OW): UI1= 0∩ttP5://aPP5·5〔mPe·Ce∩ter/aPi/‖Oγie/! i十千1o侧.reque5t。urL5t己rt5wit∩(ur1): teXt=十1OW.re5pO∩5e.teXt i十∩otteXt: retur∩

data≡]5o∩.1oad5(text) ite‖5=data°get(』re5u1t5‖)



+or1te‖j∩1te川5:



ctx.1og.1∩+o(5tr(iteⅦ))

‖‖

之后重新打开app5,并在电脑端控制台观察输出结果’如图l2ˉ39所示° ●



■「■■【■■‖∏凸『{|■尸‖巳『「

{,诅": 41, 0m"e0 : ,萤火之森『 ’ 0αⅡtoS0 : ,擞火o社^0 , 『cOγe尸0 8 『https2//p1。爬i七u α门·∩et/}∏oⅥe“C55臼b「5「α%锄口b3ch70M651o0g502670纠.jpg鹤6▲Wˉ6q4h=1e≡1〔{ ’ 0C◎teg ◎Fte5, 5 [,剧恫°’ ,爱悄,0勋蕊,,倚幻0], ,pub1ts∩ed雷αt』: !2011ˉ”ˉ17, , ,m们吐e0 : 啡5, ,scoFe, ° 8.8’ °「egio∏3,: [·日本。]′ !d尸o∏m` ; 0累年Ⅲ天0 6岁小女孩竹川萤来到 爷爷豪攫蹋!她闯迸了传说住满妖怪鳃山摔森林·正当她困为迷踏浦憾怠乃分的时馒,~个 戴哲狐狸面奥的大男孩出现在她面前, 井弓|领穗萤找到回家的路o虽然盈分外感n可魁男 孩却禁止撼磁触自己的身体,原来逮名叫银的男孩并非人类0他一旦被人类碰触就台烟淌云

敝·在此后的日子里萤租跟咸为好腮友,他们走泪了森林的撼=个角落玩耍. 日厦≡日,

‖▲■『▲■■■「■■■■

年贝一年,每翻Ⅲ天的时儡董就会如约来到森林朝好朗友见面·她遵守秘和银的约定,无沦 如问也不磁触银的野体·穗椅年龄的墩长’葡粕锻对彼此的恫颗翻悄悯没生了变化』他们共 同期搏捐聚的日子, 共同期搏拥抱对方,=本片根据绿」||幸的原作敌镰 ,]

{,1d0 :哩’ 『mⅦG, 8 ,囊姬,, 0α1m5, 『 !土翘。’ ,仁oγe厂, : 『胁ttp5://p0·阳eituα∩。爬t/呐γ ie/』9653e8◎f59C于473cd蛔于9〔〔出658dg36g∑3鹏.jP辫田佣ˉ6q4↓L1名≡1〔0 ’ ,〔□七ego7ie£『 『 [, 翻悯,], 0publiEhedO士,: ,2013=10=哑0 ’ ,丽i叫te,: 1Z3’ ,SCoFe『 ; 8·8’ ,厂egtO们S‖ : [Ⅺ 韩圃,],`o7咖α,: 0她(李菜馋)愿-个羹丽牢凡的小女孩’粕董笆闯妈生活在位子街阐 的蒙中·歉里经慧循以她的名字蠢婚命名的杂赏店, 母亲 (严翻罐愉》作为毛鞭娘日夜忙

碌不搏闲暇,父亲娜在工厂干管繁殖的工作·在那个汛雨的阜上,素娘打覆雨伞独富上学. 在离学校近在罐尺的地万,她遇遇-个祖貌狼琐、酒气冲天的大叔, 田此开启了她的悲醚之 腿·桑嫩的小花沮到贝风雨无悯摧残″受伤的岂止橙儡阶叶‖更腿哪溜秘阳光无忧咸长的心 °无良媒体镰天鳖地大键漓染,作为受吝者的囊姬-家仿佛成了阉身污秽的耻辱之人°被四 鹰诧异好舒的目光所包围·妈妈慈痛欲绝,几近崩演·爸芭金力保护女儿′ 但受伤的小天便

0

■ 『 ‖ 巴 ■ ■ ■ 〗 ‖ 【 ▲ 】 ■ 卜 ■ ‖ ◆ 卜β■

翻拒绝爸琶的蘸近·问凶之路伴髓巷天使的治惠旅程,羹髓的女孩可会再■汕烂笑容? ’}

图l2ˉ39电脑端控制台的输出结果

可以看到输出了每部电影的数据’数据以JSON形式呈现°

8.提取保存

将返回结果保存下来,保存为文本文件即可’改后的脚本内容 现在再修改_下spiderpy脚本’将返回结果保存下来,阴 如下 1∏pOrt『SO∩ 〖



β)卜

十ro川川1t∏pro×γ1∏port〔tx 1Ⅲporto5

回■

■ ‖ 》 》】

550

第12章App数据的爬取 Ⅷovjes‖

α」丁P0「「0[0[R=

o5.path.exi5ts(α」丁plJ丁「0lD[R)oIo5.Ⅲa|(edir5("『P0『「0LD[R) de千re5pO∩5e(十1OW): ur1= 0∩ttp5目//己pp5.5〔mpe·〔e∩ter/apj/∏℃γie/` j+千1o训.reque5t.ur1.5tart5Ⅳjth(ur1): te×t=十1咖.re5po∩se。text 1十∩OtteXt吕 retur∩

dat己≡j5o∩。1oad5(te×t) ite川5=data。get(!Ie5u1t5!) 千orjte∏i∩ite‖5吕

〔tx.1og.i∩十o(5tI(jte{∏〉〉

"ithope∩(千|{α」丁p0「「0[D[R}/{jteⅧ["∩a川e"]}°j5o∩‖’ 』侧0 ′ e∩〔odi∩g二‖ut千ˉ80) 己5于: 于.捌r1te(j5o∩.du们p5(jteⅧ’ e∩5l」rea5〔1j=「a15e’ j∩de∩t≡2)〉

然后重新打开app5,滑_下屏幕’这时候观察movles文件夹,会发现成功生成了—些JSON文 件,这些文件以电影名称命名’如图l2ˉ40所示。

盟.弛

q■_

■王别姬jBO∩



本杰明,巴■奇 pˉ‖“∩



=一

-■一=

馆蝎侠;用■呀±

辩护人′“∏

搏击俱乐部.0S◎∏

初恋这件』吧.}≡◎∩

楚门的世界ˉl$o∩

触不可及.jSo∏

匣蹭

匡刻罗

霉剖=



墨司弓

当幸祖来敲门.]…〗

盗罗空闽。]…

氓起↓Sα]

∑鹃,



T

_

`Pcb

__

大话西游之大圣R大毋灭挚之月光宝

容辉遁。‖m∩

豪.…‖

大闭矢■jm】

■--

▲.』蛔]

熬嗡嚼鹤豁鹤



勺‖|」·】■■〗】■■』■】■γ■■□∏‖‖司(■■‖」·』‖]Ⅵ]■】】■』■』■‖|‖□司·

£匙譬



漫.回京.』6O∩

α

割 ■

■=

同.飞正传.『S◎∩







■=

—……

∑整题



■—

闰凡达』m∩





唾 _一

7号房的礼粗.旧O∩

~V ^√





γ







◎ 阳









| 』

图1240movies文件夹下生成的文件

任意打开其中—个文件’如阿飞正传json文件,可以看到如图124l所示的结果。 《.一、.…· ˉ



亡叮b正传争i$创0■

厂·●● .=…

.

ˉ

ˉ…≡. . 南 .

.ˉ ˉ

●1际吕 】马□

■≡■8 ■■飞正仔0 ■■1m6■8■…OT睡…■n征0

|(



■…广8呛…8〃阳.■蚊…·陛t′■v皿′田】】巫…Zd刀…『…21畴m·j…幽…】气】皆p ■□t…了』≈■8 [ ■面o ■…0 ■…■

l□

■酗b1心减m■吕 ■酌1B丢工·0

甲 ·■ 》 -乙

h

图l24‖ 打开阿飞正传json文件

」勺《』可■」·Ⅲ■】|□‖』∏|□】·‖‖』|

■了·■匡●吁■死在-冈…火车上唾仔·在…’另一个何飞(肇田伟宙)用心打Ⅷ自己准■出门0■唾卜■■…的放



酶去丁蔽律瓦,并将呻的车子宾掉0鲤仕…乙哲宣望岿仔·一m宅苏■…■仔唾华旧)目■了…坛雕决 ■°控母豪硒,决…行夫■妇■仔…征宾店人街Ⅲ沮卜丁■仔°不列住…臼已不认识伯◇不久0旭仔臣为买民…面在~通 ■斗中■●■伤·■…酥雨纂嘲月】6日下午酮●存■∩么′沮仔…记砷他永远记■,但■●倒■髓…醒不记





■担见’好■…Ⅲ开·″唾病沧■o符车于治予Ⅱ仔 (张罕…》·…发…走了p问Ⅲ仔佃不知丑唾■■,歪船诉■

■{

■sc●7=g9□1p ●『印』m马■: [ ·密汀 lp ■d…8■…不…仔盯飞 !工■=r)E卜…臣,■从去巴时牛母,自小田碎母《西…)界大,因此长大后幽生命 中…-个女人邯冲温和·m不=解体∏舍…■m矽僻■韦饰》租刃女啊闻■玲恒}■■’■匿工∏ …弃了m】·甩仔…处…°自只■巴……努…律赛■亡■…生于旭仔油…■生母’为吨只身睡连…°可已生

□‖

■m…=:弘0



「‖●『已】‖‖β′‖■『β『‖』‖『■■‖『|‖

l24 Appium的使用

55l

可以看到这部电影的数据已经实时保存下来了’其他电影也是如此° 9.总结

本节主要讲解了mjtmdump的用法及脚本的编写方法’借助mjtmdump’我们可以直接使用Python 脚本实时处理拦截的请求和响应,由于可以实时获取响应内容’因此可以在此基础上进行一些实时处 理’如提取和存储数据,这样就能轻而易举地把数据爬取下来了。

本节代码见ht印s://githuhcom/Python3WebSpider/MitmProxyTest°

}}

↑24∧pp|um的使用 Appjum是一个跨平台的移动端自动化测试工具’可以非常便捷地为iOS和Android平台创建自 动化测试用例°它可以模拟ApP的各种操作,如点击、滑动`文本输人等’我们手工能完成的操作

Appjum也都能完成。我们在第7章曾了解过Selenjum’这是_个网页端的自动化测试工具,Appjum实 际上就类似于它’也是利用WebDrjver实现自动化测试。对于iOS设备,Appium使用UIAutomation实 现驱动;对于Android设备,使用UiAutomator和Selendrojd实现驱动°

Appium提供了一个服务器,我们可以向这个服务器发送-些操作指令’然后Appium会根据不同 的指令驱动移动设备完成不同的动作°

爬虫使用Selenium爬取JavaScnpt喧染的页面’实现所见即所爬°Appium同样可以’所以在某 些情况下’用Appium做App爬虫不失为一个好的选择。

本节我们就来了解Appjum的基本使用方法,学习利用Appium进行自动化爬取的基本操作’主 要目的是了解利用Appium进行自动化测试的流程以及相关API的用法° ↑.准备工作

请确保已经做好如下准备工作°

□在电脑上安装好Appium客户端,并且客户端可以正常运行° □在电脑上配置好Andmid开发环境并能正常使用adb命令。 □安装好Python版本的AppiumAPI°



以上Appium环境的具体配置方法可以参考htms:〃setup.scmpe.centeI/appium。 除了配置好环境,还需要做到下面两步°

□准备一部Android真机或启动一个Android模拟器,并在上面安装好示例App,下载地址为 https://app5.scrape。centeI/·

□用USB线连接电脑和Android真机或模拟器,确保adb能够正常连接到Android真机或模 拟器。

2∧pP‖um启动∧PP

Appium启动App的方式有两种:_种是用Appium内置的驱动器打开App’另一种是利用Python 代码打开ApP。下面我们分别进行说明°

两种方法都需要启动Appium服务’因此先打开Appjum,启动界面如图12ˉ42所示°



■■■」■■■■■■∏{=■Ⅵ□】

552

第l2章App数据的爬取 ^pplu『∏

●●●

■□■‖■■‖‖

』■■‖‖』‖|』■■Ⅵ‖‖□■



OOOˉ0

NOSt



pO「t



▲72S

d

St己「tSe『γe「γl↑巳0



{|

·‖·■■■

| |

僵ditC◎∩↑|gu「已t}o∩s嘲



图l2ˉ42 Appium的启动界面

● ● ●

∧ppⅡum

(刘‖{

直接点击StanServervl.l5.l按钮即可启动Appjum服务’这就相当于开启了_个Appium服务器° 我们可以通过Appium内置的驱动器或Python代码向Appium服务发送-系列操作指令,它会根据不 同的指令驱动移动设备完成不同的动作°Applum启动后的运行界面如图l2ˉ43所示°

』■·‖■Ⅵ‖■■‖■勺‖」引」■∏





|| ‖o门(抢「αo}tSe『°γG7∩『g臼.

O

Gl1OWIⅦ已cq』厂e。{ ` 0



|‖‖

枷[CO『↑{c(@∧pp叫肌vl 15.1

oP∩yI「瞒e它u厂巴°{ }

AppI|」铡R巴『陆tp 【‖]tc『↑α〔U I`马罐『↑谭‖命已十酗qtedO∩0.00.0》4咆3



Appium启动后的运行界面

Appium正在监听4723端口,我们向此端口对应的服务接口发送操作指令,运行界面就会显示这 个过程的操作日志°可以输人adb命令测试和手机的连接情况,命令如下:

·■]■■可■■】日■

图l2ˉ43

日dbdeγi〔e5 ˉ1

[j5to十deγj〔e5atta〔们ed

R5〔‖3OR肋Qtdeγj〔eu5b:33869oo48Xprodu〔t:y2qz〔×咖de1:SNC9860deγi〔e8y2qtIa∏5poIt-id:1

其中"ode1是设备的名称’就是即将会介绍到的devlceName’对于不同的手机其结果不同,请以 自己的结果为准。

‖■·Ⅵ|」□】‖|·』可‖‖|■‖凸·】∩|』勺]·∏|‖|」司‖」‖|‖〗」γ』□■

如果输出结果类似下面这样,说明电脑已经正确连接手机:



l2.4Appium的使用



553

「凸■



■=「||

注意这步一定是成功获取了设备信息才能证明电脑和于机连接成功了°如果提示找不到adb命 今’那么请检查Androjd开发环境和环境变量是否配置成功°如果可以成功调用adb命今但

|}

不显示设备信息’那么请检查手机和电脑的连接情况,如USB调试功能是否开启等°







接下来用Appium内置的驱动器打开App,点击运行界面右上方的StartInspectorSession按钮,如

F



图l2ˉ44所示·





们 凹







似 尸 ∩



0

l



′一凹F〕 |占飞

》『巴■■「△■尸卜》匠尸

(→ˉ

附OlcO阳etOAppiⅧ‖ v1 15°1

S‖日「【‖∩SpeC【O『Se瞻|O∩

△l

"O∏ˉdefα[‖1ts?Fve干α蛔岛; ■

P 夕

【ˉ

de00yI吧e〔仙广e: { 可





八pp1‖』ⅧR伯5丁帅ttp1nte广fk0ce1`龟te门c『· t◎FlPdO∩0·()O°0q流3

陋』』

α]1砷I〗〗昂eCu7e:『



∩卜‖「|卜‖广|〖‖■保◆■「‖卜「▲■『‖巴■β‖『

图l2斗4操作示例

会打开一个配撮页面,如图l2ˉ45所示。

我们需要在这里配置启动App时的DesiredCapabilities参数’包括platfOrmName、deviceName` appPackage和appActlvlty。

□platfbrmName:平合名称’取值有Android和iOS,此处填写Android。 □deviceName:设备名称’是手机的具体类型’即刚才获取的"ode1值.

□appPackage:App包名,示例App的包名为comgo‖dzemvvmhabit° □appActivity:人口Actjvity名’示例App的人口Actjvity名为.uiMainActivity°

□noReset:不重置App的状态’此处需要填tme,如果不填’那么每次打开App都和新安装时 一样°例如启动了微信,而此参数没有设置,就会变成未登录状态。

…一~_…….

● .● 儿〗『;〗加色肌

Cu刮O顷翁e『丫e『

晒幽●……

鳞贴c『吵口肺…



一__0}

■■ 置

『:$了23

>蛔γa№尔|“t〔‖『Vg巳

↑2 、■■



≤…









■田

酿轴l蝎圃c刨∩勋◎〗『0〖』●的

0



」SO刊∩·pγ巴9eⅣtO"O∩

.{

翘 {》

k二ˉ ■

















图l2-45配置页面



■庐

叮甘



■】■、

」 ■ ■ γ

』 ■ ■ ■

‘口■■‖

第l2章App数据的爬取

554

当前配置页面的左下角链接了包含更多配置参数相关说明的文档,大家可以参考盖文档配置更多

‖‖‖·‖』■∏|

的参数°

不同App的appPackage和appActMty是不一样的’如果不知道它们的值,可以用jadx(https://

githuhcom/skylot〈jadx)这样的工具解析App安装包中的AndroidManjfestxml文件获取。例如这里用





■一≡…



—色





■铀



●砚=心

●文

…←…【p醉叫9吭确γ』牌∏稻

丁……·…



m■ 牵…沁』血

| ■C■



| ■山炳C… ■m·冗■〔t止… 击“

■◎恤tt碑

‖‖

■了皿汹沪皿

| ■■nmm



‖‖△■

jadx工具打开示例App,就可以看到它的AndroidManifest.xml文件,如图l2ˉ46所示°

r· 25

e的m

〗丁 】Q

凹吨∏「O?1tⅢ ■ 资m件

驹 】〕

脯|A『创m1油m「旧St,绚l

;麓孺簿「“

尸 腮】…to陀

‖‖□

酌■■心■■阿回臼ⅦⅥ抑见川‖凹况‖■

2】

酗呕Ⅷˉ酶 渔@恤…】 酗由3

口』







8



-旦=■.≡… 回…睡=~…

≡≡

■…

=≈→…≡…二.;.占_……一—ˉ ‖

图l2斗6 app5的AndroidManifest.xml文件

appPackage的值就是们a∩i{e5t根节点的pa〔kage属性,这里它的值就是〔o『『‖.go1dze.‖γⅧhab1t, 如图l247所示。



‖…0『B………冗沁】…1…B

=…■仙I…°~1t■ ‖酞帕…』↓味791●Ⅲ=矿 乙 ≡

嘛β…tu爬



二≡

固[儿c午,曾G幻例『 ■c吐唾画 …砖 ■「3 C ■…r函.●厂

……·孕

囱≈

厂_

■℃古件

乙…碑 钮…

~隘—_

习■》》·′夕

■…… ■吐m ■m硒?1■

■尸‖‖{′=|↓··□·β·『『仁『|尸 …$『■{‖『』‖|{↓{『′‘′·}}恨『’‘『}{『巳

●皿o≡1… ●酝



■血……

『_

■′皿皿汹四皿四”汹四刃叫刃佃亚■

■…冗』血 ●C■

…、□…″



■涸

弓=’

叫—









……_

_ˉ鸵…

~函

巡■

≡←□■■甲

|←唾 ■凹

—喊闭

文件祖■…工■阑助

°尸1……』t·呵·…umt四O“冗皿:●1……t硒∩…而挝?…穴酝tl^面…向10r……如

~1‖·□■≡1mt……■蝉面』≈■m1□:皿1Ⅷ心T≠101口′≥

】p磅…t=…厂·皿8巳【≈矿几【坚画0…◎惯〗…p≡2tqu…le=w●』m=′≥



0



■‖巳



_

‖■■

=…F

鲤□晶~≈

』■■日‖|纠■〗□日日·』■■□Ⅱ

…=…

●;■●

蹿

图l2ˉ47 "日∩1十e5t根节点的pa〔代age属性值 (

‖‖

| ‖■【「



l2.4Appium的使用

555

叮以看到,AndroldManlfest.xml文件中有很多个a〔tjγ1ty节点’其中_个包含如下关键内容: 〈1∩te∩tˉ十11ter〉

<actjo∩日∩drojd:∩a们e="a∩droid·1∩te∩t·a〔tio∩.趴I‖" /〉 ‖‖巴『|}‖

<c日tegoIγa∩drojd:∩己‖e≡圃a∩droid.j∩te∩t·〔日tegory.[∧0‖〔‖[R| | /〉 〈/i∩te∩tˉ十11ter〉



卜■)卜



所示。 ●

哇……■皿67…而℃U‖=』回z和



文件…守以工贝胃助 二■

°‖ °

¥』●博凿′『 帘





■■→■■艳◇

m宰→■■■=■=■■■

■■

■…=■≈■

■凸

沪 影唾玛…唾曙吓…m…10印tˉ■〗 ×

烬…

凸…m坦

生咖m卫N

2

J

哗…

0]

硼促?西〔…

1R

「|卜

碾m.「●正tj腔x 选亡

1Q

】〗

汕…锤

】d

吁0kl@ 』尚「et「◎寸上t2

£S

】y M

■■沮文行

J】

四肚『片』刊卜

】0

鸣0mRtp3

〕〕

Ť

w

.~

4n

■Lγ■合=色QCm

l●

■砷忌o0廊户≤.a穴『



尸∩厢5』gmt凹把

一毛

卜|「|‖「·「『■「|卜》|「■「|



‖?

冒ˉ→…

|||



这4行内容代表app5在启动时会启动该a〔tjγjty节点中声明的∧ct1γjty对应的页面’找到该 a〔tiγ1ty节点中的a∩drojd:∩a"e属性值,这里就是〔o".go1dze.ⅧγγⅧhabit.uj.‖a1∩∧〔t1vjty,如图l248

》 ■ 「

■■「「巴尸■「

图l2ˉ48 a〔tjγitγ节点中的a∩drojd:∏日‖e属性

在Applum配置的时候’需要去掉属性值里前面的appPackage信息’于是结果为.u1№j∩ACtiγ1ty°

住∏『′

接下来在Applum中加人上面5个配置’如图l2ˉ49所示°

■尸「

●、.



|匹尸『|

c脉剖o订`S·『晦「

A刨【0∩M0C己0T汗∏

S咎灯c『C胸《呵尉帅…古





P





D哈S『似‖〔动p.山i0『司蚌

颇↑碰Mo鳃酷『仿‖

导n二号弓弯…■

=…

凹t

品∩山已灿

咖【

bp℃u0吵

……=…四

__—=

瓣……

″‘』 早∏l

「石蕊_



.………, 」,‖MF盐u′w`γ

E



■■ ■■



■≈



■■

巴■



◆■



===≡=

噎z!

=…_二

‖|



>∧.Ⅶ冗[ed鳃炕叮巴2

□□口○口



■■■■■■■■

咖唾……沼…“…融γ憋r自0汐〃■…舔″】s

. .、,`:憋′′乓带.

5■0陛∩

圈嗡. ˉ ,!

图l2ˉ49配置信息



划‖·Ⅵ|||四■可纽■□]{‖`

第l2章App数据的爬取

556

点击SaveAs…按钮将配置信息保存下来’以后就可以继续使用这个配置°然后点击StartSessjon

按钮’即可启动Androjd手机上的App并进人启动页面。同时,电脑端会弹出一个调试窗口,我们可

ˉ-=

-二=一了≡■甚勤翼辽涸一



『沪

、、

^^^勺岭

二二二=

■■ ■

←』

◎…饵…

●■…

■=廓`分由

一…→ 勺■……L肘■■=■

,s{『

疆爵贺

达十杀乎不缅

獭.…匹

9愚

m、咆灾■

.一~~….……←…

簿……屠…盅鲤龋黔

°~喇.…知………~、…≈·~≈ ■_

p马懂日



■≡………■…■≈■=…≡■

95

吨■■ˉm

■v℃…

■■

■~≡…~_

…←勺℃乒?臼…Ⅶ印…▲↑闪

●……=

…●点秧竹

…咀…位…

05

…ˉm、右■



0

≠=▲军

…~一

≡至ˉ



回≈≡二…酶

≡……字铀≡

牢】

■Y…

m孕T→拄

…1告一…≈

→……≡

■由人

95

阐m`″.■O

哦=~∑的…巴=■…■=卫

…>不

9S

申·啤m

■舀嚼 囚霞怠

…∏

0Q

m刷幻

o…中■□=■』→唾ˉ..

~~≈←

b≡=田≡

…→

° ˉ-≡=」■≡=●

……



樊苟



0……=≥

鸦乎王

9n」

…m、m

陋擅々……

o ≤p·咽■啤0』≈…> ·≡~】一←卜

……

·…≡0=-



| ;



踪嚣

{=:蹿

凸一击…



b- ^



点击左栏中手机页面的某个元素,它就会高亮显示’如这里的电影名称‘霸王别姬”°这时中间栏

会显示它对应的源代码;右栏会显示它的基本信息’如index、class、 text等°我们还可以在右栏执行

一些操作,如Tap` SendKeys`Clear,现在点击右栏的Tap按钮’即执行点击操作’如图l2ˉ5l所示°

≈=?盯



xpa!∩

加烛■c0wm油酮ˉw…k尸『m把Lβγ°凹归…·嗣瞬M们Qa几■扣qt′3价◎『。Ⅺ

●哗吨xpa『Moc碰O了$旧∩αf碑om『∏●Qo酗■∩□C田‖‖··ot◎?『喇●t·S1s

|」·∏|』■‖·‖川■|

『贾}……‘颠° 尸i∩◎

二■Ⅵ|』■日]

◇Se‖“0e◎日em·∩t

〈‖·〗||··‖‖|■□可‖‖·可

图l2ˉ50电脑端弹出的调试窗口

{{‖ | 叫 ‖ { | ‖

圈署翌 幽繁嚼 困骤h 照爵瓷

己■■】|」』∏■

歪鳃爵

ˉ~…■…≡………■≈…申霸

!

日|

αs}

m、…

●宁宁克G

ˉ~…

≈酣 ■■■ =■…≈■==≈……≈渔■…7今… 山

●■亏亏Ⅲ~≡=—

≈贺警

■中…

问| ‖ 〗

●■■■



-

|‖』■‖」‖|」』□■□□]‖

以从这个窗口预览当前的手机页面,以及查看页面源代码,如图l2ˉ50所示。

A咏沁o『d0囤卯们entt唾闸!◎枷w‖口●u『】《口瞄酥ceS引b‖脚沁仁M◎『■

」 ■ 】

("巴teaO!

|■■

γ■m●

e跑m0爪‖d

38o5b7“聋7a63=q020≈b593铝↑]↑cmC86]‖↑

{响●】

O

pOck$”

c的↑.9◎伯∑●ˉ『Vwvmmb『t

…S

mG「喇窜Md0e0.购xMOW

t·xl

■主别旧

图12ˉ5l 点击Tap按钮 可以发现电脑端的页面发生了变化,如图l2ˉ52所示。

·』●|||■■■〗‖‖■】■‖|勺』■|己∏」日·‖创|{』■■■■】‖‖■■』□■】■∏|』■】|』■』·■

础化ut●

l2.4Appjum的使用 闷留潍…堕澄 了■一=

蠕轴" ,■ |



▲■「

W0份砷

r以

0…缅·郝



<…响……… ■←=0…△兰=-红

.妇…~…… →=…≡—←-的吵…

■ ■ 厂 | 巴

●匡了且二▲o.r∏仁‘G2JⅢ0』二卫

■ 「

●—l…罕D

睦■…茁■■》…》←鉴些飞+人虫俘■…

≈~~≡

肪仰)■=…■人→Ⅷ烛.=悔 ■·=…″狡…蛔≤■…0m●凸∏

·…-

潍酗.,HA…∩…《■*…■…▲■…

=一…~~…哗

少=—-= ◆—■…

尸》匹β「

…~…一≡~……

p



O





『)■|厂

—≡

!l『



…渺0一b面..…雪写″唾4嚼 m毗p■么二▲→出…●户= ≡…豁≈0……

{{



■A郁雪乒■←←当宁二卧…■T儿◆·… ■但不伪·■……T…T●些娶

—≡…一≡…………乙…

↑…Ⅱ……■股≈《出取■m助=〔蛔

→…

■割…牵辑≡=…≈~

■呻介

…=



·~…伪…≈=■………B











α

●.0

′←’…

汗甘

≡尹… ……`…净

呻·囊田

……………·

巳▲=…■… ■

=≈时_…~一匡………一~≡……~ ……………≈…咆 …乙



■=别姬





=三犁』每渔记

墅■







尸尸

^■碎

勾$



■■「||匹◆『}广



557

0泅

==-

图l2ˉ52电脑端的页面发生变化

=■尸■■■「

左栏的手机页面跳转到了《霸王别姬》电影的详情页,中间栏的Source面板显示了当前页面的节 点信息°那怎么返回呢?点击中间栏上方的Back按钮,如图l2ˉ53所示。

匹『β卜卜′巴■『

Recordmg按钮,Appium就会开始录制,之后我们在窗口中操作App的行为都会被记录下来’Recorde『

这时就返回首页了°Appium还提供了动作录制功能’如图l2ˉ54所示,点击中间栏上方的Sta戒 面板中可以自动生成指定语言编写的代码。

》■ 7



■‖卜■「■∩■■}■尸匹「「■∩卜「』「巴■∏『「{「

潭 闸雪/α旦ˉ1蕊

… —~亡≈

-

———

—=~≡

_一

图l2ˉ53点击Back按钮返回

图l2ˉ54点击StartRecording按钮录制动作

例如’点击StartRecoding按钮后,选中电影条目‘初恋这件小事”,然后点击Iap按钮,再点击 返回,可以看到Appium的Recorder面板中出现了这些过程的操作代码’如图l2ˉ55所示° 这里我们选择的语言是Python’代码逻辑是选中某个节点然后执行点击操作’接着返回,和我们 手工操作的内容-一对应°

总结_下,我们通过在电脑端弹出的调试窗口中点击不同的动作按钮’即可实现对App的控制, 同时Recorder面板可以生成对应的代码°在Appjum客户端控制和操作App的方法就介绍完了°

下面我们看看使用Python代码打开App的方法,这里需要借助我们已经安装好的Appium的

Python库实现°首先在代码中指定一个Appjum服务,而这个服务在刚才打开Appjum的时候就已经 开启了’运行在4723端口上,配置如下所示:

p

■『卜 )户

0

■={

` 0

5erγer二 ‖http;//1oca1们o5t:4723/wd/hub!



■司·Ⅵ‖』】]』■■■』】‖‖』■■■■】‖||』■■■可

| 第l2章App数据的爬取

558

◆c ↓0 α◎x 0§| |◆c

『…

.

~〕s·~「q

■■…… …

















89

m碎严呻

≡→凸=么宁=b



= = 当,弓.

0·勺O●……内■==畴4…o巴≈……‖■◆b

≈●0◆■…

合 三动■·≡摊●→■ ■争■=

●让pP居琶△垒Ⅱ0

合午兰-二=

忌弓宦;总0…~尸…·……=“=ˉ…~口………=■…~……

a9

哮m∏■

…″~〃

■≡尾≡◎…出叼…军份o琶′乞●占<U山≈·■…毋…≈f、m锦.←~0≡…凶罕… L’沪饵叼…°…▲审…°…$”·■…▲0L……摊偶l闻

=~

u■、住Pˉ■■T

畦■

←●审0≡■●p■

8B

」『凹

…m …

』■■■|‖|』可】】■■■■□‖□□』●】‖|||

■-

●0■·….0』吼≡~片≈■■广寸……占≡汗■■字P.m…′…^■A←勺坠酵……P=≈U∩O →凹~=-一 …

■吕邑



…趴

■■田■■回



◇≡…

…=…



0a

……、m

二■司■■|』』■■■·■』■‖■、`■】■Ⅵ

≈………O



≈■…

$钡8

m°■妈.凹

…▲尘

9‖

蚀◆b哺`蜘

一=‖■=-【一—‖≡0一一=…≡

=ˉ△一…~……~~●

·…=~一`巨亏亏分

■十…



9】

U—望=

r

●…~=●≈-…妇牵… 哩芦-=≡=~劈 …

吨■■°■钮 ■—≡…=恒~戊 ←…

干●午0

≥〖…≡2沮了…… 墨≡←≡‖蹿V”

. ˉ:::窒罩



| 9‖

E●伪

●凹

】.■■一■-

~m=-…|

9V













【酶…~

==一Ⅲ=≡动



~0…

……

图l2ˉ55

|| |{

Recorder面板中自动生成操作代码

接下来用字典配置DesiredCapabilitles参数,代码如下: des1redˉcap己bj1jtje5={ "p1己t十Om‖a爬": 圃∧∩drO1d"』 "deγi〔e‖a『∏e00 : "驯69860"’ 00

apppackage": "〔o们.go1d2e.刚γⅧ∩ab1t"』 !0app∧〔tjγjty倒; "°uj。阅ai∩∧〔tiγity00』 "∩o∩eset";『rue } 夕

新建—个Sesslon’这和点击Applum内置驱动器的StartSesslon按钮功能相同,代码实现如下: +ro‖app1u们加port眠bdriver

drjver=肥bdrjveI。Re加te(5erver’de5ired-〔日p己bj1jtie5)

配置完成后,运行代码就可以启动示例App了’但现在仅仅能启动App,还不能做任何动作°接 下来实现-个加载等待和下拉的逻辑: fro『∏5e1e∩ju们.刊ebdI1γeI.〔o『Ⅷ】℃∩·bymport8y froⅦ5e1e∩ju∏。webdriγeI·5upportj川portexpectedco∩ditjo∩5a5〔〔 +ro「‖5e1e∩ju们·webdrjγer.5upport,ui加port日eb0rjγer‖ajt

"ajt=‖eb0rjγe瑚日it(driver’ 3O)

wa1t.u∩tj1(〔〔.pre5e∩〔eo千a11e1e爬∩t51o〔ated( (By。XpA丁‖’ 』//a∩droid。5‖」pport.γ7."idget.Re〔yc1erγje"/己∩droid.wjdget.li∩e日Il己yout‖))〉 "j∩dow5jze=drjγer.getˉ"i∩dow5jze() "jdth’ he1ght=切j∩do侧5ize.get(Wjdth‖)’"1∩dow51ze.get(0he1g∩t0) driver·5wjpe("idt∩*O。5’ height*O。8’"idt∩*O.5’∩eig∩t*O.2’1OO0)

这段代码先确保所有电影条目加载成功°因为_个电影条目对应_个a∩dro1d."1dget儿j∩eaI[ayout 节点,这些节点的父节点是a∩drojd.5upport.v7."jdget.Recy〔1erγ1e"’所以这里构造了一个取值为 ●

//a∩droid.5upport.γ7."jdget.Recyc1erγje"/a∩dro1d.w1dget.[1∩ear[ayout的XPath’用来查找每个电 影条目’外层pre5e∩ceo十a11e1e阳e∩t51o〔ated的意思是确保所有电影条目都加载出来,其外再套 -层‖ebDr1γer‖a1t对象的u∩tj1方法,设置加载的超时时间为30秒°于是’最长会等待30秒’如

司■■‖』‖□■】□■】〗〗』■』』■■·■】■』■]』□Ⅵ■】

◆↑

…m.●■芦6■



| 「 ′ 卜 「卜 「



l24Appium的使用

559

果这期间所有电影条目都加载出来’就立即向下执行,如果没有加载成功’就代表数据加载失败’抛 出超时异常°

接着获取了手机页面的宽高信息,然后调用5"jPe方法执行 |』n`:嗡曹 , 唾田王别姬 了-次屏幕滑动’这个方法接收5个参数,分别是x1` y1、X2、

穗.…蠕o| 95

疚宽职妨、n拽

|■■「|‖■尸|

翘爵嚼 躺灌赠勘飘翻剿栅鹏盏赣攘 左上角的坐标是(0,0)’向右为x轴的正方向,向下为y轴的正方

向°这里设置X1为手机页面宽度的05倍,设置y1为手机页面高



巴肖申宛的戴触

巴霞嚼 ■…、…

q5

■■厂

■■「|「·任》■「‖

高度的03倍。duratjo∩是滑动时间’这里设置为100o(单位为

‖〗●卜|‖■■‖|》似广‖■‖卜『|【■‖匡■「|》▲厂‖|■■■『β■【「‖



度的α8倍, ×2同样为手机页面宽度的α5倍’γ2则是手机页面

上所述’完整的代码如下:

毫秒),即l秒°滑动效果如图l2ˉ56中的红色箭头所示°

我们模拟了垂直向上滑动,触发加载下一页数据的过程°综 十roⅧapp1u"mportwebdr1γer 十Io"se1e∩1uⅧ°webdIiγer.cαⅧ℃∩·byi∏port8y

+ro‖5e1e∩iuⅦ·"ebdriγer°5upportj"portexpe〔ted〔o∩ditjo∩5己5〔〔 十ro们se1e∩ju∏°webdrjγer。5uppoIt.uj1‖port‖ebDrjγer‖ajt 5eIγer= 0‖ttp8//1o〔己1ho5t:4723/"d/hub0 0

de5jred=〔apab11jt1es≡{ "p1at千om‖a阳e"; ′『A∩dro1d"』

歪爵絮 盂爵嘿“ 画歹马假日 鹰黑嚼 田″`同邱m

95

95

幽蹦嚼 幽爵嘴:·

95

园器麓 -ˉ一 `露灌.|卜 =呻`呻 二〃少丰 期 熙愿凰≡ ˉ…、■■、

95

←獭

=■■■■一

95

.…、■■ b【钱

■留繁 ■留瞥 网嚣忘 口· 囚嚣私

g0

M

"deγj〔eNa雁": 005‖C986o"’ 00

3pppac代age|‖: "co们.8o1dze·们wⅦhabit"’ "appActjγity"8 "·u1·‖aj∩Actjγjty"’ "∩oRe5et":「rue

β》卜卜庐「|■【■「|■尸||■■■「||』■■尸|巴■尸



‖‖|





图l2ˉ56滑动效果

dr1γer=webdrjver.Re『∏ote(5eIver’ de51red-〔ap日bj11tje5〉 "ajt=‖eb0rjγer‖ajt(driγeI》 3O〉

阳jt.u∩tj1(【〔。pre5e∩ceo+a11e1e眶∩t51o〔ated(

(8y.XpA丁‖′ 』//a∩dro1d.support。γ7。widget.Re〔y〔1erγje"/a∩drojd.widget.[j∩earlayout‖)〉)

wj∩do"5ize≡driγeI·getˉm∩do们5jze()

"1dtb’‖eight="i∏dow5iZe.8et(|widt什)’wj∩do"51ze.get(`heig‖t|) Oriver.5wjpe("1dt‖*O。5’ hejg∩t*O.8’w1dt∩*O.5’∩ejght*O.2’10OO)

重新运行代码,App会重启,首页的电影数据加载出来之后’屏幕会向上滑动一下’接着第2页 电影数据成功加载出来°

β『

}}}【

3.∧pp|u『∏的相关∧P|

本节我们来总结一下Appium的相关API怎么用。使用的Python库是AppiumPythonClicnt

(https;//githuhcom/appium/pythonˉclient),此库继承自Selenium’因此使用方法与Selenjum有很多共 同之处。 ●初始化

}■■■「}|■『巴∏「| ‖

需要先配置启动App的DesiredCapabilitjes参数,完整的配置说明可以参考ht‖ps://githuhcom/

appjum/appium/blob/master/docs/en/writingˉmnningˉappium/caps.md’-般配置几个基本参数即可: 十ro‖‖appiuⅦ加portwebdriγer

5erγer= ‖http://1oca1ho5t:』723/Nd/hub‖ Oe51【ed-〔己pab111tje5={ "p1at十om‖日贬": "∧∩dIo1d00’ "deγj〔e‖a们e": "5‖C986O"』



□■

{ 第l2章App数据的爬取

560

apppac代age00 : "c咖·go1dze。‖γⅧ‖日bit"』 ,0appA〔t1γity困: 00 。uj·‖ai∩∧ctiγity脚’ "∩oReset":『Iue 〕 J

driγer≡训ebdIiγerRe巾te(5erγeI’ de5ired-〔apabj1ities〉

』■■ⅥⅥ|■■可

这样Appium就会自动按照DesjredCapabilities参数设置的内容查找手机上的包名和人口类’然 后将App启动°如果没有事先在手机上安装要打开的App’可以直接指定参数aPP为安装包所在的路 径’这样程序启动时就会自动在手机上安装并启动APp’代码如下:

∩·‖|‖|

00

+ro‖| app】uⅦj"portwebdrjγer



‖p1日t+om‖a∏吧|: |凰∩droid|’ !deγjce‖a∏把! : 0删C986o` ’

□|』

5erγer= 0∩ttp://1oca1ho5t;4723/wd/hub0 de5ired-capabi1itie5={

日pp|: ‖ °/5cmPe-app5。日p|〈| }

drjγer≡webdrjγer.Re∏℃te(5erver′ de5iredˉ〔ap日bi11tje5)

·查找节点



可以使用和Selenium类似的通用查找方法来查找节点,代码如下: e1≡dIiγer.「j∩de1e∏记∩t-byˉid(′〈package>:1d/〈1d〉』)

找节点,针对Android平台的代码如下: e1=5e1「.dIiγeI.+i∩de1e爬∩t-by-a∩drojduja0toⅦ日tor(!∩e"015e1e〔tor().de5〔riptjo∩("A∩j川atjo∩")′)

||

Selenlum中其他用来查找节点的方法在此处也同样适用’不再赘述°还可以使用UIAutomator查

e15=5e1+.drjver.+i∩de1e川e∩t5_by_a∩droidu1autoⅧator(!∩e"0i5e1e〔toI().c1i〔炮b1e(true)0)

针对jOS平台的代码如下: e1≡5e1「.dr1γer.十j∩de1e|∏e∩tˉbyˉjo5-uiautoⅧat1o∩(‖。e1e‖∏e∩t5()[0]!) e15≡5e1+°dIjver·千j∩de1e川e∩t5ˉbyˉio5_(」1a0to川at1o∩(! .e1e爬∩ts()‖)





此外’使用jOSPredicates查找节点的代码如下:

使用iOSClassChain查找节点的代码如下: e1三5e1+.drjγer.十j∩de1e爬∩tˉby-io5ˉ〔1a55〔hai∩(』X〔0I[1e爬∩t『γpe‖j∩do"/XO」I[1e∏mt丁ype80tto∩[3]‖) e15=5e1+.drjγeI.「i∩de1e‖e∩t5→by一io5-〔1a55c∩日j∩(0X〔0I[1e阳e∩t丁ype‖i∩dow/X〔0I[1e|∏e∩t丁γpe80tto∩|)

注意这种方法只适用于XCUITest驱动’具体可以参考htlpsy′githuhcomappjumappjumˉxcu1testˉdrivα。 ● 』点击屏慕

(‖

可以使用tap方法模拟点击操作’该方法能够模拟手指点击(最多五个手指)’设置和屏幕的接

」■■■∏■■]』■■■】』■】|||」‖■■■‖‖‖|]』■勺

e1=5e1十。drjγer.f1∩de1e|∏e∩t-by-jos-predj〔己te(0wd‖a"e== 0′8utto∩5" ‖ ) e15=5e1+.drjγeI.千j∩de1e‖e∩t5-by-jo5-pIedj〔日te(|wdγa1ue=!!5e己rc‖Bar"∧‖Di5ⅦOiγj5jb1e==1!)

触时长(单位为毫秒),定义如下:



参数有po51t1o∩5和duratjo∩.



□PO5jt1O∩5:点击位置组成的列表° □d|」mt1o∩:点击持续的时间。

|‖

tap(5e1于」 positjo∩5’ dur日tjo∩二‖o∩e)

实例如下: dIiγer。tap([(10o』 2o)’(1oo’ 6o)′(1oo’10o)]’ 5o0)

}|}

运行这行代码,就可以模拟点击手机页面中几个指定位置的点°另外,我们可以直接调用〔i1〔长方 法模拟点击某个节点(如按钮)的操作,实例如下:

|「 巴■■『兰『∩||■尸匡■『「■■尸

124Appium的使用

56l

butto∩≡十i∩d—e1e『‖e∩tˉbγ-jd(』〈pa〔|〈age〉:1d/〈1d〉|) bl」tto∩.c1j〔代()

这里先获取节点’然后调用C11〔R方法模拟点击该节点。

|『

『『|■「「|‖巴■‖|

●屏慕滑动

可以使用5〔rO11方法模拟屏幕滑动,其定义如下: 5cro11(5e1f’ oIjg1∩—e1’ de5tj∩己tio∩e1)

表示从元素orjgj∩ˉe1滑动至元素de5t1"at1o∩e1° 实例如下:

卜|‖●

drjγer.5〔ro11(e11’ e12)

还可以使用5wjpe方法模拟从A点滑动到B点的动作’这个方法之前已经应用过’其定义如下:

‖■■「仍■厂

5w1pe(5e1「’ 5tartx’ 5tart-y’ e∩dˉx’ e∩dˉy’ dumtio∩=‖o∩e)

参数有startx` 5tart_y` e∩d-x、 e∩dˉy和duratjO∩° □StaItx:开始位置的横坐标。

□5tart-y:开始位置的纵坐标。 □e∩dx:结束位置的横坐标。

■卜

□e∩dˉy:结束位置的纵坐标°

0

p

□dumt1o∩:持续时间,单位为毫秒°



实例如下:



dr1γer·5"1pe(100’ 1o0’ 100’ 40o′ 50oo〉

p







运行这行代码’可以在5秒内由点(l00’l00)滑动到点(l00’400)°另外可以使用十11〔促方法模拟从 A点快速滑动到B点的动作,用法如下: 千1iC促(5e1+’ 5tartX’ 5tartˉy’ e∩dˉx》 e∩d-y)



参数有5tartx、5taIt-y、e∩d-X和e∩d-y。



□5tartx:开始位置的横坐标。

□5tartˉy:开始位置的纵坐标° 〕e∩d—y:结束位置的纵坐标°

|}

刁e∩dx:结束位置的横坐标°

‖■■「■■尸



实例如下:

巴■「△■厅〖「‖‖坠■■■■尸

drjγeI.+11〔低(1oo’ 100’ 100’ 40o〉

●拖动

可以使用dragˉa∩dˉdrop方法模拟把一个节点拖动到另—个节点处的动作’其用法如下:

■■[庐『◆【「匹伊『●『「‖|■■尸『[【■『‖■■□■‖■『■『|伊‖{■〔}卜‖【■∏|■「‖■■「|■■【■■厉

dr日gˉa∩d-drop(5e1十’ origi∩—e1’ de5tj∩atjo∩e1)

可以把节点orjgi∩ˉe1拖动到节点de5ti∩at1o∩e1处。 参数有orjg1∩ˉe1和de5tj∩atjo∩e1。

□origj∩a1ˉe1:被拖动的节点。 □de5t1∩atjo∩e1: 目标节点°

实例如下: dI1γeI·dragˉ己∩dˉdrop(e11’ e12)



562

第12章App数据的爬取

●丈本输入

可以使用5ettext方法模拟文本输人,实例如下: e1=+i∩de1e∏旧∩t—by-jd〈|〈p日〔阳8e〉;1d/cj|〈|) e1.5ette×t(‖什e11o‖)

●动作链

与Selenium中的ActionChains类似,Appium中的T℃uchAction可支持tap、pre55、1o∩8-pre55` re1ease`Ⅶoveto、wajt、c日∩ce1等方法,实例如下: e1=5e1+.dr1γer.千1∩de1e『∏e门t-by-a〔〔e551bj1itγ-jd(0A∩j爬tio∩|)

这里首先选中一个节点,然后利用TbuchAction点击此节点。如果想实现拖动操作,可以这样: e15二5e1+。drjveI.十j∩de1e们e∩t5一byˉ〔1a55∩a"e(』1i5tγ1ew‖) a1=丁Ol」〔M〔tiO∩()

a1.pre55(e1s[O]).mveto(x=10’ y=O).加veto(x=10’ y≡ˉ75).|∏oveto(×=10’ y≡ˉ6OO〉.re1ea5e() a2=「OUCM〔tjO∩()

a2。PIes5(e1s[1]).∏)oγeto(x=1O’ y二10)·∏℃γeto(x=1O’ y=ˉ〕OO)°帅γeto(x=10’ y=ˉ60O)·re1ea5e()

|||

己〔tjo∩=「ou〔h∧〔tio∩(5e1「.drjγer) aCt1O∩.t己p(e1).per+Om()

■■|·□■可』』』司」■‖』■■■】■』‖|』■∏·

这阻先选中_个文本框元素,然后调用5et—teXt方法输人文本。

利用本节所讲的API’可以完成绝大部分自动化操作。更多的API详情可以参考https://applumjo/ 4总结

本节我们主要了解了Appium操作App的基本用法’以及常用API的用法,在l25节我们会用—. 个实例演示Appium的使用方法。

(已】』‖|勺‖‖|々』ˉ■Ⅵ|司·

docs/en/aboutˉappium/apl/。

本节代码见https://gjthuhcom/Python3WebSpider/AppiumTest°

本节中我们会完整地讲述如何用Appium爬取—个App° ↑.准备工作 本节的准备工作和124节基本_样,请参考那里°

另外,本节会用到一个日志输出库logum’可以使用pip3工具安装: p1p3 1∩5ta111Oguru

2.思路分析

首先,我们观察_下整个app5的交互流程,其首页分条显示了电影数据’每个电影条目都包括 封面`标题、类别和评分4个内容’点击一个电影条目’就可以看到这个电影的详情介绍’包括标题`

类别`上映时间、评分`时长、电影简介等内容°

可见详情页的内容远比首页丰富’我们需要依次点击每个电影条目’抓取看到的所有内容’把所 有电影条目的信息都抓取下来后回退到首页° 另外,首页_开始只显示l0个电影条目,需要上拉才能显示更多数据’-共l00条数据°所以

为了爬取所有数据,我们需要在适当的时候模拟手机的上拉操作’以加载更多数据° 综上’这里总结出基本的爬取流程。

□遍历现有的电影条目,依次模拟点击每个电影条目’进人详情页°

·』=■司|』■■■■可||』=■■|‖|‖』】■‖|■■〗」■■司|』司||」■■`||乙■■Ⅵ」■■=■】]□=■】■■曰]二■』■■‖』·Ⅵ■■■■■二■可|■■■』■■|』■殉|』句|』=■■口=■■‖■■■∏■■]』=■司

|25基于∧pp|um的∧pp爬取实战

■■「■■∏‖■「|}







l2.5基于Appium的App爬取实战

563

□爬取详情页的数据,爬取完毕后模拟点击回退按钮的操作’返回首页° □当首页的所有电影条目即将爬取完毕时’模拟上拉操作,加载更多数据° □在爬取过程中,将已经爬取的数据记录下来’以免重复爬取。 □l00条数据全部爬取完毕后’终止爬取。

尸 } }

a基本实现

现在我们着手实现整个爬取流程吧。

■ 凸 ■ 尸 [

在编写代码的过程中’我们依然需要用Appium观察现有App的源代码.以便编写节点的提取规 则°和l2.4节类似’启动Appium服务,然后启动Sesslon,打开电脑端的调试窗口’如图l2ˉ57所示°

『 □ 卜 □ ‖ ∩ ■ ■ ° | ‖ ■ ■ ■ 「



▲尸■}■■~■「■泞■■■■■∏||‖‖|〖=厂

p

■厂|‖‖|■『■二

》「【■|■β

〖 匹■「止尸『‖■「■【▲■「

卜|〉 ■=【。}||》「卜

》|卜



p

图l2≡57电脑端的调试窗口

首先观察一下首页各个电影条目对应的UI树是怎样的。通过观察源代码可以发现,每个电影条

目都是_个a∩drojd.w1dget.[j∩ear[ayol』t节点,该节点带有_个属性Ie5our〔eˉjd为〔咖.go1d∑e. Ⅶvγ∏habjt:jd/1te∏L条目内部的标题是一个a∩droid."jdgetJextγjew节点,该节点带有_个属性

Ie5our〔e_1d’属性值是co们.8o1dze.ⅦvⅧ∩abjt:id/tvtjt1e。我们可以先选中所有的电影条目节点, 同时记录电影标题以去重°

注意这时可能有读者会疑惑’为什么妥去重呢?因为对于已经被演染出来但是没有呈现在屏慕上 的节点,我们是无法获取其信息的°在不断上拉爬取的过程中,我们在同一时刻只能获取屏 慕中能看到的所有电影条目节点,被滑动出屏慕外的节点已经获取不到了。所以需婆记录一 下已经爬取的电影条目节点,以使下次滑动完毕后可以接着上一次爬取°由于此案例中的电 影标题不存在重复,因此我们就用它米实现记录和去重° 接下来做_些初始化声明: 十ro们app1u‖1∏portwebdriγeI 十Io「∏5e1e∩iuⅧ.眶bdriγer。〔omo∏.byj川portBy 千roⅧ5e1e∩jl』Ⅶ。切ebdrjγer.5l』pporti呻ortexpe〔ted〔o∩djtjo∩sa5[〔 千roⅦ5e1e∩i〔」们·webdrjγer.support.uj加port‖eb0rjγer胞jt





□‖



第l2章App数据的爬取

564

q

千ro们5e1e∩iu『∏。〔o「∏∏o∩°ex〔ept1o∏5 j呻oIt‖o5u〔h[1e∏e∩t[xceptio∩ ‖

"deγj〔e‖a『贬": "5NC986o"’

"apppackage": "co阳°go1dze.‖γⅧhabjt"’ "app∧ctiγjty": ".ui°‖ai∩∧ctjγity"’ "∩O【e5et":『Iue

p∧〔Ⅸ∧C[‖州[=D[5IR[0〔∧p∧8I[I丁I[5[ 0apPpa〔kage′] 丁0丁∧L‖∏‖B[R=1OO

这里我们首先声明了5[Rγ[R变量,即Appium在本地启动的服务地址°接着声明了0[5IR[D 〔∧p∧8I[I「I[5’这就是Appium启动示例App的配置参数,其中的deγ1ce‖a‖e需要更改成自己手机的 们ode1名称,具体的获取方式可以参考124节的内容°另外,这里额外声明了一个变量p∧〔悦∧C[‖∧‖[’ 数为l00’之后以此作为判断爬取终止的条件°

接下来,我们声明drjγer对象’并初始化-些必要的对象和变量: drjver="ebdrjγer·Re|∏ote(5[【γ[R’ 0[SIR[D〔ApA8I[I∏[5) wajt=‖eb0river‖ajt(drjγer’ 3O) wi∩dow5ize=dr1γer。8et-‖j∩do佃-51ze()



Q



|』‖

这里的Wa1t变量就是一个‖eb0r1ver‖a1t对象’调用它的U∩tj1方法可以实现如果查找到目标节

‖|

即包名’这是为后续编写获取节点的逻辑准备的°最后声明「0丁∧[‖0‖B[R为1OO,代表电影条目的总

■·■■·‖■■■】‖||‖■Ⅲ■】□□】|】·‖■■



‖|(|」■〗‖‖』□■‖||■□‖

5[Rγ雕≡ !http://1oca1∩o5t;4723/wd/∩ub0 D[5IR[D〔∧pABI[I丁I[5≡ { "p1己t千or‖N3‖∏e同; 00∧∩dIojd"’

点就立即返回,如果等待30秒还查找不到目标节点就抛出异常°我们还声明了"1∩do""1dt∩`

初始化的工作完成’下面先爬取首页的所有电影条目:

(8y.XP∧『‖』 千』//a∩drojd."jd面et了U∩百ar[ayout[0res。urceˉid=′`{p∧α∧C[‖AN[}:jd/jte"』』]『))) retur∩ite∩5

这里实现了—个5cr日pe-j∩dex方法,使用Xpat∩选择对应的节点,开头的//代表匹配根节点的所 有子孙节点’即所有符合后面条件的节点都会被筛选出来,这里对节点名称a∩dro1d.Mdget.[1∩ear[ayout 和属性re5ourCeˉ1d进行了组合匹配。在外层调用了wait变量的u∩t11方法,最后的结果就是如果符 合条件的节点加载出来,就立即把这个节点赋值为iteⅧ5变量’并返回1teⅧ5,否则抛出超时异常。

所以在正常情况下,使用5〔mpeˉ1∩dex方法可以获得首页上呈现的所有电影条目的数据° 接下来就可以定义-个们a1∩方法来调用5cmpe-1∩dex方法了: 千ro们1oguruj呻ort1ogger de+们aj∩(〉:

e1e∏e∩t5=5〔mpeˉi∩dex() 千Ore1e爬∩t1∩e1e们e∩t5:

1+

∩己∏记

== ‖

们ai∩

‖:

"ai∩()

这里在‖a1∩方法中首先调用5〔mpeˉ1∩deX方法提取了当前首页的所有节点’然后遍历这些节点, 并想通过-个5〔rape—detaj1方法提取每部电影的详情信息’最后返回并输出日志。 那么问题明确了’5〔rapeˉdetai1方法如何实现?大致思考_下,可以想到该方法需要做到如下 三件事情°

■《凸‖』■■】‖‖|●■‖‖』(|■■■||」·』』可·]□]|‖』●□‖■【■■【■尸巴■■尸▲■■尸巴■尸巴■尸‖

e1e爬∩t6at己=5〔rape-deta11(e1e∏e∩t) 1ogger·debug(千!5cr己pedd己ta{e1e们e∩tdata}‖)

=■||』】·||纠】■』|■■]■■|||」■·』∩』□】‖●】|ˉ·司□|||

de十5〔rape_1∩dex(): ite们5≡wait.u∩t11([〔.pIe5e∩ceo于己11e1e川e∩t51o〔ated(

刘』■可

w1∩dow-∩e1g‖t变量,分别代表屏幕的宽`高°

l2.5基于Appium的App爬取实战

「 ■「『》|厂β|『■β‖ ■ 尸 』 卜 | | 卜 尸 口 「 ) 》 司 尸 ‖ ■ ■ ■ 「 『



565

□模拟点击e1e‖e∩t’即首页的电影条目节点° □进人详情页后爬取电影信息。 □点击回退按钮后返回首页。 所以’这个方法实现为: de十5crape_deta11(e1e们e∩t): 1ogger.debug(+05〔mp1∩g{e1e们e∩t}|) e1e『∏e∩t·〔1iCk() W己it° (」∩ti1([〔.pIe5e∩〔eO十e1e爬∩t1O〔ated( (By.I0’ f』{pAα∧C[‖酬[}:id/deta11』))) tit1e=wait。u∩t11(f〔。pre5e∩ceo于e1e帐∩t1o〔ated(

(8y.ID’+‖{p∧α∧6[‖酬[}:id/tit1e』)))。getˉattribote(0text0) 〔ategor】e5="己it。u∩tj1([〔·pre5e∩ceo十e1e"e∩t1ocated(

(8y。I0’ 十‖{p∧α∧C〔‖A′0[}:jd/〔ategorje5γa1∏e』))).get—attribute(‖text0)

=■{|●■【■「|‖『■厂|■厂凸■β

s〔ore≡"ajt.u∩t11(〔〔.pre5e∩〔eo千e1e爬∩t1o〔ated( 〈8y.I0’「‖{pAα∧C[‖州[}:id/scoreva1ue‖)))。get-己ttrjbute(』text‖) "i∩ute="ait.u∩t11([〔。pre5e∩ceo十e1eⅦe∩t1o〔ated( (By。I0」 +』{p∧α∧6[‖酬[}:1d/m∩uteγa1ue!))).get-attribute(‖text‖) pub1i5hed日t="ait。u∩ti1([〔.pre5e∩〔eo千e1e爬∩t1ocated( (8y.I0’+|{p∧α∧C[‖酬[}:1d/pub115∩edatγa1ue‖)))·get-日ttIib0te(0text|) dr己爪a≡wa1t.u∩ti1([〔.pre5e∩〔eo千e1e『∏e∩t1ocated( (Bγ.ID’「,{P∧α∧C[‖酬[}:jd/draⅦav日1ue‖))).get-己ttrjbute(‖teXt‖〉 drjver.ba〔k() retur∩{ !t1t1e‖ : tjt1e’

0〔己tegoIjes0 : 〔ategor1e5’ 口‖·尸卜》伊尸

5〔ore『 : 5〔ore’ |m∩ute0 目m∩ute’ !pub115∩edat‖ ; pub1is门edat’ 0dra川a0 : dr日‖a



》「‖尸

实现该方法需要先弄清楚详情页每个节点对应的节点名称、属性都是怎样的’于是再次打开调试

窗口,点击_个电影标题进人详情页’查看其DOM树’如图l2ˉ58所示°

■『●■巴■「■■「◆■尸|■■尸|『■『

b0

| 埠占扭o■ ■·

F

『『





}}

}『 }|‖

~≈



■叮m′0晒



=…

臣藤獭§

『|

溅瓣 图l2ˉ58进人详情页

尸‖



566

第l2章App数据的爬取

为co们.go1dz巳们vⅧ∩ab1t:1d/detai1°详情页上的标题、类别、评分`时长`上映时间、剧情简介也

都有各自的节点名称和re5ourceˉ1d,这里不展开描述了,从Appium的Source面板里面即可查看。

在5〔rape—detaj1方法中’首先调用e1eⅦe∩t的〔11c代方法进人对应的详情页’然后等待整个详 情页的信息(即〔o刚.go1d∑eˉⅧγγ川∩ab1t:1d/detaj1)加载出来,之后顺次爬取了标题`类别、评分、 时长、上映时间`剧情简介,爬取完毕后模拟点击回退按钮,最后将所有爬取的内容构成一个字典 返回。

其实到现在,我们已经可以成功获取首页最开始加载的几条电影信息了,运行-下代码’返回结 果如下:

∑o21ˉ02ˉ240o:〕5:42。929 | 0[8|」C

∩m∩ :5crape—detai1:31 ˉ 5crapi∩8〈appju∏.webdriver."ebe1e爬∩t.

2021ˉ0卫ˉ240o:35:44.S12 | 0[Bl」0

们aj∩ :们ai∩:68 ˉ 5crapeddata{0tit1e‖: 』霸王月|)姬|’ |cate8orje5‖: 0

"eb[1臼肥∩t (5e551o∩="』《+5o6d1ˉ2q8千ˉ438eˉ918bˉ]82cb5〔eb6aa"’ e1e∏mt="eb3e768十ˉ〕7e千ˉ』〔7dˉ9d3bˉ3「d〔d84b6101卿)〉

剧价`绽情0 ′ |5core! : |9。5‖’ `m∩|」te’了‖171分钟0 ’ ‖p0b1j5∩edat|: !1993ˉ07ˉ26’ ’ 0dm‖a, : ‖影片借一出《霸 王别姬》的京戏’牵扯出二个人之间一段随时代风云变幻的爱恨怕仇°段小楼(张丰破饰)与程蝶衣(张国荣饰)是一



| ||

可以观察到整个详情页对应—个a∩dro1d."1dget.5〔Io11γ1ew节点,其包含的re5our〔eˉjd属性值







对打′|、一起长大的师兄弟,两人一个演生,一个饰旦’一向配合天衣允缝’…‖} 2021ˉo2ˉ2q0o:35:44.513 | 0[B0C 川a1∩ :5cr日pe-det日j1:31 ˉ 5〔Iapi∩g<appju们。webdriγer.webe1e爬∩t.

‖eb[1e们e∩t(se551o∩="14千5o6d1_248千ˉ438e_918bˉ382〔b5〔eb6aa"’ e1e∏旧∩t="62a0c23eˉdodbˉ纠28一93adˉ754〔a』+67e6a0,)〉 4上拉加载更多数据 q

现在在上面代码的基础上’加人上拉加载更多数据的逻辑,因此需要判断在什么时候上拉加载数 据°想想我们平时在测览数据的时候是怎么操作的呢?_般是在即将看完的时候上拉,那这里也一样, 可以让程序在遍历到位于偏下方的电影条目时开始上拉。例如’当爬取的节点对应的电影条目差不多 位于页面高度的80%时,就触发上拉加载。将"aj∩方法改写如下:

V







de千爪ai∩():

e1e爬∩t5=5〔Iape_i∩de×() 千oIe1e爬∩t i∩e1e∏]e∩t5;

e1e爬∩t1ocatio∩=e1e『∏e∩t.1o〔atjo∩

e1e爬∩t〕≡e1e爬∩t1o〔at1o∩.get(』y』) j{e1e∏论∩t-y/wj∩do切-height〉O。8:

1oggeI.debug(「‖5〔ro11uP|) 5〔Io11一uP() e1e‖记∩tdata≡5〔mpe-deta11(e1e『∏e∩t)

1ogger。deb仙g(+05〔mpeddata{e1e∩记∩td己ta}0)

这里在遍历时判断了e1eⅦe∩t的位置’获取了其y坐标值’当该值小于页面高度的80%时’触发 上拉加载’加载方法是5CrO11-up,其定义如下: de+5〔rO11-up(): dr1ver。5"jpe〈wi∩d刚wjdt∩*0·5′闪j∩do们-hej8ht*0.8’ wi∩d叫width*O.S’wj∩d叫-height*O。5’1mO)

这个上拉逻辑的实现和124节基本一样’只是上拉动作的起始位置和结束位置有所变化。这样, 在爬取过程中就可以自动触发下_页电影条目的加载了。 5.去重`终止和保存数据



| q

{ 0







‖ ‖





b|







在本节开始部分我们曾提到’需要额外添加根据标题进行去重和判断终止的逻辑,所以在遍历首 页中每个电影条目的时候还需要提取一下标题’然后将其存人一个全局变量中: de「get-e1e眶∩t-tit1e(e1e爬∩t): try;

e1e∩论∩ttjt1e=e1e爬∩t。+j∩de1e∏记∩t—by_id(+0{p∧〔趴C[‖刚[}:id/tvtit1e』).get-attIjbute(0text‖) retuI∩e1e丽e∩ttjt1e

ex〔ept‖o5u〔怔1e∩记∩t[x〔ept1o∩: retl』r∩‖o∩e

d



} {

‖ d

d







0

‖ q



| 巴 ■ ■ 〗 「 △ ■ 「 | |



)|



l25基于Appium的App爬取实战

567

这里定义了_个get—e1eⅧe∩tt1t1e方法,该方法接收_个e1e们e∏t参数’即首页电影条目对应 的节点对象’然后提取其标题文本并返回。最后将∏aj∩方法修改如下: 5〔raped_tjt1e5二 [] defⅦ3j∩():

●「|‖■■■■『「』尸|仿|■「|)【尸

whj1e1e∩〈5〔I己Ped=tjt1e5)〈丁0丁∧l‖0‖B[【: e1e|∏e∩tS≡5〔r己pe=j∩de×() +OIe1e眶∩t 1∩e1e『∏e∩tS:

e1e‖e∩ttit1e=getˉe1e爬∩tt1t1e(e1e|∏e∩t)

i十∩ote1eⅦe∩ttit1eore1eⅦe∩ttjt1e1∩5cmped-t1t1es; 〔O∩ti∩ue

e1e眶∩t1O〔atio∩≡e1e爬∩t·1oCatiO∩

e1e‖记∩tˉy=e1e爬∩t1ocatio∩.get(‖y|) j「e1印e∩t-y°/wj∩dow-bejg∩t〉0·8: 1oggeI.debug(f|5〔ro11up‖)



5〔rO11-up() e1e『∏e∩tdat日≡s〔r己pe-det己j1(e1e们e∩t〉 5〔mped-tit1e5。日ppe∩d(e1e爬∩ttjt1e)



1og8er.deb|」8(「|scrapeddata{e1e们e∩td3ta}|)



这里在们aj∩方法里添加了"h11e循环’如果爬取的电影条目数量尚未达到目标数量『0『∧[‖0‖B[R’

‖「

就接着爬取,直到爬取完毕°其中就调用getˉe1e川e∩tt1t1e方法提取了电影标题’然后将已经爬取 的电影标题存储在全局变量5〔raped-tjt1e5中’如果经判断,当前节点对应的电影已经爬取过了,就

△■=■‖□‖●厂‖β『

跳过,否则接着爬取,爬取完毕后将标题存到S〔rapedˉt1t1es变量里,这样就实现去重了° 6保存数据

最后’可以再添加_个保存数据的逻辑’将爬取的数据保存到本地movje文件夹中,数据以JSON 膛尸厂|▲β卜‖■‖『■【■■尸■■尸△■‖卜『β■厂‖巴β|■‖卜●‖尸

形式保存,代码如下: 1『mOrtO5 j呻ortj5o∩ "「p0『「OlD[R= 0‖mγje0

o5。path.exj5ts(α)∏pⅥ「O[D〔R)oro5·爬促edjr5(仙W0∏「0[0[R) de十5己vedat己(e1e眠∩tdata):

"ithope∩(f,{α』W‖∏「OlD[R}/{e1e贬∩tdata.get(口tjt1e口)}。j5o∩』’W‖’ e∩codj∩g=‘utfˉ8,)a5「: +。mjte(j5o∩。d呻5(e1e∏记∩td己ta’ e∩suⅢea5〔ii=「a15e’ i∩de∩t≡2)) 1og8er·debug(十!5aveda5「j1e{e1e∏记∏tdat己.get(口tjt1e口)}。j5o∩!)

在Ⅷaj∩方法添加调用逻辑即可:

[∏

5avedat己(e1e眠∩tdata)

7.运行结果

我们再运行—下"ai∩方法’看看最后的爬取结果: 2o21ˉ02ˉ24o1:o1:0q。269|D[Bl几 | 归i∏ :三〔mpe-士tai1:33ˉ5cⅢ己pj∩g<appjm·喉bdrjγeⅢ.匪be1…∏t. №b[1…∩t(5e55io∏=闻2刀6e217ˉ8的dˉ』9b3ˉ89〔eˉ888b「86§6〔93口’e1e酝∩t=口63b刀a1十ˉ8e〔bˉqebbˉM〔O-1621〕82e仟4「口)) Ro21ˉo2ˉ2401:o1:05.724 | D[BlL 归i∩ :硒j∏:1o7ˉ5〔Ia酝ddata{!tjt1e‖: |英丽人生0’ ,〔ategorie50 : , 战个、剧价、灾价‖’ ,s〔ore0 : ‘9。1,’ 0刚j∏ute! : ‖116分钟|′ 0Wb1j5hedat|: 0202oˉo1ˉo3」’ ,dm帕|: ’犹太付午 圭多(歹伯托。贝尼尼饰)辽追类丽的★妆芹朵拉(尼可某塔.布担斯基饰) ,他彬彬有札的向多拉勒躬: 口平安!公主| ■ 历经诸多今人宁更皆非的周折后,天遂人总,…0}

2o21ˉ02ˉ2401:01:0S.725|D[BlL

∏m∏ :saγedata:76ˉ5aγeda5十j1e矣丽人生。j5o∩

■●●

■■

『□尸尸「■『‖■■》『尸‖【■■巴■尸巴「|『『■「■厂)‘

此时movic文件夹下的文件如图12ˉ59所示。

纠■■□』■■■·‖

568

第l2章App数据的爬取 <

●●●

■■

一==

■■俄:■■=

翅□ 一■

僻护人.h酶∩

=.ˉL

因。戳氰们◎∩

■手m』■田V



■…

冈飞正侍°回]

■汇

钩=

本蠢■.巴■■ 硕!…↑



l

=■

弘.

■先乍君4…大话E茁之大圣■

.司←



放牛班的P天↓■m飞■环游酬S◎"



n不盯及洒0

喇乱← 大懂T啦之用光宝

宁毡。 大闭矢■…0

■础■q

°扫·1 当牢″酗7↓mn

■■■

狂罗■闰‖…

Ⅳ妖蚊■衣的贝

孩↓匈‖

古巩.

田背山↓Em\

■…∩

丸气

贝之谷加∩

臣狂原脸人睡∏





钢可东↓B□∏

n予来了↓■α`

. ·. 咕尔的移助沮

旦■□】■‖■■■‖

串归m

土↓…

==■■

问●仅乐■』四` 翻攀速件′』吧.阿定门俯世∏.j●m

■起↓… 钨

≡乙使:用沮鸦

割ˉ

■■

■■Ⅵ‖|』■■■

≡L

一≡

冈凡达』由∩

=也

Q

司|



7号房的村扫扫仅`

闻潞~

e√ 回凸

缉弛ˉ

爆v

圈c

〉 『γ酗帕

■0■◎∩

|{



图l2ˉ59movie文件夹

文件。

』可」

至此,我们成功利用Appium爬取了示例App的所有电影数据.并把爬取结果保存成了JSON 8.总结

仪似■■

本节我们通过_个实战案例介绍了利用Appium爬取App数据的过程.学完这节后,App的自动 化爬取不再是难题。

本节代码见https://gjthuhcom/Python3WebSpider/AppiumTest。

有了Applum,我们已经可以方便地自动化控制App,但在使用过程中或多或少还会有些不方便 的地方,例如连接的稳定性一般`提供的API功能有限等°

这里我们再介绍一个更好用的自动化测试工具—Ai扎est,它提供了一些更好用的API’以及_ 个非常强大的IDE’开发效率和响应速度与Appium相比也有提升。 ↑.∧|「test介绍

AmestPrQject是网易游戏推出的_款自动化测试框架’其项目由如下几部分构成。 □Ai∏est:_个跨平台的、基于图像识别的UI自动化测试框架’适用干游戏和App’支持 Windows、Androjd和iOS平台’基于Python实现°

□Poco:一款基于UI组件识别的自动化测试框架, 目前支持Unity3D` cocos2dx、Android原生

App` lOS原生App和微信小程序’也可以在其他弓|擎中自行接人pocoˉsdk使用,基于Python 实现°

能够快速、简单地编写Ajnest和Poco代码°

□AirLab:真机自动化云测试平台目前提供IDpl00手机兼容性测试`海外云真机兼容性测试 等服务。

□私有化手机集群技术:从硬件到软件’提供在企业内部私有化手机集群的解决方案。

绸‖』□||■■】勺|■』】■■■■■■』司‖{‖`二□]■■

□AirtestIDE:提供一个跨平台的UI自动化测试编辑器’内置了Ai碰est和Poco的相关插件功能,

』』‖|■■□■■■■』□】纠曰■■(·‖■■』‖■■』□」□」‖』■■■∏司

|26∧|「test的使用



■■∏■■■■■‖〖■尸『【「|■■匣‖)}巴■「|}【「「‖

l26

Ai〗test的使用

569

2.准备工作

请确保已经安装好AinestIDE、AinestPython库和PocoPython库°

只使用Al戊estIDE实现自动化模拟和数据爬取也是没问题的,因为它里面已经内置了Python模 块、AlrtestPython库和PocoPython库,并且提供了非常便捷的可视化点选和代码生成等功能’即使 使用者没有任何Python基础’也能自动化控制App和完成数据爬取。 但是对于需要爬取大量数据和控制页面跳转的场景而言’仅依靠可视化点选和自动生成代码来自 动化控制APP’其实是不灵活的°进—步讲’如果我们加人_些代码逻辑’例如流程控制、循环控制 语句,就可以爬取批量数据了,这时候需要依赖Amest、Poco以及一些自定义逻辑和第三方库°

Ainest的官方文档(https://ainest.doc.joneteasecom/tuto∏al/l-quick-sta∏_guide/)中已经详细介绍 了Aj∏est的安装方式,包括AlrtestIDE`Ai∏estPython库和PocoPython库°所以’这里建议同时安 装AmestIDE`Amest和Poco°

安装完AjnestIDE之后,它还会安装_个Python环境’这个环境中附带安装了AirtestPython库 和PocoPython库’不过这个被打包在AirtestIDE里面的环境,和系统里安装的Python环境并不是同 一个’所以推荐直接使用pjp3工具将AjrtestPython库和PocoPython库安装到系统的Python环境下。 安装AinestPython库的命令如下: pip31∩5ta11日irte5t

安装PocoPython库的命令如下: pip3 1∩5ta11pOCOl」j

安装完成后’在AlrtestlDE中把默认的Python环境由AirtestIDE附带的Python环境更换成系统 的Python环境。打开AirtestIDE菜单的“选项,→“设置”’页面如图l2ˉ60所示° F--



—_



■■■

SOttt∩够



oe VD启γ『〔e

艾盯坐标盈示

乌i0;咎4』? ˉ

W0∏dOW$荫刁允盗人卢冶.

f机没蠢显示命揪.棋



b0〗:j

■印‖tO「

戴霉棋式,

Ow占、‖〖

卞埠大小.

』..

拐阳器宇酗:

口邑∩2q』Ⅲ

忠勤补全:

;M:



】三挚



0』. ‖『′三Ao『[《吗2‖,》唾上配撰f三拙



:副.§j琶j『缸减-定呸:婴『糙0k

宁∧』∩C5《

《.2;‖ ° 0』M.!:4『 . 』`』《『a"‖剑‖;『0.?.. .

目定义MlmⅧ{P『夕仁涸‘自

酞认№g褥藏渺个:

’b口『『o[d哈「勒′tF’h男『‖b0°.0;}∩u〕息.23u0;】丫」此b刁↑『0C′J0;0『

皇题义刚hO∩.cxe湿嚼.

瓤弓

DD

『/A『r『飞l『l〗kˉ b「『】伯【≈

咐′』9.w}mm?洲o嘲延`′p′:,Mb『; 0』‘.伊。榜′.o.吨′v吨′;!∩『` ,

』『)『0′;.h虑吊w0》0』

0■ 】 0

‖β

■『|‖尸■「■■『‖|■■【『|■β『『`■■『|』尸』′户●「■■【「|侣■「广卜「|△●□「尸|卜』尸|广}|=■「|□卜)『)β°』|『|阻ˉ■尸◆山■「口■ˉ■厂■尸|卜|■■「[β「’『「|■「β■「|●「卜坠■

总之’Amest建立了—个比较完善的自动化测试方案,我们能利用它实现所见即所爬,个人认为 比APPjum更加简单易用。本节我们先简单了解AinestIDE的基本使用’同时介绍-些Ainest和Poco 的基本API用法, l2.7节用它实际爬取一个App。

■■

●知r昆

W肌dow$出司!×嘲倔茄

架用溜攫介瓣串目

■■凸儿■『甘

阂翰阀螺《5; ;

』4『 ‖↓ °↑ .二泌 甩



尸■



忿



〔■



■_-—≡

图l2ˉ60AjrtestIDE的设置页面

■■

可以看到其中有_个选项是‘』自定义Pythonexe路径,,将其值修改为系统的Python路径即可’具 体的设置方法可以进-步参考https://airtestdocjo.netease.com/IDEdocs/settings/l—ide—settjngs/#python°

安装好AlrtestIDE、AirtestPython库和PocoPython库之后,准备一台Androld真机或者模拟器’ 真机的话还需要通过USB线和电脑相连,确保adb能够正常连接到手机,具体的设置方法可以参考 https://aiItest。doc·lo.netease.com/tutoria‖/l-quick—staIt-guide/#一4°

如果以上的准备T作在操作过程中遇到问题’可以在https://setupscrape.center/alltest参考更加详

■■■‖■■∏■∏』■■‖■■·可■■∏■■·‖■■司

第12章App数据的爬取

570

细的配置。

我用_台Android真机演示AiHestlDE的使用方式。首先确保可以使用adb正常获取手机的相关

■』

3.∧|∩est|D巨体验

信息如执行如下命令:

旦■■■■|口■■`勺白■`|。·`

adbdeγj〔e5

如果能正常输出手机的相关信息,则证明连接成功’示例输出如下: adb5erverver5jo∩ (4O)doe5∩"t刚at〔htMs〔1ie∩t (41)j 代111i∩8… *dae∏‖o∩5t己Ited50〔〔es5千u11y U5to「deγice5atta〔hed

RS〔‖3OR阳Q[devj〔e

丁蔫—

从中能看到我的设备名称为R5CN30RM0QL°

然后启动Ai『teStIDE,打开菜单中的‘文件”→』新 建脚本”→`‘.ajrAj吭est项目,,,新建-个脚本,页面

◎0

□Q

局促ba$e 》累



■打开髓本

罚O

■儡″卒

攫5

■■本另存为..

O器5

■近打开"°

l‖哩d5p‖∩

选职

.

0

巴·W纬尔们o∩《偶级用户) 弘马坍坎血





选定一个路径’将脚本命名为scrlptai『,之后点 图l2ˉ6l

击“确定”,进人如图‖2ˉ62所示的页面°

新建_个脚本

A,…闲百≈…乒琶弓≈△■■L『4■→口』…』匹. ;哗『

宁≈





ˉ■

■ ● ■

+● L』







■●





」】■■■】■■■■■当·■■··|■·‖‖■司=■司■■■■‖』‖乙■司』■■■可■■』■■∏‖□·』■■]

凹]≈~∏浮Ⅷ卫T

『□

■口

(□|日(

如图l2ˉ6l所示。

运行

文件 ◆

‖‖(日‖‖‖‖

{ 图‖2ˉ62将脚本保存为script.ajr文件





) ▲■『『匹■■日凸■■「|■尸‖|■■厂

l2。6

Airtest的使用

57l

正常情况下,在图l2ˉ62右侧可以看到已经连接的设备’如果没有看到’可以查看h仗ps:〃aj札estdoc. ionetease.com/IDEdocs/deviceconnection/2androjd_faq/来排查问题出在哪°接下来点击设备列表右侧 的connect按钮’如图l2ˉ63所示。

此时往往就可以在AlrtestlDE中看到手机的屏幕了’如图l2ˉ64所示°



v移动设备连接

■■=}■β「卜尸■厂|卜尸

呼列闻.

沮备秋悲

Rj(=靶`b『圃0$.ko9』〔j‖

恩…f0(.』

镭。

■■■

■■

=■■

[◎∩∏■cⅡ





0

仆‖‖尸卜卜俱∩

图l2ˉ63点击设备列表右侧的connect按钮

我们可以点击页面中的屏幕对手机进行控制’如果出现了连接问题,可以参考https://ai冗estdoc.io netease.com/IDEdocs/deviceconnection/2androld—faq/中的描述来排查常见问题并尝试解决。 ·

之A刚叶似u〉0w坷咕…酗瞬讼w辨↑』『瑶褂伊7…『冉哄棘鹅牵lD回四h』鸦唾愈榔喇 『

几『|

p

■凸 ■T ■



卯胎

亡□

D0△l

·?0■铅Ⅱ■【



■■■

■ 凸

■Ⅷ



■■厂伊■匹广『■■■|巴■■巳尸■■■「{‖■■【

■■DZ【 可·T

■β

『 ■℃旧出

+◇

◆勘〃 0■■· □■□ 目

□■· 『 ■■

驴 ▲

硷 心…

■●



PB



0



卜‖



◎耗.





□ □叶





「↑2

□·

』 Ph■■1

■≈■

向■

● *团蔼



k





b

-

h∏

@四





·





图l2ˉ64AirtestIDE中出现手机屏幕







……=



◎^°







′鱼

至此’请一定确保已完成的步骤都成功了’否则之后的内容将无法进行° 我们再来观察—下整个Aj冗estIDE页面’分为左`中、右=部分,以下内容为对各组件的介绍。

□左侧靠上的部分是Ai∏est辅助窗’可以通过_些点选操作实现基于图像识别的自动化配置。 □左侧中间偏上的部分是Poco辅助窗’可以通过—些点选操作实现基于Ul组件识别的自动化 配置°

|‖』·』】』】】_■Ⅱ』】■■〗‖‖』■】』|□■】‖』】■】□】』■‖』】』【‖〗■■Ⅷ‖〗■■|』』■■‖』】可■■|可司』〗■勺‖·〗‖』■】■‖」司■■]』]■】』』□』可|』·‖‖』■|司』‖々」到‖‖■

第l2章App数据的爬取

572

□中间靠上的部分是脚本编辑窗,即代码编写区域’可以 通过AirteSt辅助窗和Poco辅助窗自动生成代码,也可以 自己编写代码,这个代码是基于Python语言的° □中间靠下的部分是Log查看窗,即日志区域’会输出运

∧↓7te田t辅助因 咕



梢too〔h oWa}t

行、调试时的一些日志。

□右侧是设备窗’内容为手机屏幕’用鼠标直接点击这个 屏幕,真机或模拟器的屏幕也会跟着变化’而且响应速

稳sW|pe

度非常快°

eex‖5t5

4.∧|忱est的图像识别与自动化控制

旧text

Ai吭est可以基于图像识别来自动化控制App’本节我们就体 ●

卤促eγeγe∏t

验-下这个功能。例如先点击左侧的touch按钮’意思是点击屏 幕上的某个位置,如图l2ˉ65所示。

趣S∩ap咖ot

这时AirteStIDE会提示我们在右侧的手机屏幕上截图’这里 图l2ˉ65点击touch按钮

我截取的是‘大众点评,’App的图标,会发现sc∏ptair脚本中出 现了_行代码。代码内容为tOu〔‖方法,其参数是刚截取的图片, 如图l2ˉ66所示° 00

△.二二二8mvt2●←′………T门陌h尸汪…啡″″▲



≥■,0■ 『



沙≡

伯掌毡



+b



i够

●2刻

!

|」|‖‖』‖‖

可』



■h

|_

●…

=■



= 恕“



≡一ˉˉ≡~…一鹅″修●‘

静*

《。…°回°

°●〖



q

‖ ′·企

0

图l2ˉ66 scriptair脚本中生成了touch方法

| U

然后在右侧的手机屏幕上点击‘大众点评,,的图标’进人这个App’再点击左侧的wajt按钮’意 思是等待指定内容加载出来’之后同样根据提示截图,如截取首页左上角的』美食”图标,如图l2ˉ67



■』』■】■司■■■■■■司□■|』■】■■■‖』■■〗‖可

所示°

■ ■

●e

. .卢.□.·





……豫…萨…

…●…菌…

■已

呻…了…鸳…

0.

◎产…敏…

□;皂皿





…m

篆国巢

| ; ′ˉ…

■』



p

573

″讳

刁■





工 …

□℃哩Ⅷ■.





8

…■·w



b□□

f..

= 丁

--~-=-≡

■-



四n臼唾吨由

■■■



″鹏

…。……



隐. , 肾

虽FR3 ^凹

-■b~



≡硒≡

图Ⅱ2ˉ67等待首页左上角的“美食,图标加载出来

再后点击左侧的Sw』pe按钮,意思是滑动屏幕,操作示意和结果如图Ⅱ2ˉ68所示° !●





ˉJ≡ˉ-——-—ˉ—--

‖ ,Ⅷ具 5卓】、

□ □

P【F







∏‖Ⅳ甲「|打丫)|肘′↓|\徊|、『||β}卜}扣℃囚‖}|′队职|(「』ˉ沪〉□′`广、「||》’伪∩γ≥冈/阶》|岛尸》沪})毛厂鼠〔′|~「〕户沪|)》\户||`咕「\§「℃■|」`‖\旦■「\罗|■「‘仰『|凸了))Ⅳ马∩『|>

l26Airtest的使用

p

宠『

-_一 =厂户≈匡…▲■←气■厂迂洲▲扣厂∏■…

■■





『■ ■卢■R



+.

| |

酮…

厂ˉ——一_

|徊

■P□

匡-盂一盂-乙≡刁

■■●■

导日P. ■







0二





图Ⅱ2ˉ68点击sw』pe按钮

这时AirⅡCst』D旧会提示我们框选一个位置°联想自己平时滑动屏幕的场景’手指一开始先放在个位置,然后滑动,到某个位置后停止。那么这时第一步需要框选的位置就是手指_开始需要放置的

【■



二-



5"1pe方法,其第-个参数是我们框选的菜单栏的图片’第二个参数是一个γe〔tor’代表滑动方向。

||

位置’例如图l2ˉ68中标l的地方—中间的菜单栏(菜单栏下方加载的内容会变化,故选择相比之 下更加通用不变的菜单栏作为识别目标)°框选完毕后,AjITestmE会提示我们点选_个滑动的目标位置, 这时选择框选位置上方的一个点即可,如图l2ˉ68中标2的地方。此时会发现scripta∏脚本中生成了

日 |



第l2章App数据的爬取

574

这样我们就通过一些可视化的配置完成了自动化控制。

最后我们再通过左侧的keyevent按钮添加两个键盘事件’在已经生成的代码的开头和结尾分别添 二

■*■■■

八爪@9t辗勘■ ◆

+◆

卜〔『0pt.d‖『



·■‖|司三■■■』‖』』■』■

加一个HOME键盘事件’代表进人首页和返回首页’添加结果如图l2ˉ69所示°

岿0山喧∩

owa{t {

°e斑‖$↑∑ 』≈

圃『eNl

』■」■■Ⅵ‖‖‖】‖』■□□■‖』尸



隅,瓣pe

q





ehG雅Ⅶ砒

勉巳∩出p∑h◎〔



_. .

岳刨eep

Ⅷ`

◎邵睡『te又‖3t3 LoQp岔鸦

0

°巫Te厕-0酗ˉGx『眶

图12ˉ69添加了两个键盘事件

现在总结一下我们实现自动化控制的流程’分以下几步°

(l)进人手机首页° (2)点击“大众点评”App的图标° (3)等待左上角的“美食’’图标加载出来° (4)向上滑动手机屏幕。 (5)返回手机主页° 怎么样,是不是很简单?

注意每个子机的内容可能不一样,灵活配五就好了’原理都是类似的’示例的作用主婆是让大家 熟悉一些操作流程。

接下来点击scnpt.air脚本上方的运行按钮(三角按钮)’会发现Ai冗estIDE可以驱动手机完成指 定操作了’和我们期望的流程一模-样’点击、等待、滑动操作顺次执行’且‘‘Log查看窗’,会显示 执行的具体过程,如图l2ˉ70所示。

以上便是Alnest提供的基于图像识别来自动化控制App的过程’利用这项技术’我们不用编写 任何代码就可以让手机自动操作°

」■‖』■‖|司√|』·■■』■■|』■·』■■|】■』■||』■■∏|』■||■习‖四■■】■■■]||」■〗■|·』■‖{」冈可‖]』■■|□■■]□〗』□■‖■■】Ⅷ



1

} p

「|β

Ai∏est的使用

l26 ■





























0■

■比红吓‘把「马女∩口





F

■■■唱■吕

勺■



·■ 吕■□■r



●ˉ ■

■ ■■

■■

■巳『|■厂‖■■尸

■斗■Ⅶ纂

§已■■x

575

+≈

T0□ ·Q0】■

●2呵、 ·●□■

≈□ ■

□q ‖

》■β■广‖‖‖△■『■■「

■『

0

0T□巳



■■

■□任·『

)尸β尸

圃 $p厨闪

p

f■lGB

簿

● *o

b





■ ■Q

■■

唾m色

g ■■■尸■》■■■■■厂■■●「△■■■

.锋j

f~

其实sc∏ptajr脚本内部对应的就是Python代码,只不过利用AirtestIDE封装了一层使得编写和 操作更加简单了°我们可以追踪一下源码’在当前脚本的选项卡上右击,在弹出的菜单项中选择“打

仪■■【∩

开当前项目目录’’如图]2ˉ7l所示° 会看到源码内容如图l2ˉ72所示。 、

厂ˉ

Sc『『pt.日!f

^√

0嗡

四由



■L0



■ 5.′已.;舌亏q!巴型 ◇●q■







.

■尸匹’|△》■







图l2ˉ70运行script.ajr脚本

乙年些0凶二



f》

0‖°

.

·■

■■ ■ ■≡0=

8工宅

导 00 :=廷◆…;



h;i牙丰】:庐



b巳!↑弘酞脉徊

秽?f卜i咎躺



Sc「bpt.pγ

汹瑚

■酷

【D|‖62∏34986O6 〖p|06丑2350O37‖6 tp‖↑622350097‖6 』6p∏9

O口∩g

6p例g

尸 [ □ = ■ ■ ‖ | | | ‖ ■ 尸 止 ■ ■ 匹 『 ■ ‖ ‖ ■■尸抄【卜||』■■■|‖‖■「户|庄『■■「止=∩‖凸|止>[∩[〔■■■■■厂『□■■■『

图l2ˉ7l 选择‘打开当前项目目录”

图l2ˉ72 scnpt.ajr的源码内容

可以看到其中有l个Python脚本’和3张刚才截取的图片,打开Python脚本: +ro川31Ite5t.〔ore·apimport*

■uto5etup(-千j1e ) |(eyeγe∩t("}|帆[")

tou〔∩(∏eⅦp1ate(r"tp11622349860646.p∩g"》 re〔oId_po5=(ˉo。12’ o.1o3)’ Ie5o1utio∩=(108o’ 2400))) wait(丁e‖p1己te(r"tp1162235o037160.p∩g"’ Ie〔ord一po5=(ˉo.394’ˉ0。826)’ Ie5o1ut1o∩≡(1080’ 240o))) wait(丁e‖p1己te(r"tp1162235O037160°p∩g"’

≤M∩尸∩e"oI日te(r"tD11622350197166°p∩凰 ’ re〔ord一po5=(ˉ0。O04’ ˉ0。419)’ Ie5o1ut1o∩=(1o80′ 240o))’ sⅦjpe(丁eⅦp1己te(r"tp11622350197166°p∩g"’ 00

γe〔tor=[ˉ0.o278’ ˉ0·2121]) 促eγeve∩t(""州[")

≯α

第l2章App数据的爬取

可以看到其内容和AirtestIDE中自动生成的代码基本_致’不同之处在于这里用-个『e阳p1ate对

| ‖

象代替了图片’该对象包含图片名、位置、分辨率三个参数,而AirtestIDE对图片进行了可视化,使 0

其更加直观°

我们可以直接使用Python环境运行这个脚本吗?可以’但是需要在代码开始的autoˉset0p(-{i1e_) 和keyeγe∩t("‖州[||)之间添加—行代码j∏jtdev1ce()。调用i∩jtdeγ1〔e方法的作用完成—些手机的 初始化配置’不做这_步可能会有错误。运行脚本之后’会产生同样的效果—手机被自动化控制执 行了—系列操作’同时控制台输出对应的操作日志,如图l2ˉ73所示° 厂



| q





●●●

499329}

[13:21:5S][IN「闪<oi7te5七。core。口Pi>T尸y「i门矾"g:丁e呵1□te(tPU唾2350197166。p∩g) te(tpⅡ1唾235019716 50197166。p∩g) 「"口江∩1∩g [13;21:55][0[BU6]≤m仕eSk.Co「e.αPt>t广y印otC柯悯i七∩50R「№江∩i∩g [13821:55]["∧RMM]<αi穴est.coFe锤卯i> !su厂「‖/.51「七0/0b『ie「, i5i∩oUe∩cvˉco∏tFib i5i∩Oue∩CV 厂b0m〔γ 旧mαu1e. γoucα晚use 0七p10/0kαZe0/bb了i怠k『/·αkα∑G0/0◎厂b『m〔γS丁朋丁“γ’ ·αkα工G0/0◎厂b『m〔γS丁RA丁“γ’ o厂厂e1∩St OUoPe∏cγwit向七∩ec◎∩tFib‖∏muIe·

e呻1□te№ [13:21855][D【806]<m穴es之·co尸e·αpt>t庐y帕t〔∩wit∩ t〔∩Wit∩『c呻l□te№tch加g 『c呻l□te№tCh加g [13日Z1:55〕[D旧B06]<αiFte5t。α1广cv。te呻1°teˉmtchi∏g≥[『e呻1◎七e]thFG5∩oId二0.7’ ◎tc∩i∏g>[『e呻1◎七e]thFGs∩oId二0.7’ 丁e呻1◎七e] 「e5 Pe5 ult={,「e£u1t0:(530’5q4), ,7eC七α∩gIe, 目 ((40, 40, q88),(硼,铜0)’ (‖a铜0)’ (10z1, 6硼)0 (10Z

1’ 488)), ,〔o∩fiOef汇e0 ; 0°9985886812210083} } ][0跟0‘]≤m「tG酞ˉα1下Cγ°te呵1毗e=mtc [13:21吕55][D[80‘]≤m「tG酞ˉα1下cγ°te呻1口te=mtc向i∩g>「i门dˉh唾t=7esM1tO庐u∩t1『∏et αtc伪i∩g>「i"dˉh“t=7esM1to庐u∩t加et $0°065.

1221酚83}

[13:21:56][匪BU6]<mPtest.co庐e°o∩d沪o洞·odb>/M5F/1◎CαI/(◎靴厂oα∏/m"icon口α/bαse/e [匪Bu6]<mPteSt.co庐e°o∩d沪o洞′odb>/M5F/1◎CαI/(◎ >/M5F/1◎CαI/(◎靴厂oOw厕i"icOn口α/bαSe/e

5.∧|「test的相关∧尸|

用法示例如下:

示例代码的运行结果如下: 〈airte5t.core.a∩droid.a∩droid。∧∩drojdobje〔t己to×1o18「3a58〉

可以发现返回结果是—个∧∩dro1d对象。这个∧∩droid对象实际上属于ajrtest.coreandroid包’继 承自日1rte5t.〔ore.deγice.0eγjce类’与之并列的对象还有ajrte5t.〔ore.1o5.1o5.I05`ajrte5t.core. 1i∩ux.1j∩u×.[1∩ux` ajrte5t.CoIe."i∩."1∩.M∩dows等。这些对象都有一些用来操作设备的API,下 面我们以这个∧∩dro1d对象的API为例总结—下° □8etˉde+au1tdeγj〔e:获取默认设备° □‖ujd:获取当前设备的UUID。

□1i5tˉapp:列举设备上的所有App°

』■』』■可■■』■】■日|‖■■■|‖{」■】叫□■■·|纠■』■】|」■■】】』■|」■』尸■■]」■■|■■■■■■

deγice=j∩1t-deγj〔e(0∧∩dro1d|) Prj∩t(deγjce)

□ 、 □ 】 ■ 曰

de十j∩jtˉdeγi〔e(p1日t十or川="A∩droid"’ uujd=‖o∩e’ **|《"arg5):

』 ■ ■

上面的内容仅展示了AinestPython库的冰山-角’本节列举—些它提供的便捷API°从刚才添加 的i∩1tdevjCe方法说起,这个方法就是用来连接设备并初始化—些连接对象的°如果设备没有初始 化则会先初始化设备,并把初始化后的设备当作当前设备。这个方法定义如下:

□|■■|||·■∩|‖』】■■■Ⅵ」』■■|

至此,基于图像识别来自动化控制手机ApP的流程就介绍清楚了°

■引」■ ||』∩」‖

图l2ˉ73控制台输出的操作日志

』■Ⅵ」=■

F/loCα1/〔◎纬「o咖/耐t∏i〔o∩ [1引z1:5刁[0[Buc]<αt7te5t.CoFe.□∩dF◎td.αdb>/u二F/loCα1/〔◎纬「◎αM‖tmCo门o□/mSe/e e/e ∩v5/py3γ/1ib/py饰oⅣ3·7/5iteˉpock□ge5/αiFte5t/c◎「e/□徊厂oid/stottc/◎db/日m〔/α创b-5 悯5α30R酗QL「o刚α厂d≡厂e‖耐wetCP:』乙886



■■

5t/c◎「e/□mFoid/St◎tic/αdb/加〔/mbˉ5 ∏γs/py37/1tb/pyth◎"3,7/5iteˉp◎ckαge三/°i厂te5t/c◎「e/□"刨Foid/st◎tic/αdb/加〔/mbˉ5 代5〔‖驹R蛔0[5∩eu1∩p毗赏eyeγe∩t"叫幅 [13:21:57][0懂B0愚]<□1『te邑tˉco7e。α∏β「◎iα。αdb>/u5F八om1/〔αs怖…加imco∏do/b◎乌e/e 了e/α∩口P◎id/邑tαttC/Gdb/‖旭 =s ∩γ$/py37/Mb/py恤o∩3·7/5让e=pα〔k四e5/αi「te白t/C◎re/α∩βP◎M/邑tαttC/Gdb/啊C/◎db=s R5〔州30RⅧQlfopWm创ˉˉ「e∏℃vetCP:1278Q

‖■

][0EB汕6]≤口iFte5七.〔O厂e。op1>们αtCh沪e5 [132Z1自55][0EB汕6]≤口i厂te5七.〔o厂e。op1>们αtchFesu1t5 FeSu1t5 {,Fe5u1t,: (5300醉4)’ (530p函叫), 0厂e〔to∩ 0庐e〔to∩ g1G『:((4Q088)’ 》 q88)’(蛔’“0)’O0Z1’ (q0’的0)’ (10Z1’6删)’ 600)’(1021, (1021’£88)), 488)), ` `〔o∩fide∩〔e0 目 0°的858868 0°9g858868

‖‖ 』 ∏ ■ ‖ ‖ 』 · | □ ‖ ● 日 ■■||凹



576

l26

Airtest的使用

577

□pat∩ˉapp:打印出某个App的完整路径。 □c∩ec代—app:检查某个App是否在当前设备上° □5tart-app:启动某个App。

□5tartˉappˉt1‖1∩g:启动某个App’并计算启动时间° □5top-app:停止某个App° □〔1earˉapp:清空某个App的全部数据° □j∩5ta11ˉapp:安装某个App° □i∩5ta11‖u1t1p1e-app:安装多个App° □u∩1∩5ta11-app:卸载某个App°

□5∩ap5‖Ot:获取屏幕截图° □5be11:获取adbshell命令的执行结果。 ■■■【『|》巴■厂卜【∩||[■〖■「|■{∩『‖卜『‖●‖|『■「|『「●「■』●「}卜[■厂■卜『|■■■∩}■「|〗β卜』∩‖◆『







□促eyeγe∩t;执行键盘操作。 □Wake:唤醒当前设备。

□∩o"e:点击HOME键°

□teXt:向设备输人内容°

□tOuC∩:点击屏幕上的某处。 □do‖b1e〔11〔低:双击屏幕上的某处°

□5W1pe:滑动屏幕’由一点滑动到另外一点。

□pj∩〔∩:通过手指的捏合操作放大或缩小手机屏幕° □1og〔at:记录日志。

□getpIop:获取某个特定属性的值° □get一1p—addres5:获取IP地址。 □getˉtOpˉa〔t1γ1tγ:获取当前∧〔tjγ1ty。

□get-top-a〔t1γjty-∩aⅦe-a∩d-p1d:获取当前∧ct1γjty的名称和进程号。 □get-top—actiγjtγ一∩a"e:获取当前∧〔t1γjty的名称° □15ˉ促eyboardˉ5how":判断当前是否显示键盘了。 □151o〔ked:判断设备是否锁定了。 □u∩1oC促:解锁设备。

〕d15p1ay—1∩+o:获取当前的显示信息,如屏幕宽高等。 〕get-dj5p1aγˉ1∩十o:同d15p1ayˉ1∩+o°



□get一curre∩tre5o1ut1o∩:获取当前设备的分辨率° □8et_re∏derre5o1ut1o∩:获取当前喧染的分辨率° □5tartrecoIdi∩g:开始录制°

□5topˉrecordj∩g:结束录制。

□adju5t—a115〔ree门:调整屏幕的适配分辨率。 了解了这些API的功能之后,下面用一个实例感受一下它们的用法: 十ro‖airte5t°core。a∩droidi刚port∧∩dro1d 十IoⅧa1rte5t.core°api加port* mport1ogg1∩8

1oggj∩g.get[ogger("a1rte5t").5et[eve1(1ogg1∩g.‖∧∩‖I‖C) deγj〔e; ∧∩drojd= 1∩itdeγj〔e(|∧∩dro1d|) 151o〔|《ed=dev1〔e.j51o〔促ed()

pr1∩t(+015ˉ1o〔|(ed: {151o〔|〈ed}』)

F

‖‖■田

第l2章App数据的爬取

‖|‖

578

j+j51oC代ed:

deγ1〔e.u∩1oc代()

uu1d=αeViCe.uu1d

prj∩t(「{uujd{0uid}‖)

di5p1ay-i∩千o=deγi〔e.get-d15p1ay-i∩+o() pri∩t(f|dj5p1ayi∩+o{dj5p13yˉi∩「o}!)

‖‖‖

app—115t=deγj〔e.1j5t-己pp() pr1∩t(十!app115t{app-1iSt}!)

』|■

deγj〔e."冰e(〉

re5o1ut1o∩≡deγjce。get-re∩derre5o1utjo∩() prj∩t(+!Ie5o1utjo∩{re5o1utio∩}|) ■

1p—己ddre5s=deγice.getˉip-addre55()

pri∩t(+』1p3ddres5{ip—addre55}』)

topˉactjv1ty≡deγj〔巳get-top-日〔tjγjty(〉

pri∩t(f!top己〔tjγ1ty{top-a〔tiγjty}‖)

」|‖《

j5ˉkeybo己m5hoM)二deγjce。i5ˉReybo己msh酗∩() pri∩t(「|iskeyboardshow∩{i5ˉ代eybo己Ⅲd5∩咖∩}0)



这里我们调用API获取了设备的-些基本状态’运行结果如下: i51o〔长ed:「a15e

app1j5t [‖〔o们.|(mcy929.5〔ree∩Iecorder!’ ‖co′∏.a∩dro1d.proγjder5.te1epho∩y! ) !io.appju".5ett1∩g5!’ 〔oⅧ.a∩dro1d.Ⅳa11p己per〔ropper 》 co∏]·a∩drojd。docu爬∩t5uj‖’ !co∏‖°a∩droidga1axy40 ’

〔o们°己∩droid.extema15torage‖’ 0〔o∏·a∩dro1d°ht川1γjeweI‖’ 『c咖·己∩dIojd°quj〔代5ear〔∩box‖’ 〔o川.日∩dIojd.m5.5erv1ce0 ’ |〔o『∏.a∩droid.proγ1der5.d叫∩1oad5‖》 0吧rk。qr〔ode0 』…’

co".goog1e.日∩droid。p1己y.g己『‖e5! ′ 0jo.|(kz5‖’ ‖tv。d日∩阳促u·bi1i0 ’ |co瓜己∩drojd.〔aptiγepoIta11og1∩|]



‖‖‖《日

co".a∩droid.pro`′1der5.ca1e∩d;r』{ 』c咖.a∩drojd.pro`′jder5.爬d1a』’ 』co‖||.go1dze.|wⅧhabjt|’

0l」1deⅧu1日torˉ555』

di5p1己yi∩+o{‖id‖:o, ‖mdth‖ : 108o’ |heig∏t′: 1920’ |xdpi|: 〕2o.o’ ′ydp1! : 32o.o’ 0s12e|: 6,88’ 0de∩sjty0 8 2·0’ 』千p5‖ : 6O.O’ |se〔l」Ie0 :丫n」e’|rotatjo∩‖:O’ ‖orje∩t己tjo∩0 :O.O’ !phy5jca1wjdt∩‖ : 1O80’ ‖phy5ic己1ˉ∩eight‖: 192O}

√』

1paddre551o°0°2.15

{ ‖ {

re5o10tio∩ (O.0’ O.0’ 1080.0’ 192O。0)

topa〔t1vjty(‖〔oⅧ。mcro5o什。1au∩〔her.deγ|’ ‖〔o们.川1〔ro5o代.1al』∩〔∩er.[au∩c‖eⅢ! ’ |160400)

i5代eγboard5how∩「a15e

从结果可以看到’借助一些常用的API’我们就完成了唤醒手机和获取App列表`UUID`显示 ●获取当前设各

Ailtest中有一个全局变量C,它的0[γI〔[属性代表当前的设备对象。直接调用deγ1ce方法即可 获取当前设备’该方法定义如下: ret0r∩6·0[γI〔[

●获取所有设备

‖ 日 引

de+deγi〔e(〉:

·司||‖■曰‖|」■■‖‖|』■

器信息`分辨率` IP地址、当前运行的ACt1γity、是否显示键盘等_系列操作。

获取所有设备的方法如下:

pr1∩t(C.D[γI〔[ [I5丁) uIj≡ 0∧∩droid8//127·0·0.1:5O37/e∏u1atorˉS554|

dev1〔e: A∩dIojd≡〔o∩∩e〔tdevj〔e(ur1) prj∩t(C.D[γI〔[ [I5「)

运行结果如下:

』■■γ』‖|』■可司□■‖■‖纠·可||■』■|』■■‖』■|』■〗‖伊|』■■】■∏‖■■■■■

「ro‖ajrte5t°〔ore。a∩dro1dmport∧∩droid 十ro‖a1rte5t.core·己pj1呻ort*

l26Airtest的使用

579

[]

[〈ajrte5t.〔oIe,a∩droid.a∩drojd。∧∩drojdobje〔tat0x1oba03978〉]

0[γI〔[[I5丁是_个列表,元素是Airtest当前已经连接的设备°需要注意—点,在没有调用 〔o∩∩ectdeγi〔e方法时, D[γI〔[ [I5丁是空的,调用co∩∩ectdevj〔e方法之后, 0[γI〔[ [I5丁中会自动 添加已经连接的设备° ●执行命令行 可以调用5he11方法,传人CⅧd参数来执行命令行’该方法定义如下: 01Ogwmp de「5he11(Cm): Iet0r∩C.0[γI〔[.5∩e11(〔刚)

直接调用adb命令就可以了’例如执行如下命令获取内存信息: +r咖日jrte5t·core°ap1j呻ort* uIi= 0∧Mrojd://127·O·0.1:5O37/咖』1atorˉ5554|

Cα]∩eCtdeγi〔e(uri)

}‖

||‖卜[仿‖『「≈[厂||■『卜‖‖份【■「|·[伊『|卜|■「|[■[尸||厂|卜



resu1t=s∩e11(!cat/pro〔/‖e∏‖i∩十o|) pri∩t(reSu1t)

运行结果如下: ∩团「ota1:

36279O8代8

‖e川「ree: "e∏叭γai1日b1e:

265556ORB 2725928RB

0u仟er5; 〔a〔∩ed:

D1re〔t陶p眺: Djrect陋Pq":

3496促B 107△72仪8

16376k8 892928促B

这样我们就成功获取到了设备的内存信息描述。

●启动和俘止ApP

调用设备的5tart=aPP和5tOPˉaPP方法,然后传人App的包名,即可启动和停止这个App,两个 ■■「

方法的定义如下:

|『

【■「[厂)‖β「|』■「|【■「||[■||{■『||■『|》■β巳『「}}■■「|■「「‖}■■■口■『

01嘲rap

def5tartˉapp(pa〔仪a8e’a〔tiγjty=炯论)目 C.D[γI〔[·三t■rL印P(Package’己ctivity) 01睡mp

de十5topˉ印p(pa〔ha8e): 6ˉD[γI〔[ˉ吕t卯ˉ日pp(package)

用法示例如下: 仕叼airteSt.〔Ore.日pii…rt* 皿i= 0灿droid://127.O.O°1:5037/酗1atorˉS554` Co∏腮Ct灾γi〔e(uri)

paCRage= 0〔m,teme∩t·m0 5tartˉapp(pa〔吨e〉 51eep(』O) 5top一app(p己〔仪a8e)

这里我指定pac低3ge为徽信的包名’然后调用5tartˉapp方法启动了徽信,等待l0秒后’调用 5tOpˉapp方法停止了微信运行°



】〗】●■■‖‖‖白』‖』」■■‖‖』】■‖‖|

580

第12章App数据的爬取 ◆0□知

G▲●

日」·‖·

●安装和幻载App

[动卯a听

调用设备的j∩5ta11和u∩j∩5t己11方法,前者传人App的保存

路径’后者传人App的包名,即可安装和卸载对应的App,两个 方法的定义如下:



@1Ogwmp defi∩5ta11(千i1ep3th’**代"arg5): retur∩6。D[γI〔[.i∩5ta11-app(+j1ep己t们’ **kⅣarg5)



|」

01OgWr己p de千u∩j∩Sta11(pa〔kage): retur∩OD[γI〔巳u∩j∩5t日11→app(pa〔促age)



●截图

」Ⅱ■』‖|叫|』勺」·〗‖‖日』‖‖司

调用5∩aP5∩ot方法获取屏幕截图可以通过参数设置存储截 图的文件名称和图片的质量等°该方法的声明如下: de千5∩3ps∩ot(+11e∩a爬=‖o∩e’"5g="′′’ qua1jty=5丁.5‖∧p5‖0『Q0A[IⅣ)

用法示例如下: 图l2ˉ74生成微信截图

+ro『∏ajrte5t·core·日piiⅧPort* ur1二 !∧∩dro1d://127.0·O.1:50〕7/e刚1atorˉ5554‖

Co∩∩ectdeγ1〔e(uri)

p日Ckage≡ 〔o们°te∩Ce∩t。咖‖ st己rtˉapp(p日〔促age〉 51eep(3) 5∩aP5hot(Wej×j∩.p∩g ’ qu311ty=30)

‖0:49』 · 』 8|830. ∏吨蛔z『咖胎仇…『



运行这段示例代码后’当前目录下会生成_个名为weixinpng 的图片’如图l2ˉ74所示°

↑辐 妇见‖f▲△…, .≡凸

ˉ…司≡…旷……~,.…









01O8WI己p de+"日ke(): C.0[γI〔[。"日ke()

▲-

●←

个方法定义如下:

图…

田…

调用设备的w冰e和∩oⅦe方法,即可唤醒App和回到首页,两



●c●● 01OgWr3p de「hO∏|e(): CD[γIα.ho∏论(〉

阁l2ˉ75

当前的手机屏幕

这两个方法不需要任何参数’直接调用即可。 ●点击屏纂

调用tou〔∩方法点击屏幕’可以传人要点击的图片或者绝对位 置’还可以指定点击次数’该方法声明如下: 01ogWrap de+touc‖(v’ tme5≡1’ **灿aI85)

例如我的手机屏幕现在如图‖2ˉ75所示°



Weathe『

然后把这张图片声明成-个『e们p1ate对象传人tou〔∩方法:

图l2ˉ76从手机屏幕上截-张图片

■·■·纠■|」|■■■■■■=■■‖‖□■■】|』■■□』∏‖□■□〗■■‖■■■■■■■

截_张图片,如图12ˉ76所示°

□■■■』匀■】■』■]‖(」□|」可」■‖‖甲■□■■■{·」■●{|」■■‖』《

≡-~凸■

闯…

霹二罚T

●唤醒和回到首页

] 日β





使 的

●●·■■

$ 巳







β

]∑





十rO们日jrte5t.〔OIe·apji们pOrt * 0

「}

』▲■「|卜但∩}‖||■~|■∏|

))



uIi= ‖∧∩drojd://127.O·0·1:SO〕7/e爪u1己torˉ5554‖

〔o∩∩e〔tdeγi〔e(uIi〉 touc‖(丁e帅1ate(‖tpLp∩g0 ))

运行这段代码’设备启动后,就会识别这张图片所在的位置,然后点击。这个例子是传人了要点 击的图片’我们也可以传人要点击的绝对位置’示例代码如下: 十rOⅧajrte5t。〔ore。己p1i们pOrt* ur1= `A∩droid;//127°O°0·1:5O37/e们u1己torˉ555』‖

Co∩∩ectdeγice(qri) ∩oⅦe() touch((4OO’ 6")’ tme5=2〉

【β『‖‖|■尸『『‖||

另外’touch方法完全等同于〔11c低方法。如果要双击’还可以调用doub1ec11c代方法,其参数 和to0〔h方法_样°

‖「

●滑动

β■ ‖巴■【|巴’巴∩|■■■‖尸=■尸|『巳■|■仕》β『■「β●β〖=■「‖‖【■■■■■■=■『』■尸|■β‖■尸〖=■厂■尸||■β『}■■■「卜卜‖巴■【卜巴■■『●=■‖匹■「ˉ■■■■『【■‖巴|△■「止尸匹■■∏【■

调用5"jpe方法滑动屏幕’可以传人起始位置和结束位置’两个位置都可以是图片或者绝对位置’ 该方法的声明如下: 01OgWrap

de+5wjpe(v1’ v2=‖o∩e’ γe〔tor=‖o∩e’ **灿ar85)

例如这时候我想让控制手机向右滑动即可实现如下代码: 十ro川3jrte5t。〔oI巳ap1jⅦpoIt* ‖r1= 0∧∩droid://127.0·o。1;5037/e‖01atorˉ5554| 〔o∩∩ectdev1〔e(ur1) hα‖e(〉

5wjpe((2OO’ 3O0)’(9OO’ 3OO)) ●放大缩小

放大缩小调用的是p1∩〔h方法,可以通过j∩orout参数指定放大还是缩小’还可以指定手指捏 合的中心位置点和放大缩小的比例°该方法的声明如下: β1ogwrap

de十pj∩〔‖(j∩ˉoI一out=|i∩|’ ce∩ter=‖o∩e’ per〔e∩tˉ0·5)

用法示例如下:



千r咖日jrte5t·〔ore。apii川port* uri= 0∧∩dIojd://127。0。o·1:5O37/e∏‖」1atorˉ5554! 〔o∩∩ectdeγ1ce(ur1) hα肥〈)

pi∩c∩(j∩一orˉout=`out|’ 〔e∩ter=(3OO’ 30O)′ perce∩t≡O.4)

这里我们调用了p1∩C∩方法’并且指定了放大动作Out’同时指定了捏合中心点和放大的比例’ 运行代码之后手机上便会模拟执行此操作。 ·锭盘享件

调用促eyeγe∩t方法来按下某个键’例如HOME键、返回键等°该方法的声明如下: de千促eyeγe∩t(|〈ey∩a们e’ **k"aIg5) 用法示例如下:

促eyeγe∩t("‖渊["〉

这行代码的意思是按下HOME键。

[忆『

‖〈』■、』‖|■

第l2章App数据的爬取

582

●输入内容

调用text方法来输人内容,前提是目标Wjdget需要处于active状态°该方法的声明如下: @1ogwrap de十text(text’ e∩teI=「rue》 **kwarg5)

调用该方法之后, 目标‖idget就会输人相应的字符’输人完之后会执行一次确认(按回车键)。 以上就是对Aj励est常用一些的API的用法总结。

| 〕



6.基于尸oco的0|组件自动化控制

在某些场景下’基于图像识别来自动化控制APp是比较方 便的.但也存在—定的局限性。例如图像识别的速度可能并不

快,以及App中的某些UI如果更换了,就无法和之前截的图

砷叼助■ √s《°p 脑叼助■

巳们气 ·巳C》气

√stop UⅧW U∩‖W

q

0田 Cmo5=‖u■ 0田

『“佃ˉ!:

片匹配成功’这些很可能会影响自动化测试的流程°

『∩f佃=【■

.A∩o『O础

所以,这里再介绍—个基于Poco的Ul组件自动化控制,

0

瞪 jo5

‖■ □ ‖ 』 ‖

α 玖dˉb『Oke「

说白了就是基于UI名称和属性选择器的自动化控制’有点类_酗三bmke『-

似于Appjum` Selenjum中的XPath。







m

匹』

巳 巳

然后点击左侧‘Poco辅助窗,,的下拉菜单,选择』oAndroid,’,



图l2ˉ77选择“Android’’

操作有点像在测览器开发者工具里选取网页源代码,其中的Ⅲ组件树就相当于网页里的HIMLmM树° =■vl凹

~w



■□□司□』■■··|



当·可·‖‖‖|·』■勺

意思就是导人了Poco包的∧∩dIo1d0jautα旧t1o∩po〔o模块,然后声明了—个po〔o对象°接下来就可以 通过po〔o对象选择_些内容了°例如点击左侧UI组件树中的“Dianpmg,,节点’会发现右侧手机屏幕上 对应的App高亮显示了’在‘‘Log查看窗,中还可以看到该节点对应的所有属性,如图l2ˉ78所示。这个

二■|司·】||‖』■

十ro川po〔o.drjγer5·a∩drojd.uiauto们atjo∩mport∧∩dIo1dl」1auto们己tio∩po〔o poco=∧∩dro1dUj己l」t咖己t1o∩po〔o(u5eaiIte5t-1∩put=「Iue」 s〔ree∩5hotea〔h己〔t1o∩二「a15e)

‖ 』 ■

这时AlrteStlDE会提示我们更新代码,点击确定后脚本中自动添加了如下代码:

■■‖=■司

如图l2ˉ77所示。



‖‖|‖·〗

新建_个脚本’命名为sCrjpt2.air,右侧同样连接好手机,

』■‖{

擎咖;umⅧ们则… 5.柏蹦;"mwm‘°鞠

‖】 ld



@回

g 啮』

图l2ˉ78点击“Djanp!∏g,,节点

●』

/司回Ⅺ■■厕问川凹





■■可|□■‖|■■‖』··□■■可‖■■■●□■■■』■』■■□

″歇 °



°

■ *o●

』』](









l26Airtest的使用

583







直接双击“Dianpjng,,节点’sc前pt2ajr脚本中便会出现对应的代码:



poco(!|0ia∩pi∩g闻)



这是什么意思呢?为什么这么写?我们来看—下poco这个API,它是—个∧∩drojd0jaum‖at1o∩poco

0。

对象,查阅Poco官方文档ht‖ps://Poco【℃adthedocsjo/zhCN/latest/source/pocαpocMWhtml可以得知, 其用法类似如下这样:

〉 } 卜







pO〔O≡∧∩drO1d0jautO|∏己t1o∩pOCO(…) 〔1o5ebt∩=po〔o(|〔1ose|’ type=‖B‖tto∩|)

会发现, po〔o本身就是一个对象,但可以直接调用UI组件的名称,这归根结底是因为实现了个 〔a11方法: def





〔a11—(5e1千’∩a爬=‖O∩e’ **kN): i十∩ot∩a爬a∩d1e∩(低")二≡O:

"ar∩1∩g5."ar∩(0!‖j1d〔ard5e1e〔tor‖己ycauseper「om己∩cetrol』b1e. p1ea5egiveat1ea5to∩e〔o∩djtjo∩ tOBhIi∩促ra∩geO+reSu1t5") retur∩0I助jectproxy(5e1f’∩a低’**灿)

b







「 p







|□

P

0

p











β

卜 b

可以看到 〔a11

方法的第_个参数就是

∩a爬,其他参数都以k"的形式传人,可以任意指

定,最后返回—个0IObjectproxy对象。 回过头来’我们看看“Dianpmg,’这个节点的 ∩aⅧe参数值是什么,这个在“Log查看窗”内显示

tOg豫岳■

伯尸



团恤十卜OⅦ沪□ot 『YOd臼吕 [o′`〕 ’ :

01副∩pj∏g

:

山a∩p】∩g



oia咖1间g :

γ『boe

:

γ广l』e

: {0g1◎b己』,:

: 〔



j





′ 。1o〔目1` :





: 「a15e

可以看到其∩a爬值就是0j己∩pj∩g’而且整个

「己1与G

。 [ 』 ] : [

UI组件树中没有与其同名的节点,于是可以直接

这3种写法都能选取同样的节点°



b0c◎∏0甸Ⅶ止「o二O千t·1吕uⅣ〔∩e『,

:



皿o(!Dja∩pj∏g0’ type=‖a∏drojd.mdget.『extγieW) 应o(,D1己∩pmg!’text≡Dia∏pi呕『) pmo(`D1■∏pj∩g0’text=,Dia∩pmg!’des〔=!Di己∩pmg!)

0

a间d厂Om.Ⅶ1dget.γe讽Wi遏W

8

得很清楚’如图12ˉ79所示。

调用poco(!0ia∩pj∩g|)选取这个节点°当然,也 可以任意指定pOCO的其他参数’例如:



ay1o己Gd噎tm15:



丁厂u台

ˉ

丁「u僧 吕

·



『庐ue



·



L

】 「 仁a』ge

·

p副15e

?

8

p司1∑己



「吕15e

;

Fa1≡G

图12ˉ79“Dianpmg阔节点的∩a‖距参数

刚才说到, 〔311 方法会返回一个0I0bje〔tPⅢoxy对象,现在我们来看_下这个对象的实现’ 其API链接为bt胚:/巾ocommm剑ocs.iα迹CN/lmes″soume/poco.pmxyhml°从中可以看到它实现了

-getjt印、 iter 、 1e∩ 、c∩j1d、chj1dre∏、o仟5pri∩g等方法,所以可以实现链式调用、索 引操作和循环遍历° 其中—些比较常用的方法如下°

0



0













□c∩j1d:选择子节点。第_个参数是∩a雁,即Ⅲ组件的名称,如a∩drojd.wjdget儿j∩ear[ayout

等’还可以额外传人_些属性辅助选择,其返回结果也是0IObjectproxy对象°

□pare∩t:选择父节点°该方法无须传人参数,可以直接返回当前节点的父节点’返回结果同样 是0I0bjeCtproxy对象°

□5ib1j∩g;选择兄弟节点°第_个参数是∩a|"e;即UI组件的名称’同样可以额外传人一些属性 辅助选择,返回结果依然是0I0bje〔tproxy对象° □〔1j〔代、r〔1i〔代、doub1e〔1ic戊、1o∩g≡c1jC促:分别是点击、右击、双击、长按°0I0bje〔tproxy 对象可以直接调用这几个方法,参数focu5用于指定点击的偏移位置’51eepˉj∩teIγa1用于指 定点击完成后等待的时间(单位为秒)°



第l2章App数据的爬取

584

□5w1pe:滑动操作°参数djre〔t1o∩用于指导滑动方向, 十ocu5用于指导滑动焦点的偏移量’ dumt1o∩用于指导完成滑动所需的时间°

□wa1t、侧ajtˉ卡orˉappeara∩〔e:等待某节点出现°参数t1"eout用于指定最长等待时间。 □attr:获取节点的属性值°参数∩a‖e用于指定要获取的属性名,如γ15ab1e、text、type、po5、 5iZe等°

□getˉteXt:获取节点的文本值°这个方法非常有用’可以获取某个文本节点内部的文本数据。 下面调用一下〔1j〔促方法’将代码改写为:

这样就可以选中并点击‘Dianpjng”节点了°点击之后,就进人了“大众点评”这个App,然后 可以设置一下等待条件’等待某个节点加载出来,证明已经进人App’例如设置图l2ˉ80中框选的 部分° _~≡一→一

—=一一

▲■铀呸Ⅵa

』■h

泅『

古∩ +

卜■QT■凹



№唾防闻

£峪





N

■1习

.‖

点[●■≈· ●『甲■

闺旷

■ p■■

∏司■■汐

≈■·∏Ⅷ…

+中



b

禽●■D日



户℃乓←



°·中伙0

簿√瓣′…

■□瓣

■凹′

尹m严

一`喻

←口■■〗‖

O山″■.…·◇O

宁^定

山々

亏三百—烹亏_百雨 _ 恩画

了q■■



尸x 牡色■P■

…·口■



凸■□G■l

…酗〗α…】0q】■0■ 叮■●蝇■咀西蝇仙OpT″□甲0角■p函

● □←山dd…0卢.吁o乙■种■

■〗■=■]|

p■吧诊□■■凶O巳o◎凸←冗叮0 □E

;…

■巴醉军0□个『bq勺〖划『弘t古口□■汐 『…哪巳『c∏P吼

|』■■】‖|』■■■■〗{』■■‖〗|」』■=■{|

■ ▲皿0∩冶00i占亡●吼q

◆…·中U由酌.仑0·■TvO 寸 q产≈≈f尸■ ■□ 出 ■ 『雨日』冗咱户■●1

■Ⅷˉ

宁凸牡】『..】■.幼 0O■■■尸F



■p马口■寸C ●

吕=咐·

●…』 ■

』… ■c

凸≠◆mF叮■哈h■一



p

臣@

β≤牢 q









o n

图l2ˉ80设置等待条件

通过左侧的PocoPause按钮’可以在右侧屏幕上点击想要查看的位置,左侧的UI组件树会自动 定位到对应节点’同时“Log查看窗,会实时显示节点信息°双击左侧UI组件树中定位到的节点, sc∏pt2air脚本中又会增加如下内容 poco("a∩dIojd."1dget儿1∩earLayout||).o仟5pr1∩g(|′a∩drojd:jd/tab们o5t").o仟5pr1∩g("coⅦ.d1a∩p1∩g.γ1;id/1d∩日i∩ —「rag眶∩t")·o仟5pr1∩g("co『∏.dia∩p1∩g.γ1:jd/∏Ej∩1i5tγiew")。o仟5pri∩g("〔oⅧ.dia∩p1∩gv1:1d/∩m陀category-1ayout")

可以看到其中包含-系列链式调用’通过连续调用O仟5pI1∩g方法选取了_层层节点。 提示刚才我们分析API的时候也了解到’0I0bjectproxy对象实现了 日et1te川—、 1ter 、 1e∩ 、 〔hi1d、c‖j1dre∩、 o仟spri∩g等方法’这些方法的返回结果都是0I0bje〔tProxy对 象’所以0I0bje〔tproxy可以实现链式调用°

|」■□]||」==■〗‖‖||』■‖‖则{‖』■■〗‖



| { | ||

poco("0ja∩pi∩g铡)。〔1i〔长()



0





| q

Q

p

{ q





|(

||||}

‖2.7基于Airtest的App爬取实战

585

由于o仟5pIj∩g方法返回的结果依然是0IObje〔tproxy对象’因此可以继续调用相关方法’例如 "日1t—十orˉappeara∩〔e方法’等待结果加载出来’代码改写如下:

po〔o("己∩droid。widget儿1∩ear〔日yol」t").o仟5pri∩g("a∩droid:1d/tabho5t").o仟5pri∩g("〔oⅦ.dia∩p1∩g.v1:1d/1dⅧa1∩ —frag∩e∩t")。o仟5prj∩g("〔o"·dia∩pj∩g·v1:id/Ⅶaj∩115tγiew腻).o仟5prj∩g("co".dia∩pj∩g·v1:1d/h咖ecate8ory-1ay out").wait—+orˉ己ppeam∩〔e(1O)

这里往"ait十orˉappeaIa∩〔e方法的参数中传人了1O,代表最长等待l0秒’如果超出l0秒还没 有加载出结果’就报错。

同理可以选中中间菜单栏的位置,然后向上滑动’代码如下: poco("a∩droid.w1dget.U∩eartayout")。o仟spⅢ1∩g("日∩droid:id/tab∩o5t")°o仟5prj∩g("c咖.d1a∩pj∩g.v1:jd/id晌i∩ˉ千ragⅦ e∩t||)·o仟5prj∩g("〔o『‖.d1a∩pj∩g。γ1:1d/tabˉ1ayo0t||).chi1d("a∩droid.widget儿i∩e日r儿ayout").5Mpe([O’ˉO.1])







这里往5W1pe方法的参数中传人了—个列表,代表滑动方向,列表的第_个元素代表横向偏移量’ 第二个元素代表纵向偏移量,由于我们要向上滑动,因此第一个元素是o’第二个元素取了ˉ0.1° 最后在代码的开头和结尾添加键嚣事件’回到首页,整理代码如下:



p







十ro们a1rtest°core°apimport* 十ro"po〔o·dI1γer5。己∩droid°lnaut咖日tjo∩加portA∩dro1d0jauto帕tjo∩po〔o poco=∧∩dro1d0i日uto"atio∩po〔o(u5e≡airte5t-j∩put=丁rue’ 5cree∩5hotea〔∩act1o∩=「a15e) wF







} p



日uto5etup(—十j1e一) keγeve∩t(‖‖删〔!) poco("0ia∩pj∩g").〔1j〔R() poco("a∩drojd·wjdget.Li∩ear1己yout")。o仟5pri∩g("a∩drojd:jd/t已bho5t"),o仟5pr1∩g("c咖.dia∩p1∩8·γ1:id/id吧1∩ -千rag爬∩t厕)o仟sprj∩g("〔o‖.dia∏pi∩g.γ1:id/们己1∩1i5tγjew闪)°o仟spri∩g("co∏‖.dja∩pi∩g.γ1;jd/h酬记〔ategorγ=1ay α』t").Waitˉ十orˉappeara∩Ce(1O) 仰〔o("a∩dro1d切jdget儿j∩ear儿ayoutn).o仟5pri∩g("a∩dIojd:id/t日bho5t").o仟5pri∩g("c刚.dja∩pj∩g.γ1:jd/id阳j∩ ≡fm8眠∩t").o仟spr1∩g("coⅧ.d1a门pj∩g.γ1:jd/tab-1ayout圃)。〔bi1d〈"a∩drojd."idget。li∩ear1ayout圆).5wiPe([O’ ˉα1]’ duratio∩=1)

keyeγe∩t(|卜Ⅷ[‖)











b卜

卜 }

















运行这段代码’之后手机上的操作和用Airtest实现的一样’都是先进人首页’然后点击大众点评

的图标进人ApP’等待相应内容加载出来’之后向上滑动,最后返回首页。 可以看出, POcO使用起来灵活度更高°可以选定某个Ⅲ节点,然后调用其各种操作方法执行一 些特定的动作,API设计也更灵活’支持链式调用’功能非常强大°

更多API的使用方法可以直接参考官方文档https://poco.readthedocs.jo/zhCN/latest/source/Poco.

proxyhtml’掌握了这些API,就可以更加得心应手地使用Poco控制App操作’做爬虫自然也变得易 如反掌° 7.总结

本节我们讲解了Ajrtest和Poco的基本用法,并用它们体验了一下控制App操作的流程。 本节代码见https://githuhcom/Python3WebSpide∏AirtestT℃st。

↑2.7基干∧|「test的∧pp爬取实战 本节中我们通过实例讲述如何使用Amest爬取—个App° |.准备工作

我们要爬取的示例App依然是app5’因此准备工作请参考l25节° 2.思路分析

由于这里的爬取流程和l25节的—样’因此可以通过对比感受使用Airtest和Appjum的不同。具



{||

第l2章App数据的爬取

586

体的爬取原理这里不再赘述’同样可以参考l25节。下面再总结一次基本的爬取流程。



□遍历首页已有的所有电影条目,依次模拟点击每个电影条目,进人详情页°



□爬取详情页的数据,之后模拟点击回退按钮返回首页。



□当首页已有的电影条目即将爬取完毕时,模拟上拉操作,加载更多数据°

l

□爬取过程中将已经爬取的数据记录下来’以免重复爬取。

|〗

q

□l00条数据全部爬取完毕后’终止爬取。



3.实战爬取

‖‖

请再次确保app5已经正常安装在了Android手机上,并且可以正常启动’然后打开AinestIDE’ 切换到Poco模式’如图l2ˉ8l所示° —β

_

支件

A』『k·$《‖p【γ乱a.9

递特 它



「0Fe恤1中

∩ea◎5m∏

遮啪







■□

巨丙窟=_ˉ产_—万F可—≡宁=盂丁琴刁7更 尺“『…

硒励



…Ⅷ





T 〔』厂Q◇目上0.2PwP面■P』F0i o□;酗肮k 寸 】.臼电·□凸乙吾肾B □;b0.0殉0蛇e■Q飞 ■ 〔【P..γ=吕ef】□ F■0Qb庐.■0β00 0□ p 『仔·尸凹Q‖9邑■.疗‘γ¥P巳T竹出酗?ˉ‖0『

· t丁叮■眉`4r口. ,词.口M倘卜nh‘:o巳

鲤嚣警河

00

■蹿隐嘲

孰$

≈$、`1

O L≈巾哪:乙J00 『.勺□白p炉究碑0名□

伊 『

□■■:0 已十·〃■ 《■··b厂U0圭h‖【u

‖巴沪■i吁°■□■■ ‖句vb闪·尸二心·: □咀 f△智邑□口口月□■■

■ v■≈■吕u0『 □0■

O.马

凹凸

H墅厩



■爵麓橇 ■睬9

‖|‖

P ●

簿翰

日」

00口■面尸O!潞u · 巴『

白」』‖」‖



0 0 已.呜.】.『1J· , ·可P罚『≥T.O □0』

…皿

F□已■F0口0





蕊;



● 〔f丁母f昏f.p.■■已w『@印护日】!c ■



(‖

守4『回·c已w□呜砚L0副Pd『!几γ0凹t

{|

■ c函0g◎M吞.mfUm》□e弓『霄c山目fp爪t仲"t

‖∩■

▲内』rQ0··d』c付刁【亩l

09



T

■∩吐【旧〗

■ [■尸弓Q‖q≈ˉ厅w句飞矾〕【?E』它;M0∩bd『『mJ§



0∩ef◎0◎w〗』G@q.『.aTP0d沁bP

■■





■ 1】YdF●,d★‖屿吨·k0….‖『0&γ∩. 9

蔓贾嚼 …嚣繁碾慧 =震铲



■嗣d『pmⅧGQp〖.『■已吓βm0世■;





……】聪b.〗c瓣扔O川



←启 .·

№d『αd





0唾Tp■

哇亨≤∑空





卜X}

卜I』

■霞私睦

■】■■勺·尸■■可

吼0

凋! ■剐■P .吕叶0◎∏亡羊口飞□夕陀吕Dp飞 0习..8O飞8e叮P V甲

=图乙

||‖

本节中,AjrtestIDE仅仅是辅助我们审查节点属性的,所以界面左侧可以只展示“Poco辅助窗”’

中间栏只保留“Log查看窗”,右侧依旧展示“设备窗”。至于代码,可以在单独的Python文件中编写,

■‖|

图l2ˉ8l做好准备后的AirteStIDE界面

·■可|

岂吕

不_定非要存试里∩

十I咖ajrte5t.〔Ore.apimpOrt* fIo∏]po〔o°dIjγer5°己∩drojd.ui己l』tα∏atio∩j呻ort∧∩droid0jautα旧t1o∩po〔o

po〔o=∧∩dIoid0ial』t咖atio∩Po〔o( u5eajItest-j∏pl」t=「rue’ 5〔Iee∩5hote3〔ha〔tjo∩=「a15e) wj∩d叫width’切j∩d呻_hejght=po〔o.get-5〔ree∩三ize() p∧〔趴C[‖酬[ = 〔咖·go1dze.‖wⅧhabjt|

‖ ( |

首先引人—些必要的库,并初始化_些变量:



丁O「∧[M」"B[R=1" ‖

这里引人了Al!test的API和∧∩dIo1d0jautoⅧat1o∩po〔o类’然后初始化了po〔o对象。接着调用po〔o 对象的get—5〔Iee∩5j∑e方法获取了屏幕的宽高,并分别赋值为wj∩dow"jdth和M∩dowˉ∩e1ght。之后



■尸■『》仁『「β

| l2.7基于Airtest的App爬取实战

定义了两个常量, p∧〔Ⅸ∧C[‖∧‖[代表包名’ 丁0丁∧[‖0‖B[R代表爬取数据的总条数。

接下来就先爬取首页的所有电影数据,用AmestlDE来查看一下节点的属性,选中_个电影条目’ 如图l2ˉ82所示° 够

^『t●$『『D巴门20

乙0旨

文评



587



007e唾$巳 『07e唾M

"●螺‖ "e凸倒5p肌

Bb 氓



p





由凹

行叼



p迂o■■●

^[G「呵



q铂臼■由

〃‖G「◎‖□

曲■■



』■‖丛■厂‖■甘卜卜■厂卜巴尸》■尸□

电□出凹疽●

…‖8∩吁lub幻吕●仆●时

0 2任由导0gw0吕N论?『『己∩0B.d吁【〗帆

〔O硒‘喇d唾°…伪□晒T 凶′…

v □■『(1P『〗0■口肋』“『『〗∏P』l拍Uo凹8

■亏了页≡亏

v丛∩旬『Q0』★0色中□『G▲门宁l△Vou` 矿 0@尸□《h0山0m^∏?h吵〗Ⅶ↑似/江『,O倪bd■『旬! 守

■蜘瓢·

】∏0〗.b晒0d′k「0尸lp耐【 v [OmqO9□f尸″Wmmbttjd′色O郝G邱

寸d旬d了o0q氏0嘲G【`lj沁■r迪洒■《 ● 《∩而,9o↓‘Ze佩…0灶b吵6d′贴0∩h‖

● a耐了Od汕p″厕v′0*刚尸!.∩c Pc◎吼ogo测z智瘫介mmb化灿 P延u师,qo↑q£e吓w冈ha句『【 ‘d

pc白m吵ld么e行″V》‖凸h□Ⅱ 0d 少[o厢qO『d忍eo吓w肮枷0■n.『凶 ◆fowL凹o『□了宙.7吨□"h■b‖『 0α

伊〔◎m咖0d押.∏啊`向Jbt阅』 β屯om咖侧巫加w』Ⅷ0▲M0 伊 々眼冗q∏MJ卧瘫…h●h■t 0‖

卜〔Om.qO↓□rQ…m帕b■∏·d

凸四∏,gO『山e匪响蛔b□【 .d

『止=■「 ▲■ △ 『 ●

■ ‖●『》巳尸△■■厂}▲■『



e P

图12ˉ82选中首页的一个电影条目

从图l2ˉ82可以看到,所选中节点的∩a川e是co肌go1dze."γγ∏‖abjt:jd/jte们’而且不会和其他层 级节点的∩a"e有重复,所以我们可以直接使用∩a"e属性选择节点’实现—个5〔rape-j∩dex方法: oe十5〔Iape-1∩deX(): e1e爬∩t5=pO〔O(千‖{P∧〔Ⅸ∧C[‖州[}:1d/jte"!) e1e爬∩t5.wajtˉ+orˉappe日ra∩ce() retur∩e1e们e∩t5

这里直接将∩aⅦe作为参数传给了POCo对象,并赋值为e1e川e∩t5变量,然后调用它的"ajtfor apPeaIa∩ce方法等待节点加载出来,加载出来后返回。在正常情况下’5〔mPeˉ1∩dex方法可以获得首 页当前呈现的所有电影条目°和l25节一样’我们定义一个Ⅷaj∩方法来调用5〔mpeˉj∩deX方法:

Ⅶ ■ ■ 队 『|||}■厂份||巴尸△■◆伊企『■『‖卜『■

千ro们1oguru1Ⅷpo【t1oggeI de+"a1∩(): e1e眶∩t5≡5〔mpe-i∩dex() 「ore1e贮∩t1∩e1e们e∩t5:

e1e|∏e∩tdata=5cr己pe-detai1(e1e‖e∩t) 1ogger.debu8(千』scmpeddata{e1e阳e∩tdata}‖) 1「

∩aⅦe

== | Ⅶaj∩

0 ;

1∩jtdeγjce("A∩droid")

5top二app(pAαM[‖∧"[) Start-3pp(p∧〔氏M[‖州[) 们日j∩()

任■【尸「仍





在"aj∩方法中’我们首先调用5〔mpeˉ1∩dex方法提取了首页当前已有的所有电影条目’赋值为 e1e∏e∩t5变量°然后就遍历这个变量中的元素’并希望通过—个5〔Iapeˉdet日j1方法爬取每部电影的

他_

|砂



广□



第12章App数据的爬取

588

详情信息’之后输出日志’返回°

这里提到的s〔mpe一detaj1方法也和l25节一样’基本实现思路如下° □模拟点击e1e"e∩t,即首页中的某个电影条目。 □进人电影详情页之后’爬取详情信息° □点击回退按钮返回首页°

在AirtestIDE中’点击首页的任意—个电影条目’进人详情页’查看节点信息,如图l2ˉ83所示。

| 0

A』γY●$i‖匪γ‖20



『↑『古rm护

波Gb

文吓 +



0‖G己d晌"

记骚







包古!

只…≈

刚曲



口 斟径沮

吕c∩句00噬

…邮F]出

A庐°『◎‖u



=》

■乎翱幽

『箍′

…恤…l.贼"№“哟0

怠除 《瓣

=■

■ 巴dT《』『凹缄,w四ge『.『『■me@V可』〖 7Mp"「αd口wle9刨10而■『儿a扣L《 T▲『0巾αdw汕9■【『7■me凹归m

哆‖}

‖■F

户…^陋】

粉叶





0 〔o∏〗.gO咆沁巾釉■h■刁甘k ‖d/亚γm∩b盈77UO盯



l ■

】窍D酗秘

|【|

电儡■■▲

0『b帜

●a∩d№呵‖口/《◎∩Qe丁『

T 〔….喇d三■0…mbOt1d′cm〖e阅《 v 〖O爪啪;□贮丽Ⅶ厅佃ub仙n′口eta‖| ● ■∩o7O灿ˉ山!“■1儿0辟a「[巴沁N』Ⅲ

pⅫⅫ∏Omw灿q侣l.1mea『1…u‖

≈▲Ⅸ俞

v ■啊『O‖αWl“C《[↓∩GMkaⅧ以 C【吓g卜Ur0.们wm掘bB?0d 【l顶qo;d″mw向吨b川心



≡≈山



哦凸启 』 ≡出迅



鸯‖卜

●■

■■

丛洱 α■ c





=』

p

■ =

忿龄



A=≤澳 7钳

灯磊『

嚷鳃°羔=

■ ■→ 『



卜闪

总吉0

_-… =





◎ ■

图l2ˉ83 《霸王别姬》电影的详情页

可以看到整体详情信息的最外侧是∩a们e为co‖.go1dze.∏γⅧ∩abjt:jd/Co∩te∩t的面板.内部是一 个个具体的kxtView,所以这里可以先选定这个面板节点’然后等待其加载’加载出来之后,再依次 选择标题`类别、评分等节点’通过调用attr方法并传人对应的属性名称text’即可获取节点文本。 5crape-deta11方法的实现如下: de+5〔rape-detaj1(e1e爬∩t); e1e爬∩t.〔1i〔k〈〉

pa∩e1=pO〔O(「‖{pAαM[‖州[}:jd/CO∩te∩t‖) pa∩eL"a1tˉ千or-appeara∩〔e() tjt1e≡po〔o(千‖{p∧α∧C[‖州[}:jd/t1t1e0).attr(0text‖)

〔3te8ories=po〔o(十‖{p∧α∧C[‖酬[}:jd/〔ategor1e5γ日1ue‖)·attr(‖text|〉

5core≡poco(+0{p∧α∧C[‖酬[}:1d/5〔oreva1ue‖).attr(!text‖) pub1j5∩edat≡poco(十‖{p∧αM[‖酬[}:id/pub1j5hedatva1ue‖).attr(0text0 ) dm刚a=poco(千0{p∧αAC[‖酬[}:jd/dr3"aγa1ue0).attI(‖text0) 促eyeve∩t(!8Aα』) retur∩{ 0t1t1e! : tjt1e』

|〔ategorje50 : categoI1es』 5〔ore‖: 5〔ore』

|Pub1j5hedat0 : pub1i5hedat’ ‖dIa川a‖ : dra∏日



「「

} ■厂}‖『「||

} 0

|卜

0

l2.7基于Airtest的App爬取实战

589

这里5〔raPy-detaj1方法的e1e"e∩t参数就是某个电影条目’对应_个0IObje〔tProxy对象,调 用其〔11ck方法就会跳转到对应的详情页’然后爬取其中的信息,爬取完毕后调用keyeγe∩t方法并传 人8A〔贝参数返回首页’最后将爬取的信息返回即可。 运行-下代码’结果如下:

[16:53:29][D[B0C]〈31rte5t.core.a∩dro1d°己db〉/u5I/1oca1/日db ˉ5R5〔‖3o【‖O0[5们e11j∩put促eyeve∩tBAα

2O21ˉ02ˉ3O16:53:30。446 | 0〔B0C 川a1∩ 8‖己j∩;』5 ˉ 5〔r己peddat日{‖tit1e0 : |霸王别姬|’ ‖c己tegoI1e5‖: | 剧价、爱价|’ ‖5〔ore‖ : ′9.5‖ ’ !pub1jshedat『 : {1993ˉo7ˉ260 ’ ,draⅦ己‖ : ‖影片借一出《霸王别姬》的京戏,伞扯出

三个人之间一段随时代风云变幻的爱恨悄仇。役′」`楼(张半盘饰)与程蝶衣(张国荣饰)足一对打小一起长大的师兄弟, 两人一个演生,一个饰旦’一向配合天衣元缝,…0}

卜 仁

[16:53:32][D[8{」C]<airte5t。〔ore.a∩drojO°日db〉/u5I/1oca1/adbˉ5R5〔‖3o刚0OL5∩e111∩put代eyeve∩tB∧α

会发现’到目前为止’我们已经可以成功获取首页最开始加载的几条电影信息了。 ■「皿尸卜△广‖|‖厂|卜‖仿|■尸卜协||‖■『[■厂 卜广} =■尸∩‖■「|■■「

■■尸「■【■厂



b

b

4. 』卜拉加载逻辑

现在添加上拉加载逻辑_当爬取的节点对应的电影条目差不多位于页面高度的80%以下时’ 就触发加载。将闸aj∩方法改写如下: de千∩]ai∩(): e1e『∏e∩t5≡5craPe—1∩dex() 十ore1e『爬∩tj∩e1eⅧe∩t5:

’ e1e爬∩tˉy=e1e用e∩t.get-po5itjo∩() j「e1e"e∩t-y>α88 5〔ro11-up() e1e爬∩tdata≡5crapedeta11(e1e川e∩t) 1ogger。debug(+|5〔mpedd日ta{e1e们e∩td■ta}0)

这里调用e1eⅧe∩t的get-po51tjo∩方法获取了当前节点的纵坐标,返回结果是o和1之间的数字’

而非绝对的像素点位置’所以这里可以直接做判断’当返回的数字大于0.8时,就调用5cro11ˉup方 法模拟上拉’以加载新的数据。5〔ro11-up方法的定义如下: de+5〔Io11ˉup(): 5wipe((wi∩do们w1dth*o.5’"i∩dow=‖ejght*o.8)’ vector≡[0’ˉα5]’ dumtio∩≡1)

这里我们直接调用了AjnestAPI里的5Mpe方法,第_个参数是初始点击位置’第二个参数是滑 动方向,第三个参数是滑动时间(试里传人1’代表l秒)。 这样’在爬取过程中就可以自动触发下_页数据的加载了。

β卜

5.去重`终止和保存数据 我们需要额外添加根据标题进行去重和判断终|卜的逻辑’所以在遍历首页中每个电影条目的时候

还需要爬取_下标题’并将其存人ˉ个全局变量中。将"aj∩方法改写如下:



‖|

de十∩aj∩(): w‖j1e1e∩(5cmped-tit1es)〈丁0丁∧[‖0"B[伺: e1e「∏e∩t5≡5〔mpei∩dex(〉 》

位厂|●

+ore1e爬∩tj∩e1e『‖e∩tS:

e1e爬∏ttjt1e=e1e爬∩t.o仟5pri∩g(千‖{p∧α∧C[‖叫[}:id/tvtjt1e!) jf∩Ote1e‖)e∩ttjt1e.exi5t5(): 〔O∩t1∩ue

t1t1e=e1e|Ⅵe∩ttit1e.attr(‖text‖)

1ogger°debug(「‖gett1t1e{tjt1e}0) j十tjt1ej∩5〔raped-tjt1e5: 〔O∩ti∩ue

[‖



■尸尸■厂‖■「巴■「『|■份『‖|厂『■■■

’ e1e「∏e∩t-y=e1e∏‖e∩t.getˉpo5jtio∩() j千e1e∩e∩t-y〉0.7: 5〔ro11ˉup() e1e爬∩td3t日=5〔rape-detai1(e1e爬∩t) 5〔raped_t1t1e5。日ppe∩d(tit1e)

p

第l2章App数据的爬取

泣里我们调用e1e『∏e∩t的o仟5prj∩g方法传人了标题对应的∩a"e,并提取了其内容’然后声明全

局变量5cmped_t1t1e5来存储已经爬取的电影标题°每次爬取之前,先判断tit1e是否已经存在于 5〔raped-t1t1e5中,如果已经存在’就跳过,否则接着爬取’爬取完后将得到的标题存到5craped-tjt1e5 里’这样就实现去重了°另外’我们在刚aj∩方法中添加了wb11e循环,如果爬取的电影条目数尚未达 到目标数量丁0丁∧[‖0‖8[R’就接着爬取’直到爬取完毕。

现在再添加_个保存数据的逻辑,将爬取的数据以JSON形式保存保存到本地的movje文件夹’



‖‖

6.保存数据

||√《】‖‖□】』』■】{‖臼|□

590

相关方法的定义如下: 1川portos

1Ⅷportj5o∩ 00丁p0丁「0[D[偶=加γje|

o5.Path.exj5t5(凹丁p0『「O[D[R) oro5.们冰edjr5(α」『p0『「0l0[【) de十5aγed3ta(e1e爬∏td日ta):

wjt‖ope∩(+|{0U丁pU丁「0lD[∩}/{e1e"e∩tdata。get("tit1e")}.j5o∩ ’ "’e∩codj∩g≡!ut十ˉ8|)a5+: {.write(j5o∩。du们ps(e1e爪e∩tdata’ e∩5urea5〔1i=「己15e’ j∩de∩t=2)) 1oggeI.debug(于‖5aγeda5+i1e{e1e爬∩td日t3。get("tjt1e")}.j5o∩0)

可|‖

最后在Ⅶaj∩方法添加对5aγedata方法的调用逻辑即可。

』■|』■当□可』□

7.运行结果

运行-下最终的Ⅷa1∩方法,控制台会输出如下结果:

2021ˉ02ˉ3017:O5:37.584 | O[B0C Ⅷa1∩ :∏日1∩:67 ˉ8ett1t1e这个杀于不太冷 [17:05:39][0[Bl」C]〈己irte5t.〔ore.a∩dIo1d°己db〉/usr/1oca1/adbˉ5R5〔‖30R晒[ 5‖e11i∩pot代eyeγe∩t8∧α

●●● 〈

韶e

〉 肮咖‖e

霉. 由◇o√

Q

咱嗡锄嚣囊勉墅 7Q房的礼物o』蛔]

冈飞正侍。№∩

闪凡这№∩

Ⅲ·回应jBm

■王闭■冗O∩

±。』m∩



的■侠:曰■历

Ⅷ。l翱∩



本酿闲.巴田膏

{|(||

本地movje文件夹下生成的文件如图l2ˉ84所示°



|‖|』■■■‖■■■■可

两人一个演生’一个饰旦,一向配合天衣无缝,…0} 2021ˉ02ˉ3017:05:〕7.5O3 | D[B0C 爪aj∩ :saγed3ta:26 ˉ 5aγeda5「j1e霸王别姬.j5o∩



||

[17:O5:36][0[8[」C]〈ajrte5t.〔oIe.a∩drojd,3db〉/u5r/1o〔a1/adbˉ5R5〔‖30附OQl5∩e11j∩put代eyeγe∩t8Aα 2021ˉ02ˉ3017:05:37.501 | 0[80C 川a1∩ :们己1∩:74ˉ 5〔rapedd己t己{!t1t1e|:霸王别姬0 ’ !〔ategorje50 ; ‖ 剧忱金悄『 』 ‖5〔ore0 : ‖9.5! ’ 0p0b1i5hed3t! : !1993ˉ07ˉ26‖ ’ |draⅦa‖: ‖彰片借一出《霸王月||姬》的京戏,伞扯出 二个人之间一段随时代风云变幻的爱恨悄仇°役′」、楼(张半盘饰)与租蝶衣(张囚秉饰)是一对打小一起长大的师兄弟,

■ 可 】 ■ ■ ] { | 』

勒嗡蜀魏嗡嗡嗡

·

儡沪人0陋∩

闪击■乐■·』■Q∏酚砸速件小巾0h◎∩巳门的世∏0…向

■不可及°洒们

印乐放只衣的贝 孩0呻∩

啪嗡锄嗡嚣蛹嗡 大阑天■。)9α]

岂申■宗丘∩↓$α〗

■罗空曰lm)

西汀山↓8m

■.问◎价

康.j已◎『T

锄嗡铂绳锄锄嗡 ■牛田的■天j●◎∩飞■环啪记』■α`

风之●.‖m∩

■旺Ⅸ赠人恼n

悦■∏」m∏

见于爪了q肛们

砖尔的■动沮 ■jm佃

图12ˉ84本地生成的JSON文件

至此’我们成功爬取了示例App的所有电影数据’并保存为JSON文件’和l25节的结果是—样的°

|引]|‖‖■』‖||可‖(‖」』|‖]|‖‖√

■光乍定。‖B雨大活■Ⅲ之大…大话西■之月光宝

日‖

唾.户◎拙



妇启快:用■功±

二■]』二■■■

匡■厂‖‖|β■日



l2.8子机群控爬取实战

59l

8.总结

本节介绍了利用Airtest爬取App数据的过程’可以发现和Appium相比,Ajnest的API更加方便 易甩同时使用体验也更好,是实现ApP爬虫的一个不错的选择。

p 巴尸||尸||『|尸

本节代码见https://github.com/Python3WebSpjde∏Ai∏estT℃st°

↑2.8手机群控爬取实战

■尸|·°■『

我们已经学习的使用Airtest爬取App数据的流程,仅限爬取一部手机,如果想同时爬取多部手 机’该怎么办呢?

本节就来探讨一下如何基于AiItest实现手机群控’即同时爬取两部及以上手机的数据° 「|广

↑.准备工作

巴尸|心巴尸

请准备好多部移动设备’真机或模拟器都可以’然后将它们与电脑相连(能通过adb命令访问即 可)。这里我配置了三部手机,运行如下命令可以查看当前的连接状态:

△■『「■■■■

adbdeγ1Ce5

运行结果如下:

》》△●厂‖『|尸

本dae‖∏o∩∩otn』∩∩j∩gj 5tartj∩g∩o"己ttcp;5o37 *d日e|m∩5tarted5u〔〔e55「u11y lj5to十deγ1〔esattached

R5〔‖〔0「9Q[Xdeγi〔e e‖u1atorˉ5554

deγ1〔e

e们‖1atorˉ5556

deγi〔e

如果结果中的第二列显示的不是deγ1ce’请检查手机的配置’例如检查USB调试有没有打开` 手机和电脑有没有正常连接。如果检查完还是没有显示devj〔e,可以重启adb的服务器: 〖=尸‖■■■「·■■■■『■尸■■■=广■■尸■■▲■厂伊『△=尸■■=厂|‖β|「户『||巳『『■

adb促j11ˉ5eIγer

然后重新运行adbdeγjCe5命令°按这个步骤依次检查每部设备’直到电脑可以通过adb命令正 常访问到它们。另外,这里还需要额外安装adbutils库’通过pip3工具安装即可: p1p3i∩5ta11adbqtj15

更详细的安装方式可以参考https://setup.scrape.center/adbutjls。 2.群控

群控其实很简单’说白了就是同时控制,具体到实现上’就是新建多个进程’让它们同时执行同 一个逻辑°第一步’为了能访问到已经连接的多部手机’我们使用adbutils命令替代adb命令: mportadb仙ti15

己db=adbut115.∧db〔11e∩t(∩o5t="127.o.0.1』′’ port=5037) pr1∩t(己db.deγi〔e5())

运行结果如下:

[∧dbOeγjce(5erja1=R5〔‖〔0「9Q[X)’ Adb0evj〔e(5eI1a1≡e刚1atoIˉ5554)’∧dbDeγjce(5er1a1=e爪u1atorˉ5556)]

P

广‖■『■■巳



可以看到返回了-个列表’列表中的每个元素都是AdbDeγj〔e对象’这个Adb0eγj〔e对象包含— 个5eri日1属性,代表设备序列号’这和运行adbdeγCjeS命令获取的结果是一致的° 3.群控实战

仕β尸

为了更加方便地实现群控’建议将l2.7节的实战代码封装成单独的_个类,由这个类维护对应的 deγjce对象和po〔o对象’继而执行一系列操作。下面就新建一个类〔o∩tro11er,并初始化一些内容:



『}快{

||

第l2章App数据的爬取

592

de十

】‖』■

c1as5(o∩tro11er(obje〔t);

j∩1t—(5e1「’ deγj〔e∩a们e’ pac代ageˉ∩aⅧe’ 己pk-path’ ∩eedIej∩5t已11≡「a15e):

‖‖】■■■■■〗‖』■司

5e1「.deγjCe∩a"e=deVjCe∩a|‖e

5e1千·paC炮ge-∩a眠≡paC炮ge-∩a眶 5e1十。ap促=path=己p代-p日t们

|」

5e1十.∩eedIe1∩ta11=∩eedrej∩5ta11

□devj〔e∩己‖e:刚才使用adbdeγi〔e5命令获取的各个设备的序列名称。

□paC代age-∩aⅦe:包名。 □apkˉpat∩:安装包文件的路径’这个参数是为安装所用的’因为很多手机可能没有安装过安装 包,所以用该参数来指定安装包的路径°

□∩eed1∩5ta11:是否需要重装安装包,因为有时候不需要重装’所以预留该参数来控制是否需 要重装。

然后添加_些常用的初始化方法:

q

|||

』{」

对于群控,需要批量实现一些操作,包括初始化设备和安装apk安装包等°所以这里在构造方法 中声明了4个参数°

+ro们airte5t°〔ore.apjmport* 十r咖po〔o·driγer5。a∩drojd·ujauto‖∏己tjo∩1们portA∩drojd0iauto爬tio∩po〔o

de十

‖‖‖|刁

c1己55〔o∩tro11er(object):

i∩1t (5e1十’ deγi〔eur1’ p己ckageˉ∩日|∏e’ ap促ˉp己th’ ∩eedrei∩5t日11=「a15e’∩eedre5tart≡「a15e):

5e1千。deV1Ceur1=deV1〔euIi

5e1十。p3Ckage-∩日『∏e=paC炮ge-∩aⅦe 5e1千.ap找-p己th=ap代-pat∩

de十〔o∩∩e〔tdevj〔e(5e1于):

5e1「.device≡〔o∩∩ectdevj〔e(5e1十.devj〔eurj) de十j∩5t日11ˉapp(5e1千): i+5e1千.dev1〔e。〔he〔|〈—app(5e1十.p己〔腥age-∩aⅧe) a∩d∩ot5e1千。∩eedIej∩ta11: retuI∩

5e1十.devj〔e。u∩i∩5ta11—app(5e1+.pa〔kageˉ∩日"e) 5e1十.deγj〔e.1∩5ta11-日pp(5e1十.己pR-patb) de十5tart-app(5e1十): i千5e1十.∩eedre5tart:

5e1千。deγi〔e.5top-己pp(5e1f°pa〔代己ge-∩己Ⅶe) 5e1「.deγj〔e.5tartˉapp(Se1+。p己c低age-∩a爬) de十1∩itdevi〔e(5e1+): 5e1+·〔o∩∩e〔tdeγj〔e()

5e1+.po〔o=∧∩drojd01autoⅦatjo∩po〔o(5e1十.deγi〔e)

5e1+。"i∩do切"jdt∩’ 5e1千·wj∩do比∩eight≡5e1f.poco.getˉ5〔ree∩5ize()

下面介绍-下添加的几个方法°

□〔o∩∩eCtde`′jce:里面直接调用了AjItest的Co∩∩e〔tdeγ1Ce方法,需要传人一个参数deγ1〔eur1’

会返回-个deγ1ce对象,这里其实是ajrte5t.〔ore.a∩drojd.a∩drojd.∧∩drojd对象’并将该对 象赋值给全局变量devj〔e。

□j∩5ta11ˉapp:里面使用deγjce变量的〔∩ec[app方法检查App有没有安装’使用∩eedre51∩ta1l

方法检查是否需要重装App’只有在App已经安装且不需要重装的时候才不做任何操作°在其

他情况下’都需要重装这个App,先使用u∩1∩5ta11—app方法卸载App’再通过1∩5ta11-app安装°

』|Ⅷ·□寸|‖可」可‖′‖‖‖』‖」‖|』司|■|□(刊]』■■

Se1十。j∩Sta11ˉapp() 5e1十.5tartˉapp()

|■〗』■】■司|■勺■■■司‖‖』■■■口』■■司■■■■■■■■■可』司‖』■Ⅵ』■■

5e1+.∩eedre1∩t日11=∩eedre1∩5↑3]] 5e1+。∩eedre5t日rt=∩eedre5tart

l2.8子机群控爬取实战

593

□5tart一app:里面使用∩eedre5tart判断是否需要重启App’如果需要’就先停止App再启用, 否则直接启用ApP° □1∩1tdevjce:这是一个初始化方法’里面先调用co『〕∩ectdeγ1〔e方法连接了设备’接着将

deγ1〔e对象传给A∩drojd01auto∏at1o∩poco构造了一个po〔o对象’然后获取了-些基础参数’ 例如Wjndow屏幕的宽高,最后调用1∩5ta11ˉapp和5tartˉapp方法完成了App的安装和启动。 到这里,其实我们就能控制手机安装和重启App了。下面继续往〔o∩tro11er类中添加两个方法: c1日55〔o∩tIo11er(object): ‖『[■■「||卜巴『【‖卜【>‖‖|■卜‖【‖》【厂『|卜}■『|七}■『|}『■∏【∏■【≥■『》‖/「卜「伍「》匹■‖『『【厂

de千S〔rO11=up(5e1十): 5e1十.devi〔e。swjpe((5e1+.wi∩dowwjdth *0.5’5e1十。wi∩do"—∩e1ght*o.8)’ (Se1「.倒j∩dOWⅣidt∩ *0。S」 5e1+."1∩do"-height*o。3〉」 dumtio∩≡1) de+r(」∩(5e1十):

「Or

1∩ra∩ge(1O): 5e1+.5〔rO11-up()

添加的方法_个是5〔ro11ˉup’里面调用了devjce对象的5wipe 方法°另一个是ru∩’里面调用 了l0次卜滑操作°下面再实现_个总的调用方法: p∧〔MC[‖酬[= CO".gO1dZe.|WⅧhabjt0 ApⅦp∧「‖= 5〔rapeˉapp5.ap代』 de+ru∩(devi〔eur1): 〔o∩tro11er≡〔o∩tro11er(devi〔euri=deγi〔e l」r1」 p己C阳ge-∩a爬=p∧〔灿C[‖∧例[’ ap代-path=ApRp∧丁‖』 ∩eedre1∩5t己11≡「a15e’ ∩eedreStart=『rue) 〔o∩tro11er.1∩itdevice() CO∩trO11er.n」∩() ●

‖卜

■■「|■β‖|』■尸|||巴■「|「|■■}[伊『「|巴尸■「》)]【■

注意这里的scrapeˉapp5.apk需要下载下来’和当前代码放在同_ 文件夹下’这样在安装apk的时候才能找到对应的安装包°最后完善

厂 〗】司?@

-下群控的调用逻辑即可:

∩a们e

∏己1∩

==

◆ ■ ■

@■

ⅧV

■『市

十ro们刚1t1proce551∩g1们portpIo〔e55 1十



:

pro〔e55es= [] adb二adbuti1s.Adb〔1ie∩t(ho5t≡"127。0.0.1"’ port=5037) 「ordeγice1∩adb.devi〔e5(): devj〔e∩aⅧe≡deγjc巳5eIia1



p≡proce55(target=Iu∩’ aIg5≡[deγI〔eˉurj]) proce55e5.appe∩d(p) p。5taIt() +OⅢpj∩prO〔eS5e5;

p。joj∩()

■『■「‖‖■■『『‖【■『》〗β【β「■「}Ⅲ■厂【『伊|匹「|【●■‖‖『|》「‖『

这里我们就是使用多进程实现了手机群控’一个爬取进程对应_ 个Process进程,声明进程的时候直接指定目标方法为ru∩’参数就设 置为设备的连接字符串’格式为∧∩drojd:///{deγjce∩aⅧe},例如 ∧∩dro1d:///e∏u1atorˉ5554。

在本节的案例中’由于我连接了三部手机’所以就新建了三个进 程,它们同时执行数据爬取操作°运行代码之后’可以发现三部手机 同时运行着爬取流程,如图l2ˉ85所示。

圃蕊·

;

囚愚繁锤 臣蟹戳. ■漂, ■嚣u…旗" 围霖滤脉※ 因嚣谬

g↑

■.8

■宁■●

. 〖〗

刊‖

‘;

夺醚 |疆茧翌

m ‘, ●





■ 图l2ˉ85运行着爬取

流程的手机



第l2章App数据的爬取

4总结

本节介绍了手机群控爬取的简单实现,由于我们使用的是Python脚本,所以可以直接使用多进 程multiprocessjng库中的Process模块为每个爬取进程建立单独的进程,最终成功实现了群控爬取° 本节代码见https:〃github.com/Python3WebSpider/AirtestTest° 5.商业服务

闯』·■‖■=曰||■■■‖‖·Ⅵ‖』=■|‖‖』□]』■■』■呵乙■

594

以上演示的仅是_个简单案例,通过多进程实现群控爬取自然没有问题不过距离真正商业级的

现在市面上的手机群控系统支持同时控制上百部手机长时间稳定运行,并不仅仅满足于爬取_个 简单的项目·手机也几乎都是真机’被统一放置在—个支架上维护起来’如图l2ˉ86所示°

| ‖‖

手机群控还是有~定差距的°



可|』■‖|凸■」■]|々司‖

■‖□·司■可』·』■∏|

关于商业级群控系统’由于其类型五花八门且经常发生变动和更新’故这里不再展开介绍,大家 可以参考https://setupscrape.center/multlˉcontrol了解更多信息。

↑29云手机的使用 云服务器大家肯定不陌生了`有了云服务器’我们可以通过ssh连接云服务器’并可以执行相应 的命令对云服务器进行控制°

云服务器大多是Llnux`WindowsServer系统,这些其实都是电脑,于是有的朋友就会好奇:既

应命令实现需求。

」』■]●日

所谓的云手机就是_种搭建在云服务器上的虚拟手机’云手机的功能和真手机的基本相同,只不 过我们拿不到真机°云手机平台会提供_些控制面板或者API’使我们可以通过操控手机或者执行相

q

司 己 ■ Ⅵ 』 ■ ■ ∏

然有云电脑,那有没有云手机呢?答案当然是有°

■可■□■|■■■可□■■■』■■■】■|□Ⅵ‖■■■|』』■∏』■■可||■■司

图l2ˉ86利用支架维护手机

厂■■□

‖』可‖』■】〗∏|口刘]·

目前的云手机平台还是比较多的,个人比较推荐河马云手机平台(h仗p://www』ongene.comcn/)’

其首页如图l2ˉ87所示°

{·

↑.平台

‖‖



l29云于机的使用

595





(墨》河马云手机





专业群控平台售后有保障

△尸「■■厉}}』

■■■

F

再玛云7肌





它·f….



口□

7寺



…"丙巴



□□∩巴

…"丙■

□声田

卜}



什钒昼云手…控T n过电■璃Ⅲ丝■咙丁上万色云手砸■

《■台煽铲啤回于安申巩饥斗旧似■〉 瞬惠卖时R■门步呻任迎



·

..迅霍▲

p

‖∩

图l2ˉ87河马云手机平台的首页

p

仁‖》▲尸户|▲尸

在这个平台上,我们可以选购相应的云手机并开通相应的服务°购买云手机之后’就可以在河马云 手机平台的网页中控制相应的云手机,其功能非常丰富’包括基础控制、应用管理、IP切换`数据备份` 日志调试、实时直播、adb调试`远程虚拟相机等’平台官网也提供了相应介绍,如图l2ˉ88所示。 P











β『}「|任「》‖‖■「庐「·【■厂〖尸「

云手机基功能

设备大师

P代理

云机■nˉ应用m

女∏仍份、…

-…0■宠匹伍











}|

‖「

多路实时Ⅲ捆

日志词试

玩蒋…翻l

■平白■鸥■扫方芦灭时宜硒用

=匹出丢α出■日正

…、壁…丁■ 甘■

山;

巳Q、』











刃级谰试AD日

文字识别

sD腻对接

■…●由睡字

再■唾≡▲■巴●辟●



≈匡■…■氧工且°砷≡匡西回□.











′ b

图12ˉ88平台官网提供的功能介绍

p













P



下面我就使用河马云手机平台来演示云手机的申请和使用过程° 2.购买云手机

先注册_个河马云手机平台的账号,注册之后登录’然后点击页面右上角的订购人口’即可进人 订购页面,如图l2ˉ89所示°

第l2章App数据的爬取

596





~…≡



_

≡』Ⅱ』’≡

一…

■■ ‖ 护 《

广《【■

~…

{…≡









订■









…_

鲍0

屑它

唾—

硒↑2w

■_■=≡—■■■

四_

—| ■■■■■■

…问:



安但P中甲…本0■尸不可臼仔沁





[雪疆巫忌盂;总盂二豆



∏■『『

「冈「藩!「≡]「獭]「赢志]



「慈蔑盂盂葱盂盖黄蕊了53艺盂ˉ__-

‖‖■||』门

|;-ˉˉˉm禽冶s汀 . 了.$

订瓣■凸



|丘mi毋

→~翱

-—≡■_≡●-→-

订■…区劣

■|』·、||‖‖‖■

…; 〔鲤蹬」囤…」i望…〕 回酗闻哪…闸雹愚氮k. `…獭im徽f瓣豁 开迅回≡茁

□■]|

■■宁—…军-=_→■尸■■≡■←

…■

3。m元

』·■·∏|

支付盏凸

』■■■

……』阔潭…………瓤.不…甲_…

=0■□厂弓…·

‖■=串=■争←-→■□ ‖■■_…≡≈=q■■≥乖

|」■■当■■

图l2ˉ89订购云手机的页面

|·』·

这里有多个套餐供选择—基础版`标准版、高级版和荣耀版,不同套餐对云主机的基本配置也 不同’配置信息包括分辨率`DPI、内存、Oash` cpu和版本号°本节我们是用来测试’先购买“试用

■■{■■■■■

l天的荣耀版”套餐。

3.管理云手机

纠 □ □ 】

支付金额后便可以看到控制台出现了一部云手机’同时菜单栏中显示了很多配置信息’如应用安 装/卸载`云机同步、多路实时直播等’如图l2ˉ90所示。 零紧齿、

垒上蛔则

云机租丑

士幻文件

旧窃H舞

铱励



=〔§…ˉ 气恤蹿唾



』 ■ ■ 司

■_

■■■■

( 垒簿马后手Ⅲl

■ ■ 可



仿垃

应用…

|蔼『日↑|呻咖肚

‖可

| . 奎 通§ 怠 厂【

台●

●■门抄■《Ⅶ





p



^ ■







‖{

■可





≈>曲二



菌…舀

■■□凸‖

图l2ˉ90支付金额后的控制台



‖‖Ⅵ■□Ⅲ

} l29云于机的伎用

597

可以点击该云手机打开控制面板’如图12ˉ9l所示。

在这里’我们可以控制手机屏幕做任何操作,如打开某个APp、下拉查看通知栏等,和使用真机 时的操作基本—致°另外’云手机的右侧有—栏基本控制项’如音量控制、返回首页键、返回上步等。

接下来我们尝试在云手机上装_个APp,在此之前,先对云手机分组,直接在控制面板操作即可’ 如图l2ˉ92所示° 云机{α0∩(‖睡『23小■后剧朋

× ■

憾〃

暇劲押



品:赚





文馋黔助 ■

0≤0l叼

!……喇 【-鸳浮q …(莎副

拍洒 0‖

|■■■■■■■■】



■■

盏翻鲤锥

…0D尸§惫瓣



;,



圈|

e 鹏攒

增》‖云机注;则非必贝,语不负租不阀版卒的云于旧赋圃黔=分组=,丙司撇盏影绚画"贸缓°







°

h u.

Q◇

j



Ⅶ蛔

划ˉ

g §谬

0

■! °』″锋

簿∩争



…———一

m田

妆保■云桃巍用γ咖先颧翁……酮 勤攒共钉】臼五机;…0凸o

图l2ˉ9l

打开云手机的控制面板

图l2ˉ92对云手机分组

分组完成后’点击“应用安装/卸载,按钮来安装App°这里安装我们的示例App’访问https://app5.

scrapecenter下载安装包,安装包保存为scrapeˉapp5apk文件’然后上传这个文件’如图l2ˉ93所示。 -

上传应用试阐用户上传的应用仅保■的矢°■村仓油由助明m

选吨甩!支持盯点蛾侍,肚m选,上传的宦用文件名不蔼色醒格`中文`拯号`田杠导特殊瘫.

囤 c……爆赡……[芍2w

上传应用列寝

_

67%

西………文件大小{427M巳]

≡■比=一-=-~^…

—-=—-—■-洋=~-●===

图l2ˉ93上传下载的安装包

上传之后,即可看到云主机提示‘应用同步中…”’如图l2ˉ94所示。稍等片刻’App就安装好了° 我们可以在云手机上查看其运行效果’如图12ˉ95所示°

第l2章App数据的爬取

598

_—



×司

|今分组剿魔云瓤=-|_Q…望鞭墅ˉ-

=雹…沮…■室■幽■≡照_■

云机‖o目3Q25,23小时后到朋





《》

■上午Ⅵ2:】〗 广■

■羊q‖姐

5

剿钥`爱胡

荣曝版 即





这个杀手不太冷

咖、雕、…



财铂`…

&;“



龋阐



m●

吟q捻B

赣 <





瓣雷眉醒盯

醒厘克号

酮`攀俐`…

矽睁…、Ⅷ翻罐…盎谗



…妖田蝇佰■句

…蘸===瞬、

露8

肖申克…丘



罗马镊日

+=

戊■

闯鸿、



’$·巳ˉ一α筋

赢}

御撼^



’爵

恫洲肥6 ■



—~示=古歹厂叮9.

期镰`■崩`R例 ←

启伯况铡香



窑■、爱俏、占筷

丑册佳人 期箭、繁仿`历鸥 幽牟

…磁霹

■ …

9′ 5,

≡枷

窑■女丰

獭、酮`鳃=ˉ

■门的世界

;|守 ■■~

9. ∩

…$】钉



图l2ˉ94即将安装好App

5

图l2ˉ95示例App的运行效果

至此我们成功实现了在云手机上安装和启动App。 4ˉ高级服务

云手机还有很多高级服务’如切换IP、远程相机、消息转发、ADB调试等,点击“配套服务,, 即可查看’如图l2ˉ96所示。

▲—q全

.………|■蟹警°

·●≡M0





慰留雪尸 @窒舒圆躇

■=≡

=。….~

已…….…

■■厂声司

==…

…[=≡]

.■m厂ˉ百、

‖●■丁■

■m…}

≈□〔=〕. :…日r琶司

-7-—

-

■-←

厂≈=■—

■m》斡●







罩=虱Q黔 翻





≡-0-≈.r=--

■■rˉˉ面ˉ~]

…副

……

■…屯」.几F=■≡



●疆:.

=一≡_——

|…n

◎蜜县 伪…兄吨Ⅷ

由m{-霹司 ′

■罩. 吩==P≡

…『≡声司

≡.ˉˉ-·! …匠=〕

■盟瑟 ■■贷■■●■■吧■下R■守■



…[≡声ˉ可 _■=L≡≈一

0



‖ ‖ 凸

【‖

→洁∑■一

一……………/=

0f=▲≡=…°·、一

叫『『『§ˉ『

巴≡≡■ ■蟹气》{霞墅ˉ

≈……勿■■仆Ⅷ

……

守·厂亩-}

囤…

| =·_ ■

===I二莹嚣

罚≡—-_-_ˉ_.

√●…碑



■■础吼

■□

厅立亏◇…c■



巫曰

曰…

凹刀

月肌

《圣闻弓胃m画ˉ闻. |

.

图l2ˉ96查看云手机的高级服务



‖■日·■■■□□





l2.9云手机的使用

599

点击“云机预览,,→‘批量操作”’会弹出操作菜单’如图l2ˉ97所示°

| .捌" 云机

‖}



上传文件

配 蠢服务

帮助

}辆 [夺分爆…吼■◆……似〔盒遮瑟



离泪版

阻蛔26 ■0



■丁=01酗

=导■

}[》



—$锄·~·…



申ˉ舌

}}



牟上传应用





甲■





■ ■「厂||

》 ■尸 尸沿

■■『尸△■『



图12ˉ97_些基本操作

卜 ‖尸

对于切换Ip服务,选择地域并点击“确定”’即可切换到对应地域的IP,如图l2ˉ98所示° =

■ ■

卜「||仙『Ⅱ卜

卜坠尸‖匹●「‖仙「|)‖■β



切换|p与定位

切换方式:●切换新伊Q绑定当阑‖p 选择地域;

厂己—)「-磊_]厂≡_〗「—茵_]「_≡二可 厂二歹_|厂盂了ˉ可}ˉ毛忘_]「_≡Ⅲ_]{_了霸了] [—而ˉ]}_盂—v「_薄了ˉ]厂赢_}厂亏可=}

[恒

{_盂ˉ—}厂r≡_]厂ˉ盂—]「_盂可「_二石弓rr] 巨 0■州盯

■炽币

连和市

Ⅲ他城市

■州市

■州巾

■钓■市

b

|「 厂-T=子m

0



取筋 !

尸』)『)|》■「|Ⅷ【『‖〖■【『



T■

图l2ˉ98切换新IP

对于云机扫码功能,选择扫码云机,然后在被扫码云机上打开二维码页面’点击“扫码”按钮’ 即可开始扫码’扫码成功后会执行相应的处理流程,如图12ˉ99所示。



■『



』·■■■∏|||旦■■〗‖|‖』■■·ˉ‖』■臼|||』■】‖』■‖」』■■】〗‖』‖|当■】{|』』■‖□』■]】‖|』■习■】|‖‖』■】■‖』』■』■〗」·‖旬·、■■Ⅵ‖】口』■■■‖‖』勺乙■

第l2章App数据的爬取

600



取ˉ

■√



—=

_…

…p〗



■…扫田云扯(Ⅷ云酗巳妄…`Ⅲ付宜廓扫酗幽■■〗

扫码ˉ硒■《以王■趣枷》

甲”

扫码云机

/一停:.≡…ˉ\



·-

…码云机



「≡· 叮

■.

!

『《

擎鳖



●-

●-

■『叮上■鄂乙







吕■钟■出







d〗

d



·

Q







| H



陆粤ˉ□`唾P`9 鱼



ˉ忠锤

向…■晕成酗丢蜒鲤久=…F■

a琅■值°开……

王p…■趣

点违.鳃.按钮′措辩表α卜幼=…

k≈伊



图l2ˉ99云机扫码

q

q

5.∧D日调试



云手机的ADB调试功能需要单独付费购买’购买之后,我们可以点击云机菜单中的“调试ADB”

| q

选项获取ADB远程连接信息’如图12ˉl0O所示°



点击之后’页面会弹出-个包含远程连接IP和远程连接端口的提示框’分别是l8322Ol96.75和 l5998,如图l2ˉl0l所示。

| 甲■鞠蛋



翻3·25



离泊版 远程连接『p





{『8a鲤O‖髓。75



远程连接端口||↑5鳃6









确定 钩

d 引日‖【匹

|调砌oB 调试AoB



§

渺彰和棒留 图l2ˉl00点击“调试ADB,,

图l2ˉ10l

页面提示框





那怎么连接呢?运行adbco∩∩ect命令即可: 己db〔o∩∩e〔t183。22O°196.75;15988

0





|]



0

l29云手机的使用

60l

如果顺利的话’运行结果会包含〔o∩∩e〔tedto183.22o.196.75: 15998,如图l2ˉl02所示。 -__

—≡--■■_-=

●■●

◆≈α创bC◎"∩eCt183 ≈α◎bC◎∩∩eCt183 226·196珍7基;159g8

mb5e「γe「Ve「5i◎∏〔 口b5e「γe「ve「5i◎∏〔qO)◎oeS∏,t啊tc∏t∩i三c1ie"七(41); kiUi∏g

巾口αe『购∩StαFteαS仙〔 口αe『∏◎∩stαFteαsu〔ces5fuUy

C◎∩∩eCtedto183.220 ◎∩∩eCtedto183.2乙0。196°75:1S998 心~

图l2ˉl02连接结果



一不





.●

◆≈αdbco们∩ect183°∑20。196°7B吕15998 Se「VeFγe αdb5e7γeFγe尸$iO∏(咽)创oeS∩0t帕tC‖ db5e7γeFγe尸$io∏(物)创oes∩0t蛔tc‖ thisc1ie∩t(41); kUlt∩g *d口eⅧO∩S七α『ted50CCeSS『uuy d口eⅧO∩S七α『ted50CCeSS『uuy

Co∏∩e〔teGt◎183.220°196.75;15998 o∏∩e〔tedt◎183.220°196.75;15998 ◆

≈α ≈αdbdeviCe5



15 止istof◎eγice白αtmche□ istof◎eγice白αtmche□ 183。220°1g6。73;15”8 83。220°1g6。73;15”8 deγtCe

e闸uI◎toF=55墓4 闸uI◎toF=55墓4

qeγiCe deγiCe “γiCe

e阴ulotO「=5556 ⅧulOto「ˉ5556 二了

||||||

〕 ]β

〗∑

连接成功后’运行adbdevice5命令,返回结果的设备列表中就会包含_部远程的手机,如图

图l2ˉl03返回的设备列表



现在尝试修改-下l27节的内容’将手机的连接信息修改为该远程主机:

「 [)|



p∧α∧C[‖酬[ = ‖〔o肌8o1dze。ⅧγⅧ们ab1t| ∧p民pA丁‖= 0s〔r己peˉapp5°aP捉0

de+ru∩(devj〔e-uri): 〔o∩tro11er=〔o∩tro11er(deγ1〔euIi=deγ1〔euri」 paC阳ge-∩a刚e≡p∧α∧C【‖∧N[’ apR=pat们=ApⅨp∧丁‖’ ∩eedrei∩5ta11=「a15e’

∩eedreStart=丁rue) 〔o∩tro11er.1∩itdeγjce() CO∩tro11eI.ru∩()

i+

∩a爬=



帕1∩

0

:

deγ1〔eur1= 0A∩dro1d://183·22o·196.75:159980

Iu∩(deV1〔e-urj)



』·」■】Ⅵ|」■■■■‖|

运行这段代码后,Ainest进行了-系列初始化操作,在云手机上安装了_些和Airtest相关的App’ 然后示例App自动启动了,运行界面如图12ˉl05所示。 …唾露…:虫蕴患矗ˉ巍捶熟 ■上午嘘:O9

k唾塞″醒≡:二肄;弓



0



感唾色簿,



,

匀=* —







狞〖

′飘γ



ˉ





■蕊陕呻 …=▲ 0■.·

q

V

曰□

鸥勤









。 .. .



bq

叫宁

. .f



■肖嚼警

^ 厂



围爵懒….



参秒巴 0

■爵嚼 ■~■■=●

队α



.

5

8. q







∏·

禹■

隘贸嘿











■ ■ ■■ □

a

8 □

.

图l2ˉ105示例ADp的迄 图l2ˉ105示例App的运行界面

6.总结

‖({



其最终的运行结果和l27节的是相同的’爬虫控制手机点击了_个个电影条目并爬取了每部电影 的详情信息,最后将信息保存下来°

· ‖ 』 □ ‖ 」 ‖ ( | 』 ‖ ■ ■ ■ ■ 』 】 可



■台估

图嚷嚼紧… 田赢n… 一□

图l2ˉl04安装了和Ai肮est相关的App

已’





银色

口台伯

圈骤:唾

】■■■(」‖■■∏】‖‖』·』■可{‖`』■■●∏|||划■■□■γ勺□·■■|尸」日司Ⅵ

m . "±







×

■上 午]2:】2

吃啡啡

《ˉ》



■■|」‖‖」」■】』{ |■■■∏□《]〗■】』|

如图l2ˉl04所示。

』■■■】·‖‖■□■■可|||■·|』■·‖

第l2章App数据的爬取

602

本节中我们了解了云手机的申请和使用方法,云手机和真机没有太大差别’甚至还在真机的基础

上提供了增值服务,这在_定程度上使我们的开发和数据爬取变得更为便利。

‖‖Ⅵ■■□』■日|」‖■■Ⅵ■■■■■■·】□

■‖|」门‖、‖□〗■·司‖】」·门|‖|□‖‖]■Ⅵ

乙■可■■·

□■Ⅵ‖



‖■「||口β||》■尸 ■■『|‖■■|||‖‖|卜厂

第〗3章

∧∩d『o|d逆向

‖尸

■■『‖【巴「■△■‖‖●『◆■尸

在第l2章中’我们了解了爬取App数据的_些基本操作,主要包括抓包原理和使用Applum` Alnest这些自动化工具完成爬取的流程°但很多时候,这些内容并不足以保证我们有效地完成爬取T

作’例如在抓包过程中遇到问题’大家常听说的SSLPinning就可能导致无法正常抓包。另外’使用

Appium`Ainest这些工具爬取数据的效率并不高,因为这些都属于纯UI层的操作’如果爬取过程中 一些点击、滑动等交互比较复杂’那么实现起来就相对困难°除了这些利用Appium、Amest爬取

β『■△■

图片或视频数据时也确实比较麻烦°

|伯卜‖厂

实际上’App中的很多数据是通过接口获取并喧染在App中的°如果我们能直接从根源出发,找 出App构造接口请求的真正逻辑’包括_些加密参数怎么生成、-些风控怎么避免,就可以利用Python

‖「卜户

脚本构造请求或者通过Hook的方式爬取数据了,这样不仅能省去一些基于Appium、Ai门est的UI交 互操作’还能大大提高爬取效率。另外, SSLPinning技术其实是在App内部做了_些限制和校验’ 我们找到根源就可解决其带来的抓包问题°

『‖》}

总之’要想真正地挖掘底层深处的逻辑,就必须学习_些Androjd逆向相关的知识°例如借助jadx`

JEB等工具对apk文件进行反编译,还原Java代码并分析内部的逻辑;借助Hook工具(如Xposed` F∏da)拦截和改写一些关键逻辑’从而截获关键的数据或加密信息;借助反汇编工具(如IDAPro)

■尸

逆向分析`调试、模拟AndroidNative层的实现逻辑’探寻Native层的实现逻辑° 本章就主要介绍Android逆向的相关知识,包括以下内容。

卜 p

p



l

□jadx` JEB等工具的用法’实现apk文件的反编译和代码分析° □Xposed框架、F∏da等工具的用法,Hook或模拟AndroidJava层和Native层的代码。 □如何利用Xposed框架` F∏da工具解决抓包过程中常见的SSLPimmg问题。 □反编译过程中常用的脱壳技术和相关原理°

p

□AndroidNative层so文件的逆向分析方法。

0

□AndroidNative层so文件的模拟调用和常见的数据爬取方法°

0















|3.↑ ]adx的使用 我们知道’每个AndroidApp都有对应的安装包’是以apk为名字后缀的文件’App的实现逻辑 都包含在这个文件中。apk文件往往包含资源文件(如图标、字体等),由Java代码编译而成的dex文

件(可通过反编译dex文件得到Java代码)’和_些相关的配置文件(如AndroidManifestxml文件)。 关于其中的细节,这里就不再_一展开介绍了’如果想了解更多内容可以学习Android开发相关的基 本知识。

逆向中关键的-步就是反编译apk文件’将其还原成可读性高的Java代码,在多数情况下’我们 通过观察并分析这个Java代码就能找到想要的核心逻辑°工欲善其事,必先利其器°用来反编译apk

_

↑3 巴

604

第l3章Androld逆向

文件的工具有很多,例如jadx` JEB、Apktool等,不同工具的用法和定位也有所不同° 本节我们先来了解_下jadx的使用方法°

↑, ]adx的简介

jadx是一款使用广泛的反编译工具,可以—键把apk文件还原成Java代码,使用起来简单’功能 强大,还具有—些附加功能可以辅助代码追查°其GitHub地址为h忱ps://githuhcom/skylot(jadx°主要 具有如下几个功能。

□除了反编译apk文件,还可以反编译jar` class、dex、aar等文件和zlp文件中的Dalvlk字 节码。

□解码AndroidManifest.xml文件和-些来自resourcesarsc中的资源文件°

□_些apk文件在打包过程中增加了Java代码的混淆机制’对比jadx提供反混淆的支持°

jadx本身是一个命令行工具,仅仅通过jadx这个命令就可以反编译—个apk文件°除此之外’它 也有配套的图形界面工具—jadxˉguj,这个使用起来更加方便’能直接以图形界面的方式打开一个

apk文件°同时’jadxˉguj对反编译后得到的Java代码和其他资源文件增加了高亮支持(就像在IDE中 打开这些内容-样),还具有快速定位、引用搜索全文搜索等功能°所以,我们往往直接使用jadxˉgul 完成-些反编译操作°

2准备工作

本节会以-个App为示例,介绍jadx的命令和jadxˉgui的使用方法。在开始之前’请先安装好这 两个工具, jadx的安装方法可以参考h肮ps://setupscrapecenter(jadx°

| ■■■

然后提前下载好示例App对应的apk文件(h肮ps://app5.scrape.center/)’下载下来的文件保存为 scrapeˉapp5°apk。

‖‖

3. ]adx的命令

使用jadx的命令执行文件的反编译操作,主要是指定_些输人参数和输出参数,这些参数的设置 细节直接参考参数说明即可°运行jadXˉ∩命令’查看jadX命令的用法: OptjO∩5: ˉd』 ˉˉoutputˉdjr ˉd5』 ˉˉoutputˉdiIˉ5r〔 ˉdr’ ˉˉoutputˉdjrˉre5 ˉrp

ˉˉ∩o-res

ˉ十’ ˉˉ+a11bac代 ˉ_1o8_1eγe1

ˉoutputd1re〔toIy ˉoutputd1re〔tory「oIsour〔e5 ˉoutputdirectory十oIre5ource5 ˉ do∩otde〔odeIe5o0r〔e5

ˉ们ake5mp1eduⅦp (u5j∩ggotoi∩5teadof 』1+0 ’ {十or‖’ et〔〉 _5et1og1eγe1’γ己1ue5:α」I[丁’ pR卯旺55’[RRO∩′"∧R‖’ I‖「0’ 0[8l」C’

de十己U1t: pRm【[55

ˉγ’ ˉˉγerbo5e ˉq′ ˉˉqujet ˉˉγer5io∩ ˉh’ ˉˉhe1p

ˉ γerbo5eoutput (5et ˉˉ1ogˉ1eγe1toD[80C) ˉtur∩o仟output (5et ˉˉ1ogˉ1eγe1to凶I[「) ˉ pri∩tjadxγer5jo∩ ˉ pri∩tt∩15he1p

可以看到,参数<1∩put十j1e5〉就是输人文件的路径,其他参数如ˉd可以指定反编译后输出文 件的路径, _r可以指定不解析资源文件(能够提升整体反编译的速度)。于是我们可以使用下面的命

令对已经下载好的scrapeˉapp5apk文件进行反编译: j己dx5crapeˉ己pp5.ap促ˉd5crapeˉapp5

运行完毕后’本地会生成一个scrapeˉapp5文件夹’其内容如图l3ˉl所示。

】·Ⅵ‖二■Ⅵ■■‖|■■」|·|《■■■■可·‖■■■]‖||」ˉ■■||■】■□■■】勺|■‖|到■〗|』∏』□‖|」勺』■‖‖‖二■|■』■■

jadx[ˉgu1][optjo∩5]〈j∩put十i1e5〉(·己p促’ .dex’ ·jar’ .〔1己55′ 。s刚a1i’ .2ip’ .a己r’ ·ar5〔’ .aab)



【『▲■【『【卜凸■「|





→ˉ-_≡ˉ,…ˉ≡雹趣≡~ˉ

◇●◇

〉醒馆pe唾app愿 ` . ..

^{酗G丛酗《泊d 寸

…→趴←~β■_

■龄≈F

鳞酗

…庐“盯

γ…酞Ⅺ2畔a

剧=c唾归■

γD钧w拭狸;■a ■′



`N蛔

Ⅶ血γ碱z2叫7

■^硒创d棚·∏腮醚·咖‖ 甜

镭.』≥■酶γ出ˉ0吁

锄v

605

←……←←锅鸭←→丰=PP←■=_→←=~

γ■『·s酗…S



霹√由◎e。 蕊

凶●… ←胎b■…乙L■凸甲▲~■÷

!;

一一一

●●● 〈 !〈滩.

^√

二『■『卜匡■■△∩|■●∩

l3.l jadx的使用

a3瓣隐……Vf

哦 /蕊轴、 Ⅱ咀



∩垒∩ .≡B

‖●『

`′■。找)忧tD3 》H0…翘



沁哟披m鸿3

》■′U$



驴f



庐…

>》

F慰…

…哦狸『口7



庐…

汹臼γ■t2惩Q7

=F醚=

■酗

‖■『■卜

哮锈

、′■四丽 》■蓟°贴zmd

沁q翻寺t鳃蹲6 Yα■翻0t22冯6

:督慰舔

沁…$t忽忽削′

√■加何Z·

__沁…m22:们



■a狗γ■ eh尸归

庐…】

…·

尸…

沁酗歉鲤跑忍

γ"凰

…γ锹鳃:43

堕§… 2卫鲤

…日《鲤鸿s

m…响“捌●呻mmpq…



静≡

…y●t£2:“

…γ■t狸台43

》■e喊γ

沁“y碱潍§鳃

□R…

…y■t22:d3

…碱2憨鳃

■■仪仙『



—→=—

p叮≡==■■石司

由尔杠tW伪y↓…



=≡■々■凸△Y酵U

!ˉ_’■…恤…

沁山γ■`鳃《鳃



cNB

萨剑感翻



…_…_…

■坠β凸■β卜

∧…pp‖c邱m4■m

Tb呻■t鳃哟7

…—…_…ˉ…

翻塑

■△■『

`′■ 印p

!



良 〖

硒■γ碰狸;鳃

…γ酞鲤鸿藤

,

…γ■k22鸿7 …酞m鸿息

‖卜

〉■帕“·c田· 抄■ m仙闷陪 》■…

V…γ碱艺忿q6

…庐…

γb…碱鳃熬惫

沪酬蛔

…酗…

→ˉ 庐喇豺

鳖启

—△

坠 |‖

‖》■‖巴■『■●■卜们●■ 卜

■日■■「卜|}■■■■β■仔■【尸·‖‖■■■∏|匹◆『■「Ⅵ匹尸「▲■■巴尸|匹■

仅》



=庐…

q

涵』 √■ 巾w硕‖…!

图l3ˉ1

反编译生成的文件夹

从中可以看到,AndrojdManifbst.xml文件、资源文件和原始的java文件等都成功还原出来了°例

如comglodzemvvmhabituiMainActivityjava文件的内容还原结果如下: Q

p己〔阳ge〔咖.go1dze·ⅧγⅧ∩abit·lnj

mport日∩drojd.o5.Bl』∩d1ej i刚poIt〔o们·go1dze·们w『∏h己bjt.R; mport〔咖°go1dze°们γⅧ‖abjt·l』i.i∩dex·I∩dex「mg爬∩tj i"port爬·8o1dze·‖∏wⅦhabit°ba5e°8a5e∧ctjγity;

pub1i〔c1a55阳i∩∧ctiγityexte∩d36a5e∧ctiγity〈1’"ai∩γ1e喇ode1〉{ p仙b1ici∩ti∏jt〔o∩te∩tγj印(8Ⅷd1ebu∩d1e){ retur∩R.1己yout·aCtiγity=吧i∩; }

pub1j〔j∩t1∩itγ3riab1eId(){ retuI∩2j



pub1j〔γoidi∩1tp日Ia们(){ 5uper.1∩itpamⅦ()j 5etReque5tedOrie∩tatio∩(ˉ1)β }

pub1icγojdo∩〔reate(8u∩d1ebu∩d1e){ 5uper。o∩〔reate(bu∩d1e)j

■L

■■■·Ⅷ‖|‖γ■■□〗‖{

■■■】‖□■

第l3章Android逆向

606

5tart〔o∩t己i∩er∧〔tiγity(I∩de)(「mg‖e∩t.c1a55.get〔a∩o∩jca1‖aⅦe()〉j



({

} }

可见还原效果还是比较理想的°就这样,我们只用—条简单的命令就完成了对apk文件的反编译, 其中Java代码的逻辑—览无遗°

下面就来了解—下jadxˉgui的用法。 ●启动和反编译

安装jadxˉgui工具后,可以直接使用命令启动它: j己dxˉg‖j

运行该命令后, jadxˉgui便启动了’这时我们看到的界面类似图l3ˉ2所示的这样。 ■≡

…从_

怕■=

←瓣’■·坠每

≡吟岛←

助~…′…

嚷…·』蹿

巫_……

“_岭

………



愈倒…慨…E←『鳃茁C噬 乓……u芭铂■…喊…抽舜p旗■≡■ ■

≡=≡--一≈

=≡==≡…

—ˉ

窗.艘驴 劳吊吕 串

涵用丛……



■■咀■

…~…■■≡一←→■~■=

咖■■≈=^……p

□面

…≡询=■

…≡≈~届陶



…球Ⅲ钮β巷■『哪翱…

图l3ˉ2 」adxˉgul启动后的界面

口」以通辽义仟赌任了」汁不例apk又仟,也口」以直接将apk文件拖到jadxˉgui的窗口中’还可以从 可以通过文件路径打开示例apk文件,也可以直接将apk文件拖到jadxˉgui的窗口中’

当■』■`■』■可□凸尸{】□、||ˉ■{二■■]』旬■】|」=■`』■」■ˉ■]」■|■|‖□』■■】‖■·‘纠■||□」■曰=可|Ⅱ司□】■‖|』=■■司|‖■〗|」■■■】‖。·■||‖』■`■■』■]|」■=■曰」■■■{

jadxˉgui是—个图形界面工具’它就像_个IDE’支持很多方便快捷的交互式操作(例如把_个apk 文件拖到jadxˉgui后,它会直接打开这个文件,之后高亮显示反编译后的代码)’以及代码搜索、定位 等°相比jadx’我个人更推荐直接使用jadxˉguj反编译apk文件。



当■】□‖‖|‖●■■】|」·■〗‖|‖

4.]adxˉgu|的使用方法

菜单栏中的“文件”→‘打开文件,,调出资源管理器来打开apk文件。文件打开之后’稍等片刻’反 编译就完成了,这时看到的界面如图l3ˉ3所示。

刽‖|」]‖|‖』(|‖|‖‖」】

|| 匡■■「|卜}■■厂

l3.l jadx的伎用

607

‖△■

喇mp…■…四

●●● 文件祖■导航工贝■助

■■■■●宁■▲■■■■■■■■■≈←■■■■







■■■■■■■■■户■■■■■■=■-■■--■±■

甄∑]!疆蹿;翱铸罐I熏M蔓膘j;■瓣瓣 皑田C丁……5·已pk ■邱m沮 ■…m1d×

嚣∩函m…止7·■t.mⅡx

,窿l想粥惯;媳,瓣撼;说憋…徊』…′…′,.……,…砸;…`° 7| 四■==”h…吨m〗田mmhve『9』o帘o0E"…「◎』□:t■『味(”凸Ⅵ.尸弘1O…■2铲/≥

■C酗

.■哇

11 】2 M 2〕

.■O№ttp3

24|

。■de↑p■C们蓟e .■1o.「哩Ct1vex

.■o灶O 》■陀t冗↑1t】 田■H文件 凶眶Ⅷ=mP `乙O灿ttp3

■■』■■□■■■■∩

p乙「e$

翌m Cl二醒呸恒止Y ■cl■ 『esm了唾SDa厂$C ■7es

山巫●→即向1■S…硒m【d〗…#■耐rO』d·m徊皿巴L酚.m…丁0/≥ <uze田中■巾1Z■m∩■咽『om导W…●…m1□.碑叮上■∑1m.肛〔匡5…了…=5丁A〗毛倔/≥ 』 ≤mpuCat』吨…「◎1d;!…』●w]″№p丫…"田对「Om:比恍《=以串t厂1吧/a卯~…尸■i ●乙t』v』W硒厂om?…时C四导仰l血●·酌喊▲b1t.叫°m』呻Mv人t丫0≥ <mte肋t=f1lte≈ …b面…「Tm弓…凸晒m1d0m{mt□●ct』m·…/>

25 ■』■』■‖】凸‖■■

△■)■■■『巴尸 | β 『 β 『 } β 『 ■ 尸 ‖ ■ = ‖ 仿

‖押二

.■砷码

军m…叮m0了mc:…00●旧冗1d·1∏t卸t.c■t…巾.L…促R凹/≥

Z7

.

〃皿t航←f』lte■

■q

■3

</■ct』γ1t户

30

≤Ct1v1tγ…怕m8=≈°0olnrG◇…幽鲍1t.m5e.m佃t■』唾『№t1V1t沪〗df沏j0R

〗3

由ct如1tγ匈旬电1o:∩…铲醛.g°出2e°■v吨凶』t·C『醉hDef·u1t[r″,尚乙t1U1tγ"am!

]7‖ 02{

如『w峰厂凶‘如』d:…脚酝.辨Mm□■畸她】T·c≈蜘.〔…】∩1【庐『耐』“F0 凸m厂O1α. 中『酣』“「以m『o1O号∩■馋a必Ⅸ出记皿.●厂ch.11『ecwl●.p了…让1f唾γCl…了】∩皿皿l.

l躺g锚皆°…

≥∧郎5』 《 51g徊tu「e



■■『『‖■■■■厂

}‖β |}

!■

二■厂似△■『△尸

__ˉ~ˉ..….……!ˉˉ;ˉ`:慧ˉ一ˉˉ…ˉ.…ˉˉ…ˉˉ…. ˉ. ˉ 。→…盟:..::二是.二飞飞.~.ˉ::号ˉ二二:自:。.ˉ塞:己二是

1

图l3ˉ3反编译后的界面

■■

从界面的左侧可以发现’反编译后的Java源代码以一个个包的形式组织在-起,另外还有资源文 件,其中包括图片文件、布局文件和AndroidManifestxml文件(内含apk文件的基本信息)等°在左

匹 尸 ’ ■ ■

侧展开想要查看的包’右侧就会出现对应的Java源代码,如图l3ˉ4所示。

▲ ■ 尸

啪■p…坦…″

●●●

≡_■==■■■■■■■■-唁■ ■≈=≡■≡勺_■

■■■■■■=■

腥!『Fˉ ‖『

-=ˉP…仰哩■≈0臀°…铆绚..哨ˉ

●吨…处·



’●∩ D■…1■.9■们】 0■」■…■冗碗·『…,

和■山…… D■m·硒威1哩 )◆哇

0■m诚四

『}

沪挡咖t印3

|:

‖3

′互〗』fQc肛】Gr{ §

厂e『『@°

u…(

! ! |! ■







.

』 』 ≈■■

■≈■-=●_

.ˉ…

.…ˉ。

图l3ˉ4查看Java源代码

■■■■

b

』 !

f″`

_≡■■●■≡■==■

卜「∩|■◆}■

可以看出’Java源代码的还原度还是很高的°

》》;



p控「eT

□.】`;捍=.茅.;估F:。.ˉ o.



■■■■■■

■凸

》纽咏■ⅨP



b■味』D 》■帕t厕↑1也 筋萤m件



▲●■2S ■巳■ ■孕■ …■ 』□■ ■■ ●■ 勺早

b■t爬1h·喊[m“y〔唾

ˉ■‖□凸■■‖〖‖巴■ˉ

■厂‖△『口■『=■尸

D■】~∩阳°tⅧ帕「画

》■tb『…`〗eo……》



】■■· 』 ■ ■● 10

0●腮1∏γq…■〗

.●鲤但鲤1唾…f

■■■■·□■□□

●m1蜘■F【独Ⅱ

u{

■ ■ ■ ■ 』 ■■【〗

】q

■毛∏□〗〗■

p

} 〗γ

■■■尸■■尸日‖■砂『■凸『■凸厂

■ !■ltr…t

●加1选…《】

』‖〗□《■■■门『‖■■□‖』〗巳■巴■‖〗勺‖‖‖旧‖』』‖|‖□〗·■□』〗】凸■】〗〗□□】〗‖〗□‖』‖‖



:邑二驾

≈■■■■■■甲■■■==■中~===裙■■■■

)凸■■尸 巴 ■ 尸 尸 △ 厂

第l3章Android逆向

●保存为Gradle项目

我们也可以把反编译后的文件另存为Gradle项目Gradle项目就是开发版本的Androjd项目, 如 图l3ˉ5所示·

导出后,会发现项目的目录结构如图l3ˉ6所示。 司



●≡● 〈

sαape=app5



^幻改日闸 幻改日朗

名称

bu‖αqg■d‖e

蹿打开文件.… 绳∧dd∩‖e5 保存项目

瞳全部保存

髓5

| 儡另存为C『ad‖e项目蹿| 最近的项目

少菌选项

仑器p

文件央 ˉˉ文件央 →寸耸辜 文件夹 2【B

拿天oo翅`

xMLDocu们α汰

鼠9Ms

文稍

今天0o:30

文件央

》■a∩d『o‖d 》■a∩◎『…x 〉■com

争天0Q3‖

文件夹

舍天0O;3‖

文件史

今天00:3↑

文件央

》■呵p·ck鳃e

今天0oγ3?

文件央

》■℃ )■∏℃ 》■O仅http3 》■o伐‖o

今天O0凶『

文件夹

今天0O3]

文件突

今天00:g↑

文件央

》■眶T沁ˉ‖N「 》■。Ⅸ∩ttp3 》■′·5



_-

3q6字节 ·』av3p『呻e『m{ 」av3p『…『眶s

今天o0;3↑

》■「etrof‖t2

=~—=一一

今天0O:9↑ 今天0O3↑

今矢oO;3‖

文件央

伞天00{鳃

文件央

今天00:3↑

〈|

另存项目为…

56S字节文稿 文稿

锣天0O:30 今天OO:9‖

√■j□Ⅶ

新建项目

种典

大小

今齐OO渤’ 今天0o:3艺

c‖曰sBes·d●N °|……"

Q

今天0O3l 今天00:3l

今天OO;32

■A∩□「o旧Mam↑es1.x肮‖ ■∧∩□「o旧Ma们Ⅳest.x∏}[

器o |

G√ √G√

,

(·‖

|oC已|.Ⅸ印●竹|eS

√■S贮 √■们曰‖∩ √■′帕‖∩

|文件|视图导航工具帮团

票√

≡◇

文件央

今天OO:3]

文件夹

≈=

今天O0,3↑

文件夹

▲萨



图l3=6项目的目录结构

导出后的项目目录结构和我们在jadxˉgul界面里看到的结构基本_致’这个项目是可以被Androjd

|]{|

×退出

图l3ˉ5另存为Gradle项目

■■』■‖』』】■】□‖‖‖|』■■Ⅵ‖』‖‖■■司‖■·]」■■」■□】‖|』■■■■]‖』|』■∏||

608

Studio工具打开的,打开的界面如图l3ˉ7所示。

司」门|







■■





≈■













■■

可』



纠■■‖』‖

孕 越□∩■

」■了|

■■







」■■可

|』□|□

·■■■ ■∩=

■●■







■Ⅶ

■『

Q

f

■■

□飞□g0

. ·吧:》日bp. 』0它吕0 飞 \.P『

..口 ·k蝇魁[呵. ′.u『u√l已6口三







.

]ˉ ·

·0.



飞…

『‖·. . 0‖ .′,蛤" 申0儿. . =『『b厚√‖乙□咕‖』歹… .叫.b ...0‘铲口、0『”‖『P p苫.巳 ?h0P■口□ 巳 巳· □ b·■ □‖ 0 U.LQ哦!』 →纪





↑ 贮

图l3ˉ7在AndroldStudjo工具中打开项目

|■■‖||||■可|‖|」」■司·|ˉ日|』‖·‖‖‖‖』■■‖|‖‖)‖|勺■■|||□■】]‖||叫』■】』‖』∩□■■■‖‖■Ⅷ□■』□』■■‖』■■】』■』·■

□步』





∏二



尸『■炉□

趴飞°









■ ■





l3』 jadx的使用

609

■■■■■■■■司‖』■■■■■■

打开之后的代码_般没法直接运行’因为毕竟整个项目是反编译出来的,我们不太可能完全还原 出开发版本的Android项目。如果你对Android开发比较了解,可以试着修改一下源码和Gradle配置, 是可以使项目正常运行的。即使不能运行也没有关系’因为我们的目的并不是运行这个代码’而是分 析其中的逻辑,所以要把目光聚焦在查找和定位目标方法与逻辑定义上’AndroidStudio能够帮我们 更方便地完成这些操作。

■》

当然, jadxˉgui也提供了查找和定位的相关功能’现在我们回到jadxˉgui,了解_下它的其他常见 ‖■凸‖

‖|

广

巴■「β■β『●『



′ 『■‖[『匹β「

β|仿》「尸β|■「■厂

0

用法。

●丈本技索

在l23节,我们已经分析过本节的示例App’并对其进行了抓包处理’知道了该App在启动阶 段会请求/apⅡmovie这个API获取数据,同时在请求的过程中会带加密参数token,完整的请求URL是 http5://app5.5cr日pe.ce∩ter/apj/∏℃γje/?o仟5et=3081jmt≡108toke∩=‖205‖jγxZj[w‖‖Dy‖D1惯γⅦγγ‖丁Iz2C胀γ‖ZⅦγzdj‖丁 h1γTgⅢ…yWx‖jIx‖x∧ZⅧ∧5油A。

学习本章内容之前,我们只能通过抓包知道最终的tokcn参数取什么值’现在不—样了’我们已

经成功反编译了apk文件,得到了Java源代码’就有办法找出这个token的生成逻辑。可以先寻找— 些突破口’例如搜索固定的字符串,像这里URL中的/apJmovie和token这个字符串都是可以的,因 为在构造URL的时候,它们经常就是写死的常量’如果能找到对应的字符串,就可以顺藤摸瓜找到 token的生成逻辑°

那我们尝试在源代码中搜索_下URL中的/ap〗movie字符串,可以使圈工具 用jadxˉgui提供的搜索功能’打开菜单栏里的“导航’,→“搜索文本”,如 |.燃搜索文本 ■攫索类

图l3ˉ8所示。

~_

这时jadxˉgui会显示一个搜索框,如图l3ˉ9所示。在“搜索文本:,’ 下方填人“/apUmovie,,’同时可以从类名`方法名`变量名和代码中选择

甘‖}β●【■「匹【尸『■【■厂■◆『■■■尸『‖■「|△■■·》|■尸』「兰■■「■【■『『=■■■『凸「住匹■【■『匹■■

搜索位置’自行勾选即可,下方会显示搜索结果° ●

争后退 哼前进

帮助 仑潞「 器N _●

T器←

T能→

.

图l3ˉ8搜索字符串

攫索文本





攫索文本8

/蛔′…■

…冗攫索jz项:= !′团忽赂大小写

.在以下旬世℃迸g ˉ—= ˉ=

田类名o方法名□变世名

r

●代码

宇.ˉ一司

巴亩…钡≡→~…鳃k←…=≡≡唾……、=←=ˉ≡≡ˉ…q←ˉˉ琶勾…←稗…衡寓Q乓马≡勘

!节点

|●“↑p·ck…。1.缅ex{1∩t′mt)№v1唾∩t』w≡

|ode『p·c腐sge。悯

[i3

<→

| =≥

显示了2个结果中的嘛1至冤2个

■■■ (取消

图l3ˉ9设置搜索细节

可以看到’搜索到了两处包含/api/movie字符串的位置’可以依次看一下这两处的内容,先选中 卜『



第_个搜索结果’然后点击‘转到”按钮,即可跳转到对应的代码处,如图l3ˉl0所示°



6l0

《{{

| 第l3章Android逆向 助_~″

卵_蘑

工唾

宇侈2爱蹿



!§. |`伊 = -__■一■ =∏



●吹9≡.·■≥.q×‖

』(

≈≡





田~咏

蜘阉

件‖

●文=…

●祖

.N…p…tˉl酮xˉ9田 =__



tγ月

| 中/ ‖

16



〗7



…』cZtmc19●t】mt酗C·(优№7》{ 』?《b=■tt)《 ■…h…《」.c1■巳9) { ∧7(b=印u) 《 b江扁止(Ⅷv■「)β

】0

20 〗1

》 夕

≡』



} 吨t■加D〗

R5



v■寅H文件

■o付‖ttp3

2且

…〖加$t■t1Cγ□汹仁艺t■γ【∩5t…(){ b工即l1j

29



》酗睦 』 ↑e3t·x田【 圈∧m『o』…↑』

■C1己£写e■啼“x 》■「e$◎U厂CeS.己 厂5C

】】

pm沁■止(仪仪γ■「){

t逊O·a■Ⅻ●厂;

33





尸∧肛51g∩atu「e



“v巳『『1“〃“…贞…□仆

…止酝j扣γ1褪优t1t沪≥1…仪《k∩t』′屿1皿){

O0

∧7「■yk1击tp「厂瑚▲耳t工…▲『「■yk』DR();

Q1

●F…蛙吼喇[铀…〗甘′ 』笋`

Q2 d3

》 儡|引啊吟…′韩 ■■■■■■′■=~≈≡~=~哉…护□喂=司T~ˉ

!…辅啪 于←ˉ=…甲

=肆审尹勺湃啦马F

《,



酶■5丽ah ~—

--_皑=~■=~←—== —~



←~

可以看到,图l3ˉ10中有-个名为1的类’里面有_个j∩de×方法,该方法接收两个参数’分别 是j和j2’目前我们还不知道它们代表什么°

」■■司

含着token的加密逻辑吗?似乎也不好确认°



逆向过程其实就是包含—些不确定性的,在查 刨γe『「1de//咋巾臼Ck日ge.门

3,

脚bUc∑<j酬oγ1e眉∏t1W>>1∩dex《人∏t1p 1n亡12){ ∧厂厂己γL13t■厂「■yL止5t刁∩■∧厂厂己γL15t();

▲0

》l

和追查的过程。

43

■『■yL15t.md(00/巳p刀mv1em》; 5t厂1叼e∩c厂ypt=t.哟c尸霹′二广广■M0 ,0■犹〗〗 「●tu丁∏tMS.a·1∩dex(( }



到声明,,’如图13ˉll所示°

这时就会跳转到声明e∩Crypt方法的位置’如 图l3ˉl2所示°

图l3ˉl2中显示了e∩Crypt方法的源代码’初 步观察其逻辑是传人-个包含字符串的[15t对象’

』e"c 厂γpt)

撤消 无珐沮■

剪切 Ⅲ制 粘贴 删除

全选 折叠

P

敷 图l3ˉll 跳到e∩〔rypt方法的声明处

q

■■■‖{(』‖■■∏‖‖{■】■乙■·可‖`}■]|』』·』■】‖|』■]』■Ⅵ〗〗』■■』■】‖‖■可|』■∏‖』】】■■

我们可以试着寻找_下e∩〔rypt方法的声明, 右击e∩〔Iγpt方法名’会打开—个菜单’选择“跳

1



‖日

是我们想要的’就继续深人研究,这就是—个推敲 ●查找方法的户明

■■■■∏

现在’我们大致了解了整个流程,但对其中的-些参数和调用过程还是一头雾水,难道这里就包

■■司司〗|(‖』』■‖||□□‖

不妨先看-下1∩dex方法的基本逻辑吧°方法内首先构造了-个∧rray[15t对象并赋值给 arr日y[i5t变量’这相当于Python中初始化的空列表;然后调用add方法往army[15t中添加了-个 字符串/apj/"oγ1e;接着调用了_个e∩〔rypt方法,并传人参数army[j5t’通过名字可以大致推测 e∩〔rypt方法实现的是加密过程,加密后的结果被赋值为e∩〔rypt变量;最后调用1∩dex方法本身,并 传人参数1和12的组合计算结果以及刚刚得到的e∩〔rypt变量,并返回最终的结果°

|·ˉ||‖{」勺‖|·】□司|』·

图l3ˉl0第—个搜索结果对应的代码

到_些蛛丝马迹之后,如果不确定查到的内容是不

·|‖

■■■





5《「1』Ⅶ嘲c砸t■t°…「”t《■『…▲5t); 『●tu厂■t灿■°··』…(《1←】》v』2『血’ehc『γpt}〗

`((

3g



|」|

■…Ⅳ「

■■厂匹■β|■尸■■{‖■■ | ■ 伊

} l3.l jadx的使用

6ll

然后经过—些加密处理返回_个高度疑似Base64编码的字符串’而我们之前看到的token字符串的格 式也符合Base64的编码格式’至于这里究竟是不是token的生成过程’我们会在l32节做—步验证’

这里先主要了解jadxˉgui的—些用法。 _翁



_喇



鲤翻

巫_’…

…_…

……_醒

【■厂■卜

……t噎…… _…≡

鞭!〃

β巴尸[尸■■尸

叼sC『……5d印伐 √田x代码

●do,p·ch….n掷 南镭凰≡a其x墅

》■吨「Om D■…吨1血

__=_-_…__~__… —二=_~唾—占~

阴蹿撼′潞′‘蹄熙′…!′……′

=■C唾 》■a7oue$tm·mte厂汕ld

】3

产艘龋}!Ⅱ‖罩墨……… | }.蹦镶瞬{;蹿翻.鹏j』《 〗6

》■…tech,g1蝉 》■‖g0l由e·腑vv晌ab1t{

19



p◆…1G·gm∩

》■j■仪阐记冗m.mh』m1叫 》■[c…Co『e.汰『Pf『E£hu

p■tb…ue口「x庐田』5S



】7



m

】〗



》■t侣uoQ缸ufeCγc〗唾

》●“f…

■■‖

》●m.…t1… 》■崖

p■◎付tt由

■■■■

》■曲加 p 》■≈t7Df1t丑 √■ 贸沮文件

}`

o



鸿…鲤●…5t厂』叼叮t蛇…(叮■l〕刨rr} 《 B7 5t厂1啊Bt「■■p; 灯{叮蚀h 〗吐斤)《 潍 St了1叼…t厅纯■I∩t■卯「◇t…t同叼《b乙…》; ” 』7《…rm哺·1豌恤{〕南】》 {



9t7立m厅◆■铲; 》

■…”

Zt了●Zt「◇陋四tm吨;

‖》

■ON吨p3



但)

n窿

■》巴尸

p

■■■■■■■■■罗β’“■■■■『■■■〖■Ⅱ□Ⅱ|■■■■]

p■■础mm口dSmb1响1四·



圈幻Ⅲ门j鲍m↑est·m1 舌Cm£5哮.由x ■「…』厂唾巴·■丁三C

『忱■7∏就「『 》

o】| ` ′ .……跳■砖……皿■…峙…》恕 』0{

尸哪9…tu『e

Stf」吨T●1…■5t「1闻·v●`…(净丁…9…《5γpt唾.凶厂面t丁…2u血

uZt·…(■■‖…「》β

S·| 田1{

StF』吨…∩c「”t■…rYpt《V酞铡m1S·j°汹《固D凹0 1屿t》》『

鲍!

凤『『…1$t■了丁叼也』■tm…月『厂wl蚂《《);

9】 弘

■「…】■t·…(■…7”t); ■「…im.…(TOl…7》; }

』k一≈ˉ-~

△■■■



…而m忌…·…障t「〗硼(了·m0t』u.j◎m(田0同, ■厂「毗1St).忻t·yt唾《》

$7

~→■二乙==b

奎 ″

ˉ~≡~=一一-一--一=~—…≡ →







≡斋—

△-二□__蒂■==囊蕊ˉ磊盏尸…

β‖

…5喇‖

!



良Ⅶ『■■『巴■庐

图l3ˉl2 e∩〔rypt方法的声明

刚才我们通过“跳到声明,’选项到了声明e∩cryPt方法的位置’那能不能通过该声明,找到调用 e∩Crγpt方法的位置呢?那当然是可以的° ●查找用例

『▲可■■「■■尸●□卜■■厂‖‖

右击声明处的e∩crypt方法名,在打开的菜单中可以看到一个“查找用例,,的选项,如图l3ˉl3 所示° 67

《》》.庐tT…《)/1酶》〗

q9 S0

51

50 凸β





·

57

●■∏

| )

52 53

}}

■尸》「■■■『●‖∩巴β■■『伊「}■巴■【■尸『|伊

图13-13 “查找用例,’选项

点击之后,查找的结果如图13ˉ14所示。

空正





6l2

第l3章Android逆向



■■

$t了绚 ●喊『Ⅶt■t′…r”t{■斤■叭】βt); …』c Ut■t江st厂』叼颧屯咖t《L1$t<t『1…uSt》《

●由7….』·』…(狮t9』∩t》…』唾∩t』t…

d■?…仑R.…「”t《stm…》5t「』吧





=>

皿示了2个■泉中的■1至弟2个

■■■ ▲

取消 丑

图13ˉ14调用e∩〔rypt方法的代码 搜索结果有两处,直接双击结果,或者

刨ve「「』oe/′de巾圈c腮翻gah pobUcz≤j纲oγ1唾∩t1ty>>mde×(mt土,mt边) {

39

:隅鹏.:腑糕′励臆鹏↑『′ayu爵t(|‖ 蕊除鹏:}1丙d爵↑iTpI}°删1if};e∩c叮pt|;

》! 』;

先选中结果再点击“转到,,按钮’都可以跳 转到对应的代码处。我们看第_个结果,可 以发现这就是之前调用e∩Crypt方法的位置,

如图l3ˉl5所示°

图l3ˉl5第—个搜索结果









●反混淆 q

jadxˉgui还有-个强大的功能’就是反混淆°从图l3ˉl6中,我们看到e∩〔rypt方法所在的类是1, 它实现了-个接口∩,仅从这些字母并不好推测究竟是什么意思’这是ApP在编译和打包阶段做了_ 些混淆操作导致的结果,和JavaSc∏pt中的变量混淆非常相似。



·脚ovp沁i·ctˉ』D中泪Ⅲ

●●●

文件视■砸工贝帮助

==_=←-==——==≡—_—-P



--帚■==~__ ——--_===≡-≡===-Tˉ--■=_----二

瓤』咀!呜1簿∧篷翻磁I墅』撮{黔锄息髓,辙 呜占Cr…≤浑5愈apN

k·山…唾·↓ 墨尉

芍妇门代■ ■…厂om 》■咖m1dx

……·佃1d玄e.酌…1t.■t1tγ·№v』E伍∩t1w片

障芹溉翻糕:;…·

■C■

■■fO1】■■十≈°…t●Pm■

■加≈tech△9u0· …m1°…m血啊{ ■goldZe·硒晒`ab1t ■a印

■e∩t1ty



↓掣′…°°′′……咖;“′………″ w

k6″u屯cl■■■1……t■h《 ;严』mt●■t●t』cmm』l·1b; | ■7m■t●R■8

$■u』

「》{

》●mt由』』哗f佣却碑「碱 ′

p●R

■…1c°g■o∩ ■j■…穴回·押hj回l毗 》■1…忙·t仪爬?…‖1|

■tb「叮■ue°「N沁「■』■■ ’■t陀uO.mu?ecyc1e2 ■“?配C…



■血o「囱Ct1γex ■眶

2·′

萨■o脱∩ttp3 卜■冰』◎ ■『et厂O↑1t2

…』C■洒t1〔■◎m由Zt叼【∩$tmm(》 〔 b≥呻u;

29|

铀眶丁卜Ⅸ「

蹈O付『ttp3 ■厂eS

广≈

=净



巳幻mm1…∏1?e三t·四1 〔1·冬仁Pg“x ■〔1·5$唾,“x 厂e5ou『Ce5·a ■「esou『Ce5°a 尸 …51…召tu「e 《51铆巳tu「e

‖β□‖‖启↑‘■『‖↓‖凸卜『『∏■〖『↑『《‖↓』‖‖『卜‖【『【《『■

凹 阳H文件

} 】】

33

】9 △·

41 Q2

p厂1mt●1《h倔γ■「)《 tM3·■■Ⅻ日「; }

酗γ送「71de//唯巾●c….∩ ■l血Z司…』硒t1t辉』…《』■t』d jm』2》 { A厂厂叮u$k·「呵u■t■函Ar「■叭】S『(); ■「「wuZtp…《·/呻』/■γ』■■); 5f‖ˉmg句c咖t■t.…丽t(■厂叼u2t》;



…■涵t吨o■Q』…(《』←1)中皿o皿0mC叮Ot》;

Q3



R■



图l3ˉl6类1和接口h



|′ l3.l jadx的使用



6l3



帮助

器 看

◎儿 能器 飞仑

淆查 混志 反日 圃胸

■厂|≈▲尸‖}||}■『『·■厂

针对这个问题’jadxˉgui具有反混淆功能。我们可以打开反混淆开关,点击菜单中的“工具”→ “反混淆”’如图l3ˉl7所示。

}}

》|

图l3-l7反混淆功能

神奇的事情发生了!原来的类名、接口名都还原出来了’如图l3ˉl8所示° oN●wp扣』“0.‖■dXˉ铆‖

●●●

P

文件视■导航工贝帮助

L≈驾■≡训羽沁

亡晶=^

·【■厂◆「止尸卜■■■■■‖Ⅷ=■「『}■=》「「β巴■厂

P_—ˉˉ-----——…=-盖-ˉ-—-ˉ-ˉ—_一—-_

鹤]ˉE ˉQl蜘鳃颧′‖` 串雌… 也腥区 》■巳砸m』d



γ 士↑

γ





匠∏ 巳

}′



【)

·■



田■卯 ■e∩t1tγ ■咽5u1 @〔077酿

◆■导■■

■9·Mze.厕vv腻巾abit

γ● [] 恤曲 知门

■己砸「o1d口幽t己bj陋1』Ⅷ· 田n】mteCh·gu0e

‖巳◆企●

■■7oue5t己d.蹿te「mM

蔓唾螺1盛w鲤4[pβ野l辩×ˉ ……… 血皿陀

■己∏d「□1dx ■C咖

…………



v四沮代码

●■■矽血■∩=

赚蹿

噶Sc「■胯己pp5.■p促

隆隘瑶,;嚼硷甚卸….`呻…“雕…·…。《 碱m帕■m江池l●t』1·"武晒t■…「ceI印l7田9b〗



■gm9Ie·g5°∩

|兔罐留"瞬撞剐匡…;

蹦蹦』

■o门`ttp3



》■O灶o

17

{Ⅶ

@四t出m“…『哩

18 20

m



■… ■…mo『e己〔t1v唾 〕出田07睡

}|

25

■「et宛f1t2 v酶贸沮文件 褪门巨丁∧~哑尸

2B

{|

29

》跑O灿ttp3 髓厂e已

霞A∩d「c1“aMfeSt°m\

】1

■C记s田e5·de又

33

已7e£oo「Ce5°ar5C

户自咏S1g咱tO「G



{』



〕g 马0





■『‖|卜■■=尸□‖『‖●『{巴■「|‖△■「也■『|■■■】『=■厂伯【▲■■‖||》||坠■厂|『|β‖=■『『‖‖‖■■=■「「||●■=■【『‖‖卜■==■厂‖||||■■∏〖‖「



/本厂e∩…厂厂凹fp■/

图l3ˉl8真实的类名和接口名

代码可读性大大增强!反混淆能够进_步提升代码的还原度’从而让我们更方便地推敲代码中的 逻辑。 _

●设丑选项

↑3

jadxˉgui还提供了很多设置功能’可以点击工具栏中的‘更多设置”按钮’如图l3ˉl9所示°

}史」虫‖ 图l3ˉl9 “更多设置,’按钮

然后会打开—个设置页面’如图l3ˉ20所示°

这其实是一个总的设置页面,我们可以在这里配置jadxˉgui的各个选项,如是否启用反混淆、反

编译过程允许的并行线程数`系统是否区分大小写、是否反编译资源文件等’这些和jadx的一些命令 功能是一致的°



■■■■■■

‖■||□■刀■】】|||』』■■■□□■□|凸=■】●』‖〗『‖己■

第13章Android逆向

6l4



甘nⅧ



反归译

反混刑 凹

强印|■整反混削映射文件

@

H小命名长胚

.….

并行…矽

. = 卫』

擒■

≡3$ 排触的包

■大命名长度

^=

启用反混期

“G 自动进行后台反编译



■示不~数的代码





姨析长°tl{"元数据以获问矣利包名巴

■可‖』]』■□〗‖」』■■】‖』』■‖〗『‖』·■■‖‖‖●』■|· ■ ■ ‖ | 」 √ · | □ 可 」 可 ‖ ‖ 』 】 | ■ γ ‖ 』 □

使用Ⅲ沮名作为类的别名

■命名 `…

系婉s分大小写



昼有效的标识词



■可打印



将0∩‖〔“e宇瞬义 田

岳换耐R

项目

H守字书码访问佬饰符■

自动保拜 ↑雨=田

烟泪困ˉ

妇辑■牢体;佃e∩‖。ˉRegu‖a『田a[∩12

血「au‖t

巴宁=宇■ 其他. ≡





内联■名矣

o

ˉ…ˉo-ˉ

.

语宫

使用j而四犹语句

唾Ⅲ改

中文(■体) ●; 文件及筑区分大$匈 ■

启动时柏■Ⅷ研

■出中臼代日



生…法的C「C■(以ˉom格式保存) 不反妇译Ⅲ三文件

牛血■始的CFC田 ■→宇争蒂=■■

厂Ⅷ

CowtOC‖j…『d





取消

图l3=20设置页面

| (

●日志查看

日志查看器如图l3ˉ22所示,可以通过卜方的选择框选择日志等级’例如这里选择了ERROR级 别,即显示错误日志。

·

在jadxˉgui运行的过程中,还可以查看运行日志’点击下具栏中的“日志,,按钮即可打开日志查 看器’如图l3ˉ21所示。



〈‖|{|





日志■■■





日志唾:

■■旧■■

; ″襄灌

t ■〖】…口co瘫.凹∏皿■·…呵皿·…函【≡「·庐阳1西爪■‖…t…t≡丁·】…;田〕

趾j….c@陀.吹1u口■■m』d.mZ…t“t≡「·…(胎…【c憾t……r.j…8Q3) ··· 〗·气7~尸0O◆回

〔…叮: 』….m.酵[又C印nm8■u 抓]的■·…』…口皿.m血喇t5t…「…mt〔凹t■】…tst….】…g玲7》

■t】…·m≈◎utnS·…mm斡mt●Ⅱ……l…e.…Ⅲm《酝■Ⅲ………te.j…吕6】} 吭】….CO吨·ut1l■·…冗m·…牵t…厂°f』…tc…〔…t西t……「·j…:了4》

‖』

·°·n司7匡二■』tt曰

…:殉1】“t●山……t吐酶…,脾忱吕旧■/d「…le=窝…1≈〃立=t山ˉ』m』■to7ˉm厂LDl….9·肉 j….m忙■吭Ⅱ也口画〔印t』…◇〕…R唾…mm:…tch……e厂■丁 ■t』….Co陀.m』归p…m』n.≈…t回凸t……『.血C唾《肚O华∏■始t……「.j酌●;兜》 mj….m山…忙≈…丁.……(…丁〔=…「.j…:Ⅲ17‖ 扒】…口如』·心Z≈冗犁…fp…t可(…冗■L…「°】m■81≈) m〗….呻』.…『……厅.……画…《………厂·j…弓田)

Ⅵ{|

■tj… ·印』.脏…厂〔=1…7 ■rj….m』.肛…丁……7.…t~(≈………「·j■■:n) ■t 】…·印』·………「.…■tmt(≈…「…l…『.』…吕田》 j…·印』·≈…「〔=止…「 ●t

■tj….坤1◇屿山…1贮□m ■tj….却儿陷山≡ne.…曰t{…=1【e.j…目“』 吭j… ·m定·m1…口睦凶丁■ 吭j….m≈.■l回.……「.m(…∏m…「口j…吕卫) atJ酌■.■悲/j的●.ut』l.c… 己t』…令■酝/』刨●.吭』l.c…『回t·加……tO丁·了……7(沁…l…吐O「°j酗●81267) pt】mp .也2●/』酣■.ut11俞〔m色 ■tj….m乞e/j酣a.ot11铡c…丁呵t钡Ⅶ…眶…to…『悦m.呵‖Ⅶ…1匠…ut@了.』■m2“】)

」■■■】‖‖』‖』■】■|

Utj酣■.蚀T●/〗mp.l呐.肮….m(Ⅶ「…◇〗…g凶》 Utj酣■.蚀T●/〗mp.1呐.沁≈ 〔°≈·凹t』1■□巳Ⅲ…1… 凶Z四坷: 】…·〔°≈·凹t』1■■■】… 1m:〔■t↑m∏』碑饵t<h… ·凹陀·凹t1lB 里m皿· ■tj… Btj…·凹忙,Ut1lB·…m皿· …蛔tc旧t≡∩「』…tc…h(≈…【…t…厂.j…g76》 ■tj… ■tj….四『e°Ut』1【.…m皿.…tC旧t……r.酝悯1…cM匪…t…t……厂口j酣■日6田} ■tj… a[j…·CO≈put』19.…而四◇…t<馅f……「.……《≈…t呸t…厂°jm●日0〕) ·.. ··. 】0厂≡?「-■1咏田 】0

〔卸…叮: d叮: 1…·』O·酣口C印t蛔8吨u j ■tj酣■ ■tj…唾恤锤/j■γ■.1◎.mt■Ⅲ…t5t….厂…】∩t《mt■I呐t5t「….jm■÷弱7》

■ 】 ■

■tj….C@巴.m1l$口…「D灿.mtm…t啤l率te.厂…】爪(mtaX…f比1甲te谬j酣●t61) atj■又.Co匪.ut』1凸.酮呵皿,陛$…t〔hSt≈…c…「.↑1…t[诚…《出$…t〔睡t陀…C…厂.j加■:7』》 ··■ 12厂…f「=■1tt■

雹甄

0

●■



ERROR级别的日志信息

■ =尸q

』■』■■可|‖』■■|‖口





钮 按

士心

凸 △













图13ˉ22

口兰.

{】■■□]|」∩‖‖』』■■】】』■Ⅲ■■■■

l32

JEB的使用

615

如果反编译过程出现了错误,就可以来这里查看错误细节°当然,我们也可以从运行jadxˉguj命 令的控制台观察—些日志输出结果。 5.常见问题

如果有些apk文件比较大, jadxˉgui反编译所需的时间和消耗的资源就会更多,所以有时候在反 编译过程中会提示如下错误: jaγa.1a∩g°0ut0+"e∏℃ry[rror;αoγer∩e己d1mitex〔eeded

atj己dx.core。dex.vjsitor5.b1o〔陋刚己啦eI.81o〔炉ro〔e5sor.〔咖pute0o∏]i∩己tor5(B1o〔|(proce55or。jaγ己:189) atjadx.cor巳dex.γj5itor5.b1oc长5们己ker.61oc代proce55or.proce55B1o〔k5『ree(B1o〔庇proces5or.jaγ日:52) 己tjadx。core.dex.γi5itors.b1o〔促s爬促er。81oc代proce55or.γi5it(B1o〔长proce55or.jaγa:42) 日tjadx.core.dex。γi5ito【5.Depth丁mγer5己1.γj51t(0ept盯r己ver5a1.jav日:27) 己tj3dx.〔oIe.dex.γj5jtor5Depth丁raγer5a1.1a‖Ⅵbda$γi5jt$1(0epth丁raγer5a1.jaγa;14)

这里报了_个0ut0删e『∏ory[rror错误,代表内存溢出,对于_些比较大的apk文件’是会出现这 种问题的’我们可以尝试用如下两个方案解决°

□增加JVM的最大内存:设置JVMOPTS,把ⅣM的最大内存调大,之后内存溢出的问题自 然可以得到有效解决° 丛■■ ■

□减小线程数:线程多了’反编译过程消耗的内存自然也会增多,可以在运行jadx的命令时通 过ˉj命令适当将线程数量设置为更小的值。

■『‖‖■厂■尸〗‖陆止尸【‖■■β◆

详细的设置方法可以参考ht印s://setupscrape.center′jadx,本节不深人讲解° 6总结

本节我们学习了jadx和jadxˉgui的基本使用方法,利用这两个工具,我们可以非常方便地反编译 apk文件’还原出原始的Java代码’从而找到我们想要的核心逻辑。

}}}

本节内容比较基础’需要好好掌握’之后我们会经常使用jadx来反编译apk文件°

↑3。2 」巨B的使用 上一节中我们了解了jadx的基本使用方法,体验了其强大的功能。 当然,类似提供反编译功能的工具还有很多, JEB就是_个°本节我们会结合_个案例学习使用

JEB反编译和分析apk文件的过程。 ↑.」巨B的简介

JEB是由PNF软件(PNFSoRwaIe)机构开发的一款专业的反编译AndmidApp的工具,适用于

}}

逆向和审计工程’功能非常强大°相比jadx, JEB除了支持apk文件的反编译和AndmidApp的动态 调试,还支持ARM、MIPS、AVR、Intelˉx86、WebAsSembly、EtheTeum(以太坊)等程序的反编译、 反汇编、动态调试等。另外, JEB能解析和处理_些PDF文件,是一个极其强大的综合性逆向和审计 工具。

由于本章主要讲Android逆向相关的内容,所以多关注它和AndIoid相关的功能°对于Andmid

A”, JEB主要提供如下功能°

□可以对AndmidApp和Dalvjk(Andmid虚拟机,类似Java中的ⅣM)字节码执行精确和快速 的反编译操作°

□内置的分析模块可以对高度混淆的代码提供虚拟层次化重构,对分析混淆代码很有帮助° □可以对接JEBAPI来执行一些逆向任务,支持用Java和Python编写自动化逆向脚本。

‖‖

』‖|



『■■■■■■■■厂巴

第l3章Android逆向

6l6

』|{

JEBAndroid和JEBPro都是收费的’需要购买许可证才可以使用°

日|

JEB支持Windows、Linux、Mac三大平台’目前主要分为三个版本:JEBCE(社区版)、JEBAndroid (Androld版)` JEBPro(专业版)°JEBCE提供一些基础的功能’如反编译dex文件,反编译和反汇 编I∏telˉx86,但不支持反编译Dalvik字节码°JEBAndrold则更专注于Android系统’支持反编译dex 文件’也支持反编译和反汇编Dalvik字节码。JEBPro则是“完全体,,’支持官网介绍的所有功能。三 个版本的具体功能对比可以参考官网(https://wwwpnfSof↑ware.coⅡmjeb)的介绍°JEBCE是免费的’



2.准备工作

本节我们要使用JEB(JEBAndroid或JEBPro)来反编译和动态调试_个AndroidApp,关于JEB 的下载地址和安装方式可以参考https://setupscrape.center(jeb。

安装好JEB之后’需要下载示例apk文件’地址为https://app5.scrape.cente门’下载好后保存为 scrapeˉapp5apk文件即可。

3.实战

打开JEB’把示例apk文件直接拖到窗口里’经过-段时间的处理’JEB就完成了反编译’如图 l3ˉ23所示。 ● ■

dFB令炉励Ⅷ0啡而Ⅳ江…ˉ…□·唾



■】

『□『了沪□【

矽●酝←…民学

占№c化叼0: c唾,驴M投e·蜘v励呻tt F知p1t■ttm弓 Cm省懒t血e°"wN洒〔t.…°…1iC◎t1m徊蜘…Dl■](∩印…Mmt1匈) 矿〔…诞∩tS: 5αCtwit烛$, 2■ew1〔e3b 〕p『℃吮de厂■,0PeCe1γQ广 $№1∩№tw1tγ;…令卯1醒·°…唾lt°gi.№mA仁twity(啪t叭Ctwtty》 6丁…[ ‖ !№唾…$p·雨[Z田j◎∩吕 §丁帕“[

←赎……=… 融… 砂←

←…■们刃

矽 ■B

甲 p 么

」■曰』·」●』■可』■∏·■】‖」■■■■可尸』■司口■■《·■〗‖可」‖日

然后准备好一部Android手机,真机和模拟器都可以’在手机上安装好刚下载的apk文件并启动 App°另外还需要确保在电脑上能使用adb命令正常连接到手机°



》勘≡

柞脑

旧占辱尸

。c1@$■…lic代『mlR .$u“了凸j●ct



α啃0″f…·m吭二=

助ject.…i∩i◎()γ‖脚

…t∩mh●=di「·ct

■月

…沪e仑uF∩■m(d

·晒≈t…

! | j

`B…尸比j呸t 抄》矽旧山

.i呻]…仓5[x·C几』m7 ‖

.…□t0团3y∑t酶f∩仁tp】i∩帜I“B γ□1哇■∧f前α£k[x江utoF

p{

·0徊…吮l哪 ‖■‘′

申{

■ccQ■5∩吨5■仇且 ∩…■mM

、==●==…0

田…

5γB…8№cO5X渔。16(x唾“)m=α

贷勒≥巳』

p庐呵唾矾陀ct◎叮; /啤P/I“巴1/·tc/j啮/bt∩

‖|

〔u「陀吐毗≈Ctαγ;/山广/l…1/etC/j由

8αB@出P■Ct□叮:/u5「/`“αI/etC/j吨 `当曰一

Q…牺■…舶=ˉ.

γ

■=■■■≡■■■=…■■■■

〗‘

-≡-≡夕=~=■

J旧83·19°』.2哑…71锤〔j■b=p徊)i£zt硫i叼…





_链←≡≡≡——一

°…ottm巳y乏t画〗…吨m巴b

一■|■■■司』■■可』■口』■司|■■

·C1“$伺m1∧厂Ch丁回ek匡xe〔吐旷$1

一………臼~_

■√■√√√

≡◎°…●●◎●●●…°●钞 ≡…≡咆咆咽……心■】》■’》》’》》■》’》

≡…………寸

》》■

‘砖t刚p7Qt”tGcm5t≈Ct◎了<wVtp◎γ .雨9t$te厂$1

图13ˉ23

JEB的界面

我们大体能够看出~些执行逻辑和数据操作的过程°

』□‖■■■■∏|■■∏」●]|‖」■■]■∏|

从左侧的Bytecode/Hierarchy部分可以看到’反编译后的代码以一个个包的形式组织在-起’右 侧显示的则是Sma‖j代码(Dalvjk的反汇编程序实现,类似x86平台下的汇编语言)’通过这个代码,

|〗







}}



l3.2 ■「|色尸[■厂|■「|■■「β|

卜 ■

JEB的使用

6l7

虽然我们得到了Smali代码,但这似乎不是用Java语言编写的,我们从哪个地方人手呢?由于 我们要找的是URL中加密参数token的位置’因此最简单的方式当然是借助API的一些标志字符串查 找了。

我们知道示例App在启动的时候会开始请求数据,请求URL里包含关键字/api/movie’以及 ofTSet、 token等参数,具体的抓包过程这里就不再赘述了’可以参考l23节的内容。

因为API的路径通常是用字符串定义的,而且_般写死在App代码里,所以我们可以尝试使用 /apj/movic来搜索’看看是否能搜索到相关的逻辑。点击菜单中的“Edit,’→“Find”’打开JEB的查 找功能,输人‘./apⅡmovie”’如图13ˉ24所示°

0

》』

点击“Find”按钮’可以看到JEB帮我们找到了对应的Sma‖i代码’如图l3ˉ25所示°

|!:瞬鹏…』雄……….

F‖m《B脓Gcme》

●●● s醒「ch$t『j∏9 卜『巴■巴尸|仆卜『「■厂

卢q

同咽; 霞!-_=≡—■■

雨………..′…哦°·]

} .m…户山Iic曲$t7@仁tt∩“x(1p 【’5t凤闻〕●ae沪…1e

Op"o『bS

O

、臼加mtt刨↑5ySt鄙5lα渺tu厂e v□1匹■{ ·α【冈’

pCaSG琶·饥s『加e瓤Regul恤OXp旧S${o∩ ■W「a口a『oU∩d

■■「●『■■厂●■■|■【■伊‖·【

| ■

P!∩o

圆kj卵g/1…St「i叼;□’ .〕w’

簿∩eγe「SeSea『瞳h

碱MO/「凶Ctiγe羹心Se『…1唾■p

口上C…邯I血e…唾i卸…/3αrm/Ntt碑eS四]Se< 口[…凹M匡e俩…it/mtt可/…ieE砒tty§口0

c|◎3e

—-=≈-=

句≥;≥;蝇

| }

图l3ˉ24在JEB工具中搜索/apⅡmovie

图13ˉ25 /apVmovie对应的Smali代码

这里其实是声明了一个静态不可变的字符串’叫作j∩dexpat∩°但这里是Smali代码’我们如何找 到对应的源码位置呢?可以先选中该字符串’然后右击,在菜单中选择‘‘Decompile”,如图13ˉ26所示。 |||

■【■|「





(‖‖tco"t了o|十S四cet◎b「◎WS·γOL』『|∩put们|D〖◎叮)



。Supe广仙jeCt

。千ie1dpub1iC5t@tiC伺『m1i∩αex″th:StPt∩g■ 凹/呻



°阳et↑]odpub1tCαbSt侨αctimex〔I, I’ 5tPi∩g)肋臼ewo .◎『Wm七□tiO『] Sy二t颐5ig『]α七uFe vα1ue卒[ "(II",

p



画ⅥeWcO航们e∩ts

沸!

图l3-26选择“DeCompile”



之后就成功定位到了Java代码的声明处’如图13ˉ27所示。



■0

|0

碎ck…cm.gOld【e·■v袖■b』tp曲f■·S酗厂℃■·掀句·■e「U1cG;









…忙1◎◇7eⅨt1吨x·…7γ山le;

…穴7试fo↑』ta‖仕p·6盯; …穴「由t冈↑』【∏□∩ttp·o妇叮:

喊ucl吨『0■■顺1…』mw1“{ 酞u厄■t■t1c?1心1St「1叼1唾H拘【h■口′私/…t●■〗

“丁《归1啤■臼/印d/■m●■》凶$●rv山比』n“翼(酗·7v(ml啤■·?0mt■》』■t●「g10徊‖e叮(γ■1四■.u■t■》』m■mr0凹纪叮《m【…t…●》 》

图l3ˉ27声明1∩dexpath字符串的源码

这里就是i∩dexpat∩的原始声明’同时还能看到j∏dex方法的声明,它包含3个参数: o仟5et` 1jmt和toke∩。由此可以发现’这里的参数和声明恰好跟请求URL的格式是相同的°

我们可以在Java代码处再次选择“Decompile”’即可回到对应的Smali代码处,如图13ˉ28所示°

{|||

第l3章Android逆向

6l8

卜硒c硒…堕@鲤ct1呼g凸…咱汹“!、呻l° .画百画运邑i朝】乌y岳i翻5i沪miU7邑 mI晓匈{

口〔n国’

闪儿jαγO/m闻/S七村叼〗口, 口)臼D 曰M″γ面C七w四砸S●尸…`●字0

□[…邯l血……it知m/…Pm/"t七棚e巴四晦e<画, wkC呵″1立●/w呵泣twmtlty…i酝灶ity;α,

}元Ⅲ

→≡丁

I…ˉ

·……0呻

IE

口…imF啦t■味丫

网u°ˉ凰/酗/呻vte.← .……tm

.m画p1

.“,蹦T瞬鉴涩 .……壶6m

·…四「口

·『□司勺■■

·m「■p2 ·…tmP…i≈购7y

m1聪ˉ蹿u口it獭←

·硒…叼

·蛔m… ·m匝p3



°…吐tm「皿ti畦o●Py

v°1ue.趣融颧橱← ·…“… ·…畦…

图13ˉ28

回到Smalj代码

可以看到Smali代码的定义和Java代码的定义__对应°(日这里似乎仅仅定义了API,并没有真

正的实现’因此我们可以接着搜索引用/apⅡmovie的位置’如图13ˉ29所示°

」■■||·‖』■■■]』】‖■‖‖‖■·{|■∏‖‖』可|■】

·……硅dm

{ -" …

√■

√●■←…=

°■……uCt…α’】……由l●

√D……~

,网t…■8

■…

°…m即S…5i…恤「e m1啤■{

…ˉ

●≡■晒

…—

p≥0≥;■

( q

q





αP…叮面订…●协闪二二≡

.[…l立硅it/……c■脸…<·o ·[…邯1……唾tt/mt《如…{哇冈tity;·0



—…

≡…—=



·〔∏)·o

·ktw…ti……αF…l●<·D







催糕~棚贮匡_…=

·蛔…■

……=in″M■

咱·p10 ˉ1

q

_

…i…≈α…t …6t呻ˉl肋蹿征● …l…罕哎匹企k

A「呵u■t≡…i矾◎〔W0嘘 u■tˉD哟凸jmQZD忆’vl ……=…咖t(u乙txtPi昭’呕

……囤lt钩j·亡t叼 …▲ t庐tˉ●j“t 叫p馋,洗…t…“…1ˉ≥印0…iCD:"







…8 t…≡i∏蚀护伪cCmt…i印7v{■~>t…αo I, 5t「t叼…● …【…ˉP●≈【tˉ山j“t叫 …≈…宙曲』mt 鸭



·…… 0q

°cl…№t刷i…1$1

.…炉肋j唾t

图13ˉ29引用/api/movie的位置

{ ‖

| √



胆 ■ 「 ‖ ‖ | ■ ■ 『 | ■ 厂 | ∩ } ■ ■ 巴 ■ 「

} l32

JEB的使用

6l9

同样在查找结果处右击’在打开的菜单中选择“Decompjle,,’就跳转到了对应的Java代码处’如 图l3ˉ30所示°

卜|

肋γe7下i创e〃C咖霉goM王e渗阳V喊吨it,d°蛔.5凶忙G川ttpmmS叫尸ce pl巾1ic恤5ew吨1ei碉e又〔t门t叮g6’i砒α呵乃{ A广尸叮kt芍t$t炉i吨s■0…″庐”li岛t◎; 凸



″j5ewiCeˉifMe×〔〔∩P鳞=1)牢o「g7’ o尸97’【m『vpt°酗c『Ypt〔吐Ft肉gS));

} ‖|『卜

图l3ˉ30引用/apUmovie的Java代码

尸『

很明显’这里就是逻辑实现相关的代码了°稍微读_下这里的Java代码’大致是调用了_个

aPj5erγ1ce对象的i∩dex方法’并传人了几个参数,第一个参数是arg6和arg7计算后的结果,第二 个参数是arg7,第三个参数是e∩cryPt方法返回的结果(这个方法的参数还是_个包含/apymovje字 符串的∧rmy〔15t对象)°

0

卢 | 「 β γ β ■ 『

这里看起来似乎是请求API的一个操作,但是我们也不确定是不是真的是这个位置。为了更好地 确定这里是不是我们想要的数据加载人口’下面尝试使用JEB的动态调试功能验证—下’例如在刚才

的代码位置添加_个断点,然后滑动ApP加载数据,看在运行到断点的位置时是否停止了运行,如果 停止了,就证明我们找的这个位置是正确的’否则继续寻找°

那怎么动态调试呢?其实操作很简单’首先确保本节的示例ApP已经安装在了手机上,并且能在 电脑上通过adb命令与手机连接°然后运行adb命令:

■、甘

adb5‖e11a‖5tartˉ0ˉ∩coⅦ.go1dze°‖wγ∩∩ab1t/°ui。‖己1∩∧ctjγity

这条命令的功能就是让App以调试模式启动, ˉ0指定了App以调式模式启动, ˉ∩指定了启动人 口’这里设置为示例App的包名和MainActivity°运行这条命令后’可以看到手机上显示如图l3ˉ31所

}‖



示的字样°

卜 『 ‖ ■ ■ [ 尸 ■ ■ ■ 尸

Wajt‖∩g『o『Debugge『



哆 g■

∧pp|『cat{o∩∧pp5(p『ocess comgo{dzemγv∩`∩ab|t)|swa|t‖∩g↑o『t门e debugge『toattach



宁 △ 『

血■厅[■尸|‖『△■∏『■尸●■尸∩二■卜□■■尸∩ ■『血『■尸》厂|■Ⅲ 厂‖〖尸|



「oRc巨c[oS[

踞篮…皂}啤…酗摊豁翻Ⅱ龋

辨畔



|蟹



图l3ˉ31 App正在等待Debugger的连接

这时回到JEB的界面’点击工具栏里的“Debug”按钮’如图13ˉ32所示°

图l3ˉ32点击“Debug,,按钮



■】■{(■■‖‖|‖|

·‖|」■

第l3章Android逆向

620



之后会检测出正在运行的Android设备,如图l3ˉ33所示° ∧tt■◎枷t№debugg●「

●钞●

】■|·



‖刘

〗勺‖‖‖』】司‖‖□‖纠□司■■』■可■Ⅵ臼■■‖

点击下方的“Attach”按钮,Debugger就成功挂载了手机上的App进程’JEB的界面变成图l3ˉ34 所示的这样’弹出了几个调试窗口° ·瓣露瞬‘ ; ,′ 』.

创鳞D“,“宫滁≈t…■, ?既腮傲●】土》



…总

泻$…≡=

b 0

L■.伊l■吁j…1$

豢…



…幽? 4…!诫饥■b·Ⅱ】喊ic昧v 】……,0…″ 6心t∩睁↓vi甸;…回戊O审=出〃喊画≈t…恤诫勋蚀《…tivt蛾)

…→

产……■?们】》



婶●~== 尸ˉ

已‖■■

助t丫嗽炽……丑

矽…_…

. . 』



0

口 ‖呻守≈■…浊tm2 ∑γ…

□■‖■■可■■可」■』■‖

图l3ˉ33显示正在运行的Android设备





≡÷兰~ .

… ……

T罕唾…



7

J

.cl酶■…Mc『t心!R



O







0U

…蝉 … …





.唾…庐t…G…公t…t0F<mib(w

》】衫

□…$它●≈1

0出



…m …







啼F…



…7…

V

↑ 凸

…岁…=吨0d 弓 分 讳 坪 伊 迅 『

喇尸午…

p…田…

△《呻l…■… ·…■七1“ZyFc■…〗≈t竿1辑冯 m‖必■ 令

→…←■

.……哇《■

‖‖

鹰……或〔m……J…飞“$

…巳$「1呻■k0 …●酗1l



.……惦t口 □

r糯警宇留=…

田fF 电_—-

…`『……p"一●…皿 吐予砧严唾……铭铲

竹斗乌^·巧

q

=■

· □

□ 0

…解…~…~…. ˉ. 【 1$∩…“产=→le

0锨长酗″i日…伊UBR咱咖啼β6酮:L0,01

≡凶i∏比插哉●T化7呵h砂听…吨m…■馋七恼cm…MU{囱

」□■]|』口√□■

_=~≡一_—_ __一

~肆●●≡●●…●◆●●…

=·

~ ← ≡ _ 心 心 ≡ 心 叼 岳 ·_鼠 》 … 》 》 》 》 》 ’ ■ 哆 ’ ’ ’ 》 ≡ 碌

■|‖

』■□■■]‖』■■〗』■■∏」」■口

≡…箔肆

睦:;



…镶 ■■ ■≡ 妙~ ≡跨 …●… `

·…勘j●值t

β

《七中钩`0”tmM`吧…、啪4■… ■岭∩…“…Ie

( 嘲

■邑

熟炉

图13-34现在的JEB界面

( ‖







l3。2

JEB的使用

62l

与此同时,手机上的‘WajtmgfbrDebugger”提示也消失了,示例App正常运行并加载出了第— 页数据’如图l3ˉ35所示。

这证明挂载成功了。在JEB中,我们可以选中想要调试的Smali代码’然后点击菜单栏中的

“Debugger”→“TDggleBreakpoint,,来添加断点’如图l3ˉ36所示° |

唾谊王别掇

95

赢“阳擒`囊镰

95

. ←颤蔚哦“ 9.5 !靶戴,磁7《

oetac∩ 95

0径Ru∏

←霹

|卧攫{L言志

9.5

’尸||『}■「|β[●[|尸

沮爬「『wj∩ate 95

良eSume丁h『ead

}||

尸[■厂

塑蕊嚼繁 h慰铲. 盅露聪" 魔:蹦… 幽器繁:Ⅸ

Su$pe∏d丁"『ead

■…^

9.5

‘ˉ垒· ■镰、H钥·历史、战争

瓣龋

■「|‖‖卜「|■尸■「|卜■■『[‖「■『『})■「

戮Step|∩tO

闽器盂蒸· ■翻嚼 圆蕊… 闺蕊矗曰`霄殿

锤Stepoγe『

9s



^虚StepO毗

萨费

锄∩u"tou∩e

9ˉ0

酞良

!贮9g!e B『eakpo"》t 9。0

器B



|虹.…「A“l〔}·"/■

例如在刚才的/api/movie对应的Smali代码处添加—个断点’效果见图l3ˉ37° ~』Ⅱ巳丁●

化【■厂■■『}}卜【「‖|[■「|)■尸

图l3ˉ36点击“ToggleBreakpoint,,

图13ˉ35第一页电影数据



p=$

} .e∩dα"∩Otα七iQ∩

铡铡0硼o

αdd=i"t/1it8

7

”鹏0硼4

[■「||〖队[|||}『【‖〖【■『‖‖■『「|}■尸

|.



咖u1ˉi"t

适o"st≡st铲i阀g

鹏00000〔

∩eWˉt∏stα∩Ce mγo揽eˉα讣ect 腮000016 i"voke~i∩teF「αce 硼00001〔 i∩γokeˉstαtic

晌”0010

″伍^^亿’7

m^o′^m^尸0 0V+^k宅^尸+

V0’ p1’ =1 Ⅷ’γ0’ p2

v1, “ ″ˉ Ⅱ露}瓣锄 , | γ2’ ∧「FαyLi5t ∧沪卜回yLiStˉ><i∩it>○γ’ γ2 [ist≡>αdd(0bject)Z, γ2’ γ1 〔门c7yptˉ>e∩c「ypt([ist)5tFi"g』



o夕■

图l3ˉ37添加_个断点

}『『|}【■■「|■「『‖)}●■厂|■

这时再次滑动App触发数据的加载’然后神奇的事情发生了—JEB显示代码执行到断点的位置 时停下来了,如图l3氢38所示°

这说明什么?说明数据加载的过程正是调用这个断点位置处代码的过程’即数据加载人口找对 了。我们可以点击“StepOver’,按钮尝试逐行执行此处的代码’如图l3ˉ39所示。



■』·■■■■■』■司·‖】■■■■‖

622

第13章Android逆向

.●旧…企Tttα〗

.e佣口α「Ⅵ℃tOtiα`

』‖

! 、带

}{;; }…Omdˉi『↑七Jut8

』↑′ !…酗1ˉi砒

……″^■广

1耐□收e=qi厂ect 兰→←白▲0==

召□亡■±~厂←=■

A厂TαyLiBt导><∩i◎◎γ’ γ2 ▲

Ⅶ0 ■勾t回iQ代

…磅t∏z…

v乙0 ▲″◎外t味

ˉ

啄梦Ⅵ赡睡嘲‘…茧p.-哼慰慈瓣馏』 `` . ., L{匡t萨…(锄j邮t)Z,泥’V】

…16 t…k●.m远厂化Ce

γ2,∧F庐αyli5t FI=』

蝇D p10 =1 蜘0咐’匪

……tˉ3tFi咱



vQv0, pE

… 帅e讶ˉ`∩5七□∩Ce

…10

叼’p1’ ≡1

…哟=t门t/u七8 …凶【~t吭

≡』▲尸∩陛当==』、●

= □■

■■司』■‖·』口《』已司

但L…如〖Om…{@■*1ty/m《褪"仑\ty;■‖ )炉;俱 }

←1〔 i…‖■ˉ钝试tc

怎∩仁7”tˉ≥…P”t〔{↑与k)$t「`吨’丫R

…2…ˉ帕$u1t=的j●ct

v3

…Q t0etˉ呻ject …28mmh●≈t问切P「◎cQ

叫$馋0肚…仑□“u7唾】呻lˉ>四i5·′vt“:…ic …i●即i5●向t仁Oˉ≥ifⅧ●x《I’ I,3仑广i吗汹“门让

图13ˉ39逐行执行数据加载人口处的代码

图13ˉ38执行至断点位置

在执行的过程中’我们可以观察“VM/Locals”窗口’这里显示了各个变量的类型和对应的值, 如图13ˉ40所示°

4

垫理2O?

DT"∩口

棚儡`·

St"『珊

6

】【

6

; ! P

0o l↑

雹. 呼刚!儿CAp№汀M耐 ■Z●



呼■α帕Ⅳ彻幽喊」『烟0s

2 』

撵77a 二2097865879

{ "3

sM吨

『)γ马

』L饱UU

…2卯

匹喇

…385



l’v6《pO》 『

蝎仙↑》

|…》



醒弘52阳‖‖z280岔↑89

ht

■□…$=…↓赋



以】勤∧呕脆∑胁Z‖0h佣◎z硕z…剐Zγ↑"■冈M酬{

;赋

2

}赋

‖0

图13ˉ40 “VM/Locals’’窗口中的内容

|』‖』

@…◎师

〉 ■■hm刚$上捶s6L扣Ⅷ

』!

…飞



{}



3

f

=…

憾雾 刚哪刚椰哪铡哪酗呻呻∑沮



;!

嗽二

↑o

日t『j吨

√■呵

审惶 裂【





凸『



lⅦh』● 划塞6a35

憋 { ! i『



~—

!Ⅳ■

!|了矗



另外,可以点击工具栏中的“Run,,按钮继续执行到下一个断点,如图13ˉ4l所示°





翠晨≡≡…了

如果没有下—个断点’会直接完成数据请求’APp中加载出下_页数据。

『‖□

图l3ˉ4l “Run”按钮



(‖|

| l3.2





JEB的使用

623

上■「||■■■『|■|■■□【■尸「

经过多次的数据加载和调试,以及观察对比‘‘VM/Locals’,窗口中的各个变量(如果有必要,还

可以用抓包软件验证)’最终不难发现,变量γ2就对应Java源代码里面的∧rraγ[15t对象’γ0对应 ofrSet参数的值, v7对应llmjt参数的值, γ3对应GET请求过程中的token参数°

■广‖卜「|■■「|)■‖

另外’可以推测γ3,即token参数的值就是刚才图l330中e∩crγpt方法返回的结果’也就是token 字符串的生成逻辑包含在这个e∩〔Iypt方法里°

我们先详细看看e∩〔Iypt方法是怎么定义的’再单独对这个方法进行反编译操作,如图l3ˉ42 所示°

二■■尸

… O总d-mt^让8

}…q 帝|……8

}擞

b广

p

∩eW℃i门Stm〔e

曲’p1, =1 咐p γap2 γ1’ 鳞/却i/…te. V2,AF『瞳匈u5k

t∏V◎ke=di『qeC七

Ar徊yLt巴t-≥<t∩`t>◎v’ γ∑

mvoke旱i∩te厂fαce

ktS七=≥□dd(比ject)Z》γz’ γ1

枷』I~i∏t

CO∩stˉ吐「i吨

『‖|‖′◆「

…硼 拇霹钩* 儡沪 .…蹲叫 }

γ

翼》 ′』



「「

卜‖「▲■■矽厂|广

图l3ˉ42反编译e∩crγpt方法

找到对应的e∩〔Iypt方法后,再定位到Java代码中它的声明处’如图l3ˉ43所示° 阻由M仁Smtic§t「i叼瓢c7ypt〈M$tα厂日7〕[

St碱吨o囤1■5tPt四.γαIgeOfo$w丁t腮岛t…《约吐铀°m「Fe"t7飞巾甜i1Ms◎)0ge七Ti础o/1网); αFg7·□dd(口7g1);

}卜

趾减吨$i硕■【∩C‖如t。巴№[∩CJvpt(丫鳃t毗11$·j◎i∏(凰,口’α厂g了)》; ″沪αyM毗t…■『蹿幻厂叮m£t◎F ….四d〔巴i卯); t…°md(α呵1)i

喊uF"购£e60.●加四●丁◎5tPmg(了●xtUt↑i色.jom(口0口》 t…).蜘t8yte5◎’0); }



p山uc5mtic叁tFmgs‖m旧∏cFypt(St戒吨鸵P5PC){



坷te□吐由St「S「C.眠tByte5◎; tTy{

l

滩Bm9冬Di卯∑七咆■腮琶$嚼ep《瞬$tGe七I∩■也∩Ce(口5"A肆1睹)》 咱嗡up山t●《ht〕;

庐e恤m〔"c0”t窜byt●5酬e又〔咐.dig·zt〔)〕; }

)}

比巳■|‖|‖■■‖||■∏



唾tctX蜘酗侣M1酗Pit峰赃e休i咖e){ 厂etomf凹u;

} ]

图l3斗3声明e∩〔rγpt方法的代码

0 ■尸「匹尸「|儿■『|‖■尸『巴■厂

其实很明显了,我们分析_下这段Java代码,传人e∩〔rγpt方法的参数是arg7,经过刚才的分析

可知’arg7其实是一个长度为l的列表’其内容是["/apj/‖oγ1e"]。方法中先定义了一个叫作arg1的 字符串’其实就是获取的时间戳信息;然后把arg1添加到aI87中’现在arg7里就有两个内容了,一

个是字符串/ap1/‖Oγ1e,-个是时间戳信息;接着声明了5ig∩变量’可以看出其是用逗号把aIg7中 的两个内容拼接在_起,外层再调用5ha[∩crypt方法的结果(经过观察’5ha[∩crγpt方法其实实现的

就是S‖∧1算法);后面又声明了-个∧Iray[j5t对象’赋值给te"P变量’并把5jg∩和arg1的值添加 进去,再把te"p中的内容使用逗号拼接起来’最后进行Base64编码及返回。



那么现在token字符串的整体加密逻辑就清楚了° =■■‖‖■■■尸~=■『|■「卜「』■「|卜=「|炉■



4.模拟

了解了基本的算法流程后’我们可以用Python代码实现这个流程:



』口□】■■司■■叮|‖■■

第l3章Android逆向

624

1ⅧporthaS∩1ib i帅orttme 加portbase64 1ⅧportIeque5t5

[1‖I丁=1O

』|‖

I‖D〔X0【儿≡ ‖httpS://app5.5〔rape·〔e∩ter/己Pj/爪Oγie?1j『‖jt≡{11mt}&O仟5et={O仟5et}8tO戏e∩={tOke∩} ∩∧XP∧C[ =1O

de+get_to代e∩(日rg5): tme5ta爪p=5tr(1∩t(t加e.t加e())) arg5。appe∩d(t1眠sta田P) 5jg∩=hash1ib.5ha1(|’ 0 .joj∩(日rg5).e∩code(‖(」t+ˉ8‖))。hexd1ge5t() Ietur∩ba5e64·b64e∩code(! ’ ! ·joi∩([5ig∩’ tj们e5ta们p]).e‖code(‖ut十ˉ8‖〉)。de〔ode(’ut十ˉ80) 于orii∩r日∩ge(灿X≡p∧C[):

i∩dexuIf1≡I‖0[X0RL.+omlat(1加jt≡[I‖I『’ o仟set≡o仟5et′to代e∩=to代e∩) re5po∩5e=req0e5t5.get(i∩dex-ur1) prj∩t(|re5po∩5e ’ re5po旧5e.j5o∩(〉)

这里最关键的就是token字符串的生成过程’我们定义了—个get-to代e∩方法来实现,整体思路 就县k面梳理的内容:

□在列表中加人当前时间戳;

□将列表内容用逗号拼接起来;

‖‖■』■」□回■■||』■|■■‖|■∏|』■■』□·■

O仟5et二i*lI‖I丁

to促e∩≡get≡toⅨe∩(arg5=[‖/己Pj/|∏oγie! ])

□对拼接结果进行SHAl编码;

□将编码结果和时间戳再次拼接; □对拼接后的结果进行Base64编码° 最后的运行结果如下: V

re5po∩5e{0〔ou∩t{ : 1O0’ {Ie5u1t5』: [{‖id』: 1’ ‖∩a川e| : 』霸王别姬0 ’ 0a1ja5‖ : 0「aIeⅣe11‖y〔o∩〔ubi∩e|’ 0〔oγer0 :

』∩ttp5://pO。"eitua∩.∩et/Ⅷoγ1e/〔e4da3eO3e655b5b88ed31b5cd7896〔十62472·jpg@464"ˉ644h_1eˉ1〔‖’ ‖c日te8oI1e50 : [』 剧悄{ ’ "爱怕‖]’ ‖pub1i5hedat0 : 01993ˉo7ˉ26‖ ’ ‖们i∩ute′: 171’ ‖5core, : 9.5’ ‖regio∩50 : [ !中国大陆』」 ,中国奋 港‖]’ ‖dm爪a|; p





{0jd』: 10’ ‖∩日爬| 8瞬于王0 ’ !a1j己5|: |丁he[io∩佣1∩g! ’ !〔over′: ‖http5://pO.『∏ejtu己∩.∩et/‖oγie/ 27b76fe6〔十3903+3d74963+7o7860o1e1438qo6.jpg@464wˉ644∩ˉ1e—1〔!′ 0〔ategor1e5‖: [ 0动画0 ’歌舞0 ’ 0冒险』]’ !p0b1i5∩edat|: |1995ˉ07ˉ15‖’ ‖m∩ute0 : 89’ |5〔ore‖: 9.0’ 0reg1o∩s|自 [‖吴国0 ]》 0dm阳a|; ‖辛已是荣耀圆的小 王子’他的父亲木法沙是一个威尸的国王°然而叔叔刀疤却对木法沙的王位舰饥已久.妥想坐上王位索座’ ■



0

』 Qq



】 q





这样我们就成功爬取到示例APp的数据了。 5.总结

本节我们通过-个案例讲解了比较基本的App逆向过程’包括JEB工具的使用方法`动态调试 和代码追踪操作等,还通过分析代码厘清了基本逻辑’最后模拟实现了API的参数构造和请求发送,

当然本节介绍的内容仅是JEB所有功能的冰山—角,更多关于JEB的使用教程可以参考其官方 文档h呻s:〃wwwpnfSoftware.com(jeb/manual/°

|3。3Xposed框架的使用 在ll.3节中’我们已经初步了解了Hook技术,利用这个技术可以在某一逻辑的前后加人我们自 定义的逻辑处理代码实现我们想要的功能’例如数据截获`输人和返回值修改等。 那Hook技术能否应用在App上呢?当然也是可以的°



d

□】|当‖·』□∏■《]■■‖』‖」■■□||■可‖∩]

得到最终的数据。

0



} β

Xposed框架的使用

l3.3

625



} ■■「‖■■「|

■ 仲 『 ●■■『|△『庐|‖『『■∏匹尸)|‖■丛■尸

「「供》 ∩『■厂‖■尸「》■■「

P

P

β





















F



Xposed模块修改真实App的执行逻辑° 2准备工作

由于Xposed运行在Android平台上,所以我们本节的环境和Androjd相关。 在开始之前’先做好如下准备工作°

□配置好Android开发环境’具体的配置方法可以参考https://sempscrapecenteⅣandroid°

□准备_个已经ROOT的Android设备(真机或模拟器均可)’并把它和PC连接好,可以在PC 上使用adb命令正常连接到该设备°

°在设备上安装好Xp。sed工具具体的安装方法晤/r醒Ⅲ— 可以参考https://setuPscrape.centeI/xposed°

‖、=



Xposed框架的原理我们稍作了解即可:替换系统级别的/system/bin/app-process程序,控制zygote 进程,使得app—process在启动过程中加载XposedB∏dgejar包’这个jar包里定义了对系统方法`属 性所做的—系列Hook操作’同时提供了几个HookAPI供我们编写Xposed模块使用°我们在编写 Xposed模块时’引用几个Hook方法就可以修改系统级别的任意方法和属性了。 这么说可能有点抽象’下面我们编写一个Xposed模块’带大家体会_下它的用法’最后再使用

…帝



以在功能不冲突的情况下同时运作°

~=←一



Xposed框架是一套开源的,在Androjd高权限模式下运行的框架服务,可以在不修改App源码的 情况下影响程序运行(修改系统)°基于Xposed框架’可以制作许多功能强大的模块,且这些模块可

α≡

P`



↑.×posed框架的简介





那这个技术怎么实现呢?这里不得不提到_个框架_Xposed框架。

阅=



的截获°





Hook技术在App上的应用非常广泛,例如修改朋友圈的微信步数’其实就是通过发送Hook数 据的方法修改了步数;又如处理SSLPinjng问题时’用Hook技术修改SSL证书的校验结果,从而绕 过校验过程°对于App爬虫’也可以Hook—些关键的方法拿到方法执行前后的结果’从而实现数据

翻.度

Xposed本身对Android系统和设备是有一定要求 的’如果你的设备不满足要求’这里有几个备选方案° □对于高版本的Android系统’Xposed可能不提供 支持’此时可以安装EdXposed’具体的安装方 法可以参考h忱ps://setup.scrape.center/edxposed。

□对于没有ROOT的Android系统’可以使用

VirtuaⅨposed’VirtuaⅨposed不需要ROOT即可 使用’具体的安装方法可以参考https://setup. scrape.center/virtualxposed。

∧C勋e『即…林助手 下哗攫’…‘-蘸下蹿

…《骚*0月…‖

砷茅瓣$〃!野瓣°…瓣1鹤t么/满 襟←盅命

=曲=←-~

P■凸



…个忘”警……磁出糠 厂

…《″‖剑

*髓警蹿0翻γt′豌.″于鳃?孵↑【磁舅 ≈……■→≈卑—~~■~■

=■_

k↑3

PⅥ……呵…0 1卜……=酗……斌 酶糊…″……膏蹿…獭呵k… ‖…旷/……`馋碾幻‖q酗N僵攀獭…$酗囊蝉;龋§‖碱鳃『 …;″酗酚 谦鹏子泌`翻窟/R.疆″∑0‖g/!/↑

3.Xposed模块

Xposed框架现在的生态系统非常庞大’基于它开发 的模块非常多,点击下载菜单就可以看到已发布的 Xposed模块’如图l3ˉ44所示。 XpOSed惧吠, 贝‖固ljˉ44/y「小。

"侧嗜x

…x樱卿婶…

雹″《髓*!0酗

…于鳃潞/‖/塑.矗舞于瓣0″γ/酗

肌DWec们at

…个鳃跳滋够趴a化颧越…鳃易…d嚷峨

可以看到’这里有各种各样的模块’当然我们也可瓣瞬魏9k…|… 以自己编写模块来实现想要的功能。这时大家可能会问,

这些模块究竟是干嘛的?到底是什么东西?其实从本质

图l3ˉ“已发布的Xposed模块





—」

{■‖日

第l3章Androjd逆向

626

上讲’Xposed模块就是—个AndroidApp,开发一个Xposed模块和开发一个AndrojdApP的流程是 差不多的’只不过前者多了下面4个步骤°



(2)需要引人XposedBndgejar包,从而实现Hook操作°

(3)需要定义—些Hook操作,对本App或其他App的逻辑进行修改。 (4)定义完Hook操作的逻辑后’需要告诉Xposed框架哪些是我们自己定义的Hook操作逻辑, 以便Xposed执行这些逻辑°

』可‖‖√□□勺

下面我们就—步步实现以上4步°

■■Ⅵ‖■■|‖□■■‖』■■■】』·■

(l)需要添加_些标识符’表明这个App是一个Xposed模块’以便在Andro|d设备上安装后’ Xposed框架可以被识别出来。

4开发-个Xposed模块

首先在AndroidStudlo中新建一个Android项目,会提示我们选择Actlvity,直接选择默认的Empty Activity即可’如图l3ˉ45所示。 邵

@ 』曲■

5e‖eCtap丫oject丁e咖p‖ate





】.





▲■

■′止

习』

■Tf●●●

【■『巴乙■

γ

●■伪

『■







■勺 『□





尸◎巴

飞出

沪■





■司■□■□■

p■ ■■



·■■■■可‖当■■■」■■可

■ ■

■pp体F■

β■



■‖■



鹏Ⅶ





●白



「~

肚巴x丁

‖』

图l3ˉ45新建_个Android项目 一=口

然后做—些基础配置’项目名称配置为XposedIest,包名可以任意取’配置好项目路径和编写语 ’

同时指定最小SDK版本为l5’如图l3ˉ46所示°



』』〗一■■



‖{

) 、/≈夕|||、「′「|■[广’|Ⅳ哇广、以、≥`少似仙比/》≥‖巴出具∩≥)、′/卜<田

l3.3

XPosed框架的使用

627

厂一_=

|ff

●{



C◎"闹gu『e`′◎"『p『·jeα

| ‖

↓ | | 〗■

| |

飞 ■



=〉



■■■



■■



| | ‖

■■■



〕〕〕》~

『‖"『驹

图l3ˉ46_些基础配置

点击FINlSH按钮’XposedTest项目就创建好了,生成的界圃如图l3ˉ47所示° ●叼p

°· . 』

■□0己

回口□■ 0P° °·□ 0■·

□ o.□





0



· .



嘲甲o▲=旷冈

日由

■□■





■屯

D

ah 可血■ ˉ乒→Q

□ d■



@■■





0

L….27≈罗.0盯.率0.旧孕8

尾□













……

『■





『□













■≡

§

■∩

■乒



□%…

…喀

>》`′

凸旨□■巳<"

户\/净′巴「「乌‖巴阐厂

■尸凹山『T■



◎□□□□◎巳

、’凡庐‖‖‖|、尸卜习岂‖>吧尸『、)『止′口′》|佃白广\尸’并「〖勋′》^尸弘『〕卜心|、√》

| |

| ■0

O

b■

″□■■

凹·■.·0 `■■· ‖△

l

图l3ˉ47Xp酶edTest项目

■■

|司口(

第l3章Android逆向

628

〈爬t3ˉd3ta

己∩droid:∩日Ⅶe=0『xpo5ed『∏od‖1e|| a∩drojd:γ己1ue=脚true" /〉 〈Ⅶetaˉdat己

』‖‖‖

之后我们实现之前说的第l步,添加_些标识符,表明这是_个Xposed模块°打开 AndrojdManilest.xml文件,找到app1j〔at1o∩标签,在与act1vjty标签并列的位置添加如下内容:

日∩dIo1d:∩a爬="xposeddescrjptjo∩00 日∩droid:γa1ue="Xposed丁e5t" /〉 〈们etaˉdata

|』■

a∩droid:∩aⅦe≡×posedm∩γer5io∩" a∩droid:γa1ue="5〕" /〉

最终AndroidManifestxml文件的内容如图l3ˉ48所示° 【



T己



■…ˉ…α■■

■…≈…匀=■

■邱咖q.



p田仔●

泌●渺■=匀己Qq

●≈…韵…· -L二





…w0Pb0■‖“云…←·讯如:〃m恒3·…硕d◎…醉′「●S/…寸

■· b

…熏〔■·低酗.咖蛔k俩t■少

■ 飞

O

…1延■℃叫·

■ 户

…】U『回『〗…∧…·t硒·

■●





…【口:0…←0…/i[→l……·

…徊吕【…l凸.呐■“丁●巳t.



…℃id徊″『C呻■知(…^仁→l…帖千=尸■蛔□



…』d.】…厂t甄tl■·t….



…j吐?=·钩tylG/……白>



c■□

←■

0

…iα占跨.…睡…lO■ …jαβ归!…曰七…■/≥ 9¥■■E

0

q

…ld:…■蛔苫g呐∑〔「赋0侧■

…i口:ml呻·酗0颧γ噬t■/≥ 字巳?品

铝□〗●■凸

纠■‖

…jα;…·蹿历枷呐碾0m· …i口『蛔l…凹乙3p/≥

」‖‖■

◇』口.『 口马●o…蛔『…泊□幽〖哨ctwtty口≥ <■『·Q0 0 · .‖k?■′≥

』 ■ ■ ■ ■

屯岔( . 尸…‖df…■…冗1■.t忙啡.“tl…伞毗】N■^

< √冤. ?

1口0.· 0 .…徊悸■…‖d.i吨●让c@t…y·l…尼w^

·.

√ .

o

0

.·∏ "≥

> 内■·●<】●≤公■∩■■Ⅱ■

.V…匆0己@QαB 〃吨. ·.

p■1



山F硒…■m0

b

=■~■

丹0·◇≈□ ■◆■r●;4沪■

. vV尺x〗

●‘←′ 0埔

…牵■0凹巳=0…●……户芦~□回凹…$°…■hO◆8″■户『=…T`…向v■■…0■■吵0p●仕此咱§0醒●』.】 □‖牢□l‖』 l◇■■

啄弓己

-=~

巨p

』卜ˉ导ˉ面●止plTu

●…0D ■



图l3ˉ48AndroidManifest·xml文件的内容

如图l3ˉ49所示。

‖ | | ‖||

定义好这3个内容后’把这个App安装到准备好的Android设备上’Xposed框架就能识别出这个 App是-个Xposed模块了°点击运行按钮,在设备上运行这个App’可以看到设备上显示如下界面,

■■

□xpo5edⅦodu1e:这里设置为true,代表这是_个Xposed模块° □xpo5edde5〔ript1o∩:模块的描述’此处填写模块名称就好’就是一个字符串° □xpo5ed川1∩γer5jo∩:模块运行所要求的Xposed最低版本号’这里是53°

」 = ■ Ⅵ 勺

这段内容指定了3个阳eta_d日ta°

·□】●■』■■■Ⅵ』■■■■

■弓■吕≈●噎■∏β■』■●■弓召肛≈■$·■■日Ⅵ岂二眨●

<■

‖」‖

●□·



巳■■■导■$∏▲β丛Ⅷ■

=Q=…■

■■日峪旧博旧栏旧【$『§【】【午门『■∏Ⅱ】·

…翱b●■》●mD■屯m、●≈…鸭呵’

□■】■司|

可』■∏‖|■∏』■‖

q

』■■■■■

■【■■■■【『□■∏■■『〗■■【■‖□匹■「‖‖〔β‖「||■●∏‖『|■尸『‖「}■尸||■厂}‖||也尸「『‖‖■

l33Xposed框架的使用 ≡_-击口

629

■=~

◆》0沁



…一=≡=-

}{ ||‖

吨 ≡







凸锤







广}’ˉ■=■卜‖■「|协|■厂』·尸`●■尸『■■}|■『=『『[卜谚`〗尸}■「|■『■「|}■■∩′■「■侈『|『【■「β∩’【■=■『卜坠■■厉‖〔【尸‖『‖■∏●β■【′`■【■》■「●「●『卜尸卜旧}}■「■「广|【■『巴

0鳃熊…

图l3ˉ49在设备上运行XposedTestApp

此时打开XposedInstaller的模块界面’就会发现它检测到了这个模块,如图13ˉ50所示。 内臼▲

;工日

≡樱块

;国 xp”e‘『总S↑ X…丁e筑

_

图l3ˉ50XposedInstaller的模块界面

我们勾选这个模块’它就成功被启用了。不过需要注意’这个操作得重启设备才能生效’可以手 动启动设备或者通过XposedInstaller首页的重启选项进行重启。 但是’现在启用了也没什么用啊’因为XposedI℃st里还什么功能都没有呢,需要引人与Xposed相

关的SDK’我们才能调用Xposed提供的一些Hook操作方法’实现Hook操作。

于是打开app/buildgradle文件,在depe∩de∩〔1e5部分添加如下两行代码: cα∏pi1e0"1y `de。robv.a∩droid·xpo5ed:api:82| 〔咖pi1e0∩1y 0de。robv·a∩dro1d。xpo5ed:apj:82:5ol」rce5

这是Xposed的SDK’添加之后AndroidStuido会检测到项目配置发生的变化’并在上方显示提 示信息。我们点击右上角的“SyncNow”选项’就会自动下载和安装新添加的XposedSDK,如图l3ˉ5l 所示。

现在Xposed的SDK就安装成功了’下面我们就能使用里面的方法Hook代码逻辑了。那怎么实 现呢?具体Hook什么呢?总得有点头绪吧’头绪在哪呢?不妨先自己写_个’这里我们会增加一个 鼠标响应事件’在点击鼠标后触发算式计算操作°

d■■■■■■



↑3

■ ■ · ■ 丫 ■ ■ ∏ (

第l3章Android逆向

630 ●≤仁矽$炉企茁冒白β■ ■



.●b啦钠

!



厂.□国z



囱J切,

锣℃. . 凹



●‖ Ⅺ

∏叼l…袱◎t【Q∩『1i芒γ广鳞αt尸: .1tD∑′ . 1欣c1四e: 『··.]g厂0D

0

亡…t1呐1Y 0由.…v令@沁伊°触.xmb泌:却ng82·

″∩

i呻【…仍mt8◎" .0…7oiqx霄呻p危…↑:……t21。1000

决化飞引■

Ⅱ呻I…倔加tt咖0md徊i幽△c“3t沁t可tl◎y吨t目cw‖豌7α枷tl4蛔l它:1口1.3· teStI呻【…"t◎t〗c∩ ‖jm让:】鳃i亡:qˉ巫0

. . .

.…. .

O oγU『αl矾“f』叼D1■″冈tnt0◎" op?刘mi少.le迅t·G”护e5憋O:G凸p厂e鳃卧仁◎F●;3曰2°0Q 削『

″□.

□b

通.‘1嚼.. 如■



■品·

=□■□ ∏ $D

0

B



. ■

·



…△呼·



T



个必『坤化〔…tl酗F 8吵t◎…池马tC…“timG2mb1t占沁d枷o2耳9汗40』tM218占

盯凸酗屁吕 E◆fT巴蛔≈t■斗障1’串t‖回i陇□硒FOi血.……七.叭…t.D厂…儿t∑七γi酞,1…『@疮elG唾叼1…习飞↑【◎∏《`砒0…1…)翱ld…em亡◎尸广·锭ly…7尸i…门

■贮

t腮“泻…pPt蛔tc≈灶蛔i"md冈id△wl…t儿t$tⅦ卸

扯 ■峦0∧nbf趾: l◎…@/”玉t…/hD/蛇lJ1〖龋L【蜘1=〔址…t吐tcn巴o

O″li摊α: 〗…/田沁t喇hb/鳃l〃i娜』K5γ乙e毋』lotTm,的

自 F

0 I/砷●蝇碘臼唾忘P: Imtiα1M喇【αⅡ瞻炉$↑o硼1.4 吕M°a圃` !‘ i .`

:ˉ:

ˉqˉ. ..;. .严…。耙·.· .

§ .... ..



,≡ ′§.." .‖ ,. 学癣;吵; .



吕奄

愤…炳1混◆…≈厂8知i1edtO已ekE6L锌枷八p≈睡》…佃m3叫?『@C巳枷刨gfq″,●P巾「■启吼ˉH№~酗7α

‖」』·‖|二■Ⅵ」‖|』■■可‖|』■司二■■■■」』叮|■■‖』』勺

d

■■■

■隘″喝●m蝴正0B…■

句o∩…"

》qR□■

o§内≈q二0

Qγ…

山【·U四唾■…巴●0C1 【『 bw=8 △9pM◇ °■

m唾B但H…0沁…■国咱』0凹J庭闭〗…申》

‖ (

乙警晤苫■■巴■■呈乙■■但刀■从≈■←■怠巳忍$巳日Ⅷ●

G

| ■ ■ ■ 可

口喇矿Oi盯G吐【呻1侣罐々lt□仓飞@" !四Mmi幽.亡eZc.e又t:ju∏w`1镭】.1,

■` 串口,l皆

√■■

‖ `. 揖酗钾锣绊′

‖ 』 · ■

c…il勘↑lγ c“口了吨v.@m尸otβ0x凹md目却t:睡8SpM厂C“Q

□臼$曲占■≈『



邑■ ■罗 已 巨 ■ 『 弓 ‘ ≈ 葛 爵 萤 ← ■ 月 霹 ● ‘‘ ■司 百百图

‖p

仿 ‘P…ae内°e弓£

■■■●司|』】□』■可‖‖」‖‖|」■■司

α

佯■淘钞……晒『c0q0 . ■≈■…(…. ◆A酗o…“O【▲碱口

c『■d掩卯GZ仇石腔幼…bi酝G1a0【p吨$〔『$协‘‖久α毗已『仰∩β『私约毖∩唾呵B呐b『t№‖m《o尚@「k…严碱v,

■■§井旦●差§己恿月



α ■ 乙

啸…











→…













乙“…″……o ■…诅w幻严】 ■



●彤 ! ≡



}■倘铡“《ˉ

|彭◎……『惭"· ` |‖ . |<萨. Ⅶ. ` :. ,、′.!; °?

←…



『 ∏“…呻O 》撵”β》〃b“.…p

2

图l3ˉ5l 点击“SyncNow”选项

』·■、

〈?xⅧ1veI51o∩=001.000 e∩codi∩g=00l」t+ˉ8"?> ≤a∩dIoidx.〔o∩5trai∩t1己yout°wjdget.〔o∏5traj∩tl己yout x川1∩5;3∩drojd="http://5〔he∏a5·a∩droid·〔o‖‖/己pk/re5/a∩dro1d0‘ x川1∩5:app≡"http://5〔∩e川a5·a∩drojd·co↑Y‖/ap促/re5ˉauto0| x∏1∩5:too15="忙tp吕//5〔he∏己s.a∏dIo1d.〔α『〗/too15" a∩drojd:1aγout-"1dth≡00川at〔h-pare∩t" a∩droid:1日yout_height=00Ⅷatch-pare∩t耐 too15:co∩text=00.例ai∩∧ct1γjty"〉 ^■

■‖■■∏(■|

〈8uttO∩

a∩dro1d:jd≡圃αid/b‖tto∩"

°·‖二·Ⅵ

首先修改_下页面内容,设置一个按钮,将activity≡maInxml文件中的内容替换为下面的代码即可:

a∩droid:1己yol』t_Njdth="wmp〔o∩te∩to0 3∩droid:1ayo0t_hGjght="Nmp-〔o∩te∩t0‖

这时重新运行App,就会出现—个TEST按钮而不是文本框了。然后修改MajnActjvityjava文件, 其内容如下: p己〔炮ge〔α∏。geI∏论γ.xpo5edte5tj

mporta∩dIojdx.app〔o‖∏pat.app.∧pp〔α即己t∧〔tjγityj mporta∏dIojd.o5.Bu∩d1ej i『卯orta∩dIojd.γiew.γjewj iⅦporta∩dro1d.widget.8utto∩j i呻orta∩droid。wjdget。「oa5t; mpoIta∩dIo1d.o5.Bu∩d1e;

pub1j〔〔1as5舶j∩∧ctjγityexte∩d5∧pp〔o∏p日tA〔tiγjtγ{ pr1vateBl』tto∩b[』t↑o∩;

《』·|·■■■』■■■口■甲□·】■■■Ⅵ』日』可司勺‖‖■■■勺‖■』■‖日』■■■‖■■

a∩droid:text=0丁e5t厕

app:13yout_〔o∩str己1∩tBottα∏toBotto刷十=00paIe∩t" app:13yout-〔o∩5tmj∏t[∩dto[∩dO「="pare∩t" app:1己γol』t-〔o∩5trai∩t5tartto5tart听=wpare∩t" app:1ayout-〔o∩5tmj∩t「op_toγo"十=圃paIe∩t" /〉 </a∩droidx.〔o∩5tmi∏t1ayout·widget.〔o∩5trai∩t[ayol』t〉





l3.3

Xposed框架的使用

63l

叫errjde

prote〔tedγojdo∩〔Ie己te(8u∩d1e5己γedI∩5ta∩〔e5tate){ 5upeI.o∩〔re日te(5avedI∩5ta∩ce5tate)j 5et〔o∩te∩tγ1ew(R。1日γOut.己Ctjγitγ-Ⅶaj∩); butto∩=+j∩dγje"ById(R·1d。butto∩); butto∩.5et0∩(1j〔代[j5te∩er(∩ewγiew.0∩〔11〔代[i5te∩er(){ pub1j〔voido∩〔1i〔长(γjewγ){ ■●『||||『≥「|■β「|『‖■‖仔厅●■厂‖|β「∩■■‖巴尸■■■厂广巳尸『但■厂◆卜)■‖■「〖■厂心广卜‖‖■尸「●■■厂■∩■β■尸卜■■厂△■「△■厂|■■■■‖『》|■■尸■「■■■『卜β‖■尸▲■【■「心}‖「|印■【

『Oa5t.∏冰e丁e×t(问ai∩∧Ctiγjty.t们j5’ ShO喇eSSage(1’ 2)’ 丁OaSt.[[‖C丁‖5旧R丁).S∩OW()j }

})j }

pub11c5trj∩g5∩o喇e55己ge(1∩tx’ j∩ty){ retur∩ 00x+y≡ "+(x+y)j } }

这里我们定义了一个8utto∩对象,然后使用十1∩dγje"ById方法从视图里获取了这个对象’并为 它添加了一个点击事件’具体是点击该按钮之后生成丁oa5t提示’提示内容为s‖ow‖e55age方法的返 回结果°

5bo酗e55age方法接收两个参数—1∩t类型的x和y’返回结果是_个字符串’由“x+y=,’字 符串和计算结果拼接而得,其实就是一个算数表达式°此处我们给5how付e5sage方法传人的参数是1和 2’所以点击按钮后,界面上应该显示x+y=3。我们重新运行App’然后点击TEST按钮,运行结果如 图l3ˉ52所示° 刀刀回▲

■↑Ud

…二“爬6‖

| p琶∏



■■ 图13ˉ52点击TEST按钮的结果

这就是一个基本的逻辑°下一步我们使用Xposed模块对这个逻辑进行Hook,在与MamActivity. java文件同级的位置新建_个HookMessagejava文件,文件内容如下: P己c促a8e〔咖·geI眶y。xpo5edte5tj 1『印ortde·robγ.a∩droid.×po5ed.IXpo5ed}们ok[o己dpacRage; j呻ortde.robγ.a∩drojd.xpo5ed°X〔-惟thod‖们ok;

●■勺‖||{‖』■■|」‖』■■‖』·

第l3章Android逆向

632

mportαe.robγ.a∩droid。xpo5ed·Xposed8rjdgej i们portde.robγ°a∩droid°xpo5ed·Xposed‖e1per5; j∩portde°robγ·a∩drojd.×po5ed°ca11b己〔代5·X〔[oadpa〔阳8ej

pub1j〔vo1dha∩d1e[o日dpa〔代age(X〔ˉ[o己dpa〔|ege儿o己dpa〔{(agepar日川1oadPa〔|〈己gepam") thro"5丁打Io"ab1e{ i十(1o3dP己c惯agePam‖.pa〔kage‖己爬.eq0a15(‖0co们.8emey.xpo5edte5t"〉){ Xpo5edBrjdge.1og(厕‖oo低ed〔o∏|·gemey.xpo5edtestpa〔|(age")j 〔1a55c1a亚=1oadpac炮gepara肌〔1as5止oader。1o日d〔1a55( "co肌8emey.xpo5edte5t."己j∩∧ctiγity")j

Xposed}|e1peI5.「j∏dA∩d}{oo刚ethod(c1az卫’ 0|5ho0咋55age"’ i∩t。〔1a55, 1∩t.〔1a55’∩ewX〔‖et∩od}{oo|((){ protectedγojdbe千ore‖oo代ed‖ethod(粉ethod‖oo代pam∏p日ra们) throw5「hro"ab1e{ Xpo5edBr1dge.1og("〔a11edbe于ore刊ooked"ethod"); par咖。arg5[O] =2j Xpo5edBrjdge.1og("〔ha∩ged日rg50to"+p己r日Ⅶ。arg5[0])j }

prote〔tedvo1da什er‖ooked‖et‖od(‖et打od|ˉ{oo代p己m们pam") thIo"5丁hrowab1e{ Xpo5edBIjdge.1og("〔a11eda+ter‖oo代ed‖et‖od0』)j } })j } }



这里我们定义了与Hook操作相关的逻辑’下面梳理_下其中的关键点。

□‖ook‖e55age类实现了IXposed‖oo灶oadpa〔促age接口,需要定义ha∩d1e[oadpa〔阳ge方法,这个 方法会在加载每个ApP包时被执行。 □在∩a∏d1e[oadpac促age方法中,调用1oadpa〔kagepam阳.pa〔促a8e‖aⅧe属性获取了当前运行的 App包名’并判断其是否为当前Xposed模块对应App的包名,然后做后续处理。注意这 里的包名可以是任意App的’不一定非得是当前Xposed模块对应App的包名’只不过我们是 要Hook在这个App中定义的逻辑,所以做这个判断° □如果上一步的判断结果为‘是,,’就调用1o己d〔1a55方法’并在其参数中指定要加载的类(这

■■■■■■【】■■■】□■■■‖‖‖|□可‖凸·臼‖|『』□■■」■‖|』』■∏」■】】■‖■』=■司‖』■■■■司‖‖』勺‖」■□□‖』□|」■■习|】■]』■‖|』■勺|』■■可■可」■∏||』■‖‖·|』●]■□■乙■■』■司〗‖||

pub11〔c1a55‖oo刚e5sagej川p1e‖e∩ts IXpo5ed‖oo|([oadp日〔阳ge{

里是刚才定义的‖a1∩∧〔t1γ1ty类)的路径,然后把动态加载出的类赋值为C1aZZ’这是_个 类对象°

数)和_个X〔‖et‖od‖oo代方法°

a十ter‖ooⅨed‖et∩od’分别代表Hook5ho"‖e55age方法前、后所做的操作,这两个方法都有_

修改被Hook方法的结果°

||

个‖et∩od‖ookpamⅧ类型的参数,里面包含方法执行的参数和结果等信息。 □_般而言, be十ore‖oo代ed‖et∩od方法用来修改被Hook方法的参数内容,或者直接定义被Hook 方法的运行流程。a+ter‖oo促ed‖ethod用来对被Hook方法做后处理,例如拦截、保存、转发、

□】‖月

□X〔‖et∩od‖oo代方法里定义了Hook操作的真正逻辑’包含两个方法—be+ore‖oo代ed‖ethod和

‖‖

的就是c1azz类, 5‖ow‖e55age方法’两个i∩t.C1a55(5∩ow‖e55age方法有两个1∩t类型的参

|‖』■】■司

□调用XposedHelpers模块提供的+j∩d∧∩d‖oo低‖et∩od方法’需要传人类名`方法名`方法的参 数类型(有几个参数就写几个类型’写法是参数类型加上类的声明)和Hook逻辑°这里传人

□XposedB∏dge模块里的1og方法可以将日志信息记录到Xposcdlnstaller中’我们在Xposed Installer的日志页面就能看到对应的结果,很方便在调试时使用° ‖

】』■□‖』日刽Ⅵ|‖□Ⅱ』·‖』』·■』`』』‖‖』∩

这里我们先用be十ore‖oo长ed‖ethod方法做处理’修改参数paraⅧ的arg5属性值,这个值其实是_ 个列表,元素是5∩ow↑‖e55age方法的参数,因为之前传人的参数是1和2,所以这里的arg5属性值就 是[1’ 2]’我们把其中的第_个元素修改成了2,那arg5属性值就会变成[2’2]。



/||、〔币『γ’|\γ>归′‖『『此′夕|趴γ|△歹\从阳「`「『\>|‖`山‖‖||)队≥厂似\也产∑团四》’■|‖●『>/》|\凹「『扩、四

Xposed框架的使用

l3.3

633

现在我们已经把删ook的逻辑实现好了,还差最后—步’就是告诉Xposed模块我们的删ook逻辑 在哪定义着’因此需要新建_个Xposed人口文件。首先在maⅡn文件夹下新建—个AssetsFo!der,如 图Ⅱ3ˉ53所示°

.……黔园「↑雪

』°宁.

“ m…

…』

§ `: 虽墨晌 ■ 已…凹

@



ˉ;

唾[……q

c……

吁′‖}伊≥}、『

.已…■宙



2→啊

回△←四刚m 旦『镭叫



』ˉ ■

@



吧… ……

@◎幻h



口 『

鹏|…

:墨

.哩‘″…

=≡=■

■m■■■

O…凸…呻『∏@【-≡Ⅲ吕卫.

′Q■■止■四啤阳凶F

辩§磁辑旦…闭…钮啼

……·.



:蹦臼

喧聪

Ⅱ■西u企…



留α 亡@

§

…0………ˉ…

ˉ !

m…

o…□吨

≡℃『

=…·



』 』f·



已…呻回闪….

』罕虽.=…′

|}



■0■V~闪■

吐=二盂Pˉˉ二ˉ

◎ 幻…

{!

!

h叮.毯懒〖叼■P目 …1….【咀凹■〗…酝伊俐锤·OmP甲.…阅〗6

……

】 …唾



tm六〗【回b■0可旧劈O…~肉7动■…■ …Pi竿口』呕Ⅷ■8●【G肌韶一≡》;

巳………………

…■■■→

!.愚.Ⅶ



.警网.fhp幽唾…啤0镭■0锤p0m响…由.■■……□0 ‖尸e.C〗m旧o B叶.c』°s罗°厢口渣些@←岂置

′…撼

g…些…

旦=坠g三==~巳

●′

蝎■1四…伊■P争口色8睡…°…‖●≈[

.■

.….………鳃蜘…·.’;

■…∏卸

□u…■■O■呻q 口〔■l0翻团沁■州…们0…·〗; …………o…囱……… . □……Lp醒■出』邱呻巳…『=

………`

: .



..ˉˉ

■洒m

@

瞬臻爵瑶爵!蕊.慧嚣蠢谬;;;

△烂=些巳空



函■ ●

□‖

。…′^

….…,.

.≈■刚h. …●■…



□0

0 『PL凸·…v■·阳

. .…"

. 凰…霉` . . .°….…,·.晕缚愚`°□

.………·…….·.≡….固……. ■…声□■a0m‘‖U∩ !…叮西可…….…固c…面锤m■津…m桓0酣■0…匹1

咽瞎

』 ;匿』 ※国 □

` 鸟

__Ⅲ丁面伞≈……呛

…〖°◎……≡

刁p00凹巴们凸●…●be

图Ⅱ3ˉ53新建_个AssetsFolder ~/》

然后在asSe!s文件夹_「新建一个xpoSed_ini『文件,文件名不需要有任何后缀,如图l3ˉ54所示。 8…D●唾

■G电〗■…0■…0

!.宁P

●F

0



.

』.萨

!

?

·

.

.、ˉoo职`

.

°什Pe·● ..俭0■曰禹导B吨α□ @

口; 6

;

cl…刨…■l■凸酌c阻…印P″°色…°0…lm吨 卫…■….雨…9髓.啦…w0旷〗°



…曲P0午.〗喊惋呵□ml喇0■-〗°

;■



…审.…闽■凸;

●●

…t………髓..…聊; 蹿蔚= 』……’

|; 」! [篮

厂—一—ˉ



●◎



Q

i

匝D『PG『蛔·o0□bm口吓N…甲≈【…0≡印『P】【h…B』…. . 《 "…`……°‖…`……………. 』…………!



■■

【】



。已

〗P《0吨…哟em….….…l巩°≡≡=》〗【 …P1…°!呕■m□……°…·…跨□》§





..

…‘……』,m…=……0….………甄……』阐…』.……

.∩





也■0■

…■眶′Ⅷ■J了叮mD



i

■■

■■马盯

■T



; 盈









1

p d







〔呼…凹Ⅲ唾空白旦.l佃←



〔). 日■.供0口飞 .. 0 .0■0飞□刮hQ 尸■. .G巳 毗…呵呻=……蜘∩〗…

≈黑∏】■■】吾■■■■

■帅≈■■山西●0牢■G砷Ⅶ

□帝□

△■呻酗 —歹尸



…。■□●

龟■■£≈旧嚼【巴□■=图■冗

尸门|》山丁|||`吵「|刃】‖「|『|\/||\出『‖」°’}防庚/「}扒『′‖(「|^‖厂已\〃八∏抄「阳忻\/卜[□、「)↓|》|卜|廷√【凶

! 。



凸….…∩□铂. ●……沪巴·●函乙≈□●≈……□m′.○…~户°·

o■乱…

Q■…昆〗O凶

O……吧■吨■…≈■…………』』磅剐巴■…哮〖凹…0〃·独^占田□凹心~F.山0唾凸晒°U纪凸邮…w………钝罕……●四〗 00Ⅶ『·■e唾■b 昌

图l3ˉ54新建_个xposed-′nit文件

0

把‖oo刚e55age类的路径写在这个文件中’即文件内容如下: 〔o‖·ger‖ey°×po5edte5t·‖oo刚e55age

写好后保存这个文件’Xposed模块就会自动读取xpose处mt文件’并执行我们自定义的Hook逻辑。 最后’重新安装一下XposedTestApp看看效果’记得安装完成之后重启_下Xposed模块’否则 是没有效果的°重启Xposed模块之后’点击App界面上的TEST按钮,结果如图l3ˉ55所示° 可以看到’这里的运行结果就和图l3ˉ52中的不一样了,丁oa5t提示信息变成了x+y=4,这说明我

们通过be+ore‖oo促ed‖et∩od方法’成功把arg5属性值的第_个元素’也就是5∩o州e55age方法的x参 数值修改成了2,第二个元素则还是2’相当于在5ho"‖e55age方法被调用之前’其两个参数就被修改 成了2和2,所以最后的计算结果是q。

现在大家对be+ore‖oo|〈ed‖ethod方法的用法应该有进_步了解了。这个方法学习完’我们再来体 会_下a十ter‖oo促ed‖et∩od方法的用法’它用来对被Hook方法的返回结果做后处理’例如这里我们把 a「ter‖oo代ed‖etbod方法的内容修改为: pIote〔tedγoida十ter‖oo促ed例et∩od(鞭thod‖oo长ParaⅧpara‖)thro"s『hroNab1e{ Xpo5ed8ridge·1og(圃〔a11eda仕er‖oo促ed№thod!』); para川.5etRe5u1t(碱"oo促ed")j }

这里我们增加了一行代码_调用para"的5etRe5u1t方法,这样可以直接修改方法的返回结果°

修改完成之后,重新安装_下XposedT℃stApp’并重启这个Xposed模块,还是点击TEST按钮’ 运行结果变成了图13ˉ56所示的这样° ●》$8

Q▲



妥m酶d丁eS

厂忘一] ■=-一乙



图l3ˉ55

Hmk后点击TEST按钮的结果

…|

xpOs“爬st

百-—≡豆q

洒r 〗

● ■ 图13ˉ56修改返回结果后的「oa5t提示信息

可以看到提示信息变成了我们修改的内容,说明a什er‖oo代ed‖et‖od方法起作用了°最后我们来 看_下日志’打开XposedInstaller的日志页面’其内容如图13ˉ57所示°

■■||」ˉ■〗‖‖‖』‖|」■■]|〗·】■】」■】γ|』■』∏‖』】‖‖』可』』■】■





隘▲

‖··|」■■|‖|■■】■|』】■可■·】■■||』■■□司‖司|」‖』■]』‖』』■」■||‖』·‖‖勺■■‖‖□|』■‖‖』●|‖■■∏|」』·』‖‖』■{□司·日{|』■∏」■■‖‖』■]司〈‖纠■叫■|』·{■||司β|■】

第l3章Android逆向

634



l3.4

基于Xposed的爬取实战案例

635

p

‖■「β



可以看到这里输出了我们用XposedBridge 模块的1og方法输出的内容° 5.Xposed模块提供的∧尸|

现在我们来看-下Xposed模块提供的API°

}回▲ 三

●y!鲍

.【 日寇

B>



| §

…(2%}

-←一

…《206)窃郝叼N……婶0的°c碑呻舶0『筑鳃Ⅸ羽 …《四6》k …№侧o丁匡3(x嚼∏0忽…v砌申鲸`β0]《S眺23‖

…《”6》…Ⅶw〗只→

】…《”6》.…代呻甲耐ˉα″悦闯…幽∑/碳憾尸h鲸2↓60M…∑蛔?“7腮?」

■■|‖卜广「‖【【■

本节前面所讲的Hook操作就是由Xposed模块

…《∑%》之 p憾汕两m“鳃.域酗倔”辆蛔w…蹿霸瞬3 …《2%》髓凹『腮臀”b鄙『n……h鞠硒

提供的—个API’即十1∩d∧∩d‖oo促‖et‖od实现的°

…《”6)_ …(Ⅱ%〉蜘锄议p◎…(/沮…丁】/『…龋点凝购钢翻匈@灯)购″6…『N

大家可以打开https://api.xposedinfD/reference/de/

=■‖巴■『卜

robv/android/xposed/XposedHelpershtml查看所 有的API,这里简单列举几个。

…《窟%》…0酮卿!a蹦咖色 喇《溺b》…x蚀酚Qdp陋辨抛0…;…耀/阅m…贞p忿s“敞}鞠Q…!‖蹦哪 …{忿p6》LQ“呐lw铡硼龋《…`/鳃酮…/呻γ咱侧F“y…tWb…哩 〕…《2%) …(泅四》:

…(Ⅵ$”》≡ 嘲《M”〉 …αp50‖◎∑ ≈糊E耐“……回… 》…《?呐9)

□〔a115tatj〔‖ethod:调用静态方法°

□十1∩d∧∩d‖oo代〔o∩5tructor:查找并Hook构

图13ˉ57 日志页面

巴 ■ 「

造方法°

厂巴■

□+j∩d〔1a55I十[X15t5:查找某个类是否存在。

□十1∩d「je1d:获取成员变量° 巴■=●■厂卜■■「■{尸

很多API是有类似功能或重合功能的,这里不再—一列举,如果感兴趣可以查看官方的文档说明°

另外’非常推荐大家研究一下Xposed模块里各个包的用法’地址为https://apixposedlnfO/reference/ de/robv/android/xposed/packageˉsummaryhtml°大家还可以多研究_些优秀的Xposed模块’例如 https:〃dev习oumalcom/bestˉxposedˉmoduleshtml里就列举了几款很受欢迎的Xposed模块°Xposed框 架中文站的地址是h仗p://xposedappkgcom/’大家可以从中找一些优秀模块的源码研究-下’收获会 非常大°

|卜}》『卜

6。总结

本节中我们通过一个案例实现了Xposed模块的Hook逻辑’大家应该可以体会到Xposed模块的 作用了, l3.4节我们会使用Xposed模块爬取真实的数据°

本节代码见h呻s://glthuhcom/Python3WebSpider/XposedTest。 》■厂巴β·■》■β

↑34基于Xposed的爬取实战案例 l33节我们介绍了Xposed模块的基本使用方法’本节中我们结合—个真实的案例学习如何使用 Xposed爬取App的数据° ↑.准备工作

β‖巴■「

本节需要的环境与l3.3节是一样的,请参考那里的内容来配置环境°除此之外,还需要额外安装



↑3 L

jadxˉgui工具并掌握它的基本用法’这个我们在l3.l节已经学习过,如果忘记了,那么可以回顾—下°

凸「

由于本节的内容是实战,所以也需要-个示例App,这个App和前3节是_样的’下载地址依然 为https:〃app5scrape.center’下载之后依然保存为scrapeˉapp5apk.

‖″尸卜『尸|◆【尸

做好这些后’因为本节我们需要用Flask搭建_个简易的测试服务器’所以还要安装好Flask和 loguru’使用pjp3工具安装即可: pip3j∩5ta11十1a5代1oguru

『『仪巴■「》’卜田

2.反编译

既然要用Xposed模块爬取数据’就免不了要借助Xposed提供的一些Hook方法’那具体Hook什

巴巳‖巴『| 巴■【『■尸庄『▲■



第l3章Android逆向

636

么内容呢?选择其实有很多’例如我们可以Hook与构造HTTP请求参数相关的方法,之后可以得到 _些token字符串;又如可以Hook用来获取HTTP响应结果的方法,这样相当于直接拿到了数据°

可以看出, 目标方法是关键。既然我们要爬取数据’那么干脆—步到位好了’直接通过Hook的 方式拦截HTTP响应结果’然后用某种方式保存下来,数据爬取就完成了°

那用来获取HTTP响应结果的方法究竟是什么呢?目前我们无从知道’所以有必要对apk文件做 _些反编译操作,反编译之后尝试分析_下代码逻辑’应该就能知道哪个方法是我们想要Hook的方 法了°

和l3.l节介绍的_样,打开jadxˉguj工具’然后直接打开apk文件’就可以看到反编译的结果了, 如图l3ˉ58所示° 心=蜘锤…

蜘…

)宙尸扯

■叶§



呛`

■巾

←司

—_—-一=-=…≡=…. - 簿.

}{

′ 、 ˉ! 0哮 急 卜 沪

ˉ′ ≡

~≈←乙≈=-—□==~-■-



E

一ˉ兰弯=≈_当

啸″…宁睦嘲庄



用■一=≡…o谗~



●文·

凸=牡几

≡=…—_

_g

~々■■

…冒摩…

.…

巴≡=巴_

′-ˉ…℃^=。…、

-

=≡跨吟

■■毛 ■≡■=■■0■ 群斡冉…镭=■■■=△F…

β$■日】■〖■■‖

………………………… 攀…含休

户呻F…■它

图l3ˉ58反编译的结果

我们还是以/ap″movie为突破口进行搜索’同时打开jadxˉguj的反混淆开关’就可以找到与请求 定义相关的逻辑,如图l3ˉ59所示° 厂





…—

●触密 ‖

广■寸」



.蹿, .档



』 弓

°`

郎睁′ b

.-T酗??蝗§鸳鳞….. -≈ . 铲

■~=≡--.一-



鞠’国

ˉ尸

″ˉ- ˉ.ˉˉ

文件换■导n工贝阳助

隐≡蠢 !f。≥

`′●】…∏吐土l■



敷e】t…蛔』…I

坠喧〗Z…比厂

》矽〕$■…te汕■■t吭1■汀Wi .』eJ■呵~军 @】g咖丁呻1te厂

●n叫

oLα哪m马…「 @釉1…3jt◎了γ

,盂—_…

@….≈尸曰….=≡…一≡ˉ-ˉ-=——

·

≡_≡一Ⅻ—一~=

网臆烹r了

即』 噪^ 墅户

′ˉ醛堂ˉ一一ˉ

尸~→…苛

驴咀~≡—一铅

_一∏卒≈

0卓醇=已■■中

o≈兽…j呻1坐Fv』山蹲 -一--==≈—磊—-—-=±_—≈~≈→=≡=≈←=~口

■■ ·■=冗≈=≡摆~控世===生…凸=▲≡=—与=

{ {尸…·…』

){

雾=.

[ {{》……茹……·v…………惭《.·′………|雪…》

@№mγh≈却15碑S油1·

| 』 |

e№p丁γ…apfG汗“t◎厂γ

e啪te厂四ID止l喇」t1l■ 儡倘』酝《Ⅶ…fC「

o师唾r宁=▲b奄t皿1St唾

op7田1cDte

■荔西翱铀嚼麓萍吼甲角脉Ⅵ霉感扭肚已’

:严…『





其.

op唾工P ≥凰……………!

2opubu二hG厂

》oα归u山』5阳S砷le 》o中…宅「p凹▲b1但

po匹…b2C「』pmm

$ 拥,

![ 『.

》@肚↑‖臼ct』α叭f亡唾g◎「

p备贮7lect1γGⅣ…印te汗颤 ●腔tmf蚊[11坟T

$o…汹「m御1吧』』↑S b@肚脚锣1吨mS 3●灿t11■

B

>●脚1凸

@助↑哑t●『№1牵p

》o父珍10汇■ll立1● 紧e父‖…\e≈

…喀…啥…=…锋弓、`≡=≡ˉP熟≡…京$=≡-ˉ≡…

》e5e厂坦u】…= 巴

`咖c0……!●…_二ˉ E=

〃…… =…睁h巴

凸■

0





#凶



…亏

翻S丽蓟! . 息

.

.

.

`醒

. ^…辫肆. ..

ˉˉ

. … ′″ ·

斡≡. .=

图l3ˉ59搜索得到的结果

很明显可以看到’这里定义了—个1∩deX方法接收参数O仟5et` 11‖1t和tO代e∩’这和示例App

l3.4基于Xposed的爬取实战案例

637

加载列表数据时发出的请求完全—致: pub11〔j∩ter+a〔e‖oγie∧pi5erγi〔e{ 既[丁("/ap1/№γje")

Obserγab1e<‖ttp【e5po∩5e<"oγie[∩tity〉〉j∩de×(刨uery("o仟5et") i∩t1’刨uery("1j们jt") i∩ti2’

刨』ery("toke∩") 5tri∩g5tr)j }

可以看到它的返回结果是—个0b5ervab1e〈}{ttpRe5po∩5e<‖ov1e[∩tjty〉〉对象’为了Hook获取响 应结果的方法’我们可以试着搜索0b5erγab1e` ‖ttpRe5po∩5e和川oγ1e[∩tity相关的引用及定义° 例如搜索‖ttpRe5po∩5e相关的引用’结果如图l3ˉ60所示°





P

》p





『 p

p p



图l3ˉ60与‖ttp【e5po∩se相关的弓|用







一共返回了7个结果,这里我们分析_下第_个’其外层是reque5t‖etwork方法’该方法的定义



如图l3ˉ6l所示°











8】

9]

…ucm坦砸亡印t(0皿m●吨1e跑「)th面■酥…【t面{



》》°巴吨5c厂1逸(…〔田四峰丁哦t≡1唾∩t』w>≥(》《 /矽〔【m5c”趣gO】咙G嘲咖…』t·…U』·』哗月◇』′哗w1…l·们对刀匀



93



104



】14

1】4

P



】们d●xv』…“l锣tM∏°们】yb.?4nb°〔Du(》;

122

丁mZt低1($回$…冗(片宙…诅鹤》; 》 》0啼〔……庐Vh「…1=‖) 《 /本cI匹5c■.″I…·…r.…皿.1…·I…门Ⅶ…l·C·…O/

…ucU●』■■cC印t《沁『…hl●th) tN『…〔x‘ep〖1“{ Ⅱ…山】…`α…·f41抢·7鳃沁mu(》;

1Ⅱ〕

|}划



盯℃『n;

}. 113





I呻Ⅻ…1·mD·↑巧18仁·由d(…I…【t副』…1(Im●xv』斟唾l°thm pmV1●隘t1tγ》》〗

I睡xⅦ…l·tb』■·?但泌特; I∩“刚…lo津必.fq17b.7缚汕·田u()『

110

0



】…1…1.恤1●.门泌j■】Ⅶ了.庐tCO锄t()『

凹′《婶殴…↑》·酵…{》≥O》砸 ′ 他7《№v』e侣∏t』tγ…』唾∩Mty; jⅦr.啪f胆S0【t3《)》《

100





1?[jγ■丁.g●t〔◎皿t(》≥·] l

w





…【血U●涸■cc印t(毗t…巴四■…γ1画t止尸』γO厂》逾…僵Zcept1o"{

96





《《≈』…』to叮)讳1q7m7m》·』…(恤mo?0睡h0 thm·7幽』}·C…■(酗t』lS.5〔圃凶1e「Z丁心∩$『◎岿厂())·c…Se《咖』t1L∑.mC印t1m了r…S /本仁I″■c幻.幽!巾G·…沾hm.呻鲤』.』0…·【…xu!…“l.田…″



k



…厂亡■ZLMt({■【…占■笔凹It回》)

…c……唾m吃{) 《

』7 《恤m■t…0№■…央Ⅶ「…hle)《 丁O■■创丘1u□凸…代(《(∩e$…$e沛…ble》 t狗》·…o…)〗

1∑q

】∑S

》 }

图l3ˉ6l reque5t‖etworR方法的定义 通过名字’我们初步推测这个方法是用来发起网络请求的。另外’可以看到这里调用了图l3ˉ59中



638

第l3章Android逆向

定义的i∩de×方法’还定义了_个5ub5〔rjbe方法来接收一些处理回调逻辑。再仔细观察,可以看到 比较关键的a〔〔ePt方法’其参数为jγaI,是_个‖ttPRe5PO∩5e<‖OV1e[∩tity〉对象’方法内部调用jγar

变量的getRe5u1t5方法得到响应结果,然后用_个十or循环遍历这些结果,并把结果添加到 I∩dexγje州ode1里°

到这里我们可以推测,这很可能就是App获取了首页的 电影列表数据后,处理响应结果的过程,不然也不会有+Or循 环相关的逻辑°既然数据是通过jγar变量的getReSu1t5方法

辟』熬…

卜′℃″medf厂酮: /*/

8pubuCc1■$s 00tt…爸pO∩se叮≥{

获取的,那getRe5u1t5方法返回的一定是—个‖ov1e[∩tity列 表’我们可以进-步追踪’看看getRe5ut15方法是怎样定义

/*厂白…0f厂硒『己*/



pr』v■t●mtf3148a; /*尼∏…厂厂m『b*/

的,于是我们跳转到定义‖ttpRe5po"5e类的位置,如图l3ˉ62

p「iv■t●uSt叮≥?31帕b;

所示°

g

…止t……ˉ铆…皿t钒》 { 「●t凹「∩tM■·f3M9b;

10 }

注意并不能保证目前的椎测l00%正确’只不过正确的概率很大。

如果前面的推测是『确的’那我们通过Hook就可以百接拿到响应数据了。如果数据无效,再接 着进行分析和尝试°

我们还是在133节的XposedTest项目下尝试Hook’新建—个类’名字为‖oo炽e5po∩5e,同时还 是按照之前的方法修改包名`类名、方法名等,修改后的内容如下: pa〔促age〔o肌geI爬y。xpo5edte5t;

pub1i〔〔1己55 {ˉ|oo版e5po∩5emp1e‖∏e∩tsIXpo5ed‖ook[oadp3〔阳ge{

pub1i〔γoidha∩d1e[oadpac促age(X〔Loadpad(age.[oadpa〔代agepam们1o己dp日ck己gepara川) t∩row5丁hr咖ab1e{

j+〈1oadpac阳8ep己m‖.pac代a8e‖a爬.eq0a15("〔oⅦ·go1dze.们vv帅abjt0』)){ Xpo5edBrjdge.1og("什oo低edco∏。go1dze.们γⅧ∩abitpackage")j 「j∏己1〔1a55〔1azz=1oadpa〔代agep日r3肌c1as5[oadeI.1oad〔1as5( 〔o肌go1dze.‖|γγⅧhab1t。data.5our〔e.‖ttPReSpo∩5e")i Xpo5ed‖e1per5.十i∩d∧∩d‖oo代雌thod(c1azz』"getRe5u1ts"’∩e"X〔问et∩od川oo代(){ protectedγoidbe十ore‖ooⅦed‖ethod(‖ethod‖oo代par己Ⅶpar日")throw5丁hIowab1e{ Xpo5edBridg巳1og("〔a11edbe千ore‖oo代ed№thod")j ∏■

〕 』

protectedγoid日什er卜{oo艇ed付et∩od(‖et∩od刊oo低par己Ⅷpara川)thIow5「∩rowab1e{

」 ■ | | | · ‖ ‖ 』 · · ‖ ‖ 』 】 ■ ■ 〗 ‖ 』 ■ · · ` 』 | 」 | 』 · 】 ■ ■ ■ 司 ■ γ ‖ ‖ 』 ■ 】 』 ■ ‖ 」■】■{」引(」划||』■

1川portde.robγ·日∩dro1d。xpo5ed。Ⅸpo5ed‖oo促Loadpac代agej 加portde.robγ°a∩drojd·xpo5ed。X〔‖et‖od‖oo代j mportde.robγ。a∩drojdxpo5ed°Xpo5ed8Iidgej 1们poItde.robγ°a∩dro1d.xpo5ed。Xpo5ed‖e1per5j 1∏portde°robγ·a∩droid°xpo5ed.〔a11ba〔促5·X〔[oadPac代agej

司□‖‖』勺』】‖』■□■】]】』■■可|』■■】‖』』■■‖】】】■】‖】‖■■|

3.实现‖oo低

■]‖|』●』】■】』■】■可』■‖‖‖己口

可以看到‖ttpRe5po∩5e类使用了泛型’有_个占位符丁’ 13 仙bUc』∏tget〔ou∩t(){ 14 mt凹mtM■·「3148己β 这是什么意思呢?例如给‖ttpRe5po∩5e<「〉中的丁传人 } } ‖oγje[∩tjtγ类’这里就会变成‖ttpRe5po∩5e〈‖oγ1e[∩t1ty〉’ 代表‖ttpRe5po∩5e绑定的是‖Oγ1e[∩t1tγ类’包含的数据也 ′、◆`| |烂仁F…F…~彭厂~H=|…←←』』=’^’巴…≡…巴 图l3氧62 ‖ttp【e5po∩5e类的定义 和‖oγ1e[∩tjty类相关’而刚才往a〔〔ept方法传人的参数正 是‖ttpRe5po∩5e〈‖oγ1e[∩titγ〉对象,所以我们可以认为这里的丁就是‖ovje[∩tjty°还可以看到, 8etRe5u1t5方法的返回值类型是[15t〈『〉’所以获取的响应结果是[15t<‖ov1e[∩tjty〉类型的’是 ‖ov1e[∩t1ty类返回的列表。所以’如果我们可以HookgetRe5u1t5方法,其实就能拿到响应结果中 包含的‖oγje[∩t1ty列表数据了°

} 巴■厂『■■『↓■厉「■■「△尸 ‖尸‖| ■■■厂心=尸



l34基于Xposed的爬取实战案例

639

Xpo5ed8rjdge。1og("〔a11ed日千ter‖oo抿ed"ethod")j }

})j } }



这里我们修改了如下几处代码°

□将当前App的包名修改为〔o肌go1dze.们γⅧ∩ab1t。 □将1oad〔1a55方法中类的路径修改为co".go1dzeⅢγⅧ∩abjt.data.5ource.‖ttpRe5po∩5e°

□将+i∩d∧∩d‖oo长‖et‖od方法的第二个参数修改为8etRe5l」1t5,由于8etRe5u1t5方法没有任何参 数’因此直接往+j∩d∧∩d‖oo|(‖ethod方法的第三个参数传人X〔‖et‖od‖oo代的回调定义。

P

□这里的be+ore‖oo代ed‖et‖od方法和a十ter‖oo促ed‖et∩od方法仅仅是打印对应的日志°

巴■)〗■

另外,需要在xpo5edˉj∩1t方法里定义好这个人口文件’添加如下引用: co"°geI贬y.×po5edte5t°‖oo趴pI

匹β匣卜)

添加好后’先重新安装并启动XposedT℃st这个Xposed模块,另外App当然也要重新安装到手机

上并运行,我们来看看能不能成功HookgetRe5u1t5方法°重新启动App后,运行结果和往常-样, 如图l3ˉ63所示°

这时再打开XposedInstaller的日志页面’可以看到输出了这些日志 一一—

尸||■■

b

〔a11edbe十ore川oo倔ed"ethod 〔a11eda十ter‖oo促ed‖et∩od

〔日11edbe+ore‖{oo代ed例et∩od 〔a11eda+ter‖oo代ed№t∩od 〔a11edbe+ore‖oo低ed川et∩od

■ 尸

〔日11eda+ter‖oo促ed"et侗od



App的运行结果如图l3ˉ64所示。





、c

q,锣

·· ˉ

俊■王翻姬 ■』Q钞

墅圃簿`臣田

蹿 赡哦 触….

睹?7簿 O瓣丁杂

-

9.5

-

◆这" 〖蜘 ◆膘二丑′净

. 圃> a>

臼志

-

尸■「匹■「■■■·■仿■口■卜■伊【‖=■尸



ˉ-

≡=一=

| §

f阳a归

团睬瓣 ■器铲 盅:蕊k· 鹰撇."

幽爵鹏



√|

■■■『【卜

匿撇…, 熙器孟豆" 图l3ˉ63重启App的结果

『铆孰■0↑

’‘ “, ,‘ ,,

睡f/v憋‖罐l《hNp"?钒`2》;Bj喊吨印p肋鳞i酗〗“『UbⅨ日『`‘『Q越xpc峙S 叫〃创獭懒咖叭潞‖沤)`X∩@8ed‖心翱敏沁d`

!s〃蜘◎…《帕w2)ˉ“徊枷巳】》□酗j感慧Np呛s“j门腻a‖蜜潞『喊 盯咖』脚恤劝e《!6》哟.归“Bn:m陷iSAnj{『ue ‖『雁p↑c№tⅣ酬‖6↑1刀u5e(∏…?倒鳃 ‖↑‖x…喇h`g喊@{》60汹:『x』‖gm灿脯嚼lγ讽wS“γ钉S』砷Ca{喊

闷{/xp◎Sed|门刨翻e↑〈γ6Ⅶz》;DoW∏{O吝d“恤p.〃毗xpO瓣d‖向彻… i!『/…m}∩S帕↓憾06Ⅷ2)』upd抓0『↑g爪mu↑警$‖钠 ′8″γα胸0↑↑『W叫?6蹿5》ˉ6‖∩由∩0apphe碱0O涧co∩?剿酮KG.励w『γ〗↓》锚 0它{/γα{哪‖‖阳创《]6235)Xp酶“[蜜颧记儡铡

06‖/愿m◎$edS『jdge(】6235) }oed∩g∏ⅡD口u↓a霉f油γ)′Oat已)0…γ哑『◎ 1γ}/FxpO曰edB∩Oge(↑6油5) k四α∏ge!酶3comgO‖d∑PmvWV咖aM n|/X…m()6鲤铆洲…mcα∏鳞憾ze咖vWt〗ha颐缸k…



js{/佣咖"me(]6渔5)己‖监“印t「l爬‖S∧Ⅸtwue

;7|/[β抢№t】嘲↑β23旬.u3e咖Su爬7『al筋噎

"↓/凰mSedB『勋9e〈『

va锤m宙“6刨↓『

熊嘿搬‖

坤!/X沁sm(‖鳃s钟 玲‖/xpOβ韶(‖62鳃》

蔼似Xm$“(↑6姆砷ca{}eo睡『O『e卜0斡撇翱肌e↑‖“

,‘

竭{/x仰… l5ⅡNm…

熊翻…

佰咖…铡 随lj芳p◎s喇

〃γxm8ed

9, ,‘

膨vγC1‖蜘!!

『野wp!α]《j″(〗鹅0纠:xpOO刨晦蜘3D‖醚.

巾}/怎舶o8cdB『}d9e(‖b‖02〉;demb似end睡dxpO鳃d‖『》钒哉钉晦∩G(

『t‖/xm●刨(γ“c鸣总xp◎s图仁炽〗m侗帕ex獭5.Cbec仅ve『酗如

睡‖′慰ms“B〔憾qe《M月02).XpoSedγ锣巳jQI↑R智ep色a们eCO『`‖‖mje 淘l/∩u『Wme《v“叼‖s64B0I ‖「ue′!s∧"; t『Me

j‘″mcN哪vG{『6勾O立》.睦eu0`S日抛Qf咱!鳃

心‖/河pose训"孰a‖它『《?“02》苫∩O‖pekActM1γXpO蹿w瞧撼}硼喇翻.

图l3ˉ64App的运行结果

=■》





〗‖

』 ■ ] ■ ■

第l3章Android逆向

640

{《||

这证明我们成功Hook了getRe5U1t5方法! 4提取结果

我们现在回过头看看getRe5u1t5方法的定义: pub1jc [i5t<丁>getRe5u1t5(){ retur∩thi5·「3149bj





这个定义非常简单’我们最关心的就是返回结果,那怎么可以拿到这个结果呢?很简单,



a+ter‖oo代ed‖et∩od方法是专门做这件事的,我们可以利用它获取或者修改返回结果。这里我们不做 修改,只获取’所以把a+ter‖oo代ed‖et∩od方法的内容修改为下面这样: protectedγojda+ter‖oo促ed"etbod("ethod‖oo炉日r己们para阳)t∩row5丁hro"3b1e{ Xpo5edBrjdge。1og(‖!〔a11eda什er‖ooked№t‖od"〉j l15tIe501t5= (U5t)paraⅧ.getRe5u1t()j `



这里我们做了一个强制类型转换’将返回结果转换成了[15t类型,并赋值为re5O1t5变量,这

个Ie5q1t5其实就是[15t<‖oγ1e[∩tity〉°

那怎么把真实数据提取出来呢?我们可以进_步看看‖oγ1e[∩tity类的定义’回到jadx≡guj工具’ 搜索‖oγ1e[∩tjty类的定义’可以看到其中包含很多字段: 05er1a1jZed‖a们e("日11a5") pr1γate5tri∩ga1ia5j

05eIja11zed‖a∏e("〔oγer0`) Dmγ日te5tr1∩gCOγeIj 05erja1jZed‖3「∏e("dra"a") private5tr1∩gdra刚aj 05eria1j乙ed‖己爬("id脚)







日‖』··』■

@5erja11zed‖a刚e(』』〔日tegor1e5") pI1γate[i5t〈5tri∩g〉categorje5=∩ew∧rray儿j5t()j

q

另外’‖ovie[∩tjty类中还定义了-个to5tr1∩8方法,其返回值包含我们想要的很多字段信息: 刨O∩‖u11

pl』b11c5trj∩gto5tri∩g(){

■(《

retur∩5trj∩g.+or"at("‖oγ1e[∩tity{1d=%5」 ∩a‖e=%s’ a1ia5=%5’ pub1j5∩edAt=咒5’〔oγer≡%5’ dm∩a=%s’ 〔ategOrie5=%5’ regjO∩5≡%5’ 5〔Ore≡%5’川j∩0te≡%5}"’ I∩teger.γa1‖e0+(thi5.于41O1d)’ t}nS.∩aⅧe’ t∩i5.日1ja5’ thjs.p0b115hed∧t’ t∩i5。〔oγer’ thi5。dm阳a’ tMs.〔ategorje5’ t‖j5.Iegio∩5’ 「1oat.γa10e0+(t∩i5。5core)′ I∩teger.γa1ue0f(tM5.m∩ute))j 、







』|‖

} 、





‖」

prote〔tedγoida十teI‖oo代ed∩ethod(№t∏od‖oo哎p3r日"para") t∩ro"5丁browab1e{ Xpo5ed8ridge.1og(!0〔a11ed己什er‖oo促ed贴thod"〉j Li5tre5(』1t5二 ([j5t) para川.getRe5u1t()j +or(0bje〔to : re5u1t5){ Xposed8rjdge.1o8(o.to5tr1∩8())j 5tr1∩ge∩t1ty=o.to5trj∩g()j Xpo5ed8ridge.1og(""ovie[∩t1ty"+e∩tjty);

』■■■可||」■■口

我们可以逐个提取想要的字段’也可以直接使用to5tI1∩g方法获取所有字段°为了方便’我们 采取后者’于是按下面这样修改a「ter‖oo促ed‖et‖od方法的内容:

这里我们遍历了re5u1t5变量中的元素’每次都将当前元素赋值为变量o,这个o其实就是 ‖oγ1e[∩t1ty对象’我们调用它的to5trj∩g方法可以得到-个长字符串’这个字符串中包含1d、∩aⅦe、

」|

a11aS等我们想要的字段信息。

现在重新运行-下Xposed模块和App,并再次观察XposedInstaller的日志页面,结果如图13ˉ65 所示°

』』



} p

l3.4基于Xposed的爬取实战案例

p

64l



‖β











志中’例如第_条电影数据:



‖oγ1e[∩t1ty{1d二1’ ∩aⅦe=霸王〗||姬』 己11己5≡庐are"e11‖y〔o∩〔ub1∩e’ pub115hed∧t=1993ˉo7ˉ26’〔oγeI=∩ttp5;//po·们e1t‖己∩.∩et/们oγie/ 〔e4da3e03e655b5b88ed31b5〔d7896〔千62472。jpg046q"_644b-1e_1C′

dr日"a=影片借一出《霸王月|」姬》的京戏,牵扯出三个人之间一役随时代 风云变幻的爱′|R′叶仇°段′」`楼(张丰毅饰)与程蝶衣(张国荣饰)是 一讨才丁′」、一起长大的师兄弟, 两人一个演生,一个饰旦,一向配合犬

衣尤缝’尤其一出《霸王别姬》 ,史是谷满京城,为此’ 两人约定合演

一紫于《霸王月||姬》°但两人对戏剧与人生关系的理解有本庙不同`段 ′』、楼深余回戏非人生’程蝶衣则是人戏不分°段′]、搂在认为该成京立业 之时迎娶了名妓菊仙(巩俐饰) ,敛使程蝶衣认定菊仙是可耻的第二

者,使段小楼做了叛徒, 自比,三人围绕一出《霸王别姬》生出的爱恨 ′滑仇战开始随豺时代风云的变迂不断什级,终酿成』毯剧° ’



categorie5≡[剧悄’爱』肘]’ regjO∩5≡[中国大陆’ 中国谷港]’ 5〔ore=9.5’ Ⅷi∩ute≡171}







我们想要信息都包含在试里面!



咽↑7咖》:L』sem■Me7『辨

∩S{凸『}额w3瞬∩哪庐0Ac】Mtγx“各“γ盯己lmc刨ed ∏碱a!‖盯(w3“)O四at0吗『∏o‘`剧霹.枷{

∩钒a腮073O6)Dow∩↓硷呸“滩沙〃做Ⅸp蛇“∏‖蛔呐?u‖xw↑l0r 阶蝇↑7则鹏》Bj砸l∩gapp}「c孤如舵心Qwh嘴酬d∑遮JVwv叮晦蝉叮〖《c◎门g◎Q |wp‖(↑7邻q↓x卸臼却‖∑e∩d仰酗

}:獭{翻儿翻:d醛瓣;b臆`渊篇娜翻闭·d

(wq的l州。叶cdcαno°d正们wm∩ab『!pa吨刷ge (w4鳃)州 [w0”)卜《O呻edc四∏四dZ串Mwmhab『↑pachB卵 〔w4碉》』J$“B‖1 ‖「噬囚Alt t『Ue γ赠〈w贴09} useu∩S■↑巴7『B|8e

|启《!!蝴假〔 』{『《』qe《w4”)∩`Odl雌/“↑孵o芍铡w(Q.γ□…田蛔碱‖v饥uadma w硼瞥〉C肄}$刨be《o『钵…点酗触碳沁钞 w勒09}c刨‖喇6『l领牌“贞ed删敏h◎朗

]′A鹏)鹏oⅥee`Mγd坠]幅∩`e窒翰玉别谨a‖归5三「已…创‖陶Qo

〗7叮的}Mo·『e〔∩MyMov『e知hty恤=↑∩阎巾e=lq·跟a‖盔尸a陋 (w409) ↑7q拥)汹OⅥ唾问mV佃°z∩αγ】e营灌个睦。李不大冷‘a蛔锣k知\呻

/w』翻》Mov0e〔∩t‖y仙◎γ0e即啊(测ˉ2∩q∩]e画汉卜琴手 】 a日 (w啡”》M田!e〔^‖∩γ(『o辑3』呻We雪赡§申麓的救云′a‖『as岭釉eS↑冶W『 (|7锄钟MUW唾mnγ州o惟愿门狮vd翰息骸腮口胀身潞甲冤豁鞍raa$副 《w4蝴娜白γ盗F帆l‖y{0萨匀′门a‖TV隐患磷谨蹿冤锻′由‖|aS营丁晌Ⅷαp咖′° 《‖γA09)M◎『『唾∩mγ川◎We[∩r峨‖dˉ冯,∩思帅e 、曰;′=刁刊』髓丫‖ (‖了▲酗》MOv酝lw1γdS∩a扣詹嚣瓣础^臼S||‖e°ˉ∩mV凹∩α0“γ‖

}|腮)腮籍|"|睬;艘熊臀"慧;蛔具熬翻

5.数据保存

(w乌O9〉.Mo√泡[∩『wMⅦ健P∩t[酬减啪,川□me

二厕°谷淤

O己`

我们已经成功在手机端拿到电影数据了’还剩两个问题需 |{瓣憋藤腮腮瓣搬扁感隧鞍躺`渊 要解决—-怎么把数据保存下来和保存到哪里?

k

旷瞅《??瓣锄xp…d|e9∏Bb‖撼 ‖B『『则g芍《‖730b)dG『恤γ啼md〖D倒x”毒ed.‖∏『;‖a↑{e『‖B∏饭axm巴eo触 !(‖73“)『$64B0↑ ↑「岭BSA「0 t『屿

◇『‖■从∏飞‖『`■′■



可以看到’每条电影数据都成功被解析出来并输出到了日

《wA0锄栅◎v‖帐]l|y(划霉80门勃砧矿鹤憋′逛蔑出|‖日$奢丫he袄Ⅷp◎『cc丁}

{|′‘硼) 』…^…M°钒…{M{ds.…皇,磁雹疆铡|.爵 『馋|

(t7乌O9)旧o可e〔们‖『γ『d9na『wc霉f" 酗|冶

刮|己S霉丫》`e丁『u

"S

如果直接保存在手机上,可行是可行,但是不方便我们做撼|鹏‖{獭!赣撇患篷撼辙 后续的数据处理;如果保存到指定的数据库中,那我们还需要 从手机中进_步提取数据。两种方式好像都有弊端。

"’酗)c惠{}…………网 图l3ˉ65 日志内容

S

_个简单方便的方案是通过API把数据转发出来°我们可以自己搭建一个HTTP服务器用于接收 b



p

数据,然后在手机上通过HTTP客户端程序把数据转发到刚搭建的服务器上’服务器接收到数据后直 接人库。 .

那接下来我们就有两件事需要做°

□搭建服务器:搭建—个HTTP服务器,这个服务器可以接收HTTP客户端的请求,从请求中解



p

析出数据,然后将数据保存下来°

□发送数据:在手机上通过Xposed模块截获数据后’将数据通过HTTP客户端程序发送到搭建



) b

p









的HTTP服务器上。

●搭建HTTP服务器

我们可以使用_些轻量级的框架(例如Flask)搭建HTTP服务器°Flask提供_个支持POST请 求的API,能从请求体中解析出数据’然后做后续处理,代码实现如下: 十ro们于1a5|( i∏port「1a5|〈’ Iequest’ j5o∩j「y +ro‖1oguru1∏port1ogger 日pP=「1己5k( ∩a『∏e )

0app.rOute(,/data‖’ |∏et‖Od5=[ 』p05丁! ]) de十Ie〔ejγe():

data≡reque5t。千orⅦ.get(‖data‖)

p



















1ogger。debug(千0re〔ejved{data}0) retur∩j5o∩i十γ(5tatu5=`5u〔〔e55』) 1千

∩a爬

==

‖a1∩

:

app.ru∩(debug=『rue’∩o5t=00·o.o.00)

这个实现过程非常简单’就是从请求体的表单数据中提取出data字段’然后将其打印出来°运 行此Python脚本, Flask会默认在5000端口上提供服务’运行结果如下:

|↑3 h

尸■■■■■■■■『【巳

第l3章Android逆向

642

*5erγ1∩g「1a5kapp "5erγer"(1azy1oad1∩g) ‖AR‖I‖C: 丁‖js15adeve1op「爬∩t5erγer。 Do∩otu5eiti∩apIodu〔t1o∩dep1oyⅧe∩t° 05e己productio∩‖5CIserverj∩5tead.

*Debug『"de目 o∏

‖ {

*[∩γiro∩爬∩t: pIOduCtiO∩

*Ru∩∩mgo∩http://0.o。o.o:5000/(pre5s〔丁RL+〔toqujt) *Re5tartj∩gW1t∩5tat *Debuggeri5a〔tiγe! *0ebuggeIpI‖8 269ˉ657ˉ055



如果手机和电脑处在同一局域网下,用手机其实就能访问到该服务器了’调用客户端程序直接发 送数据即可° 0

如果手机和电脑不在同一局域网下’那么我们可以使用∩8ro低命令将电脑上的服务暴露出去: ∩gro挝http5O0O

●发送数据

那怎么在手机上发送数据呢?我们可以借助Androjd中比较流行的OkHttp库’其GitHub地址是 htms://githuhcom/square/okhttp°在Xposed爬stApp中的buidgradle文件中的depe∩de∩〔1e5部分添加 对OkHttp库的引用: iⅦp1e『‖e∏tatjo∩ 〔oⅦ。5quareop°o促http3:o促∩ttp:3·10°o0

然后在刚才定义的‖oo代【e5po∩5e类中添加_个5e∩d0ata丁o5erγer方法,方法定义如下: pub11cc1a55‖ookRe5po∩5ejⅦp1e爬∩t5IXpo5ed‖oo灶o3dpa〔kage{ pub11〔stati〔fj∩a1‖edja丁γpe]50‖

=‖ed1a『ype.parBe(0|app1jcatio∩/j5o∩j 〔∩己r5et=utfˉ8")j pub1i〔γo1d5e∩dData丁o5erγer(5trj∩gdata) throⅣ5I0[x〔eptio∩{ StIi∩g5erγer= "∩ttp://<5[Rγ[R‖05丁〉/data"; Reque5t8ody千omBody=∩ew「orⅧ8ody.8u11der() .add("data0』’ data) °add(耐+roⅦ"’ "Xpo5ed") 。己dd("〔r日N1edat"’ 5tIi∩g.va1ue听(5y5te川.curre∩t『j∏e"111i5())) .buj1d()j

0k‖ttp〔11e∩t〔11e∩t=∩ew0代‖ttp〔1je∩t()j Reque5treque5t≡∩ewReque5t.Buj1der() .ur1(5erγeI) .po5t(千or们Body) ·bu11d(); 〔1je∩t.∩eMa11(reql」e5t).e∩que(」e(∩eW〔a11b己〔惯(){ pub1j〔γoido∩「aj1ure(〔日11ca11’ I0[x〔eptjo∩e){ Xpo5ed8rjdge.1og("5aγe「a11ed: !0 +e.get‖e55age())i }

p(」b11cγojdo∩Re5po∩se(〔a11c己11’ Re5po∩5ere5po∩se) thro"5I0[x〔eptjo∩{ Xpo5ed8rjdge.1og("Savedsu〔ce55+u11y: "+re5po∩5巳body().5tri∩g())j

} })j }



在5e∩d0ata「o5erγeI方法中’我们首先声明了—个5erγer变量,在具体运行的时候,请把其值

中的5[Rγ[R‖05「修改成自己电脑的IP或者∩grok命令暴露出的地址°然后用OkH仇p库构造了-个 Reque5t8ody对象,该对象包括三个字段,其中data是字符串类型的数据; 千ro"是爬取来源,此处值 为Xpo5ed’代表数据是从Xposed模块爬取的; cra"1edat是当前的时间戳。最后新建了-个

《|■‖《』‖』‖」■]|‖】|‖■」』■〗』』■||日|』尸』●】‖』■‖』■■』‖■〗』■‖‖■Ⅵ|」■』■‖』】』‖]■】』‖』□』■‖』■』·]」■■|■■■旦■|‖』□■门■■日』勺】』■·‖勺』|‘‖』·□□/们■日」■』●‖‖|·

这个命令运行之后’会提供公网可以访问的HTTPURL和HTTPSURL’这两个URL和电脑的 5000端口相映射’这样即使手机和电脑不在同_局域网下,手机也能把数据发送给电脑。

| =■



l3.5

Frida的使用

643

【尸

0代‖ttP〔1ie∩t对象’赋值给C11e∩t变量’并根据5erγeI变量和尺eque5t8ody对象构造了一个Reque5t

`|‖

对象’发起HTTP请求。再修改—下a+ter‖oo代ed‖et∩od方法:

}}

protectedγojda什er什oo长ed‖ethod(爬thod卜{ookpara们p己Ia们)thro"5「∩roⅣ3b1e{ Xpo5ed8ridge。1og("〔a11eda十ter‖oo代ed问ethod")j [j5tre5u1ts≡ ([ist) Pam刚.get【e5u1t()i

十oI(0bje〔to : Ie5u1t5){ Xposed8Iidge.1og(o.to5trj∩g()〉; 5tIj∩ge∩t1tγ≡o.to5tri∩g()j Xpo5edBridge.1og("‖oγje[∩tjty" +e∩tjty)j



5e∩d0ata丁o5erγer(e∩tity); }









P



「 b

广

0







重新运行Xposed模块和XposedTestApp’这时Flask服务器的输出结果如下: 2021ˉ07ˉ1821:11:52·316 |0[8" |—阳1∩ :receiγe:10ˉreceiγed∩oγie[∩tjty{jd≡20’∩a|『|e=迁徒的乌’a11a5=丁he 「mγe111∩gBjrd5, pub115∩ed∧t=20O1ˉ12ˉ12』 coγer=http5;//p1.『∏e1tua∩.∩et/吨γie/a16〕4+4e49〔8517aeoa3e4adcac 6bodc』3994jpg“64w6“∩1e1〔’dm爬≡当鸟儿用羽义去实现梦想,翱翔在我们永远元法凭借自身企及的犬空’人类又 该赋予他们怎样的赞叹呢?‘‘鸟的迁彼是一个关于承诺的敌本,一种对于回归的承诺°’’雅克.贝汉以这样一匀话带我们踏

上了乌与梦飞行之旅°’categorje5≡[纪录片],Iegio∩5=[法囚’捻国’怠大利’西班牙’璃士]’5〔ore=9.1’m∩ute=98} 127。0。O.1ˉˉ[18/〕u1/20卫121:11:52]∩p05丁/d己ta川∏p/1.1■2OOˉ2O21ˉ07ˉ1821:11:52。317|D[BlL | 爬i∩ : Ie〔eive:10ˉ Iece1ved"oγje[∩tjty{id=18’∩a爬=海上钢卑师’ a1jas=[日1eg8e∩dade1pj3∩j5ta5u110ocea∩o’ pub1i5们ed∧t=2O19-11ˉ15’〔oveI=http5;//p0。雁itu日∩.∩et/加γie/6O9e45bd4O3』6eb8b927〕81be8+b27a6176o914.jpg0 464w6‖qh1e1〔’dm∏a≡190O年的吊一天’往返于欧吴两地的坏轮γjrgj∩ia∩号上, 贞计邮轮上添加煤炭的工人丹尼·博 雄员 (比尔.努恩饰)在头干抢上欲捡拾有伐人残留下米的本物时, ●















可以看出’在手机端获取的数据已经成功转发到F‖ask服务器上了!后面我们只需要完善-下 Flask服务器的相关逻辑,对数据进行处理并保存即可,具体流程这里不再展开讲解。 6.总结











本节我们通过实例讲解了利用Xposed模块Hook关键方法的实现过程’利用Xposed模块’我们 可以成功拦截想要的数据,还可以对数据做进一步处理’将其转发到电脑上保存起来°

有了Xposed,我们几乎可以Hook所有方法来截获想要的内容,App尽在我们掌握之中’“为所 欲为”不再是奢望,爬虫自然也不在话下。

本节代码见https:〃github.com/Python3WebSpider/XposedT℃st°

















↑3.5 「「|da的使用 在13.4节和13.5节,我们了解了…刨的基本用法,可以说只要找到位置,就能通过Hmk拿到

数据。然而Xmsm是具有局限性的,例如它只能HookJava层的逻辑’不能HookNatjve层的。另外, 整个Xp醋m模块的逻辑需要使用Java语言实现,如果我们对Java不熟悉,那么实现起来会有_定难度° 什么是Native层的逻辑呢?简单理解这就是使用C/C++编写的一些逻辑°假设某个App中的某 些算法是用C/C+十实现的,它们最终会被编译到_个so格式的文件中’Java层可以直接调用该so文 件执行对应的加密算法’而无须知道文件内部的具体逻辑°Xposed是用Java实现的’可以HookJava 层的逻辑’但对于‖ookNative层的逻辑,就无能为力了°

本节我们就介绍另外—个简单好用的Hook神器—F∏da!如果要用几个词描述F∏da,那就是强 大、方便、灵活°

↑.「∏da的简介

F∏da是一个基于Python和JavaSc∏pt的Hook与调试框架’是一款易用的跨平台Hook工具’无 论Java层的逻辑’还是Nativc层的逻辑,它都可以Hook。F面da可以把代码插人原生App的内存空 间’然后动态地监视和修改其行为,支持Windows、Mac、Lmux、Android、iOS全平台。

F∏da是使用Python注人JavaScript脚本实现的,可以通过JavaSc∏pt脚本操作手机上的Java代码, Python脚本和JavaScript脚本的编写跟执行是在电脑上进行的’而且无须在手机上额外安装APP和插件’ 所以整体实现起来更加灵活和轻量级’调试起来也更加方便°而Xposed需要使用Java实现—个模块’ 然后编译并安装到手机上’灵活性相对差一些’但如果要做持久化的Hook’还是推荐使用Xposed。

●Xposed的优缺点

【■

优点:非常适合编写Java层的Hook逻辑,因为自己就是用Java语言编写的;适合_些持久化的

‖|‖

下面简单列-下Xposed和F∏da的优缺点°

‖‖|二■■■■〗‖‖』‖□==■‖||‖■■‖|‖‖■■司{』】■■■■■□■■□■尸々‖』·】』■■∏■□□■■■■口

第l3章Android逆向

6“

Hook操作’编写完毕后可以独立且水久地运行在手机上’适用于生产实践°

●Frida的优缺点

优点: Java层和Natjve层的逻辑都能Hook;在电脑上编写和执行脚本’修改之后无须重新编译

··」■■■〗■∏』■‖|■·』

缺点:配置环境的过程比较烦琐’在调试过程中需要编译和重新安装Xposed模块’对HookNatlve 层逻辑无能为力°

和额外在手机上安装App,操作方便又灵活;环境配置简单’能很好地支持跨平台° 缺点:是用JavaScnpt操作Java逻辑’所以兼容性会差—些;更适合在开发阶段调试时使用’不 太适合应用于生产实践。

2.准备工作

□在电脑上安装好f丁idaˉtools’并可以成功导人使用° □在手机上下载并运行f丁jdaˉserver文件’即在手机上启动—个服务’以便电脑上的Fnda客户端

月■■□门』□司叫

请确保已经配置好F∏da的环境’并能成功在电脑上用Fnda连接到手机,具体有3个要求°



程序与之连接° □让电脑和手机处在同_个局域网下’并且能在电脑上用adb命令成功连接到手机°

具体的安装方法可以参考h忱ps://setup.scrape.cente门fma。

+rjdaˉp5 ˉ0

运行结果类似图l3ˉ66所示的这样。

|』

伯 ˉ攀 ·.

‖句|』■■□□□〗』■|」‖‖』】■可■=司二■■司《`|‖】】■■■‖||

以上准备工作做好之后’就可以在电脑上运行十r1daˉp5命令查看手机上运行着的App进程了, 命令如下:

≈ 了广t回α津p已 ·U

p【0‖钾旧 r的◎咖α

83Z

α倔d宁O1d,p于o〔e匀5.可弛d↑o

1』33 (弓mLd“7o飞d尺嘻VC沪口m 皿e2 〔o咏〗.α∩d徊〗°p「〔jVⅥO鸣广导.c画!e∩“7 ′31 也o闷°o∩d7o1dW>tG严刨【

l765 笆◎们】,辨『嘲毗cγˉ●ppDα民i〔2 B76“bUgge「d Z78

q

哦伊枷瓣厂γe7

181g什idαˉ已e厂V怎厂1迅.2.1bˉ口∩碰o1dx86



‖{

283鳞tekeepe『d 晦9h瞳α`恤d 1 i∩it 280 i“寸□1M

281舶ystoFe ∑c1

t嘛β

耘IQ<O1.ˉ〔α倔e『.αD回C汝

26S loCGI噬p已



· 』 ■ ■

18g1 1Og咆毗 113 I◎gd £7g pieGi口SeTγe了 鳃3咖』…b毗

手机上运行着的App进程

□■∏纠■当■■‖

图l3ˉ66







l35

Frida的使用

645

■ ■ 尸 『 厂 ■

□=■「■■尸

|‖尸|)|位口■尸|



『}β ▲■「‖‖‖▲冈』【尸〖□β■「|『■

‖ ■■『卜‖【▲尸‖|■庐

雨』|

^ppBa60C‖

本节接下来会以两个简单的App为例,讲解Fnda的基础 使用方法’所以请先下载并安装这两个APp° □AppBasjcl: https://appbasiclscIape°centeⅣ°

□AppBaslc2: https:〃appbasic2°scrape.center/。 3.∩oo促」ava层的逻辑

首先’我们把下载好的第_个APp安装到模拟器上’该 App启动后的页面如图13ˉ67所示°



整个页面非常简洁’中间有_个I℃St按钮,点击该按钮,

会出现丁Oa5t提示信息’内容为3。这其中的逻辑是怎样的

呢?我们可以直接用jadxˉgui反编译一下apk文件,从源码中 查找人口’如图l3ˉ68所示° 可以看到源码非常简单,整体逻辑就是点击按钮后触发 O∩〔1jc盯e5t方法’然后这个方法直接调用『oa5t的肌aRe『ext



方法,显示get‖e55日ge方法的返回结果。这里get‖e55age方 法实现的是基本的加和操作’因为在调用时传人的参数是1和

图l3ˉ67

2’所以显示的丁Oa5t内容就是3°

AppBasicl启动后的页面





_







||

驾卯琶工】·… O凶■代田

·函.俏回.费↓c1l≈哇tMtγ×

》●m”m·…穴.m1叫

声…尸……i怎l;

户■酮门』血 狞■c唾

蹿圈硼:圃鳃凰

■■吠回愈…Zm1

●刚n…7」g ●〔…

●■■■■■■■ ●…S…(mt0 上∩t》

p





…_

●尸

=广





—知翘

_鳃…_

一密=

咖_广



工意耳

■—翻



…_…

●倪

●文声

_驻

匹β|尸△■





控制台输出了手机上运行的进程’证明电脑和手机连接 成功!

●呵Ⅷ…t(Ⅶ印)Ⅶ ●吓厂田te(山咽1e)m1!

■【■「 ■ ■ 尸

四》》》户

已 「

91G.αⅦ「◎四.≈te丁皿1 | ●…℃·■

●… Ⅲm件

出睡吟” 凶厂m

怕5t.四l 磁幻门的』…』 迅clm已唾.尘x 「G…∏汇e乞·■7Zc ■『●乞山穴eS.■

蹿§岂醚≡谣.………v』叮』 酗噎 菏顾《h唾

.

钾■7m巾〃…』申口“t』皿嘲■…止tjγjry』剧■】dⅢ·〔w■,…0〔…咋t』邱ty夕…“咖′…$·…己 I】……辆口■《…1●…凹》 { 〗□ …·……《…l●》〗

■…蓟t…〔…□l…t°“t1■』RY尸m);

14 |



17

≈0T咆■…ucⅨ丁■Mγ』≈v』≈』 《

〗,{ 》 γ…m…膛|c…′0G…尸(」.","…"; 12面…已t′lm碗……(蚀仕』Omt皿》 {

咀■』钾■tU淀 ■■厂|■『‖

2j| 》…m碱………(l·皿〕; {}

■■■■■■■

↑3

■■

■■■■■■■ |

β‖}卜











≡…`、 ……

=-←■

诡蜀0 s吨!‖

图13ˉ68反编译的结果

那怎么进行Hook呢’我们可以定义这样一个JavaScript脚本: 〕aγa.per十or川(()=〉{

1et‖己1∩A〔t1γ1ty=〕日γa.u5e(!co‖.8emey.appba51〔1.∩ai∩∧〔tjγ1ty!) co∩5o1e.1og(`5tart∩oo代』) ‖a1∩∧〔tjγjtγ.get‖e55age·iⅧP1e爬∩tatjO∩= (日rg1’ arg2) ≡〉{

第l3章Androjd逆向

646

5e∩d(‖5tart‖oo低|‖ ) retur∩ 060



})

加和操作’而是直接返回了数字6,这样就完成了方法的改写—不使用接收到的参数,直接返回数 字6。

Hook逻辑定义好了,怎么让它生效呢?使用Python脚本调用即可’于是新建一个hookˉjavapy文 件,文件内容如下: j们portfIjda mport5y5

‖、|‖·‖‖』‖■■■■Ⅷ□】』∏■∏

将其保存为hookˉjavajs文件°这里我们编写的是_个全局可用的〕aγa对象,通过调用其per+orⅧ 方法来实现我们的Hook逻辑°首先调用]ava对象的u5e方法获取指向‖a1∩∧〔tiγ1ty类的指针’并赋 值为‖aj∩∧〔t1vity°然后改写‖a1∩∧ct1vjtγ中的get‖e55age方法,由于这个方法接收两个参数,因 此这里也写两个参数—己rg1和arg2,分别代表源码中的1和12,但这里我们没有对arg1和arg2做

0

门|」■|』■■』●■■‖●■‖■■‖』】』■‖{‖`

〔卯[≡ope∩(‖hoo促-jaγa.j5』’ e∩codj∩g=』ut+ˉ80 ).Iead() p∩叹[5S‖∧N[ = ‖〔咖.gem论y°appba5j〔10

de「o∩-眶5Sage(爬55age’ d己ta): pIi∩t(爬S5a8e)

proces5=十I1da.get-u5bˉdeγj〔e()。attad](pRm[55‖酬[)

5〔ript.o∩(』雁55age ’ oC"e5sage)

s〔ript.1o己d() 5γ5.5tdj∩.read()

ˉ:舔

AppBa哪c↑

》韶



这里我们首先读出刚编写的JavaSc∏pt代码,并赋值为 〔卯[变量’即把代码转成了Python字符串’然后声明了_ 个包名’并赋值为pRⅨ[55‖州[变量°

当前连接的设备’并调用设备的atta〔h方法挂载了对应的

进程’该进程被赋值为prO〔e55变量°之后我们调用prOCe55

变量的〔reateˉ5cⅢ1pt方法往进程中注人了Hook脚本(就 是传人〔卯[变量),并将返回结果赋值为5〔ript变量°

□■〗‖

■§■

对于5〔rjpt变量,我们可以设置事件监听和回调方法’ 例如这里监听message事件,回调方法设置为o∩ˉ『肥55age, 这样_来, JavaSc∏pt代码中任何通过5e∩d方法发送的数 据’o∩—|∏e55age方法都会接收到对应的内容,这就实现了 JavaScript到Python的消息通信°最后,调用5Crjpt变量

‖·||」∩‖|』‖·』口|ˉ』□〗|

接着我们使用mda包中的getˉu5b—deγice方法获取了

日‖』当□(』·□旦■□‖』]|可|(·

5〔rjpt=pro〔e55。〔Ie己te5〔rjpt((卯[)



的1oad方法注人脚本。

接下来我们先启动AppBasic1,再启动编写的Py‖hon



脚本:





pytho∩3‖ook一jaγ3·py

图13ˉ69Hook操作后的AppBasjc1

此时点击TEST按钮,页面如图l3ˉ69所示°

启动页面

〕t代码中定义的返回值,证明Hook成 可以看到这阻显示的「o3st信息是6,正是我们在Jav【 6,正是我们在JavaSc∏pt代码中定义的返回值,

显示的内容如图13ˉ70所示。 功了!同时观察—下电脑上的控制台’显示的内容如图13



d







d



『 p

Frjda的使用

l3.5

p

647



从这里可以看到,每点击一次按钮,控制台就会输出_行代码,代码内容为:



厂`臼 {0type| : ‖5e∩d’ 0p己y1oad,: 05t己rt‖oo促! 0}

b







p

这里pay1oad的内容就是我们在JavaScrlpt代码中使用 se∩d方法发送的消息内容’代表我们在Python脚本中成功 接收到了这个消息,实现了JavaSc门pt脚本与Python脚本的

瞎■5巳‘

………鳞. …』

通信° 如果我们能Hook某个方法的执行结果’然后通过

卜卜

JavaScript代码把它保存为某个变量’再利用5e∩d方法把这 个变量发送给Python脚本’Python就能成功获取代码的返

| ‖

回结果了’之后对结果进行处理和保存’数据爬取就完成了° 4‖oo代Nat|γe层的逻辑 、卜』∏卜■◇卜〃 ■卜』

β

现在我们尝试用F∏da工具HookNative层的代码,即



》『

so文件中的方法。先来看一下AppBasic2在Hook之前的启 动页面如图l3ˉ7l所示° ●■亡◆

Pyt∩o∏∩oo义jαvα.py

(py37) 「mdp门P呵, ●『『■β=尸■『△■■【■「‖▲■尸‖仆止『β

‖[

e∩d0 ,n ′pαyIoαd『 : ,5mFt‖Oo揽! 0} {’type! : 『5e∩d『 e∩d09 『poylmd; !5tαFt"oO席! 0} { {,typ诌! : ‖5e∩d,, {vtype,; 05e"d! , 0pαy1oαd『 : ’5tα庐t№o促! ,}



v5tα「t‖oOk! ,}

{『type『 : 05e∏d, , 0p□y1Oα创,:

图l3ˉ70控制台显示的内容

图l3ˉ7l AppBasic2的启动页面

同样地’使用jadxˉguj反编译apk文件,查看逻辑人口,如图13ˉ72所示° `

—鳖

产■尸…。……』



!. ˉ1…℃坞■…心



Ⅱ■

●庐…】mp ●庐……】nt· 』m) ●…】 ●…uT■…t{v皿 !定…t【v…)m

●亩=二!尸 ●亩=二《后 △·亡》■』 △·■ )

l2

、.■圃〗O·□m皿◇…f ■闷1e口…抢鲍◇…厂止l T■…

●=瞬 D■l汕 D凶叼≯w

/◆…■■……∏=……■叼叼

…w…〃…l血·…』vⅡ师·…山0』∏』叮·…±p… ●” =唾mβ1m 16

1丁

】■硒

1■

■囤■…M■t.■■l ■〔….…

…→_≡0

ˉ ˉ.△必……………〗 《 …·■……06 …=………■西t加…皿》J 》

』 ■ ■ 尸

p■……·●…

】】

】】

§

………Ⅱ…t《v年v…》 ( …仑~叮可《…o-‖1o R》0 1》·彰); l

「 躁

=吧





可知

6 ≥

←~

●「价卜|■「||『卜「尸、门】





;字

=→琶》静宁≡

图l3ˉ72反编译的结果

‖〖『{|||}||′}‖}‖|『【『『||『卜Ⅸ【「卜【·。。喉『『}腹『尸佳}『『°ˉ『】『遁

■《■··》由u

觅■↓’『弓↓‖ ■ · ■ · 〗 『 ‖ 』 ● 【 ■ □ ‖ ‖ § ■ 】 】 Ⅵ 司 二 刮 叼 』 列 』 ′ ‖ ‖ 】 { ‖ 』 包 司 『 』 ■ ■ 】 ‖ 』 ■ 】 ‖ 旧 〗 ‖ |

F●卜』.略〔!二扩 0| 『

尸饵■…■≈



[●乙…一°……叮∏」

凹≈

.?g…鹰2 o●…



》匹~≈』全

_臣

岁■…汹.….… √■c■

| --一二 写-ˉ ˉ气

‖‖『■■■伊【【〗■■□〔■

β卜▲■■β『■◆·β■■『’|

呜 廷……亚2·… 凹…

_…



_

……—…

p

Stα「t‖oo度

雷警]



h

↑3

可以看到‖ai∩∧Ctjγity类中声明了一个∩at1γe方法’叫作get例e55age’其参数也是i和j2, 但是这里并没有它的具体实现°紧接着的实现也很关键: 5t己t1C{

5y5ten1o己dljbrary(00∩atiγe"〉j }

这里通过5y5te"类的1oad[1brary方法加载了—个natjve库,其实就是加载了-个Natjve层的so 文件’所以源码中应该有对应的so文件,在源码中仔细找一下’是可以找到的,如图13ˉ73所示。 Ⅷ

●◆●

咽酮衍…↑罕蛔∏.“

文件钥■Qα工尺■■



醚‘蹿鳃.…膨寨幽

^…

酶疆匣n酞翻…!…簿!旧ˉ晦 皑■c……〕画飞硒

ˉ●…°≡.=虐m…1叮×」

γ钞…

D■…m田·…穴·…拙 】■…芯』qT



√■Cm

?嚣…c〗 》●匹:二二

!

!,

`●巾』…t1U』叮 | ■ O ● ●

嚣撬醋剿} Ⅱ;

每| ‖

n■…u°…■皿.m·丁血1|

』■`』

!

●…

】0 】7 】0



胖『巴‖『《$℃〖

》锄硒

擅钢吓毛≈止佑St°■l

Ⅷc1■■….… 》■厂…『…令■■C

||}|∩‖「吼司■‖■



2』

”8C口陛爪v…《〔…·…亡·●氏』Ⅶt…皿》;

}『

{` }

芦uc……1』c盯“t(v』凸γ』■}《

∏】



了■■0。■■饥面0〔…’铆生a·申(】p 2》o 〗).…《)β

}』} | } | |



尸狮0蛔t仙冗

驴■凸■‖■■『口

||

·《]‖■∏■■Ⅵ‖||』■呵||』■■Ⅵ』■■∏‖|‖』■■γ】‖■‖』■·‖|』‖·可‖‖(』勺■】‖‖‖』■■`●]●‖■□』■】■Ⅱ〗〗〗‖』■‖‖■】□‖』』·‖|□】‖』■■‖·』〖】■‖‖■】‖‖·『·‖‖二■可‖」■〗‖』■■

第l3章Androjd逆向

648

| |







『 Qˉ=

『■沮

=~■

…T 二_ __≡… 菲_

==≡

甘^…由◇℃

…mm[

】』』|』



_一_■_~-—=

图l3ˉ73Native层的so文件

可以看到这里有好几个so文件,它们适用于不同平台’名字都是libnatjveso°对于so文件,jadXˉgUl 就无能为力了,因为这是由C/C{ˉ+编译成的文件, jadxˉguj没法通过反编译得到其源码。

那能用F∏da进行Hook吗?能!我们来修改_下get‖e55age方法的返回结果。同样先实现一个 JavaScrjpt脚本: 〕aγa.per+om〈+u∩ctjo∩(){

I∩ter〔eptor·attach(肋du1e.+i∩d[xportBy‖a爬(01ib∩3tjve.5o! ′

!]aγaˉ〔o‖Lger爬yˉappba5jc2ˉ阳1∩∧ctjγ1tyˉget"e55age!)′{ O∩[∩ter: 千u∩〔tjO∩ (己rg5){ 5e∩d(0hoo促o∩[∩ter′) 5e∩d(|arg5[1]=` +arg5[2]) 5e∩d(‖arg5[2]=0 +aIg5[3]) }’ O∩[eaγe: 千u∩Ct1O∩(γ己1){

5e∩d(’∩oo低o∩Leaγe‖) }

v己1.rep1己〔e(〕己γa.γ∩lget[∩v().∩ew5trj∩gl」t+(05‖))

}) })

将其保存为hooknativejs文件°跟HookJava层时的逻辑不同,要HookNative层,需要利用 I∩terceptor对象的attach方法’其第一个参数是指向‖atiγe方法的指针,第二个参数是Hook逻辑 的实现.





■■■■■■■·■『止可■·■『‖■■■■】‖■■■『□□『■■·‖‖|【·■厂‖‖『『■■『〗·【■■尸‖』『‖■■『}‖|‖■■尸【‖□「■尸卜〖‖‖‖■「凸||〖■【尸‖‖‖卜旧■【『『■「巴旧『‖{卜【「■|‖∩||●

■「|卜「匹■『[卜‖‖【■『‖「▲■「伊『|■ 【「=尸



[ ■ 『 『 巴 ■ | ■ 厂||·【尸‖| ~■『■=

Frida的使用

649

□对于第_个参数’这里直接调用0们d01e对象的+i∩d[xpOrtMa爬方法获取了指针,该方法的第 -个参数是so文件的名称,这里就是limatjve.so;第二个参数是符合一定命名规范的方法路径’ 开头是Java’然后是包名,注意包名中间的连接字符变成了下划线’接着是被Hook方法所在的 Actjvity的名称’这里就是MajnAcnvjty,最后就是方法名称,这些内容都通过下划线连接° □对于第二个参数’这里我们定义了两个Hook方法,其中o∩[∩ter代表被Hook方法执行前的 逻辑, o∩[eaγe代表被Hook方法执行后的逻辑。o∩[eave方法的参数是γa1,代表被Hook的

方法,即get‖e55age。根据图l3ˉ7l, get‖e553ge原本的返回结果是3,这里我们调用γa1的 reP1a〔e方法,将其替换成了5’实现了返回结果的修改°

然后调用这个脚本’新建-个hooknativepy文件: 1呻ort+rjd己 j∏Port5γ5

〔0O[=ope∩(|hoo促ˉ∩atjγe.j50 ′ e∩〔odi∩g詹′‖t+ˉ8|).read() pR叹[55‖酗[= 0〔o∩↑.gemey.己ppb己5i〔2′ de千o∩ˉ贬55a8e(爬S5age’ data); pri∩t(眶55己8e) proce5s=千r1da。getˉu5bˉdeγi〔e()·3tta〔h(p日O〔[55‖酬[) SCrjPt=PrO〔e55.〔re3teˉ5CriPt(〔卯[) 5〔rjpt.o∩(』爬55日ge ’ o∩ˉ爬5sage) 5CrjPt.1Oad() 5y5。5td1∩。read()

这里跟HookJava层时的不同体现在JavaSc∏pt文件的路径和App的包名上’其他完全_样,这 里不再展开讲解°

重新启动AppBasjc2’同时启动该Python脚本: pyt∩o∏〕hoo促-∩atiγe·py

此时点击TEST按钮,页面如图13ˉ74所示。

可以看到「oa5t信息变成了5,同时控制台的输出内容如图l3ˉ75所示° ◆t扩9

■ ■

…■伍2

|↑3= …●



l3.5

图l3ˉ74Hook操作后的AppBasic2启动页面





b

(py37) pyjO

『『·.“D P广旧 αD儡扔@

{,type ,type v : pse∩d0, {0tγpe 0tγpe‖ 8 0Se∏o0 0 {0type 0type0 : ,5e∩d‖ ’ {|type 0type0 : 05e∩d0 ’

;|

‖◎∩hoOk≡∩◎t pyt"o∩hooⅦ≡∩◎t1ve.pγ

0pGy1Ooα』: ,hoo啦o∩[∩teT!} 0p口yIomQ 8 ,口「g5[1]=0X1v} 0pαyIo□d0 吕 ,αFg二[Z]绚xg,} 『p□yl◎αo, : 0hOo|《O门leαγe0}

图13ˉ75控制台的输出内容



第l3章Android逆向

一样地’这里的pay1oad值就是我们在JavaScnpt脚本中使用5e∩d方法发送的消息内容,我们在 o∩[∩ter方法中调用5e∩d方法’发送了arg1和arg2的值,然后Python脚本成功接收到了这个消息’ 实现了JavaScnpt脚本与Python脚本的通信。 5.总结

方法可以参考官方文档htms:〃fTjdare/docs/home/° 本节代码见https://githuhcom/Python3WebSplder/FridaDemo° 本节内容的参考来源° □F∏da官方文档。

□CSDN网站上“Androjd逆向之旅—Hook神器F前da使用详解,,文章。

最后,如果你想深人学习F门da’这里推荐_本书—陈佳林(网名r0ysue)的《安卓Fnda逆向 与抓包实战》’这本书讲述了利用Frida进行AndroidApp逆向和抓包的相关知识’可以学习_下。

↑3.6 SS[尸|∩|∩g问题的解决方案

Ⅵ■■□『司‖‖■■■‖」■‖|□■』■■】■〗〗〗】■』■∏(||」■∏■■可|‖』■Ⅵ‖]■〗|‖●】|』■■〗■〗』■||■■■

本节我们使用FndaHook了Java层和Native层的逻辑,通过这两个基本的案例’相信大家可以 初步体会到F门da的基本操作和API的编写方法°当然’ F∏da能做的远远不止这些,更多的API使用

■Ⅷ可|□∏」■■Ⅵ‖〗‖‖■■』·可‖■■■■】■‖』■■■】〗】∏■■∏|{

650

在第l2章中,我们了解了App抓包的相关内容’但并不是每时每刻都能顺利地抓到包°在某些 到最终的结果,报错信息一般跟SSLPinjng (证书锁定)有关系°

本节我们就具体了解—下什么是SSLPjning以及怎么解决这个问题°本节的解决方案和Xposed、 F∏da有关系’正好我们刚学习了这两个工具,因此也可以加深对它们的理解°

↑ˉ实战案例 为了更好地复现SSLPjnmg场景’我们对_个 App(https:〃app4scmpe.centeI/)进行抓包,这个App 里包含SSLPining的相关设置’如果我们将手机的

代理设置为抓包软件提供的代理服务,那么这个

●…|

蔓嚣嘿 翘荔霞爵

95

,‘

■迅…

,二m.E■.厉定、绒◆

95

m:臆口·

,,

应的数据便会加载失败。 首先,在手机上安装这个ApP,此时的手机没

接下来就要抓包了,我们还是以Charles为例, 当然用其他抓包软件(如Fiddler)也可以°在电脑

上启动CharleS之后,确保手机和电脑连在同一个局

·, 95

,‘

图l3ˉ76示例App正常加载数据

域网下’然后在手机上设置Charles的代理,具体的 配置方法见121节°

然后重启手机,重新打开App’会出现“证书 验证失败’,的提示信息,而且不会加载出任何数据’ 如图13ˉ77所示。

●证…失败■ 证书! 书验iⅡ失败

||||

如图l3ˉ76所示°

的证书,从而直接断开连接’不继续请求数据,相

95

■■■■‖||‖·■■■‖|』■■Ⅵ』■■■·■』■

有设置任何代理,可以发现数据是能正常加载的,

■舶蜜. 至嚣嘴露Ⅺ 圈器盟震" 幽熊嘴蓄.

App在请求数据的时候会检测出证书并不是受信任

■■可·‖‖·■‖■■|‖《||·勺』日二■■《‖·‖司■■|‖|·■可||□∏■

情况下,我们可能会抓包失败’一个比较典型的现象是包能抓到,响应状态码是200,但就是获取不

图l3ˉ77 ‘证书验证失败’,的提示信息 q

(■



=冗

| 巴■「‖|

l3.6 SSLPinjng问题的解决方案

65l

■■广|‖『『■■「|

与此同时’Charles的抓包结果如图l3ˉ78所示。

■■■「位■■「|β■厂 迅″

一一

≡~

=—



—≡

ⅦSv】2 n■Ⅵo2

…惮

构= 了==



ˉ讹辱

—≡

″≡

∩窒谷型

β



卜‖伯|



β 》 『 》 卜 膛■「户△■‖门·「卜■■■「β■厂 隘鲸 ■■『|尸匹■「|[■[■『‖β■■|巴=■[■『‖|匹卧「■■厂||■「日伊■『}

鳃兰兰… 慰扩



图13ˉ78

Charles的抓包结果

可以看到这里报了_个错误的原因(Failure):ClientclosedtheconnectionbefOrearequestwasmade. PosSiblytheSSLcerti6catewasrejected.°

此时如果取消Charles的代理’然后重新打开App’就又能成功加载数据了。

以上展示的就是SSLPining导致的抓包失败现象’为什么会这样呢?下面我们具体了解一下其中

仆『=庐

的原理°

2.SSL尸‖∏|∩g技术的原理

SSLPjning是一种防止中间人攻击的技术,只 针对HTTPS协议°在遵循HTTPS协议的数据通信 过程中,客户端和服务端在握手建立信任时’有一

步是客户端收到服务器返回的证书’然后对该证书 进行校验’如果这个证书不是自己信任的证书’就 直接断开连接,不再进行后续的数据传输’这就会 导致整个HTTPS请求失败。

为了更好地理解其中的原理’我们在电脑上做 一个小实验’打开百度首页,在测览器左上角看一

如图13ˉ79所示° 下证书的信息’如图13ˉ79所示° 点击‘证书’,’可以看到证书详情’如图l3ˉ80

×|+

●,≡攀 |麓盲度←下,“蛔 +, =≡一≈

Ⅲ多

断闽

「百 k

‖园…蜘

冷■7几

出d』尸

{:占谣… 沼 ∏ 闷酪g翼沪

ˉ″ˉ’ `







!





b



ˉ…

图l3ˉ79百度首页的证书

所示°

可以看到证书的签发者是GlobalSjgnOrganizationValidationCA°GlobalSignOIganjzation成立于 l996年’是一家声誉卓著,备受信赖的CA中心和SSL数字证书提供商’鉴于其权威性’我们认为其 颁发的证书是可信的。

接下来,我们将电脑的全局代理设置为Charles’-般在Char‖es的菜单中可以设置’打开

Proxy→macOSProxy/WindowsProxy,将此选项勾选上即可°

注意在设五全局代理之前’请先在电脑上设五信任CharlesProxyCA这个根证书颁发机构(这也是 一种证书) ,具体的设五方法可以参考https://setupscrapecenter/charles° 现在刷新-下百度首页,再次查看证书详情’如图13ˉ81所示° 厂

.

ˉ….…~………ˉˉ·≡…………….可



″兰兰…以.s肌∑■6■G2

隐二一啥■哩型u.c◎m ■0■≈

■≈■≈≈≈≈



■cm…p『mγc∧(m州匈庄020,α呵吨·比↓M·c》 0.·田b刨duc◎m



… …≈≈一=■ ■罕…≡凸≈≈ ≈ ■~

厂.…^……….……~….ˉ



●■≈=■■≈■--毡→=~咕≈…

■←…司





=■≡P

√■任

■∏』■司‖‖‖』■‖」=田】‖』』■口|■Ⅵ‖‖」·∏|‖■■司||□』■■‖』■

}圈瓣辉鞠鹅龋搬‖翻,』

圈蹦摇蹿蘸:蹿;肖`:瓣.

} 》伯任

} 、′■节

便■此怔书盯: 值用爪筑H认■、?

xGo·■本困■宋■定■

主■名称^哼…

■京琅地区酗

● ′

v■节

■′巾′目治区b叫j吨

组织n但酶w‖c●…■m0d·…硒爪 组织S●嘲∩g助jqu川αco∏lSc‖●晒·丁·cⅦ刘吨γCoo0Lto

■欣…酗

■′巾′■泊■衅吨

腔沽… ■炽n位…◎四■…d…{…0

勋用名卯b●仙ucα∏

■织…os■胸门·贮■W5■mc●了韩h囤鲍ycα0ud

!

叠发钉名称



■■』■■」·口〗‖』■司」‖」■】‖』■

汀用名向…m

泊用名称Che∩●巳p匝γC∧(22M■γ2Om,凶j颐∩g■迢α川■c) ■织单位htma优m「妇m妇叮.cα∏′凹‖

匡n■名亦

■织x灯2Ltd

m■地区匪

所在沧仙c咖∩d

妇炽α◎b■泊幻∩∩γ←m



宙用名称酗b田S咖◎…工〗m0…■创◎∩cA.S川哩“ˉG2

L

古′市′自泊区∧UcR』8∏d

』ˉ…~……≡.… ·′=

…2

?

.

所在地b·§‖吨

主■名印





L



图l3ˉ8l设置Charles代理后的证书详情

电脑上是这样,手机上自然也是°在抓包之前,我们先在手机上设置信任Charles的证书(也就 是信任CharlesProxyCA)’之后在手机上使用Charles代理访问HTTPS网站的时候,所有的网站证 书就会是CharlesProxyCA颁发的,因为手机信任CharlesProxyCA,所以自然就能正常访问对应的 那么关键点来了°

我们在开头提到客户端(这里就是指APP)在获取证书信息之后’是可以对证书做校验的,如果 不做校验,那么不会有任何问题’但-旦校验’并发现指纹不匹配’就会直接中断连接,请求自然就

||

HTTPS网站了。

二■‖‖||」■■■‖』■■|●■】‖■

于是我们可以初步得出_个结论:在电脑上设置了信任Char‖esProxyCA证书后,如果PC使用 Charles的代理来访问HTTPS网站,所使用的证书就会变成CharlesProxyCA颁发的。

‖』

度页面就会出现SSL安全提示°

[山

可以看到,当前的证书签发者变成了CharlesProxyCA°那此时的电脑要不要信任CharlesPmxyCA 颁发的证书呢?答案是要,因为我们已经设置了信任CharlesProxyCA’如果没设置,那现在访问百

{‖

图l3ˉ80百度首页的证书详情

旦勺勺(|」‖■■■■|■■■(■■‖|■■司‖||」■■■■·‖|』』■■可‖‖‖]』■■‖|‖勺叮{·‖·□】■■』‖|」·」■]

第l3章Androjd逆向

652

失败了?

那这个校验过程怎么实现呢?校验证书的指纹即可°因为使用代理和不使用代理的证书颁发机构



不是_个’所以两个证书的指纹也不—样,只要证书的指纹跟指定的指纹不一样’就算校验失败°例

■■|‖』】刊|{』∏‖〈‖|」□■■

如当前证书的指纹’见图l3ˉ82中框出来的内容。

}「 尸‖‖‖『

膜■■「|

13.6 SSLPining问题的解决方案

‖巴■『}|■■■

在开发阶段’如果知道服务器返回的证书指

□一…α丙 皿ˉ ,=0…`ˉ^→一. —` △…ˉ . ˉ 。 . .

_、

纹’是可以提前把指纹写死在客户端这边的°客户

端获取证书后’对比证书的指纹跟写死的指纹是否

653

′T;=了x:页其憨::『:. :;二r:Ⅲ江……丫r; 、 ;已CM∩·zp「°叮cA〔22M”202],cu均‖`gU』蛔川·c) 』 | 吗包.b颧db典6·∏` :ˉ:T::=二: =默`霸:丁.::…莽::-;汀§琴『:::Ⅵ:….广…广ˉ…= . . 赞名70字节8 30冯6o22069866830..,

-致’如果一致就通过校验’否则不通过’中断后

!

续的数据传输°

!

“丁版本, 日忠运茁问s●c↑lgo

0

国忘酶阐!°‘…,‘^c额庐o瓢陶o69°o。A刨5`…万’5|c`卜 D9O2C↑0O2g屹8o巳2089∧37D9]3 时口碱2020年4月2日■期四中四标准时闽『6:OS"

‖▲『

这个过程具体怎么实现呢?通常有两种方式°

锤鲁口露s…“僵c·s^

;

攫名刀字节目 30450220“q巴6c30…

□对于7O及以上版本的Androld系统’SDK

,cv砷`

‖‖「』伊『仔「|■=■「||匹■

日忠运茁■○…‖O

ˉ

提供了原生的支持°在A卯开发阶段,会

:

直接将指纹写死在_个Xml文件里’然后

;

在AndroidMani允st.xml文件中添加一个

!

日惠…{°……2候。{刀so22…`soa3o……4。 《 ‖3|9鳃日「□尸“2「200BCcq〔P]“〔3 时阀■2020年4月2日■屈四中■标准时阀↑5弓05:↑0

…耀……c。s^

蠢鲁,`零霸;…60a20°s0…假`.=

广 | · β 尸 「 β 广

枷…ˉˉ.ˉ.ˉ….ˉˉˉ……ˉ~…ˉ=ˉˉ…ˉ……. .ˉˉ

a∩drojd:∩etwor长5e〔l」r1ty〔o∩十1g配置’具

Ⅷ双

8O45∧937尸仁B5↑0e97日73376■弘26

体的配置可以参考Androjd官方文档 https:/)develo严mndroidcom/hajnmg/aItjcles/

酣M:;罐蕊凳腮I:灌淄;:唇^sα… .S‖A■0

`—ˉ Ⅷ……ˉ

△■■「‖

securityˉconfig。不过要注意Android系统

■『■尸·口■■「『『巴「[【■「「}■「

的版本°



S间A←25e巨867C尸96『8门匠4AP36「2B3了巳7?C‖鳃6CC502 S间A■26e 启867c尸9608门匠4AP36「2B3了巳7?C‖386Cc602

3DS0□5c3巨370AoD2q恒0C37巨巨AO6「∧Bc↑元82 80〔〔 B0〔〔

,

… 纠



□直接将指纹和校验流程写在Android代码 里’现在Android的很多HTrP请求库是

图13ˉ82当前证书的指纹

巴尸『「℃「‖∩「■■厂{◆[『[■「

基于OkHttp库开发的,OkHttp的SDK就提供了对SSLPinmng的支持,一般可以在初始化 0代‖ttp〔11e∩t对象的时候添加〔erti+1Catepi∩∩er这个选项,将信任的证书指纹写死。当然除 了OkHt‖p’其他库也提供类似的支持。

第二种方式的适用性更广’不局限于特定的AndIOid版本’本节也将基于第二种方式实现°

′|)

■厂|‖【尸止■「‖|[■

至此’SSLPjnjng技术的原理就解释清楚了°简单点讲’就是客户端和服务端在握手过程中’客 户端对服务端返回的证书进行校验,如果证书不是自己信任的’就拒绝后续的数据传输过程’这样抓 包工具自然抓不到有效的信息。 3.绕过

明白了原理,那怎样才能绕过这个技术,解除它的限制呢?有以下几个解决思路°

□某些App是使用第一种方式实现的SSLPinnlng’这种方式对Android版本有要求。所以’直 接使用70以下的Androjd系统’即可解除限制。

□既然客户端会校验证书’那我们可以直接Hook某些用于校验证书的API’不管证书是否可信’ 都直接返回true’从而绕过校验证书的过程°我们已经学习了Xposed、Fnda等工具’可以基 于它们实现Hook操作°

□通过反编译的方式还原App代码’修改AndroidManifestxml文件或者代码中用于校验证书的 逻辑,修改完后重新打包签名°不过由于APP代码不好完全还原’该方法的可行度并不高。 其中第二个的可行度最高,所以下面介绍三种基于第二个思路的解决方案。 ●Xposed+JustTrustMe

JustTmstMe是一个Xposed模块(h吨s://githuhcom/Fuzjon24/JustTTustMe)’其基本原理就是通过 Hook证书校验相关的API0绕过证书校验的过程°

注意请先确保子机已经ROOT并安装好了Xposed模块’具体配五流程可以参考https://setupscrape centeI/xposed·



第l3章Androld逆向

首先下载apk文件(https://githuhcom/Fuzion24/ ‖■■

JustTrustMc/releases/)’并把JustTrustMeApp安装到

雪 @

◆?务q| 凹手鳃癣

95

→■■

鳃髓`爱懒

手机上`然后在Xp°scd的模块设置里开启圈 翘麓霸嚼

JustTmstMe’如图l3ˉ83所示°

开示源熟蕊翻麓瞬甥蹦J ■嚣嚼· ■ ◆

九▲■



删◎αα驰$

贮野7



2阻 …=垣



幽嚣嚼撕蓖· 圈…人 0

9

照愉`爱咐历史凸l争

鄂嚣慧`翔

图l3ˉ83开启JustTmstMe

图l3ˉ84数据成功加载‖{来

』日■■司

Charles中也能正常抓取数据包了’如图l3ˉ85所示。

…….响…镭@m幻0 ′=峰…

}…句…吨 ……蚀…Ⅶ`O

…助口γ…° 凰m 【

飞…t■8丽■ .了·】ol0β·8 《0 ■』庐0u● ■Q…0盯唾巴■0

冯●M“■』≈0●『…t酗■p =“v亡产l ■M0pS6〃邮‘…驼邮码峨●Fw≈』●/馋咏D】【“”鸵●X◆▲岭昏…e鞠●〗△e例…〗.】…包饵O丸【●≡Ⅱc◇o

面凹$…丁mZp8沪″》 伊鲸嚼0 虫w矿0■…‖p 冉…uO…Ot■8●…■鲤←R1■p ■…巴钾●〗▲蛀□

●…【0『0O

■『叼…O■8 !怜酉口 臼酝‘齿〗, 句●~〖■■戴=…泊卡位…■■■…吕也!蹿…丁…■● ■…(傅旦≈酗…=■巴A■出‘ γ曰 》o 《 ■1铲0皿■ …■■·…≡≡『

■■l…〗 ■L】0●·0P』◆p

●c们●庐『 ●∩0t郸『〃脚·叶k《砌屁q闻屯心v屿′9d钝·●3〗台凹7·y5↑i…m】日7□1UlR卫VO逊j…ˉ弘4几2■→】【诊′

■龟m…mm■』 】·=‖ ■D叮· 净娠】p ■…l…巴犹●8 铂拘M荤Ⅱ甚…】丁o ■●绅■■〖晦■ 兽■龟●7亏』 ■■0■

·丁卓…‖护…o 钞≈伪w· ■画. ■窍◆→lo

闰df…●〗砷《少……j…Ⅷ许■….…x干…·~名m■氏…政.…了丘◆m▲々山…可■由…乙

》0 《 ■』厂8 1】p

■ ■ Ⅵ □ 可 ■ ■ 可 □ ■ ■ ■ 勺 』 ■ 】 ● ■ Ⅵ ■ ■ 』 ■ ■ · ■ ■ ■ 』 · ■ ■ 司 √ ■ ■ ■ ■

.●…″~心《 p●唾=≡二==二e=鸣

」■‖■司|』■司|』■Ⅵ

■飞;?沁【s锄alv巾0 0O

日‖|』■‖|」■■■』·〗



√■甲 ◆撵…

』·|乙■〗勺||」■■■■Ⅵ°□‖‘=·司||‖」■〗□■可|」■■■]」{|●】‖|■■司

贸 涩嚣赣鳃" 窿::隐噎Ⅷ

所示°

‖‖‖■■』■』‖‖』■‖』‖‖‖‖凶□』〗〗‖‖■可‖‖|」■】‖〗‖」■■可‖〗‖』■】□□]‖●□■∏】‖‖‖■γ〗‖‖|■‖

654

因幻…〖 ·…■0 ≈●u▲0钢已■∩…t』7●1忧A“◆·

=c所e尸】旬Mt●Q8〃的‘■」tu刨·∏t′…№′沁F61硒△3“y皿β0凸]龟蝇例】7m■■〗萨…09…6“仍2G此◇.

争cm……●8P西· ●″‖0 ■…0…少$■已 ■2■』乙睡争岭■』 闻■…″lm△

■【幻『寸0 0·0● ■「匈』m■.t卜…lp

Charles抓取的数据包

Xposed+JustTmstMe的方案有_个限制,就是手机需要ROOT,解锁bootloader等°对于_些系

统版本比较高的手机,ROOT操作是比较困难的’所以提供了另_种解决方案,用VirtuaⅨposed代替 Xposed(https://githuhcom/androidˉhacke『/VjrtualXposed)°

‖二■■■■■■□■■』□■■司纽‖

●VirtuaⅨposed+JustTrustMe

=■■|■■■∏』□】■可

图l3ˉ85





l3.6 SSLPining问题的解决方案

655

VjITualXposed是基于VirtualApp和epjc,在非ROOT环境下运行Xposed模块实现(支持Androjd 5.0~Androidl0.0)的,是一款运行在Android系统中的沙盒产品’可以将其理解为轻量级的Android虚 拟机。

要实现Hook,我们需要将App安装到VirtuaⅨposed对应的沙盒里,再配以-些Xposed模块(如 JustTmstMe),即可绕过SSLPlnjng技术°

首先’安装VirtualXposed’其运行页面如图13ˉ86所示。 然后点击界面下方的菜单按钮,进人设置页面’如图l3ˉ87所示° 佐十

仁嗽撼v■



芍酗鳃

设置 添加应用

必顿先把应用以及Xms“攫块瓤添腿到γ械ua‖xp◎Smp酉剔 xpO£“攫块不会生效·

摸块笛理

开镭逆蜜关闭xpo6“攫块(xpos酶摸姨安骏之滔必瘦手劲开 启才鳃生效) ^

田四

●■「|伊『尸}|‖甘‖’■户●甘『|》巳■『|■■「}卜■尸[■▲『|∩‖「◆「■■『‖‖尸卜广‖|快▲■厂)》尸〖■已尸「■■「【■‖|■广|■◆「厂|[■「●「■旧

6月6日星期日

窜用攫块

离级设■

图l3ˉ86 VirtualXposed的运行页面

图l3ˉ87 VirtualXposed的设置页面

点击“添加应用,,’将手机中的App安装到VirtualXposed的沙盒环境中,如图13ˉ88所示,勾

选对应的两个App(这里需要提前在手机上安装好示例App和JustTmstMeApp),再点击“安装” 即可。

在安装过程中,可能会提示‘‘是安装到VirtuaⅨposed还是TniChl”,这里我们直接选择 VirtuaⅨposed°

补充TaiChl(太极)也是一个类似Xposed的模块’同样不需妥ROOT就能伎用’大家可以了解一下° 另外’它还有一个增强版的模块’叫作太极Magisk’功能非常强大’大家也可以试试看° 接下来返回设置页面’点击‘模块管理,,,如图l3ˉ89所示。

这里会自动检测到刚安装的JustTnlstMe模块’勾选即可’如图l3ˉ90所示°



[↑3

盛块管理

|』■■】□】】■■·‖]●】‖||□】||||』司■』α‖「□■■■【】】■】‖‖■■Ⅱ‖‖

开鹰咖羡儡x…“攫诀《即o6“酞块安较之篱必颊手勋开

q

第l3章Android逆向

656 克‖巴…

设置

!|

}同…‖懒

叼可呵可呵可









……_一_…

固肇窒抄陕■

同∧…≡Ⅷ0

≡≡产



钾豁=-…

-的

添加应用

密撅先归威用以及x脚$“攫块郁蘸酗翱VM』』怠‖x”S囱|晋姆 N…“碘块不禽熊蚊·

启才傀线效)

0■

崩用攫块

‖.

■」





离级设Ⅲ



■………忿

■■|

图l3ˉ89设置页面中的‘模块管理’,





@付欣盅■.▲m…n蛔





塑……词川5







0

|‖

■ ~

图l3ˉ88安装App

F

」US∏『ustMe





侧‖‖巴SS儿…娥凹tev剖i排m0叼

图l3ˉ90勾选JustTrustMe模块

之后需要重新启动VlrtualXposed°

最后,我们在VirtualXposed里打开安装好的示例App’即可发现数据能成功加载出来了’Charles 也可以成功抓取数据包了°



●Frida+DroidSSLUnpinning

既然Xposed+JustTrustMe的原理是Hook证书校验的逻辑,这个逻辑是通过Xposed模块实现的,

如果想基于Frjda实现Hook,那么可以结合DroidSSLUnpinning这个开源库,其GltHub地址是 ht‖ps://github.com/WOoyunDota/DrojdSSLUnpinni∏g°



{ | (

那能不能基于同样的原理利用F门da实现Hook呢?能。

首先下载对应的GitHub仓库: q

gjt〔1o∩ehttp5://gjth‖b·〔o们/‖ooyu∩0ota/Droid55[0∩p1∩∩j∩g



〔dDrojd55[0∩p1∩∩j∩g/0bje〔tio∩0∩pj∩∩i∩gp1u5 十Iida ˉ0ˉ千〔o∩‖,go1dzeⅦvγⅧhabitˉ1∩oo促5.j5 ˉˉ∩oˉpau5e

这里要给+Ijda命令的ˉ千选项传人要处理的App包名,给ˉ1传人要Hook的脚本’命令的运行 结果如图l3ˉ9l所示°

|』■■Ⅱ{|{』■可]|」●■‖■■可

该仓库中有_个Hook脚本’我们可以直接使用+I1da命令启动:

叫‖



凸·‖(‖|



l3.7

Android脱壳技术简介与实战

657

●p◆ 岭§| ∩9p1 鲍je C七1O∏枷pM∏1∩9P1u£

「 f而d口ˉUˉfC咖.9◎MZe.冈γⅦ↑0αb让ˉ1∩◎O代S

〕5≡聋∩◎ˉp□u5e ~…→

/→≡| 斤ld口1q镭0。8ˉAmFMˉcm£巴叮「画tci∩5仓尸U阳e砒Gtl◎∩t◎o1恨it | (一‖| }



〔…"d58

/=/ {…| ●咏







隘e1p

ˉ>Di臼ploySthe‖e1p£yst刨W

objeCt?

ˉ>0tSpIoy1∏『Q7∏℃tto∏αb◎ut 0ObjeCt,

exit/quitˉ≥fxtt

№厂e1∏「oO七∩ttpS://″.什id□.「e/“c巳/Ⅶ咖e/

· · . °

5…‖e口、Cm].g◎mZe.∏γⅦmb1t`.Res颐1吨mi∩恤『qeGd! …]ed、Cm].g◎mZe.∏γ呵mb1t`.Res颐〗∏gmi∩恤『qeGd!

[∧∩d铲◎iα[呻lαtO广55S小『CmlgOldRe·"VⅧ↑mbit]ˉ≥爬S乞蛔e: ∧∩dF◎诅[呻lαto广55弘:『cmlgoldRe·|‖vⅧ恤bit]ˉ≥爬sm钾: {′f”e! ; °邑em0 , 0PαyI◎ 0PαyI| αd《 d《 : ,〔qst咖, ,〔‖5t咖,印口ty『厂uSt№∩蛔e下Feody0]dαt□2 【呻ty『厂uSt№∩蛔e下Feody0]dαt□2№∩e 掷O∩e 雨e55oge食 e55口ge: {0tγpe,: 05em, , !poy1oo口, : 00刚∏p3.x「oⅧd!}dot◎; 00刚丁Tp3.x「oⅧd!}dot◎; ‖o『Te e巴£◎ge: {,type, ° 『Se∩d0 ′ !p口y1oαd! ; {〔蜘,Squ◎厂eop,o灿ttp∩Ot千Omd0}dot◎: No∩| 爬巴£◎ge: {〔蜘,Squ◎厂eop,o灿仕p∩Ot千oo∩d0}dot◎: N◎∩e

∩es5◎鳃: {,type,; |5e∩d! , ,pαyⅡ◎◎d, : 『广egiSte厂〔lαS5什咖们ost同°掀eve厂i「ie『>>>>>> ∩gt而pIe闹e∩t□tIo∩「w: booIcα∩ve了t「y(j□γo·1□∩g5t庐mg’ joγαx。∩et。551.55 ≥>毗蟹si∩gt而pIe闹e∩t□tto∏「w: joγαx。∩et。551.5 k5e55i◎∩)0}αdtα: )‘}αdtα: ‖O∩e

卜|

砸5S口ge: {0kype` : ,5e∩O,『 ,pαy1◎oα0 : ,Xl』tl1二∏四比∩Ot庐◎@∩d‖ldoto: ‖酮e ‖o∏e 0pαy[oαd0 ! ,杜亡p仁l1e∩tα∩创FoMIib胁oks∩◎tfou硒,}dot ,杜亡p仁l1e∩tα∩创FoMIib‖ooks∩◎tfou硒0}do 网es5◎ge: {,七γpe, ; 05e∩d0 , 0pαy[oαd,



o日 NO"e

耐e55◎ge: {,typG·: ,$e∩口0 ’, ’pαym□d|. ’丁「uSt№mge了I呻lUeFt竹咱hαi∏问outf◎u∏db ,丁「uSt№mgeFI呻lUeFt印《hαi∏问outf◎u∏dbe }d◎tO: ‖O门e 1◎W7.0v}dom:№门e

图l3ˉ9l

启动命令的运行结果

之后,Hook脚本便会生效’而且我们可以发现’示例App可以成功加载数据了!Charles也可以

成功抓取数据包’和使用Xposed/VirtualXposed+JustTmstMe的效果是一样的° 4总结

本节我们介绍了SSLPinning技术的原理和解决办法’随着移动互联网的发展’使用SSLPinning 的现象会越来越普遍’因此绕过它成为了移动爬虫开发者的必备技能之—’需要好好掌握°

↑3.7∧∩d『o|d脱壳技术简介与实战 在Android逆向中,大家应该或多或少听说过加壳`脱壳等词’那这个壳是做什么的?如果一个

apk文件加了壳’我们又该怎么进行逆向?本节我们就学习_下与壳的管理和脱壳相关的技术° |.实例引入

我们知道’AndroidApp的安装包文件是apk格式的’从本质上讲’这就是_个压缩包,解压之 后;其中包含AndroidApp的源码、配置文件和资源文件等。

我们先做一个测试’下载l36节使用的示例App’得到一个scrapeˉapp4apk文件’然后将文件扩

展名修改为zip,即文件名变成scrapeˉapp4.zip’随后用解压软件直接解压这个文件’得到的结果如图 l3ˉ92所示。 〈

● ◆ ◆



…≡_==

△°二

巳口●…呻“

≡◎

^匡△=



矽≈汐二〗当ˉg=·h雨

…动,0·花碱“加 ……【甄妇沁■“;仰

刨匹些·…

i} 爵影



》Q

…F一△-—

仅m



』贝B “…』二羔

酞aM8 ……



》■睬丁肖.杆

…以0▲的

「…

》■…如8 》■心6

7@崎碘↑Q翻 …吼?▲啪

…空

们馆型毋坦◎●…

0…γ瓣| ↑9油n“f佣

■…

鳃$ⅨB

…l…

…`h



k

图l3ˉ92解压scrapeˉapp4.zip文件的结果

这里的classesdex文件就是apk文件的指令集’只需要简单地反编译一下就能得到Java代码,我

们直接用jadxˉgui打开这个classesdex文件’等一会儿’源码就反编译出来了’如图l3ˉ93所示°

』 ■ 勺 』 □

到■乙■■可■■可

Android逆向

第l3章

658

由●■工nm

=-→~=≡■一Q

些=…

份 酬c■



二己≈坦 ●…门…

■=巴舞o铂『…q→】▲旦

■…

10

】●■γ@1炉■+已=山…… ≡≡答≡Q0 ~d…趋·1…叮 ■…·0l屿

』…m几·▲『f军』0『』

』≈o此』l·匹』6℃↓

√■沪…□…t



●…和疗●【→】▲ r=……1呼≥【)

ˉ●…uc·c…

U



■βh0Ⅷ≡》… ●这≡吼〗m皿

…『Ⅲ…〃…■…吐雄…咱 ˉ ..=$←0≈……刨《…0 四℃■M 《

●鳃……呵 ●m体t』四

i

…≈由 △-■-■沏户…uβ 0沁 t’l





0



…寸心〃…幻·■·…疟舜了

-■Lˉ=丑 ●~■毛1

汕《

●Q=





Ⅱ冗



〗冗



■……1QM》〗

●≈p≈0■= 兰〕1

…广如uD=‖●巾…》 …】t「……『 …耐■l1~(氏″》 …吨■l』 =酝("″》

■■■■●

…Ⅵ·…







~0w0叼…『

◎■硒

●……啤{

!



l {

″…′…m叮

` ;

…坤硒汕 串『■01…《■刨…》





…『比!0~(■…》

y

…βlf〗≈… …f山1凹…0泊l…沪)

…1…





尸…3t了呵…l…0β

■叼洒



=黔…←r…″→

◆旧ⅥfD7』t2

心碰3…‖

■识■文仲

…≡』



图13ˉ93示例App的源码

由此可见’如果—个App能轻而易举地被反编译出源码’那对开发者`运营该App的公司无疑 是很严重的一击°毕竟ApP里的代码实现—览无遗’加密算法也能被看得_清二楚,如果有人想复制 和抄袭’简直是轻而易举。所以,做好防护是必不可少’目前最常见的防护手段就是加壳°

顾名思义’壳就像一个盔甲,可以起到防护作用,AndroidApp的壳就是用来保护App的源码不 被轻易反编译和修改的°也就是说,加壳之后,真正的App源码会被“隐藏”起来,直接反编译apk文 件是无法得到的°

打开它’结果见图l3ˉ94。 勺』、

·"″w….…

●… 『…°pt山』

·g铲…■」`

…行0■≡△宁宁UO田B0…县 …■◆网r…·酝0■t】

●q….哑1l ●≈】

●〔m0』锣r■Y』m

8髓龋飞·″…m…, ●…→0■… GOt山

e■■■■

■■■女件

尸娜p…tu定

0丁

磅ut■〖』晌00〗



| /·′ γ′尸■; 疫 呼 | 页巡……』‘…`』c·Mm′〗°喊`; ′●≡『『■〖 p

° j



m…■…3t厂1叮↑马■■u乙2d→■; /F=′…; 0叫 F…←d■[叮…05〗

L ′·…′旷■箭

″ 陛















…蛔§

」‖』■·‖‖|」■■■·‖||■■

图l3ˉ94App加壳后的反编译结果

°! ,

日 』 ■

=≡ˉ二Ⅲ≡,忌嚣_尝-~

口 勺 』 ■ · 旦 Ⅲ ■ ] 」 ■ ∏ | ‖ ‖ ■ ● 」 ■ · ■ ∏ | | 」 · ‖ · ■

■〔雷7·呻 ≥潭代田

‖■可刁‖·‖

我们再做一个测试,打开hhps://app7.scrapecenter下载另—个apk文件,下载好后尝试用jadxˉgui

』·」■‖|{纠■■‖』·∏||司引勺|门||·|□‖|√‖|■|‖□‖

●■■●

…坤■唾■〗

蕊ms 瀑m,

‖』■】■】』]·】‖□』□}|」■」□|

j

●亚』…叫』”皿 ●m』【Ⅶ7o凸1咽《0 1m ●…面tP〖p≈D勺》 Ⅷ蛔

□‖|』□

ˉ·鄂t,

』■■■〗‖|□■】‖|{■·∏{‖‖』·Ⅵ‖

垂』凸广■ˉ霞震k

■铂 『

一—-—

l3.7

Android脱壳技术简介与实战

659

可以看到’这次没有直接得到APP的源码’原来的comgoldze.mvvmhabjt包里只有一个R』ava文 件’另外有了comqihooutil和comstub两个包,这就是App加壳(本案例为360壳)之后的效果。 2.加壳的原理

其实壳本身也是—个dex文件’我们可以称之为shell.dex文件°通常,在加壳之后,原apk文件 中的dex文件会被加密’于是我们没法直接破解和反编译它了。但是’shell.dex文件可以解密已经加 密了的dex文件,并运行解密后的dex文件,在这个过程中’Shelldex文件承担了—个人口的角色, 对已经加密的dex文件进行解密并运行解密结果’从而达到和运行加密前的dex文件同样的效果° 加壳过程分为如下三个步骤°

□对原dex文件加密:从需要加壳的apk文件中,可以提取出-个dex文件’我们称之为ongindex 文件’利用某个加密算法(如异或、对称加密、非对称加密等)对该文件进行加密’可以得到

_个encO′pt.dex文件。 □合成新dex文件:合并加密得到的encryptdex文件和shell.dex文件’将encryptdex文件追加 在shelldex文件后面,形成_个新的dex文件’我们称之为newdex文件。

□替换dex文件:把apk文件中的o∏gindex文件替换成new.dex文件,并重新进行打包签名。

如此一来,原来的apk文件就完成加壳了,我们也无法利用jadxˉgui等工具直接反编译ongindex 文件了。那加壳之后的App怎么运行呢?

其实很简单’通常在shelldex文件中’有一个继承自∧pp1i〔atjo∩类的类,App在启动时会最先 运行这个类’例如上面的案例中就定义了一个5tub∧pp类: pub1i〔十i∩a1〔1己s5Stl」b∧ppexte∏ds∧pp11〔atjo∩

这个类做了什么事呢?其实里面定义的就是-些解密encryptdex文件和加载解密后的dex文件 的操作,即解密、还原操作°

具体怎么实现的呢?有两个关键的方法°

□attach8a5e〔o∩text:这个方法主要负责从newdex文件中读取出encryptdex文件’然后对其 进行解密,利用自定义的0ex〔1a55[oadeI对象加载解密后的omgm.dex文件°

□o∏〔reate:通过反射机制修改∧〔tjγjty『∩Iead类的内容’将∧pp1j〔atjo∩指向o∏gin.dex文件

中的∧pp1i〔atio∩’然后调用o∏gjndex文件中的∧pp1j〔atjo∩的o∩〔Ieate方法启动原程序° 通过实现这两个关键方法’origm.dex文件中定义的逻辑就能正常执行了,这就保证了加壳前后 整个App的运行效果完全一致° 厂

3.壳的分类

上面介绍的加壳技术应用比较广泛,但是道高—尺魔高一丈,随着越来越多的App采用这种加壳 技术作为防护,脱壳技术也在不断更迭°两个技术不断地抗衡’不断地进化°

目前,壳已经发展到第三代了,上面介绍的只能算第一代°下面简单给三代壳归一下类。 □一代壳:整体加壳’整体保护,即上面介绍的加壳技术,对App中原本的dex文件整体加密

后’将其和壳dex文件合成—个新的dex文件°壳dex文件负责对App中的加密dex文件解密 并还原’从而保证App可以正常运行°对于这类壳,利用jadxˉgui这种工具通常只能看到壳dex 文件’原dex文件则看不到°

□二代壳:提供方法粒度的保护,即方法抽取型壳°保护力度从整体细化到了方法级别,也就是 将dex文件中的某些方法置空,这些方法只在被调用的时候才会解密加载’其余时候则都为空。 对于这类壳,利用jadxˉgui反编译的结果中,方法全是∩oP指令°

↑3 k

|}

660

第l3章Android逆向

□三代壳:提供指令粒度的保护’即指令抽取型壳。目前主要分为VMP壳和dex2C壳’就是将 Java层的方法Native化。VMP壳会对某些代码进行抽离’将其转变为中间字节码’VMP相当 于一个字节码解释器,可以对中间字节码进行解释执行°dex2C壳几乎把所有Java方法都等 价进行了Native化°

那怎么判断_个壳是第几代呢?

□对于一代壳’反编译之后如果只能看到继承自∧pp1icatio门类的壳代码,其他诸如Android四 大组件的类都被隐藏了’那就是一代壳°

□对于二代壳,反编译之后看看方法的实现是不是为空,如果Java代码的方法实现是空的,Smali 代码有很多∩op指令,那基本可以断定是二代壳° □对于三代壳’反编译之后看看-些方法是不是被Native化了,例如o∩〔reate方法的声明前面 如果有-些∩at1ve关键词’那就是三代壳°至于到底是VMP壳还是dex2C壳’可以根据方







法注册地址等做进一步判断°

4脱壳实践

上面讲了壳的原理和分类’那么问题来了?如何给加壳的ApP脱壳呢? □一代壳:目前市面上的_些免费加壳(加固)服务几平都是_代壳,例如360加固、腾讯加固`

阿里加固`爱加密’现在已经有较为成熟的查壳工具(如PKID),选择apk文件之后’该工 具就可以根据壳里面的_些特征判断是哪家的壳°另外’对于_代壳’现在主流的脱壳工具非

常多’有fijda-dump` FRIDA_DEXDump等’稍后会详细介绍° □二代壳:加这种壳一般是需要付费的,现在有不少银行App是用的这种壳°由于方法只有在 被调用的时候才会解密加载’因此脱壳的基本思路就是主动调用’现在主流的脱壳工具是 □三代壳:这种壳目前没有成熟的脱壳工具,基本上得靠手工分析,只要工具深肯钻研,也是



FART。

能解开的°

下面我们还是以本节开头的App为例介绍一下脱壳方式’由于这个App使用的是一代壳,所以 这里先介绍f了ida-dump和FRIDAˉDEXDump两个工具° ●frida-dump的使用方法

颇da—dump的基本原理是通过文件头的内容搜索dex文件并dump下来’其GltHub地址是: h呻s://gjthuhcom/lastingˉyang/仿lda-dump°

要使用这个工具’需要先下载其源码: g1t〔1o∩e‖ttp5://8jthub。co"/1a5tj∩gˉγa∩g/「rjda-duⅧp.8jt

之后在电脑上运行如下命令: +rjd日 ˉ0ˉ十〔o川.go1dze.ⅦγⅧh日bjt ˉ1du"p—de又.j5 ˉˉ∩oˉpau5e

这里指定了脱壳脚本dump-dex.js和App的包名’运行之后就开始脱壳了’控制台的输出结果如 图l3ˉ95所示°

』■‖‖‖·|』·〗】‖』」■可‖』』■■】|]‖■冈|‖●〗■□]||·■·∏‖‖●‖·〗」●■■|

然后在手机上安装下载好的apk文件,运行App,另外还需要在手机上运行1Tidaˉserver,具体的 配置见l3.5节。

|』■□]』司||■■■■■■』

l3.7 Android脱壳技术简介与实战

66l



丁 什i血型四甲 7于1“卫四甲

f 沪idα≡0~fCαngO1眶e.pwγ帧咖i七』~1d『』呻ˉ创e×°jS点∏O f

GⅦ丘e p□

/=| 仟idα1牛°0.8ˉ∩Ⅳo「1d~仁1α5s固y∩α印Ic1∩5t铲(me∩mtio∩to◎1Rit =‖ ‖ ‖ (二‖

>≡

(…◎膛创S:



′=/ ‖=‖ 【庐『|卜|‖■卜

























∩e1p object?

ˉ≥oisp!αy霹t们e肘eIpsy$te耐 ˉ>Dt印lαyi"fo厂吨t1o"αbout ,object,

exit/quit ˉ>匠xit

. . , .

鸭◎Fet∏foαth士tp巴://咖ˉ什idα.厂e/d◎C5/∏m0e/

5pαW『0e口 pαW『0e口 、cm0·gOM正e°ⅣⅣγ励T□btt、. 、cm0·goM正e°ⅣⅣγ励T°btt、. Re司《』獭1吨晒i∩t汀eαd! [∧m厂otα【吨1αto「55鹃; Mo厂otα【吨1αto「55鹃: :C颐。gO1创Ze.酮W『讨》αbtt]ˉ>[蚀1o口e∩:〕1ib□厂t.5O =Z‖3α庐七1亚1α5S虹∩艇e尸110ef1∩eααS5僵州5=6『hPeαd[p№炯毗=G抛∩dleI‖5=枷1尸「o沪11[l哦B5儿o αde庐庄[RⅫ5=70eX「i1e[账N59=8〔1α55De「[0X汗岭仟“4b驹 x『j1e[账N59=8〔1α5SDe「[0X汗o9仟“4b90 0x7锄9仟4f4b细 [De乍i∩eαo5s〗〕0x7锄9仟4f化细

[「加dαe剐『 /酮tα/“t□/c咖.g◎1灾e』咖γⅣ〗竹αbtt/仇1eS/汗dge9儡劝a8=5α口汗8.口e× tα/“t□/m扣。g◎1灾e0擒wγⅣ〗竹αbtt/仇1eS/汗dge9儡劝a8=5α口汗8ˉ口e× [d‖mp口e刚; /“tα/印to/CαⅥ.goⅡdⅫe侧w∏『mbit/∩1e5/7『刨9e9f6乙ba8=5□u7「8口e× tα/印to/CαⅥ含goⅡdⅫe侧γv∏『mbit/∩1e5/7『刨9e9f6乙ba8=5□u7「8口e× [ft"◎日ex〕: /如m/“t□/垣咖°go1d∑e0俄wm炯bit/fMeS/7闹,e<6佬228=2e157q.αe× m/“m/垣oⅣ↑°go1d∑e0俄γγm炯bit/fMeS/7闹,e<6佬228=2e157q.αe× [du丽Pdex]; /d口m/dαt·/c咖.goMze。‖wγ∏↑加btt/file已/7「d9eC6傀228≡2e157▲。dex m/dαt·/c创Ⅵ·goMze.‖wγ∏↑加btt/file已/7「d9eC6傀228≡2e157▲.d巳x [fi∩dde×]; /如m/dαtα/cm↑°goMZe翰mw用hαbtt/file三/汗‘9e阀5103旦0。1614C.蚀ex m/dαm/cm↑°goMZ巳mvγ用hαbtt/file三/汗‘9e阀5103旦0.1614C·蚀ex [dt顺pde×]: J“t□/“m/C咖·9◎IdZenww↑h@b仕/ftIe巴/汗d9eα510320=16Mc·de× t◎/“m/C○『Ⅷ.9◎IdZenww↑h@b仕/ftIe巴/汗d9eα510320=16Mc·de×

b

■尸■■■厅

t0SO [dIope∩;]1ib如t,SO [fmdαex]: /qαtq/“tα/m∏‖·g◎1血G.』wⅦ『mbit/f讥eS/71αo穴8q一9b9α3C.dex tq/“tα/m∏‖.g◎M王G.!wⅦ『mbit/file5/71α0穴鹏=9b9α3C.oex

}尸‖『

脱壳完毕后的结果都在手机的/data/data/comgoldzemvvmhabit/files文件夹里’我们可以运行如下 命令把它们拉取到电脑上: adbpu11/dat己/d日t日/co∏.go1dze。∩γⅧh己bjt/十11e5≈/dexe5

这里我们将结果放到了电脑的√dexes文件夹中’如图l3ˉ96所示。 r§



〉dexe它

^■



$■■

● ● ●

一一~

|}}



图l3ˉ95控制台的输出结果

蹿漂彰

色◇

≯Q

一 —■_≡

名鞠

修改日期

大小



种类

■凸■』

尸|‖卜皿■「‖||(■尸「||(【■「||}■尸‖‖‖‖止■■「|『‖▲尸『「||‖■「‖‖|【■尸‖‖|[■■【『||□■【〗‖‖〖■「|}|『卜【『『|』■■勺

7]巳0′c8鸟=9D9a3c·dex

今天002“

『0`2MB

文稻

了?d9O刨88b285ad7fadex

今天6硕耳

6捌B

文藕

7↑26已7e0ˉ∩d『cd0.dex

今天佣1“

5写?帜B

文Ⅷ

汗日9ec6↑22匿坠2e『57d。◎ex

今天o004q

3朋日文Ⅷ

7‖7f076』」3030Oqex

今天oO:“

↑.艺棚B文Ⅷ

7078c8b』5deoO.dex

今天oO:“

鳃5ⅨS

0 7]7qa▲mq24OQ。d酗

今天0o;尽Q

Z7↑ⅨB

文Ⅷ

今天0o:4q

9o低B

文罚

7↑a9ea536320ˉj6涧c·dex

文漓





k



图l3ˉ96电脑的~/dexes文件夹

可以看到,这里_共得到了8个dex文件,一般而言,核心逻辑存在于较大的dex文件里°我们

使用jadxˉguj打开一个dex文件’看看还原效果。如图l3ˉ97所示’这个dex文件中就包含一些核心 逻辑’比如colngoldZemvvmhabit包里定义的内容。



哨■

睡TE睡鲤◆望蹿蹿擎′衅攀翻山/≥八 ●

●■

c■



■■… 》■…蛔 》■…m…



吵△…



》■●幻lu 】■…

喝严睡

||





】】

】…

1c……≈T·《 0 〖

M

0罕『v辆…〗】 n四0…M7■…)8

■ 》 》 》 ● ■ ● ●● 啪加止违加

寸少卜》卜■●■■■

19 0’

■■■{』■』■■·‖‖咐■■|‖』‖』】■]‖|」·]‖|

第13章Android逆向

662

皿1tt『■●《》〗







□■弓…













丁…





《■ ℃ 』 ■



『…











…0■·四户◎c…倒》●←(·0





=· 兰0…血1…()〖

〔■

20







‖■■‖|』刊」■

…………1 》●…L·… p●』≡…■…c汕 ’■1…■…畸…《

p●申…`帕0…■mk…■ D■c盯1Ⅲ·mlM…皿 》

◆…p3 贱犁o



庄… ●…血●……



■…

T

图l3ˉ97反编译一个dex文件的结果

通过文本搜索,我们也能找到_些关键的代码’如图l3ˉ98所示°

ˉ在以磕自…-≡…ˉ---ˉˉ一ˉˉ-.ˉˉ.~-ˉ~一=ˉ一ˉˉˉ`…: .

玛名 `万蝉.√1…田翘

〔巴■出大‘叮

b →一一一含~运一≡--…→=…←~诌-_-=←-=~~→…≡→手~~…←三…ˉ.ˉ ‘ ˉ 宁 专< . .丙i‘ .≡ˉˉ

.Ⅻ

=≡≡→=ˉ≡.ˉ=≡二=≡=

『…

【″ˉˉ:.ˉ′….ˉ』′…‘ 0′ :`

徊…蛔…』宾伊γ』唾

〗■‖■■‖〗》仔■〗■仕〗■勺】】■γ■■■

一…■盂了2个■果中的疽1至m十 =

||

-

■|‖二■·■■』‖‖■‖▲■‖‖■』·‖{纠●司』·Ⅵ■■Ⅷ‖‖司]|」■■〗‖‖·』可|■Ⅵ{●Ⅵ|·』●可■·‖■■习‖‖■]‖』■』■勺‖‖|■■■可‖』司■■可||■□〗|■■■■二■司

●硒mfk捏

■■ …白仲

d

图13ˉ98搜索调用‘‘/apⅡmovie卸的代码 图13ˉ98中有两个搜索结果,我们转到第一个’这就是之前分析( l32节)过的1∩deX方法的还 原结果: 创γerIjde//〔m。8o1dze。|wγ帅3bit.data.5our〔e.‖tt卯at己5ouI〔e pub1j〔∧bstr己〔t〔1387z〈∩ttpRespo∩5e〈№γje[∩tjtγ〉〉i∩dex(i∩tj’ i∩ti2){ ∧rmγu5tarmy[i5t=∩eW∧rrayliBt()j armyljst.add("/apj/硒vje,0);

』卫■】「‖□』』■〗』■|■■]■■|』■∏‖■]‖|]▲■■■■Ⅲ



5trj∩ge∩〔rγpt=[∩〔rypt.e∩〔rypt(日rIaγl15t)〗 retumth1s.十468a。i∩dex((j ˉ 1)*12’ j2’ e∩crγpt)5

■■■■∏■■伊■‖‖‖■『‖‖|△尸『||·『‖‖|β『}|■β|||『◆【『‖‖‖[β‖|》》|■β|卜伊|

l3.7Android脱壳技术简介与实战

663

可见还原度还是比较高的。 ●FRIDAˉDEXDump

FRIDAˉDEXDump也是_款比较不错的脱壳工具’同样是基于Fnda’由于Frlda提供了在电脑卜

对手机App进行内存搜索的支持’因此FR〗DAˉDEXDump根据一些暴力内存搜索的原理实现了脱壳° 对于完整的dex文件,暴力搜索‘』dex035”即可找到壳;对于一些抹头的dex文件’可以通过特征匹 配找到壳,例如搜索DexHcader中的长度信息`索弓|指向的位置顺序等。 FRlDAˉDEXDump的GitHub地址是h卯s://gjthuhcom/hluwa/FRIDAˉDEⅪ〕ump,可以直接使用 Pjp3工具安装它: p1p]i∩5ta11千r1daˉdexdⅧp

份■尸

同样地,在手机上启动lTidaˉserver和示例App后’运行如下命令即可完成脱壳: 运行结果如图l3ˉ99所示° ●‖P●

(py37〕℃ P沁j田〔k≈什i“唾哦exq呻ˉ∏c酮。卯M盔′阿γ耐0吨止ˉf



/’

儡既

}|}

■尸}》∩·估「‖尸·■「》′|◆『仁》′■■『》》|卜■■口》仆△∩)卜「■『广■【「□■■·β□尸些尸"■■°炉『)■尸■【■厂『广[■「

十r1daˉdexdu呻ˉ∩〔oⅦ.go1dzeⅦγⅧh3bjtˉf

ˉ07/01目01:11】吁0[D[XDl』巾]: Sleep10凸 ˉ07/01ˉ01?Z】 I"「0「0〔xuu知p]8 「Ou∏°tO沪qe仑 『Mq9§]《□尸g<,iGz中.门√…扣α邯’t

图13ˉ99利用FRIDAˉDEXDump脱壳的结果

脱壳之后的文件就直接保存在电脑上了,控制台会输出dex文件的保存路径。之后利用和

mda≡dump那里同样的方式对得到的dex文件进行反编译即可,这里不再赘述° ●FART

对于二代壳, 目前主流的解决方案是FART’这是ARr环境下基于主动调用的自动化脱壳方案, 本节不再展开讲解,如果感兴趣可以参考https:〃github.com/hanbmglengyue/FART,这个项目中介绍了 FART的原理和使用方法°

5.总结

本节总结了Android脱壳技术的原理和解决方案,熟练掌握脱壳技术已经是现在Android逆向和 爬虫开发者的必备技能之_°

对于脱壳技术,我们不但要知其然,还要知其所以然’这个技术领域涉及了非常多Android底层 的知识’要想深人研究是需要下_定功夫的° 本节内容的参考来源如下°

|‖{

664

第l3章Android逆向 「

□吾爱破解网站上的“FRIDAˉDEXDump:-吻便杀-个人’三秒便脱一个壳”文章。 □博客园网站上的“VMP壳基础原理”文章。

↑3.8利用|D∧尸「o静态分析和动态调试so文件

=】‖‖‖」‖||『

□掘金网站上的“Android脱壳之整体脱壳原理与实践,’文章° □CSDN网站上的“基于FRIDA的几种安卓脱壳工具,,文章。

我们已经初步了解了-些逆向相关的知识’通过jadxˉgui和JEB等工具’我们可以成功把apk文 件中的Java代码反编译出来’在此基础上就可以查看实现逻辑了。但这个反编译过程仅仅停留在Java 层面’这是什么意思呢?本节我们来详细解释-下°

在Java中有_个叫作JNI的东西’它的全称是JavaNativeInterface’即Java本地接口’这是Java 编写的代码了°

JNI是Java语言里本身就存在的’由于Android代码是基于Java编写的’因此Android自然也能 使用JNI调用C/C针编写的代码°

使用JNI有什么好处呢?对_些AndmidApp来说’其中_个很大的好处便是可以提升防护等级,

结果°重要的是,如果仅通过jadxˉgui反编译’是无法把这个so文件还原成原来的C/C++代码的, 因为jadxˉgui只能处理到Java层,对Natjve层则无能为力。换言之,如果某个加密算法是在Native层 实现的’那么仅依靠反编译是无法知晓其中的真正逻辑的,这就进一步提高了逆向的难度° 那要想还原so文件中原本的C/C什代码,有办法吗?有,但不能是反编译了,需要用反汇编。 其实,还原完整的C/C++代码几乎是不可能实现的’但我们可以通过一些反汇编工具得到底层的汇编



‖、‖叮|勺|』‖·】|』‖■可。‖■勺‖|‖■■

因为使用C/C++编写好某个代码逻辑后,这部分代码会被编译到一个以so为名字后缀的文件(例如 libnativeso文件)中’然后Java层需要直接加载该so文件并调用so文件暴露出来的方法来得到某个

■刁』■∏|‖‖』■|二■司|」■乙■‖」』●■

调用Native语言的-种特性(这里说的Natjve语言通常指C/C++)°有了JNI,Java就可以调用由C/Oˉ+

代码’我们可以通过这些代码的执行逻辑大致还原出对应的C/C++代码°那有什么工具可以做到这— 点呢?目前比较流行的就是IDAPro工具。 ■

本节我们会以_个实现了Native层参数加密的App为例’初步分析其基本情况’然后试着用IDA

(‖

Pro逆向它并还原so文件中的逻辑°在这个过程中,我们需要用IDAPro工具对so文件进行静态分析 和动态调试,以便更好地理解SO文件中隐含的逻辑。



IDAPro的英文全称是InteractiveDisassemblerProfessional,即交互式反汇编器专业版’大家也称

之为IDA。它由_家总部位于比利时的HeXˉRayd公司开发,功能十分强大’是目前流行的反汇编软 件之-,也是安全分析人士必备的一款软件。

IDAPro最重要的功能便是可以将二进制文件中的机器代码(如010l0l)转化成汇编代码,甚至

】|■

可以进_步根据汇编代码的执行逻辑还原出高级语言(如C/Oˉ+)编写的代码’从而大大提高代码的 可读性°IDAPro不仅仅局限于分析Androjd中的so文件,它可以处理和分析几乎所有的二进制文件’

□■■■司■■可』■‖■■

↑.|D∧尸『o的简介

Windows、DOS、Unix、Linux、Mac、Java、.NET等平台的二进制文件都不在话下。另外, IDAPro提

||

总之, IDAPro是一款极其强大的反汇编软件’已经成为业界安全分析必不可少的_个工具,更 多介绍可以查看IDAPro的官网。

纠■□■■∏‖■·司

供了图形界面和强大的调试功能,利用它我们可以直观地实时调试和分析二进制文件。除了这些’IDA Pro还提供开放式的插件架构’我们可以编写自定义的插件轻松扩展其功能。

·‖‖』



卜》卜|旬●》)户



l38利用IDAPro静态分析和动态调试so丈件

665

2.准备工作

由于本节需要用IDAPro工具对so文件进行逆向分析,因此首先要安装IDAPro软件’具体的安 装方式可以参考https:〃setupscrap巳center/ida。 其次需要准备-台Android真机并ROOT,注意这次不能使用模拟器’因为动态调试的过程需要 用支持ARM指令的设备运行,这里我使用的Androjd真机是Nexus5,Androld版本是ll,CPU是32 位.准备好真机后,需要确保能使用adb命令在电脑上成功连接到该真机。

最后就是示例App了’可以打开https://app8.scrapecenter下载安装包°本节我们需要的运行结果

p

p

和之前的结果是类似的’不过这次不是在Java层实现请求URL中加密参数token的加密逻辑,而是 改为了在Natjve层’真正的加密逻辑在so文件中。



l份

3。抓包和反编译



首先在Androld手机上安装示例App,然后使用Charles抓包’抓包的具体过程可以参考l2.l节

p



的内容’抓包结果如图l3ˉl00所示°



ch二“已月6】p…●■…↑山









G





…M网‖ˉ雨Ⅵ亏}…而`叮◎℃耐



P

促°▲鲤 ~=—-=_——≡=圭~=

「 卜



γγVOVw…a【∏…γγ塑『…盯【…Y…w哩. 唾γ…≡=Ⅶ●…乙『DU`

■硒

峙啼‖mm陶『吨…….

√≈画伯

…●醒西……■

盘…?助…∑矾j0以-…吵■

0

鳞≡泪…lO●吐…呻V■∧…ˉ→■…



山■…山蛔3川0。0

乒吠

翅?喊恼↑=m巳"∏■↑≡↑雌协占颤宙汀u3



^…啼油镭……r测《

仓…樟

P

侣蕊『;瓣{ ■$铲宜∏】° ■『‖‖【『·』〗『·■】』【‖‖·‖■









■=8拎■●=…囱0 ■●l1●薪自 ■r1…O 1【bm【t·p 11…t1跑△■■ ■c…尸E·们t询G2〃尸…▲t■田锰…凹…d″忠…●锥…yB●87…●儡】cm…”仓j………→』父】铲0

国色●k…了m●■8 1…·鳞”00 P…M…O旦D■1畔1H■迹0 ■■…0蛇ap

■0g●7铲g0·Ⅱ0

口…2獭Z■〖『■…凸·…p■m〗· .w≡0 ■=—°■….…谗》■≡十=洒◇…』△●=‖吨■《冗呻芭m嗽

》‘ 《 ●崎〗蜘o ■…■2■…?

■●l…£ p“α制广● ■…尸【 ■肮nU0』〃p1≈蛇…,蹿£′■v1e/■〗■●7↑…1“凹…7囱7≥咽γn】】0〗7·』…四几】e=】铲o

=c■…于…■0 6■伪肇厅】砂 ■砷〗…·8鲜沁8〗“←』沪『



…彭:…0

■Dc面寸20·10

●…$■0炉函G■…0 ■…◇西0…lp

蛤=8蚀F片睡够—p吨和.■¥父…唾”……x……w■皿…n 》‘ 《 ■』矿8凹O …田【…o ■DM″习■习咽啮邯侧■□ 同肘啄:



潍…■

『mq

"唾…闸…[蝎owm』 {鸳ow回』优函

唾丁拙"8……………悸…厄6凶■人卜·“=5h68q歇36咱06qO0赡额o…E…『獭U7…吟≡助… b

图l3ˉl00 Charles的抓包结果

可以看到’请求数据的URL中带有-个token加密参数’而且每次请求时的这个参数值都不一样。

返回结果和运行ApP后展示的电影列表是__对应的,包含电影标题`类型`评分等数据’我们本节 就是想爬取这些数据’因此需要把整个URL的参数构造逻辑还原出来°

为了找到加密参数token的加密逻辑,我们先用jadxˉgul对apk文件进行反编译’并做初步分析’ 反编译结果如图l3ˉ10l所示°



匡汪9Dˉ:ˉˉ



■■●●●●●

曰丛oqP.呼罕绍…$°O·甲.p■q弓Q呻……….x』

=一

D■面吨7』t2

■■甲文件



||

世堡四b



二`书,≈7■

■儿…』沁·匆

■N阑

,·鳃瞬

D■…t●3 』鲍吨$





…广…〃…尸…≡t◇…宁…酝它……F■

| |舅 |l



园………渔` { }搬 ■c皿…·≈

」■……·■∑c

尸…■…如≈

||

≡∏

…………肌产…1或…omkⅡ皿tM

↓饿节狞Ⅱ〗乙

|荔 F雹髓瓣:麓藏蜀……国驯《

蹿口…=嚼s·

蝉●…■‖≈→〗》山l斡J ↓』0超【广…已■宁…●…〃7…山…0)』

》』 ≡Ⅸ雪哮堕g唾墅狰≈h鳞″噬圃w瓤幽…愈《廖颧…嘲"哮鹏 》 『ˉd| 母Ⅳ汹

g

图l3ˉl0l

反编译结果

从图l3ˉl0l中框出来的内容可以看出, token的值是调用e∩〔rypt方法得到的,需要传人5tr1∩85 和O仟5et两个参数’5tr1∩g5是一个列表[』/aP1/川Oγ1e』], O仟5et是数据的偏移量’这和前面案例 的分析结果非常相似°

进_步追踪一下e∩〔Iypt方法’其实现如下: pl」b1i〔〔1a55[∩〔rypt{ pub1j〔5tatj〔5trj∩ge∩crγpt([j5t〈5trj∩g〉5tIi∩g5’ i∩to仟5et){ retur∩‖atiγe0ti15.e∩crypt(「ext0tj15.joj∩(园‖{’ 5tri∩g5)’ o仟5et); }

可以看到它里面又调用了‖at1γe0tj15类的e∩〔rypt方法,该方法的第_个参数是5tri∩g5中的 内容拼合之后的结果,也就是/ap1/咖γje这个字符串’第二个参数还是o仟5et。

pub1i〔〔1a55№tiγel」tj15{ pub1i〔5t3tic∩ative5trj∏ge∩〔rypt(5tri∩g5tr’ i∩ti)Ⅱ St己ti〔{

5ysteⅦ.loadUbrary(圃∩atjve口); }





睁■■■ √些ar酗=γ8己

可以看到’这里并没有e∩〔rypt方法的具体实现,并

、′困己厂…b1=wa

Native层,即e∩〔rypt方法是用C/C++实现的。另外,在 e∩〔rypt方法下面,可以看到对1oadL1brary方法的调用,

圈1jm■t1γe°So √团×86

■um己t1Ve°5O √囱×痴矾

囤1止【]at』γe°So

传人的参数是∩at1ve字符串’这里其实就是指定了So文件

》蹿厂e5

的名称’在apk文件里会有一个叫作libnativeso的so文件 隐含了e∩〔rypt方法的实现。

国∩硒『o1…∩1↑e5t.m1 圈c阻55eS.dex 》园「e5山「Ce叁°a厂sc

我们继续观察反编译结果’如图l3ˉ102所示°

图 l3ˉl02反编译结果中的资源文件

可以看到资源文件里的lib文件夹下正好有libnativeso文件, 这就是刚才所说的so文件°ljb文

0

□』□||·|』』勺||《‖|‖‖|』■□|』』《‖|‖‖|‖」‖』■■□

且方法声明中多了_个∩atiγe关键字,这证明实现过程在

■um已t1γe°SO

·||■】‖||』■■■∏‖‖‖‖』■■■|‖』】』■■】‖‖|·=■

接着我们看一下‖ative0t115类的实现代码:

‖■||』■」■]』‖《‖·】』]叫』』·|·|■]‖』●】』■‖】』门』』·■‖‖■`||司|‖」·〗‖口司‖〗·‖』■】】甲|司‖‖〗■‖』■可||·||‖‖‖司|‖」□‖‖||·‖·Ⅵ|

■吨m

p■·咕Q…t…t… 》■…m°『…1唾 ■…



}|爵罢

攀论矿呐1屿·…回Z6』回 ■t≈1mQmu?…h2 》■……

凸≈←

卜■□■



,;灌熟簿|{|嚣

幽≡

ecm伪】由…~…仙z,臼鱼′郝

】‖】‖』■■■‖‖]|』■可‖』]|旦■■‖‖』■□‖|(〈■■〗〗〗』·■】】【□■■】【』∩■■日■‖』〗□■■】‖‖』|创

第l3章Androjd逆向

666

|| |

| ‖3.8利用IDAPro静态分析和动态调试so丈件

667

件夹下—共有4个文件夹,分别是aIm64ˉv8a` armeabjˉv7a`x86和x8664’llbnative.so文件可以运 行在使用对应指令架构的设备上’这些设备分别如下°



‖▲■

□arm64ˉv8a:适配第8代`64位ARM处理器’主要是Androjd真机°

△■■■ 『

□aImeabjˉv7a:适配第7代、32位ARM处理器’主要是Android真机° □x86:适配X86架构` 32位的处理器,主要是模拟器或_些平板设备°

》β‖|卜◆「

□x8664:适配x86架构` 64位的处理器’主要是模拟器或一些平板设备。

要想知道自己的手机是用的哪种处理器,可以运行该命令来获取: ■ 巴

adb5he118etpropro.pIoquct。〔pu·ab1

『∩[■卢 ‖■■

由于我使用的是Androjd真机,而且CPU是32位的,所以运行结果是: ar爬abjˉγ7a

卜▲■厂

这样当App运行时’就会加载执行armeabiˉv7a文件夹下的libnativeso文件° 4.静态分析

p

现在我们使用jadx≡gui工具把so文件导出,然后根据实际情况用IDAPro打开so文件°这里我

■尸巴■■■■—矽‖

使用的是armeabiˉv7a文件夹下的libnativeso文件’如果你的手机的CPU是64位的,可以使用 aTm64_v8a文件夹下的so文件。

打开IDAPro后,直接把so文件拖人窗口中’就会出现配置选项’如图l3ˉl03所示°

巴■厂△●厂■‖|

…ˉˉ皇啸p…dq厕…恤

9吕



阐|

L囱d∩妇′」S硕s招田∏哪咖碱№钩■s

ˉ—-_ˉ

■■

憾耐=…v∩阳 } |助■Y№ 什

β β|广 广|「‖■厨『尸|

■『仿|■◆厉‖卜尸



′似



「) ‖



|…__ |! } 0,,叮日胃舔雨w7 ■—一 e■雪达 _~-—

=~=-■——







R呵w婶

|………遮咖“ooo°。o

…°’…`

…|呻…

}……0:鹏…=≡气… | }

}蕊磷

§龄…

|删:= =≡识… 【

图l3ˉl03 IDAPro工具的配置选项

可以在‘Processortype”中填写处理so文件的方式’这里已经默认选好了ARM相关的处理器, 我们直接点击“OK”按钮’保持默认配置即可。稍等片刻后,就可以看到IDAPro把so文件的内容 解析出来了,如图l3ˉl04所不°

可以看到’页面左侧是SO文件中的_个个方法及声明,右侧是sO文件的反汇编结果’都是一些 汇编指令。和我们平常见到的用高级语言(如Java、Python)编写的代码相比’汇编指令的可读性要

差很多’几乎都是底层的一些操作寄存器的命令,难道我们要一行行分析汇编指令把逻辑找出来吗? 这就太烦琐了°

—-

↑3 ■



‖ 668

第l3章Android逆向

‖‘





』 《 □



IDAPro有一个非常强大的功能,就是可以帮我们把汇编指令转换成可读性更高的C/C++代码, 怎么操作呢?我们来看一下°

通过刚才的分析,我们知道e∩〔rγpt方法在so文件中,于是按这个方法搜索-下’结果找到了 _个〕avaˉ〔oⅧ一go1dze-Ⅷγγ"‖ab1tutj15‖日tiγe0tj15-e∩〔Iypt方法’点击该方法之后就可以看到对应

■□|■日』』‖』·』Ⅲ■司·』■‖■‖‖‖‖』■‖□■|]|司|‖■■〗|』□‖」■]|』■Ⅵ■■|■|●∏|■』■■‖‖‖勺‖』■■||」■‖|』■]‖』·■

图l3ˉl04解析出的SO文件内容

的汇编代码实现,如图l3ˉl05所示。

■■Ⅵ□■可』··■■·Ⅵ·|」】■、‖口」■■司‖』■■二=■|■■||‖」■|‖||』■■∏』■■■」■】■■」■】·‖|』■■』■■‖‖』■]勺』■】|■‖|』■■■■■

图l3ˉ105 e∩Crγpt方法的汇编代码实现

l3.8利用IDAPro静态分析和动态调试so丈件

669

接着在右侧最上方的区块中,选中〕aγ己ˉcoⅦˉgo1dzeˉ‖)γⅧ∩ab1tuti15‖at1`′e0t115—e∩crypt这个 方法名称’使其高亮显示’如图l3ˉ106所示。

图13ˉl06高亮显示选中的方法名称

此时直接点击F5或者从菜单中选择View→Opcnsubviews→Generatepseudocode选项’代表生

||



成伪代码,如图l3ˉl07所示。

|v{·瓣 o.bu…『 o."°厕, Wi耐d。w雹 剧·|p ■ ■P化

融■瓣

G「ap们s Ym‖m『S



国Ca|cu}ato「… 仁Ⅷsαee『`

血α□p0`◎γe『v{ew

固Rece献scm恼 智·at固baS凰s∩ap蹦.t顾a陋ge「 嗣pr|∏tSe口Ⅷe∩t『凶喊e晤

~=-

咸Qu‖c和vj·W

≡≡铂



勘酗sesse砸b‖γ h片UⅫ耐tyb「o瞻·「 固丹exd咀∏p





} 罢因:

》》》

Ope"5U◎v『eWa





『】

Sh硼TDb曰a『

^q■

影g

温旧xp。『【雹 晒帅p°「ts 面"smes =



图l3ˉl07选择Generatepseudocode选项

|`





之后原来的汇编代码就被还原成了C语言代码’如图l3ˉl08所示°

严刀

●●●●■●●●●●●●●●●●●●●●●●●●◆●●●●●●●●●●●●●●

‖「尸「|}|巳■「}卜》|■■『|■■「|△尸「)‖■■尸|甘■||△尸|■【「’■■■尸■『·「炉『■‖|『〗『『|‖■尸■「‖}■尸『[尸『|匡■■『

}】{】〗{‖〗

p

≡■

…-

翱蝴……蹦……瓣………



例出Ⅳ●伊办泅凹山砷酗泌廖咖旧酬…”血酗凹”四狮酗酗耻测皿皿蜘喇呵喇酗酗咖叫凹酗钟酗酗酗“蜘”叭凹凹必””呵邱恤“酗酗呐

』′ 0.

鳃 喇鹏喂〃〃〃〃〃″〃〃〃〃〃〃〃

■!〖



镭…



!="窒谭=:盅;!=莹i耀=;=躲《■u…)彦《 廖霄γ翻:‘胃铲四凶=,■…`ˉ…`°α巫~…→,`≡约ˉ…巡■(…』′ ■=∏0_◆≈a0=‖0哑』—00→0mT■》0 ●》的B7◆w》0

■…O……0~『…0=…0∏型—■O…凶…《隘】90p

■=0U_·…0=0四—c…=鳃伍110的汹00 0=…〖……=…0=■pu—β■=归■…‖脉`』0『



!二墅;;墨=蹿‖二起;遇…=:{=量癣墩瞬{} l二蛊!链=蹿『=;;殷=!…{篇l↑}}

忘=Ⅲ…』w

图l3ˉ108还原成的C语言代码



整体代码其实并不多’我们可以分析—下,首先是_个很长的方法调用’而且这个调用连续出现 了很多次,类似下面这样:

5td: 8 ∩dk1; ;b351cˉ5tri∩g<char’5td: : ∩dk1: :〔hartrajt5〈ch日r〉’5td: ; ∩d|〈1: ;a11ocator<〔har〉〉; :≈ba5j〔-5tr1∩8 (8γ2O);

这里其实就是调用了∩d促中的ba5iCˉ5trj∩g方法,功能是把γ2O变量转换成—个字符串°另外, 代码中还有几个看不出具体逻辑的方法,例如5ub「8O4、sub「85O等,我们可以逐个点进去看看,这 里点开5ub「8o4方法: j∩t +己5t〔a115ub「8O4(i∩ta1’ j∩ta2) { u∩51g∩edi∩tγ2j //5丁o8』 i∩tγ3j //rO 1∩tre5u1tj //rO j∩tγ5j //rO

j∩tγ6j // [5p+〔h][bp-14h] γ6=己1;

γ2≡*(ˉ咖R0*)(a1+4); i+(v2〉=*(咖RD*)5Ub1O9「[(〉〉

】‖‖■■}■■■〗∏‖‖■】‖‖』■·‖』‖』■■‖〗□】·■■□■】】■■■■□□■·〗】〗〗■■‖‖|」·■‖」∩‖‖●』■■□□□□】●■□日■】■■‖‖■可‖■】【』·

第l3章Android逆向

670

d



口□■■

5ub10M0(); re5u1t=5td;: ∩d氏1: :vector〈std: : ∩d代1: :ba5j〔5trj∩g〈char’5td: : ∩d代1::c∩artm1t5〈char〉’ 5td8 : ∩d促18 :a11o〔ator<c∩ar〉〉’5td: : ∩d代1: ;a11o〔ator〈5td: : ∩d代1: :ba5ic5trj∩g〈〔har’ 5td; : ∩dk1: :〔hartmjt5<〔har〉’5td: : ∩d代1: :a11oc3tor<〔∩ar〉〉>〉: :—pu5∩-ba〔低51ow-pat‖< 5td: : ∩dk1: :b日5i〔StIj∩g<〔‖己r’5td; : ∩d促18 ;〔∩日Itrajt5<〔‖aI〉’ 5td: : ∩d代1: :a11oCatoI<〔har〉〉〉(γ6’γ5)j 1



5l』b1OMO()〗 re5u1t=5td: : ∩d虹: :γector<5td; : ∩d促1: :b己5i〔5tIj∩g<〔∩ar』5td: : ∩dk1: :〔帕rtrait5〈char〉’ 5td: 8 ∩dk18 :a11oc日tor<〔∩ar〉〉’5td: : ∩d代1: :己11o〔ator〈5td: : ∩d|〈1: 8ba51cstr1∩g<〔har’ std: ;

∩dⅪ: :〔hartmjt5〈char〉’5td: :

∩d代1: :311o〔ator〈〔har〉〉〉〉: 8

co∩5tru〔to∩eate∩d〈

5td8 : ∩d仪1: 吕b日5i〔5trj∩g〈〔har’5td: ; ∩d促1; 8〔h己rtrajt5<〔har〉」 5td: ; ∩dR1: :a11o〔atoI〈〔bar〉〉〉(γ6’γ3);

]|{

e15e



} ′

retur∩re501tβ



因为生成的是伪代码’所以有些语句并不完全符合代码的编写规范,经分析’5ub1OMO是-个

空实现, j+分支和e15e分支分别调用 pu5‖ˉba〔代_51o"ˉpat∩方法和 〔o∩5tructo∩eate∩d方法得 到了返回结果re5u1t。查阅相关文档(如LLVM的文档)后,发现5ub「8oq方法就是列表的pu5∩ba〔促 对其他方法’可以按照类似的逻辑分析’例如这个调用:

』●■·

方法,功能是把a2指向的变量添加到a1指向的列表变量甲.



5td: : ∩d代1: 目ba5i〔5tr1∩g<〔‖3r’5td; : ∩d代1: :〔∩己rtrait5<Char〉’5td: ; ∩d|〈1: :a11o〔ator〈〔‖ar)〉: ;b己51〔5trj∩8

<deC1type(∩u11ptr)〉( 8γ19’

"9+dL∩〔jγM「×Qbri")j 5ub「8O4((j∩t)8γ21’(1∩t)8γ19)j

0 ‖ 】 ■ ■ ■

可以看到,这里先通过ba51〔5trj∩g把—个常量字符串9+d[∩Cjγ∩4「X0br1赋值给γ19变量,接着

‖ ‖ | |

调用5ub「804方法把γ19代表的字符串插人γ21指向的列表尾部°

‖ 日 ■ 】 ∏

{〗|

(l)初始化_个空列表γ21。



现在我们大概总结—下e∩crypt方法的实现流程。



再往后’还可以观察到对t1Ⅶe` jo1∩、 5∩a1` b64e∩〔ode方法的调用’虽然我们不能完全确定这 些方法的实现细节’但大致可以推测出一些相关的逻辑是怎样实现的°

■□■纠·■』■曰■■司





l3.8利用IDAPro静态分析和动态调试so丈件

67l

(2)把a3赋值给γ5,然后转化为字符串赋值给γ20,再将其插人γ21列表的尾部。 (3)把字符串9+d〔∩〔1γh4「xQbri赋值给γ19,然后插人γ21列表的尾部° (4)把a4赋值给v6,然后转化为字符串赋值给γ18,再将其插人γ21列表的尾部。 (5)获取当前时间戳γ7’然后转化为字符串赋值给γ17’再将其插人γ21列表的尾部。

(6)调用jo1∩方法将v21列表中的元素拼接在—起’拼接字符对应的ASCII码是44,即拼接字符 是-个逗号,把拼接结果赋值给γ15。

(7)调用γ15的5ha1方法’把结果赋值为v16°

(8)接着按同样的逻辑’初始化_个空列表γ10’然后把γ16和时间戳γ17插人这个列表的尾部。

(9)再次调用joi∩方法将γ14中的元素拼接在一起,拼接字符依然是-个逗号,把拼接结果赋值 给V13°

(l0)把γ13转换为字符串,然后赋值给γ11° (11)对γ11进行Base64编码。 (12)再进行一些字符串的赋值转换后,返回。

[ 『

} p

以上是我们观察还原后的C/C十ˉ+代码,并加以_些推敲后总结出的大致流程’但内部的具体细节 我们还是不知道’例如进行的Base64编码是否标准,以及一些细节是否真的和我们推测的_样,这

…■ ■…

些都是待验证的°



所以,我们接下来借助IDAPro的动态调试功能真正运行_下e∩crypt方法,看看整个过程是不 是和我们想的-样°







5.动态调试



卜 p

0

p



P p



要进行动态调试,需要额外做_些准备工作°

首先找到IDAPro安装目录下的dbgsrv文件夹,里面第_个就是androldserver文件,如图l3ˉl09 所示,我们需要把它放到手机里,然后运行,类似f丁idaˉserver那样°有了它,电脑上的lDAP「o才能 和手机连接起来,从而实现动态调试° 厂

●●● 〈

》 db0s‘v

鹤漂彰 色。

田◎

》Q





P









′ 卜

md『田◎…呵0g ……m▲=g●w●∩d『哦d=x8…w额灿…S●『呵

●…d=s·…







β

0

怕●=m#lub.o‖



■■■ ″而颐

湘‖Ⅸ=s●w●『64

硕$c绅Ⅳe了



D



) p

…=画vα铀

卸顿割γ四



!

■壤赠

叮∏ot蝇Q々晒"“=旧…e上『喊『…『…●『c 酬们3兔≡℃『"O$·.■W白0凹『■∏ot蝇Q ■xe ●He

丽n回

p=□「Ⅳ`°■呻

图l3ˉl09 dbgsrv文件夹的内容

『 ∩











接着使用adb命令把它放到/data/1o〔a1/t阳p文件夹下,命令如下: adbp05∩a∩dIoid5eIγer/data/1o〔a1/mp

如果你的手机CPU是64位的’就把androidserve协4文件放到对应的文件夹下’命令如下:



|』‖‖」■■』‖‖‖|二■Ⅵ■■‖』=■■】■``‖

672

第l3章Androjd逆向

3dbpu5ha∩droid5erγer64/d日ta/1o〔a1/t阳p



接下来,运行adbs∩e11命令,进人/data/local/tmp目录’并切换到Root模式,命令如下:



adb5he11

$〔d/dat己/1O〔己1/mp $5u

再给androjdserver授予执行权限: #〔∩Ⅶod777a∩dro1d5erγeI

如果你的手机CPU是64位的’就执行:



#〔h∩↑od777a∩droid5erver64

『】□』勺‖■■』】●】‖■∏‖□●‖」

之后运行androidse『ver或androjdserver64即可 ·/a∩dIOjd5erVer

整个操作流程如图l3ˉll0所示. ●÷8 d

向∩∩们】

o创b5heu

o『meF‖Teαd/$C创/勒αtα/1oCα1/tⅧp



α!me尸∩eαd·/dotO/1OCα1/t们p$su ∩砸"e了|ve□d:/d∩tα/I◎co1/t爪p孝c∩‖∏o蚀777αm庐o1d=se「ve厂

O∏Pγ}e「∩eO口;/dαtα/I◎Cαl/t〗Ⅵp# 。/α∩d了O1α-5e厂γeF DA∧∩d「o1d32七1t「e!Ⅶote“bug臼ePγe尸(S丁)γ1.22. ‖exˉRαy5(c)20鹏ˉZ017

√ˉ□|』】』■□■‖』‖||」■刁』□‖‖』■】‖』·

图l3ˉll0所有准备T作

在默认情况下’androidserver会运行在手机的23946端口上’为了能够在电脑上访问到该端口, 需要配置-下adb的端口转发: 日db+orⅣardtcp;2〕946t〔p;23946

这样访问电脑的23946端口,就相酗j于访问手机的23946端口了。 0

这个选项用于连接—个远程的Android调试器,其实就是连接刚才我们启动的androidserver’下

面我们填写地址和端口’地址是localhost’端口是23946,如阁l3ˉl|l2所示。 De战咆蹿= ○pt『即s

W{∩d。Ws

针e腑





谷比b汕s熟pp‖c·0‖”zs酗p:曰mt0航m

||

现在打开手机上的示例App’让它运行起来°再新开_个lDAPro窗口,在菜单中选择Attach→ RemoteARMLjnux/Androlddebugger选项,如图l3ˉlll所示。



i趟蹬|饿鳃::岛,. ·

"O丁巳a‖四枷sⅧUstbeV巳‖do∩t陀『e‖WOteC◎m问ute『

■罐…蜜m

Loca|p‖Noebu99e「

L◎Ca|Rep|aye『debugge「 Re刚◎teGOBdebugg巳『

且◎st∏日巾e

bc□』m$t

■p◎〔t

″S5Ⅻo旧



∩e们otek『∩ux创eDugge「 ∩e爪◎teⅧacOSXdebugSe「

Saven臼two巾蹈tt航庐mdefau扯

∩e们°tew{∏C巨qebugge厂{丁CW|p) ∩e而OteW↓∩doWSdebugge丫 ∩e丽ote‖OSdebugge「

图l3ˉ]ll

c日∩吨‖

ON

图l3ˉll2填写地址和端口

点击OK按钮, IDAPro会提示我们选择要挂载的进程,如图l3ˉll3所示°

」 | |

RemoteARMLlnux/Androjd debugge『选项

‖刨p

239“■

』■|』日‖‖《‖』■‖‖‖‖·勺|||■■

雨酗vA∩豌恿阿



||

l38利用lDAPro静态分析和动态调试so丈件

673

283‖冯 2836巳

Ⅺ巳q



2B5



2B7

28G

288

289

29o 29Oq9 β

}‖‖

29↑65 ‖ ■ 厂 | 伊 匹 尸

n92 29qOO

29q56

29Q6O 29▲63



29Q65



″ 3O2



′……m…=…ˉ蜘…

S]6

|叼芯…肆…ˉ划●…∩

卜‖■厂凸厂

3w

时S0…呻h

q32

■γ啦皿“啊

4780

巨m》a∩宙刨d…冶

75O

巳mV.■硒酮玲$归蜘帧

S46

巳om、gO叼GE∩口烟回ow3.p■凶$帕∩t

92O

…口破…●∩.hu∏@h盯

自· ↑

· ∩

■□ ←◎



■」



p

图l3ˉll3选择要挂载的进程 》尸

我们找到对应App的安装包comgoldze.mvvmhabit后点击OK,稍等片刻,就会发现IDAPro停 下来了’如图l3ˉ]l4所示° 毡呛

∏ ‖

…。…罕

串师

q』 .?.a瞬伊E蹿! .矿

·蛔■■



≡……

丙■‖

■「

旷孤v■

L……《m■

●娠

=知

d■

m…静蹿

己●◎ …………≈……■

卸雨●

β 〗

倒 $

伞 . 酝■ 日 .申t■丛Mq0佃 ■■·

……………

●●·■■■●■·■

烬山…幅………

鸯丫丸乎

■邢

●T≡●■

■凸丛80巳b凶… ■乙皿10Oq酝…■

■化驴巳n↑

虽…山0

■耶F…$°

≡…

■■□■■

≈D d · 孪

o函0M□■凸…酒■ ^0凹〗0各■具4■



出o ■色· p …7p日坠v…

岳…

≡■

红■ ■



… …■

■■■

O



0





咋0●『■乙△■G午【炉■凸

·凹0丛l0巳巴二4

■试■



曰●钞

■摆

■‖广■队|》任卜伊}△β卜‖‖尸巴β广■β}》β「

广≈』□

幽■

……… 叫拿…

b

■ ■ ■



0■凸

‖ ■

9

-■ ●7=口

·兰

▲.=●●■■■■■■凸

=●■

辟■■·…

唾≡

鲜■工…

……

够B…



…■…

咖…

=^宅内∩∩向

……∩

…■

f惩■■

□□

酝■



吨■



化□■□



□□ ■·□ ■

■■

□■■=□

□ □

=□

狡毗…

】…■

巳●@



…心…〖

… …

寺…馋……

■■



■■■■■■■■

■…

……≡△g



0Q … ″咆

№Ⅲ…?

□●@P曰. □o◎

…0o阁:溅;『!;潍;嚼器舅"胳舅爵蕊:; !? |『‖}|■尸||『■「巳■匹■『『|{■■「}|卜『‖[『‖●『『‖止厂〖「‖‖【匹厂巳『‖■「【『‖【●β|

′p“· ■P怀00已〗 P●邹m切蚀〗0鸯0M0■ ,0r『 色00弓 〗■ 日0e0』∏ 4●@02己M■〗】哩M

=,雨



·



■■■●■■■■

0

;; |`,



缚0GOO《0PGo·但■功h·卧岔已0沁凶拙O· ■0 □ 豁t·00c付0·cf 0T呐0O0●鳃帕”蜘β·00 p◇ 0■■●·O00·Q■●绅忧“96eQ沁四硼"·0 ■ ■ 沁■O000●●Q吁f 巳0S■ 0●呻斡归“铡”00 □ □ 搀田0■0■■仁押可0e 乙◇导·的恤“w·0G0 ° □ 古0≈0O0(●0SG勺G帕吕吕o·“·6“的■甘·· □ w■●0tc00吕 【白凸■凹0 0白§●的哟“幽“G0 · ■0牌·00P·臼hO6G■回 □■00和筛凹馋0Oβ秒 po

必·■□

0D牟

●●■■

O■

0

■吕■

■■▲■■P■

芭D【

……≡



■■L

D■

………



□@@

“叫凹钠巾w 凸■■■■■□ ■ ■

■■■

G●■p=

■■■■■■■■■■

■■■■凸申■■■■■■■■占=■■

■·■

■』=■■■

■■■■■■—●

出马溶·0言◇:0·←【■p邑巳□■■■ ■Ⅱ〗』〗 0 b8□0■ 02J0 巳□r…』●皿如·』◇L·0 · 名凹0■b】00■ 日0■】● 蚀O】 □●』日唾■■】叮■p■0■■■0■■■■■…』叮 ·…0■…v】■ ■■色9l■】 □■■·■l0□ g6■砧【……恤■Qi…■匹0…0……口出b=



图l3ˉll4 IDAPro反编译的结果

我们可以在右侧的Modulcs面板中找到已经加载好的ljbnative.so文件,如图l3ˉll5所示°

||

| q

674

第l3章Android逆向 曰@@

卫Mmu℃S p■【∏

鳃S■

S『工O

可厂厂∩∩∩∩∩

∩∩∩∩司∩∩∩

g ==0…·■△

L口■■二』

■夕·■铝p■·f=厅→§■■=^=◎∩可尸^■



鹊ˉ →电

llhe洞of2显’

图l3-ll5

Modules面板

■■□二■■

双击进人llbnative.so文件.查看其中定义的方法,如图l3ˉll6所示。 @豆

M。du|es ∧dd呛Zs

N■『∏e

D~mjo『Ⅵ∩长NSt6-∩毗↑6γeαo""S≡↑2baS‖QS《『J们g‖C

7臼9C27q

画ˉ乙NSt坠m代↑↑2bas『cˉ雹t「‖∩g|c川Sˉ训cha『 t「a『ts‖c巨《 回-ZNSt6 ∩d仪‖?2b°s‖cˉSt「i∩g‖c"S-训c晌f≡t「a『ts|c巨』 回ˉZ4s∩a↑川St6-∩dⅨ↑‖2bas‖c-st"∩g‖cNSˉ"c幅rt『a

7曰9C▲q8

饲7∩h∩△即m‖lR‖St∩

n∩kq↑’∩冈刽阮气↑『mα‖伯川aj让

7僵↑9C36O

7曰9C邱90

7庐‖9们4F∩

kl∩e0◎f228

图l3ˉll6 libnative,so文件中的万法



我们找一下刚才在静态分析中找到的〕aγaˉco肌ˉgo1dzeˉⅦγⅧbabjtˉ仙t115-‖ati`′e0tj15—e∩〔rypt方 法’如图l3ˉll7所示。 Mm止窒

@虽

@豆 ▲∧“乙=

…呛



|o」svaˉcα∏ˉg。脑ze皿vvm∩sb『!_utj『昆"at↑γ臼Utj『se∩…7卸9c62O

d





画ˉz↑o|n‖tˉ己『圃w 7旧]9c568 画ˉX耳尸i∩即"St6-∩α龋6voct°删S↑2ba剧c-st∏∩9|c.。· 炬09C27▲ 日ˉZ4sm0"St6≡m阉02bO制αst外∩glcNS」‖chaLm…7曰9Cq90 俩7qh∩▲p∩i弓嗣刚R↑∩ O毗↑↑’m哼‖厉弓Mm|∩"∩?0民

7尸0Q价d尸∩

蜘穗l碱卫2日

图{3ˉll7找到]ava_co们-go1dzeˉⅧγⅧ∩己b1tuti15‖atjve(」t115-e∩〔rγpt方法 双击这个方法,即口I在IDAPro的左侧看到对应的汇编代码,这和刚才静态分析时看到的↑[编代 码几乎是一样的,如阁l3ˉll8所示° 翰‖D∧ⅥeW_pc

□@°

i韭贝■仁儿vQud■Q5?E】9侥b1r匹B

11Dn■七土ve.s◎g7E19C620 ;



ˉ.

0

…≡ˉ ■-÷ .令=…镭.==.ˉ=ˉ. ˉ. . .=

ˉ .ˉ .….ˉ

..

11bn■七1v●·曰◎87E19C620

1上Dn■七上V●.白◎:7E19C620J巳V■-C…g◎1dZ●mVmh■b1亡u七土1■H●t土V·Ut11邑一enCryP七 Mh励■七xve·s°87E19C6乙0PU旦∏ {R4′R6′R77IR》 R7p SP0 p8 SP’ SP′P0xB0

Ⅷh∩■+‖v●.巴◎目7E19C626LDR

R4′ ■(◎ff-7E1B7Bp4=0x7E19C62C》

I0h门■亡1v●.日◎【7E19C62ALDR.W

R12′ [R6】

Ⅶlm■十〈v●.s◎87E19C632STR.w 〗lbn□仁iv●.■◎87E19C6365m Y卜w▲七1ve.巳◎87E19C638STR

R12D [SP′00xB4] R0′ 【SP′F0x2c] R1‘ [SP『F0x2B]

]{b∏at1v巴.巳◎【7E19C63CSTR ]{bna七土v●·s◎〗7E19C63ERDD

R3g [SP矿◆0x乙O] RO日 SPJF0xA8

`『hD■士±ve.s◎宫7E19C644LDR 『d胎∩■七止v●·S◎87E19C646L□R . 11bn巳七1V●·gOl7F19C648HDD

R1’ [SP’伊0x2C] R2′ [sP′◆0x20l R0’ SP′ *0X9C

Mbn■亡土v●·S◎吕7E19C628EDD

Ⅶbn■七止v●.巳◎i7E19C62ELDR·w

I(b∏■t1V●·巳O87E19C6dRBLX

—]lh■□亡1v●□曰°吕7E19C64EB

.

.鸟哲飞 .°日0α 弓山诬0』

RH′ [6Pp和x24] unk7∑19B894

Unk7E19B8A0 1唾7E19C650

1止bna七土vG·B◎〖7E19C65O ; ˉ.≡.ˉ…=…. .~.- ˉ→←≡= ˉ=ˉ .= . . ˉ..←ˉ ·^ .ˉ .

.

】§倘门■十↑v●·s◎87E19C650

◇ {翱疆;::::;瞬;::::猛7m…o

R°′ s圆′徽0xM

箭 c°°E…; 1」bn…·

U川Ⅸ"OW"7曰9CC2α0咖吨γe!Sαp狰蹿腮@m″!d乎匙皿V(Sy∩G∩『oO蚀edw‖thPC)

图l3~ll8方法对应的汇编代码

在这里’我们就可以添加断点进行动态调试了,点击代码左侧的蓝点,之后这个点会变成红色,

|』■】‖」‖|』』■∏‖‖』■】‖‖|」』■■Ⅶ〗‖』』■■】‖‖|」■■‖||』』■■■】□

】βhn●t土v●.□◎87E19C640∏丁ˉT

. 《》Zi .、邑ˉ:‖ .YjD0

R12p [R12】

‖□

. 1lb∩吐1ve·日◎『7E19C63nSTR

R40 PC

■■■

1§》mH七1ve·曰o87E19C622HDD 10h∏n七土v■·s◎〖7E19C626SUB

. .

整行代码会有红色背景’这证明断点成功打」二厂,如阁l3ˉl|9所水°

《■二■■』■

} |



l3.8利用lDAPro静态分析和动态调试so丈件 —=

_

固}O∧γ{ewˉpC ■

675

□@@

1土bh巳tive·S◎i7E1gC61FmB 0 11bnu亡土v■淘□◎】7直19c620 ;≡—一ˉ.霹ˉ一≡…~≡≡-≡= .=-ˉ==一=…≡唾-ˉ · .ˉ-= 一ˉˉˉ≡……~…… Ⅶk门■七土ve·■◎邑7E19C620

?撇灌慧_ 掣

】ib…仁1ve.s◎【7E19C62《SUB

SP’ sP,役OxE8

芯 ]§h闻■七1v●·e◎『7E1gC626LDR

M‘ ■(◎ff→7mB7B田→0x7E19C62C》

嗡 γTh∏^亡1ve奄s◎Ⅱ7E19C628HDD 刨 ■ ·■ · p ˉ ■ ≈■■^^矿pb■ ←=←■≈

; 嗡fi 『F1B7BM

R4′ PC■=■ ●

=●^



图l3ˉ]l9为代码添加断点

接着我们点击lDAPro页面左上角的运行按





钮,使App的运行恢复正常’如图l3ˉl20所示°

App之前已经打开过了,因此已经执行了第_

L『b「a「y↑u∩ct『°∩■∩egu|a「↑U『lCt(°∩■ˉ{nSt叫Ct『Qnˉ= Oata

次数据请求’那怎么再次触发断点呢?很简单’发 广

送第二次请求即可’我们可以在App中上拉列表’



辑‖D∧ⅥewˉpC

触发新的数据加载,然后就能看到IDAPro的反编

图l3ˉl20点击运行按钮

译停在了断点处,如图l3ˉl2l所示。



p p



P





● 『--



P



□@@

1』hn■仁1帕.曰◎z7E19c61FDCB

{此

Mbm忙w●□B◎【7E19C620 Ⅷh∏a七立v●.■◎t7E19C62

v□cmα◎1dZ●m吓们■Ⅷ」七u仁i1■日■七止m口七土1S●nC

事—_—=_

懦膘摄耀2几DD

R7p SPP sPⅡ sP’ SPp SPF 护0xB0

Mb∏□亡儿v●·s◎g7E19C624Sm `『h们n士0v●△曰◎】7E19C6∑6LDR 1爸bn巳七1ve°●◎87E19C628mD T0h∏■十0ve·■◎87E19C62ALDR● ·W 例 】fb武■七ive·巳◎』7E19C62ELm· ·w W 】;户协几十0v●△■◎87E19C632Sm·| ■Ⅳ Ⅳ ‖寸b而■十ive.S◎:7E19C636Sm Ⅶbn■十0v●.■◎:7E19C638sm 〗《胎税■士1v巳°■◎g7E19C63ASm

R4『■(◎ R4『■(◎f£=7B1B7BD4=0x7E19C62C)

Ⅶh向■十;v●·■◎87E19C63CSm lik∏■ 仑』沁·B◎『7E19C63E汕D 】lbm■十.Tv●·■◎27E19C640■丁当x ]0倘Ⅶ△七土ve.■◎『7E19C644mR 】‖h询n仑0ve·B◎【7EI9c646LDR ];伪■■十牡U■·■◎87E】9C648nDD ];伪■■仁儿U■·日 Ⅵ↓h■■+w●口■◎87E19C64A∏了oⅢ Ⅵ↓h■■◆1v●口□ 】《Ⅶ面刃七1v●°H 】4Ⅶ冗■七1v●°●◎87■19c6cEB ↑0隘■▲货土…·§ 11bn·仁土…·■◎∏7E19C650 ; == .]0伪愉■十《吨·Ⅱ 0《伪愉■+《吨°□◎【7E19C65O ]§…●企iv●.】 ]§雨■亡iv●令■◎27E19c6501匹 7E19C650

R0「 PC R4「 rC R12’ 『R R12′ 『R4] R12〃 [R12] [R R12Ⅱ RI2′ R12′ [S [SPp■0xB4] R0, [SP [SP′◆0x2c】 R1, [SP 【sP′p0x2s〗 R20 R2P [SP [SPGp0x24〗 R3′ [SP [SP′F0x20] R0′ R0p SP′ SP〃◆0xA8 lU门r7E1

unk7■】9B894

R1c [SP R1′ [SP,◆0文鬼C]

〗 ◎m7E1日7BD4

°

■七□ekchk-7u旦rd



ˉ荆‘

0=

R2, R2o [SP [BP’P0x24] RODSP0 R0●SP0F0x9C u血7E19F■■∩ 1m7E19C650

≈南=…==→=→=============≡…==□■■≡≈=←→==守■←===■==-=======■…~==→■

】』TH■十dv●·■◎习7E19c650… MhH■仑1ve·0 鸟里兰7E19c650RDD

〗 C…mEF吕 1土m■七土v【

n006P′p0x巫 L= ˉn0々唾尘印工△β

-ˉ---

‖dZ









0

1义bn●七1ve·□◎87E19C6之0 『 =-≡一~宁~~=--=-≡-≡≡= 0=一=一≡=一≡--一~…

~■●谷●●●巳●●●●●●●●●









●『‖‖止尸「》匹■「‖|■‖‖匹■■【‖■『|}~■厂|‖》■「卜巴尸

p

‖@匣



圃〗D∧Ⅵ。wˉpC

图l3ˉ12l

再次发送请求触发断点

仍 p

P

在页面右侧,有-个Genemlregiste!君面板’其中显示了寄存器R0到R10的值,如图l3ˉ122所示。







P

p







0 防

}|





图l3ˉl22Gene【al爬giste『s面板

■■可』■∏|||二■■‖‖』‖|』Ⅲ■■

第l3章Android逆向

676

我们可以点击IDAPro页面上方的‘逐行执行”按钮进行单步调试’如图l3ˉl23所示°

Sm冗tu旧s

■■■可■■∏‖‖』可·Ⅵ

●‘霹



谷■■



L巾『a「y‖咽膊妇"■呻蹿侦幻唾m"■…!n≡唾蜘■u唾…m喀巨xt§m慰lsmh°{

@

□●@魁G●∩●「刨『■刨■t·腮

囤旧∧Ⅵ呻ˉpc

图l3ˉl23点击‘逐行执行”按钮

还可以点击Generalregisters面板中间的“jump’按钮,查看对应寄存器中内容的详情’如阁l3ˉl24 所示· q

@圃

St「uctu『eS

〔∩u们S

迁Ge∩e「a‖『e9jSte「S

同蹦龋……

R0砸70E640

R1

向愧nFC674

R2

R程nPC678

0000000A

Rq

民∏nD6R89

吟坠吗吗吟坠坠●

R3



R5BEAFC768||巴|[巳亡ackl吕EnAFC768

7PC548C0||巴|Pase.apX】07FC548C0 BEhFC658||巴| [巳七ack〗:BE凰pC658

R6 R7 R日

0000o000

R9

nc6凡■正00

R10n∏RFC680 R11AC巴n囱淀O0

R12BEArC66O●

SpBEnFC598吗 LRn77B36DF

|R

7倪】q尸厉’促

可‖‖

n尸

图l3ˉ124查看寄存器中内容的详情

点击“jump”按钮后’IDAPro页面下方的HexView面板会同步显示寄存器中的内容’其中左边 是十六进制的数据’右边是数据对应的明文°在HexView面板中’还可以设置‘伺步查看的寄存器



的值”’例如图l3ˉl25中就设置了要同步查看R0寄存器的值。 画巾沉№wˉ0 叮9PB抑6D9P 叮9PBm6D3P6970 70 2T626·阳皿∏P?】797】706白 化D■■0包c 化℃■0包c

■它『四01净 凸见『四01净 ∩ ∩

0◎2厅 0◎2■O】的6■皿卑D7】 797】70吗0Dr7■62 60“ 606E】A2汀6POd6O2T 0】09$∏靶2P76●日6■

FO3$ 【06rT21P626●血皿j■7●“6■6d6Pγ2】P 【0△■ FO3$

U□0醉』0

丁062

背.翻: I:器潍瓣圈涩腿潍;黑胆器圈膀阑 q

】■d6】

】CⅡD

鳞鳃罐 .】..·口』p..·, .°°.

Q=·邯▲豁O6曲帕06a1靶0O卯1J0口0◎啪60啤△0m 叮■7t“"w】p丛m∏0OO0Q的酌加巫0020帕印

· °°·〖·口·。□···●0o

0000的Q0"m”酝

口□◎◇o□◇D·◎◎·◎ □ ··

唾爷■0’@

0J00000000·000O刁

≈°a■谗G螺

D】O·O00000O20●佃000000“□6酌酗00

咕配■酝 陋℃嘛0汀拿

1c■D工丛70■∏2H丛00郸“碑0】0@”咖 01◎O000O■1O·凹00 】2“OQ0O000D《0正

丛mb00卯003■皿0◎刁0瞄佣006o沁心O·20蚀■O 压了#■T呛0】00·006曲“■1G厅

0000“的卯00m睦

侄…】刁

O〗0●0·0·OO0200”酌000口□O呻mOOm

怎·●■』0 晒■■P0@

0Ⅱ凹·O陆■O“鲍卿OO·000枷O●吨oo晒 1□印0P■■■9》6G■7】 70佣咖0000■■y0碑

“9■γb钞 m勺纪了0p 肛≈咱H77田

00■O7■№田■丁·睡00】0硼00拥0d■△〗 00000O000o00000O 00的0000□O00■O亚 0000O000O0凶@00◎ 0O00□0□pO】的的俩 ■→■■

■■■

●■→●

■■≈●

p●■p●守■

[ Sv∩c‖m∩‖】eW『th



m■们『mm

■●



囤um$







□ ·●●·p■p●凸■◆■■■■

一0●Tm····●·■ ····0 ·Q·□····d·· △··巳□ □□·O··b·■□h pO勺p.·●□····□·Q.

团R·=

.‖`·.惠·.…. ° .°.

剧酗vdb: . ·|′

·●●。‖◆△·· 0 .■■·0◇ ··∩△··■°p0●·□。·。 ° 0···□.p□·° ■Q0^Q

尸O赋…

≈‘

眩J●…蛔9】80仰叫O0“O· 0·00““0Ⅱ□■∏而

■●■●●

■●■■■■■■●■■●■●■■

=尸·

肛0乙岳0鞠O〗“0O0●D0凹7■廷m60m″m“n匹 出 】田几F 〖β6Dm6■6●70●Y72 7〗00鲍““四丁0匹 ■T■岂■0酶吵■7■巫0●■07■■0·】◎"啪■■4■&7 呼■0迢凸吨100F心叫】pw№c·1p瓣凶】】凹的m



…om『′产←←f …□■●■●■■■ U · ■ ■ ■■

m…~吨

[√衡0





∩3

∩4

∩5

m ∏7

一』企…

d··

∩8

□·P





■■~■■

酗 …

■≈

∩0O

图l3ˉl25设置同步查看R0寄存器的值

就这样’我们可以在调试过程中观察到代码的实际执行过程和对应的明文°

举个例子,在静态分析时’我们曾观察到_个常量字符串9「d[∩〔jγ‖4「xQbr1的声明和赋值操作, 在这里下拉找_下’这个赋值操作对应的就是[0R指令,我们在这个位置下一个断点,如图l3ˉl26

0





‖‖

所示。



∩2

·‖‖





■【『【■∏□『‖‖|●「||

■「‖||【■厂||■「|}|‖■「「

l3.8利用lDAPro静态分析和动态调试so丈件 圃}D∧Ⅵew=尸C

口@@

11bn■七1v●●●◎吕7E19c63CSTR 】{№■T勺v●·巳◎S7E19c63E皿D 1§b俐■七±碑吟■◎『7E19C640□皿 γ‖陆闪■七1v●·■◎87E19C644LDR 】』bm十‖vG∩■◎07■19c6d6LDR 】‖h盯□七1v■°■◎$7E19C“0№D 1山ha七血v●Gs◎〗〉E19C6qAmoY —1人bn□七人v●.■◎87E19C6OEB 】0hn■+占v巴.E◎】7E19C650 》‖hn·七土v●·巳◎『7E19C6凸o



R3′ [SP〃◆ .0· 0 n0’ SP’ F!;x\占 unX7宜】9D090

R【0 [SP,P它x三 } R2』 !色P′◆‘′x盎] R00 SPPp0苫弓『

■■

unk7E19B8R0 1…7E19C650







γβ ≈



旬 □

■ 凸

γ

■ ∏

■■●

拈■



γ



■■







·



尸 吁

■凸 啼 上

『■

■压



早匹把

呛职旧马 仙 ↑



■●

■●

R10加

Ⅶ估■●士』v■·■°〗7E19c666■m 】0b■■七儿v●.纽◎87E19C邮6R∏Ⅲ

p



unh7Ⅲ】9■7P0

■19C66 ■19c662皿D



汀●

□也

R0『 SP『 pcⅡ00总

二Ⅷb吼■十i v●.●◎87E19C65cBY二Ⅲ



尸√

unk7E19CUO4 1…7B19C6日凡

◆辩骤罐:;:瓣;:墨猛ˉ7z1’c65^

P

杏·

R0『 SP’ p′』汪PM R1『 BrF ■[)∏7『

llb沉■仑iv●.c◎〖7E19C652nDD Mh吼●c」T●·■◎〗7■19C650且L 】《肘巩■仑『v●.■◎87E19c658B 〗0胎■■十』0e◇日◎吕7■19C6田R . 】‖佑■■十0vO·■◎【7F19C63R

b

鼠 Ⅺ

■‖

吩i龋{龋::滁;::::猛’m…° ∩

677

R0′ SP’凸0xq0 unk7E19B7Ec 1军了H19C66C

-1上hn■七止v●.■◎$7E19C66几B

|ii蹿耀:::瞬;:::: ●‖

γ√

■=















■■



=^

■凸 ′

←吵

胆几

|卜l潞瞪;::::猾;掇龋’z…°

R0p sPp 0p》R踞 R1p SP∏ ■§x0《F

Ⅶh■●七1v●·■◎:7E19c66E四D Y !h冗●十』v簿含囱◎07E19c670■L

p

汾■

p

unk7E】9C904 1哇7E19C676

『_}耀§鳃吕督日踞}:鳃1§



图l3ˉl26在L0R指令处下一个断点



0□

p



然后继续单步执行,可以看到Rl寄存器被赋值了.如图|3ˉl27所示° ·‘ β了] ‘』β丁了旦



p

( p b







ˉ墨量里月`ˉ噶、! ′:..』。 .|睁



DU℃ DD℃

.

∏厅巳□〕了∏∑◎]巳日

; βββ了β 了《β丁]了β :-

P6

β匿《℃‖β〕 口△◎]$

旷酗v挥

.室 :土

.

魁丸睁尸

】丁◎β ]丁巴↓β丁 日

’日◎万∏‘△’『β◎‘∩ 【刘· 【刘甲『懈S咖γm ●厅 @∩

0旧Hp恤m 0旧Hp恤m

← . . .』舅戳龄! !. t咽↓.庐I蠕.乙



恿“Ⅵ峙吨 |

匡≈=

●■

s0们…B…

□@@ 色≈`●m『…●蛔S

iX凶■皿巾.■△?B巳=匹·

b·蹿龋:吕蹬瑟嚣蛊,口↓,…刚°■°户‘W 飞坠…°m■7■【帕■胚… …Ⅷ』■7m

′°勘。…;

ˉ 。 . `°



●o

… ■0

田:h≡□丙≡,画…m了 ˉ ˉ 里°些 ,…忘=滞i岳嚣亩、 ˉˉ ˉ;慧焉辩 已 1…1吨,■B7■】m◆皿■ 》≈曰可∏8铸但≡ ■ 】…【m.m』m』m“B…

7厅酝““℃ m【 〗■正=

刚丁■l●丁…℃

ˉ

■0…7c■Q

丁穴Sqm0b

∩6

[撬鞠蕊…“ ″′….

L串:■

■学■■

■ ■■ ■

■呼■



D…●9■巴 ■· 0…0·回0 □

■■呸…·□≈

■』·■驴· ●□■■

函i元i贞◎崖

1≡1丙占面j冠面76面



【磋℃1wOT0

1…t』m·■0〗U〗加巳丁0■

@’

[橇嚣嚣!疆…”…· P 1…u沁.■0TⅡ…】0画

■血】F1●■Tp■

巳 且…c…昼的0泊】沁6●0匹 巳 】d-0→ 当7汹k●咀● ~婆4啦·…7m戊●咀●

面【元;垂口正 】西..mⅡ死Ⅷ●Q

■m…』■■≈【’■4辑07忘… 吐 』…』■·■wm沁67宜…

〔疆露嚼息…

■q

■■□



■·





圆ˉZ00古甘↑ˉ〃mw 泥…S鲤 圆ˉZ0知"RN瞻诡-『旧mO沁库呵附Sˉ02…m,Ⅶ胆..汇↑仰刀d …0解2塑 电



冤^7沁●m包

■0■ ■■0 β品凹■ ■↓’凹U“■■●

…△=兰

|↓

1≈7■』诬O0O

卜啦

孰血

…3恤00沁α户…

女凸~…b司0■6晒凸090 Ⅷ~坐』尤·■【丁pq■耶90 ■0==4元·■〗7■≈…7m…份0



□■P【

●=■p■ · ■







■……啦≈‖

p

阁l3ˉl27把常墩了:符串赋给 丁Rl寄存器







切换到页面下方的HexVlew面板’就ⅦI以看到对l哑的明文「,如|冬|l3ˉl28所示°



p



【 p



∩ b

b p



■ · Q ◇ . · . · . · °R· □ ·

p · · · □ ◇ · ·B■ · 口 ° · · ·

· ■ ◎ ·R· ·●·· · · □ ·勺 ·

E· . · .·Q. · 0 ° ·A□ · · . · 0 · · · · ·P· ° □ . 0 · ·

· · ° △A. o · ···.◎ ■ □ ·

C□ ·.□ ··· ···o· · · ·

·· · · □ · · ··9fd『 ∩尸《

糕;溯瓣

706F72 287369

vh4FxQbr1.ba■」C ■仁r上ng·a11唾巳七◎r ≤T>82■11唾●t●(■1

7且迅E导:oa0

3c50 3E3B3几6】6C6C

6F63617』

p己D毡莎qe

7A655r74E06■2920 7A65

276E272(

弓皂βBa3札0 7弓ia2SB0

786]65

6D756D2‖ 7几65007‖ 4740094』

757070

ed●·匹x土■四,●u即

63746F

◎r亡●d·S止■●°v●c亡◎

5798595』 6D6E6F7(

626364

omRSmⅦxY匝■…d

3233343』

727374

●£qMjkL■n◎函r■亡

『"1D2bPQ 『性D2鸵Q 〗7阻:皿2G0p 7阻:皿2G0p

656473206D6〗7969 6564 6r72 6r727465642O7369 7■00 7■0041024】 44d5d6 4P臼0 4P臼05】5253545556 6566 65666768696A6B6C 7576 757677 78797R3031 2B2P 2B2P007〕746P69O0

]7]839

uⅦxy■01HM■6789

73746P

+/·■t◎土·■℃◎1.St◎

7L瓣Z钝岛 7n1n2b20 .『姓1B2b]0 7肾1R塑640

756C0073706P6C6C 756C 7374 73746F6600 73746r 2566 2566O02S4C66003八 657∑ 657273696F6E003h

o′E13?gc0 ′E13?gc0



它口 C °Ⅱ°·■

4〕97 B0B0

■△□◎】◎◎】

7EiB〗540 ≈翌上B?bg0

已□』已■』】β◎日房〕◎ 巳β了○●β丁】◎丁【β乙 《◎◎‘肛几·□◎矿●◎▲ 了∏∑γ书日丁习◎β丁∑γ 】了口◎’’■‘罗·〕刀邑 β∑β·▲已β〕β了了βγ 习囚日日□日■〕▲〗○■狞 巳β丁β△已β习了丁∩ββ ∏丁巳几丁丁□■]◎《◎◎ β白βγ《固β]γ□β】自

p

4597 B0B0 0000

它◎’’日‘℃』·亡狞八册 β∏固‘《昌β习·巳β〕〕 但’日〕吕吕∏◎’℃△□。 日■丁了《曰旧〕ββ丁◎° 】■〗∩‖‖■几牙■]旧且 巳ββ】《巳β了旧β了β日 几□∏△】日’’《《◎℃了 】∑ββ《□巳丁γ丁◎△日 凡《◎日∏已日日〕习巳日’ 己了∑巳▲固βγ了γβ∑β ■∏习△』】丁了○口肝·】 〕巴丁丁§日β丁□∩β◎γ 《吕勺乙◎·ββ『◎《β∑ 巳ββ丁◎臼βγ∑β丁‘γ

p

B0B0

O0o◎

泄■凹2函i0 ≈P】弓2b20 『H且四冗凸]q

』β□】◎◎] $日◎日日◎日

p

7E2B2书F0 7H1§`2500

◎◎◎∩居◎◎ 了◎◎丁巳·】 ’日β’八◎巳 儿·◎】◎◎刁 ‖∏◎《日○◎ 日】▲◎』咕β ◎β·◎日·◎ ◎】∏◎】日◎ ◎□日◎◎□◎ ○了○□丁◎◎ □’■◎’■β ·】··β∩β ◎↓巳β△巳β

b

皿“”胆卯”胆

眶已3A乙卜.(n 《397

◎◎】□○】日 $◎日巳◎日日 ◎◎』◎◎】◎ 』·○巳◎◎』

』霉‖exⅦewˉ‖

□·】□◎』◎ 日◎《日◎↓日 』△◎上牡◎] 日日∩日日日日 』□□』Ⅱ◎】 ◎β◎◎□◎□ 了◎◎了□·丁 ’巳◎’巳◎’ 〕◎·已◎◎〕 《日◎β已◎△

l

7已1B1勤D0 7m‖2■EQ

●〗.

▲≡

圆…£叼尉…酗4Ⅷv…ˉ……吨镐m=涸…

…7■Ⅱ蟹坐巳

崔】占=■●…·≈『7n矿▲≈●



埋乙·

… ■

■10 ‖■口念Ⅷβ R 唤q■Pp仇■■巳

TM…三I元5乙i7蘸天$66酝 ? l‘■=【语 ≡j记I歹占迅画

p

. .备..,江. ,ˉ .

@



b

愈兵q′孕



山■y∩m…饵毗炉“肘及mm■如m"回m

?

p



,=譬赌沁钨鹤县ˉ树凸;:;戈、

73746F6( 0073706】 6q0O737』 2O6E6F2( 206F75γ』

0C4D4E

■·■七令D》· ,n‖ .●xce r·n呸∏正reErJ页T.■N

6C6C00

u1·■亡◎11°■↑∩qu11.

6c640O 6F6E76 6F6620

■t◎f‘■七函·■七°1d. 0f.0乙f.日·n◎0c◎nv ●r●1◎n·2 ·◎u亡◇◎f·

UNR"oWN7曰B255默‖mat№·sα聪徊h闽γ …h 呐脚】》ch盯∩『Ze创WjthR!)

阁l]ˉl28

HexVjeⅥ[盯板中的内容

第l3章Androjd逆向

678

这时大家可能会有疑问,这些汇编代码对应的C/C++代码是什么呢?在某些情况下’动态调试的 过程中也可以将汇编代码还原成C/C++代码’这样整个调试过程会变得更加直观°但在其他情况下,



从汇编代码到C/C++代码的转换并不可用,这时我们可以借助静态分析的结果°图l3ˉl29中展示的 是动态调试过程中得到的汇编代码° ● β β





■■

』’













『上



口· ℃ 止 儿

圃‖D∧ⅥeWˉpC

吉嚼

…ˉˉˉ` .一…_ _….~… —_———ˉ一一ˉ_一___-…一≡

_=一=■←≡7_=_—一— —己 ≈≡■占~■_~■=- ≡_一



g

!;{霉霉霉霹羔盂盂ˉ蕊霉_ˉ了獭;蕊忌词

一1」m■七1v●°■◎F7匣19C674B 1◎◎7■19C676 { 1止n■七儿v■.■◎:7B19C676 『--一一-一==~因…≡…≡≡~→≡一==←ˉ=一_~←一ˉ←一≡←一→≡~→

■ w

Ⅷ胎■■七1v●·■◎齿7E19C678m辽 】凸胀■吐止v■.■◎【7E19c67cLDR

]i肘∩■仑」v●导■◎87E19c67E月DD ]lh冗■十4v●°■◎87E19C680■T售Ⅲ

里um■七止ve.s°‘7E19c664B



un凡7匡】9B7P8 R1’ [sP′仙x20]

·‖





』·瞬…,1………{

↓彰 !蹬蹦蹦!龋…‘ ,,′……



R0‘鲤‘◆0x84 unk拒】cp■■尸

0

1°c=7■】9c“6

髓撬霉藤盂;

;_了葱盂=恿|

髓蕊霹壹孟;;;_墨囊三__而葱盂;=司





● jd0遍■企1v■.■◎87E19C69cSm

R0p [白P0抑x14】

1………1…』 ˉ__—_=___-__一~弓=字……={ 广』』………· 』…… 』 |0川腻NoW川7曰9c酮僵;‖ma甘隧鼻Q;,』鳃…0囱T毖…~=ˉ、→、毯(剑【》@t】fmi配“陋th购|





图l3ˉl29动态调试过程中得到的汇编代码

图l3ˉl30中展示的是静态分析过程中的汇编代码区块° ●愚…●z=| …

邑辨窒j

■■{□Ⅵ』■■

●恿=一=G



』●|■可



屈—

…0=■…ˉ_=■=0……■一■≡0_←_■□二□ 』

寸筐

| ‖伊同

→臂= 富零= 愚驴霉斗 凸〖 =

寸冒



~B…●建≈≡·…0=·←·_=…■型—·≡… …

匙←厨叼

曰}

0…0=…■电…≈

/__/ _≡| 图l3ˉl30静态分析过程中的汇编代码区块

|■■勺回■间』·』·』■■■]|



■ ■ 】 | ] |

‖凸PT■

■ □ } 』 】 ■ ‖ 』 ■ 日 』 ■ · ■ 勺 ■ 己 ■ ]

→ ■■

| | 」 ■ Ⅵ■■■‖‖|‖】■∏|‖|

d=0…-◎≈0■户』』禹…-夕…·≈·■_0●≡0

|』■‖可司■■■■●■‖■■■■■■■

||‖ l38利用IDAPro静态分析和动态调试so丈件

679

可以看到’两个汇编代码是—-对应的’由于我们能在静态分析过程中找到对应的C/Oˉ+代码的 位置’因此动态调试过程中的位置也就可以找到了°经过-些调试分析,我们就能知道变量在整个 C/Oˉ卜代码执行过程中的大致变化情况了,它的值肯定存在于-个或者多个寄存器中’我们通过Hex View面板就可以查看和验证° 6算法还原

现在我们已经可以还原出基本的算法流程。

(l)声明—个空列表,然后将传人的/ap1/Ⅷoγje字符串(对应a3参数)` 9千dL∩〔iγ∩4「xQbri字符 串、O仟5et变量(对应a4参数)和时间戳信息放人列表’再使用逗号把列表中的这些内容拼接起来° (2)对拼接得到的字符串使用shal算法加密°

(3)再声明_个空列表’然后将上述加密结果和时间戳信息放人列表’同样使用逗号把列表中的 这些内容拼接起来。

(4)对拼接得到的字符串进行Base64编码’最后返回即可。

以上便是token参数的加密逻辑’我们可以试着用Python代码实现-下: mPorth日5∩1jb mpOIttj爬 i呻or↑ba5e6』

‖|卜‖卜‖●‖↓‖■‖‖『厂‖|卜■「皿■■「|『广}■β■

de+get-tO代e∩(γa1ue’ O仟5et): arr日y= [] aⅢray.apPe∩d(va1ue) arIay·appe∩d(‖9+d[∩〔jγM「xQbIj0) array.appe∩d(5tI(o仟5et)) t加e5ta|∏p=5tr(j∩t(ti们e.tj!∏e())) armγ。日ppe∩d(t1爬5taⅦp)

5jg∩≡h己5∩1jb.sh31(‖’ ‖ .joj∩(army)·e∩〔ode(!l』t十ˉ8!)).bexdige5t() retur∩ba5e64.b64e∩code(‖’0 .joj∩([5j8∩’ tme5ta‖p]).e∩〔ode(!ut千ˉ80)).de〔ode(‖l」t+ˉ8!)

这里我们用一个8etˉto促e∩方法实现了上述的加密逻辑°最后添加对该方法的调用即可: I‖0[X0R[= 0∩ttps://app8.5〔mpe.〔e∩ter/apj/Ⅷγje?1mit={1jmt}8o仟5et={o仟set}8toke∏≡{to代e∩}, ∩∧Xp几[≡1O u付I丁=1O

千orji∩m∩ge(肌Xp∧C[): O什5et=i*lI"I丁

to代e∏≡get-to代e∩(『/api/mγie0’ o仟set) i∩dexur1≡I佃[X0R[.fomat(11Ⅶit≡LI‖I『’ o仟5et≡o仟5et’to仅e∩≡toke∏) Ⅲe5po∩5e≡reql』e5t5.get(i∩dexur1)

|〖■■厂‖|||°■■■『||||‖=尸‖‖‖||‖==■『‖‖匹尸‖▲■尸但■■

pⅢj∏t(0re5po∏se ’ re5po∩5e。j5o∩())

运行结果如下: re5仰∩se{,cou∩t|目 1m’ ,Ⅲe5‖1t5! : [{0id! 日 1’ ,∏a爬! 吕 0蓟王别烃‖’|a1i35,: ,「aⅢ…11‖y〔o∏〔ubi∩e!’!〔oveI0 : ,http5://p0.眠itua∏。∩et/mγje/〔eqd己3e03e655b5b88ed31b5cd7896〔十62472.jpgα“w6判4∩1e1c|’ °〔ategorie5! : [! 剧份|’ 0金价,]’ ‖pub1i5hedat, : ,1993_o7ˉ26,’ !m∩‖te": 171’ ‖s〔ore0: 9.5’ ,regio∩5! : [‘中国大陆!’|中囚谷 港|]’ ,dra们己, : ●





{,id! : 1o’ °∩a∏论! : ‖狮于王!’ ,a1ja5! : ,丁heLjo"陋∩g『’ ,〔oγer’: 」∩ttp5://凹。『‖eitua∩.∩et/mvie/

27b76千e6〔千39o3十3d7496〕于7o786凹1e14384o6·jpgα64问6“∩1e1〔』’ 0〔ategorje5! : [!动昌′’ ′砍斤′’ 0∏险0]’ 0pub1j5hedat! : |199SˉO7ˉ1S,’ !m∏ute0 : 89’ |s〔ore,: 9。0’ !regio∩50 : [,矣国|]’ ,dm爬『 : !辛巳是荣钮■的0』、 王于,他的艾亲木法沙是一个威尸的国王。然品叔叔刀疤却对木法沙的王位况饥已久°共想坐上王位宝皮,

■冈‖}皿『◆【‖‖‖■●。|||}□【队】‖‖■■【■‖|‖伊‖卜||}■■■『

我们成功爬取到了数据!

7.总结

本节我们介绍了使用IDAPro工具对so文件进行逆向分析的过程’直接还原出了so文件中的算

』』 |

第l3章Android逆向

法并实现了数据爬取’整个难度其实不小°当然’本节主要介绍的是利用IDAPro逆向分析AndroidApp 的基本流程’其功能远不止这个’更多强大的功能等待着你的探索。 本节代码见https://githuhcom/Python3WebSpjder/ScrapeApp8。

|3.9基于P「|daˉ只尸C模拟执行so文件

‖‖』■■‖‖□尸|·■■■」■■■‖|‖|‖』勺■■」■■■

680

在l3.8节中,我们使用IDAPro对so文件进行了逆向处理’还原了其中的_些逻辑’把汇编代 码转化为了可读性更好的C/Gˉ+代码’再加以适当的动态调试’便找出了so文件中隐含的加密算法。 但so文件本身也可以设置一定的保护措施。我们已经在l1.l节了解了JavaScript的混淆机制’混

淆之后的JavaScnpt代码可读性变得非常差,会给我们分析带来很大的难度。同理’如果在so文件中 添加了—些混淆机制’那么so文件内部的代码逻辑也会进一步变得不可读,即使把其内容转化为 C/C++代码,也难以阅读和分析’这就是Natlve层的混淆。



在Native层实现混淆’常用的技术是OLLVM,即针对LLVM的代码混淆工具。



|.O[LγM的简介

·

一个项目’该项目旨在提供一套开源的针对LLVM的代码混淆工具’以增加对逆向工程的难度°项目



OLLVM是Obh』scatorˉLLVM的简称’是瑞士西北应用科技大学安全实验室于20l0年6月发起的

地址是h仗ps:〃githuhcom/obfUscatoPllvm/obh』scator’ 目前的最新版本是40°

‖」‖‖叮

如图l3ˉl3l所示’整个Lu/M架构从广义上分为三部分_前端`优化器`后端°前端会用到

-个叫作Clang的套件’Clang是LLVM项目的一个子项目,负责完成—些代码的词法分析、语法分

·■‖·

到这里大家可能还是_头雾水,OLLVM看起来是在LLVM上增加了一些混淆机制的结果’那 LLVM又是什么?LLVM就是_个编译器架构,是模块化、可重用的编译器和工具链技术的集合’功 能是把源代码(如C/C++代码)转化成目标机器能执行的代码°

析和语义分析`生成中间代码。之后的代码优化和生成目标程序可以归类为LLVM后端’可以将前端 生成的中间代码转化为机器码。 q

[山M架构

(((

广■~→亡=户■≈■~=≈→■←≈■~■■■≈←←=≈=~■=→≈=■←■=→←←===■===←■←==■■=…←≡

前端

0==■■■二 Pd■■←→■

优化器! ‖

后端

>叫卿辟

」勺‖|‖』‖

-●C|己∩g

■ ■ 司 | 』 · ■

LLVM架构的示意图

我们深人了解_下这个架构中的中间代码生成的过程,这个过程中会用到_些LLVMPass模块, 内部架构如图l3ˉl32所示。

■日』司』■』■■■|||』·〗|□●一■可』口■■‖

图l3ˉl3l



|■「{■■「|■伊「|‖||■■■「|■■尸||巴■■尸



■尸巴=■「||◆甘尸



l3.9基于FridaˉRPC模拟执行so丈件

68l

』■■■■■■■■

C/C+十



前端

pa蹈



p●

后端 -



—止一

陵目标程序



图13ˉl32生成目标程序的示意图

OLLVM的核心原理就是修改Pass模块’对中间代码进行混淆’这样后端依据中间代码生成的 目标程序也会相应被混淆。因此, LIVM和OLLVM最大的区别就是Pass模块不同°

OLLVM支持LLVM支持的所有前端语言(C、C{ˉ+、OhjectiveˉC、Fortran等)和所有目标平台 仿|△■■尸|△庐仔‖

(x86`x86ˉ64、PowerPC、PowerPCˉ64,、ARM、Thumb、MIPS等),具有三大功能,分别是Instmct1ons

Substitution(指令替换)`BogusControlFlow(混淆控制流)和ControlFlowFla仗ening(控制流平展), 具体可以参考https://github.com/obfUscatoFllvm/obfUscator/wiki/Features中的介绍°通过这些混淆功能’

■『『户止凸「

原本的目标程序会被混淆得更加复杂,使我们难以分析,从而增加了逆向难度°

2.案例介绍

‖卜卜)■亿

本节我们介绍—个在Native层进行OLLVM混淆的案例’示例App安装包的下载地址为

https://app9scrape.center°如果用IDAPro对这个App中的so文件进行逆向’就可以看到它的混淆 效果。

■ ■ 「 [ 【

App9是在App8的基础上增加OLLVM混淆得到的,所以我们可以对比一下App8和App9的不 同°在IDAPro中,打开e∩cIypt方法的GraphOvervjew面板’可以大致看出这个方法内部的调用逻

伊 | |

辑层级。

[ ■ ■ 止 巳

在混淆之前’so文件(即App8中的so文件)的GraphOverview面板是图l3ˉl33展示的这样。

囚 ■ 「 ‖

□@◎

仁u∩ct;◎∩SWi∩dOw

| 卜 「

尸u∩刨◎肋冗佃∏●

▲…●∩t

αm

} 卜

}}|

卜|

团巳a··6q::日ase“(v°‖d) 圃酗·e6冯:;mc。de(v创dcmst·′u‖∩tcms↑&) 团跑se“::o∩c。d°(γαdcαm.′uⅡ∩tcmst&) 田Base64::°p●「a℃「()(蛔::-∩酞↑:尤a·亿ˉ£打m锤m吗… 团由se6q::卯刽圃℃「0(蛔::当碰?::bo刨色sU协9→m吗赣… 困Base6q::『国露t(vo0口) 团由■“:蓟…啊d)

。t°对 .叭 .toxt .p‖t .t·xt 。p{t 蹿t

0o0"O0



"00O00

O0O0O0o咖0『

m0oooo咖陕 "0ooo0o0o0↑

0000o000"0c o""m0o00↑ oo000o0o00α



o0000000“ot

ˉ





图l3ˉl33 App8中so文件的GraphOvervjew面板

第l3章Android逆向

可以看到,整个调用逻辑还是相对清晰的—层层嵌套’没有复杂的依赖关系。在混淆之后, SO

文件(即App9中的so文件)的GraphOvervjew面板是图l3ˉ134展示的这样° ≈●_…豁

口镰翻

毛·=邑卧牲电?…审〃气唾羚宽:

OOOo0o0000o0仟巨c

0Oo00〔

O“o0O0OOooOo6^C

ooOOO〔



0凹O00ooo000"0q

o0oOo(

露p…"oO0"E3弘

00o刨;

00o00000OoooD720

00o00[

【Ⅲ〔《 耐馅创

团S"A↑::S什∧‖〔v°ld) 团S卜lm::S卜‖A↑《γ咽) 团S川m:;a四《v创dc°陋t·『凶∩t)

O仰0oo00O0o↑o020

oo000邑

O0ooOO000o0Oo75O

0OO00〔

0凹oO"0ooO0o38O

0000O[

O"oO00o000‖07尸c

OooO0《

o"0oOOOoOo0O92q

卯000《

]|

Lmg巾

田BaS·64;:…o6冬《v创o》 .↑mt 田Saso“::·∏c°d·(v刨dcmzt°′Ⅲ『γtcα`st&》 .吨 团Ba$·60::mc°d·《v°jdcm酞窜』untcmst&) 。t·Nt 团日■s·田:mα瑟m『0《口测:~∩m:力m儿ˉmhg<ch·『!.… .plt 田Sa··田:np·馆m『0《s阅;~∩酞‖;;h=沁ˉ自耐∩g<↑赋b…蛔xt

■■

S…

▲…

……

|‖」■】‖‖‖』_■〗‖‖】〗·』‖|■··】|{』■曰|』■·|」|」■■』‖』●□‖||‖■■‖|

682

0

厂} ‖





||

可以看到’这里e∩crypt方法的调用逻辑就复杂了很多’经过一些混淆之后’方法内部的调用关

‖□]|』口

图l3ˉl34App9中so文件的GraphOverview面板

|{



‖({



d

系和依赖关系变得愈发复杂’此时我们想深人分析方法内部的逻辑,已经变得十分困难。

对于刚提出的问题’如果再像l3.8节那样逆向so文件和通过动态调试进行分析,那就是难上加

·

·

键加密逻辑°



□直接硬刚:和l3.8节的内容类似,通过各种辅助调试工具和追踪工具找出so文件中隐含的关



答案当然是有’解决思路通常有两种°



难。有没有什么方法可以使这个流程更加简单’或者说有没有可以绕过这个流程的方法呢?

当■司口|』■引

3.解决思路





到执行结果·即纯黑盒调用°



□模拟执行:不关心sO文件的内部逻辑,通过某种方式直接调用SO文件,传人对应的参数,得



本节示例App中的so文件没有设置任何风控检测’所以我们可以采取“模拟执行,,的方式调用so

二■‖|||川司‖

要我们深人SO文件中找出核心问题并解决°



是万能的,因为某些App的so文件中包含一些风控检测,例如检测外部执行环境是不是正常等,如 果检测有异常,就可能拒绝返回数据或者返回假数据等°所以有时候’这种思路不一定有效,这时需



这两种思路各有优劣,如果我们采用第二种,那么确实可以免去—些复杂的分析过程°但这并不

文件’得到对应的执行结果°

其实模拟执行so文件的方法有很多,有F∏daˉRPC`AndServerˉRPC、unidbg等°本节我们先介



)□、··|

绍利用F∏daˉRPC模拟执行so文件的过程。



}卜

‖|} ‖ | ‖ ‖

仁■■日『}

| l39基于FridaˉRPC模拟执行so丈件

683

4准备工作

请确保已经正确配置好了Frida的环境’并能成功在电脑上用Frida连接到手机,具体有如下几个 要求。

□在电脑上安装好fTidaˉtools并可以成功导人使用°

□在手机上下载并运行frjdaˉserver文件’即在手机上启动一个F∏da服务,以便电脑上的Frida客 户端可以与之连接°

□让电脑和手机连在同_局域网下’并且能在电脑上用adb命令成功连接到手机。 卜‖{■』■

具体的配置可以参考l3.5节的内容°

■■

另外我们需要在手机上安装App9’并确保数据可以正常加载,运行效果和之前各个示例App的 效果是_样的。 匡■厂‖‖止■β「‖‖■|■■『

尸坠∩■■|β■‖■广卜△■‖ ■尸|巴■‖‖|β‖‖尸‖‖ 伊

5.实战

β

首先我们可以使用jadxˉguj和IDAPro对整个App9进行反编译和反汇编分析’分析过程可以参 考l3.8节的内容’这里不再展开°分析之后,可以得到如下信息。

□关键的token参数的加密逻辑是在Native层实现的’即隐含在so文件中°

□调用so文件的过程是通过调用‖atjγe0tj1S类的e∩Crypt方法实现的’也就是说是在Java层 实现的。

□e∩〔rypt方法接收两个参数’第—个参数是一个字符串, 目前是固定的/apj/Ⅷovje’第二个参 数是数据偏移量°

基于这些信息,我们可以使用F∏daˉRPC实现对e∩〔rypt方法的调用,首先新建一个Ipcjs文件’ 其内容如下: rp〔·expOrt5= { e∩〔rγpt(5tr1∩g’ O仟5et){ 1ettO惯e∩=∩u11j

β|仙

〕ava.per于Or∩↑(十u∩〔tiO∩(){

v日rutj1≡〕aγa.u5e(圃〔oⅦ。go1d∑e.|wⅧ∩己b1t.uti15.‖at1γe0tj15").$∩eN()i to低e∩≡Utj1.e∩〔rypt(5tr1∩g’ o仟5et)j })j retur∩to促e∩j

b ■『伊■■庐皿■■『【【■「■尸|■■》「‖但■厂…『「|■尸

} };

这里在最外层使用Ip〔.export5导出了一个e∩crypt方法的定义’e∩〔rypt方法接收两个参数,一

个是5tri∩g,另一个是O仟5et’方法内部的实现逻辑我们也了解过了°这里还是使用了〕aγa对象的 per+orⅦ方法,调用u5e方法初始化了‖atjγe0t115类’并赋值为ut11变量’接着调用这个变量的 e∩〔rypt方法得到执行结果并赋值为to恨e∩变量,最后返回这个变量°所以’最后e∩crγpt方法的返回 结果就是川at1γe0tj15类中e∩〔rypt方法的执行结果。

现在我们已经在F∏da脚本中声明了对应的RPC方法,那怎么调用它呢?很简单,使用一个 Python脚本调用即可,脚本内容如下: mport+r1da 1"portreque5t5

8A5[UR[= ‖http5://app9·5cIape。〔e∩teI0 〖『β】『【■■【尸巳尸|匹■「|

I‖D【X0∩[ ≡8A5[0R[+ ‖/日p1/帅γie?1mit={1imt}8o仟5et≡{o仟set}8to代e∩={to促e∩}0 ‖∧NpM[ =10 [I‖I丁=1O

5e551o∩=「r1da.get-u5b—deγj〔e()·attach(』〔o‖·8o1dze.Ⅶγγ帅abjt』)

但■『·「Ⅱ■『‖广|

■「 「



」■■刁‖‖|』=■|■】■

第l3章Androjd逆向

684

」 ‖ ‖ ‖

5ouI〔e=ope∩(0rp〔°j50 ’ e∩〔odi∩g=!ut十ˉ8‖).Iead() S〔ript=5e55iO∩.〔re己te5〔IiPt(5OUIfCe) $cript.1oad()

de十getˉto代e∩(5trj∩g’ O仟5et): retur∩5〔rjpt.export5·e∩crypt(5tr1∩g’ o仟5et) 「or1j∩m∩ge(灿Xp∧C[): O仟5et=j* [I‖I丁

toke∩=get-to|〈e∏("/api/∏oγje′0』 o仟5et) 1∩dexur1=I‖D[贝0RL.+or∏Et(11∏Ut=LI‖I丁’ o仟5et=o仟5et」 to促e∩=to代e∩) re5po∩5e二req0e5t5.get(i∩de×ur1) prj∩t(』resPO∩5e ’ re5Po∩5e.j5o∩〈))

□8A5[0R[:请求电影数据的API的前缀°

□I‖0[x0R[:请求电影数据列表的API的完整URL,这里预留了几个占位符’1imt是每次请 求要获取的数据量’o仟5et是数据偏移量’toRe∩是加密参数token。

二□‖■□·■■■■■

这里和之前一样’首先声明了几个常量。



接着新建了一个5e551O∩对象’试里依然是使用attaC∩方法将其关联到了当前执行的包名上’然

勺|·‖叫

□‖∧Xp∧C[:最大的页码数。 □[I‖∏:就是I‖0[X0R[中的11mt参数’是_个常量°

d



后读取了并加载刚才定义的JavaScnpt脚本,将脚本赋值为5〔rjpt变量。

‖』‖‖

随后定义了-个getˉto代e∩方法,它接收两个参数,这两个参数和刚才e∩〔rypt方法的参数-对应’方法中有-个关键的调用声明,是5〔r1pt.export5,其返回结果和刚才JavaSc∏pt脚本中的 rpC.eXpOrt5是对应的’由于我们在rpC.expoIt5中声明了e∩〔rγpt方法,所以在5〔r1pt.expOrt5里就 能调用e∩〔rypt方法’传人对应的参数后,就能得到JavaScnpt脚本中e∩〔rypt方法的返回结果° 之后遍历了所有的电影数据列表页,构造好o仟5et’得到对应的to促e∏值’最后用1mjt`o仟5et`

to代e∩拼接成完整的APIURL,并调用requests库的方法请求这个URL。 运行结果如下:

港0 ]’ ‖dra"a : ●



·司{‖‖引■||』‖|‖』■■

re5po∩se{‖〔ou∩t‖ : 1OO’ 0re5u1ts‖ : [{‖1d‖ : 1’ 』∩日Ⅶe0 : 0霸王月||姬0 ’ ‖日1ja5‖ : ‖「arewe11料y〔o∩〔ubi∩e‖’ 』coγeI : ‖‖ttP5://pO·"e1tua∩.∩et/Ⅷoγje/〔e4da3e03e655b5b88ed31b5cd7896c+62472.jpgβ464w644h1e1c‖’ 0categorje50 : [ 0 剧怕‖’ |爱悄』]′ 0pub115hedat|: 01993ˉO7ˉ260 ′ `|『|1∩ute| ; 171’ |5〔ore| : 9.5’ !re8io∩50 : [|中国大陆』」 』中圆奋 巾

王于,他的父亲木法沙是一个威尸的国王。然品叔叔刀疤却对木法沙的王位砚饥已久°妥想坐上王位宝皮,

‖‖|

{‖1d‖ : 10’ {∩a"e0 : ‖狮于王‖’ ‖a1ia50 : 0『beUo∩恨i∩g‖’ ‖〔oγer‖ ; !∩ttp5://pO.们eitu己∩.∩et/们oγje/ 27b76十e6c「39o3「3d7』963+70786oo1e1』384o6。jpg0』64w644b1e1〔0 ′ 』〔ategor1e50 : [ 0动画′’ 0砍毋|’W险‖]’ |Pub1ishedat』: |1995ˉ07ˉ15` ’ ′m∩ute|: 89’ |5core』: 9。O’ !regio∩5|: [ ,矣国|]’ 』dra|∏a| ; |辛已是荣沮囚的′」、

可以看到,我们成功模拟执行了so文件,直接得到了加密参数token的值’然后构造请求实现了 数据的爬取。 6.总结



本节我们介绍了利用FridaˉRPC技术模拟执行so文件的过程,在这个过程中我们不需要关心so文

件内部的混淆机制’对SO文件纯黑盒调用即可得到关键信息°



本节的~些对OLIVM和LLVM的概念介绍,部分参考自下面两个内容。





□看雪论坛上的“ollvm快速学习,文章。

本节代码见https://githuhcom/Python3WebSpider/ScrapeApp9°

』□■】■|‖』■]』■‖■】■可」■|」司■■〗■■■

□CSDN网站上的“OLLVM环境搭建、源码分析及使用’文章。

l3.l0基于AndServerˉRPC模拟执行so丈件

685

↑3.↑0基干∧∩dSeⅣe「ˉ只尸C模拟执行so文件 本节介绍利用AndServePRPC模拟执行so文件的过程° ‖.∧∩dSe『ve『的简介

平时我们编写服务器脚本’代码都是运行在电脑上的。例如写_个简单的Flask服务器脚本’就 需要在电脑上运行该脚本来启动对应的服务°那服务器能不能直接运行在手机上呢?答案是肯定的。 AndServer是可以运行Android手机上的—个HTTP服务器,其实就是—个Android的第三方包,

我们可以开发_个AndroidApp后将其引人;再将其提供的服务器功能设置为随之启用,并指定运行 的端口’这样在App启动的时候就可以在Android手机上启动一个HTTP服务了。

O

|「 ●

||、 D

AndServer包是基于Java编写的’在Java生态中有一个非常流行的服务器框架叫作SpnngMVC’ 不过它是运行在电脑端的°AndServer借鉴了Sp∏ngMVC的—些设计思路’具有和其相似的功能,例 如利用注解(Amotations)来定义-些路由规则和处理规则’使用起来非常方便。

那AndServer和我们本节要讲的内容有什么关系呢?接下来我们详细看-下°

0

p













2基本思路

由于sO文件有其特定的指令架构,因此我们不能直接在电脑上调用和执行它,而SO文件又隐含 了我们想要的token结果,那么在Android端模拟执行so文件后’怎么能把结果方便地暴露出来呢? 在l39节’我们通过Fnda成功在电脑上拿到了so文件的执行结果’那这里的AndServer,其实就是 换了一个暴露结果的思路,即通过HTTP接口把结果暴露出来。

通过以上介绍我们可以发现’AndServer相当于在手机上启动了一个HTTP服务器,这个服务器

内部可以直接调用App中的方法得到结果并返回。参数怎么传递呢?很简单,通过HTTP请求的参数 传递即可’例如我们已经了解到e∩〔rypt方法接收5tri∩g和o仟5et参数,那我们就可以把这两个参



数映射为HTTP中URL的参数或者请求体°执行结果怎么返回呢?很自然地’通过响应结果返回就

}p

HTTP服务后,我们就可以通过requests等库传人对应的参数来获取token结果了’而这个token本质 上是App调用so文件产生的,算是一个模拟执行的过程我们可以将这整个过程称为AndServerˉRPC°







好了。

我们现在相当于借助AndServer把AndroidApp中的方法包装了一下’提供了HTTP服务。有了

3.准备工作

烂 7

本节会开发—个AndroidApp’所以请确保正确配置好了Android 开发环境,具体的配置方式可以参考https:〃setupscrapecente∏android。

4∧pp的初始化

我们首先创建_个AndroidApp。打开AndroidSmdio’新建一个 空白项目,包名可以随意取’这里我把App名称取为AndServerT它st’ 包名取为comgermeyandservenest’如图13ˉ136所示。



●■■

■『||■‖||巴尸|田尸■∏【伊【尸〖『{[■■■|但尸「■口|【■【■「‖|}尸‖卜■厂■『

然后准备一台Android手机’模拟器和真机均可,将其和电脑连 接’并确保能在电脑上使用adb命令访问到该Android手机。

》哆源代码 V鹤资源文件 肋

p

将反编译后的项目导出’保存其中的lib目录’以备下面使用°

呜5c旧pe一app9°己p促 巴



本节的示例App和l3.9节的一样°我们使用jadxˉgui反编译其 apk文件’之后可以看到对应的so文件,如图l3ˉl35所示°

v■己Ⅷ6牛γ8a



圈ub∏atiγe°5o √圃a『Ⅶeab1=γ7a

困ub∩at1γe.5O √凶×86

■Ub"己tjγe°So √酌X8α矾 ■1止mt1γe°5O —

》互巨∏E]瓦=IN卜

〉田◎肋ttp3 〉酶「e5

图∧∩d厂o1硼a∩1「e5t°x"l ■C1a5SeS°‘ex 》■厂eSou厂Ce5。a「5C

尸∧p低51gⅣ已tu厂e

图l3ˉl35 App9反编译结果

0

|||■可】‖

ˉ■

』 ‖{|卜|」‖刁]||□□】‖‖□■■〗‖』■■|』』■旧|‖』‖司||·‖■]|‖司■■‖|‖|□‖‖』|」日|」‖|■

第l3章Android逆向

686





◎ Co∩f|gu『eγou「P『Qject ÷

图l3ˉl36创建_个AndroidApp

文件放到本项目中了,这里我把整个lib目录放置在app目录 下,代码结构如图l3ˉl37所示。



.

√腮 〉



明,代码如下:





己∩drO1d{ γ

5our〔eSet5 { 阳己i∩{ j∩i[1b5·5rC0jr5= [ ‖11b0 ] } }

,



√|二·‖|{‖〗‖||‖二□]|』〗引||」叫 「

的部分添加对这个llb目录的引用,定义-个5our〔e5et5的声



二■

另外我们需要修改-下bulld.gradle文件’在a∩droid声明

|」■γ』{‖』■∏{纠□Ⅵ·■■

做-些项目的初始化工作。接着就需要将刚才准备好的SO

削○‖|‖|」■‖‖‖』ˉ‖‖·|」`|‖■‖』|

「0滩5伪

v

■ v

} ■

文件了。

图l3ˉl37所创建App的代码结构

SO文件已经准备就绪,那应该怎么调用它呢?具体的调用茎 它呢?具体的调用参数和方法名又怎么写呢?_个比较好

的方法是从原来的APp里找出对应的调用逻辑,然后把调用相i :辑,然后把调用相关的定义复制到当前的APp项目中。 使用jadxˉgui反编译原来的apk文件,我们可以从结果中按 我们可以从结果中搜索关键字轻松找到对llbnativ已so文件 的调用声明’如图l3ˉ138所示。

| 』 ■ ‖ □司|■■‖|||■·司』■』■■■■∏|

径,这样App在加载Natlve库的时候就知道应该从哪里寻找so



■■Ⅵ

这里我们通过j∩1[1b5.5rc01r5指定了so文件的保存路

|」■】■■

」●占■〗司

』司■■』】■

l3.l0基于AndServePRPC模拟执行so丈件



687

} 邀 6c…….… 凹咏码 ≥■酗叼蛔 瞥攀硒吨必血 澎■c■

v



p

:■■「□u凶t函.m锤厂蝉M边1四乌 意印…吨m.血唾』』旧1啊·1汕7■患

》田αmt●口‖·gu由

;

ˉ′■啪Mze叮m晒哟止t 狱垒…●丁





罗$… ’■凹t■

2出匈t吨蛔1呵 羔它颤1ty



`瘴…1 γ■ut1`■



。@Gm了Ⅶt 瞥e粉试p■仙t』1田



≡@胆寸1…』tⅡ1已

警《…》沁』d

矽|喧『`唁γy喊!sf厂j哩!蓟 》o晦厂w1t〔M启∩t

〗o酗11“Q∩?10 》●〔唾翻 》o〔磐

》·…………` 》@助t出mq』叫I∩↑o |

0

图l3ˉl38源码中调用so文件的地方

P





在图l3ˉl38中’有一个‖atiγe0t115类’这个类在com.goldze.mvvmhabitutils包中。‖at1ve0t115

类里定义了一个e∩〔rypt方法’接收参数5tr和1。这个e∩crypt方法前面有一个"at1γe关键字,证 p





0

明真正的方法定义在Natjve层’逻辑定义其实就在so文件中°我们使用IDAPro对so文件进行逆向, 可以找到so文件中定义的人口方法]aγaˉco们-go1dze—‖γγ"∩abjtuti15‖atiγe0tj15_e∩〔rypt’其方法 命名是有一定规律的’就是把Java代码中的包名、类名、方法名都用下划线连接起来’对应的其实是

刚才所说的e∩〔Iypt方法的真实定义。在Java层调用e∩〔rypt方法,就相当于调用了so文件中对应的 Native层的]aγaˉ〔o们ˉgo1dzeˉ"γⅧbabjtut115‖atjve0t115—e∩crγpt方法’后者的参数和前者的参数 -_对应’后者的执行结果会被回传给前者’最后前者返回的结果就是后者的执行结果。 所以’如果我们想在刚才创建的App里完全模拟对so文件的调用,就需要遵循其调用规范,即









包名、类名、方法名要和so文件中的〕aγa〔o川ˉgo1dze—"γγ"hab1t‖ti15‖atjγe0t115—e∩crγpt方法对 应起来°于是我们创建_个comgoldzemvvmhabltutils包’然后定义_个‖atiγe0ti15类’再在 ‖atjγe0ti15类中定义一个e门crypt方法’其实就是把原来App中的定义原封不动地复制到新的App项 目中°最终新的App项目变成了如图l3ˉl39所示的这样。 ∧卸姆wvw了疗仑Ⅱˉ》 』pp》 $γf》穴四叶》 』a”》 g妇岗》…r岳》知雨m酗》舰龋》翻NU沁峨》



琶虫◎红≤■■

0

@÷*=嚼

必p『w千11↑00P`●

吁鳃.. ˉ .



. ″蛾…c咖.卯l血e鹤咖Ⅶ§吨让.Mti1S;





》■″ 吨

b

邑d



b

p■

g 々

僻泌b】《仁色1oS$№ti腔毗i腮{

≥、

..

↑3

私 驴

■四

←/≈

°

$t@仑〗痘[

立 ◆

■●

巳目电





5脾t田。I…油″′,《『0b睡颁eˉ ■mtwEv,);

芍比



》铂

b

√m



pt心〗jc豆徊t『喀m摊爬5tFmgGⅥ屯铲ypt《5tFmg咏『■, Ⅵ门fQ仟各〖§V)b

Y■ √四

p

》 蹲摊议喊0‖篱

)≥

「 p



P

□■ ■』

》■

睹. 必

图l3ˉl39所创建App的最终内容









h撇



h

厂△

第l3章Android逆向

至此’我们已经成功引人了so文件’调用方法也声明好了°下面我们就引人AndServer来调用so

5.弓|入∧∩dSeⅣe「

截至编写本节内容时’AndServer的最新版本是2.l.9’所以在App项目的buildgradle文件中的 depe∩de∩c1e5部分添加如下引用AndServer的内容:

』】《{』□勺

加p1e川e∩t己tjo∩ 0〔o川ya∩zhe∩jje.日∩d5erγer:apj:2.1.9! a∩∩ot己tio∩Proce55or co‖.ya∩zhe∩jie.a∩d5erver:pro〔e5sor:2.1.90

‖□|■■Ⅷ】』■■■口〗〗‖■口|

文件’并将结果通过HTTP服务器暴露出来°

』■■】‖‖‖■■■‖■刁

688

添加之后,AndroldStudio会提示我们要不要下载AndServer包’点击确认即可’这样AndServer包

注意由于AndServer一直在更新,所以最新版本以官方发布为准’见https://glthub.com/yanzhen}ie/

|‖

■ ■ Ⅵ 』 ‖ 词

就成功被下载到项目中了°

AndServer°

接下来’我们先定义-个基本的页面人口,修改src/main/res/layout/actjvity—ma1nxml文件’添加

a∩droid:id=川@+jd/togg1e_5erver"

app;1ayoutˉco∩str日j∩t[∩dto[∩d0f="p日re∩t" 日PP:1ayOUtˉ〔O∩5tra1∩t丁OP-tO丁OP0f="P日re∩t" app;1ayout-〔o∩5tmj∩t5t日rtto5t日rt0十二"DaIe∩t" ‖

日pp;1ayout-〔o∩straj∩t8otto们toBotto∏U千≡"paI.e∩t"

‖」

a∩droid:text≡"05trj∩g/5tart5erγer" a∩drojd:1ayout-w1dth="wr日p-co∩te∩t" a∩dIoid:1ayout-∩ejg∩t="wIap≡co∩te∩t"

■■‘』■

〈B〔』ttO∩

厂■■■■『■

〈?xⅧ1γer51o∩="1。0"e∩〔od1∩g="l」t+ˉ8||?〉 〈a∏drojdx·〔o∩5tIa1∩t1aγout.wjd8et.〔o∏5traj∩t[ayout x‖1∩5;日∩drojd=||∩ttp://5che川a5·日∩droid.〔o"/日p代/re5/a∩drojd" x‖1∩s:too15="∩ttp;//5〔heⅦa5.a∩drojd.co川/too15" x们1∩5:aPP="∩ttp://5c∩e们a5。日∩dro1d。co∏/ap代/Ie5ˉ己uto" a∩dIojd:1ayoutwjdt‖="们日tc∩-p日re∏t" a∩dIoid:1ayo仙t-heig∩t=| |川atcb-pare∩t" tOo15:〔O∩teXt="。‖a1∩∧〔tiγjty"〉

||

—个按钮和—个文本控件’代码如下:

a∩dIo1d;o∩〔11〔促="togg1e5eIveI" /〉 〈丁eXtγ1eW

a∩dIo1d:text=""

日∩dro1d:1ayout-Ⅶjdth="wmpˉco∩te∩t" a∩drojd:1ayo‖t-∩eight≡"wrapˉ〔o∩te∩t" 日∩dIojd81d="@+id/5eIγer5t日tu5"

app:1ayout-〔o∩5trai∩t[∩dto[∩d0+=′0pare∩t" app;1己yout=〔o∩5tr己1∩t5tartto5tart0千="p日re∩t" app:1ayout-〔o∩5traj∩t「op一to8otto∏C千="触id/togg1e_5erver" app:1ayout-〔o∩5tm1∩t8otto阳to8otto汕十=||pare∩t" 日pp:1ayout-co∩5tmj∩t"orizo∩ta1bja5="0°5" 日pp:1日youtˉco∩5traj∩tγerti〔日1bj日5="O.15" /〉

〈/a∩droidx。co∩5tra1∩t1ayout.wjdget.〔o∩5tr日i∩tlayol」t〉

这个按钮就是用来控制AndServer启动和停止的,文本控件是用来显示AndServer的状态信息的°

然后修改-些文本值的定义’打开src/main/res/values/stringsxml文件’把内容修改成如下这样: 〈re5ource5〉

〈5tri∩g∩a‖e="app一∩aⅧe"〉A∩d5erγeI丁e5t〈/5trj∩g〉 〈Str1∩8∩己"e="St己rt5erγer"〉5t己rtSerγeI〈/5tri∩g〉 <5trj∩g∩aⅦe="5top=5erγeI"〉5top5erγer〈/5tri∩g〉

<5tI1∩g∩己川e="5erγer5tarted"〉丁he5erVer155tarted〈/5trj∩g〉

■|}

|「



l3』0基于AndServerˉRPC模拟执行so丈件

689

<5tI1∩g∩驯e=5erγer-5topped"〉丁∩e5erγerj55topped〈/5tri∩g〉 </re5our〔eS〉

对于按钮’我们给它绑定了_个叫作togg1e5erγer的方法,其含义是关闭或者打开AndServer’ 我们需要在‖aj∩∧〔tiv1ty类里定义-下这个方法’并实现启动和停止AndServer的相关逻辑,因此 ‖a1∩∧〔t1γjty类的内容被修改成了这样: pa〔阳ge〔o∏‖。ger爬y.a∩d5erγerte5tj

mport己∩αrojdx。appcoⅦpat。app°∧pp〔o刚patAct1γjtyj mport己∩droid.o5.Bu∩d1e; 1∩porta∩dro1d·uti1.[ogj mporta∩droid.γ1ew。γ1ew; 1肌porta∩drojd.Ⅳ1dget°8utto∩; mporta∩droid°w1d8et。丁extViewj 1"portco∏Wa∩zhe∩jie.a∩d5erγer。∧∩d5erγeI; j川poItco∏.ya∩zhe∩jje.a∩d5erγer.5erγer; 1Ⅷportjaγa。utj1。co∩curIe∩t.丁me0∩it;

pub1j〔〔1a55‖ai∩∧ct1γjtyexte∩d5∧pp〔o‖patActiγjty{ prjγate5erγer5eIγerⅡ priγate8utto∩butto∩j pr1vate丁extγjewtextγiewj 凹γerride

prote〔tedγojdo∩〔reate(8u∩d1e5avedI∩5ta∩ceState){ 5uper.o∩〔reate(5aγedI∩5t己∩〔e5tate)j

set〔o∩te∩tγje"(R.1ayo0t.a〔tjγ1ty一Ⅶai∩)j butto∩≡千1∩dγiew8γId(R.jd.togg1e-5erver)j teXtγi硼=千i∩dγiew8yId(R.id.5erγer5tatu5); 5eIγer≡∧∩d5erveI.web5eIγer(get∧pp1i〔at1o∩〔o∩text()) .port(8080) .t1爬out(1O》 丁1帕0∩it。5[〔O‖D5) .115te∩er(∩eW5eIVer.5erγer[iSte∩er(){ 刨verr1de

pub1jcvojdo∩5t己rted(){ b‖tto∩.5et『ext(R.5tr1∩g.5topˉ5erγer); teXtγieW。5et丁ext(R.5trj∩8.5erγer5tarted); 、

■■



刨γerrjde

pub11cvojdo∩5topped(){

butto∩.5et丫eXt(R.5trj∩g.5tart5erγer)j te×tγie".5et丁e×t(R.5tri∩g.5erγerˉ5topped)j

} .刨γerrjde

pub1i〔γoido∩[xceptio∩([×〔eptjo∩e){ [og.d("∧∩d5erver", e.to5tI1∩g())j } 夕

}) .bui1d()j

bl」tto∩°set丁ext(【.5tri∩g.st己rt5erver〉】 textγiew·5et丁ext(R.5tImg.5erγer-5toPPed)j )



pub11〔 vojdtogg1e5erγer(γi酗γiew){ jf

(|5erγer.i5Ru∩∩j∩g()){ 5erγer。5tartqp()j

}e15e{ 5erγer。5∩utdow∩()i } }





| ■·∏

·口

在o∩〔reate方法里’我们初始化了∧∩d5eIγer对象,指定其运行端口为8080,同时调用1i5te∩er

方法添加了5erγer[i5te∩er对象°在初始化5erγer[15te∩er对象的时候,定义了o∩5tarted`o∩5topped、

o∩[xceptio∩三个方法,它们分别对应在AndServer启动后`停止后、出现异常后的处理逻辑’我们在 三个方法中改变了刚才声明的按钮和文本控件的内容°例如在AndServer启动后,文本控件会显示The serverisstaIted’证明服务器启动成功。

对于和按钮绑定的togg1e5erγer方法,这里的逻辑是判断AndServer是不是在运行’如果没有运 行’就调用5tartup方法启动它,如果已经在运行,则调用5hutdow∩方法停止运行。 这样我们就定义好了AndServer的声明和控制逻辑,同时将其启动和停止行为与按钮绑定在了-

pac代ageco川.gemey.a∩d5erγerte5tβ mpOrt 〔o们。go1dze.们γⅧhab1t°utj15.‖己t1γe0t115j jⅧpOrt 〔o阳。ya∩Ⅲbe∩jie.a∩d5erver。a∩∩otat1o∩.0et日app1∩8j jⅧport 〔o刚.y日∩zhe∩jie。日∩d5erγer.a∩∩otatjo∩。Querypara们; i∏pOrt 〔o∏Wa∩zhe∩jje.a∩dserγer。a∩∏otatio∩。Re5t〔o∩tro11erj

■·Ⅵ‖‖】门」■■】‖‖』■]|」■■司|」■|{』■■‖‖‖(

起。接下来我们还需要声明对应的接口定义’新建一个叫作∧pp〔o∩tro11er的类:

|‖‖■■



mportorg·〕5o∩.〕50‖Obje〔t;

=▲■■■尸■■■■尸|巴



第l3章Android逆向

690

1mportjava。uti1.‖as∩"apj i呻ortjaγa.ut1L"apj 0Re5t〔o∩trO11er

pub1j〔〔1a55App〔o∩tro11er{

} }

0



‖·

这里我们引人了0Re5t〔o∩tro11er`刨uerγPara∏和“et‖app1∩g三个注解,其用法类似Python中 的装饰器,我们将0Re5t〔o∩tro11er注解作用在∧pp〔o∩tro11er类上,同时声明_个1og1∩方法’并将

二■■‖』■|·■■Ⅷ‖‖』■|

αet‖appj∩8(闻/e∩Crypt") pub11〔〕50‖Object1ogi∩(刨uerypara"("5tri∩g") 5tr1∩g5tIi∩g’ 刨ueryParaⅧ(捌o仟5et00) 1∩tO仟5et){ ‖己p〈5trj∩g’ 5trj∩8〉川aP≡∩e"‖a5打阶ap◇(); 5trj∩g5jg∩≡‖ative0t115.e∩cryPt(Stri∩g′ o仟set); Ⅷ日p。put(00s1g∩"」 5j8∏〉】 ret0r∩∩ew]5酬0bje〔t(∏己P)j



“et‖appj∩g注解作用在1ogj∩方法上,绑定对应的路由°

1og1∩方法接收两个参数,-个是5tr1∩g’另_个是o仟5et’方法中会直接调用我们刚才声明的

经过这样的定义’我们就利用AndServer创建了可以接收GET请求的服务’URL路径也是 e∩〔rypt’查询字符串参数是5tr1∩g和O仟5et’返回结果是-个JSON字符串。

』□』■司|』■■Ⅵ|』■■■

‖at1γe0t115类中的e∩crγpt方法’得到5jg∩的内容,最后以〕50‖0bject形式返回5jg∩的值°



整个AndServer就实现完毕了’我们在手机上运行_下整个APp,打开的页面如图l3ˉ140所示°

可以看到页面中间有—个‘‘STARTSERVER,,按钮,同时下方显示“TheserverisstopPed”的 我们点击‘‘STAR『SERVER”按钮’即可看到页面变成图l3ˉl4l所示的这样°

■]■■||‖纠|』■■■||」日■』□〗□】□』《】引』■』■■

字样。

}|卜

l3,10基于AndServerˉRPC模拟执行so丈件

}}

∧∏qSe『γe『TeSt

∧∩dSe『γe『丫e它t

69l



■β「

|}

′』尸

卜「伊 γ

冈≡









笆∩

台γ

『 《

|[

■■·β 卜匡■■》广



∏■…哺灯…

7№…■…

■囚∩‖∩ ∩▲■「∩炉尸■ p

p

图l3~140AndServerTestApp的运行页面

图l3ˉl4l 点击“STARTSERVER”按钮的结果

可以看到按钮下方的文字变成了“TheserverisstaIted,,,就这证明AndServer启动成功了。接下 来打开手机上的测览器’试着访问—下8080端口的服务’输人http:〃localhost:8080/encrypt?strin医 test&oIYSet=0,显示的页面如图l3ˉ142所示。 ◆



| t8O↑↑Set= 圈|oCa|host:8080/e∩c『γpt?st「『∩g二tes

·





b

]↑:32



{"s1g∏,,:""2Q0刀削y"z№γzBjZⅦ"ⅦγⅦ」x"丁d促叫[3‖zQy硼]‖丁γb"丁



p

p



图13ˉ142在手机上访问8080端口的结果

广



可以看到,AndServer通过HTTP响应的方式返回了51g∩的值°

l

6.爬取数据

β ■■■■「 『 β ‖ 》

■■ˉ‖



其实图13ˉl42中返回的5jg∩值就是我们_直说的加密参数token,关于它的含义和生成过程,这 里就不再赘述了。现在我们可以使用Python实现一下数据爬取了’在电脑上新建一个spide【py脚本’ 内容如下: i呻ortreque5t5

8∧5〔0肌= 0‖ttps://3pp9.5〔mpe◎〔e∩ter0

I肋[X0Rl=BA5[0Rl+ °/apj/响γje?1jmt={1mit}№仟set={o仟5et}8to代e∩={to促e∩}° AⅫ5[【γ[R0砒= !∩ttp://1o〔日1ho5t;8O8O/e∩crypt?5trmg={5tri∩8}8o仟5et={o仟5et}』 ⅧXpM[=1O

kI"I『=10

■■■「|

【■■■■

de千get-to代e∩(strj∩g’ o仟5et): a∩dseIγerur1≡∧‖D5[Rγ[R0肌.fomEt(5trmg≡■tⅢj∩8’o仟5et=o仟5et)

|′

|=尸■□「

■‖



第l3章Android逆向

692



句巳





口〕







厅巳



) (





←≈=



γ

◆可‖√





) ]

工 巨



Ⅲ 巳 巳



凸α

′`



■已

』[







α·



△[

巳 巳

巳 Ⅲ



口[



巳 Ⅱ



十oI1j∩ra∩ge(趴XˉP∧6[): O仟5et=j*儿I付I丁

to代e∩=get-toke∩("/api/『∏oγje"’ o仟set) j∩dexuI1=I‖D[X0肌.于omat(1加it=U"I丁’ o仟5et=o仟5et’ to代e∩=to低e∩) re5po∩5e=reque5t5.get(1∩dexur1) pri∩t(‖re5po∩5e0 ’ re5po∩5e.jso∩())

这里我们定义了—个getˉto代e∩方法’接收5tr1∩g和o仟5et两个参数,内部逻辑就是构造刚才

√|·

AndServer提供的请求URL’然后使用requests库请求这个URL’并将响应结果转为JSON字符串, 最后提取出51g∩值,即token°

利用get一to低e∩方法的到token之后,我们就可以构造用来请求列表页的URL’继而爬取列表页 的数据了。现在试着运行—下spider`py脚本’运行结果如下:

可以发现发生了错误’请求被拒绝了,这是因为AndServer是运行在Android手机卜的’只有在

手机上才能访问到localhost:8080,而脚本是在电脑上运行的。解决办法其实很简单,我们只需要使用

』■■』司■‖|‖』■|‖』●||』·|‖|■可』■·〗】‖■■

reque5t5。ex〔eptio∩5.〔o∩∩e〔t1o∩[∏or: ‖丁『p〔o∩∩ectio∩poo1(‖ost≡01o〔a1ho5t|’ port=8080〉: ‖日xretrie5ex〔eeded wjt∩ur1; /e∩〔rypt?5trj∩g=/api/‖∏oγ1e&o仟set二O(〔日u5edby ‖e训〔o∩∩ectjo∩[rror(‖〈ur11jb3。〔o∩∩ectio∩.‖∏P〔o∩∩ectjo∩obje〔tatOx7+d7十O10“5O〉:「ai1edtoe5tab1i5ha∩e" 〔o∩∩ect1o∩: [[rr∩o61]〔o∩∩e〔tio∩re+u5ed|))



adb命令配置—下端口转发就好了: adb十or切己mt〔p:808OtCp;8080

脑上访问8080端口就相当于访问手机上的8080端口了°重新运行spjderpy脚本’运行结果如下: re5Po∩5e{‖cou∩t‖: 1OO, 0re5u1ts0 : [{!1d| ; 1’ ‖∩a们e|: ‖霸王月||姬0 ’ ′a1ja50 : !「arewe11‖γCo∩〔ubj∩e|’ |〔over : ‖∩ttp5://po·"ejtua∩.∩et/川ovje/〔e』da3eo3e655b5b88ed31b5cd7896c「62472.jpg@464脚6▲4们1e1〔』’ 0〔日tegor1e50 : [ 0 剧愉’‖爱』情』]’ ‖pub1i5∩edat! ; {1993ˉ07ˉ26|’ ‖m∩ute0 : 171’ 05〔ore‖: 9·5’ 0reg1o∩50 : [‖中囚大陆‖’ 0中国杏 港‖]′ ‖dra们3 ; ●



|用|[司|』■■』□司』‖■■】〗』■{{」Ⅵ

执行这个命令之后’电脑上8080端口收到的请求’就会被转发到手机上的8080端口,这样在电



{0jd: 10’ ‖∩aⅦe|: !狮于王!」 !a11a50 :丁打e[jO∩瓜j∩g|’ |〔Oγer0 : 』∩ttP5;//PO.眠1tUa∩.∩et/川Oγ1e/ 27b76十e6c于3903+3d7496〕十70786O01e1▲38406.jpg饥64w644h1e1c|’ ,categor1e5! : [‖动昌『 ’ 『歌舞|’ |F险|]′ ‖pub1j5‖edat0 : |1995ˉ07ˉ15|’ 0m∩0te‖ : 89」 05core‖ : 9.0’ 0reg1o∩5‖ : [ 』共国!]’ ‖dra"a0 : ‖辛巴是荣耀囚的′」` 王于,他的父亲木法沙足一个威尸的国王°然品叔叔刀疤却对木法沙的王位砚饥已久·共想坐上王位宝皮’ ■





这次我们成功爬取了数据° 7.总结

本节中我们利用AndServer成功在Android手机上搭建了HTTP服务器,并模拟执行了so文件’

使执行结果可以通过HTTP服务器暴露出来。最后我们通过Python脚本调用了该HTTP服务器,拿到 了关键的token值’成功爬取了数据。





对于模拟执行so文件的场景,AndServerˉRPC不失为_个不错的解决方案’大家在实际生产环境 中也可以尝试应用它。





本节代码见https://githuhcom/Python3WebSpider/AndServerT℃st° q

|3川| 基干u∩|dbg模拟执行so文件 l39节和l3.l0节介绍的两种方式都是在Android手机上执行的so文件,那有没有办法可以在电

脑上直接执行so文件呢?当然也是有方法的’Python的AndroidNativeEmu和Java的unjdbg等都支持 在电脑上直接执行SO文件°

‖|

q





」 0



‖qd

|| 0

l3.ll

基于unidbg模拟执行so丈件

693

b

目前’unldbg的功能相对来说更为强大,使用也更为广泛,所以本节我们介绍利用unidbg模拟执





行SO文件的方法。 ◎凹 血立百 昏←屋 ℃● 竹 ←=巨匠◎

↑.u∩jdbg的简介

△‖「[‖‖·『■「||卜[卜■‖「‖卜

unldbg是一个基于unicom的逆向工具(unicom是_个CPU模拟框架),在unicom的基础上’ unjdbg可以模拟JNI调用NativeAPI’支持模拟调用系统指令,支持JavaVM、JNIEnv和模拟ARM32` ARM64指令°于是unjdbg就可以支持执行基于ARM指令的so文件,也就是可以模拟执行Androld手 机上的so文件°另外除了模拟执行, unidbg还支持Native层的Hook操作’我们可以通过Hook的方 式拦截和修改Native层的—些逻辑。

卜『『‖匹卜

unldbg的GjtHub地址是ht‖ps://githuhcom/zhkl0228/unldbg,里面包含更多详情介绍。 2.准备工作

unidbg是基于Java编写的,这里我们建议使用IntelliJIDEA

√凹资源文件

巴■‖

巴■【『

. U囱a「∏旧q~γ8a -]

编写代码·所以需要安装_下lnte|liJIDEA,具体的安装方式可

山「

以参考https://sempscrapecenter/intelliJ。

■Ub∩at1γe°5o ■Ub∩at1ve·5o

√团巳『爬ab1~γ7己

■ub"己t1γe.5o ■Ub"at1ve.so

我们还需要复制unidbg的源码’命令如下:

√酗X86

p

厂 ■



》■「

;…。隘http3

文件’然后用jadxˉgui提取出so文件,如图l3ˉl43所示°

卜【[■「|‖□■尸)『}|‖■■「■■「

本节我们使用armeablˉv7a文件夹中的libnatjveso文件°

图l3ˉl43 App9源码中的so文件

3.模拟执行

使用IntelliJIDEA打开复制好的unjdbg文件夹’打开后的项目结构如图l3ˉl44所示。

卜}

匹广『|伍尸■厂

我们将得到的libnatjveso文件放到unldbgˉandroid/src/test/resources/app9目录下,如图l3ˉl45所示。 u"憾鞠)u∏‖d鲍=a秘『O‖d) S『c》 teS[)‖酗己〉四倔〉

F气P『ojPα寸





5

≡√媳 ■ 〉睡

》 ·里

巨 匠

√聪 √毋

◎ 凹

‖·

〉知

〉 ?尸

O

P



呛≥

v映

p



L蝉墓副

本节使用的示例App还是App9,和l3.l0节一样’先下载apk



β

■ub"at1ve°5o v蹈x86≈64 囤ub陋t1γe.50

gjt〔1o∩e∩ttp5://gjt‖ub。〔o爪/zhk1o228/u∩1dbg。git

o0



驾 口







雾 三

〉 锣】口户



〉 b?.鸟

.

■■【「|



》 .飞 ℃

》赵

■·

『【|『 ●

.泞app9

■ 「 ‖ 】 ‖ ■ 「 ‖ ‖

■‖巾!|d1MP岛《》 …尸

》睫‖ 》 .早$

『| ■ 『|止■■口

,

〉狗

巴尸匹广『||

( ■



』呈

β ●■』‖‖『 ● 叮 卜 卜 但 尸 } [ | 卜 ■ ■

图l3ˉ]“unjdbg文件夹的项目结构

图l3ˉl45放置llbnative,so文件



」|

| 第l3章Android逆向

694

·‖·

放好后’我们来编写_个Java类实现对so文件的模拟执行°在unidbgˉandroid/src几est〈java目录 下’已经有一些写好的测试文件’都是以包名形式出现。我们同样可以根据App9的包名新建对应的 文件夹’这里我们新建_个名为comgoldzemvvmhabitutlls的包,如图l3ˉl46所示。 □■√

■咕■







■←■



、扩



」|

出□■份

·》〉`厂`≥ˉ≥

{ d





图l3_l46新建一个包

我们再新建_个‖atjγe0tj15类’代码文件保存为NatlveUtils』ava,内容如下: p己〔kage〔o川.go1dze。ⅦγⅧ∩abit°‖t115j

e∩N」1atoI=AmⅢojd[|‖l』1己torBui1deI。十or328it()。5etpIo〔es5‖a眶(铡〔oⅧ.go1dze.‖w咖abit圃)。bui1d()j 于j∩a1№mry贬"℃Iy=e∏H』1己tor.get月e∏℃ry()j ∏m℃Iy.5et[ibraryRe5o1veⅢ(∩酗∧∩drojd【e5o1γer(Ⅱ3))〗 Ⅷ=e∩罚』13tOI.〔reate0日1γikⅦ(∏u11)i

||

Pub1j〔№tjγe‖』ti15(){

(‖‖‖‖∩‖

pub1j〔〔1a55‖己tjγeUtj15{ priγate十j∩a1A∩drojd[Ⅶu1atoIe刚13torj prjγate十i∏己1ⅦⅧ; prjvate「i∩310v喊1a55c15j prjγate+i∏310a1γi化№dl』1edⅧj

』■

i呻oItjaγa。io.「11ej 1mortjaγ3.io.I0[xceptio∩i

」■■〗』■|■‖‖』■Ⅵ||」■■∏|

j们port〔o∩°gjt∩ub.u∩jdbg。A∩dro1d[‖u1atoIj mpoIt〔o∩.g1t∩l』b.u∏idbg。1i∏ux。己∩dIoid。∧∩dIo1d[∩↑u1atorBui1deIi mportco阳。gjthub。l」∩idbg°1i∩ux。日∩drojd。∧∩dIo1dRe5o1veIj 1‖∏port〔oⅦ。g1t∩ub°u∩jdbg.1j∩ux.a∩drojd.dⅧ°*j i呻ort〔o∏。gjthl』b。l』∩idbg·爬∏℃rγ·"e|∏orγj

dⅧ=Ⅷ.1oadUbmry(∩ew「i1e("u∏idbgˉa∩dIo1d/5r〔/te5t/re5our〔e5/app9/1jb∩atjγe。5o闰〉’「a15e)j

咖.〔己11〕‖I仰koad(e∏U3tor);



这里的写法可能看起来比较陌生,不用着急,我会_点点讲其中的原理°首先可以看到,在 ‖ative0tj15类中调用了一些unjdbg提供的类’有∧∩droid[Ⅷu1ator、0γ『∏〔1a55、Ⅶ`0a1γj代№du1e、

■」·□‖‖■■

c15=Ⅷ.re5o1ve〔1a55(闪〔咖/8o1dze/『∏vv∏hab1t/ut11s/‖atjve0t115")j }

№Ⅷry等°

□∧"dro1d[Ⅶu1ator:顾名思义’这代表Android进程模拟器, e阳u1ator就是一个Andmjd进程模



]‖

拟器对象。

d

司』□■

l3.ll

基于unidbg模拟执行so文件

695

■■●】】·■■■厂■

卜‖

□‖e们ory:代表内存’利用它我们可以定义一个模拟器的内存操作接口’例如调用它的"a11o〔方 法可以分配内存空间’调用get5tac促51ze方法可以获取内存栈的大小° □γ‖:代表虚拟机(VlrtualMachine)’我们可以调用∧∩droid[Ⅶu1ator对象的〔reate0a1γ1kγ‖方

法创建_个Dalvik虚拟机对象,有了这个虚拟机后,我们就可以模拟加载so文件了。

□0a1γj刚odu1e:代表Dalvik模块’γ‖对象可以调用1oad[ibrary方法把so文件加载到虚拟内 》》β】

存中,其返回结果就是_个Dalvik模块对象’我们可以模拟调用该对象的〕‖I0∩1oad方法执

△■卜β

行_些SO文件的加载和初始化工作。 □0γ爪1as5:可以把它视为Java层的〔1a55对象°调用γ‖对象的re5o1γe〔1a55方法并传人我们

定义好的Java类的路径’该方法便会返回一个Java类的操作对象’即0Ⅷ〔1a55对象。通过

0`′∏汇1a55对象的_些方法(如〔a115tatjc〕∩i‖et∩od0bje〔t),我们就可以调用Natjve方法了° 直接看上述内容’可能比较难理解°如果想深人了解’可以多看unidbg、unicom的源码’或者

b

学习Android虚拟机的_些基础知识。

所以,‖atjγe0tj15类的构造方法的实现流程基本分如下几步°



(1)利用∧∩drojd["u1ator6u11der类创建一个模拟器对象e们u1ator,这里使用5etPro〔e55‖a删e方

▲β·■■尸

法指定了App9的包名。

‖卜

(2)声明_个‖e‖∏ory对象,这里使用5et[jbraryRe5o1γer指定了其适配哪个版本的AndroidSDK’ 这里指定的版本是23°unidbg目前提供对19和23这两个AndroidSDK的支持’这里使用23,对应 Android6·0·

(3)调用e∏‖u1ator变量的create0a1v业Ⅶ方法创建—个Dalvik虚拟机对象,赋值为Ⅷ° (4)利用Ⅷ变量的1oad[jbrary方法加载so文件,这里我们直接指定了_个「j1e对象’并指定 了so文件的路径’createDa1γ1代γ‖方法的返回结果是_个0a1vj低‖odu1e对象,将其赋值为dⅦ变量。



(5)调用d"变量的〕‖I0∩1oad方法执行-些so文件的加载和初始化工作。 (6)调用Ⅷ变量的re5o1ve〔1a55方法返回一个Java类的操作对象’即0Ⅷ〔1a55对象’赋值为〔15

巴尸△■『『‖|‖尸●‖■=『

变量。

以上流程完成后’我们就可以利用〔15变量调用so文件中的方法了。我们再在‖atjγe0tj15类中 增加_个调用方法,代码如下: Pub1j〔5tIj∩8e∩crypt(5trj∩g5tIi∩g」 j∩to仟5et){ 0γ∏仇je〔t〈?〉re5u1t=〔15.ca115tatj〔〕∩i№t∩odObje〔t(eⅧ1ator’ "e∩cryPt(Ljaγa/1a∩g/5trj∩gi)[java/1己∩g/5tri∩g口’

Ⅷ.addloca1Object(∩ew5trj∩gObje〔t(Ⅷ’ strj∩g))’ o仟set)j

■■●■尸『■■■厅位■「β‖■■厂

retum(5tIi∩g) re5u1t.8etγa1ue()】 }

这里我们定义了一个e∩〔rypt方法’接收5tr1∩g和o仟5et参数,因为其在so文件中对应的

广〔

13

〕aγa=〔o∏‖ˉ8o1dze=们γⅧhab1tutj15‖at1γe0tj15-e∩cIypt方法就是接收5tr1∩g和o仟5et参数°方法中 我们调用c1s的ca11Stati〔〕∩j"et们od0bject方法实现了对Native方法的调用,第_个参数是模拟器 对象’第二个参数是要调用的Native方法的名称,即e∩crγpt’之后的参数就是这个e∩〔rγPt方法的

~田『‖■

参数°对于5trj∩g类型的参数’这里我们使用Ⅷ变量的add[oca10bje〔t方法创建了一个代表字符串 类型的参数。对于i∩t类型的参数’则可以直接传人°

■β「■■■■|

〔a115tatjc〕∩1‖et∩odObject方法返回的是_个0γ帕bject对象’通过调用这个对象的getγa1ue方 法我们就能得到Native方法e∩crγpt最终的返回结果了’这里我们加了一个强制类型转换,把返回结

仿●厂卜‖凸■■尸份∩【尸‖》「|■■卜‖■厂卜|β【卜【

果转换成了字符串类型°

最后我们来测试—下’在‖atjγe0ti15类中添加一个∏ai∩方法:



||

第I3章Android逆向

q

696

pub1jC5tatj〔γojd们aj∩(5tr1∩g[] 己r85){ ‖己tjγeUtj15ut115≡∩eW‖atjγe0tj15()j

5trj∩gto长e∩二uti15.e∩〔rypt("/api/『∏oγie"’ 2)j 5y5teⅧ.ol」t.pm∏t1∩("toke∩:"+to代e∩)j





运行‖at1γe0t115类’操作过程如图l3ˉl47所示。



彤■



赠〔Opγ √≈

丫P刨th

k

》■

伺P0 l

》≈



√■ 咖











@N龋牌鳃潍

甲匹■∏β『巴

●‖

■■

·飞

夕■



●■ ■飞





》■

』●〗】□■】■■司】■·]

γ曲 \∩



q

》■ 0

》■

8『胎

『γ0》0↓‖‖出『a?《低

》■



》穗

o‖》l‖面府谭‖WVpo忧$



中『U .0『〔u凹小

阀网出

d

!〕e0Pl由

8u0‖o嘲0咖|p刚憾bqⅣ`

怜隘m硼…t醚蟹血』厕矿

^OR

享?DPbug靶峨}V0Mt↑剑$"∩刚∩《

钝司触V巴山0‖S



~■

川◎『 ∩u∏DGh仙(〗



q

图l3ˉl47运行‖日tive0ti15类

最终的运行结果如下:

+i∩j5∩ed5p=u∩idbg彻xb仟仟788’ o仟5et=2帅5 02:36$49.192 [∏a1∩]0[80Cco们。g1thub。u∩jdbg.5pj.Ab5tr日〔tLoaderˉ则∩Ⅷapa11g∩ed=0x10oo’ 5taIt≡0x↓0192OOO’

可以看到结果中输出了最终的token值’我们成功模拟执行了so文件。

』 ■ ]

』■■■可

‖□□】

〈depe∏de∩〔je5〉 <depe∩de∩〔y〉

|‖

〈?x‖1γer5jo∩=《o1·0"e∩codi∩g=||0丁「ˉ8“?〉 〈proje〔t

司‖』

首先需要在unjdbgˉandroid/pom.xml文件里面添加对Sp前ngBoot的引用,添加两个depe∩de∩cy即 可,代码如下:

■■~

我们可以借鉴l3.l0节的思路,也通过HTTP服务器将unldbg的运行结果暴露出来’这个可以用 Java中的Sp∏ngBoot实现°



我们已经成功拿到token结果了’接下来如何爬取数据呢?现在模拟执行so文件的逻辑是用Java

语言编写的,难道爬虫也要用Java语言编写吗?虽然可以,但这不是唯一选择。

』■■■可|

4暴露结果

{|

b日se=0x4o192oo0’ 5ize=4096

to促e∩:Z‖R田ZD代xZjR耐j[2Ⅷ[o‖0U3“I1γz01Z丫h耐zdi仰[zZ丁∧x‖0czZ5"x‖jI叫zγx‖D∧5



』‖

o2:36:』9。191 ["aj∩] 0[8lL〔o∏.g1t∩ub.u∩1dbg·∧b5tra〔t[川u1atorˉe「∏u1ateRX刨×40O0佃e1[1ib∩atjγe。5o]Ox+4e1



』■■日||‖』·∏』日



l3』l

基于unidbg模拟执行so丈件

697

〈groopId〉org.spri∩gfr己∏ewor低.boot〈/groupId〉 〈art1千actId〉spri∩8-bootˉ5t己rterˉ眶b</己Iti+日〔tId〉 〈γer5jO∩〉2。4.3</γeISjO∩>

〈/depe∩de∩cy〉 〈depe∩de∩〔y〉

〈gIoupId〉org°5pr1∩g于m∏记切ork。boot</groupId) 〈aIti十actId〉5pIi∩gˉmotˉ5t己rterˉtest〈/a】ti千actId〉 〈γersio∩〉∑·4°3〈/γer5io∩〉

〈/depe∩de∩〔y〉 ●







</dePe∩de∩〔ies〉 〈/proje〔t〉 [■厂‖|‖■尸「|■厂

添加完后, IntellUIDEA会把SpnngBoot对应的包下载到本地。接着我们定义_个∧pp〔o∩tro11er 类’这个类和‖at1Ve0tj15类同级,其基本写法和13』0节非常相似’类内容如下: pack日geco‖↑°go1dze·刚Ⅷ∩abit.uti15;

G

● }



i‖portorg。5pIi∩g十ra"ewor促·web·bi∩d·a∩∩otatio∩。【eque5t∩日ppj∩8; j∏portor8。5pri∩g+r日"ewor代°腮b°bj∩d。a∩∩otatio∩·Re5t〔o∩tro11er】 j∏portjava·utj1.‖日5∩‖日p5 iⅧportj3γa·utj1.‖ap;





@∩e5t〔o∩tro11er

P

pub1i〔〔1as5∧pp〔o∩tro11er{





‖atjγe‖ti15 (』ti15=∩eW‖atjγe‖]tj15()j





卜.

"eque5t‖appi∩8("/e∩Crypt") pub1i〔胞p〈Stri∩g’ 5tIj∩g〉e∩crypt(5tIi∩8stri∏g’ i∩to仟set){ 5trj∩gto代e∩二uti15.e∩〔rypt(stri∩g’ o仟5et)j ∩ap〈5tri∩8」 5tr1∩g〉‖ap≡∩eW‖己5∩"ap◇()j 『∏ap.p(」t("to代e∩||’ to长e∩);

F

●『’圆尸卜尸|尸■□|β巴仔「)瞬●「‖【广‖『|户|

retur∩∏ap】

} }

这里其实也是定义了~个GET请求’接收的查询字符串参数也是5tIj∩g和o仟5et,再加上调用 ‖atiγe0t115类的e∩〔rγpt方法获取的token结果’最后返回一个Map对象。 下面在App〔o∩tIo11er类同级的地方定义一个Sp∏ngBoot人口类∧pp5erγer’其内容如下: pa〔阳geco『∏.go1dze°∏γⅧb吕bjt°uti15j

‖卜=尸■仆□■=厂

mpo∏or8.5pr1∩g十ra|∏ewor代·boot。5pri∩gApp1ic日tjo∩j j‖portorg.5prj∩8千m阳e刊ork.boot.autoco∩十igure.5pri∩gBoot∧pp1i〔atjo∩j 05pri∏g8ootApp1ic日t1o∩ pub1i〔〔1a55App5erver{ pub1i〔5tatj〔γo1d∩m∩(5tIj∩g[] arg5){



5prj∩g∧pp11〔atio∩app≡∩e"5pri∩g∧pp11〔atio∩(∧pp5erver.c1a55)j



3Pp.ru∩(arg5)j

| |



| |



● 尸 ■ ■ ■ | ■ ■ △

这个类也是可以百接运行的’运行之后就会开启一个SpnngBoot服务°那这个服务运行在哪个端 口呢?这个似乎还没指定?对此可以创建_个umdbgˉandroid/sm/tesUresources/applicationprope】ties文 件来声明Sp∏ngBoot服务运行的地址和端口,文件内容如下:

■ 『 ■

5erγeI.addre5s≡O。O.0·O

■ 》

5erγer.port=9999



■■■『■厂仪■■匠◆「‖}‖}‖■尸巴‖◆「■■■■

最后,运行AppServer即可启动SprmgBoot服务’该服务会运行在9999端口’操作如图l3ˉl48 所示。

■ ■ ■ ■ ■ ■





第l3章Androjd逆向

698

《e v

》》√



{』

尸旧



‖ 卜,【o『『

∧pp5e‖ve『

J

‖ 』 ·

N1『唾0

』|」·】】□■』■】■·〗‖●‖‖』∏」●‖】』勺‖|□‖』◎‖|刑‖』《‖‖】司‖』‖」』■』月]





.` 『 『〗e枷.「u『〔w

B

v







de

!‖ 日□『】

『J



^痴 回∑辟『ve7

』 〔O 〕∩So‖G

二二ˉ==≡|ˉ|===二=二≡ˉ=|

N

γ程

D酗↓ Aph〗5准‖ve′ p

v2.43 (v2.43)

SD『、i∏q8o◎t 二: p『、X∩g



忱· 0

///



图l3ˉ148运行AppSeⅣer

在测览器中访问测试URLhttp://loca‖host:9999/encrypt?strjng=test&offSet=0’返回结果如图l3ˉl49 所示°

÷

-》○

‖◎c□‖∩o$t;9999/印c『vpt?9叮0∩g×



}+

●|oca‖肘ost:9999/e∩C叮pt?St「i∩g=teSt&o什Set=o

—_■≈—

→≡≡~—___■

=旁-吩■←硒~

咒 户

-一≈■

~冗

巴-~■Ⅷ午≈企=哦→

■←

■→~=←←≡≈→←=■…

』■|■■‖』‖』`』勺|



』·‖』·|』‖··】·|』■〗】可|』·‖』□〗』‖日‖|··

:吕

‖d‖】〗e山∩dbqdncod

|〖Ⅶ『x〗『滤

{圆七◎ken■8.Zj0…IwZ印yNWE3Yz1mZjZ1ZDFhNDE咖ⅣFjHZVjZDcwNTE2ZmHXYSwxNjI4ⅢzYyNDH3叼}

可以看到返回了token结果,这样我们就成功把利用unldbg模拟执行so文件的结果也通过HTTP 服务器暴露出来了,从而就可以实现调用。

□]□」』■■】〗‖」□■■

图l3ˉ149测览器返回的token结果

i呻ortreque5t5

|‖□■』■、□■

B∧5[0RL= 0http5://app9。5〔rape.ce∩ter|

|」』□〗|

5.爬取数据

我们使用Python脚本来调用上面定义的HTTP接口和爬取数据:

0‖I00C‖R[≡ |http://1o〔a1ho5t:9999/e∩crypt?5trj∩g={5tIi∩g}ho仟set≡{o仟5et}』

‖ ■ Ⅶ

I"[I0∩[=B∧5[0R[+ ‖/ap1/帅γie?1i‖it={1加it}8o仟5et={o仟5et}&to低e∩={to代e∩}‖ 灿Xp∧C[=1O

de+8et-to促e∩(5tr1∩g’ o仟5et): u∩jdb8-ur1≡ l」‖ID8C0R[·+or吨t(5tri∩g=5tIj∩8’ o仟5et=o仟5et) retur∩request5.8et(u∩1db8-0r1).jso∩().get(|to找e∩0)

■■‖□」‖■■■■‖|‖』●■口{■■』

儿I甘I丁≡1o

』■】■■



13.l1

基于unidbg模拟执行so文件

699

千Orii∩m∩8e(灿X-p∧C[): O仟set=j*u料∏

to促e∩=get~to促e∩("/apj/‖∏oγie厕’ o仟set) 1∩dex l』r1≡ I‖D[X0RL.千omat(1jmt=lI"I「’ o仟5et=o仟5et’ toke∩≡to出e∩) respo∩5e=reque5t5.get(i∩dexur1) prj∏t(|re5po∩5e ’ re5po∩se.j5o∩())

爬取结果和l3.l0节是_样的’这里不再赘述。

6.总结

本节中我们学习了利用unidbg模拟执行so文件的方法’同时为了实现数据爬取’我们通过 Sp∏ngBoot暴露了模拟执行的结果,最后顺利通过Python脚本对接接口的方式爬取了数据。

本节内容其实仅用到了unjdbg所有功能的冰山_角,要想深人了解更多内容,可以研究umdbg的

■ △庐【■‖|止尸‖『且■「||■■「■尸「‖‖■『『匡尸「|匹『‖广匹伊|匡■『‖【′′

源码。

本节代码见https://githuhcom/Python3WebSpider/UnidbgSelver°

●■[●■‖|△■「卜|【~尸‖‖■『|}●厂卜||‖卜『|》|‖‖匹≥「亡「■『‖■‖■厂「|「■「}【【■■『|卜■「||卜巳厂}■『『‖『■ˉ尸||【【【『|止尸「‖【■『′|||■「||[「口『|仑==■【『‖『『『□「‖■■■■■■■■口巳■

厂[

↑3







□■■∏』|」·■、‖」‖】‖■·‖·‖|



△□门

巳巳



面 页



章 第]4章 ▲



■Ⅱ

二■』□■■‖‖‖■■Ⅵ



在前面所讲的内容中,解析页面利用的都是规则匹配,这种方式可能需要借助测览器找到最佳的

表达式`Selector`XPath等’那工作量实在太大了°如果配置不当’还会产生解析错误的问题。例如 正则表达式在某些情况下无法匹配, Selector、XPath编写错误或者提取不全。另外’如果页面突然改 版了,之前配置的规则可能就没法用了’这也是—个隐患。



目前有-种更智能的方法可以帮我们解析出网站内的新闻列表链接、标题、正文`发布时间等’ 用起来很方便。但内容的爬取过程毕竟是用算法实现的’所以正确率达不到l00%’而且即便是人工 写出来的正则表达式Selector`XPath’也难免会有不兼容和错误的情况,因此在能容忍_定错误的

‖|■‖|‖日』‖|』√〗」‖‖‖』|』口‖‖

但如果切换了场景,例如分析舆情,就需要爬取成千上万个新闻网站’把这些网站上的新闻文章 都爬取下来’包括标题、正文、发布时间等’我们会发现不同新闻网站的页面差别非常大,标题`正 文、发布时间对应的正则表达式、Selector`XPath等各不相同。这时如果手动针对每_个网站写正则



‖(‖」

Selector`XPath,甚至需要正则表达式辅助提取细节’同时利用BeautjhllSoup、pyquery、Re等库提 取和解析内容°在—般情况下’这么做是没有问题的°

d

情况下’用比较智能的解析方案爬取页面内容是明智的。

↑4↑

□‖刁

本章中我们就来学习-下智能解析页面的原理和实现算法。

页面智能解析简介



简言之,页面的智能解析就是利用算法 从页面的HTML代码中提取想要的内容,算

法会自动计算出目标内容在代码中的位置并





故宫,你低调点!故宫:不p实力已不允许我继续低调

‖ m…问扣日叼缉钩

1,副s^.、…恿鹤mm囱

泉Ⅷ总中…码

将它们提取出来。 .我的名丰凹续戴缄》快耍600岁了,这上元的夜田′总是让我沉醉,这么久了却从未

↑.实例引入

以_篇新闻的预览页面为例’如图l4ˉl 所示。

q



俘止· ■

■■铂之上的月光’■照进古人的宫殿;缄妇士■延的灯影,映出了角攫的现丽·今

q

夜0-群呻馆人将残点亮′我在北京的中央,印给团四的你们』-座壮浪的城·囱





我们的需求是提取该页面中的标题`正 文、发布时间等。

想必大家可能见过,现在不少测览器提 供了阅读模式,例如我们用Sahrj测览器打 开示例新闻页面’然后开启阅读模式,效果





{ √ ↓ˉ′/

宁.芍寸烂宁飞… P一



ˉ\\iˉ|

■■■■■■

将如图l4ˉ2所示。



d





图l4ˉl

新闻的预览页面 q

q



· q



l4」 页面智能解析简介 十

.…≈翘…

叮■

·喧

一…

ˉ—言≡ˉ ≡…=二

●◆岔田〈

70l

· _



故富,你低调点!故富:不,实力已不允许 我继续低调 ■我的名字叫紫旗城0快耍6O0岁了,泣十元的夜臼′总是让我沉醉’这么久了却从未 俘止· °

.■■之上的月光■照进古人的宜Ⅲ;壤墙上绵延的灯影,映出了角攫的n丽·今

夜’-群呻馆人将我点况‖我在北京的中央′献纬团田的你们′-魔壮混的械· ∩

赵 ■

≡=守葱

亡■

啥~■■

一°宁



瓣 ▲°鳞

订●■朋…皿≡■

°`\` ! .

醋●●

ˉ.、.→-

图l4ˉ2用阅读模式打开示例页面

可以看到页面变得非常清爽,只保留了标题和正文。原先页面中的导航栏、侧栏、评论等统统消

失了。这是怎么做到的?难道提前针对这个页面写好提取规则了吗?当然不可能。其实是阅读模式内 置了一些页面解析算法’可以自动抽出并呈现页面中的标题`正文等内容° 本节中’我们就来了解—下页面的智能解析相关的知识。 2页面的智能解析

所谓页面的智能解析,就是不需要再专门写提取规则,而是利用算法直接计算页面中特定元素的 位置和提取路径°针对我们的需求’可以通过算法计算出新闻标题是什么,正文应该在哪个区域’发 布时间是什么时候。

其实智能解析操作起来非常难,人在看到网页上的一篇文章时,可以迅速找到它的标题、正文` 发布时间、广告位、导航栏等°但把这篇文章放在机器面前’机器面对的仅仅是—系列HTML代码’ 怎么做到智能提取呢?其中融合了多方面的信息和规律°

□标题:其字号一般比较大’长度通常介于l句话和2句话之间’位置_般在页面上方’且大多 数时候和tjt1e节点里的内容一致°

□正文:其内容一般最多,且包含多个p标签(段落)或者1"g标签(图片)’宽度_般占页面 的三分之二’文本密度(字数除以标签数量)比较大°

□时间:不同语言的页面中显示的时间格式可能不同(如202lˉ02ˉ20或者202l/02/20等,也可 能是美式的记法)’但格式的种类是有限的’可以通过特定的模式识别° □广告:其标签一般会带有ad5字样’另外大多数广告会处于文章底部、页面侧栏,并包含-些



特定的外链内容°

所以说’页面中的内容对应的节点是有一定特征的,包括节点位置、节点大小、节点标签、节点 内容、节点文本密度等°智能提取除了利用这些特征,在很多情况下还需要借助视觉特征和文本特征, 因此其中结合了算法计算、视觉处理、自然语言处理等多方面内容°把这些特征综合运用起来’再经 过大量的数据训练’是可以得到一个非常不错的效果的° 3.业界进展

随着互联网的发展和信息的爆炸式增长,互联网上的页面会越来越多,页面的喧染方式也会发生



702

第l4章页面智能解析

很大的变化’智能解析能够大大减轻我们抽取信息的工作量。

其实,工业界已经有了落地的智能解析算法应用’例如DifTbot`Embedly等°目前,DMIbot的提 取效果算是比较领先的,其官方曾做过_个评测’使用不同的算法依次提取Google新闻上一些文章 的标题和文本’然后与真实标注的内容做比较’比较指标就是文字的正确率和召回率’以及根据二者 计算出的Fl分数,结果如下°

□正确率: 0968° □召回率: 0978。 □Fl: 097l°

我们可以发现’对于Google新闻的这些数据’DifIbot大约能达到97%的正确率’效果还算不错° DifTbot是一家专门做网页智能提取的公司,提供了许多API来自动解析各种页面°其算法依赖于 自然语言技术`机器学习、计算机视觉`标记检查等,并且所有页面都会考虑当前页面的样式以及可

视化布局,还会分析其中包含的图像内容`CSS甚至Ajax请求°在计算_个节点的置信度时’会考 虑该节点和其他节点的关联关系,基于周围的标记来计算每个区块的置信度°总之’DifIbot自20l0年 以来_直致力于提供这方面的服务’Dj问bot就是从页面解析起家的,现在也专注于页面解析服务,正 确率高自然不足为怪了°

但DifTbot的算法并没有开源,只是以商业化API的形式售卖’我目前没有找到介绍其具体算法 的论文。不过不妨碍这里拿它做案例’可以稍微体会-下智能解析算法能达到的效果° 4。D|∏bot

打开DjfTbot的官网(h忱ps:〃www.diHbot.com/),首先注册—个账号,会有15天的免费试用期, 注册之后会获得_个DeveloperTbken,这就是使用DjfIbot接口服务的凭证° 接下来切换到测试页面(https:〃wwwdifIbotcom/dev/home/),测试—下Djffbot的解析效果。这里 我们选择的测试页面就是本节开始所述的页面,填人页面链接’API类型选择“ArticleAPI”’然后点 击“TestDnve”按钮’就会出现对测试页面进行解析的结果’如图l4ˉ3所示° ∧UtO们己t‖〔∧p‖爬5t0『‖γe

∏【甲乞$/′∩砷3.j忙∏g【o丽/【′7ⅦQ〔卯∑pGwu

|…^翱.卜`『 .. |

田f↑b°tm引W妇o『恤碎′/…弘…【吨欠′…u

} Ⅷ翅]修丽-、 赋



故官,你任■点‖故■:不0实力已不允许″迟任■ …『20「曲泊闸键沥9"口川『



中酥闻网

驭…∏

帕唾惯(′钓吨喇…/…



^拽的名字刚Ⅸ蘸迅,快贝6”歹了,泣卜…旧,总zi上戮洒,这么久了却从未俘止· ˉ

ˉ■瘴亥卜的月光0■照进古人的宫hi阻坦卜…蛔『影,以出丁●恨的■酮.今覆`-Nm 恤人将拽点况,我在此京的中央"鳖耸钮…你们,一座壮n…· .

图14ˉ3解析测试页面的结果

‖‖〔‖』■■】】『‖}■■■「

■■■■■尸

页面智能解析简介

l4.l

703

■■尸

巴 ■ ■ 『

■广β



可以看到,DifTbot帮我们提取了标题(title)`发布时间(date)`发布机构(author)`发布机构 链接(authorUrl)和正文内容(text),而且目前来看都十分正确,发布时间也在自动识别之后做了转 码’格式是标准的。

继续住下’看还有什么字段°可以看到∩t们1字段,和teXt字段不同的是’它包含文章内容的真 实HTML代码,因此里面会包含图片,如图14ˉ4所示°

尸||仆「凸「

故呻角橙被灯光装点出∑癸的节日气

卜【尸′|

……

卜>

呵“ →

■■

.我的名宇叫饿攒罐,快兵舶0岁了|这上秘覆硝°总思讣戮孵′这么久了酗紊停止. .

》 ■

■伯之上的月光.■煤进古人的宙■塘追上蜘延mT影·缺出了角攫的瑰■·舍夜,ˉ鲜涛 蛔人m点亮,戮在北寂的中央蚁给…的你们,_座杜琅的缄. .

∏ 巴

■厂仁■尸》■尸

|{|

「β

故■镰睬蛔

今年元■节°故■通来了E…年以来的茁次ˉ叮盆° 灯台开始■『敌宫…在m上耳下

了这祥-段话· △■户‖》 ■「

△ β | 【 庐 ■ 『 『 〖 ∩ β ● [ ■『 |

之后还有加age5字段,以列表形式返回了文章套图及每一张图的链接’另外还返回了文章的站 点名称`页面所用语言等,如图l4ˉ5所示。



|~型′′ |』…曾v厉 『







…位′′↑■嘻….缅‖′■撇mγ…

感P

中…闻网

■ ■ 「

△■「巴■=

呻…蛔,沁铀2们9隧5〗哑c瓤丁

′} 『「

「|

9…乙

lh吧〖αⅧ

↑皿∏■nm呻』as| z" G

p咖州

…″………欠′=

图l4ˉ5其余返回结果



『仔■尸|[·、「坠β

!

p酷…

厂▲

k乓库万[

■■ 厂中

p

图l4斗文章内容的真实HTML代码

|■■▲■「)|儿尸『‖「■【■■

704

第l4章页面智能解析

当然’我们也可以选择JSON格式的返回结果,其内容会更加丰富,例如图片的宽度`高度`图 片描述’以及面包屑导航等,如图14ˉ6所示° α什bOtm■‖v5‖SOfh蝉■M/晦咆0f嚼Ⅷ·mm′【门…唾尸WU

乙咆〔丁■ 《

°0了蝇T●£t≡■ 《 ,0呻t工Q间■=: 『 0?≡1田3…D4〕29S3w『

0·〔■ub●ch宫】α心「γ】】】1O0了99〗1”q】〕8718】5E拓Q34]29S〗■· 凹了□…t=j$Q∩p≈ ‖‘

.°p鲍亡U丁1■: ■ht【p苏2〃"G蹈.1↑e"@.cc叮c/7ko〔“2p翻jM0 =牢』叶8 ■凸厂tt[lG0o 0 o°■◆厂乞卫m■g 〕

》·

国°■j●〔t■=g [ {

酵“t■闻: 哟唯d『 20Feb201902日26f酮酗Y00′

碑』■啪■■日 [ (

·o巳■t皿厂■1贴i9ht■8 460∏ ■■』·『h的『640’

■d立??bot0r』00》锄2闲■qe|3卜』〗3931“M"′

■汕丁1d恩8 岭http8//Go°1↑e∩0h响·〔□■/02/20】w唾1纫1731DC8∧∑死D2】』吸7沪2γ73锣9rF319B330】酗∧】 ■ 1巴·$m蜒鳃≈…0·侧》 M∩■t●r■1N』□th酗; 69o『

晦p厂…『y口0 8 〖rD冶o

图l4ˉ6

JSON格式的返回结果

经过手工核对’发现其返回的结果完全正确,这说明正确率还是很高的° 所以,如果你对正确率的要求没有那么严苛,那么使用DjfIbot可以快速提取页面中所需的结果’ 省去了绝大多数手工劳动,可以说非常赞°

另外’Diffbot也提供了官方的API文档,如AnalyzeAPI、ArticleAPI`DisscussionAPI等。下面 我们以ArticleAPI为例说明_下它的用法,其官方文档地址为htms://wwwdiffbot.com/dev/docs/ aItjcle/,API调用地址为https:〃apj.di1Tbot.com/v3/article° 我们可以用GET方式请求这个API’其必选参数有下面两个。 □to促e∩:开发者的TDken°

□ur1:要解析的URL链接。 可选参数有下面几个。

|||

□千1e1d5:用于指定返回哪些字段’默认已经有_些—定要返回的字段,还可以指定需要额外返 回的字段°

□pagj∩g:如果文章跨了多页,那么将这个参数设置为十a15e可以禁止多页内容拼接。 □‖a×『ag5:用于设置返回的Tag的最大数量,默认是1O(单位为个)。 □tag〔o∩十ide∩ce:用于设置置信度的阐值’置信度超过这个值的Tag才会被返回’默认是o.5° □d15cu551o∩:将其值设置为十a15e’代表不会解析评论内容° □t1"eout:解析时的最长等待时间’默认是3O(单位为秒)°

□〔a11b日〔戊:为JSONP类型的请求设计的回调。

其中大家关注更多的应该是+je1d5’这里我专门梳理了_下需要返回的字段’首先是—些_定要 返回的字段°

□type:文本类型’这里就是artjC1e。 □tjt1e:文章的标题。

□teXt:文章的纯文本内容,如果是分段内容,那么里面会以换行符分隔每—段。





l4l

页面智能解析简介

705



『.



卜 卜

□∩t‖1:提取结果的HTML内容°

□date:文章的发布时间’格式为RFCll23。

□e5t1Ⅷ己ted0ate:如果发布时间不太明确,就返回-个预估的时间;如果发布时间超过两天或 者没有发布日期,就不返回这个字段。

□authOI:文章的发布机构°

) p

□authOr0r1:发布机构链接。



□d15〔u55io∩:评论内容,和DisscussionAPI的返回结果一样°





p





p





↑ ◆

P

P

□hu∏a∩[a∩guage:语言类型,如英文、中文等。 □∩u"page5:如果文章是多页的’那么这个参数会控制最大的翻页拼接数° □∩extpage5:如果文章是多页的’那么这个参数可以指定文章的后续链接。 □51te‖a∏e:站点名称°

□pub1j5herRegjo∩:文章的发布地区° □pub1j5∩er〔ou∩try:文章的发布国家° □page0r1:文章的链接° □re5o1vedpage0r1:如果文章是从page0r1重定向过来的’则返回此内容°

□tag5:文章的标签或者文章包含的实体’根据自然语言处理技术和DBpedia计算生成’是-个 列表’里面又包含以下子字段°



k

■1abe1:标签名。

P

■COu∩t:标签出现的次数。



■5〔oIe:标签置信度°



■rd+『ype5:如果实体可以由多个资源表示’那么返回相关的URL°

p

■tyPe:标签类型° ■uri:DiffbotKnowledgeGraph中的实体链接°









□j川日ge5:文章中包含的图片° □γjdeo5:文章中包含的视频°









p









} b



□bread〔IuⅧb:面包屑导航信息° □dj仟bot0rj:Dlffbot内部的URL链接。

以上固定字段就是“如果可以返回就—定会返回’的字段,是不能定制的。我们也可以通过「je1d5 参数扩展如下可选字段。

□quote5:引用信息。 □5e∩tjⅦe∩t:文章的情感值,取值在ˉ1和1之间° □1j∩促5:所有超链接的顶级链接°

□query5trj∩g:请求的参数列表°

好’以上便是AmcleAPI的用法’大家可以在申请之后使用它做智能解析。下面用-个实例来看 加portreque5t5’ j5o∩

l』I1= 0∩ttp5://api.dj仟bot.〔o∩/γ3/artj〔1e′ par日Ⅶ5={ 0to代e∩` 8 077b41+6千bb2“96dS113d528306528+a‖’

‖ur1! : 0http5://∏ewS.j千e∩g·〔咖/〔/7代QCα2Pe‖0‖』





h

-下这个API的用法:

‖p心

h

厂↑4

|千ie1d50 ; !|∏eta‖



re5po∩5e≡reque5t5。get(uI1’ para‖5≡Pam"5) pri∩t(j5o∩°duⅢp5(re5po∩5巳j5o∩()’ i∩de∩t≡2」 e∩5urea5cii≡「a15e))







!



第14章页面智能解析

706

这里首先指定AIticleAPI的链接,然后指定了para‖5参数’即GET请求的参数。参数中包含必 选的to促e∩、 ur1字段’以及可选的「ie1ds字段’f1e1d5字段的内容为们eta标签。 运行结果如下: {

"reque5t":{

"page0r1": "‖ttp5://∩ew5。i十e∩g。coⅧ/〔/7促Qcm2Pe‖0′,’ "日Pj": 回己rti〔1e圃’ "+ie1d5曲吕 闻5e∩tme∩t’ Ⅷet日"’



"γer5iO∩": 3

}’

"object500: [ { "d己te": "‖ed’ 2O「eb2O190卫826;OO酬『"’ ·、‖|·

"jⅦ3ge5国: [ { "∩3tum1‖eight": 46O’ ""jdth冈: 6』0’

"dj仟bot0Ii|0 : "m己8e|3|ˉ1139316034"’ |0ur1圃: "‖ttp://e0·i「e∩8mg·〔咖/o2/2o19/o219/1731D〔8A29[8∑219〔7「2773〔「9〔「319B3503D0A15jze382 "69oh46o°p∩g"’ "∩atuIa1‖idth": 690’

// ·..

〈√

"prmary■8 true’ "height": 426 }’ →

」’

"tit1e铡8 "故宫’你低词点|故宫:不,实力已不允许我继续低词"’ "bread〔rⅧb": [ {

"1i∩促"8 "∩ttp58//∩e佣s.j千e∩g.〔o『∏/00’ "

∩a爬闻: ■资讯"

}’ { "1j∏代N: "打ttP5://∩ew5.i+e∩g。〔刚/5∩a∩低1jst/3ˉ35197ˉ/"’ 00

∩己爬圆: 冈大陆"

} ]’ "∩u帕∩la∩guage": "z∩"’ "爬ta圃:{ "og蜒:{ 凹og:tme口: 002O19ˉ02ˉ2O02826:"00’ 圃og8i阳ge闪8 "http5://eo·i「e∩gi吧.coⅧ/02/2O19/0219/1731D〔8A∑9田2219〔7「2773〔「9〔「319B35O3m∧1 size382w69OM60·p∩g同’ 闽og:〔ategory": "凤凰资讯"′ og:眶btype00 : "∩ews"’ "og目tit1e": "故宫,你供调点|故宫:不’实力已不允许我继续低词"’ og:ur1圆: "http5://∩ews。j「e∩8·co∏‖/〔/7代Q〔卯2pe即口’ "og:de5〔rjptio∩": " “我的名字叫紫禁成,快兵6oo罗了’这上元的夜有’总是让我沉醉,这么久了 却从术件止°’’ “玄■ }’ "re千erIer口: "a1Way5阔’ "de5〔r1ptio∩国8 " “我的名字回{紫禁咸,快共6OO岁了’这上元的夜啊’总是让我沉醉’这么久了匀从术 伴止° ” “宜"’

』■】叮‖』‖●■‖]勺·削‘‖』■』|」■‖■]划‖‖|·|||】■】■司|||□』

"5jte‖日爬": "i十e∩g.〔o们"’ "type剥8 ∩己rtiC1e圃’

√‖‖

"al」thOr缉: "中国浙闻网"’ "e5tmated0ate■: "‖ed’ 2O「eb2O1906:《7;52C盯"’ ′‖di仟bot0ri": "aIti〔1e|3|15911372o8阐’





‖|

"代eywoId5阔; "故宫紫禁域故宫博物吭灯尤元订节博物馆一某难求元之中所杜午门杜洋戴品文化

"日ut∩or0r1圃: "∩ttp58//十e∩g·i「e∩g·〔咖/author/30890▲"’ "page0r1": "∩ttp5://∩eW5·1十e∩g·C咖/C/7kQC仰2peⅧ口’ !!‖tⅧ1阐: ■〈p〉&1dqUo;我的名字叫紫禁成’快共6创岁了’这上允的夜啊’总是让我沉醉,这么久了却从术停止.

({

立帝滑明上河图元宵十里江山图冬"’ "tit1e圃; "故宫,你低调点!故宫:不’实力已不允许我蛀绘仗词凤凰资讯" }’

■■司■|√





l42详情页智能解析算法简介

707

…〈/b1oc促quote〉〈/b1o〔代quote〉"’



·text口: "画我的名字.{紫禁出’快共6OO岁了’这上元的夜钉,总足让我沉醉,这么久了却从术停止. ″\∩囱…赋’ "己uthor5": [ { ·∩a爬m8 "中圆所闻冈!!’

闻1j∩代阅: 厕httP5;//十e∩g.i于e∩g。c咖/a0t∩oI/3089O4闪 } 广

尸「□『‖=■厂







如上返回内容以JSON格式呈现,包含文章的标题、正文、发布时间等内容°可见’不需要配置 任何提取规则,我们就完成了页面的分析和爬取°

‖Ⅲ》‖~尸 β

|■尸|‖



|匹尸||●「[尸Ⅱ『[炉‖|_■厉卜‖|



另外’Di∏恤t提供了几乎所有编程语言的SDK支持(详见h呻:〃wwwdj∏赋comdeWdmS川imn镭/), 因此我们也可以使用SDK实现上述功能。如果使用的是Python语言,那么可以直接使用Python的 SDK--Dj问botCljent’链接为https;//ghhuhcom/diffboUdi1Tbotˉpythonˉclient°这个库并没有发布到 PyPi’需要自己下载并导人使用°此外,这个库是使用Python2编写的’本质上就是调用了requests库, 大家感兴趣的话可以看_下° 下面是一个调用示例: fro"c1ie∩tj呻ortDi仟botαie∩t’0j仟bot〔mN1

di仟bot=Dj仟bot〔1ie∩t() to代e∩≡ |yo0I-to代e∩| ur1= 0‖ttp8//5M〔hua∩。git∩‖b.io/jaγa5〔rjptˉp日tter∩5/0 ap1= 0artjC1e0 re5po∏5e=di仟bot.Ieque5t(ur1’ toke∩’ api)

运行这段代码’就可以调用ArticleAPI来分析我们想要的URL链接了’返回结果跟前面的结果 类似° 5.总结

‖【●‖‖|}◆|□●「‖|卜|●厂卜β‖■■「‖|·△尸『》[‖■■■■『『‖′[广

△■「‖[■「|‖「■厂巴■厂||■ˉ

| 0

本节介绍了智能解析的原理和Dj贤bot的用法。通过DifTbot的案例’我们大体了解了智能解析算 法可以提取什么信息以及提取正确率如何。但DifIbot总归是一个商业化的API’我们不能只知其然, 不知其所以然。虽然很多时候只能靠调用商用API的方式智能解析页面,但_方面是费用高昂,另一 方面是如果出了问题,没办法做针对性的处理和优化’我们显得非常被动。 如果我们能了解智能解析算法的核心原理和实现’很多问题就迎刃而解了°

之后几节我们会针对资讯类网站’介绍智能解析算法的-些原理和实现流程。对于大部分资讯类 网站来说,除去一些特殊的页面(如登录页面、注册页面等),剩下的页面可以分为两大类—列表 页和详情页,前者提供多个详情页的索引导航信息,后者则包含具体的内容。我们会针对这两类页面 介绍如下知识点。

□详情页中文章标题、正文、发布时间的提取算法和实现。 □列表页中链接列表的提取算法和实现。 □如何判断一个页面是详情页还是列表页°

↑42详情页智能解析算法简介 本节中我们来了解_下详情页提取算法的基本思路, 主要包括如下内容° □我们定义的详情页是指怎样的页面。

□详情页中的哪些信息是需要我们提取的关键信息。



↑4 k

}卜 「〗

‖‖||

708

第l4章页面智能解析

↑.怎样的页面属于详情页

先划定_个大的范围°我们处理的网站属于资讯类网站’如新闻网站、博客网站等°这类网站通常 包含两种页面:_种是包含导航信息的列表页’如新浪新闻的首页;另_种是从列表页点击导航信息后 进人的页面’如-篇新闻的页面。例如,新浪新闻的首页如图l4ˉ7所示,我们称这类页面为列表页°

″(□【■■■Ⅲ■■□·】〗‖■■〗□□·□·】□‖□」Ⅶ‖

□介绍标题、正文、发布时间的提取算法°

一———_





=■………

甲≡宁=~==

田=■≡户■早=F=■=ˉ≡审



亡内●! —

沥淑众】

唾目〕》

创p记

P→

T

马用Ⅲ▲巴■

囊嚼●:



● ·P曳些~■m ·■■由…w■·蚀下≈==



库克租苹果矗好的十年 库克利苹粱矗好的十年

■记

∩……■℃∩■宁

0↑快迅■佃兰保■、三Ⅸ快迅公叼狂双■0月0日巴■顽上m.↑元 0↑快递员权拄保■`三家快迅公■法戚例U9月0日起■∏上m.‖元

02=外墨∏强■粗闪后砍伤订■a■丁么数欲■方回应巳刑拘 03■评‖●翻■佃“佣7■违违人m、H离法雄业囱囚红油困 “翱培诵靴■,这种炮诅卸突田o‘妇■副



鹰■■亡w论



■峦汽本



诬任■■…画□人冲■w丁铂

=下

仅早乃亿巾侄■凸■爪丘g汪妇■ 乙乙些,P●■≡宁

Ⅷ长闺■向赋兴… m冈入■万0…Ⅲ|,【Ⅷ′ 陛下





-个■…m …■球

鹏东京贝违村-粥牢透劫田r臼劫■p汽牢钮伤,已■■扫■治行





门也不■■心…曰■向"了‖阂卒 nb→■≈■

图l4ˉ7列表页的示例

凤凰网的一篇新闻的页面如图l4ˉ8所示, 页面如图l4ˉ8所示’我们称这类页面为详情页°

2m0绚2月豁臼0z:26m

幂……阔

Ⅱ93s人鳞. 2…Ⅷ陋m晒

“我的名字叫紫禁城’快要600岁了,这上元的夜酮’总是让我沉醉’这么久了却从未 停止°口

哟■榆之上的月光’ 曾照进古人的宫殿j城墙上绵延的灯彩0映出了角楼的瑰丽°今



-

图l4ˉ8详情页的示例







F■=





|日

=~■



‖ |

夜’-群博物馆人将我点亮,我在北京的中央’献给团圆的你们,一座壮观的城°脚

·

‖‖√

故宫’你低调点!故宫:不’实力已不允许我继续低调

二■可】■】‖』』■■〗」■∏

匪蕊琵

05托ⅥD认收的AR凶∏丑■力醒:符陌拉`亚马迅三■疆透反时



闪■

■伊■■■咽七夕渔『其

唾贷二■…■Ⅲ

=伞宁

‘』■■』■■〗』■■]‘

Ⅷ■

m灵

■叫■不上拐D泌…凶早 ●≈● ●=●

主…三n电H闪让

■盅后厂村0吟屈

仍H=■一值



l42详情页智能解析算法简介

709

可以看到,这两类页面有很大不同’列表页通常包含许多详情页的标题和链接’而不会呈现具体 的内容,布局样式也是千变万化’而且可能分多个区块°详情页则是某个内容的展示页面’通常包含 醒目的标题、发布时间和占据版面最大的正文部分°另外’详情页的侧栏通常会有—些关联或推荐的 内容列表、页面头部的导航链接`评论区`广告区等°

凸|=◆「|卜



到现在’相信大家对分辨列表页和详情页有了基本的概念。 2提取内容

这里主要了解一下我们要在详情页中提取的内容°

■■■■Ⅲ■叼■

尸■■厂↓|▲『|‖炉「‖‖●■『■『广『β『‖》『

-般来说,详情页包含的信息非常多,例如标题`发布时间、发布来源、作者、正文`封面图、 评论数目、评论内容等,不过由于其中_些内容并不常用’而且提取算法大同小异’因此本节主要介 绍3个关键信息的提取算法_—标题、正文`发布时间。 还是以l4.l节开始的新闻页为例’我们需要提取图l4ˉ9中框选出来的内容° ■Ⅲ齿■m■出n拽盛乐体∏冈■灾年臼尸旧扳■书文化历史甲●m

凰凰纲酗■讯≥裙≥正文



|故宫,你低谰点!故宫 不,实力已不jb许…低调|

「 ■「■‖『{β厨

「雨雨雨盂;;亏壶ˉ}

Ⅸ,as^碑…v瞬网闷陶

狠■收钮腰闽闪‖■■

u我的名宇凹篮俄城,快耍蛔岁了‘这上元的夜田°总是让我沉辟"遮么久了却从禾 卜‖·『■尸■巴尸『■尸‖■「

■止·净 ■■橡之上的月光·■照进古人的∏殿;瞻出上烛旦的灯影‘欣出了妇银的■田·今

授‖=辟呻记人将我点宛粉守北京的中央′酞辑团四的仰饥斡■壮混的城· 闪

广

尸卜白



〃吁孽雨雨毒b` 耐` O

4{′

…烫●◆

垮‘′ =



′′.

幽 .



■■‖匹尸「‖仪尸■血■■■■■「『

′ 〃 "

■憋◆

_毯÷_

\平

故■蹿院供田

今年元■节!故宫迎来了迁胰鳃年以来的茁次罕灯会簿0w会赋开始前’故宫闯们踩在

图l4ˉ9详情页中需要提取的内容



下面来分析这三部分内容的特点。

■■

●「|卜二■|尸‖止■■『户■片■|

□标题:上方矩形框选的内容’是页面的主标题,通常字号比较大’比较醒目,能概括本页面的 内容°

□正文:下方矩形框选的内容’是页面的核心’这里由于篇幅所限因此未在图l4ˉ9中将正文 内容展现完整。

□发布时间:中间矩形框选的内容,通常在页面标题的下方或者正文的下方,格式大多为常见的 时间格式,代表本页面内容的发布时间°







第14章页面智能解析 =

后面会分别介绍这三部分内容的提取思路° 3.准备工作

现在很多网页是由JavaScnpt喧染而成的,导致通过请求获取的页面源代码不-定是我们在测览 器看到的页面源代码’这里要求我们提取的必须是谊染完整的HTML代码。

■■‖』‖|‖{』■●|||」』■■‖‖‖■司』■■■‖‖|』■』·、{|‖』□』■】‖』‖』■|■

7l0

首先把示例详情页的HTML代码保存下来。在测览器中打开这个页面,打开开发者工具并切换到 E‖ements选项卡,如图l4ˉl0所示°

==…二羔』 ■Ⅲ良讯四Ⅲ■吨篇爪质淬∏■出门卒■尸崭伍浊书x此□定■■0■■!∩

{{叫

凰凰绸…m谚蛔>… 故宫’你低泪点!故宫:不’实力已不允密词

q

…B酌臼…

峨鹏^.慰…郴固mm

m3中…■■

■■■‖

闯钱的名割囊禁氰,快妄蛔岁了’这上元的夜■′总是让我沉博‖这么久了却从朱 俘止· □



惧■啦亥h的月光0●照进古人的■肚nn卜■区的灯聪,焕出了龟极的现a今

夜’≡群呻钮人将我赢兜,我窿北融中央′幽因四的像们,≡醚囊吨.w 夕

旧幻西m

Cα……〃…

0…悦

(日

| Ⅷ田

陆≈斌■■鞠画v…S…mv四■mm

■0…宛…≥

′●l=↑台】

p<止↑=皿■·O…≈∏=9…”1■Tk…~…= ◆<c叼■厂幽…t….■tγ妒…肌…【…mQ〗…1甲】■…8…;…向1≈0h…』碱≈呼乙U少 ◆过Aγ1“●而●饯

0》 ∩■【《

』门

≥◆…·广

』 血罕[n {』

●酌jⅪ山t+~仑P……′d怔 <′0』m



■江mmfy…℃■β c…9●F甲皿≥=■句…∩Q■【m旧≥

▲■可·■

<■怎…tt…嚼箍,…1o·w…啦t…〃F凸ˉ1 c「□S■O了10j洒≈■…D″由7位回Dt7』0tD <匹7mt tV……u■■冗冉■h试■■∑〃乙.1 c回0●F幻…=·…户■′gC「2… P≤呸了lp@=′庄f酗> …7士p怯 ~■

ˉ

. ==~

-~≈…只…、←…~←←……■

………咖

O

图l4ˉ10找到页面的HTML代码

所示。 0

民田…宙m

cα…包蟹…

p蝴…贞

釉如w■m山画γ…m…口叮啤世■四

Gl吨『v谣优t·`扛





;|0>=c/M…≥

th: 0pⅨ;恤』帧t8印x『 OV·「↑l山8 『l』dge∏;回沪=≤/■叼≥





■■‖』■可日□□■】■■■

然后右击∩t朋1节点,在弹出菜单中选择Copy→Copyelement’复制整个页面的源代码,如图1牛ll





巳m吨匝…″

且′·



c刨呻蹿口h!由B∩



跑m…γ沁Ⅶ



<■c7如℃

…U8



阿……曲

创■

1丘睁

蕊铲



≤Sc厂加Yt擂库…l·醉9r c■‘

【厂四■O「』0』价叮4翰…■内 h<“「如b≡々△cr绝t≥

∩山…咖t





≤c7』βtt…臃…l●■9

国“

√0』卜

【r“■O7』仪灾鸭γ■晌…■户 ≤●c「帅tt痞酗…`·固3「田 〔厂“仍O『l01∩叮°E…Z尸■

合◆=~■■■{『厂L■0 ■=■=仕【Ⅲ



复制页面源代码



图l4ˉll





接着新建一个HTML文件,命名为detai|html,并格式化源代码, l43节会用到这个文件°







’■【尸【〗‖■■■【■β匹尸▲尸【‖‖‖卜■「《』■』■「‖〖【尸【↓‖■『~■『『『》『『‖‖【「‖|尸【‖‖∩『■

l42详情页智能解析算法简介

7ll

4.提取标题

一般来说’标题是相对比较好提取的,根据几个关键信息就能完成绝大多数标题的提取°详情页 的标题—般包含在tjt1e节点中’.例如: 〈tjt1e〉故宫,你仗调点!故宫:不,实力已不允许我继续低闪凤凰冈资讯凤且冈〈/tit1e〉

此时如果直接进行提取,那么得到的标题内容如下: 故宫,你低词点!故宫:不,实力已不允许我蛙续低调凤且用资讯凤瓜冈

但真实的标题的内容应该为: 故宫’你低闪点!故宫:不’实力已不允许我继续低词

所以~味提取t1t1e节点内的内容并不准确’因为网站会额外增加-些信息,例如这里网站本 身的信息°此时怎么办呢?在绝大部分情况下’标题是由∩节点表示的,_般为∩1` h2` ‖3、∩4等’ 其内部的文本就是完整的标题’那么问题又来了’‖t"1里面有那么多∩节点,怎么确定哪个是标题对 应的h节点呢?



答案你应该也想到了’把tit1e节点和∩节点的内容结合起来不就好了吗?于是可以初步总结出 下面两步提取思路°

「■

尸 ‖

(1)提取页面的∩节点(如∩1、∩2等)’将其内容和tjt1e节点的文本做比对’和后者相似度最高

矽 【 ‖

的内容很可能就是详情页的标题°

【 ■ 「 「

(2)如果未在页面中找到h节点’则只能使用t1t1e节点的文本作为结果°



巴尸|■厂匹尸卜『■『[巴旧「■■『|■

_般来说,使用以上方法可以应对90%以上的标题提取问题°另外,有些网站为了使SEO效果

比较好’会添加一些‖et日标签如ur1、t1t1e` |(eyword5、 category等,这些信息也可以成为参考依 据,用它们进_步校验或补充网站的基本信息° 在上面的例子中’就有一个们eta节点: 〈爬tapropertγ=碱o8:t1t1e"〔o∩te∩t=m故宫,你低闪点|故宫:不,实力已不允许我继续低词"〉

可以看到其中指定了property为og:t1t1e,这是—种常见写法,其内容正好是标题信息’于是我

b

P

b

们能通过它提取标题°

综合以上内容’借助t1t1e节点、h节点和Ⅷeta节点,我们已经可以应对绝大多数的标题提取了°





|`

|D



) p













式字符’往往也需要归类到正文内容中。

受开源项目GeneralNewsExhactor和论文《基于文本及符号密度的网页正文提取方法》(以下简称 论文)的启发’我得到了一个比较有效的正文文本提取依据指标—文本密度°



p



文末广告等’这些都属于噪声。

□正文内容所在的p节点中会夹杂5ty1e` 5〔ript等节点,这些并非正文内容。 □正文内容所在的p节点内可能包含Code、5pa∩等节点’这些内容大部分属于正文中的特殊样





□正文内容所在的p节点中也不—定全是正文内容,可能掺杂噪声’如网站的版权信息、发布人、





等节点内°



.0

□正文内容通常被包含在body节点的p节点中’而且p节点_般不会独立存在’而是存在于d1γ

=ˉ■□·□



观察正文内容的特征’能够发现_些规律。 |



正文可以说是详情页中最难提取且最为重要的部分’如果不能有效地把正文内容提取出来’那么 这次解析相当于失败了-大半。





5.提取正文

712

第l4章页面智能解析

文本密度是什么呢?简单理解就是单位标签内包含的文字个数°例如一个p节点内包含l00个字,

那么可以简单地计算出文本密度为l00;如果包含5个字,那么文本密度为5°一般来说,正文区域

以_个p节点为—个段落,而_个段落包含的字通常比较多’文本密度可能上百;对于其他区域’例 如页面导航区域’通常会包含多个a节点,这些链接可能总共也就十几二十个字’因此文本密度要低 很多°综上,文本密度可以作为判断正文内容的重要参考指标。

当然,论文本身不仅局限于纯文本和节点的大小比例,还考虑到了文本中包含的超链接°论文中 定义,如果j为HTMLDOM树中的_个节点’那么该节点的文本密度为:

TD= n-Ln ′

TGj-LTGj

||

□TD′ :节点j的文本密度。 □Ⅲ:节点j中字符串的字数。 □LT}:节点j中带链接的字符串的字数° □TG′ :节点j中标签的数量。 □LTG′ :节点j中带链接的标签的数量。



二·■】□Ⅵ□■

如下为其中各个符号的含义。



以上各项需要好好理解一下,其实文本密度基本上等同于单位标签内包含的文字个数’这里额外 考虑了节点内包含超链接的情况°因为_般而言,正文区域带超链接的情况是比较少的,而侧边栏、 页面导航等区域,带超链接的概率非常高,所以这些地方的文本密度就会低下来’上面那么算能够更 好地排除这些内容的干扰°

内容。

论文中对于符号密度的定义如下:



另外’论文中还提到了一个指标,叫作符号密度°研究发现,正文中_般会带标点符号,而网页 链接、广告信息由于文字比较少,通常是不包含标点符号的,因此我们还可以借助符号密度排除一些

6bD′≡盖罢 如下为其中各个符号的含义。

□SbD》:节点j的符号密度° □T}:节点j中字符串的字数° □LⅢ:节点j中带链接的字符串的字数° □Sb′ :节点j中符号的数量(分母另外加1是为了确保除数不为0)° 可以看出’符号密度为文字数量和符号数量的比值。

论文的作者经过多次实验’发现利用文本密度和符号密度相结合的方式提取正文信息能取得很不 错的效果’可以结合二者为每个节点分别计算_个分数,分数最高的节点就为正文内容所在的节点。 分数计算公式如下:

□Sco赡『 :节点j的分数。

‖』||

如下为其中各个符号的含义°

二■司{

Sco酝′=lnSD×TD′×‖g(PNum『+2)×lnSbD′





■【■

■■『【■【‖■『β■厂【『『‖【■■‖〗↓〖卜伊|‖■■『|『占■「‖‖|匹「●『||∩〖∩『|〗′尸||β′尸‖}卜尸|‖●『■匹■『|‖■■「》■尸|》∩卜|[■「



■∏儿■「■矽‖『■■「『匹尸「{‖止尸『∩巴■「|‖■「

▲■尸‖‖【■匹尸「}‖■乙■|『=■□『仪‖■尸

} ■尸止尸似【伊■■■『匹∩『■【■『血■尸『「■厂『■■厂|=■| ■■尸}|■伊·『



l42详情页智能解析算法简介

713

□SD:所有节点的文本密度标准差°

□TD旷 :节点j的文本密度°

□PNum》:节点j包含的p节点的数量。 □SbD′ :节点j的符号密度。

遍历各个节点,利用该公式为每个节点计算分数’然后根据最终得分确定正文节点,提取正文内 容°通过对比实验数据’可知—些中文新闻网站的正文内容提取正确率能达到90%以上’甚至部分可 以达到99%°另外,论文作者还在不同网站上对该算法进行了评测,计算出了P(Precision)`R(Recall)` Sco【e(F1ˉScore)值,结果如表1牛1所示°

表↑4ˉ‖在不同网站对算法做的评测 网



P

R

Sco「e

cleanEva‖ˉEng

93·88%

77.43%

73』l%

cleanEvalˉZh

81·62%

69·]8%

62』6%

凤凰网新闻

97·51%

98』8%

95.76%

参考信息

98·80%

99·88%

98.68%

可以看到’该算法在凤凰网新闻上的正确率可达95%以上° 我们已经可以借助以上算法得到不错的正文内容提取效果了,满足一般需求可以说没问题°如果

想追求更高的正确率,还可以结合视觉信息°因为在多数情况下’正文所占的版面是最大的’所以可 以通过计算节点所占区域的大小来排除-些干扰’例如找到了两块内容都疑似正文’而它们所占的网 页面积-个很大,-个很小’那么面积大的是正文内容的可能性会更大。

6提取发布时间 对于发布时间,也有_些线索可以利用°

和标题类似’_些正规的网站为了使SEO效果比较好’会把时间信息放到"eta节点内’例如我 们的例子中就有这样一个Ⅶeta节点: 〈眠t己∩a‖∏e="og:tj爬" co∩te∩t="2o19ˉO2ˉ2002826:O0"〉

这个"eta节点指定了∩aⅦe为og:tj∏|e’这是_种常见写法’其内容正好就是发布时间,我们可 以通过这部分信息提取发布时间°注意’不同网站的Ⅷeta节点中∩a‖e属性的值大概率不一样,根据 经验和调研’我得到了—些写法,如: 〈贬t己property≡"r∩ews;datep0b1j5∩ed00 〔o∩te∩t="2O19ˉo2ˉ2O02826:00"〉 〈爬tajte‖∏prop="datepub1j5hed问 〔o∩te∩t="2o19ˉ02ˉ∑0o2:26:oo00〉 <爬ta∩a爬≡"pub1ic己tio∏dateαco∩te∩t=闪2019-02ˉ20o2;26:""〉 o∏eta∩3爬="pub1j5h0ate0『〔o∩te∩t=。2o19ˉo2ˉ2oo2:26:"α〉

可以看到,不同网站的写法差异还是蛮大的’我们可以总结常见的写法,-旦匹配成功,那么其 CO∩te∩t属性值极有可能就是发布时间°

但是’并不是所有网站都会加上这样的Ⅷeta节点’如果碰到没有『∏eta节点的网站’该怎么办呢?

我们知道’时间有一些固定的写法’如2019ˉ02ˉ2002:26:00。而且发布时间通常会包含_些关键 的字符’例如“发布”“发表于,,等’它们可以作为重要的参考依据°因此,-些固定的匹配模式往 往也能起到不错的效果。例如,定义-些正则表达式,或者基于某种特定的模式来提取时间信息°

这时可能有人会说,如果正文内容本身包含时间,或者侧栏、底栏部分包含时间,不会提取错吗?

卜『』『卜=■【『‖‖阳}■『↓■‖巴尸【尸



第14章页面智能解析

□对于正文内容本身包含时间的情况,根据提取的正文结果过滤即可,例如直接将正文从提取目 标中删除。

□对于侧栏或底栏部分包含时间的情况’可以根据节点距离算得结果°发布时间往往和正文距离 较近,甚至紧贴着,而侧栏或底栏的时间常常分布在其他区块,其日期节点和正文节点的距离 相对较远,这样就能找到权重最高的时间节点了° 综上所述’发布时间的提取标准如下。

□根据爬ta节点的信息提取时间,提取结果大概率就是真实的发布时间,可信度较高° □根据正则表达式提取时间,如果匹配到_些置信度比较高的规则,那么可以直接提取;如果匹 配到置信度不高的规则或者提取到多个时间信息,则可以进行下_步的提取和筛选。 □针对上面的第二种情况’通过计算节点和正文的距离’再结合其他相关信息筛选出最优节点作 为结果。

按照以上标准,可以提取出绝大部分的发布时间° 7.总结

本节中我们介绍了详情页的3个关键信息—_标题、正文、发布时间的提取思路,了解了基本原 理之后,我们在l43节会用代码实现其中的-些解析算法。

↑43详情页智能解析算法的实现 本节中我们来动手实现详情页的提取算法°

↑.本节目标 还是以14l节开始时的页面为例,用算法提取其标题、正文和发布时间°

注意由于部分算法比较复杂’因此本节介绍的耳法是简化后的版本’叉多细节处理可以参考本节 最后的说明°

2准备工作

在142节’我们已经将案例页面的HTML代码保存成了文本文件dctailhtml。这里我们主要会用

XPath解析页面和操作节点’所以需要用到lxml库,如果尚未安装该库’可以参考https:〃setupscmpe. centeI/lxml里面的说明°

定义如下代码,将HTML代码里面的字符转化成lxml里面的‖tⅦ1〔1e爬∩t对象: +r咖1x们1·ht盯1j∩port"tⅧ1[1e爬∩t’「r咖5trj∩g ht‖1=ope∩(‖detaj1.∩t川1‖’ e∩〔odi∏g≡|ut「ˉ8!)。re己d() e1e眠∩t≡+r咖5tri∩g(htⅦ1=ht‖1)

这里的e1e们e∩t其实就是整个网页对应的‖t"1[1e‖e∩t对象,它的根节点就是ht"1’我们在解析 页面的时候会用到它,从中可以提取我们想要的标题`正文和发布时间° 3.提取标题

首先来实现标题的提取,根据l42节的内容,提取分为3个步骤。

(1)查找们eta节点里的标题信息’如果能查到’那结果通常是非常准确的,直接返回即可。 (2)查找t1t1e节点里的标题信息’由于tjt1e中通常会包含冗余信息’因此需要将查找结果和‖ 节点中的内容做比对,以便得到更准确的结果.

`■`卜』勺]‖‖|●】·□‖』可·】‖{■■||]■』■〗‖』可■】‖|当■、‖(■‖』』·』曰|··‖]√到■■』■‖』〗|」■]|‖|□]』日|●‖」』■〗』】|』■|‖‖』■■Ⅵ□]‖‖《』』■](‖□]司|」·|」‖|■■■|』■‖勺Ⅵ』|岂

714



l43详情页智能解析算法的实现

7l5

(3)如果上述两个步骤都不能得到有效结果,则可以直接用t1t1e节点中的内容作为结果(保底)° 当然,此逻辑还存在很多可以优化的地方,但应该能够应对大多数详情页的标题提取任务°接下 来就用代码实现_下这个逻辑吧°

首先定义利用XPath从"eta节点中提取标题的规则: "[丁A5= [

|//爬ta[5tart5-Njtb(0pIoperty’"og:tit1e")]/0co∩te∩t‖’ ,//眶ta[5tart5ˉNit∩(@∩己爬’ □og;tjt1e")]/伙o∩te∩t|’ |//爬ta[5t己rt5ˉ"jt‖(印rOPeⅢty’"tjt1e阑)]/αO∩te∩t,’ ,//贬t己[5tart5ˉ门jt∩(αa贬’ 。tit1e闰)]/伙O∩te∩t『’ 0//爬ta[5tart5ˉwith〈0Property’αpa8e:tit1e僻)]/0co∩te∩t|

这里我们定义了一系列XPath,用于匹配爬ta节点并提取co∩te∩t属性的值。然后我们实现一个

extra〔t=by」爬ta方法: de十extraCtˉbyˉ眶t己(e1e爬∩t: ‖t朋1[1e∏mt)ˉ〉Str: 千orxpat∩i∩‖[丁A58 tit1e=e1e眶∩t.xp己t∩(×田t∩) i千tit1e: 队‖[「|甘「尸》‖|‖巴尸「卜匹∩|尸厂’【尸|■■厂■|快|↓卜炉『□卜)低■}|卜尸∩【厂仁●『|卜□β「|■广|■■‖Ⅲ●「卜|△■尸

retUr∩ 』』 .joi∩(tit1e)

这里遍历了‖[『A5的内容,然后依次进行匹配,如果能够匹配到结果,就直接返回°这里可以尽

量把更常见和更精准的XPath放到∩[『A5的前面’同时避免填写-些置信度较低的XPath’以便提取

出更准确的内容°

接下来’对于tit1e节点’就是直接提取其纯文本内容;对于∩节点,则是提取‖1节点、h2节

点和∩3节点的内容,通过基本的XPath表达式就可以实现°这部分的代码实现如下: de十extra〔tˉbyˉtjt1e(e1e眶∩t8 ‖t‖1[1e‖mt)8 retur∩` 』 .joi∩(e1e∩mt.xp己th(,//tjt1e//text()|)).5tⅢip() de十eXtr■Ctˉby-‖(e1e眠∩t:毗"1[1e眠∩t): ‖5=e1即记∩t.xp3t∩(|//∩1//text()|//h2//teXt()|//h3//text(),) retur∩∩sor []

这里我们提取了tjt1e节点、h1节点、h2节点和h3节点的信息,然后返回了它们的纯文本内容’

其中eXtraCtˉbyˉtit1e方法返回的是字符串类型的内容’extra〔t-by-h方法返回的是包含h节点中所 有纯文本内容的列表。

下面我们依次调用3个方法,看看针对这个案例’结果是怎样的:



tjt1eextm〔ted-byˉ赡ta-extra〔t-by-贮ta(e1e∏mt〉



tjt1eextmCtedˉbyˉtit1e=extra〔tˉbyˉtjt1e(e1e‖记∩t)

运行结果如下: tjt1eeXtm〔ted一by=赡ta故官,你低切点!故官:不’实力已不允许我继续低词

tjt1eeXtm〔ted-bγˉ∩[0仗官,你低切点!故宫:不’实力已不允许我继续低词|’0为您相耳,’珊品有户0 ’ ‖好书柑迎|] 二

可以观察到, 3个方法返回的结果差不多’都包含真实的标题信息,另外后两个结果中有-些不 太一样的内容°如我们所料’tjt1eextraCtedˉbyˉ』‖eta是完全正确的标题° 假设不存在和爬ta节点相匹配的结果’如何依靠tit1eextraCtedˉby≡tit1e和tjt1eextraCted



『 》『卜



by=‖得到真实的标题呢?可以观察到, tjt1eextraCted-byˉtit1e相对真正的标题多了网站名称, tit1eextraCtedˉbyˉh是h节点组成的列表’其中有-个是真正的标题°有了这两部分信息’只需要 求得和tit1eextra〔tedˉbyˉt1t1e最相似的∩节点的内容就可以了°



‖】‖

7l6

第l4章页面智能解析



可以采取的解决方案有很多’例如直接使用最基本的相似度算法_JaccaId算法’即用两个字符 串的交集字符数量除以两个字符串的并集字符数量°代码实现如下: de千5mi1aⅢjty(51’ S2〉: j十∩Ot51Or∩Ot52: Ietur∩0

515et=5et(1j5t(S1)) 525et=5et〈1j5t(S2)) i∩ter5e〔tiO∩=515et.1∩terSe〔tiO∩(s25et) u∩iO∩=525et。i∩ter5eCtiO∩(52=Set) retum1e∩(j∩tersectio")/1e∩(u∩io∩)



这里我们定义了一个5jⅧj1ar1ty方法,它接收两个字符串: 51和52。首先该方法将51和52的

‖‖‖

字符拆分为集合’然后求出两个集合的交集和并集’最后返回交集字符数量和并集字符数量的比值° 我们来验证-下这个结果,如果51和52完全相同,那么返回的结果就是1;如果S1和52毫不相干, 那么返回的结果就是O。这个算法并没有考虑字符的数量和重复度,因此存在一定的局限性’但用来 求解-般情况下的相似度已经足够了,而且计算速度非常快°

接下来只需要遍历tjt1eextmCted一byˉ∩的每个元素,然后找出和tjt1eeXtmCtedˉby-tjt1e相 似度最高的那个’就是真正的标题了°如果遍历完依然没有结果,就用tit1eextra〔ted-byˉtit1e作 为最终结果。



综上,可以把提取标题的过程定义成_个方法eXtraCtt1t1e:

〈』

tjt1eextm〔tedˉbyˉ眠t3≡extra〔tˉbyˉ爬ta(e1e爬∩t〉 tit1eeXtr己Cted-by-h=eXtra〔t≡by-h(e1e爬∩t) tjt1eextmCted-byˉtjt1e≡eXtra〔t_byˉtjt1e(e1e∏记∩t)

‖二□

de十extm〔tt1t1e(e1e爬∩t: ‖tⅦ1[1e「∏e∩t):

j+tjt1eextmCted-by-「∏et己: IetuI∩tjt1eextIaCted-by-∏论ta

■‖‖

tjt1eext】己〔ted=by-∩≡5orted(tjt1eextm〔ted-by-h’ 代ey≡1日朋bda×; 5加j1arity(x」 tit1eextmCted-by-tit1e)’ reγer5e≡丁rue) i十t1t1ee×traCted-by-h: retuI∩tjt1eeXtra〔ted-by-h[0] retl』r∩tit1eeXtm〔ted-by—tit1e

4提取正文



终于轮到重头戏——提取正文了,我们一起来实现l42节介绍的文本密度和符号密度的计算吧° ‖

5ty1e这些内容不仅一定不会包含正文,还会严重影响文本密度的计算’所以有必要先定义-个预处 理方法:

』‖●‖■』

首先需要做-些预处理工作。∩t"1节点内通常有很多噪声’非常影响正文内容的提取’ 5〔r1pt、

+ro川1x"1·ht们1加port‖m1[1e爬∩t’ etIee ■·■‖

〔删「[‖丁05[[[55『M5=[`∏赔ta‖’ 05ty1e‖ ’ 05〔rjpt‖’ 01j∩促‖’ ’γjdeo‖ ’ !aud1o』’ ‖j+m′ne0 ’ |5our〔e,」 ,5vg0′ ‖path‖ ’ 5Ⅶbo10 ’ |mg0 ’ ‖+ooteI‖’ |header,] 〔删丁[‖丁5『RIp丁奶5≡ [|5pa∩0 ’ 0b1ocRquote0] 〔删丁[‖丁‖0I5[XpA丁‖5= [



‖//diγ[〔o∩t己1∩5(0c1a55’||come∏t")]! ’ ‖//d1v[〔o∩taj∩5(0c1a55’ ‖‖adγerti5e"e∩t")]‖’ 0//djγ[co∩taj∩5(0〔1a55’ "adγert")]‖’ |//d1γ[〔o∩taj∩5(@5ty1e’"dj5p1ay: ∩o∩e』』)] ‖ ’





#耐除标签和内容

etree。5tr1pˉe1e‖恰∩t5(e1e∏记∩t’ *〔删丁[‖丁05[L[55丁M5)

‖‖‖(

de十prepro〔e55q〔o∩te∩t(e1e爬∩t: ‖t"1[1e贮∩t);





|}

l43详情页智能解析算法的实现

7l7

#只闭除标签对

etree.5trjpˉtag5(e1e∏论∩t′*〔酬「["丁5丁RIp『M5) #用除噪户标签

re∏℃ve〔∩i1dre∩(e1e爬∩t’〔侧丁[‖丁肋I5[Xp∧丁‖5) 十or〔M1dj∩C∩j1dre∩〈e1e"论∩t):

#把5p己∩和stro∩g标签豆面的丈本合并到义纽P标签卫凸 j于〔hi1d.tag.1o眶r()≡·p,: etree。5trjp≡ta85(Chi1d』‘5p己∩’) etree。Btripˉtag5(Ch11d’ 05trO∩g|) i十∩ot(chi1d。texta∩d〔hj1d。te×t。stmp())8 remγee1e贬∩t(〔M1d)

#七果djγ标签卫没有任何于节点,就把它特换为p标签 j+chi1d.tag.1o脂Ⅲ()=,djγ| a∩d∩ot〔hi1d·8etc‖i1dre∩(): 〔∩j1d·tag= 0p|

这里我们定义了_些规贝‖,〔0‖『[‖丁05[儿[55『∧65代表_些噪声节点,直接调用5trjpˉe1eT把∩t5方 法把这些节点及其内容删除即可°〔删丁[‖『5「RIp丁∧65中节点的文本内容是需要保留的,但是标签可 以删掉°〔O‖∏[‖「ˉ‖0I5[—XpA丁‖5代表_些很明显不是正文的节点’如评论、广告等,盲接删除就好。 其中还调用了几个工具方法,这些方法的定义如下: de于Ie们ovee1e爬∩t〈e1即e∩t: ‖m1[1…∩t): p日re∩t=e1e眶∩t。getp日Ie∩t() i+pare∏tiS∩Ot‖o∩e: paIe∩t.re硒γe(e1e爬∩t)

de十r…γe〔M1dⅢe∩(e1e∏记∩t: ‖m1[1e贬∩t’ xpaths=№∩e); j千∩otxP己t∩5; retur∩

「or×p己t∩i∩xpath5: ∩ode5二e1e∏论∩t.xpath(xpat∩) 千or∩odei∩∩ode58

re∏℃vee1e∩mt(∩ode) retume1…∩t

『‖『‖【『[■「|■【厂‖日【■【「‖】巳■【】‖■■「|》卜■■『|●■尸『‖■「‖|‖■∏『}‖|■■「||■【「||『[■『‖[【■【「‖}■■■『

de「〔M1dre∩(e1…∩t: ‖t‖1[1e眶∩t): yje1de1e爬∩t +or〔hi1de1e爬∩ti∩e1…∩t目

j+i臼i∩5ta∩Ce(Chi1de1e赡∩t’肚Ⅶ1[1e眶∩t)8

y1e1d「ro∏‖cM1dre∩(cM1de1e‖陀∩t)

这里还对一些节点做了特殊处理°例如对p节点内部的5Pa∩节点和5tro∩g节点,去掉其标签’ 只保留内容°对于没有子节点的djv节点’则将其换成p节点。当然’如果大家再想到什么细节,可 以继续优化°

预处理完毕之后’整个e1e∏)e『]t因为没有了噪声和干扰数据,变得比较规整了。下-步,我们来 实现文本密度、符号密度和最终分数的计算。

为了方便处理’我会把节点定义成-个Python对象,名字叫作[1e爬∩t,它包含很多字段,代表 某个节点的信息,例如文本密度、符号密度等。[1e『‖e∩t的定义如下: C1aS5[1e赡∩t(砒"1[1e眶∩t〉; jd; i∩t=‖o∩e

.

ta8-∩a眶: 5tr■‖o∩e ∩唾rO千Char: i∩t=‖o∩e

∩呻er二O〔aˉC∩ar: j"t=‖o∩e ∩咖ero千de5〔e∩d日∩t5: i∩t=‖o∩e

二 =ˉ ∩mer-o「-pˉde5〔e∩da∩t58 j∩t■‖o∩e ∩呻er-o+-pu∩〔t‖atjo∩; i∩t=‖O∩e de∩5ity=o「一pl』∩Ctuat1o∩: j∩t=‖o∩e



一■可

第l4章页面智能解析 de∩5jtyˉoLtext: 十1oat=‖o∩e de∩5jty-5〔ore:「1o己t="o∩e

以下为其中包含的字段的简析° □id:节点的唯_jd。

■■·■·可■·□可」‖□』■司□】可‖

7l8

□tag-∩aⅦe:节点的标签值,例如p、d1`′、 i"g等° □∩uⅧberofc∩ar:节点的总字符数° □∩u‖bero十ac∩ar:节点内带超链接的字符数° □∩uⅧbero+de5〔e∩da"t5:节点的子孙节点数。

□∩u"bero十ade5〔e∩da∩t5:节点内带链接的节点数,即己的子孙节点数°

‖司■■|

司』■

□∩uⅦberˉo十一p-de5〔e∩da∩t5:节点内的p节点数。 □∩u们ber-o十-pu∩〔tuatjo∩:节点包含的标点符号数° □de∩5jty-o+-pu∩ctu己tjo∩:节点的符号密度。 □de∩5jty-o千ˉtext:节点的文本密度。 □de∩51ty-5〔ore:最终评分° 这些字段都是我们计算最终节点评分需要的,在此列举几个字段的计算方法: de千∩uⅧberofa〔haI(e1eⅧe∩t: [1e爬∩t):

■■

1十e1e爬∩tj5‖O∩e: retur∩o

text= ‖ 0 .jo1∩(e1eⅧe∩t.xpath(』.//a//text()|)) teXt=re.5ub(r|\5*0’ | 』’ text’ 千1ag5=re.5) retur∩1e∩(text)

j十e1e爬∩t i5‖O∩e:

■·■■ ·纠■可

de+∩u‖ber一o+-p—de5ce∩da∩t5(e1e|∏e∩t: [1e爬∩t):

0

retur∩O

P0‖〔丁U∧『I0‖≡5et(| |0 .‖ T @ ’. 、 ; : “’’‘’ 《》%()◇{}「」 【】*~、’.?: 》{ 0′ !%(〉‖‖) de+∩u川ber=o+-pu∩〔tuatjo∩(e1e爬∩t: [1e爬∩t):

』‖‖』

retur∩1e∩(e1eⅦe∩t.xpat∩(‖ .//p0))

j十e1e∏论∩t j5‖o∩e: retur∩o

text≡‖.joi∩(e1e|∏e∩t.xpath(‖.//text()!)) teXt=re.5ub〈I0\5*0 ’ | 0 ’ teXt」 十1己gS=re。5) pu∩〔t0atjo∩5= [c+or〔j∩textj十〔j∩p0‖〔「0∧『I0‖] retuI∩1e∩(pu∩〔tuatio∩5)

』 · ■■■‖

de十de∩5jtyˉo仁text(e1eⅧe∩t: [1e爬∩t):



i千e1e爬∩t°∩u∏bero+de5ce∩da∩ts ˉe1e爬∩t°∩uⅦbeIofade5〔e∩da∩t5==0; retur∩o

retur∩ (e1e「∏e∩t.∩u刚bero「〔harˉe1e爬∩t。∩u∏bero千a〔har)/ (e1e爬∩t。∩0川beIo+de5〔e∩da∩t5 ˉe1e‖e∩t°∩Ⅷbero+ade5ce∩da∩t5)

Ietur∩re5l』1tor1

必里’‖李砌儿/|~汀异刀坛,按收田蚕叙郁是[」e肌e∩t呵家’返凹值是对应字段的结呆· 这里列举的几个计算方法,接收的参数都是[1e肌e∩t对象’返回值是对应字段的结果°∩u|『‖beIof

0

| 勺

·■{‖|{■■■·‖

己Char方法用于获取节点内带超链接的字符数,实现流程是查找当前节点内所有的a节点,然后统计这 些a节点内的字符数量; ∩Ⅷberˉo十ˉpu∩〔tuat1o∩方法用于获取节点内标点符号的数量,实现流程是先获 取节点内的所有文本’然后统计其中属于标点符号的字符,这里声明了标点符号的集合p0‖〔丁0∧∏0‖; de∩5jt儿o仁text方法用于计算节点的文本密度,其计算规则和142节的公式完全-致,这里就是 ∩u"bero于〔∩aI和∩u"bero十己〔haI的差除以∩u们beIo十de5〔e∩da∩t5和∩u刚bero+ade5ce∩da∩t5的 差;de∩51ty=o于-pu∩〔tuatio∩方法用于计算节点的符号密度,其计算规则也和l42节的公式完全_致’

·可·■‖■■可‖』■

de十de∩51tyˉo十ˉpu∩〔tu己tjo∩〈e1e爬∩t: [1e眠∩t): Iesu1t≡ (e1e爬∩t.∩l』刚bero+〔harˉe1eⅦe∩t.∩u「∏bero十a〔har〉/ (e1e∏记∏t.∩Ⅷber-o十—pu∩ctuatjo∩+1)

||■Ⅶ■可{

‖|‖



l43详情页智能解析算法的实现

7l9

}’β

即∩0Ⅶbero十〔∩ar和∩Ⅷbero+a〔har的差除以∩u"berˉo仁pu∩Ctuatjo∩加1° 通过这些方法’我们就可以计算[1eⅦe∩t对象的各个指标了’最重要的当属文本密度de∩51tyˉo十ˉ text和符号密度de∩51tyˉo+-pu∩ctuat1o∩。

}…「

最后_步是利用l42节介绍的公式’计算节点的最终分数并选取分数最高的节点提取其文本内 容’最终得到的结果就是正文内容°提取正文的方法定义如下: de千Pro〔es5(e1e爬∩t: 【1e们e∩t): #预处理

prepIo〔es5‖〔o∩te∩t(e1e阳e∩t) ■厂)|卜『巴■|

卜‖「||卜|▲■■「卜仿『「|户■厂)尸‖[尸■厂卢卢}■||||■「『『|[■厂〖●弓}|卜『|尸|匹》『|■「|[■厂|{■{尸‖‖‖■◆



#找出当前节点的子孙节点

de5〔e∩da∩t5=de5ce∩da∩t5-o+-body(e1e『‖e∏t) #找出所有节点的de∩51ty-o十ˉtext伍的方差

de∩5jtyˉo〔text= [de5〔e∩da∩t.de∩5ity-o十-text千orde5〔e∩da∩tj∩de5〔e∩d己∩t5] de∩5jtγˉo+-text5td=∩p.5td(de∩5jty-o千-text’ ddo+二1) #计#所有节点的de∩5ity-5coIe伍 千oIdes〔e∩da∩t1∩de5〔e∩da∩t5:

5〔ore≡∩p.1og(de∩5jty-ofˉtext5td) * de5〔e∩da∩t.de∩5ity-o十-text* ∩p.1og1o(des〔e∩d己∩t.∩uⅢber-o十-p-de5ce∩da∩ts+2) * ∩p。1og(de5〔e∩da∩t。de∩5jtyˉo十ˉpu∩〔t0atjo∩) de5〔e∩da∩t°de∩sity=5core=5〔ore

#根据de∩5jty—5〔ore对节点进行排序

de5〔e∩da∩ts=5orted(de5ce∩da∩t5’ 长eγ=1a爪bdax8 x.de∩51tγ-5〔ore’ reγer5e=『Il』e) de5〔e∩da∩t+jrst≡de5ce∩da∩ts[o] i+de5ce∩da∩t5e1seNo∩e j十de5〔e∩da∏t「1r5t j5‖o∩e: retur∩‖O∩e

para8rap"s≡de5ce∩da∩t十irst.xpath(』·//p//text(〉』)

pamgrap∩5= [paragr己ph.5trip() i「paragraphe15e 0 ‖ 千orPaI己gmPhj∩PamgmPh5]

pamgmph5≡115t(十11ter(1a吨d3x: x’ paragIaph5)) text= ‖\∏! .joj∩(p日Iagr己ph5) text≡text.5triP() retl」r∩teXt

这里定义了—个proce55方法’并向其中传人HnH根节点进行处理。首先调用prepro〔e55斗〔o∩te∩t 方法做预处理,然后调用de5〔e∩da∏tSo+body方法获取了body节点的所有子孙节点,赋值为 de5Ce∩da∩t5°接着对de5〔e∩da∩t5进行遍历’计算出各个子孙节点的文本密度、符号密度以及文本密 度的标准差’最后求得分数de∩5jty-5coIe°

求得所有子孙节点的de∩5jty—5core之后,排序找出de∩s1ty-5〔ore最高的节点,然后提取其P节 点的文本内容即为正文’如上代码中的最后一部分便实现了排序和提取过程°

调用prO〔e55方法来提取示例新闻页面的正文,运行结果如下: "“我的名字叫紫禁域’快妄600岁了,这上九的夜啊’总是让我沉醉,这么久了却从术停止. ”\∩"宜椽之上的月尤’ 甘 照进古人的宫殿;域墙上绵延的灯彩,映出了角楼的瑰丽°今夜,一群博物馆人将我点一役话°\∩半′』`时后, “紫禁戏上

允之夜”的灯光点尧了北京夜空°\∩午门城楼及东西雁翅楼用白、甘、红二种颜色光源装扮! \∩太和门广场变成了超大的

夜景灯光秀场! \∩图片米源: 东方I〔版权作品讨勿转伎\∩午门威宫博物陇供图\∩故宫的角楼赦灯光装点出泻泻的节日 兄执! \∩故宫博物院供图\∩今人惊叹的是’故宫的“冈红′’戴品《汁明上河图》 《十里江山图堪》在“灯会′’中瓜开画卷°

\∩灯光版《滁明上河图》\∩以灯为笔,以星顶为’故官博物陇最北端种式「]也枝灯尤点兜| \∩故宫博物倪供图\∩上元之

夜,故宫邀叶了劳动模范、北京榜样、′诀递小牙、环卫工人、肝放军和式警官兵、浦防指战页、公安千驴牙各界代农以 及预约咸功的观众`共30OO人故宫博物吭供图\∩时间退回到两天前,故宫博物陇发布了2月19日(正月十五)、 20日(正

月十六)即将举办“紫禁域上元之夜″丈化活动的消′坠°\∩图片米源:视觉中国\∩18日哎瓜,一众冈友前往故宫博物陇官网 抢慕’网站甚节就有诸多讲究°\∩有灯元月不娱人,有月无灯不耳容· \∩容到人间人似王,灯烧月下月如怠°\∩澜街珠 翠游村女,沸地蔓歌赛社神°\∩不瓜芳尊开口更,如何浦得此良辰°\∩—唐伯庞《元宵》\∩明代宫中过上元节,立宵节 晚会’’° \∩∑月 18 日’北京故宫午门泪试灯尤°中新社记者杜洋极\∩其中,灯戏颇为有趣°由多人什灯拼出吉抨丈字 及图案’皋人于执彩灯、身着不同颇色的服装’嘲瑚起毋,矣似于现代的大型团体操农演°\∩但这紫禁城’总亲王奥与 英法联军交换了《犬津条约》批准书’并订立《中英北京条约》《中法北京条约》作为朴克°\∩战十结求了’俊咯者桩身 一变成了游客·一位外圆“掇影师”拍下了当年的紫禁城,并在日记豆写到,百年°\∩Ⅲ到上世纪40年代时,故宫的环境

|{4

720

第l4章页面智能解析

仍然并不足想象中的博物馆的状态°\∩甘有故宫博物茂工作人员撰文回忆, 当时的故宫内杂苹丛生,房倒压汤,有且顶 龙长出了树木°光足汁理当时宫中存留的垃圾、杂苹蛇用单非翔到任故宫院长°那时,他伞到的故宫博物戌介绍’写了 这座博物馆诸多的“世界之录’’°\∩可他觉得, 当自己兵正走到观众中间,这些“世界之丘”邮没有了。\∩2月18日,北京 故官午门词试灯光.中靳社记者杜洋摄外环咙进行了大整治°\∩游客没有地方休息’坏枕拆除了宫中的临时这筑、浙』甘 供游客休』魁的椅于; \∩游客排队上厕所’娜就将一个职工食堂邮改成了沈于间; \∩游客买纂难,邪就全面采用电于购 慕,靳淆多个但桑点;馆°\∩今平,持续鳖个正月的“过大午”辰览和"紫禁域上元之夜”’让本坟足淡芋的故官变得一桑 难求. \∩在不少什通人爪中,近6卯岁的故宫正变得越米越午轻°\∩资针图:故宫博勃陇陇长单非翔°中所社记者刘关 关祖元宵节活动进行评估后’或站合二十四节礼于支矣时间节点扣出夜场活动. \∩你期待吗?\∩作者:上官云宋宇及"

可以看到’正文被成功提取出来了° q

5.提取发布时间



提取发布时间,-般根据两个内容,_个是"eta节点,—个是匹配规则。

如果阴eta节点里包含发布时间的相关信息,那么通常就是对的’可信度非常高’提取出来并返 回就行;如果不包含,就用正则表达式匹配—些时间规则来提取。

首先我们根据"eta节点提取’下面列出了一些用来匹配发布时间的XPath规贝‖: ∩[丁∧5= [ |//爬ta[5t己rt5ˉNith(0property’"r∩ew5:datepub1j5hed")]/依o∩te∩t|’ |//爬ta[5tart5ˉwjth(0property’"artic1e8pub1i5∩edtj雁画〉]/依o∩te∩t0 ’













·■]·‖|‖■∏』可‖】』■■】·月』‖』●■‖·〗‖」·∏|勺‖』■■|』□

0//爬ta[5t己rt5ˉwit∩(0property’ .o8:pub115∩edtj爬.)]/0co∩te∏t0 ’ ′//贬ta[5tart5-"ith(0property’"og:re1ea5edate圃)]/依o∩te∩t! ’ ‖//爬ta[5tart5ˉwjt∩(0jteⅦprop’"d己tepub1is‖ed!!)]/αo∩te∩t′’ |//脆ta[5tart5ˉ"jth(0jte‖prop’"date0pdate0!)]/欧o∩te∩t`’ !//爬ta[5tart5ˉNjth(刨a爬’"0rigi∩a1pub1i〔atjo∩0己te")]/Qco∩te∩t!’ ,//爬ta[5tart5ˉwith(饥a爬’"aIti〔1edate_orjgi∩a1")]/伙o∩te∏t! 』 }//爬t己[5tart5ˉwitb(助a‖记’ .og:tj爬凰)]/依o∩te∩t|’ !//爬t3[5taItsˉwith(0∩a爬’"apub;ti爬∩)]/0co∩te∩t』’ ‖//眶ta[start5ˉwith(0∩己爬’"pub1j〔atjo∩date")]/伙o∩te∏t‖’ 0//爬ta[5t己rtsˉ"jth(0∩a眶’"5aj1thru.date")]/依o∩te∩t0 ’ ‖//爬ta[startsˉ训ith(0∩a贬’"pub1i5‖0ate")]/0co∩te∩t|’ !//爬ta[5tart5ˉwit∩(0∩a爬’"pub1jshdate,0)]/@co∩te∩t! ’ !//贬ta[5tart5ˉwjt‖(0∩a眶’"pubDate圃)]/伙o∩te∩t0 ’ ,//爬ta[5tart5ˉ倒jt‖(0∩己爬’ ∩pubtj爬赋)]/伙O∩te∩t|’ 0//爬ta[5tart5ˉ"jth(@∩a赡’"-pubtj爬圃)]/αo∩te∩t‖’ 0//爬ta[5tart5ˉmt∩〈枷a『‖e’""eibo: artj〔1e:createˉat闻)]/伙o∩te∩t! ′ 0//酝ta[5tart5ˉ"it∩〈0pubd己te’"p0bdate触)]/0co∩te∩t! ’



·





以上规则都是通过经验总结得来的,可以自行添加或修改°







·

然后我们同样定义一个方法eXtra〔tˉby-刚eta来提取发布时间’它接收—个‖t们1[1e爬∩t对象’该







方法的定义如下:

















Ietur∩| ‖ .jo1∩(d3tetj爬)



i十d己tetj∏记:



de十extm〔tˉbyˉ爬ta(e1e爬∩t: ‖m1[1e‖e∩t): +orxpathj∩例[丁∧5; dateti‖∏e=e1e川e∩t.xpath(xp3th)









这里其实就是遍历‖[丁∧5中的XPath规则’然后查找整个‖t"1[1e爬∩t对象中有没有与当前规则





匹配的内容’例如:







//爬ta[startsˉ"1t∩(印roperty’"o8:pub1j5hedtme")]/0〔o∩te∩t









这行代码就是查找川eta节点中是否存在以o8:pub115∩edt1Ⅶe开头的property属性’如果有’就



提取出其Co∩te∩t属性的值.

假如我们的案例中刚好有-个『∏eta节点的内容为: 〈爬t己∩a爬="og:tj爬" co∩te∩t="2O19ˉO2ˉ2OO2:26:00"〉

口□厂(‖■〗·]』·■■||勺』■‖」■■■■】

经过处理,它会匹配到下面的XPath表达式:

| }





l4.3详情页智能解析算法的实现

72l

//‖eta[StartsˉNith(0∩a爬’"og:tj贬圃)]/0co∩te∩t

其实extraCtˉbyˉ‖}eta方法就成功匹配到时间信息了,提取出2019ˉ02ˉ2002;26:00这个值就是发 布时间了。-般来说这个结果可信度非常高,可以直接将其返回作为最终的提取结果°

可是,并不是所有页面都会包含这个Ⅷeta节点°如果不包含’就要尝试用_些时间匹配规则来 提取’其实就是定义一些时间的正则表达式:

●尸}||■■厂

R[C[X【5= [ ,|(\d{4}[ˉ|/| .]\d{1’2}[ˉ|/|。]\d{1’2}\s*?[Oˉ1]?[Oˉ9]:[0ˉ5]?[Oˉ9]:[Oˉ5]?[Oˉ9])口’ ′0(\d{4}[ˉ|/| .]\d{1’2}[ˉ|/|。]\d{1’2}\5*?[2][0ˉ〕]:[Oˉ5]?[Oˉ9]自[O-5]?[O-9])廓’ "(\d{4}[ˉ|/| .]\d{1’2}[ˉ|/| .]\d{1’2}\5*?[0ˉ1]?[Oˉ9]:[Oˉ5]?[0ˉ9])"′

▲尸|■伊■『尸 ●「■『Ⅲ尸∩

"〈\d{q}[ˉ|/|.]\d{1’2}[ˉ|/|。]\d{1,2}\5*?[2][Oˉ3]:[Oˉ5]?[Oˉ9])n’ "(\d{q}[ˉ|/| .]\d{1’2}[ˉ|/|.]\d{1』2}\s*?[1ˉ24]\d叶[Oˉ6O]\d分)([1ˉ24]\d时)口’ "(\d{2}[ˉ|/| .]\d{1’2}[ˉ|/|.]\d{1’2}\5W[0ˉ1]?[O-9]:[0ˉ5]?[O≡9]:[Oˉ5]?[Oˉ9]).’ 。(\d{2}[ˉ|/| .]\d{1’2}[ˉ|/|·]\d{1’2}\s*P[2][0ˉ3]:[Oˉ5]?[Oˉ9]:[Oˉ5]?[0ˉ9]).』 α(\d{2}[ˉ|/|·]\d{1’2}[-|/|·]\d{1’2}\s*?[0ˉ1]?[Oˉ9]:[0ˉ5]?[O-9])口’ ”(\d{2}[ˉ|/|。]\d{1’2}[ˉ|/| .]\d{1’2}\5*?[2][0ˉ3]:[0ˉ5]?[0ˉ9])日’ α(\d{2}[ˉ|/| .]\d{1’2}[ˉ|/|.]\d{1’2}\5*?[1ˉ24]\d时[Oˉ60]\d分)([1ˉ24]\d叶)n’

》■『|■厂β}【■■■■『‖′■「「|■「》仔「’■|卜凶`′卜‖||队『|◆「ββ雪『佃ˉ·[尸||巾【=∏■凹■■∩「[尸||■■■口||匹尸||||[二■厂|=『{匹■=尸||||■『||凸■■‖|||匹「『『『|『■■|‖『‖■厂□厂‖』【ˉ■■∏

"(\d{4}[ˉ|/|。]\d{1’2}[-|/| .]\d{』’2})口’ "(\d{2}[ˉ|/| .]\d{1’2}[ˉ|/| .]\d{1’2})闻』 "(\d{↓}午\d{1’2}月\d{1’2}日)口’ ′′(\d{2}年\d{1’2}月\d{1’2}日)口’ "(\d{1’2}月\d{1’2}日)"

由于内容比较多,因此这里省略了部分内容°其实就是_些常见的日期格式, 日期格式毕竟是有 限的,所以通过一些有限的正则表达就能完成匹配° 接下来’定义一个正则搜索的方法: 1‖portIe

de十extm〔t-by-regex(e1e∏论∩t:肮‖1[1e爬∩t)ˉ〉5tI: text≡| ! .joj∩(e1e‖记∩t.xp己th(! .//text()!)) +oIIegexj∩R[C[X[5: re5u1t=re。5e3rC‖(regex’teXt) i十re5‖1t8

retur∩re5u1t.8Ioup(1)

这个方法中先查找了e1e『‖e∩t的文本内容,然后对文本内容进行正则表达式搜索,符合条件的就 直接返回°

最后’我们直接把提取发布时间的方法定义为: extmct-byˉ爬ta(e1e‖论∩t)oIextract=by-regex(e1e爬∩t)

这样就会优先根据‖eta节点提取,其次根据正贝‖表达式提取°

另外’对于处在特殊位置的时间,可以对要处理的肮"1[1e"e∩t对象进行预处理,先排除—些干 扰信息,以提高提取的正确率° 6.整合

现在规整—下’将提取标题、正文和发布时间的方法合并为extract方法,然后输出JSON格式 的结果: de十extIaCt(ht‖1); retur∩{ ,tjt1e|: extr日Cttjt1e(∩m1)’ |datetj爬′: extraCtd己tetj爬(ht∏1〉’ `Co∩te∩t|: extm〔tCO∩te∩t(btⅦ1) }



■|‖‖‖||』·】|||

722

第14章页面智能解析



‖|

最后直接调用eXtraCt方法,运行结果如图l4ˉl2所示° 懂

伯′{

p

铡t让1e"』 ′敖窜,你儡谰点! 敬富§不,实力已不允许我鳃续低调||, 钟dαtetiTe"£ ′』2019毒腿ˉ泌02昌26:硼"旗 ’『贮尸■■

『0CO印七e问t"8 "CO印七e问t"8 丽憾我的名字四囊絮壤,快囊蜘岁了这上元做夜蜗| 丽憾我的名字四囊絮壤,快囊酗岁了 这上元做夜润| 总篷让裁沉醉’这热 久 了却私未停止° 馋`就“臣栅之上的月光, 曾解进窗人的宫殿‘城堰上绵延灼灯彩·映出了熏楼 掏班丽·今夜。 ~酵博物馆人将我点亮′我在北柬的中只,颧给团圆的像们’ -座壮观约峨 . "’\∏故宫博鞠院供图\∩今年元宵节,故宫迎聚了缝院叫年以采的菌旗“灯会” α灯会闻开始 前,故窜博物院在徽博上写下了这祥一殷诺·\∏半 』`时后· “紫壤髓上元之夜逊敞灯光点亮了



0



土京夜空.\"午门坡樱及东西廉蹦漓用白、黄、红三神獭色炎源赣扮 \∩太棚『司广场蜜成了 超大的夜只灯光秀爆! \∩圈片来螺东方1《愚权作品『§勿辕裁\∩午门城楼的酉马遵上` ‘〔 囊禁城里过大年展览上无夜也依旧开放! \门故富薄燃院侠翻\∏故宫的慈楼蔽式了兜装点出满藩



望去羹不胜收! \呻了光版《渴明上河图》\∩太阳殿骏灯光勾挝出宏伟胸轮廓! \∩故宫博吻皖 故宫博输胰最北腾神武门也披灯光点嘉! \"故蜜博物院供圈\∩上元之覆′蚊宫邀谱了劳动橇

范`北京榜祥`决递小哥、环卫工人、鳞放军粕武田官兵`消肪擂战虽`公安干蟹博各界代 裹以及撅约成功晌观众,共3哑人游览紫赣域·\∩更多^薄过瞬络鹰播-曝敏富“灯会”盛贝

° \∩故富博物院供圈\∩阮闽退回到两天前’故宫薄潮院发梅7∑月19臼(正日十五)、 23日(正

」叫‖」』∏‖■]

月十六)即蒋举办“紫慧城上冗之菠蹿文化话潮的消愿°\「圈片来源:概觉中国\问:8日凌凰 一众网友前往故富博渝腰官网抢囊,网姑葱至-度嚼挤剥疆痪°\们即甥一壤滩求,这溺天夜 间的门囊仍然在短时间内掇-抢两空·Ⅵ蛀窜博物院供蹋\q今傣的元宵蒲夜晚故宫≡下热 阉了起来.\∩其实‘远在明漓两代,富里的上元节靴有谢多讲究· \蹈灯无痘不娱人, 有月



·■‖‖』‖‖叫

· \∏故宫博物胰供图m窄网友还在感叹这次馏越警运难度膨欲抢寨0 `懒约门囊挪圆‘秒光,椭』 愈懒约门凛翻圆‘秒光,,, |

‖{』]{曰

供图\∩上元之夜" 宫申的畅音阁戏楼持壤上漓蔷戏鞠赛演.\们故窟博吻院供踊\"爆后-迹,

巴百陌■诬■∏凹‖凹】‖■『‖

江山圈卷》在“灯会”中履开画卷°\"灯光版《洞明上河图》\∩以灯为憋, 以愚r为纸, 一膜

‖」

的节目缉氛! \弥故窗博物陕供图\∩令人慎戚的篷" 放宫的斟踊红尸戴晶《渴明上河图》《干里

图l牛12提取结果

至此,我们成功提取了示例页面的标题、正文和发布时间’并以JSON格式输出这些内容。由于

整个提取算法实现起来比较复杂’因此本节对部分代码的逻辑做了简化,不过大家不用担心,我已经

pip3i∩5ta11geIapyˉ己utoˉextI己〔tor

‖‖‖

将以上提取算法封装成了_个完整的Python包’可以直接调用’感兴趣的话也可以查看其源码。包叫 作GempyAutoExtIactor,可以通过pjp3工具安装: 安装完成后就可以导人使用了,调用流程也非常简单:



ht们1≡〔o∩te∩t(‖detai1.们tⅦ1|)

prj∩t(j5o∩1+y(extra〔tdetaj1(ht‖1)))

这里我们调用co∩te∩t方法读取了详情页的HTML代码’调用extractdeta11方法提取了详情页 的内容’并调用j5o∩1fy方法对提取结果进行格式化,运行结果同样如图l4ˉ12所示°

』□旦■Ⅵ■

另外’GerapyAutoExtractor包还在很多细节上对提取算法进行了优化’大家可以查看其说明来了

ˉ■习□日‖■■‖●可■尸‖《∏』可|

千ro阳gerapy=己uto=extⅢ3ctormporte×tIa〔tdetai1 十ro‖gerapy-autoˉextractor·∩e1per5mport〔o∩te∩t’j5o∩i十y

解更多用法’或者直接查看源码来详细了解本节内容的实现流程°

·■■■γ』■

本节中我们介绍了详情页提取算法的代码实现’不同的内容对应不同的实现思路。本节代码见 https://gjthub·com/Gerapy/GerapyAutoExtractor°

二■‖■←』·】可

7.总结



↑44列表页智能解析算法简介 对应算法°除了智能解析详情页外,我们还需要考虑到列表页° 本节中我们来了解_下列表页的智能解析算法,主要包括如下内容°

|■■□{■■■■纠··‖||■■■

我们在142节和l43节中了解了提取详情页中标题、正文和发布时间的过程,并用代码实现了



□我们定义的列表页是指怎样的页面。

‖‖

《】■曰‖‖■■■■】

■■■凸■■■β■‖β〗

| l44列表页智能解析算法简介

723

□列表页的哪些信息是我们需要提取的° □介绍列表页的提取算法。

↑.怎样的页面属于列表页



在142节’我们已经了解了列表页和详情页的区分方法,这里就不再做对比阐述了°列表页包含 一个个详情页的标题和链接’点击其中某个链接’就可以进人对应的详情页’简言之,列表页相当于 导航页°图14ˉl3所示的页面就是_个非常典型的列表页,这里我们就以它为例进行介绍°

0

尸‖■■

能够看到,页面主要区域里的新闻列表很醒目’每行都包括新闻的类别、标题和发布时间,点击 其中任意~个标题,都能进人对应的新闻详情页°

′尸

唾;;唾;…〔“} mm; 『画…哪 圈四D蚀出■手….

广

.肚刽



》‖『■

。…_ 。肚台】~唾…一 ,四m .触刽

■丁二】—二上莹兰:二→二二; …

=芒Q丘协=乙

…L…0Q …=坠野“!B …0●】坐■

f…、尝!巳加拿哗8已

●厂‖■尸尸伊‖

瑟渴墨 蔑:避



…泌n巍】】

卜匡■『‖』■

『■∏!

△■卧吕■苔呛■

卜膛β| 浑

》‖

勺阐叮懒坦咐粥瓣螺路坷γ习啃思凸筋广射男韶凸沮甲

■■■‖|■「 ≈

…沁】纱0讣

肚刽

叠■巴2●■巴■

牡刽

…■…

…岭巫0四



〔…=g▲>a鲤

肚刽

…■…



…』△二…



…骆吐〗蚀



…沁m仓叼

…N…9

图l4ˉl3示例列表页



卜‖

2提取内容

我们需要做的是从当前列表页中把详情页的标题和链接提取出来’并以列表的形式返回,例如对 于图14ˉ13,我们想要提取的结果就类似如下这样:

■尸「

{ 伪

口tit1e口吕

闰进入职业大沈肿时代, “吃谷词职业还吃谷吗? ′′’

"ur1": α

bttp5://∩ew°qq。〔m/α∏)/2021o828/2021o828∧o25[倔o0.ht川1 "

}’ ■■厂巴■β巴β日●[■尸■【●「[贮尸■‖

{ "tjt1e闪: 倒他,活出了我们理想的抨于"’ 00ur1"8 固

} |

http5://∩ew°qq·c咖/αm/20210821/2021o821Ao2oID".‖t‖1



||

第14章页面智能解析

724

由于内容较多’这里省略了大部分内容。返回的这个列表中’每_个元素各代表一个详情页’包 含标题`链接这两部分内容°

如果能够实现这些内容的自动化提取,再结合详情页的自动化提取,那我们不需要编写XPath’ 就可以把_个网站的关键信息都爬取下来了° ∏■

|‖|

3.准备工作 ■■■■

如图l牛l4所示。 咆↑硒■‖题

×

●■■









■=

==▲



扭沁r呻∩t■【y‖G$障『

m罕Ⅱ”』b1Ock/ }

……TM[

≈=→一…—聋→■…痔伊…■P=≡峙=

c呵…… …」S… …贰阿

…尸∏…F■●

C如γx≈恤 …m‖x呵h

}斟翻飘"



…■『■『

■…=∩…】≡…■

弘←α==■

■『‖凸〗』〗】】】】‖·

『“Ⅻ9



广也…■〔…m

……… 韩汹饥田…

=→●■

∩帕lf

户~ ˉ■Ⅱ

…烯 …‖

……酗吩

D▲

…∩

…沉n· 巳哩m

0

c″y9沁晌酶【 智.司



》仿

嚏蕊—「 单广■导凸巳履导β竹显『〗■■导

……





…ˉˉ

ˉ…

m…·些回

c”】

………



…~||





F蓟m佣丁M止

垂…~







…≡

~……

审$冈 郝一…呻

吕碰…1工

叮心二

…≡峦

ˉ

……·



l三

4提取思路

要提取列表页中的标题和链接,首先需要观察标题的源代码特征。我们随机选取—个标题查看其

源代码,如图l4ˉl5所示。

‖ { ‖ 』 ■ ■] 』 ■ ■ Ⅵ ‖ 」 ■ √ ‖ ■ ■

旧回

…稗刚酗 口□□□

ˉ舅 、盯寺兰■碑

°归内l…二二 °{■内] =0瓣了 ‖ · 田内!~

酗丁 l…



●1…m·StVl●《 》 —_-__

<■仰∩c1“>口r』砷〔『np问…『w闻≥{E·2…10〗7〗m830》匈S…

O10 M1《

□】‖口■■■‖凸■·β

儡田

v匈1vc山■■■■[■t竹>

」·〗|』■■■|』■■‖』■】‖」■■∏】■」‖|』■γ』·〗|

图14ˉl4保存列表页的HTML代码

■》

1mt剖叮IG8卜∩m名; 》

}0≤n>…′u>

U1{-一一

{》<u≥■</u>

{√u1> <$脚∩cm■D■■b1mR四■x/呻斡∑ </01P

{』

■叮mg◇0; 》

‖』■■··

p…m0】◆·;

}匿}摆/{鉴

‖‖■

__—…一-=

…U01Uo q【0砒p匈p 凹10 o10 ` p砧□ γ●硒∏ 7此1…tG 1…t0 t巳Ⅲt

■徊m=Ol“★=Zt■厂t: 】■; ■′■灿由l“∧≤呵占 〗■k

可以发现该标题对应-个a节点, hre+属性值就是对应的链接’同时这个a节点的前面还有一个 a节点’代表这篇新闻所属的类别。这两个a节点的外层是11节点,该1j节点有4个兄弟节点’这5

‖■■‖■■‖||‖‖』□■』‖』□‖』Ⅲ■■■■|||』■

图l牛l5列表页中某标题及对应的源代码

个1j节点同属—个u1节点。

|{

和提取详情页时—样’先把列表页的HTML代码从测览器里复制出来’并保存为listhtml文件,





■■■】■■□β止■■厅■【■【■■■●■■『

l44列表页智能解析算法简介

725

所以’如果想把当前页面中的所有链接都提取出来,需要提取所有u1节点内所有1i节点内的所 有类似标题的a节点°

p

初步分析貌似没发现什么通用的规律。如果换成其他列表页’其页面结构可能完全不同,例如不 会有1j节点,不会有u1节点,如果我们按照固定的u1、11等信息来提取’那么算法的可用性是非



『‖巴厂伊|■『[[β

P

常低的°

既然要实现通用的列表页信息提取算法,关键还是要找_些通用的提取模式°说到这里,可以观 察到_个现象:列表里的标题通常是_组_组呈现的,如果仅观察-组,可以发现组内包含多个连 续并列的兄弟节点。如果我们把这样连续并列的兄弟节点作为寻找目标’就可以得到这样一个通用 的规律:

广‖『广

□这些节点是同类型且连续的兄弟节点’数量至少为2个; □这些节点有一个共同的父节点。

『「炉丛尸卜●■|仅←■尸「|~尸『卜「■■

为了更好地表述算法流程’这里做一下定义’把共同的父节点称为“组节点”’同类型日连续的 兄弟节点称为“成员节点,,。有了这个规律’来看看从示例列表页中能找到多少满足这个要求的组节 点,如图14ˉl6所示°

口蓟m

烟易…滚动新闻顾=… m入"《…m}…∏=… ‖ 尉擞 n口 搏}. |呐掇臃 隧愈 "隐翻嚏 ,熟 蹦噶獭旗耐" 簿徽潍慰嚼翁倒 ·健 』惑…, ≡萨,` | _口巴一

□■际

…沁…

0}■内]

嚣:期

, l■内)

! ‖Ⅲ内j

…0卢Ⅸ丘=■

臼评论

′『■内『

=≈g▲0▲古…

巴拓众

□■片

□m

—∏~击~.

≡≡



0旧内『-~工Ⅲ工_ˉ弓!

『■内】— 『

…γb…旧 …00…》

[■内!… !

…凶『…

[■内}—~ 』 0归内]~

…椰n蛔



















}甲●■」■●●

=啡≈■

…6贮『‖叫



|:阂}墨″

…旧『哗‖∏



…"…

‖■内‖…泊=ˉ 扁 尸 【■内】…彪切层羔岂琵≡=F

≡卤-

■内『■ˉ一ˉ_l硒≡=__丁、≡p

…阅… …陷…

…每‖●…

_=→≡■■口P■▲Q一●→·■≈—学=●-· ■≈≡←■≡■≡p■≈●≈一→

■●■■●一

尸|↑■|}||》卜|)》|卜卜|‖





0『田内‖

□杜台

口军卒



·…~~←→←≡示~≈….^ =~

=气=。=~咱-→≈~≈面匙…=_…

□金■ 囱闽内



m癣■翱呻嘲蹿■甸臼"2·…熏…

回倒… —

p

立…重重重l【…己■

■内‖

隅ˉ黑

睡内l

壹蓟

阻内l…≡兰 ‖■内‖…三 ‖■内…=一

…托…



…严坠些乙

≡沪≈=

…炉≈乙醚

■■■■■■■

↑4

…肥些a▲问

图I4ˉ16示例列表页中的组节点

在图14ˉl6中’我们用矩形框选出了符合要求的组节点,可以看到这样的组节点还是蛮多的°但 我们只想要列表中的组节点(目标组节点)’因此需要想办法排除其他组节点(冗余组节点)°要想排 除冗余组节点,需要先找到目标组节点和冗余组节点的不同。直观来看’最明显的不同当属字数’冗 余组节点对应的每个成员节点都比较短’可能就包含2~3个字’而目标组节点对应的成员节点普遍比 较长’因此可以利用成员节点的最小平均字数设限,例如平均字数在3以下的成员节点对应的组节点 就会被排除。还有其他一些特征也可以利用’例如成员节点的数量’规定一个组节点对应的成员节点

k

第l4章页面智能解析



的最小数量为3,也能排除_些不必要的组节点°



726

为了更好地说明算法的思路,我们规定成员节点的最小平均字数为3’_个组节点对应的成员节 点的最小数量也为3,经过过滤,剩下的组节点如图14ˉl7所示.



||

冈■汀Ⅲ°所■休∏.起A.碘乐■■Ⅸz仇*。曰仅ˉ早机女人■■镭■■■.■户m杖∏蚀书ˉm●■■p口‖免≈■.m伍H灶|=■■

姻易…翻新闻『口日口Ⅲ=冈…垂mm]__ 巫入厕 憋m口匡睡xmⅧ}m只jⅡ鳃} 匝` α·撼′′ 懒内 阀腮艇禽 爵蕊愿鹰 ,礁厕幽熟藏 陋′』 燃臃岭.α 玲墩醛健 ″睡卿喊· ‘′.庶. | ~

画全■ 回■内

n民惊

唾了■!雹!唾冈■酶!画簿! 巴0浊“由辱磅



囱分癸列五

厅=—

▲一闪一

ˉ一ˉ .哄带ˉ′ ` |.田内]钮ˉ. .四内〗■ ≡ 当 …副岳二

固仕会 巳汗论

.四内‖‖

圈■片

二-

臣笛慧 …】O…

F _

二二=

…秘…



≡ ˉ≡二^=令盖

.‖■内l■

徊内』|ˉ尝ˉ …^

.旧内l0

; ..



i` 凰斯.愿~≥惠γ -

二莹ˉ ˉ…ˉ∑ b 司



α ·雪

悟点

…l0『户皿涵

~■

·『用内门_二= . 口-当ˉ÷之.2蔼壹…q震 .『■内‖‖~气

…〗0…心蝎 …沁〗…}

垦H 一.击当一ˉ



…们拓晚8… …必%岭Ⅷ绷

…县≈←=≈叮……抄◇.◆伊≈…、叶$c■←踏二=巴凸…

.旧内l、

ˉ ˉˉ民睡

·!■内阉、″主 °[■Ⅷ

ˉ .

瑟削鳃 -…=

…"0踪Ⅶ

■ˉ-、幂睡==

.旧邮彦己丙ˉ已 …弘÷办…广‘ ‘

= 一

·陋内刚F二卤=…. ^醉α岂=≡

F 盯



‘←副ˉ=

赚·愿■

削…

酗乒…一←^



品…=…≡一……



≡==■=…’~…≡=

…"≡_←=~一凸亨山■

●田■■●

咱丫划〗杜 内内内内内 旧Ⅷ旧Ⅷ旧

尸心-=■=口耳■Ⅱ…

麓臆鳃 蹬!!踩 蹬隐爵

|]■∏‖』■‖‖|■■】■■门』■】‖‖||■〗』■‖‖‖』■■』Ⅵ||{』■∏□■·■■】·■】‖』■‖‖■口

臼四

≡→-≡≡乏

l

ˉ

ˉ 0

≈o≡一≡钾

…00…

·田内)g宇 ≡≡≡.ˉ卓碌 佃^… ·旧内! ˉ ■. 缉 ˉ` 司 ≡…雷 =.

圃… 巴不巾





…00铀№逸

在图l4ˉl7中,就只剩下最上方的“网易首页>网易新闻>滚动新闻,这个面包屑导航对应的组

节点,还有下方标题列表中的几个组节点了° 下一步该怎么办呢?

先想一下我们想要的结果在哪里,对于这里’只有标题列表中的几个组节点是我们想要的。再回 想一下l42节提取详情页正文的逻辑’我们是怎么做的?是根据不同节点的特征计算了节点的分数,

最终根据分数排序,选出分数最高的节点作为提取目标,其内容就是正文内容。按照这个思想’这里 就是要根据组节点的特征计算每个组节点的分数’然后选出分数最高的组节点作为提取目标。稍等,

最高岂不意味着最终结果只有—个,可我们的目标组节点不止一个(实际比图l4ˉl7中显示的更多)’

在这种情况下’可以考虑“降维”操作,即把同类型的组节点合并为一个组节点’这样-来’图

1牛l7中的所有目标组节点就会成为—个整体,我们想要提取的节点就都在这个整体内部了,合并后

』■□■』』∏||』』■■‖‖』·‖‖

如果只有一个提取结果,那其他目标组节点的内容不就丢了吗?怎么保留多个结果,这个数量又怎么

定成为了我们面临的新问题°

■∏|」■‖|{』勺二■∏|」■■□‖』■|‖‖』■』■‖』‖=·」·】‖‖‖』●】■|||」』■」·{」●■·

图14ˉ17过滤之后的组节点

的组节点如图l4ˉl8所示°



可以看到’下方这个大的组节点包含我们想要提取的所有成员节点,因此最后只要能把这个组节

点选出来就好了。至此’我们终于可以使用提取详情页正文时的思想了,计算每个组节点的置信度分 数,然后选出分数最高的作为目标组节点。

| 二 ■ ■ 曰 』 ■ ] ■ ‖ 」 ■ 】 ■ ] 】

■【【‖■■『■【■■【‖■■∏『卜〖尸【‖卜[Ⅱ‖尸卜‖[卜[厂‖‖『|伊‖『■|●『[卜[「|尸|(≥|卜|[■『》||●|卜|■『卜|『■[「}巳『「|》[「心「厂}|■[尸[β

l45

列表页智能解析算法的实现

727

闪■■■田饵体∏m∩仅乐凹皿■Z砷坍■干仍丈人X仍Ⅷ皿凹■尸HHS■田俏拧■■吼E步审,几■m.m丁■鱼丘‖=■■

阐易…滚动新闻……雨] 一_一~m人副 {氧…√酗x良鞠…墨m | ` 娥娥簿崎颜陶阔愿做禽 赚隧…v嚷 |蹦哩煽‘ 删Ⅷ 馋窿啮瓤蹿潍α憾 .q 则… ·萨晒|



吨:铅m!唾`m■勋唾串m!…□0凹抄筋彰¥…

圃分酮安

…吗≈唾■=→=■■—d=~←■●7■

口金■

挫≡宅狞……巴

幽■内

≡<凹沁…

□■压

…碎‖0…

口杜会

…吟℃…

巴w谴

…lβ》神·唾

巴任宜

…~

尸≈←_

幻啤

!田内]

归■片

[■内]

巴悦■

‖■内]

…储》≥乙凶

【■内l

…忆『私▲瞬

‖■内l

…心粉幽〗舰

瑟‖蹦

…腊00泌〗v …码】…

…诞l…翻 …‖6田哟 …购…垫 …



…_~

■=~

·l■内l==产—≡≡≡■二

‖-

o!…] =





≡辆,=

=-≡≡≈=



‖ ˉ~霉ˉ司口二恿二,~=-

.!■内l ·‖■内l

一≡

·‖■刚 尘≡.≡氧皂



b≡…

p

绊-| ^总 .呻

瑟}念嚣 …帕… …】▲==山》 …沁解必m

图l4ˉl8合并所有目标组节点

组节点筛选出来了,下_步就是从其内部的所有成员节点内提取标题’这要相对简单一些’例 如根据字数`标签信息、超链接信息就能判断出标题对应的节点,提取其中的标题内容和超链接就 好了。



口}■∏|[『『|◆「|′[尸‖|●‖}■「‖[■[■「|■「■■厂

5.总结

经过本节的学习,我们可以自动化地找出页面中所有的标题和链接信息了°总体来说,提取思路 分为下面几步。

■■仿||仔[■「■·「【岗∩▲岳「「||伊■尸■【

(l)根据成员节点的特征(同类型且连续)找出所有符合条件的候选组节点° (2)根据规定的组节点特征(例如字数`成员节点数量等)排除冗余组节点。 (3)合并同类型的组节点,总的组节点数量减少° (4)计算置信度分数’从现有组节点中选出最佳组节点° (5)从最佳组节点的所有成员节点内提取标题和链接° 这个思路虽然不_定是最优的列表页提取方案’但用来提取大部分列表页的内容应该不是问题。

↑45列表页智能解析算法的实现 本节中我们来动手实现列表页的提取算法。

↑.本节目标

还是以图l4ˉ13所示的页面为例’用代码实现l44节‘‘总结”部分的提取思路。

注意由于部分算法比较复杂’本节介绍的算法是简化后的版本’更多细节处理可以参考本节最后 ■尸|||}■∏伊‖||}■■「‖‖Ⅱ‖卜【尸‖‖‖■●〗『

的说明°

第l4章页面智能解析

728

2准备工作

上_节中我们已经将示例列表页的HTML代码保存下来了,文件名为ljsth‖m1°另外’本节主要 还是用XPath解析页面和操作节点,所以需要用到lxml库°

{ {

3.数据预处理

和提取详情页正文时一样,由于原始HTl∏代码中包含很多干扰内容’所以先对HTML代码进 行预处理,整个预处理方法和提取详情页正文时也基本类似,这里为了更加灵活地修改处理逻辑,单 独定义了一个preproces5』1j5t方法:





千Io∩‖ 1xⅧ1.hm1mport‖tⅧ1[1e爬∩t’et】ee

LI5丁l」5[[[55丁∧65≡ [ 』‖∏et己’’ !sty1e0 ’ |5cript|’ !1i∩代0 ’ |γideo|’ |audio|’ !j十r己爬|’ !5o‖』I〔e,’ !5γg‖’ !path|′ ,S帧o1』′ 』i吨』′ |千ooter』』 』向eader』]

kI5T5丁RIp『M5≡ [!5pa∩′’ ‖b1oc促quote|]





|//djγ[〔o∩ta1∩5(0c1己55’ 』!co∏■赡∩t闻〉]』’ |//djv[〔o∩taj∩5(α1a55’"advert15eⅦe∩t")]』’ 0//djv[〔o∩t日j∩5(0c1己55’ !』adγert")],’ 『//diγ[〔o∩taj∩5〈05ty1e′ 』!di5p1aγ: ∩o∩e闰)]|’

de十prepro〔e5541i5t(e1e爬∩t: ‖m1[1e『∏e∩t): Ⅲ0】 00

prepIOCe55e1e∏记∩t十Or115teXtra〔tiO∩ :p己m‖e1e滤∩t:

°

;retur∩8 ∏■∏

#剧除标签和其中的内容

etree.5tripˉe1e∩记∩t5(e1e们e∩t’ *LI5丁05[正55丁∧C5) #只移动标签对

etree.strjpˉtags(e1e爬∩t’*lI5『S『RIp『AC5) re"oγe〔hi1dre∩(e1e爬∩t’[I5丁‖OI5[Xp∧阳5) +or〔hj1dj∩c∩i1dre∩(e1e爬∩t):

#将5p己∩和5tro∩8节点内的丈本合并到父级p节点内 j十〔h11d.tag.1o脆r()…‖p0 : etree.5trip-t日8S(Ch11d’|Spa∩`) etree.5trjpˉt己85(chj1d’|stIo∩g‖) i千∩ot (cbi1d.text己∩d〔∩i1d.text。5trjp()): re∏℃γee1印e∩t(〔bi1d〉 #扣采diγ标签不包含任何于节点,它可以杖转换为p节点

j十C‖i1d.tag.1ower()=!div』 a∩d∩otChi1d.8etC打i1dre∩(): 〔∩j1d。tag= !p‖

这里同样定义了-些规则’ [1S「0S[[[55「AC5代表_些噪声节点,可以直接调用5tr1pˉe1e们e∩t5

|□】』』■|●∏]■∏』■∏】』■】■■‖|·|』●γ‖』■】■可‖‖』·』』■Ⅵ|‖』■』■‖|』』■〗·]‖‖』■·】|



q

方法把其整个节点和内容删除。[I5丁5丁∩Ip「∧C5节点的文本内容需要保留,标签可以删掉。

其中还用到了工具方法re们oγec‖11dre∩、 reⅧoγee1e"e∩t等,它们的定义也已经在l43节阐明, 过里不再赘述°

4.选取组节点 现在实现前两步:根据成员节点的特征(同类型日连续)找出所有符合条件的候选组节点,然后

根据规定的组节点特征(例如字数、成员节点数量等)排除冗余组节点°

∩|』勺」■‖■■|』】|}|■■|||」■】二■■|||‖■】■‖|』』■■】

{I5丁‖0I5[Xp∧丁‖5代表—些明显不是列表内容的节点,例如评论、广告等’直接删除就好°

|| ‖尸

l45列表页智能解析算法的实现

729

|■尸

为了方便操作,这里扩展_下[1e佃e∩t对象的属性: C1a55[1e∏悟∩t(刊tⅧ1〔1e贬∩t): id8 j∩t=‖o∩e

tag-∩a‖∏e: 5tr=‖o∩e ∩uⅧbero十c帕r: j∩t=‖o∩e =■【【『■乙「◆■尸

0

∩u们bero+aChar: i∩t=‖o∩e

∩u‖bero千de5ce∩da∩t5; j∩t=‖o∩e ∩u刚bero十ade5ce∩da∩t5: j∩t≡‖o∩e

∩u∏]beI-o十=p-de5〔e∩da∩t5: 1∏t=‖o∩e ∩u们beⅢ≡o十-pu∩ctuatio∩; i∩t=‖o∏e de∩5itγ-o+-pu∩〔tu日tio∏: j∩t=‖o∩e de∩5jty-o千-text: 十1o己t=‖o∩e de∩5jty=5Core: 十1oat=‖o∩e #扩瓜冯性

巴‖巴■■▲尸■■■‖■尸‖▲◆

∩u‖beIo十5ib1j∩858 i∩t≡‖o∩e ade5ce∩da∩t5-gIoup-text=m∩=1e∩gt‖: i∩t=‖o∩e ades〔e∩da∩t5-group-text=爬x-1e∩gth: i∩t=‖o∩e 5i‖i1arity=w1th5jb1i∩gs: 千1oat=‖o∩e p己re∩t5e1ector: 5tr=‖o∩e

这里我们扩展了如下几个属性。

□∩uⅦbero千Sjb11∩g5:兄弟节点的数量°用于过滤冗余组节点’当_个节点的兄弟节点的数量 小于_定数值时,就过滤掉对应的组节点°

□ades〔e∩da∩t5ˉgrol」p_text们i∩1e∩gth:组节点内成员节点的文本内容的最小长度°用于过滤 冗余组节点°

□ade5ce∩da∩t5ˉgroup-text们ax1e∩gt∩:组节点内成员节点的文本内容的最大长度°同样用于 过滤冗余组节点°

□S1m1arityˉWit‖5ib1i∩g5:节点和兄弟节点的相似度。如果这个相似度过低’那么这些节点 可能并不是同类型日连续的节点,对应的组节点也不是我们想要的节点。

□pare∩t5e1ector:父节点的选择器°成员节点用它选择组节点,它们是父子关系° 接下来就找出同类刮日连续的成员节点对应的组节点吧,代码实现如下: ‖『

∏j∩∩u"ber=s

●『广■尸■止尸〖∩■■=厂

m∩=1e∩gth=8 吧X≡1e∩8th=“ 5i们j1arity-thIe5∩o1d=o·8 de千buj1dc1u5ter5(e1e『∏e∩t): de5〔e∩da"tStree≡de+au1tdj〔t(1j5t) de5Ce∩da∩t5=de5〔e∩da∩t5-o+ˉbody(e1e爬∩t) 「ordeS〔e∩da∩tj∩de5ce∩d日∩t5:

j十de5Ce∩d日∩t.∩u‖bero十51b1i∩gS+1〈Ⅶj∩∩Ⅷber: co∩t1∩ue

尸伊β卜》巴β ◆「|■尸

卜‖》 》卜|=■「∩



1+de5ce∩da∩t·ade5〔e∩da∩t5ˉgroup-text-m∩-1e∩gth〉∏己x一1e∩gth: Co∩tj∩ue

i+de5ce∩da∩t。己de5〔e∩da∩t5£roup-text-们ax-1e∩gt∩<m∩-1e∩gth目 CO∩t1∩ue

j于de5〔e∩d3∩t.51m1arity-们ith5ib1i∩g5〈5j爪i1arity-thre5‖o1d8 〔o∩ti∩ue

de5〔e∩da∏t5tIee[de5〔e∩d己∩t.paIe∩t5e1ector]appe∩d(de5〔e∩da∩t) de5Ce∩da∩t5tree=djCt(de5〔e∩da∩t5tree)

这里我们先定义了几个阂值°

□"1∩∩u"ber:用于限制兄弟节点的数量’这里定义为5’即成员节点至少要是5个同类型且连 续的节点°

□‖1∩1e∩gth:用于限制成员节点的文本内容的最小长度’这里定义为8。比较ade5〔e∩da∩t5 groupˉtextⅦax1e∩gt‖和m∩1e∩gth的值,如果前者小于后者’也就是组节点内成员节点的



」■■∏

文本内容的最大长度小于8’就说明该组节点内所有成员节点的文本内容都很短’而标题-般 得8个字以上’说明该组节点内不可能包含标题’就把它排除了°用这个阂值可以过滤掉很多

gIoup-text-川1∩-1e∩gth和"ax-1e∩gt∩的值’如果前者大于后者,也就是组节点内成员节点的

|」』■】●|‖|』■■∏

导航菜单组节点°

□们aX-1e∩gt‖:用于限制成员节点的文本内容的最大长度,这里定义为44°比较ade5Ce门da∩t5

(|」|‖·‖叫

第l4章页面智能解析

730

文本内容的最小长度大于44,就说明该组节点内所有成员节点的文本内容都很长,而标题一 般最多40字’说明该组节点内不可能包含标题,同样把它排除了°用这个阑值可以过滤掉很 多长文本组节点。

□5加i1arjtyˉthres∩o1d:用于限制兄弟节点的相似度,这里可以用tagˉ∩a|∏e、 〔1a55属性值、子 节点的数量或其他属性来判断节点的相似度’例如〈atjt1e="b1oc代"c1asS="1te阳ite‖ˉ1!』×/a〉 和<atjt1e="b1o〔代"〔1a5s=』』1teⅧ1teⅦˉ2"×/a>的相似度是比较高的,而〈atit1e=|b1oc代"

0

c1a55="1te阳1te们ˉ1"〉和〈5pa∩〔1a55=‖bo1d|』〉〈/5pa∩〉的相似度就很低°如果成员节点的相似

这里我们将de5〔e∩da∩t5tree定义为了de+au1tdjCt(115t)类型,其键名是父节点的选择器,键

□‖■‖‖|■■‖尸|」

度很低’就证明这个组节点里根本没有同类型且连续的成员节点’那么这个组节点就不能作为 目标组节点了°

〗■■

值是成员节点组成的列表。

运行上述代码’就能得到_些符合要求的组节点了,结果如图l4ˉl9所示°

…入厕 隧…豆酗x蔓mm瓢■魔』

腮易…滚动新闻l∏m贝雷冈日历闯≥「7vTⅧ]

|霄" 嚼仍翱栅腮嘲 "膘健健瓣谰厦 蠕廖 蹦. 慰嗽徽" 卸" ′昨厘口您倒 侧雌 磁|‘ 网`

◎,砸|

—=→

扣=吩ˉ■魏m∏■..唾km函□!跑●G哺硒四

回分m■ :

回金好

圃■内

0q ‖冈内l‖ ‖冈内l|

固■区

‖■内〗 .. ‖■内〗

田牡会

0旧内‖‖ 0旧内‖0

{ 回评论

·旧内】 °旧内】

} 圆瘴索

固军卒

.‖■内1|

□■片

嗡四内‖‖ .旧刺‖

□钒H

ˉU=

『 呵勺

°0 l■内‖|= l■内‖{

…‖6虱… …‖6…腮

…乙比扯n〃Q

=径毋型…

≡ ≡

ˉ

…妈佣0伪凸m刁

…佣0…瓣



F ■

甲急

… .丝

座呼0$‖肛蜘叼

咽= ^ˉ罩

ˉ兰~ˉ ˉ=. 叮面



、″

ˉ

= 0

■=

雨 勺

…■0凸〖酗l● …8叫6…》

…碎‖6w边竭

·旧闪1dˉ==

=垂

ˉ

=一←

·旧内!8

瑟协鳃 碑…咐0肚2翻

·‖』■|』■』‖‖‖』■■‖■|‖|□●】』■■‖]』』■]』■』■|‖■●』】】』■■‖」■■〗‖β□]□■■γ〖‖』■

冈】■页G■休可问A压丘■■α■静代本毋仗耙★人∏■.田n■■″京尼■■汝书mUH只多. 免日■.■臼■哇‖…■

…险

…■‖O碾‖●勘



…田‖●灶0〗锄

■≡…



q

」 · !■内l

…0$0…↑刁

. !■内『: .{■内】

…001…



°『田邮

=空= ^=

ˉ"■=

马=



…0巳Ⅱ…魏

钳≡=. 渔.

…0O嘲7′锄



』 〗

‖『■禽】s乙史



…06…

0

‖ 7

.旧禽『

0四邮

,

·‖N湾l =一 .旧禽‖…





·旧内! ■

口 b



·ˉ

…腿… …唾罢二丝琶

-二≡

…‖●…父翱



色T罕四!合坠0●纫

-

矿 T嘿

…■坚哇馋翼》

图l4ˉl9初步筛选出的组节点

5.合并组节点





| 0

现在要根据相似度合并组节点了,怎么计算相似度呢?简单来说可以直接使用选择器的路径表达 式’相似组节点对应的选择器相似度-定很高。例如这里一共有5个组节点的XPath路径表达式:

(』

|| ■



{』 ■凸「▲■■|卧「『『}|炉伊》『‖『「

l4.5

列表页智能解析算法的实现

73l

□/htⅦ1/body/diγ[@〔1a55=""a1∩"]/djγ[1]/u1

□/∩t川1/bodγ/djγ[0〔1a55≡|Ⅶa1∩"]/d1γ[2]/u1

□/∩tⅧ1/body/diγ[0c1a55=』』"aj∩』』]/djγ[3]/u1 □/∩t"1/body/∩eader/d1v[1]

□/‖t‖1/body/‖eadeI/d1γ[2]

p

你能找出哪些组节点属于同一组吗?很明显’前3个属于同_组,后2个属于同_组。那么如何 用算法实现呢?这个算法属于聚类的范畴了。聚类就是把相似的内容聚在_起成为_堆’聚类方法有 很多’例如Kˉmeans、DBSCAN等,不过仗里我们仅仅根据选择器聚类。-个简单的聚类方法如下:

p

de十〔1u5ter(jte∩s’ thre5ho1d≡0·9): ∩u恤eI=ˉ1

〔1u5teI5一爬p≡{} 》‖▲尸》■■∏‖》|卜‖●■『■尸‖■『△巴『[β■■}巴尸

〔1u5ter5= [] 千Or∩a↑爬i∩ite∏5: 「Or〔1∩〔1u5ter5:

i十a11(5mi1arity(∩己爬′ w) 〉thre5ho1d千orNj∩〔): 〔.apPe∩d(∩a爬) c1u5teI5-们ap[∩a爬] =∩u‖∏ber bre冰

e15e:

∩u∏beI+≡1

〔1u5teIs·appe∩d([∩a爬]) C1u5ter5ˉ阳p[∩日爬] =∩u仙er retur∩〔1u5ter5←∏ap

■卜|尸

de十〔1‖5teIdict(data: dj〔t’ t∩re5ho1d=0.9): id5≡data.促ey5() 〔1u5ter5ˉ阳p=〔1u5teI(1d5’ t∏re5ho1d) re501t≡de十au1td1〔t(1j5t) +or促’γj∩data。jt钢5(〉: i十i5i∩5ta∩Ce(γ’ 1i5t): 十OIjj∩γ:

Ie5u1t[c1u5ter5ˉⅢap[k]]。己ppe∩d(1) 伊 【 尸

P

e15e8

re5[」1t[c1l』5ter5ˉ归p[k]].3ppe∩d(v) retur∩di〔t(reSu1t)

》》『卜|

这里的C1u5terdjCt就是对字典类型的内容进行聚类处理’其输人数据就是de千au1tdjCt(115t) 类型的’键名是父节点的选择器’键值是成员节点列表° 泣阻我们可以根据上面的例子测试_下:

‖|●「卜■「‖}‖■尸|■■『||■■「■尸「■■「

data≡{

′/ht|『|1/body/div[0〔1a55="阳1∩n]/div[1]/u1』: [』〔hj1d1|’ 』c∩j1d2|’|chj1d3|]’ |/ht∏1/body/djγ[0〔1a55≡口阳i∩α]/djγ[2]/u1』: [!〔‖j1d40’ !〔∩i1d5|’ |cM1d6』]’ !/‖t∩1/body/djγ[0c1a5s≡∩∏ai∩闻]/div[〕]/u1! : [0〔hi1d7!’ ’〔hi1d8′’ ,chj1d9|]’ ,/∩t们1/body/headeI/djγ[1] | : [’chi1d1o0’ ,〔∩i1d11,’ 0chi1d12『 ]’ 0/‖t刚1/body/header/djγ[2]0 : [ !〔hi1d13,’ ‖〔∩j1dM! ’ 0〔hj1d15|]’

.



↑4

pri∩t(〔1u5terdi〔t(data’ thre5bo1dˉO.7))



运行结果如下:

{0:[『Chj1d1!’|Ch11d2|’ 0〔hj1d30’|Chj1“’!C∩j1d5『’|C∩j1d6,′!〔∩i1d7|’ 』〔hi1d8|》 』〔hi1d90]’1:[,〔bj1d10』’

|◆『|||巴■『「|}|■尸|||■■|止尸『‖}■■‖‖◆『『|■■尸

‖〔hi1d110’‖〔∩i1d12’’ ‖Chi1d13|’‖〔hi1d14|』 !Chi1d15,]}

可以看到成功把前3个组节点聚在_起了’成为第一组’另外2个组节点则聚为第二组’和我们 肉眼观察到的结果_致。

.

接下来调用〔1u5terdj〔t方法对上一步的de5ce∩da∩t5tree进行聚类处理: 〔1u5ter5=〔1u5terdj〔t(de5Ce∩da∩t5tree)

第l4章页面智能解析

732

这样得到的结果就是合并后的组节点了,结果变成图l4ˉ20所示的这样° 冈p禽日F阅体∏川■▲饥姑■■成及汽*.∩铂干饥文人Ⅲ■■■■嫩臼产巾■彼订·涣书■n■用史多≡ 兔白叮■.■行证蛋n}…■■

姻易…滚动新闻{ 瓦Q

必从苑「『

阴内

侗h忙会 ■【仔『‖

翻分罚岗出

巴全∏

巴■内 .

□■际

□仕会

m∩咽【■吓■m■」| 评诧栅宛审4

征屈m宁

■≡八,口 冈吝

′h史保宏恿片

讫尔日■

■仅

公口

赞坷

Ⅷ■l葬·q!鳃又Z$

雨魔0.〖

~二gα◆吵…



=巴9乌旦凹哩

·『闭内『

爱尝:翻

ˉ [■内‖

·{■内]

甲■

□评论

°〔阻内》



·‘徽|

m鳃}…}蘸』酮■p{薄}画…□|凹甘毋彰b乎动睡

· }阻内l

磅βm酮

固撅索 …■〗6〗幽谰

曰邓宇

o ‖研内!

□■片

·|田内‖

…O…》

·‖■闪[

…■06↑7… 凸

』■

□祝m

…白0竹l翻



·!■肉〗

产趟司

·{儡内『

o『■内】

-

…』●〗“`:鳃

·旧内》

盟隐|鳃

台{田内】

…■〗00狰蜘

{■内I

F



…岭…… …衫…

·!田肉

-

.‖簿内]

硒…

‖旧内‖

…1c……

:蜀:嚣 窒鳖蹦

·‖闭内] ·‖阂内]

. l腾内}

6.挑选最佳组节点

现在要从所有组节点中挑选最佳组节点’同样是依据多个指标计算各个组节点的分数。依据的指 标有很多’例如成员节点的数量、平均字数分布、文本密度等’分数的计算公式可以自行设计°下面

{|

图l4ˉ20合并后的组节点

实现_个挑选方法: de十〔∩ooseC1u5ter(〔1uSter5): 〔1u5ter55〔ore=de千a01tdjct(d1ct) C1U5ter55〔oIe一己Ig-‖ax≡O c1u5teIs5〔ore爬×= ˉ1

千or〔1l』5terjd’〔1u5teri∩c1l」5ter5.jte川5();

retur∩be5tC1uSter

这甲向〔‖oo5ec1u5ter方法传人上_步得到的合并后的所有组节点’然后eva1uatec1u5ter方法

会计算每一个组节点的得分’最后返回得分最高的组节点。 其中eγa1uate〔1u5teI方法比较关键’就是实现分数计算的方法,下面给出一个参考实现: de+eva1uatec1u5ter(C1u5ter): 5〔ore≡dj〔t()

5〔ore[|aγgˉsi|m1arjty-"ith51b1j∩g5』] =∩p。川ea∩( [e1e爬∩t。5im1arityˉwitb5ib1j∩gs十ore1e爬∏ti∩〔1u5ter]) 5〔ore「∩uⅧbero于e1e"e∩t50 ] =1e∩(〔105ter) 5〔ore[‖5ize‖] ≡getˉe1e爬∩t5ize(〔1u5teI 5〔ore[0〔1u5ter55〔ore,] = 5CoIe[`aγg-5加i1aIity-)0it∩51b1j∩85′]

|』□|□】|‖』■■司||』■■]|■■|』□‖」·|□■■■]‖|乙■■‖』■■』]』■■‖』■■■]■■■Ⅵ|

〔1u5ter55〔ore[c1u5ter1d] =eγa1uatec1u5ter(〔1u5ter) i十〔1‖5teI55〔ore[〔1u5terjd][ 』〔1u5ter55〔ore!] 〉〔1usterss〔oIe阳×: 〔1U5terS5〔Ore川aX≡〔1U5ter55COre[〔1u5ter-jd][‖C1USter55〔ore‖] 〔10sters5core-arg="ax=c1usterid be5t〔1u5ter=〔1o5ter5[〔1l」5ter55〔ore=ar8-们ax]

.}



l45

列表页智能解析算法的实现

733





*∩p.1o810(score[|∩u‖涯rofe1e‖记∩t50] +1) *∩p。1og10(5〔Ore[|5jZe』])



Ietur∩5COre



b户



可以看到’这个方法根据成员节点的相似度`数量和节点大小计算出了组节点的分数° 拿上面的案例来说,由于标题列表中的组节点对应的成员节点数量更多`节点更大,因此最终得 到的分数也更高’自然就被选为最佳组节点了。

仿



P



P

7.提取标题和链接

最佳组节点已经选出来了,如果这个结果是正确的,那么其每个成员节点里就包含着我们想要提 取的标题和链接。







广

p



p p



例如在上面的例子中’-个成员节点就是—个11节点,其源码为: <1i〉

〈5pa∩〔1a55≡倒right十12px〔Ⅲmy"〉(1o2oˉo8ˉ17o4:46:38)〈/spa∩〉 〈a∩re十≡口http://∩ew5。163.〔o∏}/dα爬5tj〔/"〔1as5=口〔B1ue阅〉[囚内]</a〉 〈a‖Ie千=圆∩ttp5://∩e切5.163.〔咖/2o/0817/04/「盯4"‖〕呕18990.ht川1"〉陕凸汉中略阳县主成区拉淹当地启动一组 应总钧应〈/a〉 〈/1i)

可以看到这个1j节点内包含_个5pa∩节点,两个a节点,其中第二个a节点才包含我们想要提



取的标题和链接信息°我们怎么用算法自动提取1j节点内的第二个a节点呢?同样可以根据一些特



征,这里最明显的特征就是字数了。





字数有什么规律呢?经过大量统计’可以发现标题长度是满足高斯分布的’其概率密度函数为:



′|列ˉ凉蹿,(—粤|







如果我们把这个概率密度函数应用到标

题长度分布上,那么〃就是标题长度的均值’

0

p

O就是标题长度的标准差°这里为了拟合一个

0

广

较为合适的概率密度曲线’经过调优’〃取了











26’O取了6’拟合的概率密度曲线如图142l 所示°

0

骨 庭0

根据图l4ˉ2l中的曲线’标题长度为26的

0

概率最高,随着标题长度的减小或增大’概率

0

会逐渐减小。当长度小于5的时候,概率已经

o

肠随佃仍恤川"





‖〖

h

p

趋近于0,事实上一篇新闻的标题少于5个字









p



p

0

的概率确实非常小°通过这个方式,我们能够 筛选出更贴近于标题的节点,例如上文中第二 个a节点计算得到的概率就比第_个a节点的 概率更大’因此会倾向于选择第二个a节点。

P





l0

l5

20

25

3O

35

q0

45

长度

图l4ˉ21 标题长度分布的概率密度曲线

当然,仅仅依靠单个节点计算还是不够科学’目 当然,仅仅依靠单个节点计算还是不够科学’更优的方法是针对整个组节点内所有可能的同类a 节点’计算总体置信度’然后选出第二个a节点对应的选择器路径,再统一按照这个路径查找标题° 根据标题长度获取置信度的方法实现如下: de+probabj1ity-o千-tjt1eˉmthˉ1e∩8th(1e∩gth): 5jg阳=6



5

Ietur∩∩p.exp(ˉ1* ((1e∩gthˉaγg-1e∩8t∩〉**2)/(2*(sjg阳**2)))/(爬th.5qrt(2*∩p.pj)*5i8阳)



734

第l4章页面智能解析

这里其实就是实现了高斯分布的概率密度函数’接收标题的长度值,返回对应的概率。借助这个 方法,可以实现标题的提取逻辑: de十extm〔tC1uSter(〔1l」5ter): #寻找标题的迁优节』点略径

probabj1jtie5o十tit1e≡de千au1tdi〔t(1i5t) 于Ore1e∏归∩tj∩C1u5ter:

de5〔e∩da∩tB=e1e‖论∩t.adeS〔e∏d己∩t5 「oIde5〔e∩da∩ti∩des〔e∩d己∩t5:

path=de5ce∩da∩t.path de5〔e∩da∩ttext=de5Ce∩da∩t.teXt

pmbabi1ity=o+-t1t1e"1t‖=1e∩gth≡probabj1ity=o千-tit1e切ith-1e∩gt‖(1e∩(de5ce∩da∩ttext)) probabi1ity-o十-tit1e=probabi1jty-o十-tit1ewit‖-1e∩gt‖ probabj1jtje5oftit1e[pat∩].appe∩d(probabj1ityˉo+-tjt1e)

probab11jties—o+-tit1e~avg={R: ∩p窿∩e己∩(v)「ork’vi∩prob己b111tie5o千tit1e.iteⅧ5()} j十∩otprob己bj1ities-of-tjt1e_aγ8: ret0r∩‖o∩e

be5tˉpath=‖ax(probabj1itje5-o十ˉtit1e—avg.jte∏5()’ 伐ey=operator.jte‖∏getter(1))[0] #根掂录优略径捉取内容 re5u1t≡ [] 十ore1e∏旧∩tj∩c1‖5ter: de5〔e∩d己∩t5=e1e‖论∩t.己deS〔e∩d己∩t5 十orde5〔e∩da∩tj∩desce∩da∏t52

path=de5〔e∩da∩t.p己t向 i+path !=best-path: 〔o∩tj∩ue t1t1e=deS〔e∩d己∩t.text

0r1=de5Ce∩d己∩t.at↑rib.get(0∩Ie+‖) re5u1t。appe∩d({ 0tjt1e0 : tit1e’ 0ur10 : ur1

}) ret0r∩Iesu1t

这里我们定义了一个extra〔t〔1u5ter方法,参数是组节点的信息,遍历它可以得到所有成员节

点°对于每个成员节点,提取其所有的a节点,然后根据probabi11tyˉo十ˉtjt1eˉmt∩—1e∩gth方法计 算出每个a节点可能是标题的概率,同时记录这个a节点相对成员节点的节点路径°接着根据上一步

计算得到的置信度找出最优的a节点路径’即best-path。最后根据best-path去成员节点里提取标题 和链接’并组成一个列表返回。

至此’我们完成了列表页的提取。

8.整合

由于整个提取算法实现起来比较复杂’所以上述内容简化了部分代码逻辑。我已经将整个提取算

法封装成了一个完整的Python包,大家可以直接调用’感兴趣的话也可以查看其源码。

这个包叫作GerapyAutoExtIactor’在143节已经介绍过’可以通过pjp3工具安装: pip3j∩5ta11gerapy-autoˉe×tr日ctor

安装完成后便可以导人使用,调用流程非常简单: 十I咖8empy=己uto-extra〔toImportextract1i5t 十r咖gerapyˉautoˉextr己ctor.∩e1per5mport〔o∩te∩t’ j5o∩j十y ∩t∩1=〔O∩te∩t(!1j5t.∩m10)

pri∩t(j5o∩i于y(extIa〔t1j5t(ht‖1)))

这里调用〔o∩te∩t方法读取了列表页的HTML代码’调用extr日〔t11st方法提取了列表页的内

容’并调用j5o∩j十γ方法对提取结果进行了格式化’运行结果如下:

·■』■■】】』■■』■】』□】■】‖●〗·】】·Ⅲ■】〗■』■■‖|■〗』|||·|‖|‖』‖|‖』●】引‖‖』■】{《

#寻找最可能的节点略径

} ■ 尸 | | 匹 ■ ■ 「 》 匹 ■ 厂

| l46如何智能分辫列表页和详情页

735

■「||β△尸



「}

"tit1e阐: 。进入职业大沈牌时代, “吃杏蜀职业还吃杏吗? "’ "0r1": 伺 http58//∩e"·qq°c咖/o‖‖∩/2o210828/2o21o828∧o25[偶oo.ht∏1 " }’



β 卜 『

"tjt1e园: "他,活出了我们理想的抨于"’ 闻ur1口: 回∩ttPs://∩ew。qq。〔咖/咖∩/2O210821/2021O821AO2OIDOO.ht们1 " }

可以看到’示例列表页的标题和链接被提取出来了。

另外,GerapyAutoExUactor包在很多细节上对提取算法进行了优化,大家可以查看其说明来了解 更多用法’也可以直接查看源码来详细了解本节介绍的实现流程。



9.总结

本节中我们介绍了列表页提取算法的代码实现,同样无须任何规则’经过_定的算法和节点结构 ■『}▲尸

分析后便可以得到想要的新闻列表数据°

本节代码见https:〃github.com/Gerapy/GerapyAutoExtractor。 ■尸卜△■『□广

↑46如何智能分辨列表页和详情页 在前面几节’我们介绍了详情页和列表页的内容提取方案,传人对应的HTML代码就能获取对应 的提取结果了。但试困有个问题,就是在调用提取方法之前,需要先分辨哪种页面是列表页,哪种是 详情页。

0

这自然而然引出了_个问题:能否用_个算法来区分列表页和详情页,直接根据算法返回的结果 调用对应的提取方法,从而省掉很多麻烦?

p

‖.本节目标

■「■『=■■)》厂

本节中我们需要设计_个算法来自动区分列表页和详情页’要求是传人-个页面的HTML代码’ 然后返回分类结果和对应的分类概率。 下面我们就来了解其基本思路和算法实现吧。

■‖~■



2问题分析

我们首先分析这个问题属于什么问题°其实很明显’既然要返回分类结果(要么是列表页,要么 是详情页),那么就可以把它归为二分类问题°

b

=■卜β「「 |巴厂「■仑■厂





注意这里我们不考虑特殊页面(例如登录页面、注册页面等),并把非列农页和详情页的页面一 律归为其他页面,如果把此类页面也考虑进去,本节的问题就是三分类问题,在此为了方 使,仅分析二分类问题°

二分类问题怎么解决呢?实现一个基本的分类模型就好了’大范围是传统机器学习和现在比较流 行的深度学习。总体上讲’深度学习的精度要高—点,处理能力也强-点°想想我们的应用场景,要 追求精度的话可能需要更多的标注数据,而我们也有比较不错的易用模型’例如SVM° 所以,不妨先用SVM模型实现—个基本的二分类模型试试看,如果效果已经很好或者提升空间 不大了’就直接用;如果效果比较差’再选用其他模型做优化。



||

736

第14章页面智能解析

a数据标注



既然要实现分类模型,最重要的当然是数据标注,这里分两组数据,-组是列表页数据,-组是 详情页数据°先手工配合爬虫找一些列表页和详情页的HTML代码(例如新闻网站`博客网站的列表 页和详情页,覆盖的网站越多’训练得到的分类器就越准确)’然后将它们保存下来° 经过一些收集和处理,将列表页和详情页的HTML代码保存在两个文件夹中,分别取名为list和 detail’结果类似图l牛22这样° ●二◆

|=由t瓣尸0荤

回■《疆≡皿息{画…诬画甄熏 馆改日期

骡 种矣

大小

≡-—…-≡≡≡= ■=龟■_■牡…=~■—~≡=罕审嘻≡=-≡→邯巴……—=己-→→韩←一==…■=■≈_△

广■detSj‖

2om年7月『旧o!:2O

v■t语t

之O2O郧y月〗日0血0∑

ˉˉ

◆aS『「O=S‖∩己工◎皿c∩ht丽‖

2O∑O年7月γ↑日O↑『56

347瓜B

Oaut◎=chk旧=C叼ht们‖

2O2O年7月↑‖日O2‖O‖

736ⅨB

川丁M儿文卒

35 低已

"丁M止文本 ‖丁酚L文本



autoˉl「0eW础eek尸『l}》tm{

20∑O年7月]旧田:』6

●■u↑q功心ˉc们∏ˉm=~mdexˉdˉhm‖ˉhm‖

2O2O年7月"日0‖:56

246RS

·■ut◎←s‖∩日ˉc◎↑VLc∩bt∏l|

∑02O年7月!旧00巳56

230xB

例丁川L文本

●础loˉS◎们UˉCmL?s…93mW∩0十↑『9∩m|

202O年7月们日O↑:42

293袄巳

片丁仙[文本



202o年7月7↑日oM2

292长B

"丫ML彝

2o20年7月]旧o↑t58

84Ⅵ偶B

日丁ML文本

矽ba『duDeWShtm‖

202a年7月7旧0O:45

263Ⅸ已

川丁M止文本

●吐‖Zh0‖旧oCO『∏吼■…危吨.ht刷

202O年7月】旧O↑;蛆

↑g0长B

问丁腆L文本

■田oα8MaˉC◎mˉC∩=‖m]j拭◎『yˉhm〗0

2O2O年7月们日0↑:“

027低S

∩丁佃L彝

Z…~z‖Eˉc◎皿cn∩m‖ ◆bo◎此C‖`HˉCO∏LC∏.ht刚

202O年7月↑旧0〗;56 2O2O年7月`旧9↑:68

乞O5风S

"丁川L文本

2“狐a

∏丁ML文本

■刨画hoas-so↓N上c◎…68672挝O旧Cγ8htm|

2O2O年γ月↑7曰O!吕q2

↑89长B

‖丁ML文本

●~

●●M℃肆SQh止c◎爪h师|

●mbⅨˉSmaˉCo∏Lc∩hm‖

园「t◎◎『L狮eⅦ”e●■ˉcn∩tm‖

oC∩O"‖治‖cⅧ∩■岭W…g谷c恤ˉDht刚.hm‖

m20年了月↑旧O↑;d6

a9低日

扦了川L文本

2mO年7月↑↑日o↑:▲5

γ3民B

"丁州L文本

≡……=~

◆c∩a∩∩e〔ˉchj∏■∩●w…上『忘C9『xˉ劝tm‖画bt∏l‖

rO20年7月↑旧0↑:月6

70促B

柠丁仙止文本

●广h■■(●向●;冠■^^m伪0而〗

勺∩T∩午丁口■0口∩勺.胁

罚∩∩∏■

凹丁u』0仓★

图l牛22

-

文件央 文…

list文件夹和detail文件夹

每个文件夹里保存几百份HTML代码就行了,不用太多。接下来从这些代码里提取特征’然后实 现_个二分类模型。

4.特征提取

·‖■√‖‖·】·■〗』‖■〖】·|||」』■■‖】■|·γ|』■〗■■]』』■■■〗‖』■■‖勺】‖‖■君』】|』■】司|·|』■■·】』】■‖司|‖二■]·|□』■‖』·‖|



名称

选用SVM模型,首先得想清楚-件事:要分清两个类别’需要哪些特征。既然是特征’就要选 出各自独有的特征’才更有区分度°

这里总结了几个可以用来区分列表页和详情页的特征。

□文本密度:详情页通常包含密集的文字’例如_个p节点内部就包含几十甚至上百个文字,如 果用单个节点内的文字数量表示文本密度,那么详情页的文本密度会很高° □超链接节点的数量和比例:列表页通常包含多个超链接’而且有很大_部分是超链接文本;详 情页则包含更多的文字’超链接很少。

□正文标题和tjt1e内容的相似度:_般来说,详情页的正文标题和t1t1e内容很可能相同,而 列表页的t1t1e内容通常是网站名称°

以上便是几个基本的特征’此外其他一些特征也可以自行挖掘并使用,例如视觉信息、节点大小°

『■匹

则通常没有°



□符号密度:列表页通常相当于标题导航页,很少包含句号’而详情页的正文内容普遍包含句号, 如果用单位文字包含的句号数量来表示符号密度’那么详情页的符号密度会很高° □列表簇的数量:列表页常常包含多组具有共同父节点的条目,多个条目构成一个列表簇°虽说 详情页的侧栏也会有_些列表,但至少这个数量和列表页的相比,是可以分清的° □爬ta信息:有—些特殊的"eta信息是列表页独有的’例如详情页往往包含发布时间,列表页

l46如何智能分辨列表页和详情页

737

5.模型实现

代码实现的过程就是对现有的HTML代码做预处理’提取出上面的基本特征,然后声明一个SVM 分类模型。先声明_个特征列表和特征对应的获取方法: 5e1+·于eatuIe千0∩〔5≡{ 0∩u‖∏bero千a〔har‖: ∩uⅦbero十a〔h己I’

0∩l』帅ero「ac∩ar-1og10’8 se1千. ∩u∏〗bero于ac∩ar-1og1o’ ∩u‖beIo「〔bar{ : ∩0们bero千〔∩ar’

∩uⅧbero十c∩ar-1og1O′8 5e1千。∩u们bero十char=1og10’ 0rateo十a〔har′: 5e1+. r日teo千a〔haI’ !∩u恤er=o+一p一de$ce∩da∩ts0 目 ∩u团ber-o+-p-desce∩da∩ts’ 0∩u们beIo+ades〔e∩da∩t50 8 ∩u团beIo+ade5〔e∩da∩t5’

∩Ⅷber≡o+≡pu∩ctuatio∩0 : ∩u恤er≡o十-pu∩ctuatjo∩』 ‖de∩5ity-o十=pu∩〔tuatjo∩0 : de∩5jtyˉO十-pu∩ctuatjo∩』



| |

0∩u『∏beIo于〔1u5ter5 : 5e1千. ∩u用beIo于〔1u5ter5’

0de∩s1ty-o+-text! ; de∩5ity=o千-text’ Ⅷ日xde∩5ity-o+≡text0 : se1十.吨xde∩5ity-o千-text』 帕×∩u↑∏ber-o+一p=chi1dre∩ : 5e1「·刚己x∩u"ber-o+-p-c∩11dre∩』 0门a5dateti贬‖eta0 ; 5e1十. ha5datetj爬们at己』

05j爪j1日rity-o+-tjt1e‖ 8 se1千。 5jm1arjty-o于-tit1e’ } 5e1十.「eature∩3爬5=5e1十.十eature「u∩〔5。keys()

然后就是关键部分

处理数据和训练模型:

}广|●■

1j5t「i1e-pat们s=1i5t(g1ob(+|{D∧丁∧5[丁5lI5丁DIR}/*.∩t刚1《)) detai1十j1eˉpath5=115t(g1ob(+’{DA丁A5[丁S0[丁∧IlDI【}/*.‖tⅧ1‖)) xdata’yˉdata= []’[]



于oIi∩dex’1i5t十j1eˉpathi∩e∩u∏论Iate(1j5t千i1e-pat∩5): 1og8er。1og(`j∩5pect|’ 十01ist十i1e~path{1i5t+i1e-path}!) e1e爬∩t≡十j1e2e1e们e∩t(1i5t千i1eˉpath) jfe1e∏记∩tj5"o∩e:

P

CO∩tj∩ue

prepro〔e5541j5t〔1己55i十ier(e1e∏记∩t) x=Se1千.千eature5tO1i5t(5e1十。千e己ture5(e1e爬∩t))





xdata。apPe∩d(x) yˉdata.appe∩d(1)



| b



千ori∩dex’ detaj1+j1e-pathj∩e∩u′∏erate(detai1fj1e-p己tb5):

1ogger。1og(′i∩5pect! ’ 十0detaj1「i1e-pat∩{detaj1+i1e-pat∩}‖) e1e爬∩t=千j1e2e1e∏e∩t(det己i1十j1e-path)

广

j千e1e眶∩ti5‖O∩e: co∩ti∩ue

b

p工eprO〔e5541j5tC1日55j十ieI(e1e‖记∩t) x≡5e1+.+eaturesto115t(5eM。千eat0Ie5(e1e爬∩t)) xdata·appe∩d(x) y-data.appe∩d(o)

p■





|‖‖炉「■■尸)



#预处理数掂

5S≡5ta∩dardSCa1eI()

xd己ta=55.千jttra∩5千om(x-data)

job1jb.du呻(55’se1十。5ca1erˉpath)

xtr日1∩’ xte5t’ yˉtraj∩’ γˉte5t=trai∩testˉ5p1it(xˉdata’ y—data’ te5t5ize=o.2’ra∩d咖5tate=5)



#设ⅢCrjd5eaICh

〔ˉm∩ge≡∩p.1og5pace(ˉ5’ 2O’ 5’ b日se≡2) 83m旧ˉIa∩ge=∩p.1o85pa〔e(ˉ9’ 1o’ 5’ ba5e=2) paraⅦˉgrid二 [

{0代er∩e1! : [0rb十‖]’ ′〔‖ ; Cˉm∩ge’ !8am日 8 g己ma-r己∩8e}’ {,代er∩e1‖: [‖1j∩ear』]’ ‖〔0 : 〔ˉm∩ge}’



grid=6rjd5eaI〔h〔γ(Sγ〔(pIobabi1ity=丁rue)’ pam"ˉgrid’ cγ≡5’ γerbo5e=10’∩-job5二ˉ1) C1「=grjd.+1t(Xtraj∩’ y-traj∩)

|■厅■||卜『』▲■【■『‖||~■■■■■

yˉtrue’ y-pIed=y一te5t’〔1十。predjct(x_te5t)



738

第14章页面智能解析





#保存模型

job1ib.du呻(grjd.be5te5tmator ’ 5e1十.晒de1-path)

这里首先对数据做预处理,将特征保存到xdata中,将标注结果保存到y-data中°接着使用 5ta∩d日rd5〔a1eI对数据进行标准化处理’并进行随机切分°最后使用GrjdSearch训练了一个SVM模 型并保存下来°

●】‖』□】Ⅶ‖〗‖‖]■■‖‖■]‖]|‖‖叫

1ogger.1og(}j∩spect‖’ +|\∩{c1a5si千i〔atio∩-report(y=true’ y-pred)}0) B〔ore=grjd.5core(×-te5t’ y-te三t) 1o8geI。1og(0i∩5pe〔t!’「|testac〔uracy{5core}‖)

以上便是基本的模型训练过程’具体代码可以自己再完善_下。 6.使用

将保存的模型用于分类处理就好了°我已经把使用流程放在GerapyAutoExmctor包里面了,大家 可以使用pip3工具安装:



』·∏■□

pip3j∩5ta11gerapyˉautoˉextm〔tor



这个包针对于以上算法提供了4个方法。 □i5detai1:判断_个页面是否是详情页° □151j5t:判断一个页面是否是列表页°

□probabj1ityˉo十-deta11:一个页面是详情页的概率’返回结果是O~1° □probabi1jtyˉo于ˉ1i5t:_个页面是列表页的概率,返回结果是O~1。





例如,随便找个网址,把列表页和详情页的HTML代码分别保存为listhtml文件和detailhtml文 +ro‖‖gempy-己uto-extm〔tormportj5-det3i1’ j5-1i5t’ prob己bj1ity-o+-detaj1’ probabj1ity=o于≡1i5t +ro∏‖geIapyˉautoˉextractoI.∩e1per5 i肌portco∏te∩t’ j5o∩j十y

∩t川1=〔o∩te∩t(,detaj1。hm1|)

pⅢi∩t(probabi1ity=ofˉdetaj1(‖tⅧ1)’ probabi1ityˉo仁1jst(∩tⅦ1)) pri∩t(j5-det日j1(hm1)’ i5-1i5t(htⅦ1)) ‖t‖1≡Co∩te∩t(!1iSt。ht‖1‖)

prj∩t(pIob己bj1ity-o十=deta11(bt们1)’ probabj1ity-o十-1j5t(ht∏1))

(』■」』■〗』]||』■■·』勺√|』可|」』■γ‖」(

件°然后用如下代码做测试:





prj∩t(j5ˉdetaj1(htⅦ1)’ j5ˉ1i5t(ht"1))

这里就调用上述4个方法判断了两个页面的类型和置信度° 运行结果如下: 0。999O6O5〕1仰333920·咖9394685966607814 丁me「a15e

0.033q77426883“168SO.9665225731165583

d

「a15e『Iue

7.总结

本节介绍了判断页面是列表页还是详情页的原理和代码实现,如需了解更多细节,可以参考 GeIapyAutoExtmctor项目的源码°

本节代码见ht印s:〃gjthub.com/Gerapy/GerapyAutoExtractor。

至此,我们完成了详情页和列表页的内容提取以及详情页和列表页的分辨’有了这三类算法’就 可以完成大部分新闻页面的智能解析了°

勺|||」』■」■Ⅵ|‖』』■■■{|‖」‖■■|||」』■■』■|■■∏‖`

可以看出’我们得到了正确的页面类型和置信度。







■■■】■■■■【■卜■尸

第】5章 |』

Sc「apy框架



■‖



坠≥卜仿「



=尸■尸‖匹=尸‖|■β『‖■「| ▲ 广 「 ‖ ■ 厂 ■ ■ 『 [ 甘 「



『「β|》 ■「||■尸|‖〖■尸口『|伊|巴■■◆『

[ ■△■【伊|》|■=尸■■「‖■■厂▲β●尸广|‖■尸)■=|』卜》■「∩卜■尸 ‖)





前面的章节给大家展示了很多案例’其中大多实现了爬虫的整个流程,将不同的功能定义成不同 的方法’甚至抽象出模块的概念°比如在95节’我们已经有了爬虫框架的雏形,实现了调度器`队

列、请求对象、异常重试机制等,如果我们将各个组件独立出来,把它们定义成不同的模块,其实也

就慢慢形成了_个框架。有了框架之后,我们就不必关心爬虫的流程了’异常处理、任务调度等都会 集成在框架中°我们只需要关心爬虫的核心逻辑即可’如页面信息的提取`下-步请求的生成等°这 样’不仅开发效率会提高很多,而且爬虫的健壮性也更强。

9.5节的实现算是_个爬虫框架的雏形’但其距离一个标准的爬虫框架还很远。我们要以它为基

础,继续完善,编写-个爬虫框架吗?可以是可以’但是没必要°因为Python爬虫生态圈中已经有个成熟、稳定且强大的爬虫框架了’它就是ScIapy°

↑5.↑

Sc「apy框架介绍

ScraPy是一个基于Python开发的爬虫框架’可以说它是当前python爬虫生态中最流行的爬虫框 架’该框架提供了非常多爬虫相关的基础组件’架构清晰,可扩展性极强。基于Scrapy,我们可以灵 活高效地完成各种爬虫需求。

本节会首先介绍SCrapy框架的基本架构和功能° ↑.简介

在本章之前,我们大多是基于requests或aiohttp来实现爬虫的整个逻辑的°可以发现,在整个过 程中,我们需要实现爬虫相关的所有操作’例如爬取逻辑`异常处理、数据解析、数据存储等,但其 实这些步骤很多都是通用或者重复的°既然如此’我们完全可以把这些步骤的逻辑抽离出来,把其中 通用的功能做成-个个基础的组件。

抽离出基础组件以后’我们每次写爬虫只需要在这些组件基础上加上特定的逻辑就可以实现爬取 的流程了’而不用再把爬虫每个细小的流程都实现一遍°比如说我们想实现这样_个爬取逻辑:遇到 服务器返回403状态码的时候就发起重试’遇到4叫状态码的时候就直接跳过°这个逻辑其实很多爬 虫都是类似的,那么我们就可以把这个逻辑封装成一个通用的方法或类来直接调用’而不用每次都把 这个过程再完整实现一遍,这就大大简化了开发成本,同时在慢慢积累的过程中,这个通用的方法或 类也会变得越来越健壮’从而进_步保障了项目的稳定性,框架就是基于这种思想逐渐诞生出来的。

注: ScIapy框架几乎是Python爬虫学习和工作过程中必须掌握的框架’需要好好钻研和掌握° 这里给出ScIaPy框架的一些相关资源,包括官网、文档、GjtHub地址’建议不熟悉相关知识的 读者在阅读之前测览-下基本介绍° .

□官网: https://scrapyo【g/° □文档: https://docs.scrapy.o【g/。

「|05 `■■■■■

||

740

第l5章Scrapy框架的使用

□GitHub: https://gjthub.com/scrapy/scIapy° 2架构

说了这么多, scrapy框架的功能到底强在哪里?组件丰富在哪里?扩展性好在哪里?不要着急, 首先从整体上看一下Scrapy框架的架构,如图15ˉl所示° ■■■m仪酗厕[

卯!旺Rs

一-

『ˉ——-

}晒T圃鸥『

图l5ˉl

Scrapy框架的架构

图l5ˉl来源于Scrapy官方文档’初看上去可能比较复杂,下面我们来介绍-下°

□Engjne:图中最中间的部分,中文可以称为引擎’用来处理整个系统的数据流和事件’是整个 框架的核心,可以理解为整个框架的中央处理器,负责数据的流转和逻辑的处理。

□Item:它是-个抽象的数据结构’所以图中没有体现出来,它定义了爬取结果的数据结构’爬 取的数据会被赋值成Item对象。每个Item就是—个类,类里面定义了爬取结果的数据字段’ 可以理解为它用来规定爬取数据的存储格式。 .

□Scheduler:图中下方的部分,中文可以称为调度器,它用来接受Engine发过来的Request并 将其加人队列中,同时也可以将Request发回给Engine供Downloader执行’它主要维护 Request的调度逻辑,比如先进先出、先进后出、优先级进出等等°

□Spjders:图中上方的部分’中文可以称为蜘蛛,Spjders是一个复数统称’其可以对应多个Spider, 每个Spider里面定义了站点的爬取逻辑和页面的解析规则’它主要负责解析响应并生成Item 和新的请求然后发给Engine进行处理° □Downloader:图中右侧部分,中文可以称为下载器’即完成“向服务器发送请求’然后拿到响



纠‖|□」·■口|■■

Downloader之间的Hook框架’负责实现Downloader和Engjne之间的请求和响应的处理过程°



中间件,同样这也是复数统称,其包含多个DownloaderMiddleware,它是位于Engine和



应,,的过程’得到的响应会再发送给Engine处理° □ItemPipeljnes:图中左侧部分’中文可以称为项目管道’这也是一个复数统称’可以对应多个 ItemPipelme° ItemPipeline主要负责处理由Spider从页面中抽取的Item,做~些数据清洗` 验证和存储等工作’比如将Item的某些字段进行规整’将Item存储到数据库等操作都可以由 ItemPjpeline来完成° □DownloaderMiddlewares:图中Engme和Downloader之间的方块部分,中文可以称为下载器

·|』■可』‖匀]·】□■‖‖□■|』●Ⅱ||ˉ』』□〗|」叫‘』司‖』■〗』■]□■■‖·‖■|‖|』■]』‖」■]·‖』·】■划』·|」■∏」·′‖‖{□]|■』可||』‖‖■□‖||■]」□]‖|』□‖《·]■■』■《■■■·]■■□

本章后文会逐-讲解它的功能和各个组件的用法。

0

■▲砂巴

尸 ‖ ■ 卜 ‖

})|} 『[■〗‖‖‖|出尸|■厂■〖≥『『|』[●「二■厂

■厂|》□‖「》尸



巴■「∩|巳『巴尸『

》「|怔‖■厂‖乌



| | 巴 尸 ‖ 〖 ∩ 「 ‖ ■ 『 门 巴 尸 | ▲ 『 | 『 ■ 厂 》

| }





l5』 Scrapy框架介绍

74l

□SpiderMiddlewares:图中Engine和Spiders之间的方块部分,中文可以称为蜘蛛中间件’它是

位于Englne和Spiders之间的Hook框架,负责实现Spiders和Englne之间的Item`请求和响 应的处理过程°

以上便是Scrapy中所有的核心组件,初看起来可能觉得非常复杂并且难以理解’但上手之后我们 会慢慢发现其架构设计之精妙’后面让我们来一点点了解和学习。 3。数据流

上文我们了解了Scrapy的基本组件和功能’通过图和描述我们可以知道,在整个爬虫运行的过程 中,Engine负责了整个数据流的分配和处理,数据流主要包括Item、Request、Response这三大部分, 那它们又是怎么被Engine控制和流转的呢? 下面我们结合图15ˉl来对数据流做-个简单说明°

(l)启动爬虫项目时,Engine根据要爬取的目标站点找到处理该站点的Spider’Spidcr会生成最初 需要爬取的页面对应的_个或多个Request,然后发给Engme°

(2)Engjne从Spider中获取这些Request’然后把它们交给Scheduler等待被调度。 (3)Engjne向Scheduler索取下_个要处理的Request’这时候Scheduler根据其调度逻辑选择合适 的Request发送给Engine。

(4)Engine将Scheduler发来的Request转发给Downloader进行下载执行,将Request发送给 Downloader的过程会经由许多定义好的DownloaderMjddlewares的处理°

(5)Downloader将Request发送给目标服务器,得到对应的Response’然后将其返回给Engine。 将Response返回Engjne的过程同样会经由许多定义好的DownloaderMjdd‖ewares的处理°

(6)Engine从Downloder处接收到的Response里包含了爬取的目标站点的内容, Engine会将此 Response发送给对应的Spider进行处理,将Response发送给Spider的过程中会经由定义好的Spider Midd‖ewares的处理°

(7)Spider处理Response’解析Response的内容,这时候Spider会产生-个或多个爬取结果Item 或者后续要爬取的目标页面对应的-个或多个Request’然后再将这些Item或Request发送给Engjne

进行处理’将Item或Request发送给Engine的过程会经由定义好的SpiderMiddlewares的处理°

(8)Engine将Spider发回的_个或多个Item转发给定义好的ItemPipeljnes进行数据处理或存储的 -系列操作,将Spider发回的_个或多个Request转发给Schedulcr等待下_次被调度° 重复第(2)步到第(8)步’直到Scheduler中没有更多的Request’这时候Engjne会关闭Spider’ 整个爬取过程结束°

以上步骤介绍了爬虫执行过程中的数据流转过程’起初看起来确实比较复杂,但不用担心’后文 我们会结合一些实战案例来慢慢理解这些过程。

从整体上看来’各个组件都只专注于一个功能’组件和组件之间的藕合度非常低’也非常容易扩

展。再由Engjne将各个组件组合起来’使得各个组件各司其职’互相配合,共同完成爬取工作°另外 加上Scrapy对异步处理的支持,Scrapy还可以最大限度地利用网络带宽’提高数据爬取和处理的效率。 4项目结构

了解了Sc〖Hpy的基本架构和数据流过程之后’我们再来大致一看下其项目代码的整体架构是怎样的° 在这之前我们需要先安装Scrapy框架’一般情况下’使用pip3直接安装即可: pjP31∩5t己115〔I己py

但Scrapy框架往往需要很多依赖库’如果依赖库没有安装好,Scmpy的安装过程是比较容易失败



第l5章Scrapy框架的使用

■■

742

‖‖

的°如果安装有问题,可以参考https:〃setupscrape.centeⅣscrapy里面的详细说明° 安装成功之后,我们就可以使用scrapy命令行了’在命令行输人5〔rapγ可以得到类似如图l5ˉ2所 ‖‖

示的结果° ● ● ● ~巴〔7opy 5C C沪opy2°5.0ˉ∏o口〔twep广o〕e〔t



05 5oge:

二cmpy<〔Ol?m□∩』≥[oPtio∩5][o沪g田]

De∩〔‖

Ru∩qui仁kbe∩〔‖Ⅶm厂kte5t

〔…□∩dS

MoFe〔o′咖α咽5ovojlob[ew∩e∩广0∩什o冈口厂oje仁td1「0e吐o7y

‖」

[刚o广e] U 5e

0

日」

「et〔∩ 「et仁h◎U∩Lu■i∩gt们e5cF◎pyd叫∩1oαde广 ge∏Spjde庐 6e∩e厂αte"e洲Spide厂u51∩gp了e~de「t∩edte耐p【α亡e写 「u∩spide厂 Ru∩o5e1「ˉco∩t□t∩e口”iαeF(wi恤◎utc庐e□t1∩gop!q◎ject) sett1∩g5 6et5ett1∩g5V口1ue5 5‖eIl . I∩te厂◎CtiγeS〔尸op1∩gc◎∩5O1径 St□下tp厂O〕ect 〔厂e□te∩eⅣp厂O〕eCt γe广s1O∩ P「[∩t5〔r□pyγ巴了S1O∩ γ1ew 0pe∩URL i门bPo∏se厂, os5ee∩bγ5c厂oPy

‖ ‖ |

Aγαi1αble〔o啊妇洲d5弓

‖05c「opy<co瞒m∩α>ˉh00 tosee门◎Pet∩「o◎boutocα呵□∩d

』‖■川

图15ˉ2运行结果

Scrapy可以通过命令行来创建_个爬虫项目,比如我们要创建_个专门用来爬取新闻的项目,取 5cmpy5tartproject∩印5

这里我们使用5tartproject命令加上项目的名称就创建了_个名为news的Scmpy爬虫项目°执行 完毕之后,当前运行目录下便会出现_个名为news的文件夹,该文件夹就对应一个Scmpy爬虫项目°

5cmpyge∩5p1der5j∩己∩ew5°5i∩a°〔m。c∩

这里我们利用ge∩5pjder命令加上Spider的名称再加上对应的域名’成功创建了-个Spjder’这 个Spider会对应—个Python文件,出现在项目的spjders目录下°

广栏湍翁”

匡蹦. spjdeIE

亡盂备w

|_scmpy.cfg

在此将各个文件的功能描述如下。

■■可|‖‖·」』■■■■‖

□scrapy.cfg: Scmpy项目的配置文件’其中定义了项目的配置文件路径、部署信息等° □itemsPy:定义了Item数据结构,所有Item的定义都可以放这里°

』勺凸日』勺』勺||‖■■」」■己■‖||‖』■■■■■』■■』■】‖||』·|】‖|

现在项目文件的结构如下:

{|

接着进人news文件夹’我们可以再利用命令行创建一个Spider用来专门爬取某个站点的新闻’ 比如新浪新闻’我们可以使用如下命令创建一个Spider:

d

|』』●■』■■||』‖』■■』勺】』』■|■■‖』司』‖‖|

名为news,那么我们可以执行如下命令:



‖√



l5.2

Scrapy入门

743

□PjPellnesPy§定义了ItemPjpeline的实现,所有的ItemPjpeline的实现都可以放在这里。 □semngs。py:定义了项目的全局配置°

□mjddlewarespy:定义了SpjderMidd‖ewares和DownloaderMidd‖ewaIes的实现。

|』 ||》||口卜}尸)/厂卜|[厂||■『‖卜|‖}广■「尸′ˉ}‖■『膀●「`吵′卜||尸^}尸 卜『|■‖巴尸『

厂|》「【尸||

‖■■‖尸||卜‖庐



□spiders:里面包含_个个Spjder的实现,每个Spjder都对应_个Python文件° 在此我们仅需要对这些文件的结构和用途做初步的了解’后文会对它们进行深人讲解。 5.总结

本节介绍了Scrapy框架的基本架构`数据流过程以及项目结构’如果你之前没有接触过Scrapy, 可能觉得本节的内容很难理解’这个很正常°

不用担心’后面我们会结合实战案例逐节了解Scrapy每个组件的用法’在学习的过程中,你会慢 慢了解到Scrapy的强大和设计精妙之处,到时候再回过头来看看这-节,就会融会贯通了°

↑5.2 Sc『apy入门 上-节我们介绍了ScIapy框架的基本架构、数据流过程和项目架构,对Scrapy有了初步的认识° 接下来我们用Scmpy实现一个简单的项目’完成一遍Scrapy抓取流程。通过这个过程,我们可以对 Scmpy的基本用法和原理有大体了解° ↑.本节目标 本节要完成的目标如下°

□创建_个Scrapy项目,熟悉Scrapy项目的创建流程。 □编写-个Spider来抓取站点和处理数据’了解Spider的基本用法° □初步了解ItemPipeline的功能,将抓取的内容保存到MongoDB数据库。 □运行Scrapy爬虫项目’了解Scmpy项目的运行流程°

这里我们以ScIapy推荐的官方练习项目为例进行实战演练,抓取的目标站点为ht卯s://quotes. toscrape。com/,页面如图15ˉ3所示°

k::·=ˉ

■| ★‖∩●

Ou◎testoScraPe

L呐

7砧…U■”恤0■α…际■■α…wα汀门凶…门…耐蚀…硒硒汹↑

ββ膛∩



…U″励…口 叮…铸〔…

…≈□■■■■■B四



"句″…′枷阴蛔…耐呵啪呐…烛……α〃…口

■尸‖)

叮d其…哟

…==



… 啊

陋 硒 … … …

□沁■臼■α叮硒…的№阿r蹿α泊句“0m聊…℃●■刃泊…°γ洒”■7 幻m蛔初…句□励…·

0

丁℃pTE∩tags

叮…≡…》

…、-雪■=… △■尸|『■『■■似『【『『匹◆■

·7洒…m∩…叮………Jm…勿■……∏西绅



向………· 叮…∩…《…

…;…………

图15ˉ3

目标站点

| |





■ ■■

〔15

|β||■■■■

7“

第15章Scrapy框架的使用

这个站点包含了一系列名人名言、作者和标签,我们需要使用Scrapy将其中的内容爬取并保存 下来°

2准备工作

在开始之前,我们需要安装好Scrapy框架、MongoDB和PyMongo库,具体的安装参考流程如下°

□ 』 ■ 】

MongoDB数据库并写人数据了°



安装好这三部分之后’我们就可以正常使用Scrapy命令了,同时也可以使用PyMongo连接



□Scrapy: https://setup.scrape.center/scrapy。 □MongoDB: https://setup。scrape。center/mongodb· □PyMongo: https:〃setupscrape.center/pymongo。

〗 』 ■

做好如上准备工作之后,我们便可以开始本节的学习了°

■ 】 · ‖

3.创建项目

‖ ‖ 口

首先我们需要创建一个ScIapy项目’可以直接用命令生成,项目名称可以叫作scrapytuto前al,创

‖ 】 日

建命令如下:

{ □

5crapy5taItproject5〔rapytutor1己1

∏ · 、 (

运行完毕后,当前文件夹下会生成_个名为scrapytutona‖的文件夹’文件夹结构如下所示:

』 ‖ 』 ‖ 日 』 凸 ‖ 』 ■ 】 」

-i∩it_.py



jte‖5·py #It印5的定义`定义爬取的数掂结构 mdd1ew己res。py #‖idd1ew己re5的定义,定义爬取时的中间件 pjpe1i∩eS°py #p1pe11∩e5的定义,定义数据管道 5ettj∩g5。py #配Ⅲ丈件 5pjder5 #放Ⅲ5pjder5的丈件央



_1∩jt—°pγ



5cmpy.c「g #5cr己Py部丹时的配Ⅲ文件 5crapytutoria1#项目的模块,引入的时侠簿兵从这卫引入

■ | □ ■

4创建Sp|de「

{ 可 」 ● ] | 』

爬取后的结果的方法°



Spider是自己定义的类’Scrapy用它来从网页里抓取内容,并解析抓取的结果°不过这个类必须 继承Scmpy提供的Spider类5crapy.5pjder,还要定义Spider的名称和起始Request’以及怎样处理

』 ■ 〗

也可以使用命令行创建一个Spider°比如要生成Quotes这个Spider,可以执行如下命令:

| ■ ■ 』 ·

cd5〔rapytutorja1 5crapy8e∩sPiderquote5quote5·to5〔mpe°〔o∩]

· □ 」 】 ■ 乙 ■ ■

进人刚才创建的scrapytuto∏al文件夹’然后执行ge∩5p1der命令°第_个参数是Spider的名称’ 第二个参数是网站域名°执行完毕后, spiders文件夹中多了一个quotespy,它就是刚刚创建的Spjder, 我们再把5tartuI15中的http协议改成∩ttpS’最终代码如下所示:

■ | |

1呻oIt5〔raPy

』 ■ ‖ | 纠 ˉ ■ 、

〔1己55Quote55pjder(scr己py。5pider): ∩a眶= °|quote5" a11oweddα∏m∩5= [圃quote5.tos〔r己pe.c刚卿] 5t日rtuI15= [』httP5://quotes.to5cmpe.Co"/! ]

■ 」 ■ |

pa55



de十par5e(5e1十’ re5po∩5e):

」 □ ■ { 』 ■

这个Q0ote55Pjder就是刚才命令行自动创建的Spider’它继承了scrapy的5pideI类’Quote55pjder 有3个属性’分别为∩a阳e、a11oweddo们ai∩5和5t日rtur15’还有一个方法par5e°

■ | | | 《 · 」 ■ ■ ■ ■

|「 卜| ‖ 】 二

l52 Scrapy入门

745

}(

□∩a"e是每个项目唯-的名字,用来区分不同的Spider。 □a11o"eddoⅦa1∩5是允许爬取的域名,如果初始或后续的请求链接不是这个域名下的’则请求 链接会被过滤掉°

伊『}△■‖■尸

‖■〗仔■伊

◆『|‖》卜>『||卜}【◆『‖

尸■尸 》 仆 ■ 尸 ‖

} p

∩β仿 巴尸『卜||■厂

■ 「 卜 } 厂 ■ 厂

} ■厉■「『◆『|▲【尸‖|‖■「}[■尸|■】■「|【【□「【‖【厂|〖‖■■‖‖Ⅲ》「|尸「‖[‖》■‖‖|■「||■队|||■■■



□5t己rtuI15包含了Spider在启动时爬取的URL列表’初始请求是由它来定义的。

□paI5e是Spider的—个方法°在默认情况下, startur15里面的链接构成的请求完成下载后’ parse方法就会被调用’返回的响应就会作为唯_的参数传递给parse方法。该方法负责解析返 回的响应、提取数据或者进_步生成要处理的请求。 5.创建|te丽

上_节我们讲过’Item是保存爬取数据的容器,定义了爬取结果的数据结构°它的使用方法和字 典类似。不过相比字典’Item多了额外的保护机制’可以避免拼写错误或者定义字段错误° 创建Item需要继承scrapy的Ite‖类’并且定义类型为「ie1d的字段’这个字段就是我们要爬取 的字段。

那我们需要爬哪些字段呢?观察目标网站’我们可以获取到的内容有下面几项。 □text:文本’即每条名言的内容’是一个字符串° □aut∩Or:作者,即每条名言的作者’是_个字符串°

□tag5:标签,即每条名言的标签,是字符串组成的列表°

这样的话’每条爬取数据就包含这3个字段’那么我们就可以定义对应的Item,此时将itemspy修 改如下: mPort5〔mpy

C1a55QUOteIte‖(5〔mPy.IteⅦ): text=5crapy.「ie1d() a0t∩or=5crapy。「ie1d() tag5=5Crapy.「ie1d()

这里我们声明了0uoteIte‖’继承了Ite"类,然后使用「1e1d定义了3个字段,接下来爬取时我

们会使用到这个Item°

6.解析只espo∩se

前面我们看到, par5e方法的参数re5po∩5e是5tartur15里面的链接爬取后的结果’即页面 请求后得到的Response, Scrapy将其转化为了—个数据对象’里面包含了页面请求后得到的 Re5po∩5e5tatu5、8ody等内容°所以在par5e方法中’我们可以直接对re5Po∩5e变量包含的内容 进行解析’比如测览请求结果的网页源代码’进_步分析源代码内容’或者找出结果中的链接而得 到下-个请求°

我们可以看到网页中既有我们想要的结果,又有下一页的链接’这两部分内容我们都要进行 处理°

首先看看网页结构’如图l5ˉ4所示。每—页都有多个c1a55为quote的区块’每个区块内都包含 text、author` tag5。那么我们先找出所有的quote,然后提取每个quote中的内容。



↑5 匹

746

Scrapy框架的使用

第l5章

』…敷』 |



≤b、

■■■■■■

螺′P罐戳’辑簿…≈ 嚼宁鳃” 恳′;》“嘴。攒鹤蹿瓣业″融^霹-

J潞衫勒“翻没 α.券蹿. °

『鞠缴夕臆蜘辨鳃嚼嚼沏扣?.阐厂霍“ p

{『 °



Q

γ铀鳃,一磕.…尸撵_

=-=

.=-= ˉ. 一…

‖≡-臀净嚼基…≡≡=一≡… .一=研=紫吉≡…奎孵$鞠…螺早~===

■樱牟峙≡■醉砷鞠狰铂罚蕊`…芭0藕p~谬 ≡_罐一 ■





















广













ˉ晦









→凸=≡…豁蹿…气=-

ˉ



……





—…

_…





哪ˉ=



…U=←………醚

7吨1vC1mS=∏亡O1→B闺户



「百=西〔〔■ST黔狙唾t凹1t…『呻瘫哩tmt痢>

,

■●了∏■山了M□■唾mγ□C『■□r“1t屿●p厂Oc●饵昏o?ou丁t"』∩皿闻· 〗tC■帅肋◎tbech●拥0GdⅧ1thα】tC们m叼m0恤7mm戊l吨pw钞

? √… ↑◆≤…≥

避{`c呻……m′" im…p=碉ou…户≥A…穴e」…』∩鸳′酗`脸

! ‖

≤■『它V正001■』tm厅/A1…7【≈但mEt■1p0A>《■…t》邹酸 √■…≥

《v啦1vcl舔6幻蛇鲍B尸> 』



!

…t·clas….旭神呻霹1t…7呻哩攒e…Ⅱ对0缉鹰mte"R″膜′ch日帅g·,d唾忻t回额tZ,th皿R蛔′山了l旷嚣

了■998 伶



qcu0黔哩t鲍闪"伟↑宙“腆m/亡h…′…〃Jw×m啊″

!

妇c1m母…t叼』矿h7E7逗辑】t四′d■■■tmmt■/…〃尸…≈t‖@u呻t3</>

;

→c〗■■炉必t叼萨∩厂mw似Itm…厂ln』■■G/M^…「1d≤/触



心clas≥t叼αhre?潭臃八m陀∩mhjn@′m…JM窟霹t∩1廊mU</a≥

} </6』v> }记/回』怜

广咱』VclaZ£芍■…te00 1t…sC…』t■w碑捶供mtp8〃6c…令O呵/t摊■t1γ呐『h嚼蟹>■≤/□1庐 仿刺1Uc1●$5■的…t臼0 』《m早c…1r■w砷叮萨镭"ttpf〃D…■o馆/c■■T』V…砒仙>=√d1…

卜≤口1Vc1■a季呻…t●叮』t酗目cO体1tmty库凹们ttp8〃$C唾·of0/c≈■u0吨水群>■压/d1w

广妇』γc1■S■醒斡卑m它G师1t四巴c◎pe1t■tγm叮玲Mm岛〃■口…hO『wC≈·t1γ砷冰,°>=</“v> p<汕仁`■色■=时…r铲1寸…巳■…』t唾kv函■‘httO8〃sc…·◎『O′c硒t1v…欣‘o>■噶/o1吵

图15ˉ4网页结构

我们可以使用CSS选择器或XPath选择器进行提取’这个过程我们可以直接借助re5po∩5e的〔55 或xpath方法实现,这都是Scrapy给我们封装好的方法’直接调用即可° 在泣里我们使用CSS选择器进行选择,可以将paI5e方法的内容进行如下改写: de千paI5e(se1千’ Ie5po∩5e): quOte5=re5po∩5e.〔s5(′.quote‖〉 千orq0otei∩quote5:

text=quote.c55(‖°text; :text‖)·extra〔t+ir5t() 日uthor=quote.〔55(‖.日l』t‖or: :text‖).extract「ir5t() tag5=quOte.C55(0 .tag5 。tag: :teXt!).eXtr3〔t()

泣里首先利用CSS选择器选取所有的quote并将其赋值为quote5变量,然后利用+or循环遍历 每个quote,解析每个quote的内容。

对teXt来说,观察到它的〔1a55为teXt,所以可以用.text选择器来选取’这个结果实际上是整

个带有标签的节点’要获取它的正文内容’可以加::teXt。这时的结果是长度为l的列表’所以还需 要用extra〔t+jr5t方法来获取第一个元素°而对于tag5来说’由于我们要获取所有的标签’所以用 eXtm〔t方法获取整个列表即可°

为了更好地理解以上内容的提取过程’我们以第_个qUote的结果为例,看-下各个提取写法会 得到怎样的提取结果°源码如下: 〈djγ〔1a55="ql』ote" ite肌5〔ope二"00it咖type="http://5che们a。org/〔reatjγe‖or低"〉 〈5pa∩〔1a55="text"jte"prop="te×t"〉"「hewor1da5we∩aγe〔reatedjtj5己pro〔e5so十ourthi∩hi∩g. It 〔a∩∩otbecha∩gedwjthoutcha∩gi∩gourthi∩促i∩g°’』〈/5pa∩〉

<spa∩>by〈5"己11c1a55="己ut∩or" iteⅧprop="aut∩or"〉∧1beIt [i∩5tej∩</s‖a11〉 〈a∩re十≡"/aut∩or/∧1bertˉ[j∩5tei∩!0〉(about)</a〉 </5pa∩〉 <diγ〔1己55="tag5"〉 丁a85目

〈爬t日c1a55="|〈eywords" jt即prop="|(eγwoId5"〔o∩te∩t="〔∩a∩ge’deepˉthoug‖t5’t打j∩|(1∩g’"or1d"> 〈aC1a55="tag" hre十="/tag/cha∩ge/Page/1/"〉〔ha∩ge〈/a〉

}} 『}

l5.2

ScIapy入门



747

<ac1as5="tag"hre于=口/tag/deepˉthoughts/page/1/"〉deepˉthought5〈/a〉

‖尸)「}

〈a〔1a55="tag‖0 ∩re千二口/tag/tM∩促j∩8/page/1/"〉t∩i∩低j∩g</a) 〈ac1a5s="t3g"hre十=闰/tag/wor1d/page/1/冈〉蜘r1d</a〉

</djγ〉 〈/djγ〉

不同选择器的返回结果如下:

●「‖‖「‖‖‖凸厂||

quote.〔55(0 .text|)

[<Se1ectorxpath="de5〔e∩da∩tˉorˉse1f: :*[@c1a55a∩d〔o∩tai∩5(〔o∩cat(』 』’∩or川311∑eˉ5pa〔e(@〔1a55)’ , ‖)’ 0 text ‖ )]"data≡!〈spa∩〔1a55=‖‖text闻jte们prop=00te×t"〉‘‘丁he !〉]

quote。〔ss(0 .te×t: 8text!〉

[<5e1ector)《path=0|desce∩da∩t_oIˉ5e1十: :*[0c1a55a∩d〔o∩t己j∩5(〔o∩cat(! !’∩om己1jzeˉ5pa〔e(@c1a55)’ ‖ ,)’|text 0)]/text()口dat3=,臼「heNor1da5wehaγe〔reatediti5己pr』〉] 巴∏

quote.〔55(0 .text!)。extr己〔t()

[!<spa∩〔1a55=甸text阐iteⅧprop=圃text词〉盯『∩ewor1da5wehaγe〔re己teditjs己pIoce55o+ourthi∩ki∩g. Itc司∩∩ot bec∩a∩8edwit∩outc∩a∩gi∩gourthj∩促1∩g.〃〈/5pa∩〉|]

■■厂■仔厂■■尸

quote.〔55(! 。text: :text|).extr己〔t()

[0“丁heb0or1da5we‖aγe〔re日tedjti5apro〔e55o千ourthj∩|〈i∩g. It〔a∩∩otbecha∩gedwit‖out〔ha∏gj∏gour thj∩促j∩g.’’‖] quOte.〔55(』.text: :text‖)。extract千ir5t()

“丁‖e"or1da5wehaγe〔Ieateditj5己pro〔e55o+ourt∩i∩M∩g。It〔a"∩otbe〔h己∩gedNithout〔ha∩gj∩gourtM∩促j∏g°”

卜‖■『

这里我们演示了不同提取过程的写法’其提取结果也是各不相同,比如单独调用C55方法我们得 到的是5e1e〔tor对象组成的列表;调用extra〔t方法会进—步从5e1e〔tor对象里提取其内容,再加 上::text则会从HTML代码中提取出正文文本°

●「■ˉ■匹『■β‖■■「■■■

》 ‖ ‖ ■尸‖卜△β‖卜

心「》「■「■■■厂■■厂|▲■∏■尸▲∏





■∏『|●■·|}■■





因此对于text,我们只需要获取结果的第-个元素即可’所以使用extmct十jr5t方法’得到的

就是-个字符串°而对于tag5,我们要获取所有结果组成的列表,所以使用extmCt方法,得到的就 是所有标签字符串组成的列表。 7.使用|teⅧ

上文我们已经定义了Q0oteIte|∏,接下来就要使用它了。 我们可以把Item理解为_个字典’和字典还不太相同,其本质是_个类’所以在使用的时候需 要实例化°实例化之后’我们依次用刚才解析的结果赋值Item的每_个字段’最后将Item返回。

0uote55pider的改写如下: mport5〔r己py 于ro∏‖ 5〔mpytutoIia1。jte们5mPortq』oteIte阳

c1a55Quote55pider(5〔rapy.5pjder): ∩a爬=伺quote5阅 a11咖edd刚aj∩5≡ ["quote5.to5〔mpe·〔咖"] 5tart0r15≡ [!∩ttps://quote5.to5craPe.〔oⅦ/′] de+par5e(5e1十’Ie5po∩5e): quote5=respo∩5e.〔55(|.q0ote‖〉 +orquote1∩quote5: iteⅧ=QuoteIte∏‖() ite‖[,text,] =ql』ote.c55(′°text: :text,).extm〔t千ir5t() ite‖[』aUthOr|] =qUOte.〔55(‖ .al』t∩OI:;teXt,).eXtraCt+jr5t() 1teⅦ[ 0tag5|] 宣qUote.〔55(! .tags .tag: :text!).extIaCt() yie1djteⅦ 厂↑5

如此-来,首页的所有内容就被解析出来并被赋值成了-个个Q0oteIte"了,每个QuoteIteⅦ就 代表—条名言’包含名言的内容`作者和标符.

8.后续∩equest

上面的操作实现了从首页抓取内容’如果运行它’我们其实已经可以从首页提取到所有q‖ote信 息并将其转化为_个个QuoteIteⅦ对象了°



}■



第l5章Scrapy框架的佳凰

748

但是’这样还不够,下_页的内容该如何抓取呢?这就需要我们从当前页面中找到信息来生成下

一个Request,利用同样的方式进行请求并解析就好了°那再下_页呢?也是一样的原理,我们可以 在下-个页面里找到信息再构造再下—个Request°这样循环往复迭代,从而实现整站的爬取。



我们将刚才的页面拉到最底部,如图l5ˉ5所示。

{|



钥由y叼协◎哑Sm剑〗″旧妇脓ayUq灯》◎毗∩域k幻



wS…删■ftm(…叭》

丁ags:……雨 N酗t→

==■~~-→~—=←~■≈=—■=—←←■勺≈=←■≡→≥←■=~砖≡-…一■一南=■凸~■一■■=■一=桶→■卞←≡面~=白==≡—≡甲≡二≡=←≈~■■■■■=-■■■■ ■■■-一■==■■-■■ 7=

良菌 〔身哇→=≡鳃思≈占唾令唾噎唾古缉A=…≡樊剑!囱…… p匈1MC`“巳尉』印mt·瞬让…自o碑1t■t洒″∩ttp:〃5口…pO巾/〔7四t』γ韵『笛≥=≤/d1够

v…

v<u1〔l“9■防p呵●广≥ :〗惺f◎≈ 丁<k1〔1■工■>…t必>

广 a悯『G? /…e/2J



己./n=铆′≡_…

々t拴 浴■↑ter

菌l5ˉ5页面最底部 这里我们发现有一个Next按钮,查看-下源代码,可以看到它的链接是/page/2/’实际上全链接 就是ht印s://quotes.tosCrapecom/page/2’通过这个链接我们就可以构造下_个Request了°

构造Request时需要用到scrapy的Reque5t类°这里我们传递两个参数’分别是ur1和〔a11back, 这两个参数的说明如下°

□url: 目标页面的链接°

□callback:回调方法,当指定了该回调方法的Request完成下载之后’获取Response,Engine会

将该Response作为参数传递给这个回调方法。回调方法进行Response的解析生成_个或多个 Item或Request’比如上文的par5e方法就是回调方法。

由于刚才所定义的par5e方法就是用来提取名言text、author、tag5的方法,而下一页的结构和 刚才已经解析的页面结构是_样的’所以我们可以再次使用Par5e方法来做页面解析° 接下来我们要做的就是利用选择器得到下-页链接并生成请求’在par5e方法后追加如下的代码: ∩ext≡re5po∩5e.〔55(‖.pageI 。∩exta::己ttr(hre十)‖).extm〔t十jr5t(〉 ur1=re5po∩5e。ur1joi∩(∩ext)

γie1d5cmpy.偶eque5t(ur1=ur1’〔a11ba〔促=se1千.par5e)

■刮||』■』□‖‖』■】··』‖|■习司|』{」』』■』‖』』·■·‖己■』■勺|{」■Ⅷ□Ⅷ‖‖』■叮

◆酚1γcl■EE■钟甲■t铲1t…C□馋1f…″钓∩tt@:〃Sc….◎帕汇阳·t1γ和了伐■>■</o1v> ≥匈加C1m…县印■t■丙蛇■吕c…1t■tγ隋哩ht如8〃■创…·◎0p′C7蹿t』…『楞↑D→</d1岭 p钮』UCID$护伺…t它钟1t…c…血■w…尸∩忱p1〃9c…·O0w[…t1…术α>=</d工w 仿<0』Dc1理乞=…te" jxm巴C@佐1t■t严■■赋tp弓〃5C…·◎丁创〔陌■t』U呻巾ob…≤/d1岭













‖ q

第一行代码首先通过CSS选择器获取下_个页面的链接’即要获取超链接a中的hre+属性’这 里用到了::attr(hre+)进行提取,其中attr代表提取节点的属性, bre千则为要提取的属性名,然后



再下-步调用eXtra〔t千1r5t方法获取内容。



第二行代码调用了ur1jo1∩方法, ur1jo1∩方法可以将相对URL构造成一个绝对URL°例如’获取 到的下_页地址是/page/2/, ur1jo1∩方法处理后得到的结果就是https;//quotes.toscIapecom/page/2/° 第三行代码通过ur1和〔a11b日〔促变量构造了一个新的Request,回调方法〔a11ba〔k依然使用par5e 方法°这个Request执行完成后’其对应的Response会重新经过par5e方法处理’得到第二页的解析 结果’然后以此类推’生成第二页的下-页’也就是第三页的请求。这样爬虫就进人了_个循环,直







q

q

到最后—页。

‖ q





l5.2

Scrapy入门

749

通过几行代码’我们就轻松实现了_个抓取循环,将每个页面的结果抓取下来了° 现在,改写之后的整个5Pjder类如下所示: mport5craPy

于Io∏‖ 5〔r己pytotoIja1。ite川5j呻ortQt』oteIteⅦ c1as5Quote55pjder(5〔r日py。5pjder)8 ∩a爬= 冈quote5冈

a11o们edd刚aj∩5≡ ["ql」otes。to5〔rape.co‖∏!|] 5tartuI1s= [!https://quote5.tos〔mpe.〔oⅦ/!] de+p己r5e(5e1+’ respo∩5e): q0Ote5=re5PO∩5e.C55(|。qUOte0) +orquotei∩quote5: jt咖=咖oteIt印()

jt咖[`text‖] ≡quote.〔55(‖.text: :text|)。eXtm〔t「jr5t() ite‖[ 03ut∩or0 ] =quote·〔55(! .al」t‖or::text0).extmct干jrst() ite‖[ 0t日g5‖] =quote.〔5s(0 .tags .tag: :text|).extm〔t(〉 yje1d1te‖

||||

∩eXt=re5PO∩5e。C55(0 .Pa8er .∏eXta: :attr("hre千圃〉,).eXtra〔t十jr5t() ur1≡re5po∩se.uI1joj∩(∩ext) yie1d5〔rapy.Reque5t(ur1=ur1’〔a11back=se1+.paI5e)

可以看到整个站点的抓取逻辑就轻松完成了’不需要再去编写怎样发送Reque引,不需要去关心异 常处理,因为这些工作Scrapy都帮我们完成了,我们只需要关注Spider本身的抓取和提取逻辑即可。 9.运行



接下来就是运行项目了’进人项目目录,运行如下命令: 5〔rapycra训1quote5

就可以看到Scrapy的运行结果了: 2O2OˉO8ˉ2919:55;46 [5〔mpy.uti15.1og] I‖「0: 5〔mpy2。2。15tarted(bot: 5crapytutori己1) 2o2oˉo8ˉ2919:55;46[5cIapy。utj15.1og] I‖「0: Ver5io∩s: 1m1q.3。3°0′1ibx爪122.9。9’c555e1e〔t1.1.o’paI5e1 1.6.o’"31jb1.2卫.0」『刊j5ted20.3.0’Pytho∩3.7。〕(de伯u1t’∧pr242o20’1885』:23〉 ˉ [〔13∩811.o。3 (〔1己∩gˉ11O3。O。32。62)]’ pyOpe∩55L19.1.o(Ope∩55L1.1。1g 21∧pr2O2O)’cryptogr己phy2。9.2’ p1at十om 0aIwi∩ˉ19。4.oˉx8664ˉi386ˉ64bjt

2O2Oˉo8ˉ2919:55;46 [5cmpy.l』tj1s。1og]D田(」C: 05j∩gre己〔tor: twjsted.j∩ter∩et.5e1ectreactoI.5e1ect【ea〔tor 2020ˉO8ˉ2919:55:46 [5〔rapy.craw1er] I‖「0:0verrjdde∩setti∩g5$ {|80『‖叫[` : |5cmpytutorja1|’ . 0‖[N5pI0[【泅儿[0 : ‖scrapytutoria1°5pjder5 ’ 0ROB0『5Ⅸ『OB[γ0 吕 丁rue’ 05pI0fR灿」[[5|: [|5crapytutorj己1.spjder50]}

2020ˉ08ˉ2919:55846 [5crapy.exte∩5io∩s.te1∩et] I‖「0:『e1∩etp己55word: 691q6568e6十e206〔 2020ˉ08ˉ2919:55:46 [5crapy。mdd1ewa【e] IN「0: [∩日b1ede×te∩5jo∩5: [ ′5cmpy.exte∩5jo∩5.core5tat5·〔ore5tat50 ’

|》‖|[『|卜}}‖「||『||β『〖『‖|■「

05〔raPy.exte∩51O∩5.te1∏et.『e1∩et〔O∩SO1e‖’ |5cmPy·exte∩51o∩5.『∏e↑∏l』5age.№mryl」5a8e ’ |5Cr己py.exte∩5jO∩5.1og5t己t5儿O85tat50] 202oˉ08ˉ2919:55:46 [5〔rapy.Ⅶjdd1出are] I‖「0: [∩ab1eddow∩1oaderⅧidd1e"are5: [`5cmpy.do树∩1oademidd1ewaIe5。robot5txt。∩obots『xt俏idd1酗are ’ ‖5cmpy罐dow∩1oademidd1e"are5·‖ttpauth.‖ttpAuth问jdd1即are ’ 05〔mpy·dow∩1oademjdd1剐aIe5°dow∩1oadti爬o‖tDo切∩1oad丁i∏记out"idd1硼are0 』 05crapy.dow∩1oadermdd1出are5·de千au1t‖e3der5.De千au1t‖eader5∩idd1eware ’ |5〔mpy.do佣∩1oadem1dd1eN3res.u5emge∩t.05er吧e∩t"jdd1e刊aIe ’ ‖5crapy.doⅦUoaderⅦidd1硼are5.retry.Retry付idd1印are! ’ 05cr己py·do们∩1oademidd1ew己re5.redire〔t·腮taRe十re5hMdd1e佣are0 ’

5〔mpy.do切∩1oademjdd1ew己re5。∩ttpcα∏pre55io∩。"ttp〔α印re55jo∩∩jdd1印are! ’ ‖5〔rapy·do仍∩1o3dermdd1e们are5.Iedire〔t°Redjre〔t月jdd1ewaIe0’ ‖5〔mpy。dow∩1o己demidd1eware5。〔m代ie5°〔oo代ie5"idd1酗are0’ 05〔mpy·do仍∩1oademidd1eware5°∩ttpproxγ·‖ttppIoxγ‖jdd1ew己re0’

5crapy.do倒∩1oademidd1eware5·5tat5Do"∩1o日der5tat5!] 2o2oˉo8_2919:55;』6 [5〔rapy.爪jdd1酣己re] I‖「0: [∩ab1ed5pider‖lidd1e"are5:

厂↑5

b

■■■■■■尸■■■■■■尸‖巴

第l5章Scrapy框架的伎用

750

2O20ˉO8ˉ2919:55:46[5crapy.exte∩5io∩5.1og5tat5] I‖「0:〔raw1edOpages(atOPage5/m∩)’5〔raPed0ite"|5(at 01te"5/川i∩)

202Oˉo8ˉ2919:55:46 [s〔Iapy.exte∩5jo∩5.te1∩et] I‖「08 丁e1∩et〔o∩5o1e1iste∩1∩go∩127.O.O.1:6O卫3 2o20ˉ08ˉ2919:55:47 [B〔rapy。〔ore·e∩gi∩e]0[8[」C:〔raw1ed(qo4)〈C[丁http5://quote5。to5〔mpe.co‖∏/robot5。txt〉

(re千erer: ‖O∩e)

2020ˉo8ˉ2919:55:q8 [s〔Iapy.〔ore.e∩gi∩e]D[B0C:〔I己"1ed(20o)〈C[丁∩ttp5://quote5.to5cIape。〔o∏|/〉(re千ereI: 202oˉo8ˉ2919:55:』8 [5〔mpy.〔ore·5〔rapeI]D[BⅦ: 5〔Iaped「I刚<2oohttp5://quote5.toB〔mp巳co|∏/〉

{‖author` 8 !∧1bert[j∩5tei∩‖’

0text0 : Mu丁‖ewoI1d己5wehaγe〔reatedjtj5apIo〔e55o十o…!}

‖(

!tag50 : [0〔∩a∩ge! ’ !deep—thought5,’ 0thi∩促j∩g! 」 0wor1d|]’



■■‖□·

‖o∩e)



|(

2020ˉ08ˉ2919:5§:q6[5〔rapy.〔ore.e∩gi∩e] I肝0: 5PjderoPe∩ed

‖|

[|5〔Iapytutoria1。pipe1i∩e5。丁e×tpipe1j∩e0’ 05crapytl』torja1.PjPe1i∩es。№∩gopiPe1i∩e0 ]

‖』

[ ′5cI己py。spjdemjdd1出aIes。httperror。毗tp[rror问idd1出are! ’ {scrapy.5pidemidd1e们ares°o仟5ite.听千5jt酬idd1副are0』 ‖5〔rapy.5pjdemjdd1eware5°re+ereI·Re+eIe渊jdd1e旧aIe’ 0SCrapy.Spjdemidd1eWare5°Ur11e仰gt‖。Ur1[e∩gth"idd1eWare0’ ‖5cIapy.5pidermdd1ewares.depth·Dept刚idd1eware0] 2o2oˉ08ˉ2919:5S:』6 [5〔mpy.Ⅷidd1印are] I‖「O: [∩ab1edjt印PiPe1j∩e5:



2020ˉ08ˉ2919:55:』8 [5〔mpy.〔ore.5cI日per] D[80C: 5〔raped「r咖〈200http5://quote5.to5〔Iape.co‖/〉 {‖aut‖or|: ‖〕.巩. Ro们1j∏g0 』

{0al』t∩or! : !∧1bert[j∩5tej∩‖’

‖tag50 : [0j∩5p1mtio∩a1‖’ ‖1j十e‖’ ‖1jγe0 ’ ‖刚im〔1e』’ !mmc1e5‖ ]’

0text0 ; |“丁∩ereaIeo∩1ytwoNay5to1iγeyour1i十e。O∩ei5…,} 2O20ˉO8ˉ2919:55:48[5〔mpy.〔ore.5〔mper]0[8‖L: 5〔r己ped十ro∩‖〈20Ohttp5;//quotes°to5〔mpe.co‖/〉

|tag50 : [ !a1item〔y′’ 0boo|〈5! ′ !〔1己55jc! ’ |huⅧr0 ]’ !text|: !叮‖eper5o∩’ beitge∩t1e们a∩or1ady’whoha5∩ot…!} ◆



日‖‖‖

{‖aut∩Or0 : 』〕a∩e∧u5te∩0 ’

日 ‖ ‖ ‖ | 』 □ | | 勺 ‖ ● 】 ●

‖tag5』: [ !abi1itje5‖ ’ {〔hoi〔e5‖]」 |text! ; 『“Iti5our〔hoj〔e5’"arry」 that5∩o切w↑]atwetrU1y…|} 2o20ˉ08ˉ2919:55:48[s〔Iapy。〔ore.5cr日per] D[80C: 5〔mped十I咖〈20Ohttp5://quote5.to5cmpe.〔o‖/〉



|=■■卧|●

2o20ˉo8ˉ2919:56;32 [5〔rapy.5tat5〔o11ectoIs] I‖「0: 0u∏pi∩85〔Iapy5tats: {!dow∩1oader/Ieque5t-byte5|: 2881’ 0dow∩1oader/request〔ou∩t|: 11』 0dow∩1o己der/reque5t爬t∩odcou∩t/C[『′: 11’ |dow∩1o日der/re5po∩se-byte5‖: 2』911′ 0dow∩1oader/re5po∩Se-〔ou∩t0 : 11’ |do"∩1o己der/Ie5po∩5e5tatu5〔ou∩t/2OO|: 1O’ 0dow∩1oadeI/Ie5po∩se5tatl』5〔ou∩t/』04‖: 1’ |dupe+i1teI/「j1tered‖: 1’ ‖e1apsedˉtme5e〔o∩d5|: 1o.565782’ 0十j∩i5hIea5o∩0 : 0千i∩j5hed‖’

0jte"5〔raped-〔ou∩t0 8 1o0’ 01Og-〔OU∏t/0[8l几0 ; 112』 01og-cou∩t/I‖「O! ; 1O’ 爬则5age/爬X0 : 57卯8128’ ‖∏e『‖usage/5tartl』p0 8 57oo4o32’ 0Ieque5t-dePt∩-Ⅶax0 : 10’ 0re5po∩5ere〔ejγedcou∩t0 : 11’ 0robot5txt/reque5tcou∩t,: 1’ 0robot5txt/re5po∩5e-〔ou∩t0 8 1’ 0robot5txt/re5po∩5e5tat‖5〔ou∩t/404,: 1』 0s〔∩edu1eI/dequeued0 : 10』 05〔hedu1er/dequeued/爬Ⅷry0 : 1O’ 05〔们edu1er/e∩q0eued|: 10’ |5chedu1er/e∩queued/爬帅ry’; 1o’ ,5t日rttme‖: dateti爬.dateti‖∏e(202o’ 8’ 29’ 11’ 56’ 21’ 949055)}

2o2OˉO8ˉ2919:56:]2 [5〔mpy.〔ore.e∩gj∩e] I‖「0: 5pjdeI〔1o5ed (千j∩i5∩ed)

这里只是部分运行结果,省略了—些中间的抓取结果°

首先’ScIaPy输出了当前的版本号以及正在启动的项目名称°然后输出了当前settingspy中-些 重写后的配置°接着输出了当前所应用的Mjddlewares和ItemPipe‖ines°Middlewares和ItemPipelmes

都沿用了Scrapy的默认配置’我们可以在settjngspy中配置它们的开启和关闭’后文会对它们的用法

』厂‖可‖』■门■■■|』□]|■■』■司|■司』■‖|]‖|』々□■■|』■■|口□】可】」■■■

‖+j∩j5htj爬|: datet加e。d日teti眠(2020’ 8’ 29’ 11’ 56’ 32’ 514837)’







l5.2

Scrapy入门

75l

进行讲解°

接下来就是输出各个页面的抓取结果了,可以看到爬虫_边解析,_边翻页,直到将所有内容抓

‖■||‖凸■|‖伊}|β■厂}|‖)|

取完毕’然后终止。

最后’Scrapy输出了整个抓取过程的统计信息,如请求的字节数、请求次数、响应次数、完成原 因等°

整个ScIapy程序成功运行°我们通过非常简单的代码就完成了-个站点内容的爬取’所有的名言 都被我们抓取下来了°

↑O保存到文件 可 尸



运行完Scrapy后’我们只在控制台上看到了输出结果°如果想保存结果该怎么办呢?

●『「卜仍『|

要完成这个任务其实不需要任何额外的代码’Scrapy提供的FeedExports可以轻松将抓取结果输 出°例如’如果我们想将上面的结果保存成JSON文件’那么可以执行如下命令: 5〔rapy〔ra"1ql』ote5 ˉoquote5.j5o∩

气■

二■「■■「巴■「『‖▲尸『

命令运行后,项目内多了—个quotes.json文件,文件包含了刚才抓取的所有内容’内容是JSON 格式。

另外我们还可以让每_个Item输出一行JSON’输出后缀为jl’为jsonline的缩写,命令如下 所示:

0

5cmpy〔raw1quote5ˉoquote5°j1

=凸厂■『|已■「■■尸卜‖△■

或 S〔raPy〔ra"1qUOte5ˉOqUOte5。j5刚1i∩eS

FeedExpo【ts支持从输出格式还有很多’例如csv`xml、pickle`marshal等’同时它支持fm、s3等 远程输出’另外还可以通过自定义ItemExpo戒er来实现其他的输出。

∩「卜

例如’下面命令对应的输出分别为csv`xml、pickle、marshal格式以及f↑p远程输出: 5〔mpγcmw1quote5 ˉoquote5·csγ .

■厉‖『仙『■庐■彦

其中’f↑p输出需要正确配置用户名、密码、地址、输出路径,否则会报错°

伊|》仅

5Crapy〔r己W1quOte5ˉOquote5·XⅦ1 5cmpy〔m"1quote5ˉoquote5.pj〔仪1e 5cr3pycr酗1quotesˉoquote5·阳r5b己1

目来说,这应该足够了°

5cmpycra刊1quote5ˉo千tp://u5eⅢ:pa550+tp。exa‖∏p1e.〔咖/path/to/quote5°〔5γ

通过Scrapy提供的FeedExports,我们可以轻松地将抓取结果到输出到文件中。对于一些小型项

△尸|‖▲■Ⅷ■■‖△■「■尸||▲■=

如果想要更复杂的输出’如输出到数据库等’我们可以使用ItemPile‖ine来完成。 ↑↑ˉ使用‖temP‖pe||∩e

如果想进行更复杂的操作,如将结果保存到MongoDB数据库中或者筛选某些有用的Item’那么 我们可以定义ItemPipeline来实现°

ItemPipeline为项目管道。当Item生成后,它会自动被送到ItemPipeline处进行处理,我们可以 用ItemPipeline来做如下操作: □清洗HTML数据;





兰■「





□验证爬取数据,检查爬取字段; □查重并丢弃重复内容;

厂—

↑5 h

第l5章Scrapy框架的使用

752

□将爬取结果储存到数据库°

要实现ItemPjpeline很简单,只需要定义_个类并实现proce55ite爪方法即可°启用ItemPipeline 后’ItemPjpeline会自动调用这个方法°proce55jte们方法必须返回包含数据的字典或Item对象,或 者抛出DropItem异常。

pro〔e551teⅦ方法有两个参数。一个参数是1teⅧ,每次Spider生成的Item都会作为参数传递过 来°另一个参数是5pider’就是Spider的实例°

接下来,我们实现一个ItemPjpeljne’筛掉text长度大于5O的ltem’并将结果保存到MongoDB°

修改项目里的pipelinespy文件,之前用命令行自动生成的文件内容可以删掉’增加一个 丁extpjpe11∩e类,内容如下所示: 十ro∩] 5crapy.exceptjo∩5i‖portDIopIteⅦ

de千

j∩it (5e1+): 5e1+.1iⅦit=5O

de+pIoce55it酗(5e1+’ ite‖’ 5p1der): i十jt副[‖text0]: j十1e∩(it酬「text0 ]) 〉5e1f.11Ⅶjt: ite们[ !text! ] =jte"[0te×t‖][O:5e1千.1i川it].I臼tr1p()+…

纠|‖|」■■β】■■■■■■■】□□‖■∏‖

c1a55「extp1pe1i∩e(obje〔t):

retur∩ite们

e15e『

retuI∩0roPIte|∏(恫j551∩g丁ext|)

这段代码在构造方法里定义了限制长度为5O,实现了proce5s jte"方法,其参数是1te刚和 5Pider°首先该方法判断jte们的te×t属性是否存在’如果不存在’则抛出0ropIte‖异常°如果存在’ 再判断长度是否大于5O,如果大于,那就截断然后拼接省略号’再将jte"返回°

接下来’我们将处理后的ite"存人MongoDB,定义另外_个Pipe‖ine。同样在pipelinespy中’ 我们实现另_个类‖o∩gopipe1j∩e’内容如下所示: j呻ortpyⅧ∩go

c1as5№∩go08pjpe1i∩e(obje〔t): def j∩it-(5e1「’ co∩∩ectjo∩str1∩g’ databa5e): se1+·co∩∩ectjo∩5tIj∩g=〔o∩∩ectio∩5tri∩g 5e1千。databa5e=d日taba5e

伏1aB5眠t∩od

se1千。〔1ie∩t=py∏℃∩go.‖o∩go〔1je∩t(5e1「.〔o∩∩e〔tjo∩5tri∩g)

||

de千ope∩-5pider(5e1「’ 5pider):

〈‖

de+于ro∏‖ Cra切1er(c15′ Cmw1er): ret0I∩C15( 〔o∩∩e〔t1o∩5tr1∩8=〔ra"1eI.5ett1∩g5.get(!ⅧM卯B〔叫‖[〔丁I0‖5丁RI‖G)’ databa5e=CraW1er.5ettj∩g5。get(0哪仰B0∧丁∧B∧5[』) )

5e1十。db=se1「.C1ie∩t[5e1+.d3taba5e]

de十pIo〔e55jte冈(5e1十’ jte们’ 5pjder): ∩a爬=ite∏· C1a55

.

∩己川e



5e1+.db[∩aⅦe].j∩5erto∩e(dj〔t(jte们)) retl」r∩jte阳

de+〔1o5e-5pider(5e1十’ 5p1der): 5e1千.〔1je∩t.〔1O5e()

№∩go01pe11∩e类实现了另外几个API定义的方法。





q

』■■‖■■■】■‖‖』■■‖

l5.2

Scrapy入门

753

广|》『||●巳尸‖|{「巴■「

□十ro"〔raw1er:一个类方法’用0c1a55Ⅶet∩od标识’这个方法是以依赖注人的方式实现的,方

法的参数就是〔raW1er°通过〔r日w1er’我们能拿到全局配置的每个配置信息,在全局配置

seningsPy中,可以通过定义刚‖C00RI和Ⅷ‖C00B来指定MongoDB连接需要的地址和数据 库名称,拿到配置信息之后返回类对象即可。所以这个方法的定义主要是用来获取se忱mgspy

} ■尸「■厂‖‖他

中的配置的°

■■||′‖‖||「■尸尸◆『『巴■■「β『『止尸~■■■■‖二■》卜『■尸尸卜|‖●

□ope∩ˉ5p1der:当Splder被开启时’这个方法被调用’主要进行了一些初始化操作° □〔1o5e-5pider:当Spider被关闭时,这个方法被调用,将数据库连接关闭°

最主要的proce55jteⅧ方法则执行了数据插人操作,这里直接调用1∩5erto∩e方法传人1te们对 象即可将数据存储到MongoDB°

定义好『extpjpe11∩e和№∩go08pjpe1j∩e这两个类后,我们需要在semngspy中使用它们° MongoDB的连接信息还需要定义° 我们在se仗ingspy中加人如下内容: I「[‖pIP[[I‖[5={

|5cmWtutoria1.pipe11∩e5·『e×tpjpe1j∩e|; 3o0’

|5〔r己pytutoria1。pjpe11∩e5.№∩8oD8pipe1i∩e|: 4O0’ } 哪咖8〔酬‖[〔丁I0‖5丁RI肌= 01O〔a1hOSt0

哪咖80A丁∧B∧5[= !5〔mpytutorj日1|

这里我们声明了I丁[‖pIp[[I‖[5字典’键名是pjpe1j∩e的类名称’键值是调用优先级’是-个

数字’数字越小贝‖对应的pjpe1i∩e越先被调用’另外我们声明了MongoDB的连接字符串和存储的数 据库名称°

再重新执行爬取,命令还是一样的: ▲■尸■■{■尸

p

5〔mpycmw1quote5

爬取结束后’我们可以看到MongoDB中创建了一个5〔rapγtutoria1的数据库和QuoteIteⅦ的表’ 内容如图l5ˉ6所示°

■■匹甘巳■

@必钾c…啃‖…) .女……t

≥囤抽■也沁07

■「卜}′

— 贴





+助mt…

…冯田手审甲≈中…怕毛…~己

己…=■毛岳尺=■□锣~…亡巴℃■罕二~→

■■■■■■■■■■■■■■■

■~乙…虫`№←P逗≈■←≈■■≡■

…≈

■≈咀≈≈面■ △尸·严~△ 咕■二■′心 p … ° 午…≈』~≈…=~≈■ ~■≈~~…□ 司

□ ■…■≡~己■甲 些空啪≈兑LE≈巴 △…`←甲 →早△∑

〈 』

誓α…∏oM“涵住 mγ



T…

宁必({)◎句●Cm卜5↑▲凹2“…贮山弘OⅣO匝0") ≡“

《』0恤■》 …≡酗…函U》

o它j簿G《? 蝇唾↑出

回 图l5ˉ6爬取结果

长的text已经被处理并追加了省略号’短的text保持不变’author和tag5也都相应保存到了数 据中°

↑2总结

本节我们通过抓取Quotes网站完成了整个Scrapy的简单人门,到此为止我们应该能对Scrapy的 基本用法有_个初步的概念了。

不过本节内容仅仅是Scrapy所有功能的冰山一角’还有很多内容等待我们去探索’我们后续章节 继续学习。

本节代码参见: ht印s://githuhcom/Python3WebSpide∏ScmpyTutorjal°

↑5.3

Se|ecto「的使用

我们之前介绍了利用BeautjfillSoup、pyqueIy以及正则表达式来提取网页数据的方法’确实非常 方便。不过Scrapy提供了自己的数据提取方法,即内置的Selecto『°

在3.4节我们已经初步了解了parsel库的基本用法’Scrapy中的Selector是就是基于parsel库来构

建的,而同时paI容el又依赖于lxml’ Selector对parsel进行了封装,使其能更好地与Scrapy结合使用° Selector支持XPath选择器、CSS选择器以及正则表达式’功能全面’解析速度和准确度非常高° 本节我们就来详细介绍_下Selector的用法° ↑.直接使用

Selector其实并不-定非要在ScIapy中使用’它也是—个可以独立使用的模块°我们可以直接利 用5e1ector这个类来构建一个选择器对象’然后调用它的相关方法(如xpath` c5s等)来提取数据°

例如’针对-段HTML代码,我们可以用如下方式构建5e1e〔tor对象来提取数据: 千r咖5〔rapyi呻ort5e1e〔tor

body≡ 0〈ht耐1〉〈∩ead〉〈tjt1e〉‖e11o‖or1d</tjt1e〉〈/∩ead〉<bodγ〉〈/body〉〈/ht川1〉0

5e1eCtoI=5e1e〔tor(text=body〉

tjt1e=5e1eCtOr.xpath(|//tjt1e/text()‖).extra〔tˉ千jr5t()

pri∩t(tjt1e)

运行结果如下: 肥11o‖or1d

司|·司■

这里没有在Scrapy框架中运行,而是把ScTapy中的Selector单独拿出来使用了,构建的时候传人

■□‖司】日■司ˉ■■司』』■Ⅵ|乙■司·]■■Ⅵ||∩·‖‖』■』■■」·‖‖·‖■■|■■‖|·‖」□■`‖《ˉ□■■|司■■]」■|■■‖列司■]司日√』■■□]』■□|■■可||司■■」·|■‖』‖|」■|

第15章Scrapy框架的使用

754

text参数,就生成了一个Se1ector选择器对象’然后就可以像Scmpy中的解析方式-样,调用xpath` CS5等方法来提取数据了。 q

以上内容就是Selector的直接使用方式°同BeautihllSoup等库类似, Selector也是强大的网页解 析库。如果方便的话,我们也可以在其他项目中直接使用Selector来提取数据° 接下来,我们用实例来详细讲解Selector的用法。



』|

2Sc「apyS∩e‖

‖□】』〗|■·

在这里我们查找的是源代码中tit1e内的文本,在XPath选择器最后加text方法就可以实现文本 的提取了。

由于Selector主要是与Scrapy结合使用,如Scrapy的回调函数中的参数re5po∩5e直接调用xpath

或者c5s方法来提取数据,所以在这里我们借助Scmpyshell来模拟Scmpy请求的过程,讲解相关的 □





l5.3

Selector的伎用

755

提取方法°

我们用官方文档的—个样例页面来做演示: ht印s://doc.scrapyoIg/α]川a脆st/ˉStatjc/Scl“mrsˉsan甲lelhhnl。 开启Scrapyshell,在命令行输人如下命令: 5〔mpy5∩e11bttps://doc.5〔mpy。org/e∩/1atest/5tatjc/5e1ectorsˉ5a∏p1e1.ht们1

我们就进人Scrapyshell模式了。这个过程其实是Scrapy发起了_次请求’请求的URL就是刚才 命令行下输人的URL,把—些可操作的变量传递给我们’如reque5t、re5po∩5e等’如图15ˉ7所示° ≤≡^>

●b■●

□ O∩ 乙O21-07ˉZ519目57:Z2[sc尸opy.exte门51o∩5.七e1门et]I‖「0: 丫e1∩etco竹5◎【eliS亡e∩i∏go∏



127°0.0。1:6023 △■『‖尸|‖‖【■■『‖『||巴尸‖■『「■■『》「止尸『■||}[■‖卜}‖|[■『■『||=■「「}|炉「β||』■「‖

2021-07ˉa519:5了目22[s〔尸αpy.co厂e.e∩gi∩e] I‖「0; 5Pide「opG∩C创

乙021ˉ07ˉ2519:57:乙3[S〔广αPy.co尸e。e∩gme]D[B(」G吕〔mwle口〔2")<G叮∩ttPs://doc.55 Cmpy.o『g/e∩/1◎te5t/~Stαt1〔/5ele〔tOF5ˉSomp1e1.∩m1≥(广e「e「e「; NO∩e)

[5]Aγ口jl@ble5C下□pyobjeCt5吕 5C沪αpy cmwIe7

[三] [£]

SCmpy硒αu1e(Co∏t□1∩ssC厂□py.Reque另t’ SC沪°py.5ele〔tO尸, etC) <5亡mpy.C广ow1e卜·〔广αⅦ1e「◎bjectα七0x108d63970>

它 0 呕 ■■

5

[5]

ite闸

[s]

7eq切est

<6[∏h仕p5://“〔.scmpy.oPg/e∩/l□te5t/=st□tic/5e1ectoP5ˉ巴oⅧple1【

7e5Po∏se

<Z硼https://doc.§〔广opy·◎尸g/e∩/1ote£t/,ˉsto七tc/己e1ecto厂宙ˉ5oⅧplG1

5et七i∏g5 SpmeF

<5〔尸opy.5etti∩g5.5ett1∩g5ob]ectαt0x108d63670≥″ ≤De千αUl七5ptde厂 00e「αu1t0 ot0又】”08e乙50>

{}

·内t厕1>

[三] htⅧ1>

[5] [5]

[5]U5e「u15hoPtCut5:

[5]

§ 〃



: ■

p ∏





fetch(uF1[’「e血「e〔t=丁FUe])「et〔h0侗Lα∏dupdOte1Qcα1object5(bydGfαu1tt 值

’ 「edi『eCtSo厂efOuO洲e口)

[5] 「etch(厂eq)

「etC‖osCmpy.Reques七α∩dupdotP1o〔o[ob〕e〔t

寻 巴

5

[s]

she{p◎

5∩eU∩elP(p尸i∏ttm5∩eIp)

[5]

γiew(庐espo∩5e)

γieW「eSpo∏5e1∏αh厂◎佣5e「

』 ■

h

≥>> > ——_

匹■「止尸|

图l5ˉ7 Scrapyshell模式

【■厂「■■ 匹■「卜

]』 我们可以在命令行模式下输人命令’调用对象的_些操作方法,按下回车之后实时显示结果。这 与Python的命令行交互模式类似°

=■厂

接下来演示的实例都将页面的源码作为分析目标,页面源码如下所示:

巴■尸‖[■■「|}■「

〈∩t‖1> 〈∩ead〉

=■||似}[『「【■『

〈ba5e∩re+≡|http://exa‖p1e°coW0/〉 〈tit1e〉[xa川p1eweb5jte〈/tit1e〉 〈/∩ead〉

▲■「△■【■『‖||◆『‖匹■「‖》‖卜凸■尸

〈body) 〈djγid=0i阳8e5〉

〈a∩Ie+=|j帕ge1.htⅦ1‖〉‖己‖∏e: ‖y加age1<br/〉〈j∏g5rc二,i爬ge1-t}M『}b.jpg0/〉</a〉 〈日∩re+=!ma8e2。hm1|〉‖己贮: "y1爬ge2<bI/〉〈j吧5r〔=‖i阳ge2ˉthu『∏b.jpg0/〉〈/己〉 〈己们Ie+≡0mage3。ht∩1|〉‖a们e: 粉y1爬ge3〈bI/〉〈1吧5r〔=,j爬ge3-t∩l』‖b.jpg,/〉</己〉 〈ahre+=!j阳ge』.∩t们10〉‖aⅦe:帅i爬ge4<br/〉〈i吧5Ⅲ〔=‖mage4ˉth‖Ⅷb.jpg|/〉≤/a〉 <ahre十≡』i帕ge5。ht川10〉‖a『∏e: 问γmage5<br/〉〈加g5rc二|加3ge5一t∩u‖b.jpg|/〉</a〉 〈/diγ〉

〈/body〉

■■『‖‖■■尸

〈/∩tⅦ1〉

3。X尸ath选择器

卜△■∏[β『|

进人ScrapyShe‖‖后,我们主要通过操作re5po∩se变量进行解析。因为我们解析的是HTML代码’



Selector将自动使用HTML语法来分析。

re5Po∩5e有_个属性5e1e〔tor,我们调用re5Po∩5e.5e1e〔tor返回的内容就相当于用re5PO∩5e的 te》《t构造了-个5e1e〔tor对象°通过这个5e1ector对象,我们可以调用如xpat‖、〔s5等解析方法, 向方法传人XPath或CSS选择器参数就可以实现信息的提取°



第15章Scrapy框架的伎用

我们用_个实例感受一下’代码如下所示: 0

〉〉〉re5u1t=re5po∩5e,5e1e〔tor.xPat‖(,//a,) )〉〉re5u1t

[<5e1ectoIxpath=!//己0 〈5e1ectorxpat‖=!//a, <5e1e〔torxp己th=|//a‖ <5e1ectorxp己th≡‖//a, 〈5e1ectorxp己t∩=|//3| 〉〉〉type(re5u1t)

data=0<ahre千≡闪j晒ge1.ht肌100〉‖a爬: "yi吨8e1〈‖〉’ data=‖<a‖re「≡闻i陋ge2.hm1圃〉‖a眶8叮1爬ge2〈‖〉’ data=‖<ahre千=口mage3·hm1同〉‖3爬:帅1帕ge3〈‖〉’ dat3=|<a∩re十="mage4.∩tⅦ1!|〉‖a爬8 "yi爬ge4<′〉’ data=,<a∩re千=闰i归ge5.ht∏1"〉‖a爬: "yi阳ge5<|〉]

■□|||‖」《■]」‖‖』■■Ⅷ‖〗‖‖■∏司||司′‖||·|·‖|」勺』●】‖】■』■■∏‖‖‖|‖|

756

5crapy°5e1e〔tor。l』∩i+ied.5e1ector1i5t

打印结果的形式是Selector组成的列表,其实它是5e1ector[15t类型, 5e1e〔tor[15t和5e1e〔tor

在上面的例子中’我们提取了a节点。接下来,我们尝试继续调用xpath方法来提取a节点内包 含的jⅦg节点,代码如下所示:

表从根节点开始提取。此处我们用了./mg的提取方式,代表从a节点里进行提取°如果此处我们用

我们刚才使用respo∩5e.5e1e〔tor.×path方法对数据进行了提取。Scrapy提供了两个实用的快捷 方法, re5po∩5巳xpath和respo∩5e.c55,二者的功能完全等同于re5po∩5e.5e1e〔tor.xpath和 re5Po∩5e。5e1ector·c55。

方便起见,后面我们统-直接调用re5po∩5e的xPatb和〔55方法进行选择° 现在我们得到的是5e1ector〔i5t类型的变量,该变量是由5e1e〔tor对象组成的列表°可以用索 引单独取出其中某个5e1e〔tor元素,代码如下所示: 〉〉〉re5u1t[O]

〈5e1ectorxpath=0//己‖ data=|<己hre「="ma8e1.h加1"〉‖a『∩e: "yj们己ge1<|)

∩yj爬ge2〈br)〈j吧5rc=0!i阳8e2-thu『∏b。jpg00〉〈/a〉|〗 !≤ahIe+="mage3.hm1"〉‖a帐8 "yi阳ge3〈br〉〈j吧 5r〔≡"i∏{age3-th帅b.jpg0』×/a〉|’ !〈ahre千=闻ma8e4.ht∏1"〉‖a爬:日yj吨ge』〈br〉〈i吧5r〔≡"j帕ge4ˉthu"b.jpg"〉</a〉』’ 〈a∩Ie+=圃j帕ge5.htⅧ1口〉‖a眠:帅i阳ge5<br〉〈i吧5rC≡赋i爬geSˉt‖u们b。jp8"〉〈/a〉!]

这里使用了extraCt方法’我们可以把真实需要的内容获取下来°

〉〉〉respo∩5e。xpat∩(|//己/text()′).extra〔t()

[|‖a『∏e: ‖yma8e1 { ’ ‖‖a爬:附j腮ge2 { ’ ‖‖a爬: "ymage3 0 ’ |‖a爬: "yi阳ge4 0 ’ ‖‖a∩e: ‖yj"age5 ! ] 〉〉〉re5po∩5e.xpath(,//己/励re「,).extm〔t()

[`j帕ge1.ht别1` ’ ,ma8e2.∩tⅦ1』’ `j"age3。hm1!’ |j∏age4.∩m1’’ !j帕ges.htⅧ1』]



■■■‖‖||」·‖|』■|■■|‖」■司|‖■Ⅲ□γ|‖‖」■■‖

我们还可以改写XPath表达式’来选取节点的内部文本和属性,代码如下所示:

』纽■{|||创■■∏|||』■■‖‖|||■■曰|

)〉〉re5u1t.eXtr己Ct()

[!〈日∩re千≡廓i阳8e1.h加1"〉‖a爬:"y加age1〈br〉〈j吧5r〔="j爬ge1-t∩Ⅷb.jpg"〉</a〉0 ’ 0<ahre千="j∏‖age2.∩t刚1厕〉‖日厕e:

‖‖

比如我们现在想提取a节点元素’就可以利用eXtmCt方法’代码如下所示:

‖‖

但是现在获取的内容是5e1e〔tor或者5e1ector[j5t类型’并不是真正的文本内容°具体的内容 怎么提取呢?



{(

我们可以像操作列表一样操作这个5e1e〔tOr[j5t。

』■〗』■|||』】可」·『』■‖■■■■‖■■■司‖司■■司□■■」■]』■]■■

//mg,则还是从htⅦ1节点里进行提取。

」{

值得注意的是’选择器的最前方加.(一个点)代表提取元素内部的数据’如果没有加点,则代

』·■‖·』

我们获得了a节点里面的所有mg节点’结果为5°

`(

〉〕〉re5l』1t.xp己t∩(0 ./i吧{) [<5e1ectorxp已t∩=』。/j吧′ d己ta宫,<j吧5rc=圃i阳8e1ˉthu帅。jpg厕〉′〉’ 〈5e1e〔toIxpath=0 ./i吧』 dat己=』〈i吧5rc匡α1∏a8e2一thu汕.jp8阐)′〉』 〈5e1ectoIxpath曰! ./j吧』 data军`〈i吧5r〔=园i帕ge3ˉt∩u帅·jpg口〉,>’ 〈5e1ectorxp己t∩=! 。/1吧‖ data≡′〈i吧5rc≡"j吧ge4ˉthu晌.jpg阐〉0〉’ 〈5e1e〔torxpath=|./i吨’ data=,〈i吧src画口mage5-t‖u油·jpg口〉『〉]

』■可|』■■‖‖』■‖』曰|■■可

都可以继续调用xpath和C55等方法来进一步提取数据°

■■■■▲厂□■■‖『◆】〗‖‖【『■『『卜】≥【『‖『■■「)似〖∩}旧|卜| ■『|‖●「|■『 ~■「|》

△尸‖|佃↓◆『户凸尸|》■尸『●



l5.3

Selector的使用

757

我们只需要再加-层/te×t()就可以获取节点的内部文本’或者加一层/@∩re十就可以获取节点的 ‖re十属性。其中, 0符号后面内容就是要获取的属性名称。

现在,我们可以用_个规则获取所有符合要求的节点’返回的类型是列表类型。

但是这里有_个问题:如果符合要求的节点只有一个’那么返回的结果会是什么呢?我们再用个实例来感受_下’代码如下所示: 〉〉〉IeSpo∩5e.xpath(‖//a[勋re+=卿j阳ge1.ht们1嚼]/text()‖)。e×tm〔t() [‖‖aⅧe: ‖ymage1 ′ ]

我们用属性限制了匹配的范围,使XPath只可以匹配到_个元素。然后用extract方法提取结果’

其结果还是一个列表形式’文本是列表的第_个元素°但很多情况下,我们想要的数据其实就是第一 个元素内容,这里我们通过加一个索引来获取’代码如下所示: 〉〉〉Ie5Po∩5e.xPath(0//a[助re+=圃m昭e1.ht们1撼]/text()|).e×tm〔t()[O] 0‖a爬: Nyi帕ge1 |

但是,这个写法很明显是有风险的。-旦XPath有问题’extra〔t后的结果可能是一个空列表°

巴「二■|尸~■「‖片【户‖‖■「『‖田◆》{尸

如果我们再用索引来获取’就可能导致数组越界°

所以’另外-个方法可以专门提取单个元素’它叫作extraCt「ir5t。我们可以改写上面的例子’ 相关代码如下: 〉〉〉Ie5po∩5e.xpatb(‖//a[0∩re于≡,0mage1.htⅦ1"]/te×t()0).extra〔t千ir5t() Wa爬:盯mage1|

这样,我们直接利用extraCt千jr5t方法将匹配的第_个结果提取出来’同时也不用担心数组越 界的问题了°

广β

■厂〔卜仕‖■β■尸‖|尸|β匹■『|卜「|户|巴■■「[■世■尸卜广·■厂■尸}血◆「}丛■‖||△■厂血∩‖「『■尸}}匹■『||》卜】β

另外,我们也可以为extm〔t千jr5t方法设置-个默认值’这样当XPath规则提取不到内容时’ 就会直接使用默认值。例如将XPath改成—个不存在的规则’重新执行代码’代码如下所示: 〉〉〉re5po∩5e.xp3t∩(0//a[0hre千="mage1"]/text()|〉.extm〔t+ir5t()



De+au1tI晌ge|

这里’如果XPath匹配不到任何元素,调用extmCt十jI5t会返回空’也不会报错。

在第二行代码中,我们还传递了一个参数当作默认值’如0e十au1tI"age°这样’如果Ⅻath匹 配不到结果,返回值会使用这个参数来代替,可以看到输出正是如此。

到现在为止’我们了解了Scrapy中的XPath的相关用法,包括嵌套查询、提取内容`提取单个内 容、获取文本和属性等。

4.cSS选择器

接下来,我们看看CSS选择器的用法°

Scrapy的选择器同时还对接了CSS选择器,使用re5po∩5e.〔55方法就可以使用CSS选择器来选 择对应的元素了°

例如在上文我们选取了所有的a节点,那么CSS选择器同样可以做到’相关代码如下: 〉〉〉reSpo∩5e.〔55(,a|)

[〈5e1e〔torxpat∩={de5〔e∩da∩tˉorˉ5e1卡8 8a| d日ta≡0〈a∩Ie十="j阳ge1.∩tⅦ1"〉‖a‖e: ‖yma8e1〈』〉’

〈5e1ectoIxpatb=0de5〔e∩da∩tˉoIˉ5e1十: 2a‖ data=!〈己hre十=口i帕ge2·ht‖1">Na眠: 日ymage2〈‖〉’ 〈5e1e〔torxp己t∩=0de5ce∩da∏tˉorˉ5e1十8 :a0 data=|<己∩re+="mage3.∩t∏U"〉‖a眠8 "yj阳8e3〈0〉』 〈5e1ector×pat∩二‖de5〔e∩da∩tˉorˉse1千8『a‖ data=『<a∩re+=闽mageq。∩tⅦ1匝〉‖a爬: 付ymage』〈』〉』

〈5e1e〔toIxpath=‖de5〔e∩da∩tˉorˉ5e1千: :a』 dat日≡』〈ahre+≡"mage5.∩t们1"〉‖a『∏e: "ymage5<‖〉]

同样’调用eXtraCt方法就可以提取节点’代码如下所示:

—〔

↑5

第l5章Scrapy框架的使用

〉〉〉re5pO∩5e°〔55(!a『).extIa〔t〈)



匹 }

[ !〈a∩re+=阐1mage1.htⅦ1.〉‖a∩e:"y1帕ge1<br〉〈i昭5r〔≡口i阳ge1-t∩uⅦb。jpg"〉</a〉|’ 0<ahIe「≡"ma8e2.ht∏r0〉‖a眠: ‖ymage2<br×i吧5r〔=阐i爬ge2=t∩u巾.jpg■〉〈/a〉|’ ,〈日hre十≡。j阳ge3.‖m1圃〉‖a爬: ‖γi爬ge3〈br〉<i吧 5rc=!』mage3-t∩u晌.jpg圃〉〈/a〉! ’ ‖〈ahre「=闻加age4.htⅦ1"〉‖a爬:"ymageq<br〉〈mg5I〔≡"iⅧage4ˉthuⅦb.jpg"〉</a>0 ’ |<ahre+=■i阳ge5.∩t∏1口〉‖己爬:附i∏Ege5〈br〉<i吧5rc=.j∏Ege5-t∩u∏b.jpg"〉</a〉′] 可以看到’用法和XPath选择是完全-样的°

另外’我们也可以进行属性选择和嵌套选择’代码如下所示: 〉〉〉re5po∩5e。c5s(|a[∩Ie于=制j帕ge1.们t∏1"]|).extmct()

[‖〈ahre+≡00i爬ge1.hm1"〉‖aⅦe: "yi帕ge1(bI〉〈mg5r〔=闻jⅧage1—thu川b.jpg"〉〈/a〉』] >〉)re5po∩5e.〔55(0a[hIe「≡圃j爬ge1.ht们1"] 1吧!)。extra〔t() [,〈j吧5rC=圃i阳ge1ˉthu|∏b.jpg闯〉‖]

这里用[hre十=』,i们age。htⅧ1"]限定了∩re十属性,可以看到匹配结果就只有一个了。另外如果想查 找a节点内的1爪g节点,只需要再加_个空格和1们g°选择器的写法和标准css选择器写法如出一辙° 我们也可以使用extm〔t十1r5t方法提取列表的第_个元素’比如: 〉〉〉re5po∩Se.〔55(,a[∩Ie于="mage1.htⅦ1圃] i吧‖).e×tm〔tˉ十1r5t() 0≤mg5rC≡口mage1=t打u帅.jpg"〉!

接下来的两个用法不太_样°节点的内部文本和属性的获取是这样实现的: 〉〉〉re5po∩5e.c5s(『a[∩re「="1|『|a8e1.ht‖100]: :text|).extr日ct「ir5t() |‖3贬: ‖ymage1 ‖

〉〉〉re5po∩5e.〔55(‖a[hre「=00i帕ge1.hm1"] i吧::attr(5r〔)0).extmct+jr5t() ‖1‖E8e1-t‖u∏由.jpg『

获取文本和属性需要用::text和::attr的写法,而其他库如BeautifUlSoup或pyqueIy都有单独 的方法°

另外,CSS选择器和XPath选择器—样’能够嵌套选择°我们可以先用XPath选择器选中所有a节

点’再利用CSS选择器选中1Ⅶg节点’然后用XPath选择器获取属性°我们用一个实例来感受一下’ 代码如下所示: 〉〉〉Ie5PO∩Se.×Path(|//己|).〔55(′j吧『).xPath(』05rc』).extra〔t(〉 [|jⅧage1ˉthu|‖b.jp8』’ 』mage2-thⅧb。jpg{ ’ |ma8e3-thu∏b.jpg』’ ‖ma8e』—t∩u"b.jpg,’ |mage5ˉt∩u『∏b。jpg|]

我们成功获取了所有j‖g节点的5rC属性。

因此,我们可以随意使用Xpath和〔55方法,二者自由组合实现嵌套查询,它们是完全兼容的。 5.正则匹配

Scrapy的选择器还支持正则匹配°比如在示例的a节点中’文本类似于‖日"e: "yiⅦage1,现在 我们只想把‖a"e:后面的内容提取出来,就可以借助re方法,代码实现如下:

我们给re方法传了_个正则表达式’其中(.*)就是要匹配的内容,输出的结果就是正则表达式 匹配的分组’结果会依次输出° 如果同时存在两个分组,那么结果依然会被按序输出’代码如下所示: 〉〉〉IeSpO∩5e.xpath(|//a/text()』).re(』(.*?):\5(.*)!)

[|‖a∩侣′’ 0叮j∏Ege1`’’‖田肥|’ !叮j∏Ege2 』’ 』‖■肥‘ ’ 『叮j∏Bge3 |’№怔|’|帅i∩■8e4 ,’ |‖田庇′’ |帅j∏Ege5 0 ]

类似eXtmCt十ir5t方法, re千1r5t方法可以选取列表的第_个元素,用法如下: 〉〉〉Ie5Po∩5e。xPath(0//a/text()|)。reˉ于1I5t(′(.*?):\5(.*)‖)

|判』■]』■】■〗‖|‖』‖■■■■□‖‖‖■■可β■■可‖‖‖°』■■■■■■〗‖‖|

〉)〉Ie5po∩5e.xp3th(‖//a/text()|).re(|‖a眠:\5(.*)0) [|"ymage1 0’ |帅mage2 ′ ’ ‖‖ymage3 0 ’ |‖yma8e4 ‖ ’ 0问y1‖age5 0]



·|』■‖{■∏』■】‖』】■‖‖〗<』司‖|』■】■〗∏】■凹』■■〗〗』】■】Ⅲ】‖■】■‖∏』■】●Ⅱ‖|●】』■|‖●〗‖■‖|』■■】』■‖□■‖』·°』■】‖」‖‖〗□〗』‖‖‖』■|』■】·



758

0』

‖‖a∏记{

〉〉〉re5Po∩5e.xPat∩(!//己/text()0).reˉ+ir5t(刊a爬:\5(.*)|) |"yi阳ge1|

( ‖| ‖‖







| 「}



l5.4

Spider的使用

759

不论正则匹配了几个分组,结果都会等于列表的第_个元素°

值得注意的是, re5po∩5e对象不能直接调用re和re千1r5t方法。如果想要对全文进行正则匹配’ 可以先调用xpath方法再正则匹配,代码如下所示: 〉〉〉Ie5pO∩5e。re(刊a爬:\5(.*),) 『ra〔eba〔低 (加5treCe∩t〔a111a5t): 「i1e"〈〔O∩5O1e〉冈’ 1i∩e1’ 1∏〈帅du1e〉

∧ttribute[rror: 』毗∏1Re5po∩5e| obje〔thas∩oattrjbute 『re‖ 〉〉〉re5po∩5e.xpat打(0 . 0).re(‖a′∏e:\5(.*)<br〉!)

["yj‖age1|′ !‖ymage2| ’佣ymage3 0 ’Ⅷymage《 , ’ⅧyjⅦage5 |] 》‖|)》■「■■{

〉>>Ⅲespo∩5e.×path(0 . !).re-十ir5t(0‖己贬:\5(.*)〈br〉!) 0问ymage1 0

通过上面的例子我们可以看到’直接调用Ie方法会提示没有re属性°但是这里首先调用了 xpat∩(|.|)选中全文’然后调用了re和re十jrst方法’就可以进行正则匹配了°

■尸》卜‖‖尸

6总结

以上便是Scrapy选择器的用法,它包括两个常用选择器和正则匹配功能°熟练掌握XPath语法` CSS选择器语法和正则表达式语法,可以大大提高我们的数据提取效率°

伊|■■『岛■■》广β尸|》巴■

↑5ˉ4Sp|de『的使用 在Scrapy中,网站的链接配置、抓取逻辑、解析逻辑其实都是在Spider中配置的°在前_节的 实例中,我们发现抓取逻辑也是在Spjder中完成的°本节我们就来专门了解一下Spider的基本用法。 ↑.Sp|de『运行流程

在实现Scrapy爬虫项目时’最核心的类便是5p1der类了’它定义了如何爬取某个网站的流程和

『|β∩

解析方式°简单来讲, Spider就是要做如下两件事: □定义爬取网站的动作;

□分析爬取下来的网页° =■厂‖‖尸炉巴∏ ■尸匹尸山■‖》■■尸

■■尸伊匹∩■【■「■■尸‖■■■■■『

卜 |〖尸}巴■「‖|‖‖【■■



对于5pider类来说’整个爬取循环如下所述° (l)以初始的URL初始化Request并设置回调方法。当该Request成功请求并返回时,将生成 Response并将其作为参数传给该回调方法。 (2)在回调方法内分析返回的网页内容°返回结果可以有两种形式’_种是将解析到的有效结果 返回字典或Ite们对象’下_步可直接保存或者经过处理后保存;另-种是解析的下—个(如下一页)

链接’可以利用此链接构造Request并设置新的回调方法’返回Request° (3)如果返回的是字典或Ite『『l对象’可通过FeedExpons等形式存人文件’如果设置了Pipeline, 可以经由Pipe‖jne处理(如过滤`修正等)并保存° (4)如果返回的是Reqeust’那么Request执行成功得到Response之后会再次传递给Request中定 义的回调方法’可以再次使用选择器来分析新得到的网页内容’并根据分析的数据生成Item° 循环进行以上几步’便完成了站点的爬取°

2.5pjder类分析 在上_节的例子中’我们定义的5p1der继承自5〔mpy.5p1deI5.5p1der’即5〔rapy.5pjdeI类’二 者指代的是同一个类,这个类是最简单最基本的5pjder类’其他的5pider必须继承这个类。

这个类里提供了5tart—reque5t5方法的默认实现,读取并请求5tartur15属性,然后根据返回 的结果调用par5e方法解析结果。另外它还有一些基础属性,下面对其进行讲解。



第15章Scrapy框架的使用

760

□∩a‖e:爬虫名称,是定义5pider名字的字符串。5pjder的名字定义了Scrapy如何定位并初始

化5p1der,所以它必须是唯一的。不过我们可以生成多个相同的5pjde工实例’这没有任何限 制°∩a们e是5pjder最重要的属性,而且是必须的。如果该5pjder爬取单个网站,一个常见的

做法是以该网站的域名名称来命名5p1der。例如5p1der爬取mywebsite.com,该5pider通常 会被命名为mywebsite。

□a11o"eddo"|己1∩5:允许爬取的域名,是_个可选的配置’不在此范围的链接不会被跟进爬取。 □startur15:起始URL列表’当我们没有实现5tartˉreque5ts方法时,默认会从这个列表开 始抓取。

□cu5to∏5ett1∩g5:_个字典’是专属于本5p1der的配置,此设置会覆盖项目全局的设置’而 且此设置必须在初始化前被更新,所以它必须定义成类变量.

□〔raw1er:此属性是由十roⅧcmw1er方法设置的,代表的是本5pjder类对应的〔raⅦ1er对象’ 〔raW1er对象中包含了很多项目组件,利用它我们可以获取项目的一些配置信息,常见的就是

| q

| ‖

获取项目的设置信息’即5ettj∩g5°

□5etti∩g5:_个5ett1∩g5对象,利用它我们可以直接获取项目的全局设置变量° 除了一些基础属性’5pjder还有一些常用的方法,在此介绍如下°



□5tartˉrequeSt5:此方法用于生成初始请求,它必须返回_个可迭代对象’此方法会默认使用 5tartur15里面的URL来构造ReqUest’而且ReqUest是GET请求方式°如果我们想在启动时以 POST方式访问某个站点,可以直接重写这个方法,发送POST请求时我们使用「omReque5t即可。

□par5e:当Response没有指定回调方法时,该方法会默认被调用’它负责处理Response,并从 中提取想要的数据和下_步的请求,然后返回。该方法需要返回一个包含Request或Item的 可迭代对象。

□〔1o5ed:当5pideI关闭时,该方法会被调用,这里一般会定义释放资源的_些操作或其他收 尾操作。

3.实例演示

接下来我们以一个实例来演示一下Spjder的一些基本用法°首先我们创建一个Scrapy项目,名 作scraDvsDjderdemo,创建项目的命令如下: 字叫作scrapyspjderdemo,创建项目的命令如下: 5crapy5t己rtproject5〔rapγ5piderde∏℃

运行完毕后’当前运行目录便出现了—个scrapyspiderdemo文件夹’即对应的Scrapy项目就创建 成功了。

scrapyge∩5p1derhttpbi∩硼w.httpbi∩°oIg

这时候我们可以看到项目目录下生成了_个‖ttpbj∩5p1der,内容如下: jⅦpOrt5Cr己pγ

〔1a55‖ttpbj∩5pider(5cmpy.5pjder): ∩a们e= 0∩ttpbj∩0 a11oweddα∏aj∩s= [|"Ⅷ·∩ttpbj∩.org』]

5tartuI1B= [0http5://…。httpbj∩°org/|]

de「par5e(5e1于’ re5po∩5e):

口』】·||√」·

接着我们进人demo文件夹’来针对wwwhttpbino吧这个网站创建一个Spider’命令如下:

」↑

| (

( 0



paS5 q

这时候我们可以在par5e方法中打印输出_些re5po∩5e对象的基础信息同时修改5tartur15 为h忱ps://wwwh忱pbin.org/get’这个链接可以返回GET请求的_些详情信息’最终我们可以将Spider



|{

15.4

Spider的使用

76l

修改如下: 1ⅦportS〔mpy 》卜『|卜『■『‖卜|■【|卜‖‖仿‖【厂|■『『卜|■厂|′|也β|》「|匡尸「『『[尸‖》′}户β》|′份『卜■

〔1己55‖ttpbi∩5pider(5〔rapy。5pider): ∩a『∏e= 0httpbi∩0

a11oweddo‖aj∩5= [Www.∩tt曲i∩。org0 ] 5tartur15≡ [』∩ttp5://删w.∩ttpbj∩.org/get, ] de十paI5e(se1f’ respo∩5e); pri∩t(|ur1! ’ Iespo∩5e.uⅢ1) pri∩t(‖reque5t0 ’ re5po∩5e.request) Pri∩t(!5tat050 ’ respo∩5e。5t己tu5) prj∩t(‖‖eader5 ’ re5po∩5e.he己der5) pri∩t(‖text0 ’ respo∩5巳text) pIi∩t(|∏eta‖’ re5po∩5e.眶ta)

这里我们打印了re5Po∩5e的多个属性°



□Ur1;请求的页面URL’即RequestURL。 □reque5t: re5po∩5e对应的reque5t对象° □5tatU5:状态码,即Re5po∩5e5tatus〔ode° □header5:响应头’即Re5po∩5e‖eaders。 □text:响应体,即Re5po∩5e8ody° □‖eta:-些附加信息,这些参数仲往会附在"eta属性里.



∩■□●伊

运行该Spjder,命令如下: 5cr己pyCraw1httpbi∩

运行结果如下:

巴厂■[β「■■■■|[■ 【●『‖「尸「|=■「■|[尸}

匹■■「‖●【尸|‖卜任|炉『‖||『匹∩‖|



卜)

■ ■ 厂 ■ ■ ■ 尸



202oˉ08ˉ〕001:37:11[5〔mpy.core.e∩gi∩e]D[8[」C:〔r3w1ed(2O0)〈C[丁∩ttp5://哪·‖ttpbj∩.org/get〉(re+erer:‖o∩e) uI1‖ttp5://硼°∩ttpbi∩.org/get req‖e5t〈C[『http5://州。httpbi∩.org/get〉 5tatu52m

headers{b′0己te』: [b|5at’ 29∧ug∑o2o17:37:11酗丁』]’ b0〔o∩te∩tˉ『ype‖ : [b‖app1jcatjo∩/j5o∩!]’ b‖5erγer : [b|gl」∩j〔or∩/19.9·0‖ ]」 b』∧〔〔e55ˉ〔o∩tro1ˉA11owˉ0rjgj∩‖: [b!*, ]’ b‖∧cce55ˉ〔o∩tm1ˉ∧11o"ˉ〔rede∩tja150 : [b‖tme|]} text{ "己rgS!0 : {}’ "beaderS!! : { "∧C〔ePt"g "text/ht川1’app11〔atio∩/x∩t‖1+m1’日PP1i〔atio∩/×∩1;q=O·9’*/*;q=O·8||’ "∧〔〔eptˉ[∩codi∩g": "8∑jp′ de十1ate"’ 00AC〔eptˉ[a∩guage00 8 "e∩"’ 〖『‖o5t": "删w·∩ttpbj∩°org"’ ‖0l」seIˉ∧ge∩t厕; "5crapy/2.2.1〈+http5://5〔rapy.org)"’ "Xˉ肋z∩ˉ丁m〔eˉId口: 00Root≡1ˉ5千4a9247ˉ770十3“dO8d+9a6c18daa55300

}’ "oIig1∩": 00219.142·145。226′0』 闪ur1": "https://州·∩ttpb1∩·org/get!0 }

Ⅶeta{|do"∩1oadtj们eout! : 18O。O’ 0dow∩1oads1ot|: |"肌‖.httpbi∩·org0 ’ ‖dow∩1o日d1日te∩〔y‖:o。277271O32333374} 2020ˉ08ˉ30o1:37:11 [scr己py.core.e∩gj∩e] I‖「0:〔1o5j∩85pideI (+j∩j5∩ed)



以上省略了部分结果’只摘取了关键的par5e方法的输出内容°

■■■『}【■∏『|

可以看到’这里分别打印输出了ur1` reque5t、5tatu5、‖eadeI5` text、们eta信息°我们可以观 察一下’text的内容中包含了我们请求所使用的UsePAgent`请求IP等信息’另外"eta中包含了几 个默认设置的参数。

卜 『

P =

· 勺

第15章

762

Scrapy框架的使用 {

注意’这里并没有显式地声明初始请求,是因为Spjder默认为我们实现了_个5tart=reque5ts方 |

法’代码如下: de+5tart-req0eSt5(5e1「): 千Orur1j∩5e1千.5taItur1S:



yie1dReque5t(ur1了do"t「j1ter≡「rue)



可以看到,逻辑就是读取5tartur15然后生成Request’这里并没有为Request指定〔a11baC代,

默认就是par5e方法°它是一个生成器,返回的所有Request都会作为初始Request加人调度队列。 自定义请求页面链接和回调方法,可以把Startˉreque5t5方法修改为下面这样: 1呻ort5〔rapy

千ro∏‖ 5〔r己pyi∩portReque5t

〔1a55‖ttpbm5pjder(5〔mpy.5pider):

』■Ⅷ‖』可‖|■■』‖‖』·】』·‖|

因此’如果我们想要自定义初始请求,就可以在Spider中重写5taItˉreque5t5方法’比如我们想

0

∩a‖e≡ 0httpbi∩‖

a11oweddma1∩5=[!….httpbj∩.org0 ] 5tartur1= |bttp5吕//州.∩ttpbj∩。org/get| he己deI5≡{

0U5erˉ∧ge∩t‖: ‖"oz111a/5°o(阳〔i∩to5‖j I∩te1"ac05X1015』)∧pp1e‖ebRjt/53736(阳『"[’11促e6e〔促o) 〔‖rα爬/83·O·4103。1165a+arj/537°36!

〔oo代1e5≡{0∩a们e! : ‖gemey|’ 0己ge0 : !260}

de于5tartˉkeque5ts(se1「): 「oro仟5eti∩ra∩ge(5); ur1≡5e1十.Startur1+十|?O仟5et={O仟5et}|

γje1d∩eque5t(ur1′ headeI5=5e1十。header5’ 〔ookie5=5e1十.〔oo代je5’

〔a11ba〔促=5e1+。par5ere5po∩5e’ ∏)eta≡{‖o仟5et, : o仟set})

上了Query参数,如o仟5et=0,拼接到https://Wwwhttpbino!g/get后面’这样请求的链接就变 成了https://wwwhttpbjnoIg/get?ofTSet=0°

门‖|‖‖」

□ur1:我们不再依赖5tartur15生成ur1’而是声明了一个5tartur1,然后利用循环给URL加

■■习‖』■■■■Ⅵ||』■日

泣里我们自定义了如下内容°

{‖

de千par5eˉre5po∩5e(5eM’ re5po∩5e); prj∏t(0ur1|’ re5pO∩5e.ur1) prj∩t(|reque5t‖’ Ie5po∩5e。reque5t) prj∩t(‖statu5‖’ re5Po∩5e.5tato5) pri∩t(0header5 ’ re5po∩5e。headers〉 prj∩t(!te×t|’ re5pO∩5e.text) pIi∩t(|爬ta』’ re5po∩5e。爬ta)



■∏‖■■』司二勺』■‖」■‖■‖■■‖』‖‖□‖·■





□he日der5:泣里我们还声明了header5变量,为它添加了05er_∧ge∩t属性并将其传递给Request 的header5参数进行赋值。

法进行处理°

□Ⅷeta:爬ta可以用来传递额外参数,泣里我们将o仟5et的值也赋值给R闪`胆st’通过Ie5po∩5巳爬ta 重新运行看看效果’输出内容如下:

2020ˉ叫ˉ3oo1:叫:19[5cr日W.core.曰唱i『P]0[α几:α副‖1ed(2")〈旺『https://哗0.h仕恤1∩.oIg/get?o仟set=1〉(Ie{erer:归记) 2o20ˉ叫ˉ3o01:剑:19[5〔rapy.〔ore·曰旧1肥]0[队几:α翻1ed(2")〈C[丁https://哪httpbj∩.org/8et?o仟5et≡2>(re十erer:恤冶〉

|‖ ((

就能获取这个内容了’这样就实现了Request到Response的额外信息传递°

‖|』|

□ca11bacⅨ:在‖ttpbi∩5pider中,我们声明了-个par5ere5po∩5e方法,同时我们也将Request的 〔a11bac|〈参数设置为paI5eIe5po∩5e’这样当该Request请求成功时就会回调par5ere5po∩5e方



|』●■■■■‖|

□〔oo促je5:另外我们还声明了Coo虹e,以_个字典的形式声明’然后传给Rcquest的coo|(1e5参数°

|』』■■■

■司

l5.4

Spider的使用

763

2O2O_08ˉ30O1:到:19[scmW。core.曰旧j爬]旺队L:〔mded(2凹)〈庄『|]ttp5://b凸凹vhtt恤m.o】g/霹?o仟set≡3〉(Ie↑它reI:陋论)

2O20ˉ明ˉ3OO1:弘:19[scraW.core.曰哩爬]0[H几:〔I副‖1ed(2凹)〈C[丁httpB://灿Ⅷ。htt曲j∩.oIg/get?o仟Set=4〉(re↑它rer:陋记)



ur1∩ttp58//棚。httpb1∩·or8/8et?o仟set=1

reql』e5t<6[丁们ttps://0卿‖°bttpbi∩.or8/get?o仟5et≡1〉 5tatu52m



header5{b,D3te』: [b』5at’ 29∧ug202O17:54:19硼『0]’b』〔o∩te∩tˉ丁ype』: [b,己pp1i〔atio∩/j5o∩,]′ b05erver0 : [b!gu∩i〔or∩/19·9。o0]’ b‖∧〔〔e55ˉ〔o∩tro1ˉ∧11o0‖ˉ0rjgi∩|: [b』*‖ ]’ b0∧〔〔e55ˉ〔o∩tro1ˉ∧11o00ˉ〔rede∩tja15! : [b{true‖]}

卜||卜|尸「|卜■尸「尸■

text{ "arg5阐:{

”O仟5et": 闪1圃

}’ "∩eader5闻目 {

"∧〔cePt口: 阅text/ht川1』app1icatio∩/xht阴1+x川1’己pp1jc3tio∩/×Ⅶ1jq=0.9’*/*’q=o。8口’ w∧c〔eptˉ[∩codj∩g": 口gzjp’ de「1ate"』 0!∧CCeptˉ[a∩guage": "e∩"’ "〔oo代je■; "∩a爬=gerⅧey】 age=26"’ 闻}|o5t": 口….httpbj∩°org口》

G

=■「〉β「)伊

.U5erˉ∧ge∩t憾: "№zj11a/5.O(‖ac1∩to5h; I∩te1肥〔O5X10154)∧pp1e‖ebNit/537.36(朋丁川’1j低e6e〔戊o)

〔hrα肥/83。O·41O3.1165a「ari/5〕7.36"’ "Xˉ枷∑∩ˉ「I己ceˉId|: "Root=1ˉ5千4己96』bˉ89q8a0〔4千〔6〔d3a〔2e5573a〔" }’ 同oI1gi∩口: 口219。1耳2°1q5·226"』

"ur100 : "http58//训咖。httpbj∩.org/get?o仟5et=1"



广

爬t己{,o仟5et‖8 1′ !do0‖∩1oadtj爬out|: 180.O’ ‖do切∏1oad51ot』: ‖№州。httpbi∩橇org‖’ |dow∩1o3d1ate∩cy』:

厂卜

0.61040616O35461q3}

ur1‖ttp58//“。∩ttpbi∩。org/get?o仟5et=2 reque5t〈C[丁http5日//哪.httpbi∩·org/get?o仟5et=2〉

■『■‖巴矽

这时候我们看到相应的设冒就成功了°

□ur1: ur1上多了我们添加的Que1y参数°

’「●

□text:结果的header5可以看到Cookie和UsePAgent’说明Request的Cookje和UserˉAgent都 设置成功了。 □‖∏eta: 阳eta中看到了O仟5et这个参数,说明通过"eta可以成功传递额外的参数°

b

通过上面的案例’我们就大致知道了Spjder的基本流程和配置,可以发现其实现还是很灵活的。

p

当然除了发起GET请求’我们还可以发起POST请求°POST请求卞要分为两种,一种是以Fo∏n

p





‖0

Data的形式提交表单,一种是发送JSON数据’二者分别可以使用「omReque5t和〕so∩Reque5t来实 现。例如我们可以分别发起两种POST请求,对比-下结果:

p

i呻oItS〔mpy 十r咖5〔mpy.httpj呻ort〕5o∩Reql』e5t’「omReql』e5t ‖口

〔1a55‖ttPbi∩5pjdeI(5crapy.5pjder): ■厂卜∩》■「‖广■■尸『△■「■『

∩a爬= 0bttpbj∩0

a11…dd咖ai∩5≡ [‖州。∩ttpbi∩.org!] 5tartur1= 0http5;//瞅·httpbi∩·org/post0 data={{∩a眶0 8 !8er∏w’ ,age! : !26‖} de千5tart-reql』e5t5(5e1十): yie1d「omRequeSt(5e1十·5tartUI1’ 〔a11bac促=5e1+·p日r5e-re5po∩5e’

』■■■■■■

+om圃3ta=Se1+.d日ta)

yje1d〕5o∩Reque5t(5e1十.5tartur1’ ca11ba〔低霉5e1于·par5e=re5po∩se’ data=5e1千.dat日)

↑5 k

■■■

de千p己r5e-re5po∩5e(5e1十’ re5po∩5e〉: pIi∩t(‖text0 ’ re5po∩5e.text) 巴■「|‖‖‖『‖|‖‖■■「}■■■∏‖『‖『匡尸

这里我们利用5tartˉreque5t5方法生成了-个「omReque5t和〕5o∩日eque5t’请求的页面链接修改





第l5章Scrapy框架的使用

7“



·

q

为了https://wwwhttpbinorg/post’它可以把POST请求的详情返回,另外data保持不变° 运行结果如下:

2O2Oˉ08ˉ30o2:11:38 [5crapy.core.e∩g1∩e]D[B‖L8〔raw1ed(20O)〈pO5丁http5://‖w山‖.httPbi∩.oIg/Po5t〉(re十erer:№∩e) teXt{

‖0argS|0 : {}’





"data"8 ""’

"十j1eS"; {}’ !干Om": { age": "26"’ "∩己"e": "gemw" }’ "headerS":{



00

q



"∧c〔ept": "text/ht∏1’app1i〔atjo∩/x∩tⅦ1+x‖1’app1jcat1o∩/xⅦ1;q=O.9’*/*;q=O·8N’ !0A〔〔eptˉ[∩〔odi∩g": "gzip’ de千1ate00’ ∏

"A〔〔eptˉLa∩gu日ge :

■■



e∩ ’

"〔o∩te∩tˉ[e∩8t∩": "18"’

"〔o∩te∩tˉ『ype : 己pp11〔己tjo∩/xˉ…ˉ千omˉuI1e∩〔oded"’ Ⅲ

00



| q

00

00‖o5t": "哪.httpbj∩.org 」

"05eIˉ∧8e∩t阐: !05〔I己Py/2.2.1 (+httP5://S〔r己Py.Org)"’

00Xˉ蛔Z∩ˉ「m〔eˉId0 : "Root=1ˉ5十4a9日59=千0ad26千76d7577c+〔b201d〔6咖 }’

q



00

"jso∩ : ∩u11’ "origi∩": "219°142.145.22600’ 00ur100 : "http5://叭州.∩ttpbj∩.org/po5t"





} ‖d

20n0ˉ08ˉ3Oo2:11:38[5〔rapy°core.e∩gj∩e]D[B(L:〔r3w1ed(2")〈pO5「http5://‖‖曲‖.httpbj∩.org/po5t〉(Ie+erer: ‖o∩e) teXt{ aIg5": {}’ 】■





0』data!0 : "{\"age\圃: \‖026\"’ \"∩a爬\徽: \"ger∏冶y\"}"’

|千11e5": {}」 "十OrⅧ": {}’ "‖eader5": {





"∧〔〔ept": "app1jcatjo∩/j5o∩’ text/j3va5〔rjpt’ */*】 q≡o.01回’ "∧〔〔eptˉ[∩cod1∩g00 : "gzip’de于1ate圃’ "∧〔Ceptˉ[a∩g0age‖0 : "e∩"少 "〔o∩te∩tˉle∩gth00 : "〕1"’ "〔o∩te∩tˉ丁ype": "app1icatjo∩/j5o∩ ’ ∏

0

{ 0







00

"‖o5t": 0o….‖ttpbi∩°or8 ’

q

"05erˉAge∩t": "5crapy/2.2.1(+∩ttp5://5cmpy.org)"’

"0Xˉ枷z∩ˉ丁raceˉId"; "Root=1ˉ5佃a9己5aˉ5oa95cd〔881a9dcob十3〔9十28" }’

"j5o∩":{ age||: 厕26"’





q

0U

00

∩a‖∏e"8 ‖!geI爬y"

}’

"oIigi∩": "219.1』2。145°216"’ |0ur1o0 ; 00http5://卿·httpbj∩.oIg/po5t" }









d

q

这里我们可以看到两种请求的效果。

第_个「omReq|』e5t’我们可以观察到页面返回结果的千om字段就是我们请求时添加的data内 容’这说明实际上是发送了〔o∩te∩tˉ丁ype为app1i〔atjo∩/xˉ"wwˉ+omˉur1e∩coded的POST请求,这种 对应的就是表单提交°

第二个]5o∏Reque5t’我们可以观察到页面返回结果的jso∩字段就是我们所请求时添加的data内 容’这说明实际上是发送了〔o∩te∩tˉ『ype为app1j〔at1o∩/j5o∩的POST请求’这种对应的就是发送 JSON数据。

这两种POST请求的发送方式我们需要区分清楚’并根据服务器的实际需要进行选择。

《|■■‖|』□■∏‖‖{|□■】‖|』■■∏||』=■‖|■■■可■■∏』■□|





□可』■】引||』■|‖

·【■【■〗【■Ⅱ『■尸〖‖|∩皿■「

| ‖|巴尸『「|伪■∩[‖「■}|■厂‖‖‖·

|【尸「■ˉ■‖卜‖■厂「■■「[『「|△■「‖▲■「日炉

■厂『卜■■卜■厂

P

β 卜

p



l5.4

Spider的使用

765

4∩equest和只espo∩se

在上面的SPider例子中’大部分流程实际是在构造Request对象和解析Response对象’因此对于 它们的用法和参数我们需要详细了解_下° ●RequeSt

在Scrapy中,【eque5t对象实际上指的就是s〔rapy.∩ttp.Reque5t的一个实例,它包含了HTTp请

求的基本信息’用这个Reque5t类我们可以构造Reque5t对象发送HTTP请求,它会被Engine交给 Downloader进行处理执行’返回_个Respo∩se对象°

这个Reque5t类怎么使用呢?那自然要了解_下它的构造参数都有什么’梳理如下。 □ur1: Reque5t的页面链接’即Reque5t0R[° □〔a11bac代: Reque5t的回调方法’通常这个方法需要定义在5pjder类里面’并且需要对应一个

re5po∩5e参数,代表Reque5t执行请求后得到的Re5po∩5e对象°如果这个cj11bac代参数不指 定,默认会使用5pider类里面的par5e方法° □们et∩od: Reque5t的方法’默认是C[『,还可以设置为P05『、 P0「、 0[[[『[等。

□们eta: Reque5t请求携带的额外参数’利用爬ta’我们可以指定任意处理参数’特定的参数经 由Scrapy各个组件的处理,可以得到不同的效果°另外,爪eta还可以用来向回调方法传递信息。 □body: Reque5t的内容,即Reque5tBody’往往Reque5t8ody对应的是POST请求,我们可以 使用「omReque5t或〕5o∩Reque5t更方便地实现POST请求° □headers: Reque5t‖eader5’是字典形式° □cookje5: Request携带的Cookje’可以是字典或列表形式° □e∩〔odj∩g: Reque5t的编码’默认是ut十ˉ8。

□prority: Reque5t优先级,默认是0’这个优先级是给Scheduler做Request调度使用的’数值

尸‖

越大,就越被优先调度并执行°

■「}卜■厂巴[尸|[∏|■■||β|■「■■「匹∏||匹「|厂‖|□|》||『■「|‖巴「■厂|』「||■∩■■■「■厂』■■■】【【=■『【〗‖匹▲■『‖「||巳■∩『|||■■厂}

□do∩t+11ter: Reque5t不去重, Scrapy默认会根据Reque5t的信息进行去重’使得在爬取过程 中不会出现重复请求,设置为『rue代表这个Reque5t会被忽略去重操作,默认是「a15e。 □errba〔促:错误处理方法,如果在请求处理过程中出现了错误’这个方法就会被调用。

□十1ag5:请求的标志’可以用于记录类似的处理。 □〔bˉ代"arg5:回调方法的额外参数’可以作为字典传递° 以上便是Reque5t的构造参数,利用这些参数’我们可以灵活地实现Reque5t的构造° 值得注意的是’"eta参数是一个十分有用而且易扩展的参数’它可以以字典的形式传递’包含的

信息不受限制’所以很多Scrapy的插件会基于爬ta参数做一些特殊处理。在默认情况下, Scmpy就 预留了—些特殊的低ey作为特殊处理° 比如reque5t.‖eta[!proxy!]可以用来设置请求时使用的代理,reque5t."eta[,们axˉretryˉtme5|] 可以设置用来设置请求的最大重试次数等。

更多具体内容可以参见: https://docs.scrapy.o【g/en/latest/topics/requestˉ爬sponsehtml#requestˉmetaˉ s庐cialˉkeys。

另外如上文所介绍的’Scrapy还专门为POST请求提供了两个类——「omReque5t和〕5o∩Reque5t’ 它们都是Reque5t类的子类,我们可以利用「omReque5t的于or们data参数传递表单内容,利用 〕5o∩Reque5t的j5o∩参数传递JSON内容’其他的参数和Reque5t基本是_致的°二者的详细介绍可 以参考官方文档:



』■

』】·』

766

第l5章Scrapy框架的佳甩

□「or∏|Reque5t: https://docs.scrapy.org/en/latest/topjcs/『四uestˉresponsehtml#fbrmIequestˉohjects □〕5o∩Reque5t; https://docs.scIapyo【g/en/latest/topics/requestˉresponsehtml#jsonrequest

Reque5t由Dow∩1oadeI执行之后,得到的就是Re5po∩5e结果了’它代表的是HTTP请求得到的响 应结果’同样地我们可以梳理_下其可用的属性和方法’以便我们做解析处理使用。

|‖‖」』

●Response



□ur1: Reque5t0R[。

□body: Re5po∩5eBody’这个通常就是访问页面之后得到的源代码结果了,比如里面包含的是 HTML或者JSON字符串,但注意其结果是bγte5类型°

q

日 □ 厂 ‖ ■

□Statu5: Re5po∩5e状态码’如果请求成功就是200° □‖eaders: Re5po∩5e‖eader5,是_个字典’字段是-一对应的°

□reque5t: Re5po∩5e对应的尺eque5t对象° □certj+iCate:是t"j5ted.1∩ter∩et.551.〔ert1十1〔ate类型的对象’通常代表_个SSL证书对象。

□1pˉaddre55:是一个1paddre55.Ip`′4∧ddIe55或jpaddIe55.Ipv6∧ddre55类型的对象,代表服务 器的IP地址。

另外【e5po∩5e还有几个常用的子类,如『extRe5po∩5e和‖t‖‖1Re5po∩5e’‖tⅦ1【e5po∩5e又是 『extRe5po∩se的子类’实际上回调方法接收的re5po∩5e参数就是_个‖t"1Re5po∩5e对象’它还有几

勺·

的是,该方法接收的uI1可以是相对URL’不必_定是绝对URL°

』■■

就是绝对URL。

□+o11ow/十o11o"a11:是_个根据URL来生成后续Reque5t的方法,和直接构造Reque5t不同

‖可』

□ur1jo1∩:是对URL的_个处理方法’可以传人当前页面的相对URL’该方法处理后返回的

个常用的方法或属性。

□text:同body属性’但结果是5tr类型° □e∩〔odj∩g: Re5po∩5e的编码,默认是ut「ˉ8°

□5e1e〔tor:根据Re5po∩5e的内容构造而成的5e1e〔toI对象’5e1e〔tor在上—节我们已经了解

□j5o∩:是Scrapy22新增的方法’利用该方法可以直接将text属性转为JSON对象°



‖」■‖(‖‖

过,利用它我们可以进一步调用Xpat∩、C55等方法进行结果的提取° □xpath:传人Xpat∩进行内容提取’等同于调用5e1ector的xpat∩方法° □〔55:传人CSS选择器进行内容提取,等同于调用5e1ectoI的〔55方法。

以上便是对Response的基本介绍,关于Response更详细的解释可以参考宫方文档: https://docs. scrapy.o【g/en/latest/topjcs/【equestˉresponse.html榆esponseˉsubc‖asses°

本节中我们介绍了5p1der的基本使用方法以及Reque5t、Re5po∩5e对象的基本数据结构,通过了 解本节内容,我们便可以灵活地完成爬取逻辑的定制了。

本节代码参见: https://githuhcom/Python3WebSpider/ScrapySpjderDemo。

|5.5

Oow∩|oade「M|dd|ewa「e的使用

《』■可|■■■■□■■∏(°■‖‖{』■■

5.总结

DownloaderMjddleware即下载中间件°在l5.1节我们已经提到过,它是处于Scrapy的Engine和

Middleware的处理,如图l5ˉ8所示°



■∏』■■‖■■■‖||■■■■

Downloader之间的处理模块°在Engjne把从Scheduler获取的Request发送给Downloader的过程中, 以及Downloader把Response发送回Engme的过程中’Request和Response都会经过Downloader

U



DownloaderMiddleware的使用

l5.5



767

↓ 怔…[

舵00[割s



图l5ˉ8

DownloaderMjddleware

也就是说’DownloaderMjddleware在整个架构中起作用的位置是以下两个°

□Engjne从Scheduler获取Request发送给Downloader’在Request被Engjne发送给Downloader 执行下载之前,DownloaderMiddleware可以对Request进行修改°

■「|卜|·厂卜|户『■『||■厅『■『|尸|「‖}|●『『‖‖‖‖■厂卜||

□Downloader执行Request后生成Response’在Response被Engine发送给Spjder之前’也就是

在Resposne被Spider解析之前,Down‖oderMiddleware可以对Response进行修改° 不要小看DownloderMiddleware,其实它在整个爬虫执行过程中能起到非常重要的作用’功能十

分强大’修改UserˉAgent、处理重定向、设置代理、失败重试、设置Cookie等功能都需要借助它来实 现。

本节我们来了解一下DownloaderMiddleware的详细用法。 ↑.使用说明

》【■『|||卜卜‖}◆卜||||■[尸|[|』广‖●「

需要说明的是, Scrapy已经提供了许多DownloaderMjddleware,比如负责失败重试`自动重定问 等功能的DownloaderMiddleware’它们被000‖‖[0∧D[R例I00l[‖∧R[58∧5[变量所定义。 00‖‖[0∧D[R‖I00[[‖∧R[58∧5[变量的内容如下所示: {

|5〔rapy.d硼∩1oadem1dd1酣己re5.robot5txt。Robot5『xt"jdd1酬are0 : 1OO’ ,5〔rapy·d叫∩1oademidd1印are5。httpal』th·‖ttp∧uth∩idd1酣are|: 3oo’

05〔r己py°dow∩1oademidd1eware5.d叫∩1o己dti贬outD咖Uo己d丁1嘘ol』t"jdd1eware0 : 35o’ ‖5cr己py。do创∩1oademidd1酣ares.de+au1theadeI5。De+3l』1t‖eader5∩idd1酬are‖; qm’ !5cmpy·d叫∩1oadermdd1eware5°u5erage∏t。05eI∧ge∩t"idd1翱are0 8 5oo’ |5〔mpy·do仍∩1oademidd1印aIes·retry·RetIy"idd1酗are0 : 550’ !5cmpy.d础∩1oademidd1酣are5.aja×〔mw1.Ajax〔mN1"jdd1eware|: S6o’ 『5〔rapy.d叫∩1oademjdd1eware5。redjrect°眶taRe千re5刚idd1酣are0 8 S8o’

5cmpy.do们∩1oademidd1印are5。∩ttpcoⅧpre55jo∩。砒tp〔咖pre55jo∩‖idd1即are|: 59o’ 05〔rapy.d叫∩1oademjdd1ew己re5。redirect·Redire〔t"jdd1由aIe0 8 60o’

0

05〔mpy·d叫∩1oademndd1ew己Ie5.〔oome5.〔oo促ie5"idd1ew己re{ : 7卯’ 5〔rapy。d山∩1oademidd1印are5.∏ttppIoxy·毗tpproxy∩jdd1ew己re! 8 75o’ 05〔mpγ.dow∩1oadermdd1eNares°stat5Do‖∩1oader5t己t50 : 850』 ‖5〔mpy.do刊∩1oademidd1副are5。http〔ache。‖ttp@〔∩e∩jdd1酣aIe! 吕 9卯’ }

这是一个字典格式’字典的键名是Scrapy内置的DownloaderMidd‖ewaIB的名称’键值代表了调 用的优先级,优先级是一个数字,数字越小代表越靠近Engjne,数字越大代表越靠近Downloader° 在默认情况下, Scrapy已经为我们开启了0O0‖‖[0∧0[R问IDD[[‖∧R[58∧5[所定义的Downloader

Midd‖eware,比如【etry日jdd1ew日re带有自动重试功能,Redirect∩idd1e"are带有自动处理重定向功能, 这些功能默认都是开启的°

那DownloaderMiddleware里面究竟是怎么实现的呢?

其实每个DownloaderMiddleware都可以通过定义proce55一reque5t和proces5resPo∩se方法来 分别处理Request和Response,被开启的DownloaderMjddleware的pro〔e55ˉreque5t方法和 proces5re5Po∩5e方法会根据优先级被顺次调用° ′

由于Request是从Engine发送给Downloader的,并且优先级数字越小的DownloaderMiddleware

越靠近Engine,所以优先级数字越小的DownloaderMiddleware的pro〔e55-req0e5t方法越先被调用。

∏5

768



第15章Scrapy框架的使用

proce55ˉre5po∩5e方法则相反,由于Response是由Downloder发送给Engine的’优先级数字越大的 DownloaderMiddlewaI℃越靠近Downloader,所以优先级数字越大的DownloaderMiddleware的

proces—re5po∩se越先被调用° 如果我们想将自定义的DownloaderMiddleware添加到项目中’不要直接修改卯‖‖[0AD[R

"I0D[[‖∧R[58∧5[变量。Scrapy提供了另外—个设置变量0O‖‖[0∧D[RNI0D[[‖∧R[5,我们直接修改这 个变量就可以添加自己定义的Down‖oaderMiddleware,以及禁用D侧‖[O∧D[R日I0D[[‖∧R[58∧5[里面 定义的DownloaderMiddleware了。

·■伊

说了这么多可能比较抽象,下面我们具体来看—看DownloaderMidd‖eware的使用方法’然后结 合案例来体会_下DownloaderMiddleware的使用方法°

2核心方法

■引

Scrapy内置的DownloaderMiddleware为Scrapy提供了基础的功能’但在项目实战中’我们往往 需要单独定义DownloaderMiddleware。不用担心,这个过程非常简单,我们只需要实现几个方法。 每个DownloaderMjddleware都定义了-个或多个方法的类’核心的方法有如下3个:

□pro〔e55ˉexceptjo∩(request’ excePtio∩’ 5pjder)

我们只需要实现至少一个方法’就可以定义一个Down‖oaderMiddleware°下面我们来看看这3个 方法的详细用法°

|{

□proce55ˉre5po∩5e(reql」e5t’ respo∩5e’ 5p1der)

‖□日

□proce55ˉreque5t(reque5t’ 5p1der)

·proces5-reque5t(reque5t’5pider)

Request被Engjne发送给Downloader之前, pro〔e55-reque5t方法就会被调用’也就是在Request 从Scheduler里被调度出来发送到Downloader下载执行之前,我们都可以用proce55ˉrequest方法对 Request进行处理。

这个方法的返回值必须为‖o∩e、Response对象、Request对象三者之一’或者抛出Ig∩oreReque5t



异常°

pro〔e55ˉreque5t方法的参数有两个° □reque5t:Request对象’即被处理的Request° □5p1der: Spdjer对象’即此Request对应的Spjder对象。





返回类型不同,产生的效果也不同。下面归纳_下不同的返回情况。

Request进行修改,最后送至Downloader执行°

□当返回为Response对象时,更低优先级的DownloaderMjddlewaIe的pro〔e5sˉreque5t和 proce55ˉex〔eptjo∩方法就不会被继续调用’每个DownloaderMidd‖eware的pro〔e55-re5po∩se 方法转而被依次调用°调用完毕后,直接将Response对象发送给Spjder处理。 □当返回为Request对象时’更低优先级的DownloaderMiddleware的proce55ˉreque5t方法会停 止执行°这个Request会重新放到调度队列里,其实它就是一个全新的Request’等待被调度° 如果被Scheduler调度了’那么所有的DownloaderMiddlewaIe的proces5ˉrequest方法会被重

」■■】||■■可■■□Ⅲ■|」』■■司』』■|』■■|』■■‖|‖‖|‖■■■

□当返回是‖o∩e时, ScIapy将继续处理该Request,接着执行其他DownloaderMjdd‖eware的 proce55-req0e5t方法’_直到Down‖oader把Request执行得到Response才结束°这个过程其 实就是修改Request的过程’不同的DownloaderMjddleware按照设置的优先级顺序依次对

新按照顺序执行。 ‖







l5.5

DownloaderMiddleware的使用

769

□如果抛出IgnoreRequest异常,则所有的DownloaderMidd‖eware的pro〔e55-ex〔eptjo∩方法会 依次执行.如果没有-个方法处理这个异常’那么Request的errorba〔代方法就会回调。如果 该异常还没有被处理,那么它便会被忽略°

●pxoces5ˉre5po∩5e(reql』e5t’re5po∩5e’ 5pider)

Downloader执行Request下载之后,会得到对应的Response。Engjne便会将Response发送给



厂》卜}尸卜



SPlder进行解析。在发送给Spider之前,我们都可以用pro〔e5s—Ie5po∩5e方法来对Response进行处 理。Proce55ˉre5Po∩5e方法的返回值必须为Request对象和Response对象两者之一,或者抛出 Ig∩oreReql』e5t异常。

proce55ˉre5po∩5e方法的参数有3个°

卜·『

□reque5t:Request对象’即此Response对应的Request。 □Ie5po∩5e:Response对象,即被处理的Response。

□5pjder: Spider对象,即此Response对应的Spider对象°

广 卜 ■ 仿 『 》 ■

下面对不同的返回情况做_下归纳。

□当返回为Request对象时’更低优先级的DownloaderMiddleware的proce55ˉre5po∩5e方法不 会继续调用°该Request对象会重新放到调度队列里等待被调度’相当于一个全新的Request。 然后,该Request会被pro〔ess_reque5t方法顺次处理°

□当返回为Response对象时’更低优先级的DownloaderMiddleware的proces5ˉre5po∩5e方法会 巴尸【广『β■巴■『△’伯■■仙{◆「■■巳■

继续被调用’对该Response对象进行处理。

□如果抛出IgnoreRequest异常’则Request的eIrorba〔R方法会回调°如果该异常还没有被处理, 那么它会被忽略°

●pmce55ˉexceptio∩(reql』e5t’exceptio∩’ 5pider)

当Downloader或proce55ˉreque5t方法抛出异常时’例如抛出Ig门oreReque5t异常’pro〔e55ˉexceptjo∩ 方法就会被调用°方法的返回值必须为‖o∩e、Response对象、Request对象三者之一° pro〔e55ˉex〔eptjo∩方法的参数有3个。





P p







} 巴



|P

□reque5t:Request对象’即产生异常的Request°

□ex〔ept1o∩: Exception对象’即抛出的异常° □5pdier: Spider对象,即Request对应的SpideT° 下面归纳一下不同的返回值°

□当返回为‖o∩e时,更低优先级的DownloaderMiddleware的proces5e×ceptjo∩会被继续顺次 调用,直到所有的方法都被调用完毕°

□当返回为Response对象时’更低优先级的DownloaderMjddleware的pro〔e55exceptjo∩方法 不再被继续调用,每个Down‖oaderMiddleware的pro〔e55ˉrespo∩5e方法转而被依次调用°

广







□当返回为Request对象时’更低优先级的DownloaderMiddleware的proce55ex〔ePtjo∩也不再 被继续调用’该Request对象会重新放到调度队列里面等待被调度’相当于—个全新的 Request°然后,该Request又会被proce55-reque5t方法顺次处理°

b

b







| ‖

以上内容便是这3个方法的详细使用逻辑°在使用它们之前,请先对这3个方法的返回值的处 理情况有一个清晰认识°在自定义DownloaderMiddleware的时候’也_定要注意每个方法的返回 类型°

厂=—‖]5

3.项目实战

卜面的内容确实有点难以理解,下面我们可以结合_个实战项目来加深对DownloaderMiddleware





第15章Scrapy框架的使用

770

的认识°

首先让我们新建—个Scrapy项目’名字叫作5〔rapydo"∩1oaderⅧ1dd1ewarede|m’命令如下所示: 5cmpy5t己rtproje〔t5〔mpydow∩1oademidd1ewarede们o

接下来进人项目,新建—个Spider’我们还是以https://wwwhttpbinoIB/为例来进行演示,命令如 下所示: 5〔mpy8e∩5pjder∩ttpbj∩…·httpbj∩.org

命令执行完毕后’就新建了_个Spider’名为∩ttpb1∩。 接下来我们修改5t日rtur1s为: [0http5://咖.‖ttpbi∩.org/get』]。随后将par5e方法添加—行

打印输出’将re5po∩5e变量的text属性输出,这样我们便可以看到Scrapy发送的Request信息了。



{ □



修改Spjder内容如下所示: j呻Ort5〔mPy

〔1己55‖ttpbj∩5pjder(5c【apy。5pjder): ∩a‖记= 0httPbj∩‖ a11o佣eddα∏m∩5≡ [‖"洲.httpbi∩.org』] 5tartur15= [ !https://哪.httpbj∩.org/get‖]

de「p3I5e(se1「’ re5po∩5e): prj∩t(re5po∩5e.text)

接下来运行此Spider,执行如下命令: 5crapy〔Iaw1httpbj∩

Scmpy的运行结果包含Scrapy发送的Request信息,内容如下所示:

叫■





| q

| □

{ 而

aIgS闻; {}’ ·header5圆8 {

国∧c〔ept■: 口te×t/∩t∩1’app1j〔atjo∩/xht‖1+x‖1’app1i〔己tjo∩/x们1jq=o.9’*/木jq=o.8■’ "∧c〔eptˉ[∩〔odi∩g阐: 厕gzjp’ de十1ate"’ "∧〔〔eptˉl己∩guage闻: 日e∩闰’ 曰‖o5t": w瞅.‖ttpbj∩°oIg"』 ”05erˉ∧ge∩t闻: .5c】apy/2·2。1 (+httP5://5〔mpy。org)圃’ "xˉ蛔z∩ˉ丁Ia〔eˉId"8 "Root=1ˉ5十4bd897ˉ53q3〔5do8o302b8o69+696oo" }’ 口orjgj∩冈: 口219。142。145·226"』 "ur1": "∩ttps://咐.∩ttpbj∩。org/get口 }



」 g

q



q





( d

| d

我们观察_下∩eader5, Scmpy发送的Request使用的(」5erˉ∧ge∩t是5〔mpy/2.2.1 (+∩ttp5:// 5αaPy.org),这其实是由Scrapy内置的l」5er∧ge∩t例jdd1eware设置的, 05er∧ge∩t"jdd1eware的源码如 下所示;

q





「ro们5〔mpyj呻ort5ig∩a15

〔1a5505er∧ge∩t月jdd1ew己re(obje〔t): de「 i∩1t_(Se1+’ u5eI≡a8e∩t≡05crapy‖ ): 5e1千辙u5er=a8e∩t=05er-己ge∩t





伙1a55爬t‖Od

de于十ro∩↑〔mw1er(〔15’ craw1er)8 O=〔15(〔mw1er。5etti∩g5[!05[R∧6[‖丁‖]) Ⅲetur∩O

■■√‖|』···]』

〔m"1er。5ig∩a15.〔o∩∩e〔t(o.5Pjder_ope∩ed’ 5jg∏日1=5jg∏a15.5pjder-ope∩ed)



l55DownloaderMiddleware的使用

77l



■■尸↑炉‖巴■厂卜℃β‖‖■〗△尸‖■尸■=尸

def5pjderˉope∩ed(5e1十’ 5pider): 5e1十。userˉage∩t=get日ttr(5Pjder’ ,u5er-a8e∩t|’ 5e1+.userˉa8e∩t) de「proces5-reql』est(5e1「’ request’ 5pider): j十5e1+°u5er-age∩t: req0e5t.header5.Setde+au1t(b005erˉ∧ge∩t|′ Se1十。u5er≡age∩t)

在+ro们〔mw1er方法中’ 05er∧ge∏t‖idd1eware首先尝试获取5ettj∩g5里面的05[R∧C[‖「,然后 把05[R∧C[‖「传递给i∩1t方法进行初始化’其参数就是‖5erˉage∩t°如果没有传递05[R∧C[‖丁参 数’就会默认将其设置为Scrapy字符串°我们新建的项目没有设置05[R∧C[‖丁,所以这里的u5er-age∩t 变量就是Scrapy°

接下来’在proce55ˉreque5t方法中’将u5erˉage∩t变量设置为header5变量的一个属性’这样就 成功设置了05erˉ∧ge∩t°因此,UsePAgent就是通过此DownloaderMiddlewaIe的proce55ˉreque5t方法 设置的,这就是_个典型的DownloderMiddlewaJE的实例,我们再看一下刚‖[0∧D[R‖I卯[[‖AR[5B∧5[ 卜叼『匹●『

的配置, 0Ser∧ge∩t‖1dd1eWare的配置如下: {

‖5〔mpy。do"∩1oademidd1e"are5。l』5em8e∩t。l」5erAge∩t"1dd1酣are‖: 5OO }

S

广△■『=广

‖》 ■[■厂卜》|‖|匡尸‖■尸‖●『■尸「



可以看到, 05er∧ge∩t‖idd1ew日re被配置在了默认的0O0‖‖[0∧0[RⅦDD[[‖∧R[SBA5[里,优先级为 50O,这样每次Request在被Downloader执行前都会被05er∧ge∩t‖idd1e"are的proce5sˉrequest方法 加上默认的UserˉAgent°

但如果这个默认的UserˉAgent直接去请求目标网站,很容易被检测出来,我们需要将UsePAgent 修改为常见测览器的UseFAgent°修改UserˉAgent可以有两种方式: □-是修改5ettj∩g5里面的05[R∧C[‖丁变量° □二是通过Down‖oaderMiddleware的pro〔e55ˉreque5t方法来修改°

第-种方法非常简单’我们只需要在se仗ingpy里面加_行对05[【∧C[‖「的定义即可: 05[R∧C[‖『= !№zi11a/5.0(巾〔j∩to5∩〗 I∩te1∩a〔05X1O126)∧pp1e"ebⅨjt/537。36(刚丁∩[’1汕e0ec代o) 〔hrα∏e/59.O·3o71.11S5己千ari/537°36‖

一般推荐使用此方法来进行设置°但是如果想设置得更灵活,比如设置随机的UsePAgent,那就 需要借助DownloaderMiddleware了。所以接下来我们用DownloaderMiddleware实现一个随机

}}

「卜‖β}

UserˉAgent的设置°

在middlewarespy里面添加一个Ra∩do"05er∧ge∩t"idd1eware类’代码如下所示: mportm∩d咖

〔1a5sRa∩do∏N』5er∧ge∩t∩idd1eNare(object); de+一j∩it一(5e1千): 5e1十.u5erˉage∩t5= [

′№zj11a/3.0(M∩do们5j ‖; ∩5I[9。0; ‖j∩do仍5‖丁9.0; e∩-05)! ’

「 尸 ’ 「 ■ 巴 ■ ‖ 「 卜 ■ ∏ | 伍 尸 「





!№zi11a/5.0(刊i∩dow5‖『6.1)∧pp1e‖ebⅨit/537.2 (刚『肌’ 1让e6ec促o)〔hro‖记/22.O.1216翻0 5a怕ri/537.20 ’

!‖ozi11a/5.o(Ⅻ1; 0bu∩tu; li∩uxi686j rv:15.o)Ce〔|(o/2010o1O1「ire千ox/15.0.1‖ 厂↑5

de千pro〔e5s-reql』e5t(5e1十」 Ieque5t’ 5Pjder):

reque5t.headers[』05erˉ∧ge∩t!] ≡ra∩do‖.〔hoi〔e(5e1+.u5er-age∩t5)

我们首先在类的 1"jt方法中定义了3个不同的UsePAgent’并用-个列表来表示。接下来实

现了pro〔e55ˉreque5t方法,它有_个参数reque5t,我们直接修改request的属性即可。在这里直接 设置了req‖e5t对象的∩eader5属性的05erˉ∧ge∩t,设置内容是随机选择的UserˉAgent’这样一个 DownloaderMlddleware就写好了°

k

第l5章Scrapy框架的使用

772

不过’要使之牛效还需要去调用这个DownloaderMjddleware。在settingspy中’将0O‖‖[0∧D[R ‖I0D[[‖∧R[5取消注释,并设置成如下内容: 刚‖[O∧D[R∩I凹L[灿R[5={

5〔I己pyd叫∩1oader‖jdd1ewaIede晒.∏idd1ewaIe5°陶∩do∏U』ser∧8e∩t"idd1ewaIe0 : 543’ }

接下来我们重新运行Spider,就可以看到05erˉ∧ge∩t被成功修改为列表中所定义的随机的-个 05erˉ∧ge∩t了: { 00

arg50』: {}》 !0∩e日deI5": { 00∧ccept": 闪text/bt川1’app1i〔atjo∩/xht‖1+x∏1’app1i〔日tio∩/xⅧ1jq=O°9’*/*jq=O。8"》 "∧cceptˉ[∏〔odj∩g": ||g∑ip』 de于1己te训』 "∧C〔eptˉ[己∩guage冈8 00e∩"’ 00‖o5t": 口哪°httpbj∩.org"D "05erˉ∧ge∩t": "№zi11a/5.o(M∩dow5‖丁6.1)∧pp1e"ebRit/537.2 (刚丁‖[’ 1北eCe〔促o)〔∩ro『‖e/22.O.1216.O 5己千3ri/537·200’ "Xˉ帅z∩ˉ『m〔eˉId闻: 00Root=1ˉ5+↓bdb4dˉ287+2430aa14d37a6ab50仟6"

}’ 00orjg1∩": 00219·142.145·2】6"’ "uI1": "http5吕//卿.httpbi∩·or8/geto0 、





我们通过实现DownloaderMjddlewaIe并利用proce55ˉreq0e5t方法,成功设置了随机的UseI烂Agent° 另外我们还可以借助DownloaderMiddleware来设置代理°比如这里我有一个HTTP代理运行在 2O3.184·132.103:7890’如果我想使用此代理请求目标站点’可以通过定义一个DownloaderMjddleware 来设置: c1a55pro×y"jdd1ew己re(obje〔t): de+proce5sˉreque5t(5e1「’ Ieque5t’ 5p1der): reque5t。∏eta[ 0pIoxy|] = 0http;//203.184.132.103:7890!

日‖ 』■∏‖』■|

这里我们定义了_个pro×y‖jdd1e"are,在它的proce55ˉreque5t方法里面’修改了reque5t的"eta 属性的proxγ属性’赋值为∩ttp://203.184.1〕2.1o]:789o,这样就相当于设置了_个HTTP代理° 注意此代理并不一定是长期可用代理’你需妥将其吏换成你自己的可用HTTP代理’有关代理的 具体获取方案可以参考本书前丈代理的使用相关章节。

要使Proxy‖jdd1ewaIe生效’我们需要进_步启用这个proxy‖jdd1eware’修改D州[0∧D[R‖Im[〔‖‖∧R[5 为如下内容: 0佣肌0AD[R日IOD[["AR[5={

scmpydo切∩1oademidd1ewarede晒°mdd1eware5。Ra∩do刷BeⅢ∧ge∩t∩idd1eware0 : 543’ 5cmpydow∩1oader∏jdd1出己rede‖℃。|mdd1ewaIe5·proxy例idd1eware‖: 544



这样我们就启用了两个自定义的DownloaderMiddleware’执行优先级分别为5』3和544°

Ra∩do们05er∧ge∩t‖1dd1eware的pIo〔e55ˉreque5t方法会首先被调用,为Request赋值05erˉAge∩t’随

后Proxy‖1dd1e"are的proce55-reque5t会被调用,为Request赋值Ⅷeta的Proxy属性。 { ■

arg5": {}’ "header5": {

"∧〔〔ept": !|text/htⅧ1’app1i〔atjo∩/x‖t们1+x∏1’app11〔己tjo∩/x"1jq=O°9’*/*】q=O·8"』 "A〔〔eptˉ[∩cod1∩g": "8∑jp’ de于1日te"’ "∧〔〔eptˉ[a∩g0age曰; "e∩"’ "‖o5t00 : "州.∩ttpbj∩°org"’

|■■】||』■■]‖|」■』■■】‖‖||』■■∏|

重新运行,可以发现输出结果如下:

』·`纽■(」划|‖‖‖■】厂』□∏‖■可



0

(‖

□』

|■厂|||‖八■∏‖【■『止∩



l5°5

DownloaderMiddleware的使用

773

"05eIˉ∧ge∩t闻; "№zi11a/5.o(M∩do阅5; 0;"5I[9.oj ‖1∩dow5‖丁9.0; e∩ˉ05)"’ "Xˉ蛔z∩ˉ『raceˉId口8 "∩oot≡1ˉ5十4bdcb5ˉ〔〔bobdooa31〔〔48o6己b9178o" }’ "ori8j∩"8 "203。184·132。1O3"’

‖0ur1冈吕 圃http5://硼.∩ttpbi∩.org/get闪 p

卜|〖「|卜

■「■|≥厂|尸■『‖∏■尸「

卜卜》‖





这里我们看到网站返回结果的orig1∩字段就是代理的IP,所以可以验证出:UserˉAgent和代理 的设置都生效了。至于代理为什么生效’是因为Scrapy对|∏eta的proxy属性做了针对性处理,使得 最终发送的HTTP请求启用我们配置的代理服务器’具体的处理逻辑可以查看Scrapy的Downloader Middleware和Downloader的源代码。

我们使用Proce55ˉreque5t对Request进行了修改’但刚才写的两个DownloaderMiddleware的 pro〔e55ˉreque5t都没有返回值’即返回值为‖o∩e’这样_个个DownloaderMjddlewaJ℃的proce55ˉreque5t 就会被顺次执行°

上文我们还提到了proce55ˉreque5t,如果返回其他形式的内容会怎样?比如pro〔e55ˉreque5t直 接返回Request°我们修改pIoxy∩jdd1eware试—下:



尸》仍

c1a55proxy"idd1ew己re(object): de+pIo〔e55ˉreq仙e5t(5e1十’ Ieque5t’ 5pider); request。眶ta[′proxy′] ≡ |http://127.O.O。1:789O0 ret‖r∩reque5t

》|

这里我们在方法的最后加上了返回Request的逻辑,根据前文介绍的内容’如果pro〔e55-reque5t

司 宦 ● 尸 ■ 厂 匹 ■ ■ 当

返回的是_个Request’那么后续其他DownloaderMiddleware的proce55ˉreque5t就不会被调用,这

个Request会直接发送给Engine并加回到Scheduler’等待下_次被调度°由于现在我们只发起了一个 Request’所以下—个被调度的Request还是这个Request,然后会再次经过proce55-reque5t方法处理,

伊『『尸

接着再次被返回’又一次被加回到Scheduler,这样这个Request就不断从Scheduler取出来放回去’ 导致无限循环°

因此,这时候运行会得到一个递归错误的报错信息: ■『■■■尸卜『巴尸■■尸|■β止∩

∩e〔0rsjo∩[Iror:帕xj刚∏re〔urBjo∩dept∩exceeded"M1eca11j∩gapytho∩obje〔t

所以说’这一句简单的返回逻辑就整个改变了Scrapy爬虫的执行逻辑’-定要注意。

另外,如果我们返回一个Response会怎么办呢?根据前文所述,更低优先级的Downloader

Mjddleware的proce55ˉreque5t和pro〔e55-exceptjo‖方法就不会被继续调用’每个Downloader

β卜)β尸卜

Middleware的pro〔es5ˉre5po∩5e方法转而被依次调用。调用完毕后,直接将Response对象发送给 Spider来处理°所以说,如果返回的是Response,会直接被proce55=re5po∩5e处理完毕后发送给Spjder, 而该Request就不会再经由Downloader执行下载了° 我们再尝试改写_下proxγ付1dd1eware’修改如下: 千r咖5〔mpy.们ttpmport‖t们1Re5po∩5e

||

卜 } 卜

■「|△■氏[『

〔1as5proxy"idd1eware(obje〔t):

de十processˉreque5t(5e1+’ reque5t’ 5p1der): retur∩‖t∏1伺e5po∩5e( 皿1=req仙e5t·0r1’ Statu5=2OO’

e∩codi∩g=『ut「ˉ80 ’

body=‖丁e5t0o仍∩1oader"1dd1eware!)

这里我们盲接把代理设置的逻辑去掉了,返回了一个‖tⅦ1Re5po∩5e对象,构造‖t"1Re5po∩5e对 象时传人了ur1` statu5`e∩codj∩g、 body参数,其中直接赋给bodγ一个字符串°

■ ■

||

第l5章Scrapy框架的使用

774



重新运行_下,看看输出结果: 丁e5t助切∩1oader问jdd1酗己Ie



|‖

这就是par5e方法的输出结果,可以看到原本Request应该去请求htms://wwwhttpbjn.oIg/get得到 返回结果,但是这里Response的内容直接变成了刚才我们所定义的‖t∏1Re5Po∩5e的内容,丢弃了原 本的Request°因此,如果我们在proce55-reque5t方法中直接返回Response对象’原先的Request就 会被直接丢弃’该Response经过proce55—re5po∏5e方法处理后会直接传递给Spider解析° 到现在为止,我们应该能够明白pro〔e5s-reque5t的用法及其不同的返回值所起到的作用了°



Downloader对Request执行下载之后会得到Response’随后Engine会将Response发送回Spider进





上面我们讲了pIoce55-reque5t的用法,它是用来处理Request的’相应地’proce55-re5po∩5e就 是用来处理Response的了’我们再来看—下pro〔e55ˉIe5po∩5e的用法°

行处理°但是在Response被发送给Spider之前’我们同样可以使用pro〔e55ˉre5po∩5e方法对Response 进行处理°

q

〔1a55〔h日∩ge同e5po∩5酬jdd1酗are(obje〔t〉: de+pro〔es5-re5po∩5e(se1+’ reque5t’ re5Po∩5e’ 5pideI): respo∩5e.statu5=2o1 retur∩re5po"5e

‘尸■【■■■■厂■【■■卜·■

比如这里修改-下Response的状态码,添加一个〔ba∩geRe5po∩5e‖jdd1eware的Downloader Middleware,代码如下:

我们将re5po∩5e对象的5tatu5属性修改为2O1,随后将re5po∩5e返回,这个被修改的Response就 会被发送到Spider°

我们再在Spjder里面输出修改后的状态码,在par5e方法中添加如下的输出语句: pri∩t(05t己tu5〔ode: ‖ ’ re5po∩5e.st3tu5)

然后将00‖‖[0∧D[R‖I00[[‖∧R[5修改为如下内容: 刚‖[弘D[R"I卯[[肌R[5≡{



‖5crapydo创∩1o己deImdd1eN己rede‖℃.mdd1ewaIes·Ra∩do∏U5er∧ge∩t"jdd1e"are‖; 543’ 05〔Iapydo佣∩1oademidd1ewarede晒.∏idd1刨aIe5。〔ha∩geRe5po∩5e∩idd1eⅣare! : 5』4’

接着将proxy‖idd1e"are换成了〔∩a∩geRe5po∩5e‖idd1e"日re’重新运行’控制台输出了如下内容: 5tatuS〔ode; 201

可以发现’Response的状态码被成功修改了°因此如果要想对Response进行处理’就可以借助 pro〔e55ˉre5po∏5e方法。

当然pro〔e55ˉre5po∩5e方法的不同返回值有不同的作用’如果返回Request对象,更低优先级的 DownloaderMiddlewa』℃的proce55ˉreque5t方法会停止执行°这个Request会重新放到调度队列里, 其实它就是—个全新的Request’等待被调度°感兴趣的话可以尝试-下° 「‖▲

另外还有—个pro〔e55ˉex〔eptjo∩方法,它是专门用来处理异常的方法。如果需要进行异常处理, 我们可以调用此方法°不过这个方法的使用频率相对低一些’不在进行实例演示。 4总结

本节讲解了Dow∏loaderMiddleware的基本用法°此组件非常重要’后面我们进行代理设置、反



本节代码参见: https://githuhcom/Python3WebSpider/ScrapyDownloaderMiddlewareDemo。

■·可』□旧‖』□■■

爬处理、动态喧染处理都需要用到DownloaderMiddleware。



l5.6

SpiderMjddleware的使用

775

Sp|de「M|dd|ewa「e的使用

↑5.6

SpiderMlddleware,中文可以翻译为爬虫中间件,但我个人认为英文的叫法更为合适°它是处于

SPjder和Engine之间的处理模块°当Downloader生成Response之后’Response会被发送给Spider, 在发送给SPider之前’Response会首先经过SpiderMiddleware的处理,当Spider处理生成Item和 Request之后’Item和Request还会经过SpiderMiddleware的处理。 SpiderMiddleware有如下3个作用°

□Downloader生成Response之后’Engjne会将其发送给Spider进行解析,在Response发送给 Spider之前,可以借助SpjderMjddleware对Response进行处理° □Spider生成Request之后会被发送至Engine’然后Request会被转发到Scheduler,在Request被 发送给Engine之前,可以借助SpiderMjddleware对Request进行处理° □Spider生成【tem之后会被发送至Engjne’然后Item会被转发到ItemPipeljne’在Item被发送 ‖|〔厂[尸|》「●‖任‖|匹■【尸‖|●[【■「》||■尸卜〖『|卜■厂卜|■}[「|止尸侄|忙‖||卜「|■[尸卜■尸巴尸|凹|卜|●「『卜「||■【■『|‖‖■■)·◆它尸匹■■厂||[『‖}′■■「■||■『■■【■「■『‖

给Engine之前,可以借助SpiderMiddleware对Item进行处理°

总的来说’SpiderMiddleware可以用来处理输人给Spider的Response和Spjder输出的Item以及 Request°

↑.使用说明

同样需要说明的是’Scrapy其实已经提供了许多SpjderMiddleware’与DownloaderMiddleware类 似’它们被5pI0[尺‖I00[[‖∧R[58∧5[变量所定义° 5PID[R‖I00[[‖∧R[58∧5[变量的内容如下: {

5〔rapy。5pidem1dd1ewaIes.httperror。‖ttp[rror例1dd1eware‖: 5o’ 5〔mpy.spjdem1dd1eware臼·o仟5ite·听+5jt酬idd1eware|日 5oo』 5〔rapy。5pidermdd1ew日Ⅲe5°re+erer.Re于erer付idd1出are0 : 70o’ 5crapy。spidermdd1ewaIe5。ur11e∩gt‖。0r1le∩8t∩料idd1eware! : 8O0’ 5cmpy°5pidermdd1e闪己Ies.dept‖·Dept∩‖idd1eware0 : 9oo’ }

5pI0[R‖I00[[‖∧R[58∧5[里定义的SpiderMiddleware是默认生效的,如果我们要自定义Spider Mjddleware,可以和DownloaderMjdd‖ewa】℃-样,创建SpiderMiddlewa』E并将其加人5p10[R‖I卯1[趴【[S° 直接修改这个变量就可以添加自己定义的SpjderMiddleware’以及禁用5pID[R‖I00[[‖∧R[58A5[里面 定义的SpjderMiddleware。 这些SpiderMiddleware的调用优先级和DownloaderMjdd‖eware也是类似的’数字越小的Spider Middleware是越靠近Engine的’数字越大的SpjderMiddleware是越靠近Spjder的° 2核心方法

Scrapy内置的SpjderMidd‖eware为Scrapy提供了基础的功能。如果我们想要扩展其功能’只需 要实现某几个方法。

每个SpjderMiddleware都定义了以下-个或多个方法的类’核心方法有如下4个° □proce55ˉ5pjder—j∩put(re5po∩5e’ 5pider) □proce55_5p1derˉoutput(re5po∩5e’ re5u1t’ 5pjder) □pro〔e555pjderˉex〔eptjo∩(re5po∩5e’ ex〔eptjo∩′ 5pjder) □pro〔es5-5tartˉIeque5t5(5tartˉreque5t5’ 5p1der)

第l5章Scrapy框架的使用

只需要实现其中_个方法就可以定义一个SpiderMiddleware°下面我们来看看这4个方法的详细

‖〗』■】‖司·‖】□□】』∏‖|』√||



776

用法°

●proce55ˉ5p1der一1∏put(re5po∩se’ 5p1der)

当Response通过SpiderMiddleware时’proce5s-5pjder-j∩put方法被调用’处理该Response。 它有两个参数。

□re5po∩5e:Response对象,即被处理的Response°

□5p1der: Spider对象,即该Response对应的Spider对象° pro〔e5s-5pjder-i∩put应该返回‖o∩e或者抛出一个异常° □如果它返回‖o∩e ’ Scrapy会继续处理该Responsc,调用所有其他的SpiderMiddlewa赡直到 Spider处理该Response。



□如果它抛出一个异常’Scrapy不会调用任何其他SpiderMiddleware的proce55ˉ5p1derˉ1∩put方 法’并调用Request的errbac|〈方法°errbac低的输出将会以另_个方向被重新输人中间件, 使用proce55ˉ5p1deIˉo0tput方法来处理’当其抛出异常时则调用pro〔e55-5piderˉexcept1o∩ ●proces5=5p1deⅢˉOutput(respo∏5e’ re5u1t’ 5p1der)

当Spider处理Response返回结果时’proce55-sp1der-output方法被调用°它有3个参数° □re5po∩5e:Response对象,即生成该输出的Response° □resl」1t:包含Request或Item对象的可迭代对象’即Spider返回的结果° □5pjder: Spjder对象,即结果对应的Spjder对象。

pro〔e55ˉ5pjderˉoutput必须返回包含Request或Item对象的可迭代对象° ●proce5三ˉsp1der=exceptio∩(re5po∩5e’ exceptio∩’ 5p1deⅢ)

当Spjder或SpiderMjddlewa』℃的proce55ˉ5pjderˉj∩put方法抛出异常时, pro〔e55ˉ5pjder-ex〔eptio∩ 方法被调用°它有3个参数。

proce55ˉ5pjderˉexceptio∩必须返回‖o∩e或者_个(包含Response或Item对象的)可迭代对象° □如果它返回‖o∩e ’那么Scrapy将继续处理该异常,调用其他SpiderMiddleware中的 proces5ˉ5pjderˉexceptjo∩方法’直到所有SpiderMiddleware都被调用° □如果它返回_个可迭代对象’则其他SpiderMiddleware的proce55=5pjderˉol』tput方法被调用, 其他的proce55=5pider一e×ceptjo∩不会被调用° ●proce55=5tmtˉmquests(start=request5’5p1der)

pro〔e55-5tartˉrequest5方法以Spjder启动的Request为参数被调用,执行的过程类似于 proce55ˉ5piderˉol」tput,只不过它没有相关联的Response并且必须返回Request。它有两个参数° □startˉreque5t5:包含Request的可迭代对象’即StartRequests° □5pjder: Spjder对象’即StartRequests所属的Spjder。

』■』■|‖■‖‖』■〗‖』■‖』‖』√‖]』■〗□‖』』■』·】□月|{』■■』·‖‖‖□Ⅱ‖‖■

□re5po∩5e:Response对象,即异常被抛出时被处理的Response° □exceptio∩: Exception对象,被抛出的异常° □5p1der: Spider对象,即抛出该异常的Spider对象°

‖』|||·‖』‖日■』』』·]|」‖勺]‖■■‖`‖司』引」‖】‖·‖(|』】‖

来处理°

proce55ˉ5tartˉreque5t5方法必须返回另-个包含Request对象的可迭代对象°

勺」■■|‖』■可■■■■

l56

SpjderMiddleware的使用

777

3.实战

上面的内容理解起来还是有点抽象’下面我们结合_个实战项目来加深_下对SpiderMiddleware 的认识。

首先我们新建一个Scrapy项目叫作5〔mpy5pjdermdd1e"aredeⅦo,命令如下所示: ~卜『‖『』匹尸‖【「}卜|卜|[尸『『||户β仿‖[■‖「|『》‖「







5〔Iapy5tartproje〔t5cmpy5pidermdd1酗3rede『∏o

然后进人项目’新建-个Spjder。我们还是以ht‖ps:〃wwwhttpbino【g/为例来进行演示’命令如下 所示:

5〔rapyge∩5p1derhttpbi∩哪.httpb1∩.org

命令执行完毕后,新建了_个名为∩ttpb1∩的Spider°接下来我们修改5tartur1为∩ttp5:// "`√w.∩ttpb1∩.org/get’然后自定义5tart=request5方法’构造几个Request’回调方法还是定义为paI5e 方法.随后将par5e方法添加_行打印输出,将re5Po∩5e变量的text属性输出’这样我们便可以看 到Scrapy发送的Request信息了。

修改Spider内容如下所示: +roⅦ5crapyj川port5pider’ Reque5t 〔1a55‖ttpbi∩5pider(5pjder): ∩3爬= 0httpbj∏0 a11o"eddo|∏a1∩5= [|….httpbj∩.org{] 5t日rtur1= |http5://州。∩ttpbi∩.org/get|

de+5tart-reque5t5(5e1十): 千orii∩m∩ge(5): Ur1≡「‖{5e1千。5tartUr1}?ql」eIy={i}0 yie1d【eque5t(ur1’〔a11ba〔Ⅸ二5e1+.par5e)



|0

|·

de千par5e(5e1十’ re5po∩5e): prj∩t(re5pO∩S巳text)

尸 b

「 b

卜 b

接下来运行此Spider,执行如下命令: 5crapγ〔raw1httpbj∩

Scrapy运行结果包含Scrapy发送的Request信息’内容如下所示: {

"arg5"; { query00 : "o" 00

广■■「「尸卜{尸‖|尸[尸‖|快匹尸||口口‖‖【△尸仿「

}’ "‖e己der5呵: {

"∧c〔ept||: "text/‖tⅦ1’app11〔atjo∩/x∩t们1+x刑1’app1jcatjo∩/x们1;q=O。9’*/*jq=O·8"’ "∧c〔eptˉ[∩〔odj∩g": "gzip’ de+1日te"》 !!ACCeptˉ[a∩gu己ge"; "e∩"p

‖:蕴::懈!们嘿鹏!1(杂∩ttps;′′5…org〉』』』 αXˉ蛔z∩ˉ『m〔eˉId": "Root=1ˉS十↓b十132ˉ365da06d〔b〔13e〕a己+日1d2日3" }’

"origj∩": "219。M2。1』5。226"’ "0r1"8 {!∩ttp58//洲w。∩ttpbi∩。org/get?q‖ery=1" } 夕

|■广‖‖‖▲■■尸β■=■「■■||卜■■、弓■■■■■■■■二■■■■■■■■】‖‖』

厂〔

05

{ 00

日rg5!! : { queIy"8 厕4" 00

}’ 0』headerS": {

!『∧〔〔ept0|: "tex↑/ht‖1’app1i〔日tio∩/xbtⅦ1+xⅢ1’app1i〔atio∩/x川1jq=O·9’*/*jq=O。8"』 |‖∧c〔eptˉ[∩〔od1∩8": "gzjp’ de+1ate"』 00

"A〔〔eptˉLa∩gua8e :

00



e∩ ’



■■■‖」|||■■■可■■■■■』可

第l5章Scrapy框架的使用

778

"什ost"8 "哪·httpbi∩°org"』

"05erˉ∧ge∩t曰: .5cmpy/2.2.1(+http5://5cmpy.org)"’ "ori8j∩■: 口219.1q2.14S·226"’ "ur1"8 "‖ttp5://哪°httpbi∩·org/get?queIy=4" }

‖」可|□

"Xˉ蛔Ⅲ∩ˉ丫I己〔eˉId"8 "∩oot=1ˉ5千4b千132ˉ36十95千671162688己dea〔q6b20‖

}’

试里我们可以看到几个Request对应的Response的内容就被输出了’每个返回结果带有arg5参 数, query为忻4。

另外我们可以定义_个Item, 4个字段就是目标站点返回的字段,相关代码如下: i呻oIt5〔Iapy

〔1as50e帅IteⅦ(5〔rapy。Ite加): origj∩=5〔r己py.「ie1d() header5≡5〔mpy.「je1d(〉 arg5=5Cr己py.「ie1d() ur1=5cmpy。「ie1d()

■■

de「paIse(5e1十’ re5po∩5e): jteⅦ≡0e加IteⅦ(**re5po∩se.j5o∩())

凸·‖

可以在par5e方法中将返回的Response的内容转化为0e们oIte‖,将par5e方法做如下修改:

0

yie1dite∩

这样重新运行’最终Spjder就会产生对应的0e‖oIte∏了,运行效果如下:

‖|

2020ˉo8ˉ3102:38:14 [5crapy.〔ore.5cmper] 0[8[」C: 5〔raped+r刚〈2o0∩ttp5://哪.httpbj∩.org/get?q仙ery≡』〉 {0arg5|8 {0queIy|; ′4|}’

0‖eader5! : {!∧〔〔ept0 : ‖text/们tⅦ1’app1j〔己tio∩/xht们1+x们1’app1i〔atjo∩/x川1jq≡O.9’*/*jq=0。8|’ ‖Acceptˉ[∩〔odi∩g0 : !gzip’ de千1ate|』 0A〔〔eptˉ[a∩guage|: !e∩|’ !‖ost0 :W枷°httpbi∏.org ’

|05erˉ∧ge∩t! : !5〔rapy/2。2.1 (+http5;//5cI己py.org)0 ’

!Xˉ枷z∩ˉ『race≡Id|: !【oot≡1ˉ5+db「215ˉc48272「87R〕8b41q53d5M82‖}’

0origj∩! : !219.1q2。145。226|’ |0I10 : |http5://棚.‖ttpbi∩。org/get?query≡4‖}

接下来我们实现一个SpiderMiddleware’看看如何实现Response` Item、Request的处理吧!

■可口□■

可以看到原本Response的JSON数据就被转化为了0eⅦoIte"并返回° 在mjddlewarespy中重新声明_个〔u5to们1ze‖jdd1eware类,内容如下: 〔1己5s〔u5tomze‖jdd1e"are(obje〔t): +orreql」e5t i∩5tart=reque5t臼: ur1=IeqUe5t°Ur1 ur1+= !8∩a爬=ger爬y0

■■‖|刁

de+PrO〔ess5tart-reque5t5(5e1+’ 5tart-Ieql」eStS’ 5PideI):

reque5t=reque5t.rep1己ce(0r1≡ur1) yie1dreque5t

等于gemey’然后我们利用reque5t的Iep1a〔e方法将ur1属性替换,这样就成功为Request赋值了 接着我们需要将此〔u5tomze‖1dd1ewaIe开启’在settjngspy中进行如下的定义: 5pID[【∩I即L[灿R[5={

‖5〔r己pyspjder∏1dd1ewaIedeⅧ川1dd1ewaIe5·〔u5to‖ize‖jdd1eware|: 543’ }

这样我们就开启了O」5to‖]jze‖1dd1e"are这个Spide『Midd‖ewaI℃°

』‖‖|‖·′勺』

新的URL。

‖||』

这里实现了Pro〔es55tartˉreq0e5t5方法’它可以对5tartˉreque5t5表示的每个Request进行处 理,我们首先获取了每个Request的URL,然后在URL的后凹又研接上J另外_个Query参叙, 我们首先获取了每个Rcquest的URL’然后在URL的后面又拼接上了另外_个Query参数’∩a阳e

l5.6

SpiderMjddleware的使用

779

重新运行Spider,这时候我们可以看到输出结果就变成了类似下面这样的结果: 2o2Oˉo8ˉ31o2:43:29 [5〔r日py.〔oIe.5cmpeI] 0[Bl」C: 5〔raped+IoⅦ〈2oo ∩ttp5;//哪·httpbi∩.org/get?q0ery=28∩a爬=gemeγ〉

{!arg5! ; {‖∩日爬,: !8emey‖ ’ 0query": !2!}’

‖beader5, : {|∧〔〔ept, : 0te×t/∩t∏1’己pp1i〔atjo∩/x∩tⅦ1+x刚1’app1i〔atio∩/x∏1】q≡0.9’*/*jq≡0。80 ’ |■∩}广|『『|【卜■『『|■「}「}|止■『‖|||卜|伯|‖‖【≥「□□■■=仿)‖『■尸·■| 止■『■『‖凸‖’‖■『’‖『|〖■厂’|||世■『■『‖■『■【厂匹■‖|巴■『|卜|▲尸『【止■『



`∧〔〔eptˉ[∩〔odi∩g0 ; 0g2ip’ de十1ate0 ’ ,∧〔〔eptˉta∩gua8e0 : 0e∩0 ’ 0‖o5t0 : ‖哪.httpbj∩.org0 ’

‖05erˉ∧ge∩t! : {5crapy/2.2。1(+∩ttp5://5〔rapy.org)|’

0Xˉ蛔z∩ˉ丁r己〔eˉId′: 0Root=1ˉ5十』b十35oˉ千be66+7o6「2b8〔a8o5db+388‖}′

‖or1gj∩0 目 02O3。184°132.1030 ’

‖ur1‖ : !http5://….httpbj∩.org/get?query=28∩己爬=ger眶y!}

可以观察到ur1属性成功添加了∩a们e=gen‖‖ey的内容,这说明我们利用SpiderMiddleware成功改 写了Request。

除了改写5tart—reque5t5’我们还可以对Response和Item进行改写’比如对Response进行改写, 我们可以尝试更改其状态码,在〔u5to『nze‖jdd1ewaIe里面增加如下定义: de千pro〔e55ˉspideLi∩put(5e1+′ re5po∩5e’ spideI): re5po∩5e.5tatu5=201

de十pro〔e55ˉ$piderˉoutpl」t(5e1十’ re5po∩5e’ resu1t’ 5pider): 千Ori1∩re5U1t:

i千i5j∩5ta∩〔e(i’ De№Ite∏]): i[ !origj∩,] ≡川o∩e y1e1di

这里我们定义了pro〔e5sˉ5piderˉi∩put和pm〔e55—5pjder-output方法’分别来处理Spjder的输 人和输出°对于proce55ˉ5piderˉj∩put方法来说’输人自然就是Response对象,所以第一个参数就是 respo∩5e,我们在这里直接修改了状态码。对于proce55ˉ5pjderˉoutput方法来说,输出就是Request或 Item了,但是这里二者是混合在—起的,作为re5u1t参数传递过来°re5u1t是一个可迭代的对象,

我们遍历了re5u1t’然后判断了每个元素的类型’在这里使用j5i∩5ta∩〔e方法进行判定:如果i是

0e‖oIte‖类型’就把它的orig1∩属性设置为空。当然这里还可以针对Request类型做类似的处理,此 处略去。

另外在par5e方法里面添加Response状态码的输出结果: prj∩t(』5tatus: ’ re5po∩5e.5tatus)

重新运行一下Spjder’可以看到输出结果类似下面这样: 5tatu5: 2O1

202Oˉ08ˉ31O2:57:33 [5〔mpy.coIe.5craper] 0[80C: 5cmped十r咖〈201 http5://哪.‖ttpbi∩.org/get?query=18∩a『∏e=gemeγ〉 {|arg5|8 {,∩a爬‖: |gem论y,’ !query|: !1|}’ ,header5! : {|∧c〔ePt|; 0text/hm1’app1j〔atio∩/xhtⅢ1+x们1’app1j〔3t1o∩/x川1jq≡O。9’*/*jq=O.8! ’ |∧〔〔eptˉ[∩codj∩g0 : ‖gzjp’ de千1ate0 ’ |Ac〔eptˉ[a∩g归ge! : 0e∩,’ 』‖o5t0 : 0….httpbi∩.oIg ’

』05erˉ∧ge∩t『 : 『5CmPy/2.2。1 (+httP5://5CraPγ.Org)|’

|Xˉ蛔z∩ˉ丁raceˉId, : !佣oot≡1ˉ5+4b+69cˉ9e十Ocea4〔9c353a3+b』67o6b0}’ !origi∏{ 8 ‖o∩e’

‖ur1|: 0‖ttp5://棚.∩ttpbj∩·org/get?query=18∩日爬=gemW}

状态码变成了2O1’Item的or1g1∩字段变成了‖o∩e’证明〔u5tom2e∩1dd1eware对Spider输人的 Response和输出的Item都实现了处理°

到这里,我们通过自定义SpjderMjddleware的方式’实现了对Spider输人的Response以及输出 的Request和Item的处理。

另外在Scrapy中’还有几个内置的SpiderMiddleware’我们简单介绍_下°

〖5〔

{|

第l5章Scrapy框架的使用

780

厂 ■ ‖ `

●‖ttp[mo耐jdd1酗are

理’5OO以上的不会处理°其核心实现代码如下: de千_j∩1t-(Se1+’ 5etti∩gS):

5e1「.h己∩d1ehttp5t日tu5a11=5etti∩g5.getboo1(!‖∏P[R【OR∧l[侧∧[[|) 5e1十.门a∩d1e二‖ttp5tatu51i5t≡5etti∩gs.get1ist(』‖丁W[【【0R∧[L叫[D〔"[5』)

|‖|]

‖ttp[rror‖1dd1eware的主要作用是过滤我们需要忽略的Response’比如状态码为2OO~299的会处

de十proce55-5pider一i∩put(5e1+’ re5po∩5e’ 5pjder); i千2印〈=re5PO∩5e.5tat0S〈3OO自 Ietur∩

『∏eta=re5pO∩5e.『∏eta

i千 0ha∩d1e-http5tatu5a11| i∩爬ta: retur∩

if 0怕∩d1e-http5tatus1j5t0 i∩们eta: 己11owed5tatuses=爬t己[|ha∩d1e∩ttp5tatu51i5t!] e1j+5e1千.ha∩d1e-http5tatusa118 retur∩

e15e:

a11owed5tatu5e5=getattr(spider’|↑E∩d1ehttp5tatu51ist|’ se1十ha∩d1eˉ∩ttp5tatu51i5t)

i+re5po∩5e.5tatu5j∩a11owed5tatu5e5:

·

司 ■■‖

■■叫

可以看到它实现了proce55-5pjder-1∩put方法’然后判断了状态码为2OO~299就直接返回’否则会根 据ha∩d1ehttp5tatu5a11和ha∩d1ehttp5tatu5115t来进行处理°例如状态码在∩a"d1e‖ttp5tatu5115t 定义的范围内,就会直接处理,否则抛出‖ttp[rror异常。这也解释了为什么刚才我们把Response 的状态码修改为201却依然能被正常处理的原因,如果我们修改为非20O~299的状态码’就会抛出

‖‖

retur∩

rai5e‖ttp[Iror(Ie5po∩se’ !Ig∩or1∩8∩o∩ˉ2OOre5po∩5e!)

异常了。 ·

另外,如果想要针对一些错误类型的状态码进行处理,可以修改Splder的ha∩d1ehttp5t3tu51j5t

属性’也可以修改Requestmeta的∩a∩d1e‖ttp5tat‖51ist属性’还可以修改全局5etttj∩g5



‖∏p[RR0日AL[0‖[0〔00[5。

●肝千5it酬jdd1副are

·‖·

比如我们想要处理4o4状态码’可以进行如下设置: ‖丁丁p[RROR∧[l叫[0〔仰[5≡ [404]

0

a11o"eddoⅧaj∩5其实就是在这个SpiderMlddlewa!e里生效的°其核心代码实现如下:

‖‖

肝+5ite‖jdd1eware的卞要作用是过滤不符合a11o"ed—doⅦa1∩5的Request, Spider里面定义的 de+Pro〔e55—5piderˉoutput(5e1十’ re5po∩5e’ Ie5u1t’ 5pjder): 千OIXj∩reSu1t:

i+j5j∩5ta∩Ce(x’ Reque5t): i十x.do∩t千i1teror5e1「.sho01d+o11ow(×’ 5pjder): yie1dX e15e:

do‖m∩≡ur1par5e〔己ched(x).‖o5t∩己们e j千do们a1∩a∩ddo『∏m∩∩otj∩5e1+.do们aj∩55ee∩;

e15e:

yje1d×

』|‖‖

{|do帕j∩|: do帕j∩′ request』: x}’ extra={|5pider』: 5p1deI}) 5e1千.5tats.i∩〔γa1ue(′o仟5jte/do们ai∩50 ’ 5pideI=5pider) 5e1千.5tat5.j∩〔γa1ue(0o仟51te/千i1tered|’ 5pider=5p1deI)

‖‖

5e1十.do帕j∩55ee∩.add(d咖aj∩) 1ogger.debug( "「j1teIedo仟5iteIeque5tto咒(do帕i∩)r:咒(reque5t)5"」

可以看到,这里首先遍历了re5U1t’然后判断了Request类型的元素并赋值为x°然后根据x的

do∩t十i1ter` ur1和Spjder的a11oweddoⅦa1∩5进行了过滤’如果不符合a11oweddoⅧa1∩5,就直接输 ‖

g



l5.7



) ■尸【‖‖止尸「}■■「巴■尸|△口

|卜「|)

伊‖|卜「||■「匹β「丛∩『尸◆「〖『‖|卜‖|■厂卜|≥炉巴尸β|■Ⅲ■ 卜|||)△尸∩卜|■↓

} )|尸广|匹β|≥「∩门户■‖广「卜}广|》『■厂|》[尸|伊巴■「|卜【广|卜巳■「|■■『|

仁》



ItemPipeline的使用

78l

出日志并不再返回Request’只有符合要求的Request才会被返回并继续调用。 ●0r1Le∩gt渊1dd1副are

0r1[e∩gth"jdd1e"are的主要作用是根据Request的URL长度对Request进行过滤,如果URL的 长度过长,此Request就会被忽略°其核心代码实现如下: 0c1a5s爬t∩od

de++ro‖∏5ettj∩g5(〔15’ 5ettj∩g5〉8 爬x1e∩8t∩=Settj∩g5.getj∩t(00RLl[肌『‖lI问I丁‖) de+proces5-BpideI-output(5eM』 Ie5po∩Be’ Ie5u1t’ 5pjder): de千 +j1ter(Ieque5t):

j十15j∩Sta∩Ce(reql」eSt’ Reque5t)a∩d1e∩(reque5t。ur1) 〉5e1十.阳×1e∩gt∩: 1og8er.debl』g(阑I8∩orj∩g1i∩k(ur11e∩gth〉乃(|∏ax1e∩8th)d):%(ur1)5 ,『’ {!∏ax1e∩8t∩! ; 5e1f·阳x1e∩gth’ 』ur1! : reque5t.ur1}, extra={,5pider,: 5pider}) retur∩「a15e e15e: retur∩『rue

retur∩(I「oIrj∩re5u1toI()i千ˉ「i1ter(r))

可以看到,这里利用了Pmce55ˉ5P1derˉoutput对re5u1t里面的Request进行过滤,如果是Request 类型并且URL长度超过最大限制’就会被过滤。我们可以从中了解到’如果想要根据URL的长度进 行过滤’可以设置0R[{[‖C丁‖[I∩I『° 比如我们只想爬取URL长度小于50的页面,那么就可以进行如下设置: 0R[[[陋Ⅷu日I丁■5O

可见SpiderMjddlewaIB能够非常灵活地对Spider的输人和输出进行处理,内置的_些Spjder MiddlewaIe在某些场景下也发挥了重要作用。另外,还有一些其他的内置SpiderMjddleware’就不在 此--赘述了’更多内容可以参考官方文档: htlps://docs.scrapyorg/en/latest/topics/spjdePmiddlewaI℃ˉ html#builtˉinˉspjdePmjddlewareˉrefeTBnce。

4总结

本节介绍了SpiderMiddleware的基本原理和自定义SpiderMiddleware的方法’在必要的情况下’ 我们可以利用它来对Spider的输人和输出进行处理’在某些场景下还是很有用的。 本节代码参见: https://githuhcom/Python3WebSpider/ScrapySpiderMiddlewareDemo。

↑5.7 |tem尸|pe||∩e的使用 在前面的章节,我们初步介绍了ItemPipelme的作用’本节我们再详细了解_下它的用法°

ItemPipeline即项目管道,它的调用发生在Spider产生Item之后°当Spider解析完Response,ltem 就会被Engjne传递到I贮mPjpelme,被定义的ItemPipeljne组件会顺次被调用,完成—连串的处理过 程’比如数据清洗、存储等°

ItemPjpelme的主要功能如下° □清洗HTML数据。

〖5

□验证爬取数据,检查爬取字段。 □查重并丢弃重复内容°

□将爬取结果储存到数据库中。







0



‖■‖■曰‖|』

782

第15章Scrapy框架的使用

□pro〔e55ite们(ite"’ 5pideI)

另外还有几个比较实用的方法,它们分别是: □ope∩ˉ5p1der(5pjdeI) □c1o5eˉ5pider(5pider) □+ro∏〔mN1er(〔15’ 〔raw1er)

下面我们对这几个方法的用法进行详细介绍°

』|‖

●proce55=jt印(it印’5pider)

■■』』■〗〗■■□】□】■Ⅵ|二■〗』〗句|』■■□〗〗·]」■‖」■】||□■】』■】‖]|」■]】·|

我们可以自定义ItemPipeline’只需要实现指定的方法就好,其中必须实现的一个方法是:

』‖|』可

‖.核心方法



proce551te阳是必须实现的方法’被定义的ItemPipeline会默认调用这个方法对Item进行处理, 比如进行数据处理或者将数据写人数据库等操作°proce5site阳方法必须返回Item类型的值或者抛出 —个DropItem异常°

pro〔e55jte∏方法的参数有两个° □1te": Item对象’即被处理的Item°

处理,直到所有的方法被调用完毕°

□如果抛出DropItem异常,那么此Item就会被丢弃,不再进行处理° ●ope∩=5pider(se1于’ 5pider)

〔1o5e5p1der方法是在Spider关闭的时候自动调用的,在这里,我们可以做_些收尾工作,如关 闭数据库连接等,其中参数5pjder就是被关闭的Spjder对象。

||

●c1osα5pider(5pider)

□·‖■]‖■■||』■、」』■||」】■】‖』■

ope∩ˉ5pjder方法是在Spider开启的时候被自动调用的,在这里我们可以做_些初始化操作,如 开启数据库连接等°其中参数sp1der就是被开启的Spider对象°

‖』■■』■】‖‖■■□∏〗‖|』·■』■‖|

□如果返回的是Item对象,那么此Item会接着被低优先级的ItemPipeline的pIoce55jteⅦ方法





该方法的返回类型如下°





□5p1der: Spider对象,即生成该Item的Spider°

0

●十rmCm"1er(C1s’ 〔r酮1er)

十IoⅦ〔ra"1er方法是_个类方法’用0〔1a55们ethod标识’它接收-个参数〔raw1er°通过cmw1er

对象,我们可以拿到Scrapy的所有核心组件’如全局配置的每个信息°然后可以在这个方法里面创建 —个Pipeljne实例°参数c15就是Class’最后返回_个C‖ass实例。 下面我们用一个实例来加深对ItemPjpeline用法的理解° 2本节目标

本节我们要爬取的目标网站是https:〃ssrl.scrapecenter/,我们需要把每部电影的名称、类别、评 分`简介、导演、演员的信息以及相关图片爬取下来,同时把每部电影的导演、演员的相关图片保存

成一个文件夹,并将每部电影的完整数据保存到MongoDB和Elasticsearch里°

这里使用Scrapy来实现这个电影数据爬虫’主要是为了了解ltemPipeline的用法。我们会使用 ltemPjpeline分别实现MongoDB存储`E‖asticsearch存储` Image图片存储这3个Pipeline。



| q



















l5.7 ItemPipeline的使用

783

在开始之前,请确保已经安装好MongoDB和Elasticsearch,另外安装好Python的PyMongo、 Elasticsearch、Scrapy包’安装参考如下° □Scrapy: https://setupscrape.center/scrapy·

□MongoDB: https://setup.scmpe.centeI/mongodb°

□PyMongo: https://setup.scrape.centeI/pymongo° □Elastjcsearch: https://setup.scrape.centeⅣelastjcseaI℃h°

□ElasticsearchPython包: ht‖ps://setup.scrape.centeI/elasticsearch-py°

做好如上准备工作之后’我们就可以开始本节的实战练习了° 3.实战

我们之前已经分析过此站点的页面逻辑了’在此就不再逐一分析了’直接上手用Scrapy编写此站 点的爬虫’同时实现几个ItemPipeline。

首先新建_个项目’我们取名为5〔rapy1te刚pipe1j∩ede"o’命令如下: 5〔rapy5tartproject5〔mpyiteⅦpjpe1j∩ede『‖o

接下来新建_个Spider’命令如下: 5cmpyge∏5pjder5〔rape55r1。5〔mpe。〔e∩ter

这样我们就成功创建了-个Spjder,名字为5crape’允许爬取的域名为55r1.5cmPe.ce∩ter。

接下来我们来实现列表页的爬取°本站点_共有l0页数据,所以我们可以新建10个初始请求, 实现5tartˉreque5t5方法的代码如下: +ro∏5〔mpyj呻ortReque5t’5pjder 〔1a555〔r己pe5pider(5pjder): ∩己爬= 05Cmpe‖

a11…ddo们ai∩5= [055r1.5〔mp巳ce∩teI0 ] ba5euI1= 0‖ttp5://55I1·5〔r己pe.〔e∩teI0

爬xJage≡1o de千Start-reql』e5t5(5e1+): 千oIii∩Ia∩ge(1’ 5e1「。阳x-page+1): uⅢ1=+|{5e1十.ba5eur1}/pa8e/{i}0

yie1d∩eque5t(ur1’〔日11ba〔k=5e1十·p日r5eˉi∩dex) de十p己rsej∩dex(5e1十》 re5po∩5e): pⅢj∩t(re5Po∩5e)

在这里我们声明了阳x—page即最大翻页数量,然后实现了5tartˉIeque5ts方法,构造了!0个初 始请求分别爬取每-个列表页,Request对应的回调方法修改为了par5ei∩dex,最后我们暂时在 par5ei∩dex方法里面打印输出了re5po5∩e对象° 运行这个Spider的命令如下: 5〔rapy〔raW15〔r己pe

运行结果类似如下:

2O20=08ˉ3121;O6:1S [5〔r日py.〔ore.e∩g1∩e]D[B0C:〔m侧1ed(2o0)〈C[『∩ttp5://55r1.5〔rape·〔e∩ter/Pa8e/1) (re+ere【: ‖o∩e)

2O2oˉ08ˉ3121:o6:15 [5crapy·〔ore.e∩gi∩e]D[80C:〔m门1ed(2卯)〈C[『http58//55r1.5cmpe.〔e∩ter/page/S〉 2020ˉ08ˉ3121:06:15 [s〔Iapγ.core.e∩8j∩e] 0[B0C:〔r日N1ed(2OO)〈C[『∩ttp5://s5r1.5craPe.〔e∩teI/P38e/』〉

(re千erer: ‖o∩e) (re+ereI: ‖o∩e)

〈20Ohttp58//55r1.5crape.ce∏ter/pa8e/1〉 〈200‖ttp5://55r1。5cIape.〔e∏ter/page/5〉

〖5

第l5章Scrapy框架的伎用

784

〈2"http5://ssr1.5crape.〔e∩ter/p38e/4〉

‖■■

2o2oˉo8ˉ〕121:o6:16 [5cr己py。core。e∩gj∩e] D[Bl」C;〔m"1ed(200) <C【『http5://5Br1.5cmpe。〔e∩ter/page/9〉

』 ·

(re+erer: ‖o∩e) 〈2OO∩ttp5自//55r1.5crape。ce∩ter/page/9〉



·

2020ˉ08ˉ3121:o6:16 [5cmpy.core.e∩gj∩e]D[B0C:〔ra"1ed (2oO)〈6[Thttp5://55r1。5cmpe.ce∩ter/page/8> (re十eⅢer: ‖O∩e〉 〈200https;//55r1·5〔rape.〔e∩ter/page/8〉

2O2OˉO8ˉ3121:O6:16 [5cmpy.〔ore.e∩gi∩e]0[8l」C:〔m"1ed〈2m)<C[丁http5://s5r1.5crape.〔e∩ter/p己ge/2〉 〈refeIer: ‖O∩e) <2OOhttp5://55r1。s〔mpe·ce∩teⅢ/page/2〉

202Oˉ08ˉ3121:o6:16[s〔r3py·core.e∩gi∩e]0[B[)C:〔ra"1ed(2m)<6[丁http5;//55r1.5〔rape.〔e∩ter/pa8e/1O〉 (re千erer: ‖o∩e) <20ohttps://55r1。5cmpe。ce∩teⅢ/p日ge/10>

可以看到对应的列表页的数据就被爬取下来了’Response的状态码为2OO°

接着我们可以在par5eˉj∩dex方法里对re5po∩5e的内容进行解析,提取每部电影的详情页链接’ 通过审查源代码可以发现’其标题对应的CSS选择器为.1teⅦ .∩aⅦe’如图l5ˉ9所示°

| (



·藤·『闽…|…

-=

〈. 步○



▲■mU≈■『玛qc宙洒





0

■王别姬ˉ下己rewe‖肌yC◎∩cubi∩O m● 】w3p7.26上蚁

…望I严.呻…h

‖‖

国!盈删 这个杀手不太冷.L自◎∩ 空m



■/√

|{

舔‖"!

≡-一≡一—_==-==—乙≡■=—

|』■■■司

中■内边.中■什记/T7】分钟

」』■Ⅲ■■■‖||■■】·{

=_二≡二羔ˉˉ二癸≡:弓

些三

田田

★0力●

5crape

亩赢满ˉˉ雨一==ˉ ≈恼硒淘…mγ…m l咖冲m……α

■__→.司ˉ=…宁琶…=.…≤…肖ˉ一ˉ… :S梳■ _

■·=■-

ˉ二{r: 聋…-凹 …′尝—=ˉ、

≡一 , ˉ

◆■T●叶℃■樟四恒u$坐凸●1…l◆1…l■■·■`…1凸…●l=■l~●`=四l雪·尸≈d1诉 T<』■■【仔U→F…蝇cu$>■广■●l和〖■1…l■汹白‖乙l≤L≡0●l<●l→l】●1≤@l=→〗0■■

』………≈刑…′…″ . ˉ博

蔗黔噬,鸭`翱谜γ″, 』扩嚼电司—

蛔“f→7…1铸d■…叶”D■它…=P●…u时仁=穴山qP宁≡′钓∑≥

<心

→→

‖) P←=







■■■■

■■■←←=→

■-→■●宁■



= 早■qS…

—m∏■■凹1叼邑m『山『ˉm■j

吓==

审≈t0■>



0

v…t印■■印=wm】四t庐■■w↑t■■C{汕□尸Gl=坤tk■c■『军叮■l=■『【面=中7m厂y台`=■tt畴■』日1□D Q密吟′≡

巾p[



→.





·.

…「幻…∏?∏《…

刨”叮J b‖…『

<…『…



々′山■

件→眶怜

T啦d■■△?→丁郎凹■吨广D→≡』M◎铀 ●l叼呵《

域=m≡丁7…/■f…

.≡三乙ˉ÷j毒匡〗;Ⅱ 拓;…·

{…〃…!oh…佰

刨…●F{≥0=y…t吗●】…■丁=N上砰′0… √O』四 ■….白

==-

瑟ˉ办辅m嚣=…….杰a苞°宅占!酝~≡·…互’—…谣=ˉ:ˉ宅忘工≡ˉ己二.≡≡″.ˉ_= 》 ■

■ ■…



= 夕0b酗门

′ §:霹;『瓣撼飞…’,』 O挝叫磅四‖O『; 飞喊↑0

■…mt凸←丁……】丁Ⅱ沁叼■…■ 毛′·№沪

T■d』■…←】T…≈怎l凹……』∩↑@山● 可■d』v…←】T…■怎l凹……』∩↑@山●



■仔炉≈■■■

…■出q

乙…凹吟诈w…=审m√…■

“■…-■■

▲~■■

…』t…〖 「P吐』虑$

丁可…仁咨←刀…四t…菌wttm乒cu■巳≥·1=t渔m【中叮●1劫t哇p『…叮■l→0←9■l≈

,

■■■■=●

! ·●R=…{

■<w■《→P画咯■皿■………∏

▲ .

ˉ

.■旧m力^钩po山≈鱼●■■鲍凸七α●<仕‖0■…■■O二二.二≡二←≡唁二g≡ˉ≡兰≡引■公凸…=■◇毗■雪v

四m「0■睡u】; 韵伊~=→ h『…8‖叼0●◆0讥

》≡■盎萨.患尸…·……-=一…………→……←…, .^习

图l5ˉ9页面源代码

‖』日

所以这里我们可以借助re5po∩5e的cs5方法进行提取’提取链接之后生成详情页的Request°可 以把par5eˉ1∩deX方法改写如下: de「parse=i∩dex(5e1十’ re5po∩5e): fOrit印i∩re5po∩5e.C55(‖.it咖!):

hre「≡jt印.〔S5(0 .∩己爬: :日ttr(hre「)|)·extmCtˉ「jr5t()



ur1二re5po∩5e.ur1joi∩(hIe千)

yje1dReque5t(ur1’ca11bac促=5e1f。p己r5eˉdetai1) de「par5e-det日j1(se1十′ re5po∩5e): pri∩t(Iespo∩5e)



在这里我们首先筛选了每部电影对应的节点,即.1teⅦ’然后遍历这些节点提取其中的.∩a肌e选 择器对应的详情页链接’接着通过re5po∩5e的ur1joj∩方法拼接成完整的详情页URL’最后构造新的

|(



详情页Request’回调方法设置为par5eˉdetaj1’同时在par5e-deta11方法里面打印输出re5po∩5e。



h

p



h

l5.7 ItemPipeline的使用

785

重新运行’我们可以看到详情页的内容就被爬取下来了’类似的输出如下: 2O2OˉO8ˉ3121自18:O1 [5〔mPy。COre.e∩8i∩e] D[8l」O〔IaW1ed (200)〈C[丁∩ttpS://55r1.5Crape.〔e∩ter/detaj1/6〉 (re十eIeI8 ‖ttp58//5sr1.5crape.〔e∩ter/p己ge/1〉 〈200∩ttp5://55r1°5〔mpe·〔e∩ter/detaj1/6〉 2020ˉ08ˉ3121:18:02 [s〔r己Py.〔ore.e∩gj∩e]D〔B0C:〔mN1ed (2O0)〈C[丁∩ttp5://55r1.5〔rape.ce∩teI/detai1/3〉 (re十erer: http5://55r1.5〔r己pe.〔e∩ter/page/1) 2OⅪ0ˉO8ˉ3121:18:02 [5〔mPy.〔ore.e∩gj∩e] 0[8l」C:〔raw1ed (2OO) <C[丁‖ttp5://55r1.5crape.〔e∩ter/det日i1/4〉 (re+erer: ∩ttp5://s5I1。5cmpe.ce∩ter/p己ge/1) 〈2OOhttp5://$5r1·5〔Iape.〔e∩ter/detai1/3〉 〈】o0‖ttps://55r1°5〔mpe.〔e∩ter/detai1/4〉

2O20ˉ08ˉ3121:18:O2 [5〔r己Py.〔ore.e∩gi"e]0【80C:〔raw1ed(2OO) <C[『∩ttp5://5sI1.5〔r己pe.ce∩ter/detaj1/1O〉 (re十erer: http5://s5r1.5〔mpe·ce∩ter/page/1) <2oo∩ttp5://s5r1°5〔rape.〔e∩teI/detai1/10〉

2O20ˉO8ˉ3121:18:02 [scrapy.〔oIe.e∩gi∩e]0[80C:〔mw1ed (20O)〈C[丁bttp5://55r1.5cmpe.〔e∩ter/detai1/8〉 (re十erer: http5://55r1。5〔rape.ce∩te工/p己ge/1) 〈20o∩ttps8//5sr1°5crape.ce∩ter/detai1/8〉

202Oˉ08ˉ31∑1:18:O〕[5crapy.〔ore.e∩gj∩e]0[B[」C:〔m"1ed (2OO)〈C[丁们ttp5://s5r1。5〔r己pe。〔e∩teI/detai1/5O〉

(re十eIer; http5://55m。5〔Iape.〔e∩teI/pa8e/5) 〈2OOhttp5;//55r1.5〔mpe·〔e∩ter/detai1/50〉

其实现在par5edeta11里面的re5po∩5e就是详情页的内容了,我们可以进一步对详情页的内容 进行解析,提取每部电影的名称、类别、评分、简介、导演、演员等信息°

| ■

首先让我们新建一个Item’叫作‖ovjeIte们’定义如下: 十Io旧5〔rapympoItIte‖』「ie1d 〔1a55∩oγjeIt印(It印): ∩a爬≡「je1d()

[●■∩‖‖「|■□二

〔ategorie5≡「je1d() 5〔oIe≡「je1d() dm帕=「je1d() directoI5=「ie1d() aCtor5=「ie1d()

泣里我们定义的几个字段∩a∏‖e、categorje5、 5〔ore、 dm"a、d1re〔tor5、aCtor5分别代表电影名 称、类别、评分、简介、导演、演员°接下来我们就可以提取详情页了,修改par5edetaj1方法如下: ●



de「p己r5edetai1(5e1十’ re5po∩5e): ite们=№γieIteⅦ()

it印[|∩3|∏e|] =re5po∩5e.xpat∩(』//dj`/[co∩t己i∩5(依1a55’ 』』1t咖刷)]//b2/text()`)。e》(tract+iI5t()

ite‖〔|cate8orje5‘ ] ≡re5po∩5e.xpatb(′//butto∩[〔o∩tai∩5(α1355’"〔ategory闻)]/5p3∩/te)《t()!)。extra〔t() ite"[ !5〔oIe‖] =re5po∩se。〔55(‖.5core: :text!).reˉ十irst(‖[\d\.]+!) jte"[‖dra阳‖] =re5po∩5e.〔55(! .dIa|∏ap: :te)《t|).extmctfir5t()。5trjp() jte刚[,diIe〔toI5‖] ≡ []

djrector5≡re5po∩5e.》(p己t‖(‖//djγ[〔o∩taj∩5(依1a55’"director50!)]//djγ[〔o∩t3j∩5(@〔1a55’"dire〔tor口)]!) +ordire〔tori∩dire〔tor5目



dire〔torˉma8e=dire〔tor.》(p日th(|.//j吧[依1a5s≡口mage赋]/05r〔`).e)《tmct十ir5t() djre〔toI∩a∏记=djIe〔toI.)《pat∩(|.//p[〔o∩tai∩5(伙1a55’|0∩a爬00)]/text()!).extra〔t十1I5t() ite们[|dire〔tor5』].appe∩d({ 0∩日贬|: dire〔tor∩a∏记’

!mage『 8 director-i阳ge p

}) jte刚[0a〔tOr50] ≡ []

actOrS≡re5Po∩5e.C55(‖.actOr5 .a〔tOI‖) ■巳■‖‖凸|β△∩》



b

十OIa〔tori∩a〔tOr5;

actor-1阳ge≡a〔tor。〔55(|.a〔toI ·1∩age: 8attr(sr〔)‖).e×tra〔t+ir5t() ●

a〔tor∩a雁=a〔toI.〔55(|.actor 。∩a爬: :text!)。extract+ir5t() jteⅧ[ !a〔tor50]。日ppe∩d({ !∩a‖旧: a〔tOr∩a眠’

|j爬ge‖: a〔toI-mage })

yie1d1te阳

厂〔

↑5

』□」

786

第l5章Scrapy框架的使用





在这里我们首先创建了_个∩oγ1e1te"对象,赋值为jteⅦ。然后我们使用xpat‖方法提取了∏己门e、 〔ategorie5两个字段°为了让大家不仅仅掌握xpat‖的提取方式’我们还使用CSS选择器提取了5core 和dmⅦa字段,同时5〔ore字段最后还调用了re「jr5t方法传人正则表达式提取了分数的内容°对于 』■■■■□』□』■■‖』

导演djre〔tor5和演员actor5,我们首先提取了单个djrector和actor节点,然后分别从中提取了姓

名和照片,最后组合成_个列表赋值给d1re〔tor5和actor5字段° 重新运行—下,可以发现提取结果类似如下: 2o20ˉ08ˉ3122:凹;“[5cmpy.〔ore·5〔ra医I]D[BlL; 5cmped仕咖〈2m∩ttp5://55r1.5cra征.ce∩ter/detai1/33〉

{!己ctor5! : [{!j∏Ege! : ‖‖ttp5://p1.眠jtua∩。∩et/‖mvje/4O4c9882∑552S75b卯61〔6be12〔1d刺088qO°j喊128吐170|]ˉ1eˉ1〔0’ 』∩田肥| 弓 0里允.臭巴璃0}’

{0imge|: ,http5://p1.爬jtua∏.爬t/′mγje/7b3a7d3ed65b5eo+O〔+89d▲dod34b1e126818.jpg@皿蜒17叭1eˉ1〔』’ ‖∩■肥! : !略易。西往尤斯|}’



{|

{|nE8e0 : |∩ttp5://pO.爬jtua∩。∩et/mγie/6893be们7e0己+2e「829be6c5ae99283o27633.j哩12蜒17帅ˉ1e—1〔|’ 0∩田眨0 : 0哈迪.琼斯‖}」

{0n田ge! : 0http5;//p1.爬it0a∩。∩et/∏mb/3a2061d771d98566d3e5+日Sc08〔5eOb〕3685。p∩澳128w-170h-1e_1〔|’ !∩田肥! 2迈允尔.利弗|}’

{』j∏■ge『 : ,http5://p1.贬jtu己∩.∩et/mγie/己1d8q己千3ad30917431己7q9a6068be18a1667O.jpg0128比17oh—1e—1〔‖’

°□日‖』■|』

‖∩刮庇! ; "RiChard0Ba∏y"}’

{|i∏Ege0 : 0http5://四.爬jtoa∩·∩et/巾vie/d4a4十85a2Sd+beo86e〔7阳7128ac2ee210261.j喊128吐170h_1巳1c0’

,∩引肥』: {也∩5PeterROt∩|}]’ ,〔己tegoIjes『: [0纪录片,]’

!d1re〔tor5》: [{,j∏Ege|: !http5://p1.爬jtlE∩。爬t/mγie/7b3a7d3ed65b5e0+仗十89d』d侧34b1e126818.j唾12趾17助—1eˉ1〔|’ ’∩蓟记0 : ,略易。凸右尤斯!}]’

≡■】纠□】·■■■出」口」■■■□|

|dm∏a|: 0日本和砍山县太地,是一个景邑优类的′」`渔村,然而这里却常平上演豺惨允人道的一幕·每年,数以万计的海脉 经过这片海战,他们的猿程却在太地Ⅲ然品止°渔氏们将海脖驱赶到靠近序边的一个地方’米自牌j||练师拄选合适的 对象’剁下的大批海牌则枕渔氏迁尤理由地赶尽杀绝°这些屠杀’这些罪行, 因为种种利A而枝玫府和相关姐织所 隐瞒°理查捻.贝璃年轻时甘足一名淹牌训练师,他所*与拍摄电影《海胖的故本》各爱的朋友…,’ 0∩a爬! ; 0海胖湾ˉ『∩e〔Oγe′’ 05core|: !8.8,}

可以看到这里我们已经成功提取了各个字段然后生成了MovieItem对象了。

下一步就是本节的重点内容了’我们需要把当前爬取到的内容存储到MongoDB和Elasticsearch 中’然后将导演和演员的图片也下载下来°

要实现这个操作,我们需要创建3个ItemPipeline’其中两个分别用来将数据存储到MongoDB、

‖·Ⅵ〗‖■

ElaSticseamh,另外_个用来下载图片。 ●MongoDB

《(

之前我们已经实现过MongoDB相关的Pipeline了’这里我们再简略说一下°

首先确保MongoDB已经安装并且正常运行,既可以运行在本地,也可以运行在远程’我们需要 把它的连接字符串构造好,连接字符串的格式如下:

乙■■‖°■司‖■‖{』口‖·』■■■

晒∩godb://[useI∩a贬:pa5s"or娟]∩o5t1[:port1][’…ho5t‖[:port‖]][/[de十au1tautbdb][?optjo∩5]]

比如运行在本地27017端口的无密码的MongoDB可以直接写为: m∩godb://1o〔a1ho5t:27O17

如果是远程MongoDB,可以根据用户名、密码、地址、端口等构造。



我们实现-个№∩go08pipe1i∩e,将信息保存到MongoDB’在pipelinespy里添加如下类的实现:

‖仆■■|』■■』·■■

j呻ortpyⅧ∏go

+r咖5〔rapyjte‖mpe1i∩ede∏njteⅧ5j呻ort№γjeIte"

c1a55月o∩8o08Pjpe1i∩e(object): 饮1a55爬t∩od

de++r咖Cra"1er(C15′〔m"1er)8

「 }

l5.7 ItemPipeline的使用

787

‖尸=‖|》‖|户||β》|

c15.〔o∩∩e〔tjo∩5trj∩g≡cI己闪1er.5ettj∏g5.get(↓侧四B〔删‖[〔∏删5『RI陋‖) C15。databa5e≡〔r日w1er.5etti∩g5.get(!ⅧM"8D∧丁∧8A5[|) 〔15°〔o11e〔tjo『` =〔m"1er.5ettj∩g5.get(‖…8〔0[l[〔『I叫0) retur∩c15()



de千ope∏一5pjder(5e1+’ 5pider〉: 5e1+。〔1ie∩t=py‖〕o∩go.№吧o〔1ie∩t(se1千.〔o∩∩e〔tio∩5tri∩g) 5e1十·db=5e1+。〔11e∩t[Se1千.databa5e]

〖‖|仁尸|

de+proce55jteⅦ(5e1+’ ite■’ 5pjder): 5e1千.db[5e1千.〔o11e〔tio∩].‖pdate-o∩e({ ∩a眠! ; ite们[!∩a们e,] }’{

,$5et|: d1Ct(1te川) }’ 丁rue)

p

ret0r∩iteⅧ

p

de+〔1oseˉ5Pider(5e1+’ 5pider): 5e1f.〔1ie∩t.C1o5e()

P

这里我们首先利用+ro『∏craw1er获取了全局配置Ⅷ‖C"8〔0‖‖[〔∏0‖5『RI顺、刚‖印080A『∧8∧5[



和‖0‖C008〔0[[[〔丁I0‖’即MongoDB连接字符串、数据库名称、集合名词,然后将三者赋值为类属性°

p

接着我们实现了ope∩ˉ5pider方法,该方法就是利用十IoⅧcra切1eI赋值的co∩∩e〔tjo∩-5trj∩g创 建-个MongoDB连接对象,然后声明数据库操作对象,〔1ose-5pjder则是在Spider运行结束时关闭

p





MongoDB连接° p



接着最重要的就是pro〔e55iteⅧ方法了’这个方法接收的参数ite"就是从Spider生成的Item对 象,该方法需要将此Item存储到MongoDB中。这里我们使用了updateˉo∩e方法实现了存在即更新’







不存在则插人的功能°



接下来我们需要在settingspy里添加|‖O‖C008〔0‖‖[〔丁I0‖5「RI‖C、阳‖C卯80∧『∧8∧5[和阳‖C卯8



〔0L[[〔「10‖这3个变量,相关代码如下:

β

侧咖8〔Ⅷ‖[〔『I删S『RI陋=o5.gete∩v(|肋‖C"8〔删‖[〔丁I侧5丁RI‖G)



P p

肋‖咖8〔0ll[〔丁I叫= 『ⅦOVje50

p

这里可以将Ⅷ‖CO0B〔0‖‖[〔『IO‖5『RI‖C设置为从环境变量中读取,而不用将明文将密码等信息





写到代码里°

如果是本地无密码的MongoDB’直接写为如下内容即可: 阳‖C008〔删‖[〔『I0‖S『RI‖C≡ ‖咖∩godb://1oca1∩ost:27O17| #orju5tu5e ,1o〔a1bo5t!



这样’一个保存到MongoDB的Pipeline就创建好了,利用proces51te"方法我们即可完成数据



插人到MongoDB的操作’最后会返回Item对象。





●ElaSticsearch





存储到Elasticsearch也是—样’我们需要先创建一个Pipeljne’代码实现如下:







千ro爪e1a5t1CSear〔∩ i们port[1aStiC5eaI〔h 〔1a55[1a5tj〔5earcbpipe1i∏e(obje〔t): 0c1a55眶t∩od



de千+r咖〔mⅦ1er(C15’ Craw1er):







〔15。〔O∩∩e〔tjO∩5trj∩g=Cm"1er.5etti∩g5。get(|[l∧5丁I〔5[∧R〔‖〔删‖[〔丁I删5丁∩I‖C』)

〔15·j∩dex=〔raW1eI.5etti∩gS.get(|[[∧5∏〔5[AR〔‖IⅦ[X,) retur∩〔15()

de「ope∩ˉ5pider(5e1+’ 5p1der): 5e1+·〔O∩∩=[1a5tj〔5ear〔h([5e1+.Co∩∩e〔tio∏5trj∩g])

「 p

|U

| 』

|』

第15章Scrapy框架的使用

788



i千∩ot5e1+.co∩∩.i∩dice5.exj5t5(se1「.1∩dex); 5e1千.co∩∩.i∩di〔es.create(i∩dex≡se1+。i∩dex)

司』』□

de十PIo〔e55ite"(5e1十’ jteⅦ’ 5p1deI)8 5e1十。co∩∩。i∩dex(i∩dex≡5e1十.j∩dex’ body≡dj〔t(jteⅧ)’ id≡h己5h(ite们[0∩a爬』])) retuI∩1te"

de+〔1o5eˉ5pjder(5e1+’ 5p1der): 5e1千.〔o∩∩.tr己∩5port。c1o5e()

这里同样定义了[[∧5丁I〔5[∧R〔‖〔删‖[〔丁I0‖5『RI‖C代表Elastjcsearch的连接字符串’[[∧5「I〔5[∧R〔‖

I‖0[X代表索引名称’具体初始化的操作和‖o∩go08p1pe1i∩e的原理是类似的。 在pro〔e551teⅦ方法中,我们调用了1∩dex方法对数据进行索引’我们指定了3个参数,第一个 参数i∩de×代表索引名称’第二个参数body代表数据对象’在这里我们将Item转为了字典类型,第 三个参数1d则是索引数据的id’这里我们直接使用电影名称的ba5h值作为id,或者自行指定其他jd 也可以的°

同样地,我们需要在settingspy里面添加[[∧5丁I〔5[∧R〔‖〔删‖[〔丁I0‖5丁RI‖C和[[∧5丁I〔5[∧R〔‖I‖0[X: [L∧5『I〔5[∧R〔‖〔侧‖[〔丁I删5丁RI‖C≡o5.gete∩γ(‖[[∧5丁I〔5[∧R〔‖〔O‖‖[〔丁I删5「RI‖G)



这里的[[∧5「I〔5[AR〔‖〔0‖‖[〔丁I0‖5「RI‖C同样是从环境变量中读取的’它的格式如下: http[5]$//[uSer∩己爬:pa55咖rdβ]hO5t[:pOrt]

这里你可以根据实际情况更换成你的连接字符串,这样[1a5t1〔5ear〔hpjpe11∩e就完成了° ●ImagePipeⅡ∏e

Scrapy提供了专门处理下载的Pjpcline,包括文件下载和图片下载°下载文件和图片的原理与抓 取页面的原理_样’因此下载过程支持异步和多线程’十分高效°下面我们来看看具体的实现过程°

首先定义存储文件的路径,需要定义_个I灿C[55「0R[变量,在semngspy中添加如下代码:

|{

官方文档地址为: https:〃doc.scmpy.org/en/latest/topics/mediaˉpipelinehtml°



』■‖

http5;//l』5er:pa5mrd0e5·〔ujqj∩g〔己1°co∏↑:92oo



||

比如我实际使用的[[A5丁I〔5[AR〔‖〔0‖‖[〔「I0‖5「RI‖C值就类似:



I趴C[55『0R[= 0 °/i阳ge5‖

但是现在生成的Item的图片链接字段并不是加ageˉur15字段表示的’我们是想下载djrector5和 actor5的每张图片°所以为了实现下载’我们需要重新定义下载的部分逻辑,即自定义1‖agePjpe1j∩e 继承内置的I们age5pjpe1me’重写几个方法°

我们定义的I们agepjpe1j∩e代码如下: 「ro∏‖ 5〔r己py加portRequest 于Io阳已〔Iapy°e×cept1o∩5加portDropItm

』■■〗■=■】』』』·|■■】|』■】】·〗□‖』·』■■∏‖‖‖』■叮』■〗■』■■■|‖】‖■■■〗

内置的I阳age5pjpe1j∩e会默认读取Item的iⅧageˉuI15字段,并认为它是列表形式,接着遍历该 字段后取出每个URL进行图片下载。

司纠□∏』□

在这里我们将路径定义为当前路径下的images子文件夹,即下载的图片都会保存到本项目的images 文件夹中。

+roⅧ5〔rapy°p1pe1i∩e5。mage5mportIⅦage5Pjpe1j∩e



c1a55magepjpe11∩e(I爬ge5p1pe1i∩e):

de千「j1eˉp己t‖(5e1千’ req(」e5t’ respo∩5e≡‖o∩e’ i∩+o=‖o∩e): 川ovie≡reque5tⅧeta[ 「 0 ∏oγje0 ]

l5°7 ItemPipeline的使用

789

tγpe=req‖e5t.爬ta[0type! ] ∩a爬=Ieque5t·爬ta[|∩a爬′]

+j1e∩a爬=+0{mγie}/{type}/{∩aⅦe}.jpg』 retur∩十j1e∩a∏e

de「1te们-cα印1eted(5e1十’ re5u1t5’ 1te∩’ j∩「o): mageˉpath5= [×[{p己th‖ ] 十oroⅧ’ xj∩Ie5u1t5i+o长] j+∩otj们ageˉpath5; rai5eDropIteⅦ(‖I爬geD叫∩1o己ded「ai1ed) retur∏jte∏

de于get-爬di己ˉreque5t5(5e1十’ it咖’j∩十o): +ordirectori∩iteⅦ[,djreCtorS,]: direCtor∩a爬≡director[′∩己爬|] director=j爬8e=director[!ma8e!] yie1dReque5t(diIe〔tor=nE8e’爬t己={ 0∩a眠0 8 dire〔tor∩a眶’

0type0 8 0djre〔tor0』 Ⅷγie|: iteⅧ[!∩a爬,]

‖p

})



十ora〔tori∩jte‖[0actor5’]: 己Ctor∩a|∏e≡aCtor[!∩a腮!]



己ctor=j幅8e=a〔tor[!i吨ge!] yje1d【eque5t(a〔tor-j旧ge’爬ta={

「 》

0∩a∏归0 : 己CtOr∩a眶』

0tyPe0 : 0a〔tor0 ’ |巾v1e0 : ite‖[ 0∩己爬! ]



})



在这里我们实现了I们agepjpe1j∩e,继承Scrapy内置的I"age5pjpe1i∩e’重写下面几个方法。



□getˉ0]edjaˉrequest5:第一个参数jte"是爬取生成的Item对象,我们要下载的图片链接保存



在Item的dire〔tor5和actor5每个元素的1们age字段中。所以我们将URL逐个取出,然后构

▲尸[伊|≡■厂■【◆「△’『|〖■厂‖■|■「‖仔》[■「户匹■「|■〖■厂仕■|}|

造Request发起下载请求°同时我们指定了Ⅷeta信息,方便构造图片的存储路径’以便在下 载完成时使用。

□+j1eˉpath:第-个参数reque5t就是当前下载对应的Request对象。这个方法用来返回保存的 文件名’在这里我们获取了刚才生成的Request的删eta信息’包括佃oγ1e (电影名称)、 type (电影类型)和∩aⅦe(导演或演员姓名)’最终三者拼合为千j1e∩a"e作为最终的图片路径。

□1te‖ˉ〔o∩p1eted:单个Item完成下载时的处理方法。因为并不是每张图片都会下载成功’所以 我们需要分析下载结果并剔除下载失败的图片°如果某张图片下载失败,那么我们就不需将此

Item保存到数据库。jte阳ˉc咖p1eted方法的第_个参数re5u1t5就是该Item对应的下载结果’ 它是一个列表’列表的每个元素是_个元组’其中包含了下载成功或失败的信息。这里我们遍 历下载结果’找出所有成功的下载列表。如果列表为空’那么该Item对应的图片下载失败’

随即抛出DropItem异常,忽略该Item;否则返回该Item’说明此Item有效°

△ ■ ■ 尸 ‖ 尸 [ ▲ ■ 「



现在为止, 3个ItemPipeline的定义就完成了°最后只需要启用就可以了,修改semngspy’设置 I丁[‖pIp[[I‖[5的代码如下所示: I丁["pIp[[I‖[5≡{



■■|■厂「■■

05〔mpyjt咖pipe11∩ede∏℃.pipe1j∩es.I阳8epjpe11∩e0 ; 3oo』 {s〔rapγjt酬pjpe11∩ede咖°pjpe1i∩e5。№∩go0Bpipe1j∩e! 8 3o1’ 05crapyite‖pipe1j∩ede『m·pjpe1i∩es.[1a5ti〔5ear〔∩p1pe1i∩e! : 30】 }

这里要注意调用的顺序°我们需要优先调用I"agepjpe1j∩e对Item做下载后的筛选,下载失败的 Item就直接忽略,它们不会保存到MongoDB和MySQL里°随后再调用其他两个存储的Pjpeline,这





样就能确保存人数据库的图片都是下载成功的。





V 」



≡■

上-

=■=园巴—

□■■·』■■

| 第l5章Scrapy框架的使用

790

0

接下来运行程序,执行爬取,命令如下所示: 5CmpyCraw1j爬geS

爬虫一边爬取一边下载,速度非常快’对应的输出日志如图l5ˉl0所示° q



-



S://p1.咖e1t山α∏·"et/晒γ1 ,咖7ec亡O7S0 · L{,】∩αge, : ∩七印S://p1咖e1t山口∏·"et/硒Ⅵe/yb』α/“edb5唾刨壬0c寸89“…」4b1

e皿〔616.jp窿塑8洲170{}1e2色『, ↑ ` =ˉ搜图· ■□困e,2 `路筋ˉ曲在尤勋,卜」, 曲在尤斯,卜」, 丁‖

■■■

【-个■任 快茫佐』lb淹村 八n伏,口厂cm0 ,口厂c而q0 吕 ↑日本租歌山县太地°呈-个贝色比羹险小渔村然而这里却谢年上茨■俗无∧迫伏一 ↑日本引 ■■

早°簿千! ′ °簿千〗 蚁以力计的:刁脉经遭这片询臆0抱们的脓僵却在太地Ⅲ然向止·渔巨;】持沟膊疆赶副 蚁‖乳力 唾Ⅲ 宋目世界各继的海膘测龋痈挑遵台忌的对豫,剩下曲大姓谰啄则被灌民■ 抑近淖边的-个地万, 面|

尤0匠由池赶庸余鲍.逗些Ⅷ桑这些罪行p圃为稗仲$」益吹疆纽陶秘相关蛆鳞斯隐瞒·埋盒德,又 ≈

∏刁′≈J珍■】…歹仁■

0□「】】庐司】凹耳雨胆乙≈p厉■可…7■■≡…=●≡…0

头海际的

·∏

溺鱼轻厂笆尾名海历‖|练页°他所铲与拍受电影《弯臃的故串】留妥浓迎·但足,

^=□■P尸』

‖·

死i上哦窒阁的心灵受到强烈仇震温·从此0趣淑刀子运熬宠界的活韧钨不Ⅷ岂饿政府和柯畏百舷 的心灵受到强烈仇震温·从此0越淑刀子运熬宠界的活韧钨不Ⅷ岂饿政府和柯畏百舷 阻锤°呛粕他的旧影圃队慰力设法耐入太地盛淘脉周杀溺,只为将邪行公之丁众,撬救人矣可爱 他的旧影圃队慰力设法耐入太地盛淘脉周杀溺,只为将邪行公之丁众,攫救人矣可爱



的朋友ˉ . , ,

,陋跟c,: ,海饵湾 海饵湾 ●

0■ ≈《ˉO厂份. .

丫∩e〔oVe,’

°8月0》

03026:阴[白cFopγ·C@尸e。e吨j"e】C[806:〔mWleC《2硼)<‘[了卜七tpS:〃p1°mitMα 舀锤Gˉ碉ˉ距03026:阴[白cF□py·C@尸e。e吨j"e]C[806: 〔mWleC《2硼)<‘[了卜ttpS:〃p1°Tmtuα∏ 。『瞪t∧沁。√【巳)e5〔α〔72u7622印4e〔4bu60876〔52泊刨12657°〕p诀128W1硒↑1吧1〔≥(例e「e「e『: h咖e》 乙0乙eˉ馏ˉ犯0」:乙b:蜘[SC「◎pγ·p】pe[l∏e巴.+1l唾0亡趴6。 =tle《目锄『`lO口qeG); 0酗仍l◎口…仇l



αU≤ 巴 「『m≤匝丁↑lˉtp当。//p1·;胀匙t山●| ,l0eM『@vl巴/e5〔6〔〔71d762乙c凹qe巳4l义』锄8刑〔5m9m2657。jp嘘12

8冈→1〉0∩=1e=1c>尸Cfe尸内巳C〗∩…∩■≥

ef》□me]c[B062 〔『oWl≈《2碉》≤‘[T‖.ttp55〃p2.泥itm 乙02eˉ阴ˉ距0〕;巳6;帕『3〔mpγ.co厂e°efm"e]c[B0c: 〔『OWl≈《2拥》≤‘[T卜ttpS5〃p己。泥itm∩ .′]et′卿Jte/5仁Zq55d8龟75瓣3髓知d4b1饵C152“◎2%31ˉjp蜘卫βW-170↑↑ˉaQˉ1@(^efe「·P;№∩G)

乙0aG例ˉ驼03:26:”[s〔尸◎Oγ.pipe1hTe巴石iles气D[仪6日 量Ue(口咐〕lome◎); 0锄7)1oα土c∩l f『 e「伊酮<5F丁们士pSˉ//p1腮1t』m∩pt/沁Ⅵ白/w24gm8「G75况3硼M弘61佰收1墅阅鲤驼〕1ˉjp唤12 r鲤.<

‖‖■■〗·

8内≡1了9h≡1C=1C≥广e千e产eci∩池∩e> ‖"e.l C[B0喝吕〔m训leC《∑”》<6〔丫卜它tnSMJo1硬lt 2OzGˉ酚ˉ褪03』26:Oq[巴〔mpⅡ.CO尸e9『鸭i"e]C[B0T吕 〔「oWlec(2哟<碰了挝tp5日〃p1.唾]tuα∏

图l5ˉl0输出日志

■■‖



匿__



触碾晦;图毡

m■7号■的扎拘ˉ7酗玛“·‖n… P:■问凡达ˉ蛔■

巳睡『晌o吨

ˉ〗

h已cm「

墅匪■唾皿唾 伊■面曰αα

■冈飞正侍ˉ坠刨吟昭W间dp



●■艾米田.卡■J四 ■安■·■尼,“

■安托万.劳伦特j叼 ■员■■·弗勒歹↓pg

■本悉昭·甲…呵■凹Bumn慎

■贝玛。怨胸

■怕■仅:H■…·函k…1伊

□2■特博■埃』四

′■■■侠:用砧…m域k∩妇·$ ’ 』■闭护人ˉ白鳃 p

′■门击■乐■ˉ「嘲`tch」b

p

■■恋这件4U→批知网可..枷p ■■门的世异ˉˉJmⅣ■0剑酗p

困| □

■E·回京ˉ召四..°

|■■王…ˉ胞…γCα咆巾倔p

目■闪尔邑.贝£■蛔

□□司



酶_…凶



●铝

查看本地images文件夹,发现图片都已经成功下载,如图l5ˉll所示°

■w■宏瓦°克■■jp9 ■巧诌′布讶钠灌j” ■克沿葡尔■·Ⅲ砧.胸 ■玛■~浴尔·■库洛↓叼 ■臣旗尔ˉ罗兰.巴唾·闽 ∑F■马特.卡乌特.m

■托马,囊利宅尔.jpp ■的H芬知.■。肛』p0

』■大话■沏之月…的·“γSc的p

■cγ门M·∩dy购

■裙■游之大...◎ˉc■呻徊|阳p

『■大■天宫ˉ丁‖e"αmγNm0p ■型牢■聚础门■矾0…∩鸽■p

■c刁幽Ⅶ…吧}m

qα00o‖DO锤0■∏Ⅷm炉g ■」·m尸『■…S回γ「·wpg

■出梦空闰-■淀甲m ◆ ■断订山ˉα吐……Ⅷα』∏1●↓∩p ■放牛Ⅸ的巴天.L·雹c恤泣嫡·

】■飞■碎游记ˉ咖 ■贝之谷ˉ■o谷●十窃彭力

图l5=ll



jmages文件夹

可以看到图片已经分路径存储了’一部电影_个文件夹,演员和导演分二级文件夹’[图 片名直接 以演员和导演名命名°

然后我们用Kjbana查看Elastjcsearch,相应的电影数据也成功存储’如图l5ˉl2所示。

d

({|{」‖‖‖





‖‖‖

√■簿肇m砷……p申n唾h ■吞光乍泊ˉ0坤pγV叼m·『 p





‖ ’ γ

°睦臣自●触吕勤审刨己§甲°

■■『尸『▲∩

◎□●

~=.=…■叫=.…■

@尸…勿ty阳

O

∧白…m…°=《·…白8 .F刀仍■0 ·…早:

小屋

辑诚t尸占〃饥每■1□m挚≈t′…■门c●「T…7………6$…8.〗…0之■ˉw助=】·~0c°〗■酝■吾 《 ●…p片 口…勺 宁』…□.

哇→

0

?皿

0

凹…■回m■~f』』r〖』叼≈■Lr′=■′m°己■…『9.B…风■唾归m宙》●和…才于之■,■W■*….宁于■0■≡■■空 佩n~·■m雇■■■…0m丁…●碱■凶…=见油凯决°…■∏丁职…■0■…F寇噬钒后笑宁→■们丁宁王上厂问n… ■…….…丁■……咆…m申m■-◇华夫入…吨`啤二人■月■n论·m…■Ⅷ生酗灯》0轴八不■闪早。牢“户山巾早′华尖

……

P







■■■■



■■■■

矽·■旦





Ⅲ 巳

〔 【

丁 日 ]

使 的

|| 区 ■o…°呻

臼侣…=

…S………

→奎

V…硒付 ■≡

0…

<■…7yy5…

!蕊

@…

0…

…■



;嚣

●曲…

△『仕‖

0~≡

■… 》《

B唾cOF●

.….: 每…。

『…

■….吕 ·m…;′俩.■….啄…1Ⅳ…幽γ沁……0…Ⅷ锑,』…勉h=‖m】●ˉ】c□ 》0 (

↑…

●…●8 ●m·o ■0=Pp ■姑0涵o〃R0≈‖T0口≈宁惮↑…0■′呻……■■m■■钞7巧月0功V丁■■∩●Ⅶ■0…2≈V玲0■0尸·

■■0■问β古H

0 c怕t…1■



●□皿m■『●

.■·:簿力汀0



·…亏6 ■肮t芦吕〃p‖旦【空~呐t/…】■′爬切0…∩……m…“5·】m02■ˉT硷0●ˉ0c. ) 0

■唾旧四凹●远…才于之■呵…·宁m让■m…民『--_≡k■■甄年■■山闻.■ ■了呻…■吨四并…■m.……■■丁m耿m,…罕安.厢中…丁宁王上』↑沁…■● …并■了■己=份·……■…■■砚·罕炎人…,≈二八■开妇耳逛m宁…■书筐门

0F■

汉上∩o鞍人…宇o■…■早·…人…→…

■庐卜

0 『■■■

…m●庐】1『……山『

0睡O厂e

90S



图l5ˉl2查看Elasticsearch 0

查看MongoDB,下载成功的图片信息同样已成功保存,如图l5ˉl3所示。 ■‖巴■【■『‖「■伊■■厂}|■「·『|||■厂|‖[尸|卜

阉……t■…恼■哩7m7巴…

}她鳞t〔°uectm′M,…隐墨.)代…Ⅵ]) →七=_■■■■■■■■—-■■-■≈◇宅~■℃-■■■■■



■●■■■■凸■

| ■■

疆硒W“●四田Sm.

Q



蛔四

丁…



v趣《0)o词…蛀P刨q四e鳃?00…q『』80…) 凶归 已二∏

《8…}

==-

…侣…臼"由…4‖叫8↑鳃7■) ? -

N于王ˉTml幼》灿旧

尸晒…~

‖”…1

仍m…■m

[S牵呵■]

v蛔■≈=

i∑山∏西■] —-~-——-—-

7画们l

《2…》 歹伯.■可夫

… …咱

■■『■∏‖▲■尸

涵回而·

簿手蘸ˉ№……

函…■

m



卜′β

≥幽α》…《翘凸…靶?↑‖6…出"刨…口》 p画旧}…α沮P臼q唾3狗们O…“S08验》

{a…》



《8…》



p…《o叫…(口已00由…?06…团9叼

《a…》



护回间◎切…(■50…S8↑00S≈…旧8‖弘0口》

{:麓}



《·哟●}



《8呻●》

… …

◆画《Ⅷ蛔…迢《·6『“凹S00"…“04创…0 >酗《饱)…d(●6“幽锤‖↑↑…侧刨…·)

{:臆} {;圈

≥凹《涸》…宅f々…?"…帕8‖陡2■》

《7…}

尸…“》响…T6『…屿酣↑?6…测B?≈口》

《7…■}

卜画《6》…d■……"0S…″m酗s刁 尸四仍…山『颤乌…007…叫旧80s冯仔》 广翻《8》…‖■《吧00凶囱m0F·…蛔硒 护酗伯)呻c0蛔《·6仙曲63协↑0…■们8刨9口》

图l5=l3查看MongoDB

这样我们就可以成功实现图片的下载并把图片的信息存人数据库了°

…钠 …



……

■Ⅲ■厂|‖■厂|





…t

串巴■m毋的小王于.伯的父取木法沙品=个庶尸的■王·= 孰『m9

任凹倔创鹊翻…弓m凹田

p

鳃 …h

函…



脚吨 …

{2…》

巴旦~≡

0

…旧

P凹p] =…





792

第15章Scrapy框架的使用

4总结

ItemPipelme是Scrapy非常重要的组件,数据存储几乎都是通过此组件实现的’请认真掌握此内容° 本节代码参见: https://gjthub.com/Python3WebSpider/ScmpyItemPipelineDemo°

|5B巨xte∩s|o∩的使用 前面我们已经了解了Scrapy的常用的基本组件’如Spider、DownloderMiddleware、Spider

‖(

Middleware、ItemPipeline等,其实另外还有_个比较实用的组件Extensjon,中文翻译叫作扩展°利



用它,我们可以完成我们想自定义的功能°

■■』句

↑.匠xte∩s|o∩介绍

厂■■β『■■=■■■

本节中我们就来了解下Scrapy中ExtenSion的用法。

Scmpy提供了-个Extensjon机制,可以让我们添加和扩展_些自定义的功能。利用Extension我 们可以注册—些处理方法并监听Scrapy运行过程中的各个信号,做到在发生某个事件时执行我们自定 义的方法°

Scmpy已经内置了_些Extension,如LogStats这个Extension用于记录-些基本的爬取信息,比 如爬取的页面数量、提取的Item数量等,CoreStatS这个Extension用于统计爬取过程中的核心统计信

和DownloaderMlddleware` SpiderMiddlewaIE以及ItEmPipeline-样, Extension也是通过

semngspy中的配置来控制是否被启用的,是通过EXTENSION这个配置项来实现的,例如: [X丫[‖5I删5≡{

!scmpy.exte∩5io∩5°core5tat5·〔ore5tat50 ; 5"’ 5〔mpy·exte∩5jo∩5·te1∩et.丁e1∩et〔o∩5o1e『 : 5o1’

』■Ⅲ■」■∏」』■■∏|‖{□】]||」』■』■‖

息,如开始爬取时间`爬取结束时间等°

q



另外我们也可以实现自定义的Extension,实现过程其实非常简单,主要分为两步:

的5ig∩a15对象将ScIapy的各个信号和已经定义的处理方法关联起来° 接下来我们就用_个实例来演示一下Extensjon的实现过程°

2准备工作

本节我们来尝试利用Extension实现爬取事件的消息通知°在爬取开始时、爬取到数据时`爬取

开始本节的学习之前,请确保已经成功安装好了Scrapy框架并对Scmpy有_定的了解°本节的 实例是以152节的内容为基础进行编写的,所以请确保已经理解了15.2节的全部内容并准备好了152 节的代码°

P1P3i门5ta11+1a5kreq0e5t51OgUrU

‖‖∏|

另外本节我们需要用到Flask来搭建一个简易的测试服务器,也需要利用requests来实现HTTP请 求的发送,因此需要安装好Flask、requests和loguru这3个库’使用pjp3安装即可:



{||‖

结束时通知指定的服务器’将这些事件和对应的数据通过HTTP请求发送给服务器。



■司□■纠■引□■■■Ⅵ』勺|』■司‖‖』■|

□定义十roⅧcra"1er类方法’其第_个参数是〔15类对象’第二个参数是craw1er。利用crawler



·

□实现_个Python类,然后实现对应的处理方法,如实现一个5pjder=ope∩ed方法用于处理 Spjder开始爬取时执行的操作,可以接收一个spjder参数并对其进行操作。

‖‖‖

通过如上配置我们就开启了CoreStats和TUnetConso1e这两个Extension°

0



l5.8

Extension的使用

793

3.实战

为了方便验证’这里可以用Flask定义—=个轻量级的服务器,用于接收POST请求并输出接收到 的事件和数据, serveⅢpy的代码如下: +rO们+1a5促mPOrt「1a5促’ reqUeSt’ j5O∩j+y +roⅦ1o8uru加port1ogger

己pp≡「1己5假(—∩a"e )

0app.route(!/门otjfγ! ’ Ⅷethod5≡[‖p05「0]) de千reCeiγe(); postˉdata≡request.get=j5o∩〈) eγe∩t=po$t-data。get(0eve∩t0)

data=po5tˉd日ta.get(,d3ta,) 1ogger.debl』g(「,re〔ejvedeve∩t{eγe∩t}’ data{data}!) retur∩j5o∩1+y(5tatu5=5uc〔e550) j十

∩己‖∏e

==

Ⅷa1∩

°

app。Iu∩(debug=TIue’ ∩o5t≡‖0.0.0.00 ’ port=5OO0)

然后运行它: pyt∩o∩]5erγer·py

这样Flask服务器就在本地5000端口上运行起来了°

接下来我们基于l52节的代码’在scrapytutonal文件夹下新建—个extensjonsPy文件’先实现几 个对应的事件处理方法: 1ⅦpoItreque5t5

‖0∏「I〔∧丁I0‖0肌≡ ‖http://1oc己1ho5t:5oo/∩otj+y!

c1a55‖ot1十i〔atjo∩[xte∩sjo∩(obje〔t): de十5piderˉope∩ed(se1十’ 5pideI〉; reqUeSt5.PO5t(‖0「I「I〔∧丁10‖0肌’ j5O∩={ eve∩t! : ,SpI0[∩0p【‖[0‖’

!data! : {』5pjder∩a贮: 5pider.∩a「∏e} })

de十5P1der〔1o5ed(5e1十’ 5pjder): reql』e5t5.po5t(‖0丁I「Iα『IO‖0R[’ j5o∩={ eve∩t‖ 8 05pI0[Rop[‖[00 ’

!dat日! : {‖5pider∩a|∏e‖ ; 5pjder°∩a爬} })

de千jte们5〔Iaped(5e1「′ 1te们’ 5pjdeI): reque5t5。po5t(‖0丁I「I〔A丁IO‖0Rl’ j5o∩≡{ 0eγe∩t‖ : ‖I丁[‖5〔R∧p[D0 ’

|data』: {05pider∩日爬‖: 5pjder.∩a爬’‖jte刚0 : di〔t(1te刚)} })

试里我们定义了—个‖ot1+1cat1o∩[xte∩5jo∩类,然后实现了3个方法’ 5p1deIˉope∩ed、 5pjder

c1o5ed和jte们5〔raped’分别对应爬取开始、爬取结束和爬取到Item的处理°接着调用了requests向 刚才我们搭建的HTTP服务器发送了对应的事件’其中包含两个字段:一个是eγe∩t’代表事件的名 称;另一个是data’代表_些附加数据,如Spider的名称、Item的具体内容等。



↑5 L

但仅仅这么定义其实还不够,现在启用这个Extension其实没有任何效果的,我们还需要将这些 方法和对应的Scrapy信号关联起来,再在‖oti十jcat1o∩[xte∩5jo∩类中添加如下类方法: @〔1a55『∏et‖od

de++rO刚〔ra"1er(〔15)〔m"1er): eXt≡〔15()

〔mw1eL518∩a15。co∩∩ect(ext°5pjder≡ope∩ed’ sig门a1霉5ig∩己15.5piderˉope∩ed)



■】■」□■■■『||』□□■Ⅵ」曰|』■

第15章Scrapy框架的使用

794

〔Iaw1er。51g∏a15。〔o∩∩e〔t(e×t.5pider=〔1o5ed」 5jg∩a1=已jg∩己15.spjder_〔1o5ed) 〔mw1er.5jg∩a15.co∩∩e〔t(ext.jte们5〔mped’ 5jg∩己1≡5ig∩315。ite∏scraped〉 Ietur∩ext

于roⅦ5〔rapyi呻ort5ig∩a15

Scrapy运行过程中全局的〔raw1er对象。

〔ra"1er对象里有一个子对象叫作51g∩315,通过调用5jg∩a15对象的cO∩∩e〔t方法,我们可以将 Scrapy运行过程中的某个信号和我们自定义的处理方法关联起来°这样在某个事件发生的时候’被关



司||』叫』□司刽|‖|

其中’{ro"〔ra"1er是_个类方法’第一个参数就是〔15类对象’第二个参数cra"1er代表了

□Ⅵ‖|·

这里我们用到了Scrapy中的5jg∩a15对象,所以还需要额外导人一下:

联的处理方法就会被调用。比如这里,co∩∩e〔t方法第_个参数我们传人e×t.5pjder—ope∩ed这个对象, 而ext是由〔15类对象初始化的,所以ext.5pjder-ope∩ed就代表我们在‖otj+jcatio∩[xte∩5jo∩类中

定义的5pjder-ope∩ed方法。co∩∩e〔t方法的第二个参数我们传人了5jg∩a15ˉ5p1der-ope∩ed这个对象, 这就指定了5pjder_ope∩ed方法可以被sp1derˉope∩ed信号触发°这样在Spider开始运行的时候’ 会产生5ig∩a15.5pjderˉope∩ed信号,‖oti十jcatjo∩[xte∩51o∩类中定义的5piderˉope∩ed方法就会被调



用了。

完成如上定义之后’我们还需要开启这个Extensjon,在se仇jngs.py中添加如下内容即可: !5〔raPytutoIj日1°exte∩5jo∩5.‖oti十1〔atjo∩[xte∩5jo∩|: 1OO′

■■·■■】‖

[X丁[‖5I删S={

} ■■《

我们成功启用了‖ot1十1〔at1o∩[xte∩51o∩这个Extension°

下面我们来运行_下quOte5:

尸■

s〔raPy〔raw1quote5

这时候爬取结果和l52节的内容大致_样,不同的是日志中多了类似如下的几行: ■







202Oˉ11ˉ2601:』6:0O [ur11ib3.〔o∩∩e〔t1o∩poo1] 0[B06: 5tartj∩g∩ew‖丁丁p〔o∩∩ectio∩ (1): 1o〔a1ho5t:5O0O 202Oˉ11ˉ26O1:46:00[u【11ib3.co∩∩e〔tjo∩poo1]0[806: ∩ttp://1o〔a1ho5t:5O0O"p05丁/∩ot1+y‖∏p/1·10』 2"26 ‖ ●





日‖

有了这样的日志,说明成功调用了reqUeStS的PO5t方法完成了对服务器的请求。 这时候我们回到Flask服务器’看_下控制台的输出结果: | |∏a1∩ :re〔ejγe:12ˉre〔ejγedeve∩t5pI0[R0p[‖[0’data{{5pjder∩3爬:

quote5!}

202oˉ11ˉ2601:46:02.888 | O[B0C 『∏31∩ ;re〔e1ve:12ˉre〔e1γedeγe∩tI丁["S〔R∧p[0’d己ta{{5pjdeI∩a爬自 quote50 ’ |jteⅦ0 : {0te×t0 : "“∧per5o∩5己per5o∩, ∩o们atteIhow5Ⅶa11.’’"’ !aut∩or‖ : |Dr. 5eu550 』 |tag5 : [|j∩5pjIatjo∩日10]}} 127·0.0.1 ˉ ˉ [26/〕u1/2021o1:46:o2]"p05『/∩otj+y‖∏p/1°1" 2O0ˉ 202oˉ11ˉ2601:46:02.891| 0[80C Ⅷ日j∩ :Ie〔ejve;12 ˉ recejγedeve∩tI丁[‖5〔RAp[D′ data{‖5pider∩己爬: quote5』’ 』ite叮: {!text0 : !“ …aⅧ1∩d∩eed5book5a535Ⅳord∩eed5日whetsto…!’ !a0t∩or‖ :℃eorgeR。【. 例arti∩|’ 0t日g5|: [book50 』 !m∩d! ]}} 127·0.o·1_[26/〕u1/2o21o1:46:02]"p05「/∩ot1+y‖丁丁p/1.1" 2ooˉ R020ˉ11ˉ2601:』6:0乙897 | 0[80C | Ⅶai∩ :re〔e1γe:12ˉre〔eivedeγe∩t5pI0[R0p[‖[0’data{‖5pjder∩a爬: q0ote50}

口‖纠□‖·

2O2Oˉ11ˉ26O1:45:57.829 |0[8〔」C

q

可以看到F‖ask服务器成功接收到了各个事件(SPIDEROPENED` ITEMSCRAPED、SPIDER

OPENED)并输出了对应的数据’这说明在Scrapy爬取过程中,成功调用了Extenslon并在适当的时 机将数据发送到服务器了,验证成功!

我们通过一个自定义的Extension’成功实现了Scrapy爬取过程中和远程服务器的通信’远程服











l5.9

Scrapy对接Selenium

795

务器接收到这些事件之后就可以对事件和数据做进-步的处理了。 4.总结

当然,本节的内容仅仅是一个Extension的样例°通过本节的内容我们体会到了Extension强大 又灵活的功能’以后我们想实现一些自定义的功能可以借助于Extension来实现了° 另外Scrapy中已经内置了许多Extensjon’实现了日志统计`内存用量统计、邮件通知等各种功 能’可以参考官方文档的说明: https:〃docs.scrapyo!g/en/latesUtopics/extensjonshtml

}}|}|}}

另外也可以参考其源码实现来学习更详细的Extensjon的实现流程°

本节代码参见: https:〃githuhcom/Python3WebSpjder/ScmpyExtensionDemo。

↑5.9 Sc『apy对接Se|e∩|um 之前我们都是使用Scrapy中的Request对象来发起请求的,其实这个Request发起的请求和 【℃quests是类似的’均是直接模拟HTTP请求°因此’如果—个网站的内容是由JavaSc"pt谊染而成的, 那么直接利用Scrapy的Request请求对应的URL是无法进行抓取的° 前面我们也讲到了’应对JavaSc∏pt喧染而成的网站主要有两种方式:-种是分析Ajax请求’找 到其对应的接口抓取,用Scmpy同样可以实现;另—种是直接用Selenium、Splash`Pyppeteer等模拟 测览器进行抓取’在这种情况下,我们不需要关心页面后台发生的请求,也不需要分析喧染过程,关 心页面的最终结果即可,可见即可爬°

所以,如果我们能够在Scrapy中实现Selenium的对接,就可以实现JavaSc∏pt喧染页面的爬取了, 本节我们就来了解一下Scmpy对接Selenjum的原理和实现°

「『||广卜|‖伊「『『|■厂卜‖卜口『卜[卜‖『|厂「′|■『■『》『Ⅲ「■「|卜}■『|卜匹尸『「‖●『【【■●ˉ【‖巴■『‖■『【『‖β『‖‖巴仆‖|[卜}|||口二====□====ˉ=

↑.本节目标

本节中我们来了解一下Scrapy框架如何通过对接Selenlum来实现JavaScript喧染页面的爬取, 爬取的目标网站为https://spa5.scTapecenter/’这是一个图书网站,展示了多本图书的信息’如图l5ˉl4 所示。 ◎

+争◎

▲★■ ▲●!

▲=望≡~ =■比→■

■~

回s.°p· 一~_

鞍. {



■ 刚瞪:闺ˉ慧…Ⅷ 圈瓣{重→ ■

W◎郝■ R」.p…

h

!

〗■■ ■ …儿 …月

b

■L■





■… 戳

』■■■■■■

■■

庸翻…翌累萝膘…T瓣蜀(=

匹j-■』 ;`……-■而■■■■= 图l5ˉ14图书网站

蜘们■(≡·

…■■…

↑5 k

□■·|‖|‖』■■|■』』□■』■■』■』‖‖‖‖□Ⅵ』白‖|

第15章Scrapy框架的使用

796

点击任意一个图书条目即可进人对应的详情页面,如图l5ˉ15所示。 ●_‖

≡秆



_

审◇

×

—★

回…‖肆 ÷+◎

■立罕=r…烂咎同●…

≡-

7.2法老的宠纪终结■《上下册》

…■●●●■ …ˉ“."元 佃■ˉ呸由△疆屯… 炽=历{m〗2α名0

糊■卧.江Z文宁矾… ■盐. 302 …Pww…7M3

■『‖||叫』■■』‖々]■■‖』』■|』■}|■■||■】‖■‖■|■■勺□●■°■|·■|‖‖‖

回射…

‖‖

b

图l5ˉl5图书的详情页面

本节开始之前请确保安装好Scrapy框架’另外还需要安装好Selenium库,这次Selenium库对应

{‖

2.准备工作

」月帅‖

图l5ˉl5所示的信息是经过Ajax获取并通过JavaSc∏pt喧染出来的,我们要实现的就是使用 Scrapy对接Selenium’对图书详情进行爬取,包括名称、评分、标签等。

的测览器依然还是Chrome’请确保已经安装好了Chrome测览器并配置好了ChromeDrjver,具体的安 准备工作完成之后,我们就可以开始本节的学习了。 3.对接原理

‖‖■■●□`

装过程可以参考: https://setupscrapecenter/se‖enium°

在实现之前,我们需要先了解Scrapy如何对接Selenjum’即对接的原理是什么° 我们已经了解了DownloaderMjddlewaIc的用法’非常简单’实现pIo〔e55ˉreque5t`pro〔e55ˉ工e5po∩5e、

其中有一个知识点我们可以利用。在pro〔e55ˉreque5t方法中’当返回为Response对象时’更低 优先级的DownloaderMjddleware的pro〔e55ˉreque5t和pro〔e55=except1o∩方法不会被继续调用’每 个DownloaderMiddleware的pro〔es5ˉre5po∩5e方法转而被依次调用°调用完之后,直接将Response对 象发送给Spider来处理°

所以,原理其实就很清楚了’我们可以自定义_个DownloaderMiddleware并实现pro〔e55ˉreque5t

{』√

在l5.4节中’我们其实已经演示了这个过程的实现和最终效果’在pro〔e55ˉreque5t方法中直接 返回了_个‖t"1Re5po∩5e对象并被Spider接收`处理了°

‖·

那也就是说,如果我们实现_个Down‖oaderMiddleware’在pro〔e55ˉreque5t方法中直接返回个Response对象,那么pro〔e55-reque5t所接收的Request对象就不会再传给Spider处理了,而是经 由pro〔e55_re5po∩5e方法处理后交给Spider’ Spider直接解析Response中的结果°

』 ‖ ‖ ‖ 〗 ‖ ■ · ‖ ‖ ‖ ■

proce55—exceptjo∩中的任意一个方法即可’同时不同方法的返回值不同’其产生的效果也不同°

方法’在proce55ˉrequest中,我们可以直接获取Request对象的URL’然后在pro〔e55ˉreque5t方法









15.9



Scrapy对接Selenium

797

p





卜 }



中完成使用Selenium请求URL的过程,获取JavaScript喧染后的HTML代码’最后把HTML代码构

造为‖t刚1Re5po∩se返回即可°这样‖t阳1Re5Po∩5e就会被传给Spider,Spider拿到的结果就是JavaScrjpt 喧染后的结果了。 4对接实战

▲■厉尸卜|佃‖~尸「‖尸》凸尸卜||■尸卜巴■「■巴■厂◆〗‖|巳尸》■∏卜■■∏尸●■■■【「●【卜●【|●厂卜『「匡厂‖‖■∏|▲■「◆■‖凸■■巴■『「止尸『|仁「【广|匹尸匡『『‖β‖β■『‖′[■尸||■【■■■〗『●【■「‖′■■■■■‖『八伏匹厂‖「[·■【‖〗』■■■尸「|‖△■【『【□【■ˉ■■■■□口{睡。圃·。。’匹ˉ

首先新建项目,名为5〔rapy5e1e∩juⅧde们o’命令如下所示: /

5crapy3tartproject5cr己py5e1e∩iM网em



然后进人项目目录’新建一个Spider,命令如下所示: 5cr己py8e∩5pjderboo促5p己5。5〔mpe·〔e∩ter

这次我们爬取的是书籍信息’包括标题、评分、标签等信息°首先定义Item对象,名为8oo《Ite‖, 代码如下所示: +ro团5crapy·iteⅦj‖portIte‖』「je1d 〔1a55BOo代It印(It酮〉: ∩己↑∏e=「ie1d() tag5≡「ie1d() 5〔ore=「ie1d() COγer=「je1d() pri〔e≡「ie1d()

这里我们定义了5个「je1d,分别代表书名、标签、评分、封面和价格’我们要爬取的结果会被 赋值为_个个8oo代Ite"对象°

接着我们来实现一下主要的爬取逻辑’先定义初始的爬取请求’使用5tartˉreque5t5方法定义 即可: 千r咖5crapy1呻ortReque5t’5pideI

〔1a558oo低5pider(5p1der〉: ∩a爬= 0boo仅0

a11…ddma1∩5≡ [‘5p己5.scrape.〔e∩ter|] ba5e l』r1= 0∩ttp5://5p己5·5Crape.Ce∩ter0

de十5tartˉreque5t5(5e1千): 5tartur1=「!{5e1十.b己5eˉur1}/page/10 yje1dReque5t(5tart=ur1’〔a11b日〔R≡5e1f.par5e=i∩dex)

这里我们就构造了列表页第_页的URL’然后将其构造为Request对象并返回了’.也就是说最开 始爬取第一页的内容,爬取的结果会回调par5eˉi∩dex方法°

那么parseˉj∩dex方法自然就要实现列表页的解析’得到详情页的一个个URL’与此同时还要解 析下—页列表页的URL’逻辑比较清晰,代码实现如下: i呻ortre

de+paI5ei∩dex(5e1千’ re5po∩5e): iteⅧ5=respo∩5e.c55(’.it鄙!) 十orjte们i∩1t印58

hre千≡it酮。C55(’.topa88attI(hre十)!)·eXtraCt+jⅢ5t() det己i1ur1=re5po∩se·ur1joj∩(hre「)

yie1dReque5t(detai1ur1』〔a11bac促=5e1千.par5edetai1’ priority二2) ∏m〔‖=re°5ear〔∩(r』pa8e/(\d+)|’ re5po∩5e.ur1) 1千∩ot晌t〔∩: retur∏

pa8e=i∩t(爬t〔h.8rouP(1))+1

∩extur1二+,{5e1千.ba5eur1}/p■8e/{pa8e}|

y1e1dReque5t(∩ext0r1’〔a11bac卜se1f.pa【sei∩dex)



[15

||{

第15章Scrapy框架的使用

798

』‖(

在par5e1∩de×方法中实现了两部分逻辑:第一部分逻辑是解析每本书对应的详情页URL,然后 构造新的Request并返回’将回调方法设置为paI5edetai1,并设置优先级为2;另-部分逻辑就是 获取当前列表页的页码’然后将其加l构造下一页的URL’构造新的Request并返回’将回调方法设 置为par5ej∩de×。

「匹■■Ⅲ

最后的逻辑就是par5edetaj1方法,即解析详情页提取最终结果的逻辑°我们需要在这个方法里 实现提取书名、标签、评分`封面和价格的任务’然后构造8oo代IteⅦ并返回,代码实现如下:



de「par5edetai1(5e1千’ re5po∩5e):

∩a|‖e=re5po∩5e·cs5(』.∩a∏`e::text,).extract+irst() tags=re5po∩5e.c55(0 °tag5butto∩5pa∩; $text|).e×tm〔t() 5core=reSpo∩5e.〔55(‖.s〔ore::text‖).extra〔t「jr5t〈) pri〔e=re5pO∩se°〔55(‖。Price5pa∩::text′)。extractfir5t() 〔oγer≡re5po∩5e.〔s5〈』.〔over;:己ttr(sr〔)0〉.e×tmct千ir5t() tag5≡ [tag.5trip()千ortagi∩tag5] j十tag5e15e []

这样-来’每爬取_个详情页’就会生成-个Boo代Ite"对象并返回°

我们已经完成了Spider的基本逻辑实现,但运行这个Spjder是得不到任何爬取内容的°因为原网 站的页面信息是经由JavaSc而pt喧染出来的’所以单纯使用Scrapy的Request得到的ResponseBody并 不是JavaSc∏pt痘染后的HTML代码°为了使得ResponseBody的结果都是JavaSc∏pt喧染后的HTML 我们需要定义_个DownloaderMiddleware并在pro〔es5ˉreque5t方法里实现Selenium的爬取, 相关代码如下: 十IO川5〔Iapy.∩ttpj∏pOIt什m1Re5po∩5e +ro∏〗 5e1e∩ju们1∏port眶bdIiγeI 1∩pOrtti爬



』】■■·]■■■〗‖‖‖

代码’我们需要像上文所说的’把Selenium对接进来°

|‖

γie1dite∏‖

■匹

5〔oIe≡5core.5tmp() i十s〔oIee15e‖o∩e

1te们=8oo代It咖(∩a「∏e≡∩a|∏e’ tag5=tag5’ score=5〔ore’ pIj〔e=pri〔e’〔oγer=〔oγer)



| q

de千pro〔e55-reque5t(5e1「’ reque5t’ 5pider)$

」□

〔1a555e1e∩ju刷jdd1印are(obje〔t〉;

ur1=Ieque5t.ur1 bro"5er≡"ebdrjγer.〔∩ro∩e()

browSer.get(uI1) t1爬。51eep(5) btp1=bI叫ser·pa8e→5our〔e bro"5eⅢ.〔1o5e() retur∩毗‖1Respo∩5e(l』I1=reque5t。ur1’ body=hm1’ reque5t=reque5t’ e∩Codi∩g=0ut+ˉ80 』 5tatl』5=2OO)

这里完成了最基本的逻辑实现°在PrOCe55-reque5t方法中’我们首先获取了正在爬取的页面









| ■

URL;然后开启Chrome测览器请求这个URL,简单地加个固定的等待时间’获取最终的HTML代码;

喧染后的结果了。

接下来我们还需要在settings.py里面做一些设置,开启这个DownloaderMiddleware’同时禁用 R08O丁5「ⅪO8[γ=「a15e

刚肌0A0[∩佣I卯[[肌R[5≡ { ’s〔raPy5e1e∩皿网e∏℃°mdd1e"aIe5°5e1e"1u耐idd1eware0 : 543’ }

‖』■■司‖』■■已

robots°txt:



{(

接着使用HTML代码构造‖tⅧ1Re5po∩5e并返回°由于返回的是付tⅦ1Re5po∩5e对象’所以原本的Request 就会被忽略了’这个‖tⅦ1Re5po∩5e对象会被发送给Spider来解析,所以Response拿到的就是Selenjum



■■■■■■■■■■『■「【【「■□『【■厅‖■「|卜|β∏‖【β‖‖卜广‖|户厂‖|‖|▲■尸『‖|■「卜|||口|卜

l5.9 ScIapy对接Selenium

799

这样就成功开启了SelenjumMiddleware’每次爬取Scmpy都会使用Selenium来喧染页面了° 然后我们运行_下Spjder,命令如下: scrapy〔m"1boo促

在运行过程中’Chrome测览器就弹出来了,被爬取页面的URL会被测览器谊染出来,最终Spider 得到的Response就是JavaSc∏pt喧染后的结果了。 同时可以看到,控制台显示的运行结果如下:

户|》′}|■『「|■「■|■∏●尸卜●「●β}▲=■厂‖‖■「‖|■■『「|■『’■「●■卜》□尸|β|■尸[|尸|●「|血■「【尸|[广口■厂■[厂△【『}■厂|『尸》|■厂『■「||△■■■尸‖尸『『|■巴「『■■「‖■■「}■「|

2O2Oˉ09ˉ1322:42:O9[5crapy.exte∩5io∩5。1og5tat5] I‖「0:〔m"1edmPage5〈at21P己ge5/m∩)’5〔rapedOjte∏‖5 (at0it印5/m∩)

2020ˉO9ˉ1322:42:1O[s〔r日py.〔or巳s〔rapeI]0〔8l」C:5cmped千IoⅧ<20O们ttp5://5pa5.5〔raPe.〔e∏ter/detaj1/16926』8〉 {|〔over0 : 』http5://j吧9.douba∩jo.〔m/γjew/5ubje〔t/1/Pub1j〔/59018o34.jPg‖’ 0∩a爬0 : !一个人的村庄‖’

!Pri〔e0 ; !28。卯元0’ {5〔ore‖: !8·90 ’

’ta85, : [′刘光程0 ’ 0依丈|’ 0乡土』’ !一个人的村庄′’ °仗丈随皂|]} 202OˉO9ˉ1322:42:1O[5〔rapy,coIe.5〔mper]D[B06:5〔mped十m∏〈2OO∩ttp5://5PaS.5〔I己Pe.ce∩ter/detai1/1055976〉 {‖〔over! : ‖https://i吧3.douba∩io.〔咖/γiew/5ubje〔t/1/P仙b1ic/52157331。jPg‖’ ‖∩3爬‖: 0慧▲剑(上下) 0 ’

0pIi〔e‖: |∑〕。卯元‖’ 05〔oIe0 : 07·20 ’

!t己85! : [,金启|’ 』式侠!’‖小说0’|碧▲剑!′ 0式侠′』、说,]}

这样我们就成功对接Selenjum实现了JavaSc∏pt喧染页面的爬取。 5.对接优化

细心的读者也许会发现,我们刚才实现的5e1e∩1u州1dd1e"are的功能太粗糙了’简单列举几点° □ChIomc初始化的时候没有指定任何参数’比如head1e55、Proxy等,而且没有把参数可配置化° □没有实现异常处理’比如出现丁i爬[x〔eptjO∩后如何进行重试° □加载过程简单指定了固定的等待时间’没有设置等待某_特定节点° □没有设置Cookie、执行JavaSc∏pt、截图等—系列扩展功能°

□整个爬取过程变成了阻塞式爬取,同_时刻只有一个页面能被爬取’爬取效率大大降低。

优化过程我就不—一列举了’我写了一个python包,对以上的5e1e∩1{m‖jdd1副are做了一些优化: □Chrome的初始化参数可配置’可以通过全局se仗ings配置或Request对象配置° □实现了异常处理’出现了加载异常会按照Scrapy的重试逻辑进行重试° □加载过程可以指定特定节点进行等待,节点加载出来之后立即继续向下执行°

□增加了设置Cookie、执行JavaSc前pt、截图、代理设置等一系列功能并将参数可配置化° □将爬取过程改为非阻塞式’同-时刻支持多个测览器同时加载,并可通过〔0‖〔0RR[‖丁R[α」[5丁5 控制°

□增加了5e1e∩juⅦReque5t’定义Request更加方便而且支持多个扩展参数° □增加了WebD∏ver反屏蔽功能,将测览器伪装成正常的测览器防止被检测。 这个包叫作GeTapySelenium’安装方式如下: pip3j∩5ta11geraPyˉ5e1e∩iU‖

安装之后我们只需要启用对应的DownloaderMiddleware并改写Request为5e1e∩1Ⅷ尺eque5t 即可: 刚‖l0AD[R付I卯l["∧R[5≡{

|geIapyˉ5e1e∩juⅧ。dow∩1oadermdd1印aIe5。Se1e∩m湖1dd1印are0 : 5』3′ }

] 』

800

第15章Scrapy框架的使用

另外我们还可以控制爬取时并发的Request数量’比如: 〔删〔0R只[‖『【[q」[5T5=6

这甲我们将并发量修改为了6’这样在爬取过程中就会同时使用Chrome喧染6个页面了,如果 你的电脑性能比较不错的话,可以将这个数字调得更大_些。

在Spider中,我们还需要修改Reque5t为5e1e∩juⅦReque5t,同时还可以增加一些其他的配置,比 如通过"ajt+Or来等待某一特定节点加载出来’比如原来的: y1e1dReque5t(5tartαI1’ca11b己〔低≡5e1千。par5ei∩dex)

就可以修改为: yje1d5e1e∩i(』"Request(5taIt l」r1’ ca11ba〔代≡5e1十.par5ei∩dex’"己it+or=0 .ite" .∩a眶!)

其他两处进行同样的修改即可°重新运行Spider: 5CIapyCmW1boO长

可以看到这次测览器没有再弹出来了,这是因为在默认情况下’GeIapySelenium启用了Chrome的 比ad1e5S模式,同时可以看到控制台也有对应的输出结果’爬取速度相比之前有成倍提高’运行结果 如下: 2020ˉ09ˉ1〕23:』4:05[gerapy°se1e∩jⅧ]D[8{L: 5e1e∩iⅧ爬ta{‖"ajt+or|: ‖ 。ite们.∩a爬|’ |5〔rjpt』8‖o∩e’ ‖51eep : ‖o∩e’ pro×y! : ‖o∩e’ prete∩d′ $ ‖o∩e’ 0ti爬out|: ‖o∩e’ 5〔ree∩5hot|: ‖o∩e} 2O2OˉO9ˉ1323:“:OS[s〔rapy.core.e∩gi∩e]0[80C:〔ra"1ed(2OO)〈C[『http6://5pa5.5〔rape.ce∩ter/detai1/1851672〉 (re十erer: ‖ttp5://spa5.5〔rape。〔e∩teI/page/2) 2OⅡ0ˉ09ˉ1323:q4:O5[5〔mpy.〔ore.5〔raper]0〔8l」C:5cmped十ro『∏<∑"∩ttp5://5pa5.5crape.ce∩ter/deta11/1927763) {0〔oγer! : ,http5://j吧3.douba∩1o.〔oWγiew/5ubje〔t/1/pub1i〔/59128381.jpg! ’ ∩a‖‖e| : !仔迅家庭家族和当年绍兴氏俗0 ’

0pIiCe0 : 018.卯元!’ 05〔Ore0 : ‖o∩e’

‖tag5‖ : [ 0仔迅‖’ !人物传记与研宽』’ 0绍兴′’ ‖传记‖’ |仔迅周作人胡迫|]} 2O∑OˉO9ˉ13∑〕:叫:05[gempy.5e1e∩ju肌]0[8凹:5e1e∩juⅦ爬ta{0wait+oI0 : 0 .jt印.∩己眠0 似 ‖5crjpt‖ :‖o∩e’ ′51eep : ‖o∩e’ proxy! 8 ‖o∩e’ ‘pIete∩d|: ‖o"e’‖ti爬out0 8 ‖o∩e’ |scree∩5∩ot|8 ‖o∩e} 2020ˉ09ˉ132〕:44:05[5〔rapy.〔ore.e∩gi∩e]D[8lL:〔ra"1ed(2")〈C[『http5://sp日5.5〔r日pe。ce∩ter/detai1/169o7驯〉 (re+eIer: http5://5pa5.5〔r己pe.〔e∩teI/page/2)

这样我们就使用GerapySelenjum提供的DownloaderMiddleware实现了Scmpy与Selenjum的对 接,非常方便。我们也不需要再去自定义Down‖oaderMjddleware了,同时爬取效率有成倍提升,实 现了参数可配冒化°

另外,GempySelenjum还提供了很多其他实用配置° ●关闭HeadleSS模式

将C[RApγ5〔〔[‖I0‖‖[∧0[[55设置为「a15e即可, settingspy增加如下代码: C[R∧pγ5[l[‖I删"[汕[[55≡丁rue

●忽咯HTTPS铅误

将C[R∧pγ5[[[‖I0"I6‖0R[‖∏P5[RROR5设置为True即可’ settings.py增加如下代码: C[R∧pγ5[[[‖I叫IC‖0R[‖『丫p5[∩R0【5=丁Iue

●开启WebDriver反ˉ屏蔽功能

该功能默认是开启的’就是将当前测览器伪装成正常的测览器’隐藏WebD∏ver的_些特征’如 需关闭’则给semngSpy增加如下代码: C[R∧pγ5[[[‖I0"pR[『[‖0=「a15e





l5』0 Scrapy对接Splash

801

●设Ⅲ加载超时时间

将C[R∧pγ5[[[‖I0‖DO‖‖[0∧D∏‖[00丁设置为默认的秒数,默认等待30秒,例如设置超时60秒,

settings.py增加如下代码: C[R∧pγ5[[[‖I|」"刚肌0∧D丁I"[α」丁≡6O

●设五代理

设置代理可以借助于5e1e∩1u"Reque5t,设置proxy参数即可,例如: yie1d5e1e∩iu‖Reque5t(5tartˉur1’〔a11bac低=5e1十.par5e-j∩dex’"ajt千oI=|。ite阳.∩a爬’proxy=|127.o.0°1:789o0)

更多用法可以直接参考Ge【apySe‖enium的GjtHUb仓库地址:https:〃gjthuhcom/Ge田py/GeTaWSelenjum。 6.总结

0

本节我们介绍了Scrapy和Selenium的对接解决方案’有了这个方案, Scrapy爬取JavaScnpt喧染 的页面不再是难事了°



本节代码参见: https://gjthuhcom/Python3WebSpider/ScrapySeleniumDemo° ■『『|=■【『■尸卜

↑5.↑0 Sc「apy对接Sp|as∩ 上一节我们了解了Scrapy对接Selenium的原理和实现流程’当然这是—种实现Scrapy爬取

JavaSc前pt痘染页面的方案,但方案不止这_种,利用Splash和Pyppeteer同样可以实现°



本节我们来了解-下ScIapy对接Sp‖ash爬取JavaSc∏pt喧染页面的流程° ‖.准备工作





本节要爬取的目标网站和需求与上一节是一致的,在这里就不再展开介绍了°不同的是实现方案 由Selenium切换为了Splash,所以我们要实现Scrapy和Splash的对接°

要实现Scrapy和Splash的对接’我们需要借助于ScrapyˉSplash库,另外还需要一个可以正常使 用的Splash服务。

β

在开始本节的学习之前,请确保安装好Scrapy框架,另外请确保Sp‖ash已经正确安装并正常运

} p

行,另外我们还需要安装好ScrapyˉSplash库’具体的安装过程可以参考: https://setupscrape.ccnteⅣ scrapyˉsplash°

2.对接原理

△∩||匹尸·■||巴尸

Scrapy对接Splash和Selenjum的原理是不同的,上_节对接Selenjum是借助于Downloader

Middleware实现的’在DownloaderMiddleware里’我们实现了Chrome测览器喧染页面的过程,并构 卜|■■||仁||任‖|『「『「|△■■■■|∩卜『‖》■「|匹■尸『|■■厂|■■】■■■■则〔‖·【■■■■■■■□

造了‖t们1ReSpo∩5e返回给Spider°

而Splash本身就是_个JavaSc∏pt页面喧染服务,我们只需要将需要谊染页面的URL发送给 Splash就能得到对应的JavaSc∏pt痘染结果,而ScrapyˉSp‖ash则是提供了这个过程基本功能的封装, 比如Cookie的处理、URL的转换等. 下面我们来具体了解下它的用法。



|↑5 k

3.对接实战

首先新建一个项目,名为5〔rapy5p1a5‖de∩o,命令如下所示: 5〔rapystartpIoje〔t5〔r己pysp1a5Me∏℃



第l5章SCrapy框架的使用

802

进人项目,新建—个Splder,命令如下所示: 5〔mpyge∩5pjderboo低5pa5.5〔rape。ce∩ter

这样我们便创建了初始的Spider,然后创建一个同样的8oo促Ite川,代码如下: +ro∏s〔rapy。jte肌mportIteⅧD 「ie1d 〔1a55BOO促IteⅧ(IteⅧ): ∩aⅦe≡「je1d() tag5=「1e1d() score≡「1e1d() 〔over=「je1d() Pri〔e≡「ie1d()

接下来就需要进行ScrapyˉSplash相关的配置了’可以参考ScrapyˉSplash的配置说明:h呻s://githuh





|‖

com/scrapyˉplugins/scrapyˉsplash#configumtjon。



修改settings.py,配置5p[∧5‖0R〔。这里的Splash运行在本地,所以可以直接配置本地的地址: 5p[∧5‖0R[= ‖‖ttp://1oca1‖o5t:8O5O{

」■】·』■■】|{||■■‖‖·

如果Splash是在远程服务器运行的,那么此处就应该配置为远程的地址’例如我配置了_个Splash 集群’地址为https://splashscrapecenter,用户名和密码均为admin,则此处的配置应该是这样的: 5P[AS‖0R[= 0‖ttp5://Sp1a5h。5Crape.〔e∩ter|

另外还需要在Spider里面增加下面两个变量的定义以支持H∏PBasicAuthentication:

司‖□‖

〔1己55Book5pideI(5pider); httP-‖5er= 0己dm∩0 http-pas5= ‖adm∩0

接着配置几个Middlewa阳’代码如下所示: 刚肌0∧D[R"I卯[[‖∧R[5≡ { 5crapy=5p1a5h°5p1ash〔oo代ie5佣idd1eware0 : 723’ 5cmpγ-5p1a5∩.5p1a5h"jdd1eware,: 725’

5〔I己py.do切∩1oademidd1ew日re5.‖ttp〔o∏0pIe55jo∩°‖ttp〔咖pIe55jo∩∩idd1ewaIe0 ; 81o’

} 5pID[R‖ID0[["∧R[5={ 5〔r己py-5p1a5h.5p1a5hDedop1i〔ate∧rg5‖jdd1eⅦare0 : 1oo }





日《」□|‖·|」』■〗』■』】■∏‖‖·■|

这里配置了3个DownloaderMjddleware和一个SpjderMiddleware’这是ScrapyˉSplash的核心部 分°我们不再需要像对接Selenjum那样实现—个DownloadcrMiddleware, ScrapyˉSplash库都为我们 准备好了’直接配置即可°

还需要配置一个去重的类00p[「Il『[RαA55’代码如下所示: D‖」p[「I∏[【α∧55= 05crapy-5p1己5h。5p1a5h∧川are0upe「i1ter!

最后配置—个Cache存储‖『『p〔∧〔‖[5丁0R∧C[’代码如下所示: ‖∏p〔∧〔‖[5丁0R∧C[= 0scmpy-5p1a5h.5p1己5Mware「S〔ache5torage!

配置完成之后,我们就可以利用Splash来抓取页面了°我们可以直接生成—个5p1a5hReqoe5t对 象并传递相应的参数’Scrapy会将此请求转发给Splash, Splash对页面进行痘染加载’再将喧染结果 传递回来。此时Response的内容就是喧染完成的结果了’最后交给Spider解析即可。

{』

我们来看一个示例,代码如下所示: ·(

yie1d5p1a5hRequest(ur1’ 5e1+。par5e-re5u1t’ args={ wait! 目 0.5’ #辛待时间

}’







l5.l0 Scrapy对接Splash

803

e∩dpoj∩t=`re∩der·]5o∩! 』 #可选狼数, 5p1a5∩溢染终辅 5p1a5∩ur1=0〈ur1〉! ’

#可选牟数,反盐5P[∧5‖0R[



在这里构造了-个5p1a5hReque5t对象’前两个参数依然是请求的URL和回调函数,可以通过 arg5传递—些喧染参数,例如等待时间"a1t等,还可以根据e∩dpo1∩t参数指定喧染接口’更多参数 可以参考文档的说明: https://githuhcom/scrapyˉplugins/scrapyˉsplash#requests° 另外我们也可以生成Request对象’关于Splash的配置通过‖eta属性配置即可,代码如下: p

儿■厅‖■『『|止■「·『|||●「「|||〖■「『【】『‖【■【「|■[β||凸■■『|‖|【◆》‖|||■卜『 巴■「|||巴■「‖|卜▲口■『}|■■【■「|止■■■『|◆『{β||

β



γie1d5〔mpy°Reql』e5t(ur1’ 5e1千。paI5ere5u1t’爬t日≡{ !5p1aS‖! : { argS0 : { ‖hm10 : 1’

p∩g『 : 1』 }’ #以下为可选拳数

0e∩dpoi∩t0 : 0re∩deI.j5o∩|’ #可选拳数’5p1a5∩溢染终端’耿认为Ie∩deI.j5o∩ #可选牟数,ⅢA5p[∧5‖0R〔 05p1己Shur10 : ′<uI1>′’ #可选拳数,发送给5p1a5h溢染时侠设Ⅲ的|{eader5 ‖Sp1aSh∩e己der5‖: {}’

‖do∩t-pro〔e55-re5po∩5e|; 丁rue’#可选牟数’不处理Respo∩se’欧认是「a15e ,do∩t5e∩d∩e3der50 : 丁Iue’ #可选牟数,不发送‖eaders’旺认足「a15e



})

通过arg5来配置5p1a5hReque5t对象与通过阳eta来配置Request对象,两种方式达到的效果是相 同的°

我们可以首先定义—个Lua脚本,来实现页面加载,代码如下所示: 十u∩ctjo∩阳j∩(5P1a5h’ arg5) a55ert(5P1a5∩:gO(arg5。ur1)) assert(5p1aSh:wait(5)) retur∩{

ht‖1=5P1a5h:们m1()’ p∩g=5p1a5们;p∩g()’ ‖日I≡5p1a5∩:∩己I() } e∩d

逻辑非常简单,就是获取参数中的ur1属性并访问,然后等待5秒,最后把截图、HTML代码、 HAR信息返回°

我们将脚本放到Splash中运行’同时设置目标URL为https://spa5.scrape.center,如图l5ˉ16所示°



£unC℃1◎nm己jn(SP1aSh′ 己rgS)

苫门β|膛

巴∩‖■■》似『■甘■



∩巳∏de「丽e

门mS‖〃S…』“『a睡^鳃∩婶

】之〕《巳β了日’



■≡

己sSer七(SP1己sh8g◎(己rgS·ur1)》

己SSert(SP1己Sh8w己卫七( )) 工巳七urn {

htm1=sp1aSh8h七m1( )′

png=sp1己sh8png()′

h己r≡sp1己sh:har()′ 帜

} ·

会n□ ‖『

「·■尸’

图l5ˉl6 Splash配置

点击Renderme按钮’∏]以看到Splash中就出现了对应的喧染结果’如图l5ˉl7所示。

〖5

【|〈引‖‖‖‖

8叫

第l5章Scrapy框架的使用 色p‖■■仇U9,$

…O……·0℃内

坠=■甲■

9●0wC●c@d·

?vt咖T

」00》■》q■【●d罕0户0ⅦF

因…■国

|||||

回…≡==……■≡=≡…

望……■_≡…蹈……

=殿≡…熟……■=

_■===…====…·=_==

』卜=叮颧遗=酗件一

=…

≡稗

‖』■

≡…

……

乌一

……



…■≡…≡=~…已…………≈—=■

绅1山h舶■砷n■●0贮jmc p回■$ X匹卯(户弓p 1□ad江760》…

h■rγ…▲m一】~■ ■

…L尸》=D… p…压●xc

目登…

】■汀·由■』p巳

■■…r●≈■t

■…■一

■…

■…儿●·…飞

■…

■…

≡■=·←T●广▲▲硒

■■■』屯

■厅皿■b

■…■…1硒

■琶Ⅱ厂2…

P■吱1Q1



PF■C≡



汹…皿硒咖啊

唾巫】巳

凸·‖

…………………

悸………………… ……吟记呕

■O七…■

■…』可●

“】·5■0

0



m

3`4Ⅸ■

m2】ˉ9R0

毗▲7参了贝■

22°”“

图l5ˉl7谊染结果

(‖‖

5〔rjpt= 0|"0!



+ro‖s〔mpy1‖poIt5P1der 「ro∏scmpy-5p1a5hmPort5P1a5∩Reql』e5t



测试成功之后’我们只需要在Spider里用5p1a5hReque5t对接Lua脚本就好了,代码如下所示:

十l』∩CtjO∩Ⅶai∩(5P1己5h’ aIg5) as5ert(sp1a5h:go(aIg5.ur1)) 己55ert(5p1a5h:"ait(5)) retur∩5p1aSh巾tⅧ1() e∩d 0□"00

■■‖

〔1a55Boo伐5pider(5pjder): ∩己‖论= ‘book0

这里我们把Lua脚本定义成长字符串’通过5p1a5‖Reque5t的arg5来传递参数°另外’arg5参数 里有_个1u己5our〔e字段,它可以用于指定〔ua脚本内容°于是我们成功构造了—个5p1a5∩Reque5t’ 对接Sp‖ash的工作就完成了°

‖□

de十5tartˉIeque5t5(5e1+): 5tartur1≡十‖{5e1+·ba5eˉur1}/pa8e/1‖ y1e1d5p1a5h同eque5t(5tartur1′ 〔a11bac代=5e1+。par5e1∩dex’ arg5≡{‖1ua5ource : 5cript}’ e∩dpoj∩t≡!execute‖)

【■

a11oweddα∏ai∩5= [05pa5.scrape.ce∩ter! ] ba5eur1= 0们ttps://spa5.5〔rape。〔e∩ter0



实现其他方法也_样’我们需要把Request都按照要求修改为5p1a5hReque5t’相关代码改写如下: mportre

「Orjte们1∩1teⅧ5:

∩re十=jte∏·c55(‖.toPa: :attr(∩re千〉』).extm〔t「jr5t() det己11ur1=re5po∩5e。ur1joj∩(∩Ie+) yie1d5p1a5∩【eque5t(deta11皿1’〔a11back=5e1+。par5edetaj1’ prjority≡2’ args={』1u日sour〔e : 5〔ript}’ e∩dpo1∩t≡|execute|)

|{

de十par5ei∩dex(5e1于’ Ie5po∩5e)$ jte川5=Ie5卯∩5e.C55(‖.ite"!)



』 ■



l5』0

Scrapy对接Splash

805

∩at〔∩≡re.sear〔h(I0page/(\d+)‖」 re5po∩5e.ur1) i千∩ot帕tch: retqr∩ 卜□■■■‖|▲■「也『‖广‖■■『旦■『◆『}■『■‖『‖■厂卜『|巴厂【厂||‖‖■■尸「也ββ|}■

page=i∏t(阳tch。group(1))+1 ∩extuI1≡千‖{5e1千.ba5eur1}/page/{p日ge}0 yje1d5p1a5∩Reque5t(∩extuI1’ ca11b己c代≡5e1+。par5ei∩dex’ arg5≡{!1ua5ouI〔e : 5cript}’ e∩dpoj∩t=‖execute0) de千par5edet己j1(se1千’ Iespo∩se): ∩己们e=re5po∩5e.c55(|.∩日爬: ;text!).extractfjr5t() tag臼=re5po∏5e.cs5(! .ta85butto∩5pa∩: 8te×t0).extract() 5〔ore≡re5Po∩5e.c55(` .5〔ore::te×t|)。e×tmct十irst() PIj〔e=re5Po∩5e.〔55(‖.prj〔e5pa∩::text‖).extm〔t「jr5t() 〔oγer=re5Po∩5e.c55(↑.coγer: :日ttr(5rc)|)dextract+ir5t() ta85≡ [tag.5tIiP()十ortagj∩tag5] j+tag5e1Se [] 5Core=5〔Ore.5trjP() i十5〔oree15e‖o∩e ite刚=8oo低It印(∩aⅦe=∩a爬’ tag5=tag5’ s〔ore≡5〔ore’ prj〔e≡pri〔e’〔oγer=〔oγeI) yje1dite们

这里我们参考上一节的内容’将5e1e∩jⅧReq0e5t修改为了5p1a5∩Reque5t,同时增加了arg5参 数配置,其他的逻辑基本—致°这样_来, Scrapy和Splash的对接就全部完成了°



接下来’我们通过如下命令运行爬取:



5crapycraN1book



可以看到我们同样完成了结果的爬取’运行结果如下:























卜 }



202oˉ09ˉ1qO0:57;34[scrapy.core.5crapeI]D[80C:5〔mped+r咖〈2oohttp5://5pa5.5〔Iape.ce∩teI/detai1/3026840〉 {0〔over! : ‖http5://i吧9.do‖ba∩jo.〔o‖]/γj刨/5qbject/1/pub1jc/5299o855.jpg|’ ‖∩a『∏e0 : 0具理之剑1巫押第一守则(上) ! ’ !price` : |‖『$280↑’ |5〔ore‖ ; 08.o0》

|tags‖: [,计幻!’ !小说!’ |应幻!’ 0吴国!’ |『erryCood促i∩d,]} 2O2OˉO9ˉ140O:S7:35[5cmpy.〔ore·e∩gj∩e]D[Bl」C:〔r日"1ed(2OO)〈C[『http5://5p己5.5cmpe.ce∩ter/detai1/1160131O γiahttps://5p1a5h.〔uiqj∏g〔aj.c咖/exe〔ute> (re「erer: ‖o∩e) 20∑0ˉ09ˉMOo:57:35[5crapy。〔ore.e∩gi∩e]0[B‖」C;〔m刊1ed(ⅪoO)<C[丁https://5pa5.5〔mpe.ce∩ter/detaj1/21993o88 vjahttP5://5P1a5∩.〔0iqj∩g〔日i.cO∏l/exe〔ute〉(re十erer: ‖O∩e) 2O2OˉO9ˉ1qOo:57:35[5〔rapy.core.e∩gi∩e]D田[」C:〔ra"1ed(20o)〈6[丁https://5pa5.5crape·ce∩ter/detai1/21365989 γj己∩ttPs://5P135h.〔ujqi∩g〔aj.〔咖/exeCute〉(re+eIer:№∩e)

2O2oˉO9ˉ14OO:57:35[s〔mpy.coIe·e∩gj∩e]0[8[」C:〔I日w1ed(2OO)〈C[丫http5://5pa5.5crape.ce∩ter/deta11/22231998 γia∩ttp5://5P1a5∩。〔0jqi∩g〔aj。co∏‖/exe〔ute〉(re+erer: ‖o∩e) 2O20ˉO9ˉMoO:57:35[5〔I日py。〔ore.e∩gj∩e]D[80C:〔ra"1ed(2OO)〈C[丁https://5pa5。5cIape.ce∩teI/detaj1/叫845O99 γia∩ttP5://5P1a5∩.Cu1qi∩gcaj.C咖/execute)(re十eIer: ‖o∩e) 2020ˉ09ˉ140O;57:35[5〔mpy.〔ore。5cmper]D[8卯;5〔mped千r咖〈20O∩ttp5://spa5.5crape.〔e∩ter/detaj1/11601310〉 {』〔oγer|: ,http5://mg〕.douba∩io.〔o‖∏/γiew/5ubje〔t/1/pub1i〔/527290132.jpg0’ ‖∩a爬|: 0传统下的独白|』

|pri〔e|: ‖38°"允‖’



0sCore0 日 ‖8.20 ’



‖tag50 ; [‖牛孜!’ !杂丈,’ 』台湾,’ 0散丈随笔0 ′ |历史!]}

P







卜`













由于Sp‖ash和Scrapy都支持异步处理,我们可以看到同时会有多个抓取成功的结果°另外在使 用了Splash之后’爬虫的主体逻辑和JavaSc∏pt喧染流程是完全分开的,因此只要Splash能够承受对 应的喧染并发量’爬取效率还是不错的°

为了提高Splash的喧染能力’我们可以将Splash配置为集群,这样_来其喧染能力会成倍提升, 具体的配置方案可以参考htlps://setupscrape.cente门splashˉcluster里面的说明。

厂↑5



4总结

本节中我们介绍了Scrapy对接Splash实现爬取JavaSc∏pt喧染页面的流程’同样不失为一个不错 的方案。

本节代码参见: https://github.com/Python3WebSpide″ScmpySplashDemo。



|防



q



↑5.↑↑

第15章Scrapy框架的使用

Sc『apy对接pyppetee『

前面两节我们了解了Scrapy对接Selenjum和Splash的流程’还差_个Pyppeteer,本节我们来了 解Scmpy和Pyppeteer的对接方式° ↑.爬取目标

本节的爬取目标与l58节和l59节是_样的’这里不展开介绍了,本节我们改为用Scrapy和 Pyppeteer实现°

Scrapy对接Pyppeteer的流程和原理与对接Selenium的流程基本-致,同样借助于Downloader

』‖■■』‖』』】|■】■〗』勺‖■Ⅺ‖■Ⅷ』■】】可』■■】■月】■】白〗‖勺‖■Ⅷ‖■】‖‖〗‖

806

Middleware来实现。最大的不同是, Pyppeteer需要基于asyncio异步执行,这就需要我们用到Scrapy 2.准备工作

在本节开始之前’请确保已经安装好了Scrapy’这里要求Scrapy的版本不能低于20, 20版本以 下的Scrapy是不支持asyncio的。另外还需要安装好Pyppeteer并能正常启动°如尚未安装可以参考 https://setupscrape.centeI/pyppeteer里面的介绍。 3.对接原理

在实现之前’我们还是来了解_下Scrapy对接Pyppeteer的原理° 在前面我们已经了解了ScIapy对接Selenlum的实现方式, Scrapy和Pyppeteer的对接方式和它是 基本一致的°我们可以自定义_个DownloaderMiddleware并实现pIo〔e55ˉreque5t方法’在 pro〔e55ˉreque5t中直接获取Request对象的URL’然后在pro〔e55ˉreque5t方法中完成使用Pyppeteer 请求URL的过程’获取JavaSc∏pt喧染后的HTML代码’最后把HTML代码构造为‖t刚1Re5po∩5e返 回°这样’‖tⅦ1侗e5po∩5e就会被传给Spider, Spider拿到的结果就是JavaSc∏pt痘染后的结果了。

■■●■‖(‖』』■■勺|‖]■■■■『』■■■】‖‖』■■∏」‖|」■■■‖|||』■●】』‖《‖纠·可{‖旦■■‖||』□■】(司

对asyncio的支持°

这里唯一不太_样的是’Pyppeteer需要借助asyncio实现异步爬取,也就是说调用的必须是async 修饰的方法。虽然Scrapy也支持异步’但其异步是基于TWisted实现的’二者怎么实现兼容呢?Scrapy 开发团队为此做了很多工作,从Scrapy20版本开始’Scrapy已经可以支持asyncjo了°我们知道, TWjsted的异步对象叫作Def[ered,而asyncio里面的异步对象叫作Future’其支持的原理就是实现了 Future到Deffered的转换’代码如下:

de+a5de十erred(「): retur∩0e+erred.「roⅦ「ut0re(a5y∩cio.e∩5uIe+‖ture(于))

Scrapy提供了_个+ro们「uture方法,它可以接收一个Future对象’返回_个De旅red对象,另 外还需要更换TWisted的Reactor对象’在Scrapy的settmgspy中需要添加如下代码:

这样便可以实现Scmpy对Future的异步执行,从而实现Scrapy对asyncjo的支持°

首先我们新建_个项目’叫作5〔rapypγppeteerdeⅦo’命令如下: 5〔rapγ5tartproje〔t5cmpypyppeteerde加

接着进人项目,然后新建-个Spjder,名称为booR,命令如下:

| { {

4.对接实现

‘卢‖(‖

好’以上便是基本的原理’下面让我们来动手实现—下上面的流程吧。



‖」∏|』□■‖{

『‖I5『[0【[∧〔丁OR= 0twj5ted.j∩ter∩et°a5y∩cjorea〔tor·A5y∩〔io5e1ectorRea〔tor0

=■』■〗‖』■〗‖」■Ⅵ‖』■■Ⅵ■·■

i‖porta5y∩〔io 十roⅦtwi5ted°1∩ter∩et.de+ermportDe十erreq





■■|‖】■■曰β■

γ ‖ β

·





β

γ











伊队}|□「卜|’『「||伊}|}巴∏|》)||〖【「||『「■「||β|‖「



… ]



] 日 ]



5〔rapyge∩5p1derbooR5pa5·5〔mpe°〔e∩ter

同样地’首先定义Item对象,名称为8oo代Ite"’代码如下所示: +ro们5〔rapy°jte们1们portIte∩’「ie1d

〔1a558OO低Ite旧(IteⅧ): ∩己Ⅷe≡「ie1d() tag5=「1e1d() 5coIe≡「je1d() 〔oγeI=「je1d() PIjce≡「ie1d()

这里我们定义了5个「je1d,分别代表书名、标签、评分、封面、价格,我们要爬取的结果会赋 值为_个个8ook1te‖对象。

接着我们定义主要的爬取逻辑’包括初始请求、解析列表页、解析详情页,整个流程和对接 Selenium的流程基本是—致的。

初始请求5tartˉreql』e5t5的代码定义如下: j呻oIt1o鸥i∏g imOItre 「ro『∏scrapyh印ortReque5t’5pider 十r咖S〔rapypyppeteerde∏℃·it咖5mPortBooⅨIte爪 〔1a558ook5pjder(5pider): ∩己∏记= 0boo代0

a11咖eddα∏ai∩5≡ [|5p日5。5〔r3pe.〔e∩ter0] ba5euI1= 0http5://spaS。scrape°ce∩teI0

de千5tart-reque5t5(5e1+): 5tartur1=+!{5e1+.ba5e-‖r1}/page/1‖ γie1dReql』e5t(st己Itur1’ ca11ba〔代=5e1十。paI5eˉi∩dex)

■β【尸‖巴■「尸|||‖■「‖■■「{‖|△■’‖[■「卜仍‖匹尸■「‖[■尸‖|』■■尸『〖=■■「也■「β‖』■厂|||■尸「匹■「‖‖‖止■■庐

其实就是在Start=req0e5t5方法里面构造了第-页的爬取请求并返回, 回调方法指定为 par5eˉi∩dex° par5eˉj∩deX方法自然就是实现列表页的解析’得到详情页的_个个URL°与此同时还 要解析下一页的URL’逻辑和Selenium-节也是一样的,代码实现如下: i旧pOItre

de十par5e-j∩dex(5e1十’ re5po∩5e); jte川5≡respo∩5e.〔s5(‖。ite『∏!) +orjte刚j∩iteⅦ5:

hIe十=1te们.c55(‖.top己::attr(hre「〉!).extr日〔t+iI5t(〉 detai1ur1=re5po∩5e.ur1joj∩(hre于) yje1dReque5t(detai1ur1’ca11ba〔促=5e1千。paI5eˉdetaj1’ priority=2) 川atc∩≡re.5earch(r!page/(\d+)′’ re5po∩se.uI1) j+∩Ot腮t〔∩8 ret‖r∩

page=j∩t(爬t〔h.group(1))+1 ∩extur1=十!{5e1十.b己5eˉl』r1}/page/{page}` yje1d【equest(∩extuI1’ c己11bac代=5e1十。paI5e-i∩dex)

在par5eˉi∩dex方法中我们实现了两部分逻辑。第一部分逻辑是解析每_本书对应的详情页URL, 然后构造新的Request并返回’将回调方法设置为par5e=deta11方法’并设置优先级为2;另一部分 逻辑就是获取当前列表页的页码’然后将其加1,构造下-页的URL’构造新的Request并返回’将

回调方法设置为par5e=1∩dex方法。

那最后的逻辑就是Par5eˉdetai1方法,即解析详情页提取最终结果的逻辑了,这个方法里面我们 需要将书名、标签、评分、封面、价格都提取出来’然后构造8oo长Ite"并返回,整个过程和对接Selenium 一节也是一样的,代码实现如下:



}‖

广■

第15章Scrapy框架的使用

808

de+par5edetai1(se1十’【e5po∩se):

∩a雁=re5po∩5e.〔55(, .∩a|‖e: ;text』).extm〔t于jrBt() tag5=respo∩5e。〔5g(0 .t己gsbutto∩5p己∩8 :te×t,).extr己ct() S〔Ore=re5pO∩臼e·C55(|。S〔Ore::text|)。extmCt千ir5t()

PIj〔e≡re5po∩5e.c55(』。PIjceSPa∩::text0).extra〔t「ir5t()

COγer≡Ie5PO∏5e.C55(! .〔Oγer: :attr(5Ⅲ〔)』)°extⅢa〔tfjⅢ5t() tags≡ [t己g.5tIip()「ortagj∩tag5] ift己gse15e[] 5COre=5COre。Strip() j十5COIee1Se‖O∩e

jte‖0 =BookIt咖(∩a爬=∩己眠’ tag5=tag5’ 5〔ore=score’ prjce=pⅢj〔e’〔over≡〔oveI)

γie1dite∩‖

这样一来,每爬取一个详情页,就会生成一个Boo促IteⅦ对象并返回°

同样地,现在我们只是完成了Sp!der的主逻辑,现在运行同样是得不到任何爬取结果的, 因为当 前Response里面包含的并不是JavaScnpt痘染页面后的HTML代码° 所以下面至关重要的就是利用DownloaderMiddleware实现与Pyppeteer的对接°

我们新建—个pyppeteeI‖jdd1e"are’实现如下:

0

+r咖pyppeteeIj呻ort13u∩〔‖ +rmS〔rapy.httpj呻ort‖m1Re5PO∩5e i呻orta5y∩cjo i呻Ort1oggi∩g 于ro‖)twi5ted.j∩ter∩et°de千erj‖portDe+erred

1oggj∩g.getlogger(‖眶b5oc代et5』).5etleγe1(0I‖「0』) 1oggi∩g.get[ogger(0pyppeteer』).5etleve1(|I‖「0‖) ■

de十a5de十erred(十〉:

retur∩De十erred.+ro∏‖「uture(a5y∩〔io.e∩5ure+utuIe(「)) 〔1a55pyppeteer"idd1eware(object): a5γ∩cde千-pro〔e55=Ieque5t(5e1千’ reque5t’ 5pjder): bI叫5er=awajt1al』∩C∩(∩ead1eS5=『己15e)

page=awajtbI叫5eI.∩ewP日ge() PyPPeteer-re5PO∩5e=日"a1tPage.goto(request.uI1) a"己ita5y∩cjo。51eep(5) ∩t‖1≡日"aitpage.〔o∩te∩t() pyppeteerˉre5po∩5e.header5。pop(0co∩te∩tˉe∩〔od1∩g!’‖o∩e) pyppeteerˉre5po∩5e.∩eader5.pop(0〔o∩te∩tˉ[∩codj∩g‖’‖o∩e) reSpo∩5e=毗Ⅷ1ReSpo∩5e( page°ur1’ 5tatu5=pyppeteer-Ie5po∩5e.5tatu5’ header5=pyppeteer=re5po∩5e.header5’ body=5tr。e∩code(∩t‖1)’ e∏〔odi∩g=0ut十ˉ80 ’ reque5t≡reque5t



3wajtpage。C1o5e() awajtbro"5er.〔1ose() retur∩re5po∩5e

defproce55-reque5t(5e1千’ reque5t’ 5pjder): retur∩a5de十erred(5e1+。-pro〔e55ˉIeque5t〈reque5t’ 5pider))

首先我们声明了Pyppeteer的日志级别’防止控制台输出过多的日志。然后 我们声明了一个 a5Oe丁erreO〃沽, de十erred方法’如上文所述,它可以将Future对象转化为Deffe爬d对象。接着在pro〔e55=req0e5t 贝‖工义H『还, ˉ巳口」以猾卜utureXⅥ家按忆刀De∏ered呵象°按看仕PrO〔e55=reql」e5t 方法中’我们调用了a5de十erred方法’它的参数是=pro〔e55ˉreque5t方法返回的Future对象,该 Futu!℃对象会被转换为DefTered对象°-proce55ˉreque5t方法中实现了Scmpy对接Pyppeteer的核心 逻辑,主要流程就是获取Request对象的URL,然后使用Pyppeteer把它打开,将最终的喧染结果构 造-个‖tⅧ1Re5po∩5e对象并返回’这里我们将Pyppeteer的∩ead1e55参数设置为了「a15e’以便观察 ←■

■止











l5」l

Scmpy对接Pyppeteer

809

定义好pyppeteer‖1dd1ewaIe之后’我们还需要在semngspy里面增加一些配置。



第—个至关重要的就是更换TWister的Reactor对象,在se仗ingspy中增加如下定义:



接着我们可以再定义_下并发数`DownloaderMiddlewaI℃的配置和其他配置, settlngspy配置如下:





「 }











P

ⅧI5丁[DR[A〔「0R= ‖tⅣi5ted.j门ter∩et.a5y∩〔ioreactoI.∧5y∩cioSe1e〔toIRea〔toI‖

R0B0丁5丁X丁08[γ=「己15e

〔删〔0RR[‖丁R[α」[5『5=3

刚‖t趴D[R问I"L[刚R[5={ 5crapγpyppeteeIde咖。川idd1酗are5.pyppeteer佣jdd1ewaIe0 : 543』 }

以上我们初步完成了Scmpy和Pyppeteer的对接流程。 下面我们运行_下Spjder’命令如下: scr己py〔raN1boo低

可以看到Spjder在的运行过程中’与Pyppetcer对应的Chmmium测览器弹出来并加载了对应的 页面,控制台输出如下: 202oˉo9ˉ1914:58:25 [5〔mpy.〔ore.e∩8j∩e]0[B0C:〔m"1ed(2oO) <C[『http5://5pa5.5cIaPe.ce∩teI/det己i1/ 3』672176> (Ie+ereⅢ: http5://5p己S.scmpe.ce∩ter/page/1) [I:pγppeteer.1au∩〔heI] 8row5er1jste∩j∩go∩: "s://127.0·O。1:62777/deγtoo15/br叫5eI/6Sd32e49ˉ48d6ˉ4d1eˉ bc53ˉ134+509Oba32

202Oˉ09ˉ191』:58:26 [5〔mpy.〔oIe°5〔mpeI]0[8l几: 5〔mped十rm<2oO∩ttp5://5pa5。5〔raPe.ce∩ter/det己i1/ 3▲672176〉

{!〔oγer0 : ′http5://mg1.do仙ba∩io镭c哪/vi印/subject/1/Pl」b1j〔/533519539。jPg0 ’ ∩己眠′: 0呼吸0 ’



0prjCe0 : 0420 ’ 5〔oIe0 : 08.60 ’



,tag5, : [‖科幻!’ ‖科幻′]、说0’ ‖特捻。县,’| ′」、说, ’ |短乃」」、说』]} [I:pyppeteer.13U∩〔∩er] term∩ate〔hrO‖论PrOCe55… 2O∑OˉO9ˉ191q:58:26[5〔rapy.〔ore.e∩8j∩e]D[8lL;〔raw1ed(20O)<C[丁∩ttP5://5Pa5.5craPe.〔e∩ter/detai1/6O82808〉 (Ie十eIer: ∩ttps://5P己5。5〔mPe.ce∩teI/Page/1) [I:pyppeteer.1au∩〔her]8ro"5er1jste∩i∩go∩:ws://127.O.0.1;630〕3/deγtoo1s/br咖5er/d7ea1〔79ˉ2787ˉ4b68≡9596ˉ



202Oˉ09ˉ1914;58:27[5〔rapy.core.scrapeI]D[8{」C:5〔mped十r咖〈200http5://5Pa5.5〔mPe.〔e∩ter/det己i1/60828O8>

0





卜 P



l0



0

9e894C6186己5

{0〔oγeI0 : ‖bttps://i吧9。douba∩jo.〔o∏]/γj出/subje〔t/1/Pub1j〔/56384944.jPg0 ’ ‖∩aⅧe‖ 月 !百午募独|』

‖Pri〔e0 : 039.50元0 ’ 5〔ore0 : `9·2|’

‖tag5‖ : [‖百年巫独』’ 0加西亚。马尔允斯‖’ ‘R幻巩实主义‘ ’ ,经典0’ 0拉臭丈学|]}

我们爬取到了喧染后的页面,至此我们就借助Pyppeteer实现了Scmpy对JavaSc∏Pt喧染页面的 爬取°

p

p p

5.对接优化

.

■|●「·『‖匹】「●『‖|巴【■『||[■■‖||■尸||匹■■∏|【β||■■尸|匹『「|||旧尸『|‖‖■【『‖‖■『‖

同样’我们刚才实现的pyppeteerNidd1eware功能也是比较粗糙的,简单列举几点。

□Pyppeteer初始化的时候仅指定了head1e55参数,还有很多配置项并不支持自定义配置° □没有实现异常处理’比如出现page[rroI或『j"eout[rror如何进行重试° □加载过程简单指定了固定等待时间’没有设置等待某一特定节点。 □没有设置Cookie、执行JavaScrjpt、截图等—系列扩展功能°

〖§—

为了解决这些问题’我写了—个Python包’对以上的pyppeteer"jdd1eware做了一些优化。

□Pyppe$cer的初始化参数可配置’可以通过全局se忱ings或Request对象进行配置° □实现了异常处理,出现加载异常会按照Scmpy的重试逻辑进行重试。

□加载过程可以指定在特定节点处进行等待,节点加载出来立即继续向下执行。



|」日

8l0

第l5章Scrapy框架的伎用 ‖

□增加了设置Cookje、执行JavaScnpt、截图代理设置等_系列功能并将参数可配置化。

□增加了PyppeteerReque5t’定义Request更加方便而且支持多个扩展参数° □增加了WebD∏ver反屏蔽功能,将测览器伪装成正常的测览器防止被检测° □增加了对IWjster的Reactor对象的设置’不用额外在setttings.py里面声明丁‖I5丁[0R[∧〔丁0R° 助pip3来安装’命令如下:

■■□|■■‖』〗

这个功能和GeIapySelenium的功能基本是—样的’这个包名叫作GerapyPyppeteer’我们可以借

0

pip3i∩5ta11gerapyˉpyppeteer

同样地,GerapyPyppeteer提供了两部分内容’_部分是DownloaderMjddlewaIe’一部分是Request°

·‖〈{·

首先我们需要开启中间件,在settjngs里面开启PyppeteerMjddleware,配置如下: 刚肌0∧D[R‖1m[["∧旺5= {

gerapy-pyppeteer°dow∩1oadem1dd1ewares.pyppeteeI‖jdd1eN己Ie0 : 543’ }

的定义去掉°

然后我们把上文定义的Request修改为pyppeteerReque5t即可:

■■■』叮·■Ⅶ

定义了pyppetee洲idd1eware之后’我们无须额外声明ⅧI5丁[DR[∧〔丁0R,可以把刚才ⅧI5丁[0尺[∧α0R

mport1oggj∩g 1卯ortIe

〔1a558oo代5pider(5p1der): ∩a‖e= 0boo代『

a11o"eddo『∏a1∩5= [‖5pa5。5〔r日pe.〔e∩ter′] ba5eur1= 0‖ttps://5p己5°5〔r己pe.〔e∩ter0

□司当■■■■Ⅲ■■‖

「ro∩]gerapy-pyppeteerj川portPyppeteerReque5t 十ro↑『‖ 5〔rapyi呻ort【eque5t』 5pjder 千r咖5〔rapypyppeteerde∏℃°ite‖5jⅦportBookIte们

de十5tart-reql』eSt5(5e1千)怠 5tartur1=「|{5e1+.ba5eur1}/p己ge/10

y1e1dpyppeteerRequest(5tartuI1』〔a11ba〔贸=5e1千。paI5ej∩dex’ wait+or=|.ite们 。∩aⅦe!) 0

de「par5ei∩dex(5e1「’ re5po∩5e): 00



00∏

extm〔tboo促5a∩dget∩extpage

q

目para∏re5po∩5e; :retur∩; 00■00

iteⅦs=re5po∩5e.〔55(, .jte‖|) 于Orjte们i∩jte们5:

yie1dpyppeteerReque5t(detaj1ur1’〔a11ba〔代=5e1十.paI5edetaj1’prjority≡2’w己it十oI=0 .jt咖.∩a爬!) 爬t〔h≡Ie。5e日I〔h(I‖page/(\d+)‖ ’ re5po∩5e.ur1) i+∩Ot∏at〔h8 Ietur∩

page=i∩t("atch.group(1))+1 ∩extur1=十‖{5e1千·ba5eur1}/page/{p日ge}‖ yie1dpγppeteerReque5t(∩extuI1’〔a11b日c代=5e1+.p己r5ei∩dex’wait千or=0 .1te" .∩a阳e‖)



‖‖‖(‖|

‖re千≡iteⅦ。〔55(0·top日;;attr(hre+)‖).extra〔t+1r5t() det己11ur1≡re5po∩5e.uI1joj∩(∩re「)





这里pyPpeteerReque5t和原本的Request多提供了一个参数: wa1t+or。通过这个参数我们可以 指定Pyppeteer需要等待特定的内容加载出来才算结束’然后返回对应的结果° 为了方便观察效果’我们把并发限制修改得小一点’然后把Pyppeteer的Headless模式设置为 「a1se,在semngspy中进行如下配置:

| ■□■|{‖】■‖‖□□』■■■司』‖‖』□。□

这样其实就完成了Pyppeteer的对接了’非常简单。





l5.ll

Scrapy对接Pyppeteer

8l]

〔删〔0职[‖丁【[α」[5丁S≡3 c[R∧pγpγpp[丁[[R‖[∧Dk[55=「己15e

这时候我们重新运行下Spider’就可以看到在爬取的过程中, Pyppeteer对应的Chromium测览器 弹出来了,并且逐个加载对应的页面内容’加载完成之后测览器关闭° 控制台输出如下:

2O20ˉO9ˉ1915:16:48[gerapy.pyppeteer]0[80C:5etoptjo∩5{‖head1e55‖:丁rue’』du|∏pio‖:「a15e’‖devtoo150 :「a15e’ arg5{ :[!ˉˉ"i∩d酣ˉ5ize=14m’7"』’ !ˉˉdjsab1eˉexte∩5jo∩5‖ ’ 』ˉˉ‖jdeˉ5〔ro11bars,’|ˉˉ则teˉaudio‖’ 』ˉˉ∩oˉ5a∩dbox0 ′ 0ˉˉdj53b1eˉ5etujd-5a∩dbo×‖ ’ !ˉˉdj53b1eˉgpu0 ]’‖jg∩oreOe十au1tArg5! : [ 0-e∩日b1eˉ己uto↑∏atjo∩!]’ 0怕∩d1e5I6I‖丁|:

丁n』e’ ‖h己∩d1eSIC丁[R∩‖ : 丁n』e’ ‖h日∩d1e5IC‖0p‖:『rue’‖auto〔1ose‖: 『rue}

2020ˉo9ˉ1915:16:q8[5〔rapy.〔ore.s〔Iaper]0[80C: 5cmped+r咖<2卯http5://spa5.5cmpe.〔e∩ter/detaj1/26838522〉 {』〔oγer! : |httPs://j吧1.douba∩jo.c咖/γj印/5ubje〔t/1/pub11c/5289叫9』7。jpg,’ ∩a川e0 : ’发瓜心理学8儿亡与片少午(累9版)(万十心理)|’ ‖pri〔e『 8 088."0』 5core0 : 09.2! ’

|ta85′: 『心理学}’ ‖育儿0 ’ !儿亡心理学,’ 0发瓜心理学|’ |杖订` ]}

202OˉO9ˉ1915:16:《8 [5crapy.〔ore.e∩gj∩e]D[B0C:〔Ia"1ed(2o0) <C[丁∩ttp5://5p己5.5cmpe。〔e∩ter/detaj1/ 3o143叫2〉(re+erer8 ∩ttp5://5pa5。5crape.ce∩ter/page/1) 2O20ˉo9ˉ191S:16:48[gerapy。pyp帐teer]D[队见: proce55j∩greque5t〈C[丁∩ttp58//5pa5.5crape.〔e∩teI/deta11/3o356718〉 2020ˉO9ˉ1915:16:08 [gerapy。PyPPeteer]D[8l几: PyPPeteer贮ta{} 2o20ˉ09ˉ1915:16:48[gempy.pyppeteer]D[8l几: 5etoptjo∩5{0be己d1es5‖:丫n』e』 0duⅧpjo|:「己15e’‖deγtoo15, ;「a1se’ arg5! : [,ˉˉ"i∩d础ˉ51ze二1斗m’7"! ’ !ˉˉdjsab1eˉexte∩5io∩S‖ ’ 0ˉˉ∩jdeˉScro11bar50 ’ |ˉˉ刚teˉaudio|’ 0ˉˉ∩oˉ5a∩dbox`’ ‖ˉˉdi5ab1eˉ5etuidˉsa∩dbox‖ ′ 』ˉˉdj5ab1eˉgp00]’ 0ig∩oreDe+au1t∧rg5|: [‖ˉˉe∩ab1eˉauto‖己tio∩』]’‖h己∩d1e5ICI‖『|: 丫rue’|ha∩d1e5I6丁[R∩|: 丁n』e’ ‖‖己∩d1e5I6Ⅷp|:『rue’ 0auto〔1ose』: 『rue}

2o20ˉ09ˉ1915:16:明[5crapy.〔oⅢe°5〔r日per]D[8[几:文m闷「r咖<2卯∩ttp5://5pa5.scrape.〔e∩ter/det日11/3o143叫2〉 {‖coγer‖ : !http5://j吧1.douba∩jo.〔o‖]/γi酗/5ubje〔t/1/pub1jc/529688S』7。jpg|』 ∩a『∏e’: !W[Rl帜0(11)|’ !Prj〔e0 5 ‖o∩e’ 5〔ore0 : 07·60 ’

0ta85|: [|轻′‖`说0 ’ 0丸山《炉扫0 ’ ,日本‖’ ‖介幻0 ’ ,骨做犬|]}

这样我们就借助CempyPyppeteer完成了JavaScnpt喧染页面的爬取°

另外pyppeteer‖jdd1ewaIe还提供了很多配置项’下面我们来展开说_下° ●开启WebDrive『反屏蔽功能

该功能默认是开启的’就是将当前测览器伪装成正常的测览器’隐藏WebDnver的-些特征,如

需关闭’在semngsPy中增加如下代码: 6[R∧pγpγpp[『[[RpR[丁[‖0=「315e ●

●升启Head‖eSs模式



在默认情况下,Headless模式是开启的,刚才我们将C[R∧pγpγpp[『[[R‖[∧0〔[55配置为「a15e取 消了Headless模式’如果想开启可以将其配置为「rue’或者不执行任何配置: C[R∧pγpγpp[「[[R‖[汕[[55=丁rue

●超时时间

我们可以设置Pyppeteer加载所需的超时时间,单位为秒。如果该时间内页面没有加载出来或者 pγppteeerReque5t指定的等待目标没有加载出来’就会触发超时,默认情况下会进行重试爬取,超时 时间配置如下: C[RApγpγpP[『[[只咖l0汕丫I问[α」『=3o

●宵口大小

我们可以设置Pyppeteer的窗口大小,例如: C[【∧pγpγpp[丁[[R‖I‖刚‖I0丁‖=14"

C[RApγpγpp[『[[β‖I‖0叫‖[[6‖『=7"



| Q

第l5章Scrapy框架的使用

812



●Pyppeteer启动参数

Pyppetter在启动时可以配置多个参数,如deγtoo15、dl』"pjo等,这些参数在CeraPypγPPeteeI中 也得到了支持’可以直接进行如下配置: 6[R∧pγpγPp[丁[[Rα川pI0=「a1Se



二 二 C[RApγpγpp[『[[RDI趴8[[[X∏[‖5I删5=∏rue

二 C[R∧pγpγppf丁[[RR」∏[∧lDIO≡丁Iue





6[R∧pγpγpp[∏[[R-DI弘8l[5[n」ID5∧"D6ox=∏me



这里的一些配置和Pyppetter的启动参数是_一对应的,具体可以参考Pyppeteer的官方文档: https://pyppeteer.github.io/pyppetee门I℃ference.html#launcher。 ●忽咯加载资源类型

Pyppteer可以自定义忽略特定的资源类型的加载’比如忽略图片文件、字体文件的加载,这样做 可以大大提高爬取效率,常见类型如下° □do〔uⅦe∩t:HTML文档。

厂‖△

□5ty1e5heet:CSS文件。 □5〔rjpt: JavaScript文件° □j"age:图片。 □"edja:媒体文件’如音频、视频°

□十O∩t:字体文件。 □texttra〔k:字幕文件。

□×∩r:Ajax请求° □+et〔∩: Fetch请求。

□eve∩t5our〔e:事件源° □web5o〔低et:WebSocket请求° □‖a∩i「e5t:Manifest文件°

□other:其他°

6[R∧pγpγpp[『[[RIC"日[R[5α」R〔[Ⅳp[5= [|i阳ge0 ’千O∩t,]

默认情况下是留空的,即加载所有内容° ●截图

(□■可」〗|』■‖』■■】‖■』‖《‖‖

比如我们想要在爬取过程中忽略图片`字体文件的加载’可以进行如下配置:

GcrapyPyppeteer提供了截图功能,其参数可以在pyppteeerReque5t的5〔Iee∩5∩ot中定义,格式 和PyPPetecr的5〔ree∩5hot的参数一致,可以参考官方文档: https:〃pyppeteeⅢgithuhlo/Pyppetc团胎危cnoc. html#pyppeteer.page.Page.screenshot。

例如我们可以在PyppteerRequest中增加scree∩5hot参数’配置如下: yie1dpyPPeteer只eque5t(5tartur1’〔a11ba〔R≡5e1十.par5e-i∩dex’wait十or=0 。jte川 .∩a‖e ’ 5〔Iee∩5hot={



‖tyPe0 : 0P∩g|』 0于u11p己ge0 : 「n』e }〉

「匹

然后对应的Response对象的Ⅷeta属性里面便会多了一个5〔Iee∩5hot属性,比如在回调方法里面 便可以使用下面的方法将截图保存为文件:



‖{巴■『‖广止∩‖「

| l5.l2

ScIapy规则化爬虫

8l3

》「‖■‖巳■尸◆仕|‖广‖■『|卜‖∩

de+par5eˉi∩dex(5e1「’ re5Po∩5e): Nithope∩(|5cree∩5hot.p∩g,’Wb,)a5f:

「·wrjte(re5po∩5e。爬ta[|5〔ree∏s打ot|].getbu仟er())

以上我们便介绍了GerapyPyppetcer的基本用法,通过GempyPyppeteer我们可以更方便地实现 Scrapy和Pyppeteer的对接,更多的用法可以参考GerapyPyppteer的仓库地址: https://github.com/ Gempy/GerapyPyppeteer° 6.总结

本节我们介绍了Scmpy和Pyppteer的对接解决方案和优化方案°至此’Scmpy对接Selenium` Splash、Pyppeteer的方案就都介绍完了,大家可以根据情况自行选择对应的方案° p

本节代码参见: h呻s://github.com/Python3WebSpideI/ScrapyPyppeteerDemo。

■』■■

|5.↑2 Sc『apy规则化爬虫

△■■

前文我们了解了Scmpy中Spider的用法,在实现Spider的过程中,我们需要定义特定的方法完 成-系列操作’比如生成Response、解析Response、生成Item等°由于整个过程是由代码实现的,

■■‖=『■甘■

所以逻辑控制比较灵活’但是可扩展性和可维护性相对比较差°

试想,如果我们现在要实现对非常多站点的爬取,比如爬取各大站点的新闻内容,那么可能需要

为每个站点单独创建_个Spider,然后在Spider中定义爬取列表页`详情页的逻辑。其实这些Spider的

▲■■△β■

基本实现思路是差不多的,可能包含很多重复代码’因此可维护性就变得比较差°

如果我们可以保留各个站点的Spjder的公共部分’提取不同的部分进行单独配置(如将爬取规则、

■伊‖▲■‖▲■尸

页面解析方式等抽离出来’做成一个配置文件)’那么我们在新增一个爬虫的时候’只需要实现这些 网站的爬取规则和提取规贝‖’而且还可以单独管理和维护这些规则°

本节,我们就来探究一下Scmpy规则化爬虫的实现方法° ↑。C『aw|Sp|de「

卜『■‖β

在实现规则化爬虫之前,我们需要了解—下CmwlSpider用法°它是Spider类的子类’利用它我 们可以方便地实现站点的规则化爬取’其官方文档链接为: http://scrapy.readthedocs.io/en/latest/topics/ spiders.h‖ml#crawlspjdeT。

‖尸

在CrawlSpjder里,我们可以指定特定的爬取规则来实现页面的解析和爬取逻辑,这些规则由_ 个专门的数据结构Rule表示°Rule里包含提取和跟进页面的配置’CrawlSpider会根据Rule来确定当 前页面中哪些链接需要继续爬取’哪些页面的爬取结果需要用哪个方法解析等°

CrawlSpider继承自Spjder类’除了Spider类的所有方法和属性’它还提供了一个非常重要的属 卜■ 尸巴■【尸■■[■厂■■■『■■■■∏■◆■■■尸■∏

性rules°rules是爬取规则属性,是包含一个或多个Rule对象的列表°每个Rule对爬取网站的规则都 做了定义’Craw‖Spjder会读取m‖es的每_个Rule并执行对应的爬取逻辑° 它的定义和参数如下所示: ■ ■ ■ ■ ■

〔1己s55〔mpy.5pider5。Ru1e(1i∩kˉextⅢactor=‖o∩e’〔a11ba〔低=‖o∩e’〔bˉ代w日rg5≡‖o∩e’「o11oN=‖o∩e’ proce55-1i∩kB≡‖o∩e’ pro〔e55-reql』e5t=‖o∩e’ errba〔低≡‖o∩e)



↑5

下面对其参数依次说明°

□1j∩促extractoI:一个[i∏促[xtractor对象°通过它’ Spider可以知道从爬取的页面中提取哪

些链接进行后续爬取,提取出的链接会自动生成Request,这些提取逻辑依赖LinkExtractor对 象里面定义的各种属性’下文会具体介绍。

■乙

0



第l5章Scrapy框架的使用

□〔a11ba〔代:回调方法’和之前定义Request的〔a11ba〔代有相同的意义°每次从11∩代extIaCtor

||

8l4

中提取到链接时,该方法将会被调用。该回调方法接收Ie5po∩se作为其第-个参数并返回_

个包含Item或Request对象的列表°需要注意的是,避免使用Par5e方法作为回调方法,因 为CrawlSpidcr使用par5e方法来实现其解析逻辑’如果par5e方法被重写了,CrawlSpider可 能无法正常运行°

□〔b-k"arg5:一个字典类型,使用它我们可以定义传递给回调方法的参数。

认为「a15e。

□proce551j∩促5:可以是_个〔a11ab1e方法,也可以是—个字符串(需要和CrawlSpider里面 定义的方法名保持一致)°它用来处理该Rule中的11∩kextra〔tor提取到的链接’比如可以进 行链接的过滤或对链接进行进一步修改°

□pro〔e5sˉreque5t:可以是一个ca11ab1e方法’也可以是_个字符串(需要和CrawlSpider里 面定义的方法名保持一致)°根据该Rule提取到每个后续Request时’该方法都会被调用,该 方法可以对Request进行进—步处理,必须返回Request对象或者‖o∩e° □[rrba〔促:该参数是Scrapy20版本之后新增的参数,它也可以是_个〔a11ab1e方法,也可以是 -个字符串(需要和CrawlSpjder里面定义的方法名保持—致)°当该Rule提取出的Request在被 处理的过程中发生错误时’该方法会被调用’该方法第—个参数接收一个「"15ted「日j1ure对象°

以上内容便是CrawlSpider中的核心数据结构Rule的基本用法’利用Ru‖e我们可以方便地实现 爬取逻辑的规则化° 2.L|∩恨匠xt『acto『

上文我们了解了Ru‖e的基本用法,其中-个重要的属性就是1j∩代extr己ctor,下面我们再来专门 了解—下它的用法°

LlnkExtractor定义了从Reponse中提取后续链接的逻辑’在Scrapy中它指的就是5〔mpy. 1i∩促extractor5.1xⅦ1btⅦ1.[xⅧ1[i∩代[xtm〔tor这个类,为了方便调用’ Scrapy为其定义了一个别名’ 叫作LlnkExtmctor,二者是指的都是[x们1[j∩代[xtm〔tor。 [X"1[j∩k[xtmCtor接收多个用于提取链接的参数,下面依次对其进行说明。 □a11O":_个正则表达式或正则表达式列表’它定义了从当前页面提取出的链接需要符合的规 则’只有符合对应规则的链接才会被提取。

□de∩y:和a11ow正好相反,它也是_个正则表达式或正则表达式列表,定义了从当前页面中禁 止被提取的链接对应的规则,相当于黑名单’它的优先级比a11Ow高°

□a11o"do阳ai∩5:定义了符合要求的域名,只有此域名的链接才会被提取出来’它相当于域名 白名单。

尸司||」□】勺】‖‖』■〗勺‖‖||刽司|』■‖司′□□■■」■■|‖|■■‖|‖·■』■■‖』·勺|■■‖」叮』·刊‖』‖」■|·■|‖‖‖‖■司|‖β‖‖·‖司■■

□十o11ow:一个布尔值’它指定根据该规则从re5po∩5e提取的链接是否需要跟进爬取°跟进的 意思就是将提取到的链接进—步生成Request进行爬取;如果不跟进的话’一般可以定义回调 方法解析内容,生成Item。如果ca11bac《参数为‖o∩e’十o11ow值默认设置为丁rue,否则默

□de∩y-do"a1∩5:和a11o"doⅦa1∩5相反,相当于域名黑名单’该域名所对应的链接都不会被提 □de∩yˉexte∩s1o∩s:在提取链接的过程中可能会遇到_些特殊的后缀,即扩展名。de∩y=exte∩51o∩5



』■■|□=■司□勺■■■

的链接都会被忽略。

‖|

定义了后缀黑名单’包含这些后缀的链接都不会被提取出来°de∩yˉexte∩51o∩5的默认值由 5crapy.1j∩促extmctor5.IC‖0R[0[X丁[‖5I0‖5变量定义°在Scrapy2.0中, I6‖0R[0[X「[‖5I0‖5 包含了7z` 7z1p` ap|〈、 bz2、〔dr` d"g` j〔o、 j5o` tar、tar.8z、web们` xz等类型,这些后缀

』■■■■|』■■■■∏

取出来。

l5.l2 Scrapy规则化爬虫

815

□re5trict—×Path5:如果定义了该参数,那么Spider将会从当前页面中XPath匹配的区域提取 链接’其值是XPath表达式或XPath表达式列表°

□restrictc55:和re5trj〔tˉxpatb5类似,如果定义了re5trict〔55, Spjder将会从当前页面 中CSS选择器匹配的区域提取链接’其值是CSS选择器或CSS选择器列表°

□tag5:指定了从什么节点中提取链接’默认是(|己』’|area|),即从a节点和日re日节点中提取 链接。

□attrS:指定了从节点的什么属性中提取链接,默认是(』hre十』’)’和tag5属性配合起来,那 将会从a节点和area节点的∩re于属性中提取链接°比如我们需要从i们g节点的5rC属性中

提取链接,那可以将tag5定义为(|a′’|area』’ 』mg!)’ attr5定义为(|∩re十』’|5rC,)° □ca∩o∩jca1j∑e:是否需要对提取到的链接进行规范化处理,处理流程借助"311b.0I1. ca∩o∩1ca1jzeur1模块’该参数默认为「a15e。

□u∩jque:是否需要对提取到的链接进行去重,默认是『rue°

□proce55γa1ue:是一个〔a11ab1e方法’可以通过这个方法来定义一个逻辑’这个逻辑负责完 成提取内容到最终链接的转换°比如说hre+属性里面的值是_段JavaScrjpt变量,值为

j己γa5cr1pt:go「opage(` . ./other/page.‖tⅧ1′),这明显不是—个有效的链接’proce55γa1ue对 应的方法可以接收这个值并对这个值进行处理,提取真实的链接再返回°

□Strjp:如果从节点对应的属性值中提取到了结果’是否要去掉首尾的空格,默认是丁rue° 以上便是LinkEx!ractor的—些参数的用法’其中前几个参数使用频率较高,可以重点关注。有关 LinkEmactor更详细的介绍可以参考官方文档:http://scrapy.readthedocsjo/en/latesUtopics/linkˉextractors. hUnl#moduleˉscrapy』inkextracto蹈』xmlhtml。 3.‖temLoade『s

我们了解了利用CIaw‖Spider的Rule来定义页面的爬取逻辑’这是可配置化的_部分内容’借助 Rule’我们可以实现页面内容的提取和爬取逻辑°但是,Rule并没有对Item的提取方式做规则定义。 对于Item的提取,我们需要借助另一个模块ItemLoadeIB来实现°

可以这么理解’Item提供的是保存抓取数据的容器’而ItemLoaders提供的是填充容器的机制。 尽管Item可以直接由代码进行构造’但ItemLoaders提供一种便捷的机制来帮助我们方便地提取Item,

它提供了更灵活`可扩展的机制来实现Item的提取逻辑,同时也有助于我们实现爬虫的规则化。 ItemLoaders的用法如下所示:

〔1a555〔mpy。1oader.Ite∏[oader([jt铆′ 5e1ector’ re5po∏5e’] **促warg5)

这里我们使用的Scrapy提供的IteⅦ〔oader类, Ite们[oader的返回_个新的Ite们[oader来填充 给定的Item。如果没有给出Item’则使用de于au1tjte‖〔1a55中的类自动实例化°另外’它传人 5e1e〔tor和re5po∩se参数来使用选择器或响应参数实例化° 下面将依次说明ItemLoader的参数°

□it铆: Item对象,可以调用add=Xpath、add〔S5或addγa1Ue等方法来填充Item对象°

-

□5e1ector: Selector对象,用来提取填充数据的选择器。

□re5po∩5e:Response对象,用于使用构造选择器的ResPonse°

↑5 ■

一个比较典型的Ite们[oader实例如下: 「rm5crapy·1oader加portIte‖[oader

十I刚PrOje〔t.jt咖5i"POrtp】OdUCt de千paI5e(5e1千’ re5po∩5e): 1oader=IteⅧloader(ite"|三produ〔t()’ Iespo∩5e=Ie5po∩5e)



第15章Scrapy框架的使用 1oadeI.3dd-xp己th(|∩己眶! ’ ,//dM伙1a55=园produ〔t=∩蓟记回],) 1oader.addˉxpath(0∩己|∏e,’ ,//div[肛1ass=口produ〔t-tit1e口]|) 1oader。addˉxpat∩(|prj〔e!, 0//p[0id=圃pIi〔e圃]|) 1Oader.己dd〔55(,5tO〔促』’w5toC惯]|) 1o3der.addˉγa1ue(,1a5t-l』pdated,’ ,tod己y』) retuI∩1Oader.1o己dit印()

这里首先声明一个Pmductltem’用该Item和Response对象实例化It印[oader’调用addˉxPatb方 法把来自两个不同位置的数据提取出来,分配给∩己‖∏e属性,再用add-xpath、add〔55、addγa1ue等 方法对不同属性依次赋值,最后调用1oadite∏‖方法实现对Item的解析。这种方式比较规则化,我们

■]■可■■]■■可■■司」‖』■|{|』■可‖』□■■‖‖』■|]』■■■】』词|]』■∏』』·』·口‖』·‖|□■{■■日』

8l6

数据时立刻提取数据’InputProcessor的结果被收集起来并且保存在IteⅦ[o日der内’但是不分配给

Item。收集到所有的数据后,1oadjte们方法被调用来填充再生成Item对象。在调用时会先调用Output



』●]■‖‖‖|■■可《

另外, ItemLoader的每个字段中都包含了_个InputPmcessor(输人处理器)和一个Ou印ut Processor(输出处理器),利用它们我们可以灵活地对Item的每个字段进行处理°InputProcessor收到

厂■■『■

可以把_些参数和规则单独提取出来,做成配置文件或存到数据库’实现可配置化°

Processor来处理之前收集到的数据,然后再存人Item中’这样就生成了Item°

+ro∏] jt即1oader5.pro〔e55or5j呻ort∏冰e「jrst’阳p〔呵o5e’〕oj∏ +rα∏5〔mpy.1oaderj呻o∏IteⅦloader

c1a55produ〔tIt印[oader(It翻儿o己deI):

de十au1t-oMtpl」t-pIo〔e55or=丁己长e「ir5t() ∩a匪—i∩≡陋pm卯O5e(l』∩iCOde·tjt1e) ∩己眶Out=〕Oi∩()

prj〔eˉj∩=胞p〔αmo5e(u∩i〔ode.5trjp)

这里我们定义了-个produCtIte们〔oadeI继承了IteⅧ[oader类’并定义了几个属性的Input Pmcessor和OutputProcessor’比如∩a账属性的InputPmcessor就使用了舶p〔oⅦpo5e,Ou印utProcessor 就使用了〕oi∩,这样利用productIteⅦ[oader,我们就可以灵活地实现特定属性的数据收集和处理°

另外可以看到这里用到了『a低e「jr5t、阳p〔o∏‖pose、]oi∩’这些都是Scrapy提供的—些Processor’ 分别可以实现提取首个内容、迭代处理、字符串拼接的操作’利用这些Processor的组合’我们可以 灵活地实现对特定字段数据的处理°

其实Scrapy已经给我们提供了不少Processor’我们来了解_下° ●Ide∩tity

●丁aRe「iⅢSt

q

』●可日|』■Ⅵ』·

Ide∩tjty是最简单的Processor,不进行任何处理,直接返回原来的数据°

■■■■‖|{』●■■|■曰■■■可二■可』■■■□●■‖』■■■‖■■】‖■|」■』■■■』·可|■‖□■

类似的用法如下:

『a|(e「jr5t返回列表的第—个非空值,类似extmctfjr5t的功能’常用作Ou印utPmcessor,示 例代码如下:



+I咖5〔r己py·1oadeI.proce55orsj呻ort『3ke「jr5t PrOCe55Or=了ake「ir5t()

pri∩t(pro〔es5or([,‖’ 1」 2’ 〕]))

输出结果如下所示: 1

经过此Processor处理后的结果返回了第一个不为空的值°









●〕Oi∩

〕oj∩方法相当于字符串的joj∩方法,可以把列表拼合成字符串,字符串默认使用空格分隔,示

({









} 【尸||·『巴卧尸

15.12

Scrapy规则化爬虫

■「‖■厂·■「■巴尸■尸|

例代码如下:

8l7

·

{ro‖∏5〔rapy°1oadeI.pIo〔e55oIsi呻ort]oi∏ pro〔es5or=〕oi∩() Pri∩t(Pro〔e55oⅢ([|o∩e,’ 0tm°’ 0three0]))

输出结果如下: o∩etmthIee

′||广

它也可以通过参数更改默认的分隔符,例如改成逗号:

》厂■■尸|

十ro『∏5〔rapγ.1oader·pIo〔e55orBi呻ort]oi∩ proce5soI=]oi∩(‖’ 0)

prj∩t(proce5sor([,o∩e』’ 0two! ’ ,thⅢee!]))

运行结果如下: ■尸「▲住炉■■‖←■β凸■●■■■尸■厂尸■甘β■■∏

o∩e’tm’thIee

●〔m卯5e

〔oⅦpo5e是使用多个函数组合构造而成的Pmcessor,每个输人值被传递到第一个函数,其输出再 传递到第二个函数,以此类推’直到最后一个函数返回整个处理器的输出,示例代码如下: 十ro"scmpy·1o己der·pIo〔e55or5i呻ort〔咖po5e

pro〔e55or=〔咖pose〈str.upper’1a则bda5: 5.5tr1p()) pIi∩t(pro〔eB5oI(‖ he11owoI1d,))

运行结果如下: ‖[uO朋R[0

在这里我们构造了一个ComposeProccssor,传人一个开头带有空格的字符串。ComposeProcessor

的参数有两个:第一个是5tr.upper’它可以将字母全部转为大写;第二个是一个匿名函数,它调用

5trip方法去除头尾空白字符°〔O∏|pO5e会顺次调用两个参数’最后返回结果的字符串全部转化为大 写并日去除了开头的空格。 ●陋p〔…se

■■‖■尸仕伊

与〔咖po5e类似,‖ap〔o『mose可以迭代处理_个列表输人值,示例代码如下: +ro‖5crapγ.1oader.pro〔es5or5iⅣportNap〔oⅦpo5e

pro〔e55or=阳p〔o『∏po5e(str。upper’1a∏bda5: s.5trip()) prj∩t(pm〔e55oI([|‖e11o|’ |‖oI1d! ’Wtho∩|]))

运行结果如下:



[0‖[[l0『’|刚RlO|’ !w「}们‖!]

被处理的内容是—个可迭代对象,‖ap〔咖po5e会将该对象遍历然后依次处理° 仿’■|甘|尸「|≈广「[■[■■■∏

『‖}『



●Se1eCt〕哩≤

5e1ect〕Ⅷes可以查询JSON’传人瓜ey’返回查询所得的γa1ue。不过需要先安装jmespath库才可 以使用它,安装命令如下: 厂

pjp3i∏5ta11j爬5path

安装好jmespath之后’便可以使用这个Processor了,示例代码如下: 十ro‖5〔rapy°1oadeI。pro〔e5soI5mport5e1ect]『∏e5 PrO〔e55OI≡5e1e〔t〕『‖e5(0千oO`) pr1∩t(pro〔e5sor({干oo‖:b己r,}))

运行结果如下: baI



|↑5

广 「 β _



第l5章Scrapy框架的使用

818

以上内容便是Ite"[oader和-些常用的Processor的用法。

实现_个规则化的Scrapy爬虫° 4。本节目标

本节我们以前文所爬取过的电影示例网站作为练习来实现一下Scrapy规则化爬虫的实现方式’爬 取的目标站点是ht印s://ssrl。scrape.centeⅣ,如图15ˉ18所示° 。|

|··『■=!…■奢 ◆◆c



我们一下子又接触了不少新概念’如CrawlSpider、Rule、LinkExtmctor` ItemLoaders、PIocessor’ 你可能感觉有点憎,不知道如何使用°不用担心,下面我们通过_个实例来将这些内容综合运用一下’

分●

■…■~▲宁…

回sc,… 厂▲

■王别姬ˉ仁a「ew●↓lⅧyC◎∩cubhb● ●●

9°5 ●

!

‖…刁皿山

■h

这个杀手不太冷ˉld◎∩

9,5

●●■●

★亡古古☆

m′〗晌“ ●

涌钾汹皿

ˉ



‖■■■■■■■

肖申克唾赎ˉ丁‖●5hawBh·拘h∩●d·0wp甘°∩ ●■●

95 分白☆凸企

m川凹坤 0串啤0◎卜■

图l5ˉl8

』‖‖‖‖‖■Ⅵ‖|』■勺|』可|| 』 ■ ■ ∏ ‖ ■ ■ ■ 』 ■ □ ■ | | |

白∩凸白

…袍●沮/07‖”

目标页面



和之前不同,这次我们需要利用CmwlSpjder、Rule` ItemLoaders等实现对该站点的爬取’最后 我们还需要将爬取规则进行进_步的抽取’变成JSON文件,实现爬取规则的灵活可配置化°

在开始之前请确保已经安装好了Scrapy框架°



5.实战

首先新建_个Scrapy项目,名为scrapyunjversaldemo,命令如下所示: 月

5〔rapy5tartprojects〔rapyu∩1γeI5a1de‖℃

然后我们进人到该文件夹’这次我们便需要创建一个CrawlSpider,而不再单纯的是Spider°要创 建CrawlSpider’需要指定-个模板。 』

我们可以先看看有哪些可用模板,命令如下所示: 运行结果如下所示: ∧γaj1ab1ete∩‖p1ate5: baSj〔 〔raW1

{|{

5〔mpyge∩5pjderˉ1

C5γ十eed

之前创建Spider的时候’我们默认使用了第~个模板basj〔°这次要创建CmwlSpider’需要使用 第二个模板〔mW1,创建命令如下所示:

●』·‖』■■□司』■‖■闪』|』■‖

x∏Meed



l5.l2 Scrapy规则化爬虫

819

运行之后便会生成_个CrawlSpider,其内容如下所示: mPort5〔rapy +ro‖∏5crapy.1i∩促e×tractor5加portU∩匪xtra〔tor 千r咖scrapy.5pjder5mport〔raN1Spjder’Ru1e 〔1己55∩oγ1e5p1der(〔mw15pjder); ∩a爬= |mγie『

a11oweddα∏aj∩s=「55r1。5crape。ce∩teI‖] 5tartur15= [ ′http;//5sr1.s〔mpe.〔e∩ter/‖]

n」1e5=(

Ru1e({j∩代[xtractor(a11o门=I!Ite‖|5/0)’〔a11ba〔低=,par5eˉiteⅧ|’ fo11ow=「Il」e〉’ )

de十p己r5eˉjteⅦ(5e1十’ Ie5po∩se): it咖={}

#jt酮[|do阳1∩-jd|] =re5po∩5e。xp己th(』//j∩put[·jd=风51d碱]/0va1ue,).get() #1te∏|[‖∩a爬|] =Iespo"5e.xpath(|//djv[01d="∩a爬n]|).8et() #it酮[0descriptio∩0] =re5po∩5e.xpatb(|//djγ[0jd二阔de5〔riptjo∩"]‖).8et() Ietur∩iteⅧ

这次生成的Spider内容多了一个对mles属性的定义°Rule的第-个参数是LjnkEmactor,就是 上文所说的LxmlLinkExtractor°同时’默认的回调方法也不再是parse’而是parse一item°在parse-item

里面定义了Response的解析逻辑’用于生成Item°

接下来我们需要完善一下Rule,使用Rule来定义好爬取逻辑和解析逻辑’下面我们来_步步实 现这个过程。

由于当前需要爬取的目标网站的首页就是第-页列表页,所以5tartur15这边我们不需要做额外 更改了°运行该CrawlSpider,CIawlSpider就会从首页开始爬取’得到首页Response之后,CrawlSPider

便会使用mles属性里面配置的Rule从Response中抽取下一步需要爬取的链接’生成进-步的

Request。所以’接下来我们就需要配置Rule来指定下一步的链接提取和爬取逻辑了° 我们再看下页面的源代码,如图l5ˉl9所示° — 训 `

_…

=…一

匹■匣〗『尸}巴■厂卜■『}‖■||「巴■「[尸||●【【『『「■尸’|■「卜‖‖|[■尸‖尸卜■尸β■厂户■尸》■||『△■■匹■厂|■■■‖【尸β|■「■尸β■尸‖■「Ⅲ「}■尸‖|『‖|β■了仁■厂△■伊「卜|[■「||●「[■

5〔rapyge∩5pjderˉt〔mw1加γjeSSr1·5Crape。Ce∩ter

| _~-_ˉ-一~一…-亏-—_~≡~~-力●↑ ^ .▲

…△乙=■◇句●■=已●△凸己凸●△已妙■●·≈巴●≈凸.■△·Cb■■■◇凸D■■毋·巳●℃乙■●罕●■■△●▲尸■二●△←岳乌二

回s°°p°

一--≡=一←≡■

.白←·…~二二.◇●……≈←…←←■□·←≈·■≡■曰





■-

-



| |

严—■|

·

霸王剔峨 瞬忿睁 —

譬孽噎毯管警:

了■

·■t口■….内…田帆■…■……p ●≈的w-→】~·=晴…色…■…哼碰…晰●…田…●0……m→ =…=

‖互萨ˉ ˉ ˉ ˉ ˉ ˉ · ˉ ˉ=≡·≡→■→≡=………·、.. ˉ ˉ . . · . …《 叮令·α、o凸nO 《…哮……(…∩画r〗 「 0

▲≡

·^壳……←γ们蚂l“磅^……c山≡■l■呻■0宇『V·飞々抗希厅=丁Ql≡己j≈ ←……

匈它至■=

f●‖ˉˉ^……ˉˉˉ…—~……ˉ;赶萝菏《≡可

;…屿fO…′ ≈闻』∩『Ⅲ…P

l

如岭 7≤加●屿←Ⅳ二?邹tu些~■寸D

』…畔…■t〖··… ;=…『●.…

《…时■Ⅱ…硒0『酵〗

←…■←〗……≡…≡

0…闰●惶;学扩 ;勺→…w■…

…■=…′√= ……西=7…』n…=

,』

如妒

}_~←宁■■■d

v■1v…四[〗=…■■…■≥

←←凸■崩』=≡二

……≡了…酵【m韧“上…

g■‖q

匆…

″…

pmv吉ˉ≡亏7≡■【ut西乱南l●l→←型●1立1也岛●l→l≡●l和l=~…尸

0

仁Ⅱ=【=0l仓0



0韩∏奄……J●…0

ˉ-^一鱼≡===

∏白口一=≡_=~

=令巳·01唾■■

二潭琶缮《…ˉ;虽a矗;.…≡腻≡盖‘ˉ垂自=猩辑a≡.垂句d==蛋≡翘摔亩稻ir=≡.….…≡≡节碱苞盒.’ 图l5ˉl9页面的源代码



820

第l5章Scrapy框架的使用

我们要提取的详情页链接处于C1a55为iteⅦ对应的节点中,对应的是〔1aS5为∩a∩e的a节点, 其中bre十属性就是需要提取的内容’每页有l0个°

此处我们可以用LinkExtractor的re5trjCt〔55属性来指定要提取的链接所在的位置’之后

CmwlSpidcr就会从这个区域提取所有的超链接并生成Request°默认情况下会提取所有a节点和area 节点的hre「属性,符合我们的需求,所以无须额外配置tag5、attr5° 接下来我们将mleS修改为如下内容: ru1e5≡(

即1e(U∩旺xtmctor(re5trict〔55≡0 .ite∏` .∏a"e』)’ 十o11叫=「Iue’ ca11ba〔低=,par5e-detai1,)’

这里我们指定了LinkEmactor并声明了re5trictc55属性,另外十o11o切属性设置为『rue代表

5P1der需要跟进这些提取到的链接进行爬取’同时还指定了〔a11bac长为字符串par5e—detaj1’这样 提取到的链接被爬取之后会回调par5e-detaj1方法进行解析。

这里我们可以简单定义—个par5eˉdetai1方法打印输出被爬取到的链接内容: de十paI5eˉdet3j1(5e1「’ re3po∩se): pri∩t(re5pO∩5e.0r1)

运行_下当前CmwlSpjder,命令如下: 5Cr3pγCmW1mγie

便可以看到如下输出: 2O20ˉ1oˉ0911:20:S3 [5cr己Py.〔ore.e∏gi∩e]D[8lL:〔r己"1ed (2卯) <C[『‖ttp5://55r1.5〔Iape。〔e∩ter/det己i1/9〉 (re于erer: ‖ttp5://55r1.5〔Ⅲape。〔e∩ter/) http5://55r1·5crape.〔e∩ter/deta11/3 202Oˉ1Oˉ0911:20:53 [5craPy·〔ore。e∩8j∩e]0[8|」C:〔ra"1ed (2O0)〈C[『http5;//5sI1.5〔rape.〔e∩teI/detaj1/6〉 (re+erer: ∩ttpS://S5r1.SCI己pe。〔e∩ter/) httP5://55I1.5〔rape.〔e∩teI/detai1/9

出来了,同时这些链接又被进_步构造成了Request执行了爬取,爬取成功后,通过回调par5eˉdetaj1

Ⅷ□』βⅥ」■司

由于内容过多’这里省略了部分输出结果°我们可以看到首页对应的10个详情页链接就被提取

日」

∩ttps://5sr1.scrape.ce∩ter/) 2O20ˉ10ˉO911:20;53 [5cr日Py。〔ore.e∩gj∩e]0[8lL:〔mw1ed (2卯)〈C[『∩ttp5://55r1.5〔mpe.〔e∩teI/det3j1/1〉 (re+erer: http5://S5r1.5〔mpe.〔e∩ter/) 2020ˉ10ˉ0911:2O:53 [5cr己Py.〔ore.e∩gj∩e]D[80C:〔m"1ed (2OO) <C[『∩ttp5://s5r1.s〔mpe°〔e∩ter/detaj1/5〉 (re+erer: ∩ttP5://55r1.5〔raPe.〔e∩ter/) http5://s5r1·scmpe·ce∩ter/detai1/2 http5://55r1.5〔mpe.〔e∩ter/detai1/1 httP5日//55I1.s〔rape.〔e∩ter/detaj1/5

方法’打印输出了对应的链接°这些逻辑我们通过_个简单的Rule的配置就完成了,是不是感觉比 之前方便多了?

好’到现在我们仅仅爬取了首页的内容,后续的列表页怎么办呢?不用担心,我们可以定义另外 _个Ru‖e来实现翻页。



ˉ·‖|‖‖■Ⅲ■司|」』·■■■〗|

‖|

我们再看下_页的页面源码,查看下-页链接对应的节点信息,如图l5ˉ20所示°

■■‖‖」‖|Ⅵ‖』■■』·司■■∏|

821

■灿 ‖‖划‖



蹲下

↑眨节峪

◆已

_

…≡

` 卢蠕瓣!



…≡



蹿~



…=守—…__…=

可】≈…

职 $·腮矛 p 二

~_

钳税



辉…









守守隶 罪 鹤 ~

` 钝 g月



尸■

v■tO问∩N



☆》■●§

》!》…沁沁r`` .=—占ˉhˉ蜂丝′′.\





ˉˉ ^蘸

龄′ ^徘≡…樱·…瓣铸勘

● 戮 ″ 霉 噪 翼 ′ 恿



争邪\肛靶

戚嚣

/|‖崖









一…侧『」德啤撼礁嚼唯螺霞嚼



品 ■ 悟 鳞 副 研 ˉ 弗 搏 同 汗 嚼 … 〔 Ⅺ 凸厂



◎醒』

ScIapy规则化爬虫

l5.12

刃子王ˉ丁‖●u◎∩闪∩g

j≡

9o

RP●

禽食禽含萨

.

!

0…ˉ↑5上眩

;

!ˉ 广



′` 钳. 。 . .必气岭′辑辑豌

p

舞典 .…` ~轩~镭鞠固m←*毯萍…′《…j淘簿

L

竿







锯…叮 『≡…. . ` 已■←W…N…=

ˉ





……… ‖

◆口加呵泞y…■■…●十~n≈d脑 ■……击…【…P… 【8… F



山γ…汗…T铡pR→■…1 ●8冠·t■』●·`…1℃v…Ⅱ2~



ˉ

穴…二■~■—■■≠■<~≈±户=●午已宁==■

吁…▲【审沛■宁元tY■■尘_□ Q■■加■雨…古…… →‖铲户

■=



■凸





哮≡z≈二哩沁

●I t饥■Ⅱ…』t′

●…■……e1=●…1蚀邮一

…≡寒β=P△=吕

f【捏■~



…c…闰●l→^啄■『坝四由…

·【……幽f=≡唁

●…四扁洒■m……咳乓lD…加……… !

…氧营,ˉ告=P础≈ …Ⅱ囤翻■宁了 …~~

盘 融

》~…=

闪泊…枷m敷■d,籍禽L铜峙h= . U■0…{

…T

-ˉ }

产伯韩Fγ『〗

→6T■ˉ亏-≡髓… …j≈‖F冲沟逛; 一 恼…么≈忠磺‖

“… 吻″

』9…订 …P





……酗…



≡ˉ_.~撼

}霹肖蹿『`『….》’

●■加……=……0皿

◆巡D…………鲸山…

…■u蹿

;…0萨………《m必1…〗



$≡←≡O●喻夕



凸=▲吕≈0b…←→→0■…0·=J~

……

诌…~■…^…岭四沦~Ⅶ≡≈铲●么■←■→卤$≡…■≈

一…



■《

■… …

ˉ

n〉/|舅



图l5ˉ20查看页面源码

泣里可以观察到,下-页链接对应的是C1a55为∩eXt的a节点’其∩re十属性就是下-页的内容。 相似地’我们可以修改Rule为如下内容: m1e5= (

RM1e(li∩k[×tm〔tor(re5trj〔t〔55二! .ite".∩a‖∏e』)’ +o11四=『r(』e’ ca11bac促≡`p己I5edetaj1『)’ Ru1e(li∩旺xtractor(restrictc55=『 。∩ext|)’ 千o11o们=丁rue)’ )

这里我们又增加了一条Rule’定义了re5trict—c55为.∩ext’同时指定了十o11o"为丁rue。但因 为这次我们不需要从列表页提取Item’所以这里我们无须额外指定〔a11b3CR°

.

这样整个爬取逻辑就已经定义好了,我们重新运行_下CrawlSpider,可以看到CrawlSpider就可 以爬取分页信息了’输出结果如下:

202Oˉ10ˉ0911:52;29 [5crapy.core.e∩8i∩e] 0[80C:〔m切1ed(20O)〈C[丁∩ttp5://55r1.5〔mpe.〔e∩ter/detaj1/76〉

}·|

(re+erer: 们ttp5;//55r1.scrap巳〔e∩ter/page/8) http5;//55r1。5〔mpe°〔e∏ter/detaj1/75

20∑0ˉ10≡O911:52:29 [5cmpy.〔ore。e∩gi∩e] D[806:〔r己切1ed(2")〈C[『http5://55Ⅲ1.5〔mpe·ce∩ter/detaj1/72〉

(re于erer: http5;//55r1.5〔mpe.ce∩ter/page/8) ‖ttp5://55r1°5〔mpe。〔e∩ter/detaj1/7』

∑0Ⅺoˉ1oˉo911:52:29 [scrapy.〔ore.e∩gi∩e]D[B";〔raw1ed(20o) <C[丁∩ttps://55r1.s〔Iape.ce"ter/page/10〉 (re千ereI: ∩ttp5;//55r1.5〔mpe翻〔e"ter/p日ge/9)





芒尸 ≡广

L详

接下来我们需要做的就是解析页面内容了,刚才我们只是简单定义了paI5edetai1方法,下面

↑5 .. h



我们来使用ItemLoaders实现内容的提取。

首先我们还是需要定义—个∩oγjeIte‖’内容如下: ■ β 「 |

+I叼5〔mpyj呻ort「ie1d’IteⅧ



[■■「|■■∏|■卜‖

厂]自ss"oγjeIt印(It咖):

{∩「■■尸



822

第l5章Scrapy框架的使用 Coγer=干ie1d()

〔ategorje5=「je1d() p‖b1i5们edat=「je1d(〉 dra阳=「je1d() 5core=「je1d()



□‖·勺司

这里的字段分别指电影名称、封面、类别、上映时间、剧情简介、评分,定义好‖ov1eIteⅧ之后, 我们如果不使用IteⅦ[oadeI正常提取内容,就直接调用re5po∩5e变量的xPat‖、c5s等方法即可’ par5edetai1方法可以实现为如下内容: qe于par5edetai1(5e1「’ re5po∩5e): ite‖=问ovjeIteⅧ()

■■

■■Ⅵ

ite"「〔over『] =re5PO∩5e.C55(‖。〔Over: :attI(5r〔)0).extmCt千ir5t() jte们[0pl」b1i5hedat』] =respo∩se.css(’°i∩「o5pa∩: :text!).re+jr5t(‖(\d{4}ˉ\d{2}ˉ\d{2})\S?上映』) iteⅦ[!5coIe! ] =re5po∩5e.xp己th(0//p[co∩t己i∩5(伙1己55’ .5core")]/text(){).extra〔t+jr5t().5trjP() 1te们[0dm爬|] ≡respo∩5e.xp己t∩(!//div[co∩t日j∩5(0c1a5s’ °dr己‖a")]/p/text()!).extm〔t+jIst().5trjP()



」■■■‖』‖

ite‖[‖∩a爬!] =Iespo∩5e.〔55(! .iteⅦh2::te×t!)·extract+jr5t() ite川[,〔ategor1e5‖] ≡re5po∩5e.c55(!·〔ategorje5butto∩5p己∩::text|)。extra〔t()

yie1djt咖

这样我们就把每条新闻的信息提取形成了-个‖oγ1eIte"对象。

这时实际上我们就已经完成了Item的提取°再运行一下CrawlSpider: 5〔rapy〔Ia"1Ⅷγje

可以看到-部部电影信息就被提取出来了’运行结果类似如下: 202oˉ10ˉo912:31:03 [5〔mpy.〔oIe.5cr日peI]0[8[」C: 5c工aped十r咖〈2OOhttp5://55m·5cr己pe·ce∏teⅢ/detai1/6〉 {‖categoI1e5! : [!粤剧! ’ ‖金怕』’ 0古装! ]’ coγer, : ‖http5://pO.「∏eitua∩.∩et/们oγie/da6466o十82b98cdc1b8338O4e69609eO41108。jpg以64N6“h1e1〔|′ |dra阳a|: |唐伯戊(周星驰饰)身为江晌四大才子之首’却有道不尽的心酸·宁王想让庶伯戊帮其图谋作反’枚鹰伯戊 拒绝愈淫结仇.唐伯庇在与朋友出游时,遇到了戊若天仙的秋杏(巩俐饰)并对她一见仲′阶,决心妥到华府当隶丁以迫 求秋杏,唐伯戊枚取名华安°期间华太师遇到了宁王上门刁难,幸好有唐伯戊出面相助’并n邱了自己是鹰伯瓦的身 份·秋奋才知道华安足自已欣订的唐伯戊·华夫人跟鹰家有怨’ 因此二人使开始斗法°怎针宁王限夺命书生再次上门’ 华夫人不是对于’幸得鹰伯虎出于’华夫人也答应把秋杏许配给唐伯庇.|’ ∩己‖e| : !唐伯戊点秋杏ˉ「11rt1∩g5C∩O1己r』’ 0Pub115hedat‖ : 01993ˉ07ˉ01『 』 5core! : ‖9.5‖}

2o2oˉ1oˉo912:31:03 [5crapy.coIe.e∩gi∩e] 0[B0O〔r日w1ed (2oo)〈C[∏http5://55r1°5〔mpe。ce∩ter/detaj1/9〉 (re十erer: http5://55r1.scr己pe.〔e∩ter/)

但现在这种实现方式并不能实现可配置化’下面我们尝试将这个方法改写为ItemLoaders来实现°

通过addˉxpat∩` addc55、addva1ue等方式实现配置化提取°我们可以改写par5edeta11’如下所示: 、

de+par5edetaj1(5e1千’ re5po∩5e): 1oader=№vieIte"[oader(jt即=№γjeIte刚()’ re5po∩5e=re5po∩5e) 1oader.add-c5s(|∩a∏e|’| .it咖hR::text!) 1oader.add〔55(0categorje5‖’ 0 。c日tegorje5butto∩5pa∩::text0) 1oader.add〔s5(』〔oγer!’ ! .〔oγeI; ;日ttI(5rc)′) 1oader.addcs5(0pub1i5hedat,’ ‖ .1∩+ospa∩::text』’ re=‖(\d{q}ˉ\d{2}ˉ\d{2})\5?上映,) 1oader。3ddˉxpath(』5〔ore! ’ 『//p[〔o∩taj∩5(伙1己55’ ,‖5〔ore憾)]/text()|〉 1oader.addˉxpath(|dr日晌|’ ‖//d1v[〔o∩taj∩5(Qc1a55’ 』』draⅦa")]/p/text()!) yje1d1oader.1oaditeⅧ()

这里我们定义了_个Ite‖『‖[oader的子类’名为付oγ1eIte‖『‖[oader,其实现如下所示:



‖ ‖

d ‖







‖ q





{ q d

‖ 叫









(|

q」



十r咖5〔r己py.1oadermportIt咖toader +ro刚jte『∏1oader5°proce55oI5j"port丁冰e「1r5t’ 1de∩tjty’〔oⅦpo5e

5〔oreOut=〔咖pO5e(「a代e「ir5t()’ 5tr.5trjp) dIa阳out=〔α∏po5e(『a促e「ir5t()’ 5tr.5trjp)

泣里我们定义了4个字段,说明如下:

凸■司■■■■可·■■‖■■∏■■■‖』■曰■■|■■■■‖|

〔1a55"ovieIt印[oader(Ite肌oader): de十au1t-output-proce55or≡「冰e「ir5t() 〔3tegorie5out=Ide∩tity()

〗『■【■■『止■『〗}`≥□‖Ⅱ’|■尸|尸|卜‖『「|位■『卜|尸|}||》『卜■「■‖‖[尸卜|尸‖|■厂〖■伊$巴■【卜『■厂二■■●「『▲■‖尸’}|∩°卜‖|■「■′β`口‖β『◆∏》「广

l5』2

Scrapy规则化爬虫

823

□de千au1t—output-pro〔e55or:上文中’由于大多数字段需要利用e×tm〔t十jI5t方法来获得第

一个提取结果’而在par5e-deta11方法中我们并没有指定抽取第—个结果’所以最终的结果 仍然是_个列表形式°那eXtra〔t千1r5t方法对应的逻辑我们需要放到哪里实现呢?答案是需 要‖oγ1eIteⅦ[oader来实现°这里我们定义了_个de十au1t-output-pIo〔e55or’意思是通用的 输出处理器’这里指定为了「冰e「jI5t°这样默认情况下,每个字段的第一个提取结果就会作 为该字段的最终结果’相当于默认情况下每个字段提取完毕之后都调用了extm〔t千jr5t方 法°比如∩a‖e字段,原本抽取结果为[|少年派的奇幻漂流ˉ[j十eo「pi|]’经过丁a促e「1I5t处 理后’结果就是少年派的奇幻漂流ˉ[i+eO十pi。

□〔ategorje5-out:原本的提取结果是-个列表,而我们希望最终获取的也是列表,所以需要保

持原来的结果不变’而刚才我们已经定义了de十au1t-output-pro〔e55or来提取第_个结果作

为字段内容’这里我们需要将其覆盖’定义〔ategorie5-out字段’覆盖默认的de「au1tˉo0tputˉ pIo〔e55or’这里定义为Ide∩tjty’保持原结果不变° □5〔OreOut:使用默认的丁a代e「ir5t提取之后,结果前后包含-些空格信息,我们需要进—步

将其去除’所以这里使用了〔o们po5e’参数依次传人了『a代e「ir5t和5tr.5trjp,这样就能取 出第—个结果并去除前后的空格了。

.

□dIaⅧaout;和5〔oreout也是_样的逻辑°

好’这时候我们重新运行_下CrawlSpider,结果和刚才是完全-样的° 至此,我们已经实现了爬虫的半规则化。

6。配置抽取

为什么现在只做到了半规则化?—方面,我们在代码层面上使用了Rule将爬取逻辑进行了规则 化’但这样可扩展性和维护性依然没有那么强°如果我们需要扩展其他站点,仍然需要创建_个新的

CrawlSpider’定义这个站点的Rule,单独实现p日r5e-deta11方法°还有很多代码是重复的,如 〔raw15P1der的变量`方法名几乎都是_样的°那么我们可不可以把多个类似的几个爬虫的代码共用, 把完全不相同的地方抽离出来,做成可配置文件呢?

当然可以。那我们可以抽离出哪些部分?所有的变量都可以抽取,如∩a们e、a11oweddo"aj∩5、

5taItur15、ru1e5等°这些变量在CrawlSpider初始化的时候赋值即可°我们就可以新建—个通用的 Spider来实现这个功能’命令如下所示: 5cr日pyge∩5p1deI ˉtcmw1u∩1γeI5日1u∩jγer5日1

这个全新的SpjdeT名为unjversal。接下来’我们将刚才所写的Spider内的属性抽离出来配置成_ 个JSON’命名为moviejson’放到conhgs文件夹内,和spjders文件夹并列’JSON文件内容如下所示: {

卜■

"spjder": "u∩iveI5a1"」 "type": "电影"’ 叭ho爬"8 "http5://55r1·5cr己pe·ce∩ter/"’

尸 伊 广

005ettj∩g5": { "05[R∧C[‖丁": ‖0№zj11a/5·0(∩ac1∩to5hj I∩te1‖日c05风10ˉ12ˉ6)App1e‖e冰jt/537·36(长‖Ⅷt’ 11keCec促o) 〔∩ro|∏e/6o。0.3112.905a十ari/537.36"

|}}卜

}’ "5t日rtuI15": [ "∩ttp5://55r1°5craPe。ce∩ter/" ]」 "己11oweddo爬1∩50|: [ "5sr1。5cmPe。ce∩ter" ]’ "ru1e5": [ { 』011∩|(extra〔tor": {

↓ 卜

卜 」





第15章Scrdpy框架的使用

824

}’ { "11∩促eXtm〔tOr圃:{

』□∏‖

闪十o11叫口: true』

"〔a11baC促口8 口par5edetai1o

■■■凸■

"re5tmCt〔S5阅g 00.ite们·∩a眶口 }’





}’ "+o11酗曰: tn』e



当■〗|·凸□】‖勺|二■■

"IeStriCt〔55■: "·∩ext制





‖■

泣里我们将一些配置进行了抽离’第—个字段spider即Spider的名称,在这里是u∩jγer5a1°然 后定义了一些描述字段’比如type、home等说明爬取目标站点的类别、首页等° 然后就是一些重要配置了,比如可以使用semngs定义CmwlSpider的〔u5to‖]ˉ5ett1"g5属性,使 用5tartur15定义初始爬取链接’使用a11o"eddo"aj∩5定义允许爬取的域名’这些信息都会被读取 另外我们还将mles进行了抽离,配置为了JSON形式,是列表类型’每个成员都代表-个Rule的 配冒°进一步地,每个Rule的配冒又单独分离了1j∩《extraCtor并配置上对应的属性,比如re5trj〔tˉ〔55 代表Lin皿xUacmr的re5tr1ctc55属性。我们会使用rules字段的信息来初始化CIawlSpikT的Iules属性。

+r咖o5.pathmportrea1path’diI∩a眠’ joj∩ i呻ortj5o"

de「getˉco∩+ig(∩己爬):

■■■■■|□〗』』■口

文件并列’内容如下所示:



这样我们将基本的配冒抽取出来。如果要启动爬虫,只需要从该配置文件中读取’然后动态加载

到Spider中即可。所以我们需要定义—个读取该JSON文件的方法,新建_个utils.py文件’和jtems.py

』‖

然后初始化为CIawlSpider的属性。

path≡joi∏(djr∩a眶(rea1path(_+i1e_))’ ,〔o∩十ig5,’ 十’{∩a爬}。j5o∩′) "it∩ope∩(pat∩’|r|’ e∩codi∩g=0ut「ˉ8|)as千: retl」r∩j5o∩·1oad5(千.read())

定义了getˉco∩十jg方法之后,我们只需要向其传人JSON配置文件的名称即可获取此JSON配置 信息。随后我们定义人口文件runpy,把它放在项目根目录下’命名为mnpy,它的作用是启动Spjder’ 代码如下所示:



q

+ro∏‖B〔mpy。ut115。projectj呻oItget-project-5ettj∩85 「ro‖∏s〔rapy‖』∩iγer5己1de∩℃°ut115jⅦport8et≡〔o∩十ig 千ro∏| 5cmpy。〔raw1ermport〔mw1erproce55 加port己rgPar5e



P己r5eI=aIgparse.Argl』爬∩tpar5er(de5〔rjptjo∩=|0∏jγer5a15pider0) paIser.日dd-3rgu爬∏t(0∩a爬0 ’ ∩e1p=‘∏己爬o+5pideⅢtoru∩0) argS≡p日r5er.par5eˉarg5() ∩己‖∏e=arg5°∩3‖∏e ‖

de+Iu∩():

prOCeS5.5tart() j千

∩a爬

n」∩()

=≡

爬j∩

:

■■】■■□■■■■■】■]』·□∏°□勺

co∩千ig=get=〔o∩十jg(∩己爬) 5pjdeI≡〔o∩「ig.get(′5pjder,’ 0u∩jγeI5a1,) proje〔tˉ5ettj∩gs=get-pxoject—5ettj∩85() 5etti∩g5=d1ct(proje〔t-5ettj∩85.〔opy()) 5etti∩g5.uPd己te(〔o∩「ig.get(|5ettj∩g5,)) proce55≡〔mw1erproce55(settj∩85) proce55。〔ra"1(5pider’ **{·∩aⅧe : ∩己爬})



‖5.l2

Scmpy规则化爬虫

825

这里我们使用了argpar5e要求运行时指定∩aⅧe参数,即对应的JSON配置文件的名称°我们首 先利用get—co∩+jg方法传人该名称,读取刚才定义的配置文件°获取爬取使用的Spider的名称以及配 置文件中的se忱ings配置,然后将获取到的se忱mgs配置和项目全局的senings配置做了合并° 随后我们新建了_个CrawlerProcess,利用CrawlerProcess我们可以通过代码更加灵活地自定义需

要运行的Spider和启动配置,更加详细的用法可以参考官方文档:htms:〃docs.scrapyo『g/en/latesUtopjcs/ pmctjces.html◎

在unjversalpy中,我们新建~个 1∩jt

方法进行初始化配置’实现如下所示:

千I咖5〔mpy.1i∩促extIactor5j‖∏portlj∩k[xtr日ctor 十Io‖] s〔mpy·5pidersj呻ort〔raⅣ15pjder’ Ru1e froⅦ 。.ut115加poItget-co∩+ig 〔1a550∩iγer5a15p1der(〔ra"15pjder) ∩3Ⅷe= ‖u∩jγer5日1‖

■■】

de十

i∩jt-(5e1+’∩a爬’*arg5’**kW日rg5): co∩+ig=getˉ〔o∩+i8〈∩aⅦe〉 Se1「。co∩+jg=〔o∩十i8 5e1f.5tart l」r15=〔o∩十jg.get(0startuI15』) 5e1千.a11o眶ddoⅧai∩5=co∩于ig。get(』a11o眶ddo‖∏a1∩50)



rl』1e5= []

p [■厅‖『‖‖巴■∏】‖『‖伊□『‖卜〖■厂

「orru1eRwaIg5j∩co∩「ig.get(‖n』1e5‖); 1j∩促extm〔toI=[i∩促[xtractor(**ru1e灿arg5·get(01j∩代extmctor‖)) rl」1ek"己r85[|1j∩促extractoI‖] =1j∩促extractor ru1e≡Ru1e(**ru1e刚aIg5) ru1e5.appe∩d(n』1e) Se1千°n』1eS=ru1eB

5uper(0∩jveⅢ5a15pider’ 5e1f). j∩it-(*己rg5’**kwarg5)

在 1∩it方法中’我们接收了∩a"e参数’然后通过getˉco∩伍g方法读取了配置文件的内容, 接着将5tartur15、a11oweddoⅦaj门5` ru1e5进行了初始化°

其中ru1es的初始化过程相对复杂,这里首先遍历了ru1e5配置,每个ru1e的配置赋值为 ■■「}【【厂◎▲尸凡|‖■|『|[■「·|卜「卜||卜

ru1ekwarg5字典,然后读取了ru1e代"arg5的1i∩《extractor属性’将其构造为LinkExtractor对象’ 接着将11∩代extm〔tor属性赋值到ru1e代Ⅶarg5字典中,最后使用ru1e促waIg5初始化_个Rule对象° 多个Rule对象最终构造成一个列表赋值给CrawlSpider’这样就完成了mles的初始化。 现在我们已经实现了Spider基础属性的可配置化°剩下的解析部分同样需要实现可配置化,原来 的解析方法如下所示:

■[■■「‖【「■『‖【[》|尸‖||◆日≥『『||尸‖■厂■尸|■β|■尸|■■■|■尸■∩

de千par5edeta11(5e1十’ re5po∩5e): 1oader≡№γjeIteⅦloader(jt印≡‖oγieIte们()’ respo∩5e=Ⅲe5po∩5e) 1oader.add-〔55(!∩a们e! ’ , °jt即h2::text|) 1oadeI·addc5s(0〔ategorje5‖’ 0 .categorje5butto∩5pa∩; :text!) 1oadeI.add〔55(|〔oγer‖’| .coveI: :attI(5Ic)‖) 1oader°己ddc5s(′pub1j5∩edat! ’ ‖ °j∩+o5pa∩;:text』’ Ie≡‖(\d{4}ˉ\d{2}ˉ\d{2})\5?上映!) 1o己der.addˉxpat∩(!score‖′ ′//p[co∩tai∩5(0c1as5’ "5〔ore圃)]/text()|) 1oader.addˉxpath(』dra爬‖’ ‖//djv[〔o∩taj∩5(0c1a55’ "draⅧa")]/p/text()』) yie1d1oadeI.1oad1te们()

我们需要将这些配置也抽离出来。这甩的变量屯要有Ite"[oader类的选用、Ite们类的选用、

Ite『∏[oadeI方法参数的定义。我们将可变参数进行抽离,在JSON文件中添加ite川的配置,参考如下: { "5pider"; "u∩jγer5己1"’ 0





"ru1eS圃: [ ]’



,!iteⅧ"8 { 0‘C1a55": "№γieIte们"’ 001o己der00 : ■№γieIte肌o日der"’

"attr5":{ ∩a爬": [ { 00

闻爬thOd": "C55"’ "

己rg": ".ite‖h2::text"

} ]’

"〔ategoIje5°: [ { 00爬thod"8 "〔5500’

闻argα: 0‖ 。〔ategorje5butto∩5pa∩:目text" } ]’ "〔oγer甸: [ { 00

"们ethod": "C55 ’ ■0

日rg : 00

00

.〔oγer: :attI(5r〔)"

} ]」

"pub1i5hedat": [ { 『0

00爬thOd"8 00〔55 ’

"aIg圃: 剧°j∩千o5p己∩8 :text"』 "re阐: "(\\d{4}ˉ\\d{∑}ˉ\\d{2})\\5?上映" } ]’ S〔Ore": [ { "Ⅶethod阐: "×path"’ 00

!』己rg圃: "//p[〔o∩tai∩5(@〔1a55’ \"5〔ore\")]/text()" } ]’ "dIa∏a": [ { 00『∏ethod,‖; "xp日th『’’

"ar8阅$ "//div[co∩tai∩5(@c1as5’ \"dra们a\")]/p/text()" }

] ]》』

} }

注意’ite"的配置和ru1es是并列的°在1teⅧ中,我们定义了c1a5s和1oader属性,它们分别 代表Ite‖和1te阳[oader所使用的类°定义了attr5属性来定义每个字段的提取规则,例如’t1t1e定 义的每-项都包含_个们ethod属性’它代表使用的提取方法’如xpat们代表调用ItemLoader的

∏||』□」■■■‖】』‖·‖|■】‖』日』」■‖‖·」■‖』□(』■Ⅷ‖·』□】』■‖|」■■】』∏||《|√日刘□■刮‖■■‖|■】《·■刁」】‖|■■·司‖·』·■】尸‖刊‖■■∏‖■|‖□』·‖■■

Scrapy框架的使用

第15章

826

addˉxpat‖方法° arg即参数,它是add-xpat‖方法的第二个参数’代表的是XPath表达式°另外针对 正则提取,这里还可以定义-个re参数来传递提取时所使用的正则表达式。

我们还要将这些配置动态加载到par5edetaj1方法里’实现par5edetai1方法如下: i千jte们:

‖‖‖

de千p日r5edet3j1(se1+’ respo∩5e): jte‖=5e1「.〔o∩十j8.get(‖1te"‖) 〔15=8etattr(jte们5’ jte们.get(,C1a55|))()

1o己der二get日ttr(1oader5’ jteⅦ.get(01oader』))(〔15’ re5po∏5e≡re5po∩5e) 千OⅢ氏ey’γa1uei∩jte们.get(』attr5‖).iteⅦ5(): 十Orextm〔tor1∩γa1ue: b

Q 」 妇 ■ ■ 〗 ‖

i+extmctor.get(| 『∏et∩od0)≡xpath|: 1oader.日ddˉxpath(代eγ’ extmctor.get(0aⅢg|)’ **{』re』: extr3ctor.get(0re0 )}) j「extra〔toI。get(!‖ethod0)=〔55 : 1oader.addc55(促ey’ extra〔tor·get(‖arg0)’ **{‖re0 : extractor.get(‖re‖)}) i千extractor.get(0爬thod‖〉 ≡≡ !γa1ue! :



‖』‖

l5.l3 Scrapy实战

827

1oader.addˉγ己1ue(促ey’ extmctor.get(0aI850〉’**{|re』: extra〔tor。get(|Ie0)})

yie1d1oader.1oadˉjte"()

这里首先获取Item的配置信息,然后获取C1a55的配置’将Item进行初始化°接着利用ltem再 初始化ItemLoader,赋值为1oader对象。

接下来我们遍历Item的a仗rs代表的各个属性依次进行提取。首先我们需要判断们ethod字段,调 用对应的处理方法进行处理°如Ⅶethod为c55’就调用ItemLoader的add〔55方法进行提取°所有配 置动态加载完毕之后’调用1oad1te‖方法将Item提取出来. 至此’Spider的设置、起始链接、属性、提取方法全部实现了可配置化°

这时候我们就可以使用配置文件来启动CrawlSpider了,运行命令如下: pytho∩3Iu∩.py咖γie }止■厂『‖「|巴●●『‖『|[【ˉ■百|||||匹■’「β▲■了》■■尸■厂~■■『任■【‖「}|卜′卜|||[=■∏||■》『■‖||[尸‖|皿■『[「

运行结果如下: 2020_10ˉO918:42:24 [5crapy°〔ore.5〔mper] 0[8UC: 5〔raped十ro们〈20O∏ttP5://55I1。5cmPe.〔e∩ter/det己i1/6〉 {0〔己tegorie5』: [|各‘||‖ ’ ‖爱册‖’ |古装0]’

0〔oγer|: ‖∩ttp5://pO.‖e1tua∩.∩et/‖∏oγie/d己6』660千82b98cdc1b8a38O0e696o9eO411O8。jpgα64贮6“hˉ1eˉ1〔』’

|draⅦa0 : ‖唐伯戊(周星驰饰)身为江俞四大才于之首’却有近不尽的‖迷酸。宁王想让唐伯庞帮其图谋作反,彼鹰伯戊 拒绝怠淫结仇°唐伯虎在与朋反出游时,遇到了貌若天仙的秋奋(巩俐饰)并对她一见仲情,决心矣到华府当索丁 以迫求秋杏,唐伯虎枕取名华安°期间华太师遇到了宁王上门刁难,伞好有唐伯虎出西柯助,并暴红了自己是唐伯 庇的身份°秋杏才余口道华安是自己欣贫的唐伯戊·华犬人跟唐家有怨’ 因此二人使开始斗法°怎朴宁王跟夺命书生 再次上门,华夫人不是对于,伞得唐伯虎出子,华犬人也答应把秋谷许配给唐伯庇. 0 ’

0∩a『∏e0 目 0唐伯戊点秋谷ˉ「1iIti∩g5〔∩o1日r! ’ 0pub1j5∩edat! ; 01993ˉO7ˉO1‖ ’ 05〔oIe‖ : 』9.5』}



可以看到爬取结果和之前也是完全相同的’抽离规则成功!

综上所述’整个项目的规则化包括如下内容。

□5pider:指定所使用的Spider的名称。 □5ettj∩g5:可以专门为Spider定制配置信息’会覆盖项目级别的配置° □5tartur15:指定爬虫爬取的起始链接。

□a11o"eddoⅦaj∩5:允许爬取的站点° □ru1e5:站点的爬取规则。

□jte∏:数据的提取规贝‖°

●「`■|卜「|′》「|β|尸『||卜|■■「|‖止■【‖|[■「巴「「|》』广‖||■【■厂■■「||■■■|■广

到现在’就可以灵活地对爬取逻辑进行控制了° 7.总结

当然’本节仅仅是示例’主要介绍规则化爬虫的配置和抽离规则的基本思路°更多更复杂的配置 大家可以举_反三’灵活处理。

我们既然已经将配置抽离成JSON格式的文件了’那么就可以将这些配置文件的内容存到数据库 中,然后对接可视化配置’这样我们就可以更加方便地管理爬虫项目了° d■■■

本节代码参见: https:〃githuhcom/Python3WebSpider/ScrapyUnjversalDemo。

↑5 k

↑5.↑3 Sc『apy实战 通过本章前面几节的学习,我们已经了解了Scmpy的基本用法`规则化爬虫` JavaSc∏pt喧染页

面的爬取,而且在之前的章节’我们还学习了运用代理池、账号池等规避反爬措施。本节中我们就来 综合—下前面所学的知识,完成_个Scrapy实战项目’加深对Scrapy的理解°

卜卜 卜 =

|‖」□

二■‖

第15章Scrapy框架的使用

■■日

828

↑.本节目标

||

曰5°口.·

^〃

一=

露 V

…文一■气■

…■■…■■■ 《BIm砧疆!=

…■





.=色





隆 ≡

p巳∏父…

■■与m

吐巫…

任几H■■

…贝示●■■丙@

碑骑啪■…



旷_

刃只红↓啦女●刃r

■p^■孕儿■■■■ …■……≈

…m

■■0≡



:

……们‖)

m`巴…中

些哆呻

啦何■二■Q■=

螺蘸坚/■■〕嘿,箩娜



登录后的页面

近一万本°

不过,这个站点设置了_些反爬措施。它限制单个账号5分钟内最多访问页面l0次’超过的话 账号会被封禁;另外该站点限制了单个IP的访问频率,同样是5分钟内最多访问l0次’超过这个频

率, IP便会被封禁。因此,该站点从账号层面` IP层面都做了限制’如果仅用一个账号和一个Ip,那 么在短时间内是无法完成爬取的。

总而言之,这个限制算是相对严格了,要爬取这个站点’我们就需要结合之前的知识综合实现。 现在主要面临两个问题°

□封禁账号:前文我们已经讲解了账号池的用法’利用账号池’我们可以采用分流策略大大降低

■■‖{□■‖‖刘|·■■∏‖日〗■|■·』■‖|‖‖』

这里是_些书籍信息’我们需要进人每-本书对应的详情页,将该本书的信息爬取下来’总数将

司 ■

图l5ˉ2l

0"酗盂 βvm伊o

』田

■司·■」可||

●人矣宁叮耙·

、,瓣′ $ 。恐‖





-





↑砖§婶■…巾凹●陆

产稗

嚼`

`獭· m之仕

=.

3∏)m◆户0

q

■■Ⅵ口■

『霍…

只&Ⅳ大凹





贤腮



■■





「■

□■■】■■■巧$Ⅸ●口‖〗『■β■口【巳凸Ⅶ』■日■《伊∏护ˉ■ˉ■】■■■『■【巳■■铀锣





{‖

印★△白●

●一7~=宁.=■

{|□

-=≡

+△◎

|·

本节我们需要爬取的站点为https://antisplder7.scrape.center/,这个站点需要登录才能爬取,登录之 后(测试账号的用户名和密码均为admin)我们便可以看到类似图l5ˉ21所示的页面。

单个账号的请求频率’从而降低账号被封禁的概率。

□封禁|P:前文我们已经讲解了代理池的用法’利用代理池,我们可以维护大量IP’每次请求

都随机切换—个IP,这样-来就可以解决IP被封禁的问题° ‖

因此,本节我们需要实现以下几点功能。 ‖

□对接代理池’突破代理访问频率的限制°

2准备

在本节开始之前’请确保你已经安装好了Scrapy框架’并准备好了前面章节所讲解的代理池和账

|付□‖

号池并可以成功运行’具体说明如下:

』∏」司||‖■]叼|||□■」■‖|{』】∏‖‖』□■■|

□利用Scrapy实现站点的爬取逻辑° □对接账号池’突破账号访问频率的限制。

·∏』=叮|||■■]〗

8乙9

Scrapy头酗

l5°l3

□对于代理池’可以参考https:/′gjthuhcom/Python3WebSpide【/PmxyPool里面的说明来安装’具 体的原理和实现可以参考本书92节°

□对于账号池,可以参考https://githuhcom/Python3WebSpideI/AccountPoo‖里面的说明来安装, 具体的原理和实现可以参考本书1O4节°另外注意本节我们需要基于1O4节所述的内容对账 号池进行进一步改写,所以建议下载antjspjdeIb这个分支的账号池代码’可以直接使用如下命 令更新对应代码:

gjt〔1o∩eˉˉsi∩g1eˉbr3∩chˉˉbra∩〔ha∏ti5pider6∩ttps://git∩ub.〔o『∏/pytho∩3№b5pideI/∧〔〔ou∩tpoo1°git

这样下载的代码就是antispider6这个分支的代码’本节我们需要基于它来扩展antjspider7这个 站点的账号池逻辑° 〖尸【「}▲尸仆||

3.分析

首先我们来分析_下这个站点如何来爬取,直接爬取页面还是利用Ajax接口?

数据是通过Ajax加载的,接口如图15ˉ22所示°



一亢◆夕》△■‘勺Ⅱ叮

| ■叼□$≡△凸↓°引=







……







蟹磅的…三二四患

■』》』』■忽毛

~已《『『占□□占叮$β铲『$之凸

『□省■『户_

锤口 …

■∏‖‖}广『卜■‖【■『|「◆『》仿|坠尸||■「

打开https:〃antispjder7.sc田pe.centeⅣ,首先页面会提示需要登录,我们可以先使用用户名admin’ 密码admin登录°然后分析页面的呈现逻辑,综合前面的知识,我们可以轻易分析出来每—页的列表

≡=≡:`』Ⅱˉ≡h辩≥蹿鳃2=!攀曰l勇愈…蟹冀基二二二豆累廷2蹿 ˉ” , ˉ `≡ˉ{R箩w翅.=

ˉ|

.….ˉ胸岛’=………、{-ˉˉ

‖…m占吐t”0〃mt』Zp1“「了·■cr■酝·唾吭e〃呻』/b…/7〖mix■】…行■哎绚

……~

{←…距丁



酌●■…矽n锄

!哇≡~80·73‘82·2屿:“D



!…呵wp…"…fG…





~ ~



≈宁●■=~罕■▲≈●≈■●■■■≈■■■已■■=唾~≈~≈■吼寻牡■吟垫■■心

▲讳元■色

■■D≈己■巴■→≈亡■■■■▲巾●△…■≈■■铃■链■

■■≈■

《v~…



i…座L眶了介蹿凹’0pTT呻



| 四西击■园…啄9z№ 』→乙即u蚀t』m/j3呻

卜 p

…酗tO2■吮t趣·“〗20953田丁

p

…2■

…吨l』阻/】◇】了o8

{蜜腻:麓……酶’…』mc`o……n5 』…AC…t=臼lc鲤延g

| |





卜 b

0



0 出

0

||

…庐0r吨m

||

≯∩……5…m『倪vu

图15ˉ22分析页面的呈现逻辑 \

接着切换到PI℃view选项卡看返回结果,如图15ˉ23所示。 x 卜…D ∩…·…洒m…咖曲叮… 守■=7



≈■



≈≈七乙唾啡中

■b

=←

▲化■





琶尸

. ″

.嚣 !

…户…▲▲=磊峭啥~…好哺佣邮<电





《 v《c@mt89a佣0吨B叭t乞〖 [《m: 铡2…76□3饲p ∏…; 卿中■文呻闭■■待辑0凶t№恫1Fq砸韶“10』’■l》 …t80】的

●m酗lt£吕 [《1d【α2“·7“驴0 ∩…8『D…化屿…“,翻tm「巴8 [zu…仰】$=}9■] ◆0t{m: o·2…了酗罗’ ∏…B 遍中■支O山■…0 ■ut№晤〖 [●…】·=》 v酗t№丁臼〗 [叮吧乙褂‖ ·〖 饵…卯



CDv■r0 馋惟tp$〗〃纯9.do山m1O°c回V』到E啤』●CwU…uc/■Zm】“田越j呵 mγ ■2呻了“3∏

…: 巴牺文…■… £cO陋8 ■8口3m

·

p】5《m8 盈蹿nwS2坤·∏=0……■鸥翱陋§.5■, ■utm晤0 [鳃!日】■■w′叶[曰]……注,0]‘■》 户28 《皿8 ■2“0】417伪p…〗 旬p·Zo丁·父m簿西pmth@z8 【坷…·…]p=} 卜3:《m: "2w0渔钝岭· ∩…: 蜕■屿mU ■喊恼『Z‖[凹凸…]≥酗印■丙,…俐L←》

叫〖 《1d8 ■】6…“】n0o ∩…: ■仕变≈峨p ■MthO向5 [斌l羹]马毋.硒■№tt…0、儿1曲e四∩)■]0≡》 户50 《m目 恤2韵7瓣10阅口…: ■…犬晦p酣t∩Omg 【刨《E》m棒铲lo=》

图15ˉ23切换到PI它view选项卡



b ■

第15章Scrapy框架的使用

●■■

…晌Ⅷ0〗钮

司‖

作■油■

■、



m.“"天

■]|‖■|



中闺丈化

\◆

…磅粒会α津宁人与粒尸■田一…一一

‖■■●■■■■…=



深层结构

,▲8°3中■文化的瀑儡熔拘 ■汁…介】◎″…唾巴α宁▲包怀6■骗儡征…●入亦三鲍文化剪》…洒臼∏蛔名■妇文●■

』·■□』■■■】□

回s画。,.

勺‖

这里我们可以看到返回结果只包含了jd、∩a"e、 5〔ore、〔oγeI、 aut∩or5这几个字段’明显还不 全°我们进人书籍详情页面来看—下,例如进人https://antispider7.scrape.centeⅣdetai‖26607683这个页 面,可以看到更全的信息’如图15ˉ24所示°

二■■■■■□■■■‖

830



山吐…哇≈ ■n四 …吨】…凹Ⅶ

‖|引 {

|评价 辽工■啦■

图15ˉ24书籍详情页面 ·

x 卜…P……碑

c■taI叼』 凹材

娜靶缸γ赋叼

O…

【{m〗 ,′m7m70576闻0 〔◎∩te∩t8 ,a这本书m■00}o {』d; 必鳃13】犯M,,,=》′■]p=》

新干年贩犀剧

倔t『Ⅷ序■

用=殷序■

■一■0谚

(=》蚀从一个俩的■度扫中霹文化°」

(二

仆2; 《1d日 蝇】】67017四铲0 CO"t印t【 醉反旦,韩,…Z■理序…唾唾啥ˉ 卸》

■■

卜38 【M; 斡〗177217”了■』=} 》0目{M: "】mQ77酚57·d≈} 》S8《m: “】“%3▲57铲o→}

|‖

寸c…t∑: [{1d吕 闺2z7晒74576闽g c酚t印t〖 脚这本袒曲■饵}′ 《m: 印9g正1沁〕q晦0=》』=】 份08 《1d『 凹227897qS76.′ 〔mt●∩t: 泌这卒书耍取盅伺} P18 {1d日 ■9g咀E…肉o=》

■■‖·■■

守{1q: ,0266w683闻° f受∩tS: 《M: ,0266w683闻′ 〔…∩tS: o■ut帕屈〖 [60仲蛀■"]

二■|□∏■■■

这里我们可以看到还有标签、定价、出版时间、ISBN、评价等内容’分析其数据来源发现这些也 是通过Ajax接口加载的’如图15≡25所示°

尸6!{M:『02四7了Q闽75幽j c卸t■"t日 撼0卜拯扭杜盎料学任m迅鸥钮设王■之后的…话■问的佃沿∏旦,逼当■澜不好么田不泪评■∏所忠及的“》 p78 《坦: 傅22w08576】凹’』 ◆8: 《蛔目 蜘2四〕的3吕8驴’ 〔mte∩t8 "砂蚀·●》 pg【 《坦〗"】2刀Sq“〕3图’〔呻te们t§ w收疆很p一右∏Ⅷ巧不而■簿攫‖ ■}

∏…: "蛔文睦■■…睡 p■驴=∩…e厂8 Q亚 p「江e日 铜“.韶元匈 p山1】S№d■tg 切2·】5=11=2们】6:00邑曲Zw pubus№厂$ 僻中■出庙仕c0 ■[O「e8 008·3凸

仁t吨巳g [00…凹o ,0中田文化凶B ,0文化灿p 哩蛔■0’擒文化旧究"0 "历史"’ 凹唾"0 ■oH阅l t了■∩5恤to盾〖 []

图15ˉ25

」】|■

↓』pd日tGd3t: 钟2020剖3■21Ⅶ7〗q601∑oγ774732" u了1: 二htt郎『//…点.“ub酗.〔咖/驰bjeα/266·768〕/■

||』■可‖|·■■∏|』■』』■■〗‖|■■■』●■可

m: 的2硒7棚〕山

mt「□0OCt1o∩8 向【内臼■介】…乙先生虽贝疽名之作0全闲毋tγ腋阀峰入木三分的文化肛列0 80矩↑cm识Ⅳ的君名战斗m●■疆…中■文化语阻下的口巴京与‖ 巫0∩2 80970乃…唾T11■

』 日 Ⅵ

亡oγe厂: 00∩ttp■g//j闯9°do呻mio.C酮/γ1团/$Mbje亡t/1/p吨uc/雏831“86镶jpg白

Preview选项卡 q

到现在,爬取逻辑就已经梳理清楚了°接下来我们看看怎样解决权限的问题’分析一下Reqeust Headers,可以看到有_个aut‖orizatjo∩字段’以jwt开头,内容类似下面这样:

·{‖‖{

因此,要爬取全部数据,我们需要从列表页接口获取书籍的ID’然后根据ID从详情页接口爬取 每-本书的详情°



{|| α■「|巴■厂}|尸||‖■「β『广‖●尸|[〗【■『‖■巴■厂『β■【■『『「[△尸[卜

l5.13

Scrapy实战

83l

a‖thomzatjo∩: jwtey]0eX∧j0j〕氏γ1Qjl□h加〔j0i〕I0zI1‖j〕9.ey〕1〔2γγX21低Ijox[□1c2γym「tI5I6IⅦ「比"1uIi"iZX∩" Ijox‖j∧z‖『γ酬D[z[□1b‖「pb〔I6I"「恨b‖1u卯「比付1u[耐γb5I5I耐y己‖d+己‖「0Ijox‖j∧z‖丁[2"[z句.I「1w【Ct』1e"℃]yα↓ˉ刚vl2 py‖4dp8Bzq〔x〔ˉ1I8jˉ6I

另外RequestsHeaders也有coo代1e字段,里面包含了SessionID相关的信息° 为了验证其认证方式’我们可以对author12at1o∩和cooR1e进行删减测试’比如去掉cookje字

段,仅使用aut‖Or1Z己t1O∩仍然可以成功获取数据’那就证明其权限认证需要autbOr1∑日t1O∩字段而不 _定需要CoO代je°

凸■■尸』■‖■|′■∏●巴■卜△卜『仑『|■尸「||‖『’匡■■『■厂■『■■【》》卜匣‖尸卜伊『》}∩》巴尸■巴■》′△尸‖忙『

最后经验证可以得到,其权限认证是基于JWT的,我们仅使用aut∩orixat1o∩就可以成功获取数 据。如此一来,权限认证我们就大体清楚了。 接下来我们就来实现该站点的爬取流程吧!

4实战

下面我们就开始利用Scrapy实现对示例网站https:〃antispider7.scrape.centeI/的爬取了。 ●主逻辑实现

首先我们可以利用Scrapy实现_下基本的爬取逻辑’首先新建一个Scmpy项目,名字叫作 scrapycompositedemo’创建命令如下: 5cmpy5tartproje〔t5cmpy〔m甲o5itede咖

接下来进人项目’然后新建一个Spider,名称为book,命令如下:



scrapy8e∏5pjderboo代a∏tj5pider7.5cmpe·〔e∏ter

然后我们来定义一个Item’定义需要爬取的字段,这里我们直接和详情页接口返回的字段_致就 好了’在itemspy里面定义_个Boo代Ite阳’代码如下: +I咖5cmpyi呻ort「ie1d’Ite" C1a558oO低IteⅦ(Itm): author5=「ie1d()

〔ata1o8=「ie1d() Cα门mt5=「ie1d() Coγer=「ie1d() jd=「je1d() i∩trodu〔tio∩≡「ie1d() j5b∩=「ie1d() ∩a爬=「je1d()

pageˉ∩‖油er≡「1e1d() pri〔e≡「je1d() pub1j5hedat≡「ie1d() pub1i5her=「ie1d()



5〔ore=「1e1d()

tag5=「je1d() tra∩51atoI5=「ie1d()

匹■尸■■〖厂■■{尸

定义好8oo代IteⅦ之后’我们便可以将爬取结果转换成一个个8oo促Ite"了°

然后我们来实现主要的爬取逻辑’这里我们先直接实现爬取Ajax接口的逻辑’在bookpy里面改 写代码如下:



十r咖s〔mpy1呻ort佣eq‖』e5t』 5pjder ■尸》》‖



〔1a55Book5pjder(5pjder):

.

∩a『爬= !boo低0

a11oweddo‖m∩5≡ [|己∩tj5pjdeI7。5〔rape.〔e∩ter!] ba5e0r1= |https://a∩ti5pjder7·scrape°ce∩ter0 ‖ax-page=512

‖ ◆ 「 ■ = ■ ∏







7

第l5章Scrapy框架的使用 de+5t己】tˉreque5t5(5e1千): 「oIpagej∩ra∏8e(1’5e1千。晦x-P日8e+1)8 ur1=十,{5e1f.ba5e一ur1}/apj/book/?1i川it二188o仟5et={(pageˉ 1)*18}′ yje1dReque5t〈ur1’ 〔a11b己c促=5e1千.p3I5ei∩dex) de十par5ej∏dex(5e1十’ re5po∩5e): Pr1∏t(re5po∩5e)

这里我们构造了512页的列表页Ajax请求,指定1imt和o仟set参数, o仟5et根据页码动态计 算,构造URL之后生成Request,然后回调方法设置为par5ej∩dex方法,打印输出re5po∩5e对象° 这里我们仅仅是实现了基本的请求逻辑,并没有加任何模拟登录操作,运行结果会是怎样的呢?

|}|

832

我们来尝试一下,运行该Spider,命令如下: 5〔rapy〔raw1book

我们会得到如下的运行结果: 2O20ˉ10ˉ2419826:O4[5〔rapy.〔ore.e∩8j∩e]D[BlL:〔r日"1ed (401)〈6[『http5://a∩tj6pjder7.5〔I己pe.〔e∏ter/己pj/ s〔rape。〔e∏ter/api/book/?1i∏nt=1肥o仟5et=o)吕 ‖丁『pstatus〔odei5∩otha∩d1edor∩ot己11…d

2o2oˉ10ˉ2419:26:o4[5〔mpy。core.e∩8i∩e]0[8Ⅶ:〔m阑1ed(』o1)(C[丁∩ttp5://己∩ti5pjder7.5〔mpe。ce∩te【/己pi/ booR/?1加it=1酗o仟5et=1“〉(re千erer: ‖o∩e)

可以看到状态码都是40l,而401就是代表未授权的意思’这就是因为没有登录造成的。 ●模拟登录

前面我们也已经分析过了怎样以登录身份请求接口’其实就是在RequestHeaders中加上 authori2atjo∩这个字段就好了,怎么来实现呢?在前面我们也已经学习了DownloaderMjddlewm℃的 用法,它可以在Request被下载执行前对Request做一些处理,所以这里我们可以借助于Downloader Middleware来实现。

接下来我们在mjddlew缸espy里面添加_个DownloaderMiddleware’代码如下: 〔1as5∧uthorizatjo∩‖jdd1甜are(obje〔t):

authoIizatio∩= ‖jwtey]oeXM0j〕Nγ10i[〔〕∩bCcj01〕I0zI1‖j]9.ey〕1〔2γyX21促Ijox儿□1〔2γy咖「tZ5I6IⅦ肚刚1uIjwj ZX∩wIjox‖j∧z‖Ⅳ咖[2L□1b‖「pb〔I6I‖于kb‖1lⅨ毗b‖1ul耐vb5I5I昭ya‖d十a肝OIjox‖jAz‖『[2"[z旬.I「1wRCt41e‖D〕y叫ˉR "v[Zpy‖4dp8Bzq〔xCˉ1I8jˉ6r

de于proce55ˉreq0e5t(5e1「’ reque5t’ 5pider); reque5t°header5[|authorizatio∩0 ] =5e1十.aut‖orjzatio∩

这里实现了一个∧uthor1zatio∩"idd1eware类,并实现了一个pro〔e55-Ieq0e5t方法’在这个方法 里,我们为reque5t变量的∩eader5属性添加了aut∩orizatio∩字段°注意这里authoIizatjo∩的值

你可以改写成自己的’当前的authorjzatjo∩可能已经无法使用了’请登录站点并分析Ajax接口,复 制author1Ⅲat1O∩字段并替换°

定义之后我们还需要开启对∧l』t∩orjzat1o∩∩jdd1eware的调用,在settingspy里面添加代码如下: 刚‖l0∧D[R‖I卯[[肌R[5≡{



,scr己pyc咖po5itede‖℃.‖jdd1eware5.∧uthorizatio∩"jdd1酗are0 : 543’

好’这样我们就开启了Autho∏mtjonMiddleware了’重新运行_下Splder’可以看到如下结果: 2o2oˉ1oˉ2419:39:56[5〔mpy.core.e∩gi∩e] 0田邯:〔m"1ed(2")〈6[丁

bttp5://a∩tj5pider7.5〔mpe.〔e∩ter/api/booⅨ/?1imt≡1肥o仟5et=36〉(re+erer: ‖o∩e) 卫020ˉ10ˉ2019:39:57 [5cmpy.〔ore.e∩gj∩e]0[Bl几:〔m"1ed (2o0)〈C[丁

∩ttp5://a∩tj5pjder7.5crape.ce∩ter/api/boo代/?1imt=1肥o仟5et=162〉(re千ereI: ‖o∩e) 2o20ˉ10ˉ2419:39:57 [5crapy.core.e∩gi∩e]0[8(几:〔m"1ed(4o3)〈6[丁

』■Ⅷ■■□划勺■■|≡日二∏·』■|=·|‖刚‖‖」·司』司』‖|‖●】Ⅵ‖|」勺□|』勺』·|||□】』|』』·」■〗』■‖』■』‖■‖■』可|』■□|」■】■]‖‖』■】』■‖|』』]●〗‖|‖‖··■||』』】■‖』■■」■·]‖■■

book/?1jmt=1朋o仟5et=o〉(re「erer: ‖o∩e)

∑OZoˉ10ˉ2419:26:o4[5cr己py.5pidemidd1eware5.httper工or] I‖「O: Ig∩orj∏gre5po∩5e〈』01∩ttp5://己∩tj5pjder7。

卜「|》『|||●∏‖但■厂‖





l5』3

Scmpy实战

833

http5://a∩t15pjder7。s〔Ⅲape.ce∩ter/己pi/boo促/?1mjt=188o仟set≡18〉(refeIeI: ‖O∩e)

2020ˉ10ˉ∑419:39:57 [5〔rapy。core.e∩gi∩e] D[Bl」C:〔ra"1ed (403) <C[丁 bttp5://a∩ti5pjder7.5〔rape.〔e∏ter/apj/book/?1iⅧjt≡18&o仟5et=252〉(re+ereI ‖o∩e〉

不幸的事情又发生了,最初的几次请求结果的状态码是200’代表爬取成功’说明模拟登录已经

成功了°可是后续的请求状态码又变成了403’403代表禁止访问,其实这就是因为爬取频率过高’ 当前账号或IP已经被禁止访问了’因为这个站点有IP和单个账号请求频率限制°

广}卜》■∩佃卜~●『■「『止「尸》



出现现在这个情况’如果我们不知道当前站点的反爬策略,一般得经过一些实验来找出来其中的 封禁规律°比如这时候可以通过_些控制变量法的实验来进行验证°

□如果想验证是不是IP被封禁’我们可以尝试更换当前计算机的IP或者使用代理来更换IP重 新进行请求,如果这时候可以正常请求了,那就证明是IP被封禁了° □如果想验证是不是账号被封禁,可以尝试更换账号重新进行请求’如果这时候可以正常请求了, 那就证明是账号被封禁了°

所以_般在不知道封禁原因的情况下’可以多进行尝试°在这里我就不再进行尝试了,这个站点 就是既封禁IP,又封禁账号’有双重反爬。

好,那我们就来一个个解决吧。 ●解决封IP问题

匹 ■

在这甩我们重新将前面所讲的代理池运行起 来’代理池运行之后’便可以通过URL来获取一个



●@|oca‖打oSt:6555/「a∩doⅦ

×





÷÷G●‖◎ca|host:5555/馏『‖创◎汛

随机代理’例如访问h即://localhost:5555/random就

‖ 厂

可以获取一个随机代理’如图15ˉ26所示°

!40。↑4a‖42·2Oo:]o8o

尸 『

这样我们就可以把该代理对接到爬虫项目中

■ 『

了°我们可以再实现一个DownloaderMiddleware,



图l5ˉ26获取一个随机代理

实现如下:

尸 》

mportaiOhttp j‖port1oggi∩g p

〔1a55proxy∩idd1出are(obje〔t):

pIoxγpoo1-ur1= 0∩ttp://1o〔a1∩o5t:55S5/m∩d咖|

1o88er=1og8j∩g。get[ogger(0mdd1eware5。proxy!) ■厂「■卜‖仆■『』尸■■

a5y∩cde「pro〔e55-reque5t(5e1十’ reque5t’ 5pjder): 日5y∏〔倒ithaiohttp.〔1je∩t5es5io∩()a5〔1je∩t: re5Po∩5e≡a"ait〔1je∩t.get(5e1+.PrOxyPOo1一u【1〉 i+∩otre5po∩5e。5tatu5==2O0; IetuI∩

P ■■‖【巴■‖■厂炉『‖△■仿尸『■厂|怔◆「■■■■尸「■■■「||■■尸‖■■∏『

proxy≡awajtre5po∩5e.text() 5e1十.1ogger.debug(「|5etproxγ{pIoxy}!) reque5t.|∏et日[`proxy』] ≡十|∩ttp://{Proxy}』

这里我们实现了一个proxy∩jdd1e"are,它的主要逻辑就是请求该代理池然后获取其返回内容, 返回的内容便是一个代理地址。接着我们直接将代理赋值给reque5t的们eta属性的proxy字段即可° 值得注意的是’由于Scmpy20及以上版本支持asyncio,所以这里我们获取代理使用的是a1o∩ttp’ 可以更方便地实现异步操作,可以看到我们给proce55=Iequest方法加上了a5γ∏c关键字,这样在方 法内便可以使用asyncio的相关特性了°

为了开启Scrapy对asyncio的支持’我们需要手动配置一下ⅧI5丁[0R[∧〔『0日,在settingsPy里面



第l5章Scrapy框架的使用

834

添加设置如下: ⅧI5『[DR[∧〔『0R= 0twj5ted°i∩ter∩et.a5y∩〔joIeactor°Asy∩cio5e1e〔torRea〔tor|

接着我们再开启proxy‖jdd1eware的调用,配置如下: 刚‖[0∧D[R甘I卯[[趴R[5={ 05〔rapyc咖po5itedem.们idd1ew己re5.∧l』t∩oriz日tjo∩"idd1eNaIe0 : 543’ 05〔r己py〔α∏po51tede‖℃.mdd1e创aIe5.pro×γ‖idd1e切are0 : 5“’ }

解决了° ●解决封账号问题

不过这样可没完,实际运行仍然会出现403状态码,这是因为账号也被封禁了°为了解决账号封 禁的问题,我们需要进—步对接_个账号池°

关于账号池的原理’就不再过多叙述了°接下来我们需要基于lO4节的账号池对该站点进行扩展, 使其可以应用于目标站点°

首先我们需要在settjngpy里面修改配置,改成antjspjder7站点’改写Generator和T℃ster的类的 配置,修改如下: C[‖[【∧『0【肌p={ 0己∩tj5pideI70 : 0∧∩tjspjder76e∩emtor0 }

丁[5『[βⅧp={ }a∩ti5pideI70 : 0∧∩ti5pider7∏eSteI』’ }

丁[5「0R[趴p={ }

°a∩ti5pjder70 : ‖http5://a门ti5pjder7·s〔rape°ce∩ter/3pi/boo促/?1j‖jt=18&o仟5et≡o!

这里∧∩tj5p1der7Ce∩erator就是负责当前站点模拟登录的类,A∩t15p1der7丁e5ter就是负责当前站 点测试登录的类’我们需要分别实现一下对应的逻辑。

接下来我们在generatoⅢpy里面定义∧∩tj5p1der7Ce∩erator,实现如下:



‖』‖|‖』■■□‖‖‖‖』■■∏』□〗■■·】‖‖|·□口勺■‖‖■Ⅵ‖‖‖‖』·‖■」■|‖‖□可司刮{凸■■可||‖‖●』||」■】叫||』』■】勺|

好,这样我们就可以在每次发起一个请求的时候随机切换代理池中的代理了, IP被封禁的问题就



mportrequest5 O

de「ge∩emte(5e1「′ 05er∩日爬’ pa55woId): i+5e1+。〔rede∩tj己1ˉopemtor.get(u5er∩a眠): 1ogger.deb0g(「,〔rede∩tj己1o「{user∩aⅦe}exi5ts’ 5代ip!) retur∩

1ogi∩-ur1= 0∩ttp5://a∩ti5pjder7。s〔mpe。〔e∩ter/api/1ogj∩! s≡Ieque5t5.5eS5jo∩()

r=5.PO5t(1Ogj∩-Ur1’ j5O∏={ u5er∩a爬; u5er∩3∏eD

pa65刚rd0 : P己s5咖rd

可、』||■Ⅵ』‖|■∏』』』』‖‖』‖』·‖|‖‖□

c1a55∧∩tj5pjder7Ce∩emtor(8a5e6e∩emtor):

}) j十r.5t3t05〔ode !=2": IetuI∩

这里的主要逻辑就是实现ge∩erate方法’利用用户名和密码’模拟请求登录接口API,然后获取 返回结果的to代e∩°模拟登录的返回结果类似如下:

二■〗|口■】|』■·■■』●■■■日■」■■■

to代e∩=I.j5o∩().get(!to代e∩!) 1ogger.debug(十,get〔rede∩tj31{to低e∩}!) 5e1千。〔】ed即tia1—operator。set(u5er∩a贬’ toke∩)

=■■■■凸■■■■□

■ ■ ■ ■ = ■ ■ ■ ■ β · 〖 ■ 【 ‖ 『 ‖ ■ 广 日 ‖ 卜 | 〖 ■ □ 『 卜 | ‖ β 『 卜 [ 『 ■ 『 卜 | } 【 ■ 『 卜 〖 『 午 ‖》||尸】『『「|‖≥「厂|||■厂卜‖『‖‖{=■「‖

15.l3 Scrapy实战

卜巴「||‖|[■厉‖||

∧3‖Dγx[◎1b‖「pb〔I6I"「比‖1u邻「比日1uk州vb5I5I呐ya‖d千己"「oIjox‖j∧z‖丁γ渊jγx旬.Xˉag‖句Z〔C1I[2γdγj9Ox346ou卯uBbr ∧gtmM十1∧阐}

这里to促e∩的值其实就是RequestHeaders里面aut|]orizatjo∩字段jwt后面跟的内容,我们利 用此to代e∩来构造author1zatio∩即可°

接下来我们再实现_下∧∩tj5pjder7丁ester’实现如下: 〔1日5s∧∩tj5pider汀e5ter(8a5e丁e5ter〉: de+

i∩jt (5e1+’web5jte≡‖o∩e):

8aSe∏e5teI。—i∩it一(5e1十’肥b5jte) de十te5t(5e1十’ u5er∩a爬′〔rede∩tj己1):

1ogger。1∩十o(十,te5tmgcrede∩tia1「or{u5er∩me}』) try:

te5tuI1≡丁[5「0RL趴p[5e1十。眶b51te] Ie5po∩5e=reque5t5。get(te5tˉur1’‖eader5={ !authorizatio∩|: 于|jwt{〔Iede∩tia1}| }’ tj爬out=S’ a11咖redjre〔t5=「a15e) j千re5po∩5e。5tatu5code==20O8

「 | | ■

1og8er.j∩十o(’cIede∩tja115γa11d!) e15e8

』 ■ 厂 } | 伊[|卜「||■『|||■| ■「》|■尸[■厂| 亿【尸|尸卜『

■[尸|■尸〖β||[∏■「









} 卜







{00to低e∩耐:"ey〕OeX∧jO1〕Rγ1Qj[□bbCciOj]I0zI1‖i〕9。ey〕1〔2γyX∑1kIjoxL〔〕1〔2γyb′∏「tZ5I6I爪「代b"1uIi"jZX∩wIjox‖j∧z‖j

■■『|·



835

1o88er°j∩+o(′〔rede∩tja1i5∩otva1jd’ de1eteit0〉 5e1十°crede∩tia1ˉoperator.de1ete(u5er∩a们e) ex〔ept〔o∩∩eCtiO∩[IrOr:

1oggeI。j∩十o(′te5t十a11ed’)

由于crede∩t1a1就是刚才我们存的to代e∩值,所以这里我们只需要获取crede∩tja1构造 aut∩or1zat1o∩即可完成模拟登录检查°这里我们添加了RequestHeaders的a0thor12at1o∩之后,请 求「[5丁0R[酗p里面指定的首页URL’即https:〃antispider7.scrape.center/api/book/?limi仁18&ofTSet=0’

如果返回状态码是200,则证明接口正常请求’模拟登录成功’当前的账号没有被封禁;否则就删除 该账号对应的〔rede∩t1a1内容’等待∧∩ti5pjder76e∩erator再次生成。 这样我们就完成了账号池的Crede∩tja1生成和测试逻辑了°

另外’由于该站点有单账号访问频率限制’所以这里我们不能让TeSter的测试频率设定得太高’ 不然它占用了请求次数就得不偿失了°

比如我们可以在settingpy里面把〔γα[丁[5『[R设置得很大’比如6O0就是l0分钟检查-次,18OO 就是半小时检查_次,可以自行设定°

接下来我们就可以导人一些账号来运行账号池了’这里我们可以简单实现一个脚本,导人-些账 号和密码’那账号密码怎么来呢?我们可以自己利用注册接口注册,也可以使用我已经注册好的一些 账号’用户名和密码都是一样的’有admm1、admin2、admin3……°

这里我们可以先导人100个账号来测试下’编写脚本如下: +ro‖ac〔ou∩tpoo1°5tomge5。Iedi5mPo工tRedj5〔1je∩t co∩∩=Red15〔1ie∩t(!ac〔ou∩t|’ |a∩ti5pjder丁). |a∩ti5pjder7‘). 〔o∩∩=Red15〔1ie∩t(!ac〔ou∩t| 5tart=1

e∩d=1卯

千orji∩ra∩ge(5tart’ e∩d+1): u5er∩a爬=p日55word≡+!adm∩{j}| 〔o∩∩.5et(u5er∩a爬’ Pa55mrd)

这样l00个账号就被导人Redis数据库中了’结果如图15ˉ27所示。



■■』

巳 工

。…ˉˉ . ·‖

咖‖〗』●

悦w

『砷

◎ ◎ 』





"吧":…Ⅵt:己m‖■口山「7

—■■■

1

]▲_田

;2

已dm}门1

adm‖∩1

ad叮m2

己d而‖佃2

己dm0∩3

ad肮‖∩3

admj∩q

ad『∏j∩q

■‘『W}∩3

己d们l∩5

■纠』·■□‖』■■〗‖■]】】γ‖‖■‖‖』■■∏』·‖|』〗●】』可】·』』■】□·】』·■■■■●可‖』‖|判

第15章Scrapy框架的使用

836

ad‖w‖∩6 日

ad们‖"7 『

己d丽j"8



8



ad炳‖∏7

。α

7

mm‖∩1】



■··

·

ad川‖"12

adm‖∏13

ad们{∩13

aGmi∩14

adm0∩14

■γ



■■■ ■■

■■『

】β



◎□



■〗





■口m‖m6

16

△□





ˉ□

‖】5



|■

日 』

adm‖p1■

日 】

■ !13

图15ˉ27将数据导人Redis数据库

接下来我们来运行-下账号池,就可以看到账号池开始执行这些账号的模拟登录和测试流程’运 行账号池,命令如下:

‖‖((·

admj『‖1o

11

】 】

admj∏9

己d∏‖∩】O



admi∩g

10



9

pytho∩3ru∩.pya∩ti5pider7

类似运行结果如下:

』■■|‖‖‖‖』■〗‖』||』』■]

202Oˉ1Oˉ25O2:5482』.〕75 | D田lL | accou∩tpoo1.5chedu1er:ru∩te5ter:31 ˉte5ter1oop05tart… 2O20ˉ10ˉ2502:54:2』.376 | 0[B0C | a〔〔ou∩tpooL5〔hedu1er:Iu∩ˉge∩eratoI:46ˉ8etteI1oopO5taIt… *5ervj∩g「1as代3pp"accou∩tpoo1.pro〔e55or5.5erver"(13zy1oadj∩g) *[∏γjro∩「‖e∩t: produ〔t1o∩

肌R‖I‖6:『∩j51sadeγe1op∏记∩t5erγer· 0o∩otu5eitj∩aprodu〔tjo∩dep1oy们e∩t°

05eaproductio∩"亚I5erverj∩5tead° 本0ebugmde: o仟

·

|』■』]

o千ad川j∩1

|ac〔ou∩tpoo1°pro〔e55or5°ge∩emtor:IM∩:39 ˉ 5tarttoIu∩ge∩er己tor | 3c〔ou∩tpoo1.proce55or5.ge∩eI己toI;Iu∩:43 ˉ 5tarttoge∩emte〔Iede∩tia1

■■]

202Oˉ10ˉ25O2$54:24.376 | D〔B[L 2O2Oˉ1Oˉ2502:54:24.377 | 0[B‖」C

*Ru∩∩i∩go∩http://0.α0.O:6789/(pre臼5〔『Rl+〔toq‖jt)

|■』■〗‖』』‖二■■●〗‖

2020ˉ10ˉ2502:5啡:31。273 | D[8l几 |a〔cou∩tpoo1.pro〔e5sors.ge∩emtoI:ge∩emte:68ˉget〔rede∩tja1 ey〕OeXMOj]咀γ1Q1l□∩bCci01〕I0zI1‖i]9.ey〕1〔∏γyX21代Ijox砸"idX‖1〔Ⅶ5hb‖0i0i〕hZC1pbj∩j[□1e肌i0j[20O0‖2肥g4"z【s IⅦγtⅧ15IjojIj"jb3〕pZ19pγXQj0j[2烟1‖j02‖z「9。‖n№2z酬3们8o1γp1iγ1zo5ut〕gs肘v—hc咖I5z于68『∧I ■

|‖





这里我们看到∧∩t15pjder7Ce∩erator和∧∩tj5pjder7丁e5ter就成功运行起来了’它会遍历已经导 人的账号,然后不断模拟登录并生成crede∩t1a1存储到Redis数据库中’结果类似图l5ˉ28所示。 --_●●●

0↓A5什



~乙仁≈宁宇●□→■■■→=丙=

~▲●

甲■■

■→ 帮≈



兰巴



-牛

□me杠■l■msp密7

5‖玄e: 16∏L: ˉ1

■→≈≡==■≡=—■■■_

唾_—畸ˉˉ…一—-……。ˉˉ…ˉ『V工ˉ≡ˉˉˉ-=ˉˉˉˉ=ˉ…ˉˉ…_—…ˉˉ一ˉ….…

乙=→-司=■■≡--_≡→~→~_~=令←…→~←~~→■■~=≡-■≡_==■≈哼■■≡■=■=-一■→甲■≈■■≡■■■■~÷■■ ■■■~■■ 坐 ≈

eⅦoex∧』o(』Ⅸv1QllQhm〔0o‖‖0∑‖】N‖9.喇1c2Wx2‖R‖°γ‖5酗dx川‖c巾,hbW(沁0∩zc1

●凶=h2

…∩=■^…g…Ⅷ…锚伪…mm【0■【p△口檀丘^=■■=@0几″m山』韧-△嗅含…巳′△0Oˉ-广▲……垂..□≡

3

三■m酮1q

四m『n乙

eWO■X∧』O(lNV】Q『LC』∩bCcjO0{Uz‖1M』9°en1c2WXR‖代‖◎γ佃γw|dX"‖〔0w5们bwUjO‖∩ZC1

4

2 日

ˉ

5

祖肌0∩17

6

a6∏们6

1N09°eⅥ1c2WXE|k‖◎×佃印‖OxMcm5‖bⅧ』『O‖hzC

切dO‖‖UZ‖1"09·W‖】〔2γγX2{k‖o5lq1(∑Wbm『t乙S‖6‖m「kbW‖u

纲OeXAjO‖旧γ1QjLC』"bGαO0‖0z『1川09.e训1〔2VVX2‖灿oγ"M』dX川|c们ShbWUjO帅ZC1p 酗…1酗醚嗽酗删』圃皿鹏喇】〔2…脓‖o又川5WM酗kmSh蜘m‖』‖rC1· !

7

四m‖∩】1

8

渔ml∏7

0





助鲤x∧jO0ⅨV〗α儿q‖匪cio00UⅢ‖1N‖9喇1c乙W讥2‖k‖w川CwJdX川‖〔肮ShbWUjO0们ZG、

·γ"ex∧℃0Ⅻ1Qj止□∩bcc‖O腆o王l1刚9e刊1c2W弧Ⅱ‖h‖◎x川佃‖dxM〔mβ∩bⅧ』jO‖hzC1p

9

辫m↓∩9

Cw促x∧‖o0届V1αLc』"酶C℃0‖0左‖1"‖9。喇1〔2WX∑‖k‖◎xOCW‖dxM〔0WSh毗」lO0hZC..』

I0

m巾0∏8

11

己d佩∏"q

eⅦ0eX∧i叫长γ1Q』LqhbCc‖O0‖O王‖】N0g°eγ』1〔2WX2‖刚Ox"γwMX侧‖〔丽5hbWU‖O‖bZC】. e训OeX∧』O0Ⅸγ1Q』山hbC创O0‖0xl1N‖9。驯1〔2γ咖乙‖k灿◎Ⅸ旧γwjdXM〔m5hbWU∏O‖hZC1。

12

adm‖∩13

口WX2‖则oγ朋jw0创X刚cm3∩bWU‖O0hm1

13

巫Ⅷ0∩13

G训蚀XAj◎‖Ⅸγ1αLC』‖比〔‖O‖』↑U刁1M』9.邮1C乙WX2‖k‖oγNCw0dXN‖c爪5h叫UiO0hZC1

M

己d加‖∏】0

13

四m‖∏12

16

ad们0∩5



‖OXN‖〔mSb…jo0抠C1

eⅥ』OeX∧0Ol』贝γ1Q0止C』hbCc‖O0‖lm1N‖9.cM‖1〔2W沉2‖k‖』oV川5w‖dXNl〔而ShbWUjO0hZG1 eⅥOeX∧jO0长V】Q』kαhbCαO0{U乙‖1"09.刨1c2W叉2‖k』jox丹“$dXN|c丽5hbWU℃0hZG

{』{ ‖‖

图l5ˉ28生成的〔rede∩t1a1被存储到Redjs数据库中

■|■■尸



l5.l3



Scrapy实战

837

这时候我们也可以访问账号池提供的API,账号池目前运行在6789端口’我们可以使用API来 获取随机的〔rede∩tja1,如图l5ˉ29所示° ■厉》

●『●

o■C凹唾冗:w89扛汹…『欢盯× ‖ +

++· o



△■【■『‖|[■[卜|卜卜}■■「》′|巴■「●■「■「■「≥「|》||∩「|β△尸■尸■『β|广卜卜卜)尸卜|∩■‖■■尸但尸}匹『|》‖■厂心■|)「厂【■伊ββ■■■□尸【■△=◆「‖『

田吵…川◎k』‖w‖o儿G】巾Gc‖○‖o』‖u画?N则9.eγ0?c2呻2测°3"Ⅷ‖酗N{口∏5↑州」‖◎『d沤G‖p刨γ4‖|W回↑W‖°xN』∧Z『

=n







图l5ˉ29使用API来获取随机的〔rede∩tja1

接下来我们再修改-下DownloaderMiddleware’使其使用该账号池里面的〔rede∩t1a1来进行爬

取’修改Scrapy项目中的∧uthorizatjo∩‖jdd1e"are如下: 〔1a55Aut∩orjz己tjo∩∩1dd1印己re(object)8 a〔〔ou∩tpoo1-ur1≡ 0‖ttp://1o〔a1‖o5t86789/己∩tjspjder7/ra∩dα∏0 1o88eI=1og8i∩8.get[o88er(!mdd1印are5.authori2atjo∩,) 己sγ∩〔de千pⅢoce55ˉrequest(5e1+’ reque5t’ 5pjder): a5y∩〔"jth己io∩ttp.〔1je∩t5e55jo∩()a5c1ie∩t: Ie5pO∩5e.己Wait〔1ie∩t。8et(5e1十.aCCOu∩tpOo1ˉur1) 1十∩otrespo∩5e·5tat05==2“: retur∩

crede∩tia1=a"3itIe5po∩5e.text() authorjzatjo∩≡千|jNt{Crede∩tia1}, 5e1十.1o鹏er.debu8(+|5etaut∩orizatjo∩{al』t∩orizatjo∩}!) reque5t.们eader5[0a0t∩oriz日tjo∏|] 二己u恤orizatio∩

通过修改’我们利用ajohttp请求了账号池的接口获取随机credential’然后将其转化为

authorjⅢatjo∩字段的格式,前面拼接上j毗即可’构造了aut∩orjzatio∩之后’我们将其赋值到 reque5t的header5属性的authorjzatio∩字段即可。 这样我们就可以实现随机aut们OrjZat1o∩的设定了,每次请求相当于随机取用了一个账号信息, 这样单个账号的访问频率就大大降低了。 ●运行测试

接下来再次运行Spider’结果如下: 2020ˉ1Oˉ25o〕:10:S8 [s〔mpy.〔ore.e∩8i"e]D[8Ⅶ8 〔ra"1ed (20o) ≤6[『 ‖ttps://己∩tjspideI7.5〔r己pe。ce∩ter/apj/boo仅/?1jmt画1泌o仟set=5o4〉(re千erer8 ‖o∩e) 202o=1oˉ25o〕81q:58[川jdd1…re5.aut加rjzatio∩]0[α几;5etauthorj【atio∩jwtey〕陛X∧j0j〕刚1Qi[□h怔ci0i]I0∑I1‖j〕9。 eγ〕1〔2γyX21kIjo酬yw1dⅫ1cⅦ5h刚』i0i〕hZC1pbj阳Ij"1ZXh"Ijox‖jAz‖jA5ND代1[□1删「pb〔I6IiI5I‖∏9ya刚千a‖「oIjox‖j∧z‖ Ⅳ2∩j仅1句。刚句7∧6ˉqj2=‖Bohb酬9ˉy"十D7o1ZZˉ阳γ‖3〕‖「d「PI 2020ˉ10ˉ25o3:14:58[mdd1印已re5.proxy]0[8": 5etproxy12S·26.56·87s8o8o



〈20O∩ttp5://a∩tj5pider7.5〔rape.〔e∩ter/apj/booR/?1jⅦit=1肥o仟5et=5O4) ●





2020ˉ10ˉ25O381S:0〕[5cmpy.core.e∩gi∩e]D[8‖L:〔mN1ed(2m)<C[丁https§//a∩tj5pjdeI7.5〔Ⅲ己pe.ce∩ter/api/ boo促/?1i川it=188o仟5et=684〉(re十erer; ‖o∩e)

202Oˉ1Oˉ2503:15:0〕[mdd1印己re5蹦authorjzatio∩]D[趴L:5etauthomz己tio∩j毗ey]贮X∧1Oi]Ⅻ10j[〔〕h切cj0i〕I0zI1‖i〕9.

ey〕1c2γyX21促IjozMmdX‖1〔肌5‖bⅧj0i]hZαpbjI3Ij"iZXhwIjox‖jAz‖jA5‖Ⅸ』x儿□1删「pb〔I6IiI5m9ya‖d于a"「OIjox‖j∧z‖ 丁γ2∩j0x侧.jojBγ∏yR〔6I』1‖8〔ˉ[j5h98倒[刚q81xpX78“1yγ刚〔 202Oˉ10ˉ25O〕:15:03 [mdd1酗are5·proxy] D[B卯: setpIoxyq3.225。195.9O:5o878

这时候我们就可以发现’绝大多数的请求都经成功返回了200状态码,持续爬取_段时间,依然 没有问题°

到现在,封lP和封账号的问题就被解决了。最后让我们完善一下Spider的逻辑: 1呻ortj5o∩ ↑r咖5〔mpyj呻ortReque5t』5pider fr咖5〔mpycα∏po5itede‖nite∏↑51肌port8oo代Ite∏`



第15章Scrapy框架的使用

∩己∏呛= 0boo促0

a11o"eddo∏Ei∩5≡ [,a∩tj5pideI7.scmpe.〔e∩teI|] ba5eur1= ‖∩ttp5://己∩ti5pider7.scmpe·ce∩ter0 ∏己×-page=512

de+5tart-reque5t5(5e1千〉: 十orpagej∩r己∩ge(1’ 5e1「.归x-page+1)8 ur1=「|{5e1「.baseur1}/api/book/?1mit=18&o仟5et≡{(pageˉ 1)*18}! yie1dReque5t(ur1’〔己11bac促=5e1+.paI5ei∩de×)

+orre5l』1ti∩re5u1t5:

jd=re5u1t.get(0id0) ur1=「!{5e1千.ba5e l』r1}/api/boo伐/{id}/! yie1dReque5t(uI1’ca11ba〔代≡se1+°par5edetaj1’ prjorjty=2)

de十p己r5edet3i1(5e1+’ re5po∩se): data=j5o∩。1o己d5(re5po∩5e。text) jt咖=BOO促It酬() 十or+je1di∏jt印.十1e1d58

jt印[十ie1d] =data·8et(十je1d) yje1djt咖

■〗门||■■|■·|■■(‖■〗■]

de+p己I5ej∩dex(seM’ respo∩5e): dat日=j5o∩.1o己d5(re5po∩5e。text) reSu1t5≡data·8et(『re5u1tS! ’ [])

」●‖□』■]‖』|』■可』●■‖]』旦■■】·■‖‖·■|』■

〔1a55Boo底5pider(5pider):

■‖】■■□〗】□■∏|】■∏』』·{司|‖■■

838

另外’个人推荐再配置-些参数: R080丁5Ⅸ丁0B[γ=「a15e

R[丁Rγ‖∏p〔"[5= [4O1’ 403’ 5"’ 502’ 5O3’ 5O4] 〔酬〔0【R[‖丁【〔α」[5丁5=10 皿肌咖丁I"["丁=10 旺丁∩γ『I日[5=1O

下面解释一下泣些参数。 ‖

□R080『5丁X丁08[γ:是否遵守mbots协议,这里设置为了「a15e,在爬取时不遵守robots.txt协议,

如果遇到这些状态码’该请求会重新发起’如果不进行这样的设置’该请求失败了就会被丢弃。

■门

□R[丁Rγ‖∏p〔卯[5:需要重试的状态码’这里设置为[4O1’403’ 5OO’ 5O2’ 5O3’ 5O4]。这样

q

‖‖

可以免去最开始robots.txt的爬取步骤°

□〔0‖〔0RR[‖丁R[Q0[5『5:并发量’这甲设置为l0’稍微降低了并发数目’降低账号被封禁的概 率°当然,如果账号和IP足够多,可以将该值调高° □"‖‖[0∧D丁I∩[0U丁:超时时间,这阻设置为了l0,默认是180’默认的超时时间太长,这军设 ■■■·■■

置短一点,如果请求不成功,可以尽早重试°

□R[丁Rγ∏例[5:重试次数,这里设置为了l0’默认是2’提高重试次数’可以提高总的爬取成 功率° 最后’可以看到运行结果如下:



对时间及金钱运用,均有0吐得,否】‖不能应什日常生活。\r\∩|’ !id8 02282313S|}’ ●◆●

{|CO∩te∩t! 8 ′亦奸的文,女人有甘独立自我的骄似。\∩0 『术婚的乃奶做了个婚烟的问题专柬°一针见血哦为别人指出彼此的不足,婚姻的出略. 但在工作中谊′」、惧似,还有点自平° \∩′ !是个处处考虑,打肯人前落下笑柄的人°去拜访朋友,师长,点头之交都会买花求采且°\∩, |其实,主共是自己认如了』仆烟的不可仿,又受了新式自主自立自由的教∏°不肯委身于

‖‖』‖(日司』(』■■■‖■□』■□』《《■

2020ˉ1Oˉ25O3;33:54 [s〔rapy.〔ore.5〔mper] 0[B[L: 5〔mped十r咖〈2O0 http5://a∩tj5pider7.5〔rape°〔e∩ter/api/boo恨/1322342/〉 {‖authOr5,: [|\∩ [加伞大]\∩ 亦纤{’‖亦舒靳经典』]’ !〔ata1og: 付o∩e’ 〔咖∏记∩t5‖ : [{|〔o∩te∩t|: 』所有本庭主妇坏足玫治岛子,上有公婆下有于女’还共巴站伴侣,邮得软段肚免’才摆得乎’

l5.13 Scrapy实战

839

任何一个家庭伏低做′』`, 苟且为生°又怕缸颜弹指’过得不愉°\∩0 『禽到后面乃娟与半至中志同道合乎仲踏入殿壹…′ ‖jd! : 』2219254915!}]’

〔oγer! : !http5;//mg3.douba∩1o.co∏】/γieN/5ubje〔t/1/pl」b1jc/s1331501.jpg‖’ 0id0 : ‖132234】0 ’

0i∩tIoductjo∩0 : 0 0

p

015b∩0 8 097878o1876362『 J ∩a们e! : !花常好月常四人长久!’ page-∩u汕er|8 190’ 0Pri〔e0 8 ‖16."元!’ |pub1js∩edat0 : 02卯5ˉO5ˉ20『168":卯Z0 ’ 0pub1i5her‖ 8 !所世界出版社! 』 5〔ore0 : 07.60D

‖tag50 : [‖亦舒,’ ! ′」、说|’ ‖花估好月常囚人长久0 ’ !杏港』’ !爱悄′’ 0师太』’ 0愈忻’!★性』]》

`tra∩51atOr5|: []}

这时候’我们便可以成功绕过反爬手段’爬取到大量内容了。 5·总结

本节是Scrapy综合实战练习’为了解决与反爬虫相关的问题,我们综合了代理池、账号池和Scrapy }卜||′|尸‖》||

的_些优化设置,完成了对站点反爬虫的绕过和数据的爬取° 在进行对其他站点的实际爬取时,可以借鉴本节的思路’希望大家好好体会° 本节代码的参考来源如下°

□ScIapy项目: htms://github.com/Python3WebSpide∏ScmpyCompositeDemo □账号池: htlps://github.com/Python3WebSpide门AccountPoo‖tree/antispider7 □代理池: https://githuhcom/Python3WebSpide【/ProxyPoo‖

‖‖



} p









卜 b



































第06章

分布式爬虫

‖ 刘





在上一章中,我们了解了ScmPy爬虫框架的用法°这些框架都是在同一台主机上运行的,爬取效率 比较有限。如果能够用多台主机协同爬取,那么爬取效率必然会成倍增长,这就是分布式爬虫的优势°

本章我们就来了解~下分布式爬虫的基本原理,以及Scmpy实现分布式爬虫的流程°

我们在前面已经实现了Scrapy爬虫,虽然爬虫是异步加多线程的,但是我们只能在_台主机上运 行’所以爬取效率还是有限的°分布式爬虫则是将多台主机组合起来’共同完成—个爬取任务’这将

大大提高爬取效率°

l分布式爬虫架构

Scmpy单机爬虫中有一个本地爬取队列Queue’这 个队列是利用deque模块实现的°新的Request生成就

翻_ 由

会被放到队列里,随后被调度器Scheduler调度,交给

爬取队列α」e吧

如果两个Scheduler同时从队列里面取Request’每

图16ˉ1

调度器S创`·山』崎 简单的调度架构

个Scheduler都有其对应的Downloader,那么在带宽足

够、正常爬取且不考虑队列存取压力的情况下,爬取效率会有什么变化?没错’爬取效率会翻倍。

单调度架构如图l6ˉ2所示。

由—翻—曲

…≡

.↑… 调度器≈砂』阉

图l6ˉ2调度架构

| (

‖‖{‖』|



.….

『|司●』■|到‖|■■‖■』■■可{‖■■|·∏||」■■∏■]‖』‖』■】‖‖‖‖|

这样, Schedule【可以扩展多个,Downloader也可以扩展多个°而爬取队列Queue必须始终为_ 个’也就是所谓的共享爬取队列。这样才能保证Scheduler从队列里调度某个RequeSt后,其他Scheduler 不会重复调度此Request,就可以做到多个Scheduler同步爬取了。这就是分布式爬虫的基本雏形,简

‖司《·』纠‖〗□〗』Ⅲ··]||」■可』

Downloader执行爬取,简单的调度架构如图16ˉ1所示。



↑6。| 分布式爬虫理念

| b 血 尸 巴 ■ 〔 尸 | 们

卜 p

l6.l

分布式爬虫理念

841

我们需要做的就是在多台主机上同时运行爬虫任务协同爬取,而协同爬取的前提就是共享爬取队

列。这样各台主机就不需要各自维护爬取队列,从共享爬取队列存取Request就行了°但是各台主机

厂‖卜伊′卜}卜∩‖「‖■■庐卜■厂》队二β■尸■

还是有各自的Scheduler和Downloader,所以调度和下载功能分别完成°如果不考虑队列存取性能消 耗,爬取效率还是会成倍提高° 2维护爬取队列 爬取队列怎样维护比较好呢?我们首先需要考虑的就是性能问题,什么数据库存取效率高?我们

自然能想到基于内存存储的Redis’而且Redis支持多种数据结构,例如列表(List)`集合(Set)`有 序集合(SortedSet)等,存取的操作也非常简单’所以在这里我们采用Redis来维护爬取队列° 实际上’这几种数据结构存储各有千秋。

□列表数据结构有1PU5h、1POP、rP划5‖、rPOP方法,我们可以用它实现_个先进先出式的爬取 队列,也可以实现一个先进后出的栈式爬取队列°

□集合的元素是无序且不重复的,这样我们可以非常方便地实现一个随机排序的不重复的爬取 队列°

□有序集合带有分数表示’而Scrapy的Request也有优先级的控制,所以用有序集合我们可以实 现_个带优先级调度的队列°

我们需要根据具体爬虫的需求来灵活选择不同的队列。 3.去重

Scrapy有自动去重功能,它的去重使用了Python中的集合°这个集合记录了ScIapy中每个Request 的指纹’这个指纹实际上就是Request的散列值°我们可以看一下Scrapy的源代码,如下所示:

p

de+reque5tˉ十i∩gerprj∩t(reque5t’ i∩〔1udeheader5=‖o∩e): j+i∩C1ude‖eader58

j∩〔1udeheader5≡top1e(to-byte5(h.1o眶I〈))



十or‖i∩5orted(i∩〔1l」deheader5))

||

j爪portha5h11b



■■

·∩





〔 己 〔

↑~

·]

·∩

◎ ∩ 日 】 巳

日 巳

』∩



□α





●■■凸



勺儿

●■□凸



厂沪‖

ca〔‖e=ˉ十i∩gerpIi∩tˉ〔ache.5etde「己u1t(reque5t’{}) 千p≡‖a5∩11b.S‖a1() 十p。upd3te(toˉbyte5(reql』e5t.贬t∩od)) △%尸巴|卜

「p.upd己te(to一byte5(c己∩o∩i〔a1izeur1(Ⅲeque5t.ur1))) 「p.‖pdate(reque5t.bodyorb,|) j+i∩〔1ude∩e日der58

「orhdri∩j∩c1ude∩e己der5;

i+Mrj∩reque5t°‖eadeI5: 千p.uPdate(hdr)

p

『口巴■尸伯●『△■厂 ▲■「■『||■■△尸‖■●△■「||

■「|△β尸



+orγi∩reql」e5t。header5.get1i5t(‖dr): 千p.upd己te(γ) 〔a〔he[j∩c1udehe己der5] =十p.‖exdj8e5t() retur∩Ca〔he[i∩〔1Ude∩e己deIs]

reque5t十j∩gerpr1∩t就是计算Request指纹的方法,其方法内部使用的是∩a5b1jb的5ha1方法。 计算的字段包括Request的Method、URL、Body`Headers这几部分内容,这里只要有_点不同’那 么计算的结果就不同。计算得到的结果是加密后的字符串’也就是指纹。每个Request都有独有的指 纹’指纹就是一个字符串’判定字符串是否重复比判定Reque5t对象是否重复容易得多,所以指纹可 以作为判定Request是否重复的依据° 我们如何判定重复呢?Scrapy是这样实现的: de+

i∩1t-(5e1十): 5e1f.千1∩gerPri∩t5=5et() ●

■=■■『『Ⅱ■■户血■』■■尸

de千Ieque5t5ee∩(5e1+’ Ieque5t):



第l6章分布式爬虫 +p=5e1十。Ⅲeq‖e5tˉ十j∩gerpr1∩t(reque5t) j「fpi∩5e1f。十i∩gerpri∩t5: Ietur∏「rue

5e1十.十i∩gerpri∩t5.add(千p)

在去重的类R「P0upe「i1ter中’有一个request_5ee∩方法’该方法有一个参数Ieque5t’它的作 用就是检测Reque5t对象是否重复。这个方法调用Ieque5t-十j∩gerpri∩t获取该Reque5t的指纹,检测 这个指纹是否存在于fj∩gerpIj∩t5变量中’而f1∩gerprj∩tS是一个集合’集合的元素都是不重复的° 如果指纹存在,就返回「rue’说明该Request是重复的,否则就将这个指纹加人集合。如果下次还有

相同的Request传递过来’指纹也是相同的,指纹就已经存在于集合中了,那么Req‖est对象就会直 接判定为重复。这样,去重的目的就实现了°

Scrapy的去重过程就是’利用集合元素的不重复特性来实现Request的去重° 对于分布式爬虫来说,我们肯定不能再用每个爬虫各自的集合来去重了°因为这样还是每个主机

|||

842

单独维护自己的集合,不能做到共享°多台主机如果生成了相同的Request’只能各自去重’各个主 机之间就无法做到去重了。

那么要实现去重’这个指纹集合也需要是共享的°Redls正好有集合的存储数据结构,我们可以 利用Redis的集合作为指纹集合,那么这样去重集合也是利用Redis共享的°每台主机新生成Request

后’把该Request的指纹与集合比对’如果指纹已经存在,说明该Request是重复的,否则将Request 的指纹加人这个集合。利用同样的原理,我们在不同的存储结构中实现了分布式Reqeust的去重。 4防止中断

5cmpycraw1sP1deI ˉ5]080IR=〔mw15/5P1deI

更加详细的使用方法可以参见官方文档: https://docscrapyorg/en/latesUtopics(jobshtml°

在Scrapy中,我们实际是把爬取队列保存到本地,第二次爬取直接读取并恢复队列°那么在分布 式架构中’我们还用担心这个问题吗?不需要。因为爬取队列本身就是用数据库保存的,如果爬虫中

断了’数据库中的Request依然存在’下次启动就会接着上次中断的地方继续爬取° 所以’当Redis的队列为空时,爬虫会重新爬取;当Redis的队列不为空时’爬虫便会接着上次 中断之处继续爬取。 5.架构实现

』』`|』勺々‖』』』】勺||『(」‖』】‖‖|‖』■】勺∩‖】Ⅵ』·可‖』』■】』』』日|‖

变量来标识,可以用如下命令来实现:

曰‖

要做到中断后继续爬取’我们可以将队列中的Request保存起来,下次爬取直接读取保存数据即 可获取上次爬取的队列。我们在Scmpy中指定_个爬取队列的存储路径即可,这个路径使用〕08DI【

□』■’■□司□‖】■】■■‖‖‖{

在Scrapy中’爬虫运行时的Request队列放在内存中°爬虫运行中断后’这个队列的空间就被释 放’此队列就销毁了°所以_旦爬虫运行中断’爬虫再次运行就相当于全新的爬取过程°

我们接下来就需要在程序中实现这个架构了。首先实现_个共享的爬取队列’还要实现去重的功

幸运的是,已经有人实现了这些逻辑和架构,并发布成叫ScrapyˉRedis的Python包° 接下来’我们看一下ScrapyˉRedls的源码实现’以及它的详细工作原理°

↑62Sc「apyˉRed|s原理和源码解析 ScrapyˉRedis库已经为我们提供了Scrapy分布式的队列、调度器`去重等功能’其GjtHub地址 为: https://githuhcom/rmax/scrapyˉredjs°

』|』■』■』〗‖■』■||。□β||』■■■|||■勺‖||』■■]」』】】■■‖|‖』■■■‖

能°另外,重写_个Scheduer的实现’使之可以从共享的爬取队列中存取Request°

l62 ScrapyˉRedis原理和源码解析

8q3

本节我们深人了解_下’如何利用Redjs实现Scrapy分布式。 ↑.获取源码 可以把源码克隆下来’执行如下命令: gitc1o∩ebttp5://8jthub.coⅧ/rⅧ己×/5cr3pγˉredj5。git

核心源码在5〔mpyˉredj5/5r〔/5cmpy-redj5目录下。 2.爬取队列

从爬取队列人手,看看它的具体实现°源码文件为queuepy’它有3个队列的实现,首先它实现 了一个父类Ba5e’提供_些基本方法和属性’代码如下所示: 〔1a558a5e(object): de+ j∩jt-(5e1+’ serγer’ 5pideI’ 仅ey’ 5eri日1jzer=‖o∩e): i+5eria1izeri5‖o∩e;

5er1a1亚eI二pj〔低1ecα‖Pat j十∩ot∩己Sattr(5e【ja1j乙er’|1Oad5‖):

rai5e丁ype[IIoI(n5eri己1izerdoe5∩otmp1e∏论∩t 』1oad5| 千u∩ctio∩:%I. %5erja1izeI) i十∩oth己5己ttr(5erja1iZer’|duⅦp5!):

rai5e「ype[rIoI("seri日1izeI !%5` doe5∩ot加p1e∏记∩t {du『∏p5! +u∩〔tio∩:%r" %5erja1iZer) 5e1「.5erγer=5erγer

5e1十.5pider=5pider

5e1「。key=低ey%{‖5pjder0 : 5pider.∩3"e} 5e1「°5eria1iZer=5erja1iZer

de千 e∩code-reque5t(se1十’ reque5t):

obj =reque5t一to-dj〔t(request’ 5e1十.5pjdeI) retur∩5e1千。5eria1jzeI.dum5(obj)

de+ decodeˉreque5t(5e1十’ e∏codedˉIequest): obj≡se1十°5eria1jzeI.1oad5(e∩〔oded—Ieque5t) retur∩req‖e5t于r咖di〔t(obj’ se1+.5pjder) de千

1e∩ (5e1十): mi5e‖otI∏p1e"e∩ted[rIor

de+pu5h(5e1+’ reque5t): raj5e‖otI∩p1e『】e∩ted[rroI

de+pop(5e1十》 t1爬out=0): mj5eNotI们p1e爬∩ted[rIoI de十〔1eaI(5e1+): 5e1「.5erγeI。de1ete(Se1十。代ey)

首先看一下e∩code=reque5t和de〔ode≡reque5t方法’因为我们需要把一个Request对象存储到 但数据库无法直接存储对象’所以需要将Request序列化转成字符串再存储,而这两个方

数据库中

,

0≡…■N夕丁歹=0≈_p^■■

■.冗尸



列化和反序列化的操作’这个过程可以利用pickle库来实现°—般在调用p05‖方法将 法分别是 序列化和反序列化的操作’

ReqUest存人数据库时,会调用e∩〔ode-reque5t方法进行序列化’在调用pop取出Request的时候’ 会调用-decode-reque5t进行反序列化°

在父类中—1e∩—、pus∩和pop方法都是未实现的’会直接抛出‖otI们p1印e∩ted[rIor,因此这个

类是不能直接被使用的,必须实现一个子类来重写这3个方法,而不同的子类就会有不同的实现’也 就有着不同的功能°

接下来就需要定义一些子类来继承Ba5e类’并重写这几个方法,那在源码中就有3个子类的实 现’它们分别是「j十oQueue、 priorityQueue、li十oQueue’我们分别来看一下它们的实现原理。

[↑β

∏匹■

8“

第l6章分布式爬虫



首先是「1十山ueue: 〔1己55「if山仙eue(Ba5e):

de十-1e∩_(5e1十); retur∩5e1「。5erγer.11e∏(5e1千.仪ey) defpuSh(Se1+’ reque5t): se1千.5erver°1pu5h(5e1+.促ey’ 5e1+。-e∏codeˉIequest(reque5t))



de+pop(Se1千’ tj∏帽out=O)自 j于ti贬out)0: 尸▲

data=5e1十。5eIγer。brpop(se1十.促ey’ ti爬out〉 j「j5i∩5ta∩ce(dat己’ tup1e): d己ta=dat已[1] e15e:

dat己二se1千.5erγeI.Ipop〈5e1十.key) j「d3ta目 ‖

retur∩5e1十°=de〔ode=Ieque5t(data)

库进行操作°可以看到这里的操作方法有11e∩、1pu5h`rpop等’这就代表此爬取队列使用了Redjs的 列表。序列化后的Request会被存人列表,就是列表的其中一个元素; 1e∩方法是获取列表的长度; p05h方法中调用了1pu5h操作,这代表从列表左侧存人数据; pop方法中调用了rpOp操作,这代表从

□■Ⅵ·‖|■■』■】‖‖{|

可以看到这个类继承了8a5e类,并重写了-1e∩—、pu5∩、pop°在这3个方法中’都是对5erγer 对象的操作’而5erver对象就是一个Redjs连接对象’我们可以直接调用其操作Redis的方法对数据

q

列表右侧取出数据°

·

Request在列表中的存取顺序是左侧进、右侧出,这是有序的进出,即先进先出(hrst input而rst outputˉFIFO),此类的名称就叫作「j十oQueue° 还有-个与之相反的实现类,叫作Lj十oQueue,代码实现如下: □

de「pl』5h(5e1千’ reque5t): 5e1于。5erver。1pu5‖(5e1+.促ey’ 5e1千.ˉe∩〔ode-reque5t(Ieque5t)) de+pop(5e1+’ tj贬out=0): j千tj爬Out〉O:

dat己=5e1千。serγer.b1pop(5e1十.hey’ ti爬out) 1「j5i∩5t己∩〔e(data》 tup1e): d己ta≡data[1] e15e:

d己ta=5e1f.5erγeI。1pop(5e1+.庶ey) i+d己ta:

retl』r∩5e1千.ˉde〔ode-Ieque5t(dat己)

与「j于oQueue不同的是’它的pop方法在这里使用的是1pop操作’也就是从左侧出,而pu5∩方 法依然是使用的1pu5h操作,是从左侧人°那么这样达到的效果就是先进后出`后进先出(lastinfirst outˉLIFO)’此类名称就叫作[i「oQue0e°同时这个存取方式类似栈的操作,所以其实也可以称作 5ta〔促Queue°

在源码中还有一个子类叫作prior1tyQueue,顾名思义,它是优先级队列,代码实现如下: 〔1a55prjoI1tyα」eue(835e〉: de于_1e∩—(5e1+): retur∩5e1十.5erγeI.江ard(5e1千.促ey)

de十pu5b(5e1「’ reque5t): data=5e1「.-e∩〔ode-Iequest(Ieque5t)



■■■□]‖《|||‖□‖日』□|』■』∏』』■』·]‖』■■】』|』□‖〗』·|』勺』■‖‖】·】□·‖‖·]||」勺■】]』■■】‖||』■■|||』■■■〗|·勺■■■‖

〔1a55[i千叫ueue(8a5e): de十 1e∩ (Se1+): Ietur∩5e1十.5erγer.11e∩(5e1十.key)

l6.2

ScrapyˉRedis原理和源码解析

845

s〔ore= ˉreque5t。pr1oI1ty

5e1「.5erver.exe〔ute〔叼■∏a∩d(!Z∧m! ’ 5e1十.代ey’ s〔ore’ data) de十pop(5eM’tj爬o0t≡0): pipe≡5e1「·5erγer.pi障1i∩e() pipe。刚1tj() pipe.zIa∩ge(5e1「key’ 0’ o).zIema∩gebym∩促(5e1f.代ey’ o’ o) Ie5u1tS’〔Ou∩t=pi阮。exeCute() jfre5u1t5:

retur∩5e1f.ˉde〔ode≡reque5t(re5u1t5[0]) 「| |「



『「 ■厂|》「|





在这里我们可以看到_1e∩—、pl』s们、pop方法中使用了5er`′er对象的z〔aId、zadd、zra∩ge操作, 可以知道这里使用的存储结果是有序集合’在这个集合中,每个元素都可以设置一个分数’这个分数 就代表优先级。

1e∩方法调用了z〔ard操作,返回的就是有序集合的大小,也就是爬取队列的长度。在Push方 法中调用了Zadd操作,就是向集合中添加元素’这里的分数指定成RequeSt优先级的相反数’因为分 数低的会排在集合的前面’所以这里高优先级的Request就会存在集合的最前面°pop方法首先调用

了zra∩ge操作取出了集合的第-个元素,因为最高优先级的Request会存在集合最前面,所以第一个 元素就是最高优先级的R叫uest’然后再调用zre∏lra∩gebym∩促操作将这个元素删除’这样就完成了取 出并删除的操作°

此队列是默认使用的队列,也就是爬取队列默认使用有序集合来存储°

巴尸『|■尸〖●「伊|

3.去■讨滤

前面说过, Scmpy的去重是利用集合来实现的,而Scmpy分布式中的去重需要利用共享的集合’ 这里使用的是RediS中的集合数据结构。我们来看一看去重类是怎样实现的。

■广『凸■【卜|卜|巴■}■||||

源码文件是dupe∩lteⅢpy,其内实现了_个R「p0upe「i1ter类,代码如下所示: 〔1a5sR「pmpe「j1ter(8aSe0t』pe「j1ter): 1Ogger=1O肥er

de十

j∩it (5e1「’ SeⅣeI’促eγ’ debug=「a15e): 5e1「.5eⅣeI=5erγeⅢ

5e1+key=仅ey

5e1千.debu8=debu8 5e1十.1o8d仙pe5=丁rue α1a55臃thod

‖ 卜 ■ 尸

de于「ro‖5etti∩85(〔15’ 5ettj∏8■)8 5erver=8et-redi5千r咖5etti∩85(5ettj∩85)

仅ey=de十au1t5·"p[「I∏[R旺γ宠{!ti贬5ta呻! : i∩t(tj贬。ti爬())}

debug=5etti吧s.8etboo1(!凹0[「I∏[R0[0‖炬{)

|β|》

retuI∩c15(5erver’促ey=代ey’debu8=debug)

『卜)》

伙1a55眶t们od

de十十r咖〔raN1er(〔15’〔r己门1er);

retur∏〔1s.+rm5etti∩g5(CⅢ酗1er。5ettj∩85) de+requeSt5ee∩(5e1十’ reque5t): 十P霉5e1十愈reql』e5t-十j∏gerPri∩t(reque5t)

} ■【尸「‖■|



added=5e1千。5erγer·5add(5e1十。促eγ’ fp) retur∩added==O

de十reque5t-fi∩gerpr1∩t(5e1f’ reque5t): retuI∩reque5tˉ+j∏8erpri∩t(reque5t)



↑6 de「〔1o5e(5e1f’ rea5o∩=, ,);

β

5e1十.C1e3r()

卜β

|)卜卜

de+〔1eaⅢ(5e1十): 5e1+.5erγeI.de1ete(5e1千.促ey)

L

司 』 』 ■

{ 第l6章分布式爬虫

□‖司‖

846

de十1og(5e1于’ reque5t’ 5pideI): j「5e1「.debug:

ˉ ∩o『∏oredup1jcate5门j11be5们o切∩" .(5ee山p[「I∏[RD[B四to5bow日11d‖p1icate5)")

5e1于镭1ogger。debug("5g’{‖request|: reque5t}’ extra={′5p1der|: 5pider}) 5e1十°1o8dupes≡「a15e

|‖|

了数据库的存储方式°

■‖‖

这里同样实现了一个reque5t—5ee∩方法’与Scrapy中的reque5t—5ee∩方法实现极其类似°不过 这里集合使用的是5eIver对象的5add操作’也就是集合不再是一个简单数据结构了’而是直接换成

■■‖|』』】‖||■■↑||司』」』

"5g="「j1teredd0p1jcateIeque5t:咒(Ieque5t) 5" 5e1千。1ogger.debug(Ⅶ5g’{』Ieq‖e5t0 : reqqe5t}’ extm={『5pider‖: 5Pider}) e1j+5e1千.1ogdupe5; ‖5g=("「i1tereddup1i〔atereque5t%(reque5t) 5"

鉴别重复的方式还是使用指纹,指纹同样是依靠reque5tˉfj∩gerpr1∩t方法来获取的°获取指纹 之后直接向集合添加指纹’如果添加成功’说明这个指纹原本不存在于集合中’返回值为1°代码最 后的返回结果是判定添加结果是否为0’如果刚才的返回值为1’那么这个判定结果就是「a15e’也就

4.调度器

ScrapyˉRedjs还帮我们实现了配合Queue、DupeFilter使用的调度器Scheduler’源文件名称是 schedule【py°我们可以指定_些配置’如5〔‖[00l[R「[05‖0‖5丁∧R丁即是否在爬取开始的时候清空爬 取队列, 5〔‖[00[[Rp[R5I5丁即是否在爬取结束后保持爬取队列不清除°我们可以在se仗ingspy里自由 配置,而此调度器很好地实现了对接。

接下来我们看两个核心的存取方法,代码如下所示: de+e∩quel』e-reque5t(5e1千」 Ieque5t): i+∩otreque5t.do∩t千j1ter3∏dse1「。d千.reque5t-5ee∩(reque5t): 5e1「.d千.1og(reque5t’ 5e1+°5pjder)

5e1+.5tat5.1∩〔-γa1ue(‖5chedu1eI/e∩queued/redi5! ’ 5pider=5e1十.5pider)

5e1+.queue·p(』5h(Ieque5t)



‖|」‖

retur∩「a15e

j+5e1+。5t己t5:

{当·`□勺|』■‖||』■坦■■

这样我们就成功利用Redjs的集合完成了指纹的记录和重复的验证°



‖|

是不重复’否则判定为重复。



retur∩丁rue

5e1十。stat5.1∩〔ˉγa1ue(‖5〔hedu1er/dequeued/redj5! ’ 5pjder=5e1「。5pider) retur∩Ieque5t

e∩queue-reque5t可以向队列中添加Request’核心操作就是调用Queue的pu5h操作’还有—些

』|‖」

j十request日∩dse1十°stat5:

』司二·】』■

de+∩ext-reque5t(5e1十): b1oc戊-pop-tmeout=se1「°jd1ebe+ore〔1o5e reql』e5t≡5e1+.queue.pop(b1o〔低-pop-ti爬out)

统计和日志操作°∩ext-reque5t就是从队列中取Request,核心操作就是调用Queue的pop操作,此 时如果队列中还有Request’则Request会直接取出来’爬取继续;如果队列为空’则爬取会重新开始° 目前’我们把之前说的3个分布式的问题解决了,总结如下。

·‖

5.总结



□去重的实现:这里使用Redjs的集合来保存Request指纹’以提供重复过滤°

□中断后重新爬取的实现:中断后Redis的队列没有清空,再次启动时调度器的∩extˉreque5t会 从队列中取到下-个Request’继续爬取°

‖{‖{(

□爬取队列的实现:这里提供了3种队列,使用Redjs的列表或有序集合来维护°

{{

| l63基于ScrapyˉRedis的分布式爬虫实现

849



R[0I50R[= 『redj5://192°168.2.3:6379‖

第二种配置方式是分项单独配置。这个配置就更加直观了’如根据我的RediS连接信息’可以在 settingspy中配置如下代码: R[DI5"5『= 0192·168°2·30



R[DI5pA55刚RD=‖o∩e

这段代码分开配置了Redjs的地址、端口和密码,密码为空°

注意,如果配置了R[DI50R[,那么ScIapyˉRedis将优先使用R[0I50R[连接,会覆盖上面的3项 配置°如果想要分项单独配置’请不要配置R[0I50R[。 p

p

在本项目中,我们选择的是配置R[0I50R[° ●配五调度队列



此项配置是可选的’默认使用prjomtyq』eue°如果想要更改配置,可以配置5O{[山[[Rq』[0[αA55 p

变量’如下所示:

伊|

5田[山l[R仙[U[α∧55≡ 05cr日py=redjB。queue.priorjtyq』eue0

|止尸‖「|■『‖『||





5O‖[0l」l[【"[0[〔[A5S= ,5〔【apγ-redj5·queue。U千叫ueue,

以上3行任选其一配置’即可切换爬取队列的存储方式。

在本项目中不进行任何配置’我们使用默认配置,即priorityQueue° ●配Ⅲ持久化

q卜

此配置是可选的,默认是「a15e°ScmpyˉRedis默认会在爬取全部完成后清空爬取队列和去重指纹

P



集合° 如果不想自动清空爬取队列和去重指纹集合’可以增加如下配置:

P

5〔‖【DUt[【p[R5I5丁≡『Iue

} 『 |尸》||||卜》

β}}



■日‖凡|〔〖■ ■



将5〔‖[0|」L[Rp[R5I5『设置为丁rue之后,爬取队列和去重指纹集合不会在爬取完成后自动清空’ 如果不配置,默认是「a15e’即自动清空°

值得注意的是,如果强制中断爬虫的运行,爬取队列和去重指纹集合是不会自动清空的。 在本项目中不进行任何配置’我们使用默认配置° ●配Ⅲ重爬

此配置是可选的’默认是「a15e°如果配置了持久化或者强制中断了爬虫,那么爬取队列和指纹

集合不会被清空,爬虫重新启动之后就会接着上次爬取。如果想重新爬取’我们可以配置重爬的选项: 5〔‖[山l[R「山洲删5『∧盯=丫Iue

这样将5〔‖[00[[R「[05‖O‖5丁∧R『设置为「rue之后’爬虫每次启动时’爬取队列和指纹集合都会 清空°所以要做分布式爬取’我们必须保证只能清空一次,否则每个爬虫任务在启动时都清空-次’ 就会把之前的爬取队列清空,势必会影响分布式爬取°

巴■厂伊|卜)八户‖■尸「|||■}■厂|



注意,此配置在单机爬取的时候比较方便,分布式爬取不常用此配置。 _

在本项目中不进行任何配置,我们使用默认配置。

.

↑6 -

●PjPeline配五

此配置是可选的’默认不启动Pipeline°ScrapyˉRedjs实现了_个存储到Redis的ItemPiPeline’

}}









850

第16章分布式爬虫

如果启用了这个Pipeline’爬虫会把生成的Item存储到Redis数据库中°在数据量比较大的情况下’ 我们-般不会这么做°因为RediS是基于内存的’我们利用的是它处理速度快的特性’用它来做存储 未免太浪费了’配置如下: I「[∩PIp[lI‖[5= {‖5cmpyˉred15.pjpe1i门e5.Redj5pipe1j∩e‖: 30o}

本项目不进行任何配置,即不启动Pipeline°

到此为止,ScrapyˉRedis的配置就完成了°有的选项我们没有配置’但是这些配置在其他Scrapy项 目中可能会用到,要根据具体情况而定。 5.配置代理池和账号池

在middlewarespy中’我们需要修改代理池和账号池的请求地址,之前我们使用的是1o〔a1∩ost, (日这里我们需要统-修改为上文提到的A主机的地址。

在proxy‖1dd1eware修改proxypoo1—ur1内容如下: proxypoo1-uI1= 0http://192.168.2.3:5555/ra∩do∏0

在∧l」t∩OrjZatjO∩‖idd1e"are修改aCCOu∩tpOO1ˉl』r1内容如下: a〔〔ou∏tpoo1-l』r1= 0http://192.168.2°〕:6777/a∏tj5pjder7/r己∩d刚0

注意这里需要根据实际情况修改为你的A主机的代理池和账号池地址°







d■





| q



q

Q

q



6.运行

以上修改需要同时在A`B`C三台主机上执行,三台主机上的代码是完全一样的°修改完毕后’



我们便完成了分布式爬虫的配置了,这样三台主机就共享了同一个Redis爬取队列’同时共享了—个 代理池和账号池’共同完成协同爬取°

q

接下来我们就可以运行一下实现分布式爬取了’在每台主机上都执行如下命令: 5〔mpγ〔mw1boo促

0

主机的运行顺序不分先后,每台主机启动了此命令后’就会从配置的A主机的Redis数据库中调



度Request并利用Request的指纹集合进行去重过滤。同时每台主机占用各自的带宽和处理器,不会



互相影响,爬取效率成倍提高°

2O21ˉo1ˉO3o2:57:56 [mdd1eware5.authorjzatjo∩] 0[8[L: 5eta0t∩orizatjo∩jwt

ey〕oeⅫj0j〕氏γ1Qi[〔〕hbC〔i0j〕I0zI1M〕9.ey〕1〔2γyX21代Ij叫‖ywidX‖1〔Ⅷ5hb"0i0i〕∩ZC1pbj〔0Ij"jZXhwIjox‖jA5‖z[0Nj酗

‖日

值得注意的是’第_个启动的Spider会检测到爬取队列为空’这时候就会调用startˉreque5t5方 法生成初始Request,后续启动的Spjder如果检测到爬取队列不为空,就会直接从当前爬取队列中获 取Request进行爬取°这样’只有第_个Spider的5tart≡reque5t5方法会被调用,其他Spjder在队列 不为空的情况下是不会调用5tartˉreqUe5t5方法的’于是保证了多个Spider的协同爬取。

■■β|司

{〔〕1b‖「pb〔I6IiI5I昭ya"d「a‖「OIjox‖j∧5‖j〔x‖肿千Q。d40ai丁1丁6p8Xd4〔o「5daIu"e∑t∑〔〔1十7刊79X8IO5朋 2o21ˉo1ˉo302:57:56 [刚jdd1ew日re5.proxy]D[8lL: 5etproxy149.28.218.1o8:3128 2O21ˉO1ˉ0302:57:S6 [5〔r己py.〔ore.5〔mper]D[8lL: 5〔raped「Io∏| <2oo http58//a∩ti5pider7.5cmpe.ce∏ter/api/boo促/1oo1885/〉

|‖

每台主机运行过程中的输出结果类似这样:





7.结果

‖|

所示。

(』口日■

-段时间后,我们可以用RedisDcsktop观察Redjs数据库的信息°这里会出现两个Key:_个 叫作boo代:d‖Pe十i1teI,用来储存指纹;另—个叫作boo促:reque5ts,即爬取队列,如图16ˉ5和图l6ˉ6

□ 可 ■



『‖卜||尸『‖|卜‖|‖■「‖卜|}》「但|怔仍[|β||Ⅱ尸‖『‖‖‖|∩

l64基于BloomFilter进行大规模去重 5口: }…仅:d帖碑∩lte『

S‖Ze: 17

85l

丁丁1: =1

≡-厉—≡=≡=≡≡=-——~■→≡甲…一一…==丁~~=~—=…≈=早磊田ˉ=尹■^公…磊__蛇…■←→=

m0ue

,…

-……≈……≈

1 任『‖队《‖「|巴尸「‖‖尸『●「「}

2

=_←—^ 勺艳

k_—_

O74qab5ad3e29211e↑g63d3ecab755d721b5276e



e0筛70(b98%b5耳丽77eb良豆斟§闻98c879M06o

.

一=

40d022b4257a826捷f823418d3fd4b9d8531〔db7

‘3 《q

bS41d16f3f牛b8c39a71M7b塑1MOd899aO25b5∑



90Cb↑ga8db〔『b83a60e43O80仔b1b舷27202f78〔

5



81811a9ee§倔qC7e3eα90973唾a656昏9比299w2

6150O5365e3〔↑885e69b9a2b2af680S87ed72370

p

8

o77a1酥83dbe8毗9鳃厄「e吗ahd29a2C773CeaO09b



9

321b3陀773303661e〔4783ee〔a凶e1d▲9463a74C『

… 3282e9qa7痴臼e8Oa乍19175369d6e228〗7e朽5e72



b56Cf9s1bb9671d87仁244837C8be12752b9de8$1

11

dbb『71ab679e酗36雹dbc612C雾9】ec9I6e8〔058d



X2 7甲

≡【



a7ce徊d37124eba3αe7e408ab了a02603e1“6″

!13 L_

1q



|[■「|

e6103e66∑O76O88O86917掩fB№了75e8ba8e47矗0 ≈■

.15

9触2cO5bc己0己6dq5记10〔5629d〔518斗gOb606e96

巴 ■ 「 ‖ = ■

16

3c8e8师6∑6〕f1dC三酮cbC扫md9耳50α2了387a8e

;17

710e7601a们d82C1b7m「d6b999e〔0a589169a68

图l6ˉ5去重指纹

「| ■≡=

…■~■■≈■平■-□描~…b

■□

□一=…←…己~=ˉ…}

■=

■『‖|尸|↑卜[尸{■■厂■「|

四口: !…k『…=蹈 …

;

\X80\Xo5`x9S`x13\x03\x00\x0O\x

1

\x80\X0g\x93`X】3\Xo3\×0叭x0O`X禽簿. 1

\x80\x0§\x95\xM\xo3\X0o\x0O\x

』】

∑_〕斗$

』§

wLˉ1

色… 互_夏熏了_二二了二∑_二二二(≡蹦

哩M◆



1

5j正e:5

…≈■≡■==≈=〗

\冀8O`义05`x9s\x16\潞03`x”`x卿\x `x8O\xO5\x951`x03\XO0\×Oo`x"\ \X8O\xO5\x951`x03\Ⅲ00\×oo\xm\…10

|′

图l6ˉ6爬取队列

随着时间的推移,指纹集合会不断增长’爬取队列会动态变化。

另外值得注意的是,在爬取的过程中’去重指纹集合是不断增长的’如果中途想要中断所有的

Spider重新进行爬取,需要先停止所有Spider’然后手动从Redis中删除指纹集合和爬取队列,再重 新运行°

至此, Scmpy分布式的配置已全部完成,通过简单的配置,我们就完成了多主机多Spjder的协同 爬取。

8.总结

本节通过对接ScrapyˉRedis成功实现了分布式爬虫,实现了多机协同爬取° 本节代码所在的地址为https://github.com/Python3WebSpjde″ScmpyComposjteDemo/位ee/scrapyˉredjs, 注意这里是scrapyˉredis分支而不是默认分支°

↑6.4基干B|oomP||te「进行大规模去重 首先回顾一下ScrapyˉRedis的去重机制°ScrapyˉRedis将Request的指纹存储到了Redjs集合中,每 个指纹的长度为40’例如27ad〔〔2e8979cdeeo〔9〔ecbbe8b十8仟51ede十b61就是一个指纹’是_个字符串。 我们计算一下用这种方式耗费的存储空间。每个字符占用l字节’即lB’l个指纹占用空间为40 B’ l万个指纹占用空间约400KB’l亿个指纹占用空间约4GB。当爬取数量达到上亿级别时’Redjs 的占用的内存就会变得很大,而且这仅仅是指纹的存储°Redjs还存储了爬取队列’内存占用会进一步

■■■■■■■

↑6 b





852

第16章分布式爬虫

提高’更别说多个Scrapy项目同时爬取的情况了°当爬取达到亿级规模时,SC呼yˉRedis提供的集合去 重已经不能满足我们的要求了。所以我们需要使用—个更加节省内存的去重算法—BloomFlltcr。 ↑.了解B|oo∏尸‖|te『

BloomFilter,中文名称是布隆过滤器,它在l970年由Bloom提出,可以被用来检潞一个元素是 否在-个集合中°B‖oomFjlter的空间利用率很高,使用它可以大大节省存储空间°BloomFilter使用 位数组表示一个待检测集合’并可以快速通过概率算法判断一个元素是否在这个集合中。利用这个算

| q

法我们可以实现去重效果°

2B‖◎◎丽乍‖te『的算法

【β■

本节我们来了解BloomFilter的基本算法,以及ScmpyˉRedis中对接BloomFilter的方法。 斡褪

在BloomFilte【中使用位数组来辅助实现检测判断°在初始状态下’我们声明一个包含加位的位 数组,它的所有位都是0,如图16ˉ7所示°

■口迟 ■

0

0

0

0

0

0

0

0

0

0

0

0

图16ˉ7初始位数组 □

现在我们有了一个待检测集合,表示为S~{x|,x2,…’x厕},接下来需要做的就是检测一个x是否已 经存在于集合S中°在B‖oomFi‖ter算法中,首先使用k个相互独立的、随机的散列函数来将这个集合 s中的每个元素映射到长度为m的位数组上,散列函数得到的结果记作位置索引,然后将位数组该位 置的索引设置为1°例如这里我们取片为3’即有3个散列函数,x|经过3个散列函数映射得到的结

果分别为l`4、8’x2经过3个散列函数映射得到的结果分别为4、6、10’那么就会将位数组的1`4、 6` 8、10这5个位置设置为l’如图l6ˉ8所示。

■■■■

迁i

■■■■

0

呜~≤~ =

■■■■

0

■■■■

0



■■■■

〗■■■

0

0

厂己 ■■■■

0

图l6ˉ8映射后的位数组

这时如果再有一个新的元素x,我们要判断它是否属于s集合,便会将仍然用k个散列函数对x求 映射结果’如果所有结果对应的位数组位置均为l ’那么就认为x属于s集合’否则x不属于S集合°

例如一个新元素x经过3个散列函数映射的结果为4` 6、8,对应的位置均为l’则判断x属于S 集合°如果结果为4、6、7,其中7对应的位置为0’则判定x不属于s集合。 注意这里厕、〃、k的关系满足厕>肋’也就是说位数组的长度m要比集合元素个数″和散列函数 k的乘积还要大°

合,我们来估计_下它的错误率°当集合庐{x|’x2’…’x厕}的所有元素都被k个散列函数映射到砸位 的位数组中时’这个位数组中某一位还是0的概率是:



幻\∧

` (|—斋) /{ l-

因为散列函数是随机的,所以任意一个散列函数选中这一位的概率为l/》″’那么lˉ1/m就代表散 列函数-次没有选中这_位的概率,要把S完全映射到″′位的位数组中,需要做肋次散列运算,所

创‖|(|■■司‖』∏‖』|口||‖‖||』■||‖‖|」●‖]|‖·〗‖|』■■

这样的判定方法很高效’但是也是有代价的,它可能把不属于这个集合的元素误认为属于这个集

■■]‖』司门」·』{|』』■可司」■|』|』‖凹■■□』□叼‖|《

0



l6.4基于BloomFilter进行大规模去重

853

以最后的概率就是lˉ1/″′的肋次方°

—个不属于s的元素x如果要被误判定为在S中,那么这个概率就是k次散列运算得到的结果对 应的位数组位置都为1,所以误判概率为:

腮(|ˉ+「≡. 根据:



片 `′







≈~

`!(!_湍)′

(‖

勿`|∧

|/

可以将误判概率转化为:

旦ln2≈塑≈07匹 13″





在给定炯、″时,可以求出使得/最小的k值为:

k=匹ln2≈0.7匹 门



也就是说,当k约等于′″与〃比值的0.7倍时,使得误判概率最小,这里将误判概率归纳为表101° 表↑6ˉ↑ 误判概率表

‖‖ ]] ∑【 ]〗 斗】 §] β] γ〗 目〗 ’∑ ·〗 ]∑ ∑ ∑]↓日βγ且’】

呵∏

吕优∧

乍↑

∧≡2

∧=3

∧刮

∧=5

∧=6

∧=7

∧=8

1·39

0。393

0°4"

2.08

0·283

0.237

0.253

2·77

0·22l

0·l55

0·l47

0·160

3·46

0.l81

0』"

0.佣2

0·092

0』0l

4』6

0·l54

0.08叫

0.%佣

0.056l

0。0578

0.佣〕8

4.85

0·]33

0·%18

0·佃23

0.0359

0.0347

003“

5。55

0·ll8

0.叫89

0.03佣

0·024

0.02l7

O0216

0.0229

6。24

0』05

0.0397

0。0228

0.0l66

00l4l

00133

0。0135

6.93

0·”52

0。0329

0.0l70

0.0ll8

0.呕43

0·"斟4

0·"819

0·"8%

7.62

0.0869

0.0276

0.0l36

0.008“

0。0佣5

0·"552

0·005l3

0。"5"

8。32

0·08

0.0236

0·0l08

0·啊6

0。叫59

0·"37l

0."329

0."314

9·0l

0°074

0。0203

0."875

0.叫92

0·"332

0."255

0。002l7

0·"1”

9。7

0·俩89

00l77

0。"7l8

0。"381

0.00244

0"l79

0.00I06

0·"l29

10.4

0.叫5

O0156

0."596

0."3

O·"183

0·"l28

0·"l

0·Ⅷ852

ll°1

0。佣仍

0。0138

0·"5

0·"239

0。"139

0·Ⅷ935

0.Ⅷ702

0·Ⅷ574

l1.8

O057l

00l23

0.酗23

O"193

O"107

0.哑92

O哑99

0。Ⅻ390

l2·5

0·05q

0·0111

0·00362

0。"l58

0·Ⅷ839

0·Ⅷ519

0。00036

0。Ⅷ275

13°2

0.0513

0·吧8

0。"3l2

0·"l3

0。毗63

OⅫ394

O"02“

0.刚l94

l3。9

0.叫88

0·"呕

0·"27

0."l08

0·0"53

0。Ⅷ303

0.0"196

0·Ⅷl4

l46

0.佣65

0。"825

0·"236

0.…5

0.刚27

0·Ⅷ236

0.Ⅷ147

0。ⅧlO1

l5。2

0.叫4

0·"755

O"207

0.刚7“

0。刚347

0.Ⅻl85

0°"01l2

7°46eˉ05

0.0145

第l6章分布式爬虫

854

(续)

∧=2

∧=↑

∧=3

∧=6

∧=5

∧=7

∧=8

m/∏

最优∧

23

l5°9

0。阻25

0.00694

0."183

0.O00“9

0."0285

0°"0l47

8.56e≡05

5·55eˉ05 5 05

24

l6.6

0.凹08

00O639

0·"l62

0000555

0.000235

0。000ll7

6。63eˉ05

4.l7eˉ05

25

l73

00392

0.0O59l

0。00145

0·0O佣78

0。"0l96

9翰q4e=05

5.l8eˉ05

3。l6e05

26

l8

0ˉ0377

0。"548

0。"l29

0·0叫l3

0。"0164

766eˉ05

4·08eˉ05

2.42eO5

626eˉ05

324e-05

l87e=05 l.46eˉ05

∧=4

27

l8.7

0°0364

0.0O5l

0.001l6

0·000359

0·000l38

28

19°4

O0351

0.00475

000l05

0。0003l4

00001l7

5°l5eˉ05

2.59eˉ05

29

20.1

0.0339

0.0畔

0.OⅧ49

0.0"276

9。96eˉ05

4.2“≡05

2。09eˉ05

l。lqeˉ05

355eˉ05

l69eˉ05

9.01eˉ06

30

20.8

0.0328

0°004l6

0O"862

0.0O0243

8.53eˉ05

3l

2l°5

O.O3l7

0·0039

0·Ⅷ785

0,0O02l5

7·33e←05

2.97eˉO5

1。38e.05

7』“ˉ肠

32

22。2

0.0308

0.00367

0·Ⅻ7l7

O000l9l

6·33eˉ05

2。5eˉ05

l』3eˉ05

5.73eˉ06

可以看到,当比值确定时,随着砸/〃的增大’误判概率逐渐变小。当加/″的值确定时, k越靠近 最优k值’误判概率越小。另外误判概率总体来看都是极小的,在容忍此误判概率的情况下’大幅减 小存储空间和判定速度是完全值得的。

接下来我们就将BloomFilter算法应用到ScrapyˉRedis分布式爬虫的去重过程中,以解决Redis内 存不足的问题°

3.对接Sc『apyˉRed|s

实现BloomFjlter时’我们首先要保证不能破坏ScrapyˉRedis分布式爬取的运行架构,所以我们

需要修改ScrapyˉRedjs的源码,替换它的去重类。同时BloomFilter的实现需要借助一个位数组’既 然当前架构还是依赖于Redis的,位数组的维护直接使用Redjs就好了°

首先我们实现-个基本的散列算法,可以将—个值经过散列运算后映射到_个叼位位数组的某_ 位上,代码实现如下: 〔1己55‖己5∩"aP(object): de十 1∩it (Se1十’ ∏’ 5eed): 5e1十。∏≡们

5e1十°5eed=5eed

de于∩aS∩(5e1+』 γa1ue): ret=0

十orji∩r日∩ge(1e∩(va1ue)): ret十≡Se1「.Seed*ret+ord(va1ue[1]) retum(5e1+。们ˉ 1)&Iet

在这里新建了-个‖a5‖‖ap类’构造函数传人两个值’_个是′′′位数组的位数’另_个是种子值 5eed,不同的散列函数需要有不同的5eed’这样可以保证不同散列函数的结果不会碰撞。 在∩a5‖方法的实现中,va1ue是要被处理的内容,在这里我们遍历了该字符的每—位并利用ord方

法取到了它的ASCII码,然后混淆seed进行迭代求和运算’最终会得到一个数值。这个数值的结果 由γ己1ue和5eed唯_确定’然后我们再将它和们进行按位与运算’即可获取加位位数组的映射结果’

这样我们就实现了-个由字符串和5eed来确定的散列函数°当厕固定时’只要5eed值相同’就代表 是同_个散列函数’相同的γa1ue必然会映射到相同的位置。所以如果我们想要构造几个不同的散列 函数’只需要改变其5eed就好了,以上便是一个简易的散列函数的实现。 接下来我们再实现BloomFilter’ BloomFllter里面需要用到k个散列函数,所以在这里我们需要

对这几个散列函数指定相同的Ⅶ值和不同的5eed值,在这里构造如下:

「巳‖=炉■【卜

■厚仆仕■

l64基于BloomFilter进行大规模去重



855

8L刚「I[『[R‖∧S‖‖0"B[R=6 ■〗’■

8l刚「I[『[R8I丁=3O

c1a5581oo∏]「i1ter(object); de十 j∩it (5e1f’ 5erγer’ key’ bit=B[刚「I[丁[RBI丁’ haS∩∩u刚ber≡8[刚「I[丁[R‖∧5‖‖0"B[R): #de「au1tto1〈<3o=1o’7374’1824=2^3o=128"B’Ⅶ己×十i1ter垄^30/∩a5∩∩uⅦber=1’7895’6970fj∩gerpri∩t5 5e1「°们≡1〈〈bit

} ■尸|‖|【■尸『|■『『「■■「「|‖■■=



′ ~尸=尸|止■|〖■■■||||■尸| ■■|匹■◆‖■■尸匡■【β『|↓||■【尸|巴■■尸尸■尸■■卜|【■「|尸匡■「′卜·‖‖■厂‖‖▲■



5e1于.5eed5=ra∩ge(∩aSh∩u‖ber) 5e1+。们3ps= [‖a5h例ap(5e1+。‖’ seed)十or5eedi∩5e1+.5eed5] Se1+。SerγeI=5erγer

5e1「.艇ey=低ey

由于我们需要完成亿级别数据的去重,即前文介绍的算法中″为l亿以上,散列函数的个数∧大

约取l0左右的量级’而m>肋,所以这里们保底在l0亿方右°由于这个数值比较大’所以这里用移

位操作来实现,传人位数bjt’将其定义为30,然后做-个移位操作1≤<3O’相当于2的30次方’ 等于1073741824,量级恰好在l0亿左右。由于是位数组,所以这个位数组占用的大小就是230bjt=l28

MB’而本文开头我们计算过,ScrapyˉRedis集合去重的占用空间大约在4G左右’可见BloomFjlter的 空间利用效率之高°

随后我们再传人散列函数的个数’用它来生成几个不同的5eed’用不同的5eed来定义不同的散 列函数’这样我们就可以构造一个散列函数列表,遍历5eed,构造带有不同5eed值的‖a5∩‖ap对象’ 保存成变量Ⅶap5供后续使用°

另外’5erγer就是Redis连接对象, key就是这个m位位数组的名称。

接下来我们就要实现比较关键的两个方法了’一个是判定元素是否重复的方法e×15t5’另一个是 添加元素到集合中的方法j∩5ert’代码实现如下: de千eXj5t5(5e1+’γa1ue): i+∩otγa10e: retur∩「a15e

eXi5t=1

千OI‖api∩5e1「.阳日p5: O仟Set=阳p.ha5∩(γ日1ue)

exi5t≡exj5t85e1十.5erγer。getbjt(5e1十.代ey’ o仟5et) retur∩eXi5t

de十1∩5ert(臼e1十’ va1ue): +or+i∩5e1+.帕p5: o仟5et=+.∩a5b(γa1ue) 5e1十.5erver。setbit(5e1「.代ey’ o仟set’1)

首先我们先来介绍1∩5ert方法,BloomFilter算法会逐个调用散列函数’对放人集合中的元素进 行运算’得到在腕位位数组中的映射位置,然后将位数组对应的位置置l’所以这里在代码中我们遍 历了初始化好的散列函数,然后调用其ha5h方法算出映射位置o仟5et,再利用Redis的5etbit方法 将该位置置l°

在eXj5t5方法中’我们就需要实现判定是否重复的逻辑了°方法参数γa1ue为待判断的元素,在 这里我们首先定义了_个变量e×j5t,然后遍历了所有散列函数对γa1ue进行散列运算,得到映射位

●厅■■■■

置’接着我们用getb1t方法取得该映射位置的结果,依次进行与运算°这样,只有getbjt得到的结 果都为1时’最后的ex15t才为丁Iue’表示va1ue属于这个集合。其中只要有—次getb1t得到的结果 为o’即呐位位数组中有对应的0位’最终的结果exj5t就为「a15e’代表γa1ue不属干这个集合。这

|『

样’此方法最后的返回结果就是判定重复与否的结果了。

到现在为止BloomFilter的实现已经完成,我们可以用一个实例来测试_下’代码如下: 》‖}





〔o∩∩=5tr1〔t【edj5(‖o5t=,1o〔a1bo5t』’ port=6379’ pa55刚rd=!↑oobared|) b十≡81oo『‖「j1ter(〔o∩∩’ !te5tb+‖’ 5’ 6) b+.1∩5ert(‖‖e11o|)



||6



第l6章分布式爬虫

856

b+.j∩5ert(!‖or1d!) re5u1t≡b「.exists(|‖e11o|) prj∩t(boo1(resu1t)〉 Ie3u1t=b十。exi5t5(‖Pyt∩o∩‖〉

pⅢi∩t(boo1(re5u1t))

在这里我们首先定义了一个Redjs连接对象,然后传递给BloomFilter,为了避免内存占用过大,



这里传的位数比较小,设置为5,散列函数的个数设置为6°

■■■』

首先我们调用j∩5ert方法插人了‖e11o和‖or1d两个字符串,随后判断了—下‖e11o和pyt∩o∩ 这两个字符串是否存在’最后输出它的结果’运行结果如下:



『rue

「己15e



很明显’结果完全没有问题,这样我们就借助于Redjs成功实现了BloomFilter算法°



下面我们需要继续修改ScrapyˉRedis的源码’将它的去重逻辑替换为BloomFilter的逻辑,在这

里主要是修改R「pDupe「j1ter类的reque5tˉ5ee∏方法,实现如下: de于Ieque5tˉSee∩(5e1+’ Ieque5t): 「p≡5e1十.reql』eSt≡+j∩gerprj∩t(reql』eSt)

0|

‖」

d

if5e1+.b十。exj5t5(十p): 山

re恤I∩『Il」e

5e1+.b于.i∩5ert(+p) retum「a1Se



对于BloomFilter的初始化定义’我们可以将 de十

j∩it

方法修改为如下内容:



{||‖□

首先还是利用reque5tˉ十1∩gerprj∩t方法获取Request的指纹’然后调用BloomFjlter的exj5t5方 法判定该指纹是否存在°如果存在’证明该Request是重复的’返回丁rue;否则调用BloomFilter的 j∩5ert方法将该指纹添加并返回「a1se°这样就成功利用BloomFilter替换了ScmpyˉRedis的集合去重。



i∩jt (5e1「’ 5erver’ 促ey’ debug’ bjt’ ha5h∩u∩ber): 5e1「·5erγer=5eIγer











·



5e1「.b「≡B1oo们「j1ter(5erγer’ 5e1千.代ey’ bjt’ ∩日5h-∩u们ber)



5e1十。h35h∩u帅er=ha5h∩u阳ber

5e1+·1ogdupe5=『rue



5e1十.bjt≡bjt



5e1「。代ey=促ey Se1+.deb0g=debu8

其中b1t和‖a5h∩u们ber需要使用千ro"‖ 5ettj∩gs方法传递’修改如下: @〔1a55‖ethod

ha5∩∩u"ber=5etti∩g5.getj∩t(’8[叫‖「I∏[R肌5‖‖0"8[R,’ 8[刚「I口[Rˉ肌5‖-‖0‖8[")

Ietur∩〔15(5er`′eI’ key≡促ey’ debug=debug’ bIt=bi[」 ∩a5∩∩0Ⅶber=∩a5∩_∩u帅er)



■■』‖‖]司|』■】』‖

de「千ro|∏ˉ5ettj∩g5(〔15」 5etti∩g5): 5erver≡get-rediSˉ千r咖-5ettj∩85(5ettj∩g5) 代eyˉde伯u1t5.凹p[「I[T[R-Ⅸ[γ%{』tme5taⅦp』: j∩t(tme。tj爬〈))} debug≡5ett1∩85·getboo1(|山P[「I∏[R0[8"‖’ D‖」p[「I[丁[R0[8Ⅶ) bit=5ettmgs.geti∩t(|BL刚「I∏[R8I『0’ B儿刚「I∏[RBI『)

尸=■■■■■■■■『匹■■■■■

其中常量O0p[「I∏[R0[806和B[000‖「I[「[RBI『统_定义在de炮ultspy中,默认如下: 8[刚「I∏[R肌5‖‖l朋B[R=6



ˉ

为了方便使用’本节的代码我已经打包成了_个Python包并发布到了PyPi’链接为https:〃pypi. python.org/pypj/scrapyˉredisˉbloomfilter°

大家以后如果想基于ScrapyˉRedis对接BloomFilter,直接使用scrapyˉredjsˉbloomfilter包就好了,

□]」■‖‖]‖]□■■∏」■Ⅵ』■‖|||』』■■■可‖‖‖‖|■■■■】‖‖

4.使用

■■■

到此为止,我们就成功实现了BloomFilter和ScmpyˉRedjs的对接。

‖(|





l6.4基于BloomFilter进行大规模去重

857

|尸|‖



不需要再自己实现_遍。可以直接使用pip3来安装’命令如下:

可■巴β△厂|卜

pjp3 j∩5t己115crapyˉredj5ˉb1oo∏‖+i1teI

使用的方法和ScmPyˉRedis基本相似’在ScrapyˉRedis的基础上,接人BloomFiltcr需要修改如

}|匹■厂伶■■『■≡■厂‖

下几个配置: #去支类,要仗用81OO∏「11ter讨替换山p[「I[丁[【αA55

D0p[「∏丁[RαA55= "5cmpγ-redi5-b1oα∏十i1ter。dupe+j1ter°日「PDupe「i1ter" #俄列函数的个数,耿认为6’可以自行修改 8l刚「I∏[R‖∧5‖‖0‖8[佣=6

Ⅱ伊「β庐

#B1o咖「i1teⅢ的bit拳数,耿认3o, 占用128"8空间,去亡数世级1亿 8LⅫ「I∏[R8I『=3O

这军进行_下说明°

β ‖ 》

□00p[「I∏[R〔[∧55:去重类,如果要使用BloomFjltcr,需要将山p[「I[『[R〔[∧55修改为该包的 去重类。

□8[刚「I[『[R肌5‖‖0"8[R:BloomFiltcr使用的散列函数的个数’默认为6,可以根据去重量级 自行修改。

●■卜‖■尸

□8[D‖「I∏[R8∏:前文所介绍的81oo"「11ter类的b1t参数’它决定了位数组的位数’如果 8["|‖「I[丁[R8I「为30’那么位数组位数为2的30次方’将占用Redisl28MB的存储空间,

去重量级在‖亿左右’即对应爬取量级l亿左右°如果爬取量级在10亿、20亿甚至l00亿, 请将此参数调高° 5.测试

在源代码中,附有一个测试项目’放在tests文件夹下,该项目使用了scrapyˉredisˉbloomβlter包 来去重’ Spider的实现如下: 「ro‖5〔rapγmportReque5t’5pjder

■ )

〔1a55「e5t5pjder(5pjdeI): ∩a‖e= `te5t‖

ba5eur1= ‖∩ttp5://州°baidu.co∏l/5?wd=0

』■



de「5t己rt-Ieque5tS(5e1+〉: #先发起1O次请求

+Orji∩m∩ge(10):



l』I1=5e1「.ba5euI1+5tr(i)

yje1dReque5t(ur1’〔a11bac|(=5e1+.par5e) p





#再发起包含上述请求的玄Ⅲ请求 千orjj∩r己∩ge(1oo):

卜广‖}尸|七恬|庐|血『|仁

ur1≡5e1千.baSe‖r1+5tI(1)

yie1dRequest(ur1’〔a11b己c|(=se1「.par5e) de十par5e(se1十’ re5po∩se):

5e1千.1o8ger。debu8(0∩e5po∩5eo千 0 +Ie5po∩5e。uI1)

其中, 5tart-reque5t5方法先循环了10次’构造了参数为0~9的URL’然后重新循环了l00次,构

造了参数为0~99的URL’那么这里就会包含l0个重复的Request。这样’后发起的l00次请求的前10

=■尸

次请求就会被过滤掉’实现请求去重。

△■「匹■厂

要运行测试代码,可以先把5〔rapyˉRed15ˉ81oo『∏「11ter包的源码下载下来’命令如下:

》叼■■厂价|抄「|[■■■「||||℃抄『||[■■■厅‖‖|[■■『『||匹■▲■「

gjt〔1o∩ehttp5://git∩ub。co‖/pytho∩3‖eb5pider/5cmpγ【edi581oo‖‖「i1ter。gjt

然后进人tests文件夹,运行测试项目测试一下; 5〔Iapy〔raN1te5t

可以看到最后的输出结果如下:



■|■‖■』■■■】‖汕∏■■□‖〗』■】』■】□]■】】●‖‖』■】■■■‖‖』■】■Ⅺ‖‖』□■■

858

第l6章分布式爬虫

{0b1o咖十i1ter/千i1tered! : 1o’ |do们∩1oader/reque5tˉbytes0 : 34021’ 0do"∩1oader/reque5t〔ou∩t‖ 目 1oo’ !do训∩1oader/reque5t爬恤od〔o0∩t/C[丁|: 1OO’ |do们∏1oader/re5po"seˉbyte5『 : 72943’ ,dow∩1oader/re5po∩5ecou∩t0 : 1OO’ 0dow∩1oader/re5po∩5e5tatu5〔ou∩t/2OO‖ ; 1OO’ 0十1∩15hre己so∩0 : 0十i∩15hed‖’

可以看到最后统计的第_行的结果: 0b1oo‖十i1ter/千i1tered: 1o’

这就是BloomFilter过滤后的统计结果’可以看到它的过滤个数为l0,也就是说,它成功将重复

的]0个Reqeust识别出来了,测试通过° 6案例集成

对于上_节ScmPyˉRedis分布式的实现,如果我们需要集成BloomF||ter,使用上述的scrapyˉredjsˉ bloomfilter包即可轻松实现。

再上_节代码的基础上,我们在A、B`C三台主机上分别安装scrapyˉ『edjsˉbloom∏lter,命令如下: pjp〕j∩5t3115〔mpyˉredj5ˉb1oo"+i1teI

D(」p[「I[丁[R〔[A55= "5〔mPy=redi5b1ooⅦfj1teI.dupe+j1ter·R「PDupe「i1ter,’ 8l刚「I∏[R8I丁≡20

这里我们修改了5〔‖[00[[R和00p[「I[丁[【〔[∧5S’使得项目既可以使用Sc『apyˉRedis原有的爬取 队列’又可以依赖BloomFilter进行去重,另外我们根据爬取量级预估了8[00‖「I[丁[R8I丁为20,其 他的保持默认值即可。 修改之后重新运行爬虫: 5〔mpy〔r己W1bOO氏

这时候运行效果和之前是_样的’不过背后的去重逻辑已经修改为了BloomFilter’这时候我们

可以使用RedisDesktopManager来查看当前BloomFilter的Key在Redis中对应的结果’如图l69所示。 v■匝a肋…乃3扫

|m…

v■哟◎《q》

≠ ●…户钧户中…≈

峭 用【{

B●‖









B□













·

■□ ■■

【、乞卫·匝·Ⅲ』·△0ˉ■罢.二驾g且吕曰旦客〗∑芭巫占吕 二□日陵企已.≥吕b且●】.二:?二凸工口ˉ口ˉc三°』pˉ旦□ˉ■且土工0Z ‖ =

△$□□b■△

且°且口二芭目◎三o三□且·∑色且

·. ˉ·=·.□令哪ˉ·ˉ △二口 ˉˉ=■.□℃二片—P□I口】·【三·ˉF‘x·■口I.∑■I

■=■m

=■

=□□■■■





■■

■△■



■□●△■■■■±■





■』●尸■△■

0=■■■































‖…■∏茂△b·△□ · 吁























』■…{创 山



■凸5C》 △

·■ ■



▲□』L■△△△

△ △ △

°■凸 ■ △ □ ° △=·巳≡

勺 □ 、



b□ ■ ■=L

□■1●=●□■=LL~ ·□·□·=■ △■■□■□△◇二·▲●▲甲△q□

□』

.·=·三忽=.·巳◆=一■

■…们)

≡】南Ⅲ晓≡〗]

—~且=1■』〗悦旦丝?→〗●〗伯1=?■0铂’M鲍】】=】黔』1 1…1】0〗馋汕∩…〗■■q=】呻】】骋!尝已0〗M四】0〗0皿】·】·四11p0■?u●1■TU■11…

】11……〗■

■立7伯)

』■1切!≡】≡…】』 11…j…1咱】】』巴=Ⅲ…1】宅≡【=1墨u…】…≈〗m·】…】】的】m…

●1〗·1…=】睡

|舅瞬‖岭』】O〗O

凸←

d〗…』牵1知2=!●】(

m‖≡·污·□?□Z“∏·】■口.‘D【.r△Y百γ;· 『~,一〗巨冗丁孤5m亏〖己二孽∑5∑ ..【 【□■

=O=w·Ⅲ0=△□□r=q…

7■



■』 □ 型

BloomFilter结果



图l6ˉ9

0【■】噎=M =-□ ≡■屯…

△』· · ·.o〗…■=.‖口 °.ˉ=1~ ■



■…《q

…●1粥】0』1鲍…馋1:

■D

凶■■



B冗●

■=

■■司|』■」■‖‖■●||·』□]|」』■■Ⅲ■‖』】‖』■』■‖‖‖|」□】■■|』■■司|』Ⅲ_■】

》 扦 ≈● ·△●■









||



■凸3o



中扣。















邹广尸 、…T、 ` ``0『 引 。 亚凸 p′瑰→…潭p引





l● 0

■立0 (伪

●凸2侧

γW韵ˉ °■蹿怨冶■……

`厕e出远:….

!脚呻γ, 汕

h

v酌°k厢q陛‘■ 7陇幽e唾田…雹刨

….……ˉ . . ˉ.…蓖….ˉb·.…….ˉ. ..■ . .……-. ˉ .ˉ·出…撼岭

|}

v■…出回



:=…ˉ:蕊鳃ˉⅪ:冒.ˉ:ˉ…ˉ.…ˉ ′i.. . . ^

Ⅶ…:

?扛…ma厕U‘…77

」■‖|尸||』』■∏|||·』|二■口‖』·司□‖‖□·||‖』·|』‖|||‖□`』

然后增加如下配置:

‖‖|■∏』】‖』』■‖‖‖‖』■■‖||」∩■□|司|」■司|||」■‖||‖|』]|||』■{||■■

‖千j∩ishti雁|: d己tetme.datetj眶(2O2O’8’ 11’ 9’M’ 3O’ 』19597)’ ‖1og-〔ou∩t/O[8Ⅶ0 目 202’ 『1o8-〔ou∩t/I‖「0‖: 7’ 『『陷『∏u5age/阳aX’: S4153216’ 爬川05age/5tartup|: 541S3216’ re5po∩5e-rece1γedcou∩t|: 10o’ !5C们eαu1er/dequeued/red15! 目 10O少 05〔hedu1er/e∩queued/redjs‖: 1o0’ |5tarttme! : d3tetj爬.datetme(2O2O’8’ 11’ 9’ 34’ 26’ q95O18)}

l65基于RabbitMQ的分布式爬虫

859



我们可以发现,有一个叫作boo代:b1oo们+j1ter的Key出现了,点击该Key并切换到BlnaIy查看

模式’可以看到其真实值’它是~个非常长的二进制串°由于_开始所有的位都被初始化为0了,所 以绝大部分位是0;在爬虫运行的过程中,部分位经过计算并设置为l ,所以可以看到部分位的结果 为l’随着爬取的进行,被置为l的位数也会越来越多。 7.总结

以上便是B|oomFllter的原理及对接实现’使用BloomFjlter可以大大节省Redis内存’在数据量

大的情况下推荐使用此方案°

本节代码参见: ht印s:〃githUhcom/Python3WebSpidm/ScraWCompositcⅨmo/h配/scmpyˉ‖℃djsˉbloomhl贮r’ 注意是scrapyˉredisˉbloom倔|ter分支°

↑6。5基干Rabb|tMQ的分布式爬虫 前面我们了解了Scrapy如何利用Redjs实现分布式爬虫’可以注意到’当爬取数量过大时’Redis

占用的内存非常大’因此对于数据去重’我们使用了BloomFilter来进行优化,大幅减少了Redis的 内存占用。

不过,现在我们似乎依然面临_个问题,爬取队列仍|日是基于Redis实现的’那它同样会占据非 常大的内存呀!其实在—般情况下’Redjs作为分布式爬取队列是完全够用的。但在数据量比较大’ 比如爬取上亿级别数据时,Redis消耗的内存也是比较大的,这时候我们可以考虑将爬取队列进行迁移° 迁移到哪里呢?仔细想想,爬取队列类似一个消息队列,可以先进先出`先进后出、按优先级进 出等,只要能满足类似的需求就可以°现如今,消息队列中间件也有很多,如RabbitMQ、RocketMQ 等’它们都可以用来做爬取队列的实现°

本节我们就选取目前比较流行的RabbitMQ来实现—下Scrapy分布式爬虫吧!

p

口卜卜







↑。准备工作

在本书48节中,我们已经初步了解了RabbitMQ的基本原理和使用方法’如果你还不了解

RabbitMQ是什么’建议先回看_下前面的基础内容°

在本节开始之前’请确保已经正确安装好了RabbitMQ和Python的plka库’具体的安装说明可

以参考本书48节°

2对接Sc『apy

RabbitMQ就是_个消息队列’那它怎么对接Scmpy实现分布式爬取呢?通过ScrapyˉRedis的源 码,我们可以知道ScmpyˉRedjs利用Redjs实现了—个爬取队列’所以同样的原理’我们可以仿照



ScrapyˉRedis的实现’将Redjs换成RabbitMQ°

仿照ScrapyˉRedis的源码,我们先来解决RabbjtMQ的连接问题’首先定义_个〔o∩∩e〔toi∩对象: P

i呻Ortp让a

仿》

de十千IoⅦ5ettj∩g5(5etti∩85):

〔o∩∩e〔tjo∩-para『∏eter5≡5ettj∩g5.get(!R∧BBm仙〔O‖‖[〔∏O‖』 p∧R州[『[R5|’ 【∧88mQˉ〔删‖[〔丁I酬P∧R酗〔丁[R5〉 〔o∩∩ectio∩≡p1|(a。B1O〔|(i∩8〔O∩∩e〔tjO∩(p1|(a.〔O∩∩e〔tjo∩pamⅧeter5(**〔o∩∩e〔tio∩一Pam|∏eter5))

〔∩a∩∩e1≡〔O∩∩eCtio∩.〔ha∩∩e1() β

retuI∩〔h己∩∩e1



这里定义了+ro们—5ett1∩g5方法’可以根据全局的R∧88I丁‖q〔0‖‖[〔丁I0‖-pAR∧‖[丁[R5来创建一个

‖}}



RabbitMQ连接对象,返回cba∩∩e1信息。另外在ScrapyˉRedis中’优先级队列是使用有序集合来实现 的,每个元素都有一个分数值,Redis可以根据分数来排序’这样分数越小的就排到越前面’下次就



』 ■

』·■■■

·■·]|」■‖■■□』■∏{

860

第l6章分布式爬虫

会被优先获取。

」■■□】■■‖|‖

C1己5gPriOIityα』eue(BaSe):

「oI〔e「1u5们=5〔‖[凹k[R仙[0[ 「O只〔[「山5‖’

pr1ority-o仟5et≡S〔‖[山l[Rα」[U[p【I0RI∏0「「5[丁): 5e1十.1∩ited=「a15e 5e1+.domb1e=dur己b1e

se1于.q‖eueˉopemtor=5e1千。5erγer.queuede〔1are(queue=5e1f.key’ aIgu爬∩t5={

5e1千.1∩jted≡『rue

00

■■□■〗

ex〔ept〔∩a∩∩e1〔1o5ed8y8ro促eIa5e: 1ogger.error("γo0‖aγecha∩8edqueueco∩千j8ur己tjo∩』 you 00 "uStde1etequeue爬∩ua11yor5et `5〔什[山L[∩凹[0[「0R〔[「l05‖` "to『me’eⅢrordetai1%5"%str(e.ar85)’ex〔j∩千o=『Iue)

佃 』

1og8er。deb‖8(‖Quel」eoperator%s』’ 5e1十.queueˉopeIator)

■■■

0×ˉ阳xˉprioIity‖:帕x-prjority }’ durab1e=d‖r己b1e)

■■司‖■■司

i∩it (5erγeI’ 5pider’ Rey)

■‖‖=■■·

j∩jt (5e1十’ 5erVer’ Sp1der’炔y’ 爬x-prjority≡5〔‖m」[[Rˉα」[0[≡灿X-pRI0日IⅣ’ durab1e=5〔‖[D(」l[Rˉq」[0[ˉ卯M8[[’

■■』□‖□】·|■■■可‖

在这里我们仿照ScrapyˉRedis的prjorjtyQueue来进行改写’写法如下:

5uper(priorityQueue’ se1千). try8

凸■∏■■

布消息的时候添加优先级参数。

de于

■‖』■({‖‖

那RabbitMQ怎么实现优先级队列的功能呢?48节我们也学习了,RabbitMQ已经提供了对优先 级队列的支持,需要在声明队列的时候设置xˉⅦaxˉprioI1ty参数来设定最大的优先级数量,同时在发

Se1+.i∩1ted=「a15e

retl』r∩O

retuI∩5e1十.qoe‖e-opemtor°眶thodⅦe55己ge-cou∩t

de于poP(5e1十):



‖·〗』《

de千p05∩(5e1「’ reque5t〉: prjorjty=Ieque5t.prjority+5e1千.pIiority-o仟5et i十priority〈0: prjority=0 de1iγeIy一mde=2j「5e1十°dumb1ee15e‖o∩e 5e1十.serveI.basi〔ˉpub1j5h( ex〔ha∩8e=0 0’ pIOpertjeS=pi阳.8a5i〔pIOpertje5( Prjority=pIioIjty’ de1iγery-网de=de1jγery-Ⅷde )’ rOut1∏8-促ey=5e1十°Rey’ bodγ=se1千· e∩codeˉreque5t(reque5t) )

■■可■β■‖一■■‖《

de十—1e∩_(5e1十): j十∩otha5attr(5e1十’‖queueˉopemtor0):

■■‖■■

5e1f.priorjty-o仟set≡prioIjtyˉo仟5et

贬t∩od十ra腮’ headeI’ body=5e1十.5erγer.basic一get(queue=5e1千.keγ’ al』to3〔代≡『Iue)

i+body:

retur∩5e1+. decodeˉreque5t(body)



1∩jt方法’这里自定义了—些参数’如d0Iab1e代表是否持久化, ; 认读取了配置 1∩1t万法’这里目定义『—些参数’如d0Iab1e代表是否持久化,默

」■乙■

‖」‖||{」■□■曰||」】日||‖】□∏」□·‖■■‖

接着对于p05h方法’和前文的样例—样,调用了ba5ic-pub1j5h方法’不过由于这里支持优先级, 所以额外传人了peopert1e5对象并指定了pr1orjty。

□勺」■■』·

5〔‖[山[[R仙[0[山∩∧8[[’其值为丁rue°另外优先级的最大值帕xˉpriorjty默认读取了配置5〔‖["[[【 00[0[ ‖‖∧XPRIO∩IⅣ’其值为10O°在 1∩it 方法中’最关键的就是queue—de〔1are方法’它用来声 明一个消息队列’指定了参数×ˉ"axˉprjor1ty为"ax_priorjty,代表这是_个支持优先级的队列’最 大优先级的数值为Ⅷax-pr1ority。

』■|」■‖|

首先对于 自元叮十

‖■

| l65基于RabbitMQ的分布式爬虫

86l

■』

对于PoP方法,则是使用了ba5i〔ˉget方法并设置了autoa〔代参数为丁rue’这样便可以从队列中

取出一个当前优先级最高的消息并返回了°另外对于de〔ode-reque5t和e∩code-reque5t方法’其原 理和ScrapyˉRedis_样’这里就不再赘述了°

对于Scheduler’基本原理就是将Queue对象更换为刚才声明的PrjorjtyQueue对象,同时-些初 卜

始化参数通过5ettj∩gS获取即可° 这样我们就成功将爬取队列迁移到RabbitMQ里面了°





以上的内容我已经整理发布了—个Python包’叫作GempyRabbitMQ,其GitHub链接为: https://gjthub.com/Gerapy/GerapyRabbitMQ’安装方式也非常简单’只需要pip3安装即可:

■■『|■厂|Ⅶ■卜|||}厂□口■『■尸|■尸|■||||■厂[『|■■|■卜|·『’|[=■「〉

pjp3i∩5ta11ger己pyˉrabbimq

接下来我们就基于GerapyRabbitMQ,把上-节基于Redis的爬取队列迁移到RabbitMQ上° 3.迁移

安装好GerapyRabbitMQ包后,我们需要更改如下配置: 5〔‖[D|」l[【="gerapy=rabbit咽·s〔∩edu1er·5〔hedu1er圃 5〔‖[山t[RO[0[旺γ= ‖%(5pider)5ˉ【eque5t5! R∧88∏刚〔删‖[〔丁I删p∧R州[丁[R5={ 0∩o5t0 : 0192.168·2.30



这里首先需要更改5〔‖[00[[R,切换到GempyRabbitMQ里面定义的调度器类’然后调度器队列的 名称格式也可以定义’这里定义为5〔‖[Dl」[[Rα」[0[Ⅸ[γ,意思是SpideT名称和Requests的组合,然后

趴8B∏贬〔删‖[〔「I侧P∧R∧)‖[『[R5就是RabbjtMQ的连接对象’其参数可以参考htms;//pikaJ℃adthedocs.io/ en/stab‖e/modules/parametershtml里面的说明°

注意如果出现连接失败的问题’是因为默认情况下RabbitMQ只允许Guest用户使用1oca1host访 问,妥解决这个问题’请参考htms://mbbitmq.docs.pivotaljo/37/mbbj卜webˉdocs/accessˉconUolhnnl 里面的解决方案°

同样地’A、B`C三台主机都需要修改为同一个RabbitMQ地址’重新运行就可以实现用三台主 机协同爬取了’分布式爬取就完成了。

具体的运行方式和l63节是—样的,这里不再赘述°

[尸户||庐卜||■【「‖卜『β}快|卜

4总结

本节中我们介绍了利用RabbitMQ实现分布式爬取的过程,成功将爬取队列由Redis更换到了 RabbitMQ上,解决了Redis的内存占用问题°

本节代码参见: https://gjthuhcom/Python3WebSpide【/ScrapyCompositeDemo/tree/gerapyˉmbbitmq’ 注意是gempyˉrabbi(mq分支°

■|巴∩【厂|户|〖尸『||卜‖广『||尸[广‖‖‖户』■『‖[‖[[■■『‖‖■尸‖‖||』【伊‖

本章的内容到此就结束了。在这—章’我们了解了分布式爬虫的原理,并介绍了Scrapy分布式爬 虫基于Redis的实现以及—些优化方案。有了分布式爬虫的加持,一些超大规模数据量的爬取就可以 得到有效解决了。

「↑6 [

||

爬虫的管理禾

7

q

■‖|一■■

本章中’我们就来了解—下分布式爬虫在部署万面可以采取的—些措施,以方便地实现爬虫任务 的批量部署和管理。



改动的话,那么还需要额外把改动同时更新到所有主机上,操作非常烦琐,并且也容易出错°

Ⅵ|

在前_章中,我们成功实现了Scmpy分布式爬虫’但是在这个过程中我们发现有很多不方便的地 方°比如在将Scrapy项目放到各台主机上运行时,我们采用的是文件上传或者Gjt同步的方式’这样 需要各台主机都进行操作’如果有100台、l000台主机’那么工作量可想而知。另外’如果代码需要

| { |{

」 第 】7 章

本章主要介绍两种Scrapy分布式爬虫管理方案:基于Scrapyd的管理方案和基于Kubemetes的管 理方案°

↑7.↑



Sc『apyd和Sc『apyd∧P|的使用

↑.了解Sc『apγd Scrapyd是一个运行Scrapy爬虫的服务程序’它提供-系列HTTP接口来帮助我们部署、启动、 停止和删除爬虫程序。Scrapyd支持版本管理同时还可以管理多个爬虫任务,利用它我们可以非常 方便地完成ScIapy爬虫项目的部署任务调度° 2.准备工作

■■可‖■■·】■■‖·‖‖」日

在上_章中’我们学习了Scrapy框架,利用它可以快速开发~个爬虫程序。Scrapyd又是什么呢? 跟Scrapy相比’Scrapyd多了_个字母d,这个d其实就是部署(dep‖oy)的意思,所以Scrapyd就是 为了方便管理和部署Scrapy爬虫程序而诞生的°本节中,我们先简单了解下Scmpyd及其用法°



请确保本机或服务器已经正确安装好了Scrapyd,安装命令如下: pjp3j∩5ta115〔mpyd

更详细的安装流程可以参考: https://setupscrapecenteI/scrapyd。 安装并完成Scrapyd相应的配置之后’我们直接输人5〔rapyd即可启动对应的服务’命令如下:

』‖

S〔Iapyd

运行之后,会有类似如下的输出:

这就代表Scrapyd已经启动成功了。

3.访问Sc『apyd

勺当·〗‖|√‖

[[au∩〔her] 5〔I己pyd1.2.15tarted:阳x-pIo〔=8’ r0∩∩er≡‖5cmpγd。I0∩∩er

q

Scrapyd默认会在6800端口上运行。访问服务器的6800端口,我们就可以看到-个WebUI页面

{」□

了。本案例中,我们依然在A( l92.l68.23)服务器上启动Scrapyd服务。启动完成之后’我们打开

『卜巴



Scrapyd和ScrapydAPI的使用

863

http:〃192.l682.3:6800/’即可看到类似如图l7ˉl所示的页面° ÷ ˉ》G

O↑92。06a23:68O0 酗

Sc『apγd

}》}「



l7.l

∧vai|ab|ep『Ojects: ●」◎bS

●LOQS ●OocuⅦe∩tat‖o∩

"◎wtoschedu|easp‖de『?

′「

丁os◎hedu|easp『de「γ◎u『旧edtouset∩e∧尸| (th『s呢bU!|so∩‖W◎「Ⅷo∏|t◎『‖∩g) 巳amp‖eus『∏g◎u「‖: cur1h七七p8//1◎ca1h◎日七86800/Sch●du1e·js◎nˉdP正◎j●c七■d●£■u1七=d□P1dex■■…■P土der

p

尸o「mo「e『∩↑o∏∏at|o∩abou↑协e∧p|,See巾eSc『日Dmdocume∩tatj◎∩





图17ˉl

Scrapyd页面



0



}■



[尸||『|卜



如果可以成功访问到此页面’那么证明Scrapyd配置就没有问题了°

如果访问失败’那ScIapyd监听的地址很有可能是127.O.O.1,这是默认配置°此时可以修改 scrapyd.conf文件’将b1∩daddre55修改为0。o.o.o’具体可以参见https:〃scrapyd.readthedocsjo/en/ stable/configh1m‖#config。

比如’可以在当前命令行所在目录下新建一个scrapydconf文件’将其内容进行如下修改: [5〔rapyd] b1∩daddIe$5≡0.0。O·0

http-port

≡680O

这样就指定了Scmpyd可以被公开访问,同时运行在6800端口。修改完成之后’再次重启Scrapyd’

}卜

它应该就可以被访问到了°







4Sc『apyd的功能

Scrapyd提供了_系列HTTP接口来实现各种操作,这里我们可以将接口的功能梳理-下’以 Scrapyd所在的IP192』6823为例来进行说明。

注意此处伎用cur1命今米模拟HTTP的各种请求’你也可以使用其他工具(如Postman、Python

等)米进行请求,效果都是一样的。另外,这里伎用的IP是A主机的IP’请自行替换为你的 Scrapyd服务所在主机的lP°





●daemonstatus.json

这个接口负责查看Scrapyd当前的服务和任务状态,我们可以用cur1命令来请求这个接口,具体 如下:

〔ur1∩ttp://192.168.2.3:6800/d己e∏℃∩5t己t05.j5o∩

这样我们就会得到如下结果:

{"5tatu5|! : "o|("’ "「1∩15hed"; 9O’ "ru∩∩i∏g口: 9』·∩ode-∏a睡.: αv∏U"’ "pe∩dj∩g倒: 0}



第l7章爬虫的管理和部署

返回结果是JSON字符串,其中5tatu5是当前运行状态,fj∩15hed代表当前已经完成的Scrapy任

务, ru∩∩i∩g代表正在运行的Scmpy任务, pe∩dj∩g代表等待被调度的Scrapyd任务, ∩ode∩a们e就是 主机的名称。

●addversion.jsoⅢ

这个接口主要用来部署Sc田py项目。在部署的时候,我们需要首先将项目打包成egg文件’然后 传人项目名称和部署版本。 我们可以用如下方式实现项目部署: cuⅢ1http://192.168。2。3:68O0/addγersio∩。j5o∩ˉ「project=<proje〔t-∩a爬〉ˉ「ver5jo∩=v1ˉ「 egg≡<pIoje〔t-∩a爬〉。egg

这里ˉ「即代表添加一个参数,同时我们还需要将项目打包成egg文件放到本地。另外,还需要 将〈proje〔t-∩aⅧe〉替换成真实的项目名称。 这样发出请求之后’我们可以得到类似如下结果: {闽statu5": "o揽.’"spjdeⅢ5": 3}

这个结果表明部署成功,并且其中包含的Spider的数量为3。

‖」·】】可‖‖」■】】□】口●■■】‖〗·■勺‖‖□】勺‖||《』‖‖』□■·】‖‖■刁‖||··‖|■】‖‖|』·】‖‖|‖{||二■∏■可当■可|‖

864

注意Spjder的数量视具体的项目为准’不同的项目包含的Spjder数量可能不同,此处样例为3. 使用此方法部署可能比较烦琐,后文会介绍更方便的工具来实现项目的部署°

我们可以用如下方式实现任务调度:

这里需要传人两个参数: proje〔t即Scrapy项目名称’5p1der即Spjder名称。 返回结果类似如下: {"5tatu5": "o代圃’ "jobjd": "6487e〔79947edab3∑6d6db28a2d86511e8247』44"}

类似于执行了如下命令: 5cr己pγ〔r己w1〈5pjdeI-∩己爬〉

这就相当于用Scrapyd启动了对应项目的_个Spjder°Spjder是由Scrapyd运行的,运行之后就相

当于运行了一个任务,其任务标识代号就是jobjd’我们可以根据这个jobjd来查看或操作该Spjder的 运行状态° ●cancel.jsoⅢ

这个接口可以用来取消某个爬取任务。如果这个任务是pe∩dj∩g状态’那么它将会被移除;如果 这个任务是ru∩∩i∩g状态,那么它将会被终止° 〔0I1bttp://192.168.2。〕:68oO/c日∩〔e1°j5o∩ˉdproject≡<proje〔tˉ∩a爬〉 ˉd job≡6487ec79947edab326d6db28a2d86S11e82q7444

‖‖』‖||』」‖‖‖‖□‖

我们可以用下面的命令来取消任务的运行:



‖』■■■■〗|||||·】■|』■可」·||‖』』■∏‖口‖|■]‖』·」】】■■‖‖』·〗||■■‖」·〗」』●‖]‖』口|

其中5tatus代表Scrapy项目启动情况’jobjd代表当前正在运行的爬取任务代号°

■■可■Ⅷ`|』■‖]

〔ur1http://192·168。2.3:680o/5〔hedu1e.j5o∩ˉdproje〔t≡〈proje〔tˉ∩ame〉ˉd5pjder≡〈5pideⅢˉ∩己贬〉

』■可‖■■

部署完成之后,项目其实就存在于Scrapyd之上了’那么怎么来运行这个Scrapy项目呢?此时可 以借助schedule」son这个接口’它负责调度已部署好的Scrapy项目°



||

●schedule.json

||

‖刮





0

l7.1 ⅧScmpyd和ScrapydAPI的使用



865

p

0





β

b



这里需要传人两个参数: pIoject即项目名称, job即爬取任务的代号,其值就是上文所说的 schedulejson接口返回的jobjd的内容。 返回结果如下: {"5tatu5圃: .ok国’"preγState赋: "ru∩∩1∩g口}

其中5tatu5代表请求执行情况, prev5tate代表之前的运行状态°

『卜「[■匣’‖■『■「广∏|尸|∩卜}■尸「}β|■■「■》|

●listprOjects.json

这个接口用来列出部署到Scrapyd服务上的所有项目的描述信息。 我们可以用下面的命令来获取Scmpyd服务器上的所有项目描述: cuI1http://192.168.2.3:680O/1i5tprojects.j5o∩

这里不需要传人任何参数° 返回结果类似如下: {』|5tatu5": αok闻’"pIoje〔t5": ["proje〔t1口’"proje〔t2α]}

其中5tatu5代表请求执行情况, project5是项目名称列表。 ●li9tversions·json

这个接口用来获取某个项目的所有版本号°版本号是按顺序排列的,其最后一个条目是最新的版 本号°

我们可以用如下命令来获取项目的版本号: 〔l』r1httP8//192。168.2.3:680O/115tγer5io∩5.jso∩?project=<pIoje〔tˉ∩a眶〉

0

| =■尸

} p



这里需要用到参数proje〔t’就是项目的名称° 返回结果如下:

{闻5tatU5闻: ‖|Ok阔’ ·γer5jO∩5阐: [凰γ1闽’ "γ2圆]}

其中5tatu5代表请求执行情况, ver51O∩5是版本号列表° ●listspiders.json

这个接口用来获取某个项目最新的_个版本的所有Spide『名称。 我们可以用如下命令来获取项目的Spider名称: 〔0r1∩ttp://192。168。2.3:680o/1i5t5pjder5.j5o∩?project=<proje〔tˉ∩己爬〉

这里需要用到参数proje〔t’就是项目的名称° 返回结果类似如下:

{赋5tatu5": "o陨阅’ .5pider5": [圃5pider1.]}

其中5tatu5代表请求执行情况, 5pjders是Spjder名称列表° ●‖istjobs.json

这个接口用来获取某个项目当前运行的所有任务详情° 我们可以用如下命令来获取所有任务详情: 〔uI1http://192。168·2.3:68O0/1istjob5.j5o∩?project=project

这里需要用到参数proje〔t,就是项目的名称°

厂↑7 巴

、| 0

866

第l7章爬虫的管理和部署

{"5tatu5": "Ok0!’

°pe∩di∩g圃: [{"1d闰: .78391ccO十ca千11e1boo9o8m272a6do6赋’·spideI": 005pider1厕}]’ Iu∩∏i∩g": [{"id』』: "422e6O8千9「28〔e+127b3d5e+93+e9399"’"5pider": 『05pider1"’"5t己rttme": "2o2Oˉo7ˉ12



1O814:03.5946640‖}]’

"于i∩i5hed闻: [{蹿jd■: "2+16646〔千〔己于11e1b0o9o80o272a6do6圃’ ■5pider": "5p1der1阐’ .starttme呵: "2o2oˉ07_12 1o:1』:o3.59』664"’"e∩dtme』』: 』』2020ˉo7ˉ121o:24:o3。590664圃}]}

其中5tatu5代表请求执行情况’pe∩di∩g代表当前正在等待的任务, ru∩门1∩8代表当前正在运行的任 务’+i∩i5hed代表已经完成的任务。 ●delversion。json

这个接口用来删除项目的某个版本。 我们可以用如下命令来删除项目版本: 〔uI1http://192。168.2.3:68oo/de1γer5jo∩.j5o∩ ˉdpIoje〔t=<pIoje〔tˉ∩aⅧe〉ˉdversjo∩≡<γersio∩∩a|∏e〉

这里需要用到参数proje〔t’就是项目的名称;还需要用到参数γer5jo∩,就是项目的版本。 返回结果如下: {"5tatl』5": "OⅨ"}

其中5tatu5代表请求执行情况’这样就代表删除成功了。 ●delprOject.json

这个接口用来删除某个项目°

我们可以用如下命令来删除某个项目: cur1∩ttp://192。168.2.3:68OO/de1proje〔t.jso∩ˉdproje〔t=<proje〔t-∩a眶〉

这里需要用到参数project’就是项目的名称° 返回结果如下: {圃5tatu5!! : ′0O促憾}

■■』‖|』■】可‖||』】口|』】■Ⅲ·‖‖‖|■】‖‖凹Ⅱ‖‖{』·】{‖|』■Ⅵ|‖‖■■|」∏|司勺●□|』■]||‖|』■‖‖‖|』■】‖‖』』』■‖‖||」■∏|‖』』·】‖‖‖』■‖||‖】□】|]‖」=■

返回结果如下:

其中5tatU5代表请求执行情况,这样就代表删除成功了。

以上就是Scrapyd所有的接口’我们可以直接请求HTTP接口来控制项目的部署`启动、运行等 操作°

5.Sc「apyd∧尸‖的使用

以上这些接口用起来可能还不是很方便,没关系’ScrapydAPI库对这些接口又做了-层封装’使 用pip3即可安装它: pjp3j∩5ta11pytho∩ˉ5〔mpydˉapj

我们可以用如下方式建立—个ScmpydAPI对象: +ro∩| scrapyd-己pjmport5cmpγd∧pI 5〔rapyd=5〔mpyd∧pI(,http://192。168.2.3:68卯!)

然后就可以调用它的方法来实现对应接口的操作了,例如部署操作可以使用如下方式: egg=ope∩(|proje〔t.e8g』’ 『rb|) s〔rapyd.3dd-γeI5io∩(|project』’ |γ1』’ egg〉

《」·】‖□】〗】·】】■‖‖归」』』】■■】■‖□〗】■∏〗】】】■■】】勺』■■∏】□■■■■■■■【尸【■■

下面我们来看下ScrapydAPI的使用方法,其核心原理和HTTP接口请求方式并无二致,只不过 用Python封装后使用更加便捷。

l7.2

ScIapydˉClient的伎用

867

这样我们就可以将项目打包为egg文件,然后把本地打包的egg项目部署到远程Scrapyd了° 另外, ScrapydAPI还实现了所有Scrapyd提供的API接口’名称都是相同的’参数也是相同的。 例如,我们调用115t-project5方法即可列出Scrapyd中所有已部署的项目: 5〔rapyd.115t-p】oje〔t5() [ 0proje〔t10 ’ ‖proje〔t2』]

另外’其他方法在此不再__列举了’名称和参数都是相同的,更加详细的操作可以参考其官方 文档: http:〃pythonˉscrapydˉapi.爬adthedocs。io/° 6.总结

本节介绍了Scrapyd及ScIapydAPI的相关用法,我们可以通过它来部署项目,并通过HTTP接口

来控制爬虫任务的运行’不过这里有一个不方便的地方,那就是部署过程°首先它需要打包egg文件’ 然后上传’这还是比较烦琐的。在下_节中,我们介绍一个更加方便的工具来完成部署过程°

↑72Sc「曰pydˉC||e∩t的使用 前面我们了解了Scrapyd的基本用法’Scrapyd提供了_系列API来帮我们实现Scrapy爬虫项目 的管理,不过其中有—个不是很方便的流程,那就是部署’即如何将Scrapy项目部署到Scrapyd上。 一般来说,部署的这个过程需要把项目打包成egg文件,可是这个打包过程其实相对还是比较烦琐的° 所以这里推荐由现成的工具来完成部署过程’它叫作ScrapydˉClient。本节将简单介绍使用 Sc【apydˉC]ient部署Scmpy项目的方法° ↑.准备工作

请先确保ScmpydˉClient已经正确安装,使用pip3安装即可: pip3i∩5t己115〔mpydˉ〔1je∩t

具体的安装方式可以参考: https:〃setupscrape.centeⅣscrapydˉclient°

2Sc「apγdˉC|‖e∏t的功能 为了方便Scrapy项目的部署, SclapydˉCljent提供两个功能° □将项目打包成egg文件。 □将打包生成的egg文件通过addversionjson接口部署到Scrapyd上°

也就是说,Scrapyd=Client帮我们把部署全部实现了’我们不需要再去关心egg文件是怎样生成的’ 也不需要再去读egg文件并请求接口上传了,这_切的操作只需要执行一个命令即可完成。

3.Sc『apydˉC|‖e∩t部署

要部署Scrapy项目,我们首先需要修改_下项目的配置文件。例如我们之前写的Scrapy爬虫项 目’在项目的第—层会有_个scrapycfg文件’它的内容如下: [5etti∩gS] de「au1t=s〔rapy〔咖po51tede咖·5etti∩g5

[dep1oy] #ur1=http8//1o〔a1ho5t;68O0/ project=5cIapyc咖po5itedeⅧ

这里我们需要配置一下dep1oy部分,例如我们要将项目部署到A主机( l92.l6823)的Scrapyd上’ 此时就需要将内容修改为: [deP1oy]



↑7 k



‖‖

第l7章爬虫的管理和部署

ur1=http8//192·168.∑.3:68oo/

■{‖

868

pIoje〔t≡5cIapyCmpo5itede‖m

这样我们再在scmpy.cfg文件所在路径下执行如下命令: 运行结果如下:

■□』‖■·』

5〔Iapydˉdep1oy

q

p己〔促j∩gγeI自io∩15O1682277

Dep1oyi∩gtoproje〔t "scmpy〔α∏po5itede∩℃" i∩‖ttp://192.168.2。3:68oo/addγeI5io∩.jso∩ 5eⅢγerrespo∩5e〈2OO):

q

{国5t3tu5喇: .o促脚’"spjdeI5′|8 1’ "∩ode∩己爬蹦: "Ⅷ1","project.: "5cmpy〔咖positedeⅧ"’!!γersio∩": "15o1682277阐} ‖

返回这样的结果就代表部署成功了。 ‖

我们也可以指定项目版本(如果不指定的话,默认为当前时间戳),此时可以通过γer5jo∩参数传 递’例如: 5〔Iapydˉdep1oyˉˉγeI5jo∩2o17O7131』55

值得注意的是,在Python3的Scrapydl.20版本中’我们不要指定版本号为带字母的字符串,要 为纯数字’否则可能会报错° 另外,如果有多台主机,我们可以配置各台主机的别名’例如可以修改配置文件为: ‖

[dep1oy:vm] ur1二http://192。168.2°3868oo/ proje〔t≡s〔mpγ〔咖positede‖℃ (

[dep1oy:v∏P] ur1=http://192°168°2°q8680o/ pIoje〔t≡5〔rapy〔咖po5jtedem



[dep1oy:Ⅷ3] uI1=httP://192.168.2.5:680O/ pⅢoje〔t=scmpy〔咖po5itede∩℃

有多台主机的话,就在此统—配置’一台主机对应—组配置,在dep1oy后面加上主机的别名即 可。这样如果我们想将项目部署到IP为l92』68.2.5的vm3主机上,只需要执行如下命令: 5〔rapydˉdep1oγⅧ3

默认情况下’Scrapyd是没有登录验证的,比如BasicAuth的功能是不具备的’如果想要开启, 可以使用Ngmx服务器实现。比如此处利用Ngmx实现了Scrapyd的登录验证,Ngjnx的监听端口修 改为了680l ’用户名和密码都是adm∩’那么scrapy.cfg可以这样配置:

』‖·

如此一来,如果有多台主机,我们只需要在scrapy.c龟文件中配置好各台主机的Scrapyd地址’ 然后调用5crapydˉdep1oy命令加主机名称即可实现部署,非常方便°

q

[dep1oγ:vm] uI1=‖ttP://192°168。2·3868O1/ project=B〔rapyco‖‖posjtede加 uSer∩a佣e=3dm∩

pa55word=ad‖j∩

这样通过加人u5er∩aⅦe和pa55切ord字段,我们就可以在部署时自动进行BasicAuth验证,然后 成功实现部署。

4总结

ˉ日』』■]‖■■

本节介绍了利用ScrapydˉCljent来方便地将项目部署到Scrapyd的过程’有了它,部署不再是麻 烦事。

q

|| l7.3 }止『【β|□■尸卜‖

‖卜

仿

Gerapy爬虫管理框架的使用

869

↑7.3Ge「apy爬虫管理框架的使用 我们可以通过ScraPydˉCljent将Scrapy项目部署到Scrapyd上,并且可以通过ScrapydAPI来控制 Scrapy的运行°那么,我们是否可以做到更优化?方法是否更方便可控? 我们重新分析一下当前可以优化的问题。

【 尸 ‖ | ■ 厂 卜 |

□使用ScmpydˉClient部署时’需要在配置文件中配置好各台主机的地址’然后利用命令行执行 部署过程°如果我们省去各台主机的地址配置,将命令行对接图形界面,只需要点击按钮即可

β『|『巴■■|■‖‖‖‖■尸「■厂■厂

实现批量部署’这样就更方便了°

□使用ScIapydAPI可以控制Scmpy任务的启动、终止等工作,但很多操作还需要代码来实现, 同时获取爬取日志还比较烦琐°如果我们有-个图形界面,只需要点击按钮即可启动和终止爬 虫任务,同时还可以实时查看爬取日志报告,这将大大节省我们的时间和精力° 所以我们的目标其实是:更方便地控制爬虫运行、更直观地查看爬虫状态、更实时地查看爬取结

果、更简单地实现项目部署、更统-地实现主机管理,而所有这些工作均可通过GeIapy来实现° Gerapy是-个基于Scmpyd、ScmpydAPI、Django、VUe』s搭建的分布式爬虫管理框架’本节中我

|仁卜■β‖‖‖∩‖

们来简单介绍它的用法。

↑.准备工作

在开始之前’请确保已经正确安装好了Gerapy’同样使用pip3安装即可: pip3i∩5ta118erapy

更详细的安装说明可以参考: https://setupscmpecenter/gempy° 2使用说明

安装完Gerapy之后,我们就可以使用gerapy命令了。首先,可以利用gerapy命令新建_个工作 目录,如下:

0

厂}》色『|‖甘■[巴■

gemPyi∩it

这样会在当前目录下生成—个gerapy文件夹’然后进人该文件夹’会发现~个空的prQjec屿文件 夹’这在后文会提及°

这时先对数据库进行初始化: gerapymgmte

b





这样即会生成一个SQLjte数据库,该数据库中会保存各个主机配置信息、部署版本等° 接下来’我们可以生成-个管理账号: gerapyj∩itadⅧj∩



这时候可以生成_个用户名和密码都为admin的管理员账号’用于后续系统的登录。

当然,如果不想使用默认的admin账号,也可以利用如下命令来创建单独的账号: gempγCreateS仙peIu5eI

【【‖【「巴尸『‖仁止「‖‖‖户匹■厂『卜仿





输人用户名和密码之后’就可以创建一个管理员账号了°

接下来,启动Gempy服务’命令如下: geI3pγru∩Serγer

这样即可在默认8000端口上开启Gerapy服务’用测览器打开http://localhost:8000即可进人

↑7 ▲

=■≡

870

第l7章爬虫的管理和部署

Gerapy的管理页面。

这时候会提示输人用户名和密码,如图l7ˉ2所示°





』 七 ∏

G巨∩∧尸γ

||

赞』徽

图l7ˉ2登录界面

输人用户名和密码,即可登录系统了°可以看到,左侧菜单栏有主机管理`项目管理、任务管理 三大模块。

在主机管理中,我们可以添加各台主机的Scrapyd运行地址和端口’并加上名称标记°比如’要 添加主机A(l92l682.3)’就可以按照图l7ˉ3这样填写° 皑 护



创■丰机

陶名称

o↑尸

Ⅵ7〗|

↑9206823



| ·皑口

6800



认证乙

…钠返回





图l7ˉ3创建主机

7



这里的端口我们填写的是6800’即Scrapyd的运行端口° 添加之后’该主机便会出现在主机列表中,Gerapy会监控各台主机的运行状况并以不同的状态标 识,如图l7ˉ4所示°









■■

l73Gerapy爬虫管理框架的使用 0

中文′曰…





G匡∩∧尸γ 峭.″:罗

|窒"

琐目嚼理

□■齿…

=—ˉ~一

■『■|||巴■【‖广‖户|止■「

诞●ˉ●●



87l

! …

任务镀理

ˉ .…。…ˉ~



」: ; !



量x舅二

0



选□



名■

0o



0鲤…分钝

W∏?





心≈

■■●●守

◆&≈玛

旗作

……… ………

……硒



≈▲



. .二迅坤.…钱

` ·



【■■■■■



,





=一_

…}

k



图l7ˉ4主机列表

■■厂『|■止β‖■卜■卜『▲尸■卧『止尸■尸|

另外’刚才我们提到’在gerapy目录下有一个空的prQjects文件夹’这就是存放Scrapy目录的文 件夹°如果我们想要部署某个Scrapy项目,只需要将该项目文件放到prQjects文件夹下即可° 这里我们可以将l63节的分布式爬虫项目放人prQjects文件夹’如图l7ˉ5所示° 丙—≡





〉Ge「aw

●●● 〈

■=

=■≡=…≡匡

=e



舔_…====■≡=≡_

翻漂″仍◇

^{修改日期

名称 ■→

》■吨s 》■logS √■p「ol·cts



→……Ⅷ→凸锌■…■_→→审咕

---≡≡←→_7

| `′宙Sc『日pγc·Ⅳ|posjteoe加。 Sc『apγcfg

oct◎herR…20at↑鳃O?

邑B?字节文穗 ˉ争文件夹

o字节p眺№h$刨『c姆 文件夹 …文件只

5↑5字节尸y恤硼S°u『ce

尸≡



●m‖“‖e晌『eS.py

| !|

■p{pe‖∩e$py

Ocmbe「窒…m锨↑牛矾

●0Settj∩gs.pγ

Ⅵ◎Oeγat0O日39

丁◎口aγat00↓38 ---一-=→==

‖长S ▲土≡_



pγth硼sou『ce ■□≡≡…=~~≡一—

373字节pγth◎∏印攫『ce

q腻B

pγth◎∩S°u『ce

…文件夹

丁odaγ臼to0:38

》■sp撼e『s





文件烫

“字节川a『炯◎…cu锨eh↑

Oct◎Der2…20日t?4;37 ˉ

h

↓a∩l』固″4『..刨ato2《0冯

灿a了动2a·oˉ0纫日t?9↓』7

》■-pγcache宇=

乙=≈盛

—…————■~

OCt◎be『9々..稻OaⅧα纲

■_m!t一·w





ˉ→文件夹 ←文件夹 ≡-文件夹

沁由γat0o$39

√■Sc「apγc°顾p◎s∏tede丽◎

≡7司

■■

≯Q

{种类

′大小 诌…=≈≈℃妇…■ ■…b~

沁dayat00叫0 陶$te闷酣献搏蹲0 γDoaγa[oo;3$ 丁o耐5γatoO.36

岂| ∩旧∧DM巳‖∏o

「■ iteⅧSpγ



凸■『|[■■厂



■■



P靶≈

■尸





二■「

图17ˉ5 prOjects文件夹

名称

sc函…0佩…em 硒

可配R

打包





打包时阉

描述



‖□《‖|‖』叼』′‖|‖‖|‖

D■罚

旧■】】】Ⅺ凸』

p

髓|}|儿 | ||』

》 }

然后重新回到Gerapy管理界面’点击“项目管理”’即可看到当前项目列表’如图l7ˉ6所示°

撇作

唾〗…疆画翻



坤蛹

图17ˉ6项目列表 ∏国临陪社臼



0

GeraPy提供了项目在线编辑功能,我们点击“编辑’’按钮即可可视化地对项目进行编辑’如图l7ˉ7



所示° [

↑7

■■■■■』

|||

第l7章爬虫的管理和部署

872

”~ 鸟」

司〗〗|| 〕

…一‖

…△

◎→

…≈呵



…雨≡ ■

】←



巳◎n

………●

■…

q

二crapy

5

SCrapyc◎mP◎■1t●deD◎· ℃em宣

Request□ SPjder



∩〗

B◎◎k工七e∏

6 6

粪钾 8T



上叮

…·w

昌O凹‖电〗 ∏a爬■

9

1】 】 12 2

ma延Page=

』3 J 14 0守

翱…”

(se1f》8

『5

1; ;寸

口aqe

』6 6

…叮

《Spider》g b◎◎k

己11◎w巳dd◎ma1nS≡ [‘己nt1sp1d巳r7,SCr己脖.Cen七巳r l b己Seur1■ 0ht℃p■g//an亡止臼P庄der7°二cr己严.ce∏ter

10 0

ur1■

『 】『

←oRg@( ′ se1f.碑X.Pug倔+ )旨 £′《Be1【.h巳曰eur1)/巴P1′bo◎x/?儿儿mLt 』8品◎【fse七={(Page 二 RGqu●巳t(ur1′ c己11b已Ck■曰e1f.『〗迅1【岂合 ;ktd巴×)

) 凸

} ‖

』8

…叼

…吨

归岛.

2O :0]

d己仁a■j■◎n.1◎□d口(ⅢeBP◎n巴e蕾℃Oxt』 reSu1亡巳=d■亡己.9·七《 rGsu1亡鲁 ′ [ 』)

20

2〕

γ■



口上





RequeS亿(ur1? ca11bac欠■Se1f°pdrsG$ dQ亿己虫1』

云 ◎

口户

2弓

、]

ˉ…

reEu1亡S自

d■re曰u1七°get( ,二d 》

uⅢ1=r {巳e1壬.ba思但~以∑】》/ap土/№◎x/{儿创》

2q





仍飞‖■e1f0

reSu1七

22 °mU

re二p◎n曰e)目

19

26 贝7

28

驼手『 《Se1f′ reBP◎"日e)吕 d己仁己宅】■◎n. ◎江固S『reSP°nse‘℃Gx↑ )

29

1七em■Bo◎次Item()



30

fi巴1d

〕l

止巳m[f1e1d] =d已t己ˉ勺配(f土e1d》

{仆■n·f1edz目

儿仁em

32

3〕

图l7ˉ7编辑项目

如果项目没有问题,可以点击“部署”按钮进行打包和部署。但是部署之前需要打包项目,打包



时可以指定版本描述’如图l7ˉ8所示° 】凹叮小出ˉ削£】■■■。

汀■■目

ˉ ; °

名称

打包名稼

名牌

1….7,呐

·Ⅷ速

!

v↑

凸’







瞥=





打□吨醒‖…0盯鲤.四

|』

‖Ⅷ■ 「一_

】P

;







图l7ˉ8项目打包 〗

打包完成之后,直接点击‘部署”按钮即可将打包好的Scrapy项目部署到对应的云主机上’如图



』‖』■可||」』·□』‖‖|‖」‖「

l7ˉ9所示°当然,我们也可以批量部署°

』尸

撇作

… (

… ‖



〗』‖‖||■■□】」‖|

图l7ˉ9部署项目







l7.4将Scrapy项目打包成Docker镜像

873

部署完毕之后’就可以回到“主机管理,,页面进行任务调度了。点击‘调度’,即可进人“任务管



理”页面’查看当前主机所有任务的运行状态°我们可以通过点击‘新任务,,‘停止”等按钮来实现 任务的启动和停止等操作’同时也可以通过展开任务条目查看日志详情,如图l7ˉl0所示。 __≈



—_≡



—_刊

名休

_≡

口}}



_●…

—一

—≡

醒…c…了庐… ■



… _革--_■∩≈_…-

|■■「坠■「



白■虫名称;…乌任分…∏■闸m↑…遭赖起7≡徽翻困…

△■虫名称{…纶任另:‘寂,….″mⅦ……毯哩?→,响

●开烛时问:…|心70`酚驹…=





0 ●■亡●■t迪西』■■t』…j■定■YJO●Ⅻ』m巫加1O1】纪J…儿◎儿mmⅡ』mJD.…〗e】叮mlm』◎…■】■昨皿Jm■】1∏沁mm色●■09时【●】w0“】m叮■■宫田…Z叼佃1■w】皿m』◎』】止巫J■团』■xm…X



6丽加■□唾m】0.…2…■≡扣陆x0eⅨ9回m互鸿…·… 20■1→07■310985G【】o[■儿…r●■.Pm叮l…8 ●吭P∏■yⅡ10·00·20q·1Jq806669

广「|〔尸||

20■】.07哼】1o9:凸6弓50 〖■Cm盯.@◎r■°■Cr■砷r) D画JG0 S@m≈【…≤200№k严』〃皿仁L■pm●m.■cr●户孕C■拥m匡/●P人/…/14$】64o/≥ {0■吐№r■,s ‖ °〖又】 u归°氏克布■尼文00 0更茂■。■■■0】〃 0c■t■】叼.T…0 ˉ巳■·〗[l0

0舌◎T●r°‘ |h七c产〗〃…』ˉ山伯mn人◎.□m/v』印/■凹b』●◎仁』L/咖buc/■刁0S09957.j闪` 0 O1d‖ 〖 ·M】36400〃

广

‖|卜

0』nc∏…Ck1mo1 □ 0O



D1…0 弓 c97■75】R△6】《39凸p 。问…0了 0刃呻有B■何0〃

严g●←n…亡·『 2■’ □pr』c●0 : !之9.8◎元0 0



■尸·【‖尸》‖尸|卜『「『广

图17ˉ10查看日志详情

另外’我们还可以在“定时任务,,面板中添加-些定时任务,支持单次执行`crontab执行等规则’

更多的介绍可以参考Gerapy的官方文档: h仗Ps:〃docsgerapy.com。

3.总结

本节中’我们介绍了Gerapy的简单用法’利用它我们可以方便地实现Scrapy项目的部署、管理 等操作°尤其是对于分布式爬虫的管理来说’Gerapy可以帮我们提高更多效率’省去更多烦琐的步骤°

↑7.4将Sc『apy项目打包成Doc|〈e「镜像

●■》|〗》

在本章前三节的内容中’我们了解了Scrapy项目的一种部署方式_Scrapyd,这是Scrapy官方 提供的-种用于部署和管理Scrapy项目的解决方案’再配合GeIapy,我们可以更加方便地管理基于 Scrapyd部署的Scrapy项目°

}≥「卜■

当然’上述方案并不是唯_的。随着容器化技术的发展’Docker+Kubemetes的解决方案变得越 来越流行,Kubemetes毫无疑问已经成了最主流的容器化编排工具’而且使用也越来越广泛。那么’

||

‖β|‖||匹■|尸「|匹■■厂匹■||

我们能否把Scrapy打包成Docker容器,并迁移到Kubemetes进行管理和维护呢?当然是可以的°

接下来’我们就来了解下Scrapy项目的另外一种部署方式—基于Docker+Kubemetes的部署 和维护方案,具体的内容包括:

□如何把Scrapy项目打包成一个Docker镜像;

□如何利用DockerCompose来方便地维护和打包镜像; □如何使用Kubemetes来部署Scmpy项目的Docker镜像; □如何监控Scrapy项目的爬取状态。

接下来’我们先来了解如何把一个Scrapy项目制作成一个Docker镜像°

厂——↑7 匹

|』 ■

874

第l7章爬虫的管理和部署

↑.准备工作

本节中’我们要把前文的Scrapy项目打包成一个Docker镜像°

首先’本节基于前文Scmpy≡Rcdjs的分布式爬虫进行改写,代码见h呻s训gjthub.comPython3WebSpjdeI/

ScrapyCompositeDemo/tree/scrapyˉredis,可以直接克隆代码,注意切换到scraPyˉ[edis分支,命令如下: g1t〔1o∩eˉb5cIapyˉredi5http5://g1t∩ub.c咖/pytho∩3"eb5pjder/5cmPy〔o∏‖Po5iteDe咖。git

运行上述命令之后’我们得到的就是ScrapyCompositeDemo项目的scrapyˉredis分支的代码。 另外,我们还需要确保已经安装好Docker并能正常使用docker命令,具体的安装方式可以参考 https://setup.scrape.centeⅣdocker。

另外’由于本项目需要用到代理池和账号池,所以还需要确保二者可以正常运行’具体的内容可 以参考163节。

2创建Doc代e而|e

首先,在项目的根目录下新建~个requirements.txt文件’将整个项目依赖的Python环境包都列 出来’如下所示: 5Cmpy

ajO‖ttp 5〔rapyˉredj5 e∩γjro∩5

如果库需要特定的版本’我们还可以指定版本号,如下所示: 5〔mpy>二2·o·o

py∏℃∩go〉=〕°7.3

在项目根目录下新建-个Dockerfile文件,文件不加任何后缀名,将其内容改为: 「旧酬pyt‖o∩:3.γ ‖0肌0IR/app 〔0pγrequjre∏论∩t5.txt ° 尺0‖pjp1∩sta11 ˉrrequ1re‖∏e∩t5·txt 〔0pγ 。 °

〔Ⅷ[0』5cmpy赋’"〔m"1"’ 0boo低"]

第_行的「R侧代表使用的Docker基础镜像’这里我们直接使用pytho∩:3.7的镜像’在此基础上 运行Scrapy项目°





第二行的Ⅷ肌DI【是运行路径,这里我们将其设置为/app’这样在Docker中,最终运行程序所 在的路径就是/app° 第三行的〔0pγ是将本地的requjIe‖e∩t5.txt复制到Docker的工作路径下’即复制到/app下°

第四行的R0‖指定了_个pjp的命令’用来读取上一步复制到Docker工作路径下的requi【emen巴Ⅸt 文件,并安装该文件里面列出的所有依赖库。

第五行的〔0pγ是将当前文件夹下所有的文件全部复制到Docker的/app路径下°这时候大家可 能有疑惑,为什么第三行不直接复制而需要再复制—次呢?这是因为这样可以单独将较为耗费构建时

间的安装依赖步骤独立为Docker镜像单独的层级°这样的话,只要requirements.txt不变’以后再次 构建Docker镜像的时候,就会直接利用已经构建的层级’不会再耗费构建时间°所以在适当的情况 下,我们可以试着将-些较为耗时的初始化操作单独放到相对靠前的层级来实现°





第六行的〔‖0是容器启动命令°在容器运行时’此命令会被执行°这里我们直接用5Cmpγ〔raW1



boo促来启动爬虫。

0

0

l7.4将Scrapy项目打包成Docker镜像

875

注意如果你对Dockerfile的编写还不够熟悉,可以额外学习一下Docker相关的基础知识和Dockerfile 的编写教程°

3.修改代码 由于我们对接的是Docker’所以需要修改几处代码,比如代理池、账号池的API地址以及Redis的

连接地址,之前是直接写死在代码里面的,现在我们构建了Docker镜像,那这些定义建议改成环境 变量的形式°

首先在middlewarepy文件中’a〔cou∩tpoo1ˉur1和proxypoo1ˉur1变量的定义需要修改如下: i‖poItO5

日〔〔ou∩tpoo1-l」r1≡o5.gete∏γ(』∧〔〔α」‖『P"[0R[`) pIoxypoo1-0r1=o5·gete∩γ〈!PR0XγP"[0RL』)

这里将固定的URL改写成通过gete∩v方法获取的环境变量,这时候需要另外导人o5这个库°对 应的两个环境变量分别为∧〔〔00‖丁p卯[0R1和pR0Xγp"[0Rl°

另外,在sett1ngspy中’R[0I50R[的定义也需要修改为通过环境变量获取的方式,具体如下: R[DI50Rl≡o5.gete∩v(0R[DI50R[,)

修改完毕之后’我们就可以构建镜像了°

4构建镜像

接下来’我们便可以构建镜像了’相关命令如下: do〔代erbuj1dˉt5〔mpyc咖posjtede‖℃.

注意这条命令最后有一个.点号,代表当前运行目录。 输出结果类似如下: 5e∩di∩gbui1d〔o∩textto0o〔促erd己e∏℃∏ 257.5低8 5tep1/6 : 「R侧pyt‖o∩:3。7 ˉˉ-〉22eb61a2Cb9q

5tep2/6 :肋肌0IR/aPP ˉˉ~〉05j∩gCaChe ˉˉˉ〉5a965b3af33a

5tep3/6 : 〔Opγrequire『∏e∩t5°t×t . ˉˉˉ〉8d949288babe

5tepq/6 : R0‖pip1∩5ta11ˉrrequjre‖记∩t5。txt ˉˉˉ〉Ru∩∩i∩gj∩d‘bbd8b879〔〔 〔o11e〔tj∩g5〔rapy

0ow∩1oadi∩gS〔mpyˉ2.4.1ˉpy2.py3ˉ∩o∩eˉa∩y."‖1(239阳〉

〔o11e〔tj∩g3jo‖ttp

0o0‖∩1oadj∩gajo‖ttpˉ3.7.3ˉ〔p37ˉ〔p37们ˉ爬∩y1j∩ux2o1』x866』.倒‖1 (1。3日B)

自d〔cess十u11yi∩5ta11ed∧uto"mˉ2O.2.OpyDjspat〔herˉ2.0.5py什a肛restˉ2.O.〗『"istedˉ20.3.0ajohttpˉ〕.7.3 a5y∩〔ˉtmeOUtˉ3.O·1attr5ˉ2O·3.O〔仟jˉ1°14.4〔hardetˉ3.O.4CO∩5t己∩t1yˉ1S.1.O〔ryPtOgmP∩γˉ3.3。1 c555e1e〔tˉ1.1.o‖ypeI1j∩‖(ˉ21。o.o1d∩aˉ3.11∩〔re爬∩ta1ˉ17.5。ojte爬d己pterˉ0.2.oite‖|1o己der5ˉ1.0.▲

j爬5p日thˉ0。10.01m1ˉ』.6.2Ⅷ1tjdi〔tˉS.1。0par5e1ˉ1.6.OprotegoˉO。1。16py0Pe∩55lˉ20.O.1Py日5∩1ˉ0.48 pγas∩1ˉⅧdl』1esˉo.2·8py〔par5eIˉ2°2oqueue1ibˉ1·5°oredj5ˉ3·5。35〔rapyˉ2·4.15〔rapyˉredi5ˉ0°6。8 5erγiceˉide∩tityˉ18.1·05jxˉ1°1S·0typi∩gˉexte∩5jo∩5ˉ3.7.4。3w31jbˉ1.22.Oy己r1ˉ1。6.3zope·1∩teI十aceˉS.2·O Re帅γi∩gi∩ter∏miate〔o∩tai∩erdqbbd8b879cc ˉˉˉ〉7b5O52S996O7

5tep5/6 :〔0pγ· 。 ˉˉˉ〉1d693eedb484

5tep6/6 : 〔Ⅻ[同s〔rapy"’"〔ra罚1.’ °book"] _〉Ru∩∩i∩gi"19d954〔9137b

【e‖℃γj∩gj∩temmj3te〔o∩tai∩er19d954c9137b ˉ-〉346de4b66刀6

剑〔〔e55+u11ybuj1ta』6de4b66276

5u〔〔e55+u11γtaggeds〔r己py〔咖po5itede∏n1ate5t

第l7章爬虫的管理和部署

876

这就证明镜像构建成功了’这时执行如下命令,可以查看构建的镜像: do〔促ermage5

返回结果中有_行就是: 5〔r己py〔mpo5jtede∏℃1己test叫6de4b662762m∩ute5ago968"B

这就是我们新构建的镜像。 5.运行

运行的时候’我们需要先指定环境变量°可以新建一个.e∩γ文件,其内容如下: ∧〔〔山‖W"L0R儿=http;//host·doc促eI·j∩ter∩a1:6777/a∏tj5pideI7/m∩do∏‖ pR0XγP"[0R[=∩ttp://∩o5t.doc代er.j∩ter∩己1:5555/r己∩do『∏

行在6379端口°

我们可以先在本地测试运行,此时可以执行如下命令: do〔ke】ru∩ˉˉe∩γˉ千j1e .e∩v5crapy〔o∏]po5itede加

这样我们就成功运行了刚才构建的Docker镜像,运行结果类似如下: 2o21ˉ02ˉo418:3o:49 [5〔rapy.ut11s.1o8] I‖「0: 5〔mpy2.4.15tarted (bot: 5〔Iapy〔咖po51tede∏‖o) 2o21ˉo2ˉo418:〕0:49[5〔mpy.l」tj15.1og] I‖「0: γer51o∩5: 1x‖1』.6.2.0’1ibx川122.9.1o’ 〔555e1ect1.1。o」par5e1 1·6.0’"〕1jb1.22.o’「"i5ted2o.3·o’ pytbo∩3.7.9(de+au1t’〕a∩122o21’17:26:22) ˉ [C〔〔8.3.o]’ py0pe∩55[ 2O.0.1(Ope∩55l1.1.1i 80e〔2O2O)’ 〔ryptograPhy3.3.1’p13t千om] 1j∩0Xˉ4·19·76ˉ11∩u×代itˉ×8664ˉWithˉdebia∩ˉ10·7

2O21ˉO2ˉ0418:〕O:』9 [5〔rapy.utj15.1og]0[806: U5i∩grea〔toI: twi5ted°i∩temet·35y∩〔1oIea〔tor°∧sy∩〔joSe1ectorRea〔tor

2021ˉO2ˉ0q18:30:49 [5〔r己py.uti15。1o8] D[8l」C: 05j∩ga5y∩〔1oeve∩t1oop: a三γ∩cio°u∩1xeγe∩t5. 0∩jx5e1e〔tor[ve∩ttoop 2o21ˉ02ˉ0』18:3o:49 [5〔rapy·cra"1er] I‖「0: 0γerridde∩5etti∩g5: ●





2021ˉo2ˉO418:30:49 [s〔rapy.mdd1e"己re] I‖「0: [∩ab1ed5pjdermdd1eNaIe5: [』5〔rapy.5pideImdd1eware5。httpeIror。什ttp[rror‖idd1ew己re ’ 5〔r日pγ.spjdem1dd1eware5.o仟5ite.O仟5ite问1dd1ewaIe ’ 05〔rapy.5pjdermdd1ewaIe5·re+erer°Re十erer"idd1eware0 ’ ]scIapy°5pidemndd1eware5.ur11e∩gth.l」I1[e∩gt‖‖jdd1eⅦare ’ 5〔rapy.5p1der∏1dd1eware5.depthDepth"jdd1eware′] 20∑1ˉ02ˉo418§〕o:49 [5〔rapy.mdd1e"aIe] I‖「0: [∩ab1editeⅧpjpe1j∩e5: [] 2021ˉ02ˉoq18:3o:q9 [5〔rapy.〔ore°e∩8j∩e] I‖「0: 5piderope∩ed 2o21_o2ˉo418:30:49 [boo仪] D[B06; Re5um∩g〔Iaw1(40reque5t55〔hedu1ed) 2021-02ˉO』18:30:49[5cr己py·exte∩5io∩5.1og5tat5] I‖「0:〔ra"1edOp己ge5 (3tOpage5/‖j∩)’5cr己pedOjteⅧ5(at oite们5/m∩) 2O21_02ˉ0q18:3O:49 [s〔rapy.exte∩5io∩5.te1∩et] I‖「0: 丁e1∩et〔o∩so1e1i5te∩j∩go∏127.O.O。1:6O23 2021ˉO2_0q18:〕O:49 [mdd1e"aIe5.3ut∩orjzatjo∩] D田(L: 5eta0thorizatio∩jwt ey〕0eⅫi0j〕Rγ1Qj[〔〕hbCci0j〕I0zI1‖i〕9.ey]】〔2γyX21促Ijo2O5"jdX‖1〔Ⅷ5∩b‖0jO1〕hZC1pbj02Ij"jZ风∩wIjox‖j[y‖丁Ax0丁I1 [〔〕1b‖「pb〔I6IjI5I吧ya"d+a‖「oIjox‖j[y‖凹4‖zI1们。11x8阳f3肌528p3q67XI6755rO∩55Ⅶ4Rz‖z8γv5bOk 2o21ˉo2ˉ0418;30:49 [mdd1eware5.pro×y] 0[80Gsetproxy183.88.169.162:8o8o ●



』■■‖』■■■』‖‖‖□■■■‖□■■■■■』■■〗||」』■‖|||」■■司|||』■可■■‖〗】■司

同时,这里需要确保账号池运行在本机的6777端口’代理池运行在5555端口,RediS数据库运



在Docker内部便可以访问宿主机的相关资源。

q



这里定义了三个环境变量,分别是账号池、代理池、Redis数据库的连接地址’其中每个变量的host 地址都是‖o5t.doc促er.1∩ter∩a1,这代表Docker所在宿主机的IP地址’通过‖o5t.do〔Ⅸer.1∩ter∩a1,



『■■■■■■

R[DI50R[=redjs://host。do〔长er.j∩ter∩a1目6379





| (



2021ˉO2ˉO418:3O:S1 [s〔rapy·〔ore.e∩gj∩e]D[B0C:〔m"1ed (2OO)〈C[『

可以看到’在Docker中可以正常从代理池和账号池获取随机代理和随机token’同时也能r常爬

取到结果,这说明代理池、账号池、Redjs数据库均能正常连接°

■■■可‖|司‖』■」·‖|‖·

https://己∩ti5p1der7.5〔mpe.〔e∩ter/api/boo促/2237621/〉(re+erer: http5://a∩ti5pider7·5〔Iape.ce∩ter/apj/boo贸/?1jⅧ1t≡188o仟set=71)



‖日

■=「巴『|卜‖β|止■厂β『||止Ⅸ‖□【_■■□■「『■■尸卜「|■「‖■『|||匡’||【ˉ■■■■■=■=■■■■|』』□=—=■△=■厂△■「

[ ]7.4将Scrapy项目打包成Docker镜像

877

6.推送至Docke『‖ub

构建完成之后,我们可以将镜像推送到Docker镜像托管平台,如DockerHub或者私有的Docker RegjstIy等,这样我们就可以从远程服务器下拉镜像并运行了。

以DockerHub为例’如果项目包含一些私有的连接信息(如数据库),我们最好将Reposjto【y设

为私有或者直接放到私有的DockerReg1stry°

首先在https://huhdocke∏com上注册_个账号,然后新建—个Reposito【y,名为scmpycomposj℃dcmo。 比如’我的用户名为germey,新建的Reposjto1y名为scrapycomposjtedemo,那么此Repository的地址 就可以用germey/scrapycomposjtedemo来表示,这里需要修改为你的用户名° 另外’我们还需要使用如下命令来登录DockerHub: do〔代er1ogj∩

输人DockerHub的用户名和密码之后,就可以完成登录了。接下来,我们就可以往DockerHub推 送自己构建的Docker镜像了。

为新建的镜像打_个标签’命令如下所示: do〔代erta85〔r己pyco|∏po5jtede『∏o:1ate5tgemey/5〔Iapyco‖po5itede『∏o吕1日te5t

推送镜像到DockerHub即可’命令如下所示: dockeIpu5hgeI眠y/5cIapy〔o们po5itede∏℃



此时DockerHub便会出现新推送的Docker镜像了,如图l7ˉl1所示°

卜 R嗜pm0『o门鹤

O『鹏Ⅶ选《『o∩$

『牢

Bu↑时田

〔创№m□蛔T

睁『枷

l」$曲Ⅵ0Of}顾…●『…趋.G■@℃『■



G■助●『O‖

Q凸〔肘e‖p宁



巴订p|@「昭

萨『m叮′幻……“酶硒

■口』凹巴

腿p昭t硕归田

Q

屯~



| 吵d。c庶e『ˉ

we恤O…

Seo佃悸



@ge『们ey/5c『己pγc◎mpo5|tedeⅧ◎



O°〔找e『Co丽!wa∏d爵

《h汇及磁rph】丛hgem℃y/弘々【&py℃◎『∩p0』Ppl电喳α吐玩Qft啤5na冗心

oka£『pus榨□‖a『eW$e〔o∩山凸8◎

b

|pub{jw…|

丁opu3h●∩翻吨m巾幅阳网…y.

γ池厂…凹yⅡ…∩α标o“…/



-—

『 oγUL"[R^B|止‖w5〔∧川偶|侧GˉⅨSABl【□

丁■g已a∩d5c己n5

P

E心孕●

T]由『…蛔T〔m】『田们3‖率■)·

Re(e"tbu‖d5 (々吠◎…fe……′mo…m唾α胸…陆

p



} 仿





了晒

OS

p‖】上怎p

●|a帕筑



■…D“蛔△呕◎

矾″【o

●烯沁『面刊

5ee占‖



{ ‖…二 —

卜 b

图17ˉll

在DockerHub上出现新推送的Docker镜像

这个镜像可以供后面使用’如果我们想在其他主机上运行这个镜像,在主机上装好Docker,运行 代理池、账号池`Redis数据库后’按照同样的方式新建.e∩γ文件’可以直接执行如下命令: do〔代errl』∩ˉˉe∩γˉ十i1e °e∩γger∏记y/5crapy〔o‖posjtede∏℃

这样就会自动下载刚才我们所推送的镜像’然后读取环境变量并运行了’运行效果和刚才-模 一样。



878

第l7章爬虫的管理和部署

7.总结

我们讲解了将Scrapy项目制作成Docker镜像并推送到DockerHub的过程’接下来我们会介绍 DockerCompose和Kubemetes的用法,来尝试部署和运行ScraPy项目。

↑7.5 Doc|〈e『Compose的使用 在上_节中’我们了解了将Scrapy项目打包为Docker镜像的方式’不过其中有_些不太方便的



地方。

□构建镜像的命令比较烦琐而且难以记忆’比如需要额外添加—些配置选项° □如果需要同时启动多个Docker容器协同运行,仅使用doc促erIu∩命令是难以实现的°



下面我们再来介绍_个工具DockerCompose’使用它’我们可以方便地实现镜像的打包、容器的 协同管理°





|.DocⅨe『Co∏pose

DockerCompose是用于定义和运行多容器Docker应用程序的工具°通过它’我们可以使用YAML 格式的文件来配置程序需要的所有服务,比如YAML文件里面定义了构建的目标镜像名称、容器启动 的端口、环境变量设置等,把这些固定的内容配置到YAML文件之后’我们只需要使用简单的 doc代erˉco‖po5e命令就可以实现镜像的构建和容器的启动了,非常方便。 接下来’我们尝试利用DockerCompose来构建上_节介绍的镜像吧° 2.准备工作

首先’我们需要准备好上_节学习的所有内容’包括ScmpyCompositeDemo的代码以及对应的 Dockerfile’能正常构建Docker镜像。另外,我们还需要准备好账号池`代理池并能成功运行。 除了如上内容外,我们还需要安装好DockerCompose’安装方法可以参考: h忱ps://setupscmpe.

center/dockePcompose°安装好之后’我们就可以使用doc代eIˉ〔o"po5e命令了° 3.创建γ∧ML文件

首先’我们需要确保代理池在本级的5555端口上正常运行,账号池在本级的6777端口上正常运 行’具体的内容可以参考15.l3节。

接下来’我们在ScrapyCompositeDemo根目录下创建一个dockerˉcompose.yaml文件’内容如下: γer51O∩: "3" 5erγi〔e5:

redi5:

mage: redi5:己1pi∩e co∩t日i∩er∩a爬8 redjs

pOrt5: ˉ "6〕79‖!

5〔rapy〔oⅦpo5itedeⅧ: buj1d: "."

ma8e: 闻ger爬y/5〔mpγ〔o"positedem曰 e∩γjⅢo∩爬∩t8

∧〔〔创‖「p凹l0R[: 圃‖ttp$//ho5t.do〔Rer.j∩teI∩a1:6777/己∩ti5pider7/ra∩d咖" pR0XγP"[0Rl: ■http://ho5t°do〔促er.j∩ter∩a1855S5/m∩d咖w R[0I5‖05「: ■redj5://redi5;637900

depe∩d5-o∩8 ˉredj5



首先’第_行我们指定了γersjo∩,其值为3’即DockerCompose的版本信息。目前,绝大多数 情况下都将其指定为3°



』』

‖‖

尸仪‖卜‖|‖『卜|卜卜『卜

l7.5

DockerCompose的使用

879

}「

然后我们指定servj〔e5的配置’一个是redi5’一个是5crapycoⅧpo51tede∏]o,即启动的两个服务 一个是Redis数据库,一个是我们的Scrapy爬虫项目ScmpyCompositeDemo°

对于Redjs来说’我们直接使用了已有的镜像来构建,所以直接指定了i‖age字段,内容为

redis:alpine ’这是一个公开镜像,直接下载并运行即可启动_个Redis服务°接下来,我们指定了 〔o∩tai∩er∩a"e,即redis:alplne这个镜像启动之后的容器名称,我们就直接赋值为redi5即可。当然, 也可以换其他的名称’然后就是运行端口’这里我们直接指定了运行端口是6379。



对于Scrapy爬虫项目来说’由于代码在本地,所以构建位置就直接指定为.’代表当前目录°接 着,我们指定了构建的目标镜像名称,这里就直接指定为我的DockerHub为该项目配置的镜像地址

ger们ey/5〔rapy〔o‖po5jtede|∏o’其中gemey是我的DockerHub用户名,这里请自行替换成你的用户名。 接下来’我们利用e∩Viro∩|∏e∩t来指定环境变量,还是上_节所述的内容°另外’我们还指定了

depe∩d5ˉo∩配置’内容为redjS’即该容器的启动需要依赖于刚才声明的redi5服务,这样只有等redjs 巴∏

|卜}|‖巴『‖【‖【■尸『‖‖■■|〗‖|■■『|‖『■■■■■■巴■■■■■



对应的容器正常启动之后,该容器才会启动。

注意如果你想了解更多DockcrCompose的配五选项,可以参考DockerCompose的官方丈档: https://docs.docker。com/compose/。 4.构建镜像

接下来,我们就可以利用do〔|(erˉco∏]po5e命令来构建Docker镜像了。在dockePcompose.yaml目 录下运行该命令即可: doc低eIˉco们po5ebui1d

运行结果类似如下: redi50se5a∩j阳ge’5代jppj∩g 8uj1dj∩gs〔Iapycα印o5itede‖℃

[+] Bui1di∩g2.75 (11/11〉「I‖I5‖[0

=〉[i∩tema1] 1oadbuj1dde+i∩itjo∩「rm0oc低er千j1e =〉≡〉tra∩5千erIi∩gdo〔促er十i1e: 37B =〉[j∩ter∩a1] 1oad .do〔促eIig∩ore ≡〉=〉tm∩s+eIIj∩gco门text8 28

≡〉[j∩tema1] 1oad爬tadata十ordoc《er.jo/1ibrary/pyt∩o∩:3.7 ≡〉[auth] 1jbrarγ/pyt∩o∩:p‖11to‖(e∏+orregj5tryˉ1·dod《eI.jo ≡〉[1/5]「R叫

do〔代er。io/1ibrary/pyt∩o∩83。7·5ha256:oba96o71「e7ob9〔「6dd2247千7901e35961〔b已o‖459933〔4301己d6a593o十9c2b9 ≡〉[i∩tema1] 1oadbui1d〔o∩text =〉=〉tm∩5十erri∩8〔o∩text: 37。O8低8 =〉〔虹‖[0[2/5]刚郎0I【/aPP =〉〔∧〔‖[D[3/5]〔0pγrequire∏冶∩t5.txt .

=〉叭〔‖[D[q/5] R0‖pipj∩5ta11ˉ0pjp&

pjpi∩5ta11ˉIrequire爬∩t5。txt

≡〉[5/5]〔0pγ . .

=〉exPortj∩8to1阳8e



=〉=〉exporti∩81ayer5

=〉=〉writ1∩81阳ge5ha256:b2「3+35e9e51十2b38eab〔7o〔3a6a5十oe仟1e84o9a86eoS25968Sb2ebb736515b ●

=〉=〉∩a∏i∩gtodoc|〈er°io/gem论y/5〔rapyc咖po5itede咖

泣里就输出了构建镜像的整个过程;和上一节构建的过程非常相似’只不过我们不用再关心怎样 指定镜像名称,不用指定构建路径了。 5.运行镜像

运行镜像也是十分简单的’我们也无须再指定环境变量、容器名称、容器运行端口等内容,只需 要一条命令就可以一下子启动Iedi5和5〔rapy〔oⅦpo5jtede∏旧这两个服务: doc代eI-c咖po5eup

—↑7 k



第l7章爬虫的管理和部署

880

运行结果如下:

redi5

』□■■■■尸【■■■■卜■■■■■尸■■■■

5tartj∏gredi5…do∩e Re〔reati∩g5cmpy〔o∩甲o5itede帅5cⅢapycoⅦpo5jtede‖℃1…do∩e ∧tta〔hj∩gtoredi5’ 5crapycα∏po5jtedems〔r日py〔m巾ositede陋1

|1:〔31〕u12o2118:29:10.828#o…哑Redj5j55taIti∩go哑哑哑

redi5 |1:〔31〕u1202118;29:1O.828#日edi5γer5jo∏=6。2。3’ bits=64’ 〔αmjt="O…’ |∏odi+ied=O’ pid≡1’ jl』5tstarted

redjs |18〔31]q1202118:29:1o.828#‖aI∩i∩88 ∏oco∩「ig千i1e5pe〔jf1ed』 l』sj∩gthe de+己u1tco∩十18. I∩ordeIto5pe〔i句a〔o∩千jg千i1e05eIedi5ˉserveI/pat∩/to/redi5。〔o∩十 Ied15 |18"31〕u1202118:29;10·829*m∩oto∩ic〔1oc低: p05IXc1oc促-gettj『∏e

redi5 redj5

|1:"31〕u12o2118:29:1o.829*Ru∩∩i∩g咖de霉st己∩da1o∩e’ port.6379. | 1:∩31〕u12o2118:29:10。830#5erverj∩jtia1ized

γ

redis | 1:‖31〕u1202118:29:1O。83o*Readytoac〔ept〔o∩∩ectio∩s 5〔rapy〔刚po5jtedem1 |2o21ˉo7ˉ3118:29:13 [5crapy.uti1s.1o8] I‖「O: 5〔rapy2.S.05t3rted(bot:

5cmpy〔mposjtede咖)

5〔rapyco∏]po5jtedem1 |2O21ˉo7ˉ〕118:29:13 [5〔r己py.uti1s.1og] I‖「O: γer5io∩s: 1x|‖|146.3.0’ 1ibx‖12 2。9。1o’〔555e1e〔t1.1.O, par5e11。6.0′w31ib1.22.o』 丁切i5ted21。7.o’ Pyt内o∩3·7.11 (de+au1t’〕u1222021’

d

15:50:o9) ˉ [α〔8.3°0], pγOpe∩55[2o.0.1(印e∩55l1.1.止25阳r2021)’crypto8rap∩y3.4.7’ P1at+o【"



li∩uxˉ5.1O。25ˉ1i∩uxⅨjtˉx8664ˉwjthˉdebi己∩ˉ10。10

s〔rapyc咖po5jtede『m1 |2O21ˉO7ˉ3118:2981〕[s〔rapy·utj15.1o8]0[8[L; 05j∩gIe己〔tor: t"j5ted.j∩ter∩et.

a5y∩cjorea〔toI°A5y∩cjo5e1ectorRea〔toⅢ

5crapy〔o刚po5itede∩℃1 |2O卫1ˉ07ˉ3118:29:13 [5〔rapy。uti15.1og]D[B卯: 05i∩ga5y∩〔ioeγe∩t1oop:



a5y∩〔jo°‖∩ixeγe∩t5. 0∩ix5e1ector[γe∩tloop

scrapyc咖po5itedem1 |2o21ˉ07ˉ3118:29:13 [5〔Iapy.craw1eI] I‖「O: 0\′errjdde∩5etti∩g5: ●







’{‖||



这里我们就可以看到,首先启动了redj5服务,然后创建了5crapy〔o『『lposjtede∏℃这个服务’接着 运行了Scrapy爬虫项目并开始爬取对应的数据,在爬取过程中同时在控制台输出了对应的日志内容° 6.推送镜像

||

如果我们在本地测试镜像没有任何问题了,那接下来就可以把镜像推送到DockerHub或其他的 DockerRegistry服务上了’这里依然使用doc促erˉ〔o『∏po5e所提供的命令即可:



doc代erˉ〔o∏‖po5ep05h

是的,也是非常简洁明了的命令,运行结果如下: pU5∩i∩8SCr己Py〔咖PO5jtede加(gem论y/5〔r3Py〔α∏PO5itede网;13te5t)… 丁hepu5门re+er5torepo5jtory[do〔低er.io/ger∏记y/5cmpy〔α‖po5itede∩℃] 83616Od』〔5〔O: pu5bed

十MO8c千c4c』5: pu5hed 053b8bbO〔2〔1: p‖5hed

5仟6千38a1637: pu5hed

62499da5千ab9:№u∩ted十r咖1ibrary/pytho∩ ●





a千a3e488己0ee:№u∩ted「r咖1ibrary/pyt∩o∩ 1ate5t: dige5t: 5∩a256:8d13〔b16〔b77ca十a5oa9c96653b1593千6997c〔6a5c9bb95abd1d8458』54〕93d25ize: 3oS2

勺』可{‖‖』■司《■卧

执行完毕之后,我们指定的镜像就可以被推送到DockerHub供其他主机下载运行了。 7.总结

|』■】』■‖□■■■□〗』■]|‖|』■■】』··勺‖■]‖

如此_来’我们就成功使用DockerCompose来构建、运行、推送我们构建的Docker镜像°正因 为DockerCompose所提供的配置化功能,我们可以将Docker启动和构建所需的参数都写到dockerˉ compose.yaml文件里面,我们只需要使用dockeIˉco"po5e对应的命令就可以轻松完成想要的操作°

好,现在我们已经构建好镜像了。在下_节中’我们就来了解在Kubemetes里面如何运行镜像吧°

↑7.6 |〈ube「∩etes的使用

引|·日

前面我们已经学习了Docker镜像的搭建过程,另外还学习了DockerCompose工具的用法°我们 可以使用DockerCompose非常方便地启动Docker容器运行爬虫,然而这个过程距离真正的大规模运

■ ■

■ l7.6Kubemetes的伎用







!「

‖‖





卜 |

88l

维还是不够。 还是前几节的几个问题:

□如何快速部署几十、上百、上千个爬虫程序并协同爬取? □如何实现爬虫的批量更新?

□如何实时查看爬虫的运行状态和日志?

其实利用Kubemctes’我们同样可以非常方便地解决上文提到的各个问题°

本节中,我们会首先了解Kubemetes的基本概念和原理、核心的组件和Kubemetes的基本使用方 式’以便为后文实现Kubemetes部署和管理Scrapy爬虫程序打下基础。 ↑.准备工作



在本节开始之前’请确保已经对Docker等容器技术有了一定的了解’如果你没有相关的经验’ p 仁



请先学习_下Docker和容器技术的相关知识。

另外’还需要在本机安装Docker并在本地启用Kubemetes服务’具体的操作可以参考 https://setup.scrape°center/kubemetes·

■◆匣『■■■■‖■‖

配置好Kubemetes之后’我们就可以使用|(ube〔t1命令来操作一个Kubemetes集群了。 2|〈ube『∏etes简介

Kubemctes,简称K8s(K和s中间含有8个字母),它是用于编排容器化应用程序的云原生系统。 Kubemetes诞生自Google’现在已经由CNCF (云原生计算基金会)维护更新°Kubemetes是目前最 受欢迎的集群管理方案之_’可以非常容易地实现容器的管理和编排°



刚刚我们提到,Kubemetes是一个容器编排系统°对于‘编排”二字,我们可能不太容易理解其 中的含义。为了对它有更好的理解,我们先回过头来看看容器的定位以及容器解决了什么问题,不能 解决什么问题’然后了解下Kubemctes能够弥补容器哪些缺失的内容°

)‖|‖

好’首先来看容器°最常见的容器技术就是Docker了,它提供了比传统虚拟化技术更轻量级的 机制来创建隔离的应用程序的运行环境。比如对于某个应用程序’我们使用容器运行时,不必担心它 与宿主机之间产生资源冲突’不必担心多个容器之间产生资源冲突°同时借助于容器技术’我们还能 更好地保证开发环境和生产环境的运行一致性°另外,由于每个容器都是独立的’因此可以将多个容 器运行在同一台宿主机上’以提高宿主机的资源利用率,从而进一步降低成本°总之,使用容器带来 的好处有很多,可以为我们带来极大的便利°

不过单单依靠容器技术并不能解决所有问题’也可以说容器技术也引人了新的问题,比如说: □如果容器突然运行异常了,怎么办?

□如果容器所在的宿主机突然运行异常了,怎么办? □如果有多个容器,它们之间怎么有效地传输数据? □如果单个容器达到了瓶颈,如何平稳且有效地进行扩容?

□如果生产环境是由多台主机组成的,我们怎样更好地决定使用哪台主机来运行哪个容器? 以上列举了一些单纯依靠容器技术或者单纯依靠Dockcr不能解决的问题,而Kubcmetes作为容 器编排平台’提供了一个可弹性运行的分布式系统框架,各个容器可以运行在Kubemetes平台上’容 器的管理、调度`部署、扩容等各个操作都可以经由Kubemetes来有效实现°比如说’Kubemetes可 以管理单个容器的生命周期,并且可以根据需要来扩展和释放资源°如果某个容器意外关闭’ Kubcmctcs可以根据对应的策略选择重启该容器,以保证服务正常运行°再比如说’Kubemetes是-



↑7 k

882

第l7章爬虫的管理和部署

个分布式平台’当容器所在的主机突然发生异常,Kubemetes可以将异常主机上运行的容器转移到其 他正常的主机上运行°另外,Kubemetes还可以根据容器运行所需要占用的资源自动选择合适的主机 来运行。总之,Kubemetcs对容器的调度和管理提供了非常强大的支持’可以帮我们解决上述的诸多 问题°

3.低ubemetes关键概念

●Node

Node’即节点,在Kubemetes中,节点就意味着容器运行的宿主机°因为Kubemetes是一个集群, 所以我们可以把节点看作组成集群的-台台主机。

●NamesPace

Namespace’即命名空间,对—组资源和对象的抽象集合°可以认为Namespace是Kubemetes集 群中的虚拟化集群°在—个Kubemetes集群中,可以拥有多个命名空间,它们在逻辑上彼此隔离° ●Pod

Pod’它运行在Node上,是Kubemetes的最小调度单位’也是Kubemetes针对容器编排作出的设 计方案。

面的容器可以共享资源、网络、存储系统°

●Deployment



]‖

通常情况下,我们不会单独显式地创建Pod对象,而是会借助于Deployment等对象来创建。



‖|‖」□|{

这时候大家可能有个疑问’为什么最小的调度单位不是容器’而是又另外设计了-个Pod的概念 呢?因为容器单独运行,这确实是没有问题的’但有时候几个容器是需要协同运行的,它们需要共享 同样的资源、同样的网络,比如说这里运行了一个MySQL容器,但这个容器在启动时需要进行-些 初始化的配置,比较好的设计就是单独有_个Sidecar容器为这个MySQL主容器进行初始化操作,所 以这个MySQL容器就需要配有_个Sidecar容器’它们还需要共享相同的网络和资源°所以在容器的 基础上’Kubemetes进—步抽象了一层,叫作Pod°Pod里面是可以运行多个容器的’同一个Pod里

d

《 ‖ ‖

既然是集群’那么多个Node相互协作_定是_个需要解决的问题’到底应该听谁的呢?所以 Node又分了MasterNode和WOrkerNode,其中MasterNode可以认为是集群的管理节点,负责管理 整个集群,并提供集群的数据访问人口’在它之上运行着—些核心组件,如APISe『ver负责接收API指 令,Con∏ol‖erManager负责维护集群的状态,比如故障检测、自动扩展、滚动更新等°

|||

下面我们来介绍Kubemetes中的关键概念,包括Node、Namespace、Pod、Deployment、Servlce、 IngIcss等,了解了这些,有助于我们更加得心应手地实现Kubemetes的管理和操作°

Deployment’即部署’利用它我们可以定义Pod的配置’如副本、镜像`运行所需要的资源等°

因此’我们只需要在Deployment中描述想要的目标状态是什么。Kubemetes有_个Dep‖oyment Con∏oller’它会帮我们将Pod和ReplicaSet的实际状态改变到我们想要的目标状态。 ●Service

设想这么-个场景,假如—个服务’我们在部署的时候声明了副本数量为2,即创建两个Pod, 每个Pod都有自己在Kubemetes中的IP地址并在对应的端口上启动了服务’但这-组Pod的服务怎 么统一暴露给Kubemetes之外来访问呢?这就引人了SeIvice的概念°

·]勺」』■|』■■]」|‖』■■■■∏〗ˉ口·|□|」‖刨|刀|』】Ⅲ

Deployment在Pod和ReplicaSet之上,提供了—个声明式定义方法’比如说我们声明一个 Deployment并指定Pod副本数量为2,应用该Deployment之后,Kubemetes便会为我们创建两个Pod°

| l7.6Kubemetes的伎用

883

『 ‖ β |



Se「vice是将运行在_组Pod上的应用程序公开为网络服务的抽象机制°Service相当于一个负载 均衡器,通过一些定义可以找到关联的_组Pod’当请求到来时’它可以将流量转发到对应的任_Pod 上进行处理°所以对外来说’客户端不需要关心怎么调用具体哪个Pod的服务, Servlce相当于在Pod 之上的_个负载均衡器’对应的请求会由Servjce转发给Pod° ●IngreSS



■「||「〖巴尸|■厂△■「|

Ingress用于对外暴露服务,该资源对象定义了不同主机名(域名)及URL和对应后端Service的 绑定,根据不同的路径路由HTTP和HTTPS流量°比如通过lngreSs,我们可以配置哪个域名对应的 流量转发到哪个Service上,还可以配置一些HTTPS证书相关的内容。

以上我们就简单介绍了Kubemetes里面的部分基本组件’这些全新的概念其实还是比较难理解

的,接下来我们就通过一个实战例子来加深理解°另外,也强烈推荐查看Kubemetes的官方文档了解



更加详细的内容: https;//kubemetesjo/docs/concepts/°



4.低ube『∏etes案例上手



下面我们来用—个实际案例了解Kubemetes的部署过程°这里我们会介绍如何创建Namespace’

}〖

如何使用YAML来定义Deployment和Service’以及怎样访问Service,通过这些操作’我们可以先对 Kubemetes的操作有个简单的认识°

‖)尸

‖卜

接下来的演示是基于Docke『自带的Kubemetcs集群实现的,安装好Docker之后,我们在Docker 的设置面板中只需要勾选Enab‖eKubemetes即可在本地开启一个Kubemetes服务,如图17ˉl2所示° 厅■

卜‖



@碑●

心Q·cke厂

×

p「e「e『e∏Ce$

蜀Ge∩e「己{



|〈ube『"ete5 γ‖.‖65

酒ReSOu『〔e占

■尸{■尸巴■尸■■尸■『「||广也‖『|但■∏‖匹尸■●『仆》『

●Oo〔Re「启∏g↓∩e 儿

C◎∏〗m己∩dL}∩e



o』o」ber"ete5

回[∩司b{eⅨube「∩ete5 臼mJT占Num问■e$■咆名『…P〔hmGfW『滩∩5四洲『吨Do丁比7o吩№t鳃

回Dep|oyO◎〔ke「Sta冰5to‖〈l』be∏e惶5byde「au|t 陋№N…m…间0ede『du"αr呻h饥O‘脑闺OO砍ers睡k.rm`m知由《〔峙门8田 .=′0唾迫『′佰刨`缸』■o行』

□5howsys【e!wc◎∩taj∩er5(adγa∩〔ed)



引℃w低…碴ⅧSmⅡ叼涸cα,‖D↑呻$础]e∩必由耶D吭h总『乙◎…】酶.

|……"…鹰h雨] 酗qD二f仁6酗魁Ⅻ…∏…呻刨』〔程已“比Oe赋“

■0O〔们e『』MwⅦ

「E忘ˉ{ …」

●Ⅸube「∩e[es川y肋叫

~-_≡′

b障

■尸‖

图l7ˉl2Docker的设置面板

配置完成之后’可以发现左下角的Kubemetes是绿色的running状态’这就说明配置成功了。 ■叮卜●『●■厉β}■



kubectl是用来操作Kubemetes的命令行工具,可以参考https://kubemetes.jo/zh/docs/tasks/tools/ installˉkubectl/来安装°安装完成之后’请将KubemetesContext切换为本地Kubemetes°

比如’我这边DockcrDcsktop创建的Kubemetes的Context名称叫作dockerˉdesktop°如果你也使





□■』■可

q

第l7章爬虫的管理和部署

884

■■■■■■

用同样的方法创建集群,名字默认也是一样的,此时可以运行如下命令使用该Context: ‖

灿bect1co∩十igu5eˉco∩te×tdo〔kerˉdesktop



运行之后’会有如下提示:



5Nitchedtoco∩te×t 口doc长erˉde5灶op"。

b

如果你使用的是其他Kubemetes集群,可以自行更改Context名称.

■‖



促ube〔t1get∩odes

类似的输出如下: ‖酗[

doc仪erˉde5促top

5『∧丁05

RO[[5

A6[

Readγ

Ⅶa5ter

1d

γ[只5I删

γ1.16·6ˉbeta.O

这里列出来了节点的相关信息: ‖酬[代表名称; 5丁∧丁05代表当前节点的状态’如果其值是Ready

接着,我们再来查看下Namespace,命令如下: 灿be〔t1get∩a爬5p日Ce5

输出类似如下: ‖州[

5丁∧『05

∧C[

defau1t

∧〔tjγe

1d

hubeˉ∩odeˉ1ea5e

∧ctjγe

1d

灿beˉpub11c 代ubeˉ5y5te∩

∧〔tjγe ∧〔tjve

1d 1d

‖■■■】】〗□·■‖』·}‖■■|■■■〗□■■∏□‖‖』』■■‖

的话,代表节点状态正常; R0[[5代表角色,这里因为只有-个节点,所以它的R0[[5就是川a5ter; ∧C[代表节点自创建以来到现在的时间; γ[R5I0‖代表当前Kubemetes的版本号°

』』■∏·纠■司二■■■■■』■■■■■■

这时候我们可以使用kubect1命令来查看当前本地Kubemetes集群的运行状态。首先看下节点的

信息,命令如下:

这里列出了当前所有的Namespace,它们都是Kubemetes预置的Namespace°我们可以自行创建 一个Namespace,将资源部署到新的Namespace下,比如创建—个叫作5er`′1〔e的Namespace’命令 如下: kubeCt1Create∩己爬5pa〔eSerγjCe

运行结果如下:

如果看到如上提示,就说明Kubemetes已经创建好了。

』‖』■』■司|

这时候我们来创建_个示例Docker镜像°首先’新建-个文件夹并将其当作工作目录,在该工

‖‖‖

∩3爬5pace/5eIγ1〔e〔reated

作目录下创建一个app文件夹’其内创建一个mainpy文件, 目录结构如下: q

长app ~Ⅶ日j∩°Py

mainpy文件的内容如下: 十I咖「a5tapij‖甲ort「a5t∧pI

app≡「a5t∧pI()



@己pp.get(‖/0) de于i∩dex(): retur∩ 0‖e11o"or1d0

这是FaStAPI编写的一个服务,通过代码可以看出,这里定义了一个路由,访问根路径就可以返 回‖e11o‖or1d° ‖■





l7·6Kubemetes的使用

}}

0

0

885

接着’我们在app同级目录下创建—个Dockerfile文件, 目录树结构如下:



仁睬.施№ L--main.py Dockerfile的内容如下:

‖‖|

「R侧pytho∩:3.7 RⅧPjp1∩5t己11千a5taPjuv1〔or∩



〔0pγ 。/app/己pp



〔阳[.uγjcor∩口, .app。Ⅷaj∩:app°’ .ˉˉ‖o5t回’ .O.O。0.O阑’ .ˉˉport"’ "80"]

[XpO5[8O

》 }

接下来’我们可以在Dockerfile所在文件夹下运行命令构建一个Docker镜像,如:



doC低erb0i1d-tte5t5eIγer .

「}

这样—个镜像就构建好了°我们来运行_下试试看: do〔keIru∩ˉp8888880te5t5erγe【

)‖

这里我们运行了当前的镜像’启动了_个容器’容器本身是在80端口上运行的。由于我们设置 了端口映射,将宿主机的8888端口转发到容器的80端口’因此我们在测览器中打开

卜卜

http://‖ocalhost:8888/,就可以看到如图17ˉl3所示的结果。 厂

『o

●岛b※●

‖oca‖们oSt:8888

》■比‖巴凸■

÷今G■‖。ca‖h。St:888S

×}+ 匡圃髓☆0冉●

卜△》■△『■

嚼H●11◎W◎r1dw

图17ˉ13运行结果

这说明本镜像是没有任何问题的° 》卜

接下来,我把镜像推送到DockerHub°先修改镜像名称’然后推送即可:

卜‖「

do〔代ertagte5t5erγer8er赡y/te5t5eIver do〔代erp‖5向geI爬y/te5t5erγer

这里请自行修改DockerHub的用户名,将ger"ey替换为你自己的用户名。 如果出现类似下面的结果’就说明推送成功了: W]epu5∩re十er5torepo5itory[doc仅er.io/8em论y/te5t5erver] 0〔8Obe9761b3:[ayera1readyexi5t5 a4bo+6己9292〔:[ayera1readyexj5t5 ●





0

◆||}‖

0

a1千2千42922b1:[ayera1readγeXiSt5 47625S2己d7d88 儿ayera1reMyexj5t5 1ate5t: djge5t8 5ba256:b92c66da「4627ebo69dc3343e9a9c3d24d6b122db47e8』7〔4debS2dfa5b2f2eo5ize8 16〕6

当然,你也可以选择不推送自己的镜像,直接使用我的镜像ger们ey/te5t5erver也是没问题的°

好,接下来让我们创建一个YAML文件,叫作deployment.yaml,其内容如下: aPjγeI5jo∩吕 app5/γ1 灶∩d: Dep1oy∏记∩t 眶tadata:

0

∩a爬: te5t5erγer

∩a爬5pa〔e; 5erγ1Ce

∩户

1abe15:

app8 teStSerγer 5peC:



伊‖『

o





第l7章爬虫的管理和部署

886

rep1i〔aS8 3 5e1e〔tor: 帕tCh[abe1S:

app: te5t5erγer t咖p1ate: 爬tadata8

1abe15:

app: test5erγer SpeC: 〔o∩tai∩er5: ˉ ∩己爬: teSt5eIγeI

mage自 gemey/te5t5erγeI pOIt5: ˉco∩taj"eIport: 8O

这里我们定义了-个0ep1oγ"e∩t对象,一些配置项如下°

■∩a们e5pa〔e:命名空间,这里就使用刚才我们所创建的5eI`′iCe这个命名空间° ■1abe15:声明了一些标签是一些键值对的形式,可以任意取值,它旨在用于指定对用户有

||

■∩a"e: 0ep1oy雁∩t的名称’我们可以任取’这里也取名为te5t5erver。

·□|日

□促j∩d:其值就是Dep1oⅦe∩t,代表我们声明的是0ep1oy"e∩t对象° □Ⅶetadata:定义了0ep1oy『∏e∩t的基本信息。



意义且相关的对象的标识属性°

■rep11ca5:这里指定为3’这就声明了需要创建三个Pod’即创建三个Pod副本。 ■5e1e〔tor:声明了该Dep1oyⅧe∩t如何查找要管理的Pod,这里通过Ⅶat〔∩[abe15指定了_ 个键值对’这样符合该键值对的Pod就归属该0ep1oy们e∩t管理。另外’还有一些更复杂的 匹配’如使用"atC∩[xpre55jO∩5匹配某个表达式规则。 ■te"p1ate:声明了Pod里面运行的容器的信息’其中们etadata里面声明了Pod的1abe15’

‖」{

□5pec:声明该0ep1oy们e∩t对象对应的Pod的基本信息。





这和上述5e1e〔tor的们atch[abe1s匹配即可°〔o∩taj∩er5字段指定运行容器的配置,其中 包括容器名称、使用的镜像、容器运行端口等°

通过如上配置’我们就完成了Dep‖oyment的声明°现在我们来执行一下部署’此时可以运行如 下命令: 灿be〔t1app1yˉ「dep1oγ∏记∩t。y己‖1

这里app1y命令就代表kubectl应用该项配置, ˉ十代表文件选项’后面要跟_个文件路径’即 deployment.yam‖。 运行结果类似如下:



dep1oy眶∩t。app5/te5t5erγer〔reated

如果出现这样的提示’就说明该部署已经生效了°

接着我们可以用如下命令来看下Pod的运行状态: 灿be〔t1getpodˉ∩5erγj〔e

这里注意我们需要使用ˉ∩指定Namespace,运行结果类似如下: ‖酬[

R[汕γ

5『∧丁05

test5erγeIˉ685978千9句ˉ1j4γ6 te5t5erγerˉ685978句「9ˉq6v5低 te5t5erγerˉ685978千9+9ˉt5pzz

1/1 1/1 1/1

Ru∩∩i∩g R‖∩∩i∩g R0∩∩j∩g

【[5丁∧盯5

0 O O

∧C[

1‖ 1" 1‖



·』

可以看到’这里创建了3个Pod(就是刚才rep11〔a5参数所指定的3)’而且都是Ru∩∩1∩g状态。





l7.6Kubemetes的使用

887





好’现在Pod已经创建好了’接下来我们需要将服务通过5erγjCe暴露出来。

0

接下来’我们声明_个5eIv1〔e对象,再创建一个serviceyaml文件,内容如下: apiγer5iO∩: V1 长j∩d: 5erγjCe 爬tad己t己: ∩日『爬: te5t5eIγer

∩己们e5pa〔e; 5eIγ1Ce 5pe〔:

tγpe: ‖odeport 5e1e〔tOr:

} ■=■■厂△■『△◆『【尸■■●【■△‖伊‖‖■■「■■尸『△Ⅲ尸「‖尸■∩‖》■卜‖|止尸||【■■

己pp日 test5erγer port5: ˉprotoco1: 『〔p port: 8888 targetpoIt: 8O

这里我们定义了一个5erVjCe对象’部分配置项如下°

□代j∩d:其值就是5ervjce,代表我们声明的是5erγi〔e对象° □Ⅷetadata:定义了5erγiCe的基本信息。

■∩a川e: 5erγi〔e的名称’我们可以任取,这里也取名为te5t5erVer°它和其他对象重名,这 是不冲突的,只要在_个命名空间下没有其他相同名称的5ervjCe即可。

■∩aⅦeSpa〔e:表示命名空间,这里就使用刚才我们所创建的Serγ1〔e这个命名空间。 □5pe〔:声明该5erγj〔e对象对应的Pod的基本信息。 ■5e1ector:声明该5erγ1ce如何查找要关联的Pod,这里通过5e1ector指定了-个键值对’

这样所有带有app为te5t5erγer标签的Pod都会被关联到这个5erγice上。

■port5:声明该5erγice和Pod的通信协议’这里指定为『〔p°同时’port指明了5erγi〔e的 运行端口,这里声明为8888,但是targetport指的是Pod内容器的运行端口°由于在 Deployment中容器是运行在80端口的,所以targetport指定为80° 现在我们再部署这个5erγi〔e对象’其命令如下: 代ube〔t1app1yˉ千5erγ1〔e。ya"1

运行结果如下: 5erγi〔e/te5t5erγerCreated

这就表明5erV1Ce已经创建成功了° ‖』■

|‖八■■「『}‖■甲似[∏

接下来’我们其实是仍然不能访问这个5erγ1〔e的。要访问的话’可以通过端口转发的方式将服 务端口映射到宿主机,或者修改Serγi〔e相关的配置,把5erγi〔e的类型修改为‖odeport或者将 5erγ1〔e进一步通过Ingress暴露出来。这里我们直接采取端口转发的方式将Kubemetes中的Servjce转 发到本机的某个端口上,命令如下:

‖尸「》|}卜「』仍|■厂β

长ube〔t1portˉ十orNaId5erγj〔e/te5t5erγer9999:8888 ˉ∩5erγice

这里我们将宿主机的9999端口转发到Service的8888端口’这样我们在本地访问9999端口就相 当于访问Kubemetes的Service的8888端口了°

|‖卜|||卜【■『|』『|}广||

运行结果类似如下: 「om己mi∩g十I咖127·0。o·1:9999ˉ>8o 「oIw己rd1∩g千r咖[::1]:9999ˉ〉8O

傍》|卜

这里输出的其实是9999映射到80’因为这里显示的是容器的端口’容器的运行端口是80。



↑7 匹

尸『止尸『■■■■■■ =

■|‖`|』□



第l7章爬虫的管理和部署

纠 ■ ■ 】

888

●0‖●

÷÷○

×

}+

●‖◎ca‖To3t:9999





力●

|■●■●‖|

∩ ·(C「富雨雨孟§§雨

■■】Ⅵ■』■‖‖

这时候在测览器中打开http://localhost:9999/,即可看到刚才部署的服务,如图17ˉl4所示。

■■可|‖]』■■‖||

"He11◎№工1d■

尸‖

图l7ˉl4运行结果

到此’我们通过声明Deployment和Service实现了_个服务的部署,同时体会了Kubemetes的部 署流程。

可{‖‖

5.总结

在后面,我们会介绍利用Kubemetes进行Scrapy分布式爬虫部署的方案。

账号池、Scrapy爬虫项目部署到Kubemetes集群上来运行。 ↑.Na∏espace

在开始之前’我们首先新建_个kubemetes文件夹并将命令行切换到该文件夹。在该文件夹中’

我们会创建多个Kubemetes的YAML部署文件用于资源部署°

另外’我们需要新建一个Namespace’叫作crawler,我们将所有的资源都部署到这个Namespace 下°创建Namespace的命令如下:

‖‖|‖‖‖

在上-节中,我们已经学习了Kubeme贮s的基本操作,本节中我们进行_个实战练习,将代理池`

‖ ‖

↑7.7用|〈ube「∩etes部署和管理Sc「apy爬虫

Rube〔t1〔reate∩己爬5P己ce〔raw1er

创建成功后’我们就开始部署吧° 2。只ed‖s

首先,我们可以先进行Redls数据库的部署’因为代理池、账号池、Scrapy爬虫项目都是以Redjs 此处我们部署-个最基础的单实例Redis数据库。首先在kubemetes文件夹下创建_个redis文件

夹’再在redjs文件夹中创建_个deploymentyam‖文件,其内容如下:

13be15:

aPp: redj5 ∩a『∏e: redj5

∩a爬5p己〔e:〔ra"1er 5pe〔目

rep11〔3S: 1 "at〔b[abe15;

■ |

5e1e〔tor;

■■·■』□■■』■■■■■■

∏etadata:

=■习

apjγer5iO∩: app5/γ1 低j∩d; 0ep1oy∏论∩t

‖勺|

为基础的°

己Pp8 Ied15 te"P1ate; 『∏etadata:

1abe15:

■』

aPp; red15 5pe〔:



0



l7.7用Kubemetes部署和管理Scrapy爬虫

0

889



〔O∩ta1∩eI5:

ˉ j∏Ege: redj58日1pi∩e ∩a爬: redi5 Ie5Our〔e5:

0

1imtS目

『∏e∏℃ry: "2Ci" Cpu: 圃50O「∏" reque5tS:

‖论∏℃ry: "5O硼1" cpu8 "2卯∏‖" pOrt5: ˉ〔o∩taj∩erPort: 6379 伍 ■ 『 ●

这里我们声明了_个Dep1oγ附e∏t’并在co∩ta1问ers字段里面使用redis:alpine这个镜像进行部署’ rep1i〔a5实例个数设置为1’端口设置为6379。

矽「‖■■「■■巴■『{『心》份‖∩匣■卜′|■■◆『巴■尸|巴■■●卜■■尸

接下来’在redjs文件夹下创建一个service.yam‖文件,其内容如下: apWer5jo∩: γ1 kj∩d: 5erγi〔e

∩etadata: 1abe15目

aPp8 redj5 ∩a"e; redj5

∩a爬5pa〔e:〔mN1er 5Pe〔:

Port5: ˉ∩a‖论: 口6379"

port: 6〕79 taIgetport: 6379 5e1eCtOr:

app8 redj5

这里的5erγ1ce同样声明使用6379端口’targetport需要和0ep1oⅦe∩t的co∩taj∩erport对应起 来’也是6379。

|‖仿『

接下来’我们切换到redis文件夹的上级文件夹’即kubemetes文件夹,执行如下命令进行Redis的 部署: 灿be〔t1app1yˉ十redj5



} p



注意这里ˉ十后面跟的是-个文件夹名称,这样会应用该文件夹下所有的YAML文件’相当于执行了 如下两条命令: Rube〔t1app1yˉ千Ⅲedj5/dep1oy爬∩t.yaⅦ1 低ubect1app1yˉ+Iedi5/5erγjce.y己Ⅶ1

可以看到如下的运行结果:

|∏卜|卜‖

dep1oy∏论∩t.己pp5/red15〔reated 5erγi〔e/redj5〔reated

稍微等待片刻,Redjs数据库就部署成功了°

|[■「《‖|】|■卜|口仪伊△■|‖『||■‖||[■『‖卜尸【广|||■厂|△■|‖‖△》【|[广「}|卜『【■「||}■■≡■■■

我们可以使用如下命令查看Redis的部署状态: kubect1getdep1oγ‖e∩t/red15ˉ∩craN1er

运行结果类似如下: ‖州[ red15

R[∧Dγ

0pˉ丁OˉD∧「[

∧γAIl∧8{〔

M[

1/1

1

1

155

这个结果表明刚才声明的0ep1oy刚e∩t在Kubemetes的部署情况,我们可以看到R[∧Dγ这_列的结果是 1/1’这说明期望部署l个Redis实例。现在已经部署了1个实例’所以已经部署成功了。 如果你看到的结果不是这样的’可以耐心等待一会,可能现在Kubemetes还在下载Redis相关镜

厂}

↑7

q



‖‖

像’你可以使用长ubect1命令查看相关Pod运行状态°但如果长时间都无法部署成功’请检查Kubemetes 日志°

另外,为了方便学习和操作’部署的仅仅是最基础的Redjs数据库实例’并没有配置Redjs集群’ 也没有配置持久化存储°在实际生产环境中推荐使用Helm部署Redis集群,具体的操作可以参考 https://githuhcom/bjmamⅡcha1ts/tree/master/bimami/redis· 3.代理池

Redis数据库部署好了,接下来就开始部署代理池了°

』■·‖■■■■可■·‖□』·■■」■■■‖=■司|‖■■‖|■司□·

第l7章爬虫的管理和部署

890

在kubemetes文件夹下新建proxypool文件夹,再在proxypoo‖文件夹下创建deployment.yaml文 件’其内容如下:

■■

apiγer5io∩: aPP5/γ1 长j∩d: Dep1oy∏论∩t 爬tadata;

app: prOXypOO1 ∩a『∏e: proxypoo1 ∩a爬Spa〔e:〔raw1er

■■‖||□】·‖■··

1己be15:

5pe〔:



rep11〔aS: 1 5e1e〔tOr: 们at〔h[abe15:

爬tadata:

』‖

app: pIoxypoo1 te"p13te: 1abe1S:

app: pIoxypoo1

=e∩γ目

ˉ ∩a爬: R[DI5朋5「

‖‖

5pe〔: 〔o∩tai∩er5:

γa1l』e: 0redj5°craN1er。5v〔°〔1u5ter°1oca10 ˉ ∩己‖e: R[0I5p0盯

γa1ue; |6379,

1‖age: gemey/proxypoo1 ∩a‖∏e8 prOXypoo1 re5our〔e5;

1imt5:



‖爬Ⅷry日 "5oo‖1" 〔pu: "3"『∏厕

‖论∏℃Iy: "50叫j00 〔pu: "3卯『∏" pOrt5: ˉ〔o∩tai∩erport目 5555

■‖|引‖

和Redis的0ep1oy们e∩t声明类似,这里我们按照同样的格式声明了代理池的0ep1oy们e∩t,这里镜 像使用的是ger"ey/proxypoo1’即我的DockerHub上的代理池镜像,当然这里你也可以自行替换成你

■■}|二■■‖』

req0e5t5:

的镜像.另外值得注意的是’这里通过e∩`′声明了两个环境变量,指定了RediS的链接地址’其中 R〔0I5‖05「的值是redj5.〔mw1er.5γc.c1u5ter.1o〔a1,这个值是有一定规律的’是Kubemetes根据我

|·勺‖‖‖■■|■‖

们部署的5erγ1Ce和‖aⅧe5PaCe名称自动生成的,其格式是<5er`′j〔eˉ∩aⅧe〉.<∩aⅧe5pa〔eˉ∩aⅦe〉.5γ〔. <c1u5terˉdoⅧaj∩〉。_般情况下〔105teI_do"31∩的值为c1u5ter.1oca1,而此时‖a"e5pa〔e的名称为 craw1er’Redis的Servj〔e的名称是red15,所以最后的结果就是redj5.cr己w1er.5γc.c1u5ter.1o〔a1° 在Kubemetes其他容器里’可以通过这样的Host访问其他容器°尺[DI5p0盯这里就是RcdisService的

运行端口,即6379。另外,这里还指明了容器运行端口〔o∩ta1∩erport为5555。



接下来,再创建_个对应的5erv1〔e’在proxypool文件夹下新建servjc巳yam‖,其内容如下:







l7.7

t

用Kubemetes部署和管理Scrapy爬虫

89l

ap1γer51o∩; γ1 代j∩d: 5erγi〔e

刚etad日t己: 1abe15:

||

己pp: pro×ypoo1 ∩a|∏e8 prOXγpOO1 ∩a「∏e5pa〔e:〔mW1er 5pe〔;

pOrt5; ˉ ∩a‖论: "5555"

■『 ‖ ▲ ■ ‖ ■ 尸 巴 ■ 「 } 巴■「

■■

■■}■『『| 卜□尸卜|广|■【甘「▲■■「■■尸匹■β■伊 }『‖|}|||||炉「|

卜}



Port: 5555

tar8etport; 5555 5e1e〔tOr;

己pp; proxypoo1

还是同样的格式,这里声明了5erγj〔e的运行端口还是5555’targetpoIt和〔o∩tai∩rport对应起 来’也是5555°

接下来’执行如下命令进行部署: 仅ube〔t1app1yˉ千pro×ypoo1

运行成功的结果类似如下: dep1oy∏e∩t.app5/proxypoo1〔reated 5erγ1〔e/proxypoo1created

和Redis类似,这里使用如下命令即可查看代理池的部署状态: 代ube〔t1getdep1oγ∏论∩t/pⅢoxypoo1 ˉ∩〔raw1er

如果出现类似如下结果’就说明部署成功了: ‖州[

proxypoo1

R[∧Dγ

0pˉ丁Oˉ0∧「[

Aγ∧I[∧8[[

M[

1/1

1

1

635

此时我们可以通过长ube〔t1的portˉ+oIwaId命令将Kubemetes里面的服务转发到本地测试’执行 如下命令:

灿be〔t1portˉ千orwardsγ〔/proxypoo188888555S ˉ∩〔raN1eI

portˉ十orward命令可以创建本地和Kubemetes服务的端口映射’这里指定了转发的服务为 svc/pIoxypool’ 5v〔就是5erγj〔e的意思,端口映射配置为8888:5555°因为我们部署的代理池5erγj〔e 运行端口是5555,这里我们将其转发到本机的8888端口上° 运行之后,会有类似如下的输出结果:

|}



「orw己rdj∩g十r咖127.0·0°1:8888ˉ〉5S55 「orward1∩8千r咖[::1]:8888ˉ〉5555 {

此时我们在本机测览器上打开http://localhost:8888/ |卜「炉 ||凸「‖■|‖【】广『|』尸||

|止尸『|■■|‖【尸「||卜



random’就可以直接访问到代理池的API服务了’如 图l7ˉl5所示° 圈l7ˉl3肌不。

@|°CD‖mSt:8“引「■∏do丽 ÷÷G

●|。ca|h。…s割『s"d。m

因±_| ■■

↑‖2·7a↑6553BO8O

这就表明代理池正在运行并能正常提供API服务。 验证完毕之后,停止如上命令即可’这不会对Kubemetes

里面的代理池服务产生任何影响。

图17ˉl5运行结果

4账号池

账号池和代理池的部署非常相似。在kubemetes文件夹下创建accoumpool文件夹,在accountpool 文件夹下创建deployment.yaml文件’其内容如下:

{ ||||



第l7章爬虫的管理和部署

892

aPiγer51o∩: app5/γ1 低1∩d; 0ep1oy∏记∩t 爬t3d己ta: 13be15:

app: 己〔Cou∩tpOO1 ∩a爬; a〔COl』∩tpOo1 ∩a|∏e5Pa〔e目 Craw1er Spe〔:

reP11〔a5: 1 Se1e〔tOr: 川日tCh[abe15:

app: aC〔Ou∩tpOO1 te∩]P1日te: |∏et3d己t己:

1abe15:

app日 a〔Cou∩tpOo1 Bpe〔: 〔o∩taj∩er5: →e∩V:

ˉ ∩a『∏e: R[0I5"5『

γa1ue: 0Iedis。〔raw1er。5γ〔·c1u5ter°1oca10 ˉ ∩a『爬: R[DI5pO盯

γa1ue: 063790 ˉ ∩a眠: ∧pIp0盯

γ己1ue5 06777‖ ˉ ∩a爬: ‖[85I丁[

γa1l』e; a∩ti5Pjder7 1帕8e8 gem记y/a〔〔ou∩tpoo1 ∩a‖]e: aC〔ou∩tpoo1 Ie5our〔eB:

11mts;

‖记∩℃ry: 因50酬i" Cpu: "3m∏" reque5t5: 爬咖Iy; ■50叫i" 〔pu: "30α‖" port5: ˉco∩taj∩erport: 6777



这里的原理和代理池_样’它指定了R[0I5‖05『和R[0I5p0R丁,同时额外指定了∧pIp0盯和

‖[85I丁[这两个环境变量,并i [丁[这两个环境变量,并设置了〔o∩tai∩erport为6777。 再创建servic巳vaml文件,其内容如下: 再创建serviceyaml文件’ ap1γer5iO∩; γ1 ki∩d: 5eIv1〔e

Ⅷetad日ta: 1abe15:

app: aC〔oU∩tPOO1 ∩a‖∏e: a〔〔ou∩tpOO1 ∩a爬5Pace: cr3N1er 5pe〔:

PoIt5: ˉ ∩a∩沦: 冈6777冈

port; 6777 t己rgetPort: 6777 5e1e〔toI:

q

app: a〔COu∩tpOO1

执行如下命令进行部署: 找ubect1己pp1yˉ十a〔co0∩tpoo1

【■■■尸■■■■■■■■尸■■■■■■■尸匹

这里指定5erγj〔e的运行端口为6777。

运行结果类似如下: dep1oⅦe∩t°apps/a〔〔ou∩tpoo1〔reated 5erγ1ce/ac〔ou∩tpoo1〔reated



‖‖·】■■■■■



l7.7

0

用Kubemetes部署和管理Scrapy爬虫

893

■卜

这就说明部署命令执行成功了’稍等片刻’账号池也会部署成功°

同样’我们也可以使用同样的命令来将账号池服务转发到本地进行验证:

■【β「|β

≡■】】】【■■■■■仔□‖■■}|‖}〗|)

代ube〔t1portˉ+oIw己Id5γ〔/accou∩tpoo1777786777ˉ∩cmw1er

运行结果类似如下: 「orw己Idj∩8+r咖127°o.o·1:7777 ˉ〉6777 「orNardj∩g十r咖[::1]:7777 ˉ〉6777

此时在本机测览器打开http:/川ocalhost:7777/antispider7/mndom’如果能正常获取到结果,就说明 账号池恤丽常运行了。 5.爬虫项目



▲■厅‖■「‖|■砂「|止β‖∩‖‖卜|||止■■尸□‖‖[‖}■口■‖‖‖「‖『■■■尸』‖}|巴■■β■‖「||■■■尸「||▲■=尸‖‖「||||■■

因为爬虫项目依赖账号池和代理池’所以这里我们最后才进行部署。在kubemetes文件夹下创建

scIapycomposjtedemo文件夹’并在此文件夹下创建deployment.yaml文件’其内容如下: apiγeI5iO∩8 app5/γ1 低i∩d:kp1oy眶∩t Ⅷetadata8 1abe15:

app: 5crapy〔o『∏po5jtedem ∩己『∏e: 5〔mpy〔咖po5itedeⅧ ∩己爬5paCe: Cra"1eI 5pe〔目

rep1j〔a58 1 5e1eCtor:

ⅧatChk己be15:

app8 5〔mpyco『∏po5jted臼Ⅷ te‖p1ate: 爬tadat己: 1abe15:

3pp日 5〔rapy〔α印o5itedem 5pe〔: 〔o∩tai∩er5: ˉe∩γ:

ˉ ∩a‖爬: ∧〔〔α」‖『p"l0R[

γa1ue目 !‖ttp日//a〔cou∩tpoo1°cmw1e【。5γ〔。〔1u5ter°1o〔a1;6777/a∩tisPideI7/m∩d咖| ˉ∩a『胎8 pROXγ咖[0R[

γa1ue; 0们ttp://pro×γpoo1·cm"1er·5γ〔.〔1u5ter。1oca1:5555/m∩d咖| ˉ ∩a『爬8 【[DI50R[

va10e; !IedI5://redj5·〔ra"1er.5v〔.c1u5ter.1oca1:6379`

mage: ger∏隐y/5〔mpy〔α即o5itedem

∩a‖∏e: 5〔【apyc咖po5jtede∏℃ re5our〔e5: 伊■

1jmt5:

∏记∏℃ry8 "5刚i" 〔pu: "3四∏回 Ieque5t5:

∏把∏℃ry: 0050侧j" 〔pu: 国3帅∏"



这里我们配置了geI爬y/5〔Ⅲapycα巾o5itedem作为镜像’同时配置了M〔α川Wm[0R[`pR0Xγp凹t0R[ 以及R[DI50R[,这里的Host都设定为了redj5.〔mw1er.sγc.c1u5ter.1oca1,端口都是各个服务的运 行端口。

因为Scrapy爬虫项目并不提供HTTP服务’所以我们只需要部署0ep1oy‖e∩t即可。运行如下命 令执行部署:

■■■■■■■■■』■■■■■■■■■■■■■■■】■口』■■∏·■】■

代ubect1app1yˉ十sc【apyco旧po5itede‖℃

我们可以执行如下命令查看部署状态:

-

↑7 止

{』‖

第l7章爬虫的管理和部署

894

R0bect1getdep1oy∏记∩t/5cmpyc咖po5jtede∏℃ˉ∩〔raw1eI

如果出现类似如下结果’就说明部署成功,并正常运行了: ‖∧N[

R[∧Dγ

0pˉ『0ˉD∧丁[

∧γ∧I[∧81[

∧C[

5cmpyCo∏PoSjtede∩℃

1/1

1

1

2叫65

另外’我们还可以查看Pod的状态: kubect1getpodˉ∩cmw1er



运行结果类似如下: ‖州[

ac〔ou∩tpoo1-57d498655十ˉ6wW4 pro×ypoo1ˉ8646+8bcb7ˉ64z98 redisˉ5689〔9b5〔bˉzd十t2 5〔mpyc咖positedeⅧˉcb仟d87ddˉx8pj8

R[∧Dγ

5丁∧丁05

R[5「∧R『5

∧C[

1/1 1/1 1/1 1/1

Ru∩∩i∩g Ru∩∩i∩g Ru∏∏j∩g Ru∩∩j∩g

0 0 O 0

17Ⅷ 22Ⅶ 4▲‖] 3Ⅷ54s

可以看到’前面部署的所有实例都正常运行了° 怎么知道爬虫有没有爬取到数据呢?我们可以运行命令查看日志:





0

灿be〔t11og55cmpy〔o呻o5jtede『∏oˉ〔b仟d87ddˉx8pj8ˉˉtaj1≡20ˉ∩〔raw1er

这里我们通过1og5命令输出_个Pod的日志’这里Pod的名称需要根据上面命令的输出结果得 到°另外’这里指定了ˉˉtai1=2O代表输出最后20条日志,运行结果类似如下: {|〔o∩te∩t0 : !我当时的感觉??和电影一模一样° 电影不lw′」、说写的°|’ ′jd『 : 02297019oo6′}’ {‖〔O∩te∩t0 :突然想起这本书,还是初中生的时俊,汀电影之前埃的(当时比起电彤,还是埃书史吸引

{‖〔o∩te∩t0 : ‖具的不袭回道在说啥, 屯影也不知道在农达啥???? 0 ’ 』id! : ‖2277859717′}]」 〔over! : 0httP5://j吧3.douba∩jo.〔o「‖/γiew/巴ubje〔t/1/pub1j〔/51463O73.jpg0 ’ 0jd0 : 014』99810 ’ 0i∩trOduCtiO∩0 : 0 0 ’ ‖j5b∩‖ : 09787O2OO54398′ ’ ∩a爬0 : 0尤极!’



我),读的时侠觉得故亨还可以,好像述立了一个史庞大的世界观(虚构的世界秩序)呆豺°如果让作者自己米拍,可能 又足另一部《月迹》·| ’ ‖1d′ : !228142574O′}′

q



p38e-∩u肋er|: 153’ |pri〔e| 8 ‖24·OO元0 ’ ‖pub115hedat0 : 02m6ˉ01ˉ2o『16:";"Z0 ’ ‖pub1j5her! : 0人氏丈学出版社|’ 5core‖ : !6.】0 ’

』tag5‖: [‖郭敬明! ’ )允极|’| ′」、说! ’ !计幻』’ !电影′」、说|’ ) ′」`四! ’ ′什尽丈学』’ ‖付容,]’ ,tra∩S1atOrS』: []} 2021ˉo3ˉ1316:36:23 [mdd1ewaIe5.proxy]D[B[几: 5etproxy3』.90.54。218:8o (

可以看到’Scrapy爬虫项目正常运行并爬取到了数据° 6Oas∩boa『d

我们可能发现,每次都要通过命令查看日志和运行状态非常烦琐,有没有更直观的查看Kubemetes

资源的工具?当然有,我们可以直接使用Kubemetes推荐的Dashboard°

官方文档链接为: https://kubemetesjo/docs/tasks/accessˉappljcatjonˉclusteⅣwebˉujˉdashboard/。我们 可以试着部署一下: 代l』be〔t1app1yˉ千∩ttp5://raw。gjthubu占er〔o∩te∩t。c咖/代uber∩ete5/da5hboard/γ2。2,0/aio/dep1oy/re〔o『Ⅶ肥∩ded。yaⅧ1

注意随着Dashboard版本的更新,此命今可能并不一定是最新的,请参考官方丈档的说明进行 部署°

执行完上述命令之后’可能会看到如下结果:







l7.7

用Kubemetes部署和管理ScIapy爬虫

895

∩a∏论5pace/代uber∩ete5.da5hboard〔reated 5eIγ1〔ea〔〔ou∩t/h』ber∩ete5ˉda5hboard〔reated

5erγ1〔e/代uber∩ete5ˉda5们bo己rd〔reated 5ecret/促ubeI∩ete5ˉd己5hboardˉ〔ert5cIeated 5e〔ret/促uber∩ete5ˉd己5hboardˉc5r十created

5e〔ret/促uber∩ete5≡dashboamˉkeyˉ∩o1der〔Ieated

〔o∩fjg阳p/代uber∩ete5ˉd3shboardˉ5etti∩85〔reated ro1e°rbac.aut∩oriz日tio∩。Ⅸ8s。io/促uber∩etesˉda5∩board〔re日ted

〔1u5terro1e。rba〔°authorjzat1o∩。代85°io/促uber∩ete5ˉd35∩boardCreated

ro1ebi∩dj∩g.rb3〔°authorjz己tjo∩.k8s°io/|(uber∩ete5ˉdas∩boardCreated 〔1u5terro1eb1∩d1∩g.rb日〔°al』t∩oIjzatio∩.k85°jo/代uber∩ete5ˉda5打board〔reated dep1oγ『‖e∩t.app5/|〈‖」ber∩ete5ˉdashboaId〔reated ▲■尸日■巴■∏□■尸》■■

seIγ1〔e/d己shbo己Idˉ‖∏etIic5ˉ5crapeI〔re己ted

dep1oy『∏e∩t.app5/da5hboardˉ爬trj〔5ˉ5cmper〔reated

然后执行如下命令: kube〔t1pIoxy

通过本地访问http://localhost:800l/api/vl/namespaces/kubemetesˉdashboard/se【vices/https:kubemetesˉ

dashboard:/proxy/’即可看到一个Dashboard登录界面,如图l7ˉl6所示° |

●`●



==—一●=~■吕构…

十◆O —←——~~_=■—_

☆‖△白●【 二二罢二…●;■

P

‖伊



||

卜仲

窒董罪

|!



_

丝≡g

b

图l7ˉl6Dashboard登录界面

此时可以参考官方文档的说明获取登录的token’参考命令如下: |《‖be〔t1ˉ∩kubeI∩ete5_d35bbo3Idget5ecIet$(|(ubect1ˉ∩|(ubemete5ˉd己5hboardget5a/促uber∩ete5ˉda5hboardˉo

]5o∩pat∩="{.5ecIet5[o].∩己眶}") ˉogoˉte‖p1ate=倒{{。dat己。to|(e∩ | ba5e64de〔ode}}□

可以看到会得到一个token,类似的内容如下:

ey〕hbCci0j〕50ⅢI1‖jI5mtpZ〔I6Ijγ日Q3「湖长「u酬Z刷W6γ∩pp0Mh00∩Zpd2pQγ"「h2"γ1∏刚Z51Xd"5xe№j句·ey〕pc3"j0j]Idb↓

〕1〔"51dCγ∑l〕‖1〔∩Zpγ2γ‖γ酬γd"5oIim己3γj2X〕uZⅫ1〔y5pby9zZX〕2a"‖1γ‖0‖jb3γud〔9uⅧ11c38∩γ20i0i]rd"〕1〔闹51dCγzl‖R h〔2hjb2「yZ〔15I"t1γ"γy咖γoZX№己"8γ〔2γyd‖1jZ"「jγ291b∩q/〔2γjc∩γo[Ⅶ5∩b0‖Uj0i〕r础〕1〔∏51dCγ∑l‖R∩c2hib2「yZαob2t1 bj1"〔3问yaiI5I顾1γ们γyb∏"/0Z酬‖a‖8γ〔2γyd∏1jI‖「jγ291b∩Q′c2γyd刚1jZ51hγ2‖γ删5Ol∏5hb0d」i0i]rdN]1〔∏51dWz[Ⅷ∩c2Mb 2「γI〔I5I∩t1γ『Wyh∏γ0Z邓ua‖8v〔∑γyd∏UjZb0「jγ291b∩Qγc∑γyd们1jZ51∩γ2‖vdb05Ol∏γpZ〔I6mZ∩αDk删Izl阳1叫ItⅫ戊删jO5m

r

γ肌『gz‖2‖h‖2[yZD】1问yIsI∩‖1γjI6I∩‖Sc3R1b丁p∑DO2aⅧ1γ‖‖‖jb3γudDprd‖〕1〔们51dCγzLⅧh〔2Mb2「yZDprd0m〔耐51dWz[Ⅷ 们〔2hib2「yZ□9.8p5d训5gA70XD`/2gwR〔ozˉ卯rj止γZ7R趴x叫[刷〔2〔丁8Ⅸ88O‖be∩∩I15γl[oe2u7g‖jα0240‖Z「8708j2十a5`′〔]∩ⅨQ γojγb【γ〕∧ˉ∩09dDL1pu仟1p‖D256RXˉ‖rb[0z1』Qi旺ˉpuwr8p5毗0【DRγs2』yt于α52r促gt6mB086∏讼‖‖jex5〔U∑5〔p∩们「1t丁促ZB‖γ‖{ u01gj[yˉ0∩‖hquZp[3wvr∧95〕[d5‖g7"58ˉγγ3yg3j1rt7γ〔8oiⅧR〕[cse阳γ4gvpq‖15d—6‖dxq212eu[[y8by『Q1tB[oa〕酗50o6【「

|‖|

DRbdbγ72〔刊「1b〔1P6[∩]03[p05o9r0ˉg58腮y1『oqQ

注意由于Secret的名称可能不同’所以如上命今可能会不同’请自行根据实际情况修改,以官方 丈档为准。

[↑7 7

』■γ』■■



第l7章爬虫的管理和部署

896

接下来,将输出的token粘贴到Dashboard的登录页面中,即可登玻成功,如图17ˉl7所示°

呈—匪



… ‖

戍现和负n均■ ‖



亏 ■

琵*





吩■

●书宅△

■…·

■肋办

蹿留室

■…

可◆

唾≡m…向=g≡ …=q~

寸c阅0!





日→七

m ■≡〔网p

0■↑■『

0《

!





》l

■丘粕存储 已■





岛平

DS空■

仍r

氏】∏

早■『P●

…些巴雪乙=≥



-=

0■0■]

$≡≡ ‖≤

° 》



》【



纠□∏』·■■』■纠可‖《·『■■‖可|

…二二三£Y



[|

≈甄9

图17ˉ17登录成功后的页面



占■



■●

』·可■

将命名空间切换为〔raW1er’即∏I看到各个服务的运行状态,如图17ˉ18所示° 十书 =→

一_■~●■~←-==

钝;函掌尊蝇; }:』管;芋蹿β露蕊

γ



。心



咖 ■田



_宁

…。

工作丘 ……



=~→

■●ⅧD e≡

m

. o



…S

工作■

∩≡廷

… 生

……

D…mt· 醉

伊■

●…

哼…

●…

←~

●…

w咕

……■ …山■□



二二仲▲■些

0

@_







Ⅱ自…

■■0黔

〗′】

】【…二≡

n●

0′‖

…==

!门

盯崖二=

M0

n…一宁≡

●●●



…氏■…





|||{}

……



→●

鲸—

■…







的■





m呻■



● ||

…鞋≡

.. 】



图l7ˉ18

Cra"1er命名空间下各个服务的运行状态

||}‖)

比如’这里可以看到Deployments、ReplicaSets和Pods的运行状态都是正常的’颜色为绿色 我们还可以进—步查看Scrapy爬虫项目对应的Pod,如图l7ˉl9所示。

』■■‖司||』■·习‖‖‖』‖■■‖|||司Ⅵ‖』】】■Ⅷ‖‖』‖‖|』=■■‖‖■‖□‖■』·‖‖』■■{‖」日‖」‖‖』勺‖|||」■‖」■】‖‖‖|』■|{|√‖|』』■]‖可二■·』■■■■

@贞o坤『冗·te

[ 卜

用Kubemetes部署和管理Scrapy爬虫

l7.7



了百尸□●□00





题ap……呵倾7“. Ⅲ8冈0

··



897

‖〕





元钠窿



青…

-

■守

亡■m

回■…

叮伺87山汉8冈8

… =…→w





醒1匡3日l4日

a且m……

m

"鳃]99cb…7]▲8M〗烟…佣3c

……



邮蚀

呼四_…劳口…



吐∩‖|~■尸

m

工∏■ ≈

∩吻■mg

~生





■■

●■U

?O‖Oj33

邑型乙硒当

0

…=●











兰 三 飞邑



……





0

吠c



r曲

■■…岭■

●瞪々〗咕砂〗■

丁■

巫…呻



匹≡=

■●

=_ ≡△=

县…

工7◆=



巫声==



空=≡

… ~ …

■尸≡ ■‖伯‖

控制



…… …

==唾≡~



卜 『 ‖

d0



巴∏■伊『||△∏但尸|

阁l7ˉl9



『■p|‖caset







】/]

25∏汕Ⅺnea呐

Scrapy爬虫项目对应的Pod

这里展示了Pod的详细信息’比如运行状态、时间、元数据等。另外’点击右上角(右起第四个) 按钮’即可查看该Pod对应的运行日志,如图l7ˉ20所示。 -—_—叶-→→■

} ●●■

















■=



●吾

↓‖

≈≈弓≈哇 …凸■=

■■m■= = ≡

小0【



叼皂个≡



■P宁=二 回仁1

山陋叶占=■

]◆品 【



民”己№【哗M

…… ‖〈

■■∏



『∏

●≈



『口

丁 】

∏ ∏



■尸













……=γ…… …■

图17ˉ20 Pod对应的运行日志

=■

=ˉ-





》‖

0导Z





§



■●



勺·■





怯评



卸々叫如

例冯

岁■■仍祷

喧』

佣助·

仕■

仕思





四必…‖〗霹咐肘孙″…

‖·)眠 么ˉ口 ■片警

γ



』…。躺厅『▲≥沁 贷 … 》∏ … 坐吓 仍… · `榷□ ■儿 肌估 肌Ⅲ伯正

↑几山涛翻』‖雕蛔…岭屿稻…

′』二■□庄凸′

■锰飘■■■β◇℃■



〗 》》 『↓峨■融翻…础…Ⅱ邪



诽山 狼下 牟 ‖仅 啥 的 Ⅵ №

■巴



……



h

令=

7“ˉx8硼8●



■‖∩

甲」川’……“蚁吨雄^…咆……‖… 但 础 爪 郡勤 ∩β 刑妙 惦朗` Ⅲ巳 ■∏ ˉ』 已

陆。□抛…骗郡啡阳方≡^

代=肝=当吵…口伯…〗^印…沁响…睡…



『■



气·





h……





≡…

叮斗

工作■

屈揪Ⅷ舌鳃■● ■】 =邱∏·■贡··●□

(‖

m

砰耳■·』●●





■m 孔

~~…





[吨S



竿



‖引『

γ

一…

□ …哑 `碱 …″叫……乙…山… 后◎压 泞…已 亡

叮毋



日志

牡…郁…疗庐咋 导≈ $… 巳…足呀…

哇→ 唾

.飞.sⅡ」、

■■■■■□仑见■■』■■■■■■■■■『●●■】■■■■■■■【■■■■■吞●■■■■■■■■■区





ˉ以 郁〗 旦酗 』 牵 防 ‖『 ◎

钢■

0



辕震,←ˉ一=ˉ绸雪露`霉=窜了霞露霄獭 °

…ˉ_~.…… :ube厂帕te■ q·沮■ 三

纠·■』

第l7章爬虫的管理和部署

898

可以看到,它在正常运行’并将爬取到的数据输出到了控制台°

另外,我们还可以增删爬虫的实例数。可以回到Deployments的管理页面,选择“规模,,选项卡’ 如图l7ˉ2l所示。 -≡—___

创■时阉

……

]/]

q】『…n围mo……

| @Ⅸ◎咖o刨

呻…

]/】

47响…m◎

| @ 『edt。

咖;…

]/?

mmⅢ□唾

■卯:■c……■

‖/『

28mmu恤岳■“

||

拽■

p

呻=

●■■

|@…c°"p·…m

◆■●●●■

●“cO汹∩…‖

「幽…

响∏割m………加 』■■■■



_一陛一

组洒

PodS 蛹蟹

节点

状态

■启

cP1』促用串

(…田》

!

■除

内存佐用《顾·a)

创迢



名字



■ ■ | 』 ■ ■ ]

p四●

掠疆



—→

| °…"· | 雹零





图l7ˉ2l

“规模,’选项卡

比如将目标副本数量调整为5,如图l7ˉ22所示。



缩放资源



dep|◎y爪e∏tsc「apyc◎mposjtede『Y〗o将更新为目标副本数. 目标酬本数迁∩

当舵的删本数出

5



规筷

|」

o此操作捐当于: 臆ub.。c1…1·ˉn。…1erd·p1◎y…窿…凰Py.◎mp。.止·α硒。ˉˉr·P1土…=5 取消

|」

图l7ˉ22调整目标副本数量



当然,我们也可以使用命令行实现资源的放缩’具体如下: 长ubect15〔a1eˉ∩〔raw1erdep1oy爬∩t5crapy〔o‖]po51tede硒ˉˉrep1ica5■5

执行之后’我们可以看到Kubemetes又新创建了4个Pod°我们可以在Dashboard中直观地看到

新创建的Pod’如图17ˉ23所示°

爬虫实例,可以协同运行。

如果我们想增加或减少爬虫数量’只需要更改目标副本数量即可’比如将其修改为l00,就相当

‖●可■‖‖』勺{|‖‖』■

这样就相当于创建了5个爬虫实例,因为它们共享了_个Redis队列,所以它们就是5个分布式

于我们部署了100个爬虫实例’而且这l00个爬虫是协同爬取的。是不是非常方便?

〗」{|■■

| ‖



[卜 l78 Scrapy分布式爬虫的数据统计方案

899

c刚使用本 (…》

内帮使旧《by0■)

创■时回

……

ˉ■○◎◎◎□◎‖日

龋……ˉ……≡……………←……………≠………………

…ˉ……一

~=~°=·一°~

蹿==ˉ…………/

■■[『『△‖■也■『〗‖「‖■■■■■■田内》「β△■∩『}■■「■■『「■■户■■卜卜巴‖恬■尸|『|∩尸二■厂『|仕

框……。……ˉ………←…

庶/



印…■… 日”

…』 |

…;| 2……m◎

2………

n∏……

m…



图l7ˉ23新创建了斗个Pod

7.总结

以上我们就通过一个实战练习实现了分布式爬虫在Kubemetes中的部署’并且还可以非常方便地 使用Kubemetcs对Scrapy爬虫项目进行管理°相比Scrapyd来说,Kubemetes的功能更加强大’是— 个绝佳的管理Scrapy爬虫项目的利器°

本节涉及的知识点比较多’需要好好消化和练习°

本节代码详见https://gjthuhcom/Python3WebSpideI/ScrapyCompositeDemo/tree/docker,注意是 docker分支°

↑7.8 Sc『apy分布式爬虫的数据统计方案 在上-节中’我们已经学习了如何利用Kubemetes进行Scrapy爬虫的部署’并将其对接了分布式 的实现’对接了代理池`账号池’以顺利地实现数据抓取°

}『

p

围绕这两个问题’我们来探索Scrapy爬虫监控方案° ↑.准备

在本节开始之前’请确保已经完成了上一节的所有内容并能透彻地理解其原理’另外还需要你能

厂盯~|」

0

但这时候我们又遇到了—个难题,怎样监控各个Scrapy爬虫的爬取情况呢?比如,这时候我部署 了l0个Pod来运行Scrapy分布式爬虫’它们基于ScrapyˉRedis进行协同爬取,但我们并无法知晓它 们—分钟爬取了多少条数据’成功、失败、重试了多少次’难道要通过分析日志得出来吗? 另外,假如我们已经能成功获取了这些数据,又想进_步把这些数据可视化出来,做一个实时大屏 图表’用什么方式实现比较好?需要自己额外写代码实现吗?还是说已经有了非常成熟的解决方案?

{{

900

第l7章爬虫的管理和部署

较为熟练地完成利用Kubemetes部署Scrapy爬虫和其他服务的操作。 2数据统计

我们在ScIapy运行结果中会注意到它会时不时输出类似这样的结果: 2O21ˉO3ˉ1521:52:O6[5〔IaPy.e×te∩5jo∩5.1og5tat5] I‖「O:〔Iaw1ed33page5(at33page5/∏i∩)’sCIaped172iteⅦ5 (at 172jte"5/m∩)

这里显示了Scmpy爬虫的统计结果,里面包含当前Spider的页面(page)爬取速度和结果(item) 的提取速度,本例中—分钟爬取了33个页面,提取了l72个结果°

然而,当前我们基于ScrapyˉRedis通过前面几节的方案实现了分布式爬虫之后’仔细观察会发现 每个爬虫输出的结果都是各自的统计结果,比如其中_个Spider机器性能和网络比较好’爬取速度快, 那么它的统计结果就更高,表现不太好的Spjder的统计结果就差一些。这些Spider的统计信息都是独 立的、互不影响的’数据也各不相同° 这是为什么呢?





回想一下,之前ScrapyˉRedis实现分布式爬虫时,我们有两项关键配置: 5〔‖[0{」[[R= 阐5〔mpy-Iedj5.5〔∩edu1er°5c∩ed01er" 山p[「I[「[Rα∧55≡ "5crapy-redi5·dupe十11ter.R「PDupe「11ter冈

这里我们分别配置了5c∩edu1er和R「p0upe「11ter,其中5〔∩ed‖1er可以实现所有请求通过Redls 队列共享, R「pD0pe「i1ter可以实现去重指纹通过Redls集合共享°然而统计信息呢?有哪里配置共享 吗?并没有,因此每个Spider都是各统计各的’数据各不相干。

这样就遇到了-个问题’统计信息不同步而且很分散’这么多Scrapy爬虫究竟爬取了多少数据也 无从得知。如果通过日志来进行数据收集和统计,这个难度和工作量也不小’而且不精确。 所以’有没有什么简单方法呢?

当然有’按照5〔bedu1er和R「p0upe「j1ter的思路,将统计信息也通过Redis共享不就可以了吗? 3.实现原理

要实现这个功能’我们需要用到Scrapy的—个组件,叫作StatsCollectjon’翻译过来可以叫统计 信息收集器,它是—种Scrapy的Extension,即扩展组件。

Scrapy通过Sta临Co‖lection来收集键值对类型的统计信息,其中值_般都是计数器’这么多键值 对构成了一个统计表,可以理解为Python中的集合。比如上述例子中的爬取了多少页面,提取了多少 结果’这两个信息是可以通过StatsCol‖ection来保存的°

如果说得更直观_点,在Scrapy爬虫运行完成时,想必我们还注意到过类似如下的统计信息: {『d咖∩1oader/Iequest-byte50 : 2925’ ‖d四∩1oader/req0e5t-〔ou∩t! : 11’ 0d叫∩1oader/req0e5t-眠t∩od=cou∩t/O[丁: 11’ 0d凶∩1o日deI/re5po∩se-byte50 : 2M06’ 0d叫∩1oader/re5po∩5eˉ〔ou∩t0 ; 11’ 0d叫∩1oadeⅢ/re5po∩5e_5tatu5-〔ou∩t/2OO0 : 10’ !do切∩1oadeI/re5po∩5e-5t己tus-〔ou∩t/4O40 ; 1’ 0e1ap5ed=tj眶=se〔o∩d5‖ 8 3.917599’ 0+i∩j5‖rea5o∩0 8 |十j∩j5hed|’

二 0ite几5〔raped-〔Ou∩t|: 1OO’ 01og-〔ou∩t/D[8[」C; 111’ 01Og-〔Ou∩t/I‖「0! : 1O’

0

|Ⅷe∏u5a8e/阳X‖ ; 55242752’ 0『胎T‖‖Sage/5tartup0 : 35242752’







} b

l7.8

Scmpy分布式爬虫的数据统计方案

90l

0Ieque5t≡dept∩ˉ阳×0 : 9’ 0re5po∩5e-Ie〔ejγed〔ou∩t0 : 11』 0robot5txt/Ieque5t=〔ou∩t』; 1’ 0robot5txt/re5po∩5e=col』∩t‖ 8 1’ 0robot5t×t/re5po∩5eˉst己tu5-co‖∩t/』o40 : 1’ 05Chedu1er/dequeued0 8 10’ 05chedu1eI/dequeued/爬帅ry! 8 1O’ 05c∩ed01er/e∩que‖ed0 : 1O』 0SChedl』1er/e∩queued/‖记‖℃ry0 8 1O』 ,5tarttj贬! : dateti贬.dateti爬(2O21’ 3’ 15’ 1』’ 1, 32’ 357828)}

这个结果就是StatsCollection里面存储的常用键值对,比如jte"ˉ5craped-cou∩t就代表爬取了多 少结果’d酗∩1oader/re5po∩5e=5tatu5cou∩t/2OO就代表成功的响应次数有多少°

|}

‖‖ }

看起来挺清晰的’对不对?我们可以通过这些指标清楚地得知当前状态下每个Scrapy爬虫的运行 状态°

这是怎么实现的呢?其实在Scrapy中,它是通过一个默认配置好的StatsCollector实现的,叫作 MemoIyStatSColleCtor’这是Scmpy中内置的StatsCollector,我们可以通过配置5丁∧丁5〔[∧55来更改。 看下StatsCollector的源码,内容如下: j呕rtpprj∩t mpOIt1o8gj∩8

1o8ger雪1o鸥j∩8.8etlogger(-∩a眠—)



〔1己5s5tat5〔o11e〔tor:

de于

i∩it (5e1十’ CraW1er):

5e1千.-d{刷p≡〔r己"1er。5ettj∩85。getboo1(05丁A「5α肘P‘) 5e1十. 5t日t5= {}

de千getˉγa1ue(5e1+’ 代ey’ de「己u1t≡‖o∩e’ 5pider=‖o∩e): retur∩5e1千。 5tat已。8et(戊ey’ de+au1t) de千8etˉ5t己ts(5e1「’ 5pjder=‖o∩e): ‖

retuI∩5e1千. 5tat5

de十5etγ己1‖e(5e1+’ 代ey’ γa1ue’ spjder≡‖o∩e): 5e1千. 5tat5[低ey] =γa1ue 0

de千5et5tat5(5e1十’ 5tat5’ 5pide【=‖o∩e〉: Se1+。 5t日t5=5tat5

de千j∩〔γa1ue(5e1+’ 促ey’〔α』∩t=1’ 5taIt=0’ 5p1der≡‖o∩e): d=5e1千· 5t己t5

d[促ey] =d.setde+au1t(长ey’ 5tart)+〔ou∩t

de千帕xva1ue(5e1十’ 促ey’ γa1ue’ 5pjdeⅢ=‖o∩e):

5e1「. 5tatS[促ey] =‖ax(5e1十. 5tat5.5etde+au1t(促ey’γa1ue)’ va1Ue)



de+∏i∩γa1ue〈5e1「’ 促ey’γ日1ue’ 5pider=‖o∩e):

5e1十. stat5[促ey] 二∏i∩(5e1千. 5t己t5.setde十au1t(低ey’ va1ue)’γa1ue) de+〔1e己r5tat5(5e1千’ 5pjder=‖o∩e): 5e1+. 5tat5。〔1ear()

de十ope∩ˉ5pjder(5e1千’ 5p1der): pa55

def〔1ose-5pjder(5e1千’ 5pider’ Iea5o∩): j千Se1十·-du川p:

1Ogger.j∩+O("0U呻i∩g5Cr己Py5tat5:\∏□+PPri∩t.P+Omat(5e1千. 5tat5)’ extⅢ3={0spjder! : 5pjder}) 5e1千。-per5j5t5tat5〈5e1+. 5tat5’ 5pider) b



de于-per5i5t—5tat5(5e1千’ 5tat5’ 5pider): pa55

c1a55∩e∏my5tat5〔o11e〔tor(5tat5〔o11ector): de千

i∩jt (5e1十’〔r3w1er):

5upeⅢ()。_1∩it-(CI己w1er) 5e1+.5p1deⅢ-Stat5={}

de+-persi5t—stats(5e1+’ stats’ spideI〉: 5e1+.5pjderˉ5tat5[5p1deI.∩己贬] =5tat5

这里可以很明显看到例eⅧry5tat5〔o11ector继承自5tat5〔o11e〔tor这个类,而5tat5〔o11ector里 面又提供了-系列数据获取和设置相关的方法’比如5etγa1ue接收代ey和va1ue参数,将γa1ue存

『‖‖「‖|‖|||‖||}‖『门·■■■■■■■■■『巴■■■■■■■『■■■■

第17章爬虫的管理和部暑

902



储到5tat5这个全局变量里面’get-γa1ue接收促eγ这个参数’然后将γa1ue从5tat5这个全局变量里 面取出来并返回° (

这个5tat5变量就相当于一个大的表’爬虫开始运行时将5tat5进行初始化’然后整个爬虫在有

任何事件发生的时候可以调用一下数据修改的5etγa1ue方法,将数据的修改记录下来就好了。而

眶『mry5tat5〔o11ector的实现也非常简单’就是将5tat5初始化为_个Python字典’所以所有的数据



统计结果都是在内存中存储的°如果爬虫运行停止了而且这些数据没有保存下来的话,数据就丢失了,

而且这个数据也没有任何共享机制’所以每个Scrapy爬虫的统计信息都是一个独立的Python字典, 自然也就无法做到统计信息的共享了。

到了这里,我们就知道如果要实现Scmpy分布式爬虫的统计信息的共享’应该怎么做了吧?那就 是将5tatS全局变量通过Redis共享!

仿照ScrapyˉRedjs的其他模块的实现’我们可以将其实现’代码类似如下: 十ro∏‖ 5〔mpy°5tat5co11e〔toI51呻ort5tat5〔o11e〔tor 千ro『∏ °〔o∩∩e〔tjo∩mport+ro刚setti∩g5a5redj5+ro‖5etti∩gs 十ro‖‖ .de千au1t5jⅦport5『∧∏5旺γ’ 5〔什[山[[RP[R5I5「 十Iα∏dateti爬j∏PoItdatet1爬 〔1a55Redi55tat5〔o11e〔tor(5tat5〔o11e〔tor〉:

de千

j∩jt_(Se1千’ Cm"1er’ 5pjder≡‖o∩e): super〈). j∩jt (cr日训1eI) 5e1千.5eIγer=redj5千Io‖5etti∩g5(〔ra训1er.5etti∩g5) 5e1+·5pjder=5Pider

se1于.5pider∩3爬=5pider.∩a贬j于5pjdere15e〔ⅢaⅦ1er·5p1der〔15°∩a爬 5e1十.5tat5-Rey=〔m"1er.5etti∩g5.8et(‖5丁∧丁5旺γ!’ 5丁A丁5R[γ) 5e1十.persi5t≡cm训1er.5ettj∩gs.get( `5O‖[山[[【p[R5I5丁0 ’ 5〔‖[即[[Rp[∩5I5『)

de+£etˉ促ey(5e1十’ 5pjdeI≡‖o∩e): 1+5pjder:

5e1十.5tat5-促ey%{|5pjder0 8 5pjder.∩a‖∏e} i十5e1十.5pjder:

ret‖r∩5e1十。5tat5=key%{‖5pider0 : 5e1「.sp1der。∩a∏归} retur∩se1千.5tat5=Rey%{!5p1deI0 : 5e1+.5pider∩a∏论or |5〔rapy0} 肛1a55爬t‖od

de+十roⅧcⅢ己"1er(〔15′ cm"1er): retur∩〔15(〔r己W1eI)

de于get-γa1ue(se1十’ 促ey’ de千au1t≡‖o∩e’ spjder=‖o∩e): j十5e1+.5erγer.hexj5t5(5eM-get-keγ(5p1der〉′ 促ey): retur∩j∩t(5e1「.5erγer。∩get(5e1+.ˉgetˉ低ey(5pjder〉’ 代eγ)) e15e8





0

l7.8 Scrapy分布式爬虫的数据统计方案

903

b

retur∩de伯u1t ‖

de+get-5tat5(5e1「’ 5pideI=‖o∩e): retur∩5e1+.5eIγer。∩geta11(5e1十.—getˉ促ey(5pjder))

de十5etγa1ue(5e1十’ key’ γa1ue’ 5pider≡‖o∩e): 5e1+.5erγeI.∩5et(5e1十°ˉget_代ey(5pideI)’ 低ey′γa1ue) de十5et5t日t5(5e1+’ 5t己t5’ Spjder=‖o∩e): 5e1十·5erγer。h"5et(5e1+.ˉget-key(5pjdeI)’ 5tat5) de十1∩cγa1ue(5e1+’ 促ey’ cou∩t≡1’ 5tart=o’ 5p1der=‖o∩e): i「∩ot5e1十.seIγeI.he×i5t5(se1千.£et≡长ey(5pjder)’ Ⅸey): 5e1千.5etγ日1ue(促ey’ 5tart) 5e1+.5erγeI.∩j∩〔rby(5e1+.-getˉⅨey(5pjder)’ key’ cou∩t) 匹■■■■■■|卜|‖‖』·‖■■■■「}▲■■尸∏卜『仕■■■■■〖‖芦■■『『

de「Ⅶaxγa1ue(5e1+’ key’ γa1ue’ 5pider=‖o∩e): 5e1千.5etva1ue(侦ey’川ax(5e1于.get-γa1l」e(促ey’ v31ue)’γa1ue)〉 de十‖j∩va1ue(5e1「’ 促ey’ va1ue’ 5p1deI≡‖o∩e): 5e1「·5etγa1ue(key’|m∩(5e1+.get—γ己1ue(代ey’γa1ue)’γa1ue)) de+〔1ear5tat5(5e1千’ 5pjder=‖o∩e)$ se1「。5erγer.de1ete(5e1十.ˉget-低ey(5pider)) de十ope∩-5pjder(5e1「’ 5pider): j千Sp1der: 5e1千。5p1der=5pjder

de十〔1oseˉSpider(5e1十’ 5pjder’ re己5o∩); 仲

5e1千·5pider≡‖o∩e i十∩Ot5e1十°per5j5t: 5e1+.〔1ear5tat5(5p1der)



这部分改动需要放在ScrapyˉRedjs源码里面’这里主要的改动就是将5tat5修改为RedisHSET实 现’因为HSET就是Redis中的_个用于键值对存储的数据结构。比如5etγa1ue就可以修改为h5et方 法实现, get-va10e就可以修改为∩get方法实现。

0

}[『巴‖‖‖日‖『

大家看到这甩可能会眉头一紧’心想这个功能还需要自己去修改源码实现吗?这样会增加不少工

作量°在ScrapyˉRedis068及以前的版本中’确实需要这么做°不过幸运的是’我已经把这部分功能 实现了并合并到ScrapyˉRedis源代码中了, 自ScrapyˉRedis07』版本开始,大家就可以直接使用了° 另外’默认情况下,大家会安装最新版的ScrapyˉRedis,所以大多数情况下是可以直接使用的° 巳

怎么使用呢?很简单’只需要在Scrapy爬虫项目里面的se忱ingspy中添加如下的—行配置即可: 5『A丁5α∧55= "5〔r己py-red15·5tat5。Redis5tat5〔o11ector"

仅仅通过这一行代码的配置’我们就完成了如上所有的工作’不需要手动实现Red155tat5〔o11e〔tor 这个类了°

接下来,我们重新运行下Scrapy爬虫,通过RedisDesktopManager连接Redjs看下,这时候就会 发现运行过程中就多了一个Redis的key’如图l7ˉ24所示。

vQdb0旧) v妇b。。低(3) |}■

7。。°烯°u…|!。「 vb。°低:咽…

7…… 图17-24Rediskey列表

厂↑7



p

■■■‖‖



904

第17章爬虫的管理和部署

这里多了_个book:5tat5的key,打开看下结果’如图l7ˉ25所示。 ∩■mTm 呵



… =

捏0 啦-~≡…==■

976 ……一ˉ…■■■

审北■抽…廷…△≈…飞…Ⅷ叮飞

2

〗涵 ≈…

■■乙■=已立■■巳已■■■■L屯心迅、飞≈巴~飞私、…弘飞心耻酗毗■乃…飞b■沁吐、■■h℃■→刀<■■≈■甲▲电■■■~≈

≡}二



…… △==—=■~b■■≈…

■≈■迅、立泣电元缸呻电己N、▲呛兄唾准屯甄≈电沁■诞玛_

…ˉ…Ⅲ唾….=…忧.≡…

5 =

976

67

曰■↑ ↑86 巴≈●■■弓■~舌

「■■≈■≈■℃呛钻屯坠啡啡^中但■吐L和呛……、飞LD■已≈■

3‖田 灶吐牛巴n让飞L…屯■江■∏廷咕w乙靶电巫…、廷m■…廷≈■田工b心■

2 ■、乙℃巴唾■■审0■P幻拘、m迅津■mn■`●甜龟■泊■b…咕…下已■

…7酗O



…3

…∩≡-… 乙L■己乙■■nm■≈∏m…≈屯、……飞…珐■~■■℃、■■巴■bL■

▲≈■…■■巴≈■■了………■w■电 =…





……=…=…

_■

…■……

…_{

7酪 …

__」

帕 …

汁……

γ幻

…m ■心■■廷…飞飞▲屯企▲飞屯…七≈让锰…∏0摊巴≈■屯、、屯△可■■已B泊

=……t =……‖



7

型、℃吐■≈迅虹酞nⅡ…■■、≈■、吐n、、≈弘唾……乙△■电℃◇■

ˉ

=…=『……

976

图17ˉ25运行结果

这时候我们可以看到所有的Scrapy统计信息都存储到这里了’而且多个Scrapy爬虫通过此统计 信息实现了数据共享,任何-个爬虫的数据修改都会直接反映到这个Redis的HSET里面,这样我们

就实现了Scrapy分布式爬虫的统计信息共享° 4总结

d

在本节中’我们首先介绍了ScIapy爬虫的统计信息是怎么实现的,然后介绍了如何实现Scrapy分 布式爬虫的统计信息共享。

统计信息共享是-个非常有用的功能’为后面数据可视化打下了基础,后文我们会继续学习如何 基于这些信息进行数据可视化°

↑7。9基干尸『omet∩eus和G『afa∩a的分布式爬虫监控方案



在上—节中,我们已经实现了ScIapy分布式爬虫的数据统计’这些统计数据可以通过Redjs共享’ 以实现数据的集中化存储和同步。 接下来’我们可能想做的就是基于这些数据’将其可视化出来,比如我们想要的结果可能是这



样的°

□不用太多复杂的编程或配置就可以搭建_个高性能、高可用、实用又美观的数据可视化面板’ 面板中可以实时显示爬虫的各个指标的状态°

□当某项指标异常的时候,可以通过各个方式通知我们,以便及时调整和修复°





l79基于Prometheus和Grafana的分布式爬虫监控方案



905

以上的两个需求其实就恰好对标了监控的两大核心问题—可视化+告警’这两个功能是监控系 统的核心重点。

本节中,我们要介绍的就是—个企业级监控数据的监控方案_Prometheus+Grafana’其中前者

可以实现流式监控数据的收集、存储、查询、告警’后者可以实现强大又精美的可视化面板,同时也

配备了告警功能°二者经常放在—起使用’同时二者还是云原生的重要部件’与Kubcmetes结合起来 可以实现全方位的数据监控和告警系统°

下面我们就来介绍Prometheus和Grafana’并介绍如何将二者与Scrapy爬虫结合来实现数据监控。 ↑。P『ometheus

Prometheus是一个开源的服务监控系统和时间序列数据库,用于存储一些时序数据’比如某个时

刻的各个指标数据等°它可以从-些HTTP页面来拉取符合格式的时间序列数据,同时也支持通过中 间网关来推送数据°另外’它还支持灵活的查询语言PromQL。利用PromQL,我们可以非常方便地 查询和分析某个时间段内的各项指标数据。另外,它提供了可视化UI’还支持告警配置等功能° Prometheus在记录纯数字时间序列方面表现非常好。它既适用于面向服务器等硬件指标的监控’ 也适用于高动态的面向服务架构的监控。对于现在流行的微服务’Prometheus的多维度数据收集和数 据筛选查询语言也是非常强大的。

图‖7ˉ26说明了Prometheus的整体架构’以及生态中一些组件的作用°

a伯巾吨

。..

…曲nU 呸矗顽]

…■

…呵

{鱼董]志{ˉ丽可 {-霉可



发现目标



|°……‖·』… |

服务发现



峰画

Pm们etheus

≈沙

|…删凰′ | 尸ushgaleWaγ

pm『∏et∩euS服务器

pu●h

0■

●幻m

叫…

………ˉ.

毛 =;; .…j

pmⅧoL 二







数据可视化 和导出

p『o们et∩euS

目标

图l7ˉ26 Prometheus的整体架构 2.G『a怕∩a

Gramna是一个开源的`拥有丰富Dashboard和图表编辑的指标分析平台’它专注于时序类图表分 析’而且支持多种数据源,如Prometheus`Graphite、InfluxDB`Elasticsearch`MySQL`Kubemetes、 Zabbix等。





| 906

第l7章爬虫的管理和部署

Grafana的一个最大特点就是’界面非常酷眩!非常接近于一些企业级可视化大屏的效果°图l7ˉ27 就是用Grafana做的可视化监控面板°

Gm伯na对Prometheus有非常好的支持°在Grafana中’可以添加Prometheus的数据源’同时支 持调试、查询和可视化展示°

3.准备工作

在开始学习接下来的内容之前,请确保你已经完成了上一节的内容,能将Scrapy爬虫项目在 Kubemetes上运行起来。

| |

另外’我们还需要安装另_个Kubemetes套件Helm,利用它我们可以快速部署一些Kubemetes服 务,安装流程可以参考: h仗ps://setupscrapecenter/helm°

做好如上准备∏作之后,我们就可以开始实现Scrapy与Prometheus+Grafana监控系统的对接了° 要想完成这个工作,有三步需要做:第_步便是生成Scrapy的Expo戒er,用于收集统计数据;第 二步便是将Grafana与Prometheus对接起来,构建可视化面板;第三步便是配置告警°





乒r 『 ◎■吕□

′″ ˉ\ (0 ;岛

吵…→山邱■

』′.了-\

β』霹、

、bF■■0厢

睁■■…

20↑35∧M

3W■吨巴,』dw伯‖

伐…甲呻叮

08

939

∩一

‖■■■ 仲吓呻…■

w…



0

9蹲 …8

000■

』..

′0

≡←·.■°」q.

『丁!

妒个.0 创 =凸, ..ˉ .

也…闪…砷■

· ■四庐啪■亏

.…. 】→

… 『…

〔啼

℃…凹

‘ 『

…啼…





|」 〗0……0



n》顺■丫Ⅷ了

………哼岔∩l…p●门

碑■…





?热 …

oJ…~ …

『9……



f≡尤

……『D硒什… 卢产T…1…

▲二■←… …■面

0…≡

. .-

.



h ■





■■













丁■=●占·∏广护『■■0J0q『0

【‖





F≈0二T=





~==一==;==∧→.

■■■■

=】



G…□

q ■■■贮』■■

l『″●…

≈■←■Tp“

b

←■ ●■

m谷■0吕0回…印≈…倍■≈

′ˉ



L°时■o·■F

■ 00 0 αq■■叼

图l7ˉ27用Grafana做的可视化监控面板

4.Sc『apy匠xpo忧e『

接下来’我们首先来看看怎么将Scrapy的数据对接到Prometheus,这就需要生成_个Scrapy的 DataExpo肮er。

我们首先来看看Expo冗er大约是什么样子的’如图l7ˉ28所示。



可以看到,这就是_个HTTP网页,网页内容遵循_定格式’这个格式可以被Prometheus自动解

析并存储下来°我们需要做的就是为Scrapy生成这样_个网页,然后将这个网页的地址告诉 Prometheus, Prometheus便会每隔_段时间来抓取这个页面’从而获取到Scrapy的各个指标数据了°

| {

【■■「■可》■‖■广[‖‖巳『【‖】】『》

l79基于Prometheus和Grafnna的分布式爬虫监控方案





÷争◎o。ca‖h·霹↑爵鳃‖o/『we《『|cS



907

☆p冉●

毋HE四py亿h咖→瓣少bj●c亡sC°11●c七“西亿■』吨j…亡●C◎11●◎c叫dur1冗ggc

FTr酝…h匈=醉=吨j酝t■=c°11●cc“cO四1C◎uut■正 w允h◎n~辨=◎bj…t尘@◎11●@t唾亡◎m1{9…r■c1◎n■■0●》 2305°0 ■〗『‖‖■『匹■■■‖『【〗‖『■■「

…h轮卯=亡jm亡≈e◎11●亡仁m七◎亡●1{…●x■亡1◎h■■1●} 148】·o ”C■m-辆=◎bj…t■≈c◎11●c七…仁◎c■1《…r■c1◎回■·2■》 10.0

p…w迹…=卯产jm允■=■…】1…t■D1●切亡●1U…11…仁■b】●◎b〕●etf印皿ddux』■g唾 p■P■w七hm=…一吨j…t■』n罕°L1■c●b1●t◎ca1cm∏c●正 叮亡hm一守=…j…蚀■皿m°1L…也b1●c◎它■1《庐n●正唾』◎炉●0●}O.0 阿士hOn=庐=吨』唾七●」m◎11…c…1●…●1《……■七儿m喀己』●}0●0 w℃h蜘=庐=唾〕…t□=n睦◎11●c七■b1●…■M…r■七止…闻3竿》0勺0

pmLF叮亡h◎■=痴夕◎11●◎七』◎n■七吭■1…r◎fc』…■七h止■g●n·r■cL◎n■■□@◎11唾cm ◆mm”也◎n=庐≈c◎1】…』m■七◎t■1…n亡●r

β〖■『‖『■■■■■■■■■甘。巴■■■『「

Py亡hm=如→c◎u…七1◎n■记◎四1{9…r●仁』m■□0·》 223.O py七h鲍=庐≡◎◎11…七』◎n■t◎七●1《T●■●ru七』◎n■·l酒》 R0.0 P派h叹宇=四11睡过m■t◎c■1《卯…r■tmn●□2·} 1.0 ◆m酶吓m∏=inf◎…h◎nP1■亡【◎r■儿■Ⅲ◎…亡』◎n

pmm…h■=in£◎g■n9● ”亿h…=mr◎{…】…■七a七上◎…·c……●0■』唾■□3·0mE◎r■□7·′严cch1●‖●1■●9□o…x■止◎田■■】.7.9创} 1.o ■…Pr…■●哲』rc皿1=叮=坷七●■v【rt汕■1…■上z●mhy七●■· pHP■w“m■v』rcu■1=…tm铀Ⅻ辨

PZ唾●●·-r』x士u■1=正Yˉ切七●■]嗡1■90鲤10d●+09 F…Dr…■工●■1山n≡巫…m●止巾■t…ry■』B●止n叮七●·.

pTYP■P∏唾m●=工■』“n亡=叮=酌屿■g●闻■ 江…●■=r●●』由h亡=叮=城七●■6ˉ■7J0■9…07 F…Pr…■■■七△rcc1■■●…■β■r七c…◎fth●F正唾●■■■1∏◎●un1Ⅲ●…b止n■●e◎nd·· F壶PEP【呸●■■●c■r七七垣●…md●g■″ PⅢ唾●●■■亡▲庄m…■●c…■】·6169】9而770●◆09

FH画PPZ…●■■罗咖≈■●c蛔d■c。四1…1u■●工■■d■y■t…cPU七…■严回c人n■韩◎ndU· pTYP■■r◎cm■工砰=□●c砸“c◎t■L四m七●r p正…m■=…=■…◎m■c◎龟·10·0400000咖O0O0O01



■…停r…■…~£d■…r◎F…nf11●…■c正儿P亡◎r■· GmmPr“●■●…fdD■●凹瞬 …●●●=…≡亡d■00.0

0

p…严匹●■■=几fd■……■u…◎f…f』1●d●■cmp亿or■·

FTTP■P∏…●■●=m乳£仗■□●n知 P…●■■二…刀=fd■1·000576●+06 『‖二》口‖■『‖‖‖‖匣口■‖■〗‖囚‖

p…■cr■吐』…ˉ■巳工…E醉凸r儿c…F■r… p…唾x…1…=■□ra…g■u庐 ■C正■Py-儿七‖■■=■◎r■…《■醉d●r■■■◎m”…■1七…锄》 170.0 ◆工凹■om碱=』亡…型工…8p1由r蛇‖■■dr…

伊TYm■c丘呐=1t…=dr◎…dg巳ug■ ●cr呻=1七乙≡dx◎…d《■P儿“r坊口哇r蛔…止c“…懂}0.0 p…睦r…=m●碑n■●=r…』v■SP1凸rr●■…■mr唾●』Ⅷd ■可E哇r●”=m■mn■Qr…止…g■凹知 pm四…正■雷sp』“工…砷d 伊订唾■cr■叮=…咆d9●u卵 ≡==→=■·

==^←=■0^=B▲■~=■■■←■≡≈_■■‘▲二≡■■?

0



图17ˉ28

ExpoIter

那么’Scrapy怎么生成这个页面呢?它不是用来爬取数据的吗?生成HTTP页面不又得需要一个 HTTP服务器吗?这又是怎么实现的?其实,Scrapy的功能十分强大°Scrapy确实主要用来爬取数据, 但这并不妨碍同时提供_个HTTP服务’而且HTTP服务可以和Scrapy本身关联起来,比如读取到 Scrapy的各项统计指标,然后将数据展示在网页之中’这是完全可以做到的。

怎么实现的呢?其实还是借助于ScIapy的Extensjon。关于Extension的更多知识,请参考前文内 容°另外,我们还需要借助一个开源工具,叫作pmmetheusc‖ient,利用它我们可以通过传人-些参 数构造不同的指标数据,生成Exporter格式的内容。

关于ExportcT的实现原理,这里就不展开讲解了’我们可以直接借助一个开源库GempyPmmetheusˉ



Exponer来实现。 安装方式很简单: pip3i∩5t311gempγˉprα爬t∩eu5ˉexporter

安装完成之后,需要在semngspy里面配置并启用: [Ⅺ[‖SI删5≡{

0gempy-pro爬t∩eu5expoIteI.exte∩5io∩.‖eb5ervi〔e|: 500’ } p

配置完成之后’我们可以在本地启动一下Scrapy爬虫’这时候我们就可以在本地http://loca‖host: 94l0/metrjcs看到类似图l7ˉ28的结果,比如其中有一些指标如下:



#Ⅳp[ 5〔mpy-jteⅧ55〔mpedgau8e

scrapy一jte‖5scraped{5pjder="5crapyco们positedem脚}174.O

#‖[[P5cIapy-jte"5ˉdIopped5p1deriteⅦ5dropped #Ⅳp[5〔mpy=1te∏5droppedgau8e

5crapyˉite肌5一dropped{5p1der≡赋5crapy〔o呻o51tedeⅧo冈}O.O

#‖[[P5〔rapγ-re5po∩5ereceived5pjderrespo∩se5recejved

| —

_

厂—↑7 k

#丁γp[5〔mpy-re5po∩5ere〔eiγedgau8e #‖巳p5〔mpy-ope∩ed5pideIope∩ed #Ⅳp[5crapy-ope∏edg己oge

5〔mpy=ope∩ed{5pider≡"5〔mpyco"po5jtede咖"}1.o #‖[[P5〔mpy-〔1o5ed5pider〔1o5ed #Ⅳp【 5〔rapy-〔1o5edgau8e #‖[Lp5crapy_doⅦ∩1oader-reque5t一byte5… #Ⅳp[5〔rapy-d酗∩1oaderˉreqUe5t-byte5gauge

scmpy-dow∩1oader-Ieque5t-bytes{5p1deI="5〔rapγ〔o们po51tede∏℃"}616407.o

日〗‖」】】

第l7章爬虫的管理和部署

908





5〔rapγˉ1teⅧ55〔mped就是_项Prometheus指标名称,其中有—个5p1der属性叫作 5crapγ〔o‖po51tedeⅧo’其值是174,这代表已经爬取了174条数据。另外还有其他的指标’比如 5cmpy_do"∩1oader-req‖estˉbγte5就代表do"∩1oader请求的字节数,值为6164O7.O° 随着Scrapy的运行’爬虫的各项指标肯定会变化的’我们可以隔-段时间刷新下刚才的页面: #丁γP〔5crapy一ite川55cIapedgauge

5crapy≡jteⅦ55〔raped{5pjder≡"5〔mpyco们posjtede‖∏o"} 5O9·O #‖[Lp5〔mpy-ite"5-dropped5pideriteⅦ5dropped #Ⅳp[5〔Iapy-ite∏5-droppedgal』ge 5〔mpγ-ite们5-dropped{5pider=』05crapy〔o川po5itede们o"}0。o #|ˉ{[1p5〔rapy-re5po∩5eIe〔e1γed5pideIre5po∩5esreceiγed #Wp[ 5crapy-Ie5Po∩5erece1γedg3uge #‖[lp5crapy-ope∩ed5piderope∩ed #Wp[s〔r己py-ope∩ed8al』ge 5〔mPy-oPe∩ed{5pjder="5craPy〔o|∏Po51tede『∏o0!}1.0 #‖[[p5〔mpy一〔1osed5pjder〔1o5ed #Ⅳp[5Cmpγ-〔1o5edgauge #‖巳p5crapy-dow∩1oader_reque5t-byte5 ° . 。 #Ⅳp[5cIapy-do切∩1oader≡Ieque5t=byte5gauge 5cmpyˉdow∩1oader—reque5t_byte5{5pjder≡"5cr己py〔o∏‖positede晌"}93OBo1.o

就是这样一个网页’在原来的基础上’Scrapy爬虫提供了HTTP服务,并在94l0端口上运行了 此服务°

P

| q

| 】



(|

接下来’我们就重新把这个Scrapy项目部署到Kubernetes上’并通过Servcie将94l0端口 暴露出来°首先,需要重新构建Docker镜像并更新°当然’也可以使用我已经构建好的镜像



ger们eγ/5〔mpyco阳po5itede川o°

修改kubemetes/scIapycompositedemo/deployment.yaml文件,添加co∩ta1∩erport的配置,最终修



改结果如下:

们etadat己;

||司‖‖□

ap1γeI51O∩: app5/γ1 k1∩d: Dep1oy『∏e∩t 1abe15:

app目 5〔mpycoⅧpo51tedeⅦ ∩3爬: 5〔rapy〔咖pO5itede‖‖O ∩a川e5p日〔e:〔ra"1eI 5pe〔;



rep11Ca5: 1 5e1e〔tOI:



Ⅶat〔hlabe15:

Ⅶetad己ta;

1日be1S:

app吕 5〔mpy〔o"positede川o

(||‖

app: 5crapy〔o呻o5jtedeⅦo teⅧp1ate:

5peC: CO∩tai∩er5; =e∩γ:

ˉ∩a爪e: ∧〔〔α」‖『p"[U尺[

γa1ue8 0http://a〔〔ou∩tpoo1.cIaw1er°5γ〔.〔1u5ter·1o〔a1;6777/a∩tj5pjder7/m∩doⅦ0

γa1ue: 0http://proxypoo1·cmw1er°5γ〔.〔1u5ter·1oca1:5553/m∩do刚0

‖‖{

ˉ ∩a‖记: pR0Xγp"[0R[





||

|‖

□‖‖‘‖』』』■■□·{|■■『』□■■‖

l7.9基于Prometheus和Grafana的分布式爬虫监控方案

909

|昌

ˉ∩a爬: R[DI50Rl

γa1ue8 ,IedjS://redj5.〔raw1er.5γ〔·〔1u5ter。1o〔a1:63790

1爬ge8 gemey/5〔r己py〔o‖∏po5jtede『∏o ∩a们e: 5〔rapy〔oⅧpo5itede∩℃ reBour〔es;

b

1jⅧitS:

∏记∏℃ry: "5O叫i圃 Cpu: w3帅∏" requeSt5: ‖e∏℃ry; 002O删i" 〔pl』: "1OO∩‖" port5;



ˉ〔o∩tai∩erport; 941O



这里就将94l0端口暴露出来了,其他配置保持不变°注意,此处如果使用你自己构建的镜像的

话’需要把1Ⅶage参数换_下°

同时新建_个kubemetes/scrapycompositedemo/service.yaml文件’内容如下: apiγeISio∩: γ1 促j∩d: 5erγj〔e 爬tadata;

1abe15:

appg s〔rapyco们po5itede∏旧 ∩a∏论: 5〔mpycoⅦpo5itede‖℃ ∩已‖∏e5pa〔e: 〔ra切1er SpeC:

port58 ˉ ∩a爬: 口9q1000 port: 9』1O targetpoIt8 941o Se1eCtoI:

app: 5〔mpyco们po51tede们o

重新运行部署: 灿bect1app1yˉ千代ubemete5/5〔mpy〔咖positede‖℃

结果如下:

dep1oγ∏记∩t。app5/5cmpycα∏po5jtedemco∩+j8ured 5erγi〔e/5〔rapy〔咖po5jtedemcreated

这样就成功了。

然后我们可以看下web服务有没有运行成功: 》

促ube〔t1portˉ+orward5γc/5〔I己pycαmo5itede『∏o9』1o:941Oˉ∩〔ra"1er

这时候重新打开http:/川ocalhost:9410/memcs’如果能看到如图17ˉ28所示的页面,就说明已经部署成 ‖≥『卜巴【

功了,而且通过9410端口就能获取到Scrapy分布式爬虫的运行状态统计° 5P『o『∏etheus对接



接下来,我们需要做的就是将上文实现的Exporter和Prometheus来进行对接’也就是将HTTP页 面配置到Prometheus里面,这样Prometheus就会定时抓取这些指标并存储下来了。

由于ScIapy爬虫项目是部署在Kubemetes上的’对于Prometheus来说,当然同样要部署到 Kubemetes里面才能访问了。

安装Prometheus时,我推荐使用Helm来安装’安装方法可以参考https://githuhcom/prometheusˉ 卜



community/helm=chaIts/∏ee/main/charts/kubeˉprometheusˉstack。kubeˉprometheusˉstack是-个Prometheus 和Grafana的套件,安装此套件可以同时安装好Prometheus和Gmfana,非常方便。 厂↑7 匹

□■■■■|』沮Ⅷ旧勺勺‖■■■』γ】】■■■■■‖|‖司

9l0

第l7章爬虫的管理和部署

个人推荐将Prometheus单独安装到_个新的Namespace下面,比如新建-个Namespace,叫作 们o∩jtor:



促ube〔t1〔Ie3te∩日∏记5pace |】℃∩itoI

然后利用Helm来安装对应的Repo信息: ‖e1川repoaddpIo眠theusˉ〔o咖u∩ityhttp5://pro们et∩eu5ˉ〔oⅧu∩jty。git∩ub.io/∩e1"ˉ〔harts he1Ⅶrepoupd3te

这_步相当于把一些配置文件添加到Helm中’这样Helm才能对其进行安装°

接下来,我们可以运行如下命令安装:



he1"j∩5ta11pro们etheusˉstac代pro爬theu5ˉ〔o∏■∏u∩1tγ/灿beˉpro『∏etheu5ˉ5t己c代 ˉ∩ 『∏o∩itor

运行成功后,可能会得到如下信息: ‖州[: prOⅧet∩eu5ˉ5taCk L∧5「D[pLOγ[0: 50∩例aI2822;27;372O21 ‖酬[5p∧〔[: ∏℃∩itOI

S『A『05: dep1oyed R[γI5I0‖: 1

‖O「[5:

低ubeˉproⅦet∩eu5ˉ5tackha5bee∩i∩sta11ed·〔he〔假jts5tatu5byru∩∩1∩g: 促ube〔t1 ˉˉ∩a‖e5pace |↑‖o∩jtoIgetpod5 ˉ1 00re1ea5e≡pro‖)et∩eu5ˉ5ta〔促"



如果看到如上结果,就证明已经安装成功了°

接下来,我们重新进人Dashboard,查看Prometheus套件的运行情况°运行Dashboard的流程见 l7.8节。

进人Dashboard之后’切换到Monitor命名空间,你可能看到如图l7ˉ29所示的运行结果,-些 状态还是红色或者黄色,不用担心, 目前还处在初始化的过程中,耐心等待_段时间,等待全部配置 完成之后,就好了。 @贞…丁独t·5

α

m



O



≡…

m

~=一… (

_ …

}工雁口赎葱

=…

|●●●●

≡~



d



m

≡=



←印■

…■

工∏■ 些

=空 =…

D……



■p



-







… …

●_印 .

.

……

孟=……

】』】

…ˉ=

…_】【

勺 ■v以0

-→

} ←宁趋

…≈

宦…■由

… 醉

… …

●-v山■ ■■■碉

尸…



■∩

·—-→

钧_刀0了-】』5

0′0

甘=■呻



‖0↑

j唾←一

!

啤_

…==

-…■默巴々



_~ ●…=………

但D

空=_D■

:



_…

…垦←盂^

=←≡



』■0



D

□一≈ˉ芭再=



图l7ˉ29运行结果



i

宰—…■狮百 ■◎■■T■■

■ ■



m

_~…

一劈…叮 ■粕∩

●一…~



』·」‖‖‖‖‖‖■||(·』】‖‖』■〗|‖|」|

…≈









| l7.9基于Prometheus和Grafana的分布式爬虫监控方案

‖■■



9ll

另外,每个Chart预留了—些配置项’这些配置项在values.yaml里面是可以复写`修改的,具体 的配置可以查看https://githuhcom/pIometheusˉco∏ununity/helmˉcharts/blob/main/charts/kubeˉprometheusˉ stack/values.yaml文件°比如,通过修改Prometheus的配置项,就能达到修改scrape-configs的目的, 这项配置叫作pro∏et∩eu50perator.proⅦetheu55pec.addjt1o∩a15〔rape〔o∩十1g5· 注意此项配置的名称可能会随着当前HelmChart的修改而不同,主妥是明白其原理,具体的配五

需要以官方丈档为准°

这时候我们可以将此项配置进行修改: prO『∏etheu50peratOI: #此处石略其他配Ⅲ项

pro∏论theu55pe〔; #此处石咯其他配Ⅲ项

日ddjtio∩a15〔rape〔o∩+jg58

ˉ job-∩3『‖e: 5cmpy〔咖po5jtedeⅧ 5tatj〔〔o∩千ig5: ˉ1abe15弓

『}

app8 5〔mpy〔o∏)po5jtede|∏o



taIget5:

ˉ 5c【apyc咖po5jtede∏n〔m"1er·5γ〔.c1u5ter°1o〔a1:9410

这里我们添加了刚才的Scrapy爬虫的配置项,指定了1abe15和targets’具体的配置格式可以参考 https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape-Config。 其中值得注意的是,我们将target5配置为5crapy〔咖po5itede|∏o.craw1er.5γ〔.〔1u5ter.1oca1:941O, 可以通过此Host在Kubemetes中访问到对应的服务。 配冒完成之后’可以通过如下命令应用此更新:

he1‖upgmdeˉ+γa1ue5°γaⅧ1pr叼记theu5ˉ5ta〔长prα∏et∩eu5ˉ〔o『Ⅶ∏u∩jty/灿beˉpIα∩et∩eu5ˉ5ta〔【ˉ∩m∩itor 如果出现类似下面的输出结果: (【■■■■■■【‖匹『‖■■■■『|□巳■■■■■■巳■■■凸■●『||[■■尸■|■■●【■【■【■■‖|□「‖「|卜△■『‖■【■■■

Re1ea5e"prα∏etheu5ˉ5ta〔促"ba5bee∩upgraded. ‖appy‖e1m∩8! ‖刚[; pIO∏记thel」Sˉ5ta〔k [A5丁0[pL0γ[0: 50∩‖arⅡ823822:442021

‖刚[5p∧〔[;巾∩jtOI

5「A『05自 dep1oyed R[γI5I删: 2

‖0丁[5:

促l」beˉprα∏etbeu5ˉ5tac|(ha5bee∩1∩5ta11ed。〔he〔|〈 jt55t己tu5byru∩∩j∩8: 促ube〔t1 ˉˉ∩a『∏e5pa〔e |∏o∩itorgetpod5 ˉ1 "re1e己5e=Prα∏et门e05ˉ5ta〔|(圃 就证明更新生效了° 6.G「a伯∩a配置

接下来,我们再回到Grafana的配置中,我们可以依然在本地通过低ube〔t1portˉ十orward将服务 转发到本地:

|q』be〔t1portˉ「oIward5γ〔/pro∏论t∩eu5ˉ5ta〔|〈ˉgra千a∩a30oα8oˉ∩∏℃∩jtor ■■■■【『■〗「■口『■『■∏〖■■■■■■■■■■■「}‖‖巴尸●『■尸{『凸■■【‖‖卜匹■=■『β

注意这里服务的名称可能随着HelmChart的修改而不同’主妥是明白其原理,具体的配五需妥以 官方丈档为准。

这里我们将Grafana从80转发到本地3000端口’这时候我们在本地测览器上打开h忱p:/川ocalhost 3000/’就能看到如图l7ˉ30所示的结果。



| | ‖

第l7章爬虫的管理和部署

9l2

ˉ… |

|}

‖〗

~—

■★ ■★央●§



We‖mmetoG『afa∏a 囚而′



o

!‖

L印【∩

图l7ˉ30欢迎界面

进人了Grafana的欢迎界面,但是并不知道用户名和密码是什么,这时候密码会存在对应的Secrets

文件中°在此HelmChart中,它存在-个叫作prometheusˉstackˉgrafana的Secret中,通过Dashboard我 们可以清楚地看到用户名和密码,如图l7ˉ3l所示° .kuber"ete国

α攫囊



p0UmB〔b酗8ˉs0ac伐~g饲↑巳∏a



虫# m…№恒

N■… …Q

≈和●mⅧ【Ⅳ帕8

…c归…

鳃宜{书 …

|日‖‖{‖|||‖‖°Ⅵ』‖|·‖‖‖■■■■□〗°』■□·‖』『‖·‖|



‖■曰■■■‖】『‖‖·■‖‖』|‖‖‖‖』‖』‖■□□日凸■■■∏』■‖』日||』■‖』■{』■】·□□□‖』‖‖‖□·■]Ⅱ』】■】‖』■■□】‖`』■】■】】‖||‖‖‖■





■况

■‖■‖』■|‖■·司■〗日』‖·■■■■‖‖』‖‖‖‖

工作■ ……

…蹿S

贮叫∏面协

…m

·… …

…出‖S ∩…■mcO毗喊|印$ ≈乙t°如‖盟■

=●/

●~th _一≡-≡≈—

≡=

…吨

n胆T负●唾

……

=●/

0bⅥes

…c

用户名和密码

」□]·‖■■』□□|』■■Ⅷ‖‖

图l7ˉ3l

■Ⅲ尸『血巴■■「‖■■■■■■■■厅‖‖■■■【■可■「卜Ⅱ■■■厅■■『

l79基于Prometheus和Grafana的分布式爬虫监控方案

可以看到’初始用户名就是admin’密码就是promˉoperator°在Grafana欢迎界面中输人用户名 和密码,我们便可以进人控制面板了,如图l7ˉ32所示° $

↑℃

◆+

它…`巳ˉα芭‖… ◎

践』 ☆力@





Ⅲ 喂 割



〔γ 牌

旧…

°

















它…

·□

’· 口。



ˉ们

巳 白



We|c◎metoG『afa『》a

醒 仔 ■

α



c「eatev◎u了∩悔↑抛S向b◎a『d

口□ 口口







…≡

m旧亡搽■旧℃晒m蛔早·飘QpS仰!沁『〔g肘t



nJ(α魁『妒由怨泡『h…thee∩h『冉咖@c‘龋0∩dc呻郊【眶℃刨冶









set呻刨叼『…闪0…G凶{助●『『wuh铆c∩O忱↑饭G呼侮徊摊0丁扣$

仙 ◎ 巴 日

D阿A铡阅C巴八"p鹏倒0…c$

G妇↑a刷日「0∏da门〗e础a‖S

酗…蹈∑Ⅶ刨鲤



丁w…}.

…“γ损』℃哪ck蚜 蚀咙…吧”…

己◎ 呻 卧 占 馋 ∩∏

BaS】c 丫№……肋‖

仙 讣口Ⅷ 。…

…吟…钨曰…



『∏ ◎



回回 办曰

k|

·+器翰总翻◎





…≥…鞭图碱≈土b呻陶……甸…=≡守……

镰→■≈

@…t…/酗瓤O富Ⅵ

÷



9l3

l出m"‖…↑"t仰e徘°巴$豺

L■临酣f『颜0!№b蛔q

O些小h◎■m牙



0P

o∏『N″0"祁回忙舱Ⅶw磐e″《蹿Ⅵα巳』■2`…次c乱锄el∩U山G啪!aⅦ即CkJ肘H『』4·巴!尸固庐田T决Ⅵm鄂 恐γ ■■w钾mF胎仙k℃憎∩彻Ul『@0旧问To础『凶$徊吨『qWh·瞬↑伶6‖↑份阂哪绅(he恬撼积逃w击二『…●可

{| ‖

|}

Fd p『OⅦ铆 …沁oJ智『『5口m沪β↑罚pm恫枷…柯《mh岳B…蓟陋mgfp◎蝉γ笆Un仔Fd孜『叫卸行鞠U档a哦阿【喻$片喳 ∩omv巳釉卧@,汕沮加眺腾γ口域『‖′饱烟?…洲el…(抽αⅢ§睬晦Ⅶse「《@剐』q》∧((酶蛔念卫〖″吵v管$呻o″" m√旺ⅦW障酗Ⅷ吨腻∩睁『t偷′蚀b抄q”聪【凹缸p『o恤耐诌『h罚pF印Gd匀9W裤$7啃pα№喊Ⅻ》‖0.引辫wm『



●·



WGrGd◎妒0‖O馏钉■uf住『!d咖虐砂巾白廊…绚出向 GP;0 .

&f↓00 i导龄呛

冗■f■



`.甄| 〃镭0



0川『■ ·

α刨a"Bv75}饰$蝉嘛r嚼鞠£酗06钒′5丁∩‖■8$陇■‖笋『$!№↓Gf仑潍$鳃h僧f″P吓P↓a叭巾嗽a1吵孰8〔Ba‖ 尸…孕≡→个…『训…·▲出…毡=‘≈…≡■^□=·凹≡…Q镭0钟^仆兄■T∩几0●…■▲产F讯击■伊夕●凸^↑企∩尸…≈

Grafana控制面板

图l7=32

接下来’我们切换到Explore页面,可以看到已经配置好的Prometheus数据源,如图l7ˉ33所示° -一_



罕一←~尸声=—=■贮…≈_=凹_=



■-_

~ ≈

























_

…←自←~一凸. ≡蜘臼二辨…碱含壁

冯 ∏甩



。+韶





◎∧回…γ

q0

0b擒?排?

踏吨Fy恬蛔γ

目《′沪『『



c酗吻…酗

P『O硒Lc们eatS‖eet Reqt』estRate 0o0γM胸·】p0丁q碑· 0回弓础‖沸j』

0□£呢□Q『正划电江祝△‖吐(U·髓』 i睡〔0田畸呻.行P色鸥+匀t蛇 〈匈U且犯生『凸t1U日 °b臼旧e{pd伞啮m吨Ⅷ驻■ b· 七‖ .{t0

∧‖e『t$∩「}Ⅷg g0前由么[;0扫电b■酬它『?蝉《比飘75寸■l台P甲】t蛇坠o·M士Ⅵ凹0{〗』卜.》〗 囤p (βteft内炒钳|

封铜3”k陆啪!$仆刨″凹台D翻沛!扩咐咖M‖№‖a乱扭‖…a

step

n哪|缸O沁v…『四●u↑殉0↑‖蹿`,o咖△f句↑刮En幻『m日0[唱轧?Ⅳ》ˉ恤. )拿∏份0.写绝沁可姻『铲刷小『…〖乳■h0p怨吵?卜凹‖厂狰巾‖吵b ↓舱讥轨q.;樱少,】‘们00 《,庐伊四 田UMa

》‖Ⅶ晒‖` 【』▲腮均■始"q盯白『Gp8蜘酬■‖h簿陀叔》坷!rOl0p硒翻m口』们刚吨7ⅫphbγP】砷』弓『闻}峨洲诚对圃沏肌加‖b ‖《0扦巴07p巴忙◆∩4↑『P?队钦M『『』夕户妒乙】『础w『

例如"●…印.





Q哼P可“

95↑们尸el℃e吨岭O『∩eqUe就儿at总№‖eS





剧曲醒寸

≈埠』项畴邯q轴p…G硒脸刨洲7丁尸掏吗l胞蓟呛”…爸mmuβ山"‖由W7





〈】……寸

Fm∩冲l啪岭

凸啊酮冈w尸00印翻怎…愚吗愉噶q瓜m可6鞭[沁l酝估∏p伊田鲸钧耐舱p脸淤!燃渺往饥晦m佃睡?恤驰或5‖铂仆k硝勺

| )



@臼p‖彼e 蚀p‖Ofe

学…



拿儡∩





=■

龄 · 厂

图l7ˉ33 Explore页面



第l7章爬虫的管理和部署

9l4

{|

如果上文所述的scrapecon6g配置成功的话’此时在这里我们可以搜索到scraPy相关的指标,如

」} b

图l7ˉ34所示。 钨@匠xp}O赔

α+韶。旱密·



p『o丽e小eu$

M0《吨Fv

3c厂3py



α忌Y可庐





T伞7‘0『.『ˉGO妈ptmad巳γˉ广C闽吗@■】 +…q匹v

ˉ巴□ .0.0. ‖甲『0飞◇砸沁广罕厂eq‖』韩‖ p了tPb b

.0《』.· T0‖协∩m哪6『~「伞咖字凸?=↑ordOⅥ







p『om〔 ‘总ˉˉ叭′.‘。wm.碱e0 『^;p°"爵萨 90尸』o》=d°门肛【°adG厂学丁FSpO∩■09=oγ《午舌

代eques .….vd…q10aO霉铲ˉ『e呼oFs守已魁鸟『.。



厂己t■1h00『

巳‖. , .〗.B了ˉˉ山罚l【仁al守 〔么!1G7亡〖】

G【γe∩a∩佣

G瞬『舍See@砸芯γe触ge陀q 『…e铡

.「1;汛. 1!G阶5□了Opβed

95thPe



’q .t已吁 】宁e呐串=弓rF坤间

Ⅳ】StOg厂≯【

0.

ˉ.

°. .′′

, .

.

『tp「谷αue$tdl』厂·『lO∩马 e电o『

.,



ca蛔湘↑Q巴咖eq5thpe『ce∩t‖!eOf洲丁丁p『它qt』谷Sq『抛eo爬『5『YMM』t台W|『`do删S 匠 ≈



■…









图l7ˉ34搜索scrapy相关的指标



比如此时我们输人5〔mpγ-jteⅧ5ˉ5〔raped,即抓取数据的条数,然后将时间范围设置为5min’点 击右上角的RunQue【y按钮,可以得到如图l7ˉ35所示的图表°









—…

| ‖

甘@它】p|ofe

『p.皱电T0庐F,;b

霸……P

||

【 . .于『 ‖··b~P . ’←巾『a』

隅P…`

$出p

山凡丁!7罗0 · b叮酣≈0 .0■∏■

f?G0·■√『

口日J广γ,叮舒瞻:″

虽『■□2°『》







●咆



j助已 .

■ §□罗p

■■ .

甩 瓣

仲 ■ · ≈■■■ ■

■0·个】 ■ ■□

■□ ■= ■

L

p·p■D

□■■

.■□〗…尸

臣pq々■….·■■F曲巳 =【P飞



『田b{b舍

〗■

尸=个■『■四『 护划

飞■



矿~见

凸■

厂□■■≈■●

…灼 凸□由岁 Ⅱ^■

?£邑

弓』台仕

巴‖豁 印…



●■

『■













■∩

图l7ˉ35搜索结果



这里我们就可以观察在过去五分钟内抓取到的数据量的变化情况了° 0q

当然’上面的页面仅仅是调试用的,我们可以试着创建—个Dashboard









l79基于Prometheus和Grafana的分布式爬虫监控方案

9l5

点击+号并选择创建Dashboard,创建一个Dashboard,如图l7ˉ36所示。

然后点击Addnewpane|按钮’创建_个新面板’如图l7ˉ37所示。 ●88刊eWoashb°a『d QuQ『yw碑



【■【∏卜■■■‖〗■ˉ〗■‖『’‖『■【「‖■■■‖『』■■尸∏■■■|β■

·→。哗函仔°垒……ˉˉ窍



丁扛=-

◎ c「eate 』ue『y |{

u『」

酮:〕

88oaS∩boa『d 0

臼「O|de「 f p

■凸■

出‖厕p◎『t ..】■ ■■■

q0‖:

L





+呻″…

=…℃睦

3OO



2OO



图l7ˉ37创建新面板

图l7ˉ36创建-个Dashboard

||}

这里我们可以按照如下情况配置,如图l7ˉ38所示°

□Met∏cs:可以任意选择,比如这里依然选择5〔rapyˉ1teⅦ5-5〔mped’代表已经爬取到的数据 条数。

□Legend:图例名称’这里我们可以直接使用{{app}}来获取其中的属性。因为该条统计信息 的app的属性就是5crapy〔o"po5jtede们o,所以这里图例的名称就叫作5〔rapycoⅦpo5jtede‖℃° □Format:保持默认值Timesseries即可’即按照时间序列显示°

□右侧的一些样式配置可以自行选择,比如配置面板标题、选择连线样式`是否显示数据点、预



@

NeWd日s|Vma『d/[drtp日∩e|





↓『p0d

P…

pi6c哎0

…Q



ob□00『,o}爸邑

翱呵$

"呵Ⅶ凯「■p函

■B

■■







‖〖T庐h□口虏■凸凸二

M

●F′



尸■■



〖 矿



…甲

■倪



■■

■ ■■■

■■■

■ ■

■■

ˉ峪



■■

呀·叫

■●





、■γ

’凸肌

■ ■ ■

…『y恤…

〗■…

…≡=■

■】■寺■屯◆ 驴鳃■0 ·

‖■q口I勺』讥

凸皂G弓b 飞『『凸Fc□

■巳∏□咀

0口∩■0乙召』;F庐

…■h□

un■巾 帅【0■耻



●■ □』

0r且· □』



l守扩.四

… ≈

■■■〖『‖□■『Ⅱ【『【■■■‖‖‖〔‖‖‖‖‖■■■■「『■『‖‖‖卜■■|□□■■【『‖『】‖尸「》∏『‖‖〖■′}‖’口■■】|□■□‖‖心□■■■Ⅱ‖『‖■■■■】】『二■尸性’‖■∏日~■尸■■■■■■■■△■■■■■【■■■■■■■『

警值等°

『lF伊…

p…兰止=pT

§』

7

…■

^蜘『〖咕巳…』q

o… ≈=…■蛔■咽0Ⅶ0‖b■!凹啤 且●=■

图l7ˉ38配置面板

-↑7 止

~-

h

第l7章爬虫的管理和部署

9l6

配置完成之后,点击右上角的Apply按钮’保存该配置面板。另外,还可以继续增加其他面板。 配置好了之后’将整个仪表盘保存下来°

比如’最后可以配置成类似图l7ˉ39所示的页面’这样我们就可以一目了然地看到爬虫的爬取状 态了. ■





β

■〗

■■

















$ 〗



门户



F■■

m=■▲0洼△帝

■|●□〗■{□■



伊==·^■F尸击0Ⅶ■…

回■■■





图l7ˉ39仪表盘

另外,我们还可以设置告警,它既可以在Grafana里面使用内置的Ale沉设置’也可以使用Alen

Manager设置。前者配置更加简单、方便,后者功能更为灵活、强大’可以根据个人偏好进行选择° 具体设置方式可以参考如下内容°



□GrafanaAlen配置: h仗ps:〃肛ahna.com/docs/gra1ana/latesUale∏jng/cIeate=alerts/。

□PrometheusStackAlen配置: ht‖ps://github.com/pIometheusˉoperator/prometheusˉoperator。 7.总结

本节中’我们了解了在Kubemetcs中监控Scrapy爬虫数据的解决方案°利用Prometheus’我们可 以非常便捷地收集Scrapy爬虫项目的各项指标数据,同时利用Grafana我们可以轻松地创建非常酷烛 的图表从而实现实时监控。



另外’我们还可以通过配置告警规则来开启告警,当爬虫数据或者主机数据异常时,我们就可以 及时收到告警信息了。



| | 』 (‖‖《』】□■叮—



附录爬虫与法律 近年来’很多和网络爬虫相关的法律案件和新闻报道层出不穷,如《裁判文书网数据竟被标价售 卖:爬虫程序抓取或构成侵权》°看到相关报道后’—些数据`爬虫从业者甚至发出了这样的感叹— ‘爬虫写得好’监狱进得早;爬虫玩得溜,牢饭吃个够°,,’还有人会将所有罪过都归咎于爬虫本身’ 甚至有人认为爬虫本身就涉及违法犯罪,于是不再敢学习和编写任何爬虫程序° 实际上,上面的这些认识是不对的。爬虫作为_种计算机技术’其技术本身是具有中立性的’法

律上也从来没有禁止爬虫技术,当今互联网的很多技术是依托于爬虫技术而发展的’如搜索引擎、数 据分析、人工智能和聚合导航等°爬虫是采集数据时的_个重要手段’不同数据本身的敏感性`安全

性和版权等又是不同的’因此如果我们不注意甄别哪些数据可以爬取`哪些不可以,或者使用了_些 非法手段进行数据爬取’就有可能触及法律红线,构成违法犯罪°基于上述内容’下面专门讲解~下 爬虫与法律的相关事宜,希望各位读者可以合理`合法地使用爬虫技术°

-`相关法律法规 目前’和爬虫、数据相关的法律法规《中华人民共和国数据安全法》已经出台并于202l年9月l日





起正式施行。另外’除了《中华人民共和国数据安全法》之外,《中华人民共和国网络安全法》《中华 人民共和国刑法》《中华人民共和国反不正当竞争法》《中华人民共和国著作权法》也是重要的参考。 经过‖k内法律相关人士统计,可能会和爬虫技术挂钩的罪名有:侵犯公民个人信息罪、非法获取计 算机信息系统罪、侵犯著作权罪、掩饰隐瞒犯罪所得罪、破坏计算机信息系统罪、传播淫秽物品牟 利罪、提供侵人计算机信息系统程序罪等°

二`判罚标准 根据业界相关律师的说法’目前相关案件的判罚是综合三个要素来看的,分别是动机、行为、结 果°一般来说’如果动机单纯且行为正常,同时没有造成不好的影响’那这种数据爬取是不会有问题 的。可如果其中某个要素不符合要求,比如结果非常恶劣’那么即使动机和行为没什么问题’也可能 会判定为违法犯罪。下面分别介绍_下这三个要素°

□动机:就是用爬虫爬取数据的目的是什么°比如’当在某个网站上看到_些不错的美文佳句 时’想把这些内容爬取下来进行深度学习模型的训练,那这本身是出于研究目的,是没有恶 意的。再如,当在某个网站上看到一些付费课程后,想把这些课程的数据爬取下来并放到自 己的网站上公开售卖,那这个动机就是不纯的,就构成了侵权和非法竟争°

□行为:就是用怎样的手段进行数据爬取°比如’对于一些公开的新闻数据’如果我们控制好 爬取频率’均匀地将其标题、正文等内容保存下来’那完全没问题;但如果我们毫无节制地 疯狂爬取,甚至导致网站不能正常对外提供服务’那这个行为就比较恶劣了。 □结果:就是爬取行为造成了怎样的影响°比如’我们用爬虫以适当的请求频率爬取了-个小 说网站上面的内容’然后只留作自己阅读’不做传播,这不会给该网站带来严重影响°但如 果我们通过某些方式爬取了用户的个人信息’还将这些信息贩卖给了其他公司,这就可能造 成数据和个人隐私的泄露,会产生非常恶劣的影响。





9l8

附录爬虫与法律

三`注意事项 下面我们再来重点说一下爬虫相关的_些雷区’在做爬虫开发时大家-定要注意避开相关的危险 行为°

□不碰个人信息:有些公司需要基于个人信息发展征信`风控、借贷等各种业务,于是会想方 设法获取这些信息,比如通过非正常手段采集用户的社保`公积金`工作、账单等信息’这 些都属于严重的违法行为°不管是爬虫的开发者还是公司,千万不要碰这种涉及获取、贩卖 个人信息的行为°

□不影响目标站点正常运行:爬虫的请求频率是可以由我们自己控制的’在做爬虫的时候_定

要控制好请求频率,如果请求频率过高,就会影响目标网站的正常运行,这是比较恶劣的一 个行为,如果目标网站的运营方以影响己方网站正常运行为由来告发我们’那我们有很大概 率是需要承担法律责任的°

□不碰黑产`黄赌毒:其中比较典型的例子是接外包或者合作项目。在接外包时,甲方可能只

是给_个需求’如做—个验证码识别服务`_套模拟登录程序等’这个需求看起来并没有什

么异常’但当我们把程序写好并给到对方后,对方可能会做黑产`黄赌毒相关的~些事情, 此时如果对方出事了,那我们就是从犯。所以,在接项目的时候要多加小心’事先就跟对方

问清楚程序会用作何处,_旦发现项目涉及黑产、黄赌毒’就_定不要接’或者如果在中途 发现项目有什么不对劲’也要及早抽身° 司 | 』 』 ■ ■ ■

□避免不正当的商业竞争:现在的很多网站和App’其-大运营目的就是盈利’这也离不开平 台积累的数据’如视频网站上的视频数据’天眼查、企奋奋等网站上的工商数据。虽说上面 所举的这些数据多是公开的’但如果我们写爬虫把它们爬取下来’然后放到自己搭建的网站 上,或者到处传播、贩卖,甚至从事相关的商业行为,就构成了不正当的商业竞争,对方完 全可以以不正当商业竞争为由来告发我们,我们就需要承担相关的法律责任° □注意版权保捣=版权亦称』著作权,,,指作者或其他人依法对某一著作物享受的权利°比如,

多数视频网站上的视频数据`小说网站上的小说内容`图书网站上的图书内容都是有版权 的,虽然我们可以爬取下来以便自己查看,但要注意不能在网上随意传播贩卖这些内容, 要注意对应的版权声明’否贝帜寸方也是有理由以侵犯著作权来告发我们的° □谨慎对待非公开数据:对于非公开数据,我们也需要谨慎,毕竟这是需要获得某个权限才能 获取的数据°比如’我们是某个网站的会员’登录会员账号可以看到_些只有会员才能看的 数据’如果我们将这些非公开数据爬取下来,之后_旦没有得当地保管这些数据’造成数据 的泄露或者恶意传播’就会惹上—定的麻烦°

□规避黑客行为:爬虫本身不属于黑客行为,它做的仅仅是把∏I以通过正常行为访问的数据采

集下来°违反国家规定,非法侵人计算机信息系统或者采用其他技术手段获取计算机信息系 统中存储`处理或者传输的数据’以及对计算机信息系统实施非法控制的行为都触犯了非法 获取计算机信息系统罪’需要承担相应的法律责任°

□不要公开分享逆向、破解方案:很多公司的软件会注明』未经本公司授权’修改、破解`反 编译、反汇编、逆向工程本产品,发布本产品的修改版、破解版等’需要承担相应的法律责 任”类似条款°实际上,对方很难知道我们私下对软件做的—些修改、破解等行为,但如果 我们在逆向`破解完毕之后还公开分享方案、代码,情况就很不_样了’对方可能会因此遭 受_定的损失,如果对方掌握—定证据并告发我们,我们就很难逃过干系了。我们发布的逆 向`破解方案和代码要是被不法分子利用了,我们就又变成了从犯,情节也变得非常严重° 所以,不要公开分享软件的一些逆向、破解方案。

以上就介绍了—些爬虫和法律相关的知识°学会爬虫之后’知法`懂法也是非常重要的!

|am∩appytoseet∩at尸Ⅵ∩o∩|ssow|de|γused|∩t∩eC∩|∩ese|丁co∏)∏)u∩|ty」∩ope t∩|sboo假w|||∩e|p∩)o「epeop|eu∩de「sta∩d尸Ⅵ∩o∩a∩dwebc「aw||∩g/sc「ap|∩g.

…°…°"』眉删删{慰;训| 时代在不断进步’我们需要不断学习,庆才在工作上是这样’在兴趣上也是这样’这本爬虫

书充分体现了这—点°第2版相比第|版内容更加全面,覆盖的知识点更为广泛’也更贴近技术前 沿。本书旦有通俗易懂的讲解和丰富的案例代码’可以让读者系统地学习爬虫相关的各种知识,我 极力推荐大家阅读本书°

·蛾亚洲……隐憾…嚣援| 在今天这个数据驱动的人工智能时代’这个有越来越多移动互联网数据来自∧pp的时代’主 流的数据来源平台几乎都提高了数据采集的风控水平)这导致数据采集的难度越来越大’此时行业

中需要-本书来帮助爬虫工程师提高技术水平°崔庆才的这本《尸Ⅵ∩o∩3网络爬虫开发实战(第2 版)》是市场上截至目前公开数据采集领域最好的图书之—,这本书能解答数据采集工作中遇到的 大部分问题,更难得的星作者还建立了技术讨论群’万便大家交流和提高°

梁斌pe∩∩y 北京∧友科技总经理`清华大学博士

作为第|版的升级版,本书增加了很多前沿的爬虫相关技术°从爬虫入门到分布式抓取’本书

详细介绍了爬虫技术的各个要点’并针对不同场景提出了不同的解决方案°另外′书中的实战案例 也在第|版的基础上做了重构升级’能帮助读者更好地学习爬虫技术°本书内容通俗易懂’干货满 满,强烈推荐给大家|

·国人属…人…橇蹦|

封面设计:崔庆才

定价: |3980元

巴≥



■≈乙





引∩

=吕

■‖









=‖



扫码领取 随书代码资料

ˉ了

图灵社区: |丁u「|∩gc∩ 分类建议:计算机/网络技术/尸Ⅵ∩O∩ 人民邮电出版社网址:www.ptp「ess.com.c∩



978-7-‖‖5=57709=2

ISB‖