【从零开始写爬虫一】批量下载表情包

软件维
• 阅读 5298

打算写个关于node的爬虫菜鸟教程,接下来将带大家一步一步写一个表情包爬虫,从获取页面,解析表情包链接, 清洗脏数据,下载表情包到本地。开始之前你需要有对chrome调试工具和ES6有一定了解,包括 async/await 的使用。

获取页面地址

我们打开 U表情搜索地址,如下图

【从零开始写爬虫一】批量下载表情包

我们可以看到 http://www.ubiaoqing.com/search/ 加上关键字, 就是完整的搜索结果地址。

准备工作

你需要先安装 node7.x 以上版本,如果没装请自行下载安装,这里就不做说明了。
我们新建一个js文件 memes.js 和存放表情包的文件夹 memes

接下来安装我们需要的模块

npm install cheerio --save
npm install superagent --save

cheerio node版的jQuery,用法和jQuery一样。中文资料
superagent 一个轻量的、渐进式的请求模块。中文资料

正式开始

我们先请求地址拿到HTML

'use strict'
const request = require('superagent');
const cheerio = require('cheerio');

const SEARCH_URL = 'http://www.ubiaoqing.com/search/';
const keyword = '单身狗';
let page  = 1;
let linkAssemble = []; // 链接集合

function requestURL(keyword, page) {
    let url = `${ SEARCH_URL }${ encodeURI(keyword) }/${ page }`;   // 抓取地址
    return request.get(url).then(res => res.text);
}

我们可以看到表情包的链接在 liclassver-middlediv -> a -> imghref 属性,用jQuery选择器即为div.ver-middle>a;

【从零开始写爬虫一】批量下载表情包

我们要拿到所有 li 下的表情包

function selectLink (html) {
    let $ = cheerio.load(html); // 加载html到cheerio
    // 遍历所有的标签并获取href属性
    return Array.from($('li .ver-middle').map(function () {
            return $(this).find('img').attr('src');
        }))
}

解析后返回的数据如下:

[
  "http://ubq.ubiaoqing.com/ubiaoqing18891b279231433893c19bc0a7507f5a.jpg",
  "http://ubq.ubiaoqing.com/ubiaoqingb8b5240336c953316b99d3e4963b13b6.jpg",
  "http://ubq.ubiaoqing.com/ubiaoqing50ff2027dd2e0b257da02fe6cb364ea5.gif",
  "http://ubq.ubiaoqing.com/ubiaoqing1ad82a6cf881cd0205765b69a5073188.jpg",
  "http://ubq.ubiaoqing.com/ubiaoqing57e7ad299029e31406.gif",
  "http://ubq.ubiaoqing.com/ubiaoqingcadd8ff3468c4c03a052e55b1a7ad825.gif",
  "https://img.alicdn.com/imgextra/i2/3161190279/TB2kn9rdMRkpuFjy1zeXXc.6FXa_!!3161190279.jpg",
  "http://ubq.ubiaoqing.com/ubiaoqing7a8e3432684650ab2e12bf3a234e4203.jpg",
  "http://file.ubiaoqing.com/mp-weixin.jpg"
  ]

但是我们取到的数据并不干净,中间包括了一些广告,这显然不符合我们的期望,我们需要过滤掉不包含 http://ubq.ubiaoqing.com/ubiaoqing 前缀的链接

【从零开始写爬虫一】批量下载表情包

function cleanseLink (links) {
    return links.filter((link) => link.includes('http://ubq.ubiaoqing.com/ubiaoqing'));
}

清洗后返回

[
  "http://ubq.ubiaoqing.com/ubiaoqing18891b279231433893c19bc0a7507f5a.jpg",
  "http://ubq.ubiaoqing.com/ubiaoqingb8b5240336c953316b99d3e4963b13b6.jpg",
  "http://ubq.ubiaoqing.com/ubiaoqing50ff2027dd2e0b257da02fe6cb364ea5.gif",
  "http://ubq.ubiaoqing.com/ubiaoqing1ad82a6cf881cd0205765b69a5073188.jpg",
  "http://ubq.ubiaoqing.com/ubiaoqing57e7ad299029e31406.gif",
  "http://ubq.ubiaoqing.com/ubiaoqingcadd8ff3468c4c03a052e55b1a7ad825.gif",
  "http://ubq.ubiaoqing.com/ubiaoqing7a8e3432684650ab2e12bf3a234e4203.jpg",
  ]

我们把整个流程整理下

async function getLinksByPage (keyword, page) {
    // step1 获取页面
    let html = await requestURL(keyword, page);
    // step2 解析数据
    selectLink(html);
    // step3 清洗脏数据
    cleanseLink(html);
}

接着我们要递归获所有页面的表情包
我们翻到最后一页可以看到他是没有 下一页 的按钮的, 我们可以根据这个条件来判断是否是最后一页

【从零开始写爬虫一】批量下载表情包

async function getLinksByPage (keyword, page) {
    try {
        
        // step1 获取页面
        console.log(`获取页面 -> 关键字: ${keyword} 第${page}页`);
        let html    = await requestURL(keyword, page);
        
        // step2 解析数据
        console.log(`解析数据...`);
        let links   = selectLink(html);
        
        // step3 清洗脏数据
        console.log('清洗数据...');
        let result  = cleanseLink(links);
        
        // 将结果添加到linksAssemble
        Array.prototype.push.apply(linkAssemble, result);

        // 如果有下一页继续抓取下页表情包链接
        if ( html.includes('下一页') ) {
            return getLinksByPage(keyword, ++ page);
        }
        console.log(linkAssemble);
        return linkAssemble;
        
    } catch(err) {
        console.error(err.message);
        // 错误则跳过当前页,继续抓取!
        return getLinksByPage(keyword, ++ page);
    }
}

运行测试

然后我们来运行下,由于使用了 async/await 我们运行时需要加 --harmony 参数

node --harmony memes.js

【从零开始写爬虫一】批量下载表情包

至此第一部分代码已经完成了,完整代码如下: github

'use strict'    // 开启严格模式
const request = require('superagent');
const cheerio = require('cheerio');

const SEARCH_URL = 'http://www.ubiaoqing.com/search/';
const keyword    = '单身狗';
let page         = 1;
let linkAssemble = []; // 链接集合


async function getLinksByPage (keyword, page) {
    try {

        // step1 获取页面
        console.log(`获取页面 -> 关键字: ${keyword} 第${page}页`);
        let html    = await requestURL(keyword, page);

        // step2 解析数据
        console.log(`解析数据...`);
        let links   = selectLink(html);

        // step3 清洗脏数据
        console.log('清洗数据...');
        let result  = cleanseLink(links);

        // 将结果添加到linksAssemble
        Array.prototype.push.apply(linkAssemble, result);

        // 如果有下一页继续抓取下页表情包链接
        if ( html.includes('下一页') ) {
            return getLinksByPage(keyword, ++ page);
        }

        return linkAssemble;

    } catch(err) {
        // 错误则跳过当前页,继续抓取!
        console.error(err.message);
        return getLinksByPage(keyword, ++ page);
    }
}

getLinksByPage(keyword, page);

/**
 * @requestURL
 * @param   keyword {String} 搜索关键字
 * @param   page    {String} 页数
 * @return  request
 * */
function requestURL(keyword, page) {
    let url = `${ SEARCH_URL }${ encodeURI(keyword) }/${ page }`;   // 抓取地址
    return request.get(url).then(res => res.text);
}

/**
 * @requestURL
 * @param   html    {String} 待解析的html
 * @return  links   {Array}
 * */
function selectLink (html) {
    let $ = cheerio.load(html); // 加载html到cheerio
    // 遍历所有的标签并获取href属性
    return Array.from($('li .ver-middle').map(function () {
        return $(this).find('img').attr('src');
    }))
}

/**
 * @cleanseLink
 * @param   links    {Array} 待清洗的link
 * @return  links    {Array}
 * */
function cleanseLink (links) {
    return links.filter((link) => link.includes('http://ubq.ubiaoqing.com/ubiaoqing'));
}

第一部分我们已经完成了表情包链接的获取,接下来我们开始批量下载表情包到本地。

下载表情包到本地

观察表情包地址我们发现表情包后面22位就是它完整且唯一的文件名。

【从零开始写爬虫一】批量下载表情包

我们首先判断本地是否存在这个文件,如果存在则跳过下载,如果不存在,
我们就创建一个可写的文件 stream ,然后请求表情包地址,并 pipestream,
监听 close 事件,触发时完成Promise

function downloadMeMe (url) {
        console.log(`下载: ${url}`);
        let filePath = `./memes/${url.substr(-22)}`;    // 取到后22位作为文件名
        let stream   = fs.createWriteStream(filePath);  // 创建一个可写 stream 对象
        // 请求表情包地址,并 pipe 到刚才创建的 stream 对象
        request.get(url).pipe(stream);
}

限流器

假设我们打开表情包页面,他会同时请求一整页的表情包,所以我们只需要限制批量请求之间的间隔就好。
写个限流器,控制单次请求数,访问频率过快会导致爬虫被发现。你也可以设置随机延时。

function timerChunk(any, fn, limit, wait = 0) {
    let run = async function () {
        if (!any.length) {
            return;
        }

        // 延时等待 这里是随机0到wait毫秒
        await (new Promise((resolve, reject) => setTimeout(resolve, ~~(Math.random() * wait))));

        let params = any.splice(0, limit);              // 每次取出 limit 数量的任务
        params.forEach((param) => fn(param));
        return run();
    }

    return run();
}

组装函数

最后步骤就是搭积木把函数拼起来

(async function crawler() {
    let keyword = '单身狗';
    try {
        // 获取该关键字所有的表情包链接
        let links = await getLinksByPage(keyword, 1);
        // 下载表情包到本地
        await timerChunk(links, downloadMeMe, 5, 3000);
        console.log('完成!');
    } catch (err) {
        console.error(err);
    }
})();

我们来运行下我们的项目

【从零开始写爬虫一】批量下载表情包

完整代码

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
6个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
3年前
Java爬虫之JSoup使用教程
title:Java爬虫之JSoup使用教程date:201812248:00:000800update:201812248:00:000800author:mecover:https://imgblog.csdnimg.cn/20181224144920712(https://www.oschin
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这