File size: 9,640 Bytes
e0b68d2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""

用户代理工具模块

提供动态随机用户代理生成功能,包含更丰富的浏览器指纹特征

"""

import random
import re
from typing import Any, Dict, Optional, Tuple

try:
    from fake_useragent import UserAgent as _FakeUserAgent
except ImportError:
    _FakeUserAgent = None

# 全局 UserAgent 实例(单例模式)
_user_agent_instance: Optional[Any] = None


def get_user_agent_instance() -> Any:
    """获取或创建 UserAgent 实例(单例模式)"""
    global _user_agent_instance
    if _user_agent_instance is None:
        if _FakeUserAgent is None:
            raise RuntimeError("fake_useragent is not installed")
        try:
            # 开启缓存以提高性能
            _user_agent_instance = _FakeUserAgent(use_external_data=True, cache=True)
        except Exception:
            # 如果外部数据加载失败,提供降级机制
            _user_agent_instance = _FakeUserAgent(use_external_data=False)
    return _user_agent_instance


def get_random_user_agent(browser_type: Optional[str] = None) -> str:
    """

    获取随机用户代理字符串



    Args:

        browser_type: 指定浏览器类型 ('chrome', 'firefox', 'safari', 'edge', 'mobile_safari', 'mobile_chrome')

                     如果为 None,则随机选择



    Returns:

        str: 用户代理字符串

    """
    try:
        ua = get_user_agent_instance()
        
        # 定义更多的浏览器选择及其权重
        if browser_type is None:
            browser_choices = [
                "chrome", "chrome", "chrome", "chrome", 
                "edge", "edge", 
                "firefox", "safari", 
                "mobile_chrome", "mobile_safari"
            ]
            browser_type = random.choice(browser_choices)

        # 根据浏览器类型获取用户代理
        if browser_type == "chrome":
            user_agent = ua.chrome
        elif browser_type == "edge":
            user_agent = ua.edge
        elif browser_type == "firefox":
            user_agent = ua.firefox
        elif browser_type == "safari":
            user_agent = ua.safari
        # 对于移动端特定的 UA, fake_useragent.random 支持通过 OS 等获取,这里为增加可靠性可以显式定义些移动端特征
        elif browser_type == "mobile_chrome":
            # 尝试生成包含 Android 的 Chrome UA
            user_agent = f"Mozilla/5.0 (Linux; Android {random.randint(10, 14)}; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{random.randint(110, 125)}.0.0.0 Mobile Safari/537.36"
        elif browser_type == "mobile_safari":
            ios_ver = f"{random.randint(15, 17)}_{random.randint(0, 5)}"
            user_agent = f"Mozilla/5.0 (iPhone; CPU iPhone OS {ios_ver} like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/{random.randint(15, 17)}.0 Mobile/15E148 Safari/604.1"
        else:
            user_agent = ua.random

        # 确保不返回 None 或空字符串
        if not user_agent:
            raise ValueError("获取到的 User-Agent 为空")
            
        return user_agent

    except Exception:
        # Fallback 机制:如果 fake_useragent 失败,使用硬编码的常用 UA 列表
        fallback_uas = [
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0",
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0",
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15"
        ]
        return random.choice(fallback_uas)


def parse_ua_features(user_agent: str) -> Tuple[str, str, str, str, str]:
    """

    解析 UA 字符串以提取平台、移动端标识、Chrome/Edge/Firefox 等版本号

    """
    platform = '"Windows"'
    if "Windows" in user_agent:
        platform = '"Windows"'
    elif "Macintosh" in user_agent or "Mac OS X" in user_agent:
        platform = '"macOS"'
    elif "Android" in user_agent:
        platform = '"Android"'
    elif "Linux" in user_agent:
        platform = '"Linux"'
    elif "iPhone" in user_agent or "iPad" in user_agent:
        platform = '"iOS"'
    elif "CrOS" in user_agent:
        platform = '"Chrome OS"'

    is_mobile = "?1" if "Mobile" in user_agent or "Android" in user_agent or platform in ('"Android"', '"iOS"') else "?0"

    chrome_version = "124"
    edge_version = "124"
    firefox_version = ""

    # 解析 Chrome 版本
    chrome_match = re.search(r"Chrome/(\d+)\.", user_agent)
    if chrome_match:
        chrome_version = chrome_match.group(1)

    # 解析 Edge 版本 (Edge, Edg, EdgiOS, EdgA)
    edge_match = re.search(r"Edg[a-zA-Z]*/(\d+)\.", user_agent)
    if edge_match:
        edge_version = edge_match.group(1)
        
    # 解析 Firefox 版本
    firefox_match = re.search(r"Firefox/(\d+)\.", user_agent)
    if firefox_match:
        firefox_version = firefox_match.group(1)

    return platform, is_mobile, chrome_version, edge_version, firefox_version


def get_dynamic_headers(

    referer: Optional[str] = None,

    origin: Optional[str] = None,

    browser_type: Optional[str] = None,

    additional_headers: Optional[Dict[str, str]] = None

) -> Dict[str, str]:
    """

    生成动态浏览器 headers,包含随机 User-Agent 以及逼真的客户端提示 (Client Hints)



    Args:

        referer: 引用页面 URL

        origin: 源站 URL

        browser_type: 指定浏览器类型

        additional_headers: 额外的 headers



    Returns:

        Dict[str, str]: 包含动态 User-Agent 的 headers

    """
    user_agent = get_random_user_agent(browser_type)
    
    # 随机选择常见的支持语言组合
    accept_languages = [
        "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
        "zh-CN,zh;q=0.9,en;q=0.8",
        "zh-CN,zh-TW;q=0.9,zh;q=0.8,en-US;q=0.7,en;q=0.6",
        "zh-CN,zh;q=0.9",
        "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
    ]

    # 基础 headers
    headers = {
        "User-Agent": user_agent,
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
        "Accept-Language": random.choice(accept_languages),
        "Accept-Encoding": "gzip, deflate, br, zstd",
        "Cache-Control": "max-age=0",
        "Connection": "keep-alive",
        "DNT": "1" if random.random() > 0.5 else "0", # 随机的 Do Not Track
        "Upgrade-Insecure-Requests": "1",
    }

    # 添加可选的 headers
    if referer:
        headers["Referer"] = referer

    if origin:
        headers["Origin"] = origin
        
    # 解析特征
    platform, is_mobile, chrome_version, edge_version, firefox_version = parse_ua_features(user_agent)

    # 识别是否是 Chromium 内核浏览器 (包含 Chrome, Edge 等)
    is_chromium = "Chrome/" in user_agent or "CrOS" in user_agent
    is_edge = re.search(r"Edg[a-zA-Z]*/\d+\.", user_agent) is not None
    is_safari = "Safari/" in user_agent and "Chrome/" not in user_agent and "Chromium/" not in user_agent
    is_firefox = "Firefox/" in user_agent

    # 针对 Chromium 内核(Chrome, Edge, Opera 等)添加特有的 Sec-CH-UA 头部
    if is_chromium:
        # Chromium 随机品牌策略
        brand_list = [
            f'"Not-A.Brand";v="99"',
            f'"Not/A)Brand";v="8"',
            f'"Chromium";v="{chrome_version}"'
        ]
        
        if is_edge:
            brand_list.append(f'"Microsoft Edge";v="{edge_version}"')
        else:
            brand_list.append(f'"Google Chrome";v="{chrome_version}"')
            
        # 打乱品牌顺序使其更随机
        random.shuffle(brand_list)
        # 通常 Sec-CH-UA 有 3 个品牌
        sec_ch_ua = ", ".join(brand_list[:3])

        headers.update({
            "sec-ch-ua": sec_ch_ua,
            "sec-ch-ua-mobile": is_mobile,
            "sec-ch-ua-platform": platform,
            "Sec-Fetch-Dest": "document",
            "Sec-Fetch-Mode": "navigate",
            "Sec-Fetch-Site": "cross-site" if origin else "same-origin",
            "Sec-Fetch-User": "?1",
        })
    elif is_firefox:
        # Firefox 特有请求头
        headers.update({
            "Sec-Fetch-Dest": "document",
            "Sec-Fetch-Mode": "navigate",
            "Sec-Fetch-Site": "cross-site" if origin else "same-origin",
            "Sec-Fetch-User": "?1",
            "TE": "trailers",
        })
    elif is_safari:
        # Safari 特有请求头
        headers.update({
            "Sec-Fetch-Dest": "document",
            "Sec-Fetch-Mode": "navigate",
            "Sec-Fetch-Site": "cross-site" if origin else "same-origin",
        })
        
    # 添加现代浏览器常见的优先度请求头
    if "Priority" not in headers and (is_chromium or is_firefox):
        headers["Priority"] = "u=0, i"

    # 添加额外的 headers (覆盖默认值)
    if additional_headers:
        headers.update(additional_headers)

    return headers