stock-tracker
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding:utf-8 -*-
|
||||
"""
|
||||
Date: 2020/2/13 21:21
|
||||
Desc:
|
||||
"""
|
||||
@@ -0,0 +1,10 @@
|
||||
# !/usr/bin/env python
|
||||
"""
|
||||
Date: 2024/4/7 15:30
|
||||
Desc: 通用变量
|
||||
"""
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Chrome/114.0.0.0 Safari/537.36"
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
class AkshareConfig:
|
||||
_instance = None
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._instance.proxies = None
|
||||
return cls._instance
|
||||
|
||||
@classmethod
|
||||
def set_proxies(cls, proxies):
|
||||
cls().proxies = proxies
|
||||
|
||||
@classmethod
|
||||
def get_proxies(cls):
|
||||
return cls().proxies
|
||||
|
||||
|
||||
config = AkshareConfig()
|
||||
|
||||
|
||||
# 导出 set_proxies 函数
|
||||
def set_proxies(proxies):
|
||||
config.set_proxies(proxies)
|
||||
|
||||
|
||||
def get_proxies():
|
||||
return config.get_proxies()
|
||||
|
||||
|
||||
class ProxyContext:
|
||||
def __init__(self, proxies):
|
||||
self.proxies = proxies
|
||||
self.old_proxies = None
|
||||
|
||||
def __enter__(self):
|
||||
self.old_proxies = config.get_proxies()
|
||||
config.set_proxies(self.proxies)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
config.set_proxies(self.old_proxies)
|
||||
return False # 不处理异常
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,77 @@
|
||||
# !/usr/bin/env python
|
||||
"""
|
||||
Date: 2025/3/10 18:00
|
||||
Desc: 通用帮助函数
|
||||
"""
|
||||
|
||||
import math
|
||||
import random
|
||||
import time
|
||||
from typing import List, Dict
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from akshare.utils.request import request_with_retry
|
||||
from akshare.utils.tqdm import get_tqdm
|
||||
|
||||
|
||||
def fetch_paginated_data(url: str, base_params: Dict, timeout: int = 15):
|
||||
"""
|
||||
东方财富-分页获取数据并合并结果
|
||||
https://quote.eastmoney.com/f1.html?newcode=0.000001
|
||||
:param url: 股票代码
|
||||
:type url: str
|
||||
:param base_params: 基础请求参数
|
||||
:type base_params: dict
|
||||
:param timeout: 请求超时时间
|
||||
:type timeout: str
|
||||
:return: 合并后的数据
|
||||
:rtype: pandas.DataFrame
|
||||
"""
|
||||
# 复制参数以避免修改原始参数
|
||||
params = base_params.copy()
|
||||
# 获取第一页数据,用于确定分页信息
|
||||
r = request_with_retry(url, params=params, timeout=timeout)
|
||||
data_json = r.json()
|
||||
# 计算分页信息
|
||||
per_page_num = len(data_json["data"]["diff"])
|
||||
total_page = math.ceil(data_json["data"]["total"] / per_page_num)
|
||||
# 存储所有页面数据
|
||||
temp_list = []
|
||||
# 添加第一页数据
|
||||
temp_list.append(pd.DataFrame(data_json["data"]["diff"]))
|
||||
# 获取进度条
|
||||
tqdm = get_tqdm()
|
||||
# 获取剩余页面数据
|
||||
for page in tqdm(range(2, total_page + 1), leave=False):
|
||||
params.update({"pn": page})
|
||||
# 添加随机延迟,避免请求过于频繁
|
||||
time.sleep(random.uniform(0.5, 1.5))
|
||||
r = request_with_retry(url, params=params, timeout=timeout)
|
||||
data_json = r.json()
|
||||
inner_temp_df = pd.DataFrame(data_json["data"]["diff"])
|
||||
temp_list.append(inner_temp_df)
|
||||
# 合并所有数据
|
||||
temp_df = pd.concat(temp_list, ignore_index=True)
|
||||
temp_df["f3"] = pd.to_numeric(temp_df["f3"], errors="coerce")
|
||||
temp_df.sort_values(by=["f3"], ascending=False, inplace=True, ignore_index=True)
|
||||
temp_df.reset_index(inplace=True)
|
||||
temp_df["index"] = temp_df["index"].astype(int) + 1
|
||||
return temp_df
|
||||
|
||||
|
||||
def set_df_columns(df: pd.DataFrame, cols: List[str]) -> pd.DataFrame:
|
||||
"""
|
||||
设置 pandas.DataFrame 为空的情况
|
||||
:param df: 需要设置命名的数据框
|
||||
:type df: pandas.DataFrame
|
||||
:param cols: 字段的列表
|
||||
:type cols: list
|
||||
:return: 重新设置后的数据
|
||||
:rtype: pandas.DataFrame
|
||||
"""
|
||||
if df.shape == (0, 0):
|
||||
return pd.DataFrame(data=[], columns=cols)
|
||||
else:
|
||||
df.columns = cols
|
||||
return df
|
||||
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
利用多进行执行 js 代码方案
|
||||
|
||||
1. 未能解决 gevent 调用问题
|
||||
2. 导致 js 代码执行缓慢
|
||||
3. 该方案废弃,这里仅作参考
|
||||
|
||||
同时发现 gevent 里面无法调用异步的接口
|
||||
"""
|
||||
|
||||
import concurrent.futures
|
||||
|
||||
|
||||
# 定义在模块级别的函数
|
||||
def js_executor_function(js_code_str, method, args):
|
||||
"""在新进程中执行 JavaScript 代码的函数"""
|
||||
from py_mini_racer import MiniRacer
|
||||
|
||||
js_code = MiniRacer()
|
||||
js_code.eval(js_code_str)
|
||||
|
||||
if method == "call":
|
||||
fn_name = args[0]
|
||||
fn_args = args[1:]
|
||||
return js_code.call(fn_name, *fn_args)
|
||||
elif method == "eval":
|
||||
return js_code.eval(args[0])
|
||||
else:
|
||||
raise ValueError(f"不支持的方法: {method}")
|
||||
|
||||
|
||||
def execute_js_in_executor(js_code_str, method, *args, timeout=30):
|
||||
"""
|
||||
使用 ProcessPoolExecutor 在独立进程中执行 JavaScript
|
||||
|
||||
参数:
|
||||
js_code_str: JavaScript 代码字符串
|
||||
method: 'call' 或 'eval'
|
||||
args: 如果 method 是 'call',第一个参数是函数名,后续是函数参数
|
||||
如果 method 是 'eval',只需提供一个参数:要评估的代码
|
||||
timeout: 超时时间(秒)
|
||||
|
||||
返回:
|
||||
执行结果
|
||||
"""
|
||||
with concurrent.futures.ProcessPoolExecutor(max_workers=1) as executor:
|
||||
future = executor.submit(js_executor_function, js_code_str, method, args)
|
||||
try:
|
||||
return future.result(timeout=timeout)
|
||||
except concurrent.futures.TimeoutError:
|
||||
# 清理资源并抛出超时异常
|
||||
executor.shutdown(wait=False)
|
||||
raise TimeoutError("JavaScript 执行超时")
|
||||
@@ -0,0 +1,64 @@
|
||||
# !/usr/bin/env python
|
||||
"""
|
||||
Date: 2025/12/31
|
||||
Desc: HTTP 请求工具函数
|
||||
"""
|
||||
|
||||
import random
|
||||
import time
|
||||
from typing import Dict, Tuple
|
||||
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
|
||||
|
||||
def request_with_retry(
|
||||
url: str,
|
||||
params: Dict = None,
|
||||
timeout: int = 15,
|
||||
max_retries: int = 3,
|
||||
base_delay: float = 1.0,
|
||||
random_delay_range: Tuple[float, float] = (0.5, 1.5),
|
||||
) -> requests.Response:
|
||||
"""
|
||||
带重试机制的 HTTP GET 请求
|
||||
:param url: 请求 URL
|
||||
:type url: str
|
||||
:param params: 请求参数
|
||||
:type params: dict
|
||||
:param timeout: 超时时间(秒)
|
||||
:type timeout: int
|
||||
:param max_retries: 最大重试次数
|
||||
:type max_retries: int
|
||||
:param base_delay: 基础延迟时间(秒),用于指数退避
|
||||
:type base_delay: float
|
||||
:param random_delay_range: 随机延迟范围(秒)
|
||||
:type random_delay_range: tuple
|
||||
:return: Response 对象
|
||||
:rtype: requests.Response
|
||||
:raises: 最后一次请求的异常
|
||||
"""
|
||||
last_exception = None
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
# 每次请求创建新的 Session,避免复用连接
|
||||
with requests.Session() as session:
|
||||
# 禁用连接池复用
|
||||
adapter = HTTPAdapter(pool_connections=1, pool_maxsize=1)
|
||||
session.mount("http://", adapter)
|
||||
session.mount("https://", adapter)
|
||||
|
||||
response = session.get(url, params=params, timeout=timeout)
|
||||
response.raise_for_status()
|
||||
return response
|
||||
|
||||
except (requests.RequestException, ValueError) as e:
|
||||
last_exception = e
|
||||
|
||||
if attempt < max_retries - 1:
|
||||
# 指数退避 + 随机抖动
|
||||
delay = base_delay * (2**attempt) + random.uniform(*random_delay_range)
|
||||
time.sleep(delay)
|
||||
|
||||
raise last_exception
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding:utf-8 -*-
|
||||
"""
|
||||
Date: 2020/2/13 21:22
|
||||
Desc: 存储和读取 Token 文件
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from akshare.pro import cons
|
||||
|
||||
|
||||
def set_token(token):
|
||||
df = pd.DataFrame([token], columns=["token"])
|
||||
user_home = os.path.expanduser("~")
|
||||
fp = os.path.join(user_home, cons.TOKEN_F_P)
|
||||
df.to_csv(fp, index=False)
|
||||
|
||||
|
||||
def get_token():
|
||||
user_home = os.path.expanduser("~")
|
||||
fp = os.path.join(user_home, cons.TOKEN_F_P)
|
||||
if os.path.exists(fp):
|
||||
df = pd.read_csv(fp)
|
||||
return str(df.iloc[0]["token"])
|
||||
else:
|
||||
print(cons.TOKEN_ERR_MSG)
|
||||
return
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
@@ -0,0 +1,27 @@
|
||||
def get_tqdm(enable: bool = True):
|
||||
"""
|
||||
返回适用于当前环境的 tqdm 对象。
|
||||
|
||||
Args:
|
||||
enable (bool): 是否启用进度条。默认为 True。
|
||||
|
||||
Returns:
|
||||
tqdm 对象。
|
||||
"""
|
||||
if not enable:
|
||||
# 如果进度条被禁用,返回一个不显示进度条的 tqdm 对象
|
||||
return lambda iterable, *args, **kwargs: iterable
|
||||
|
||||
try:
|
||||
# 尝试检查是否在 jupyter notebook 环境中,有利于退出进度条
|
||||
# noinspection PyUnresolvedReferences
|
||||
shell = get_ipython().__class__.__name__
|
||||
if shell == "ZMQInteractiveShell":
|
||||
from tqdm.notebook import tqdm
|
||||
else:
|
||||
from tqdm import tqdm
|
||||
except (NameError, ImportError):
|
||||
# 如果不在 Jupyter 环境中,就使用标准 tqdm
|
||||
from tqdm import tqdm
|
||||
|
||||
return tqdm
|
||||
Reference in New Issue
Block a user