class06:node获取数据

一、node获取数据-爬虫

1. 问题分析

第三节中我们可以在自己的后端设置跨域而访问数据,如果我们要获取别人的数据呢?

比如我们打开baidu搜索图片:
在这里插入图片描述

通过上图我们可以获得该页面图片的源码请求URL,复制粘贴该url到浏览器搜索得到源码:

在这里插入图片描述

我们可以随便选择一张图片的地址,并复制该地址在浏览器中搜索查看:

图片地址在预览中的位置:data -> xx(数字下标) -> replaceUrl -> 0 -> ObjURL
在这里插入图片描述

复制该地址在浏览器中搜索查看:
在这里插入图片描述

但是我们如果将该图片地址复制到我们写的html页面用axios发起请求可以获得这张图片吗?

答案是否定的,因为跨域问题。我们不可能去修改baidu的后端,那怎么解决呢?

解决方法:通过我们自己的后端去访问数据,因为后端是不存在跨域问题的。通过我们自己的后端模拟浏览器获取数据之后再返回给我们的前端。而我们知道axios可以作用于node和浏览器中,所以我们使用node通过axios获取数据。

2. 初始化

通过npm init -y 初始化生成json文件:
在这里插入图片描述

3. 安装axios

npm i axios -S

在这里插入图片描述

4. 后端引入axios

const axios = require("axios");

5. 模拟浏览器获取数据

模拟浏览器,即我们自己的后端通过响应头发送信息告诉baidu后端我们的node是一个浏览器(这里的浏览器相当于后端,没有跨域问题)。

找到baidu的请求标头中的User-Agent和请求标头Accept复制到后端node的header中:
在这里插入图片描述
在这里插入图片描述

访问地址为标头中的请求URL:

在这里插入图片描述

所以js文件代码为:

const axios = require("axios");
axios({
    method: "get",
    url: "https://image.baidu.com/search/acjson?tn=resultjson_com&logid=10647297934415395588&ipn=rj&ct=201326592&is=&fp=result&fr=&word=%E9%A3%8E%E6%99%AF%E5%9B%BE%E7%89%87%E5%A4%A7%E5%85%A8&queryWord=%E9%A3%8E%E6%99%AF%E5%9B%BE%E7%89%87%E5%A4%A7%E5%85%A8&cl=2&lm=-1&ie=utf-8&oe=utf-8&adpicid=&st=&z=&ic=&hd=&latest=&copyright=&s=&se=&tab=&width=&height=&face=&istype=&qc=&nc=1&expermode=&nojc=&isAsync=&pn=30&rn=30&gsm=1e&1657702473476=",
    // 我node后端要告诉baidu的后端我是一个浏览器来请求数据---后端不会产生跨域问题
    // headers 发送给baidu后端的信息
    headers: {
        // 模拟浏览器信息    告诉其他人后端我是一个浏览器
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36 Edg/103.0.1264.49",
        //   告诉其他后端 我接收的数据格式
        "Accept": "text/plain, */*; q=0.01"
    }
}).then(result => {
    console.log(result.data)
});

执行js文件获取数据:

node app.js

在这里插入图片描述

这样我们就获得了baidu图片的网页数据,可以通过"ctrl + 单击"终端的链接查看图片:

在这里插入图片描述

6. 返回数据到前端

在js文件中引入http模块,创建和开启http服务并设置跨域:

const http = require("http");   // 引入http模块    用于创建服务器应用
const axios =require("axios");  // 引入 axios 请求数据

let server = http.createServer((req,res)=>{
    // 后端设置跨域 允许任何人 能够访问 并返回数据
    res.setHeader("Access-Control-Allow-Origin","*");
});
server.listen("3000",()=>{
    console.log("启动")
});

设置发起请求的地址,当请求该地址时通过axios获取baidu图片的数据(将之前的模拟浏览器的代码复制到条件语句中):

这里我设置的请求地址后缀为:/img_baidu

if(req.url == "/img_baidu"){ 
    axios({
        method: "get",
        url: "https://image.baidu.com/search/acjson?tn=resultjson_com&logid=10647297934415395588&ipn=rj&ct=201326592&is=&fp=result&fr=&word=%E9%A3%8E%E6%99%AF%E5%9B%BE%E7%89%87%E5%A4%A7%E5%85%A8&queryWord=%E9%A3%8E%E6%99%AF%E5%9B%BE%E7%89%87%E5%A4%A7%E5%85%A8&cl=2&lm=-1&ie=utf-8&oe=utf-8&adpicid=&st=&z=&ic=&hd=&latest=&copyright=&s=&se=&tab=&width=&height=&face=&istype=&qc=&nc=1&expermode=&nojc=&isAsync=&pn=30&rn=30&gsm=1e&1657702473476=",
        // 我node后端要告诉baidu的后端我是一个浏览器来请求数据   后端不会产生跨域问题
        // headers 发送给baidu后端的信息
        headers: {
        	// 模拟浏览器信息    告诉其他人后端我是一个浏览器
        	"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36 Edg/103.0.1264.49",
        	// 告诉其他人后端 我接收的数据格式
        	"Accept": "text/plain, */*; q=0.01"
        }
    }).then(result => {
    	console.log(result.data);
        res.end(JSON.stringify(result.data))
	});
}

注意:.then之后要将获取的数据返回前端,且要进行格式转换:

res.end(JSON.stringify(result.data))

前端html的代码:

<body>
    <button id="test">get请求</button>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <script>
        test.onclick = function () {
            axios({
                method: "get",
                url: "http://localhost:3000/img_baidu",
            }).then(result => {
                console.log(result.data)
            })
        }
    </script>
    
</body >

注意:前端代码中发起请求的地址url要与后端node我们自己设置的一致:/img_baidu;

alt + B 运行html文件,点击按钮获取数据:
在这里插入图片描述

7. 将数据渲染到html页面

我们知道使用axios时页面中的img标签会自动发起请求,所以我们可以通过循环遍历将图片的链接地址赋值到img标签中的src上,而获得图片地址是通过数组中每一项数据的thumbURL获得的。在html页面.then的回调函数中:

<body>
    <button id="test">get请求</button>
    <h2>将我们获取的图片数据加载到页面中</h2>   
    <!-- 获取后端数据 渲染到前端页面中  渲染到wrap元素中 -->
    <div id="wrap"></div>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <script>
        test.onclick = function () {
            axios({
                method: "get",
                url: "http://localhost:3000/img_baidu",
            }).then( result  => {
                console.log(result.data.data);
                let frg = document.createDocumentFragment();    // 创建frg文本仓库,否则会重复渲染
                    result.data.data.forEach(item => {
                        // 创建img标签,将地址赋值到img上,再将img 插入文本仓库
                        let img = document.createElement("img");
                        img.src = item.thumbURL;
                        frg.appendChild(img)
                    });
                    //插入元素
                    wrap.appendChild(frg)
            })
        }
    </script>
</body>

注意:在接收到后端返回的数据时,我们要遍历的是数组中每一项的图片,是result.data.data;后端代码与上节一样,未做改变。
在这里插入图片描述

渲染结果部分截图:
在这里插入图片描述

终端输出:
在这里插入图片描述
该错误与数据有关,后续讲解。

8. 下载html页面的图片

在后端axios的.then函数中先通过结构赋值和.的方式获取到所有图片所在的data数组;然后通过循环遍历该数组获取每一项图片的链接地址thumbURL:

if (req.url == "/img_baidu") {
	axios({
		method: "get",
		url: "https://image.baidu.com/search/acjson?tn=resultjson_com&logid=10647297934415395588&ipn=rj&ct=201326592&is=&fp=result&fr=&word=%E9%A3%8E%E6%99%AF%E5%9B%BE%E7%89%87%E5%A4%A7%E5%85%A8&queryWord=%E9%A3%8E%E6%99%AF%E5%9B%BE%E7%89%87%E5%A4%A7%E5%85%A8&cl=2&lm=-1&ie=utf-8&oe=utf-8&adpicid=&st=&z=&ic=&hd=&latest=&copyright=&s=&se=&tab=&width=&height=&face=&istype=&qc=&nc=1&expermode=&nojc=&isAsync=&pn=30&rn=30&gsm=1e&1657702473476=",
		headers: {
			"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36 Edg/103.0.1264.49",
			"Accept": "text/plain, */*; q=0.01"
		}

	}).then(({ data }) => {
            //  data 第一个axios 获取到的数据,data.data是一个数组
            //  console.log(data.data);
            data.data.forEach((item, index) => {    // 数据遍历,获取到每个图片的地址
                console.log(item.thumbURL); // 图片的地址
            })
	});
}

打印输出:

在这里插入图片描述

然后通过第二个axios请求获取每一张图片的二进制数据:

// 作用:避免 item.thumbURLios请求的地址为空,地址随意设置
let url =  item.thumbURL || "https://img2.baidu.com/it/u=3507648251,3424133896&fm=26&fmt=auto";
axios({         // 第二个axios 请求图片的真正的数据
	method: "get",
	url: item.thumbURL,
}).then((result) => {
	console.log(result)
})

注意:axios获取完整个数组之后会出现undefined的情况,我们可以在循环最后随意设置一个地址解决该情况。

打印输出:

在这里插入图片描述

得到二进制数据之后,我们可以将其存入同目录下的data文件夹中,需要引入fs模块:

const fs = require("fs");

但是axios请求将二进制转换成了utf-8格式,我们需要获取数据时要求对方后端以流的格式发送回来,然后通过管道向data文件夹写入数据。

responseType: "stream" 	//以流的格式发送回来
// 通过管道写入数据
result.data.pipe(fs.createWriteStream("./test/" + index + ".jpg"))

代码如下:

let url =  item.thumbURL || "https://t7.baidu.com/it/u=2168645659,3174029352&fm=193&f=GIF";  // 默认自己随意设置的
axios({         // 第二个axios 请求图片的真正的数据
    method: "get",
    url: url,
    responseType: "stream", // 告诉对方的后端将数据以流的格式发送回来
}).then((result) => {
    console.log(result); 
    // 通过管道写入数据
    result.data.pipe(fs.createWriteStream("./test/" + index + ".jpg"))
})

打开写入图片的文件夹,图片获取成功:
在这里插入图片描述

完整代码:

const http = require('http');   //引入http模块,用于创建服务器应用
const axios = require("axios");
const fs = require("fs");

// 创建服务器应用
let serve = http.createServer((req, res) => {
    res.setHeader("Access-Control-Allow-Origin", "*");

    if (req.url == "/img_baidu") {
        axios({
            method: "get",
            url: "https://image.baidu.com/search/acjson?tn=resultjson_com&logid=10647297934415395588&ipn=rj&ct=201326592&is=&fp=result&fr=&word=%E9%A3%8E%E6%99%AF%E5%9B%BE%E7%89%87%E5%A4%A7%E5%85%A8&queryWord=%E9%A3%8E%E6%99%AF%E5%9B%BE%E7%89%87%E5%A4%A7%E5%85%A8&cl=2&lm=-1&ie=utf-8&oe=utf-8&adpicid=&st=&z=&ic=&hd=&latest=&copyright=&s=&se=&tab=&width=&height=&face=&istype=&qc=&nc=1&expermode=&nojc=&isAsync=&pn=30&rn=30&gsm=1e&1657702473476=",
            headers: {
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36 Edg/103.0.1264.49",

                "Accept": "text/plain, */*; q=0.01"
            }

        }).then(({ data }) => {
            //  data 第一个axios 获取到的数据    data.data是一个数组
            //  console.log(data.data);
            data.data.forEach((item, index) => {    // 数据遍历 获取到每个图片的地址
                // console.log(item.thumbURL); // 图片的地址
                // 作用避免 axitem.thumbURLios请求的地址为空   地址随意设置
                let url =  item.thumbURL || "https://t7.baidu.com/it/u=2168645659,3174029352&fm=193&f=GIF";  // 默认自己随意设置的
                axios({         // 第二个axios 请求图片的真正的数据
                    method: "get",
                    url: url,
                    // url: item.thumbURL,
                    responseType: "stream", // 告诉对方的后端将数据以流的格式发送回来
                }).then((result) => {
                    // console.log(result); 
                    // 通过管道写入数据
                    result.data.pipe(fs.createWriteStream("./data/" + index + ".jpg"))
                })

            })
        });
    }

});

// 启动监听端口号,我们应用运行的地址
serve.listen("3000", () => {
    console.log("开启3000端口服务")
});

二、小说爬虫

1. 单个页面爬取

比如我们搜索biqvge,随便打开一本小说:
在这里插入图片描述

用网址通过axios发起请求:

const axios = require("axios");
const fs =require("fs");

axios({
    method:"get",
    url:"https://www.bbiquge.net/book/133303/56520852.html"
}).then(( {data} )=>{
    console.log(data)   
});

打印输出解构赋值后的data:
在这里插入图片描述

乱码是因为node不能解析gbk格式,解析gbk格式非常复杂,所以我们只能换小说。。。。爬取utf-8格式的网页。
在这里插入图片描述

找到啦找到啦:

https://www.17k.com/chapter/3006464/38107770.html

打印输出:

在这里插入图片描述

现在我们获取到整个页面,文字也是页面中的数据,只要把文字复制下来再写入就可以了。但是node是没有办法操作节点的,我们可以通过第三方包来获取 => jsdom => 用于node中操作dom节点

安装jsdom:

npm i jsdom -S

在这里插入图片描述

jsdom的使用说明可以在npm官网查看:jsdom - npm (npmjs.com)

引入jsdom:

const jsdom = require("jsdom");
const { JSDOM } = jsdom;

使用jsdom:

向构造函数传递一个字符串。您将获得一个对象,该对象具有许多有用的属性;

通过new方式使用jsdom,参数是字符串(html整个页面相当于字符串):

通过对节点操作,我们可以获取到取出标签后的纯文字内容:

axios({
    method: "get",
    url: "https://www.17k.com/chapter/3006464/38107770.html"
}).then(({ data }) => {
    const dom = new JSDOM(data);	// 将后端具有标签字符串转换可以通过js方法获取节点或者内容
    console.log(dom.window.document.querySelector(".p").textContent);  // 获取节点 和 js的使用方法一样
});

在这里插入图片描述

注意: 代码.querySelector(“.p”)中的p指的是html页面中小说内容的class类的名称。

在这里插入图片描述

写入文本:

通过fs模块的writeFile方法将文本内容写入文档:

let value = dom.window.document.querySelector(".p").textContent.trim(); //trim()用于将文本格式从头写入
fs.writeFile("./text/01.txt", value, () => {
	console.log("获取成功")
})

爬取结果:
在这里插入图片描述

完整代码:

const axios = require("axios");
const fs = require("fs");
const jsdom = require("jsdom");
const { JSDOM } = jsdom;

axios({
    method: "get",
    url: "https://www.17k.com/chapter/3006464/38107770.html"
}).then(({ data }) => {
    // console.log(data)
    // 向构造函数传递一个字符串。您将获得一个对象,该对象具有许多有用的属性
    // 通过new方式使用jsdom,参数是字符串
    const dom = new JSDOM(data);
    // console.log(dom.window.document.querySelector(".p").textContent);
    let value = dom.window.document.querySelector(".p").textContent; //trim()用于将文本格式从头写入
    fs.writeFile("./text/01.txt", value, () => {
        console.log("获取成功")
    })
});

2. 整个小说爬取

要获取整个小说,我们可以先获取第一个页面,获取到里面的文字同时获取到下一章的跳转地址,再通过axios获取下一章页面的数据,再获取里面的文字,以此循环,就可以拿到整个小说 。
在这里插入图片描述

可以看到,html给的下一章的地址是不全的,前面还需要拼接https://www.17k.com/;

我们通过对节点操作获取属性值尝试获取类.nextChapter中的href下一章地址:

function fun(url) {
    axios({
        method: "get",
        url: url
    }).then(({ data }) => {
        // 获取属性值:元素.getAttribute("属性名") 返回属性值
        let nexturl = dom.window.document.querySelector(".nextChapter").getAttribute("href");
        console.log(nexturl);
    });
}
fun("https://www.17k.com/chapter/3006464/38107770.html");   // 函数执行

在这里插入图片描述

然后通过拼接得到完整地址,再将文本内容通过追加的方式 {flag:“a”} 写入文档:

写入之后,我们通过循环调用函数不断的读取下一章内容,使程序自动运行:

完整代码:

const axios = require("axios");
const fs = require("fs");
const { JSDOM } = require("jsdom");

function fun(url) {
    axios({
        method: "get",
        url: url
    }).then(({ data }) => {
        const dom = new JSDOM(data);
        let value = dom.window.document.querySelector(".p").textContent;
        // 获取属性值:元素.getAttribute("属性名") 返回属性值
        let nextUrl = dom.window.document.querySelector(".nextChapter").getAttribute("href");
        // console.log(nextUrl);
        // nextUrl是一空值,则打断程序运行
        if (!nextUrl) return;
        // 拼接地址
        nextUrl = "https://www.17k.com" + nextUrl;
        // 在一个文件内 追加写入内容
        fs.writeFile("./text/02.txt", value, { flag: "a" }, () => {
            console.log(nextUrl + "的上一章写入成功");
            fun(nextUrl) // 函数循环调用,获取下一章内容
        })
    });
}
fun("https://www.17k.com/chapter/3006464/38107770.html");   // 函数执行,先获取第一章内容

爬取结果:
在这里插入图片描述
在这里插入图片描述

注意:在读取到第五十一章时报错了,复制该章链接到浏览器中搜索:

在这里插入图片描述
在这里插入图片描述

结果是需要登录才能阅读,所以获取不到网页的文本内容和下一章地址。。。