
简介这是一份面向C#初学者与WinForm开发者的实战型爬虫项目资源聚焦桌面端关键词驱动的网页数据采集场景帮助学习者掌握HTTP请求、HTML解析、异步UI响应及基础反爬策略等核心技能。资源包共37个文件含5个关键C#源码文件.cs、2个可执行程序.exe及配套配置.json、缓存.cache、项目工程文件.sln、.csproj和界面资源.resx、.png整体仅248KB轻量易上手。已有191人下载学习适合用于课程设计、技术验证或小型数据采集工具开发。读者可直接运行exe体验关键词搜索→页面抓取→结果展示全流程源码结构清晰包含完整WinForm界面逻辑、HtmlAgilityPack解析实现、异常处理与线程安全控制是理解C#桌面爬虫工程化落地的典型范例。1. C# WinForm 爬虫不是“用 WinForm 写个界面套 Python 爬虫”而是真正在 .NET 框架里完成请求、解析、状态管理、UI 响应闭环的桌面端采集工具你搜“C# winform 爬虫”十有八九会看到一堆“用 WebBrowser 控件点点点”或“调用 Python 脚本再读 stdout”的缝合方案——它们不是真正的 WinForm 爬虫只是把爬虫逻辑外包了。而标题里这个“C# WinForm 爬虫输入关键词得到结果”指的是一套完全运行在 .NET Framework/.NET 6 进程内、不依赖外部解释器、能响应 UI 事件、可中断/重试/限速、带 Cookie 管理与反反爬基础适配的原生 Windows 桌面采集器。它解决的是运营要批量查竞品标题含“防水”“快充”的商品页数质检员需每小时抓取某工业设备官网的最新参数 PDF 下载链接HR 需从招聘站拉取“C# 上位机”“WinForm”岗位的薪资区间分布——这些场景不需要分布式、不走 Kafka但要求开箱即用、双击运行、输入框敲完回车就出 Excel 表格、失败时能看清哪一步卡住、老板临时加个“只抓近 7 天发布”的过滤条件改两行代码就能上线。它适合 C# 初级开发者会写 Button_Click、产线自动化工程师熟悉 VS2015/VS2022、常打安装包、以及被 Python 环境部署搞怕的国企 IT——因为整个程序就是一个 .exe 一个 config.json连 SQLite 都能嵌入成单文件。下面所有步骤我都在 VS2022 .NET 6 WinForms App 模板下实测过不调用任何第三方爬虫框架如 Scrapy.NET只用 System.Net.Http HtmlAgilityPack Newtonsoft.Json —— 这才是 WinForm 爬虫该有的样子。2. 从零搭起核心骨架HttpClient HtmlAgilityPack UI 线程安全三件套2.1 为什么不用 WebBrowser 或 WebView2 做“模拟点击”WebBrowser 是 IE 内核已弃用WebView2 虽新但本质是 Chromium 嵌入启动慢、内存高、调试难。而真实爬虫 80% 场景是 GET 请求 JSON API 或静态 HTML 解析——比如查“藏宝阁 爬虫”目标站其搜索结果页实际是https://xxx.com/search?kw防水sorttime返回的 HTML再如“闲鱼关键词监控”其列表接口走的是https://api.idlefish.com/v1/item/search?_kxxxqWinForm的 JSON。这类请求根本不需要渲染引擎。用 WebBrowser 反而引入 JS 执行不确定性页面异步加载、防爬跳转、线程阻塞Navigate 同步卡死 UI、以及无法细控超时/重试/UA 轮换。我经手的 17 个 WinForm 爬虫项目里仅 2 个必须用 WebView2目标站强制校验 navigator.webdriver其余全用 HttpClient 原生发包启动时间从 3s 降到 0.2s内存占用稳定在 40MB 以内。2.2 创建最小可行爬虫类支持关键词、分页、超时、自动重试新建 WinForm 项目后先通过 NuGet 安装HtmlAgilityPackv1.11.60用于解析 HTMLNewtonsoft.Jsonv13.0.3用于解析 JSON API可选System.Data.SQLite.Core若需本地存结果然后定义核心爬虫类KeywordCrawlerpublic class KeywordCrawler { private readonly HttpClient _httpClient; private readonly string _userAgent Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36; public KeywordCrawler() { var handler new HttpClientHandler { AutomaticDecompression DecompressionMethods.GZip | DecompressionMethods.Deflate, UseCookies true, // 关键保持登录态 MaxAutomaticRedirections 3 }; _httpClient new HttpClient(handler) { Timeout TimeSpan.FromSeconds(15) // 必设否则 UI 卡死 }; _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(_userAgent); } public async TaskListSearchResult SearchAsync(string keyword, int page 1) { try { // 示例构造百度搜索结果页 URL实际项目替换为目标站 var url $https://www.baidu.com/s?wd{Uri.EscapeDataString(keyword)}pn{(page - 1) * 10}; var response await _httpClient.GetAsync(url); response.EnsureSuccessStatusCode(); // 抛异常而非静默失败 var html await response.Content.ReadAsStringAsync(); var doc new HtmlDocument(); doc.LoadHtml(html); var results new ListSearchResult(); // 提取百度搜索结果标题和链接XPath 可按目标站调整 var nodes doc.DocumentNode.SelectNodes(//div[classresult]//h3/a); if (nodes ! null) { foreach (var node in nodes) { results.Add(new SearchResult { Title node.InnerText.Trim(), Url node.GetAttributeValue(href, ) }); } } return results; } catch (HttpRequestException ex) when (ex.StatusCode System.Net.HttpStatusCode.TooManyRequests) { // 遇到 429主动降速 await Task.Delay(3000); return await SearchAsync(keyword, page); // 递归重试 } catch (OperationCanceledException) { throw; // 供 UI 层捕获取消 } catch (Exception ex) { throw new Exception($关键词 {keyword} 第 {page} 页抓取失败: {ex.Message}, ex); } } } public class SearchResult { public string Title { get; set; } public string Url { get; set; } }关键说明HttpClient实例复用WinForm 中全局单例避免 socket 耗尽.NET Core 之后推荐IHttpClientFactory但 WinForm 项目简单复用更直观Timeout强制设为 15 秒防止 DNS 慢、CDN 故障导致 UI 线程假死UseCookies true维持会话对需要登录的“闲鱼关键词监控”类场景必不可少EnsureSuccessStatusCode()让 4xx/5xx 显式抛异常便于 UI 层统一处理重试逻辑写在catch里而非通用中间件WinForm 不同页面策略不同搜索页可重试详情页失败直接跳过Uri.EscapeDataString()安全编码关键词避免空格、中文、特殊字符破坏 URL。2.3 WinForm 界面线程安全交互Progress CancellationTokenSource InvokeUI 线程不能直接 await 异步方法也不能在非 UI 线程操作控件。正确做法是用ProgressT回传进度CancellationTokenSource控制取消// Form1.cs 中 private CancellationTokenSource _cts; private KeywordCrawler _crawler; private async void btnSearch_Click(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(txtKeyword.Text)) { MessageBox.Show(请输入关键词, 提示, MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // 初始化取消令牌 _cts?.Cancel(); _cts new CancellationTokenSource(); // 清空结果列表 lvResults.Items.Clear(); lblStatus.Text 正在搜索...; btnSearch.Enabled false; btnCancel.Enabled true; try { var progress new ProgressSearchResult(result { // 此委托在 UI 线程执行 var item lvResults.Items.Add(result.Title); item.SubItems.Add(result.Url); item.SubItems.Add(DateTime.Now.ToString(HH:mm:ss)); }); // 启动爬虫注意await 在 UI 线程上等待但不阻塞 var results await _crawler.SearchAsync(txtKeyword.Text, 1) .ConfigureAwait(false); // 防止上下文捕获提升性能 // 批量更新 UI比逐条 Add 更快 lvResults.Items.AddRange(results.Select(r new ListViewItem(new[] { r.Title, r.Url, DateTime.Now.ToString(HH:mm:ss) }).ToArray()).ToArray()); lblStatus.Text $共找到 {results.Count} 条结果; } catch (OperationCanceledException) { lblStatus.Text 已取消; } catch (Exception ex) { lblStatus.Text $错误: {ex.InnerException?.Message ?? ex.Message}; MessageBox.Show(ex.ToString(), 抓取异常, MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { btnSearch.Enabled true; btnCancel.Enabled false; } } private void btnCancel_Click(object sender, EventArgs e) { _cts?.Cancel(); }为什么用ProgressT而不是InvokeInvoke需手动判断InvokeRequired易漏写ProgressT内部自动调度到创建它的线程即 UI 线程且支持泛型类型安全。实测 1000 条结果逐条Invoke耗时 1200ms用ProgressTItems.AddRange仅 180ms。3. 关键词驱动的动态请求构建URL 拼接、参数签名、Referer 伪造三原则3.1 识别目标站的关键词参数规律GET vs POST vs JSON API不是所有站都像百度一样用?wd关键词。实战中需快速判断纯前端渲染站如部分 Vue SPAF12 → Network → 输入关键词搜索 → 查看 XHR 标签页 → 找到fetch或axios发起的请求 → 复制其 URL 或 Request Payload服务端渲染站如传统 PHP 站直接看地址栏变化观察?q、?keyword、?search等参数名JSON API 站如闲鱼、Boss 直聘XHR 中找Content-Type: application/json的 POST 请求Payload 里必含q或keyword字段。血泪经验曾有个“C# 上位机”招聘数据爬虫目标站用POST /api/job/searchBody 是{ keyword: C# 上位机, city: shanghai, page: 1 }但 Header 必须带X-Token和X-Timestamp。后者是当前毫秒时间戳前者是MD5(keyword timestamp salt)。这种签名必须在 C# 里实时计算不能硬编码。3.2 构建可配置的请求模板用 JSON 配置替代硬编码 URL把 URL 规则抽成配置文件config.json避免每次换站都改代码{ TargetSite: baidu, BaseUrl: https://www.baidu.com/s, ParamTemplate: ?wd{keyword}pn{offset}, Pagination: { StartPage: 1, PageSize: 10, OffsetStep: 10 }, Headers: { Referer: https://www.baidu.com/, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8 } }对应 C# 加载逻辑public class CrawlConfig { public string TargetSite { get; set; } public string BaseUrl { get; set; } public string ParamTemplate { get; set; } public PaginationConfig Pagination { get; set; } public Dictionarystring, string Headers { get; set; } } public class PaginationConfig { public int StartPage { get; set; } 1; public int PageSize { get; set; } 10; public int OffsetStep { get; set; } 10; } // 加载配置 private CrawlConfig LoadConfig() { var json File.ReadAllText(config.json); return JsonConvert.DeserializeObjectCrawlConfig(json); } // 构建 URL private string BuildUrl(string keyword, int page, CrawlConfig config) { var offset (page - config.Pagination.StartPage) * config.Pagination.OffsetStep; var url config.BaseUrl config.ParamTemplate .Replace({keyword}, Uri.EscapeDataString(keyword)) .Replace({offset}, offset.ToString()); return url; }参数说明OffsetStep百度每页 10 条第 2 页 offset10第 3 页 offset20但某电商站用?page2此时OffsetStep设为 0ParamTemplate改为?q{keyword}page{offset}Headers字段Referer防止防盗链如图片站返回 403Accept影响服务器返回格式设application/json可能拿到结构化数据而非 HTMLTargetSite后续可扩展多站点切换逻辑如switch(config.TargetSite)加载不同 XPath。3.3 Referer 和 User-Agent 动态轮换绕过基础反爬很多站检查Referer是否来自自身域名或User-Agent是否为常见浏览器。硬写一个 UA 极易被封。解决方案维护 UA 池 Referer 池随机选取private static readonly string[] UserAgents { Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36, Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 }; private static readonly string[] Referers { https://www.google.com/, https://www.baidu.com/, https://www.bing.com/ }; // 在 HttpClient 初始化时设置 _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(UserAgents[new Random().Next(UserAgents.Length)]); _httpClient.DefaultRequestHeaders.Referrer new Uri(Referers[new Random().Next(Referers.Length)]);注意new Random()在循环中高频创建会导致种子重复产生相同 UA。生产环境应使用static readonly Random _random new Random()。4. 解析层深度定制XPath 与 CSS Selector 双模支持应对不同站点结构4.1 HtmlAgilityPack 的 XPath 语法实战从百度到招聘站的提取差异XPath 是 WinForm 爬虫最稳的解析方式CSS Selector 在 HtmlAgilityPack 中支持有限。关键技巧用//而非/html/body/div[3]绝对路径一改就崩//div[classresult]//h3/a更健壮用contains(class, xxx)替代classxxx防 class 名含空格或动态添加如classresult c-container用normalize-space()清理空白normalize-space(//title/text())自动 trim 换行符和多余空格。以三个典型站点为例目标站XPath 提取标题说明百度搜索//div[contains(class,result)]/h3/a百度结果块 class 动态用contains前程无忧职位列表//div[idresultList]/div[classel]/p[classt1]/span/aID 锁定区域再层层定位某工业设备官网静态 HTML//article[contains(class,product-item)]/h2/aclass 名含语义直接匹配// 通用提取方法 public Liststring ExtractByXPath(string html, string xpath, string attributeName null) { var doc new HtmlDocument(); doc.LoadHtml(html); var nodes doc.DocumentNode.SelectNodes(xpath); if (nodes null) return new Liststring(); var results new Liststring(); foreach (var node in nodes) { if (attributeName ! null) results.Add(node.GetAttributeValue(attributeName, )); else results.Add(node.InnerText.Trim()); } return results; } // 调用示例 var titles ExtractByXPath(html, //div[contains(class,result)]/h3/a); var urls ExtractByXPath(html, //div[contains(class,result)]/h3/a, href);4.2 JSON API 解析Newtonsoft.Json 的强类型反序列化当目标站返回 JSON如闲鱼、BOSS 直聘直接反序列化比正则快 10 倍且安全// 定义模型字段名严格匹配 JSON key public class JobResponse { public ListJobItem content { get; set; } public int total_count { get; set; } } public class JobItem { public string job_name { get; set; } public string salary { get; set; } public string company_name { get; set; } public string job_link { get; set; } } // 解析 var json await response.Content.ReadAsStringAsync(); var apiResult JsonConvert.DeserializeObjectJobResponse(json); foreach (var job in apiResult.content) { // 添加到 ListView... }避坑点JSON key 含下划线如job_nameC# 属性名用 PascalCase需加[JsonProperty(job_name)]特性否则反序列化为空。4.3 处理 JavaScript 渲染内容Headless Chrome 的轻量替代方案极少数站如部分 React SSR 站关键数据藏在script的window.__INITIAL_STATE__里。此时不必上 PuppeteerSharp太重用正则提取更高效// 从 HTML 中提取 script 标签内的 JSON 数据 var scriptMatch Regex.Match(html, script[^]*.*?window\.__INITIAL_STATE__\s*\s*(\{.*?\});.*?/script, RegexOptions.Singleline); if (scriptMatch.Success scriptMatch.Groups.Count 1) { var jsonData scriptMatch.Groups[1].Value; var data JsonConvert.DeserializeObjectInitialState(jsonData); // 处理 data... }警告正则解析 JSON 有风险仅用于__INITIAL_STATE__这类结构固定、无嵌套引号的场景。复杂 JSON 必须用 HtmlAgilityPack 提取 script 文本后再用 Json.NET 解析。5. 避坑指南WinForm 爬虫开发中 5 个高频翻车点及解法5.1 现象UI 卡死 10 秒后弹出“未将对象引用设置到对象的实例”原因在btnSearch_Click里直接写var html _httpClient.GetStringAsync(url).ResultResult阻塞 UI 线程同时HttpClient在同步上下文中可能引发死锁尤其 .NET Framework。解决绝对禁用.Result和.Wait()所有异步方法必须async/await链式调用若必须同步如初始化用GetAwaiter().GetResult()替代.Result.NET Core 安全。5.2 现象抓取结果总是空但浏览器打开 URL 能看到数据原因目标站通过document.write()或 AJAX 动态注入内容而 HttpClient 只拿原始 HTML没执行 JS。解决先确认是否真需 JS 渲染F12 → Network → Disable Cache → 刷新 → 查看第一个 HTML 响应体是否含目标数据若不含且必须 JS 渲染改用 WebView2 控件加载页面再用ExecuteScriptAsync注入 JS 提取document.body.innerHTML不推荐PuppeteerSharpWinForm 打包后体积暴增 100MB且需 Chromium 二进制。5.3 现象同一关键词多次抓取结果条数不一致有时 0 条原因目标站启用 IP 限频首次请求成功后续返回 403 或跳转到验证码页但代码没检查 HTTP 状态码或重定向 URL。解决在SearchAsync中增加状态码判断if (response.StatusCode HttpStatusCode.Forbidden || response.RequestMessage.RequestUri.ToString().Contains(captcha)) { throw new Exception(触发反爬请降低频率或更换 IP); }添加请求间隔await Task.Delay(1000)在每次请求后可配置使用代理池需额外集成此处略。5.4 现象中文关键词搜索乱码URL 中出现%E4%BD%A0%E5%A5%BD但页面显示“浣濂”原因Uri.EscapeDataString()编码正确但目标站用 GB2312 解码而 .NET 默认 UTF-8。解决强制指定编码Encoding.GetEncoding(GB2312)构造 URL 时手动编码var encodedKw HttpUtility.UrlEncode(keyword, Encoding.GetEncoding(GB2312)); var url $http://xxx.com/search?q{encodedKw};5.5 现象打包成安装程序后双击运行报错“找不到 HtmlAgilityPack.dll”原因NuGet 包未设为“复制到输出目录”或目标机器无 .NET Runtime。解决在项目属性 → “发布” → “依赖项”中勾选“Microsoft .NET Runtime”.NET 6对每个 NuGet 包在解决方案资源管理器中右键 → “属性” → “复制到输出目录”设为“始终复制”最终生成单文件项目属性 → “生成” → “生成单文件”设为 True.NET 5。6. 进阶技巧把关键词结果导出为 Excel 并自动邮件发送实现无人值守监控6.1 用 EPPlus 生成免 Office 依赖的 Excel 文件Microsoft.Office.Interop.Excel需本机装 Office且 WinForm 服务化时崩溃率高。EPPlus 是纯 .NET 库支持 .NET 6导出速度比 NPOI 快 40%// NuGet 安装 EPPlus // 注意需在 LicenseContext.SetLicenseContext(LicenseContext.NonCommercial) 或购买商业许可 private void ExportToExcel(ListSearchResult results, string fileName) { ExcelPackage.LicenseContext LicenseContext.NonCommercial; // 开发测试用 using (var package new ExcelPackage()) { var worksheet package.Workbook.Worksheets.Add(搜索结果); // 写表头 worksheet.Cells[1, 1].Value 标题; worksheet.Cells[1, 2].Value 链接; worksheet.Cells[1, 3].Value 抓取时间; // 写数据从第 2 行开始 for (int i 0; i results.Count; i) { worksheet.Cells[i 2, 1].Value results[i].Title; worksheet.Cells[i 2, 2].Value results[i].Url; worksheet.Cells[i 2, 3].Value DateTime.Now; } // 自动列宽 worksheet.Cells.AutoFitColumns(); // 保存 File.WriteAllBytes(fileName, package.GetAsByteArray()); } }参数说明LicenseContext.NonCommercial仅限非商业用途生产环境请购正式 licenseGetAsByteArray()内存中生成不写临时文件适合 WinForm 快速导出AutoFitColumns()避免中文列宽过窄用户体验关键细节。6.2 集成 SMTP 邮件发送定时任务触发关键词监控WinForm 可用System.Threading.Timer实现简易定时配合邮件发送形成闭环private Timer _monitorTimer; private void StartMonitor(string keyword, TimeSpan interval) { _monitorTimer new Timer(async state { try { var results await _crawler.SearchAsync(keyword); if (results.Count 0) { var fileName $result_{DateTime.Now:yyyyMMdd_HHmmss}.xlsx; ExportToExcel(results, fileName); await SendEmailAsync( to: admincompany.com, subject: $【关键词监控】{keyword} 新增 {results.Count} 条结果, body: $抓取时间{DateTime.Now}\n附件为详细列表。, attachmentPath: fileName ); // 删除本地文件可选 File.Delete(fileName); } } catch (Exception ex) { // 记录日志不中断定时器 LogError(ex); } }, null, TimeSpan.Zero, interval); } private async Task SendEmailAsync(string to, string subject, string body, string attachmentPath null) { var smtpClient new SmtpClient(smtp.qq.com) { Port 587, Credentials new NetworkCredential(yourqq.com, your-app-password), EnableSsl true }; var mailMessage new MailMessage { From new MailAddress(yourqq.com), Subject subject, Body body, IsBodyHtml false }; mailMessage.To.Add(to); if (!string.IsNullOrEmpty(attachmentPath) File.Exists(attachmentPath)) { var attachment new Attachment(attachmentPath); mailMessage.Attachments.Add(attachment); } await smtpClient.SendMailAsync(mailMessage); }安全提示QQ 邮箱需开启 SMTP 并生成“授权码”非登录密码填入Credentials企业邮箱请替换为对应 SMTP 服务器和端口。6.3 一键打包成安装程序WiX Toolset 实战配置VS2022 自带“安装项目”已废弃WiX 是目前最稳定的 WinForm 打包方案。步骤下载 WiX Toolsetwixtoolset.org并安装在解决方案中添加“WiX Project”编辑Product.wxs关键配置如下?xml version1.0 encodingUTF-8? Wix xmlnshttp://wixtoolset.org/schemas/v4/wxs Product Id* NameKeywordCrawler Language1033 Version1.0.0.0 ManufacturerYourCompany UpgradeCodePUT-GUID-HERE Package InstallerVersion200 Compressedyes InstallScopeperMachine / MajorUpgrade DowngradeErrorMessageA newer version of [ProductName] is already installed. / MediaTemplate / Feature IdProductFeature TitleKeywordCrawler Level1 ComponentGroupRef IdProductComponents / /Feature /Product Fragment Directory IdTARGETDIR NameSourceDir Directory IdProgramFilesFolder Directory IdINSTALLFOLDER NameKeywordCrawler / /Directory /Directory /Fragment Fragment ComponentGroup IdProductComponents DirectoryINSTALLFOLDER !-- 主程序 -- Component IdMainExecutable Guid* File IdexeFile Source$(var.KeywordCrawler.TargetPath) KeyPathyes / /Component !-- 配置文件 -- Component IdConfigFile Guid* File IdconfigFile Sourceconfig.json / /Component !-- 依赖 DLL -- Component IdHtmlAgilityPackDll Guid* File IdhapDll Source$(var.KeywordCrawler.ProjectDir)\bin\Debug\HtmlAgilityPack.dll / /Component !-- ... 其他 DLL -- /ComponentGroup /Fragment /Wix关键点$(var.KeywordCrawler.TargetPath)自动引用主项目输出路径Guid*WiX 自动生成 GUID避免手动维护InstallScopeperMachine安装到 Program Files所有用户可用打包后生成.msi双击即可安装卸载干净无残留。我做 WinForm 爬虫这七年最深的教训是别在 UI 线程里做任何耗时操作别信“这个站很简单”也别省那几行配置代码。一个能跑通的关键词爬虫核心不在技术多炫而在请求可控、解析可靠、失败可查、交付可装。现在你手里的 VS2022已经具备做出真正生产力工具的一切条件——去试去调去打包去给同事演示“输入‘C# 上位机’3 秒出 Excel”。希望帮到你。本文还有配套的精品资源点击获取