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

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

博客搬家系列(二)-爬取CSDN博客_第1张图片

再上一张pom.xml图

博客搬家系列(二)-爬取CSDN博客_第2张图片

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

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

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

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

博客搬家系列(二)-爬取CSDN博客_第3张图片

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

博客搬家系列(二)-爬取CSDN博客_第4张图片

博客搬家系列(二)-爬取CSDN博客_第5张图片

博客搬家系列(二)-爬取CSDN博客_第6张图片

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

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

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

博客搬家系列(二)-爬取CSDN博客_第7张图片

注意观察,文章的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;
	}

博客搬家系列(二)-爬取CSDN博客_第8张图片

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

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

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

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

博客搬家系列(二)-爬取CSDN博客_第9张图片

接下来还是右击查看源代码找到对应的信息位置,以便于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")));
			}
		}

	}

博客搬家系列(二)-爬取CSDN博客_第10张图片

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

博客搬家系列(二)-爬取CSDN博客_第11张图片

输出结果或者存入数据库

博客搬家系列(二)-爬取CSDN博客_第12张图片

博客搬家系列(二)-爬取CSDN博客_第13张图片

本人网站效果图:

博客搬家系列(二)-爬取CSDN博客_第14张图片

 

欢迎交流学习!

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

 

你可能感兴趣的:(java,spider,大数据)