博客搬家系列(二)-爬取CSDN博客

不念不忘少年蓝@ 2022-04-22 01:32 481阅读 0赞

博客搬家系列(二)-爬取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工程,先上一下项目代码截图

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3JpY29femhvdQ_size_16_color_FFFFFF_t_70

再上一张pom.xml图

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3JpY29femhvdQ_size_16_color_FFFFFF_t_70 1

爬取CSDN文章仅需要htmlunit和jsoup即可,当然完整项目是都需要的,htmlunit的简单使用请自行百度。

基本逻辑是这样,我们先找到CSDN网站每个用户文章列表的规律,然后获取目标条数的文章列表URL,再遍历每个url获取具体的文章内容,标题,类型,时间,以及图片转移等等

三.开干(获取文章URL集合)

首先打开一个博主的主页,我们注意到网址就是很简单的https://blog.csdn.net/ + userId

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3JpY29femhvdQ_size_16_color_FFFFFF_t_70 2

当我们点击下一页的时候,网址变了,变成了https://blog.csdn.net/rico\_zhou/article/list/1 出现了1,当我们把最后的1改成2后发现果然可以到达第二页,规律出现,那么我们只要循环拼接url,每一个url都可以获取一些(20条左右)文章,这样就可以获取目标数了。但是也要注意页数过大出现的空白

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3JpY29femhvdQ_size_16_color_FFFFFF_t_70 3

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3JpY29femhvdQ_size_16_color_FFFFFF_t_70 4

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3JpY29femhvdQ_size_16_color_FFFFFF_t_70 5

页数计算:根据目标文章条数获取总共的页数,然后循环获取文章URL的方法即可

  1. String pageNum = (blogMove.getMoveNum() - 1) / 20 + 1;

再来分析一下主页的源码,浏览器右击鼠标选择查看网页源代码,我们可以发现,此页的文章摘要信息均存在于网页源码中,这是个好兆头,意味着不需要添加啥cookie或者动态执行js等就能获取目标,再观察一下,即可发现文章信息都在class为article-list的div中

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3JpY29femhvdQ_size_16_color_FFFFFF_t_70 6

注意观察,文章的URL都在此div下的子元素中,具体为class:article-item-box > h4 > a:href,找到了url就可以写代码了,使用jsoup可以方便的解析出html内容,强推!

请大家注意!不知为何,查找了好多博主主页源码,第一条均是标题为“帝都的凛冬”这篇博文且隐藏并无法查看,这里我们不管他,只需不存他入list即可,方法如下:存入list

  1. /**
  2. * @date Oct 17, 2018 12:30:46 PM
  3. * @Desc
  4. * @param blogMove
  5. * @param oneUrl
  6. * @return
  7. * @throws IOException
  8. * @throws MalformedURLException
  9. * @throws FailingHttpStatusCodeException
  10. */
  11. public void getCSDNArticleUrlList(Blogmove blogMove, String oneUrl, List<String> urlList)
  12. throws FailingHttpStatusCodeException, MalformedURLException, IOException {
  13. // 模拟浏览器操作
  14. // 创建WebClient
  15. WebClient webClient = new WebClient(BrowserVersion.CHROME);
  16. // 关闭css代码功能
  17. webClient.getOptions().setThrowExceptionOnScriptError(false);
  18. webClient.getOptions().setCssEnabled(false);
  19. // 如若有可能找不到文件js则加上这句代码
  20. webClient.getOptions().setThrowExceptionOnFailingStatusCode(false);
  21. // 获取第一级网页html
  22. HtmlPage page = webClient.getPage(oneUrl);
  23. // System.out.println(page.asXml());
  24. Document doc = Jsoup.parse(page.asXml());
  25. Element pageMsg22 = doc.select("div.article-list").first();
  26. if (pageMsg22 == null) {
  27. return;
  28. }
  29. Elements pageMsg = pageMsg22.select("div.article-item-box");
  30. Element linkNode;
  31. for (Element e : pageMsg) {
  32. linkNode = e.select("h4 a").first();
  33. // 不知为何,所有的bloglist第一条都是这个:https://blog.csdn.net/yoyo_liyy/article/details/82762601
  34. if (linkNode.attr("href").contains(blogMove.getMoveUserId())) {
  35. if (urlList.size() < blogMove.getMoveNum()) {
  36. urlList.add(linkNode.attr("href"));
  37. } else {
  38. break;
  39. }
  40. }
  41. }
  42. return;
  43. }

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3JpY29femhvdQ_size_16_color_FFFFFF_t_70 7

注意一些null或者空值的处理,接下来遍历url list获取具体的文章信息

四.开干(获取文章具体信息)

我们打开一篇博文,以使用爬虫框架htmlunit整合springboot不兼容的一个问题 为例,使用Chrome打开,我们可以看到一些基本信息

如文章的类型为原创,标题,时间,作者,阅读数,文章文字信息,图片信息等

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3JpY29femhvdQ_size_16_color_FFFFFF_t_70 8

接下来还是右击查看源代码找到对应的信息位置,以便于css选择器可以读取,注意找的结果要唯一,这里还要注意一点,当文章有code标签,也就是有代码时,使用Chrome模拟获取html会把code换行导致显示不美观,而使用edge模拟则效果好一些,开始写代码,老规矩,还是使用htmlunit模拟edge浏览器获取源码,使用jsoup解析为Document

  1. /**
  2. * @date Oct 17, 2018 12:46:52 PM
  3. * @Desc 获取详细信息
  4. * @param blogMove
  5. * @param url
  6. * @return
  7. * @throws IOException
  8. * @throws MalformedURLException
  9. * @throws FailingHttpStatusCodeException
  10. */
  11. public Blogcontent getCSDNArticleMsg(Blogmove blogMove, String url, List<Blogcontent> bList)
  12. throws FailingHttpStatusCodeException, MalformedURLException, IOException {
  13. Blogcontent blogcontent = new Blogcontent();
  14. blogcontent.setArticleSource(blogMove.getMoveWebsiteId());
  15. // 模拟浏览器操作
  16. // 创建WebClient
  17. WebClient webClient = new WebClient(BrowserVersion.EDGE);
  18. // 关闭css代码功能
  19. webClient.getOptions().setThrowExceptionOnScriptError(false);
  20. webClient.getOptions().setCssEnabled(false);
  21. // 如若有可能找不到文件js则加上这句代码
  22. webClient.getOptions().setThrowExceptionOnFailingStatusCode(false);
  23. // 获取第一级网页html
  24. HtmlPage page = webClient.getPage(url);
  25. Document doc = Jsoup.parse(page.asXml());
  26. // 获取标题
  27. String title = BlogMoveCSDNUtils.getCSDNArticleTitle(doc);
  28. // 是否重复去掉
  29. if (blogMove.getMoveRemoveRepeat() == 0) {
  30. // 判断是否重复
  31. if (BlogMoveCommonUtils.articleRepeat(bList, title)) {
  32. return null;
  33. }
  34. }
  35. blogcontent.setTitle(title);
  36. // 获取作者
  37. blogcontent.setAuthor(BlogMoveCSDNUtils.getCSDNArticleAuthor(doc));
  38. // 获取时间
  39. if (blogMove.getMoveUseOriginalTime() == 0) {
  40. blogcontent.setGtmCreate(BlogMoveCSDNUtils.getCSDNArticleTime(doc));
  41. } else {
  42. blogcontent.setGtmCreate(new Date());
  43. }
  44. blogcontent.setGtmModified(new Date());
  45. // 获取类型
  46. blogcontent.setType(BlogMoveCSDNUtils.getCSDNArticleType(doc));
  47. // 获取正文
  48. blogcontent.setContent(BlogMoveCSDNUtils.getCSDNArticleContent(doc, blogMove, blogcontent));
  49. // 设置其他
  50. blogcontent.setStatus(blogMove.getMoveBlogStatus());
  51. blogcontent.setBlogColumnName(blogMove.getMoveColumn());
  52. // 特殊处理
  53. blogcontent.setArticleEditor(blogMove.getMoveArticleEditor());
  54. blogcontent.setShowId(DateUtils.format(new Date(), DateUtils.YYYYMMDDHHMMSSSSS));
  55. blogcontent.setAllowComment(0);
  56. blogcontent.setAllowPing(0);
  57. blogcontent.setAllowDownload(0);
  58. blogcontent.setShowIntroduction(1);
  59. blogcontent.setIntroduction("");
  60. blogcontent.setPrivateArticle(1);
  61. return blogcontent;
  62. }

获取标题,作者等信息详细代码

  1. /**
  2. * @date Oct 17, 2018 1:10:19 PM
  3. * @Desc 获取标题
  4. * @param doc
  5. * @return
  6. */
  7. public static String getCSDNArticleTitle(Document doc) {
  8. // 标题
  9. Element pageMsg2 = doc.select("div.article-title-box").first().select("h1.title-article").first();
  10. return pageMsg2.html();
  11. }
  12. /**
  13. * @date Oct 17, 2018 1:10:28 PM
  14. * @Desc 获取作者
  15. * @param doc
  16. * @return
  17. */
  18. public static String getCSDNArticleAuthor(Document doc) {
  19. Element pageMsg2 = doc.select("div.article-info-box").first().select("a.follow-nickName").first();
  20. return pageMsg2.html();
  21. }
  22. /**
  23. * @date Oct 17, 2018 1:10:33 PM
  24. * @Desc 获取时间
  25. * @param doc
  26. * @return
  27. */
  28. public static Date getCSDNArticleTime(Document doc) {
  29. Element pageMsg2 = doc.select("div.article-info-box").first().select("span.time").first();
  30. String date = pageMsg2.html();
  31. date = date.replace("年", "-").replace("月", "-").replace("日", "").trim();
  32. return DateUtils.formatStringDate(date, DateUtils.YYYY_MM_DD_HH_MM_SS);
  33. }
  34. /**
  35. * @date Oct 17, 2018 1:10:37 PM
  36. * @Desc 获取类型
  37. * @param doc
  38. * @return
  39. */
  40. public static String getCSDNArticleType(Document doc) {
  41. Element pageMsg2 = doc.select("div.article-title-box").first().select("span.article-type").first();
  42. if ("原".equals(pageMsg2.html())) {
  43. return "原创";
  44. } else if ("转".equals(pageMsg2.html())) {
  45. return "转载";
  46. } else if ("译".equals(pageMsg2.html())) {
  47. return "翻译";
  48. }
  49. return "原创";
  50. }

获取正文的代码需要处理下,主要是需要下载图片,然后替换源码中的img标签,给予自己设置的路径,路径可自行设置,只要能获取源码,其他都好说。只有此代码中过多的内容不必纠结,主要是复制过来的懒得改,完整代码见尾部。

  1. /**
  2. * @date Oct 17, 2018 1:10:41 PM
  3. * @Desc 获取正文
  4. * @param doc
  5. * @param object
  6. * @param blogcontent
  7. * @return
  8. */
  9. public static String getCSDNArticleContent(Document doc, Blogmove blogMove, Blogcontent blogcontent) {
  10. Element pageMsg2 = doc.select("#article_content").get(0).select("div.htmledit_views").first();
  11. String content = pageMsg2.toString();
  12. String images;
  13. // 注意是否需要替换图片
  14. if (blogMove.getMoveSaveImg() == 0) {
  15. // 保存图片到本地
  16. // 先获取所有图片连接,再按照每个链接下载图片,最后替换原有链接
  17. // 先创建一个文件夹
  18. // 先创建一个临时文件夹
  19. String blogFileName = String.valueOf(UUID.randomUUID());
  20. FileUtils.createFolder(FilePathConfig.getUploadBlogPath() + File.separator + blogFileName);
  21. blogcontent.setBlogFileName(blogFileName);
  22. // 匹配出所有链接
  23. List<String> imgList = BlogMoveCommonUtils.getArticleImgList(content);
  24. // 下载并返回重新生成的imgurllist
  25. List<String> newImgList = BlogMoveCommonUtils.getArticleNewImgList(blogMove, imgList, blogFileName);
  26. // 拼接文章所有链接
  27. images = BlogMoveCommonUtils.getArticleImages(newImgList);
  28. blogcontent.setImages(images);
  29. // 替换所有链接按顺序
  30. content = getCSDNNewArticleContent(content, imgList, newImgList);
  31. }
  32. return content;
  33. }
  34. /**
  35. * @date Oct 22, 2018 3:31:40 PM
  36. * @Desc
  37. * @param content
  38. * @param imgList
  39. * @param newImgList
  40. * @return
  41. */
  42. private static String getCSDNNewArticleContent(String content, List<String> imgList, List<String> newImgList) {
  43. Document doc = Jsoup.parse(content);
  44. Elements imgTags = doc.select("img[src]");
  45. if (imgList == null || imgList.size() < 1 || newImgList == null || newImgList.size() < 1 || imgTags == null
  46. || "".equals(imgTags)) {
  47. return content;
  48. }
  49. for (int i = 0; i < imgTags.size(); i++) {
  50. imgTags.get(i).attr("src", newImgList.get(i));
  51. }
  52. return doc.body().toString();
  53. }

这里着重讲一下,下载图片的处理,本以为是比较简单的直接下载即可,但是运行居然出错,于是我在浏览器中单独打开图片发现,csdn图片访问403,但是当你打开文章的时候却可以查看,清除缓存后再次访问图片即403禁止,显然此图片链接需带有cookie等header信息的,但是当我加入cookie时,还是无法下载,经同学指导,一矢中的,加上Referrer(即主页地址) 即可

  1. // 下载图片
  2. public static String downloadImg(String urlString, String filename, String savePath, Blogmove blogMove) {
  3. String imgType = null;
  4. try {
  5. // 构造URL
  6. URL url = new URL(urlString);
  7. // 打开连接
  8. URLConnection con = url.openConnection();
  9. // 设置请求超时为5s
  10. con.setConnectTimeout(5 * 1000);
  11. // 设置cookie
  12. BlogMoveCommonUtils.setBlogMoveDownImgCookie(con, blogMove);
  13. // 输入流
  14. InputStream is = con.getInputStream();
  15. // imgType = ImageUtils.getPicType((BufferedInputStream) is);
  16. imgType = FileExtensionConstant.FILE_EXTENSION_IMAGE_PNG;
  17. // 1K的数据缓冲
  18. byte[] bs = new byte[1024];
  19. // 读取到的数据长度
  20. int len;
  21. // 输出的文件流
  22. File sf = new File(savePath);
  23. if (!sf.exists()) {
  24. sf.mkdirs();
  25. }
  26. OutputStream os = new FileOutputStream(
  27. sf.getPath() + File.separator + filename + CommonSymbolicConstant.POINT + imgType);
  28. // 开始读取
  29. while ((len = is.read(bs)) != -1) {
  30. os.write(bs, 0, len);
  31. }
  32. // 完毕,关闭所有链接
  33. os.close();
  34. is.close();
  35. return filename + CommonSymbolicConstant.POINT + imgType;
  36. } catch (IOException e) {
  37. e.printStackTrace();
  38. }
  39. return null;
  40. }
  41. /**
  42. * @date Oct 30, 2018 1:39:11 PM
  43. * @Desc 下载图片设置cookie
  44. * @param con
  45. * @param blogMove
  46. */
  47. public static void setBlogMoveDownImgCookie(URLConnection con, Blogmove blogMove) {
  48. // 这地方注意当单条获取时正则匹配出url中referer
  49. if (blogMove.getMoveMode() == 0) {
  50. // 多条
  51. if (BlogConstant.BLOG_BLOGMOVE_WEBSITE_NAME_CSDN.equals(blogMove.getMoveWebsiteId())) {
  52. con.setRequestProperty("Referer", blogMove.getMoveWebsiteUrl() + blogMove.getMoveUserId());
  53. }
  54. } else if (blogMove.getMoveMode() == 1) {
  55. // 一条
  56. if (BlogConstant.BLOG_BLOGMOVE_WEBSITE_NAME_CSDN.equals(blogMove.getMoveWebsiteId())) {
  57. con.setRequestProperty("Referer",
  58. blogMove.getMoveWebsiteUrl().substring(0, blogMove.getMoveWebsiteUrl().indexOf("article")));
  59. }
  60. }
  61. }

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3JpY29femhvdQ_size_16_color_FFFFFF_t_70 9

然后将图片的地址与文章中img标签替换,使用jsoup很好替换:

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3JpY29femhvdQ_size_16_color_FFFFFF_t_70 10

输出结果或者存入数据库

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3JpY29femhvdQ_size_16_color_FFFFFF_t_70 11

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3JpY29femhvdQ_size_16_color_FFFFFF_t_70 12

本人网站效果图:

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3JpY29femhvdQ_size_16_color_FFFFFF_t_70 13

欢迎交流学习!

完整源码请见github:https://github.com/ricozhou/blogmove

发表评论

表情:
评论列表 (有 0 条评论,481人围观)

还没有评论,来说两句吧...

相关阅读

    相关 搬家

    博客搬家 因为linux相关的一些内容始终审核不过,而Linux和ros安装必须使用。所以造成了分享给朋友的不方便。所以搬家去了博客园。 地址 https://w

    相关 JavaCSDN

    最近由于要做一个关于技术博客搜索的搜索工具,我开始接触到了爬虫,因为Java学的比较精通(主要是有很多封装的工具包),写了一个小小的Demo,进入主题吧 [参开博客链接][