注意 本文所述软件已在 GitHub  上以 MIT 协议开源,且比文章里解释的代码先进很多。由于软件后续更新等因素,不保证仓库内软件与本文所述代码同步。用户使用该软件引发的一切效应都与作者无关。 
前言 由于搭建了自己的个人博客,需要把自己先前写的文字放到个人博客上,这就需要一个爬虫。 而我很不习惯缩进狂魔 Py ,思来想去还是决定用 JavaScript 写。
必要条件 
你自己的 WordPress 博客站 
Node.js 
优秀的网络 
灵巧的双手 
聪明的大脑 
 
环境准备 进入一个新文件夹,创建 Node 环境:
 
信息什么的随便写啦( 然后可以开始编辑 index.js 了。
开始代码 准备依赖 引入 https 库以从网站获取网页:
1 2 const  https=require ('node:https' );
 
引入 fs 库来写入文件:
1 2 const  fs=require ("node:fs" );
 
引入 async 以控制流程(不然 js 默认的异步处理会导致不必要的麻烦):
 
1 const  async =require ('async' );
 
引入 JSDOM 来实现浏览器式(你甚至都不用学 jQuery )的 HTML DOM 处理:
 
1 2 3 const  jsdom=require ("jsdom" );const  { JSDOM  } = jsdom;
 
引入 webp-converter 以实现图像的自动 WebP 转换:
1 npm install webp-converter
 
1 2 const  webp=require ('webp-converter' );
 
引入 turndown.js 以实现转换成 Markdown:
 
1 2 var  TurndownService  = require ('turndown' );var  turndownService=new  TurndownService ();
 
然后我还顺便引入了 colors-console 库以实现五彩斑斓的输出()
1 npm install colors-console
 
1 const  color = require ('colors-console' );
 
让我们先写一个简单的日志函数罢:
1 2 3 4 function  info (_info ){ 	console .log (colors ("green" , "INFO" ), _info); 	return ; }
 
获取参数 这里我们直接从命令行获取参数:
1 2 3 4 5 6 7 8 9 10 11 12 if (process.argv .length  != 5 ){    console .log (colors ("red" ,"FATAL" ),"参数数目不规范!" );   process.exit (); }var  args={    link : process.argv [2 ],   filename : process.argv [3 ],   imagePrefix : process.argv [4 ] }
 
获取网页内容 使用 https.get() 或 http.get() 方法即可。
1 2 3 4 5 6 7 8 9 htmlString=new  String (); https.get (args.link ,function (resource ){   resource.on ("data" , function (chunk ){     htmlString+= chunk;   });   resource.on ("end" , function ( ){       startGenerating (htmlString, args);    }); });
 
主要入口函数 新建一个叫 startGenerating 的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function  startGenerating (html,argument ){     info ("开始生成" );     var  srcs;     async .series ([         function (callback ){             createDir (argument);             callback (null ,"createdir" ); 		},         function (callback ){             srcs= loadDOM (html,argument);             callback (null ,"loaddom" );         },         function (callback ){              processImages (srcs,argument);             callback (null ,"downimage" );         },         function (callback ){             info ("生成完成!" );             callback (null ,"done" );         }     ]); }
 
async.series: async 库提供的顺序执行代码,接受的参数是一个函数数组。 
callback: callback(err,message); 
 
创建文件夹 1 2 3 4 5 6 7 8 9 10 11 function  createDir (argument ){     try {fs.mkdirSync (`./output/${argument.imagePrefix} ` );}     catch (err){         if (err.code !="EEXIST" ){              console .error (err);             console .log (colors ("red" , "FATAL" ), "出现错误" );             process.exit (1 );         }         console .log (colors ("yellow" , "WARN" ), `目录 ${argument.imagePrefix}  已存在` );     } }
 
解析 HTML 新建一个叫 loadDOM 的方法:
1 2 3 function  loadDOM (_html, argument ){ }
 
载入 DOM 1 2 const  dom=new  JSDOM (_html);var  document =dom.window .document ;
 
获取文章主体元素 1 2 var  div=document .querySelector ("div.entry-content" );info ("DOM 已载入" );
 
在这里使用 document.querySelector 这样的浏览器式代码是得益于 JSDOM 。 
在 WordPress 中,至少在 University of Fool 使用的 Vilva 主题中,文章主体元素就是 `` 
 
获取文章图像列表 1 2 3 4 5 var  srcs=new  Array (); div.querySelectorAll ('img' ).forEach (function (node,n ){ 	srcs[n]=node.src ; 	node.src =(`/img/${argument.imagePrefix} /${argument.imagePrefix} -${n} .webp` ); });
 
parentNode.querySelectorAll(selector): 从该元素的所有子元素里选择与 selector 匹配的元素,返回值的类型是 NodeList 
NodeList.forEach: DOM 模仿 JavaScript 自带的 Array 类型的 forEach 方法,用法是一样的 
node.src: `` 等元素的 src 属性值 
此处 srcs 数组是为了下载图片并转换做准备。 
 
去除所有的样式表 1 2 3 div.querySelectorAll ('style' ).forEach (function (node ){ 	node.outerHTML =null ; });
 
Node.outerHTML: 这个节点本身的 HTML 代码 
 
去除标题外层的 <span> 我们看一看源代码,可以看见标题外面有两个 <span>: 我们应该把它去除:
1 2 3 4 div.querySelectorAll ('h1, h2, h3, h4, h5, h6' ).forEach (function (node ){ 	var  headingText=node.textContent ; 	node.innerHTML =headingText; });
 
Node.textContent: 返回节点内所有 HTML 标签的内容,不含 HTML 标签本身。 
 
去除代码块的多余内容 我们再看一看源码: 要去除。
1 2 3 4 div.querySelectorAll ('pre' ).forEach (function (node ){ 	var  codeText=node.querySelector ('code' ).textContent ; 	node.outerHTML =`<pre><code>${codeText} \n</code></pre>` ; });
 
去除空行 1 2 3 4 5 6 var  html=new  String (); div.innerHTML .split ('\n' ).forEach (function (line ){ 	if (line!=new  String ()){ 		html+=`${line} \n` ; 	} });
 
转换成 Markdown 1 2 var  output=turndownService.turndown (html);info ("文件已改写" );
 
写入文件 1 2 3 4 5 6 try {fs.writeFileSync (`./output/${argument.filename} ` , output, 'utf8' );}catch (err){ 	console .error (err); 	console .log (colors ("red" , "FATAL" ), "出现错误" ); 	process.exit (1 ); }
 
结束函数 1 2 info (`${fileType}  文件已写入` );return  srcs;
 
处理图片 -> 入口函数 为了防止异步执行和简化代码结构,我把入口函数单独分了出来:
1 2 3 4 5 6 function  processImages (srcs,argument ){     info (`开始下载和转换图片,总计 ${srcs.length.toString()}  个` );     srcs.forEach (function (value,key ){         processSingleImage (value, key, argument);     }); }
 
处理单张图片 入口函数 新建函数 processSingleImage:
1 2 3 function  processSingleImage (source, n, argument ){ 	 }
 
下载图片 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 info (`正在下载图片 ${n+1 } ` ); https.get (source, function  (response ) { 	var  data=new  String ();  	response.setEncoding ("binary" );  	response.on ("data" , function  (chunk ) { 		data+=chunk; 	}); 	response.on ("end" ,function ( ){ 		fs.writeFileSync (('./cache/'  + n.toString () + '.png' ), data, 'binary' , err  =>  { 			if  (err) { 				console .error (err); 				console .log (colors ("red" , "FATAL" ), "出现错误" ); 				process.exit (1 ); 			} 		}); 		info (`图片 ${n+1 }  下载完成` ); 		 	}); });
 
转换成 WebP 1 2 3 4 5 6 7 8 9 10 11 try { 	webp.cwebp (`./cache/${n} .png` , `./output/${argument.imagePrefix} /${argument.imagePrefix} -${n} .webp` , "-q 70 -alpha_q 50" ); }catch (err){ 	console .error (err); 	console .log (colors ("red" , "FATAL" ), "出现错误" ); 	process.exit (1 ); }finally { 	info (`图片 ${n+1 }  转换完成` ); }
 
webp.cwebp(source, output, argument): source 是需要转换成 WebP 的文件路径, output 是输出的文件路径, argument 是传递给 Google cwebp 命令行工具的额外参数 
try-catch-finally: 先运行 try ,如果出现错误就运行 catch(我们在 catch 内退出了程序),最后运行 finally。 
 
开始爬取 写好之后 记得保存!记得保存!记得保存!  ,运行我们写好的代码:
1 node index.js <网页地址> <输出文件> <图像前缀>
 
然后,你应该能在 output/ 里看到生成的文件
后记 这篇文章只是记下了我写 html2blog.js 时的思路,而且算是最初版和最新版的糅合(?),因此不保证使用。 如果你确实需要使用我的文章,请查看 GitHub 项目 。