# web-crawler **Repository Path**: JosenYu-PC/web-crawler ## Basic Information - **Project Name**: web-crawler - **Description**: node crawler - **Primary Language**: JavaScript - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2018-12-03 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # WEB-CRAWLER ## SuperAgent [简书](https://www.jianshu.com/p/98b854322260) [npm](https://www.npmjs.com/package/superagent) ### SuperAgent 是用来发起请求的,是一个轻量的,渐进式的 ajax api,可读性好,学习曲线低,内部依赖 nodejs 原生的请求 api,适用于 nodejs 环境下也可以使用 http 发起请求 1. 安装 SuperAgent > \$ npm install superagent --save 2. 开始学习 SuperAgent 吧! 般来说,我们常有的 HTTP 请求包括 GET POST DELETE HEAD PUT 这些。 在不同的应用场景,在发送请求的时候,会选择一个正确的请求方式,**然后通过 end()函数来得到请求后的返回结果。** ```js superagent .get("/api") //这里的URL也可以是绝对路径 .end(function(req, res) { //do something }); // 等价于==> superagent("GET", "/api") //这里的URL也可以是绝对路径 .end(function(req, res) { //do something }); ``` 接下来,让我们逐一的对 SuperAgent 的一些特性进行分析吧~ 🚀 - 设置请求头 - 这一点在写爬虫的时候十分的有用,因为有些网站可能设置了一些限制条件,比如它会去匹配你的请求头里面的一些字段, 像 User-Agent Referer 等,如果你没有设置这些请求头的话,可能就抓取不到数据咯~ - SuperAgent 里面设置请求头很简单,通过使用 set()方法就可以设置了,有两种方式 ```js //单个单个的设置 superagent .get("/api") .set("Referer", "https://www.google.com") .set("Accept", "image/webp,image/_,_/*;q=0.8") .end(function(req, res) { //do something }); //单个单个的设置 superagent .get('/api') .set({ 'Referer','https://www.google.com', 'Accept','image/webp,image/*,*/*;q=0.8' }) .end(function(req,res){ //do something }) ``` - GET 请求方式 - 我相信大家都一定见过这样类型的 URL:,就是 GET 方式可以带上参数,也可以不带上参数。不带参数的就不说了,相信大家都会,在 superagent 中我们可以通过 query()方法给 URL 后面带上参数,有 4 种写法 ```js //接下来所形成的URL为/api?name=An&age=20&sex=male //第一种 superagent .get("/api") .query({ name: "An" }) .query({ age: 20 }) .query({ sex: "male" }) .end(cb); //第二种 superagent .get("/api") .query({ name: "An", age: 20, sex: "male" }) .end(cb); //第三种 superagent .get("/api") .query("name=An&age=20&sex=male") .end(cb); //第四种 superagent .get("/api") .query("name=An") .query("age=20") .query("sex=male") .end(cb); ``` - POST/PUT 请求 - 这两种请求,一般是要给服务端发送数据,现在文本数据的方式一般都以 json 的方式传递。我们可以在请求头里设置 Accept=application/json,从而服务器可以根据请求头来生成 json 数据(在 java 开发后台时可以这样) - 在 superagent 里面,默认的数据传递格式是 json,所以下面几种种方式是相同的。 ```js superagent .post("/api") .set("Accept", "application/json") .send('{"name":"An","age":20,"sex":"male"}') .end(cb); //等价于 ==> superagent .post("/api") .send({ name: "An", age: 20, sex: "male" }) .end(cb); //等价于 ==> superagent .post("/api") .send({ name: "An" }) .send({ age: 20 }) .sex({ sex: "male" }) .end(cb); ``` - 当然,除了 json 的形式去传递,我们还有一种很常见的表单提交,在 superagent 里,也实现了模拟表单的提交数据类型 application/x-www-form-urlencoded, 我们可以通过 type('form')方法进行转换 ```js superagent .post("/api") .type("form") .send({ name: "An", age: 20 }) // name=An&age=20 .end(cb); ``` - 设置 Content-Type 的两种快速方式 ```js superagent .post("/api") .type("application/json") .type("png"); // 等价于==> superagent .post("/api") .accept("application/json") .accept("png"); ``` - 分析处理 response body superagent 可以帮你解析返回的数据,当前支持三种类型的数据 application/x-www-form-urlencoded , application/json 和 multipart/form-data - JSON/Urlencoded 解析后的数据会以对象的形式存在,可以通过 res.body 来得到。 - Multipart 这种格式的数据的处理,superagent 是通过 Formidable 模块,它是主要处理文件上传的模块,大家可以去了解下,也是 Node 里面十分常用的模块,也很简单易上手。上传的文件的信息可以在 res.files 去查看,当然,我觉得这个属性用的比较少,这只是个人观点了~ - Response 的属性 - res.text 包含为被解析的响应数据 - res.body 上文提到了,包含解析的数据,但是目前只支持三种格式 - res.header 响应头,是一个 Object - res.type & res.charset 类型和编码格式 - res.status 状态码 - 终止请求 req.abort() 暂停请求 req.timeout(ms) ms 表示毫秒为单位的时间 - Basic Access Authentication 认证 > 首先先简单的介绍下 Basic Access Authenication,它是在 web 应用中,通过直接提供用户名、密码来进行验证身份的一种优化的解决方案。 > > 原理是将用户名和密码通过:连接,形成 username:password 然后再进行 base64 加密,发送到服务器后再进行解密得到用户名和密码,进行进一步的匹配验证。参考文章:[HTTP Basic Authentication](https://link.jianshu.com/?t=http://smalltalllong.iteye.com/blog/912046) 认证。 - 在 superagent 里,有两种方式进行验证 ```js superagent.get("http://username:password@localhost").end(cb); //等价于 ==> superagent .get("http://localhost") .auth("username", "password") .end(cb); ``` - 可以通过 pipe 管道流入流出数据 我想大家应该知道 node 里面有个核心特性就是 stream,如果不知道的,可以参考:[nodejs 中流(stream)](https://link.jianshu.com/?t=https://segmentfault.com/a/1190000000519006)的理解,举两个栗子: ```js //第一个例子 var fs = require("fs"); var request = require("superagent"); var postJson = fs.createReadStream("./postDataJson"); var req = request.post("/api"); req.accept("json"); stream.pipe(req); //第二个例子 var fs = require("fs"); var request = require("superagent"); var getData = fs.createWriteStream("./getData"); var res = request.get("/api"); res.pipe(getData); ``` - 添加多个附件 superagent 也提供了一些高级的 API,如果你想添加多个附件可以使用 attach(name,[path],[filename]),其中你可以通过 filename 来自定义上传后文件的文件名 ```js request .post("/upload") .attach("avator", "/path/a.png", "An.png") .attach("photo", "/path/b.png") .end(cb); ``` - 复合请求 superagent 也支持复合请求,比如你想上传一个带有你的姓名和邮箱的图片,那可以通过 field(name,value)方法 ```js request .post('/upload') .field('name','An') .field('age':20) .attach('avator','/path/a.png','An.png') .end(cb) ``` - 错误处理 有时候我们可能会因为不同的原因得到 4XX 或者 5XX 的错误,这个时候我们确实是可以从 end(function(err,res){...})里的 err 得到错误信息,比如 er.status 错误的状态码啥的,但是有些时候我们想去处理这些错误,重新发送一个别的请求啥的,那么这个时候我们可以通过 on('error',handleFn)去处理了 ```js request .post("/api") .send(data) .on("error", handleFn) .end(cb); ``` ## cheerio [简书](https://www.jianshu.com/p/629a81b4e013) [node 论坛](https://cnodejs.org/topic/5203a71844e76d216a727d2e) [npm](https://www.npmjs.com/package/cheerio) cheerio 是 jquery 核心功能的一个快速灵活而又简洁的实现,主要是为了用在服务器端需要对 DOM 进行操作的地方 - 安装 > npm install cheerio - API HTML 模板如下: ```html ``` 1. 解析 html(load) 首先你需要先加载你的 HTML。jQuery 会自动完成这一步,因为 jQuery 操作的 DOM 是固定的。但是在使用 cheerio 时我们要手动加载我们的 HTML 文档 ```js var cheerio = require("cheerio"); var $ = cheerio.load(''); ``` 如果你需要自定义一些解析选项,你可以多传递一个对象给 load 方法: ```js $ = cheerio.load('', { ignoreWhitespace: true, xmlMode: true }); ``` 2. 选择器(selectors) cheerio 的选择器几乎和 jQuery 一模一样,所以语法上十分相像 ```js $(selector, [context], [root]); ``` > selector 在 context 的范围内搜索,context 的范围又包含在 root 的范围内。selector 和 context 可以是一个字符串,DOM 元素,DOM 数组或者 cheerio 实例。root 一般是一个 HTML 文档字符串 > > 选择器是文档遍历和操作的起点。如同在 jQuery 中一样,它是选择元素节点最重要的方法,但是在 jQuery 中选择器建立在 CSS 选择器标准库上。cheerio 的选择器实现了大部分的方法 ```js $(".apple", "#fruits").text(); //=> Apple $("ul .pear").attr("class"); //=> pear $("li[class=orange]").html(); //=>
  • Orange
  • ``` 3. 属性操作(atrributes) 用来获取和更改属性的方法: - .attr(name, value): 这个方法用来获取和设置属性。获取第一个符合匹配的元素的属性值。如果某个属性值被设置成 null,那么该属性会被移除。你也可以把 map 和 function 作为参数传递进去,就像在 jQuery 中一样 ```js $("ul").attr("id"); //=> fruits $(".apple") .attr("id", "favorite") .html(); //=>
  • Apple
  • ``` - .removeAtrr(name): 移除名为 name 的属性 ```js $(".pear") .removeAttr("class") .html(); //=>
  • Pear
  • ``` - .hasClass(className): 检查元素是否含有此类名 ```js $(".pear").hasClass("pear"); //=> true $("apple").hasClass("fruit"); //=> false $("li").hasClass("pear"); //=> true ``` - .addClass(className): 添加类名到所有的匹配元素,可以用函数作为参数 ```js $(".pear") .addClass("fruit") .html(); //=>
  • Pear
  • $(".apple") .addClass("fruit red") .html(); //=>
  • Apple
  • ``` - .remoteClass([className]):移除一个或者多个(空格分隔)的类名,如果 className 为空,则所有的类名都会被移除,可以传递函数作为参数 ```js $(".pear") .removeClass("pear") .html(); //=>
  • Pear
  • $(".apple") .addClass("red") .removeClass() .html(); //=>
  • Apple
  • ``` 4. 遍历 - .find(selector):在当前元素集合中选择符合选择器规则的元素集合 ```js $("#fruits").find("li").length; //=> 3 ``` - .parent():获取元素集合第一个元素的父元素 ```js $(".pear") .parent() .attr("id"); //=> fruits ``` - .next():选择当前元素的下一个兄弟元素 ```js $(".apple") .next() .hasClass("orange"); //=> true ``` - .prev():同.next()相反 - .siblings():获取元素集合中第一个元素的所有兄弟元素,不包含它自己 ```js $(".pear").siblings().length; //=> 2 ``` - .children(selector)获被选择元素的子元素 ```js $("#fruits").children().length; //=> 3 $("#fruits") .children(".pear") .text(); //=> Pear ``` - .each(function(index,element)):遍历函数返回 false 即可终止遍历 ```js var fruits = []; $("li").each(function(i, elem) { fruits[i] = $(this).text(); }); fruits.join(", "); //=> Apple, Orange, Pear ``` - .map(function(index,element)):迭代一个 cheerio 对象,为每个匹配元素执行一个函数。Map 会返回一个迭代结果的数组。the function is fired in the context of the DOM element, so this refers to the current element, which is equivalent to the function parameter element ```js $("li") .map(function(i, el) { // this === el return $(this).attr("class"); }) .join(", "); //=> apple, orange, pear ``` - .filter( selector ) ```js $("li") .filter(".orange") .attr("class"); //=> orange ``` - .filter( function(index) ) ```js $("li") .filter(function(i, el) { // this === el return $(this).attr("class") === "orange"; }) .attr("class"); //=> orange ``` - .first() ```js $("#fruits") .children() .first() .text(); //=> Apple ``` - .last() ```js $("#fruits") .children() .last() .text(); //=> Pear ``` - .eq( i ):缩小元素集合,可以用负数表示倒数第 i 个元素被保留 ```js $("li") .eq(0) .text(); //=> Apple $("li") .eq(-1) .text(); //=> Pear ``` 5. 操作 DOM 操作 DOM 结构的方法 .append( content, [content, ...] ) .prepend( content, [content, ...] ) - .after( content, [content, ...] ) ```js $(".apple").after('
  • Plum
  • '); $.html(); //=> ``` - .before( content, [content, ...] ) ```js $(".apple").before('
  • Plum
  • '); $.html(); //=> ``` - .remove( [selector] ) ````js $('.pear').remove() $.html() //=> ``` ```` - .replaceWith( content ) ```js var plum = $('
  • Plum
  • '); $(".pear").replaceWith(plum); $.html(); //=> ``` - .empty() ```js $("ul").empty(); $.html(); //=> ``` - .html( [htmlString] ) ```js $(".orange").html(); //=> Orange $("#fruits") .html('
  • Mango
  • ') .html(); //=>
  • Mango
  • ``` - .text( [textString] ) ```js $(".orange").text(); //=> Orange $("ul").text(); //=> Apple // Orange // Pear ``` 6. 杂项 - .toArray() ```js $("li").toArray(); //=> [ {...}, {...}, {...} ] ``` - .clone() ```js var moreFruit = $("#fruits").clone(); ``` ## superagent-charset [简书](https://www.jianshu.com/p/abedebacd53c) [npm](https://www.npmjs.com/package/superagent-charset) 中文乱码具体是指用 node 请求 gbk 编码的网页,无法正确获取网页中的中文(需要转码),"gbk" 和 "网页中的中文" 两个条件是缺一不可的。可以获取 utf-8 编码的网页中的中文,也可以获取 gbk 编码网页中的英文数字等。 1. 安装 > npm i superagent-charset 2. 使用 使用.charset(encoding)方法,就可以指定编码,详细如下: ```js var assert = require("assert"); var request = require("superagent-charset"); request .get("http://www.sohu.com/") .charset("gbk") .end(function(err, res) { assert(res.text.indexOf("搜狐") > -1); }); ``` **潜在问题** 到目前为止,仅仅是解决了我们如何设置编码的问题,但是通常我们在爬取网页的时候,都是动态爬取的,也就是说并不是人工来指定网页的编码,那么如何才能做到动态指定网页编码呢?可以这么来做: > 动态获取网站编码 > > 指定网站编码并爬去 为了浏览器能正常渲染网页信息,网站通常都会在设置 meta charset 信息,如 ```html ``` 我们可以写一个正则匹配规则,来匹配这段信息中的 charset 内容来获取编码,如下: ```js var charset = "utf-8"; var arr = res.text.match(/]*?)>/g); if (arr) { arr.forEach(function(val) { var match = val.match(/charset\s*=\s*(.+)\"/); if (match && match[1]) { if (match[1].substr(0, 1) == '"') match[1] = match[1].substr(1); charset = match[1].trim(); } }); } ``` 当然,前提是我们需要将网页先爬下来。完整的代码如下: ```js parseUrl: function (url, callback) { async.waterfall([ function (callback) { // 动态获取网站编码 superagent.get(url).end(function (err, res) { var charset = "utf-8"; var arr = res.text.match(/]*?)>/g); if (arr) { arr.forEach(function (val) { var match = val.match(/charset\s*=\s*(.+)\"/); if (match && match[1]) { if (match[1].substr(0, 1) == '"')match[1] = match[1].substr(1); charset = match[1].trim(); } }) } callback(err, charset) }) }, function (charset, callback) { // 内容爬取 superagent .get(url) .charset(charset) .end(function (err, res) { if (err) { console.log(err); callback(err); return; } var model = {}; var $ = cheerio.load(res.text); var title = _.trim($('title').text()); if (title.indexOf('-') > 0) { var strs = _.split(title, '-'); model.title = _.trim(title.substr(0, title.lastIndexOf('-'))); model.source = _.trim(_.last(strs)); } else { model.title = _.trim(title); } callback(err, model); }) } ], function (err, model) { callback(err, model); }); } ```