|
10楼#
发布于:2026-09-15 21:46
盘古大陆:使用输入法自带的合并用户码表,发现了以下两点不太符合我习惯的地方。拼音码表不要随便使用“码表合并”和“码表优化功能”。原因见:https://yong.dgod.net/read.php?tid=15&fid=7。 最好不要用合并用户码表和码表优化功能,因为相同编码的但实际不同的字词,如(昏暗、湖南)这样的会合并到一块,导致输入不正常。 拼音相同但实际读音不同的词不要放在一行(比如敏感和明暗)。 因为输入法为了加载速度,不会在同一行中去检测这个分词的问题。 若想优化需要自行解决拼音分词的问题,正确分词后再进行合并或优化就能完美解决问题了。这里提供一个分词的思路。前提先把编码转换为一个编码对应一个字符串的格式,也就是下面这样的形式: a 啊 a 阿 a 呵 a 腌 a 嗄 a 锕 a 吖 aba 阿坝 aba 阿爸 abao 阿宝 第二步:存储单个汉字对应的所有编码(如“重” 对应的编码“chong”“zhong”); 第三步:构建一个树形结构存储编码字符串数据,并在每个节点做以下判断。 ①、编码和对应的字符串是否已完全匹配,完全匹配表示成功输出对应的编码,每个编码之间可以用逗号进行分割; ②、记录字符串中第一个字符中所有能完全匹配编码开头字符的编码,生成下一级节点; ③、第 ② 步中若一个匹配的都没有,则返回上一个分岔处继续进行匹配; ④、所有的节点都无法匹配,表示匹配失败。原样输入编码和字符串。 |
|
|
11楼#
发布于:2026-09-17 09:49
|
|
|
12楼#
发布于:2026-09-18 00:07
pfox:楼主,能否把awk和im上传?我网上下载的awk加载第一个脚本运行出错,不知道是不是awk的问题。回到原帖脚本就是帖子里的内容,执行失败极有可能和文件的编码或换行符设置有关,请仔细检查。使用匹配的换行符,保持脚本和处理文件编码的一致 换行符小知识:换行符(linux/windows)与跨平台文件操作 一文搞懂编码Unicode、utf-8、ISO-8859-1、GBK、ASCII 、扩展ASCII码、GB2312等知识 |
|
|
13楼#
发布于:2026-09-18 00:14
非必要,不优化!
拼音码表优化实例 文件格式提示:优化时请确保码表文件、脚本文件的编码为utf8,使用的换行符为unix格式! 1、删除码表中表头(保存好,最后恢复),只保留数据部分。将以^开头的编码复制到新文件用于最后的合并; 2、修整码表,将码表中开头为格式为“{6}”这样格式的编码复制到新文件,用于最后的恢复使用;删除码表中每行开头部位非字母的部分,如“{6}”、“^”,方法如下: 2.1、在VIM中使用/^[^a-z]可以快速查找开头不是小写字母的行;
2.2、使用:%s/^{\d\+}//g可删除每行开头形式如{6}的部分。
2.3、使用:%s/^\^//g可删除每行开头的^符号。
3、使用下面的akw脚本(假设保存为了fen.ahk),将修整后的码表转换为一码一字词的形式。命令行这样写:awk -f fen.awk 修整后的码表文件.txt > 分开后的码表.txt {
key = $1 # 第一个字段是键
for (i = 2; i <= NF; i++) { # 遍历剩余字段(合并后的值)
print key, $i # 输出键 + 单个值
}
}
4、删除“分开后的码表.txt”文件中重复的行,在VIM中使用下面的脚本去重可以保持去重后各行的前后顺序不变。提取所有单个字的行到新文件,后续恢复码表使用。提取单个字的文件可以使用VIM命令:v/ .$/d删除所有词组行,保留单字即可。 function! Remove() " 不改变文件顺序删除重复靠后的行
let i=1|g/^/s//\=i.'|'/|let i+=1
sort! /^\d\{-}|/ " 将行号后面的内容进行倒序排列
g/^\d\{-}|\(.*\)$\n\d\{-}|\1$/d " 删除与下一行内容相同的行
sort n
%s/\d\{-}|//
endfunction
5、对去重后的“分开后的码表.txt”文件进行分词。分词后仔细查看文件exception.txt中分词失败的词条,人工校验。分词使用的源代码如下: /*
* 分词设计思路
* 1、拆字:将汉字词组拆分为单个汉字列表。
* 2、查码:针对每个汉字,在单字词典中查出其对应的所有编码。
* 3、剪枝匹配:从前向后逐个处理。关键优化是根据词组编码当前首字母,只尝试该汉字中以该字母开头的编码候选,过滤掉首字母不匹配的编码,大幅减少无效尝试。
* 4、输出:匹配成功则用 ' 连接各字对应编码输出;匹配失败(首字母对不上或长度不匹配)则原样输出原始词组编码。
*/
/*
* 词组编码智能分割工具 v3.8(二分查找版)
*
* 1. 读取完所有单字后,使用 qsort 对 dict 按汉字 UTF-8 字符串排序
* 2. 排序后立即 memset(used, 0, sizeof(used)),确保 used 下标与排序后的 dict 对应
* 3. 查找函数 find_dict 改用二分查找(bsearch)
* 4. 构建词典阶段使用线性查找(find_dict_linear),因为此时词典尚未排序
*
* 编译:gcc -o split_pinyin split_pinyin_v38.c
* 运行:split_pinyin data.txt output.txt exception.txt
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#define MAX_CHAR_WORDS 90000
#define PINYIN_MAX_LEN 50
#define PINYIN_PER_CHAR 10
#define PHRASE_MAX_LEN 200
#define MAX_PHRASES 200000
#define MAX_LINE_LEN 4096
/* ---------- 数据结构 ---------- */
typedef struct {
char chinese[5]; /* UTF-8 单字符 */
char pinyins[PINYIN_PER_CHAR][PINYIN_MAX_LEN];
int count;
} CharPinyin;
typedef struct {
char encoding[PINYIN_MAX_LEN * 10];
char chinese[PHRASE_MAX_LEN * 4];
} PhraseRecord;
CharPinyin dict[MAX_CHAR_WORDS];
int dict_count = 0;
int used[MAX_CHAR_WORDS];
PhraseRecord phrases[MAX_PHRASES];
int phrase_count = 0;
/* ---------- UTF-8 基础工具 ---------- */
int utf8_char_bytes(const unsigned char *s) {
unsigned char c = s[0];
if (c < 0x80) return 1;
if (c >= 0xC2 && c <= 0xDF) return 2;
if (c >= 0xE0 && c <= 0xEF) return 3;
if (c >= 0xF0 && c <= 0xF4) return 4;
return 0;
}
int utf8_char_count(const char *str) {
int count = 0;
const unsigned char *p = (const unsigned char*)str;
while (*p) {
int bytes = utf8_char_bytes(p);
if (bytes == 0 || bytes > (int)strlen((const char*)p)) break;
count++;
p += bytes;
}
return count;
}
int is_single_char(const char *str) {
if (!str || !*str) return 0;
return utf8_char_count(str) == 1;
}
int split_chinese(const char *chinese, char *chars[], int max_chars) {
int count = 0;
const unsigned char *p = (const unsigned char*)chinese;
while (*p && count < max_chars) {
int bytes = utf8_char_bytes(p);
if (bytes == 0 || bytes > (int)strlen((const char*)p)) break;
if (bytes == 1) {
if (!isspace(*p)) {
chars[count] = malloc(2);
chars[count][0] = *p;
chars[count][1] = '\0';
count++;
}
p++;
} else {
chars[count] = malloc(bytes + 1);
strncpy(chars[count], (const char*)p, bytes);
chars[count][bytes] = '\0';
count++;
p += bytes;
}
}
return count;
}
/* ---------- 词典操作 ---------- */
/* 线性查找(仅用于词典构建阶段) */
CharPinyin* find_dict_linear(const char *chinese) {
for (int i = 0; i < dict_count; i++) {
if (strcmp(dict<i>.chinese, chinese) == 0)
return &dict<i>;
}
return NULL;
}
/* qsort 比较函数 */
int cmp_charpinyin(const void *a, const void *b) {
const CharPinyin *pa = (const CharPinyin*)a;
const CharPinyin *pb = (const CharPinyin*)b;
return strcmp(pa->chinese, pb->chinese);
}
/* 排序并重置 used */
void sort_dict_and_reset_used(void) {
qsort(dict, dict_count, sizeof(CharPinyin), cmp_charpinyin);
memset(used, 0, sizeof(used)); /* 排序后 used 下标与 dict 对应 */
}
/* 二分查找(排序后使用) */
CharPinyin* find_dict(const char *chinese) {
return bsearch(chinese, dict, dict_count, sizeof(CharPinyin), cmp_charpinyin);
}
/* 添加词典条目(构建期使用线性查找) */
void add_to_dict(const char *chinese, const char *encoding) {
CharPinyin *cp = find_dict_linear(chinese);
if (cp != NULL) {
for (int i = 0; i < cp->count; i++)
if (strcmp(cp->pinyins<i>, encoding) == 0) return;
if (cp->count < PINYIN_PER_CHAR)
strcpy(cp->pinyins[cp->count++], encoding);
return;
}
if (dict_count >= MAX_CHAR_WORDS) return;
strcpy(dict[dict_count].chinese, chinese);
strcpy(dict[dict_count].pinyins[0], encoding);
dict[dict_count].count = 1;
dict_count++;
}
/* ---------- 文件解析 ---------- */
void parse_file(const char *filename) {
FILE *file = fopen(filename, "rb");
if (!file) { printf("错误:无法打开 %s\n", filename); exit(1); }
char line[MAX_LINE_LEN];
int first_line = 1;
while (fgets(line, sizeof(line), file)) {
size_t len = strlen(line);
while (len > 0 && (line[len-1] == '\n' || line[len-1] == '\r'))
line[--len] = '\0';
if (len == 0) continue;
if (first_line) {
unsigned char *p = (unsigned char*)line;
if (p[0] == 0xEF && p[1] == 0xBB && p[2] == 0xBF) {
memmove(line, line + 3, len - 3 + 1);
len -= 3;
}
first_line = 0;
}
if (len == 0) continue;
char *space = strchr(line, ' ');
if (!space) space = strchr(line, '\t');
if (!space) continue;
*space = '\0';
char *encoding = line;
char *chinese = space + 1;
while (*chinese == ' ' || *chinese == '\t') chinese++;
if (strlen(chinese) == 0) continue;
if (is_single_char(chinese)) {
add_to_dict(chinese, encoding);
} else {
if (phrase_count < MAX_PHRASES) {
strcpy(phrases[phrase_count].encoding, encoding);
strcpy(phrases[phrase_count].chinese, chinese);
phrase_count++;
}
}
}
fclose(file);
/* 单字收集完毕,排序并重置 used */
sort_dict_and_reset_used();
printf("词典:%d个单字,词组:%d条\n", dict_count, phrase_count);
}
/* ---------- 匹配算法(与 v3.6 相同) ---------- */
int match_phrase(const char *encoding, char *chars[], int char_count,
int idx, int pos, char result[][PINYIN_MAX_LEN])
{
int len = strlen(encoding);
if (idx == char_count) return (pos == len) ? 1 : 0;
if (pos >= len) return 0;
CharPinyin *cp = find_dict(chars[idx]);
if (!cp) return 0;
char first = encoding[pos];
for (int i = 0; i < cp->count; i++) {
const char *py = cp->pinyins<i>;
int py_len = strlen(py);
if (py[0] != first) continue;
if (pos + py_len > len) continue;
if (strncmp(encoding + pos, py, py_len) == 0) {
strcpy(result[idx], py);
if (match_phrase(encoding, chars, char_count, idx + 1, pos + py_len, result))
return 1;
}
}
return 0;
}
int match_phrase_brutal(const char *encoding, char *chars[], int char_count,
int idx, int pos, char result[][PINYIN_MAX_LEN])
{
int len = strlen(encoding);
if (idx == char_count) return (pos == len) ? 1 : 0;
if (pos >= len) return 0;
CharPinyin *cp = find_dict(chars[idx]);
if (!cp) return 0;
for (int i = 0; i < cp->count; i++) {
const char *py = cp->pinyins<i>;
int py_len = strlen(py);
if (pos + py_len > len) continue;
if (strncmp(encoding + pos, py, py_len) == 0) {
strcpy(result[idx], py);
if (match_phrase_brutal(encoding, chars, char_count, idx + 1, pos + py_len, result))
return 1;
}
}
return 0;
}
void process_phrase(FILE *out, FILE *exc, const char *encoding, const char *chinese) {
char *chars[PHRASE_MAX_LEN];
int count = split_chinese(chinese, chars, PHRASE_MAX_LEN);
if (count == 0) { fprintf(out, "%s %s\n", encoding, chinese); return; }
/* 检查缺字(二分查找) */
for (int i = 0; i < count; i++) {
if (!find_dict(chars<i>)) {
fprintf(exc, "%s %s\n", encoding, chinese);
fprintf(exc, " --> 缺字:'%s' 不在词典中\n", chars<i>);
for (int k = 0; k < count; k++) free(chars[k]);
return;
}
}
char result[PHRASE_MAX_LEN][PINYIN_MAX_LEN];
int ret = match_phrase(encoding, chars, count, 0, 0, result);
if (ret) {
for (int i = 0; i < count; i++) {
if (i > 0) fputc('\'', out);
fprintf(out, "%s", result<i>);
}
fprintf(out, " %s\n", chinese);
} else {
char brutal_result[PHRASE_MAX_LEN][PINYIN_MAX_LEN];
int bret = match_phrase_brutal(encoding, chars, count, 0, 0, brutal_result);
if (bret) {
int pos = 0;
for (int i = 0; i < count; i++) {
char expected = encoding[pos];
const char *py = brutal_result<i>;
int py_len = strlen(py);
if (py[0] != expected) {
fprintf(exc, "%s %s\n", encoding, chinese);
fprintf(exc, " --> 剪枝不匹配:第 %d 个字 '%s' 的正确编码为 '%s',其首字母 '%c' 不等于当前位置 %d 的期望首字母 '%c'。\n",
i + 1, chars<i>, py, py[0], pos, expected);
break;
}
pos += py_len;
}
} else {
fprintf(exc, "%s %s\n", encoding, chinese);
fprintf(exc, " --> 无匹配组合。\n");
fprintf(exc, " 词组拆分后各字及编码:\n");
for (int i = 0; i < count; i++) {
CharPinyin *cp = find_dict(chars<i>);
fprintf(exc, " '%s': ", chars<i>);
if (cp) {
for (int j = 0; j < cp->count; j++)
fprintf(exc, "%s ", cp->pinyins[j]);
} else {
fprintf(exc, "(无编码)");
}
fprintf(exc, "\n");
}
}
}
for (int i = 0; i < count; i++) free(chars<i>);
}
/* ---------- 主函数 ---------- */
int main(int argc, char *argv[]) {
const char *infile = "data.txt";
const char *outfile = "output.txt";
const char *excfile = "exception.txt";
if (argc >= 2) infile = argv[1];
if (argc >= 3) outfile = argv[2];
if (argc >= 4) excfile = argv[3];
printf("输入:%s\n输出:%s\n异常:%s\n\n", infile, outfile, excfile);
parse_file(infile);
if (dict_count == 0) {
printf("词典为空,请检查输入文件。\n");
return 1;
}
FILE *out = fopen(outfile, "w");
if (!out) { printf("无法创建 %s\n", outfile); return 1; }
FILE *exc = fopen(excfile, "w");
if (!exc) { printf("无法创建 %s\n", excfile); return 1; }
/* 处理词组,标记 used */
for (int i = 0; i < phrase_count; i++) {
char *chars[PHRASE_MAX_LEN];
int count = split_chinese(phrases<i>.chinese, chars, PHRASE_MAX_LEN);
for (int j = 0; j < count; j++) {
CharPinyin *cp = find_dict(chars[j]);
if (cp) {
int idx = (int)(cp - dict); /* 排序后下标映射到 used */
used[idx] = 1;
}
}
for (int j = 0; j < count; j++) free(chars[j]);
process_phrase(out, exc, phrases<i>.encoding, phrases<i>.chinese);
}
/* 输出未被词组使用的单字 */
for (int i = 0; i < dict_count; i++) {
if (!used<i>) {
for (int j = 0; j < dict<i>.count; j++)
fprintf(exc, "%s %s\n", dict<i>.pinyins[j], dict<i>.chinese);
}
}
fclose(out);
fclose(exc);
printf("\n处理完毕!共 %d 条词组。\n", phrase_count);
printf("正常结果 -> %s\n", outfile);
printf("异常数据 -> %s\n", excfile);
return 0;
}
6、对上一步加入分割符的词库使用下面的awk脚本(假设保存为了he.awk)进行并,命令行这样写:awk -f he.awk 已添加分隔符的词组文件.txt > 合并后的文件.txt;对提取的单个汉字文件进行合并,命令这样写:awk -f he.awk 一编码一汉字单字.txt > 一编码多汉字.txt BEGIN {
FS = "[ \t]+" # 字段间可能有多个空格或制表符
}
{
key = $1
value = ""
for (i = 2; i <= NF; i++) {
value = value (i == 2 ? "" : " ") $i
}
if (!(key in key_index)) {
# 第一次见到这个键,记录索引
key_index[key] = ++key_count
order[key_count] = key
# 初始化值(不加前导空格)
values[key] = value
} else {
# 追加值,确保格式整洁
values[key] = values[key] " " value
}
}
END {
# 按原始顺序输出
for (i = 1; i <= key_count; i++) {
key = order<i>
print key, values[key]
}
}7、删除“合并后的文件.txt”中的分隔符,使用第2步中复制出来的新文件恢复排序(…将码表中开头为格式为“{6}”这样格式的编码复制到新文件…)。将其与“一编码多汉字.txt”、第1步中保存的新文件进行合并。将合并后的文件进行排序,VIM的命令是:sort
8、添加码表表头,保存。完成优化码表的操作; |
|
|
14楼#
发布于:2026-09-18 17:16
看你们讨论合并用户词库,讨论了好多细节,我的个人意见是不要合并用户词库,保留主码表+用户词库+分词库的形式,首先有主码表,然后才有用户词库,这种情况下不会出现候选重复的,再加上分词库,只要分词库是单行单义的,即便与主码表有部分词语撞库了,小小也会自动过滤重复候选,候选框是不会出现重复候选的。分词库可以进行排序,加速程序读取,但是不要合并成单行多义的格式,看似缩小了码表行数整体文件体积变小了,但是与主码表、用户词库撞库后,出现重复候选就是不可避免地了。而且不合并用户词库也有利于对主码表的维护,合并后就彻底脱离了主码表的更新。
|
|
|
15楼#
发布于:2026-11-14 02:18
盘古大陆:非必要,不优化!简化过程,未去除结果中的分词符号,方便查看。处理文件需要文件编码转为utf-8,处理结束后再转回需要的编码。 /*
* 码表分词工具 v6.4(UTF-8 专用 + 缺字诊断 + 特殊前缀独立成行)
*
* 编译:gcc -O2 -o split_pinyin fenci5_0.c
* 运行:split_pinyin pinyin.txt out.txt [error.txt]
* 默认 输入 pinyin.txt,输出 pinyin_out.txt,异常 error.txt
*
* 输出示例:
* a 阿 啊 呵 <- 单字,原样
* ai'hao 爱好 哀号 <- 切分成功,插入 ',同码词合并到一行
* an'shi 按时 <- 切分成功
* xian 西安 <- 切分失败(该读音未收录),原样输出
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_LINE 8192 /* 码表单行最长 4094 字节,留足余量 */
#define MAX_CHARS 120000 /* 单字条目上限(支持 9 万字) */
#define MAX_PY_PER_CHAR 16 /* 每个字最多的读音数 */
#define MAX_CODE_LEN 64 /* 编码长度上限(码表约定 63) */
#define POOL_SIZE (1 << 21) /* 2MB */
#define HASH_SIZE 524287 /* 质数, 9 万字时负载因子 0.172 */
#define MAX_TOK 256 /* 单行最多字词数 */
#define MAX_SEG 300 /* 切分后编码串长度上限 */
/* ---------------- 字 -> 读音 字典 ---------------- */
typedef struct {
char ch[6]; /* 单个字符的原始字节 + NUL */
int off[MAX_PY_PER_CHAR]; /* 读音串在 pool 中的偏移 */
signed char n; /* 读音个数 */
signed char used; /* 是否已占用哈希槽 */
} CharEntry;
static CharEntry dict[MAX_CHARS];
static int dict_count = 0;
static char pool[POOL_SIZE];
static int pool_used = 0;
static int hash_tab[HASH_SIZE]; /* 存 dict 下标,-1 空 */
/* 容量不足导致的丢弃计数(只在结束时汇总报告一次,避免刷屏) */
static long drop_chars = 0; /* 因 MAX_CHARS 满被丢弃的字 */
static long drop_readings = 0; /* 因 pool 满/读音数满被丢弃的读音 */
static unsigned int hash_bytes(const char *s)
{
unsigned int h = 2166136261u; /* FNV-1a */
while (*s) {
h ^= (unsigned char)*s++;
h *= 16777619u;
}
return h % HASH_SIZE;
}
static void dict_init(void)
{
int i;
for (i = 0; i < HASH_SIZE; i++) hash_tab[i] = -1;
}
static int dict_find(const char *ch)
{
unsigned int h = hash_bytes(ch);
int probe;
for (probe = 0; probe < HASH_SIZE; probe++) {
int idx = hash_tab[(h + probe) % HASH_SIZE];
if (idx < 0) return -1;
if (strcmp(dict[idx].ch, ch) == 0) return idx;
}
return -1;
}
/* 追加一条 字->读音;已存在则忽略重复 */
static void dict_add(const char *ch, const char *code)
{
int idx = dict_find(ch);
unsigned int h;
int probe, i;
if (idx >= 0) { /* 字已存在:只可能追加读音 */
for (i = 0; i < dict[idx].n; i++)
if (strcmp(pool + dict[idx].off[i], code) == 0) return;
if (dict[idx].n >= MAX_PY_PER_CHAR) { /* 读音数到顶 */
drop_readings++;
return;
}
if (pool_used + (int)strlen(code) + 1 >= POOL_SIZE) { /* 池子满 */
drop_readings++;
return;
}
dict[idx].off[dict[idx].n] = pool_used;
strcpy(pool + pool_used, code);
pool_used += (int)strlen(code) + 1;
dict[idx].n++;
return;
}
/* 新字:三道闸门,任何一道过不去都要计数,绝不静默丢弃 */
if (dict_count >= MAX_CHARS) { drop_chars++; return; }
if (pool_used + (int)strlen(code) + 1 >= POOL_SIZE) { drop_chars++; return; }
if ((int)strlen(ch) + 1 > (int)sizeof(dict[0].ch)) { drop_chars++; return; }
idx = dict_count++;
strcpy(dict[idx].ch, ch);
dict[idx].n = 1;
dict[idx].used = 1;
dict[idx].off[0] = pool_used;
strcpy(pool + pool_used, code);
pool_used += (int)strlen(code) + 1;
h = hash_bytes(ch);
for (probe = 0; probe < HASH_SIZE; probe++) {
int p = (h + probe) % HASH_SIZE;
if (hash_tab[p] < 0) { hash_tab[p] = idx; break; }
}
}
/* ---------------- 字符/字节工具(GB18030 感知,不转换) ---------------- */
static long g_bad_bytes = 0; /* 非法 UTF-8 字节计数(仅告警) */
static int g_no_marker = 0; /* 1 = 文件里没有 [DATA] 行 */
/* 返回 p 处一个 UTF-8 字符占用的字节数;
* 严格按 RFC 3629:拒绝过长编码、代理区、超出 U+10FFFF 的序列。
* 非法序列按 1 字节处理,并累加警告计数。 */
/* 返回 p 处 UTF-8 序列长度;非法返回 0(纯判定,不计副作用) */
static int u8_seq_len(const unsigned char *p)
{
unsigned char c = p[0];
int n, i;
if (c < 0x80) return 1;
if (c >= 0xC2 && c <= 0xDF) n = 2;
else if (c >= 0xE0 && c <= 0xEF) n = 3;
else if (c >= 0xF0 && c <= 0xF4) n = 4;
else return 0;
for (i = 1; i < n; i++)
if ((p[i] & 0xC0) != 0x80) return 0;
if (n == 3) {
if (c == 0xE0 && p[1] < 0xA0) return 0;
if (c == 0xED && p[1] > 0x9F) return 0;
}
if (n == 4) {
if (c == 0xF0 && p[1] < 0x90) return 0;
if (c == 0xF4 && p[1] > 0x8F) return 0;
}
return n;
}
/* 解析用:非法序列按 1 字节跳过并计数 */
static int ch_len(const unsigned char *p)
{
int n = u8_seq_len(p);
if (n == 0) { g_bad_bytes++; return 1; }
return n;
}
/* 把字符串拆成"字"的指针与长度;返回个数,-1 表示含 ASCII */
static int split_chars(const char *s, char *ptrs[], int lens[], int maxn)
{
const unsigned char *p = (const unsigned char *)s;
int n = 0;
while (*p) {
int len = ch_len(p);
if (len == 1) return -1; /* 含单字节,不是纯字词 */
if (n >= maxn) return -1;
ptrs[n] = (char *)p;
lens[n] = len;
n++;
p += len;
}
return n;
}
/* 把 code 安全拷进结果缓冲 */
static void copy_code(char *dst, const char *code)
{
size_t n = strlen(code);
if (n > MAX_SEG - 1) n = MAX_SEG - 1;
memcpy(dst, code, n);
dst[n] = '\0';
}
/* 词里是否含真正的单字节字符(ASCII)。
* 注意:必须按"字符"走,不能按字节判 —— GBK/GB18030 的双字节汉字
* 尾字节可能是 0x40~0x7F(如 硚=B3 7E、赑=DA 50),按字节判会误伤。 */
static int tok_has_ascii(const char *t)
{
const unsigned char *p = (const unsigned char *)t;
while (*p) {
int l = ch_len(p);
if (l == 1) return 1; /* 真单字节:ASCII */
p += l;
}
return 0;
}
/* 数一个词里有几个字;含 ASCII 时返回 1(视作不可切分) */
static int count_chars(const char *s)
{
const unsigned char *p = (const unsigned char *)s;
int n = 0;
while (*p) {
int len = ch_len(p);
if (len == 1) return 1;
n++;
p += len;
}
return n;
}
/* 拷贝第 i 个字到 buf(NUL 结尾) */
static void copy_char(char *buf, const char *base, int len)
{
memcpy(buf, base, (size_t)len);
buf[len] = '\0';
}
/* ---------------- 首尾空格与行处理 ---------------- */
static int is_blank(char c) { return c == ' ' || c == '\t'; }
/* 去掉行尾 CR/LF,返回处理后的长度 */
static int strip_eol(char *line, int len)
{
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r'))
line[--len] = '\0';
return len;
}
/* 判断是否为 [DATA] 行(忽略大小写与首尾空白) */
static int is_data_marker(const char *s)
{
char buf[32];
int i = 0, j;
while (*s == ' ' || *s == '\t' || *s == '\r' || *s == '\n') s++;
while (*s && *s != ' ' && *s != '\t' && *s != '\r' && *s != '\n'
&& i < (int)sizeof(buf) - 1) {
char c = *s++;
if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
buf[i++] = c;
}
buf[i] = '\0';
for (j = i; j > 0; j--) { /* 尾部空白已排除 */
if (is_blank(buf[j - 1])) buf[j - 1] = '\0';
else break;
}
return strcmp(buf, "[data]") == 0;
}
/* 编码是否"可切分":纯小写字母(排除 ^x、{6}x、date/time 等特殊码) */
static int code_is_pure(const char *code)
{
if (!*code) return 0;
for (; *code; code++)
if (*code < 'a' || *code > 'z') return 0;
return 1;
}
/* 解析编码前缀,返回 1 表示 core 为纯小写字母(可切分):
* "abc" -> prefix="" core="abc"
* "^abc" -> prefix="^" core="abc" ^ 表示组词常用字
* "{5}abc"-> prefix="{5}" core="abc" {n} 表示候选排序位置
* 返回 0 表示该码是特殊码(date/time 等),整行原样处理
*/
static int code_split_prefix(const char *code, char *prefix, int psize,
const char **core)
{
const char *p = code;
int n = 0;
if (psize > 0) prefix[0] = '\0';
if (*p == '{') {
const char *q = p + 1;
if (*q < '0' || *q > '9') return 0;
while (*q >= '0' && *q <= '9') q++;
if (*q != '}') return 0;
n = (int)(q - p) + 1;
if (n >= psize) return 0;
memcpy(prefix, p, (size_t)n);
prefix[n] = '\0';
p = q + 1;
} else if (*p == '^') {
if (psize < 2) return 0;
prefix[0] = '^';
prefix[1] = '\0';
p++;
}
if (!code_is_pure(p)) return 0;
*core = p;
return 1;
}
/* 把 prefix 与切分结果拼成最终编码 */
static void join_code(char *dst, const char *prefix, const char *body)
{
size_t a = strlen(prefix);
size_t b = strlen(body);
if (a + b > (size_t)(MAX_SEG - 1)) b = (size_t)(MAX_SEG - 1) - a;
memcpy(dst, prefix, a);
memcpy(dst + a, body, b);
dst[a + b] = '\0';
}
/* ---------------- 切分:把 code 按每个字的读音切开 ---------------- */
typedef struct {
const char *code;
int code_len;
char *ptrs[MAX_TOK];
int lens[MAX_TOK];
int nch;
const char *pick[MAX_TOK]; /* 选中的读音串 */
int pick_i;
} SegCtx;
static int seg_dfs(SegCtx *c, int ci, int pos)
{
int idx, i;
if (ci == c->nch) return pos == c->code_len;
if (pos >= c->code_len) return 0;
{
char chbuf[6];
copy_char(chbuf, c->ptrs[ci], c->lens[ci]);
idx = dict_find(chbuf);
}
if (idx < 0) return 0;
for (i = 0; i < dict[idx].n; i++) {
const char *py = pool + dict[idx].off[i];
int py_len = (int)strlen(py);
if (py_len == 0 || py_len > c->code_len - pos) continue;
if (strncmp(c->code + pos, py, (size_t)py_len) != 0) continue;
c->pick[ci] = py;
if (seg_dfs(c, ci + 1, pos + py_len)) return 1;
}
return 0;
}
/*
* 尝试切分:成功返回 1 并把带 ' 的编码写入 out;失败返回 0
* order[] 用于优先尝试"常见读音",这里按字典收录顺序即可
*/
static int try_split(const char *code, const char *word, char *out)
{
SegCtx c;
int i, n;
char *p;
memset(&c, 0, sizeof(c));
c.code = code;
c.code_len = (int)strlen(code);
n = split_chars(word, c.ptrs, c.lens, MAX_TOK);
if (n < 2) return 0; /* 单字或含 ASCII:不切分 */
c.nch = n;
if (!seg_dfs(&c, 0, 0)) return 0;
p = out;
for (i = 0; i < n; i++) {
int py_len = (int)strlen(c.pick[i]);
if (i > 0) *p++ = '\'';
memcpy(p, c.pick[i], (size_t)py_len);
p += py_len;
}
*p = '\0';
return 1;
}
/* 是否存在 [DATA] 行 */
static int file_has_marker(const char *filename)
{
FILE *fp = fopen(filename, "rb");
char line[MAX_LINE];
int found = 0;
if (!fp) return 0;
while (fgets(line, sizeof(line), fp)) {
int len = (int)strlen(line);
while (len > 0 && (line[len-1] == '\n' || line[len-1] == '\r'))
line[--len] = '\0';
if (is_data_marker(line)) { found = 1; break; }
}
fclose(fp);
return found;
}
/* 整份文件校验 UTF-8;返回 0 表示非法(并打印位置),1 表示合格 */
static int check_file_utf8(const char *filename)
{
FILE *fp = fopen(filename, "rb");
unsigned char *buf;
long size, pos = 0, line = 1, col = 1;
long bad = 0, first_pos = -1, first_line = 0, first_col = 0;
if (!fp) { printf("ERROR: cannot open input file: %s\n", filename); return 0; }
if (fseek(fp, 0, SEEK_END) != 0) { fclose(fp); return 1; }
size = ftell(fp);
if (size < 0) { fclose(fp); return 1; }
rewind(fp);
buf = (unsigned char *)malloc((size_t)size + 1);
if (!buf) { fclose(fp); printf("WARN: cannot allocate %ld bytes for check.\n", size); return 1; }
if (fread(buf, 1, (size_t)size, fp) != (size_t)size) {
fclose(fp); free(buf);
printf("ERROR: read error on %s\n", filename);
return 0;
}
fclose(fp);
buf[size] = 0;
while (pos < size) {
int n = u8_seq_len(buf + pos);
if (n == 0) {
if (!bad) { first_pos = pos; first_line = line; first_col = col; }
bad++;
pos++;
col++;
continue;
}
if (buf[pos] == '\n') { line++; col = 1; }
else col++;
pos += n;
}
free(buf);
if (bad) {
printf("\nERROR: input file is NOT valid UTF-8.\n");
printf(" first bad byte at line %ld, column %ld (byte offset %ld)\n",
first_line, first_col, first_pos);
printf(" invalid byte sequences: %ld\n", bad);
printf(" please convert the file to UTF-8 (e.g. iconv -f gb18030 -t utf-8).\n");
printf(" no output file was created.\n");
return 0;
}
return 1;
}
/* ---------------- 第一遍:建字典 ---------------- */
static void build_dict(const char *filename)
{
FILE *fp = fopen(filename, "rb");
char line[MAX_LINE];
int in_data = g_no_marker ? 1 : 0;
int first = 1;
long chars_in_dict = 0;
if (!fp) {
printf("ERROR: cannot open input file: %s\n", filename);
exit(1);
}
while (fgets(line, sizeof(line), fp)) {
int len;
if (first) { /* 首行 BOM 不参与解析 */
first = 0;
if ((unsigned char)line[0] == 0xEF && (unsigned char)line[1] == 0xBB
&& (unsigned char)line[2] == 0xBF)
memmove(line, line + 3, strlen(line + 3) + 1);
}
len = strip_eol(line, (int)strlen(line));
if (!in_data) {
if (is_data_marker(line)) in_data = 1;
continue;
}
if (len == 0) continue;
if (line[0] == '#') continue;
{
char *p = line;
char *code = p;
char *tok;
char chbuf[6];
char prefix[16];
const char *core = NULL;
while (*p && !is_blank(*p)) p++;
if (!*p) continue; /* 没有字词字段 */
*p++ = '\0';
/* ^组词常用字 / {n}候选位置的编码:去掉前缀后再入字典 */
if (!code_split_prefix(code, prefix, (int)sizeof(prefix), &core))
continue;
while (*p) {
while (is_blank(*p)) p++;
if (!*p) break;
tok = p;
while (*p && !is_blank(*p)) p++;
if (*p) *p++ = '\0';
{
int L = (int)strlen(tok);
if (ch_len((const unsigned char *)tok) == L
&& (unsigned char)tok[0] >= 0x80) {
copy_char(chbuf, tok, L);
dict_add(chbuf, core);
chars_in_dict++;
}
}
}
}
}
fclose(fp);
printf("dict: %d chars, %ld readings\n", dict_count, chars_in_dict);
if (drop_chars || drop_readings) {
fprintf(stderr,
"WARN: capacity exceeded -- dropped %ld chars, %ld readings.\n"
" please raise MAX_CHARS / POOL_SIZE / MAX_PY_PER_CHAR.\n",
drop_chars, drop_readings);
}
}
/* 找出词里第一个字典中没有的字;找到返回 1 并把该字写入 out */
static int find_missing_char(const char *word, char *out, int outsz)
{
const unsigned char *p = (const unsigned char *)word;
while (*p) {
int len = ch_len(p);
char chbuf[8];
if (len <= 0 || len > 4) { p++; continue; }
copy_char(chbuf, (const char *)p, len);
if (dict_find(chbuf) < 0) {
if (len < outsz) { memcpy(out, chbuf, (size_t)len); out[len] = '\0'; }
else out[0] = '\0';
return 1;
}
p += len;
}
return 0;
}
/* ---------------- 输出模型:按最终编码跨行合并 ---------------- */
#define CMAP_SIZE (1 << 19) /* 编码 -> 记录 的下标表,2MB */
/* 1 = 切分失败的行也参与合并(其编码保持原样,仍与同码行合并)
* 0 = 切分失败的行整行原样输出,不参与合并 */
#define MERGE_UNSEGMENTED 1
typedef struct {
int is_raw; /* 1=原样行;0=可合并的编码行 */
char *code; /* is_raw=0:切分后的编码 */
char *raw; /* is_raw=1:整行原始字节 */
char **words; /* is_raw=0:该编码下的所有词 */
int nw, cap;
char eol[3];
int eollen;
} OutRec;
static OutRec *recs = NULL;
static int nrec = 0, caprec = 0;
static int cmap[CMAP_SIZE];
static long merged_lines = 0; /* 被并入别行的行数 */
static long unseg_lines = 0; /* 一个词都没切成功的行数 */
static long dedup_words = 0; /* 因重复被丢弃的词数 */
static void out_init(void)
{
int i;
for (i = 0; i < CMAP_SIZE; i++) cmap[i] = -1;
}
static char *xstrdup(const char *s)
{
size_t n = strlen(s) + 1;
char *p = (char *)malloc(n);
if (!p) { printf("FATAL: out of memory\n"); exit(2); }
memcpy(p, s, n);
return p;
}
static unsigned int str_hash(const char *s)
{
unsigned int h = 2166136261u;
while (*s) { h ^= (unsigned char)*s++; h *= 16777619u; }
return h;
}
static int cmap_lookup(const char *code)
{
unsigned int h = str_hash(code) & (CMAP_SIZE - 1);
int probe;
for (probe = 0; probe < CMAP_SIZE; probe++) {
int idx = cmap[(h + probe) & (CMAP_SIZE - 1)];
if (idx < 0) return -1;
if (strcmp(recs[idx].code, code) == 0) return idx;
}
return -1;
}
static int rec_new(void)
{
if (nrec >= caprec) {
int nc = caprec ? caprec * 2 : 4096;
OutRec *nr = (OutRec *)realloc(recs, (size_t)nc * sizeof(OutRec));
if (!nr) { printf("FATAL: out of memory\n"); exit(2); }
recs = nr;
caprec = nc;
}
memset(&recs[nrec], 0, sizeof(OutRec));
return nrec++;
}
static void rec_add_word(int i, const char *w)
{
int k;
for (k = 0; k < recs[i].nw; k++) /* 同码下重复的词只留一个 */
if (strcmp(recs[i].words[k], w) == 0) { dedup_words++; return; }
if (recs[i].nw >= recs[i].cap) {
int nc = recs[i].cap ? recs[i].cap * 2 : 4;
char **nw = (char **)realloc(recs[i].words, (size_t)nc * sizeof(char *));
if (!nw) { printf("FATAL: out of memory\n"); exit(2); }
recs[i].words = nw;
recs[i].cap = nc;
}
recs[i].words[recs[i].nw++] = xstrdup(w);
}
/* 安全追加:dst 容量 cap,*len 为当前长度 */
static void app(char *dst, size_t *len, const char *s, size_t cap)
{
size_t n = strlen(s);
if (*len + n >= cap) n = cap - 1 - *len;
memcpy(dst + *len, s, n);
*len += n;
dst[*len] = '\0';
}
/* 组内去重:保持首次出现顺序,返回去重后的个数 */
static int dedupe_group(char *grp[], int g)
{
int i, k, n = 0;
for (i = 0; i < g; i++) {
int dup = 0;
for (k = 0; k < n; k++)
if (strcmp(grp[k], grp[i]) == 0) { dup = 1; break; }
if (dup) { dedup_words++; continue; }
grp[n++] = grp[i];
}
return n;
}
/* 原样行(不参与合并) */
static void emit_raw(const char *line, int len, const char *eol, int eol_len)
{
int i = rec_new();
recs[i].is_raw = 1;
recs[i].raw = (char *)malloc((size_t)len + 1);
if (!recs[i].raw) { printf("FATAL: out of memory\n"); exit(2); }
memcpy(recs[i].raw, line, (size_t)len);
recs[i].raw[len] = '\0';
recs[i].eollen = eol_len;
if (eol_len) memcpy(recs[i].eol, eol, (size_t)eol_len);
}
/* 编码行:同码则并到已有记录,否则新建 */
static void emit_code(const char *code, char *toks[], int ntok,
const char *eol, int eol_len)
{
int idx = cmap_lookup(code);
int k, probe;
unsigned int h;
if (idx >= 0) { /* 已有同码:追加词 */
for (k = 0; k < ntok; k++) rec_add_word(idx, toks[k]);
merged_lines++;
return;
}
idx = rec_new();
recs[idx].is_raw = 0;
recs[idx].code = xstrdup(code);
recs[idx].eollen = eol_len;
if (eol_len) memcpy(recs[idx].eol, eol, (size_t)eol_len);
for (k = 0; k < ntok; k++) rec_add_word(idx, toks[k]);
h = str_hash(code) & (CMAP_SIZE - 1);
for (probe = 0; probe < CMAP_SIZE; probe++) {
int p = (h + probe) & (CMAP_SIZE - 1);
if (cmap[p] < 0) { cmap[p] = idx; break; }
}
}
/* 全部处理完后统一落盘 */
static void out_flush(FILE *out)
{
int i, k;
for (i = 0; i < nrec; i++) {
if (recs[i].is_raw) {
fputs(recs[i].raw, out);
} else {
fputs(recs[i].code, out);
for (k = 0; k < recs[i].nw; k++) {
fputc(' ', out);
fputs(recs[i].words[k], out);
}
}
if (recs[i].eollen) fwrite(recs[i].eol, 1, (size_t)recs[i].eollen, out);
}
}
/* ---------------- 第二遍:原样复制表头 + 处理数据行 ---------------- */
static void process_line(FILE *err, char *line, int len,
const char *eol, int eol_len)
{
char buf[MAX_LINE];
char results[MAX_TOK][MAX_SEG];
char *toks[MAX_TOK];
int ntok = 0;
int used[MAX_TOK];
int i, j, changed = 0, mergeable = 1, keep_lines = 0;
char *p;
char *code;
char prefix[16];
char body[MAX_SEG];
const char *core = NULL;
memcpy(buf, line, (size_t)len);
buf[len] = '\0';
if (len == 0 || buf[0] == '#') { /* 空行、注释:原样 */
emit_raw(line, len, eol, eol_len);
return;
}
code = buf;
p = buf;
while (*p && !is_blank(*p)) p++;
if (!*p) { /* 只有编码 */
emit_raw(line, len, eol, eol_len);
return;
}
*p++ = '\0';
if (!code_split_prefix(code, prefix, (int)sizeof(prefix), &core)) {
emit_raw(line, len, eol, eol_len); /* 特殊码(date/time):原样 */
return;
}
if (prefix[0] != '\0') { /* ^组词常用字 / {n}候选位置 */
mergeable = 0; /* 不参与跨行合并 */
keep_lines = 1; /* 但保留行内去重 */
}
/* 切分每个字词 */
while (*p) {
while (is_blank(*p)) p++;
if (!*p) break;
if (ntok >= MAX_TOK) { /* 超长行:整体原样 */
emit_raw(line, len, eol, eol_len);
return;
}
toks[ntok] = p;
while (*p && !is_blank(*p)) p++;
if (*p) *p++ = '\0';
if (tok_has_ascii(toks[ntok])) /* 含 ASCII(date/time 模板等) */
mergeable = 0; /* 只降级为"本行不合并",不影响其他词 */
if (count_chars(toks[ntok]) < 2) { /* 单字:无需切分 */
copy_code(results[ntok], code);
} else if (try_split(core, toks[ntok], body)) {
join_code(results[ntok], prefix, body); /* 前缀原样保留 */
changed = 1;
} else {
char miss[8];
copy_code(results[ntok], code);
if (err) {
if (find_missing_char(toks[ntok], miss, (int)sizeof(miss)))
fprintf(err, "%s %s [缺字: %s]\n", code, toks[ntok], miss);
else
fprintf(err, "%s %s [读音组合不上]\n", code, toks[ntok]);
}
}
ntok++;
}
if (ntok == 0) {
emit_raw(line, len, eol, eol_len);
return;
}
#if MERGE_UNSEGMENTED
if (!changed) unseg_lines++; /* 全失败:编码保持原样,照样参与合并 */
#else
if (!changed) { /* 全部切分失败:完全原样 */
emit_raw(line, len, eol, eol_len);
return;
}
#endif
/* 行内先按切分结果分组;可合并的行交给输出模型,不可合并的行直接落盘 */
for (i = 0; i < ntok; i++) used[i] = 0;
for (i = 0; i < ntok; i++) {
char *grp[MAX_TOK];
char tmp[MAX_LINE];
size_t tl = 0;
int g = 0;
if (used[i]) continue;
for (j = i; j < ntok; j++) {
if (used[j] || strcmp(results[i], results[j]) != 0) continue;
used[j] = 1;
grp[g++] = toks[j];
}
if (mergeable) {
emit_code(results[i], grp, g, eol, eol_len);
} else {
if (keep_lines) g = dedupe_group(grp, g); /* ^/{n}:行内去重 */
/* 与 v5.2 行为一致:这些词照样切分,但本行不参与跨行合并 */
int k;
tmp[0] = '\0';
app(tmp, &tl, results[i], sizeof(tmp));
for (k = 0; k < g; k++) {
app(tmp, &tl, " ", sizeof(tmp));
app(tmp, &tl, grp[k], sizeof(tmp));
}
emit_raw(tmp, (int)tl, eol, eol_len);
}
}
}
static void process_file(const char *infile, const char *outfile,
const char *errfile)
{
FILE *fp = fopen(infile, "rb");
FILE *out;
FILE *err = NULL;
char line[MAX_LINE];
int in_data = g_no_marker ? 1 : 0;
int first_line = 1;
long nline = 0;
if (!fp) {
printf("ERROR: cannot open input file: %s\n", infile);
exit(1);
}
out = fopen(outfile, "wb");
if (!out) {
printf("ERROR: cannot create output file: %s\n", outfile);
fclose(fp);
exit(1);
}
if (errfile && *errfile) {
err = fopen(errfile, "wb");
if (!err) printf("WARN: cannot create error file: %s\n", errfile);
}
while (fgets(line, sizeof(line), fp)) {
if (first_line) {
first_line = 0;
if ((unsigned char)line[0] == 0xEF && (unsigned char)line[1] == 0xBB
&& (unsigned char)line[2] == 0xBF && in_data) {
fwrite(line, 1, 3, out); /* BOM 单独保留 */
memmove(line, line + 3, strlen(line + 3) + 1);
}
}
if (!in_data) {
fwrite(line, 1, strlen(line), out); /* 表头:逐字节原样 */
if (is_data_marker(line)) in_data = 1;
continue;
}
{
int raw = (int)strlen(line);
int eol_len = 0;
char eol[3];
if (raw >= 2 && line[raw - 2] == '\r' && line[raw - 1] == '\n') {
eol[0] = '\r'; eol[1] = '\n'; eol_len = 2;
} else if (raw >= 1 && (line[raw - 1] == '\n' || line[raw - 1] == '\r')) {
eol[0] = line[raw - 1]; eol_len = 1;
}
strip_eol(line, raw);
nline++;
process_line(err, line, (int)strlen(line), eol, eol_len);
}
}
fclose(fp);
out_flush(out); /* 统一落盘:合并在这里生效 */
fclose(out);
if (err) fclose(err);
printf("data lines: %ld\n", nline);
printf("out lines : %d (merged %ld, dedup words %ld, unsegmented %ld)\n",
nrec, merged_lines, dedup_words, unseg_lines);
}
int main(int argc, char *argv[])
{
const char *infile = "pinyin.txt";
const char *outfile = "pinyin_out.txt";
const char *errfile = "error.txt";
int i, npos = 0;
for (i = 1; i < argc; i++) {
if (!strcmp(argv[i], "-e") || !strncmp(argv[i], "-e=", 3)
|| !strcmp(argv[i], "--encoding")) {
printf("note: v6.x only supports UTF-8; -e is no longer needed.\n");
continue;
}
if (argv[i][0] == '-' && argv[i][1]) {
printf("usage: split_pinyin [in.txt] [out.txt] [error.txt]\n");
return 1;
}
switch (npos++) {
case 0: infile = argv[i]; break;
case 1: outfile = argv[i]; break;
case 2: errfile = argv[i]; break;
default: break;
}
}
printf("input : %s\noutput: %s\nerror : %s\n\n", infile, outfile, errfile);
if (!check_file_utf8(infile)) /* 非 UTF-8:只提示,不产出任何文件 */
return 1;
g_no_marker = file_has_marker(infile) ? 0 : 1;
if (g_no_marker)
printf("note: no [DATA] marker found; the whole file is treated as data.\n");
dict_init();
out_init();
build_dict(infile);
if (dict_count == 0) { /* 无单字条目:整体原样复制 */
printf("WARN: dictionary is empty (no [DATA] single-char entries).\n");
printf(" output = input (verbatim).\n");
}
process_file(infile, outfile, errfile);
printf("done.\n");
return 0;
} |
|
上一页
下一页