1. 程式人生 > >部落格搬家系列(二)-爬取CSDN部落格

部落格搬家系列(二)-爬取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工程,先上一下專案程式碼截圖

再上一張pom.xml圖

爬取CSDN文章僅需要htmlunit和jsoup即可,當然完整專案是都需要的,htmlunit的簡單使用請自行百度。

基本邏輯是這樣,我們先找到CSDN網站每個使用者文章列表的規律,然後獲取目標條數的文章列表URL,再遍歷每個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: