插件简介
相信写过博客的都知道,每天会经常打开自己的主页无数次,尤其是写了一篇新文章,就为了看文章浏览量增长了多少,文章获得了多少个赞,有多少人评论(谁不想自己写的文章成为爆款呢~)
因此我特意做了一个Chrome插件,让用户更方便地实时查看自己在CSDN博客上的数据
我们先来看看最后的实现效果:
这个插件能显示两部分数据,一部分是用户个人数据,包括:总阅读数、文章数、排名、粉丝数,另一部分是个人博客成就数据,包括:点赞数、评论数、收藏数、分享数
插件显示的数据默认每1小时更新一次,用户可以自行修改数据更新时间
插件的UI第一版写的暂时比较简陋,可以后期版本再修改以及增加一些功能
在开发之前,我们需要会一些前置知识,包括:Node.js爬虫、Chrome插件开发
不会的同学也没关系,可以看我之前写过的博客,里面关于爬虫和插件开发写的非常详细:
都 2023 年了还不会 Node.js 爬虫?快学起来!
Node.js 爬虫只会 Cheerio?来试试 Puppeteer!
从零入门 Chrome 插件开发
好的,现在我们就来开动吧!
爬取CSDN个人数据
这里默认大家已经学过puppeteer,具体爬取的操作在上一篇文章中有很详细的解释
这里我们分析完网页结构之后直接来编写爬虫脚本:
// 无头浏览器模块
const puppeteer = require("puppeteer");
// 目标页面
const crawlPage = "https://blog.csdn.net/weixin_46232841?spm=1000.2115.3001.5343";
// 网页爬虫
async function crawler() {
//创建实例
const browser = await puppeteer.launch({
//无浏览器界面启动
headless: "new",
//放慢浏览器执行速度,方便测试观察
slowMo: 100,
// 设置打开的浏览器窗口尺寸
defaultViewport: { width: 960, height: 540 },
});
// 新开一个tab页面
const page = await browser.newPage();
// 加载目标页,在 500ms 内没有任何网络请求才算加载完
await page.goto(crawlPage, { waitUntil: "networkidle0" });
// 在无头浏览器页面dom环境,获取页面数据
const myData = await page.evaluate(() => {
let data = {};
let name = "萌萌哒の瑞萌萌";
let achievement = {};
// 个人数据
document.querySelectorAll(".user-profile-head-info-r-c ul").forEach((ele) => {
const allReadNum = ele.querySelector("li:nth-child(1) .user-profile-statistics-views .user-profile-statistics-num").innerText;
const articleNum = ele.querySelector("li:nth-child(2) .user-profile-statistics-num").innerText;
const rank = ele.querySelector("li:nth-child(3) .user-profile-statistics-num").innerText;
const fans = ele.querySelector("li:nth-child(4) .user-profile-statistics-num").innerText;
data = {
allReadNum,articleNum,rank,fans
}
});
// 个人成就
document.querySelectorAll(".aside-common-box-achievement").forEach((ele) => {
const like = ele.querySelector("li:nth-child(1) div").innerText;
const comment = ele.querySelector("li:nth-child(2) div").innerText;
const favorite = ele.querySelector("li:nth-child(3) div").innerText;
const share = ele.querySelector("li:nth-child(4) div").innerText;
achievement = {
like,comment,favorite,share
}
});
return {
name,data,achievement
};
});
console.log(myData);
// 关闭tab页
await page.close();
// 关闭实例
await browser.close();
})
运行一下看看有没有获取到我们想要的数据吧:
完美!现在我们拿到了我们想要的数据,接下来要完成的需求就是:
- 将数据存储到json文件里,方便插件目录获取
- 定时爬取,每隔一小时爬取一次,及时更新json文件
解决办法:用 fs 模块的
fs.writeFile
来保存文件,用node-schedule
模块来实现定时爬取上代码!
// 无头浏览器模块
const puppeteer = require("puppeteer");
const schedule = require('node-schedule');
const fs = require('fs');
// 目标页面
const crawlPage = "https://blog.csdn.net/weixin_46232841?spm=1000.2115.3001.5343";
// 创建一个定时任务,每隔1小时执行一次
let job = schedule.scheduleJob('0 0 */1 * * *',// 网页爬虫
async function crawler() {
//创建实例
const browser = await puppeteer.launch({
//无浏览器界面启动
headless: "new",
//放慢浏览器执行速度,方便测试观察
slowMo: 100,
// 设置打开的浏览器窗口尺寸
defaultViewport: { width: 960, height: 540 },
});
// 新开一个tab页面
const page = await browser.newPage();
// 加载目标页,在 500ms 内没有任何网络请求才算加载完
await page.goto(crawlPage, { waitUntil: "networkidle0" });
// 在无头浏览器页面dom环境,获取页面数据
const myData = await page.evaluate(() => {
let data = {};
let name = "萌萌哒の瑞萌萌";
let achievement = {};
// 个人数据
document.querySelectorAll(".user-profile-head-info-r-c ul").forEach((ele) => {
const allReadNum = ele.querySelector("li:nth-child(1) .user-profile-statistics-views .user-profile-statistics-num").innerText;
const articleNum = ele.querySelector("li:nth-child(2) .user-profile-statistics-num").innerText;
const rank = ele.querySelector("li:nth-child(3) .user-profile-statistics-num").innerText;
const fans = ele.querySelector("li:nth-child(4) .user-profile-statistics-num").innerText;
data = {
allReadNum,articleNum,rank,fans
}
});
// 个人成就
document.querySelectorAll(".aside-common-box-achievement").forEach((ele) => {
const like = ele.querySelector("li:nth-child(1) div").innerText;
const comment = ele.querySelector("li:nth-child(2) div").innerText;
const favorite = ele.querySelector("li:nth-child(3) div").innerText;
const share = ele.querySelector("li:nth-child(4) div").innerText;
achievement = {
like,comment,favorite,share
}
});
return {
name,data,achievement
};
});
// 将数据写入文件中
fs.writeFile('../chrome/mycsdn.json', JSON.stringify(myData), function (err, data) {
if (err) {
throw err
}
console.log('文件保存成功,当前时间:' + new Date());
})
// 关闭tab页
await page.close();
// 关闭实例
await browser.close();
})
最终实现效果:
到此我们就实现了定时爬取CSDN个人主页数据的操作,接下来我们一起来完成Chrome插件开发!
Chrome插件开发
先来创建一个 Chrome 插件项目叫 mycsdn:
mkdir mycsdn # 创建插件项目
cd mycsdn # 进入项目根目录
touch manifest.json # 在项目根目录中创建一个名为 manifest.json 的文件
接下来我们简单编写 manifest.json
文件,参数配置如下:
{
"manifest_version": 2,
"name": "CSDN个人数据",
"version": "1.0",
"description": "CSDN个人数据展示",
"permissions": [
"activeTab"
],
"browser_action": {
"default_title": "CSDN个人数据",
"default_popup": "popup/popup.html",
"default_icon": {
"128": "icon/icon128.png"
}
}
}
然后我们在根目录创建popup文件夹,添加一个名为 popup.html 的文件,我们来完成点击弹出的页面结构:
<!DOCTYPE html>
<html>
<head>
<title>CSDN个人数据展示</title>
<meta charset="utf-8" name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="popup.css">
<script src="popup.js"></script>
</head>
<body>
<div class="container">
<div class="box">
<div class="card-data content">
<h2>个人数据</h2>
<p>总阅读数</p>
<p>文章数</p>
<p>排名</p>
<p>粉丝数</p>
</div>
</div>
<div class="box">
<div class="card-data card-achievement content">
<h2>个人成就</h2>
<p>点赞数</p>
<p>评论数</p>
<p>收藏数</p>
<p>分享数</p>
</div>
</div>
</div>
</body>
</html>
来点CSS装饰一下:
body {
min-width: 400px;
min-height: 300px;
font-family: Arial, Helvetica, sans-serif;
background-color: #f2f2f2;
margin: 0;
padding: 0;
}
.container {
max-width: 800px;
margin: 0 auto;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
padding: 10px 20px;
background: radial-gradient(circle, rgba(238,174,202,1) 0%, rgba(148,187,233,1) 100%);
}
.box {
position: relative;
height: 220px;
align-items: center;
transition: 0.5s;
z-index: 1;
display: flex;
flex-wrap: wrap;
align-items: center;
}
.box .content {
position: relative;
padding: 0px 40px 10px 40px;
backdrop-filter: blur(10px);
z-index: 1;
transform: 0.5s;
background-color: rgba(255, 255, 255, 0.8);
border-radius: 20px;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
overflow: hidden;
margin-bottom: 0;
font-size: 18px;
color: #666;
flex-grow: 1;
text-align: right;
}
.content::before {
content: "";
background-image: url("../bk1.jpg");
background-repeat: no-repeat;
background-size: cover;
opacity: 0.5;
filter: blur(1px);
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.box .content p {
font-size: 14px;
font-weight: bolder;
color: black;
}
.box .content h2 {
font-size: 20px;
margin-bottom: 10px;
font-weight: bolder;
color: black;
}
效果还不错,大家也可以根据自己的审美自己来设计页面
接下来我们来写JS,这里因为已经将数据存储在本地JSON文件中,所以我们用 XMLHttpRequest 对象来读取这个JSON文件,详细信息可以看一下注释:
// 当DOM加载完成时执行以下代码
document.addEventListener("DOMContentLoaded", function () {
// 创建一个新的XMLHttpRequest对象
let xhr = new XMLHttpRequest();
// 指定要读取的文件路径
xhr.open('GET', '../mycsdn.json', true);
// 当请求完成时执行的回调函数
xhr.onload = function() {
if (xhr.status === 200) {
// 将JSON字符串转换为JavaScript对象
let data = JSON.parse(xhr.responseText);
// 处理读取到的数据
const allReadNum = document.querySelector("p:nth-of-type(1)");
allReadNum.innerHTML = `总阅读数:${data.data.allReadNum}`;
const articleNum = document.querySelector("p:nth-of-type(2)");
articleNum.innerHTML = `文章数:${data.data.articleNum}`;
const rank = document.querySelector("p:nth-of-type(3)");
rank.innerHTML = `排名:${data.data.rank}`;
const fans = document.querySelector("p:nth-of-type(4)");
fans.innerHTML = `粉丝数:${data.data.fans}`;
const like = document.querySelector(
".card-achievement p:nth-of-type(1)"
);
like.innerHTML = `点赞数:${data.achievement.like}`;
const comment = document.querySelector(
".card-achievement p:nth-of-type(2)"
);
comment.innerHTML = `评论数:${data.achievement.comment}`;
const favorite = document.querySelector(
".card-achievement p:nth-of-type(3)"
);
favorite.innerHTML = `收藏数:${data.achievement.favorite}`;
const share = document.querySelector(
".card-achievement p:nth-of-type(4)"
);
share.innerHTML = `分享数:${data.achievement.share}`;
}
};
// 发送请求
xhr.send();
// 设置插件图标
chrome.browserAction.setIcon({ path: "icon/icon128.png" });
// 设置插件标题
chrome.browserAction.setTitle({ title: "CSDN个人数据展示" });
});
我们首先创建了一个XMLHttpRequest对象,然后使用open方法指定要读取的文件路径。接着我们定义了一个onload回调函数,在请求完成时执行。在这个回调函数中,我们首先检查请求的状态是否为200(表示成功),然后使用JSON.parse方法将JSON字符串转换为JavaScript对象,最后我们就可以处理读取到的数据将其显示在页面上。
进程管理工具PM2
到此为止,我们这个插件项目还剩最后一个问题没有解决:我们编写的这个nodejs 爬虫文件不可能一直在控制台运行,那么我们该怎么让这个文件一直运行从而来定时爬取更新JSON文件呢?
我们这里来用一个管理 Node.js 进程的第三方工具 PM2
PM2是一个流行的Node.js进程管理器,它可以帮助我们简化应用程序的部署、监控和管理:
- 进程管理:PM2可以启动、停止、重启和删除进程,可以使用PM2来管理单个应用程序或多个应用程序。
- 自动重启:如果进程崩溃或异常退出,PM2会自动重启它。
- 监控和日志记录:PM2可以监控应用程序的CPU和内存使用情况,并将日志记录到文件中。
- 负载均衡:如果运行多个实例,PM2可以使用负载均衡来将流量分配到不同的实例上。
- 部署:PM2可以在生产环境中部署应用程序,可以将应用程序作为系统服务运行,并在系统启动时自动启动。
以下是一些PM2的常用命令:
pm2 start <file>
:启动一个应用程序。pm2 stop <id|name>
:停止一个应用程序。pm2 restart <id|name>
:重启一个应用程序。pm2 delete <id|name>
:删除一个应用程序。pm2 list
:列出所有正在运行的应用程序。pm2 monit
:监视所有正在运行的应用程序的CPU和内存使用情况。
我们来安装一下 pm2:
npm install -g pm2
然后用 pm2 启动项目:
pm2 start /path/to/your/nodejs/script.js
这个命令会启动Node.js程序,并且在后台运行,即使关闭了终端也不会停止运行
如果想停止程序可以使用以下命令:
pm2 stop /path/to/your/nodejs/script.js
插件开发总结
本项目是Node爬虫和Chrome插件开发的一个综合案例,这个插件的功能非常实用,可以帮助我们更好地了解自己的CSDN博客个人数据。
我们首先用puppeteer编写node爬虫脚本爬取到了CSDN个人主页数据,然后将数据写入了JSON文件,并用node-schedule模块完成了定时爬取的功能。
之后我们完成了Chrome插件的主体开发,最后我们使用PM2进程管理工具让这个爬虫文件一直运行。
插件完整代码可以自取:https://github.com/KongC-X/node-creeper/tree/main/chrome