字符编码(三)|与网页或文件交互时的编码问题

994 阅读10分钟
原文链接: zhuanlan.zhihu.com

本文在上文理论的基础上,列举一些实际使用时遇到的编码问题,分为读取网页和读写文件文件两个部分。因为本人遇到的问题可能只是冰山一角,欢迎读者将自己遇到的编码问题写在评论区,如果报错种类和文中列的不同,我会加入文章之中。

与网页交互时的报错或乱码

本节列一些我在爬网页时会遇到的乱码问题,基于requests库,分为如下内容

  • r.text乱码如何得到真正的字符
  • unicode式乱码
  • 特殊字符无法显示问题

1.r.text乱码如何得到真正的字符

举一个例子,访问知乎的一个错误界面,会提示“你似乎来到了没有知识存在的荒原…”,但抓取到的这个页面的信息是乱码了,正好作为一个实例展示在这里

获取网页信息的代码如下

import requests
url='http://www.zhihu.com/api/v4/people/b4471a5431e1f53a96fa06c5e721afd5'
ua = 'Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11'
headers = {'User-Agent':ua}
r = requests.get(url,headers = headers)

我们来看一下r.text的返回结果(只截取一个乱码部分)

<strong>ä½ ä¼¼ä¹Žæ¥åˆ°äº†æ²¡æœ‰çŸ¥è¯†å­˜åœ¨çš„è’åŽŸ...</strong>

接下来需要对r.text进行编码或解码来获取真正的内容。

首先要明确

  • requests从请求的结果提取内容有两种方法
    • r.text提取的是unicode字符串
    • r.content提取的是二进制数,即经过编码的内容
    • 其实抓取页面先得到的是r.content,r.text是经过解码得到的结果,r.text乱码说明解码方式错误
  • 这个网页题头标明的是UTF-8编码<meta charset="utf-8">

最开始什么都不知道,就随便试编码,因为是中文所以试试gbk

r.text.encode('gbk') # 报错
print(r.text.encode('utf-8').decode('gbk'))
# <strong>盲陆聽盲录录盲鹿聨忙聺楼氓聢掳盲潞聠忙虏隆忙聹聣莽聼楼猫炉聠氓颅聵氓聹篓莽職聞猫聧聮氓聨聼...</strong>

这样随便试是不行的

接下来进行分析每一步是什么样的过程

  • 首先明确网页题头<meta charset="utf-8">的作用是:拿到二进制数r.content,用这个编码来解码可以获取正确的文本字符
  • r.text使用的肯定不是’utf-8’对二进制数进行解码,所以解出来的结果是错误的,我们需要知道它是用的什么编码
  • requests库有两个可以查看编码的属性,用这个可以知道r.text到底是用的什么来解码

我们一条一条来看

第一,我们可以尝试不从r.text出发获取文本,而是从r.content出发

print(r.content.decode('utf-8'))
# <strong>你似乎来到了没有知识存在的荒原...</strong>

我们发现确实正常显示了

第二,查看requests的两个编码属性

r.encoding # 'ISO-8859-1'
r.apparent_encoding # 'utf-8'

我们可以理解成,第一个是当前r.text解码时使用的编码,第二个是应该使用的编码

接下来我们就可以利用这个编码配合r.text来获取正确文本了

print(r.text.encode('ISO-8859-1').decode('utf-8'))
# <strong>你似乎来到了没有知识存在的荒原...</strong>

正常显示了!

更通用的做法是下面两种

print(r.content.decode(r.apparent_encoding))
print(r.text.encode(r.encoding).decode(r.apparent_encoding))

2.unicode式乱码

这里也是爬取知乎的一个界面,这个界面是Json格式的数据,就不贴爬虫代码了,只看r.text返回结果即可

"headline": "\u7edf\u8ba1\u4e13\u4e1a\u5b66\u751f"

这是什么,unicode编码,我们之前见过这个形式,但是从来都是我手动输入,没有用程序获得过一个字符的unicode编码,而且unicode编码打印出来不应该显示的是字符本身吗,怎么会这样显示出来?

我们想一想,什么时候print结果会出现\n就明白了,是在print('\\n')的时候,所以说明这里是原始字符是\\u7edf\\u8ba1这样的形式。而这样的形式如何处理才能得到真正的字符呢?我们先铺垫一个用法

a = '中文'
a.encode('unicode-escape')
# b'\\u4e2d\\u6587'

这是一种编码方式,所以如果我们能得到b'\\u4e2d\\u6587'就可以通过b'\\u4e2d\\u6587'.decode('unicode-escape')获取中文字符了。

现在有的是'\\u4e2d\\u6587',不要把它看成特殊的编码,就看成最普通的字符,英文数字字符,全是收录在ASCII码中的通用字符,用任意一种编码方式都可以获得b'\\u4e2d\\u6587'(因为ASCII编码的通用性,’abc’.encode(‘ascii’)结果是b’abc’而不是16进制数,之前讲过的),所以这样的乱码就可以解决了

print(r.text.encode('utf-8').decode('unicode-escape'))
# "headline": "统计专业学生"

同样,我们也可以检查这个网页的两个编码,发现r.encoding是空值,r.apparent_encoding是ascii。r.contentr.text结果是一样的,是unicode格式的数据。

3.特殊字符无法显示问题

这个问题比如在jupyter中写代码运行再print,是不会出现错误的,只有在.py文件中写好代码,在cmd中运行,将结果输出到cmd页面上才有可能发生。比如最简单的豆瓣top250的所有数据就不能全部打印到cmd页面上

在1.py文件中输入

import requests
url = 'https://movie.douban.com/top250'
r = requests.get(url)
print(r.text)

在命令行中输入python 1.py运行,会报如下错误

Traceback (most recent call last):
  File "live.py", line 119, in <module>
    print(r.text)
UnicodeEncodeError: 'gbk' codec can't encode character '\xee' in position 20554: illegal multibyte sequence

这个问题我们之前说过的,就是因为命令行下的代码页是chcp 936,会默认进行一步GBK编码的转换,而网页源码中有的字符没有被GBK收录,于是报错(类似&nbsp这样的字符)。当时我们的处理办法是将命令行改为chcp 65001就可以正常显示,这里也是这样。不过还有另外一种处理方式,这里主要讲这种方式

当这个不被识别的字符不重要时,就像豆瓣中的这个字符可能代表的是空格之类的东西,我们可以直接抛弃掉他们,从而将其他识别的字符放在cmd中显示。

print(r.text.encode('gbk', errors='ignore').decode('gbk'))

通过指定errors参数为’ignore’可以忽视无法被编码成GBK的字符,然后再重新用GBK解码,留下的就是能被识别的字符,重要的内容也都没有损失。

上面的例子可能不太直观,我们可以拿一个更直观的例子来看(下面的例子和上面略有区别,是在解码时ignore,但是思想是一样的)

>>> a = '中文'
>>> a.encode('gbk')
b'\xd6\xd0\xce\xc4'

>>> b = b'\xd6\xd0\xce'
>>> b.decode('gbk')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'gbk' codec can't decode byte 0xce in position 2: incomplete multibyte sequence
>>> b.decode('gbk', errors = 'ignore')
'中'
>>> b.decode('gbk', errors = 'ignore').encode('gbk')
b'\xd6\xd0'

在给b赋值时故意截取a的一部分,最后的\xce就无法被gbk识别,在解码时报错,通过ignore可以正确解码,发现多余的那个\xce被去掉了。

与文件交互时的报错或乱码

python中读写文件的基础内容网上有很多教程,这里就不赘述了,这里主要讲和编码相关的部分。

本文分为如下内容

  • 写入(write)文件的基础
    • 用字符写入文件默认使用的编码
    • 指定编码写入字符
    • 用二进制数写入文件的编码
  • 读取(read)文件的基础
    • 用字符读取文件默认使用的编码
    • 指定编码读取字符
    • 用二进制数读取文件的编码
  • 带BOM的问题

写入文件的基础

1.用字符默认写入文件使用的编码

在1.py文件中写入

a = '中文'
with open('1.txt', 'w') as f:
    f.write(a)

在命令行中执行python 1.py,用sublime打开默认是乱码,reload with encoding选择GBK则正常显示,说明python默认写入文件使用的编码是GBK。

.指定编码写入字符将1.py文件内容换为(不用把1.txt删掉,默认就是新文件替换旧的)

a = '中文'
with open('1.txt', 'w', encoding='utf-8') as f:
    f.write(a)

这时1.txt文件用utf-8打开正常显示

3.用二进制数写入文件

将1.py文件内容换为(不用把1.txt删掉,默认就是新文件替换旧的)

a = '中文'
with open('1.txt', 'wb') as f:
    f.write(a.encode('utf-8'))

这时1.txt文件用utf-8打开正常显示。

这里注意几点细节

  • 用二进制数open时参数要用'wb'
  • 将字符编码为utf-8的二进制数,不用再在open中指定编码
  • 使用二进制时也不允许加encoding参数,否则报错ValueError: binary mode doesn't take an encoding argument
  • 所以说你用什么编码来encode,文件打开时就要用什么编码来解码才能正确显示字符

读取文件的基础

这里我们直接使用上面的用utf-8编码的1.txt文件

1.用字符读取文件默认使用的编码

读取文件时如果不指定,默认编码也是GBK,所以直接读取上面的UTF-8文件会报错。因为UTF-8编码出来的二进制数中一些在GBK中是不对应字符的(如果对应字符应该也不是UTF-8中对应的字符,这种情况下会乱码)。

下面读取代码会报错

with open('1.txt', 'r') as f:
    a = f.read()

print(a)

2.指定编码读取字符

也是使用encoding参数

with open('1.txt', 'r', encoding = 'utf-8') as f:
    a = f.read()

print(a)

这样是可以正常显示的

3.用二进制数读取文件的编码

读取代码如下

with open('1.txt', 'rb') as f:
    a = f.read()

print(a.decode('utf-8'))

结果正常显示

总结:从上面基础内容来看,python中读写文件使用的字符和二进制数都和我们之前讲到的encode和decode对应非常好。如果有报错问题一定是编码对应错误,理解背后的编码转换原理,思路清晰就不会有问题。做到两点就好

  • 如果要写入字符(unicode),保证字符print时正常显示
  • 如果要写入二进制数,保证知道二进制数是用什么编码得到的,能正确解码即可

这部分我发现讲完基础就没什么好讲的了,读者可以用上面的爬虫代码,将获取的文本写入文件,看能否正常显示中文,来确保自己完全掌握了这部分内容。

带BOM的问题

有一个比较特殊的事情有必要在这里说明一下,就是一个文件如果用带有BOM的UTF-8保存,读取时前面加的这个BOM也会读取到。

比如我们在记事本中写入“中文”二字,保存时使用UTF-8(记事本中的UTF-8就是带BOM的UTF-8)。然后我们用程序读取这个文件

with open('2.txt', 'r', encoding='utf-8') as f:
    a = f.read()

print(a)

在cmd中运行,如果是chcp936环境下,就直接报错,错误和之前提到很多次的一样。

如果是chcp65001环境下就会显示下面这个

复制到jupyter中是这样的(前面多了一个红色的点)

(无奈知乎也显示不出来,只能放图片了)

前面多了一个符号,这就是BOM的副作用。因为它无法被GBK编码,所以可以用前面提到的GBK配合ignore方法去掉它。

专栏信息

专栏主页:python编程

专栏目录:目录

版本说明:软件及包版本说明