酷壳(coolshell.cn)镜像站建设经验分享之五——镜像站制作
本系列文章生成的镜像站已上传到 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 = StrUtil.replace(content,'https://coolshell.cn/wp-content/','/wp-content/') content = StrUtil.replace(content,'https://coolshell.cn/?tag=','/tags/') return content } for (item in pageList) { // 定义内容相关的元数据信息 var mdHtml = item.mdHtml mdHtml = dealContent(mdHtml) var tags = item.tags // 生成符合 Hexo 规范的Tag词数据 var tagList = [] if (ObjectUtil.isNotEmpty(tags)) { tagList = tags.split(',') } // 生成符合 Hexo 规范的内容分类数据 var category = item.categories var categoryList = [] if (ObjectUtil.isNotEmpty(category)) { categoryList = JSONUtil.parseArray(category).map(item => item.name) } // 得到内容id,这里加上内容id主要是为了保障跟原站URL路径保持一致 var articleId = item.pageUrl.substring(item.pageUrl.lastIndexOf('/') + 1, item.pageUrl.lastIndexOf('.html')) // 选取正文内容中的第一张图片作为内容封面图 var coverImg = '' var contentImages = Jsoup.parse(mdHtml).select('img[src]') if (ObjectUtil.isNotEmpty(contentImages)) { coverImg = contentImages.get(0).attr('src') } // 文件内容 var fileContent = ` --- id: ${articleId} cover: '${coverImg}' author: ${item.author} title: "${item.articleTitle}" originPageURL: '${item.pageUrl}' voteCount: ${item.voteCount} clickCount: ${item.clickCount} wordCount: ${StrUtil.toString(item.articleText).length()} date: ${DateUtil.formatLocalDateTime(item.publishDate)} updated: ${DateUtil.formatLocalDateTime(item.crawlerTime)} tags: ${JSONUtil.toJsonStr(tagList)} categories: ${JSONUtil.toJsonStr(categoryList)} --- {% raw %} ${mdHtml} {% endraw %} ` // 写入到自定目录 FileUtil.writeString(fileContent,FileUtil.file(`/MDC_DATA/coolshell/${articleId}.md`), "UTF-8") } log.info(`Cost Time: ${timer.intervalPretty()}.`);执行上面的代码后，会在服务器上的制定目录得到所需的 markdown 文件列表，将生成的文件直接拷贝到 hexo 工程的 source/_posts 目录，将前面步骤爬取到的所有资源文件拷贝到source目录下，即大功告成。全站搜索通过 Hexo 和 hexo-theme-butterfly 官网的介绍，系统有自带的全站搜索插件，实现过程记录如下：1、安装 hexo-generator-search 依赖2、根目录的 _config.yml 文件夹下添加如下配置项：search: path: search.json field: post content: true format: html配置好后，在主题顶部出现检索入口，单击后会加载一个名为 search.json 的静态化文件，加载完成后可输入关键词进行全站检索：
酷壳(coolshell.cn)镜像站建设经验分享之四——解析文章正文html中的图片并离线下载图片到本地
本系列文章生成的镜像站已上传到 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 resource_url= #{imgURL}`) if (chkExistRes.size() === 0) { db['MDC'].table('crawler_resource').insert(imgObj) } // 将正文中的图片URL 替换为新的路径 imgEle.attr('src', fullPath) } }) // 将替换图片URL后的新html,存储到crawler_article 表的 md_html 字段 var updateArticleMap = { id: item.id, mdHtml:doc.select('body').html() } db['MDC'].table('crawler_article').primary('id').update(updateArticleMap) } log.info(`Cost Time: ${timer.intervalPretty()}.`);下载图片资源文件到本地通过上一步骤，我们已经将正文中所有的图片资源存储到 crawler_resource 表，我们现在只需遍历这张表的内容，将对应URL的资源，下载到本地指定的目录即可，下载资源的操作，虽然 magic-api 的 http 模块也可以做，但是感觉使用起来不是那么方便，我们直接使用 hutool 的 HttpUtil 工具类，一行代码搞定，以下是完整代码：import cn.hutool.core.io.FileUtil import cn.hutool.http.HttpUtil import cn.hutool.core.date.DateUtil import log var ROOT_PATH = '/temp' var timer = DateUtil.timer(); // 定义计时器，用于记录时间 var pageList = db['MDC'].select('select id,resource_url,source_path,source_filename from crawler_resource where file_size is null and resource_ext is not null') log.info("待处理记录数据：" + pageList.size()) for (item in pageList) { var resourceURL = item.resourceUrl var filePath = ROOT_PATH + item.sourcePath FileUtil.mkdir(filePath) var distFile = filePath + item.sourceFilename try { var file = HttpUtil.downloadFileFromUrl(resourceURL, distFile) var fileSize = FileUtil.size(file) var updateResourceMap = { id: item.id, fileSize } db['MDC'].table('crawler_resource').primary('id').update(updateResourceMap) } catch(e) { } } log.info(`Cost Time: ${timer.intervalPretty()}.`);
酷壳(coolshell.cn)镜像站建设经验分享之三——分析爬取的文章html得到正文及相关的元数据
本系列文章生成的镜像站已上传到 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 = Integer.parseInt(viewText) } // 1.2 获取tag var tagsEle = doc.select('.tags-links') var tags = '' if (ObjectUtil.isNotEmpty(tagsEle)) { tags = tagsEle.text() } // 1.3 获取分类数据 var categoriesEle = doc.select('.cattegories .cat-links') var categories = [] if (ObjectUtil.isNotEmpty(categoriesEle)) { var cates = categoriesEle.select('a') categories = cates.map(cate => { var link = cate.attr('href') link = link.substring(link.lastIndexOf('/') + 1) link = URLUtil.decode(link) var text = cate.text() return { code: link, name: text } }) } // 2、获取正文 var contentEle = doc.select('.entry-content').first() // 清理不需要的数据 contentEle.select('script[crossorigin="anonymous"]').first().remove() contentEle.select('.wp_rp_wrap').first().remove() contentEle.select('.post-ratings').first().remove() contentEle.select('.post-ratings-loading').first().remove() contentEle.select("div[style='margin-top: 15px; font-size: 16px;color: #cc0000;']").first().remove() var articleHtml = contentEle.html() articleHtml = StrUtil.replace(articleHtml,' (全文完) ','') articleHtml = StrUtil.replace(articleHtml,' （全文完） ','') if(StrUtil.isNotBlank(articleHtml)) { var articleText = StrUtil.replace(contentEle.text(),'(全文完)','') articleText = StrUtil.replace(articleText,'（全文完）','') var updateMap = { id: item.id, articleHtml, articleText, author, publishDate, categories: JSONUtil.toJsonStr(categories), tags: tags, clickCount:viewCount, voteCount:commentCount, analysisTime:DateUtil.now() } db['MDC'].table('crawler_article').primary('id').update(updateMap) } } log.info(`Cost Time: ${timer.intervalPretty()}.`);这个过程主要是分析文章 html,得到文章如下元数据：正文内容、作者、发布时间、分类列表、Tag词列表、浏览次数、评论次数，并将相关元数据存储到 crawler_article 表的对应字段里。
酷壳(coolshell.cn)镜像站建设经验分享之二——整站内容爬取
本系列文章生成的镜像站已上传到 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 = pageItem.pageHtml var doc = Jsoup.parse(pageHtml) var links = doc.select('.entry-title a') log.info(links.toString()) var linkArr = [] for (linkItem in links) { var linkURL = linkItem.attr('href') var linkTitle = linkItem.text() linkArr.push({ linkURL, linkTitle }) db['MDC'].table('crawler_article').insert({ siteId:1, pageUrl: linkURL, articleTitle:linkTitle }) } var updateMap = { id: pageItem.id, targetLinks: JSONUtil.toJsonStr(linkArr) } db['MDC'].table('crawler_list').primary('id').update(updateMap) } log.info(`Cost Time: ${timer.intervalPretty()}.`);爬取文章详情页 html分析完成后，在 crawler_article 表会得到所有文章详情页的URL，然后我们遍历 crawler_article 表的记录，逐个URL爬取回每篇博客内容的 html，以下是爬取代码：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.thread.ThreadUtil import log var timer = DateUtil.timer(); // 定义计时器，用于记录时间 var pageList = db['MDC'].select('select id,page_url from crawler_article where page_html is null') var MAX_RETRY_COUNT = 100 for (item in pageList) { var currentRetryCount = 0; var needCrawler = true var pageURL = item.pageUrl // 由于爬取过程中经常会出现超时未响应的情况，这里使用while增加了重试机制 while(needCrawler) { 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(); if(StrUtil.isNotBlank(html)) { var updateMap = { id: item.id, pageHtml: html, crawlerTime:DateUtil.now() } db['MDC'].table('crawler_article').primary('id').update(updateMap) } needCrawler = false ThreadUtil.sleep(2000); } catch(e) { log.error(e.getMessage()) currentRetryCount++ log.info(`采集 ${pageURL} 失败，已重试 ${currentRetryCount} 次。`) if (currentRetryCount == MAX_RETRY_COUNT) { exit 0, '重试次数过多'; } } } } log.info(`Cost Time: ${timer.intervalPretty()}.`);需要说明一下，由于coolshell.cn 服务器在国外，爬取过程有些不稳定，所以我加入了重试机制。至此，我们需要的博客内容已经爬取完毕。接下来的章节，我将分析爬取的html，得到我们需要的博客内容数据。
酷壳(coolshell.cn)镜像站建设经验分享之一——准备工作
本系列文章生成的镜像站已上传到 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_INCREMENT=75 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='爬取站点的列表页面';2、文章表CREATE TABLE `crawler_article` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID', `page_html` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '页面HTML', `site_id` bigint NOT NULL COMMENT '站点ID', `page_url` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '页面URL', `md_html` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '转Markdown前的html', `md_text` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '转换后的markdown内容', `article_title` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '内容标题', `article_html` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '正文HTML', `click_count` int DEFAULT NULL COMMENT '阅读次数', `publish_date` datetime DEFAULT NULL COMMENT '发布时间', `vote_count` int DEFAULT NULL COMMENT '评论条数', `author` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '作者', `tags` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Tag', `article_text` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '去掉html标签后的正文内容', `add_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间', `crawler_time` datetime DEFAULT NULL COMMENT '页面内容爬取时间', `crawler_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '是否爬取成功', `analysis_time` datetime DEFAULT NULL COMMENT '数据解析时间', `categories` varchar(500) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '分类', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=741 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='内容页采集表';3、文章资源表CREATE TABLE `crawler_resource` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID', `site_id` bigint NOT NULL COMMENT '站点ID', `resource_url` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '资源完整URL', `resource_ext` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '资源扩展名', `file_size` int DEFAULT NULL COMMENT '文件大小，单位byte', `file_md5` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '文件Hash', `source_path` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '原路径', `source_filename` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '原文件名', `add_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '添加事件', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2039 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='采集的资源表';