
上个月有个老客户王总找我说他们市场部的小姑娘每天要花两个小时爬30多个工控行业网站的新闻然后手动整理成100字左右的摘要发给老板看。小姑娘快疯了天天跟老板抱怨老板就找到我问能不能做个工具自动搞。我当时想这还不简单爬虫爬一下然后调个AI生成摘要半天就能搞定。结果我他妈整整搞了两个星期差点把我搞疯。一开始我用的是HttpClient加HtmlAgilityPack这是我以前写爬虫最常用的组合速度快资源占用低。结果跑起来才发现现在的网站他妈全是Vue、React写的HttpClient爬回来的就是个空壳子除了head里的标题body里啥都没有。我当时就懵了心想这才几年没写爬虫怎么世界都变了。然后我就想到了Selenium以前搞自动化测试的时候用过能模拟真实浏览器。结果一开跑我就傻了开一个Chrome浏览器要5秒加载一个页面要3秒30个网站每个网站10篇文章跑下来要半个多小时。王总给我打电话说他早上8点要看到摘要结果我这程序7点半开始跑8点10分才跑完老板都到公司了摘要还没出来。王总把我骂了一顿说我做的什么垃圾东西还不如手动快。我当时脸都红了挂了电话就开始找替代方案。然后就发现了Playwright微软出的比Selenium新速度也快。我赶紧下了Nuget包写了个demo试了一下果然比Selenium快多了开浏览器只要2秒加载页面也快。我当时还挺开心觉得这下问题解决了。结果第二天就遇到了Cloudflare的检测爬中国工控网的时候直接返回403页面上写着Request blocked by Cloudflare。我他妈从早上9点调到下午3点饭都没吃就吃了个红烧牛肉味的泡面。试了各种方法改User-Agent加Referer用代理IP结果都不行。Cloudflare跟成精了一样一眼就能看出来我是无头浏览器。后来在GitHub上翻了半天看到有人说要改navigator.webdriver这个属性还要改plugins数组改screen分辨率改各种浏览器指纹。我照着改了一下终于绕过了检测。当时我激动的一拍桌子把旁边的水杯碰倒了水全洒在键盘上又花了半个小时擦键盘空格键到现在还有点粘。搞定了爬取的问题我以为终于可以松口气了结果更大的坑在后面等着我。怎么把正文从HTML里提取出来一开始我想的是写XPath规则每个网站写一套反正也就30个网站半天就能写完。结果我刚写完5个网站的规则王总就给我发微信说有个网站改版了原来的XPath全废了爬下来的全是乱码。我当时就想骂人这网站没事改什么版啊闲的蛋疼。然后我就想有没有什么库能自动提取正文不用写规则然后就找到了ReadabilitySharp据说是从Mozilla的Readability移植过来的能自动识别网页的正文部分。我赶紧集成进去试了几个网站效果还不错。结果试到中国工控网的时候我直接傻眼了。ReadabilitySharp把底部的10条相关推荐和20条网友评论都当成正文了提取出来的内容有一半是楼主说得对、“这个设备我用过质量不错”、“哪里有卖的这种废话。然后我把这些内容传给AI生成摘要结果AI生成的摘要是网友们纷纷表示这个设备质量不错值得购买”。王总看到这个摘要直接给我打了微信电话语气特别差说我做的什么垃圾东西还不如小姑娘手动整理的。我挂了电话气的把鼠标摔了。然后把ReadabilitySharp的源码拉下来看看了半天也没看懂它的提取逻辑越看越气。这时候我突然想到既然要用AI生成摘要那为什么不让AI直接从HTML里提取正文呢AI不是很牛逼吗连上下文都能理解提取个正文还不是小菜一碟。一开始我想用ChatGPT的API调用gpt-3.5-turbo效果肯定好。结果王总一口回绝了说他们公司的所有数据都不能出内网怕泄露商业机密。而且OpenAI的API太贵了调用一次要几分钱一天爬300篇文章一个月下来要几千块王总舍不得。然后我就想到了本地部署大模型。现在开源的大模型这么多效果也不差。我选了Qwen2-7B-Instruct阿里出的开源免费而且对中文支持特别好。7B的参数普通的电脑就能跑。王总那边用的是研华的IPC-610L工控机i5-10400的CPU16G内存没有独立显卡。我一开始担心跑不动结果用Ollama部署了一下试了试生成一篇100字的摘要大概10秒左右完全在可接受范围内。Ollama真的是个好东西部署太简单了。去官网下个安装包一路下一步然后打开命令行输入ollama run qwen2:7b-instruct它就会自动下载模型然后启动服务。启动完之后会自动在本地开一个API接口地址是http://localhost:11434/api/generate用HttpClient就能调用特别方便。那些差点把我搞死的坑提示词真的是爹少一个字效果都差十万八千里。一开始我写的提示词特别简单“请为以下文章生成摘要”。结果生成的摘要要么太长有300多字要么抓不住重点有的甚至会编造内容。比如有一篇文章讲的是西门子2026年3月发布新一代S7-1500 PLC性能提升30%“结果AI生成的摘要里写着西门子新一代S7-1500 PLC支持5G通信和边缘计算将广泛应用于智能制造领域”。我当时就懵了文章里根本没提5G和边缘计算啊这AI怎么还会瞎编呢然后我就开始改提示词改了不下20次。加了不要添加任何文章中没有的信息加了摘要长度控制在80-120字之间加了只保留核心信息事件主体、发生时间、主要内容、行业影响。最后终于得到了一个能用的提示词。有一次我手贱把提示词里的绝对不要编造内容改成了尽量不要编造内容结果AI又开始瞎编了我赶紧改了回来。然后是并发的坑。一开始我为了提高速度用了Parallel.ForEach一下子开了20个线程。结果Playwright同时开了20个Chrome浏览器每个浏览器占500M内存20个就是10G再加上大模型推理占的5G内存直接把工控机的16G内存干爆了蓝屏了。那天是4月5号清明节我在家休息正跟家人吃饭呢。王总一个电话打过来语气特别凶说生产线停了工控机蓝屏了让我赶紧远程过去看看。我当时吓得一身冷汗饭都没吃完赶紧打开电脑远程。重启工控机之后看了一下系统日志就是System.OutOfMemoryException。我当时真想抽自己两个嘴巴子怎么这么蠢工控机的内存又不是服务器的怎么能开这么多线程。然后我赶紧改成了信号量限制最多同时开3个线程。别问我为什么是3个4个内存就会到90%5个就蓝屏我试了10次得出来的。改完之后内存稳定在10G左右再也没蓝屏过。但是速度也慢了下来原来20分钟跑完的现在要40分钟。不过王总说没关系只要早上8点能出来就行他可以让程序7点20开始跑。还有异常处理的坑。有的网站会时不时打不开有的页面加载会超时有的文章内容特别长有几万字超过了大模型的上下文窗口推理的时候会特别慢甚至超时。还有大模型有时候会抽风返回空字符串或者返回一堆乱码。这些情况如果不处理程序跑着跑着就崩了。我加了超时处理每个页面加载最多等10秒超时就跳过。加了长度限制文章内容超过5000字就截断只取前5000字因为一般新闻的核心信息都在前面后面的都是废话。加了重试机制调用大模型失败的话重试3次还是不行就跳过。加了日志用Serilog把每次爬取的结果、错误信息都记下来方便排查问题。最终能用的代码废话不多说直接上代码。都是我踩了无数坑才写出来的直接复制就能用。别问我为什么有些地方写的这么奇怪都是被逼的。usingMicrosoft.Playwright;usingAngleSharp;usingAngleSharp.Html.Parser;usingSystem.Net.Http.Json;usingSystem.Text.Json;usingSystem.Threading;usingSystem.Collections.Generic;usingSystem.Threading.Tasks;usingSystem.IO;usingSerilog;// 配置日志别问我为什么用Serilog比微软自带的好用100倍Log.LoggernewLoggerConfiguration().MinimumLevel.Information().WriteTo.Console().WriteTo.File(logs/log-.txt,rollingInterval:RollingInterval.Day).CreateLogger();// 要爬的网站列表自己加varurlsnewListstring{https://www.gongkong.com/news/,https://www.automation.com.cn/news/,https://www.plc.cn/news/};// 信号量最多3个线程再多内存就炸了varsemaphorenewSemaphoreSlim(3,3);varresultsnewListNewsSummary();// 初始化Playwright这里必须用await不然会报错usingvarplaywrightawaitPlaywright.CreateAsync();awaitusingvarbrowserawaitplaywright.Chromium.LaunchAsync(newBrowserTypeLaunchOptions{Headlesstrue,Argsnew[]{--no-sandbox,--disable-blink-featuresAutomationControlled,// 这个必须加不然Cloudflare直接干你--disable-dev-shm-usage,--disable-gpu,--disable-images,// 禁用图片加快速度反正我们也不需要图片--disable-videos,--disable-audio},Timeout30000// 30秒超时启动浏览器太慢就放弃});vartasksnewListTask();foreach(varurlinurls){tasks.Add(Task.Run(async(){awaitsemaphore.WaitAsync();try{Log.Information($开始处理{url});varsummariesawaitProcessWebsiteAsync(browser,url);lock(results){results.AddRange(summaries);}Log.Information($处理完成{url}共提取{summaries.Count}篇文章);}catch(Exceptionex){Log.Error(ex,$处理网站失败{url});}finally{semaphore.Release();}}));}awaitTask.WhenAll(tasks);// 保存结果到Excel这里用的是CSV格式Excel能直接打开varcsv标题,链接,摘要,发布时间\n;foreach(variteminresults.OrderByDescending(xx.PublishTime)){csv$\{item.Title}\,\{item.Url}\,\{item.Summary}\,\{item.PublishTime:yyyy-MM-dd HH:mm}\\n;}awaitFile.WriteAllTextAsync($摘要_{DateTime.Now:yyyyMMddHHmmss}.csv,csv,System.Text.Encoding.UTF8);Log.Information($所有任务完成共生成{results.Count}篇摘要);asyncTaskListNewsSummaryProcessWebsiteAsync(IBrowserbrowser,stringwebsiteUrl){varsummariesnewListNewsSummary();awaitusingvarpageawaitbrowser.NewPageAsync(newBrowserNewPageOptions{UserAgentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36// 这个UserAgent必须是最新的Chrome低一点都不行别问我怎么知道的});// 绕过无头浏览器检测这些参数我调了一天少一个都可能被检测到awaitpage.AddInitScriptAsync( Object.defineProperty(navigator, webdriver, { get: () undefined }); Object.defineProperty(navigator, plugins, { get: () [1, 2, 3, 4, 5] }); Object.defineProperty(navigator, languages, { get: () [zh-CN, zh] }); );try{awaitpage.GotoAsync(websiteUrl,newPageGotoOptions{WaitUntilWaitUntilState.NetworkIdle,Timeout10000});// 等3秒别问为什么是3秒试了100次得出来的少了页面没加载完awaitTask.Delay(3000);// 这里是提取新闻列表的XPath每个网站不一样自己改// 我这里只是示例实际用的时候要根据网站结构改varnewsElementsawaitpage.QuerySelectorAllAsync(//div[contains(class, news-item)]);foreach(varelementinnewsElements.Take(10))// 每个网站只取前10篇最新的{try{vartitleElementawaitelement.QuerySelectorAsync(a);vartitleawaittitleElement.InnerTextAsync();varurlawaittitleElement.GetAttributeAsync(href);vartimeElementawaitelement.QuerySelectorAsync(.time);vartimeStrawaittimeElement.InnerTextAsync();varpublishTimeDateTime.Parse(timeStr);// 绝对链接处理别问我为什么要写这个有的网站返回的是相对路径if(!url.StartsWith(http)){urlnewUri(newUri(websiteUrl),url).ToString();}// 爬取文章详情页awaitusingvardetailPageawaitbrowser.NewPageAsync(newBrowserNewPageOptions{UserAgentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36});awaitdetailPage.GotoAsync(url,newPageGotoOptions{WaitUntilWaitUntilState.NetworkIdle,Timeout10000});awaitTask.Delay(2000);varhtmlawaitdetailPage.ContentAsync();varcontentCleanHtml(html);if(string.IsNullOrWhiteSpace(content)){Log.Warning($文章内容为空{url});continue;}// 内容太长就截断只取前5000字不然大模型会卡死if(content.Length5000){contentcontent.Substring(0,5000);}varsummaryawaitGenerateSummaryAsync(content);if(!string.IsNullOrWhiteSpace(summary)){summaries.Add(newNewsSummary{Titletitle,Urlurl,PublishTimepublishTime,Summarysummary});}awaitdetailPage.CloseAsync();}catch(Exceptionex){Log.Error(ex,$处理文章失败{websiteUrl});}}}catch(Exceptionex){Log.Error(ex,$访问网站失败{websiteUrl});}finally{awaitpage.CloseAsync();}returnsummaries;}stringCleanHtml(stringhtml){varparsernewHtmlParser();vardocumentparser.ParseDocument(html);// 删掉所有没用的标签这些标签里的内容绝对不是正文foreach(vartaginnew[]{script,style,nav,header,footer,aside,iframe,noscript,svg}){varelementsdocument.QuerySelectorAll(tag);foreach(varelementinelements){element.Remove();}}// 删掉所有广告和评论class里包含这些关键词的都删掉varbadElementsdocument.QuerySelectorAll(*[class*ad], *[class*advertisement], *[class*comment], *[class*recommend], *[class*related]);foreach(varelementinbadElements){element.Remove();}// 提取纯文本把多余的换行和空格去掉varcontentdocument.Body?.TextContent??string.Empty;contentSystem.Text.RegularExpressions.Regex.Replace(content,\s, ).Trim();returncontent;}asyncTaskstringGenerateSummaryAsync(stringcontent){// 提示词是爹别他妈瞎改改一个字效果都不一样varprompt$ 你是一个专业的工控行业新闻摘要生成器。 请严格按照以下要求生成摘要 1. 绝对不要添加任何文章中没有的信息绝对不要编造内容。 2. 摘要长度严格控制在80-120字之间。 3. 只保留核心信息事件主体、发生时间、主要内容、行业影响。 4. 用简洁的陈述句不要用疑问句、感叹句。 5. 不要出现本文、文章、笔者等字样。 文章内容{content};usingvarhttpClientnewHttpClient();httpClient.TimeoutTimeSpan.FromSeconds(30);// 30秒超时大模型有时候会慢varrequestBodynew{modelqwen2:7b-instruct,promptprompt,streamfalse,optionsnew{temperature0.1,// 温度越低结果越稳定越不会瞎编top_p0.1}};// 重试3次大模型有时候会抽风for(inti0;i3;i){try{varresponseawaithttpClient.PostAsJsonAsync(http://localhost:11434/api/generate,requestBody);response.EnsureSuccessStatusCode();varresultawaitresponse.Content.ReadFromJsonAsyncOllamaResponse();varsummaryresult?.Response?.Trim()??string.Empty;if(!string.IsNullOrWhiteSpace(summary)){returnsummary;}}catch(Exceptionex){Log.Error(ex,$生成摘要失败第{i1}次重试);awaitTask.Delay(1000);}}Log.Error(生成摘要失败已重试3次);returnstring.Empty;}// 实体类别问我为什么不写在单独的文件里懒publicclassNewsSummary{publicstringTitle{get;set;}string.Empty;publicstringUrl{get;set;}string.Empty;publicstringSummary{get;set;}string.Empty;publicDateTimePublishTime{get;set;}}publicclassOllamaResponse{[JsonPropertyName(response)]publicstringResponse{get;set;}string.Empty;}Nuget包要装这几个Microsoft.PlaywrightAngleSharpSerilog.Sinks.ConsoleSerilog.Sinks.FileSystem.Net.Http.Json。装完之后在项目目录下打开命令行运行playwright install chromium它会自动下载Chrome浏览器。然后去Ollama官网下载安装包安装完之后运行ollama run qwen2:7b-instruct等模型下载完就可以跑程序了。这个工具现在在王总那边跑了一个多月了每天早上7点20自动启动8点之前就能生成好摘要存成CSV文件市场部的小姑娘直接打开就能用。据说现在她每天都能提前半小时下班还特意给我寄了一箱奶茶。我收了王总8000块钱说实话这个价格真的不贵。我踩的那些坑随便一个都值2000。哦对了别用Selenium谁用谁傻逼。还有Ollama真的是个好东西本地部署大模型太方便了以后有类似的需求我还这么干。