#参考源代码结构
# 典例
#["伊沢先生",[[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}")
Comments 2 条评论