本文主要介绍了Lucene的起源、发展、现状,以及Luence的初步应用,可以作为了解和学习Lucene的入门资料。

1.起源与发展


Lucene是一个高性能、纯Java的全文检索引擎,而且免费、开源。Lucene几乎适合于任何需要全文检索的应用,尤其是跨平台的应用。


Lucene的作者Doug Cutting是一个资深的全文检索专家,刚开始,Doug Cutting将Lucene发表在自己的主页上,2000年3月将其转移到sourceforge,于2001年10捐献给Apache,作为Jakarta的一个子工程。


2.使用现状


经过多年的发展,Lucene在全文检索领域已经有了很多的成功案例,并积累了良好的声誉。


基于Lucene的全文检索产品(Lucene本身只是一个组件,而非一个完整的应用)和应用Lucene的项目在世界各地已经非常之多,比较知名的有:


l         Eclipse:主流Java开发工具,其帮助文档采用Lucene作为检索引擎


l         Jive:知名论坛系统,其检索功能基于Lucene


l         Ifinder:出自德国的网站检索系统,基于Lucene(http://ifinder.intrafind.org/


l         MIT DSpace Federation:一个文档管理系统(http://www.dspace.org/


国内外采用Lucene作为网站全文检索引擎的也很多,比较知名的有:


l         http://www.blogchina.com/weblucene/


l         http://www.ioffer.com/


l         http://search.soufun.com/


l         http://www.taminn.com/


(更多案例,请参见http://wiki.apache.org/jakarta-lucene/PoweredBy


在所有这些案例中,开源应用占了很大一部分,但更多的还是商化业产品和网站。毫不夸张的说,Lucene的出现,极大的推动了全文检索技术在各个行业或领域中的深层次应用。


3.初步应用


前面提到,Lucene本身只是一个组件,而非一个完整的应用,所以若想让Lucene跑起来,还得在Lucene基础上进行必要的二次开发。


 


下载与安装


首先,你需要到Lucene的官方网站http://jakarta.apache.org/lucene/ 去下载一份拷贝,最新版是1.4。下载后将得到一个名为lucene-1.4-final.zip的压缩文件,将其解压,里面有一个名为lucene- 1.4-final.jar的文件,这就是Lucene组件包了,若需要在项目使用Lucene,只需要把lucene-1.4-final.jar置于类路径下即可,至于解压后的其他文件都是参考用的。


接下来,我用Eclipse建立一个工程,实现基于Lucene的建库、记录加载和记录查询等功能。



如上图所示,这是开发完成后的工程,其中有三个源文件CreateDataBase.java,InsertRecords.java,QueryRecords.java,分别实现建库、入库、检索的功能。


以下是对这三个源文件的分析。


 


建库源码及说明


 


CreateDataBase.java
 
packagecom.holen.part1;


 


importjava.io.File;


importorg.apache.lucene.analysis.standard.StandardAnalyzer;


importorg.apache.lucene.index.IndexWriter;


 


/**


 * @authorHolenChen


 *初始化检索库


 */


public classCreateDataBase{


 


    publicCreateDataBase(){  


    }


   


    public intcreateDataBase(Filefile){


       intreturnValue=0;


       if(!file.isDirectory()){


           file.mkdirs();


       }


       try{


           IndexWriterindexWriter= newIndexWriter(file,newStandardAnalyzer(),true);


           indexWriter.close();


           returnValue=1;


       }catch(Exceptionex){


           ex.printStackTrace();


       }


       returnreturnValue;


    }


   


    /**


     *传入检索库路径,初始化库


     * @paramfile


     * @return


     */


    public intcreateDataBase(Stringfile){


       return this.createDataBase(newFile(file));  


    }


 


    public static voidmain(String[]args){


       CreateDataBasetemp= newCreateDataBase();


       if(temp.createDataBase("e:\\lucene\\holendb")==1){


           System.out.println("db init succ");


       }


    }


}


 
 



 


说明:这里最关键的语句是IndexWriterindexWriter= newIndexWriter(file,newStandardAnalyzer(),true)。


 


第一个参数是库的路径,也就是说你准备把全文检索库保存在哪个位置,比如main方法中设定的“e:\\lucene\\holendb”,Lucene支持多库,且每个库的位置允许不同。


第二个参数是分析器,这里采用的是Lucene自带的标准分析器,分析器用于对整篇文章进行分词解析,这里的标准分析器实现对英文(或拉丁文,凡是由字母组成,由空格分开的文字均可)的分词,分析器将把整篇英文按空格切成一个个的单词(在全文检索里这叫切词,切词是全文检索的核心技术之一, Lucene默认只能切英文或其他拉丁文,默认不支持中日韩等双字节文字,关于中文切词技术将在后续章节重点探讨)。


第三个参数是是否初始化库,这里我设的是true,true意味着新建库或覆盖已经存在的库,false意味着追加到已经存在的库。这里新建库,所以肯定需要初始化,初始化后,库目录下只存在一个名为segments的文件,大小为1k。但是当库中存在记录时执行初始化,库中内容将全部丢失,库回复到初始状态,即相当于新建了该库,所以真正做项目时,该方法一定要慎用。


 


加载记录源码及说明


 


InsertRecords.java
 
packagecom.holen.part1;


 


importjava.io.File;


importjava.io.FileReader;


importjava.io.Reader;


importorg.apache.lucene.analysis.standard.StandardAnalyzer;


importorg.apache.lucene.document.Document;


importorg.apache.lucene.document.Field;


importorg.apache.lucene.index.IndexWriter;


 


/**


 * @authorHolenChen


 *记录加载


 */


public classInsertRecords{


 


    publicInsertRecords(){


    }


   


    public intinsertRecords(Stringdbpath,Filefile){


       intreturnValue=0;


       try{


           IndexWriterindexWriter


            = newIndexWriter(dbpath,newStandardAnalyzer(),false);


           this.addFiles(indexWriter,file);


           returnValue=1;


       }catch(Exceptionex){


           ex.printStackTrace();


       }


       returnreturnValue;


    }


   


    /**


     *传入需加载的文件名


     * @paramfile


     * @return


     */


    public intinsertRecords(Stringdbpath,Stringfile){


       return this.insertRecords(dbpath,newFile(file));


    }


   


    public voidaddFiles(IndexWriterindexWriter,Filefile){


       Documentdoc= newDocument();


       try{


           doc.add(Field.Keyword("filename",file.getName()));  


                 


           //以下两句只能取一句,前者是索引不存储,后者是索引且存储


           //doc.add(Field.Text("content",new FileReader(file))); 


           doc.add(Field.Text("content",this.chgFileToString(file)));


          


           indexWriter.addDocument(doc);


           indexWriter.close();


       }catch(Exceptionex){


           ex.printStackTrace();


       }


    }


   


    /**


     *从文本文件中读取内容


     * @paramfile


     * @return


     */


    publicStringchgFileToString(Filefile){


       StringreturnValue= null;


       StringBuffersb= newStringBuffer();


       char[]c= new char[4096];


       try{


           Readerreader= newFileReader(file);


           intn=0;


           while(true){            


              n=reader.read(c);


              if(n>0){


                  sb.append(c,0,n);


              }else{


                  break;


              }


           }


           reader.close();


       }catch(Exceptionex){


           ex.printStackTrace();


       }


       returnValue=sb.toString();


       returnreturnValue; 


    }


 


    public static voidmain(String[]args){


       InsertRecordstemp= newInsertRecords();


       Stringdbpath="e:\\lucene\\holendb";


       //holen1.txt中包含关键字"holen"和"java"


       if(temp.insertRecords(dbpath,"e:\\lucene\\holen1.txt")==1){


           System.out.println("add file1 succ");


       }


       //holen2.txt中包含关键字"holen"和"chen"


       if(temp.insertRecords(dbpath,"e:\\lucene\\holen2.txt")==1){


           System.out.println("add file2 succ");


       }  


    }


}


 
 


 



说明:这个类里面主要有3个方法insertRecords(Stringdbpath,Filefile),addFiles(IndexWriterindexWriter,Filefile),chgFileToString(Filefile)。


 


ChgFileToString方法用于读取文本型文件到一个String变量中。


 


InsertRecords方法用于加载一条记录,这里是将单个文件入全文检索库,第一个参数是库路径,第二个参数是需要入库的文件。


 


InsertRecords需要调用addFiles,addFiles是文件入库的真正执行者。AddFiles里有如下几行重点代码:


doc.add(Field.Keyword("filename",file.getName()));


注意,在Lucene里没有严格意义上表,Lucene的表是通过Field类的方法动态构建的,比如Field.Keyword ("filename",file.getName())就相当于在一条记录加了一个字段,字段名为filename,该字段的内容为 file.getName()。


 


 


常用的Field方法如下:


方法
 切词
 索引
 存储
 用途
 
Field.Text(String name, String value)
 Y
 Y
 Y
 标题,文章内容
 
Field.Text(String name, Reader value)
 Y
 Y
 N
 META信息
 
Field.Keyword(String name, String value)
 N
 Y
 Y
 作者
 
Field.UnIndexed(String name, String value)
 N
 N
 Y
 文件路径
 
Field.UnStored(String name, String value)
 Y
 Y
 N
 与第二种类似
 


 


为了更深入的了解全文检索库,我们可以将全文检索库与通常的关系型数据库(如Oracle,Mysql)作一下对比。


 


全文检索库对关系型数据库对比
 
对比项
 全文检索库(Lucene)
 关系型数据库(Oracle)
 
核心功能
 以文本检索为主,插入(insert)、删除(delete)、修改(update)比较麻烦,适合于大文本块的查询。
 插入(insert)、删除(delete)、修改(update)十分方便,有专门的SQL命令,但对于大文本块(如CLOB)类型的检索效率低下。
 

 与Oracle类似,都可以建多个库,且各个库的存储位置可以不同。
 可以建多个库,每个库一般都有控制文件和数据文件等,比较复杂。
 

 没有严格的表的概念,比如Lucene的表只是由入库时的定义字段松散组成。
 有严格的表结构,有主键,有字段类型等。
 
记录
 由于没有严格表的概念,所以记录体现为一个对象,在Lucene里记录对应的类是Document。
 Record,与表结构对应。
 
字段
 字段类型只有文本和日期两种,字段一般不支持运算,更无函数功能。


在Lucene里字段的类是Field,如document(field1,field2…)
 字段类型丰富,功能强大。


record(field1,field2…)
 
查询结果集
 在Lucene里表示查询结果集的类是Hits,如hits(doc1,doc2,doc3…)
 在JDBC为例, Resultset(record1,record2,record3...)
 



两种库对比图如下:




检索源码及说明


 


QueryRecords.java
 
packagecom.holen.part1;


 


importjava.util.ArrayList;


importorg.apache.lucene.analysis.standard.StandardAnalyzer;


importorg.apache.lucene.document.Document;


importorg.apache.lucene.queryParser.QueryParser;


importorg.apache.lucene.search.Hits;


importorg.apache.lucene.search.IndexSearcher;


importorg.apache.lucene.search.Query;


importorg.apache.lucene.search.Searcher;


 


/**


 * @authorHolenChen


 *检索查询


 */


public classQueryRecords{


 


    publicQueryRecords(){


    }


   


    /**


     *检索查询,将结果集返回


     * @paramsearchkey


     * @paramdbpath


     * @paramsearchfield


     * @return


     */


    publicArrayListqueryRecords(Stringsearchkey,Stringdbpath,Stringsearchfield){


       ArrayListlist= null;


       try{


           Searchersearcher= newIndexSearcher(dbpath);


           Queryquery


            =QueryParser.parse(searchkey,searchfield,newStandardAnalyzer());


           Hitshits=searcher.search(query);


           if(hits!= null){


              list= newArrayList();


              inttemp_hitslength=hits.length();


              Documentdoc= null;


              for(inti=0;i<temp_hitslength;i++){


                  doc=hits.doc(i);


                  list.add(doc.get("filename"));


              }


           }


       }catch(Exceptionex){


           ex.printStackTrace();


       }


       returnlist;


    }


 


    public static voidmain(String[]args){


       QueryRecordstemp= newQueryRecords();      


       ArrayListlist= null;


       list=temp.queryRecords("holen","e:\\lucene\\holendb","content");


       for(inti=0;i<list.size();i++){


           System.out.println((String)list.get(i));


       }      


    }


}


 
 


 


说明:该类中Searcher负责查询,并把查询结果以Hits对象集方式返回,Hits好比JDBC中的RecordSet,Hits是 Document的集合,每个Document相当于一条记录,Document中包含一个或多个字段,可以通过Document.get(“字段名”) 方法得到每个字段的内容。


 


通过这三个类,就完成了一个简单的基于Lucene的全文检索应用。


 


4.总结


 


Lucene十分精练纯粹,就一个jar包,引入到你的工程中,调用其接口,就可以为你的应用增添全文检索功能。


 


通过上一节的初步应用会发现,Lucene使用起来很简单,与JDBC有些类似,应用时重点掌握好IndexWriter,Document,Field,Searcher等几个类即可。


 


Lucene的结构很清晰,每个package司职一项,比如org.apache.Lucene.search负责检索, org.apache.Lucene.index索引,org.apache.Lucene.analysis切词等,且Lucene的主要动作都采用了抽象类,扩展起来十分方便。


 


相对于一些商业化全文检索,Lucene的入库速度更快。因为它的存储采取分步合并的方法,先建立小索引,待时机成熟才把小索引合并到大索引树上。因此,我们在操作应用数据时可以同步进行全文检索库的操作而不会(或许很少)影响系统的效能。


 


Lucene性能稳定,使用简单,而且开源免费,有Apache基金在后面做支撑,资金和技术力量都十分雄厚,这两年也一直是稳步更新,每次新版本的推出,业界均争相报导。


 


参考资料


 


1.  Introduction to Text Indexing with Apache Jakarta Lucene


2.  Lucene Introduction in Chinese


3.  Lucene Tutorial 

Posted on July 8, 2006 8:35 PM | | Comments (0) | TrackBacks (0)
1.9 RC1
注:lucene2.0发布版本并不是100%的和1.4.3版兼容。也就是说在你用2.0版本的Lucene开发包替换原来的1.4.3版本时,应该让你的应用程序首先和1.9的兼容。
 
使用前提:
 
 1. 编译和使用Lucene需要  Java1.4 或以上版本。
 
Lucene 1.9 在运行时的变化:
 
 1. 模糊搜索 FuzzyQuery 不再抛出 TooManyClauses 异常。当 FuzzyQuery 扩展多于  BooleanQuery.maxClauseCount 时,只有最相关的term会被重新写入query,因此避免了异常的抛出。    (Christoph)
 
 2. 把系统属性 "org.apache.lucene.lockdir" 改为    "org.apache.lucene.lockDir"。(Bernhard)
 
 3.  RangeQueries 和 FuzzyQueries 默认被转换成小写。 (as it has been the case for PrefixQueries    and WildcardQueries before).使用 setLowercaseExpandedTerms(false) 来禁止大小写自动转换的行为;同样也影响    PrefixQueries 和 WildcardQueries。(Daniel Naber)
 
 4. 在使用 MultiSearcher 的时候文档频率也可以正确计算,全局性的计算各个 subsearchers 和 indices 中。以前计算的时候只是 locally 的,每个 index 的计算是分开的,这样引发的一个问题是:在多个indices中rank 是不相等的。
    (Chuck Williams, Wolf Siberski via Otis, bug #31841)
 
 5. 在打开 IndexWriter 使用 create=true 参数,Lucene 现在只是删除index目录中属于Lucene自己的文件。( 判断文件名后缀的方式 )。原来是删除整个目录中的所有文件。(Daniel Naber and Bernhard Messer, bug #34695)
 
 6. IndexReader 的版本 ,可以通过  getCurrentVersion() 和 getVersion() 返回。以前如果是新的indexes 那么返回的是0 。现在则用系统的毫秒数来初始化。
    (Bernhard Messer via Daniel Naber)
 
 7. 一些默认的初始化值不再允许通过 system properties 来设置。相反在 IndexWriter 中新增了相关的 set/get 方法来设置相关属性。主要包括以下属性:
    在 IndexWriter 的 getter/setter 方法中:
      org.apache.lucene.writeLockTimeout, org.apache.lucene.commitLockTimeout,
      org.apache.lucene.minMergeDocs, org.apache.lucene.maxMergeDocs,
      org.apache.lucene.maxFieldLength, org.apache.lucene.termIndexInterval,
      org.apache.lucene.mergeFactor,
    还有 BooleanQuery 的 getter/setter 方法:
      org.apache.lucene.maxClauseCount
    还有 FSDirectory 的 getter/setter 方法:
      disableLuceneLocks
    (Daniel Naber)
 
 8. 修改了 FieldCacheImpl 方法使用用户提供的 IntParser 和 FloatParser,来替代使用  Integer 和 Float 的相关方法。
    (Yonik Seeley via Otis Gospodnetic)
 
 9. 高级搜索返回的 TopDocs 和 TopFieldDocs 不再规范scores。
    (Luc Vanlerberghe via Yonik Seeley, LUCENE-469)
 
1.9 的新特性:
 
 1. 增加了对压缩字段存储的支持。(patch #31149)
    (Bernhard Messer via Christoph)
 
 2. 增加了对压缩字段存储的支持。(patch #29370)
    (Bernhard Messer via Christoph)
 
 3. 在 term vectors 中增加了 位置和偏移信息。(Grant Ingersoll & Christoph)
 
 4. 增加了一个新的 DateTools 。允许用户格式化日期到一种更可读的格式,以便于更好的适应索引。DateTools 不像 DateFields 类,它允许日期指定到1970年以前,但必须使用指定的日期格式。这样,在RangeQuerys中使用就更加有效率了。
    (Daniel Naber)
 
 5. QueryParser 现在可以正确的和Analyzers 一起工作了,即可以在一个位置返回多个 Token 。 比如:查询: “+fast + car”如果 Analyzer 在同一位置返回 car 和  automobile ,那么上面的查询将被解析成:”+fast +(car automobile)”。
    (Pierrick Brihaye, Daniel Naber)
 
 6. 允许unbuffered的目录实现。(e.g.,using mmap)。
    InputStream 被新类 IndexInput 替换,    BufferedIndexInput 和  OutputStream 则被    IndexOutput 和 BufferedIndexOutput。  InputStream 和 OutputStream 已经被废弃了。FSDirectory 现在是一个子类了。(cutting)
 
 7. 增加了原生 Directory 和 TermDocs 的实现,可以工作在 GCJ 下。GCJ的版本需要 3.4.0 以上。可以使用 ant gcj 来运行例子程序。(cutting)
 
 8. 增加了 MmapDirectory 类,它使用 nio to mmap 输入文件。现在MmapDirectory 比FSDirectory 要慢些。但他对每个查询term 使用更少的内存。(cutting & Paul Elschot)
 
 9. 增加  javadocs-internal 到 build.xml – bug #30360
 
10. 增加了 RangeFileter ,比 DateFilter 更加通用,实用。
    (Chris M Hostetter via Erik)
 
11. 增加了 NumberTools ,一个用来索引数字字段的工具类。
    (adapted from code contributed by Matt Quail; committed by Erik)
 
12. 增加了 public static IndexReader.main(String[] args) 方法。
IndexReader 现在可以直接在命令行方式下使用,用来 列出或者从现存的索引中抽取单独的文件出来。
    (adapted from code contributed by Garrett Rooney; committed by Bernhard)
 
13. 增加 IndexWriter.setTermIndexInterval() 方法。
    (Doug Cutting)
 
14. 增加 LucenePackage ,这些静态的 get() 方法返回 java.util.Package。调用者可以用它来获得 Lucene jar 中的版本信息。
    (Doug Cutting via Otis)
 
15. 增加 Hits.iterator() 方法和相应的 HitIterator 和 Hit 对象。
他提供了对 Hits对象标准的 java.util.Iterator 叠代操作。
每个iterator's next() 方法返回一个  Hit 对象。
    (Jeremy Rayner via Erik)
 
16. 增加 ParallelReader,这个一种IndexReader 他合并多个单独的索引到一个单独的虚拟索引上。(Doug Cutting)
 
17. 增加对 FieldCache 的 IntParser , FloatParser 接口, 这样任何格式的字段可以被以int 和float的形式缓存。
    (Doug Cutting)
 
18. 新增类: org.apache.lucene.index.IndexModifier ,它合并了    IndexWriter 和 IndexReader,好处是我们可以增加和删除文档的时候不同担心 synchronisation/locking 的问题了。
    (Daniel Naber)
 
19. Lucene 现在可以被用在一个没有签名的applet中了,Lucene’s 读取系统属性不会抛出 SecurityException 异常。
    (Jon Schuster via Daniel Naber, bug #34359)
 
20. 增加了新类 MatchAllDocsQuery 用来匹配所有文档。
    (John Wang via Daniel Naber, bug #34946)
 
21. 当索引太多的字段时,为了消减索引大小和内存消耗,提供了忽略规范化字段的功能。
    见: Field.setOmitNorms()
    (Yonik Seeley, LUCENE-448)
 
22. 增加对 contrib/highlighter 的 NullFragmenter , 这对全文本加亮很有用。
    (Erik Hatcher)
 
23. 增加了正则表达式的查询: RegexQuery 和 SpanRegexQuery。


    (Erik Hatcher)
 
24. 增加 ConstantScoreQuery 类,它包装了一个 filter  produces a score
    equal to the query boost for every matching document.
    (Yonik Seeley, LUCENE-383)
 

25. 增加了 ConstantScoreRangeQuery  类,为某个区间的每个文档提供一个不变的 score。这个类比普通的 RangeQuery 类的好处是它并不展开到 BooleanQuery ,因此也不存在区间最大term上限。
    (Yonik Seeley, LUCENE-383)

 
26. 为BooleanQuery增加了最小的匹配短语。见:BooleanQuery.setMinimumNumberShouldMatch().
    (Paul Elschot, Chris Hostetter via Yonik Seeley, LUCENE-395)
 
27. 增加了 DisjunctionMaxQuery 类,提供了针对某个短语的最大score。
这一点对多字段的搜索非常有用。
    (Luc Vanlerberghe via Yonik Seeley, LUCENE-323)
 
28. 新增类:ISOLatin1AccentFilter ,用 ISO Latin 1 字符集中的unaccented类字符替代 accented 类字符。
    (Sven Duzont via Erik Hatcher)
 
29. 新增类:KeywordAnalyzer。"Tokenizes" 整个流作为一个单独的token。
这个类对于 邮政编码,序列号,和产品名称等比较有用。
    (Erik Hatcher)
 
30. 把  LengthFilter 类从 contrib 放到了 core 代码里。从 stream 中去掉太长和太短的单词。
    (David Spencer via Otis and Daniel)
 
31. 增加了 getPositionIncrementGap 方法到 Analyzer 中。这样用户自定义的 analyzer 可以在相同字段名的实例之间增加间隙 gaps,用来防止 phrase 和 span 查询超出边界。默认的 gap 是 0 。
    (Erik Hatcher, with advice from Yonik)
 
32. StopFilter 增加了对处理stop words 的忽略大小写处理。
    (Grant Ingersoll via Yonik, LUCENE-248)
 
33. 增加了 TopDocCollector 和 TopFieldDocCollector。用来简化实现hit 集合针对 top-scoring 和 top-sorting hits的处理。
 
API 的改变:
 
 1. 几个方法和字段已经被废弃。在API 文档中包含了建议替换的内容。在这些建议中,这些不建议使用的方法和字段将会在Lucene2.0中被删除。(Daniel Naber)
 
 2. Russian 和 German 的 analyzers 被移到了 contrib/analyzers 。
   同样 WordlistLoader 类也被放到了 org.apache.lucene.analysis.WordlistLoader 下    (Daniel Naber)
 
 3.  API 包含抛出 IOException 异常的声明,但是实际上不会抛出。 These declarations have been removed. If
    your code tries to catch these exceptions you might need to remove
    those catch clauses to avoid compile errors.(Daniel Naber)
 
 4. 为BooleanClause 类的enum 标准参数增加序列化的参数类。    (Christoph)
 
 5. 为 SpanQuery 的子类嵌套其他SpanQuery 增加了 rewrite方法。
   
 
Lucene 的源代码管理器也从cvs 换到了svn:
 原文地址:
Posted on July 8, 2006 8:33 PM | | Comments (0) | TrackBacks (0)
上一页 1 2 3
相关内容
广告计划
最新评论
[评论] 鸿雁 : 默默地为他们祈祷吧
[评论] lym328 : 客源CRM非常不错-----如有需要可以了解
[评论] kevinwu : 作用肯定是有的,Google会首先搜索站
[评论] ss : 其实还真的感觉不到sitemap的作用~
[评论] kevinwu : 谢谢你的关注 :-)
[评论] h51h : 贵博客写得非常的好,界面简洁但内
[评论] snguo : 这里很好 来这里支持下呢?
[评论] redondo : 感谢你分享知识! 这篇文章我转载到
[评论] kevinwu : 就是这本;看来我买的贵了点 - 8折;我
[评论] 安妮 : 《Flex3.0 RIA开发详解:基于ActionScript3.0