在Scrapy中集成selenium采集数据

前言

scrapy 是一个很强大的框架,但是在遇到反爬很强的网站时就无能为力了,这时候最好的解决办法就是使用 selenium 控制真实的浏览器去执行网页,然后从浏览器中获取到页面的源代码,之后进行正常的采集流程。

scrapy 是一个非阻塞框架,selenium 是一个阻塞框架,将一个阻塞框架的代码放到非阻塞框架中并不是一个好主意,但是在网站反爬实在太难时也不失为一个比较好的办法。

scrapy 官方并不支持 selenium 集成,但是可以通过第三方包的方式来实现。

scrapy 框架提供了很多可以拓展它的方式,例如 Downloader Middleware,Spider Middleware 等,这就意味着可以很方便的通过自定义中间件的方式重写或修改一些功能来达到自己的目的。

集成selenium

那么如何来集成 selenium 呢?熟悉 scrapy 框架的小伙伴可能想到了下载中间件的 process_request 方法 ,它有四种返回值:

process_request() should either: return None, return a Response object, return a Request object, or raise IgnoreRequest.

  • 可以返回 None,scrapy 会继续调用其他下载中间件继续处理请求,一般修改代理 IP 或者修改请求头就使用这种方式
  • 可以返回 Request 对象,scrapy 会停止调用其他下载中间件,遇到需要手动重定向时或者需要发送其他请求时使用
  • 可以返回 Response 对象,scrapy 会停止调用其他下载中间件,并且直接返回此 Response 对象
  • 可以引发一个异常,scrapy 会自动调用异常处理函数 process_exception 来处理,如果没有人来处理,那么请求将会被丢掉并且不进行记录

更加详细的官方文档可以看这里

以上就是在 scrapy 中集成 selenium 的原理,由于 GitHub 上已经有现成的轮子,这里就不重复造轮子了,理解它的原理有助于后面调试代码和重写下载中间件来添加功能。

scrapy-selenium安装

这里使用 scrapy-selenium 包来在 scrapy 中使用 selenium,安装方法为:

1
pip install scrapy-selenium

更详细的用法可以查看项目主页

安装好之后还不能用,还需要安装一个 driver 程序。我这里是 Chrome 浏览器,所以我需要安装 Chrome 的driver

选择和自己的 Chrome 版本匹配的 driver 下载下来解压到 Chrome 的 Application 目录中即可。

依赖包安装好之后可以开始写代码了,按照项目主页中的使用方法:

  • 先将配置项添加到 settings.py 文件中
1
2
3
4
5
from shutil import which

SELENIUM_DRIVER_NAME = 'chrome'
SELENIUM_DRIVER_EXECUTABLE_PATH = which(r'C:\Program Files (x86)\Google\Chrome\Application\chromedriver.exe')
SELENIUM_DRIVER_ARGUMENTS = [] # '--headless' if using chrome instead of firefox

为了方便调试,这里不使用无头参数。

  • 添加下载中间件到配置文件中
1
2
3
DOWNLOADER_MIDDLEWARES = {
'scrapy_selenium.SeleniumMiddleware': 800 # 这里的数值要大一些,因为中间件返回响应对象后就不会调用后续的下载中间件了,所以要保证这个中间件是最后一个被调用的
}

scrapy-selenium使用

都设置好之后就可以使用了,按照主页文档中的提示写出以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import scrapy
from scrapy_selenium import SeleniumRequest


class AntiSpider(scrapy.Spider):
name = 'antispider1'

def start_requests(self):
urls = ['https://antispider4.scrape.center/']
for a in urls:
yield SeleniumRequest(url=a, callback=self.parse)

def parse(self, response, **kwargs):
print(response.text)
input()

在 parse 方法中打印出网页源代码,查看源代码即可了解 scrapy-selenium 是否正常工作,因为 urls 中的网页数据是通过 Ajax 加载的,如果 scrapy-selenium 没有按预期工作会打印出原始代码而不是渲染好的 HTML 源码。

input 函数是为了让爬虫暂时停下来,方便查看浏览器状态,默认状态下下载队列消耗完爬虫会自动停止运行,浏览器也会一并退出,不利于调试,当然,打断点也可以达到同样的目的。

运行爬虫,查看日志输出,发现爬虫打印出的是原始代码,而不是正常渲染好的 HTML,通过观察发现在浏览器还没有加载完数据时爬虫就执行到了 parse 函数,打印了原始代码。

猜测是因为 selenium 默认在网页中的所有资源加载完后获取源码的 get 函数就返回了,而 Ajax 是在这之后才执行的,为了解决这个问题,selenium 提供了 WebDriverWait 类来实现自动等待,等某个需要的元素被加载出来 get 函数才返回。

继续查看项目文档,SeleniumRequest 类提供了 wait_time/wait_until 函数用来等待,修改代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import scrapy
from scrapy_selenium import SeleniumRequest
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC


class AntiSpider(scrapy.Spider):
name = 'antispider1'

def start_requests(self):
urls = ['https://antispider4.scrape.center/']
for a in urls:
yield SeleniumRequest(url=a, callback=self.parse, wait_time=3, wait_until=EC.presence_of_element_located((By.CLASS_NAME, 'm-b-sm')))

def parse(self, response, **kwargs):
print(response.text)
input()

重新运行代码,发现添加等待后可以正常获取所需要的数据了,继续补充其他解析代码就可以正常获取数据了。

此篇文章只讲如何集成 selenium 的集成,就不写解析代码了。

完整代码详见https://github.com/libra146/learnscrapy/tree/9773cdecf76336350201a3245653b27da3f557d5

总结

通过 GitHub 上的轮子实现了 scrapy 和 selenium 的集成,并且使用参数解决了获取不到所需要的元素的问题。

scrapy-selenium 的源码也不多,只有两个文件,还是比较简单的,感兴趣的小伙伴可以自己参照着实现下,可以加深自己对 scrapy 下载中间件的理解。

本文章首发于个人博客 LLLibra146’s blog
本文作者:LLLibra146
版权声明:本博客所有文章除特别声明外,均采用 © BY-NC-ND 许可协议。非商用转载请注明出处!严禁商业转载!
本文链接https://blog.d77.xyz/archives/17668da2.html