rea_spider.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. # -*- coding: utf-8 -*-
  2. # Author : Charley
  3. # Python : 3.12.10
  4. # Date : 2026/07/10
  5. """
  6. REA (collectrea.com) 增量爬虫(日调度)
  7. 逻辑(两阶段,与 wheatland 一致):
  8. 1. GET /search 只解析「最近」板块(RECENT AUCTIONS tab,约 16 场)
  9. 2. 查库 select distinct auction_key from rea_record,得到已爬过的场次
  10. 3. 差集 = 新增场次
  11. 4. 没有新增 → 本轮无数据可抓,结束
  12. 5. 阶段一 列表:对每个新增场次翻页抓 lot 列表入库(state 默认 0)
  13. 6. 阶段二 详情:扫库 state != 1 的记录 → 逐条进详情页抓「标题 + 5 字段 + 多图」写回
  14. 目标网站: https://collectrea.com/search
  15. 说明:REA 的场次一旦结束即定型,新场次会出现在「最近」板块顶部;因此增量只需盯「最近」,
  16. 用 auction_key 差集识别新增场次即可(老场次早已在库)。
  17. """
  18. import sys
  19. import time
  20. import random
  21. import schedule
  22. from curl_cffi import requests
  23. from loguru import logger
  24. from tenacity import retry, stop_after_attempt, wait_fixed
  25. from mysql_pool import MySQLConnectionPool
  26. from rea_core import (
  27. client_identifier_list,
  28. crawl_one_auction,
  29. get_auction_list,
  30. update_details_for_pending,
  31. after_log,
  32. )
  33. logger.remove()
  34. logger.add("./logs/{time:YYYYMMDD}.log", encoding="utf-8", rotation="00:00",
  35. format="[{time:YYYY-MM-DD HH:mm:ss.SSS}] {level} {message}",
  36. level="DEBUG", retention="7 day")
  37. def get_existing_auction_keys(log, sql_pool):
  38. """查库返回已爬过的 auction_key 集合。
  39. Args:
  40. log: logger 对象。
  41. sql_pool: MySQL 连接池;为 None 时返回空集合(视作库内无任何场次)。
  42. Returns:
  43. set[str]: 已存在的 auction_key 字符串集合。
  44. """
  45. if sql_pool is None:
  46. log.warning("sql_pool 为 None,视为库内无任何场次(将全量重抓最近板块)")
  47. return set()
  48. rows = sql_pool.select_all(
  49. "select distinct auction_key from rea_record"
  50. )
  51. keys = {str(r[0]) for r in rows} if rows else set()
  52. log.info(f"库中已存在 {len(keys)} 个 auction_key")
  53. return keys
  54. def diff_new_auctions(log, recent_auctions, existing_keys):
  55. """从「最近」场次中筛出库里没有的新增场次。
  56. Args:
  57. log: logger 对象。
  58. recent_auctions (list[dict]): get_auction_list(only_recent=True) 的返回。
  59. existing_keys (set[str]): 已存在的 auction_key 集合。
  60. Returns:
  61. list[dict]: 待抓取的新增场次列表。
  62. """
  63. new_list = [a for a in recent_auctions if a["auction_key"] not in existing_keys]
  64. log.info(f"新增待抓取场次数: {len(new_list)} -> {[a['auction_key'] for a in new_list]}")
  65. return new_list
  66. def run_incremental(log, sql_pool):
  67. """增量抓取主流程。
  68. Args:
  69. log: logger 对象。
  70. sql_pool: MySQL 连接池;为 None 时不入库,仅在内存收集并 print 样本。
  71. """
  72. impersonate = random.choice(client_identifier_list)
  73. with requests.Session() as session:
  74. try:
  75. recent = get_auction_list(log, session, impersonate, only_recent=True)
  76. except Exception as e:
  77. log.error(f"获取最近场次列表失败: {e}")
  78. return
  79. existing_keys = get_existing_auction_keys(log, sql_pool)
  80. new_auctions = diff_new_auctions(log, recent, existing_keys)
  81. if not new_auctions:
  82. log.info("本轮无新增场次,跳过 list 抓取")
  83. return
  84. collected = []
  85. for idx, auc in enumerate(new_auctions, 1):
  86. log.info(f"========== [{idx}/{len(new_auctions)}] 开始抓场次 {auc['auction_key']} ({auc['auction_name']}) ==========")
  87. try:
  88. lots = crawl_one_auction(log, sql_pool, session, impersonate, auc)
  89. if sql_pool is None:
  90. collected.extend(lots)
  91. except Exception as e:
  92. log.error(f"场次 {auc['auction_key']} 抓取异常: {e}")
  93. continue
  94. if sql_pool is None:
  95. log.info(f"增量抓取结束,共 {len(collected)} 条 lot(未入库)")
  96. for row in collected[:3]:
  97. print(row)
  98. @retry(stop=stop_after_attempt(100), wait=wait_fixed(3600), after=after_log)
  99. def rea_main(log):
  100. """日调度主函数:增量 list + 补详情。
  101. Args:
  102. log: logger 对象。
  103. """
  104. log.info(f"开始运行 {sys._getframe().f_code.co_name} 增量爬虫任务 ...")
  105. sql_pool = MySQLConnectionPool(log=log)
  106. if not sql_pool:
  107. log.error("MySQL数据库连接失败")
  108. raise Exception("MySQL数据库连接失败")
  109. try:
  110. # 阶段一:抓新增场次的列表入库
  111. try:
  112. run_incremental(log, sql_pool)
  113. except Exception as e:
  114. log.error(f"增量抓取失败: {e}")
  115. # 阶段二:扫库 state != 1 的记录补抓详情
  116. if sql_pool is not None:
  117. try:
  118. update_details_for_pending(log, sql_pool)
  119. except Exception as e:
  120. log.error(f"详情补抓失败: {e}")
  121. except Exception as e:
  122. log.error(f"{sys._getframe().f_code.co_name} error: {e}")
  123. finally:
  124. log.info(f"爬虫程序 {sys._getframe().f_code.co_name} 运行结束,等待下一轮采集 ...")
  125. def schedule_task():
  126. """启动调度器:先即时跑一次,之后每天 05:00 跑一次增量。"""
  127. # rea_main(log=logger)
  128. schedule.every().day.at("05:00").do(rea_main, log=logger)
  129. while True:
  130. schedule.run_pending()
  131. time.sleep(1)
  132. if __name__ == "__main__":
  133. # 测试时直接跑一次
  134. # rea_main(log=logger)
  135. # 上生产再切回 schedule
  136. schedule_task()