curlkun commited on
Commit
ae8929a
·
verified ·
1 Parent(s): b39b692

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +133 -30
app.py CHANGED
@@ -2,14 +2,21 @@ import difflib
2
  import re
3
 
4
  HYPHENS = set(['\u002D','\uFF0D','\u2010','\u2011','\u2013','\u2014'])
 
5
  BRACKET_PAIRS = [('(',')'),('(',')'),( '〔','〕'),('《','》'),('〈','〉'),('【','】'),('[',']'),('「','」'),('『','』')]
6
  OPEN_BRACKETS = set(p[0] for p in BRACKET_PAIRS)
7
  CLOSE_BRACKETS = set(p[1] for p in BRACKET_PAIRS)
8
  BRACKET_MAP = {p[0]: p[1] for p in BRACKET_PAIRS}
9
 
10
  def build_active_flags(original, settings):
 
 
 
 
 
11
  n = len(original)
12
  flags = [True] * n
 
13
  if settings.get('ignore_brackets_with_content'):
14
  i = 0
15
  while i < n:
@@ -30,51 +37,147 @@ def build_active_flags(original, settings):
30
  i += 1
31
  else:
32
  i += 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  if settings.get('ignore_hyphen'):
34
  for i, ch in enumerate(original):
35
  if ch in HYPHENS: flags[i] = False
 
36
  if settings.get('ignore_space'):
37
  for i, ch in enumerate(original):
38
  if ch in ' \t ': flags[i] = False
 
39
  if settings.get('ignore_newline'):
40
  for i, ch in enumerate(original):
41
  if ch in '\r\n': flags[i] = False
 
42
  return flags
43
 
44
- def tokenize_with_flags(original, flags):
45
- pattern = re.compile(
46
- r'[a-zA-ZabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ]+|'
47
- r'[0-90-9]+|'
48
- r'[ぁ-んァ-ンヲ-゚\u30A0]+|'
49
- r'[\u4E00-\u9FFF\u3400-\u4DBF]+|'
50
- r'[ \t ]+|'
51
- r'\r\n|\r|\n|'
52
- r'.'
53
- )
54
- active_chars = [(ch, i) for i, ch in enumerate(original) if flags[i]]
55
- active_text = ''.join(ch for ch, _ in active_chars)
56
- tokens = []
57
- for m in pattern.finditer(active_text):
58
- s = m.start()
59
- e = m.end()
60
- orig_start = active_chars[s][1]
61
- orig_end = active_chars[e-1][1] + 1
62
- tokens.append((m.group(), orig_start, orig_end))
63
- return tokens
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
  test_cases = [
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  ("TEST-30", "abc def", "abcdef", {}),
67
  ("TEST-35", "abcXYZdef", "abcdef", {}),
 
 
68
  ]
69
 
70
  for name, src, dst, settings in test_cases:
71
- src_flags = build_active_flags(src, settings)
72
- dst_flags = build_active_flags(dst, settings)
73
- src_tokens = tokenize_with_flags(src, src_flags)
74
- dst_tokens = tokenize_with_flags(dst, dst_flags)
75
- print(f"\n=== {name} tokens ===")
76
- print(f" src_tokens: {src_tokens}")
77
- print(f" dst_tokens: {dst_tokens}")
78
- matcher = difflib.SequenceMatcher(None, [t[0] for t in src_tokens], [t[0] for t in dst_tokens], autojunk=False)
79
- for tag, i1, i2, j1, j2 in matcher.get_opcodes():
80
- print(f" {tag:10} src={repr(''.join(t[0] for t in src_tokens[i1:i2]))} dst={repr(''.join(t[0] for t in dst_tokens[j1:j2]))}")
 
2
  import re
3
 
4
  HYPHENS = set(['\u002D','\uFF0D','\u2010','\u2011','\u2013','\u2014'])
5
+ YAKUMONO = set('。、.,〔〕《》〈〉【】[]()()「」『』「」〝〟!?・…―!?〜~;:;://\\\‥++**##&&%%@@$$¥¥^^||``′″§※〒♪♫♬♩→←↑↓↗↘↙↖◯○●◎□■△▲▽▼◇◆☆★©®™')
6
  BRACKET_PAIRS = [('(',')'),('(',')'),( '〔','〕'),('《','》'),('〈','〉'),('【','】'),('[',']'),('「','」'),('『','』')]
7
  OPEN_BRACKETS = set(p[0] for p in BRACKET_PAIRS)
8
  CLOSE_BRACKETS = set(p[1] for p in BRACKET_PAIRS)
9
  BRACKET_MAP = {p[0]: p[1] for p in BRACKET_PAIRS}
10
 
11
  def build_active_flags(original, settings):
12
+ """
13
+ 各文字が比較対象か否かを示すフラグリストを返す。
14
+ True=比較対象, False=除去対象
15
+ 除去対象文字は比較からは外れるが、元テキスト上に残りハイライト対象になりうる。
16
+ """
17
  n = len(original)
18
  flags = [True] * n
19
+
20
  if settings.get('ignore_brackets_with_content'):
21
  i = 0
22
  while i < n:
 
37
  i += 1
38
  else:
39
  i += 1
40
+
41
+ if settings.get('ignore_yakumono'):
42
+ for i, ch in enumerate(original):
43
+ if ch in YAKUMONO: flags[i] = False
44
+
45
+ if settings.get('ignore_alpha'):
46
+ for i, ch in enumerate(original):
47
+ if re.match(r'[A-Za-zA-Za-z]', ch): flags[i] = False
48
+
49
+ if settings.get('ignore_digit'):
50
+ for i, ch in enumerate(original):
51
+ if re.match(r'[0-90-9]', ch): flags[i] = False
52
+
53
+ if settings.get('ignore_kana'):
54
+ for i, ch in enumerate(original):
55
+ if re.match(r'[ァ-ンヲ-゚]', ch): flags[i] = False
56
+
57
  if settings.get('ignore_hyphen'):
58
  for i, ch in enumerate(original):
59
  if ch in HYPHENS: flags[i] = False
60
+
61
  if settings.get('ignore_space'):
62
  for i, ch in enumerate(original):
63
  if ch in ' \t ': flags[i] = False
64
+
65
  if settings.get('ignore_newline'):
66
  for i, ch in enumerate(original):
67
  if ch in '\r\n': flags[i] = False
68
+
69
  return flags
70
 
71
+ def build_active_text_and_map(original, flags):
72
+ """
73
+ 有効文字のみ抽出したテキストと、
74
+ 有効テキスト内インデックス→元テキストインデックスのマップを返す
75
+ """
76
+ active_chars = [(i, ch) for i, ch in enumerate(original) if flags[i]]
77
+ active_text = ''.join(ch for _, ch in active_chars)
78
+ index_map = [i for i, _ in active_chars] # active_text[k] -> original[index_map[k]]
79
+ return active_text, index_map
80
+
81
+ def compare(src, dst, settings):
82
+ src_flags = build_active_flags(src, settings)
83
+ dst_flags = build_active_flags(dst, settings)
84
+
85
+ src_active, src_map = build_active_text_and_map(src, src_flags)
86
+ dst_active, dst_map = build_active_text_and_map(dst, dst_flags)
87
+
88
+ matcher = difflib.SequenceMatcher(None, src_active, dst_active, autojunk=False)
89
+ opcodes = matcher.get_opcodes()
90
+
91
+ # 有効文字の差分範囲を元テキスト位置に変換
92
+ src_diff_ranges = [] # 比較元の削除箇所
93
+ dst_diff_ranges = [] # 比較先の追加箇所
94
+
95
+ for tag, i1, i2, j1, j2 in opcodes:
96
+ if tag in ('replace', 'delete') and i2 > i1:
97
+ orig_start = src_map[i1]
98
+ orig_end = src_map[i2-1] + 1
99
+ src_diff_ranges.append((orig_start, orig_end))
100
+ if tag in ('replace', 'insert') and j2 > j1:
101
+ orig_start = dst_map[j1]
102
+ orig_end = dst_map[j2-1] + 1
103
+ dst_diff_ranges.append((orig_start, orig_end))
104
+
105
+ # 除去文字のハイライト:
106
+ # 除去文字(flags=False)が有効文字の差分範囲に隣接している場合、
107
+ # その除去文字もハイライト範囲に取り込む
108
+ def expand_with_removed(text, flags, ranges):
109
+ """
110
+ 差分範囲に隣接する除去文字を取り込んで範囲を拡張する。
111
+ 「隣接」とは差分範囲の直前・直後に除去文字が連続している場合。
112
+ """
113
+ expanded = []
114
+ for s, e in ranges:
115
+ # 前方向: sの直前にある除去文字を取り込む
116
+ new_s = s
117
+ while new_s > 0 and not flags[new_s - 1]:
118
+ new_s -= 1
119
+ # 後方向: eの直後にある除去文字を取り込む
120
+ new_e = e
121
+ while new_e < len(text) and not flags[new_e]:
122
+ new_e += 1
123
+ expanded.append((new_s, new_e))
124
+ # マージ
125
+ merged = []
126
+ for s, e in sorted(expanded):
127
+ if s >= e: continue
128
+ if merged and s <= merged[-1][1]:
129
+ merged[-1] = (merged[-1][0], max(merged[-1][1], e))
130
+ else:
131
+ merged.append([s, e])
132
+ return merged
133
+
134
+ src_ranges = expand_with_removed(src, src_flags, src_diff_ranges)
135
+ dst_ranges = expand_with_removed(dst, dst_flags, dst_diff_ranges)
136
+
137
+ return src_ranges, dst_ranges
138
+
139
+ def apply_highlight(text, ranges):
140
+ result = []
141
+ prev = 0
142
+ for s, e in sorted(ranges):
143
+ result.append(text[prev:s])
144
+ result.append('"' + text[s:e] + '"')
145
+ prev = e
146
+ result.append(text[prev:])
147
+ return ''.join(result)
148
 
149
  test_cases = [
150
+ ("TEST-01", "吾輩は猫である。名前はまだない。", "吾輩は猫である。名前はまだない。", {}),
151
+ ("TEST-02", "吾輩は猫である。", "吾輩は犬である。", {}),
152
+ ("TEST-03", "abcdef", "abcXYZdef", {}),
153
+ ("TEST-04", "abcXYZdef", "abcdef", {}),
154
+ ("TEST-08", "吾輩は、猫である。「名前」は(まだ)ない!", "吾輩は猫である名前はまだない",
155
+ {'ignore_yakumono': True}),
156
+ ("TEST-09", "(abc)テスト", "abcテスト",
157
+ {'ignore_yakumono': True}),
158
+ ("TEST-19", "abc-def-ghi‐jkl‑mno–pqr—stu", "abcdefghijklmnopqrstu",
159
+ {'ignore_hyphen': True}),
160
+ ("TEST-21", "abc def ghi", "abcdefghi",
161
+ {'ignore_space': True}),
162
+ ("TEST-22", "abc\ndef\nghi", "abcdefghi",
163
+ {'ignore_newline': True}),
164
+ ("TEST-23", "吾輩は(猫)である。名前は【まだ】ない!", "吾輩はである。名前はない!",
165
+ {'ignore_yakumono': True, 'ignore_brackets_with_content': True}),
166
+ ("TEST-25",
167
+ "abc(def)123アイウ〔ghi〕fi⼀zero-test \nabc\n(def)123アイウ〔ghi〕fi⼀zero-test \nabc",
168
+ "xyz(www)789カキクケコ〔jkl〕fl⼆one+test \nxyz\n(www)789カキクケコ〔jkl〕fl⼆one+test \nxyz",
169
+ {'ignore_alpha': True, 'ignore_digit': True, 'ignore_kana': True, 'ignore_yakumono': True,
170
+ 'ignore_ligature': True, 'ignore_kangxi': True, 'ignore_brackets_with_content': True,
171
+ 'ignore_kanjinum': True, 'ignore_hyphen': True, 'ignore_kumimoji': True,
172
+ 'ignore_space': True, 'ignore_newline': True}),
173
  ("TEST-30", "abc def", "abcdef", {}),
174
  ("TEST-35", "abcXYZdef", "abcdef", {}),
175
+ ("TEST-37", "abc-def-ghi‐jkl‑mno–pqr—stu", "abcdefghijklmnopqrstu",
176
+ {'ignore_hyphen': True}),
177
  ]
178
 
179
  for name, src, dst, settings in test_cases:
180
+ src_ranges, dst_ranges = compare(src, dst, settings)
181
+ print(f"\n=== {name} ===")
182
+ print(f" 比較元: {apply_highlight(src, src_ranges)}")
183
+ print(f" 比較先: {apply_highlight(dst, dst_ranges)}")