pyxllib 0.3.197__py3-none-any.whl → 0.3.200__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.
Files changed (126) hide show
  1. pyxllib/__init__.py +21 -21
  2. pyxllib/algo/__init__.py +8 -8
  3. pyxllib/algo/disjoint.py +54 -54
  4. pyxllib/algo/geo.py +541 -541
  5. pyxllib/algo/intervals.py +964 -964
  6. pyxllib/algo/matcher.py +389 -389
  7. pyxllib/algo/newbie.py +166 -166
  8. pyxllib/algo/pupil.py +629 -629
  9. pyxllib/algo/shapelylib.py +67 -67
  10. pyxllib/algo/specialist.py +241 -241
  11. pyxllib/algo/stat.py +494 -494
  12. pyxllib/algo/treelib.py +149 -149
  13. pyxllib/algo/unitlib.py +66 -66
  14. pyxllib/autogui/__init__.py +5 -5
  15. pyxllib/autogui/activewin.py +246 -246
  16. pyxllib/autogui/all.py +9 -9
  17. pyxllib/autogui/autogui.py +852 -852
  18. pyxllib/autogui/uiautolib.py +362 -362
  19. pyxllib/autogui/virtualkey.py +102 -102
  20. pyxllib/autogui/wechat.py +827 -827
  21. pyxllib/autogui/wechat_msg.py +421 -421
  22. pyxllib/autogui/wxautolib.py +84 -84
  23. pyxllib/cv/__init__.py +5 -5
  24. pyxllib/cv/expert.py +267 -267
  25. pyxllib/cv/imfile.py +159 -159
  26. pyxllib/cv/imhash.py +39 -39
  27. pyxllib/cv/pupil.py +9 -9
  28. pyxllib/cv/rgbfmt.py +1525 -1525
  29. pyxllib/cv/slidercaptcha.py +137 -137
  30. pyxllib/cv/trackbartools.py +251 -251
  31. pyxllib/cv/xlcvlib.py +1040 -1040
  32. pyxllib/cv/xlpillib.py +423 -423
  33. pyxllib/data/echarts.py +240 -240
  34. pyxllib/data/jsonlib.py +89 -89
  35. pyxllib/data/oss.py +72 -72
  36. pyxllib/data/pglib.py +1127 -1127
  37. pyxllib/data/sqlite.py +568 -568
  38. pyxllib/data/sqllib.py +297 -297
  39. pyxllib/ext/JLineViewer.py +505 -505
  40. pyxllib/ext/__init__.py +6 -6
  41. pyxllib/ext/demolib.py +246 -246
  42. pyxllib/ext/drissionlib.py +277 -277
  43. pyxllib/ext/kq5034lib.py +12 -12
  44. pyxllib/ext/old.py +663 -663
  45. pyxllib/ext/qt.py +449 -449
  46. pyxllib/ext/robustprocfile.py +497 -497
  47. pyxllib/ext/seleniumlib.py +76 -76
  48. pyxllib/ext/tk.py +173 -173
  49. pyxllib/ext/unixlib.py +827 -827
  50. pyxllib/ext/utools.py +351 -351
  51. pyxllib/ext/webhook.py +124 -119
  52. pyxllib/ext/win32lib.py +40 -40
  53. pyxllib/ext/wjxlib.py +88 -88
  54. pyxllib/ext/wpsapi.py +124 -124
  55. pyxllib/ext/xlwork.py +9 -9
  56. pyxllib/ext/yuquelib.py +1105 -1105
  57. pyxllib/file/__init__.py +17 -17
  58. pyxllib/file/docxlib.py +761 -761
  59. pyxllib/file/gitlib.py +309 -309
  60. pyxllib/file/libreoffice.py +165 -165
  61. pyxllib/file/movielib.py +148 -148
  62. pyxllib/file/newbie.py +10 -10
  63. pyxllib/file/onenotelib.py +1469 -1469
  64. pyxllib/file/packlib/__init__.py +330 -330
  65. pyxllib/file/packlib/zipfile.py +2441 -2441
  66. pyxllib/file/pdflib.py +426 -426
  67. pyxllib/file/pupil.py +185 -185
  68. pyxllib/file/specialist/__init__.py +685 -685
  69. pyxllib/file/specialist/dirlib.py +799 -799
  70. pyxllib/file/specialist/download.py +193 -193
  71. pyxllib/file/specialist/filelib.py +2829 -2829
  72. pyxllib/file/xlsxlib.py +3131 -3131
  73. pyxllib/file/xlsyncfile.py +341 -341
  74. pyxllib/prog/__init__.py +5 -5
  75. pyxllib/prog/cachetools.py +64 -64
  76. pyxllib/prog/deprecatedlib.py +233 -233
  77. pyxllib/prog/filelock.py +42 -42
  78. pyxllib/prog/ipyexec.py +253 -253
  79. pyxllib/prog/multiprogs.py +940 -940
  80. pyxllib/prog/newbie.py +451 -451
  81. pyxllib/prog/pupil.py +1197 -1197
  82. pyxllib/prog/sitepackages.py +33 -33
  83. pyxllib/prog/specialist/__init__.py +391 -391
  84. pyxllib/prog/specialist/bc.py +203 -203
  85. pyxllib/prog/specialist/browser.py +497 -497
  86. pyxllib/prog/specialist/common.py +347 -347
  87. pyxllib/prog/specialist/datetime.py +198 -198
  88. pyxllib/prog/specialist/tictoc.py +240 -240
  89. pyxllib/prog/specialist/xllog.py +180 -180
  90. pyxllib/prog/xlosenv.py +108 -108
  91. pyxllib/stdlib/__init__.py +17 -17
  92. pyxllib/stdlib/tablepyxl/__init__.py +10 -10
  93. pyxllib/stdlib/tablepyxl/style.py +303 -303
  94. pyxllib/stdlib/tablepyxl/tablepyxl.py +130 -130
  95. pyxllib/text/__init__.py +8 -8
  96. pyxllib/text/ahocorasick.py +39 -39
  97. pyxllib/text/airscript.js +744 -744
  98. pyxllib/text/charclasslib.py +121 -121
  99. pyxllib/text/jiebalib.py +267 -267
  100. pyxllib/text/jinjalib.py +32 -32
  101. pyxllib/text/jsa_ai_prompt.md +271 -271
  102. pyxllib/text/jscode.py +922 -922
  103. pyxllib/text/latex/__init__.py +158 -158
  104. pyxllib/text/levenshtein.py +303 -303
  105. pyxllib/text/nestenv.py +1215 -1215
  106. pyxllib/text/newbie.py +300 -300
  107. pyxllib/text/pupil/__init__.py +8 -8
  108. pyxllib/text/pupil/common.py +1121 -1121
  109. pyxllib/text/pupil/xlalign.py +326 -326
  110. pyxllib/text/pycode.py +47 -47
  111. pyxllib/text/specialist/__init__.py +8 -8
  112. pyxllib/text/specialist/common.py +112 -112
  113. pyxllib/text/specialist/ptag.py +186 -186
  114. pyxllib/text/spellchecker.py +172 -172
  115. pyxllib/text/templates/echart_base.html +10 -10
  116. pyxllib/text/templates/highlight_code.html +16 -16
  117. pyxllib/text/templates/latex_editor.html +102 -102
  118. pyxllib/text/vbacode.py +17 -17
  119. pyxllib/text/xmllib.py +747 -747
  120. pyxllib/xl.py +42 -39
  121. pyxllib/xlcv.py +17 -17
  122. {pyxllib-0.3.197.dist-info → pyxllib-0.3.200.dist-info}/METADATA +1 -1
  123. pyxllib-0.3.200.dist-info/RECORD +126 -0
  124. {pyxllib-0.3.197.dist-info → pyxllib-0.3.200.dist-info}/licenses/LICENSE +190 -190
  125. pyxllib-0.3.197.dist-info/RECORD +0 -126
  126. {pyxllib-0.3.197.dist-info → pyxllib-0.3.200.dist-info}/WHEEL +0 -0
@@ -1,330 +1,330 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- # @Author : 陈坤泽
4
- # @Email : 877362867@qq.com
5
- # @Date : 2022/02/15 09:51
6
-
7
- import os
8
- import shutil
9
- from tarfile import TarFile
10
- # 这个包里的ZipFile是拷贝后修改过的,不影响标准库zipfile里的功能
11
- # import pyxllib.file.packlib.zipfile as zipfile
12
- # from pyxllib.file.packlib.zipfile import ZipFile
13
- import zipfile
14
- from zipfile import ZipFile
15
- import tempfile
16
-
17
- from pyxllib.file.specialist import XlPath, reduce_dir_depth
18
- from pyxllib.prog.pupil import inject_members
19
-
20
- """ 问题:py官方的ZipFile解压带中文的文件会乱码
21
-
22
- 解决办法及操作流程
23
- 1、定期将官方的zipfile.py文件更新到pyxllib.file.packlib.zipfile
24
- 2、将代码中两处cp437都替换为gbk
25
- """
26
-
27
-
28
- def unpack_zipfile(filename, extract_dir):
29
- """ 为了修复zipfile底层的中文解压乱码问题,修改了shutil._UNPACK_FORMATS的底层功能
30
- """
31
- from shutil import ReadError
32
-
33
- zip = zipfile.ZipFile(filename)
34
- if not zipfile.is_zipfile(filename):
35
- raise ReadError("%s is not a zip file" % filename)
36
- try:
37
- for info in zip.infolist():
38
- name = info.filename
39
-
40
- # don't extract absolute paths or ones with .. in them
41
- if name.startswith('/') or '..' in name:
42
- continue
43
-
44
- target = os.path.join(extract_dir, *name.split('/'))
45
- if not target:
46
- continue
47
-
48
- os.makedirs(XlPath(target).parent, exist_ok=True)
49
- if not name.endswith('/'):
50
- # file
51
- data = zip.read(info.filename)
52
- f = open(target, 'wb')
53
- try:
54
- f.write(data)
55
- finally:
56
- f.close()
57
- del data
58
- finally:
59
- zip.close()
60
-
61
-
62
- # 解决unzip中文乱码问题
63
- shutil._UNPACK_FORMATS['zip'] = (['.zip'], unpack_zipfile, [], "ZIP file")
64
-
65
-
66
- def _parse_depth(names):
67
- """ 判断有几层目录都只有独立的一个目录
68
-
69
- :param names: 一组相对路径名
70
- :return: 如果直接目录下文件已经不唯一,返回0
71
-
72
- >>> _parse_depth(['a'])
73
- 1
74
- >>> _parse_depth(['a', 'a/1'])
75
- 2
76
- >>> _parse_depth(['a', 'a/b', 'a/b/1', 'a/b/2'])
77
- 2
78
- >>> _parse_depth(['a', 'a/b', 'a/b/1', 'a/c/3'])
79
- 1
80
- """
81
- parents = []
82
- for name in names:
83
- p = XlPath(name)
84
- ps = [x.as_posix() for x in p.parents]
85
- parents.append(ps[-2::-1] + [p.as_posix()])
86
- i = 0
87
- while len({x[i] for x in parents if len(x) > i}) == 1:
88
- i += 1
89
- return i
90
-
91
-
92
- def _unpack_base(packfile, namelist, format=None, extract_dir=None, wrap=0):
93
- """ 解压
94
-
95
- :param packfile: 压缩包本身的文件路径名称,在wrap=1时可能会用到,用来添加一个同名目录
96
- :param namelist: 压缩包里所有文件的相对路径
97
- :param format: 压缩包类型,比如zip、tar,可以不输入,此时直接采用shutil.unpack_archive里的判断机制
98
- :param extract_dir: 解压目标目录,可以不输入,会自动判断
99
- 自动解压规则:压缩包里目录不唯一,则以压缩包本身的名称创建一个目录存放
100
- 否则以压缩包里的目录名为准
101
- :param wrap: 解压时倾向的处理机制
102
- 0,默认,不进行任何处理
103
- 1,如果压缩包里一级目录下不止一个对象,使用压缩包本身的名称再包一层
104
- -1,如果压缩包里一级目录下只有一个目录,则丢弃这个目录,只取里面的内容出来
105
- 同理,可能这一个目录里的文件,还是只有一个目录,此时支持设置-2、-3等解开上限
106
- 也就-1比较常用,-2、-3等很少见了
107
- TODO 还有一种解压机制,只取里面的部分文件,有空可以继续调研扩展下
108
- :return: 无论怎样,最后都是放到一个目录里,返回改目录路径
109
- """
110
-
111
- # 1 默认解压目录是self所在目录,注意这点跟shutil.unpack_archive的当前工作目录不同
112
- p0 = XlPath(packfile)
113
- if extract_dir is None:
114
- extract_dir = p0.parent
115
-
116
- depth = _parse_depth(namelist)
117
-
118
- # 2 是否要加目录层级,或者去掉目录层级
119
- if wrap == 1 and depth == 0:
120
- # 压缩包里直接目录下有多个文件,且指定了wrap,则加一层目录
121
- shutil.unpack_archive(packfile, extract_dir / p0.stem, format)
122
- elif wrap < 0 < depth:
123
- # 压缩包有depth层冗余目录,并且指定wrap要解开这几层目录
124
- # 先正常解压(每种压缩包格式内部处理机制都有点不太一样,所以先解压,然后用目录文件功能处理更合适)
125
- shutil.unpack_archive(packfile, extract_dir, format)
126
- reduce_dir_depth(extract_dir, unwrap=-wrap)
127
- else:
128
- # 正常解压
129
- shutil.unpack_archive(packfile, extract_dir, format)
130
-
131
-
132
- def zip_filter_rule(path):
133
- p = XlPath(path)
134
-
135
- # 排除特定目录或文件
136
- if set(p.parts) & {'__pycache__', '.git', '.idea', 'dist', 'build', '.pytest_cache', '.venv'}:
137
- return 0
138
-
139
- # 排除特定文件扩展名
140
- if p.suffix in {'.egg-info', '.lock'}:
141
- return 0
142
-
143
- # 精细检查内部文件
144
- if p.is_dir():
145
- return 1
146
-
147
- # 全部包含其他情况
148
- return 2
149
-
150
-
151
- class XlZipFile(ZipFile):
152
-
153
- def infolist2(self, prefix=None, zipinfo=True):
154
- """>> self.infolist2() # getinfo的多文件版本
155
- 1 <ZipInfo filename='[Content_Types].xml' compress_type=deflate file_size=1495 compress_size=383>
156
- 2 <ZipInfo filename='_rels/.rels' compress_type=deflate file_size=590 compress_size=243>
157
- ......
158
- 20 <ZipInfo filename='word/fontTable.xml' compress_type=deflate file_size=1590 compress_size=521>
159
- 21 <ZipInfo filename='docProps/app.xml' compress_type=deflate file_size=720 compress_size=384>
160
-
161
- :param prefix:
162
- 可以筛选文件的前缀,例如“word/”可以筛选出word目录下的
163
- :param zipinfo:
164
- 返回的list每个元素是zipinfo数据类型
165
- """
166
- ls = self.infolist()
167
- if prefix:
168
- ls = list(filter(lambda t: t.filename.startswith(prefix), ls))
169
- if not zipinfo:
170
- ls = list(map(lambda x: x.filename, ls))
171
- return ls
172
-
173
- def unpack(self, extract_dir=None, format='zip', wrap=0):
174
- _unpack_base(self.filename, self.namelist(), format, extract_dir, wrap)
175
-
176
- def write_dir(self, directory, arcname=None, filter_rule=None):
177
- """
178
- 将指定目录(包含子目录)添加到 zip 文件中,并可自定义在 zip 中的存储路径。
179
-
180
- :param directory: 要压缩的目录路径
181
- :param arcname: 在 zip 文件中存储的根目录名,
182
- None, 默认跟ZipFile.write的arcname一样,直接取输入的目录名
183
- :param filter_rule: 过滤规则函数,用于排除不需要的文件。函数应接受文件路径作为参数,
184
- 返回0, 1, 或 2,分别表示排除、递归检查、全部包括。
185
- """
186
- if arcname is None:
187
- arcname = directory
188
-
189
- if filter_rule is True:
190
- filter_rule = zip_filter_rule
191
-
192
- for root, dirs, files in os.walk(directory):
193
- for file in files:
194
- file_path = os.path.join(root, file)
195
-
196
- # 调用过滤函数获取过滤状态
197
- if filter_rule:
198
- result = filter_rule(file_path)
199
- if result == 0: # 明确不要
200
- continue
201
- elif result == 1: # 需要递归精细检查
202
- pass # 继续执行文件写入逻辑
203
- elif result == 2: # 全部包含
204
- # 计算文件在 zip 文件中的相对路径
205
- zip_path = os.path.join(arcname, os.path.relpath(file_path, directory))
206
- self.write(file_path, zip_path)
207
- continue
208
-
209
- # 默认情况下,递归处理
210
- zip_path = os.path.join(arcname, os.path.relpath(file_path, directory))
211
- self.write(file_path, zip_path)
212
-
213
- for dir in dirs[:]:
214
- dir_path = os.path.join(root, dir)
215
-
216
- if filter_rule:
217
- result = filter_rule(dir_path)
218
- if result == 0: # 明确不要
219
- dirs.remove(dir) # 排除整个目录
220
- elif result == 2: # 全部包含
221
- # 添加整个目录
222
- for root2, _, files2 in os.walk(dir_path):
223
- for file2 in files2:
224
- file_path = os.path.join(root2, file2)
225
- zip_path = os.path.join(arcname, os.path.relpath(file_path, directory))
226
- self.write(file_path, zip_path)
227
- dirs.remove(dir) # 递归中不再处理此目录
228
-
229
- def write_path(self, path, arcname=None, filter_rule=None):
230
- """ 封装的同时支持文件或目录的操作 """
231
- if XlPath(path).is_dir():
232
- self.write_dir(path, arcname, filter_rule)
233
- elif XlPath(path).is_file():
234
- self.write(path, arcname)
235
- else:
236
- raise ValueError
237
-
238
- def xlwrite(self, path, arcname=None, filter_rule=True):
239
- self.write_path(path, arcname, filter_rule)
240
-
241
- @classmethod
242
- def create(cls, name=None):
243
- """ 在临时目录下新建一个目录,然后放一个压缩包文件 """
244
- # 如果没有提供 name,则生成一个临时的文件名
245
- if name is None:
246
- name = tempfile.mktemp(suffix=".zip").split('/')[-1] # 提取生成的文件名
247
-
248
- # 创建临时目录(因为可能有并发操作,每个处理都建立一个目录更安全)
249
- temp_dir = tempfile.mkdtemp()
250
- temp_zip_path = f"{temp_dir}/{name}"
251
-
252
- # 创建 ZipFile 对象
253
- zipf = ZipFile(temp_zip_path, 'w')
254
- return zipf
255
-
256
- def fastapi_resp(self):
257
- """ 返回供fastapi后端使用的文件接口 """
258
- from fastapi.responses import FileResponse
259
-
260
- self.close()
261
- return FileResponse(self.filename, media_type='application/zip',
262
- filename=XlPath(self.filename).name)
263
-
264
-
265
- class XlTarFile(TarFile):
266
-
267
- def unpack(self, extract_dir=None, format='tar', wrap=0):
268
- _unpack_base(self.name, self.getnames(), format, extract_dir, wrap)
269
-
270
-
271
- inject_members(XlZipFile, ZipFile)
272
- inject_members(XlTarFile, TarFile)
273
-
274
-
275
- def unpack_archive(filename, extract_dir=None, format=None, *, wrap=1):
276
- """ 对shutil.unpack_archive的扩展,增加了一个wrap的接口功能 """
277
- if format is None:
278
- format = shutil._find_unpack_format(str(filename).lower())
279
-
280
- if format.endswith('zip'):
281
- XlZipFile(filename).unpack(extract_dir, format, wrap)
282
- elif format.endswith('tar'):
283
- XlTarFile(filename).unpack(extract_dir, format, wrap)
284
- else:
285
- # 其他还没扩展的格式,不支持wrap功能,但仍然可以使用shutil标准的接口解压
286
- shutil.unpack_archive(filename, extract_dir, format)
287
-
288
-
289
- def compress_to_zip(source_path, target_zip_path=None, wrap=None,
290
- ignore_func=None):
291
- """ 压缩指定的文件或文件夹为ZIP格式。
292
-
293
- :param str source_path: 要压缩的文件或文件夹路径
294
- :param str target_zip_path: 目标ZIP文件路径(可选)
295
- :param str wrap: 在ZIP文件内部创建的目录名,所有内容将被放在这个目录下
296
- :param func ignore_func: 忽略文件的函数,输入目录或文件的路径,返回True表示不取用
297
- """
298
- # 根据输入路径生成默认的目标ZIP文件路径
299
- if target_zip_path is None:
300
- # 获取输入路径的基本名称(文件名或文件夹名)
301
- base_name = os.path.basename(source_path)
302
- # 为基本名称添加'.zip'后缀
303
- target_zip_path = os.path.join(os.path.dirname(source_path), f"{base_name}.zip")
304
-
305
- # 创建一个ZipFile对象来写入压缩文件
306
- with XlZipFile(target_zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
307
- # 如果是文件夹,则遍历文件夹中的所有文件和子文件夹
308
- if os.path.isdir(source_path):
309
- for root, _, files in os.walk(source_path):
310
- if ignore_func and ignore_func(root):
311
- continue
312
-
313
- for file in files:
314
- if ignore_func and ignore_func(file):
315
- continue
316
-
317
- file_path = os.path.join(root, file)
318
- # 计算文件在ZIP中的相对路径
319
- arcname = os.path.relpath(file_path, source_path)
320
- if wrap:
321
- arcname = os.path.join(wrap, arcname)
322
- zipf.write(file_path, arcname)
323
- # 如果是文件,则直接将文件添加到ZIP中
324
- elif os.path.isfile(source_path):
325
- arcname = os.path.basename(source_path)
326
- if wrap:
327
- arcname = os.path.join(wrap, arcname)
328
- zipf.write(source_path, arcname)
329
-
330
- return target_zip_path
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # @Author : 陈坤泽
4
+ # @Email : 877362867@qq.com
5
+ # @Date : 2022/02/15 09:51
6
+
7
+ import os
8
+ import shutil
9
+ from tarfile import TarFile
10
+ # 这个包里的ZipFile是拷贝后修改过的,不影响标准库zipfile里的功能
11
+ # import pyxllib.file.packlib.zipfile as zipfile
12
+ # from pyxllib.file.packlib.zipfile import ZipFile
13
+ import zipfile
14
+ from zipfile import ZipFile
15
+ import tempfile
16
+
17
+ from pyxllib.file.specialist import XlPath, reduce_dir_depth
18
+ from pyxllib.prog.pupil import inject_members
19
+
20
+ """ 问题:py官方的ZipFile解压带中文的文件会乱码
21
+
22
+ 解决办法及操作流程
23
+ 1、定期将官方的zipfile.py文件更新到pyxllib.file.packlib.zipfile
24
+ 2、将代码中两处cp437都替换为gbk
25
+ """
26
+
27
+
28
+ def unpack_zipfile(filename, extract_dir):
29
+ """ 为了修复zipfile底层的中文解压乱码问题,修改了shutil._UNPACK_FORMATS的底层功能
30
+ """
31
+ from shutil import ReadError
32
+
33
+ zip = zipfile.ZipFile(filename)
34
+ if not zipfile.is_zipfile(filename):
35
+ raise ReadError("%s is not a zip file" % filename)
36
+ try:
37
+ for info in zip.infolist():
38
+ name = info.filename
39
+
40
+ # don't extract absolute paths or ones with .. in them
41
+ if name.startswith('/') or '..' in name:
42
+ continue
43
+
44
+ target = os.path.join(extract_dir, *name.split('/'))
45
+ if not target:
46
+ continue
47
+
48
+ os.makedirs(XlPath(target).parent, exist_ok=True)
49
+ if not name.endswith('/'):
50
+ # file
51
+ data = zip.read(info.filename)
52
+ f = open(target, 'wb')
53
+ try:
54
+ f.write(data)
55
+ finally:
56
+ f.close()
57
+ del data
58
+ finally:
59
+ zip.close()
60
+
61
+
62
+ # 解决unzip中文乱码问题
63
+ shutil._UNPACK_FORMATS['zip'] = (['.zip'], unpack_zipfile, [], "ZIP file")
64
+
65
+
66
+ def _parse_depth(names):
67
+ """ 判断有几层目录都只有独立的一个目录
68
+
69
+ :param names: 一组相对路径名
70
+ :return: 如果直接目录下文件已经不唯一,返回0
71
+
72
+ >>> _parse_depth(['a'])
73
+ 1
74
+ >>> _parse_depth(['a', 'a/1'])
75
+ 2
76
+ >>> _parse_depth(['a', 'a/b', 'a/b/1', 'a/b/2'])
77
+ 2
78
+ >>> _parse_depth(['a', 'a/b', 'a/b/1', 'a/c/3'])
79
+ 1
80
+ """
81
+ parents = []
82
+ for name in names:
83
+ p = XlPath(name)
84
+ ps = [x.as_posix() for x in p.parents]
85
+ parents.append(ps[-2::-1] + [p.as_posix()])
86
+ i = 0
87
+ while len({x[i] for x in parents if len(x) > i}) == 1:
88
+ i += 1
89
+ return i
90
+
91
+
92
+ def _unpack_base(packfile, namelist, format=None, extract_dir=None, wrap=0):
93
+ """ 解压
94
+
95
+ :param packfile: 压缩包本身的文件路径名称,在wrap=1时可能会用到,用来添加一个同名目录
96
+ :param namelist: 压缩包里所有文件的相对路径
97
+ :param format: 压缩包类型,比如zip、tar,可以不输入,此时直接采用shutil.unpack_archive里的判断机制
98
+ :param extract_dir: 解压目标目录,可以不输入,会自动判断
99
+ 自动解压规则:压缩包里目录不唯一,则以压缩包本身的名称创建一个目录存放
100
+ 否则以压缩包里的目录名为准
101
+ :param wrap: 解压时倾向的处理机制
102
+ 0,默认,不进行任何处理
103
+ 1,如果压缩包里一级目录下不止一个对象,使用压缩包本身的名称再包一层
104
+ -1,如果压缩包里一级目录下只有一个目录,则丢弃这个目录,只取里面的内容出来
105
+ 同理,可能这一个目录里的文件,还是只有一个目录,此时支持设置-2、-3等解开上限
106
+ 也就-1比较常用,-2、-3等很少见了
107
+ TODO 还有一种解压机制,只取里面的部分文件,有空可以继续调研扩展下
108
+ :return: 无论怎样,最后都是放到一个目录里,返回改目录路径
109
+ """
110
+
111
+ # 1 默认解压目录是self所在目录,注意这点跟shutil.unpack_archive的当前工作目录不同
112
+ p0 = XlPath(packfile)
113
+ if extract_dir is None:
114
+ extract_dir = p0.parent
115
+
116
+ depth = _parse_depth(namelist)
117
+
118
+ # 2 是否要加目录层级,或者去掉目录层级
119
+ if wrap == 1 and depth == 0:
120
+ # 压缩包里直接目录下有多个文件,且指定了wrap,则加一层目录
121
+ shutil.unpack_archive(packfile, extract_dir / p0.stem, format)
122
+ elif wrap < 0 < depth:
123
+ # 压缩包有depth层冗余目录,并且指定wrap要解开这几层目录
124
+ # 先正常解压(每种压缩包格式内部处理机制都有点不太一样,所以先解压,然后用目录文件功能处理更合适)
125
+ shutil.unpack_archive(packfile, extract_dir, format)
126
+ reduce_dir_depth(extract_dir, unwrap=-wrap)
127
+ else:
128
+ # 正常解压
129
+ shutil.unpack_archive(packfile, extract_dir, format)
130
+
131
+
132
+ def zip_filter_rule(path):
133
+ p = XlPath(path)
134
+
135
+ # 排除特定目录或文件
136
+ if set(p.parts) & {'__pycache__', '.git', '.idea', 'dist', 'build', '.pytest_cache', '.venv'}:
137
+ return 0
138
+
139
+ # 排除特定文件扩展名
140
+ if p.suffix in {'.egg-info', '.lock'}:
141
+ return 0
142
+
143
+ # 精细检查内部文件
144
+ if p.is_dir():
145
+ return 1
146
+
147
+ # 全部包含其他情况
148
+ return 2
149
+
150
+
151
+ class XlZipFile(ZipFile):
152
+
153
+ def infolist2(self, prefix=None, zipinfo=True):
154
+ """>> self.infolist2() # getinfo的多文件版本
155
+ 1 <ZipInfo filename='[Content_Types].xml' compress_type=deflate file_size=1495 compress_size=383>
156
+ 2 <ZipInfo filename='_rels/.rels' compress_type=deflate file_size=590 compress_size=243>
157
+ ......
158
+ 20 <ZipInfo filename='word/fontTable.xml' compress_type=deflate file_size=1590 compress_size=521>
159
+ 21 <ZipInfo filename='docProps/app.xml' compress_type=deflate file_size=720 compress_size=384>
160
+
161
+ :param prefix:
162
+ 可以筛选文件的前缀,例如“word/”可以筛选出word目录下的
163
+ :param zipinfo:
164
+ 返回的list每个元素是zipinfo数据类型
165
+ """
166
+ ls = self.infolist()
167
+ if prefix:
168
+ ls = list(filter(lambda t: t.filename.startswith(prefix), ls))
169
+ if not zipinfo:
170
+ ls = list(map(lambda x: x.filename, ls))
171
+ return ls
172
+
173
+ def unpack(self, extract_dir=None, format='zip', wrap=0):
174
+ _unpack_base(self.filename, self.namelist(), format, extract_dir, wrap)
175
+
176
+ def write_dir(self, directory, arcname=None, filter_rule=None):
177
+ """
178
+ 将指定目录(包含子目录)添加到 zip 文件中,并可自定义在 zip 中的存储路径。
179
+
180
+ :param directory: 要压缩的目录路径
181
+ :param arcname: 在 zip 文件中存储的根目录名,
182
+ None, 默认跟ZipFile.write的arcname一样,直接取输入的目录名
183
+ :param filter_rule: 过滤规则函数,用于排除不需要的文件。函数应接受文件路径作为参数,
184
+ 返回0, 1, 或 2,分别表示排除、递归检查、全部包括。
185
+ """
186
+ if arcname is None:
187
+ arcname = directory
188
+
189
+ if filter_rule is True:
190
+ filter_rule = zip_filter_rule
191
+
192
+ for root, dirs, files in os.walk(directory):
193
+ for file in files:
194
+ file_path = os.path.join(root, file)
195
+
196
+ # 调用过滤函数获取过滤状态
197
+ if filter_rule:
198
+ result = filter_rule(file_path)
199
+ if result == 0: # 明确不要
200
+ continue
201
+ elif result == 1: # 需要递归精细检查
202
+ pass # 继续执行文件写入逻辑
203
+ elif result == 2: # 全部包含
204
+ # 计算文件在 zip 文件中的相对路径
205
+ zip_path = os.path.join(arcname, os.path.relpath(file_path, directory))
206
+ self.write(file_path, zip_path)
207
+ continue
208
+
209
+ # 默认情况下,递归处理
210
+ zip_path = os.path.join(arcname, os.path.relpath(file_path, directory))
211
+ self.write(file_path, zip_path)
212
+
213
+ for dir in dirs[:]:
214
+ dir_path = os.path.join(root, dir)
215
+
216
+ if filter_rule:
217
+ result = filter_rule(dir_path)
218
+ if result == 0: # 明确不要
219
+ dirs.remove(dir) # 排除整个目录
220
+ elif result == 2: # 全部包含
221
+ # 添加整个目录
222
+ for root2, _, files2 in os.walk(dir_path):
223
+ for file2 in files2:
224
+ file_path = os.path.join(root2, file2)
225
+ zip_path = os.path.join(arcname, os.path.relpath(file_path, directory))
226
+ self.write(file_path, zip_path)
227
+ dirs.remove(dir) # 递归中不再处理此目录
228
+
229
+ def write_path(self, path, arcname=None, filter_rule=None):
230
+ """ 封装的同时支持文件或目录的操作 """
231
+ if XlPath(path).is_dir():
232
+ self.write_dir(path, arcname, filter_rule)
233
+ elif XlPath(path).is_file():
234
+ self.write(path, arcname)
235
+ else:
236
+ raise ValueError
237
+
238
+ def xlwrite(self, path, arcname=None, filter_rule=True):
239
+ self.write_path(path, arcname, filter_rule)
240
+
241
+ @classmethod
242
+ def create(cls, name=None):
243
+ """ 在临时目录下新建一个目录,然后放一个压缩包文件 """
244
+ # 如果没有提供 name,则生成一个临时的文件名
245
+ if name is None:
246
+ name = tempfile.mktemp(suffix=".zip").split('/')[-1] # 提取生成的文件名
247
+
248
+ # 创建临时目录(因为可能有并发操作,每个处理都建立一个目录更安全)
249
+ temp_dir = tempfile.mkdtemp()
250
+ temp_zip_path = f"{temp_dir}/{name}"
251
+
252
+ # 创建 ZipFile 对象
253
+ zipf = ZipFile(temp_zip_path, 'w')
254
+ return zipf
255
+
256
+ def fastapi_resp(self):
257
+ """ 返回供fastapi后端使用的文件接口 """
258
+ from fastapi.responses import FileResponse
259
+
260
+ self.close()
261
+ return FileResponse(self.filename, media_type='application/zip',
262
+ filename=XlPath(self.filename).name)
263
+
264
+
265
+ class XlTarFile(TarFile):
266
+
267
+ def unpack(self, extract_dir=None, format='tar', wrap=0):
268
+ _unpack_base(self.name, self.getnames(), format, extract_dir, wrap)
269
+
270
+
271
+ inject_members(XlZipFile, ZipFile)
272
+ inject_members(XlTarFile, TarFile)
273
+
274
+
275
+ def unpack_archive(filename, extract_dir=None, format=None, *, wrap=1):
276
+ """ 对shutil.unpack_archive的扩展,增加了一个wrap的接口功能 """
277
+ if format is None:
278
+ format = shutil._find_unpack_format(str(filename).lower())
279
+
280
+ if format.endswith('zip'):
281
+ XlZipFile(filename).unpack(extract_dir, format, wrap)
282
+ elif format.endswith('tar'):
283
+ XlTarFile(filename).unpack(extract_dir, format, wrap)
284
+ else:
285
+ # 其他还没扩展的格式,不支持wrap功能,但仍然可以使用shutil标准的接口解压
286
+ shutil.unpack_archive(filename, extract_dir, format)
287
+
288
+
289
+ def compress_to_zip(source_path, target_zip_path=None, wrap=None,
290
+ ignore_func=None):
291
+ """ 压缩指定的文件或文件夹为ZIP格式。
292
+
293
+ :param str source_path: 要压缩的文件或文件夹路径
294
+ :param str target_zip_path: 目标ZIP文件路径(可选)
295
+ :param str wrap: 在ZIP文件内部创建的目录名,所有内容将被放在这个目录下
296
+ :param func ignore_func: 忽略文件的函数,输入目录或文件的路径,返回True表示不取用
297
+ """
298
+ # 根据输入路径生成默认的目标ZIP文件路径
299
+ if target_zip_path is None:
300
+ # 获取输入路径的基本名称(文件名或文件夹名)
301
+ base_name = os.path.basename(source_path)
302
+ # 为基本名称添加'.zip'后缀
303
+ target_zip_path = os.path.join(os.path.dirname(source_path), f"{base_name}.zip")
304
+
305
+ # 创建一个ZipFile对象来写入压缩文件
306
+ with XlZipFile(target_zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
307
+ # 如果是文件夹,则遍历文件夹中的所有文件和子文件夹
308
+ if os.path.isdir(source_path):
309
+ for root, _, files in os.walk(source_path):
310
+ if ignore_func and ignore_func(root):
311
+ continue
312
+
313
+ for file in files:
314
+ if ignore_func and ignore_func(file):
315
+ continue
316
+
317
+ file_path = os.path.join(root, file)
318
+ # 计算文件在ZIP中的相对路径
319
+ arcname = os.path.relpath(file_path, source_path)
320
+ if wrap:
321
+ arcname = os.path.join(wrap, arcname)
322
+ zipf.write(file_path, arcname)
323
+ # 如果是文件,则直接将文件添加到ZIP中
324
+ elif os.path.isfile(source_path):
325
+ arcname = os.path.basename(source_path)
326
+ if wrap:
327
+ arcname = os.path.join(wrap, arcname)
328
+ zipf.write(source_path, arcname)
329
+
330
+ return target_zip_path