train-invoice-organizer 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.
- train_invoice_organizer-0.1.0/LICENSE +21 -0
- train_invoice_organizer-0.1.0/PKG-INFO +114 -0
- train_invoice_organizer-0.1.0/README.md +83 -0
- train_invoice_organizer-0.1.0/pyproject.toml +53 -0
- train_invoice_organizer-0.1.0/setup.cfg +4 -0
- train_invoice_organizer-0.1.0/src/train_invoice_organizer/__init__.py +9 -0
- train_invoice_organizer-0.1.0/src/train_invoice_organizer/cli.py +57 -0
- train_invoice_organizer-0.1.0/src/train_invoice_organizer/core.py +193 -0
- train_invoice_organizer-0.1.0/src/train_invoice_organizer/gui.py +86 -0
- train_invoice_organizer-0.1.0/src/train_invoice_organizer.egg-info/PKG-INFO +114 -0
- train_invoice_organizer-0.1.0/src/train_invoice_organizer.egg-info/SOURCES.txt +13 -0
- train_invoice_organizer-0.1.0/src/train_invoice_organizer.egg-info/dependency_links.txt +1 -0
- train_invoice_organizer-0.1.0/src/train_invoice_organizer.egg-info/entry_points.txt +3 -0
- train_invoice_organizer-0.1.0/src/train_invoice_organizer.egg-info/requires.txt +6 -0
- train_invoice_organizer-0.1.0/src/train_invoice_organizer.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 train-invoice-organizer 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,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: train-invoice-organizer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 自动提取铁路电子发票 ZIP 包中的 PDF,按乘车日期、时间、票价重命名整理
|
|
5
|
+
Author-email: Violin True <ncunxi@sina.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://gitee.com/chuwanglin/train-invoice-organizer
|
|
8
|
+
Project-URL: Repository, https://gitee.com/chuwanglin/train-invoice-organizer
|
|
9
|
+
Keywords: train,invoice,railway,pdf,ticket,rename,organizer
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: PyMuPDF>=1.23.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
28
|
+
Requires-Dist: build>=0.10; extra == "dev"
|
|
29
|
+
Requires-Dist: twine>=4.0; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# train-invoice-organizer
|
|
33
|
+
|
|
34
|
+
自动提取铁路电子发票 ZIP 包中的 PDF,按**乘车日期**、**时间**、**票价**重命名整理。
|
|
35
|
+
|
|
36
|
+
## 功能
|
|
37
|
+
|
|
38
|
+
- 递归解压嵌套的 ZIP 包(F 文件 → 内部 ZIP → PDF 发票)
|
|
39
|
+
- 用 PyMuPDF 从 PDF 中智能提取乘车日期、发车时间和票价
|
|
40
|
+
- 自动按 `YYYYMMDD_HHMM_价格.pdf` 格式重命名
|
|
41
|
+
- 遇到同名文件自动添加序号后缀
|
|
42
|
+
- 支持命令行一键运行,也支持作为 Python 库导入调用
|
|
43
|
+
|
|
44
|
+
## 安装
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install train-invoice-organizer
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 使用方式
|
|
51
|
+
|
|
52
|
+
### 1. 命令行(安装后直接使用)
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# 处理当前目录下的所有 ZIP 文件
|
|
56
|
+
train-invoice-organizer
|
|
57
|
+
|
|
58
|
+
# 指定目录
|
|
59
|
+
train-invoice-organizer /path/to/your/zip/files
|
|
60
|
+
|
|
61
|
+
# 指定输出目录
|
|
62
|
+
train-invoice-organizer /path/to/zip/files -o /path/to/output
|
|
63
|
+
|
|
64
|
+
# 显示详细日志
|
|
65
|
+
train-invoice-organizer /path/to/zip/files -v
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 2. macOS 原生图形界面(推荐 Mac 用户)
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# 弹出系统原生目录选择框,处理完成后自动打开 Finder
|
|
72
|
+
train-invoice-gui
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 3. 作为 Python 库导入
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from train_invoice_organizer import organize_invoices, extract_ticket_info
|
|
79
|
+
|
|
80
|
+
# 批量整理整个目录
|
|
81
|
+
total = organize_invoices("/path/to/zip/files")
|
|
82
|
+
print(f"共提取 {total} 张发票")
|
|
83
|
+
|
|
84
|
+
# 或单独解析单张 PDF 信息
|
|
85
|
+
info = extract_ticket_info("/path/to/single_ticket.pdf")
|
|
86
|
+
print(info) # 例如: 20260807_0845_104
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## 提取规则
|
|
90
|
+
|
|
91
|
+
| 字段 | 提取策略 |
|
|
92
|
+
|------|---------|
|
|
93
|
+
| **乘车日期** | 优先匹配 "日期+时间" 连续出现格式;若 PDF 排版导致分离,则先剔除 "开票日期" 干扰项,再匹配剩余日期 |
|
|
94
|
+
| **时间** | 匹配 `HH:MM` 格式 |
|
|
95
|
+
| **票价** | 优先匹配 `¥/¥` 后的整数;备用匹配 `(小写)104.00` 格式 |
|
|
96
|
+
|
|
97
|
+
## 输出示例
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
已命名的火车发票汇总/
|
|
101
|
+
├── 20260807_0845_104.pdf
|
|
102
|
+
├── 20260808_1430_156.pdf
|
|
103
|
+
├── 20260810_0920_89_1.pdf # 重名时自动加序号
|
|
104
|
+
└── 20260810_0920_89_2.pdf
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## 依赖
|
|
108
|
+
|
|
109
|
+
- Python >= 3.8
|
|
110
|
+
- [PyMuPDF](https://pypi.org/project/PyMuPDF/) (fitz)
|
|
111
|
+
|
|
112
|
+
## 开源协议
|
|
113
|
+
|
|
114
|
+
MIT License
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# train-invoice-organizer
|
|
2
|
+
|
|
3
|
+
自动提取铁路电子发票 ZIP 包中的 PDF,按**乘车日期**、**时间**、**票价**重命名整理。
|
|
4
|
+
|
|
5
|
+
## 功能
|
|
6
|
+
|
|
7
|
+
- 递归解压嵌套的 ZIP 包(F 文件 → 内部 ZIP → PDF 发票)
|
|
8
|
+
- 用 PyMuPDF 从 PDF 中智能提取乘车日期、发车时间和票价
|
|
9
|
+
- 自动按 `YYYYMMDD_HHMM_价格.pdf` 格式重命名
|
|
10
|
+
- 遇到同名文件自动添加序号后缀
|
|
11
|
+
- 支持命令行一键运行,也支持作为 Python 库导入调用
|
|
12
|
+
|
|
13
|
+
## 安装
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install train-invoice-organizer
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 使用方式
|
|
20
|
+
|
|
21
|
+
### 1. 命令行(安装后直接使用)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# 处理当前目录下的所有 ZIP 文件
|
|
25
|
+
train-invoice-organizer
|
|
26
|
+
|
|
27
|
+
# 指定目录
|
|
28
|
+
train-invoice-organizer /path/to/your/zip/files
|
|
29
|
+
|
|
30
|
+
# 指定输出目录
|
|
31
|
+
train-invoice-organizer /path/to/zip/files -o /path/to/output
|
|
32
|
+
|
|
33
|
+
# 显示详细日志
|
|
34
|
+
train-invoice-organizer /path/to/zip/files -v
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. macOS 原生图形界面(推荐 Mac 用户)
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# 弹出系统原生目录选择框,处理完成后自动打开 Finder
|
|
41
|
+
train-invoice-gui
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 3. 作为 Python 库导入
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from train_invoice_organizer import organize_invoices, extract_ticket_info
|
|
48
|
+
|
|
49
|
+
# 批量整理整个目录
|
|
50
|
+
total = organize_invoices("/path/to/zip/files")
|
|
51
|
+
print(f"共提取 {total} 张发票")
|
|
52
|
+
|
|
53
|
+
# 或单独解析单张 PDF 信息
|
|
54
|
+
info = extract_ticket_info("/path/to/single_ticket.pdf")
|
|
55
|
+
print(info) # 例如: 20260807_0845_104
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 提取规则
|
|
59
|
+
|
|
60
|
+
| 字段 | 提取策略 |
|
|
61
|
+
|------|---------|
|
|
62
|
+
| **乘车日期** | 优先匹配 "日期+时间" 连续出现格式;若 PDF 排版导致分离,则先剔除 "开票日期" 干扰项,再匹配剩余日期 |
|
|
63
|
+
| **时间** | 匹配 `HH:MM` 格式 |
|
|
64
|
+
| **票价** | 优先匹配 `¥/¥` 后的整数;备用匹配 `(小写)104.00` 格式 |
|
|
65
|
+
|
|
66
|
+
## 输出示例
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
已命名的火车发票汇总/
|
|
70
|
+
├── 20260807_0845_104.pdf
|
|
71
|
+
├── 20260808_1430_156.pdf
|
|
72
|
+
├── 20260810_0920_89_1.pdf # 重名时自动加序号
|
|
73
|
+
└── 20260810_0920_89_2.pdf
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## 依赖
|
|
77
|
+
|
|
78
|
+
- Python >= 3.8
|
|
79
|
+
- [PyMuPDF](https://pypi.org/project/PyMuPDF/) (fitz)
|
|
80
|
+
|
|
81
|
+
## 开源协议
|
|
82
|
+
|
|
83
|
+
MIT License
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "train-invoice-organizer"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "自动提取铁路电子发票 ZIP 包中的 PDF,按乘车日期、时间、票价重命名整理"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "Violin True", email = "ncunxi@sina.com"}
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.8"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: End Users/Desktop",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.8",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: Office/Business :: Financial",
|
|
27
|
+
"Topic :: Utilities",
|
|
28
|
+
]
|
|
29
|
+
keywords = ["train", "invoice", "railway", "pdf", "ticket", "rename", "organizer"]
|
|
30
|
+
dependencies = [
|
|
31
|
+
"PyMuPDF>=1.23.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
dev = [
|
|
36
|
+
"pytest>=7.0",
|
|
37
|
+
"build>=0.10",
|
|
38
|
+
"twine>=4.0",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.scripts]
|
|
42
|
+
train-invoice-organizer = "train_invoice_organizer.cli:main"
|
|
43
|
+
train-invoice-gui = "train_invoice_organizer.gui:main"
|
|
44
|
+
|
|
45
|
+
[project.urls]
|
|
46
|
+
Homepage = "https://gitee.com/chuwanglin/train-invoice-organizer"
|
|
47
|
+
Repository = "https://gitee.com/chuwanglin/train-invoice-organizer"
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.packages.find]
|
|
50
|
+
where = ["src"]
|
|
51
|
+
|
|
52
|
+
[tool.setuptools.package-dir]
|
|
53
|
+
"" = "src"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""
|
|
2
|
+
train-invoice-organizer
|
|
3
|
+
自动提取铁路电子发票 ZIP 包中的 PDF,按乘车日期、时间、票价重命名整理。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .core import extract_ticket_info, process_single_f_file, organize_invoices
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
__all__ = ["extract_ticket_info", "process_single_f_file", "organize_invoices"]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
命令行入口。安装库后可直接运行:
|
|
3
|
+
train-invoice-organizer /path/to/zip/files
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from .core import organize_invoices
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main() -> int:
|
|
15
|
+
parser = argparse.ArgumentParser(
|
|
16
|
+
description="自动提取铁路电子发票 ZIP 包中的 PDF,按乘车日期、时间、票价重命名整理。"
|
|
17
|
+
)
|
|
18
|
+
parser.add_argument(
|
|
19
|
+
"input_dir",
|
|
20
|
+
nargs="?",
|
|
21
|
+
default=os.getcwd(),
|
|
22
|
+
help="包含 ZIP 文件的目录路径(默认为当前目录)",
|
|
23
|
+
)
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"-o",
|
|
26
|
+
"--output",
|
|
27
|
+
dest="output_dir",
|
|
28
|
+
default=None,
|
|
29
|
+
help="指定输出目录(默认为 input_dir/已命名的火车发票汇总)",
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"-v",
|
|
33
|
+
"--verbose",
|
|
34
|
+
action="store_true",
|
|
35
|
+
help="显示详细日志",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
args = parser.parse_args()
|
|
39
|
+
|
|
40
|
+
log_level = logging.DEBUG if args.verbose else logging.INFO
|
|
41
|
+
logging.basicConfig(
|
|
42
|
+
level=log_level,
|
|
43
|
+
format="%(levelname)s: %(message)s",
|
|
44
|
+
stream=sys.stdout,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
organize_invoices(args.input_dir, args.output_dir)
|
|
49
|
+
except FileNotFoundError as exc:
|
|
50
|
+
logging.error(exc)
|
|
51
|
+
return 1
|
|
52
|
+
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""
|
|
2
|
+
核心功能:从 ZIP 嵌套包中提取铁路电子发票 PDF,
|
|
3
|
+
按乘车日期、时间、票价重命名整理。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
import tempfile
|
|
11
|
+
import zipfile
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
import fitz
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def extract_ticket_info(pdf_path: str | Path) -> str:
|
|
21
|
+
"""
|
|
22
|
+
从铁路电子发票 PDF 中提取乘车日期、时间和票价,
|
|
23
|
+
返回格式为:YYYYMMDD_HHMM_价格 的字符串。
|
|
24
|
+
|
|
25
|
+
提取策略:
|
|
26
|
+
1. 优先匹配 "日期+时间" 连续出现的格式(乘车日期 T2)
|
|
27
|
+
2. 若排版导致换行,则先剔除 "开票日期" 干扰,再匹配日期
|
|
28
|
+
3. 票价优先匹配 ¥ 符号后的整数,备用匹配 "(小写)" 格式
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
pdf_path: PDF 文件路径。
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
组合后的文件名主体,如 "20260807_0845_104"。
|
|
35
|
+
若某字段提取失败,会用 "未知日期"/"未知时间"/"未知票价" 占位。
|
|
36
|
+
"""
|
|
37
|
+
date_str = "未知日期"
|
|
38
|
+
time_str = "未知时间"
|
|
39
|
+
price_str = "未知票价"
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
doc = fitz.open(str(pdf_path))
|
|
43
|
+
if len(doc) > 0:
|
|
44
|
+
page = doc[0]
|
|
45
|
+
text = page.get_text()
|
|
46
|
+
|
|
47
|
+
# 策略1: 直接匹配 "日期紧接着时间"(乘车日期 T2)
|
|
48
|
+
datetime_match = re.search(
|
|
49
|
+
r"(\d{4})\s*[年-]\s*(\d{1,2})\s*[月-]\s*(\d{1,2})\s*日?\s*(\d{2})\s*:\s*(\d{2})",
|
|
50
|
+
text,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if datetime_match:
|
|
54
|
+
year, month, day, hour, minute = datetime_match.groups()
|
|
55
|
+
date_str = f"{year}{month.zfill(2)}{day.zfill(2)}"
|
|
56
|
+
time_str = f"{hour}{minute}"
|
|
57
|
+
else:
|
|
58
|
+
# 策略2: 日期和时间被排版分离的情况
|
|
59
|
+
time_match = re.search(r"(\d{2})\s*:\s*(\d{2})", text)
|
|
60
|
+
if time_match:
|
|
61
|
+
hour, minute = time_match.groups()
|
|
62
|
+
time_str = f"{hour}{minute}"
|
|
63
|
+
|
|
64
|
+
# 剔除 "开票日期" 所在的 T1,避免干扰乘车日期 T2
|
|
65
|
+
text_without_invoice_date = re.sub(
|
|
66
|
+
r"开票日期\s*[::]?\s*\d{4}\s*[年-]\s*\d{1,2}\s*[月-]\s*\d{1,2}\s*日?",
|
|
67
|
+
"",
|
|
68
|
+
text,
|
|
69
|
+
)
|
|
70
|
+
date_match = re.search(
|
|
71
|
+
r"(\d{4})\s*[年-]\s*(\d{1,2})\s*[月-]\s*(\d{1,2})\s*日?",
|
|
72
|
+
text_without_invoice_date,
|
|
73
|
+
)
|
|
74
|
+
if date_match:
|
|
75
|
+
year, month, day = date_match.groups()
|
|
76
|
+
date_str = f"{year}{month.zfill(2)}{day.zfill(2)}"
|
|
77
|
+
|
|
78
|
+
# 提取票价(整数部分)
|
|
79
|
+
# 优先匹配 ¥/¥ 符号后的数字
|
|
80
|
+
price_match = re.search(r"[¥¥]\s*(\d+)", text)
|
|
81
|
+
if not price_match:
|
|
82
|
+
# 备用:匹配 "(小写)104.00" 格式
|
|
83
|
+
price_match = re.search(r"小写.*?(\d+)\.\d{2}", text)
|
|
84
|
+
|
|
85
|
+
if price_match:
|
|
86
|
+
price_str = price_match.group(1)
|
|
87
|
+
|
|
88
|
+
except Exception as exc:
|
|
89
|
+
logger.warning("无法读取 PDF %s: %s", os.path.basename(str(pdf_path)), exc)
|
|
90
|
+
finally:
|
|
91
|
+
if "doc" in locals():
|
|
92
|
+
doc.close()
|
|
93
|
+
|
|
94
|
+
return f"{date_str}_{time_str}_{price_str}"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def process_single_f_file(f_zip_path: str | Path, output_dir: str | Path) -> int:
|
|
98
|
+
"""
|
|
99
|
+
处理单个 F 文件(主 ZIP 包)。
|
|
100
|
+
|
|
101
|
+
解压主 ZIP 后,递归查找内部嵌套的 ZIP 包,
|
|
102
|
+
从中提取 PDF 发票,解析信息后重命名并存入输出目录。
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
f_zip_path: 主 ZIP 文件路径。
|
|
106
|
+
output_dir: 提取后的 PDF 存放目录。
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
成功提取的 PDF 数量。
|
|
110
|
+
"""
|
|
111
|
+
output_dir = Path(output_dir)
|
|
112
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
113
|
+
extracted_count = 0
|
|
114
|
+
|
|
115
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
116
|
+
logger.info("正在解压并分析: %s", os.path.basename(str(f_zip_path)))
|
|
117
|
+
try:
|
|
118
|
+
with zipfile.ZipFile(f_zip_path, "r") as zip_ref:
|
|
119
|
+
zip_ref.extractall(temp_dir)
|
|
120
|
+
except zipfile.BadZipFile:
|
|
121
|
+
logger.warning("[跳过] %s 不是有效的 ZIP 包。", os.path.basename(str(f_zip_path)))
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
temp_path = Path(temp_dir)
|
|
125
|
+
for inner_zip in temp_path.rglob("*.zip"):
|
|
126
|
+
extract_subdir = inner_zip.with_suffix(inner_zip.suffix + "_extracted")
|
|
127
|
+
extract_subdir.mkdir(exist_ok=True)
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
with zipfile.ZipFile(inner_zip, "r") as zf:
|
|
131
|
+
zf.extractall(extract_subdir)
|
|
132
|
+
except zipfile.BadZipFile:
|
|
133
|
+
logger.debug("跳过无效嵌套 ZIP: %s", inner_zip.name)
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
for pdf_file in extract_subdir.rglob("*.pdf"):
|
|
137
|
+
base_name = extract_ticket_info(pdf_file)
|
|
138
|
+
new_filename = f"{base_name}.pdf"
|
|
139
|
+
dest_path = output_dir / new_filename
|
|
140
|
+
|
|
141
|
+
# 处理重名:自动添加递增序号后缀
|
|
142
|
+
counter = 1
|
|
143
|
+
while dest_path.exists():
|
|
144
|
+
new_filename = f"{base_name}_{counter}.pdf"
|
|
145
|
+
dest_path = output_dir / new_filename
|
|
146
|
+
counter += 1
|
|
147
|
+
|
|
148
|
+
shutil.copy2(pdf_file, dest_path)
|
|
149
|
+
logger.info("[成功] 提取并重命名: %s", new_filename)
|
|
150
|
+
extracted_count += 1
|
|
151
|
+
|
|
152
|
+
return extracted_count
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def organize_invoices(
|
|
156
|
+
input_dir: str | Path,
|
|
157
|
+
output_dir: Optional[str | Path] = None,
|
|
158
|
+
) -> int:
|
|
159
|
+
"""
|
|
160
|
+
批量整理指定目录下的所有铁路电子发票 ZIP 包。
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
input_dir: 包含 ZIP 文件的目录路径。
|
|
164
|
+
output_dir: 输出目录路径。默认为 input_dir/已命名的火车发票汇总。
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
成功提取的 PDF 总数量。
|
|
168
|
+
"""
|
|
169
|
+
input_dir = Path(input_dir)
|
|
170
|
+
if not input_dir.exists():
|
|
171
|
+
raise FileNotFoundError(f"输入目录不存在: {input_dir}")
|
|
172
|
+
|
|
173
|
+
if output_dir is None:
|
|
174
|
+
output_dir = input_dir / "已命名的火车发票汇总"
|
|
175
|
+
else:
|
|
176
|
+
output_dir = Path(output_dir)
|
|
177
|
+
|
|
178
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
179
|
+
|
|
180
|
+
zip_files = sorted([f for f in input_dir.iterdir() if f.suffix.lower() == ".zip"])
|
|
181
|
+
|
|
182
|
+
if not zip_files:
|
|
183
|
+
logger.warning("在 %s 下没有找到任何 .zip 文件!", input_dir)
|
|
184
|
+
return 0
|
|
185
|
+
|
|
186
|
+
logger.info("共找到 %d 个 ZIP 文件,开始处理...", len(zip_files))
|
|
187
|
+
|
|
188
|
+
total = 0
|
|
189
|
+
for zip_file in zip_files:
|
|
190
|
+
total += process_single_f_file(zip_file, output_dir)
|
|
191
|
+
|
|
192
|
+
logger.info("处理完成!共提取 %d 张发票,存放于: %s", total, output_dir)
|
|
193
|
+
return total
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
macOS 原生图形交互入口。
|
|
3
|
+
|
|
4
|
+
安装库后,macOS 用户可直接运行:
|
|
5
|
+
train-invoice-gui
|
|
6
|
+
|
|
7
|
+
它会弹出系统原生的目录选择框,处理完成后自动在 Finder 中打开结果文件夹。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
from .core import organize_invoices
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def select_directory_mac() -> str:
|
|
21
|
+
"""使用 Mac 原生的 AppleScript 调出目录选择框。"""
|
|
22
|
+
apple_script = """
|
|
23
|
+
try
|
|
24
|
+
tell application "SystemUIServer"
|
|
25
|
+
activate
|
|
26
|
+
set myFolder to choose folder with prompt "请选择包含火车票发票 ZIP 文件的目录:"
|
|
27
|
+
end tell
|
|
28
|
+
return POSIX path of myFolder
|
|
29
|
+
on error
|
|
30
|
+
return ""
|
|
31
|
+
end try
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
result = subprocess.run(
|
|
35
|
+
["osascript", "-e", apple_script],
|
|
36
|
+
capture_output=True,
|
|
37
|
+
text=True,
|
|
38
|
+
check=True,
|
|
39
|
+
)
|
|
40
|
+
return result.stdout.strip()
|
|
41
|
+
except Exception as exc:
|
|
42
|
+
logger.error("调出目录选择器失败: %s", exc)
|
|
43
|
+
return ""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def open_finder(path: str) -> None:
|
|
47
|
+
"""在 Finder 中打开指定路径。"""
|
|
48
|
+
try:
|
|
49
|
+
subprocess.run(["open", path], check=False)
|
|
50
|
+
except Exception as exc:
|
|
51
|
+
logger.warning("无法自动打开 Finder: %s", exc)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main() -> int:
|
|
55
|
+
"""macOS 图形交互入口。"""
|
|
56
|
+
logging.basicConfig(
|
|
57
|
+
level=logging.INFO,
|
|
58
|
+
format="%(levelname)s: %(message)s",
|
|
59
|
+
stream=sys.stdout,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
print("正在启动目录选择器...")
|
|
63
|
+
selected_dir = select_directory_mac()
|
|
64
|
+
|
|
65
|
+
if not selected_dir:
|
|
66
|
+
print("\n已取消选择。")
|
|
67
|
+
return 1
|
|
68
|
+
|
|
69
|
+
print(f"\n您选择的目录是: {selected_dir}")
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
organize_invoices(selected_dir)
|
|
73
|
+
except FileNotFoundError as exc:
|
|
74
|
+
logger.error(exc)
|
|
75
|
+
return 1
|
|
76
|
+
|
|
77
|
+
# 处理完成后自动打开 Finder
|
|
78
|
+
output_dir = os.path.join(selected_dir, "已命名的火车发票汇总")
|
|
79
|
+
if os.path.exists(output_dir):
|
|
80
|
+
open_finder(output_dir)
|
|
81
|
+
|
|
82
|
+
return 0
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: train-invoice-organizer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 自动提取铁路电子发票 ZIP 包中的 PDF,按乘车日期、时间、票价重命名整理
|
|
5
|
+
Author-email: Violin True <ncunxi@sina.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://gitee.com/chuwanglin/train-invoice-organizer
|
|
8
|
+
Project-URL: Repository, https://gitee.com/chuwanglin/train-invoice-organizer
|
|
9
|
+
Keywords: train,invoice,railway,pdf,ticket,rename,organizer
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: PyMuPDF>=1.23.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
28
|
+
Requires-Dist: build>=0.10; extra == "dev"
|
|
29
|
+
Requires-Dist: twine>=4.0; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# train-invoice-organizer
|
|
33
|
+
|
|
34
|
+
自动提取铁路电子发票 ZIP 包中的 PDF,按**乘车日期**、**时间**、**票价**重命名整理。
|
|
35
|
+
|
|
36
|
+
## 功能
|
|
37
|
+
|
|
38
|
+
- 递归解压嵌套的 ZIP 包(F 文件 → 内部 ZIP → PDF 发票)
|
|
39
|
+
- 用 PyMuPDF 从 PDF 中智能提取乘车日期、发车时间和票价
|
|
40
|
+
- 自动按 `YYYYMMDD_HHMM_价格.pdf` 格式重命名
|
|
41
|
+
- 遇到同名文件自动添加序号后缀
|
|
42
|
+
- 支持命令行一键运行,也支持作为 Python 库导入调用
|
|
43
|
+
|
|
44
|
+
## 安装
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install train-invoice-organizer
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 使用方式
|
|
51
|
+
|
|
52
|
+
### 1. 命令行(安装后直接使用)
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# 处理当前目录下的所有 ZIP 文件
|
|
56
|
+
train-invoice-organizer
|
|
57
|
+
|
|
58
|
+
# 指定目录
|
|
59
|
+
train-invoice-organizer /path/to/your/zip/files
|
|
60
|
+
|
|
61
|
+
# 指定输出目录
|
|
62
|
+
train-invoice-organizer /path/to/zip/files -o /path/to/output
|
|
63
|
+
|
|
64
|
+
# 显示详细日志
|
|
65
|
+
train-invoice-organizer /path/to/zip/files -v
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 2. macOS 原生图形界面(推荐 Mac 用户)
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# 弹出系统原生目录选择框,处理完成后自动打开 Finder
|
|
72
|
+
train-invoice-gui
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 3. 作为 Python 库导入
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from train_invoice_organizer import organize_invoices, extract_ticket_info
|
|
79
|
+
|
|
80
|
+
# 批量整理整个目录
|
|
81
|
+
total = organize_invoices("/path/to/zip/files")
|
|
82
|
+
print(f"共提取 {total} 张发票")
|
|
83
|
+
|
|
84
|
+
# 或单独解析单张 PDF 信息
|
|
85
|
+
info = extract_ticket_info("/path/to/single_ticket.pdf")
|
|
86
|
+
print(info) # 例如: 20260807_0845_104
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## 提取规则
|
|
90
|
+
|
|
91
|
+
| 字段 | 提取策略 |
|
|
92
|
+
|------|---------|
|
|
93
|
+
| **乘车日期** | 优先匹配 "日期+时间" 连续出现格式;若 PDF 排版导致分离,则先剔除 "开票日期" 干扰项,再匹配剩余日期 |
|
|
94
|
+
| **时间** | 匹配 `HH:MM` 格式 |
|
|
95
|
+
| **票价** | 优先匹配 `¥/¥` 后的整数;备用匹配 `(小写)104.00` 格式 |
|
|
96
|
+
|
|
97
|
+
## 输出示例
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
已命名的火车发票汇总/
|
|
101
|
+
├── 20260807_0845_104.pdf
|
|
102
|
+
├── 20260808_1430_156.pdf
|
|
103
|
+
├── 20260810_0920_89_1.pdf # 重名时自动加序号
|
|
104
|
+
└── 20260810_0920_89_2.pdf
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## 依赖
|
|
108
|
+
|
|
109
|
+
- Python >= 3.8
|
|
110
|
+
- [PyMuPDF](https://pypi.org/project/PyMuPDF/) (fitz)
|
|
111
|
+
|
|
112
|
+
## 开源协议
|
|
113
|
+
|
|
114
|
+
MIT License
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/train_invoice_organizer/__init__.py
|
|
5
|
+
src/train_invoice_organizer/cli.py
|
|
6
|
+
src/train_invoice_organizer/core.py
|
|
7
|
+
src/train_invoice_organizer/gui.py
|
|
8
|
+
src/train_invoice_organizer.egg-info/PKG-INFO
|
|
9
|
+
src/train_invoice_organizer.egg-info/SOURCES.txt
|
|
10
|
+
src/train_invoice_organizer.egg-info/dependency_links.txt
|
|
11
|
+
src/train_invoice_organizer.egg-info/entry_points.txt
|
|
12
|
+
src/train_invoice_organizer.egg-info/requires.txt
|
|
13
|
+
src/train_invoice_organizer.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
train_invoice_organizer
|