引言
github地址:aizuyan/pinyin
無意中看到了overtrue/pinyin這個項目,感覺很有意思,
這個項目做了這么一件事情:
將漢字轉化為拼音
剛看到這里是不是覺得沒什么難度,沒什么意思?您不妨接着往下看。要是只是將漢字轉為拼音好像
很容易就實現了,但是要是給轉換之后的漢字帶上音調呢,這樣難度就很大了,因為漢字博大精深,
其中一方面就表現在多音字
,同樣一個字在不同的語句場景下,音調是不一樣的。看到這里你在考
慮下如何處理?
這里我還是很佩服安正超(要是不了解他,不妨點進去看看,他
有好幾個開源項目都非常棒)這位大神的,他的思路讓我眼前一亮:
替換的時候首先將常用的詞組替換了,比如短語、成語、常用的詞語,這些詞語替換的時候按照常
用程度由高到低排序。
接着對剩余的未被替換的漢字進行替換,這里直接按照所有漢字和拼音的映射,沒有特定順序。
如果是姓名,首先對姓氏進行一遍特定替換,姓氏的聲調可能和常用的不一致。
除了項目的想法很棒之外,還有一個大難題要解決,就是收集詞語,姓氏等,這里我直接使用了安正 超
的項目中的數據,再次也該寫下安前輩
。
我的這個項目可以說是很大程度上是安前輩
的overtrue/pinyin
項目的另一個版本。
設計
這個項目中會用到一個數據結構,單鏈表,用來存儲拼音、漢字對應關系的數據,一開始想着使用Bu cket
數據結構,后來覺得,這個數據結構體里面冗余的信息太多了,也不是很符合我預想的數據結構,
so,最后使用了下面的數據結構:
typedef struct mylist {
char *key; //詞語、成語或者單個漢字
char *val; //對應的拼音,拼音前面都有一個制表符`\t`
struct mylist *next; //指向下一個漢字拼音結構體
} MyList;
除此之外,我還考慮了性能問題,安前輩php版本的有個固有的缺陷,每次請求都要去加載一遍數據文
件,大概有600~700kb左右,轉換為數組,元素個數為40000~50000個左右,每次請求都會分配、釋放這部分內
存。而且這個過程會有大量的計算過程(查找、替換),這也是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或者提建議。