博客搬家系列(二)-爬取CSDN博客
博客搬家系列(一)-简介:https://blog.csdn.net/rico_zhou/article/details/83619152
博客搬家系列(三)-爬取博客园博客:https://blog.csdn.net/rico_zhou/article/details/83619525
博客搬家系列(四)-爬取简书文章:https://blog.csdn.net/rico_zhou/article/details/83619538
博客搬家系列(五)-爬取开源中国博客:https://blog.csdn.net/rico_zhou/article/details/83619561
博客搬家系列(六)-爬取今日头条文章:https://blog.csdn.net/rico_zhou/article/details/83619564
博客搬家系列(七)-本地WORD文档转HTML:https://blog.csdn.net/rico_zhou/article/details/83619573
博客搬家系列(八)-总结:https://blog.csdn.net/rico_zhou/article/details/83619599
创建java maven工程,先上一下项目代码截图
再上一张pom.xml图
爬取CSDN文章仅需要htmlunit和jsoup即可,当然完整项目是都需要的,htmlunit的简单使用请自行百度。
基本逻辑是这样,我们先找到CSDN网站每个用户文章列表的规律,然后获取目标条数的文章列表URL,再遍历每个url获取具体的文章内容,标题,类型,时间,以及图片转移等等
首先打开一个博主的主页,我们注意到网址就是很简单的https://blog.csdn.net/ + userId
当我们点击下一页的时候,网址变了,变成了https://blog.csdn.net/rico_zhou/article/list/1 出现了1,当我们把最后的1改成2后发现果然可以到达第二页,规律出现,那么我们只要循环拼接url,每一个url都可以获取一些(20条左右)文章,这样就可以获取目标数了。但是也要注意页数过大出现的空白
页数计算:根据目标文章条数获取总共的页数,然后循环获取文章URL的方法即可
String pageNum = (blogMove.getMoveNum() - 1) / 20 + 1;
再来分析一下主页的源码,浏览器右击鼠标选择查看网页源代码,我们可以发现,此页的文章摘要信息均存在于网页源码中,这是个好兆头,意味着不需要添加啥cookie或者动态执行js等就能获取目标,再观察一下,即可发现文章信息都在class为article-list的div中
注意观察,文章的URL都在此div下的子元素中,具体为class:article-item-box > h4 > a:href,找到了url就可以写代码了,使用jsoup可以方便的解析出html内容,强推!
请大家注意!不知为何,查找了好多博主主页源码,第一条均是标题为“帝都的凛冬”这篇博文且隐藏并无法查看,这里我们不管他,只需不存他入list即可,方法如下:存入list
/**
* @date Oct 17, 2018 12:30:46 PM
* @Desc
* @param blogMove
* @param oneUrl
* @return
* @throws IOException
* @throws MalformedURLException
* @throws FailingHttpStatusCodeException
*/
public void getCSDNArticleUrlList(Blogmove blogMove, String oneUrl, List urlList)
throws FailingHttpStatusCodeException, MalformedURLException, IOException {
// 模拟浏览器操作
// 创建WebClient
WebClient webClient = new WebClient(BrowserVersion.CHROME);
// 关闭css代码功能
webClient.getOptions().setThrowExceptionOnScriptError(false);
webClient.getOptions().setCssEnabled(false);
// 如若有可能找不到文件js则加上这句代码
webClient.getOptions().setThrowExceptionOnFailingStatusCode(false);
// 获取第一级网页html
HtmlPage page = webClient.getPage(oneUrl);
// System.out.println(page.asXml());
Document doc = Jsoup.parse(page.asXml());
Element pageMsg22 = doc.select("div.article-list").first();
if (pageMsg22 == null) {
return;
}
Elements pageMsg = pageMsg22.select("div.article-item-box");
Element linkNode;
for (Element e : pageMsg) {
linkNode = e.select("h4 a").first();
// 不知为何,所有的bloglist第一条都是这个:https://blog.csdn.net/yoyo_liyy/article/details/82762601
if (linkNode.attr("href").contains(blogMove.getMoveUserId())) {
if (urlList.size() < blogMove.getMoveNum()) {
urlList.add(linkNode.attr("href"));
} else {
break;
}
}
}
return;
}
注意一些null或者空值的处理,接下来遍历url list获取具体的文章信息
我们打开一篇博文,以使用爬虫框架htmlunit整合springboot不兼容的一个问题 为例,使用Chrome打开,我们可以看到一些基本信息
如文章的类型为原创,标题,时间,作者,阅读数,文章文字信息,图片信息等
接下来还是右击查看源代码找到对应的信息位置,以便于css选择器可以读取,注意找的结果要唯一,这里还要注意一点,当文章有code标签,也就是有代码时,使用Chrome模拟获取html会把code换行导致显示不美观,而使用edge模拟则效果好一些,开始写代码,老规矩,还是使用htmlunit模拟edge浏览器获取源码,使用jsoup解析为Document
/**
* @date Oct 17, 2018 12:46:52 PM
* @Desc 获取详细信息
* @param blogMove
* @param url
* @return
* @throws IOException
* @throws MalformedURLException
* @throws FailingHttpStatusCodeException
*/
public Blogcontent getCSDNArticleMsg(Blogmove blogMove, String url, List bList)
throws FailingHttpStatusCodeException, MalformedURLException, IOException {
Blogcontent blogcontent = new Blogcontent();
blogcontent.setArticleSource(blogMove.getMoveWebsiteId());
// 模拟浏览器操作
// 创建WebClient
WebClient webClient = new WebClient(BrowserVersion.EDGE);
// 关闭css代码功能
webClient.getOptions().setThrowExceptionOnScriptError(false);
webClient.getOptions().setCssEnabled(false);
// 如若有可能找不到文件js则加上这句代码
webClient.getOptions().setThrowExceptionOnFailingStatusCode(false);
// 获取第一级网页html
HtmlPage page = webClient.getPage(url);
Document doc = Jsoup.parse(page.asXml());
// 获取标题
String title = BlogMoveCSDNUtils.getCSDNArticleTitle(doc);
// 是否重复去掉
if (blogMove.getMoveRemoveRepeat() == 0) {
// 判断是否重复
if (BlogMoveCommonUtils.articleRepeat(bList, title)) {
return null;
}
}
blogcontent.setTitle(title);
// 获取作者
blogcontent.setAuthor(BlogMoveCSDNUtils.getCSDNArticleAuthor(doc));
// 获取时间
if (blogMove.getMoveUseOriginalTime() == 0) {
blogcontent.setGtmCreate(BlogMoveCSDNUtils.getCSDNArticleTime(doc));
} else {
blogcontent.setGtmCreate(new Date());
}
blogcontent.setGtmModified(new Date());
// 获取类型
blogcontent.setType(BlogMoveCSDNUtils.getCSDNArticleType(doc));
// 获取正文
blogcontent.setContent(BlogMoveCSDNUtils.getCSDNArticleContent(doc, blogMove, blogcontent));
// 设置其他
blogcontent.setStatus(blogMove.getMoveBlogStatus());
blogcontent.setBlogColumnName(blogMove.getMoveColumn());
// 特殊处理
blogcontent.setArticleEditor(blogMove.getMoveArticleEditor());
blogcontent.setShowId(DateUtils.format(new Date(), DateUtils.YYYYMMDDHHMMSSSSS));
blogcontent.setAllowComment(0);
blogcontent.setAllowPing(0);
blogcontent.setAllowDownload(0);
blogcontent.setShowIntroduction(1);
blogcontent.setIntroduction("");
blogcontent.setPrivateArticle(1);
return blogcontent;
}
获取标题,作者等信息详细代码
/**
* @date Oct 17, 2018 1:10:19 PM
* @Desc 获取标题
* @param doc
* @return
*/
public static String getCSDNArticleTitle(Document doc) {
// 标题
Element pageMsg2 = doc.select("div.article-title-box").first().select("h1.title-article").first();
return pageMsg2.html();
}
/**
* @date Oct 17, 2018 1:10:28 PM
* @Desc 获取作者
* @param doc
* @return
*/
public static String getCSDNArticleAuthor(Document doc) {
Element pageMsg2 = doc.select("div.article-info-box").first().select("a.follow-nickName").first();
return pageMsg2.html();
}
/**
* @date Oct 17, 2018 1:10:33 PM
* @Desc 获取时间
* @param doc
* @return
*/
public static Date getCSDNArticleTime(Document doc) {
Element pageMsg2 = doc.select("div.article-info-box").first().select("span.time").first();
String date = pageMsg2.html();
date = date.replace("年", "-").replace("月", "-").replace("日", "").trim();
return DateUtils.formatStringDate(date, DateUtils.YYYY_MM_DD_HH_MM_SS);
}
/**
* @date Oct 17, 2018 1:10:37 PM
* @Desc 获取类型
* @param doc
* @return
*/
public static String getCSDNArticleType(Document doc) {
Element pageMsg2 = doc.select("div.article-title-box").first().select("span.article-type").first();
if ("原".equals(pageMsg2.html())) {
return "原创";
} else if ("转".equals(pageMsg2.html())) {
return "转载";
} else if ("译".equals(pageMsg2.html())) {
return "翻译";
}
return "原创";
}
获取正文的代码需要处理下,主要是需要下载图片,然后替换源码中的img标签,给予自己设置的路径,路径可自行设置,只要能获取源码,其他都好说。只有此代码中过多的内容不必纠结,主要是复制过来的懒得改,完整代码见尾部。
/**
* @date Oct 17, 2018 1:10:41 PM
* @Desc 获取正文
* @param doc
* @param object
* @param blogcontent
* @return
*/
public static String getCSDNArticleContent(Document doc, Blogmove blogMove, Blogcontent blogcontent) {
Element pageMsg2 = doc.select("#article_content").get(0).select("div.htmledit_views").first();
String content = pageMsg2.toString();
String images;
// 注意是否需要替换图片
if (blogMove.getMoveSaveImg() == 0) {
// 保存图片到本地
// 先获取所有图片连接,再按照每个链接下载图片,最后替换原有链接
// 先创建一个文件夹
// 先创建一个临时文件夹
String blogFileName = String.valueOf(UUID.randomUUID());
FileUtils.createFolder(FilePathConfig.getUploadBlogPath() + File.separator + blogFileName);
blogcontent.setBlogFileName(blogFileName);
// 匹配出所有链接
List imgList = BlogMoveCommonUtils.getArticleImgList(content);
// 下载并返回重新生成的imgurllist
List newImgList = BlogMoveCommonUtils.getArticleNewImgList(blogMove, imgList, blogFileName);
// 拼接文章所有链接
images = BlogMoveCommonUtils.getArticleImages(newImgList);
blogcontent.setImages(images);
// 替换所有链接按顺序
content = getCSDNNewArticleContent(content, imgList, newImgList);
}
return content;
}
/**
* @date Oct 22, 2018 3:31:40 PM
* @Desc
* @param content
* @param imgList
* @param newImgList
* @return
*/
private static String getCSDNNewArticleContent(String content, List imgList, List newImgList) {
Document doc = Jsoup.parse(content);
Elements imgTags = doc.select("img[src]");
if (imgList == null || imgList.size() < 1 || newImgList == null || newImgList.size() < 1 || imgTags == null
|| "".equals(imgTags)) {
return content;
}
for (int i = 0; i < imgTags.size(); i++) {
imgTags.get(i).attr("src", newImgList.get(i));
}
return doc.body().toString();
}
这里着重讲一下,下载图片的处理,本以为是比较简单的直接下载即可,但是运行居然出错,于是我在浏览器中单独打开图片发现,csdn图片访问403,但是当你打开文章的时候却可以查看,清除缓存后再次访问图片即403禁止,显然此图片链接需带有cookie等header信息的,但是当我加入cookie时,还是无法下载,经同学指导,一矢中的,加上Referrer(即主页地址) 即可
// 下载图片
public static String downloadImg(String urlString, String filename, String savePath, Blogmove blogMove) {
String imgType = null;
try {
// 构造URL
URL url = new URL(urlString);
// 打开连接
URLConnection con = url.openConnection();
// 设置请求超时为5s
con.setConnectTimeout(5 * 1000);
// 设置cookie
BlogMoveCommonUtils.setBlogMoveDownImgCookie(con, blogMove);
// 输入流
InputStream is = con.getInputStream();
// imgType = ImageUtils.getPicType((BufferedInputStream) is);
imgType = FileExtensionConstant.FILE_EXTENSION_IMAGE_PNG;
// 1K的数据缓冲
byte[] bs = new byte[1024];
// 读取到的数据长度
int len;
// 输出的文件流
File sf = new File(savePath);
if (!sf.exists()) {
sf.mkdirs();
}
OutputStream os = new FileOutputStream(
sf.getPath() + File.separator + filename + CommonSymbolicConstant.POINT + imgType);
// 开始读取
while ((len = is.read(bs)) != -1) {
os.write(bs, 0, len);
}
// 完毕,关闭所有链接
os.close();
is.close();
return filename + CommonSymbolicConstant.POINT + imgType;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* @date Oct 30, 2018 1:39:11 PM
* @Desc 下载图片设置cookie
* @param con
* @param blogMove
*/
public static void setBlogMoveDownImgCookie(URLConnection con, Blogmove blogMove) {
// 这地方注意当单条获取时正则匹配出url中referer
if (blogMove.getMoveMode() == 0) {
// 多条
if (BlogConstant.BLOG_BLOGMOVE_WEBSITE_NAME_CSDN.equals(blogMove.getMoveWebsiteId())) {
con.setRequestProperty("Referer", blogMove.getMoveWebsiteUrl() + blogMove.getMoveUserId());
}
} else if (blogMove.getMoveMode() == 1) {
// 一条
if (BlogConstant.BLOG_BLOGMOVE_WEBSITE_NAME_CSDN.equals(blogMove.getMoveWebsiteId())) {
con.setRequestProperty("Referer",
blogMove.getMoveWebsiteUrl().substring(0, blogMove.getMoveWebsiteUrl().indexOf("article")));
}
}
}
然后将图片的地址与文章中img标签替换,使用jsoup很好替换:
输出结果或者存入数据库
本人网站效果图:
欢迎交流学习!
完整源码请见github:https://github.com/ricozhou/blogmove