备份CSDN博客正文到本地存档

晨曦之光 发布于 2012/04/10 14:56
阅读 156
收藏 0
大哥有了新想法,然而没有技术,令人欣慰的是大哥想到了我,于是我便答应免费帮个忙,这是一个基于云的项目,具体细节也就不透露了,然而在实现的过程中,其中一个模块我觉得可以自用,于是我就想把这个模块抽出来,该模块的功能就是将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]:代理地址 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方法中注释了一大段的内容,你也可以将其展开,然后他就是通用的了,试试看。


 



原文链接:http://blog.csdn.net/dog250/article/details/7234170
加载中
返回顶部
顶部