您好,欢迎来到九壹网。
搜索
您的当前位置:首页爬虫进阶:反反爬虫技巧

爬虫进阶:反反爬虫技巧

来源:九壹网
爬⾍进阶:反反爬⾍技巧

主要针对以下四种反爬技术:Useragent过滤;模糊的Javascript重定向;验证码;请求头⼀致性检查。

⾼级⽹络爬⾍技术:绕过 “403 Forbidden”,验证码等

爬⾍的完整代码可以在 github 上⾥找到。

简介

我从不把爬取⽹页当做是我的⼀个爱好或者其他什么东西,但是我确实⽤⽹络爬⾍做过很多事情。因为我所处理的许多⼯作都要求我得到⽆法以其他⽅式获得的数据。我需要为 Intoli 做关于游戏数据的静态分析,所以我爬取了Google应⽤商店的数据来寻找最新被下载的APK。Pointy Ball插件需要聚合来⾃不同⽹站的梦幻⾜球(游戏)的预测数据,最简单的⽅式就是写⼀个爬⾍。在我在考虑这个问题的之前,我⼤概已经写了⼤约 40~50 个爬⾍了。我不太记得当时我对我家⼈撒谎说我已经抓取了多少 TB 的数据….,但是我确实很接近那个数字了。我尝试使⽤ /、 和⼀些其他的⼯具。但我总是会回到我个⼈的最爱 。在我看来,Scrapy是⼀个出⾊的软件。我对这款软件毫⽆保留的赞美是有原因的,它的⽤法⾮常符合直觉,学习曲线也很平缓。

你可以阅读Scrapy的教程,在⼏分钟内就可以让你的第⼀个爬⾍运⾏起来。然后,当你需要做⼀些更复杂的事情的时候,你就会发现,有⼀个内置的、有良好⽂档说明的⽅式来做到这⼀点。这个框架有⼤量的内置功能,但是它的结构使得在你⽤到这些功能之前,不会妨碍到你。当你最终确实需要某些默认不存在的功能的时候,⽐如说,因为访问了太多的 URL 链接以⾄于⽆法存储到内存中,需要⼀个⽤于去重的bloom filter(布隆过滤器),那么通常来说这就和继承其中的组件,然后做⼀点⼩改动⼀样简单。⼀切都感觉如此简单,⽽且scrapy是我书中⼀个关于良好软件设计的例⼦。

我很久以前就想写⼀个⾼级爬⾍教程了。这给我⼀个机会来展⽰scrapy的可扩展性,同时解决实践中出现的现实问题。尽管我很想做这件事,但是我还是⽆法摆脱这样⼀个事实:因为发布⼀些可能导致他⼈服务器由于⼤量的机器⼈流量受到损害的⽂章,就像是⼀个⼗⾜的坏蛋。

只要遵循⼏个基本的规则,我就可以在爬取那些有反爬⾍策略的⽹站的时候安⼼地睡个好觉。换句话说,我让我的请求频率和⼿动浏览的访问频率相当,并且我不会对数据做任何令⼈反感的事情。这样就使得运⾏爬⾍收集数据基本上和以其他主要的⼿动收集数据的⽅法⽆法区分。但即使我遵守了这些规则,我仍感觉为⼈们实际想要爬取的⽹站写⼀个教程有很⼤的难度。

直到我遇到⼀个叫做Zipru的BT下载⽹站,这件事情仍然只是我脑海⾥⼀个模糊的想法。这个⽹站有多个机制需要⾼级爬取技术来绕过,但是它的 robots.txt ⽂件却允许爬⾍爬取。此外,其实我们不必去爬取它。因为它有开放的API,同样可以得到全部数据。如果你对于获得torrent的数据感兴趣,那就只需要使⽤这个API,这很⽅便。

在本⽂的剩余部分,我将带领你写⼀个爬⾍,处理验证码和解决我们在Zipru⽹站遇到的各种不同的挑战。样例代码⽆法被正常运⾏因为Zipru 不是⼀个真实存在的⽹站,但是爬⾍所使⽤的技术会被⼴泛应⽤于现实世界中的爬取中。因此这个代码在另⼀个意义上来说⼜是完整的。我们将假设你已经对 Python 有了基本的了解,但是我仍会尽⼒让那些对于 Scrapy ⼀⽆所知的⼈看懂这篇⽂章。如果你觉得进度太快,那么花⼏分钟的时间阅读⼀下吧。号:923414804

群⾥有志同道合的⼩伙伴,互帮互助,群⾥有不错的视频学习教程和PDF!

建⽴⼯程项⽬

我们会在 中建⽴我们的项⽬,这可以让我们封装⼀下依赖关系。⾸先我们在~/scrapers/zipru中创建⼀个virtualenv ,并且安装scrapy包。

Python

1mkdir ~/scrapers/zipru2cd ~/scrapers/zipru3virtualenv env4. env/bin/activate5pip install scrapy

你运⾏的终端将被配置为使⽤本地的virtualenv。如果你打开另⼀个终端,那么你就需要再次运⾏. ~/scrapers/zipru/env/bin/active 命令 (否则你有可能得到命令或者模块⽆法找到的错误消息)。

现在你可以通过运⾏下⾯的命令来创建⼀个新的项⽬框架:

Python

1scrapy startproject zipru_scraper这样就会创建下⾯的⽬录结构。

Python

1└── zipru_scraper2 ├── zipru_scraper3 │ ├── __init__.py4 │ ├── items.py

5 │ ├── middlewares.py6 │ ├── pipelines.py7 │ ├── settings.py8 │ └── spiders

9 │ └── __init__.py10 └── scrapy.cfg

⼤多数默认情况下产⽣的这些⽂件实际上不会被⽤到,它们只是建议以⼀种合理的⽅式来构建我们的代码。从现在开始,你应该把 ~/scrapers/zipru/zipru_scraper 当做这个项⽬的根⽬录。这⾥是任何scrapy命令运⾏的⽬录,同时也是所有相对路径的根。

添加⼀个基本的爬⾍功能

现在我们需要添加⼀个Spieder类来让我们的scrapy真正地做⼀些事情。Spider类是scrapy爬⾍⽤来解析⽂本,爬取新的url链接或是提取数据的⼀个类。我们⾮常依赖于默认Spider类的实现,以最⼤限度地减少我们必须要编写的代码量。这⾥要做的事情看起来有点⾃动化,但假如你看过⽂档,事情会变得更加简单。

⾸先,在zipru_scraper/spiders/⽬录下创建⼀个⽂件,命名为 zipru_spider.py ,输⼊下⾯内容。

Python

1import scrapy2

3class ZipruSpider(scrapy.Spider):4 name = 'zipru'

5 start_urls = ['http://zipru.to/torrents.php?category=TV']

你可以在上⾯的⽹页中看到许多指向其他页⾯的连接。我们想让我们的爬⾍跟踪这些链接,并且解析他们的内容。为了完成这个任务,我们

⾸先需要识别出这些链接并且弄清楚他们指向的位置。

在这个阶段,DOM检查器将起到很⼤的助⼒。如果你右击其中的⼀个页⾯链接,在DOM检查器⾥⾯查看它,然后你就会看到指向其他页⾯的链接看起来像是这样的:

122334

接下来我们需要为这些链接构造⼀个选择器表达式。有⼏种类型似乎⽤css或者xpath选择器进⾏搜索更适合,所以我通常倾向于灵活地混合使⽤这⼏种选择器。我强烈推荐,但是不幸的是,它有点超出了本教程的范围。我个⼈认为xpath对于⽹络爬⾍,web UI 测试,甚⾄⼀般的web开发来说都是不可或缺的。我接下来仍然会使⽤css选择器,因为它对于⼤多数⼈来说可能⽐较熟悉。

要选择这些页⾯链接,我们可以把 a[title ~= page] 作为⼀个 css 选择器,来查找标题中有 “page” 字符的 标签。如果你在 DOM 检查器中按 ctrl-f,那么你就会发现你也可以使⽤这个css表达式作为⼀条查找语句(也可以使⽤xpath)。这样我们就可以循环查看所有的匹配项了。这是⼀个很棒的⽅法,可以⽤来检查⼀个表达式是否有效,并且表达式⾜够明确不会在不⼩⼼中匹配到其他的标签。我们的页⾯链接选择器同时满⾜了这两个条件。

为了讲解我们的爬⾍是怎样发现其他页⾯的,我们在 ZipruSpider 类中添加⼀个 parse(response) ⽅法,就像下⾯这样:

Python

1def parse(self, response):

2 # proceed to other pages of the listings

3 for page_url in response.css('a[title ~= page]::attr(href)').extract():4 page_url = response.urljoin(page_url)

5 yield scrapy.Request(url=page_url, callback=self.parse)

当我们开始爬取的时候,我们添加到 start_urls 中的链接将会被⾃动获取到,响应内容会被传递到 parse(response) ⽅法中。之后我们的代码就会找到所有指向其他页⾯的链接,并且产⽣新的请求对象,这些请求对象将使⽤同⼀个 parse(response) 作为回调函数。这些请求将被转化成响应对象,只要 url 仍然产⽣,响应就会持续地返回到 parse(response)函数(感谢去重器)。

我们的爬⾍已经可以找到了页⾯中列出的所有不同的页⾯,并且对它们发出了请求,但我们仍然需要提取⼀些对爬⾍来说有⽤的数据。torrent 列表位于

标签之内,并且有属性 class=\"list2at\" ,每个单独的 torrent 都位于带有属性 class=\"lista2\" 的 标签,其中的每⼀⾏都包含 8 个
标签,分别与 “类别”,“⽂件”,“添加时间”,“⽂件⼤⼩”,“保种的⼈”,“下载⽂件的⼈”,“⽂件描述”,和“上传者”相对应。在代码中查看其它的细节可能是最简单的⽅法,下⾯是我们修改后的 parse(response) ⽅法:

Python

1def parse(self, response):

2 # proceed to other pages of the listings

3 for page_url in response.xpath('//a[contains(@title, \"page \")]/@href').extract():4 page_url = response.urljoin(page_url)

5 yield scrapy.Request(url=page_url, callback=self.parse)6

7 # extract the torrent items

8 for tr in response.css('table.lista2t tr.lista2'):9 tds = tr.css('td')

10 link = tds[1].css('a')[0]11 yield {

12 'title' : link.css('::attr(title)').extract_first(),

13 'url' : response.urljoin(link.css('::attr(href)').extract_first()),14 'date' : tds[2].css('::text').extract_first(),15 'size' : tds[3].css('::text').extract_first(),

16 'seeders': int(tds[4].css('::text').extract_first()),

17 'leechers': int(tds[5].css('::text').extract_first()),18 'uploader': tds[7].css('::text').extract_first(),19 }

我们的 parse(response) ⽅法现在能够返回字典类型的数据,并且根据它们的类型⾃动区分请求。每个字典都会被解释为⼀项,并且作为爬⾍数据输出的⼀部分。

如果我们只是爬取⼤多数常见的⽹站,那我们已经完成了。我们只需要使⽤下⾯的命令来运⾏:

Python

1scrapy crawl zipru -o torrents.jl

⼏分钟之后我们本应该得到⼀个 [JSON Lines] 格式 torrents.jl ⽂件,⾥⾯有我们所有的torrent 数据。取⽽代之的是我们得到下⾯的错误信息(和⼀⼤堆其他的东西):

Python

[scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)1

[scrapy.extensions.telnet] DEBUG: Telnet console listening on 127.0.0.1:60232

[scrapy.core.engine] DEBUG: Crawled (403) (referer: None) ['partial']3

[scrapy.core.engine] DEBUG: Crawled (403) (referer: None) ['partial']4

[scrapy.spidermiddlewares.httperror] INFO: Ignoring response <403 http://zipru.to/torrents.php?category=TV>: HTTP status code is not5

handled or not allowed6

[scrapy.core.engine] INFO: Closing spider (finished)我好⽓啊!我们现在必须变得更聪明来获得我们完全可以从公共API得到的数据,因为上⾯的代码永远都⽆法爬取到那些数据。

简单的问题

我们的第⼀个请求返回了⼀个 403 响应,所以这个url被爬⾍忽略掉了,然后⼀切都关闭了,因为我们只给爬⾍提供了⼀个 url 链接。同样的请求在⽹页浏览器⾥运⾏正常,即使是在没有会话(session)历史的隐匿模式也可以,所以这⼀定是由于两者请求头信息的差异造成的。我们可以使⽤ 来⽐较这两个请求的头信息,但其实有个常见错误,所以我们应该⾸先检查: user agent 。

Scrapy 默认把 user-agent 设置为 “Scrapy/1.3.3 “,⼀些服务器可能会屏蔽这样的请求,甚⾄使⽤⽩名单只允许少量的user agent 通过。你可以在线查看 ,使⽤其中任何⼀个通常就⾜以绕过基本反爬⾍策略。选择⼀个你最喜欢的 User-agent ,然后打开 zipru_scraper/settings.py ,替换 User agent

Python

1# Crawl responsibly by identifying yourself (and your website) on the user-agent2#USER_AGENT = 'zipru_scraper (+http://www.yourdomain.com)'使⽤下⾯内容替换 USER_AGENT :

Python

1

USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95Safari/537.36'

你可能注意到了,默认的scrapy设置中有⼀些令爬⾍蒙羞的事。关于这个问题的观点众说纷纭,但是我个⼈认为假如你想让爬⾍表现的像是⼀个⼈在使⽤普通的⽹页浏览器,那么你就应该把你的爬⾍设置地像普通的⽹络浏览器那样。所以让我们⼀起添加下⾯的设置来降低⼀下爬⾍响应速度:

Python

1CONCURRENT_REQUESTS = 12DOWNLOAD_DELAY = 5

通过 ,上⾯的设置会创建⼀个稍微真实⼀点的浏览模式。我们的爬⾍在默认情况下会遵守 robots.txt,所以现在我们的⾏为⾮常检点。现在使⽤ scrapy crawl zipru -o torrents.jl 命令再次运⾏爬⾍,应该会产⽣下⾯的输出:

Python

[scrapy.core.engine] DEBUG: Crawled (200) (referer: None)1

[scrapy.downloadermiddlewares.redirect] DEBUG: Redirecting (302) to http://zipru.to/threat_defense.php?defense=1&r=78213556> from 3

[scrapy.core.engine] DEBUG: Crawled (200) (referer: None) ['partial']4

[scrapy.core.engine] INFO: Closing spider (finished)这是⼀个巨⼤的进步!我们获得了两个 200 状态码和⼀个 302状态码,下载中间件知道如何处理 302 状态码。不幸的是,这个 302 将我们的请求重定向到了⼀个看起来不太吉利的页⾯ threat_defense.php。不出所料,爬⾍没有发现任何有⽤的东西,然后爬⾍就停⽌运⾏了。

注: 假如⽹站检测到你的爬⾍,那么⽹站就会把你的请求重定向到 threat_defense.php 页⾯,使你的爬⾍失效,⽤来防⽌频繁的爬⾍请求影响了⽹站正常⽤户的使⽤。

下载中间件

在我们深⼊研究我们⽬前所⾯临的更复杂的问题之前,先了解⼀下请求和响应在爬⾍中是怎样被处理的,将会很有帮助。当我们创建了我们基本的爬⾍,我们⽣成了⼀个 scrapy.Request 对象,然后这些请求会以某种⽅法转化为与服务器的响应相对应的 scrapy.Response对象。这⾥的“某种⽅法” 很⼤⼀部分是来⾃于下载中间件。

下载中间件继承⾃ scrapy.downloadermiddlewares.DownloaderMiddleware 类并且实现了 process_request(request, spider) 和 process_response(request,

response, spider) ⽅法。你⼤概可以从他们的名字中猜到他们是做什么的。实际上这⾥有⼀⼤堆的默认开启的中间件。下⾯是标准的中间件配置(你当然可以禁⽤、添加或是重新设置这些选项):

Python 12345678

DOWNLOADER_MIDDLEWARES_BASE = {

'scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware': 100, 'scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware': 300,

'scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware': 350, 'scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware': 400, 'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': 500, 'scrapy.downloadermiddlewares.retry.RetryMiddleware': 550,

'scrapy.downloadermiddlewares.ajaxcrawl.AjaxCrawlMiddleware': 560,

9 'scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware': 580,

10 'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 590,11 'scrapy.downloadermiddlewares.redirect.RedirectMiddleware': 600,12 'scrapy.downloadermiddlewares.cookies.CookiesMiddleware': 700,13 'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 750,14 'scrapy.downloadermiddlewares.stats.DownloaderStats': 850,

15 'scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware': 900,16}

当⼀个请求到达服务器时,他们会通过每个这些中间件的 process_request(request, spider) ⽅法。 这是按照数字顺序发⽣

的,RobotsTxtMiddleware 中间件⾸先产⽣请求,并且 HttpCacheMiddleware 中间件最后产⽣请求。⼀旦接收到⼀个响应,它就会通过任何已启⽤的中间件的 process_response(request,response,spider) ⽅法来返回响应。这次是以相反的顺序发⽣的,所以数字越⾼越先发送到服务器,数字越低越先被爬⾍获取到。

⼀个特别简单的中间件是 CookiesMiddleware。它简单地检查响应中请求头的 Set-Cookie,并且保存 cookie 。然后当响应返回的时候,他们会适当地设置 Cookie 请求头标记,这样这些标记就会被包含在发出的请求中了。由于时间太久的原因要⽐我们说的要稍微复杂些,但你会明⽩的。

另⼀个相对基本的就是 RedirectMiddleware 中间件,它是⽤来处理 3XX 重定向的。它让⼀切不是 3XX 状态码的响应都能够成功的通过,但假如响应中还有重定向发⽣会怎样? 唯⼀能够弄清楚服务器如何响应重定向URL的⽅法就是创建⼀个新的请求,⽽且这个中间件就是这么做的。当 process_response(request, response, spider) ⽅法返回⼀个请求对象⽽不是响应对象的时候,那么当前响应就会被丢弃,⼀切都会从新的请求开始。这就是 RedirectMiddleware 中间件怎样处理重定向的,这个功能我们稍后会⽤到。

如果你对于有那么多的中间件默认是开启的感到惊讶的话,那么你可能有兴趣看看 。实际上同时还有很多其他的事情在进⾏,但是,再说⼀次,scrapy的最⼤优点之⼀就是你不需要知道它的⼤部分原理。你甚⾄不需要知道下载中间件的存在,却能写⼀个实⽤的爬⾍,你不必知道其他部分就可以写⼀个实⽤的下载中间件。

困难的问题

回到我们的爬⾍上来,我们发现我们被重定向到某个 threat_defense.php?defense=1&... URL上,⽽不是我们要找的页⾯。当我们在浏览器⾥⾯访问这个页⾯的时候,我们看到下⾯的东西停留了⼏秒:

在被重定向到 threat_defense.php?defense=2&... 页⾯之前,会出现像下⾯的提⽰:

看看第⼀个页⾯的源代码就会发现,有⼀些 javascript 代码负责构造⼀个特殊的重定向URL,并且构造浏览器的cookies。如果我们想要完成这个任务,那我们就必须同时解决上⾯这两个问题。

接下来,当然我们也需要解决验证码并提交答案。如果我们碰巧弄错了,那么我们有时会被重定向到另⼀个验证码页⾯,或者我们会在类似于下⾯的页⾯上结束访问:

在上⾯的页⾯中,我们需要点击 “Click here” 链接来开始整个重定向的循环,⼩菜⼀碟,对吧?

我们所有的问题都源于最开始的 302 重定向,因此处理它们的⽅法⾃然⽽然应该是做⼀个⾃定义的 。我们想让我们的中间件在所有情况下都像是正常重定向中间件⼀样,除⾮有⼀个 302 状态码并且请求被重定向到 threat_defense.php 页⾯。当它遇到特殊的 302 状态码时,我们希望它

能够绕过所有的防御机制,把访问cookie添加到 session 会话中,最后重新请求原来的页⾯。如果我们能够做到这⼀点,那么我们的Spider类就不必知道这些事情,因为请求会全部成功。

打开 zipru_scraper/middlewares.py ⽂件,并且把内容替换成下⾯的代码:

Python

1import os, tempfile, time, sys, logging2logger = logging.getLogger(__name__)3

4import dryscrape5import pytesseract6from PIL import Image7

8from scrapy.downloadermiddlewares.redirect import RedirectMiddleware9

10class ThreatDefenceRedirectMiddleware(RedirectMiddleware):11 def _redirect(self, redirected, request, spider, reason):12 # act normally if this isn't a threat defense redirect13 if not self.is_threat_defense_url(redirected.url):

14 return super()._redirect(redirected, request, spider, reason)15

16 logger.debug(f'Zipru threat defense triggered for {request.url}')17 request.cookies = self.bypass_threat_defense(redirected.url)

18 request.dont_filter = True # prevents the original link being marked a dupe19 return request20

21 def is_threat_defense_url(self, url):

22 return '://zipru.to/threat_defense.php' in url

你可能注意到我们继承了 RedirectMiddleware 类,⽽不是直接继承 DownloaderMiddleware 类。这样就允许我们重⽤⼤部分的重定向处理函数,并且把我们的代码插⼊到 _redirect(redirected, request, spider, reason) 函数中,⼀旦有重定向的请求被创建,process_response(request, response, spider) 函数就会调⽤这个函数。我们只是把对于普通的重定向的处理推迟到⽗类进⾏处理,但是对于特殊的威胁防御重定向的处理是不⼀样的。我们到⽬前为⽌还没有实现 bypass_threat_defense(url) ⽅法,但是我们可以知道它应该返回访问cookies,并把它附加到原来的请求中,然后原来的请求将被重新处理。

为了开启我们新的中间件,我们需要把下⾯的内容添加到 zipru_scraper/settings.py中:

Python

1DOWNLOADER_MIDDLEWARES = {

2 'scrapy.downloadermiddlewares.redirect.RedirectMiddleware': None,3 'zipru_scraper.middlewares.ThreatDefenceRedirectMiddleware': 600,4}

这会禁⽤默认的重定向中间件,并且把我们的中间件添加在中间件堆栈中和默认重定向中间件相同的位置。我们必须安装⼀些额外的包,虽然我们现在没有⽤到,但是稍后我们会导⼊它们:

Python

1pip install dryscrape # headless webkit2pip install Pillow # image processing

3pip install pytesseract # OCR

请注意,这三个包都有 pip ⽆法处理的外部依赖,如果你运⾏出错,那么你可能需要访问 , , 和 的安装教程,遵循平台的具体说明来解决。

我们的中间件现在应该能够替代原来的标准重定向中间件,现在我们只需要实现 bypass_thread_defense(url) ⽅法。我们可以解析 javascript 代码来得到我们需要的变量,然后⽤ python 重建逻辑,但这看起来很不牢靠,⽽且需要⼤量的⼯作。让我们采⽤更简单的⽅法,尽管可能还是⽐较笨重,使⽤⽆头的 webkit 实例。有⼏个不同选择,但我个⼈⽐较喜欢 (我们已经在上⾯安装了)⾸先,让我们在中间件构造函数中初始化⼀个 dryscrape 会话。

Python

1def __init__(self, settings):2 super().__init__(settings)3

4 # start xvfb to support headless scraping5 if 'linux' in sys.platform:6 dryscrape.start_xvfb()7

8 self.dryscrape_session = dryscrape.Session(base_url='http://zipru.to')

你可以把这个会话对象当做是⼀个单独的浏览器标签页,它可以完成⼀切浏览器通常可以做的事情(例如:获取外部资源,执⾏脚本)。我们可以在新的标签页中打开新的 URL 链接,点击⼀些东西,或者在输⼊框中输⼊内容,或是做其他的各种事情。Scrapy ⽀持并发请求和多项处理,但是响应的处理是单线程的。这意味着我们可以使⽤这个单独的 dryscrapy 会话,⽽不必担⼼线程安全。现在让我们实现绕过威胁防御机制的基本逻辑。

Python

1def bypass_threat_defense(self, url=None):

2 # only navigate if any explicit url is provided3 if url:

4 self.dryscrape_session.visit(url)5

6 # solve the captcha if there is one

7 captcha_images = self.dryscrape_session.css('img[src *= captcha]')8 if len(captcha_images) > 0:

9 return self.solve_captcha(captcha_images[0])10

11 # click on any explicit retry links

12 retry_links = self.dryscrape_session.css('a[href *= threat_defense]')13 if len(retry_links) > 0:

14 return self.bypass_threat_defense(retry_links[0].get_attr('href'))15

16 # otherwise, we're on a redirect page so wait for the redirect and try again17 self.wait_for_redirect()

18 return self.bypass_threat_defense()19

20 def wait_for_redirect(self, url = None, wait = 0.1, timeout=10):21 url = url or self.dryscrape_session.url()22 for i in range(int(timeout//wait)):23 time.sleep(wait)

24 if self.dryscrape_session.url() != url:25 return self.dryscrape_session.url()

26 logger.error(f'Maybe {self.dryscrape_session.url()} isn\\'t a redirect URL?')

27 raise Exception('Timed out on the zipru redirect page.')

这样就处理了我们在浏览器中遇到的所有不同的情况,并且完全符合⼈类在每种情况中的⾏为。在任何给定情况下采取的措施都取决于当前页⾯的情况,所以这种⽅法可以稍微优雅⼀点地处理各种不同的情况。

最后⼀个难题是如果如何解决验证码。⽹上提供了 服务,你可以在必要时使⽤它的API,但是这次的这些验证码⾮常简单,我们只⽤ OCR就可以解决它。使⽤ pytessertact 的 OCR 功能,最后我们可以添加 solve_captcha(img) 函数,这样就完善了 bypass_threat_defense() 函数。

Python

1def solve_captcha(self, img, width=1280, height=800):2 # take a screenshot of the page

3 self.dryscrape_session.set_viewport_size(width, height)4 filename = tempfile.mktemp('.png')

5 self.dryscrape_session.render(filename, width, height)6

7 # inject javascript to find the bounds of the captcha

8 js = 'document.querySelector(\"img[src *= captcha]\").getBoundingClientRect()'9 rect = self.dryscrape_session.eval_script(js)

10 box = (int(rect['left']), int(rect['top']), int(rect['right']), int(rect['bottom']))11

12 # solve the captcha in the screenshot13 image = Image.open(filename)14 os.unlink(filename)

15 captcha_image = image.crop(box)

16 captcha = pytesseract.image_to_string(captcha_image)17 logger.debug(f'Solved the Zipru captcha: \"{captcha}\"')18

19 # submit the captcha

20 input = self.dryscrape_session.xpath('//input[@id = \"solve_string\"]')[0]21 input.set(captcha)

22 button = self.dryscrape_session.xpath('//button[@id = \"button_submit\"]')[0]23 url = self.dryscrape_session.url()24 button.click()25

26 # try again if it we redirect to a threat defense URL

27 if self.is_threat_defense_url(self.wait_for_redirect(url)):28 return self.bypass_threat_defense()29

30 # otherwise return the cookies as a dict31 cookies = {}

32 for cookie_string in self.dryscrape_session.cookies():33 if 'domain=zipru.to' in cookie_string:

34 key, value = cookie_string.split(';')[0].split('=')35 cookies[key] = value36 return cookies

你可能注意到如果验证码因为某些原因识别失败的话,它就会委托给 back to the bypass_threat_defense() 函数。这样就给了我们多次识别验证码的机会,但重点是,我们会在得到正确结果之前⼀直在验证码识别过程中循环。这应该⾜够让我们的爬⾍⼯作,但是它有可能陷⼊死循环中。

Python

1[scrapy.core.engine] DEBUG: Crawled (200) (referer: None)

2[zipru_scraper.middlewares] DEBUG: Zipru threat defense triggered for http://zipru.to/torrents.php?category=TV3[zipru_scraper.middlewares] DEBUG: Solved the Zipru captcha: \"UJM39\"

4[zipru_scraper.middlewares] DEBUG: Zipru threat defense triggered for http://zipru.to/torrents.php?category=TV5[zipru_scraper.middlewares] DEBUG: Solved the Zipru captcha: \"TQ9OG\"

6[zipru_scraper.middlewares] DEBUG: Zipru threat defense triggered for http://zipru.to/torrents.php?category=TV7[zipru_scraper.middlewares] DEBUG: Solved the Zipru captcha: \"KH9A8\"8...

⾄少看起来我们的中间件已经成功地解决了验证码,然后补发了请求。问题在于,新的请求再次触发了威胁防御机制。我第⼀个想法是我可能在怎样解析或是添加cookie上⾯有错误,但是我检查了三次,代码是正确的。这是另外⼀种情况 “唯⼀可能不同的事情就是请求头” 。很明显,scrapy 和 dryscrape 的请求头都绕过了最初的触发 403 响应的过滤器,因为我们现在不会得到任何 403 的响应。这肯定是因为它们的请求头信息不⼀致导致的。我的猜测是其中⼀个加密的访问cookies包含了整个请求头信息的散列值,如果这个散列不匹配,就会触发威胁防御机制。这样的⽬的可能是防⽌有⼈把浏览器的cookie复制到爬⾍中去,但是它只是增加了你需要解决的问题⽽已。所以让我们在 zipru_scraper/settings.py 中把请求头信息修改成下⾯这个样⼦。

Python

1DEFAULT_REQUEST_HEADERS = {

2 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',3 'User-Agent': USER_AGENT,4 'Connection': 'Keep-Alive',

5 'Accept-Encoding': 'gzip, deflate',6 'Accept-Language': 'en-US,*',7}

注意我们已经把 User-Agent 头信息修改成了我们之前定义的 USER_AGENT 中去.这个⼯作是由 user agent 中间件⾃动添加进去的,但是把所有的这些配置放到⼀个地⽅可以使得 dryscrape 更容易复制请求头信息。我们可以通过修改 ThreatDefenceRedirectMiddleware 初始化函数像下⾯这样:

Python

1def __init__(self, settings):2 super().__init__(settings)3

4 # start xvfb to support headless scraping5 if 'linux' in sys.platform:6 dryscrape.start_xvfb()7

8 self.dryscrape_session = dryscrape.Session(base_url='http://zipru.to')9 for key, value in settings['DEFAULT_REQUEST_HEADERS'].items():10 # seems to be a bug with how webkit-server handles accept-encoding11 if key.lower() != 'accept-encoding':

12 self.dryscrape_session.set_header(key, value)

现在,当我们可以通过命令 scrapy crawl zipru -o torrents.jl 再次运⾏爬⾍。我们可以看到源源不断的爬取的内容,并且我们的 torrents.jl ⽂件记录把爬取的内容全部记录了下来。我们已经成功地绕过了所有的威胁防御机制。

总结

我们已经成功地写了⼀个能够解决四种截然不同的威胁防御机制的爬⾍,这四种防御机制分别是:1. User agent 过滤

2. 模糊的 Javascript 重定向3. 验证码

4. 请求头⼀致性检查

我们的⽬标⽹站 Zipru 可能是虚构的,但是这些机制都是你会在真实⽹站上遇到的真实的反爬⾍技术。希望我们使⽤的⽅法对你⾃⼰爬⾍中遇到的挑战有帮助。

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- 91gzw.com 版权所有 湘ICP备2023023988号-2

违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务