Fork me on GitHub

koa学习-数据抓取-3

利用 Puppeteer 对数据爬取

puppeteer是使用无头浏览器来模拟人工登录一个网站来对数据进行一个抓取,然后抓取之后注入一些人工的脚本来对数据进行一个处理。

关于 puppeteer 可以去github上看看官方文档

我们这里利用puppeteer来完成一个豆瓣爬虫的开发.

首先我们要爬取的网站是:https://movie.douban.com/tag/#/?sort=U&range=6,10&tags=, 我需要得到电影记录篇的海报图片的url,以及电影的名称,电影的豆瓣ID和电影的评分

首先在项目下面新建一个一个叫做trailer-list.js的文件,然后在里面写代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const url = `https://movie.douban.com/tag/#/?sort=U&range=6,10&tags=`
const puppeteer = require('puppeteer');
const sleep = time => new Promise(resolve => {
setTimeout(resolve,time)
}); // 封装一个延缓函数
// 然后开始封装一个爬虫函数
let scrape = async() => {
console.log('Start Right Now');
}
// 执行这个函数
scrape().then(value=>{
console.log(value)
})

主要需要使用的就是一些puppeteer里面的API.

主要代码

其中主要的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
const url = `https://movie.douban.com/tag/#/?sort=U&range=6,10&tags=`
const puppeteer = require('puppeteer');
const sleep = time => new Promise(resolve => {
setTimeout(resolve,time)
});
let scrape = async() => {
console.log('Start Now');
// 将其设置为没有沙箱的模式
const browser = await puppeteer.launch({
args: ['--no-sandbox'],
dumpio: false
})
// 模拟打开一个新的页面
const page = await browser.newPage();
// 访问对应的url
await page.goto(url,{
waitUntil:'networkidle2'
})
// 等个3s
await page.waitFor(3000);
await page.waitForSelector('.more');
for(let i = 0;i<2;i++){
// 点击页面中的更多按钮,获取到更多的数据
await page.click('.more');
await page.waitFor(3000);
}
// evaluate 可以使得在函数里面能够使用dom操作
const result = await page.evaluate(()=>{
let items = document.querySelectorAll('a.item')
let links = [];
if(items.length>=1){
items.forEach((item,index)=>{
let text = item.innerText;
// 电影的评分
let rate = Number(text.slice(text.length-4,text.length));
// 电影的标题
let title = item.innerText.slice(0,-4);
// 电影的doubanID
let doubanID = item.pathname.match(/\d+/)[0];
// 电影的海报图片地址
let poster = item.childNodes[0].childNodes[0].childNodes[0].currentSrc.replace('s_ratio','l_ratio')
links.push({
rate,
title,
doubanID,
poster
})
})
}
return links;
})
browser.close();
return result;
}
scrape().then(value=>{
console.log(value)
})

然后通过运行js文件我们可以得到跑出来的结果了。其实上面的DOM操作我们使用JQuery来操作就很简单了。

利用child_process fork 子进程来运行爬虫脚本

关于子进程可以查看另外一篇blog,里面有比较详细的讲解.

我们可以先用子进程来将上面的爬虫脚本跑起来。首先新建一个叫做tasks的文件,里面放一个movie.js的文件。

在movie里面通过child_process来起到引入子进程的作用。

然后写入这样的代码来使得上面的爬虫代码成为一个子进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 首先调用一个子进程,子进程和事件循环这个机制是有很大区别的.
const cp = require('child_process');
const {
resolve
} = require('path');
(async () => {
// 通过相对路径来引入爬虫
const script = resolve(__dirname, '../crawler/trailer-list.js')
// 调用子进程上面的 fork 方法 ,它可以派生出一个子进程对象
const child = cp.fork(script, []);
// 设置一个invoked变量来标识爬虫脚本是否有被运行过
let invoked = false;
// 用一个回调函数来监听异常
child.on('error', err => {
if (invoked) {
return;
} else {
invoked = true;
}
console.log(err);
})
child.on('exit', code => {
if (invoked) {
return;
} else {
invoked = true;
}
let err = code === 0 ? null : new Error('exit code ' + code);
console.log('err: ', err);
})
child.on('message', data => {
let result = data.result;
console.log('result: ', result);
})
})();

然后在爬虫代码那边,响应一下这个进程,将获取到的数据发送回去,在scrape函数的后面加上这样的一些代码:

1
2
3
4
5
6
7
8
browser.close();
// 把结果发送到主进程中去
process.send({
result
});
// 让进程退出
process.exit(0);
return result;

这样在movie.js里面就可以收到返回过去的结果了。

使用豆瓣 API 来进行数据的抓取

参考最新的豆瓣API官网地址,我们可以通过里面的接口来获取到电影条目的信息。

根据文档的一般形式而言是这样的的:http://api.douban.com/v2/movie/subject/1764796 (以其中一个电影的url为例子)

前面的baseURL是http://api.douban.com/v2 ,后面接一个和豆瓣ID相关的东西。

但是后来我使用过后发现这样使用接口会出现状态码返回104的情况,后来去google之后才找到了解决办法。

解决办法地址:https://www.imooc.com/qadetail/318861, 解决方法就是在url的最后面拼接上一个APIKEY的url。

然后这样我们这就可以在代码里面使用这个接口了,在tasks目录下面新建一个api.js的文件

在api.js里面我们需要模拟去请求douban的接口 API,这里会用到一个npm的工具库叫做request-promise-native

然后调用这个库去请求豆瓣的API,具体的代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 引入一个服务端的请求库
const rp = require('request-promise-native');
async function fetchMovie(item) {
const url = `http://api.douban.com/v2/movie/subject/${item.doubanId}?apikey=0df993c66c0c636e29ecbb5344252a4a`;
// rp能够直接去请求这个url
const res = await rp(url);
return res;
};
(async () => {
let movies = [{
doubanId: 1292052,
title: '肖申克的救赎',
rate: 9.6,
poster: 'https://img3.doubanio.com/view/photo/l_ratio_poster/public/p480747492.jpg'
},
{
doubanId: 1652592,
title: '阿丽塔:战斗天使',
rate: 7.6,
poster: 'https://img3.doubanio.com/view/photo/l_ratio_poster/public/p2544987866.jpg'
}]
movies.map(async movie=>{
let movieData = await fetchMovie(movie);
// movieData最后返回的数据是string,我们把它格式化成JSON
try{
movieData = JSON.parse(movieData);
console.log(movieData.tags);
console.log(movieData.summary);
} catch(err){
console.log(err);
}
// console.log('movieData: ', movieData);
})
})();

这里先在movie那里随便请求了两条数据来试试。后面会将数据转存到我们自己的服务器上面。

puppeteer深度取图片和视频地址

之前我们已经完成了电影的一些功能的爬去,现在我们还需要更深度爬去一些信息(关于视频预告片的video的url和宣传图片的链接)。

直接在tasks目录下面新建一个trailer.js,让这一次的爬虫成为一个子进程任务,然后在crawler目录下面新建一个video.js来作为我们本次的爬虫图片。

其中trailer.js的代码和之前movie.js的代码基本是相同的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 首先调用一个子进程,子进程和事件循环这个机制是有很大区别的.
const cp = require('child_process');
const {
resolve
} = require('path');
(async () => {
const script = resolve(__dirname, '../crawler/video.js')
// 调用子进程上面的 fork 方法 ,它可以派生出一个子进程对象
const child = cp.fork(script, []);
// 设置一个invoked变量来标识爬虫脚本是否有被运行过
let invoked = false;
// 用一个回调函数来监听异常
child.on('error', err => {
if (invoked) {
return;
} else {
invoked = true;
}
console.log(err);
})
child.on('exit', code => {
if (invoked) {
return;
} else {
invoked = true;
}
let err = code === 0 ? null : new Error('exit code ' + code);
console.log('err: ', err);
})
child.on('message', data => {
console.log(data);
})
})();

然后video.js的代码是这样的。(中间进行了一次页面的跳转,因为我们需要点击进入到另外一个浏览器里面去进行一次爬取)。

video.js的代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// 前缀是subject,后缀是豆瓣ID
const puppeteer = require('puppeteer');
const base = `https://movie.douban.com/subject/`;
const doubanId = '26739551';
const videoBase = `https://movie.douban.com/trailer/219491/#content`;
const sleep = time => new Promise(resolve => {
setTimeout(resolve, time);
});
(async () => {
console.log('开始访问详情界面');
// 相当于是个看不到的浏览器
const broswer = await puppeteer.launch({
args: ['--no-sandbox'],
dumpio: false
})
const page = await broswer.newPage();
await page.goto(base + doubanId, {
waitUntil: 'networkidle2'
// 当网页空闲的时候,表示页面已经加载完成了
})
await sleep(3000);
// 来获取详情页面
const result = await page.evaluate(() => {
// 回调函数里面的代码都是在浏览器环境里面执行的,用 var 声明变量可能比较保险
var $ = window.$;
var it = $('.related-pic-video');
if (it && it.length > 0) {
var link = it.attr('href');
// 将括号里面的url提取出来
var cover = it.attr('style').match(/\((.+?)\)/g)[0].slice(1, -1);
return {
link,
cover
}
}
return {};
})
let video;
if (result.link || result.cover) {
await page.goto(result.link, {
waitUntil: 'networkidle2'
})
// 等待个2s
await sleep(2000);
video = await page.evaluate(() => {
var $ = window.$;
var it = $('source');
if (it && it.length >= 0) {
return it.attr('src');
}
return ''
})
}
const data = {
video,
doubanId,
cover: result.cover
}
broswer.close();
process.send({
data
});
process.exit(0);
})();

上传图片和视频到七牛云图床上

CDN指的是静态资源节点。
现在我们需要用CDN来存储我们的静态资源,首先使用 node server/tasks/movie.js 跑出一份测试数据来。

然后再用跑出来的随便一组数据里面的豆瓣ID复制到video.js里面去,然后再跑一组node server/tasks/trailer.js

可以得到一组这样的数组(emm那个poster是我中途加上去的hhh):

1
2
3
4
5
6
{
video: 'http://vt1.doubanio.com/201907181341/46676423975134252890470f56d08bd2/view/movie/M/301210226.mp4',
doubanId: '1849031',
poster: 'https://img3.doubanio.com/view/photo/l_ratio_poster/public/p1312700744.jpg',
cover: 'https://img1.doubanio.com/img/trailer/medium/1696797559.jpg?'
}

然后可以在tasks目录下面新建一个qiniu.js的文件,同时得线上七牛上面把上面一些隐私相关的东西获取下来,隐私的文件新建在一个根目录下的config的文件夹里面,然后里面放入一个index.js的文件

其内容大概为这样(这里的这些配置全部都换成你自己的= =):

1
2
3
4
5
6
7
8
module.exports = {
"qiniu":{
"bucket":"",
"video":"",
"AK":"",
"SK":""
}
}

然后在qiniu.js 里面的对象大致是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
const qiniu = require('qiniu');
const nanoid = require('nanoid');
const config = require('../config');
const bucket = config.qiniu.bucket;
const mac = new qiniu.auth.digest.Mac(config.qiniu.AK, config.qiniu.SK);
const cfg = new qiniu.conf.Config();
// 首先在qiniu.js里面声明一个上传对象
const client = new qiniu.rs.BucketManager(mac, cfg)
const uploadToQiniu = async (url, key) => {
return new Promise((resolve, reject) => {
// 上传对象有个方法叫做fetch,它能够从网络上来获取某一份静态的资源
client.fetch(url, bucket, key, (err, ret, info) => {
if (err) {
reject(err);
} else {
if (info.statusCode === 200) {
resolve({
key
});
} else {
reject(info);
}
}
});
})
};
(async () => {
let movies = [{
video: 'http://vt1.doubanio.com/201907181341/46676423975134252890470f56d08bd2/view/movie/M/301210226.mp4',
doubanId: '1849031',
poster: 'https://img3.doubanio.com/view/photo/l_ratio_poster/public/p1312700744.jpg',
cover: 'https://img1.doubanio.com/img/trailer/medium/1696797559.jpg?'
}];
movies.map(async movie => {
// 如果movie对象里面有视频,并且这个视频没有被上传过的话
if (movie.video && !movie.key) {
try {
console.log('开始上传 video');
let videoData = await uploadToQiniu(movie.video, nanoid() + '.mp4');
console.log('开始上传 cover');
let coverData = await uploadToQiniu(movie.cover, nanoid() + '.png');
console.log('开始上传 poster');
let posterData = await uploadToQiniu(movie.poster, nanoid() + '.png');
if (videoData.key) {
movie.videoKey = videoData.key;
}
if (coverData.key) {
movie.coverKey = coverData.key;
}
if (posterData.key) {
movie.posterKey = posterData.key;
}
console.log(movie);
} catch (err) {
console.log(err);
}
}
})
})()

使用node server/tasks/qiniu.js来运行这段脚本,最后跑出来的结果是:

1
2
3
4
5
6
7
8
9
{
video: 'http://vt1.doubanio.com/201907181341/46676423975134252890470f56d08bd2/view/movie/M/301210226.mp4',
doubanId: '1849031',
poster: 'https://img3.doubanio.com/view/photo/l_ratio_poster/public/p1312700744.jpg',
cover: 'https://img1.doubanio.com/img/trailer/medium/1696797559.jpg?',
videoKey: 'http://wdlj.zoomdong.xin/3I3u7OEhVXrWQL_lP3Au3.mp4',
coverKey: 'http://wdlj.zoomdong.xin/A25ceUF_TE7LpmYd9OX3z.png',
posterKey: 'NnWxoxvl2a3AhQLSJwKsE.png'
}

跑出来的videokey和coverkey和posterkey前面加上我们自己七牛云的域名前缀就可以访问到之前打包上传过去的资源了。

-------------本文结束感谢您的阅读-------------