crawlerkeeper-sdk 0.2.1__tar.gz
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.
- crawlerkeeper_sdk-0.2.1/.gitignore +80 -0
- crawlerkeeper_sdk-0.2.1/LICENSE +21 -0
- crawlerkeeper_sdk-0.2.1/PKG-INFO +264 -0
- crawlerkeeper_sdk-0.2.1/crawlerkeeper_sdk/README.md +243 -0
- crawlerkeeper_sdk-0.2.1/crawlerkeeper_sdk/__init__.py +28 -0
- crawlerkeeper_sdk-0.2.1/crawlerkeeper_sdk/env.py +29 -0
- crawlerkeeper_sdk-0.2.1/crawlerkeeper_sdk/output.py +104 -0
- crawlerkeeper_sdk-0.2.1/crawlerkeeper_sdk/params.py +42 -0
- crawlerkeeper_sdk-0.2.1/crawlerkeeper_sdk/pool.py +114 -0
- crawlerkeeper_sdk-0.2.1/crawlerkeeper_sdk/proxy.py +173 -0
- crawlerkeeper_sdk-0.2.1/crawlerkeeper_sdk/py.typed +0 -0
- crawlerkeeper_sdk-0.2.1/crawlerkeeper_sdk/stealth_session.py +196 -0
- crawlerkeeper_sdk-0.2.1/pyproject.toml +43 -0
- crawlerkeeper_sdk-0.2.1/tests/__init__.py +1 -0
- crawlerkeeper_sdk-0.2.1/tests/test_pool.py +289 -0
- crawlerkeeper_sdk-0.2.1/tests/test_stealth_session.py +80 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Build artifacts
|
|
2
|
+
target/
|
|
3
|
+
*.class
|
|
4
|
+
*.jar
|
|
5
|
+
*.war
|
|
6
|
+
|
|
7
|
+
# Maven
|
|
8
|
+
.mvn/
|
|
9
|
+
mvnw
|
|
10
|
+
mvnw.cmd
|
|
11
|
+
|
|
12
|
+
# Node
|
|
13
|
+
node_modules/
|
|
14
|
+
dist/
|
|
15
|
+
pnpm-lock.yaml
|
|
16
|
+
yarn.lock
|
|
17
|
+
package-lock.json
|
|
18
|
+
npm-debug.log*
|
|
19
|
+
|
|
20
|
+
# IDE
|
|
21
|
+
.idea/
|
|
22
|
+
.vscode/
|
|
23
|
+
*.iml
|
|
24
|
+
*.ipr
|
|
25
|
+
*.iws
|
|
26
|
+
.project
|
|
27
|
+
.classpath
|
|
28
|
+
.settings/
|
|
29
|
+
*.swp
|
|
30
|
+
*.swo
|
|
31
|
+
|
|
32
|
+
# OS
|
|
33
|
+
.DS_Store
|
|
34
|
+
Thumbs.db
|
|
35
|
+
*.tmp
|
|
36
|
+
|
|
37
|
+
# Logs
|
|
38
|
+
*.log
|
|
39
|
+
logs/
|
|
40
|
+
!openspec/specs/logs/
|
|
41
|
+
|
|
42
|
+
# Config (sensitive)
|
|
43
|
+
*.env
|
|
44
|
+
*.env.local
|
|
45
|
+
application-local.yml
|
|
46
|
+
application-*.properties
|
|
47
|
+
|
|
48
|
+
# Local config (generated by CLI)
|
|
49
|
+
CLAUDE.local.md
|
|
50
|
+
|
|
51
|
+
# Worktrees
|
|
52
|
+
.worktrees/
|
|
53
|
+
worktrees/
|
|
54
|
+
|
|
55
|
+
# Temp
|
|
56
|
+
tmp/
|
|
57
|
+
temp/
|
|
58
|
+
crawlerkeeper-agent/config/config.yaml
|
|
59
|
+
|
|
60
|
+
# Agent data
|
|
61
|
+
crawlerkeeper-agent/data/
|
|
62
|
+
|
|
63
|
+
.playwright-mcp
|
|
64
|
+
*/.playwright-cli/
|
|
65
|
+
|
|
66
|
+
__pycache__/
|
|
67
|
+
settings.local.json.worktrees/
|
|
68
|
+
|
|
69
|
+
findings.md
|
|
70
|
+
progress.md
|
|
71
|
+
task_plan.md
|
|
72
|
+
*/superpowers/
|
|
73
|
+
.claude/settings.local.json
|
|
74
|
+
uv.lock
|
|
75
|
+
|
|
76
|
+
out/
|
|
77
|
+
crawlerkeeper-agent/.last_daemon_args
|
|
78
|
+
crawlerkeeper-agent/agent.pid
|
|
79
|
+
.gstack/
|
|
80
|
+
.omo/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CrawlerKeeper Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: crawlerkeeper-sdk
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: CrawlerKeeper 爬虫 SDK:日志上报、结果写入、任务参数解析、TLS 指纹请求、代理管理与并发线程池
|
|
5
|
+
Author: CrawlerKeeper Team
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: crawler,crawlerkeeper,scraping,spider,tls-fingerprint
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: curl-cffi>=0.7.0
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# CrawlerKeeper SDK for Python
|
|
23
|
+
|
|
24
|
+
CrawlerKeeper Python SDK 提供了爬虫与 Agent 通信的封装,支持日志记录、结果写入、参数获取、代理管理和并发爬取。
|
|
25
|
+
|
|
26
|
+
## 安装
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install crawlerkeeper-sdk
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 快速开始
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from crawlerkeeper_sdk import params, info, result, StealthSession
|
|
36
|
+
|
|
37
|
+
# 获取任务参数
|
|
38
|
+
config = params()
|
|
39
|
+
info(f"收到参数: {config}")
|
|
40
|
+
|
|
41
|
+
# 使用 StealthSession 发送请求
|
|
42
|
+
session = StealthSession()
|
|
43
|
+
resp = session.get("https://example.com/api/data")
|
|
44
|
+
data = resp.json()
|
|
45
|
+
|
|
46
|
+
# 写入结果
|
|
47
|
+
result({"data": data, "source": "example.com"})
|
|
48
|
+
|
|
49
|
+
info("爬取完成")
|
|
50
|
+
session.close()
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 核心功能
|
|
54
|
+
|
|
55
|
+
### 日志记录
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from crawlerkeeper_sdk import info, warning, error
|
|
59
|
+
|
|
60
|
+
info("信息日志")
|
|
61
|
+
warning("警告日志")
|
|
62
|
+
error("错误日志")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 结果写入
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from crawlerkeeper_sdk import result
|
|
69
|
+
|
|
70
|
+
result({"count": 100, "items": [...]})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 任务参数
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from crawlerkeeper_sdk import params
|
|
77
|
+
|
|
78
|
+
# python spider.py --params '{"keyword": "python", "page": 1}'
|
|
79
|
+
config = params() # {"keyword": "python", "page": 1}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## StealthSession
|
|
83
|
+
|
|
84
|
+
`StealthSession` 提供隐式浏览器指纹的 HTTP 请求能力,自动管理 TLS 指纹和代理。
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from crawlerkeeper_sdk import StealthSession, result
|
|
88
|
+
|
|
89
|
+
session = StealthSession()
|
|
90
|
+
|
|
91
|
+
# GET 请求
|
|
92
|
+
resp = session.get("https://example.com/api/data")
|
|
93
|
+
print(resp.status_code)
|
|
94
|
+
print(resp.json())
|
|
95
|
+
|
|
96
|
+
# POST 请求
|
|
97
|
+
resp = session.post("https://example.com/api/submit", json={"key": "value"})
|
|
98
|
+
|
|
99
|
+
session.close()
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 主要方法
|
|
103
|
+
|
|
104
|
+
| 方法 | 说明 |
|
|
105
|
+
|------|------|
|
|
106
|
+
| `get(url, **kwargs)` | 发送 GET 请求 |
|
|
107
|
+
| `post(url, **kwargs)` | 发送 POST 请求 |
|
|
108
|
+
| `close()` | 关闭会话 |
|
|
109
|
+
|
|
110
|
+
## ThreadPool 并发爬取
|
|
111
|
+
|
|
112
|
+
`ThreadPool` 提供并发爬取能力,内部管理 `StealthSession` 的创建和关闭。
|
|
113
|
+
|
|
114
|
+
### 推荐用法:使用上下文管理器
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from crawlerkeeper_sdk import ThreadPool, StealthSession, result
|
|
118
|
+
|
|
119
|
+
def crawl(session: StealthSession, keyword: str, page: int):
|
|
120
|
+
"""爬虫函数:session 会自动传入"""
|
|
121
|
+
resp = session.get(f"https://example.com/search?kw={keyword}&page={page}")
|
|
122
|
+
return resp.json()
|
|
123
|
+
|
|
124
|
+
# 使用 with 语句自动管理生命周期,不会遗漏 close()
|
|
125
|
+
with ThreadPool(max_workers=5) as pool:
|
|
126
|
+
pool.submit(crawl, keyword="python", page=1)
|
|
127
|
+
pool.submit(crawl, keyword="java", page=1)
|
|
128
|
+
pool.submit(crawl, keyword="go", page=1)
|
|
129
|
+
|
|
130
|
+
# 阻塞等待所有任务完成
|
|
131
|
+
results = pool.wait()
|
|
132
|
+
|
|
133
|
+
# 处理结果
|
|
134
|
+
for data in results:
|
|
135
|
+
if data is not None:
|
|
136
|
+
result(data)
|
|
137
|
+
# with 块结束时自动调用 wait() 和 close()
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 传统用法
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
pool = ThreadPool(max_workers=5)
|
|
144
|
+
pool.submit(crawl, keyword="python", page=1)
|
|
145
|
+
pool.submit(crawl, keyword="java", page=1)
|
|
146
|
+
|
|
147
|
+
results = pool.wait()
|
|
148
|
+
pool.close() # 需要手动调用
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### 环境变量配置并发数
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
export CONCURRENT_NUM=10
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
# 不指定 max_workers 时使用 CONCURRENT_NUM 环境变量
|
|
159
|
+
with ThreadPool() as pool:
|
|
160
|
+
pool.submit(crawl, keyword="python", page=1)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### ThreadPool 方法
|
|
164
|
+
|
|
165
|
+
| 方法 | 说明 |
|
|
166
|
+
|------|------|
|
|
167
|
+
| `submit(crawl_fn, **kwargs)` | 提交任务,worker 全满时阻塞等待 |
|
|
168
|
+
| `wait()` | 阻塞等待所有任务完成,返回结果列表 |
|
|
169
|
+
| `close()` | 等待所有任务完成后关闭线程池 |
|
|
170
|
+
|
|
171
|
+
### 异常处理
|
|
172
|
+
|
|
173
|
+
爬虫函数抛出的异常会被捕获并通过 `output.error()` 记录,任务结果记为 `None`,不中断其他任务。
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
def failing_crawl(session):
|
|
177
|
+
raise ValueError("some error")
|
|
178
|
+
|
|
179
|
+
pool = ThreadPool(max_workers=2)
|
|
180
|
+
pool.submit(failing_crawl)
|
|
181
|
+
results = pool.wait() # results[0] is None
|
|
182
|
+
pool.close()
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## 代理配置
|
|
186
|
+
|
|
187
|
+
`StealthSession` 的代理行为由环境变量控制(通常由 Agent 下发),不支持按请求传入代理参数:
|
|
188
|
+
|
|
189
|
+
| 环境变量 | 说明 |
|
|
190
|
+
|---------|------|
|
|
191
|
+
| `PROXY_ENABLED` | `on` 开启代理,其他值直连 |
|
|
192
|
+
| `PROXY_MODE` | `tunnel` 静态代理 / `private` 管理端动态分配 |
|
|
193
|
+
| `PROXY_URL` | tunnel 模式的静态代理 URL |
|
|
194
|
+
| `PROXY_VENDOR` | private 模式的供应商标识 |
|
|
195
|
+
| `PROXY_TARGET_SITE` | private 模式的目标站点标识 |
|
|
196
|
+
| `ADMIN_SERVER_URL` | private 模式下管理端地址 |
|
|
197
|
+
| `AGENT_API_KEY` | private 模式下调用管理端的第三方 Token(`td_` 前缀;代理分配/上报接口仅对第三方 Token 开放,节点 `ck_` Key 无权调用) |
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
# tunnel 模式示例
|
|
201
|
+
export PROXY_ENABLED=on
|
|
202
|
+
export PROXY_MODE=tunnel
|
|
203
|
+
export PROXY_URL=http://proxy:8080
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
private 模式下代理用尽或持续被目标站封禁时,`get`/`post` 抛出 `ProxyExhaustedError`。
|
|
207
|
+
|
|
208
|
+
## 运行模式
|
|
209
|
+
|
|
210
|
+
SDK 自动检测运行模式:
|
|
211
|
+
|
|
212
|
+
- **Agent 模式**: 当 `LOG_FILE_PATH` 和 `DATA_FILE_PATH` 环境变量存在时,日志和结果写入文件
|
|
213
|
+
- **开发模式**: 环境变量不存在时,输出到控制台
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
# 开发模式
|
|
217
|
+
python spider.py
|
|
218
|
+
|
|
219
|
+
# Agent 模式
|
|
220
|
+
export LOG_FILE_PATH=/tmp/test.log
|
|
221
|
+
export DATA_FILE_PATH=/tmp/test.json
|
|
222
|
+
python spider.py
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## 完整示例
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
#!/usr/bin/env python3
|
|
229
|
+
from crawlerkeeper_sdk import params, info, result, StealthSession, ThreadPool
|
|
230
|
+
|
|
231
|
+
def crawl(session: StealthSession, keyword: str, page: int):
|
|
232
|
+
"""爬虫函数"""
|
|
233
|
+
info(f"爬取 {keyword} 第 {page} 页")
|
|
234
|
+
resp = session.get(f"https://example.com/api?kw={keyword}&page={page}")
|
|
235
|
+
return resp.json()
|
|
236
|
+
|
|
237
|
+
def main():
|
|
238
|
+
config = params()
|
|
239
|
+
keywords = config.get("keywords", ["python"])
|
|
240
|
+
max_workers = config.get("max_workers", 3)
|
|
241
|
+
|
|
242
|
+
with ThreadPool(max_workers=max_workers) as pool:
|
|
243
|
+
# 提交所有任务
|
|
244
|
+
for keyword in keywords:
|
|
245
|
+
for page in range(1, 4):
|
|
246
|
+
pool.submit(crawl, keyword=keyword, page=page)
|
|
247
|
+
|
|
248
|
+
# 阻塞等待所有任务完成
|
|
249
|
+
results = pool.wait()
|
|
250
|
+
|
|
251
|
+
# 输出结果
|
|
252
|
+
for data in results:
|
|
253
|
+
if data is not None:
|
|
254
|
+
result(data)
|
|
255
|
+
|
|
256
|
+
info("爬取完成")
|
|
257
|
+
|
|
258
|
+
if __name__ == "__main__":
|
|
259
|
+
main()
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## 许可证
|
|
263
|
+
|
|
264
|
+
MIT License
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# CrawlerKeeper SDK for Python
|
|
2
|
+
|
|
3
|
+
CrawlerKeeper Python SDK 提供了爬虫与 Agent 通信的封装,支持日志记录、结果写入、参数获取、代理管理和并发爬取。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install crawlerkeeper-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 快速开始
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from crawlerkeeper_sdk import params, info, result, StealthSession
|
|
15
|
+
|
|
16
|
+
# 获取任务参数
|
|
17
|
+
config = params()
|
|
18
|
+
info(f"收到参数: {config}")
|
|
19
|
+
|
|
20
|
+
# 使用 StealthSession 发送请求
|
|
21
|
+
session = StealthSession()
|
|
22
|
+
resp = session.get("https://example.com/api/data")
|
|
23
|
+
data = resp.json()
|
|
24
|
+
|
|
25
|
+
# 写入结果
|
|
26
|
+
result({"data": data, "source": "example.com"})
|
|
27
|
+
|
|
28
|
+
info("爬取完成")
|
|
29
|
+
session.close()
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 核心功能
|
|
33
|
+
|
|
34
|
+
### 日志记录
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from crawlerkeeper_sdk import info, warning, error
|
|
38
|
+
|
|
39
|
+
info("信息日志")
|
|
40
|
+
warning("警告日志")
|
|
41
|
+
error("错误日志")
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 结果写入
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from crawlerkeeper_sdk import result
|
|
48
|
+
|
|
49
|
+
result({"count": 100, "items": [...]})
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 任务参数
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from crawlerkeeper_sdk import params
|
|
56
|
+
|
|
57
|
+
# python spider.py --params '{"keyword": "python", "page": 1}'
|
|
58
|
+
config = params() # {"keyword": "python", "page": 1}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## StealthSession
|
|
62
|
+
|
|
63
|
+
`StealthSession` 提供隐式浏览器指纹的 HTTP 请求能力,自动管理 TLS 指纹和代理。
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from crawlerkeeper_sdk import StealthSession, result
|
|
67
|
+
|
|
68
|
+
session = StealthSession()
|
|
69
|
+
|
|
70
|
+
# GET 请求
|
|
71
|
+
resp = session.get("https://example.com/api/data")
|
|
72
|
+
print(resp.status_code)
|
|
73
|
+
print(resp.json())
|
|
74
|
+
|
|
75
|
+
# POST 请求
|
|
76
|
+
resp = session.post("https://example.com/api/submit", json={"key": "value"})
|
|
77
|
+
|
|
78
|
+
session.close()
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 主要方法
|
|
82
|
+
|
|
83
|
+
| 方法 | 说明 |
|
|
84
|
+
|------|------|
|
|
85
|
+
| `get(url, **kwargs)` | 发送 GET 请求 |
|
|
86
|
+
| `post(url, **kwargs)` | 发送 POST 请求 |
|
|
87
|
+
| `close()` | 关闭会话 |
|
|
88
|
+
|
|
89
|
+
## ThreadPool 并发爬取
|
|
90
|
+
|
|
91
|
+
`ThreadPool` 提供并发爬取能力,内部管理 `StealthSession` 的创建和关闭。
|
|
92
|
+
|
|
93
|
+
### 推荐用法:使用上下文管理器
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from crawlerkeeper_sdk import ThreadPool, StealthSession, result
|
|
97
|
+
|
|
98
|
+
def crawl(session: StealthSession, keyword: str, page: int):
|
|
99
|
+
"""爬虫函数:session 会自动传入"""
|
|
100
|
+
resp = session.get(f"https://example.com/search?kw={keyword}&page={page}")
|
|
101
|
+
return resp.json()
|
|
102
|
+
|
|
103
|
+
# 使用 with 语句自动管理生命周期,不会遗漏 close()
|
|
104
|
+
with ThreadPool(max_workers=5) as pool:
|
|
105
|
+
pool.submit(crawl, keyword="python", page=1)
|
|
106
|
+
pool.submit(crawl, keyword="java", page=1)
|
|
107
|
+
pool.submit(crawl, keyword="go", page=1)
|
|
108
|
+
|
|
109
|
+
# 阻塞等待所有任务完成
|
|
110
|
+
results = pool.wait()
|
|
111
|
+
|
|
112
|
+
# 处理结果
|
|
113
|
+
for data in results:
|
|
114
|
+
if data is not None:
|
|
115
|
+
result(data)
|
|
116
|
+
# with 块结束时自动调用 wait() 和 close()
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 传统用法
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
pool = ThreadPool(max_workers=5)
|
|
123
|
+
pool.submit(crawl, keyword="python", page=1)
|
|
124
|
+
pool.submit(crawl, keyword="java", page=1)
|
|
125
|
+
|
|
126
|
+
results = pool.wait()
|
|
127
|
+
pool.close() # 需要手动调用
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 环境变量配置并发数
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
export CONCURRENT_NUM=10
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
# 不指定 max_workers 时使用 CONCURRENT_NUM 环境变量
|
|
138
|
+
with ThreadPool() as pool:
|
|
139
|
+
pool.submit(crawl, keyword="python", page=1)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### ThreadPool 方法
|
|
143
|
+
|
|
144
|
+
| 方法 | 说明 |
|
|
145
|
+
|------|------|
|
|
146
|
+
| `submit(crawl_fn, **kwargs)` | 提交任务,worker 全满时阻塞等待 |
|
|
147
|
+
| `wait()` | 阻塞等待所有任务完成,返回结果列表 |
|
|
148
|
+
| `close()` | 等待所有任务完成后关闭线程池 |
|
|
149
|
+
|
|
150
|
+
### 异常处理
|
|
151
|
+
|
|
152
|
+
爬虫函数抛出的异常会被捕获并通过 `output.error()` 记录,任务结果记为 `None`,不中断其他任务。
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
def failing_crawl(session):
|
|
156
|
+
raise ValueError("some error")
|
|
157
|
+
|
|
158
|
+
pool = ThreadPool(max_workers=2)
|
|
159
|
+
pool.submit(failing_crawl)
|
|
160
|
+
results = pool.wait() # results[0] is None
|
|
161
|
+
pool.close()
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## 代理配置
|
|
165
|
+
|
|
166
|
+
`StealthSession` 的代理行为由环境变量控制(通常由 Agent 下发),不支持按请求传入代理参数:
|
|
167
|
+
|
|
168
|
+
| 环境变量 | 说明 |
|
|
169
|
+
|---------|------|
|
|
170
|
+
| `PROXY_ENABLED` | `on` 开启代理,其他值直连 |
|
|
171
|
+
| `PROXY_MODE` | `tunnel` 静态代理 / `private` 管理端动态分配 |
|
|
172
|
+
| `PROXY_URL` | tunnel 模式的静态代理 URL |
|
|
173
|
+
| `PROXY_VENDOR` | private 模式的供应商标识 |
|
|
174
|
+
| `PROXY_TARGET_SITE` | private 模式的目标站点标识 |
|
|
175
|
+
| `ADMIN_SERVER_URL` | private 模式下管理端地址 |
|
|
176
|
+
| `AGENT_API_KEY` | private 模式下调用管理端的第三方 Token(`td_` 前缀;代理分配/上报接口仅对第三方 Token 开放,节点 `ck_` Key 无权调用) |
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
# tunnel 模式示例
|
|
180
|
+
export PROXY_ENABLED=on
|
|
181
|
+
export PROXY_MODE=tunnel
|
|
182
|
+
export PROXY_URL=http://proxy:8080
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
private 模式下代理用尽或持续被目标站封禁时,`get`/`post` 抛出 `ProxyExhaustedError`。
|
|
186
|
+
|
|
187
|
+
## 运行模式
|
|
188
|
+
|
|
189
|
+
SDK 自动检测运行模式:
|
|
190
|
+
|
|
191
|
+
- **Agent 模式**: 当 `LOG_FILE_PATH` 和 `DATA_FILE_PATH` 环境变量存在时,日志和结果写入文件
|
|
192
|
+
- **开发模式**: 环境变量不存在时,输出到控制台
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# 开发模式
|
|
196
|
+
python spider.py
|
|
197
|
+
|
|
198
|
+
# Agent 模式
|
|
199
|
+
export LOG_FILE_PATH=/tmp/test.log
|
|
200
|
+
export DATA_FILE_PATH=/tmp/test.json
|
|
201
|
+
python spider.py
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## 完整示例
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
#!/usr/bin/env python3
|
|
208
|
+
from crawlerkeeper_sdk import params, info, result, StealthSession, ThreadPool
|
|
209
|
+
|
|
210
|
+
def crawl(session: StealthSession, keyword: str, page: int):
|
|
211
|
+
"""爬虫函数"""
|
|
212
|
+
info(f"爬取 {keyword} 第 {page} 页")
|
|
213
|
+
resp = session.get(f"https://example.com/api?kw={keyword}&page={page}")
|
|
214
|
+
return resp.json()
|
|
215
|
+
|
|
216
|
+
def main():
|
|
217
|
+
config = params()
|
|
218
|
+
keywords = config.get("keywords", ["python"])
|
|
219
|
+
max_workers = config.get("max_workers", 3)
|
|
220
|
+
|
|
221
|
+
with ThreadPool(max_workers=max_workers) as pool:
|
|
222
|
+
# 提交所有任务
|
|
223
|
+
for keyword in keywords:
|
|
224
|
+
for page in range(1, 4):
|
|
225
|
+
pool.submit(crawl, keyword=keyword, page=page)
|
|
226
|
+
|
|
227
|
+
# 阻塞等待所有任务完成
|
|
228
|
+
results = pool.wait()
|
|
229
|
+
|
|
230
|
+
# 输出结果
|
|
231
|
+
for data in results:
|
|
232
|
+
if data is not None:
|
|
233
|
+
result(data)
|
|
234
|
+
|
|
235
|
+
info("爬取完成")
|
|
236
|
+
|
|
237
|
+
if __name__ == "__main__":
|
|
238
|
+
main()
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## 许可证
|
|
242
|
+
|
|
243
|
+
MIT License
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CrawlerKeeper SDK for Python
|
|
3
|
+
|
|
4
|
+
提供爬虫与 Agent 通信的封装,支持日志记录、结果写入、参数解析和代理管理。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .output import (
|
|
8
|
+
info,
|
|
9
|
+
warning,
|
|
10
|
+
error,
|
|
11
|
+
result,
|
|
12
|
+
)
|
|
13
|
+
from .params import params
|
|
14
|
+
from .stealth_session import StealthSession
|
|
15
|
+
from .proxy import ProxyExhaustedError
|
|
16
|
+
from .pool import ThreadPool
|
|
17
|
+
|
|
18
|
+
__version__ = "0.2.1"
|
|
19
|
+
__all__ = [
|
|
20
|
+
"info",
|
|
21
|
+
"warning",
|
|
22
|
+
"error",
|
|
23
|
+
"result",
|
|
24
|
+
"params",
|
|
25
|
+
"StealthSession",
|
|
26
|
+
"ProxyExhaustedError",
|
|
27
|
+
"ThreadPool",
|
|
28
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
环境变量管理模块
|
|
3
|
+
|
|
4
|
+
统一管理所有环境变量访问。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
# Agent 环境
|
|
11
|
+
LOG_FILE_PATH: Optional[str] = os.environ.get("LOG_FILE_PATH")
|
|
12
|
+
DATA_FILE_PATH: Optional[str] = os.environ.get("DATA_FILE_PATH")
|
|
13
|
+
|
|
14
|
+
# Admin Server
|
|
15
|
+
ADMIN_SERVER_URL: str = os.environ.get("ADMIN_SERVER_URL", "http://localhost:8080")
|
|
16
|
+
AGENT_API_KEY: str = os.environ.get("AGENT_API_KEY", "")
|
|
17
|
+
|
|
18
|
+
# 代理配置
|
|
19
|
+
PROXY_ENABLED: bool = os.environ.get("PROXY_ENABLED", "off").lower() == "on"
|
|
20
|
+
PROXY_MODE: str = os.environ.get("PROXY_MODE", "private").lower()
|
|
21
|
+
PROXY_URL: Optional[str] = os.environ.get("PROXY_URL")
|
|
22
|
+
PROXY_VENDOR: str = os.environ.get("PROXY_VENDOR", "default")
|
|
23
|
+
PROXY_TARGET_SITE: str = os.environ.get("PROXY_TARGET_SITE", "default")
|
|
24
|
+
|
|
25
|
+
# 并发配置
|
|
26
|
+
CONCURRENT_NUM: int = max(1, int(os.environ.get("CONCURRENT_NUM", "1")))
|
|
27
|
+
|
|
28
|
+
# 请求超时配置(秒)
|
|
29
|
+
CRAWL_TIMEOUT: int = max(1, int(os.environ.get("CRAWL_TIMEOUT", "30")))
|