Preserve public summaries for unavailable historical paid articles

Validate Wallstreetcn business responses and retain list provenance for historical paid articles whose detail API returns code 50000. Keep other failures explicit and cover the boundary with eight isolated tests.
This commit is contained in:
eezd committed 2026-10-26 12:01:29 +08:00
1 parent bc480fb806
commit 5fa1e37fd4
2 files changed
+149 -5

No files matched your search

+38 -5
View File
@@ -1,6 +1,7 @@
import base64
import json
from datetime import datetime
from html import escape
from scrapy import Request
from scrapy.http import Response
@@ -10,6 +11,12 @@ from alphapulse.crawler.spiders.base import NewsSpider, ts
API = "https://api-one-wscn.awtmt.com/apiv1/content"
def _api_data(body: dict) -> dict:
if body["code"] != 20000:
raise ValueError(f"华尔街见闻接口业务错误(code={body['code']}):{body['message']}")
return body["data"]
class WscnNewsSpider(NewsSpider):
"""华尔街见闻·文章:列表只有摘要,新条目或标题/摘要变化时再拉详情"""
@@ -27,14 +34,21 @@ class WscnNewsSpider(NewsSpider):
return self.list_request(base64.b64encode(cursor.encode()).decode(), 1, history=True)
async def parse_list(self, response: Response):
data = response.json()["data"]
data = _api_data(response.json())
# 信息流里混有快讯,快讯由 wscn_live 负责
rows = [x["resource"] for x in data["items"] if x["resource_type"] == "article"]
ids = [str(r["id"]) for r in rows]
known = await self.known_items(ids)
for r in rows:
if known.get(str(r["id"])) != (r["title"], r["content_short"]):
yield self.request(f"{API}/articles/{r['id']}?extract=0", self.parse_detail, summary=r["content_short"])
listed = {key: r[key] for key in ("id", "uri", "title", "display_time", "is_priced")}
listed["author"] = (r.get("author") or {}).get("display_name")
listed["source_name"] = r.get("source_name")
yield self.request(
f"{API}/articles/{r['id']}?extract=0", self.parse_detail,
summary=r["content_short"], listed_article=listed,
list_raw_response_id=response.meta["raw_response_id"], history=response.meta.get("history", False),
)
page = response.meta["page"]
oldest = ts(min(r["display_time"] for r in rows)) if rows else None
if response.meta.get("history"):
@@ -44,11 +58,30 @@ class WscnNewsSpider(NewsSpider):
yield self.list_request(data["next_cursor"], page + 1)
async def parse_detail(self, response: Response):
a = response.json()["data"]
body = response.json()
listed = response.meta["listed_article"]
if body["code"] == 50000 and response.meta["history"] and listed["is_priced"]:
# 旧付费课程的详情和官网页面均可能失效;保存列表公开信息,不伪装成已取得正文。
self.logger.warning("历史付费文章 %s 详情接口失败(code=%s):%s;仅保存列表公开摘要",
listed["id"], body["code"], body["message"])
summary = response.meta["summary"]
item = self.item(
response, external_id=str(listed["id"]), url=listed["uri"], title=listed["title"],
summary=summary, content_html=f"<p>{escape(summary)}</p>",
published_at=ts(listed["display_time"]), author=listed["author"], is_paywalled=True,
extra={"source_name": listed["source_name"], "is_priced": True,
"content_scope": "public_summary", "historical_backfill": True,
"detail_error": {"code": body["code"], "message": body["message"],
"raw_response_id": response.meta["raw_response_id"]}},
)
item["raw_response_id"] = response.meta["list_raw_response_id"]
yield item
return
a = _api_data(body)
yield self.item(
response,
external_id=str(a["id"]),
url=f"https://wallstreetcn.com/articles/{a['id']}",
url=listed["uri"],
title=a["title"],
summary=response.meta["summary"],
content_html=a["content"],
@@ -75,7 +108,7 @@ class WscnLiveSpider(NewsSpider):
return self.list_request(str(int(before.timestamp())), 1, history=True)
async def parse_list(self, response: Response):
data = response.json()["data"]
data = _api_data(response.json())
rows = data["items"]
# 先查已入库集合,再产出条目:否则管道可能先落库,导致误判为没有新条目而停止翻页
ids = [str(x["id"]) for x in rows]