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
| """
基础爬虫类
"""
import time
import random
import requests
from typing import Dict, Any, Optional
from config.settings import *
from config.logging import logger
class BaseScraper:
"""基础爬虫类"""
def __init__(self):
self.session = requests.Session()
self.session.headers.update(DEFAULT_HEADERS)
def request(
self,
method: str,
url: str,
**kwargs
) -> Optional[requests.Response]:
"""
发送 HTTP 请求(带重试)
Args:
method: HTTP 方法 (GET, POST, etc.)
url: 请求 URL
**kwargs: 其他请求参数
Returns:
Response 对象或 None
"""
for attempt in range(MAX_RETRIES):
try:
# 合并代理配置
if PROXIES:
kwargs.setdefault('proxies', PROXIES)
# 设置超时
kwargs.setdefault('timeout', REQUEST_TIMEOUT)
# 发送请求
response = self.session.request(method, url, **kwargs)
response.raise_for_status()
logger.info(f"请求成功: {method} {url} - {response.status_code}")
return response
except requests.exceptions.RequestException as e:
logger.warning(f"请求失败 (尝试 {attempt + 1}/{MAX_RETRIES}): {e}")
if attempt < MAX_RETRIES - 1:
time.sleep(RETRY_DELAY * (attempt + 1))
else:
logger.error(f"请求最终失败: {method} {url}")
return None
def get(self, url: str, **kwargs) -> Optional[requests.Response]:
"""GET 请求"""
return self.request('GET', url, **kwargs)
def post(self, url: str, **kwargs) -> Optional[requests.Response]:
"""POST 请求"""
return self.request('POST', url, **kwargs)
def sleep(self):
"""随机休眠"""
sleep_time = random.uniform(*SLEEP_INTERVAL)
logger.debug(f"休眠 {sleep_time:.2f} 秒")
time.sleep(sleep_time)
def save_json(self, data: Dict[Any, Any], filename: str):
"""保存 JSON 数据"""
import json
filepath = PROCESSED_DATA_DIR / filename
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
logger.info(f"数据已保存: {filepath}")
def save_csv(self, data: list, filename: str):
"""保存 CSV 数据"""
import csv
if not data:
logger.warning("没有数据可保存")
return
filepath = PROCESSED_DATA_DIR / filename
with open(filepath, 'w', encoding='utf-8', newline='') as f:
writer = csv.DictWriter(f, fieldnames=data[0].keys())
writer.writeheader()
writer.writerows(data)
logger.info(f"数据已保存: {filepath} ({len(data)} 条)")
|