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.
- pyxllib/__init__.py +9 -2
- pyxllib/algo/__init__.py +8 -0
- pyxllib/algo/disjoint.py +54 -0
- pyxllib/algo/geo.py +541 -0
- pyxllib/{util/mathlib.py → algo/intervals.py} +172 -36
- pyxllib/algo/matcher.py +389 -0
- pyxllib/algo/newbie.py +166 -0
- pyxllib/algo/pupil.py +629 -0
- pyxllib/algo/shapelylib.py +67 -0
- pyxllib/algo/specialist.py +241 -0
- pyxllib/algo/stat.py +494 -0
- pyxllib/algo/treelib.py +149 -0
- pyxllib/algo/unitlib.py +66 -0
- pyxllib/autogui/__init__.py +5 -0
- pyxllib/autogui/activewin.py +246 -0
- pyxllib/autogui/all.py +9 -0
- pyxllib/autogui/autogui.py +852 -0
- pyxllib/autogui/uiautolib.py +362 -0
- pyxllib/autogui/virtualkey.py +102 -0
- pyxllib/autogui/wechat.py +827 -0
- pyxllib/autogui/wechat_msg.py +421 -0
- pyxllib/autogui/wxautolib.py +84 -0
- pyxllib/cv/__init__.py +1 -11
- pyxllib/cv/expert.py +267 -0
- pyxllib/cv/{imlib.py → imfile.py} +18 -83
- pyxllib/cv/imhash.py +39 -0
- pyxllib/cv/pupil.py +9 -0
- pyxllib/cv/rgbfmt.py +1525 -0
- pyxllib/cv/slidercaptcha.py +137 -0
- pyxllib/cv/trackbartools.py +163 -49
- pyxllib/cv/xlcvlib.py +1040 -0
- pyxllib/cv/xlpillib.py +423 -0
- pyxllib/data/__init__.py +0 -0
- pyxllib/data/echarts.py +240 -0
- pyxllib/data/jsonlib.py +89 -0
- pyxllib/{util/oss2_.py → data/oss.py} +11 -9
- pyxllib/data/pglib.py +1127 -0
- pyxllib/data/sqlite.py +568 -0
- pyxllib/{util → data}/sqllib.py +13 -31
- pyxllib/ext/JLineViewer.py +505 -0
- pyxllib/ext/__init__.py +6 -0
- pyxllib/{util → ext}/demolib.py +119 -35
- pyxllib/ext/drissionlib.py +277 -0
- pyxllib/ext/kq5034lib.py +12 -0
- pyxllib/{util/main.py → ext/old.py} +122 -284
- pyxllib/ext/qt.py +449 -0
- pyxllib/ext/robustprocfile.py +497 -0
- pyxllib/ext/seleniumlib.py +76 -0
- pyxllib/{util/tklib.py → ext/tk.py} +10 -11
- pyxllib/ext/unixlib.py +827 -0
- pyxllib/ext/utools.py +351 -0
- pyxllib/{util/webhooklib.py → ext/webhook.py} +45 -17
- pyxllib/ext/win32lib.py +40 -0
- pyxllib/ext/wjxlib.py +88 -0
- pyxllib/ext/wpsapi.py +124 -0
- pyxllib/ext/xlwork.py +9 -0
- pyxllib/ext/yuquelib.py +1105 -0
- pyxllib/file/__init__.py +17 -0
- pyxllib/file/docxlib.py +761 -0
- pyxllib/{util → file}/gitlib.py +40 -27
- pyxllib/file/libreoffice.py +165 -0
- pyxllib/file/movielib.py +148 -0
- pyxllib/file/newbie.py +10 -0
- pyxllib/file/onenotelib.py +1469 -0
- pyxllib/file/packlib/__init__.py +330 -0
- pyxllib/{util → file/packlib}/zipfile.py +598 -195
- pyxllib/file/pdflib.py +426 -0
- pyxllib/file/pupil.py +185 -0
- pyxllib/file/specialist/__init__.py +685 -0
- pyxllib/{basic/_5_dirlib.py → file/specialist/dirlib.py} +364 -93
- pyxllib/file/specialist/download.py +193 -0
- pyxllib/file/specialist/filelib.py +2829 -0
- pyxllib/file/xlsxlib.py +3131 -0
- pyxllib/file/xlsyncfile.py +341 -0
- pyxllib/prog/__init__.py +5 -0
- pyxllib/prog/cachetools.py +64 -0
- pyxllib/prog/deprecatedlib.py +233 -0
- pyxllib/prog/filelock.py +42 -0
- pyxllib/prog/ipyexec.py +253 -0
- pyxllib/prog/multiprogs.py +940 -0
- pyxllib/prog/newbie.py +451 -0
- pyxllib/prog/pupil.py +1197 -0
- pyxllib/{sitepackages.py → prog/sitepackages.py} +5 -3
- pyxllib/prog/specialist/__init__.py +391 -0
- pyxllib/prog/specialist/bc.py +203 -0
- pyxllib/prog/specialist/browser.py +497 -0
- pyxllib/prog/specialist/common.py +347 -0
- pyxllib/prog/specialist/datetime.py +199 -0
- pyxllib/prog/specialist/tictoc.py +240 -0
- pyxllib/prog/specialist/xllog.py +180 -0
- pyxllib/prog/xlosenv.py +108 -0
- pyxllib/stdlib/__init__.py +17 -0
- pyxllib/{util → stdlib}/tablepyxl/__init__.py +1 -3
- pyxllib/{util → stdlib}/tablepyxl/style.py +1 -1
- pyxllib/{util → stdlib}/tablepyxl/tablepyxl.py +2 -4
- pyxllib/text/__init__.py +8 -0
- pyxllib/text/ahocorasick.py +39 -0
- pyxllib/text/airscript.js +744 -0
- pyxllib/text/charclasslib.py +121 -0
- pyxllib/text/jiebalib.py +267 -0
- pyxllib/text/jinjalib.py +32 -0
- pyxllib/text/jsa_ai_prompt.md +271 -0
- pyxllib/text/jscode.py +922 -0
- pyxllib/text/latex/__init__.py +158 -0
- pyxllib/text/levenshtein.py +303 -0
- pyxllib/text/nestenv.py +1215 -0
- pyxllib/text/newbie.py +300 -0
- pyxllib/text/pupil/__init__.py +8 -0
- pyxllib/text/pupil/common.py +1121 -0
- pyxllib/text/pupil/xlalign.py +326 -0
- pyxllib/text/pycode.py +47 -0
- pyxllib/text/specialist/__init__.py +8 -0
- pyxllib/text/specialist/common.py +112 -0
- pyxllib/text/specialist/ptag.py +186 -0
- pyxllib/text/spellchecker.py +172 -0
- pyxllib/text/templates/echart_base.html +11 -0
- pyxllib/text/templates/highlight_code.html +17 -0
- pyxllib/text/templates/latex_editor.html +103 -0
- pyxllib/text/vbacode.py +17 -0
- pyxllib/text/xmllib.py +747 -0
- pyxllib/xl.py +39 -0
- pyxllib/xlcv.py +17 -0
- pyxllib-0.3.197.dist-info/METADATA +48 -0
- pyxllib-0.3.197.dist-info/RECORD +126 -0
- {pyxllib-0.0.43.dist-info → pyxllib-0.3.197.dist-info}/WHEEL +4 -5
- pyxllib/basic/_1_strlib.py +0 -945
- pyxllib/basic/_2_timelib.py +0 -488
- pyxllib/basic/_3_pathlib.py +0 -916
- pyxllib/basic/_4_loglib.py +0 -419
- pyxllib/basic/__init__.py +0 -54
- pyxllib/basic/arrow_.py +0 -250
- pyxllib/basic/chardet_.py +0 -66
- pyxllib/basic/dirlib.py +0 -529
- pyxllib/basic/dprint.py +0 -202
- pyxllib/basic/extension.py +0 -12
- pyxllib/basic/judge.py +0 -31
- pyxllib/basic/log.py +0 -204
- pyxllib/basic/pathlib_.py +0 -705
- pyxllib/basic/pytictoc.py +0 -102
- pyxllib/basic/qiniu_.py +0 -61
- pyxllib/basic/strlib.py +0 -761
- pyxllib/basic/timer.py +0 -132
- pyxllib/cv/cv.py +0 -834
- pyxllib/cv/cvlib/_1_geo.py +0 -543
- pyxllib/cv/cvlib/_2_cvprcs.py +0 -309
- pyxllib/cv/cvlib/_2_imgproc.py +0 -594
- pyxllib/cv/cvlib/_3_pilprcs.py +0 -80
- pyxllib/cv/cvlib/_4_cvimg.py +0 -211
- pyxllib/cv/cvlib/__init__.py +0 -10
- pyxllib/cv/debugtools.py +0 -82
- pyxllib/cv/fitz_.py +0 -300
- pyxllib/cv/installer.py +0 -42
- pyxllib/debug/_0_installer.py +0 -38
- pyxllib/debug/_1_typelib.py +0 -277
- pyxllib/debug/_2_chrome.py +0 -198
- pyxllib/debug/_3_showdir.py +0 -161
- pyxllib/debug/_4_bcompare.py +0 -140
- pyxllib/debug/__init__.py +0 -49
- pyxllib/debug/bcompare.py +0 -132
- pyxllib/debug/chrome.py +0 -198
- pyxllib/debug/installer.py +0 -38
- pyxllib/debug/showdir.py +0 -158
- pyxllib/debug/typelib.py +0 -278
- pyxllib/image/__init__.py +0 -12
- pyxllib/torch/__init__.py +0 -20
- pyxllib/torch/modellib.py +0 -37
- pyxllib/torch/trainlib.py +0 -344
- pyxllib/util/__init__.py +0 -20
- pyxllib/util/aip_.py +0 -141
- pyxllib/util/casiadb.py +0 -59
- pyxllib/util/excellib.py +0 -495
- pyxllib/util/filelib.py +0 -612
- pyxllib/util/jsondata.py +0 -27
- pyxllib/util/jsondata2.py +0 -92
- pyxllib/util/labelmelib.py +0 -139
- pyxllib/util/onepy/__init__.py +0 -29
- pyxllib/util/onepy/onepy.py +0 -574
- pyxllib/util/onepy/onmanager.py +0 -170
- pyxllib/util/pyautogui_.py +0 -219
- pyxllib/util/textlib.py +0 -1305
- pyxllib/util/unorder.py +0 -22
- pyxllib/util/xmllib.py +0 -639
- pyxllib-0.0.43.dist-info/METADATA +0 -39
- pyxllib-0.0.43.dist-info/RECORD +0 -80
- pyxllib-0.0.43.dist-info/top_level.txt +0 -1
- {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
|
-
# @
|
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
|
-
|
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 =
|
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()
|
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,
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
pyxllib/algo/matcher.py
ADDED
@@ -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
|