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/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
+