中文

中文的字数统计

统计字符串包含多少字数,中、英文的统计逻辑是不同的,在 unicode 编码中,一个英文字母长度为 1,而一个汉字的长度为 1。
关于如何进行中英文混合时的字数统计,在课程的前面中已经出现参考的代码,可以翻回去看看。
正则的性能,在多数时候是非常高的。一般来说,字数统计 我们是要追求性能的,你也可以换另外的方式来实现统计,再比对正则的实现方式,性能是否有大的变化。

按照自己的想法实现的代码,是可以提高统计精度的。自行权衡之:一个快速、高效的统计方式,精度或许只有 95% 左右,是否有必要提高到更高?是否值得付出更多的代价?
这种权衡,离开技术的层面,就会来到用户体验层面。有些产品,在处理字数统计时,会细化到中文字数、非中文字数、英文词数 .etc 的程度,至于是否有必要,是产品本身特质决定的。
但就我的个人喜好,是会把中文的和英文的,计数时同等对待,并且不会在统计结果中呈现其它统计细节。如果,并非超高专业性的产品,细节多了,会增加用户的认知负担。

中文的 URL 化

一个 URL 中出现中文,在浏览器中或许显示正常,是因为浏览器已经做了特别的对应,即便如此,直接复制 URL 粘贴到其它地方,视觉上看起来仍然是非常不友好的。
如果我们做的产品是 Web 端的,并由用户、编辑不断提供内容的,那么就会不断产生新的 URL,为了更好的用户体验,我们一般允许使用者自定义 URL 之外,也会自动生成这个 URL。这个自动的 URL,可能是数字 ID,或者文章对应到数据库中的 ID,也有可能是文章的标题本身。同样为了更好的用户体验,我们一般会从使用者提供的内容中(比如标题)提取 URL,唯一的问题,就是如果出现中文又会导致 URL 的视觉感不好。
一个解决方法,就是将中文 URL 化,这个 URL 化并不是简单的编码转义,而是彻底转成 (人类可认知的) 英文字符,比如 中文 转为 zhong-wen

关于如何实现,虽然课程之前并未有参考代码出现,但是具体使用什么 package,前面已经介绍过了,再返回前面翻阅看看?
诚然,一些第三方的 package,拿来用就好了,但是在细节上,我们仍然需要注意处理。比如 - 这个连接符,可能会连续出现 --,也可能结尾或开头出现 -,这些都需要处理掉,不然不好看。也有可能出现转化结果为空的情况 (比如标题就是一个句号),这种情况也要考虑到。

在实现 中文的 URL 化 过程中,我也曾考虑过直接将中文翻译成英文,而不是这种编码转义的方式,终究还是放弃,倒不是因为无法实现,而是它背后会带来更多的问题。
我们此次使用的 package,逻辑上已算简单,但是仍然还有一个问题,就是中文的多音字 到底是 xing 还是 hang? 而一个处理编码的 pacakge,肯定不会考虑语义的问题。
一个简单的解决方案,都存在一些无可解决的问题,那么一个复杂的解决方案,是否会牵涉到更多无法解决的潜在问题呢?
单纯多音字的问题,对于使用者而言,不算友好了,用户他首先怀疑的是不是你的产品出错了。用者有时会纯粹站在自己的角度,首先未必能认识到是 多音字 的问题,其次认识到了也很可能认为应该是容易解决的。这里就存在巨大的认知偏差。如果我们使用 中文自动翻译成英文 的方案,这种认知偏差会更大,为了弥补这种认知偏差,可能要在视觉界面、交互逻辑上增加其它的对应,而另外一方面,新增的交互逻辑,很可能又会被使用者忽略过去,同时认知偏差依旧会存在。

就技术而言,各环节的叠加,有些原本不是瓶颈的会成为瓶颈,然后,性能的问题就会爆发。
就用户体验而言,认知上的偏差,多个环节叠加之后,也同样会造成巨大的费解。

标点符号的标记

我们先看一个截图:

如果还有印象的话,你会发现截图来自于 《FirstWeb》,是一个名为 Ze 的模板,并且当时介绍它的时候,说纯粹通过前端技术是无法实现的。因为它有两个技术点:

  1. WebFont 以及 字体子集的提取;
  2. 中文标点符号的标识。

Google 的开源项目 sfntly https://github.com/googlefonts/sfntly 中有一个子工具,叫 sfnttool.jar,可以用来提取一个字体的子集。可以编译 sfntly 并最终获得 sfnttool.jar,也可以直接在网上找到别人编译好的版本。如果自己编译的话,会获得一个小成就 『编译了 Java 项目』。
获得 sfnttool.jar 之后,你需要将其转为 Python 可调用的 module,对于外部命令的调用,可以使用 os.popen 或者引入 subprocess 这个模块 (subprocess.Popen)。跨语言之间的调用,数据的传递或许会成为问题,但是 sfnttool.jar 可以传入一个源文本的文件路径以及导出后的字体文件的路径,所以在 Python 的调用中,只需要提供这些文件路径就可以了。
当你完成上述要求后,你会发现,已经会使用 Python 去调用其它语言提供的工具了。如果看了上文的提示后,仍然迷糊的,可以在 Github 上找找其他人的实现方式,不过此时,搜索 Repositories (repo) 是很难匹配结果的,可以搜索 Code,并且指定为 Python 的程序语言试试。

然后,就是要识别出网页中 (也就是 HTML 源码中) 的标点符号了,最后需要 CSS 的配合,从而完成页面的渲染。
参考代码如下:

import re

def cjk_html_punctuation_replace(obj):
    head = obj.group(1)
    body = obj.group(2)
    foot = obj.group(3)
    if head.startswith('<div') or head.startswith('<p') or head.startswith('<a') or re.match(r'<h\d', head):
        body = re.sub(u'([\{\}\(\)\[\]:\u3010\u3011\uff08\uff09\u300a\u300b\uff1a])',
                      '<span class="cjk_punctuation cjk_punctuation_v"><span class="wrap">\g<1></span></span>',body)
        body = re.sub(u'([,\.\uff0c\u3002\u3001])',
                      '<span class="cjk_punctuation cjk_punctuation_h"><span class="wrap">\g<1></span></span>',body)
    return head+body+foot

def set_cjk_punctuation(html):
    html = re.sub(r'(<[^<>]*?>)(.*?)(</[^<>]*?>)', cjk_html_punctuation_replace, html, re.DOTALL)
    return html
head.startswith('<div') or head.startswith('<p') or head.startswith('<a') or re.match(r'<h\d', head)

上面这个代码挺临时的,相当于草稿的状态,完事之后,也没有做进一步的调整,比如完全可以替代为:

re.match('<div|<p|<a|<h\d', head)

还有,正则表达式中,类似于 [\{\}] 是明显的冗余, [{}] 就可以了。
再进一步说,一个 HTML 标签,仅仅判断 <div 开头来匹配 DIV 元素,是非常不严谨的。

综上而言,示例代码中流淌着一些坏味道?
其实不然,坏味道的代码,不长这个模样的。但就在临界点了,如果再多一两个连续的 startswith 的判断,或者这个代码片段要处理所有通用的 HTML 源码,那么,它就是坏的代码了。
坏味道 是一个比较主观的看法,不苛求,但有时候又会显得很苛求,比如下面两段代码:

# 一个 类 以小写开头,特殊用途(比如修饰器),可以理解
class hello(object):
    pass

# 一个 函数 以这样的命名方式, 真的非常难以理解
def TellYouSomething(something):
    pass

蛮矛盾的状态,一方面会强调不要对自己要求过高,另一方面,又担心学习者对自己太放松,从而成了一个水货一般的程序员。
有人说 Python 是(万能) 胶水一般的语言,也有人说 Python 可以做快速的原型,都非常有道理,希望本段的内容,能给你带去一点点的启发。

简体、繁体转换

中文的简体、繁体转化,会涉及到一些细节问题,比如软件軟體(软体)这种习惯说法不同的地方有不少。
转化的过程中即使考虑到习惯说法,也仍然会有问题,除非增加分词的逻辑,而即使增加了分词的逻辑,也不能保证分词的准确率有 100%,比如 它的柔软体现在 里出现了 软体 ,但明显它在上下文中不是一个词。
上面的例子不恰当的是用简体中文举了繁体中文的例子,但所表达的意图是清晰的。没有举繁体中文的例子,是因为不在那个语文环境之下,有些一知半解然后直接举例可能会贻笑大方,比如繁体中文中,某个字它的繁体、简体都存在,而且场景不同还不能乱用。

上文提到的这些局限性,当你尝试解决多一个环节,代码运行的性能就会急剧下降,甚至写代码的过程中,复杂度也会增加不少。
有时候,技术 没有办法做到 100% 的精准,各种限制性条件存在,我们只是权衡了一种解决方案而已。
所以,现在我们实现的方案,只是一个 简体、繁体转换的简单对应, 只转化 ,而不关心 ,更不会去分析语义。

一些提示:

  1. 将字符串当做 list 做循环,一般用 for,使用 dict 类型作为映射关系。
  2. 创建两个 dict,一个是繁体转简体的,一个是简体转繁体。
  3. 很可能会用到 Python 内置的函数 zip。
  4. 获得简体中文的所有字符后,可以使用一些在线的简体转繁体的工具,从而获得对应的相同次序的繁体字符。
  5. 使用 unicode,换句话说,如果是 Python 3 的环境,使用 str。
  6. 可以有三个函数: 一个是 简体转繁体,一个是 繁体转简体,一个是前面两者的基础函数类似于 def exchange(text, exchange_map),3 个函数的代码总行数应该是小于 20 行或者小于 10 行。

最后: 当完成所有代码之后,你可以去 Github 上找一些别人使用 Python 写的简繁体转化的代码,然后看看它们的实现逻辑,或者进一步比较下跟自己程序的性能差异,说不定你的代码性能超出别人很多呢。
如若如此,请开心吧,我们还是新手不是?或许很快也将不再是新手了。