node抓取网页数据乱码

一、起因

抓取网页数据学习,使用node利用axios发起网络请求抓取数据时,出现以下乱码:

 

 可以看到英文格式正确显示,中文格式乱码,这个问题感觉很是常见,无非是编码问题。

 

二、开始着手解决

初步:

首先查看了一下我要抓取的网页的源代码:

 

 再查看了一下axios的官方文档:

 

 其默认解码是utf8,那么改一下这个解码的类型变为gbk总行了吧,但是试了一下,发现无果,还是乱码:

async function get_test(id) {
    return await new Promise(async(resolve)=>{
      let res = await axios.request(
        url='https://www.szxuexiao.com/Examination/html/' + id + '.html',
        headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/18.17763"},
        responseType= 'text', 

        // `responseEncoding` indicates encoding to use for decoding responses
        // Note: Ignored for `responseType` of 'stream' or client-side requests
        responseEncoding='gbk', // 修改解码类型为gbk
      );
    resolve(res.data)
    })
}

 

自己的理解:

猜想1:网上说nodejs是解析不了gbk编码的,且nodejs原本就是默认utf8解析的,所以我直接返回了gbk编码,nodejs还是使用utf8解析,所以乱码;

猜想2:可能让axios指定了gbk编码,但是可能axios也不认识gbk,所以返回的还是乱码。

当然以上都是我的猜想。

 

为了进一步验证我的猜想,我在nodejs使用了request来代替axios发起请求:

 

async function getSome(){
 return await new Promise((resolve)=>{
  Request.get({url,headers},(err,status,resData)=>{
    resolve(resData)
    // console.log(resData)
    return resData;
  })
  })
}
Request.encoding = 'gbk';//设置以gbk方式解码
let a = getSome()
a.then((html)=>{
  console.log(html)
})

 

 

 对比了我使用了request与axios指定编码格式,但是输出的还是乱码!但是我请求的页面的确是以gbk方式编码的,所以用gbk方式解码应该没有问题才对!

对比axios与request输出同一个文档的title看看有没有差异!

axios设置了以gbk编码方式解码输出:

�˽̰�2014-2015ѧ��Сѧ���꼶������ѧ�����п����Ծ�������ҳ��

axios设置了以UTF-8编码方式解码输出:

�˽̰�2014-2015ѧ��Сѧ���꼶������ѧ�����п����Ծ�������ҳ��

request设置了gbk编码方式解码输出:

�˽̰�2014-2015ѧ��Сѧ���꼶������ѧ�����п����Ծ�������ҳ��

request设置了UTF-8编码方式解码输出:

�˽̰�2014-2015ѧ��Сѧ���꼶������ѧ�����п����Ծ�������ҳ��

总结:

对比以上两个不同的库,无论以gbk解码还是UTF-8解码,都一样的结果!所以说axios与request的gbk,UTF-8编码方式解码的结论是一样的,所以问题不出在这些解码工具包身上,那么会不会出现在nodejs平台这个语言身上呢

然后看了网上给的资料:

 

 

所以我的进一步猜想是:axios与request的包是符合nodejs平台规定的,所以他们在这个平台上面其实也是 不能用gbk编码方式解码的,他们的解码方式应该和nodejs原生方式一样。

 

接着进行这个猜想,我进一步实验,我换个平台,使用Python下的request库,试试:

我首先使用的是 UTF-8格式看看我的解码情况。

为什么要首先使用UTF-8呢,原因如下:

(1)试试看在Python平台下用UTF-8解码的结果和用nodejs平台下的UTF-8的解码结果是否一致?

 

 同样的title输出

�˽̰�2014-2015ѧ��Сѧ���꼶������ѧ�����п����Ծ�������ҳ��_Сѧ�Ծ�


发现结果与上面的输出结果一致!

然后在试试GBK格式解码:

 输出结果:真爽!

 

 所以我的结论应该是:nodejs使用这些axios与request库的编码方式解码与原生nodejs是一样的,我还以为这些库有所提高,赋予了nodejs解码gbk的能力,事实证明这些网络请求并没与赋予nodejs解码以gbk方式编码的内容!

后话:

 本来我想试一下axios在Python下的表现如何,但是Python好像没有这个库

----这段话是后来我写完写下的,你可以提取看------

PS:写这篇文章花了我一天,因为我用了一天的时间去了解:编码解码这个知识点,只是这点我以前不在意,
最开始接触乱码是在我大一,学校在教JSP技术,也就是大一的时候,当时只认识乱码问题是因为不同编码方式,并没与想太多,后来没有继续学习JSP,
因为总觉得这东西写个网页太慢了,偏题了,回到正题。


首先乱码问题是促使我去解决的,从而我认识到了编码解码,但是我一开始的理解是,服务端设置了编码方式,发给浏览器,通过<meta http-equiv="content-type" content="text/html; charset=gbk">
告知浏览器进行解析方式为:html,且它的编码方式是gbk,告诉浏览器以gbk方式解码,这个理解是没错的。

但是当我使用get请求时,我发现竟然使用这些网络请求模块默认都会使用encoding,也就是对我们请求到的数据再encoding,可以看看官方文档:

 


 当时只看到了个responseEncoding:‘utf8’//default,没有认真仔细看这些注释,认为encoding就是继续编码


因为encoding的英语含义是编码,为什么还要对请求到的数据编码呢?服务器已经编码一次了,
我们此时编码,不是造成了二次编码吗,这样还解码个屁啊,当时越想越不爽,什么傻逼逻辑,就感觉好气。但是我继续顺着他的意思,觉得要编码就再编码
一次吧,到时候再解码。为什么存在这个思路呢,你等下看到我介绍的两个模块就知道了,有一个叫iconv的模块,里面有个decode方法,用来解码的,
看起来见名知意,decode嘛,那就是解码,我刚才encoding了现在就decode;

所以我又开始了我的猜想,觉得服务器端发给我们数据时,已经encoding了,当我们使用网络请求的时候,他会默认再encoding一遍,那现在我有个模块(下面会介绍),
并且这模块有个decode的方法属性,那么我就可以decode了,也就是decode两次不就好了吗,后来实验多次,都是乱码,并且后来直接全都变成问号了。

现在解释我的思路为什么会这样,因为encoding与decode本来就是反义词,但是它们再干的都是一件事,那就是根局请求到的数据,对请求数据解码,
并没有编码之说,这两个东西都在解码,为什么要用意义相反的名称,现在还是会觉得不爽,因为我觉得这个是导致我用了一天宝贵时间铺在它身上的原因,

现在画一下图,记录一下我当时的错误思维:
图1:思维较正确:

 


 图2:所以我觉得我抓取数据,那么首先都知道要假装自己是浏览器,所以在请求体加上user-Agent:模拟浏览器的信息,企图骗过网页。所以有了以下思维:

 

 所以一直试错了一整天:

试错的错误思维原因:
(1)认为encoding和decode是两个不同的东西,执行不同的操作;
(2)没有认真理解nodejs本身的可解码范围;
(3)没有好好看文档
(4)英语语义上的相反误导了我

然而仔细看看上面提到的request与axios都可以看到注释清晰的语义:

request:

axios:


 

 

   responEncoding:表示用于解码响应体的编码

注意:它会忽略stream和客户端原本的请求(的编码)。这个stream意思是流,但是什么是流的请求体,我还不懂><!

也就是官方文档都说了encoding是用来解码的,但是我根据它的意思,以为它是用来编码的。


----上面这段话是后来我写完写下的,------



接着下一步:

 既然我返回的编码格式只要不是utf-8,那么nodejs就不认识,那么我返回二进制呢,不是说所有机器都认识二进制吗,返回二进制总行了吧,然后再使用其他包将二进制转码行了吧?但是原生js设计的时候根本没有想到二进制;而nodejs有个buffer的概念,专门来处理二进制的,但是如果把它转成二进制,由于gbk编码的二进制与utf8编码的二进制是不一样的,也就是:比如 "中国" 这两个字,gb2312码是 D6 D0 B9 FA,GBK码是4E 2D 56 FD,utf-8是 E4 B8 AD E5 9;所以说解析成了二进制也没办法获取想要的内容,因为nodejs本身就不认识gbk编码格式,转换成了二进制,它能对应的还是utf-8的编码表,翻译后还是错误的结果!

 

所以根据网上提示,推荐了两个node模块(这两个东西认识很多编码,那么gbk编码当然不在话下):

 

var iconv = require('iconv-lite')
var encodingConvert   = require('encoding');
模块解释:
(1)iconv
iconv:是在类Unix系统中一种标准字符集转换接口,用于在不同字符集编码之间进行转换,最早出现在HP-UX系统中。
 (2)iconv-lite
1. 安装 
npm install iconv-lite
iconv-lite:是iconv的纯js实现,支持的编码包括node.js原生编码:utf8, ucs2, ascii, binary, base64;同时支持广泛使用的单字节编码:Windows 125x family, ISO-8859 family, IBM/DOS codepages, Macintosh family, KOI8 family, latin1, us-ascii;多字节编码:gbk, gb2313, Big5, cp950。官方宣称比node-iconv更快。
2.使用:
iconv.decode('数据', 'gb2312');//将数据编码为gb23312格式

例子:
let data = iconv.decode('��0�2014-2015g��!g���6������g�����?����>����������','gbk')
console.log(data)

 至于为什么上面的乱码解码后还是乱码,原因是我的字符串本身是GBK格式,然后通过nodejs的UTF-8编码格式解码的帮助下,帮我转换成这个鬼样子,所以当然用GBK解码不回来了,

因为它现在既不是UTF-8,也不是GBK.

 虽然运行结果可以看到,直接传入字符串这种方法是被遗弃的,虽然它还是输出了结果,但是官方并不推荐我们使用这个格式:下面是官方的话:

 

 

 

 

 翻译一下(网页翻译有时候表达的不太准确,一切以官网为准):

 

 

 

 

 上面方法我自己的理解是,首先使用了一个http模块,然后使用里面的res.on方法监听数据,监听到数据就把它push进数组,chunk其实是一个数据块,计算机原理可以搜一下chunk的概念,数据监听完毕后使用concat把字符数组连接为字符串,并转成buffer类型,然后开始编码成win1252格式。

 

全文通读下来,作者告诉我们两件事:

第一:使用iconv时,推荐传入buffer数据类型,因为使用buffer数据解码效率高,因为有buffer缓冲区;

第二:使用iconv时,直接传入数据就好,然后指定解码的格式就好(此处我觉得他的decode方法并不是在解码,而是在转换编码,尽管它称为解码,但是应该叫转换编码更为贴切),

传入decode的参数buffer不需要再传入编码格式:

例子:

iconv.decode(new Buffer(body, 'utf8'), 'win1252')//这种是错误的,它指定把body数据字符串编码为utf8的buffer格式,然后在转换成win1252;想法是对的,但是内部

nodejs已经帮助我们进行这个编码操作了,不需要再指定buffer编码格式了;

 

iconv.decode(Buffer.concat(chunks), 'win1252');//这个为正确操作

let body = Buffer.concat(chunks)//这个为正确操作

iconv.decode(body'win1252');

 
(3)encoding
encoding:是对node-iconv和iconv-lite的再次封装,encoding首先调用node-iconv,如果node-iconv无法解析,则调用iconv-lite作为替代方案。
注:encoding解析出来的为buffer类型,你可以理解为nodejs为处理二进制流而定义的数据类型,即buffer。

encoding 安装使用

1. 安装 

npm install encoding

2.使用

encoding 模块就一个方法 convert(),使用方法为:encoding.convert(text, toCharset, fromCharset)。

text: 需要转换的对象,可以为 Buffer 或者 String 对象。
toCharset: 转换后的编码。
fromCharset: 转换前的编码,缺省为 uft8。

例子:
html = encodingConvert.convert('��0�2014-2015g��!g���6������g�����?����>����������', "utf-8","GBK");
输出:
html = encodingConvert.convert('��0�2014-2015g��!g���6������g�����?����>����������', "GBK","UTF8");
输出:buffer(nodejs不认识二进制,它认识buffer,因为这个是他定义来代替二进制的)

Buffer 介绍
在客户端javascript脚本代码中,对于二进制数据并没有提供一个很好的支持。然后在nodejs中需要处理像TCP流或文件流时,必须要处理二进制数据。因此在node.js中,定义了一个Buffer类,该类用来创建一个专门存放二进制数据的缓存区。  

一句话概括: Buffer 类是一个全局变量,用于直接处理二进制数据,提供工具类方法。
官网上关于 Buffer 解释的非常清楚。可以去nodejs官网查看更加详细的介绍。

 

三、解决方案

首先:本来是不用这些字符编码模块的,直接在网络请求时设置解码格式,我们就能得到正确数据,但是偏偏原生nodejs不认识GBK编码,导致axios与request设置了GBK编码方式解码还是无效,大概示意图如下:

 

 

 

 

所以接下来使用上面两个模块中的iconv模块解决问题:

方案1:

request+iconv(request可以指定不对服务器返回的数据进行解码,所以我们不设置解码,因为即使他解码了,nodejs也看不懂)

(1)使用request不解码,直接把数据发出去交给iconv处理

async function get_test(id) {

  return await new Promise(async (resolve) => {
  await request({
      url: "https://www.szxuexiao.com/Examination/html/"+id+'.html', 
      headers:headers,
      encoding: null  // 关键代码,设置请求数据不编码
    },(err,status,resData)=>{
      resolve(resData)
      return resData;
    });
  })
}
(2)iconv处理buffer流,并且把buffer流翻译成gbk,再转换成字符串输出:
a.then(html=>{ console.log(html) var html = iconv.decode(html, 'gbk')//将请求数据编码为gbk })

 

 示意图:

 

 本来想给一下axios的处理方案,但是它官方并没有给出可以设置编码格式为null,即不编码,大概是它认为它可以处理大部分的编码,而忽略了nodejs这种只能处理少量编码的特性了吧。所以axios请求的数据返回给我们一定是经过解码处理的的,所以不管我们对axios返回的数据再进行任何解码都会造成二次解码,数据乱码,所以此处无法给出axios的解决方案。

方案2:和官网一样使用http,不过我爬的是htpps的网页,所以使用https

首先下载直接
npm install http --save
或者
npm install https --save
let https = require('https')
var iconv = require('iconv-lite')
let url = 'https://www.szxuexiao.com/Examination/html/4484.html'
https.get(url, function(res) {
  var chunks = [];

  res.on('data', function(chunk) {
    chunks.push(chunk);
  });

  res.on('end', function(res) {
    // 将二进制数据解码成 gb2312 编码数据
    var data = iconv.decode(Buffer.concat(chunks), 'gb2312');
      console.log(data)  
    
  });
});

 此处不用encoding模块是因为其如果接受的数据为buffer或字符串,但是输出的仍然是buffer不是字符串,不是我们要的数据。

posted @ 2021-03-16 20:30  兜里还剩五块出头  阅读(1230)  评论(1)    收藏  举报