引言:
Scrapy
是Python开发的一个快速、高层次的屏幕抓取和web抓取框架,用于抓取web站点并从页面中提取结构化的数据。Scrapy用途广泛,可以用于数据挖掘、监测和自动化测试。
一、Scrapy架构的来源与详解
首先,框架的作用本质是将一堆轮子融合在一起,可以方便快速的解决一类问题。
最初,我们在入门学习爬虫使用requests
库时,一套基本的爬取网页内容流程如下:
基本流程伪代码如下:
import requests
from lxml import etree
// 1.URL队列
urls = [
'https://aaai.org/ocs/index.php/AAAI/AAAI18/paper/viewPaper/16488',
'https://aaai.org/ocs/index.php/AAAI/AAAI18/paper/viewPaper/16583',
...
]
data = []
for url in urls:
// 2.发送请求
response = requests.get(url)
html = response.content // 获取网页内容(content返回的是bytes型数据,text()获取的是Unicode型数据)
// 3.内容提取
title = etree.HTML(html).xpath('//*[@id="title"]/text()')
// 4.数据队列
data.append(title)
// 保存数据.....
而以上流程基本在爬取过程中是固定不变的,因此诞生了Scrapy爬虫框架,用来整合上面的每一步骤,用户无需编写每一步的代码,仅需要改写需要自定义的模块即可。下图为Scrapy架构图:
【注意】:于上图基本流程不同的是,从爬虫➡️调度器,以及调度器➡️下载器不再以URL
传递,而是以封装好的Requests
请求传递。
下面为Scrapy各个组件的功能介绍:
组件 | 功能 | 是否实现 |
---|---|---|
Scrapy Engine(引擎) | 总指挥:负责不同模块间数据的传递 | 已实现 |
Scheduler(调度器) | 一个存放引擎发过来的request请求 的队列 | 已实现 |
Downloader(下载器) | 下载从引擎发过来的requests请求 ,并返回给引擎 | 已实现 |
Downloader Middlewares (下载中间件) | 可自定义的下载扩展,比如设置代理 | 可选改写 |
Spider(爬虫) | 处理引擎发来的response :1⃣️提取数据,交给引擎传输到管道进行处理和存储 2⃣️提取URL,组装成 requests 请求并从引擎传输到调度器 | 需手写 |
Spider Middlewares (爬虫中间件) | 可自定义requests请求 和进行response过滤 | 可选改写 |
Item Pipeline(管道) | 处理引擎传过来的数据,比如存储 | 需手写 |
【注意】:使用Scrapy爬虫框架,可提升可靠性,低耦合
,因为以上模块相关且独立,若其中一个模块出错,则不影响其他模块的运行。
二、Scrapy模块的安装与初始配置
2.1 安装Scrapy
$ pip install scrapy
2.2 创建Scrapy项目
$ scrapy startproject <projectName>
创建成功后根据终端提示进入项目目录:
$ cd <projectName>
2.3 生成Scrapy爬虫
$ scrapy genspider <spiderName> <domin>
以上命令需要两个参数,分别是自定义的爬虫名(即新建.py爬虫文件的名字),以及要爬取网址的域名。
例如要爬取百度中的某一页面就可以写成如下格式:
$ scrapy genspider baidu baidu.com
2.4. 执行Scrapy爬虫
$ scrapy crawl <spiderName>
2.5. 使用Scrapy Shell
在未启动Scrapy爬虫的情况下,可以使用shell
进行debug
和测试:
$ scrapy shell <爬取网站网址>
2.6 修改Scrapy项目settings.py文件
在编写爬虫代码前,我们需要先修改配置文件:
如上图,我们需要设置USER_AGENT
为自己浏览器的代理,表示我们模拟浏览器登录。然后我们进行如下设置:
ROBOTSTXT_OBEY = False # 不遵循爬虫协议
LOG_LEVEL = 'WARN' # 设置日志级别为'WARN',则仅打印警告以上级别的日志
日志的四个级别优先级由高到低分别是:
ERROR
WARN
INFO
DEBUG
同时,还有解除ITEM_PIPELINES
注释,如下图:
该配置项表示可以使用管道,后面的值表示距离,即优先级,值越小,管道距离越近,优先执行距离近的管道。如下,优先执行MyspiderPipeline1
,然后在执行MyspiderPipeline
。
ITEM_PIPELINES = {
'mySpider.pipelines.MyspiderPipeline': 300,
'mySpider.pipelines.MyspiderPipeline1': 299,
}
以下通过文件树列出创建后的scrapy项目文件,并注释了主要文件的功能:
$ tree
.
├── mySpider
│ ├── __init__.py
│ ├── __pycache__
│ ├── items.py # 自定义需要爬取的内容
│ ├── middlewares.py # 下载中间件+爬虫中间件,可自定义
│ ├── pipelines.py # 管道,用于数据保存
│ ├── settings.py # 设置文件,如代理、日志级别等
│ └── spiders
│ ├── __init__.py
│ ├── __pycache__
│ └── itcast.py # 生成的爬虫文件
└── scrapy.cfg
三、基本代码入门
在Scrapy框架中,我们至少需要修改爬虫和管道文件以实现最基本的爬虫。
3.1 编辑爬虫文件
在上一步生成Scrapy爬虫后可在新建项目的spiders
文件夹下看到爬虫文件,如下图。此时,需要将start_urls
修改为自己最开始要爬取的页面URL
,并在parse
函数中编写页面解析代码,此处使用response
自带的XPath
方法选取节点。[不懂XPath的用法见此]
由于xpath
返回的文本内容是Selector
特殊列表,因此我们还需要使用extract()
或extract_first()
函数提取数据文本内容,具体代码如下:
class ItcastSpider(scrapy.Spider):
name = 'itcast' # 爬虫名
allowed_domains = ['itcast.cn'] # 允许爬取的范围
start_urls = ['http://www.itcast.cn/channel/teacher.shtml'] # 最开始请求的URL地址
def parse(self, response): # 数据提取方法,接收来自下载中间件传来的response
// 分组
li_list = response.xpath('//div[@class="tea_con"]//li') # xpath返回含有selector对象的列表
for li in li_list:
item = {}
item['name'] = li.xpath('.//h3/text()').extract()[0]
item['title'] = li.xpath('.//h40/text()').extract_first()
yield item
此处,笔者强烈建议使用extract_first()
函数,其等价于extract()[0]
,主要优点是如果提取出空字符串,则返回None
值而不是空列表,这里使得我们无需再判断是否为空!
最后使用关键字yield
进行中断返回值,相比return
优点在于,借用生成器特点逐个返回值,从而减少内存占用。
此处返回值类型必须如下,如果是列表则会报错:
返回值类型 | Request(向调度器传) | BaseItem | dict(向管道传) | None |
---|
3.2 编辑管道文件
管道文件是pipelines.py
,此处使用MongoDB
保存爬取的数据[Python安装并操作MongoDB见此]:
// pipelines.py
from pymongo import MongoClient
client = MongoClient('127.0.0.1', 27017)
db = client.LaGou
col = db.info
class LagouPipeline(object):
def process_item(self, item, spider):
col.insert_many([item])
return item
以上便可实现最基本的爬取数据入库的流程,方便理解,以下将进行完整爬虫项目。
四、实战Scrapy之阳光政务平台爬虫项目
【项目描述】:爬取「东莞阳光热线问政平台」中的投诉标题、申诉部门、处理状态、发布时间、详情页面中的投诉文本内容以及图片。
【网站地址】:东莞阳光热线问政平台
【完整项目代码】:我的GitHub.
【爬取结果】:
以下为本项目的主要模块代码,已在必要的地方做了详细注释:
4.1 爬虫文件
// spiders/yg.py
import scrapy
import random
from YangGuang.items import YangguangItem
class YgSpider(scrapy.Spider):
name = 'yg'
allowed_domains = ['sun0769.com']
start_urls = ['http://wz.sun0769.com/index.php/question/questionType?type=4&page=0']
def parse(self, response):
'''首页解析函数'''
tr_list = response.xpath('//div[@class="greyframe"]/table[2]//tr')
headers = {'User-Agent': self.get_ua()}
for tr in tr_list:
item = YangguangItem()
item['title'] = tr.xpath('.//a/@title').extract_first()
item['department'] = tr.xpath('.//a[@class="t12h"]/text()').extract_first()
item['status'] = tr.xpath('.//td[3]/span/text()').extract_first()
item['publish_date'] = tr.xpath('.//td[last()]/text()').extract_first()
item['href'] = tr.xpath('.//a[@class="news14"]/@href').extract_first()
yield scrapy.Request(
url=item['href'],
callback=self.parse_detail, # 指定传入的URL由parse_detail解析函数处理
meta={'item':item}, # 向parse_detail传递的元数据
headers=headers, # 传入包含随机UA的headers
)
next_url = response.xpath('//div[@class="pagination"]/a[text()=">"]/@href').extract_first()
if next_url is not None:
yield scrapy.Request(
url=next_url,
callback=self.parse,
)
def parse_detail(self, response):
'''详情页面解析函数'''
item = response.meta['item'] # 取出从parse传来的元数据
img = response.xpath('//div[@class="textpic"]/img/@src').extract_first() # 获取详情页面的图片地址(不含域名)
if img is None: # 若获取图片地址失败,则1.该页面仅有文本内容;2.该页面不存在-404
item['img'] = None
item['contentext'] = response.xpath('//div[@class="wzy1"]//tr[1]/td[@class="txt16_3"]/text()').extract()
else: # 若成功获取图片地址,加上域名前缀,且文本内容xpath如下
item['img'] = 'http://wz.sun0769.com'+img
item['contentext'] = response.xpath('//div[@class="contentext"]/text()').extract()
yield item
def get_ua(self):
'''随机生成User-Agent用户代理'''
first_num = random.randint(55, 76)
third_num = random.randint(0, 3800)
fourth_num = random.randint(0, 140)
os_type = [
'(Windows NT 6.1; WOW64)', '(Windows NT 10.0; WOW64)', '(X11; Linux x86_64)',
'(Macintosh; Intel Mac OS X 10_14_5)'
]
chrome_version = 'Chrome/{}.0.{}.{}'.format(first_num, third_num, fourth_num)
ua = ' '.join(['Mozilla/5.0', random.choice(os_type), 'AppleWebKit/537.36',
'(KHTML, like Gecko)', chrome_version, 'Safari/537.36']
)
return ua
4.2 管道文件
// pipelines.py
import re
from YangGuang.settings import MONGO_HOST, MONGO_PORT
from pymongo import MongoClient
class YangguangPipeline(object):
def open_spider(self, spider):
'''爬虫开始执行时执行的函数(仅run一次),可放入数据库连接代码'''
client = MongoClient(MONGO_HOST, MONGO_PORT) # 新建MongoDB客户端实例
self.col = client['YangGuang']['content'] # 新建数据库为YangGuang,集合为content的实例
def process_item(self, item, spider):
item['contentext'] = self.process_content(item['contentext']) # 清洗item中content数据
self.col.insert_many([dict(item)]) # 向MongoDB中插入数据
# print(item['href'])
return item
def process_content(self, content):
'''对content数据进行清洗,去除空白字符等不必要的数据'''
if content == []: # 如果content是空列表,说明详细页面响应404
return None
# 使用列表切片和sub替换,逐个清洗数据中的空格、\t、\n和\xa0等任意空白字符
content = [re.sub('\s', '', item) for item in content]
# 将含有多个字段的列表连接成只有一个字段的列表
content = ''.join(content)
return content
4.3 项目文件
// items.py
import scrapy
class YangguangItem(scrapy.Item):
title = scrapy.Field() # 标题
department = scrapy.Field() # 负责部门
status = scrapy.Field() # 处理状态
publish_date = scrapy.Field() # 发布时间
href = scrapy.Field() # 详情页面超链接
contentext = scrapy.Field() # 详情页面投诉文本内容
img = scrapy.Field() # 详情页面投诉图片内容
【参考文献】:
[1] XPath常用语法总结及应用.
[2] 东莞阳光热线问政平台.