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-事件),如下图:


图片引用来自: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>>() {@Overridepublic 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));// 这个方法获取x,y坐标时,有问题
//      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>>(){@Overridepublic 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();}}});}@Overridepublic void startEdit() {super.startEdit();}@Overridepublic void cancelEdit() {super.cancelEdit();}@Overridepublic 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>() {@Overridepublic 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>() {@Overridepublic 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中的Document
public org.dom4j.Element convert( Document doc) throws ParserConfigurationException
{// Convert w3c document to dom4j documentorg.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,如下图:
    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

JavaFX和可视化信息抽取相关推荐

  1. 关于NLP相关技术全部在这里:预训练模型、图神经网络、模型压缩、知识图谱、信息抽取、序列模型、深度学习、语法分析、文本处理...

    NLP近几年非常火,且发展特别快.像BERT.GPT-3.图神经网络.知识图谱等技术应运而生. 我们正处在信息爆炸的时代.面对每天铺天盖地的网络资源和论文.很多时候我们面临的问题并不是缺资源,而是找准 ...

  2. 【ACL2020】这8份Tutorial不可错过!包括:常识推理、多模态信息抽取、对话、解释性等...

    点击上方,选择星标或置顶,不定期资源大放送! 阅读大概需要7分钟 Follow小博主,每天更新前沿干货 自然语言处理领域顶级会议 ACL 2020 将于 7 月 5 日至 10 日在线上举行.本届 A ...

  3. 详解预训练模型、图神经网络、模型压缩、知识图谱、信息抽取、序列模型、深度学习、语法分析、文本处理...

    NLP近几年非常火,且发展特别快.像BERT.GPT-3.图神经网络.知识图谱等技术应运而生.我们正处在信息爆炸的时代.面对每天铺天盖地的网络资源和论文.很多时候我们面临的问题并不是缺资源,而是找准资 ...

  4. 面向知识图谱的信息抽取

    面向知识图谱的信息抽取 人工智能技术与咨询 点击蓝字 · 关注我们 来源:< 数据挖掘,> ,作者赵海霞等 关键词: 知识图谱:信息抽取:实体抽取:关系抽取:开放域 摘要: 摘要: 随着大 ...

  5. 百分点认知智能实验室:基于不完全标注样本集的信息抽取实践

    编者按 信息抽取是从文本数据中抽取特定信息的一种技术,命名实体识别(Named Entity Recognition, NER)是信息抽取的基础任务之一,其目标是抽取文本中具有基本语义的实体单元,在知 ...

  6. NLP专栏简介:数据增强、智能标注、意图识别算法|多分类算法、文本信息抽取、多模态信息抽取、可解释性分析、性能调优、模型压缩算法等

    NLP专栏简介:数据增强.智能标注.意图识别算法|多分类算法.文本信息抽取.多模态信息抽取.可解释性分析.性能调优.模型压缩算法等 专栏链接:NLP领域知识+项目+码源+方案设计 订阅本专栏你能获得什 ...

  7. 深度学习应用篇-自然语言处理[10]:N-Gram、SimCSE介绍,更多技术:数据增强、智能标注、多分类算法、文本信息抽取、多模态信息抽取、模型压缩算法等

    [深度学习入门到进阶]必看系列,含激活函数.优化策略.损失函数.模型调优.归一化算法.卷积模型.序列模型.预训练模型.对抗神经网络等 专栏详细介绍:[深度学习入门到进阶]必看系列,含激活函数.优化策略 ...

  8. 【PaddleOCR-kie】关键信息抽取1:使用VI-LayoutXLM模型推理预测(SER+RE)

    背景:在训练自己数据集进行kie之前,想跑一下md里面的例程,但md教程内容混乱,而且同一个内容有多个手册,毕竟是多人合作的项目,可能是为了工程解耦,方便更新考虑--需要运行的模型和运行步骤散落在不用 ...

  9. 第三届“达观杯”文本智能信息抽取挑战赛丰厚奖金,群英集结,等你来战!...

    近日,第三届"达观杯"文本智能信息抽取挑战赛正式上线启动(点击阅读原文,跳转报名页面),6月28日至8月31日,面向所有参赛选手开放竞赛结果提交.本届"达观杯" ...

最新文章

  1. 学习web前端你必须要了解的主流框架!
  2. BFS之三(单向bfs和康托压缩)
  3. java io 网络编程_[笔面] Java IO和网络编程相关面试
  4. Java黑皮书课后题第8章:*8.23(游戏:找到翻转的单元格)假定给定一个填满0和1的6*6矩阵,所有的行和列都有偶数个1。让用户翻转一个单元,编写一个程序找到哪个单元格被翻转了
  5. oracle主从表分离怎么实时更新数据_高可用数据库UDB主从复制延时的解决
  6. Fixed Function Shader
  7. Undefined control sequence.l.113 \LinesNumbered
  8. App开发流程之图像处理工具类
  9. php led显示屏控制软件下载,中航led控制软件
  10. opengl 知识点2
  11. 解放生产力!20 个必知必会 VSCode 小技巧
  12. html5动漫动态背景图片,简单动画-让你的背景图动起来!!!
  13. 分享一个简易的AT变速箱(TCU)换挡逻辑控制模型
  14. 新玺配资:股票波段操作中的操作法则
  15. 自动化控制面试问题整理
  16. 6-27 实验9_7_设计函数int getVowel(char str[],char vowel[]); (100 分)
  17. 使用Optional类来消除代码中的null检查
  18. 330. 按要求补齐数组
  19. Dynamics 365 for Phones Android APK(v4.3.22042.2)下载
  20. 选股策略之MACD指标选股

热门文章

  1. 我开发过程中遇到的Echarts地图立体描边问题解决方式
  2. 工作了4年的JAVA程序员应该具备什么技能?
  3. 农业病虫害数据集与算法——调研整理
  4. react黑马前端学习笔记
  5. 第一周《人月神话》读书笔记-------黄志鹏
  6. uniapp附件上传及预览
  7. Cannot download sources Sources not found for:xxx解决方法汇总
  8. Python 编码错误UnicodeDecodeError: ‘gbk‘ codec can‘t decode byte 0xac in position 131: illegal multibyte
  9. 【PTA题目】7-5 阶梯电价 (15 分)
  10. 评论:“哭不起”的王君为何还流泪