前言
Python中协程的使用
一、多任务爬虫之协程
简介:Python语言弥补多进程的GIL(全局解释器锁)缺陷,借助于Linux中的非阻塞模式(I/O多路复用,包含三种模式:轮询select、事件回调poll、增强型的事件回调epoll),开发出异步非阻塞的框架。
- 协程(又称之为微线程),只需要一个线程即可,同时由用户自己调度。Python高质量代码中,包含协程、上下文、装饰器和生成器。
- 协程涉及的库,Python内置了asyncio模块,第三方库包含gevent和eventlet
- 协程是从Python3开始的,最早使用yield和send关键字,从Python3.4开始出现asyncio,从Python3.5版本后出现async和await关键字。
二、使用步骤
1.实现一个简单的协程
协程的创建(示例):
import asyncio
@asyncio.coroutine
def download(url):
print("开始下载",url)
# 调用其他协程,使用yield from,作用是将当前协程挂起等待被调用的协程完成任务
yield from asyncio.sleep(2)
print(f"{url}下载完成")
yield from parse(f"解析开始{url}")
@asyncio.coroutine
def parse(response):
print(response)
yield from asyncio.sleep(1)
yield from item_piprline(f"{response[4:]}开始数据存储")
@asyncio.coroutine
def item_piprline(item):
"""
数据存储
:param item:
:return:
"""
print(f"{item}开始存储")
yield from asyncio.sleep(2)
print(f"{item}存储完成")
在多任务的软件开发环境(SDK)下,使用协程的流程:
- 方法一:使用asyncio模块:
- 将函数升级为协程函数,使用装饰器(@asyncio.coroutine)装饰函数
- 在协程中调用其他协程,通过yield from调用,yield from的作用是将当前协程挂起,等待被调用的协程完成任务
- asyncio.sleep() 协程的时间阻塞
- 方法二:使用async和await关键字
- 把async放在def定义函数前,将函数升级为协程函数
- 在协程中调用其他协程,通过await调用
协程的启动(示例):
if __name__ == '__main__':
urls = ['http://www.baidu.com','http://jd.com','http://taobao.com']
# download(url)调用之后不会立即调用函数而是生成一个协程对象
tasks = [download(url) for url in urls]
loop = asyncio.get_event_loop()
# run_until_complete()参数是一个协程对象,等待协程执行完成
# wait()返回一个协程对象,参数可以接受多个协程对象
loop.run_until_complete(asyncio.wait(tasks))
print("执行完毕~~~")
协程启动的流程:
- 创建协程对象,将URL传入协程函数,生成一个协程对象(任务task),只有在事件模型中才能运行
- 获取事件模型对象loop = asyncio.get_event_loop()
- 运行事件模型对象,loop.run_until_complete(task),但是一次只能运行一个任务
- 使用asyncio.wait()将多个协程对象接受,并返回一个协程对象
2.协程爬虫实战
代码优化如下(示例):
import asyncio
import json
import requests
import xlwt
from lxml import etree
from db import DB
from argparse_2 import parse_args # 从命令行获取文件名等信息
from url_screen import url_screen
headers = {'User-Agent':"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36"}
base_url = ""
is_repeat = {}
# 请求所有分类url
async def get(url):
try:
resp = requests.get(url,headers=headers,timeout=10)
# 如果有异常,间隔两秒之后继续请求
except:
await asyncio.sleep(2)
await get(url)
# 请求无异常
else:
if resp.status_code == 200:
resp.encoding = 'gb2312'
await parse(resp)
else:
print(f"{url}请求失败",resp.status_code)
# 首次请求,获取分类url
async def run_spider(url):
print("开始下载",url)
# 任务一:从任意一个url中提取base_url(包含协议端口等)
global base_url
# 声明全局变量,方便后续使用
base_url = url_screen(url)
resp = requests.get(url,headers=headers)
if resp.status_code == 200:
# 网站编码格式为gb2312
resp.encoding="gb2312"
html = etree.HTML(resp.text)
rt = html.xpath("//div[@class='topmenu cbody']/ul/li/a")
for a_node in rt[2:]:
# 获取href链接
cate_url = a_node.get("href")
# 拼接base_url(schema+hostname+port)
await get(base_url+cate_url)
# 解析
async def parse(response: requests.Response):
print(response.url,'开始解析')
bs = etree.HTML(response.text)
a = bs.xpath("//div[@class='newslist']/dl/dt/a")
# 进入到详情页面的标识是页面出现newsview类名
if bs.xpath("//div[@class='newsview']"):
info = {}
# 标题标签
title_tag = bs.xpath("//div[@class='title']")[0]
info['title'] = title_tag.text
# 内容标签
content_tag = bs.xpath("string(//div[@class='content'])")
content = content_tag
info["content"] = content
# 调用保存协程函数
await item_piprline(info)
# 将第二页的连接继续请求和解析
for a_node in a:
await get(base_url + a_node.get("href"))
async def item_piprline(item):
"""
数据存储
:param item: 解析后的字典
:return:None
"""
# 写入到mysql库中
# 写入到.json文件中
args = parse_args()
# 判断保存数据文件的格式
if args.outfile:
filename = args.outfile
if filename.endswith(".json"):
await json_save(item,filename)
elif any((filename.endswith(".xls"),filename.endswith(".xlsx"))):
await xls_save(item,filename)
else:
raise Exception("仅支持json和xls/xlsx")
async def mysql_save(item):
db = DB()
# 存储json文件
async def json_save(item,filename):
# open 函数打开的流对象可以使用with管理上下文
# 当进入是调用流对象的__enter__()方法,返回流对象本身,在外部用as关键字指定接收的变量名
# 退出时调用流对象的__exit__()方法
if item["title"] not in is_repeat:
with open(filename,"a",encoding="utf-8") as f:
f.write(json.dumps(item,ensure_ascii=False))
is_repeat.update(item["title"])
else:
pass
# 存储xls文件
async def xls_save(item,filename):
# 创建workbook对象
workbook = xlwt.Workbook()
# 添加一个工作表
sheet = workbook.add_sheet("笑话")
headers = ["title","content"]
for index,header in enumerate(headers):
sheet.write(0,index,header)
row = sheet.nrows + 1
for col,key in enumerate(item):
sheet.write(row,col,item[key])
workbook.save("xiaohua.xls")
代码执行如下(示例):
if __name__ == '__main__':
start_spider = "http://www.haha56.net/"
task = [run_spider(start_spider)]
loop = asyncio.get_event_loop()
# run_until_complete()参数是一个协程对象,等待协程执行完成
# wait()返回一个协程对象,参数可以接受多个协程对象
loop.run_until_complete(asyncio.wait(task))
print("执行完毕~~~")
重构函数名快捷键shift + F6,修改后,同步所有函数名称
从命令行获取文件名的脚本
from argparse import ArgumentParser
def parse_args():
parser = ArgumentParser()
parser.add_argument("-o",dest="outfile",help="指定输出文件位置,目前文件格式仅支持json和excel")
parser.add_argument("--logfile",help="指定日志文件",default="spider.log")
parser.add_argument("-l",dest="level",default="INFO",help="指定日志的等级")
return parser.parse_args()
连接数据库类
from pymysql import Connect
from pymysql.cursors import DictCursor,Cursor
CONFIG = {
"host":"localhost",
"port":3306,
"user":"root",
"password":"root",
"db":"mydb1",
"charset":"utf8",
"cursorclass":DictCursor
}
class DBConnect():
def __init__(self,**kwargs):
if kwargs:
# 使用kwargs非空的字典更新数据库连接的配置
CONFIG.update(kwargs)
self.__conn = Connect(**CONFIG)
# 使用with管理上下文进行创建游标
def __enter__(self):
return self.__conn.cursor()
def __exit__(self, exc_type, exc_val, exc_tb):
"""
:param exc_type: 异常类型
:param exc_val: 异常信息
:param exc_tb: 异常追踪
:return:
"""
if exc_type:
self.__conn.rollback()
print("--数据库操作异常--")
else:
self.__conn.commit()
# True内部处理异常,False由外部处理异常,True程序会接着向下执行
return True
# 关闭数据库
def close(self):
if self.__conn:
self.__conn.close()
class DB():
def __init__(self):
self.conn = DBConnect()
print("连接成功")
def create_table(self,table,*fields):
"""
:param table: 表名
:param fields: 字段描述,如(name(ID),type_or_length(varchar(20)),constraint(not null))
:return:
"""
fields_str = ",".join([" ".join(field) for field in fields])
sql = "create table %s(%s)" % (table,fields_str)
print(">>>",sql)
with self.conn as cursor:
cursor.execute(sql)
print(f"{table}创建成功")
def insert(self,table,**item):
"""
:param table: 表名
:param item: 插入记录,如(title="karsa",content="good")
:return:
"""
# "%(name)s,%(title)s"%{"name":"ddd","title":"very"} 括号里写字典中的key(字典格式化)
# 用逗号连接key(字段名)
field = ",".join(item)
print(field)
# 使用字典格式化
value = ",".join(["%%(%s)s" % key for key in item])
sql = "insert into %s(%s) values(%s)" %(table,field,value)
print(sql)
with self.conn as c:
# args可以是tuple 即表示位置替换sql中的%s
# args也可是dict,即表示按字典的key 的value替换sql中的%(key)s
c.execute(sql,args=item)
print("插入成功")
def query(self,table,*fields,where=None,args=None):
field = ",".join(fields) if fields else "*"
sql = f"select {field} from {table}"
if where:
sql += " "+ where
ret = None
with self.conn as c:
c.execute(sql,args=args)
ret = c.fetchall()
return ret
if __name__ == '__main__':
db = DB()
# 数据库操作
# db.create_table("xiaohua",("title","varchar(80)"),("content","text"))
# db.insert("xiaohua",title="karsa",content="good")
ret = db.query("tb_book","count(*)")
print(ret)