nosp 0.8.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.
- nosp-0.8.1/PKG-INFO +67 -0
- nosp-0.8.1/README.md +55 -0
- nosp-0.8.1/nosp/__init__.py +2 -0
- nosp-0.8.1/nosp/cache.py +104 -0
- nosp-0.8.1/nosp/config.py +34 -0
- nosp-0.8.1/nosp/core.py +41 -0
- nosp-0.8.1/nosp/database.py +451 -0
- nosp-0.8.1/nosp/err.py +5 -0
- nosp-0.8.1/nosp/file.py +381 -0
- nosp-0.8.1/nosp/http.py +380 -0
- nosp-0.8.1/nosp/lazy.py +12 -0
- nosp-0.8.1/nosp/monitor.py +359 -0
- nosp-0.8.1/nosp/mq.py +204 -0
- nosp-0.8.1/nosp/parse.py +399 -0
- nosp-0.8.1/nosp/spider.py +122 -0
- nosp-0.8.1/nosp/utils.py +100 -0
- nosp-0.8.1/nosp.egg-info/PKG-INFO +67 -0
- nosp-0.8.1/nosp.egg-info/SOURCES.txt +21 -0
- nosp-0.8.1/nosp.egg-info/dependency_links.txt +1 -0
- nosp-0.8.1/nosp.egg-info/requires.txt +5 -0
- nosp-0.8.1/nosp.egg-info/top_level.txt +1 -0
- nosp-0.8.1/pyproject.toml +13 -0
- nosp-0.8.1/setup.cfg +4 -0
nosp-0.8.1/PKG-INFO
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nosp
|
|
3
|
+
Version: 0.8.1
|
|
4
|
+
Author-email: noybzy <noybzy@qq.com>
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: dbutils==3.1.0
|
|
8
|
+
Requires-Dist: loguru>=0.7.3
|
|
9
|
+
Requires-Dist: parsel==1.10.0
|
|
10
|
+
Requires-Dist: pymysql==1.1.1
|
|
11
|
+
Requires-Dist: requests>=2.32.3
|
|
12
|
+
|
|
13
|
+
# NOSP:轻量爬取工具
|
|
14
|
+
|
|
15
|
+
**NOSP** 是一个轻量级、模块化的 Python 框架,专为高效的网络爬取而设计。它为开发者提供了构建健壮且可扩展的网络爬虫所需的基本工具,同时处理常见的技术挑战,如并发、HTTP 请求、内容解析和数据存储。
|
|
16
|
+
|
|
17
|
+
作为一个专门的工具包,NOSP 通过提供预构建的组件,简化了复杂的爬取流程,帮助开发者专注于数据提取,而非底层实现细节。
|
|
18
|
+
|
|
19
|
+
------
|
|
20
|
+
|
|
21
|
+
## 主要特性
|
|
22
|
+
|
|
23
|
+
| 特性 | 描述 |
|
|
24
|
+
| ----------------- | ------------------------------------------------------- |
|
|
25
|
+
| **多线程爬取** | 内置线程池管理,支持高效并发操作 |
|
|
26
|
+
| **HTTP 请求处理** | 强大的请求管理,支持重试机制、代理配置、请求/响应拦截器 |
|
|
27
|
+
| **内容解析** | 基于 XPath 的强大内容提取工具,支持结构化数据抽取 |
|
|
28
|
+
| **数据库集成** | 直接集成 MySQL 和 Redis,支持便捷的数据存储与缓存 |
|
|
29
|
+
| **错误处理** | 全面的异常管理机制,确保爬虫在异常情况下的稳定性与韧性 |
|
|
30
|
+
| **资源提取** | 提供专用工具,用于从网页中提取附件、图片等二进制资源 |
|
|
31
|
+
|
|
32
|
+
------
|
|
33
|
+
|
|
34
|
+
## 框架架构
|
|
35
|
+
|
|
36
|
+
NOSP 采用**模块化设计**,将关注点分离到独立组件中,同时保持清晰的接口定义。这种设计允许开发者按需使用特定模块,提升灵活性与可维护性。
|
|
37
|
+
|
|
38
|
+
### 核心组件
|
|
39
|
+
|
|
40
|
+
- **`BaseSpider` 类**:框架的核心,负责协调爬取工作流程,调度请求、解析与存储。
|
|
41
|
+
- **模块化结构**:各功能模块(如请求、解析、存储)松耦合,易于替换或扩展。
|
|
42
|
+
|
|
43
|
+
------
|
|
44
|
+
|
|
45
|
+
## 何时使用 NOSP?
|
|
46
|
+
|
|
47
|
+
NOSP 在以下场景中特别有价值:
|
|
48
|
+
|
|
49
|
+
- ✅ 需要构建**可维护、结构化**的网络爬虫
|
|
50
|
+
- ✅ 项目要求**高效率**,需支持并发操作
|
|
51
|
+
- ✅ 希望避免重复实现常见功能(如代理管理、解析逻辑、数据存储)
|
|
52
|
+
- ✅ 需要一个**灵活可扩展**的基础框架,适应不同爬取需求
|
|
53
|
+
|
|
54
|
+
> **适用范围**:既适用于简单的数据抓取任务,也足以支撑复杂的生产级爬虫系统。
|
|
55
|
+
|
|
56
|
+
------
|
|
57
|
+
|
|
58
|
+
## 核心概念
|
|
59
|
+
|
|
60
|
+
NOSP 围绕以下核心概念构建,理解这些概念是高效使用框架的关键:
|
|
61
|
+
|
|
62
|
+
| 概念 | 说明 |
|
|
63
|
+
| ------------------- | ------------------------------------------------------------ |
|
|
64
|
+
| **爬虫 (Spider)** | 协调整个爬取流程的核心类,定义起始 URL、解析逻辑和后续请求 |
|
|
65
|
+
| **请求 (Request)** | 封装 HTTP 请求,支持自定义 headers、cookies、代理、重试等 |
|
|
66
|
+
| **解析器 (Parser)** | 负责从网页 HTML 内容中提取结构化数据,支持 XPath 和正则表达式 |
|
|
67
|
+
| **存储 (Storage)** | 处理爬取数据的持久化,支持写入数据库(MySQL/Redis)或本地文件 |
|
nosp-0.8.1/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# NOSP:轻量爬取工具
|
|
2
|
+
|
|
3
|
+
**NOSP** 是一个轻量级、模块化的 Python 框架,专为高效的网络爬取而设计。它为开发者提供了构建健壮且可扩展的网络爬虫所需的基本工具,同时处理常见的技术挑战,如并发、HTTP 请求、内容解析和数据存储。
|
|
4
|
+
|
|
5
|
+
作为一个专门的工具包,NOSP 通过提供预构建的组件,简化了复杂的爬取流程,帮助开发者专注于数据提取,而非底层实现细节。
|
|
6
|
+
|
|
7
|
+
------
|
|
8
|
+
|
|
9
|
+
## 主要特性
|
|
10
|
+
|
|
11
|
+
| 特性 | 描述 |
|
|
12
|
+
| ----------------- | ------------------------------------------------------- |
|
|
13
|
+
| **多线程爬取** | 内置线程池管理,支持高效并发操作 |
|
|
14
|
+
| **HTTP 请求处理** | 强大的请求管理,支持重试机制、代理配置、请求/响应拦截器 |
|
|
15
|
+
| **内容解析** | 基于 XPath 的强大内容提取工具,支持结构化数据抽取 |
|
|
16
|
+
| **数据库集成** | 直接集成 MySQL 和 Redis,支持便捷的数据存储与缓存 |
|
|
17
|
+
| **错误处理** | 全面的异常管理机制,确保爬虫在异常情况下的稳定性与韧性 |
|
|
18
|
+
| **资源提取** | 提供专用工具,用于从网页中提取附件、图片等二进制资源 |
|
|
19
|
+
|
|
20
|
+
------
|
|
21
|
+
|
|
22
|
+
## 框架架构
|
|
23
|
+
|
|
24
|
+
NOSP 采用**模块化设计**,将关注点分离到独立组件中,同时保持清晰的接口定义。这种设计允许开发者按需使用特定模块,提升灵活性与可维护性。
|
|
25
|
+
|
|
26
|
+
### 核心组件
|
|
27
|
+
|
|
28
|
+
- **`BaseSpider` 类**:框架的核心,负责协调爬取工作流程,调度请求、解析与存储。
|
|
29
|
+
- **模块化结构**:各功能模块(如请求、解析、存储)松耦合,易于替换或扩展。
|
|
30
|
+
|
|
31
|
+
------
|
|
32
|
+
|
|
33
|
+
## 何时使用 NOSP?
|
|
34
|
+
|
|
35
|
+
NOSP 在以下场景中特别有价值:
|
|
36
|
+
|
|
37
|
+
- ✅ 需要构建**可维护、结构化**的网络爬虫
|
|
38
|
+
- ✅ 项目要求**高效率**,需支持并发操作
|
|
39
|
+
- ✅ 希望避免重复实现常见功能(如代理管理、解析逻辑、数据存储)
|
|
40
|
+
- ✅ 需要一个**灵活可扩展**的基础框架,适应不同爬取需求
|
|
41
|
+
|
|
42
|
+
> **适用范围**:既适用于简单的数据抓取任务,也足以支撑复杂的生产级爬虫系统。
|
|
43
|
+
|
|
44
|
+
------
|
|
45
|
+
|
|
46
|
+
## 核心概念
|
|
47
|
+
|
|
48
|
+
NOSP 围绕以下核心概念构建,理解这些概念是高效使用框架的关键:
|
|
49
|
+
|
|
50
|
+
| 概念 | 说明 |
|
|
51
|
+
| ------------------- | ------------------------------------------------------------ |
|
|
52
|
+
| **爬虫 (Spider)** | 协调整个爬取流程的核心类,定义起始 URL、解析逻辑和后续请求 |
|
|
53
|
+
| **请求 (Request)** | 封装 HTTP 请求,支持自定义 headers、cookies、代理、重试等 |
|
|
54
|
+
| **解析器 (Parser)** | 负责从网页 HTML 内容中提取结构化数据,支持 XPath 和正则表达式 |
|
|
55
|
+
| **存储 (Storage)** | 处理爬取数据的持久化,支持写入数据库(MySQL/Redis)或本地文件 |
|
nosp-0.8.1/nosp/cache.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from typing import Optional, Callable, Any, Dict, TypeVar, Generic
|
|
3
|
+
|
|
4
|
+
from loguru import logger
|
|
5
|
+
|
|
6
|
+
from nosp.config import RedisConfig
|
|
7
|
+
from nosp.database import Redis
|
|
8
|
+
from nosp.utils import get_md5
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CacheBaseModel:
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self._on_change: Optional[Callable[[str, str], None]] = None
|
|
14
|
+
self._on_get: Optional[Callable[[str], str]] = None
|
|
15
|
+
self._live = False
|
|
16
|
+
|
|
17
|
+
def __init_subclass__(cls):
|
|
18
|
+
cls._on_change: Optional[Callable[[str, str], None]] = None
|
|
19
|
+
cls._on_get: Optional[Callable[[str], str]] = None
|
|
20
|
+
cls._live = False
|
|
21
|
+
|
|
22
|
+
def on_change(self, callback: Callable[[str, str], None]) -> None:
|
|
23
|
+
"""注册变更回调"""
|
|
24
|
+
self._on_change = callback
|
|
25
|
+
|
|
26
|
+
def on_get(self, callback: Callable[[str], str]) -> None:
|
|
27
|
+
"""注册Get回调"""
|
|
28
|
+
self._on_get = callback
|
|
29
|
+
|
|
30
|
+
def __setattr__(self, key: str, value: Any):
|
|
31
|
+
super().__setattr__(key, value)
|
|
32
|
+
if not key.startswith("_") and hasattr(self, '_on_change') and self._on_change is not None:
|
|
33
|
+
self._on_change(key, str(value))
|
|
34
|
+
|
|
35
|
+
def __getattribute__(self, key: str) -> Any:
|
|
36
|
+
if key.startswith("_"):
|
|
37
|
+
return super().__getattribute__(key)
|
|
38
|
+
|
|
39
|
+
if hasattr(self, '_live') and self._live and hasattr(self, '_on_get') and self._on_get is not None:
|
|
40
|
+
try:
|
|
41
|
+
result = self._on_get(key)
|
|
42
|
+
if result is None and super().__getattribute__(key) is not None:
|
|
43
|
+
return super().__getattribute__(key)
|
|
44
|
+
return self._on_get(key)
|
|
45
|
+
except Exception as e:
|
|
46
|
+
logger.warning(f"on_get({key}) failed: {e}")
|
|
47
|
+
|
|
48
|
+
return super().__getattribute__(key)
|
|
49
|
+
|
|
50
|
+
def set(self, data: Dict[str, Any]):
|
|
51
|
+
annotations = self.__class__.__annotations__
|
|
52
|
+
for k, v in data.items():
|
|
53
|
+
if k in annotations:
|
|
54
|
+
setattr(self, k, v)
|
|
55
|
+
|
|
56
|
+
def get(self) -> Dict[str, str]:
|
|
57
|
+
return {
|
|
58
|
+
k: str(getattr(self, k))
|
|
59
|
+
for k in self._class__.__annotations__
|
|
60
|
+
if hasattr(self, k)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
T = TypeVar('T', bound=CacheBaseModel)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Cache(Generic[T]):
|
|
68
|
+
def __init__(self, cfg: RedisConfig, db=15, data_type=None, live=False, key=None):
|
|
69
|
+
self.key = key if key else get_md5(sys.modules.get("__main__").__file__)
|
|
70
|
+
self.redis = Redis.simple(cfg, db=db)
|
|
71
|
+
self.data: Optional[T] = None
|
|
72
|
+
if data_type is not None:
|
|
73
|
+
self.data: Optional[T] = data_type()
|
|
74
|
+
self._load()
|
|
75
|
+
self.data._live = live
|
|
76
|
+
|
|
77
|
+
def _save_to_redis(self, key, value):
|
|
78
|
+
"""私有方法:保存数据到 Redis"""
|
|
79
|
+
try:
|
|
80
|
+
self.redis.r.hset(self.key, key, value)
|
|
81
|
+
except Exception as e:
|
|
82
|
+
logger.warning(f"Save failed: {e}")
|
|
83
|
+
|
|
84
|
+
def _on_get(self, key: str) -> str:
|
|
85
|
+
return self.redis.r.hget(self.key, key)
|
|
86
|
+
|
|
87
|
+
def _load(self):
|
|
88
|
+
if self.data is None:
|
|
89
|
+
return None
|
|
90
|
+
data = self.redis.r.hgetall(self.key)
|
|
91
|
+
if data and isinstance(data, dict):
|
|
92
|
+
try:
|
|
93
|
+
self.data.set(data)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.warning(f"Load failed: {e}")
|
|
96
|
+
self.data.on_change(self._save_to_redis)
|
|
97
|
+
self.data.on_get(self._on_get)
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
def get(self, key: str) -> str:
|
|
101
|
+
return self.redis.r.hget(self.key, key)
|
|
102
|
+
|
|
103
|
+
def set(self, key: str, value):
|
|
104
|
+
return self.redis.r.hset(self.key, key, value)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
class MysqlConfig(object):
|
|
2
|
+
def __init__(self, host: str, port: int, username: str, password: str, db: str, charset: str = 'utf8mb4'):
|
|
3
|
+
self.host = host
|
|
4
|
+
self.port = port
|
|
5
|
+
self.username = username
|
|
6
|
+
self.password = password
|
|
7
|
+
self.db = db
|
|
8
|
+
self.charset = charset
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RedisConfig(object):
|
|
12
|
+
def __init__(self, host: str, port: int, password: str, db: int):
|
|
13
|
+
self.host = host
|
|
14
|
+
self.port = port
|
|
15
|
+
self.password = password
|
|
16
|
+
self.db = db
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RabbitMQConfig(object):
|
|
20
|
+
def __init__(self, host: str, port: int, username: str, password: str, virtual_host: str = '/'):
|
|
21
|
+
self.host = host
|
|
22
|
+
self.port = port
|
|
23
|
+
self.username = username
|
|
24
|
+
self.password = password
|
|
25
|
+
self.virtual_host = virtual_host
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class OssConfig(object):
|
|
29
|
+
def __init__(self, access_key: str, access_secret: str, endpoint: str, bucket_name: str, region: str = None):
|
|
30
|
+
self.access_key = access_key
|
|
31
|
+
self.access_secret = access_secret
|
|
32
|
+
self.endpoint = endpoint
|
|
33
|
+
self.bucket_name = bucket_name
|
|
34
|
+
self.region = region
|
nosp-0.8.1/nosp/core.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def load_path():
|
|
7
|
+
"""
|
|
8
|
+
向上查找 [config.py or pyproject.toml] 所在目录,并将其添加到 sys.path 中。
|
|
9
|
+
"""
|
|
10
|
+
current_dir = Path(sys.modules.get("__main__").__file__).resolve().parent
|
|
11
|
+
root_dir = None
|
|
12
|
+
for parent in [current_dir, *current_dir.parents]:
|
|
13
|
+
if (parent / "config.py").exists() or (parent / "pyproject.toml").exists():
|
|
14
|
+
root_dir = parent
|
|
15
|
+
break
|
|
16
|
+
if root_dir is not None:
|
|
17
|
+
root_path = str(root_dir)
|
|
18
|
+
if sys.path[0] != root_path:
|
|
19
|
+
sys.path.insert(0, root_path)
|
|
20
|
+
print(f"[load_path] 添加路径到 sys.path: {root_path}")
|
|
21
|
+
else:
|
|
22
|
+
print("[load_path] 未找到 config.py 或 pyproject.toml,未修改 sys.path。")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_env(filepath='.env'):
|
|
27
|
+
"""
|
|
28
|
+
从指定路径读取 .env 文件,并将变量加载到 os.environ 中。
|
|
29
|
+
:param filepath: .env 文件路径,默认为当前目录下的 .env
|
|
30
|
+
"""
|
|
31
|
+
if not os.path.isfile(filepath):
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
with open(filepath, encoding='utf-8') as file:
|
|
35
|
+
for line in file:
|
|
36
|
+
line = line.strip()
|
|
37
|
+
if not line or line.startswith('#'):
|
|
38
|
+
continue
|
|
39
|
+
if '=' in line:
|
|
40
|
+
key, value = line.split('=', 1)
|
|
41
|
+
os.environ[key] = value.strip('"\' ')
|