Skip to main content

crawlkit_core/
response.rs

1//! HTTP 响应封装
2
3use std::collections::HashMap;
4
5/// 统一的 HTTP 响应结构
6///
7/// 无论底层使用 reqwest、wreq 还是 Chrome,都统一转换为该类型。
8#[derive(Debug, Clone)]
9pub struct Response {
10    /// 最终请求的 URL(可能经过重定向)
11    pub url: String,
12    /// HTTP 状态码
13    pub status: u16,
14    /// 响应头
15    pub headers: HashMap<String, String>,
16    /// 响应体(文本)
17    pub body: String,
18}
19
20impl Response {
21    /// 状态码是否为 2xx
22    pub fn is_success(&self) -> bool {
23        (200..300).contains(&self.status)
24    }
25
26    /// 获取 Content-Type
27    pub fn content_type(&self) -> Option<&str> {
28        self.headers.get("content-type").map(String::as_str)
29    }
30
31    /// 是否为 HTML 内容
32    ///
33    /// 匹配 `text/html`、`application/xhtml+xml`,或 body 以 `<!doctype html` / `<html` 开头。
34    pub fn is_html(&self) -> bool {
35        if let Some(ct) = self.content_type() {
36            let ct_lower = ct.to_ascii_lowercase();
37            if ct_lower.contains("text/html") || ct_lower.contains("application/xhtml+xml") {
38                return true;
39            }
40        }
41        let trimmed = self.body.trim_start();
42        let lower = trimmed[..trimmed.len().min(64)].to_ascii_lowercase();
43        lower.starts_with("<!doctype html") || lower.starts_with("<html")
44    }
45
46    /// 检测响应是否为机器人验证页面
47    ///
48    /// 很多反爬服务(PerimeterX、Cloudflare、DataDome、Akamai 等)在检测到爬虫时
49    /// 返回 HTTP 200,但 body 内容是 CAPTCHA/challenge 页面,不是真实内容。
50    /// 此方法通过检测 body 中的特征关键词来识别这类伪成功响应。
51    pub fn is_bot_challenge(&self) -> bool {
52        if !self.is_html() {
53            return false;
54        }
55        let body_lower = self.body.to_ascii_lowercase();
56
57        // PerimeterX / HUMAN Security
58        if body_lower.contains("_pxhd")
59            || body_lower.contains("_pxuuid")
60            || body_lower.contains("px-captcha")
61            || body_lower.contains("pxcaptcha")
62            || body_lower.contains("_pxappappid")
63            || body_lower.contains("px-cloud.net")
64            || body_lower.contains("humansecurity.com")
65        {
66            return true;
67        }
68
69        // Cloudflare challenge
70        if body_lower.contains("cf-challenge")
71            || body_lower.contains("challenge-platform")
72            || body_lower.contains("ray id")
73                && body_lower.contains("cloudflare")
74                && body_lower.contains("enable javascript")
75        {
76            return true;
77        }
78
79        // DataDome
80        if body_lower.contains("datadome")
81            || body_lower.contains("captcha-delivery")
82        {
83            return true;
84        }
85
86        // Akamai Bot Manager
87        if body_lower.contains("akamai")
88            && (body_lower.contains("bot") || body_lower.contains("challenge"))
89        {
90            return true;
91        }
92
93        // 通用 challenge 关键词组合(需同时命中多个以减少误判)
94        if body_lower.contains("access to this page")
95            && body_lower.contains("denied")
96            && (body_lower.contains("captcha")
97                || body_lower.contains("human")
98                || body_lower.contains("press & hold")
99                || body_lower.contains("verify you are"))
100        {
101            return true;
102        }
103
104        // 常见验证页面 title
105        if (body_lower.contains("<title>")
106            && (body_lower.contains("attention required")
107                || body_lower.contains("access denied")
108                || body_lower.contains("please verify")
109                || body_lower.contains("security check")
110                || body_lower.contains("robot")
111                || body_lower.contains("blocked")))
112            && body_lower.contains("challenge")
113        {
114            return true;
115        }
116
117        false
118    }
119}