Copyright © 2005 Stephen Suen. All rights reserved.
Java q程Ҏ(gu)调用QRemote Method Invocation, RMIQ得运行在一?Java 虚拟机(Java Virtual Machine, JVMQ的对象可以调用q行另一?JVM 之上的其他对象的Ҏ(gu)Q从而提供了E序间进行远E通讯的途径。RMI ?J2EE 的很多分布式技术的基础Q比?RMI-IIOP 乃至 EJB。本文是 RMI 的一个入门指南,目的在于帮助读者快速徏立对 Java RMI 的一个感性认识,以便q行更深层次的学?fn)。事实上Q如果你了解 RMI 的目的在于更好的理解和学?EJBQ那么本文就再合适不q了。通过本文所了解?RMI 的知识和技巧,应该_服务于这个目的了?
本文的最新版本将发布?a >E序员咖啡馆|站上(中)。欢q订阅我们的邮gl?/font>Q以获得关于本文的正式发布及更新信息?/p>
全文在保证完整性,且保留全部版权声明(包括上述链接Q的前提下可以在L媒体转蝲——须保留此标注?/p>
我们知道q程q程调用QRemote Procedure Call, RPCQ可以用于一个进E调用另一个进E(很可能在另一个远E主ZQ中?span>q程Q从而提供了q程的分布能力。Java ?RMI 则在 RPC 的基上向前又q进了一步,x供分布式 对象间的通讯Q允许我们获得在q程q程中的对象Q称E对象)的引用(UCؓq程引用Q,q而通过引用调用q程对象的方法,好像该对象是与你的客户端代码同栯行在本地q程中一栗RMI 使用了术?Ҏ(gu)"QMethodQ强调了q种q步Q即在分布式基础上,充分支持面向对象的特性?/p>
RMI q不?Java 中支持远E方法调用的唯一选择。在 RMI 基础上发展而来?RMI-IIOPQJava Remote Method Invocation over the Internet Inter-ORB ProtocolQ,不但l承?RMI 的大部分优点Qƈ且可以兼容于 CORBA。J2EE ?EJB 都要求?RMI-IIOP 而不?RMI。尽如此,理解 RMI 大大有助于 RMI-IIOP 的理解。所以,即便你的兴趣?RMI-IIOP 或?EJBQ相信本文也会对你很有帮助。另外,如果你现在就?API 感兴,那么可以告诉你,RMI 使用 java.rmi 包,?RMI-IIOP 则既使用 java.rmi 也用扩展的 javax.rmi 包?/p>
本文的随后内容将仅针?Java RMI?/p>
在学?RMI 之前Q我们需要了解一些基知识。首先需要了解所谓的分布式对象(Distributed ObjectQ。分布式对象是指一个对象可以被q程pȝ所调用。对?Java 而言Q即对象不仅可以被同一虚拟Z的其他客L(fng)序(ClientQ调用,也可以被q行于其他虚拟机中的客户E序调用Q甚臛_以通过|络被其他远E主Z上的客户E序调用?/p>
下面的图C明了客户E序是如何调用分布式对象的:
从图上我们可以看刎ͼ分布式对象被调用的过E是q样的:
客户E序调用一个被UCؓ Stub Q有时译作存根,Z不生歧义,本文用其英文形式Q的客户端代理对象。该代理对象负责对客L(fng)隐藏|络通讯的细节。Stub 知道如何通过|络套接字(SocketQ发送调用,包括如何调用参数{换ؓ适当的Ş式以便传输等?/p>
Stub 通过|络调用传递到服务器端Q也是分布对象一端的一个被UCؓ Skeleton 的代理对象。同P该代理对象负责对分布式对象隐藏网l通讯的细节。Skeleton 知道如何从网l套接字QSocketQ中接受调用Q包括如何将调用参数从网l传输Ş式{换ؓ Java 形式{?/p>
Skeleton 调用传递给分布式对象。分布式对象执行相应的调用,之后返回g递给 SkeletonQ进而传递到 StubQ最l返回给客户E序?/p>
q个场景Z一个基本的法则Q即行ؓ的定义和行ؓ的具体实现相分离。如图所C,客户端代理对?Stub 和分布式对象都实C相同的接口,该接口称E接口(Remote InterfaceQ。正是该接口定义了行为,而分布式对象本n则提供具体的实现。对?Java RMI 而言Q我们用接口Q?font color="red">interfaceQ定义行为,用类Q?font color="red">classQ定义实现?
RMI 的底层架构由三层构成Q?/p>
首先?Stub/Skeleton 层。该层提供了客户E序和服务程序彼此交互的接口?/p>
然后是远E引用(Remote ReferenceQ层。这一层相当于在其之上?Stub/Skeleton 层和在其之下的传输协议层之前的中间gQ负责处理远E对象引用的创徏和管理?/p>
最后是传输协议QTransport ProtocolQ?层。该层提供了数据协议Q用以通过U\传输客户E序和远E对象间的请求和应答?/p>
q些层之间的交互可以参照下面的示意图Q?/p>
和其它分布式对象机制一PJava RMI 的客L(fng)序用客L(fng)?Stub 向远E对象请求方法调用;服务器对象则通过服务器端?Skeleton 接受h。我们深入进去,来看看其中的一些细节?/p>
注意: 事实上,?Java 1.2 之后QRMI 不再需?Skeleton 对象Q而是通过 Java 的反机ӞReflectionQ来完成Ҏ(gu)务器端的q程对象的调用。ؓ了便于说明问题,本文以下内容仍然Z Skeleton 来讲解?/p>
当客L(fng)序调?Stub ӞStub 负责方法的参数转换为序列化QSerializedQŞ式,我们使用一个特D的术语Q即~列QMarshalQ来指代q个q程。编列的目的是将q些参数转换为可UL的Ş式,从而可以通过|络传输到远E的服务对象一端。不q的是,q个q程没有惌中那么简单。这里我们首先要理解一个经典的问题Q即Ҏ(gu)调用Ӟ参数I竟是传D是传引用呢?对于 Java RMI 来说Q存在四U情况,我们分别加以说明?/p>
对于基本的原始类型(整型Q字W型{等Q,被自动的序列化Q以传值的方式~列?/p>
对于 Java 的对象,如果该对象是可序列化的(实现?java.io.Serializable 接口Q,则通过 Java 序列化机制自动地加以序列化,以传值的方式~列。对象之中包含的原始cd以及所有被该对象引用,且没有声明ؓ transient 的对象也自动的序列化。当Ӟq些被引用的对象也必L可序列化的?/p>
l大多数内徏?Java 对象都是可序列化的?对于不可序列化的 Java 对象Q?font color="red">java.io.File 最典型Q,或者对象中包含对不可序列化Q且没有声明?transient 的其它对象的引用。则~列q程向客户E序抛出异常Q而宣告失败?/p>
客户E序可以调用q程对象Q没有理q止调用参数本w也是远E对象(实现?java.rmi.Remote 接口的类的实例)。此ӞRMI 采用一U?span>模拟?/i>传引用方式(当然不是传统意义的传引用Q因为本地对内存的引用到了远E变得毫无意义)Q而不是将参数直接~列复制到远E。这U情况下Q交互的双方发生的戏剧性变化值得我们注意。参数是q程对象Q意味着该参数对象可以远E调用。当客户E序指定q程对象作ؓ参数调用服务器端q程对象的方法时QRMI 的运行时机制向服务器端的远E对象发送作为参数的q程对象的一?Stub 对象。这h务器端的q程对象可以回调(CallbackQ这?Stub 对象的方法,q而调用在客户端的q程对象的对应方法。通过q种Ҏ(gu)Q服务器端的q程对象可以修改作为参数的客户端远E对象的内部状态,q正是传l意义的传引用所具备的特性。是不是有点晕?q里的关键是要明白,在分布式环境中,所谓服务器和客L(fng)都是相对的。被h的一方就是服务器Q而发求的一方就是客L(fng)?
在调用参数的~列q程成功后,客户端的q程引用层从 Stub 那里获得了编列后的参C及对服务器端q程对象的远E引用(参见 java.rmi.server.RemoteRef APIQ。该层负责将客户E序的请求依据底层的 RMI 数据传输协议转换Z输层h。在 RMI 中,有多U的可能的传输机Ӟ比如点对点(Point-to-PointQ以及广播(MulticastQ等。不q,在当前的 JMI 版本中只支持点对点协议,卌E引用层生成唯一的传输层hQ发往指定的唯一q程对象Q参?java.rmi.server.UnicastRemoteObject APIQ?/p>
在服务器端,服务器端的远E引用层接收传输层请求,q将其{换ؓ对远E对象的服务器端代理对象 Skeleton 的调用。Skeleton 对象负责请求{换ؓ对实际的q程对象的方法调用。这是通过与编列过E相对的反编列(UnmarshalQ过E实现的。所有序列化的参数被转换?Java 形式Q其中作为参数的q程对象Q实际上发送的是远E引用)被{换ؓ服务器端本地?Stub 对象?/p>
如果Ҏ(gu)调用有返回值或者抛出异常,?Skeleton 负责~列q回值或者异常,通过服务器端的远E引用层Q经传输层传递给客户端;相应圎ͼ客户端的q程引用层和 Stub 负责反编列ƈ最l将l果q回l客L(fng)序?/p>
整个q程中,可能最让hqh的是q程引用层。这里只要明白,本地?Stub 对象是如何生的Q就不难理解q程引用的意义所在了。远E引用中包含了其所指向的远E对象的信息Q该q程引用用于构造作为本C理对象的 Stub 对象。构造后QStub 对象内部维护该q程引用。真正在|络上传输的实际上就是这个远E引用,而不?Stub 对象?/p>
?RMI 的基本架构之上,RMI 提供服务与分布式应用E序的一些对象服务,包括对象的命?注册QNaming/RegistryQ服务,q程对象Ȁz(ActivationQ服务以及分布式垃圾攉QDistributed Garbage Collection, DGCQ。作为入门指南,本文指介绍其中的命?注册服务Q因为它是实?RMI 所必备的。其它内容请读者自行参考其它更加深入的资料?/p>
在前一节中Q如果你喜欢刨根问底Q可能已l注意到Q客L(fng)要调用远E对象,是通过其代理对?Stub 完成的,那么 Stub 最早是从哪里得来的呢?RMI 的命?注册服务正是解决q一问题的。当服务器端惛_客户端提供基?RMI 的服务时Q它需要将一个或多个q程对象注册到本地的 RMI 注册表中Q参?font color="red">java.rmi.registry.Registry APIQ。每个对象在注册旉被指定一个将来用于客L(fng)序引用该对象的名U。客L(fng)序通过命名服务Q参?java.rmi.Naming APIQ,指定cM URL 的对象名U就可以获得指向q程对象的远E引用。在 Naming 中的 lookup() Ҏ(gu)扑ֈq程对象所在的L后,它将索该L上的 RMI 注册表,q请求所需的远E对象。如果注册表发现被请求的q程对象Q它?yu)生成一个对该远E对象的q程引用Qƈ其q回l客L(fng)Q客L(fng)则基于远E引用生成相应的 Stub 对象Qƈ引用传递给调用者。之后,双方可以按照我们前面讲q的方式q行交互了?
注意: RMI 命名服务提供?Naming cdƈ不是你的唯一选择。RMI 的注册表可以与其他命名服务绑定,比如 JNDIQ这样你可以通过 JNDI 来访?RMI 的注册表了?/p>
理论M开实践Q理?RMI 的最好办法就是通过例子。开?RMI 的分布式对象的大体过E包括如下几步:
定义q程接口。这一步是通过扩展 java.rmi.Remote 接口Qƈ定义所需的业务方法实现的?/p>
定义q程接口的实现类。即实现上一步所定义的接口,l出业务Ҏ(gu)的具体实现逻辑?/p>
~译q程接口和实现类Qƈ通过 RMI ~译?rmic Z实现cȝ成所需?Stub ?Skeleton cR?/p>
RMI 中各个组件之间的关系如下面这个示意图所C:
回忆我们上一节所讲的QStub ?Skeleton 负责代理客户和服务器之间的通讯。但我们q不需要自q成它们,相反QRMI 的编译器 rmic 可以帮我们基于远E接口和实现cȝ成这些类。当客户端对象通过命名服务向服务器端的 RMI 注册表请求远E对象时QRMI 自动构造对应远E对象的 Skeleton 实例对象Qƈ通过 Skeleton 对象远E引用返回给客户端。在客户端,该远E引用将用于构?Stub cȝ实例对象。之后,Stub 对象?Skeleton 对象可以代理客户对象和q程对象之间的交互了?
我们的例子展C一个简单的应用场景。服务器端部|了一个计引擎,负责接受来自客户端的计算dQ在服务器端执行计算dQƈ结果返回给客户端。客L(fng)发送ƈ调用计算引擎的计Q务实际上是计指定精度的 π 倹{?/p>
重要: 本文的例子改~自 The Java?Tutorial Trail:RMI。所有权利属于相应的所有h?/p>
定义q程接口与非分布式应用中定义接口的方法没有太多的区别。只要遵守下面两个要求:
q程接口必须直接或者间接的扩展?java.rmi.Remote 接口。远E接口还可以在扩展该接口的基上,同时扩展其它接口Q只要被扩展的接口的所有方法与q程接口的所有方法一h下一个要求?/p>
在远E接口或者其接口(Super-interfaceQ中声明的方法必L下列对q程Ҏ(gu)的要求:
q程Ҏ(gu)必须声明抛出 java.rmi.RemoteException 异常Q或者该异常的超c(SuperclassQ,比如 java.io.IOException 或?java.lang.Exception 异常。在此基上,q程Ҏ(gu)可以声明抛出应用特定的其它异常?/p>
在远E方法声明中Q作为参数或者返回值的q程对象Q或者包含在其它非远E对象中的远E对象,必须声明为其对应的远E接口,而不是实际的实现cR?/p>
注意: ?Java 1.2 之前Q上面关于抛出异常的要求更严|卛_L?java.rmi.RemoteExcptionQ不允许cM java.io.IOException q样的超cR现在之所以放宽了q一要求Q是希望可以使定义既可以用于q程对象Q也可以用于本地对象的接口变得容易一些(x EJB 中的本地接口和远E接口)。当Ӟqƈ没有佉K题好多少Q你q是必须声明异常。不q,一U观点认不是问题Q强制声明异常可以开发h员保持清醒的头脑Q因E对象和本地对象在调用时传参的语意是不同的。本地对象是传引用,而远E对象主要是传|q意呛_参数内部状态的修改产生的结果是不同的?
对于W一个要求,java.rmi.Remote 接口实际上没有Q何方法,而只是用作标记接口。RMI 的运行环境依赖该接口判断对象是否是远E对象。第二个要求则是因ؓ分布式应用可能发生Q何问题,比如|络问题{等?/p>
?1 列出了我们的q程接口定义。该接口只有一个方法:executeTask() 用以执行指定的计Q务,q返回相应的l果。注意,我们用后~ Remote 表明接口是远E接口?/p>
?1. ComputeEngineRemote q程接口
package rmitutorial; import java.rmi.Remote; import java.rmi.RemoteException; public interface ComputeEngineRemote extends Remote { public Object executeTask(Task task) throws RemoteException; }
?2 列出了计Q务接口的定义。该接口也只有一个方法:execute() 用以执行实际的计逻辑Qƈq回l果。注意,该接口不是远E接口,所以没有扩?java.rmi.Remote 接口Q其Ҏ(gu)也不必抛?java.rmi.RemoteException 异常。但是,因ؓ它将用作q程Ҏ(gu)的参敎ͼ所以扩展了 java.io.Serializable 接口?/p>
接下来,我们实现前面定义的q程接口?a>?3l出了实现的源代码?/p>
?3. ComputeEngine 实现
package rmitutorial; import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; public class ComputeEngine extends UnicastRemoteObject implements ComputeEngineRemote { public ComputeEngine() throws RemoteException { super(); } public Object executeTask(Task task) throws RemoteException { return task.execute(); } }
c?ComputeEngine 实现了之前定义的q程接口Q同时承自 java.rmi.server.UnicastRemoteObject 类?font color="red">UnicastRemoteObject cL一个便L(fng)Q它实现了我们前面所讲的Z TCP/IP 的点对点通讯机制。远E对象都必须从该cL展(除非你想自己实现几乎所?UnicastRemoteObject 的方法)。在我们的实现类的构造函CQ调用了类的构造函敎ͼ当然Q即使你不显式的调用q个构徏函数Q它也一样会被调用。这里这样做Q只是ؓ了突出强调这U调用而已Q。该构造函数的最重要的意义就是调?UnicastRemoteObject cȝ exportObject() Ҏ(gu)。导出(ExportQ对象是指ɘq程对象准备qAQ可以接受进来的调用的过E。而这个过E的最重要内容是建立服务器套接字Q监听特定的端口Q等待客L(fng)的调用请求?/p>
Z让客L(fng)序可以找到我们的q程对象Q就需要将我们的远E对象注册到 RMI 的注册表。这个过E有时被UCؓ"引导"q程QBootstrapQ。我们将为此~写一个独立的引导E序负责创徏和注册远E对象?a>?4 l出了引导程序的源代码?
?4. 引导E序
package rmitutorial; import java.rmi.Naming; import java.rmi.RMISecurityManager; public class Bootstrap { public static void main(String[] args) throws Exception { String name = "ComputeEngine"; ComputeEngine engine = new ComputeEngine(); System.out.println("ComputerEngine exported"); Naming.rebind(name, engine); System.out.println("ComputeEngine bound"); } }
可以看到Q我们首先创Z一个远E对象(同时导出了该对象Q,之后该对象l定?RMI 注册表中?font color="red">Naming ?rebind() Ҏ(gu)接受一?URL 形式的名字作l定之用。其完整格式如下Q?/p>
其中Q协议(ProtocolQ默认ؓ rmiQ主机名默认?localhostQ端口默认ؓ 1099。注意,JDK 中提供的默认 Naming 实现只支?rmi 协议。在我们的引导程序里面只l出了对象绑定的名字Q而其它部分均使用~省倹{?/p>
?5 l出了我们的客户端程序。该E序接受两个参数Q分别是q程对象所在的L地址和希望获得的 π 值的_ֺ?
?5. Client.java
package rmitutorial; import java.math.BigDecimal; import java.rmi.Naming; public class Client { public static void main(String args[]) throws Exception { String name = "rmi://" + args[0] + "/ComputeEngine"; ComputeEngineRemote engineRemote = (ComputeEngineRemote)Naming.lookup(name); Pi task = new Pi(Integer.parseInt(args[1])); BigDecimal pi = (BigDecimal)(engineRemote.executeTask(task)); System.out.println(pi); } }
?6. Pi.java
package rmitutorial; import java.math.*; public class Pi implements Task { private static final BigDecimal ZERO = BigDecimal.valueOf(0); private static final BigDecimal ONE = BigDecimal.valueOf(1); private static final BigDecimal FOUR = BigDecimal.valueOf(4); private static final int roundingMode = BigDecimal.ROUND_HALF_EVEN; private int digits; public Pi(int digits) { this.digits = digits; } public Object execute() { return computePi(digits); } public static BigDecimal computePi(int digits) { int scale = digits + 5; BigDecimal arctan1_5 = arctan(5, scale); BigDecimal arctan1_239 = arctan(239, scale); BigDecimal pi = arctan1_5.multiply(FOUR).subtract( arctan1_239).multiply(FOUR); return pi.setScale(digits, BigDecimal.ROUND_HALF_UP); } public static BigDecimal arctan(int inverseX, int scale) { BigDecimal result, numer, term; BigDecimal invX = BigDecimal.valueOf(inverseX); BigDecimal invX2 = BigDecimal.valueOf(inverseX * inverseX); numer = ONE.divide(invX, scale, roundingMode); result = numer; int i = 1; do { numer = numer.divide(invX2, scale, roundingMode); int denom = 2 * i + 1; term = numer.divide(BigDecimal.valueOf(denom), scale, roundingMode); if ((i % 2) != 0) { result = result.subtract(term); } else { result = result.add(term); } i++; } while (term.compareTo(ZERO) != 0); return result; } }
~译我们的示例程序和~译其它非分布式的应用没什么区别。只是编译之后,需要?RMI ~译器,?rmic 生成所需 Stub ?Skeleton 实现。?rmic 的方式是我们的q程对象的实现类Q不是远E接口)的全cd作ؓ参数来运?rmic 命o。参考下面的CZQ?/p>
E:\classes\rmic rmitutorial.ComputeEngine
~译之后生?rmitutorial.ComputeEngine_Skel ?rmitutorial.ComputeEngine_Stub 两个cR?/p>
q程对象的引用通常是通过 RMI 的注册表服务以及 java.rmi.Naming 接口获得的。远E对象需要导出(注册Q相应的q程引用到注册表服务Q之后注册表服务可以监听ƈ服务于客L(fng)对远E对象引用的h。标准的 Sun Java SDK 提供了一个简单的 RMI 注册表服务程序,?rmiregistry 用于监听特定的端口,{待q程对象的注册,以及客户端对q些q程对象引用的检索请求?
在运行我们的CZE序之前Q首先要启动 RMI 的注册表服务。这个过E很单,只要直接q行 rmiregistry 命o卛_。缺省的情况下,该服务将监听 1099 端口。如果需要指定其它的监听端口Q可以在命o行指定希望监听的端口Q如果你指定了其它端口,需要修改示例程序以适应环境Q。如果希望该E序在后台运行,?Unix 上可以以如下方式q行Q当Ӟ可以~省端口参数Q:
$ rmiregistry 1099 &
?Windows 操作pȝ中可以这栯行:
C:\> start rmiregistry 1099
我们?rmitutorial.Bootstrap cd用于启动q程对象Qƈ其l定?RMI 注册表中。运行该cdQ远E对象也进入监听状态,{待来自客户端的Ҏ(gu)调用h?/p>
$ java rmitutorial.Bootstrap ComputeEngine exported ComputeEngine bound
启动q程对象后,打开另一个命令行H口Q运行客L(fng)。命令行的第一个参Cؓ RMI 注册表的地址Q第二个参数为期望的 π 值精度。参考下面的CZQ?/p>
$ java rmitutorial.Client localhost 50 3.14159265358979323846264338327950288419716939937511
在演C示例程序时Q我们实际上是在同一L上运行的服务器和客户端,q且无论是服务器和客L(fng)所需的类都在相同的类路径上,可以同时被服务器和客L(fng)所讉K。这忽略?Java RMI 的一个重要细节,卛_态类装蝲。因?RMI 的特性(包括其它几个Ҏ(gu))q不适用?J2EE ?RMI-IIOP ?EJB 技术,所以,本文不作详l介l,误者自行参考本文给出的参考资料。不q,Z让好奇的读者不至于q分失望Q这里简单介l一下动态类装蝲的基本思想?
RMI q行时系l采用动态类装蝲机制来装载分布式应用所需的类。如果你可以直接讉K应用所涉及的所有包括服务器端客L(fng)在内的主机,q且可以把分布式应用所需的所有类都安装在每个L?CLASSPATH
中(上面的示例就是极端情况,所有的东西都在本地LQ,那么你完全不必关?RMI c装载的l节。显Ӟ既然是分布式应用Q情况往往正相反。对?RMI 应用Q客L(fng)需要装载客L(fng)自n所需的类Q将要调用的q程对象的远E接口类以及对应?Stub c;服务器端则要装蝲q程对象的实现类以及对应?Skeleton c(Java 1.2 之后不需?Skeleton c)。RMI 在处理远E调用涉及的q程引用Q参C及返回值时Q可以将一个指定的 URL ~码到流中。交互的另一端可以通过 ?URL 获得处理q些对象所需的类文g。这一点类g Applet 中的 CODEBASE 的概念,交互的两端通过 HTTP 服务器发布各自控制的c,允许交互的另一端动态下载这些类。以我们的示例ؓ例,客户端不必部|?ComputeEngine_Stub 的类文gQ而可以通过服务器端?HTTP 服务器获得类文g。同P服务器端也不需要客L(fng)实现的定制Q?Pi 的类文g?/p>
注意Q这U动态类装蝲需要交互的两端加蝲定制的安全管理器Q参?java.rmi.RMISecurityManager APIQ,以及对应的策略文件?/p>
David Flanagan, Jim Farley, William Crawford and Kris Magnusson, 1999, ISBN 1-56592-483-5E, O'Reilly, Java?Enterprise in a Nutshell
Ed Roman, Scott Ambler and Tyler Jewell 2002, ISBN 0-471-41711-4, John Wiley &Sons, Inc., Matering Enterprise JavaBeans?/i> , Second Edition
关键字:正则表达? 模式匚w Javascript
摘要Q收集一些常用的正则表达式?/p>
正则表达式用于字W串处理Q表单验证等场合Q实用高效,但用到时L不太把握Q以致往往要上|查一番。我一些常用的表达式收藏在q里Q作备忘之用。本贴随时会更新?/p>
匚w中文字符的正则表辑ּQ?[\u4e00-\u9fa5]
匚w双字节字W?包括汉字在内)Q[^\x00-\xff]
应用Q计字W串的长度(一个双字节字符长度?QASCII字符?Q?/font>
String.prototype.len=function(){return this.replace([^\x00-\xff]/g,"aa").length;}
匚wI的正则表辑ּQ\n[\s| ]*\r
匚wHTML标记的正则表辑ּQ?<(.*)>.*<\/\1>|<(.*) \/>/
匚w首尾I格的正则表辑ּQ?^\s*)|(\s*$)
String.prototype.trim = function()
{
return this.replace(/(^\s*)|(\s*$)/g, "");
}
利用正则表达式分解和转换IP地址Q?/font>
下面是利用正则表辑ּ匚wIP地址QƈIP地址转换成对应数值的JavascriptE序Q?/font>
function IP2V(ip)
{
re=/(\d+)\.(\d+)\.(\d+)\.(\d+)/g //匚wIP地址的正则表辑ּ
if(re.test(ip))
{
return RegExp.$1*Math.pow(255,3))+RegExp.$2*Math.pow(255,2))+RegExp.$3*255+RegExp.$4*1
}
else
{
throw new Error("Not a valid IP address!")
}
}
不过上面的程序如果不用正则表辑ּQ而直接用split函数来分解可能更单,E序如下Q?/font>
var ip="10.100.20.168"
ip=ip.split(".")
alert("IP值是Q?+(ip[0]*255*255*255+ip[1]*255*255+ip[2]*255+ip[3]*1))
匚wEmail地址的正则表辑ּQ\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*
匚w|址URL的正则表辑ּQhttp://([\w-]+\.)+[\w-]+(/[\w- ./?%&=]*)?
利用正则表达式去除字串中重复的字W的法E序Q[注:此程序不正确Q原因见本脓(chung)回复]
var s="abacabefgeeii"
var s1=s.replace(/(.).*\1/g,"$1")
var re=new RegExp("["+s1+"]","g")
var s2=s.replace(re,"")
alert(s1+s2) //l果为:abcefgi
我原来在CSDN上发贴寻求一个表辑ּ来实现去除重复字W的Ҏ(gu)Q最l没有找刎ͼq是我能惛_的最单的实现Ҏ(gu)。思\是用后向引用取出包括重复的字符Q再以重复的字符建立W二个表辑ּQ取C重复的字W,两者串q。这个方法对于字W顺序有要求的字W串可能不适用?/font>
得用正则表达式从URL地址中提取文件名的javascriptE序Q如下结果ؓpage1
s="http://www.9499.net/page1.htm"
s=s.replace(/(.*\/){0,}([^\.]+).*/ig,"$2")
alert(s)
利用正则表达式限制网表单里的文本框输入内容Q?/font>
用正则表辑ּ限制只能输入中文Qonkeyup="value=value.replace(/[^\u4E00-\u9FA5]/g,'')" onbeforepaste="clipboardData.setData('text',clipboardData.getData('text').replace(/[^\u4E00-\u9FA5]/g,''))"
用正则表辑ּ限制只能输入全角字符Q?/font> onkeyup="value=value.replace(/[^\uFF00-\uFFFF]/g,'')" onbeforepaste="clipboardData.setData('text',clipboardData.getData('text').replace(/[^\uFF00-\uFFFF]/g,''))"
用正则表辑ּ限制只能输入数字Qonkeyup="value=value.replace(/[^\d]/g,'') "onbeforepaste="clipboardData.setData('text',clipboardData.getData('text').replace(/[^\d]/g,''))"
用正则表辑ּ限制只能输入数字和英文:onkeyup="value=value.replace(/[\W]/g,'') "onbeforepaste="clipboardData.setData('text',clipboardData.getData('text').replace(/[^\d]/g,''))"
使用JXLdExcel表格,拯、更新Excel工作?/b> | ||
xymiser 原创 (参与分:41669Q专家分Q?761) 发表Q?006-01-18 22:11 版本Q?.0 阅读Q?b>1666? |
/** * <p>dExcel表格,拯、更新Excel工作薄?lt;/p> * <p>Description: 可以dExcel文g的内?更新Excel工作?br />* </p> * <p>Copyright: Copyright (c) Corparation 2005</p> * <p>E序开发环境ؓeclipse</p> * @author Walker * @version 1.0 */ package cn.com.yitong.xls; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.util.Vector; import cn.com.yitong.ChartImg; import cn.com.yitong.VireObj; import cn.com.yitong.platform.log.YTLogger; import jxl.CellType; import jxl.Workbook; import jxl.format.CellFormat; import jxl.format.Colour; import jxl.format.UnderlineStyle; import jxl.write.Formula; import jxl.write.Label; import jxl.write.Number; import jxl.write.WritableCell; import jxl.write.WritableCellFormat; import jxl.write.WritableFont; import jxl.write.WritableImage; import jxl.write.WritableSheet; import jxl.write.WritableWorkbook; import jxl.write.WriteException; import jxl.write.biff.RowsExceededException; public class XLSDemo { private static final int TITLE_LENGTH = 7; private static final int SHEET_WIDTH = 32; private static final int SHEET_HEIGHT = 116; /** * 创徏Excel */ private void makeXls() { Workbook workbook = null; try { // 构徏Workbook对象, 只读Workbook对象 // 直接从本地文件创建Workbook, 从输入流创徏Workbook InputStream ins = new FileInputStream("D:/Workspace/testproj/source.xls"); workbook = Workbook.getWorkbook(ins); // 利用已经创徏的Excel工作薄创建新的可写入的Excel工作?br /> File outFile = new File("D:/Workspace/testproj/test.xls"); WritableWorkbook wwb = Workbook.createWorkbook(outFile, workbook); // dW一张工作表 WritableSheet dataSheet = wwb.getSheet(0); // 讄ȝ单元?br /> dataSheet.getSettings().setVerticalFreeze(7); dataSheet.getSettings().setHorizontalFreeze(2); // 试模拟数据 Vector vecData = new Vector(); for(int i = 0; i < 50; i ++) { VireObj obj = new VireObj(); obj.setOrgNo("00" + i + "0"); obj.setOrgName("机构" + (i + 1)); obj.setOpenAcc((int)(100 * Math.random())); obj.setDestoryAcc((int)(10 * Math.random())); obj.setTotalAcc((int)(500 * Math.random())); obj.setMonthInCount((int)(500 * Math.random())); obj.setMonthInMoney(500 * Math.random()); obj.setMonthOutCount((int)(500 * Math.random())); obj.setMonthOutMoney(500 * Math.random()); vecData.add(obj); } // 插入数据 insertData(wwb, dataSheet, vecData); // 插入模拟囑փ数据 Vector vecImg = new Vector(); for(int i = 0; i < 3; i ++) { ChartImg img = new ChartImg(); img.setImgTitle("囑փ" + (i + 1)); img.setImgName("D:/Workspace/testproj/images/barchart.png"); vecImg.add(img); } // 插入图表 insertImgsheet(wwb, vecImg); //写入Excel对象 wwb.write(); wwb.close(); } catch (Exception e) { YTLogger.logDebug(e); } finally { // 操作完成Ӟ关闭对象Q释攑֍用的内存I间 workbook.close(); } } /** * 插入数据 * @param wwb WritableWorkbook : 工作?br /> * @param dataSheet WritableSheet : 工作?br /> * @throws RowsExceededException * @throws WriteException */ private void insertData(WritableWorkbook wwb, WritableSheet dataSheet, Vector vecData) throws RowsExceededException, WriteException { // 获得标题单元格对象 ?br /> modiStrCell(dataSheet, 2, 0, "工商银行江苏省分行 个人网上银行业务种c?开销hl报表(2005-12Q?, null); // 修改数据单元格数?br /> for(int i = 0; i < vecData.size(); i ++) { VireObj obj = (VireObj)vecData.get(i); modiStrCell(dataSheet, 0, TITLE_LENGTH + i, obj.getOrgNo(), null); modiStrCell(dataSheet, 1, TITLE_LENGTH + i, obj.getOrgName(), null); modiNumCell(dataSheet, 2, TITLE_LENGTH + i, obj.getOpenAcc(), null); modiNumCell(dataSheet, 3, TITLE_LENGTH + i, obj.getDestoryAcc(), null); modiNumCell(dataSheet, 4, TITLE_LENGTH + i, obj.getTotalAcc(), null); modiNumCell(dataSheet, 5, TITLE_LENGTH + i, obj.getMonthInCount(), null); modiNumCell(dataSheet, 6, TITLE_LENGTH + i, obj.getTotalInMoney(), null); modiNumCell(dataSheet, 7, TITLE_LENGTH + i, obj.getMonthOutCount(), null); modiNumCell(dataSheet, 8, TITLE_LENGTH + i, obj.getMonthOutMoney(), null); } // 删除I for (int j = vecData.size() + TITLE_LENGTH; j < SHEET_HEIGHT; j++) { dataSheet.removeRow(vecData.size() + TITLE_LENGTH); } // 插入公式 for(int i = 2; i < SHEET_WIDTH; i ++) { modiFormulaCell(dataSheet, i, vecData.size() + TITLE_LENGTH, 8, vecData.size() + TITLE_LENGTH, null); } } /** * 修改字符单元格的?br /> * @param dataSheet WritableSheet : 工作?br /> * @param col int : ?br /> * @param row int : ?br /> * @param str String : 字符 * @param format CellFormat : 单元格的样式 * @throws RowsExceededException * @throws WriteException */ private void modiStrCell(WritableSheet dataSheet, int col, int row, String str, CellFormat format) throws RowsExceededException, WriteException { // 获得单元格对?br /> WritableCell cell = dataSheet.getWritableCell(col, row); // 判断单元格的cd, 做出相应的{?br /> if (cell.getType() == CellType.EMPTY) { Label lbl = new Label(col, row, str); if(null != format) { lbl.setCellFormat(format); } else { lbl.setCellFormat(cell.getCellFormat()); } dataSheet.addCell(lbl); } else if (cell.getType() == CellType.LABEL) { Label lbl = (Label)cell; lbl.setString(str); } else if (cell.getType() == CellType.NUMBER) { // 数字单元g?br /> Number n1 = (Number)cell; n1.setValue(42.05); } } /** * 修改数字单元格的?br /> * @param dataSheet WritableSheet : 工作?br /> * @param col int : ?br /> * @param row int : ?br /> * @param num double : 数?br /> * @param format CellFormat : 单元格的样式 * @throws RowsExceededException * @throws WriteException */ private void modiNumCell(WritableSheet dataSheet, int col, int row, double num, CellFormat format) throws RowsExceededException, WriteException { // 获得单元格对?br /> WritableCell cell = dataSheet.getWritableCell(col, row); // 判断单元格的cd, 做出相应的{?br /> if (cell.getType() == CellType.EMPTY) { Number lbl = new Number(col, row, num); if(null != format) { lbl.setCellFormat(format); } else { lbl.setCellFormat(cell.getCellFormat()); } dataSheet.addCell(lbl); } else if (cell.getType() == CellType.NUMBER) { // 数字单元g?br /> Number lbl = (Number)cell; lbl.setValue(num); } else if (cell.getType() == CellType.LABEL) { Label lbl = (Label)cell; lbl.setString(String.valueOf(num)); } } /** * 修改公式单元格的?br /> * @param dataSheet WritableSheet : 工作?br /> * @param col int : ?br /> * @param row int : ?br /> * @param startPos int : 开始位|?br /> * @param endPos int : l束位置 * @param format * @throws RowsExceededException * @throws WriteException */ private void modiFormulaCell(WritableSheet dataSheet, int col, int row, int startPos, int endPos, CellFormat format) throws RowsExceededException, WriteException { String f = getFormula(col, row, startPos, endPos); // 插入公式Q只支持插入Q不支持修改Q?br /> WritableCell cell = dataSheet.getWritableCell(col, row); if (cell.getType() == CellType.EMPTY) { // 公式单元?br /> Formula lbl = new Formula(col, row, f); if(null != format) { lbl.setCellFormat(format); } else { lbl.setCellFormat(cell.getCellFormat()); } dataSheet.addCell(lbl); } else if (cell.getType() == CellType.STRING_FORMULA) { YTLogger.logWarn("Formula modify not supported!"); } } /** * 得到公式 * @param col int : ?br /> * @param row int : ?br /> * @param startPos int : 开始位|?br /> * @param endPos int : l束位置 * @return String * @throws RowsExceededException * @throws WriteException */ private String getFormula(int col, int row, int startPos, int endPos) throws RowsExceededException, WriteException { char base = 'A'; char c1 = base; StringBuffer formula = new StringBuffer(128); // l装公式 formula.append("SUM("); if (col <= 25) { c1 = (char) (col % 26 + base); formula.append(c1).append(startPos).append(":") .append(c1).append(endPos).append(")"); } else if (col > 25) { char c2 = (char) ((col - 26) / 26 + base); c1 = (char) ((col - 26) % 26 + base); formula.append(c2).append(c1).append(startPos).append(":") .append(c2).append(c1).append(endPos).append(")"); } return formula.toString(); } /** * 插入图表工作?br /> * @param wwb WritableWorkbook : 工作?br /> * @param vecImg Vector : 囑փ链表 * @throws RowsExceededException * @throws WriteException */ private void insertImgsheet(WritableWorkbook wwb, Vector vecImg) throws RowsExceededException, WriteException { // 插入囑փ WritableSheet imgSheet; if((wwb.getSheets()).length < 2) { imgSheet = wwb.createSheet("图表", 1); } else { imgSheet = wwb.getSheet(1); } for (int i = 0; i < vecImg.size(); i++) { ChartImg chart = (ChartImg) vecImg.get(i); // 插入囑փ标题 Label lbl = new Label(0, 2 + 20 * i, chart.getImgTitle()); WritableFont font = new WritableFont(WritableFont.ARIAL, WritableFont.DEFAULT_POINT_SIZE, WritableFont.NO_BOLD, false, UnderlineStyle.NO_UNDERLINE, Colour.DARK_BLUE2); WritableCellFormat background = new WritableCellFormat(font); background.setWrap(true); background.setBackground(Colour.GRAY_25); imgSheet.mergeCells(0, 2 + 20 * i, 9, 2 + 20 * i); lbl.setCellFormat(background); imgSheet.addCell(lbl); // 插入囑փ单元?br /> insertImgCell(imgSheet, 2, 4 + 20 * i, 8, 15, chart.getImgName()); } } /** * 插入囑փ到单元格Q图像格式只支持pngQ?br /> * @param dataSheet WritableSheet : 工作?br /> * @param col int : ?br /> * @param row int : ?br /> * @param width int : ?br /> * @param height int : ?br /> * @param imgName String : 囑փ的全路径 * @throws RowsExceededException * @throws WriteException */ private void insertImgCell(WritableSheet dataSheet, int col, int row, int width, int height, String imgName) throws RowsExceededException, WriteException { File imgFile = new File(imgName); WritableImage img = new WritableImage(col, row, width, height, imgFile); dataSheet.addImage(img); } /** * 试 * @param args */ public static void main(String[] args) { XLSDemo demo = new XLSDemo(); demo.makeXls(); } } |
![]() |
Import java.io. *; //Java基础包,包含各种IO操作 Import java.util. *; //Java基础包,包含各种标准数据l构操作 Import javax.xml.parsers. *; //XML解析器接? Import org.w3c.dom. *; //XML的DOM实现 import org.apache.crimson.tree.XmlDocument;//写XML文g要用?br />Import javax.xml.transform. *; Import javax.xml.transform.dom. *; Import javax.xml.transform.stream. *; |
Private Boolean is Merging (String mainFileName, String sub Filename) throws Exception { Boolean isOver = false; DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); Document Builder db = null; Try { Db = dbf.newDocumentBuilder (); } Catch (ParserConfigurationException pce) { System.err.println(pce); //出现异常Ӟ输出异常信息 } Document doc_main = null,doc_vice = null; //获取两个XML文g的Document?br /> Try { Doc_main = db.parse (mainFileName); Doc_vice = db.parse (sub Filename); } Catch (DOM Exception dom) { System.err.println (dom.getMessage ()); } Catch (Exception ioe) { System.err.println (ioe); } //获取两个文g的根元素?br /> Element root_main = doc_main.getDocumentElement (); Element root_vice = doc_vice.getDocumentElement (); //下面d被合q文件根节点下的每个元素 Novelist message Items = root_vice.getChildNodes (); Int item_number = messageItems.getLength (); //如果L根节点下的第一个元素,比如<所属管理系l?gt; Q那么i?开始。否则i?开始?br /> For (int i=1; i < item_number; i=i+2 ) { //调用dupliate()Q依ơ复制被合ƈXML文档中根节点下的元素? Element messageItem = (Element) messageItems.item (i); IsOver = dupliate (doc_main, root_main, messageItem); } //调用 write To()Q将合ƈ得到的Document写入目标XML文档?br /> Boolean isWritten = write To (doc_main, mainFileName); Return isOver && isWritten; } |
Private Boolean dupliate (Document doc_dup, Element father, Element son) throws Exception { Boolean is done = false; String son_name = son.getNodeName (); Element sub ITEM = doc_dup.createElement (son_name); //复制节点的属?br /> If (son.hasAttributes ()){ NamedNodeMap attributes = son.getAttributes (); For (int i=0; i < attributes.getLength () ; i ++){ String attribute_name = attributes. Item (i). GetNodeName (); String attribute_value = attributes. Item (i). GetNodeValue (); SubITEM.setAttribute (attribute_name, attribute_value); } } Father.appendChild (sub ITEM); //复制节点的?br /> Text value son = (Text) son.getFirstChild (); String nodevalue_root = ""; If (value_son! = null && value_son.getLength () > 0) nodevalue_root = (String) value_son.getNodeValue (); Text valuenode_root = null; If ((nodevalue_root! = null)&&(nodevalue_root.length () > 0)) valuenode_root = doc_dup.createTextNode (nodevalue_root); If (valuenode_root! = null && valuenode_root.getLength () > 0) subITEM.appendChild (valuenode_root); //复制子结?br /> Novelist sub_messageItems = son.getChildNodes (); int sub_item_number = sub_messageItems.getLength(); if (sub_item_number < 2){ //如果没有子节?则返?br /> Is done = true; } Else { For (int j = 1; j < sub_item_number; j=j+2) { //如果有子节点,则递归调用本方? Element sub_messageItem = (Element) sub_messageItems.item (j); Is done = dupliate (doc_dup, subITEM, sub_messageItem); } } Return is done; } |
Private Boolean write To (Document doc, String fileName) throws Exception { Boolean isOver = false; DOM Source doms = new DOM Source (doc); File f = new File (fileName); Stream Result sr = new Stream Result (f); Try { Transformer Factory tf=TransformerFactory.newInstance (); Transformer t=tf.newTransformer (); Properties properties = t.getOutputProperties (); Properties.setProperty (OutputKeys.ENCODING,"GB2312"); T.setOutputProperties (properties); T.transform (doms, sr); IsOver = true; } Catch (TransformerConfigurationException tce) { Tce.printStackTrace (); } Catch (Transformer Exception te) { Te.printStackTrace (); } Return isOver; } |
Public static void main (String [] args) throws Exception { Boolean is done = is Merging ("D:/a.xml","D:/b.xml"); If (is Done) System.out.println ("XML files have been merged."); Else System.out.println ("XML files have NOT been merged."); } |
作者: 车东 Email: chedongATbigfoot.com/chedongATchedong.com
写于Q?002/08 最后更斎ͼ
02/22/2006 14:42:55
Feed Back >> (Read this before you ask question)
版权声明Q可以Q意{载,转蝲时请务必以超链接形式标明文章原始出处和作者信息及本声?br />http://www.chedong.com/tech/lucene.html
关键词:Lucene java full-text search engine Chinese word segment
内容摘要Q?/p>
Lucene是一个基于Java的全文烦引工具包?/p>
Lucene不是一个完整的全文索引应用Q而是是一个用Java写的全文索引引擎工具包,它可以方便的嵌入到各U应用中实现针对应用的全文烦?索功能?/p>
Lucene的作者:Lucene的A(ch)献?a >Doug Cutting是一位资深全文烦?索专Ӟ曄是V-Twin搜烦引擎(Apple的Copland操作pȝ的成׃一)的主要开发者,后在Excite担Q高pȝ架构设计师,目前从事于一些INTERNET底层架构的研I。他贡献出的Lucene的目标是为各U中型应用E序加入全文索功能?/p>
Lucene的发展历E:早先发布在作者自qwww.lucene.comQ后来发布在SourceForgeQ?001q年底成为APACHE基金会jakarta的一个子目Q?a >http://jakarta.apache.org/lucene/
已经有很多Java目都用了Lucene作ؓ其后台的全文索引引擎Q比较著名的有:
Eclipse:ZJava的开攑ּ发^収ͼ帮助部分的全文烦引用了Lucene
对于中文用户来说Q最兛_的问题是其是否支持中文的全文索。但通过后面对于Lucene的结构的介绍Q你会了解到׃Lucene良好架构设计Q对中文的支持只需对其语言词法分析接口q行扩展p实现对中文检索的支持?/p>
Lucene的API接口设计的比较通用Q输入输出结构都很像数据库的?=>记录==>字段Q所以很多传l的应用的文件、数据库{都可以比较方便的映到Lucene的存储结?接口中。M上看Q可以先?b>Lucene当成一个支持全文烦引的数据库系l?/b>?/p>
比较一下Lucene和数据库Q?/p>
Lucene | 数据?/td> |
索引数据源:doc(field1,field2...) doc(field1,field2...) |
索引数据源:record(field1,field2...) record(field1..) |
DocumentQ一个需要进行烦引的“单元?br />一个Document由多个字D늻?/td> | RecordQ记录,包含多个字段 |
FieldQ字D?/td> | FieldQ字D?/td> |
HitsQ查询结果集Q由匚w的Documentl成 | RecordSetQ查询结果集Q由多个Recordl成 |
全文??like "%keyword%"
通常比较厚的书籍后面常常附关键词索引表(比如Q北京:12, 34,上vQ?,77……)Q它能够帮助读者比较快地找到相兛_容的늠。而数据库索引能够大大提高查询的速度原理也是一P惛_一下通过书后面的索引查找的速度要比一一地d定w多少倍……而烦引之所以效率高Q另外一个原因是它是排好序的?b>对于索系l来说核心是一个排序问?/b>?/p>
׃数据库烦引不是ؓ全文索引设计的,因此Q?b>使用like "%keyword%"Ӟ数据库烦引是不v作用?/b>Q在使用like查询Ӟ搜烦q程又变成类g一页M的遍历过E了Q所以对于含有模p查询的数据库服务来_LIKEҎ(gu)能的危x极大的。如果是需要对多个关键词进行模p匹配:like"%keyword1%" and like "%keyword2%" ...其效率也可惌知了?/p>
所以徏立一个高效检索系l的关键是徏立一个类gU技索引一L(fng)反向索引机制Q将数据源(比如多篇文章Q排序顺序存储的同时Q有另外一个排好序的关键词列表Q用于存储关键词==>文章映射关系Q利用这L(fng)映射关系索引Q[关键?=>出现关键词的文章~号Q出现次敎ͼ甚至包括位置Qv始偏U量Q结束偏U量Q,出现频率]Q检索过E就是把模糊查询变成多个可以利用索引的精查询的逻辑l合的过E?/b>。从而大大提高了多关键词查询的效率,所以,全文索问题归l到最后是一个排序问题?/p>
由此可以看出模糊查询相对数据库的_查询是一个非怸定的问题,q也是大部分数据库对全文索支持有限的原因。Lucene最核心的特征是通过Ҏ(gu)的烦引结构实C传统数据库不擅长的全文烦引机Ӟq提供了扩展接口Q以方便针对不同应用的定制?/p>
可以通过一下表格对比一下数据库的模p查询:
Lucene全文索引引擎 | 数据?/td> | |
索引 | 数据源中的数据都通过全文索引一一建立反向索引 | 对于LIKE查询来说Q数据传l的索引是根本用不上的。数据需要逐个便利记录q行GREP式的模糊匚wQ比有烦引的搜烦速度要有多个数量U的下降?/td> |
匚w效果 | 通过词元(term)q行匚wQ通过语言分析接口的实玎ͼ可以实现对中文等非英语的支持?/td> | 使用Qlike "%net%" 会把netherlands也匹配出来, 多个关键词的模糊匚wQ用like "%com%net%"Q就不能匚w词序颠倒的xxx.net..xxx.com |
匚w?/td> | 有匹配度法Q将匚wE度Q相似度Q比较高的结果排在前面?/td> | 没有匚wE度的控Ӟ比如有记录中net出现5词和出现1ơ的Q结果是一L(fng)?/td> |
l果输出 | 通过特别的算法,最匚w度最高的?00条结果输出,l果集是~冲式的批量读取的?/td> | q回所有的l果集,在匹配条目非常多的时候(比如上万条)需要大量的内存存放q些临时l果集?/td> |
可定制?/td> | 通过不同的语a分析接口实现Q可以方便的定制出符合应用需要的索引规则Q包括对中文的支持) | 没有接口或接口复杂,无法定制 |
l论 | 高负载的模糊查询应用Q需要负责的模糊查询的规则,索引的资料量比较?/td> | 使用率低Q模p匹配规则简单或者需要模p查询的资料量少 |
全文索和数据库应用最大的不同在于Q让
最相关?/span>
?00条结果满?8%以上用户的需?br />
Lucene的创C处:
大部分的搜烦Q数据库Q引擎都是用B?wi)结构来l护索引Q烦引的更新会导致大量的IO操作QLucene在实CQ对此稍微有所改进Q不是维护一个烦引文Ӟ而是在扩展烦引的时候不断创建新的烦引文Ӟ然后定期的把q些新的烦引文件合q到原先的大索引中(针对不同的更新策略,Ҏ(gu)的大可以调_Q这样在不媄响检索的效率的前提下Q提高了索引的效率?/p>
Lucene和其他一些全文检索系l?应用的比较:
Lucene | 其他开源全文检索系l?/td> | |
增量索引和批量烦?/td> | 可以q行增量的烦?Append)Q可以对于大量数据进行批量烦引,q且接口设计用于优化扚w索引和小扚w的增量烦引?/td> | 很多pȝ只支持批量的索引Q有时数据源有一点增加也需要重建烦引?/td> |
数据?/td> | Lucene没有定义具体的数据源Q而是一个文档的l构Q因此可以非常灵zȝ适应各种应用Q只要前端有合适的转换器把数据源{换成相应l构Q, | 很多pȝ只针对网,~Z其他格式文档的灵zL?/td> |
索引内容抓取 | Lucene的文档是由多个字D늻成的Q甚臛_以控刉些字D需要进行烦引,那些字段不需要烦引,q一步烦引的字段也分为需要分词和不需要分词的cdQ?br /> 需要进行分词的索引Q比如:标题Q文章内容字D?br /> 不需要进行分词的索引Q比如:作?日期字段 | ~Z通用性,往往文档整个烦引了 |
语言分析 | 通过语言分析器的不同扩展实现Q?br />可以qo掉不需要的词:an the of {, 西文语法分析Q将jumps jumped jumper都归l成jumpq行索引/?br />非英文支持:对亚z语aQ阿拉伯语言的烦引支?/td> | ~Z通用接口实现 |
查询分析 | 通过查询分析接口的实玎ͼ可以定制自己的查询语法规则: 比如Q?多个关键词之间的 + - and or关系{?/td> | |
q发讉K | 能够支持多用L(fng)使用 |
对于中文来说Q全文烦引首先还要解决一个语a分析的问题,对于英文来说Q语句中单词之间是天焉过I格分开的,但亚z语a的中日韩文语句中的字是一个字挨一个,所有,首先要把语句中按“词”进行烦引的话,q个词如何切分出来就是一个很大的问题?/p>
首先Q肯定不能用单个字符?si-gram)为烦引单元,否则查“上”时Q不能让含有“v上”也匚w?/p>
但一句话Q“北京天安门”,计算机如何按照中文的语言?fn)惯q行切分呢?
“北?天安门?q是“北 ?天安门”?让计机能够按照语言?fn)惯q行切分Q往往需要机器有一个比较丰富的词库才能够比较准的识别句中的单词?/p>
另外一个解决的办法是采用自动切分算法:单词按?元语?bigram)方式切分出来Q比如:
"北京天安? ==> "北京 京天 天安 安门"?/p>
q样Q在查询的时候,无论是查?北京" q是查询"天安?Q将查询词组按同L(fng)规则q行切分Q?北京"Q?天安安门"Q多个关键词之间按与"and"的关pȝ合,同样能够正确地映到相应的烦引中。这U方式对于其他亚z语aQ韩文,日文都是通用的?/p>
Z自动切分的最大优Ҏ(gu)没有词表l护成本Q实现简单,~点是烦引效率低Q但对于中小型应用来_Z2元语法的切分q是够用的。基?元切分后的烦引一般大和源文件差不多Q而对于英文,索引文g一般只有原文g?0%-40%不同Q?/p>
|
自动切分 | 词表切分 |
实现 | 实现非常?/td> | 实现复杂 |
查询 | 增加了查询分析的复杂E度Q?/td> | 适于实现比较复杂的查询语法规?/td> |
存储效率 | 索引冗余大,索引几乎和原文一样大 | 索引效率高,为原文大的30Q左?/td> |
l护成本 | 无词表维护成?/td> | 词表l护成本非常高:中日韩等语言需要分别维护?br />q需要包括词频统计等内容 |
适用领域 | 嵌入式系l:q行环境资源有限 分布式系l:无词表同步问?br />多语a环境Q无词表l护成本 |
Ҏ(gu)询和存储效率要求高的专业搜烦引擎 |
目前比较大的搜烦引擎的语a分析法一般是Z以上2个机制的l合。关于中文的语言分析法Q大家可以在Google查关键词"wordsegment search"能找到更多相关的资料?/p>
下蝲Q?a >http://jakarta.apache.org/lucene/
注意QLucene中的一些比较复杂的词法分析是用JavaCC生成的(JavaCCQJavaCompilerCompilerQ纯Java的词法分析生成器Q,所以如果从源代码编译或需要修改其中的QueryParser、定制自q词法分析器,q需要从https://javacc.dev.java.net/下蝲javacc?/p>
lucene的组成结构:对于外部应用来说索引模块(index)和检索模?search)是主要的外部应用入口
org.apache.Lucene.search/ | 搜烦入口 |
org.apache.Lucene.index/ | 索引入口 |
org.apache.Lucene.analysis/ | 语言分析?/td> |
org.apache.Lucene.queryParser/ | 查询分析?/td> |
org.apache.Lucene.document/ | 存储l构 |
org.apache.Lucene.store/ | 底层IO/存储l构 |
org.apache.Lucene.util/ | 一些公用的数据l构 |
单的例子演示一下Lucene的用方法:
索引q程Q从命o行读取文件名Q多个)Q将文g分\?path字段)和内?body字段)2个字D进行存储,q对内容q行全文索引Q烦引的单位是Document对象Q每个Document对象包含多个字段Field对象Q针对不同的字段属性和数据输出的需求,对字D还可以选择不同的烦?存储字段规则Q列表如下:Ҏ(gu) | 切词 | 索引 | 存储 | 用?/th> |
---|---|---|---|---|
Field.Text(String name, String value) | Yes | Yes | Yes | 切分词烦引ƈ存储Q比如:标题Q内容字D?/td> |
Field.Text(String name, Reader value) | Yes | Yes | No | 切分词烦引不存储Q比如:META信息Q?br />不用于返回显C,但需要进行检索内?/td> |
Field.Keyword(String name, String value) | No | Yes | Yes | 不切分烦引ƈ存储Q比如:日期字段 |
Field.UnIndexed(String name, String value) | No | No | Yes | 不烦引,只存储,比如Q文件\?/td> |
Field.UnStored(String name, String value) | Yes | Yes | No | 只全文烦引,不存?/td> |
public class IndexFiles {
//使用Ҏ(gu)Q? IndexFiles [索引输出目录] [索引的文件列表] ...
public static void main(String[] args) throws Exception {
String indexPath = args[0];
IndexWriter writer;
//用指定的语言分析器构造一个新的写索引器(W?个参数表C是否ؓq加索引Q?br /> writer = new IndexWriter(indexPath, new SimpleAnalyzer(), false);
for (int i=1; i<args.length; i++) {
System.out.println("Indexing file " + args[i]);
InputStream is = new FileInputStream(args[i]);
//构造包?个字DField的Document对象
//一个是路径path字段Q不索引Q只存储
//一个是内容body字段Q进行全文烦引,q存?br /> Document doc = new Document();
doc.add(Field.UnIndexed("path", args[i]));
doc.add(Field.Text("body", (Reader) new InputStreamReader(is)));
//文档写入烦?br /> writer.addDocument(doc);
is.close();
};
//关闭写烦引器
writer.close();
}
}
索引q程中可以看刎ͼ
索过E和l果昄Q?/p>
搜烦l果q回的是Hits对象Q可以通过它再讉KDocument==>Field中的内容?/p>
假设Ҏ(gu)body字段q行全文索,可以查询结果的path字段和相应查询的匚w?score)打印出来Q?/p>
public class Search {在整个检索过E中Q语a分析器,查询分析器,甚至搜烦器(SearcherQ都是提供了抽象的接口,可以Ҏ(gu)需要进行定制?
public static void main(String[] args) throws Exception {
String indexPath = args[0], queryString = args[1];
//指向索引目录的搜索器
Searcher searcher = new IndexSearcher(indexPath);
//查询解析器:使用和烦引同L(fng)语言分析?br /> Query query = QueryParser.parse(queryString, "body",
new SimpleAnalyzer());
//搜烦l果使用Hits存储
Hits hits = searcher.search(query);
//通过hits可以讉K到相应字D늚数据和查询的匚w?br /> for (int i=0; i<hits.length(); i++) {
System.out.println(hits.doc(i).get("path") + "; Score: " +
hits.score(i));
};
}
}
化的查询分析?/b>
个h感觉lucene成ؓJAKARTA目后,d了太多的旉用于调试日趋复杂QueryParserQ而其中大部分是大多数用户q不很熟(zhn)的Q目前LUCENE支持的语法:
Query ::= ( Clause )*
Clause ::= ["+", "-"] [<TERM> ":"] ( <TERM> | "(" Query ")")
中间的逻辑包括Qand or + - &&||{符P而且q有"短语查询"和针对西文的前缀/模糊查询{,个h感觉对于一般应用来_q些功能有一些华而不实,其实能够实现目前cM于Google的查询语句分析功能其实对于大多数用户来说已经够了。所以,Lucene早期版本的QueryParser仍是比较好的选择?/p>
d修改删除指定记录QDocumentQ?/b>
Lucene提供了烦引的扩展机制Q因此烦引的动态扩展应该是没有问题的,而指定记录的修改也似乎只能通过记录的删除,然后重新加入实现。如何删除指定的记录呢?删除的方法也很简单,只是需要在索引时根据数据源中的记录ID专门另徏索引Q然后利用IndexReader.delete(Termterm)Ҏ(gu)通过q个记录ID删除相应的Document?/p>
Ҏ(gu)某个字段值的排序功能
lucene~省是按照自q相关度算法(scoreQ进行结果排序的Q但能够Ҏ(gu)其他字段q行l果排序是一个在LUCENE的开发邮件列表中l常提到的问题,很多原先Z数据库应用都需要除了基于匹配度QscoreQ以外的排序功能。而从全文索的原理我们可以了解刎ͼM不基于烦引的搜烦q程效率都会D效率非常的低Q如果基于其他字D늚排序需要在搜烦q程中访问存储字D,速度回大大降低,因此非常是不可取的?/p>
但这里也有一个折中的解决Ҏ(gu)Q在搜烦q程中能够媄响排序结果的只有索引中已l存储的docID和scoreq?个参敎ͼ所以,Zscore以外的排序,其实可以通过数据源预先排好序,然后Ҏ(gu)docIDq行排序来实现。这样就避免了在LUCENE搜烦l果外对l果再次q行排序和在搜烦q程中访问不在烦引中的某个字D倹{?/p>
q里需要修改的是IndexSearcher中的HitCollectorq程Q?/p>
...
scorer.score(new HitCollector() {
private float minScore = 0.0f;
public final void collect(int doc, float score) {
if (score > 0.0f && // ignore zeroed buckets
(bits==null || bits.get(doc))) { // skip docs not in bits
totalHits[0]++;
if (score >= minScore) {
/* 原先QLucenedocID和相应的匚w度score例入l果命中列表中:
* hq.put(new ScoreDoc(doc, score)); // update hit queue
* 如果用doc ?1/doc 代替 scoreQ就实现了根据docID排或逆排
* 假设数据源烦引时已经按照某个字段排好了序Q而结果根据docID排序也就实现?br /> * 针对某个字段的排序,甚至可以实现更复杂的score和docID的拟合?br /> */
hq.put(new ScoreDoc(doc, (float) 1/doc ));
if (hq.size() > nDocs) { // if hit queue overfull
hq.pop(); // remove lowest in hit queue
minScore = ((ScoreDoc)hq.top()).score; // reset minScore
}
}
}
}
}, reader.maxDoc());
更通用的输入输出接?/b>
虽然lucene没有定义一个确定的输入文档格式Q但来多的h惛_使用一个标准的中间格式作ؓLucene的数据导入接口,然后其他数据Q比如PDF只需要通过解析器{换成标准的中间格式就可以q行数据索引了。这个中间格式主要以XMLZQ类似实现已l不?Q?个:
数据? WORD PDF HTML DB other
\ | | | /
XML中间格式
|
Lucene INDEX
目前q没有针对MSWord文档的解析器Q因为Word文档和基于ASCII的RTF文档不同Q需要用COM对象机制解析。这个是我在Google上查的相兌料:http://www.intrinsyc.com/products/enterprise_applications.asp
另外一个办法就是把Word文档转换成textQ?a >http://www.winfield.demon.nl/index.html
索引q程优化
索引一般分2U情况,一U是批量的索引扩展Q一U是大批量的索引重徏。在索引q程中,q不是每ơ新的DOC加入q去索引都重新进行一ơ烦引文件的写入操作Q文件I/O是一仉常消耗资源的事情Q?/p>
Lucene先在内存中进行烦引操作,q根据一定的扚wq行文g的写入。这个批ơ的间隔大Q文件的写入ơ数少Q但占用内存会很多。反之占用内存少Q但文gIO操作频繁Q烦引速度会很慢。在IndexWriter中有一个MERGE_FACTOR参数可以帮助你在构造烦引器后根据应用环境的情况充分利用内存减少文g的操作。根据我的用经验:~省Indexer是每20条记录烦引后写入一ơ,每将MERGE_FACTOR增加50倍,索引速度可以提高1倍左叟?br />
搜烦q程优化
lucene支持内存索引Q这L(fng)搜烦比基于文件的I/O有数量的速度提升?br />http://www.onjava.com/lpt/a/3273
而尽可能减少IndexSearcher的创建和Ҏ(gu)索结果的前台的缓存也是必要的?br />
Lucene面向全文索的优化在于首次索引索后Qƈ不把所有的记录QDocumentQ具体内容读取出来,而v只将所有结果中匚w度最高的?00条结果(TopDocsQ的ID攑ֈl果集缓存中q返回,q里可以比较一下数据库索:如果是一?0,000条的数据库检索结果集Q数据库是一定要把所有记录内定w取得以后再开始返回给应用l果集的。所以即使检索匹配L很多QLucene的结果集占用的内存空间也不会很多。对于一般的模糊索应用是用不到这么多的结果的Q头100条已l可以满?0%以上的检索需求?br />
如果首批~存l果数用完后q要d更后面的l果时Searcher会再ơ检索ƈ生成一个上ơ的搜烦~存数大1倍的~存Qƈ再重新向后抓取。所以如果构造一个SearcherL1Q?20条结果,Searcher其实是进行了2ơ搜索过E:?00条取完后Q缓存结果用完,Searcher重新索再构造一?00条的l果~存Q依此类推,400条缓存,800条缓存。由于每ơSearcher对象消失后,q些~存也访问那不到了,你有可能惛_l果记录~存下来Q缓存数量保证?00以下以充分利用首ơ的l果~存Q不让Lucene费多次索,而且可以分q行l果~存?br />
Lucene的另外一个特Ҏ(gu)在收集结果的q程中将匚w度低的结果自动过滤掉了。这也是和数据库应用需要将搜烦的结果全部返回不同之处?/p>
我的一些尝?/a>Q?/p> Luene的确是一个面对对象设计的典范 q些优点都是非常值得在以后的开发中学习(fn)借鉴的。作Z个通用工具包,Lunece的确l予了需要将全文索功能嵌入到应用中的开发者很多的便利?/p> 此外Q通过对Lucene的学?fn)和使用Q我也更深刻地理解了Z么很多数据库优化设计中要求,比如Q?/p> 参考资料: Apache: Lucene Project The Lucene search engine: Powerful, flexible, and free Lucene Tutorial Notes on distributed searching with Lucene 中文语言的切分词 搜烦引擎工具介绍 Lucene作者Cutting的几论文和专利 Lucene?NET实现QdotLucene Lucene作者Cutting的另外一个项目:ZJava的搜索引擎Nutch 关于Z词表和N-Gram的切分词比较
http://jakarta.apache.org/lucene/
Lucene开?用户邮g列表归档
Lucene-dev@jakarta.apache.org
Lucene-user@jakarta.apache.org
http://www.javaworld.com/javaworld/jw-09-2000/jw-0915-Lucene_p.html
http://www.darksleep.com/puff/lucene/lucene.html
http://home.clara.net/markharwood/lucene/
http://www.google.com/search?sourceid=navclient&hl=zh-CN&q=chinese+word+segment
http://searchtools.com/
http://lucene.sourceforge.net/publications.html
http://sourceforge.net/projects/dotlucene/
http://www.nutch.org/ http://sourceforge.net/projects/nutch/
http://china.nikkeibp.co.jp/cgi-bin/china/news/int/int200302100112.html
2005-01-08 Cutting在Pisa大学做的关于Lucene的讲座:非常详细的Lucene架构解说
]]>