rquote 0.3.4__py3-none-any.whl → 0.3.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rquote/__init__.py +53 -5
- rquote/api/__init__.py +32 -0
- rquote/api/lists.py +175 -0
- rquote/api/price.py +73 -0
- rquote/api/stock_info.py +49 -0
- rquote/api/tick.py +47 -0
- rquote/cache/__init__.py +9 -0
- rquote/cache/base.py +26 -0
- rquote/cache/memory.py +77 -0
- rquote/config.py +42 -0
- rquote/data_sources/__init__.py +10 -0
- rquote/data_sources/base.py +21 -0
- rquote/data_sources/sina.py +92 -0
- rquote/data_sources/tencent.py +90 -0
- rquote/exceptions.py +40 -0
- rquote/factors/__init__.py +8 -0
- rquote/factors/technical.py +150 -0
- rquote/markets/__init__.py +14 -0
- rquote/markets/base.py +49 -0
- rquote/markets/cn_stock.py +186 -0
- rquote/markets/factory.py +82 -0
- rquote/markets/future.py +92 -0
- rquote/markets/hk_stock.py +46 -0
- rquote/markets/us_stock.py +69 -0
- rquote/parsers/__init__.py +8 -0
- rquote/parsers/kline.py +104 -0
- rquote/plots.py +1 -1
- rquote/utils/__init__.py +13 -0
- rquote/utils/date.py +40 -0
- rquote/utils/helpers.py +23 -0
- rquote/utils/http.py +112 -0
- rquote/utils/logging.py +25 -0
- rquote/utils/web.py +104 -0
- rquote/utils.py +9 -195
- rquote-0.3.5.dist-info/METADATA +486 -0
- rquote-0.3.5.dist-info/RECORD +38 -0
- rquote/main.py +0 -503
- rquote-0.3.4.dist-info/METADATA +0 -286
- rquote-0.3.4.dist-info/RECORD +0 -8
- {rquote-0.3.4.dist-info → rquote-0.3.5.dist-info}/WHEEL +0 -0
- {rquote-0.3.4.dist-info → rquote-0.3.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
市场工厂
|
|
4
|
+
"""
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from .base import Market
|
|
7
|
+
from .cn_stock import CNStockMarket
|
|
8
|
+
from .hk_stock import HKStockMarket
|
|
9
|
+
from .us_stock import USStockMarket
|
|
10
|
+
from .future import FutureMarket
|
|
11
|
+
from ..data_sources import TencentDataSource, SinaDataSource
|
|
12
|
+
from ..cache import Cache, MemoryCache
|
|
13
|
+
from ..config import default_config
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MarketFactory:
|
|
17
|
+
"""市场工厂类"""
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def create_from_symbol(symbol: str, cache: Optional[Cache] = None) -> Market:
|
|
21
|
+
"""
|
|
22
|
+
根据股票代码创建对应的市场实例
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
symbol: 股票代码
|
|
26
|
+
cache: 缓存对象,如果为None且配置启用缓存,则创建MemoryCache
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Market实例
|
|
30
|
+
"""
|
|
31
|
+
# 处理缓存
|
|
32
|
+
if cache is None and default_config.cache_enabled:
|
|
33
|
+
cache = MemoryCache(ttl=default_config.cache_ttl)
|
|
34
|
+
|
|
35
|
+
# 标准化代码
|
|
36
|
+
if symbol[0] in ['0', '1', '3', '5', '6']:
|
|
37
|
+
symbol = 'sh' + symbol if symbol[0] in ['5', '6'] else 'sz' + symbol
|
|
38
|
+
|
|
39
|
+
# 根据前缀选择市场
|
|
40
|
+
if symbol[:2] == 'BK':
|
|
41
|
+
# 板块,使用A股市场处理
|
|
42
|
+
return CNStockMarket(TencentDataSource(), cache)
|
|
43
|
+
elif symbol[:2] == 'fu':
|
|
44
|
+
return FutureMarket(SinaDataSource(), cache)
|
|
45
|
+
elif symbol[:2] == 'pt':
|
|
46
|
+
# PT代码,使用A股市场处理
|
|
47
|
+
return CNStockMarket(TencentDataSource(), cache)
|
|
48
|
+
elif symbol[:2] in ['sh', 'sz']:
|
|
49
|
+
return CNStockMarket(TencentDataSource(), cache)
|
|
50
|
+
elif symbol[:2] == 'hk':
|
|
51
|
+
return HKStockMarket(TencentDataSource(), cache)
|
|
52
|
+
elif symbol[:2] == 'us':
|
|
53
|
+
return USStockMarket(TencentDataSource(), cache)
|
|
54
|
+
else:
|
|
55
|
+
raise ValueError(f'Unsupported symbol format: {symbol}')
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def create(market_type: str, cache: Optional[Cache] = None) -> Market:
|
|
59
|
+
"""
|
|
60
|
+
根据市场类型创建市场实例
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
market_type: 市场类型 ('cn_stock', 'hk_stock', 'us_stock', 'future')
|
|
64
|
+
cache: 缓存对象
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Market实例
|
|
68
|
+
"""
|
|
69
|
+
if cache is None and default_config.cache_enabled:
|
|
70
|
+
cache = MemoryCache(ttl=default_config.cache_ttl)
|
|
71
|
+
|
|
72
|
+
if market_type == 'cn_stock':
|
|
73
|
+
return CNStockMarket(TencentDataSource(), cache)
|
|
74
|
+
elif market_type == 'hk_stock':
|
|
75
|
+
return HKStockMarket(TencentDataSource(), cache)
|
|
76
|
+
elif market_type == 'us_stock':
|
|
77
|
+
return USStockMarket(TencentDataSource(), cache)
|
|
78
|
+
elif market_type == 'future':
|
|
79
|
+
return FutureMarket(SinaDataSource(), cache)
|
|
80
|
+
else:
|
|
81
|
+
raise ValueError(f'Unsupported market type: {market_type}')
|
|
82
|
+
|
rquote/markets/future.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
期货市场
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from typing import Tuple
|
|
8
|
+
from .base import Market
|
|
9
|
+
from ..parsers import KlineParser
|
|
10
|
+
from ..exceptions import DataSourceError, ParseError
|
|
11
|
+
from ..utils import hget, logger
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FutureMarket(Market):
|
|
15
|
+
"""期货市场"""
|
|
16
|
+
|
|
17
|
+
def normalize_symbol(self, symbol: str) -> str:
|
|
18
|
+
"""标准化期货代码"""
|
|
19
|
+
if not symbol.startswith('fu'):
|
|
20
|
+
return 'fu' + symbol
|
|
21
|
+
return symbol
|
|
22
|
+
|
|
23
|
+
def get_price(self, symbol: str, sdate: str = '', edate: str = '',
|
|
24
|
+
freq: str = 'day', days: int = 320, fq: str = 'qfq') -> Tuple[str, str, pd.DataFrame]:
|
|
25
|
+
"""获取期货价格数据"""
|
|
26
|
+
symbol = self.normalize_symbol(symbol)
|
|
27
|
+
|
|
28
|
+
# 特殊处理BTC
|
|
29
|
+
if symbol[2:5].lower() == 'btc':
|
|
30
|
+
return self._get_btc_price(symbol)
|
|
31
|
+
|
|
32
|
+
cache_key = f"{symbol}:{sdate}:{edate}:{freq}:{days}"
|
|
33
|
+
cached = self._get_cached(cache_key)
|
|
34
|
+
if cached:
|
|
35
|
+
return cached
|
|
36
|
+
|
|
37
|
+
future_code = symbol[2:] # 去掉'fu'前缀
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
raw_data = self.data_source.fetch_kline(future_code, freq=freq)
|
|
41
|
+
parser = KlineParser()
|
|
42
|
+
df = parser.parse_sina_future_kline(raw_data, freq=freq)
|
|
43
|
+
|
|
44
|
+
result = (symbol, future_code, df)
|
|
45
|
+
self._put_cache(cache_key, result)
|
|
46
|
+
return result
|
|
47
|
+
except (DataSourceError, ParseError) as e:
|
|
48
|
+
logger.warning(f'Failed to fetch {symbol} using new architecture, falling back: {e}')
|
|
49
|
+
return self._get_price_fallback(symbol, future_code, freq)
|
|
50
|
+
|
|
51
|
+
def _get_btc_price(self, symbol: str) -> Tuple[str, str, pd.DataFrame]:
|
|
52
|
+
"""获取比特币价格"""
|
|
53
|
+
url = 'https://quotes.sina.cn/fx/api/openapi.php/BtcService.getDayKLine?symbol=btcbtcusd'
|
|
54
|
+
response = hget(url)
|
|
55
|
+
if not response:
|
|
56
|
+
raise DataSourceError("Failed to fetch BTC data")
|
|
57
|
+
|
|
58
|
+
data = json.loads(response.text)['result']['data'].split('|')
|
|
59
|
+
df = pd.DataFrame([i.split(',') for i in data],
|
|
60
|
+
columns=['date', 'open', 'high', 'low', 'close', 'vol', 'amount'])
|
|
61
|
+
for col in ['open', 'high', 'low', 'close', 'vol', 'amount']:
|
|
62
|
+
df[col] = pd.to_numeric(df[col], errors='coerce')
|
|
63
|
+
df = df.set_index('date').astype(float)
|
|
64
|
+
|
|
65
|
+
result = (symbol, 'BTC', df)
|
|
66
|
+
self._put_cache(symbol, result)
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
def _get_price_fallback(self, symbol: str, future_code: str, freq: str) -> Tuple[str, str, pd.DataFrame]:
|
|
70
|
+
"""降级方法"""
|
|
71
|
+
from ..utils.helpers import load_js_var_json
|
|
72
|
+
|
|
73
|
+
if freq in ('min', '1min', 'minute'):
|
|
74
|
+
url = f'https://stock2.finance.sina.com.cn/futures/api/jsonp.php/var%20t1nf_{future_code}=/InnerFuturesNewService.getMinLine?symbol={future_code}'
|
|
75
|
+
df = pd.DataFrame(load_js_var_json(url))
|
|
76
|
+
df.columns = ['dtime', 'close', 'avg', 'vol', 'hold', 'last_close', 'cur_date']
|
|
77
|
+
for col in ['close', 'avg', 'vol', 'hold']:
|
|
78
|
+
df[col] = pd.to_numeric(df[col], errors='coerce')
|
|
79
|
+
df = df.set_index('dtime')
|
|
80
|
+
result = (future_code, future_code, df)
|
|
81
|
+
else:
|
|
82
|
+
url = f'https://stock2.finance.sina.com.cn/futures/api/jsonp.php/var%20t1nf_{future_code}=/InnerFuturesNewService.getDailyKLine?symbol={future_code}'
|
|
83
|
+
df = pd.DataFrame(load_js_var_json(url))
|
|
84
|
+
df.columns = ['date', 'open', 'high', 'low', 'close', 'vol', 'p', 's']
|
|
85
|
+
for col in ['open', 'high', 'low', 'close', 'vol', 'p', 's']:
|
|
86
|
+
df[col] = pd.to_numeric(df[col], errors='coerce')
|
|
87
|
+
df = df.set_index('date').astype(float)
|
|
88
|
+
result = (symbol, future_code, df)
|
|
89
|
+
|
|
90
|
+
self._put_cache(f"{symbol}:{freq}", result)
|
|
91
|
+
return result
|
|
92
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
港股市场
|
|
4
|
+
"""
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from typing import Tuple
|
|
7
|
+
from .base import Market
|
|
8
|
+
from ..parsers import KlineParser
|
|
9
|
+
from ..exceptions import DataSourceError, ParseError
|
|
10
|
+
from ..utils import logger
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HKStockMarket(Market):
|
|
14
|
+
"""港股市场"""
|
|
15
|
+
|
|
16
|
+
def normalize_symbol(self, symbol: str) -> str:
|
|
17
|
+
"""标准化港股代码"""
|
|
18
|
+
if not symbol.startswith('hk'):
|
|
19
|
+
return 'hk' + symbol
|
|
20
|
+
return symbol
|
|
21
|
+
|
|
22
|
+
def get_price(self, symbol: str, sdate: str = '', edate: str = '',
|
|
23
|
+
freq: str = 'day', days: int = 320, fq: str = 'qfq') -> Tuple[str, str, pd.DataFrame]:
|
|
24
|
+
"""获取港股价格数据"""
|
|
25
|
+
symbol = self.normalize_symbol(symbol)
|
|
26
|
+
|
|
27
|
+
cache_key = f"{symbol}:{sdate}:{edate}:{freq}:{days}:{fq}"
|
|
28
|
+
cached = self._get_cached(cache_key)
|
|
29
|
+
if cached:
|
|
30
|
+
return cached
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
raw_data = self.data_source.fetch_kline(
|
|
34
|
+
symbol, freq=freq, sdate=sdate, edate=edate, days=days, fq=fq
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
parser = KlineParser()
|
|
38
|
+
name, df = parser.parse_tencent_kline(raw_data, symbol)
|
|
39
|
+
|
|
40
|
+
result = (symbol, name, df)
|
|
41
|
+
self._put_cache(cache_key, result)
|
|
42
|
+
return result
|
|
43
|
+
except (DataSourceError, ParseError) as e:
|
|
44
|
+
logger.warning(f'Failed to fetch {symbol}: {e}')
|
|
45
|
+
raise
|
|
46
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
美股市场
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from typing import Tuple
|
|
8
|
+
from .base import Market
|
|
9
|
+
from ..parsers import KlineParser
|
|
10
|
+
from ..exceptions import DataSourceError, ParseError
|
|
11
|
+
from ..utils import hget, logger
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class USStockMarket(Market):
|
|
15
|
+
"""美股市场"""
|
|
16
|
+
|
|
17
|
+
def normalize_symbol(self, symbol: str) -> str:
|
|
18
|
+
"""标准化美股代码"""
|
|
19
|
+
if not symbol.startswith('us'):
|
|
20
|
+
return 'us' + symbol
|
|
21
|
+
return symbol
|
|
22
|
+
|
|
23
|
+
def get_price(self, symbol: str, sdate: str = '', edate: str = '',
|
|
24
|
+
freq: str = 'day', days: int = 320, fq: str = 'qfq') -> Tuple[str, str, pd.DataFrame]:
|
|
25
|
+
"""获取美股价格数据"""
|
|
26
|
+
symbol = self.normalize_symbol(symbol)
|
|
27
|
+
|
|
28
|
+
cache_key = f"{symbol}:{sdate}:{edate}:{freq}:{days}:{fq}"
|
|
29
|
+
cached = self._get_cached(cache_key)
|
|
30
|
+
if cached:
|
|
31
|
+
return cached
|
|
32
|
+
|
|
33
|
+
# 特殊处理分钟数据
|
|
34
|
+
if freq in ('min', '1min', 'minute'):
|
|
35
|
+
return self._get_minute_data(symbol)
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
raw_data = self.data_source.fetch_kline(
|
|
39
|
+
symbol, freq=freq, sdate=sdate, edate=edate, days=days, fq=fq
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
parser = KlineParser()
|
|
43
|
+
name, df = parser.parse_tencent_kline(raw_data, symbol)
|
|
44
|
+
|
|
45
|
+
result = (symbol, name, df)
|
|
46
|
+
self._put_cache(cache_key, result)
|
|
47
|
+
return result
|
|
48
|
+
except (DataSourceError, ParseError) as e:
|
|
49
|
+
logger.warning(f'Failed to fetch {symbol}: {e}')
|
|
50
|
+
raise
|
|
51
|
+
|
|
52
|
+
def _get_minute_data(self, symbol: str) -> Tuple[str, str, pd.DataFrame]:
|
|
53
|
+
"""获取分钟数据"""
|
|
54
|
+
url = f'https://web.ifzq.gtimg.cn/appstock/app/UsMinute/query?_var=min_data_{symbol.replace(".", "")}&code={symbol}'
|
|
55
|
+
response = hget(url)
|
|
56
|
+
if not response:
|
|
57
|
+
raise DataSourceError(f'Failed to fetch minute data for {symbol}')
|
|
58
|
+
|
|
59
|
+
data = json.loads(response.text.split('=')[1])['data'][symbol]
|
|
60
|
+
name = data['qt'][symbol][1]
|
|
61
|
+
df = pd.DataFrame([i.split() for i in data['data']['data']],
|
|
62
|
+
columns=['minute', 'price', 'volume']).set_index('minute')
|
|
63
|
+
for col in ['price', 'volume']:
|
|
64
|
+
df[col] = pd.to_numeric(df[col], errors='coerce')
|
|
65
|
+
|
|
66
|
+
result = (symbol, name, df)
|
|
67
|
+
self._put_cache(f"{symbol}:min", result)
|
|
68
|
+
return result
|
|
69
|
+
|
rquote/parsers/kline.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
K线数据解析器
|
|
4
|
+
"""
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from typing import Dict, Any, Tuple, Optional
|
|
7
|
+
from ..exceptions import ParseError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class KlineParser:
|
|
11
|
+
"""K线数据解析器"""
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def parse_tencent_kline(data: Dict[str, Any], symbol: str) -> Tuple[str, pd.DataFrame]:
|
|
15
|
+
"""
|
|
16
|
+
解析腾讯K线数据
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
data: 腾讯API返回的数据
|
|
20
|
+
symbol: 股票代码
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
(name, DataFrame)
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
symbol_data = data.get('data', {}).get(symbol, {})
|
|
27
|
+
if not symbol_data:
|
|
28
|
+
raise ParseError(f"No data for symbol {symbol}")
|
|
29
|
+
|
|
30
|
+
# 查找时间键
|
|
31
|
+
time_keys = ['day', 'qfqday', 'hfqday', 'week', 'qfqweek', 'hfqweek',
|
|
32
|
+
'month', 'qfqmonth', 'hfqmonth']
|
|
33
|
+
tk = None
|
|
34
|
+
for tkt in time_keys:
|
|
35
|
+
if tkt in symbol_data:
|
|
36
|
+
tk = tkt
|
|
37
|
+
break
|
|
38
|
+
|
|
39
|
+
if not tk:
|
|
40
|
+
raise ParseError(f"No time key found for {symbol}")
|
|
41
|
+
|
|
42
|
+
# 提取名称
|
|
43
|
+
name = ''
|
|
44
|
+
if 'qt' in symbol_data and symbol in symbol_data['qt']:
|
|
45
|
+
qt_data = symbol_data['qt'][symbol]
|
|
46
|
+
if len(qt_data) > 1:
|
|
47
|
+
name = qt_data[1]
|
|
48
|
+
|
|
49
|
+
# 解析K线数据
|
|
50
|
+
kline_data = symbol_data[tk]
|
|
51
|
+
df = pd.DataFrame(
|
|
52
|
+
[j[:6] for j in kline_data],
|
|
53
|
+
columns=['date', 'open', 'close', 'high', 'low', 'vol']
|
|
54
|
+
).set_index('date')
|
|
55
|
+
|
|
56
|
+
# 转换数据类型
|
|
57
|
+
for col in ['open', 'high', 'low', 'close', 'vol']:
|
|
58
|
+
df[col] = pd.to_numeric(df[col], errors='coerce')
|
|
59
|
+
|
|
60
|
+
return name, df
|
|
61
|
+
except Exception as e:
|
|
62
|
+
raise ParseError(f"Failed to parse Tencent kline data: {e}")
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def parse_sina_future_kline(data: Dict[str, Any], freq: str = 'day') -> pd.DataFrame:
|
|
66
|
+
"""
|
|
67
|
+
解析新浪期货K线数据
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
data: 新浪API返回的数据
|
|
71
|
+
freq: 频率
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
DataFrame
|
|
75
|
+
"""
|
|
76
|
+
try:
|
|
77
|
+
raw_data = data.get('data', [])
|
|
78
|
+
if not raw_data:
|
|
79
|
+
raise ParseError("Empty data from Sina")
|
|
80
|
+
|
|
81
|
+
if freq in ('min', '1min', 'minute'):
|
|
82
|
+
# 分钟数据
|
|
83
|
+
df = pd.DataFrame(raw_data)
|
|
84
|
+
if 'dtime' in df.columns:
|
|
85
|
+
df = df.set_index('dtime')
|
|
86
|
+
# 转换数值列
|
|
87
|
+
numeric_cols = ['close', 'avg', 'vol', 'hold']
|
|
88
|
+
for col in numeric_cols:
|
|
89
|
+
if col in df.columns:
|
|
90
|
+
df[col] = pd.to_numeric(df[col], errors='coerce')
|
|
91
|
+
else:
|
|
92
|
+
# 日线数据
|
|
93
|
+
df = pd.DataFrame(
|
|
94
|
+
raw_data,
|
|
95
|
+
columns=['date', 'open', 'high', 'low', 'close', 'vol', 'p', 's']
|
|
96
|
+
).set_index('date')
|
|
97
|
+
# 转换数值列
|
|
98
|
+
for col in ['open', 'high', 'low', 'close', 'vol', 'p', 's']:
|
|
99
|
+
df[col] = pd.to_numeric(df[col], errors='coerce')
|
|
100
|
+
|
|
101
|
+
return df
|
|
102
|
+
except Exception as e:
|
|
103
|
+
raise ParseError(f"Failed to parse Sina future kline data: {e}")
|
|
104
|
+
|
rquote/plots.py
CHANGED
rquote/utils/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
工具模块
|
|
4
|
+
"""
|
|
5
|
+
from .http import HTTPClient
|
|
6
|
+
from .date import check_date_format
|
|
7
|
+
from .logging import logger, setup_logger
|
|
8
|
+
from .web import WebUtils, hget
|
|
9
|
+
from .helpers import load_js_var_json
|
|
10
|
+
|
|
11
|
+
__all__ = ['HTTPClient', 'check_date_format', 'logger', 'setup_logger',
|
|
12
|
+
'WebUtils', 'hget', 'load_js_var_json']
|
|
13
|
+
|
rquote/utils/date.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
日期工具模块
|
|
4
|
+
"""
|
|
5
|
+
import time
|
|
6
|
+
import re
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def check_date_format(date_str: str) -> str:
|
|
11
|
+
"""
|
|
12
|
+
检查并标准化日期格式
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
date_str: 日期字符串
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
标准化后的日期字符串(格式:YYYY-MM-DD)
|
|
19
|
+
|
|
20
|
+
Raises:
|
|
21
|
+
ValueError: 日期格式无法识别
|
|
22
|
+
"""
|
|
23
|
+
if not date_str:
|
|
24
|
+
return ''
|
|
25
|
+
|
|
26
|
+
# 允许格式: YYYY-MM-DD
|
|
27
|
+
if re.match(r'^\d{4}-\d{2}-\d{2}$', date_str):
|
|
28
|
+
return date_str
|
|
29
|
+
|
|
30
|
+
# 尝试转换其他格式
|
|
31
|
+
formats = ["%Y/%m/%d", "%Y%m%d", "%Y.%m.%d", "%Y_%m_%d", "%Y-%m-%d"]
|
|
32
|
+
for fmt in formats:
|
|
33
|
+
try:
|
|
34
|
+
t_struct = time.strptime(date_str, fmt)
|
|
35
|
+
return time.strftime("%Y-%m-%d", t_struct)
|
|
36
|
+
except ValueError:
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
raise ValueError(f"date format not recognized: {date_str}")
|
|
40
|
+
|
rquote/utils/helpers.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
辅助工具函数
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
from .web import hget
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load_js_var_json(url: str):
|
|
10
|
+
"""
|
|
11
|
+
加载JavaScript变量中的JSON数据
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
url: 请求URL
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
JSON数据
|
|
18
|
+
"""
|
|
19
|
+
a = hget(url)
|
|
20
|
+
if a:
|
|
21
|
+
a = json.loads(a.text.split('(')[1].split(')')[0])
|
|
22
|
+
return a
|
|
23
|
+
|
rquote/utils/http.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
HTTP客户端模块
|
|
4
|
+
"""
|
|
5
|
+
import time
|
|
6
|
+
import logging
|
|
7
|
+
import httpx
|
|
8
|
+
from typing import Optional, Dict
|
|
9
|
+
from ..config import default_config
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HTTPClient:
|
|
15
|
+
"""改进的HTTP客户端"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, timeout: Optional[int] = None, retry_times: Optional[int] = None,
|
|
18
|
+
retry_delay: Optional[float] = None, pool_size: Optional[int] = None):
|
|
19
|
+
"""
|
|
20
|
+
初始化HTTP客户端
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
timeout: 超时时间(秒)
|
|
24
|
+
retry_times: 重试次数
|
|
25
|
+
retry_delay: 重试延迟(秒)
|
|
26
|
+
pool_size: 连接池大小
|
|
27
|
+
"""
|
|
28
|
+
self.timeout = timeout or default_config.http_timeout
|
|
29
|
+
self.retry_times = retry_times or default_config.http_retry_times
|
|
30
|
+
self.retry_delay = retry_delay or default_config.http_retry_delay
|
|
31
|
+
pool_size = pool_size or default_config.http_pool_size
|
|
32
|
+
|
|
33
|
+
self.client = httpx.Client(
|
|
34
|
+
timeout=self.timeout,
|
|
35
|
+
limits=httpx.Limits(
|
|
36
|
+
max_keepalive_connections=pool_size,
|
|
37
|
+
max_connections=pool_size * 2
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def get(self, url: str, **kwargs) -> Optional[httpx.Response]:
|
|
42
|
+
"""
|
|
43
|
+
带重试的GET请求
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
url: 请求URL
|
|
47
|
+
**kwargs: 传递给httpx.get的其他参数
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Response对象,失败返回None
|
|
51
|
+
"""
|
|
52
|
+
headers = kwargs.pop('headers', {})
|
|
53
|
+
headers.update(self._get_default_headers())
|
|
54
|
+
|
|
55
|
+
for attempt in range(self.retry_times):
|
|
56
|
+
try:
|
|
57
|
+
response = self.client.get(
|
|
58
|
+
url,
|
|
59
|
+
headers=headers,
|
|
60
|
+
**kwargs
|
|
61
|
+
)
|
|
62
|
+
response.raise_for_status()
|
|
63
|
+
return response
|
|
64
|
+
except httpx.HTTPError as e:
|
|
65
|
+
if attempt == self.retry_times - 1:
|
|
66
|
+
logger.error(f'Failed to fetch {url} after {self.retry_times} attempts: {e}')
|
|
67
|
+
raise
|
|
68
|
+
logger.warning(f'Attempt {attempt + 1} failed for {url}, retrying...')
|
|
69
|
+
time.sleep(self.retry_delay * (attempt + 1))
|
|
70
|
+
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
def _get_default_headers(self) -> Dict[str, str]:
|
|
74
|
+
"""获取默认请求头"""
|
|
75
|
+
import uuid
|
|
76
|
+
from .web import WebUtils
|
|
77
|
+
return {
|
|
78
|
+
'User-Agent': WebUtils.ua(),
|
|
79
|
+
'Referer': str(uuid.uuid4())
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
def close(self):
|
|
83
|
+
"""关闭客户端"""
|
|
84
|
+
self.client.close()
|
|
85
|
+
|
|
86
|
+
def __enter__(self):
|
|
87
|
+
return self
|
|
88
|
+
|
|
89
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
90
|
+
self.close()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# 全局HTTP客户端实例(向后兼容)
|
|
94
|
+
_default_client = HTTPClient()
|
|
95
|
+
|
|
96
|
+
def hget_new(url: str, **kwargs) -> Optional[httpx.Response]:
|
|
97
|
+
"""
|
|
98
|
+
改进的HTTP GET函数
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
url: 请求URL
|
|
102
|
+
**kwargs: 传递给httpx.get的其他参数
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Response对象,失败返回None
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
return _default_client.get(url, **kwargs)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.error(f'hget failed for {url}: {e}')
|
|
111
|
+
return None
|
|
112
|
+
|
rquote/utils/logging.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
日志工具
|
|
4
|
+
"""
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def setup_logger():
|
|
9
|
+
"""设置日志记录器"""
|
|
10
|
+
logger = logging.getLogger('rquote')
|
|
11
|
+
if not logger.handlers:
|
|
12
|
+
logger.setLevel(logging.INFO)
|
|
13
|
+
file_handler = logging.FileHandler('/tmp/rquote.log')
|
|
14
|
+
|
|
15
|
+
formatter = logging.Formatter('%(asctime)-15s:%(lineno)s %(message)s')
|
|
16
|
+
file_handler.setFormatter(formatter)
|
|
17
|
+
|
|
18
|
+
logger.addHandler(file_handler)
|
|
19
|
+
logger.addHandler(logging.StreamHandler())
|
|
20
|
+
|
|
21
|
+
return logger
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
logger = setup_logger()
|
|
25
|
+
|