前几天给朋友帮忙,想要一个一模一样的网站...自告奋勇去帮忙.. 结果发现之前一直没处理过类似的情况,虽然也写过爬虫,不过看了下网站,也不算麻烦,于是简单实现了这个自动抓站的功能,最终整理成为一个自动抓站的工具,能省很多的事情。
这里先说下目标网址:http://www.sddingda.cn ,大体看了下,页面内的链接都是相对地址,外链肯定是绝对地址,这里就不考虑了。
首先,一个页面的组成肯定是包括:html + 静态资源(css + js + img 等等) ,所以我们只要确保完全把页面内的数据下载下来就可以了,剩下的就是地址替换和匹配问题。
本身他的站其实是动态的,不是纯粹静态的,所以就导致很多页面在后台实际上是一个页面,但是对于浏览器来说是不同的页面,但是也没办法,如果想完全还原,而且不做动态.. 只得如此了.. 。
既然是抓静态站的话,那么抓站就简单多了,只需要访问然后保存即可,无非是解析内容,然后通过nodejs 把静态资源下载而已。
Ps: 这里说明下.. css中还有很多图片的,需要匹配出来去下载的。
fs
path
async
axios
mkdirsp
cheerio
superagent
以上各个包的作用大家一查就了解了,这里不多做介绍。
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
}
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