|
@@ -32,7 +32,16 @@ from parsel import Selector
|
|
|
from curl_cffi import requests
|
|
from curl_cffi import requests
|
|
|
from curl_cffi.requests import BrowserType
|
|
from curl_cffi.requests import BrowserType
|
|
|
from urllib.parse import urljoin
|
|
from urllib.parse import urljoin
|
|
|
-from tenacity import retry, stop_after_attempt, wait_fixed
|
|
|
|
|
|
|
+from tenacity import retry, stop_after_attempt, wait_fixed, retry_if_not_exception_type
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+class LotNotFound(Exception):
|
|
|
|
|
+ """lot 详情页不存在(HTTP 404 或站点软 404「Nothing Found」页)。
|
|
|
|
|
+
|
|
|
|
|
+ 该异常表示拍品已被撤下/移除,是永久性错误,不应重试;
|
|
|
|
|
+ 捕获后应把记录标记为终态 state=3(不再进入补抓队列)。
|
|
|
|
|
+ """
|
|
|
|
|
+
|
|
|
|
|
|
|
|
# —— 站点常量 ——
|
|
# —— 站点常量 ——
|
|
|
BASE_URL = "https://catalogs.scpauctions.com" # 站点根,用于拼接相对链接
|
|
BASE_URL = "https://catalogs.scpauctions.com" # 站点根,用于拼接相对链接
|
|
@@ -104,7 +113,10 @@ def get_proxys(log):
|
|
|
raise e
|
|
raise e
|
|
|
|
|
|
|
|
|
|
|
|
|
-@retry(stop=stop_after_attempt(5), wait=wait_fixed(2), after=after_log)
|
|
|
|
|
|
|
+# retry_if_not_exception_type(LotNotFound):404 是永久性错误,直接抛出不重试,
|
|
|
|
|
+# 避免对已下架的 lot 做 5×3 次徒劳请求
|
|
|
|
|
+@retry(stop=stop_after_attempt(5), wait=wait_fixed(2), after=after_log,
|
|
|
|
|
+ retry=retry_if_not_exception_type(LotNotFound))
|
|
|
def _get_selector(log, session, impersonate, url):
|
|
def _get_selector(log, session, impersonate, url):
|
|
|
"""GET 一个页面并返回 parsel Selector(带重试,走代理)。
|
|
"""GET 一个页面并返回 parsel Selector(带重试,走代理)。
|
|
|
|
|
|
|
@@ -118,10 +130,15 @@ def _get_selector(log, session, impersonate, url):
|
|
|
Selector: 响应 HTML 的 parsel 解析对象。
|
|
Selector: 响应 HTML 的 parsel 解析对象。
|
|
|
|
|
|
|
|
Raises:
|
|
Raises:
|
|
|
- Exception: HTTP 状态非 2xx 时透传异常以触发重试。
|
|
|
|
|
|
|
+ LotNotFound: 页面 HTTP 404/410 或软 404「Nothing Found」时抛出(不重试)。
|
|
|
|
|
+ Exception: 其他 HTTP 非 2xx(如 5xx/超时)时透传以触发重试。
|
|
|
"""
|
|
"""
|
|
|
resp = session.get(url, headers=headers, impersonate=impersonate,
|
|
resp = session.get(url, headers=headers, impersonate=impersonate,
|
|
|
proxies=get_proxys(log), timeout=30)
|
|
proxies=get_proxys(log), timeout=30)
|
|
|
|
|
+ # 页面不存在:404/410 直接判定;部分站点软 404 返回 200 但页面是「Nothing Found」
|
|
|
|
|
+ if resp.status_code in (404, 410) or (
|
|
|
|
|
+ resp.status_code == 200 and "can not be found" in resp.text):
|
|
|
|
|
+ raise LotNotFound(f"页面不存在({resp.status_code}): {url}")
|
|
|
resp.raise_for_status()
|
|
resp.raise_for_status()
|
|
|
return Selector(resp.text)
|
|
return Selector(resp.text)
|
|
|
|
|
|
|
@@ -436,7 +453,8 @@ def parse_lot_detail(selector):
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
-@retry(stop=stop_after_attempt(3), wait=wait_fixed(2), after=after_log)
|
|
|
|
|
|
|
+@retry(stop=stop_after_attempt(3), wait=wait_fixed(2), after=after_log,
|
|
|
|
|
+ retry=retry_if_not_exception_type(LotNotFound))
|
|
|
def fetch_lot_detail(log, session, impersonate, detail_url):
|
|
def fetch_lot_detail(log, session, impersonate, detail_url):
|
|
|
"""GET lot 详情页并解析。
|
|
"""GET lot 详情页并解析。
|
|
|
|
|
|
|
@@ -535,7 +553,10 @@ def get_details(log, detail_url, sql_pool, sql_id):
|
|
|
|
|
|
|
|
|
|
|
|
|
def update_details_for_pending(log, sql_pool):
|
|
def update_details_for_pending(log, sql_pool):
|
|
|
- """扫库里 state != 1 的 lot,逐条补抓详情。
|
|
|
|
|
|
|
+ """扫库里待抓 / 瞬时失败的 lot,逐条补抓详情。
|
|
|
|
|
+
|
|
|
|
|
+ state 状态机:0 待抓,1 已抓成功,2 瞬时失败(可重试),3 页面不存在(404,永久跳过)。
|
|
|
|
|
+ 只选 state in (0, 2)——即跳过已成功(1)与永久不存在(3),避免 404 记录每轮死循环重试。
|
|
|
|
|
|
|
|
Args:
|
|
Args:
|
|
|
log: logger 对象。
|
|
log: logger 对象。
|
|
@@ -546,13 +567,22 @@ def update_details_for_pending(log, sql_pool):
|
|
|
"""
|
|
"""
|
|
|
log.debug("Updating detail pages ...")
|
|
log.debug("Updating detail pages ...")
|
|
|
rows = sql_pool.select_all(
|
|
rows = sql_pool.select_all(
|
|
|
- "select id, detail_url from scp_lot_record where state != 1"
|
|
|
|
|
|
|
+ "select id, detail_url from scp_lot_record where state in (0, 2)"
|
|
|
)
|
|
)
|
|
|
for row in rows:
|
|
for row in rows:
|
|
|
sql_id, detail_url = row[0], row[1]
|
|
sql_id, detail_url = row[0], row[1]
|
|
|
try:
|
|
try:
|
|
|
get_details(log, detail_url, sql_pool, sql_id)
|
|
get_details(log, detail_url, sql_pool, sql_id)
|
|
|
|
|
+ except LotNotFound as e:
|
|
|
|
|
+ # 页面已下架/不存在:置终态 3,不再进入补抓队列
|
|
|
|
|
+ log.warning(f"lot 页面不存在,标记 state=3 不再重试: {detail_url} ({e})")
|
|
|
|
|
+ sql_pool.update_one_or_dict(
|
|
|
|
|
+ table="scp_lot_record",
|
|
|
|
|
+ data={"state": 3},
|
|
|
|
|
+ condition={"id": sql_id},
|
|
|
|
|
+ )
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
|
|
+ # 瞬时错误(超时/5xx/代理抖动):置 2,下轮再试
|
|
|
log.error(f"Error getting details for {detail_url}: {e}")
|
|
log.error(f"Error getting details for {detail_url}: {e}")
|
|
|
sql_pool.update_one_or_dict(
|
|
sql_pool.update_one_or_dict(
|
|
|
table="scp_lot_record",
|
|
table="scp_lot_record",
|