# 网络数据采集 (爬虫) —— 并发和并行编程(5)

网络数据采集(爬虫) —— 并发和并行编程(5)

Python中的并发编程

Python中的并发编程:多线程,多进程(用的比较少),异步I/O

并发(concurrency):加入程序中有两个需要执行的部分A和B,通过A和B的轮流切换,达到让外界感到A和B同时执行的效果。(短时间快速切换)

并行(parallel):程序中有多个部分同时执行,前提条件是计算机需要有多个CPU或者CPU有多核

进程:启动一个程序,通常就启动了一个或多个进程,进程是操作系统分配内存的基本单位。

在这里插入图片描述

线程:一个进程通常包括一个或多个线程,线程是比进程更小的单元,也是操作系统分配CPU的基本单位

python test.py ----->启动一个python解释器进程,该进程中只有唯一的一个主线程

requieriments.txt -------->依赖项清单

生成依赖项清单:在Terminal 中输入 pip freeze > requierments.txt

根据依赖项清单生成依赖项:pip install -r requierments.txt

查看依赖项清单:pip list

检查依赖项冲突:pip check

1.不使用多线程的场景

import random
import time


def download(filename):
    start = time.time()
    print(f'开始下载{filename}.')
    time.sleep(random.randint(5, 8))
    print(f'{filename}下载完成.')
    end = time.time()
    print(f'下载{filename}花费时间: {end - start:.3f}秒')


def main():
    start = time.time()
    download('Python从入门到住院.pdf')
    download('MySQL从删库到跑路.avi')
    download('Linux从精通到放弃.mp3')
    end = time.time()
    print(f'总共花费时间: {end - start:.3f}秒')


if __name__ == '__main__':
    main()

"""
开始下载Python从入门到住院.pdf.
Python从入门到住院.pdf下载完成.
下载Python从入门到住院.pdf花费时间: 7.013秒
开始下载MySQL从删库到跑路.avi.
MySQL从删库到跑路.avi下载完成.
下载MySQL从删库到跑路.avi花费时间: 8.010秒
开始下载Linux从精通到放弃.mp3.
Linux从精通到放弃.mp3下载完成.
下载Linux从精通到放弃.mp3花费时间: 6.010秒
总共花费时间: 21.033秒
"""

# 一个程序运行完才能运行下一个程序,耗费时间长,效率低

2.使用多线程— 将下载任务派发到多个线程中执行

~ 多线程
Thread(target=…, args=(…),kwargs={…}, daemon= True)

​ -----> start():启动线程

​ ------>join():等待线程结束

​ ------> is_alive():线程是否还在运行

​ ------->terminata():终止线程

import random
import time
from threading import Thread
# 首先导入

def download(filename):
    start = time.time()
    # 启动线程
    print(f'开始下载{filename}.')
    time.sleep(random.randint(3, 8))
    print(f'{filename}下载完成.')
    end = time.time()
    print(f'下载{filename}花费时间: {end - start:.3f}秒')


def main():
    start = time.time()
    # 创建线程对象,通过target和args属性指定线程要执行的代码及对应的参数
    # target属性,目标的意思,args参数可能有一个参数也可能是多个,因此用元组
    t1 = Thread(target=download, args=('Python从入门到住院.pdf',))
    # 通过线程给对象发出start消息启动线程(调用start()方法)
    t1.start()
    t2 = Thread(target=download, args=('MySQL从删库到跑路.avi',))
    t2.start()
    t3 = Thread(target=download, args=('Linux从精通到放弃.mp3',))
    t3.start()
    # 要等三个线程执行结束之后进行主线程
    # 通过给线程对象发出join消息等待线程执行结束(调用join()方法)
    t1.join()
    t2.join()
    t3.join()
    end = time.time()
    print(f'总共花费时间: {end - start:.3f}秒')


if __name__ == '__main__':
    main()

"""
开始下载Python从入门到住院.pdf.开始下载MySQL从删库到跑路.avi.

开始下载Linux从精通到放弃.mp3.
Linux从精通到放弃.mp3下载完成.
下载Linux从精通到放弃.mp3花费时间: 4.005秒
MySQL从删库到跑路.avi下载完成.
Python从入门到住院.pdf下载完成.下载MySQL从删库到跑路.avi花费时间: 8.013秒

下载Python从入门到住院.pdf花费时间: 8.014秒
总共花费时间: 8.015秒

Process finished with exit code 0
"""


import random
import time
from threading import Thread

# 定义函数时可以在参数列表中使用*作为一个分隔
# *前面的参数叫做位置参数,传参时只需要对号入座即可
# *后面的参数是是命名关键字参数,传参时要写成”参数名=参数值“的形式


def download(*, filename):
    start = time.time()
    print(f'开始下载{filename}.')
    time.sleep(random.randint(3, 8))
    print(f'{filename}下载完成.')
    end = time.time()
    print(f'下载{filename}花费时间: {end - start:.3f}秒')


def main():
    start = time.time()
    # 创建线程对象,通过target和args属性指定线程要执行的代码及对应的参数
    # target属性,目标的意思,args参数可能有一个参数也可能是多个,因此用元组
    # 如果函数的参数是位置参数,可以通过args属性指定参数值
    # 如果函数的参数是关键字参数,传参时要写成”参数名=参数值“的形式
    t1 = Thread(target=download, kwargs={'filename': 'Python从入门到住院.pdf'})
    # 通过线程给对象发出start消息启动线程(调用start()方法)
    t1.start()
    t2 = Thread(target=download, kwargs={'filename': 'MySQL从删库到跑路.avi'})
    t2.start()
    t3 = Thread(target=download, kwargs={'filename': 'Linux从精通到放弃.mp3'})
    t3.start()
    # 通过给线程对象发出join消息等待线程执行结束(调用join()方法)
    # 主线程中调用了三个线程的join方法,目的是让主线程等待三个线程结束
    t1.join()
    t2.join()
    t3.join()
    end = time.time()
    print(f'总共花费时间: {end - start:.3f}秒')


if __name__ == '__main__':
    main()

~ 继承Thread类, 重写run()方法

子类在继承父类的过程中,可以对父类已有的方法给出新的实现版本(重新实现父类方法)
这个动作就称为重写(override)
自定义线程类:继承Thread类,重写run方法(指定线程要执行的任务)

import random
import time

from threading import Thread


class DownloadThread(Thread):

    def __init__(self, filename):
        self.filename = filename
        super(DownloadThread, self).__init__()

    def run(self) -> None:
        start = time.time()
        print(f'开始下载{self.filename}.')
        time.sleep(random.randint(3, 8))
        print(f'{self.filename}下载完成.')
        end = time.time()
        print(f'下载{self.filename}花费时间: {end - start:.3f}秒')


def main():
    start = time.time()
    threads = [
        DownloadThread('Python从入门到住院.pdf'),
        DownloadThread('MySQL从删库到跑路.avi'),
        DownloadThread('Linux从精通到放弃.mp3')
    ]
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    end = time.time()
    print(f'总共花费时间: {end - start:.3f}秒')


if __name__ == '__main__':
    main()

多线程程序对其他程序来说并不友好
操作系统创作和释放线程也有开销,最好的使用方式是创建好一个线程重复使用他,不用的时候留作备用

import requests
import time
from threading import Thread


def download_picture(url):
    resp = requests.get(url)
    filename = url[url.rfind('/') + 1:]
    with open(f'../image360/{filename}', 'wb') as file:
        file.write(resp.content)
    time.sleep(0.5)


def main():
    resp = requests.get(url='https://image.so.com/zjl?ch=beauty&sn=0')
    data_dict = resp.json()
    threads = []
    start = time.time()
    for image_dict in data_dict['list']:
        url = image_dict['qhimg_url']
        # download_picture(url)
        t = Thread(target=download_picture, args=(url,))
        threads.append(t)
        t.start()
    for t in threads:
        t.join()
    end = time.time()
    print(f'执行时间:{end- start:.3f}秒')

if __name__ == '__main__':
    main()

~ 线程池:ThreadPoolExecutor(max_workers=…)----->submit(…)

​ -------> Future (返回值将来时对象)------> result()
threading.current_thread () ------> 获取当前正在执行的线程对象
创建和使用线程的正确姿势————创建线程后,要实现对线程对象的重复利用
不要频繁的创建和释放线程,因为这两个操作本身也是比较大的开销

在进行商业项目开发时,如果是多线程,基本都会使用一种叫做线程池的技术
线程池  ————> 一个容器,提前放置好若干个线程,用的时候直接从线程池获取线程,
用完了之后,将线程对象交还给线程池,使用线程的过程中没有创建和释放的开销,
这是一种典型的空间换时间的技术。
import random
import requests
import time
from threading import Thread
from concurrent.futures.thread import ThreadPoolExecutor
import threading


def download_picture(url):
    # threading.currentThread()函数获取当前正在执行的线程对象
    print(threading.currentThread().name)
    resp = requests.get(url)
    filename = url[url.rfind('/') + 1:]
    with open(f'../image360/{filename}', 'wb') as file:
        file.write(resp.content)
    return random.randint(0,100)


def main():
    resp = requests.get(url='https://image.so.com/zjl?ch=beauty&sn=0')
    data_dict = resp.json()
    futures = []
    with ThreadPoolExecutor(max_workers=16) as pool:
        start = time.time()
        for image_dict in data_dict['list']:
            url = image_dict['qhimg_url']
            # 线程池的submit方法会返回一个Future对象
            f = pool.submit(download_picture, url)
            futures.append(f)
        # 等待线程池中所有的线程执行结束
        for f in futures:
            f.result()
        end = time.time()
        print(f'执行时间:{end- start:.3f}秒')
        pool.shutdown()


if __name__ == '__main__':
    main()

—— 守护线程:不值得保留,主线程结束即使守护线程还没执行完,也就不再执行了,主线程一结束,守护线程也要停下来

import time
from threading import Thread


def display(message):
    # 死循环,程序不会停下来
    while True:
        print(message, end='')
        time.sleep(0.0001)

def main():
    # 通过Thread类初始化方法的daemon参数可以设置守护线程
    Thread(target=display, args=('ping', )).start()
    Thread(target=display, args=('pong', )).start()
    time.sleep(5)
操作系统的输入输出缓冲区
Ctrl + D 直接复制一段代码
Ctrl + Y 直接删除一段代码
想要查看程序的真实过程,在Terminal里面输入python 文件名.py(python example_4

练习:

1.定义类 ---> 银行账户  ---> 账户余额/存钱
2.创建一个银行账户对象,启动100个线程,每个线程向该用户存入一元钱
3.查询账户余额
import time
# 重入锁
from threading import Thread, RLock


class BankAccount(Thread):
    """银行账户"""
    def __init__(self):
        self.balance = 0
        self.lock = RLock()
        super(BankAccount, self).__init__()


    def save_money(self, amount):
        """存钱"""
        # 将锁对象放在上下文语法中,进入上下文时,自动执行锁对象的acquire方法(获得锁)
        # 离开上下文时(不管正常还是异常),都会自动执行锁对象的release方法(释放锁)
        with self.lock:
            # 想存钱必须先获得锁
            # self.lock.acquire()
            new_balance = self.balance + amount
            # 用休眠民业务受理需要10毫秒时间
            time.sleep(0.01)
            self.balance = new_balance
            # 存钱完成之后必须释放锁
            # self.lock.release()


def main():
    c1 = BankAccount()
    threads = []
    start = time.time()
    # 创建100个进程,每个线程想账户中转入1元
    for _ in range(100):
        t = Thread(target=c1.save_money, args=(1, ))
        threads.append(t)
        t.start()
    for t in threads:
        t.join()
    end = time.time()
    print(f'账户余额:{c1.balance}')
    print(f'执行时间:{end - start}秒')


if __name__ == '__main__':
    main()

—— 多线程竞争一个资源 ------>线程不安全对象 ------> Rlock

​ ------> acquire() / release()

​ -------> with lock:

—— GIL ------> Global Inperpreter Lock (全局解释器锁)

​ – python解释器是CPython用C语言写的

--malloc()/free()

--只有获得GIL的线程能够执行--> 无法发挥CPU多核的优势

—— CPython中多线程有没有用?

--有用

--提高程序对CPU的占用率

--改善用户体验(程序不会因为一个地方的阻塞陷入假死状态)

--I/O密集型任务(需要太多的CPU处理,主要I/O操作),多线程足以胜任,也用不上多核特性

--计算密集型任务(处理渲染图象、压缩文件、视频解码、音视频压缩解压缩)可以使用多进程

3. 多进程

—— Process类, 用法跟THread类类似

—— ProcessPoolExecuter ------> 进程池

使用多进程时,多个进程间互相协商需要使用IPC机制,比较复杂,多在执行计算密集型任务时使用

—— IPC(Inter-Process Commounication)机制 ——>管道 / 套接字 / 共享存储区

from multiprocessing import Process
import time


def display(message):
    # 死循环,程序不会停下来
    while True:
        print(message, end='')
        time.sleep(0.01)


def main():
    # 通过Thread类初始化方法的daemon参数可以设置守护线程
    #
    Process(target=display, args=('ping', ), daemon=True).start()
    Process(target=display, args=('pong', ), daemon=True).start()
    Process(target=display, args=('hello', ), daemon=True).start()
    Process(target=display, args=('goodbye', ), daemon=True).start()
    time.sleep(5)


if __name__ == '__main__':
    main()

4.异步编程,异步I/o

IO在计算机中指Input/Output,也就是输入和输出。由于程序和运行时数据是在内存中驻留,由CPU这个超快的计算核心来执行,涉及到数据交换的地方,通常是磁盘、网络等,就需要IO接口。

几种常见的I/O模式:

—— BIO – 阻塞式IO(IO即input/output,阻塞式IO指的是“一旦输入/输出工作没有完成,则程序阻塞,直到输 入/输出工作完成”。)

一个壶烧水,必须等水烧开

—— NIO – 非阻塞式IO(非阻塞式IO其实也并非完全非阻塞,通常都是通过设置超时来读取数据的。未超时之前,程序阻塞在读写函数上;超时后,结束本次读取,将已读到的数据返回。通过不断循环读取,就能够读到完整数据了。如果多次连续超时读到空数据的话,则可以断开。)

多个壶烧水(多路IO操作),轮流观察哪个水烧开了

—— AIO – 异步IO

CPU不等待,只是告诉磁盘,“您老慢慢写,不着急,我接着干别的事去了”,于是,后续代码可以立刻接着执行,这种模式称为异步IO

多个壶烧水,你可以去干别的事情,哪个壶烧开了就发出声响

使用异步IO来编写程序性能会远远高于同步IO,但是异步IO的缺点是编程模型复杂。

aiohttp异步IO爬取网站的三方库

协程(coroutine) ——> 异步编程

可以相互协作的子程序 ——> 协作式并发(谁发生了IO中断,就主动将CPU让给其他子程序,CPU没有闲着利用率自然就提高了)

生成器 ————> 迭代器的语法简化升级版本

生成器可以通过预激活操作升级为一个可以和其他子程序进行协作的协程

协程 —> 微线程 ----> 纤程

"""生成斐波拉切数列"""

def fib(num):
    a, b = 0, 1
    for x in range(num):
        a, b = b, a+b
        yield a


gen = fib(100)
print(type(gen), gen)
# 通过生成器获取数据
# 方法一:使用next函数
print(next(gen))
print(next(gen))
print(next(gen))
print(next(gen))
print(next(gen))

# 方法二:使用for-in循环
for value in gen:
    print(value)
def calc_avg():
    total, counter, result = 0, 0, None
    while True:
        num = yield result
        total += num
        counter += 1
        result = total / counter


def main():
    gen = calc_avg()
    # 生成器预激活(升级为协程)
    gen.send(None)
    # next(gen)
    print('平均值', gen.send(10))
    print('平均值', gen.send(20))
    print('平均值', gen.send(30))


if __name__ == '__main__':
    main()

生成器可以通过预激活操作升级为一个可以和其他子程序进行协作的协程

python 3.4引入协程 ——> 基于生成器实现协程

python 3.5引入async/await ——> 编写异步函数 ------> 协程对象

python 3.7中async和 await成为正式关键字

import asyncio


def display(num):
    print(num)
    # 休眠一秒钟,休眠的时候主动让出CPU给协程的程序
    await asyncio.sleep(1)

# cos_list = []
# for n in range(1, 10):
#     co = display(n)
#     cos_list.append(co)


# Pythonic代码
cos_list = [display(n) for n in range(1, 10)]
print(cos_list)

loop = asyncio.get_event_loop()
# 事件循环对象的run_until_complete方法需要的参数是协程对象或者是任务对象(Task)
# 如果直接给协程对象,该方法会自动将协程对象处理成Task对象
# 通过asynic模块的wait函数,将保存写成对象的列表转成任务对象
# 将协程对象的对应的任务挂载到事件循环上
loop.run_until_complete(cos_list)
import asyncio


async def display(num):
    print(num)
    # 休眠一秒钟,休眠的时候主动让出CPU给协程的程序
    await asyncio.sleep(1)

# Pythonic代码
cos_list = [display(n) for n in range(1, 10)]
# print(cos_list)

loop = asyncio.get_event_loop()
# 事件循环对象的run_until_complete方法需要的参数是协程对象或者是任务对象(Task)
# 如果直接给协程对象,该方法会自动将协程对象处理成Task对象
# 通过asynic模块的wait函数,将保存写成对象的列表转成任务对象
# 将协程对象的对应的任务挂载到事件循环上
loop.run_until_complete(asyncio.wait(cos_list))
loop.close()
# 异步结果是无序的,不需要一个等一个

—— aiohttp / httpx(版本还比较新,最近才出现,不完善)

"""通过aiohttp获取爬虫页面"""
import aiohttp
import asyncio
import re
# 命名捕获组(?p<名字>)
pattern = re.compile(r'<title>(?P<foo>.*?)</title>')
urls = ['http://python.org',
        'http://www.taobao.com',
        'http://www.jd.com',
        'http://www.baidu.com',
        'https://static4.scrape.center/',
        'https://www.cnblogs.com',
        'https://bj.zu.anjuke.com/?from=navigation',
        ]


async def fetch_page(session, url):
    with session.get(url, ssl=False) as resp:
        return await resp.text()


async def show_title(url):
    """根据指定的URL获取网站标题"""
    async with aiohttp.ClientSession() as session:
        async with session.get(url, ssl=False) as resp:
            html_code = await resp.text()
            match = pattern.search(html_code)
        if match:
            # group主操作
            print(match.group('foo'))

cos_list = [show_title(url) for url in urls]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(cos_list))

5.常用的Python解释器

CPython --------> C语言写的 -------->官方 <------- Anaconda

Jython ----------> Java

IronPython -------->C#

PyPy ---------> Python -------->性能最好--------->JIT

p.text()

async def show_title(url):
“”“根据指定的URL获取网站标题”""
async with aiohttp.ClientSession() as session:
async with session.get(url, ssl=False) as resp:
html_code = await resp.text()
match = pattern.search(html_code)
if match:
# group主操作
print(match.group(‘foo’))

cos_list = [show_title(url) for url in urls]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(cos_list))

5.常用的Python解释器

CPython --------> C语言写的 -------->官方 <------- Anaconda

Jython ----------> Java

IronPython -------->C#

PyPy ---------> Python -------->性能最好--------->JIT

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值