JavaFX和可视化信息抽取

来源:互联网 发布:张维为 知乎 编辑:程序博客网 时间:2024/06/05 18:51

1、阅读说明

        本篇博客是非介绍类的,即,不含有关于JavaFX的基础介绍。博客主要描述实现可视化信息抽取时,如何利用JavaFX的WebView组件。仅介绍涉及到的JavaFX的内容,至于可视化信息抽取的算法是哪个,博客不进行介绍,提供的源码中有一个可视化信息抽取的演示Demo,Demo不涉及到核心算法,是基于定制规则进行抽取的。
        博客内容是我毕业设计(基于反馈学习的半结构化信息抽取研究及应用)可能涉及到的基础技术的准备和总结,技术准备的时间较短,同时心里着急找工作(真心着急。。。),里面可能有不正确的,如有发现,请指正,同时提醒,本博客内容只是作为技术实现的可行tips,千万不要作为标准样板,关于某些问题,如果您有发现新的解决方案,请您一定要告诉我一下,非常感谢!

2、主要解决的问题

  1. 如何获取WebView渲染后网页中元素的基本信息,比如x,y坐标、字体大小、字体颜色、背景色等等;
  2. 如果用TreeView绑定DOM树,同时实现右键TreeView时,查看元素的基本信息或者高亮指定元素;
  3. 如果直接通过右键浏览器界面来高亮点击的元素;
  4. 如何获取WebView的滚动条对象、滚动条的宽度和判断滚动条的方向;
  5. 如何在拖拽窗体、最大化窗体等改变窗口大小时,获取WebView中的元素坐标位置;
  6. 对于已生成的Document文档对象,如何在Java中监听网页中对应DOM树的结构的变化;
  7. 如何对JavaFX生成的DOM对象,进行XPath操作;
  8. Java如何执行WebView渲染网页中的按钮自动点击操作,比如“下一页”自动点击下一页,“百度一下”自动进行搜索。

3、尚未完全解决的问题

  • 网页随着滚动条滚动加载时,如何快速让网页滚动至网页底部,使其加载完全(因为滚动条不止滚动一次,即到底部以后,有可能还需要继续滚动)
  • WebView对象怎么具体识别水平滚动条和垂直滚动条(因为编程过程中,发现WebView有多个ScrollBar对象,有时候多达10多个,如何区分哪两个是有效的,暂时不会)
  • WebView.onScrollProperty().addListener((ChangeListener<? super EventHandler<? super ScrollEvent>>),这个addListener函数如何添加参数,因为<? super EventHandler<? super ScrollEvent>>是嵌套类型的,实现时,总是提示错误,当时解决了,撤销以后,隔天又忘了。

本文涉及到的JavaFX对应的JDK版本是JDK1.7

4、关于JavaFX的基本概念

        关于JavaFX的基本概念,比如,JavaFX UI组件的使用、布局相关、Web组件相关等等,可参考JAVAFX中文资料。这个网页相当于Oracle官方文档的翻译,里面有基础知识介绍,以及相关实现代码,非常推荐。对应英文版入口JavaFX: Getting Started with JavaFX 和 Java Platform, Standard Edition (Java SE) 8相关资料(含JavaFX),JavaFX 2.0的API文档。
       JavaFX是和DOM模型以及CSS紧密相结合的。尤其是WebView,获取渲染后的网页内容,一般都是通过执行JavaScript获取的。所以,学习或者利用JavaFX解决相关问题时,一定不要局限于窗体程序思维,一定要记住JavaFX和JavaScript的交互性。我刚开始接触JavaFX,就因为这个,想很多问题时就太死板,浪费很多时间。
    JavaFX事件模型是DOM2事件模型,DOM2事件模型有一个典型的特性,就是事件从“顶层开始捕获,直至目标元素,然后事件相应处理从目标元素冒泡到顶层”(推荐文档:JAVAFX-事件),如下图:

DOM2事件模型
图片引用来自:http://www.csdn123.com/html/topnews201408/5/11405.htm

    之所以要强调DOM2模型,是因为这个特性对于绑定有层次关系的元素结点时,不需要每个结点都进行绑定,只需要绑定根结点就可以了。比如,TreeView是一个典型的层次树形结构,给这个TreeView中的每个TreeItem绑定触发收起事件(collapsed event)时的特殊处理操作时,不需要每一个TreeItem都需要调用addEventHandler函数,而只需要对TreeView的root的TreeItem进行绑定就可以了,在事件里面通过(TreeItem)event.getTarget()来获取被点击的TreeItem或者通过event.getTreeItem()直接获取。参考代码片段如下,完整代码,请看最下面附件。

treeItemParent.addEventHandler(TreeItem.<DOMBox> branchCollapsedEvent(),        new EventHandler<TreeItem.TreeModificationEvent<DOMBox>>() {            @Override            public void handle(TreeItem.TreeModificationEvent<DOMBox> event) {                TreeItem<DOMBox> tCurrent = event.getTreeItem();                DOMBox domBox = (DOMBox) tCurrent.getValue();                if (!ElementTools.isSelfClose(domBox.getTagName())) {                    String strHtml = domBox.getSelfHtml() + "...</" + domBox.getTagName() + ">";                    domBox.setSelfHtml(strHtml);                    TreeItem<DOMBox> tNext = tCurrent.nextSibling();                    tNext.getParent().getChildren().remove(tNext);                }            }        });

    上述代码片段是实现html树结点展开后的收起功能,如下图:

展开后 -> 收起后

    treeItemParent就是TreeView的根TreeItem。DOMBox是TreeItem关联的数据类型,可以String等任意类型,TreeView在显示TreeItem时,是调用TreeItem关联的数据的toString()函数,本样例就是DOMBox的toString()函数,所以你可以对toString()进行重载来显示自己需要展示的东西
    强调JavaFX和CSS的交互性,是因为可以通过样式表查找元素,或者通过修改样式表来改变原始文档的展示效果。下述代码片段是通过CSS来查找ScrollBar的:

    /**     * Returns the vertical scrollbar of the webview.     *     * @param webView webview     * @return vertical scrollbar of the webview or {@code null} if no vertical     * scrollbar exists     */    private ScrollBar getVScrollBar(WebView webView) {        Set<Node> scrolls = webView.lookupAll(".scroll-bar");        for (Node scrollNode : scrolls) {            if (ScrollBar.class.isInstance(scrollNode)) {                ScrollBar scroll = (ScrollBar) scrollNode;                if (scroll.getOrientation() == Orientation.VERTICAL) {                    return scroll;                }            }        }        return null;    }

    通过WebView的lookup或者lookupAll函数来进行查找相关元素。
    以下是遍历元素结点样式表的代码片段,以ScrollBar为例。

List<CssMetaData<? extends Styleable, ?>> css = scroll.getCssMetaData();for (int i = 0; i < css.size(); i++) {    CssMetaData<? extends Styleable, ?> oneAttr = css.get(i);    System.out.print(oneAttr.getProperty() + ":" + ((CssMetaData<ScrollBar, ScrollBar>) oneAttr).getStyleableProperty(scroll).getValue() + "\t");}System.out.println(); 

    scroll是ScrollBar对象,其他具有getCssMetaData函数的对象均可如上述进行结点遍历,比如WebView对象。关于JavaFX控件的样式表,可以参考官方文档 JavaFX CSS Reference Guide
    关于JavaFX的布局元素的介绍,除了JAVAFX中文资料中的介绍,再推荐一篇文章,个人感觉很有帮助:JavaFX 2.0 Resizing of UI Controls。

5、问题具体解决方案

强烈推荐Stack OverFlow,我的解决方案好像都是在其中看的代码片段知道的。

5.1 如何获取WebView渲染后网页中元素的基本信息,比如x,y坐标、字体大小、字体颜色、背景色等等

    因为毕业设计的信息抽取是基于可视化模块后,进行抽取的,主要是采用VIPS算法进行Page Segment,就是对网页进行分块。VIPS算法进行分块时,需要用到元素的字体大小、背景色、坐标位置等等信息,VIPS的Github上有Java版具体实现vips_java,不过这个版本基于CSSBox实现的,CSSBox不能解析JavaScript,只能渲染纯Html+CSS的网页,所以有限制,所以才采用JavaFX对网页进行渲染解析,同时不得不使用JavaFX获取元素的这些基本信息。
    最初的想法是以为JavaFX会有单独存储CSS DOM的结构,不过最终查阅了一些文档,发现很有有说通过底层API访问CSS DOM的(最初都快疯了,以为需要阅读JavaFX的源码,然后改源码呢。。。),在查找的过程中,有人提及通过执行JavaScript代码来获取相应的属性,这才给了一个思路,而且以后的很多问题解决方案,也自然而然的往这方面考虑了。在将具体怎么获取元素的基本信息之前,先说一下在查找中发现的一个有趣的事情,是关于CSS的style的。就是样式表的种类:

  1. 浏览器默认样式,就是浏览器自身所带的样式表,因为添加样式时,你不可能将所有样式都添加进去,比如h1默认展示的样式,就是浏览器自身的。这个概念之前还真没有注意。
  2. .css文件中的样式
  3. 网页中style结点中定义的样式
  4. 标签内嵌的样式,就是元素中style属性中定义的样式

    JavaFX一般提供的接口获取的是第四种,即“标签内嵌的样式”,这个功能是满足不了我的需求的,所以需要借助JavaScript来获取渲染后的所有样式。
    通过JavaScript获取字体颜色等信息,可以使用document.defaultView.getComputedStyle来获取具体元素的信息,关于getComputedStyle的具体使用,可以百度,有很多相关介绍。以下是部分代码片段:

    // 计算对应元素的属性    public DOMBox getDOMBoxByNode(WebEngine wEngine, Element e){        JSObject obj_defaultView = (JSObject)webEngine.executeScript("document.defaultView");        JSObject obj_ComputedStyle = (JSObject)obj_defaultView.call("getComputedStyle", e,null);        JSObject obj = (JSObject)e;        String tag_name = e.getTagName().toLowerCase();        String strTemp = obj_ComputedStyle.getMember("font-size").toString();        float font_size = Float.parseFloat(strTemp.substring(0, strTemp.length()-2));        // 这个方法获取xy坐标时,有问题//      float width = (int)obj.getMember("offsetWidth");//      float height = (int)obj.getMember("offsetHeight");//      float x = (int)obj.getMember("offsetLeft");//      float y = (int)obj.getMember("offsetTop");        JSObject bounds = (JSObject) obj.call("getBoundingClientRect");        float right = Float.parseFloat(bounds.getMember("right").toString());        float top = Float.parseFloat(bounds.getMember("top").toString());        float bottom = Float.parseFloat(bounds.getMember("bottom").toString());        float left = Float.parseFloat(bounds.getMember("left").toString());        float width = right - left;        float height = bottom - top;        float x = left;        float y = top;        String font_color = obj_ComputedStyle.getMember("color").toString();        String background_color = obj_ComputedStyle.getMember("background-color").toString();        boolean is_link = tag_name.compareToIgnoreCase("a") == 0;        String strSelfHtml = ElementTools.getElementHtml(e);        return new DOMBox(e, tag_name, font_size, width, height, x, y, font_color, background_color, is_link, strSelfHtml);    }

    上述是获取元素的基本信息,并存储到DOMBox(我自定义的类)中。执行JavaScript代码是,通过WebEngine.executeScript方法进行执行的,它返回一个JSObject对象,这个对象,可以通过call来调用所属对象的方法,比如,通过JSObject obj_defaultView = (JSObject)webEngine.executeScript(“document.defaultView”);获取了文档对象,然后就可以通过调用JSObject obj_ComputedStyle = (JSObject)obj_defaultView.call(“getComputedStyle”, e,null);返回一个存储元素基本信息的对象,再进一步通过结果的getMember方法获取具体的属性值。注意:元素的x,y坐标值没有通过getComputedStyle获取,因为其对部分元素会返回auto值,具体可以查相关文档,为了解决这个问题,通过调用getBoundingClientRect方法来进行获取。推荐参考文档:【CSS进阶】原生JS getComputedStyle等方法解析

5.2 如果用TreeView绑定DOM树,同时实现右键TreeView时,查看元素的基本信息或者高亮指定元素

    如果需要右键点击TreeView弹出菜单,则需要实现TreeItem对应的TreeCell元素,因为TreeItem本身是不接受鼠标类事件的,比如,鼠标移动、鼠标点击等等。实现这个功能,可参考JAVAFX中文资料中的使用JavaFX UI组件 -> 树视图(TREE VIEW)。下面是自己实现的主要代码片段:

rightTreeView.setCellFactory(new Callback<TreeView<DOMBox>, TreeCell<DOMBox>>(){            @Override            public TreeCell<DOMBox> call(TreeView<DOMBox> para){                return new DOMTreeCellImpl();            }        });
    private final class DOMTreeCellImpl extends TreeCell<DOMBox>{        private final ContextMenu addMenu = new ContextMenu();        public DOMTreeCellImpl(){            MenuItem addMenuItem1 = new MenuItem("查看信息");            MenuItem addMenuItem2 = new MenuItem("高亮元素");            addMenu.getItems().add(addMenuItem1);            addMenu.getItems().add(addMenuItem2);            addMenuItem1.setOnAction(new EventHandler<ActionEvent>(){                public void handle(ActionEvent t) {                    // 以对话框的形式,弹出结点的基本信息                    TreeItem<DOMBox> treeItem = getTreeItem();                    Label lblInfo = new Label(treeItem.getValue().getDetailInfo());                    BorderPane pane = new BorderPane();                    pane.setCenter(lblInfo);                    pane.setPadding(new Insets(30, 0, 0, 0));                    BorderPane.setAlignment(lblInfo, Pos.TOP_CENTER);                    Stage secondWindow=new Stage();                    Scene scene=new Scene(pane,300,275);                    secondWindow.setTitle("DOM结点详细信息");                    secondWindow.setScene(scene);                    secondWindow.show();                }            });            addMenuItem2.setOnAction(new EventHandler<ActionEvent>(){                public void handle(ActionEvent t) {                    MenuItem itemTemp = (MenuItem)t.getTarget();                    // 根据被点击的节点信息,对浏览器中对应的元素进行高亮显示                    Node nd = getTreeItem().getValue().getBindNode();                    if(nd != null && nd instanceof Element){                        Object obj = itemTemp.getUserData();                        if(obj == null){                            if (lstRectNode.indexOf(nd) == -1) {                                lstRectNode.add(nd);                                drawRectangle();                            }                            itemTemp.setUserData(true);                            itemTemp.setText("取消高亮");                        }                        else if((Boolean)obj && lstRectNode.indexOf(nd) != -1){                            itemTemp.setUserData(null);                            lstRectNode.remove(nd);                            drawRectangle();                            itemTemp.setText("高亮元素");                        }                    }                    else{                        Alert alert = new Alert(AlertType.WARNING, "");                        alert.initModality(Modality.APPLICATION_MODAL);                        alert.initOwner(null);                        alert.getDialogPane().setContentText("该结点不可进行高亮!");                        alert.getDialogPane().setHeaderText(null);                        alert.showAndWait();                    }                }            });        }        @Override        public void startEdit() {            super.startEdit();        }        @Override        public void cancelEdit() {            super.cancelEdit();        }        @Override        public void updateItem(DOMBox item, boolean empty) {            super.updateItem(item, empty);            if (empty) {                setText(null);                setGraphic(null);            } else {                setText(item.toString());                setGraphic(getTreeItem().getGraphic());                Node nd = getTreeItem().getValue().getBindNode();                // 必须是元素才可以有菜单                if (nd != null && nd instanceof Element) {                    if(lstRectNode.indexOf(nd) != -1){                        addMenu.getItems().get(1).setText("取消高亮");                        addMenu.getItems().get(1).setUserData(true);                    }                    else{                        addMenu.getItems().get(1).setText("高亮元素");                        addMenu.getItems().get(1).setUserData(null);                    }                    setContextMenu(addMenu);                }                else{                    setContextMenu(null);                }            }        }    }

    rightTreeView是TreeView对象,这里需要强调两点,第一点是TreeView必须调用setCellFactory,函数进行TreeItem和TreeCell的关联,你可以会有疑问,怎么将TreeItem传递给TreeCell的,你怎么知道哪个TreeCell对应哪个TreeItem的,这个具体原因我不知道,不过我知道在TreeCell类中可以直接通过getTreeItem()获取该Cell对应的TreeItem,TreeItem和TreeCell的关联,应该系统已经做好,不需要我们特殊关心的,具体可以看setCellFactory的实现机制;另外一点是updateItem函数进行弹出菜单的设计,比如你点击TreeView中的TreeItem,就会触发这个时间,折叠或者展看都会触发这个事件。 还有最后一点,不过这个不确定,就是有人说,不要显示保存TreeCell和TreeItem的对应关系,就是说不要在setCellFactory保存二者的对应关系,因为TreeCell并不是一成不变的,这个具体没有验证,仅供参看。

5.3 如果直接通过右键浏览器界面来高亮点击的元素

    这个实现思路是:自定义WebView的右键菜单,添加“高亮元素”选项,并实现相应菜单项的点击事件,在这个事件中我将被点击元素保存起来,然后在这个元素的同样位置,画一个Rectangle,透明度是0.5的矩形框,来表示选中效果。这里需要强调的是Rectangle和WebView的布局模式必须是StackPane才可以,这样后来的Rectangle才可以覆盖到WebView上面,其他布局模式都不行,而且WebView必须位于StackPane的最里层。 stackoverflow中有提到这个思路:How to make an overlay on top of JavaFX 2 webview?,JAVAFX中文资料中的使用JavaFX UI组件 -> 列表视图(LIST VIEW)也有相关思路的介绍。获取元素的具体位置信息,是通过如下代码片段实现的:

JSObject jsNd = (JSObject) nd;JSObject bounds = (JSObject) jsNd.call("getBoundingClientRect");Double right = Double.parseDouble(bounds.getMember("right").toString());Double top = Double.parseDouble(bounds.getMember("top").toString());Double bottom = Double.parseDouble(bounds.getMember("bottom").toString());Double left = Double.parseDouble(bounds.getMember("left").toString());

    nd表示org.w3c.dom中的Node类型的变量,一般来说都是Element类型的。

5.4 如何获取WebView的滚动条对象、滚动条的宽度和判断滚动条的方向

    这个问题目前来说没有完全解决,遇到的困难是,能获取滚动条对象(ScrollBar),不过会获取到很多个,原因是因为,你不断改变窗体大小,滚动条每次消失/重现,都会不定规律的重新创建ScrollBar对象,而且旧的ScrollBar对象在内存中依然可以获取,不能区分真正的滚动条和旧的滚动条。判断旧滚动条消失的可能条件有:1、visible属性不是可不见的 2、css中的capacity透明度属性为0 3、滚动条的enble属性为false 4、旧滚动条对象为null。这是我目前想到的即可可以判断的可能条件,然而结果是所有滚动条(同一反向的,比如水平)其属性都是相同的,不能够进行区分。我当时通过scrollbar.getCssMetaData();都将ScrollBar对应的属性都打印出来了,不过结果显示都是一样的,所以最终我没能明确获取滚动条对象。最终的解决方案是让窗体始终都保持有滚动条,这样我在绘制Rectangle时,将不会再受滚动条存在的影响。使窗口始终保持有滚动条的解决方案是通过css修改body的overflow属性,具体代码如下:

// 添加样式表,使WebView始终有滚动条String strCss = "body {"         + "    overflow-x: scroll;"         + "    overflow-y: scroll;"         + "}";Document doc = webEngine.getDocument() ;Element styleNode = doc.createElement("style");Text styleContent = doc.createTextNode(strCss);styleNode.appendChild(styleContent);                                doc.getDocumentElement().getElementsByTagName("head").item(0).appendChild(styleNode);

    直接在head结点中添加style元素,在里面修改body的overflow属性,使窗口始终保持有滚动条。
    获取所有的滚动条对象有两种方法,第一种是通过css的.scroll-bar获取,具体在上面关于JavaFX的基本概念有介绍说明。第二种方法是通过WebView的getChildrenUnmodifiable方法获取ScrollBar对象,代码片段如下:

// 获取水平、垂直滚动条的宽度ObservableList<javafx.scene.Node> lst = webView.getChildrenUnmodifiable();for(javafx.scene.Node n : lst){    if (ScrollBar.class.isInstance(n)) {        ScrollBar scroll = (ScrollBar) n;        if(scroll.getParent() == webView){                                         scroll.valueProperty().addListener(scrollChangeListener);            if(dScrollBarHHeight == -1 && scroll.getOrientation() == Orientation.HORIZONTAL){                dScrollBarHHeight = scroll.getLayoutBounds().getHeight();            }            if(dScrollBarVWidth == -1 && scroll.getOrientation() == Orientation.VERTICAL){                dScrollBarVWidth = scroll.getLayoutBounds().getWidth();            }        }    }}

    注意:JavaFX元素的OnScroll事件和传统的窗体的事件不同,因为其兼容考虑移动设备的手势滑动,所以,对WebView直接绑定Scroll事件是无效的。

5.5 如何在拖拽窗体、最大化窗体等改变窗口大小时,获取WebView中的元素坐标位置

    这个问题等效于,对于已经高亮的元素,在窗口大小改变或者滚动条滚动时,如何保持高亮不变。这个问题主要分两个步骤:第一步,监听窗口发生变化或者滚动条进行滚动;第二步,重绘所有已经高亮的元素(即Rectangle)。这里面有一个主要的问题:WebView实际渲染和JavaScript的DOM重构两者是异步的,或者说窗口大小改变后,你获取的DOM元素的位置信息时变化之前的位置。比如,原先窗口大小是100*100,最大化成1024*768,这个事件中,如果你立即执行“JSObject bounds = (JSObject) jsNd.call(“getBoundingClientRect”);”这时候你获取的元素的位置,是相对于100*100,所以Rectangle重绘时,位置是不对。不过如果你只是拖拽或者滚动滚动条,这个问题是不明显的,因为本次改变和上一次的差值可能就1-3px,所以从绘制效果来看你是看不出来的。针对获取元素位置不正确的问题,暂时的解决方案是,在窗口或者滚动条滚动后,延迟50ms左右后,再重新获取DOM元素的位置,并进行获取,这样在前台视觉效果上,用户是感觉不出来的。思路参考来源:How to listen for resize events in JavaFX
    如何监听窗口大小是否发生变化,是通过给WebView的layoutBoundsProperty()属性添加监听事件实现的,具体代码片段如下:

// 监听webView的大小是否发生变化,变化时,重绘所有的矩形webView.layoutBoundsProperty().addListener(new ChangeListener<Bounds>() {    @Override    public void changed(ObservableValue<? extends Bounds> observableValue, Bounds oldValue, Bounds newValue) {        // 采用延迟一点时间,获取坐标进行绘制,是因为窗口变化时,WebView对控件进行排版,这个事件暂时不知道怎么控制        animationRect.play();    }});

    animationRect是一个动画对象,用来延迟50ms后执行获取元素位置,并重绘Rectangle的操作的,下面有具体代码片段。
    如何监听滚动条滚动事件的,注意,这里我不关系是水平滚动条,还是垂直滚动条,我只需要关系元素的位置,因为是直接获取元素位置,不涉及到和滚动条滚动距离的计算,所以不需要关心是水平还是垂直滚动条。具体实现是,首先为页面加载成功后的所有滚动条添加滚动监听事件,然后监听是否有新滚动条增加,如果有,则对新的滚动条添加滚动监听事件。代码片段如下:

// 获取水平、垂直滚动条的宽度ObservableList<javafx.scene.Node> lst = webView.getChildrenUnmodifiable();    for(javafx.scene.Node n : lst){        if (ScrollBar.class.isInstance(n)) {            ScrollBar scroll = (ScrollBar) n;            if(scroll.getParent() == webView){                scroll.valueProperty().addListener(scrollChangeListener);                if(dScrollBarHHeight == -1 && scroll.getOrientation() == Orientation.HORIZONTAL){                    dScrollBarHHeight = scroll.getLayoutBounds().getHeight();                }                if(dScrollBarVWidth == -1 && scroll.getOrientation() == Orientation.VERTICAL){                    dScrollBarVWidth = scroll.getLayoutBounds().getWidth();                }            }        }    }

    这个是在初次页面加载成功后,给相应滚动条添加滚动监听事件。

// 监听是否有新的控制条产生webView.getChildrenUnmodifiable().addListener(new ListChangeListener<javafx.scene.Node>() {    public void onChanged(Change<? extends javafx.scene.Node> c) {        while (c.next()) {            // 如果是增加元素            if (c.wasAdded()) {                for (javafx.scene.Node ndTemp : c.getAddedSubList()) {                    if (ScrollBar.class.isInstance(ndTemp)) {                        ScrollBar scroll = (ScrollBar) ndTemp;                        if (scroll.getParent() == webView) {                            scroll.valueProperty().addListener(scrollChangeListener);                            if(dScrollBarHHeight == -1 && scroll.getOrientation() == Orientation.HORIZONTAL){                                dScrollBarHHeight = scroll.getLayoutBounds().getHeight();                            }                            if(dScrollBarVWidth == -1 && scroll.getOrientation() == Orientation.VERTICAL){                                dScrollBarVWidth = scroll.getLayoutBounds().getWidth();                            }                        }                    }                }            }        }    }});

    这个是监听在有新的滚动条产生时,绑定滚动监听事件。注意,虽然body被设置成始终都有滚动条,不过滚动条产生的时间,却不一定是页面加载完成以后,里面就会有,而是可能会延迟一点点时间产生,所以监听是否有新的滚动条产生是有必要的。

    // WebView中的滚动条,滚动时对应的事件    final ChangeListener<Number> scrollChangeListener = new ChangeListener<Number>() {        @Override public void changed(ObservableValue<? extends Number> observableValue, Number oldValue, Number newValue) {            drawRectangle();        }    };

    监听到滚动条滚动时,直接重绘所有Rectangle。

    // 窗体大小改变时,重绘所有矩形的动画    Timeline animationRect = new Timeline(            new KeyFrame(Duration.seconds(0.05), // 动画操作被调用后的延时时间            new EventHandler<ActionEvent>() {                @Override                public void handle(ActionEvent actionEvent) {                    drawRectangle();                }            }));

    定义动画操作,用于延迟50ms后,进行所有Rectangle重绘,同时设置循环执行次数是1次。

// 设置动画动作的循环次数animationRect.setCycleCount(1);

5.6 对于已生成的Document文档对象,如何在Java中监听网页中对应DOM树的结构的变化

    这个主要是利用Html5中的MutationObserver对象,对DOM改变的异步监听特性来实现的。具体实现是:先在JavaScript中注册一个java对象,可以在JavaScript调用该java对象,该java对象就是实际处理DOM结构变化时,重新获取Document对象,并重新生成TreeView的对象。然后执行脚本注册MutationObserver监听,并在对应回调函数中执行处理变化DOM的函数。代码片段如下:

JSObject jsWin = (JSObject)webEngine.executeScript("window");jsWin.setMember("cn_edu_hitsz_ices_automaticExtractor", cn_edu_hitsz_ices_automaticExtractor);// 设置DOM树发生结构时,回调修改TreeView的脚本String strScript = "var MutationObserver = window.MutationObserver ||" // 获取MutationObserver对象    +"  window.WebKitMutationObserver || "    +"  window.MozMutationObserver;"    +"  var mutationObserverSupport = !!MutationObserver;"    // DOM被修改时,具体被调用的文本    +"  var callback = function(records){"    +"      cn_edu_hitsz_ices_automaticExtractor.callDomChanged(records);"    +"      console.log('MutationObserver callback');"    +"      records.map(function(record){"    +"          console.log('Mutation type: '+ record);"    +"      });"    +"  };"    +"  var option = {"//  +"      'attributes': true," // 对属性的变化不进行监听    +"      'childList': true, "//  +"      'characterData':true," // 文本内容变化不进行监听    +"      'subtree': true"    +"  };"    +"  var mo = new MutationObserver(callback);"    +"  mo.observe(document.body, option);";// 执行脚本,注册回调webEngine.executeScript(strScript);

    webEngine是WebEngine的具体对象。这个函数是注册MutationObserver对DOM结构变化的监听回调。

    public class Cn_Edu_Hitsz_Ices_AutomaticExtractor{        // 这个地方还需要优化,因为Mutation事件记录了哪些Node发生变化(包括,被删除,被添加,属性被修改),可以通过判断,动态修改树,而不是直接全部重构右侧树        public void callDomChanged(JSObject obj){            buildRightTreeView();        }    }

    这个是在JavaScript中被具体调用对象对应的类,里面的callDomChanged函数,会在DOM树结构发生变化时,做出相应的响应动作,比如重构右侧的TreeView树。里面具体的buildRightTreeView();这里不再具体介绍,可以参看附件中的源码。推荐参考文档:HTML5新特性之Mutation Observer, Is there a JavaScript/jQuery DOM change listener?,

5.7 如何对JavaFX生成的DOM对象,进行XPath操作

    对于WebEngine获取的Document直接使用XPath进行操作是不行的,因为WebEngine生成的Document对象,结点的Tag名字是大写的,比如html是HTML,XPath编辑的路径识别不出来(这个原因是我猜的,因为我对XPath不是很了解,不知道这个原因对不对,这有英文版的解释 XPath expressions are evaluated incorrectly)。一个解决方案是,克隆WebEngine产生的Document,就是手动新建对应的Node结点,树结构的“父、子”对应关系和原来的Document一样,不过因为我需要引用原生的Node来执行相应的事件,比如click事件,如果采用这个方案,将导致我不能和WebView进行交互,当然也可以解决,就是通过一个HashMap将新的Node和旧的Node对应起来,使用XPath时,对新的Document进行操作,获取到结点后再通过HashMap找到原来的Node,这个方案是可行的,不过我没有采用。而且为了方便,还可以将原生的Document对象转换成dom4j中的Document对象,利用dom4j中丰富的操作接口进行操作。上述解决办法,对于不关心和WebView进行交互的用户,方案还是很好的(个人感觉。。)。还有一种方案,就是利用JavaScript自身对XPath的访问,通过Java调用JavaScript中的XPath对象,来查找和获取目标元素,我采用的就是这种解决方案。如果你担心Java和JavaScript频繁交互,会不会太消耗性能,我个人感觉不会太影响,第一,你是在本地交互,没有跨服务器,只是调用Webkit(WebView是就是对Webkit的封装)本身的接口,消耗应该不大;第二,信息抽取(含爬取过程)本身就不适合密集型访问Server,对于些许的延迟是可以忍受的。以下是涉及到的代码片段

// 将org.w3c.dom中的Document转换成dom4j中的Documentpublic org.dom4j.Element convert( Document doc) throws ParserConfigurationException{    // Convert w3c document to dom4j document    org.dom4j.io.DOMReader reader = new org.dom4j.io.DOMReader();    org.dom4j.Document docNew = reader.read(doc);    return docNew.getRootElement();}

    这个是将org.w3c.dom中的Document转换成dom4j中的Document,我在附件中的工程中没有采用,给注释掉了。参考来源:Converting org.w3c.dom.Document into org.dom4j.Document

// 下面是利用java标准的api执行xpath获取操作,不过这个对于JavaFX产生的DOM是不可行的,因为JavaFX的tag是大写的,而XPath是小写的,好像是因为这个原因。public List<String[]> TestInformationExtraction_Old(Document doc){    XPathFactory xpfactory = XPathFactory.newInstance();    XPath  path = xpfactory.newXPath();    try{        System.out.println(doc.getNodeName());        NodeList nodes = (NodeList)path.evaluate("//div", doc, XPathConstants.NODESET);        System.out.println("结果:"+nodes.getLength());    }catch(Exception ex){        ex.printStackTrace();    }    return null;}

    这个是标准的使用XPath对org.w3c.dom进行搜索定位。因为对于WebEngine产生的Document没有用,所以工程中给注释掉了。

// 获取下一页的按钮JSObject eTarget = null;Element page = doc.getElementById("AspNetPager1");if(page != null){    JSObject express = (JSObject)webEngine.executeScript("document.createExpression(\"//a[@class='mypaper']\")");    JSObject jsNodeList = (JSObject)express.call("evaluate", page, "XPathResult.ANY_TYPE");    Element eTemp = null;    while((eTemp = (Element)jsNodeList.call("iterateNext")) != null){        if(eTemp.getTextContent().compareTo("[下一页]") == 0){            eTarget = (JSObject)eTemp;            break;        }    }}

    这个就是利用JavaScript本身自带的XPath对象,获取“下一页”元素的代码实现。也是本工程推荐采用的方式。关于如何在JavaScript中使用XPath对象,具体参考W3School中的教程 -》XML DOM - XPathExpression 对象
    对于自动点击下一页,可以直接调用“下一页”元素的click函数即可。代码片段如下:

if(eTarget != null){    eTarget.call("click");}

5.8 Java如何执行WebView渲染网页中的按钮自动点击操作,比如“下一页”自动点击下一页,“百度一下”自动进行搜索

    这个功能上面已经介绍了,这里面在重复叙述一下。在JavaFX中,Java和JavaScript进行交互,是通过JSObject或者WebEngine.executeScript(String script)函数。二者的效果是等效的。对于JSObject对象,有call函数,直接调用JSObject拥有的函数,getMember/setMember是设置属性成员的。如果想要增加click函数,可以将JSObject(或者对应org.w3c.dom中的结点,注意:JSObject和org.w3c.dom关系是一一对应的,就是可以将org.w3c.dom中的Element直接强制转换成JSObject对象,反之也可,系统实际转换过程是将org.w3c.dom的instance值(或者对象引用值)作为句柄对象传递到Webkit中,获取对应的JSObject对象)转换成EventTarget对象,然后调用EventTarget的addEventListener函数来增加指定事件类型的处理函数。因为JavaFX采用的是DOM2事件模型,具体的事件类型,可以参考js-dom2高级事件列表。下面是部分代码片段:

JSObject btn = (JSObject)dom.getElementById("su");JSObject text = (JSObject)dom.getElementById("kw");text.setMember("value", "哈工大深研院");btn.call("click");

    dom是从WenEngine中获取的Document文档,上述是模拟在百度首页搜索框中输入“哈工大深研院”后自动点击搜索的功能。

((EventTarget)btn).addEventListener("click", new EventListener() {        public void handleEvent(Event ev) {            System.out.println("Hello World!");        }}, false);

    这个是给btn绑定事件的函数,这个好像不能覆盖原有的click,工程实际过程中是添加mousedown事件,测试时,好像不能覆盖掉原有的click函数,只能将新事件追加到事件链。(这个具体忘了,请自行验证)。

6、附件

  1. 基于JavaFX的可视化信息抽取Demo,说明:工程默认是采用GBK编码格式的,导入时请注意,否则相应注释会出现乱码。
    我的eclipse下载的是直接集成JavaFX开发环境的版本,下载链接是:http://downloads.efxclipse.bestsolution.at/downloads/released/2.3.0/sdk/eclipse-SDK-4.5.2-win32-x86_64-distro-2.3.0.zip

    因为这个工程不涉及到FXML的布局,只是简单的Java代码,所以如果你不想用定制版,想用原先的eclipse,简单设置一下JDK规则也可以,默认eclipse不开放对JavaFX的访问,设置方法如下:
    1. Build Path -》 Configure Build Path… -》 Libraries,如下图:
      编辑Access rules规则
    2. 添加允许的规则,如下图:
      添加规则
    3. 然后点击OK即可。代码便可在eclipse正常运行了。注意,工程不涉及任何第三方包,都是利用JDK自带的JavaFX包进行编写的。
  2. Oracle提供的一些JavaFX的demo代码,这里面包括了所有的关于JavaFX的基础使用样例,不是仅局限于WebView的使用。下载网址:javafx_samples-8u102-ea-b04-windows-25_apr_2016

参考文献汇总

  1. JAVAFX中文资料:http://www.javafxchina.net/blog/docs/tutorial1/
  2. JavaFX: Getting Started with JavaFX:http://docs.oracle.com/javase/8/javafx/get-started-tutorial/index.html
  3. Java Platform, Standard Edition (Java SE) 8相关资料(含JavaFX:http://docs.oracle.com/javase/8/index.html
  4. JavaFX 2.0的API文档:http://docs.oracle.com/javafx/2/api/
  5. JavaFX CSS Reference Guide:https://docs.oracle.com/javafx/2/api/javafx/scene/doc-files/cssref.html
  6. JavaFX 2.0 Resizing of UI Controls:http://blog.e-zest.net/javafx-20-resizing-of-ui-controls/
  7. 【CSS进阶】原生JS getComputedStyle等方法解析:http://www.tuicool.com/articles/M7fyQv6
  8. vips_java:https://github.com/tpopela/vips_java
  9. XPath expressions are evaluated incorrectly:https://bugs.openjdk.java.net/browse/JDK-8090173
  10. Converting org.w3c.dom.Document into org.dom4j.Document:https://community.oracle.com/thread/2051697?start=0&tstart=0
  11. JAVAFX-事件:http://blog.csdn.net/ice00mouse/article/details/25491565
  12. How to make an overlay on top of JavaFX 2 webview?:http://stackoverflow.com/questions/10894903/how-to-make-an-overlay-on-top-of-javafx-2-webview
  13. 使用JavaFX UI组件 -> 列表视图(LIST VIEW):http://www.javafxchina.net/blog/2015/04/doc03_list-view/
  14. How to listen for resize events in JavaFX:http://stackoverflow.com/questions/10773000/how-to-listen-for-resize-events-in-javafx/25812859#25812859
  15. 使用JavaFX UI组件 -> 树视图(TREE VIEW):http://www.javafxchina.net/blog/2015/04/doc03_treeview/
  16. HTML5新特性之Mutation Observer:http://www.cnblogs.com/jscode/p/3600060.html
  17. Is there a JavaScript/jQuery DOM change listener?:http://stackoverflow.com/questions/2844565/is-there-a-javascript-jquery-dom-change-listener/11546242#11546242
  18. XML DOM - XPathExpression 对象:http://www.w3school.com.cn/xmldom/dom_xpathexpression.asp
  19. js-dom2高级事件列表:http://www.cnblogs.com/lianzi/archive/2011/09/17/2179735.html
  20. Is it possible to retrieve HTML element in web engine without using Javascript?:http://stackoverflow.com/questions/31957218/is-it-possible-to-retrieve-html-element-in-web-engine-without-using-javascript
  21. Detect DOM changes with Mutation Observers:https://developers.google.com/web/updates/2012/02/Detect-DOM-changes-with-Mutation-Observers?hl=en
1 0