探索可拓展的多语言字符计数器
一、背景
之所以做这个工具,其实也是来自于项目的需要。我们在进行AI翻译项目的时候,需要评估每一份翻译文件的字符数,根据字符数来对比旧的购买人工翻译服务的费用,以及使用AI翻译的成本。 需求方要求能有一个工具,可以对多语言的文档进行字数统计,字数统计基本与word文档中的计数器的结果基本一致。(意味着在开发时可以以word文档计数器作为参考,进行偏差修正)二、难点
计算一份文档中的字符的个数,咋一看似乎很简单,各大语言都有不管是占用字节的计数,或者是单独一个字符的计数。以php为例:
$str = "你好"; |
$str = "你好"; |
三、探索
先来看一张图:
2、go模块bufio,对于英文支持比较好,不支持中文;
3、unicode编码进行计数,效果不理想,英文勉强误差较小,中文效果不好;
4、分词库gojieba,这个库是“结巴分词”的go版本,它的计数不是计算字符,而是将中文语义中的“词”进行拆分,因此跟word文档中的计数相差很大,但是感觉这种可能才是中文计数的正确打开方式,效果是这样:
四、方案选择
最终我们选择了正则的方式。那我们如何解决复杂的不同语种的字符计数呢?
解决这个问题,主要围绕着“通用计数“+”灵活扩展“进行。
五、通用计数
其实正则的核心实现也并非我原创,而是在查找字符计数主流解决方案时,看到有一个PHP博客的一个回复,大概的意思是可以通过填充非主流的中文占位符”龘“,然后再用中文的计数方式mb_strlen来计数,可以解决中文+英文,甚至中英混搭的字符计数问题。 我将这个思路通过正则进行了代码实现,再加入了处理符号的正则,形成一个基础的处理函数:运行了一下,中英文确实可以与word统计一致,不得不说这个方式确实有点骚。 随后我让小七帮我转成Go版本的实现,变成这样:// 仿 Word 统计文章字数,和 word 统计出来的一致function comment_count_word($str){//$str =characet($str);//判断是否存在替换字符$is_tihuan_count=substr_count($str,"龘");try {//先将回车换行符做特殊处理$str = preg_replace('/(\r\n+|\s+| +)/',"龘",$str);//处理英文字符数字,连续字母、数字、英文符号视为一个单词$str = preg_replace('/[a-z_A-Z0-9-\.!@#\$%\\\^&\*\)\(\+=\{\}\[\]\/",\'<>~`\?:;|]/',"m",$str);//合并字符m,连续字母、数字、英文符号视为一个单词$str = preg_replace('/m+/',"*",$str);//去掉回车换行符$str = preg_replace('/龘+/',"",$str);//返回字数return mb_strlen($str)+$is_tihuan_count;} catch (Exception $e) {return 0;}}
到这一步,中英文的适配基本ok了。func CommentCountWord(str string) int {// check if replacement character existsisTihuanCount := strings.Count(str, "龘")defer func() {if r := recover(); r != nil {// Here can handle error}}()target := str// replace return and newline characters with special handlingre := regexp.MustCompile(`(\r\n+|\s+| +| +| +|[\x{00A0}]+)`)target = re.ReplaceAllString(target, "龘")// handle English characters and numbers, consecutive letters, numbers, and English symbols are// considered as one wordre = regexp.MustCompile(`[a-zA-Z0-9\-\.!@\#$%\\\^&\*\(\)+=\{\}\[\]/",\'<>~\?:;|]+`)target = re.ReplaceAllString(target, "m")// merge character m, consecutive letters, numbers, and English symbols are considered as one wordre = regexp.MustCompile(`m+`)target = re.ReplaceAllString(target, "*")// remove return and newline charactersre = regexp.MustCompile(`龘+`)target = re.ReplaceAllString(target, "")// return the countreturn utf8.RuneCountInString(target) + isTihuanCount}
六、灵活扩展
前面看起来挺顺利,但是接下来我就碰到难题了。 当我将其他语言的字符串当成参数输入这个函数的时候,不停调整正则的匹配,怎么都不对,甚至影响了原来对于中英的计数。 甚至,当字符串中包含了乱码、emoji等预期之外的字符时,字符计数就开始飘了,囧rz 正当我一筹莫展的时候,在进行葡萄牙语的正则匹配的时候,发现了可以不断进行追加替换的方式,把不同语言的连贯计数的逻辑进行叠加,最后再变成非主流中文进行计数,像这样:
七、完整代码共享
最后附上完整的Go的代码,需要的可以自取:package word_countimport ("regexp""strings""unicode/utf8")// 利用正则提取类word字数统计func CommentCountWord(str string) int {// check if replacement character existsisTihuanCount := strings.Count(str, "龘")defer func() {if r := recover(); r != nil {// Here can handle error}}()target := str// replace return and newline characters with special handlingre := regexp.MustCompile(`(\r\n+|\s+| +| +| +|[\x{00A0}]+)`)target = re.ReplaceAllString(target, "龘")// handle English characters and numbers, consecutive letters, numbers, and English symbols are// considered as one wordre = regexp.MustCompile(`[a-zA-Z0-9\-\.!@\#$%\\\^&\*\(\)+=\{\}\[\]/",\'<>~\?:;|_é“”�]+`)target = re.ReplaceAllString(target, "m")// ------ 追加匹配其他语种 start -----// handle Portuguese characters (葡萄牙语)re = regexp.MustCompile(`[ãàáêéíóõúçÃÀÁÊÉÍÓÕÚÇ]+`)target = re.ReplaceAllString(target, "m")// handle Polish characters (波兰语)re = regexp.MustCompile(`[ąćęłńóśźżĄĆĘŁŃÓŚŹŻ]+`)target = re.ReplaceAllString(target, "m")// handle Arabic characters (阿拉伯语)re = regexp.MustCompile(`[\x{0600}-\x{06FF}]+`)target = re.ReplaceAllString(target, "m")// handle Persian(Farsi) characters (波斯语)re = regexp.MustCompile(`[\x{0600}-\x{06FF}]+`)target = re.ReplaceAllString(target, "m")// handle Russian characters (俄语)re = regexp.MustCompile(`[\x{0400}-\x{04FF}]+`)target = re.ReplaceAllString(target, "m")// handle Spanish characters (西班牙语)re = regexp.MustCompile(`[áéíóúñüÁÉÍÓÚÑÜ¡¿]+`)target = re.ReplaceAllString(target, "m")// handle Turkish characters (土耳其语)re = regexp.MustCompile(`[çğıöşüâîûÇĞIÖŞÜÂÎÛ]+`)target = re.ReplaceAllString(target, "m")// handle French characters (法语)re = regexp.MustCompile(`[éèêëàâæçœôùûüÿÉÈÊËÀÂÆÇŒÔÙÛÜŸ]+`)target = re.ReplaceAllString(target, "m")// handle Hebrew characters (希伯来语)re = regexp.MustCompile(`[\x{0590}-\x{05FF}]+`)target = re.ReplaceAllString(target, "m")// handle Italian characters (意大利语)re = regexp.MustCompile(`[àèéìòùÀÈÉÌÒÙ]+`)target = re.ReplaceAllString(target, "m")// handle Kazakh characters (哈萨克语)re = regexp.MustCompile(`[ӘҒҚҢӨҰҮҺәғқңөұүһ]+`)target = re.ReplaceAllString(target, "m")// handle Thai characters (泰语)re = regexp.MustCompile(`[\x{0E00}-\x{0E7F}]+`)target = re.ReplaceAllString(target, "m")// handle Ukrainian characters (乌克兰语)re = regexp.MustCompile(`[ҐЄЇІїієґ]+`)target = re.ReplaceAllString(target, "m")// handle German characters (德语)re = regexp.MustCompile(`[äöüßÄÖÜẞ]+`)target = re.ReplaceAllString(target, "m")// handle Vietnamese characters (越南语)re = regexp.MustCompile(`[àáảãạăằắẳẵặâầấẩẫậđèéẻẽẹêềếểễệìíỉĩịòóỏõọôồốổỗộơờớởỡợùúủũụưừứửữựỳýỷỹỵÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬĐÈÉẺẼẸÊỀẾỂỄỆÌÍỈĨỊÒÓỎÕỌÔỒỐỔỖỘƠỜỚỞỠỢÙÚỦŨỤƯỪỨỬỮỰỲÝỶỸỴầấẩẫậằắẳẵặềếểễệồốổỗộờớởỡợừứửữựầấẩẫậằắẳẵặềếểễệồốổỗộờớởỡợừứửữự]+`)target = re.ReplaceAllString(target, "m")// handle Emoji characters (表情符号)re = regexp.MustCompile(`[\x{1F300}-\x{1F5FF}\x{1F600}-\x{1F64F}\x{1F680}-\x{1F6FF}\x{2600}-\x{26FF}\x{2700}-\x{27BF}]+`)target = re.ReplaceAllString(target, "m")// 此处可以继续拓展其他语种的规则支持// ------ 追加匹配其他语种 end -----// merge character m, consecutive letters, numbers, and English symbols are considered as one wordre = regexp.MustCompile(`m+`)target = re.ReplaceAllString(target, "*")// remove return and newline charactersre = regexp.MustCompile(`龘+`)target = re.ReplaceAllString(target, "")// return the countreturn utf8.RuneCountInString(target) + isTihuanCount}