多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Python + Selenium + webdriver-manager 网页自动化截图实战

Python + Selenium + webdriver-manager 网页自动化截图实战 做网页自动化的人早晚都会遇到一个需求把网页当前的样子变成一张图片留着存档、做巡检、发报告或者仅仅是“眼见为实”。以前我接到这类需求第一反应是requests拿页面源码但很多页面是动态渲染出来的拿到的只是没填充数据的空壳。后来切到selenium配上webdriver-manager自动匹配浏览器驱动一套截图脚本几分钟就能跑通省掉了手动下载驱动、还要对版本号的麻烦。这篇文章把完整的实现过程、关键参数、为什么这样选型和踩过的坑都整理出来。适合刚入门 Python、想给网页做自动化截图的朋友参考也适合测试岗的同学拿去改造成 UI 巡检工具。不需要你有很深的浏览器内核知识只要会基本的 Python 语法就够了。1. 网页截图工具的技术选型为什么是 selenium webdriver-manager1.1 截图工具的常见实现路线做网页截图市面上其实有好几条路可以走。最朴素的是直接用requests拉 HTML配合html2image之类的东西转换但这条路对现代网页基本走不通——现在的前端页面大量依赖 JavaScript 动态渲染数据直接拿回来的 HTML 里往往只有骨架没有真实的商品价格、图表数据、登录后内容截出来的图跟用户看到的是两回事。另一条路是 Playwright微软出的自动化框架API 设计比 Selenium 现代很多自带截图 API还能自动管理浏览器。我用过一段时间确实好用但如果你身边同事、网上资料大多用的是 Selenium出问题时能搜到的答案反而更多。还有 Node 生态里的 Puppeteer也不错但要为了截图去搭一套 Node 环境对 Python 开发来说不划算。Selenium 的最大优势是生态成熟。它是 WebDriver 规范的老牌实现资料多、案例多、各种奇奇怪怪的报错几乎都有人踩过。配合 Python 使用代码写起来也简单启动浏览器、访问 URL、调用截图方法三步就完事。它就像给浏览器装了一个遥控器通过 WebDriver 协议向浏览器下发指令我们不用关心底层是怎么通过 DevTools 通信的只要知道“调这个方法 按了遥控器上的截图键”就行。1.2 webdriver-manager 到底解决了什么问题用 Selenium 有个绕不开的麻烦它需要浏览器驱动。Chrome 要用 chromedriverFirefox 要用 geckodriver这些驱动负责在测试代码和浏览器之间翻译指令。问题在于驱动和浏览器版本是严格对应的——Chrome 大版本更新了旧驱动就不能用了你得手动去 Chrome for Testing 官网找对应版本下载、解压、放到某目录、写进环境变量或者代码里写死路径。WebDriver-Manager 干的事就是把这坨手动操作替换掉。它在代码里自动检测你本机 Chrome 的版本号然后到对应源下载匹配的 chromedriver缓存到用户目录再把这个缓存路径交回给 Selenium全程不需要你关心驱动下载到了哪里。可以这么理解驱动是浏览器和 Selenium 之间的“翻译官”版本不对就像翻译官拿着过时的词典谈话双方明明是同一个意思他却翻译得牛头不对马嘴。Manual 下载就是每次都要亲自去给翻译官找新词典webdriver-manager 则是雇了个助理让他全权负责词典的更新和携带。这个库还支持 Chrome、Firefox、Edge 等多类浏览器API 也统一比如 Firefox 就是把ChromeDriverManager换成GeckoDriverManager其余逻辑基本不变。这就意味着你的截图脚本想换个浏览器内核做对比改动成本非常低。1.3 为什么驱动版本必须和浏览器版本匹配这背后其实是浏览器与 WebDriver 之间通过 CDPChrome DevTools ProtocolChrome 浏览器调试协议通信的机制。Selenium 发指令给驱动驱动再通过 CDP 和浏览器实例对话。浏览器每次更新都会调整内部实现旧版驱动如果不知道新版的命令格式就会回报 unknow error。WebDriver 协议本身是 W3C 标准设计上是向后兼容的但浏览器的快速迭代导致驱动必须跟着发版。所以你会看到官方文档里永远强调“确保 chromedriver 和 Chrome 主版本号一致”。版本对应关系建议永远以官方维护的表格为准不要凭记忆猜。Chrome 是 120 版本驱动就用 120.x这种“主版本对齐”基本能保证稳定运行。2. 环境准备版本关系搞不懂后面全是坑2.1 先想清楚 Python 版本问题这个项目对 Python 版本不算挑剔Python 3.8 以上基本都能跑。我自己的习惯是装 Python 3.10 或 3.11不是因为它功能新而是因为 Selenium、WebDriver-Manager 这些库在新版本上的测试最充分遇到问题处理起来也容易。环境方面强烈建议用虚拟环境venv不要直接往全局 Python 里装。原因很现实每个项目依赖的库版本不一样今天这个项目要 Selenium 4.x明天那个项目要 3.x全局环境没法同时满足。虚拟环境就像给每个项目单独开一个工作舱依赖互不干扰想删就删不会一不小心把公司线上项目需要的库版本给覆盖掉。创建命令很简单python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate看到命令行前面出现了(venv)就说明环境激活成功。接着检查一下 Python 版本确认没问题再继续装依赖。2.2 三行命令装完依赖但有个版本对应要先搞懂依赖其实就两个核心库一条命令搞定pip install selenium webdriver-manager国内网络环境下如果下载速度慢或者超时可以加-i https://pypi.tuna.tsinghua.edu.cn/simple指定清华源。装完之后可以验证一下版本号pip show selenium webdriver-manager版本不是越高越好但也不要用手动下载的老版本。Selenium 4.x 及以上是现在的主流API 比 3.x 清晰不少尤其是 Service 对象的引入让驱动的指定方式变得更严谨。同时确认你电脑里装了 Chrome 浏览器。这点看起来废话但真有人会在没装浏览器的服务器上跑这类代码然后报错cannot find Chrome binary。Chrome 的版本号可以在浏览器地址栏输入chrome://version查看我们需要的是第一位数字比如 126.0.6478.126就记 126。虽然 webdriver-manager 会自动检测但你自己心里有数排查问题时更容易判断报错是因为版本不匹配还是网络问题。这里多提一句Selenium 4 和 3 在启动驱动的方式上有差异。旧写法webdriver.Chrome(executable_path...)在 4.x 中已经被标记为废弃新写法是通过Service指定驱动路径。关于这一点下面的代码会体现出来建议新写的项目直接按新 API 来。3. 核心代码实现从打开浏览器到保存截图3.1 最小可用的完整截图脚本下面是一份可以直接运行的代码注释写得比较全。它做的事情就是打开 Chrome 无头浏览器访问一个页面等待几秒截全屏图然后退出import os import time from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager # 配置 Chrome 启动参数 options webdriver.ChromeOptions() options.add_argument(--headlessnew) # 无头模式不弹出浏览器窗口 options.add_argument(--no-sandbox) # 部分 Linux 环境下必须加 options.add_argument(--disable-gpu) # 服务器环境减少 GPU 兼容问题 options.add_argument(--window-size1920,1080) # 设置窗口/截图分辨率 # 自动获取与本地 Chrome 匹配的 chromedriver service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice, optionsoptions) try: url https://example.com driver.get(url) # 等待页面加载完成直接 sleep 是最简单但不优雅的做法 time.sleep(3) # 保存截图返回 True 表示成功 result driver.save_screenshot(screenshot.png) if not result: print(截图失败检查页面是否正常打开) else: print(截图已保存尺寸为, driver.get_window_size()) finally: driver.quit()如果你只是想在本地试一试这个代码足够了。ChromeDriverManager().install()首次运行时会下载驱动之后命中缓存就直接复用第二次开始启动速度会快很多。但要注意time.sleep(3)是写死的等待时间如果页面资源多、加载慢3 秒很可能不够截出来是一半内容甚至白屏。更稳妥的方式是用显式等待后面我会单独讲。3.2 ChromeOptions 里的参数到底有什么讲究--headlessnew是核心参数。它让浏览器在后台运行不弹出任何界面特别适合部署在服务器上或者你在本地写脚本的同时还想继续用浏览器干别的活。旧的--headless已经被标记为过时新版 Chrome 推荐--headlessnew它对 HTML 的渲染逻辑跟有头模式差别更小截出来的图也更接近真实用户所见。--no-sandbox在本地 macOS/Windows 上通常不是必须的Linux 服务器上缺了它经常报权限错误。这是 Chrome 的沙箱隔离机制和服务器默认用户权限冲突导致的加上这个参数就是在告诉浏览器“别开沙箱了我信任这个环境”。个人本机调试可以不加线上部署最好加上。--disable-gpu是为了规避服务器上没有 GPU 导致的黑屏、花屏问题。你本地开发机有显卡这个问题不明显但在云服务器上很常见。加上之后强制使用软件渲染稳定性更高截图内容不会缺图层。--window-size决定了截图的分辨率。这句话很关键无头模式下浏览器窗口大小往往默认只有 800x600你不设置这个参数截出来的图就是又窄又矮的。你要的是像用户用 1920 宽度屏幕看到的效果就得把窗口设成对应尺寸。这也是新手最常忽略的参数没有之一。另外这两个参数也建议按需添加options.add_argument(--disable-extensions) # 禁用浏览器扩展避免干扰 options.add_argument(--blink-settingsimagesEnabledtrue) # 开启图片加载后者默认就是开启的但如果你之前做过页面性能优化、把图片加载关掉过就会影响截图内容显式加上更稳妥。3.3 显式等待比 sleep 更靠谱的等待方式time.sleep最大的问题就是拍脑袋定时间。网络慢的时候不够用网络快的时候浪费时间。Selenium 提供了两种官方等待机制from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By # 最多等待 10 秒直到页面上出现 id 为 “content” 的元素 WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, content)) )这段代码的意思是轮询页面每 0.5 秒检查一次指定元素是否出现超过 10 秒就抛异常。它的好处是页面 2 秒加载完就 2 秒继续5 秒加载完就 5 秒继续不会因为固定等待而白白浪费时间。截图场景里最常用的几个条件是presence_of_element_located元素出现、visibility_of_element_located元素可见、element_to_be_clickable元素可点击。具体用哪个取决于你的目标页面。比如截一个商品详情页至少要等“价格”元素渲染出来才能保证图里有核心信息截一个登录后的后台面板就要等某个图表容器出现。如果没有合适的元素条件退而求其次等待document.readyState complete也行但注意它只表示文档结构加载完不保证动态数据填充完毕。3.4 从单页截图进化到批量截图有了基础版本下一个需求通常就是批量截图给一串 URL逐个打开、逐个截图、存成不同的文件。批量场景下有个关键认知如果 URL 都是同一个网站的页面可以只启动一个浏览器实例连续访问不需要每个页面都重新启动一次浏览器那样既慢又容易触发风控。import os import time from datetime import datetime from selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from webdriver_manager.chrome import ChromeDriverManager urls load_urls() # 你的 URL 列表 options webdriver.ChromeOptions() options.add_argument(--headlessnew) options.add_argument(--window-size1920,1080) options.add_argument(--no-sandbox) options.add_argument(--disable-gpu) service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice, optionsoptions) # 按日期建目录避免文件堆积 out_dir datetime.now().strftime(shots_%Y%m%d) os.makedirs(out_dir, exist_okTrue) for idx, url in enumerate(urls, start1): try: driver.get(url) # 等页面标题出现说明已经跳转完成 WebDriverWait(driver, 10).until(EC.title_contains()) time.sleep(2) # 留点缓冲时间 filename os.path.join(out_dir, f{idx:03d}_{url.replace(https://, ).replace(/, _)}.png) driver.save_screenshot(filename) print(f[{idx}] 已保存: {filename}) except Exception as e: print(f[{idx}] 截图失败: {url} - {e}) driver.quit()这个版本里我加了几个实用细节按日期建目录避免多次运行文件堆在一起。文件用序号加域名组合命名方便追溯。每一条都包了 try/except单个页面失败不会中断整批任务。driver.quit()放在最后确保循环结束释放浏览器进程。有一点需要提醒如果目标网站有反爬机制高频连续截图容易被限制。稍微懂礼貌一点的做法是每次截图之间加随机延时比如time.sleep(2 random.uniform(0.5, 2))不要完全固定间隔。4. 实测过程中的参数调优4.1 不同页面的截图表现全屏截图与长页面问题我在实际测试中发现save_screenshot()截取的是“当前视口”的内容但对于比较长的页面它并不会自动把整个页面拼起来。你设置--window-size1920,1080它截的就是 1920x1080 那么大一块页面内容超过一屏下面的部分就漏掉了。为什么因为save_screenshot的底层行为就是截取浏览器可视区域而浏览器在无头模式下就认为你的屏幕只有那么大。Chrome 支持一个--force-device-scale-factor参数影响缩放还有一个思路是先把窗口高度设置为页面总高度再截图。Selenium 官方还提供了一个add_cdp_capabilities的方式可以直接调用 Chrome DevTools Protocol 的Page.captureScreenshot接口来截全图。这个方法更底层但代码量多一些适合需要高精度的场景。如果不想引入复杂逻辑很多场景下直接把--window-size的纵向开大比如--window-size1920,3000也能覆盖大多数页面。缺点是你无法事先知道页面多长要么调大到一个足够用的值要么动态获取文档高度再重设窗口。4.2 动态计算页面高度并调整窗口这里给出一个实际项目里更优雅的解法。核心思路是先用一个小窗口打开页面通过 JavaScript 获取到页面完整的滚动高度再重新设置窗口大小然后截图# 打开页面 driver.get(url) # 获取页面完整高度 scroll_height driver.execute_script(return document.body.scrollHeight) window_height driver.execute_script(return window.innerHeight) # 如果页面高度大于当前窗口高度就重新设置窗口大小 if scroll_height window_height: driver.set_window_size(1920, scroll_height) time.sleep(1) # 等待布局稳定 driver.save_screenshot(full_page.png)这个方案能解决大多数长页面截不全的问题。但要注意有些页面通过懒加载机制实现“滚动到什么位置就加载什么内容”比如图片是在滚动到可视区域附近才加载的那么单纯把窗口调高、不触发滚动事件图片依然不会加载出来截出来的图底部是空白的。针对懒加载页面更可靠的做法是用程序模拟滚动到底部、等待图片加载、再滚回顶部然后再截图。简单实现如下# 模拟滚动加载 previous_height 0 while True: driver.execute_script(window.scrollTo(0, document.body.scrollHeight)) time.sleep(2) # 等待懒加载内容出现 new_height driver.execute_script(return document.body.scrollHeight) if new_height previous_height: break previous_height new_height # 回到顶部并截图 driver.execute_script(window.scrollTo(0, 0)) time.sleep(1) driver.set_window_size(1920, driver.execute_script(return document.body.scrollHeight)) driver.save_screenshot(full_page_loaded.png)这个循环的终止条件就是“页面高度不再变化”说明滚到底也没有新内容加载了。实话说这种技巧对内容特别多的网站不一定完美但由于不用引入额外定位体验已经比直接截图好很多。4.3 图片保存格式与路径的选择Selenium 的save_screenshot只支持 PNG 格式这是它的限制。如果你需要 JPEG 或者 WebP就得自己加一步图像转换。通常做法是先用driver.get_screenshot_as_png()拿到二进制的 PNG 数据再用 Pillow 库转成别的格式from PIL import Image import io png_data driver.get_screenshot_as_png() img Image.open(io.BytesIO(png_data)) img.convert(RGB).save(screenshot.jpg, quality85)这里有两个细节值得提。第一如果页面背景是透明的少数页面会这样转成 JPEG 时背景会变黑最好还是用 PNG第二质量参数quality85能兼顾文件大小和清晰度如果你只是存档用80 到 85 是一个不错的区间肉眼基本看不出差别文件体积能小不少。文件路径的坑我也踩过几次。Windows 下文件名里不能包含英文冒号、反斜杠等特殊字符所以用 URL 直接做文件名前最好把特殊字符替换掉。上面的代码里用replace(/, _)但还是建议再做一次清洗保守一点safe_name url.replace(https://, ).replace(http://, ) safe_name .join(c if c.isalnum() or c in ._- else _ for c in safe_name)这句把除了字母数字和._-之外的字符全部替换成下划线避免各种隐藏的编码问题。5. 常见问题与排查技巧实录5.1 排错速查表我用这个方案写了不下十个自动化脚本把最常遇到的报错整理成一份速查表方便大家直接对照着看。每次程序跑不通的时候先看报错文本属于哪一类而不是急着改代码。报错信息关键词可能原因解决办法Fail to download chromedriver网络无法访问驱动下载源或源不稳定换镜像源或者手动下载驱动后通过Service指定路径SessionNotCreatedException: This version of ChromeDriver only supports Chrome version X浏览器和驱动版本失配清理 webdriver-manager 缓存或者升级 webdriver-manager 到最新版WebDriverException: unknown error: cannot find Chrome binary系统里没有安装 Chrome或路径不是默认路径安装 Chrome或者用options.binary_location指定浏览器路径截图结果全白/只有背景色页面资源加载太慢等待时间不足改用显式等待或者增加time.sleep缓冲截图尺寸不对比如只有 800x600无头模式下没有设置--window-size加上--window-size1920,1080后再启动chrome not reachable上一次运行进程没有driver.quit()残留了僵尸进程杀掉残留进程或者重启终端/服务器DevToolsActivePort file doesnt existChrome 启动参数冲突通常是临时目录权限问题加--no-sandbox必要时指定--user-data-dir到有权限的路径图片中有大面积灰色图标图片懒加载未触发模拟滚动到底部等加载完成后再滚回顶部截图表格里的前两类问题在初学阶段出现频率最高尤其是版本失配它也是很多人第一次接触 webdriver-manager 的原因。如果升级完库依然报版本不对最直接的办法是删除 webdriver-manager 的缓存目录让它强制重新下载。缓存位置一般在用户目录下比如~/.wdm删掉再运行脚本就会重新拉取。5.2 我踩过的三个细节坑第一个坑是 webdriver-manager 的缓存机制。你以为它自动检测到浏览器升级了就会自动更新驱动实际上它默认优先复用缓存里已有的版本。如果你的 Chrome 从 126 升到了 127但缓存里还有 126 的驱动脚本不会主动重新下载而是继续用旧驱动直到跑出版本报错。解决办法就是删~/.wdm目录或者手动调用ChromeDriverManager().install()的新版本 API强制重装。第二个坑是 headless 模式和窗口尺寸的关系。我在一台服务器上部署截图任务时明明代码里设了--window-size1920,1080截出来的图片却还是 800 宽的小图。查了半天发现问题出在启动参数传递顺序以及 Chrome 新版 headless 模式的内部处理逻辑。后来我把无头模式的写法从--headless改成--headlessnew问题就消失了。新版无头模式对窗口尺寸的解析更接近真实浏览器如果你还在用旧写法建议尽快升级。第三个坑是忘记driver.quit()。脚本在本地跑的时候你可能觉得它结束就结束了没什么影响。但在服务器上跑批量任务每次不退出浏览器Chrome 进程就会一直挂在后台吃内存。跑了几百个 URL 之后服务器内存被占满后续任务全部卡死。正确做法是写try/finally或者with语句确保不管中间报不报错解析器退出后都会关闭浏览器。不要小看这一句在长任务里它会把你从内存爆炸的边缘拉回来。5.3 面对异步渲染页面的确定性策略有些页面不是一次性渲染完的而是异步请求接口拿到数据再更新 DOM。这种页面你在请求刚发出去的时候截图大概率截到的是占位图。除了显式等待元素出现我还有一个习惯等待网络请求数量从有到无或者等待某个关键图片加载完成。Selenium 判断图片加载完成没有直接的 API通常是通过元素属性检查。比如等待一个img标签的naturalWidth大于 0from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By WebDriverWait(driver, 10).until( lambda d: d.execute_script( return document.querySelector(img.hero) ? document.querySelector(img.hero).naturalWidth 0 : false ) )虽然代码写起来比单纯等标题复杂但在截图场景里很实用。判断依据是图片真正加载完成之前naturalWidth永远是 0这个值不会被 CSS 尺寸影响非常稳定。6. 从截图工具到自动化巡检的扩展思路单页截图是最小场景但真实工作中这个工具通常只是完整链路的一部分。我自己就把它扩展成了一个简单的页面巡检工具每天定时爬一批页面截图存档然后用 Python 的 Pillow 库对比前后两天的截图像素差异一旦发现页面关键区域变化超过阈值就推送邮件提醒。这个扩展过程其实很简单核心代码就是上面这些加三样东西就够定时调度Linux 上有 cron也可以直接用schedule库、邮件发送smtplib、图像对比PIL.ImageChops。另一种常见扩展是配合 OCR。截完图后用 Tesseract 识别图里的文字再和预期关键词做匹配比如“价格”“库存”“已售罄”等。这在商品页面监控、活动页面巡检中都非常有用不需要前端给你任何接口所见即所得。如果你想做得更工程化还可以把 URL 列表放到配置文件里用yaml或json维护或者用数据库记录每次截图的元信息方便回溯。不过这些都属于锦上添花了核心还是先把截图的稳定性练好。最后再分享一个操作上的小习惯。我写截图脚本时一定会在driver.get(url)之后把滚动位置强制恢复到顶部再等一会儿截图。原因很简单Chrome 有时候会记住上次访问该页面的滚动位置或者页面有“回到原来位置”的自动行为不处理的话截出来的图可能是页面中间的某个位置而不是首屏。这个坑很难从代码层面一眼看出但看到截图内容的瞬间你就明白了。
返回列表