mineworker 0.4.0__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.
Files changed (128) hide show
  1. mineworker-0.4.0/.gitignore +36 -0
  2. mineworker-0.4.0/CHANGELOG.md +73 -0
  3. mineworker-0.4.0/LICENSE +21 -0
  4. mineworker-0.4.0/PKG-INFO +157 -0
  5. mineworker-0.4.0/README.md +83 -0
  6. mineworker-0.4.0/docs/api.md +53 -0
  7. mineworker-0.4.0/docs/async-kernel.md +81 -0
  8. mineworker-0.4.0/docs/batch-spider.md +110 -0
  9. mineworker-0.4.0/docs/cli.md +46 -0
  10. mineworker-0.4.0/docs/distributed.md +136 -0
  11. mineworker-0.4.0/docs/index.md +59 -0
  12. mineworker-0.4.0/docs/item-pipeline.md +98 -0
  13. mineworker-0.4.0/docs/middleware-proxy.md +45 -0
  14. mineworker-0.4.0/docs/observability.md +56 -0
  15. mineworker-0.4.0/docs/quickstart.md +77 -0
  16. mineworker-0.4.0/docs/render.md +49 -0
  17. mineworker-0.4.0/docs/roadmap.md +33 -0
  18. mineworker-0.4.0/docs/settings.md +73 -0
  19. mineworker-0.4.0/docs/spider.md +83 -0
  20. mineworker-0.4.0/docs/user-pool.md +77 -0
  21. mineworker-0.4.0/mineworker/__about__.py +3 -0
  22. mineworker-0.4.0/mineworker/__init__.py +81 -0
  23. mineworker-0.4.0/mineworker/buffer/__init__.py +8 -0
  24. mineworker-0.4.0/mineworker/buffer/item_buffer.py +198 -0
  25. mineworker-0.4.0/mineworker/buffer/request_buffer.py +77 -0
  26. mineworker-0.4.0/mineworker/commands/__init__.py +22 -0
  27. mineworker-0.4.0/mineworker/commands/cmdline.py +103 -0
  28. mineworker-0.4.0/mineworker/commands/create/__init__.py +183 -0
  29. mineworker-0.4.0/mineworker/commands/retry.py +99 -0
  30. mineworker-0.4.0/mineworker/commands/shell.py +38 -0
  31. mineworker-0.4.0/mineworker/core/__init__.py +5 -0
  32. mineworker-0.4.0/mineworker/core/base_parser.py +74 -0
  33. mineworker-0.4.0/mineworker/core/base_scheduler.py +223 -0
  34. mineworker-0.4.0/mineworker/core/batch_monitor.py +165 -0
  35. mineworker-0.4.0/mineworker/core/batch_store.py +364 -0
  36. mineworker-0.4.0/mineworker/core/collector.py +55 -0
  37. mineworker-0.4.0/mineworker/core/parser_control.py +211 -0
  38. mineworker-0.4.0/mineworker/core/redis_scheduler.py +148 -0
  39. mineworker-0.4.0/mineworker/core/redis_task_scheduler.py +112 -0
  40. mineworker-0.4.0/mineworker/core/scheduler.py +44 -0
  41. mineworker-0.4.0/mineworker/core/spiders/__init__.py +10 -0
  42. mineworker-0.4.0/mineworker/core/spiders/air_spider.py +50 -0
  43. mineworker-0.4.0/mineworker/core/spiders/batch_spider.py +177 -0
  44. mineworker-0.4.0/mineworker/core/spiders/spider.py +55 -0
  45. mineworker-0.4.0/mineworker/core/spiders/task_spider.py +96 -0
  46. mineworker-0.4.0/mineworker/core/task_queue.py +92 -0
  47. mineworker-0.4.0/mineworker/core/task_source.py +45 -0
  48. mineworker-0.4.0/mineworker/db/__init__.py +7 -0
  49. mineworker-0.4.0/mineworker/db/mysqldb.py +105 -0
  50. mineworker-0.4.0/mineworker/db/redisdb.py +50 -0
  51. mineworker-0.4.0/mineworker/dedup/__init__.py +110 -0
  52. mineworker-0.4.0/mineworker/dedup/bloom_filter.py +66 -0
  53. mineworker-0.4.0/mineworker/dedup/lite_filter.py +30 -0
  54. mineworker-0.4.0/mineworker/dedup/redis_filter.py +71 -0
  55. mineworker-0.4.0/mineworker/exceptions.py +43 -0
  56. mineworker-0.4.0/mineworker/network/__init__.py +8 -0
  57. mineworker-0.4.0/mineworker/network/downloader/__init__.py +69 -0
  58. mineworker-0.4.0/mineworker/network/downloader/_async_httpx.py +119 -0
  59. mineworker-0.4.0/mineworker/network/downloader/_common.py +53 -0
  60. mineworker-0.4.0/mineworker/network/downloader/_httpx.py +88 -0
  61. mineworker-0.4.0/mineworker/network/downloader/_playwright.py +233 -0
  62. mineworker-0.4.0/mineworker/network/downloader/base.py +33 -0
  63. mineworker-0.4.0/mineworker/network/item.py +97 -0
  64. mineworker-0.4.0/mineworker/network/middleware.py +70 -0
  65. mineworker-0.4.0/mineworker/network/proxy_pool/__init__.py +34 -0
  66. mineworker-0.4.0/mineworker/network/proxy_pool/api.py +97 -0
  67. mineworker-0.4.0/mineworker/network/proxy_pool/base.py +17 -0
  68. mineworker-0.4.0/mineworker/network/request.py +177 -0
  69. mineworker-0.4.0/mineworker/network/response.py +157 -0
  70. mineworker-0.4.0/mineworker/network/user_agent.py +23 -0
  71. mineworker-0.4.0/mineworker/network/user_pool/__init__.py +24 -0
  72. mineworker-0.4.0/mineworker/network/user_pool/base.py +37 -0
  73. mineworker-0.4.0/mineworker/network/user_pool/local.py +92 -0
  74. mineworker-0.4.0/mineworker/network/user_pool/middleware.py +57 -0
  75. mineworker-0.4.0/mineworker/network/user_pool/redis.py +82 -0
  76. mineworker-0.4.0/mineworker/pipelines/__init__.py +9 -0
  77. mineworker-0.4.0/mineworker/pipelines/base.py +19 -0
  78. mineworker-0.4.0/mineworker/pipelines/console.py +23 -0
  79. mineworker-0.4.0/mineworker/pipelines/csv.py +62 -0
  80. mineworker-0.4.0/mineworker/pipelines/mongo.py +56 -0
  81. mineworker-0.4.0/mineworker/pipelines/mysql.py +70 -0
  82. mineworker-0.4.0/mineworker/py.typed +0 -0
  83. mineworker-0.4.0/mineworker/setting.py +236 -0
  84. mineworker-0.4.0/mineworker/templates/__init__.py +1 -0
  85. mineworker-0.4.0/mineworker/templates/air_spider.py.jinja +23 -0
  86. mineworker-0.4.0/mineworker/templates/item.py.jinja +13 -0
  87. mineworker-0.4.0/mineworker/templates/item_fields.py.jinja +21 -0
  88. mineworker-0.4.0/mineworker/templates/project/README.md.jinja +30 -0
  89. mineworker-0.4.0/mineworker/templates/project/main.py.jinja +6 -0
  90. mineworker-0.4.0/mineworker/templates/project/spider.py.jinja +21 -0
  91. mineworker-0.4.0/mineworker/templates/setting.py.jinja +43 -0
  92. mineworker-0.4.0/mineworker/utils/__init__.py +1 -0
  93. mineworker-0.4.0/mineworker/utils/alert.py +118 -0
  94. mineworker-0.4.0/mineworker/utils/log.py +58 -0
  95. mineworker-0.4.0/mineworker/utils/metrics.py +97 -0
  96. mineworker-0.4.0/mineworker/utils/stats.py +52 -0
  97. mineworker-0.4.0/mineworker/utils/tools.py +93 -0
  98. mineworker-0.4.0/mkdocs.yml +58 -0
  99. mineworker-0.4.0/pyproject.toml +122 -0
  100. mineworker-0.4.0/tests/conftest.py +18 -0
  101. mineworker-0.4.0/tests/test_air_spider.py +364 -0
  102. mineworker-0.4.0/tests/test_alert.py +158 -0
  103. mineworker-0.4.0/tests/test_async_downloader.py +114 -0
  104. mineworker-0.4.0/tests/test_batch_spider.py +345 -0
  105. mineworker-0.4.0/tests/test_cli.py +175 -0
  106. mineworker-0.4.0/tests/test_dedup.py +67 -0
  107. mineworker-0.4.0/tests/test_downloader.py +118 -0
  108. mineworker-0.4.0/tests/test_integration_httpserver.py +48 -0
  109. mineworker-0.4.0/tests/test_item.py +110 -0
  110. mineworker-0.4.0/tests/test_item_buffer.py +141 -0
  111. mineworker-0.4.0/tests/test_log.py +34 -0
  112. mineworker-0.4.0/tests/test_metrics.py +59 -0
  113. mineworker-0.4.0/tests/test_middleware.py +110 -0
  114. mineworker-0.4.0/tests/test_mysql.py +172 -0
  115. mineworker-0.4.0/tests/test_packaging.py +73 -0
  116. mineworker-0.4.0/tests/test_pipelines.py +80 -0
  117. mineworker-0.4.0/tests/test_proxy_pool.py +121 -0
  118. mineworker-0.4.0/tests/test_redis_infra.py +148 -0
  119. mineworker-0.4.0/tests/test_render.py +164 -0
  120. mineworker-0.4.0/tests/test_request.py +108 -0
  121. mineworker-0.4.0/tests/test_response.py +75 -0
  122. mineworker-0.4.0/tests/test_setting.py +115 -0
  123. mineworker-0.4.0/tests/test_smoke.py +18 -0
  124. mineworker-0.4.0/tests/test_spider.py +182 -0
  125. mineworker-0.4.0/tests/test_spider_persistence.py +99 -0
  126. mineworker-0.4.0/tests/test_task_spider.py +150 -0
  127. mineworker-0.4.0/tests/test_tools.py +88 -0
  128. mineworker-0.4.0/tests/test_user_pool.py +195 -0
@@ -0,0 +1,36 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ *.so
9
+
10
+ # 虚拟环境
11
+ .venv/
12
+ venv/
13
+ env/
14
+
15
+ # 测试 / 类型检查 / lint 缓存
16
+ .pytest_cache/
17
+ .mypy_cache/
18
+ .ruff_cache/
19
+ .coverage
20
+ .coverage.*
21
+ htmlcov/
22
+ coverage.xml
23
+
24
+ # 运行产物
25
+ logs/
26
+ *.log
27
+ failed_items.jsonl
28
+ failed_requests.jsonl
29
+
30
+ # IDE / 系统
31
+ .idea/
32
+ .vscode/
33
+ .DS_Store
34
+
35
+ # 文档构建
36
+ site/
@@ -0,0 +1,73 @@
1
+ # Changelog
2
+
3
+ 本文件格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
4
+ 版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
5
+
6
+ ## [Unreleased]
7
+
8
+ ## [0.4.0] - 2026-09-04
9
+
10
+ **首次发行到 PyPI。** 0.3.0 之后积累的全部分布式能力(v2.1–v2.7)随本版本一次性发出。
11
+
12
+ ### 新增
13
+
14
+ - **Redis 基础设施** —— `db/redisdb.py`(连接缓存、`acquire_once` 一次性锁、统一 key 前缀)、
15
+ `RedisTaskQueue`(zset 优先级队列,原子 `zpopmin`)、`RedisSetFilter` / `RedisBloomFilter`;
16
+ `DEDUP_FILTER=redis|redis-set` 即可整体切换去重后端
17
+ - **`Spider`(分布式)** —— Redis 队列 + Redis 布隆去重 + 断点续爬 + `start_requests` 一次性锁 +
18
+ 多节点心跳结束检测 + 失败请求落 Redis。抽出 `BaseScheduler` 公共骨架
19
+ - **`TaskSpider`** —— 从 Redis / DB 任务源持续拉任务,多节点分摊,`keep_alive` 常驻
20
+ - **账号 / Cookie 池** —— `LocalUserPool` / `GuestUserPool` / `RedisUserPool`;
21
+ `user_pool()` + `check_login()` 钩子,掉登录自动换号重试
22
+ - **MySQL 管道** —— `MysqlPipeline`(`executemany` 批量写 + `ON DUPLICATE KEY UPDATE` upsert)、
23
+ `MysqlDB`(PooledDB 连接池);`mineworker create -i --table <表>` 读 `SHOW FULL COLUMNS` 反射生成 Item
24
+ - **`AsyncHttpxDownloader`** —— 可选异步下载器(`DOWNLOADER_ASYNC=True`),独立事件循环线程 +
25
+ 共享 `AsyncClient`,爬虫代码零改动;新增 `HTTPX_HTTP2` 开关
26
+ - **`BatchSpider`** —— 周期性批次采集:MySQL 任务表状态机 + 批次记录表 + 进度追踪 + 任务防丢;
27
+ master(`start_monitor`)/ worker(`start`)分离,抽象 `BatchStore`(`MysqlBatchStore` / `MemoryBatchStore`)
28
+ - 文档站新增《分布式》《批次采集》《账号 / Cookie 池》《async 内核评估》四篇
29
+
30
+ ### 修复
31
+
32
+ - `cb_kwargs` 从未传给 callback(`parser_control` 现在按 `callback(request, response, **request.cb_kwargs)` 调用)
33
+ - `BaseScheduler.run()` 中 `_seed()` 移到 `_start_threads()` 之前,避免 worker 抢跑断点续爬遗留的队列
34
+ - **刚启动的机器 / 容器上第一条告警被静默吞掉**:`AlertManager` 用 `0.0` 当「从没发过」的哨兵,
35
+ 而 `time.monotonic()` 的原点是开机,`now - 0.0 < WARNING_INTERVAL` 在低 uptime 时恒真。
36
+ 现在用「key 缺席」表示从没发过
37
+ - **同源问题让代理池在新容器里起不来**:`ApiProxyPool` 的 `_last_fetch` 初值 `0.0` 会让第一次
38
+ 拉取代理被间隔限流跳过,池子一直是空的。现在初值为 `None`
39
+ - 只装核心包(不带 `[cli]`)时执行 `mineworker` 会抛 `ModuleNotFoundError` 堆栈,
40
+ 现在提示 `pip install "mineworker[cli]"`
41
+
42
+ ### 变更
43
+
44
+ - 包元数据:`Development Status` 提到 `4 - Beta`,补 `Typing :: Typed` 与完整的 `[project.urls]`
45
+ - 新增 PyPI 发布与文档部署流水线(Trusted Publishing,无 token)
46
+
47
+ ### 说明
48
+
49
+ - 关于「是否把内核改成全 async」的评估结论见
50
+ [async 内核评估](https://apersonw.github.io/mineworker/async-kernel/):**不做全量重写**,
51
+ 只落地隔离的异步下载器。工作线程仍是「1 线程 1 在途」,`AsyncHttpxDownloader` 的收益是
52
+ 连接复用 / HTTP2 / 更低 FD
53
+
54
+ ## 0.3.0 - 2026-09-03
55
+
56
+ 轻量单机版(`AirSpider`)完整可用。**未发行到 PyPI**(当时仅本地开发)。
57
+
58
+ ### 新增
59
+
60
+ - **运行时** —— 内存优先级队列、`Collector` / `ParserControl` / `RequestBuffer` / `ItemBuffer`、
61
+ 优雅退出、`AirSpider`
62
+ - **网络层** —— `Request` / `Response`(w3lib 编码检测 + parsel 选择器)、`Downloader` ABC +
63
+ `HttpxDownloader`、重试与超时
64
+ - **数据与去重** —— `Item` / `UpdateItem`、内存布隆过滤器、
65
+ `Console` / `CSV` / `Mongo` 管道
66
+ - **浏览器渲染** —— `PlaywrightDownloader` + 渲染池,`Request(render=True)`
67
+ - **中间件与代理** —— `DownloaderMiddleware` 链、`ProxyPool` ABC + `ApiProxyPool`
68
+ - **可观测性** —— `MetricsReporter` + Prometheus exporter、`AlertManager`(飞书 / 邮件 / 日志)
69
+ - **命令行** —— `mineworker create` 脚手架、`shell` 交互调试、`retry` 失败重放
70
+ - mkdocs-material 文档站
71
+
72
+ [Unreleased]: https://github.com/apersonw/mineworker/compare/v0.4.0...HEAD
73
+ [0.4.0]: https://github.com/apersonw/mineworker/releases/tag/v0.4.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MineWorker contributors
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,157 @@
1
+ Metadata-Version: 2.5
2
+ Name: mineworker
3
+ Version: 0.4.0
4
+ Summary: 一个上手简单、结构清晰的 Python 爬虫框架(对标 feapder)
5
+ Project-URL: Homepage, https://github.com/apersonw/mineworker
6
+ Project-URL: Repository, https://github.com/apersonw/mineworker
7
+ Project-URL: Documentation, https://apersonw.github.io/mineworker/
8
+ Project-URL: Changelog, https://github.com/apersonw/mineworker/blob/main/CHANGELOG.md
9
+ Project-URL: Issues, https://github.com/apersonw/mineworker/issues
10
+ Author-email: Lucas <schummshelly26@gmail.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: crawler,feapder,scraping,spider,爬虫
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
21
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: bitarray>=2.9
25
+ Requires-Dist: httpx>=0.27
26
+ Requires-Dist: loguru>=0.7
27
+ Requires-Dist: parsel>=1.9
28
+ Requires-Dist: w3lib>=2.1
29
+ Provides-Extra: all
30
+ Requires-Dist: dbutils>=3.0; extra == 'all'
31
+ Requires-Dist: jinja2>=3.1; extra == 'all'
32
+ Requires-Dist: playwright>=1.44; extra == 'all'
33
+ Requires-Dist: prometheus-client>=0.20; extra == 'all'
34
+ Requires-Dist: pymongo>=4.7; extra == 'all'
35
+ Requires-Dist: pymysql>=1.1; extra == 'all'
36
+ Requires-Dist: redis>=5.0; extra == 'all'
37
+ Requires-Dist: typer>=0.12; extra == 'all'
38
+ Provides-Extra: cli
39
+ Requires-Dist: jinja2>=3.1; extra == 'cli'
40
+ Requires-Dist: typer>=0.12; extra == 'cli'
41
+ Provides-Extra: dev
42
+ Requires-Dist: dbutils>=3.0; extra == 'dev'
43
+ Requires-Dist: fakeredis>=2.20; extra == 'dev'
44
+ Requires-Dist: jinja2>=3.1; extra == 'dev'
45
+ Requires-Dist: mongomock>=4.1; extra == 'dev'
46
+ Requires-Dist: mypy>=1.11; extra == 'dev'
47
+ Requires-Dist: playwright>=1.44; extra == 'dev'
48
+ Requires-Dist: pre-commit>=3.7; extra == 'dev'
49
+ Requires-Dist: prometheus-client>=0.20; extra == 'dev'
50
+ Requires-Dist: pymongo>=4.7; extra == 'dev'
51
+ Requires-Dist: pymysql>=1.1; extra == 'dev'
52
+ Requires-Dist: pytest-cov>=5; extra == 'dev'
53
+ Requires-Dist: pytest-httpserver>=1.0; extra == 'dev'
54
+ Requires-Dist: pytest>=8; extra == 'dev'
55
+ Requires-Dist: redis>=5.0; extra == 'dev'
56
+ Requires-Dist: respx>=0.21; extra == 'dev'
57
+ Requires-Dist: ruff>=0.6; extra == 'dev'
58
+ Requires-Dist: typer>=0.12; extra == 'dev'
59
+ Provides-Extra: docs
60
+ Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
61
+ Requires-Dist: mkdocstrings[python]>=0.25; extra == 'docs'
62
+ Provides-Extra: metrics
63
+ Requires-Dist: prometheus-client>=0.20; extra == 'metrics'
64
+ Provides-Extra: mongo
65
+ Requires-Dist: pymongo>=4.7; extra == 'mongo'
66
+ Provides-Extra: mysql
67
+ Requires-Dist: dbutils>=3.0; extra == 'mysql'
68
+ Requires-Dist: pymysql>=1.1; extra == 'mysql'
69
+ Provides-Extra: redis
70
+ Requires-Dist: redis>=5.0; extra == 'redis'
71
+ Provides-Extra: render
72
+ Requires-Dist: playwright>=1.44; extra == 'render'
73
+ Description-Content-Type: text/markdown
74
+
75
+ # MineWorker
76
+
77
+ [![PyPI](https://img.shields.io/pypi/v/mineworker)](https://pypi.org/project/mineworker/)
78
+ [![Python](https://img.shields.io/pypi/pyversions/mineworker)](https://pypi.org/project/mineworker/)
79
+ [![CI](https://github.com/apersonw/mineworker/actions/workflows/ci.yml/badge.svg)](https://github.com/apersonw/mineworker/actions/workflows/ci.yml)
80
+ [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
81
+ [![Docs](https://img.shields.io/badge/docs-apersonw.github.io-teal)](https://apersonw.github.io/mineworker/)
82
+
83
+ 一个上手简单、结构清晰的 Python 爬虫框架,对标 [feapder](https://github.com/Boris-code/feapder):
84
+ 你只写 `start_requests` 和 `parse`,框架负责调度、下载、重试、去重、批量落库。
85
+
86
+ > **0.4.0** —— 单机(`AirSpider`)到分布式(`Spider` / `TaskSpider` / `BatchSpider`)全部可用。
87
+ > 变更见 [CHANGELOG](CHANGELOG.md),后续规划见 [Roadmap](https://apersonw.github.io/mineworker/roadmap/)。
88
+
89
+ ## 安装
90
+
91
+ ```bash
92
+ pip install mineworker # 核心
93
+ pip install "mineworker[all]" # 含渲染 / MongoDB / MySQL / Redis / CLI / 指标
94
+ ```
95
+
96
+ 也可以按需装单项:`render` · `mongo` · `mysql` · `redis` · `cli` · `metrics`。
97
+
98
+ ## 快速开始
99
+
100
+ ```bash
101
+ pip install "mineworker[cli]"
102
+ mineworker create -p news_crawler
103
+ cd news_crawler && python main.py
104
+ ```
105
+
106
+ 或直接写:
107
+
108
+ ```python
109
+ import mineworker as mw
110
+
111
+
112
+ class NewsSpider(mw.AirSpider):
113
+ def start_requests(self):
114
+ yield mw.Request("https://news.ycombinator.com/", callback=self.parse)
115
+
116
+ def parse(self, request, response):
117
+ for a in response.css("span.titleline > a"):
118
+ yield {"title": a.css("::text").get(), "url": a.css("::attr(href)").get()}
119
+
120
+
121
+ NewsSpider().start()
122
+ ```
123
+
124
+ ## 文档
125
+
126
+ 完整文档:**<https://apersonw.github.io/mineworker/>**
127
+ (本地预览:`pip install "mineworker[docs]" && mkdocs serve`)
128
+
129
+ 设计与实施计划见 [IMPLEMENTATION_PLAN.md](IMPLEMENTATION_PLAN.md)。
130
+
131
+ ## 能力一览
132
+
133
+ | | |
134
+ |---|---|
135
+ | 运行时 | 单进程多线程、优先级队列、结束检测、`Ctrl-C` 优雅排空、崩溃 dump + `retry` 回放 |
136
+ | 网络 | `Request`/`Response`(httpx + parsel)、自动重试、`validate`/`failed_request` 钩子、随机 UA、会话复用 |
137
+ | 分布式 | `Spider`:Redis 队列 + 布隆去重 + 断点续爬 + 种子一次性锁 + 多节点心跳结束检测 |
138
+ | 任务驱动 | `TaskSpider` 从任务源持续消费;`BatchSpider` 周期批次采集(任务表状态机 + 进度追踪 + 防丢) |
139
+ | 数据 | `Item`/`UpdateItem`、`Pipeline`(Console/CSV/MongoDB/MySQL upsert)、请求级 + Item 级去重(布隆/精确) |
140
+ | 渲染 | `Request(render=True)` —— Playwright 渲染池、`wait_for`/`render_time`/`render_script` |
141
+ | 扩展 | 下载中间件链、代理池接口、账号 / Cookie 池(掉登录自动换号) |
142
+ | 观测 | Prometheus exporter、卡死/失败率告警(飞书/邮件)、`debug=True` |
143
+ | 工具 | `mineworker create/shell/retry`,`create -i --table` 读表结构反射生成 Item |
144
+
145
+ ## 开发
146
+
147
+ ```bash
148
+ conda env create -f environment.yml && conda activate mineworker
149
+ pre-commit install
150
+ pytest && ruff check . && mypy
151
+ ```
152
+
153
+ CI(GitHub Actions):ruff + mypy(strict) + pytest(Python 3.10–3.13)+ mkdocs build。
154
+
155
+ ## License
156
+
157
+ [MIT](LICENSE)
@@ -0,0 +1,83 @@
1
+ # MineWorker
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/mineworker)](https://pypi.org/project/mineworker/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/mineworker)](https://pypi.org/project/mineworker/)
5
+ [![CI](https://github.com/apersonw/mineworker/actions/workflows/ci.yml/badge.svg)](https://github.com/apersonw/mineworker/actions/workflows/ci.yml)
6
+ [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
7
+ [![Docs](https://img.shields.io/badge/docs-apersonw.github.io-teal)](https://apersonw.github.io/mineworker/)
8
+
9
+ 一个上手简单、结构清晰的 Python 爬虫框架,对标 [feapder](https://github.com/Boris-code/feapder):
10
+ 你只写 `start_requests` 和 `parse`,框架负责调度、下载、重试、去重、批量落库。
11
+
12
+ > **0.4.0** —— 单机(`AirSpider`)到分布式(`Spider` / `TaskSpider` / `BatchSpider`)全部可用。
13
+ > 变更见 [CHANGELOG](CHANGELOG.md),后续规划见 [Roadmap](https://apersonw.github.io/mineworker/roadmap/)。
14
+
15
+ ## 安装
16
+
17
+ ```bash
18
+ pip install mineworker # 核心
19
+ pip install "mineworker[all]" # 含渲染 / MongoDB / MySQL / Redis / CLI / 指标
20
+ ```
21
+
22
+ 也可以按需装单项:`render` · `mongo` · `mysql` · `redis` · `cli` · `metrics`。
23
+
24
+ ## 快速开始
25
+
26
+ ```bash
27
+ pip install "mineworker[cli]"
28
+ mineworker create -p news_crawler
29
+ cd news_crawler && python main.py
30
+ ```
31
+
32
+ 或直接写:
33
+
34
+ ```python
35
+ import mineworker as mw
36
+
37
+
38
+ class NewsSpider(mw.AirSpider):
39
+ def start_requests(self):
40
+ yield mw.Request("https://news.ycombinator.com/", callback=self.parse)
41
+
42
+ def parse(self, request, response):
43
+ for a in response.css("span.titleline > a"):
44
+ yield {"title": a.css("::text").get(), "url": a.css("::attr(href)").get()}
45
+
46
+
47
+ NewsSpider().start()
48
+ ```
49
+
50
+ ## 文档
51
+
52
+ 完整文档:**<https://apersonw.github.io/mineworker/>**
53
+ (本地预览:`pip install "mineworker[docs]" && mkdocs serve`)
54
+
55
+ 设计与实施计划见 [IMPLEMENTATION_PLAN.md](IMPLEMENTATION_PLAN.md)。
56
+
57
+ ## 能力一览
58
+
59
+ | | |
60
+ |---|---|
61
+ | 运行时 | 单进程多线程、优先级队列、结束检测、`Ctrl-C` 优雅排空、崩溃 dump + `retry` 回放 |
62
+ | 网络 | `Request`/`Response`(httpx + parsel)、自动重试、`validate`/`failed_request` 钩子、随机 UA、会话复用 |
63
+ | 分布式 | `Spider`:Redis 队列 + 布隆去重 + 断点续爬 + 种子一次性锁 + 多节点心跳结束检测 |
64
+ | 任务驱动 | `TaskSpider` 从任务源持续消费;`BatchSpider` 周期批次采集(任务表状态机 + 进度追踪 + 防丢) |
65
+ | 数据 | `Item`/`UpdateItem`、`Pipeline`(Console/CSV/MongoDB/MySQL upsert)、请求级 + Item 级去重(布隆/精确) |
66
+ | 渲染 | `Request(render=True)` —— Playwright 渲染池、`wait_for`/`render_time`/`render_script` |
67
+ | 扩展 | 下载中间件链、代理池接口、账号 / Cookie 池(掉登录自动换号) |
68
+ | 观测 | Prometheus exporter、卡死/失败率告警(飞书/邮件)、`debug=True` |
69
+ | 工具 | `mineworker create/shell/retry`,`create -i --table` 读表结构反射生成 Item |
70
+
71
+ ## 开发
72
+
73
+ ```bash
74
+ conda env create -f environment.yml && conda activate mineworker
75
+ pre-commit install
76
+ pytest && ruff check . && mypy
77
+ ```
78
+
79
+ CI(GitHub Actions):ruff + mypy(strict) + pytest(Python 3.10–3.13)+ mkdocs build。
80
+
81
+ ## License
82
+
83
+ [MIT](LICENSE)
@@ -0,0 +1,53 @@
1
+ # API
2
+
3
+ ::: mineworker.AirSpider
4
+ options:
5
+ members: [start, stop]
6
+
7
+ ::: mineworker.Spider
8
+ options:
9
+ members: [start, stop]
10
+
11
+ ::: mineworker.TaskSpider
12
+ options:
13
+ members: [task_requests, fetch_tasks, add_tasks, push_tasks, start, stop]
14
+
15
+ ::: mineworker.BatchSpider
16
+ options:
17
+ members: [task_requests, update_task, failed_request, start, start_monitor, stop]
18
+
19
+ ::: mineworker.core.batch_store.BatchStore
20
+
21
+ ::: mineworker.core.batch_monitor.BatchMonitor
22
+ options:
23
+ members: [run, run_once, stop]
24
+
25
+ ::: mineworker.BaseParser
26
+
27
+ ::: mineworker.Request
28
+
29
+ ::: mineworker.Response
30
+
31
+ ::: mineworker.Item
32
+
33
+ ::: mineworker.UpdateItem
34
+
35
+ ::: mineworker.pipelines.base.BasePipeline
36
+
37
+ ::: mineworker.pipelines.mysql.MysqlPipeline
38
+
39
+ ::: mineworker.db.mysqldb.MysqlDB
40
+
41
+ ::: mineworker.User
42
+
43
+ ::: mineworker.network.user_pool.base.UserPool
44
+
45
+ ::: mineworker.LocalUserPool
46
+
47
+ ::: mineworker.GuestUserPool
48
+
49
+ ::: mineworker.RedisUserPool
50
+
51
+ ::: mineworker.network.middleware.DownloaderMiddleware
52
+
53
+ ::: mineworker.network.proxy_pool.base.ProxyPool
@@ -0,0 +1,81 @@
1
+ # async 内核评估
2
+
3
+ > Roadmap 里「async 内核 —— 评估用 async httpx 替换线程模型」的结论。
4
+ > **结论:不做全量 async 重写;落地一个隔离的异步下载器 `AsyncHttpxDownloader` 作为可选加速项。**
5
+
6
+ ## 现在的线程模型
7
+
8
+ ```
9
+ 主线程 run(): 注入种子 → 起线程 → _wait_until_done 轮询 → teardown
10
+ N×ParserWorker collector.get → 中间件 → download_request(阻塞 httpx) → validate → parse → 分发
11
+ RequestBuffer 线程,周期 flush → 去重 → 任务队列
12
+ ItemBuffer 线程,周期 flush → 去重 → pipeline.save_items(阻塞 pymongo / pymysql)
13
+ _Heartbeat 线程,周期 hset(阻塞 redis) —— 仅 Spider
14
+ _TaskPoller 线程,周期 lpop(阻塞 redis) —— 仅 TaskSpider
15
+ _RenderPool pool_size×线程,各持一个 sync chromium
16
+ ```
17
+
18
+ 并发靠 `SPIDER_THREAD_COUNT` 个工作线程。socket 等待时 GIL 释放,lxml 解析在 C 扩展里
19
+ 也释放 GIL,所以线程模型对 I/O 密集抓取是够用的——feapder 本身就这么跑生产。
20
+
21
+ ## async 能买到什么 / 买不到什么
22
+
23
+ | | 说明 |
24
+ |---|---|
25
+ | ✅ 单线程驱动上千并发连接 | 每连接开销更低、FD 更省。仅在「大扇出 + 解析极轻 + 目标站不限速」时真正兑现 |
26
+ | ✅ `redis.asyncio` / `httpx.AsyncClient` / playwright async API | 都现成 |
27
+ | ⚠️ 解析是 CPU-bound | 单事件循环会把所有 `parse` 串行化,最终还得甩进 executor → 又回到线程 |
28
+ | ❌ 落库 | `pymongo` / `pymysql` 没有干净的 async 路径(要换 `motor` / `aiomysql`)。而 ItemBuffer 本来就批量写,不是瓶颈 |
29
+ | ❌ feapder 心智兼容 | 用户写同步 `def parse` + `yield` 是明确约束。async 内核要么逼用户写 `async def`,要么陷入混合模型 |
30
+
31
+ **混合模型是陷阱**:保留同步 `def parse` 丢进 executor 跑 → 事件循环**和**线程池同时存在,
32
+ 只把「下载等待」挪出了线程,解析吞吐照旧,还多一套并发模型要 debug。Scrapy 能走 async 是
33
+ 因为它整个 API 都是 async 原生的——那正是本项目排除掉的路。
34
+
35
+ ## 重写的成本
36
+
37
+ 几乎是整个 core + network + dedup + 一半测试:`base_scheduler`(TaskGroup / `asyncio.Queue` /
38
+ 信号)、`parser_control`、`collector`、两个 buffer、`task_queue`、`_httpx`、`downloader/base`
39
+ (连带 `Request.download()` 和 `mineworker shell` 的同步入口)、`redis_scheduler` /
40
+ `redis_task_scheduler` / `redis_filter`、`proxy_pool`、`user_pool/redis`,以及
41
+ `test_air_spider` / `test_spider` / `test_task_spider` / `test_integration_httpserver` /
42
+ `test_downloader` / `test_render` / `test_spider_persistence` 全部重写(现有测试深度依赖
43
+ 「`start()` 阻塞 + 线程不泄漏 + `threading.Timer` 停止」)。这是 v3 级别的工作量。
44
+
45
+ 投入产出比不成立:收益只在少数派工作负载上兑现,而那类负载通常先撞上目标站限速。
46
+
47
+ ## 落地:`AsyncHttpxDownloader`
48
+
49
+ 作为评估的产出,做了一个**隔离的异步下载器**,捕获大部分收益而 API 零改动:
50
+
51
+ - 一个专属事件循环线程 + 一个共享 `httpx.AsyncClient` 承载所有在途连接
52
+ - 对外仍是同步 `Downloader.download()`:工作线程提交协程到内部 loop 并阻塞等结果(和渲染池同套路)
53
+ - 连接池 / keep-alive / HTTP/2 多路复用被**所有 worker 共享**(同步下载器在 `use_session=False`
54
+ 时每请求新建 client,没有 keep-alive)
55
+ - `DOWNLOADER_ASYNC_CONCURRENCY` 信号量 + 连接池上限双重限流
56
+
57
+ ```python
58
+ # setting.py
59
+ DOWNLOADER_ASYNC = True # 普通请求走 AsyncHttpxDownloader
60
+ DOWNLOADER_ASYNC_CONCURRENCY = 200 # 最大在途请求数
61
+ HTTPX_HTTP2 = True # 需 pip install "httpx[http2]",同步 / 异步下载器都生效
62
+ ```
63
+
64
+ `render=True` 的请求不受影响(仍走渲染池)。
65
+
66
+ ### 目前的天花板
67
+
68
+ 工作线程是「1 线程 : 1 在途请求」,所以真正在途的请求数仍 ≈ `SPIDER_THREAD_COUNT`。
69
+ `AsyncHttpxDownloader` 现在的收益是**连接复用 + HTTP/2 + 更低 FD**,不是「少量线程跑上千并发」。
70
+
71
+ 要突破这个天花板,需要 worker 侧批量分发:少量 worker,每个从 collector 取一批、
72
+ `await asyncio.gather(*downloads)` 拿到全部响应后再逐个 `parse`。这会改动 `parser_control`
73
+ 和结束检测,属于「真需要时再做」——目前没有实测证据表明线程调度是瓶颈。
74
+
75
+ ## 何时重新评估
76
+
77
+ - 实测某目标:在途连接数远小于期望、且 CPU / 带宽 / 目标站限速都不是瓶颈
78
+ - 需要单机十万级并发连接(此时线程栈内存和上下文切换才真正咬人)
79
+
80
+ 否则:调大 `SPIDER_THREAD_COUNT`(开到 ~100 无妨)、开 `HTTPX_HTTP2`、开 `DOWNLOADER_ASYNC`
81
+ 拿连接复用,通常就够了。
@@ -0,0 +1,110 @@
1
+ # 批次采集(BatchSpider)
2
+
3
+ `BatchSpider` 用于**周期性全量 / 增量批次采集**:任务放在 MySQL 任务表里,每隔
4
+ `BATCH_INTERVAL` 天开一个新批次、重跑全部任务,进度写在批次记录表,卡死的任务会被
5
+ 自动回收重跑。
6
+
7
+ ```bash
8
+ pip install "mineworker[redis,mysql]"
9
+ ```
10
+
11
+ ## 任务表
12
+
13
+ 由你自己建,至少要有:主键列、状态列、更新时间列(防丢检测用)。
14
+
15
+ ```sql
16
+ CREATE TABLE `crawl_task` (
17
+ `id` BIGINT NOT NULL AUTO_INCREMENT,
18
+ `url` VARCHAR(500) NOT NULL,
19
+ `batch_status` TINYINT NOT NULL DEFAULT 0, -- 0 待处理 / 1 完成 / 2 处理中 / -1 失败
20
+ `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
21
+ PRIMARY KEY (`id`)
22
+ );
23
+ ```
24
+
25
+ 列名可配(`BATCH_TASK_ID_FIELD` / `BATCH_TASK_STATE_FIELD` / `BATCH_TASK_TIME_FIELD`)。
26
+ 批次记录表 `crawl_task_batch_record` 由框架自动建。
27
+
28
+ ## 写爬虫
29
+
30
+ ```python
31
+ import mineworker as mw
32
+
33
+
34
+ class CrawlTask(mw.BatchSpider):
35
+ __task_table__ = "crawl_task"
36
+
37
+ def task_requests(self, task): # 一行任务 -> 请求(必须实现)
38
+ yield mw.Request(task["url"], callback=self.parse)
39
+
40
+ def parse(self, request, response, task): # 框架自动带上 task
41
+ yield {"url": task["url"], "title": response.css("h1::text").get()}
42
+ self.update_task(task["id"], ok=True) # 关键:回写任务状态
43
+
44
+
45
+ if __name__ == "__main__":
46
+ import sys
47
+
48
+ if sys.argv[1:] == ["monitor"]:
49
+ CrawlTask().start_monitor() # master
50
+ else:
51
+ CrawlTask(keep_alive=True).start() # worker
52
+ ```
53
+
54
+ `self.update_task(task_id, ok=True)` 标完成,`ok=False` 标失败(不再重试)。
55
+ 不回写的任务会一直停在「处理中」,被防丢机制反复重跑。重试耗尽的请求,框架默认帮你
56
+ `update_task(..., ok=False)`(覆写 `failed_request` 时记得 `super()`)。
57
+
58
+ ## 两种角色
59
+
60
+ | | 启动 | 职责 |
61
+ |---|---|---|
62
+ | **master** | `spider.start_monitor()` | 批次生命周期:到点开新批次(重置任务表)、把待处理任务推进 Redis 待抓队列、按任务表刷新进度、回收卡死任务、批次跑完收尾 |
63
+ | **worker** | `spider.start()` | 消费 Redis 队列,对每个任务调 `task_requests`,解析完回写状态。可多进程 / 多机 |
64
+
65
+ 同一命名空间只允许一个 master(Redis 锁),worker 随便起几个。
66
+
67
+ ```bash
68
+ python main.py monitor # 一台,常驻;或 cron 每天跑一次 start_monitor(once=True)
69
+ python main.py # 多台 / 多进程,worker
70
+ ```
71
+
72
+ - `start_monitor(once=True)`:把当前批次跑到完成即返回,适合塞进 crontab。
73
+ - `start_monitor()`:常驻,一个批次跑完后等到下个 `BATCH_INTERVAL` 再开下一批。
74
+ - worker 建议 `keep_alive=True` 常驻(等 master 派活);`keep_alive=False` 则队列抽干即退出。
75
+
76
+ ## 一次巡检做什么
77
+
78
+ master 每 `BATCH_MONITOR_INTERVAL` 秒:
79
+
80
+ 1. 把卡在「处理中」超过 `BATCH_LOST_TASK_STALE` 秒的任务重置回「待处理」
81
+ 2. 认领「待处理」任务(置为处理中),最多 `BATCH_PUSH_LIMIT` 个,推进 Redis 队列 `<ns>:batch_pending`
82
+ 3. 按任务表统计刷新批次记录的 done / fail / total,打印「批次进度 X/Y」
83
+ 4. 所有任务都已结算(完成 + 失败 == 总数)→ 批次记录 `is_done=1`,置 `<ns>:batch_done` 标志
84
+
85
+ ## 换存储后端
86
+
87
+ 默认落 MySQL(`MysqlBatchStore`)。测试或小规模内存跑批可传 `batch_store=`:
88
+
89
+ ```python
90
+ from mineworker.core.batch_store import MemoryBatchStore
91
+
92
+ store = MemoryBatchStore([{"id": 1, "url": "..."}, {"id": 2, "url": "..."}])
93
+ CrawlTask(batch_store=store).start_monitor(once=True)
94
+ ```
95
+
96
+ 自定义后端继承 `mineworker.core.batch_store.BatchStore`。
97
+
98
+ ## 配置
99
+
100
+ | 配置 | 默认 | 说明 |
101
+ |---|---|---|
102
+ | `BATCH_INTERVAL` / `BATCH_INTERVAL_UNIT` | `7` / `"day"` | 批次间隔(`day` \| `hour`) |
103
+ | `BATCH_MONITOR_INTERVAL` | `10.0` | master 巡检间隔(秒) |
104
+ | `BATCH_LOST_TASK_STALE` | `600.0` | 「处理中」超过这么久算丢,重置回待处理 |
105
+ | `BATCH_PUSH_LIMIT` | `5000` | master 单次最多认领 / 推送多少任务 |
106
+ | `BATCH_TASK_ID_FIELD` | `"id"` | 任务表主键列名 |
107
+ | `BATCH_TASK_STATE_FIELD` | `"batch_status"` | 任务表状态列名 |
108
+ | `BATCH_TASK_TIME_FIELD` | `"update_time"` | 任务表更新时间列名 |
109
+
110
+ 队列 / 去重 / 命名空间沿用 [`Spider`](distributed.md) 那套(Redis)。
@@ -0,0 +1,46 @@
1
+ # 命令行
2
+
3
+ 需要 `pip install "mineworker[cli]"`。
4
+
5
+ ## create
6
+
7
+ ```bash
8
+ mineworker create -p news_crawler # 项目脚手架
9
+ mineworker create -s ProductSpider # 一个 AirSpider(写到 ./product_spider.py)
10
+ mineworker create -i ProductItem # 一个 Item
11
+ mineworker create -i news --table news # 读 MySQL 表结构反射字段(需 [mysql])
12
+ mineworker create --setting # 一份注释齐全的 setting.py
13
+ mineworker create -s Foo --force # 覆盖已存在文件
14
+ ```
15
+
16
+ 名字会自动转换:`product-list` / `product_list` / `ProductList` 都能识别,
17
+ 生成 `class ProductListSpider` + 文件 `product_list_spider.py`。
18
+
19
+ `-i --table <表名>` 会连 MySQL 读 `SHOW FULL COLUMNS`,按主键填 `__unique_key__` 并把字段
20
+ + 注释列进 Item。连接默认取 `setting` 的 `MYSQL_*`,也可 `--mysql mysql://user:pwd@host:3306/db` 覆盖。
21
+
22
+ ## shell
23
+
24
+ ```bash
25
+ mineworker shell https://example.com
26
+ mineworker shell https://spa.example.com --render
27
+ ```
28
+
29
+ 抓一个页面进交互式 shell,绑定 `request` / `response` / `mw`。装了 IPython 用 IPython,
30
+ 否则用内置 REPL(带 tab 补全)。
31
+
32
+ ## retry
33
+
34
+ ```bash
35
+ mineworker retry --items # 重放 failed_items.jsonl 到当前 ITEM_PIPELINES
36
+ mineworker retry --requests # 重新下载 failed_requests.jsonl 里的 URL
37
+ mineworker retry # 两者都做
38
+ ```
39
+
40
+ 仍失败的记录写回文件,全部成功则删除文件。
41
+
42
+ ## 版本
43
+
44
+ ```bash
45
+ mineworker --version
46
+ ```