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
pyxllib/prog/pupil.py CHANGED
@@ -1,1197 +1,1197 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- # @Author : 陈坤泽
4
- # @Email : 877362867@qq.com
5
- # @Date : 2021/06/03 23:21
6
-
7
-
8
- """ 封装一些代码开发中常用的功能,工程组件 """
9
-
10
- from collections import Counter
11
- from concurrent.futures import ThreadPoolExecutor
12
- from urllib.parse import urlparse
13
- import builtins
14
- import datetime
15
- import functools
16
- import hashlib
17
- import inspect
18
- import io
19
- import itertools
20
- import json
21
- import logging
22
- import math
23
- import os
24
- import pprint
25
- import queue
26
- import random
27
- import re
28
- import signal
29
- import socket
30
- import subprocess
31
- import sys
32
- import tempfile
33
- import threading
34
- import time
35
- import traceback
36
-
37
- from pyxllib.prog.newbie import classproperty, typename
38
-
39
-
40
- # from loguru import logger
41
-
42
-
43
- def system_information():
44
- """主要是测试一些系统变量值,顺便再演示一次Timer用法"""
45
-
46
- def pc_messages():
47
- """演示如何获取当前操作系统的PC环境数据"""
48
- # fqdn:fully qualified domain name
49
- print('1、socket.getfqdn() :', socket.getfqdn()) # 完全限定域名,可以理解成pcname,计算机名
50
- # 注意py的很多标准库功能本来就已经处理了不同平台的问题,尽量用标准库而不是自己用sys.platform作分支处理
51
- print('2、sys.platform :', sys.platform) # 运行平台,一般是win32和linux
52
- # li = os.getenv('PATH').split(os.path.pathsep) # 环境变量名PATH,win中不区分大小写,linux中区分大小写必须写成PATH
53
- # print("3、os.getenv('PATH'):", f'数量={len(li)},', pprint.pformat(li, 4))
54
-
55
- def executable_messages():
56
- """演示如何获取被执行程序相关的数据"""
57
- print('1、sys.executable :', sys.executable) # 当前被执行脚本位置
58
- print('2、sys.version :', sys.version) # python的版本
59
- print('3、os.getcwd() :', os.getcwd()) # 获得当前工作目录
60
- print('4、gettempdir() :', tempfile.gettempdir()) # 临时文件夹位置
61
- # print('5、sys.path :', f'数量={len(sys.path)},', pprint.pformat(sys.path, 4)) # import绝对位置包的搜索路径
62
-
63
- print('【pc_messages】')
64
- pc_messages()
65
- print('【executable_messages】')
66
- executable_messages()
67
-
68
-
69
- def is_url(arg):
70
- """输入是一个字符串,且值是一个合法的url"""
71
- if not isinstance(arg, str): return False
72
- try:
73
- result = urlparse(arg)
74
- return all([result.scheme, result.netloc])
75
- except ValueError:
76
- return False
77
-
78
-
79
- def is_file(arg, exists=True):
80
- """相较于标准库的os.path.isfile,对各种其他错误类型也会判False
81
-
82
- :param exists: arg不仅需要是一个合法的文件名,还要求其实际存在
83
- 设为False,则只判断文件名合法性,不要求其一定要存在
84
- """
85
- if not isinstance(arg, str): return False
86
- if len(arg) > 500: return False
87
- if not exists:
88
- raise NotImplementedError
89
- return os.path.isfile(arg)
90
-
91
-
92
- def len_in_dim2_min(arr):
93
- """ 计算类List结构在第2维上的最小长度
94
-
95
- >>> len_in_dim2([[1,1], [2], [3,3,3]])
96
- 3
97
-
98
- >>> len_in_dim2([1, 2, 3]) # TODO 是不是应该改成0合理?但不知道牵涉到哪些功能影响
99
- 1
100
- """
101
- if not isinstance(arr, (list, tuple)):
102
- raise TypeError('类型错误,不是list构成的二维数组')
103
-
104
- # 找出元素最多的列
105
- column_num = math.inf
106
- for i, item in enumerate(arr):
107
- if isinstance(item, (list, tuple)): # 该行是一个一维数组
108
- column_num = min(column_num, len(item))
109
- else: # 如果不是数组,是指单个元素,当成1列处理
110
- column_num = min(column_num, 1)
111
- break # 只要有个1,最小长度就一定是1了
112
-
113
- return column_num
114
-
115
-
116
- def print2string(*args, **kwargs):
117
- """https://stackoverflow.com/questions/39823303/python3-print-to-string"""
118
- output = io.StringIO()
119
- print(*args, file=output, **kwargs)
120
- contents = output.getvalue()
121
- output.close()
122
- return contents
123
-
124
-
125
- class EmptyPoolExecutor:
126
- """伪造一个类似concurrent.futures.ThreadPoolExecutor、ProcessPoolExecutor的接口类
127
- 用来检查多线程、多进程中的错误
128
-
129
- 即并行中不会直接报出每个线程的错误,只能串行执行才好检查
130
- 但是两种版本代码来回修改很麻烦,故设计此类,只需把
131
- concurrent.futures.ThreadPoolExecutor 暂时改为 EmptyPoolExecutor 进行调试即可
132
- """
133
-
134
- def __init__(self, *args, **kwargs):
135
- """参数并不需要实际处理,并没有真正并行,而是串行执行"""
136
- self._work_queue = queue.Queue()
137
-
138
- def submit(self, func, *args, **kwargs):
139
- """执行函数"""
140
- func(*args, **kwargs)
141
-
142
- def shutdown(self):
143
- # print('并行执行结束')
144
- pass
145
-
146
-
147
- def xlwait(func, condition=bool, *, limit=None, interval=1):
148
- """ 不断重复执行func,直到得到满足condition条件的期望值
149
-
150
- :param condition: 退出等待的条件,默认为bool真值
151
- :param limit: 重复执行的上限时间(单位 秒),默认一直等待
152
- :param interval: 重复执行间隔 (单位 秒)
153
-
154
- """
155
- t = time.time()
156
- while True:
157
- res = func()
158
- if condition(res):
159
- return res
160
- elif limit and (time.time() - t > limit):
161
- return res # 超时也返回目前得到的结果
162
- time.sleep(interval)
163
-
164
-
165
- class DictTool:
166
- @classmethod
167
- def json_loads(cls, label, default=None):
168
- """ 尝试从一段字符串解析为字典
169
-
170
- :param default: 如果不是字典时的处理策略
171
- None,不作任何处理
172
- str,将原label作为defualt这个键的值来存储
173
- :return: s为非字典结构时返回空字典
174
-
175
- >>> DictTool.json_loads('123', 'text')
176
- {'text': '123'}
177
- >>> DictTool.json_loads('[123, 456]', 'text')
178
- {'text': '[123, 456]'}
179
- >>> DictTool.json_loads('{"a": 123}', 'text')
180
- {'a': 123}
181
- """
182
- labelattr = dict()
183
- try:
184
- data = json.loads(label)
185
- if isinstance(data, dict):
186
- labelattr = data
187
- except json.decoder.JSONDecodeError:
188
- pass
189
- if not labelattr and isinstance(default, str):
190
- labelattr[default] = label
191
- return labelattr
192
-
193
- @classmethod
194
- def or_(cls, *args):
195
- """ 合并到新字典
196
-
197
- 左边字典有的key,优先取左边,右边不会覆盖。
198
- 如果要覆盖效果,直接用 d1.update(d2)功能即可。
199
-
200
- :return: args[0] | args[1] | ... | args[-1].
201
- """
202
- res = {}
203
- cls.ior(res, *args)
204
- return res
205
-
206
- @classmethod
207
- def ior(cls, dict_, *args):
208
- """ 合并到第1个字典
209
-
210
- :return: dict_ |= (args[0] | args[1] | ... | args[-1]).
211
-
212
- 220601周三15:45,默认已有对应key的话,值是不覆盖的,如果要覆盖,直接用update就行了,不需要这个接口
213
- 所以把3.9的|=功能关掉
214
- """
215
- # if sys.version_info.major == 3 and sys.version_info.minor >= 9:
216
- # for x in args:
217
- # dict_ |= x
218
- # else: # 旧版本py手动实现一个兼容功能
219
- for x in args:
220
- for k, v in x.items():
221
- # 220729周五21:21,又切换成dict_有的不做替换
222
- if k not in dict_:
223
- dict_[k] = v
224
- # dict_[k] = v
225
-
226
- @classmethod
227
- def sub(cls, dict_, keys):
228
- """ 删除指定键值(不存在的跳过,不报错)
229
-
230
- inplace subtraction
231
-
232
- :param keys: 可以输入另一个字典,也可以输入一个列表表示要删除的键值清单
233
-
234
- :return: dict2 = dict_ - keys
235
- """
236
- if isinstance(keys, dict):
237
- keys = keys.keys()
238
-
239
- return {k: v for k, v in dict_.items() if k not in keys}
240
-
241
- @classmethod
242
- def isub(cls, dict_, keys):
243
- """ 删除指定键值(不存在的跳过,不报错)
244
-
245
- inplace subtraction
246
-
247
- keys可以输入另一个字典,也可以输入一个列表表示要删除的键值清单
248
-
249
- 效果相当于 dict_ -= keys
250
- """
251
- if isinstance(keys, dict):
252
- keys = keys.keys()
253
-
254
- for k in keys:
255
- if k in dict_:
256
- del dict_[k]
257
-
258
-
259
- class EnchantCvt:
260
- """ 把类_cls的功能绑定到类cls里
261
- 根源_cls里的实现类型不同,到cls需要呈现的接口形式不同,有很多种不同的转换形式
262
- 每个分支里,随附了getattr目标函数的一般默认定义模板
263
- 用_self、_cls表示dst_cls,区别原cls类的self、cls标记
264
- """
265
-
266
- @staticmethod
267
- def staticmethod2objectmethod(cls, _cls, x):
268
- # 目前用的最多的转换形式
269
- # @staticmethod
270
- # def func1(_self, *args, **kwargs): ...
271
- setattr(_cls, x, getattr(cls, x))
272
-
273
- @staticmethod
274
- def staticmethod2property(cls, _cls, x):
275
- # @staticmethod
276
- # def func2(_self): ...
277
- setattr(_cls, x, property(getattr(cls, x)))
278
-
279
- @staticmethod
280
- def staticmethod2classmethod(cls, _cls, x):
281
- # @staticmethod
282
- # def func3(_cls, *args, **kwargs): ...
283
- setattr(_cls, x, classmethod(getattr(cls, x)))
284
-
285
- @staticmethod
286
- def staticmethod2classproperty(cls, _cls, x):
287
- # @staticmethod
288
- # def func4(_cls): ...
289
- setattr(_cls, x, classproperty(getattr(cls, x)))
290
-
291
- @staticmethod
292
- def classmethod2objectmethod(cls, _cls, x):
293
- # @classmethod
294
- # def func5(cls, _self, *args, **kwargs): ...
295
- setattr(_cls, x, lambda *args, **kwargs: getattr(cls, x)(*args, **kwargs))
296
-
297
- @staticmethod
298
- def classmethod2property(cls, _cls, x):
299
- # @classmethod
300
- # def func6(cls, _self): ...
301
- setattr(_cls, x, lambda *args, **kwargs: property(getattr(cls, x)(*args, **kwargs)))
302
-
303
- @staticmethod
304
- def classmethod2classmethod(cls, _cls, x):
305
- # @classmethod
306
- # def func7(cls, _cls, *args, **kwargs): ...
307
- setattr(_cls, x, lambda *args, **kwargs: classmethod(getattr(cls, x)(*args, **kwargs)))
308
-
309
- @staticmethod
310
- def classmethod2classproperty(cls, _cls, x):
311
- # @classmethod
312
- # def func8(cls, _cls): ...
313
- setattr(_cls, x, lambda *args, **kwargs: classproperty(getattr(cls, x)(*args, **kwargs)))
314
-
315
- @staticmethod
316
- def staticmethod2modulefunc(cls, _cls, x):
317
- # @staticmethod
318
- # def func9(*args, **kwargs): ...
319
- setattr(_cls, x, getattr(cls, x))
320
-
321
- @staticmethod
322
- def classmethod2modulefunc(cls, _cls, x):
323
- # @classmethod
324
- # def func10(cls, *args, **kwargs): ...
325
- setattr(_cls, x, lambda *args, **kwargs: getattr(cls, x)(*args, **kwargs))
326
-
327
- @staticmethod
328
- def to_moduleproperty(cls, _cls, x):
329
- # 理论上还有'to_moduleproperty'的转换模式
330
- # 但这个很容易引起歧义,是应该存一个数值,还是动态计算?
331
- # 如果是动态计算,可以使用modulefunc的机制显式执行,更不容易引起混乱。
332
- # 从这个分析来看,是不需要实现'2moduleproperty'的绑定体系的。py标准语法本来也就没有module @property的概念。
333
- raise NotImplementedError
334
-
335
-
336
- class EnchantBase:
337
- """
338
- 一些三方库的类可能功能有限,我们想做一些扩展。
339
- 常见扩展方式,是另外写一些工具函数,但这样就不“面向对象”了。
340
- 如果要“面向对象”,需要继承已有的类写新类,但如果组件特别多,开发难度是很大的。
341
- 比如excel就有单元格、工作表、工作薄的概念。
342
- 如果自定义了新的单元格,那是不是也要自定义新的工作表、工作薄,才能默认引用到自己的单元格类。
343
- 这个看着很理想,其实并没有实际开发可能性。
344
- 所以我想到一个机制,把额外函数形式的扩展功能,绑定到原有类上。
345
- 这样原来的功能还能照常使用,但多了很多我额外扩展的成员方法,并且也没有侵入原三方库的源码
346
- 这样一种设计模式,简称“绑定”。换个逼格高点的说法,就是“强化、附魔”的过程,所以称为Enchant。
347
- 这个功能应用在cv2、pillow、fitz、openpyxl,并在win32com中也有及其重要的应用。
348
- """
349
-
350
- @classmethod
351
- def check_enchant_names(cls, classes, names=None, *, white_list=None, ignore_case=False):
352
- """
353
- :param list classes: 不能跟这里列出的模块、类的成员重复
354
- :param list|str|tuple names: 要检查的名称清单
355
- :param white_list: 白名单,这里面的名称不警告
356
- 在明确要替换三方库标准功能的时候,可以使用
357
- :param ignore_case: 忽略大小写
358
- """
359
- exist_names = {x.__name__: set(dir(x)) for x in classes}
360
- if names is None:
361
- names = {x for x in dir(cls) if x[:2] != '__'} \
362
- - {'check_enchant_names', '_enchant', 'enchant'}
363
-
364
- white_list = set(white_list) if white_list else {}
365
-
366
- if ignore_case:
367
- names = {x.lower() for x in names}
368
- for k, values in exist_names.items():
369
- exist_names[k] = {x.lower() for x in exist_names[k]}
370
- white_list = {x.lower() for x in white_list}
371
-
372
- for name, k in itertools.product(names, exist_names):
373
- if name in exist_names[k] and name not in white_list:
374
- print(f'警告!同名冲突! {k}.{name}')
375
-
376
- return set(names)
377
-
378
- @classmethod
379
- def _enchant(cls, _cls, names, cvt=EnchantCvt.staticmethod2objectmethod):
380
- """ 这个框架是支持classmethod形式的转换的,但推荐最好还是用staticmethod,可以减少函数嵌套层数,提高效率 """
381
- for name in set(names):
382
- cvt(cls, _cls, name)
383
-
384
- @classmethod
385
- def enchant(cls):
386
- raise NotImplementedError
387
-
388
-
389
- def check_install_package(package, speccal_install_name=None, *, user=False):
390
- """ https://stackoverflow.com/questions/12332975/installing-python-module-within-code
391
-
392
- :param speccal_install_name: 注意有些包使用名和安装名不同,比如pip install python-opencv,使用时是import cv2,
393
- 此时应该写 check_install_package('cv2', 'python-opencv')
394
-
395
- TODO 不知道频繁调用这个,会不会太影响性能,可以想想怎么提速优化?
396
- 注意不要加@RunOnlyOnce,亲测速度会更慢三倍
397
-
398
- 警告: 不要在频繁调用的底层函数里使用 check_install_package
399
- 如果是module级别的还好,调几次其实性能影响微乎其微
400
- 但在频繁调用的函数里使用,每百万次还是要额外的0.5秒开销的
401
- """
402
- try:
403
- __import__(package)
404
- except ModuleNotFoundError:
405
- cmds = [sys.executable, "-m", "pip", "install"]
406
- if user: cmds.append('--user')
407
- cmds.append(speccal_install_name if speccal_install_name else package)
408
- subprocess.check_call(cmds)
409
-
410
-
411
- def run_once(distinct_mode=0, *, limit=1, debug=False):
412
- """ 装饰器,装饰的函数在一次程序里其实只会运行一次
413
-
414
- :param int|str distinct_mode:
415
- 0,默认False,不区分输入的参数值(包括cls、self),强制装饰的函数只运行一次
416
- 'str',设为True或1时,仅以字符串化的差异判断是否是重复调用,参数不同,会判断为不同的调用,每种调用限制最多执行limit次
417
- 'id,str',在'str'的基础上,第一个参数使用id代替。一般用于类方法、对象方法的装饰。
418
- 不考虑类、对象本身的内容改变,只要还是这个类或对象,视为重复调用。
419
- 'ignore,str',首参数忽略,第2个开始的参数使用str格式化
420
- 用于父类某个方法,但是子类继承传入cls,原本id不同会重复执行
421
- 使用该模式,首参数会ignore忽略,只比较第2个开始之后的参数
422
- func等callable类型的对象也行,是使用run_once装饰器的简化写法
423
- :param limit: 默认只会执行一次,该参数可以提高限定的执行次数,一般用不到,用于兼容旧的 limit_call_number 装饰器
424
- returns: 返回decorator
425
- """
426
- if callable(distinct_mode):
427
- # @run_once,没写括号的时候去装饰一个函数,distinct_mode传入的是一个函数func
428
- # 使用run_once本身的默认值
429
- return run_once()(distinct_mode)
430
-
431
- def get_tag(args, kwargs):
432
- if not distinct_mode:
433
- ls = tuple()
434
- elif distinct_mode == 'str':
435
- ls = (str(args), str(kwargs))
436
- elif distinct_mode == 'id,str':
437
- ls = (id(args[0]), str(args[1:]), str(kwargs))
438
- elif distinct_mode == 'ignore,str':
439
- ls = (str(args[1:]), str(kwargs))
440
- else:
441
- raise ValueError
442
- return ls
443
-
444
- def decorator(func):
445
- counter = {} # 映射到一个[cnt, last_result]
446
-
447
- def wrapper(*args, **kwargs):
448
- tag = get_tag(args, kwargs)
449
- if tag not in counter:
450
- counter[tag] = [0, None]
451
- x = counter[tag]
452
- if x[0] < limit:
453
- res = func(*args, **kwargs)
454
- x = counter[tag] = [x[0] + 1, res]
455
-
456
- return x[1]
457
-
458
- return wrapper
459
-
460
- return decorator
461
-
462
-
463
- def set_default_args(*d_args, **d_kwargs):
464
- """ 增设默认参数
465
-
466
- 有时候需要调试一个函数,试跑一些参数结果,
467
- 但这些参数又不适合定为标准化的接口值,可以用这个函数设置
468
-
469
- 参数加载、覆盖顺序(越后面的优先级越高)
470
- 1、函数定义阶段设置的默认值
471
- 2、装饰器定义的参数 d_args、d_kwargs
472
- 3、运行阶段明确指定的参数,即传入的f_args、f_kwargs
473
- """
474
-
475
- def decorator(func):
476
- def wrapper(*f_args, **f_kwargs):
477
- args = f_args + d_args
478
- d_kwargs.update(f_kwargs) # 优先使用外部传参传入的值,再用装饰器里扩展的默认值
479
- return func(*args, **d_kwargs)
480
-
481
- return wrapper
482
-
483
- return decorator
484
-
485
-
486
- def utc_now(offset_hours=8, microseconds=0):
487
- """ 有的机器可能本地时间设成了utc0,可以用这个方式,获得准确的utc8时间
488
-
489
- :param microseconds: 微秒,如果不指定(None),就是当前时间的微秒
490
- """
491
- dt = datetime.datetime.utcnow()
492
- dt += datetime.timedelta(hours=offset_hours)
493
-
494
- if microseconds is not None:
495
- dt = dt.replace(microsecond=microseconds)
496
-
497
- return dt
498
-
499
-
500
- def utc_now2(offset_hours=8):
501
- """ 转字符串格式 """
502
- return utc_now().isoformat(' ', timespec='seconds')
503
-
504
-
505
- def utc_timestamp(offset_hours=8):
506
- """ mysql等数据库支持的日期格式
507
- """
508
- return utc_now(offset_hours).isoformat(' ', timespec='seconds')
509
-
510
-
511
- class Timeout:
512
- """ 对函数等待执行的功能,限制运行时间
513
-
514
- 【实现思路】
515
- 1、最简单的方式是用signal.SIGALRM实现(包括三方库timeout-decorator也有这个局限)
516
- https://stackoverflow.com/questions/2281850/timeout-function-if-it-takes-too-long-to-finish
517
- 但是这个不支持windows系统~~
518
- 2、那windows和linux通用的做法,就是把原执行函数变成一个子线程来运行
519
- https://stackoverflow.com/questions/21827874/timeout-a-function-windows
520
- 但是,又在onenote的win32com发现,有些功能没办法丢到子线程里,会出问题
521
- 而且使用子线程,也没法做出支持with上下文语法的功能了
522
- 3、于是就有了当前我自己搞出的一套机制
523
- 是用一个Timer计时器子线程计时,当timeout超时,使用信号机制给主线程抛出一个异常
524
- ① 注意,不能子线程直接抛出异常,这样影响不了主线程
525
- ② 也不能直接抛出错误signal,这样会强制直接中断程序。应该抛出TimeoutError,让后续程序进行超时逻辑的处理
526
- ③ 这里是让子线程抛出信号,主线程收到信号后,再抛出TimeoutError
527
-
528
- 注意:这个函数似乎不支持多线程
529
- """
530
-
531
- def __init__(self, seconds):
532
- self.seconds = seconds
533
- self.alarm = None
534
-
535
- def __call__(self, func):
536
- @functools.wraps(func)
537
- def wrapper(*args, **kwargs):
538
- # 1 如果超时,主线程收到信号会执行的功能
539
- def overtime(signum, frame):
540
- raise TimeoutError(f'function [{func.__name__}] timeout [{self.seconds} seconds] exceeded!')
541
-
542
- signal.signal(signal.SIGABRT, overtime)
543
-
544
- # 2 开一个子线程计时器,超时的时候发送信号
545
- def send_signal():
546
- signal.raise_signal(signal.SIGABRT)
547
-
548
- alarm = threading.Timer(self.seconds, send_signal)
549
- alarm.start()
550
-
551
- # 3 执行主线程功能
552
- res = func(*args, **kwargs)
553
- alarm.cancel() # 正常执行完则关闭计时器
554
-
555
- return res
556
-
557
- return wrapper
558
-
559
- def __enter__(self):
560
- if self.seconds == 0: # 可以设置0来关闭超时功能
561
- return
562
-
563
- def overtime(signum, frame):
564
- raise TimeoutError(f'with 上下文代码块运行超时 > [{self.seconds} 秒]')
565
-
566
- signal.signal(signal.SIGABRT, overtime)
567
-
568
- def send_signal():
569
- signal.raise_signal(signal.SIGABRT)
570
-
571
- # 挂起一个警告器,如果"没人"管它,self.seconds就会抛出错误
572
- self.alarm = threading.Timer(self.seconds, send_signal)
573
- self.alarm.start()
574
-
575
- def __exit__(self, exc_type, exc_val, exc_tb):
576
- if self.seconds == 0:
577
- return
578
-
579
- # with已经运行完了,马上关闭警告器
580
- self.alarm.cancel()
581
-
582
-
583
- @run_once('str')
584
- def inject_members(from_obj, to_obj, member_list=None, *,
585
- check=False, ignore_case=False,
586
- white_list=None, black_list=None):
587
- """ 将from_obj的方法注入到to_obj中
588
-
589
- 一般用于类继承中,将子类from_obj的新增的成员方法,添加回父类to_obj中
590
- 反经合道:这样看似很违反常理,父类就会莫名其妙多出一些可操作的成员方法。
591
- 但在某些时候能保证面向对象思想的情况下,大大简化工程代码开发量。
592
- 也可以用于模块等方法的添加
593
-
594
- :param from_obj: 一般是一个类用于反向继承的方法来源,但也可以是模块等任意对象。
595
- 注意py一切皆类,一个class定义的类本质也是type类定义出的一个对象
596
- 所以这里概念上称为obj才是准确的,反而是如果叫from_cls不太准确,虽然这个方法主要确实都是用于class类
597
- :param to_obj: 同from_obj,要被注入方法的对象
598
- :param Sequence[str] member_list: 手动指定的成员方法名,可以不指定,自动生成
599
- :param check: 检查重名方法
600
- :param ignore_case: 忽略方法的大小写情况,一般用于win32com接口
601
- :param Sequence[str] white_list: 白名单。无论是否重名,这里列出的方法都会被添加
602
- :param Sequence[str] black_list: 黑名单。这里列出的方法不会被添加
603
-
604
- # 把XlDocxTable的成员方法绑定到docx.table.Table里
605
- >> inject_members(XlDocxTable, docx.table.Table)
606
-
607
- 240826周一,其他可参考学习的三方现成工具库:from fastcore.foundation import patch
608
- """
609
- # 1 整理需要注入的方法清单
610
- dst = set(dir(to_obj))
611
- if ignore_case:
612
- dst = {x.lower() for x in dst}
613
-
614
- if member_list:
615
- src = set(member_list)
616
- else:
617
- if ignore_case:
618
- src = {x for x in dir(from_obj) if (x.lower() not in dst)}
619
- else:
620
- src = set(dir(from_obj)) - dst
621
-
622
- # 2 微调
623
- if white_list:
624
- src |= set(white_list)
625
- if black_list:
626
- src -= set(black_list)
627
-
628
- # 3 注入方法
629
- for x in src:
630
- setattr(to_obj, x, getattr(from_obj, x))
631
- if check and (x in dst or (ignore_case and x.lower() in dst)):
632
- logging.warning(f'Conflict of the same name! {to_obj}.{x}')
633
-
634
-
635
- def __debug系列():
636
- pass
637
-
638
-
639
- def func_input_message(depth=2) -> dict:
640
- """假设调用了这个函数的函数叫做f,这个函数会获得
641
- 调用f的时候输入的参数信息,返回一个dict,键值对为
642
- fullfilename:完整文件名
643
- filename:文件名
644
- funcname:所在函数名
645
- lineno:代码所在行号
646
- comment:尾巴的注释
647
- depth:深度
648
- funcnames:整个调用过程的函数名,用/隔开,例如...
649
-
650
- argnames:变量名(list),这里的变量名也有可能是一个表达式
651
- types:变量类型(list),如果是表达式,类型指表达式的运算结果类型
652
- argvals:变量值(list)
653
-
654
- 这样以后要加新的键值对也很方便
655
-
656
- :param depth: 需要分析的层级
657
- 0,当前func_input_message函数的参数输入情况
658
- 1,调用func_input_message的函数 f 参数输入情况
659
- 2,调用 f 的函数 g ,g的参数输入情况
660
-
661
- 参考: func_input_message 的具体使用方法可以参考 dformat 函数
662
- 细节:inspect可以获得函数签名,也可以获得一个函数各个参数的输入值,但我想要展现的是原始表达式,
663
- 例如func(a),以func(1+2)调用,inpect只能获得“a=3”,但我想要的是“1+2=3”的效果
664
- """
665
- res = {}
666
- # 1 找出调用函数的代码
667
- ss = inspect.stack()
668
- frameinfo = ss[depth]
669
- arginfo = inspect.getargvalues(ss[depth - 1][0])
670
- if arginfo.varargs:
671
- origin_args = arginfo.locals[arginfo.varargs]
672
- else:
673
- origin_args = list(map(lambda x: arginfo.locals[x], arginfo.args))
674
-
675
- res['fullfilename'] = frameinfo.filename
676
- res['filename'] = os.path.basename(frameinfo.filename)
677
- res['funcname'] = frameinfo.function
678
- res['lineno'] = frameinfo.lineno
679
- res['depth'] = len(ss)
680
- ls_ = list(map(lambda x: x.function, ss))
681
- # ls.reverse()
682
- res['funcnames'] = '/'.join(ls_)
683
-
684
- if frameinfo.code_context:
685
- code_line = frameinfo.code_context[0].strip()
686
- else: # 命令模式无法获得代码,是一个None对象
687
- code_line = ''
688
-
689
- funcname = ss[depth - 1].function # 调用的函数名
690
- # 这一行代码不一定是从“funcname(”开始,所以要用find找到开始位置
691
- code = code_line[code_line.find(funcname + '(') + len(funcname):]
692
-
693
- # 2 先找到函数的()中参数列表,需要以')'作为分隔符分析
694
- # TODO 可以考虑用ast重实现
695
- ls = code.split(')')
696
- logo, i = True, 1
697
- while logo and i <= len(ls):
698
- # 先将'='做特殊处理,防止字典类参数导致的语法错误
699
- s = ')'.join(ls[:i]).replace('=', '+') + ')'
700
- try:
701
- compile(s, '<string>', 'single')
702
- except SyntaxError:
703
- i += 1
704
- else: # 正常情况
705
- logo = False
706
- code = ')'.join(ls[:i])[1:]
707
-
708
- # 3 获得注释
709
- # 这个注释实现的不是很完美,不过影响应该不大,还没有想到比较完美的解决方案
710
- t = ')'.join(ls[i:])
711
- comment = t[t.find('#'):] if '#' in t else ''
712
- res['comment'] = comment
713
-
714
- # 4 获得变量名
715
- ls = code.split(',')
716
- n = len(ls)
717
- argnames = list()
718
- i, j = 0, 1
719
- while j <= n:
720
- s = ','.join(ls[i:j])
721
- try:
722
- compile(s.lstrip(), '<string>', 'single')
723
- except SyntaxError:
724
- j += 1
725
- else: # 没有错误的时候执行
726
- argnames.append(s.strip())
727
- i = j
728
- j = i + 1
729
-
730
- # 5 获得变量值和类型
731
- res['argvals'] = origin_args
732
- res['types'] = list(map(typename, origin_args))
733
-
734
- if not argnames: # 如果在命令行环境下调用,argnames会有空,需要根据argvals长度置空名称
735
- argnames = [''] * len(res['argvals'])
736
- res['argnames'] = argnames
737
-
738
- return res
739
-
740
-
741
- def dformat(*args, depth=2,
742
- delimiter=' ' * 4,
743
- strfunc=repr,
744
- fmt='[{depth:02}]{filename}/{lineno}: {argmsg}',
745
- subfmt='{name}<{tp}>={val}'):
746
- r"""
747
- :param args: 需要检查的表达式
748
- 这里看似没有调用,其实在func_input_message用inspect会提取到args的信息
749
- :param depth: 处理对象
750
- 默认值2,即处理dformat本身
751
- 2以下值没意义
752
- 2以上的值,可以不传入args参数
753
- :param delimiter: 每个变量值展示之间的分界
754
- :param strfunc: 对每个变量值的文本化方法,常见的有repr、str
755
- :param fmt: 展示格式,除了func_input_message中的关键字,新增
756
- argmsg:所有的「变量名=变量值」,或所有的「变量名<变量类型>=变量值」,或自定义格式,采用delimiter作为分界符
757
- 旧版还用过这种格式: '{filename}/{funcname}/{lineno}: {argmsg} {comment}'
758
- :param subfmt: 自定义每个变量值对的显示形式
759
- name,变量名
760
- val,变量值
761
- tp,变量类型
762
- :return: 返回格式化好的文本字符串
763
- """
764
- res = func_input_message(depth)
765
- ls = [subfmt.format(name=name, val=strfunc(val), tp=tp)
766
- for name, val, tp in zip(res['argnames'], res['argvals'], res['types'])]
767
- res['argmsg'] = delimiter.join(ls)
768
- return fmt.format(**res)
769
-
770
-
771
- def dprint(*args, **kwargs):
772
- r"""
773
- # 故意写的特别复杂,测试在极端情况下是否能正确解析出表达式
774
- >> a, b = 1, 2
775
- >> re.sub(str(dprint(1, b, a, "aa" + "bb)", "a[,ba\nbb""b", [2, 3])), '', '##') # 注释 # 注
776
- [08]<doctest debuglib.dprint[1]>/1: 1<int>=1 b<int>=2 a<int>=1 "aa" + "bb)"<str>='aabb)' "a[,ba\nbb""b"<str>='a[,ba\nbbb' [2, 3]<list>=[2, 3] ##') # 注释 # 注
777
- '##'
778
- """
779
- print(dformat(depth=3, **kwargs))
780
-
781
-
782
- # dprint会被注册进builtins,可以在任意地方直接使用
783
- setattr(builtins, 'dprint', dprint)
784
-
785
-
786
- class DPrint:
787
- """ 用来存储上下文相关变量,进行全局性地调试
788
-
789
- TODO 需要跟logging库一样,可以获取不同名称的配置
790
- 可以进行很多扩展,比如输出到stderr还是stdout
791
- """
792
-
793
- watch = {}
794
-
795
- @classmethod
796
- def reset(cls):
797
- cls.watch = {}
798
-
799
- @classmethod
800
- def format(cls, watch2, show_type=False, sep=' '):
801
- """
802
- :param watch2: 必须也是字典类型
803
- :param show_type: 是否显示每个数值的类型
804
- :param sep: 每部分的间隔符
805
- """
806
- msg = []
807
- input_msg = func_input_message(2)
808
- filename, lineno = input_msg['filename'], input_msg['lineno']
809
- msg.append(f'{filename}/{lineno}')
810
-
811
- watch3 = cls.watch.copy()
812
- watch3.update(watch2)
813
- for k, v in watch3.items():
814
- if k.startswith('$'):
815
- # 用 $ 修饰的不显示变量名,直接显示值
816
- msg.append(f'{v}')
817
- else:
818
- if show_type:
819
- msg.append(f'{k}<{typename(v)}>={repr(v)}')
820
- else:
821
- msg.append(f'{k}={repr(v)}')
822
-
823
- return sep.join(msg)
824
-
825
-
826
- def format_exception(e, mode=3):
827
- if mode == 1:
828
- # 仅获取异常类型的名称
829
- text = ''.join(traceback.format_exception_only(type(e), e)).strip()
830
- elif mode == 2:
831
- # 获取异常类型的名称和附加的错误信息
832
- text = f"{type(e).__name__}: {e}"
833
- elif mode == 3:
834
- text = ''.join(traceback.format_exception(type(e), e, e.__traceback__))
835
- else:
836
- raise ValueError
837
- return text
838
-
839
-
840
- def prettifystr(s):
841
- """对一个对象用更友好的方式字符串化
842
-
843
- :param s: 输入类型不做限制,会将其以友好的形式格式化
844
- :return: 格式化后的字符串
845
- """
846
- title = ''
847
- if isinstance(s, str):
848
- pass
849
- elif isinstance(s, Counter): # Counter要按照出现频率显示
850
- li = s.most_common()
851
- title = f'collections.Counter长度:{len(s)}\n'
852
- # 不使用复杂的pd库,先简单用pprint即可
853
- # df = pd.DataFrame.from_records(s, columns=['value', 'count'])
854
- # s = dataframe_str(df)
855
- s = pprint.pformat(li)
856
- elif isinstance(s, (list, tuple)):
857
- title = f'{typename(s)}长度:{len(s)}\n'
858
- s = pprint.pformat(s)
859
- elif isinstance(s, (dict, set)):
860
- title = f'{typename(s)}长度:{len(s)}\n'
861
- s = pprint.pformat(s)
862
- else: # 其他的采用默认的pformat
863
- s = pprint.pformat(s)
864
- return title + s
865
-
866
-
867
- class PrettifyStrDecorator:
868
- """将函数的返回值字符串化(调用 prettifystr 美化)"""
869
-
870
- def __init__(self, func):
871
- self.func = func # 使用self.func可以索引回原始函数名称
872
- self.last_raw_res = None # last raw result,上一次执行函数的原始结果
873
-
874
- def __call__(self, *args, **kwargs):
875
- self.last_raw_res = self.func(*args, **kwargs)
876
- return prettifystr(self.last_raw_res)
877
-
878
-
879
- def hide_console_window():
880
- """ 隐藏命令行窗口 """
881
- import ctypes
882
- kernel32 = ctypes.WinDLL('kernel32')
883
- user32 = ctypes.WinDLL('user32')
884
- SW_HIDE = 0
885
- hWnd = kernel32.GetConsoleWindow()
886
- user32.ShowWindow(hWnd, SW_HIDE)
887
-
888
-
889
- def get_installed_packages():
890
- """ 使用pip list获取当前环境安装了哪些包 """
891
- output = subprocess.check_output(["pip", "list"], universal_newlines=True)
892
- packages = [line.split()[0] for line in output.split("\n")[2:] if line]
893
- return packages
894
-
895
-
896
- class OutputLogger(logging.Logger):
897
- """
898
- 我在jupyter写代码,经常要print输出一些中间结果。
899
- 但是当结果很多的时候,又要转存到文件里保存起来查看。
900
- 要保存到文件时,和普通的print写法是不一样的,一般要新建立一个ls = []的变量。
901
- 然后print改成ls.append操作,会很麻烦。
902
-
903
- 就想着能不能自己定义一个类,支持.print方法,不仅能实现正常的输出控制台的功能。
904
- 也能在需要的时候指定文件路径,会自动将结果存储到文件中。
905
- """
906
-
907
- def __init__(self, name='OutputLogger', *, log_file=None, log_mode='a', output_to_console=True):
908
- """
909
- :param str name: 记录器的名称。默认为 'OutputLogger'。
910
- :param log_file: 日志文件的路径。默认为 None,表示不输出到文件。
911
- :param bool output_to_console: 是否输出到命令行。默认为 True。
912
- """
913
- super().__init__(name)
914
-
915
- self.output_to_console = output_to_console
916
- self.log_file = log_file
917
-
918
- self.setLevel(logging.INFO)
919
- # 创建格式化器
920
- formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(filename)s/%(lineno)d - %(message)s',
921
- '%Y-%m-%d %H:%M:%S')
922
-
923
- # 提前重置为空文件
924
- if log_file is not None:
925
- if not os.path.isfile(log_file) or log_mode == 'w':
926
- with open(log_file, 'w', encoding='utf8') as f:
927
- f.write('')
928
-
929
- # 创建文件日志处理器
930
- if log_file:
931
- file_handler = logging.FileHandler(log_file, encoding='utf-8')
932
- file_handler.setLevel(logging.DEBUG) # 日志文件是最详细级别都记录
933
- file_handler.setFormatter(formatter)
934
- self.addHandler(file_handler)
935
-
936
- # 创建命令行日志处理器
937
- if output_to_console:
938
- console_handler = logging.StreamHandler()
939
- console_handler.setLevel(logging.WARNING) # 有些太详细的问题,不想写在控制台,而是写到文件
940
- console_handler.setFormatter(formatter)
941
- self.addHandler(console_handler)
942
-
943
- # 只输出到控制台:标准库的print
944
- # 只输出到文件:debug, info
945
- # 同时输出到控制台和文件:warning, error, critical, print
946
-
947
- def print(self, *args, **kwargs):
948
- """ 使用print机制,会同时输出到控制台和日志文件 """
949
- msg = print2string(*args, **kwargs)
950
-
951
- if self.output_to_console:
952
- print(msg, end='')
953
-
954
- if self.log_file:
955
- with open(self.log_file, 'a', encoding='utf-8') as f:
956
- f.write(msg)
957
-
958
- return msg
959
-
960
- def tprint(self, *args, **kwargs):
961
- """ 带时间戳的print """
962
- self.print(utc_now2(), *args, **kwargs)
963
-
964
- def log_json(self, data):
965
- """ 类似print,但是是把数据按照json的格式进行记录整理,更加结构化,方便后期处理 """
966
- data['time'] = utc_timestamp()
967
- msg = json.dumps(data, ensure_ascii=False, default=str)
968
- self.print(msg)
969
-
970
-
971
- def xlmd5(content):
972
- if isinstance(content, str):
973
- content = content.encode('utf-8')
974
- elif not isinstance(content, bytes):
975
- content = str(content).encode('utf-8')
976
-
977
- if len(content) <= 32: # 32位以下的字符串,直接返回
978
- return content
979
- else:
980
- return hashlib.md5(content).hexdigest()
981
-
982
-
983
- @run_once()
984
- def get_hostname():
985
- hostname = socket.getfqdn()
986
- return hostname
987
-
988
-
989
- @run_once()
990
- def get_hostname2():
991
- """ 更加定制化的操作 """
992
- hostname = socket.getfqdn()
993
- hostname = hostname.replace('-', '_')
994
- hostname = hostname.split('.')[0]
995
- return hostname
996
-
997
-
998
- @run_once()
999
- def get_username():
1000
- return os.path.split(os.path.expanduser('~'))[-1]
1001
-
1002
-
1003
- class XlThreadPoolExecutor(ThreadPoolExecutor):
1004
- def __init__(self, *args, **kwargs):
1005
- super().__init__(*args, **kwargs)
1006
- self.futures = []
1007
-
1008
- def submit(self, *args, **kwargs):
1009
- future = super().submit(*args, **kwargs)
1010
- self.futures.append(future)
1011
- return future
1012
-
1013
- def yield_result(self, timeout=None):
1014
- for future in self.futures:
1015
- yield future.result(timeout=timeout)
1016
-
1017
-
1018
- def shuffle_dict_keys(d):
1019
- keys = list(d.keys())
1020
- random.shuffle(keys)
1021
- d = {k: d[k] for k in keys}
1022
- return d
1023
-
1024
-
1025
- def generate_int_hash_from_str(s):
1026
- """ 对字符串使用md5编码,然后转出一个数值哈希,一般是用来进行随机分组
1027
- 比如获得一个整数后,对3取余,就是按照余数为0、1、2的情况分3组
1028
- """
1029
- return int(hashlib.md5(s.encode()).hexdigest(), 16)
1030
-
1031
-
1032
- def get_groupid_from_string(s, n_groups):
1033
- """ 通过计算一个字符串的哈希值来对其进行分组,需要提前知道总组别数n_groups """
1034
- hash_value = generate_int_hash_from_str(s)
1035
- return hash_value % n_groups
1036
-
1037
-
1038
- def safe_div(a, b):
1039
- """ 安全除法,避免除数为0的情况
1040
-
1041
- :param a: 被除数
1042
- :param b: 除数
1043
- :return: a/b,如果b为0,返回0
1044
- """
1045
- if b == 0:
1046
- return a / sys.float_info.epsilon
1047
- else:
1048
- return a / b
1049
-
1050
-
1051
- def inplace_decorate(parent, func_name, wrapper):
1052
- """ 将指定的函数替换为装饰器版本
1053
- 允许在运行时动态地将一个函数或方法替换为其装饰版本。通常用于添加日志、性能测试、事务处理等。
1054
- (既然可以写成装饰版本,相当于其实要完全替换成另外的函数也是可以的)
1055
-
1056
- 当然,因为py一切皆对象,这里处理的不是函数,而是其他变量等对象也是可以的
1057
-
1058
- 这个功能跟直接把原代码替换修改了还是有区别的,如果原函数在被这个装饰之前,已经被其他地方调用,或者被装饰器补充,
1059
- 太晚使用这个装饰,并不会改变前面已经运行、被捕捉的情况
1060
- 遇到这种情况,也可以考虑在原函数定义后,直接紧接着加上这个函数重置
1061
-
1062
- 对于类成员方法,直接用这个设置可能也不行,只能去改源码了
1063
- 比如要给函数加计算时间的部分,可以考虑使用 get_global_var 等来夸作用域记录时间数据
1064
- """
1065
-
1066
- if hasattr(parent, func_name):
1067
- if callable(wrapper): # 对函数的封装
1068
- original_func = getattr(parent, func_name)
1069
-
1070
- @functools.wraps(original_func)
1071
- def decorated_func(*args, **kwargs):
1072
- return wrapper(original_func, *args, **kwargs)
1073
-
1074
- setattr(parent, func_name, decorated_func)
1075
- else: # 对数值的封装,不过如果是数值,其实也没必要调用这个函数,直接赋值就好了
1076
- return setattr(parent, func_name, wrapper)
1077
- else: # 否则按照字典的模式来处理
1078
-
1079
- original_func = parent[func_name]
1080
-
1081
- @functools.wraps(original_func)
1082
- def decorated_func(*args, **kwargs):
1083
- return wrapper(original_func, *args, **kwargs)
1084
-
1085
- parent[func_name] = decorated_func
1086
-
1087
-
1088
- def check_counter(data, top_n=10):
1089
- """ 将一个数据data转为Counter进行频数分析 """
1090
- # 1 如果是list、tuple类型,需要转counter
1091
- if isinstance(data, (list, tuple)):
1092
- data = Counter(data)
1093
- if not isinstance(data, Counter):
1094
- raise ValueError(f'输入的数据类型不对,应该是Counter类型,而不是{typename(data)}')
1095
-
1096
- # 2 列出出现次数最多的top_n条目
1097
- # 打印基本统计信息
1098
- total_items = sum(data.values())
1099
- print(f"总条目数: {total_items}")
1100
-
1101
- if top_n > 0:
1102
- top_items = data.most_common(top_n)
1103
- max_n = len(data)
1104
- print(f"出现次数最多的{min(top_n, max_n)}/{max_n}条数据(频率):")
1105
- for item, count in top_items:
1106
- print(f"\t{item}\t{count}")
1107
-
1108
- # 3 打印基本统计信息
1109
- # 对原始Counter的计数值进行再计数
1110
- count_frequencies = Counter(data.values())
1111
-
1112
- # 打印各计数值出现的次数
1113
- print("各计数值出现的次数,频率的频率(频率分布):")
1114
- for count, frequency in count_frequencies.most_common():
1115
- print(f"\t{count}\t{frequency}")
1116
-
1117
-
1118
- def tprint(*args, **kwargs):
1119
- """ 带时间戳的print """
1120
- print(utc_now2(), *args, **kwargs)
1121
-
1122
-
1123
- def is_valid_identifier(name):
1124
- """ 判断是否是合法的标识符 """
1125
- return re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', name)
1126
-
1127
-
1128
- def get_number_width(n):
1129
- """ 判断数值n的长度
1130
-
1131
- 参考资料:https://jstrieb.github.io/posts/digit-length/
1132
-
1133
- >>> get_number_width(0)
1134
- 1
1135
- >>> get_number_width(9)
1136
- 1
1137
- >>> get_number_width(10)
1138
- 2
1139
- >>> get_number_width(97)
1140
- 2
1141
- """
1142
- # assert n > 0
1143
- # return math.ceil(math.log10(n + 1))
1144
-
1145
- return 1 if n == 0 else (math.floor(math.log10(n)) + 1)
1146
-
1147
-
1148
- def aligned_range(start, stop=None, step=1):
1149
- """ 返回按照域宽对齐的数字序列 """
1150
- if stop is None:
1151
- start, stop = 0, start
1152
-
1153
- max_width = get_number_width(stop - step)
1154
- format_str = '{:0' + str(max_width) + 'd}'
1155
-
1156
- return [format_str.format(i) for i in range(start, stop, step)]
1157
-
1158
-
1159
- def percentage_and_value(numbers, precision=2, *, total=None, sep='.'):
1160
- """ 对输入的一串数值,转换成一种特殊的表达格式 "百分比.次数"
1161
-
1162
- :param list numbers: 数值列表
1163
- :param int precision: 百分比的精度(小数点后的位数),默认为 2
1164
- :param int total: 总数,如果不输入,则默认为输入数值的和
1165
- :param str sep: 分隔符
1166
- :return: 整数部分是比例,小数部分是原始数值的整数部分
1167
- """
1168
- if total is None:
1169
- total = sum(numbers)
1170
- width = get_number_width(total)
1171
- result = []
1172
- for num in numbers:
1173
- percent = safe_div(num, total) * 10 ** precision
1174
- result.append(f"{percent:.0f}{sep}{num:0{width}d}")
1175
- return result
1176
-
1177
-
1178
- def get_local_ip():
1179
- """ 获得本地ip,代码由deepseek提供 """
1180
- try:
1181
- # 使用UDP协议连接到外部服务器
1182
- s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1183
- s.connect(("8.8.8.8", 80)) # Google的DNS服务器和常用端口
1184
- local_ip = s.getsockname()[0] # 获取套接字的本地地址
1185
- s.close()
1186
- return local_ip
1187
- except Exception as e:
1188
- # 如果失败,尝试通过主机名获取IP列表
1189
- try:
1190
- hostname = socket.gethostname()
1191
- ips = socket.gethostbyname_ex(hostname)[2] # 获取所有IPv4地址
1192
- for ip in ips:
1193
- if ip != "127.0.0.1": # 排除回环地址
1194
- return ip
1195
- return "127.0.0.1" # 默认返回回环地址
1196
- except:
1197
- raise ValueError("无法获取IP地址")
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # @Author : 陈坤泽
4
+ # @Email : 877362867@qq.com
5
+ # @Date : 2021/06/03 23:21
6
+
7
+
8
+ """ 封装一些代码开发中常用的功能,工程组件 """
9
+
10
+ from collections import Counter
11
+ from concurrent.futures import ThreadPoolExecutor
12
+ from urllib.parse import urlparse
13
+ import builtins
14
+ import datetime
15
+ import functools
16
+ import hashlib
17
+ import inspect
18
+ import io
19
+ import itertools
20
+ import json
21
+ import logging
22
+ import math
23
+ import os
24
+ import pprint
25
+ import queue
26
+ import random
27
+ import re
28
+ import signal
29
+ import socket
30
+ import subprocess
31
+ import sys
32
+ import tempfile
33
+ import threading
34
+ import time
35
+ import traceback
36
+
37
+ from pyxllib.prog.newbie import classproperty, typename
38
+
39
+
40
+ # from loguru import logger
41
+
42
+
43
+ def system_information():
44
+ """主要是测试一些系统变量值,顺便再演示一次Timer用法"""
45
+
46
+ def pc_messages():
47
+ """演示如何获取当前操作系统的PC环境数据"""
48
+ # fqdn:fully qualified domain name
49
+ print('1、socket.getfqdn() :', socket.getfqdn()) # 完全限定域名,可以理解成pcname,计算机名
50
+ # 注意py的很多标准库功能本来就已经处理了不同平台的问题,尽量用标准库而不是自己用sys.platform作分支处理
51
+ print('2、sys.platform :', sys.platform) # 运行平台,一般是win32和linux
52
+ # li = os.getenv('PATH').split(os.path.pathsep) # 环境变量名PATH,win中不区分大小写,linux中区分大小写必须写成PATH
53
+ # print("3、os.getenv('PATH'):", f'数量={len(li)},', pprint.pformat(li, 4))
54
+
55
+ def executable_messages():
56
+ """演示如何获取被执行程序相关的数据"""
57
+ print('1、sys.executable :', sys.executable) # 当前被执行脚本位置
58
+ print('2、sys.version :', sys.version) # python的版本
59
+ print('3、os.getcwd() :', os.getcwd()) # 获得当前工作目录
60
+ print('4、gettempdir() :', tempfile.gettempdir()) # 临时文件夹位置
61
+ # print('5、sys.path :', f'数量={len(sys.path)},', pprint.pformat(sys.path, 4)) # import绝对位置包的搜索路径
62
+
63
+ print('【pc_messages】')
64
+ pc_messages()
65
+ print('【executable_messages】')
66
+ executable_messages()
67
+
68
+
69
+ def is_url(arg):
70
+ """输入是一个字符串,且值是一个合法的url"""
71
+ if not isinstance(arg, str): return False
72
+ try:
73
+ result = urlparse(arg)
74
+ return all([result.scheme, result.netloc])
75
+ except ValueError:
76
+ return False
77
+
78
+
79
+ def is_file(arg, exists=True):
80
+ """相较于标准库的os.path.isfile,对各种其他错误类型也会判False
81
+
82
+ :param exists: arg不仅需要是一个合法的文件名,还要求其实际存在
83
+ 设为False,则只判断文件名合法性,不要求其一定要存在
84
+ """
85
+ if not isinstance(arg, str): return False
86
+ if len(arg) > 500: return False
87
+ if not exists:
88
+ raise NotImplementedError
89
+ return os.path.isfile(arg)
90
+
91
+
92
+ def len_in_dim2_min(arr):
93
+ """ 计算类List结构在第2维上的最小长度
94
+
95
+ >>> len_in_dim2([[1,1], [2], [3,3,3]])
96
+ 3
97
+
98
+ >>> len_in_dim2([1, 2, 3]) # TODO 是不是应该改成0合理?但不知道牵涉到哪些功能影响
99
+ 1
100
+ """
101
+ if not isinstance(arr, (list, tuple)):
102
+ raise TypeError('类型错误,不是list构成的二维数组')
103
+
104
+ # 找出元素最多的列
105
+ column_num = math.inf
106
+ for i, item in enumerate(arr):
107
+ if isinstance(item, (list, tuple)): # 该行是一个一维数组
108
+ column_num = min(column_num, len(item))
109
+ else: # 如果不是数组,是指单个元素,当成1列处理
110
+ column_num = min(column_num, 1)
111
+ break # 只要有个1,最小长度就一定是1了
112
+
113
+ return column_num
114
+
115
+
116
+ def print2string(*args, **kwargs):
117
+ """https://stackoverflow.com/questions/39823303/python3-print-to-string"""
118
+ output = io.StringIO()
119
+ print(*args, file=output, **kwargs)
120
+ contents = output.getvalue()
121
+ output.close()
122
+ return contents
123
+
124
+
125
+ class EmptyPoolExecutor:
126
+ """伪造一个类似concurrent.futures.ThreadPoolExecutor、ProcessPoolExecutor的接口类
127
+ 用来检查多线程、多进程中的错误
128
+
129
+ 即并行中不会直接报出每个线程的错误,只能串行执行才好检查
130
+ 但是两种版本代码来回修改很麻烦,故设计此类,只需把
131
+ concurrent.futures.ThreadPoolExecutor 暂时改为 EmptyPoolExecutor 进行调试即可
132
+ """
133
+
134
+ def __init__(self, *args, **kwargs):
135
+ """参数并不需要实际处理,并没有真正并行,而是串行执行"""
136
+ self._work_queue = queue.Queue()
137
+
138
+ def submit(self, func, *args, **kwargs):
139
+ """执行函数"""
140
+ func(*args, **kwargs)
141
+
142
+ def shutdown(self):
143
+ # print('并行执行结束')
144
+ pass
145
+
146
+
147
+ def xlwait(func, condition=bool, *, limit=None, interval=1):
148
+ """ 不断重复执行func,直到得到满足condition条件的期望值
149
+
150
+ :param condition: 退出等待的条件,默认为bool真值
151
+ :param limit: 重复执行的上限时间(单位 秒),默认一直等待
152
+ :param interval: 重复执行间隔 (单位 秒)
153
+
154
+ """
155
+ t = time.time()
156
+ while True:
157
+ res = func()
158
+ if condition(res):
159
+ return res
160
+ elif limit and (time.time() - t > limit):
161
+ return res # 超时也返回目前得到的结果
162
+ time.sleep(interval)
163
+
164
+
165
+ class DictTool:
166
+ @classmethod
167
+ def json_loads(cls, label, default=None):
168
+ """ 尝试从一段字符串解析为字典
169
+
170
+ :param default: 如果不是字典时的处理策略
171
+ None,不作任何处理
172
+ str,将原label作为defualt这个键的值来存储
173
+ :return: s为非字典结构时返回空字典
174
+
175
+ >>> DictTool.json_loads('123', 'text')
176
+ {'text': '123'}
177
+ >>> DictTool.json_loads('[123, 456]', 'text')
178
+ {'text': '[123, 456]'}
179
+ >>> DictTool.json_loads('{"a": 123}', 'text')
180
+ {'a': 123}
181
+ """
182
+ labelattr = dict()
183
+ try:
184
+ data = json.loads(label)
185
+ if isinstance(data, dict):
186
+ labelattr = data
187
+ except json.decoder.JSONDecodeError:
188
+ pass
189
+ if not labelattr and isinstance(default, str):
190
+ labelattr[default] = label
191
+ return labelattr
192
+
193
+ @classmethod
194
+ def or_(cls, *args):
195
+ """ 合并到新字典
196
+
197
+ 左边字典有的key,优先取左边,右边不会覆盖。
198
+ 如果要覆盖效果,直接用 d1.update(d2)功能即可。
199
+
200
+ :return: args[0] | args[1] | ... | args[-1].
201
+ """
202
+ res = {}
203
+ cls.ior(res, *args)
204
+ return res
205
+
206
+ @classmethod
207
+ def ior(cls, dict_, *args):
208
+ """ 合并到第1个字典
209
+
210
+ :return: dict_ |= (args[0] | args[1] | ... | args[-1]).
211
+
212
+ 220601周三15:45,默认已有对应key的话,值是不覆盖的,如果要覆盖,直接用update就行了,不需要这个接口
213
+ 所以把3.9的|=功能关掉
214
+ """
215
+ # if sys.version_info.major == 3 and sys.version_info.minor >= 9:
216
+ # for x in args:
217
+ # dict_ |= x
218
+ # else: # 旧版本py手动实现一个兼容功能
219
+ for x in args:
220
+ for k, v in x.items():
221
+ # 220729周五21:21,又切换成dict_有的不做替换
222
+ if k not in dict_:
223
+ dict_[k] = v
224
+ # dict_[k] = v
225
+
226
+ @classmethod
227
+ def sub(cls, dict_, keys):
228
+ """ 删除指定键值(不存在的跳过,不报错)
229
+
230
+ inplace subtraction
231
+
232
+ :param keys: 可以输入另一个字典,也可以输入一个列表表示要删除的键值清单
233
+
234
+ :return: dict2 = dict_ - keys
235
+ """
236
+ if isinstance(keys, dict):
237
+ keys = keys.keys()
238
+
239
+ return {k: v for k, v in dict_.items() if k not in keys}
240
+
241
+ @classmethod
242
+ def isub(cls, dict_, keys):
243
+ """ 删除指定键值(不存在的跳过,不报错)
244
+
245
+ inplace subtraction
246
+
247
+ keys可以输入另一个字典,也可以输入一个列表表示要删除的键值清单
248
+
249
+ 效果相当于 dict_ -= keys
250
+ """
251
+ if isinstance(keys, dict):
252
+ keys = keys.keys()
253
+
254
+ for k in keys:
255
+ if k in dict_:
256
+ del dict_[k]
257
+
258
+
259
+ class EnchantCvt:
260
+ """ 把类_cls的功能绑定到类cls里
261
+ 根源_cls里的实现类型不同,到cls需要呈现的接口形式不同,有很多种不同的转换形式
262
+ 每个分支里,随附了getattr目标函数的一般默认定义模板
263
+ 用_self、_cls表示dst_cls,区别原cls类的self、cls标记
264
+ """
265
+
266
+ @staticmethod
267
+ def staticmethod2objectmethod(cls, _cls, x):
268
+ # 目前用的最多的转换形式
269
+ # @staticmethod
270
+ # def func1(_self, *args, **kwargs): ...
271
+ setattr(_cls, x, getattr(cls, x))
272
+
273
+ @staticmethod
274
+ def staticmethod2property(cls, _cls, x):
275
+ # @staticmethod
276
+ # def func2(_self): ...
277
+ setattr(_cls, x, property(getattr(cls, x)))
278
+
279
+ @staticmethod
280
+ def staticmethod2classmethod(cls, _cls, x):
281
+ # @staticmethod
282
+ # def func3(_cls, *args, **kwargs): ...
283
+ setattr(_cls, x, classmethod(getattr(cls, x)))
284
+
285
+ @staticmethod
286
+ def staticmethod2classproperty(cls, _cls, x):
287
+ # @staticmethod
288
+ # def func4(_cls): ...
289
+ setattr(_cls, x, classproperty(getattr(cls, x)))
290
+
291
+ @staticmethod
292
+ def classmethod2objectmethod(cls, _cls, x):
293
+ # @classmethod
294
+ # def func5(cls, _self, *args, **kwargs): ...
295
+ setattr(_cls, x, lambda *args, **kwargs: getattr(cls, x)(*args, **kwargs))
296
+
297
+ @staticmethod
298
+ def classmethod2property(cls, _cls, x):
299
+ # @classmethod
300
+ # def func6(cls, _self): ...
301
+ setattr(_cls, x, lambda *args, **kwargs: property(getattr(cls, x)(*args, **kwargs)))
302
+
303
+ @staticmethod
304
+ def classmethod2classmethod(cls, _cls, x):
305
+ # @classmethod
306
+ # def func7(cls, _cls, *args, **kwargs): ...
307
+ setattr(_cls, x, lambda *args, **kwargs: classmethod(getattr(cls, x)(*args, **kwargs)))
308
+
309
+ @staticmethod
310
+ def classmethod2classproperty(cls, _cls, x):
311
+ # @classmethod
312
+ # def func8(cls, _cls): ...
313
+ setattr(_cls, x, lambda *args, **kwargs: classproperty(getattr(cls, x)(*args, **kwargs)))
314
+
315
+ @staticmethod
316
+ def staticmethod2modulefunc(cls, _cls, x):
317
+ # @staticmethod
318
+ # def func9(*args, **kwargs): ...
319
+ setattr(_cls, x, getattr(cls, x))
320
+
321
+ @staticmethod
322
+ def classmethod2modulefunc(cls, _cls, x):
323
+ # @classmethod
324
+ # def func10(cls, *args, **kwargs): ...
325
+ setattr(_cls, x, lambda *args, **kwargs: getattr(cls, x)(*args, **kwargs))
326
+
327
+ @staticmethod
328
+ def to_moduleproperty(cls, _cls, x):
329
+ # 理论上还有'to_moduleproperty'的转换模式
330
+ # 但这个很容易引起歧义,是应该存一个数值,还是动态计算?
331
+ # 如果是动态计算,可以使用modulefunc的机制显式执行,更不容易引起混乱。
332
+ # 从这个分析来看,是不需要实现'2moduleproperty'的绑定体系的。py标准语法本来也就没有module @property的概念。
333
+ raise NotImplementedError
334
+
335
+
336
+ class EnchantBase:
337
+ """
338
+ 一些三方库的类可能功能有限,我们想做一些扩展。
339
+ 常见扩展方式,是另外写一些工具函数,但这样就不“面向对象”了。
340
+ 如果要“面向对象”,需要继承已有的类写新类,但如果组件特别多,开发难度是很大的。
341
+ 比如excel就有单元格、工作表、工作薄的概念。
342
+ 如果自定义了新的单元格,那是不是也要自定义新的工作表、工作薄,才能默认引用到自己的单元格类。
343
+ 这个看着很理想,其实并没有实际开发可能性。
344
+ 所以我想到一个机制,把额外函数形式的扩展功能,绑定到原有类上。
345
+ 这样原来的功能还能照常使用,但多了很多我额外扩展的成员方法,并且也没有侵入原三方库的源码
346
+ 这样一种设计模式,简称“绑定”。换个逼格高点的说法,就是“强化、附魔”的过程,所以称为Enchant。
347
+ 这个功能应用在cv2、pillow、fitz、openpyxl,并在win32com中也有及其重要的应用。
348
+ """
349
+
350
+ @classmethod
351
+ def check_enchant_names(cls, classes, names=None, *, white_list=None, ignore_case=False):
352
+ """
353
+ :param list classes: 不能跟这里列出的模块、类的成员重复
354
+ :param list|str|tuple names: 要检查的名称清单
355
+ :param white_list: 白名单,这里面的名称不警告
356
+ 在明确要替换三方库标准功能的时候,可以使用
357
+ :param ignore_case: 忽略大小写
358
+ """
359
+ exist_names = {x.__name__: set(dir(x)) for x in classes}
360
+ if names is None:
361
+ names = {x for x in dir(cls) if x[:2] != '__'} \
362
+ - {'check_enchant_names', '_enchant', 'enchant'}
363
+
364
+ white_list = set(white_list) if white_list else {}
365
+
366
+ if ignore_case:
367
+ names = {x.lower() for x in names}
368
+ for k, values in exist_names.items():
369
+ exist_names[k] = {x.lower() for x in exist_names[k]}
370
+ white_list = {x.lower() for x in white_list}
371
+
372
+ for name, k in itertools.product(names, exist_names):
373
+ if name in exist_names[k] and name not in white_list:
374
+ print(f'警告!同名冲突! {k}.{name}')
375
+
376
+ return set(names)
377
+
378
+ @classmethod
379
+ def _enchant(cls, _cls, names, cvt=EnchantCvt.staticmethod2objectmethod):
380
+ """ 这个框架是支持classmethod形式的转换的,但推荐最好还是用staticmethod,可以减少函数嵌套层数,提高效率 """
381
+ for name in set(names):
382
+ cvt(cls, _cls, name)
383
+
384
+ @classmethod
385
+ def enchant(cls):
386
+ raise NotImplementedError
387
+
388
+
389
+ def check_install_package(package, speccal_install_name=None, *, user=False):
390
+ """ https://stackoverflow.com/questions/12332975/installing-python-module-within-code
391
+
392
+ :param speccal_install_name: 注意有些包使用名和安装名不同,比如pip install python-opencv,使用时是import cv2,
393
+ 此时应该写 check_install_package('cv2', 'python-opencv')
394
+
395
+ TODO 不知道频繁调用这个,会不会太影响性能,可以想想怎么提速优化?
396
+ 注意不要加@RunOnlyOnce,亲测速度会更慢三倍
397
+
398
+ 警告: 不要在频繁调用的底层函数里使用 check_install_package
399
+ 如果是module级别的还好,调几次其实性能影响微乎其微
400
+ 但在频繁调用的函数里使用,每百万次还是要额外的0.5秒开销的
401
+ """
402
+ try:
403
+ __import__(package)
404
+ except ModuleNotFoundError:
405
+ cmds = [sys.executable, "-m", "pip", "install"]
406
+ if user: cmds.append('--user')
407
+ cmds.append(speccal_install_name if speccal_install_name else package)
408
+ subprocess.check_call(cmds)
409
+
410
+
411
+ def run_once(distinct_mode=0, *, limit=1, debug=False):
412
+ """ 装饰器,装饰的函数在一次程序里其实只会运行一次
413
+
414
+ :param int|str distinct_mode:
415
+ 0,默认False,不区分输入的参数值(包括cls、self),强制装饰的函数只运行一次
416
+ 'str',设为True或1时,仅以字符串化的差异判断是否是重复调用,参数不同,会判断为不同的调用,每种调用限制最多执行limit次
417
+ 'id,str',在'str'的基础上,第一个参数使用id代替。一般用于类方法、对象方法的装饰。
418
+ 不考虑类、对象本身的内容改变,只要还是这个类或对象,视为重复调用。
419
+ 'ignore,str',首参数忽略,第2个开始的参数使用str格式化
420
+ 用于父类某个方法,但是子类继承传入cls,原本id不同会重复执行
421
+ 使用该模式,首参数会ignore忽略,只比较第2个开始之后的参数
422
+ func等callable类型的对象也行,是使用run_once装饰器的简化写法
423
+ :param limit: 默认只会执行一次,该参数可以提高限定的执行次数,一般用不到,用于兼容旧的 limit_call_number 装饰器
424
+ returns: 返回decorator
425
+ """
426
+ if callable(distinct_mode):
427
+ # @run_once,没写括号的时候去装饰一个函数,distinct_mode传入的是一个函数func
428
+ # 使用run_once本身的默认值
429
+ return run_once()(distinct_mode)
430
+
431
+ def get_tag(args, kwargs):
432
+ if not distinct_mode:
433
+ ls = tuple()
434
+ elif distinct_mode == 'str':
435
+ ls = (str(args), str(kwargs))
436
+ elif distinct_mode == 'id,str':
437
+ ls = (id(args[0]), str(args[1:]), str(kwargs))
438
+ elif distinct_mode == 'ignore,str':
439
+ ls = (str(args[1:]), str(kwargs))
440
+ else:
441
+ raise ValueError
442
+ return ls
443
+
444
+ def decorator(func):
445
+ counter = {} # 映射到一个[cnt, last_result]
446
+
447
+ def wrapper(*args, **kwargs):
448
+ tag = get_tag(args, kwargs)
449
+ if tag not in counter:
450
+ counter[tag] = [0, None]
451
+ x = counter[tag]
452
+ if x[0] < limit:
453
+ res = func(*args, **kwargs)
454
+ x = counter[tag] = [x[0] + 1, res]
455
+
456
+ return x[1]
457
+
458
+ return wrapper
459
+
460
+ return decorator
461
+
462
+
463
+ def set_default_args(*d_args, **d_kwargs):
464
+ """ 增设默认参数
465
+
466
+ 有时候需要调试一个函数,试跑一些参数结果,
467
+ 但这些参数又不适合定为标准化的接口值,可以用这个函数设置
468
+
469
+ 参数加载、覆盖顺序(越后面的优先级越高)
470
+ 1、函数定义阶段设置的默认值
471
+ 2、装饰器定义的参数 d_args、d_kwargs
472
+ 3、运行阶段明确指定的参数,即传入的f_args、f_kwargs
473
+ """
474
+
475
+ def decorator(func):
476
+ def wrapper(*f_args, **f_kwargs):
477
+ args = f_args + d_args
478
+ d_kwargs.update(f_kwargs) # 优先使用外部传参传入的值,再用装饰器里扩展的默认值
479
+ return func(*args, **d_kwargs)
480
+
481
+ return wrapper
482
+
483
+ return decorator
484
+
485
+
486
+ def utc_now(offset_hours=8, microseconds=0):
487
+ """ 有的机器可能本地时间设成了utc0,可以用这个方式,获得准确的utc8时间
488
+
489
+ :param microseconds: 微秒,如果不指定(None),就是当前时间的微秒
490
+ """
491
+ dt = datetime.datetime.utcnow()
492
+ dt += datetime.timedelta(hours=offset_hours)
493
+
494
+ if microseconds is not None:
495
+ dt = dt.replace(microsecond=microseconds)
496
+
497
+ return dt
498
+
499
+
500
+ def utc_now2(offset_hours=8):
501
+ """ 转字符串格式 """
502
+ return utc_now().isoformat(' ', timespec='seconds')
503
+
504
+
505
+ def utc_timestamp(offset_hours=8):
506
+ """ mysql等数据库支持的日期格式
507
+ """
508
+ return utc_now(offset_hours).isoformat(' ', timespec='seconds')
509
+
510
+
511
+ class Timeout:
512
+ """ 对函数等待执行的功能,限制运行时间
513
+
514
+ 【实现思路】
515
+ 1、最简单的方式是用signal.SIGALRM实现(包括三方库timeout-decorator也有这个局限)
516
+ https://stackoverflow.com/questions/2281850/timeout-function-if-it-takes-too-long-to-finish
517
+ 但是这个不支持windows系统~~
518
+ 2、那windows和linux通用的做法,就是把原执行函数变成一个子线程来运行
519
+ https://stackoverflow.com/questions/21827874/timeout-a-function-windows
520
+ 但是,又在onenote的win32com发现,有些功能没办法丢到子线程里,会出问题
521
+ 而且使用子线程,也没法做出支持with上下文语法的功能了
522
+ 3、于是就有了当前我自己搞出的一套机制
523
+ 是用一个Timer计时器子线程计时,当timeout超时,使用信号机制给主线程抛出一个异常
524
+ ① 注意,不能子线程直接抛出异常,这样影响不了主线程
525
+ ② 也不能直接抛出错误signal,这样会强制直接中断程序。应该抛出TimeoutError,让后续程序进行超时逻辑的处理
526
+ ③ 这里是让子线程抛出信号,主线程收到信号后,再抛出TimeoutError
527
+
528
+ 注意:这个函数似乎不支持多线程
529
+ """
530
+
531
+ def __init__(self, seconds):
532
+ self.seconds = seconds
533
+ self.alarm = None
534
+
535
+ def __call__(self, func):
536
+ @functools.wraps(func)
537
+ def wrapper(*args, **kwargs):
538
+ # 1 如果超时,主线程收到信号会执行的功能
539
+ def overtime(signum, frame):
540
+ raise TimeoutError(f'function [{func.__name__}] timeout [{self.seconds} seconds] exceeded!')
541
+
542
+ signal.signal(signal.SIGABRT, overtime)
543
+
544
+ # 2 开一个子线程计时器,超时的时候发送信号
545
+ def send_signal():
546
+ signal.raise_signal(signal.SIGABRT)
547
+
548
+ alarm = threading.Timer(self.seconds, send_signal)
549
+ alarm.start()
550
+
551
+ # 3 执行主线程功能
552
+ res = func(*args, **kwargs)
553
+ alarm.cancel() # 正常执行完则关闭计时器
554
+
555
+ return res
556
+
557
+ return wrapper
558
+
559
+ def __enter__(self):
560
+ if self.seconds == 0: # 可以设置0来关闭超时功能
561
+ return
562
+
563
+ def overtime(signum, frame):
564
+ raise TimeoutError(f'with 上下文代码块运行超时 > [{self.seconds} 秒]')
565
+
566
+ signal.signal(signal.SIGABRT, overtime)
567
+
568
+ def send_signal():
569
+ signal.raise_signal(signal.SIGABRT)
570
+
571
+ # 挂起一个警告器,如果"没人"管它,self.seconds就会抛出错误
572
+ self.alarm = threading.Timer(self.seconds, send_signal)
573
+ self.alarm.start()
574
+
575
+ def __exit__(self, exc_type, exc_val, exc_tb):
576
+ if self.seconds == 0:
577
+ return
578
+
579
+ # with已经运行完了,马上关闭警告器
580
+ self.alarm.cancel()
581
+
582
+
583
+ @run_once('str')
584
+ def inject_members(from_obj, to_obj, member_list=None, *,
585
+ check=False, ignore_case=False,
586
+ white_list=None, black_list=None):
587
+ """ 将from_obj的方法注入到to_obj中
588
+
589
+ 一般用于类继承中,将子类from_obj的新增的成员方法,添加回父类to_obj中
590
+ 反经合道:这样看似很违反常理,父类就会莫名其妙多出一些可操作的成员方法。
591
+ 但在某些时候能保证面向对象思想的情况下,大大简化工程代码开发量。
592
+ 也可以用于模块等方法的添加
593
+
594
+ :param from_obj: 一般是一个类用于反向继承的方法来源,但也可以是模块等任意对象。
595
+ 注意py一切皆类,一个class定义的类本质也是type类定义出的一个对象
596
+ 所以这里概念上称为obj才是准确的,反而是如果叫from_cls不太准确,虽然这个方法主要确实都是用于class类
597
+ :param to_obj: 同from_obj,要被注入方法的对象
598
+ :param Sequence[str] member_list: 手动指定的成员方法名,可以不指定,自动生成
599
+ :param check: 检查重名方法
600
+ :param ignore_case: 忽略方法的大小写情况,一般用于win32com接口
601
+ :param Sequence[str] white_list: 白名单。无论是否重名,这里列出的方法都会被添加
602
+ :param Sequence[str] black_list: 黑名单。这里列出的方法不会被添加
603
+
604
+ # 把XlDocxTable的成员方法绑定到docx.table.Table里
605
+ >> inject_members(XlDocxTable, docx.table.Table)
606
+
607
+ 240826周一,其他可参考学习的三方现成工具库:from fastcore.foundation import patch
608
+ """
609
+ # 1 整理需要注入的方法清单
610
+ dst = set(dir(to_obj))
611
+ if ignore_case:
612
+ dst = {x.lower() for x in dst}
613
+
614
+ if member_list:
615
+ src = set(member_list)
616
+ else:
617
+ if ignore_case:
618
+ src = {x for x in dir(from_obj) if (x.lower() not in dst)}
619
+ else:
620
+ src = set(dir(from_obj)) - dst
621
+
622
+ # 2 微调
623
+ if white_list:
624
+ src |= set(white_list)
625
+ if black_list:
626
+ src -= set(black_list)
627
+
628
+ # 3 注入方法
629
+ for x in src:
630
+ setattr(to_obj, x, getattr(from_obj, x))
631
+ if check and (x in dst or (ignore_case and x.lower() in dst)):
632
+ logging.warning(f'Conflict of the same name! {to_obj}.{x}')
633
+
634
+
635
+ def __debug系列():
636
+ pass
637
+
638
+
639
+ def func_input_message(depth=2) -> dict:
640
+ """假设调用了这个函数的函数叫做f,这个函数会获得
641
+ 调用f的时候输入的参数信息,返回一个dict,键值对为
642
+ fullfilename:完整文件名
643
+ filename:文件名
644
+ funcname:所在函数名
645
+ lineno:代码所在行号
646
+ comment:尾巴的注释
647
+ depth:深度
648
+ funcnames:整个调用过程的函数名,用/隔开,例如...
649
+
650
+ argnames:变量名(list),这里的变量名也有可能是一个表达式
651
+ types:变量类型(list),如果是表达式,类型指表达式的运算结果类型
652
+ argvals:变量值(list)
653
+
654
+ 这样以后要加新的键值对也很方便
655
+
656
+ :param depth: 需要分析的层级
657
+ 0,当前func_input_message函数的参数输入情况
658
+ 1,调用func_input_message的函数 f 参数输入情况
659
+ 2,调用 f 的函数 g ,g的参数输入情况
660
+
661
+ 参考: func_input_message 的具体使用方法可以参考 dformat 函数
662
+ 细节:inspect可以获得函数签名,也可以获得一个函数各个参数的输入值,但我想要展现的是原始表达式,
663
+ 例如func(a),以func(1+2)调用,inpect只能获得“a=3”,但我想要的是“1+2=3”的效果
664
+ """
665
+ res = {}
666
+ # 1 找出调用函数的代码
667
+ ss = inspect.stack()
668
+ frameinfo = ss[depth]
669
+ arginfo = inspect.getargvalues(ss[depth - 1][0])
670
+ if arginfo.varargs:
671
+ origin_args = arginfo.locals[arginfo.varargs]
672
+ else:
673
+ origin_args = list(map(lambda x: arginfo.locals[x], arginfo.args))
674
+
675
+ res['fullfilename'] = frameinfo.filename
676
+ res['filename'] = os.path.basename(frameinfo.filename)
677
+ res['funcname'] = frameinfo.function
678
+ res['lineno'] = frameinfo.lineno
679
+ res['depth'] = len(ss)
680
+ ls_ = list(map(lambda x: x.function, ss))
681
+ # ls.reverse()
682
+ res['funcnames'] = '/'.join(ls_)
683
+
684
+ if frameinfo.code_context:
685
+ code_line = frameinfo.code_context[0].strip()
686
+ else: # 命令模式无法获得代码,是一个None对象
687
+ code_line = ''
688
+
689
+ funcname = ss[depth - 1].function # 调用的函数名
690
+ # 这一行代码不一定是从“funcname(”开始,所以要用find找到开始位置
691
+ code = code_line[code_line.find(funcname + '(') + len(funcname):]
692
+
693
+ # 2 先找到函数的()中参数列表,需要以')'作为分隔符分析
694
+ # TODO 可以考虑用ast重实现
695
+ ls = code.split(')')
696
+ logo, i = True, 1
697
+ while logo and i <= len(ls):
698
+ # 先将'='做特殊处理,防止字典类参数导致的语法错误
699
+ s = ')'.join(ls[:i]).replace('=', '+') + ')'
700
+ try:
701
+ compile(s, '<string>', 'single')
702
+ except SyntaxError:
703
+ i += 1
704
+ else: # 正常情况
705
+ logo = False
706
+ code = ')'.join(ls[:i])[1:]
707
+
708
+ # 3 获得注释
709
+ # 这个注释实现的不是很完美,不过影响应该不大,还没有想到比较完美的解决方案
710
+ t = ')'.join(ls[i:])
711
+ comment = t[t.find('#'):] if '#' in t else ''
712
+ res['comment'] = comment
713
+
714
+ # 4 获得变量名
715
+ ls = code.split(',')
716
+ n = len(ls)
717
+ argnames = list()
718
+ i, j = 0, 1
719
+ while j <= n:
720
+ s = ','.join(ls[i:j])
721
+ try:
722
+ compile(s.lstrip(), '<string>', 'single')
723
+ except SyntaxError:
724
+ j += 1
725
+ else: # 没有错误的时候执行
726
+ argnames.append(s.strip())
727
+ i = j
728
+ j = i + 1
729
+
730
+ # 5 获得变量值和类型
731
+ res['argvals'] = origin_args
732
+ res['types'] = list(map(typename, origin_args))
733
+
734
+ if not argnames: # 如果在命令行环境下调用,argnames会有空,需要根据argvals长度置空名称
735
+ argnames = [''] * len(res['argvals'])
736
+ res['argnames'] = argnames
737
+
738
+ return res
739
+
740
+
741
+ def dformat(*args, depth=2,
742
+ delimiter=' ' * 4,
743
+ strfunc=repr,
744
+ fmt='[{depth:02}]{filename}/{lineno}: {argmsg}',
745
+ subfmt='{name}<{tp}>={val}'):
746
+ r"""
747
+ :param args: 需要检查的表达式
748
+ 这里看似没有调用,其实在func_input_message用inspect会提取到args的信息
749
+ :param depth: 处理对象
750
+ 默认值2,即处理dformat本身
751
+ 2以下值没意义
752
+ 2以上的值,可以不传入args参数
753
+ :param delimiter: 每个变量值展示之间的分界
754
+ :param strfunc: 对每个变量值的文本化方法,常见的有repr、str
755
+ :param fmt: 展示格式,除了func_input_message中的关键字,新增
756
+ argmsg:所有的「变量名=变量值」,或所有的「变量名<变量类型>=变量值」,或自定义格式,采用delimiter作为分界符
757
+ 旧版还用过这种格式: '{filename}/{funcname}/{lineno}: {argmsg} {comment}'
758
+ :param subfmt: 自定义每个变量值对的显示形式
759
+ name,变量名
760
+ val,变量值
761
+ tp,变量类型
762
+ :return: 返回格式化好的文本字符串
763
+ """
764
+ res = func_input_message(depth)
765
+ ls = [subfmt.format(name=name, val=strfunc(val), tp=tp)
766
+ for name, val, tp in zip(res['argnames'], res['argvals'], res['types'])]
767
+ res['argmsg'] = delimiter.join(ls)
768
+ return fmt.format(**res)
769
+
770
+
771
+ def dprint(*args, **kwargs):
772
+ r"""
773
+ # 故意写的特别复杂,测试在极端情况下是否能正确解析出表达式
774
+ >> a, b = 1, 2
775
+ >> re.sub(str(dprint(1, b, a, "aa" + "bb)", "a[,ba\nbb""b", [2, 3])), '', '##') # 注释 # 注
776
+ [08]<doctest debuglib.dprint[1]>/1: 1<int>=1 b<int>=2 a<int>=1 "aa" + "bb)"<str>='aabb)' "a[,ba\nbb""b"<str>='a[,ba\nbbb' [2, 3]<list>=[2, 3] ##') # 注释 # 注
777
+ '##'
778
+ """
779
+ print(dformat(depth=3, **kwargs))
780
+
781
+
782
+ # dprint会被注册进builtins,可以在任意地方直接使用
783
+ setattr(builtins, 'dprint', dprint)
784
+
785
+
786
+ class DPrint:
787
+ """ 用来存储上下文相关变量,进行全局性地调试
788
+
789
+ TODO 需要跟logging库一样,可以获取不同名称的配置
790
+ 可以进行很多扩展,比如输出到stderr还是stdout
791
+ """
792
+
793
+ watch = {}
794
+
795
+ @classmethod
796
+ def reset(cls):
797
+ cls.watch = {}
798
+
799
+ @classmethod
800
+ def format(cls, watch2, show_type=False, sep=' '):
801
+ """
802
+ :param watch2: 必须也是字典类型
803
+ :param show_type: 是否显示每个数值的类型
804
+ :param sep: 每部分的间隔符
805
+ """
806
+ msg = []
807
+ input_msg = func_input_message(2)
808
+ filename, lineno = input_msg['filename'], input_msg['lineno']
809
+ msg.append(f'{filename}/{lineno}')
810
+
811
+ watch3 = cls.watch.copy()
812
+ watch3.update(watch2)
813
+ for k, v in watch3.items():
814
+ if k.startswith('$'):
815
+ # 用 $ 修饰的不显示变量名,直接显示值
816
+ msg.append(f'{v}')
817
+ else:
818
+ if show_type:
819
+ msg.append(f'{k}<{typename(v)}>={repr(v)}')
820
+ else:
821
+ msg.append(f'{k}={repr(v)}')
822
+
823
+ return sep.join(msg)
824
+
825
+
826
+ def format_exception(e, mode=3):
827
+ if mode == 1:
828
+ # 仅获取异常类型的名称
829
+ text = ''.join(traceback.format_exception_only(type(e), e)).strip()
830
+ elif mode == 2:
831
+ # 获取异常类型的名称和附加的错误信息
832
+ text = f"{type(e).__name__}: {e}"
833
+ elif mode == 3:
834
+ text = ''.join(traceback.format_exception(type(e), e, e.__traceback__))
835
+ else:
836
+ raise ValueError
837
+ return text
838
+
839
+
840
+ def prettifystr(s):
841
+ """对一个对象用更友好的方式字符串化
842
+
843
+ :param s: 输入类型不做限制,会将其以友好的形式格式化
844
+ :return: 格式化后的字符串
845
+ """
846
+ title = ''
847
+ if isinstance(s, str):
848
+ pass
849
+ elif isinstance(s, Counter): # Counter要按照出现频率显示
850
+ li = s.most_common()
851
+ title = f'collections.Counter长度:{len(s)}\n'
852
+ # 不使用复杂的pd库,先简单用pprint即可
853
+ # df = pd.DataFrame.from_records(s, columns=['value', 'count'])
854
+ # s = dataframe_str(df)
855
+ s = pprint.pformat(li)
856
+ elif isinstance(s, (list, tuple)):
857
+ title = f'{typename(s)}长度:{len(s)}\n'
858
+ s = pprint.pformat(s)
859
+ elif isinstance(s, (dict, set)):
860
+ title = f'{typename(s)}长度:{len(s)}\n'
861
+ s = pprint.pformat(s)
862
+ else: # 其他的采用默认的pformat
863
+ s = pprint.pformat(s)
864
+ return title + s
865
+
866
+
867
+ class PrettifyStrDecorator:
868
+ """将函数的返回值字符串化(调用 prettifystr 美化)"""
869
+
870
+ def __init__(self, func):
871
+ self.func = func # 使用self.func可以索引回原始函数名称
872
+ self.last_raw_res = None # last raw result,上一次执行函数的原始结果
873
+
874
+ def __call__(self, *args, **kwargs):
875
+ self.last_raw_res = self.func(*args, **kwargs)
876
+ return prettifystr(self.last_raw_res)
877
+
878
+
879
+ def hide_console_window():
880
+ """ 隐藏命令行窗口 """
881
+ import ctypes
882
+ kernel32 = ctypes.WinDLL('kernel32')
883
+ user32 = ctypes.WinDLL('user32')
884
+ SW_HIDE = 0
885
+ hWnd = kernel32.GetConsoleWindow()
886
+ user32.ShowWindow(hWnd, SW_HIDE)
887
+
888
+
889
+ def get_installed_packages():
890
+ """ 使用pip list获取当前环境安装了哪些包 """
891
+ output = subprocess.check_output(["pip", "list"], universal_newlines=True)
892
+ packages = [line.split()[0] for line in output.split("\n")[2:] if line]
893
+ return packages
894
+
895
+
896
+ class OutputLogger(logging.Logger):
897
+ """
898
+ 我在jupyter写代码,经常要print输出一些中间结果。
899
+ 但是当结果很多的时候,又要转存到文件里保存起来查看。
900
+ 要保存到文件时,和普通的print写法是不一样的,一般要新建立一个ls = []的变量。
901
+ 然后print改成ls.append操作,会很麻烦。
902
+
903
+ 就想着能不能自己定义一个类,支持.print方法,不仅能实现正常的输出控制台的功能。
904
+ 也能在需要的时候指定文件路径,会自动将结果存储到文件中。
905
+ """
906
+
907
+ def __init__(self, name='OutputLogger', *, log_file=None, log_mode='a', output_to_console=True):
908
+ """
909
+ :param str name: 记录器的名称。默认为 'OutputLogger'。
910
+ :param log_file: 日志文件的路径。默认为 None,表示不输出到文件。
911
+ :param bool output_to_console: 是否输出到命令行。默认为 True。
912
+ """
913
+ super().__init__(name)
914
+
915
+ self.output_to_console = output_to_console
916
+ self.log_file = log_file
917
+
918
+ self.setLevel(logging.INFO)
919
+ # 创建格式化器
920
+ formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(filename)s/%(lineno)d - %(message)s',
921
+ '%Y-%m-%d %H:%M:%S')
922
+
923
+ # 提前重置为空文件
924
+ if log_file is not None:
925
+ if not os.path.isfile(log_file) or log_mode == 'w':
926
+ with open(log_file, 'w', encoding='utf8') as f:
927
+ f.write('')
928
+
929
+ # 创建文件日志处理器
930
+ if log_file:
931
+ file_handler = logging.FileHandler(log_file, encoding='utf-8')
932
+ file_handler.setLevel(logging.DEBUG) # 日志文件是最详细级别都记录
933
+ file_handler.setFormatter(formatter)
934
+ self.addHandler(file_handler)
935
+
936
+ # 创建命令行日志处理器
937
+ if output_to_console:
938
+ console_handler = logging.StreamHandler()
939
+ console_handler.setLevel(logging.WARNING) # 有些太详细的问题,不想写在控制台,而是写到文件
940
+ console_handler.setFormatter(formatter)
941
+ self.addHandler(console_handler)
942
+
943
+ # 只输出到控制台:标准库的print
944
+ # 只输出到文件:debug, info
945
+ # 同时输出到控制台和文件:warning, error, critical, print
946
+
947
+ def print(self, *args, **kwargs):
948
+ """ 使用print机制,会同时输出到控制台和日志文件 """
949
+ msg = print2string(*args, **kwargs)
950
+
951
+ if self.output_to_console:
952
+ print(msg, end='')
953
+
954
+ if self.log_file:
955
+ with open(self.log_file, 'a', encoding='utf-8') as f:
956
+ f.write(msg)
957
+
958
+ return msg
959
+
960
+ def tprint(self, *args, **kwargs):
961
+ """ 带时间戳的print """
962
+ self.print(utc_now2(), *args, **kwargs)
963
+
964
+ def log_json(self, data):
965
+ """ 类似print,但是是把数据按照json的格式进行记录整理,更加结构化,方便后期处理 """
966
+ data['time'] = utc_timestamp()
967
+ msg = json.dumps(data, ensure_ascii=False, default=str)
968
+ self.print(msg)
969
+
970
+
971
+ def xlmd5(content):
972
+ if isinstance(content, str):
973
+ content = content.encode('utf-8')
974
+ elif not isinstance(content, bytes):
975
+ content = str(content).encode('utf-8')
976
+
977
+ if len(content) <= 32: # 32位以下的字符串,直接返回
978
+ return content
979
+ else:
980
+ return hashlib.md5(content).hexdigest()
981
+
982
+
983
+ @run_once()
984
+ def get_hostname():
985
+ hostname = socket.getfqdn()
986
+ return hostname
987
+
988
+
989
+ @run_once()
990
+ def get_hostname2():
991
+ """ 更加定制化的操作 """
992
+ hostname = socket.getfqdn()
993
+ hostname = hostname.replace('-', '_')
994
+ hostname = hostname.split('.')[0]
995
+ return hostname
996
+
997
+
998
+ @run_once()
999
+ def get_username():
1000
+ return os.path.split(os.path.expanduser('~'))[-1]
1001
+
1002
+
1003
+ class XlThreadPoolExecutor(ThreadPoolExecutor):
1004
+ def __init__(self, *args, **kwargs):
1005
+ super().__init__(*args, **kwargs)
1006
+ self.futures = []
1007
+
1008
+ def submit(self, *args, **kwargs):
1009
+ future = super().submit(*args, **kwargs)
1010
+ self.futures.append(future)
1011
+ return future
1012
+
1013
+ def yield_result(self, timeout=None):
1014
+ for future in self.futures:
1015
+ yield future.result(timeout=timeout)
1016
+
1017
+
1018
+ def shuffle_dict_keys(d):
1019
+ keys = list(d.keys())
1020
+ random.shuffle(keys)
1021
+ d = {k: d[k] for k in keys}
1022
+ return d
1023
+
1024
+
1025
+ def generate_int_hash_from_str(s):
1026
+ """ 对字符串使用md5编码,然后转出一个数值哈希,一般是用来进行随机分组
1027
+ 比如获得一个整数后,对3取余,就是按照余数为0、1、2的情况分3组
1028
+ """
1029
+ return int(hashlib.md5(s.encode()).hexdigest(), 16)
1030
+
1031
+
1032
+ def get_groupid_from_string(s, n_groups):
1033
+ """ 通过计算一个字符串的哈希值来对其进行分组,需要提前知道总组别数n_groups """
1034
+ hash_value = generate_int_hash_from_str(s)
1035
+ return hash_value % n_groups
1036
+
1037
+
1038
+ def safe_div(a, b):
1039
+ """ 安全除法,避免除数为0的情况
1040
+
1041
+ :param a: 被除数
1042
+ :param b: 除数
1043
+ :return: a/b,如果b为0,返回0
1044
+ """
1045
+ if b == 0:
1046
+ return a / sys.float_info.epsilon
1047
+ else:
1048
+ return a / b
1049
+
1050
+
1051
+ def inplace_decorate(parent, func_name, wrapper):
1052
+ """ 将指定的函数替换为装饰器版本
1053
+ 允许在运行时动态地将一个函数或方法替换为其装饰版本。通常用于添加日志、性能测试、事务处理等。
1054
+ (既然可以写成装饰版本,相当于其实要完全替换成另外的函数也是可以的)
1055
+
1056
+ 当然,因为py一切皆对象,这里处理的不是函数,而是其他变量等对象也是可以的
1057
+
1058
+ 这个功能跟直接把原代码替换修改了还是有区别的,如果原函数在被这个装饰之前,已经被其他地方调用,或者被装饰器补充,
1059
+ 太晚使用这个装饰,并不会改变前面已经运行、被捕捉的情况
1060
+ 遇到这种情况,也可以考虑在原函数定义后,直接紧接着加上这个函数重置
1061
+
1062
+ 对于类成员方法,直接用这个设置可能也不行,只能去改源码了
1063
+ 比如要给函数加计算时间的部分,可以考虑使用 get_global_var 等来夸作用域记录时间数据
1064
+ """
1065
+
1066
+ if hasattr(parent, func_name):
1067
+ if callable(wrapper): # 对函数的封装
1068
+ original_func = getattr(parent, func_name)
1069
+
1070
+ @functools.wraps(original_func)
1071
+ def decorated_func(*args, **kwargs):
1072
+ return wrapper(original_func, *args, **kwargs)
1073
+
1074
+ setattr(parent, func_name, decorated_func)
1075
+ else: # 对数值的封装,不过如果是数值,其实也没必要调用这个函数,直接赋值就好了
1076
+ return setattr(parent, func_name, wrapper)
1077
+ else: # 否则按照字典的模式来处理
1078
+
1079
+ original_func = parent[func_name]
1080
+
1081
+ @functools.wraps(original_func)
1082
+ def decorated_func(*args, **kwargs):
1083
+ return wrapper(original_func, *args, **kwargs)
1084
+
1085
+ parent[func_name] = decorated_func
1086
+
1087
+
1088
+ def check_counter(data, top_n=10):
1089
+ """ 将一个数据data转为Counter进行频数分析 """
1090
+ # 1 如果是list、tuple类型,需要转counter
1091
+ if isinstance(data, (list, tuple)):
1092
+ data = Counter(data)
1093
+ if not isinstance(data, Counter):
1094
+ raise ValueError(f'输入的数据类型不对,应该是Counter类型,而不是{typename(data)}')
1095
+
1096
+ # 2 列出出现次数最多的top_n条目
1097
+ # 打印基本统计信息
1098
+ total_items = sum(data.values())
1099
+ print(f"总条目数: {total_items}")
1100
+
1101
+ if top_n > 0:
1102
+ top_items = data.most_common(top_n)
1103
+ max_n = len(data)
1104
+ print(f"出现次数最多的{min(top_n, max_n)}/{max_n}条数据(频率):")
1105
+ for item, count in top_items:
1106
+ print(f"\t{item}\t{count}")
1107
+
1108
+ # 3 打印基本统计信息
1109
+ # 对原始Counter的计数值进行再计数
1110
+ count_frequencies = Counter(data.values())
1111
+
1112
+ # 打印各计数值出现的次数
1113
+ print("各计数值出现的次数,频率的频率(频率分布):")
1114
+ for count, frequency in count_frequencies.most_common():
1115
+ print(f"\t{count}\t{frequency}")
1116
+
1117
+
1118
+ def tprint(*args, **kwargs):
1119
+ """ 带时间戳的print """
1120
+ print(utc_now2(), *args, **kwargs)
1121
+
1122
+
1123
+ def is_valid_identifier(name):
1124
+ """ 判断是否是合法的标识符 """
1125
+ return re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', name)
1126
+
1127
+
1128
+ def get_number_width(n):
1129
+ """ 判断数值n的长度
1130
+
1131
+ 参考资料:https://jstrieb.github.io/posts/digit-length/
1132
+
1133
+ >>> get_number_width(0)
1134
+ 1
1135
+ >>> get_number_width(9)
1136
+ 1
1137
+ >>> get_number_width(10)
1138
+ 2
1139
+ >>> get_number_width(97)
1140
+ 2
1141
+ """
1142
+ # assert n > 0
1143
+ # return math.ceil(math.log10(n + 1))
1144
+
1145
+ return 1 if n == 0 else (math.floor(math.log10(n)) + 1)
1146
+
1147
+
1148
+ def aligned_range(start, stop=None, step=1):
1149
+ """ 返回按照域宽对齐的数字序列 """
1150
+ if stop is None:
1151
+ start, stop = 0, start
1152
+
1153
+ max_width = get_number_width(stop - step)
1154
+ format_str = '{:0' + str(max_width) + 'd}'
1155
+
1156
+ return [format_str.format(i) for i in range(start, stop, step)]
1157
+
1158
+
1159
+ def percentage_and_value(numbers, precision=2, *, total=None, sep='.'):
1160
+ """ 对输入的一串数值,转换成一种特殊的表达格式 "百分比.次数"
1161
+
1162
+ :param list numbers: 数值列表
1163
+ :param int precision: 百分比的精度(小数点后的位数),默认为 2
1164
+ :param int total: 总数,如果不输入,则默认为输入数值的和
1165
+ :param str sep: 分隔符
1166
+ :return: 整数部分是比例,小数部分是原始数值的整数部分
1167
+ """
1168
+ if total is None:
1169
+ total = sum(numbers)
1170
+ width = get_number_width(total)
1171
+ result = []
1172
+ for num in numbers:
1173
+ percent = safe_div(num, total) * 10 ** precision
1174
+ result.append(f"{percent:.0f}{sep}{num:0{width}d}")
1175
+ return result
1176
+
1177
+
1178
+ def get_local_ip():
1179
+ """ 获得本地ip,代码由deepseek提供 """
1180
+ try:
1181
+ # 使用UDP协议连接到外部服务器
1182
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1183
+ s.connect(("8.8.8.8", 80)) # Google的DNS服务器和常用端口
1184
+ local_ip = s.getsockname()[0] # 获取套接字的本地地址
1185
+ s.close()
1186
+ return local_ip
1187
+ except Exception as e:
1188
+ # 如果失败,尝试通过主机名获取IP列表
1189
+ try:
1190
+ hostname = socket.gethostname()
1191
+ ips = socket.gethostbyname_ex(hostname)[2] # 获取所有IPv4地址
1192
+ for ip in ips:
1193
+ if ip != "127.0.0.1": # 排除回环地址
1194
+ return ip
1195
+ return "127.0.0.1" # 默认返回回环地址
1196
+ except:
1197
+ raise ValueError("无法获取IP地址")