尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
C# + Selenium + Chrome无头模式爬虫实战:核心坑点与避坑方案
聊聊C# Selenium Chrome无头模式爬虫里的那些坑先说结论Selenium做爬虫难度不在于写代码而在于跑起来之后它不按你预期走。我之前用C#写了一套基于Selenium Chrome无头模式的数据采集脚本前后折腾了几周踩过的坑能排成一条街。今天不聊高大上的框架就只讲C#实战里那些最要命的细节——从环境搭建到上线稳定运行把能避开的坑尽量给你指出来。这篇内容适合谁看如果你正在用C#写爬虫想用Chrome无头模式做自动化数据采集或者已经在用Selenium但被各种诡异问题折磨那这篇文章就是为你准备的。读完你至少能避开我当初跳进去的大部分坑。1. 环境与依赖准备搞C# Selenium首先绕不开包引用的问题。项目创建好之后用NuGet装这么几个包就够开始干活了Selenium.WebDriverSelenium.WebDriver.ChromeDriverSelenium.Support可选用于WebDriverWait等扩展这三个包是基础配置。Selenium.WebDriver.ChromeDriver这个包的作用是帮你下载对应版本的ChromeDriver它会根据你当前Chrome浏览器的版本自动匹配一个驱动版本。但这里就有坑——它在某些网络环境下载驱动非常慢甚至卡住不动。我遇到过好几次明明代码没问题就是驱动下载不下来整个环境卡在启动阶段。提示如果NuGet下载ChromeDriver卡住直接用ChromeDriver镜像站手动下载然后把驱动文件放到项目output目录下用ChromeDriverService.CreateDefaultService(驱动目录)来指定路径。这样最稳。另一个比较隐蔽的坑是版本匹配。Chrome浏览器是自动更新的今天用的还是129版本过两个星期刷新就变成131了。如果你用的ChromeDriver版本和浏览器版本差了太多启动时直接报版本不匹配的异常连排查的方向都很迷惑。比较好的做法是锁定驱动的版本同时把Chrome的自动更新关掉或者每次更新浏览器后同步检查ChromeDriver版本。Chrome 117版本之后ChromeDriver的下载机制发生了变化几乎每个小版本都有独立的驱动文件必须一一对应千万别抱有“差不多能用”的侥幸心理。我现在的做法是在代码里加一个启动时的驱动版本校验函数启动前先读取Chrome浏览器的版本号再去驱动目录里确认版本是否匹配不匹配就抛异常提示更新。这比报错后再去排查要省时间得多。环境方面还需要注意无头模式运行在服务端的时候整台机器最好有虚拟显示器环境或者至少系统不能完全锁屏断会话。如果是Windows系统建议用任务计划程序配置一个独立会话去跑这样断开了远程桌面任务也会照常执行。Linux服务器上如果跑无头模式一般不需要额外装XvfbChrome自身的无头模式已经能处理渲染任务。2. 核心原理入门讲原理之前先明确一个基础概念Selenium的本质不是“爬虫工具”而是一个浏览器自动化工具。它做的事情是严格按照W3C WebDriver协议向浏览器驱动ChromeDriver发送标准化的HTTP请求然后驱动再把指令转成浏览器能理解的动作比如打开页面、点击按钮、填写表单、获取页面源码。好多人不理解这一点就会在遇到问题时钻牛角尖。比如你让Selenium去执行一段JS代码结果返回值是一个复杂对象但拿到的却是字符串。深刻理解了“Selenium只是个管道”这个事实后你就会明白它不负责处理JS返回值的类型转换你拿到字符串反而正常进一步处理得靠你自己。无头模式是Chrome提供的一种启动参数形态它不需要真实的窗口界面但浏览器引擎依然会完整地执行页面渲染流程、加载视频、跑JavaScript。有头模式和无头模式的对比大致如下对比项有头模式无头模式资源占用更高需要渲染窗口更低省去窗口绘制运行速度较慢较快被反爬识别概率相对较低相对较高调试便利性能看到页面实时变化只能通过截图调试适用场景调试代码阶段正式采集任务初次尝试无头模式的常见误区是认为无头模式完全等同于把浏览器的图形界面藏起来了。实际上旧的Headless模式Chrome 112以前用的是独立的精简实现很多特征和完整版Chrome不一致网站用脚本一查就容易暴露。好在Chrome 112以后推出了新的Headless模式本质就是运行框架一致、只是不绘制窗口兼容性问题大幅减少这也是为什么我建议你阅读资料时尽量找比较新的文章——老教程里的坑在现在的新版本里可能已经不存在了。从反爬识别角度来说无头模式的核心软肋是浏览器指纹特征。比如有些网站会用Canvas去绘一段特定图形然后比较普通浏览器和无头模式绘制出来的渲染结果还有一些网站通过WebGL的getParameter参数判断是否为无头模式。虽然这一部分的需求因网站而异但理解了原理之后你至少能知道网站的“识别”不是玄学是有一套可追踪的规则的。3. 实战演练采集一个列表页我拿一个实际案例来说明整个实现思路。假设需要对某个数据网站做一个列表页采集页面内容包含标题、内容摘要、发布时间和详情页链接。数据量比较大大概有几百页的自定义翻页我选择用C# Selenium无头模式来跑完整套流程。先梳理整个项目的核心逻辑打开列表页等待页面内容渲染完成提取当前页的目标数据点击下一页继续循环全部轮询结束后保存数据代码层面引入Selenium之后的启动流程大致是var options new ChromeOptions(); options.AddArgument(--headlessnew); // 使用新版无头模式 options.AddArgument(--disable-gpu); options.AddArgument(--no-sandbox); options.AddArgument(--disable-dev-shm-usage); options.AddArgument(--user-agentMozilla/5.0 ...); options.AddArgument(--window-size1920,1080); using var driver new ChromeDriver(options); driver.Navigate().GoToUrl(https://example.com/list);这些启动参数是起步配置。下面逐个解释我为什么这么设--headlessnew只对Chrome 112及以上版本有效。老版本用这个参数会报错或自动忽略降级为旧模式。--disable-gpu纯保险实际上新版Chrome在无头模式下已经不太依赖GPU了但加上这个参数能减少一部分硬件环境带来的诡异问题。--no-sandbox在root用户或部分容器环境中必须加否则Chrome会拒绝启动。--disable-dev-shm-usage当你看到Chrome崩溃或者页面加载异常时多半是共享内存不够加上这个参数能规避掉相当一部分问题。--window-size1920,1080设置在无头模式下浏览器视口的大小。如果你不设置无头模式默认窗口尺寸可能是800x600有些网站会因此加载移动端布局选择器全部失效。我强烈建议所有项目都显式设置这个参数。启动之后进入等待页面渲染的环节。这里有一个原则能不用Thread.Sleep就不用。固定休眠看起来很省事但网络快慢、机器负载都会影响实际渲染时间固定等待要么等太久浪费效率要么等不够导致拿不到元素。比较正确的方案是显式等待var wait new WebDriverWait(driver, TimeSpan.FromSeconds(10)); IWebElement titleElement wait.Until(d d.FindElement(By.CssSelector(div.item h2 a)));显式等待的“等待条件”有很多种比如元素存在、元素可见、元素可点击、指定的JS函数返回特定值等。我习惯优先等“元素存在且可见”这两个条件其他的按业务需要再加。再来看元素提取。写爬虫最忌讳选择器写得过于具体比如body div.container div:nth-child(3) div ul li:nth-child(2) a这种路径依赖一改版就失效维护成本非常高。我一般用CSS选择器优先配合相对定位来写var items driver.FindElements(By.CssSelector(div.list-item)); foreach (var item in items) { string title item.FindElement(By.CssSelector(h2.title)).Text; string summary item.FindElement(By.CssSelector(p.summary)).Text; string link item.FindElement(By.CssSelector(a)).GetAttribute(href); }这里提一个值得一提的细节获取文本时用.Text属性拿到的内容通常经过了浏览器的排版计算比较干净而用GetAttribute(textContent)拿到的是原始的文本内容可能包含多余的空格和换行。两者到底用哪个需要根据页面结构来判断。若遇到隐藏元素的文本.Text拿不到内容就要靠GetAttribute(textContent)。翻页处理的写法大致是while (true) { // 提取当前页的数据 // 判断是否存在下一页按钮 if (!IsNextPageExists(driver)) break; driver.FindElement(By.CssSelector(a.next_page)).Click(); wait.Until(d /* 判断页面内容发生变化的条件 */); }这里也有一个非常典型的坑点击下一页之后页面可能是部分刷新的也可能整个URL都变了还有可能打开了一个新标签页。不同的情况处理方式完全不同。如果网站在新标签页打开链接你得先用driver.SwitchTo().Window(driver.WindowHandles.Last())切换过去。点击下一页时我建议不要直接Click()更安全的是用IJavaScriptExecutor去执行element.Click()有些页面里按钮被遮住了普通的WebDriver点击会报元素不可交互反而是JS注入点击一下就成功了。等数据全部抓取结束后写入文件或数据库即可这部分逻辑不复杂。需要留个心眼的是文件编码C#写文件默认UTF-8基本没问题但如果你要交给Excel处理建议统一转成UTF-8带BOM格式否则Excel打开会出现乱码。4. 避坑工具箱以下内容是我在实战中反复踩坑总结出的核心避坑经验按照踩坑频率和影响程度排列。4.1 无头模式被识别这是无头模式遇到概率最大的一类问题。网站通过JS检测你是否处于无头状态常见的手段包括检测navigator.webdriver是否为trueCanvas指纹检测WebGL渲染器名称检测浏览器插件数量、字体列表检测是否有window.chrome对象应对思路有几个层次。最低层是加--disable-blink-featuresAutomationControlled参数这个参数会修改Blink引擎的特征信息让navigator.webdriver保持为false。我在实际测试中加了这个参数之后被识别率下降非常明显。再往上层走可以通过ExecuteScript在页面加载前预先修改属性var js (IJavaScriptExecutor)driver; js.ExecuteScript(Object.defineProperty(navigator, webdriver, {get: () undefined}););这个方案主要是在页面加载完成之后去修改属性有效的关键在于脚本要在网站执行检测之前运行。Selenium搞不定这个太早的时机配合Chrome扩展或者代理脚本能做到更好的效果。如果采集目标网站的反爬机制更强比如返回一个验证码页面要求你拖拽滑块、点击文字纯靠Selenium去硬搞就会非常吃力。我的建议是及时止损优先评估是否值得用更复杂的方案比如切换成Playwright或者接打码平台。纯粹的C# Selenium在遇到强人机验证时性价比不高。4.2 动态内容的等待策略页面中有接口返回数据之后才去渲染列表这类页面是所有爬虫都躲不开的。等待策略做得不好采集的数据时多时少甚至抓到空页面。此前我提到过固定等待的弊端这里再展开讲一讲正确的等待策略应该是“你清楚等什么”之后才去等待。等一个元素出现你要等的是“这个元素已经代表了页面渲染完成”。常见的选择器wait.Until(d d.FindElements(By.CssSelector(div.list-item)).Count 0);如果页面是用无限滚动加载列表的还需要模拟滚动触发加载。常见做法是循环执行JS滚动到页面底部然后等待新数据追加进来。需要注意的是在无头模式下滚动事件会出现直接触发但不加载内容的情况——有些网站的懒加载依赖于视口检测而你滚得太快浏览器还没来得及触发加载又继续往下了。此时比较好的节奏是每滚动一次等个0.2到0.5秒再滚动下一次。等待加载完成还有一个常用的技巧检查页面中某个Loading元素是否存在wait.Until(d d.FindElements(By.CssSelector(.loading-indicator)).Count 0);这个方法在某些场景下比直接等列表元素更好用因为它是从业务逻辑角度判断整个加载过程是否结束。4.3 iframe陷阱遇到嵌套iframe的内嵌页面如果你直接FindElement查找内容大概率是找不到的。Selenium默认会操作当前最近的文档上下文而iframe里的元素对你来说是“另一个世界”的。处理思路是先切换到目标iframedriver.SwitchTo().Frame(iframe_name_or_id);操作完再切换回默认内容driver.SwitchTo().DefaultContent();比较麻烦的是嵌套多层iframe的情况你需要一层一层切进去。我踩过的坑是每次切换之后如果马上又切换到另一个iframe可能因为默认内容上下文不对而报错。写一个通用的切换函数将iframe路径作为参数传进去就能避免不少问题。4.4 下载文件路径控制用Selenium触发浏览器下载Excel或PDF时如果没有预先配置下载目录默认是进到系统指定的下载文件夹并且还可能弹出来一个下载询问框导致下载卡住。这类问题解决办法很直接options.AddUserProfilePreference(download.default_directory, downloadPath); options.AddUserProfilePreference(download.prompt_for_download, false);第一行指定下载目录第二行禁止弹窗询问。如果页面触发的是单文件下载你还需要在代码里做一个轮询判断目标目录下是否出现了新文件且文件名是否符合预期再继续后续逻辑。4.5 内存泄漏问题长期跑无头模式不断打开新页面、不断执行脚本你会发现内存占用越来越高最终系统变慢甚至爬虫本身崩溃。Chrome的每个Tab都是一个独立进程页面的Close和置空操作有时候不会立刻释放全部内存。比较实用的处理策略是每个采集批次完成后主动调用driver.Manage().Cookies.DeleteAllCookies()。定期重启Driver比如每运行50个页面就销毁当前Driver实例重新new一个。页面不再需要的窗口及时driver.Close()并切换到其他窗口。这些方法不能彻底杜绝内存泄漏但可以把单次运行的内存消耗控制在一个合理范围内。如果是常驻型任务建议做成定时任务单轮退出不要指望一个进程无限跑下去。4.6 页面加载超时无头模式下页面加载受网络环境影响特别大。遇到一个阻塞资源加载导致页面卡住时Selenium的默认行为非常不优雅——它会一直等直到WebDriver超时。限制页面加载时间的方法是driver.Manage().Timeouts().PageLoad TimeSpan.FromSeconds(30);如果页面加载超过30秒WebDriver会抛出一个超时异常。你可以在异常处理中强制刷新页面或者直接跳过这一页继续后面的逻辑。有时候某个页面里的第三方统计脚本加载很慢你甚至可以设置脚本超时时间driver.Manage().Timeouts().AsynchronousJavaScript TimeSpan.FromSeconds(10);如果整个页面有大量外部资源加载慢还可以优先考虑拦截外部资源请求。用--host-resolver-rules参数可以屏蔽特定域名或者用--blink-settingsimagesEnabledfalse禁用图片加载。这些参数配合起来能显著提升页面加载速度但注意别过度优化——如果网站把关键数据也通过动态资源返回禁掉就废了。4.7 代理与反爬策略——这一块必须特别说明很多反爬问题本质上不是技术问题而是工程层面的规划问题。当你的采集频率过高时无论用什么浏览器自动化方案都没有意义。我见过一台机器同一时间跑几十个WebDriver实例去采集同一个目标站没过五分钟就被封了IP。合理的采集频率应当加上随机延迟并且用小规模并发去试探目标网站的容忍度。对于“封IP”问题最稳妥的方案是使用合规的代理服务或者数据中心IP池——但这一块涉及合规要求不展开讲。核心原则是不要挑战一个网站的底线采集纪律远比技术手段重要。4.8 关于验证码现在主流网站基本都有人机验证。Selenium对付纯数字、纯文字类的验证码可以通过截图裁剪后用OCR识别成功率看验证码图片质量。滑块验证码和点选验证码就需要专门的算法比如轨迹拟合或者图像目标检测。这些方案不是不能做但C#生态里现成代码相对少自己实现一遍的成本很大。我的建议是如果网站重要性高、采集量又大尽早评估第三方打码平台或者换用浏览器自动化的其他工具别在验证码这个环节耗太久。5. 常见问题与排查实录我整理了一份很常见的Selenium无头模式排查速查表按照频率从高到低排列问题现象可能原因排查与解决步骤启动时报ChromeDriver版本异常ChromeDriver和Chrome版本不匹配查看Chrome版本下载对应版本的ChromeDriver或关掉Chrome自动更新启动时提示“Chrome failed to start”缺少启动参数或权限不足添加--no-sandbox、--disable-dev-shm-usage确认驱动有可执行权限定位不到元素页面动态渲染未完成或iframe未切换改用显式等待检查是否在iframe内是则先切换上下文元素存在但点击无效元素被遮挡或不可交互用JS执行点击IJavaScriptExecutor.ExecuteScript(arguments[0].click();, element)页面一直加载中外部资源阻塞设置PageLoad超时或禁用图片及其他阻塞资源抓取的数据不全/为空懒加载没触发或窗口太小加载移动端布局设置大窗口尺寸模拟滚动触发懒加载脚本跑一段时间后卡死内存泄漏或页面异常定期重启Driver主动清理Cookies单轮任务退出Cookie登录失效登录态过期或被网站主动清理每次任务开始前重新登录并保存Cookie再复用排查问题有一个很基础但很高效的方法——先把无头模式关闭在有头模式下人工跑一遍代码看页面实际表现。很多问题在无头模式下表现诡异但一开有头模式就暴露了真相。无头模式只是少了界面不代表它应该和“剥掉界面的普通Chrome”行为完全一致一旦出现行为偏差我第一件事是开着有头模式去复现。再有一个实用的小技巧给代码里关键节点加日志。不要直接输出到控制台就完事写进日志文件里包括当前URL、页面标题、循环次数、已获取的元素数量、耗时。排查问题时日志几乎是救命稻草。我之前遇到过一个问题是某次页面改版导致列表选择器失效但没有日志脚本只是默默跑完了数据一条没抓到还白白占了几小时机器资源。加了数据量日志之后每次循环都在观测数据条数有问题立刻能发现。6. 性能优化与稳定运行的长期思路无头模式跑爬虫大多数时候不是搞定一个页面就结束的而是要持续运行一个比较长的时间。这时候稳定性和性能就成了比功能更重要的核心命题。性能方面首要优化是页面加载策略。Chrome支持的加载策略有normal、eager和none三种options.PageLoadStrategy PageLoadStrategy.Eager;Eager策略的意思是在页面DOM加载完成后就结束等待不去等所有脚本、图片等子资源都加载完。对采集来说很多时候关键数据已经在DOM里了这个策略能把整体速度提升接近一倍。如果你的采集目标完全不需要执行页面里的异步脚本直接选None策略效率还能更高一些。其次是资源拦截。可以设置扩展将不需要的脚本或图片请求全部拦截掉。没有展开讲是因为这个方案依赖每个网站的具体资源和URL规则得自己调试匹配。稳定性方面有个我以前不重视后来发现很关键的细节——让采集任务具备“断点续跑”能力。当脚本中途崩溃重启任务时不应该从头开始而是应该知道上次采集到多少页了。实现方式不复杂在循环的每一步都记录当前页码到本地文本文件重新启动时读取这个状态。这个机制看起来朴素但实际运行体验会好非常多。另外异常处理不要只是捕获后打印日志还要有明确的降级逻辑。比如某个页面加载失败是重试一次就跳过还是整个任务终止我一般分两级处理单页失败连续重试三次三次都失败就跳过去记入失败列表。如果失败率超过50%则判定为目标网站反爬触发任务立即暂停并等待一个较长的冷却时间再继续。int retryCount 0; const int maxRetry 3; while (retryCount maxRetry) { try { // 加载并采集页面 break; } catch (WebDriverTimeoutException) { retryCount; if (retryCount maxRetry) { // 记录失败 // 短暂冷却后继续 } } }这段代码几乎是整个爬虫稳定运行的骨架。别小看这种简简单单的重试逻辑没有它线上运行任务基本撑不过半天。最后再提一个不是每个人都有需求但确实很实用的点抓取到的数据尽量以“原始状态”落地一次不要清洗后再落地。原因是后期处理数据时常常发现当初以为的“脏数据”其实是唯一的原始信息。数据清洗放在采集之后单独做反而是最稳的架构。我在实际运行中还发现把采集过程中的HTML快照保留下来也很值得推荐。尤其遇到网站改版、选择器失效的时候有快照就能对比出旧版页面和当前页面的结构差异快速定位问题不用重新开着浏览器去对比。最后的几点碎碎念如果把C# Selenium无头模式的爬虫开发浓缩成一句话我觉得是跑的慢不怕跑不稳才可怕。无头模式的优势在于效率但效率不等于盲目追求速度一套具备合理的等待策略、稳定的重试机制、完整日志记录的采集方案远胜过一个看似飞快上线即崩的方案。从我个人的实操经验来说有一件事值得反复强调——在正式编写爬虫代码之前先去浏览器的开发者工具里手动研究一遍目标页面结构确认数据是怎么加载来的翻页的逻辑是改URL还是发Ajax页面是否存在iframe。把这个完成之后写代码只是把已知的步骤机械翻译的过程真正踩坑的概率会大幅下降。另外一点私人建议是尽量在项目早期就加入异常状态的可观测性。不需要过度工程化到接入监控告警平台但至少要做到日志能看到采集进度、失败数据能定位到URL、异常能通过邮件或者IM消息推送出来。否则一台服务器上静默运行着的爬虫某一天开始悄悄挂掉你都不知道应该从哪里查起。这篇文章写下来很多场景都是我真刀真枪跑过的案例。其中一些问题的解决方案并不是唯一的比如无头模式被识别的问题不同网站对参数的敏感度不一样。如果你在实际复现过程中遇到和我不一样的情况不妨从“浏览器特征差异”这个角度去排查往往能打开新思路。愿你少踩几个坑我们的目标是一致的花最少的时间稳定地拿到想要的数据。
RELATED

相关推荐

JdbcTemplate setFetchSize 分批拉取数据的效果确认方法

JdbcTemplate setFetchSize 分批拉取数据的效果确认方法

用 Spring 的 JdbcTemplate 流式导出大数据量(几十万行写 CSV)时,标准做法是在 PreparedStatement 上设 setFetchSize,希望驱动按批拉取、内存里只驻留少量行。但 rs.getFetchSize() 只是回显了设置值,堆内存该涨还是涨。怎么确认分批真的生效了,本文给出可操作的验证手段…

📅 2026/10/23 5:21:45
ArcPy批量迁移MDB到FileGDB实战指南

ArcPy批量迁移MDB到FileGDB实战指南

简介:本资源是一份面向GIS开发人员与ArcGIS桌面用户(尤其是ArcMap使用者)的自动化数据迁移实践指南,聚焦解决个人地理数据库(.mdb)向文件地理数据库(.gdb)批量转换的效率与可靠性问题…

📅 2026/10/23 5:16:44
ArcPy自动化迁移MDB到GDB:解决路径、权限与空间完整性问题

ArcPy自动化迁移MDB到GDB:解决路径、权限与空间完整性问题

简介:本资源是一份面向GIS开发人员与ArcGIS桌面用户(尤其是ArcMap使用者)的自动化数据迁移实践指南,聚焦解决个人地理数据库(.mdb)向文件地理数据库(.gdb)批量转换的效率与可靠性问题…

📅 2026/10/23 5:16:44
MORE NEWS

更多资讯

📰

从一个 HelloWorld 到 49 个示例模块:LangChain4j 在 Java LLM 应用开发中的完整实践路径

从一个 HelloWorld 到 49 个示例模块:LangChain4j 在 Java LLM 应用开发中的完整实践路径 【免费下载链接】langchain4j-examples 项目地址: https://gitcode.com/GitHub_Trending/la/langchain4j-examples langchain4j-examples 是 LangChain4j 框架的官方…

📰

TradeX-TPS交易插件:轻量级协议翻译器与跨语言调用实践

简介:TradeX-TPS交易插件系统v1.0是一款面向量化交易开发者与券商系统集成人员的轻量级股票自动化交易工具,专为适配大智慧、通达信等主流行情软件设计,解决第三方程序对接券商交易通道难、接口兼容性差、功能扩展受限等实际问题。资源包共20…

📰

洛谷-P3650 [USACO1.3] 滑雪课程设计 Ski Course Design 题解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

用Codex和Chromatic复刻街机游戏:AI辅助开发实践记录

用 Codex 在 Chromatic 上复刻街机游戏:AI 辅助开发的实践记录最近在做一个复古游戏复刻的项目,核心思路是用 Codex 生成代码,跑在 Chromatic 引擎上,目标是还原经典街机游戏的玩法和手感。这个组合很有意思:Codex 负责…

📰

洛谷-B3930 [GESP202312 五级] 烹饪问题 题解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

【opencv】Opencv python: seamlessClone泊松融合 (我把Lena变成了彼岸花怪/(ㄒoㄒ)/~~)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬