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

简介:
大哥有了新想法,然而没有技术,令人欣慰的是大哥想到了我,于是我便答应免费帮个忙,这是一个基于云的项目,具体细节也就不透露了,然而在实现的过程中,其中一个模块我觉得可以自用,于是我就想把这个模块抽出来,该模块的功能就是将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方法中注释了一大段的内容,你也可以将其展开,然后他就是通用的了,试试看。


 

 本文转自 dog250 51CTO博客,原文链接:http://blog.51cto.com/dog250/1269013


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
5天前
百度文库的内容怎么复制
百度文库的内容怎么复制
17 0
|
5天前
|
PHP
菊子曰备份Wordpress网站的博客内容出错的解决办法(原创)
菊子曰备份Wordpress网站的博客内容出错的解决办法(原创)
10 2
|
5天前
|
数据可视化 定位技术
全球卫星信息数据下载网站整理
全球卫星信息数据下载网站整理
181 1
|
6月前
AXURE怎么找回丢失的文件
AXURE怎么找回丢失的文件
|
缓存
Discuz!论坛如何去除隐藏文章内容图片鼠标经过时显示“下载附件”等信息解决方法本文来自:XM技术学习分享,原地址:http://xmwl.cc/mb/41.html
在discuz!系统中发帖上传图片,鼠标经过的时候会显示一个小菜单,显示图片的基本信息和下载链接,有些站长觉得每次鼠标经过的时候弹出这个体验不好希望去掉!本文来自:XM技术学习分享,原地址:http://xmwl.cc/mb/41.html
672 0
快速恢复10月20日谷歌翻译
谷歌翻译一般指Google翻译。 Google 翻译是谷歌公司提供一项免费的翻译服务,可提供109 种语言之间的即时翻译,支持任意两种语言之间的字词、句子和网页翻译。
快速恢复10月20日谷歌翻译
|
SQL 安全 前端开发
百度快照被改成博彩内容的解决办法
那如何确定网站是否被快照劫持?SINE安全老于给大家详细的介绍一下,首先可以打开百度站长工具,看下近期的收录是否异常,像收录突然猛增,百度的蜘蛛抓取次数是否多了很多,再一个看下site:www.***.com自己的网站,看最近一个月的收录,是否收录大量的世界杯体育,菠菜、QP等恶意内容的百度快照。再一个快照劫持的特征是,直接访问网站是不会出现跳转,从百度点击进入网站,会直接跳转到违规网站上去。我们sinesafe以实际的客户案例给大家看下,像下面这种收录内容,基本上就是网站快照被劫持了,也可以说是网站被黑客攻击了。
184 0
百度快照被改成博彩内容的解决办法
|
存储 安全 数据安全/隐私保护
电子邮件存档与备份的关键区别
任何电子邮件归档解决方案的主要目标是确保电子邮件数据随着时间的推移保持可用和可检索。对于公司而言,这对于与业务交易的准备、完成、执行或撤销有关的电子邮件(例如发票、报价、支持查询或任命请求)尤其重要。 相比之下,备份的目的是在短期和中期存储数据,从而提供有关数据的常规快照。此概念允许制作整个数据集的副本,以便可以将它们放到外部存储或云环境中,并在以后还原。
767 0
电子邮件存档与备份的关键区别
|
NoSQL 数据安全/隐私保护 对象存储
Fundebug是这样备份数据的
数据还是要备份的,万一删库了呢?
1683 0
|
存储
Confluence 6 从一个备份中获得文件附件
页面中的文件附件可以从备份中获得而不需要将备份文件导入到 Confluence 中。这个在用户删掉了附件,但是你还是想恢复这个附件的时候就变得非常有用了。
1053 0