前言
本文目标:爬取百度搜索引擎的关键词搜索结果,并部署到阿里云的函数计算中。
开始前请先简单看看下面函数计算和Puppeteer的概念,方便接下来的实战。
函数计算
函数计算是什么
函数计算(Function Compute):函数计算是以事件为驱动的全托管计算服务,使用函数计算,你只用专注编写代码,无需关注服务器相关基础设施。当触发事件后,函数计算会在云服务中弹性地、可靠的运行任务,并且支持日志查询,性能监控和报警等功能。
使用函数计算的优势
- 您无需采购和管理服务器等基础设施,运维成本低。
- 您只需专注业务逻辑的开发,使用函数计算支持的开发语言设计、优化、测试、审核以及上传自己的应用代码。
- 以事件驱动的方式触发应用响应用户请求。与阿里云对象存储 OSS、API 网关、日志服务和表格存储等服务无缝对接,帮助您快速构建应用。
- 提供日志查询、性能监控和报警等功能快速排查故障。
- 毫秒级别弹性伸缩,快速实现底层扩容以应对峰值压力。
- 按需付费,支持百毫秒级别收费。只需为实际使用的计算资源付费,适合有明显波峰波谷的用户访问场景。
Puppeteer
Puppeteer是什么
Puppeteer是一个Node库,它提供了一个高级API来通过DevTools协议控制无头 Chrome或Chromium ,它也可以配置为使用完整(非无头)Chrome或Chromium。
你可以通过Puppeteer的提供的api直接控制Chrome模拟大部分用户操作来进行UI Test或者作为爬虫访问页面来收集数据。
Puppeteer能做什么
-
生成页面的截图和PDF
-
可以抓取SPA或SSR网站
-
自动化测试,模拟表单提交,键盘输入,鼠标事件等行为
-
捕获网站的时间线,帮助诊断性能问题
-
创建一个最新的自动化测试环境。使用最新的JavaScript和浏览器功能,直接在最新版本的Chrome中运行测试。
1. 前期准备
开始之前请确保如下工具已经正确的安装,更新到最新版本,并进行正确的配置。
Funcraft
Funcraft 是函数计算提供的一种命令行工具,通过该工具,您可以便捷地管理函数计算、API 网关、日志服务等资源。通过一个资源配置文件 template.yml,Funcraft 即可协助您进行开发、构建、部署操作。本文提供安装 Funcraft 的三种方式。
-
安装
-
执行以下命令安装Funcraft
npm install @alicloud/fun -g -
安装完成之后,在控制终端执行fun命令查看版本信息。
fun --version
-
-
配置Funcraft
-
执行以下命令
fun config -
按照提示依次配置 Account ID、AccessKeyId、AccessKeySecret、Default Region Name。
-
Docker
Funcraft 进行依赖编译、安装、本地运行调试等,需要依赖于Docker来模拟本地环境。
Windows
-
安装
根据系统下载并安装 Docker Desktop
-
配置国内镜像
{ "registry-mirrors": [ "https://docker.mirrors.ustc.edu.cn", "https://registry.docker-cn.com", "http://hub-mirror.c.163.com" ] }在桌面右下角状态栏中右键 docker 图标,修改在 Docker Daemon 标签页中的 json ,把 上面的镜像地址加到"registry-mirrors"的数组里,保存即可。
Tips: 推荐使用阿里云Docker镜像。
2.实践
初始化项目
-
执行以下命令,并选择
http-trigger-nodejs10模板fun init -n xxx-n, --name选项要作为文件夹生成的项目名称。默认值是fun-app
-
创建存放百度爬虫函数的文件夹
cd fun-puppetter && mkdir baiduKeywordResult -
生成package.json文件
npm init -y -
生成Funcraft的配置文件
fun config按照提示依次配置 Account ID、AccessKeyId、AccessKeySecret、Default Region Name。
-
替换template.yml文件中的内容,内容为:
ROSTemplateFormatVersion: '2015-09-01' Transform: 'Aliyun::Serverless-2018-04-03' Resources: FunPuppetter: Type: 'Aliyun::Serverless::Service' Properties: Description: 'Puppetter服务, 一个服务下可以创建多个函数' baiduKeywordResult: Type: 'Aliyun::Serverless::Function' Properties: Handler: index.handler Runtime: nodejs10 CodeUri: './baiduKeywordResult' Timeout: 600 MemorySize: 1024 InstanceConcurrency: 3 Events: httpTrigger: Type: HTTP Properties: AuthType: ANONYMOUS Methods: ['POST', 'GET']这个 template.yml 的含义如下:声明一个名为 FunPuppetter的 服务。并在这个服务下,再声明一个名为 baiduKeywordResult的 函数,配置函数触发方式为httpTrigger,入口为 index.handler,以及函数的 runtime 为 nodejs10。并且,我们指定
Timeout处理函数可以运行的最长时间为 600 秒,指定MemorySize函数执行分配的内存大小为1024 MB。指定InstanceConcurrency为函数设置一个实例并发度 ,表示单个函数实例可以同时处理多少个请求。指定CodeUri 为当前目录。在部署时,Fun 会将 CodeUri 指定的目录打包上传。更多的配置规则 请参考。 -
把/index.js文件移动到/baiduKeywordResult目录下
index.js
var getRawBody = require('raw-body'); var getFormBody = require('body/form'); var body = require('body'); module.exports.handler = function(req, resp, context) { console.log('hello world'); var params = { path: req.path, queries: req.queries, headers: req.headers, method : req.method, requestURI : req.url, clientIP : req.clientIP, } getRawBody(req, function(err, body) { resp.setHeader('content-type', 'text/plain'); for (var key in req.queries) { var value = req.queries[key]; resp.setHeader(key, value); } params.body = body.toString(); resp.send(JSON.stringify(params, null, ' ')); }); }此时目录的内容应该是这样:
执行fun local start进行调试,此时Funcraft响应如下:
打开生成Url,如果响应如下,就可以开始Coding了
Coding
以下为我们要实现的接口:
百度关键词搜索结果
// request
{
url: 'http://localhost:8000/2016-08-15/proxy/FunPuppetter/baiduKeywordResult',
params: {
keyword, // 要搜索的关键词
page, // 要爬取多少页
},
}
// response
{
msg: 'success',
code: 2000,
data: [
{
title,
abstract,
redirectUrl,
url,
domain,
keyword,
pageNum,
}
]
}
/baiduKeywordResult/index.js
/baiduKeywordResult/index.js文件内容如下:
依赖的package
const puppeteer = require('puppeteer');
const _ = require('lodash');
const async = require('async');
const axios = require('axios');
const cheerio = require('cheerio');
const nodeUrl = require('url');
-
lodash
高性能的 JavaScript 实用工具库
-
async
async库是一个非常出色的异步控制库,除了 函数外,还提供了大量的其他工具函数,在当年没有async/await的时候,async库的作用尤为突出。
-
axios
Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。
-
cheerio
cheerio是jquery核心功能的一个快速灵活而又简洁的实现,主要是为了用在服务器端需要对DOM进行操作的地方
-
url
用于处理与解析 URL
handle函数
module.exports.handler = async function(req, resp, context) {
// 接收参数
let { keyword, page } = req.queries;
if (_.isEmpty(keyword) || _.isEmpty(page)) {
resp.send(JSON.stringify({
msg: '参数不正确!',
code: 4005,
data: null
}))
}
try {
// 百度搜索结果最多只有76页
page = Math.min(page, 76);
const task = new Task({ keyword, page })
const result = await task.start();
console.log('response result', result)
resp.send(JSON.stringify(result))
} catch(e) {
console.log(e)
}
}
Task Class
class Task {
// 构造函数, 创建示例时会调用
constructor(task) {
this._result = {
msg: 'success',
data: {},
code: 5000
};
this._browser = null;
this._task = task;
}
async start() {
try {
await this.initialize();
await this.execute();
this._result.code = 2000;
} catch(e) {
console.log(e.stack);
this._result.msg = e.stack;
this._result.code = 5000;
} finally {
await this.destroy();
}
return this._result;
}
async initialize() {
// 打开一个浏览器实例
this._browser = await puppeteer.launch({
headless: true,
ignoreDefaultArgs: ['--disable-extensions'],
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
]
});
}
async execute() {
const { keyword, page } = this._task;
const pageRange = _.range(0, page * 10, 10);
let results = [];
// 并发获取每页搜索结果
results = await async.mapLimit(
pageRange,
50,
async (offset) => {
// 失败重试机制
let retry = 0;
let success = false;
do {
// 打开一个Tab页
let entryPage = await this._browser.newPage();
try {
const url = `https://baidu.com/s?wd=${keyword}&pn=${offset}`;
console.log('Crawler url:', url)
await entryPage.goto(url,{
waitUntil: 'load',
timeout: 1000 * 30,
});
let pageData = [];
if(this.isLastPage(entryPage)) {
pageData = await this.structureData(offset, entryPage);
}
success = true;
return pageData;
} catch(e) {
console.log('error', e);
retry++;
// 重试6次后依旧失败则抛出异常,由handler函数内的catch捕捉
if (retry >= 6) {
throw e;
}
} finally {
await entryPage.close()
}
} while(!success && retry < 6)
}
);
results = _.flatMapDepth(results).map((item, index)=>{
item.rank = index + 1;
return item;
})
console.log(results);
this._result.data = results;
}
async structureData(offset = 0, entryPage) {
const htmlContent = await entryPage.content();
let htmlData = await this.htmlParse(htmlContent);
// 遍历解析后的数据,增加page和keyword字段
htmlData = _.map(htmlData, (data) => {
data.keyword = this._task.keyword;
data.pageNum = Math.max(1, offset / 10);
return data;
});
return htmlData;
}
async htmlParse(html) {
// 解析html获取数据
const $ = cheerio.load(html);
let pageItems = [];
$(".result.c-container").each(function (i, el) {
const that = $(el);
const item = {
title: _.trim(that.find("h3 > a").text()),
abstract: _.trim(that.find(".c-abstract").text()),
redirectUrl: _.trim(that.find("h3 > a").attr("href")),
url: "",
};
pageItems.push(item);
});
// 并发请求url, 获取百度重定向后的真实url
pageItems = await new Promise((resolve, reject) => {
async.mapLimit(
pageItems,
50,
async (item) => {
const redirectResponse = await axios.head(item.redirectUrl, {
timeout: 1000 * 10, // 10秒
maxRedirects: 0,
validateStatus: function (status) {
return status >= 200 && status < 400;
},
});
item.url = redirectResponse.headers.location || item.redirectUrl;
item.domain = nodeUrl.parse(item.url).host;
return item;
},
(err, results) => {
if (err) {
reject(err);
} else {
resolve(results);
}
}
);
});
return pageItems;
}
async isLastPage(entryPage) {
const htmlContent = await entryPage.content();
// 解析html获取数据
const $ = cheerio.load(htmlContent);
return $("#page").length && $("#page .n").length
}
async destroy() {
await this._browser.close();
}
}
/baiduKeywordResult/package.json
"dependencies": {
"async": "^3.2.0",
"axios": "^0.19.2",
"cheerio": "^1.0.0-rc.3",
"lodash": "^4.17.15",
"puppeteer": "^2.0.0",
"url": "^0.11.0"
},
安装依赖
$ fun install -d
如果直接使用npm install安装依赖,那么运行时puppeteer则会报错。这里的问题在于,puppeteer 依赖了 chromium,而 chromium 又依赖一些系统库。所以 npm install 后,还会触发下载 chromium 的操作。这里用户经常遇到的问题,主要是:
- 由于 chromium 的体积比较大,所以经常遇到网络问题导致下载失败。
- npm 仅仅只下载 chromium,chromium 依赖的系统库并不会自动安装。用户还需要自行查找缺失的依赖进行安装。
好在函数计算命令行工具Funcraft 已经集成了 Puppeteer 的解决方案,只要 package.json 中包含了 puppeteer 依赖,然后使用 fun install -d 即可一键安装所有系统依赖。
3. 本地调试函数
在本地调试代码,可以使用如下命令:
$ fun local start
using template: template.yml
HttpTrigger httpTrigger of FunPuppetter/baiduKeywordResult was registered
url: http://localhost:8000/2016-08-15/proxy/FunPuppetter/baiduKeywordResult
methods: [ 'POST', 'GET' ]
authType: ANONYMOUS
浏览器打开http://localhost:8000/2016-08-15/proxy/FunPuppetter/baiduKeywordResult,会自动下载Response
Response:
{"msg":"参数不正确!","code":4005,"data":null}
携带keyword和page参数后Response:
http://localhost:8000/2016-08-15/proxy/FunPuppetter/baiduKeywordResult?keyword=vue&page=3
{
"msg": "success",
"data": [
{
"title": "vue.js官网",
"abstract": "Vue.js - The Progressive JavaScript Framework... 订阅我们的周刊 (英文) 你可以在 news.vuejs.org 翻阅往期的 issue,也可以收听 podcast。",
"redirectUrl": "http://www.baidu.com/link?url=Men7IMCzaXf2qP148hYmJKK54l5fL03Wbya_S4L25_i",
"url": "https://cn.vuejs.org/",
"domain": "cn.vuejs.org",
"keyword": "vue",
"pageNum": 1,
"rank": 1
},
{
"title": "Vue.js 教程 | 菜鸟教程",
"abstract": "Vue.js 教程 Vue.js(读音 /vjuː/, 类似于 view) 是一套构建用户界面的渐进式框架。 Vue 只关注视图层, 采用自底向上增量开发的设计。 Vue 的目标是通过...",
"redirectUrl": "http://www.baidu.com/link?url=WXIdaqC4EhUmm3Vdis5p0BCM3vUo139WwLQCB28LV8p5epqoiZMceQ1AWV_HpjKAb2jaqVpsXyWytUzPrnDqt_",
"url": "https://www.runoob.com/vue2/vue-tutorial.html",
"domain": "www.runoob.com",
"keyword": "vue",
"pageNum": 1,
"rank": 2
},
{
"title": "介绍— Vue.js",
"abstract": "Vue.js - The Progressive JavaScript Framework... Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设...",
"redirectUrl": "http://www.baidu.com/link?url=RjryFjnGxvreIzhFX1iicF8hHcRbNhkoTTTrFLjsLk4EmqM5ydhCbTR2vye8NBUv",
"url": "https://cn.vuejs.org/v2/guide/",
"domain": "cn.vuejs.org",
"keyword": "vue",
"pageNum": 1,
"rank": 3
}
.......
],
"code": 2000
}
4. 一键部署服务
在本地调试代码,可以使用如下命令:
- 确认yml文件中的配置,选择Y就可以了,使用
fun deploy -y部署时可跳过确认
- 使用nas服务管理依赖
FunPuppetter/baiduKeywordResult函数大小超过50M,需要使用Nas服务来管理依赖。
-
? Do you want to let fun to help you automate the configuration?
询问是否使用 Fun 来自动化的配置 NAS 管理依赖,选择Yes
-
? We recommend using the 'NasConfig: Auto' configuration to manage your function dependencies.
是否使用NasConfig: Auto配置来管理函数依赖关系, 选择Yes。
Tips: 可以选择手动配置。函数计算挂载NAS访问。如果你已经手动配置,这里则提示用户选择已经配置的 NAS 存储函数依赖
看到这里就表示部署成功了。
为什么Response会强制下载
因为服务端会为response header中强制添加content-disposition: attachment字段,此字段会使得返回结果在浏览器中以附件的方式打开。此字段无法覆盖,使用自定义域名将不受影响。
配置自定义域名
接下来我们给函数服务配置一个自定义域名,这样Http trigger触发的函数响应就不会再强制下载了。
-
登录阿里云函数计算控制台
-
打开自定义域名,创建域名
把
fun.root2.cn换成你们的域名地址 -
解析域名到函数计算的Endpoint
Endpoint在函数计算控制台/概览的右上角获取。
打开云解析DNS控制台,选中域名,添加记录
记录类型选择
CNAME,记录值为函数计算的Endpoint -
测试解析是否生效
如下图则解析成功
添加了新依赖,如何更新?
如果添加了新依赖,只需要重新执行fun nas sync进行同步即可。
如果修改了代码,只需要重新执行fun deploy重新部署即可。