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, 所以请大家指出!

0 0
原创粉丝点击