朱治龙个人博客 - 酷壳
type: channel
link: https://blog.zhuzhilong.cn/tag/酷壳/
items: 酷壳(coolshell.cn)镜像站建设经验分享之五——镜像站制作
items: 酷壳(coolshell.cn)镜像站建设经验分享之四——解析文章正文html中的图片并离线下载图片到本地
items: 酷壳(coolshell.cn)镜像站建设经验分享之三——分析爬取的文章html得到正文及相关的元数据
items: 酷壳(coolshell.cn)镜像站建设经验分享之二——整站内容爬取
items: 酷壳(coolshell.cn)镜像站建设经验分享之一——准备工作

酷壳(coolshell.cn)镜像站建设经验分享之五——镜像站制作
type: item
date: 2024-02-27T18:20:00+00:00
description: 本系列文章生成的镜像站已上传到 Gitee Pages 托管平台，可通过如下地址查看完成后的效果：https://jhlxge.gitee.io/coolshell-mirror/ 2024年6月5日更新 ：Gitee Pages服务已停止提供服务,可通过本机托管的如下地址访问：https://coolshell-mirror.work.zhuzhilong.com/通过前面的操作，我们已经得到了原站点所有的信息，为了能保障镜像站能永久提供服务，不依赖于后端的数据库及中间件，我们需要将爬取到的正文内容做静态化发布，静态化发布后，我们可以将内容托管到 Gitee 或 Github。为了满足上面的使用场景，春节期间我调研试用了市面上大部分静态网站软件：Jekyll、Hugo、Hexo、VitePress。经过多方对比，我选择了让我上手最快的 Hexo + hexo-theme-butterfly主题。主要是Butterfly 主题的颜值确实没法说，而且功能也很齐全，通过简单的折腾也可以完全定义成我们想要的任何效果，算是能满足我对制作镜像站的所有需求。下面我们就开工。安装 Hexo根据 Hexo 的官方文档（链接为：https://hexo.io/zh-cn/index.html）的指引，我本地已经安装了 Node.js，我们直接通过下面的命令全局安装 Hexo。npm install -g hexo-cli安装完 Hexo 后，我们需要使用 Hexo 的指令，初始化一个最原始的 Hexo 工程：hexo init coolshell-mirror初始化后我们得到了一个基于 Node.js 的基础工程。安装 hexo-theme-butterfly 主题根据 hexo-theme-butterfly 主题的说明，我们需要在 hexo 工程的 themes 文件夹下克隆 https://github.com/jerryc127/hexo-theme-butterfly 的所有文件。命令如下：git clone -b master https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly克隆完后我们调整工程根目录下 _config.yml 中的 theme 值为 hexo-theme-butterfly。重启服务，我们就可以看到新主题的效果Hexo 功能验证做完上面的操作后，我们根据 Hexo 的规范，在 source/_posts 目录下，添加一篇 markdown 文档，验证一下相关的内容结构，经过验证我们可以得到如下结论：1、内容元数据在 markdown 顶部的以--- 开头和结尾的中间，可以定义任意元数据，核心的元数据是 title、cover、date，tags、categories，也可以在这些元数据的基础上扩展其他的属性，新加的属性在模板中通过属性名获取数据。然后元数据后面是正文内容。2、正文内容支持常规的 markdown 格式，同时也支持 html，可以使用 {% raw %}将内容包裹起来进行原生展示。3、 添加内容后，可以使用 hexo generate 将整站内容静态化生成博文内容Hexo 功能验证完后，我们得到了正文内容的基本格式，然后我们可以使用 magic-api 根据格式生成对应的 markdown 文档，然后拷贝到 source/_posts 目录即可。以下是完整代码：import cn.hutool.core.util.ObjectUtil import cn.hutool.core.util.StrUtil import org.jsoup.Jsoup import cn.hutool.core.date.DateUtil import cn.hutool.json.JSONUtil import cn.hutool.core.io.FileUtil import log var timer = DateUtil.timer(); // 定义计时器，用于记录时间 var pageList = db['MDC'].select('select * from crawler_article where md_html is not null') log.info("待处理记录数据：" + pageList.size()) // 对正文呢日哦那个做一些替换，提供为新站点的地址 var dealContent = (content) => { content = StrUtil.replace(content,'https://coolshell.cn/articles/','/articles/') content = …

酷壳(coolshell.cn)镜像站建设经验分享之四——解析文章正文html中的图片并离线下载图片到本地
type: item
date: 2024-02-27T17:32:00+00:00
description: 本系列文章生成的镜像站已上传到 Gitee Pages 托管平台，可通过如下地址查看完成后的效果：https://jhlxge.gitee.io/coolshell-mirror/ 2024年6月5日更新 ：Gitee Pages服务已停止提供服务,可通过本机托管的如下地址访问：https://coolshell-mirror.work.zhuzhilong.com/上一步我们已经得到了文章详情所需的数据，要想保障酷壳站停掉后，我们仍然能完整的查看内容，我们需要将正文中的图片也都下载到本地，然后后续发布成镜像站的时候就不用外链图片资源了，这样也在最大程度保障原站无法访问的情况下，能通过镜像站完整的浏览博客。要想下载正文内容中的图片，我们第一步需要得到正文中的所有图片，然后将图片资源保持到资源表，然后我们再遍历资源表下载所有的资源。相关代码如下：转换正文html及解析正文中的图片资源文件import cn.hutool.core.util.NumberUtil import cn.hutool.core.util.ObjectUtil import cn.hutool.core.util.StrUtil import org.jsoup.Jsoup import cn.hutool.core.date.DateUtil import cn.hutool.json.JSONUtil import cn.hutool.core.util.URLUtil import log var timer = DateUtil.timer(); // 定义计时器，用于记录时间 var pageList = db['MDC'].select('select id,article_html from crawler_article where article_html is not null') log.info("待处理记录数据：" + pageList.size()) for (item in pageList) { var articleHtml = item.articleHtml var doc = Jsoup.parse(articleHtml) // 得到正文中所有的图片 var imgEles = doc.select('img[src]') imgEles.each(imgEle => { // 博文中有不少图片支持多终端响应式，也就是在不同的尺寸下显示不同的图片，但是我们一般不需要，所以我们把响应式相关的图片删掉 imgEle.removeAttr('srcset').removeAttr('sizes') // 得到图片URL var imgURL = imgEle.attr('src') // 部分图片是相对路径，我们需要替换成绝对路径，便于后续通过完整URL进行下载 if (StrUtil.startWith(imgURL,'../wp-content')) { imgURL = StrUtil.replace(imgURL,'../wp-content','https://www.coolshell.cn/wp-content') } // 经过分析排除掉部分不能下载的图片资源 if (StrUtil.isNotBlank(imgURL) && imgURL.indexOf('yuml.me') === -1 && imgURL.indexOf('chart.apis.google.com') === -1) { var fullPath = URLUtil.getPath(imgURL) var splitIndex = fullPath.lastIndexOf('/') var fileName = fullPath.substring(splitIndex + 1) var path = fullPath.substring(0, splitIndex + 1) var ext = '' if (fileName.indexOf('.') > 0) { ext = fileName.substring(fileName.lastIndexOf('.') + 1) } var imgObj = { siteId: 1, resourceUrl: imgURL, resourceExt: ext, sourcePath:path, sourceFilename:fileName } // 查询是否存在，如果不存在则插入，加上这个判断是一个图片有可能在多篇内容中被引用 var chkExistRes = db['MDC'].select(`select id from crawler_resource where res…

酷壳(coolshell.cn)镜像站建设经验分享之三——分析爬取的文章html得到正文及相关的元数据
type: item
date: 2024-02-27T17:14:00+00:00
description: 本系列文章生成的镜像站已上传到 Gitee Pages 托管平台，可通过如下地址查看完成后的效果：https://jhlxge.gitee.io/coolshell-mirror/ 2024年6月5日更新 ：Gitee Pages服务已停止提供服务,可通过本机托管的如下地址访问：https://coolshell-mirror.work.zhuzhilong.com/在上篇文章中我们把需要的html的内容爬取并存储到 crawler_article 表了，不少系统都是边爬取边分析，然后一步得到所需的内容，我们这边为什么要分这么多步骤来实现呢？我自己感觉这样的设计的好处是：这个过程比较长，我们很难一次性把所有的流程都实现完，并且保证不出错，一步一步来的话，在某一步出了问题，我们可以仅调整这一步的代码，这样也能将一个大任务解耦。解耦后也更便于我们做代码维护。好了，闲话少说，所需要的内容我们已经爬取完毕了，接下来我们开展下一步工作：解析获取文章元数据，这个过程的完整代码如下：import cn.hutool.core.util.NumberUtil import cn.hutool.core.util.ObjectUtil import cn.hutool.core.util.StrUtil import org.jsoup.Jsoup import cn.hutool.core.date.DateUtil import cn.hutool.json.JSONUtil import cn.hutool.core.util.URLUtil import log var timer = DateUtil.timer(); // 定义计时器，用于记录时间 var pageList = db['MDC'].select('select id,page_html from crawler_article where page_html is not null') log.info("待处理记录数据：" + pageList.size()) for (item in pageList) { var pageHtml = item.pageHtml // 1、获取内容元数据 // 1.1 获取发布时间、作者、评论数、阅读数 var doc = Jsoup.parse(pageHtml) var entryDateEle = doc.select('time.entry-date') var publishDate = '' if (ObjectUtil.isNotEmpty(entryDateEle)) { publishDate = DateUtil.parse(entryDateEle.attr('datetime')) publishDate = DateUtil.formatDateTime(publishDate) } var authorEle = doc.select('.author.vcard a') var author = '' if (ObjectUtil.isNotEmpty(authorEle)) { author = authorEle.text() } var commentEle = doc.select('a.comments-link') var commentCount = 0 if (ObjectUtil.isNotEmpty(commentEle)) { var commentCountText = StrUtil.replace(commentEle.text(),' 条评论', '') commentCountText = StrUtil.replace(commentCountText,',','') if (NumberUtil.isNumber(commentCountText)) { commentCount = Integer.parseInt(commentCountText) } } var appNodes = doc.select('.entry-date').textNodes() var viewEle = appNodes.get(appNodes.size() - 2) var viewCount = 0 if (ObjectUtil.isNotEmpty(viewEle)) { var viewText = viewEle.text().replace(' 人阅读', '') viewText = StrUtil.replace(viewText,',','') viewText = StrUtil.replace(viewText,' ','') viewCount =…

酷壳(coolshell.cn)镜像站建设经验分享之二——整站内容爬取
type: item
date: 2024-02-27T16:59:00+00:00
description: 本系列文章生成的镜像站已上传到 Gitee Pages 托管平台，可通过如下地址查看完成后的效果：https://jhlxge.gitee.io/coolshell-mirror/ 2024年6月5日更新 ：Gitee Pages服务已停止提供服务,可通过本机托管的如下地址访问：https://coolshell-mirror.work.zhuzhilong.com/在前面的数据准备章节咱们已经在 magic-boot 基础工程上整合了 jsoup，而 jsoup 正是 java 领域数据爬取及 html 解析的神器，对于 html 的处理如同 JS 中的 jQuery 一般神一样的存在，而且一些 API 跟 jQuery 也比较接近，使用起来不得不说那叫一个丝滑。废话少说，咱们开始 show me the code。01采集列表通过分析酷壳首页的内容列表，我们发现一共有74页，并且页面URL规则是：https://coolshell.cn/page/${页码}，经过验证该规则也适用于第一页，那我们就先把所有列表页的html采集过来，存储到 crawler_list 表，爬取代码如下：import cn.hutool.core.util.StrUtil import org.jsoup.Jsoup import cn.hutool.core.date.DateUtil import cn.hutool.core.thread.ThreadUtil import log var START_PAGE = 1 var END_PAGE = 74 var BASE_URL = 'https://coolshell.cn/' var currentPage = START_PAGE var timer = DateUtil.timer(); // 定义计时器，用于记录时间 var MAX_RETRY_COUNT = 100 var currentRetryCount = 0; while(currentPage <= END_PAGE) { var pageURL = currentPage === 1 ? BASE_URL : BASE_URL + "page/" + currentPage try { var doc = Jsoup.connect(pageURL).timeout(30000).userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36").get(); var html = doc.outerHtml(); log.info(pageURL) if (StrUtil.isNotBlank(html)) { db['MDC'].table('crawler_list').insert({ siteId:1, pageUrl: pageURL, pageHtml: html, successFlag:'Y' }) } currentPage++ currentRetryCount = 0 } catch(e) { // 暂停1秒，避免请求间隔太短，导致 Matomo 负载过高拿不到数据 ThreadUtil.sleep(1000); currentRetryCount++; if (currentRetryCount == MAX_RETRY_COUNT) { exit 0, `爬取第${currentPage}页数据失败，页面URL:${pageURL}`; } } } log.info(`Cost Time: ${timer.intervalPretty()}.`);所有列表页数据爬取完后，crawler_list表的数据如下：分析列表页获取文章详情页的URL列表得到列表数据后我们需要分析列表html，得到每页中博客页面的URL，下面是代码：import cn.hutool.core.util.StrUtil import org.jsoup.Jsoup import cn.hutool.core.date.DateUtil import cn.hutool.json.JSONUtil import log var timer = DateUtil.timer(); // 定义计时器，用于记录时间 var pageList = db['MDC'].select('select id,page_html from crawler_list') for (pageItem in pageList) { var pageHtml = …

酷壳(coolshell.cn)镜像站建设经验分享之一——准备工作
type: item
date: 2024-02-25T08:24:00+00:00
description: 本系列文章生成的镜像站已上传到 Gitee Pages 托管平台，可通过如下地址查看完成后的效果：https://jhlxge.gitee.io/coolshell-mirror/ 2024年6月5日更新 ：Gitee Pages服务已停止提供服务,可通过本机托管的如下地址访问：https://coolshell-mirror.work.zhuzhilong.com/缘起陈皓（1976年-2023年5月13日），男，MegaEase创始人、资深技术专家、程序员。2023年5月13日，陈皓心梗逝世，享年47岁。以上是百度百科中关于陈皓冷冰冰的直白描述，我跟陈皓在生活中没有任何交集，但是在技术层面却给了我很多的指引，他的酷壳站点上的一些文章对成长阶段充满迷茫的我都有醍醐灌顶、茅塞顿开般的功效。随着陈皓的离世，作为其思想精髓的酷壳站上的700多篇文章还是很有必要永久流芳于世，看酷壳网的一些评论，也有不少人像我一样，在担心以后域名或服务器到期后网站不能访问，为了解决这个后顾之忧，我便在工作之余开启了酷壳网站镜像之路，这一系列的文章也就是将这一过程做一个记录，也便于帮助有需要的朋友能够做到举一反三。成果展示首先我们可以直接访问Gitee Pages 上托管的最终页面效果，看看镜像后的站点效果：https://jhlxge.gitee.io/coolshell-mirror/2024年6月5日更新 ：Gitee Pages服务已停止提供服务,可通过本机托管的如下地址访问：https://coolshell-mirror.work.zhuzhilong.com/首页第一屏如下图所示：相较原站，这个镜像站做了如下优化：1、采用 Hexo 进行构建，整站构建完后是纯静态的页面，无需后端应用服务器和数据库服务器，可直接托管在提供Pages 服务的云平台上。2、数据获取更直观，当前酷壳站多了很多特别影响浏览体验的广告，新的镜像站更聚焦内容的阅读体验3、适配移动端，增加暗黑模式，可在各种不同场景下有更好的阅读体验4、添加时间轴栏目，可以以时间维度阅读按顺序所有内容5、修正部分内容中的图片资源404问题：以前不少内容是直接外链外网资源的，一些外网关站或改版后不少资源便404了，本次镜像过程中由于需要爬取内容中的图片资源一同镜像，把发现的部分404资源手工修正了镜像站建设过程数据爬取最近有在使用 magic-boot 做一些小功能，magic-boot 的一些介绍可以参考官网：https://www.ssssssss.org/magic-boot，magic-boot 是基于 magic-api 开发的一套快速开发框架，magic-api 提供了非常方便的在线开发体验。本次数据爬取和爬取内容解析主要基于jsoup实现，主要过程如下：整合jsoup在 magic-boot 工程的 pom.xml 文件中引入 jsoup 依赖： org.jsoup jsoup 1.16.2 新建数据表爬取数据和分析数据过程中，需要将相关的数据保存到中间表中，便于后续利用，以下是本任务相关表的建表语句：1、列表表先爬取所有列表页面，然后分析列表中的内容链接再爬取内容页CREATE TABLE `crawler_list` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `site_id` bigint DEFAULT NULL COMMENT '站点ID', `page_url` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '页面URL', `page_html` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '页面URL', `success_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '是否采集成功,Y:是，N:否', `create_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `target_links` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'page_html分析得到的目标链接，使用json数组存储', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCR…