|
|
用户名:APOO 笔名:APOO 地区: 北京-北京 行业:本科 |
| 日 | 一 | 二 | 三 | 四 | 五 | 六 |
看QQ圈子如何泄露你敏感信息(第3次更新)
首先咱们看一段QQ圈子的功能介绍:
http://im.qq.com/quan/index.html

Q1:圈子是什么?有什么应用场景?
圈子是腾讯推出的一款内测的试验性产品,可帮助用户更方便管理社交人脉。例如帮助用户找回失去联系的中学同学或大学同学。
Q2:QQ用户如何参与内测?
如果你希望参与QQ圈子内测,只需要3月20日至4月11期间在腾讯体验中心进行申请,当申请通过后,您可以下载最新内测体验版本进行体验。申请地址:http://exp.qq.com/details.html#pid=276
Q3:圈子使用了哪些信息?采用什么样的技术?是否涉及用户隐私?
QQ圈子会严格保护您的隐私安全,QQ圈子是基于QQ好友关系、分组名、备注名,通过聚合分类数学算法进行人脉推荐。这是社交网络类产品进行人脉推荐所采用的技术,不会涉及任何用户的聊天内容、历史操作等个人信息。
Q4:圈子是否使用了我的QQ历史信息和操作数据?
你
在使用QQ的过程中,不会保存任何历史信息和操作数据。当开通QQ圈子功能时,QQ圈子功能将会根据您当前的QQ好友关系、分组名等信息智能生成圈子。如
果你在圈子中发现了你已删除多年的中学同学,那是因为你的QQ好友中还有多位与该同学是QQ好友,并非使用了您的QQ历史信息。
Q5:圈子分组是如何得来的?圈子是如何推荐出我失去联系的同学?
圈子中的分组是通过你和你的QQ好友的QQ分组得来的,好友关系比较密集的一群人会聚成一个圈子分组。
给
您举一个例子:你和你的大多数中学同学都在彼此的“同学”QQ分组下,那就会形成“同学”这个圈子分组。但有可能你的同学小王并不是你的QQ好友,但她却是你其他同学的QQ好友并在他们“同学”QQ分组中,于是圈子就为你进行了一次人脉拓展,所以可能有些同学多年失散不是你的好QQ友,但你却可以通过圈子找到他们。
Q6:圈子分组名是如何得来的?
大家在使用圈子的过程中,可能会看到“高中同学”、“同事”等不同的圈子分组,这些圈子分组名都是根据圈子内好友使用频次最高的QQ分组名来命名。
Q7:圈子中展现的好友名称是如何得来的?
QQ备注名是圈子中人名的唯一来源。如果圈子内有半数以上的好友对你设置相同的QQ备注名,这个备注名将成为在圈子内展现的名称。比如,同学圈中一半以上的同学备注王某为“小王”,那么王某在圈子中的好友名称将展示为“小王”。
Q8:我的哪些信息会被圈子好友看到?
圈子中的好友将会查看到你的如下信息:
1) 你的名称:这个名称来源于圈子内半数以上的好友对你设置相同的QQ备注名。如果只有一两个人备注你,系统是不会采用的。系统已过滤了称呼、绰号类的备注名,比如:老婆、妈妈等。
2) 你设置公开的QQ资料:圈子好友只会看到你设置为公开的QQ资料。
3) 你设置为公开的QQ空间:圈子好友只会看到你设置为公开的QQ空间动态。如果你设置了QQ空间的访问权限,没有权限的圈子好友将无法查看你的QQ空间动态。
Q9:如何退出内测?如果我不想出现在别人的圈子里怎么办?
在QQ圈子右上角的设置图标里选择“退出圈子”,你将不会再出现在别人的圈子里。如果你希望退出圈子内测,可在官网下载并安装QQ正式版本即退出内测。官网地址:http://im.qq.com/
然后咱们再吐几个槽:
帮助用户找回失去联系的中学同学或大学同学。(潜台词:我们不考虑被找的人想不想被找到。也许你跟某个名人有些交集,还能拿到某些影星甚至涛哥的QQ号呢)
如果你希望参与QQ圈子内测,只需要申请通过后,即可以下载最新内测体验版本进行体验。(潜台词:我们可不会告诉你,就算你不参与内测,你的所有数据都是默认被我们展现在其他人面前的哦)
QQ圈子会严格保护您的隐私安全,基于QQ好友关系、分组名、备注名进行筛选的。(潜台词:所谓保护隐私,就是把你最敏感的好友关系、分组名、备注名都挖掘出来,呈献给别人看哦)
你在使用QQ的过程中,不会保存任何历史信息和操作数据。(潜台词:请大家暂时忘记QQ会员的聊天记录漫游功能,我们暂时还没从那上面挖掘数据)
如果你在圈子中发现了你已删除多年的中学同学,那是因为你的QQ好友中还有多位与该同学是QQ好友,并非使用了您的QQ历史信息。圈子中的分组是通过你和你的QQ好友的QQ分组得来的,好友关系比较密集的一群人会聚成一个圈子分组。(潜台词:并非使用了QQ历史信息,也就是说其实还是保存历史信息的咯?只不过目前使用的是同样属于敏感信息的好友关系)
这些圈子分组名都是根据圈子内好友使用频次最高的QQ分组名来命名。QQ备注名是圈子中人名的唯一来源。(潜台词:不要以为你设置的分组名和备注名这些敏感信息不会被泄露,敏感信息这种东西,不仅想漏就漏,而且公开漏给大家看)
圈子中的好友将会查看到你的名称,而这个名称来源于圈子内半数以上的好友对你设置相同的QQ备注名。(潜台词:不要以为你不给别人备注你就是安全的,只要别人备注了你,系统就能让你无处遁形)
吐槽完毕,最后做几个真正用户关心的Q&A吧。据说中国法律对隐私权没有明确规定,所以不能说隐私被侵犯,只能说敏感信息被泄露。读者自己明白即可。
Q1:为什么我的敏感信息可能会被泄露?
简单来说,因为圈子的计算是根据你的好友关系,而计算出来的圈子的分组名是根据你的好友的QQ分组命名计算得来,而用户名又是根据备注名计算得来,所以你的好友关系、设置的分组名和设置的备注名都被用于数据计算。然而在业内,利用用户数据进行挖掘并不是新鲜的事情,真正泄露了敏感信息的是QQ如何利用挖掘到的数据。它挖掘了数据,自己拿去做广告定向投放,这没有问题。但是分组信息和备注信息都展现在别人的面前,这就是毫无疑问的泄露敏感信息了。
可能有一个人,不认识你,但是通过QQ圈子,他恰好在你某些朋友的圈子里,就可以找到你,添加你为好友,骚扰你。也可能有一个人,认识你,但是不知道你真实姓名,他同样可以用这个圈子功能看到你的好友对你的备注,而这往往是真实姓名。还有可能有一个人,认识你,也知道你的真实姓名,但是不知道你的QQ号,你也很讨厌他,并且庆幸你跟他不是QQ好友,但是通过圈子,他就可以找到你的QQ号,要求加你为好友,而你又不好意思拒绝,这就尴尬了。
Q2:QQ圈子如此肆意泄露敏感信息,是不是我不用最新版就没事了呢?
这是普遍的误解。事实上,目前QQ圈子采用了“默认开通,手动关闭”的方案,这也是最近腾讯数个泄露用户敏感信息的新产品普遍采用的方案。是不是你们很诧异有的时候写了个QQ签名就同步到QZone去了呢?是不是有的时候发现写个什么结果腾讯微博、QQ空间、说说、QZone全都有了呢?这就是因为腾讯最近不断默默开通新的同步功能,并且很“人性”地要求你手动关闭。可能你刚注册QZone的时候没有这个同步,可能你刚注册微博的时候也没有这个同步,但是突然哪一天他就有了。为了防止以后突然有了同步打你个措手不及,最好不要用QQ以外任何带有社交性质的产品,因为你不知道哪天它就会变成泄露你敏感信息的恶魔。只用QQ号玩腾讯游戏就好了,注意不要选上什么成就同步到微博之类的,绝对安全。当然如果你不开微博,也绝对安全。
言归正传,那么如果我不用有圈子功能的最新版QQ会怎么样呢?很简单,那就是你会出现在别人的圈子里,你的敏感信息会被一览无余。 关于怎么不让自己显示在别人的圈子里,目前有两种解决方案。第一个方案,也就是官方的Q&A里第9条给出的解决方案,是到exp.qq.com安装最新版QQ,然后开通QQ圈子,再关闭,就可以退出了。但是有的同学说,开通QQ圈子时要同意一个使用协议,而那个使用协议(我也没看,不好意思)里很可能有陷阱,比如用户自愿把敏感信息贡献给QQ以便其“使用”之类的。所以这里还有第二个方案。安装最新版QQ,进入“隐私设置”,选择“不允许他人在QQ圈子中看到我”即可。我试过,用了第一个方案,这里会自动打上勾。但是不知道单独打上勾是否可以确保安全。
Q3:我装了最新版QQ,点了试用QQ圈子,说我的好友数量太少,不够试用,是不是就安全了呢。
不是的。我用一个只有7个好友的小号测试,依然可以试用QQ圈子功能。如果你遇到“好友数量太少”的提示,大概是服务器出了某些错误而导致的虚假提示。
此外就算你真的用不了这个功能,也只是别人不会出现在你的圈子里而已。仍然不妨碍你出现在别人的圈子里。所以照着Q2做吧少年!
Q4:那我用老版本QQ或者WebQQ能进行Q2的方案二“隐私设置”吗?
不行。你只有用特殊的最新版QQ,才有那个“隐私设置”。
Q5:我已经把自己从QQ圈子退出了,不会再显示在别人的圈子里了。这样就安全了吗?
并不是这样的。因为QQ圈子做数据挖掘的依据是分组名和备注名,所以虽然你暂时安全了,但是你填写的分组数据和备注数据都会继续威胁你朋友们的安全。
唯一的解决方案是把所有的分组名都改成毫无意义的字符串,备注名也改成毫无意义的字符串,比如李腾讯之类的,或者干脆删掉。什么?你说备注都删掉了搞不清谁是谁了?那再正常不过了。备注本来就是IM软件的基本功能。如果这个IM软件的备注功能不能用,那就换一个嘛。记得告诉你的朋友,他们的敏感信息在危险之中。
Q6:我已经把分组名和备注名都删除了。这样就安全了吗?
目前来看是的。但是从整个故事发展的尿性来看,你永远也想不到QQ什么时候会又利用你的敏感信息做什么事情。请你右键单击好友,选择“查看资料”,滚动到最下方,是不是有“添加备注信息”?里面可以填写“手机”、“邮箱”、“更多”等。这些将来都有可能成为数据挖掘的对象。
与此类似的敏感信息还有“QQ通讯录”里的资料,那可都是真实人际网络的真实信息。现在里面的资料都是安全的,但是如果有一天QQ想把你的真实人际网络和虚拟人际网络打通,你一定跑不掉。此外还有“朋友网”,公认的山寨“人人网”,也是一个用户会轻易填写真实资料的地方。在这次风波中,我们可以看到很多愤怒的网友选择关闭“朋友网”。但是其实“朋友网”是无辜的,现在QQ圈子并没有使用“朋友网”的资料。但是俗话说,防患于未然,你懂的。
Q7:那还有什么是安全的呢?
至于“QQ旋风”,我觉得应该是安全的。就算你拿那玩意儿下载点爱情动作片,甚至还用了离线下载,留了记录在离线下载服务器上,你也不必担心什么。毕竟离线下载服务器对传播这些玩意儿提供了极大的便利,你可以反咬一口,所以我揣测这东西应该是安全的。还有“QQ浏览器”、“QQ电脑管家”、“QQ手机卫士”什么的,目前都没有任何证据表明它们是会泄露用户敏感信息的。
至于互娱部门(即腾讯游戏,Tencent Games)的产品也应该都是比较安全的。一般人在游戏里什么真实信息也不会用,所以就算哪个产品真想泄露点用户敏感信息,也是巧妇难为无米之炊。
Q8:简单说一下我应该怎么做吧!
从目前来看,你需要做的两件事是:1、关闭圈子或设置“隐私设置”(参见“更新2”),保护自己;2、删除所有分组名和备注名,保护朋友(参见Q5、Q6)。
但是,从长远来看,如果你担心哪一天你的敏感信息又被腾讯挖掘出来公之于众的话,以下产品和服务都属于目前没有泄露,但涉及你的敏感信息,因而值得对隐私有忧虑的人考虑清除所有数据并卸载或退出服务的(产品/服务:敏感信息)。
还有,以下产品和服务是不涉及或涉及较少敏感信息,因而相对比较安全的,个人认为可以放心使用:
更新1:“腾讯体验中心”(exp.qq.com)似乎已经不能再参与“QQ圈子”试用,也不能下载这个特殊的版本了。经过Google搜索关键字“QQ2012_FriCir.exe”,还是能找到http://dl_dir.qq.com/qqfile/qq/QQ2012/QQ2012_FriCir.exe这个地址,可以用下载工具下载到特殊版的QQ来保护自己。
更新2:“腾讯体验中心”(exp.qq.com)网页更新,提示信息如下:

由于用户的踊跃试用,第一批60万的试用名额已经提前发放完毕。
试用过程中我们收到的意见和建议将持续用于产品改进,届时,将开展第二批试用活动。
您可以在此登记,我们将在第二批试用开始时,发送邀请到您的QQ邮箱。
QQ圈子是能够让朋友之间找到彼此的新功能,即使你们并未加为QQ好友。
如果您不愿意使用,也不愿意被任何人找到,请在此设置,我们保证您将不会被别人看到。
得出三个结论:1、现在即使下载了“QQ圈子试用特别版”的QQ,可能你的号码也无法登录,也就无法修改“隐私设置”保护自己。2、从该页面筛选的“精华反馈”可以看出,产品将持续改进方向就是,挖掘你越来越多的敏感信息,以及挖掘得越来越精确,而绝不会下架了。3、依然是不尊重用户的“默认开通,手动关闭”。
不过好消息是不用装特别版QQ也可以修改“隐私设置”了。
我愈发觉得我删除“QQ空间”、“QQ通讯录”、“QQ邮箱”和“财付通”所有信息并关闭的选择是正确的。万一哪天又来一个“默认开通,手动关闭”岂不是死定了?不给QQ留任何敏感信息才是王道!作为一个用户,真的寒心了……
诺基亚 808 PureView 发布
诺基亚+卡尔蔡司这种组合,照相果然无敌。N8戴了两年的拍照王者被诺基亚808夺走,实至名归。苹果、三星、HTC这种拍照烂货怎么能比!
同时发布的另外五款半手机是:
Nokia Asha 202
Nokia Asha 203
Nokia Asha 302
Nokia Lumia 610
Nokia Lumia 900 国际版
今天真是一个激动人心的日子。诺基亚808,我等你,上市就买!
百度输入法(for Symbian^3)更新到2.2啦
信使类软件那么多,702T能用的没几个
Windows的两个buggy API函数
I was looking for APIs that could get "Language for Non-Unicode programs" setting of Regional and Language Settings of Control Panel, when I found these these two API functions. However, they behave quite buggy. I wonder if the developers of Windows had ever noticed this, or maybe they had noticed, but were too late to change it.
But hey, it is never too late to refine your documents, right? The explanations are still very simple and vague. It looks that the one who wrote the documents just assumed that the functions should work as their names. Unfortunately, they do not.
Anyway, let's review these two buggy API functions.
1. GetUserDefaultLangID()
At first glance, I thought it would return the "Language for Non-Unicode programs" setting for the current user. The document at http://msdn.microsoft.com/en-us/library/windows/desktop/dd318134(v=vs.85).aspx also says that it "returns the language identifier for the current user locale". I think it is not that easy to write so ambiguous a document as there is obviously not enough details for "current user locale". So I have to try it out by myself.
However, what this function actually returns is the Format settings (date, time, currency, etc.) of current user in Regional and Language Options in Control Panel. Anyone could try it out without any difficulty, since changing the format settings does not require a system restart. At first, when I saw that the return value is, as expected, "Chinese (PRC)", I thought it works. But when later I changed my "Language for Non-Unicode programs" setting to Japanese, I found it remained unchanged. On the other hand, GetSystemDefaultLangID() returned "Japanese" to my surprise!
To make sure what the function actually returns, I looked for any possible items that are currently "Chinese (PRC)" in Control Panel and the first thing I found was "Format Setting". I changed it to "Italian (Switzerland)" and the return value of GetUserDefaultLangID() changed accordingly. Now I am very sure about what the function actually returns.
I hope Microsoft could change their document of
GetUserDefaultLangID() and tell the programmers that it "returns the
format setting of date, time, currency and etc. of current user", which has nothing to do with "default".
2. GetSystemDefaultLangID()
As I mentioned above, the function GetSystemDefaultLangID() returns what I expected, the"Language for Non-Unicode programs" setting. However, its document is also written as bad as the former function. The document at http://msdn.microsoft.com/en-us/library/windows/desktop/dd318120(v=vs.85).aspx says that it "returns the language identifier for the system locale". As meaningless as ever.
Actually it returns the"Language for Non-Unicode programs" setting for the current user. There seems to be no simple way getting the "Language for Non-Unicode programs" for the default user, which I thought what System means. Probably reading registry is a possible way to solve the problem, but I think I could stop here. After all, I found that the setting of the default user is not that useful for me.
Same as above, I hope Microsoft could change their document of GetSystemDefaultLangID() and tell the programmers that it "returns the language setting for non-Unicode programs of current user", which has nothing to do with "system".
Python的控制台输出语句print的一个bug
Python 的 print 语句有一个很奇怪的 bug。它的功能是向控制台输出字符,这本身不是问题。但是 Python 内部是支持 Unicode 字符串的,而 Unicode 字符串在用 print 输出时 print 要进行一次从 Unicode 到 ANSI/MBCS 编码的编码,编码后才会以 8-bit 流输出结果。
编码就编码吧,这也是很正常的。对于控制台程序来说,输出可能被重定向到文本文件。如 果不指定编码,重定向时就不知道以何种 8-bit 字节流写入文本文件,所以,输出到控制台的东西理论上也应该是经过编码的 8-bit 流。综上所述,确实有必要进行一次 WCHAR 到 char 的转码。
但是问题在于,Python 的 print 语句在转码时,居然用的是 strict 规则。即,待输出字符串若含有当前代码页之外的字符,就会在转码过程中出现不可转码的文字,从而抛出 exception。print 语句又不处理这个 exception,导致一个平平常常 print 语句竟然会引起 Python 程序的异常!这简直是不可思议。
比如说你写了这么一段代码:
a = u'测试啊'
print a
然后把控制台切到某个不包含这些汉字的编码页例如 437,输入chcp 437。然后再运行这段程序,就会看到异常。实际上直接输出到控制台的是另外一种 UnicodeEncodeError 异常,因为控制台设置了代码 页,Python 会试图转码到那个代码页。而更典型的(使开发者发现问题的)异常通常是把输出重定向到文件时,看到的下面这个更典型的异常:
UnicodeEncodeError: 'ascii' codec can't encode character u'\xa1' in position 0-2: ordinal not in range(128)
注意,控制台直接输出有异常,重定向输出也会有异常。这两种异常在系统内部具体过程不同,但原理都是一样的。就是 python 遇到了它认为不能把
Unicode 字符编码成 8-bit 流的情况。区别在于,输出到控制台时,python 会试图按照控制台设置的代码页去编码,而重定向时干脆就按
ASCII
编码,那自然是只有128以内的字符才能显示出来。由此可以看出,输出到控制台时产生的异常更隐蔽,因为绝大部分程序员都是在一种编码下编码+
开发的,很少有考虑到这方面的情况。在一种编码下开发,写进代码的字符串,以及从文本读出来的字符串,通常也能在这个编码下在控制台输出,从而把问题的发
现推迟到了用户(使用了不同代码页)阶段,或是推迟到了重定向输出的时候(因为重定向默认用 ASCII 编码,字符集最小)。知道了原因,会觉得错误可以理解。
说句题外话,令我最不能理解的是,一个好好的 print 语句,输出字符串也不是 zero-terminated,不存在烫烫烫烫过了越到不可访问内存崩溃了的结果,竟然会导致程序异常!首先别跟我说让程序员去控制print 里字符串的内容,这有的时候程序根本控制不了。比如,读出一个文件并显示内容的时候。也别跟我说去 try-except,连 print 都失败了你叫程序员情何以堪啊?看来只能想想办法自己解决这个问题了。
首先要说明的是,既然事关控制台,要做
8-bit 流的输入输出,就没有完美的解决方案。我个人的建议是,在Windows下,一切字符串操作,都应该尽可能使用 WCHAR
及相关函数。遇到需要
跨平台和网络传输的情况,再使用UTC-8编码的char字符串。在与古老的 ANSI/MBCS 程序交互时,在严格限制的情况下使用该种编码的
char 字符串。尽管并没有完美的解决方案,在实际情况中,Windows 下 Python 程序也许应该可以有更好的表现。
解决方案一、最简单解决重定向异常的方法是:
import sys
reload(sys)
sys.setdefaultencoding("utf-8")
然后再输出就可以了。直接调sys.setdefaultencoding()这个函数是不行的,必须要reload一次。具体原因可以参见http://docs.python.org/library/sys.html,我就没有深入研究了。
这个不会影响控制台直接输出,只会影响重定向,所以最好是写 utf-8 反正连 Windows 的记事本都可以打开 UTF-8
的文本。当然这么做也有不足,就是如果某一个程序,调用了你写的 Python
程序,把输出重定向到它的窗口里,这时这个程序很可能是按系统默认编码去解码的,用户就看到一片乱码了。这个没什么好办法,要么外围程序做好点可以设置控
制台解码,要么你就只能获取一下当前控制台编码设置(不知道 Python 里有没有好方法,我可以用 Windows API
做到),当然这样的话就无法防止异常了……
解决方案二、用 print a.encode("gbk", "replace") 取代 print a:
对控制台来说,由于输出的是字节流,所以具体显示成什么字符,取决于控制台的代码页设置。输出重定向也是一样,取决于你打开文件的方式。如果打开文件发现乱码了,那你要说:一定是我打开的方式不对!
这个方案好处在于可以让程序完全像使用了 Windows ANSI 函数的程序那样工作。输入、输出全都是按某个特定编码来做的,仿佛程序内部固化的字符串就是按某个特定编码写的。不过,程序里有几千个 print 就得换几千次就不说了,万一你换漏了,又要出悲剧。
当
然,既然完全像一个 Windows ANSI 程序的行为,那么不可避免的问题就是乱码。假设你所有字符串都按 GBK
在输入输出时编码了,那如果用户设置的控制台代码页根本就不是 GBK 呢?又乱码了不是……而且既然我输入输出都是 GBK,干嘛程序内部还要用
Unicode 呢?大概就只是为了防止内部处理时即出现异常吧。
最关键的是这实在不是一个程序员的作风。就没有自动化一点的方案吗?
解决方案三、更改 sys.stdout 的编码:
既然问题出在 sys.stdout 的编码往往不能满足字符集需求上,为什么不直接更改它的编码呢?http://www.doughellmann.com/PyMOTW/codecs/ 提供了一种方案:
import sys, codecs
sys.stdout = codecs.getwriter('utf-8')(sys.stdout)
这
个方案的好处就是它同时影响控制台直接输出和重定向输出,比方案一强,已经达到了方案二的水平。不过它面临一个方案二没有而方案一还有的问题,就是如果设
置的不是 "utf-8",那么就有可能出 UnicodeEncodeError。如果设置的是 "utf-8",那就要面临配套设施不完善而看到的乱码问题。
最要命的是,其实你是根本无法在控制台设置成 cp65001 的情况下让程序正常运行的!这是方案二也会同样遇到的问题。假设我们设置了 utf-8,要想在控制台正常阅读输出结果,那也就要把控制台用 chcp 65001 设置成 UTF-8。但是,设置之后,python 会以为当前代码页叫 "cp65001",不认,会出这个错误:
LookupError: unknown encoding: cp65001
呃,好吧,这也是有办法可以解决的,出自 http://stackoverflow.com/questions/878972/windows-cmd-encoding-change-causes-python-crash:
import codecs
codecs.register(lambda name: name == 'cp65001' and codecs.lookup('utf-8') or None)
这样 Python 就认 "cp65001" 这个东西就是 "utf-8" 的别名了。这样,你就可以在控制台 chcp 65001 然后看到输出字符了。不过遗憾的是,这只是理论上的。实际上如果你 print a 的时候第一个字符不是纯 ASCII 的,即 Unicode 码在 128 以上,根本无法正常显示。我们不妨把前面学到的知识都拼起来,写一段代码,期望它能正常工作吧:
#coding=utf-8
a = u'测试啊'
import sys
reload(sys)
sys.setdefaultencoding("utf-8")
import codecs
codecs.register(lambda name: name == 'cp65001' and codecs.lookup('utf-8') or None)
print a.encode("utf-8", "replace")
实际上运行结果是:
???试啊Traceback (most recent call last):
File "C:\Python25\Test1.py", line 11, in
print a.encode("utf-8", "replace")
IOError: [Errno 2] No such file or directory
这 莫名其妙的 IOError 是怎么回事?而且字符串第一个字符也无法正常显示,会变成若干个“?”。该字符在 UTF-8 中是几个字节,就有几个“?”字符。我™想破了脑袋也想不出 Python 是怎么写出这样的 bug 来的!注意,不是说第一个字符是纯 ASCII 就可以了,只是那样做的话输出来的异常信息是可以看,但是异常还是有的。如果是用 sys.stdout = codecs.getwriter() 法直接 print a 的话,出现的错误是:
???试啊Traceback (most recent call last):
File "C:\Python25\Test1.py", line 13, in
print a
File "C:\Python25\lib\codecs.py", line 304, in write
self.stream.write(data)
IOError: [Errno 0] Error
所以实际上是根本没法用的。我测试的版本是 Python 2.5.2,不知道后续版本是否有改进。
而且还有一个问题是如果你 chcp 65001 之后,打过一些汉字或者用 type 显示过文件,就会发现怎么光标的位置都不对啊!换行也不对啊喂后面怎么好多东西超出去了看不到啊!
没 错恭喜你遇到了最头疼的问题!在 cp65001 下,并不像那些中国、日本、韩国的代码页下面那样区分全角和半角,所有的字符在计算光标的时候都占同样的宽度,但是字体渲染仍然正常。也就是说,如果(假 设一行设置的是 80 个字符)你在一行里写了 80 个汉字,那么前 40 个渲染的时候就已经把整行占满了,可是没有自动换行,自动换行要到 80 列才有,所以后 40 个汉字就看不见了。
坑爹呀。
遗憾的是这还根本没有解决办法。要想让全角字符正确地占两个半角字符的宽度,就只能用一些支持这个特性的代码页,比如 cp936,就是 GBK。当然,这样就不能显示全部 Unicode 字符了,万一有用户输入了这个,就只能被替换成 ? 或者其它什么东西了。
所以说,只要还跟该死的 char 字节流打交道,跟 stdout 打交道,就没法有一个完美方案。
解决方案四、彻底不使用stdout:
这 堆乱七八糟的事情从根本上来说是因为控制台的 stdout 只能接受 8-bit 字节流,也就是 char,所以才有了这么多有的没的编码问题。如果能够让 python 在用 print 的时候底层使用一个接受 WCHAR 的函数来做事,也许事情就有很大转机。
事实上,还是在 http://stackoverflow.com/questions/5109970/linux-python-encoding-a-unicode-string-for-print 就有一篇终极解决方案。它用接受 WCHAR 的 Windows API 做控制台输出,而同时把重定向交由原有方式处理,在兼顾重定向的情况下,实现了控制台下最完美的输出方案。
首先请看代码:
import sys
if sys.platform == "win32":
import codecs
from ctypes import WINFUNCTYPE, windll, POINTER, byref, c_int
from ctypes.wintypes import BOOL, HANDLE, DWORD, LPWSTR, LPCWSTR, LPVOID
original_stderr = sys.stderr
# If any exception occurs in this code, we'll probably try to print it on stderr,
# which makes for frustrating debugging if stderr is directed to our wrapper.
# So be paranoid about catching errors and reporting them to original_stderr,
# so that we can at least see them.
def _complain(message):
print >>original_stderr, isinstance(message, str) and message or repr(message)
# Work around .
codecs.register(lambda name: name == 'cp65001' and codecs.lookup('utf-8') or None)
# Make Unicode console output work independently of the current code page.
# This also fixes .
# Credit to Michael Kaplan
# and TZΩTZIOY
# .
try:
#
# HANDLE WINAPI GetStdHandle(DWORD nStdHandle);
# returns INVALID_HANDLE_VALUE, NULL, or a valid handle
#
#
# DWORD WINAPI GetFileType(DWORD hFile);
#
#
# BOOL WINAPI GetConsoleMode(HANDLE hConsole, LPDWORD lpMode);
GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)(("GetStdHandle", windll.kernel32))
STD_OUTPUT_HANDLE = DWORD(-11)
STD_ERROR_HANDLE = DWORD(-12)
GetFileType = WINFUNCTYPE(DWORD, DWORD)(("GetFileType", windll.kernel32))
FILE_TYPE_CHAR = 0x0002
FILE_TYPE_REMOTE = 0x8000
GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD)) \
(("GetConsoleMode", windll.kernel32))
INVALID_HANDLE_VALUE = DWORD(-1).value
def not_a_console(handle):
if handle == INVALID_HANDLE_VALUE or handle is None:
return True
return ((GetFileType(handle) & ~FILE_TYPE_REMOTE) != FILE_TYPE_CHAR
or GetConsoleMode(handle, byref(DWORD())) == 0)
old_stdout_fileno = None
old_stderr_fileno = None
if hasattr(sys.stdout, 'fileno'):
old_stdout_fileno = sys.stdout.fileno()
if hasattr(sys.stderr, 'fileno'):
old_stderr_fileno = sys.stderr.fileno()
STDOUT_FILENO = 1
STDERR_FILENO = 2
real_stdout = (old_stdout_fileno == STDOUT_FILENO)
real_stderr = (old_stderr_fileno == STDERR_FILENO)
if real_stdout:
hStdout = GetStdHandle(STD_OUTPUT_HANDLE)
if not_a_console(hStdout):
real_stdout = False
if real_stderr:
hStderr = GetStdHandle(STD_ERROR_HANDLE)
if not_a_console(hStderr):
real_stderr = False
if real_stdout or real_stderr:
# BOOL WINAPI WriteConsoleW(HANDLE hOutput, LPWSTR lpBuffer, DWORD nChars,
# LPDWORD lpCharsWritten, LPVOID lpReserved);
WriteConsoleW = WINFUNCTYPE(BOOL, HANDLE, LPWSTR, DWORD, POINTER(DWORD), \
LPVOID)(("WriteConsoleW", windll.kernel32))
class UnicodeOutput:
def __init__(self, hConsole, stream, fileno, name):
self._hConsole = hConsole
self._stream = stream
self._fileno = fileno
self.closed = False
self.softspace = False
self.mode = 'w'
self.encoding = 'utf-8'
self.name = name
self.flush()
def isatty(self):
return False
def close(self):
# don't really close the handle, that would only cause problems
self.closed = True
def fileno(self):
return self._fileno
def flush(self):
if self._hConsole is None:
try:
self._stream.flush()
except Exception, e:
_complain("%s.flush: %r from %r"
% (self.name, e, self._stream))
raise
def write(self, text):
try:
if self._hConsole is None:
if isinstance(text, unicode):
text = text.encode('utf-8')
self._stream.write(text)
else:
if not isinstance(text, unicode):
text = str(text).decode('utf-8')
remaining = len(text)
while remaining > 0:
n = DWORD(0)
# There is a shorter-than-documented limitation on the
# length of the string passed to WriteConsoleW (see
# .
retval = WriteConsoleW(self._hConsole, text,
min(remaining, 10000),
byref(n), None)
if retval == 0 or n.value == 0:
raise IOError("WriteConsoleW returned %r, n.value = %r"
% (retval, n.value))
remaining -= n.value
if remaining == 0: break
text = text[n.value:]
except Exception, e:
_complain("%s.write: %r" % (self.name, e))
raise
def writelines(self, lines):
try:
for line in lines:
self.write(line)
except Exception, e:
_complain("%s.writelines: %r" % (self.name, e))
raise
if real_stdout:
sys.stdout = UnicodeOutput(hStdout, None, STDOUT_FILENO,
'<Unicode console stdout>')
else:
sys.stdout = UnicodeOutput(None, sys.stdout, old_stdout_fileno,
'<Unicode redirected stdout>')
if real_stderr:
sys.stderr = UnicodeOutput(hStderr, None, STDERR_FILENO,
'<Unicode console stderr>')
else:
sys.stderr = UnicodeOutput(None, sys.stderr, old_stderr_fileno,
'<Unicode redirected stderr>')
except Exception, e:
_complain("exception %r while fixing up sys.stdout and sys.stderr" % (e,))
# While we're at it, let's unmangle the command-line arguments:
# This works around .
GetCommandLineW = WINFUNCTYPE(LPWSTR)(("GetCommandLineW", windll.kernel32))
CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int)) \
(("CommandLineToArgvW", windll.shell32))
argc = c_int(0)
argv_unicode = CommandLineToArgvW(GetCommandLineW(), byref(argc))
argv = [argv_unicode[i].encode('utf-8') for i in xrange(0, argc.value)]
if not hasattr(sys, 'frozen'):
# If this is an executable produced by py2exe or bbfreeze, then it will
# have been invoked directly. Otherwise, unicode_argv[0] is the Python
# interpreter, so skip that.
argv = argv[1:]
# Also skip option arguments to the Python interpreter.
while len(argv) > 0:
arg = argv[0]
if not arg.startswith(u"-") or arg == u"-":
break
argv = argv[1:]
if arg == u'-m':
# sys.argv[0] should really be the absolute path of the module source,
# but never mind
break
if arg == u'-c':
argv[0] = u'-c'
break
# if you like:
sys.argv = argv
简单来说这段代码做了这么几个事:
1、如果输出到控制台,改用 WriteConsoleW()。
2、如果输出被重定向,用 utf-8 编码输出。
3、用 GetCommandLineW() 和 CommandLineToArgvW() 获取命令行参数,在最后一行取代 sys.argv 传入的参数。
这个是我目前能找到的最完美的解决方案了。在控制台下也能不出错,在重定向的时候也可以按 UTF-8 去编码成 char 字节流。唯一的问题是 Python 2.5.2 里似乎没有 LPVOID。我用 c_void_p 取代 LPVOID,似乎是可行的。
当然,它仍然有前述不可避免的问题。例如在非原生支持汉字的代码页(简936繁950日932韩949)下,光标和换行的位置会出问题。如 果对汉字显示有很高的要求,不妨调用 Windows API 设置一下控制台的代码页。此外,输出重定向到外围程序时,如果外围程序不能设置按 UTF-8 解码,就会看到乱码的问题也依然存在。这些问题,就留待读者自行解决吧。
最后,特别说明一下以上问题都是 Windows 平台限定的。Linux 下问题没有这么显著(现在的Linux发行版本多数都设置了默认代码页为 UTF-8),而且就算用户代码页不是 UTF-8,也没有 Windows 下 WriteConsoleW 这么淫霸的函数,所以洗洗睡吧。