備份CSDN部落格本文到本地存檔

來源:互聯網
上載者:User
大哥有了新想法,然而沒有技術,令人欣慰的是大哥想到了我,於是我便答應免費幫個忙,這是一個雲端式的項目,具體細節也就不透露了,然而在實現的過程中,其中一個模組我覺得可以自用,於是我就想把這個模組抽出來,該模組的功能就是將CSDN部落格上的文章下載到本地。
        假期只完成了一個模板,雖然很垃圾,但是卻能滿足自用的需求,一直以來,我都很害怕自己喝懵了寫的一些感悟放在網上會在某一天再也打不開,事實上,這種事 情確實也發生過很多次。忘記使用者,文章被管理員刪除,部落格被封閉,網站不再維護等都會導致這樣的問題,於是我就不得不定期將自己在各個網站註冊的部落格複製 到一個本地的文檔上,包括網易的,百度的,CSDN的,51CTO的,以及老婆的QQ空間的(我總是將內容發布在老婆的QQ上,因為那上面可以暢所欲 言),這麼多的網站,如此多的日誌,工作量真的不少,久而久之,本地的存檔也越來越亂,漸漸的,有很多文章都被遺漏了。特別是CSDN的部落格,一直以來, 我都想將其完整的dump到本地,一篇一篇的複製,簡直不可能,因為太多了,也找過整站下載器,但是效果不理想。趁此機會,別人委託我做的這個小玩意正好 可以用於此目的,而且比較滿意的一點就是dump下來的每一篇文章都裁掉了不相關的內容,比如友情連結,部落格訪問量以及廣告等,唯一被保留的就是本文和正 文的圖片資源。
        起初我是使用C++手工實現的,然而卻需要自行解析HTML文檔的各個標籤,落實下來就是複雜的字串解析,其實字串解析可以堪稱是編程的精髓,但是對 於實際做項目,這種工作還是直接使用現有的解析庫比較好,後來我發現使用指令碼語言更簡單,比如使用python,perl,甚至grep/awk/sed 都可以,然而字元編碼卻始終是一個大問題。通過諮詢一個超猛的同事,我認識了htmlparser這個java庫,實在是太方便了,它將html檔案元素 抽象成了各個類,這樣可以很方便的實現過濾,更可貴的是,這種過濾甚至都不用自己實現,htmlparser中內建了過濾功能,你要做的只是重載一些方法 即可,這樣就是使用很簡單的代碼實現這個部落格下載功能了,下載完了之後最好將其儲存成一個單獨的PDF文檔,雖然java也可以實現這個功能,然而目前已 經有了很多這樣的工具,有現成工具的就不編程實現,這永遠是一個真理。
        首先看一下效果,然後看一下代碼。
        儲存在本地的存檔擁有下面的目錄結構,首先是一個頂級目錄,以我部落格的標題來命名,內部是一個按月份存檔的目錄集合以及一個index.html索引檔案,如所示:
 
 

展開一個月份存檔目錄,你將看到本月的文章集合,每一篇文章包含一個目錄,如所示:


每一篇文章包含的目錄中儲存有該篇文章包含的所有圖片,如果沒有包含圖片,則目錄為空白,如:
 

隨意開啟一篇文章,你將看到該篇文章的標題以及本文,所有的圖片也被包含,連結到了該文章的_files目錄中的對應圖片,如:


 
 

index.html呈現處以下的樣子:


點擊每一篇,將跳轉到該篇文章
如果你以文字編輯器或者xcode開啟每一篇文章或者index.html檔案,你將看到其中的大部分連結都被改成了本地的相對路徑了,並且刪除了大量的無關的內容,這種修改很簡單,手工改其實很可以做到,使用程式來做當然更簡便些,問題是當你寫程式所帶來的麻煩超過了手工修改的麻煩時,這種編程就很沒有意義,幸運的是,htmlparser可以很簡單的做到這一點,一點也不複雜,這樣的話這種編程就顯得很有意義了。
        代碼很簡單,基本就是幾大塊:
1.幾次遍曆-按首頁遍曆月份資訊,按月份存檔遍曆文章,按每一篇文章遍曆圖片;
2.解析關鍵資訊,比如標題,文章中的圖片等,並填充資料結構。這種事可以通過Filter來完成;
3.根據filter的副作用填充的資訊組建目錄。

需要說明的是,以下的代碼完全是過程化的,沒有使用java語言的OO特性,因此它的資料以及方法完全是static的,沒有產生任何對象,我只是想使用htmlparser的API以及java語言IDE的諸多良好的功能,比如方法以及方法參數的自動補全功能,老手或者科班高年級學生可能會較真地說,C/C++的IDE也可以支援這樣的功能,如果碰到這樣反駁的,我也可以說,其實嘛,組合語言也是可以自動補全的…另外,代碼中有很多的寫入程式碼,其實應該將它們再抽象一下的,或者說定義成變數也可以,只是因為自用,以後也不準備維護,就這麼著了。還有,那就是最大的問題,代碼有一些bug,比如對於標題中含有奇怪字元的支援,以及錯誤記錄檔(這很重要)的記錄的缺失等等。不管怎麼說,代碼如下:

import  org.htmlparser.Node;import  org.htmlparser.NodeFilter;import  org.htmlparser.Parser;import  org.htmlparser.filters.TagNameFilter;import  org.htmlparser.util.NodeList;import org.htmlparser.tags.*;import java.io.*;import java.net.*;import java.nio.*;import java.util.List;import javax.management.*;/* 類名使用test很不規範,然而為了方面胡亂起的名字,可是不管怎麼說,它確實是個test */public class  test {/* 月份文章的月份名稱/月份存檔URL對的列表 */final static AttributeList indexList = new AttributeList();/* 每月文章名稱/每月文章的URL對的列表 */final static AttributeList articleList = new AttributeList();/* 每篇文章圖片本地存檔地址/每篇文章圖片URL對的列表 */final static AttributeList resourceList = new AttributeList();/* 儲存月份以及該月文章本地存檔的列表,用於組建目錄 */static AttributeList monthList = new AttributeList();/* 用於產生本地存檔目錄的writer */static OutputStreamWriter index_handle = null;static String proxy_addr = null;static int proxy_port = 3128;/** @param url 網頁的URL* @param type 類型:1為文本,0為二進位* @return 內容的位元組數組*/public static byte[] GetContent(String url, int type) {byte ret[] = null;try  {HttpURLConnection conn = null;InputStream urlStream = null;;URL surl = new  URL(url);int j = -1;if (proxy_addr != null) {InetSocketAddress soA = new InetSocketAddress(InetAddress.getByName(proxy_addr), proxy_port);Proxy proxy = new Proxy(Proxy.Type.HTTP, soA);conn =  (HttpURLConnection) surl.openConnection(proxy);} else {              conn =  (HttpURLConnection) surl.openConnection();}/* 必須加上這一句偽裝成Mozilla瀏覽器,否則CSDN會拒絕串連 */conn.setRequestProperty( "User-Agent","Mozilla/4.0"); conn.connect();urlStream = conn.getInputStream();if (type == 1) {String sTotalString = "";BufferedReader reader =  new  BufferedReader(new  InputStreamReader(urlStream, "UTF-8")); CharBuffer vv = CharBuffer.allocate(1024);while ((j = reader.read(vv.array())) != -1) {sTotalString += new String(vv.array(), 0, j);          vv.clear();}sTotalString = sTotalString.replace('\n', ' '); sTotalString = sTotalString.replace('\r', ' ');ret = sTotalString.getBytes();} else {ByteBuffer vv = ByteBuffer.allocate(1024);/* CSDN允許最大圖片有上限 */ByteBuffer buffer = ByteBuffer.allocate(5000000);while ((j = urlStream.read(vv.array())) != -1)   { buffer.put(vv.array(), 0, j);vv.clear();} ret = buffer.array();}}catch (Exception e){e.printStackTrace();//追加出錯日誌} return ret;}/** @param path 檔案路徑* @param content 檔案內容的位元組數組* @return 成功或者失敗*/public static boolean WriteFile(String path, byte[] content) {try {FileOutputStream osw = new FileOutputStream(path);osw.write(content);  osw.close();} catch  (Exception e) {e.printStackTrace();//追加出錯日誌return false;}return true;}/** @param path 目錄路徑* @return 成功或者失敗*/public static boolean MKDir(String path) {try {       File fp = new File(path);if(!fp.exists()) {fp.mkdir();}} catch(Exception e) {e.printStackTrace();//追加出錯日誌return false;}return true;}/** @param path 檔案路徑* @param url 文章在blog上的URL* @param articles 儲存本月存檔的列表* @return 無*/public static void HandleHtml(String path, String url, AttributeList articles) {try {         StringBuffer text = new  StringBuffer();NodeList nodes = HandleText(new String(GetContent(url, 1)), 3);Node node = nodes.elementAt(0);String title = (String)((List<Attribute>)resourceList.asList()).get(0).getValue();                String filepath=path+"/"+title;List<Attribute> li = resourceList.asList();/* 加入meta資訊 */text.append( new  String("<meta http-equiv=\"Content-Type\" content=\"text/html; chaset=utf-8\"/>"));text.append("<h1>"+title+"</h1>");                  if (node != null) {Div dv = (Div)node; text.append( new  String(dv.toHtml().getBytes("UTF-8"), "UTF-8"));} else {text.append("<h3>Download error</h3>");}test.MKDir(filepath+"_files"); articles.add(new Attribute(filepath.split("/", 2)[1], title));                               for (int i = 1; i< li.size(); i ++) {byte[] imgString = GetContent((String)li.get(i).getValue(), 0);test.WriteFile(filepath+"_files/"+li.get(i).getName()+".gif", imgString);}resourceList.clear();test.WriteFile(filepath+".html", text.toString().getBytes());}  catch  (Exception e) {//追加出錯日誌e.printStackTrace();} }/** @param nlist HTML本文的子標籤鏈表* @param index 用於索引圖片的個數以及當前的圖片數* @return 當前的圖片數*/public static int parseImg(NodeList nlist, int index) {Node img = null;int count = nlist.size();for (int i = 0; i < count; i++) {img = nlist.elementAt(i);if (img instanceof ImageTag ) {ImageTag imgtag = (ImageTag)img;if (!imgtag.isEndTag()) {String title = (String)((List<Attribute>)resourceList.asList()).get(0).getValue();/* 將圖片的URL映射成本地路徑 */resourceList.add(new Attribute(""+index, new String(imgtag.extractImageLocn().getBytes())));title = title.trim();imgtag.setImageURL(title+"_files/"+index+".gif");/* 遞增本地路徑序列 */index++;}} else {NodeList slist = img.getChildren();if (slist != null && slist.size() > 0) {index = test.parseImg(slist, index);}}}return index;}/** @param nlist HTML月份存檔的子標籤鏈表* @param index 無用* @return 無用*/public static int parseMonthArticle(NodeList nlist, int index) {Node atls = null;int count = nlist.size();for (int i = 0; i < count; i++) {atls = nlist.elementAt(i);if (atls instanceof LinkTag ) {LinkTag link = (LinkTag)atls;indexList.add(new Attribute(link.getLinkText(), link.extractLink()));} else {NodeList slist = atls.getChildren();if (slist != null && slist.size() > 0) {index = test.parseMonthArticle(slist, index);}}}return index;}/** @param nlist HTML標題的子標籤鏈表* @param index 無用* @return 無用*/public static int parseTitle(NodeList nlist, int index) {Node tit = null;int count = nlist.size();for (int i = 0; i < count; i++) {tit = nlist.elementAt(i);if (tit instanceof Span ) {Span span = (Span)tit;                                               if (span.getAttribute("class") != null && span.getAttribute("class").equalsIgnoreCase("link_title")) {LinkTag link = (LinkTag)span.childAt(0);String title = link.getLinkText();/* 將檔案名稱中不允許的字元替換成允許的字元 */title = title.replace('/', '-');title = title.trim();title = title.replace(' ', '-');resourceList.add(new Attribute("title", title));}} else {NodeList slist = tit.getChildren();if (slist != null && slist.size() > 0) {index = test.parseTitle(slist, index);}}}return index;}/** @param nlist HTML每月份存檔的子標籤鏈表* @param index 無用* @return 無用*/public static int parsePerArticle(NodeList nlist, int index) {Node atl = null;int count = nlist.size();for (int i = 0; i < count; i++) {atl = nlist.elementAt(i);if (atl instanceof Span ) {Span span = (Span)atl;if (span.getAttribute("class") != null && span.getAttribute("class").equalsIgnoreCase("link_title")) {LinkTag link = (LinkTag)span.childAt(0);articleList.add(new Attribute(link.getLinkText(), "http://blog.csdn.net"+link.extractLink()));}} else {NodeList slist = atl.getChildren();if (slist != null && slist.size() > 0) {index = test.parsePerArticle(slist, index);}}}return index;}/** @param nlist HTML分頁顯示標籤的子標籤鏈表* @param index 無用* @return 無用*/public static int parsePage(NodeList nlist, int index) {Node pg = null;int count = nlist.size();for (int i = 0; i < count; i++) {pg = nlist.elementAt(i);if (pg instanceof LinkTag ) {LinkTag lt = (LinkTag)pg;if (lt.getLinkText().equalsIgnoreCase("下一頁")) {try {test.HandleText(new String(test.GetContent("http://blog.csdn.net"+lt.extractLink(), 1)), 2);} catch (Exception e) {//追加出錯日誌}}}}return index;}/** @param nlist HTML作者資訊標籤的子標籤鏈表* @param index 無用* @return 無用*/public static int parseAuthor(NodeList nlist, int index) {Node aut = null;int count = nlist.size();for (int i = 0; i < count; i++) {aut = nlist.elementAt(i);if (aut instanceof LinkTag ) {LinkTag link = (LinkTag)aut;resourceList.add(new Attribute("author", link.getLinkText()));} else {NodeList slist = aut.getChildren();if (slist != null && slist.size() > 0) {index = test.parseAuthor(slist, index);}}}return index;}/** @param input 輸入的html文檔字串* @param skip 是否執行的類別* @return 匹配的鏈表,很多類別通過副作用而起作用*/public static  NodeList HandleText(String input, final int skip)  throws  Exception {Parser parser = Parser.createParser(input,  "UTF-8" );NodeList nodes = parser.extractAllNodesThatMatch(new  NodeFilter() {public boolean accept(Node node) {if (node instanceof Div) {Div dv = (Div)node;NodeList nlist = dv.getChildren();if(dv.getAttribute("id") != null && nlist != null) {if(dv.getAttribute("id").equalsIgnoreCase("article_content")  && skip == 3) {parseImg(nlist, 0);return   true ;} else if (dv.getAttribute("id").equalsIgnoreCase("article_details") && skip == 3) {parseTitle(nlist, 0);} else if (dv.getAttribute("id").equalsIgnoreCase("archive_list") && (skip == 1 || skip == 4)) {parseMonthArticle(nlist, 0);} else if (dv.getAttribute("id").equalsIgnoreCase("papelist") && skip == 2) {parsePage(nlist, 0);} else if (dv.getAttribute("id").equalsIgnoreCase("blog_title") && skip == 4) {parseAuthor(nlist, 0);}}if (dv.getAttribute("class") != null && nlist != null) {if (dv.getAttribute("class").equalsIgnoreCase("article_title") && skip == 2) {parsePerArticle(nlist, 0);}}}  return false;}});return nodes;}/** @param filepath 本地存檔的路徑* @param url 儲存本月存檔的網頁的URL* @param articles 儲存本月存檔的鏈表* @return 無*/public static void parseMonth(String filepath, String url, AttributeList articles) {List<Attribute> li = articleList.asList();try  {HandleText(new String(GetContent(url, 1)), 2);}catch (Exception e){//追加出錯日誌}test.MKDir(filepath);for (int i = 0; i < li.size(); i++) {HandleHtml(filepath, (String)li.get(i).getValue(), articles);try {/* 慢一點,否則會被認為是惡意行為 */Thread.sleep(500);} catch (Exception e) {}}articleList.clear();}/** @param url blog入口文章的URL* @return 無*/public static void parseAll(String url) {try  { String author = null;HandleText(new String(GetContent(url, 1)), 4); author = (String)((List<Attribute>)resourceList.asList()).get(0).getValue();resourceList.clear();test.MKDir(author);List<Attribute> li = indexList.asList();for (int i = 0; i < li.size(); i++) {                         AttributeList articles = new AttributeList();monthList.add(new Attribute(li.get(i).getName(), articles));parseMonth(author + "/" + li.get(i).getName(), (String)li.get(i).getValue(), articles);                     }HandleIndex(author);}catch (Exception e){e.printStackTrace();}indexList.clear();}/** @param dir 本地存檔根路徑名稱* @return 無*/static void HandleIndex(String dir) {try  {index_handle = new OutputStreamWriter(new FileOutputStream(dir + "/index.html"), "GB18030");String header = "<html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"><title>CSDN文章歸檔</title></head><body bgcolor=\"white\" text=\"black\" link=\"#0000FF\" vlink=\"#840084\" alink=\"#0000FF\"><hr></div><div><h1 class=\"title\"><a name=\"id2747881\"></a>"+dir+"CSDN文章歸檔</h1></div></div><hr></div><div class=\"toc\"><p><b>目錄</b></p><dl><dt><span class=\"preface\"><a href=\"preface.html\">摘要</a></span></dt>";String tailer = "</div></div><hr></body></html>" ;index_handle.write(header);                                List<Attribute> li = monthList.asList();for (int i = 0; i < li.size(); i++) {String mindex = "<dt><span class=\"part\"><h4>"+li.get(i).getName()+"</span></dt><dd><dl>";AttributeList articles = (AttributeList)li.get(i).getValue();List<Attribute> al = articles.asList();index_handle.write(mindex);for (int j = 0; j < al.size(); j++) {String per = "<dt><span class=\"part\"><a href=\""+al.get(j).getName()+".html\">"+al.get(j).getValue()+"</a></span></dt>";index_handle.write(per);}index_handle.write("</dl></dd>");                                    }index_handle.write(tailer);index_handle.close();}catch (Exception e){ }}/** @param args args[0]:blog入口文章的URL args[1]:Proxy 位址 args[2]:代理連接埠 【用法:java DownBlog http://blog.csdn.net/dog250 192.168.40.199 808】* @return 無*/public static void main(String[] args)  throws  Exception {parseAll("http://blog.csdn.net/dog250");/*boolean valid = false;if (args.length == 1) {valid = true;} else if (args.length == 2) {proxy_addr = args[1];valid = true;} else if (args.length == 3) {proxy_addr = args[1];proxy_port = Integer.parseInt(args[2]);valid = true;} if (valid) {parseAll(args[0]);} else {}*/}}


那麼,如果使用上面的代碼備份你自己的CSDN部落格呢?很簡單,將dog250改成你的ID即可,我在main方法中注釋了一大段的內容,你也可以將其展開,然後他就是通用的了,試試看。


 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.