优化日报脚本网络与路径配置
新增 network.use_environment_proxy 配置项,默认禁用 urllib 对 HTTP_PROXY/HTTPS_PROXY 等环境代理的自动读取,避免代理 CONNECT 连接被重置时影响 Nasdaq、FRED 和 QQBot 请求。 将示例配置中的 state、delivery log 和 runtime log 路径改为相对路径,由脚本按 NdxDailyReport 所在目录解析,方便项目迁移和定时任务部署。 保留真实 config.yaml、state 和投递日志为本地文件,不纳入提交。
This commit is contained in:
@@ -1,13 +1,18 @@
|
|||||||
paths:
|
paths:
|
||||||
state_file: D:/project/Python-tools/NdxDailyReport/ndx-daily-report-state.json
|
state_file: ndx-daily-report-state.json
|
||||||
delivery_log_file: D:/project/Python-tools/NdxDailyReport/ndx-daily-report-delivery-log.jsonl
|
delivery_log_file: ndx-daily-report-delivery-log.jsonl
|
||||||
runtime_log_file: D:/project/Python-tools/NdxDailyReport/ndx-daily-report-runtime.log
|
runtime_log_file: ndx-daily-report-runtime.log
|
||||||
|
|
||||||
nasdaq:
|
nasdaq:
|
||||||
quote_api: https://api.nasdaq.com/api/quote/NDX/info?assetclass=index
|
quote_api: https://api.nasdaq.com/api/quote/NDX/info?assetclass=index
|
||||||
fred_csv: https://fred.stlouisfed.org/graph/fredgraph.csv?id=NASDAQ100
|
fred_csv: https://fred.stlouisfed.org/graph/fredgraph.csv?id=NASDAQ100
|
||||||
request_timeout_seconds: 15
|
request_timeout_seconds: 15
|
||||||
|
|
||||||
|
network:
|
||||||
|
# false 表示 urllib 不读取 HTTP_PROXY/HTTPS_PROXY 等环境代理。
|
||||||
|
# 某些代理会重置 CONNECT 连接,定时脚本默认直连更稳定。
|
||||||
|
use_environment_proxy: false
|
||||||
|
|
||||||
qqbot:
|
qqbot:
|
||||||
appid: "用户填写"
|
appid: "用户填写"
|
||||||
appkey: "用户填写"
|
appkey: "用户填写"
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ def fetch_nasdaq_quote(config: dict[str, Any], logger: logging.Logger) -> Market
|
|||||||
url = str(config["nasdaq"]["quote_api"])
|
url = str(config["nasdaq"]["quote_api"])
|
||||||
timeout = int(config["nasdaq"].get("request_timeout_seconds", 15))
|
timeout = int(config["nasdaq"].get("request_timeout_seconds", 15))
|
||||||
logger.info("Nasdaq quote API 请求开始")
|
logger.info("Nasdaq quote API 请求开始")
|
||||||
payload = http_json("GET", url, timeout=timeout)
|
payload = http_json("GET", url, timeout=timeout, use_environment_proxy=use_environment_proxy(config))
|
||||||
data = payload.get("data", {})
|
data = payload.get("data", {})
|
||||||
primary = data.get("primaryData", {})
|
primary = data.get("primaryData", {})
|
||||||
key_stats = data.get("keyStats", {})
|
key_stats = data.get("keyStats", {})
|
||||||
@@ -308,7 +308,8 @@ def fetch_fred_csv(config: dict[str, Any], logger: logging.Logger) -> MarketData
|
|||||||
timeout = int(config["nasdaq"].get("request_timeout_seconds", 15))
|
timeout = int(config["nasdaq"].get("request_timeout_seconds", 15))
|
||||||
logger.info("FRED CSV 请求开始")
|
logger.info("FRED CSV 请求开始")
|
||||||
request = urllib.request.Request(url, headers={"User-Agent": DEFAULT_HEADERS["User-Agent"]})
|
request = urllib.request.Request(url, headers={"User-Agent": DEFAULT_HEADERS["User-Agent"]})
|
||||||
with urllib.request.urlopen(request, timeout=timeout) as response:
|
opener = build_url_opener(use_environment_proxy(config))
|
||||||
|
with opener.open(request, timeout=timeout) as response:
|
||||||
text = response.read().decode("utf-8-sig")
|
text = response.read().decode("utf-8-sig")
|
||||||
rows = [row for row in csv.DictReader(text.splitlines()) if row.get("NASDAQ100") not in {"", "."}]
|
rows = [row for row in csv.DictReader(text.splitlines()) if row.get("NASDAQ100") not in {"", "."}]
|
||||||
if len(rows) < 2:
|
if len(rows) < 2:
|
||||||
@@ -436,7 +437,14 @@ def send_qqbot_message(config: dict[str, Any], content: str, logger: logging.Log
|
|||||||
url = f"{base_api}/v2/users/{target_openid}/messages"
|
url = f"{base_api}/v2/users/{target_openid}/messages"
|
||||||
logger.info("QQBot 投递开始: target_type=%s", target_type)
|
logger.info("QQBot 投递开始: target_type=%s", target_type)
|
||||||
body = {"content": content, "msg_type": 0}
|
body = {"content": content, "msg_type": 0}
|
||||||
http_json("POST", url, body=body, timeout=15, headers={"Authorization": f"QQBot {token}"})
|
http_json(
|
||||||
|
"POST",
|
||||||
|
url,
|
||||||
|
body=body,
|
||||||
|
timeout=15,
|
||||||
|
headers={"Authorization": f"QQBot {token}"},
|
||||||
|
use_environment_proxy=use_environment_proxy(config),
|
||||||
|
)
|
||||||
logger.info("QQBot 投递成功")
|
logger.info("QQBot 投递成功")
|
||||||
|
|
||||||
|
|
||||||
@@ -444,7 +452,7 @@ def get_qqbot_token(config: dict[str, Any], logger: logging.Logger) -> str:
|
|||||||
qqbot = config["qqbot"]
|
qqbot = config["qqbot"]
|
||||||
url = str(qqbot.get("token_api", "https://bots.qq.com/app/getAppAccessToken"))
|
url = str(qqbot.get("token_api", "https://bots.qq.com/app/getAppAccessToken"))
|
||||||
body = {"appId": str(qqbot["appid"]), "clientSecret": str(qqbot["appkey"])}
|
body = {"appId": str(qqbot["appid"]), "clientSecret": str(qqbot["appkey"])}
|
||||||
payload = http_json("POST", url, body=body, timeout=15)
|
payload = http_json("POST", url, body=body, timeout=15, use_environment_proxy=use_environment_proxy(config))
|
||||||
token = payload.get("access_token") or payload.get("accessToken")
|
token = payload.get("access_token") or payload.get("accessToken")
|
||||||
if not token:
|
if not token:
|
||||||
raise ValueError("QQBot token 响应缺少 access_token")
|
raise ValueError("QQBot token 响应缺少 access_token")
|
||||||
@@ -486,6 +494,7 @@ def http_json(
|
|||||||
body: dict[str, Any] | None = None,
|
body: dict[str, Any] | None = None,
|
||||||
timeout: int = 15,
|
timeout: int = 15,
|
||||||
headers: dict[str, str] | None = None,
|
headers: dict[str, str] | None = None,
|
||||||
|
use_environment_proxy: bool = False,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
request_headers = dict(DEFAULT_HEADERS)
|
request_headers = dict(DEFAULT_HEADERS)
|
||||||
if headers:
|
if headers:
|
||||||
@@ -495,8 +504,9 @@ def http_json(
|
|||||||
data = json.dumps(body, ensure_ascii=False).encode("utf-8")
|
data = json.dumps(body, ensure_ascii=False).encode("utf-8")
|
||||||
request_headers["Content-Type"] = "application/json"
|
request_headers["Content-Type"] = "application/json"
|
||||||
request = urllib.request.Request(url, data=data, headers=request_headers, method=method)
|
request = urllib.request.Request(url, data=data, headers=request_headers, method=method)
|
||||||
|
opener = build_url_opener(use_environment_proxy)
|
||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(request, timeout=timeout) as response:
|
with opener.open(request, timeout=timeout) as response:
|
||||||
text = response.read().decode("utf-8")
|
text = response.read().decode("utf-8")
|
||||||
except urllib.error.HTTPError as exc:
|
except urllib.error.HTTPError as exc:
|
||||||
detail = exc.read().decode("utf-8", errors="replace")[:300]
|
detail = exc.read().decode("utf-8", errors="replace")[:300]
|
||||||
@@ -507,6 +517,16 @@ def http_json(
|
|||||||
return parsed
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
def build_url_opener(use_proxy: bool) -> urllib.request.OpenerDirector:
|
||||||
|
if use_proxy:
|
||||||
|
return urllib.request.build_opener()
|
||||||
|
return urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
||||||
|
|
||||||
|
|
||||||
|
def use_environment_proxy(config: dict[str, Any]) -> bool:
|
||||||
|
return bool(config.get("network", {}).get("use_environment_proxy", False))
|
||||||
|
|
||||||
|
|
||||||
def serialize_market_data(data: MarketData, now: datetime) -> dict[str, str]:
|
def serialize_market_data(data: MarketData, now: datetime) -> dict[str, str]:
|
||||||
return {
|
return {
|
||||||
"tradeDate": data.trade_date,
|
"tradeDate": data.trade_date,
|
||||||
|
|||||||
Reference in New Issue
Block a user