博客搬家系列(二)-爬取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: