pyxllib 0.3.197__py3-none-any.whl → 3.201.1__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 (127) hide show
  1. pyxllib/__init__.py +14 -21
  2. pyxllib/algo/__init__.py +8 -8
  3. pyxllib/algo/disjoint.py +54 -54
  4. pyxllib/algo/geo.py +537 -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 +145 -149
  13. pyxllib/algo/unitlib.py +62 -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 +846 -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 +236 -240
  34. pyxllib/data/jsonlib.py +85 -89
  35. pyxllib/data/oss.py +72 -72
  36. pyxllib/data/pglib.py +1111 -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 +251 -246
  42. pyxllib/ext/drissionlib.py +277 -277
  43. pyxllib/ext/kq5034lib.py +12 -12
  44. pyxllib/ext/qt.py +449 -449
  45. pyxllib/ext/robustprocfile.py +493 -497
  46. pyxllib/ext/seleniumlib.py +76 -76
  47. pyxllib/ext/tk.py +173 -173
  48. pyxllib/ext/unixlib.py +821 -827
  49. pyxllib/ext/utools.py +345 -351
  50. pyxllib/ext/webhook.py +124 -119
  51. pyxllib/ext/win32lib.py +40 -40
  52. pyxllib/ext/wjxlib.py +91 -88
  53. pyxllib/ext/wpsapi.py +124 -124
  54. pyxllib/ext/xlwork.py +9 -9
  55. pyxllib/ext/yuquelib.py +1110 -1105
  56. pyxllib/file/__init__.py +17 -17
  57. pyxllib/file/docxlib.py +757 -761
  58. pyxllib/file/gitlib.py +309 -309
  59. pyxllib/file/libreoffice.py +165 -165
  60. pyxllib/file/movielib.py +144 -148
  61. pyxllib/file/newbie.py +10 -10
  62. pyxllib/file/onenotelib.py +1469 -1469
  63. pyxllib/file/packlib/__init__.py +330 -330
  64. pyxllib/file/packlib/zipfile.py +2441 -2441
  65. pyxllib/file/pdflib.py +422 -426
  66. pyxllib/file/pupil.py +185 -185
  67. pyxllib/file/specialist/__init__.py +681 -685
  68. pyxllib/file/specialist/dirlib.py +799 -799
  69. pyxllib/file/specialist/download.py +193 -193
  70. pyxllib/file/specialist/filelib.py +2825 -2829
  71. pyxllib/file/xlsxlib.py +3122 -3131
  72. pyxllib/file/xlsyncfile.py +341 -341
  73. pyxllib/prog/__init__.py +5 -5
  74. pyxllib/prog/cachetools.py +58 -64
  75. pyxllib/prog/deprecatedlib.py +233 -233
  76. pyxllib/prog/filelock.py +42 -42
  77. pyxllib/prog/ipyexec.py +253 -253
  78. pyxllib/prog/multiprogs.py +940 -940
  79. pyxllib/prog/newbie.py +451 -451
  80. pyxllib/prog/pupil.py +1208 -1197
  81. pyxllib/prog/sitepackages.py +33 -33
  82. pyxllib/prog/specialist/__init__.py +348 -391
  83. pyxllib/prog/specialist/bc.py +203 -203
  84. pyxllib/prog/specialist/browser.py +497 -497
  85. pyxllib/prog/specialist/common.py +347 -347
  86. pyxllib/prog/specialist/datetime.py +198 -198
  87. pyxllib/prog/specialist/tictoc.py +240 -240
  88. pyxllib/prog/specialist/xllog.py +180 -180
  89. pyxllib/prog/xlosenv.py +110 -108
  90. pyxllib/stdlib/__init__.py +17 -17
  91. pyxllib/stdlib/tablepyxl/__init__.py +10 -10
  92. pyxllib/stdlib/tablepyxl/style.py +303 -303
  93. pyxllib/stdlib/tablepyxl/tablepyxl.py +130 -130
  94. pyxllib/text/__init__.py +8 -8
  95. pyxllib/text/ahocorasick.py +36 -39
  96. pyxllib/text/airscript.js +754 -744
  97. pyxllib/text/charclasslib.py +121 -121
  98. pyxllib/text/jiebalib.py +267 -267
  99. pyxllib/text/jinjalib.py +27 -32
  100. pyxllib/text/jsa_ai_prompt.md +271 -271
  101. pyxllib/text/jscode.py +922 -922
  102. pyxllib/text/latex/__init__.py +158 -158
  103. pyxllib/text/levenshtein.py +303 -303
  104. pyxllib/text/nestenv.py +1215 -1215
  105. pyxllib/text/newbie.py +300 -300
  106. pyxllib/text/pupil/__init__.py +8 -8
  107. pyxllib/text/pupil/common.py +1121 -1121
  108. pyxllib/text/pupil/xlalign.py +326 -326
  109. pyxllib/text/pycode.py +47 -47
  110. pyxllib/text/specialist/__init__.py +8 -8
  111. pyxllib/text/specialist/common.py +112 -112
  112. pyxllib/text/specialist/ptag.py +186 -186
  113. pyxllib/text/spellchecker.py +172 -172
  114. pyxllib/text/templates/echart_base.html +10 -10
  115. pyxllib/text/templates/highlight_code.html +16 -16
  116. pyxllib/text/templates/latex_editor.html +102 -102
  117. pyxllib/text/vbacode.py +17 -17
  118. pyxllib/text/xmllib.py +741 -747
  119. pyxllib/xl.py +42 -39
  120. pyxllib/xlcv.py +17 -17
  121. pyxllib-3.201.1.dist-info/METADATA +296 -0
  122. pyxllib-3.201.1.dist-info/RECORD +125 -0
  123. {pyxllib-0.3.197.dist-info → pyxllib-3.201.1.dist-info}/licenses/LICENSE +190 -190
  124. pyxllib/ext/old.py +0 -663
  125. pyxllib-0.3.197.dist-info/METADATA +0 -48
  126. pyxllib-0.3.197.dist-info/RECORD +0 -126
  127. {pyxllib-0.3.197.dist-info → pyxllib-3.201.1.dist-info}/WHEEL +0 -0
pyxllib/algo/matcher.py CHANGED
@@ -1,389 +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
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