备份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方法中注释了一大段的内容,你也可以将其展开,然后他就是通用的了,试试看。
转载于:https://blog.51cto.com/dog250/1269013
备份CSDN博客正文到本地存档相关推荐
- Python备份CSDN博客
用Python实现备份CSDN博客: 功能:备份输入user的所有博文到当前目录下的user文件夹内(html格式) 不足:暂时还没开多线程,并且反盗链之类还没有处理(虽然转载的文章一般都是保存没反盗 ...
- python+shell 备份 CSDN 博客文章,CSDN博客备份工具
python+shell 备份 CSDN 博客文章,CSDN博客备份工具 在 csdn 写了几年的博客了.多少也积累了两三百篇博文,近日,想把自己的这些文章全部备份下来,于是开始寻找解决方案. 我找到 ...
- 【工具】复制别人的CSDN博客文章到本地
复制别人的CSDN博客文章到本地 操作流程 1.打开自己喜欢的博客的文章,然后同时按shift+ctrl+I三个键,打开开发者工具. 2.选择elements选项,按下面图片选择(chorme浏览器) ...
- Python备份CSDN博客的完整页面
在CSDN论坛发现了一个求助帖,帮忙修改一个备份CSDN博客的Python脚本,应该是运行不了的代码.代码的时间比较久远,而且是用python2写的,所以我并没有尝试运行就直接用Python3来进行改 ...
- 备份 CSDN 博客(上)
文章目录 背景 思路解析 如何获得每篇文章的 URL urllib.request HTML 的元素构成 BeautifulSoup 根据标签和属性识别 根据标签和内容识别 其他操作 代码 参考资料 ...
- CSDN博客导出备份工具
写的文章太多,虽然都不是什么有深度的文章,大多只是做做笔记,但还是担心,有一天博客被误删了怎么办,所以上网找,碰巧找到了这样一个csdn博客导出工具: [csdn博客文章]导出备份 貌似有的用户反馈没 ...
- python csdn博客_GitHub - 1783955902/CSDNBlogBackup: Python实现CSDN博客的完整备份
Python实现CSDN博客的完美备份 出发点 之所以造这个轮子无非是现有的轮子不好使,CSDN官网是推出的博客备份在系统中读不到博客数据,打开后还会闪退,其他人写的工具,要么是收费,要么只是对网页的 ...
- 自己动手编写CSDN博客备份工具-blogspider之源码分析(3)
作者:gzshun. 原创作品,转载请标明出处! 来源:http://blog.csdn.net/gzshun 周星驰:剪头发不应该看别人怎么剪就发神经跟流行,要配合啊!你看你的发型,完全不配合你的脸 ...
- CSDN博客 专用备份工具
CSDN博客 专用备份工具 用要的朋友可下载. 本程序为个人所用,仅供学习. 作者:潇湘博客 网站:http://blog.csdn.net/fkedwgwy 默认文件存放位置为用户名文件夹下,也可以 ...
最新文章
- java应用中spring自动注入_java-Spring Security会自动过滤注入吗?
- 【原】时势造英雄 Times makes heros, while heros shape the times!
- Python 技术篇-使用pygame库播放音乐没有声音问题解决办法
- JavaScript对UNIX时间戳的转换
- 数据结构之树状数组(候补)
- pytorch入门-简介及安装
- 计算机专业考试知识点,2016计算机专业知识:精选知识点练习(126)
- 2018春考计算机技能考试题目,2018年山东省春季高考技能考试信息技术类专业考试(样题).PDF...
- Redis面试题及分布式集群
- Star Schema完全参考手册学习笔记九
- ndk android studio万年坑
- 聊题“谈、闲、想、省”
- 2017年最受欢迎的10个编程挑战网站,值得收藏!
- wxpython学习笔记
- abaqus算出来的转角单位是什么_ABAQUS中的单位制是如何规定的;
- python py转exe逆向
- dell主板恢复出厂设置_DELL(戴尔)BIOS进入及恢复默认值
- 国外硕博论文下载网址资源
- mysql数据库安全开关_对MySQL数据库的安全进行的详述
- 网络词汇泛滥:神马都是浮云?