From 4a592cf457d4673dd5fabafbe5bd52623d1bed7b Mon Sep 17 00:00:00 2001 From: wangwei0518 <1329996666@qq.com> Date: Sat, 6 Jun 2026 00:26:04 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=97=A5=E6=8A=A5=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E7=BD=91=E7=BB=9C=E4=B8=8E=E8=B7=AF=E5=BE=84=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 network.use_environment_proxy 配置项,默认禁用 urllib 对 HTTP_PROXY/HTTPS_PROXY 等环境代理的自动读取,避免代理 CONNECT 连接被重置时影响 Nasdaq、FRED 和 QQBot 请求。 将示例配置中的 state、delivery log 和 runtime log 路径改为相对路径,由脚本按 NdxDailyReport 所在目录解析,方便项目迁移和定时任务部署。 保留真实 config.yaml、state 和投递日志为本地文件,不纳入提交。 --- NdxDailyReport/config.example.yaml | 11 ++++++++--- NdxDailyReport/ndx_daily_report.py | 30 +++++++++++++++++++++++++----- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/NdxDailyReport/config.example.yaml b/NdxDailyReport/config.example.yaml index 1ce113f..551e63c 100644 --- a/NdxDailyReport/config.example.yaml +++ b/NdxDailyReport/config.example.yaml @@ -1,13 +1,18 @@ paths: - state_file: D:/project/Python-tools/NdxDailyReport/ndx-daily-report-state.json - delivery_log_file: D:/project/Python-tools/NdxDailyReport/ndx-daily-report-delivery-log.jsonl - runtime_log_file: D:/project/Python-tools/NdxDailyReport/ndx-daily-report-runtime.log + state_file: ndx-daily-report-state.json + delivery_log_file: ndx-daily-report-delivery-log.jsonl + runtime_log_file: ndx-daily-report-runtime.log nasdaq: quote_api: https://api.nasdaq.com/api/quote/NDX/info?assetclass=index fred_csv: https://fred.stlouisfed.org/graph/fredgraph.csv?id=NASDAQ100 request_timeout_seconds: 15 +network: + # false 表示 urllib 不读取 HTTP_PROXY/HTTPS_PROXY 等环境代理。 + # 某些代理会重置 CONNECT 连接,定时脚本默认直连更稳定。 + use_environment_proxy: false + qqbot: appid: "用户填写" appkey: "用户填写" diff --git a/NdxDailyReport/ndx_daily_report.py b/NdxDailyReport/ndx_daily_report.py index 85efb43..f1b58f3 100644 --- a/NdxDailyReport/ndx_daily_report.py +++ b/NdxDailyReport/ndx_daily_report.py @@ -264,7 +264,7 @@ def fetch_nasdaq_quote(config: dict[str, Any], logger: logging.Logger) -> Market url = str(config["nasdaq"]["quote_api"]) timeout = int(config["nasdaq"].get("request_timeout_seconds", 15)) 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", {}) primary = data.get("primaryData", {}) 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)) logger.info("FRED CSV 请求开始") 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") rows = [row for row in csv.DictReader(text.splitlines()) if row.get("NASDAQ100") not in {"", "."}] 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" logger.info("QQBot 投递开始: target_type=%s", target_type) 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 投递成功") @@ -444,7 +452,7 @@ def get_qqbot_token(config: dict[str, Any], logger: logging.Logger) -> str: qqbot = config["qqbot"] url = str(qqbot.get("token_api", "https://bots.qq.com/app/getAppAccessToken")) 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") if not token: raise ValueError("QQBot token 响应缺少 access_token") @@ -486,6 +494,7 @@ def http_json( body: dict[str, Any] | None = None, timeout: int = 15, headers: dict[str, str] | None = None, + use_environment_proxy: bool = False, ) -> dict[str, Any]: request_headers = dict(DEFAULT_HEADERS) if headers: @@ -495,8 +504,9 @@ def http_json( data = json.dumps(body, ensure_ascii=False).encode("utf-8") request_headers["Content-Type"] = "application/json" request = urllib.request.Request(url, data=data, headers=request_headers, method=method) + opener = build_url_opener(use_environment_proxy) try: - with urllib.request.urlopen(request, timeout=timeout) as response: + with opener.open(request, timeout=timeout) as response: text = response.read().decode("utf-8") except urllib.error.HTTPError as exc: detail = exc.read().decode("utf-8", errors="replace")[:300] @@ -507,6 +517,16 @@ def http_json( 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]: return { "tradeDate": data.trade_date,