Update app.py
Browse files
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
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 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)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|