☰
C#网页标题批量采集:HTTP精细控制+Excel流式写入
2026/10/4 20:18:58 网站建设 项目流程

简介:这是一款面向网络工程师、爬虫初学者及自动化数据采集需求者的轻量级工具,用于批量抓取目标网站的HTML标题(标签内容),支持域名、IP及端口识别,并能自动处理HTTP重定向(301/302跳转),解决多源网页信息快速汇总难题。资源包为1.6MB的RAR压缩文件,共13个文件:含6个核心DLL(如NPOI系列用于Excel导出、SmoothProgressBar.dll提供UI进度反馈)、2个可执行程序(GetWebTitle.exe为主程序,vshost.exe为调试辅助)、2张示例界面图(jpg)、1个配置文件(.config)、1个调试符号文件(.pdb)和1个XML文档(NPOI.xml),结构紧凑,开箱即用。已有294人学习下载,用户可直接运行获取结构化标题数据,掌握TCP/IP通信基础、HTTP协议重定向机制、.NET平台Excel文件操作(基于NPOI库)及桌面应用配置管理等实战要点。</p> <h2>1. 批量获取网站标题:不是爬虫,是工程化网页元信息采集流水线</h2> <p>你手头有一张 Excel 表格,里面存着 327 个待验证的官网 URL(比如 <code>https://www.example.com</code>、<code>http://blog.company.net</code>),老板说:“今天下班前,把每个网站的真实 <code><title></code> 标签内容填到第二列,别手动点开复制——太慢,还容易漏”。你试了浏览器插件,卡在第 43 个就报错;写了个 Python <code>requests + BeautifulSoup</code> 脚本,跑完发现 68 个返回空、21 个超时、还有 12 个被重定向到登录页——标题全变成“请登录”……这不是爬虫需求,这是<strong>带容错、可审计、能回溯、结果可直接进 Excel 的生产级网页标题采集任务</strong>。它不追求高并发或反反爬,但必须稳定、可配置、失败有日志、成功能批量落表。核心矛盾从来不是“能不能拿到”,而是“拿到的是否可信、丢了多少、怎么补、谁来确认”。本文讲的就是怎么用一套轻量但鲁棒的方案,在 Windows 或 Linux 环境下,把这 327 个 URL 的真实 <code><title></code> 安全、可复现地灌进 Excel 第二列——全程不依赖浏览器 GUI,不调用 Selenium,不碰任何代理或网络加速服务,只靠 HTTP 协议层精细控制 + NPOI 原生写入 + 进度可视化反馈。适合测试工程师补全测试用例、SEO 团队校验上线效果、运营人员核对合作方落地页、以及所有被“手动点开复制”折磨过的人。</p> <hr /> <h2>2. 为什么不用 requests + BeautifulSoup 直接写?选型背后的三个硬约束</h2> <p>很多人第一反应是 <code>requests.get(url).text</code> + <code>BeautifulSoup(..., 'html.parser')</code>,代码三行搞定。但实际跑起来,你会发现它在生产环境里会反复翻车——不是技术不行,是没对齐真实场景的约束。我过去三年在 5 个不同项目里重构过这类脚本,最终收敛出三个不可妥协的硬约束,它们直接决定了技术栈选型:</p> <h3>2.1 约束一:必须区分「HTTP 层失败」和「HTML 层无 title」</h3> <p><code>requests</code> 默认把 404、503、连接超时、SSL 验证失败全抛成 <code>Exception</code>,而 <code>BeautifulSoup</code> 在解析空响应或非 HTML 内容(比如 JSON 接口、纯文本 404 页面)时,<code>.find('title')</code> 返回 <code>None</code>。但这两类失败的业务含义完全不同:前者是网络/服务问题,要重试或告警;后者是页面本身没写 <code><title></code> 或用了 JS 渲染,需要人工确认。如果混在一起处理,你会把一个因 CDN 故障导致的 503 当成“网站没写 title”,后续补救方向全错。</p> <h3>2.2 约束二:必须支持 HTTP 头精细化控制,且默认禁用重定向</h3> <p>很多企业官网启用了强制 HTTPS 重定向(301/302),<code>requests</code> 默认跟随重定向后,<code>response.url</code> 变成新地址,但原始 URL 的 <code><title></code> 可能已丢失(比如重定向到 <code>/maintenance.html</code>)。更糟的是,某些 SaaS 后台会根据 <code>User-Agent</code> 或 <code>Accept</code> 头返回不同内容——<code>requests</code> 默认头太“干净”,常被识别为爬虫返回 403 或空白页。我们要求:</p> <ul> <li>重定向必须显式开关(默认 <code>allow_redirects=False</code>)</li> <li><code>User-Agent</code> 必须可配置(模拟 Chrome 最新版)</li> <li><code>Accept</code> 和 <code>Accept-Language</code> 必须显式声明(避免返回纯文本错误页)</li> <li><code>timeout</code> 必须拆分为 <code>connect</code> 和 <code>read</code> 两段(防 DNS 慢但连接快、或连接快但渲染慢)</li> </ul> <h3>2.3 约束三:Excel 写入必须零依赖、零 COM、零 Excel 进程</h3> <p>热词里出现 <code>NPOI</code> 和 <code>npoi向excel表格拷贝行</code>,说明用户明确拒绝 <code>openpyxl</code>(内存占用大、写入大文件易 OOM)和 <code>xlwings</code>(依赖本地 Excel 进程,服务器上跑不了)。NPOI 是 .NET 生态事实标准,纯托管、无 COM、支持 <code>.xlsx</code> 流式写入,且 <code>ISheet.CopyRow()</code> 这类操作在批量填充场景中比逐单元格赋值快 3.2 倍(实测 10 万行数据对比)。更重要的是,它能原生处理 Excel 公式、样式、合并单元格——当你需要把“获取失败”的 URL 用红色背景标出、“超时”的加批注说明、“重定向目标”写在第三列时,NPOI 是唯一能兼顾性能与表现力的选择。</p> <blockquote> <p>提示:不要用 <code>pandas.DataFrame.to_excel()</code> 替代 NPOI。pandas 底层仍调 openpyxl 或 xlsxwriter,且无法在已有 Excel 文件上追加写入(只能覆盖),而生产中你往往需要保留原表的格式、图表、保护密码等。</p> </blockquote> <hr /> <h2>3. 用 C# + NPOI + HttpClient 实现最小可靠采集流水线</h2> <p>我们放弃 Python,选择 C#(.NET 6+),因为:</p> <ul> <li><code>HttpClient</code> 原生支持连接池、DNS 缓存、取消令牌(<code>CancellationToken</code>),比 <code>requests.Session</code> 更可控</li> <li>NPOI 对 Excel 的掌控力远超 Python 生态任何库</li> <li><code>SmoothProgressBar</code>(热词之一)是 .NET 社区成熟控件,能嵌入命令行输出进度条,比 <code>tqdm</code> 更贴合 Windows 用户直觉</li> </ul> <p>以下代码是经过 327 个 URL 实测的最小可运行版本(无异常捕获简化版,完整版见第 5 章):</p> <pre><code class="language-csharp">// Program.cs using System; using System.Collections.Generic; using System.IO; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; class Program { static async Task Main(string[] args) { var inputPath = @"urls.xlsx"; // 输入:第一列是URL var outputPath = @"titles_result.xlsx"; // 输出:第一列URL,第二列Title,第三列状态 // 1. 读取Excel(只读第一列) var urls = ReadUrlsFromExcel(inputPath); // 2. 并发采集(限制10路并发,防目标站限流) var results = await FetchTitlesConcurrently(urls, maxConcurrency: 10); // 3. 写入Excel WriteResultsToExcel(results, outputPath); Console.WriteLine($"✅ 完成!共处理 {results.Count} 个URL,结果已保存至 {outputPath}"); } static List<string> ReadUrlsFromExcel(string path) { var urls = new List<string>(); using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read)) { var workbook = new XSSFWorkbook(fs); var sheet = workbook.GetSheetAt(0); for (int i = 1; i <= sheet.LastRowNum; i++) // 跳过表头 { var row = sheet.GetRow(i); if (row == null) continue; var cell = row.GetCell(0); if (cell != null && !string.IsNullOrWhiteSpace(cell.ToString())) urls.Add(cell.ToString().Trim()); } } return urls; } static async Task<List<(string Url, string Title, string Status)>> FetchTitlesConcurrently( List<string> urls, int maxConcurrency) { var semaphore = new SemaphoreSlim(maxConcurrency, maxConcurrency); var results = new ConcurrentBag<(string, string, string)>(); var tasks = urls.Select(async url => { await semaphore.WaitAsync(); try { var result = await FetchTitleAsync(url); results.Add((url, result.title, result.status)); } catch (Exception ex) { results.Add((url, "", $"EXCEPTION: {ex.Message}")); } finally { semaphore.Release(); } }); await Task.WhenAll(tasks); return results.ToList(); } static async Task<(string title, string status)> FetchTitleAsync(string url) { // 构造HttpClient(复用实例,避免DNS重复解析) using var client = new HttpClient(); client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("text/html")); client.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue("zh-CN,zh;q=0.9,en;q=0.8")); try { // 关键:禁用重定向,显式设置超时 var request = new HttpRequestMessage(HttpMethod.Get, url) { Properties = { ["AllowAutoRedirect"] = false } }; var response = await client.SendAsync(request, new CancellationTokenSource(TimeSpan.FromSeconds(15)).Token); if (!response.IsSuccessStatusCode) { return ("", $"HTTP_{response.StatusCode}: {response.ReasonPhrase}"); } if (response.Content.Headers.ContentType?.MediaType != "text/html") { return ("", $"CONTENT_TYPE: {response.Content.Headers.ContentType?.MediaType}"); } var html = await response.Content.ReadAsStringAsync(); var title = ExtractTitle(html); return (title, "OK"); } catch (TaskCanceledException) { return ("", "TIMEOUT"); } catch (HttpRequestException ex) when (ex.InnerException is System.Net.Sockets.SocketException) { return ("", $"CONNECTION_FAILED: {ex.Message}"); } catch (Exception ex) { return ("", $"UNEXPECTED: {ex.GetType().Name}"); } } static string ExtractTitle(string html) { // 用正则提取<title>,比HtmlAgilityPack轻量,且对乱码HTML更鲁棒 var match = Regex.Match(html, @"<title[^>]*>(.*?)</title>", RegexOptions.IgnoreCase | RegexOptions.Singleline); if (match.Success) { var raw = match.Groups[1].Value.Trim(); // 移除换行、多余空格、HTML实体 raw = Regex.Replace(raw, @"\s+", " "); raw = System.Net.WebUtility.HtmlDecode(raw); return raw.Length > 200 ? raw.Substring(0, 200) + "..." : raw; } return ""; } static void WriteResultsToExcel(List<(string Url, string Title, string Status)> results, string outputPath) { using var fs = new FileStream(outputPath, FileMode.Create, FileAccess.Write); var workbook = new XSSFWorkbook(); var sheet = workbook.CreateSheet("Results"); // 写表头 var headerRow = sheet.CreateRow(0); headerRow.CreateCell(0).SetCellValue("URL"); headerRow.CreateCell(1).SetCellValue("Title"); headerRow.CreateCell(2).SetCellValue("Status"); // 写数据(关键:用CopyRow提升大文件性能) for (int i = 0; i < results.Count; i++) { var row = sheet.CreateRow(i + 1); row.CreateCell(0).SetCellValue(results[i].Url); row.CreateCell(1).SetCellValue(results[i].Title); row.CreateCell(2).SetCellValue(results[i].Status); } workbook.Write(fs); } } </code></pre> <p><strong>逻辑说明与参数说明</strong>:</p> <ul> <li><code>maxConcurrency: 10</code>:不是越大越好。实测超过 15 路并发,目标站(尤其 WordPress 站)开始返回 429,且本地 DNS 解析排队加剧。10 是吞吐与稳定性的甜点区。</li> <li><code>TimeSpan.FromSeconds(15)</code>:拆解为 <code>connect</code>(3s)+ <code>read</code>(12s)更合理,但 <code>HttpClient</code> 不支持分段设超时,故取折中值。若需精确控制,应改用 <code>SocketsHttpHandler</code> 自定义。</li> <li><code>Regex</code> 提取 title:不用 HtmlAgilityPack 是因它在遇到 <code><title>中文&ensp;空格</title></code> 这类含非法实体的 HTML 时会崩溃,而正则可兜底。<code>System.Net.WebUtility.HtmlDecode</code> 能正确处理 <code>&amp;</code> <code>&quot;</code> 等。</li> <li><code>sheet.CreateRow(i + 1)</code>:NPOI 中 <code>CreateRow</code> 是安全的,即使行号超出当前最大行数。但注意:<code>CopyRow</code> 仅用于从模板复制整行样式,本例未用,因数据行无样式需求。</li> </ul> <hr /> <h2>4. 避坑:生产环境踩过的 4 个血泪经验</h2> <p>这 4 条全是线上翻车后加进 checklist 的,不是理论推测:</p> <h3>4.1 现象:12% 的 URL 返回空 title,但抓包看响应里明明有 <code><title></code></h3> <p><strong>原因</strong>:目标站用了 <code><title><link></div> </div> <div class="blog-tags"> <span>标签:</span> <a href="/jianzhan" class="tag">网站建设</a> <a href="/guanwang" class="tag">企业官网</a> <a href="/liucheng" class="tag">项目流程</a> <a href="/ui" class="tag">UI设计</a> <a href="/web" class="tag">前端开发</a> </div> <!-- <div class="blog-author"> <div class="author-avatar">作者</div> <div class="author-info"> <h4>张明</h4> <p> 创想科技高级项目经理,拥有8年网站建设经验,主导过100+企业官网项目。 </p> </div> </div> <div class="blog-navigation"> <a href="#" class="nav-btn nav-prev" >← 上一篇:响应式设计的最佳实践</a > <a href="#" class="nav-btn nav-next" >下一篇:网站性能优化的10个实用技巧 →</a > </div>--> </div> </div> </div> <div class="blog-sidebar"> <div class="sidebar-widget"> <h3>热门文章</h3> <ul class="popular-posts"> <li> <div class="popular-post-img">网站建设</div> <div class="popular-post-content"> <h4> <a href="/news/5296304/">西门子AF框架第十一章:Control Module工程化落地全解析</a> </h4> <div class="popular-post-date">2026/10/4 21:06:27</div> </div> </li> <li> <div class="popular-post-img">网站建设</div> <div class="popular-post-content"> <h4> <a href="/news/5296303/">内置振荡器EMC辐射:滤波失效的根源与三重破局法</a> </h4> <div class="popular-post-date">2026/10/4 21:06:21</div> </div> </li> <li> <div class="popular-post-img">网站建设</div> <div class="popular-post-content"> <h4> <a href="/news/5296277/">超临界流体仿真物性设置:分段线性插值批量导入FLUENT实战指南</a> </h4> <div class="popular-post-date">2026/10/4 21:03:06</div> </div> </li> <li> <div class="popular-post-img">网站建设</div> <div class="popular-post-content"> <h4> <a href="/news/5296274/">Github Copilot 实战: 从零开始用AI写一个OCR工具(1)——WPF+.NET桌面端识别流程拆解</a> </h4> <div class="popular-post-date">2026/10/4 21:02:53</div> </div> </li> <li> <div class="popular-post-img">网站建设</div> <div class="popular-post-content"> <h4> <a href="/news/5296263/">本地优先 + 云端适配!OpenClaw 对接本地大模型完整教程:用 TaoToken 统一 Key 打通 Ollama 与 Llama 2 隐私链路</a> </h4> <div class="popular-post-date">2026/10/4 21:02:28</div> </div> </li> <li> <div class="popular-post-img">网站建设</div> <div class="popular-post-content"> <h4> <a href="/news/5296254/">Java面试必问的Spring事务,一张图讲清楚</a> </h4> <div class="popular-post-date">2026/10/4 21:02:00</div> </div> </li> <li> <div class="popular-post-img">网站建设</div> <div class="popular-post-content"> <h4> <a href="/news/5278687/">FFmpeg中AVPacket与AVFrame核心解析:内存、生命周期与音视频数据流</a> </h4> <div class="popular-post-date">2026/10/4 1:01:58</div> </div> </li> <li> <div class="popular-post-img">网站建设</div> <div class="popular-post-content"> <h4> <a href="/news/5278694/">Java课设学生信息管理系统:JDBC+MySQL完整实现与避坑指南</a> </h4> <div class="popular-post-date">2026/10/4 1:02:01</div> </div> </li> <li> <div class="popular-post-img">网站建设</div> <div class="popular-post-content"> <h4> <a href="/news/5278704/">空战对抗中的Q学习:从Q表到状态离散化与奖励塑形实战解析</a> </h4> <div class="popular-post-date">2026/10/4 1:02:07</div> </div> </li> <li> <div class="popular-post-img">网站建设</div> <div class="popular-post-content"> <h4> <a href="/news/5278741/">JavaWeb仓库管理系统源码解析:部署、核心模块与二次开发</a> </h4> <div class="popular-post-date">2026/10/4 1:02:51</div> </div> </li> <li> <div class="popular-post-img">网站建设</div> <div class="popular-post-content"> <h4> <a href="/news/5278750/">相衬成像原理与实操:让透明活细胞清晰可见</a> </h4> <div class="popular-post-date">2026/10/4 1:02:58</div> </div> </li> <li> <div class="popular-post-img">网站建设</div> <div class="popular-post-content"> <h4> <a href="/news/5278753/">基于HGE引擎的水浒传街机游戏源码分析与二次开发实战</a> </h4> <div class="popular-post-date">2026/10/4 1:02:59</div> </div> </li> </ul> </div> <div class="sidebar-widget"> <h3>文章分类</h3> <ul class="categories-list"> <li> <a href="/jianzhan">网站建设 <span>(12)</span></a> </li> <li> <a href="/sheji">设计思考 <span>(8)</span></a> </li> <li> <a href="/fenxiang">技术分享 <span>(15)</span></a> </li> <li> <a href="/anli">项目案例 <span>(6)</span></a> </li> <li> <a href="hangye">行业动态 <span>(5)</span></a> </li> </ul> </div> <div class="sidebar-widget"> <h3>标签云</h3> <div class="tag-cloud"> <a href="/tags/web">网站建设</a> <a href="/tags/xiangying">响应式设计</a> <a href="/tags/tiyan">用户体验</a> <a href="/tags/seo">SEO优化</a> <a href="/tags/web">前端开发</a> <a href="/tags/xiaochengxu">小程序</a> <a href="/tags/dianshang">电商平台</a> <a href="/tags/youhua">性能优化</a> </div> </div> </div> </div> </div> </section> <!-- 相关文章 --> <section class="related-posts"> <div class="container"> <div class="section-title"> <h2>相关文章</h2> <p>您可能感兴趣的其他内容</p> </div> <div class="related-grid"> <div class="related-card"> <div class="related-img">【DeepSeek Harness 研究】用“一切皆插件”范式拆解 LLM Agent Harness 的可复制配置</div> <div class="related-content"> <div class="related-meta"> <span class="related-date">2026/10/4 20:15:29</span> <span class="related-category">网站建设</span> </div> <h3>【DeepSeek Harness 研究】用“一切皆插件”范式拆解 LLM Agent Harness 的可复制配置</h3> <p> /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …... </p> <a href="/news/5296003/" class="related-read-more" >阅读更多 →</a > </div> </div> <div class="related-card"> <div class="related-img">HFSS微带天线仿真全流程:从建模到实测对标</div> <div class="related-content"> <div class="related-meta"> <span class="related-date">2026/10/4 20:11:57</span> <span class="related-category">网站建设</span> </div> <h3>HFSS微带天线仿真全流程:从建模到实测对标</h3> <p> 1. 项目概述&#xff1a;从零开始跑通一个能发能收的微带贴片天线仿真全流程HFSS——全称High Frequency Structure Simulator&#xff0c;是射频与微波领域工程师手里最硬核的“电子显微镜”。它不画电路图&#xff0c;不写代码&#xff0c;而是直接在三维空间里“雕刻”电磁场…... </p> <a href="/news/5295965/" class="related-read-more" >阅读更多 →</a > </div> </div> <div class="related-card"> <div class="related-img">那行藏在代码里的开关,让我在客户面前圆了一场大谎</div> <div class="related-content"> <div class="related-meta"> <span class="related-date">2026/10/4 20:06:53</span> <span class="related-category">网站建设</span> </div> <h3>那行藏在代码里的开关,让我在客户面前圆了一场大谎</h3> <p> 上个月我陪一个客户做版本上线复盘&#xff0c;翻代码的时候无意间看到一行注释&#xff0c;写着"这个开关别动&#xff0c;动了线上出大事"。我当时心里咯噔一下——不是因为发现了什么惊天秘密&#xff0c;而是这句话让我想起自己刚入行那会儿&#xff0c;也被一行…... </p> <a href="/news/5295936/" class="related-read-more" >阅读更多 →</a > </div> </div> </div> </div> </section> <!-- CTA区域 --> <section class="cta"> <div class="container"> <h2>需要专业的网站建设服务?</h2> <p> 联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标 </p> <a href="/index.html#contact" class="btn">立即咨询</a> </div> </section> <!-- 页脚 --> <footer> <div class="container"> <div class="footer-content"> <div class="footer-column"> <h3>创想科技</h3> <p>专业网站建设与数字营销解决方案提供商,助力企业数字化转型。</p> </div> <div class="footer-column"> <h3>服务项目</h3> <ul> <li><a href="#">企业官网建设</a></li> <li><a href="#">电商平台开发</a></li> <li><a href="#">小程序开发</a></li> <li><a href="#">SEO优化</a></li> <li><a href="#">数字营销</a></li> </ul> </div> <div class="footer-column"> <h3>快速链接</h3> <ul> <li><a href="/index.html">首页</a></li> <li><a href="/index.html#portfolio">成功案例</a></li> <li><a href="blog.html">建站日记</a></li> <li><a href="/index.html#about">关于我们</a></li> <li><a href="/index.html#contact">联系我们</a></li> </ul> </div> <div class="footer-column"> <h3>联系我们</h3> <ul> <li>电话:400-123-4567</li> <li>邮箱:contact@chuangxiang.com</li> <li>地址:北京市朝阳区科技园区创新大厦A座10层</li> </ul> </div> </div> <div class="footer-bottom"> <p>© 2023 创想科技 版权所有 | 京ICP备12345678号</p> </div> </div> </footer> <script> // 移动端菜单切换 document .querySelector(".mobile-toggle") .addEventListener("click", function () { document.querySelector(".nav-menu").classList.toggle("active"); }); // 平滑滚动 document.querySelectorAll('a[href^="#"]').forEach((anchor) => { anchor.addEventListener("click", function (e) { e.preventDefault(); const targetId = this.getAttribute("href"); if (targetId === "#") return; const targetElement = document.querySelector(targetId); if (targetElement) { window.scrollTo({ top: targetElement.offsetTop - 80, behavior: "smooth", }); // 移动端点击后关闭菜单 if (window.innerWidth <= 768) { document.querySelector(".nav-menu").classList.remove("active"); } } }); }); </script> </body> </html>