km_spdier.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. # -*- coding: utf-8 -*-
  2. # Author : Charley
  3. # Python : 3.12.10
  4. # Date : 2026/08/04
  5. """得卡 DECA 商品卡牌清单(checklist)采集:拉某商品 groupbuy/card/list 的全部卡牌,入 deca_onsale_kami_record。
  6. 翻页「翻到底」(靠 data.total 判断),uuid 唯一键 INSERT IGNORE 去重。
  7. 本接口需 Authorization Bearer,复用 deca_on_sale_daily_spider 的签名与 token 续期逻辑。
  8. 字段对应(列表项):
  9. sortOrder 序号 / aliasName 别名 / teamNameEn·teamNameZh 球队 / playerNameEn·playerNameZh 球员 /
  10. cardType 卡种 / limitNumber 限编 / cardNumber 卡编 / uuid 卡牌唯一标识
  11. 用法:
  12. python km_spdier.py <商品code> # 采集指定商品的全部卡牌清单,默认用示例 code
  13. 或作为模块:from km_spdier import get_card_list; get_card_list(log, code, pool)
  14. """
  15. import sys
  16. import time
  17. from loguru import logger
  18. from mysql_pool import MySQLConnectionPool
  19. import deca_on_sale_daily_spider as deca # 复用 do_request / ensure_token / make_signature
  20. # 覆盖 deca 导入时装的 logger handler,本任务日志独立到 kami_*.log
  21. logger.remove()
  22. logger.add("./logs/kami_{time:YYYYMMDD}.log", encoding="utf-8", rotation="00:00",
  23. format="[{time:YYYY-MM-DD HH:mm:ss.SSS}] {level} {message}",
  24. level="DEBUG", retention="7 day")
  25. PAGE_SIZE = 20 # 每页条数(抓包实测 20)
  26. MAX_PAGES = 600 # 翻页保护上限(单商品清单可上万张,20/页约需数百页)
  27. def parse_kami(item: dict, product_code: str) -> dict | None:
  28. """把卡牌清单项解析成 deca_onsale_kami_record 一行。
  29. Args:
  30. item (dict): card/list 的 data.list 项。
  31. product_code (str): 所属商品编码(列表项不含,由外部带入)。
  32. Returns:
  33. dict | None: 与 deca_onsale_kami_record 列对应的数据字典;无 uuid 时返回 None。
  34. """
  35. uuid = item.get("uuid")
  36. if not uuid:
  37. return None
  38. return {
  39. "uuid": uuid,
  40. "product_code": product_code,
  41. "sort_order": item.get("sortOrder"),
  42. "alias_name": item.get("aliasName"),
  43. "team_name_en": item.get("teamNameEn"),
  44. "team_name_zh": item.get("teamNameZh"),
  45. "player_name_en": item.get("playerNameEn"),
  46. "player_name_zh": item.get("playerNameZh"),
  47. "card_type": item.get("cardType"),
  48. "limit_number": item.get("limitNumber"),
  49. "card_number": item.get("cardNumber"),
  50. }
  51. def get_card_list(log, product_code: str, pool) -> int:
  52. """翻页拉取某商品的全部卡牌清单(翻到底),INSERT IGNORE 写入 deca_onsale_kami_record。
  53. Args:
  54. log: 日志对象。
  55. product_code (str): 商品编码。
  56. pool (MySQLConnectionPool): MySQL 连接池。
  57. Returns:
  58. int: 本商品实际写入的卡牌数(已去重)。
  59. """
  60. page = 1
  61. saved = 0
  62. total = None
  63. while page <= MAX_PAGES:
  64. body = {"code": product_code, "page": page, "pageSize": PAGE_SIZE}
  65. try:
  66. log.info(f"商品 {product_code} 卡牌清单请求第 {page} 页")
  67. resp = deca.do_request(log, "/api/v1/app/groupbuy/card/list", body, need_auth=True)
  68. except Exception as e:
  69. log.error(f"商品 {product_code} 卡牌清单第 {page} 页请求失败: {e}")
  70. break
  71. if not resp or resp.get("code") != 0:
  72. log.info(f"商品 {product_code} 卡牌清单返回异常: {resp.get('msg') if resp else None}")
  73. break
  74. data = resp.get("data") or {}
  75. if total is None:
  76. total = data.get("total")
  77. log.info(f"商品 {product_code} 卡牌清单共 {total} 张,开始翻页入库")
  78. items = data.get("list") or []
  79. if not items:
  80. break
  81. rows = [r for r in (parse_kami(it, product_code) for it in items) if r]
  82. if rows:
  83. pool.insert_many(table="deca_onsale_kami_record", data_list=rows, ignore=True)
  84. saved += len(rows)
  85. # 翻到底:本页不足一页(<PAGE_SIZE)即最后一页。
  86. # total 是动态值(清单持续新增),仅作日志参考,不用于终止判断。
  87. if len(items) < PAGE_SIZE:
  88. break
  89. page += 1
  90. if page % 50 == 0:
  91. log.info(f"商品 {product_code} 已翻到第 {page} 页,累计入库 {saved}")
  92. # time.sleep(1) # 每页间隔 1s,降低请求频率
  93. log.info(f"商品 {product_code} 卡牌清单入库完成 {saved} 张(total={total})")
  94. return saved
  95. def main(product_code: str):
  96. """采集单个商品的全部卡牌清单入库。
  97. Args:
  98. product_code (str): 商品编码。
  99. """
  100. pool = MySQLConnectionPool(log=logger)
  101. if not pool.check_pool_health():
  102. logger.error("数据库连接池异常")
  103. return
  104. deca.ensure_token(logger) # 预取 token(后续请求自动续期)
  105. get_card_list(logger, product_code, pool)
  106. if __name__ == "__main__":
  107. # GB26081928662 / GB26082068604 / GB26082040642
  108. code = sys.argv[1] if len(sys.argv) > 1 else "GB26082040642"
  109. main(code)