简单介绍
用 jsoup 爬取csdn博客的一些信息。项目已上传到github,有兴趣可以去看看:https://github.com/yansheng836/jsoup-crawl-csdn.
初衷
逛CSDN时,总会发现有些博客因为年代比较久远,而造成博客图片的丢失,阅读博客的体验极差;因为嵌入的图片中可能包含一些和内容联系比较紧密的信息,图片丢失会使博客断断续续、不易理解。
于是我突然想将csdn博客中的图片下载下来,然后进行备份:保存到本地、上传百度云、上传图床等,然后这个项目就诞生了。
主要内容
- 获得博客数量。
- 获得博客列表页数。
- 获得博客列表信息(网址,创建时间,标题),如:
Blog [url=https://blog.csdn.net/weixin_41287260/article/details/92185040, createTime=2019-06-15 20:45:43, title=阿里巴巴主导的“华山版《Java 开发手册》”简介]
- 获得博客的图片链接。
- 下载博客的所有图片,进行备份,防止博客中的图片丢失。
构建环境
jdk1.8+,maven3.0+,myeclipse2017
<!-- https://mvnrepository.com/artifact/org.jsoup/jsoup -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.12.1</version>
</dependency>
注:如果不使用maven框架进行构建,直接下载项目中的jsoup-1.12.1.jar
或者到 https://mvnrepository.com/artifact/org.jsoup/jsoup 下载jar包,然后加到类路径中。
主要代码
0.说明:
- 以下代码是主要代码,不可独立运行,仅供参考。如需要全部代码可到github上查看:github仓库jsoup-crawl-csdn
- 如果你想要使用该项目备份自己的博客图片,你需要进行以下操作:
2.1 按照github的README.md内容将项目导入到IDE。
2.2 修改/jsoup-crawl-csdn/src/main/java/xyz/yansheng/utility/BlogUtil.java
文件中的三个常量:
字符串常量名 | 注释 | 举例 |
---|---|---|
PERSONAL_HOME_PAGE | 个人主页网址 | https://me.csdn.net/username |
BLOG_HOME | 个人博客主页 | https://blog.csdn.net/username |
PARENT_PATH | 保存博客图片的根路径 | E://2CSDN// |
2.3 运行 /jsoup-crawl-csdn/src/main/java/xyz/yansheng/main/DownloadBlogsPictures.java
即可下载你的博客中的所有图片。
1.主要的工具函数
/**
* @Title BlogUtil.java
* @Package xyz.yansheng.utility
* @Description TODO
* @author yansheng
* @date 2019-08-10 18:37:58
* @version v1.0
*/
package xyz.yansheng.utility;
import java.io.IOException;
import java.util.ArrayList;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import xyz.yansheng.entity.Blog;
/**
* <p>Title: 爬取博客的工具类</p>
* <p>Description: </p>
* <p>Company: </p>
* @author yansheng
* @date 2019-08-10 18:37:58
* @version v1.0
*/
public class BlogUtil {
// 定义常量字符串
/**
* @Fields PERSONAL_HOME_PAGE : 个人主页网址:personal-home-page,如https://me.csdn.net/username
*/
public static final String PERSONAL_HOME_PAGE = "https://me.csdn.net/weixin_41287260";
/**
* @Fields BLOG_HOME : 个人博客主页:personal-blog-page,如https://blog.csdn.net/username
*/
public static final String BLOG_HOME = "https://blog.csdn.net/weixin_41287260";
/**
* @Fields PARENT_PATH : 保存博客图片的根路径
*/
public static final String PARENT_PATH = "E://2CSDN//";
/**
* @Title getBlogCounter
* @author yansheng
* @version v1.0
* @date 2019-08-10 18:54:17
* @Description 获取某博主的csdn博客的数量
* @param personalHomePage 个人主页网址:personal-home-page,如https://me.csdn.net/username
* @return
* int 博客数量
*/
public static int getBlogCounter(String personalHomePage) {
// 1.获取文档对象
Document doc = null;
try {
doc = Jsoup.connect(personalHomePage).get();
} catch (IOException e) {
e.printStackTrace();
}
// 2.查找包含博客数量的元素
Element countElement = doc.select("span.count").first();
// 3.取出元素包含的文本(字符串),这里为博客数量
String blogCount = countElement.text();
return Integer.parseInt(blogCount);
}
/**
* @Title getBlogListPage
* @author yansheng
* @version v1.0
* @date 2019-08-10 18:58:07
* @Description 获取个人博客的页数,一页有20个博客;如果除不尽、有余数,整除博客页面+1
* @param blogCount 博客数量
* @return
* int 博客页数
*/
public static int getBlogListPage(int blogCount) {
// 向上取整,注意20.0,如果是20就是整数除法了,而不是浮点数除法
int blogListPage = (int) Math.ceil(blogCount / 20.0);
return blogListPage;
}
/**
* @Title getBlogs
* @author yansheng
* @version v1.1
* @date 2019-08-10 19:05:04
* @Description 返回博客列表
* @param blogListPage 博客列表页数
* @param blogHome 博客主页网址
* @return
* ArrayList<Blog> 博客列表
*/
public static ArrayList<Blog> getBlogs(int blogListPage, String blogHome) {
// 拼接博客列表页网址
// 如列表第二页为:https://blog.csdn.net/weixin_41287260/article/list/2
final String articleListString = "/article/list/";
// 用于存放博客列表,设置初始容量为:页面*20
ArrayList<Blog> blogs = new ArrayList<Blog>(blogListPage * 20);
// 定义变量:博客列表页的网址
String blogListUrl = null;
for (int i = 1, pageNum = blogListPage + 1; i < pageNum; i++) {
// 拼接网址
blogListUrl = blogHome + articleListString + Integer.toString(i);
// 1. 获取文档对象
Document doc = null;
try {
doc = Jsoup.connect(blogListUrl).get();
} catch (IOException e) {
e.printStackTrace();
}
//System.out.println(doc);
// 2. 查找包含博客列表的元素
Element articleList = doc.select("div.article-list").first();
// 3. 查找每篇博客的元素
// 注意这里因为class里面有空格,class="article-item-box csdn-tracking-statistics",
// 相当于是两个class
Elements blogInfo = articleList.select("div.article-item-box.csdn-tracking-statistics");
for (int j = 0, size = blogInfo.size(); j < size; j++) {
// 如果不含blogHome(https://blog.csdn.net/weixin_41287260),则移除(或者直接跳出该重循环)
// 排除乱入的第一个网址 :https://blog.csdn.net/yoyo_liyy/article/details/82762601
if (j == 0) {
// blogInfo.remove(j);
continue;
}
// 取出博客信息
Element h4 = blogInfo.get(j).select("h4>a").first();
String blogUrl = h4.attr("href");
// 裁剪博客标题,前面有:原,空格
String blogTitle = h4.text().substring(2);
Element date = blogInfo.get(j).select("span.date").first();
String blogDate = date.text();
Blog blog = new Blog();
blog.setUrl(blogUrl);
blog.setCreateTime(blogDate);
blog.setTitle(blogTitle);
blogs.add(blog);
}
}
return blogs;
}
/**
* @Title getBlogPictures
* @author yansheng
* @version v1.0
* @date 2019-08-10 22:22:34
* @Description 获取博客中的图片的链接
* @param blogUrl 博客的网址
* @return
* ArrayList<String> 博客中的图片的链接
*/
public static ArrayList<String> getBlogPictures(String blogUrl) {
ArrayList<String> picUrls = new ArrayList<String>();
// 1. 获取文档对象
Document doc = null;
try {
doc = Jsoup.connect(blogUrl).get();
} catch (IOException e) {
e.printStackTrace();
}
/*2. 先判断该文章是用富文本写的,还是用markdown写的,因为他们的图片的标签有些差别
* <article class="baidu_pl">
* <!-- 富文本写的博客,注意不同之处在这里,类不同 -->
* <div id="content_views" class="htmledit_views" >
*
* <!-- markdown写的博客,注意不同之处在这里 -->
* <div id="content_views" class="markdown_views prism-kimbie-light">
*
*/
Element article = doc.selectFirst("article.baidu_pl");
Element contentViews = article.selectFirst("div#content_views");
String contentViewsClass = contentViews.attr("class");
// 进行判断:如果类包含htmledit_views,说明是富文本编辑器写的;否则是markdown写的
// 3.查找该篇博客中的所有图片
String htmleditViews = "htmledit_views";
Elements images = null;
if (contentViewsClass.contains(htmleditViews)) {
images = article.select("img.has");
}else {
images = article.select("img");
}
for (Element element : images) {
String picUrl = element.attr("src");
// 对特殊字符串(如下)进行裁剪
picUrl = StringUtil.subPicUrl(picUrl);
picUrls.add(picUrl);
}
return picUrls;
}
}
2. 要特别注意下面这段代码
这段代码是查找博客中的图片的,这里是要区分博客是用富文本写的,还是用markdown写的,因为他们的图片的标签有些差别。(这个问题一开始我也没有注意到,是在后面备份博客图片时才发现的,已修复)
类型 | 文章标识 | 图片标签 | 选择器 |
---|---|---|---|
富文本 | <div id="content_views" class="htmledit_views" > | <img class="has"> | select("img.has") |
markdown | <div id="content_views" class="markdown_views prism-kimbie-light"> | <img> | select("img") |
我们可以发现这两个标签虽然id一样,但是class不同,因此我们可以以此进行判断,如下:
/*2. 先判断该文章是用富文本写的,还是用markdown写的,因为他们的图片的标签有些差别
* <article class="baidu_pl">
* <!-- 富文本写的博客,注意不同之处在这里,类不同 -->
* <div id="content_views" class="htmledit_views" >
*
* <!-- markdown写的博客,注意不同之处在这里 -->
* <div id="content_views" class="markdown_views prism-kimbie-light">
*
*/
Element article = doc.selectFirst("article.baidu_pl");
Element contentViews = article.selectFirst("div#content_views");
String contentViewsClass = contentViews.attr("class");
// 进行判断:如果类包含htmledit_views,说明是富文本编辑器写的;否则是markdown写的
// 3.查找该篇博客中的所有图片
String htmleditViews = "htmledit_views";
Elements images = null;
if (contentViewsClass.contains(htmleditViews)) {
images = article.select("img.has");
}else {
images = article.select("img");
}
3. 下载博客图片的主要代码
/**
* @Title DownloadBlogsPictures.java
* @Package xyz.yansheng.main
* @Description TODO
* @author yansheng
* @date 2019-08-11 02:05:34
* @version v1.0
*/
package xyz.yansheng.main;
import java.io.File;
import java.util.ArrayList;
import xyz.yansheng.entity.Blog;
import xyz.yansheng.utility.BlogUtil;
import xyz.yansheng.utility.FileUtil;
import xyz.yansheng.utility.StringUtil;
/**
* <p>Title: </p>
* <p>Description: </p>
* <p>Company: </p>
* @author yansheng
* @date 2019-08-11 02:05:34
* @version v1.0
*/
public class DownloadBlogsPictures {
/**
* @Title main
* @author yansheng
* @version v1.2
* @date 2019-08-11 02:05:34
* @Description 备份博客的图片
*/
public static void main(String[] args) {
// 1.获取博客数量
int blogCount = BlogUtil.getBlogCounter(BlogUtil.PERSONAL_HOME_PAGE);
// 2.获取博客列表页数
int blogListPage = BlogUtil.getBlogListPage(blogCount);
// 3.获取博客列表
ArrayList<Blog> blogs = BlogUtil.getBlogs(blogListPage, BlogUtil.BLOG_HOME);
ArrayList<String> picUrls = new ArrayList<String>();
// 首先保证保存图片的父目录是存在的,如果不存在就创建
File parentPath = new File(BlogUtil.PARENT_PATH);
if (!parentPath.exists()) {
if (parentPath.mkdirs()) {
System.out.println("创建保存图片的跟目录:'" + parentPath + "' 成功。");
}else {
System.err.println("创建保存图片的跟目录:'" + parentPath + "' 失败。");
return ;
}
} else {
System.out.println("保存图片的跟目录:'" + parentPath + "' 已存在。");
}
// 下载所有博客的图片
for (int i = 0, blogNo = blogs.size(); i < blogNo; i++) {
Blog blog = blogs.get(i);
System.out.print("第" + Integer.toString(i + 1) + "篇博客-图片");
// 创建文件夹,以博客的创建时间+博客标题为文件夹名,如:2018-10-15-html表单提交问题
String blogTitle = blog.getTitle();
// 替换文件名中的特殊字符,使能够成功创建该文件夹
blogTitle = StringUtil.replaceSpecialCharacters(blogTitle);
String dirPath = BlogUtil.PARENT_PATH + blog.getCreateTime().substring(0, 10) + "-" + blogTitle
+ "//";
// 判断创建文件夹的返回值,如果是0,即已存在,则认为已下载该博客的图片,跳出该循环;
// 如果是-1,则文件夹创建失败,故路径错误,不可能成功保存图片,也直接跳出循环。
int result = FileUtil.mkdir(dirPath);
if (result == 0 || result == -1) {
// continue;
}
// 获取该博客的所有图片的url列表
picUrls = BlogUtil.getBlogPictures(blog.getUrl());
// 下载图片
for (int j = 0, picNo = picUrls.size(); j < picNo; j++) {
String picUrl = picUrls.get(j);
// 暂时注释,用于测试图片保存的文件名
FileUtil.downloadPic(picUrl, dirPath);
if (j == picNo - 1) {
System.out.println(" -已下载" + picNo + "张图片\n");
}
// System.out.println("图片文件名:" + picUrl);
}
}
// 结束语
System.out.println("\n****已成功下载博主:" + BlogUtil.BLOG_HOME + "所有博客中的图片!****");
}
}
4.实现效果
- 控制台输出:
(1)下图是用富文本编辑器写的博客,因为之前我已经下载过来,使用显示已存在。
(2)下图是用markdown编辑器写的博客,因为之前我没有下载,所以会下载到本地。
- 本地根目录(部分截图)
- 次级目录(部分截图)
资源获取
- github:地址:https://github.com/yansheng836/jsoup-crawl-csdn ,下面方法2选1
- 下载release,地址:https://github.com/yansheng836/jsoup-crawl-csdn/releases
- 克隆到本地:git clone https://github.com/yansheng836/jsoup-crawl-csdn
- 百度云
链接:https://pan.baidu.com/s/1Fe4Z6qoFGwUWtag9-z-RSQ
提取码:vmt2