03 crawler
来源:互联网 发布:新西兰旅游 知乎 编辑:程序博客网 时间:2024/06/05 10:38
[… 卧槽 这mdeditor, 编辑了一下另一个播客, 然后现在的这篇播客就不加了, 又要重写?? oh no !!]
这是仿照之前做爬虫的时候, 还是因为公司的东西不能带出来嘛, 然后自己模仿着实现了一个, 这个是一个json格式的xpath模板, 利用xpath来抓取需要的数据
涉及的知识点 : httpClient + tagSoup + dom4j + json
httpClient : 获取网络中的数据
tagSoup : 规范化xml数据
dom4j : 解析xml, 解析xpath
json : 工具数据结构
模板规则
索引字符串 中可能的键为name, xpath, attribute, values
name : 用作JSONObject 或者JSONArray的键
xpath : 主要用于xml的导航
attribute : 属性结点, 对应于一个JSONObject中的一个数据[数组结点例外[后面有介绍] ], 如果给定的xpath能够获取到多个结点, 那么只获取第一个结点, attribute 可能的值为 “:index”, “text”, “innertext”, “innerhtml”, “outerhtml”, 以及当前结点的其他的属性名称, attribute结点的xpath可有可无
:index : 表示, 当前结点在values中所有结点中的索引, 从1开始
text : 表示当前结点内的文字
innertext : 表示递归当前结点的所有结点的文字
innerhtml : 表示递归当前结点的所有结点的标签以及文字
outerhtml : 表示在innerhtml的基础上面 添加一个当前结点的标签
values : 多个值的结点, 对应于一个JSONArray[包含多个JSONObject, 或者多个JSONArray], values中的多个模板JSONObject的每一个对应的数据, 并封装成一个JSONObject, name作为key, attribute 或者values作为值, values结点的xpath必需存在
根据索引字符串构建抓取树
1 索引字符串[素材来自于某电影网站]
2 构建抓取树
3 更新每一个结点的xpath [基于父节点]
其实在实际过程中, 构建抓取树之后, 则从抓取树的根节点开始向下遍历, 进行配合给定的xml文档构建的dom树进行抓取数据 , 不过 这里为了便于理解, 表面化[, 形象化]了而已
抓取模板到抓取结果 [xpath 用于xml导航]
代码托管在gitHub上面, 这里就不展示了
gitHub 地址 : https://github.com/970655147/HXCrawler
这里 只介绍关于对于使用抓取数据模板解析抓取源的这一部分, 至于使用httpClient发送请求, 获取页面那些, 不过是一层对于httpClient的封装, 这里就不赘述了
接下来 会介绍两个使用的实例 [小应用]
======================= add at 2015.10.25 =======================
今天 没事的时候, 想爬爬csdn的那些资料, 以及播客, 但是资料下载浏览页, 和播客浏览页的页面的版型是不一样的, 所以这里 就需要两个xpath来抓取数据
category -> pageList -> infoList [下载资料列表 / 播客列表]
我思考了一两种方式
方式一 : 对于两种页面编写两个脚本, 在进入productList脚本之前进行分支判定, 不同的任务跳转到不同的脚本, 但是这两个脚本的区别仅仅在于xpath不同, 因为其他的处理业务是一致的, 所以 多了一个脚本, 并且多了许多重复的内容
方式二 : 在一个脚本中输入两个xpath, 如果第一个xpath没有获取到结果, 则使用第二个xpath, 这样 虽然多了一个xpath[并且 在第二种xpath版型的页面, 查询xpath两次], 但是却没了重复的脚本, 而且不用编写分支逻辑
对于 业务来说, 今天增加了一个比较重要的方法”getResultByXPathes”, 也就是通过多个xpath 来获取结果, 获取到的结果是否符合要求由judger决定
在之前的使用中, 使用多个xpath来尝试查询结果我是放在脚本逻辑中的, 今天思考了一下, 改进了一下, 增加了这个方法
Tools. getResultByXPathes(String html, String url, String[] xpathes, ResultJudger judger) : 使用多个xpath来获取给定页面的数据, judger判定给定的结果是否合法
Tools. getResultByXPathes(String html, String url, String[] xpathes, ResultJudger judger)
public static JSONArray getResultByXPathes(String html, String url, String[] xpathes, ResultJudger judger) throws Exception { for(int i=0; i<xpathes.length; i++) { JSONArray res = getResultByXPath(html, url, xpathes[i]); if(! judger.isResultNull(i, res)) { return res; } } return null; }
ResultJudger : 判断给定的结果是否符合条件
// 结果判断接口public interface ResultJudger { // 判断给定的结果是否为空 public boolean isResultNull(int idx, JSONArray fetchedData);}
对了, 本来是想吧这个放在github上面的, 但是 想了一下, 这个又不经过很正规的编写流程, 测试啊什么的, 还是算了, 将其作为一个完成的小项目吧
======================= add at 2016.03.25 =======================
先简单的说一下增加的特性
1. 对于属性数组的数据的抓取[可能现在看会有些懵懂, 但是 一会儿举一个例子就好了]
2. 对于某些可配置的数据提供了setter接口[以前没有写,, 是因为个人没有这个需求..], 以及对于打印日志的控制[默认使用LOG_ON_MINE_CONF, 打印所有的日志, 提供setter]
3. 提供缓冲相关API[创建缓冲, 关联文件, 写出数据, 关闭缓冲等等]
4. handler相关特性, 也是最大的一个feature, 可以使用”map“, “filter“直接写在索引字符串中, 然后对结果进行处理
feature 1 : 试想如下场景, 现在需要抓取以下页面的所有的博客名字
那么, 如果使用以前的方法, 则需要写如下xpath
[ { "name":"blogs","xpath":"//div[@id='article_list']/div[@class='list_itemarticle_item']", "values":[ { "name":"blogName", "xpath":".//span[@class='link_title']/a", "attribute":"text" } ] }]
解释一下 : 1. 首先blogs一级的xpath需要写到各个blogName对应的结点的’最近公共结点’的’下一级结点’, 这里的每一个’下一级结点’是各个’blogName结点’对应的结点的’父节点’
2. 然后由values抓取到多个’父节点’进行处理, 对于每一个’父节点’, 使用values结点的子节点来处理’父节点’, 这里 是使用xpath导航到各个’播客名称结点’, 然后提取text属性, 即为所求
1) 找到’最低公共父节点’
2) 找到需要抓取的元素
但是, 现在, 使用这个’属性数组’的特性的话, 我们就只需要如下xpath了
[ { "xpath":".//span[@class='link_title']/a", "attribute":"text" }]
注意到与上面的xpath的区别了么, 对 没有’name’属性, 这就是与’普通Attribute结点’区分的地方
意思在于, 获取当前文档下面的所有的”//span[@class=’link_title’]/a”结点的text属性, 存放在结果数组中
注意 : ‘属性数组’ 和’普通的Attribute结点’, ‘Values结点’不兼容
所以 在同一个’Values结点’中, 只能存在’属性数组’ 或者是(‘普通的Attribute结点’, ‘Values结点’)
组合使用的xpath示例
root结点下面, 一个xpath抓取播客的相关信息, 使用’属性数组’, 另外一个抓取评论的相关信息, 使用’普通的Attribute结点’ 和’Values结点’配合
[ { "name":"blogs", "xpath":"//blogsXPath", "values":[ { "xpath":".//blogNameXpath", "attribute":"text" }, { "xpath":".//dateXpath", "attribute":"text" } ] }, { "name":"comments", "xpath":"//commentsXpath", "values":[ { "name":"commentAuthor", "xpath":".//authorXpath", "attribute":"text" }, { "name":"commentContents", "xpath":".//contentsXPath", "attribute":"text" }, { "name":"goodVisitor", "xpath":".//visitorXpath", "values":[ // 略去好评的人的相关部分 ... ] } ] }]
feature 2 : 提供配置各个可配置的参数就不多说了, 主要说一下关于日志标志位的配置
当然 这个也没有什么太大的说头,, 直接一个例子说明一切, 普通用户直接根据自己的偏好配置一下Tools.LOG_ON_MINE即可
feature 3 : 这个主要是提供了一个缓冲映射, < buffName, strBuffer >, 常用于输出爬取到的数据[缓冲的持久化由框架维护],, 但是注意使用完之后关闭buffer, 节省资源
同样直接一个例子说明一切,
feature 4 : 这个特性是最近三天添加的, 使用起来还是挺方便的, 主要是实现了一系列的字符串处理的方法, 两个用户使用的Operation : map, filter
多个 用户可以使用的函数 : concate(‘str’, ‘str’, …), replace(‘regex’, ‘replacement’), subString(‘from’ [, ‘to’]), indexOf(‘idxStr’ [, ‘from’]), lastIndexOf(‘idxStr’ [, ‘to’]), equals(‘str’), match(‘regex’), contains(‘str’), trim, trimAll, trimAsOne, toUpperCase, toLowerCase, length, gt[>], lt[<], get[>=], let[<=], eq[==], neq[!=], not[!], and[&&], or[||], add / sub / mul / div / mod(‘Int’, ‘Int’, …), 条件运算符condExp[isTrue ? truePart : falsePart], toInt, toBoolean
没有参数的函数 : [toUpperCase, trimAsOne, toLowerCase, length, trimAll, toBool, trim, toInt, doNothing]
这些操作, 默认的操作符是处理结果的字符串对象, 当然用户也可以自己传入参数, 如下示例
比如说, 可以使用”map(length )” 也可以使用”map(length(‘abc’) )”, 前者表示对当前结果使用length操作, 后者表示对于’abc’使用length操作
一个字符串参数的函数 : [matches, get, let, equals, gt, contains, neq, lt, eq]
这些操作, 默认的操作符是处理结果的字符串对象, 当然用户也可以自己传入参数, 如下示例
比如说, 可以使用”map(equals(‘abc’) )” 也可以使用”map(equals(‘abc’, ‘df’) )”, 前者表示对当前结果 和’abc’使用equals操作, 后者表示对于’abc’ 和’df’使用equals操作
这四个操作 : replace, subString, indexOf, lastIndexOf默认的操作对象是处理结果的字符串对象
其他操作 : 操作符 则由用户传入
注意
1. 对于结果为Int, Boolean的函数, 在其之后, 不能再叠加操作, 否则 会报错
2. 对于各个函数的参数, 需要传入正确的参数类型, Int, Boolean 可以向String转换, 但是反之不行
3. $this 表示当前处理结果
对了, 对于handler在于xpath中的使用有必要说明一下
1. 使用map, filter关键字配置mapHanlder, filterHandler. filterHandler的参数类型必须为Boolean
2. 配置handler字符串的时候, 第一个字符为’+’或者 ‘-‘, ‘+’表示在父节点的Handler基础上面叠加上当前指定的Handler, ‘-‘表示重写Handler, 抛弃父节点的Handler
3. 使用mapHandler的时候, 使用map叠加各个逻辑比如 : “+map(trimAsOne() ).map(toUpperCase.toLowerCase )”, 使用filterHandler, 则使用filter进行处理[因为filter的操作数为Boolean, 所以filter操作不能叠加[?] ]
这部分知识主要涉及 : 1.分词, 2. 语法分析, 3. 逻辑的校验, 4. 生成AttrHandler[适配器模式, 组合模式, 等等]
这里 虽然说得轻描淡写, 但是 实际上涉及了很多东西
接下来便使用一个实例来说明如何使用
1. ‘普通的Attribute结点’ 或者’数组属性结点’ 获取各个播客名字
2. (‘普通的Attribute结点’ 配合’Values结点’) 或者’数组属性结点’ 获取各个播客名字, 日期, 访问数
3. (‘普通的Attribute结点’ 配合’Values结点’) 或者’数组属性结点’ 获取各个播客名字, 日期, 访问数, 并分别处理
测试代码 :
/** * file name : Test17CrawlForCSDNBlogInfoByAttrArray.java * created at : 8:24:34 PM Mar 25, 2016 * created by 970655147 */package com.hx.crawler;public class Test17CrawlForCSDNBlogInfoByAttrArray { // 爬取csdn的个人播客的相关信息 public static void main(String[] args) throws Exception { String userId = "u011039332"; String categoryId = null;// String categoryId = "5721351"; getCSDNPostsInfo(userId, categoryId); } // 获取给定的用户的所有的帖子相关的信息 // categoryId为null表示查询该用户所有的播客 public static void getCSDNPostsInfo(String userId, String categoryId) throws Exception { String requestUrlTemplate = "http://blog.csdn.net/%s/article/list/%d"; Object[] args = new Object[]{userId, 1 }; if(categoryId != null) { requestUrlTemplate = "http://blog.csdn.net/%s/article/category/%s/%d"; args = new Object[]{userId, categoryId, 1 }; } String requestUrl = String.format(requestUrlTemplate, args); Crawler crawler = HtmlCrawler.newInstance(); // 获取博客名的xpath// String postsInfoXPathForBlogName = "{\"name\":\"blogs\",\"xpath\":\"//div[@id='article_list']/div[@class='list_item article_item']\",\"values\":[{\"name\":\"blogName\",\"xpath\":\".//span[@class='link_title']/a\",\"attribute\":\"text\"}]}"; // 获取博客的blogName, date, commentsNum的xpath// String postsInfoXPathForAll = "{\"name\":\"products\",\"xpath\":\"//div[@id='article_list']/div[@class='list_item article_item']\",\"values\":[{\"name\":\"title\",\"xpath\":\".//span[@class='link_title']/a\",\"attribute\":\"text\"},{\"name\":\"date\",\"xpath\":\".//span[@class='link_postdate']\",\"attribute\":\"text\"},{\"name\":\"view\",\"xpath\":\".//span[@class='link_view']\",\"attribute\":\"text\"}]}"; // normal // 使用AttrArray属性结点 获取blogName, date, commentsNum// String postsInfoXPathForBlogName02 = "{\"xpath\":\".//span[@class='link_title']/a\",\"attribute\":\"text\"}";// String postsInfoXPath02ForDate02 = "{\"xpath\":\".//span[@class='link_postdate']\",\"attribute\":\"text\"}";// String postsInfoXPath03ForComments02 = "{\"xpath\":\".//span[@class='link_view']\",\"attribute\":\"text\"}"; // handler // 使用AttrArray属性结点 获取blogName, date, commentsNum // 并使用handler对blogName简化名字, 将date的'-'替换为':', 去掉commentsNum的两边的括号 String postsInfoXPathForBlogName03 = "{\"xpath\":\".//span[@class='link_title']/a\",\"attribute\":\"text\",\"map\":\"+map(trimAsOne).map(length>10?subString(0,10)+'...':$this)\"}"; String postsInfoXPath02ForDate03 = "{\"xpath\":\".//span[@class='link_postdate']\",\"attribute\":\"text\",\"map\":\"+map(replace('-',':'))\"}"; String postsInfoXPath03ForComments03 = "{\"xpath\":\".//span[@class='link_view']\",\"attribute\":\"text\",\"map\":\"+map(subString(add(indexOf('('), 1),indexOf(')')))\"}"; int pageSum = 1; if(pageSum < 0) { Log.log("the user : " + userId + " does not exists !"); } else { JSONObject res = new JSONObject(); for(int i=1; i<=pageSum; i++) { args[args.length-1] = i; requestUrl = String.format(requestUrlTemplate, args); // get for blogName// String xpath = Tools.getRealXPathByXPathObj(postsInfoXPathForBlogName);// String xpath = Tools.getRealXPathByXPathObj(postsInfoXPathForBlogName02 ); // get for all// String xpath = Tools.getRealXPathByXPathObj(postsInfoXPathForAll);// String xpath = Tools.getRealXPathByXPathObj(postsInfoXPathForBlogName02, postsInfoXPath02ForDate02, postsInfoXPath03ForComments02 ); String xpath = Tools.getRealXPathByXPathObj(postsInfoXPathForBlogName03, postsInfoXPath02ForDate03, postsInfoXPath03ForComments03 ); JSONArray pageNowRes = Tools.getResultByXPath(crawler.getPage(requestUrl).getContent(), requestUrl, xpath ); Tools.trimSpaces(pageNowRes); String pageN = "page-" + i; res.put(pageN, pageNowRes); Log.log("get data from " + pageN); } Log.log(res); } }}
啊啊啊啊, 整理了三个小时, 好累。。 闪人
======================= add at 2016.03.26 =======================
1. 将”mapHandler” 和”filterHandler”, 合并在为”handler”中配置, filter操作仅仅是校验当前结果是否满足filter条件, 满足filter条件则去掉当前结果, 否则 不做任何事情
2. 增加assert操作, 用于断言
示例 :
{ "name":"products", "xpath":"//div[@id='article_list']/div[@class='list_itemarticle_item']", "values":[ { "name":"title", "xpath":".//span[@class='link_title']/a", "attribute":"text", "handler":"+map(trimAsOne).map((length>15)?(subString(0,12)+'...'):$this).filter(length<15).assert(length==15).map($this+'abc').filter(contains('.abc'))" }, { "name":"date", "xpath":".//span[@class='link_postdate']", "attribute":"text" }, { "name":"view", "xpath":".//span[@class='link_view']", "attribute":"text", } ]}
解释一下
1. 首先trim空格
2. 然后将结果长度大于15的结果转换为”结果的前12个字符 + ‘…’”[长度为15]
3. 然后过滤掉结果长度小于15的结果
4. 然后现在确保所有的结果的长度为15, 否则报错
5. 然后将每一个结果映射为”结果 + ‘abc’”[长度为18]
6. 然后过滤掉包含”.abc”的字符[所有的trim之后结果长度大于15的结果都会被去掉, 以及trim之后长度为15 并包含’.abc’的结果去掉]
所以 最后剩下来的字符串为, trim之后长度为15, 并且不包含’.abc’的结果
这里的”13 gifGenerator”, 因为其长度为15, 并且没有’.abc’子串, 所以保留了下来
请注意下载依赖 :
httpClient + tagSoup + dom4j + json
下载链接 [包含图片, 源码] :
http://download.csdn.net/detail/u011039332/9152893
crawler.jar
注 : 因为作者的水平有限,必然可能出现一些bug, 所以请大家指出!
- 03 crawler
- Crawler
- 开源crawler
- Registry Crawler
- Web Crawler
- JAVA crawler
- Crawler Basic
- Web crawler
- python-crawler
- Heritrix Crawler vs. Nutch Crawler
- Crawler学习:3.Crawler Design
- Crawler学习:1.Overview of Crawler
- Web crawler作业报告
- crawler的关键技术
- Heritrix-- 开源crawler
- Nutch Crawler工作流程
- Spider与crawler不同点
- Nutch Crawler工作流程
- ocp-32
- 创建String对象过程的内存分配小结
- Add Two Numbers - leetcode002
- ocp-33
- 【因子算法】——求一个数的因子、质因子、求两个数的公因子
- 03 crawler
- Python基础
- 如何用tomcat配置虚拟目录,方法要详细明了
- Hibernate懒加载
- poj 3580 splay
- JAVA实现链表中倒数第K个节点问题(《剑指offer》)
- 《php和mysql web开发》笔记——第10章 使用MySQL数据库
- 函数冗余参数
- Non-negative Partial Sums(单调队列,好题)