通过node.js自动抓站(静态页面)

通过node.js自动抓站(静态页面)

BUG集散地

2019-06-12 09:14 阅读 48 喜欢 0 爬虫 抓站 nodejs

前几天给朋友帮忙,想要一个一模一样的网站...自告奋勇去帮忙.. 结果发现之前一直没处理过类似的情况,虽然也写过爬虫,不过看了下网站,也不算麻烦,于是简单实现了这个自动抓站的功能。

这里先说下目标网址:http://www.sddingda.cn ,大体看了下,页面内的链接都是相对地址,外链肯定是绝对地址,这里就不考虑了。

首先,一个页面的组成肯定是包括:html + 静态资源(css + js + img 等等) ,所以我们只要确保完全把页面内的数据下载下来就可以了,剩下的就是地址替换和匹配问题。

本身他的站其实是动态的,不是纯粹静态的,所以就导致很多页面在后台实际上是一个页面,但是对于浏览器来说是不同的页面,但是也没办法,如果想完全还原,而且不做动态.. 只得如此了.. 。

既然是抓静态站的话,那么抓站就简单多了,只需要访问然后保存即可,无非是解析内容,然后通过nodejs 把静态资源下载而已。

思路

Ps: 这里说明下.. css中还有很多图片的,需要匹配出来去下载的。

准备

以上各个包的作用大家一查就了解了,这里不多做介绍。

分布实现

页面抓取并分析内容下载

fetch.js

let axios = require('axios');
let fs = require('fs');
let mkdirsp = require('mkdirsp');
let path = require('path');
let cheerio = require('cheerio');
let superagent = require('superagent');
let async = require('async');

/**
 * 抓取指定地址内的html内容并返回
 **/
let fetchHtml = function(url){
    return axios.get(url)
    .then(res=>{
        return res.data;
    })
}
/***
 * 将html写如指定的文件中
 **/
let writeHtml = function(filePath,content){
    let parentDir = path.dirname(filePath);
    return mkdirsp(parentDir)
    .then(()=>{
        return writeFile(filePath,content);
    })
}
let writeFile = function(filePath,content){
    return new Promise((resolve,reject)=>{
        fs.writeFile(filePath,content,function(err){
            if(err){
                reject(err);
            }else{
                resolve();
            }
        })
    });
}
/***
 * 解析html中的字段,将css img script 全部下载下来,如果是全路径则不处理,只针对相对路径
 * 如果是a标签 并且是相对路径,则返回a标签数据
 ***/
let analysisHtml = async function(html,folderPath,hostname){
    return new Promise((resolve,reject)=>{


        let $ = cheerio.load(html);
        let $link = $('link'),
            $script = $('script'),
            $img = $('img'),
            $a = $('a');//页面

        // console.log(`
        // 当前页面中,包含css文件 ${$link.length} 个;
        // 包含js文件 ${$script.length} 个;
        // 包含img文件 ${$img.length} 个;
        // `);
        //循环处理。
        let arr = [];//需要下载的静态文件
        $link.each((i,item)=>{
            let href = $(item).attr('href');
            if(href.startsWith('http') || href.startsWith('//') ){}else{
                arr.push(href);
            }
        })
        $script.each((i,item)=>{
            let href = $(item).attr('src');
            if(href == null || href == undefined || href.startsWith('http') || href.startsWith('//') ){}else{
                arr.push(href);
            }
        })
        $img.each((i,item)=>{
            let href = $(item).attr('src');
            if(href.startsWith('http') || href.startsWith('//')){}else{
                arr.push(href);
            }
        })
        var pageArr = [];
        $a.each((i,item)=>{
            let href = ($(item).attr('href') || '');
            href = href.toLowerCase().trim();
            if(href!= '' && href.indexOf('javascript') < 0 && !href.startsWith('http') && href.length > 2
                && !href.startsWith('#')
                ){
                let name = path.basename(href)+'.html';
                let realPath = hostname + (href.startsWith('/') ? href : ('/'+href));
                pageArr.push({
                    url : realPath,
                    name : name
                })
            }
        })
        resolve({
            page : pageArr,
            static : arr
        });
    });
}

/***
 * 下载静态资源文件,js img css
 **/
let download = function(href,folderPath,hostname,cb){
    let abpath = path.dirname(path.join(folderPath,href));
    let realPath = hostname+''+href;
    mkdirsp(abpath)
    .then(function(){
        return superagent.get(realPath).buffer(true);
    })
    .then(res=>{
        let extname = (path.extname(href)).toLowerCase();
        console.log(realPath);
        let content = null;
        if(extname == '.css' || extname == '.js'){
            content = res.text;
        }else{
            content = res.body;
        }
        fs.writeFile(path.join(folderPath,href),content,function(){
            setTimeout(function(){
                cb(null);
            },3000)
        });
    })
    .catch(err=>{
        console.log(err);
        cb(err);
    })
}

module.exports = {
    //暴露API
    fetchHtml : fetchHtml,
    writeFile : writeFile,
    writeHtml : writeHtml,
    analysisHtml : analysisHtml,
    download : download
}

读取css 并解析下载内容

fetchcss.js

//查找css文件中的图片并下载到对应的目录中
let fs = require('fs');
let path = require('path');


function getCssFile(targetFolder){
    let files = fs.readdirSync(targetFolder);
    let arr = [];
    if(files.length > 0){
        files.forEach(item=>{
            let realPath =path.join(targetFolder,item);
            //判断是否是css和文件夹
            let extname = (path.extname(realPath) || '').toString().toLowerCase();
            let stats = fs.statSync(realPath);
            if(extname == '.css'){
                arr.push(realPath);
            }else if(stats.isDirectory()){
                arr = arr.concat(getCssFile(realPath));
            }
        })
    }
    return arr;
}

function search(realPath,targetFolder,hostname){
    let content = fs.readFileSync(realPath);
    content = content.toString();
    let arr = content.match(/url\(([\s\S^]*?)\)/g);
    let dirFolder = path.dirname(realPath);
    if(arr && arr.length > 0){//存在
        let urlArr = arr.map(item=>{
            let tempPath = (item.match(/url\(([\s\S^]*?)\)/)[1]).trim();
            let filePath = path.join(dirFolder,tempPath);
            let xdpath = filePath.replace(targetFolder,'');
            xdpath = xdpath.replace(/\\/g,'/');
            let urlPath = hostname +'/'+ xdpath;
            return {
                filePath : filePath,
                url : urlPath
            }
        });
        //然后对url根据当前路径匹配一个正确的地址。
        return urlArr;
    }
    return [];
}

function searchCss(targetFolder,hostname){
    return new Promise((resolve,reject)=>{
        let cssFiles = getCssFile(targetFolder);
        let rs = [];
        cssFiles.forEach(item=>{
            rs.concat(search(item,targetFolder,hostname));
        })
        async.mapLimit(rs,1,function(item,cb){
            download(item,cb);
        },function(){
            console.log(`all css files image has downloaded`);
            resolve();
        });
    });
}

let download = function(item,cb){
    let filePath = item.filePath,url = item.url;
    let abpath = path.dirname(filePath);
    let realPath = hostname+''+href;
    mkdirsp(abpath)
    .then(function(){
        return superagent.get(url).buffer(true);
    })
    .then(res=>{
        let extname = (path.extname(href)).toLowerCase();
        let content = null;
        if(extname == '.css' || extname == '.js'){
            content = res.text;
        }else{
            content = res.body;
        }
        fs.writeFile(filePath,content,function(){
            setTimeout(function(){
                cb(null);
            },3000)
        });
    })
    .catch(err=>{
        console.log(err);
        cb(err);
    })
}

module.exports = searchCss;

最后替换相对路径地址

replace.js

//根据json内容,替换所有html中的地址页面
let cheerio = require('cheerio');
let path = require('path');
let async = require('async');
let fs = require('fs');

let json = require('../name.json');



function rephref (targetFolder,targetUrl){
    return new Promise((resolve,reject)=>{
        fs.readdir(targetFolder,function(err,files){
            let arr = [];
            files.forEach(function(temp){
                if(path.extname(temp) == '.html'){
                    arr.push(temp);
                }
            })
            async.mapLimit(arr,1,function(item,cb){
                item = path.join(targetFolder,item);
                re(item,targetUrl,cb);
            },function(){
                console.log('全部处理完毕')
            })
        });
    });
}



function re (item,targetUrl,cb){

    let content = fs.readFileSync(item);
    //替换
    let $ = cheerio.load(content);
    let $a = $('a');
    $a.each(function(i,item){
        let href = ($(item).attr('href')||'').trim();
        console.log(href);
        if(href!= '' && href.indexOf('javascript') < 0 && !href.startsWith('http') && href.length > 2
            && !href.startsWith('#')
        ){
            let realPath = targetUrl + (href.startsWith('/') ? href : ('/'+href));
            if(json[realPath]){
                console.log('替换:'+href);
                $(item).attr('href','/'+json[realPath]+'.html');
            }
        }

    })
    let html = $('html').html();
    html = '<html>'+html+'</html>';
    fs.writeFileSync(item,html);
    cb();
}

module.exports = rephref;

主函数执行

app.js

/****
全站爬虫
@author chrunlee
@description 提供入口地址,并进行抓取所有页面


****/

let targetUrl =  '此处省略';
let name = '名字只是代号';

let path = require('path');
let async = require('async');
let fs = require('fs');
let pageCount = 0;//用于映射。

let nameMap = {};

const fetch = require('./lib/fetch');
const rephref = require('./lib/replace');
const fetchcss = require('./lib/fetchcss');

let folderPath = path.join(__dirname,'site',name);

let static = [];//静态资源地址存放
let page = [{url : targetUrl,name : 'index'}];//页面存放,入口函数
let pageMap = {};//是否已经抓取过,包括静态资源

pageMap[targetUrl] = false;//


function start(){

    if(page.length > 0){
        var newPage = [];
        page.forEach(item=>{
            newPage.push(item);
        })
        let length = newPage.length;
        async.mapLimit(newPage,1,function(item,cb){
            fetchPage(item,cb);
        },function(){
            page.splice(0,length);
            console.log('当前已经抓取完毕')
            console.log('新的一轮:'+page.length);
            start();//继续啊
        })
    }else{
        console.log(page);
        console.log(`no page to found`);
        async.mapLimit(static,1,function(item,cb){
            fetch.download(item,folderPath,targetUrl,cb);
        },function(){
            console.log(`静态页面抓取完毕`)
            //写入namemap
            fs.writeFileSync('name.json',JSON.stringify(nameMap));

            rephref(folderPath,targetUrl)
            .then(rs=>{
                console.log('文件替换完毕')
            })
            fetchcss(folderPath,targetUrl)
            .then(rs=>{
                console.log('css 查找完毕')
            })
        })
    }
}
function fetchPage(item,cb){
    if(!item){
        cb();
    }
    console.log(item);
    let url = item.url,name = item.name;
    var content = '';
    fetch.fetchHtml(url)
    .then(html=>{
        content = html;
        pageCount += 1;
        if(name == 'index'){
            nameMap[url] = name;//首页除外
        }else{
            nameMap[url] = pageCount;    
        }
        fs.writeFileSync(path.join(folderPath,(name == 'index' ? name : pageCount)+'.html'),html);//写入
        return fetch.analysisHtml(html,folderPath,targetUrl);
    })
    .then(res=>{
        let tempA = res.page,tempB = res.static;
        let count = 0;
        tempA.forEach(a=>{
            if(!pageMap[a.url]){
                pageMap[a.url] = true;
                page.push(a);
                count ++ ;
            }
        })
        console.log(`当前获得有效页面:${count}个,共计${tempA.length}个`);
        tempB.forEach(a=>{
            if(!pageMap[a]){
                pageMap[a] = true;
                static.push(a);
            }
        })
        setTimeout(function(){
            cb(null);
        },3000)
    })
}

start();


转载请注明出处: https://chrunlee.cn/article/nodejs-fetch-site.html


如果对你有用的话,请赏给作者一个馒头吧 ...

赞赏支持
提交评论
评论信息(请文明评论)
暂无评论,快来快来写想法...
推荐
记录下通过nodejs调用imagemagick 的时候发现的一个错误,command failed -- crop .
在使用marked来做md解析的时候,部分解析规则可能并不是很如意,比如说,我在md中写了a标签,但是这些标签都是在当前页面替换的,而我想要的是新打开窗口。
前段时间学习到了nodejs的net模块这部分,正好想实现一个局域网内的文件下载小demo,噔噔噔噔... 兴趣推动 ,马上搞一搞。
开始入手webpack ,直接看的官方文档和demo,对于自动刷新这部分还是希望通过express 加载插件来控制,但是文档没有提供,经过参考github上其他高玩的套路,最终整理了一个基础的配置版本。
当我们想实现一个自己的库或模块后,发布的话,需要发布到npm上才能下载。以下是具体步骤
在开发的时候,经常会有css js 文件的变更,然后部署后发现没有起到作用,最终发现是缓存的问题,如何来方便的解决
目前了解的有两个模块可以实现二维码的模块,一个是node-qrcode ,这个算是比较大众的,不过环境比较复杂,所以...连看都没看;还有一个是小众的 qr-image ,这个比较简单,没有其他环境依赖,安装即可用,因为要实现一个简单的在线二维码生成,就先用这个试试水了
因为自己的记录笔记的应用是有道云,又想着把有道云跟自己的小网站联通起来,所以查找了有道云的,然后实现了nodejs版本的sdk.