myssia-classifier 0.0.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.
- myssia_classifier-0.0.1/.gitignore +113 -0
- myssia_classifier-0.0.1/LICENSE +21 -0
- myssia_classifier-0.0.1/PKG-INFO +98 -0
- myssia_classifier-0.0.1/README.md +73 -0
- myssia_classifier-0.0.1/myssia_classifier/__init__.py +139 -0
- myssia_classifier-0.0.1/myssia_classifier/builtin.py +433 -0
- myssia_classifier-0.0.1/myssia_classifier/custom.py +431 -0
- myssia_classifier-0.0.1/myssia_classifier/data/keywords.json +181 -0
- myssia_classifier-0.0.1/myssia_classifier/py.typed +0 -0
- myssia_classifier-0.0.1/pyproject.toml +50 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# ===== 安全红线:私有数据零上传 =====
|
|
2
|
+
# 本仓库与 .trellis/ 都会公开;任何凭据、抓取数据、密钥文件不得入库。
|
|
3
|
+
# MYIA 凭据只走 env:/keychain: 引用,配置文件出现明文凭据即拒跑(spec 铁律)。
|
|
4
|
+
|
|
5
|
+
# ---- 密钥与凭据文件(防误拷入仓库)----
|
|
6
|
+
*.pem
|
|
7
|
+
*.key
|
|
8
|
+
*.p12
|
|
9
|
+
*.pfx
|
|
10
|
+
secrets.*
|
|
11
|
+
# 例外:核心源码模块不叫"secrets 文件",必须入库(fresh clone / wheel 缺它即崩)
|
|
12
|
+
!src/myssia/secrets.py
|
|
13
|
+
.env
|
|
14
|
+
.env.*
|
|
15
|
+
.envrc
|
|
16
|
+
|
|
17
|
+
# ---- 本地私有路径索引(真实姓名/内网拓扑,仅本机)----
|
|
18
|
+
LOCAL-NOTES.md
|
|
19
|
+
|
|
20
|
+
# ---- MYIA 运行时数据(抓取的情报内容属私有数据)----
|
|
21
|
+
myssia.db
|
|
22
|
+
# 改名过渡期旧缺省名落下的运行时库(桌面 serve 仓库 cwd 直跑),同零容忍口径
|
|
23
|
+
/myia.db
|
|
24
|
+
*.db-journal
|
|
25
|
+
*.db-wal
|
|
26
|
+
*.db-shm
|
|
27
|
+
data/
|
|
28
|
+
# 例外:随包分发的程序数据(prompt/关键词表)不是抓取数据,必须入库与进 wheel
|
|
29
|
+
# (classify 词表已随 10-01-v10-classifier-pypi 拆去 myssia-classifier 包,例外同步删;
|
|
30
|
+
# 模块改名后路径须跟 src/myssia,否则 hatchling 按 gitignore 吞数据文件进不了 wheel)
|
|
31
|
+
!src/myssia/enrich/data/
|
|
32
|
+
!myssia-classifier/myssia_classifier/data/
|
|
33
|
+
# credhunter 指纹库数据(20 发现查询包 + 25 验证规格)同上:程序数据非抓取
|
|
34
|
+
# 产物,fresh clone/CI 缺它即三 lane 启动全坏(10-03-aipocket-fusion R3)。
|
|
35
|
+
!plugins/myssia-credhunter/credhunter/data/
|
|
36
|
+
out/
|
|
37
|
+
output/
|
|
38
|
+
exports/
|
|
39
|
+
backups/
|
|
40
|
+
/logs/
|
|
41
|
+
*.log
|
|
42
|
+
# 场景插件运行时产物(osint 类插件的侦察情报:邮箱/密钥泄漏指纹等)
|
|
43
|
+
# 与抓取数据同级零容忍:插件安装根可在仓库树内(repo 内 compose 副本原地跑、
|
|
44
|
+
# MYIA_PLUGIN_DIR 指向仓库),loot/ 落进工作树就必须被防线覆盖。
|
|
45
|
+
plugins/**/loot/
|
|
46
|
+
|
|
47
|
+
# ---- Python ----
|
|
48
|
+
__pycache__/
|
|
49
|
+
*.py[cod]
|
|
50
|
+
.venv/
|
|
51
|
+
venv/
|
|
52
|
+
dist/
|
|
53
|
+
build/
|
|
54
|
+
*.egg-info/
|
|
55
|
+
.pytest_cache/
|
|
56
|
+
.mypy_cache/
|
|
57
|
+
.ruff_cache/
|
|
58
|
+
.coverage
|
|
59
|
+
htmlcov/
|
|
60
|
+
coverage.xml
|
|
61
|
+
|
|
62
|
+
# ---- 工作流运行态(本机私有,勿提交;.zcode/workflows/ 保存的工作流除外)----
|
|
63
|
+
.zcode/workflow-drafts/
|
|
64
|
+
.zcode/workflow-runs/
|
|
65
|
+
|
|
66
|
+
# ---- Node / 桌面端(desktop/ 是打包正式出处;构建产物可达 ~1GB,严禁入库)----
|
|
67
|
+
node_modules/
|
|
68
|
+
desktop/src-tauri/target/
|
|
69
|
+
desktop/src-tauri/gen/
|
|
70
|
+
desktop/.venv-build/
|
|
71
|
+
desktop/build-pyi/
|
|
72
|
+
desktop/.pyinstaller-cache/
|
|
73
|
+
desktop/ui/
|
|
74
|
+
desktop/src-tauri/binaries/
|
|
75
|
+
|
|
76
|
+
# ---- GitNexus 本地索引(知识图谱 DB,本机私有;WAL 损坏勿提交)----
|
|
77
|
+
.gitnexus/
|
|
78
|
+
# gitnexus analyze 的自动副作用副本:MYIA 用全局技能(~/.agents/skills)+ AGENTS.md,不提交
|
|
79
|
+
# (将来要手写 CLAUDE.md 时,先移除下一行规则)
|
|
80
|
+
.claude/
|
|
81
|
+
CLAUDE.md
|
|
82
|
+
|
|
83
|
+
# ---- 编辑器 / OS ----
|
|
84
|
+
.idea/
|
|
85
|
+
.vscode/
|
|
86
|
+
.DS_Store
|
|
87
|
+
*.bak
|
|
88
|
+
*~
|
|
89
|
+
Thumbs.db
|
|
90
|
+
|
|
91
|
+
# ---- 冒烟运行态(本机私有,勿提交)----
|
|
92
|
+
.zcode/smoke/
|
|
93
|
+
|
|
94
|
+
# ---- 技能工具侧副本(本机私有,勿提交)----
|
|
95
|
+
# .zcode/skills/ 下 trellis-* 系列是仓库内容(已跟踪,trellis 工作流件);
|
|
96
|
+
# 其余(frontend-design/tauri/webapp-testing 等)是技能工具落盘的本地副本
|
|
97
|
+
# (与 ~/.agents/skills 全局件同源),不入库——只挡副本,不误伤 trellis-*。
|
|
98
|
+
.zcode/skills/*
|
|
99
|
+
!.zcode/skills/trellis-*/
|
|
100
|
+
|
|
101
|
+
# ---- 仓库根误落的运行时产物(防误提交)----
|
|
102
|
+
# 消息屏目录/别名是运行态数据,正常应落数据根(MYIA_HOME);desktop serve 在
|
|
103
|
+
# 仓库 cwd 下直跑时缺省写到仓库根——内容含真实平台 chat_id 与群名(私有
|
|
104
|
+
# 标识,同 myssia.db 零容忍口径),且会随每次 discover 变化,严禁入库。
|
|
105
|
+
/channel_aliases.json
|
|
106
|
+
/channel_directory.json
|
|
107
|
+
/channel_dead.json
|
|
108
|
+
# credhunter 全文密钥库(指纹→原文,含真实密钥——比 channel_* 更敏感,
|
|
109
|
+
# 零容忍口径同 myssia.db):正常应落数据根(MYIA_HOME),引擎缺省路径在仓库
|
|
110
|
+
# cwd 直跑时会落仓库根,严禁入库(10-03-aipocket-fusion Q9)。
|
|
111
|
+
/credhunter-keystore.json
|
|
112
|
+
# 技能工具链的本地锁文件(本机技能版本快照,非仓库配置)
|
|
113
|
+
/skills-lock.json
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 xinzhuzi
|
|
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,98 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: myssia-classifier
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Zero-dependency seven-category keyword classifier with free/paid dual-signal adjudication, data-driven keyword tables, and a safe custom-rule API. 七大类双信号关键词分类引擎。
|
|
5
|
+
Project-URL: Homepage, https://github.com/xinzhuzi/myia
|
|
6
|
+
Project-URL: Repository, https://github.com/xinzhuzi/myia
|
|
7
|
+
Project-URL: Issues, https://github.com/xinzhuzi/myia/issues
|
|
8
|
+
Author: xinzhuzi
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: chinese,classifier,information-filtering,keyword-matching,myia,rule-engine,text-classification
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Natural Language :: Chinese (Simplified)
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Text Processing :: Filters
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# myssia-classifier
|
|
27
|
+
|
|
28
|
+
myssia 七大类关键词分类引擎,独立 pip 包:零运行时依赖、免费/付费双信号裁决、数据与代码分离、命中可追溯。
|
|
29
|
+
|
|
30
|
+
从 myssia 核心的分类层(`myssia.classify`)拆出,忠实移植生产爬虫关键词分类器(2026-09-28 纠错版,金测集钉住语义)。可脱离 myssia 单独引用,myssia 核心也以固定版本范围依赖本包。
|
|
31
|
+
|
|
32
|
+
## 特性
|
|
33
|
+
|
|
34
|
+
- **零依赖**:纯 Python 标准库(3.11+),`pip install` 即跑,无任何传递依赖
|
|
35
|
+
- **双信号裁决**:免费信号绝对优先于付费信号;注册引流过滤;死词/噪声词过滤
|
|
36
|
+
- **数据与代码分离**:词表是包内 `data/keywords.json`,调关键词/正则不用改代码
|
|
37
|
+
- **命中追溯**:每条结果都带命中明细(哪张表、哪个关键词/正则、命中区间)
|
|
38
|
+
- **自定义规则 API**:`when` 表达式走白名单 AST,永不 `eval`,未知构造加载期即拒
|
|
39
|
+
- **类型完备**:全量类型标注(`py.typed`),mypy strict 友好
|
|
40
|
+
|
|
41
|
+
## 安装
|
|
42
|
+
|
|
43
|
+
尚未上架 PyPI;在那之前从 myssia 仓库以 uv workspace 安装(`git clone https://github.com/xinzhuzi/myia && cd myssia && uv sync`),PyPI 上架后即可 `pip install myssia-classifier`。
|
|
44
|
+
|
|
45
|
+
## 最小示例
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from myssia_classifier import classify_item
|
|
49
|
+
|
|
50
|
+
result = classify_item({"title": "Claude 免费领100$"})
|
|
51
|
+
print(result.category) # "token"
|
|
52
|
+
print(result.tags) # 自定义规则命中的 tag(无规则时为 [])
|
|
53
|
+
for evidence in result.matched:
|
|
54
|
+
print(evidence.describe()) # 命中追溯: 表:类型=值
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
七大类:`credit-card` / `proxy-node` / `buying-agent` / `server` / `token` / `ai-news` / `freebie`;另有第八个生产标签 `channel`(付费信号且无免费信号的导购/渠道帖)。被过滤标题的 `category` 为 `None`。
|
|
58
|
+
|
|
59
|
+
## 自定义规则
|
|
60
|
+
|
|
61
|
+
规则读取条目字段(字典键或对象属性),在粗分类之上打 tag:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from myssia_classifier import Rule, classify_item
|
|
65
|
+
|
|
66
|
+
rule = Rule(name="big-move", when="abs(change_pct) >= 3", tag="大波动")
|
|
67
|
+
result = classify_item({"title": "NVDA 盘前大跌", "change_pct": -4.2}, rules=[rule])
|
|
68
|
+
print(result.tags) # ["大波动"]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`when` 表达式支持的语法(白名单 AST):字面量、字段名、list/tuple、四则与比较运算、`and`/`or`/`not`、白名单函数(`abs/min/max/round/len/int/float/str`)。属性访问、下标、lambda、f-string、推导式、任意调用等一律在构造期抛 `RuleSyntaxError`。运行期单条规则失败只按"未命中"处理并记 WARNING,绝不打断整批评测。
|
|
72
|
+
|
|
73
|
+
## 自定义词表
|
|
74
|
+
|
|
75
|
+
默认词表随包分发(`myssia_classifier/data/keywords.json`);也可以加载自己的 JSON:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from myssia_classifier import load_table, classify_title
|
|
79
|
+
|
|
80
|
+
table = load_table() # 缺省即打包词表;自定义时传自己的 JSON 路径(结构非法时结构化报错)
|
|
81
|
+
print(classify_title("便宜出极速服务器三台", table).category) # "server"(打包词表实测命中)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
词表 JSON 支持在 `keywords`(普通关键词)与 `patterns`(正则)两列增改,`case_insensitive` 按节可调;顶层未知字段与断裂引用(如 `fallback_target` 指向不存在的类目)加载即报错,不做静默忽略。
|
|
85
|
+
|
|
86
|
+
## 从 YAML 加载规则
|
|
87
|
+
|
|
88
|
+
`load_rules` 可直接读取 myssia 插件 YAML 的 `classify.rules` 节(需要环境里装有 PyYAML;本包不把它列为依赖):
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from myssia_classifier import load_rules
|
|
92
|
+
|
|
93
|
+
rules = load_rules("plugin.yaml")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## 许可
|
|
97
|
+
|
|
98
|
+
MIT
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# myssia-classifier
|
|
2
|
+
|
|
3
|
+
myssia 七大类关键词分类引擎,独立 pip 包:零运行时依赖、免费/付费双信号裁决、数据与代码分离、命中可追溯。
|
|
4
|
+
|
|
5
|
+
从 myssia 核心的分类层(`myssia.classify`)拆出,忠实移植生产爬虫关键词分类器(2026-09-28 纠错版,金测集钉住语义)。可脱离 myssia 单独引用,myssia 核心也以固定版本范围依赖本包。
|
|
6
|
+
|
|
7
|
+
## 特性
|
|
8
|
+
|
|
9
|
+
- **零依赖**:纯 Python 标准库(3.11+),`pip install` 即跑,无任何传递依赖
|
|
10
|
+
- **双信号裁决**:免费信号绝对优先于付费信号;注册引流过滤;死词/噪声词过滤
|
|
11
|
+
- **数据与代码分离**:词表是包内 `data/keywords.json`,调关键词/正则不用改代码
|
|
12
|
+
- **命中追溯**:每条结果都带命中明细(哪张表、哪个关键词/正则、命中区间)
|
|
13
|
+
- **自定义规则 API**:`when` 表达式走白名单 AST,永不 `eval`,未知构造加载期即拒
|
|
14
|
+
- **类型完备**:全量类型标注(`py.typed`),mypy strict 友好
|
|
15
|
+
|
|
16
|
+
## 安装
|
|
17
|
+
|
|
18
|
+
尚未上架 PyPI;在那之前从 myssia 仓库以 uv workspace 安装(`git clone https://github.com/xinzhuzi/myia && cd myssia && uv sync`),PyPI 上架后即可 `pip install myssia-classifier`。
|
|
19
|
+
|
|
20
|
+
## 最小示例
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from myssia_classifier import classify_item
|
|
24
|
+
|
|
25
|
+
result = classify_item({"title": "Claude 免费领100$"})
|
|
26
|
+
print(result.category) # "token"
|
|
27
|
+
print(result.tags) # 自定义规则命中的 tag(无规则时为 [])
|
|
28
|
+
for evidence in result.matched:
|
|
29
|
+
print(evidence.describe()) # 命中追溯: 表:类型=值
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
七大类:`credit-card` / `proxy-node` / `buying-agent` / `server` / `token` / `ai-news` / `freebie`;另有第八个生产标签 `channel`(付费信号且无免费信号的导购/渠道帖)。被过滤标题的 `category` 为 `None`。
|
|
33
|
+
|
|
34
|
+
## 自定义规则
|
|
35
|
+
|
|
36
|
+
规则读取条目字段(字典键或对象属性),在粗分类之上打 tag:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from myssia_classifier import Rule, classify_item
|
|
40
|
+
|
|
41
|
+
rule = Rule(name="big-move", when="abs(change_pct) >= 3", tag="大波动")
|
|
42
|
+
result = classify_item({"title": "NVDA 盘前大跌", "change_pct": -4.2}, rules=[rule])
|
|
43
|
+
print(result.tags) # ["大波动"]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`when` 表达式支持的语法(白名单 AST):字面量、字段名、list/tuple、四则与比较运算、`and`/`or`/`not`、白名单函数(`abs/min/max/round/len/int/float/str`)。属性访问、下标、lambda、f-string、推导式、任意调用等一律在构造期抛 `RuleSyntaxError`。运行期单条规则失败只按"未命中"处理并记 WARNING,绝不打断整批评测。
|
|
47
|
+
|
|
48
|
+
## 自定义词表
|
|
49
|
+
|
|
50
|
+
默认词表随包分发(`myssia_classifier/data/keywords.json`);也可以加载自己的 JSON:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from myssia_classifier import load_table, classify_title
|
|
54
|
+
|
|
55
|
+
table = load_table() # 缺省即打包词表;自定义时传自己的 JSON 路径(结构非法时结构化报错)
|
|
56
|
+
print(classify_title("便宜出极速服务器三台", table).category) # "server"(打包词表实测命中)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
词表 JSON 支持在 `keywords`(普通关键词)与 `patterns`(正则)两列增改,`case_insensitive` 按节可调;顶层未知字段与断裂引用(如 `fallback_target` 指向不存在的类目)加载即报错,不做静默忽略。
|
|
60
|
+
|
|
61
|
+
## 从 YAML 加载规则
|
|
62
|
+
|
|
63
|
+
`load_rules` 可直接读取 myssia 插件 YAML 的 `classify.rules` 节(需要环境里装有 PyYAML;本包不把它列为依赖):
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from myssia_classifier import load_rules
|
|
67
|
+
|
|
68
|
+
rules = load_rules("plugin.yaml")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 许可
|
|
72
|
+
|
|
73
|
+
MIT
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""myssia-classifier: standalone seven-category keyword classifier.
|
|
2
|
+
|
|
3
|
+
Zero-dependency classification engine extracted from MYIA's classification
|
|
4
|
+
layer. Public entry point is :func:`classify_item`: it runs the built-in
|
|
5
|
+
dual-signal adjudication (seven categories + channel) over the item title
|
|
6
|
+
and applies optional custom rules (safe ``when`` expressions) on top,
|
|
7
|
+
returning ``category`` + ``tags`` + the hit trace for debugging.
|
|
8
|
+
|
|
9
|
+
The keyword tables live in the packaged ``data/keywords.json`` so the owner
|
|
10
|
+
or an AI can tune them without code changes.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import Any, Iterable, Mapping
|
|
18
|
+
|
|
19
|
+
from myssia_classifier.builtin import (
|
|
20
|
+
ALL_CATEGORIES,
|
|
21
|
+
CATEGORY_CHANNEL,
|
|
22
|
+
DEFAULT_TABLE_PATH,
|
|
23
|
+
SEVEN_CATEGORIES,
|
|
24
|
+
BuiltinResult,
|
|
25
|
+
ClassifyDataError,
|
|
26
|
+
ClassifyTable,
|
|
27
|
+
MatchEvidence,
|
|
28
|
+
category_label,
|
|
29
|
+
classify_title,
|
|
30
|
+
load_table,
|
|
31
|
+
)
|
|
32
|
+
from myssia_classifier.custom import (
|
|
33
|
+
ALLOWED_FUNCTIONS,
|
|
34
|
+
Rule,
|
|
35
|
+
RuleConfigError,
|
|
36
|
+
RuleEvalError,
|
|
37
|
+
RuleSyntaxError,
|
|
38
|
+
evaluate_expression,
|
|
39
|
+
load_rules,
|
|
40
|
+
rules_from_config,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"ALL_CATEGORIES",
|
|
45
|
+
"ALLOWED_FUNCTIONS",
|
|
46
|
+
"CATEGORY_CHANNEL",
|
|
47
|
+
"DEFAULT_TABLE_PATH",
|
|
48
|
+
"SEVEN_CATEGORIES",
|
|
49
|
+
"BuiltinResult",
|
|
50
|
+
"ClassifyDataError",
|
|
51
|
+
"ClassifyResult",
|
|
52
|
+
"ClassifyTable",
|
|
53
|
+
"MatchEvidence",
|
|
54
|
+
"Rule",
|
|
55
|
+
"RuleConfigError",
|
|
56
|
+
"RuleEvalError",
|
|
57
|
+
"RuleSyntaxError",
|
|
58
|
+
"category_label",
|
|
59
|
+
"classify_item",
|
|
60
|
+
"classify_title",
|
|
61
|
+
"evaluate_expression",
|
|
62
|
+
"load_rules",
|
|
63
|
+
"load_table",
|
|
64
|
+
"rules_from_config",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
__version__ = "0.0.1"
|
|
68
|
+
|
|
69
|
+
logger = logging.getLogger(__name__)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class ClassifyResult:
|
|
74
|
+
"""Full classification outcome: built-in category + custom rule tags + trace."""
|
|
75
|
+
|
|
76
|
+
category: str | None # category id, or None when the title is discarded
|
|
77
|
+
tags: list[str] # tags from fired custom rules (falls back to rule name)
|
|
78
|
+
matched: tuple[MatchEvidence, ...] # which keyword/pattern hit where
|
|
79
|
+
normalized_title: str
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _item_title(item: Mapping[str, Any] | object) -> Any:
|
|
83
|
+
"""Read the title; raise TypeError when the field is absent or non-str (fail fast)."""
|
|
84
|
+
if isinstance(item, Mapping):
|
|
85
|
+
if "title" not in item:
|
|
86
|
+
raise TypeError("分类条目缺少 title 字段")
|
|
87
|
+
return item["title"]
|
|
88
|
+
if not hasattr(item, "title"):
|
|
89
|
+
raise TypeError("分类条目缺少 title 属性")
|
|
90
|
+
return item.title
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def classify_item(
|
|
94
|
+
item: Mapping[str, Any] | object,
|
|
95
|
+
*,
|
|
96
|
+
table: ClassifyTable | None = None,
|
|
97
|
+
rules: Iterable[Rule] | None = None,
|
|
98
|
+
) -> ClassifyResult:
|
|
99
|
+
"""Classify one item: built-in keyword funnel, then custom rules.
|
|
100
|
+
|
|
101
|
+
The built-in stage reads only ``item.title`` (production semantics:
|
|
102
|
+
classification never looks at the URL). Custom rules may read any item
|
|
103
|
+
field, e.g. ``abs(change_pct) >= 3``.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
item: dict-like item or object with a ``title`` attribute.
|
|
107
|
+
table: preloaded keyword table; defaults to the packaged
|
|
108
|
+
``data/keywords.json``.
|
|
109
|
+
rules: optional custom rules (see :mod:`myssia_classifier.custom`).
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
ClassifyResult with ``category`` (or ``None`` for dead/noise/
|
|
113
|
+
registration-lure/no-signal titles), fired ``tags`` and the hit
|
|
114
|
+
trace. A failing rule never breaks the batch (WARNING + skipped).
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
TypeError: ``item.title`` is missing or not a string.
|
|
118
|
+
ClassifyDataError: keyword table malformed (fail fast at startup).
|
|
119
|
+
"""
|
|
120
|
+
title = _item_title(item)
|
|
121
|
+
if not isinstance(title, str):
|
|
122
|
+
raise TypeError(f"分类条目缺少字符串 title 字段, got {type(title).__name__}")
|
|
123
|
+
builtin_result = classify_title(title, table)
|
|
124
|
+
tags: list[str] = []
|
|
125
|
+
for rule in rules or ():
|
|
126
|
+
if rule.evaluate(item):
|
|
127
|
+
tags.append(rule.tag or rule.name)
|
|
128
|
+
logger.debug(
|
|
129
|
+
"分类条目完成: category=%s tags=%s title=%r",
|
|
130
|
+
builtin_result.category,
|
|
131
|
+
tags,
|
|
132
|
+
builtin_result.normalized_title,
|
|
133
|
+
)
|
|
134
|
+
return ClassifyResult(
|
|
135
|
+
category=builtin_result.category,
|
|
136
|
+
tags=tags,
|
|
137
|
+
matched=builtin_result.matched,
|
|
138
|
+
normalized_title=builtin_result.normalized_title,
|
|
139
|
+
)
|