多任务爬虫之协程


前言

Python中协程的使用


一、多任务爬虫之协程

简介:Python语言弥补多进程的GIL(全局解释器锁)缺陷,借助于Linux中的非阻塞模式(I/O多路复用,包含三种模式:轮询select、事件回调poll、增强型的事件回调epoll),开发出异步非阻塞的框架。

  1. 协程(又称之为微线程),只需要一个线程即可,同时由用户自己调度。Python高质量代码中,包含协程、上下文、装饰器和生成器。
  2. 协程涉及的库,Python内置了asyncio模块,第三方库包含gevent和eventlet
  3. 协程是从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模块:
  1. 将函数升级为协程函数,使用装饰器(@asyncio.coroutine)装饰函数
  2. 在协程中调用其他协程,通过yield from调用,yield from的作用是将当前协程挂起,等待被调用的协程完成任务
  3. asyncio.sleep() 协程的时间阻塞
  • 方法二:使用async和await关键字
  1. 把async放在def定义函数前,将函数升级为协程函数
  2. 在协程中调用其他协程,通过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("执行完毕~~~")

协程启动的流程:

  1. 创建协程对象,将URL传入协程函数,生成一个协程对象(任务task),只有在事件模型中才能运行
  2. 获取事件模型对象loop = asyncio.get_event_loop()
  3. 运行事件模型对象,loop.run_until_complete(task),但是一次只能运行一个任务
  4. 使用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)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值