基于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了。

总结

分享就到这里了,请多指教~

posted @ 2024-06-13 14:11  七夜魔手  阅读(17)  评论(0编辑  收藏  举报  来源