六、数据解析方式之xpath
xpath属于xml/html解析数据的一种方式, 基于元素(Element)的树形结构(Node > Element)。选择某一元素时,根据元素的路径选择,如
/html/head/title
获取标签。
- XML 被设计用来传输和存储数据。XML 指可扩展标记语言(EXtensible Markup Language)
- HTML 被设计用来显示数据。
6.1. xpath解析原理:
- 1、实例化一个etree的对象,且需要将被解析的页面源码数据加载到该数据对象中
- 2、调用etree对象中的xpath 方法结合着xpath表达式标签的定位和内容的捕获
6.2. 环境安装:
- pip install lxml
6.3. 实例化一个etree对象:
- 1、导包:
from lxml import etree
- 2、将本地的html文档中的源码数据加载到etree对象中:
etree.parse(filePath)
- 3、可以将从互联网上获取的源码数据加载到该对象中
etree.HTML('page_text')
6.4. xpath(‘xpath表达式’)
- /:表示的是从根节点开始定位。表示的是一个层级。
- //:表示的是多个层级。可以表示从任意位置开始定位。
- 属性定位://div[@class='song'] tag[@attrName="attrValue"]
- 索引定位://div[@class="song"]/p[3] 索引是从1开始的。
- 取文本:
- /text() 获取的是标签中直系的文本内容
- //text() 标签中非直系的文本内容(所有的文本内容)
- 取属性:
/@attrName ==>img/src
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hx41BC9a-1602569751032)(E:\07-notes\picture\04-路径查询.png)]
6.5. 示例1:
需求:爬取58二手房中的房源信息和价格
#!/usr/bin/python3 # 需求:爬取58二手房中的房源信息和价格 import requests from lxml import etree if __name__ == '__main__': headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0' } # 爬取到页面源码数据 url = 'https://xa.58.com/ershoufang/' page_text = requests.get(url=url, headers=headers).text # 数据解析 tree = etree.HTML(page_text) # 存储的就是li标签对象 li_list = tree.xpath('//ul[@class="house-list-wrap"]/li') fp = open('58二手房', 'w', encoding='utf-8') for li in li_list: # 局部解析 title = li.xpath('./div[2]/h2/a/text()')[0] price = li.xpath('./div[3]/p//text()')[0] price2 = li.xpath('./div[3]/p//text()')[1] price3 = li.xpath('./div[3]/p//text()')[2] fp.write(title + '\n' + price + price2 + price3 + '\n') print(title + '\n' + price, price2, price3)
示例2:
需求:解析下载图片数据 http://pic.netbian.com/4kmeinv/
import requests from lxml import etree import os if __name__ == '__main__': # 创建文件夹,保存所有图片 if not os.path.exists('E:/03-图片/03-picture/picbz'): os.mkdir('E:/03-图片/03-picture/picbz') headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0' } # url = 'http://www.netbian.com/meinv/' url = 'http://pic.netbian.com/4kmeinv/index_%d.html' for pageNum in range(1,10): new_url = format(url % pageNum) response = requests.get(url=url, headers=headers) # 手动设定响应数据的编码格式 # response.encoding = 'utf-8' page_text = response.text # 数据解析:src的属性值 alt属性 tree = etree.HTML(page_text) li_list = tree.xpath('//ul[@class="clearfix"]/li') for li in li_list: img_src = 'http://pic.netbian.com' + li.xpath('./a/img/@src')[0] img_name = li.xpath('./a/img/@alt')[0] + '.jpg' # 通用处理中文乱码的解决方案 img_name = img_name.encode('iso-8859-1').decode('gbk') # 请求图片进行持久化存储 img_data = requests.get(url=img_src,headers=headers).content img_path = 'E:/03-图片/03-picture/picbz/' + img_name # print(img_name, img_src) with open(img_path, 'wb') as f: f.write(img_data) print(img_name, '下载成功')
6.6 示例
6.6.1 要求:站长之家照片采集
# xpath 是解决HTML页面中数据的提取
import requests
from lxml import etree
url = 'http://sc.chinaz.com/tupian/'
resp = requests.get(url)
if resp.status_code ==200:
print('--请求成功--')
# 开始使用xpath
# 1、获取网页根元素
resp.encoding='utf-8' # 获取文本数据之前,可以设置字符集
root = etree.HTML(resp.text) # 将下载的html文本转成xpath的根元素的Element
# with open('a.html', 'wb') as f:
# f.write(resp.content) # 将网页写入文件,方便查找标签
# 根据属性条件者直接或间接查找子元素
img_elements = root.xpath('//div[@class="pic_wrap"]//img') # 返回列表类型list[,...]
print(img_elements)
for img_element in img_elements:
print(img_element) #
print(img_element.get('alt')) # 霸气手持香烟美女图片
print(img_element.xpath('./@alt')[0]) # 霸气手持香烟美女图片
data = {
}
# data['title'] = img_element.xpath('./@alt')[0] # 返回的是list[,....]
# data['src'] = img_element.xpath('./@src2')[0]
# data['src'], data['title'] = img_element.xpath('./@alt |./@src2')
data['title'] = img_element.get('alt') # 获取标签元素的属性
data['src'] = img_element.get('src2')
print(data)
# 获取下一页的连接
# xpath()返回的是list类型
page_next_url = url + root.xpath('//a[@class="nextpage"]/@href')[0]
print(page_next_url)
6.6.2 代码优化
—> 脚本封装为函数并采集到下一页数据
import os
import random
import time
from urllib.request import urlretrieve
import requests
from lxml import etree
from utils.es import ESIndex
index = ESIndex('chinaz_zhb', '10.36.172.79' , 9200)
index.create_index()
# 1.开始
def start_spider(url, **kwargs):
print('正在下载', url)
resp = requests.get(url, **kwargs)
if resp.status_code == 200:
print('下载成功', url)
# 开始xpath解析(封装函数)
resp.encoding='utf-8'
parse(url, resp.text)
print(resp.text) # //div[@class="pic_wrap"]//img
# index = 1
# 2. 解析函数
def parse(url, html):
root = etree.HTML(html) # 获取当前网页的根节点
# global index
# with open('%s.html' % index, 'w', encoding='utf-8') as f:
# f.write(html)
#
# index += 1
xpath_str = '//div[@class="pic_wrap"]//img' if url.endswith('/')\
else '//div[@id="container"]//img'
print(xpath_str)
for img in root.xpath(xpath_str): # 返回列表类型list[,...]
item = {
}
item['title'] = img.get('alt') # 获取标签元素的属性
item['src'] = img.get('src2')
# 保存数据
item_pipeline(item)
# 获取下一页的url
if url.endswith('/'):
base_url = url
else:
base_url = url[:url.rfind('/')+1]
next_page_url = base_url+root.xpath('//a[@class="nextpage"]/@href')[0]
print(next_page_url)
time.sleep(random.uniform(0.2, 3.5)) # 休眠随机时间
# 开始循环拿数据
start_spider(next_page_url, headers={
'Referer': url,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0'
})
# 3. 保存数据处理
def item_pipeline(item):
print(item)
filename = item['title'] + item['src'][item['src'].rfind('.'):] # item['src'][item['src'].rfind('.'):]扩展名
download_img(item['src'], filename)
# 保存数据到ES搜索引擎
index.add_doc('images', **item)
# 下载图片的方法
def download_img(url, filename):
urlretrieve(url, os.path.join('images', filename))
if __name__ == '__main__':
start_spider('http://sc.chinaz.com/tupian/')
6.6.3 古诗文网站数据采集
# 古诗文网站数据采集
import requests
from lxml import etree
from lxml.etree import _Element
class GswSpider():
# 起始的url
start_url = ['https://so.gushiwen.org/shiwen/']
base_url = 'https://so.gushiwen.org'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0'
}
DEBUG = True
# 打印函数
def log(self, *args, sep='\n'):
if self.DEBUG:
print(*args, sep=sep)
# 启动爬虫
def start_spider(self):
for url in self.start_url:
self.get(url)
# 封装请求函数(下载)
def get(self, url, parse_func=None, **kwargs):
resp = requests.get(url, headers=self.headers) # 发起get请求
self.log(url, resp.status_code, sep=' ') # 打印url和状态码 https://so.gushiwen.org/shiwen/ 200
if resp.status_code == 200:
resp.encoding = 'utf-8' # 字符集处理
if parse_func is None:
self.parse(url, resp.text)
else:
parse_func(url, resp.text, **kwargs)
# 解析函数
def parse(self, url, html):
# 解析 右侧类型
root = etree.HTML(html) # 获取当前网页的根节点
for a_element in root.xpath('//div[@class="main3"]/div[last()]/div[1]//a'):
name = a_element.text # 拿到text属性 小学古诗
href = a_element.get('href') # https://so.gushiwen.org/gushi/xiaoxue.aspx
# self.log(name, href, sep='--->') # 小学古诗--->https://so.gushiwen.org/gushi/xiaoxue.aspx
self.get(href, self.parse_gsw_list) # 第二次发起请求
# 解析某一个分类下的所有古诗文
def parse_gsw_list(self, url, html):
root = etree.HTML(html)
left_div: _Element = root.xpath('//div[@class="main3"]/div[1]')[0]
# self.log(type(left_div)) #
cate_name = left_div.xpath('.//h1/text()')[0] # 当前标签的文本 小学古诗文
for subtype_element in left_div.xpath('.//div[@class="typecont"]'):
try:
subtype_name = subtype_element.xpath('.//strong/text()')[0] # 一年级上册
except:
subtype_name = ''
# self.log(cate_name, subtype_name, sep='=>') # 小学古诗文=>一年级上册
print(subtype_element.xpath('.//span/a'))
for span_a in subtype_element.xpath('.//span/a'):
href = span_a.get('href') # https://so.gushiwen.org/shiwenv_ef9cd9ba44bb.aspx
print(href)
if href:
href = href if href.startswith('http') else self.base_url + href
book_name = span_a.text # 诗名 江南
self.log(cate_name, subtype_name, book_name, href, sep='=>')
self.get(href, self.parse_gsw, category=cate_name, subtype=subtype_name) # 第三次发起请求
# 解析某一古诗详情
def parse_gsw(self, url, html, category=None, subtype=None):
self.log('parse_gsw', category, subtype, sep=' ')
root = etree.HTML(html)
# 提取诗文的内容
left_div = root.xpath('//div[@class="main3"]/div[1]')[0]
gsw_element = left_div.xpath('./div[2]/div[1]')[0]
name = gsw_element.xpath('.//h1/text()')[0]
era, author = gsw_element.xpath('./p/a/text()')
content = '\n'.join([c.replace('\n', '').strip() for c in gsw_element.xpath('./div[last()]/text()')])
# self.log(name, author, era, sep='+')
# print(content)
self.item_pipeline(dict(name=name, author=author, era=era, category=category, subtype=subtype, content=content))
def item_pipeline(self, item):
print(item)
# 写入到ES中
if __name__ == '__main__':
spider = GswSpider()
# spider.DEBUG=False
spider.start_spider()
【提示】:
1、解析类型数据获取标签举例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P9dVyAhw-1602569751036)(E:\07-notes\picture\06-古诗文采集标签举例.png)]
2、古诗文采集解析某一分类数据标签举例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VSEskUR9-1602569751039)(E:\07-notes\picture\07-古诗文采集解析某一分类数据标签举例.png)]
6.6.4 协程版古诗文数据采集
# 古诗文网站的数据采集
import requests
from lxml import etree
from lxml.etree import _Element
import asyncio
class GswSpider():
start_urls = ['https://so.gushiwen.org/shiwen/']
base_url = 'https://so.gushiwen.org'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:80.0) Gecko/20100101 Firefox/80.0'
}
DEBUG = True
# 加装饰器
@asyncio.coroutine
def log(self, *args, sep='\n'):
if self.DEBUG:
print(*args, sep=sep)
@asyncio.coroutine
def start_spider(self):
for url in self.start_urls:
yield from self.get(url)
@asyncio.coroutine
def get(self, url, parse_func=None, **kwargs):
resp = requests.get(url, headers=self.headers)
self.log(url, resp.status_code, sep=' ')
if resp.status_code == 200:
resp.encoding = 'utf-8'
if parse_func is None:
yield from self.parse(url, resp.text)
else:
yield from parse_func(url, resp.text, **kwargs)
@asyncio.coroutine
def parse(self, url, html):
root = etree.HTML(html)
for a_element in root.xpath('//div[@class="main3"]/div[last()]/div[1]//a'):
name = a_element.text
href = a_element.get('href')
yield from self.log(name, href, sep='->')
yield from self.get(href, self.parse_gsw_list)
@asyncio.coroutine
def parse_gsw_list(self, url, html):
# 解析某一个分类下的所有诗文
root = etree.HTML(html)
left_div: _Element = root.xpath('//div[@class="main3"]/div[1]')[0]
# self.log(type(left_div))
cate_name = left_div.xpath('.//h1/text()')[0]
for subtype_element in left_div.xpath('.//div[@class="typecont"]'):
try:
subtype_name = subtype_element.xpath('.//strong/text()')[0]
except:
subtype_name = ''
for span_a in subtype_element.xpath('.//span/a'):
href = span_a.get('href')
if href:
href = href if href.startswith('http') else self.base_url + href
book_name = span_a.text
yield from self.log(cate_name, subtype_name, book_name, href, sep='>')
yield from self.get(href, self.parse_gsw, category=cate_name, subtype=subtype_name)
@asyncio.coroutine
def parse_gsw(self, url, html, category=None, subtype=None):
yield from self.log('parse_gsw', category, subtype, sep=' ')
root = etree.HTML(html)
left_div = root.xpath('//div[@class="main3"]/div[1]')[0]
# 提取诗文内容
gsw_element = left_div.xpath('./div[2]/div[1]')[0]
name = gsw_element.xpath('.//h1/text()')[0]
era, author = gsw_element.xpath('./p/a/text()')
content = '\n'.join([c.replace('\n', '').strip() for c in gsw_element.xpath('./div[last()]//text()')])
# self.log(name, author, era, sep='+')
yield from self.item_pipeline(dict(name=name,
author=author,
era=era,
category=category,
subtype=subtype,
content=content))
@asyncio.coroutine
def item_pipeline(self, item):
print(item)
# 写入到ES中
if __name__ == '__main__':
spider = GswSpider()
spider.DEBUG=False
# asyncio.run(spider.start_spider()) # 获取事件模型对象
loop = asyncio.get_event_loop()
loop.run_until_complete(spider.start_spider()) # 单协程对象启动
# tasks = [spider.start_spider(), spider.start_spider(), spider.start_spider()]
# loop.run_until_complete(asyncio.wait(tasks)) # 多协程对象启动
# spider.get('https://so.gushiwen.org/shiwenv_e4df1367a39a.aspx', spider.parse_gsw)
七、验证码
7.1 Cookie:
http/https协议特性:无状态。
没有请求到对应页面数据的原因:
发起的第二次基于个人主页页面请求的时候,服务器端并不知道该此请求是基于登录状态下的请求。
cookie:用来让服务器端记录客户端的相关状态。
- 手动处理:通过抓包工具获取cookie值,将该值封装到headers中。(不建议)
- 自动处理:
- cookie值的来源是哪里?
- 模拟登录post请求后,由服务器端创建。
session会话对象:
- 作用:
1.可以进行请求的发送。
2.如果请求过程中产生了cookie,则该cookie会被自动存储/携带在该session对象中。
- 创建一个session对象:session = requests.Session()
- 使用session对象进行模拟登录post请求的发送(cookie就会被存储在session中)
- session对象对个人主页对应的get请求进行发送(携带了cookie)
八、代理
代理:破解封IP这种反爬机制。
什么是代理:
- 代理服务器。
代理的作用:
- 突破自身IP访问的限制。
- 隐藏自身真实IP
代理相关的网站:
- 快代理
- 西祠代理
- www.goubanjia.com
代理ip的类型:
- http:应用到http协议对应的url中
- https:应用到https协议对应的url中
代理ip的匿名度:
- 透明:服务器知道该次请求使用了代理,也知道请求对应的真实ip
- 匿名:知道使用了代理,不知道真实ip
- 高匿:不知道使用了代理,更不知道真实的ip
九、多任务爬虫
在爬虫中使用异步实现高性能的数据爬取操作
1. 进程和线程
-
multiprocessing模块(进程)
- Process 进程类
- Queue 进程间通信的队列
- put(item, timeout)
- item = get(timeout)
进程使用场景:
-
服务程序与客户程序分离,如:mysql服务和mysql客户端、Docker、Redis、ElasticSearch等服务都属于进程使用场景
-
服务框架中使用,如:scrapy、Django、Flask。
-
分离业务中使用,如:进程池的定时任务和计划编排等,Celery框架
-
使用场景图示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8uyxkhNQ-1602569751043)(E:\07-notes\picture\08-进程使用场景.png)]
-
threading 模块(线程)
- Thread 线程类
- 线程间通信(访问对象)
- queue.Queue 线程队列
- 回调函数(主线程声明, 子线程调用函数)
2.进程
2.1 进程的生命周期
# 进程的生命周期(状态):
# 1-> 创建 Xxx()
# 1->2: 启动 .start()
# 2-> 就绪,等待执行
# 3-> 运行,run()方法被执行,CPU分配时间执行片
# 3->2: 就绪, CPU执行片用完,则进入就绪状态
# 3->5 结束4
# 4-> 阻塞,在run()方法中,执行程序的过程遇到IO操作(IO操作:文件的读写、网络流的读写、网络请求)
# 4->2: 阻塞结束后,进入就绪状态
# 5-> 结束, 代码运行完毕,程序运行结束
图示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-58e83gBO-1602569751045)(E:\07-notes\picture\11-进程的生命周期图示.png)]
2.2 进程间的通信
# 进程之间的通信方式(进程间的内存是相互独立的):
# 1. multiprocessing.Queue 进程队列
# 2. multiprocessing.Pipe 进程管道
# 3. multiprocessing.Manager 共享内存 ,基于C实现的,Manager中所指放入数据类型都是c的
# 4. Linux下的socket.AF_UNIX 套接字
# 5. signals 信号(键盘事件监听等)
2.3 爬虫进程设计图示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7XzryDDS-1602569751047)(E:\07-notes\picture\09-爬虫进程设计.png)]
2.4 进程队列
2.4.1 说明
使用管道和少量的锁/信号量实现的进程共享的队列
当进程首先将一个项目放到队列中时,启动一个将线程从缓冲区转移到管道中的Feeder线程
2.4.2 Queue对列的方法
- Queue(maxsize=0) 最大进程任务数,<=0 表示不限制
- 输入: put(obj[, block[, timeout]])
- 获取输出: get([block[, timeout]])
- 队列大小: qsize()
- 是否为空: empty()
- 关闭: close()
# 需求: Boss派20项活给5个工人完成
from multiprocessing import Process, Queue
import time
import os
def boss(q): # 大老板安排任务
for i in range(20):
msg = 'Boss按排的任务: %d ' %i
q.put(msg) # 如果msg量没有达到最高值,则直接存入,反之,则等待
print(time.strftime('%x %X', time.localtime()), msg) # 08/29/20 09:12:06 Boss按排的任务: 0
time.sleep(0.5)
print('--boss任务派发完成----')
def worker(q):
while True:
msg = q.get(timeout=5) # 5秒内没有消息,表示工作完成
print('at {} 工人<{}> 收到: {}'.format(time.time(), os.getpid(), msg))
time.sleep(2)
if __name__ == '__main__':
q = Queue(maxsize=2) # 最大的消息数量
workers = []
for i in range(5):
p = Process(target=worker, args=(q, ))
p.start()
workers.append(p) # 将所有工人进程管理起来
boss(q) # 老板开始派活
for worker in workers:
worker.terminate() # 解散工人
q.close()
print('--工作完成---over--')
2.4.3 晒了吗网站多任务数据爬取
2.5 进程管道 (半/全双工)
管道也叫无名管道,它是 UNIX 系统 IPC(进程间通信 (Inter-Process Communication) 的最古老形式
管道用来连接不同进程之间的数据流
pipe.recv() 接收消息
pipe.send(str) 发送消息
2.5.1.1 半双工
import time
from multiprocessing import Process,current_process as cp,Pipe
def send_msg(conn):
print('--send msg--',cp().name)
time.sleep(3)
conn.send((1,2,3)) #管道通信尅发送任何类型对象
# conn.send('are you ok?')
print('--msg已发送--',cp().name)
def receive_msg(conn):
print('--receive msg--',cp().name)
msg=conn.recv() #阻塞到收到消息为止
print(cp().name,'receives msg->',msg)
if __name__ == '__main__':
# duplex=Flase 表示单双工
# conn1仅接收(只读) conn2仅发
conn1,conn2=Pipe(duplex=False)
p1=Process(target=send_msg,args=(conn2,))
p2 = Process(target=receive_msg, args=(conn1,))
p2.start()
p1.start()
p1.join()
p2.join()
print('--over--')
2.5.1.1 全双工
import time
from multiprocessing import Process,current_process as cp,Pipe
def a(conn):
print(cp().name,'--a msg--')
time.sleep(1)
conn.send('are you ok?')
print(cp().name,'a->b sended msg')
msg=conn.recv() #等待消息到达
print(cp().name,'接收到b的消息',msg)
def b(conn):
print(cp().name,'--b msg--')
msg=conn.recv() #等待消息到达
if msg.find('are you ok?')>-1:
conn.send('ok!')
else:
conn.send('收到你的消息真开心')
if __name__ == '__main__':
# duplex=True 表示全双工
# conn1/conn2 可读可写
conn1,conn2=Pipe(duplex=True)
p1=Process(target=a,args=(conn1,))
p2 = Process(target=b, args=(conn2,))
p2.start()
p1.start()
p1.join()
p2.join()
print('--over--')
3. 线程
3.1 线程的概念
1、一个进程里面至少有一个线程,进程的概念只是一种抽象的概念,真正在CPU上面调度的是进程里的线程
2、线程是真正干活的,线程用的是进程里面包含的一堆资源,线程仅仅是一个调度单位,不包含资源
3、每一个进程在启动的时候都会默认创建一个线程, 这个线程叫主线程(MainThread)
4、一个进程(任务)里面可能对应多个分任务,如果一个进程里面只开启一个线程的话,多个分任务之间实际上是串行的执行效果,即一个程序里面只含有一条执行路径
3.2 线程与进程的关系
一个程序启动起来以后,至少有一个进程,这个进程至少有一个线程
3.2.1 功能
- 进程,能够完成多任务,比如 在一台电脑上能够同时运行多个QQ。
- 线程,能够完成多任务,比如 一个QQ中的多个聊天窗口。
3.2.2 定义的不同
- 进程是系统进行资源分配和调度的一个独立单位.
- 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
3.2.3 区别
-
一个程序至少有一个进程,一个进程至少有一个线程.
-
线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高。
-
进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
-
线程不能够独立执行,必须依存在进程中
-
可以将进程理解为工厂中的一条流水线,而其中的线程就是这个流水线上的工人
优缺点
线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。
开发中多用 ---> 多进程 + 协程
进程:占用资源多,效率相对较低,便于管理和维护
线程:占用资源少,效率相对较高,不便于管理和维护
存在一个平衡
例:
开4个进程 —> 一般有几个CPU就开几个进程(4核双线程就可以开8个进程),可以实现并行
开400个线程 —> 实现并发,一个CPU不断切换执行任务
3.3. 同步与异步
同步:即是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去。
异步:与同步相反,即进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。
当有消息返回时系统会通知进行处理,这样可以提高执行的效率。
3.4. 串行与并发
-
CPU地位:
无论是串联、并行或并发,在用户看来都是同时运行的,不管是进程还是线程,都只是一个任务而已, 真正干活的是CPU,CPU来做这些任务,而一个cpu(单核)同一时刻只能执行一个任务
-
串行:
在执行多个任务时,一个任务接着一个任务执行,前一任务完成后,才能执行下一个任务。
-
并行:
多个任务同时运行,只有具备多个cpu才能实现并行,含有几个cpu,也就意味着在同一时刻可以执行几个任务
-
并发:
是伪并行,即看起来是同时运行的,实际上是单个CPU在多个程序之间来回的切换
3.5. 线程案例
import random
import time
from threading import Thread, current_thread as ct, Lock
from queue import Queue
class DownloadThread(Thread):
def __init__(self):
super(DownloadThread,self).__init__()
def run(self) -> None:
global sum
print(ct().name, 'running...')
time.sleep(random.uniform(0.5, 3.5))
n = random.randint(1, 100)
# lock 在上下文中使用时
# 进入上下文: 加锁 lock.acquire()
# 退出上下文: 释放锁 lock.release()
with lock:
print(ct().name, '产生了', n, '当前的sum->', sum)
sum += n
time.sleep(0.2)
print(ct().name, '当前的sum->', sum)
if __name__ == '__main__':
sum = 100 # 可以在多个线程中使用
lock = Lock()
# 创建了10个线程
ts = [
DownloadThread()
for i in range(10)
]
# 启动线程
for t in ts:
t.start()
# 等待所有线程执行完成
for t in ts:
t.join() # 阻塞方法
3.5.1. 安全锁
- 创建锁
lock = threading.Lock()
lock = threading.RLock()
- 加锁
lock.acquire()
- 解锁
lock.release()
3.5.2. 线程本地变量
理解:
ThreadLocal 变量,它本身是一个全局变量,但是每个线程却可以利用它来保存属于自己的私有数据,这些私有数据对其他线程也是不可见的
一、对 ThreadLocal 的理解
ThreadLocal,有的人叫它线程本地变量,也有的人叫它线程本地存储,其实意思一样。 ThreadLocal 在每一个变量中都会创建一个副本,每个线程都可以访问自己内部的副本变量。
二、为什么会出现 ThreadLocal 的技术应用
我们知道多线程环境下,每一个线程均可以使用所属进程的全局变量。如果一个线程对全局变量进行了修改,将会影响到其他所有的线程对全局变量的计算操作,从而出现数据混乱,即为脏数据。为了避免线程同时对变量进行修改,引入了线程同步机制,通过互斥锁、条件变量或者读写锁来控制对全局变量的访问。
只用全局变量并不能满足多线程环境的需求,很多时候线程还需要拥有自己的私有数据,这些数据对于其他线程来说是不可见的。因此线程中也可以使用局部变量,局部变量只有线程自身可以访问,同一个进程下的其他线程不可访问。
有时候使用局部变量不太方便,因此 Python 还提供了ThreadLocal 变量,它本身是一个全局变量,但是每个线程却可以利用它来保存属于自己的私有数据,这些私有数据对其他线程也是不可见的。
ThreadLocal 真正做到了线程之间的数据隔离,将线程的数据进行私有化
import time
from threading import Thread, current_thread as ct, local
class Download(Thread):
def __init__(self, url):
super(Download, self).__init__()
self.url = url
def run(self) -> None:
# 初始化请求头
# 设置代理或Cookie等
print(ct().name, '---set user-agent---', self.url)
# id(current_thread())
# headers 是线程的本地变量,添加属性时,使用当前的线程对象的ID作为key,属性作为value()
# headers 本地变量实际的数据结构是 {id(thread):{属性名:属性值}}
headers.user_agent = '%s Firefox 66.72' % self.url
time.sleep(1)
print(ct().name, headers.user_agent)
time.sleep(1)
if __name__ == '__main__':
headers = local() # 线程的本地变量
# Python提供了 threading.local 类,将这个类实例化得到一个全局对象,
# 但是不同的线程使用这个对象存储的数据其它线程不可见(本质上就是不同的线程使用这个对象时为其创建一个独立的字典)。
urls = ('http://www.baidu.com','http://hao123.com','http://jd.com')
ts = [
Download(url)
for url in urls
]
for t in ts:t.start()
for t in ts:t.join()
# 输出结果:每一个线程读到的headers都不同,真正做到了线程之间的隔离
# Thread-1 ---set user-agent--- http://www.baidu.com
# Thread-2 ---set user-agent--- http://hao123.com
# Thread-3 ---set user-agent--- http://jd.com
# Thread-1 http://www.baidu.com Firefox 66.72
# Thread-3Thread-2 http://hao123.com Firefox 66.72
# http://jd.com Firefox 66.72
3.5.3. 线程条件变量
条件变量(Condition)
作用:
实现多线程的数据安全问题,当数据不满足条件时,可以让线程挂起,反之唤醒其他等待线程。
内部使用线程锁
原理:
互斥锁,主要作用是并行访问共享资源时,保护共享资源,防止出现脏数据。python 条件变量Condition也需要关联互斥锁,同时Condition自身提供了wait/notify/notifyAll方法,用于阻塞/通知其他并行线程,可以访问共享资源了。可以这么理解,Condition提供了一种多线程通信机制,假如线程1需要数据,那么线程1就阻塞等待,这时线程2就去制造数据,线程2制造好数据后,通知线程1可以去取数据了,然后线程1去获取数据
- 用法:
条件变量利用线程间共享的全局变量进行同步的一种机制
- 两个动作:
1、一个 线程等待"条件变量的条件成立"而挂起
2、另一个线程使“条件成立”
- 与互斥锁结合使用:
- 为了防止竞争,防止死锁
- 线程在改变条件状态前必须首先锁住互斥量(Lock)
- 把条件变量到等待条件的线程列表上
- 对互斥锁解锁
- 常用函数:
# 创建条件变量
cond = threading.Condition(threading.Loca())
acquire(*args) 线程锁,注意线程条件变量中的所有相关函数使用必须在acquire() /release() 内部操作;
release() 条件变量解锁
wait([timeout]) 等待唤醒,timeout表示超时
notify(n=1) 唤醒最大n个等待的线程
notifyAll()、notify_all() 唤醒所有等待的线程
示例:
import time
from threading import Thread, Condition, Lock, current_thread as ct
from queue import Queue
class ConQueue(Queue):
def __init__(self, maxsize=10): # 初始化
super(ConQueue, self).__init__(maxsize)
self.cond = Condition(Lock()) # 条件变量对象,传入一把锁
def consume(self, **kwargs):
# 消费的方法
name = ct().name + '<%s>' % ct().ident # 线程ID
if self.cond.acquire(): # 加锁
while self.empty(): # 如果仓库为空
print(name, '当前仓库是空的')
self.cond.wait() # 等待、挂起
item = self.get_nowait() # 不用等待 获取数据
self.cond.notify() # 唤醒所有等待线程
self.cond.release() # 释放锁
return item
def product(self, item):
# 生产的方法
name = ct().name + '<%s>' % ct().ident # 线程ID
if self.cond.acquire():
while self.full():
print(name, '仓库已满')
self.cond.wait()
self.put_nowait(item) # 不需要等待 存入仓库
self.cond.notify() # 唤醒其他(消费)线程
self.cond.release() # 释放锁
class ProducterThread(Thread): # 生产线程
def __init__(self, con_queue):
super(ProducterThread, self).__init__(name='Producyor')
self.queue:ConQueue = con_queue
def run(self) -> None:
name = ct().name + '<%s>' % ct().ident # 线程ID
global num
while True:
with lock:
item = '%s 面包' % num
self.queue.product(item)
print(name, '生产了', item)
num += 1
time.sleep(2)
class ConsumThead(Thread): # 消费线程
def __init__(self, con_queue):
super(ConsumThead, self).__init__(name='Consumer')
self.queue = con_queue
def run(self) -> None:
name = ct().name + '<%s>' % ct().ident
while True:
item = self.queue.consume() # 可能存在等待状态
print(name, '消费了', item)
time.sleep(1)
def start(*threads):
for t in threads:
t.start()
def join(*threads):
for t in threads:
t.join()
if __name__ == '__main__':
queue = ConQueue(20)
cs = [ConsumThead(queue) for _ in range(5)]
ps = [ProducterThread(queue) for _ in range(2)]
num = 1 # 面包的序号
lock = Lock()
start(*cs, *ps) # 可以解包list或tuple元组
# start(*ps)
join(*cs, *ps)
# join(*ps)
print('--over--')
4. 协程
协程是线程的替代品, 区别在于线程由CPU调度, 协程由用户(程序)自己的调度的。
协程需要事件监听模型(事件循环器),它采用IO多路复用原理,在多个协程之间进行调度
4.1 协程的定义原理
- 1.协程是以协作式调度的单线程,协程又称之为“微线程”,它是在一个线程内完成函数或子程序之间调度
- 2.一个函数(子程序)在调用时都是按层级调用,如A调用B,B调用C,再依次返回结果
- 3.函数调用是通过栈实现的,栈中存放函数的局部变量
- 4.函数或子程序的调用一个入口, 一次返回,调用顺序是明确。
- 5.协程在执行子程序或函数时,函数内部是可以中断,转而执行别的子程序,在适当时再返回接着执行。
4.2 协程的事件模型
协程的事件模型(IO异步模型):
- selector 轮询
- poll 事件回调
- kqueue/epoll 增强式事件回调
4.3. 协程的三种方式
- 基于生成器 generator (过渡)
- yield
- send()
- Python3 之后引入了 asyncio模块
- @asyncio.coroutine 协程装饰器, 可以在函数上使用此装饰器,使得函数变成协程对象
- 在协程函数中,可以使用yield from 阻塞当前的协程,将执行的权限移交给 yield from 之后的协程对象。
- asyncio.get_event_loop() 获取事件循环模型对象, 等待所有的协程对象完成之后结束。
- Python3.5之后,引入两个关键字
- async 替代 @asyncio.coroutine
- await 替代 yield from
协程对象的运行方式:
- loop = asyncio.get_event_loop()
- loop.run_until_comlete(协程对象)
- 自定义的协程函数,由@asyncio.coroutine装饰器装饰的函数,即协程对象
- 通过asyncio.wait()协程函数封装多个自定义协程对象
4.3.1. 基于生成器
# 基于生成器方式实现协程
# yield/send
# 斐波那契数列: 1,1,2,3,5,8,.....
import random
import time
def fib(n):
a, b = 0, 1
index = 0
while index < n:
wait = yield b # 将b值输出给调用者,等待调用者输入(传入)等待时间wait
print(wait, ' 秒之后将会产出')
time.sleep(wait)
a, b = b, a+b
index += 1
def main():
f = fib(10)
n = next(f) # 从fib函数中获取数字
while True:
try:
print('--->', n)
wait_second = random.uniform(0.1, 2.0)
n = f.send(wait_second) # 向fib生成器函数发送等待时间(数据),等待fib函数产出下一个数字
except: # StopIteration异常:生成器产出完成
break
if __name__ == '__main__':
# for n in fib(10):
# print(n, end=' ')
main()
4.3.2. 引入asyncio模块
# 斐波那契数列: 1,1,2,3,5,8,.....
import random
import time
import asyncio
from asyncio import coroutine
from utils.ua_ import *
import requests
@coroutine
def get(url):
print('--正在GET请求-->', url)
resp = requests.get(url, headers={
'User-Agent': get_ua()
})
if resp.status_code == 200:
resp.encoding = 'utf-8'
items = yield from parse(url, resp.text)
print(url, '===解析完成===>', items)
def download(*urls):
# 下载任务的入口函数
# 创建异步协程的时间循环模型
loop = asyncio.get_event_loop() # 获取事件循环器
# loop.run(协程对象) 单个协程运行
# 生成批量的协程任务,并添加事件模型执行
# 启动循环知道结束为止
loop.run_until_complete(
asyncio.wait([
get(url)
for url in urls
])
)
@coroutine
def parse(url, html):
print(url, '正在解析')
# time.sleep() # 当前线程挂起,阻塞
yield from asyncio.sleep(random.uniform(0.1, 3.0)) # 当前协程挂起,阻塞
return {
'url': url,
'data':time.localtime()}
if __name__ == '__main__':
# download('http://www.baidu.com','http://hao123.com','http://jd.com')
# 单个运行协程的方式
coroutine_1 = get('http://www.baidu.com')
asyncio.get_event_loop().run_until_complete(coroutine_1)
4.3.3. async和await
# 斐波那契数列: 1,1,2,3,5,8,.....
import random
import time
import asyncio
from asyncio import coroutine
from utils.ua_ import *
import requests
async def get(url):
print('--正在GET请求-->', url)
resp = requests.get(url, headers={
'User-Agent': get_ua()
})
if resp.status_code == 200:
resp.encoding = 'utf-8'
items = await parse(url, resp.text)
print(url, '===解析完成===>', items)
def download(*urls):
# 下载任务的入口函数
# 创建异步协程的时间循环模型
loop = asyncio.get_event_loop() # 获取事件循环器
# loop.run(协程对象) 单个协程运行
# 生成批量的协程任务,并添加事件模型执行
loop.run_until_complete(
asyncio.wait([
get(url)
for url in urls
])
)
async def parse(url, html):
print(url, '正在解析')
# time.sleep() # 当前线程挂起,阻塞
await asyncio.sleep(random.uniform(0.1, 3.0)) # 当前协程挂起,阻塞
return {
'url': url,
'data':time.localtime()}
if __name__ == '__main__':
# download('http://www.baidu.com','http://hao123.com','http://jd.com')
# 单个运行协程的方式
coroutine_1 = get('http://www.baidu.com')
asyncio.get_event_loop().run_until_complete(coroutine_1)
十、selenium
Selenium是驱动浏览器(chrome, firefox, IE)进行浏览器相关操作(打开url, 点击网页中按钮功连接、输入文本)
10.1.什么是selenium模块
-
基于浏览器自动化的一个模块。
-
支持通过各种driver(FirfoxDriver,IternetExplorerDriver,OperaDriver,ChromeDriver)驱动真实浏览器完成测试
selenium也是支持无界面浏览器操作的。比如说HtmlUnit和PhantomJs。
10.2. 为什么使用selenium
-
模拟浏览器功能,自动执行网页中的js代码,实现动态加载
-
页面渲染
在浏览器请求服务器的网页时, 执行页面的js,在js中将数据转成DOM元素(HTML标签) -
UI自动测试
- 定位输入DOM节点
- 点击某一个DOM节点(Button/a标签)
10.3 使用selenium
安装环境:
pip install selenium
下载一个浏览器的驱动程序(谷歌浏览器)
- 下载路径:http://chromedriver.storage.googleapis.com/index.html
- 驱动程序和浏览器的映射关系:http://blog.csdn.net/huilan_same/article/details/51896672
导入模块:
from selenium import webdriver
实例化一个浏览器对象
-
编写基于浏览器自动化的操作代码
-
发起请求:get(url)
-
标签定位:find系列的方法
-
标签交互:send_keys(‘xxx’)
-
执行js程序:excute_script(‘jsCode’)
-
前进,后退:back(),forward()
-
关闭浏览器:quit()
-
【总结】元素定位:
1、find_element_by_id WebElement 元素
2、find_elements_by_name
- 根据标签的name属性查找多个Dom元素
- form表单中的字段标签都具有name属性
- iframe标签具有name属性
3、find_elements_by_xpath
- 基于xpath方式查找DOM元素
- 依赖lxml库
4、find_elements_by_tag_name
- 根据标签名查找多个Dom元素
5、find_elements_by_class_name
6、find_elements_by_css_selector
- #id
- .class_name
- div
- div p>a
7、find_elements_by_link_text
- 根据a标签的文本,根据a标签
示例:
需求:打开淘宝搜索IPhone,再打开百度–>回退–>前进
# 需求:打开淘宝搜索IPhone,再打开百度-->回退-->前进
from selenium import webdriver
from time import sleep
# 实例化浏览器(谷歌)
path = r'D:\01-soft\12-spider_chromedriver\chromedriver.exe'
bro = webdriver.Chrome(executable_path=path)
# 发起请求
bro.get('https://www.taobao.com/')
# 标签定位
search_input = bro.find_element_by_id('q')
# 标签交互
search_input.send_keys('Iphone')
# 执行一组js程序
# scrollTo(0,document.body.scrollHeight) scrollTo(x,y)屏幕滚动,x表示左右滚动,y表示上下滚动
# document.body.scrollHeight表示向下滚动一屏
bro.execute_script('window.scrollTo(0,document.body.scrollHeight)')
sleep(2)
# 点击搜索按钮
btn = bro.find_element_by_css_selector('.btn-search')
btn.click()
bro.get('https://www.baidu.com')
sleep(2)
# 回退
bro.back()
sleep(2)
# 前进
bro.forward()
sleep(5)
# 退出程序(关闭浏览器)
bro.quit()
from selenium.webdriver import Chrome, Firefox
path = r'D:\01-soft\12-spider_chromedriver\chromedriver.exe'
chrome = Chrome(path)
# chrome = Firefox(executable_path=r'D:\01-soft\12-spider_chromedriver\geckodriver.exe')
# 打开必应
chrome.get('http://cn.bing.com')
# 截图工具
chrome.save_screenshot('bing.png')
chrome.close() # 关闭页签,如果浏览器只有一个页签,即也会退出浏览器
chrome.quit() # 退出程序(关闭浏览器)
10.4. selenium处理iframe
- selenium处理iframe
- 如果定位的标签存在于iframe标签之中,则必须使用switch_to.frame(id)
- 动作链(拖动):from selenium.webdriver import ActionChains
- 实例化一个动作链对象:action = ActionChains(bro)
- click_and_hold(div):长按且点击操作
- move_by_offset(x,y)
- perform()让动作链立即执行
- action.release()释放动作链对象
示例1:
from selenium import webdriver
from time import sleep
#导入动作链对应的类
from selenium.webdriver import ActionChains
path = r'D:\01-soft\12-spider_chromedriver\chromedriver.exe'
bro = webdriver.Chrome(executable_path=path)
bro.get('https://www.runoob.com/try/try.php?filename=jqueryui-api-droppable')
#如果定位的标签是存在于iframe标签之中的则必须通过如下操作在进行标签定位
bro.switch_to.frame('iframeResult')#切换浏览器标签定位的作用域
div = bro.find_element_by_id('draggable')
#动作链
action = ActionChains(bro)
#点击长按指定的标签
action.click_and_hold(div)
for i in range(5):
#perform()立即执行动作链操作
#move_by_offset(x,y):x水平方向 y竖直方向
action.move_by_offset(17,0).perform()
sleep(0.5)
#释放动作链
action.release()
bro.quit()
示例2:
需求:模拟登陆qq空间
from selenium import webdriver
from time import sleep
path = r'D:\01-soft\12-spider_chromedriver\chromedriver.exe'
bro = webdriver.Chrome(executable_path=path)
bro.get('https://qzone.qq.com/')
bro.switch_to.frame('login_frame')
a_tag = bro.find_element_by_id("switcher_plogin")
a_tag.click()
userName_tag = bro.find_element_by_id('u')
password_tag = bro.find_element_by_id('p')
sleep(1)
userName_tag.send_keys('1174333100')
sleep(1)
password_tag.send_keys('zhb1174333100')
sleep(1)
btn = bro.find_element_by_id('login_button')
btn.click()
sleep(3)
bro.quit()
10.5. 交互
1.点击 click()
2.输入 send_keys()
3.模拟 JS滚动
var q = window.document.documentElement.scrollTop=10000
execute_script() 执行js代码
今日头条
# ......
# 模拟滚动操作
# document.documentElement 表示当前页面元素,指
# 获取窗口高度
print(chrome.get_window_rect(), chrome.get_window_size())
for n in range(50):
script = 'var q=window.document.documentElement.scrollTop=%s' % ((n+1)*500)
chrome.execute_script(script)
time.sleep(0.5)
# ......
10.6. 页面异步ajax的解决办法
原因:
由于网页中有ajax的异步执行的js, 导致driver.get()之后查找元素报 NoSuchElementException异常
导包:
from selenium.webdriver.common.by import By
from selenium.webdriver.support import ui
from selenium.webdriver.support import expected_conditions as EC
解决:
# 等待某一个Element出现为止,否则一直阻塞下去,不过可以设置一个超时时间
ui.WebDriverWait(driver, 60).until(EC.visibility_of_all_elements_located((By.CLASS_NAME, 'soupager')))
10.7. switch的用法
原因:
- 当页面中出现对话框 alert,或内嵌窗口iframe
- 如果查找的元素节点在alert或iframe中的话,则需要切入到alert或iframe中
解决:
1. 查找iframe标签对象
iframe = driver.find_element_by_id('login_frame')
2. 切换到iframe中
driver.switch_to.frame(iframe)
10.8. 获取浏览器的页签
brower.window_handlers[0] # 第一个页签,一般都存在
brower.window_handlers[1] # 如果浏览器打开新的资源在新的页签时,可以获取,如果不存在第二个页签,则会报错
退出:browser.quit()
案例:
登录邮箱(内嵌窗口)
import time
from selenium.webdriver import Chrome
chrome = Chrome(r'D:\01-soft\12-spider_chromedriver\chromedriver.exe')
chrome.get('https://mail.qq.com') # 阻塞方法,等待网页中的所有js执行完毕
# 以下两个输入框是在
login_frame = chrome.find_element_by_id('login_frame')
chrome.switch_to.frame(login_frame) # 切入到内嵌窗口中
uesr_input = chrome.find_element_by_id('u')
pwd_input = chrome.find_element_by_id('p')
uesr_input.send_keys('123344556@qq.com')
pwd_input.send_keys('zdx1233456')
# 查找登录按钮,并点击
chrome.find_element_by_id('login_button').click()
10.9 无头浏览器
from selenium import webdriver
from time import sleep
#实现无可视化界面
from selenium.webdriver.chrome.options import Options
#实现规避检测
from selenium.webdriver import ChromeOptions
#实现无可视化界面的操作
chrome_options = Options()
chrome_options.add_argument('--headless')
chrome_options.add_argument('--disable-gpu')
#实现规避检测
option = ChromeOptions()
option.add_experimental_option('excludeSwitches', ['enable-automation'])
#如何实现让selenium规避被检测到的风险
path = r'D:\01-soft\12-spider_chromedriver\chromedriver.exe'
bro = webdriver.Chrome(path,chrome_options=chrome_options,options=option)
#无可视化界面(无头浏览器) phantomJs
bro.get('https://www.baidu.com')
print(bro.page_source)
sleep(2)
bro.quit()
10.10 12306模拟登陆
10.10.1. 超级鹰验证码使用
十一、scrapy框架
11.1. 介绍
什么是scrapy:
Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中。
功能:高性能的持久化存储,异步的数据下载,高性能的数据解析,分布式
官方网站:
https://doc.scrapy.org/en/latest/
http://www.scrapyd.cn/doc/ 中文
http://scrapy-chs.readthedocs.io/zh_CN/latest/ 中文
11.2. scrapy框架的基本使用
- 环境的安装:
```
- mac or linux:pip install scrapy
- windows:
- pip install wheel
- 下载twisted,下载地址为http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
- 安装twisted:pip install Twisted‑17.1.0‑cp36‑cp36m‑win_amd64.whl
- pip install pywin32
- pip install scrapy
测试:在终端里录入scrapy指令,没有报错即表示安装成功!
```
-
创建一个工程:
- scrapy startproject xxxPro
-
cd xxxPro
在spiders子目录中创建一个爬虫文件
- scrapy genspider spiderName www.xxx.com
-
执行工程:
- scrapy crawl spiderName
11.3. 框架组成
11.3.1. 五个核心
-
engine 引擎, 协调其它四个组件之间的联系,即与其它四个组件进行通信,也是scrapy框架的核心。自动运行,无需关注,会自动组织所有的请求对象,分发给下载器
-
spider 爬虫类, 爬虫程序的编写代码所在, 也是发起请求的开始的位置。spider发起的请求,经过engine转入到scheduler中。
请求成功之后的数据解析
- scrapy.Spider 普通的爬虫 - scrapy.CrawlSpider - 可设置规则的爬虫类 - Rule 规则类 - 开始的函数 - start_requests()
-
scheduler 调度器, 调度所有的请求(优先级高,则会先执行)。当执行某一个请求时,由engine转入到downloader中。
-
donwloader 下载器, 实现请求任务的执行,从网络上请求数据,将请求到的数据封装成响应对象,并将响应的对象返回给engine。engine将数据响应的数据对象(以回调接口方式)回传给它的爬虫类对象进行解析。
-
itempipeline 数据管道, 当spider解析完成后,将数据经engine转入到此(数据管道)。再根据数据类型,进行数据处理(图片、文本)
1. 清理HTML数据 2. 验证爬取的数据(检查item包含某些字段) 3. 查重(并丢弃) 4. 将爬取结果保存到数据库中 5. 对图片数据进行下载
scrapy框架逻辑图:
流程:
1、爬虫引擎获得初始请求开始抓取。
2、爬虫引擎开始请求调度程序,并准备对下一次的请求进行抓取。
3、爬虫调度器返回下一个请求给爬虫引擎。
4、引擎请求发送到下载器,通过下载中间件下载网络数据。
5、一旦下载器完成页面下载,将下载结果返回给爬虫引擎。
6、引擎将下载器的响应通过中间件返回给爬虫进行处理。
7、爬虫处理响应,并通过中间件返回处理后的items,以及新的请求给引擎。
8、引擎发送处理后的items到项目管道,然后把处理结果返回给调度器,调度器计划处理下一个请求抓取。
9、重复该过程(继续步骤1),直到爬取完所有的url请求
11.3.3. scrapy使用
- 创建项目命令
- scrapy startproject 项目名称
- 创建爬虫命令
- scrapy genspider 爬虫名 域名
- 启动爬虫命令
- scrapy crawl 爬虫名
- 调试爬虫命令
- scrapy shell url
- scrapy shell
- fetch(url)
目录结构:
- spiders
- __init__.py
- 自定义的爬虫文件.py
- __init__.py
- items.py
定义数据结构的地方,是一个继承自scrapy.Item类, 属性字段的类型是 scrapy.Field()
注意: scrapy.Item类,实际是一个dict字典, 所以在spider的parse()函数返回的迭代元素应该是dict字典对象,且字典的key与 item的Field()相对应
- middlewares.py
中间件, 用于调整业务逻辑
- pipelines.py
管道文件,里面只有一个类,用于处理下载数据的后续处理
- settings.py
配置文件
比如:
是否遵守robots协议
User-Agent定义等
11.3.3. 爬虫文件
解析函数:
-
parse_detail(self, response: Response)
-
解析数据的回调函数,response保存了下载的数据,可以在此函数内对其进行解析,通常使用xpath,parse()函数,如果有返回值,必须返回可迭代的对象
-
Response的类方法:
- selector() - css() 样式选择器 , 返回Selector选择器的可迭代(列表)对象 - scrapy.selector.SelectorList 选择器列表 - x()/xpath() - scrapy.selector.Selector 选择器 - 样式选择器提取属性或文本 - `::text` 提取文本 - `::attr("属性名")` 提取属性 - xpath() xpath路径 xpath路径,同lxml的xpath()写法 - 选择器常用方法 - css()/xpath() - extract() 提取选择中所有内容,返回是list - extract_first()/get() 提取每个选择器中的内容, 返回是文本
- response 是 scrapy.http.response.HtmlResponse类对象 - response.css('.class属性') 拿到class属性的标签 - response.css('.contentHerf::attr("href")') 获取标签的href属性 - response.xpath() scrapy.selector.Selector 返回Selector对象 内部写法:self.selector.xpath() - extract()/getall() Selector对象的方法,用于获取Selector对象的内容 即提取是Selector对象中的data属性 response.xpath('//title/text()').extract() 返回list response.css('').xpath() 先使用css选择标签元素,再通过xpath提取内容 - extract_first()/get() 提取第一条内容 - css()方法中字符串:#id, .class, div, div>p, ::attr(‘属性名’), ::text 标签文本
-
Request类
scrapy.http.Request
请求对象的属性:
- url - callback 解释数据的回调函数对象 - headers 请求头 - priority 请求的优先 Request()中的meta属性可以向下一个解析函数传递数据(元数据) 注意:meta是dict字典格式,value不能是一个引用对象 (scrapy 1.5版本) Request()中的priority 请求在scheduler调度器中的优先级,值越高,级别越高,则优先下载 Request()中的dont_filter为False表示过滤重复下载的请求,为True则不过滤
-
11.3.4. 示例:
# 示例
import scrapy
from scrapy import Request
from scrapy.http import Response, HtmlResponse
from scrapy.selector import SelectorList
from qsbk import cookie_
class TxtSpider(scrapy.Spider): # 继承父类
name = 'jokes' # 糗事百科的段子
allowed_domains = ['qiushibaike.com'] # 限制请求URL中的域(host)是否允许下载
start_urls = ['https://www.qiushibaike.com/text/'] # 起始请求的url资源列表
BASE_URL = 'https://www.qiushibaike.com'
def parse(self, response: HtmlResponse):
# 获取所有文章
for article_div_selector in response.css('.article'):
author_item = article_div_selector.css('.author img')[0].attrib
author_item['name'] = author_item.pop('alt')
author_item['detail_href'] = article_div_selector.css('.contentHerf::attr("href")').get()
# yield author_item
# 发起详情的请求
# Request()中的meta属性可以向下一个解析函数传递数据(元数据)
# 注意:meta是dict字典格式,value不能是一个引用对象 (scrapy 1.5版本)
# Request()中的priority 请求在scheduler调度器中的优先级,值越高,级别越高,则优先下载
# Request()中的dont_filter为False表示过滤重复下载的请求,为True则不过滤
yield Request(self.BASE_URL + author_item['detail_href'],
callback=self.parse_detail,
headers={
'Referer': response.url},
cookies=cookie_.get_cookies(),
meta={
'author': author_item['name'],
'author_head': author_item['src']},
priority=100,
dont_filter=False)
def parse_detail(self, response: Response):
# response.request
# print('parse_detail-->', response.request.meta)
# print('parse_detail-->', response.meta)
item = {
'author': response.meta['author'],
'author_head': response.meta['author_head']
}
item['title'] = response.css('.article-title::text').get()
item['publish_time'] = response.css('.stats-time::text').get()
item['content'] = '\n'.join([c.replace('\xa0', ' ')
for c in response.css('.content::text').getall()])
yield item
def parse_test(self, response: HtmlResponse):
# scrapy.http.response.html.HtmlResponse
# print(type(response), response)
# class: contentHerf ->a标签
# css()/xpath() -> 'scrapy.selector.unified.SelectorList
# scrapy.selector.SelectorList/Selector
# -> xpath()/css() 查询子孙元素
# -> get()/extract_first(),getall()/extract() 提取是Selector对象中的data属性
# -> attrib 属性方法, 只有Selector类实例里面存在(要求:选择的是元素不是元素的属性)
# css()方法中字符串:#id, .class, div, div>p, ::attr(‘属性名’), ::text 标签文本
a_elements: SelectorList = response.css('.contentHerf::attr("href")') # list[,...]
author_elements = response.css('.author').xpath('.//img')
for i, author_element in enumerate(author_elements):
item: dict = author_element.attrib # 拿到src和alt两个属性的dict
# 修改alt key的名称为 name
item['name'] = item.pop('alt')
item['detail_href'] = a_elements[i].get()
yield item
11.3.5. scrapy shell
终端调试工具:
- 终端输入scrapy shell "http://www.baidu.com" 在终端会得到一个response对象,可以直接使用
- response.xpath() 使用xpath路径查询特定元素,返回一个selector对象的列表
- response.css() 使用css_selector查询元素,返回一个selector对象
11.4. scrapy 持久化存储
11.4.1 基于终端命令存储
- 基于终端指令:
- 要求:只可以将parse方法的返回值存储到本地的文本文件中
- 注意:持久化存储对应的文本文件的类型只可以为:'json', 'jsonlines', 'jl', 'csv', 'xml', 'marshal', 'pickle
- 指令:scrapy crawl xxx -o filePath
- 好处:简介高效便捷
- 缺点:局限性比较强(数据只可以存储到指定后缀的文本文件中)
11.4.2. 基于管道持久化存储
- 基于管道:
- 编码流程:
- 数据解析
- 在item类中定义相关的属性
- 将解析的数据封装存储到item类型的对象
- 将item类型的对象提交给管道进行持久化存储的操作
- 在管道类的process_item中要将其接受到的item对象中存储的数据进行持久化存储操作
- 在配置文件中开启管道
- 好处:
- 通用性强。
11.4.3. 示例:
spider文件—> qiubai.py
import scrapy
from qiubaiPro.items import QiubaiproItem
class QiubaiSpider(scrapy.Spider):
name = 'qiubai'
# allowed_domains = ['www.xxx.com']
start_urls = ['https://www.qiushibaike.com/text/']
# 基于命令存储
# def parse(self, response):
# #解析:作者的名称+段子内容
# div_list = response.xpath('//div[@id="content-left"]/div')
# all_data = [] #存储所有解析到的数据
# for div in div_list:
# #xpath返回的是列表,但是列表元素一定是Selector类型的对象
# #extract可以将Selector对象中data参数存储的字符串提取出来
# # author = div.xpath('./div[1]/a[2]/h2/text()')[0].extract()
# author = div.xpath('./div[1]/a[2]/h2/text()').extract_first()
# #列表调用了extract之后,则表示将列表中每一个Selector对象中data对应的字符串提取了出来
# content = div.xpath('./a[1]/div/span//text()').extract()
# content = ''.join(content)
#
# dic = {
# 'author':author,
# 'content':content
# }
#
# all_data.append(dic)
#
#
# return all_data
def parse(self, response):
# 解析:作者的名称+段子内容
div_list = response.xpath('//div[@id="content-left"]/div')
all_data = [] # 存储所有解析到的数据
for div in div_list:
# xpath返回的是列表,但是列表元素一定是Selector类型的对象
# extract可以将Selector对象中data参数存储的字符串提取出来
# author = div.xpath('./div[1]/a[2]/h2/text()')[0].extract()
author = div.xpath('./div[1]/a[2]/h2/text() | ./div[1]/span/h2/text()').extract_first()
# 列表调用了extract之后,则表示将列表中每一个Selector对象中data对应的字符串提取了出来
content = div.xpath('./a[1]/div/span//text()').extract()
content = ''.join(content)
item = QiubaiproItem()
item['author'] = author
item['content'] = content
yield item # 将item提交给了管道
items.py 文件 —>在item类中定义相关的属性
import scrapy
class QiubaiproItem(scrapy.Item):
# define the fields for your item here like:
author = scrapy.Field()
content = scrapy.Field()
# pass
pipeline.py文件
import pymysql
class QiubaiproPipeline(object):
fp = None
#重写父类的一个方法:该方法只在开始爬虫的时候被调用一次
def open_spider(self,spider):
print('开始爬虫......')
self.fp = open('./qiubai.txt','w',encoding='utf-8')
#专门用来处理item类型对象
#该方法可以接收爬虫文件提交过来的item对象
#该方法没接收到一个item就会被调用一次
def process_item(self, item, spider):
author = item['author']
content= item['content']
self.fp.write(author+':'+content+'\n')
return item # 就会传递给下一个即将被执行的管道类
def close_spider(self,spider):
print('结束爬虫!')
self.fp.close()
#管道文件中一个管道类对应将一组数据存储到一个平台或者载体中
class mysqlPileLine(object):
conn = None
cursor = None
def open_spider(self,spider):
self.conn = pymysql.Connect(host='116.85.7.220',
port=3307,
user='root',
password='root',
db='qiubai',
charset='utf8')
def process_item(self,item,spider):
self.cursor = self.conn.cursor()
try:
self.cursor.execute('insert into qiubai values("%s","%s")'%(item["author"],item["content"]))
self.conn.commit()
except Exception as e:
print(e)
self.conn.rollback()
return item
def close_spider(self,spider):
self.cursor.close()
self.conn.close()
settings.py文件中开启管道
# ...
ITEM_PIPELINES = {
'qiubaiPro.pipelines.QiubaiproPipeline': 300,
'qiubaiPro.pipelines.mysqlPileLine': 301,
#300表示的是优先级,数值越小优先级越高
}
# ...
【扩展】:
- 面试题:将爬取到的数据一份存储到本地一份存储到数据库,如何实现?
- 管道文件中一个管道类对应的是将数据存储到一种平台
- 爬虫文件提交的item只会给管道文件中第一个被执行的管道类接受
- process_item中的return item表示将item传递给下一个即将被执行的管道类
11.4.4. 全站数据爬取
- 基于Spider的全站数据爬取
- 就是将网站中某板块下的全部页码对应的页面数据进行爬取
- 需求:爬取校花网中的照片的名称
- 实现方式:
- 将所有页面的url添加到start_urls列表(不推荐)
- 自行手动进行请求发送(推荐)
- 手动请求发送:
- yield scrapy.Request(url,callback):callback专门用做于数据解析
示例:spider.py文件
import scrapy
class XiaohuaSpider(scrapy.Spider):
name = 'xiaohua'
# allowed_domains = ['www.xxx.com']
start_urls = ['http://www.521609.com/meinvxiaohua/']
#生成一个通用的url模板(不可变)
url = 'http://www.521609.com/meinvxiaohua/list12%d.html'
page_num = 2
def parse(self, response):
li_list = response.xpath('//*[@id="content"]/div[2]/div[2]/ul/li')
for li in li_list:
img_name = li.xpath('./a[2]/b/text() | ./a[2]/text()').extract_first()
print(img_name)
if self.page_num <= 11:
new_url = format(self.url%self.page_num)
self.page_num += 1
#手动请求发送:callback回调函数是专门用作于数据解析
yield scrapy.Request(url=new_url,callback=self.parse)
11.5. 请求传参
- 请求传参
- 使用场景:如果爬取解析的数据不在同一张页面中。(深度爬取)
- 需求:爬取boss的岗位名称,岗位描述
示例:boos.py文件
import scrapy
from bossPro.items import BossproItem
class BossSpider(scrapy.Spider):
name = 'boss'
# allowed_domains = ['www.xxx.com']
start_urls = ['https://www.zhipin.com/job_detail/?query=python&city=101010100&industry=&position=']
url = 'https://www.zhipin.com/c101010100/?query=python&page=%d'
page_num = 2
# 回调函数接受item
# 详情页数据解析
def parse_detail(self, response):
item = response.meta['item']
job_desc = response.xpath('//*[@id="main"]/div[3]/div/div[2]/div[2]/div[1]/div//text()').extract()
job_desc = ''.join(job_desc)
# print(job_desc)
item['job_desc'] = job_desc
yield item
# 解析首页中的岗位名称
def parse(self, response):
li_list = response.xpath('//*[@id="main"]/div/div[3]/ul/li')
for li in li_list:
item = BossproItem()
job_name = li.xpath('.//div[@class="info-primary"]/h3/a/div[1]/text()').extract_first()
item['job_name'] = job_name
# print(job_name)
detail_url = 'https://www.zhipin.com' + li.xpath('.//div[@class="info-primary"]/h3/a/@href').extract_first()
# 对详情页发请求获取详情页的页面源码数据
# 手动请求的发送
# 请求传参:meta={},可以将meta字典传递给请求对应的回调函数
yield scrapy.Request(detail_url, callback=self.parse_detail, meta={
'item': item})
# 分页操作
if self.page_num <= 3:
new_url = format(self.url % self.page_num)
self.page_num += 1
yield scrapy.Request(new_url, callback=self.parse)
11.4. 图片数据 Imagepipeline
- 图片数据爬取之ImagesPipeline
- 基于scrapy爬取字符串类型的数据和爬取图片类型的数据区别?
- 字符串:只需要基于xpath进行解析且提交管道进行持久化存储
- 图片:xpath解析出图片src的属性值。单独的对图片地址发起请求获取图片二进制类型的数据
- ImagesPipeline:
- 只需要将img的src的属性值进行解析,提交到管道,管道就会对图片的src进行请求发送获取图片的二进制类型的数据,
而且还会帮我们进行持久化存储。
- 使用流程:
- 数据解析(图片的地址)
- 将存储图片地址的item提交到制定的管道类
- 在管道文件中自定制一个基于ImagesPipeLine的一个管道类
- get_media_request
- file_path
- item_completed # 将item返回给下一个管道方法
- 在配置文件中:
- 指定图片存储的目录:IMAGES_STORE = './imgs_zhb'
- 指定开启的管道:自定制的管道类
示例:
需求:爬取站长素材中的高清图片
1、爬虫脚本(解析数据)img.py
import scrapy
from imgspro.items import ImgsproItem
class ImgSpider(scrapy.Spider):
name = 'img'
# allowed_domains = ['www.xxx.com']
start_urls = ['http://sc.chinaz.com/tupian/']
def parse(self, response):
div_list = response.xpath('//div[@id="container"]/div')
for div in div_list:
# 注意:使用伪装属性
src = div.xpath('./div/a/img/@src2').extract_first()
# print(src)
# 实例化item对象
item = ImgsproItem()
item['src'] = src
yield item # 提交item到管道
2、items.py 文件 —>在item类中定义相关的属性
import scrapy
class ImgsproItem(scrapy.Item):
# define the fields for your item here like:
src = scrapy.Field()
# pass
3、在管道文件中自定制一个基于ImagesPipeLine的一个管道类
pipeline.py文件
from scrapy.pipelines.images import ImagesPipeline
import scrapy
class ImgsPileLine(ImagesPipeline):
#就是可以根据图片地址进行图片数据的请求
def get_media_requests(self, item, info):
yield scrapy.Request(item['src'])
#指定图片存储的路径
def file_path(self, request, response=None, info=None):
imgName = request.url.split('/')[-1]
return imgName
def item_completed(self, results, item, info):
return item #返回给下一个即将被执行的管道类
4、在配置文件中:
settings.py文件
...
LOG_LEVEL = 'ERROR'
USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko)
# Obey robots.txt rules
ROBOTSTXT_OBEY = False
...
ITEM_PIPELINES = {
'imgsPro.pipelines.ImgsPileLine': 300,
}
...
#指定图片存储的目录
IMAGES_STORE = './imgs_zhb'
11.5. 中间件
中间件
- 爬虫中间件
- 下载中间件【重要】
- 位置:引擎和下载器之间
- 作用:批量拦截到整个工程中所有的请求和响应
- 拦截请求:
- UA伪装:process_request
- 代理IP:process_exception:return request
- 拦截响应:
- 篡改响应数据,响应对象
- 需求:爬取网易新闻中的新闻数据(标题和内容)
- 1.通过网易新闻的首页解析出五大板块对应的详情页的url(没有动态加载)
- 2.每一个板块对应的新闻标题都是动态加载出来的(动态加载)
- 3.通过解析出每一条新闻详情页的url获取详情页的页面源码,解析出新闻内容
11.5.1. 拦截请求:
middlewares.py
from scrapy import signals
import random
class MiddleproDownloaderMiddleware(object):
# Not all methods need to be defined. If a method is not defined,
# scrapy acts as if the downloader middleware does not modify the
# passed objects.
# 定义一个'User-Agent'池
user_agent_list = [
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 "
"(KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
"Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 "
"(KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 "
"(KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 "
"(KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 "
"(KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 "
"(KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 "
"(KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 "
"(KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 "
"(KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"
]
# 定义两个代理
PROXY_http = [
'153.180.102.104:80',
'195.208.131.189:56055',
]
PROXY_https = [
'120.83.49.90:9000',
'95.189.112.214:35508',
]
#拦截请求
def process_request(self, request, spider):
#UA伪装
request.headers['User-Agent'] = random.choice(self.user_agent_list)
#为了验证代理的操作是否生效
request.meta['proxy'] = 'http://183.146.213.198:80'
return None
#拦截所有的响应
def process_response(self, request, response, spider):
# Called with the response returned from the downloader.
# Must either;
# - return a Response object
# - return a Request object
# - or raise IgnoreRequest
return response
#拦截发生异常的请求
def process_exception(self, request, exception, spider):
if request.url.split(':')[0] == 'http':
#代理
request.meta['proxy'] = 'http://'+random.choice(self.PROXY_http)
else:
request.meta['proxy'] = 'https://' + random.choice(self.PROXY_https)
return request #将修正之后的请求对象进行重新的请求发送
11.5.2. 拦截响应 :
需求:爬取网易新闻数据
wangyi.py文件
import scrapy
from selenium import webdriver
from wangyiPro.items import WangyiproItem
class WangyiSpider(scrapy.Spider):
name = 'wangyi'
# allowed_domains = ['www.cccom']
start_urls = ['https://news.163.com/']
models_urls = [] # 存储五个板块对应详情页的url
# 解析五大板块对应详情页的url
# 实例化一个浏览器对象
def __init__(self):
self.bro = webdriver.Chrome(executable_path=r'D:\01-soft\12-spider_chromedriver\chromedriver.exe')
def parse(self, response):
li_list = response.xpath('//*[@id="index2016_wrap"]/div[1]/div[2]/div[2]/div[2]/div[2]/div/ul/li')
alist = [3, 4, 6, 7, 8]
for index in alist:
model_url = li_list[index].xpath('./a/@href').extract_first()
self.models_urls.append(model_url)
# 依次对每一个板块对应的页面进行请求
for url in self.models_urls: # 对每一个板块的url进行请求发送
yield scrapy.Request(url, callback=self.parse_model)
# 每一个板块对应的新闻标题相关的内容都是动态加载
def parse_model(self, response): # 解析每一个板块页面中对应新闻的标题和新闻详情页
# response.xpath()
div_list = response.xpath('/html/body/div/div[3]/div[4]/div[1]/div/div/ul/li/div/div')
for div in div_list:
title = div.xpath('./div/div[1]/h3/a/text()').extract_first()
new_detail_url = div.xpath('./div/div[1]/h3/a/@href').extract_first()
item = WangyiproItem()
item['title'] = title
# 对新闻详情页的url发起请求
yield scrapy.Request(url=new_detail_url, callback=self.parse_detail, meta={
'item': item})
def parse_detail(self, response): # 解析新闻内容
content = response.xpath('//*[@id="endText"]//text()').extract()
content = ''.join(content)
item = response.meta['item']
item['content'] = content
yield item
# 退出浏览器
def closed(self, spider):
self.bro.quit()
middleware.py文件
from scrapy.http import HtmlResponse
from time import sleep
class WangyiproDownloaderMiddleware(object):
# Not all methods need to be defined. If a method is not defined,
# scrapy acts as if the downloader middleware does not modify the
# passed objects.
def process_request(self, request, spider):
# Called for each request that goes through the downloader
# middleware.
# Must either:
# - return None: continue processing this request
# - or return a Response object
# - or return a Request object
# - or raise IgnoreRequest: process_exception() methods of
# installed downloader middleware will be called
return None
#该方法拦截五大板块对应的响应对象,进行篡改
def process_response(self, request, response, spider):#spider爬虫对象
bro = spider.bro #获取了在爬虫类中定义的浏览器对象
#挑选出指定的响应对象进行篡改
#通过url指定request
#通过request指定response
if request.url in spider.models_urls:
bro.get(request.url) #五个板块对应的url进行请求
sleep(3)
page_text = bro.page_source #包含了动态加载的新闻数据
#response #五大板块对应的响应对象
#针对定位到的这些response进行篡改
#实例化一个新的响应对象(符合需求:包含动态加载出的新闻数据),替代原来旧的响应对象
#如何获取动态加载出的新闻数据?
#基于selenium便捷的获取动态加载数据
new_response = HtmlResponse(url=request.url,body=page_text,encoding='utf-8',request=request)
return new_response
else:
#response #其他请求对应的响应对象
return response
def process_exception(self, request, exception, spider):
# Called when a download handler or a process_request()
# (from other downloader middleware) raises an exception.
# Must either:
# - return None: continue processing this exception
# - return a Response object: stops process_exception() chain
# - return a Request object: stops process_exception() chain
pass
items.py文件
import scrapy
class WangyiproItem(scrapy.Item):
# define the fields for your item here like:
title = scrapy.Field()
content = scrapy.Field()
settings.py文件
# 打开下载中间件
...
DOWNLOADER_MIDDLEWARES = {
'wangyiPro.middlewares.WangyiproDownloaderMiddleware': 543,
}
11.5.3. 爬虫中间件
SpiderMiddleware
@classmethod
from_crawler(cls, crawler)
当创建了spider之后创建当前的中间件类实例
同时, 连接打开爬虫类的信号处理
process_spider_input(self, response, spider):
可以返回 None 和 raise Exception
返回None,表示放行,不拦截响应被解析
raise Exception 抛出异常,到达了process_spider_exception()方法中
process_spider_output(self, response, result, spider)
可以返回 item和request
默认: for r in result: yield r
process_spider_exception(self, response, exception, spider)
可以返回 None/Request/Item
process_start_requests(self, start_requests, spider)
必须返回Request
11.5.4. 下载中间件
process_request(self, request,spider):
返回对象 :None|Request|Response|raise IgnoreRequest
1. 可以返回哪些对象?? 返回None,继续处理这个请求,或者返回一个响应对象或者返回一个请求对象,或者或触发IgnoreRequest
2. 什么时候使用此函数 下载器向引擎返回响应的时候
process_response(self,request, response, spider)
1. 可以返回对象 Request|Response|raise IgnoreRequest
2. 使用场景是什么? 从下载器返回响应时调用
process_exception(self,request,exception, spider)
返回对象:None
11.6. 总结核心模块和类
scrapy.Spider 普通爬虫类的父类
- name 爬虫名, 在scrapy crawl 命令中使用
- start_urls 起始的请求URL资源列表
- allowed_domains 允许访问的服务器域名列表
- start_requests() 方法,爬虫启动后执行的第一个方法(流程中的第一步:发起请求)
- logger 当前爬虫的日志记录器
- parse() 默认请求成功后,对响应的数据默认解析的方法
scrapy.Spider 普通爬虫类的父类
- name 爬虫名称
- start ——urls 起始的请求URL资源列表
scrapy.Request
-初始化时参数: url, method, body, encoding, callback, headers, cookiesn dont_filter, priority, meta
- meta dict格式, 可以设置proxy 代理
scrapy.http.Response/TextResponse/HtmlResponse
- status 响应状态码
- meta 响应的原信息,包含request中的meta信息
- url 请求的URL
- request 请求对象
- headers 响应头
- body 字节数据
- text 文本数据
- css()/xpath() 提取HTML元素信息(基于lxml/bs4)
scrapy.Item 类, 类似于dict, 作用: 解析出不同结构的数据时,使用不同的Item类,便于数据管道处理。
scrapy.Filed 类,用于Item子类中声明字段属性(数据属性)
scrapy.signals 信号
- spider_opened 打开爬虫
- spider_closed 关闭爬虫
- spider_error 爬虫出现异常
优先级:
- 请求优先级: 值高 == 优先级大, 值低 == 优先级小
配置settings中的优先级
- 管道优先级: 值高 == 优先级小, 值低 == 优先级大
- 中间件优先级: 值高 == 优先级小, 值低 == 优先级大
十二、规则爬虫
crawlspider
CrawlSpider是一个类,它的父类就是scrapy.Spider,所以CrawlSpider不仅有Spider的功能,还有自己独有的功能
CrawlSpider可以定义规则,再解析html内容的时候,可以根据链接规则提取出指定的链接,然后再向这些链接发送请求,所以,如果有需要跟进链接的需求,就可以使用CrawlSpider来实现
12.1. 流程
- CrawlSpider:类,Spider的一个子类
- 全站数据爬取的方式
- 基于Spider:手动请求
- 基于CrawlSpider
- CrawlSpider的使用:
- 创建一个工程
- cd XXX
- 创建爬虫文件(CrawlSpider):
- scrapy genspider -t crawl xxx www.xxxx.com
- 链接提取器:
- 作用:根据指定的规则(allow)进行指定链接的提取
- 规则解析器:
- 作用:将链接提取器提取到的链接进行指定规则(callback)的解析
#需求:爬取sun网站中的编号,新闻标题,新闻内容,标号
- 分析:爬取的数据没有在同一张页面中。
- 1.可以使用链接提取器提取所有的页码链接
- 2.让链接提取器提取所有的新闻详情页的链接
#需求:爬取sun网站中的编号,新闻标题,新闻内容,标号
sun.py文件
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from sunPro.items import SunproItem,DetailItem
#需求:爬取sun网站中的编号,新闻标题,新闻内容,标号
class SunSpider(CrawlSpider):
name = 'sun'
# allowed_domains = ['www.xxx.com']
start_urls = ['http://wz.sun0769.com/index.php/question/questionType?type=4&page=']
#链接提取器:根据指定规则(allow="正则")进行指定链接的提取
link = LinkExtractor(allow=r'type=4&page=\d+')
link_detail = LinkExtractor(allow=r'question/\d+/\d+\.shtml')
rules = (
#规则解析器:将链接提取器提取到的链接进行指定规则(callback)的解析操作
Rule(link, callback='parse_item', follow=True),
#follow=True:可以将链接提取器 继续作用到 连接提取器提取到的链接 所对应的页面中
Rule(link_detail,callback='parse_detail')
)
#http://wz.sun0769.com/html/question/201907/421001.shtml
#http://wz.sun0769.com/html/question/201907/420987.shtml
#解析新闻编号和新闻的标题
#如下两个解析方法中是不可以实现请求传参!
#如法将两个解析方法解析的数据存储到同一个item中,可以以此存储到两个item
def parse_item(self, response):
#注意:xpath表达式中不可以出现tbody标签
tr_list = response.xpath('//*[@id="morelist"]/div/table[2]//tr/td/table//tr')
for tr in tr_list:
new_num = tr.xpath('./td[1]/text()').extract_first()
new_title = tr.xpath('./td[2]/a[2]/@title').extract_first()
item = SunproItem()
item['title'] = new_title
item['new_num'] = new_num
yield item
#解析新闻内容和新闻编号
def parse_detail(self,response):
new_id = response.xpath('/html/body/div[9]/table[1]//tr/td[2]/span[2]/text()').extract_first()
new_content = response.xpath('/html/body/div[9]/table[2]//tr[1]//text()').extract()
new_content = ''.join(new_content)
# print(new_id,new_content)
item = DetailItem()
item['content'] = new_content
item['new_id'] = new_id
yield item
items.py
import scrapy
class SunproItem(scrapy.Item):
# define the fields for your item here like:
title = scrapy.Field()
new_num = scrapy.Field()
class DetailItem(scrapy.Item):
new_id = scrapy.Field()
content = scrapy.Field()
pipeline.py
class SunproPipeline(object):
def process_item(self, item, spider):
#如何判定item的类型
#将数据写入数据库时,如何保证数据的一致性
if item.__class__.__name__ == 'DetailItem':
print(item['new_id'],item['content'])
pass
else:
print(item['new_num'],item['title'])
return item
settings.py
...
ITEM_PIPELINES = {
'sunPro.pipelines.SunproPipeline': 300,
}
...
十三、分布式爬虫
- 分布式爬虫
- 概念:我们需要搭建一个分布式的机群,让其对一组资源进行分布联合爬取。
- 作用:提升爬取数据的效率
- 如何实现分布式?
- 安装一个scrapy-redis的组件
- 原生的scarapy是不可以实现分布式爬虫,必须要让scrapy结合着scrapy-redis组件一起实现分布式爬虫。
- 为什么原生的scrapy不可以实现分布式?
- 调度器不可以被分布式机群共享
- 管道不可以被分布式机群共享
- scrapy-redis组件作用:
- 可以给原生的scrapy框架提供可以被共享的管道和调度器
- 实现流程
- 创建一个工程
- 创建一个基于CrawlSpider的爬虫文件
- 修改当前的爬虫文件:
- 导包:from scrapy_redis.spiders import RedisCrawlSpider
- 将start_urls和allowed_domains进行注释
- 添加一个新属性:redis_key = 'sun' 可以被共享的调度器队列的名称
- 编写数据解析相关的操作
- 将当前爬虫类的父类修改成RedisCrawlSpider
- 修改配置文件settings
- 指定使用可以被共享的管道:
ITEM_PIPELINES = {
'scrapy_redis.pipelines.RedisPipeline': 400
}
- 指定调度器:
# 增加了一个去重容器类的配置, 作用使用Redis的set集合来存储请求的指纹数据, 从而实现请求去重的持久化
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 使用scrapy-redis组件自己的调度器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 配置调度器是否要持久化, 也就是当爬虫结束了, 要不要清空Redis中请求队列和去重指纹的set。如果是True, 就表示要持久化存储, 就不清空数据, 否则清空数据
SCHEDULER_PERSIST = True
- 指定redis服务器:
- redis相关操作配置:
- 配置redis的配置文件:
- linux或者mac:redis.conf
- windows:redis.windows.conf
- 代开配置文件修改:
- 将bind 127.0.0.1进行删除
- 关闭保护模式:protected-mode yes改为no
- 结合着配置文件开启redis服务
- redis-server 配置文件
- 启动客户端:
- redis-cli
- 执行工程:
- scrapy runspider xxx.py
- 向调度器的队列中放入一个起始的url:
- 调度器的队列在redis的客户端中
- lpush xxx www.xxx.com
- 爬取到的数据存储在了redis的proName:items这个数据结构中
示例:
day10 —> dm530项目
存储MondoDB
—>NoSQL仓库.xmind
十四、增量式爬虫
增量式爬虫
- 概念:监测网站数据更新的情况,只会爬取网站最新更新出来的数据。
- 分析:
- 指定一个起始url
- 基于CrawlSpider获取其他页码链接
- 基于Rule将其他页码链接进行请求
- 从每一个页码对应的页面源码中解析出每一个电影详情页的URL
- 核心:检测电影详情页的url之前有没有请求过
- 将爬取过的电影详情页的url存储
- 存储到redis的set数据结构
- 对详情页的url发起请求,然后解析出电影的名称和简介
- 进行持久化存储
movie.py
# -*- coding: utf-8 -*-
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from redis import Redis
from moviePro.items import MovieproItem
class MovieSpider(CrawlSpider):
name = 'movie'
# allowed_domains = ['www.ccc.com']
start_urls = ['https://www.4567tv.tv/frim/index1.html']
rules = (
Rule(LinkExtractor(allow=r'/frim/index1-\d+\.html'), callback='parse_item', follow=True),
)
# 创建redis链接对象
conn = Redis(host='127.0.0.1', port=6379)
#用于解析每一个页码对应页面中的电影详情页的url
def parse_item(self, response):
li_list = response.xpath('/html/body/div[1]/div/div/div/div[2]/ul/li')
for li in li_list:
# 获取详情页的url
detail_url = 'https://www.4567tv.tv' + li.xpath('./div/a/@href').extract_first()
# 将详情页的url存入redis的set中
ex = self.conn.sadd('urls', detail_url)
if ex == 1:
print('该url没有被爬取过,可以进行数据的爬取')
yield scrapy.Request(url=detail_url, callback=self.parst_detail)
else:
print('数据还没有更新,暂无新数据可爬取!')
# 解析详情页中的电影名称和类型,进行持久化存储
def parst_detail(self, response):
item = MovieproItem()
item['name'] = response.xpath('/html/body/div[1]/div/div/div/div[2]/h1/text()').extract_first()
item['desc'] = response.xpath('/html/body/div[1]/div/div/div/div[2]/p[5]/span[2]//text()').extract()
item['desc'] = ''.join(item['desc'])
yield item
pipelines.py
class MovieproPipeline(object):
conn = None
def open_spider(self,spider):
self.conn = spider.conn
def process_item(self, item, spider):
dic = {
'name':item['name'],
'desc':item['desc']
}
# print(dic)
self.conn.lpush('movieData',dic)
return item