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,799 +1,799 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- # @Author : 陈坤泽
4
- # @Email : 877362867@qq.com
5
- # @Date : 2020/05/30
6
-
7
-
8
- import collections
9
- import filecmp
10
- import os
11
- import pathlib
12
- import random
13
- import re
14
- import shutil
15
- import tempfile
16
-
17
- import humanfriendly
18
-
19
- # 大小写不敏感字典
20
- from pyxllib.prog.newbie import first_nonnone
21
- from pyxllib.algo.pupil import natural_sort
22
- from pyxllib.text.pupil import strfind
23
- from pyxllib.file.specialist import get_etag, PathBase, File, XlPath
24
-
25
-
26
- def __1_Dir类():
27
- """
28
- 支持文件或文件夹的对比复制删除等操作的函数:filescmp、filesdel、filescopy
29
- """
30
-
31
-
32
- class Dir(PathBase):
33
- r"""类似NestEnv思想的文件夹处理类
34
-
35
- 这里的测试可以全程自己造一个
36
- """
37
- __slots__ = ('_path', 'subs', '_origin_wkdir')
38
-
39
- # 零、常用的目录类
40
- TEMP = pathlib.Path(tempfile.gettempdir())
41
- if os.getenv('Desktop', None): # 如果修改了win10默认的桌面路径,需要在环境变量添加一个正确的Desktop路径值
42
- DESKTOP = os.environ['Desktop']
43
- else:
44
- DESKTOP = os.path.join(str(pathlib.Path.home()), 'Desktop') # 这个不一定准,桌面是有可能被移到D盘等的
45
- DESKTOP = pathlib.Path(DESKTOP)
46
-
47
- # 添加 HOME 目录? 方便linux操作?
48
-
49
- # 一、基本目录类功能
50
-
51
- def __init__(self, path=None, root=None, *, subs=None, check=True):
52
- """根目录、工作目录
53
-
54
- >> Dir() # 以当前文件夹作为root
55
- >> Dir(r'C:/pycode/code4101py') # 指定目录
56
-
57
- :param path: 注意哪怕path传入的是Dir,也只会设置目录,不会取其paths成员值
58
- :param subs: 该目录下,选中的子文件(夹)
59
- """
60
-
61
- self._path = None
62
- self.subs = subs or [] # 初始默认没有选中任何文件(夹)
63
-
64
- # 1 快速初始化
65
- if root is None:
66
- if isinstance(path, Dir):
67
- self._path = path._path
68
- # 注意用Dir A 初始化 Dir B,并不会把A的subs传递给B
69
- return
70
- elif isinstance(path, pathlib.Path):
71
- self._path = path
72
-
73
- # 2 普通初始化
74
- if self._path is None:
75
- self._path = self.abspath(path, root)
76
-
77
- # 3 检查
78
- if check:
79
- if not self._path:
80
- raise ValueError(f'无效路径 {self._path}')
81
- elif self._path.is_file():
82
- raise ValueError(f'不能用文件初始化一个Dir对象 {self._path}')
83
-
84
- @classmethod
85
- def safe_init(cls, path, root=None, *, subs=None):
86
- """ 如果失败不raise,而是返回None的初始化方式 """
87
- try:
88
- d = Dir(path, root, subs=subs)
89
- d._path.is_file() # 有些问题上一步不一定测的出来,要再补一个测试
90
- return d
91
- except (ValueError, TypeError, OSError, PermissionError):
92
- # ValueError:文件名过长,代表输入很可能是一段文本,根本不是路径
93
- # TypeError:不是str等正常的参数
94
- # OSError:非法路径名,例如有 *? 等
95
- # PermissionError: linux上访问无权限、不存在的路径
96
- return None
97
-
98
- @property
99
- def size(self) -> int:
100
- """ 计算目录的大小,会递归目录计算总大小
101
-
102
- https://stackoverflow.com/questions/1392413/calculating-a-directory-size-using-python
103
-
104
- >> Dir('D:/slns/pyxllib').size # 这个算的就是真实大小,不是占用空间
105
- 2939384
106
- """
107
- if self:
108
- total_size = 0
109
- for dirpath, dirnames, Pathnames in os.walk(str(self)):
110
- for f in Pathnames:
111
- fp = os.path.join(dirpath, f)
112
- total_size += os.path.getsize(fp)
113
- else: # 不存在的对象
114
- total_size = 0
115
- return total_size
116
-
117
- @property
118
- def psize(self) -> str:
119
- """ 美化显示的文件大小 """
120
- return humanfriendly.format_size(self.size, binary=True)
121
-
122
- def __truediv__(self, key) -> pathlib.Path:
123
- r""" 路径拼接功能
124
-
125
- >>> Dir('C:/a') / 'b.txt'
126
- WindowsPath('C:/a/b.txt')
127
- """
128
- return self._path / str(key)
129
-
130
- def with_dirname(self, value):
131
- return Dir(self.name, value)
132
-
133
- def absdst(self, dst):
134
- """ 在copy、move等中,给了个"模糊"的目标位置dst,智能推导出实际file、dir绝对路径
135
- """
136
- dst_ = self.abspath(dst)
137
- if isinstance(dst, str) and dst[-1] in ('\\', '/'):
138
- dst_ = Dir(self.name, dst_)
139
- else:
140
- dst_ = Dir(dst_)
141
- return dst_
142
-
143
- def ensure_dir(self):
144
- r""" 确保目录存在
145
- """
146
- if not self:
147
- os.makedirs(str(self))
148
-
149
- def copy(self, dst, if_exists=None):
150
- return self.process(dst, shutil.copytree, if_exists)
151
-
152
- def rename(self, dst, if_exists=None):
153
- r""" 重命名
154
- """
155
- return self.move(Dir(dst, self.parent), if_exists)
156
-
157
- def delete(self):
158
- r""" 删除自身文件
159
- """
160
- if self:
161
- try:
162
- shutil.rmtree(str(self))
163
- except OSError:
164
- # OSError: Cannot call rmtree on a symbolic link
165
- # TODO 本来不应该try except,而是先用os.path.islink判断的,但是这个好像有bug,判断不出来~~
166
- os.unlink(str(self))
167
-
168
- # 二、目录类专有功能
169
-
170
- def sample(self, n=None, frac=None):
171
- """
172
- :param n: 在 paths 中抽取n个文件
173
- :param frac: 按比例抽取文件
174
- :return: 新的Dir文件选取状态
175
- """
176
- n = n or int(frac * len(self.subs))
177
- paths = random.sample(self.subs, n)
178
- return Dir(self._path, subs=paths)
179
-
180
- def subpaths(self):
181
- """ 返回所有subs的绝对路径 """
182
- return [self._path / p for p in self.subs]
183
-
184
- def subfiles(self):
185
- """ 返回所有subs的File对象 (过滤掉文件夹对象) """
186
- return list(map(File, filter(lambda p: not p.is_dir(), self.subpaths())))
187
-
188
- def subdirs(self):
189
- """ 返回所有subs的File对象 (过滤掉文件对象) """
190
- return list(map(Dir, filter(lambda p: not p.is_file(), self.subpaths())))
191
-
192
- def select(self, patter, nsort=True, type_=None,
193
- ignore_backup=False, ignore_special=False,
194
- min_size=None, max_size=None,
195
- min_ctime=None, max_ctime=None, min_mtime=None, max_mtime=None,
196
- **kwargs):
197
- r""" 增加选中文件,从filesmatch衍生而来,参数含义见 filesfilter
198
-
199
- :param bool nsort: 是否使用自然排序,关闭可以加速
200
- :param str type_:
201
- None,所有文件
202
- 'file',只匹配文件
203
- 'dir', 只匹配目录
204
- :param bool ignore_backup: 如果设为False,会过滤掉自定义的备份文件格式,不获取备份类文件
205
- :param bool ignore_special: 自动过滤掉 '.git'、'$RECYCLE.BIN' 目录下文件
206
- :param int min_size: 文件大小过滤,单位Byte
207
- :param int max_size: ~
208
- :param str min_ctime: 创建时间的过滤,格式'2019-09-01'或'2019-09-01 00:00'
209
- :param str max_ctime: ~
210
- :param str min_mtime: 修改时间的过滤
211
- :param str max_mtime: ~
212
- :param kwargs: see filesfilter
213
- :seealso: filesfilter
214
-
215
- 注意select和exclude的增减操作是不断叠加的,而不是每次重置!
216
- 如果需要重置,应该重新定义一个Folder类
217
-
218
- >> Dir('C:/pycode/code4101py').select('*.pyw').select('ckz.py')
219
- C:/pycode/code4101py: ['ol批量修改文本.pyw', 'ckz.py']
220
- >> Dir('C:/pycode/code4101py').select('**/*.pyw').select('ckz.py')
221
- C:/pycode/code4101py: ['ol批量修改文本.pyw', 'chenkz/批量修改文本.pyw', 'winr/bc.pyw', 'winr/reg/FileBackup.pyw', 'ckz.py']
222
-
223
- >> Dir('C:/pycode/code4101py').select('*.py', min_size=200*1024) # 200kb以上的文件
224
- C:/pycode/code4101py: ['liangyb.py']
225
-
226
- >> Dir(r'C:/pycode/code4101py').select('*.py', min_mtime=datetime.date(2020, 3, 1)) # 修改时间在3月1日以上的
227
- """
228
- subs = filesmatch(patter, root=str(self), type_=type_,
229
- ignore_backup=ignore_backup, ignore_special=ignore_special,
230
- min_size=min_size, max_size=max_size,
231
- min_ctime=min_ctime, max_ctime=max_ctime, min_mtime=min_mtime, max_mtime=max_mtime,
232
- **kwargs)
233
- subs = self.subs + subs
234
- if nsort: subs = natural_sort(subs)
235
- return Dir(self._path, subs=subs)
236
-
237
- def select_files(self, patter, nsort=True,
238
- ignore_backup=False, ignore_special=False,
239
- min_size=None, max_size=None,
240
- min_ctime=None, max_ctime=None, min_mtime=None, max_mtime=None):
241
- """ TODO 这系列的功能可以优化加速,在没有复杂规则的情况下,可以尽量用源生的py检索方式实现 """
242
- subs = filesmatch(patter, root=str(self), type_='file',
243
- ignore_backup=ignore_backup, ignore_special=ignore_special,
244
- min_size=min_size, max_size=max_size,
245
- min_ctime=min_ctime, max_ctime=max_ctime,
246
- min_mtime=min_mtime, max_mtime=max_mtime)
247
- if nsort:
248
- subs = natural_sort(subs)
249
- for x in subs:
250
- yield File(self._path / x, check=False)
251
-
252
- def select_dirs(self, patter, nsort=True,
253
- ignore_backup=False, ignore_special=False,
254
- min_size=None, max_size=None,
255
- min_ctime=None, max_ctime=None, min_mtime=None, max_mtime=None):
256
- subs = filesmatch(patter, root=str(self), type_='dir',
257
- ignore_backup=ignore_backup, ignore_special=ignore_special,
258
- min_size=min_size, max_size=max_size,
259
- min_ctime=min_ctime, max_ctime=max_ctime,
260
- min_mtime=min_mtime, max_mtime=max_mtime)
261
- if nsort:
262
- subs = natural_sort(subs)
263
- for x in subs:
264
- yield Dir(self._path / x, check=False)
265
-
266
- def select_paths(self, patter, nsort=True,
267
- ignore_backup=False, ignore_special=False,
268
- min_size=None, max_size=None,
269
- min_ctime=None, max_ctime=None, min_mtime=None, max_mtime=None):
270
- subs = filesmatch(patter, root=str(self),
271
- ignore_backup=ignore_backup, ignore_special=ignore_special,
272
- min_size=min_size, max_size=max_size,
273
- min_ctime=min_ctime, max_ctime=max_ctime,
274
- min_mtime=min_mtime, max_mtime=max_mtime)
275
- if nsort:
276
- subs = natural_sort(subs)
277
- for x in subs:
278
- yield self._path / x
279
-
280
- def procpaths(self, func, start=None, end=None, ref_dir=None, pinterval=None, max_workers=1, interrupt=True):
281
- """ 对选中的文件迭代处理
282
-
283
- :param func: 对每个文件进行处理的自定义接口函数
284
- 参数 p: 输入参数 Path 对象
285
- return: 可以没有返回值
286
- TODO 以后可以返回字典结构,用不同的key表示不同的功能,可以控制些高级功能
287
- :param ref_dir: 使用该参数时,则每次会给func传递两个路径参数
288
- 第一个是原始的file,第二个是ref_dir目录下对应路径的file
289
-
290
- TODO 增设可以bfs还是dfs的功能?
291
-
292
-
293
- 将目录 test 的所有文件拷贝到 test2 目录 示例代码:
294
-
295
- def func(p1, p2):
296
- File(p1).copy(p2)
297
-
298
- Dir('test').select('**/*', type_='file').procfiles(func, ref_dir='test2')
299
-
300
- """
301
- from pyxllib.prog.specialist import Iterate
302
-
303
- if ref_dir:
304
- ref_dir = Dir(ref_dir)
305
- paths1 = self.subpaths()
306
- paths2 = [(ref_dir / self.subs[i]) for i in range(len(self.subs))]
307
-
308
- def wrap_func(data):
309
- func(*data)
310
-
311
- data = zip(paths1, paths2)
312
-
313
- else:
314
- data = self.subpaths()
315
- wrap_func = func
316
-
317
- Iterate(data).run(wrap_func, start=start, end=end, pinterval=pinterval,
318
- max_workers=max_workers, interrupt=interrupt)
319
-
320
- def select_invert(self, patter='**/*', nsort=True, **kwargs):
321
- """ 反选,在"全集"中,选中当前状态下没有被选中的那些文件
322
-
323
- 这里设置的选择模式,是指全集的选择范围
324
- """
325
- subs = Dir(self).select(patter, nsort, **kwargs).subs
326
- cur_subs = set(self.subs)
327
- new_subs = []
328
- for s in subs:
329
- if s not in cur_subs:
330
- new_subs.append(s)
331
- return Dir(self._path, subs=new_subs)
332
-
333
- def exclude(self, patter, **kwargs):
334
- """ 去掉部分选中文件
335
-
336
- d1 = Dir('test').select('**/*.eps')
337
- d2 = d1.exclude('subdir/*.eps')
338
- d3 = d2.select_invert(type_='file')
339
- print(d1.files) # ['AA20pH-c1=1-1.eps', 'AA20pH-c1=1-2.eps', 'subdir/AA20pH-c1=1-2 - 副本.eps']
340
- print(d2.files) # ['AA20pH-c1=1-1.eps', 'AA20pH-c1=1-2.eps']
341
- print(d3.files) # ['subdir/AA20pH-c1=1-2 - 副本.eps']
342
- """
343
- subs = set(filesmatch(patter, root=str(self), **kwargs))
344
- new_subs = []
345
- for s in self.subs:
346
- if s not in subs:
347
- new_subs.append(s)
348
- return Dir(self._path, subs=new_subs)
349
-
350
- def describe(self):
351
- """ 输出目录的一些基本统计信息
352
- """
353
- msg = []
354
- dir_state = self.select('*')
355
- files = dir_state.subfiles()
356
- suffixs = collections.Counter([f.suffix for f in files]).most_common()
357
- dir_size = self.size
358
- msg.append(f'size: {dir_size} ≈ {humanfriendly.format_size(dir_size, binary=True)}')
359
- msg.append(f'files: {len(files)}, {suffixs}')
360
- msg.append(f'dirs: {len(dir_state.subdirs())}')
361
- res = '\n'.join(msg)
362
- print(res)
363
-
364
- def __enter__(self):
365
- """ 使用with模式可以进行工作目录切换
366
-
367
- 注意!注意!注意!
368
- 切换工作目录和多线程混合使用会有意想不到的坑,要慎重!
369
- """
370
- self._origin_wkdir = os.getcwd()
371
- os.chdir(str(self))
372
- return self
373
-
374
- def __exit__(self, exc_type, exc_val, exc_tb):
375
- os.chdir(self._origin_wkdir)
376
-
377
-
378
- def __2_filesxxx():
379
- """
380
- 本来Path、File是能同时处理文件、目录的
381
- 改版后,files底层因为有用到File,现在却不能支持目录的操作了
382
- 可能会有些bug,尽量不要用这些旧功能,或者尽早移除
383
- """
384
-
385
-
386
- def filescmp(f1, f2, shallow=True):
387
- """只有两个存在且是同类型的文件或文件夹,内容相同才会返回True,否则均返回False
388
- :param f1: 待比较的第1个文件(文件夹)
389
- :param f2: 待比较的第2个文件(文件夹)
390
- :param shallow: 默认True,即是利用os.stat()返回的基本信息进行比较
391
- 例如其中的文件大小,但修改时间等是不影响差异判断的
392
- 如果设为False,则会打开比较具体内容,速度会慢一点
393
- """
394
- if os.path.isfile(f1) and os.path.isfile(f2):
395
- cmp = filecmp.cmp(f1, f2, shallow)
396
- elif os.path.isdir(f1) and os.path.isdir(f2):
397
- # 文件夹只确保直接子目录下的清单名称,不比较具体每个文件内容是否相同,和子目录相同
398
- t = filecmp.dircmp(f1, f2, shallow)
399
- cmp = False
400
- try:
401
- if not t.left_only and not t.right_only:
402
- cmp = True
403
- except TypeError:
404
- pass
405
- else: # 有不存在的文件
406
- cmp = False
407
- return cmp
408
-
409
-
410
- def filesfilter(files, *, root=os.curdir, type_=None,
411
- ignore_backup=False, ignore_special=False,
412
- min_size=None, max_size=None,
413
- min_ctime=None, max_ctime=None,
414
- min_mtime=None, max_mtime=None):
415
- """
416
- :param files: 类list对象
417
- :param type_:
418
- None,所有文件
419
- 'file',只匹配文件
420
- 'dir', 只匹配目录
421
- :param ignore_backup: 如果设为False,会过滤掉自定义的备份文件格式,不获取备份类文件
422
- :param ignore_special: 自动过滤掉 '.git'、'$RECYCLE.BIN' 目录下文件
423
- :param min_size: 文件大小过滤,单位Byte
424
- :param max_size: ~
425
- :param min_ctime: 创建时间的过滤,格式'2019-09-01'或'2019-09-01 00:00'
426
- :param max_ctime: ~
427
- :param min_mtime: 修改时间的过滤
428
- :param max_mtime: ~
429
- :return:
430
- """
431
- from datetime import datetime
432
-
433
- def judge(f):
434
- if root: f = os.path.join(root, f)
435
- if type_ == 'file' and not os.path.isfile(f):
436
- return False
437
- elif type_ == 'dir' and not os.path.isdir(f):
438
- return False
439
-
440
- # 尽量避免调用 os.stat,判断是否有自定义大小、时间规则,没有可以跳过这部分
441
- check_arg = first_nonnone([min_size, max_size, min_ctime, max_ctime, min_mtime, max_mtime])
442
- if check_arg is not None:
443
- msg = os.stat(f)
444
- if first_nonnone([min_size, max_size]) is not None:
445
- size = File(f).size
446
- if min_size is not None and size < min_size: return False
447
- if max_size is not None and size > max_size: return False
448
-
449
- if min_ctime or max_ctime:
450
- file_ctime = datetime.fromtimestamp(msg.st_ctime)
451
- if min_ctime and file_ctime < min_ctime: return False
452
- if max_ctime and file_ctime > max_ctime: return False
453
-
454
- if min_mtime or max_mtime:
455
- file_mtime = datetime.fromtimestamp(msg.st_mtime)
456
- if min_mtime and file_mtime < min_mtime: return False
457
- if max_mtime and file_mtime > max_mtime: return False
458
-
459
- if ignore_special:
460
- parts = File(f).parts
461
- if '.git' in parts or '$RECYCLE.BIN' in parts:
462
- return False
463
-
464
- if ignore_backup and File(f).backup_time:
465
- return False
466
-
467
- return True
468
-
469
- root = os.path.abspath(root)
470
- return list(filter(judge, files))
471
-
472
-
473
- def filesmatch(patter, *, root=os.curdir, **kwargs) -> list:
474
- r"""
475
- :param patter:
476
- str,
477
- 不含*、?、<、>,普通筛选规则
478
- 含*、?、<、>,支持Path.glob的通配符模式,使用**可以表示任意子目录
479
- glob其实支持[0-9]这种用法,但是[、]在文件名中是合法的,
480
- 为了明确要使用glob模式,我这里改成<>模式
481
- **/*,是不会匹配到根目录的
482
- re.Patter,正则筛选规则(这种方法会比较慢,但是很灵活) 或者其他有match成员函数的类也可以
483
- 会获得当前工作目录下的所有文件相对路径,组成list
484
- 对list的所有元素使用re.match进行匹配
485
- list、tuple、set对象
486
- 对每一个元素,递归调用filesmatch
487
- 其他参数都是文件筛选功能,详见filesfilter中介绍
488
- :return: 匹配到的所有存在的文件、文件夹,返回“相对路径”
489
-
490
- TODO patter大小写问题?会导致匹配缺失的bug吗?
491
-
492
- >> os.chdir('F:/work/filesmatch') # 工作目录
493
-
494
- 1、普通匹配
495
- >> filesmatch('a') # 匹配当前目录下的文件a,或者目录a
496
- ['a']
497
- >> filesmatch('b/a/')
498
- ['b\\a']
499
- >> filesmatch('b/..\\a/')
500
- ['a']
501
- >> filesmatch('c') # 不存在c则返回 []
502
- []
503
-
504
- 2、通配符模式
505
- >> filesmatch('work/*.png') # 支持通配符
506
- []
507
- >> filesmatch('*.png') # 支持通配符
508
- ['1.png', '1[.png', 'logo.png']
509
- >> filesmatch('**/*.png') # 包含所有子目录下的png图片
510
- ['1.png', '1[.png', 'logo.png', 'a\\2.png']
511
- >> filesmatch('?.png')
512
- ['1.png']
513
- >> filesmatch('[0-9]/<0-9>.txt') # 用<0-9>表示[0-9]模式
514
- ['[0-9]\\3.txt']
515
-
516
- 3、正则模式
517
- >> filesmatch(re.compile(r'\d\[\.png$'))
518
- ['1[.png']
519
-
520
- 4、其他高级用法
521
- >> filesmatch('**/*', type_='dir', max_size=0) # 筛选空目录
522
- ['b', '[0-9]']
523
- >> filesmatch('**/*', type_='file', max_size=0) # 筛选空文件
524
- ['b/a', '[0-9]/3.txt']
525
- """
526
- from pathlib import Path
527
- root = os.path.abspath(root)
528
-
529
- # 0 规则匹配
530
- # patter = str(patter) # 200916周三14:59,这样会处理不了正则,要关掉
531
- glob_chars_pos = strfind(patter, ('*', '?', '<', '>')) if isinstance(patter, str) else -1
532
-
533
- # 1 普通文本匹配 (没有通配符,单文件查找)
534
- if isinstance(patter, str) and glob_chars_pos == -1:
535
- path = Path(os.path.join(root, patter))
536
- if path: # 文件存在
537
- p = str(path.resolve())
538
- if p.startswith(root): p = p[len(root) + 1:]
539
- res = [p]
540
- else: # 文件不存在
541
- res = []
542
- # 2 glob通配符匹配
543
- elif isinstance(patter, str) and glob_chars_pos != -1:
544
- patter = patter.replace('\\', '/')
545
- t = patter[:glob_chars_pos].rfind('/')
546
- # 计算出这批文件实际所在的目录dirname
547
- if t == -1: # 模式里没有套子文件夹
548
- dirname, basename = root, patter
549
- else: # 模式里有套子文件夹
550
- dirname, basename = os.path.abspath(os.path.join(root, patter[:t])), patter[t + 1:]
551
- basename = basename.replace('<', '[').replace('>', ']')
552
- files = map(str, Path(dirname).glob(basename))
553
-
554
- n = len(root) + 1
555
- res = [(x[n:] if x.startswith(root) else x) for x in files]
556
- # 3 正则匹配 (只要有match成员函数就行,不一定非要正则对象)
557
- elif hasattr(patter, 'match'):
558
- files = filesmatch('**/*', root=root)
559
- res = list(filter(lambda x: patter.match(x), files))
560
- # 4 list等迭代对象
561
- elif isinstance(patter, (list, tuple, set)):
562
- res = []
563
- for p in patter: res += filesmatch(p, root=root)
564
- # 5 可调用对象
565
- elif callable(patter):
566
- from pyxllib.file.specialist import XlPath
567
- res = [f.relpath(root).as_posix() for f in XlPath(root).rglob('*') if patter(f)]
568
- else:
569
- raise TypeError
570
-
571
- # 2 filetype的筛选
572
- res = filesfilter(res, root=root, **kwargs)
573
-
574
- return [x.replace('\\', '/') for x in res]
575
-
576
-
577
- def filesdel(path, **kwargs):
578
- """删除文件或文件夹
579
- 支持filesfilter的筛选规则
580
- """
581
- for f in filesmatch(path, **kwargs):
582
- if os.path.isfile(f):
583
- os.remove(f)
584
- else:
585
- shutil.rmtree(f)
586
- # TODO 确保删除后再执行后续代码 但是一直觉得这样写很别扭
587
- while os.path.exists(f): pass
588
-
589
-
590
- def _files_copy_move_base(src, dst, filefunc, dirfunc,
591
- *, if_exists=None, treeroot=None, **kwargs):
592
- # 1 辅助函数
593
- def proc_onefile(f, dst):
594
- # dprint(f, dst)
595
- # 1 解析dst参数:对文件或目录不同情况做预处理
596
- # (输入的时候dst_可以只是目标的父目录,要推算出实际要存储的目标名)
597
- if os.path.isfile(f):
598
- if os.path.isdir(dst) or dst[-1] in ('/', '\\'):
599
- dst = os.path.join(dst, os.path.basename(f))
600
- func = filefunc
601
- else:
602
- if dst[0] in ('/', '\\'):
603
- dst = os.path.join(dst, os.path.basename(f))
604
- func = dirfunc
605
-
606
- # 2 根据目标是否已存在和if_exists分类处理
607
- File(dst).ensure_parent()
608
- # 目前存在,且不是把文件移向文件夹的操作
609
- if os.path.exists(dst):
610
- # 根据if_exists参数情况分类处理
611
- if if_exists is None: # 智能判断
612
- if not filescmp(f, dst): # 如果内容不同则backup
613
- File(dst).backup(move=True)
614
- func(f, dst)
615
- elif os.path.abspath(f).lower() == os.path.abspath(dst).lower():
616
- # 如果内容相同,再判断其是否实际是一个文件,则调用重命名功能
617
- os.rename(f, dst)
618
- elif if_exists == 'backup':
619
- File(dst).backup(move=True)
620
- func(f, dst)
621
- elif if_exists == 'replace':
622
- filesdel(dst)
623
- func(f, dst)
624
- elif if_exists == 'ignore':
625
- pass # 跳过,不处理
626
- else:
627
- raise ValueError
628
- else:
629
- func(f, dst) # TODO 这里有bug \2020LaTeX\C春季教材\初数\初一上\Word+外包商原稿
630
-
631
- # 2 主体代码
632
- files = filesmatch(src, **kwargs)
633
-
634
- if len(files) == 1:
635
- proc_onefile(files[0], dst)
636
- elif len(files) > 1: # 多文件模式拆解为单文件模式操作
637
- # 如果设置了 treeroot,这里要预处理下
638
- if treeroot:
639
- treeroot = filesmatch(treeroot)[0]
640
- if treeroot[-1] not in ('/', '\\'):
641
- treeroot += '/'
642
- n = len(treeroot) if treeroot else 0
643
- if treeroot: treeroot = treeroot.replace('\\', '/')
644
-
645
- # 迭代操作
646
- for f in files:
647
- dst_ = dst
648
- if treeroot and f.startswith(treeroot):
649
- dst_ = os.path.join(dst, f[n:])
650
- proc_onefile(f, dst_)
651
-
652
-
653
- def filescopy(src, dst, *, if_exists=None, treeroot=None, **kwargs):
654
- r"""会自动添加不存在的目录的拷贝
655
-
656
- :param src: 要处理的目标
657
- 'a',复制文件a,或者整个文件夹a
658
- 'a/*.txt',复制文件夹下所有的txt文件
659
- 更多匹配模式详见 filesmatch
660
- :param dst: 移到目标位置
661
- 'a',
662
- 如果a是已存在的目录,效果同'a/'
663
- 如果是已存在的文件,且src只有一个要复制的文件,也是合法的。否则报错
664
- 错误类型包括,把一个目录复制到已存在的文件
665
- 把多个文件复制到已存在的文件
666
- 如果a不存在,则
667
- src只是一个待复制的文件时是合法的
668
- 'a/',(可以省略写具体值,只写父级目录)将src匹配到的所有文件,放到目标a目录下
669
- :param if_exists: backup和replace含智能处理,如果内容相同则直接ignore
670
- 'ignore',跳过
671
- 'backup'(默认),备份
672
- 注意多文件操作时,来源不同的文件夹可能有同名文件
673
- 'replace',强制替换
674
- :param treeroot: 输入一个目录名开启该功能选项 (此模式下dst末尾强制要有一个'/')
675
- 对src中匹配到的所有文件,都会去掉treeroot的父目录前缀
676
- 然后将剩下文件的所有相对路径结构,拷贝到dst目录下
677
- 示例:将a目录下所有png图片原结构拷贝到b目录下
678
- filescopy('a/**/*.png', 'b/', if_exists='replace', treeroot='a')
679
- 友情提示:treeroot要跟src使用同样的相对或绝对路径值,否则可能出现意外错误
680
-
681
- >> filescopy('filesmatch/**/*.png', 'filesmatch+/', treeroot='filesmatch')
682
- filesmatch: 1.png,a/2.png -> filesmatch+:1.png,a/2.png
683
-
684
- >> filescopy('filesmatch/**/*.png', 'filesmatch+/')
685
- filesmatch: 1.png,a/2.png -> filesmatch+:1.png,2.png
686
-
687
- TODO filescopy和filesmove还是有瑕疵和效率问题的,有空要继续优化
688
- """
689
- return _files_copy_move_base(src, dst, shutil.copy2, shutil.copytree,
690
- if_exists=if_exists, treeroot=treeroot, **kwargs)
691
-
692
-
693
- def filesmove(src, dst, *, if_exists=None, treeroot=None, **kwargs):
694
- r"""与filescopy高度相同,见filescopy文档
695
-
696
- >> filesmove('a.xslx', 'A.xlsx', if_exists='replace') # 等价于 os.rename('a.xlsx', 'A.xlsx')
697
- """
698
- return _files_copy_move_base(src, dst, shutil.move, shutil.move,
699
- if_exists=if_exists, treeroot=treeroot, **kwargs)
700
-
701
-
702
- def refinepath(s, reserve=''):
703
- """
704
- :param reserve: 保留的字符,例如输入'*?',会保留这两个字符作为通配符
705
- """
706
- if not s: return s
707
- # 1 去掉路径中的不可见字符,注意这里第1个参数里有一个不可见字符!别乱动这里的代码!
708
- s = s.replace(chr(8234), '')
709
- chars = set(r'\/:*?"<>|') - set(reserve)
710
- for ch in chars: # windows路径中不能包含的字符
711
- s = s.replace(ch, '')
712
-
713
- # 2 去除目录、文件名前后的空格
714
- s = re.sub(r'\s+([/\\])', r'\1', s)
715
- s = re.sub(r'([/\\])\s+', r'\1', s)
716
-
717
- return s
718
-
719
-
720
- def writefile(ob, path='', *, encoding='utf8', if_exists='backup', suffix=None, root=None, etag=None) -> str:
721
- """往文件path写入ob内容
722
- :param ob: 写入的内容
723
- 如果要写txt文本文件且ob不是文本对象,只会进行简单的字符串化
724
- :param path: 写入的文件名,使用空字符串时,会使用etag值
725
- :param encoding: 强制写入的编码
726
- :param if_exists: 如果文件已存在,要进行的操作
727
- :param suffix: 文件扩展名
728
- 以'.'为开头,设置“候补扩展名”,即只在fn没有指明扩展名时,会采用
729
- :param root: 相对位置
730
- :return: 返回写入的文件名,这个主要是在写临时文件时有用
731
- """
732
- if etag is None: etag = (not path)
733
- if path == '': path = ...
734
- f = File(path, root, suffix=suffix).write(ob, encoding=encoding, if_exists=if_exists)
735
- if etag:
736
- f = f.rename(get_etag(str(f)))
737
- return str(f)
738
-
739
-
740
- def merge_dir(src, dst, if_exists='skip'):
741
- """ 将src目录下的数据拷贝到dst目录
742
- """
743
-
744
- def func(p1, p2):
745
- p1.copy(p2, if_exists=if_exists)
746
-
747
- # 只拷文件和空目录,不然逻辑会乱
748
- Dir(src).select('**/*', type_='dir', max_size=0).select('**/*', type_='file').procpaths(func, ref_dir=dst)
749
-
750
-
751
- def extract_files(src, dst, pattern, if_exists='replace'):
752
- """ 提取满足pattern模式的文件
753
- """
754
- d1, d2 = Dir(src), Dir(dst)
755
- files = d1.select(pattern).subs
756
- for f in files:
757
- p1, p2 = File(d1 / f), File(d2 / f)
758
- p1.copy(p2, if_exists=if_exists)
759
-
760
-
761
- def file_or_dir_size(path):
762
- if os.path.isfile(path):
763
- return File(path).size
764
- elif os.path.isdir(path):
765
- return Dir(path).size
766
- else:
767
- return 0
768
-
769
-
770
- def reduce_dir_depth(srcdir, unwrap=999):
771
- """ 精简冗余嵌套的目录
772
-
773
- 比如a目录下只有一个文件:a/b/1.txt,
774
- 那么可以精简为a/1.txt,不需要多嵌套一个b目录
775
-
776
- :param srcdir: 要处理的目录
777
- :param unwrap: 打算解开的层数,未设置则会尽可能多解开
778
- """
779
- import tempfile
780
- root = p = XlPath(srcdir)
781
- depth = 0
782
-
783
- ps = list(p.glob('*'))
784
- while len(ps) == 1 and ps[0].is_dir() and depth < unwrap:
785
- depth += 1
786
- p = ps[0]
787
- ps = list(p.glob('*'))
788
-
789
- if depth:
790
- # 注意这里技巧,为了避免多层目录里会有相对同名的目录,导致出现不可预料的bug
791
- # 算法原理是把要搬家的那层目录里的文件先移到临时文件,然后把原目录树结构删除后,再报临时文件的文件移回来
792
- tmpdir = tempfile.mktemp()
793
- shutil.move(str(p), str(tmpdir))
794
- if depth > 1:
795
- shutil.rmtree(next(root.glob('*')))
796
-
797
- for pp in XlPath(tmpdir).glob('*'):
798
- shutil.move(str(pp), str(root))
799
- shutil.rmtree(tmpdir)
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # @Author : 陈坤泽
4
+ # @Email : 877362867@qq.com
5
+ # @Date : 2020/05/30
6
+
7
+
8
+ import collections
9
+ import filecmp
10
+ import os
11
+ import pathlib
12
+ import random
13
+ import re
14
+ import shutil
15
+ import tempfile
16
+
17
+ import humanfriendly
18
+
19
+ # 大小写不敏感字典
20
+ from pyxllib.prog.newbie import first_nonnone
21
+ from pyxllib.algo.pupil import natural_sort
22
+ from pyxllib.text.pupil import strfind
23
+ from pyxllib.file.specialist import get_etag, PathBase, File, XlPath
24
+
25
+
26
+ def __1_Dir类():
27
+ """
28
+ 支持文件或文件夹的对比复制删除等操作的函数:filescmp、filesdel、filescopy
29
+ """
30
+
31
+
32
+ class Dir(PathBase):
33
+ r"""类似NestEnv思想的文件夹处理类
34
+
35
+ 这里的测试可以全程自己造一个
36
+ """
37
+ __slots__ = ('_path', 'subs', '_origin_wkdir')
38
+
39
+ # 零、常用的目录类
40
+ TEMP = pathlib.Path(tempfile.gettempdir())
41
+ if os.getenv('Desktop', None): # 如果修改了win10默认的桌面路径,需要在环境变量添加一个正确的Desktop路径值
42
+ DESKTOP = os.environ['Desktop']
43
+ else:
44
+ DESKTOP = os.path.join(str(pathlib.Path.home()), 'Desktop') # 这个不一定准,桌面是有可能被移到D盘等的
45
+ DESKTOP = pathlib.Path(DESKTOP)
46
+
47
+ # 添加 HOME 目录? 方便linux操作?
48
+
49
+ # 一、基本目录类功能
50
+
51
+ def __init__(self, path=None, root=None, *, subs=None, check=True):
52
+ """根目录、工作目录
53
+
54
+ >> Dir() # 以当前文件夹作为root
55
+ >> Dir(r'C:/pycode/code4101py') # 指定目录
56
+
57
+ :param path: 注意哪怕path传入的是Dir,也只会设置目录,不会取其paths成员值
58
+ :param subs: 该目录下,选中的子文件(夹)
59
+ """
60
+
61
+ self._path = None
62
+ self.subs = subs or [] # 初始默认没有选中任何文件(夹)
63
+
64
+ # 1 快速初始化
65
+ if root is None:
66
+ if isinstance(path, Dir):
67
+ self._path = path._path
68
+ # 注意用Dir A 初始化 Dir B,并不会把A的subs传递给B
69
+ return
70
+ elif isinstance(path, pathlib.Path):
71
+ self._path = path
72
+
73
+ # 2 普通初始化
74
+ if self._path is None:
75
+ self._path = self.abspath(path, root)
76
+
77
+ # 3 检查
78
+ if check:
79
+ if not self._path:
80
+ raise ValueError(f'无效路径 {self._path}')
81
+ elif self._path.is_file():
82
+ raise ValueError(f'不能用文件初始化一个Dir对象 {self._path}')
83
+
84
+ @classmethod
85
+ def safe_init(cls, path, root=None, *, subs=None):
86
+ """ 如果失败不raise,而是返回None的初始化方式 """
87
+ try:
88
+ d = Dir(path, root, subs=subs)
89
+ d._path.is_file() # 有些问题上一步不一定测的出来,要再补一个测试
90
+ return d
91
+ except (ValueError, TypeError, OSError, PermissionError):
92
+ # ValueError:文件名过长,代表输入很可能是一段文本,根本不是路径
93
+ # TypeError:不是str等正常的参数
94
+ # OSError:非法路径名,例如有 *? 等
95
+ # PermissionError: linux上访问无权限、不存在的路径
96
+ return None
97
+
98
+ @property
99
+ def size(self) -> int:
100
+ """ 计算目录的大小,会递归目录计算总大小
101
+
102
+ https://stackoverflow.com/questions/1392413/calculating-a-directory-size-using-python
103
+
104
+ >> Dir('D:/slns/pyxllib').size # 这个算的就是真实大小,不是占用空间
105
+ 2939384
106
+ """
107
+ if self:
108
+ total_size = 0
109
+ for dirpath, dirnames, Pathnames in os.walk(str(self)):
110
+ for f in Pathnames:
111
+ fp = os.path.join(dirpath, f)
112
+ total_size += os.path.getsize(fp)
113
+ else: # 不存在的对象
114
+ total_size = 0
115
+ return total_size
116
+
117
+ @property
118
+ def psize(self) -> str:
119
+ """ 美化显示的文件大小 """
120
+ return humanfriendly.format_size(self.size, binary=True)
121
+
122
+ def __truediv__(self, key) -> pathlib.Path:
123
+ r""" 路径拼接功能
124
+
125
+ >>> Dir('C:/a') / 'b.txt'
126
+ WindowsPath('C:/a/b.txt')
127
+ """
128
+ return self._path / str(key)
129
+
130
+ def with_dirname(self, value):
131
+ return Dir(self.name, value)
132
+
133
+ def absdst(self, dst):
134
+ """ 在copy、move等中,给了个"模糊"的目标位置dst,智能推导出实际file、dir绝对路径
135
+ """
136
+ dst_ = self.abspath(dst)
137
+ if isinstance(dst, str) and dst[-1] in ('\\', '/'):
138
+ dst_ = Dir(self.name, dst_)
139
+ else:
140
+ dst_ = Dir(dst_)
141
+ return dst_
142
+
143
+ def ensure_dir(self):
144
+ r""" 确保目录存在
145
+ """
146
+ if not self:
147
+ os.makedirs(str(self))
148
+
149
+ def copy(self, dst, if_exists=None):
150
+ return self.process(dst, shutil.copytree, if_exists)
151
+
152
+ def rename(self, dst, if_exists=None):
153
+ r""" 重命名
154
+ """
155
+ return self.move(Dir(dst, self.parent), if_exists)
156
+
157
+ def delete(self):
158
+ r""" 删除自身文件
159
+ """
160
+ if self:
161
+ try:
162
+ shutil.rmtree(str(self))
163
+ except OSError:
164
+ # OSError: Cannot call rmtree on a symbolic link
165
+ # TODO 本来不应该try except,而是先用os.path.islink判断的,但是这个好像有bug,判断不出来~~
166
+ os.unlink(str(self))
167
+
168
+ # 二、目录类专有功能
169
+
170
+ def sample(self, n=None, frac=None):
171
+ """
172
+ :param n: 在 paths 中抽取n个文件
173
+ :param frac: 按比例抽取文件
174
+ :return: 新的Dir文件选取状态
175
+ """
176
+ n = n or int(frac * len(self.subs))
177
+ paths = random.sample(self.subs, n)
178
+ return Dir(self._path, subs=paths)
179
+
180
+ def subpaths(self):
181
+ """ 返回所有subs的绝对路径 """
182
+ return [self._path / p for p in self.subs]
183
+
184
+ def subfiles(self):
185
+ """ 返回所有subs的File对象 (过滤掉文件夹对象) """
186
+ return list(map(File, filter(lambda p: not p.is_dir(), self.subpaths())))
187
+
188
+ def subdirs(self):
189
+ """ 返回所有subs的File对象 (过滤掉文件对象) """
190
+ return list(map(Dir, filter(lambda p: not p.is_file(), self.subpaths())))
191
+
192
+ def select(self, patter, nsort=True, type_=None,
193
+ ignore_backup=False, ignore_special=False,
194
+ min_size=None, max_size=None,
195
+ min_ctime=None, max_ctime=None, min_mtime=None, max_mtime=None,
196
+ **kwargs):
197
+ r""" 增加选中文件,从filesmatch衍生而来,参数含义见 filesfilter
198
+
199
+ :param bool nsort: 是否使用自然排序,关闭可以加速
200
+ :param str type_:
201
+ None,所有文件
202
+ 'file',只匹配文件
203
+ 'dir', 只匹配目录
204
+ :param bool ignore_backup: 如果设为False,会过滤掉自定义的备份文件格式,不获取备份类文件
205
+ :param bool ignore_special: 自动过滤掉 '.git'、'$RECYCLE.BIN' 目录下文件
206
+ :param int min_size: 文件大小过滤,单位Byte
207
+ :param int max_size: ~
208
+ :param str min_ctime: 创建时间的过滤,格式'2019-09-01'或'2019-09-01 00:00'
209
+ :param str max_ctime: ~
210
+ :param str min_mtime: 修改时间的过滤
211
+ :param str max_mtime: ~
212
+ :param kwargs: see filesfilter
213
+ :seealso: filesfilter
214
+
215
+ 注意select和exclude的增减操作是不断叠加的,而不是每次重置!
216
+ 如果需要重置,应该重新定义一个Folder类
217
+
218
+ >> Dir('C:/pycode/code4101py').select('*.pyw').select('ckz.py')
219
+ C:/pycode/code4101py: ['ol批量修改文本.pyw', 'ckz.py']
220
+ >> Dir('C:/pycode/code4101py').select('**/*.pyw').select('ckz.py')
221
+ C:/pycode/code4101py: ['ol批量修改文本.pyw', 'chenkz/批量修改文本.pyw', 'winr/bc.pyw', 'winr/reg/FileBackup.pyw', 'ckz.py']
222
+
223
+ >> Dir('C:/pycode/code4101py').select('*.py', min_size=200*1024) # 200kb以上的文件
224
+ C:/pycode/code4101py: ['liangyb.py']
225
+
226
+ >> Dir(r'C:/pycode/code4101py').select('*.py', min_mtime=datetime.date(2020, 3, 1)) # 修改时间在3月1日以上的
227
+ """
228
+ subs = filesmatch(patter, root=str(self), type_=type_,
229
+ ignore_backup=ignore_backup, ignore_special=ignore_special,
230
+ min_size=min_size, max_size=max_size,
231
+ min_ctime=min_ctime, max_ctime=max_ctime, min_mtime=min_mtime, max_mtime=max_mtime,
232
+ **kwargs)
233
+ subs = self.subs + subs
234
+ if nsort: subs = natural_sort(subs)
235
+ return Dir(self._path, subs=subs)
236
+
237
+ def select_files(self, patter, nsort=True,
238
+ ignore_backup=False, ignore_special=False,
239
+ min_size=None, max_size=None,
240
+ min_ctime=None, max_ctime=None, min_mtime=None, max_mtime=None):
241
+ """ TODO 这系列的功能可以优化加速,在没有复杂规则的情况下,可以尽量用源生的py检索方式实现 """
242
+ subs = filesmatch(patter, root=str(self), type_='file',
243
+ ignore_backup=ignore_backup, ignore_special=ignore_special,
244
+ min_size=min_size, max_size=max_size,
245
+ min_ctime=min_ctime, max_ctime=max_ctime,
246
+ min_mtime=min_mtime, max_mtime=max_mtime)
247
+ if nsort:
248
+ subs = natural_sort(subs)
249
+ for x in subs:
250
+ yield File(self._path / x, check=False)
251
+
252
+ def select_dirs(self, patter, nsort=True,
253
+ ignore_backup=False, ignore_special=False,
254
+ min_size=None, max_size=None,
255
+ min_ctime=None, max_ctime=None, min_mtime=None, max_mtime=None):
256
+ subs = filesmatch(patter, root=str(self), type_='dir',
257
+ ignore_backup=ignore_backup, ignore_special=ignore_special,
258
+ min_size=min_size, max_size=max_size,
259
+ min_ctime=min_ctime, max_ctime=max_ctime,
260
+ min_mtime=min_mtime, max_mtime=max_mtime)
261
+ if nsort:
262
+ subs = natural_sort(subs)
263
+ for x in subs:
264
+ yield Dir(self._path / x, check=False)
265
+
266
+ def select_paths(self, patter, nsort=True,
267
+ ignore_backup=False, ignore_special=False,
268
+ min_size=None, max_size=None,
269
+ min_ctime=None, max_ctime=None, min_mtime=None, max_mtime=None):
270
+ subs = filesmatch(patter, root=str(self),
271
+ ignore_backup=ignore_backup, ignore_special=ignore_special,
272
+ min_size=min_size, max_size=max_size,
273
+ min_ctime=min_ctime, max_ctime=max_ctime,
274
+ min_mtime=min_mtime, max_mtime=max_mtime)
275
+ if nsort:
276
+ subs = natural_sort(subs)
277
+ for x in subs:
278
+ yield self._path / x
279
+
280
+ def procpaths(self, func, start=None, end=None, ref_dir=None, pinterval=None, max_workers=1, interrupt=True):
281
+ """ 对选中的文件迭代处理
282
+
283
+ :param func: 对每个文件进行处理的自定义接口函数
284
+ 参数 p: 输入参数 Path 对象
285
+ return: 可以没有返回值
286
+ TODO 以后可以返回字典结构,用不同的key表示不同的功能,可以控制些高级功能
287
+ :param ref_dir: 使用该参数时,则每次会给func传递两个路径参数
288
+ 第一个是原始的file,第二个是ref_dir目录下对应路径的file
289
+
290
+ TODO 增设可以bfs还是dfs的功能?
291
+
292
+
293
+ 将目录 test 的所有文件拷贝到 test2 目录 示例代码:
294
+
295
+ def func(p1, p2):
296
+ File(p1).copy(p2)
297
+
298
+ Dir('test').select('**/*', type_='file').procfiles(func, ref_dir='test2')
299
+
300
+ """
301
+ from pyxllib.prog.specialist import Iterate
302
+
303
+ if ref_dir:
304
+ ref_dir = Dir(ref_dir)
305
+ paths1 = self.subpaths()
306
+ paths2 = [(ref_dir / self.subs[i]) for i in range(len(self.subs))]
307
+
308
+ def wrap_func(data):
309
+ func(*data)
310
+
311
+ data = zip(paths1, paths2)
312
+
313
+ else:
314
+ data = self.subpaths()
315
+ wrap_func = func
316
+
317
+ Iterate(data).run(wrap_func, start=start, end=end, pinterval=pinterval,
318
+ max_workers=max_workers, interrupt=interrupt)
319
+
320
+ def select_invert(self, patter='**/*', nsort=True, **kwargs):
321
+ """ 反选,在"全集"中,选中当前状态下没有被选中的那些文件
322
+
323
+ 这里设置的选择模式,是指全集的选择范围
324
+ """
325
+ subs = Dir(self).select(patter, nsort, **kwargs).subs
326
+ cur_subs = set(self.subs)
327
+ new_subs = []
328
+ for s in subs:
329
+ if s not in cur_subs:
330
+ new_subs.append(s)
331
+ return Dir(self._path, subs=new_subs)
332
+
333
+ def exclude(self, patter, **kwargs):
334
+ """ 去掉部分选中文件
335
+
336
+ d1 = Dir('test').select('**/*.eps')
337
+ d2 = d1.exclude('subdir/*.eps')
338
+ d3 = d2.select_invert(type_='file')
339
+ print(d1.files) # ['AA20pH-c1=1-1.eps', 'AA20pH-c1=1-2.eps', 'subdir/AA20pH-c1=1-2 - 副本.eps']
340
+ print(d2.files) # ['AA20pH-c1=1-1.eps', 'AA20pH-c1=1-2.eps']
341
+ print(d3.files) # ['subdir/AA20pH-c1=1-2 - 副本.eps']
342
+ """
343
+ subs = set(filesmatch(patter, root=str(self), **kwargs))
344
+ new_subs = []
345
+ for s in self.subs:
346
+ if s not in subs:
347
+ new_subs.append(s)
348
+ return Dir(self._path, subs=new_subs)
349
+
350
+ def describe(self):
351
+ """ 输出目录的一些基本统计信息
352
+ """
353
+ msg = []
354
+ dir_state = self.select('*')
355
+ files = dir_state.subfiles()
356
+ suffixs = collections.Counter([f.suffix for f in files]).most_common()
357
+ dir_size = self.size
358
+ msg.append(f'size: {dir_size} ≈ {humanfriendly.format_size(dir_size, binary=True)}')
359
+ msg.append(f'files: {len(files)}, {suffixs}')
360
+ msg.append(f'dirs: {len(dir_state.subdirs())}')
361
+ res = '\n'.join(msg)
362
+ print(res)
363
+
364
+ def __enter__(self):
365
+ """ 使用with模式可以进行工作目录切换
366
+
367
+ 注意!注意!注意!
368
+ 切换工作目录和多线程混合使用会有意想不到的坑,要慎重!
369
+ """
370
+ self._origin_wkdir = os.getcwd()
371
+ os.chdir(str(self))
372
+ return self
373
+
374
+ def __exit__(self, exc_type, exc_val, exc_tb):
375
+ os.chdir(self._origin_wkdir)
376
+
377
+
378
+ def __2_filesxxx():
379
+ """
380
+ 本来Path、File是能同时处理文件、目录的
381
+ 改版后,files底层因为有用到File,现在却不能支持目录的操作了
382
+ 可能会有些bug,尽量不要用这些旧功能,或者尽早移除
383
+ """
384
+
385
+
386
+ def filescmp(f1, f2, shallow=True):
387
+ """只有两个存在且是同类型的文件或文件夹,内容相同才会返回True,否则均返回False
388
+ :param f1: 待比较的第1个文件(文件夹)
389
+ :param f2: 待比较的第2个文件(文件夹)
390
+ :param shallow: 默认True,即是利用os.stat()返回的基本信息进行比较
391
+ 例如其中的文件大小,但修改时间等是不影响差异判断的
392
+ 如果设为False,则会打开比较具体内容,速度会慢一点
393
+ """
394
+ if os.path.isfile(f1) and os.path.isfile(f2):
395
+ cmp = filecmp.cmp(f1, f2, shallow)
396
+ elif os.path.isdir(f1) and os.path.isdir(f2):
397
+ # 文件夹只确保直接子目录下的清单名称,不比较具体每个文件内容是否相同,和子目录相同
398
+ t = filecmp.dircmp(f1, f2, shallow)
399
+ cmp = False
400
+ try:
401
+ if not t.left_only and not t.right_only:
402
+ cmp = True
403
+ except TypeError:
404
+ pass
405
+ else: # 有不存在的文件
406
+ cmp = False
407
+ return cmp
408
+
409
+
410
+ def filesfilter(files, *, root=os.curdir, type_=None,
411
+ ignore_backup=False, ignore_special=False,
412
+ min_size=None, max_size=None,
413
+ min_ctime=None, max_ctime=None,
414
+ min_mtime=None, max_mtime=None):
415
+ """
416
+ :param files: 类list对象
417
+ :param type_:
418
+ None,所有文件
419
+ 'file',只匹配文件
420
+ 'dir', 只匹配目录
421
+ :param ignore_backup: 如果设为False,会过滤掉自定义的备份文件格式,不获取备份类文件
422
+ :param ignore_special: 自动过滤掉 '.git'、'$RECYCLE.BIN' 目录下文件
423
+ :param min_size: 文件大小过滤,单位Byte
424
+ :param max_size: ~
425
+ :param min_ctime: 创建时间的过滤,格式'2019-09-01'或'2019-09-01 00:00'
426
+ :param max_ctime: ~
427
+ :param min_mtime: 修改时间的过滤
428
+ :param max_mtime: ~
429
+ :return:
430
+ """
431
+ from datetime import datetime
432
+
433
+ def judge(f):
434
+ if root: f = os.path.join(root, f)
435
+ if type_ == 'file' and not os.path.isfile(f):
436
+ return False
437
+ elif type_ == 'dir' and not os.path.isdir(f):
438
+ return False
439
+
440
+ # 尽量避免调用 os.stat,判断是否有自定义大小、时间规则,没有可以跳过这部分
441
+ check_arg = first_nonnone([min_size, max_size, min_ctime, max_ctime, min_mtime, max_mtime])
442
+ if check_arg is not None:
443
+ msg = os.stat(f)
444
+ if first_nonnone([min_size, max_size]) is not None:
445
+ size = File(f).size
446
+ if min_size is not None and size < min_size: return False
447
+ if max_size is not None and size > max_size: return False
448
+
449
+ if min_ctime or max_ctime:
450
+ file_ctime = datetime.fromtimestamp(msg.st_ctime)
451
+ if min_ctime and file_ctime < min_ctime: return False
452
+ if max_ctime and file_ctime > max_ctime: return False
453
+
454
+ if min_mtime or max_mtime:
455
+ file_mtime = datetime.fromtimestamp(msg.st_mtime)
456
+ if min_mtime and file_mtime < min_mtime: return False
457
+ if max_mtime and file_mtime > max_mtime: return False
458
+
459
+ if ignore_special:
460
+ parts = File(f).parts
461
+ if '.git' in parts or '$RECYCLE.BIN' in parts:
462
+ return False
463
+
464
+ if ignore_backup and File(f).backup_time:
465
+ return False
466
+
467
+ return True
468
+
469
+ root = os.path.abspath(root)
470
+ return list(filter(judge, files))
471
+
472
+
473
+ def filesmatch(patter, *, root=os.curdir, **kwargs) -> list:
474
+ r"""
475
+ :param patter:
476
+ str,
477
+ 不含*、?、<、>,普通筛选规则
478
+ 含*、?、<、>,支持Path.glob的通配符模式,使用**可以表示任意子目录
479
+ glob其实支持[0-9]这种用法,但是[、]在文件名中是合法的,
480
+ 为了明确要使用glob模式,我这里改成<>模式
481
+ **/*,是不会匹配到根目录的
482
+ re.Patter,正则筛选规则(这种方法会比较慢,但是很灵活) 或者其他有match成员函数的类也可以
483
+ 会获得当前工作目录下的所有文件相对路径,组成list
484
+ 对list的所有元素使用re.match进行匹配
485
+ list、tuple、set对象
486
+ 对每一个元素,递归调用filesmatch
487
+ 其他参数都是文件筛选功能,详见filesfilter中介绍
488
+ :return: 匹配到的所有存在的文件、文件夹,返回“相对路径”
489
+
490
+ TODO patter大小写问题?会导致匹配缺失的bug吗?
491
+
492
+ >> os.chdir('F:/work/filesmatch') # 工作目录
493
+
494
+ 1、普通匹配
495
+ >> filesmatch('a') # 匹配当前目录下的文件a,或者目录a
496
+ ['a']
497
+ >> filesmatch('b/a/')
498
+ ['b\\a']
499
+ >> filesmatch('b/..\\a/')
500
+ ['a']
501
+ >> filesmatch('c') # 不存在c则返回 []
502
+ []
503
+
504
+ 2、通配符模式
505
+ >> filesmatch('work/*.png') # 支持通配符
506
+ []
507
+ >> filesmatch('*.png') # 支持通配符
508
+ ['1.png', '1[.png', 'logo.png']
509
+ >> filesmatch('**/*.png') # 包含所有子目录下的png图片
510
+ ['1.png', '1[.png', 'logo.png', 'a\\2.png']
511
+ >> filesmatch('?.png')
512
+ ['1.png']
513
+ >> filesmatch('[0-9]/<0-9>.txt') # 用<0-9>表示[0-9]模式
514
+ ['[0-9]\\3.txt']
515
+
516
+ 3、正则模式
517
+ >> filesmatch(re.compile(r'\d\[\.png$'))
518
+ ['1[.png']
519
+
520
+ 4、其他高级用法
521
+ >> filesmatch('**/*', type_='dir', max_size=0) # 筛选空目录
522
+ ['b', '[0-9]']
523
+ >> filesmatch('**/*', type_='file', max_size=0) # 筛选空文件
524
+ ['b/a', '[0-9]/3.txt']
525
+ """
526
+ from pathlib import Path
527
+ root = os.path.abspath(root)
528
+
529
+ # 0 规则匹配
530
+ # patter = str(patter) # 200916周三14:59,这样会处理不了正则,要关掉
531
+ glob_chars_pos = strfind(patter, ('*', '?', '<', '>')) if isinstance(patter, str) else -1
532
+
533
+ # 1 普通文本匹配 (没有通配符,单文件查找)
534
+ if isinstance(patter, str) and glob_chars_pos == -1:
535
+ path = Path(os.path.join(root, patter))
536
+ if path: # 文件存在
537
+ p = str(path.resolve())
538
+ if p.startswith(root): p = p[len(root) + 1:]
539
+ res = [p]
540
+ else: # 文件不存在
541
+ res = []
542
+ # 2 glob通配符匹配
543
+ elif isinstance(patter, str) and glob_chars_pos != -1:
544
+ patter = patter.replace('\\', '/')
545
+ t = patter[:glob_chars_pos].rfind('/')
546
+ # 计算出这批文件实际所在的目录dirname
547
+ if t == -1: # 模式里没有套子文件夹
548
+ dirname, basename = root, patter
549
+ else: # 模式里有套子文件夹
550
+ dirname, basename = os.path.abspath(os.path.join(root, patter[:t])), patter[t + 1:]
551
+ basename = basename.replace('<', '[').replace('>', ']')
552
+ files = map(str, Path(dirname).glob(basename))
553
+
554
+ n = len(root) + 1
555
+ res = [(x[n:] if x.startswith(root) else x) for x in files]
556
+ # 3 正则匹配 (只要有match成员函数就行,不一定非要正则对象)
557
+ elif hasattr(patter, 'match'):
558
+ files = filesmatch('**/*', root=root)
559
+ res = list(filter(lambda x: patter.match(x), files))
560
+ # 4 list等迭代对象
561
+ elif isinstance(patter, (list, tuple, set)):
562
+ res = []
563
+ for p in patter: res += filesmatch(p, root=root)
564
+ # 5 可调用对象
565
+ elif callable(patter):
566
+ from pyxllib.file.specialist import XlPath
567
+ res = [f.relpath(root).as_posix() for f in XlPath(root).rglob('*') if patter(f)]
568
+ else:
569
+ raise TypeError
570
+
571
+ # 2 filetype的筛选
572
+ res = filesfilter(res, root=root, **kwargs)
573
+
574
+ return [x.replace('\\', '/') for x in res]
575
+
576
+
577
+ def filesdel(path, **kwargs):
578
+ """删除文件或文件夹
579
+ 支持filesfilter的筛选规则
580
+ """
581
+ for f in filesmatch(path, **kwargs):
582
+ if os.path.isfile(f):
583
+ os.remove(f)
584
+ else:
585
+ shutil.rmtree(f)
586
+ # TODO 确保删除后再执行后续代码 但是一直觉得这样写很别扭
587
+ while os.path.exists(f): pass
588
+
589
+
590
+ def _files_copy_move_base(src, dst, filefunc, dirfunc,
591
+ *, if_exists=None, treeroot=None, **kwargs):
592
+ # 1 辅助函数
593
+ def proc_onefile(f, dst):
594
+ # dprint(f, dst)
595
+ # 1 解析dst参数:对文件或目录不同情况做预处理
596
+ # (输入的时候dst_可以只是目标的父目录,要推算出实际要存储的目标名)
597
+ if os.path.isfile(f):
598
+ if os.path.isdir(dst) or dst[-1] in ('/', '\\'):
599
+ dst = os.path.join(dst, os.path.basename(f))
600
+ func = filefunc
601
+ else:
602
+ if dst[0] in ('/', '\\'):
603
+ dst = os.path.join(dst, os.path.basename(f))
604
+ func = dirfunc
605
+
606
+ # 2 根据目标是否已存在和if_exists分类处理
607
+ File(dst).ensure_parent()
608
+ # 目前存在,且不是把文件移向文件夹的操作
609
+ if os.path.exists(dst):
610
+ # 根据if_exists参数情况分类处理
611
+ if if_exists is None: # 智能判断
612
+ if not filescmp(f, dst): # 如果内容不同则backup
613
+ File(dst).backup(move=True)
614
+ func(f, dst)
615
+ elif os.path.abspath(f).lower() == os.path.abspath(dst).lower():
616
+ # 如果内容相同,再判断其是否实际是一个文件,则调用重命名功能
617
+ os.rename(f, dst)
618
+ elif if_exists == 'backup':
619
+ File(dst).backup(move=True)
620
+ func(f, dst)
621
+ elif if_exists == 'replace':
622
+ filesdel(dst)
623
+ func(f, dst)
624
+ elif if_exists == 'ignore':
625
+ pass # 跳过,不处理
626
+ else:
627
+ raise ValueError
628
+ else:
629
+ func(f, dst) # TODO 这里有bug \2020LaTeX\C春季教材\初数\初一上\Word+外包商原稿
630
+
631
+ # 2 主体代码
632
+ files = filesmatch(src, **kwargs)
633
+
634
+ if len(files) == 1:
635
+ proc_onefile(files[0], dst)
636
+ elif len(files) > 1: # 多文件模式拆解为单文件模式操作
637
+ # 如果设置了 treeroot,这里要预处理下
638
+ if treeroot:
639
+ treeroot = filesmatch(treeroot)[0]
640
+ if treeroot[-1] not in ('/', '\\'):
641
+ treeroot += '/'
642
+ n = len(treeroot) if treeroot else 0
643
+ if treeroot: treeroot = treeroot.replace('\\', '/')
644
+
645
+ # 迭代操作
646
+ for f in files:
647
+ dst_ = dst
648
+ if treeroot and f.startswith(treeroot):
649
+ dst_ = os.path.join(dst, f[n:])
650
+ proc_onefile(f, dst_)
651
+
652
+
653
+ def filescopy(src, dst, *, if_exists=None, treeroot=None, **kwargs):
654
+ r"""会自动添加不存在的目录的拷贝
655
+
656
+ :param src: 要处理的目标
657
+ 'a',复制文件a,或者整个文件夹a
658
+ 'a/*.txt',复制文件夹下所有的txt文件
659
+ 更多匹配模式详见 filesmatch
660
+ :param dst: 移到目标位置
661
+ 'a',
662
+ 如果a是已存在的目录,效果同'a/'
663
+ 如果是已存在的文件,且src只有一个要复制的文件,也是合法的。否则报错
664
+ 错误类型包括,把一个目录复制到已存在的文件
665
+ 把多个文件复制到已存在的文件
666
+ 如果a不存在,则
667
+ src只是一个待复制的文件时是合法的
668
+ 'a/',(可以省略写具体值,只写父级目录)将src匹配到的所有文件,放到目标a目录下
669
+ :param if_exists: backup和replace含智能处理,如果内容相同则直接ignore
670
+ 'ignore',跳过
671
+ 'backup'(默认),备份
672
+ 注意多文件操作时,来源不同的文件夹可能有同名文件
673
+ 'replace',强制替换
674
+ :param treeroot: 输入一个目录名开启该功能选项 (此模式下dst末尾强制要有一个'/')
675
+ 对src中匹配到的所有文件,都会去掉treeroot的父目录前缀
676
+ 然后将剩下文件的所有相对路径结构,拷贝到dst目录下
677
+ 示例:将a目录下所有png图片原结构拷贝到b目录下
678
+ filescopy('a/**/*.png', 'b/', if_exists='replace', treeroot='a')
679
+ 友情提示:treeroot要跟src使用同样的相对或绝对路径值,否则可能出现意外错误
680
+
681
+ >> filescopy('filesmatch/**/*.png', 'filesmatch+/', treeroot='filesmatch')
682
+ filesmatch: 1.png,a/2.png -> filesmatch+:1.png,a/2.png
683
+
684
+ >> filescopy('filesmatch/**/*.png', 'filesmatch+/')
685
+ filesmatch: 1.png,a/2.png -> filesmatch+:1.png,2.png
686
+
687
+ TODO filescopy和filesmove还是有瑕疵和效率问题的,有空要继续优化
688
+ """
689
+ return _files_copy_move_base(src, dst, shutil.copy2, shutil.copytree,
690
+ if_exists=if_exists, treeroot=treeroot, **kwargs)
691
+
692
+
693
+ def filesmove(src, dst, *, if_exists=None, treeroot=None, **kwargs):
694
+ r"""与filescopy高度相同,见filescopy文档
695
+
696
+ >> filesmove('a.xslx', 'A.xlsx', if_exists='replace') # 等价于 os.rename('a.xlsx', 'A.xlsx')
697
+ """
698
+ return _files_copy_move_base(src, dst, shutil.move, shutil.move,
699
+ if_exists=if_exists, treeroot=treeroot, **kwargs)
700
+
701
+
702
+ def refinepath(s, reserve=''):
703
+ """
704
+ :param reserve: 保留的字符,例如输入'*?',会保留这两个字符作为通配符
705
+ """
706
+ if not s: return s
707
+ # 1 去掉路径中的不可见字符,注意这里第1个参数里有一个不可见字符!别乱动这里的代码!
708
+ s = s.replace(chr(8234), '')
709
+ chars = set(r'\/:*?"<>|') - set(reserve)
710
+ for ch in chars: # windows路径中不能包含的字符
711
+ s = s.replace(ch, '')
712
+
713
+ # 2 去除目录、文件名前后的空格
714
+ s = re.sub(r'\s+([/\\])', r'\1', s)
715
+ s = re.sub(r'([/\\])\s+', r'\1', s)
716
+
717
+ return s
718
+
719
+
720
+ def writefile(ob, path='', *, encoding='utf8', if_exists='backup', suffix=None, root=None, etag=None) -> str:
721
+ """往文件path写入ob内容
722
+ :param ob: 写入的内容
723
+ 如果要写txt文本文件且ob不是文本对象,只会进行简单的字符串化
724
+ :param path: 写入的文件名,使用空字符串时,会使用etag值
725
+ :param encoding: 强制写入的编码
726
+ :param if_exists: 如果文件已存在,要进行的操作
727
+ :param suffix: 文件扩展名
728
+ 以'.'为开头,设置“候补扩展名”,即只在fn没有指明扩展名时,会采用
729
+ :param root: 相对位置
730
+ :return: 返回写入的文件名,这个主要是在写临时文件时有用
731
+ """
732
+ if etag is None: etag = (not path)
733
+ if path == '': path = ...
734
+ f = File(path, root, suffix=suffix).write(ob, encoding=encoding, if_exists=if_exists)
735
+ if etag:
736
+ f = f.rename(get_etag(str(f)))
737
+ return str(f)
738
+
739
+
740
+ def merge_dir(src, dst, if_exists='skip'):
741
+ """ 将src目录下的数据拷贝到dst目录
742
+ """
743
+
744
+ def func(p1, p2):
745
+ p1.copy(p2, if_exists=if_exists)
746
+
747
+ # 只拷文件和空目录,不然逻辑会乱
748
+ Dir(src).select('**/*', type_='dir', max_size=0).select('**/*', type_='file').procpaths(func, ref_dir=dst)
749
+
750
+
751
+ def extract_files(src, dst, pattern, if_exists='replace'):
752
+ """ 提取满足pattern模式的文件
753
+ """
754
+ d1, d2 = Dir(src), Dir(dst)
755
+ files = d1.select(pattern).subs
756
+ for f in files:
757
+ p1, p2 = File(d1 / f), File(d2 / f)
758
+ p1.copy(p2, if_exists=if_exists)
759
+
760
+
761
+ def file_or_dir_size(path):
762
+ if os.path.isfile(path):
763
+ return File(path).size
764
+ elif os.path.isdir(path):
765
+ return Dir(path).size
766
+ else:
767
+ return 0
768
+
769
+
770
+ def reduce_dir_depth(srcdir, unwrap=999):
771
+ """ 精简冗余嵌套的目录
772
+
773
+ 比如a目录下只有一个文件:a/b/1.txt,
774
+ 那么可以精简为a/1.txt,不需要多嵌套一个b目录
775
+
776
+ :param srcdir: 要处理的目录
777
+ :param unwrap: 打算解开的层数,未设置则会尽可能多解开
778
+ """
779
+ import tempfile
780
+ root = p = XlPath(srcdir)
781
+ depth = 0
782
+
783
+ ps = list(p.glob('*'))
784
+ while len(ps) == 1 and ps[0].is_dir() and depth < unwrap:
785
+ depth += 1
786
+ p = ps[0]
787
+ ps = list(p.glob('*'))
788
+
789
+ if depth:
790
+ # 注意这里技巧,为了避免多层目录里会有相对同名的目录,导致出现不可预料的bug
791
+ # 算法原理是把要搬家的那层目录里的文件先移到临时文件,然后把原目录树结构删除后,再报临时文件的文件移回来
792
+ tmpdir = tempfile.mktemp()
793
+ shutil.move(str(p), str(tmpdir))
794
+ if depth > 1:
795
+ shutil.rmtree(next(root.glob('*')))
796
+
797
+ for pp in XlPath(tmpdir).glob('*'):
798
+ shutil.move(str(pp), str(root))
799
+ shutil.rmtree(tmpdir)