""" 文章处理工具 · 去AI排版 + 直角引号版 Core logic ported from HTML/JS → Python GUI: CustomTkinter """ import re import random import sys import customtkinter as ctk # ────────────────────────────────────────────────────────────── # 示例文本 # ────────────────────────────────────────────────────────────── EXAMPLE_TEXT = ( '他说:"时间会证明一切"。*这是一个星号标记*,还有下划线_test_。' '实际上,这段话里包含引号、星号、下划线等复杂符号。' '我们的工具应该能自动删除这些符号,只保留常规标点,比如逗号,句号。' '感叹号!问号?冒号:以及书名号《西游记》。' '长句子超过十五个字符的时候,我们会在逗号处强制拆分成短句。' '另外,段落会随机分成2-4句一段,并且相邻段落的句子数量不会相同。' '每个段落内部,除了最后一个句子末尾是句号,其他句子末尾都会变成逗号。' '这是一个特别长的句子,它里面有很多内容,比如我们故意写很多字,' '让它超过十五个字符,并且中间有逗号,那么它就会被拆开。' ) # ────────────────────────────────────────────────────────────── # 核心处理逻辑(移植自 JS) # ────────────────────────────────────────────────────────────── def convert_quotes_to_corner(text: str) -> str: """双引号 → 直角引号「」(DeepSeek版专用)""" # 全角左右双引号 text = text.replace('\u201c', '「').replace('\u201d', '」') # 全角双引号变体 "(U+FF02) → 半角预备配对 text = text.replace('\uff02', '"') # 半角双引号顺序配对 out, left_flag = [], True for ch in text: if ch == '"': out.append('「' if left_flag else '」') left_flag = not left_flag else: out.append(ch) return ''.join(out) def clean_symbols(text: str, keep_corner: bool = False) -> str: """过滤复杂符号,只保留汉字 / 数字 / 字母 / 常规标点""" if keep_corner: pattern = r'[^\u4e00-\u9fff0-9a-zA-Z\s,。!?:《》,.!?:「」]' else: pattern = r'[^\u4e00-\u9fff0-9a-zA-Z\s,。!?:《》,.!?:]' return re.sub(pattern, '', text) def split_long_sentence(sentence: str) -> list[str]: """长句强制拆分(递归):长度 > 15 且含全角逗号 → 在第一个逗号处断开""" s = sentence.strip() if len(s) <= 15: return [s] comma_pos = -1 for i, ch in enumerate(s): if ch == ',' and 2 < i < len(s) - 2: comma_pos = i break if comma_pos == -1: return [s] chars = list(s) chars[comma_pos] = '。' new_text = ''.join(chars) parts = new_text.split('。') if len(parts) < 2: return [s] first = parts[0] + '。' rest = '。'.join(parts[1:]) second = rest if new_text.endswith('。') else rest + '。' return split_long_sentence(first) + split_long_sentence(second) def apply_split(text: str) -> str: """对整段文本执行长句拆分""" raws = re.findall(r'[^。!?!?]+[。!?!?]', text) if not raws: return text result = [] for sent in raws: result.extend(split_long_sentence(sent)) return ''.join(result) def get_sentences(text: str) -> list[str]: """提取句子列表""" matches = re.findall(r'[^。!?!?]+[。!?!?]', text) return [s.strip() for s in matches if s.strip()] def group_paragraphs(sentences: list[str]) -> list[str]: """段落分组:每段 2~4 句,相邻段句数不同""" if not sentences: return [] paras, i, total = [], 0, len(sentences) while i < total: size = random.randint(2, 4) if total - i == 1 and paras: paras[-1] += sentences[i] break end = min(i + size, total) if total - end == 1 and end < total: end -= 1 if end <= i: end = i + 1 paras.append(''.join(sentences[i:end])) i = end # 相邻段句数不同 idx = 1 while idx < len(paras): pc = len(get_sentences(paras[idx - 1])) cc = len(get_sentences(paras[idx])) if pc == cc: cs = get_sentences(paras[idx]) if len(cs) > 2: paras[idx - 1] += cs.pop(0) paras[idx] = ''.join(cs) elif len(get_sentences(paras[idx - 1])) > 2: ps = get_sentences(paras[idx - 1]) last = ps.pop() paras[idx - 1] = ''.join(ps) paras[idx] = last + paras[idx] else: paras[idx - 1] += paras.pop(idx) idx -= 1 idx += 1 # 每段至少 2 句 idx2 = 0 while idx2 < len(paras): if len(get_sentences(paras[idx2])) == 1 and len(paras) > 1: if idx2 > 0: paras[idx2 - 1] += paras.pop(idx2) idx2 -= 1 elif idx2 + 1 < len(paras): paras[idx2 + 1] = paras.pop(idx2) + paras[idx2] idx2 -= 1 idx2 += 1 return paras def unify_punctuation(paragraph: str) -> str: """末句句号,其余句子改逗号""" sents = re.findall(r'[^。!?!?]+[。!?!?]', paragraph) if not sents: return paragraph sents = [s.strip() for s in sents] result = [] for i, sent in enumerate(sents): body = sent for j in range(len(sent) - 1, -1, -1): if sent[j] in '。!?': body = sent[:j] break result.append(body + ('。' if i == len(sents) - 1 else ',')) return ''.join(result) def transform(text: str, mode: str = 'remove') -> str: """ 主处理入口 mode='remove' → 去AI排版(删除引号) mode='corner' → 转直角引号版 """ if not text or not text.strip(): return '' try: if mode == 'corner': text = convert_quotes_to_corner(text) cleaned = clean_symbols(text, keep_corner=True) else: cleaned = clean_symbols(text, keep_corner=False) split_text = apply_split(cleaned) sentences = get_sentences(split_text) if not sentences: return split_text paragraphs = group_paragraphs(sentences) return '\n\n'.join(unify_punctuation(p) for p in paragraphs) except Exception as e: return f'处理出错:{e}' # ────────────────────────────────────────────────────────────── # GUI # ────────────────────────────────────────────────────────────── # 配色常量 C_GREEN = "#1f6e43" C_GREEN_HOVER = "#155a38" C_BLUE = "#2c6e8f" C_BLUE_HOVER = "#1f5c7a" C_SLATE = "#4a6f88" C_SLATE_HOVER = "#375a70" ctk.set_appearance_mode("System") ctk.set_default_color_theme("blue") class Toast(ctk.CTkToplevel): """轻量 toast 弹窗(1.5 s 自动关闭)""" def __init__(self, parent, msg: str): super().__init__(parent) self.title("") self.geometry("300x70") self.resizable(False, False) self.grab_set() # 居中到父窗口 px = parent.winfo_rootx() + parent.winfo_width() // 2 - 150 py = parent.winfo_rooty() + parent.winfo_height() // 2 - 35 self.geometry(f"+{px}+{py}") ctk.CTkLabel(self, text=msg, font=ctk.CTkFont(size=13)).pack(expand=True) self.after(1500, self.destroy) class App(ctk.CTk): def __init__(self): super().__init__() self.title("✍️ 文章处理工具") self.geometry("1280x800") self.minsize(900, 620) self._build_ui() # ── 构建界面 ────────────────────────────────────────────── def _build_ui(self): self.grid_rowconfigure(1, weight=1) self.grid_columnconfigure(0, weight=1) # ── 顶部 Hero ────────────────────────────────────────── hero = ctk.CTkFrame(self, corner_radius=18) hero.grid(row=0, column=0, sticky="ew", padx=20, pady=(16, 8)) hero.grid_columnconfigure(1, weight=1) ctk.CTkLabel( hero, text="✍️ 文章处理工具", font=ctk.CTkFont(size=22, weight="bold"), ).grid(row=0, column=0, padx=20, pady=(14, 4), sticky="w") sub = ctk.CTkLabel( hero, text="过滤符号 · 长句拆分 · 段落分组(2~4句)· 末句句号,其余逗号", font=ctk.CTkFont(size=12), text_color=("gray40", "gray70"), ) sub.grid(row=1, column=0, columnspan=2, padx=24, pady=(0, 14), sticky="w") # 模式切换 self._mode_btn = ctk.CTkSegmentedButton( hero, values=["🧹 去AI排版", "「」 直角引号版"], font=ctk.CTkFont(size=13), width=280, ) self._mode_btn.set("🧹 去AI排版") self._mode_btn.grid(row=0, column=2, padx=20, pady=14, sticky="e") # ── 两列文本区 ───────────────────────────────────────── columns = ctk.CTkFrame(self, fg_color="transparent") columns.grid(row=1, column=0, sticky="nsew", padx=20, pady=0) columns.grid_columnconfigure(0, weight=1) columns.grid_columnconfigure(1, weight=1) columns.grid_rowconfigure(0, weight=1) # 左列 left = ctk.CTkFrame(columns, corner_radius=18) left.grid(row=0, column=0, sticky="nsew", padx=(0, 10)) left.grid_rowconfigure(1, weight=1) left.grid_columnconfigure(0, weight=1) lh = ctk.CTkFrame(left, fg_color="transparent") lh.grid(row=0, column=0, sticky="ew", padx=14, pady=(12, 0)) ctk.CTkLabel(lh, text="📄 原始文章", font=ctk.CTkFont(size=14, weight="bold")).pack(side="left") for text, cmd, color, hov in [ ("加载示例", self._load_example, C_BLUE, C_BLUE_HOVER), ("清 空", self._clear_left, C_SLATE, C_SLATE_HOVER), ("复制原文", self._copy_source, C_SLATE, C_SLATE_HOVER), ]: ctk.CTkButton(lh, text=text, width=72, height=30, corner_radius=20, fg_color=color, hover_color=hov, font=ctk.CTkFont(size=12), command=cmd).pack(side="right", padx=3) self._src = ctk.CTkTextbox(left, font=ctk.CTkFont(size=14), wrap="word", corner_radius=12) self._src.grid(row=1, column=0, sticky="nsew", padx=12, pady=10) # 右列 right = ctk.CTkFrame(columns, corner_radius=18) right.grid(row=0, column=1, sticky="nsew", padx=(10, 0)) right.grid_rowconfigure(1, weight=1) right.grid_columnconfigure(0, weight=1) rh = ctk.CTkFrame(right, fg_color="transparent") rh.grid(row=0, column=0, sticky="ew", padx=14, pady=(12, 0)) ctk.CTkLabel(rh, text="✨ 调整后", font=ctk.CTkFont(size=14, weight="bold")).pack(side="left") ctk.CTkButton(rh, text="复制结果", width=72, height=30, corner_radius=20, fg_color=C_SLATE, hover_color=C_SLATE_HOVER, font=ctk.CTkFont(size=12), command=self._copy_result).pack(side="right", padx=3) self._res = ctk.CTkTextbox(right, font=ctk.CTkFont(size=14), wrap="word", corner_radius=12) self._res.grid(row=1, column=0, sticky="nsew", padx=12, pady=10) self._set_result("等待调整… 请粘贴文章或点击「加载示例」后点击「一键调整」") # ── 底部操作栏 ───────────────────────────────────────── bar = ctk.CTkFrame(self, fg_color="transparent") bar.grid(row=2, column=0, pady=(6, 18)) ctk.CTkButton( bar, text="✨ 一键调整", width=180, height=48, corner_radius=30, font=ctk.CTkFont(size=15, weight="bold"), fg_color=C_GREEN, hover_color=C_GREEN_HOVER, command=self._do_transform, ).pack(side="left", padx=12) ctk.CTkButton( bar, text="🔄 同步并调整", width=180, height=48, corner_radius=30, font=ctk.CTkFont(size=15), fg_color=C_SLATE, hover_color=C_SLATE_HOVER, command=self._do_transform, ).pack(side="left", padx=12) # ── 辅助方法 ─────────────────────────────────────────────── def _get_mode(self) -> str: return 'corner' if '直角' in self._mode_btn.get() else 'remove' def _get_source(self) -> str: return self._src.get("1.0", "end-1c") def _set_result(self, text: str): self._res.configure(state="normal") self._res.delete("1.0", "end") self._res.insert("1.0", text) def _toast(self, msg: str): Toast(self, msg) # ── 按钮回调 ─────────────────────────────────────────────── def _load_example(self): self._src.delete("1.0", "end") self._src.insert("1.0", EXAMPLE_TEXT) self._set_result("示例已加载,点击「一键调整」查看效果。") def _clear_left(self): self._src.delete("1.0", "end") self._set_result("左侧已清空,请粘贴新文章或加载示例。") def _copy_source(self): content = self._get_source() if not content.strip(): self._toast("原文为空,无可复制内容。") return self.clipboard_clear() self.clipboard_append(content) self._toast("✅ 原文已复制") def _copy_result(self): content = self._res.get("1.0", "end-1c") skip = ("等待调整", "左侧无内容", "左侧已清空", "示例已加载", "⚠️") if not content.strip() or any(k in content for k in skip): self._toast("没有可复制的结果,请先生成。") return self.clipboard_clear() self.clipboard_append(content) self._toast("✅ 结果已复制") def _do_transform(self): src = self._get_source() if not src.strip(): self._set_result("⚠️ 左侧无内容,请粘贴文章或点击「加载示例」。") return result = transform(src, self._get_mode()) self._set_result(result) # ────────────────────────────────────────────────────────────── if __name__ == "__main__": app = App() app.mainloop()