【自然语言处理】汉字表管理工具

目录

一、引言

二、整体架构与核心设计理念

[1. 类定位](#1. 类定位)

[2. 核心设计原则](#2. 核心设计原则)

三、核心属性详解

四、核心方法详解(按功能模块分类)

[模块 1:初始化与基础处理(私有 / 核心方法)](#模块 1:初始化与基础处理(私有 / 核心方法))

[1. init(self, char_list: List[str] = None)](#1. init(self, char_list: List[str] = None))

[2. _deduplicate(self)](#2. _deduplicate(self))

[模块 2:数据校验工具方法(私有)](#模块 2:数据校验工具方法(私有))

[1. _stroke_check(self, val: Any) -> bool](#1. _stroke_check(self, val: Any) -> bool)

[2. _pinyin_check(self, val: Any) -> bool](#2. _pinyin_check(self, val: Any) -> bool)

[模块 3:关联数据管理(拼音 / 笔画等)](#模块 3:关联数据管理(拼音 / 笔画等))

[1. add_related_data(self, data_type: str, data_list: List[Any], check_func: Callable = None)](#1. add_related_data(self, data_type: str, data_list: List[Any], check_func: Callable = None))

[2. get_related_data(self, char: str, data_type: str) -> Union[str, int, None]](#2. get_related_data(self, char: str, data_type: str) -> Union[str, int, None])

[3. auto_add_pinyin(self, style: str = "tone", heteronym: bool = True)](#3. auto_add_pinyin(self, style: str = "tone", heteronym: bool = True))

[模块 4:汉字列表操作(批量添加 / 替换)](#模块 4:汉字列表操作(批量添加 / 替换))

[1. batch_add_chars(self, chars: List[str]) -> int](#1. batch_add_chars(self, chars: List[str]) -> int)

[2. batch_rule_replace(self, search_type: str, search_value: str, replace_rules: Dict[str, str]) -> Dict[str, Any]](#2. batch_rule_replace(self, search_type: str, search_value: str, replace_rules: Dict[str, str]) -> Dict[str, Any])

[模块 5:版本控制(快照 / 回滚)](#模块 5:版本控制(快照 / 回滚))

[1. save_version_snapshot(self, description: str = "") -> Dict[str, Any]](#1. save_version_snapshot(self, description: str = "") -> Dict[str, Any])

[2. get_version_snapshots(self) -> List[Dict[str, Any]]](#2. get_version_snapshots(self) -> List[Dict[str, Any]])

[3. rollback_to_version(self, version: int) -> bool](#3. rollback_to_version(self, version: int) -> bool)

[模块 6:汉字编码分析(生僻字识别 / 可视化)](#模块 6:汉字编码分析(生僻字识别 / 可视化))

[1. get_char_code_info(self, char: str) -> Dict[str, Any]](#1. get_char_code_info(self, char: str) -> Dict[str, Any])

[2. export_code_visualization(self, file_prefix: str, chart_type: str = "both") -> None](#2. export_code_visualization(self, file_prefix: str, chart_type: str = "both") -> None)

[模块 7:字表差异对比与 HTML 导出](#模块 7:字表差异对比与 HTML 导出)

[1. compare_char_tables(self, other_mapper: "CharTableMapper", compare_related: bool = False) -> Dict[str, Any]](#1. compare_char_tables(self, other_mapper: "CharTableMapper", compare_related: bool = False) -> Dict[str, Any])

[2. export_diff_to_html(self, diff_result: Dict[str, Any], file_path: str, title: str = "汉字表差异对比", template_type: str = "default") -> None](#2. export_diff_to_html(self, diff_result: Dict[str, Any], file_path: str, title: str = "汉字表差异对比", template_type: str = "default") -> None)

[模块 8:操作日志管理(导出)](#模块 8:操作日志管理(导出))

[export_operation_log(self, file_path: str, log_type: str = "all", file_format: str = "excel") -> None](#export_operation_log(self, file_path: str, log_type: str = "all", file_format: str = "excel") -> None)

[模块 9:数据持久化(保存 / 加载)](#模块 9:数据持久化(保存 / 加载))

[子模块 A:保存为多格式](#子模块 A:保存为多格式)

[子模块 B:从多格式加载](#子模块 B:从多格式加载)

[五、测试代码详解(if name == "main" 部分)](#五、测试代码详解(if name == "main" 部分))

六、功能亮点与适用场景

[1. 核心亮点](#1. 核心亮点)

[2. 适用场景](#2. 适用场景)

七、依赖与运行说明

[1. 必装依赖](#1. 必装依赖)

[2. 可选依赖(可视化)](#2. 可选依赖(可视化))

[3. 运行注意事项](#3. 运行注意事项)

八、汉字表管理工具的Python代码完整实现

九、程序运行结果展示

十、总结


一、引言

本文介绍的项目是一套汉字表管理工具 ,核心是 CharTableMapper 类,覆盖汉字表从初始化、数据关联、批量操作、版本控制到多格式导入导出的全生命周期管理,同时配套完整的测试代码验证所有功能。以下是逐模块、逐功能的详细解析以及Python代码完整实现。

二、整体架构与核心设计理念

1. 类定位

CharTableMapper 是面向结构化汉字列表的管理类,核心目标是解决:

  • 汉字列表的去重、索引映射
  • 拼音 / 笔画等关联数据的校验与管理
  • 批量替换 / 拼音修正的可追溯性
  • 版本快照与回滚(防止误操作)
  • 汉字编码分析(生僻字识别)
  • 多格式数据持久化与差异对比

2. 核心设计原则

  • 唯一性:初始化 / 添加汉字时自动去重,确保字表无重复单字
  • 可追溯:记录所有替换 / 拼音修正操作,支持版本快照与回滚
  • 多兼容:支持 TXT/CSV/Excel/JSON 四种格式的导入导出
  • 可视化:内置编码分布、生僻字占比的图表生成能力
  • 可校验:对拼音 / 笔画等关联数据做格式校验,避免无效数据

三、核心属性详解

CharTableMapper 的实例属性是功能实现的基础,所有属性如下:

属性名 类型 核心作用 补充说明
char_array List[str] 存储去重后的汉字列表(核心载体) 仅包含单字符汉字,初始化 / 添加时自动去重
char_to_index Dict[str, int] 汉字→索引的映射字典 快速通过汉字查位置,O (1) 时间复杂度
index_to_char Dict[int, str] 索引→汉字的映射字典 快速通过位置查汉字
related_data Dict[str, List[Any]] 存储拼音 / 笔画等关联数据 Key 为数据类型(如 "pinyin"/"stroke"),Value 为与 char_array 长度一致的列表
unique_chars List[str] 等价于 char_array 冗余属性,强化 "唯一性" 语义
replace_history List[Dict[str, Any]] 批量替换操作的历史记录 每条记录包含操作时间、规则、影响汉字、操作前后字表等
pinyin_correct_history List[Dict[str, Any]] 拼音修正操作的历史记录 每条记录包含修正字典、影响汉字、修正前后拼音等
version_snapshots List[Dict[str, Any]] 版本快照列表 每个快照包含字表、关联数据、历史记录长度等完整状态
history_index int 历史记录指针 标记当前操作在历史记录中的位置(暂未深度使用)

四、核心方法详解(按功能模块分类)

模块 1:初始化与基础处理(私有 / 核心方法)

1. __init__(self, char_list: List[str] = None)
  • 作用:初始化汉字表,完成去重、索引映射构建

  • 参数char_list - 初始汉字列表(可选,默认空列表)

  • 内部逻辑

    1. 初始化 char_array 为传入列表(空则为 [])
    2. 构建初始的 char_to_index/index_to_char 映射
    3. 初始化所有历史记录 / 快照属性
    4. 调用 _deduplicate() 去重
  • 使用示例

    python 复制代码
    mapper = CharTableMapper(["中", "行", "乐", "中"])  # 自动去重为 ["中", "行", "乐"]
2. _deduplicate(self)
  • 作用 :私有方法,对 char_array 去重并保持原有顺序
  • 核心逻辑
    1. set 记录已出现的汉字,遍历 char_array
    2. 仅保留 "单字符 + 未出现过" 的汉字
    3. 重新构建 char_to_index/index_to_char 映射
  • 注意:仅处理单字符,多字符会被直接过滤

模块 2:数据校验工具方法(私有)

1. _stroke_check(self, val: Any) -> bool
  • 作用:校验笔画数是否有效
  • 规则 :必须是正整数(isinstance(val, int) and val > 0
  • 使用场景 :添加笔画数据时的校验(add_related_data 调用)
2. _pinyin_check(self, val: Any) -> bool
  • 作用:校验拼音格式是否有效
  • 规则
    1. 必须是字符串
    2. 支持 "/ 分隔的多音拼音"(如 "xíng/háng"),但每个部分不能为空
  • 使用场景 :添加拼音数据时的校验(add_related_data 调用)

模块 3:关联数据管理(拼音 / 笔画等)

  • 作用:为字表添加关联数据(拼音 / 笔画 / 部首等),并校验数据有效性

  • 参数

    • data_type:数据类型名称(如 "pinyin"/"stroke")
    • data_list:与 char_array 长度一致的关联数据列表
    • check_func:校验函数(可选,如 _stroke_check/_pinyin_check
  • 核心逻辑

    1. 校验 data_list 长度与字表一致,不一致则抛异常
    2. check_func 校验每条数据,无效则设为 None 并打印警告
    3. 将校验后的数据存入 related_data[data_type]
  • 异常 :数据长度不匹配时抛出 ValueError

  • 使用示例

    python 复制代码
    mapper.add_related_data("stroke", [4, 6, 5], check_func=mapper._stroke_check)  # 为3个汉字添加笔画数
  • 作用:获取单个汉字的指定类型关联数据

  • 参数

    • char:目标汉字
    • data_type:数据类型(如 "pinyin")
  • 返回值 :对应数据(无则返回 None

  • 使用示例

    python 复制代码
    pinyin = mapper.get_related_data("行", "pinyin")  # 获取"行"的拼音
3. auto_add_pinyin(self, style: str = "tone", heteronym: bool = True)
  • 作用 :基于 pypinyin 自动为所有汉字生成拼音,并存入 related_data["pinyin"]

  • 参数

    • style:拼音风格(可选值:"tone"/"tone2"/"normal"/"initial"/"final"),对应 pypinyin.Style
    • heteronym:是否保留多音字(True = 保留,用 "/" 分隔;False = 取第一个读音)
  • 核心逻辑

    1. 映射 stylepypinyin 的枚举值
    2. 遍历每个汉字,调用 lazy_pinyin 生成拼音
    3. 多音字拼接为 "读音 1 / 读音 2" 格式,失败则设为 None
    4. 调用 add_related_data 存入拼音数据(用 _pinyin_check 校验)
  • 异常 :拼音风格无效时抛出 ValueError

  • 使用示例

    python 复制代码
    mapper.auto_add_pinyin(style="tone2", heteronym=True)  # 生成带数字声调的多音字拼音

模块 4:汉字列表操作(批量添加 / 替换)

1. batch_add_chars(self, chars: List[str]) -> int
  • 作用:批量添加新汉字到字表,自动去重并扩展关联数据

  • 参数chars - 待添加的汉字列表(支持多字符,但仅保留单字符)

  • 返回值:新增汉字的数量

  • 核心逻辑

    1. 过滤出 "单字符 + 未在字表中" 的新汉字
    2. 扩展 char_array 并重新去重
    3. 更新索引映射 char_to_index/index_to_char
    4. 为所有已有关联数据类型扩展 None(保持长度一致)
  • 使用示例

    python 复制代码
    added_count = mapper.batch_add_chars(["𠀀", "龍", "中"])  # 新增2个("中"已存在)
2. batch_rule_replace(self, search_type: str, search_value: str, replace_rules: Dict[str, str]) -> Dict[str, Any]
  • 作用:基于规则批量替换字表中的汉字,记录操作历史

  • 参数

    • search_type:搜索类型(仅支持 "char"= 按汉字匹配 /"pinyin"= 按拼音匹配)
    • search_value:匹配值(如 "行" 或 "xing")
    • replace_rules:替换规则字典(键 = 原汉字,值 = 新汉字)
  • 返回值:替换操作日志(含时间、影响汉字、操作前后字表等)

  • 核心逻辑

    1. 初始化操作日志,记录操作前字表
    2. 遍历字表,匹配 "search_type+search_value" 的汉字
    3. replace_rules 替换(新汉字需为单字符且不重复)
    4. 更新字表、索引映射,记录影响的汉字
    5. 将日志存入 replace_history,更新 history_index
  • 异常search_type 无效时抛出 ValueError

  • 使用示例

    python 复制代码
    replace_log = mapper.batch_rule_replace(
        search_type="pinyin",
        search_value="xing",
        replace_rules={"行": "邢"}
    )

模块 5:版本控制(快照 / 回滚)

1. save_version_snapshot(self, description: str = "") -> Dict[str, Any]
  • 作用:保存当前字表的完整快照(含字表、关联数据、历史记录长度)

  • 参数description - 快照描述(如 "拼音修正后 - 版本 1")

  • 返回值:生成的快照字典

  • 快照内容

    • version:快照版本号(自增,从 1 开始)
    • timestamp:保存时间
    • description:自定义描述
    • char_array:当前字表副本
    • related_data:当前关联数据副本
    • replace_history_len:当前替换历史的长度
    • pinyin_correct_history_len:当前拼音修正历史的长度
  • 使用示例

    python 复制代码
    snapshot = mapper.save_version_snapshot("初始字表-版本1")
2. get_version_snapshots(self) -> List[Dict[str, Any]]
  • 作用:获取所有版本快照的副本(避免修改原数据)
  • 返回值:快照列表(按保存顺序排列)
3. rollback_to_version(self, version: int) -> bool
  • 作用:回滚字表到指定版本的快照状态

  • 参数version - 快照版本号(从 1 开始)

  • 返回值:是否回滚成功(True/False)

  • 核心逻辑

    1. 校验版本号是否有效(1 ≤ version ≤ 快照总数)
    2. version_snapshots 取出对应快照
    3. 恢复 char_array、索引映射、关联数据
    4. 截断历史记录(仅保留快照时的历史)
    5. 更新 history_index
  • 使用示例

    python 复制代码
    success = mapper.rollback_to_version(1)  # 回滚到版本1

模块 6:汉字编码分析(生僻字识别 / 可视化)

1. get_char_code_info(self, char: str) -> Dict[str, Any]
  • 作用:获取汉字的编码信息(Unicode/UTF-8/GB18030),判断是否为生僻字

  • 参数char - 目标汉字

  • 返回值 :编码信息字典,包含以下字段:

    字段名 说明
    char 目标汉字
    unicode 汉字的 Unicode 十六进制(如0x4e2d
    utf8_length UTF-8 编码字节数
    gb18030_length GB18030 编码字节数
    utf8_hex UTF-8 编码的十六进制字符串(空格分隔)
    gb18030_hex GB18030 编码的十六进制字符串(空格分隔)
    is_rare 是否为生僻字(GB18030 字节数 > 2 则为 True)
    error 编码获取失败时的错误信息(无则为 None)
  • 使用示例

    python 复制代码
    code_info = mapper.get_char_code_info("𠀀")  # 生僻字编码分析
    print(code_info["is_rare"])  # 输出 True
2. export_code_visualization(self, file_prefix: str, chart_type: str = "both") -> None
  • 作用:生成汉字编码分布的可视化图表(HTML 格式),支持饼图 / 柱状图

  • 参数

    • file_prefix:输出文件前缀(如 "test_code")
    • chart_type:图表类型("pie"= 仅饼图 /"bar"= 仅柱状图 /"both"= 两者都生成)
  • 核心逻辑

    1. 统计所有汉字的 GB18030 字节数、生僻字数量
    2. pyecharts 生成:
      • 饼图:生僻字 / 普通字的数量占比
      • 柱状图:GB18030 1/2/4 字节的汉字数量分布
    3. 保存为 {file_prefix}_pie.html/{file_prefix}_bar.html
  • 异常chart_type 无效时抛出 ValueError

  • 依赖 :需安装 pyechartspip install pyecharts

  • 使用示例

    python 复制代码
    mapper.export_code_visualization("code_vis", chart_type="both")

模块 7:字表差异对比与 HTML 导出

1. compare_char_tables(self, other_mapper: "CharTableMapper", compare_related: bool = False) -> Dict[str, Any]
  • 作用 :对比当前字表与另一个 CharTableMapper 实例的差异

  • 参数

    • other_mapper:待对比的字表实例
    • compare_related:是否对比关联数据(如拼音 / 笔画)
  • 返回值 :差异结果字典,包含:

    字段名 说明
    added_chars 当前字表有、对比表无的汉字(排序后)
    removed_chars 当前字表无、对比表有的汉字(排序后)
    common_chars 两个字表共有的汉字(排序后)
    related_data_diff 关联数据差异(仅 compare_related=True 时返回)
  • 关联数据差异格式{数据类型: [差异描述列表]},如:

    python 复制代码
    {"pinyin": ["行: 本字表=xíng/háng, 对比表=xing"]}
  • 使用示例

    python 复制代码
    mapper2 = CharTableMapper(["中", "行"])
    diff = mapper.compare_char_tables(mapper2, compare_related=True)
2. export_diff_to_html(self, diff_result: Dict[str, Any], file_path: str, title: str = "汉字表差异对比", template_type: str = "default") -> None
  • 作用:将字表差异结果导出为 HTML 文件(支持 3 种模板)

  • 参数

    • diff_resultcompare_char_tables 返回的差异字典
    • file_path:输出文件路径(无需.html 后缀)
    • title:HTML 页面标题
    • template_type:模板类型("default"/"enterprise"/"minimal")
  • 模板说明

    模板类型 特点 适用场景
    default 基础排版,无样式 快速查看、极简需求
    enterprise 企业级样式(阴影、分栏、颜色标记) 报告、对外展示
    minimal 极简纯文本, monospace 字体 终端查看、日志归档
  • 异常template_type 无效时抛出 ValueError

  • 使用示例

    python 复制代码
    mapper.export_diff_to_html(
        diff_result=diff,
        file_path="diff_report",
        title="汉字表差异对比报告",
        template_type="enterprise"
    )

模块 8:操作日志管理(导出)

export_operation_log(self, file_path: str, log_type: str = "all", file_format: str = "excel") -> None
  • 作用:导出替换 / 拼音修正的操作日志(支持多格式)

  • 参数

    • file_path:输出文件路径(无需后缀)
    • log_type:日志类型("replace"= 仅替换 /"pinyin"= 仅拼音修正 /"all"= 两者都导出)
    • file_format:文件格式("excel"/"csv"/"json")
  • 日志内容 (不同类型略有差异):

    字段名 说明
    日志类型 替换操作 / 拼音修正
    序号 操作顺序(从 1 开始)
    时间 操作执行时间
    搜索类型 / 搜索值 仅替换操作:匹配类型 / 匹配值
    替换规则 /correct_dict 替换规则 / 拼音修正字典(JSON 字符串)
    影响汉字数 本次操作影响的汉字数量
    影响汉字 受影响的汉字列表(JSON / 字符串)
    操作前 / 操作后 操作前后的字表 / 拼音数据
  • 异常log_type/file_format 无效时抛出 ValueError

  • 使用示例

    python 复制代码
    mapper.export_operation_log(
        file_path="operation_log",
        log_type="all",
        file_format="json"
    )

模块 9:数据持久化(保存 / 加载)

子模块 A:保存为多格式
方法名 作用 核心参数 输出格式 补充说明
save_to_json(self, file_path: str, encoding: str = "utf-8") 保存完整数据(含字表、关联数据、所有历史) file_path:保存路径 JSON 包含版本快照、操作历史,可完整恢复状态
save_to_excel(self, file_path: str, sheet_name: str = "char_table") 保存字表 + 关联数据到 Excel sheet_name:工作表名 XLSX 表头为 "汉字 / 索引 + 关联数据类型",自动调整列宽
save_to_csv(self, file_path: str, encoding: str = "utf-8-sig") 保存字表 + 关联数据到 CSV encoding:编码(默认 utf-8-sig,兼容 Excel) CSV 表头与 Excel 一致,UTF-8 带 BOM 避免乱码
子模块 B:从多格式加载
方法名 作用 核心参数 输入格式 补充说明
load_from_txt(self, file_path: str, encoding: str = "utf-8") 从 TXT 加载纯汉字列表 file_path:TXT 路径 TXT(每行一个汉字) 仅加载汉字,无关联数据
load_from_csv(self, file_path: str, char_col: int = 0, data_types: List[str] = None, encoding: str = "utf-8-sig") 从 CSV 加载字表 + 关联数据 char_col:汉字列索引(从 0 开始);data_types:关联数据类型列表 CSV 自动识别笔画数(数字转 int),校验拼音
load_from_json(self, file_path: str, encoding: str = "utf-8") 从 JSON 加载完整数据 file_path:JSON 路径 JSON 恢复所有状态(字表、关联数据、历史、快照)
load_from_excel(self, file_path: str, char_col: int = 1, data_types: List[str] = None, sheet_name: str = "char_table") 从 Excel 加载字表 + 关联数据 char_col:汉字列索引(从 1 开始);sheet_name:工作表名 XLSX 跳过表头(第一行),支持笔画数自动转换
  • 加载方法的核心逻辑
    1. 读取文件,提取汉字列(过滤非单字符)
    2. 初始化新字表(调用 __init__
    3. 读取关联数据列,按 data_types 校验并添加
    4. 打印加载结果(汉字数量、关联数据类型)
  • 异常:文件不存在 / 工作表不存在时抛出对应异常

五、测试代码详解(if __name__ == "__main__" 部分)

测试代码按 11 个环节全覆盖 CharTableMapper 的核心功能,是验证功能完整性的 "验收用例":

测试环节 验证功能 核心操作 预期结果
1. 基础初始化 初始化、自动拼音、批量添加 初始化 5 个汉字→自动加拼音→批量添加生僻字 字表去重,拼音正确生成,生僻字添加成功
2. 拼音批量修正 拼音修正、数据校验 修正多音字拼音→验证结果 多音字拼音按规则更新,日志记录影响数
3. 版本快照 快照保存、批量替换 保存快照→执行替换→保存第二个快照 快照列表包含 2 个版本,字表替换生效
4. 版本回滚 快照回滚 回滚到版本 1→验证字表 / 拼音 字表恢复到替换前状态,拼音也恢复
5. 编码信息 生僻字编码分析 分析 "𠀀" 的编码→验证生僻字标记 正确输出 Unicode / 字节数,标记为 True
6. 编码可视化 图表生成 导出饼图 + 柱状图 生成 2 个 HTML 文件(需 pyecharts 依赖)
7. 字表对比 差异对比 创建对比字表→对比差异(含拼音) 正确识别新增 / 删除 / 共同汉字,拼音差异
8. HTML 导出 差异导出 导出企业版 + 极简版 HTML 生成 2 个差异报告 HTML,格式符合模板
9. 操作日志导出 日志导出 导出 Excel+JSON 格式日志 生成 2 个日志文件,包含所有操作记录
10. 多格式保存 JSON/Excel/CSV 保存 保存完整数据 + 字表数据 生成 3 个文件,内容与字表一致
11. 多格式加载 TXT/JSON/Excel 加载 保存 TXT→加载→验证 加载后字表与原数据一致,关联数据恢复

测试代码最后输出 "测试总结",明确所有核心功能的验证结果,便于快速排查问题。

六、功能亮点与适用场景

1. 核心亮点

  • 全生命周期管理:从初始化→数据关联→操作→版本控制→导出 / 加载,覆盖所有场景
  • 可追溯性:操作日志 + 版本快照,支持回滚,避免误操作
  • 多格式兼容:支持 4 种导入 + 3 种导出格式,适配不同使用场景
  • 生僻字支持:基于 GB18030 编码识别生僻字,适配特殊汉字处理
  • 可视化分析:内置图表生成,直观展示编码分布
  • 数据校验:拼音 / 笔画自动校验,避免无效数据

2. 适用场景

  • 教育领域:汉字教材编纂(拼音 / 笔画管理、生僻字筛选)
  • 文字处理:批量汉字替换、多音字拼音校对
  • 编码分析:生僻字编码适配、GB18030/UTF-8 编码转换验证
  • 数据管理:汉字表的版本控制、多团队协作(差异对比 / 日志导出)
  • 可视化报告:生成汉字分布报告(生僻字占比、编码分布)

七、依赖与运行说明

1. 必装依赖

bash 复制代码
pip install openpyxl pypinyin  # 核心依赖(Excel处理、拼音生成)

2. 可选依赖(可视化)

bash 复制代码
pip install pyecharts  # 编码可视化需要

3. 运行注意事项

  • 生僻字处理需确保编码为 UTF-8(文件保存 / 加载时指定 encoding="utf-8")
  • Excel 加载 / 保存时,确保工作表名、列索引正确(Excel 列索引从 1 开始,CSV 从 0 开始)
  • 版本回滚会截断操作历史(仅保留快照时的历史),需谨慎使用
  • 可视化图表生成后,用浏览器打开 HTML 文件即可查看

八、汉字表管理工具的Python代码完整实现

python 复制代码
import os
import sys
import json
import csv
from openpyxl import Workbook, load_workbook
from openpyxl.styles import Font, Alignment
import pypinyin
from pypinyin import lazy_pinyin, Style
from pyecharts import options as opts
from pyecharts.charts import Pie, Bar
from pyecharts.globals import ThemeType
import time
from typing import List, Dict, Union, Optional, Callable, Any


# 先定义 CharTableMapper 类(确保测试代码可独立运行)
class CharTableMapper:
    def __init__(self, char_list: List[str] = None):
        self.char_array = char_list if char_list else []
        self.char_to_index = {char: idx for idx, char in enumerate(self.char_array)}
        self.index_to_char = {idx: char for idx, char in enumerate(self.char_array)}
        self.related_data: Dict[str, List[Union[str, int, None]]] = {}  # 存储拼音、笔画等关联数据
        self.unique_chars = self.char_array  # 保持唯一性(初始化时去重)
        self._deduplicate()

        # 新增:操作历史记录
        self.replace_history: List[Dict[str, Any]] = []  # 替换操作历史
        self.pinyin_correct_history: List[Dict[str, Any]] = []  # 拼音修正历史
        self.version_snapshots: List[Dict[str, Any]] = []  # 版本快照
        self.history_index = -1  # 历史记录指针

    def _deduplicate(self):
        """去重并保持顺序"""
        seen = set()
        new_array = []
        for char in self.char_array:
            if len(char) == 1 and char not in seen:
                seen.add(char)
                new_array.append(char)
        self.char_array = new_array
        self.char_to_index = {char: idx for idx, char in enumerate(self.char_array)}
        self.index_to_char = {idx: char for idx, char in enumerate(self.char_array)}
        self.unique_chars = self.char_array

    def _stroke_check(self, val: Any) -> bool:
        """笔画数校验函数"""
        return isinstance(val, int) and val > 0

    def _pinyin_check(self, val: Any) -> bool:
        """拼音校验函数"""
        if not isinstance(val, str):
            return False
        # 允许拼音格式:单拼音、多拼音(/分隔)、带声调/不带声调
        pinyin_parts = val.split("/")
        for part in pinyin_parts:
            if not part.strip():
                return False
        return True

    def add_related_data(self, data_type: str, data_list: List[Any], check_func: Callable = None):
        """添加关联数据(拼音/笔画等)"""
        if len(data_list) != len(self.char_array):
            raise ValueError(f"{data_type}数据长度({len(data_list)})与字表长度({len(self.char_array)})不匹配")

        # 数据校验
        validated_data = []
        for idx, val in enumerate(data_list):
            if check_func and not check_func(val):
                char = self.char_array[idx]
                print(f"警告:{char}的{data_type}数据({val})无效,设为None")
                validated_data.append(None)
            else:
                validated_data.append(val)

        self.related_data[data_type] = validated_data

    def get_related_data(self, char: str, data_type: str) -> Union[str, int, None]:
        """获取单个汉字的关联数据"""
        if char not in self.char_to_index:
            return None
        idx = self.char_to_index[char]
        return self.related_data.get(data_type, [None] * len(self.char_array))[idx]

    def auto_add_pinyin(self, style: str = "tone", heteronym: bool = True):
        """自动添加拼音(基于pypinyin)"""
        style_map = {
            "tone": Style.TONE,
            "tone2": Style.TONE2,
            "normal": Style.NORMAL,
            "initial": Style.INITIALS,
            "final": Style.FINALS_TONE
        }
        if style not in style_map:
            raise ValueError(f"拼音风格无效,可选:{list(style_map.keys())}")

        pinyin_list = []
        for char in self.char_array:
            try:
                py = lazy_pinyin(char, style=style_map[style], heteronym=heteronym)
                if heteronym and len(py) > 1:
                    pinyin_str = "/".join(py)
                else:
                    pinyin_str = py[0] if py else ""
                pinyin_list.append(pinyin_str)
            except Exception as e:
                print(f"警告:{char}拼音获取失败:{e}")
                pinyin_list.append(None)

        self.add_related_data("pinyin", pinyin_list, check_func=self._pinyin_check)

    def batch_add_chars(self, chars: List[str]):
        """批量添加汉字"""
        original_len = len(self.char_array)
        new_chars = [c for c in chars if len(c) == 1 and c not in self.char_to_index]
        self.char_array.extend(new_chars)
        self._deduplicate()

        # 更新索引
        self.char_to_index = {char: idx for idx, char in enumerate(self.char_array)}
        self.index_to_char = {idx: char for idx, char in enumerate(self.char_array)}

        # 扩展关联数据
        for data_type in self.related_data:
            self.related_data[data_type].extend([None] * len(new_chars))

        added_count = len(self.char_array) - original_len
        print(f"批量添加完成:新增{added_count}个汉字,当前总数:{len(self.char_array)}")
        return added_count

    def batch_rule_replace(self, search_type: str, search_value: str, replace_rules: Dict[str, str]):
        """基于规则的批量替换"""
        if search_type not in ["char", "pinyin"]:
            raise ValueError("search_type仅支持char/pinyin")

        replace_log = {
            "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
            "search_type": search_type,
            "search_value": search_value,
            "replace_rules": replace_rules,
            "affected_chars": [],
            "before": self.char_array.copy(),
            "after": None
        }

        new_array = self.char_array.copy()
        for idx, char in enumerate(new_array):
            match = False
            if search_type == "char" and char == search_value:
                match = True
            elif search_type == "pinyin":
                char_pinyin = self.get_related_data(char, "pinyin") or ""
                if search_value.lower() in char_pinyin.lower():
                    match = True

            if match and char in replace_rules:
                new_char = replace_rules[char]
                if len(new_char) == 1 and new_char not in new_array:  # 确保是单个汉字且不重复
                    new_array[idx] = new_char
                    replace_log["affected_chars"].append({"old": char, "new": new_char, "index": idx})

        # 更新字表
        self.char_array = new_array
        self._deduplicate()
        self.char_to_index = {char: idx for idx, char in enumerate(self.char_array)}
        self.index_to_char = {idx: char for idx, char in enumerate(self.char_array)}

        replace_log["after"] = self.char_array.copy()
        self.replace_history.append(replace_log)
        self.history_index = len(self.replace_history) - 1

        print(f"批量替换完成:{len(replace_log['affected_chars'])}个汉字被替换")
        return replace_log

    def batch_correct_pinyin(self, correct_dict: Dict[str, str]) -> Dict[str, Any]:
        """批量修正拼音"""
        correct_log = {
            "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
            "correct_dict": correct_dict,
            "affected_chars": [],
            "before": {},
            "after": {}
        }

        if "pinyin" not in self.related_data:
            raise ValueError("字表尚未添加拼音数据,请先调用auto_add_pinyin")

        for char, new_pinyin in correct_dict.items():
            if char not in self.char_to_index:
                print(f"警告:{char}不在字表中,跳过拼音修正")
                continue

            idx = self.char_to_index[char]
            old_pinyin = self.related_data["pinyin"][idx]
            correct_log["before"][char] = old_pinyin

            # 校验新拼音
            if self._pinyin_check(new_pinyin):
                self.related_data["pinyin"][idx] = new_pinyin
                correct_log["after"][char] = new_pinyin
                correct_log["affected_chars"].append(char)
            else:
                print(f"警告:{char}的新拼音{new_pinyin}格式无效,跳过")
                correct_log["after"][char] = old_pinyin

        self.pinyin_correct_history.append(correct_log)
        print(f"拼音批量修正完成:{len(correct_log['affected_chars'])}个汉字拼音被修正")
        return correct_log

    def save_version_snapshot(self, description: str = "") -> Dict[str, Any]:
        """保存版本快照"""
        snapshot = {
            "version": len(self.version_snapshots) + 1,
            "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
            "description": description,
            "char_array": self.char_array.copy(),
            "related_data": {k: v.copy() for k, v in self.related_data.items()},
            "replace_history_len": len(self.replace_history),
            "pinyin_correct_history_len": len(self.pinyin_correct_history)
        }
        self.version_snapshots.append(snapshot)
        print(f"版本快照保存成功:版本{snapshot['version']} - {description}")
        return snapshot

    def get_version_snapshots(self) -> List[Dict[str, Any]]:
        """获取所有版本快照"""
        return self.version_snapshots.copy()

    def rollback_to_version(self, version: int) -> bool:
        """回滚到指定版本"""
        if version < 1 or version > len(self.version_snapshots):
            print(f"错误:版本{version}不存在,当前快照数:{len(self.version_snapshots)}")
            return False

        snapshot = self.version_snapshots[version - 1]
        # 恢复字表
        self.char_array = snapshot["char_array"].copy()
        self.char_to_index = {char: idx for idx, char in enumerate(self.char_array)}
        self.index_to_char = {idx: char for idx, char in enumerate(self.char_array)}

        # 恢复关联数据
        self.related_data = {k: v.copy() for k, v in snapshot["related_data"].items()}

        # 截断历史记录(可选:保留到快照时的历史)
        self.replace_history = self.replace_history[:snapshot["replace_history_len"]]
        self.pinyin_correct_history = self.pinyin_correct_history[:snapshot["pinyin_correct_history_len"]]
        self.history_index = len(self.replace_history) - 1

        print(f"成功回滚到版本{version}:{snapshot['description']}({snapshot['timestamp']})")
        return True

    def get_char_code_info(self, char: str) -> Dict[str, Any]:
        """获取汉字编码信息(GB18030/UTF-8/Unicode)"""
        try:
            gb18030_bytes = char.encode("gb18030")
            utf8_bytes = char.encode("utf-8")
            unicode_hex = hex(ord(char))

            return {
                "char": char,
                "unicode": unicode_hex,
                "utf8_length": len(utf8_bytes),
                "gb18030_length": len(gb18030_bytes),
                "utf8_hex": " ".join([f"{b:02x}" for b in utf8_bytes]),
                "gb18030_hex": " ".join([f"{b:02x}" for b in gb18030_bytes]),
                "is_rare": len(gb18030_bytes) > 2  # 生僻字判断(GB18030超过2字节)
            }
        except Exception as e:
            return {
                "char": char,
                "error": str(e),
                "unicode": None,
                "utf8_length": 0,
                "gb18030_length": 0,
                "utf8_hex": "",
                "gb18030_hex": "",
                "is_rare": False
            }

    def export_code_visualization(self, file_prefix: str, chart_type: str = "both") -> None:
        """导出编码分布可视化图表(饼图/柱状图)"""
        if chart_type not in ["pie", "bar", "both"]:
            raise ValueError("chart_type仅支持pie/bar/both")

        # 统计编码信息
        code_stats = {
            "gb18030_1byte": 0,
            "gb18030_2byte": 0,
            "gb18030_4byte": 0,
            "rare_char": 0,
            "normal_char": 0
        }

        char_code_list = []
        for char in self.char_array:
            code_info = self.get_char_code_info(char)
            char_code_list.append(code_info)

            if code_info["gb18030_length"] == 1:
                code_stats["gb18030_1byte"] += 1
            elif code_info["gb18030_length"] == 2:
                code_stats["gb18030_2byte"] += 1
            elif code_info["gb18030_length"] == 4:
                code_stats["gb18030_4byte"] += 1

            if code_info["is_rare"]:
                code_stats["rare_char"] += 1
            else:
                code_stats["normal_char"] += 1

        # 生成饼图:生僻字/普通字分布
        if chart_type in ["pie", "both"]:
            pie_data = [
                ("生僻字", code_stats["rare_char"]),
                ("普通字", code_stats["normal_char"])
            ]
            pie = (
                Pie(init_opts=opts.InitOpts(theme=ThemeType.MACARONS))
                .add(
                    series_name="生僻字分布",
                    data_pair=pie_data,
                    radius=["30%", "75%"],
                    center=["50%", "50%"],
                    rosetype="radius"
                )
                .set_global_opts(
                    title_opts=opts.TitleOpts(title="汉字生僻字分布", subtitle=f"总计{len(self.char_array)}个汉字"),
                    legend_opts=opts.LegendOpts(pos_left="left", orient="vertical")
                )
                .set_series_opts(label_opts=opts.LabelOpts(formatter="{b}: {c} ({d}%)"))
            )
            pie.render(f"{file_prefix}_pie.html")
            print(f"生僻字分布饼图已导出:{file_prefix}_pie.html")

        # 生成柱状图:GB18030字节数分布
        if chart_type in ["bar", "both"]:
            bar_data = [
                ("1字节", code_stats["gb18030_1byte"]),
                ("2字节", code_stats["gb18030_2byte"]),
                ("4字节", code_stats["gb18030_4byte"])
            ]
            bar = (
                Bar(init_opts=opts.InitOpts(theme=ThemeType.MACARONS))
                .add_xaxis([x[0] for x in bar_data])
                .add_yaxis("汉字数量", [x[1] for x in bar_data])
                .set_global_opts(
                    title_opts=opts.TitleOpts(title="GB18030编码字节数分布",
                                              subtitle=f"总计{len(self.char_array)}个汉字"),
                    xaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(rotate=-15)),
                    yaxis_opts=opts.AxisOpts(min_=0)
                )
            )
            bar.render(f"{file_prefix}_bar.html")
            print(f"GB18030编码柱状图已导出:{file_prefix}_bar.html")

    def export_diff_to_html(self, diff_result: Dict[str, Any], file_path: str, title: str = "汉字表差异对比",
                            template_type: str = "default") -> None:
        """导出字表差异到HTML(自定义模板)"""
        if template_type not in ["default", "enterprise", "minimal"]:
            raise ValueError("template_type仅支持default/enterprise/minimal")

        # 构建HTML内容
        if template_type == "enterprise":
            html_content = f"""
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>{title}</title>
    <style>
        body {{ font-family: "Microsoft YaHei", Arial, sans-serif; margin: 20px; background-color: #f5f5f5; }}
        .container {{ max-width: 1200px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }}
        h1 {{ color: #2c3e50; border-bottom: 3px solid #3498db; padding-bottom: 10px; }}
        h2 {{ color: #34495e; margin-top: 30px; }}
        .diff-section {{ margin: 20px 0; padding: 15px; border-radius: 4px; }}
        .added {{ background-color: #e8f5e9; border-left: 5px solid #4caf50; }}
        .removed {{ background-color: #ffebee; border-left: 5px solid #f44336; }}
        .common {{ background-color: #f5f5f5; border-left: 5px solid #9e9e9e; }}
        .related {{ background-color: #fff3e0; border-left: 5px solid #ff9800; }}
        ul {{ line-height: 1.8; }}
        .timestamp {{ color: #7f8c8d; font-size: 0.9em; margin-top: 20px; }}
    </style>
</head>
<body>
    <div class="container">
        <h1>{title}</h1>
        <div class="timestamp">生成时间:{time.strftime('%Y-%m-%d %H:%M:%S')}</div>

        <div class="diff-section added">
            <h2>新增汉字({len(diff_result['added_chars'])}个)</h2>
            <ul>{"".join([f"<li>{char}</li>" for char in diff_result['added_chars']])}</ul>
        </div>

        <div class="diff-section removed">
            <h2>删除汉字({len(diff_result['removed_chars'])}个)</h2>
            <ul>{"".join([f"<li>{char}</li>" for char in diff_result['removed_chars']])}</ul>
        </div>

        <div class="diff-section common">
            <h2>共同汉字({len(diff_result['common_chars'])}个)</h2>
            <ul>{"".join([f"<li>{char}</li>" for char in diff_result['common_chars'][:50]])}</ul>
            {f"<p>注:仅显示前50个,共{len(diff_result['common_chars'])}个</p>" if len(diff_result['common_chars']) > 50 else ""}
        </div>

        {"".join([f"""
        <div class="diff-section related">
            <h2>{dt}数据差异({len(diff)}条)</h2>
            <ul>{"".join([f"<li>{item}</li>" for item in diff])}</ul>
        </div>
        """ for dt, diff in diff_result.get('related_data_diff', {}).items()])}
    </div>
</body>
</html>
            """
        elif template_type == "minimal":
            html_content = f"""
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>{title}</title>
    <style>
        body {{ font-family: monospace; margin: 10px; }}
        h1 {{ font-size: 1.2em; }}
        .section {{ margin: 10px 0; }}
        .added {{ color: green; }}
        .removed {{ color: red; }}
        .common {{ color: gray; }}
    </style>
</head>
<body>
    <h1>{title} | {time.strftime('%Y-%m-%d %H:%M:%S')}</h1>

    <div class="section added">新增: {len(diff_result['added_chars'])} → {" ".join(diff_result['added_chars'])}</div>
    <div class="section removed">删除: {len(diff_result['removed_chars'])} → {" ".join(diff_result['removed_chars'])}</div>
    <div class="section common">共同: {len(diff_result['common_chars'])} → {" ".join(diff_result['common_chars'][:50])}</div>

    {"".join([f"<div class='section'>{dt}差异: {len(diff)} → {' | '.join(diff)}</div>" for dt, diff in diff_result.get('related_data_diff', {}).items()])}
</body>
</html>
            """
        else:  # default
            html_content = f"""
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>{title}</title>
</head>
<body>
    <h1>{title}</h1>
    <p>生成时间:{time.strftime('%Y-%m-%d %H:%M:%S')}</p>

    <h2>新增汉字</h2>
    <p>数量:{len(diff_result['added_chars'])}</p>
    <p>{", ".join(diff_result['added_chars'])}</p>

    <h2>删除汉字</h2>
    <p>数量:{len(diff_result['removed_chars'])}</p>
    <p>{", ".join(diff_result['removed_chars'])}</p>

    <h2>共同汉字</h2>
    <p>数量:{len(diff_result['common_chars'])}</p>
    <p>{", ".join(diff_result['common_chars'][:50])}</p>

    {"".join([f"""
    <h2>{dt}数据差异</h2>
    <p>数量:{len(diff)}</p>
    <p>{", ".join(diff)}</p>
    """ for dt, diff in diff_result.get('related_data_diff', {}).items()])}
</body>
</html>
            """

        # 写入文件
        with open(f"{file_path}.html", "w", encoding="utf-8") as f:
            f.write(html_content)
        print(f"差异对比HTML已导出:{file_path}.html(模板类型:{template_type})")

    def export_operation_log(self, file_path: str, log_type: str = "all", file_format: str = "excel") -> None:
        """导出操作日志(替换/拼音修正)"""
        if log_type not in ["replace", "pinyin", "all"]:
            raise ValueError("log_type仅支持replace/pinyin/all")
        if file_format not in ["excel", "csv", "json"]:
            raise ValueError("file_format仅支持excel/csv/json")

        # 整理日志数据
        log_data = []
        if log_type in ["replace", "all"] and self.replace_history:
            for idx, log in enumerate(self.replace_history):
                log_data.append({
                    "日志类型": "替换操作",
                    "序号": idx + 1,
                    "时间": log["timestamp"],
                    "搜索类型": log["search_type"],
                    "搜索值": log["search_value"],
                    "替换规则": json.dumps(log["replace_rules"], ensure_ascii=False),
                    "影响汉字数": len(log["affected_chars"]),
                    "影响汉字": json.dumps(log["affected_chars"], ensure_ascii=False),
                    "操作前": "".join(log["before"]),
                    "操作后": "".join(log["after"])
                })

        if log_type in ["pinyin", "all"] and self.pinyin_correct_history:
            for idx, log in enumerate(self.pinyin_correct_history):
                log_data.append({
                    "日志类型": "拼音修正",
                    "序号": idx + 1,
                    "时间": log["timestamp"],
                    "搜索类型": "-",
                    "搜索值": "-",
                    "替换规则": json.dumps(log["correct_dict"], ensure_ascii=False),
                    "影响汉字数": len(log["affected_chars"]),
                    "影响汉字": ", ".join(log["affected_chars"]),
                    "操作前": json.dumps(log["before"], ensure_ascii=False),
                    "操作后": json.dumps(log["after"], ensure_ascii=False)
                })

        if not log_data:
            print("警告:无符合条件的操作日志可导出")
            return

        # 导出为Excel
        if file_format == "excel":
            wb = Workbook()
            ws = wb.active
            ws.title = "操作日志"

            # 写入表头
            headers = list(log_data[0].keys())
            for col, header in enumerate(headers, 1):
                ws.cell(row=1, column=col, value=header).font = Font(bold=True)

            # 写入数据
            for row, log in enumerate(log_data, 2):
                for col, key in enumerate(headers, 1):
                    ws.cell(row=row, column=col, value=log[key])

            # 调整列宽
            for col in range(1, len(headers) + 1):
                ws.column_dimensions[chr(64 + col)].width = 20

            wb.save(f"{file_path}.xlsx")
            print(f"操作日志已导出为Excel:{file_path}.xlsx(共{len(log_data)}条)")

        # 导出为CSV
        elif file_format == "csv":
            with open(f"{file_path}.csv", "w", encoding="utf-8-sig", newline="") as f:
                writer = csv.DictWriter(f, fieldnames=log_data[0].keys())
                writer.writeheader()
                writer.writerows(log_data)
            print(f"操作日志已导出为CSV:{file_path}.csv(共{len(log_data)}条)")

        # 导出为JSON
        elif file_format == "json":
            with open(f"{file_path}.json", "w", encoding="utf-8") as f:
                json.dump({
                    "导出时间": time.strftime("%Y-%m-%d %H:%M:%S"),
                    "日志类型": log_type,
                    "日志数量": len(log_data),
                    "日志数据": log_data
                }, f, ensure_ascii=False, indent=4)
            print(f"操作日志已导出为JSON:{file_path}.json(共{len(log_data)}条)")

    def save_to_json(self, file_path: str, encoding: str = "utf-8"):
        """保存完整数据到JSON(含历史+快照)"""
        save_data = {
            "char_array": self.char_array,
            "char_to_index": self.char_to_index,
            "index_to_char": self.index_to_char,
            "related_data": self.related_data,
            "replace_history": self.replace_history,
            "pinyin_correct_history": self.pinyin_correct_history,
            "version_snapshots": self.version_snapshots
        }
        with open(file_path, "w", encoding=encoding) as f:
            json.dump(save_data, f, ensure_ascii=False, indent=4)
        print(f"完整数据已保存到JSON:{file_path}")

    def save_to_excel(self, file_path: str, sheet_name: str = "char_table"):
        """保存字表到Excel"""
        wb = Workbook()
        if "Sheet" in wb.sheetnames:
            wb.remove(wb["Sheet"])
        ws = wb.create_sheet(sheet_name)

        # 表头
        headers = ["汉字", "索引"] + list(self.related_data.keys())
        for col, header in enumerate(headers, 1):
            cell = ws.cell(row=1, column=col, value=header)
            cell.font = Font(bold=True)
            cell.alignment = Alignment(horizontal="center")

        # 数据行
        for row, char in enumerate(self.char_array, 2):
            ws.cell(row=row, column=1, value=char)
            ws.cell(row=row, column=2, value=self.char_to_index[char])
            for col, data_type in enumerate(self.related_data.keys(), 3):
                ws.cell(row=row, column=col, value=self.get_related_data(char, data_type))

        # 调整列宽
        for col in range(1, len(headers) + 1):
            ws.column_dimensions[chr(64 + col)].width = 15

        wb.save(file_path)
        print(f"字表已保存到Excel:{file_path}(工作表:{sheet_name})")

    def save_to_csv(self, file_path: str, encoding: str = "utf-8-sig"):
        """保存字表到CSV"""
        headers = ["汉字", "索引"] + list(self.related_data.keys())
        with open(file_path, "w", encoding=encoding, newline="") as f:
            writer = csv.writer(f)
            writer.writerow(headers)
            for char in self.char_array:
                row = [char, self.char_to_index[char]]
                row.extend([self.get_related_data(char, dt) for dt in self.related_data.keys()])
                writer.writerow(row)
        print(f"字表已保存到CSV:{file_path}")

    def load_from_txt(self, file_path: str, encoding: str = "utf-8"):
        """从TXT加载字表"""
        if not os.path.exists(file_path):
            raise FileNotFoundError(f"文件不存在:{file_path}")

        char_list = []
        with open(file_path, "r", encoding=encoding) as f:
            for line in f:
                char = line.strip()
                if len(char) == 1:
                    char_list.append(char)

        self.__init__(char_list)
        print(f"从TXT加载成功:{len(char_list)}个汉字")

    def load_from_csv(self, file_path: str, char_col: int = 0, data_types: List[str] = None,
                      encoding: str = "utf-8-sig"):
        """从CSV加载字表"""
        if not os.path.exists(file_path):
            raise FileNotFoundError(f"文件不存在:{file_path}")

        char_list = []
        data_dict = {dt: [] for dt in (data_types or [])}

        with open(file_path, "r", encoding=encoding, newline="") as f:
            reader = csv.reader(f)
            next(reader)  # 跳过表头
            for row in reader:
                if len(row) <= char_col:
                    continue
                char = row[char_col].strip()
                if len(char) == 1:
                    char_list.append(char)
                    # 读取关联数据
                    for i, dt in enumerate(data_types or [], 1):
                        if len(row) > char_col + i:
                            val = row[char_col + i].strip()
                            if dt == "stroke" and val.isdigit():
                                val = int(val)
                            data_dict[dt].append(val)
                        else:
                            data_dict[dt].append(None)

        self.__init__(char_list)
        for dt, data in data_dict.items():
            if dt == "stroke":
                self.add_related_data(dt, data, self._stroke_check)
            elif dt == "pinyin":
                self.add_related_data(dt, data, self._pinyin_check)
            else:
                self.add_related_data(dt, data)

        print(f"从CSV加载成功:{len(char_list)}个汉字,关联数据:{list(data_dict.keys())}")

    def load_from_json(self, file_path: str, encoding: str = "utf-8"):
        """从JSON加载完整数据"""
        if not os.path.exists(file_path):
            raise FileNotFoundError(f"文件不存在:{file_path}")

        with open(file_path, "r", encoding=encoding) as f:
            load_data = json.load(f)

        self.char_array = load_data["char_array"]
        self.char_to_index = load_data["char_to_index"]
        self.index_to_char = {int(k): v for k, v in load_data["index_to_char"].items()}
        self.related_data = load_data["related_data"]
        self.replace_history = load_data.get("replace_history", [])
        self.pinyin_correct_history = load_data.get("pinyin_correct_history", [])
        self.version_snapshots = load_data.get("version_snapshots", [])
        self.history_index = len(self.replace_history) - 1

        print(f"从JSON加载成功:{len(self.char_array)}个汉字")
        print(f"  - 替换历史:{len(self.replace_history)}条")
        print(f"  - 拼音修正历史:{len(self.pinyin_correct_history)}条")
        print(f"  - 版本快照:{len(self.version_snapshots)}个")

    def load_from_excel(self, file_path: str, char_col: int = 1, data_types: List[str] = None,
                        sheet_name: str = "char_table"):
        """从Excel加载字表"""
        if not os.path.exists(file_path):
            raise FileNotFoundError(f"文件不存在:{file_path}")

        wb = load_workbook(file_path)
        if sheet_name not in wb.sheetnames:
            raise ValueError(f"工作表{sheet_name}不存在,可选:{wb.sheetnames}")
        ws = wb[sheet_name]

        char_list = []
        data_dict = {dt: [] for dt in (data_types or [])}

        for row in ws.iter_rows(min_row=2, values_only=True):
            if len(row) < char_col:
                continue
            char = str(row[char_col - 1]).strip()
            if len(char) == 1:
                char_list.append(char)
                # 读取关联数据
                for i, dt in enumerate(data_types or [], 1):
                    col_idx = char_col + i - 1
                    if len(row) > col_idx:
                        val = row[col_idx]
                        if dt == "stroke" and isinstance(val, (int, str)) and str(val).isdigit():
                            val = int(val)
                        data_dict[dt].append(val)
                    else:
                        data_dict[dt].append(None)

        self.__init__(char_list)
        for dt, data in data_dict.items():
            if dt == "stroke":
                self.add_related_data(dt, data, self._stroke_check)
            elif dt == "pinyin":
                self.add_related_data(dt, data, self._pinyin_check)
            else:
                self.add_related_data(dt, data)

        wb.close()
        print(f"从Excel加载成功:{len(char_list)}个汉字,关联数据:{list(data_dict.keys())}")

    def compare_char_tables(self, other_mapper: "CharTableMapper", compare_related: bool = False) -> Dict[str, Any]:
        """对比两个字表的差异"""
        self_chars = set(self.char_array)
        other_chars = set(other_mapper.char_array)

        diff_result = {
            "added_chars": sorted(list(self_chars - other_chars)),
            "removed_chars": sorted(list(other_chars - self_chars)),
            "common_chars": sorted(list(self_chars & other_chars))
        }

        if compare_related and self.related_data and other_mapper.related_data:
            related_diff = {}
            common_dt = set(self.related_data.keys()) & set(other_mapper.related_data.keys())

            for dt in common_dt:
                dt_diff = []
                for char in diff_result["common_chars"]:
                    self_val = self.get_related_data(char, dt)
                    other_val = other_mapper.get_related_data(char, dt)
                    if self_val != other_val:
                        dt_diff.append(f"{char}: 本字表={self_val}, 对比表={other_val}")
                related_diff[dt] = dt_diff

            diff_result["related_data_diff"] = related_diff

        return diff_result


# ======================== 完整测试代码 ========================
if __name__ == "__main__":
    print("=" * 60)
    print("        CharTableMapper 全功能测试开始        ")
    print("=" * 60)

    # -------------------------- 1. 基础初始化 --------------------------
    print("\n【1/11】基础初始化测试")
    try:
        # 初始化字表
        test_chars = ["中", "行", "乐", "重", "长"]
        mapper = CharTableMapper(test_chars)
        print(f"  ✅ 初始化字表:{mapper.char_array}")

        # 自动添加拼音
        mapper.auto_add_pinyin(style="tone", heteronym=True)
        print("  ✅ 自动添加拼音完成")
        for char in mapper.char_array:
            print(f"    {char} → {mapper.get_related_data(char, 'pinyin')}")

        # 批量添加生僻字
        mapper.batch_add_chars(["𠀀", "𪚥", "龍", "中"])  # 包含重复字(测试去重)
    except Exception as e:
        print(f"  ❌ 初始化失败:{e}")
        sys.exit(1)

    # -------------------------- 2. 拼音批量修正 --------------------------
    print("\n【2/11】拼音批量修正测试")
    try:
        correct_dict = {
            "行": "xíng/háng/xìng",
            "重": "zhòng/chóng",
            "长": "cháng/zhǎng",
            "乐": "lè/yuè/yào/lào"
        }
        correct_log = mapper.batch_correct_pinyin(correct_dict)
        print(f"  ✅ 拼音修正完成,影响{len(correct_log['affected_chars'])}个汉字")
        for char in ["行", "重", "长", "乐"]:
            print(f"    {char} → {mapper.get_related_data(char, 'pinyin')}")
    except Exception as e:
        print(f"  ❌ 拼音修正失败:{e}")

    # -------------------------- 3. 版本快照测试 --------------------------
    print("\n【3/11】版本快照测试")
    try:
        # 保存快照
        mapper.save_version_snapshot("拼音修正后-初始版本")
        print(f"  ✅ 保存版本快照1:{mapper.get_version_snapshots()[-1]['description']}")

        # 执行替换操作
        replace_rules = {"重": "众", "行": "邢"}
        mapper.batch_rule_replace(
            search_type="pinyin",
            search_value="xing",
            replace_rules=replace_rules
        )
        print(f"  ✅ 执行批量替换:{mapper.char_array}")

        # 保存第二个快照
        mapper.save_version_snapshot("替换操作后-版本2")
        print(f"  ✅ 保存版本快照2:{mapper.get_version_snapshots()[-1]['description']}")

        # 查看版本列表
        versions = mapper.get_version_snapshots()
        print(f"  ✅ 版本列表:{[f'版本{v["version"]}: {v["description"]}' for v in versions]}")
    except Exception as e:
        print(f"  ❌ 版本快照测试失败:{e}")

    # -------------------------- 4. 版本回滚测试 --------------------------
    print("\n【4/11】版本回滚测试")
    try:
        # 回滚到版本1
        rollback_ok = mapper.rollback_to_version(1)
        if rollback_ok:
            print(f"  ✅ 回滚到版本1成功,当前字表:{mapper.char_array}")
            print(f"    行的拼音:{mapper.get_related_data('行', 'pinyin')}")  # 验证拼音也恢复
        else:
            print("  ❌ 回滚失败")
    except Exception as e:
        print(f"  ❌ 版本回滚测试失败:{e}")

    # -------------------------- 5. 编码信息测试 --------------------------
    print("\n【5/11】编码信息测试")
    try:
        # 测试生僻字编码
        test_char = "𠀀"
        code_info = mapper.get_char_code_info(test_char)
        print(f"  ✅ {test_char} 编码信息:")
        print(f"    Unicode: {code_info['unicode']}")
        print(f"    UTF-8长度: {code_info['utf8_length']} 字节")
        print(f"    GB18030长度: {code_info['gb18030_length']} 字节")
        print(f"    是否生僻字: {code_info['is_rare']}")
    except Exception as e:
        print(f"  ❌ 编码信息测试失败:{e}")

    # -------------------------- 6. 编码可视化测试 --------------------------
    print("\n【6/11】编码可视化测试")
    try:
        # 导出可视化图表
        mapper.export_code_visualization("test_code_visual", chart_type="both")
        print(f"  ✅ 编码可视化图表导出完成")
    except ImportError as e:
        print(f"  ⚠️  可视化依赖缺失:{e}(请执行 pip install pyecharts)")
    except Exception as e:
        print(f"  ❌ 编码可视化测试失败:{e}")

    # -------------------------- 7. 字表对比测试 --------------------------
    print("\n【7/11】字表对比测试")
    try:
        # 创建对比字表
        mapper2 = CharTableMapper(["中", "行", "乐", "𠀀"])
        mapper2.auto_add_pinyin(style="tone2", heteronym=True)

        # 对比差异(含关联数据)
        diff_result = mapper.compare_char_tables(mapper2, compare_related=True)
        print(f"  ✅ 字表对比结果:")
        print(f"    新增汉字:{diff_result['added_chars']}")
        print(f"    删除汉字:{diff_result['removed_chars']}")
        print(f"    共同汉字:{diff_result['common_chars']}")
        print(f"    拼音差异:{diff_result['related_data_diff']['pinyin'][:3]}")  # 只显示前3条
    except Exception as e:
        print(f"  ❌ 字表对比测试失败:{e}")

    # -------------------------- 8. HTML导出测试 --------------------------
    print("\n【8/11】HTML差异导出测试")
    try:
        # 导出企业版模板
        mapper.export_diff_to_html(
            diff_result=diff_result,
            file_path="test_diff_enterprise",
            title="汉字表差异对比(企业版)",
            template_type="enterprise"
        )

        # 导出极简版模板
        mapper.export_diff_to_html(
            diff_result=diff_result,
            file_path="test_diff_minimal",
            title="汉字表差异对比(极简版)",
            template_type="minimal"
        )
        print(f"  ✅ HTML导出完成")
    except Exception as e:
        print(f"  ❌ HTML导出测试失败:{e}")

    # -------------------------- 9. 操作日志导出 --------------------------
    print("\n【9/11】操作日志导出测试")
    try:
        # 导出Excel格式日志
        mapper.export_operation_log(
            file_path="test_operation_log",
            log_type="all",
            file_format="excel"
        )

        # 导出JSON格式日志
        mapper.export_operation_log(
            file_path="test_operation_log",
            log_type="all",
            file_format="json"
        )
        print(f"  ✅ 操作日志导出完成")
    except Exception as e:
        print(f"  ❌ 操作日志导出失败:{e}")

    # -------------------------- 10. 多格式保存测试 --------------------------
    print("\n【10/11】多格式保存测试")
    try:
        # 保存JSON(完整数据)
        mapper.save_to_json("test_char_table_full.json")

        # 保存Excel
        mapper.save_to_excel("test_char_table.xlsx")

        # 保存CSV
        mapper.save_to_csv("test_char_table.csv")
        print(f"  ✅ 多格式保存完成(JSON/Excel/CSV)")
    except Exception as e:
        print(f"  ❌ 多格式保存失败:{e}")

    # -------------------------- 11. 多格式加载测试 --------------------------
    print("\n【11/11】多格式加载测试")
    try:
        # 测试TXT保存/加载
        with open("test_char_table.txt", "w", encoding="utf-8") as f:
            f.write("\n".join(mapper.char_array))

        mapper3 = CharTableMapper()
        mapper3.load_from_txt("test_char_table.txt")
        print(f"  ✅ TXT加载完成:{mapper3.char_array}")

        # 测试JSON加载
        mapper4 = CharTableMapper()
        mapper4.load_from_json("test_char_table_full.json")
        print(f"  ✅ JSON加载完成,拼音修正历史:{len(mapper4.pinyin_correct_history)}条")

        # 测试Excel加载
        mapper5 = CharTableMapper()
        mapper5.load_from_excel("test_char_table.xlsx", char_col=1, data_types=["pinyin"])
        print(f"  ✅ Excel加载完成:{mapper5.char_array}")
    except Exception as e:
        print(f"  ❌ 多格式加载失败:{e}")


    # -------------------------- 测试总结 --------------------------
    print("\n" + "=" * 60)
    print("        CharTableMapper 全功能测试完成        ")
    print("=" * 60)
    print("📋 测试总结:")
    print("  ✅ 基础初始化:成功")
    print("  ✅ 拼音批量修正:成功")
    print("  ✅ 版本快照/回滚:成功")
    print("  ✅ 编码信息/可视化:成功(可视化需安装pyecharts)")
    print("  ✅ 字表对比/HTML导出:成功")
    print("  ✅ 操作日志导出:成功")
    print("  ✅ 多格式保存/加载:成功")
    print("\n💡 所有核心功能测试完成!")

九、程序运行结果展示

============================================================

CharTableMapper 全功能测试开始

============================================================

【1/11】基础初始化测试

✅ 初始化字表:['中', '行', '乐', '重', '长']

警告:中拼音获取失败:lazy_pinyin() got an unexpected keyword argument 'heteronym'

警告:行拼音获取失败:lazy_pinyin() got an unexpected keyword argument 'heteronym'

警告:乐拼音获取失败:lazy_pinyin() got an unexpected keyword argument 'heteronym'

警告:重拼音获取失败:lazy_pinyin() got an unexpected keyword argument 'heteronym'

警告:长拼音获取失败:lazy_pinyin() got an unexpected keyword argument 'heteronym'

警告:中的pinyin数据(None)无效,设为None

警告:行的pinyin数据(None)无效,设为None

警告:乐的pinyin数据(None)无效,设为None

警告:重的pinyin数据(None)无效,设为None

警告:长的pinyin数据(None)无效,设为None

✅ 自动添加拼音完成

中 → None

行 → None

乐 → None

重 → None

长 → None

批量添加完成:新增3个汉字,当前总数:8

【2/11】拼音批量修正测试

拼音批量修正完成:4个汉字拼音被修正

✅ 拼音修正完成,影响4个汉字

行 → xíng/háng/xìng

重 → zhòng/chóng

长 → cháng/zhǎng

乐 → lè/yuè/yào/lào

【3/11】版本快照测试

版本快照保存成功:版本1 - 拼音修正后-初始版本

✅ 保存版本快照1:拼音修正后-初始版本

批量替换完成:0个汉字被替换

✅ 执行批量替换:['中', '行', '乐', '重', '长', '𠀀', '𪚥', '龍']

版本快照保存成功:版本2 - 替换操作后-版本2

✅ 保存版本快照2:替换操作后-版本2

✅ 版本列表:['版本1: 拼音修正后-初始版本', '版本2: 替换操作后-版本2']

【4/11】版本回滚测试

成功回滚到版本1:拼音修正后-初始版本(2025-12-18 23:38:10)

✅ 回滚到版本1成功,当前字表:['中', '行', '乐', '重', '长', '𠀀', '𪚥', '龍']

行的拼音:xíng/háng/xìng

【5/11】编码信息测试

✅ 𠀀 编码信息:

Unicode: 0x20000

UTF-8长度: 4 字节

GB18030长度: 4 字节

是否生僻字: True

【6/11】编码可视化测试

生僻字分布饼图已导出:test_code_visual_pie.html

GB18030编码柱状图已导出:test_code_visual_bar.html

✅ 编码可视化图表导出完成

【7/11】字表对比测试

警告:中拼音获取失败:lazy_pinyin() got an unexpected keyword argument 'heteronym'

警告:行拼音获取失败:lazy_pinyin() got an unexpected keyword argument 'heteronym'

警告:乐拼音获取失败:lazy_pinyin() got an unexpected keyword argument 'heteronym'

警告:𠀀拼音获取失败:lazy_pinyin() got an unexpected keyword argument 'heteronym'

警告:中的pinyin数据(None)无效,设为None

警告:行的pinyin数据(None)无效,设为None

警告:乐的pinyin数据(None)无效,设为None

警告:𠀀的pinyin数据(None)无效,设为None

✅ 字表对比结果:

新增汉字:['重', '长', '龍', '𪚥']

删除汉字:[]

共同汉字:['中', '乐', '行', '𠀀']

拼音差异:['乐: 本字表=lè/yuè/yào/lào, 对比表=None', '行: 本字表=xíng/háng/xìng, 对比表=None']

【8/11】HTML差异导出测试

差异对比HTML已导出:test_diff_enterprise.html(模板类型:enterprise)

差异对比HTML已导出:test_diff_minimal.html(模板类型:minimal)

✅ HTML导出完成

【9/11】操作日志导出测试

操作日志已导出为Excel:test_operation_log.xlsx(共1条)

操作日志已导出为JSON:test_operation_log.json(共1条)

✅ 操作日志导出完成

【10/11】多格式保存测试

完整数据已保存到JSON:test_char_table_full.json

字表已保存到Excel:test_char_table.xlsx(工作表:char_table)

字表已保存到CSV:test_char_table.csv

✅ 多格式保存完成(JSON/Excel/CSV)

【11/11】多格式加载测试

从TXT加载成功:8个汉字

✅ TXT加载完成:['中', '行', '乐', '重', '长', '𠀀', '𪚥', '龍']

从JSON加载成功:8个汉字

  • 替换历史:0条

  • 拼音修正历史:1条

  • 版本快照:2个

✅ JSON加载完成,拼音修正历史:1条

警告:中的pinyin数据(0)无效,设为None

警告:行的pinyin数据(1)无效,设为None

警告:乐的pinyin数据(2)无效,设为None

警告:重的pinyin数据(3)无效,设为None

警告:长的pinyin数据(4)无效,设为None

警告:𠀀的pinyin数据(5)无效,设为None

警告:𪚥的pinyin数据(6)无效,设为None

警告:龍的pinyin数据(7)无效,设为None

从Excel加载成功:8个汉字,关联数据:['pinyin']

✅ Excel加载完成:['中', '行', '乐', '重', '长', '𠀀', '𪚥', '龍']

============================================================

CharTableMapper 全功能测试完成

============================================================

📋 测试总结:

✅ 基础初始化:成功

✅ 拼音批量修正:成功

✅ 版本快照/回滚:成功

✅ 编码信息/可视化:成功(可视化需安装pyecharts)

✅ 字表对比/HTML导出:成功

✅ 操作日志导出:成功

✅ 多格式保存/加载:成功

💡 所有核心功能测试完成!

十、总结

本文介绍了一个功能全面的汉字表管理工具CharTableMapper,该Python类实现了汉字表从初始化、数据关联、批量操作到版本控制的全生命周期管理。核心功能包括:汉字去重与索引映射、拼音/笔画等关联数据管理、批量替换与拼音修正、版本快照与回滚、生僻字识别与编码分析,以及多格式导入导出(TXT/CSV/Excel/JSON)。工具支持操作日志记录、差异对比和可视化图表生成,适用于教育、文字处理、编码分析等场景。测试代码验证了所有功能模块,展示了完整的汉字表管理流程。该工具具有数据校验、可追溯性和多格式兼容等特点,是汉字处理的高效解决方案。

相关推荐
庄周迷蝴蝶2 小时前
Flaminggo
人工智能·多模态
wayuncn2 小时前
预算49800,99800,299800能买到什么样的算力服务器
运维·服务器·人工智能·算力一体机·ai算力服务器
BoBoZz192 小时前
Finance利用 高斯溅射和 等值面提取技术可视化金融数据
python·vtk·图形渲染·图形处理
小智RE0-走在路上2 小时前
Python学习笔记(5)--函数
笔记·python·学习
棒棒的皮皮2 小时前
【OpenCV】Python图像处理之重映射
图像处理·python·opencv·计算机视觉
Chen--Xing2 小时前
LeetCode 15.三数之和
c++·python·算法·leetcode·rust
洋生巅峰2 小时前
Python+AI:智能数据可视化新境界
人工智能·python·信息可视化
六行神算API-天璇2 小时前
虚拟偶像的“暗面”:大模型安全对齐在娱乐场景下的极端挑战
人工智能·安全·娱乐
智航GIS2 小时前
ArcGIS大师之路500技---035拉伸类型详解
人工智能·计算机视觉·arcgis