爬虫教程实例

用nodejs做简单的爬虫,其实是一件简单的事情。
我们以 http://www.ledu365.com/ 做一个简单的例子。
在此之前,你需要掌握 :es6, async /await 和简单的 express;

分析页面

我们发现它的列表页地址栏 
http://www.ledu365.com/shehui/list_4_9.html
http://www.ledu365.com/shehui/list_4_13.html

大概是这样 的

http://www.ledu365.com/(栏目的中文拼音)/list_(栏目的id)_(栏目的页数).html

=> http://www.ledu365.com/${url_type}/list_${type_id}_${i}.html

于是我们可以得到

let typeArray = [
     {
        url_type: 'redu',
        type: '热读',
        type_num: 1,
    },
     {
        url_type: 'wenyuan',
        type: '文苑',
        type_num: 2,
    },
     {
        url_type: 'qinggan',
        type: '情感',
        type_num: 3,
    },
   {
        url_type: 'shehui',
        type: '社会',
        type_num: 4,
    },
     {
        url_type: 'shenghuo',
        type: '生活',
        type_num: 5,
    },
     {
        url_type: 'rensheng',
        type: '人生',
        type_num: 6,
    },
    {
        url_type: 'renwu',
        type: '人物',
        type_num: 7,
    },
    lizhi = {
        url_type: 'lizhi',
        type: '励志',
        type_num: 8,
    },
    {
        url_type: 'shiye',
        type: '视野',
        type_num: 9,
    },
     {
        url_type: 'xinling',
        type: '心灵',
        type_num: 10,
    },
     {
        url_type: 'xiaoyuan',
        type: '校园',
        type_num: 11,
    },
    {
        url_type: 'zhichang',
        type: '职场',
        type_num: 12,
    }
]

我们查看列表页的源代码,分析一下

  • 喝古龙的酒,还是下金庸的棋

    古龙爱写中年人。 李寻欢,四五十岁;傅红雪,近四十;楚留香,一路从少年写到中年…… 不过,古龙笔下的中年人,一点都不“中年”,反而很“中二”。 比如,被...

  • 曾醉

    山深问桃花 染过几度春 哪一枝勾住过客衣上风尘 有倦意三分 要行路千程 怎记这山水静默待离人 轻舟邀斜月 听岸上孤筝 回旋处 恰趁醉 踏青石前门 有余音两声 共与...

  • 写给超级马里奥的信

    嗨,马里奥老兄,停下你不止息的奔跑与跳跃吧,让金币先在天空闪亮一会儿,让蘑菇先在城墙里躲藏片刻。找块软软的草地悠闲地躺下,对着蓝天白云读读我的来信怎么...

  • 不能说的秘密_by尤妮妮

    不过很快,简慧就被带到了李宽的粉丝见面会上。 车中,简慧接过瓶子,有些犹豫:“那我要是失败了呢,我……”“不,如果那个人是你的话,就不会失败,”他打断...

  • 微写作7篇:201605

    星,无眠,伴长夜。薄雾起于南山。半月隐于柳枝间,愁怨。此日思月圆。轻起罗裳倚栏杆。恨不甘,当时情未言。若相逢,折花送,独钟。月圆星儿众。...

  • 第三把刀

    风岚帝国是整片大陆上最强盛的国家,如果单论武力,没有一个国家能与之相提并论。风岚族人人以习武为荣。风岚人十分讲究江湖道义,国内大小教派林立,一派欣欣向...

  • 贾宝玉和西门庆的共同爱好

    贾宝玉与西门庆有什么共同点? 这个问题一出,一定会让许多贾宝玉的粉丝立刻炸裂!虽然他俩都在女人圈里混,可是我们玉兄明明只得“意淫”二字,毕生致力于为年...

  • 割雪的季节

    札幌眼下正是割冰的盛时。不,也许应该说是割雪吧。 今年的札幌,比起历年来雪都下得多。这不,进入二月之后,又忽然下起来了。 讽刺的是,往年,一到二月初旬的...

  • 犬人

    有一妇人,中年得子,视若掌珍。凡诸百事,均不使为。及至弱冠,衣食起居,须人料理,如襁褓然。或有老者,劝妇人曰:“当教使言语。”妇人答曰:“我在,彼何必...

  • 谈一场情商高的恋爱

    金庸小说里情商排名前三的人里,定有任盈盈一席之地。说到任大小姐的情商,那些聪明伶俐的女主角们都要靠边站。所有人都知道,令狐冲心里有个念兹在兹的小师妹,...

  • 《红楼梦》里的鄙视链

    和地区鄙视链内陆—沿海—海外从里往外走模式不同,恰恰相反,红楼鄙视链是从外往里走,看大门.底层,其次守二门—扫院子—进房铺床叠被,最高端是屋里人,也即...

  • 以己养鸟的悲剧

    庄子是个很有意思的人,经常讲一些很有寓意的小故事。他有个故事叫《鲁王养鸟》:昔者海鸟止于鲁郊,鲁侯御而觞之于庙,奏九韶为乐,具太牢以为膳。鸟乃眩视忧悲...

  • 致妖猴:我凭什么要帮你

    孙悟空最让猴粉失望之处,就是他在打怪时,不断地向天庭或西方势力求助。 现在都流行大数据,我们就用数据来说话:西游里的所谓九九八十一难,其实有很大的水分...

  • 生死搏杀

    藏医散木旦除了给人看病,还得到山林里采药。每次出门,他都要带上牦牛扎科阿尼和藏獒柔桑吉。扎科阿尼是他的坐骑,柔桑吉可替他带路,万一遇到危险还能成为帮手...

  • 我不想自己看这落寞的人世间

    《海贼王》开始看的是娜美的故事。一个小孩子,倔强地看着凶恶的鱼人,要攒到1亿贝利来解放村子。 即使是第一次接触这动画,也被感动得一塌糊涂。一个人在寝室里...

这个时候我们可以通过dom操作 获取 相应 的 标题、缩略图和摘要

分析内容页

写给超级马里奥的信

时间:2016-03-30 21:38 来源:网络 作者:郝小靖

嗨,马里奥老兄,停下你不止息的奔跑与跳跃吧,让金币先在天空闪亮一会儿,让蘑菇先在城墙里躲藏片刻。找块软软的草地悠闲地躺下,对着蓝天白云读读我的来信怎么样?

我明白这对你来说或许很新奇,毕竟已经这个年代了,没有谁还执着于用信来交流的形式。

在我小时候,要与你相遇是很麻烦的事情。首先要有一台体积不算小的游戏机,还要有一个像是录音带般大小的游戏卡带,将卡带插进游戏机里再连接上电视,我才能看到你那令人魂牵梦萦的敏捷身影。但是我们团聚的时间总是如此短暂,还没等我沉浸在幽黑的水管世界多久,甚至还没有来得及跟随你一起跳过火焰海、躲过长长的旋转不停的火焰棒将公主救出来,我那比库克大魔王还凶猛的妈妈就会大吼着拆散我们俩,并指着钟表上的时间对我怒目而视。

写给超级马里奥的信

回想那时候为了沉浸入你那神奇的世界,我做了多少练习题,又缠着父母撒了多少娇,简直就无法统计。有人说,求之不得才会念念不忘。我想说这句话的一定是个哲人,要不就是心理学家,不然我怎么会在高中、大学、工作之后仍向往着你的世界,就算是已过而立之年也不放弃对你的执着。

幸好,现在要与你团聚早已不像过去那般费劲,不需要有庞大的游戏机和卡带,我只要打开电脑或者滑开手机,就能跟你一起爬上升起的豆藤,在云端漫步。也能跟你潜入海洋,一边躲过不怀好意缓缓游来的章鱼,一边上下浮沉着揽入金币。而且你的世界越来越广阔,你在雪山上攀爬、在赛车道里奔驰、在银河中畅游,简直让我目眩神迷。但我仍旧不满足,都说这世界上最远的距离,是我在你身边,你却不知道我爱你。可对于我来说,这句话应该变成“我爱你,却没有办法跟你站在一起”。可恶的维度世界隔开了咱们,我无力冲破二维与三维的藩篱。

不过幸运的是,或许这事儿也不是完全没有指望。科技在发展,现实世界与数据宇宙的边界正在模糊。如果你注意到我们外界的新闻,就会发现当初坐在游戏机前的那些孩子已经变得与数据融为一体。手机就是我们延伸出去的另一个新的肢体,我们在网络上分享自己的生活,对着陌生人倾吐心声,入睡前双眼的最后一个视像是手机的屏幕,醒来后的第一个动作是滑开手机给朋友圈点赞。我们的一半行走于现实世界,却有另一半沉醉于虚拟空间。

我不知道这是好事还是坏事,毕竟我也是其中的一员。毕竟以我的浅薄之见,虚拟世界只会随着科技的发展而愈加融入日常生活,维度的距离不再是困扰物理学家的宇宙性难题,而人类的数据化指日可待。

到了那一天,或许我只要启动植入神经的芯片,一段脑电波就会化为二进制的数据如长龙般急速奔入你的世界。然后等我一睁眼,就会发现自己站在蓝天绿地之间,眼前是竖起的水管与会飞的乌龟,你在我旁边,穿着那套标志性的背带裤对着我微笑。我们会肩并肩一起腾挪跳跃;我会感受到坏蘑菇在我的脚下吧唧一声被踩成扁片;会大笑着冲向一排排金币;会在乘坐云朵电梯时尖叫着抱紧你的腰,因为我恐高;最终我会跟你一起仰起头,目瞪口呆地注视着山一样的库克大魔王,看着他迈着步地动山摇而来,然后充满恐惧却坚定地打败他,为了我那早就想要一睹芳容的绝色公主。

另外悄悄问一句,你愿意把一关才有一个的加命蘑菇让给我吗?毕竟从小时候到现在,我的通关技术都实在是不咋样啊……

 

点击下载__乐读APP(Android)__每天更新好文章

乐读APP二维码

(责任编辑:Mickey)

同理我们可以获取时间、来源、作者、文章标题和文章内容。

不多说,我们看一下具体怎么做
首先安装需要的模块,大致如下

"babel-polyfill": "^6.26.0",
"cheerio": "^1.0.0-rc.1",
"ejs": "~2.5.6",
"express": "~4.15.2",
"iconv-lite": "^0.4.17",
"mysql": "^2.13.0",
"promise-mysql": "^3.0.1",
"request-promise": "^4.2.1"

其中 cheerio 是用来抓取网页数据的,iconv-lite 用来解决中文乱码, request-promise 是一个异步请求模块,跟fetch的差不多,只不过是用于服务端。

babel-polyfill 用来编译 es6

index.js

require('babel-polyfill');
const express = require('express');
const router = express.Router();
const conn = require('../server/db_connection');
const fs = require('fs');
//爬取新闻
const cheerio = require('cheerio');
const request = require('request-promise');
const iconv = require('iconv-lite');

let delayTime = 1000; //每轮间隔的时间
let currentUrl = 0; //成功写入的文章数数目
let noImgNum = 0; //剔除的没有图片的文章数目
let damagedArticle = 0; //无效链接文章数目
let noList = 0; //无效的链接列表数目
let writerFail = 0; //写入数据库失败的文章数目


router.getArticleUrl = async (req, res, next)=>{
    let startTime = getNow();
    let start_page = 1;
    let end_page = 2;
    //getListPage(start_page, end_page, "校园"); 有兴趣的话里面的参数可以配置成客户端传进来,那样就可以动态爬取,用req.query来获取,
    let listPages = getListPage(start_page, end_page);
    let listUrlsArr = await asyncControl(listPages, getListUrl, 10, delayTime);
    let listUrls = listUrlsArr.reduce((a,b)=>a.concat(b));
    let listLen = listUrls.length;
    console.log('爬取列表页完成,共有文章:'+ listLen );
    let PageContent = await asyncControl(listUrls, getPageContent, 10, delayTime);
    let endTime = getNow();
    console.log('开始时间:' + startTime);
    console.log('结束时间:' + endTime);
    console.log('原有数据:' + ' ' + listLen);
    console.log('剔除没有图片或者获取图片失败的文章数:' + ' ' + noImgNum);
    console.log('文章获取失败数:' + ' ' + damagedArticle);
    console.log('文章写入数据库失败数:' + ' ' + writerFail);
    console.log('共爬取有效文章数:' + ' ' + currentUrl);
    console.log('爬虫结束');
    res.send('爬虫结束');
}

//取得所有的列表页
/**
* 
* @param {*} start_page 起始页面
* @param {*} end_page 结束页面
* @param {*} type 爬取的类型
*/
const getListPage = (start_page, end_page, type)=>{
    let listPages = [];
    let typeArray = [
        {
            url_type: 'redu',
            type: '热读',
            type_id: 1,
            start_page,
            end_page
        },
        {
            url_type: 'wenyuan',
            type: '文苑',
            type_id: 2,
            start_page,
            end_page
        },
        {
            url_type: 'qinggan',
            type: '情感',
            type_id: 3,
            start_page,
            end_page
        },
    {
            url_type: 'shehui',
            type: '社会',
            type_id: 4,
            start_page,
            end_page
        },
        {
            url_type: 'shenghuo',
            type: '生活',
            type_id: 5,
            start_page,
            end_page
        },
        {
            url_type: 'rensheng',
            type: '人生',
            type_id: 6,
            start_page,
            end_page
        },
        {
            url_type: 'renwu',
            type: '人物',
            type_id: 7,
            start_page,
            end_page
        },
        lizhi = {
            url_type: 'lizhi',
            type: '励志',
            type_id: 8,
            start_page,
            end_page
        },
        {
            url_type: 'shiye',
            type: '视野',
            type_id: 9,
            start_page,
            end_page
        },
        {
            url_type: 'xinling',
            type: '心灵',
            type_id: 10,
            start_page,
            end_page
        },
        {
            url_type: 'xiaoyuan',
            type: '校园',
            type_id: 11,
            start_page,
            end_page
        },
        {
            url_type: 'zhichang',
            type: '职场',
            type_id: 12,
            start_page,
            end_page
        }
    ];
    if(type){
        let match = typeArray.filter(item=>item.type === type);
        if(match.length) {
            let {url_type, type, type_id } = match[0];
            for(let i = start_page; i< end_page; i++){
                listPages.push({
                    url_type,
                    type,
                    link: `http://www.ledu365.com/${url_type}/list_${type_id}_${i}.html`
                })
            }
            return listPages;
        }
    }
    for (let n = 0; n < typeArray.length; n++) {
        for (let i = typeArray[n].start_page; i < typeArray[n].end_page; i++) {
            let url_type = typeArray[n].url_type;
            let type_id = typeArray[n].type_id;
            let type = typeArray[n].type;
            listPages.push({
                url_type,
                type,
                link: `http://www.ledu365.com/${url_type}/list_${type_id}_${i}.html`
            })
        }
    }
    return listPages;
}


//取得所有的列表页的所有列表链接
const getListUrl = async (liItem, count)=>{
    //爬取文章链接
    let articleUrl = [];
    let listOption = {
        url: liItem.link,
        headers: {
            'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
        },
        encoding: null
    }
    let result = null;
    try {
        result = await request(listOption);
    } catch (error) {
        ++noList;
        console.log('错误链接列表地址:'+ liItem.link );
        console.log('错误链接列表:'+noList );
        return false;
    }
    console.log('请求列表页成功')
    console.log('数目:' +( count+1))
    let body = iconv.decode(result, 'gb2312');
    let $ = cheerio.load(body, {
        decodeEntities: false
    });
    let links = $('.listbox .title')
    links.map(function (index, item) {
        if(!$('.listbox .preview img')[index]){
            return false;
        }
        let imgSrc = $('.listbox .preview img')[index].attribs.src || '';
        if (!imgSrc) {
            ++noImgNum;
            return false;
        }
        if (imgSrc.indexOf('http') == -1) {
            imgSrc = 'http://www.ledu365.com' + imgSrc;
        }
        articleUrl.push({
            link:$('.listbox .title')[index].attribs.href,
            type: liItem.type,
            url_type: liItem.url_type,
            imgSrc
        });
        return true;
    })
    return articleUrl;
}

//取得所有的文章
const getPageContent = async(article, count)=>{
    let articleOption = {
        url: 'http://www.ledu365.com' + article.link,
        headers: {
            'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrom' +
                    'e/58.0.3029.110 Safari/537.36',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8'
        },
        encoding: null
    }
    //爬取文章
    let articleBody = null;
    try {
        articleBody = await request(articleOption);
    } catch (error) {
        ++damagedArticle;
        return false;
    }
    let body = iconv.decode(articleBody, 'gb2312');
    let $ = cheerio.load(body, {decodeEntities: false});
    let title = $('.title h2').text().trim();
    let info = $('.info').text();
    let pArray = $('p').toArray()
    let splitArray = info.split(' ');
    let date = '';
    let source = '';
    let writer = '';
    let type = article.type;
    let url_type = article.url_type;
    let cutImgUrl = article.imgSrc;
    console.log( `当前进行:${++count}, 文章:${title} `)
    if (splitArray[0] && splitArray[0].indexOf('时间:') != -1) {
        date = splitArray[0].split('时间:')[1]
    }
    if (splitArray[1] && splitArray[1].indexOf('来源:') != -1) {
        source = splitArray[1].split('来源:')[1]
    }
    if (splitArray[2] && splitArray[2].indexOf('作者:') != -1) {
        writer = splitArray[2].split('作者:')[1]
    }

    let summary = $('.content p').text();
    let contenText = '';

    $('.content p').each(function (index, item) {
        let trimText = $(this).text().trim();
        if (trimText != '' && (trimText != ('点击下载__乐读APP(Android)__每天更新好文章'))) {
            contenText += `

${trimText}

`; } }) if (!cutImgUrl) { ++noImgNum; return false; } let cutExtName = ('.' + cutImgUrl.substr(-10).split('.')[1]).replace('!cut', ''); let cutImgSrc = url_type + '_' + new Date().getTime().toString() + '!cut' + cutExtName; let folder_exists = fs.existsSync('public/images/cut/'); if(!folder_exists) fs.mkdirSync('public/images/cut/'); let cutOutResult = await writeImg(cutImgUrl, 'cut/'+cutImgSrc); if(!cutOutResult) { ++noImgNum; return false; } let imgSrc = $('.content div img').attr('src') || ''; if (!imgSrc) { ++noImgNum; return false; } if (imgSrc.indexOf('http') == -1) { imgSrc = 'http://www.ledu365.com' + imgSrc; } let extName = imgSrc.substr(-4); let downImgSrc = url_type + '_' + new Date().getTime().toString() + extName; let imgOutResult = await writeImg(imgSrc, downImgSrc); if(!imgOutResult) { ++noImgNum; return false; } //写入数据库 let sql = `INSERT INTO article(article_title ,article_date ,article_source ,article_writer ,article_img, article_content, article_type, article_url_type, article_summary,article_cutImg) VALUES(${conn.escape(title)},${conn.escape(date)},${conn.escape(source)},${conn.escape(writer)},${conn.escape(downImgSrc)},${conn.escape(contenText)},${conn.escape(type)},${conn.escape(url_type)},${conn.escape(summary)},${conn.escape(cutImgSrc)})`; let insertRes = null; try { insertRes = await conn.query(sql); console.log('写入数据库:' + (++currentUrl)); } catch (error) { console.log("写入数据库:失败:" + err); ++writerFail; return false; } } //写入图片 const writeImg = async(url, name)=>{ let imgHeader = { 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrom' + 'e/58.0.3029.110 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8' } let cutImgOption = { url: url, headers: imgHeader, encoding: null } let cutResponse = null; try { cutResponse = await request(cutImgOption); } catch (error) { console.log('读取图片失败' ,error) return false; } let cutOutResult = null; try { let cutOut = fs.createWriteStream('public/images/' + name); cutOut.write(cutResponse); cutOutResult = await new Promise(resolve=>{ cutOut.end('写入完成', ()=> { if (cutOut.bytesWritten < 1) { fs.unlink('public/images/' + name); return resolve(false) } return resolve(true) }); }) } catch (error) { console.log('写入图片失败' ,error) return false; } return true; } //获取当前时间 const getNow = () => { let now = new Date().getHours() + '时' + new Date().getMinutes() + '分' + new Date().getSeconds() + '秒'; return now; } /** * * @param {*} arr 传入需要控制的数组 * @param {*} todo 单个元素需要执行的函数 function(item, index) * @param {*} limit 并发数 * @param {*} delay 每轮并发的间隔时间 */ const asyncControl = async(arr, todo, limit, delay) => { let count = arr.length let results = [] let fn = async(arr, todo, limit, delay, results) => { if (!arr.length) return results; let current = arr.splice(0, limit); let itemResults = await Promise.all(current.map((item, index) => todo(item, count - arr.length - (current.length - 1 - index) - 1))) results = results.concat(itemResults.filter(item => !!item)); if (arr.length) { await new Promise(resolve => setTimeout(() => resolve(true), delay)); return fn(arr, todo, limit, delay, results); } else { return results; } } return fn(arr, todo, limit, delay, results); } module.exports = router;

db_connection.js

require('babel-polyfill');
const mysql = require('mysql');
const pool  = mysql.createPool({
connectionLimit : 10,
host: 'localhost',
port: 3306,
user: 'root',
password : '',
database: 'newsdemo',
});

const query = (sql)=>{
return new Promise((resolve, reject)=>{
    return pool.query(sql, (error, results, fields)=>{
    if(error)  resolve(error, fields);
    resolve(results, fields);
    })
})
}

module.exports.query = query;
module.exports.escape = mysql.escape;

article 表

github地址 https://github.com/m-Ryan/an_...

git clone [email protected]:m-Ryan/an_example_of_a_reptile.git
运行:
npm install
npm start
打开网页 http://localhost:3001/

你可能感兴趣的:(nodejs爬虫)