pyzk20r 0.1.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.
- pyzk20r-0.1.0/LICENSE +21 -0
- pyzk20r-0.1.0/PKG-INFO +298 -0
- pyzk20r-0.1.0/README.md +275 -0
- pyzk20r-0.1.0/examples/capture.py +26 -0
- pyzk20r-0.1.0/examples/enroll_verify.py +30 -0
- pyzk20r-0.1.0/examples/identify_1n.py +33 -0
- pyzk20r-0.1.0/examples/industrial_login.py +167 -0
- pyzk20r-0.1.0/examples/persist.py +83 -0
- pyzk20r-0.1.0/pyproject.toml +59 -0
- pyzk20r-0.1.0/pyzk20r/__init__.py +44 -0
- pyzk20r-0.1.0/pyzk20r/_binding.py +211 -0
- pyzk20r-0.1.0/pyzk20r/db.py +331 -0
- pyzk20r-0.1.0/pyzk20r/device.py +340 -0
- pyzk20r-0.1.0/pyzk20r/exceptions.py +79 -0
- pyzk20r-0.1.0/pyzk20r/py.typed +0 -0
- pyzk20r-0.1.0/tests/conftest.py +194 -0
- pyzk20r-0.1.0/tests/test_binding.py +65 -0
- pyzk20r-0.1.0/tests/test_db.py +283 -0
- pyzk20r-0.1.0/tests/test_device.py +221 -0
- pyzk20r-0.1.0/tests/test_integration.py +117 -0
- pyzk20r-0.1.0/tests/test_smoke_real.py +45 -0
pyzk20r-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 yunluo
|
|
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.
|
pyzk20r-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyzk20r
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ZKTeco Live20R 指纹采集器 Python 库 (ZKFinger SDK C API, 纯 ctypes, 零依赖, 支持 Windows XP / Python 3.4+)
|
|
5
|
+
Keywords: zkteco,fingerprint,live20r,biometrics,libzkfp
|
|
6
|
+
Author: yunluo
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
13
|
+
Classifier: Operating System :: Microsoft :: Windows :: Windows XP
|
|
14
|
+
Classifier: Operating System :: Microsoft :: Windows :: Windows 10
|
|
15
|
+
Classifier: Operating System :: Microsoft :: Windows :: Windows 11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Topic :: Security
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.4
|
|
21
|
+
Project-URL: Homepage, https://www.zksps.com/productinfo/46887.html
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# pyzk20r
|
|
25
|
+
|
|
26
|
+
中控智慧 (ZKTeco / 熵基) **Live20R** 指纹采集器的 Python 库。
|
|
27
|
+
|
|
28
|
+
基于官方 [ZKFinger Reader SDK C API](https://www.zksps.com/productinfo/46887.html) 的纯 `ctypes` 封装:
|
|
29
|
+
|
|
30
|
+
- **零第三方依赖**, 纯标准库
|
|
31
|
+
- 支持 **Windows XP / Python 3.4+** (单一代码库, 语法兼容 3.4, 类型标注为 PEP 484:
|
|
32
|
+
参数/返回内联标注, typing 构造以字符串形式标注, 变量用注释)
|
|
33
|
+
- 使用驱动安装的系统 DLL (不捆绑分发, 见 DLL 查找顺序)
|
|
34
|
+
- 同步阻塞 + 生成器式持续采集; 无内部线程, 单线程使用; 方法粒度小, 便于子类覆写
|
|
35
|
+
- **一人多指纹** 模型: `user_id` 可登记多枚指纹, 识别直接返回用户
|
|
36
|
+
|
|
37
|
+
## 安装
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install pyzk20r
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
> 目标机器必须安装 Live20R/SLK20R 的驱动 (libusb0 USB 驱动 + SDK 运行库)。驱动安装器会把
|
|
44
|
+
> libzkfp.dll 及其依赖链 (ZKFPCap.dll、zkfinger12.dll、ZKFPSensors\*.dll) 装进
|
|
45
|
+
> System32/SysWOW64, 本库直接加载系统副本 —— libzkfp.dll 的内部依赖按其自身所在目录解析,
|
|
46
|
+
> 单独拷贝一个 DLL 无法工作, 因此**本库不捆绑分发 DLL**。
|
|
47
|
+
> 也可用环境变量 `PYZK20R_DLL` 或 `Scanner(dll_path=...)` 显式指定 `libzkfp.dll` 路径。
|
|
48
|
+
|
|
49
|
+
## 快速上手
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from pyzk20r import FingerprintDB, MatchError, Scanner
|
|
53
|
+
|
|
54
|
+
db = FingerprintDB()
|
|
55
|
+
with Scanner() as dev: # 构造即打开, 退出自动关闭
|
|
56
|
+
print(dev.info()) # 厂商/产品名/序列号/VID&PID
|
|
57
|
+
|
|
58
|
+
# 登记: 采 3 次自动合并入库 (同一工号可多次调用 = 多枚指纹)
|
|
59
|
+
tpl = db.enroll(dev, user_id="1001")
|
|
60
|
+
|
|
61
|
+
# 1:N 识别 -> (user_id, 分数)
|
|
62
|
+
frame = dev.acquire() # 阻塞等待一次按压
|
|
63
|
+
uid, score = db.identify(frame.template)
|
|
64
|
+
|
|
65
|
+
# 1:1 验证 -> 分数 (对该用户所有指纹取最优)
|
|
66
|
+
score = db.verify("1001", dev.acquire().template)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
持续采集 (生成器):
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
with Scanner() as dev:
|
|
73
|
+
for frame in dev.capture(poll_interval=0.1): # 暂无手指时自动重试
|
|
74
|
+
open("finger.bmp", "wb").write(frame.to_bmp()) # 图像存 BMP
|
|
75
|
+
tpl = frame.template # 提取好的模板
|
|
76
|
+
break
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## API 概览
|
|
80
|
+
|
|
81
|
+
### `Scanner(index=0, dll_path=None)` — 采集器
|
|
82
|
+
|
|
83
|
+
| 成员 | 说明 |
|
|
84
|
+
| --- | --- |
|
|
85
|
+
| `acquire(poll_interval=0.1, timeout=None)` | **阻塞等待手指**并采集一帧 → `Frame`; `timeout` 秒超时抛 `CaptureError` (`None` 为一直等) |
|
|
86
|
+
| `capture(poll_interval=0.1)` | 持续采集生成器, 逐帧 `yield Frame` (设备关闭后下一次迭代抛 `ZKFPMError`) |
|
|
87
|
+
| `acquire_image()` | 只取图像字节 |
|
|
88
|
+
| `enroll(poll_interval)` | 连续采集 3 次 (SDK 合并固定要求) 返回模板列表, 每次回调 `on_enroll_progress(done, total)` |
|
|
89
|
+
| `width / height / dpi` (dpi 可写) | 图像规格; 儿童指纹建议 DPI 设 750/1000 |
|
|
90
|
+
| `fake_detect` (可读写) | 防假指纹开关 |
|
|
91
|
+
| `finger_is_live` | 最近一次采集是否真手指 (`value & 31 == 31`) |
|
|
92
|
+
| `info()` | `DeviceInfo(vendor, product, serial, vid_pid)` |
|
|
93
|
+
| `led('white'/'green'/'red', on)` | 指示灯 |
|
|
94
|
+
| `template_format` (`'ansi'/'iso'`, 可读写) | 模板标准 |
|
|
95
|
+
| `template_quality(template)` | 模板质量分 |
|
|
96
|
+
| `device_ok` | 设备状态 |
|
|
97
|
+
| `open() / close()` / `with` | 生命周期, 可重复 close |
|
|
98
|
+
|
|
99
|
+
### `Frame` — 单次采集帧
|
|
100
|
+
|
|
101
|
+
`image` (原始灰度字节) / `template` (指纹模板) / `width / height / dpi` / `to_bmp()`
|
|
102
|
+
|
|
103
|
+
### `FingerprintDB(dll_path=None)` — 内存指纹库 (一人多指纹)
|
|
104
|
+
|
|
105
|
+
| 成员 | 说明 |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| `enroll(scanner, user_id)` | 完整登记: 采 3 次 → 合并 → 存入该用户名下 |
|
|
108
|
+
| `add(user_id, template)` | 直接添加一枚模板 (同一 user_id 可多次) |
|
|
109
|
+
| `identify(template)` | 1:N → `(user_id, score)`, 无匹配抛 `MatchError` |
|
|
110
|
+
| `verify(user_id, template)` | 1:1 → 最优分数, 不匹配抛 `MatchError` |
|
|
111
|
+
| `match(a, b)` | 两枚模板比对 → 分数 |
|
|
112
|
+
| `merge([t1, t2, t3])` | 3 枚采集模板合并为登记模板 |
|
|
113
|
+
| `remove(user_id[, template])` | 删除某用户的全部/指定指纹 |
|
|
114
|
+
| `clear() / count / users / fingers_of(user_id)` | 库管理 |
|
|
115
|
+
| `dump() / load(data)` | 持久化契约: 导出/重建 JSON 兼容结构 (存储介质与时机由应用层决定) |
|
|
116
|
+
| `threshold_1_1 / threshold_1_n` (可读写) | 比对阈值, 默认 35 / 55 (0~100) |
|
|
117
|
+
| `extract_from_image(path, dpi=500)` | 从 BMP 提取模板 (仅 SDK 标准版) |
|
|
118
|
+
|
|
119
|
+
### 异常层级
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
ZKFPMError (含 .code 原始 SDK 错误码)
|
|
123
|
+
├── DeviceError
|
|
124
|
+
│ └── DeviceNotFoundError # -3 无设备 / -6 打开失败
|
|
125
|
+
├── CaptureError # -8 取像失败等
|
|
126
|
+
├── MatchError # -20 不匹配
|
|
127
|
+
├── MergeError # -22 合并失败
|
|
128
|
+
├── ExtractError # -9/-26/-27 图像提取失败
|
|
129
|
+
└── LibraryError # -1/-2/-17 库层失败
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 继承点
|
|
133
|
+
|
|
134
|
+
- `Scanner.on_enroll_progress(done, total)` — 登记进度钩子
|
|
135
|
+
- `Scanner._try_acquire()` — 采集前后的自定义处理 (如防假判定重试)
|
|
136
|
+
- `Scanner` / `FingerprintDB` 所有公开方法均可覆写
|
|
137
|
+
|
|
138
|
+
### 线程模型
|
|
139
|
+
|
|
140
|
+
本库是**同步单线程**设计: 无内部线程, SDK 句柄与全局初始化计数不加锁。
|
|
141
|
+
请勿在多线程中并发构造/关闭设备或并发采集; 需要后台采集请自行承载线程,
|
|
142
|
+
线程内独占使用一个 `Scanner`。
|
|
143
|
+
|
|
144
|
+
### 持久化
|
|
145
|
+
|
|
146
|
+
内存指纹库**不自动落盘**, 进程退出即消失; 持久化由应用层用 `dump()/load()` 接入任意介质:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
import json
|
|
150
|
+
data = db.dump() # {"version": 1, "templates": [[uid, "BASE64"], ...]}
|
|
151
|
+
json.dump(data, open("fingers.json", "w"))
|
|
152
|
+
|
|
153
|
+
db = FingerprintDB.load(json.load(open("fingers.json"))) # 启动时重建, 无需重新按手指
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
SQLite 存取完整示例见 `examples/persist.py`。
|
|
157
|
+
|
|
158
|
+
## 示例
|
|
159
|
+
|
|
160
|
+
| 脚本 | 用途 |
|
|
161
|
+
| --- | --- |
|
|
162
|
+
| `examples/capture.py` | 采集一帧指纹存 BMP |
|
|
163
|
+
| `examples/enroll_verify.py` | 登记 + 1:1 验证 |
|
|
164
|
+
| `examples/identify_1n.py` | 多人登记 + 1:N 识别循环 |
|
|
165
|
+
| `examples/persist.py` | 指纹 SQLite 持久化 |
|
|
166
|
+
| `examples/industrial_login.py` | **工业现场登录改造参考**: 登记工号指纹 -> 刷指纹自动登录 (含工号+密码兜底) |
|
|
167
|
+
|
|
168
|
+
## 登录场景流程 (industrial_login.py)
|
|
169
|
+
|
|
170
|
+
```mermaid
|
|
171
|
+
flowchart TD
|
|
172
|
+
A([程序启动]) --> B["打开 SQLite<br/>load_db() 重建内存指纹库"]
|
|
173
|
+
B --> C["打开设备 Scanner"]
|
|
174
|
+
C -->|成功| M1
|
|
175
|
+
C -->|"设备缺失 (ZKFPMError)"| M2
|
|
176
|
+
|
|
177
|
+
subgraph M1["完整模式菜单"]
|
|
178
|
+
D{"选择"}
|
|
179
|
+
end
|
|
180
|
+
subgraph M2["降级模式菜单"]
|
|
181
|
+
D2{"选择"}
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
D -->|"[1] 指纹登录"| F["等待手指<br/>acquire(timeout=60)"]
|
|
185
|
+
F -->|"超时 / 采集失败"| D
|
|
186
|
+
F -->|取到指纹| G["db.identify(模板)<br/>1:N 识别"]
|
|
187
|
+
G -->|"MatchError 未识别"| H["提示: 未识别<br/>请用工号+密码登录"] --> D
|
|
188
|
+
G -->|"返回 (工号, 分数)"| I["do_login(工号)<br/>调用你的工业软件 API"]
|
|
189
|
+
I --> OK([自动登录成功<br/>进入系统])
|
|
190
|
+
|
|
191
|
+
D -->|"[2] 登记员工指纹"| K["输入工号<br/>新员工建档 / 老员工追加"]
|
|
192
|
+
K --> L["按手指 3 次<br/>db.enroll: 采集 → DBMerge → 入库"]
|
|
193
|
+
L --> S["SQLite 保存<br/>save_fingerprints()"]
|
|
194
|
+
S --> Q{继续加录一枚?}
|
|
195
|
+
Q -->|y| L
|
|
196
|
+
Q -->|N| D
|
|
197
|
+
|
|
198
|
+
D -->|"[3] 删除员工"| R["db.remove(工号)<br/>删除 SQLite 记录"] --> D
|
|
199
|
+
|
|
200
|
+
D -->|"[p] 工号+密码"| P["check_password(工号, 密码)<br/>对接原口令校验"]
|
|
201
|
+
D2 -->|"[p] 工号+密码"| P
|
|
202
|
+
P -->|校验通过| I
|
|
203
|
+
P -->|密码错误| D
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
刷指纹自动登录时序:
|
|
207
|
+
|
|
208
|
+
```mermaid
|
|
209
|
+
sequenceDiagram
|
|
210
|
+
actor W as 员工
|
|
211
|
+
participant App as 登录终端<br/>(industrial_login.py)
|
|
212
|
+
participant DB as FingerprintDB<br/>(内存指纹库)
|
|
213
|
+
participant SDK as libzkfp.dll<br/>(ZKFinger 算法)
|
|
214
|
+
participant Store as SQLite<br/>workers.db
|
|
215
|
+
participant MES as 你的工业软件<br/>do_login(工号)
|
|
216
|
+
|
|
217
|
+
Note over App,Store: 程序启动时: load_db() 从 SQLite 重建内存库
|
|
218
|
+
W->>App: 选择 [1] 指纹登录
|
|
219
|
+
App->>SDK: acquire(timeout=60) 轮询取像
|
|
220
|
+
W->>SDK: 按压手指
|
|
221
|
+
SDK-->>App: Frame(图像 + 模板 ~1.3KB)
|
|
222
|
+
App->>DB: identify(frame.template)
|
|
223
|
+
DB->>SDK: ZKFPM_DBIdentify (1:N)
|
|
224
|
+
SDK-->>DB: fid + 分数
|
|
225
|
+
DB-->>App: (工号, 分数) 或 MatchError
|
|
226
|
+
|
|
227
|
+
alt 识别成功
|
|
228
|
+
App->>MES: do_login(工号)
|
|
229
|
+
MES-->>W: 自动登录, 进入系统
|
|
230
|
+
else 未识别 / 超时
|
|
231
|
+
App-->>W: 提示改用工号+密码登录
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
Note over App,Store: 登记流程: db.enroll(采3次合并) → save_fingerprints() 写 SQLite → 重启后 load_db() 恢复
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## DLL 查找顺序
|
|
238
|
+
|
|
239
|
+
1. `Scanner(dll_path=...)` / `FingerprintDB(dll_path=...)` 显式指定
|
|
240
|
+
2. 环境变量 `PYZK20R_DLL`
|
|
241
|
+
3. 系统副本 `C:\Windows\System32\libzkfp.dll` (32 位 Python 下经 WOW64 重定向即 SysWOW64;
|
|
242
|
+
需先安装驱动, 完整依赖链在系统目录)
|
|
243
|
+
4. 兜底: 裸名 `libzkfp.dll` 交给 Windows 搜索 (PATH 中)
|
|
244
|
+
|
|
245
|
+
> 未安装许可文件 `zkfinger.lic` 时 SDK 会打印许可警告但功能可用 (算法 v12)。
|
|
246
|
+
|
|
247
|
+
## XP / Python 3.4 部署要点
|
|
248
|
+
|
|
249
|
+
- 目标机安装 **x86 版 Python 3.4** 与 Live20R/SLK20R USB 驱动
|
|
250
|
+
- `pip install pyzk20r-0.1.0-py3-none-any.whl` (纯 Python wheel, 无需编译)。
|
|
251
|
+
注意: 构建产物为 Metadata-Version 2.4, XP 上的老版 pip 若拒绝安装,
|
|
252
|
+
先升级 pip (`python -m pip install -U "pip<20"`), 或直接把 wheel 当 zip
|
|
253
|
+
解包进 site-packages (纯 Python 包, 解包即用)
|
|
254
|
+
- XP 下 `libzkfp.dll` 依赖系统 VC 运行库; 若加载报错, 安装 VC++ 2010/2013 运行库 (x86)
|
|
255
|
+
- 本库代码不含 f-string / `typing` 导入 / 3.5+ 语法 (CI 有语法守卫测试)
|
|
256
|
+
|
|
257
|
+
## 开发
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
uv run pytest # mock 单元测试 + 真机冒烟 (无设备自动跳过)
|
|
261
|
+
uv run ruff check . # lint
|
|
262
|
+
uv run mypy # 类型检查 (PEP 484 标注)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
dev 工具由 `[dependency-groups]` 提供 (pytest / ruff / mypy), `uv run` 自动同步。
|
|
266
|
+
|
|
267
|
+
打包使用 uv 自家后端 (`uv_build`), 无需额外装 hatchling:
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
uv build # 生成 dist/*.whl 与 dist/*.tar.gz
|
|
271
|
+
uv build --wheel # 只出 wheel
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
产物校验: wheel 含 `pyzk20r/` 全部模块 + `py.typed` + `dist-info/licenses/LICENSE`;
|
|
275
|
+
sdist 额外含 `tests/` 与 `examples/`(指纹数据 `*.db` 自动排除)。
|
|
276
|
+
|
|
277
|
+
SDK 文档 (中英文 PDF + 头文件 + MFC Demo) 从 ZKTeco 官网 / "ZKFinger SDK" 安装包获取;
|
|
278
|
+
本库的接口注释即按其 C API 编写。
|
|
279
|
+
|
|
280
|
+
## 发版 (PyPI)
|
|
281
|
+
|
|
282
|
+
本库**仅支持 Windows**, 且**只通过 tag 推送发版**:
|
|
283
|
+
推送 `vX.Y.Z` tag 后, Actions 自动执行 测试 (Windows) → 构建并校验 tag 与版本一致 → 发布 PyPI。
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
git tag v0.1.0
|
|
287
|
+
git push origin v0.1.0 # 触发 release.yml
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
前置 (一次性, 二选一):
|
|
291
|
+
- **推荐**: 在 pypi.org 的 pyzk20r 项目页配置 Trusted Publisher
|
|
292
|
+
(`owner=yunluo`, `repo=pyzk_20r`, `workflow=release.yml`, `environment=pypi`)
|
|
293
|
+
- 或在 GitHub 仓库 Settings → Secrets 添加 `PYPI_API_TOKEN`,
|
|
294
|
+
并按 `release.yml` 内注释把发布 step 改为 token 方式
|
|
295
|
+
|
|
296
|
+
## License
|
|
297
|
+
|
|
298
|
+
MIT (见 LICENSE)。ZKFinger SDK 及 `libzkfp.dll` 版权归 ZKTeco 所有, 需随官方驱动获取, 仅供配合其设备使用。
|
pyzk20r-0.1.0/README.md
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# pyzk20r
|
|
2
|
+
|
|
3
|
+
中控智慧 (ZKTeco / 熵基) **Live20R** 指纹采集器的 Python 库。
|
|
4
|
+
|
|
5
|
+
基于官方 [ZKFinger Reader SDK C API](https://www.zksps.com/productinfo/46887.html) 的纯 `ctypes` 封装:
|
|
6
|
+
|
|
7
|
+
- **零第三方依赖**, 纯标准库
|
|
8
|
+
- 支持 **Windows XP / Python 3.4+** (单一代码库, 语法兼容 3.4, 类型标注为 PEP 484:
|
|
9
|
+
参数/返回内联标注, typing 构造以字符串形式标注, 变量用注释)
|
|
10
|
+
- 使用驱动安装的系统 DLL (不捆绑分发, 见 DLL 查找顺序)
|
|
11
|
+
- 同步阻塞 + 生成器式持续采集; 无内部线程, 单线程使用; 方法粒度小, 便于子类覆写
|
|
12
|
+
- **一人多指纹** 模型: `user_id` 可登记多枚指纹, 识别直接返回用户
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install pyzk20r
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
> 目标机器必须安装 Live20R/SLK20R 的驱动 (libusb0 USB 驱动 + SDK 运行库)。驱动安装器会把
|
|
21
|
+
> libzkfp.dll 及其依赖链 (ZKFPCap.dll、zkfinger12.dll、ZKFPSensors\*.dll) 装进
|
|
22
|
+
> System32/SysWOW64, 本库直接加载系统副本 —— libzkfp.dll 的内部依赖按其自身所在目录解析,
|
|
23
|
+
> 单独拷贝一个 DLL 无法工作, 因此**本库不捆绑分发 DLL**。
|
|
24
|
+
> 也可用环境变量 `PYZK20R_DLL` 或 `Scanner(dll_path=...)` 显式指定 `libzkfp.dll` 路径。
|
|
25
|
+
|
|
26
|
+
## 快速上手
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from pyzk20r import FingerprintDB, MatchError, Scanner
|
|
30
|
+
|
|
31
|
+
db = FingerprintDB()
|
|
32
|
+
with Scanner() as dev: # 构造即打开, 退出自动关闭
|
|
33
|
+
print(dev.info()) # 厂商/产品名/序列号/VID&PID
|
|
34
|
+
|
|
35
|
+
# 登记: 采 3 次自动合并入库 (同一工号可多次调用 = 多枚指纹)
|
|
36
|
+
tpl = db.enroll(dev, user_id="1001")
|
|
37
|
+
|
|
38
|
+
# 1:N 识别 -> (user_id, 分数)
|
|
39
|
+
frame = dev.acquire() # 阻塞等待一次按压
|
|
40
|
+
uid, score = db.identify(frame.template)
|
|
41
|
+
|
|
42
|
+
# 1:1 验证 -> 分数 (对该用户所有指纹取最优)
|
|
43
|
+
score = db.verify("1001", dev.acquire().template)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
持续采集 (生成器):
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
with Scanner() as dev:
|
|
50
|
+
for frame in dev.capture(poll_interval=0.1): # 暂无手指时自动重试
|
|
51
|
+
open("finger.bmp", "wb").write(frame.to_bmp()) # 图像存 BMP
|
|
52
|
+
tpl = frame.template # 提取好的模板
|
|
53
|
+
break
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## API 概览
|
|
57
|
+
|
|
58
|
+
### `Scanner(index=0, dll_path=None)` — 采集器
|
|
59
|
+
|
|
60
|
+
| 成员 | 说明 |
|
|
61
|
+
| --- | --- |
|
|
62
|
+
| `acquire(poll_interval=0.1, timeout=None)` | **阻塞等待手指**并采集一帧 → `Frame`; `timeout` 秒超时抛 `CaptureError` (`None` 为一直等) |
|
|
63
|
+
| `capture(poll_interval=0.1)` | 持续采集生成器, 逐帧 `yield Frame` (设备关闭后下一次迭代抛 `ZKFPMError`) |
|
|
64
|
+
| `acquire_image()` | 只取图像字节 |
|
|
65
|
+
| `enroll(poll_interval)` | 连续采集 3 次 (SDK 合并固定要求) 返回模板列表, 每次回调 `on_enroll_progress(done, total)` |
|
|
66
|
+
| `width / height / dpi` (dpi 可写) | 图像规格; 儿童指纹建议 DPI 设 750/1000 |
|
|
67
|
+
| `fake_detect` (可读写) | 防假指纹开关 |
|
|
68
|
+
| `finger_is_live` | 最近一次采集是否真手指 (`value & 31 == 31`) |
|
|
69
|
+
| `info()` | `DeviceInfo(vendor, product, serial, vid_pid)` |
|
|
70
|
+
| `led('white'/'green'/'red', on)` | 指示灯 |
|
|
71
|
+
| `template_format` (`'ansi'/'iso'`, 可读写) | 模板标准 |
|
|
72
|
+
| `template_quality(template)` | 模板质量分 |
|
|
73
|
+
| `device_ok` | 设备状态 |
|
|
74
|
+
| `open() / close()` / `with` | 生命周期, 可重复 close |
|
|
75
|
+
|
|
76
|
+
### `Frame` — 单次采集帧
|
|
77
|
+
|
|
78
|
+
`image` (原始灰度字节) / `template` (指纹模板) / `width / height / dpi` / `to_bmp()`
|
|
79
|
+
|
|
80
|
+
### `FingerprintDB(dll_path=None)` — 内存指纹库 (一人多指纹)
|
|
81
|
+
|
|
82
|
+
| 成员 | 说明 |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| `enroll(scanner, user_id)` | 完整登记: 采 3 次 → 合并 → 存入该用户名下 |
|
|
85
|
+
| `add(user_id, template)` | 直接添加一枚模板 (同一 user_id 可多次) |
|
|
86
|
+
| `identify(template)` | 1:N → `(user_id, score)`, 无匹配抛 `MatchError` |
|
|
87
|
+
| `verify(user_id, template)` | 1:1 → 最优分数, 不匹配抛 `MatchError` |
|
|
88
|
+
| `match(a, b)` | 两枚模板比对 → 分数 |
|
|
89
|
+
| `merge([t1, t2, t3])` | 3 枚采集模板合并为登记模板 |
|
|
90
|
+
| `remove(user_id[, template])` | 删除某用户的全部/指定指纹 |
|
|
91
|
+
| `clear() / count / users / fingers_of(user_id)` | 库管理 |
|
|
92
|
+
| `dump() / load(data)` | 持久化契约: 导出/重建 JSON 兼容结构 (存储介质与时机由应用层决定) |
|
|
93
|
+
| `threshold_1_1 / threshold_1_n` (可读写) | 比对阈值, 默认 35 / 55 (0~100) |
|
|
94
|
+
| `extract_from_image(path, dpi=500)` | 从 BMP 提取模板 (仅 SDK 标准版) |
|
|
95
|
+
|
|
96
|
+
### 异常层级
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
ZKFPMError (含 .code 原始 SDK 错误码)
|
|
100
|
+
├── DeviceError
|
|
101
|
+
│ └── DeviceNotFoundError # -3 无设备 / -6 打开失败
|
|
102
|
+
├── CaptureError # -8 取像失败等
|
|
103
|
+
├── MatchError # -20 不匹配
|
|
104
|
+
├── MergeError # -22 合并失败
|
|
105
|
+
├── ExtractError # -9/-26/-27 图像提取失败
|
|
106
|
+
└── LibraryError # -1/-2/-17 库层失败
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 继承点
|
|
110
|
+
|
|
111
|
+
- `Scanner.on_enroll_progress(done, total)` — 登记进度钩子
|
|
112
|
+
- `Scanner._try_acquire()` — 采集前后的自定义处理 (如防假判定重试)
|
|
113
|
+
- `Scanner` / `FingerprintDB` 所有公开方法均可覆写
|
|
114
|
+
|
|
115
|
+
### 线程模型
|
|
116
|
+
|
|
117
|
+
本库是**同步单线程**设计: 无内部线程, SDK 句柄与全局初始化计数不加锁。
|
|
118
|
+
请勿在多线程中并发构造/关闭设备或并发采集; 需要后台采集请自行承载线程,
|
|
119
|
+
线程内独占使用一个 `Scanner`。
|
|
120
|
+
|
|
121
|
+
### 持久化
|
|
122
|
+
|
|
123
|
+
内存指纹库**不自动落盘**, 进程退出即消失; 持久化由应用层用 `dump()/load()` 接入任意介质:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
import json
|
|
127
|
+
data = db.dump() # {"version": 1, "templates": [[uid, "BASE64"], ...]}
|
|
128
|
+
json.dump(data, open("fingers.json", "w"))
|
|
129
|
+
|
|
130
|
+
db = FingerprintDB.load(json.load(open("fingers.json"))) # 启动时重建, 无需重新按手指
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
SQLite 存取完整示例见 `examples/persist.py`。
|
|
134
|
+
|
|
135
|
+
## 示例
|
|
136
|
+
|
|
137
|
+
| 脚本 | 用途 |
|
|
138
|
+
| --- | --- |
|
|
139
|
+
| `examples/capture.py` | 采集一帧指纹存 BMP |
|
|
140
|
+
| `examples/enroll_verify.py` | 登记 + 1:1 验证 |
|
|
141
|
+
| `examples/identify_1n.py` | 多人登记 + 1:N 识别循环 |
|
|
142
|
+
| `examples/persist.py` | 指纹 SQLite 持久化 |
|
|
143
|
+
| `examples/industrial_login.py` | **工业现场登录改造参考**: 登记工号指纹 -> 刷指纹自动登录 (含工号+密码兜底) |
|
|
144
|
+
|
|
145
|
+
## 登录场景流程 (industrial_login.py)
|
|
146
|
+
|
|
147
|
+
```mermaid
|
|
148
|
+
flowchart TD
|
|
149
|
+
A([程序启动]) --> B["打开 SQLite<br/>load_db() 重建内存指纹库"]
|
|
150
|
+
B --> C["打开设备 Scanner"]
|
|
151
|
+
C -->|成功| M1
|
|
152
|
+
C -->|"设备缺失 (ZKFPMError)"| M2
|
|
153
|
+
|
|
154
|
+
subgraph M1["完整模式菜单"]
|
|
155
|
+
D{"选择"}
|
|
156
|
+
end
|
|
157
|
+
subgraph M2["降级模式菜单"]
|
|
158
|
+
D2{"选择"}
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
D -->|"[1] 指纹登录"| F["等待手指<br/>acquire(timeout=60)"]
|
|
162
|
+
F -->|"超时 / 采集失败"| D
|
|
163
|
+
F -->|取到指纹| G["db.identify(模板)<br/>1:N 识别"]
|
|
164
|
+
G -->|"MatchError 未识别"| H["提示: 未识别<br/>请用工号+密码登录"] --> D
|
|
165
|
+
G -->|"返回 (工号, 分数)"| I["do_login(工号)<br/>调用你的工业软件 API"]
|
|
166
|
+
I --> OK([自动登录成功<br/>进入系统])
|
|
167
|
+
|
|
168
|
+
D -->|"[2] 登记员工指纹"| K["输入工号<br/>新员工建档 / 老员工追加"]
|
|
169
|
+
K --> L["按手指 3 次<br/>db.enroll: 采集 → DBMerge → 入库"]
|
|
170
|
+
L --> S["SQLite 保存<br/>save_fingerprints()"]
|
|
171
|
+
S --> Q{继续加录一枚?}
|
|
172
|
+
Q -->|y| L
|
|
173
|
+
Q -->|N| D
|
|
174
|
+
|
|
175
|
+
D -->|"[3] 删除员工"| R["db.remove(工号)<br/>删除 SQLite 记录"] --> D
|
|
176
|
+
|
|
177
|
+
D -->|"[p] 工号+密码"| P["check_password(工号, 密码)<br/>对接原口令校验"]
|
|
178
|
+
D2 -->|"[p] 工号+密码"| P
|
|
179
|
+
P -->|校验通过| I
|
|
180
|
+
P -->|密码错误| D
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
刷指纹自动登录时序:
|
|
184
|
+
|
|
185
|
+
```mermaid
|
|
186
|
+
sequenceDiagram
|
|
187
|
+
actor W as 员工
|
|
188
|
+
participant App as 登录终端<br/>(industrial_login.py)
|
|
189
|
+
participant DB as FingerprintDB<br/>(内存指纹库)
|
|
190
|
+
participant SDK as libzkfp.dll<br/>(ZKFinger 算法)
|
|
191
|
+
participant Store as SQLite<br/>workers.db
|
|
192
|
+
participant MES as 你的工业软件<br/>do_login(工号)
|
|
193
|
+
|
|
194
|
+
Note over App,Store: 程序启动时: load_db() 从 SQLite 重建内存库
|
|
195
|
+
W->>App: 选择 [1] 指纹登录
|
|
196
|
+
App->>SDK: acquire(timeout=60) 轮询取像
|
|
197
|
+
W->>SDK: 按压手指
|
|
198
|
+
SDK-->>App: Frame(图像 + 模板 ~1.3KB)
|
|
199
|
+
App->>DB: identify(frame.template)
|
|
200
|
+
DB->>SDK: ZKFPM_DBIdentify (1:N)
|
|
201
|
+
SDK-->>DB: fid + 分数
|
|
202
|
+
DB-->>App: (工号, 分数) 或 MatchError
|
|
203
|
+
|
|
204
|
+
alt 识别成功
|
|
205
|
+
App->>MES: do_login(工号)
|
|
206
|
+
MES-->>W: 自动登录, 进入系统
|
|
207
|
+
else 未识别 / 超时
|
|
208
|
+
App-->>W: 提示改用工号+密码登录
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
Note over App,Store: 登记流程: db.enroll(采3次合并) → save_fingerprints() 写 SQLite → 重启后 load_db() 恢复
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## DLL 查找顺序
|
|
215
|
+
|
|
216
|
+
1. `Scanner(dll_path=...)` / `FingerprintDB(dll_path=...)` 显式指定
|
|
217
|
+
2. 环境变量 `PYZK20R_DLL`
|
|
218
|
+
3. 系统副本 `C:\Windows\System32\libzkfp.dll` (32 位 Python 下经 WOW64 重定向即 SysWOW64;
|
|
219
|
+
需先安装驱动, 完整依赖链在系统目录)
|
|
220
|
+
4. 兜底: 裸名 `libzkfp.dll` 交给 Windows 搜索 (PATH 中)
|
|
221
|
+
|
|
222
|
+
> 未安装许可文件 `zkfinger.lic` 时 SDK 会打印许可警告但功能可用 (算法 v12)。
|
|
223
|
+
|
|
224
|
+
## XP / Python 3.4 部署要点
|
|
225
|
+
|
|
226
|
+
- 目标机安装 **x86 版 Python 3.4** 与 Live20R/SLK20R USB 驱动
|
|
227
|
+
- `pip install pyzk20r-0.1.0-py3-none-any.whl` (纯 Python wheel, 无需编译)。
|
|
228
|
+
注意: 构建产物为 Metadata-Version 2.4, XP 上的老版 pip 若拒绝安装,
|
|
229
|
+
先升级 pip (`python -m pip install -U "pip<20"`), 或直接把 wheel 当 zip
|
|
230
|
+
解包进 site-packages (纯 Python 包, 解包即用)
|
|
231
|
+
- XP 下 `libzkfp.dll` 依赖系统 VC 运行库; 若加载报错, 安装 VC++ 2010/2013 运行库 (x86)
|
|
232
|
+
- 本库代码不含 f-string / `typing` 导入 / 3.5+ 语法 (CI 有语法守卫测试)
|
|
233
|
+
|
|
234
|
+
## 开发
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
uv run pytest # mock 单元测试 + 真机冒烟 (无设备自动跳过)
|
|
238
|
+
uv run ruff check . # lint
|
|
239
|
+
uv run mypy # 类型检查 (PEP 484 标注)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
dev 工具由 `[dependency-groups]` 提供 (pytest / ruff / mypy), `uv run` 自动同步。
|
|
243
|
+
|
|
244
|
+
打包使用 uv 自家后端 (`uv_build`), 无需额外装 hatchling:
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
uv build # 生成 dist/*.whl 与 dist/*.tar.gz
|
|
248
|
+
uv build --wheel # 只出 wheel
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
产物校验: wheel 含 `pyzk20r/` 全部模块 + `py.typed` + `dist-info/licenses/LICENSE`;
|
|
252
|
+
sdist 额外含 `tests/` 与 `examples/`(指纹数据 `*.db` 自动排除)。
|
|
253
|
+
|
|
254
|
+
SDK 文档 (中英文 PDF + 头文件 + MFC Demo) 从 ZKTeco 官网 / "ZKFinger SDK" 安装包获取;
|
|
255
|
+
本库的接口注释即按其 C API 编写。
|
|
256
|
+
|
|
257
|
+
## 发版 (PyPI)
|
|
258
|
+
|
|
259
|
+
本库**仅支持 Windows**, 且**只通过 tag 推送发版**:
|
|
260
|
+
推送 `vX.Y.Z` tag 后, Actions 自动执行 测试 (Windows) → 构建并校验 tag 与版本一致 → 发布 PyPI。
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
git tag v0.1.0
|
|
264
|
+
git push origin v0.1.0 # 触发 release.yml
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
前置 (一次性, 二选一):
|
|
268
|
+
- **推荐**: 在 pypi.org 的 pyzk20r 项目页配置 Trusted Publisher
|
|
269
|
+
(`owner=yunluo`, `repo=pyzk_20r`, `workflow=release.yml`, `environment=pypi`)
|
|
270
|
+
- 或在 GitHub 仓库 Settings → Secrets 添加 `PYPI_API_TOKEN`,
|
|
271
|
+
并按 `release.yml` 内注释把发布 step 改为 token 方式
|
|
272
|
+
|
|
273
|
+
## License
|
|
274
|
+
|
|
275
|
+
MIT (见 LICENSE)。ZKFinger SDK 及 `libzkfp.dll` 版权归 ZKTeco 所有, 需随官方驱动获取, 仅供配合其设备使用。
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""采集一帧指纹并保存为 BMP 图片。
|
|
3
|
+
|
|
4
|
+
用法: python capture.py [输出路径.bmp]
|
|
5
|
+
"""
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from pyzk20r import Scanner
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main():
|
|
12
|
+
out = sys.argv[1] if len(sys.argv) > 1 else "fingerprint.bmp"
|
|
13
|
+
with Scanner() as dev:
|
|
14
|
+
print("设备: %s %s 序列号=%s" % (dev.info().vendor, dev.info().product, dev.info().serial))
|
|
15
|
+
print("等待手指... (图像 %dx%d @%d DPI, 防假=%s)" % (
|
|
16
|
+
dev.width, dev.height, dev.dpi, dev.fake_detect))
|
|
17
|
+
for frame in dev.capture(poll_interval=0.1):
|
|
18
|
+
with open(out, "wb") as f:
|
|
19
|
+
f.write(frame.to_bmp())
|
|
20
|
+
print("已保存 %s (图像 %d 字节, 模板 %d 字节)"
|
|
21
|
+
% (out, len(frame.image), len(frame.template)))
|
|
22
|
+
break
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if __name__ == "__main__":
|
|
26
|
+
main()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""登记 + 1:1 验证演示。
|
|
3
|
+
|
|
4
|
+
给工号 1001 连续登记两枚指纹, 然后现场采集并用 1:1 验证身份。
|
|
5
|
+
"""
|
|
6
|
+
from pyzk20r import FingerprintDB, MatchError, Scanner
|
|
7
|
+
|
|
8
|
+
USER = "1001"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main():
|
|
12
|
+
db = FingerprintDB()
|
|
13
|
+
with Scanner() as dev:
|
|
14
|
+
dev.fake_detect = True
|
|
15
|
+
for i in range(2):
|
|
16
|
+
print("[%d/2] 请按压手指 3 次..." % (i + 1))
|
|
17
|
+
tpl = db.enroll(dev, USER)
|
|
18
|
+
print(" 登记完成, 登记模板 %d 字节" % len(tpl))
|
|
19
|
+
|
|
20
|
+
print("请再次按压手指进行 1:1 验证...")
|
|
21
|
+
try:
|
|
22
|
+
score = db.verify(USER, dev.acquire().template)
|
|
23
|
+
print("验证通过, 分数 %d (阈值 %d)" % (score, db.threshold_1_1))
|
|
24
|
+
except MatchError as e:
|
|
25
|
+
print("验证失败: %s" % e)
|
|
26
|
+
db.close()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
main()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""多人登记 + 1:N 识别演示 (一人多枚指纹)。
|
|
3
|
+
|
|
4
|
+
登记 3 个用户后进入识别循环, 按 Ctrl+C 退出。
|
|
5
|
+
"""
|
|
6
|
+
from pyzk20r import FingerprintDB, MatchError, Scanner
|
|
7
|
+
|
|
8
|
+
USERS = ["1001", "1002", "1003"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main():
|
|
12
|
+
db = FingerprintDB()
|
|
13
|
+
with Scanner() as dev:
|
|
14
|
+
for uid in USERS:
|
|
15
|
+
print("工号 %s: 请按压手指 3 次..." % uid)
|
|
16
|
+
db.enroll(dev, uid)
|
|
17
|
+
print(" 已登记 (当前模板总数 %d)" % db.count)
|
|
18
|
+
|
|
19
|
+
print("进入识别循环, Ctrl+C 退出")
|
|
20
|
+
try:
|
|
21
|
+
for frame in dev.capture(poll_interval=0.1):
|
|
22
|
+
try:
|
|
23
|
+
uid, score = db.identify(frame.template)
|
|
24
|
+
print("识别成功: 工号 %s, 分数 %d (阈值 %d)" % (uid, score, db.threshold_1_n))
|
|
25
|
+
except MatchError:
|
|
26
|
+
print("未识别 (无匹配)")
|
|
27
|
+
except KeyboardInterrupt:
|
|
28
|
+
print("\n退出")
|
|
29
|
+
db.close()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if __name__ == "__main__":
|
|
33
|
+
main()
|