本篇博客是非介绍类的,即,不含有关于JavaFX的基础介绍。博客主要描述实现可视化信息抽取时,如何利用JavaFX的WebView组件。仅介绍涉及到的JavaFX的内容,至于可视化信息抽取的算法是哪个,博客不进行介绍,提供的源码中有一个可视化信息抽取的演示Demo,Demo不涉及到核心算法,是基于定制规则进行抽取的。
博客内容是我毕业设计(基于反馈学习的半结构化信息抽取研究及应用)可能涉及到的基础技术的准备和总结,技术准备的时间较短,同时心里着急找工作(真心着急。。。),里面可能有不正确的,如有发现,请指正,同时提醒,本博客内容只是作为技术实现的可行tips,千万不要作为标准样板,关于某些问题,如果您有发现新的解决方案,请您一定要告诉我一下,非常感谢!
本文涉及到的JavaFX对应的JDK版本是JDK1.7
关于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. branchCollapsedEvent(),
new EventHandler.TreeModificationEvent>() {
@Override
public void handle(TreeItem.TreeModificationEvent event) {
TreeItem tCurrent = event.getTreeItem();
DOMBox domBox = (DOMBox) tCurrent.getValue();
if (!ElementTools.isSelfClose(domBox.getTagName())) {
String strHtml = domBox.getSelfHtml() + "..." + domBox.getTagName() + ">";
domBox.setSelfHtml(strHtml);
TreeItem 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 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> css = scroll.getCssMetaData();
for (int i = 0; i < css.size(); i++) {
CssMetaData extends Styleable, ?> oneAttr = css.get(i);
System.out.print(oneAttr.getProperty() + ":" + ((CssMetaData) 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。
强烈推荐Stack OverFlow,我的解决方案好像都是在其中看的代码片段知道的。
因为毕业设计的信息抽取是基于可视化模块后,进行抽取的,主要是采用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的。就是样式表的种类:
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等方法解析
如果需要右键点击TreeView弹出菜单,则需要实现TreeItem对应的TreeCell元素,因为TreeItem本身是不接受鼠标类事件的,比如,鼠标移动、鼠标点击等等。实现这个功能,可参考JAVAFX中文资料中的使用JavaFX UI组件 -> 树视图(TREE VIEW)。下面是自己实现的主要代码片段:
rightTreeView.setCellFactory(new Callback, TreeCell>(){
@Override
public TreeCell call(TreeView para){
return new DOMTreeCellImpl();
}
});
private final class DOMTreeCellImpl extends TreeCell{
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(){
public void handle(ActionEvent t) {
// 以对话框的形式,弹出结点的基本信息
TreeItem 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(){
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并不是一成不变的,这个具体没有验证,仅供参看。
这个实现思路是:自定义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类型的。
这个问题目前来说没有完全解决,遇到的困难是,能获取滚动条对象(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.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事件是无效的。
这个问题等效于,对于已经高亮的元素,在窗口大小改变或者滚动条滚动时,如何保持高亮不变。这个问题主要分两个步骤:第一步,监听窗口发生变化或者滚动条进行滚动;第二步,重绘所有已经高亮的元素(即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() {
@Override
public void changed(ObservableValue extends Bounds> observableValue, Bounds oldValue, Bounds newValue) {
// 采用延迟一点时间,获取坐标进行绘制,是因为窗口变化时,WebView对控件进行排版,这个事件暂时不知道怎么控制
animationRect.play();
}
});
animationRect是一个动画对象,用来延迟50ms后执行获取元素位置,并重绘Rectangle的操作的,下面有具体代码片段。
如何监听滚动条滚动事件的,注意,这里我不关系是水平滚动条,还是垂直滚动条,我只需要关系元素的位置,因为是直接获取元素位置,不涉及到和滚动条滚动距离的计算,所以不需要关心是水平还是垂直滚动条。具体实现是,首先为页面加载成功后的所有滚动条添加滚动监听事件,然后监听是否有新滚动条增加,如果有,则对新的滚动条添加滚动监听事件。代码片段如下:
// 获取水平、垂直滚动条的宽度
ObservableList.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.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() {
@Override
public void handle(ActionEvent actionEvent) {
drawRectangle();
}
}));
定义动画操作,用于延迟50ms后,进行所有Rectangle重绘,同时设置循环执行次数是1次。
// 设置动画动作的循环次数
animationRect.setCycleCount(1);
这个主要是利用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?,
对于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 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 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");
}
这个功能上面已经介绍了,这里面在重复叙述一下。在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函数,只能将新事件追加到事件链。(这个具体忘了,请自行验证)。