这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战
事情还要从一次写爬虫开始,在一个pandas DataFrame上,我用某一列做参数爬取对应的信息,但是由于那一列有几万个,导致爬取速度非常之慢,而且要是中途网断了或者被封ip了,也无法感知,中断了以后,前面爬的也功亏一篑了,显然这是一种很不合理的爬取策略,属于是被爬虫框架惯坏了。于是我考虑到要把dataFrame分割一下;还要把请求并发一下,不然效率太低,还可以加点异常处理,甚至是启用个代理IP池。。。
于是去研究了下python的多线程,写出了如下的代码:
import pandas as pd
#import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
import requests
def req(df):
#具体爬虫的实现
return 'blahblah'
def run_crawl(name):
df = pd.read_excel('{}.xlsx'.format(name))
print(len(df))
df_list = []
i = 0
while i < len(df):
df_list.append(df.iloc[i:i+200])
i += 200
final_df = pd.DataFrame(columns=df_list[0].columns)
with ThreadPoolExecutor(max_workers=5) as executor:
pending_dataFrame = {executor.submit(req, df): df for df in df_list}
for future in as_completed(pending_dataFrame):
final_df = final_df.append(future.result())
final_df.to_excel('../scripts/{}_add.xlsx'.format(name),index=False,engine='xlsxwriter')
这里主要使用了concurrent库的线程池轮子ThreadPoolExecutor,结合分割好的DataFrame数组,用executor.submit语法构造了一个任务池pending_dataFrame,as_complete方法实现了获取网络请求的回调结果,而(max_workers=5)限制了并发请求的数量,一方面控制内存的消耗,另一方面也是不要同时产生太多请求,这对服务方不好。
对比JS中的操作,简单一句Promise.all([pending_dataFrame]).then()就可以实现上面的操作(并发限制要自己再包裹一下),不得不感叹JS在设计上单线程➕事件循环的结构,带来了很多编码上的便利。
当然,虽然常常在关于NodeJS的文章中看到其擅长高并发I/O的论述,深究下去,这种描述有点问题,从上面的例子上来看,JS的优势更多在于其语言的设计原生地对异步编程友好,并且底层自动维护线程池,实际上,任何语言(比如本文python的concurrent轮子)都能够达到这种效果,因而JS拥有的更多是设计思维和语法上的便利。