前言
在csdn写博客也已经一年多了,经常忍不住想知道自己总共写了多少字。可是目前官方只能统计单篇文章的总字数,却没有提供所有文章的字数统计数据(希望官方以后增加这个统计数据)。
一篇一篇地点开统计虽说数据量也不是很大,但是作为一个前端,能用脚本搞定的事干嘛要自己去数呢?于是我就抽空自己写了一段简单的脚本,对自己目前的67篇文章进行了一次字数统计,最后统计的总字数约为50万字,跟预期还是非常接近的。下面我们就一起来看代码的实现吧(脚本可以复制下来用于统计自己的博客数据)。
注意,由于文章的列表页(https://mp.csdn.net
)与文章详情页(https://blog.csdn.net
)位于不同的域,为了避免处理跨域问题,我们的脚本分为两部分:第一段脚本在列表页控制台执行,可获取所有文章详情页的链接地址;第二部分可任意打开一个详情页在控制台执行,即可得到最终的统计结果。如果想要一次性统计出所有的字数,可能需要使用nginx进行较为复杂的代理,本文暂不涉及这种方案。
整个过程分为两步:第一步是爬取所有文章详情页的链接地址,第二步就是进行字数统计。
一、自动爬取所有文章的链接地址
首先第一步,当然是打开个人博客的文章详情页,也就是下面的页面(可在登录后,从首页头像的下拉列表中点击个人中心 > 我的博客
进入该页面):
第一步,我们先来定义一个数组,用于存储所有文章的url:
let urls = [];
第二步,定义一个获取当前页面所有文章链接地址的函数。
csdn的文章列表是分页的,每次最多只能加载20篇文章,我们的这个函数只是用于获取当前页的链接。后面我们会介绍如何通过脚本自动加载其他页的文章。代码如下:
function getUrls () {
let items = document.querySelectorAll('.item-info-oper');
urls = urls.concat(Array.from(items).map(item => {
return item.querySelectorAll('a[target]')[0].href;
}));
}
函数第一行获取的是列表每篇文章的这个部分,我们暂且称之为工具栏:
工具栏内的'查看'
按钮的href属性里带了文章详情页的地址:
于是我们遍历这20个工具栏,找出图中对应的a标签,取出其href属性的值,拼接到urls
数组中。这个函数执行一次,当前页内的20篇文章的链接地址就会自动被统计出来。
第三步,定义一个自动翻页的函数。
其实自动翻页非常简单,页面的底部有一个翻页按钮,点击即可翻页。我们所要做的,只是用js自动模拟点击:
// 获取下一页的文章数据
function fetchNextPage () {
let next = document.querySelector('.btn-next');
if (next.disabled !== 'disabled') {
next.click();
}
}
只有当翻页按钮没有disabled
属性的时候,我们才自动进行下一次翻页,否则说明已经是最后一页了。
第四步,设置DOM MutationObserver监听,检测页面翻动。
我们知道,点击了翻页按钮后,列表数据没有马上变化,页面需要向后端发请求,获取下一页的数据后再渲染到页面上。那么我们怎么知道新一页的数据已经渲染到页面上了呢?
一个可以想到的办法是给对应的ajax请求注册回调函数,但是由于请求下一页数据的代码不是我们实现的,这个方案不便于实施。不过我们还有一个更加直接了当的办法:监听DOM变动事件。
首先我们获取到列表项的根节点:
let rootDiv = document.querySelectorAll('.article_manage_list > div')[1];
图中的.article-list-item-mp
就是每一个文章列表项,我们现在获取到的就是这些列表项的根节点,即图中的那个div
。
下面我们要定义一个对该节点的观察者对象,检测该节点的变动:
// 只观测它的子元素变化
const config = { childList: true };
const observer = new MutationObserver(function () {
// 一旦发生变化就获取当前页文章的url,
// 随后继续获取下一页的数据
getUrls();
fetchNextPage();
});
// 启动观测
observer.observe(rootDiv, config);
config的配置表示我们要监听某个DOM节点子元素的变化,当文章列表变化时,它们的父节点就会触发这个事件。接着我们创建一个观察者,它会在发生DOM变动时调用getUrls()
统计当前页的文章链接地址,然后继续调用fetchNextPage()
进行自动翻页。最后就是用这个观察者去观察列表项的根节点。
第五步,爬取当前页的链接地址。
我们需要先调用getUrls
获取当前页的链接地址:
getUrls();
统计完当前页的数据,就进行翻页:
fetchNextPage();
由于DOM变动的监听事件会在监听到列表发生变动时自动进行下一次翻页,所以我们只需手动触发第一次翻页,列表页就会自动翻页,直到最后一页为止,并在这个过程中统计出每一页文章的链接地址。
第六步,输出所有链接地址。
当在控制台执行完上述代码,我们可以手动在控制台输入变量urls
,它应该已经保存了所有文章的链接地址(这里共67篇文章,因此数组有67项):
> urls
< [
"https://blog.csdn.net/qq_41694291/article/details/107877447",
...
]
上述的整个过程就是如下代码,直接复制粘贴到列表页的控制台,回车执行即可:
// 存储所有的url
let urls = [];
// 提取当前页的所有文章的url
function getUrls () {
//
let items = document.querySelectorAll('.item-info-oper');
urls = urls.concat(Array.from(items).map(item => {
return item.querySelectorAll('a[target]')[0].href;
}));
}
// 获取下一页的文章数据
function fetchNextPage () {
let next = document.querySelector('.btn-next');
if (next.disabled !== 'disabled') {
next.click();
}
}
let rootDiv = document.querySelectorAll('.article_manage_list > div')[1];
const config = { childList: true };
const observer = new MutationObserver(function () {
getUrls();
fetchNextPage();
});
observer.observe(rootDiv, config);
// 获取当前页所有文章的链接地址
getUrls();
fetchNextPage();
执行完毕,在控制台输入:
> urls
< [
"https://blog.csdn.net/qq_41694291/article/details/107877447"
......
]
这就是我们要的url数组,我们将其拷贝下来,以备后续使用。为了方便拷贝,我们可以将其转化为json:
// 格式化为JSON字符串,并在每个元素前插入2个空格
JSON.stringify(urls, null, 2);
二、字数统计
由于列表页和详情页存在跨域问题,因此我们得到上述url数组后并未直接继续爬取页面,而是把得到的url暂时保存起来了。现在我们可以任意进入一个文章的详情页,打开其控制台:
第一步,将url数组拷贝过来。
我们刚才已经得到了所有文章的详情页的链接地址,现在我们将其保存在一个变量中:
let urls = [
"https://blog.csdn.net/qq_41694291/article/details/107877447",
...
];
第二步,设置变量total和time,total用于记录已统计的总字数,time是一个延迟时间:
let time = 0;
let total = 0;
total就不用多说了,这里之所以要设置一个计时器,是因为实际测试发现,当同时向服务器使用fetch发送67个请求,大约有60个请求都报了如下的错误:
而以200毫秒的延迟逐个发送请求时则没有报错(根据网速的不同,这个延迟可能需要设置得更长)。
第三步,请求文章详情页数据,解析文本并统计字数。
为了简便,我们直接使用浏览器原生的fetch
接口来发送请求,然后使用原生的DOMParser
来解析返回的HTML页面:
// 在列表页得到的url数组
let urls = [ ... ]
let time = 0;
let total = 0;
urls.forEach(url => {
time += 200;
setTimeout(() => {
fetch(url).then(res => {
// 获取返回的页面字符串
res.text().then(data => {
let parser = new DOMParser();
// 将字符串以html格式解析,得到document对象
let result = parser.parseFromString(data, 'text/html');
// 使用原生DOM获取页面内的文章标签
let article = result.querySelector('article');
// 替换掉所有的空白字符
let length = article.textContent.replace(/\s+/g, '').length;
// 统计字数
total += length;
console.log(total);
})
})
}, time);
})
可以看到在浏览器控制台依次输出67个数据,最后一个就是我们要统计的总字数:
注意,本代码较为消耗内存(特别是文章非常多的时候),所以多次输出可能造成页面卡顿,如果发生卡顿可以关闭页面重新执行。
总结
想要使用本文的脚本统计字数,可以先把第一部分的代码粘贴到列表页控制台,回车执行。执行完毕后继续输入:urls
,获取所有文章的url数组。接着把这个数组保存起来,粘贴到第二部分的代码的变量urls
中。然后任意打开一个文章详情页(在别人的文章详情页也可以,因为访问详情页不存在权限问题),执行第二部分的代码,等待输出完毕,就可以统计出自己所有csdn文章的总字数了。
有三个注意事项:
- 必须在列表页的第一页执行第一部分脚本,因为脚本是从当前页开始统计的。
- 执行完第一部分脚本后翻页功能会失效(因为DOM监听事件一直存在,每次点击翻页按钮后它都会检测到,并自动翻到最后一页),关闭重新打开即可恢复翻页功能。
- 脚本在一个页面内只能执行一次,想要再次执行请关闭页面重新打开。
本脚本涉及到的主要知识点有三个:
- DOM MutationObserver的使用
- fetch的使用
- DOMParser的使用
希望读完本文的同学也可以简单地了解一下这几个知识点。