Sfoglia il codice sorgente

feat(auctions90s): 补采拍卖结束后的最终结果并回写数据库

- 新增 refresh_lot_result 函数,重访详情页判定拍卖是否结束
- 结束后回写最终出价次数、成交状态和成交价格
- 新增 update_open_lots 函数,批量处理数据库中 status='Open' 的记录
- 补采失败时记录日志,避免阻塞整个流程
- 在 auctions90s_spider 主流程中调用 update_open_lots,补采拍卖结果
- 注释掉原有 nineties_main 调用,便于调试和控制流程
charley 4 giorni fa
parent
commit
9e08b98135

+ 90 - 0
auctions90s_spider/auctions90s_core.py

@@ -363,3 +363,93 @@ def update_details_for_pending(log, sql_pool):
                 data={"state": 2},
                 condition={"id": sql_id}
             )
+
+
+@retry(stop=stop_after_attempt(5), wait=wait_fixed(2), after=after_log)
+def refresh_lot_result(log, url, sql_pool, sql_id):
+    """
+    重访详情页,判断拍卖是否结束;结束则回写最终 bids/status/price。
+
+    首采时 status='Open' 表示拍卖进行中、价格未定,抓到的只是当时的当前价。
+    拍卖结束后详情页会固定出现「This lot is closed. Bidding is not allowed.」,
+    此时才把最终的出价次数、成交状态、成交价写回库。
+
+    :param log: (loguru.Logger) 日志对象
+    :param url: (str) 详情页 URL(形如 .../bids/bidplace.aspx?itemid=xxx),来自 detail_url 字段
+    :param sql_pool: (MySQLConnectionPool) 数据库连接池
+    :param sql_id: (int) 该记录在 auctions90s_record 的主键 id
+    :return: (str) 'closed' 表示已结束并完成更新;'open' 表示仍在进行、跳过未改动
+    :raises RuntimeError: 页面残缺(既无结束标志也无价格框)时抛出以触发重试,避免残页被误判为进行中
+    """
+    log.info(f">>>>>>>>>>>>>> 补采拍卖结果 id={sql_id} URL={url} <<<<<<<<<<<<<<")
+    response = requests.get(url, headers=headers,
+                            impersonate=random.choice(client_identifier_list),
+                            timeout=10, proxies=get_proxys(log))
+    response.raise_for_status()
+    html = response.text
+    selector = Selector(html)
+
+    # 结束信号:拍卖结束后详情页固定出现这句话,进行中的 lot 不会有(最稳的判定依据)
+    closed = "This lot is closed. Bidding is not allowed." in html
+    # 价格框:成交显示「SOLD FOR $x」,流拍显示「UNSOLD」,进行中显示「CURRENT BID $x」
+    price_box = ' '.join(selector.xpath('//h3[@id="MainContent_currentBidBox"]//text()').getall())
+    price_box = re.sub(r'\s+', ' ', price_box).strip()
+
+    if not closed:
+        # 无结束标志且连价格框都没有 → 判定为残页/被限流,抛错重试;否则确系进行中,跳过
+        if not price_box:
+            raise RuntimeError("详情页残缺:既无结束标志也无价格框")
+        log.info(f"id={sql_id} 拍卖仍在进行,跳过(price_box={price_box!r})")
+        return "open"
+
+    # 已结束:解析最终出价次数(Bid History 链接前的 <strong> 即出价数)
+    bids = selector.xpath('//a[@id="MainContent_btnBidHistory"]/preceding-sibling::strong/text()').get()
+    bids = bids.strip() if bids else None
+
+    data = {"bids": bids}
+    price_upper = price_box.upper()
+    if "UNSOLD" in price_upper:
+        # 流拍:详情页不给最终价,只更新 status 与 bids,price 保持原值不动
+        data["status"] = "Unsold"
+    elif "SOLD FOR" in price_upper:
+        # 成交:复用 _clean_price 去掉「SOLD FOR $」前缀与千分位逗号,与列表页 price 存法一致
+        data["status"] = "Sold"
+        data["price"] = _clean_price(price_box)
+    else:
+        # 未知价格框形态:只记日志、不动 status,避免把不确定的结果误写库
+        log.warning(f"id={sql_id} 价格框形态未知,仅更新 bids: {price_box!r}")
+
+    sql_pool.update_one_or_dict(table=TABLE_NAME, data=data, condition={"id": sql_id})
+    log.info(f"id={sql_id} 拍卖已结束,更新 -> {data}")
+    return "closed"
+
+
+def update_open_lots(log, sql_pool):
+    """
+    扫描库里 status='Open' 的记录,补采其最终拍卖结果。
+
+    首采发生在拍卖进行中,只抓到当时价格;本函数逐条重访详情页,
+    对已结束的 lot 回写最终 bids/status/price,未结束的保持不动。
+    更新后 status 变为 Sold/Unsold,下一轮不再命中,随任务多轮运行自然收敛。
+
+    :param log: (loguru.Logger) 日志对象
+    :param sql_pool: (MySQLConnectionPool) 数据库连接池
+    :return: None
+    """
+    log.debug('Refreshing open lots result ...........................')
+    rows = sql_pool.select_all(
+        f"select id, detail_url from {TABLE_NAME} where status='Open' order by id")
+    total = len(rows) if rows else 0
+    log.info(f"待补采 status='Open' 记录数: {total}")
+
+    closed_cnt = 0
+    for row in rows or []:
+        sql_id, detail_url = row[0], row[1]
+        try:
+            if refresh_lot_result(log, detail_url, sql_pool, sql_id) == "closed":
+                closed_cnt += 1
+        except Exception as e:
+            # 重试耗尽仍失败:记录并跳过,保持 status='Open' 留待下一轮,不阻塞整体
+            log.error(f"补采拍卖结果失败 id={sql_id} url={detail_url}: {e}")
+            continue
+    log.info(f"本轮补采完成:共 {total} 条,已结束并更新 {closed_cnt} 条")

+ 9 - 1
auctions90s_spider/auctions90s_spider.py

@@ -11,6 +11,7 @@
   4. 没有新增 → 本轮无数据可抓,结束
   5. 对每个新增 auction:postback 切换 → 翻页 → 写库
   6. 补抓 state != 1 的详情页
+  7. 补采 status='Open' 的 lot:拍卖结束后回写最终 bids/status/price
 """
 import time
 import random
@@ -27,6 +28,7 @@ from auctions90s_core import (
     crawl_one_auction,
     get_auction_list,
     update_details_for_pending,
+    update_open_lots,
     after_log,
 )
 
@@ -127,6 +129,12 @@ def nineties_main(log):
         except Exception as e:
             log.error(f'详情补抓失败: {e}')
 
+        try:
+            # 补采:重访 status='Open' 的 lot,拍卖结束后回写最终 bids/status/price
+            update_open_lots(log, sql_pool)
+        except Exception as e:
+            log.error(f'拍卖结果补采失败: {e}')
+
     except Exception as e:
         log.error(f'{inspect.currentframe().f_code.co_name} error: {e}')
     finally:
@@ -139,7 +147,7 @@ def schedule_task():
 
     :return: None(永不返回,内部死循环)
     """
-    nineties_main(log=logger)
+    # nineties_main(log=logger)
 
     def run_semimonthly():
         # 每月 1 号和 15 号执行(半月一次)