博客搬家系列(二)-爬取CSDN博客css
博客搬家系列(一)-简介:http://www.javashuo.com/article/p-ctgxpaub-bu.htmlhtml
博客搬家系列(三)-爬取博客园博客:http://www.javashuo.com/article/p-hbjeoaya-gc.htmljava
博客搬家系列(四)-爬取简书文章:https://blog.csdn.net/rico_zhou/article/details/83619538git
博客搬家系列(五)-爬取开源中国博客:https://blog.csdn.net/rico_zhou/article/details/83619561github
博客搬家系列(六)-爬取今日头条文章:https://blog.csdn.net/rico_zhou/article/details/83619564web
博客搬家系列(七)-本地WORD文档转HTML:https://blog.csdn.net/rico_zhou/article/details/83619573spring
博客搬家系列(八)-总结: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<String> 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<Blogcontent> 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<String> imgList = BlogMoveCommonUtils.getArticleImgList(content); // 下载并返回从新生成的imgurllist List<String> 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<String> imgList, List<String> 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