diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..638bc3f --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +log.txt +/test +/.venv +*/__pycache__ diff --git a/B 高性能模式/000 普通做法.py b/B 高性能模式/000 普通做法.py index e69de29..a97e3b2 100644 --- a/B 高性能模式/000 普通做法.py +++ b/B 高性能模式/000 普通做法.py @@ -0,0 +1,74 @@ +""" +根据提供的关键词列表,爬取天水市人民政府网站上指定日期内与关键词相关的新闻的标题,并将其存储至数据库中。 + +考虑到相关因素,因此本代码只爬取前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 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) + 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) + pbar.close() + logging.info("爬取完成") + + +if __name__ == "__main__": + main(keywords=['灾害'], + begin_date='2018-01-01', + end_date='2018-12-31', + size=10) diff --git a/B 高性能模式/010 多进程.py b/B 高性能模式/010 多进程.py index e69de29..94177bc 100644 --- a/B 高性能模式/010 多进程.py +++ b/B 高性能模式/010 多进程.py @@ -0,0 +1,86 @@ +""" +根据提供的关键词列表,爬取天水市人民政府网站上指定日期内与关键词相关的新闻的标题,并将其存储至数据库中。 + +考虑到相关因素,因此本代码只爬取前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 logging +from typing import List +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 = [] + pbar = tqdm.tqdm(total=size * 10, desc='多进程爬取进度', unit='条', ncols=80) + + 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: + data = result.get() + title_list += spider.parse(data) + + lock.acquire() + pbar.update(size) + lock.release() + + spider.save(title_list) + pbar.close() + logging.info("爬取完成") + + +if __name__ == "__main__": + main(keywords=['灾害'], + begin_date='2018-01-01', + end_date='2018-12-31', + size=10) diff --git a/B 高性能模式/020 多线程.py b/B 高性能模式/020 多线程.py index e69de29..19bc3c4 100644 --- a/B 高性能模式/020 多线程.py +++ b/B 高性能模式/020 多线程.py @@ -0,0 +1,89 @@ +""" +根据提供的关键词列表,爬取天水市人民政府网站上指定日期内与关键词相关的新闻的标题,并将其存储至数据库中。 + +考虑到相关因素,因此本代码只爬取前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 +import threading +import util +import logging +from typing import List + +import tqdm + +lock = threading.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) + + pbar = tqdm.tqdm(total=size * 10, desc='多线程爬取进度', unit='条', ncols=80) + title_list = [] + tasks = [] + with ThreadPoolExecutor(max_workers=5) as executor: + for keyword in keywords: + 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): + data = future.result() + title_list += spider.parse(data) + + spider.save(title_list) + pbar.close() + logging.info("爬取完成") + + +if __name__ == "__main__": + main(keywords=['灾害'], + begin_date='2018-01-01', + end_date='2018-12-31', + size=10) diff --git a/B 高性能模式/030 协程.py b/B 高性能模式/030 协程.py index e69de29..291736f 100644 --- a/B 高性能模式/030 协程.py +++ b/B 高性能模式/030 协程.py @@ -0,0 +1,89 @@ +""" +根据提供的关键词列表,爬取天水市人民政府网站上指定日期内与关键词相关的新闻的标题,并将其存储至数据库中。 + +考虑到相关因素,因此本代码只爬取前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 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) + title_list = [] + + 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 = [ + gevent.spawn(fetch_and_parse, keyword, current) for keyword in keywords + for current in range(1, 11) + ] + + gevent.joinall(jobs) + + spider.save(title_list) + pbar.close() + logging.info("爬取完成") + + +if __name__ == "__main__": + main(keywords=['灾害'], + begin_date='2018-01-01', + end_date='2018-12-31', + size=10) diff --git a/B 高性能模式/040 异步.py b/B 高性能模式/040 异步.py index e69de29..fb2b1c1 100644 --- a/B 高性能模式/040 异步.py +++ b/B 高性能模式/040 异步.py @@ -0,0 +1,85 @@ +""" +根据提供的关键词列表,爬取天水市人民政府网站上指定日期内与关键词相关的新闻的标题,并将其存储至数据库中。 + +考虑到相关因素,因此本代码只爬取前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 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: + 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) + title_list = [] + 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) + pbar.close() + logging.info("爬取完成") + + +if __name__ == "__main__": + asyncio.run( + main_async(keywords=['灾害'], + begin_date='2018-01-01', + end_date='2018-12-31', + size=10)) diff --git a/B 高性能模式/readme.md b/B 高性能模式/readme.md index f464b8e..7470dbf 100644 --- a/B 高性能模式/readme.md +++ b/B 高性能模式/readme.md @@ -8,4 +8,18 @@ # 任务 -# 讨论分析 \ No newline at end of file +# 讨论分析 +普通做法连续进行了五次测试,时间分别为34.231s、34.091s、34.164s、34.226s、33.958s,平均时间为34.134s +多进程(进程数=5)连续进行了五次测试,时间分别为7.719s、7.716s、7.690s、7.730s、7.711s,平均时间为7.7132s +多线程(线程数=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秒 + +可以看出,协程的性能最好,普通做法的性能最差,多线程、多进程和异步的性能介于两者之间。 +考虑到多进程和多线程是故意开的5个进程和线程,而协程是单线程,所以协程的性能最好。 +另外,异步的性能最差,可能是由于异步的并发模型需要频繁地切换线程,导致性能下降。 +总的来说,协程的性能最好,多线程和多进程的性能介于两者之间,普通做法的性能最差。 + +# 总结 +协程的性能最好,多线程和多进程的性能介于两者之间,普通做法的性能最差。 \ No newline at end of file diff --git a/B 高性能模式/util.py b/B 高性能模式/util.py index 384717d..5d7495a 100644 --- a/B 高性能模式/util.py +++ b/B 高性能模式/util.py @@ -1,4 +1,188 @@ +""" -################################################################################ -# 本主题通用代码 -################################################################################ +""" +import re +import time +import functools +import json +import asyncio +import requests +from typing import Any, Dict, List + + +class Spider: + """ + 爬虫类。 + + Args: + keywords (List[str]): 用于搜索新闻的关键词列表 + begin_date (str): 开始日期,用于搜索 + end_date (str): 结束日期,用于搜索 + size (int): 一次请求返回的新闻或政策的最大数量 + + Attributes: + URL (str): 网址 + """ + # 天水市人民政府网站 + 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}') + return result + + return wrapper + + +def timeit_async(func): + + @functools.wraps(func) + async def wrapper(*args, **kwargs): + start = time.time() + result = await func(*args, **kwargs) + + print(f'{func.__name__} cost: {time.time() - start}') + return result + + return wrapper