C#網(wǎng)頁內(nèi)容智能提?。簭腍TML混沌到結(jié)構(gòu)化信息的AI之路
在這個信息爆炸的時代,程序員們經(jīng)常需要從各種網(wǎng)頁中提取有價值的內(nèi)容。傳統(tǒng)的爬蟲要么暴力抓取全部內(nèi)容,要么需要針對每個網(wǎng)站手寫復(fù)雜的解析規(guī)則。如果告訴你,現(xiàn)在可以讓AI自動分析網(wǎng)頁結(jié)構(gòu),精準(zhǔn)定位主要內(nèi)容區(qū)域,你會不會覺得這就是你一直在尋找的解決方案?
本文將帶你深入了解如何結(jié)合Semantic Kernel、HtmlAgilityPack和AI模型,構(gòu)建一個智能的網(wǎng)頁內(nèi)容提取和總結(jié)工具。這不僅是一次技術(shù)實踐,更是探索AI在傳統(tǒng)爬蟲領(lǐng)域的創(chuàng)新應(yīng)用。
傳統(tǒng)爬蟲的三大痛點
痛點一:網(wǎng)頁結(jié)構(gòu)千變?nèi)f化
每個網(wǎng)站的HTML結(jié)構(gòu)都不同,新聞網(wǎng)站、技術(shù)博客、電商平臺的內(nèi)容區(qū)域完全不一樣。傳統(tǒng)方案需要為每種網(wǎng)站類型編寫專門的提取規(guī)則。
痛點二:反爬蟲機(jī)制越來越復(fù)雜
現(xiàn)代網(wǎng)站普遍部署了sophisticated的反爬蟲策略:動態(tài)加載、驗證碼、頻率限制、User-Agent檢測等等。
痛點三:內(nèi)容質(zhì)量參差不齊
即使成功抓取到內(nèi)容,如何從海量信息中提取真正有價值的部分,依然是個技術(shù)難題。
AI驅(qū)動的智能解決方案
核心思路:三步走策略
第一步:獲取網(wǎng)頁的HTML框架結(jié)構(gòu)(去除具體內(nèi)容,保留標(biāo)簽結(jié)構(gòu))
第二步:讓AI分析HTML結(jié)構(gòu),智能識別主體內(nèi)容區(qū)域
第三步:根據(jù)AI推薦的選擇器精準(zhǔn)提取內(nèi)容,并進(jìn)行智能總結(jié)
這個方案的精妙之處在于:我們不是讓AI處理完整的HTML內(nèi)容,而是讓它分析結(jié)構(gòu)化的框架,這樣既提高了準(zhǔn)確性,又大大降低了token消耗。
代碼實戰(zhàn):構(gòu)建智能提取工具
項目準(zhǔn)備
首先安裝必要的NuGet包:
dotnet add package HtmlAgilityPack
dotnet add package Microsoft.SemanticKernel
dotnet add package Microsoft.SemanticKernel.Connectors.OpenAI核心架構(gòu)設(shè)計
整個工具分為三個核心模塊:
// 主程序流程
static async Task Main(string[] args)
{
// 1. 初始化AI服務(wù)
var kernel = InitializeSemanticKernel();
// 2. 獲取HTML框架結(jié)構(gòu)
var (html, htmlStructure) = await WebContentHelper.GetHtmlStructureAsync(url);
// 3. AI分析結(jié)構(gòu),推薦選擇器
var recommendedSelector = await AnalyzeHtmlStructure(kernel, htmlStructure, url);
// 4. 提取內(nèi)容并總結(jié)
var content = WebContentHelper.ExtractContentBySelector(url, recommendedSelector, html);
var summary = await SummarizeContent(kernel, content, url, recommendedSelector);
}AI插件系統(tǒng):讓AI成為你的結(jié)構(gòu)分析專家
這里是整個方案的核心創(chuàng)新點——AI插件系統(tǒng):
// HTML結(jié)構(gòu)分析插件
var htmlAnalysisPrompt = @"
你是專業(yè)的網(wǎng)頁結(jié)構(gòu)分析專家。請分析以下HTML框架結(jié)構(gòu),找出最可能包含主體文章內(nèi)容的元素選擇器。
## HTML框架結(jié)構(gòu):
{{$htmlStructure}}
請分析HTML結(jié)構(gòu),找出主體內(nèi)容區(qū)域。常見的主體內(nèi)容通常位于:
- article 標(biāo)簽
- main 標(biāo)簽
- 帶有 id 或 class 包含 content、article、post、main、body 等關(guān)鍵詞的div
只返回一個最佳的CSS選擇器,不要其他解釋文字。
";
var htmlAnalyzer = kernel.CreateFunctionFromPrompt(
promptTemplate: htmlAnalysisPrompt,
executionSettings: new OpenAIPromptExecutionSettings
{
MaxTokens = 200,
Temperature = 0.3 // 低溫度保證結(jié)果穩(wěn)定
},
functionName: "AnalyzeHtmlStructure"
);關(guān)鍵技術(shù)點:
- 使用低溫度(0.3)確保AI給出穩(wěn)定、準(zhǔn)確的選擇器推薦
- 限制輸出長度(200 tokens),避免AI輸出冗余信息
- 明確指示AI只返回選擇器,不要解釋文字
反爬蟲策略:模擬真實瀏覽器行為
private static void SetBrowserHeaders(HttpClient client)
{
client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.Add("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
client.DefaultRequestHeaders.Add("Accept",
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
// 隨機(jī)Referer策略
var referers = new[] {
"https://www.google.com/",
"https://www.bing.com/",
"https://www.baidu.com/"
};
client.DefaultRequestHeaders.Add("Referer",
referers[new Random().Next(referers.Length)]);
// 關(guān)鍵:自動解壓縮
handler.AutomaticDecompression = DecompressionMethods.GZip |
DecompressionMethods.Deflate |
DecompressionMethods.Brotli;
}HTML結(jié)構(gòu)清理:保留骨架,移除噪音
這是另一個技術(shù)亮點——智能HTML結(jié)構(gòu)提取:
private static async Task<string> ProcessHtmlContent(string html)
{
var doc = new HtmlDocument();
doc.LoadHtml(html);
// 移除腳本和樣式
var scriptsAndStyles = doc.DocumentNode.SelectNodes("http://script | //style");
scriptsAndStyles?.ToList().ForEach(node => node.Remove());
// 清理文本節(jié)點,保留結(jié)構(gòu)
CleanTextNodes(doc.DocumentNode);
string cleanHtml = doc.DocumentNode.OuterHtml;
// 壓縮空白字符
cleanHtml = Regex.Replace(cleanHtml, @">\s+<", "><");
return cleanHtml;
}
private static void CleanTextNodes(HtmlNode node)
{
if (node.NodeType == HtmlNodeType.Text)
{
// 用占位符替代具體文本內(nèi)容
if (!string.IsNullOrWhiteSpace(node.InnerText))
{
node.InnerHtml = "[TEXT]";
}
}
// 保留重要屬性:id, class
var importantAttrs = new[] { "id", "class", "role" };
node.Attributes.Where(attr => !importantAttrs.Contains(attr.Name.ToLower()))
.ToArray()
.ToList()
.ForEach(attr => node.Attributes.Remove(attr));
}智能內(nèi)容提取:從選擇器到文本
public static string ExtractContentBySelector(string url, string cssSelector, string html)
{
var doc = new HtmlDocument();
doc.LoadHtml(html);
HtmlNode targetNode = null;
// 智能解析CSS選擇器
cssSelector = cssSelector.Trim();
if (cssSelector.StartsWith("#"))
{
string id = cssSelector.Substring(1);
targetNode = doc.DocumentNode.SelectSingleNode($"http://*[@id='{id}']");
}
elseif (cssSelector.StartsWith("."))
{
string className = cssSelector.Substring(1);
targetNode = doc.DocumentNode.SelectSingleNode($"http://*[contains(@class, '{className}')]");
}
else
{
targetNode = doc.DocumentNode.SelectSingleNode($"http://{cssSelector}");
}
if (targetNode == null) return null;
// 清理干擾元素
var tagsToRemove = new[] { "script", "style", "nav", "header", "footer", "aside" };
foreach (var tag in tagsToRemove)
{
targetNode.SelectNodes($".//{tag}")?.ToList().ForEach(node => node.Remove());
}
return CleanTextContent(targetNode.InnerText);
}完整例子
using HtmlAgilityPack;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Microsoft.SemanticKernel;
using System.Text.RegularExpressions;
using System;
using System.Text;
using System.Net;
using System.Net.Http;
namespace AppAiWeb
{
class Program
{
static async Task Main(string[] args)
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.WriteLine("?? 智能網(wǎng)頁內(nèi)容總結(jié)工具 - AI自動識別內(nèi)容區(qū)域");
Console.WriteLine("請輸入要總結(jié)的網(wǎng)頁URL:");
var url = Console.ReadLine() ?? "";
if (string.IsNullOrWhiteSpace(url))
{
Console.WriteLine("? URL不能為空");
return;
}
// 初始化 Semantic Kernel
Console.WriteLine("?? 正在初始化AI服務(wù)...");
var kernelBuilder = Kernel.CreateBuilder();
kernelBuilder.AddOpenAIChatCompletion(
modelId: "deepseek-chat",
apiKey: Environment.GetEnvironmentVariable("DEEPSEEK_API_KEY") ?? "sk-XXXXX",
endpoint: new Uri("https://api.deepseek.com/v1")
);
var kernel = kernelBuilder.Build();
// 注冊所有插件
RegisterWebAnalysisPlugins(kernel);
try
{
// 第一步:獲取HTML框架結(jié)構(gòu)
Console.WriteLine("?? 正在獲取網(wǎng)頁HTML框架...");
var (html, htmlStructure) = await WebContentHelper.GetHtmlStructureAsync(url);
if (string.IsNullOrWhiteSpace(htmlStructure))
{
Console.WriteLine("? 無法獲取網(wǎng)頁結(jié)構(gòu)");
return;
}
Console.WriteLine($"?? 【HTML框架長度】:{htmlStructure.Length} 字符");
Console.WriteLine($"?? 【HTML結(jié)構(gòu)預(yù)覽(前300字)】\n{htmlStructure.Substring(0, Math.Min(300, htmlStructure.Length))}...\n");
// 第二步:AI分析HTML結(jié)構(gòu),識別主體內(nèi)容區(qū)域
Console.WriteLine("?? AI正在分析HTML結(jié)構(gòu),識別主體內(nèi)容區(qū)域...");
var structureAnalyzer = kernel.Plugins["WebAnalysisPlugin"]["AnalyzeHtmlStructure"];
var structureArgs = new KernelArguments
{
{ "htmlStructure", htmlStructure },
{ "url", url }
};
var selectorResult = await kernel.InvokeAsync(structureAnalyzer, structureArgs);
string recommendedSelector = selectorResult.GetValue<string>();
Console.WriteLine($"?? AI推薦的內(nèi)容選擇器:{recommendedSelector}");
// 第三步:根據(jù)AI推薦的選擇器提取內(nèi)容
Console.WriteLine("?? 正在提取主體內(nèi)容...");
string mainContent = WebContentHelper.ExtractContentBySelector(url, recommendedSelector, html);
if (string.IsNullOrWhiteSpace(mainContent))
{
Console.WriteLine("? 無法根據(jù)推薦選擇器提取內(nèi)容,嘗試備用方案...");
mainContent = WebContentHelper.ExtractContentBySelector(url, "body", html);
}
Console.WriteLine($"?? 【提取內(nèi)容長度】:{mainContent.Length} 字符");
Console.WriteLine($"?? 【內(nèi)容預(yù)覽(前500字)】\n{mainContent.Substring(0, Math.Min(500, mainContent.Length))}...\n");
// 第四步:AI總結(jié)內(nèi)容
Console.WriteLine("?? 正在生成內(nèi)容摘要...");
var summarizer = kernel.Plugins["WebAnalysisPlugin"]["SummarizeContent"];
var summaryArgs = new KernelArguments
{
{ "content", mainContent },
{ "url", url },
{ "selector", recommendedSelector }
};
var summarization = await kernel.InvokeAsync(summarizer, summaryArgs);
Console.WriteLine($"\n?? 【AI內(nèi)容總結(jié)結(jié)果】\n");
Console.WriteLine(newstring('=', 50));
Console.WriteLine(summarization.GetValue<string>());
Console.WriteLine(newstring('=', 50));
}
catch (Exception ex)
{
Console.WriteLine($"? 處理過程中出現(xiàn)錯誤:{ex.Message}");
Console.WriteLine($"詳細(xì)錯誤:{ex.StackTrace}");
}
Console.WriteLine("\n? 總結(jié)完成!按任意鍵退出...");
Console.ReadKey();
}
// 注冊網(wǎng)頁分析相關(guān)的所有插件
static void RegisterWebAnalysisPlugins(Kernel kernel)
{
// 插件1:分析HTML結(jié)構(gòu),識別主體內(nèi)容區(qū)域
var htmlAnalysisPrompt = @"
你是專業(yè)的網(wǎng)頁結(jié)構(gòu)分析專家。請分析以下HTML框架結(jié)構(gòu),找出最可能包含主體文章內(nèi)容的元素選擇器。
## 網(wǎng)頁URL: {{$url}}
## HTML框架結(jié)構(gòu):
{{$htmlStructure}}
請分析HTML結(jié)構(gòu),找出主體內(nèi)容區(qū)域。常見的主體內(nèi)容通常位于:
- article 標(biāo)簽
- main 標(biāo)簽
- 帶有 id 或 class 包含 content、article、post、main、body 等關(guān)鍵詞的div
- 最大的內(nèi)容容器
分析步驟:
1. 查看是否有 <article> 或 <main> 標(biāo)簽
2. 查看是否有明顯的內(nèi)容相關(guān)的 id 或 class
3. 分析頁面結(jié)構(gòu),找出最可能的主體內(nèi)容區(qū)域
請只返回一個最佳的CSS選擇器,格式如下幾種之一:
- article
- main
- #post_detail
- .article-content
- #content
- .post-content
- div.main-content
只返回選擇器,不要其他解釋文字。
";
var htmlAnalyzer = kernel.CreateFunctionFromPrompt(
promptTemplate: htmlAnalysisPrompt,
executionSettings: new OpenAIPromptExecutionSettings
{
MaxTokens = 200,
Temperature = 0.3
},
functionName: "AnalyzeHtmlStructure",
description: "分析HTML結(jié)構(gòu),識別主體內(nèi)容區(qū)域的CSS選擇器"
);
// 插件2:內(nèi)容總結(jié)
var contentSummaryPrompt = @"
你是專業(yè)的內(nèi)容分析師,請對以下網(wǎng)頁內(nèi)容進(jìn)行深度分析和總結(jié):
## 來源網(wǎng)址: {{$url}}
## 使用的選擇器: {{$selector}}
## 網(wǎng)頁內(nèi)容:
{{$content}}
請按以下格式提供詳細(xì)的內(nèi)容分析:
?? **文章標(biāo)題/主題識別**
[識別文章的核心主題]
?? **核心要點** (3-5個關(guān)鍵點)
? 要點1:[具體內(nèi)容]
? 要點2:[具體內(nèi)容]
? 要點3:[具體內(nèi)容]
?? **內(nèi)容分類**
類型:[技術(shù)文檔/新聞資訊/教程指南/產(chǎn)品介紹/其他]
領(lǐng)域:[相關(guān)行業(yè)或技術(shù)領(lǐng)域]
?? **關(guān)鍵信息提取**
? 重要數(shù)據(jù)/時間:[提取關(guān)鍵數(shù)據(jù)]
? 人物/機(jī)構(gòu):[相關(guān)人物或組織]
? 技術(shù)要點:[技術(shù)相關(guān)的核心信息]
?? **內(nèi)容摘要** (200字以內(nèi))
[用簡潔的語言概括整篇內(nèi)容的精髓]
?? **價值評估**
? 信息價值:[高/中/低]
? 實用性:[評估實際應(yīng)用價值]
? 時效性:[內(nèi)容的時效性評估]
?? **結(jié)論**
[一句話總結(jié)這篇文章的核心觀點或價值]
";
var contentSummarizer = kernel.CreateFunctionFromPrompt(
promptTemplate: contentSummaryPrompt,
executionSettings: new OpenAIPromptExecutionSettings
{
MaxTokens = 1500,
Temperature = 0.7
},
functionName: "SummarizeContent",
description: "對提取的網(wǎng)頁內(nèi)容進(jìn)行深度分析和總結(jié)"
);
// 插件3:快速摘要
var quickSummaryPrompt = @"
請用最簡潔的語言(不超過100字)總結(jié)以下內(nèi)容的核心要點:
內(nèi)容:{{$content}}
要求:直接輸出要點,不要格式化標(biāo)記。
";
var quickSummarizer = kernel.CreateFunctionFromPrompt(
promptTemplate: quickSummaryPrompt,
executionSettings: new OpenAIPromptExecutionSettings
{
MaxTokens = 300,
Temperature = 0.5
},
functionName: "QuickSummary",
description: "生成簡潔的內(nèi)容摘要"
);
kernel.ImportPluginFromFunctions("WebAnalysisPlugin", [htmlAnalyzer, contentSummarizer, quickSummarizer]);
}
}
// 網(wǎng)頁內(nèi)容處理輔助類
publicstaticclass WebContentHelper
{
publicstatic async Task<(string, string)> GetHtmlStructureAsync(string url)
{
try
{
// URL 格式化
if (!Regex.IsMatch(url, @"^https?://", RegexOptions.IgnoreCase))
url = "https://" + url;
Console.WriteLine($"?? 正在訪問:{url}");
using var handler = new HttpClientHandler()
{
// 自動處理cookie
UseCookies = true,
CookieContainer = new System.Net.CookieContainer(),
// SSL證書驗證
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator,
// 關(guān)鍵:自動解壓縮
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli
};
using var client = new HttpClient(handler);
SetBrowserHeaders(client);
// 設(shè)置超時時間
client.Timeout = TimeSpan.FromSeconds(60); // 增加到60秒
Console.WriteLine("?? 嘗試標(biāo)準(zhǔn)請求...");
try
{
// 嘗試標(biāo)準(zhǔn)GET請求
var response = await client.GetAsync(url);
if (response.IsSuccessStatusCode)
{
var html = await response.Content.ReadAsStringAsync();
Console.WriteLine($"? 標(biāo)準(zhǔn)請求成功,HTML長度:{html.Length}");
return (html, await ProcessHtmlContent(html));
}
else
{
Console.WriteLine($"?? 標(biāo)準(zhǔn)請求響應(yīng)碼:{response.StatusCode},嘗試其他方法...");
}
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
Console.WriteLine("?? 標(biāo)準(zhǔn)請求超時,嘗試分段請求...");
}
catch (HttpRequestException ex)
{
Console.WriteLine($"?? 標(biāo)準(zhǔn)請求失?。簕ex.Message},嘗試其他方法...");
}
return ("", "");
}
catch (Exception ex)
{
Console.WriteLine($"? 獲取HTML結(jié)構(gòu)失?。簕ex.Message}");
return ("", "");
}
}
// 處理HTML內(nèi)容的獨立方法
privatestatic async Task<string> ProcessHtmlContent(string html)
{
await Task.Delay(100); // 小延遲,模擬處理時間
Console.WriteLine($"?? 獲取到原始HTML,長度:{html.Length} 字符");
// 使用 HtmlAgilityPack 解析并清理
var doc = new HtmlDocument();
doc.LoadHtml(html);
// 移除script和style標(biāo)簽
var scriptsAndStyles = doc.DocumentNode.SelectNodes("http://script | //style");
if (scriptsAndStyles != null)
{
foreach (var node in scriptsAndStyles)
{
node.Remove();
}
}
// 移除注釋
var comments = doc.DocumentNode.SelectNodes("http://comment()");
if (comments != null)
{
foreach (var comment in comments)
{
comment.Remove();
}
}
// 清理所有文本節(jié)點,但保留標(biāo)簽結(jié)構(gòu)和重要屬性
CleanTextNodes(doc.DocumentNode);
// 獲取清理后的HTML結(jié)構(gòu)
string cleanHtml = doc.DocumentNode.OuterHtml;
// 進(jìn)一步清理和格式化
cleanHtml = Regex.Replace(cleanHtml, @">\s+<", "><"); // 移除標(biāo)簽間空白
cleanHtml = Regex.Replace(cleanHtml, @"\s+", " "); // 合并多余空白
// 限制長度
if (cleanHtml.Length > 5000)
{
Console.WriteLine($"?? HTML結(jié)構(gòu)過長({cleanHtml.Length}字符),截取前5000字符");
cleanHtml = cleanHtml.Substring(0, 5000) + "...[HTML結(jié)構(gòu)已截斷]";
}
return cleanHtml;
}
// 內(nèi)容提取方法
public static string ExtractContentBySelector(string url, string cssSelector, string html)
{
// 使用相同的反爬蟲策略
try
{
var doc = new HtmlDocument();
doc.LoadHtml(html);
// 解析CSS選擇器并查找對應(yīng)元素
HtmlNode targetNode = null;
try
{
cssSelector = cssSelector.Trim();
if (cssSelector.StartsWith("#"))
{
string id = cssSelector.Substring(1);
targetNode = doc.DocumentNode.SelectSingleNode($"http://*[@id='{id}']");
}
elseif (cssSelector.StartsWith("."))
{
string className = cssSelector.Substring(1);
targetNode = doc.DocumentNode.SelectSingleNode($"http://*[contains(@class, '{className}')]");
}
else
{
targetNode = doc.DocumentNode.SelectSingleNode($"http://{cssSelector}");
}
}
catch
{
Console.WriteLine($"?? 選擇器解析失敗,嘗試標(biāo)簽選擇器:{cssSelector}");
targetNode = doc.DocumentNode.SelectSingleNode($"http://{cssSelector}");
}
if (targetNode == null)
{
Console.WriteLine($"? 未找到匹配選擇器的元素:{cssSelector}");
return null;
}
Console.WriteLine($"? 成功找到目標(biāo)元素:{cssSelector}");
// 清理不需要的標(biāo)簽
var tagsToRemove = new[] { "script", "style", "nav", "header", "footer", "aside", "iframe", "noscript" };
foreach (var tag in tagsToRemove)
{
var nodes = targetNode.SelectNodes($".//{tag}");
if (nodes != null)
{
foreach (var node in nodes.ToArray())
{
node.Remove();
}
}
}
// 提取文本內(nèi)容
string textContent = targetNode.InnerText ?? "";
// 清理格式
textContent = System.Net.WebUtility.HtmlDecode(textContent);
textContent = Regex.Replace(textContent, @"\s+", " ");
textContent = Regex.Replace(textContent, @"^\s+|\s+$", "", RegexOptions.Multiline);
textContent = textContent.Trim();
// 限制長度
if (textContent.Length > 8000)
{
Console.WriteLine($"?? 內(nèi)容過長({textContent.Length}字符),截取前8000字符");
textContent = textContent.Substring(0, 8000) + "...[內(nèi)容已截斷]";
}
Console.WriteLine($"? 成功提取內(nèi)容,長度:{textContent.Length} 字符");
return textContent;
}
catch (Exception ex)
{
Console.WriteLine($"? 內(nèi)容提取失?。簕ex.Message}");
return null;
}
}
// 設(shè)置瀏覽器請求頭的輔助方法
private static void SetBrowserHeaders(HttpClient client)
{
client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.Add("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
client.DefaultRequestHeaders.Add("Accept",
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8");
client.DefaultRequestHeaders.Add("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8");
client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate, br");
client.DefaultRequestHeaders.Add("Cache-Control", "no-cache");
// 隨機(jī)Referer
var referers = new[] { "https://www.google.com/", "https://www.bing.com/", "https://www.baidu.com/" };
client.DefaultRequestHeaders.Add("Referer", referers[new Random().Next(referers.Length)]);
client.DefaultRequestHeaders.Add("Sec-Fetch-Dest", "document");
client.DefaultRequestHeaders.Add("Sec-Fetch-Mode", "navigate");
client.DefaultRequestHeaders.Add("Sec-Fetch-Site", "cross-site");
client.DefaultRequestHeaders.Add("Upgrade-Insecure-Requests", "1");
}
// 遞歸清理文本節(jié)點,保留標(biāo)簽結(jié)構(gòu)
private static void CleanTextNodes(HtmlNode node)
{
if (node.NodeType == HtmlNodeType.Text)
{
// 保留標(biāo)簽結(jié)構(gòu)信息,但清空文本內(nèi)容
if (!string.IsNullOrWhiteSpace(node.InnerText))
{
node.InnerHtml = "[TEXT]"; // 用占位符表示這里有文本
}
}
else
{
// 保留重要屬性:id, class, 標(biāo)簽名
var attributesToKeep = new[] { "id", "class", "role", "data-role" };
var attributesToRemove = node.Attributes
.Where(attr => !attributesToKeep.Contains(attr.Name.ToLower()))
.ToArray();
foreach (var attr in attributesToRemove)
{
node.Attributes.Remove(attr);
}
// 遞歸處理子節(jié)點
foreach (var child in node.ChildNodes.ToArray())
{
CleanTextNodes(child);
}
}
}
}
}實際應(yīng)用效果



性能優(yōu)化亮點
- Token消耗控制通過結(jié)構(gòu)化分析,將原本可能需要幾千token的完整HTML內(nèi)容,壓縮到幾百token的結(jié)構(gòu)信息
- 準(zhǔn)確率提升AI專注于結(jié)構(gòu)分析,而非內(nèi)容理解,大大提高了選擇器推薦的準(zhǔn)確性
- 通用性強(qiáng)一套代碼適用于各種網(wǎng)站結(jié)構(gòu),無需針對性適配
開發(fā)避坑指南
坑點一:HttpClient配置不當(dāng)
問題:很多網(wǎng)站返回亂碼或空內(nèi)容
解決方案:必須設(shè)置AutomaticDecompression處理gzip壓縮
坑點二:AI提示詞過于復(fù)雜
問題:AI返回冗長的分析文字而非選擇器
解決方案:明確指示"只返回選擇器,不要其他解釋文字"
坑點三:內(nèi)容長度限制
問題:超長內(nèi)容導(dǎo)致API調(diào)用失敗
解決方案:合理設(shè)置內(nèi)容截斷(HTML結(jié)構(gòu)5000字符,正文內(nèi)容8000字符)
總結(jié)與思考
這個智能網(wǎng)頁內(nèi)容提取工具展現(xiàn)了AI + 傳統(tǒng)技術(shù)結(jié)合的強(qiáng)大威力:
核心創(chuàng)新:讓AI分析網(wǎng)頁結(jié)構(gòu)而非內(nèi)容,實現(xiàn)了精準(zhǔn)度和效率的完美平衡
技術(shù)融合:HtmlAgilityPack處理HTML解析,Semantic Kernel驅(qū)動AI能力,HttpClient處理網(wǎng)絡(luò)請求
實用價值:一套代碼適配所有網(wǎng)站,大幅降低了網(wǎng)頁內(nèi)容提取的開發(fā)成本
思考題:
- 如何進(jìn)一步優(yōu)化AI提示詞,讓選擇器推薦更加精準(zhǔn)?
- 面對動態(tài)加載的SPA應(yīng)用,這套方案還需要哪些改進(jìn)?
如果你正在開發(fā)內(nèi)容聚合、信息監(jiān)控或知識管理系統(tǒng),這個方案絕對值得深入研究和應(yīng)用。覺得有用請轉(zhuǎn)發(fā)給更多同行,讓AI賦能更多C#開發(fā)者!




























