第 8章 Python 爬虫框架 Scrapy(下)
8.1 Scrapy 对接 Selenium
有一种反爬虫策略就是通过 JS 动态加载数据,应对这种策略的两种方法如下:
分析 Ajax 请求,找出请求接口的相关规则,直接去请求接口获取数据。
使用 Selenium 模拟浏览器渲染后抓取页面内容。
8.1.1 如何对接
单独使用 Scrapy 是无法抓取 JS 动态渲染页面的,可以利用 Scrapy 对接 Selenium 来处理 JS。对接的方法也很简单,自定义一个下载中间件,在 process_request()方法中对抓取请求进行处理,启动浏览器进行页面渲染,再将渲染后的结果构造成一个 HtmlResponse对象返回。
8.1.2 对接示例:爬取某网站首页文章
我们通过一个爬取某网站首页文章的例子来进行讲解。首先,打开middlewares.py 文件
from scrapy import signals
from selenium import webdriver
from scrapy.http import HtmlResponse
class JSSeleniumMiddleware:
def __init__(self):
self.browser = webdriver.Chrome()
def __del__(self):
self.browser.close()
def process_request(self, request, spider):
self.browser.get("https://www.jianshu.com/")
return HtmlResponse(url='https://www.jianshu.com/', body=self.browser.page_source,request=request,encoding='utf-8', status=200)
接着在 Settings.py 文件中启用这个下载中间件:
DOWNLOADER_MIDDLEWARES = {
'jianshu.middlewares.JSSeleniumMiddleware': 300,
}
然后,修改我们的爬虫文件,把 parse 中的内容打印出来:
from scrapy import Spider, Request
class JianshuSpider(Spider):
name = 'jianshu'
allowed_domains = ['www.jianshu.com']
start_urls = ['http://www.jianshu.com/']
def start_requests(self):
yield Request('https://www.jianshu.com', callback=self.parse)
def parse(self, response): self.logger.debug(response.body.decode('utf8'))
运行后,可以看到控制台打印出的部分信息如下:
<li id="note-34403329" data-note-id="34403329" class="">
<div class="content">
<a class="title" target="_blank" href="/p/1c0b5f1bf431">为什么月薪2万以上的人从不发
朋友圈?</a>
可以看到 Selenium 渲染 JS 后的页面代码都原封不动地传递过来了,接着我们编写一个JianshuspiderItem 类,以保存文章名、文章部分内容、文章链接和作者名称。
import scrapy
class JianshuItem(scrapy.Item):
title = scrapy.Field()
content = scrapy.Field()
url = scrapy.Field()
nickname = scrapy.Field()
接下来,我们用 Xpath 选择器来提取这些内容,修改爬虫部分代码:
from scrapy import Spider, Request
from ..items import JianshuItem
class JianSpider(Spider):
name = "jian"
allowed_domains = ["www.jianshu.com"]
start_urls = ["https://www.jianshu.com"]
def start_requests(self):
yield Request('https://www.jianshu.com', callback=self.parse)
def parse(self, response):
li_s = response.xpath('//ul[@class="note-list"]/li')
for li in li_s:
item = JianshuItem()
item['title'] = li.xpath('.//div/a[@class="title"]/text()').extract_first()
item['content'] = str(li.xpath('.//div/p[@class="abstract"]/text()').extract_first()).replace(" ", "").replace("\n", "")
item['url'] = 'https://www.jianshu.com/p/' + str(li.xpath('.//div/a[@class="title"]/@href').extract_first())
item['nickname'] = li.xpath('.//div/a[@class="nickname"]/text()').extract_first()
self.logger.info(str(item))
部分输出结果如下:
[jian] INFO: {'content': '色彩,是地球母亲赋予我们感知大自然最美、最动人的无声语言!色彩不仅和艺术相关,也跟心理学同样有紧密的联系。近年来,心理学领域对色彩的研究主要有...',
'nickname': '疗愈师_茹茵',
'title': '如何通过色彩来看到当下的课题?',
'url': 'https://www.jianshu.com/p//p/0c88d5cdbfed'}
2024-02-13 19:35:00 [jian] INFO: {'content': '影响因子:8.44关于非肿瘤生信,我们也解读过很多,主要有以下类型1单个疾病WGCNA+PPI分析筛选hub基因。2单个疾病结合免疫浸润...',
'nickname': '生信小课堂',
'title': '8.4分非肿瘤生信,免疫浸润+机器学习+动物模型验证。可谓是非肿瘤生信结合实验验证的范文,值得收藏!',
'url': 'https://www.jianshu.com/p//p/f3210edd252e'}
数据提取完成后需要 Item Pipeline 类,将解析的结果保存到 MongoDB 中。打开pipelines.py 文件,自定义一个 MongoPipeline 类,代码如下:
import pymongo
class MongoPipeline(object):
def open_spider(self, spider):
self.client = pymongo.MongoClient(host='localhost', port=27017)
self.db = self.client['js']
def process_item(self, item, spider):
self.db['index_article'].insert(dict(item))
def close_spider(self, spider):
self.client.close()
接着在 Settings.py 文件中启用 MongoPipeline 类,修改后的配置如下:
ITEM_PIPELINES = {
'jianshu.pipelines.MongoPipeline': 300,
}
把 Spider 解析部分代码的self.logger.info(str(item))修改为 yield item,运行爬虫后,可以看到 MongoDB 中已经保存了文章的相关信息.
另外,每次运行爬虫都会弹出 Chorme 浏览器,有时可能需要把脚本挂到云服务器上,而服务器上是无页面的,这时可以采用 Chrome 的 Headless 模式(Chrome 69 以上版本)。核心代码如下:
chrome_options = Options()
chrome_options.add_argument('--headless')
chrome_options.add_argument('--disable-gpu')
browser = webdriver.Chrome(chrome_options=chrome_options)
8.2 实战:用 Scrapy 实现一个简单的代理池
在爬取站点时,使用代理 IP 是一种非常有效的反反爬虫策略,而代理 IP 的获取又分为免费和收费方式。免费的代理 IP 可以从一些代理站点(如西刺代理等)获取,但是免费的代理 IP 质量非常低,大部分是不能使用的,能使用的代理 IP 寿命
极短,可能几分钟后就不能用了。而收费的代理IP 的价格也不同,高质量的收费代理 IP价格都比较高。
8.2.1 代理池的设计
在开始实现代理池前,我们需要两个核心程序。
- IP 获取程序:爬取免费代理 IP 站点,解析获得代理 IP,保存到 Redis 数据库中。
- IP 检测程序:从 Redis 数据库中取出所有代理 IP,访问目标站点看是否可用,不可用的删除。
接着我们要考虑一个问题:IP 获取程序和 IP 检测程序什么时候运行。
如果等我们爬取站点时才去执行的话,效率会非常低,毕竟爬取和检测是一个耗时的过程,对此我们需要另外编写一个调度程序,来调度这两个程序的执行。
还有一个问题是:如何制订调度策略。因为免费代理 IP 的存活期比较短,所以检测 IP可用的程序执行的频度需要高一些,比如每隔 20 秒执行一次,验证代理池里的 IP 是否可用,及时移除失效的代理 IP。
接着是爬取 IP 的程序,当代理池中的可用 IP 少于 10 个时,就会执行一次爬取 IP 的程序。另外,要设置一个保护期,当爬取完 IP 时,在一个时间段(10 分钟)里,不再触发IP 获取的爬虫,除非代理池中已无可用代理,以避免调用过于频繁被免费代理站点 block。
最后就是校验代理 IP 是否可用的方法,即直接访问测试页面,返回码不是 200 或出现超时等异常,则认为此代理 IP 为不可用代理 IP,从代理池中移除。
案例:
https://zxbcw.cn/post/196763/
8.2.2 编写获取代理 IP 的接口
Flask 是一个轻量级的 Python Web 库,安装非常简单,直接通过 pip 命令安装即可,命令如下:
pip install flask
安装后,我们在爬虫项目中新建一个proxy_server.py 文件,创建一个最简单的 Flask实例,代码如下:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def fetch_ip():
return "返回一个代理IP"
if __name__ == '__main__':
app.run()
运行后可以看到控制台输出如下信息:
Serving Flask app 'proxy_server'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
运行后,用浏览器访问http://127.0.0.1:5000/,
8.3 用 Scrapyrt 调度 Scrapy
我们现在都是通过命令行输入“scrapy crawl 爬虫名字”来启动一个 Scrapy 爬虫,如果要使爬虫在云服务器上运行,每次都需要先用 SSH 连接服务器,然后启动爬虫,显得非常烦琐,有没有办法来简化这个步骤呢,比如只要通过一个 HTTP 接口就可以完成 Scrapy 的任务调度。
常用的 Scrapy HTTP 接口调度库有两个:Scrapyrt 和 Scrapyrd。前者是轻量级的,如果不需要分布式多任务,可以直接使用 Scrapyrt 来实现 Scrapy 的远程调度。
8.4 用 Docker 部署 Scrapy
8.4.1 Docker 简介
Docker 是基于 Linux 容器的封装,提供了简单易用的容器使用接口。而 Linux 容器是一种虚拟化技术,不是模拟一个完整的系统,而是对进程进行隔离(在进程外嵌套一层),使得进程访问到的各种资源都是虚拟的,从而达到与底层系统隔离的目的。可以简单地将它理解成更轻量级的虚拟机。另外,因为容器是进程级别的,相比虚拟机而言,启动速度更快,资源占用更少。
8.4.2 下载并安装 Docker
Ubuntu 环境
- 更新软件包列表: 执行以下命令更新系统软件包列表:
sudo apt update
- 安装依赖包: 安装 Docker 所需的依赖包,以及使用 HTTPS 通过 APT 下载软件包所需的工具:
sudo apt install apt-transport-https ca-certificates curl software-properties-common
- 添加 Docker 官方 GPG 密钥: 添加 Docker 官方 GPG 密钥以确保下载的软件包是由 Docker 官方签名的
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
- 添加 Docker 存储库: 添加 Docker 的官方存储库以获取最新版本的 Docker:
- 对于 x86_64/AMD64 架构的计算机:
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
- 对于 ARM64 架构的计算机:1. 更新软件包列表: 执行以下命令更新系统软件包列表:
echo "deb [arch=arm64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
- 安装 Docker Engine: 更新软件包列表后,安装 Docker Engine
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io
- 启动 Docker 服务: 安装完成后,启动 Docker 服务:
sudo systemctl start docker
- 设置 Docker 开机自启动: 如果你希望 Docker 在系统启动时自动启动,可以执行以下命令:
sudo systemctl enable docker
- 验证安装: 运行以下命令来验证 Docker 是否已成功安装:
sudo docker run hello-world
8.4.3 创建 Dockerfile
我们通过编写 Dockerfile 文件来定制我们的 Docker 镜像,流程如下。
- 导出项目依赖的 Python 环境包
可以使用 pipreqs 库导出项目依赖的版本号,通过 pip 命令进行安装:
pip install pipreqs
安装后命令行在爬虫目录下,输入下述命令生成 requirements.txt 文件:
pipreqs ./
另外有一点要注意,Windows 系统执行上述操作会报编码错误(UnicodeDecodeError: 'gbk' codec can't decode byte 0xa8 in position 24: illegal multibyte sequence),在命令后指定编码格式即可:
pipreqs ./ --encoding=utf8
运行成功后稍等片刻,输出如下信息,代表导出完成:
INFO: Successfully saved requirements file in ./requirements.txt
如果想导出整个系统的依赖库,可以使用下述命令:
pip freeze > requirements.txt
- 编写 Dockerfile 文件
在项目根目录下创建 Dockerfile 文件,切记没有后缀,内容如下:
FROM python:3.6
ENV PATH /usr/local/bin:$PATH
ADD . /code
WORKDIR /code
RUN pip3 install -r requirements.txt
CMD scrapy crawl BingWallpaper
介绍这六行代码的作用
- FROM:使用 Docker 基础镜像,在此基础上运行 Scrapy 项目。
- ENV:环境变量配置,将/usr/local/bin:$PATH 赋值给 PATH。
- ADD:将本地代码放置到虚拟容器中,第一个参数“.”代表本地当前路径;第二个参数“/code”代表虚拟容器中的路径,就是把项目中的所有内容放到虚拟容器的/code 目录下。
- WORKDIR:指定工作目录。
- RUN:执行某些命令来做一些准备工作,如安装库。
- CMD:容器启动命令,当容器运行时,会执行此命令,这里我们通过命令来启动爬虫。
8.4.4 构建 Docker 镜像
配置完后,执行下述命令来构建 Docker 镜像:
docker build -t bing:latest .
执行过程中部分输出如下:
Sending build context to Docker daemon 5.979MB
Step 1/6 : FROM python:3.6
---> 7a35f2e8feff
Step 2/6 : ENV PATH /usr/local/bin:$PATH
---> Using cache
---> c5a031f0b181
Step 3/6 : ADD . /code
---> 1315d8135233
Step 4/6 : WORKDIR /code
---> Running in ef69980444b1
Removing intermediate container ef69980444b1
---> 735a489cd4e8
Step 5/6 : RUN pip3 install -r requirements.txt
---> Running in e81518234d70
...
Successfully built 24df73bf5460
Successfully tagged quotes:latest
至此 Docker 镜像构建完成,可以通过命令查看构建的镜像信息:
REPOSITORY TAG IMAGE ID CREATED SIZE
bing latest 70b57b1cfcb9 12 seconds ago 1GB
我们可以试着本地运行这个 Docker 镜像,输入下述命令:
docker run quotes
8.4.5 把生成的 Docker 镜像推送到 Docker Hub
镜像构建完成后,我们可以把它推送到 Docker 镜像托管平台,镜像就可以在远程服务器上运行了,打开 Docker Hub 官网(https://hub.docker.com/) ,单击 Create Repository 创建一个镜像,命名为 bing。
在本地为我们的镜像打一个标签,命令示例如下:
docker tag bing:latest coderpig/bing:latest
接着使用下述命令把镜像推送到 Docker Hub 上:
docker push coderpig/bing
等待推送完毕,单击 Tags 选项卡可以看到刚上传的镜像。
8.4.6 在云服务器上运行 Docker 镜像
镜像上传完了,接着我们在云服务器上下载这个镜像并启动,输入下述命令:
docker run coderpig/bing
运行后可以看到,不需要配置 Python 环境处理版本冲突等问题,下载完成后完成爬取。
以上就是使用 Docker 部署 Scrapy 的基本方法。
使用此类脚本下载网站内容时应遵守网站的使用条款,以及相关的法律法规。
本系列文章皆做为学习使用,勿商用。