【汉字】转【pīnyīn】

引言

github地址:aizuyan/pinyin

无意中看到了overtrue/pinyin这个项目,感觉很有意思,
这个项目做了这么一件事情:

将汉字转化为拼音

刚看到这里是不是觉得没什么难度,没什么意思?您不妨接着往下看。要是只是将汉字转为拼音好像
很容易就实现了,但是要是给转换之后的汉字带上音调呢,这样难度就很大了,因为汉字博大精深,
其中一方面就表现在多音字,同样一个字在不同的语句场景下,音调是不一样的。看到这里你在考
虑下如何处理?

这里我还是很佩服安正超(要是不了解他,不妨点进去看看,他
有好几个开源项目都非常棒)这位大神的,他的思路让我眼前一亮:

替换的时候首先将常用的词组替换了,比如短语、成语、常用的词语,这些词语替换的时候按照常
用程度由高到低排序。

接着对剩余的未被替换的汉字进行替换,这里直接按照所有汉字和拼音的映射,没有特定顺序。

如果是姓名,首先对姓氏进行一遍特定替换,姓氏的声调可能和常用的不一致。

除了项目的想法很棒之外,还有一个大难题要解决,就是收集词语,姓氏等,这里我直接使用了安正 超的项目中的数据,再次也该写下安前辈

我的这个项目可以说是很大程度上是安前辈overtrue/pinyin项目的另一个版本。

设计

这个项目中会用到一个数据结构,单链表,用来存储拼音、汉字对应关系的数据,一开始想着使用Bu cket数据结构,后来觉得,这个数据结构体里面冗余的信息太多了,也不是很符合我预想的数据结构,
so,最后使用了下面的数据结构:

typedef struct mylist {
    char *key;  //词语、成语或者单个汉字
    char *val;  //对应的拼音,拼音前面都有一个制表符`\t`
    struct mylist *next;    //指向下一个汉字拼音结构体
} MyList;

除此之外,我还考虑了性能问题,安前辈php版本的有个固有的缺陷,每次请求都要去加载一遍数据文
件,大概有600700kb左右,转换为数组,元素个数为4000050000个左右,每次请求都会分配、释放这部分内
存。而且这个过程会有大量的计算过程(查找、替换),这也是php很不擅长的,如果用c语言会好很多。

因此,我就使用了PHP扩展,在模块初始化的时候,将所有配置数据载入内存,如果是fast-cgi模式,
不用每次请求都加载一遍配置数据,只在进程启动的时候加载一遍。计算的话没有找到php里面比较合适的
函数,字节写了查找替换的函数。

还有就是如何读取配置文件数据了,我采用了下面的数据格式存储每一个汉-拼对“,csv个格式,每一行
第一列是短语、词语或者汉字,第二列是拼音,每个拼音之间使用制表符\t分割,这样读取、进一步处理
就很方便了

汉字,   han zi
......
{汉字|词语|短语},   pin yin

实现

实现部分,挑几个主要的函数出来:
首先是给链表中添加汉字拼音结构体的函数,这里有个地方要注意,这里使用了c语言原声的malloc
strdup,这是因为这个变量是全局的,不会随着请求的结束而销毁,而且也不会区分线程,因为所有的
线程都只会读取变量中的内容,所有的线程共享一套变量就可以了。

MyList *pinyin_list_append(MyList *last, const char *key, const char *value)
{
    MyList *element = (MyList *)malloc(sizeof(MyList));
    char *newKey = strdup(key);
    char *newVal = strdup(value);
    element->key = newKey;
    element->val = newVal;
    element->next = NULL;
    last->next = element;

    return element;
}

下面这个函数是从一行通过逗号分隔的字符串中取出逗号前面的部分作为汉字部分。

const char *get_key_from_line(const char *line, char *ret)
{   
    int i = 0;
    while(*line)
    {   
        if(*line != ',')
        {   
            ret[i] = *line;
        }else {
            break;
        }
        i++;
        line++;
    }
    ret[i] = '\0';
    return ret;
}

下面是同一行中分离出拼音部分:

const char *get_val_from_line(const char *line, char *ret)
{   
    int i = 0; 
    int flag = 0;
    while(*line)
    {   
        if(*line == '\n')
        {
            break;
        }
        if(*line == ',')
        {   
            flag = 1;
            line++;
            continue;
        }else if(!flag) {
            line++;
            continue;
        }
        ret[i] = *line;
        i++;
        line++;
    }
    ret[i] = '\0';
    return ret;
}

下面是最重要的一个,替换字符换函数,from是要替换的字符串,to是要替换为的字符串,
str是原始字符串,ret是临时字符串,会保存临时的结果,is_name表示是否是姓名,
如果是姓名,只替换一次。

void str_replace(const char *from, const char *to, char *str, char *ret, zend_bool is_name)
{
    int pos = 0,
        fromLen = strlen(from),
        flag = 0;
    char *tmp = NULL,
         *strTmp = str;
    while(tmp = strstr(str, from))
    {   
        pos = tmp - str;
        strncat(ret, str, pos);
        strcat(ret, to);
        str = tmp + fromLen;
        flag = 1;
        if(is_name)
            break;
    }   

    strcat(ret, str);
    if(1 == flag)
    {
        memcpy(strTmp, ret, strlen(ret));
        strTmp[strlen(ret)] = '\0';
    }
}

使用

只通过一个函数和标志位来实现,使用起来也是很方便的:
使用的时候可以参考github中的README.md,里面有详细的编译配置细节。

例子
print_r(chinese_to_pinyin("彪悍的人生不需要解释!"));

输出内容,带音标、带标点(标点和拼音挤在一起)

Array
(
    [0] => biāo
    [1] => hàn
    [2] => de
    [3] => rén
    [4] => shēng
    [5] => bù
    [6] => xū
    [7] => yào
    [8] => jiě
    [9] => shì!
)
print_r(chinese_to_pinyin("彪悍的人生不需要解释!", PINYIN_NONE|PINYIN_FORMAT_EN));

输出结果,不带音标,标点符号单独开了:

Array
(
    [0] => biao
    [1] => han
    [2] => de
    [3] => ren
    [4] => sheng
    [5] => bu
    [6] => xu
    [7] => yao
    [8] => jie
    [9] => shi
    [10] => !
)
print_r(chinese_to_pinyin("燕睿涛"));
print_r(chinese_to_pinyin("燕睿涛", PINYIN_ISNAME));
print_r(chinese_to_pinyin("罗永浩", PINYIN_ISNAME));

输出内容,可以看出PINYIN_ISNAME这个标志位还是很有用的,

rray
(
    [0] => yàn
    [1] => ruì
    [2] => tāo
)
Array
(
    [0] => yān
    [1] => ruì
    [2] => tāo
)
Array
(
    [0] => luō
    [1] => yǒng
    [2] => hào
)

初次之外,还有些关于标志位的使用规律:

PINYIN_NONE、PINYIN_UNICODE两个是对立的,使用前者没有音调,使用后者有音调,默认是前者。

PINYIN_TRIM、PINYIN_FORMAT_EN、PINYIN_FORMAT_CH是对立的,第一个清除所有标点、第二个
使用英文标点,第三个使用中文标点

PINYIN_ISNAME 如果设置了这个标志位,会使用姓氏的规则去解析读音。

总结

这是第二个PHP扩展了,这次写起来跟1年前相比容易了许多,错误也比较少了,继续努力吧~

不要停止学习的脚步,提高自身核心竞争力。

这是github地址:pinyin,欢迎大家点赞、fork、
pull-request或者提建议。

posted @ 2016-05-30 19:11  奔跑的Man  阅读(1798)  评论(2编辑  收藏  举报