From 7ccf96ac55de323b6d5b07c660aa2d5cefe3960c Mon Sep 17 00:00:00 2001
From: Kizzen983 <8342749+kizzen983@user.noreply.gitee.com>
Date: Fri, 17 Apr 2026 16:47:53 +0800
Subject: [PATCH 1/3] feat: add evolution chain scraping
Parse evolution chain from sprite detail pages, extracting each stage's
name, id, evolves_from, level requirement, and special condition.
Stored in JSON before skills field, and in CSV as semicolon-separated
entries with format name(level/condition).
---
rocom_scraper.py | 96 +++++++++++++++++++++++++++++++++++++-----------
1 file changed, 75 insertions(+), 21 deletions(-)
diff --git a/rocom_scraper.py b/rocom_scraper.py
index ab10b0d..7aa42f3 100644
--- a/rocom_scraper.py
+++ b/rocom_scraper.py
@@ -54,7 +54,7 @@
def print_progress(current: int, total: int, label: str = "", width: int = 28):
"""用 \\r 在同一行覆写进度条"""
filled = int(width * current / total) if total > 0 else 0
- bar = "█" * filled + "░" * (width - filled)
+ bar = "#" * filled + "-" * (width - filled)
pct = current / total * 100 if total > 0 else 0
label = (label[:32] + "…") if len(label) > 33 else label
print(f"\r[{current:>4}/{total}] {bar} {pct:5.1f}% {label} ", end="", flush=True)
@@ -110,35 +110,31 @@ def parse_list_page() -> list[dict]:
soup = fetch(LIST_URL)
entries = []
- # 每个精灵卡片在
内, 链接格式: /rocom/精灵名
content = soup.find("div", id="mw-content-text") or soup
- for td in content.select("table td"):
- # 找 NO.xxx
- no_text = td.get_text(" ", strip=True)
- no_m = re.search(r'NO\.(\d+)', no_text)
+ for card in content.select("div.rocom_prop_img"):
+ # NO编号
+ no_span = card.find("span", style=re.compile(r'font-size:10px'))
+ if not no_span:
+ continue
+ no_m = re.search(r'NO\.(\d+)', no_span.get_text())
if not no_m:
continue
no = int(no_m.group(1))
- # 找主链接 (精灵页面)
- links = td.find_all("a", href=re.compile(r'^/rocom/[^%]|^/rocom/%'))
- if not links:
+ # 主链接
+ a = card.find("a", href=re.compile(r'^/rocom/'))
+ if not a:
continue
- href = links[0]["href"]
+ href = a["href"]
url = urljoin(BASE_URL, href)
- name_raw = unquote(href.split("/rocom/")[-1])
- # 区分本体名和形态名: "鸭吉吉(蓬松的样子)"
- form_m = re.match(r'^(.+?)((.+))$', name_raw)
- if form_m:
- name = form_m.group(1)
- form = form_m.group(2)
- else:
- name = name_raw
- form = None
+ # 名字从 block_2,形态从 block_3
+ name_p = card.find("p", class_="block_2")
+ name = name_p.get_text(strip=True) if name_p else unquote(href.split("/rocom/")[-1])
+ form_p = card.find("p", class_="block_3")
+ form = form_p.get_text(strip=True) or None if form_p else None
- # 是否有异色图
- has_shiny = "异色" in td.get_text()
+ has_shiny = "异色" in card.get_text()
entries.append({
"no": no,
@@ -328,6 +324,55 @@ def parse_attributes_from_detail(soup: BeautifulSoup) -> list[str]:
return attrs
+def parse_evolution_chain(soup: BeautifulSoup) -> list[dict] | None:
+ """解析进化链,返回 [{name, id, condition}, ...] 或 None"""
+ box = soup.find("div", class_="rocom_spirit_evolution_box")
+ if not box:
+ return None
+
+ # 收集各阶段精灵
+ stages = []
+ for i in range(1, 4):
+ div = box.find("div", class_=f"rocom_spirit_evolution_{i}")
+ if not div:
+ break
+ a = div.find("a")
+ if not a:
+ break
+ name = a.get("title", "")
+ href = a.get("href", "")
+ sprite_id = unquote(href.split("/rocom/")[-1]) if "/rocom/" in href else name
+ stages.append({"name": name, "id": sprite_id})
+
+ if len(stages) <= 1:
+ return None
+
+ # 进化等级:在 evolution_1 和 evolution_2 之间,evolution_2 和 evolution_3 之间
+ level_divs = box.find_all("div", class_="rocom_spirit_evolution_level")
+ levels = []
+ for ld in level_divs:
+ p = ld.find("p", class_="rocom_spirit_evolution_level_num")
+ levels.append(p.get_text(strip=True) if p else None)
+
+ # 进化条件(整个 rightBox 范围内)
+ rightbox = soup.find("div", class_="rocom_sprite_temp_evolve_rightBox")
+ condition = None
+ if rightbox:
+ cond_p = rightbox.find("p", class_="rocom_evolution_data")
+ if cond_p:
+ condition = cond_p.get_text(strip=True)
+
+ # 组装:每个非首阶段附上进化到它的等级/条件
+ result = [{"name": stages[0]["name"], "id": stages[0]["id"], "evolves_from": None, "level": None, "condition": None}]
+ for i, stage in enumerate(stages[1:]):
+ level = levels[i] if i < len(levels) else None
+ # 最后一段进化才附条件(条件通常描述最终进化)
+ cond = condition if i == len(stages) - 2 else None
+ result.append({"name": stage["name"], "id": stage["id"], "evolves_from": stages[i]["name"], "level": level, "condition": cond})
+
+ return result
+
+
def parse_sprite_detail(entry: dict) -> dict:
"""爬取并解析单个精灵的详情页"""
soup = fetch(entry["url"])
@@ -370,6 +415,9 @@ def parse_sprite_detail(entry: dict) -> dict:
# 克制关系
matchup = parse_type_matchup(content)
+ # 进化链
+ evolution_chain = parse_evolution_chain(content)
+
# 技能
skills = parse_skills(content)
@@ -379,6 +427,7 @@ def parse_sprite_detail(entry: dict) -> dict:
"stats": stats,
"ability": ability,
"type_matchup": matchup,
+ "evolution_chain": evolution_chain,
"skills": skills,
}
@@ -530,6 +579,7 @@ def _save(data: list, path: Path):
"hp", "atk", "sp_atk", "def", "sp_def", "spd",
"ability_name", "ability_desc",
"strong_against", "weak_to", "resists", "resisted_by",
+ "evolution_chain",
"skills",
]
@@ -569,6 +619,10 @@ def skill_str(s: dict) -> str:
"weak_to": ",".join(matchup.get("weak_to") or []),
"resists": ",".join(matchup.get("resists") or []),
"resisted_by": ",".join(matchup.get("resisted_by") or []),
+ "evolution_chain": ";".join(
+ f"{e['name']}({e.get('level') or ''}/{e.get('condition') or ''})"
+ for e in (d.get("evolution_chain") or [])
+ ),
"skills": ";".join(skill_str(s) for s in (d.get("skills") or [])),
}
From 4afbc9de0a9887b3241adbfad654a4f403f4c9fb Mon Sep 17 00:00:00 2001
From: Kizzen983 <8342749+kizzen983@user.noreply.gitee.com>
Date: Fri, 17 Apr 2026 17:57:22 +0800
Subject: [PATCH 2/3] refactor: replace id with no in evolution chain, backfill
after scrape
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Use sprite no (numeric) instead of name as identifier in evolution_chain
stages. Backfill is done post-scrape via name→no mapping.
---
rocom_scraper.py | 18 ++++++++++++++----
1 file changed, 14 insertions(+), 4 deletions(-)
diff --git a/rocom_scraper.py b/rocom_scraper.py
index 7aa42f3..ae4177b 100644
--- a/rocom_scraper.py
+++ b/rocom_scraper.py
@@ -363,12 +363,11 @@ def parse_evolution_chain(soup: BeautifulSoup) -> list[dict] | None:
condition = cond_p.get_text(strip=True)
# 组装:每个非首阶段附上进化到它的等级/条件
- result = [{"name": stages[0]["name"], "id": stages[0]["id"], "evolves_from": None, "level": None, "condition": None}]
+ result = [{"name": stages[0]["name"], "no": None, "evolves_from": None, "level": None, "condition": None}]
for i, stage in enumerate(stages[1:]):
level = levels[i] if i < len(levels) else None
- # 最后一段进化才附条件(条件通常描述最终进化)
cond = condition if i == len(stages) - 2 else None
- result.append({"name": stage["name"], "id": stage["id"], "evolves_from": stages[i]["name"], "level": level, "condition": cond})
+ result.append({"name": stage["name"], "no": None, "evolves_from": stages[i]["name"], "level": level, "condition": cond})
return result
@@ -549,7 +548,10 @@ def main():
time.sleep(random.uniform(args.delay, args.delay + 1.5))
- # 3. 最终保存
+ # 3. 回填进化链 id(名字 → no)
+ _backfill_evolution_ids(results)
+
+ # 4. 最终保存
_save(results, out_path)
csv_path = out_path.with_suffix(".csv")
_save_csv(results, csv_path)
@@ -564,6 +566,14 @@ def main():
print(f"[完成] 失败URL已记录至: {fail_path}")
+def _backfill_evolution_ids(results: list):
+ """用名字→no映射回填进化链中的no字段"""
+ name_to_no = {s["name"]: s["no"] for s in results if s.get("name")}
+ for s in results:
+ for stage in (s.get("evolution_chain") or []):
+ stage["no"] = name_to_no.get(stage["name"])
+
+
def _save(data: list, path: Path):
# 写入前先备份原文件
if path.exists():
From f680f88a3176b42435736b84aa5144985aef2ab9 Mon Sep 17 00:00:00 2001
From: Kizzen983 <8342749+kizzen983@user.noreply.gitee.com>
Date: Fri, 17 Apr 2026 18:00:24 +0800
Subject: [PATCH 3/3] fix: backfill evolution chain no for sprites with form
variants
---
rocom_scraper.py | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/rocom_scraper.py b/rocom_scraper.py
index ae4177b..1b905a8 100644
--- a/rocom_scraper.py
+++ b/rocom_scraper.py
@@ -567,8 +567,14 @@ def main():
def _backfill_evolution_ids(results: list):
- """用名字→no映射回填进化链中的no字段"""
- name_to_no = {s["name"]: s["no"] for s in results if s.get("name")}
+ """用名字→no映射回填进化链中的no字段,支持有form的精灵"""
+ name_to_no = {}
+ for s in results:
+ if not s.get("name"):
+ continue
+ name_to_no[s["name"]] = s["no"]
+ if s.get("form"):
+ name_to_no[f"{s['name']}({s['form']})"] = s["no"]
for s in results:
for stage in (s.get("evolution_chain") or []):
stage["no"] = name_to_no.get(stage["name"])
|