文字脚本自动生成

This commit is contained in:
JA
2026-06-08 10:49:52 +08:00
commit c0dfa441a5
10 changed files with 1704 additions and 0 deletions

View File

@@ -0,0 +1,682 @@
# -*- coding: utf-8 -*-
"""
通用游戏文字脚本文档生成工具
功能自动扫描项目目录识别标准结构生成游戏文字脚本Word文档。
无需手动配置路径,只需提供项目根目录即可自动完成全部流程。
标准目录结构约定:
<project_root>/
├── *_config/ ← 配置表目录 (xlsx/csv)
├── *_game/ ← 游戏客户端目录
│ └── unity/Assets/Resources/.../atlas_ui/ ← 图标资源
│ └── unity/Assets/Scripts/.../*Const*.cs ← 系统提示常量
└── work/ ← 工作目录 (CSV缓存、输出等)
用法:
python universal_tool.py [项目路径] [--output 输出路径]
依赖pip install openpyxl python-docx
"""
import csv
import os
import re
import sys
import glob
import argparse
from docx import Document
from docx.shared import Pt, Cm
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.oxml.ns import qn
# ============ 常量 ============
CN_NUMBERS = ["", "", "", "", "", "", "", "", "", "", "十一"]
# 11章节数据源定义字段名取自CSV英文头行第3行
SECTION_DEFS = {
"quest_texts": {"csv": "quest", "field": "desc", "filter": {"category": ["1", "2"]}},
"achievements": {"csv": "quest", "field": "desc", "filter": {"category": ["9"]}},
"guide_texts": {"csv": "guide", "field": "desc"},
"props": {"csv": "prop", "fields": ["name", "icon", "tips"], "icon_key": "atlas_product_icons"},
"skills": {"csv": "skill", "fields": ["name", "icon", "desc"], "icon_key": "atlas_skill"},
"runes": {"csv": "rune", "fields": ["name", "icon", "desc"], "icon_key": "atlas_rune_icons"},
"talents": {"csv": "playerlevel", "fields": ["name", "icon", "desc"], "icon_key": "atlas_talent_icons", "dedup": "name"},
"equips": {"csv": "equip", "fields": ["name", "icon"], "icon_key": "atlas_equip_icons", "dedup": "name"},
"level_names": {"csv": "fight_sample", "field": "name"},
}
# ================================================================
# 一、项目结构自动扫描
# ================================================================
class ProjectScanner:
"""自动扫描并识别项目目录结构"""
def __init__(self, project_root):
self.root = os.path.abspath(project_root)
self.config_dir = None
self.game_dir = None
self.csv_dir = None
self.work_dir = None
self.resources_dir = None
self.icon_dirs = {}
self.scripts_dir = None
self.game_text_const = None
self.project_name = ""
self._scan()
def _scan(self):
# 1. 查找 _config / _game / work 目录
for entry in os.listdir(self.root):
p = os.path.join(self.root, entry)
if not os.path.isdir(p):
continue
if entry.endswith("_config"):
self.config_dir = p
elif entry.endswith("_game"):
self.game_dir = p
elif entry == "work":
self.work_dir = p
# 2. 项目名
if self.config_dir:
base = os.path.basename(self.config_dir)
self.project_name = base.replace("_config", "")
# 3. CSV目录: work/cvs 优先,其次 config_dir 本身
if self.work_dir:
cvs = os.path.join(self.work_dir, "cvs")
if os.path.isdir(cvs) and os.listdir(cvs):
self.csv_dir = cvs
if not self.csv_dir:
self.csv_dir = self.config_dir
# 4. 搜索根目录:优先 Resources_moved其次 Resources
if self.game_dir:
for root, dirs, _ in os.walk(self.game_dir):
for name in ["Resources_moved", "Resources"]:
if name in dirs:
self.resources_dir = os.path.join(root, name)
break
if self.resources_dir:
break
# 5. 图标目录 (递归搜索 atlas_* 子目录,只保留包含图片的叶子目录)
if self.resources_dir:
for root, dirs, files in os.walk(self.resources_dir):
for d in dirs:
if d.startswith("atlas_"):
dp = os.path.join(root, d)
# 检查是否包含图片文件
has_img = any(f.lower().endswith((".png", ".jpg", ".jpeg")) for f in os.listdir(dp))
if has_img:
self.icon_dirs[d] = dp
# 6. Scripts 目录
if self.game_dir:
unity = os.path.join(self.game_dir, "unity")
assets = os.path.join(unity, "Assets") if os.path.isdir(unity) else None
if assets:
scripts = os.path.join(assets, "Scripts")
if os.path.isdir(scripts):
self.scripts_dir = scripts
# 7. GameTextConst.cs
if self.scripts_dir:
for root, _, files in os.walk(self.scripts_dir):
for f in files:
if f.lower().endswith(".cs") and "const" in f.lower() and ("text" in f.lower() or "game" in f.lower()):
self.game_text_const = os.path.join(root, f)
return
# 宽松匹配:任意 *Const*.cs
for root, _, files in os.walk(self.scripts_dir):
for f in files:
if "Const" in f and f.endswith(".cs"):
self.game_text_const = os.path.join(root, f)
return
def find_file(self, name):
"""在 csv_dir 或 config_dir 中查找文件(支持 .csv/.xlsx"""
for d in [self.csv_dir, self.config_dir]:
if not d:
continue
for ext in [".csv", ".xlsx"]:
p = os.path.join(d, name + ext)
if os.path.isfile(p):
return p
return None
def summary(self):
"""打印扫描结果"""
lines = [
f" 项目名称: {self.project_name}",
f" 配置目录: {self.config_dir or '未找到'}",
f" 游戏目录: {self.game_dir or '未找到'}",
f" CSV目录: {self.csv_dir or '未找到'}",
f" 图标目录: {len(self.icon_dirs)}{list(self.icon_dirs.keys())}",
f" 脚本目录: {self.scripts_dir or '未找到'}",
f" 常量文件: {os.path.basename(self.game_text_const) if self.game_text_const else '未找到'}",
]
return "\n".join(lines)
# ================================================================
# 二、CSV 数据读取(自动编码 + 动态字段名解析)
# ================================================================
def read_csv(filepath):
"""
读取CSV文件自动检测编码 (utf-8-sig/gbk/gb2312/utf-8)。
返回 (headers, rows)
headers: 第3行英文字段名列表
rows: 第4行起的数据行列表 (list of list)
"""
for enc in ["utf-8-sig", "gbk", "gb2312", "utf-8"]:
try:
with open(filepath, "r", encoding=enc) as f:
all_rows = list(csv.reader(f))
if len(all_rows) < 4:
return all_rows[-1] if all_rows else [], []
return all_rows[2], all_rows[3:]
except (UnicodeDecodeError, UnicodeError):
continue
return [], []
def field_map(headers):
"""将英文头行转为 {字段名: 列索引} 映射"""
return {h.strip(): i for i, h in enumerate(headers) if h.strip()}
def field_val(row, fm, name, default=""):
"""从数据行中按字段名取值"""
idx = fm.get(name)
if idx is not None and idx < len(row):
return row[idx].strip()
return default
def xlsx_to_csv_batch(config_dir, output_dir):
"""将 config_dir 中所有 xlsx 批量转为 csv"""
from openpyxl import load_workbook
os.makedirs(output_dir, exist_ok=True)
count = 0
for xlsx in glob.glob(os.path.join(config_dir, "*.xlsx")):
fname = os.path.basename(xlsx)
if fname.startswith("~$"):
continue
csv_path = os.path.join(output_dir, os.path.splitext(fname)[0] + ".csv")
try:
wb = load_workbook(xlsx, read_only=True, data_only=True)
ws = wb.active
with open(csv_path, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.writer(f)
for row in ws.iter_rows(values_only=True):
writer.writerow([c if c is not None else "" for c in row])
wb.close()
count += 1
except Exception:
pass
# 复制非xlsx文件
import shutil
for entry in os.listdir(config_dir):
src = os.path.join(config_dir, entry)
if not os.path.isfile(src) or entry.endswith(".xlsx"):
continue
dst = os.path.join(output_dir, entry)
if not os.path.exists(dst):
shutil.copy2(src, dst)
return count
# ================================================================
# 三、图片资源查找
# ================================================================
def find_image(icon_name, icon_dirs, icon_key):
"""根据 icon 名称在指定目录中查找图片文件"""
icon_dir = icon_dirs.get(icon_key)
if not icon_dir or not os.path.isdir(icon_dir):
return None
# 直接匹配
for ext in [".png", ".jpg"]:
p = os.path.join(icon_dir, icon_name + ext)
if os.path.isfile(p):
return p
# 补零匹配 (如 icon_product_1 -> icon_product_01)
m = re.match(r"^(.+?)(\d+)$", icon_name)
if m:
prefix, num = m.group(1), m.group(2)
for pad in [2, 3]:
padded = prefix + num.zfill(pad)
for ext in [".png", ".jpg"]:
p = os.path.join(icon_dir, padded + ext)
if os.path.isfile(p):
return p
return None
# ================================================================
# 四、GameTextConst.cs 自动解析
# ================================================================
def parse_game_text_const(cs_path, scripts_dir):
"""
自动解析 GameTextConst.cs 中所有 public const string 定义,
扫描项目中其他 .cs 文件确认哪些常量被引用,
返回去重后的文本列表。
"""
if not cs_path or not os.path.isfile(cs_path):
return []
# 读取文件
content = None
for enc in ["utf-8-sig", "utf-8", "gbk", "gb2312"]:
try:
with open(cs_path, "r", encoding=enc) as f:
content = f.read()
break
except (UnicodeDecodeError, UnicodeError):
continue
if content is None:
return []
# 提取常量public const string Name = "value";
pattern = r'public\s+const\s+string\s+(\w+)\s*=\s*"((?:[^"\\]|\\.)*)"\s*;'
all_consts = {}
for m in re.finditer(pattern, content):
name, value = m.group(1), m.group(2)
# 还原转义
value = value.replace("\\n", "\n").replace("\\t", "\t").replace('\\"', '"')
all_consts[name] = value
if not all_consts:
return []
# 扫描引用
const_names = set(all_consts.keys())
referenced = set()
cs_file = os.path.basename(cs_path)
if scripts_dir and os.path.isdir(scripts_dir):
for root, _, files in os.walk(scripts_dir):
for fname in files:
if not fname.endswith(".cs") or fname == cs_file:
continue
try:
with open(os.path.join(root, fname), "r", encoding="utf-8", errors="ignore") as f:
src = f.read()
for cn in const_names:
if cn in src:
referenced.add(cn)
except Exception:
continue
# 去重文本值
seen = set()
result = []
for name, text in all_consts.items():
if name in referenced and text not in seen:
seen.add(text)
result.append(text)
return result
# ================================================================
# 五、文档格式化辅助
# ================================================================
def _run(para, text, font="仿宋", size=14, bold=False, underline=None):
"""创建带格式的 run"""
r = para.add_run(text)
r.font.name = font
r._element.rPr.rFonts.set(qn("w:eastAsia"), font)
r.font.size = Pt(size)
r.font.bold = bold
if underline is not None:
r.font.underline = underline
return r
def add_title(doc, text):
"""标题:宋体 二号(22pt) 加粗 居中"""
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
_run(p, text, font="宋体", size=22, bold=True, underline=False)
def add_section_heading(doc, cn_num, title):
"""小标题:仿宋 四号(14pt) 加粗 中文编号"""
p = doc.add_paragraph()
_run(p, f"{cn_num}{title}", font="仿宋", size=14, bold=True)
def add_numbered_items(doc, items):
"""编号列表:仿宋 四号 不加粗"""
for i, text in enumerate(items, 1):
p = doc.add_paragraph()
_run(p, f"{i}.{text}", font="仿宋", size=14, bold=False)
def _set_cell(cell, text, bold=False, size=14):
"""设置表格单元格文本"""
cell.text = ""
p = cell.paragraphs[0]
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
_run(p, str(text), font="仿宋", size=size, bold=bold)
def _insert_img(cell, img_path):
"""在单元格中插入图片高度3cm 锁定纵横比)"""
p = cell.paragraphs[0]
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
p.add_run().add_picture(img_path, height=Cm(3))
def add_data_table(doc, headers, rows, icon_dirs=None, icon_key=None):
"""通用数据表格(支持图片列)"""
table = doc.add_table(rows=1, cols=len(headers), style="Table Grid")
table.alignment = WD_TABLE_ALIGNMENT.CENTER
for i, h in enumerate(headers):
_set_cell(table.rows[0].cells[i], h, bold=True, size=10)
for rd in rows:
row = table.add_row()
_set_cell(row.cells[0], rd[0])
if len(rd) > 1:
icon_name = rd[1]
img = find_image(icon_name, icon_dirs, icon_key) if icon_name and icon_dirs else None
if img:
_insert_img(row.cells[1], img)
else:
_set_cell(row.cells[1], "", size=10)
if len(rd) > 2:
_set_cell(row.cells[2], rd[2], size=10)
def add_name_table(doc, names):
"""玩家姓名表格(姓/名 两列)"""
table = doc.add_table(rows=1, cols=2, style="Table Grid")
table.alignment = WD_TABLE_ALIGNMENT.CENTER
for i, h in enumerate(["", ""]):
_set_cell(table.rows[0].cells[i], h, bold=True)
for surname, given in names:
row = table.add_row()
_set_cell(row.cells[0], surname)
_set_cell(row.cells[1], given)
# ================================================================
# 六、数据提取函数
# ================================================================
def get_player_names(scanner):
"""获取玩家随机名称(优先 name.csv否则使用默认列表"""
fp = scanner.find_file("name")
if fp and fp.endswith(".csv"):
headers, rows = read_csv(fp)
fm = field_map(headers)
if "first" in fm and "last" in fm:
seen = set()
result = []
for r in rows:
first = field_val(r, fm, "first")
last = field_val(r, fm, "last")
if first and last:
key = (first, last)
if key not in seen:
seen.add(key)
result.append(key)
if result:
return result
# 默认名称100对
return [
("", "子轩"), ("", "梓涵"), ("", "浩然"), ("", "欣怡"),
("", "宇轩"), ("", "晨曦"), ("", "俊杰"), ("", "雅静"),
("", "天佑"), ("", "梦琪"), ("", "博文"), ("", "思彤"),
("", "睿智"), ("", "静怡"), ("", "昊天"), ("", "雨桐"),
("", "泽宇"), ("", "婉清"), ("", "俊熙"), ("", "雪柔"),
("", "子豪"), ("", "依琳"), ("", "宇航"), ("", "诗涵"),
("", "志强"), ("", "梦瑶"), ("", "文博"), ("", "雅楠"),
("", "天宇"), ("", "欣妍"), ("", "子睿"), ("", "静雯"),
("", "俊豪"), ("", "雨欣"), ("", "浩宇"), ("", "语嫣"),
("", "子涵"), ("", "思雨"), ("", "博涛"), ("", "雅婷"),
("", "天翔"), ("", "梦洁"), ("", "文轩"), ("", "雪梅"),
("", "志远"), ("", "依娜"), ("", "子安"), ("", "静雅"),
("", "俊峰"), ("", "雨婷"), ("", "浩轩"), ("", "诗琪"),
("", "子恒"), ("", "思雅"), ("", "博超"), ("", "雅琪"),
("", "天瑞"), ("", "梦菲"), ("", "文昊"), ("", "雪莲"),
("", "志鹏"), ("", "依婷"), ("", "子健"), ("", "静淑"),
("", "俊凯"), ("", "雨菲"), ("", "浩南"), ("", "诗瑶"),
("", "子宁"), ("", "思琪"), ("", "博远"), ("", "雅静"),
("", "天浩"), ("", "梦婷"), ("", "文杰"), ("", "雪琪"),
("", "志伟"), ("", "依静"), ("", "子文"), ("", "静怡"),
("", "俊杰"), ("", "雨萱"), ("", "浩天"), ("", "诗涵"),
("", "子辰"), ("", "思妍"), ("", "博文"), ("", "雅欣"),
("", "天佑"), ("", "梦瑶"), ("", "文豪"), ("", "雪莹"),
("", "志强"), ("", "依琳"), ("", "子轩"), ("", "静雯"),
("", "俊熙"), ("", "雨桐"), ("", "浩宇"), ("", "诗琪"),
]
def get_csv_list_data(scanner, csv_name, field_name, filter_dict=None):
"""通用从CSV中按字段名提取去重文本列表"""
fp = scanner.find_file(csv_name)
if not fp or not fp.endswith(".csv"):
return []
headers, rows = read_csv(fp)
fm = field_map(headers)
seen = set()
result = []
for r in rows:
# 过滤条件
if filter_dict:
ok = True
for fk, fvs in filter_dict.items():
if field_val(r, fm, fk) not in fvs:
ok = False
break
if not ok:
continue
val = field_val(r, fm, field_name)
if val and val not in seen:
seen.add(val)
result.append(val)
return result
def get_csv_table_data(scanner, csv_name, field_names, dedup_field=None):
"""通用从CSV中提取多字段表格数据 [(f1, f2, f3, ...), ...]"""
fp = scanner.find_file(csv_name)
if not fp or not fp.endswith(".csv"):
return []
headers, rows = read_csv(fp)
fm = field_map(headers)
seen = set()
result = []
for r in rows:
vals = tuple(field_val(r, fm, fn) for fn in field_names)
if not vals[0]:
continue
if dedup_field:
key = vals[0]
if key in seen:
continue
seen.add(key)
result.append(vals)
return result
# ================================================================
# 七、文档构建主流程
# ================================================================
def build_document(scanner, output_path):
"""根据扫描结果构建完整文档"""
print("\n[1/4] 准备数据源...")
# 确保CSV可用
if not scanner.csv_dir or not any(f.endswith(".csv") for f in os.listdir(scanner.csv_dir)):
if scanner.config_dir:
print(f" CSV目录为空正在从 {os.path.basename(scanner.config_dir)} 转换xlsx...")
cvs_out = os.path.join(scanner.work_dir or scanner.root, "cvs")
n = xlsx_to_csv_batch(scanner.config_dir, cvs_out)
scanner.csv_dir = cvs_out
print(f" 已转换 {n} 个xlsx文件")
# 预加载数据
system_prompts = parse_game_text_const(scanner.game_text_const, scanner.scripts_dir)
player_names = get_player_names(scanner)
print(f" 系统提示: {len(system_prompts)}")
print(f" 玩家名称: {len(player_names)}")
print("\n[2/4] 创建文档...")
doc = Document()
style = doc.styles["Normal"]
style.font.name = "仿宋"
style.font.size = Pt(14)
style._element.rPr.rFonts.set(qn("w:eastAsia"), "仿宋")
doc_title = f"{scanner.project_name}》文字脚本" if scanner.project_name else "游戏文字脚本"
add_title(doc, doc_title)
print("\n[3/4] 写入章节...")
icon_dirs = scanner.icon_dirs
sections_done = 0
# --- 一、系统提示 ---
add_section_heading(doc, CN_NUMBERS[0], "系统提示")
add_numbered_items(doc, system_prompts)
print(f" 一、系统提示: {len(system_prompts)}")
sections_done += 1
# --- 二、任务文本 ---
quest_def = SECTION_DEFS["quest_texts"]
quest_texts = get_csv_list_data(scanner, quest_def["csv"], quest_def["field"], quest_def.get("filter"))
add_section_heading(doc, CN_NUMBERS[1], "任务文本")
add_numbered_items(doc, quest_texts)
print(f" 二、任务文本: {len(quest_texts)}")
sections_done += 1
# --- 三、成就文本 ---
ach_def = SECTION_DEFS["achievements"]
ach_texts = get_csv_list_data(scanner, ach_def["csv"], ach_def["field"], ach_def.get("filter"))
add_section_heading(doc, CN_NUMBERS[2], "成就文本")
add_numbered_items(doc, ach_texts)
print(f" 三、成就文本: {len(ach_texts)}")
sections_done += 1
# --- 四、玩家随机名称文本 ---
add_section_heading(doc, CN_NUMBERS[3], "玩家随机名称文本")
add_name_table(doc, player_names)
print(f" 四、玩家随机名称文本: {len(player_names)}")
sections_done += 1
# --- 五、新手指引文本 ---
guide_def = SECTION_DEFS["guide_texts"]
guide_texts = get_csv_list_data(scanner, guide_def["csv"], guide_def["field"])
add_section_heading(doc, CN_NUMBERS[4], "新手指引文本")
add_numbered_items(doc, guide_texts)
print(f" 五、新手指引文本: {len(guide_texts)}")
sections_done += 1
# --- 六、道具说明 ---
prop_def = SECTION_DEFS["props"]
prop_data = get_csv_table_data(scanner, prop_def["csv"], prop_def["fields"])
add_section_heading(doc, CN_NUMBERS[5], "道具说明")
add_data_table(doc, ["道具名称", "图标", "道具描述"], prop_data, icon_dirs, prop_def["icon_key"])
print(f" 六、道具说明: {len(prop_data)}")
sections_done += 1
# --- 七、技能 ---
skill_def = SECTION_DEFS["skills"]
skill_data = get_csv_table_data(scanner, skill_def["csv"], skill_def["fields"])
add_section_heading(doc, CN_NUMBERS[6], "技能")
add_data_table(doc, ["技能名称", "技能图标", "技能描述"], skill_data, icon_dirs, skill_def["icon_key"])
print(f" 七、技能: {len(skill_data)}")
sections_done += 1
# --- 八、灵石 ---
rune_def = SECTION_DEFS["runes"]
rune_data = get_csv_table_data(scanner, rune_def["csv"], rune_def["fields"])
add_section_heading(doc, CN_NUMBERS[7], "灵石")
add_data_table(doc, ["灵石名称", "灵石图标", "灵石介绍"], rune_data, icon_dirs, rune_def["icon_key"])
print(f" 八、灵石: {len(rune_data)}")
sections_done += 1
# --- 九、天赋 ---
talent_def = SECTION_DEFS["talents"]
talent_data = get_csv_table_data(scanner, talent_def["csv"], talent_def["fields"], talent_def.get("dedup"))
add_section_heading(doc, CN_NUMBERS[8], "天赋")
add_data_table(doc, ["天赋名称", "天赋图标", "天赋介绍"], talent_data, icon_dirs, talent_def["icon_key"])
print(f" 九、天赋: {len(talent_data)}")
sections_done += 1
# --- 十、装备 ---
equip_def = SECTION_DEFS["equips"]
equip_data = get_csv_table_data(scanner, equip_def["csv"], equip_def["fields"], equip_def.get("dedup"))
add_section_heading(doc, CN_NUMBERS[9], "装备")
add_data_table(doc, ["装备名称", "装备图标"], equip_data, icon_dirs, equip_def["icon_key"])
print(f" 十、装备: {len(equip_data)}")
sections_done += 1
# --- 十一、关卡名称 ---
level_def = SECTION_DEFS["level_names"]
level_names = get_csv_list_data(scanner, level_def["csv"], level_def["field"])
add_section_heading(doc, CN_NUMBERS[10], "关卡名称")
add_numbered_items(doc, level_names)
print(f" 十一、关卡名称: {len(level_names)}")
sections_done += 1
print(f"\n[4/4] 保存文档...")
doc.save(output_path)
print(f" 文档已生成: {output_path}")
print(f"{sections_done} 个章节")
# ================================================================
# 主入口
# ================================================================
def main():
parser = argparse.ArgumentParser(description="通用游戏文字脚本文档生成工具")
parser.add_argument("project_path", nargs="?", help="项目根目录路径 (默认: 当前目录)")
parser.add_argument("--output", "-o", help="输出文档路径 (默认: 自动生成)")
args = parser.parse_args()
project_path = args.project_path or os.getcwd()
if not os.path.isdir(project_path):
print(f"错误: 目录不存在 - {project_path}")
sys.exit(1)
print(f"{'='*50}")
print(f" 通用游戏文字脚本生成工具")
print(f"{'='*50}")
print(f"\n扫描项目: {project_path}")
scanner = ProjectScanner(project_path)
print(f"\n{scanner.summary()}")
if not scanner.csv_dir and not scanner.config_dir:
print("\n错误: 未找到配置目录 (*_config) 或CSV目录")
sys.exit(1)
if args.output:
output_path = args.output
else:
work_dir = scanner.work_dir or os.path.join(project_path, "work")
os.makedirs(work_dir, exist_ok=True)
title = scanner.project_name or "游戏"
output_path = os.path.join(work_dir, f"{title}》游戏文字脚本.docx")
build_document(scanner, output_path)
print(f"\n{'='*50}")
print(f" 全部完成!")
print(f"{'='*50}")
if __name__ == "__main__":
main()