防御一些爬虫陷阱

This commit is contained in:
2026-04-09 17:03:10 +08:00
parent 2ab89b39db
commit 3715b03fab
3 changed files with 206 additions and 12 deletions
+89 -4
View File
@@ -3,8 +3,10 @@
package parser
import (
"net/url" // URL 解析(规范化)
"path" // 路径处理(提取目录、规范化相对路径)
"regexp" // 正则表达式(空白字符替换)
"sort" // query 参数排序(URL 规范化去重)
"strings" // 字符串操作
"golang.org/x/net/html" // 标准 HTML 解析器(将 HTML 解析为 DOM 树)
@@ -61,14 +63,15 @@ func ParseHTML(body, baseURL string) (title, description, text string, hrefs []s
// 提取 <a href="..."> 链接
if tag == "a" {
href := attrVal(n, "href")
if href != "" {
// 去除 URL 中的锚点(#fragment
if href != "" && isSafeURL(href) {
href = strings.SplitN(href, "#", 2)[0]
if href != "" {
// 解析为绝对 URL(处理相对路径、协议相对路径等)
href = resolveURL(base, basePath, href)
if href != "" {
hrefs = append(hrefs, href)
href = NormalizeURL(href)
if href != "" {
hrefs = append(hrefs, href)
}
}
}
}
@@ -177,3 +180,85 @@ func resolveURL(base, basePath, href string) string {
dir := path.Dir(basePath) // 提取当前页面的目录部分
return base + path.Clean(dir+"/"+href) // path.Clean 规范化,去除多余的 ../ 等
}
// isSafeURL 检查 href 是否为安全的 HTTP(S) 链接。
// 过滤 javascript:、data:、mailto:、tel:、ftp: 等伪协议,
// 以及空 href 和仅包含锚点的 href。
func isSafeURL(href string) bool {
if href == "" || href == "#" {
return false
}
// 检查是否包含冒号(伪协议特征)
colon := strings.Index(href, ":")
if colon < 0 {
return true // 无冒号:相对路径、绝对路径、协议相对路径,都是安全的
}
scheme := strings.ToLower(href[:colon])
switch scheme {
case "http", "https":
return true
default:
// javascript:, data:, mailto:, tel:, ftp:, vbscript: 等全部拦截
return false
}
}
// NormalizeURL 将 URL 规范化为用于去重的标准形式。
// 1. 统一为小写 scheme 和 host
// 2. path.Clean 规范化路径(去除 ./、../)
// 3. 按 key 字典序排列 query 参数(消除 ?a=1&b=2 vs ?b=2&a=1 的差异)
// 4. 去除 fragment
// 5. 去除末尾斜杠(根路径 / 除外)
// 返回空字符串表示 URL 无效。
func NormalizeURL(rawURL string) string {
u, err := url.Parse(rawURL)
if err != nil {
return ""
}
if u.Scheme != "http" && u.Scheme != "https" {
return ""
}
if u.Host == "" {
return ""
}
// 统一 scheme 和 host 为小写
u.Scheme = strings.ToLower(u.Scheme)
u.Host = strings.ToLower(u.Host)
// 规范化路径
if u.Path == "" {
u.Path = "/"
}
u.Path = path.Clean(u.Path)
// query 参数按 key 字典序排列
if u.RawQuery != "" {
u.RawQuery = sortQuery(u.RawQuery)
}
// 去除 fragment
u.Fragment = ""
// 去除末尾斜杠(根路径 / 除外)
if len(u.Path) > 1 && strings.HasSuffix(u.Path, "/") {
u.Path = strings.TrimRight(u.Path, "/")
}
return u.String()
}
// sortQuery 将 query 字符串的参数按 key 字典序排列,用于 URL 去重。
func sortQuery(query string) string {
params, err := url.ParseQuery(query)
if err != nil {
return query
}
keys := make([]string, 0, len(params))
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
// url.Values 编码后参数已排序且值已去重
return params.Encode()
}