pyxllib 0.0.43__py3-none-any.whl → 0.3.197__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 (186) hide show
  1. pyxllib/__init__.py +9 -2
  2. pyxllib/algo/__init__.py +8 -0
  3. pyxllib/algo/disjoint.py +54 -0
  4. pyxllib/algo/geo.py +541 -0
  5. pyxllib/{util/mathlib.py → algo/intervals.py} +172 -36
  6. pyxllib/algo/matcher.py +389 -0
  7. pyxllib/algo/newbie.py +166 -0
  8. pyxllib/algo/pupil.py +629 -0
  9. pyxllib/algo/shapelylib.py +67 -0
  10. pyxllib/algo/specialist.py +241 -0
  11. pyxllib/algo/stat.py +494 -0
  12. pyxllib/algo/treelib.py +149 -0
  13. pyxllib/algo/unitlib.py +66 -0
  14. pyxllib/autogui/__init__.py +5 -0
  15. pyxllib/autogui/activewin.py +246 -0
  16. pyxllib/autogui/all.py +9 -0
  17. pyxllib/autogui/autogui.py +852 -0
  18. pyxllib/autogui/uiautolib.py +362 -0
  19. pyxllib/autogui/virtualkey.py +102 -0
  20. pyxllib/autogui/wechat.py +827 -0
  21. pyxllib/autogui/wechat_msg.py +421 -0
  22. pyxllib/autogui/wxautolib.py +84 -0
  23. pyxllib/cv/__init__.py +1 -11
  24. pyxllib/cv/expert.py +267 -0
  25. pyxllib/cv/{imlib.py → imfile.py} +18 -83
  26. pyxllib/cv/imhash.py +39 -0
  27. pyxllib/cv/pupil.py +9 -0
  28. pyxllib/cv/rgbfmt.py +1525 -0
  29. pyxllib/cv/slidercaptcha.py +137 -0
  30. pyxllib/cv/trackbartools.py +163 -49
  31. pyxllib/cv/xlcvlib.py +1040 -0
  32. pyxllib/cv/xlpillib.py +423 -0
  33. pyxllib/data/__init__.py +0 -0
  34. pyxllib/data/echarts.py +240 -0
  35. pyxllib/data/jsonlib.py +89 -0
  36. pyxllib/{util/oss2_.py → data/oss.py} +11 -9
  37. pyxllib/data/pglib.py +1127 -0
  38. pyxllib/data/sqlite.py +568 -0
  39. pyxllib/{util → data}/sqllib.py +13 -31
  40. pyxllib/ext/JLineViewer.py +505 -0
  41. pyxllib/ext/__init__.py +6 -0
  42. pyxllib/{util → ext}/demolib.py +119 -35
  43. pyxllib/ext/drissionlib.py +277 -0
  44. pyxllib/ext/kq5034lib.py +12 -0
  45. pyxllib/{util/main.py → ext/old.py} +122 -284
  46. pyxllib/ext/qt.py +449 -0
  47. pyxllib/ext/robustprocfile.py +497 -0
  48. pyxllib/ext/seleniumlib.py +76 -0
  49. pyxllib/{util/tklib.py → ext/tk.py} +10 -11
  50. pyxllib/ext/unixlib.py +827 -0
  51. pyxllib/ext/utools.py +351 -0
  52. pyxllib/{util/webhooklib.py → ext/webhook.py} +45 -17
  53. pyxllib/ext/win32lib.py +40 -0
  54. pyxllib/ext/wjxlib.py +88 -0
  55. pyxllib/ext/wpsapi.py +124 -0
  56. pyxllib/ext/xlwork.py +9 -0
  57. pyxllib/ext/yuquelib.py +1105 -0
  58. pyxllib/file/__init__.py +17 -0
  59. pyxllib/file/docxlib.py +761 -0
  60. pyxllib/{util → file}/gitlib.py +40 -27
  61. pyxllib/file/libreoffice.py +165 -0
  62. pyxllib/file/movielib.py +148 -0
  63. pyxllib/file/newbie.py +10 -0
  64. pyxllib/file/onenotelib.py +1469 -0
  65. pyxllib/file/packlib/__init__.py +330 -0
  66. pyxllib/{util → file/packlib}/zipfile.py +598 -195
  67. pyxllib/file/pdflib.py +426 -0
  68. pyxllib/file/pupil.py +185 -0
  69. pyxllib/file/specialist/__init__.py +685 -0
  70. pyxllib/{basic/_5_dirlib.py → file/specialist/dirlib.py} +364 -93
  71. pyxllib/file/specialist/download.py +193 -0
  72. pyxllib/file/specialist/filelib.py +2829 -0
  73. pyxllib/file/xlsxlib.py +3131 -0
  74. pyxllib/file/xlsyncfile.py +341 -0
  75. pyxllib/prog/__init__.py +5 -0
  76. pyxllib/prog/cachetools.py +64 -0
  77. pyxllib/prog/deprecatedlib.py +233 -0
  78. pyxllib/prog/filelock.py +42 -0
  79. pyxllib/prog/ipyexec.py +253 -0
  80. pyxllib/prog/multiprogs.py +940 -0
  81. pyxllib/prog/newbie.py +451 -0
  82. pyxllib/prog/pupil.py +1197 -0
  83. pyxllib/{sitepackages.py → prog/sitepackages.py} +5 -3
  84. pyxllib/prog/specialist/__init__.py +391 -0
  85. pyxllib/prog/specialist/bc.py +203 -0
  86. pyxllib/prog/specialist/browser.py +497 -0
  87. pyxllib/prog/specialist/common.py +347 -0
  88. pyxllib/prog/specialist/datetime.py +199 -0
  89. pyxllib/prog/specialist/tictoc.py +240 -0
  90. pyxllib/prog/specialist/xllog.py +180 -0
  91. pyxllib/prog/xlosenv.py +108 -0
  92. pyxllib/stdlib/__init__.py +17 -0
  93. pyxllib/{util → stdlib}/tablepyxl/__init__.py +1 -3
  94. pyxllib/{util → stdlib}/tablepyxl/style.py +1 -1
  95. pyxllib/{util → stdlib}/tablepyxl/tablepyxl.py +2 -4
  96. pyxllib/text/__init__.py +8 -0
  97. pyxllib/text/ahocorasick.py +39 -0
  98. pyxllib/text/airscript.js +744 -0
  99. pyxllib/text/charclasslib.py +121 -0
  100. pyxllib/text/jiebalib.py +267 -0
  101. pyxllib/text/jinjalib.py +32 -0
  102. pyxllib/text/jsa_ai_prompt.md +271 -0
  103. pyxllib/text/jscode.py +922 -0
  104. pyxllib/text/latex/__init__.py +158 -0
  105. pyxllib/text/levenshtein.py +303 -0
  106. pyxllib/text/nestenv.py +1215 -0
  107. pyxllib/text/newbie.py +300 -0
  108. pyxllib/text/pupil/__init__.py +8 -0
  109. pyxllib/text/pupil/common.py +1121 -0
  110. pyxllib/text/pupil/xlalign.py +326 -0
  111. pyxllib/text/pycode.py +47 -0
  112. pyxllib/text/specialist/__init__.py +8 -0
  113. pyxllib/text/specialist/common.py +112 -0
  114. pyxllib/text/specialist/ptag.py +186 -0
  115. pyxllib/text/spellchecker.py +172 -0
  116. pyxllib/text/templates/echart_base.html +11 -0
  117. pyxllib/text/templates/highlight_code.html +17 -0
  118. pyxllib/text/templates/latex_editor.html +103 -0
  119. pyxllib/text/vbacode.py +17 -0
  120. pyxllib/text/xmllib.py +747 -0
  121. pyxllib/xl.py +39 -0
  122. pyxllib/xlcv.py +17 -0
  123. pyxllib-0.3.197.dist-info/METADATA +48 -0
  124. pyxllib-0.3.197.dist-info/RECORD +126 -0
  125. {pyxllib-0.0.43.dist-info → pyxllib-0.3.197.dist-info}/WHEEL +4 -5
  126. pyxllib/basic/_1_strlib.py +0 -945
  127. pyxllib/basic/_2_timelib.py +0 -488
  128. pyxllib/basic/_3_pathlib.py +0 -916
  129. pyxllib/basic/_4_loglib.py +0 -419
  130. pyxllib/basic/__init__.py +0 -54
  131. pyxllib/basic/arrow_.py +0 -250
  132. pyxllib/basic/chardet_.py +0 -66
  133. pyxllib/basic/dirlib.py +0 -529
  134. pyxllib/basic/dprint.py +0 -202
  135. pyxllib/basic/extension.py +0 -12
  136. pyxllib/basic/judge.py +0 -31
  137. pyxllib/basic/log.py +0 -204
  138. pyxllib/basic/pathlib_.py +0 -705
  139. pyxllib/basic/pytictoc.py +0 -102
  140. pyxllib/basic/qiniu_.py +0 -61
  141. pyxllib/basic/strlib.py +0 -761
  142. pyxllib/basic/timer.py +0 -132
  143. pyxllib/cv/cv.py +0 -834
  144. pyxllib/cv/cvlib/_1_geo.py +0 -543
  145. pyxllib/cv/cvlib/_2_cvprcs.py +0 -309
  146. pyxllib/cv/cvlib/_2_imgproc.py +0 -594
  147. pyxllib/cv/cvlib/_3_pilprcs.py +0 -80
  148. pyxllib/cv/cvlib/_4_cvimg.py +0 -211
  149. pyxllib/cv/cvlib/__init__.py +0 -10
  150. pyxllib/cv/debugtools.py +0 -82
  151. pyxllib/cv/fitz_.py +0 -300
  152. pyxllib/cv/installer.py +0 -42
  153. pyxllib/debug/_0_installer.py +0 -38
  154. pyxllib/debug/_1_typelib.py +0 -277
  155. pyxllib/debug/_2_chrome.py +0 -198
  156. pyxllib/debug/_3_showdir.py +0 -161
  157. pyxllib/debug/_4_bcompare.py +0 -140
  158. pyxllib/debug/__init__.py +0 -49
  159. pyxllib/debug/bcompare.py +0 -132
  160. pyxllib/debug/chrome.py +0 -198
  161. pyxllib/debug/installer.py +0 -38
  162. pyxllib/debug/showdir.py +0 -158
  163. pyxllib/debug/typelib.py +0 -278
  164. pyxllib/image/__init__.py +0 -12
  165. pyxllib/torch/__init__.py +0 -20
  166. pyxllib/torch/modellib.py +0 -37
  167. pyxllib/torch/trainlib.py +0 -344
  168. pyxllib/util/__init__.py +0 -20
  169. pyxllib/util/aip_.py +0 -141
  170. pyxllib/util/casiadb.py +0 -59
  171. pyxllib/util/excellib.py +0 -495
  172. pyxllib/util/filelib.py +0 -612
  173. pyxllib/util/jsondata.py +0 -27
  174. pyxllib/util/jsondata2.py +0 -92
  175. pyxllib/util/labelmelib.py +0 -139
  176. pyxllib/util/onepy/__init__.py +0 -29
  177. pyxllib/util/onepy/onepy.py +0 -574
  178. pyxllib/util/onepy/onmanager.py +0 -170
  179. pyxllib/util/pyautogui_.py +0 -219
  180. pyxllib/util/textlib.py +0 -1305
  181. pyxllib/util/unorder.py +0 -22
  182. pyxllib/util/xmllib.py +0 -639
  183. pyxllib-0.0.43.dist-info/METADATA +0 -39
  184. pyxllib-0.0.43.dist-info/RECORD +0 -80
  185. pyxllib-0.0.43.dist-info/top_level.txt +0 -1
  186. {pyxllib-0.0.43.dist-info → pyxllib-0.3.197.dist-info/licenses}/LICENSE +0 -0
@@ -2,12 +2,10 @@
2
2
  # -*- coding: utf-8 -*-
3
3
  # @Author : 陈坤泽
4
4
  # @Email : 877362867@qq.com
5
- # @Data : 2019/12/04 11:16
5
+ # @Date : 2019/12/04 11:16
6
6
 
7
7
 
8
- """
9
- 数学相关功能库
10
- 目前主要就只有一个区间类
8
+ """ 区间类
11
9
 
12
10
  关于区间类,可以参考: https://github.com/AlexandreDecan/python-intervals
13
11
  但其跟我的业务场景有区别,不太适用,所以这里还是开发了自己的功能库
@@ -15,7 +13,10 @@
15
13
  文档: https://histudy.yuque.com/docs/share/365f3a75-28d0-4595-bc80-5e9d6ab36f71#
16
14
  """
17
15
 
18
- from pyxllib.debug import *
16
+ import collections
17
+ import itertools
18
+ import math
19
+ import re
19
20
 
20
21
 
21
22
  class Interval:
@@ -246,7 +247,7 @@ class ReMatch(Interval):
246
247
  """
247
248
  if getattr(regs, 'regs', None):
248
249
  # 从一个类match对象来初始化
249
- m = pos
250
+ m = regs
250
251
  self.pos = getattr(m, 'pos', None)
251
252
  self.endpos = getattr(m, 'endpos', None)
252
253
  self.lastindex = getattr(m, 'lastindex', None)
@@ -346,7 +347,7 @@ class Intervals:
346
347
  return li
347
348
 
348
349
  def merge_intersect_interval(self, adjacent=False):
349
- """将存在相交的区域进行合并
350
+ """ 将存在相交的区域进行合并
350
351
 
351
352
  :param adjacent: 如果相邻紧接,也进行拼接
352
353
 
@@ -448,11 +449,11 @@ class Intervals:
448
449
 
449
450
  for inter in self.merge_intersect_interval(adjacent=adjacent):
450
451
  # 匹配范围外的文本处理
451
- if inter.start() >= idx:
452
+ if inter.start() > idx:
452
453
  res.append(func2(idx, inter.start()))
453
- idx = inter.end()
454
454
  # 匹配范围内的处理
455
455
  res.append(func1(inter.regs))
456
+ idx = inter.end()
456
457
  if idx < len(s): res.append(func2(idx, len(s)))
457
458
  return ''.join(res)
458
459
 
@@ -462,7 +463,7 @@ class Intervals:
462
463
  :param arg1: 可以输入一个自定义函数
463
464
  :param arg2: 可以配合arg1使用,功能同str.replace(arg1, arg2)
464
465
  :param adjacent: 替换的时候,为了避免混乱出错,是先要合并重叠的区间集的
465
- 这里有个adjacent参数,True表示临接的区间会合并,反之则不会合并临接区间
466
+ 这里有个adjacent参数,True表示邻接的区间会合并,反之则不会合并临接区间
466
467
 
467
468
  >>> s = '0123456789'
468
469
  >>> inters = Intervals([(2, 5), (7, 8)])
@@ -615,6 +616,41 @@ class Intervals:
615
616
  li.append(a & b)
616
617
  return Intervals(li)
617
618
 
619
+ def is_adjacent_and(self, other):
620
+ """ __and__运算的变形,两区间邻接时也认为相交
621
+
622
+ >>> Intervals([(2, 4), (9, 11)]).is_adjacent_and(Interval(0, 10))
623
+ True
624
+ >>> Intervals([(1, 5), (6, 8)]).is_adjacent_and(Intervals([(2, 7), (7, 9)]))
625
+ True
626
+ >>> Intervals([(2, 11)]).is_adjacent_and(Intervals())
627
+ False
628
+ >>> Intervals().is_adjacent_and(Intervals([(2, 11)]))
629
+ False
630
+ >>> Intervals([(2, 11)]).is_adjacent_and(Interval(11, 13))
631
+ True
632
+ """
633
+ # 0 区间 转 区间集
634
+ if isinstance(other, Interval):
635
+ other = Intervals([other])
636
+
637
+ # 1 区间集和区间集做相交运算,生成一个新的区间集
638
+ """假设a、b都是从左到右按顺序取得,所以可以对b的遍历进行一定过滤简化"""
639
+ A = self.merge_intersect_interval()
640
+ B = other.merge_intersect_interval()
641
+ li, k = [], 0
642
+ for a in A:
643
+ for j in range(k, len(B)):
644
+ b = B[j]
645
+ if b.end() < a.start():
646
+ # B[0~j]都在a前面,在后续的a中,可以直接从B[j]开始找
647
+ k = j
648
+ elif b.start() > a.end(): # b已经到a右边,后面的b不用再找了,不会有相交
649
+ break
650
+ else: # 可能有相交
651
+ return True
652
+ return False
653
+
618
654
  def __contains__(self, other):
619
655
  r"""
620
656
  >>> Interval(3, 5) in Intervals([(2, 6)])
@@ -749,7 +785,11 @@ def iter_intervals(arg):
749
785
  yield t
750
786
 
751
787
 
752
- def highlight_intervals(content, intervals, colors=None, background=True, showmath=False):
788
+ def highlight_intervals(content, intervals, colors=None, background=True,
789
+ use_mathjax=False,
790
+ only_body=False,
791
+ title='highlight_intervals',
792
+ set_pre='<pre class="prettyprint nocode linenums" style="white-space: pre-wrap;">'):
753
793
  """文本匹配可视化
754
794
  获得高亮显示的匹配区间的html代码
755
795
 
@@ -759,17 +799,25 @@ def highlight_intervals(content, intervals, colors=None, background=True, showma
759
799
  Intervals、[(2,4), (6,8)]
760
800
 
761
801
  请自行保证区间嵌套语法正确性,本函数不检查处理嵌套混乱错误问题
802
+ :param set_pre: 设置<pre>显示格式。
803
+ 标准 不自动换行: '<pre class="prettyprint nocode linenums">'
804
+ 比如常见的,对于太长的文本行,可以自动断行:
805
+ set_pre='<pre class="prettyprint nocode linenums" style="white-space: pre-wrap;">'
762
806
  :param colors: 一个数组,和intervals一一对应,轮询使用的颜色
763
807
  默认值为: ['red']
764
808
  :param background:
765
809
  True,使用背景色
766
810
  False,不使用背景色,而是字体颜色
767
- :param showmath:
811
+ :param use_mathjax:
768
812
  True,渲染公式
769
813
  False,不渲染公式,只以文本展示
814
+ :param only_body: 不返回完整的html页面内容,只有body主体内容
770
815
  """
771
816
  # 1 存储要插入的html样式
772
817
  from collections import defaultdict
818
+ import html
819
+ from pyxllib.text.xmllib import get_jinja_template
820
+
773
821
  d = defaultdict(str)
774
822
 
775
823
  # 2 其他所有子组从颜色列表取颜色清单,每组一个颜色
@@ -790,28 +838,7 @@ def highlight_intervals(content, intervals, colors=None, background=True, showma
790
838
  d[r] = '</font>' + d[r]
791
839
 
792
840
  # 3 拼接最终的html代码
793
- # (1)配一个特殊的pre样式
794
- head = r"""<!DOCTYPE html>
795
- <html>
796
-
797
- <head>
798
- <script src="https://cdn.jsdelivr.net/gh/google/code-prettify@master/loader/run_prettify.js"></script>
799
- """
800
- if showmath:
801
- head += """<script src="https://a.cdn.histudy.com/lib/config/mathjax_config-klxx.js?v=1.1"></script>
802
- <script type="text/javascript" async="" src="https://a.cdn.histudy.com/lib/mathjax/2.7.1/MathJax/MathJax.js?config=TeX-AMS-MML_SVG">
803
- MathJax.Hub.Config(MATHJAX_KLXX_CONFIG);
804
- </script>
805
- """
806
- head += r"""</head>
807
-
808
- <body>
809
- <pre class="prettyprint nocode linenums">
810
- """
811
- tail = '</pre>\n</body>\n</html>'
812
-
813
- # (2)辅助变量
814
- res = [head]
841
+ res = [set_pre]
815
842
  s = content
816
843
  idxs = sorted(d.keys()) # 按顺序取需要插入的下标
817
844
 
@@ -823,6 +850,115 @@ MathJax.Hub.Config(MATHJAX_KLXX_CONFIG);
823
850
  if idxs: # 最后一个标记
824
851
  res.append(d[idxs[-1]])
825
852
  res.append(s[idxs[-1]:])
826
- res.append(tail)
853
+ if not idxs:
854
+ res.append(s)
855
+ res.append('</pre>')
856
+
857
+ if only_body:
858
+ return ''.join(res)
859
+ else:
860
+ return get_jinja_template('highlight_code.html').render(title=title, body=''.join(res), use_mathjax=use_mathjax)
861
+
862
+
863
+ class StrIdxBack:
864
+ r"""字符串删除部分干扰字符后,对新字符串匹配并回溯找原字符串的下标
865
+
866
+ >>> ob = StrIdxBack('bxx ax xbxax')
867
+ >>> ob.delchars(r'[ x]+')
868
+ >>> ob # 删除空格、删除字符x
869
+ baba
870
+ >>> print(ob.idx) # keystr中与原字符串对应位置:(0, 5, 9, 11)
871
+ (0, 5, 9, 11)
872
+ >>> m = re.match(r'b(ab)', ob.keystr)
873
+ >>> m = ob.matchback(m)
874
+ >>> m.group(1)
875
+ 'ax xb'
876
+ >>> ob.search('ab') # 找出原字符串中内容:'ax xb'
877
+ 'ax xb'
878
+ """
827
879
 
828
- return ''.join(res)
880
+ def __init__(self, s):
881
+ self.oristr = s
882
+ self.idx = tuple(range(len(s))) # 存储还保留着内容的下标
883
+ self.keystr = s
884
+
885
+ def delchars(self, pattern, flags=0):
886
+ r""" 模仿正则的替换语法
887
+ 但是不用输入替换目标s,以及目标格式,因为都是删除操作
888
+
889
+ 利用正则可以知道被删除的是哪个区间范围
890
+ >>> ob = StrIdxBack('abc123df4a'); ob.delchars(r'\d+'); str(ob)
891
+ 'abcdfa'
892
+ >>> ob.idx
893
+ (0, 1, 2, 6, 7, 9)
894
+ """
895
+ k = 0
896
+ idxs = []
897
+
898
+ def repl(m):
899
+ nonlocal k, idxs
900
+ idxs.append(self.idx[k:m.start(0)])
901
+ k = m.end(0)
902
+ return ''
903
+
904
+ self.keystr = re.sub(pattern, repl, self.keystr, flags=flags)
905
+ idxs.append(self.idx[k:])
906
+ self.idx = tuple(itertools.chain(*idxs))
907
+
908
+ def compare_newstr(self, limit=300):
909
+ r"""比较直观的比较字符串前后变化
910
+
911
+ newstr相对于oldnew作展开,比较直观的显示字符串前后变化差异
912
+ >>> ob = StrIdxBack('abab'); ob.delchars('b'); ob.compare_newstr()
913
+ 'a a '
914
+ """
915
+ s1 = self.oristr
916
+ dd = set(self.idx)
917
+
918
+ s2 = []
919
+ k = 0
920
+ for i in range(min(len(s1), limit)):
921
+ if i in dd:
922
+ s2.append(s1[i])
923
+ k += 1
924
+ else:
925
+ if ord(s1[i]) < 128:
926
+ if s1[i] == ' ': # 原来是空格的,删除后要用_表示
927
+ s2.append('_')
928
+ else: # 原始不是空格的,可以用空格表示已被删除
929
+ s2.append(' ')
930
+ else: # 中文字符要用两个空格表示才能对齐
931
+ s2.append(' ')
932
+ s2 = ''.join(s2)
933
+ s2 = s2.replace('\n', r'\n')
934
+
935
+ return s2
936
+
937
+ def compare(self, limit=300):
938
+ """比较直观的比较字符串前后变化"""
939
+ s1 = self.oristr
940
+
941
+ s1 = s1.replace('\n', r'\n')[:limit]
942
+ s2 = self.compare_newstr(limit)
943
+
944
+ return s1 + '\n' + s2 + '\n'
945
+
946
+ def matchback(self, m):
947
+ """输入一个keystr匹配的match对象,将其映射回oristr的match对象"""
948
+ regs = []
949
+ for rs in getattr(m, 'regs'):
950
+ regs.append((self.idx[rs[0]], self.idx[rs[1] - 1] + 1)) # 注意右边界的处理有细节
951
+ return ReMatch(regs, self.oristr, m.pos, len(self.oristr), m.lastindex, m.lastgroup, m.re)
952
+
953
+ def search(self, pattern):
954
+ """在新字符串上查找模式,但是返回的是原字符串的相关下标数据"""
955
+ m = re.search(pattern, self.keystr)
956
+ if m:
957
+ m = self.matchback(m) # pycharm这里会提示m没有regs的成员变量,其实是正常的,没问题
958
+ return m.group()
959
+ else:
960
+ return ''
961
+
962
+ def __repr__(self):
963
+ """返回处理后当前的新字符串"""
964
+ return self.keystr
@@ -0,0 +1,389 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # @Author : 陈坤泽
4
+ # @Email : 877362867@qq.com
5
+ # @Date : 2023/09/16
6
+
7
+ from pyxllib.prog.pupil import check_install_package
8
+ # check_install_package('sklearn', 'scikit-learn')
9
+
10
+ # 这个需要C++14编译器 https://download.microsoft.com/download/5/f/7/5f7acaeb-8363-451f-9425-68a90f98b238/visualcppbuildtools_full.exe
11
+ # 在需要的时候安装,防止只是想用pyxllib很简单的功能,但是在pip install阶段处理过于麻烦
12
+ # 字符串计算编辑距离需要
13
+ # check_install_package('Levenshtein', 'python-Levenshtein')
14
+
15
+ from collections import defaultdict
16
+ import heapq
17
+ import math
18
+ import warnings
19
+
20
+ warnings.filterwarnings("ignore", message="loaded more than 1 DLL from .libs:")
21
+ warnings.filterwarnings("ignore", category=FutureWarning,
22
+ module="sklearn.cluster._agglomerative",
23
+ lineno=1005)
24
+
25
+ from more_itertools import chunked
26
+
27
+ from tqdm import tqdm
28
+
29
+ try:
30
+ import numpy as np
31
+ except ModuleNotFoundError:
32
+ pass
33
+
34
+ try: # 如果不使用字符串编辑距离相关的功能,那这个包导入失败也没关系
35
+ import Levenshtein
36
+ except ModuleNotFoundError:
37
+ pass
38
+
39
+ try: # 层次聚类相关的功能
40
+ from sklearn.cluster import AgglomerativeClustering
41
+ except ModuleNotFoundError:
42
+ pass
43
+
44
+
45
+ def calculate_coeff_favoring_length(length1, length2, baseline=100, scale=10000):
46
+ """
47
+ 根据两文本的长度计算相似度调整系数,以解决短文本过高相似度评分的问题。
48
+
49
+ 短文本之间相似或完全相同的片段可能导致相似度评分过高,从而误判文本间的相关性比实际更高。
50
+ 通过引入相似度调整系数来平衡评分,降低短文本之间的相似度得分,使评分更加合理和公平。
51
+
52
+ :param length1: 第一文本的长度
53
+ :param length2: 第二文本的长度
54
+ :param baseline: 基线长度,影响系数调整的起始点。
55
+ :param scale: 尺度长度,定义了系数增长到2的长度标准。
56
+ :return: 相似度调整系数。
57
+ """
58
+ total_length = length1 + length2
59
+ length_ratio = min(length1, length2) / max(length1, length2)
60
+
61
+ if total_length < baseline:
62
+ coefficient = 0.5 + 0.5 * (total_length / baseline)
63
+ else:
64
+ coefficient = 1 + (math.log1p(total_length - baseline + 1) / math.log1p(scale - baseline + 1))
65
+
66
+ # 考虑长度差异的影响
67
+ coefficient *= length_ratio
68
+
69
+ return coefficient
70
+
71
+
72
+ def compute_text_similarity_favoring_length(text1, text2, baseline=100, scale=10000):
73
+ """
74
+ 计算两段文本之间的相似度,引入长度调整系数以解决短文本过高相似度评分的问题。
75
+
76
+ :param text1: 第一段文本
77
+ :param text2: 第二段文本
78
+ :param baseline: 基线长度,影响系数调整的起始点。
79
+ :param scale: 尺度长度,定义了系数增长到2的长度标准。
80
+ :return: 加权后的相似度得分,范围在0到1之间。
81
+ """
82
+ base_similarity = Levenshtein.ratio(text1, text2)
83
+ coefficient = calculate_coeff_favoring_length(len(text1), len(text2), baseline, scale)
84
+
85
+ # 计算加权相似度
86
+ weighted_similarity = base_similarity * coefficient
87
+
88
+ # 确保相似度不会超过1
89
+ return min(weighted_similarity, 1.0)
90
+
91
+
92
+ class DataMatcher:
93
+ """ 泛化的匹配类,对任何类型的数据进行匹配 """
94
+
95
+ def __init__(self, *, cmp_key=None):
96
+ """
97
+ :param cmp_key: 当设置该值时,表示data中不是整个用于比较,而是有个索引列
98
+ """
99
+ self.cmp_key = cmp_key
100
+ self.data = [] # 用于匹配的数据
101
+
102
+ def __getitem__(self, i):
103
+ return self.data[i]
104
+
105
+ def __delitem__(self, i):
106
+ del self.data[i]
107
+
108
+ def __len__(self):
109
+ return len(self.data)
110
+
111
+ def compute_similarity(self, x, y):
112
+ """ 计算两个数据之间的相似度,这里默认对字符串使用编辑距离 """
113
+ if self.cmp_key:
114
+ x = x[self.cmp_key]
115
+ ratio = Levenshtein.ratio(x, y)
116
+ return ratio
117
+
118
+ def add_candidate(self, data):
119
+ """添加候选数据"""
120
+ self.data.append(data)
121
+
122
+ def find_best_matches(self, item, top_n=1, print_mode=0):
123
+ """ 找到与给定数据项最匹配的候选项。
124
+
125
+ :param item: 需要匹配的数据项。
126
+ :param top_n: 返回的最佳匹配数量。
127
+ :return: 一个包含(index, similarity)的元组列表,代表最佳匹配。
128
+ """
129
+ # 计算所有候选数据的相似度
130
+ similarities = [(i, self.compute_similarity(candidate, item))
131
+ for i, candidate in tqdm(enumerate(self.data), disable=not print_mode)]
132
+
133
+ # 按相似度降序排序
134
+ sorted_matches = sorted(similarities, key=lambda x: x[1], reverse=True)
135
+
136
+ return sorted_matches[:top_n]
137
+
138
+ def find_best_match_items(self, item, top_n=1):
139
+ """ 直接返回匹配的数据内容,而不是下标和相似度 """
140
+ matches = self.find_best_matches(item, top_n=top_n)
141
+ return [self.data[m[0]] for m in matches]
142
+
143
+ def find_best_match(self, item):
144
+ """ 返回最佳匹配 """
145
+ matches = self.find_best_matches(item, top_n=1)
146
+ return matches[0]
147
+
148
+ def find_best_match_item(self, item):
149
+ """ 直接返回匹配的数据内容,而不是下标和相似度 """
150
+ items = self.find_best_match_items(item)
151
+ return items[0]
152
+
153
+ def agglomerative_clustering(self, threshold=0.5):
154
+ """ 对内部字符串进行层次聚类
155
+
156
+ :param threshold: 可以理解成距离的阈值,距离小于这个阈值的字符串会被聚为一类
157
+ 值越小,分出的类别越多越细
158
+ """
159
+ # 1 给每个样本标类别
160
+ distance_matrix = np.zeros((len(self), len(self)))
161
+ for i in range(len(self)):
162
+ for j in range(i + 1, len(self)):
163
+ # 我们需要距离,所以用1减去相似度
164
+ distance = 1 - self.compute_similarity(self.data[i], self.data[j])
165
+ distance_matrix[i, j] = distance_matrix[j, i] = distance
166
+
167
+ # 进行层次聚类
168
+ clustering = AgglomerativeClustering(n_clusters=None, affinity='precomputed',
169
+ distance_threshold=threshold,
170
+ linkage='complete')
171
+ labels = clustering.fit_predict(distance_matrix)
172
+
173
+ return labels
174
+
175
+ def display_clusters(self, threshold=0.5):
176
+ """ 根据agglomerative_clustering的结果,显示各个聚类的内容 """
177
+ labels = self.agglomerative_clustering(threshold=threshold)
178
+ cluster_dict = defaultdict(list)
179
+
180
+ # 组织数据到字典中
181
+ for idx, label in enumerate(labels):
182
+ cluster_dict[label].append(self.data[idx])
183
+
184
+ # 按标签排序并显示
185
+ result = {}
186
+ for label, items in sorted(cluster_dict.items(), key=lambda x: -len(x[1])):
187
+ result[label] = items
188
+
189
+ return result
190
+
191
+ def get_center_sample(self, indices=None):
192
+ """ 获取一个数据集的中心样本
193
+
194
+ :param indices: 数据项的索引列表。如果为None,则考虑所有数据。
195
+ :return: 中心样本的索引。
196
+ """
197
+ if indices is None:
198
+ indices = range(len(self.data))
199
+
200
+ cached_results = {}
201
+
202
+ def get_similarity(i, j):
203
+ """ 获取两个索引的相似度,利用缓存来避免重复计算 """
204
+ if (i, j) in cached_results:
205
+ return cached_results[(i, j)]
206
+ sim_val = self.compute_similarity(self.data[indices[i]], self.data[indices[j]])
207
+ cached_results[(i, j)] = cached_results[(j, i)] = sim_val
208
+ return sim_val
209
+
210
+ center_idx = max(indices, key=lambda x: sum(get_similarity(x, y) for y in indices))
211
+ return center_idx
212
+
213
+ def find_top_similar_pairs(self, top_n=1):
214
+ """找到最相近的top_n对数据。
215
+
216
+ :param top_n: 需要返回的最相似的数据对的数量。
217
+ :return: 一个列表,包含(top_n个)最相似数据对的索引和它们之间的相似度。
218
+ """
219
+ if len(self.data) < 2:
220
+ return []
221
+
222
+ # 初始化一个列表来保存最相似的数据对,使用最小堆来维护这个列表
223
+ # 最小堆能够保证每次都能快速弹出相似度最小的数据对
224
+ top_pairs = []
225
+
226
+ for i in tqdm(range(len(self.data))):
227
+ for j in range(i + 1, len(self.data)):
228
+ similarity = self.compute_similarity(self.data[i], self.data[j])
229
+
230
+ # 如果当前相似度对数量还未达到top_n,直接添加
231
+ if len(top_pairs) < top_n:
232
+ heapq.heappush(top_pairs, (similarity, (i, j)))
233
+ else:
234
+ # 如果当前对的相似度大于堆中最小的相似度,替换之
235
+ if similarity > top_pairs[0][0]:
236
+ heapq.heapreplace(top_pairs, (similarity, (i, j)))
237
+
238
+ # 将堆转换为排序后的列表返回
239
+ top_pairs.sort(reverse=True, key=lambda x: x[0])
240
+ return [(pair[1], pair[0]) for pair in top_pairs]
241
+
242
+
243
+ class GroupedDataMatcher(DataMatcher):
244
+ """ 对数据量特别大的情况,我们可以先对数据进行分组,然后再对每个分组进行匹配 """
245
+
246
+ def __init__(self):
247
+ """ 初始化一个分组数据匹配器 """
248
+ super().__init__()
249
+ # 父类有个data(list)存储了所有数据,这里self.groups只存储数据的下标
250
+ self.groups = dict()
251
+
252
+ def _sort_groups(self):
253
+ """ 按照组员数量从多到少排序groups """
254
+ new_groups = {}
255
+ for rep, items in sorted(self.groups.items(), key=lambda x: -len(x[1])):
256
+ new_groups[rep] = items
257
+ self.groups = new_groups
258
+
259
+ def merge_group(self, indices, threshold=0.5, strategy='center'):
260
+ """ 对输入的索引进行合并,根据阈值生成分组
261
+
262
+ :param indices: 数据项的索引列表。
263
+ :param threshold: 两个数据项的距离小于此阈值时,它们被认为是相似的。
264
+ :param strategy: 选择组代表的策略,可以是'center'或'first'。
265
+ :return: 一个字典,键是代表性数据项的索引,值是相似数据项的索引列表。
266
+ """
267
+ # 1 给每个样本标类别
268
+ n = len(indices)
269
+ if n == 1:
270
+ return {indices[0]: indices}
271
+
272
+ distance_matrix = np.zeros((n, n))
273
+ for i in range(n):
274
+ for j in range(i + 1, n):
275
+ distance = 1 - self.compute_similarity(self.data[indices[i]], self.data[indices[j]])
276
+ distance_matrix[i, j] = distance_matrix[j, i] = distance
277
+
278
+ clustering = AgglomerativeClustering(n_clusters=None, affinity='precomputed',
279
+ distance_threshold=threshold,
280
+ linkage='average')
281
+ labels = clustering.fit_predict(distance_matrix)
282
+
283
+ # 2 分组字典
284
+ cluster_dict = defaultdict(list)
285
+ for i, label in enumerate(labels):
286
+ cluster_dict[label].append(indices[i])
287
+
288
+ # 3 改成代表样本映射到一组里,并且按照样本数从多到少排序
289
+ result = {}
290
+ for label, items in sorted(cluster_dict.items(), key=lambda x: -len(x[1])):
291
+ if strategy == 'first':
292
+ representative = items[0]
293
+ elif strategy == 'center':
294
+ local_indices = [i for i, idx in enumerate(indices) if idx in items]
295
+ sub_matrix = distance_matrix[np.ix_(local_indices, local_indices)]
296
+ avg_distances = sub_matrix.mean(axis=1)
297
+ representative_idx = np.argmin(avg_distances)
298
+ representative = items[representative_idx]
299
+ else:
300
+ raise ValueError(f'Invalid strategy: {strategy}')
301
+ result[representative] = items
302
+
303
+ return result
304
+
305
+ def init_groups(self, threshold=0.5, batch_size=1000, print_mode=0):
306
+ """ 初始化数据的分组
307
+
308
+ :param threshold: 两个数据项的距离小于此阈值时,它们被认为是相似的。
309
+ 这里写成1的话,一般就是故意特地把类别只分成一类
310
+ :param batch_size: 由于数据可能很大,可以使用批量处理来减少计算量。
311
+ :return: 一个字典,键是代表性数据项的索引,值是相似数据项的索引列表。
312
+ """
313
+ # 1 最开始每个样本都是一个组
314
+ groups = {i: [i] for i in range(len(self.data))}
315
+ new_groups = {}
316
+
317
+ # 2 不断合并,直到没有组数变化
318
+ while len(groups) > 1:
319
+ for indices in chunked(groups.keys(), batch_size):
320
+ # 对于这里返回的字典,原groups里的values也要对应拼接的
321
+ indices2 = self.merge_group(indices, threshold=threshold)
322
+ for idx, idxs in indices2.items():
323
+ # 获取原始分组中的索引
324
+ original_idxs = [groups[original_idx] for original_idx in idxs]
325
+ # 展平列表并分配到新分组中
326
+ new_groups[idx] = [item for sublist in original_idxs for item in sublist]
327
+
328
+ # 如果分组没有发生变化,退出循环
329
+ if len(new_groups) == len(groups):
330
+ break
331
+
332
+ if print_mode:
333
+ print(f'Groups number: {len(new_groups)}')
334
+
335
+ groups = new_groups
336
+ new_groups = {}
337
+
338
+ self.groups = groups
339
+ self._sort_groups()
340
+ return self.groups
341
+
342
+ def split_large_groups(self, max_group_size, threshold=0.5):
343
+ """ 对于样本数过多的类,进行进一步的拆分
344
+
345
+ :param max_group_size: 一个组内的最大样本数,超过这个数就会被进一步拆分。
346
+ :param threshold: 用于拆分的阈值,两个数据项的距离小于此阈值时,它们被认为是相似的。
347
+ :return: 返回拆分后的分组。
348
+ """
349
+
350
+ refined_groups = {}
351
+ for rep, items in self.groups.items():
352
+ if len(items) > max_group_size:
353
+ # 该组样本数超过阈值,需要进一步拆分
354
+ sub_groups = self.merge_group(items, threshold)
355
+ refined_groups.update(sub_groups)
356
+ else:
357
+ # 该组样本数在阈值范围内,保持不变
358
+ refined_groups[rep] = items
359
+
360
+ self.groups = refined_groups
361
+ self._sort_groups()
362
+ return refined_groups
363
+
364
+ def merge_small_groups(self, min_group_size=10):
365
+ """ 将样本数较小的组合并成一个大组
366
+
367
+ :param min_group_size: 一个组的最小样本数,低于这个数的组将被合并。
368
+ :return: 返回合并后的分组。
369
+ """
370
+
371
+ merged_group = []
372
+ preserved_groups = {}
373
+
374
+ for rep, items in self.groups.items():
375
+ if len(items) < min_group_size:
376
+ # 该组样本数低于阈值,将其添加到待合并的大组中
377
+ merged_group.extend(items)
378
+ else:
379
+ # 该组样本数大于等于阈值,保留原状
380
+ preserved_groups[rep] = items
381
+
382
+ if merged_group:
383
+ rep_item = self.merge_group(merged_group, 1)
384
+ for rep, items in rep_item.items():
385
+ preserved_groups[rep] = items
386
+
387
+ self.groups = preserved_groups
388
+ self._sort_groups()
389
+ return preserved_groups