Python在code page 65001命令行下print 非ASCII字符的问题

之前不是写过一个自动化破解Kindle提取图片的脚本嘛,里面有一部是调用别人的一个py2脚本来抽取res里的HD图片。

那个脚本我琢磨着不是很复杂,于是想想能不能改成py3,这样就可以直接当模块调用了,elegant很多。我先用2to3转了一波,把里面的printdict.keys()list(dict)之类的比较简单的转换了。跑了下果然还不行,然后就照着报错信息一点点改。

其实,绝大部分的问题都源自字符串的处理。Py2里字符串默认是byte,py3是Unicode字符串。这个其实再做一些低级操作(比如这里的抽取文件头啥的)反而是py2比较方便,抽出来就是byte直接就能操作。不过既然我们要转py3了,那就逐一处理吧。里面如果本来就是当字符串用的不用处理,那些从byte读出来的则实质是byte类型,所以如果要print或者和其他真·字符串进行合并之类的,要先decode()一下;大部分其实都是一些ASCII的控制字符,所以decode()默认的utf-8就行。原本的从Unicode encode到byte的直接去掉encode()unicode()要变str().encode('hex')(假设前面的变量是byte)直接换.hex()。其实,直接print byte也无所谓,就是会显示成b'abc'而已,但是强迫症表示受不了。

然后我着手处理之前懒得处理的print(self.title)崩掉的问题。其实转换成py3之后,这个问题就很少出现了,但是我发现当你把输出redirect到文档(比如x.py > 1.txt)还会有问题。研究了下,发现在输出到文档的时候,sys.stdout.encoding会变成非U的locale,而不是utf-8。虽然我平时也不这么干,但是保险起见搞了个

try: 
    print(self.title)
except: # It will have problem otherwise in certain env, such as when redirect output to '> 1.txt' 
    print(self.title.encode(sys.stdout.encoding, "ignore").decode(sys.stdout.encoding))

应付。这么搞了一波之后学校测试了一番没问题,很开心。结果回家之后一跑脚本报错了。

由于报错之后直接就关掉了,我于是先启动cmd然后里面再运行我的命令,结果这次报错的地方都变了。我不断地注释语句,最后得出的结论是,凡是print的地方都有可能出错——但是用VS Code的控制台(CMD)运行同样的命令就不会错。

折腾了很久之后,我想起之前的一个现象,用ebook-extract的时候,他会很自作多情地先把CMD的code page切成65001(理论上应该是UTF-8,但是微软的实现有很多缺陷经常被社区诟病)——我能发现是因为字体点阵高度会变,导致窗口变得很矮。我手动切CP(CMD命令:chcp 65001)试了下:

Active code page: 65001

C:\Users\Administrator>py -3 -c print('\u0142')
Traceback (most recent call last):
 File "<string>", line 1, in <module>

C:\Users\Administrator>

果然Python在65001的CMD下,输出任何非ASCII的字符都会直接报错(return?)。搜了下Python的bug tracker,开发者说这是Windows的bug,具体来说是在CP65001下,Win对Unicode字符错误地按ANSI来准备buffer,导致buffer大小不足导致。

其实calibre的所有命令行工具都有这个毛病。暂时不是很清楚为什么一定要切换到CP65001操作,而且最可恶的是用完还不切回来。顺便一提,还有个bug是运行完calibre的CLI工具后,在同一个CMD窗口进行复制操作会直接崩掉CMD,也是酷炫。

不过很奇怪地是,在VS Code里的CMD就不会有这个问题,不过VS Code的console一直都比较robust就是了。

既然知道了原因,解决方法也简单粗暴:直接在ebook-convert跑完后自行把CP切回来。我的函数是

import os, locale
def changeCodePageBack(): 
    cp = locale.getpreferredencoding().replace('cp','')    
    os.system('chcp '+cp)

这里用locale.getpreferredencoding()可以读取到当前电脑的默认非Unicode环境。

哦对了,我修改完的Py3版抽取res的脚本在这里。Rev.1是原py 2版,可以看diff。由于属于盗版原版,不要到处传播哈。

一个有关“Python在code page 65001命令行下print 非ASCII字符的问题”的想法

  1. 我看有人用node.js封装了py2和py3两个版本的脚本,使用npm的npx命令来拆azw6,取名为kindle hd unpack。唯一多出来的功能应该就是有log 文件了。
    我这几天也在读您的文章并学习。我是用您的py3版本加以修改,比如目录名是书名,也有log文件。
    由于py3好像不支持拖拽文件作为输入参数来执行脚本了,还需要配套一个bat文件。

    目前看,凡是涉及文本的页号,都没有高清版本。
    我的书是写真集,只有前后一些书籍信息页缺少,需要从azw3中找。

    目前有人在github有人用C#重写并完成了您的一些想法。
    https://github.com/Aeroblast/UnpackKindleS

    Liked by 1 person

留下评论