通过node.js实现抓站工具自动抓站(静态页面)

通过node.js实现抓站工具自动抓站(静态页面)

月光魔力鸭

2019-06-12 09:14 阅读 1400 喜欢 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


感谢支持!

赞赏支持
提交评论
评论信息 (请文明评论)
暂无评论,快来快来写想法...
推荐
介绍几个日常开发中常用的几个小工具: anywhere / anywhere-auth / watchlessc / changeext
从豆瓣转到网易云后,发现了不少好听的歌曲,然鹅..当我想把这些歌拿下来扔车上听的时候发现竟然不允许下载..能听不能下?这不科学,作为一名程序猿,必然要迎难而上啊.
通过nodejs来进行爬取页面的内容,这里简单试试做个小任务..
使用nodejs 连接mysql数据库还是很简单的,有现成的模块可以直接调用。下面介绍下 mysql 的调用
最近看到知乎上一话题:微信公众号文章里的视频怎么下载?。看还是有很多人推荐啥工具啊,很是捉急,当然本次的主题也是通过程序来获取内容,但是目前来说仅仅是娱乐吧。
关于js的编译和压缩,之前做过一个小工具了,主要就是自己项目成员大都没有这部分的技能,导致发布的时候总需要去编译压缩下.. 最终做了个命令行小工具.. 问题不在这里,前一阵子做压缩的时候发现压缩后竟然是undefined.最终才发现是es6的语法问题。
在通过axios读取页面的时候,经常会碰到gbk的编码,如果不进行转化的话,在获取信息或读取上都会很麻烦。
互联网应用经常需要存储用户上传的图片,比如facebook相册。 facebook目前存储了2600亿张照片,总大小为20PB,每张照片约为80KB。用户每周新增照片数量为10亿。(总大小60TB),平均每秒新增3500张照片(3500次写请求),读操作峰值可以达到每秒百万次