防御一些爬虫陷阱
This commit is contained in:
+89
-4
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user