Explorar el Código

feat(rarity): 新增稀有度筛选及回填功能

- 定义稀有度映射表 RARITY_MAP,涵盖21种稀有度标签
- 实现获取指定稀有度单页卡片列表的函数 get_rarity_single_page
- 实现分页抓取某稀有度所有卡片并回填数据库的函数 get_rarity_list
- 增加遍历全部稀有度并批量更新的函数 get_all_rarity
- 在爬取卡片详情流程末尾调用 get_all_rarity,实现稀有度信息的补充和更新
- 添加重试机制确保网络请求稳定,完善日志输出便于追踪错误
charley hace 1 mes
padre
commit
90b270e1aa
Se han modificado 1 ficheros con 112 adiciones y 1 borrados
  1. 112 1
      pokemon_tcg_spider/fan_pokemon_card_spider.py

+ 112 - 1
pokemon_tcg_spider/fan_pokemon_card_spider.py

@@ -24,9 +24,21 @@ logger.add("./logs/fan_{time:YYYYMMDD}.log", encoding='utf-8', rotation="00:00",
            level="DEBUG", retention="7 day")
 
 """
-expansion_series -> 
+expansion_series ->
 """
 
+# 稀有度筛选值 → 稀有度标签
+# 来源:tw 列表页搜索表单「稀有度」区块的 rarity[] 复选框(value → label),共 21 种
+# 用法:GET list/?rarity[]=<value>;不带 expansionCodes 表示跨全部扩充包,
+#      不带 regulation 表示全赛制(实测 == regulation=all,不会漏掉旧赛制卡)
+RARITY_MAP = {
+    "1": "C", "2": "U", "3": "R", "4": "RR", "5": "RRR",
+    "6": "PR", "7": "TR", "8": "SR", "9": "HR", "10": "UR",
+    "11": "無標記", "12": "K", "13": "A", "14": "AR", "15": "SAR",
+    "16": "S", "17": "SSR", "18": "ACE", "19": "BWR", "20": "MUR",
+    "21": "MA",
+}
+
 
 def after_log(retry_state):
     """
@@ -212,6 +224,97 @@ def get_data_list(log, sql_pool, cate_tuple):
         page += 1
 
 
+# ----------------------------------------------------------------------------------------------------------------------
+@retry(stop=stop_after_attempt(5), wait=wait_fixed(1), after=after_log)
+def get_rarity_single_page(log, rarity_value, page):
+    """请求指定稀有度、指定页码的卡片列表,返回本页所有 card_id。
+
+    :param log: logger 对象
+    :param rarity_value: 稀有度筛选值(RARITY_MAP 的 key,如 '8' 表示 SR)
+    :param page: 页码,从 1 开始
+    :return: (card_id 列表, 本页卡片数量)
+    """
+    log.debug(f'Request get_rarity_single_page rarity_value={rarity_value} page={page}')
+    url = "https://asia.pokemon-card.com/tw/card-search/list/"
+    # rarity[] 为列表页搜索表单的稀有度复选框参数;不带 expansionCodes=跨全部扩充包,
+    # 不带 regulation=全赛制(实测与 regulation=all 一致,能覆盖旧赛制卡)
+    params = {
+        "rarity[]": rarity_value,
+        "pageNo": page,
+    }
+    response = requests.get(url, headers=headers, params=params, timeout=10)
+    response.raise_for_status()
+
+    selector = Selector(response.text)
+    tag_li_list = selector.xpath('//*[@id="searchForm"]//ul/li')
+
+    card_ids = []
+    for tag_li in tag_li_list:
+        detail_url_str = tag_li.xpath('./a/@href').get()
+        # 只取指向卡片详情页的 li,过滤掉搜索表单内其它无关 li
+        if not detail_url_str or '/detail/' not in detail_url_str:
+            continue
+        card_id = detail_url_str.split('/')[-2]
+        card_ids.append(card_id)
+    return card_ids, len(card_ids)
+
+
+def get_rarity_list(log, sql_pool, rarity_value, rarity_label):
+    """翻页抓取某个稀有度下的全部卡片,并把 rarity 回填到已入库的繁中卡片。
+
+    只按 card_id + crawler_language 定位更新已存在的行,不新增卡片;
+    库中没有的 card_id(如尚未爬到的卡)更新 0 行,无副作用。
+
+    :param log: logger 对象
+    :param sql_pool: MySQL 连接池
+    :param rarity_value: 稀有度筛选值(RARITY_MAP 的 key)
+    :param rarity_label: 稀有度标签,写入 rarity 字段(如 'SR')
+    """
+    page = 1
+    max_page = 500  # 上限保护:普通卡(C/U)跨全部扩充包页数较多
+    total = 0
+    while page <= max_page:
+        try:
+            card_ids, len_items = get_rarity_single_page(log, rarity_value, page)
+        except Exception as e:
+            log.error(
+                f"{inspect.currentframe().f_code.co_name} get_rarity_single_page rarity={rarity_label} page={page} error: {e}")
+            break
+
+        if card_ids:
+            # 逐行回填 rarity:data_list 与 condition_list 等长一一对应
+            data_list = [{"rarity": rarity_label} for _ in card_ids]
+            condition_list = [{"card_id": cid, "crawler_language": crawler_language} for cid in card_ids]
+            try:
+                sql_pool.update_many(table="pokemon_card_record", data_list=data_list, condition_list=condition_list)
+                total += len(card_ids)
+            except Exception as e:
+                log.error(
+                    f"{inspect.currentframe().f_code.co_name} update_many rarity={rarity_label} page={page} error: {e}")
+
+        if len_items < 20:
+            log.debug(f'--------------- rarity={rarity_label} page {page} has {len_items} items, break ---------------')
+            break
+        page += 1
+
+    log.info(f'稀有度 {rarity_label}(value={rarity_value}) 采集完成,累计回填 {total} 张卡')
+
+
+def get_all_rarity(log, sql_pool):
+    """遍历 RARITY_MAP 全部稀有度,逐个筛选并回填繁中卡片的 rarity 字段。
+
+    :param log: logger 对象
+    :param sql_pool: MySQL 连接池
+    """
+    log.debug(f'........... 获取并回填稀有度 ..........')
+    for rarity_value, rarity_label in RARITY_MAP.items():
+        try:
+            log.info(f'正在处理稀有度 {rarity_label} (value={rarity_value}) .........')
+            get_rarity_list(log, sql_pool, rarity_value, rarity_label)
+        except Exception as e:
+            log.error(f"{inspect.currentframe().f_code.co_name} get_rarity_list rarity={rarity_label} error: {e}")
+
+
 # ----------------------------------------------------------------------------------------------------------------------
 @retry(stop=stop_after_attempt(5), wait=wait_fixed(1), after=after_log)
 def get_details(log, sql_id_detail_url: tuple, sql_pool):
@@ -286,6 +389,14 @@ def fz_pokemon_main(log):
             except Exception as e:
                 log.error(f"Request get_details error: {e}")
 
+        # 回填稀有度:跨全部扩充包按 rarity[] 逐个筛选,将 rarity 写入已入库卡片
+        # 既补历史数据,也让后续每轮采集自动带上最新稀有度(列表 upsert 不动 rarity 列,不会被覆盖)
+        log.debug(f"........... 回填稀有度 ..........")
+        try:
+            get_all_rarity(log, sql_pool)
+        except Exception as e:
+            log.error(f"{inspect.currentframe().f_code.co_name} Request get_all_rarity error: {e}")
+
     except Exception as e:
         log.error(f'{inspect.currentframe().f_code.co_name} error: {e}')
     finally: