
简介面向C#开发者的Selenium浏览器自动化入门示例适合初学自动化测试或网页数据监控的读者。内容以一个自动监控网站库存状态的小项目为例从NuGet安装Selenium.WebDriver与ChromeDriver说起逐步演示ChromeOptions配置、IWebDriver驱动创建、通过By.Id与By.XPath定位元素、SendKeys输入、Click点击等基础操作同时结合log4net记录运行日志、Queue队列按先进先出暂存日志、BackgroundWorker后台线程轮询页面变化等关键技术覆盖模拟登录、表单提交、状态检测等常见自动化场景。资源为单个PDF文档大小212KB内含核心代码片段、关键步骤说明及效果示意可帮助读者快速搭建环境并理解C#自动化测试整体流程。已有3076人学习下载适合希望用C#替代其他语言完成浏览器自动化的开发者参考。 做浏览器自动化C# 搭配 Selenium 一直是我的主力方案。趁着周末重新梳理了一套示例代码把环境配置、元素定位、等待机制、常见坑位一次讲透希望能帮到刚入门的同行。这套方案能解决的问题很直接批量填表、定时巡检、数据采集、重复性网页操作凡是每天固定要点的那些按钮和表单都可以交给脚本去跑。适合 C# 开发、测试工程师还有需要做 Web 端自动化但不想碰 Python 的朋友。我不会讲那些华而不实的框架封装就踏踏实实把 Selenium 在 C# 里的落地姿势讲清楚。1. 为什么用 C# 写 Selenium选型思路和适用场景1.1 C# 在自动化领域里的定位很多人一说 Selenium 就想到 Python确实 Python 写起来短平快但如果你本身是 .NET 技术栈或者公司里现有的测试框架、CI/CD 流程都是基于 .NET 的再用 Python 去另起炉灶反而增加维护成本。C# 是强类型语言写 UI 自动化的可读性和重构性比动态语言好很多尤其是在页面对象模型Page Object Model落地的时候接口定义、依赖注入这些玩得都很顺手。另外C# 在 Windows 生态里做桌面端和 Web 端混合自动化有天然优势。举个例子很多工厂和办公场景里扫码枪通过串口或 HID 模式输入C# 既能写上位机接收扫码数据又能调用 Selenium 操作网页一套代码把硬件输入和浏览器操作打通这是 Python 写起来比较别扭的地方。后面我会专门用一节讲扫码枪和浏览器自动化怎么联动。1.2 什么场景适合用 Selenium 硬刚什么场景应该绕道先泼一盆冷水。Selenium 不是万能的遇到以下情况我建议你先考虑别的方案别死磕纯 API 接口测试用 RestSharp 或者 HttpClient 直接调接口比驱动浏览器快一个量级。反爬极强的站点如果目标网站有复杂的行为验证、设备指纹检测Selenium 的自动化特征很容易被识别这时候 Playwright 的 stealth 模式或者直接分析接口更靠谱。高性能并发采集Selenium 启动浏览器实例太重了真要大规模采集用 HttpClient AngleSharp 解析 HTML 更实在。那什么场景最适合业务逻辑复杂、需要真实浏览器渲染、需要模拟用户完整操作流程的场景。比如 OA 系统里几十个字段的表单录入、ERP 里的跨页面审批流程、需要点击地图或拖拽的富交互页面这些用 Selenium 是最稳的。2. 从零搭建环境NuGet 包和浏览器驱动的那些坑2.1 创建项目和安装依赖包我习惯创建一个 .NET 8 的控制台项目来跑自动化脚本。如果你用的是 Visual Studio直接新建控制台应用就行也可以使用命令行快速完成dotnet new console -n SeleniumDemo cd SeleniumDemo然后安装三个核心包dotnet add package Selenium.WebDriver dotnet add package Selenium.Support dotnet add package Selenium.WebDriver.ChromeDriverSelenium.WebDriver是核心库Selenium.Support里面包含 ExpectedConditions、PageObject 等高级支持类Selenium.WebDriver.ChromeDriver这个包会自动下载对应版本的 ChromeDriver 二进制文件到本地省去很多手动配置的麻烦。用这个包要注意版本NuGet 会自动匹配与你本机 Chrome 主版本号一致的驱动如果没匹配上装完包后运行时会提醒你版本不兼容这是我踩过坑后强烈推荐用它的原因。2.2 浏览器驱动版本匹配这是新手翻车重灾区Selenium 本身不操作浏览器它通过浏览器驱动ChromeDriver、EdgeDriver、GeckoDriver与真实浏览器通信。驱动版本和浏览器版本必须大版本号一致否则启动时会直接报错The chromedriver version (X.X.X) is not supported by the browser version (Y.Y.Y)判断方法其实很简单打开 Chrome在地址栏输入chrome://version查看主版本号。然后去下载对应主版本的驱动。如果你用Selenium.WebDriver.ChromeDriver这个 NuGet 包它默认会去官方源拉驱动但在国内网络环境下这个下载经常超时。我的建议是如果 NuGet 能装成功优先用 NuGet 管理省心。如果下载超时就手动下载驱动用ChromeDriverService指定路径加载。手动指定路径的方式可以参考下面这段代码var service ChromeDriverService.CreateDefaultService(D:\chromedriver, chromedriver.exe); var options new ChromeOptions(); var driver new ChromeDriver(service, options);服务路径指向存放 chromedriver.exe 的目录文件名也一起传进去。这样无论驱动放在哪里只要版本对得上就能稳定启动。特别注意Win11 和部分系统上如果出现驱动被系统拦截的提示右键驱动文件属性在“解除锁定”那里勾选一下就行。3. 核心操作示例从打开网页到稳定定位元素3.1 浏览器实例的创建、配置和退出创建浏览器实例时最少必要的配置是这三样驱动服务、浏览器选项、超时设置。using OpenQA.Selenium; using OpenQA.Selenium.Chrome; var options new ChromeOptions(); // 无头模式适合服务器上跑定时任务 // options.AddArgument(--headlessnew); options.AddArgument(--start-maximized); // 禁用 GPU防止某些虚拟机上黑屏 options.AddArgument(--disable-gpu); // 去除自动化控制提示 options.AddExcludedArgument(enable-automation); var driver new ChromeDriver(options); driver.Manage().Timeouts().ImplicitWait TimeSpan.FromSeconds(10); try { driver.Navigate().GoToUrl(https://example.com); // 这里进行各种操作 } finally { driver.Quit(); }几点说明无头模式跑起来不显示界面在 CI/CD 服务器上非常实用。调试的时候建议注释掉用有头模式看实际操作。driver.Quit()务必放到finally里否则脚本中途抛异常会导致 chromedriver.exe 进程残留积累多了内存和进程数都受影响。ImplicitWait设置的是全局兜底等待但依赖它做同步是不够的后面我会单独讲显式等待。3.2 元素定位八种方式里实际只推荐几种Selenium 支持八种定位方式但我实际开发中基本上只用这几种定位方式用法示例适用场景IDdriver.FindElement(By.Id(username))首选页面中唯一速度最快Namedriver.FindElement(By.Name(password))表单控件常见CssSelectordriver.FindElement(By.CssSelector(#login-btn))灵活功能最强XPathdriver.FindElement(By.XPath(//button[text()登录]))文本定位、层级复杂的兜底方案LinkTextdriver.FindElement(By.LinkText(点击进入))链接导航我的经验法则ID CSS 选择器 XPath。不到万不得已不要用 XPath 绝对路径像/html/body/div[2]/div[3]/form/input[1]页面一改动就全部失效。尽量用相对路径配合属性或文本内容比如// 通过文本定位按钮比复杂层级靠谱得多 driver.FindElement(By.XPath(//button[contains(text(),提交订单)])).Click();遇到元素隐藏或者需要异步加载出来的直接交互会报ElementNotInteractableException。这时候先用 JavaScript 滚动到可视区域再操作比反复硬等待更高效var element driver.FindElement(By.XPath(//button[contains(text(),提交订单)])); ((IJavaScriptExecutor)driver).ExecuteScript(arguments[0].scrollIntoView(true);, element); element.Click();3.3 等待机制隐式等待、显式等待、强制等待怎么配合如果说定位元素是自动化的基础那等待就是自动化的灵魂。我见过的绝大多数不稳定脚本问题都出在等待写得太随意。三种等待各有用处Thread.Sleep(3000)是强制死等能在调试时用来观察页面但绝不建议在生产脚本里频繁用。网络稍微慢一点就超时快了又浪费时间。隐式等待ImplicitWait是全局轮询在指定时间内轮询查找元素找不到就继续找直到超时。显式等待WebDriverWait针对特定条件等待精确控制。这是最应该重点掌握的。因为隐式等待只能解决“元素在不在 DOM 里”的问题解决不了“元素在 DOM 里但不可见、不可点击、被遮住”这类情况。看一个实际例子很多系统保存成功后会弹一个绿色提示条等提示条消失再继续下一步直接 sleep 3 秒太粗暴了var wait new WebDriverWait(driver, TimeSpan.FromSeconds(15)); var successToast wait.Until(drv drv.FindElement(By.CssSelector(.toast-success))); // 等待某个元素消失比固定 sleep 更优雅 wait.Until(drv successToast.Displayed false);还可以使用 Selenium.Support 里的ExpectedConditions类进一步简化var wait new WebDriverWait(driver, TimeSpan.FromSeconds(10)); wait.Until(ExpectedConditions.ElementToBeClickable(By.Id(submit-btn)));这里有个使用细节隐式等待和显式等待不要混用在一个脚本里两者机制不同叠加起来反而会导致超时时间翻倍行为难以预测。我通常在初始化 driver 时只设置隐式等待做兜底后面的精确同步全部用显式等待。4. 高频实战场景拆解从表单到文件到扫码枪4.1 表单填写和下拉框、单选框的处理网页表单是自动化的主场。普通文本框直接用SendKeys输入就行但有几个细节能让你省很多事// 输入前先 Clear防止留上次默认值 var input driver.FindElement(By.Id(username)); input.Clear(); input.SendKeys(admin); // 下拉框用 Select 类来操作注意泛型类型 var selectElement driver.FindElement(By.Id(province)); var select new SelectElement(selectElement); select.SelectByText(广东省); // 或者 SelectByValue(440000)如果下拉框是自定义组件比如基于 li 标签的模拟下拉SelectElement就不管用了。这种组件通常是点击后渲染一个列表再点击对应项driver.FindElement(By.Id(custom-select)).Click(); driver.FindElement(By.XPath(//li[contains(text(),选项二)])).Click();单选框、复选框这类点击操作本身不难但要注意isSelected()的状态判断避免重复点击var check driver.FindElement(By.Name(agreement)); if (!check.Selected) { check.Click(); }4.2 文件上传的坑与不同写法网页里的文件上传目前最常见两种形态处理方式完全不同。一种是标准的input typefile直接用SendKeys把本地路径塞进去就行var upload driver.FindElement(By.CssSelector(input[typefile])); upload.SendKeys(D:\files\data.xlsx);这里有个关键点无论按钮 UI 做得多么花哨只要底层是 input[typefile]就一定能用 SendKeys 传路径。很多教程教你用 AutoIt 去模拟系统弹窗那是老黄历了现在主流浏览器都支持直接给 input 赋值根本不需要弹窗。另一种是拖拽上传或者隐藏了 input 元素的组件。对隐藏的 input可以用 JavaScript 把路径设置进去再触发 change 事件。我最近在做一个后台系统的批量导入功能时就遇到过前端组件把 input 给隐藏掉了直接定位不到用下面这段解决了IJavaScriptExecutor? js driver as IJavaScriptExecutor; js.ExecuteScript( var input document.createElement(input); input.type file; input.id tempFileInput; document.body.appendChild(input); ); var tempInput driver.FindElement(By.Id(tempFileInput)); tempInput.SendKeys(D:\files\data.xlsx);上传后千万别急着断言给上传处理留足时间等“上传成功”的文字出现或者进度条隐藏再继续。4.3 滑块验证码的处理思路学习用途滑块验证码是很多系统的安全门槛Selenium 单独搞不定因为它涉及拖拽轨迹模拟。处理思路一般是图像识别加模拟拖动而且现代滑块还会检测拖拽加速度、轨迹曲线是否是人为的。如果遇到的是基础滑块思路就是截图滑块区域用 OpenCV 找缺口位置计算位移量然后用Actions模拟拖动var slider driver.FindElement(By.CssSelector(.slider-btn)); var actions new Actions(driver); actions.ClickAndHold(slider) .MoveByOffset(offsetX, 10) .Pause(TimeSpan.FromMilliseconds(120)) .MoveByOffset(-20, 0) .MoveByOffset(offsetX 20, 5) .Pause(TimeSpan.FromMilliseconds(80)) .Release() .Perform();这段代码模拟了先快后慢、中间有回退的人类拖动轨迹。但说实话滑块验证码技术升级太快目前大厂的验证码系统已经不是单纯的轨迹检测了还有环境指纹、行为习惯分析。这类内容仅供学习 Selenium 操作原理不要用于绕过真实系统的安全机制。工作中如果遇到滑块是自己公司的内部系统尽量跟产品反馈加白名单比在自动化里死磕滑块要实际得多。4.4 扫码枪联动把硬件输入变成自动化入口这个场景在 C# 圈子里很常见工厂物料录入、仓库扫码收货都是用扫码枪扫条码然后网页系统里自动填入。扫码枪的本质是 HID 键盘设备它扫描条码后会在聚焦的输入框里快速输出一串字符并回车间隔。用 Selenium 做这件事有个天然的优势SendKeys输入的速率支持可控。思路有两种一种是最简单的把扫码枪焦点打在网页输入框上扫一下条码就录入进去了。这种场景 Selenium 只需要负责跳转页面和聚焦输入框剩下的交给扫码枪本身的输入事件。代码片段var barcodeInput driver.FindElement(By.Id(barcode)); barcodeInput.Click(); // 此时扫码枪扫描条码自动出现在输入框里 // 用显式等待判断输入框有内容后自动触发一次查询 var wait new WebDriverWait(driver, TimeSpan.FromSeconds(10)); wait.Until(drv !string.IsNullOrEmpty(drv.FindElement(By.Id(barcode)).GetAttribute(value))); driver.FindElement(By.Id(query)).Click();另一种更可控的方式C# 里用串口或 HidLibrary 监听扫码枪数据然后将拿到的条码字符串通过 Selenium 填入网页。相当于扫码枪的数据不直接走键盘模拟而是走过一层 C# 逻辑。这样做的优势是可以在填入前做数据格式化、查重、拦截异常条码。适合扫码枪通过串口和上位机通信的场景。// 伪代码示意串口事件收到条码 string rawBarcode ReceiveFromSerialPort(); string formatted FormatBarcode(rawBarcode); var input driver.FindElement(By.Id(barcode)); input.Clear(); input.SendKeys(formatted); driver.FindElement(By.Id(submit)).Click();如果有几千条数据要从 Excel 里读取并批量录入网页那更简单用OpenQA.Selenium配合循环即可Excel 读取用MiniExcel或ClosedXML比 OLEDB 省事太多。5. 常见问题与排查技巧实录5.1 高频报错速查表报错信息原因解决方案NoSuchElementException元素不存在或加载未完成改用显式等待检查选择器是否有 iframeElementNotInteractableException元素被遮挡或不可交互先滚动到可视区检查是否被其他元素遮住检查是否为 disabled 状态StaleElementReferenceException元素引用已过期页面刷新过或元素被替换重新查找元素引用WebDriverException: chrome cannot start驱动版本与浏览器不匹配检查 chromedriver 和 Chrome 主版本一致性InvalidOperationException: SessionNotCreated创建会话失败通常也是版本问题或浏览器进程残留结束进程重试TimeoutException等待条件超时分析等待条件是否正确元素是否真的不可达这里面最常见的两个一个是 iframe 里的元素定位不到一个是表格翻页后 StaleElement。iframe 的解法很固定先切进去操作完切回来driver.SwitchTo().Frame(mainFrame); // 操作 iframe 里的元素 driver.SwitchTo().DefaultContent();注意切 Frame 有两种方式传字符串是 name 或 id传 int 是 index传 IWebElement 是元素对象。调试的时候可以先输出网页源码里的 iframe 标签确认 name 属性再切。StaleElement 问题就是每次操作后重新查找元素不要缓存元素引用跨多个操作复用。我个人会封装一个小工具函数每次查找都走新的定位private IWebElement FindElementFresh(By by) { return driver.FindElement(by); }不缓存每次都是新的引用能规避大部分 StaleElement 异常。5.2 定位不稳和页面加载慢的终极排查手法页面加载策略这块容易被忽略。很多单页应用SPA用了大量 Ajax 异步加载ChromeDriver 默认的加载策略是等页面全部加载完才执行下一步但某些页面有个别外部资源加载不出来会导致一直卡着。这时可以调整页面加载策略为 none 或 eageroptions.PageLoadStrategy PageLoadStrategy.Eager;Eager 在 DOM 就绪时就返回不会等所有图片和样式表加载完适合重页面的自动化能省不少时间。定位不稳还有一个常见原因是页面存在动态 ID。有些现代化框架比如 Vue、React 渲染的列表会生成动态 ID每次刷新都变。这时就要用稳定的属性比如>// 不依赖动态 ID用结构关系定位表单行的输入框 driver.FindElement(By.XPath(//tr[contains(.//text(), 订单号)]//input)).SendKeys(20240001);5.3 调试效率和清理遗留进程开发阶段我建议开调试模式跑Chrome 不退出这样页面状态能手动检查。其实只要在创建 driver 时不调用 Quit浏览器会一直停在那里方便你观察当前 DOM 状态。也可以用 Chrome DevTools Protocol 配合执行 JS 来查看元素。跑完脚本后清理 chromedriver 进程是我每次必做的事。Windows 下直接taskkill /F /IM chromedriver.exe如果不想每次都手动清理可以在finally里加一句driver.Quit()后再显式杀掉可能残留的进程。当然正常Quit()之后不会残留但遇到浏览器崩溃或异常退出时这句兜底能省很多事。6. 几点个人实操体会做 Selenium 自动化三年多最大的感受是自动化的主要工作量在等待和定位而不是在调用浏览器 API 上。很多初学者一开始总想找一种万能的定位方法写了一套“万能封装”最后发现业务页面一改封装也要跟着改。其实不如老老实实按页面特点选择合适的定位方式用显式等待把异步加载的节奏控制好稳定性就上来了。另外跑 UI 自动化一定要有日志。每个关键步骤都记录一下当前 URL、元素位置、执行结果出了问题时才能快速定位是脚本的问题还是页面的问题。我还会在关键操作前后做个截图保存到本地排查问题的时候直接看图比看一堆文本日志要直观得多。最后分享一个小技巧如果你的自动化脚本需要长期跑在服务器上建议把 driver 的KeepBrowserOpen设置和 CI 的定时任务结合起来让脚本在深夜空闲时段运行尽量避开用户高峰。浏览器自动化本身很耗资源不合理的运行时间安排容易影响同服务器上的其他业务。本文还有配套的精品资源点击获取