day8 爬虫相关方法
1. 全局解释器锁(GIL) 全称:Global Interpretter Lock
Cpytho —> 通过C实现的python解释器,申请和释放内容的操作并非线程安全,所以需要GIL来保证只有一个线路能执行关键操作,导致无法利用CPU的多核特性。
即便如此,多线程还是可以提升程序的执行效率(对于I/O密集型任务),也可以改善用户体验。
因此,对于计算密集型任务,可以使用多进程的方式来突破GIL的限制。
from threading import Thread
def run_fast():
while True:
pass
if __name__ == '__main__':
for _ in range(5):
Thread(target=run_fast).start()
2.守护线程
守护线程 - 不值得保留的线程
主线程如果执行结束了,守护线程即便任务没有执行完也要跟着结束掉。
import time
from threading import Thread
def output(content):
while True:
print(content, end='')
time.sleep(0.01)
if __name__ == '__main__':
Thread(target=output, args=('Ping',), daemon=True).start()
Thread(target=output, args=('Pong',), daemon=True).start()
time.sleep(3) # daemon = True 守护线程 主线程一停其他线程都跟着停
练习:
定义一个类描述一个银行账户(余额=0,存钱,取钱),
创建一个银行账户对象,启动100个线程,每个线程向该账户转入1元,
转账完成后,查看银行账户的余额。注意:存钱和取钱的受理是需要耗费时间的!
当多个线程竞争一个资源的时候,如果资源本身并不是线程安全的就会出现问题。
如果代码中需要对数据进行保护(保护临界资源),可以使用锁机制
如果对象实现了上下文管理器协议,就可以用到with语法中。
上下文管理器就是两个魔术方法:
~ enter:进入上下文的时候自动调用的方法
~ exit:离开上下文的时候自动调用的方法
import time
from threading import Thread, RLock
class Account:
"""银行账户"""
def __init__(self):
self.balance = 0
# 重入锁 (一般情况下我们使用的锁都是重入锁)
self.lock = RLock()
def deposit(self, money):
"""存钱"""
# 如果多个线程同时执行到这行代码,name后面的线程对余额的更新会覆盖掉之前的更新
# 这个现象在数据库层面也有哦可能发生,我们称之为丢失更新(第一类丢失更新和第二类丢失更新)
#通过上下文语法自动获得锁和释放锁 with
with self.lock:
new_balance = self.balane + money
time.sleep(0.01)
self.balance = new_balance
def withdraw(aslf, money):
"""取钱"""
if money <= self.balance:
with self.lock:
new_balance = self.balance - money
time.sleep(0.01)
self.balance = new_balance
return True
return False
if __name__ == '__main__':
accpunt = Account() # accpunt等于类
add_money_threads = []
for _ in range(100):
t = Thread(target=account.depsit, args(1, ))
add_money_threads.append(t)
t.start()
for t in add_money_thredas:
t.join()
print(account.balance)
3.池化技术
创建和释放一个线程都会产生比较大的系统开销,所以如果程序要长时间的使用多个线程
最好的方式是通过线程池来使用线程。线程池会提前创建好若干个线程,在使用的过程中
基本上不需要创建和释放线程,程序和线程池之间是借还关系。
池化技术是一种典型的空间换时间的技术。
import time
from concurrent.futures import ThreadPoolExecuto
from threading import RLock # 池化
class Account:
"""银行账户"""
def __init__(self):
self.balance = 0
self.lock = RLock()
def deposit(self, money):
"""存钱"""
# 如果多个线程同时执行到这个代码,那么后面的线程对余额的更新会覆盖掉之前的更新
# 这个现象在数据库层面也有可能发生,我们称之为丢失更新(第一类丢失更新第二类丢失更新)
# 通过上下文语法自动获得锁和释放锁 with
with self.lock:
new_balance = self.balance + money
time.sleep(0.01)
self.balance = new_balance
def withdraw(self, money):
"""取钱"""
if money <= self.balance:
with self.lock:
new_balance = self.balance - money
time.sleep(0.01)
self.balance = new_balance
return True
return False
if __name__ == '__main__':
account = Account()
with ThreadPoolExecutor(max_workers=16) as pool: # 创建16个线程
for _ in range(100)
# 如果传入的函数有多个参数,可以直接依次传入,不需要用元组包装
pool.submit(account.deoposit, 1) # .submit 提交
pool.shutdown() # .shutdown等线程执行完关闭
print(account.balance)
4. 迭代器
迭代器:实现了迭代器协议的对象。
迭代器协议对应两个魔术方法:
~ iter:获取迭代器对象
~ next:获取到下一个值
作业:写一个可以迭代出从2开始的若干个质数(只能被1和自身整除的正整数)的迭代器
class FibIter:
def __init__(self, num):
self.a, sela.b = 0, 1
self.num = num
self.counter = 0
def __init__(self); # 固定写法
return self # 返回它自己
def __next__(self):
if self.counter < self.num:
self.a self.b = self.b, self.a + self.b
slef.counter += 1
return self.a
raise StopIteraton()
if __name__ == '__main__':
fiter = FibIter(20)
for valur in fiter:
print(value)
5.生成器
生成器:迭代器语法升级简化版本
def bif(num):
a, b = 0, 1
for _ in range(num):
a, b = b, a + b
yield a
if __name__ == '__main__':
fiter = fib(20)
print(type(fiter))
fro value in fiter
print(valur)
6. 协程
协程(coroutine): 相互协作的子程序(函数)
生成器经过预激活操作就可以升级为一个协程
yield —> 产出 / 让步
def calc_avg():
total, counter = 0, 0
avg_value = None
while True:
curr_num = yield avg_value # 产出和 让出CPU
total += curr_num
counter += 1
avg_value = total / counter
def main():
gen = calc_avg()
# 将生成器预激活两种方式 ---> 协程
next(gen)
# gen.seng(None) # 生成器预激活协程
for _ in rang(5)
num = int(input())
print(gen.send(num))
if __name__ == '__main__':
main()
7. 异步函数
使用异步函数来创建协程对象
async —> asynchronous
sync —> synchronous
# 定义异步函数
import asyncio
async def say_hello():
print('hello, world')
# 调用异步函数不是执行函数体而是得到一个协程对象
co = say_hello()
# 协程对象需要挂载到一个事件循环上才能运作起来
loop = asucio.get_event_loop()
loop.run_until_complete(co)
loop.close()
8.同步阻塞式调用
I/O 操作模式:
~ 同步:调用一个函数,必须等函数返回之后才能往下执行
~ 异步:调用一个函数,不必等函数返回之后代码才能往下执行
– 阻塞 —> 多路I/O 复用
– 非阻塞 —> 回调式I/O 最好的一种选择
python做并发(并行)编程的三种方式
~ 多线程
~ 多进程
~ 异步编程/异步I/O —> 协作式并发(通过多个子程序相互协作,提高CPU使用效率)
同步阻塞式调用:
import time
def display(num):
print(num)
time.sleep(1)
def main():
for num in range(1, 10):
display(num)
if __name__ == '__main__':
main()
9. 同步非阻塞调用
同步 —> 有序, 异步 —> 无序
异步非阻塞式调用 —> 协作式并发 —> 多个子程序相互提高CPU利用率
async def display(num): # 函数前面加asuns变成异步函数
print(num)
# 等待休眠结 + 放弃对CPU的占用
await asyncio.sleep(1)
# await 放弃对CPU的占用
def main():
# 创建一组协程对象
cos = [dispaly(num) for num in range(1, 10)] # 这里是列表
# 将协程对象挂载到事件循环上
loop = asyncio.get_event_loop() # 调用方法 形成事件循环 固定写法
looop.run_until_complete(asuncio.wait(cos))
# run_until_complete 把后面的挂载到时间任务上 asyncio.wait() 不能传列表 所以把cos转换成事件对象
loop.close() # 关闭
if __name__ == '__main__':
main()
10. 通过异步I/O爬取标签 ----> aiohttp / httpx
导入 pip install aiohttp
import re
import aiohttp
import asyncio
# 命令捕获组(具名捕获组)
pattern = re.compile(r'<title>(?P<foo>.*?)</title>')
urls = [
'https://www.python.org/',
'https://www.taobao.com/',
'https://pypi.org/',
'https://www.git-scm.com/',
'https://www.jd.com/',
'https://opendata.sz.gov.cn/',
'https://www.datacamp.com/'
]
asyns def show_title(url):
"""根据指定的URL获取网站标题"""
async with aiohttp.ClientSession() as session:
await asyncio.sleep(1)
async with session.get(url, ssl=False) as resp:
html_code = await resp.text()
match = pattern.search(html_code)
if match:
print(match.group('foo'))
cos_list = [shou_title(url) for url in urls]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(cos_list))
loop.close()





