#参考源代码结构

# 典例
#["伊沢先生",[[null,"「昔はあったそうなんですが、数年前に廃部になったとか」",27]]
#["恵凪",[[null,"「いえ、でも、あのっ、神仙には軽音部がある\\nって聞いたんですけど……?」",35,"「いえ、でも、あのっ、神仙には軽音部があるって聞いたんですけど……?」","「いえ、でも、あのっ、神仙には軽音部があるって聞いたんですけど……?」"]]
#["伊沢先生",[[null,"「あの、陽見さん? 大丈夫ですか?」",18]]
#["莉々子",[[null,"「動画を友達に広めてもらって。\\nそれをさらに、友達の友達にも頼んで。\\nついでに友達の友達の友達にも――」",50,"「動画を友達に広めてもらって。それをさらに、友達の友達にも頼んで。ついでに友達の友達の友達にも――」","「動画を友達に広めてもらって。それをさらに、友達の友達にも頼んで。ついでに友達の友達の友達にも――」"]]
#["莉々子",[[null,"「ショート動画も作って、\\nYoupipeだけじゃなく[ヒ]H[ッ]i[ク]k[ニ]N[ッ]i[ク]kにも上げて\\n新たな導線確保して」",46,"「ショート動画も作って、Youpipeだけじゃなくヒックニックにも上げて新たな導線確保して」","「ショート動画も作って、YoupipeだけじゃなくHikNikにも上げて新たな導線確保して」"]]
#「動画を友達に広めてもらって。\\nそれをさらに、友達の友達にも頼んで。\\nついでに友達の友達の友達にも――」计数num就为50
#2:["char"*****null,[["exc"*****null,"exc",num,"exc","exc"]]
#1:["char"*****null,[["exc"*****null,"exc",num]]

#一键配置环境

pip install openpyxl

#需要的文件

ks.scn->xxxxx.json 您需要提供json脚本文件。(Ulysses-FreeMoteToolkit)

xxxxx.xlsx(随便放在哪)

#教程

复制代码改成Python文件

双击运行“xxxxx.py”

添加json、选择excel输出、开始提取以完成原文的提取。

回写时逆向操作下就行。自动根据规则计算num并回写译文、人名、num等等。

#注意

原文出现的任何转义字符请在译文的对应位置保留!!!对于多种结构脚本会自动识别回写。

关于手机界面的文本请自行检索并翻译。

#代码

import json
import os
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from dataclasses import dataclass
from typing import Any, List, Optional, Tuple
from openpyxl import Workbook, load_workbook
from openpyxl.styles import Font, PatternFill, Alignment


@dataclass
class ExtractedEntry:
    source_file: str
    order_index: int
    speaker: Optional[str]  # None 代表 null
    exc_head: Optional[str]  # None 代表 null(即 "exc"*****null 的左侧值)
    original_text: str  # 原文(结构2取三个 exc 中的第一个)
    translated_text: str  # 译文(导出时为空,导入时读取)
    struct_type: int  # 1 或 2


class LimeLemonExtractorApp:
    def __init__(self, master: tk.Tk) -> None:
        self.master = master
        master.title("ライムライト・レモネードジャム 提取/回写工具")
        master.geometry("980x680")

        self.status_text = tk.StringVar(value="就绪")
        self.input_files: List[str] = []
        self.excel_path = tk.StringVar()

        self._build_ui()

    def _build_ui(self) -> None:
        nb = ttk.Notebook(self.master)
        nb.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        self.tab_extract = ttk.Frame(nb)
        self.tab_import = ttk.Frame(nb)
        nb.add(self.tab_extract, text="提取到Excel")
        nb.add(self.tab_import, text="从Excel回写")

        # 提取 TAB
        self._build_extract_tab(self.tab_extract)
        # 回写 TAB
        self._build_import_tab(self.tab_import)

        status_bar = ttk.Label(self.master, textvariable=self.status_text, relief=tk.SUNKEN)
        status_bar.pack(fill=tk.X, padx=10, pady=(0, 10))

    def _build_extract_tab(self, parent: ttk.Frame) -> None:
        file_frame = ttk.LabelFrame(parent, text="JSON 文件")
        file_frame.pack(fill=tk.X, padx=10, pady=8)

        self.file_listbox = tk.Listbox(file_frame, height=6)
        self.file_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(8, 4), pady=8)
        scroll = ttk.Scrollbar(file_frame, orient="vertical", command=self.file_listbox.yview)
        self.file_listbox.configure(yscrollcommand=scroll.set)
        scroll.pack(side=tk.RIGHT, fill=tk.Y, padx=(0, 8), pady=8)

        btns = ttk.Frame(parent)
        btns.pack(fill=tk.X, padx=10)
        ttk.Button(btns, text="添加JSON", command=self._add_json_files).pack(side=tk.LEFT, padx=4, pady=4)
        ttk.Button(btns, text="清空", command=self._clear_json_files).pack(side=tk.LEFT, padx=4, pady=4)

        out_frame = ttk.Frame(parent)
        out_frame.pack(fill=tk.X, padx=10, pady=4)
        ttk.Label(out_frame, text="Excel 输出:").pack(side=tk.LEFT)
        ttk.Entry(out_frame, textvariable=self.excel_path, width=60).pack(side=tk.LEFT, padx=6)
        ttk.Button(out_frame, text="选择...", command=self._choose_excel_save).pack(side=tk.LEFT)

        self.extract_progress = ttk.Progressbar(parent, mode='determinate')
        self.extract_progress.pack(fill=tk.X, padx=10, pady=6)

        ttk.Button(parent, text="开始提取到Excel", command=self._safe_export_excel).pack(pady=6)

        self.preview_text = tk.Text(parent, height=12, wrap=tk.WORD)
        self.preview_text.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))

    def _build_import_tab(self, parent: ttk.Frame) -> None:
        file_frame = ttk.LabelFrame(parent, text="Excel 文件")
        file_frame.pack(fill=tk.X, padx=10, pady=8)

        self.import_excel_entry = tk.StringVar()
        ttk.Label(file_frame, text="Excel:").pack(side=tk.LEFT, padx=6, pady=8)
        ttk.Entry(file_frame, textvariable=self.import_excel_entry, width=60).pack(side=tk.LEFT, padx=6)
        ttk.Button(file_frame, text="选择...", command=self._choose_excel_open).pack(side=tk.LEFT)

        src_frame = ttk.LabelFrame(parent, text="JSON 源文件(保持与导出时一致)")
        src_frame.pack(fill=tk.X, padx=10, pady=8)

        self.import_file_list = tk.Listbox(src_frame, height=6)
        self.import_file_list.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(8, 4), pady=8)
        scroll = ttk.Scrollbar(src_frame, orient="vertical", command=self.import_file_list.yview)
        self.import_file_list.configure(yscrollcommand=scroll.set)
        scroll.pack(side=tk.RIGHT, fill=tk.Y, padx=(0, 8), pady=8)

        btns = ttk.Frame(parent)
        btns.pack(fill=tk.X, padx=10)
        ttk.Button(btns, text="添加JSON", command=self._add_import_json_files).pack(side=tk.LEFT, padx=4, pady=4)
        ttk.Button(btns, text="清空", command=self._clear_import_json_files).pack(side=tk.LEFT, padx=4, pady=4)

        self.import_progress = ttk.Progressbar(parent, mode='determinate')
        self.import_progress.pack(fill=tk.X, padx=10, pady=6)

        ttk.Button(parent, text="开始回写", command=self._safe_import_excel).pack(pady=6)

        self.import_preview = tk.Text(parent, height=12, wrap=tk.WORD)
        self.import_preview.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))

    # 选择/管理文件
    def _add_json_files(self) -> None:
        files = filedialog.askopenfilenames(filetypes=[("JSON 文件", "*.json")], title="选择 JSON 脚本文件")
        if not files:
            return
        for f in files:
            if f not in self.input_files:
                self.input_files.append(f)
                self.file_listbox.insert(tk.END, os.path.basename(f))
        self.status_text.set(f"已添加 {len(files)} 个文件")

    def _clear_json_files(self) -> None:
        self.input_files.clear()
        self.file_listbox.delete(0, tk.END)
        self.status_text.set("已清空文件列表")

    def _choose_excel_save(self) -> None:
        filename = filedialog.asksaveasfilename(defaultextension=".xlsx", filetypes=[("Excel 文件", "*.xlsx")])
        if filename:
            self.excel_path.set(filename)

    def _choose_excel_open(self) -> None:
        filename = filedialog.askopenfilename(filetypes=[("Excel 文件", "*.xlsx")])
        if filename:
            self.import_excel_entry.set(filename)

    def _add_import_json_files(self) -> None:
        files = filedialog.askopenfilenames(filetypes=[("JSON 文件", "*.json")], title="选择 原始 JSON 脚本文件")
        if not files:
            return
        for f in files:
            self.import_file_list.insert(tk.END, f)
        self.status_text.set(f"回写源 JSON 文件添加 {len(files)} 个")

    def _clear_import_json_files(self) -> None:
        self.import_file_list.delete(0, tk.END)
        self.status_text.set("已清空回写 JSON 列表")

    # 提取到 Excel
    def _safe_export_excel(self) -> None:
        try:
            self._export_excel()
        except Exception as e:
            messagebox.showerror("错误", f"导出失败: {e}")

    def _export_excel(self) -> None:
        if not self.input_files:
            messagebox.showerror("错误", "请添加 JSON 文件")
            return
        if not self.excel_path.get():
            messagebox.showerror("错误", "请选择 Excel 输出路径")
            return

        all_entries: List[ExtractedEntry] = []
        order_counter = 0
        total = len(self.input_files)
        for idx, file_path in enumerate(self.input_files):
            self.extract_progress['value'] = (idx + 1) / total * 100
            self.master.update_idletasks()
            with open(file_path, 'r', encoding='utf-8') as f:
                data = json.load(f)
            file_entries = self._extract_from_data(os.path.basename(file_path), data, start_index=order_counter)
            order_counter += len(file_entries)
            all_entries.extend(file_entries)

        # 写 Excel
        wb = Workbook()
        ws = wb.active
        ws.title = "提取结果"
        headers = [
            "原文件",          # 源文件名
            "顺序排序",        # 全局顺序索引
            "人名",           # speaker(null 写 null)
            "exc/null",       # "exc"*****null 的部分(字符串或 null)
            "原文",           # 原文(结构2取第一个 exc)
            "译文",           # 留空
            "结构类型",        # 1 或 2
        ]
        for col, header in enumerate(headers, 1):
            cell = ws.cell(row=1, column=col, value=header)
            cell.font = Font(bold=True)
            cell.fill = PatternFill(start_color="DDDDDD", end_color="DDDDDD", fill_type="solid")
            cell.alignment = Alignment(horizontal="center")

        for row_idx, e in enumerate(all_entries, start=2):
            ws.cell(row=row_idx, column=1, value=e.source_file)
            ws.cell(row=row_idx, column=2, value=e.order_index)
            ws.cell(row=row_idx, column=3, value=("null" if e.speaker is None else e.speaker))
            ws.cell(row=row_idx, column=4, value=("null" if e.exc_head is None else e.exc_head))
            ws.cell(row=row_idx, column=5, value=e.original_text)
            ws.cell(row=row_idx, column=6, value="")
            ws.cell(row=row_idx, column=7, value=e.struct_type)

        # 调整列宽
        widths = [65, 10, 16, 16, 75, 75, 10]
        for i, w in enumerate(widths, start=1):
            ws.column_dimensions[ws.cell(row=1, column=i).column_letter].width = w

        wb.save(self.excel_path.get())
        self.status_text.set(f"导出完成:{len(all_entries)} 条")
        self.preview_text.delete(1.0, tk.END)
        self.preview_text.insert(1.0, f"已导出 {len(all_entries)} 条记录到: {self.excel_path.get()}")

    def _extract_from_data(self, source_file: str, data: Any, start_index: int) -> List[ExtractedEntry]:
        entries: List[ExtractedEntry] = []

        def visit(node: Any) -> None:
            # 识别两类结构:
            # 结构1: [speaker(str/None), [ [exc_or_null, text, num] , ... ]]
            # 结构2: [speaker(str/None), [ [exc_or_null, text, num, text, text] , ... ]]
            if isinstance(node, list) and len(node) >= 2 and isinstance(node[0], (str, type(None))):
                speaker_val: Optional[str] = None if node[0] is None else str(node[0])
                container = node[1]
                if isinstance(container, list):
                    for item in container:
                        if isinstance(item, list) and len(item) >= 3 and isinstance(item[1], str) and isinstance(item[2], int):
                            exc_head_val: Optional[str] = None
                            if len(item) >= 1 and (isinstance(item[0], str) or item[0] is None):
                                exc_head_val = None if item[0] is None else str(item[0])

                            if len(item) >= 5 and isinstance(item[3], str) and isinstance(item[4], str):
                                struct_type = 2
                                first_text = item[1]
                            else:
                                struct_type = 1
                                first_text = item[1]

                            entries.append(ExtractedEntry(
                                source_file=source_file,
                                order_index=start_index + len(entries),
                                speaker=speaker_val,
                                exc_head=exc_head_val,
                                original_text=first_text,
                                translated_text="",
                                struct_type=struct_type,
                            ))

            # 递归
            if isinstance(node, list):
                for elem in node:
                    visit(elem)
            elif isinstance(node, dict):
                for v in node.values():
                    visit(v)

        visit(data)
        return entries

    # 从 Excel 回写
    def _safe_import_excel(self) -> None:
        try:
            self._import_excel()
        except Exception as e:
            messagebox.showerror("错误", f"回写失败: {e}")

    def _import_excel(self) -> None:
        excel_file = self.import_excel_entry.get().strip()
        if not excel_file:
            messagebox.showerror("错误", "请选择 Excel 文件")
            return
        if not os.path.exists(excel_file):
            messagebox.showerror("错误", "Excel 文件不存在")
            return

        json_files: List[str] = list(self.import_file_list.get(0, tk.END))
        if not json_files:
            messagebox.showerror("错误", "请添加要回写的 JSON 文件(与导出时一致)")
            return

        # 读取 Excel 行
        wb = load_workbook(excel_file)
        ws = wb.active
        rows: List[Tuple[str, int, str, str, str, str, int]] = []
        # 标题: 来自哪, 顺序排序, 人名, exc或null, 原文, 译文, 结构类型
        for r in range(2, ws.max_row + 1):
            src = ws.cell(row=r, column=1).value
            order_idx = ws.cell(row=r, column=2).value
            name_cell = ws.cell(row=r, column=3).value
            exc_head_cell = ws.cell(row=r, column=4).value
            original = ws.cell(row=r, column=5).value
            translated = ws.cell(row=r, column=6).value
            struct_type = ws.cell(row=r, column=7).value

            if src and original and translated is not None:  # 允许空字符串作为译文
                rows.append((str(src), int(order_idx), str(name_cell) if name_cell is not None else "null",
                             str(exc_head_cell) if exc_head_cell is not None else "null",
                             str(original), str(translated), int(struct_type)))

        # 分文件处理
        file_groups: dict[str, List[Tuple[str, int, str, str, str, str, int]]] = {}
        for row in rows:
            file_groups.setdefault(row[0], []).append(row)

        total = len(file_groups)
        for idx, (file_name, items) in enumerate(file_groups.items()):
            self.import_progress['value'] = (idx + 1) / max(total, 1) * 100
            self.master.update_idletasks()

            json_path = self._find_json_full_path(json_files, file_name)
            if not json_path:
                self._append_import_log(f"警告:未找到源文件 {file_name}\n")
                continue

            with open(json_path, 'r', encoding='utf-8') as f:
                data = json.load(f)

            # 根据顺序索引进行匹配与替换
            order_to_row = {order_idx: (name, exc_head, original, translated, struct_type)
                            for (_, order_idx, name, exc_head, original, translated, struct_type) in items}

            current_order = -1

            def replace_visit(node: Any) -> None:
                nonlocal current_order
                if isinstance(node, list) and len(node) >= 2 and isinstance(node[0], (str, type(None))):
                    speaker_ref = node  # node[0]
                    container = node[1]
                    if isinstance(container, list):
                        for inner in container:
                            if isinstance(inner, list) and len(inner) >= 3 and isinstance(inner[1], str) and isinstance(inner[2], int):
                                current_order += 1
                                if current_order in order_to_row:
                                    name_val, exc_head_val, original, translated, struct_type = order_to_row[current_order]

                                    # 写入 speaker(人名)
                                    speaker_value_to_set = None if name_val == "null" else name_val
                                    node[0] = speaker_value_to_set

                                    # 写入 exc 或 null(第一位)
                                    inner[0] = (None if exc_head_val == "null" else exc_head_val)

                                    # 写入文本,且重算 num
                                    if struct_type == 1:
                                        inner[1] = translated
                                        inner[2] = self._recount_num(translated)
                                    else:
                                        # 结构2:第一个 exc 用译文;第二、三个 exc 用去除 \n 和 %n; 的版本
                                        inner[1] = translated
                                        compact = self._remove_newline_and_percent_n(translated)
                                        # 保障长度
                                        if len(inner) >= 3:
                                            inner[2] = self._recount_num(translated)
                                        # 确保位置 3 和 4 存在
                                        if len(inner) >= 4:
                                            inner[3] = compact
                                        if len(inner) >= 5:
                                            inner[4] = compact

                if isinstance(node, list):
                    for e in node:
                        replace_visit(e)
                elif isinstance(node, dict):
                    for v in node.values():
                        replace_visit(v)

            replace_visit(data)

            with open(json_path, 'w', encoding='utf-8') as f:
                json.dump(data, f, ensure_ascii=False, indent=2)

            self._append_import_log(f"已回写:{file_name},共 {len(items)} 条\n")

        self._append_import_log("回写完成\n")
        self.status_text.set("回写完成")

    # 计数与规范化
    def _recount_num(self, text: str) -> int:
        # 规则:所有字符各算一个;\\n 算一个;序列 "%n;" 完全不算,序列 "%n2;" 完全不算
        if not text:
            return 0
        count = 0
        i = 0
        n = len(text)
        while i < n:
            # 处理 %n; 三字符序列
            if i + 2 < n and text[i] == '%' and text[i + 1] == 'n' and text[i + 2] == ';':
                i += 3
                continue
            # 处理 %n2; 四字符序列
            if i + 3 < n and text[i] == "%" and text[i + 1] == 'n' and text[i + 2] == '2' and text[i + 3] == ';':
                i +=4
                continue 
            # 处理 \n 两字符序列
            if i + 1 < n and text[i] == '\\' and text[i + 1] == 'n':
                count += 1
                i += 2
                continue
            # 其他字符都计数
            count += 1
            i += 1
        return count

    def _remove_newline_and_percent_n(self, text: str) -> str:
        # 删除 \n 和 %n;和%n2;(与计数规则一致)
        if not text:
            return text
        # 先去掉 %n;
        res = []
        i = 0
        n = len(text)
        while i < n:
            if i + 2 < n and text[i] == '%' and text[i + 1] == 'n' and text[i + 2] == ';':
                i += 3
                continue
            if i + 3 < n and text[i] == "%" and text[i + 1] == 'n' and text[i + 2] == '2' and text[i + 3] == ';':
                i += 4
                continue 
            if i + 1 < n and text[i] == '\\' and text[i + 1] == 'n':
                i += 2
                continue
            res.append(text[i])
            i += 1
        return ''.join(res)

    def _find_json_full_path(self, candidates: List[str], file_name: str) -> Optional[str]:
        for p in candidates:
            if os.path.basename(p) == file_name:
                return p
        return None

    def _append_import_log(self, text: str) -> None:
        self.import_preview.insert(tk.END, text)
        self.import_preview.see(tk.END)


def main() -> None:
    root = tk.Tk()
    app = LimeLemonExtractorApp(root)
    root.mainloop()


if __name__ == '__main__':
    try:
        main()
    except Exception as e:
        print(f"启动失败: {e}")
  • reward_image1
愿你保持不变,保持己见,充满热血。
最后更新于 2025-09-14