基于tushare使用同步、asyncio和线程池提高爬取速度
数据来源:tushare 署名:406940
tushare是一个股票行情数据接口,通过它能获取到大量的股票数据,数据一多,必然影响爬取速度,本章我要展示的是在同步、asyncio和线程池三种情况下去爬tushare六支差不多一年的历史分笔数据
同步
同步是在一个多线程下执行,程序按照代码执行顺序一个一个来,不会乱掉,更不会出现代码没有执行完就执行下面的代码,
import time
import tushare as ts
import pandas as pd
from openpyxl import load_workbook
import redis
import logging
from stock.history.database.redis_db import pool
from stock.history import setting
from stock.history.lib.make_dir import create_dir, file_exist
# 简单的写,同步方式爬取
def handle_tushare():
try:
# 股票代号
code = '600028'
for i in range(con.llen('day_time')):
# 历史交易记录
day_time = con.lindex('day_time', index=i).decode()
print('当前获取的是股票代号为{}在{}时间的数据'.format(code, day_time))
try:
df = ts.get_tick_data(code, date=day_time, src='tt')
print(df.tail)
# 数据保存文件
# 验证文件是否存在,若存在,则在原有的基础上打开添加 若不存在,则在新增文件
file_path = setting.DATA_FILE + '\\' + code + '\\' + day_time[5:7] + '.xlsx'
print(file_path)
if file_exist(file_path):
book = load_workbook(file_path)
writer = pd.ExcelWriter(file_path, engine='openpyxl')
writer.book = book
df.to_excel(writer, sheet_name=day_time[8:], index=None)
writer.save() # 必须保存
else:
df.to_excel(file_path, sheet_name=day_time[8:], index=None)
print('股票代号为{}在{}时间的数据获取成功~~~'.format(code, day_time))
except Exception as e:
with open(setting.LOG_PATH, 'a') as f:
f.write('error: {}\n股票代号为{}在{}时间的数据获取不到\t{}\n'.format(
e, code, day_time, time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())))
logging.warning('股票代号为{}在{}时间的数据获取不到'.format(code, day_time))
except Exception as e:
print('error: {0}'.format(e))
# 将数据重新写入
con.rpush('stock_code', code)
if __name__ == '__main__':
handle_tushare()
下载完六支股票的数据花费了大约30分钟,太慢了
异步asyncio
异步是在一个多线程下执行,当接取一个任务,它会直接给后台任务队列,在接下一个任务,一直一直这样,谁的先读取完先执行谁的
import time
import tushare as ts
import pandas as pd
from openpyxl import load_workbook
import redis
import logging
import asyncio
from stock.history.database.redis_db import pool
from stock.history import setting
from stock.history.lib.make_dir import create_dir, file_exist
# 创建redis实例对象
con = redis.Redis(
connection_pool=pool
)
# 创建异步循环体对象
loop = asyncio.get_event_loop()
# 构造异步函数
async def parse(code, day_time):
try:
# 调用tushare历史分笔数据
df = ts.get_tick_data(code, date=day_time, src='tt')
"""
数据保存文件
验证文件是否存在,若存在,则在原有的基础上打开添加 若不存在,则直接新增文件
TODO:数据以每月为一个文件存储,每天一个sheet,每次都得打开文件,后期优化数据每月一次性存储
"""
# 拼接文件保存路径
file_path = setting.DATA_FILE + '\\' + code + '\\' + day_time[5:7] + '.xlsx'
# 文件已存在,则在原有的基础上打开添加并保存
if file_exist(file_path):
book = load_workbook(file_path)
writer = pd.ExcelWriter(file_path, engine='openpyxl')
writer.book = book
df.to_excel(writer, sheet_name=day_time[8:], index=None)
writer.save() # 必须保存
else:
# 文件不存在,直接新增文件
df.to_excel(file_path, sheet_name=day_time[8:], index=None)
print('股票代号为{}在{}时间的数据获取成功~~~'.format(code, day_time))
except Exception as e:
# 将错误日志保存至log文件
with open(setting.LOG_PATH, 'a') as f:
f.write('error: {}\n股票代号为{}在{}时间的数据获取不到\t{}\n'.format(
e, code, day_time, time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())))
logging.warning('股票代号为{}在{}时间的数据获取不到'.format(code, day_time))
def run():
while True:
# 获取股票代号
try:
# 从redis获取code,当code取出错误时,表示没有元素,抛出异常终止循环
code = con.lpop('stock_code').decode()
except:
break
# 创建路径--一个股票一个文件夹
create_dir(code)
tasks = []
# 从redis中循环获取时间
for i in range(con.llen('day_time')):
# 历史交易记录
day_time = con.lindex('day_time', index=i).decode()
print('当前获取的是股票代号为{}在{}时间的数据'.format(code, day_time))
# 创建循环队列,将每次调用赋值给task,再添加到tasks
task = asyncio.ensure_future(parse(code, day_time))
tasks.append(task)
# 等待循环队列执行完成
loop.run_until_complete(asyncio.gather(*tasks))
# 关闭redis和循环体对象
loop.close()
con.close()
if __name__ == '__main__':
star = time.time()
run()
print('最终耗时:{}'.format(time.time() - star))
下载完数据花费了三百多秒,好多了,但是还是不行,股票大约有四千多支呢,还是太长了,必须优化代码
查看代码,发现每次爬取下来数据都要打开一次文件,对于单线程异步来说,频繁的文件I/O操作是会影响它的爬取速度,所以优化一下代码
异步asyncio优化后
import time
import tushare as ts
import pandas as pd
from openpyxl import load_workbook
import redis
import logging
import asyncio
from tqdm import tqdm
from stock.history.database.redis_db import pool
from stock.history import setting
from stock.history.lib.make_dir import create_dir, file_exist
# 创建redis实例对象
con = redis.Redis(
connection_pool=pool
)
# 创建异步循环体对象
loop = asyncio.get_event_loop()
# 构造异步函数
async def parse(code, day_time):
try:
# 调用tushare历史分笔数据
df = ts.get_tick_data(code, date=day_time, src='tt', retry_count=1)
print('股票代号为{}在{}时间的数据获取成功'.format(code, day_time))
df['day_time'] = [day_time] * len(df)
return df
except Exception as e:
# 将错误日志保存至log文件
with open(setting.LOG_PATH, 'a') as f:
f.write('error: {}\n股票代号为{}在{}时间的数据获取不到\t{}\n'.format(
e, code, day_time, time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())))
# logging.warning('股票代号为{}在{}时间的数据获取不到'.format(code, day_time))
def run():
while True:
stock_data = pd.DataFrame(columns=['time', 'price', 'change', 'volume', 'amount', 'type'])
try:
# 从redis获取code,当code取出错误时,表示没有元素,抛出异常终止循环
code = con.lpop('stock_code').decode()
except:
break
tasks = []
# 从redis中循环获取时间
for i in tqdm(range(con.llen('day_time')), desc=code):
# 历史交易记录
day_time = con.lindex('day_time', index=i).decode()
# print('当前获取的是股票代号为{}在{}时间的数据'.format(code, day_time))
# 创建循环队列,将每次调用赋值给task,再添加到tasks
task = asyncio.ensure_future(parse(code, day_time))
tasks.append(task)
# 等待循环队列执行完成
result = loop.run_until_complete(asyncio.gather(*tasks))
# stock_data = pd.concat([stock_data, data]) for data in result
for data in result:
stock_data = pd.concat([stock_data, data])
stock_data.to_csv(setting.DATA_FILE + '\\' + code + '.csv', index=False)
# 关闭redis和循环体对象
loop.close()
con.close()
if __name__ == '__main__':
star = time.time()
run()
print('最终耗时:{}'.format(time.time() - star))
在每支股票数据全部请求完再保存,这样现在就只需要五六十秒了
线程池
线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。通过线程池我们可以自定义多个线程同步执行
线程池优化前
import time
import tushare as ts
import pandas as pd
from openpyxl import load_workbook
from concurrent.futures import ThreadPoolExecutor # 线程池
import redis
import logging # 日志模块
# 自定义功能模块
from stock.history.database.redis_db import pool
from stock.history import setting
from stock.history.lib.make_dir import create_dir, file_exist
class PoolStock:
def __init__(self, con_redis, code):
self.con = con_redis
self.code = code
self.parse()
def parse(self):
global day_time
for i in range(self.con.llen('day_time')):
try:
# 获取时间
day_time = self.con.lindex('day_time', index=i).decode()
print('当前获取的是股票代号为{}在{}时间的数据'.format(self.code, day_time))
# 调取数据
df = ts.get_tick_data(self.code, date=day_time, src='tt')
"""
数据保存文件
验证文件是否存在,若存在,则在原有的基础上打开添加 若不存在,则直接新增文件
TODO:数据以每月为一个文件存储,每天一个sheet,每次都得打开文件,后期优化数据每月一次性存储
"""
# 拼接文件保存路径
file_path = setting.DATA_FILE + '\\' + self.code + '\\' + day_time[5:7] + '.xlsx'
# 文件已存在,则在原有的基础上打开添加并保存
if file_exist(file_path):
book = load_workbook(file_path)
writer = pd.ExcelWriter(file_path, engine='openpyxl')
writer.book = book
df.to_excel(writer, sheet_name=day_time[8:], index=None)
writer.save() # 必须保存
# 文件不存在,直接新增文件
else:
df.to_excel(file_path, sheet_name=day_time[8:], index=None)
print('股票代号为{}在{}时间的数据获取成功~~~'.format(self.code, day_time))
except Exception as e:
# 将错误日志保存至log文件
with open(setting.LOG_PATH, 'a') as f:
f.write('error: {}\n股票代号为{}在{}时间的数据获取不到\t{}\n'.format(
e, self.code, day_time, time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())))
logging.warning('股票代号为{}在{}时间的数据获取不到'.format(self.code, day_time))
def main():
star = time.time()
# 创建线程池对象和最大数
thread_pool = ThreadPoolExecutor(max_workers=5)
# 创建redis对象
con = redis.Redis(
connection_pool=pool
)
while True:
try:
# 获取股票代号
code = con.lpop('stock_code').decode()
# 创建路径--一个股票对应一个文件夹
create_dir(code)
# 线程池启动程序
# 这里虽然传入了两个参数,但是真正并发的只有code,con是固定参数
thread_pool.submit(PoolStock, con, code)
except:
break
# 关闭所有线程
thread_pool.shutdown(wait=True)
con.close()
print('最终耗时:{}'.format(time.time() - star))
if __name__ == '__main__':
main()
跟异步asyncio优化前一样需要较长时间,不过比异步好点。多线程对于文件I/O操作有优势,我们来试试优化
线程池优化后
import time
import eventlet
import tushare as ts
import pandas as pd
from openpyxl import load_workbook
from concurrent.futures import ThreadPoolExecutor # 线程池
import redis
import logging # 日志模块
from tqdm import tqdm
# 自定义功能模块
from stock.history.database.redis_db import pool
from stock.history import setting
from stock.history.lib.make_dir import create_dir, file_exist
class PoolStock:
def __init__(self, con_redis, code):
self.con = con_redis
self.code = code
self.parse()
def parse(self):
global day_time
stock_data = pd.DataFrame(columns=['time', 'price', 'change', 'volume', 'amount', 'type'])
for i in tqdm(range(self.con.llen('day_time')), desc=self.code):
try:
# 获取时间
day_time = self.con.lindex('day_time', index=i).decode()
eventlet.monkey_patch()
with eventlet.Timeout(2, False): # 设置超时时间为2秒
df = ts.get_tick_data(self.code, date=day_time, src='tt', retry_count=1)
df['day_time'] = [day_time]*len(df)
stock_data = pd.concat([stock_data, df])
except Exception as e:
# 将错误日志保存至log文件
with open(setting.LOG_PATH, 'a') as f:
f.write('error: {}\n股票代号为{}在{}时间的数据获取不到\t{}\n'.format(
e, self.code, day_time, time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())))
# logging.warning('股票代号为{}在{}时间的数据获取不到'.format(self.code, day_time))
stock_data.to_csv(setting.DATA_FILE + '\\' + self.code + '.csv', index=False)
def main():
star = time.time()
# 创建线程池对象和最大数
thread_pool = ThreadPoolExecutor(max_workers=5)
# 创建redis对象
con = redis.Redis(
connection_pool=pool
)
while True:
try:
# 获取股票代号
code = con.lpop('stock_code').decode()
# 创建路径--一个股票对应一个文件夹
# create_dir(code)
# 线程池启动程序
# 这里虽然传入了两个参数,但是真正并发的只有code,con是固定参数
thread_pool.submit(PoolStock, con, code)
except:
break
# 关闭所有线程
thread_pool.shutdown(wait=True)
con.close()
print('最终耗时:{}'.format(time.time() - star))
if __name__ == '__main__':
main()
这下爬取完只需要二三十秒,比异步快了好多
异步asyncio加线程池
见识到异步asyncio与线程池的优势,琢磨着两者相加会不会效果更好,思路是异步asyncio去请求数据,多线程执行文件I/O操作保存数据,想好很好,不过最后出现bug了。
总结
分享就到这里了,请多指教~
本文来自博客园,作者:七夜魔手,转载请注明原文链接:https://www.cnblogs.com/ranbox/p/18461090