feat: 添加对爬虫类Spider的优化,包括引入psutil库来监控CPU和内存使用情况,以及使用tracemalloc库来追踪内存使用。同时,删除了冗余的代码和注释,使代码更加简洁。

pull/18/head
Yao 2 months ago
parent e2eab49065
commit b6657bd39e

@ -1,74 +1,22 @@
"""
根据提供的关键词列表爬取天水市人民政府网站上指定日期内与关键词相关的新闻的标题并将其存储至数据库中
考虑到相关因素因此本代码只爬取前10页的新闻内容即最多100条新闻作为测试
此方法为普通做法即使用requests库通过Post请求爬取网页内容再使用json提取新闻内容
注意本代码中的关键词列表默认为['灾害']日期范围默认为2018年1月1日至2018年12月31日
Args:
keywords: 用于搜索新闻的关键词列表
begin_date: 开始日期用于搜索
end_date: 结束日期用于搜索
size: 一次请求返回的新闻或政策的最大数量
Examples:
```
main(keywords=['灾害'],
begin_date='2018-01-01',
end_date='2018-12-31',
size=10)
```
"""
import util import util
import logging
from typing import List
import tqdm
@util.timeit
def main(keywords: List[str], begin_date: str, end_date: str, size: int = 10):
"""
爬取与提供的关键词列表相关的新闻.
Args: @util.measure_performance
keywords: 用于搜索新闻的关键词列表 def compute_task():
begin_date: 开始日期用于搜索 for _ in range(5):
end_date: 结束日期用于搜索 util.compute_task()
size: 一次请求返回的新闻或政策的最大数量
"""
logging.basicConfig(level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
filename='log.txt',
encoding='utf-8')
logging.info("开始运行普通爬取")
spider = util.Spider(keywords=keywords, @util.measure_performance
begin_date=begin_date, def io_task():
end_date=end_date, for url in util.urls:
size=size) util.fetch_url(url)
pbar = tqdm.tqdm(total=size * 10, desc='普通爬取进度', unit='', ncols=80)
title_list = []
for keyword in keywords:
for current in range(1, 11):
logging.info(f'keyword: {keyword}, current: {current}')
config = spider.get_config(keyword, current)
data = spider.fetch(config)
title_list += spider.parse(data)
pbar.update(size)
spider.save(title_list) def main():
pbar.close() compute_task()
logging.info("爬取完成") io_task()
if __name__ == "__main__": if __name__ == "__main__":
main(keywords=['灾害'], main()
begin_date='2018-01-01',
end_date='2018-12-31',
size=10)

@ -1,86 +1,34 @@
"""
根据提供的关键词列表爬取天水市人民政府网站上指定日期内与关键词相关的新闻的标题并将其存储至数据库中
考虑到相关因素因此本代码只爬取前10页的新闻内容即最多100条新闻作为测试
此方法为多进程做法即使用多进程并发爬取网页内容再使用json提取新闻内容
注意本代码中的关键词列表默认为['灾害']日期范围默认为2018年1月1日至2018年12月31日
Args:
keywords: 用于搜索新闻的关键词列表
begin_date: 开始日期用于搜索
end_date: 结束日期用于搜索
size: 一次请求返回的新闻或政策的最大数量
Examples:
```
main(keywords=['灾害'],
begin_date='2018-01-01',
end_date='2018-12-31',
size=10)
```
"""
import util import util
import logging
from typing import List
import multiprocessing import multiprocessing
import tqdm
lock = multiprocessing.Lock()
@util.timeit
def main(keywords: List[str], begin_date: str, end_date: str, size: int = 10):
"""
爬取与提供的关键词列表相关的新闻.
Args:
keywords: 用于搜索新闻的关键词列表
begin_date: 开始日期用于搜索
end_date: 结束日期用于搜索
size: 一次请求返回的新闻或政策的最大数量
"""
logging.basicConfig(level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
filename='log.txt',
encoding='utf-8')
logging.info("开始运行普通做法")
spider = util.Spider(keywords=keywords,
begin_date=begin_date,
end_date=end_date,
size=size)
title_list = [] @util.measure_performance
pbar = tqdm.tqdm(total=size * 10, desc='多进程爬取进度', unit='', ncols=80) def compute_task():
processes = [
multiprocessing.Process(target=util.compute_task) for _ in range(5)
]
for process in processes:
process.start()
for process in processes:
process.join()
with multiprocessing.Pool(processes=5) as pool:
results = []
for keyword in keywords:
for current in range(1, 11):
logging.info(f'keyword: {keyword}, current: {current}')
config = spider.get_config(keyword, current)
results.append(pool.apply_async(spider.fetch, (config, )))
for result in results: @util.measure_performance
data = result.get() def io_task():
title_list += spider.parse(data) processes = [
multiprocessing.Process(target=util.fetch_url, args=(url, ))
for url in util.urls
]
for process in processes:
process.start()
for process in processes:
process.join()
lock.acquire()
pbar.update(size)
lock.release()
spider.save(title_list) def main():
pbar.close() compute_task()
logging.info("爬取完成") io_task()
if __name__ == "__main__": if __name__ == "__main__":
main(keywords=['灾害'], main()
begin_date='2018-01-01',
end_date='2018-12-31',
size=10)

@ -1,89 +1,52 @@
"""
根据提供的关键词列表爬取天水市人民政府网站上指定日期内与关键词相关的新闻的标题并将其存储至数据库中
考虑到相关因素因此本代码只爬取前10页的新闻内容即最多100条新闻作为测试
此方法为多线程做法即使用多线程并行爬取网页内容再使用json提取新闻内容
注意本代码中的关键词列表默认为['灾害']日期范围默认为2018年1月1日至2018年12月31日
Args:
keywords: 用于搜索新闻的关键词列表
begin_date: 开始日期用于搜索
end_date: 结束日期用于搜索
size: 一次请求返回的新闻或政策的最大数量
Examples:
```
main(keywords=['灾害'],
begin_date='2018-01-01',
end_date='2018-12-31',
size=10)
```
"""
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
import threading import threading
import util import util
import logging
from typing import List
import tqdm
lock = threading.Lock() @util.measure_performance
def compute_task():
threads = [threading.Thread(target=util.compute_task) for _ in range(5)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
@util.timeit @util.measure_performance
def main(keywords: List[str], begin_date: str, end_date: str, size: int = 10): def compute_task_threadpool():
""" with ThreadPoolExecutor(max_workers=5) as executor:
爬取与提供的关键词列表相关的新闻. tasks = [executor.submit(util.compute_task) for _ in range(5)]
for future in as_completed(tasks):
future.result()
Args:
keywords: 用于搜索新闻的关键词列表
begin_date: 开始日期用于搜索
end_date: 结束日期用于搜索
size: 一次请求返回的新闻或政策的最大数量
"""
logging.basicConfig(level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
filename='log.txt',
encoding='utf-8')
logging.info("开始运行多线程爬取") @util.measure_performance
def io_task():
threads = [
threading.Thread(target=util.fetch_url, args=(url, ))
for url in util.urls
]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
spider = util.Spider(keywords=keywords,
begin_date=begin_date,
end_date=end_date,
size=size)
pbar = tqdm.tqdm(total=size * 10, desc='多线程爬取进度', unit='', ncols=80) # 多线程+线程池优化
title_list = [] @util.measure_performance
tasks = [] def io_task_threadpool():
with ThreadPoolExecutor(max_workers=5) as executor: with ThreadPoolExecutor(max_workers=5) as executor:
for keyword in keywords: tasks = [executor.submit(util.fetch_url, url) for url in util.urls]
for current in range(1, 11):
logging.info(f'keyword: {keyword}, current: {current}')
config = spider.get_config(keyword, current)
future = executor.submit(spider.fetch, config)
tasks.append(future)
# 更新进度条
lock.acquire()
pbar.update(size)
lock.release()
for future in as_completed(tasks): for future in as_completed(tasks):
data = future.result() future.result()
title_list += spider.parse(data)
spider.save(title_list) def main():
pbar.close() compute_task()
logging.info("爬取完成") compute_task_threadpool()
io_task()
io_task_threadpool()
if __name__ == "__main__": if __name__ == "__main__":
main(keywords=['灾害'], main()
begin_date='2018-01-01',
end_date='2018-12-31',
size=10)

@ -1,89 +1,21 @@
""" import asyncio
根据提供的关键词列表爬取天水市人民政府网站上指定日期内与关键词相关的新闻的标题并将其存储至数据库中
考虑到相关因素因此本代码只爬取前10页的新闻内容即最多100条新闻作为测试
此方法为协程做法即使用gevent库通过协程并发爬取网页内容再使用json提取新闻内容
注意本代码中的关键词列表默认为['灾害']日期范围默认为2018年1月1日至2018年12月31日
Args:
keywords: 用于搜索新闻的关键词列表
begin_date: 开始日期用于搜索
end_date: 结束日期用于搜索
size: 一次请求返回的新闻或政策的最大数量
Examples:
```
main(keywords=['灾害'],
begin_date='2018-01-01',
end_date='2018-12-31',
size=10)
```
"""
import gevent
from gevent import monkey
# 打补丁使标准库能够与gevent协同工作
monkey.patch_all()
import util import util
import logging
from typing import List
import tqdm
@util.timeit
def main(keywords: List[str], begin_date: str, end_date: str, size: int = 10):
"""
爬取与提供的关键词列表相关的新闻.
Args:
keywords: 用于搜索新闻的关键词列表
begin_date: 开始日期用于搜索
end_date: 结束日期用于搜索
size: 一次请求返回的新闻或政策的最大数量
"""
logging.basicConfig(level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
filename='log.txt',
encoding='utf-8')
logging.info("开始运行协程爬取")
spider = util.Spider(keywords=keywords,
begin_date=begin_date,
end_date=end_date,
size=size)
pbar = tqdm.tqdm(total=size * 10, desc='协程爬取进度', unit='', ncols=80) @util.measure_performance_async
title_list = [] async def async_compute_task():
await asyncio.gather(*[util.async_compute_task() for _ in range(5)])
def fetch_and_parse(keyword, current):
logging.info(f'keyword: {keyword}, current: {current}')
config = spider.get_config(keyword, current)
data = spider.fetch(config)
titles = spider.parse(data)
title_list.extend(titles)
pbar.update(size)
jobs = [ @util.measure_performance_async
gevent.spawn(fetch_and_parse, keyword, current) for keyword in keywords async def async_io_task():
for current in range(1, 11) await asyncio.gather(*[util.fetch_url_async(url) for url in util.urls])
]
gevent.joinall(jobs)
spider.save(title_list) def main():
pbar.close() asyncio.run(async_compute_task())
logging.info("爬取完成") asyncio.run(async_io_task())
if __name__ == "__main__": if __name__ == "__main__":
main(keywords=['灾害'], main()
begin_date='2018-01-01',
end_date='2018-12-31',
size=10)

@ -1,85 +1,34 @@
"""
根据提供的关键词列表爬取天水市人民政府网站上指定日期内与关键词相关的新闻的标题并将其存储至数据库中
考虑到相关因素因此本代码只爬取前10页的新闻内容即最多100条新闻作为测试
此方法为多线程做法即使用异步并行爬取网页内容再使用json提取新闻内容
注意本代码中的关键词列表默认为['灾害']日期范围默认为2018年1月1日至2018年12月31日
Args:
keywords: 用于搜索新闻的关键词列表
begin_date: 开始日期用于搜索
end_date: 结束日期用于搜索
size: 一次请求返回的新闻或政策的最大数量
Examples:
```
asyncio.run(
main_async(keywords=['灾害'],
begin_date='2018-01-01',
end_date='2018-12-31',
size=10))
```
"""
import asyncio import asyncio
from multiprocessing import Process
from concurrent.futures import ProcessPoolExecutor
import util import util
import logging
from typing import List
import tqdm
@util.timeit_async
async def main_async(keywords: List[str],
begin_date: str,
end_date: str,
size: int = 10):
"""
使用异步方式爬取与提供的关键词列表相关的新闻.
Args: @util.measure_performance
keywords: 用于搜索新闻的关键词列表 def async_compute_task():
begin_date: 开始日期用于搜索 with ProcessPoolExecutor() as pool:
end_date: 结束日期用于搜索 futures = [pool.submit(util.compute_task) for _ in range(5)]
size: 一次请求返回的新闻或政策的最大数量 for future in futures:
""" future.result()
logging.basicConfig(level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
filename='log.txt',
encoding='utf-8')
logging.info("开始运行异步爬取")
spider = util.Spider(keywords=keywords, @util.measure_performance_async
begin_date=begin_date, async def async_io_task():
end_date=end_date, chunk_size = len(util.urls) // 5
size=size) chunks = [
util.urls[i:i + chunk_size]
for i in range(0, len(util.urls), chunk_size)
]
pbar = tqdm.tqdm(total=size * 10, desc='异步爬取进度', unit='', ncols=80) for chunk in chunks:
title_list = [] tasks = [util.fetch_url_async(url) for url in chunk]
tasks = [] await asyncio.gather(*tasks)
for keyword in keywords:
for current in range(1, 11):
logging.info(f'keyword: {keyword}, current: {current}')
config = spider.get_config(keyword, current)
task = asyncio.create_task(spider.fetch_async(config))
tasks.append(task)
for task in asyncio.as_completed(tasks):
data = await task
title_list += spider.parse(data)
pbar.update(size)
spider.save(title_list) def main():
pbar.close() # async_compute_task()
logging.info("爬取完成") asyncio.run(async_io_task())
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run( main()
main_async(keywords=['灾害'],
begin_date='2018-01-01',
end_date='2018-12-31',
size=10))

@ -0,0 +1,17 @@
段:存放的是全局变量和静态变量
栈:系统自动分配释放,函数参数值,局部变量,返回地址等在此
堆:存放动态分配的数据,由开发人员自行管理
进程表会记录进程在内存的位置PID 是多少,以及当前什么状态,内存给它分配了多大使用空间以及属于哪个用户
每个用户态线程通过系统调用创建一个绑定的内核线程Windows NT 即采用这种模型
n 个用户态线程对应 m 个内核态线程。m 通常设置为核数Linux 即采用的这种模型
在 Linux 中,操作系统采用虚拟内存管理技术,使得进程都拥有独立的虚拟内存空间,理由也比较直接,物理内存不够用且不安全(用户不能直接访问物理内存)。Linux 内核看来只有进程而没有线程。Linux所谓的线程其实是与其他进程共享资源的轻量级进程。为什么说是轻量级呢在于它只有一个执行上下文和调度程序所需的信息与父进程共享进程地址空间 。
虚拟内存技术,把进程虚拟地址空间划分成用户空间和内核空间。
在 32 位的操作系统中4GB 的进程地址空间分为,用户空间和内核空间,用户空间为 03G内核地址空间占据 34G
用户不能直接操作内核空间虚拟地址,只有通过系统调用的方式访问内核空间。
线程共享虚拟内存和全局变量等资源,线程拥有自己的私有数据比如栈和寄存器。

@ -1,25 +1,269 @@
# 目标 # 目标
本节使用一个爬虫任务来展示如何追求代码的性能 。 本节使用一个爬虫任务和计算任务来展示如何追求代码的性能 。
充分理解线程、协程、进程、同步、异步、阻塞、非阻塞等概念,并能够根据具体场景选择合适的并发模型。 充分理解线程、协程、进程、同步、异步、阻塞、非阻塞等概念,并能够根据具体场景选择合适的并发模型。
主线问题如何解决IO和计算速度不匹配、如何任务分解、分发和协作 。 主线问题如何解决IO和计算速度不匹配、如何任务分解、分发和协作 。
# 任务 # 任务
## 1、CPU密集型/计算密集型:
  将```sum(i * i for i in range(10**8))```作为一个子任务运行5次分别使用单线程、多线程、多进程、多协程、异步等方式并对比运行时间、内存使用、CPU使用等指标。
## 2、IO密集型
# 讨论分析 # 讨论分析
普通做法连续进行了五次测试时间分别为34.231s、34.091s、34.164s、34.226s、33.958s平均时间为34.134s ## 1、CPU密集型/计算密集型:
多进程(进程数=5连续进行了五次测试时间分别为7.719s、7.716s、7.690s、7.730s、7.711s平均时间为7.7132s   为控制变量代码在同一台设备上运行且关闭其他进程保证CPU资源不被其他进程占用。测试结果如下
多线程(线程数=5连续进行了五次测试时间分别为7.185s、7.964s、6.983s、6.969s、7.035s平均时间为7.2272s
协程连续进行了五次测试时间分别为3.775s、3.807s、3.733s、3.824s、3.744s平均时间为3.776s ### 普通做法:
异步连续进行了五次测试时间分别为6.975s、7.675s、7.018s、7.032s、7.049s平均时间为7.1498s #### 运行前:
为保证公平性每一次Post请求后休眠3秒   CPU使用率0.0%内存使用率53.3%
可以看出,协程的性能最好,普通做法的性能最差,多线程、多进程和异步的性能介于两者之间。 #### 运行后:
考虑到多进程和多线程是故意开的5个进程和线程而协程是单线程所以协程的性能最好。   CPU使用率0.6%内存使用率53.0%内存使用0.003336MB峰值内存使用0.004216MB单个子任务运行平均时间7.86s总运行时间39.31s
另外,异步的性能最差,可能是由于异步的并发模型需要频繁地切换线程,导致性能下降。
总的来说,协程的性能最好,多线程和多进程的性能介于两者之间,普通做法的性能最差。 #### 运行结束30秒后
  CPU使用率0.6%内存使用率53.0%
### 多进程:
#### 运行前:
  CPU使用率0.0%内存使用率52.8%
#### 运行后:
  CPU使用率1.3%内存使用率52.9%内存使用0.096089MB峰值内存使用0.121865MB单个子任务运行平均时间0.654s总运行时间1.86s
#### 运行结束30秒后
  CPU使用率0.6%内存使用率52.7%
### 多线程:
#### 运行前:
  CPU使用率3.8%内存使用率53.1%
#### 运行后:
  CPU使用率3.6%内存使用率53.1%内存使用0.001472MB峰值内存使用0.019296MB单个子任务运行平均时间38.89s总运行时间39.77s
#### 运行结束30秒后
  CPU使用率0.7%内存使用率52.9%
### 多线程 + 线程池:
#### 运行前:
  CPU使用率0.0%内存使用率52.9%
#### 运行后:
  CPU使用率0.7%内存使用率53.1%内存使用0.000952MB峰值内存使用0.036604MB单个子任务运行平均时间40.49s总运行时间41.16s
#### 运行结束30秒后
  CPU使用率0.6%内存使用率52.9%
### 多协程:
#### 运行前:
  CPU使用率1.1%内存使用率53.4%
#### 运行后:
  CPU使用率0.2%内存使用率53.4%内存使用0.009448MB峰值内存使用0.011027MB单个子任务运行平均时间9.14s总运行时间45.70s
#### 运行结束30秒后
  CPU使用率0.6%内存使用率53.4%
### 多进程 + 异步:
#### 运行前:
  CPU使用率0.8%内存使用率54.9%
#### 运行后:
  CPU使用率0.6%内存使用率54.9%内存使用0.123481MB峰值内存使用0.188774MB单个子任务运行平均时间0.63s总运行时间1.85s
#### 运行结束30秒后
  CPU使用率0.9%内存使用率55.0%
## 2、IO密集型
  为控制变量,代码在同一台设备上运行,关闭其他进程,并在相同的网络环境下运行,保证网络资源不被其他进程占用。
### 普通做法:
#### 运行前:
  CPU使用率0.0%内存使用率52.5%
#### 运行后:
  CPU使用率0.9%内存使用率52.6%内存使用0.074968MB峰值内存使用4.162339MB总运行时间5.56s
#### 运行结束30秒后
  CPU使用率0.6%内存使用率52.5%
### 多进程:
#### 运行前:
  CPU使用率0.0%内存使用率52.7%
#### 运行后:
  CPU使用率5.0%内存使用率52.8%内存使用0.03219MB峰值内存使用0.085236MB总运行时间5.81s
#### 运行结束30秒后
  CPU使用率1.2%内存使用率53.1%
### 多线程:
#### 运行前:
  CPU使用率0.0%内存使用率52.9%
#### 运行后:
  CPU使用率0.1%内存使用率53.0%内存使用MB峰值内存使用MB总运行时间1.63s
#### 运行结束30秒后
  CPU使用率0.5%内存使用率52.9%
### 多线程 + 线程池:
#### 运行前:
  CPU使用率1.3%内存使用率53.2%
#### 运行后:
  CPU使用率1.3%内存使用率53.1%内存使用0.092493MB峰值内存使用4.371028MB总运行时间1.47s
#### 运行结束30秒后
  CPU使用率0.8%内存使用率53.2%
### 多协程:
#### 运行前:
  CPU使用率1.1%内存使用率54.4%
#### 运行后:
  CPU使用率0.8%内存使用率54.4%内存使用0.753684MB峰值内存使用15.09151MB总运行时间1.45s
#### 运行结束30秒后
  CPU使用率0.8%内存使用率54.4%
### 多进程 + 异步:
#### 运行前:
  CPU使用率1.5%内存使用率60.0%
#### 运行后:
  CPU使用率0.8%内存使用率59.8%内存使用0.569592MB峰值内存使用6.360097MB总运行时间7.40s
#### 运行结束30秒后
  CPU使用率1.3%内存使用率59.6%
# 总结 # 总结
协程的性能最好,多线程和多进程的性能介于两者之间,普通做法的性能最差。 ## 1、CPU密集型/计算密集型:
| | 普通做法 | 多进程 | 多线程 | 多线程+线程池 | 多协程 | 多进程+异步 |
| :----------------------: | :--------: | :--------: | :--------: | :-----------: | :--------: | :---------: |
| 运行前CPU使用率 | 0.0% | 0.0% | 3.8% | 0.0% | 1.1% | 0.8% |
| 运行后CPU使用率 | 0.6% | 1.3% | 3.6% | 0.7% | 0.2% | 0.6% |
| 运行结束30秒后CPU使用率 | 0.6% | 0.6% | 0.7% | 0.6% | 0.6% | 0.9% |
| 运行前内存使用率 | 53.3% | 52.8% | 53.1% | 52.9% | 53.4% | 54.9% |
| 运行后内存使用率 | 53.0% | 52.9% | 53.1% | 53.1% | 53.4% | 54.9% |
| 运行结束30秒后内存使用率 | 53.0% | 52.7% | 52.9% | 52.9% | 53.4% | 55.0% |
| 内存使用 | 0.003336MB | 0.096089MB | 0.001472MB | 0.000952MB | 0.009448MB | 0.123481MB |
| 峰值内存使用 | 0.004216MB | 0.121865MB | 0.019296MB | 0.036604MB | 0.011027MB | 0.188774MB |
| 单个子任务运行平均时间 | 7.86s | 0.654s | 38.89s | 40.49s | 9.14s | 0.63s |
| 总运行时间 | 39.31s | 1.86s | 39.77s | 41.16s | 45.70s | 1.85s |
  根据表格中的数据,我们可以对不同的并发和并行编程方法进行详细的总结:
### 普通做法
- **CPU使用率**在任务运行前、运行后和运行结束30秒后的CPU使用率都较低表明普通做法没有充分利用CPU资源。
- **内存使用率**:内存使用率变化不大,表明普通做法对内存的影响较小。
- **内存使用**:内存使用量和峰值内存使用量都很低。
- **运行时间**:单个子任务和总任务的运行时间都较长,表明普通做法效率较低。
### 多进程
- **CPU使用率**在任务运行后CPU使用率显著增加表明多进程方法能够充分利用多核CPU的优势。
- **内存使用率**:内存使用率略有增加,但变化不大。
- **内存使用**:内存使用量和峰值内存使用量较高,表明多进程方法对内存的需求较大。
- **运行时间**单个子任务和总任务的运行时间都显著减少表明多进程方法在处理CPU密集型任务时效率最高。
### 多线程
- **CPU使用率**在任务运行前、运行后和运行结束30秒后的CPU使用率都较低表明多线程方法没有充分利用CPU资源。
- **内存使用率**:内存使用率变化不大。
- **内存使用**:内存使用量和峰值内存使用量较低。
- **运行时间**单个子任务和总任务的运行时间都较长表明多线程方法在处理CPU密集型任务时效率较低。
### 多线程+线程池
- **CPU使用率**在任务运行后CPU使用率略有增加但变化不大。
- **内存使用率**:内存使用率变化不大。
- **内存使用**:内存使用量和峰值内存使用量较低。
- **运行时间**:单个子任务和总任务的运行时间都较长,表明多线程+线程池方法在处理CPU密集型任务时效率较低。
### 多协程
- **CPU使用率**在任务运行前、运行后和运行结束30秒后的CPU使用率都较低表明多协程方法没有充分利用CPU资源。
- **内存使用率**:内存使用率变化不大。
- **内存使用**:内存使用量和峰值内存使用量较低。
- **运行时间**单个子任务和总任务的运行时间都较长表明多协程方法在处理CPU密集型任务时效率较低。
### 多进程+异步
- **CPU使用率**在任务运行后CPU使用率显著增加表明多进程+异步方法能够充分利用多核CPU的优势。
- **内存使用率**:内存使用率略有增加,但变化不大。
- **内存使用**:内存使用量和峰值内存使用量较高,表明多进程+异步方法对内存的需求较大。
- **运行时间**:单个子任务和总任务的运行时间都显著减少,表明多进程+异步方法在处理CPU密集型任务时效率最高。
### 结论
- **多进程**和**多进程+异步**方法在处理CPU密集型任务时效率最高能够充分利用多核CPU的优势但对内存的需求较大。
- **多线程**和**多线程+线程池**方法在处理CPU密集型任务时效率较低主要是由于Python的全局解释器锁GIL的存在无法实现真正的并行。
- **多协程**方法在处理CPU密集型任务时效率较低但在处理I/O密集型任务时可能会有更好的表现。
- **普通做法**效率最低,无法充分利用系统资源。
## 2、IO密集型
| | 普通做法 | 多进程 | 多线程 | 多线程+线程池 | 多协程 | 多进程+异步 |
| :----------------------: | :--------: | :--------: | :----: | :-----------: | :--------: | :---------: |
| 运行前CPU使用率 | 0.0% | 0.0% | 0.0% | 1.3% | 1.1% | 1.5% |
| 运行后CPU使用率 | 0.9% | 5.0% | 0.1% | 1.3% | 0.8% | 0.8% |
| 运行结束30秒后CPU使用率 | 0.6% | 1.2% | 0.5% | 0.8% | 0.8% | 1.3% |
| 运行前内存使用率 | 52.5% | 52.7% | 52.9% | 53.2% | 54.4% | 60.0% |
| 运行后内存使用率 | 52.6% | 52.8% | 53.0% | 53.1% | 54.4% | 59.8% |
| 运行结束30秒后内存使用率 | 52.5% | 53.1% | 52.9% | 53.2% | 54.4% | 59.6% |
| 内存使用 | 0.074968MB | 0.03219MB | N/A | 0.092493MB | 0.753684MB | 0.569592MB |
| 峰值内存使用 | 4.162339MB | 0.085236MB | N/A | 4.371028MB | 15.09151MB | 6.360097MB |
| 总运行时间 | 5.56s | 5.81s | 1.63s | 1.47s | 1.45s | 7.40s |
  根据表格中的数据,我们可以对不同的并发和并行编程方法进行详细的总结:
### 普通做法
- **CPU使用率**在任务运行前、运行后和运行结束30秒后的CPU使用率都较低表明普通做法没有充分利用CPU资源。
- **内存使用率**:内存使用率变化不大,表明普通做法对内存的影响较小。
- **内存使用**:内存使用量和峰值内存使用量都很低。
- **运行时间**总运行时间为5.56秒,表明普通做法效率较低。
### 多进程
- **CPU使用率**在任务运行后CPU使用率显著增加表明多进程方法能够充分利用多核CPU的优势。
- **内存使用率**:内存使用率略有增加,但变化不大。
- **内存使用**:内存使用量和峰值内存使用量较低。
- **运行时间**总运行时间为5.81秒略高于普通做法表明多进程方法在处理IO密集型任务时效率较高。
### 多线程
- **CPU使用率**在任务运行前、运行后和运行结束30秒后的CPU使用率都较低表明多线程方法没有充分利用CPU资源。
- **内存使用率**:内存使用率变化不大。
- **内存使用**:内存使用量和峰值内存使用量较低。
- **运行时间**总运行时间为1.63秒显著低于普通做法表明多线程方法在处理IO密集型任务时效率较高。
### 多线程+线程池
- **CPU使用率**在任务运行后CPU使用率略有增加但变化不大。
- **内存使用率**:内存使用率变化不大。
- **内存使用**:内存使用量和峰值内存使用量较低。
- **运行时间**总运行时间为1.47秒,显著低于普通做法,表明多线程+线程池方法在处理IO密集型任务时效率较高。
### 多协程
- **CPU使用率**在任务运行前、运行后和运行结束30秒后的CPU使用率都较低表明多协程方法没有充分利用CPU资源。
- **内存使用率**:内存使用率变化不大。
- **内存使用**:内存使用量和峰值内存使用量较高。
- **运行时间**总运行时间为1.45秒显著低于普通做法表明多协程方法在处理IO密集型任务时效率较高。
### 多进程+异步
- **CPU使用率**在任务运行后CPU使用率显著增加表明多进程+异步方法能够充分利用多核CPU的优势。
- **内存使用率**:内存使用率略有增加,但变化不大。
- **内存使用**:内存使用量和峰值内存使用量较高。
- **运行时间**总运行时间为7.40秒,略高于普通做法,表明多进程+异步方法在处理IO密集型任务时效率较低。
### 结论
- **多线程**、**多线程+线程池**和**多协程**方法在处理IO密集型任务时效率最高能够显著减少总运行时间。
- **多进程**方法在处理IO密集型任务时效率较高但略低于多线程和多协程方法。
- **多进程+异步**方法在处理IO密集型任务时效率较低总运行时间较长。
- **普通做法**效率最低,无法充分利用系统资源。
<style>
h1{font:20pt "SimHei", sans-serif;}
h2{font:16pt "SimHei", sans-serif;}
p{font:12pt "SimSun", sans-serif;line-height:150%}
li{font:12pt "SimSun", sans-serif;line-height:150%}
</style>

@ -1,188 +1,107 @@
"""
"""
import re
import time import time
import functools import psutil
import json import tracemalloc
import asyncio
import requests import requests
from typing import Any, Dict, List import aiohttp
class Spider: def compute_task():
""" start_time = time.time()
爬虫类 result = sum(i * i for i in range(10**7))
end_time = time.time()
Args: print(f"compute_task耗时{end_time - start_time}")
keywords (List[str]): 用于搜索新闻的关键词列表
begin_date (str): 开始日期用于搜索
end_date (str): 结束日期用于搜索 async def async_compute_task():
size (int): 一次请求返回的新闻或政策的最大数量 start_time = time.time()
result = sum(i * i for i in range(10**7))
Attributes: end_time = time.time()
URL (str): 网址 print(f"async_compute_task耗时{end_time - start_time}")
"""
# 天水市人民政府网站
URL = ('https://www.tianshui.gov.cn/aop_component/'
'/webber/search/search/search/queryPage')
def __init__(self, keywords: List[str], begin_date: str, end_date: str,
size: int):
self.keywords = keywords
self.begin_date = begin_date
self.end_date = end_date
self.size = size
def get_config(self, keyword: str, current: int) -> Dict[str, Any]:
"""
获取配置信息
Args:
keyword (str): 关键词
size (int): 一次请求返回的新闻的最大数量
Returns:
Dict[str, Any]: 配置信息
"""
return {
"aliasName": "article_data,open_data,mailbox_data,article_file",
"keyWord": keyword,
"lastkeyWord": keyword,
"searchKeyWord": False,
"orderType": "score",
"searchType": "text",
"searchScope": "3",
"searchOperator": 0,
"searchDateType": "custom",
"searchDateName": f"{self.begin_date}-{self.end_date}",
"beginDate": self.begin_date,
"endDate": self.end_date,
"showId": "c2ee13065aae85d7a998b8a3cd645961",
"auditing": ["1"],
"owner": "1912126876",
"token": "tourist",
"urlPrefix": "/aop_component/",
"page": {
"current": current,
"size": self.size,
"pageSizes": [2, 5, 10, 20, 50, 100],
"total": 0,
"totalPage": 0,
"indexs": []
},
"advance": False,
"advanceKeyWord": "",
"lang": "i18n_zh_CN"
}
def generate_headers(self) -> dict:
"""
生成请求头
Returns:
dict: 请求头
"""
return {
'Authorization':
'tourist',
'User-Agent':
('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit'
'/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari'
'/537.36 Edg/124.0.0.0')
}
def fetch(self, config: Dict[str, Any]) -> Dict[str, Any]:
"""
普通做法
Post请求获取网页内容并返回请求结果
Args:
config (Dict[str, Any]): 配置信息
Returns:
Dict[str, Any]: 请求结果
"""
response = requests.post(self.URL,
headers=self.generate_headers(),
json=config).text
time.sleep(3)
return json.loads(response)
async def fetch_async(self, config: Dict[str, Any]) -> Dict[str, Any]:
"""
异步做法
Post请求获取网页内容并返回请求结果
Args:
config (Dict[str, Any]): 配置信息
Returns:
Dict[str, Any]: 请求结果
"""
response = requests.post(self.URL,
headers=self.generate_headers(),
json=config).text
await asyncio.sleep(3)
return json.loads(response)
def parse(self, data: Dict[str, Any]) -> List[str]:
"""
解析网页内容
Args:
data (Dict[str, Any]): 网页内容
Returns:
List[str]: 标题列表
"""
title_list = []
records = data['data']['page']['records']
for i in range(self.size):
title = records[i]['title']
title = re.sub('<[^>]*>', '', title) # 去除html标签
title_list.append(title)
# print(title)
return title_list
def save(self, title_list: List[str]):
"""
保存数据
"""
pass
# 时间装饰器
def timeit(func):
"""
计算函数运行时间
Args:
func: 函数
Return:
函数
"""
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f'{func.__name__} cost: {time.time() - start}') def measure_performance(func):
return result
def wrapper(*args, **kwargs):
print(f"开始运行{func.__name__}")
time.sleep(30)
print(f"运行前的CPU使用率{psutil.cpu_percent()}%")
print(f"运行前的内存使用率:{psutil.virtual_memory().percent}%")
print()
tracemalloc.start()
start_time = time.time()
func(*args, **kwargs)
end_time = time.time()
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f"{func.__name__}耗时:{end_time - start_time}")
print(f"当前内存使用:{current / 10**6}MB, 峰值内存使用:{peak / 10**6}MB")
print(f"CPU使用率{psutil.cpu_percent(interval=1)}%")
print(f"内存使用率:{psutil.virtual_memory().percent}%")
print()
time.sleep(30)
print(f"运行结束30秒后的CPU使用率{psutil.cpu_percent()}%")
print(f"运行结束30秒后的内存使用率{psutil.virtual_memory().percent}%")
print(f"运行结束{func.__name__}结束")
print()
return wrapper return wrapper
def timeit_async(func): def measure_performance_async(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs): async def wrapper(*args, **kwargs):
start = time.time() print(f"开始运行{func.__name__}")
result = await func(*args, **kwargs)
time.sleep(30)
print(f'{func.__name__} cost: {time.time() - start}') print(f"运行前的CPU使用率{psutil.cpu_percent()}%")
return result print(f"运行前的内存使用率:{psutil.virtual_memory().percent}%")
print()
tracemalloc.start()
start_time = time.time()
await func(*args, **kwargs)
end_time = time.time()
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f"{func.__name__}耗时:{end_time - start_time}")
print(f"当前内存使用:{current / 10**6}MB, 峰值内存使用:{peak / 10**6}MB")
print(f"CPU使用率{psutil.cpu_percent(interval=1)}%")
print(f"内存使用率:{psutil.virtual_memory().percent}%")
print()
time.sleep(30)
print(f"运行结束30秒后的CPU使用率{psutil.cpu_percent()}%")
print(f"运行结束30秒后的内存使用率{psutil.virtual_memory().percent}%")
return wrapper return wrapper
def fetch_url(url):
response = requests.get(url)
async def fetch_url_async(url):
async with aiohttp.ClientSession() as session:
async with session.get(url, ssl=False) as response:
await response.read()
urls = [
"https://www.baidu.com", "https://www.qq.com", "https://www.taobao.com",
"https://www.tmall.com", "https://www.jd.com", "https://www.sina.com.cn",
"https://www.weibo.com", "https://www.163.com", "https://www.sohu.com",
"https://www.ifeng.com", "https://www.alipay.com", "https://www.youku.com",
"https://www.iqiyi.com", "https://www.tudou.com", "https://www.douyin.com",
"https://www.meituan.com", "https://www.xiaomi.com",
"https://www.oppo.com", "https://www.vivo.com", "https://www.dianping.com",
"https://www.autohome.com.cn", "https://www.hupu.com",
"https://www.xinhuanet.com", "https://www.chinadaily.com.cn",
"https://www.people.com.cn", "https://www.cctv.com", "https://www.cntv.cn",
"https://www.zol.com.cn", "https://www.pconline.com.cn",
"https://www.techweb.com.cn", "https://www.cs.com.cn",
"https://www.eastmoney.com", "https://www.hexun.com",
"https://www.yicai.com", "https://www.21jingji.com"
]

@ -0,0 +1,54 @@
## 进程、线程、协程
运行一个软件就是开了一个进程
一个游戏,启动后为一个进程
但一个游戏需要图形渲染,联网
操作能同时运行
所以将其各个部分设计为线程
即一个进程有多个线程
从操作系统层面而言
进程是分配资源的基本单位
进程之间是独立的
一个进程无法访问另一个进程的空间
一个进程运行的失败也不会影响其他进程的运行
一个进程内可以包含多个线程
线程是程序执行的基本单位,是进程中的实际运作单位
线程是操作系统分配处理器时间的基本单元
线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉
协程运行在线程之上
协程的调度完全由用户控制,协程拥有自己的寄存器上下文和栈
## 并发/并行
【顺序】 你做作业,然后看综艺,
【并发】 你写程序到一半,综艺开始,看完综艺后继续写程序。两件事情都处于启动状态
并发把CPU运行划分成若干个时间段在一个时间段线程代码运行时其他线程挂起状态
【并行】 你写程序到一半,综艺开始,你一边做作业一边写程序。两件事情同时做
## 阻塞/非阻塞
等候消息的过程中有没有干其他事
“阻塞”是不能
## 同步异步
指的是消息通知的机制
主动听消息则为同步(一直等,轮流取)、被动听消息则为异步
异步过程调用发出后,可以继续执行其它操作
通知调用者的三种方式,如下
状态:即监听被调用者的状态(轮询),调用者没隔一段时间检查一次,效率会很低。
通知:当被调用者执行完成后,发出通知告知调用者,无需消耗太多性能。
回调:与通知类似,当被调用者执行完成后,会调用调用者提供的回调函数。
老张要做两件事:烧水,看电视
同步阻塞:老张烧水,等水开,然后看电视
同步非阻塞:老张烧水,去客厅看电视,时不时去看看水开了没有(检查状态)
老张买了把水开之后会提示音的水壶。
异步阻塞:老张烧水,然后坐在旁边等着听那个烧开的提示音。(异步阻塞)
异步非阻塞:老张烧水,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)
实现异步可以采用多线程技术或则交给另外的进程来处理
如果老张委派小张去烧水,自己看电视,那就是并行

@ -0,0 +1,54 @@
Python中多进程、多线程、协程区别和应用场景
多任务简单地说,就是操作系统可以同时运行多个任务。分为并行和并发两种。
并行指的是任务数小于等于CPU核数即任务真的是一起执行的
并发指的是实际上一个时间只有一个任务在执行那效率是怎么提升的常常程序运行的时候需要等待IO操作比如访问数据库等待网络服务器回传数据 。这个时候并发切换到其它任务就能节省时间了。
进程是资源分配的单位
线程是操作系统调度的单位
进程切换需要的资源大
线程切换需要的资源小
协程切换任务资源很小,协程是在一个线程中所以是并发
IO密集型指的是系统的CPU性能相对硬盘、内存要好很多此时系统运作大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作此时CPU Loading并不高。这可能是因为任务本身不太需要访问I/O设备。
并行做i/o操作多线程好些。简单的异步操作用协程
进程:进程是并行。没有运行的代码叫程序,运行的程序或代码就是一个进程。进程是系统进行资源分配的最小单位,进程拥有自己的内存空间,所以进程间数据不共享。多进程适合计算密集型任务
通常我们所说的电脑配置几核就是最大可以创建的进程,注意因为电脑会切换进程,所以看起来电脑会同时运行的进程数会超过核数
数据无法内存共享。各个进程之间无影响,更稳定和安全,不因个别崩溃而整个系统崩溃。
当需要创建的子进程数量巨大时就可以用到multiprocessing模块提供的Pool方法
进程是通过Queue实现多进程之间的数据传递Queue本身是一个消息队列程序。
线程:线程是并发。 一个进程至少有一个线程,叫主线程,多个线程共享内存(数据和全局变量共享)。
多线程适合 IO 密集型操作 。
缺点就是,如果多个线程同时对同一个全局变量操作,会出现资源竞争问题,从而数据结果会不正确
当多个线程几乎同时修改某个共享数据的时候,需要进行同步控制。
某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。
可以添加超时时间等,解决死锁
协程:用户态的轻量级线程,调度有用户控制。
协程是一种 允许在特定位置暂停或恢复的子程序。
和生成器 不同的是,协程 可以控制子程序暂停之后代码的走向,而 生成器 仅能被动地将控制权交还给调用者。
协程看上去像是线程,但是实际使用非线程的方式。
协程 可以只利用一个线程更加轻便地实现 多任务,将任务切换的开销降至最低。
和 回调 等其他异步技术相比,
协程 维持了正常的代码流程,在保证代码可读性的同时最大化地利用了 阻塞 IO 的空闲时间。它的高效与简洁赢得了开发者们的拥戴。
协程实现方式分为
以gevent第二种语法上以async和await关键字实现的协程可读性更好一些。asyncio
具体的说协程是函数体中包含yield或者yield from 的函数。一个协程可以通常处于四种状态之一('GEN_CREATED',,'GEN_RUNNING','GEN_SUSPENDED','GEN_CLOSED').
协程和线程都区别就是线程调度看系统协程你可以自己控制。yield 就可以实现了
很多时候系统瓶颈都在 IO很多涉及到 IO 特别是网络的程序都需要用到线程 /协程。
本来线程就复杂,还实现不完整,能用协程就协程
大部分用于 generator 这个特定场景
我自己的话有一个场景,每个登录用户定时拉取外部信息的需求,如果每个用户开一个线程开销太大,所以就用协程还是很方便的
模块
thread模块 是比较底层的模块
threading模块对thread做了一些包装
Loading…
Cancel
Save