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