# Emby刷新演员头像脚本 ## 1. 脚本总结 - 用 PersonId 直接请求图片 → 触发Emby下载,不用模拟点击 - 智能生成搜索关键词 → 中文取前1-2个字,英文取前2个字母 - 进度持久化 → 中断后可以继续,不会重复处理 - 限流保护 → 429/403/网络500自动停止 - 只处理有 Primary 标记的 → 避免无效请求 - 适用范围: Emby 4.8.x / 4.9.x 通用 - 需要科学上网的环境(脚本走系统代理) - 大批量补全演员头像 说明: 整个补齐脚本分为两个阶段: 第一阶段: 补齐打开影片页就可以自动刷新的演员头像; 第二阶段: 补齐需要打开演员详情页才能刷新的演员头像. 脚本需要使用python,请自行安装。 ## 2. 第一阶段脚本 ```bash import requests import time import json import os import sys import re # ========================= # 你需要修改的配置 # ========================= EMBY_URL = "" API_KEY = "" USER_ID = "" # 每个搜索词之间等待多少秒 SEARCH_TERM_DELAY_SECONDS = 5 # 同一个搜索词下,每个演员头像之间等待多少秒 IMAGE_DELAY_SECONDS = 4 # 每个搜索词最多请求多少个带头像标记的演员 # 先保守一点,避免一个关键词返回太多人 MAX_IMAGES_PER_TERM = 50 # 每次搜索最多返回多少个演员 SEARCH_LIMIT = 200 # 读取媒体库分页间隔 PAGE_DELAY_SECONDS = 1 # 进度文件:记录已经搜索过的关键词、已经处理过的人物头像 PROGRESS_FILE = r"D:\emby_person_image_warmer_progress.json" # 只从媒体库中收集演员 TARGET_PERSON_TYPES = {"Actor"} # ========================= # 以下一般不用修改 # ========================= session = requests.Session() session.headers.update({ "X-Emby-Token": API_KEY, "User-Agent": "Mozilla/5.0 Emby-Web-Person-Image-Warmer/1.0" }) def load_progress(): if not os.path.exists(PROGRESS_FILE): return { "terms": {}, "persons": {} } try: with open(PROGRESS_FILE, "r", encoding="utf-8") as f: data = json.load(f) if "terms" not in data: data["terms"] = {} if "persons" not in data: data["persons"] = {} return data except Exception: return { "terms": {}, "persons": {} } def save_progress(progress): with open(PROGRESS_FILE, "w", encoding="utf-8") as f: json.dump(progress, f, ensure_ascii=False, indent=2) def wait_seconds(seconds, reason): print(f"{reason},等待 {seconds} 秒...") time.sleep(seconds) def get_all_media_items(): """ 获取所有电影和电视剧,用来收集演员名字。 这里只读取 People,不刷新元数据,不替换图片。 """ items = [] start = 0 limit = 100 while True: r = session.get( f"{EMBY_URL}/emby/Users/{USER_ID}/Items", params={ "UserId": USER_ID, "Recursive": "true", "IncludeItemTypes": "Movie,Series", "Fields": "People", "StartIndex": start, "Limit": limit }, timeout=60 ) r.raise_for_status() data = r.json() batch = data.get("Items", []) items.extend(batch) print(f"已读取媒体条目:{len(items)} / {data.get('TotalRecordCount', '?')}") if len(batch) < limit: break start += limit wait_seconds(PAGE_DELAY_SECONDS, "读取下一批媒体前") return items def collect_actor_names(items): """ 从媒体条目中收集演员名字。 """ names = set() for item in items: for p in item.get("People", []) or []: if p.get("Type") not in TARGET_PERSON_TYPES: continue name = p.get("Name") if not name: continue name = name.strip() if name: names.add(name) return sorted(names) def is_cjk_char(ch): """ 判断是否中日韩字符。 """ return ( "\u4e00" <= ch <= "\u9fff" or "\u3040" <= ch <= "\u30ff" or "\uac00" <= ch <= "\ud7af" ) def make_search_terms_from_name(name): """ 从演员名字生成搜索关键词。 例: 刘若英 -> 刘, 刘若 刘诗诗 -> 刘, 刘诗 Li Ying -> li, yi Liu Shishi -> li, sh Aoi Tsukasa -> ao, ts 葵司 -> 葵, 葵司 """ terms = set() clean = name.strip() if not clean: return terms # 中日韩名字:取第一个字、前两个字 cjk_chars = [ch for ch in clean if is_cjk_char(ch)] if cjk_chars: terms.add(cjk_chars[0]) if len(cjk_chars) >= 2: terms.add("".join(cjk_chars[:2])) # 拉丁字母名字:每个单词取前两个字母 words = re.findall(r"[A-Za-zÀ-ÖØ-öø-ÿ]+", clean) for w in words: w = w.lower() if len(w) >= 2: terms.add(w[:2]) elif len(w) == 1: terms.add(w) return terms def build_search_terms(actor_names): """ 根据所有演员名字生成搜索词列表。 """ terms = set() for name in actor_names: terms.update(make_search_terms_from_name(name)) # 去掉太短且可能结果太杂的英文单字母 cleaned = set() for t in terms: t = t.strip() if not t: continue # 英文单字母容易返回太多,跳过 if re.fullmatch(r"[a-zA-Z]", t): continue cleaned.add(t) return sorted(cleaned) def search_persons(term): """ 模拟 Emby 搜索演员。 """ try: r = session.get( f"{EMBY_URL}/emby/Users/{USER_ID}/Items", params={ "UserId": USER_ID, "Recursive": "true", "IncludeItemTypes": "Person", "SearchTerm": term, "Fields": "ImageTags,PrimaryImageAspectRatio,ProviderIds", "Limit": SEARCH_LIMIT }, timeout=90 ) if r.status_code == 200: data = r.json() items = data.get("Items", []) total = data.get("TotalRecordCount", len(items)) print(f"[搜索OK] 关键词={term} | 返回={len(items)} / 总数={total}") return "ok", items, total if r.status_code == 429: retry_after = r.headers.get("Retry-After") print(f"[429 限流] 关键词={term}") if retry_after: print(f"服务端要求等待 {retry_after} 秒。现在停止脚本。") else: print("检测到限流。现在停止脚本,建议稍后再跑。") return "rate_limited", [], 0 if r.status_code == 403: print(f"[403 禁止访问] 关键词={term}") print("检测到可能的封锁/权限问题。现在停止脚本。") return "blocked", [], 0 if r.status_code == 500: print(f"[搜索500] 关键词={term} | {r.text[:200]}") return "server_error_500", [], 0 print(f"[搜索{r.status_code}] 关键词={term} | {r.text[:200]}") return "fail", [], 0 except requests.exceptions.Timeout: print(f"[搜索超时] 关键词={term}") return "timeout", [], 0 except Exception as e: print(f"[搜索错误] 关键词={term}: {e}") return "fail", [], 0 def request_person_image(person): """ 请求搜索结果里带 ImageTags.Primary 的演员头像。 这是关键: /emby/Items/{PersonId}/Images/Primary?tag=xxx 不刷新元数据。 不替换图片。 不删除图片。 """ name = person.get("Name") person_id = str(person.get("Id")) image_tags = person.get("ImageTags") or {} primary_tag = image_tags.get("Primary") if not primary_tag: print(f" [跳过无头像标记] {name} | PersonId={person_id}") return "skip_no_tag" url = f"{EMBY_URL}/emby/Items/{person_id}/Images/Primary" params = { "maxWidth": 400, "quality": 90, "tag": primary_tag } try: r = session.get(url, params=params, timeout=90) content_type = r.headers.get("Content-Type", "") if r.status_code == 200: print(f" [图片OK] {name} | PersonId={person_id} | {len(r.content)} bytes | {content_type}") return "ok" if r.status_code == 404: print(f" [图片404] {name} | PersonId={person_id}") return "not_found" if r.status_code == 429: print(f" [429 限流] {name} | PersonId={person_id}") return "rate_limited" if r.status_code == 403: print(f" [403 禁止访问] {name} | PersonId={person_id}") return "blocked" if r.status_code == 500: text = r.text[:300] print(f" [图片500] {name} | PersonId={person_id} | {text}") # 网络/代理/上游超时类 500,不记为成功,后面可以重试 lower_text = text.lower() if ( "ssl connection" in lower_text or "previous timeout" in lower_text or "timeout" in lower_text or "connection" in lower_text ): return "network_500" # Emby内部空引用一类,记录但不终止 if "object reference" in lower_text: return "server_error_500" return "server_error_500" print(f" [图片{r.status_code}] {name} | PersonId={person_id} | {r.text[:200]}") return "fail" except requests.exceptions.Timeout: print(f" [图片超时] {name} | PersonId={person_id}") return "timeout" except Exception as e: print(f" [图片请求错误] {name} | PersonId={person_id} | {e}") return "fail" if __name__ == "__main__": progress = load_progress() print("开始读取媒体库,用来收集演员名字...") items = get_all_media_items() print("\n开始收集演员名字...") actor_names = collect_actor_names(items) print(f"共收集到演员数量:{len(actor_names)}") search_terms = build_search_terms(actor_names) print(f"生成搜索关键词数量:{len(search_terms)}") print("前 50 个搜索关键词预览:") for t in search_terms[:50]: print("-", t) print("\n开始正式处理演员头像...\n") for term_index, term in enumerate(search_terms, 1): if term in progress["terms"]: old_status = progress["terms"][term].get("status") print(f"{term_index}/{len(search_terms)} 跳过关键词:{term} | 已处理状态={old_status}") continue print("=" * 70) print(f"{term_index}/{len(search_terms)} 搜索关键词:{term}") search_status, people, total = search_persons(term) if search_status in {"rate_limited", "blocked"}: save_progress(progress) print("\n检测到限流/封锁风险,脚本停止。") sys.exit(1) if search_status != "ok": print("本关键词搜索失败,不写入关键词进度,下次会重试。") wait_seconds(SEARCH_TERM_DELAY_SECONDS, "搜索词之间间隔") continue # 只处理带 Primary 图片标记的人 people_with_primary = [] skipped_no_tag = 0 skipped_done = 0 for p in people: person_id = str(p.get("Id")) image_tags = p.get("ImageTags") or {} primary_tag = image_tags.get("Primary") if not primary_tag: skipped_no_tag += 1 continue # 已经处理过这个人物头像,跳过 if person_id in progress["persons"]: skipped_done += 1 continue people_with_primary.append(p) print(f"本关键词返回人物:{len(people)}") print(f"无头像标记跳过:{skipped_no_tag}") print(f"已处理人物跳过:{skipped_done}") print(f"本次需要请求头像:{len(people_with_primary)}") processed_count = 0 ok_count = 0 fail_count = 0 network_500_count = 0 server_500_count = 0 for idx, person in enumerate(people_with_primary[:MAX_IMAGES_PER_TERM], 1): person_id = str(person.get("Id")) name = person.get("Name") print(f" {idx}/{min(len(people_with_primary), MAX_IMAGES_PER_TERM)} 请求头像:{name} | PersonId={person_id}") result = request_person_image(person) processed_count += 1 if result == "ok": ok_count += 1 progress["persons"][person_id] = { "name": name, "status": "ok", "term": term, "time": time.strftime("%Y-%m-%d %H:%M:%S") } save_progress(progress) elif result in {"not_found", "skip_no_tag", "server_error_500"}: if result == "server_error_500": server_500_count += 1 else: fail_count += 1 # 这类不是网络临时问题,可以记录,避免反复请求 progress["persons"][person_id] = { "name": name, "status": result, "term": term, "time": time.strftime("%Y-%m-%d %H:%M:%S") } save_progress(progress) elif result == "network_500": network_500_count += 1 print(" 检测到网络/SSL/timeout 类 500,本人物不写入进度,下次可重试。") # 连续/多次网络失败时,建议停止,不要继续刺激上游 if network_500_count >= 3: save_progress(progress) print("\n本关键词内出现 3 次网络类 500,脚本停止。请确认代理/Emby网络后再运行。") sys.exit(1) elif result in {"rate_limited", "blocked"}: save_progress(progress) print("\n检测到限流/封锁风险,脚本停止。") sys.exit(1) else: fail_count += 1 print(" 本次失败,不写入人物进度,下次会重试。") if idx < min(len(people_with_primary), MAX_IMAGES_PER_TERM): wait_seconds(IMAGE_DELAY_SECONDS, "同一关键词下演员头像之间间隔") # 这个关键词已经搜索并处理过一轮,记录关键词进度 progress["terms"][term] = { "status": "done", "returned": len(people), "total": total, "processed_images": processed_count, "ok": ok_count, "network_500": network_500_count, "server_500": server_500_count, "time": time.strftime("%Y-%m-%d %H:%M:%S") } save_progress(progress) print(f"关键词 {term} 完成:图片OK={ok_count},网络500={network_500_count},服务端500={server_500_count}") if term_index < len(search_terms): wait_seconds(SEARCH_TERM_DELAY_SECONDS, "搜索词之间间隔") print("\n全部处理完成。") print(f"进度文件已保存:{PROGRESS_FILE}") ``` # 3. 第二阶段脚本 ```bash import requests import time import json import os import sys # ========================= # 你需要修改的配置 # ========================= EMBY_URL = "" API_KEY = "" USER_ID = "" # 第一阶段脚本的进度文件(如果存在,就会跳过已经成功的人) PHASE1_PROGRESS_FILE = r"D:\emby_person_image_warmer_progress.json" # 第二阶段自己的进度文件 PHASE2_PROGRESS_FILE = r"D:\emby_person_detail_warmer_phase2_progress.json" # 每次拉取多少个人物 PAGE_LIMIT = 200 # 打开人物详情页后,等几秒再重新检查 DETAIL_RECHECK_DELAY_SECONDS = 2 # 每处理一个人物后等待几秒 PERSON_DELAY_SECONDS = 3 # 每处理多少个人物,休息一下 BATCH_SIZE = 100 BATCH_REST_SECONDS = 60 # ========================= # 以下一般不用修改 # ========================= session = requests.Session() session.headers.update({ "X-Emby-Token": API_KEY, "User-Agent": "Mozilla/5.0 Emby-Person-Detail-Warmer-Phase2/1.0" }) def load_json_file(path, default_value): if not os.path.exists(path): return default_value try: with open(path, "r", encoding="utf-8") as f: return json.load(f) except Exception: return default_value def save_json_file(path, data): with open(path, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) def load_phase1_ok_persons(): """ 读取第一阶段已成功的人物,避免重复处理。 """ data = load_json_file(PHASE1_PROGRESS_FILE, {"terms": {}, "persons": {}}) ok_persons = set() persons = data.get("persons", {}) for person_id, info in persons.items(): if info.get("status") == "ok": ok_persons.add(str(person_id)) return ok_persons def load_phase2_progress(): data = load_json_file(PHASE2_PROGRESS_FILE, {"persons": {}}) if "persons" not in data: data["persons"] = {} return data def save_phase2_progress(progress): save_json_file(PHASE2_PROGRESS_FILE, progress) def wait_seconds(seconds, reason): print(f"{reason},等待 {seconds} 秒...") time.sleep(seconds) def get_all_persons(): """ 直接读取 Emby 全部 Person 条目。 """ persons = [] start = 0 while True: r = session.get( f"{EMBY_URL}/emby/Users/{USER_ID}/Items", params={ "UserId": USER_ID, "Recursive": "true", "IncludeItemTypes": "Person", "Fields": "ImageTags,PrimaryImageAspectRatio,ProviderIds", "StartIndex": start, "Limit": PAGE_LIMIT }, timeout=90 ) r.raise_for_status() data = r.json() batch = data.get("Items", []) total = data.get("TotalRecordCount", "?") persons.extend(batch) print(f"已读取人物:{len(persons)} / {total}") if len(batch) < PAGE_LIMIT: break start += PAGE_LIMIT time.sleep(0.5) return persons def get_person_detail(person_id): """ 模拟打开人物详情页。 """ try: r = session.get( f"{EMBY_URL}/emby/Users/{USER_ID}/Items/{person_id}", params={ "Fields": "ProviderIds,ImageTags,PrimaryImageAspectRatio,Overview" }, timeout=90 ) if r.status_code == 200: return "ok", r.json() if r.status_code == 429: return "rate_limited", None if r.status_code == 403: return "blocked", None if r.status_code == 500: text = r.text[:300].lower() if ( "ssl connection" in text or "previous timeout" in text or "timeout" in text or "connection" in text ): return "network_500", None return "server_error_500", None return "fail", None except requests.exceptions.Timeout: return "timeout", None except Exception: return "fail", None def request_person_image(person_id, name, primary_tag): """ 请求人物头像: /emby/Items/{PersonId}/Images/Primary?tag=xxx """ try: r = session.get( f"{EMBY_URL}/emby/Items/{person_id}/Images/Primary", params={ "maxWidth": 400, "quality": 90, "tag": primary_tag }, timeout=90 ) if r.status_code == 200: print(f" [图片OK] {name} | PersonId={person_id} | {len(r.content)} bytes") return "ok" if r.status_code == 404: print(f" [图片404] {name} | PersonId={person_id}") return "not_found" if r.status_code == 429: print(f" [429 限流] {name} | PersonId={person_id}") return "rate_limited" if r.status_code == 403: print(f" [403 禁止访问] {name} | PersonId={person_id}") return "blocked" if r.status_code == 500: text = r.text[:300] print(f" [图片500] {name} | PersonId={person_id} | {text}") lower_text = text.lower() if ( "ssl connection" in lower_text or "previous timeout" in lower_text or "timeout" in lower_text or "connection" in lower_text ): return "network_500" if "object reference" in lower_text: return "server_error_500" return "server_error_500" print(f" [图片{r.status_code}] {name} | PersonId={person_id} | {r.text[:200]}") return "fail" except requests.exceptions.Timeout: print(f" [图片超时] {name} | PersonId={person_id}") return "timeout" except Exception as e: print(f" [图片请求错误] {name} | PersonId={person_id} | {e}") return "fail" if __name__ == "__main__": print("当前运行目录:", os.getcwd()) print("第一阶段进度文件:", PHASE1_PROGRESS_FILE) print("第二阶段进度文件:", PHASE2_PROGRESS_FILE) phase1_ok_persons = load_phase1_ok_persons() phase2_progress = load_phase2_progress() print(f"\n第一阶段已成功人物数量:{len(phase1_ok_persons)}") print("\n开始读取 Emby 全部人物...") persons = get_all_persons() print(f"\n共读取到人物数量:{len(persons)}") need_process = [] skipped_phase1_ok = 0 skipped_phase2_done = 0 skipped_already_has_primary = 0 for p in persons: person_id = str(p.get("Id")) name = p.get("Name", "") image_tags = p.get("ImageTags") or {} primary_tag = image_tags.get("Primary") # 第一阶段已经成功刷过 if person_id in phase1_ok_persons: skipped_phase1_ok += 1 continue # 第二阶段已经处理过 if person_id in phase2_progress["persons"]: skipped_phase2_done += 1 continue # 当前列表里已经有头像标记的,也先跳过 # 因为第二阶段目标就是补“没有头像标记但点详情会出来”的人 if primary_tag: skipped_already_has_primary += 1 continue need_process.append({ "Id": person_id, "Name": name }) print(f"第一阶段已成功,跳过:{skipped_phase1_ok}") print(f"第二阶段已处理,跳过:{skipped_phase2_done}") print(f"当前本身已有头像标记,跳过:{skipped_already_has_primary}") print(f"第二阶段需要处理:{len(need_process)}\n") processed = 0 detail_success_count = 0 image_ok_count = 0 still_no_primary_count = 0 network_500_count = 0 server_500_count = 0 fail_count = 0 for idx, person in enumerate(need_process, 1): person_id = person["Id"] name = person["Name"] print("=" * 70) print(f"{idx}/{len(need_process)} 处理人物:{name} | PersonId={person_id}") # 第一次打开人物详情 status1, detail1 = get_person_detail(person_id) if status1 in {"rate_limited", "blocked"}: save_phase2_progress(phase2_progress) print("\n检测到限流/封锁风险,脚本停止。") sys.exit(1) if status1 == "network_500": network_500_count += 1 print(" 打开人物详情时发生网络类 500,本次不写进度,下次重试。") if network_500_count >= 3: save_phase2_progress(phase2_progress) print("\n连续出现 3 次网络类 500,脚本停止。请确认代理/Emby 后再运行。") sys.exit(1) if idx < len(need_process): wait_seconds(PERSON_DELAY_SECONDS, "人物之间间隔") continue if status1 == "server_error_500": server_500_count += 1 print(" 打开人物详情时发生服务端 500,记录后跳过。") phase2_progress["persons"][person_id] = { "name": name, "status": "server_error_500_on_detail", "time": time.strftime("%Y-%m-%d %H:%M:%S") } save_phase2_progress(phase2_progress) if idx < len(need_process): wait_seconds(PERSON_DELAY_SECONDS, "人物之间间隔") continue if status1 != "ok": fail_count += 1 print(" 打开人物详情失败,本次不写进度,下次重试。") if idx < len(need_process): wait_seconds(PERSON_DELAY_SECONDS, "人物之间间隔") continue detail_success_count += 1 # 打开详情页后,等一会儿再重新读取一次详情 wait_seconds(DETAIL_RECHECK_DELAY_SECONDS, "已打开人物详情,重新检查头像标记前") status2, detail2 = get_person_detail(person_id) if status2 in {"rate_limited", "blocked"}: save_phase2_progress(phase2_progress) print("\n检测到限流/封锁风险,脚本停止。") sys.exit(1) if status2 == "network_500": network_500_count += 1 print(" 二次读取人物详情时发生网络类 500,本次不写进度,下次重试。") if network_500_count >= 3: save_phase2_progress(phase2_progress) print("\n连续出现 3 次网络类 500,脚本停止。请确认代理/Emby 后再运行。") sys.exit(1) if idx < len(need_process): wait_seconds(PERSON_DELAY_SECONDS, "人物之间间隔") continue if status2 == "server_error_500": server_500_count += 1 print(" 二次读取人物详情时发生服务端 500,记录后跳过。") phase2_progress["persons"][person_id] = { "name": name, "status": "server_error_500_on_recheck", "time": time.strftime("%Y-%m-%d %H:%M:%S") } save_phase2_progress(phase2_progress) if idx < len(need_process): wait_seconds(PERSON_DELAY_SECONDS, "人物之间间隔") continue if status2 != "ok": fail_count += 1 print(" 二次读取人物详情失败,本次不写进度,下次重试。") if idx < len(need_process): wait_seconds(PERSON_DELAY_SECONDS, "人物之间间隔") continue image_tags = detail2.get("ImageTags") or {} primary_tag = image_tags.get("Primary") if not primary_tag: still_no_primary_count += 1 print(" 打开人物详情后仍然没有头像标记,记录为 still_no_primary。") phase2_progress["persons"][person_id] = { "name": name, "status": "still_no_primary", "time": time.strftime("%Y-%m-%d %H:%M:%S") } save_phase2_progress(phase2_progress) if idx < len(need_process): wait_seconds(PERSON_DELAY_SECONDS, "人物之间间隔") continue # 已经拿到头像标记,正式请求头像 print(f" 详情补全成功,发现头像标记:{primary_tag}") image_result = request_person_image(person_id, name, primary_tag) if image_result == "ok": image_ok_count += 1 phase2_progress["persons"][person_id] = { "name": name, "status": "ok", "primary_tag": primary_tag, "time": time.strftime("%Y-%m-%d %H:%M:%S") } save_phase2_progress(phase2_progress) elif image_result == "network_500": network_500_count += 1 print(" 请求头像时发生网络类 500,不写进度,下次重试。") if network_500_count >= 3: save_phase2_progress(phase2_progress) print("\n连续出现 3 次网络类 500,脚本停止。请确认代理/Emby 后再运行。") sys.exit(1) elif image_result == "server_error_500": server_500_count += 1 phase2_progress["persons"][person_id] = { "name": name, "status": "server_error_500_on_image", "primary_tag": primary_tag, "time": time.strftime("%Y-%m-%d %H:%M:%S") } save_phase2_progress(phase2_progress) elif image_result in {"not_found"}: phase2_progress["persons"][person_id] = { "name": name, "status": image_result, "primary_tag": primary_tag, "time": time.strftime("%Y-%m-%d %H:%M:%S") } save_phase2_progress(phase2_progress) elif image_result in {"rate_limited", "blocked"}: save_phase2_progress(phase2_progress) print("\n检测到限流/封锁风险,脚本停止。") sys.exit(1) else: fail_count += 1 print(" 请求头像失败,本次不写进度,下次重试。") processed += 1 if idx < len(need_process): wait_seconds(PERSON_DELAY_SECONDS, "人物之间间隔") if processed > 0 and processed % BATCH_SIZE == 0: save_phase2_progress(phase2_progress) wait_seconds(BATCH_REST_SECONDS, f"已处理 {processed} 个人物,本批休息") save_phase2_progress(phase2_progress) print("\n全部处理完成。") print(f"成功打开详情:{detail_success_count}") print(f"成功刷出头像:{image_ok_count}") print(f"打开详情后仍无头像标记:{still_no_primary_count}") print(f"网络类500:{network_500_count}") print(f"服务端500:{server_500_count}") print(f"其他失败:{fail_count}") print(f"第二阶段进度文件:{PHASE2_PROGRESS_FILE}") ```