TextGrocery-Shop 0.2.0__py3-none-any.whl

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.
@@ -0,0 +1,733 @@
1
+ Metadata-Version: 2.4
2
+ Name: TextGrocery_Shop
3
+ Version: 0.2.0
4
+ Summary: TextGrocery_Shop: a simple, cross-platform short-text classification tool (Python 3.10+)
5
+ Author-email: ranvane <ranvane@gmail.com>
6
+ Maintainer-email: ranvane <ranvane@gmail.com>
7
+ License-Expression: BSD-3-Clause
8
+ Project-URL: Homepage, https://github.com/ranvane/TextGrocery_Shop
9
+ Project-URL: Repository, https://github.com/ranvane/TextGrocery_Shop
10
+ Project-URL: PyPI, https://pypi.org/project/textgrocery-shop/
11
+ Keywords: text classification,short text,tf-idf,logistic regression,jieba
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Natural Language :: Chinese (Simplified)
15
+ Classifier: Programming Language :: Python :: 3
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 :: Scientific/Engineering :: Artificial Intelligence
21
+ Classifier: Topic :: Text Processing :: Linguistic
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE.txt
25
+ Requires-Dist: jieba>=0.42.1
26
+ Requires-Dist: numpy>=1.23
27
+ Provides-Extra: test
28
+ Requires-Dist: pytest>=7.0; extra == "test"
29
+ Dynamic: license-file
30
+
31
+ # TextGrocery_Shop
32
+
33
+ 一个简单、跨平台的中文短文本分类工具。支持 **Python 3.10+**,安装时**不需要任何 C 编译器**,
34
+ 并完整保留旧版 `Grocery` 的调用方式。
35
+
36
+ 内置 [结巴分词](https://github.com/fxsjy/jieba),在词特征之外增加字符 n-gram、TF-IDF 加权与
37
+ L2 正则逻辑回归,对新闻标题、商品标题、工单摘要这类超短文本更友好。
38
+
39
+ 其他语言:[English](README_EN.md)
40
+
41
+ ## 项目来历
42
+
43
+ **原版 TextGrocery**(作者 [2shou](https://github.com/2shou/TextGrocery))诞生于 Python 2 时代,
44
+ 是中文短文本分类里常用的轻量基线。它的技术路线是:
45
+
46
+ - 用 jieba 做中文分词;
47
+ - 用词 unigram / bigram 计数构造特征,再做二值化和 L2 归一化;
48
+ - 通过 `ctypes` 调用编译好的 **LibShortText / LibLinear** C++ 动态库完成训练与预测;
49
+ - 模型用 `pickle` 保存。
50
+
51
+ 这套方案在当时非常简单直接,但也积累了几个问题:
52
+
53
+ 1. **Python 2 已停止维护**,原代码无法在 Python 3.10+ 上运行;
54
+ 2. **安装依赖 C 编译器**,Windows 上需要 MSVC、Linux/macOS 上需要 `make + gcc`,
55
+ 而且编译出的 `.so` 不能跨平台复用;
56
+ 3. **模型不可移植**:`pickle` 与 Python 版本、NumPy 版本绑定,换环境经常直接失败;
57
+ 4. **超短文本效果有限**:十几字的标题分词后只剩几个词,未登录词直接丢失,特征向量接近全零;
58
+ 5. **只有决策值,没有概率**,也不方便扩展特征。
59
+
60
+ **0.2.x 的目标**是保留“简单好用”的 API,同时把上述问题一次性解决:用纯 Python + NumPy
61
+ 重写训练器,去掉编译依赖;加入字符 n-gram 与 TF-IDF;模型改用 JSON + NumPy 数组;
62
+ 并补上概率输出、分词模式、用户词典等实用选项。旧版的设计思路仍然值得借鉴,
63
+ 感谢原作者与 LibShortText / LibLinear 的工作。
64
+
65
+ ## 新版特性
66
+
67
+ - **零编译安装**:纯 Python + NumPy,`pip install` 在 Windows / Linux / macOS 行为一致。
68
+ - **API 不变**:`Grocery(name)` → `train()` → `save()` → `load()` → `predict()` / `test()`。
69
+ - **更适合短文本**:字符 2–3gram + sublinear TF + 平滑 IDF + L2 归一化。
70
+ - **输出概率**:`result.probabilities`,同时保留原始决策值 `result.dec_values`。
71
+ - **模型可搬运**:`metadata.json` + `vocab.json` + `model.npz`,不使用 `pickle`。
72
+ - **分词可配置**:全模式 / 精确模式 / 搜索引擎模式,支持用户词典、停用词、NFKC 归一化。
73
+ - **速度**:10 类新闻标题模型上约 **80–85 µs/条**,训练全程向量化。
74
+
75
+ ## 安装
76
+
77
+ ```bash
78
+ pip install textgrocery-shop
79
+ ```
80
+
81
+ PyPI 发行名为 `TextGrocery_Shop`(会被规范化为 `textgrocery-shop`);导入名仍是
82
+ `tgrocery`,老代码不需要改。
83
+
84
+ 从源码安装:
85
+
86
+ ```bash
87
+ pip install .
88
+ ```
89
+
90
+ 依赖:`jieba`、`numpy`;Python ≥ 3.10。
91
+
92
+ ## 快速开始
93
+
94
+ ```python
95
+ from tgrocery import Grocery
96
+
97
+ # 创建一个杂货铺,名字同时作为模型目录名
98
+ grocery = Grocery("sample")
99
+
100
+ # 用列表训练:[(标签, 文本), ...]
101
+ train_src = [
102
+ ("education", "名师指导托福语法技巧:名词的复数形式"),
103
+ ("education", "中国高考成绩海外认可 是狼来了吗"),
104
+ ("sports", "图文:法网孟菲尔斯苦战进16强 孟菲尔斯怒吼"),
105
+ ("sports", "四川丹棱举行全国长距登山挑战赛 近万人参与"),
106
+ ]
107
+ grocery.train(train_src)
108
+
109
+ # 也可以从文件训练:标签<TAB>文本(UTF-8)
110
+ # grocery.train("train_ch.txt")
111
+
112
+ grocery.save()
113
+
114
+ # 加载模型(名字与保存时一致)
115
+ new_grocery = Grocery("sample")
116
+ new_grocery.load()
117
+
118
+ # 预测
119
+ result = new_grocery.predict("考生必读:新托福写作考试评分标准")
120
+ print(result) # education(与旧版一样直接打印标签)
121
+ print(result.dec_values) # {'education': 2.13, 'sports': -2.05, ...}
122
+ print(result.probabilities) # {'education': 0.91, 'sports': 0.06, ...}
123
+
124
+ # 批量测试
125
+ test_src = [
126
+ ("education", "福建春季公务员考试报名18日截止 2月6日考试"),
127
+ ("sports", "意甲首轮补赛交战记录:米兰客场8战不败国米10年连胜"),
128
+ ]
129
+ test_result = new_grocery.test(test_src)
130
+ print(test_result.accuracy_overall)
131
+ test_result.show_result() # 逐标签精确率 / 召回率表格
132
+ ```
133
+
134
+ 更多示例见 [sample/](sample/)。
135
+
136
+ ## 为什么对短文本更有效
137
+
138
+ 原版的特征是“词计数 → 二值化 → L2”。标题再短一些时,分词后往往只剩 3–5 个词,
139
+ 大量词不在词表里,最终输入向量接近全零。新版从三个层面缓解:
140
+
141
+ ### 1. 字符 n-gram 兜底
142
+
143
+ ```text
144
+ "中国高考" → c:中国, c:国高, c:高考, c:中国高, c:国高考
145
+ ```
146
+
147
+ 即使“高考生”“考研”是未登录词,只要共享“高考”两个字符,就能命中 `c:高考`。
148
+ 词特征和字符特征分别加 `w:` / `c:` 前缀,互不覆盖。
149
+
150
+ ### 2. TF-IDF 加权
151
+
152
+ - **sublinear TF**:`1 + log(tf)`,避免一个词重复出现就主导整条文本;
153
+ - **平滑 IDF**:`log((1 + N) / (1 + df)) + 1`,降低“的、是”等高频词权重,且不会出现 0/负值;
154
+ - **L2 行归一化**:让长短文本处于同一尺度。
155
+
156
+ ### 3. 分类器与噪声控制
157
+
158
+ - OvR 逻辑回归 + L2 正则,用 **L-BFGS**(Armijo 回溯线搜索)优化;
159
+ - `min_df` 自动策略:语料 ≥ 1000 篇时过滤只出现一次的特征,小语料则全部保留;
160
+ - `class_weight="balanced"` 处理类别不平衡;
161
+ - 稀疏矩阵乘法用 `numpy.bincount`,计算仍发生在 C 层。
162
+
163
+ 目标函数:
164
+
165
+ $$
166
+ \min_{w_k,b_k}\;\frac{1}{2}\lVert w_k\rVert^2
167
+ + C\sum_{i=1}^{n}s_i\log\!\left(1+\exp\!\left(-y_{ik}(w_k^\top x_i+b_k)\right)\right)
168
+ $$
169
+
170
+ 预测时取 `argmax_k (w_k^T x + b_k)`,再经 softmax 得到概率。
171
+
172
+ ## 分词与文本预处理
173
+
174
+ 默认分词器仍是 jieba 全模式(`cut_all=True`),这是原版行为,也是当前数据上效果最好的模式。
175
+ 新增选项都通过**仅关键字参数**提供,不传时行为不变。
176
+
177
+ ```python
178
+ grocery = Grocery(
179
+ "sample",
180
+ tokenizer="full", # full | precise | search
181
+ normalize="none", # none | nfkc
182
+ char_ngram_mode="global", # global | word_boundary
183
+ mask_patterns=False, # URL / 邮箱 / 数字占位符
184
+ stopwords=["的", "了"], # 也可传文件路径
185
+ user_dict="domain_words.txt", # 每行一个领域词
186
+ )
187
+ ```
188
+
189
+ ### 分词模式
190
+
191
+ | 模式 | 说明 | 实测准确率 |
192
+ |---|---|---:|
193
+ | `full`(默认) | 全模式,输出所有可能成词的片段,召回高 | **90.22%** |
194
+ | `precise` | 精确模式,只输出一条最可能的切分 | 89.28% |
195
+ | `search` | 搜索引擎模式,精确模式 + 长词子词 | 89.40% |
196
+
197
+ 同一份 THUCNews 30k/5k 数据、相同特征与超参数下,全模式仍最好。精确模式特征更少
198
+ (149k vs 164k)、训练略快,适合对内存/延迟更敏感的场景;领域不同可以自行 A/B。
199
+
200
+ ### 用户词典
201
+
202
+ 领域实体、产品名、缩写在通用词典里经常被切碎。把词表文件传给 `user_dict`,
203
+ 初始化时会注册进 jieba,并**随模型一起写入 metadata**,换机器加载也能复现:
204
+
205
+ ```python
206
+ grocery = Grocery("sample", user_dict="domain_words.txt")
207
+ # 或直接传列表
208
+ grocery = Grocery("sample", user_dict=["烯晶算力网络", "星云一体机"])
209
+ ```
210
+
211
+ 当前 `user_dict` 只接受 **UTF-8 文本、每行一个词**;词内可以包含空格,但不能带频次、
212
+ 拼音或词性列。搜狗导出的 `.scel` 是二进制细胞词库,**不能直接传给 `user_dict`**,
213
+ 需要先转换成纯文本。下面是一套完整的转换与清洗流程。
214
+
215
+ #### 1. 把 `.scel` 转成文本
216
+
217
+ 使用[深蓝词库转换](https://github.com/studyzy/imewlconverter)(ImeWlConverter)、`scel2txt` 等工具,把 `.scel` 导出为 `.txt`。
218
+ 不同工具的输出列不完全一样,常见形式有:
219
+
220
+ ```text
221
+ 烯晶算力网络
222
+ 烯晶算力网络 10
223
+ 烯晶算力网络 xijing suanli wangluo 10
224
+ 烯晶算力网络 10 n
225
+ 烯晶算力网络,10,名
226
+ ```
227
+
228
+ 前四种分别对应“纯词”“词 + 频次”“词 + 拼音 + 频次”“jieba 风格词 + 频次 + 词性”。
229
+ 我们的加载器只认第一种,其他格式需要清洗。**建议在转换工具里把输出列配置为
230
+ TAB 分隔**(`词<TAB>频次` 或 `词<TAB>拼音<TAB>频次`);如果工具只能输出空格分隔的
231
+ `词 拼音 频次`,请先改成 TAB/CSV 再清洗,否则拼音会被当成词的一部分。
232
+
233
+ #### 2. 清洗成“每行一个词”
234
+
235
+ 把下面的脚本保存为 `sogou_to_userdict.py`,按实际情况修改输入输出路径后运行:
236
+
237
+ ```python
238
+ from pathlib import Path
239
+ import re
240
+
241
+
242
+ def sogou_to_userdict(src: str, dst: str) -> None:
243
+ """把搜狗转换后的词表清洗成 TextGrocery_Shop 的 user_dict 格式。
244
+
245
+ 规则:
246
+ 1. 优先按 TAB / 逗号取第一列(词),丢掉频次、拼音、词性;
247
+ 2. 没有分隔符时,去掉行尾的“频次 [词性]”,兼容 "词 10 n";
248
+ 3. 跳过空行与 # 注释,按原顺序去重;
249
+ 4. 统一输出 UTF-8(无 BOM),每行一个词。
250
+ """
251
+ words: list[str] = []
252
+ seen: set[str] = set()
253
+
254
+ for raw in Path(src).read_text(encoding="utf-8-sig").splitlines():
255
+ line = raw.strip()
256
+ if not line or line.startswith("#"):
257
+ continue
258
+
259
+ # TSV / CSV:第一列是词,其余列是频次、拼音或词性
260
+ first = re.split(r"[\t,]", line, maxsplit=1)[0].strip()
261
+
262
+ # 空格分隔:只处理 "词 频次 [词性]";
263
+ # 若是 "词 拼音 频次",请在转换工具中改用 TAB/CSV 输出。
264
+ # 注意:没有 TAB/逗号时,"iPhone 15" 这类词尾数字会被误删;
265
+ # 如果词表含此类产品名,请优先使用 TAB 分隔的导出格式。
266
+ word = re.sub(r"\s+\d+(?:\s+[a-z]+)?$", "", first).strip()
267
+
268
+ if not word or word in seen:
269
+ continue
270
+ seen.add(word)
271
+ words.append(word)
272
+
273
+ Path(dst).write_text("\n".join(words) + "\n", encoding="utf-8")
274
+ print(f"写入 {len(words)} 个词 -> {dst}")
275
+
276
+
277
+ if __name__ == "__main__":
278
+ sogou_to_userdict("sogou_export.txt", "domain_words.txt")
279
+ ```
280
+
281
+ 运行后检查一下结果:
282
+
283
+ ```bash
284
+ python -c "from pathlib import Path; w=Path('domain_words.txt').read_text(encoding='utf-8').splitlines(); print(len(w)); print(w[:20])"
285
+ ```
286
+
287
+ #### 3. 交给 `user_dict`
288
+
289
+ ```python
290
+ from tgrocery import Grocery
291
+
292
+ grocery = Grocery(
293
+ "sample",
294
+ tokenizer="precise", # 领域词较多时,精确模式通常更稳
295
+ user_dict="domain_words.txt",
296
+ )
297
+ print(len(grocery.feature_config.user_words)) # 已加载的词数
298
+
299
+ grocery.train(train_src)
300
+
301
+ # 验证领域词确实进入了词表(词特征带 w: 前缀)
302
+ print("w:烯晶算力网络" in grocery.model.text_converter.feature2idx)
303
+ ```
304
+
305
+ 保存后,词表会写入 `metadata.json` 的 `features.user_words`:
306
+
307
+ ```json
308
+ {
309
+ "features": {
310
+ "tokenizer": "precise",
311
+ "user_words": ["烯晶算力网络", "星云一体机"]
312
+ }
313
+ }
314
+ ```
315
+
316
+ 之后 `Grocery("sample").load()` 会从 metadata 重新注册这些词,**换机器加载模型时
317
+ 不需要再带原始词典文件**。
318
+
319
+ #### 4. 注意事项
320
+
321
+ - **不要直接导入全量搜狗词库**。通用词对分类帮助很小,却会让 `metadata.json`
322
+ 膨胀、`load()` 变慢。建议只保留领域实体、产品名、型号、缩写、人名地名,
323
+ 规模控制在几千到一两万词。
324
+ - 搜狗词库通常带频次。如果转换工具保留了频次,可以先按频次阈值过滤,再做上面的清洗。
325
+ - 开启 `lowercase=True`(默认)时,英文词会被转成小写后写入 metadata;
326
+ 如果产品名大小写有意义,请设置 `lowercase=False`。
327
+ - 同一进程内 jieba 的词典是全局状态:多个 `Grocery` 使用不同 `user_dict` 时会互相影响。
328
+ 需要严格隔离时,把不同模型放到不同进程中使用。
329
+ - 不同 jieba 版本的基础词典可能略有差异;对分词结果要求完全可复现时,请固定 jieba 版本。
330
+ - 搜狗词库有各自的许可条款;把词表写入模型 metadata 意味着它会随模型一起分发,
331
+ 请确认授权范围。
332
+ - `user_dict` 只影响 jieba 分词;使用 `custom_tokenize` 时不会生效。
333
+
334
+ ### 停用词
335
+
336
+ `stopwords` 支持文件路径或可迭代对象,只作用于词特征与 `word_boundary` 字符特征。
337
+ `global` 字符特征不会删除停用词,否则会破坏相邻字符 n-gram。
338
+
339
+ ### 归一化与占位符
340
+
341
+ - `normalize="nfkc"`:全角字母/数字/标点转半角,统一 `ABC` 与 `ABC`;
342
+ - `mask_patterns=True`:把 URL、邮箱、数字替换为 `__url__` / `__email__` / `__num__`,
343
+ 适合链接、价格、日期很多 UGC 文本,避免特征爆炸。
344
+
345
+ ### 字符 n-gram 模式
346
+
347
+ - `global`(默认):去掉空白后整串滑窗,中文标题效果最好;
348
+ - `word_boundary`:只在词内部滑窗并补边界空格(类似 sklearn 的 `char_wb`),
349
+ 避免英文/中英混排生成跨词噪声。分词更重时开销会上升,建议搭配 `tokenizer="precise"`。
350
+
351
+ ### 效率
352
+
353
+ 默认路径(全模式 + 全局字符 n-gram + 不做 NFKC/占位符替换)与之前完全一致:
354
+ 30k 训练约 **26.7 s**,5k 预测约 **0.42 s**。新增的归一化、停用词、用户词典
355
+ 只在开启时产生额外开销;`word_boundary` 是唯一可能明显变慢的选项。
356
+
357
+ ## 实测效果
358
+
359
+ 数据:THUCNews 10 类新闻标题(训练 30,000 / 测试 5,000,标题中位数 19 字)。
360
+ 三组配置使用相同的分类器与超参数(`C=8`、`max_iter=50`、`min_df=2`),只改变特征:
361
+
362
+ | 配置 | 完整标题 | 截断到 10 字 | 训练耗时 |
363
+ |---|---:|---:|---:|
364
+ | `legacy`(词 1–2gram、二值、无 IDF) | 88.82% | 82.86% | 11.0 s |
365
+ | `word_tfidf`(词 1–2gram + TF-IDF) | 89.20% | 83.74% | 10.8 s |
366
+ | **`word_char`(词 + 字符 2–3gram + TF-IDF)** | **90.22%** | **84.66%** | 26.7 s |
367
+
368
+ 预测:5,000 条约 0.42 秒(约 84 微秒/条)。
369
+
370
+ > 该数据与旧版 README 中的 48k/16k、32 类基准不是同一份数据,绝对数值不可直接比较。
371
+ > 请运行 `benchmark.py` 在你自己的数据上验证。
372
+
373
+ ## 基准测试
374
+
375
+ ```bash
376
+ # 对比 legacy / word_tfidf / word_char
377
+ python benchmark.py --train cnews.train.txt --test cnews.test.txt
378
+
379
+ # 模拟超短文本(每条截断到前 10 个字符)
380
+ python benchmark.py --train cnews.train.txt --test cnews.test.txt --truncate 10
381
+
382
+ # 限制样本量,快速试验
383
+ python benchmark.py --train cnews.train.txt --test cnews.test.txt \
384
+ --train-size 30000 --test-size 5000
385
+ ```
386
+
387
+ 数据格式默认是 `文本<TAB>标签`;若是 `标签<TAB>文本`,加 `--label-column first`。
388
+
389
+ ## 参数说明
390
+
391
+ 所有新增参数都是**仅关键字参数**,旧代码无需修改。
392
+
393
+ ```python
394
+ Grocery(
395
+ "name",
396
+ custom_tokenize=None, # 自定义分词函数,优先级高于 tokenizer
397
+ C=8.0, # L2 正则强度的倒数
398
+ class_weight=None, # None | "balanced" | {"标签": 权重}
399
+ word_ngrams=(1, 2), # 词级 n-gram
400
+ char_ngrams=(2, 3), # 字符级 n-gram;() 表示关闭
401
+ lowercase=True,
402
+ min_df=None, # None = 自动(<1000 篇取 1,否则取 2)
403
+ max_features=None,
404
+ binary=False, # True 时退化为旧版二值特征
405
+ sublinear_tf=True,
406
+ use_idf=True,
407
+ smooth_idf=True,
408
+ norm="l2",
409
+ tokenizer="full", # full | precise | search
410
+ normalize="none", # none | nfkc
411
+ char_ngram_mode="global", # global | word_boundary
412
+ mask_patterns=False,
413
+ stopwords=None,
414
+ user_dict=None,
415
+ max_iter=50,
416
+ tol=1e-4,
417
+ verbose=False,
418
+ )
419
+ ```
420
+
421
+ 调参建议:
422
+
423
+ - 数据量大、内存吃紧:`max_features=300000` 或提高 `min_df`;
424
+ - 类别不平衡:`class_weight="balanced"`;
425
+ - 追求速度:`char_ngrams=()`,只用词特征 + TF-IDF;
426
+ - 英文/中英混排较多:`tokenizer="precise", char_ngram_mode="word_boundary"`;
427
+ - 全角字符或 UGC 文本:`normalize="nfkc"`,必要时加 `mask_patterns=True`;
428
+ - 完全复现旧版特征:`word_ngrams=(1, 2), char_ngrams=(), binary=True,
429
+ sublinear_tf=False, use_idf=False, normalize="none"`。
430
+
431
+ ## 模型目录格式
432
+
433
+ `save()` 生成一个自包含、可移植的目录:
434
+
435
+ ```text
436
+ name/
437
+ id # 模型哈希
438
+ metadata.json # 标签、特征配置(含分词/停用词/用户词典)、分类器参数
439
+ vocab.json # 特征词表(UTF-8)
440
+ vectorizer.npz # IDF 向量
441
+ model.npz # 权重矩阵 + 偏置
442
+ ```
443
+
444
+ 不使用 `pickle`,模型可在不同 Python 版本、不同操作系统之间搬运;
445
+ 出问题时也可以直接打开 `metadata.json` / `vocab.json` 检查。
446
+
447
+ ## 兼容性与迁移
448
+
449
+ - **调用方式不变**:`Grocery(name)`、`train()`、`save()`、`load()`、`predict()`、
450
+ `test()`,以及 `GroceryPredictResult` / `GroceryTestResult` 的字段与旧版一致。
451
+ - `predict()` 返回对象可以 `print` 出标签;新增 `probabilities`。
452
+ - 旧的 C 后端(LibShortText / LibLinear)已移除,当前只保留纯 NumPy 实现。
453
+ - **0.1.x 旧模型不可加载**:旧模型依赖 pickle 与 C 后端,请用新版重新训练一次。
454
+ - 0.2.0 早期模型仍可加载:缺失的新配置会自动回退到旧行为。
455
+
456
+ ## 打包与发布
457
+
458
+ 本节面向维护者,说明如何构建 wheel/sdist、上传 TestPyPI 和 PyPI。项目是纯 Python 包,
459
+ wheel 为 `py3-none-any`,构建过程不需要 C 编译器。
460
+
461
+ ### 1. 发行名
462
+
463
+ 发行名是 `TextGrocery_Shop`。PyPI 与 pip 会把它规范化为 `textgrocery-shop`,
464
+ 所以安装命令是:
465
+
466
+ ```bash
467
+ pip install textgrocery-shop
468
+ ```
469
+
470
+ Python 导入名仍然是 `tgrocery`,只有安装命令与原版不同。撰写本文时该名称在
471
+ PyPI 上可用,首次上传前请再确认 <https://pypi.org/project/textgrocery-shop/>。
472
+ 如果已被占用,修改 `pyproject.toml` 的 `[project].name`,并同步更新两份 README
473
+ 里所有 `pip install` 命令。
474
+
475
+ ### 2. 同步版本号
476
+
477
+ 版本号出现在两个地方,发布前必须保持一致:
478
+
479
+ - `pyproject.toml` 的 `[project].version`
480
+ - `tgrocery/__init__.py` 的 `__version__`
481
+
482
+ ```bash
483
+ # 查看 pyproject.toml 中的版本号(兼容 Python 3.10)
484
+ python -c "import pathlib; print([line for line in pathlib.Path('pyproject.toml').read_text('utf-8').splitlines() if line.startswith('version =')][0])"
485
+ python -c "import tgrocery; print(tgrocery.__version__)"
486
+ ```
487
+
488
+ PyPI 上的版本号不可覆盖,发布后也不能删除重传同一版本;每次发布都要提升版本号。
489
+
490
+ ### 3. 安装构建工具并清理旧产物
491
+
492
+ ```bash
493
+ # 安装构建工具
494
+ python -m pip install --upgrade build twine
495
+ ```
496
+
497
+ ```bash
498
+ # Linux / macOS / Git Bash
499
+ rm -rf build dist *.egg-info
500
+ ```
501
+
502
+ ```powershell
503
+ # PowerShell
504
+ Remove-Item -Recurse -Force build, dist -ErrorAction SilentlyContinue
505
+ Get-ChildItem -Filter *.egg-info -Recurse | Remove-Item -Recurse -Force
506
+ ```
507
+
508
+ ### 4. 运行质量检查
509
+
510
+ ```bash
511
+ ruff check .
512
+ ruff format --check .
513
+ pytest
514
+ ```
515
+
516
+ 三项都通过再打包,避免把坏版本发到 PyPI。
517
+
518
+ ### 5. 构建 wheel 与 sdist
519
+
520
+ ```bash
521
+ python -m build
522
+ ```
523
+
524
+ 产物在 `dist/`:
525
+
526
+ ```text
527
+ dist/
528
+ textgrocery_shop-0.2.0.tar.gz # 源码包
529
+ textgrocery_shop-0.2.0-py3-none-any.whl # 纯 Python wheel
530
+ ```
531
+
532
+ 只构建 wheel 用 `python -m build --wheel`;只构建源码包用
533
+ `python -m build --sdist`。`python -m build` 会创建隔离的构建环境,
534
+ 需要联网安装 `setuptools`。
535
+
536
+ ### 6. 检查产物
537
+
538
+ ```bash
539
+ python -m zipfile -l dist/textgrocery_shop-0.2.0-py3-none-any.whl
540
+ python -m twine check dist/*
541
+ ```
542
+
543
+ PowerShell 不会像 bash 那样展开 `dist/*`,可以改用:
544
+
545
+ ```powershell
546
+ python -m twine check (Get-ChildItem dist | ForEach-Object FullName)
547
+ ```
548
+
549
+ wheel 里应只包含运行时代码和元数据,例如:
550
+
551
+ ```text
552
+ tgrocery/*.py
553
+ tgrocery/learner/*.py
554
+ textgrocery_shop-0.2.0.dist-info/METADATA
555
+ textgrocery_shop-0.2.0.dist-info/licenses/LICENSE.txt
556
+ ```
557
+
558
+ 不应出现 `tests/`、`sample/`、`__pycache__/`、`build/` 或模型目录。源码包由
559
+ `MANIFEST.in` 控制,会额外包含 README、测试和示例;两个产物都应由
560
+ `twine check` 输出 `PASSED`。
561
+
562
+ ### 7. 本地安装 wheel 验证
563
+
564
+ 在干净环境中安装刚构建的 wheel:
565
+
566
+ ```bash
567
+ python -m pip install --force-reinstall --no-deps dist/textgrocery_shop-0.2.0-py3-none-any.whl
568
+ python -c "import tgrocery; print(tgrocery.__version__)"
569
+ ```
570
+
571
+ 再跑一个最小训练/预测:
572
+
573
+ ```bash
574
+ python -c "from tgrocery import Grocery; g=Grocery('smoke'); g.train([('a','上涨 突破 新高'),('b','下跌 亏损 风险')]); print(g.predict('上涨 突破'))"
575
+ ```
576
+
577
+ 该命令会在当前目录生成 `smoke/` 模型目录,验证后可以删除。如果当前目录是源码目录,
578
+ Python 可能优先导入本地 `tgrocery/`;请换到其他目录执行,或确认 `pip show textgrocery-shop`
579
+ 的 `Location` 指向安装目录。验证完后如继续开发,再执行 `pip install -e .[test]`。
580
+
581
+ ### 8.(可选)上传到 TestPyPI 彩排
582
+
583
+ 正式发布推荐用第 10 节的 Trusted Publishing;这一节只在需要本地彩排时使用。
584
+
585
+ 先在 [test.pypi.org](https://test.pypi.org/) 注册账号并创建 API token。
586
+ 建议用环境变量传递 token,不要把 token 写进仓库:
587
+
588
+ ```bash
589
+ export TWINE_USERNAME=__token__
590
+ export TWINE_PASSWORD=pypi-你的TestPyPI令牌
591
+ python -m twine upload --repository testpypi dist/*
592
+ ```
593
+
594
+ PowerShell:
595
+
596
+ ```powershell
597
+ $env:TWINE_USERNAME="__token__"
598
+ $env:TWINE_PASSWORD="pypi-你的TestPyPI令牌"
599
+ python -m twine upload --repository testpypi (Get-ChildItem dist | ForEach-Object FullName)
600
+ ```
601
+
602
+ 从 TestPyPI 安装验证:
603
+
604
+ ```bash
605
+ pip install --index-url https://test.pypi.org/simple/ \
606
+ --extra-index-url https://pypi.org/simple \
607
+ textgrocery-shop==0.2.0
608
+ ```
609
+
610
+ `--extra-index-url` 用于从正式 PyPI 获取 `jieba`、`numpy` 等依赖,因为 TestPyPI
611
+ 不保证同步所有依赖。TestPyPI 适合彩排,确认无误后再发正式 PyPI。
612
+
613
+ ### 9.(备用)手动上传到 PyPI
614
+
615
+ 推荐用第 10 节的 GitHub Actions + Trusted Publishing。只有需要绕过 CI 或排查上传
616
+ 问题时,才使用下面的本地 `twine` 流程。
617
+
618
+ 在 [pypi.org](https://pypi.org/) 创建 API token;首次发布通常需要账号级 token,
619
+ 项目创建后可改用仅限该项目的 token。账号需开启 2FA。
620
+
621
+ ```bash
622
+ export TWINE_USERNAME=__token__
623
+ export TWINE_PASSWORD=pypi-你的PyPI令牌
624
+ python -m twine upload dist/*
625
+ ```
626
+
627
+ PowerShell:
628
+
629
+ ```powershell
630
+ $env:TWINE_USERNAME="__token__"
631
+ $env:TWINE_PASSWORD="pypi-你的PyPI令牌"
632
+ python -m twine upload (Get-ChildItem dist | ForEach-Object FullName)
633
+ ```
634
+
635
+ 说明:
636
+
637
+ - `twine upload` 默认上传到正式 PyPI;显式指定可用 `--repository pypi`。
638
+ - 上传过程中断时,**不要修改代码后覆盖同一版本**。先确认哪些文件已上传,
639
+ 再决定重试同一批文件还是提升版本号。
640
+ - 版本号或文件名一旦被 PyPI 接收,即使删除也不能复用;元数据写错也要发新版本。
641
+ - 不要把 `dist/`、`.pypirc` 或 token 提交到 Git;`.pypirc` 已在 `.gitignore` 中。
642
+
643
+ 发布后验证:
644
+
645
+ ```bash
646
+ pip index versions textgrocery-shop
647
+ pip install --no-cache-dir textgrocery-shop==0.2.0
648
+ python -c "import tgrocery; print(tgrocery.__version__)"
649
+ ```
650
+
651
+ ### 10. 用 GitHub Actions 自动发布(推荐)
652
+
653
+ 仓库已包含 `.github/workflows/pypi_publish.yml`。在 GitHub **发布 Release** 时,
654
+ 工作流会自动构建 wheel/sdist 并上传 PyPI。它使用 PyPI Trusted Publishing
655
+ (OIDC 短期凭证),仓库中不需要保存 `TWINE_PASSWORD` 或 API token。
656
+
657
+ 工作流关键点:
658
+
659
+ - 触发条件:`release: types: [published]`
660
+ - 构建 job `release-build`:`python -m build`,artifact 名 `release-dists`
661
+ - 发布 job `pypi-publish`:`environment: pypi` + `permissions: id-token: write`
662
+ - 上传动作:`pypa/gh-action-pypi-publish@release/v1`
663
+
664
+ 一次性配置:
665
+
666
+ 1. 在 PyPI 的 **Publishing** 页面添加 GitHub publisher:
667
+ owner 填 `ranvane`、repository 填 `TextGrocery_Shop`、workflow 填
668
+ `pypi_publish.yml`、environment 填 `pypi`。项目尚不存在时使用
669
+ **pending publisher**。
670
+ 2. 在 GitHub 仓库 **Settings → Environments** 创建 `pypi` 环境,建议添加
671
+ required reviewers。
672
+ 3. 确认 `pyproject.toml` 的版本号与 `tgrocery/__init__.py` 的 `__version__`
673
+ 一致。
674
+
675
+ 发布流程:
676
+
677
+ ```bash
678
+ # 1. 确保代码已提交、CI 通过,然后打 tag
679
+ git tag v0.2.0
680
+ git push origin v0.2.0
681
+ ```
682
+
683
+ 2. 打开 GitHub 仓库的 **Releases → Draft a new release**,选择 tag `v0.2.0`,
684
+ 标题填 `v0.2.0`,点击 **Publish release**。
685
+ 3. 在 **Actions** 页面确认 `Upload Python Package` 工作流执行成功。
686
+ 4. 验证正式包:
687
+
688
+ ```bash
689
+ pip install --no-cache-dir textgrocery-shop==0.2.0
690
+ python -c "import tgrocery; print(tgrocery.__version__)"
691
+ ```
692
+
693
+ `permissions: id-token: write` 必须保留,否则 OIDC 无法换取 PyPI 凭证。
694
+ 若要先发 TestPyPI,在 workflow 的上传步骤加
695
+ `with: repository-url: https://test.pypi.org/legacy/`,并在 TestPyPI 的
696
+ Publishing 页面单独配置对应的 trusted publisher。
697
+
698
+ ### 11. 发布检查清单
699
+
700
+ - [ ] PyPI 发行名可用,或当前账号拥有该项目
701
+ - [ ] `pyproject.toml` 与 `tgrocery/__init__.py` 的版本号一致
702
+ - [ ] README 中的安装命令与实际发行名一致
703
+ - [ ] `ruff check .`、`ruff format --check .`、`pytest` 全部通过
704
+ - [ ] `python -m build` 生成 wheel 和 sdist
705
+ - [ ] `twine check` 全部 `PASSED`
706
+ - [ ] wheel 内没有测试、示例、缓存和模型目录
707
+ - [ ] 在干净环境安装 wheel 并能训练/预测
708
+ - [ ] (可选)在 TestPyPI 安装验证通过
709
+ - [ ] 打 tag(如 `v0.2.0`)并推送
710
+ - [ ] 创建并发布 GitHub Release,触发 `Upload Python Package`
711
+ - [ ] GitHub Actions 发布 job 成功,正式 PyPI 可用 `pip install` 安装
712
+ - [ ] 未提交 `dist/`、`.pypirc`、token 或本地模型
713
+
714
+ ## 开发
715
+
716
+ ```bash
717
+ pip install -e .[test]
718
+ pytest
719
+ ruff check .
720
+ ruff format --check .
721
+ ```
722
+
723
+ ## 更新日志
724
+
725
+ - **2026-10-24**:PyPI 发行名改为 `TextGrocery_Shop`
726
+ (`pip install textgrocery-shop`);导入名仍为 `tgrocery`。
727
+ 许可证元数据与 `LICENSE.txt` 统一为 BSD-3-Clause。
728
+ - **2026-10-24**:发布流程改为 GitHub Release + Trusted Publishing,工作流
729
+ `.github/workflows/pypi_publish.yml` 自动上传 PyPI。
730
+
731
+ ## 许可证
732
+
733
+ BSD,见 [LICENSE.txt](LICENSE.txt)。