Python3中使用Requests和BeaitfulSoup的编码问题

写在前面的话:

学习Python有一段时间了,但是一直没有太多的实战,前期的学习主要是看买的电子书 Python学习手册 (额,刚刚看了一下,这边书的电子书居然已?下架,但是我基本确定我买的就是这个,电子书和实体的特点一样:几乎都是同类书中最贵的,当时真的是买的很心痛啊!),看了不到30%,发现这本说还是有点门槛的,于是就看了一些了零基础的视频,还有比较经典的廖雪峰的Python3教程, 还有对应的视频教程哦 (重点:记得赞赏作者廖雪峰啊!)。总之一句话:找了非常多资料,看了一些书。然并卵,心里就是没底,那就实战吧!本文就是实践中遇到的第一个比较棘手的问题,以及解决方案。

问题源码:

#!/usr/bin/env python3      # 对Windows无用,可直接在UNIX内核系统运行(Mac OS, Linux...)
# -*- coding: utf-8 -*-     # 告诉调用对象,程序是用UTF-8编码的


import requests             # 导入第三方库
from bs4 import BeautifulSoup


def get_BOC_data(url):      # 封装为函数
    headers = {"User-Agent": "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Maxthon 2.0)",
               "Referer": "http://www.boc.cn/"}     # 模拟浏览器请求头信息
    proxies = {"http": "218.76.106.78:3128"}        # 设置代理IP,不稳定,随时可能失效
    response = requests.get(url, headers=headers, proxies=proxies)     #调用requsts.get()方法
    print(response.status_code, "\n", rsponse.url)     # 打印状态码以及请求的URL
    soup = BeautifulSoup(response.text, "lxml")        # 调用BeautifulSoup,解析网页数据
    print(soup.prettify())    # 格式化打印soup对象
    table_data = soup.select("body div.publish table")[0]     #调用soup对象支持CSS selectors
    for row in table_data.find_all("tr"):     # 直接打印表格数据
        for col in row.find_all("td"):      # 使用soup最常用的find_all()方法
            print(col.string, end="\t")
        print("\n")


if __name__ == "__main__":      # 好像是测试用的,具体原理暂时没去弄很清楚、
    start_url = "http://www.bankofchina.com/sourcedb/ffx/index.html"    #初始目标URL
    get_BOC_data(start_url)     # 调用函数

问题描述:

代码背景说明

我之前呆的公司是做金融数据的,获取数据的方式一般是:爬虫(大量规则的数据)、人工采集(很多数据采集员,采集不规则少量非常重要 的数据)、买。我开始做的就是数据采集员,非常枯燥,但不断学习,后来就做了质检,最后离开的时候数据策划分析员。由于那段经历,我就想学爬虫,提高效率。本次抓取的数据就是当时自己手动复制粘贴过的数据:中国银行远期结售汇牌价

主要问题:

直接打印的数据出现大量乱码,基本无法使用:

澳大利亚元 AUD 五个月 521.1877 530.3753 525.7815 2017-03-13

澳大利亚元 AUD 六个月 521.7953 530.9693 526.3823 2017-03-13

澳大利亚元 AUD 七个月 522.4445 531.8255 527.135 2017-03-13

次要问题(上面的代码中已经解决)
  1. 防爬虫常用三个技巧中的两个模拟浏览器请求、设置代理ip。由于这个暂时只是单页爬区单次请求,就没有导入time模块来降低请求频率
  2. soup对象的解析问题,这个弄了很久,用了各种方法都没有达到想要的数据,一开始还以为是我暂时不会的JS加载问题,差点放弃,后来发现是参数设置的问题,浏览器直接调用出的CSS selector不适用,分析浏览器解析的HTML,调整了一下,还是不行。然后打印soup对象解析的HTML:print(soup.prettify()),发现两者还是有微小的差别的,再做对比调整,最后成功了!(这里就不具体说明了,但个人总结就是:多对比,多分析,多调整....囧囧的)

问题分析:

问题分析说明
  1. 上述问题中,简单的防爬虫多写几次就基本没问题了(复杂一点比如建立自己的免费ip池,网上有其他资源,需要再花时间学习),soup对象解析的问题,个人认为还是有点麻烦的,首先你需要学习一些 HTML/CSS 的基本知识,我主要学习的地址:W3School 以及菜鸟教程。基础了解了,就能基本看懂网页的HTML了。
  2. 对于数据处理来说编码问题是数据处理绕不过去的问题,只有涉及到数据读取、交换、储存等环节都会涉及到编码问题。特别是互联网上的数据,互联网发明、兴起于西方,刚开始用的都是:Ascii、Latin-1等,但是这些只是用与字母编码,对于亚洲很多国家的表意文字,统统不适用。于是中国人开发了自己的编码体系:GB系列。但是中国常用的GBK和Ascii并不兼容,由此乱码问题就出现了。为了解决这个全球性的问题,于是就诞生了:Unicode编码统一体系(Ascii\Latin-1\UTF-8\unicode等...都属于这个统一编码体系)。Unicode体系的建立从根本很上解决了全球问题系统的编码问题,也就是说Unicode也可用于表意文字的编码。那么为什么还是会有乱码问题呢?这个和我们工作中会遇到的一种问题非常像,就叫:历史遗留问题。历史问题,就是时间问题,时间问题太大了,暂时没能力讨论,我们暂时就事论事:计算机传遍世界后Unicode出来之前这个空档期各国用自己编码体系保存了大量的资源,以及大量软件都是基于各种的编码,一是量太大,二是改变的意愿不强烈,三是改变不划算(技术问题)。其中不划算的问题随着 UTF-8 的流行基本得到解决。量的问题和情感的问题需要时间。总结来说:Unicode由于种种原因没有完全取代其他编码体系,因此乱码问题暂时会继续存在……
实际分析过程
查资料

好吧,我承认第一次独自遇到这种情况,我是懵逼的,之前看视频时,里面老师偶尔遇到编码问题是各种:decode(), encode(), encoding=, decoding= 用得飞起,以为很简单,以为自己弄懂了。完全不是那么回事,我连编码什么意思都不懂,立马Google一下:编码,维基百科的解释比较学术一点,但还是帮我建立了基本认知。之后结合这个实际问题再次看了一遍廖雪峰教程中关于编码的讲解,好像又多理解了一点。之后就是Google一下:Requests和BeautifulSoup编码问题,找了几篇对我处理该问题帮助非常大的文章(具体链接见文末)。这里贴出帮助最大的,但也许很多人都知道的话;对我理解问题的本质却很重要的作用,Python学习手册 这本书上看到的:

编码是根据一个想要的编码名称,把一个字符串翻译为其原始字节形式。

解码是根据其编码名称,把一个原始字节翻译为字符串形式的过程。

说一下我对这两句话的英文理解,或者说代码理解:

第一个编码是动词 encode, 第二个编码名称是名词 encoding, 代码表示为 str.encode(encoding=" "), 第二句也就是第一句的逆过程,代码表示 bytes.decode(decoding=" "); 其中str表示字符串对象,bytes表示二进制对象

截取上面的代码
response = requests.get(url, headers=headers, proxies=proxies)
soup = BeautifulSoup(response.text, "lxml")
print(soup.prettify())
解析数据交换过程

这几行代码数据的交换过程(为了突出编码问题,一下是根据个人理解写的简化版。真正互联网数据传输很复杂,可以参考 HTTP协议,以及更基础的 TCP/IP协议)

  1. 爬虫程序通过requests对象向指定服务器地址发送访问请求,服务器验证通过后向爬虫程序发送爬虫程序请求的数据(这是第一次数据交换,服务器把已经以某种特定的编码(encoding)编码(encode)过的二进制数据,发送给爬虫程序。
  2. 爬虫程序通过reqeusts接受数据后,赋值给了response变量,
  3. 通过语句 response.content 可以得到这些原始的二进制数据;如果想得到字符串,就先需要把这些数据以某种特定编码(decoding)解码(decode)为常规字符串;然后再按照指定的编码(encoding)编码(encode),最后再根据这个编码(encoding)解码(decode),通过语句 response.text 就可以完成这个解码-编码-再解码的过程(内部封装好的,直接调用相应语句就行,这也是requests强大的地方啊!)
  4. response变量的数据经过解析之后就被赋值给了soup变量
  5. 通过语句soup.encode() 可以查看二进制数据,soup.decode() 可以查看字符串数据,soup.prettify()某种编码解码后格式化答应数据
编码和解码的原则

数据是如何编码(encode)的,最终就需要以相同(或者相互兼容)的方式解码(decode)。为什么最终呢?最终的意思就是可能由于失误或者其他的一些原因导致第一次解码(decode)用的编码(encoding)和编码(encode)时的编码(encoding)不一致,导致乱码出现,如果再以相同的编码完全逆序编码一次,在解码一次,就可以还原。举个例子:

>>> s = "SacrÃ"         # 定义变量s,并把字符串 "SacrÃ" 赋值给s (注意:此时这个符号"Ã",并非是乱码,而是葡萄牙语的一个字母)
>>> s.encode("utf-8")       # 以"utf-8"进行编码
b'Sacr\xc3\x83'         # 返回二进制(十六进制的形式表示的,字母还是用对应的Ascii字母表示)
>>> s.encode("utf-8").decode("Latin-1")             # 再以"Latin-1"进行解码("西欧语系的编码")
'SacrÃ\x83'             # 由于编码和解码的方式不一致导致出现乱码
>>> s.encode("utf-8").decode("Latin-1").encode("Latin-1")           # 再用"Latin-1"进行编码
b'Sacr\xc3\x83'         # 返回二进制
>>> s.encode("utf-8").decode("Latin-1").encode("Latin-1").decode("utf-8")       # 最后再用 "utf-8"进行解码
'SacrÃ'                 # 完璧归赵!

对编码原则了解后,再来分析数据交换的过程,就能发现问题了:

  • 首先爬虫需要知道服务器发送过来的二进制数据使用什么方式编码的,才能有效的解码
    • 服务器返回的 Headers(头部信息)中一个字段 Content-Type 一般包含数据的编码方式,例如 Content-Type:text/html; charset=UTF-8 这就表明服务器发送的数据已utf-8编码。
    • 但是现在的问题是:目前有一些不规范的网站返回的头部文件中没有包含编码信息,此时requests就无法解析出编码,然后就调用程序默认的编码方式:"Latin-1",为什么是它?某个传输协议上这么规定的,开发requests库的程序员就这么写了,也没错,但就是在中国很不实用。这次编码问题就是由这个引起的
  • 解决方案
    • 发现乱码后,先通过 print(response.encoding) 查看requests使用什么方式解码的,在调用 response.content,通过正则找到类似这样的字符串:<meta http-equiv="content-type" content="text/html;charset=utf-8">\n,其中 charset=utf-8就表明了数据的编码方式。知道编码方式后通过添加语句: response.encoding = "utf-8" (指定编码方式)。正确解析之后,后面的数据交换过程就不会问题了,为什么呢?因为Requests默认的输出方式是unicode(当然你也可以修改,通过:先编码再解码的方式如: response.text.encode("gbk").decode("gbk"),但是一般情况下没必要), BeautifulSoup一般可以有效解析出传入其中的编码方式(如果传入的数据本身是字符串,那就不需要解码,如果是某种二进制编码,如果是你本生就知道的编码方式,你可以在在解析是传入原始编码方式,如: soup = BeautifulSoup(b'\x34\xa4\x3f', from_encoding="utf-8", "lxml"),并默认输出为utf-8编码格式(unicode和utf-8兼容),(默认输出的是utf-8,但是如果你想输出其它格式,你也可直接 soup.decode(),当然这个也是没必要的)。关键在第一步!后面直接默认就行。
    • 或者直接通过浏览器查看网页源码,查看HTML网页的头部<meta....>,就可以看到上面的信息了,标签的内容不多,所以可以很快查看到。
  • 讨论一下其他情况(也是在中国比较常见的问题)
    • 看到博客发现其它人出现这样的情况:服务器发送的原始编码是gbk或者gb2321等非unicode体系编码,通过设置之后requests可以正确解析代码,但是最后,soup 输出的格式依然乱码,我觉得不应该,我自己试了一下,并没有出现乱码。(可能和相关程序的版本有关,我用的是: Win10-32bit, Python3)
    • 国内情况比较复杂,因此还有一种更奇葩的情况:服务器返回的头部中或者HTML头部中有指名数据编码形式,但是:都是错的!,想想就可怕,但是确实有这种情况,如:趣彩网 ,服务器返回的头部中没有标明编码,但是你发现HTML头部中有 charset=gb2312 此时非常开心,然后设置编码 response.encoding="gb2312", 但是乱码还是如约而至,我就又开始怀疑自己了,于是查资料,发现它提供的编码可能不正确的这种可能性,越是就尝试着设置 `response.encoding="utf-8",奇迹出现了,乱码消失!,但是问题是我也是碰运气的啊,可能有能检测的方法,于是查资料发现:有两种方法可以判断原始二进制编码格式
# 第三方库bs4中模块可以再不调用BeatifulSoup的情况下检测二进制编码方式。
from bs4 import UnicodeDammit
dammit = UnicodeDammit(r.content)   #注意:这个需要传入的是二进制原始数据,不能传入字符串
print(dammit.original_encoding)
输出结果为:Some characters could not be decoded, and were replaced with REPLACEMENT CHARACTER.
gb2312      #识别正确
# 专门的编码检测库chardet,好像很牛的样子,我们来试一试:
import chardet
print(chardet.detect(r.content))    # 注意:传入字符串会报错:ValueError: Expected a bytes object, not a unicode object,也就是很上面UnicodeDammit一样,只能传入bytes数据。
输出结果为:{'encoding': 'GB2312', 'confidence': 0.99}          # 真的很棒

总结

#!/usr/bin/env python3
# -*- coding: utf-8 -*-


import pymysql
import requests
from bs4 import BeautifulSoup


def conn_to_mysql():    # 通过函数建立数据库连接
    connection = pymysql.connect(host="localhost", user="root", passwd="521513",
                                 db="spider_data", port=3306, charset="UTF8")
    cursor = connection.cursor()
    return connection, cursor


def get_boc_data(url):  # 抓取单页数据,并输出到MySQL
    headers = {"User-Agent": "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Maxthon 2.0)",
               "Referer": "http://www.boc.cn/"}
    proxies = {"http": "218.76.106.78:3128"}
    r = requests.get(url, headers=headers)
    r.encoding = "utf-8"
    print(r.status_code, "\n", r.url)
    soup = BeautifulSoup(r.text, "lxml")
    table_data = soup.select("body div.publish table")[0]
    conn, cur = conn_to_mysql()
    for row in table_data.find_all("tr"):
        boc_data = []
        for col in row.find_all("td"):
            boc_data.append(col.text)
        print(tuple(boc_data))
        if len(boc_data) == 7:
            sql = "INSERT INTO boc_data_2(cur_name, cur_id, tra_date, bid_price, off_price,mid_price, date) " \
                "values('%s', '%s', '%s', '%s', '%s', '%s', '%s')" % tuple(boc_data)
            cur.execute(sql)
        else:
            pass
    conn.commit()
    conn.close()


def get_all_page_data(pages):   # 下载多页数据
    start_url = "http://www.bankofchina.com/sourcedb/ffx/index.html"
    get_boc_data(start_url)
    for page in range(1, pages):
        new_url = "http://www.bankofchina.com/sourcedb/ffx/index_" + str(page) + ".html"
        get_boc_data(new_url)


if __name__ == "__main__":
    get_all_page_data(4)

好吧,第一次,写这样的玩意儿,用尽了洪荒之力(这个是真的),以前没接触过,因此花了大量的时间查资料,这里就没有一一列出来。这里主要希望通过这个具体的问题,把编码问题基本弄清楚,也算是入门吧,但是编码问题还有想多需要了解的地方!自己对自己的一次记录吧,也是第一次用MARKDOWN写东西,慢慢来吧。


posted @ 2017-03-18 01:27  佐罗罗  阅读(1712)  评论(0编辑  收藏  举报