ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Selenium模拟右键另存为:解决requests无法下载的浏览器专属链接

Selenium模拟右键另存为:解决requests无法下载的浏览器专属链接 去年我接过一个内部报表系统的下载需求页面上有几百个附件链接唯一的导出方式就是鼠标右键选择“链接另存为...”。用 Python 的 requests 直接请求那些 URL接口要么返回登录页要么干脆拒绝访问——同一个链接在浏览器里点却没任何问题。当时我也想到了爬虫里常见的 Header 伪装、Cookie 携带等操作但依然拿它没办法。后来我换了个思路既然浏览器能下载就让浏览器自己去下载。于是我把这套流程做成了 Selenium 自动化脚本过程中借助 DeepSeek 帮我补齐了不少关键代码和排查思路。这篇文章会从方案选型讲起把“模拟鼠标右键点击 将链接另存为本地文件”这条完整的自动化链路拆开说说每一步的实现原理和我踩过的坑。如果你也遇到过“链接只能浏览器里保存、代码里拿不到”的问题这篇应该能帮你省下不少时间。1. 这种“链接另存为”需求为什么绕不开浏览器本身1.1 三种情况让你没法直接 requests 下载我最初接到需求时第一反应也是写个 requests 脚本把链接地址挨个请求一遍但很快发现这条路根本走不通。“链接只能在浏览器里右键保存”的背后通常藏着三种情况。第一种是链接根本没有真实的 href。很多前端框架会在a标签上挂一个onclick事件点击后才通过 AJAX 拉取数据、生成 blob 临时地址再用URL.createObjectURL触发下载。这种链接在 DOM 里取到的 href 要么是空字符串要么是#或者是javascript:void(0)用 requests 去请求这种地址当然什么都拿不到。第二种是服务端对下载接口做了较严格的请求校验。比较常见的是校验 Referer、校验 Cookie 里的会话状态甚至校验浏览器的 TLS 指纹和 HTTP 头顺序。requests 构造的请求特征和真实浏览器差得比较远不少防御逻辑可以轻松识别出来。虽然可以通过改 headers、调整请求顺序慢慢试出来但成本很高而且对方一旦更新策略之前的工作就白费了。第三种情况在内部系统里尤其常见下载接口依赖当前浏览器里的会话状态而这个会话是通过网页上的一连串操作建立的比如先勾选几个查询条件、点几个按钮附件链接才会生成。你想用 requests 完整复刻这套交互等于把整个业务逻辑重新实现一遍工作量完全不可控。这三种情况叠加在一起最省事的思路就变成了别跟“让浏览器下载”这条路较劲我们用自动化工具去操作浏览器把用户手工右键、另存为的那套流程原样跑一遍。这样浏览器负责了所有鉴权和逻辑我们只需要关心自动化本身。1.2 三条技术路线的横向对比动手之前我把几条候选路线摆在一起做了个对比避免写到一半发现策略选错。技术路线实现难度稳定性适用范围是否需要系统 UI 自动化requests / pycurl 直接下载低一般静态链接、接口无强校验否Selenium 抓链接后 requests 下载中较高链接可见但接口带校验否Selenium 模拟右键 系统菜单操作高中必须走浏览器 UI 才能保存是第一条路在前面已经验证走不通。第二条路比第一条先进一些Selenium 负责把页面跑起来、登录态维持住然后通过execute_script或者浏览器性能日志拿到真实请求 URL再用 requests 带上浏览器 Cookie 去下载。这个方案在“接口只校验 Cookie”的场景下非常好用。但它依然处理不了那些由 JavaScript 动态生成的 blob 文件因为这类文件没有独立的网络请求地址只有一段浏览器内部的临时对象引用。所以在权衡之后我选了第三个组合Selenium 定位链接并模拟鼠标右键然后转向系统工具去操作浏览器弹出的原生右键菜单以及“另存为”对话框。这套方案实现成本最高但能完整复现人的操作链路面对各种奇怪的校验机制都不会慌。注意如果你的需求只是偶尔下载一两个文件这套重型的自动化方案性价比偏低。判断要不要走到这一步先看文件数量和频率。当数量上到几十上百个自动化的优势才真正体现出来。1.3 我最终选定的技术组合确定路线之后技术栈很快就定型了Python 3.11 Selenium 4.xChrome 浏览器 对应版本的 ChromeDriverActionChains 模拟鼠标右键点击pyautogui 处理右键菜单的键盘导航pywinauto 接管 Windows 的“另存为”对话框坦白说当时我对后面两个环节并没有十足把握尤其是“Windows 系统对话框自动化”这块。网上搜到的资料不多而且大多比较零散。于是我开始换个思路把整个需求拆成一个个小问题逐个去问 DeepSeek。这也是文章标题里“借助 DeepSeek”的来由——在这个项目里它确实帮我缩短了不少从思路到代码的距离。2. 向 DeepSeek 描述需求并得到第一版代码2.1 我会怎么组织第一次提问很多人向 AI 助手求助的时候习惯只丢一句话“帮我写个爬虫”。这样得到的答案往往非常泛。我的习惯是把需求拆成“目标 环境 限制条件 期望输出”四个要素一次性说清楚。当时我发给 DeepSeek 的第一段提示词是这样的我正在用 Python 写一个网络爬虫目标是批量下载某个页面上的附件。这些附件链接比较特殊用 requests 拿不到文件但浏览器里右键点击链接选择“链接另存为...”可以正常保存。我打算用 Selenium 定位链接然后模拟鼠标右键点击再操作浏览器弹出的右键菜单选择“链接另存为...”最后把文件保存到本地指定目录。当前环境是 Windows 11Chrome 最新版Python 3.11已安装 selenium、pyautogui、pywinauto。请帮我写出第一版可运行的脚本重点是模拟右键点击和后续菜单操作的部分。这段描述里包含了三个关键信息为什么不能用 requests、我想走哪条技术路径、我的环境里已经装了哪些库。这三个信息基本决定了 AI 返回的代码靠不靠谱。如果只给一句“帮我写个爬虫”AI 大概率会返回一个 requests BeautifulSoup 的通用示例完全不是我们要的方向。2.2 AI 返回的初始代码DeepSeek 给我的第一版代码骨架是这样import time from selenium import webdriver from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.by import By from selenium.webdriver.chrome.options import Options # 配置浏览器下载目录 download_dir rD:\crawler_downloads options Options() options.add_experimental_option(prefs, { download.default_directory: download_dir, download.prompt_for_download: False, safebrowsing.enabled: True, }) driver webdriver.Chrome(optionsoptions) driver.get(https://example.com/report_list) time.sleep(3) # 定位第一个附件链接 link driver.find_element(By.XPATH, //a[contains(text(),附件)]) # 模拟鼠标右键点击 ActionChains(driver).context_click(link).perform() time.sleep(1) # 下面这一步是 AI 最初建议的写法 # menu_item driver.find_element(By.XPATH, //div[text()链接另存为]) # menu_item.click()整体骨架是可以跑通的浏览器正常启动、页面正常打开、链接能定位到context_click也确实能弹出右键菜单。问题就出在最后一段注释里的写法——AI 以为右键菜单里的“链接另存为”是网页 DOM 的一部分所以在用 Selenium 的 find_element 去定位。2.3 第一版跑起来后的真实状况把这份代码放到真实环境里浏览器确实弹出了右键菜单但 Selenium 在driver.find_element那一步直接抛了NoSuchElementException。我盯着屏幕看菜单明明就在浏览器窗口里怎么会找不到这里就是整条链路第一个关键的认知点Chrome 的原生右键菜单并不是网页 DOM 的一部分而是浏览器外壳单独绘制出来的系统 UI。Selenium 的 WebDriver 协议只能操作网页文档对象模型右键菜单、地址栏下拉列表、书签栏这些区域全部超出了它的能力边界。所以无论你给它什么 XPath 或者 CSS 选择器都不可能定位到菜单里的“链接另存为”。我把这个异常信息原样发给 DeepSeek 后它很快就调整了方向既然 Selenium 够不到菜单那就把后续操作交给 pyautogui用模拟键盘的方式选择菜单项并回车。这个修正方向是对的接下来我就沿着这条路继续向下走。3. 模拟鼠标右键点击从定位元素到操作系统菜单3.1 右键点击的正确姿势Selenium 里模拟右键点击的标准写法是先拿到目标元素再通过ActionChains的context_click触发from selenium.webdriver.common.action_chains import ActionChains link driver.find_element(By.XPATH, //a[contains(text(),附件)]) ActionChains(driver).context_click(link).perform()有几个细节需要特别注意。第一context_click接收的元素不一定非得是带文字的链接如果附件是个下载图标用 CSS 选择器img[altdownload]定位会更稳。第二触发右键前最好确认元素在视口内可见否则页面的懒加载机制会让元素停留在未渲染区域右键事件可能落在空白处。针对长列表页面我习惯先让浏览器把目标元素滚动到窗口中央再触发driver.execute_script(arguments[0].scrollIntoView({block: center});, link) time.sleep(0.5)这一步看着不起眼但实际抓取中作用很大。因为很多附件列表是长滚动页面元素不在视口内时Chrome 虽然能拿到坐标但不一定会响应右键事件或者响应了也定位偏了。3.2 为什么 Selenium 一直碰不了 Chrome 的右键菜单前面提到过一句“原生右键菜单是系统 UI”这里补充一下更本质的原因。Selenium 的设计初衷是模拟用户和网页交互它的协议只定义了如何与浏览器渲染出的 DOM 交互而右键菜单这类由浏览器外壳自绘的原生控件并没有暴露给 WebDriver 协议。所以哪怕你肉眼看到菜单就挂在光标旁边WebDriver 也没有对应的对象模型去索引它。这里有个常见的认知误区。网上很多人问“Selenium 怎么点右键菜单里的某项”总有人回答“用 XPath 定位”要么是没实操过要么是目标网站的右键菜单其实是网页自定义的。现在很多站点会用 JavaScript 屏蔽原生右键菜单自己渲染一个 div 层当菜单那确实可以用 find_element 定位。但本项目的目标是 Chrome 原生菜单就必须换工具。3.3 用键盘导航选中“链接另存为”既然 Selenium 够不到菜单那就用 pyautogui 向系统发送键盘事件。Chrome 有一个特性原生右键菜单弹出后默认会高亮第一个菜单项此时可以用方向键上下移动高亮按回车相当于点击当前项。import pyautogui import time # 触发右键菜单 ActionChains(driver).context_click(link).perform() time.sleep(0.8) # 向下移动到“链接另存为...”菜单项 # 具体次数取决于 Chrome 版本和菜单顺序 pyautogui.press(down, presses4) pyautogui.press(enter)这里有一个必须亲手实测的点方向键的按压次数。不同版本的 Chrome 菜单项目录不完全一致而且右键的对象不同菜单项的顺序也会变。比如右键普通文字、图片、链接时菜单项的前几项完全不同。我的做法是先手动在目标页面上右键一次数清楚“链接另存为...”排在第几个再把这个数字写进配置文件。不要写死在代码里这样 Chrome 升级导致菜单顺序变化时改一下配置就能恢复。提示pyautogui.press(down, presses4)是连续按 4 次方向键。如果目标页面菜单结构不稳定建议拆成“按一下、停一下、再按一下”的循环for _ in range(4): pyautogui.press(down); time.sleep(0.1)。实测下来加了这个缓冲之后焦点很少出现跳过头的情况。3.4 图像识别方案备选的兜底手段键盘导航有个前提菜单项顺序稳定。如果目标系统频繁改版或者脚本需要在多台语言环境不同的机器上运行键盘导航就可能失灵。这时候可以用 pyautogui 的图像识别来兜底。思路是先把“链接另存为...”这个菜单项的文字截图保存为模板图片运行时用locateOnScreen在屏幕搜索匹配位置再让鼠标移动到该坐标点击import pyautogui pos pyautogui.locateOnScreen(menu_save_link.png, confidence0.8) if pos is not None: pyautogui.click(pyautogui.center(pos))这个方案的优点是菜单顺序怎么变都不影响识别只要文字样式没变就能找得到。缺点是依赖图片素材而且高分屏缩放比例不同的机器上confidence 阈值要重新调。我在实际项目里两种方案都实现了默认走键盘导航键盘导航失败一次后自动切到图像识别。先快后稳跑批的时候效果还可以。3.5 一个必踩的坑headless 模式没有右键菜单调试阶段我想省资源尝试过 Chrome 的 headless 无头模式。结果发现无头模式下 Chrome 根本不绘制右键菜单这类原生 UIcontext_click事件发出去没有任何响应后面的键盘操作自然全部落空。这也是为什么这套方案必须有可见的浏览器窗口。如果真的需要无头运行就得换思路第二章提到过的“拿真实链接 requests 下载”方案或者通过浏览器 DevTools 协议监听下载事件。取舍要在方案设计阶段就定下来不要等写完代码再切换否则返工成本很高。4. 最后一百米Windows 另存为对话框的自动化4.1 键盘导航进入系统对话框后的新局面按下回车选中“链接另存为...”之后Chrome 会弹出一个 Windows 原生的“另存为”对话框。这个对话框不属于浏览器 DOM也不属于网页它是操作系统桌面 UI 的一部分。好在 Windows 的对话框结构比浏览器菜单稳定得多用 pywinauto 这样的 UI 自动化库可以直接接管。我第一次看到这个对话框也有一点头疼里面的控件不少文件名输入框、保存类型下拉框、左侧导航面板、底部按钮。后来用 pywinauto 自带的控件树工具打印了一下发现实际需要操作的只有两个一个文件名输入框一个保存按钮。4.2 用 pywinauto 接管系统对话框连接“另存为”对话框的标准写法是from pywinauto import Application # 连接到已经弹出的“另存为”对话框 app Application(backenduia).connect(title_re另存为) dlg app.window(title_re另存为) dlg.wait(ready, timeout10) # 输入文件名 dlg[文件名].set_text(report_2025.pdf) # 点击保存 dlg[保存].click()这里有一个非常容易踩的坑Windows 对话框的控件名和系统语言强相关。我是在中文系统上调试的控件名是“文件名”和“保存”如果脚本部署到英文系统控件名会变成“File name”和“Save”。所以这些控件名字符串也要配置化部署时根据系统语言调整。如果set_text之后文件名没有生效可以改成先点击输入框再键盘输入file_name_input dlg.child_window(title文件名, control_typeEdit) file_name_input.click_input() file_name_input.type_keys(report_2025.pdf)这种写法兼容性更好因为它不依赖输入框是否已经自动获得焦点。4.3 输入文件名与点击保存的稳定性问题对话框这一步的稳定性是整条自动化链路里最需要花功夫的。我实际连续跑了十几轮之后发现不稳定的主要来源是对话框弹出的延迟以及与文件名输入框的交互状态。Chrome 每次弹出另存为对话框时默认文件名通常是主机名加时间戳而且文件名输入框内的文本默认处于全选状态。此时如果直接set_text大多数情况下能正确覆盖但偶尔会出现输入框没有完全获得焦点、新文件名被追加到末尾的情况。我的处理方式是在输入新文件名之前先发一个Ctrl A全选旧文本再输入新内容file_name_input.type_keys(^a) file_name_input.type_keys(report_2025.pdf)此外点击“保存”后文件不会立刻出现大文件可能要等几十秒。所以保存之后一定要等文件完整落地再处理下一个链接。判断下载是否完成的可靠方式是检查同名文件的.crdownload或.part临时后缀是否消失import os import time def wait_for_download(download_dir, filename, timeout120): target os.path.join(download_dir, filename) deadline time.time() timeout while time.time() deadline: tmp_candidates [f for f in os.listdir(download_dir) if f.startswith(filename) and (f.endswith(.crdownload) or f.endswith(.part))] if os.path.exists(target) and not tmp_candidates: return True time.sleep(1) return False这比固定 sleep 要好很多网速慢的时候多等一会儿网速快的时候立刻进入下一个文件整套流程的自适应能力会强不少。4.4 取文件名最容易翻车的中文路径与特殊字符Windows 的文件名规则和网页链接文本是两套体系。链接文字可能是“年度报表(2024).pdf”但实际下载文件名可能是别的也可能链接文字里带/、\、:这些在 Windows 文件名里非法的字符。直接用链接文本拼文件名保存时对话框会截断或弹错误提示脚本容易卡住。解决方法是加一个文件名清洗函数把所有非法字符替换掉import re def clean_filename(name): # 去除 Windows 文件名中的非法字符 name re.sub(r[\\/:*?|], _, name) return name.strip()我实际项目里遇到过链接文本带中文括号、全角空格、英文括号混合的情况。清洗之后文件名和原文不完全一样但至少不会报错下载链路能顺畅跑完。如果你对文件名的准确性要求高可以在清洗规则里维护一张映射表。4.5 完整闭环右键点击、菜单选择、对话框保存的整合代码到这里链路里的三个环节都打通了。我整合成一个函数方便后续直接调用def download_by_rightclick(driver, link_element, save_name, download_dir): # 1. 滚动到可见区域 driver.execute_script(arguments[0].scrollIntoView({block: center});, link_element) time.sleep(0.5) # 2. 模拟鼠标右键 ActionChains(driver).context_click(link_element).perform() time.sleep(0.8) # 3. 键盘选择“链接另存为...”菜单项 for _ in range(DL_DOWN_PRESSES): pyautogui.press(down) time.sleep(0.1) pyautogui.press(enter) # 4. 接管另存为对话框 try: app Application(backenduia).connect(title_re另存为, timeout10) dlg app.window(title_re另存为) dlg.wait(ready, timeout10) file_input dlg.child_window(title文件名, control_typeEdit) file_input.click_input() file_input.type_keys(^a) file_input.type_keys(clean_filename(save_name)) dlg[保存].click() except Exception as e: print(f对话框操作失败: {e}) raise # 5. 等待文件落地 return wait_for_download(download_dir, clean_filename(save_name))这段代码拿到真实环境里配合页面元素遍历就能一个接一个地批量下载。整段流程里最需要调优的无非是三个地方等待阈值、方向键次数、对话框控件名。把这些都设计成外部参数之后第一次跑通后几乎不需要再动代码。5. DeepSeek 排错实录AI 建议不总是对的5.1 三个来自 AI 的“坑”这篇笔记反复提到借助 DeepSeek但说句实话AI 给的建议也不是每条都能直接用。整个过程里我实际踩掉了三个由 AI 引入的坑写出来给大家当反面教材。第一个坑在第二章已经出现过AI 把“右键菜单项”当成了网页 DOM 元素。它给出的定位代码在真实环境里直接抛NoSuchElementException。原因前面分析过AI 在没看到真实运行结果时不知道 Chrome 原生右键菜单是系统 UI会默认按网页元素去处理。第二个坑是 AI 建议用driver.switch_to.window()去切到“另存为”对话框。它以为弹窗是一个浏览器标签页或者 JavaScript 确认框可以用 WebDriver 的窗口句柄切过去。但 Windows 原生对话框根本不在 WebDriver 的窗口枚举范围内这个操作会直接抛NoSuchWindowException。正确的做法是交给 pywinauto 去接管而不是在 Selenium 里切窗口。第三个坑是文件名去重逻辑。AI 最初生成的代码用os.path.join(download_dir, save_name)判断目标文件逻辑本身没错但它没有考虑 Chrome 在目标文件已存在时会自动在文件名末尾加(1)、(2)这类后缀。结果就是同名文件被 Chrome 自动重命名而脚本里的wait_for_download永远等不到预设文件名判定超时。解决方式有两种下载前先清理同名文件或者在轮询时同时检查带“ (1)”后缀的文件。这三个坑的共性是同一个AI 是在没有运行环境的前提下进行推理它只能根据通用规律生成代码。那些必须在真实浏览器里跑一遍才能暴露的差异AI 无法预判。所以我后来的协作方式是AI 负责快速产出框架和方案我负责在真实浏览器里验证每一个环节的可行性再带着结果回去和 AI 继续迭代。5.2 分步提问比一次性问完更有效使用 DeepSeek 一遍后我逐渐形成了一套协作节奏把大需求拆成几个小问题逐个问而不是一次抛一个长问题。拆小之后每个问题里的限制条件更清晰AI 不需要在同一个回答里兼顾所有分支生成的代码也更聚焦。以这个项目为例我实际上是分五轮问的第一轮问整体架构拿到代码骨架和依赖清单第二轮专门问右键菜单无法定位的问题把完整报错贴给 AI第三轮问 Windows 另存为对话框怎么自动化让 AI 给出 pywinauto 示例第四轮让 AI 通读完整代码找等待问题和文件重名问题第五轮让 AI 帮我把脚本参数化做成配置驱动方便切换环境。每一轮提问我都会把上一轮的实际运行结果补充进去。这种有来有回的协作方式比我当初一次性丢一个大需求过去高效得多。5.3 这套流程的稳定性与后续扩展方向脚本最终跑完了批量下载几百个文件的处理从一天人工操作压缩成了十几分钟自动运行。在菜单顺序固定的内网系统上成功率很稳定偶发的失败也基本是网络波动重试一次就能过去。如果后续继续扩展我建议从两个方向入手。一是引入失败任务队列把下载失败的文件记录到 JSON 里下次启动自动续传二是复用浏览器会话避免每个文件都重新打开页面。再进一步可以监听 Chrome 的 DevTools 网络事件拿到真实下载 URL 后直接绕过右键菜单和系统对话框——这条路更稳但前提是链接确实有对应的网络请求适合在现有框架稳定之后再作为进阶方案尝试。最后分享一个小技巧如果你也在爬虫项目里遇到“不得不走浏览器 UI”的场景先别急着写自动化代码。打开浏览器开发者工具切到 Network 面板手动右键下载一次文件观察那条下载请求里的关键参数。很多时候你会发现右键菜单只是表象背后不过是一个带签名参数的 GET 请求——把签名参数提取出来requests 就能直接下载根本不用模拟鼠标。模拟 UI 的优先级永远排在“能从请求层解决”的后面。
返回列表