Fandom Wiki 网站爬取文本信息踩坑实录

文章目录

  • 项目概述
  • 踩坑记录与解决方案
    • [坑 1:手动能看到内容,代码爬取全为空 (动态渲染问题)](#坑 1:手动能看到内容,代码爬取全为空 (动态渲染问题))
    • [坑 2:Selenium 启动报错 DevToolsActivePort file doesn't exist](#坑 2:Selenium 启动报错 DevToolsActivePort file doesn't exist)
    • [坑 3:部分页面只有简介,结构不统一](#坑 3:部分页面只有简介,结构不统一)
    • [坑 4:Selenium 访问变成"data:"页面,无内容 (反爬虫初级)](#坑 4:Selenium 访问变成“data:”页面,无内容 (反爬虫初级))
    • [坑 5:Requests 拦截与 Client Challenge (反爬虫高级)](#坑 5:Requests 拦截与 Client Challenge (反爬虫高级))
  • 最终实现逻辑
  • 经验总结
  • 源码分享

项目概述

  • 从 Doraemon Fandom Wiki 网站中的 哆啦A梦漫画道具列表网页中(哆啦A梦漫画道具列表)批量抓取哆啦A梦漫画道具的"道具介绍"和"故事内容",并保存为本地 TXT 文件。

  • 前置工作:

    • 哆啦A梦漫画道具列表网站的查看网页源代码中可以看到所有的道具名称与链接,通过 Python 代码自动提取这部分信息并写入 txt ,便于后续遍历:
  • 最终技术栈
    • requests:用于轻量级静态请求。
    • selenium:用于处理动态渲染(已被替换,详见坑4)。
    • undetected-chromedriver:最终核心驱动,用于绕过反爬虫检测。
    • beautifulsoup4:用于解析 HTML。
    • opencc:用于繁简体转换。

踩坑记录与解决方案

坑 1:手动能看到内容,代码爬取全为空 (动态渲染问题)

现象

使用 requests 获取网页源码时,状态码 200,但提取不到"道具介绍"等关键信息。用户通过浏览器右键"查看源代码"能看到内容。

原因分析

该网站属于动态加载网站。关键内容不是写在 HTML 源码里的,而是通过 JavaScript 动态注入的。requests 只能获取初始 HTML,无法执行 JS。

解决方案

引入 selenium 库,启动真实的 Chrome 浏览器来执行 JS,渲染出最终页面后再抓取。


坑 2:Selenium 启动报错 DevToolsActivePort file doesn't exist

现象

在 Windows 环境下运行 Selenium 报错,Chrome 浏览器闪退,无法启动。

原因分析

这是 Windows 上 Selenium 的一种典型崩溃,通常由于沙箱、GPU 加速或端口冲突导致。

解决方案

ChromeOptions 中添加一系列稳定参数:

python 复制代码
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
options.add_argument('--disable-gpu')
options.add_argument('--remote-debugging-port=9222')

若仍报错,则可在任务管理器中强制关闭所有在运行的 Chrome 浏览器程序。


坑 3:部分页面只有简介,结构不统一

现象

部分道具页面(如"相好伞")有标准的"道具介绍"和"故事内容"章节,但也有部分页面(如"爱的小屋")只有网页 Meta 描述,没有正文结构。

原因分析

Wiki 页面由用户编辑,模板不统一。

解决方案

  1. 多关键字匹配:同时匹配简体("道具介绍")和繁体("道具介紹")。
  2. 兜底逻辑 :如果没有找到正文,尝试提取源码中的 <meta name="description" content="..."> 内容作为备用。

坑 4:Selenium 访问变成"data:"页面,无内容 (反爬虫初级)

现象

通过代码模拟浏览器访问特定页面时,地址栏 URL 瞬间变成 data:,页面全白,无法提取任何内容,但手动打开正常。

原因分析

网站具有反自动化检测机制(可能是 Cloudflare 的初级拦截)。检测到 navigator.webdrivertrue 后,通过 JS 劫持页面,重定向到无效地址。

解决方案
采用混合策略

  • Strategy A (Requests优先) :对于"只有简介"的简单页面,尝试直接用 requests 获取静态源码。静态请求不触发 JS 检测,速度快且稳定。
  • Strategy B (Selenium兜底) :只有在 requests 失败或需要复杂正文时,才启动 Selenium

坑 5:Requests 拦截与 Client Challenge (反爬虫高级)

现象

即使使用了混合策略,Selenium 依然无法获取内容。下载的源码显示标题为 <title>Client Challenge</title>,正文全是 JS 验证代码。

原因分析

网站启用了 Cloudflare 高级保护。它会在返回页面时包含一段 JS 挑战代码,浏览器执行通过后才能加载真内容。Python 的 requests 和普通 Selenium 直接被拦在门外。

解决方案

放弃 requests 和普通 selenium,引入 undetected-chromedriver 库。

该库通过修改 Chrome 底层驱动文件,自动隐藏自动化特征,能够通过 Cloudflare 5秒盾等验证。


最终实现逻辑

为了应对上述所有坑,最终代码采用了以下稳健逻辑:

  1. 初始化 :使用 undetected-chromedriver(配置好 User-Agent 和最大化窗口),并初始化繁简转换器。
  2. 循环处理
    • Step 1 (轻量请求) :尝试用 requests 获取页面 -> 解析 HTML -> 提取 Meta Description。
    • Step 2 (成功则写入) :如果 Step 1 拿到了内容,直接转简体、保存文件、删除列表行。不启动浏览器,极快且安全。
    • Step 3 (失败则重器) :如果 Step 1 失败(遇到 Cloudflare Challenge 或只有正文),启动 undetected-chromedriver
    • Step 4 (浏览器处理):等待 10 秒(过 Cloudflare 验证) -> 提取"道具介绍"和"故事内容" -> 若无正文则再次尝试提取 Meta。
    • Step 5 (收尾):成功则保存并删除行,失败则保留行。
    • Step 6 (延时):避免请求过快被封。

经验总结

  1. 不要迷信一种工具:不要上来就用 Selenium,也不要死磕 Requests。混合策略往往能平衡速度和成功率。
  2. 工具库要与时俱进 :普通 selenium 在反爬虫面前已显吃力,undetected-chromedriver 是目前绕过 Cloudflare 的首选。

源码分享

python 复制代码
import os
import time
import re
import requests
import opencc
import undetected_chromedriver as uc
from bs4 import BeautifulSoup
from selenium.webdriver.chrome.options import Options

# --- 配置部分 ---
INPUT_FILE = "哆啦A梦漫画道具.txt"
OUTPUT_DIR = "哆啦A梦漫画道具"
HEADLESS_MODE = True  # 默认为 False (可见模式),测试通过。如需后台运行改为 True

# 伪装请求头
REQUEST_HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
    'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
}

def sanitize_filename(name):
    return re.sub(r'[\\/*?:"<>|]', "", name).strip()

def get_meta_description_via_bs4(html_text):
    """使用 BeautifulSoup 解析 requests 返回的 HTML 文本"""
    soup = BeautifulSoup(html_text, 'html.parser')
    tag = soup.find('meta', attrs={'name': lambda x: x and x.lower() == 'description'})
    if tag and tag.get('content'):
        return tag.get('content')
    return None

def get_section_content(soup, keyword_simp, keyword_trad):
    """提取 h2 章节"""
    for h2 in soup.find_all('h2'):
        headline_span = h2.find('span', class_='mw-headline')
        if headline_span:
            headline_text = headline_span.get_text(strip=True)
            if keyword_simp in headline_text or keyword_trad in headline_text:
                content_parts = []
                for sibling in h2.find_next_siblings():
                    if sibling.name == 'h2':
                        break
                    if sibling.name in ['p', 'div']:
                        text = sibling.get_text(strip=True)
                        if text and len(text) > 2:
                            content_parts.append(text)
                return "\n\n".join(content_parts) if content_parts else None
    return None

def main():
    if not os.path.exists(INPUT_FILE):
        print(f"错误:未找到文件 {INPUT_FILE}")
        return

    # 1. 检查并创建输出文件夹 (如果不存在)
    if not os.path.exists(OUTPUT_DIR):
        os.makedirs(OUTPUT_DIR)
        print(f"已创建输出文件夹: {OUTPUT_DIR}")
    else:
        print(f"输出文件夹已存在: {OUTPUT_DIR}")

    print("正在初始化繁简转换器...")
    try:
        converter = opencc.OpenCC('t2s')
    except Exception as e:
        print(f"OpenCC 初始化失败: {e}")
        return

    # --- 初始化 Undetected ChromeDriver ---
    print("正在初始化反爬虫浏览器...")
    options = Options()
    options.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36')
    options.add_argument('--start-maximized')
    options.add_argument('--disable-blink-features=AutomationControlled')
    options.add_argument('--no-sandbox')
    options.add_argument('--disable-dev-shm-usage')
    
    if HEADLESS_MODE:
        options.add_argument('--headless')

    driver = None
    try:
        driver = uc.Chrome(options=options)
        driver.execute_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})")
        print("浏览器启动成功。")

        with open(INPUT_FILE, 'r', encoding='utf-8') as f:
            lines = f.readlines()

        remaining_lines = []

        for i, line in enumerate(lines):
            line = line.strip()
            if not line:
                continue

            if ':' in line:
                parts = line.split(':', 1)
            elif ':' in line:
                parts = line.split(':', 1)
            else:
                remaining_lines.append(line)
                continue

            prop_name = parts[0].strip()
            prop_url = parts[1].strip()

            # --- 新增逻辑:检查文件是否已存在 ---
            safe_name = sanitize_filename(prop_name)
            output_file = os.path.join(OUTPUT_DIR, f"{safe_name}.txt")

            if os.path.exists(output_file):
                print(f"[{i+1}/{len(lines)}] [跳过] 文件已存在: {safe_name}.txt")
                # 文件已存在,跳过处理,并且不加入 remaining_lines (即删除源文件中的该行)
                continue
            # -----------------------------------

            print(f"\n正在处理 [{prop_name}] ({i+1}/{len(lines)})...")

            # --- 策略 A: Requests (快速通道) ---
            meta_via_requests = None
            print("  -> [尝试1] 正在使用 Requests 静态抓取...")
            try:
                response = requests.get(prop_url, headers=REQUEST_HEADERS, timeout=10)
                if response.status_code == 200:
                    meta_via_requests = get_meta_description_via_bs4(response.text)
            except:
                pass

            final_text = ""
            has_content = False
            use_selenium = False

            if meta_via_requests:
                print("  -> [成功] Requests 抓取成功,跳过浏览器。")
                final_text = f"【道具介绍】\n{meta_via_requests}\n\n(注:内容来源为网页 Meta 描述)"
                has_content = True
            else:
                # --- 策略 B: Undetected Chrome (强力通道) ---
                print("  -> [尝试2] Requests 无效,正在使用反爬虫浏览器...")
                use_selenium = True
                
                try:
                    driver.get(prop_url)
                    time.sleep(10)

                    if "challenge" in driver.current_url:
                        print("  -> [警告] 遇到 Cloudflare 挑战,再等待 10 秒...")
                        time.sleep(10)

                    soup = BeautifulSoup(driver.page_source, 'html.parser')

                    intro = get_section_content(soup, "道具介绍", "道具介紹")
                    story = get_section_content(soup, "故事内容", "故事內容")

                    if intro or story:
                        if intro:
                            final_text += f"【道具介绍】\n{intro}\n\n"
                        else:
                            final_text += "【道具介绍】\n(未找到该章节)\n\n"
                        
                        if story:
                            final_text += f"【故事内容】\n{story}"
                        else:
                            final_text += "【故事内容】\n(未找到该章节)"
                        has_content = True
                    else:
                        desc = get_meta_description_via_bs4(driver.page_source)
                        if desc:
                            final_text = f"【道具介绍】\n{desc}\n\n(注:内容来源为网页 Meta 描述)"
                            has_content = True
                        else:
                            print("  -> [失败] 浏览器页面也无法获取内容。")

                except Exception as e:
                    print(f"  -> [错误] 浏览器访问出错: {e}")
                    remaining_lines.append(line)
                    continue

            # 保存文件
            if has_content:
                final_text_simplified = converter.convert(final_text)
                
                with open(output_file, 'w', encoding='utf-8') as f_out:
                    f_out.write(final_text_simplified)
                
                print(f"成功!(已转简体) 保存至: {output_file}")
                # 成功则不加入 remaining_lines
            else:
                remaining_lines.append(line)

            if use_selenium:
                time.sleep(2)

        print("\n任务完成,正在更新列表文件...")
        with open(INPUT_FILE, 'w', encoding='utf-8') as f:
            f.write('\n'.join(remaining_lines))
            if remaining_lines:
                f.write('\n')
        print(f"列表已更新,剩余 {len(remaining_lines)} 条未处理。")

    except Exception as e:
        print(f"程序发生严重错误: {e}")
    finally:
        if driver:
            driver.quit()
            print("浏览器已关闭。")

if __name__ == '__main__':
    main()
相关推荐
CappuccinoRose21 小时前
流计算概述
python·flink·流计算·数据流·pyflink
Darkershadow21 小时前
蓝牙学习之unprovision beacon
python·学习·ble
小龙在山东1 天前
基于 plumbum 跨平台执行Shell脚本
python
CCPC不拿奖不改名1 天前
python基础:python语言中的函数与模块+面试习题
开发语言·python·面试·职场和发展·蓝桥杯
毕设源码-朱学姐1 天前
【开题答辩全过程】以 基于Python语言的疫情数据可视化系统为例,包含答辩的问题和答案
开发语言·python·信息可视化
我送炭你添花1 天前
Pelco KBD300A 模拟器:07+2.Python 专题:线程安全与信号槽机制——项目多线程最佳实践
python·自动化·运维开发
小途软件1 天前
ssm607家政公司服务平台的设计与实现+vue
java·人工智能·pytorch·python·深度学习·语言模型
laplace01231 天前
Part3 RAG文档切分
笔记·python·中间件·langchain·rag
dhdjjsjs1 天前
Day59 PythonStudy
python