1. 项目概述与核心应用场景1.1 这类爬虫解决的到底是个什么问题先说结论ctwp_spider 这个项目本质上是一套基于 Selenium 的浏览器自动化方案专门用来处理那些“需要真实浏览器行为才能完成的下载站资源采集”场景。我最初做这个项目是因为手头有一批历史资料散落在某个网盘平台上平台页面是典型的动态渲染结构接口签名复杂而且必须在登录态下才能拿到真实下载地址。用常规的 requests 直接拉页面只能拿到一堆空壳 HTML真正有用的数据都在 JS 执行完之后的 DOM 里。这类网盘有一个通病页面结构不复杂但交互链路长。用户需要经历“登录 → 找到资源 → 进入详情页 → 等待倒计时 → 点击下载 → 弹出真实链接”这一整套流程。而且每个环节都可能插入验证码、行为校验、时间锁等机制。如果只写一个脚本去模拟 HTTP 请求等于要把整个前端交互逻辑和签名算法全部逆向一遍开发成本极高维护成本更高——对方改一次前端你的脚本就废一次。而 Selenium 的思路是“我不跟你玩协议我直接帮你开一个浏览器”。它模拟的是真实用户的完整操作路径把浏览器当成一个人来用。平台再怎么做动态渲染、再怎么做 JS 加密对浏览器自动化来说基本是透明的因为它看到的是什么脚本操作的就是什么。这是 ctwp_spider 能够稳定跑起来的根本原因。1.2 为什么非要用 Selenium 而不是其他方案很多人一听说爬虫就想到 Scrapy 结合 requests确实在大规模纯数据采集场景下Scrapy 的并发和去重能力比 Selenium 强得多。但在网盘这种场景下Scrapy 有几个致命短板第一登录态维持麻烦。网盘系统的 Session 通常绑定浏览器指纹、User-Agent、Cookie 生命周期等多重信息。用 requests 模拟登录不是不行但每次都要处理验证码还要手动维护 Cookie 池一旦过期就要重新走一遍登录流程很不优雅。Selenium 可以天然复用浏览器 Profile登录一次之后把 Cookie 持久化后续启动直接注入省掉很多重复劳动。第二动态渲染问题。现在的主流网盘页面基本都是 Vue 或 React 框架数据接口虽然是异步的但关键的下载链接往往还需要再次携带签名和时间戳去请求且这些签名是在前端计算出来的。用纯 HTTP 请求很难拿全。Selenium 直接加载真实浏览器引擎页面渲染完成后的 DOM 就是最终结果不需要关心前端逻辑怎么实现。第三验证码与行为校验。Selenium 配合真实浏览器内核可以模拟鼠标移动、键盘输入、点击节奏甚至能用真实摄像头或打码平台来做验证码识别。这一点后面我会专门展开讲。当然选择 Selenium 不代表完全不写网络请求。在 ctwp_spider 里我采用的是“混合模式”Selenium 负责所有需要真实浏览器交互的环节比如登录、点击、等待倒计时而一旦拿到了直链文件下载本身我改用 requests 或迅雷等下载器完成这样既能保证交互环节稳定又不浪费 Selenium 在下载大文件时的低效等待。2. 环境准备与关键依赖安装2.1 环境版本组合在动手写代码之前环境版本是最容易踩坑的地方。Selenium 对版本兼容性非常敏感尤其是 Chrome 浏览器和 ChromeDriver 的版本必须严格对应否则启动时直接报 session not created 错误。我目前在实际项目中使用的组合是组件版本说明Python3.103.8 以下不建议某些语法和 typing 特性不受支持Selenium4.154.x 版本 API 变化较大老代码需要改造Chrome120各版本对应需匹配 ChromeDriverChromeDriver与 Chrome 完全一致手工下载或用 WebDriver Manager 自动管理操作系统Windows 10/11Linux 服务器上也能跑但验证码处理更麻烦在 Selenium 4.x 时代官方推荐使用webdriver-manager来自动处理驱动程序版本问题不需要再手动去下载 ChromeDriver。只需要简单配置一下启动时它会自动检测本机 Chrome 版本然后下载对应的驱动到本地缓存。这个工具对新手非常友好。一个常见误区是盲目追求最新版本。我在测试中发现Selenium 4.20 之后某些旧定位策略的默认行为有调整如果你是从 3.x 时期迁移过来的代码可能会出现兼容性问题。所以如果是生产环境长期运行建议锁定版本至少在核心代码跑通后不要轻易升级。2.2 WebDriver 配置要点Selenium 启动浏览器时有几个配置项直接影响爬虫稳定性这里逐一说明。pip install selenium webdriver-manager然后看一段基础配置代码from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager options Options() options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) options.add_argument(--disable-blink-featuresAutomationControlled) options.add_argument(--user-data-dirC:/selenium_profile) # 持久化登录态 options.add_argument(--start-maximized) options.add_argument(--langzh-CN) driver webdriver.Chrome( serviceService(ChromeDriverManager().install()), optionsoptions )参数说明excludeSwitches和disable-blink-features是为了隐藏“浏览器正受自动化控制”的提示。虽然这个提示不影响大多数场景但某些平台的 JS 会检查navigator.webdriver属性一旦发现是自动化环境就拒绝加载资源。user-data-dir是保存浏览器用户数据目录。这个参数非常关键它能让 Cookie、LocalStorage 持久化下次启动直接复用登录态省去二次扫码登录。start-maximized避免某些页面在非标准窗口尺寸下响应式布局导致元素定位偏移。注意user-data-dir指向的目录不要包含中文路径某些版本的 Chrome 在中文目录下会出现用户数据加载失败启动白屏。2.3 工程目录结构一个可维护的爬虫项目代码结构设计要比脚本本身重要得多。我常用的是下面这个布局ctwp_spider/ ├── main.py # 主入口 ├── config.py # 全局配置 ├── core/ │ ├── browser.py # WebDriver 初始化与销毁 │ ├── login.py # 登录流程 │ ├── parser.py # 页面解析 │ ├── downloader.py # 下载调度 │ └── utils.py # 工具函数 ├── data/ │ ├── cookies.json # 登录态快照 │ └── tasks.txt # 待处理链接清单 └── logs/ └── runtime.log # 运行时日志这样拆的好处是登录、解析、下载互相解耦。如果某个环节出了问题可以单独调试不用每次都被整条链路拖累。而且后续如果要扩展成多线程或分布式只要改 downloader 部分即可其他模块可以直接复用。3. 核心实现拆解与代码实战3.1 登录态管理与 Cookie 注入的实现网盘类平台对登录态的管控通常比较严格主要体现在两方面一是 Session 过期时间短二是登录行为本身有验证码保护。因此登录这一环节需要设计成“尽量少触发、触发则自动处理”的模式。我的做法是先检查本地是否有合法的 Cookie 快照如果有就直接注入不发任何请求去试探如果没有则启动手动登录模式。import json import time from selenium.webdriver.common.by import By LOGIN_URL https://example.com/login # 仅示例替换为实际地址 def save_cookies(driver, pathdata/cookies.json): cookies driver.get_cookies() with open(path, w, encodingutf-8) as f: json.dump(cookies, f, ensure_asciiFalse, indent2) def load_cookies(driver, pathdata/cookies.json): try: with open(path, r, encodingutf-8) as f: cookies json.load(f) for cookie in cookies: driver.add_cookie(cookie) return True except FileNotFoundError: return False def manual_login(driver): driver.get(LOGIN_URL) input(请在打开的浏览器中完成登录完成后按回车继续...) save_cookies(driver)有人可能会问为什么不用自动化直接输入账号密码我的经验是现在平台对自动填表的识别非常敏感一旦检测到自动填充行为会直接触发滑块验证码处理起来比手动登录更麻烦。手动登录一次之后无限复用登录态性价比最高。Cookie 注入之后还有几个细节需要注意Cookie 中存在expiry字段的注入时不要手动修改。如果 Cookie 已过期add_cookie 会静默失败需要捕获异常并重新登录。某些平台会把一部分状态放在 LocalStorage 里get_cookies拿不到。此时需要额外执行 JS 来导出 LocalStorage保存到本地下次再注入。代码实现类似def save_local_storage(driver, pathdata/localstorage.json): storage driver.execute_script(return window.localStorage;) with open(path, w, encodingutf-8) as f: json.dump(storage, f, ensure_asciiFalse, indent2) def load_local_storage(driver, pathdata/localstorage.json): with open(path, r, encodingutf-8) as f: storage json.load(f) for key, value in storage.items(): driver.execute_script(fwindow.localStorage.setItem({key}, {value});)3.2 页面解析与下载链接提取页面解析是整个项目中最“碎”的部分。网盘页面通常由多个异步模块拼装而成比如文件列表是一个接口、下载按钮的可用状态是另一个接口、真实链接又是第三个接口。前端框架的组件往往有相同的 class 名直接按 class 定位很容易定位错。我的策略是优先用 XPath 结合文本内容定位并且在定位之前先等待关键元素出现。from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By def wait_and_click(driver, xpath, timeout15): elem WebDriverWait(driver, timeout).until( EC.element_to_be_clickable((By.XPATH, xpath)) ) elem.click() return elem def extract_download_page_links(driver, task_url): driver.get(task_url) # 等待文件列表区域渲染完成 WebDriverWait(driver, 20).until( EC.presence_of_element_located((By.XPATH, //div[contains(class,file-item)])) ) items driver.find_elements(By.XPATH, //div[contains(class,file-item)]//a[contains(href,/s/)]) return [item.get_attribute(href) for item in items]这里有一个非常实用的经验不要用find_elements一次拿完全部元素再遍历因为异步渲染过程中后加载的元素可能还没出现在 DOM 里。更好的方式是循环等待“元素数量不再增长”再开始提取def wait_for_stable_element_count(driver, xpath, expectedNone, timeout30): count -1 start time.time() while time.time() - start timeout: current len(driver.find_elements(By.XPATH, xpath)) if current count and (expected is None or current expected): return current count current time.sleep(1) return count这个方法的逻辑是页面在加载时元素数量会不断变化当连续两次读取到的数量一致时基本可以断定渲染完成了。这比单纯固定 sleep 几秒钟要高效、准确得多。另外我强烈建议对每一类链接的提取都封装成独立的函数入参是 driver出参是结构化数据。不要在一个函数里又做跳转又做解析又做下载那样后期维护就是一场灾难。3.3 下载触发与文件保存拿到真实下载链接之后接下来的问题是怎么把文件稳定地下载到本地。这里有两种方案我分别说明它们的适用场景。方案一直接用 Selenium 点击下载然后等待浏览器自动下载完成。这种方式的优点是简单不需要额外处理链接有效性缺点是下载大文件时 WebDriver 进程会一直阻塞而且浏览器默认的下载行为会受弹窗、保存路径、多文件重名等因素干扰。方案二把链接提取出来后交给 requests 或者其他下载工具。我的做法是import requests def download_file(url, local_path, proxiesNone): headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: url.split(/)[0] // url.split(/)[2], } resp requests.get(url, headersheaders, streamTrue, timeout30) total int(resp.headers.get(Content-Length, 0)) done 0 with open(local_path, wb) as f: for chunk in resp.iter_content(chunk_size8192): if chunk: f.write(chunk) done len(chunk) # 这里可以打印进度 return local_path这里面有个容易被忽略的细节Referer 字段。很多网盘的下载直链做了 Referer 校验如果你直接用 requests 下载而不带 Referer服务器会返回 403。这个 Referer 一般就是资源详情页的 URL而不一定是下载链接本身的域名所以要自己维护好来源页面地址。如果文件特别大几个 GB 甚至几十 GBSelenium 的方案就不太合适了。这种情况下建议只把直链输出到文件然后交给成熟的下载工具去跑。ctwp_spider 中我在下载环节做了一个判断文件小于 500MB 用 requests大于 500MB 则输出链接到download_tasks.txt由外部工具处理。这样既保证小资源自动完成又不耽误大资源并行下载。3.4 反爬应对与操作节流网盘平台对自动化操作的容忍度很低但它主要检测的是“操作行为模式”而不是单纯的请求频率。我踩过很多坑之后总结了三个核心点第一操作间隔必须带随机性。固定 sleep 2 秒这种写法在平台看来就是异常信号。正确的做法是生成一个均匀分布范围内的随机延时。import random def random_sleep(mean1.5, spread0.8): time.sleep(max(0, random.uniform(mean - spread, mean spread)))当然这个延时不能太夸张否则爬取效率会变得非常低。我通常把均值控制在 1~2 秒之间具体视单个任务页面的加载速度动态调整。第二不要频繁刷新同一页面。如果某个任务解析失败先重试一次再失败就记录到失败队列等整个任务跑完之后统一回退而不是原地死循环重试。否则很容易触发访问频率限制导致 IP 或账号被临时封禁。第三滚动加载要模仿真实用户。网盘的资源列表如果是无限滚动模式你不能一次性execute_script(window.scrollTo(0, document.body.scrollHeight))滚到底。正确做法是分多次滚动每次滚一屏中间加随机延时并且偶尔上下滚动一下模拟人在浏览内容。def human_scroll(driver, target_heightNone): current 0 viewport driver.execute_script(return window.innerHeight) scroll_step int(viewport * 0.7) if target_height is None: target_height driver.execute_script(return document.body.scrollHeight) while current target_height: current scroll_step driver.execute_script(fwindow.scrollTo(0, {current});) random_sleep(0.8, 0.4) # 模拟偶尔回看 if random.random() 0.2: driver.execute_script(fwindow.scrollTo(0, {current - scroll_step * 0.6});) random_sleep(0.5, 0.3)这种模拟方式在交互层面更接近真人。虽然没法彻底规避所有检测但可以把触发风险降到最低。4. 常见问题与排查实录4.1 ElementNotInteractableException元素看得见但点不了这是 Selenium 使用过程中最经典的问题。报错的原因通常是元素被遮挡、元素不可编辑或元素尚未完成渲染。在网盘场景里最常见的就是“下载按钮”在倒计时期间不可点击或者被一个半透明遮罩层挡住。排查思路先看元素是否可见再看元素位置是否有遮挡。elem driver.find_element(By.XPATH, //button[contains(text(),下载)]) # 判断是否可见 print(elem.is_displayed(), elem.is_enabled()) # 查看元素位置和尺寸 print(elem.location, elem.size) # 如果有遮挡可尝试强制点击 driver.execute_script(arguments[0].click();, elem)我个人倾向于用element_to_be_clickable配合显式等待来解决这类问题因为强制点击虽然能绕过遮挡但也会绕过一些必要的交互约束导致后续环节状态不同步。比如倒计时还没结束就点击平台会拒绝生成下载链接。4.2 StaleElementReferenceException上一秒还在下一秒就失效页面在异步刷新或跳转后之前拿到的元素引用已经不再挂在 DOM 上再操作就会报这个异常。这在网盘页面里很常见尤其是从文件列表页跳转到详情页的过程中列表页的元素全部被销毁。解决办法是“用时再查”即每次操作前重新查找元素而不是复用之前的引用def safe_click(driver, xpath, retries3): for i in range(retries): try: elem WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.XPATH, xpath)) ) elem.click() return True except Exception as e: if i retries - 1: raise e random_sleep(1, 0.5) return False另一个有用的小技巧在页面跳转之前记录当前窗口句柄跳转完成后确认句柄是否变化如果没变化但报 StaleElement说明是同一页面内的组件刷新可以继续用原有逻辑处理如果句柄变了则需要重新初始化上下文。4.3 验证码识别的取舍网盘平台的验证码类型一般是两种滑块和图形点选。滑块验证码在 Selenium 场景下的处理思路是拖拽到缺口位置。from selenium.webdriver.common.action_chains import ActionChains def slide_to_captcha(driver): slider driver.find_element(By.XPATH, //div[contains(class,slide-btn)]) # 先算出缺口位置这里简化处理 target_x 180 ActionChains(driver).click_and_hold(slider).pause(0.2).move_by_offset(target_x, 0).pause(0.3).release().perform()但这个方案成功率不算高因为现在的滑块验证码已经加入了轨迹检测纯匀速拖动反而会被判定为机器操作。我一般采取保守策略优先走手动登录流程即第一次登录时人工完成验证后续靠 Cookie 维持。只有当 Cookie 失效且无法手动介入时才尝试自动化滑块方案。图形点选验证码更复杂需要识别文字和图片坐标这个一般建议接入第三方打码平台。不过打码平台是一把双刃剑用得好省时间用得不好账号可能被标记异常。我的观点是能用手动就别用自动能用 Cookie 复用就别频繁触发验证。4.4 headless 模式与反爬检测很多人喜欢用 headless无头模式跑爬虫认为这样更节省资源、更隐蔽。但在网盘场景下headless 模式的劣势非常明显某些平台会检查navigator.webdriver、window.chrome等对象headless 环境更容易暴露。无头模式下 canvas 指纹和 WebGL 渲染结果与真实浏览器不同可能触发风控。调试困难页面渲染成什么样完全看不到。我的建议是除非你明确知道目标平台不检测这些指标否则日常开发和调试用有头模式等到稳定了再去考虑 headless。如果服务器上没有显示器可以用 Xvfb 虚拟屏幕这样既能保持有头模式的渲染能力又不需要真实物理屏幕。4.5 常见问题与排查速查表报错信息出现原因处理方案session not createdChromeDriver 与 Chrome 版本不匹配升级/降级 ChromeDriver用 webdriver-manager 自动管理ElementClickInterceptedException目标元素被其他元素遮挡显式等待元素可点击或滚动到元素可见区域后再点击TimeoutException页面加载或元素出现超时调整定位条件、延长 timeout或检查是否触发了验证码NoSuchElementException元素未找到确认 XPath 是否正确确认页面是否已切换 iframeConnectionResetError网络被重置或触发风控降低操作频率更换用户代理切换网络出口设备下载链接 403Referer 校验未通过补全请求头中的 Referer 为详情页地址5. 实用经验与进阶扩展建议5.1 从爬虫框架角度重写ctwp_spider 跑通之后如果你想把它变成更通用的工具不能只满足于“能跑”。我后来做了一次大规模重构把核心逻辑抽象成三个基类任务基类、解析器基类和下载器基类。任务基类负责管理 URL 队列和去重解析器基类只提供一个parse(driver) - list[dict]的接口具体页面怎么解析由子类实现下载器基类则统一管理保存路径和重试机制。这样改造的好处是可扩展性。同样的框架可以接入其他网盘平台只要新增一个解析器子类把前端选择器和交互流程写好剩下的任务调度和下载逻辑完全复用。另外如果你对并发有要求可以引入多线程来处理下载任务。但注意Selenium 的 WebDriver 实例不是线程安全的多个线程不能共用一个 driver。正确做法是每个线程创建独立的 driver同时把 WebDriver 的初始化收敛到一个连接池管理器中控制总并发数。from concurrent.futures import ThreadPoolExecutor def worker(task_url): driver create_driver() # 每个线程独立 driver try: links extract_download_page_links(driver, task_url) return links finally: driver.quit() with ThreadPoolExecutor(max_workers3) as executor: results executor.map(worker, task_urls)并发数并不是越大越好。我实测下来单机 3~4 个并发浏览器实例已经接近系统资源瓶颈开太多反而会因为内存不足导致浏览器崩溃。5.2 运行监控与日志设计爬虫跑久了之后最怕的不是报错而是“没报错但任务也没推进”。为了排查这种情况日志设计必须清晰。我的策略是每个任务记录三条日志开始、成功或失败、耗时。并且在失败时把所有关键信息 dump 到日志文件包括当前页面 URL、操作到哪一步、页面的截图。import logging logger logging.getLogger(ctwp) def task_with_log(func): def wrapper(*args, **kwargs): task_name args[0] logger.info(f[START] {task_name}) try: result func(*args, **kwargs) logger.info(f[SUCCESS] {task_name}) return result except Exception as e: logger.error(f[FAIL] {task_name}, error{e}) raise return wrapper截图是排查问题最直观的方式。在异常处理中加一行driver.save_screenshot(logs/error.png)下次回看时一眼就能定位问题出在哪个页面。另外长期运行的项目还要定期清理日志文件和临时下载文件避免磁盘满了导致整体异常。5.3 合规与边界意识这是必须单独拿出来说的一点。爬虫项目虽然技术上有趣但使用边界一定要清楚。我在做这个项目时的原则是只处理自己拥有或有明确授权的资源链接不批量爬取他人私有内容不对目标平台发起高频请求避免影响平台正常服务。如果你要把类似技术用到其他场景上请先确认目标平台的用户协议和服务条款在合规范围内使用。代码本身是中性的但用的人要有边界感。注意本项目及博文中展示的所有代码均为技术学习与个人正当用途设计的示例请勿用于任何违规采集、盗链、侵犯他人权益的用途。6. 写在最后的几点实在话做了这么久的 Selenium 爬虫项目我最大的感受是真正难的从来不是写代码而是处理那些“说不清道不明”的边界情况。Selenium 赋予了我们操作浏览器的能力但每个平台的页面结构、交互设计和风控策略都不一样没有一套代码能通吃所有场景。ctwp_spider 之所以能稳定跑下来靠的不是某一次漂亮的代码设计而是反复调试中积累起来的细节——哪个环节要等多久、哪个按钮要用什么方式点击、哪些操作要随机化、哪些页面要特殊处理——这些都是文档里不会写的。最后再分享一个小技巧如果遇到某个页面怎么定位都定位不准可以试试在浏览器里手动执行一下元素选择器确认路径没问题后再搬到代码里。很多时候你以为的 XPath 和你实际看到的 HTML 结构根本不是一回事先在 DevTools 里验证一遍能省很多排查时间。这个项目后续还可以扩展的方向很多比如接入消息推送任务完成时通知到你手机上比如做一个简化的 Web 管理界面下发任务、查看日志或者把直链进一步提交给第三方离线下载服务做分发。就看你自己实际需求是什么了。
阅读完成 · 觉得有帮助?