jStyleParser介绍

最近在找一款能够解析CSS的JAVA包, 找了几个都不理想, 例如jsoup, 可以用jQuery一样的语法浏览DOM节点, 但似乎没有解析CSS的功能, CSSParser不大好用, 而且很旧了. 最后发现的是jStyleParser, 相当好用, 能够很好的解决问题, 本文将会对这个包做一个初步的介绍.

之前用java写过一个爬虫可以深度或广度的遍历网页, 同时可以下载网页上的图片等资源, 但是用处不大, 一旦启动他就会抓取看见的一切, 并且只是简单的抓取HTML文本, 最多用正则表达式提取一些超链接.

这次的一个想法是提取网页的指定部分, 并且计算每一个元素的样式, 将样式写到元素的style属性中, 事实上做出来的结果是一个保存网页的好工具, 可以做到只保留网页的正文, 保留下来的文档在浏览器中打开和在源网站上效果几乎一样, 主要的障碍是一些使用了javascript脚本做语法高亮的部分会失效.

下图是包里面的内容

简单点来说jStyleParser是基于ANTLR语法分析器的CSS解析器, 定义了CSS的文法, 词法分析器, 语法分析器, CSSParser.java是核心, 也是项目中最大的一个文件, 代码则达到了1万行. 这个分析器可以创建CSS文件的语法树, 可以将CSS选择符和DOM树中的节点对应起来, 可以将CSS样式表应用到一个DOM对象上. 这里只简介一下其语法树的结构和一个简单应用.

项目中有4个包, cz.vutbr.web.css定义了节点类的接口, cz.vutbr.web.csskit则实现了这些接口,cz.vutbr.web.csskit.antlr则是分析器, 还有一个cz.vutbr.web.domassign负责将样式和DOM节点对应起来, 给出一个Element对象,可以通过系统的API得到该元素的样式.

语法树由节点构成, 主要有以下几种类型的节点:StyleSheet表示整个CSS文件, 也是语法树的根, RuleSet表示一个CSS块, 就是由{}所表示的一块CSS声明, CombinedSelector表示一条CSS选择符, Selector表示选择符的单个组成部分,例如div p, 表示div里面的p标签,这里面有两个Selector, SelectorPart, 是比Selector还小的单元, 例如伪类, 如果一个Selector只包含一个id或者类名那么SelectorPart和Selector包含相同的内容, 如果Selector包含伪类, SelectorPart则是将Selector拆分后的结果, SelectorPart应该算是叶节点.

用来表示CSS属性的是Declaration节点, 这种节点也是RuleSet的子节点, 一个Declaration有属性名和属性值组成, 属性值是一个Term节点的List列表, Term节点种类最多, 可以是颜色值, 像素值, 字体名等等.

domassign包中的Analyzer.java实际实现了对语法树的遍历, 不过比较有用的是matchSelector函数, 函数接受一个CombineSelector对象和一个Element对象, 然后返回这个CSS选择符是否和这个元素匹配. 有了这个东西可以方便的实现用jQuery语法来定位DOM节点. 我们可以手动的构造一个CombineSelector对象, 然后遍历DOM的每一个节点, 查看该节点是否匹配给定的选择符.

        StyleSheet ss;
        RuleSet rs;
        CombinedSelector cs null;
        try {
            ss CSSFactory.parse("#main div.post-content {}");
            rs = (RuleSet)ss.get(0);
            cs = (CombinedSelectorrs.getSelectors().get(0);
        catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        catch (CSSException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

上面的代码中CSSFactory.parse会生成语法树, 得到StyleSheet的第一个子节点, 然后再得到这个子节点的第一个CombineSelector节点, 这个节点就表示#main div.post-content这个选择符. 寻找节点的方法可能像这样:

        TreeWalker w traversal.createTreeWalker(docNodeFilter.SHOW_ELEMENTnullfalse);        
        Element current;
        while ((current = (Elementw.nextNode()) != null) {
            // nextNode的顺序是深度优先

            if(Analyzer.matchSelector(cscurrentw)) {

            }
        }

那么如何用他来更加智能的保存网页呢? 思路如下: 下载网页, 解析DOM树, 解析CSS, 定义包含正文的元素的CSS选择符, 遍历DOM节点, 当碰到那个选择符指定的元素的时候将其中的所有元素的样式, 包括定义的和继承的全部计算出来并写到其style属性中, 这里可能产生大量的冗余数据, 但是为了保证外观的完整性, 似乎不得不这么做. 可选的动作是遍历正文中的每一个img标签, 下载其图片保存到本地, 并改写其src属性. 另一个可选的动作是过滤广告, 例如Adsense, 下面是过滤Adsense的方法

                if(current.getTagName().toLowerCase().equals("script")) {
                    msg("脚本内容" current.getTextContent());
                    if(current.getTextContent().indexOf("google_ad_client") != -1) {
                        current.setTextContent("");
                    }
                    if(current.getAttribute("src").indexOf("google") != -&& current.getAttribute("src").indexOf("show_ads.js") != -1) {
                        // 想直接删除这个节点, how?
                        current.removeAttribute("src");
                    }
                }