最稳妥方案是用libxml2配合xpath解析:先调用htmlparsedoc加载html并自动检测编码,再用xmlxpathevalexpression执行"//a[@href]/@href"提取href属性值,最后用xmlnodegetcontent获取内容并手动清理内存。

用正则提取 <a href="..."></a> 标签容易出错
HTML 不是正则友好的格式,嵌套、转义、属性顺序、单双引号混用、注释里含 <a> 都会让正则崩掉。比如 <code><a href="https://ex.com?q=" foo></a> 里引号嵌套,或 <!-- <a href="bad"> --> 这种注释,简单正则会误抓或截断。
真正可靠的做法是用 HTML 解析器,而不是字符串匹配。C++ 没有标准库内置 HTML 解析器,得依赖第三方库。
推荐用 libxml2 + htmlParseDoc 解析再查 //a/@href
libxml2 是成熟、轻量、支持 malformed HTML 的 C 库,C++ 可直接调用。它能容忍标签不闭合、属性无引号等常见网页脏数据。
关键步骤:
- 用
htmlParseDoc加载 HTML 字符串(注意传入NULL编码让其自动检测) - 用
xmlXPathEvalExpression执行 XPath//a[@href]/@href—— 这比遍历 DOM 更简洁,且自动跳过无href的<a></a> - 结果是
xmlXPathObjectPtr,遍历每个xmlNodeSet项,用xmlNodeGetContent提取值 - 记得调用
xmlFreeDoc和xmlXPathFreeObject清理内存
示例片段(省略错误检查):
xmlDocPtr doc = htmlParseDoc(BAD_CAST html_str, NULL);
xmlXPathContextPtr ctx = xmlXPathNewContext(doc);
xmlXPathObjectPtr obj = xmlXPathEvalExpression(BAD_CAST "//a[@href]/@href", ctx);
if (obj->nodesetval) {
for (int i = 0; i nodesetval->nodeNr; ++i) {
xmlChar* href = xmlNodeGetContent(obj->nodesetval->nodeTab[i]);
printf("%s\n", href);
xmlFree(href);
}
}
xmlXPathFreeObject(obj);
xmlXPathFreeContext(ctx);
xmlFreeDoc(doc);
如果不能引入 libxml2,Boost.Spirit 是次选但门槛高
Boost.Spirit 能写语义动作解析 HTML 片段,但它不是为完整 HTML 设计的:不处理 DTD、不恢复损坏结构、不兼容实体如 。只适合已知格式干净、结构简单的内嵌 HTML 片段(比如 CMS 导出的一小段内容)。
实际中更常见的“轻量替代”是先用 std::regex 做粗筛(如找所有 href=),再对每个匹配子串用简易状态机校验引号配对和是否在注释/脚本内——但这仍是妥协方案,漏链或误链概率不可忽视。
提取后要注意 href 值的相对路径补全
拿到的 href 很可能是相对路径:"./page.html"、"../img/logo.png"、甚至 "#section2"。如果目标是收集可访问的 URL,必须结合原始 HTML 的 <base href="..."> 或加载页面的 base URL 做解析。
libxml2 本身不提供 URL 补全,需手动处理:xmlGetProp 拿 <base> 的 href,再用简单逻辑拼接(注意处理 //example.com、http: 等协议相对路径)。忽略这点,导出的链接一大半打不开。
真正难的不是“怎么找到 href”,而是“怎么确认它指向哪里”。这个环节最容易被跳过,直到批量请求时一堆 404 才发现。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











