最近在学习pyttsx时,发现中文阅读一直都识别错误,从发音来看应该是字符编码问题,但搜索之后并未发现解决方案。自己一路摸索解决,虽说最终的原因非常可笑,大牛们可能也是一眼就能洞穿,但也值得记录一下。嗯,主要并不在于解决之道,而是探究之旅。
1、版本(python2中谈编码解码问题不说版本都是耍流氓)
python:2.7
pyttsx:1.2
OS:windows10中文版
2、系统的各种字符编码
sys.getdefaultencoding() ascii
sys.getfilesystemencoding() mbcs
locale.getdefaultlocale() (‘zh_CN‘, ‘cp936‘)
locale.getpreferredencoding() cp936
sys.stdin.encoding UTF-8
sys.stdout.encoding UTF-8
3、探究之路
(1)初体验:
按照http://pyttsx.readthedocs.io/en/latest/engine.html 的说明,传入中文,使用unicode类型,utf-8编码,结果发音并不是输入的内容。
1 #-*- coding: UTF-8 -*- 2 import sys 3 import pyttsx 4 5 reload(sys) 6 sys.setdefaultencoding("utf-8") 7 8 text = u‘你好,中文测试‘ 9 engine = pyttsx.init() 10 engine.say(text) 11 engine.runAndWait()
(2)再试探:
或许是pyttsx内部转换的问题?将传入类型str类型,依然为utf-8编码,但发音依旧不对,和之前一样。
1 #-*- coding: UTF-8 -*- 2 import sys 3 import pyttsx 4 5 reload(sys) 6 sys.setdefaultencoding("utf-8") 7 8 text = ‘你好,中文测试‘ 9 engine = pyttsx.init() 10 engine.say(text) 11 engine.runAndWait()
(3)困惑:
google、百度轮番上阵,并未发现有类似问题,难道是默认语音的问题?获取属性看看!
voice = engine.getProperty(‘voice‘)
通过上述语句,获取到的voice是 TTS_MS_ZH-CN_HUIHUI_11.0,在控制面板-语音识别中,可以看到huihui是中文语音,原因不在于此。
(4)深入深入,迷茫:
既然系统没有问题,那看pyttsx的源码怎么写的吧,开源就这点好,有问题可以直接撸代码。
在pyttsx\driver.py中的__init__函数中,可以看到windows平台,默认使用的是sapi5
1 def __init__(self, engine, driverName, debug): 2 ‘‘‘ 3 Constructor. 4 5 @param engine: Reference to the engine that owns the driver 6 @type engine: L{engine.Engine} 7 @param driverName: Name of the driver module to use under drivers/ or 8 None to select the default for the platform 9 @type driverName: str 10 @param debug: Debugging output enabled or not 11 @type debug: bool 12 ‘‘‘ 13 if driverName is None: 14 # pick default driver for common platforms 15 if sys.platform == ‘darwin‘: 16 driverName = ‘nsss‘ 17 elif sys.platform == ‘win32‘: 18 driverName = ‘sapi5‘ 19 else: 20 driverName = ‘espeak‘ 21 # import driver module 22 name = ‘pyttsx.drivers.%s‘ % driverName 23 self._module = importlib.import_module(name) 24 # build driver instance 25 self._driver = self._module.buildDriver(weakref.proxy(self)) 26 # initialize refs 27 self._engine = engine 28 self._queue = [] 29 self._busy = True 30 self._name = None 31 self._iterator = None 32 self._debug = debug
在pyttsx\driver\sapi5.py中可以看到say函数,在调用speak时,有一个toUtf8的转换,难道最终传入的是utf8编码格式的?
1 def say(self, text): 2 self._proxy.setBusy(True) 3 self._proxy.notify(‘started-utterance‘) 4 self._speaking = True 5 self._tts.Speak(toUtf8(text), 19)
继续向下探,在toUtf8的定义在pyttsx\driver\__init__.py中,只有1行
1 def toUtf8(value): 2 ‘‘‘ 3 Takes in a value and converts it to a text (unicode) type. Then decodes that 4 type to a byte array encoded in utf-8. In 2.X the resulting object will be a 5 str and in 3.X the resulting object will be bytes. In both 2.X and 3.X any 6 object can be passed in and the object‘s __str__ will be used (or __repr__ if 7 __str__ is not defined) if the object is not already a text type. 8 ‘‘‘ 9 return six.text_type(value).encode(‘utf-8‘)
继续深入,pyttsx\six.py中,对text_type有定义
1 # Useful for very coarse version differentiation. 2 PY2 = sys.version_info[0] == 2 3 PY3 = sys.version_info[0] == 3 4 5 if PY3: 6 string_types = str, 7 integer_types = int, 8 class_types = type, 9 text_type = str 10 binary_type = bytes 11 12 MAXSIZE = sys.maxsize 13 else: 14 string_types = basestring, 15 integer_types = (int, long) 16 class_types = (type, types.ClassType) 17 text_type = unicode 18 binary_type = str
可以看到,PY2中是unicode,至此到底了。根据代码,果然最终是转成utf-8编码的,可我传入的就是utf-8编码啊!
问题还是没有解决,陷入更深的迷茫...
(5)峰回路转
既然pyttsx没有问题,那难道是sapi5的问题?转而搜索sapi5。
sapi5(The Microsoft Speech API)是微软提供的语音API接口,win10系统提供的是最新的5.4版本,pyttsx中say最后调用的就是其中的ISpVoice::Speak接口,MSDN上有详细的介绍。 (https://msdn.microsoft.com/en-us/library/ee125024(v=vs.85).aspx)
从MSDN的介绍中,可以看出输入可以是字符串,也可以是文件名,也可以是XML格式。输入格式为LPCWSTR,指向unicode串。
ISpVoice:: Speak speaks the contents of a text string or file. HRESULT Speak( LPCWSTR *pwcs, DWORD dwFlags, ULONG *pulStreamNumber ); Parameters pwcs [in, string] Pointer to the null-terminated text string (possibly containing XML markup) to be synthesized. This value can be NULL when dwFlags is set to SPF_PURGEBEFORESPEAK indicating that any remaining data to be synthesized should be discarded. If dwFlags is set to SPF_IS_FILENAME, this value should point to a null-terminated, fully qualified path to a file. dwFlags [in] Flags used to control the rendering process for this call. The flag values are contained in the SPEAKFLAGS enumeration. pulStreamNumber [out] Pointer to a ULONG which receives the current input stream number associated with this Speak request. Each time a string is spoken, an associated stream number is returned. Events queued back to the application related to this string will contain this number. If NULL, no value is passed back.
似乎看不出什么,但VS针对LPCWSTR,有两种解析方式,一种是按照unicode体系,另外一种是mbcs体系了。现在utf-8编码明显不正确,证明实际COM组件并不是按照unicode体系来解析的,那似乎应该换mbcs来看看。windows中文系统在mbcs编码体系下,字符集基本使用的就是GBK了。嗯,或许应该试试GBK?
先用其他方式验证一下,参考网上的代码用js写了一段tts转换的,核心读取很简单。
1 function j2() 2 { 3 var fso=new ActiveXObject("SAPI.SpVoice"); 4 fso.Speak(arr[i]); 5 i=i+1; 6 setTimeout(‘j1()‘,100); 7 return i; 8 }
结果,当txt文件为utf-8格式时,读取的结果和python实现的一样;当text文件为简体中文格式时,能够正确朗读。而一般文本编辑器在选择简体中文时,使用的就是GBK编码。
(6)黎明到来
再次修改代码,将文件编码指定为gb18030,执行,结果还是不对...
1 #-*- coding: gb18030 -*- 2 import sys 3 import pyttsx 4 import chardet 5 6 reload(sys) 7 sys.setdefaultencoding("gb18030") 8 9 text = ‘你好,中文测试‘ 10 engine = pyttsx.init() 11 engine.say(text) 12 engine.runAndWait()
嗯,细心的同学已经猜到了,好吧,我承认我记性不好!
之前探究pyttsx时,最终实际是按照下方的链条进行了编码转化:
输入的str(gb18030)--> unicode(系统默认coding,我指定的是"gb18030") --> str(utf8)
看来,如果要使用GBK编码,只能改pyttsx的代码了。难道是pyttsx的源码错了?去github上看看,结果... 好吧,我只能捂脸,大家看代码吧。
sapi5.py中的say函数
1 def say(self, text): 2 self._proxy.setBusy(True) 3 self._proxy.notify(‘started-utterance‘) 4 self._speaking = True 5 self._tts.Speak(str(text), 19)
第5行已经被改成str了,不再是toUtf8了,而且这个修改时16/5发生的,到底我下了一个什么样的版本? 从github上重新下载版本,安装执行最后一个版本,成功。
原来是我自己的错,反思一下。
慢着慢着,第一次我好像也是从github,打开chrome的历史下载记录:
第一次:https://codeload.github.com/westonpace/pyttsx/zip/master
第二次:https://codeload.github.com/RapidWareTech/pyttsx/zip/master
李逵碰到李鬼? 重新打开第一次的下载,在https://github.com/westonpace/pyttsx 上豁然发现
westonpace/pyttsx
forked from RapidWareTech/pyttsx
哦,还是我疏忽了,大家一定要用正版啊!另外,吐个槽,https://pypi.python.org/pypi/pyttsx 也算是指定的下载点,但还是1.1的版本。
(7)复盘
问题虽然解决了,但还是有疑惑,中文只支持gbk(或者说mbcs体系)么?从结果反推是显然的,但还是要探究一下。
我们知道,python不能直接调用com组件,需要先安装pywin32。pywin32在安装后,会在 /Lib/site-packages/下生成pythonwin、pywin32_system32(我用的是32位)、win32、win32com、win32comext、adodbapi等库,其中和com组件关联的主要是win32com。
在/win32com/client下,有一个makepy.py,它会根据要求生成所需com组件的py文件,生成的文件在 /win32com/gen_py/,我们在/win32com/gen_py/下果然看到
C866CA3A-32F7-11D2-9602-00C04F8EE628x0x5x4.py的文件,C866CA3A-32F7-11D2-9602-00C04F8EE628是CLSID,x0x5x4是版本。
(注:这个可以在 genpy.py中看到, self.base_mod_name = "win32com.gen_py." + str(clsid)[1:-1] + "x%sx%sx%s" % (lcid, major, minor))
1 # -*- coding: mbcs -*- 2 # Created by makepy.py version 0.5.01 3 # By python version 2.7.10 (default, May 23 2015, 09:40:32) [MSC v.1500 32 bit (Intel)] 4 # From type library ‘{C866CA3A-32F7-11D2-9602-00C04F8EE628}‘ 5 # On Thu May 11 15:31:19 2017 6 ‘Microsoft Speech Object Library‘ 7 makepy_version = ‘0.5.01‘ 8 python_version = 0x2070af0 9 10 import win32com.client.CLSIDToClass, pythoncom, pywintypes 11 import win32com.client.util 12 from pywintypes import IID 13 from win32com.client import Dispatch 14 15 # The following 3 lines may need tweaking for the particular server 16 # Candidates are pythoncom.Missing, .Empty and .ArgNotFound 17 defaultNamedOptArg=pythoncom.Empty 18 defaultNamedNotOptArg=pythoncom.Empty 19 defaultUnnamedArg=pythoncom.Empty 20 21 CLSID = IID(‘{C866CA3A-32F7-11D2-9602-00C04F8EE628}‘)
第一行codeing是mbcs,那这个是从哪里来的呢?
从genpy.py中,文件的实际编码格式,是通过file参数指定的encoding来的。
1 def do_gen_file_header(self): 2 la = self.typelib.GetLibAttr() 3 moduleDoc = self.typelib.GetDocumentation(-1) 4 docDesc = "" 5 if moduleDoc[1]: 6 docDesc = moduleDoc[1] 7 8 # Reset all the ‘per file‘ state 9 self.bHaveWrittenDispatchBaseClass = 0 10 self.bHaveWrittenCoClassBaseClass = 0 11 self.bHaveWrittenEventBaseClass = 0 12 # You must provide a file correctly configured for writing unicode. 13 # We assert this is it may indicate somewhere in pywin32 that needs 14 # upgrading. 15 assert self.file.encoding, self.file 16 encoding = self.file.encoding # or "mbcs" 17 18 print >> self.file, ‘# -*- coding: %s -*-‘ % (encoding,) 19 print >> self.file, ‘# Created by makepy.py version %s‘ % (makepy_version,) 20 print >> self.file, ‘# By python version %s‘ % 21 (sys.version.replace("\n", "-"),)
回溯代码,发现这个file来源于makepy.py的main函数
1 def main(): 2 import getopt 3 hiddenSpec = 1 4 outputName = None 5 verboseLevel = 1 6 doit = 1 7 bForDemand = bForDemandDefault 8 try: 9 opts, args = getopt.getopt(sys.argv[1:], ‘vo:huiqd‘) 10 for o,v in opts: 11 if o==‘-h‘: 12 hiddenSpec = 0 13 elif o==‘-o‘: 14 outputName = v 15 elif o==‘-v‘: 16 verboseLevel = verboseLevel + 1 17 elif o==‘-q‘: 18 verboseLevel = verboseLevel - 1 19 elif o==‘-i‘: 20 if len(args)==0: 21 ShowInfo(None) 22 else: 23 for arg in args: 24 ShowInfo(arg) 25 doit = 0 26 elif o==‘-d‘: 27 bForDemand = not bForDemand 28 29 except (getopt.error, error), msg: 30 sys.stderr.write (str(msg) + "\n") 31 usage() 32 33 if bForDemand and outputName is not None: 34 sys.stderr.write("Can not use -d and -o together\n") 35 usage() 36 37 if not doit: 38 return 0 39 if len(args)==0: 40 rc = selecttlb.SelectTlb() 41 if rc is None: 42 sys.exit(1) 43 args = [ rc ] 44 45 if outputName is not None: 46 path = os.path.dirname(outputName) 47 if path is not ‘‘ and not os.path.exists(path): 48 os.makedirs(path) 49 if sys.version_info > (3,0): 50 f = open(outputName, "wt", encoding="mbcs") 51 else: 52 import codecs # not available in py3k. 53 f = codecs.open(outputName, "w", "mbcs") 54 else: 55 f = None 56 57 for arg in args: 58 GenerateFromTypeLibSpec(arg, f, verboseLevel = verboseLevel, bForDemand = bForDemand, bBuildHidden = hiddenSpec) 59 60 if f: 61 f.close()
可以看到,在文件指定时,打开的时候指定了mbcs的方式;在文件不指定时,后边默认也会通过mbcs来编码。因此从这里可以看到,win32com目前的版本最终都会转成mbcs编码格式。
OK,至此可以看出,win32com使用的是mbcs的编码格式,问题终于搞定。
4、总结
问题终于圆满解决了,总结一下。
(1)使用前检查是否为最新版,一定去github上。
(2)不光是最新版,还得正本清源,去源头看看,不要犯我的错误,搜索后没细看就下载了。
(3)开源时代,出现问题,多研究源码。
(4)python2的问题,编码转换太复杂,如果出现问题, 是需要具体问题具体分析的。