|
@@ -8,6 +8,7 @@ Memory Lane 公用模块:HTTP 配置、ASP.NET postback 切换 auction、单
|
|
|
|
|
|
|
|
目标网站: https://bid.memorylaneinc.com/lots/gallery
|
|
目标网站: https://bid.memorylaneinc.com/lots/gallery
|
|
|
"""
|
|
"""
|
|
|
|
|
+import re
|
|
|
import random
|
|
import random
|
|
|
from curl_cffi import requests
|
|
from curl_cffi import requests
|
|
|
import user_agent
|
|
import user_agent
|
|
@@ -196,7 +197,7 @@ def get_single_page(log, page, sql_pool, session, impersonate,
|
|
|
|
|
|
|
|
status = tag_div_p[2] if len(tag_div_p) > 2 else None
|
|
status = tag_div_p[2] if len(tag_div_p) > 2 else None
|
|
|
current_bid = tag_div.xpath('.//div[@class="item-price"]/a/text()').get()
|
|
current_bid = tag_div.xpath('.//div[@class="item-price"]/a/text()').get()
|
|
|
- current_bid = current_bid.replace('CURRENT BID $', '').replace(',', '').strip() if current_bid else None
|
|
|
|
|
|
|
+ current_bid = current_bid.replace('CURRENT BID $', '').replace('SOLD FOR $','').replace(',', '').strip() if current_bid else None
|
|
|
|
|
|
|
|
data_dict = {
|
|
data_dict = {
|
|
|
"title": title,
|
|
"title": title,
|
|
@@ -296,3 +297,97 @@ def update_details_for_pending(log, sql_pool):
|
|
|
data={"state": 2},
|
|
data={"state": 2},
|
|
|
condition={"id": sql_id}
|
|
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/current_bid。
|
|
|
|
|
+
|
|
|
|
|
+ 首采时 status='Open' 表示拍卖进行中、价格未定,抓到的只是当时价格。
|
|
|
|
|
+ 拍卖结束后详情页会固定出现「This lot is closed. Bidding is not allowed.」,
|
|
|
|
|
+ 此时才把最终的出价次数、成交状态、成交价写回库。
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ log: 日志对象。
|
|
|
|
|
+ url (str): 详情页 URL(形如 .../bids/bidplace.aspx?itemid=xxx),来自 memory_lane_record.detail_url。
|
|
|
|
|
+ sql_pool: mysql 连接池。
|
|
|
|
|
+ sql_id (int): 该记录在 memory_lane_record 的自增 id。
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ 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,current_bid 保持原值不动
|
|
|
|
|
+ data["status"] = "Unsold"
|
|
|
|
|
+ elif "SOLD FOR" in price_upper:
|
|
|
|
|
+ # 成交:提取金额并去掉 $ 和千分位逗号,与列表页 current_bid 存法一致(含买家佣金的最终成交价)
|
|
|
|
|
+ m = re.search(r'\$([\d,]+(?:\.\d+)?)', price_box)
|
|
|
|
|
+ data["status"] = "Sold"
|
|
|
|
|
+ data["current_bid"] = m.group(1).replace(',', '') if m else None
|
|
|
|
|
+ else:
|
|
|
|
|
+ # 未知价格框形态:只记日志、不动 status,避免把不确定的结果误写库
|
|
|
|
|
+ log.warning(f"id={sql_id} 价格框形态未知,仅更新 bids: {price_box!r}")
|
|
|
|
|
+
|
|
|
|
|
+ sql_pool.update_one_or_dict(table="memory_lane_record", 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/current_bid,未结束的保持不动。
|
|
|
|
|
+ 更新后 status 变为 Sold/Unsold,下一轮不再命中,随任务多轮运行自然收敛。
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ log: 日志对象。
|
|
|
|
|
+ sql_pool: mysql 连接池。
|
|
|
|
|
+ """
|
|
|
|
|
+ log.debug('Refreshing open lots result ...........................')
|
|
|
|
|
+ rows = sql_pool.select_all(
|
|
|
|
|
+ "select id, detail_url from memory_lane_record 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} 条")
|