注意 本文所述软件已在 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 项目 。