cfspider 1.7.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cfspider/__init__.py +230 -0
- cfspider/api.py +937 -0
- cfspider/async_api.py +418 -0
- cfspider/async_session.py +281 -0
- cfspider/browser.py +335 -0
- cfspider/cli.py +81 -0
- cfspider/impersonate.py +388 -0
- cfspider/ip_map.py +522 -0
- cfspider/mirror.py +682 -0
- cfspider/session.py +239 -0
- cfspider/stealth.py +537 -0
- cfspider/vless_client.py +572 -0
- cfspider-1.7.4.dist-info/METADATA +1390 -0
- cfspider-1.7.4.dist-info/RECORD +18 -0
- cfspider-1.7.4.dist-info/WHEEL +5 -0
- cfspider-1.7.4.dist-info/entry_points.txt +2 -0
- cfspider-1.7.4.dist-info/licenses/LICENSE +201 -0
- cfspider-1.7.4.dist-info/top_level.txt +1 -0
cfspider/ip_map.py
ADDED
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CFspider IP 地图可视化模块
|
|
3
|
+
|
|
4
|
+
生成包含代理 IP 地理位置的 HTML 地图文件,使用 MapLibre GL JS。
|
|
5
|
+
"""
|
|
6
|
+
import json
|
|
7
|
+
from typing import Optional, List, Dict, Any
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Cloudflare 节点代码对应的坐标(主要节点)
|
|
12
|
+
COLO_COORDINATES = {
|
|
13
|
+
# 亚洲
|
|
14
|
+
"NRT": {"lat": 35.6762, "lng": 139.6503, "city": "东京", "country": "日本"},
|
|
15
|
+
"HND": {"lat": 35.5494, "lng": 139.7798, "city": "东京羽田", "country": "日本"},
|
|
16
|
+
"KIX": {"lat": 34.4347, "lng": 135.2441, "city": "大阪", "country": "日本"},
|
|
17
|
+
"HKG": {"lat": 22.3080, "lng": 113.9185, "city": "香港", "country": "香港"},
|
|
18
|
+
"SIN": {"lat": 1.3521, "lng": 103.8198, "city": "新加坡", "country": "新加坡"},
|
|
19
|
+
"ICN": {"lat": 37.4602, "lng": 126.4407, "city": "首尔", "country": "韩国"},
|
|
20
|
+
"TPE": {"lat": 25.0777, "lng": 121.2330, "city": "台北", "country": "台湾"},
|
|
21
|
+
"BKK": {"lat": 13.6900, "lng": 100.7501, "city": "曼谷", "country": "泰国"},
|
|
22
|
+
"KUL": {"lat": 2.7456, "lng": 101.7072, "city": "吉隆坡", "country": "马来西亚"},
|
|
23
|
+
"BOM": {"lat": 19.0896, "lng": 72.8656, "city": "孟买", "country": "印度"},
|
|
24
|
+
"DEL": {"lat": 28.5562, "lng": 77.1000, "city": "新德里", "country": "印度"},
|
|
25
|
+
"SYD": {"lat": -33.9399, "lng": 151.1753, "city": "悉尼", "country": "澳大利亚"},
|
|
26
|
+
"MEL": {"lat": -37.6690, "lng": 144.8410, "city": "墨尔本", "country": "澳大利亚"},
|
|
27
|
+
|
|
28
|
+
# 北美
|
|
29
|
+
"SJC": {"lat": 37.3639, "lng": -121.9289, "city": "圣何塞", "country": "美国"},
|
|
30
|
+
"LAX": {"lat": 33.9416, "lng": -118.4085, "city": "洛杉矶", "country": "美国"},
|
|
31
|
+
"SEA": {"lat": 47.4502, "lng": -122.3088, "city": "西雅图", "country": "美国"},
|
|
32
|
+
"DFW": {"lat": 32.8998, "lng": -97.0403, "city": "达拉斯", "country": "美国"},
|
|
33
|
+
"ORD": {"lat": 41.9742, "lng": -87.9073, "city": "芝加哥", "country": "美国"},
|
|
34
|
+
"IAD": {"lat": 38.9531, "lng": -77.4565, "city": "华盛顿", "country": "美国"},
|
|
35
|
+
"EWR": {"lat": 40.6895, "lng": -74.1745, "city": "纽瓦克", "country": "美国"},
|
|
36
|
+
"MIA": {"lat": 25.7959, "lng": -80.2870, "city": "迈阿密", "country": "美国"},
|
|
37
|
+
"ATL": {"lat": 33.6407, "lng": -84.4277, "city": "亚特兰大", "country": "美国"},
|
|
38
|
+
"YYZ": {"lat": 43.6777, "lng": -79.6248, "city": "多伦多", "country": "加拿大"},
|
|
39
|
+
"YVR": {"lat": 49.1947, "lng": -123.1789, "city": "温哥华", "country": "加拿大"},
|
|
40
|
+
|
|
41
|
+
# 欧洲
|
|
42
|
+
"LHR": {"lat": 51.4700, "lng": -0.4543, "city": "伦敦", "country": "英国"},
|
|
43
|
+
"CDG": {"lat": 49.0097, "lng": 2.5479, "city": "巴黎", "country": "法国"},
|
|
44
|
+
"FRA": {"lat": 50.0379, "lng": 8.5622, "city": "法兰克福", "country": "德国"},
|
|
45
|
+
"AMS": {"lat": 52.3105, "lng": 4.7683, "city": "阿姆斯特丹", "country": "荷兰"},
|
|
46
|
+
"ZRH": {"lat": 47.4647, "lng": 8.5492, "city": "苏黎世", "country": "瑞士"},
|
|
47
|
+
"MAD": {"lat": 40.4983, "lng": -3.5676, "city": "马德里", "country": "西班牙"},
|
|
48
|
+
"MXP": {"lat": 45.6306, "lng": 8.7281, "city": "米兰", "country": "意大利"},
|
|
49
|
+
"WAW": {"lat": 52.1672, "lng": 20.9679, "city": "华沙", "country": "波兰"},
|
|
50
|
+
"ARN": {"lat": 59.6498, "lng": 17.9238, "city": "斯德哥尔摩", "country": "瑞典"},
|
|
51
|
+
|
|
52
|
+
# 南美
|
|
53
|
+
"GRU": {"lat": -23.4356, "lng": -46.4731, "city": "圣保罗", "country": "巴西"},
|
|
54
|
+
"EZE": {"lat": -34.8222, "lng": -58.5358, "city": "布宜诺斯艾利斯", "country": "阿根廷"},
|
|
55
|
+
"SCL": {"lat": -33.3930, "lng": -70.7858, "city": "圣地亚哥", "country": "智利"},
|
|
56
|
+
|
|
57
|
+
# 中东/非洲
|
|
58
|
+
"DXB": {"lat": 25.2532, "lng": 55.3657, "city": "迪拜", "country": "阿联酋"},
|
|
59
|
+
"JNB": {"lat": -26.1392, "lng": 28.2460, "city": "约翰内斯堡", "country": "南非"},
|
|
60
|
+
"CAI": {"lat": 30.1219, "lng": 31.4056, "city": "开罗", "country": "埃及"},
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class IPMapCollector:
|
|
65
|
+
"""
|
|
66
|
+
IP 地图数据收集器
|
|
67
|
+
|
|
68
|
+
收集爬取过程中使用的代理 IP 信息,用于生成可视化地图。
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(self):
|
|
72
|
+
self.ip_records: List[Dict[str, Any]] = []
|
|
73
|
+
|
|
74
|
+
def add_record(
|
|
75
|
+
self,
|
|
76
|
+
url: str,
|
|
77
|
+
ip: Optional[str] = None,
|
|
78
|
+
cf_colo: Optional[str] = None,
|
|
79
|
+
cf_ray: Optional[str] = None,
|
|
80
|
+
status_code: Optional[int] = None,
|
|
81
|
+
response_time: Optional[float] = None
|
|
82
|
+
):
|
|
83
|
+
"""添加一条 IP 记录"""
|
|
84
|
+
record = {
|
|
85
|
+
"url": url,
|
|
86
|
+
"ip": ip,
|
|
87
|
+
"cf_colo": cf_colo,
|
|
88
|
+
"cf_ray": cf_ray,
|
|
89
|
+
"status_code": status_code,
|
|
90
|
+
"response_time": response_time,
|
|
91
|
+
"timestamp": datetime.now().isoformat()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# 获取节点坐标
|
|
95
|
+
if cf_colo and cf_colo in COLO_COORDINATES:
|
|
96
|
+
coord = COLO_COORDINATES[cf_colo]
|
|
97
|
+
record["lat"] = coord["lat"]
|
|
98
|
+
record["lng"] = coord["lng"]
|
|
99
|
+
record["city"] = coord["city"]
|
|
100
|
+
record["country"] = coord["country"]
|
|
101
|
+
|
|
102
|
+
self.ip_records.append(record)
|
|
103
|
+
|
|
104
|
+
def get_records(self) -> List[Dict[str, Any]]:
|
|
105
|
+
"""获取所有记录"""
|
|
106
|
+
return self.ip_records
|
|
107
|
+
|
|
108
|
+
def clear(self):
|
|
109
|
+
"""清空记录"""
|
|
110
|
+
self.ip_records = []
|
|
111
|
+
|
|
112
|
+
def get_unique_colos(self) -> List[str]:
|
|
113
|
+
"""获取唯一的节点代码列表"""
|
|
114
|
+
colos = set()
|
|
115
|
+
for record in self.ip_records:
|
|
116
|
+
if record.get("cf_colo"):
|
|
117
|
+
colos.add(record["cf_colo"])
|
|
118
|
+
return list(colos)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# 全局收集器实例
|
|
122
|
+
_global_collector = IPMapCollector()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def get_collector() -> IPMapCollector:
|
|
126
|
+
"""获取全局 IP 收集器"""
|
|
127
|
+
return _global_collector
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def add_ip_record(
|
|
131
|
+
url: str,
|
|
132
|
+
ip: Optional[str] = None,
|
|
133
|
+
cf_colo: Optional[str] = None,
|
|
134
|
+
cf_ray: Optional[str] = None,
|
|
135
|
+
status_code: Optional[int] = None,
|
|
136
|
+
response_time: Optional[float] = None
|
|
137
|
+
):
|
|
138
|
+
"""添加 IP 记录到全局收集器"""
|
|
139
|
+
_global_collector.add_record(
|
|
140
|
+
url=url,
|
|
141
|
+
ip=ip,
|
|
142
|
+
cf_colo=cf_colo,
|
|
143
|
+
cf_ray=cf_ray,
|
|
144
|
+
status_code=status_code,
|
|
145
|
+
response_time=response_time
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def generate_map_html(
|
|
150
|
+
output_file: str = "cfspider_map.html",
|
|
151
|
+
title: str = "CFspider Proxy IP Map",
|
|
152
|
+
collector: Optional[IPMapCollector] = None
|
|
153
|
+
) -> str:
|
|
154
|
+
"""
|
|
155
|
+
生成 IP 地图 HTML 文件
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
output_file: 输出文件名
|
|
159
|
+
title: 页面标题
|
|
160
|
+
collector: IP 收集器(默认使用全局收集器)
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
生成的文件路径
|
|
164
|
+
"""
|
|
165
|
+
if collector is None:
|
|
166
|
+
collector = _global_collector
|
|
167
|
+
|
|
168
|
+
records = collector.get_records()
|
|
169
|
+
|
|
170
|
+
# 过滤有坐标的记录
|
|
171
|
+
geo_records = [r for r in records if r.get("lat") and r.get("lng")]
|
|
172
|
+
|
|
173
|
+
# 生成 GeoJSON 数据
|
|
174
|
+
features = []
|
|
175
|
+
for i, record in enumerate(geo_records):
|
|
176
|
+
feature = {
|
|
177
|
+
"type": "Feature",
|
|
178
|
+
"geometry": {
|
|
179
|
+
"type": "Point",
|
|
180
|
+
"coordinates": [record["lng"], record["lat"]]
|
|
181
|
+
},
|
|
182
|
+
"properties": {
|
|
183
|
+
"id": i,
|
|
184
|
+
"url": record.get("url", ""),
|
|
185
|
+
"ip": record.get("ip", "N/A"),
|
|
186
|
+
"cf_colo": record.get("cf_colo", "N/A"),
|
|
187
|
+
"cf_ray": record.get("cf_ray", "N/A"),
|
|
188
|
+
"city": record.get("city", "Unknown"),
|
|
189
|
+
"country": record.get("country", "Unknown"),
|
|
190
|
+
"status_code": record.get("status_code", 0),
|
|
191
|
+
"response_time": record.get("response_time", 0),
|
|
192
|
+
"timestamp": record.get("timestamp", "")
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
features.append(feature)
|
|
196
|
+
|
|
197
|
+
geojson = {
|
|
198
|
+
"type": "FeatureCollection",
|
|
199
|
+
"features": features
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
# 统计信息
|
|
203
|
+
total_requests = len(records)
|
|
204
|
+
geo_requests = len(geo_records)
|
|
205
|
+
unique_colos = collector.get_unique_colos()
|
|
206
|
+
|
|
207
|
+
html_content = f'''<!DOCTYPE html>
|
|
208
|
+
<html lang="zh-CN">
|
|
209
|
+
<head>
|
|
210
|
+
<meta charset="UTF-8">
|
|
211
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
212
|
+
<title>{title}</title>
|
|
213
|
+
<script src="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js"></script>
|
|
214
|
+
<link href="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.css" rel="stylesheet" />
|
|
215
|
+
<style>
|
|
216
|
+
:root {{
|
|
217
|
+
--bg-primary: #0a0a0f;
|
|
218
|
+
--bg-secondary: #12121a;
|
|
219
|
+
--neon-cyan: #00f5ff;
|
|
220
|
+
--neon-magenta: #ff2d95;
|
|
221
|
+
--neon-yellow: #f7f71c;
|
|
222
|
+
--neon-green: #50fa7b;
|
|
223
|
+
--text-primary: #ffffff;
|
|
224
|
+
--text-secondary: #888888;
|
|
225
|
+
}}
|
|
226
|
+
|
|
227
|
+
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
|
228
|
+
|
|
229
|
+
body {{
|
|
230
|
+
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
|
231
|
+
background: var(--bg-primary);
|
|
232
|
+
color: var(--text-primary);
|
|
233
|
+
}}
|
|
234
|
+
|
|
235
|
+
#map {{
|
|
236
|
+
position: absolute;
|
|
237
|
+
top: 0;
|
|
238
|
+
bottom: 0;
|
|
239
|
+
width: 100%;
|
|
240
|
+
}}
|
|
241
|
+
|
|
242
|
+
.info-panel {{
|
|
243
|
+
position: absolute;
|
|
244
|
+
top: 20px;
|
|
245
|
+
left: 20px;
|
|
246
|
+
background: rgba(10, 10, 15, 0.95);
|
|
247
|
+
border: 1px solid var(--neon-cyan);
|
|
248
|
+
border-radius: 12px;
|
|
249
|
+
padding: 20px;
|
|
250
|
+
min-width: 280px;
|
|
251
|
+
box-shadow: 0 0 30px rgba(0, 245, 255, 0.3);
|
|
252
|
+
z-index: 1000;
|
|
253
|
+
}}
|
|
254
|
+
|
|
255
|
+
.info-panel h1 {{
|
|
256
|
+
font-size: 1.4rem;
|
|
257
|
+
color: var(--neon-cyan);
|
|
258
|
+
margin-bottom: 15px;
|
|
259
|
+
text-shadow: 0 0 10px var(--neon-cyan);
|
|
260
|
+
}}
|
|
261
|
+
|
|
262
|
+
.stats {{
|
|
263
|
+
display: grid;
|
|
264
|
+
grid-template-columns: repeat(2, 1fr);
|
|
265
|
+
gap: 10px;
|
|
266
|
+
margin-bottom: 15px;
|
|
267
|
+
}}
|
|
268
|
+
|
|
269
|
+
.stat-item {{
|
|
270
|
+
background: rgba(0, 245, 255, 0.1);
|
|
271
|
+
border: 1px solid rgba(0, 245, 255, 0.3);
|
|
272
|
+
border-radius: 8px;
|
|
273
|
+
padding: 10px;
|
|
274
|
+
text-align: center;
|
|
275
|
+
}}
|
|
276
|
+
|
|
277
|
+
.stat-value {{
|
|
278
|
+
font-size: 1.5rem;
|
|
279
|
+
font-weight: bold;
|
|
280
|
+
color: var(--neon-cyan);
|
|
281
|
+
}}
|
|
282
|
+
|
|
283
|
+
.stat-label {{
|
|
284
|
+
font-size: 0.75rem;
|
|
285
|
+
color: var(--text-secondary);
|
|
286
|
+
margin-top: 5px;
|
|
287
|
+
}}
|
|
288
|
+
|
|
289
|
+
.colo-list {{
|
|
290
|
+
background: rgba(255, 255, 255, 0.05);
|
|
291
|
+
border-radius: 8px;
|
|
292
|
+
padding: 10px;
|
|
293
|
+
max-height: 150px;
|
|
294
|
+
overflow-y: auto;
|
|
295
|
+
}}
|
|
296
|
+
|
|
297
|
+
.colo-tag {{
|
|
298
|
+
display: inline-block;
|
|
299
|
+
background: rgba(255, 45, 149, 0.2);
|
|
300
|
+
border: 1px solid var(--neon-magenta);
|
|
301
|
+
color: var(--neon-magenta);
|
|
302
|
+
padding: 4px 8px;
|
|
303
|
+
border-radius: 4px;
|
|
304
|
+
font-size: 0.75rem;
|
|
305
|
+
margin: 2px;
|
|
306
|
+
}}
|
|
307
|
+
|
|
308
|
+
.maplibregl-popup-content {{
|
|
309
|
+
background: rgba(10, 10, 15, 0.95) !important;
|
|
310
|
+
border: 1px solid var(--neon-cyan) !important;
|
|
311
|
+
border-radius: 8px !important;
|
|
312
|
+
padding: 15px !important;
|
|
313
|
+
color: var(--text-primary) !important;
|
|
314
|
+
box-shadow: 0 0 20px rgba(0, 245, 255, 0.3) !important;
|
|
315
|
+
}}
|
|
316
|
+
|
|
317
|
+
.maplibregl-popup-close-button {{
|
|
318
|
+
color: var(--neon-cyan) !important;
|
|
319
|
+
font-size: 20px !important;
|
|
320
|
+
}}
|
|
321
|
+
|
|
322
|
+
.popup-title {{
|
|
323
|
+
color: var(--neon-cyan);
|
|
324
|
+
font-size: 1.1rem;
|
|
325
|
+
font-weight: bold;
|
|
326
|
+
margin-bottom: 10px;
|
|
327
|
+
}}
|
|
328
|
+
|
|
329
|
+
.popup-row {{
|
|
330
|
+
display: flex;
|
|
331
|
+
justify-content: space-between;
|
|
332
|
+
padding: 5px 0;
|
|
333
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
334
|
+
}}
|
|
335
|
+
|
|
336
|
+
.popup-label {{
|
|
337
|
+
color: var(--text-secondary);
|
|
338
|
+
}}
|
|
339
|
+
|
|
340
|
+
.popup-value {{
|
|
341
|
+
color: var(--neon-green);
|
|
342
|
+
font-family: monospace;
|
|
343
|
+
}}
|
|
344
|
+
|
|
345
|
+
.footer {{
|
|
346
|
+
position: absolute;
|
|
347
|
+
bottom: 20px;
|
|
348
|
+
left: 20px;
|
|
349
|
+
background: rgba(10, 10, 15, 0.9);
|
|
350
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
351
|
+
border-radius: 8px;
|
|
352
|
+
padding: 10px 15px;
|
|
353
|
+
font-size: 0.8rem;
|
|
354
|
+
color: var(--text-secondary);
|
|
355
|
+
z-index: 1000;
|
|
356
|
+
}}
|
|
357
|
+
|
|
358
|
+
.footer a {{
|
|
359
|
+
color: var(--neon-cyan);
|
|
360
|
+
text-decoration: none;
|
|
361
|
+
}}
|
|
362
|
+
</style>
|
|
363
|
+
</head>
|
|
364
|
+
<body>
|
|
365
|
+
<div id="map"></div>
|
|
366
|
+
|
|
367
|
+
<div class="info-panel">
|
|
368
|
+
<h1>CFspider IP Map</h1>
|
|
369
|
+
<div class="stats">
|
|
370
|
+
<div class="stat-item">
|
|
371
|
+
<div class="stat-value">{total_requests}</div>
|
|
372
|
+
<div class="stat-label">Total Requests</div>
|
|
373
|
+
</div>
|
|
374
|
+
<div class="stat-item">
|
|
375
|
+
<div class="stat-value">{len(unique_colos)}</div>
|
|
376
|
+
<div class="stat-label">Unique Nodes</div>
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
<div class="colo-list">
|
|
380
|
+
{"".join([f'<span class="colo-tag">{colo}</span>' for colo in unique_colos]) if unique_colos else '<span style="color: #888;">No data</span>'}
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
|
|
384
|
+
<div class="footer">
|
|
385
|
+
Generated by <a href="https://github.com/violettoolssite/CFspider" target="_blank">CFspider</a> |
|
|
386
|
+
{datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
|
387
|
+
</div>
|
|
388
|
+
|
|
389
|
+
<script>
|
|
390
|
+
const geojsonData = {json.dumps(geojson)};
|
|
391
|
+
|
|
392
|
+
const map = new maplibregl.Map({{
|
|
393
|
+
container: 'map',
|
|
394
|
+
style: {{
|
|
395
|
+
version: 8,
|
|
396
|
+
sources: {{
|
|
397
|
+
'carto-dark': {{
|
|
398
|
+
type: 'raster',
|
|
399
|
+
tiles: [
|
|
400
|
+
'https://a.basemaps.cartocdn.com/dark_all/{{z}}/{{x}}/{{y}}@2x.png',
|
|
401
|
+
'https://b.basemaps.cartocdn.com/dark_all/{{z}}/{{x}}/{{y}}@2x.png',
|
|
402
|
+
'https://c.basemaps.cartocdn.com/dark_all/{{z}}/{{x}}/{{y}}@2x.png'
|
|
403
|
+
],
|
|
404
|
+
tileSize: 256
|
|
405
|
+
}}
|
|
406
|
+
}},
|
|
407
|
+
layers: [{{
|
|
408
|
+
id: 'carto-dark-layer',
|
|
409
|
+
type: 'raster',
|
|
410
|
+
source: 'carto-dark',
|
|
411
|
+
minzoom: 0,
|
|
412
|
+
maxzoom: 19
|
|
413
|
+
}}]
|
|
414
|
+
}},
|
|
415
|
+
center: [0, 20],
|
|
416
|
+
zoom: 1.5
|
|
417
|
+
}});
|
|
418
|
+
|
|
419
|
+
map.addControl(new maplibregl.NavigationControl());
|
|
420
|
+
|
|
421
|
+
map.on('load', function() {{
|
|
422
|
+
// 添加数据源
|
|
423
|
+
map.addSource('proxies', {{
|
|
424
|
+
type: 'geojson',
|
|
425
|
+
data: geojsonData
|
|
426
|
+
}});
|
|
427
|
+
|
|
428
|
+
// 添加圆点图层
|
|
429
|
+
map.addLayer({{
|
|
430
|
+
id: 'proxy-points',
|
|
431
|
+
type: 'circle',
|
|
432
|
+
source: 'proxies',
|
|
433
|
+
paint: {{
|
|
434
|
+
'circle-radius': 10,
|
|
435
|
+
'circle-color': '#00f5ff',
|
|
436
|
+
'circle-opacity': 0.8,
|
|
437
|
+
'circle-stroke-width': 2,
|
|
438
|
+
'circle-stroke-color': '#ff2d95'
|
|
439
|
+
}}
|
|
440
|
+
}});
|
|
441
|
+
|
|
442
|
+
// 添加发光效果
|
|
443
|
+
map.addLayer({{
|
|
444
|
+
id: 'proxy-points-glow',
|
|
445
|
+
type: 'circle',
|
|
446
|
+
source: 'proxies',
|
|
447
|
+
paint: {{
|
|
448
|
+
'circle-radius': 20,
|
|
449
|
+
'circle-color': '#00f5ff',
|
|
450
|
+
'circle-opacity': 0.2,
|
|
451
|
+
'circle-blur': 1
|
|
452
|
+
}}
|
|
453
|
+
}}, 'proxy-points');
|
|
454
|
+
|
|
455
|
+
// 点击事件
|
|
456
|
+
map.on('click', 'proxy-points', function(e) {{
|
|
457
|
+
const props = e.features[0].properties;
|
|
458
|
+
const coordinates = e.features[0].geometry.coordinates.slice();
|
|
459
|
+
|
|
460
|
+
const popupContent = `
|
|
461
|
+
<div class="popup-title">${{props.city}}, ${{props.country}}</div>
|
|
462
|
+
<div class="popup-row">
|
|
463
|
+
<span class="popup-label">Node:</span>
|
|
464
|
+
<span class="popup-value">${{props.cf_colo}}</span>
|
|
465
|
+
</div>
|
|
466
|
+
<div class="popup-row">
|
|
467
|
+
<span class="popup-label">IP:</span>
|
|
468
|
+
<span class="popup-value">${{props.ip}}</span>
|
|
469
|
+
</div>
|
|
470
|
+
<div class="popup-row">
|
|
471
|
+
<span class="popup-label">Status:</span>
|
|
472
|
+
<span class="popup-value">${{props.status_code}}</span>
|
|
473
|
+
</div>
|
|
474
|
+
<div class="popup-row">
|
|
475
|
+
<span class="popup-label">Time:</span>
|
|
476
|
+
<span class="popup-value">${{props.response_time ? props.response_time.toFixed(2) + 'ms' : 'N/A'}}</span>
|
|
477
|
+
</div>
|
|
478
|
+
<div class="popup-row" style="border: none;">
|
|
479
|
+
<span class="popup-label">URL:</span>
|
|
480
|
+
<span class="popup-value" style="font-size: 0.7rem; word-break: break-all;">${{props.url.substring(0, 40)}}...</span>
|
|
481
|
+
</div>
|
|
482
|
+
`;
|
|
483
|
+
|
|
484
|
+
new maplibregl.Popup()
|
|
485
|
+
.setLngLat(coordinates)
|
|
486
|
+
.setHTML(popupContent)
|
|
487
|
+
.addTo(map);
|
|
488
|
+
}});
|
|
489
|
+
|
|
490
|
+
// 鼠标样式
|
|
491
|
+
map.on('mouseenter', 'proxy-points', function() {{
|
|
492
|
+
map.getCanvas().style.cursor = 'pointer';
|
|
493
|
+
}});
|
|
494
|
+
|
|
495
|
+
map.on('mouseleave', 'proxy-points', function() {{
|
|
496
|
+
map.getCanvas().style.cursor = '';
|
|
497
|
+
}});
|
|
498
|
+
|
|
499
|
+
// 如果有数据,自动缩放到数据范围
|
|
500
|
+
if (geojsonData.features.length > 0) {{
|
|
501
|
+
const bounds = new maplibregl.LngLatBounds();
|
|
502
|
+
geojsonData.features.forEach(feature => {{
|
|
503
|
+
bounds.extend(feature.geometry.coordinates);
|
|
504
|
+
}});
|
|
505
|
+
map.fitBounds(bounds, {{ padding: 50 }});
|
|
506
|
+
}}
|
|
507
|
+
}});
|
|
508
|
+
</script>
|
|
509
|
+
</body>
|
|
510
|
+
</html>'''
|
|
511
|
+
|
|
512
|
+
# 写入文件
|
|
513
|
+
with open(output_file, 'w', encoding='utf-8') as f:
|
|
514
|
+
f.write(html_content)
|
|
515
|
+
|
|
516
|
+
return output_file
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def clear_records():
|
|
520
|
+
"""清空全局收集器的记录"""
|
|
521
|
+
_global_collector.clear()
|
|
522
|
+
|