什么是全文檢索與全文檢索系統(tǒng)?
全文檢索是指計算機(jī)索引程序通過掃描文章中的每一個詞,對每一個詞建立一個索引,指明該詞在文章中出現(xiàn)的次數(shù)和位置,當(dāng)用戶查詢時,檢索程序就根據(jù)事先建立的索引進(jìn)行查找,并將查找的結(jié)果反饋給用戶的檢索方式。這個過程類似于通過字典中的檢索字表查字的過程。
全文檢索的方法主要分為按字檢索和按詞檢索兩種。按字檢索是指對于文章中的每一個字都建立索引,檢索時將詞分解為字的組合。對于各種不同的語言而言,字有不同的含義,比如英文中字與詞實(shí)際上是合一的,而中文中字與詞有很大分別。按詞檢索指對文章中的詞,即語義單位建立索引,檢索時按詞檢索,并且可以處理同義項(xiàng)等。
全文檢索系統(tǒng)是按照全文檢索理論建立起來的用于提供全文檢索服務(wù)的軟件系統(tǒng)。一般來說,全文檢索需要具備建立索引和提供查詢的基本功能,此外現(xiàn)代的全文檢索系統(tǒng)還需要具有方便的用戶接口、面向WWW[1]的開發(fā)接口、二次應(yīng)用開發(fā)接口等等。功能上,全文檢索系統(tǒng)核心具有建立索引、處理查詢返回結(jié)果集、增加索引、優(yōu)化索引結(jié)構(gòu)等等功能,外圍則由各種不同應(yīng)用具有的功能組成。結(jié)構(gòu)上,全文檢索系統(tǒng)核心具有索引引擎、查詢引擎、文本分析引擎、對外接口等等,加上各種外圍應(yīng)用系統(tǒng)等等共同構(gòu)成了全文檢索系統(tǒng)。
什么是Lucene?
Lucene是apache軟件基金會jakarta項(xiàng)目組的一個子項(xiàng)目,是一個開放源代碼的全文檢索引擎工具包,即它不是一個完整的全文檢索引擎,而是一個全文檢索引擎的架構(gòu),提供了完整的查詢引擎和索引引擎,部分文本分析引擎(英文與德文兩種西方語言)。Lucene的目的是為軟件開發(fā)人員提供一個簡單易用的工具包,以方便的在目標(biāo)系統(tǒng)中實(shí)現(xiàn)全文檢索的功能,或者是以此為基礎(chǔ)建立起完整的全文檢索引擎。
Lucene的原作者是Doug Cutting,他是一位資深全文索引/檢索專家,曾經(jīng)是V-Twin搜索引擎的主要開發(fā)者,后在Excite擔(dān)任高級系統(tǒng)架構(gòu)設(shè)計師,目前從事于一些Internet底層架構(gòu)的研究。早先發(fā)布在作者自己的http://www.lucene.com/,后來發(fā)布在SourceForge,2001年年底成為apache軟件基金會jakarta的一個子項(xiàng)目:http://jakarta.apache.org/lucene/。
Lucene作為一個全文檢索引擎,其具有如下突出的優(yōu)點(diǎn):
(1)索引文件格式獨(dú)立于應(yīng)用平臺。Lucene定義了一套以8位字節(jié)為基礎(chǔ)的索引文件格式,使得兼容系統(tǒng)或者不同平臺的應(yīng)用能夠共享建立的索引文件。
(2)在傳統(tǒng)全文檢索引擎的倒排索引的基礎(chǔ)上,實(shí)現(xiàn)了分塊索引,能夠針對新的文件建立小文件索引,提升索引速度。然后通過與原有索引的合并,達(dá)到優(yōu)化的目的。
(3)優(yōu)秀的面向?qū)ο蟮南到y(tǒng)架構(gòu),使得對于Lucene擴(kuò)展的學(xué)習(xí)難度降低,方便擴(kuò)充新功能。
(4)設(shè)計了獨(dú)立于語言和文件格式的文本分析接口,索引器通過接受Token流完成索引文件的創(chuàng)立,用戶擴(kuò)展新的語言和文件格式,只需要實(shí)現(xiàn)文本分析的接口。
(5)已經(jīng)默認(rèn)實(shí)現(xiàn)了一套強(qiáng)大的查詢引擎,用戶無需自己編寫代碼即使系統(tǒng)可獲得強(qiáng)大的查詢能力,Lucene的查詢實(shí)現(xiàn)中默認(rèn)實(shí)現(xiàn)了布爾操作、模糊查詢(Fuzzy Search)、分組查詢等等。
面對已經(jīng)存在的商業(yè)全文檢索引擎,Lucene也具有相當(dāng)?shù)膬?yōu)勢:
首先,它的開發(fā)源代碼發(fā)行方式(遵守Apache Software License),在此基礎(chǔ)上程序員不僅僅可以充分的利用Lucene所提供的強(qiáng)大功能,而且可以深入細(xì)致的學(xué)習(xí)到全文檢索引擎制作技術(shù)和面相對象編程的實(shí)踐,進(jìn)而在此基礎(chǔ)上根據(jù)應(yīng)用的實(shí)際情況編寫出更好的更適合當(dāng)前應(yīng)用的全文檢索引擎。在這一點(diǎn)上,商業(yè)軟件的靈活性遠(yuǎn)遠(yuǎn)不及Lucene。
其次,Lucene秉承了開放源代碼一貫的架構(gòu)優(yōu)良的優(yōu)勢,設(shè)計了一個合理而極具擴(kuò)充能力的面向?qū)ο蠹軜?gòu),程序員可以在Lucene的基礎(chǔ)上擴(kuò)充各種功能,比如擴(kuò)充中文處理能力,從文本擴(kuò)充到HTML、PDF等等文本格式的處理,編寫這些擴(kuò)展的功能不僅僅不復(fù)雜,而且由于Lucene恰當(dāng)合理的對系統(tǒng)設(shè)備做了程序上的抽象,擴(kuò)展的功能也能輕易的達(dá)到跨平臺的能力。
最后,轉(zhuǎn)移到apache軟件基金會后,借助于apache軟件基金會的網(wǎng)絡(luò)平臺,程序員可以方便的和開發(fā)者、其它程序員交流,促成資源的共享,甚至直接獲得已經(jīng)編寫完備的擴(kuò)充功能。最后,雖然Lucene使用Java語言寫成,但是開放源代碼社區(qū)的程序員正在不懈的將之使用各種傳統(tǒng)語言實(shí)現(xiàn)(例如.net framework),在遵守Lucene索引文件格式的基礎(chǔ)上,使得Lucene能夠運(yùn)行在各種各樣的平臺上,系統(tǒng)管理員可以根據(jù)當(dāng)前的平臺適合的語言來合理的選。
索引和搜索的關(guān)系
索引是現(xiàn)代搜索引擎的核心,建立索引的過程就是把源數(shù)據(jù)處理成非常方便查詢的索引文件的過程。為什么索引這么重要呢,試想你現(xiàn)在要在大量的文檔中搜索含有某個關(guān)鍵詞的文檔,那么如果不建立索引的話你就需要把這些文檔順序的讀入內(nèi)存,然后檢查這個文章中是不是含有要查找的關(guān)鍵詞,這樣的話就會耗費(fèi)非常多的時間,想想搜索引擎可是在毫秒級的時間內(nèi)查找出要搜索的結(jié)果的。這就是由于建立了索引的原因,你可以把索引想象成這樣一種數(shù)據(jù)結(jié)構(gòu),他能夠使你快速的隨機(jī)訪問存儲在索引中的關(guān)鍵詞,進(jìn)而找到該關(guān)鍵詞所關(guān)聯(lián)的文檔。Lucene 采用的是一種稱為反向索引(inverted index)的機(jī)制。反向索引就是說我們維護(hù)了一個詞/短語表,對于這個表中的每個詞/短語,都有一個鏈表描述了有哪些文檔包含了這個詞/短語。這樣在用戶輸入查詢條件的時候,就能非常快的得到搜索結(jié)果。我們將在本系列文章的第二部分詳細(xì)介紹 Lucene 的索引機(jī)制,由于 Lucene 提供了簡單易用的 API,所以即使讀者剛開始對全文本進(jìn)行索引的機(jī)制并不太了解,也可以非常容易的使用 Lucene 對你的文檔實(shí)現(xiàn)索引。
對文檔建立好索引后,就可以在這些索引上面進(jìn)行搜索了。搜索引擎首先會對搜索的關(guān)鍵詞進(jìn)行解析,然后再在建立好的索引上面進(jìn)行查找,最終返回和用戶輸入的關(guān)鍵詞相關(guān)聯(lián)的文檔。
Lucene 軟件包分析
Package: org.apache.lucene.document
這個包提供了一些為封裝要索引的文檔所需要的類,比如 Document, Field。這樣,每一個文檔最終被封裝成了一個 Document 對象。
Package: org.apache.lucene.analysis
這個包主要功能是對文檔進(jìn)行分詞,因?yàn)槲臋n在建立索引之前必須要進(jìn)行分詞,所以這個包的作用可以看成是為建立索引做準(zhǔn)備工作。
Package: org.apache.lucene.index
這個包提供了一些類來協(xié)助創(chuàng)建索引以及對創(chuàng)建好的索引進(jìn)行更新。這里面有兩個基礎(chǔ)的類:IndexWriter 和 IndexReader,其中 IndexWriter 是用來創(chuàng)建索引并添加文檔到索引中的,IndexReader 是用來刪除索引中的文檔的。
Package: org.apache.lucene.search
這個包提供了對在建立好的索引上進(jìn)行搜索所需要的類。比如 IndexSearcher 和 Hits, IndexSearcher 定義了在指定的索引上進(jìn)行搜索的方法,Hits 用來保存搜索得到的結(jié)果
Lucene包結(jié)構(gòu)功能表 | |
包名 | 功能 |
org.apache.lucene.analysis | 語言分析器,主要用于的切詞,支持中文主要是擴(kuò)展此類 |
org.apache.lucene.document | 索引存儲時的文檔結(jié)構(gòu)管理,類似于關(guān)系型數(shù)據(jù)庫的表結(jié)構(gòu) |
org.apache.lucene.index | 索引管理,包括索引建立、刪除等 |
org.apache.lucene.queryParser | 查詢分析器,實(shí)現(xiàn)查詢關(guān)鍵詞間的運(yùn)算,如與、或、非等 |
org.apache.lucene.search | 檢索管理,根據(jù)查詢條件,檢索得到結(jié)果 |
org.apache.lucene.store | 數(shù)據(jù)存儲管理,主要包括一些底層的I/O操作 |
org.apache.lucene.util | 一些公用類 |
一個簡單的搜索應(yīng)用程序
假設(shè)我們的電腦的目錄中含有很多文本文檔,我們需要查找哪些文檔含有某個關(guān)鍵詞。為了實(shí)現(xiàn)這種功能,我們首先利用
Lucene 對這個目錄中的文檔建立索引,然后在建立好的索引中搜索我們所要查找的文檔。通過這個例子讀者會對如何利用
Lucene 構(gòu)建自己的搜索應(yīng)用程序有個比較清楚的認(rèn)識。
建立索引
為了對文檔進(jìn)行索引,Lucene 提供了五個基礎(chǔ)的類,他們分別是 Document, Field, IndexWriter, Analyzer, Directory。下面我們分別介紹一下這五個類的用途:
Document
Document 是用來描述文檔的,這里的文檔可以指一個 HTML 頁面,一封電子郵件,或者是一個文本文件。一個 Document 對象由多個 Field 對象組成的。可以把一個 Document 對象想象成數(shù)據(jù)庫中的一個記錄,而每個 Field 對象就是記錄的一個字段。
接口名
備注
add(Field field)
添加一個字段(Field)到Document中
String get(String name)
從文檔中獲得一個字段對應(yīng)的文本
Field getField(String name)
由字段名獲得字段值
Field[] getFields(String name)
由字段名獲得字段值的集
Field
Field 對象是用來描述一個文檔的某個屬性的,比如一封電子郵件的標(biāo)題和內(nèi)容可以用兩個 Field 對象分別描述。
即上文所說的“字段”,它是Document的片段section。
Field的構(gòu)造函數(shù):
Field(String name, String string, boolean store, boolean index, boolean token)。
Indexed:如果字段是Indexed的,表示這個字段是可檢索的。
Stored:如果字段是Stored的,表示這個字段的值可以從檢索結(jié)果中得到。
Tokenized:如果一個字段是Tokenized的,表示它是有經(jīng)過Analyzer轉(zhuǎn)變后成為一個tokens序列,在這個轉(zhuǎn)變過程tokenization中,Analyzer提取出需要進(jìn)行索引的文本,而剔除一些冗余的詞句(例如:a,the,they等,詳見org.apache.lucene.analysis.StopAnalyzer.ENGLISH_STOP_WORDS和org.apache.lucene.analysis.standard.StandardAnalyzer(String[] stopWords)的API)。Token是索引時候的基本單元,代表一個被索引的詞,例如一個英文單詞,或者一個漢字。因此,所有包含中文的文本都必須是Tokenized的。
Analyzer
在一個文檔被索引之前,首先需要對文檔內(nèi)容進(jìn)行分詞處理,這部分工作就是由 Analyzer 來做的。Analyzer 類是一個抽象類,它有多個實(shí)現(xiàn)。針對不同的語言和應(yīng)用需要選擇適合的 Analyzer。Analyzer 把分詞后的內(nèi)容交給 IndexWriter 來建立索引。
接口名
備注
addDocument(Document doc)
索引添加一個文檔
addIndexes(Directory[] dirs)
將目錄中已存在索引添加到這個索引
addIndexes(IndexReader[] readers)
將提供的索引添加到這個索引
optimize()
合并索引并優(yōu)化
close()
關(guān)閉
IndexWriter
IndexWriter 是 Lucene 用來創(chuàng)建索引的一個核心的類,他的作用是把一個個的 Document 對象加到索引中來。
Directory
這個類代表了 Lucene 的索引的存儲的位置,這是一個抽象類,它目前有兩個實(shí)現(xiàn),第一個是 FSDirectory,它表示一個存儲在文件系統(tǒng)中的索引的位置。第二個是 RAMDirectory,它表示一個存儲在內(nèi)存當(dāng)中的索引的位置。
熟悉了建立索引所需要的這些類后,我們就開始對某個目錄下面的文本文件建立索引了,給出了對某個目錄下的文本文件建立索引的源代碼。
public class TextFileIndexer {public static void main(String[] args) throws Exception {// fileDir is the directory that contains the text files to be indexed
File fileDir = new File("C:\\index");// indexDir is the directory that hosts Lucene's index files
File indexDir = new File("C:\\luceneIndex");Analyzer luceneAnalyzer = new StandardAnalyzer(Version.LUCENE_30);
IndexWriter indexWriter = new IndexWriter(FSDirectory.open(indexDir), luceneAnalyzer, true, IndexWriter.MaxFieldLength.LIMITED);File[] textFiles = fileDir.listFiles();
long startTime = new Date().getTime();// Add documents to the index
for (int i = 0; i < textFiles.length; i++) {if (textFiles[i].isFile() && textFiles[i].getName().endsWith(".txt")) {
System.out.println("File " + textFiles[i].getCanonicalPath() + " is being indexed");Reader textReader = new FileReader(textFiles[i]);
Document document = new Document();
document.add(new Field("content", textReader));document.add(new Field("path", textFiles[i].getPath(), Field.Store.YES, Field.Index.ANALYZED_NO_NORMS));indexWriter.addDocument(document);}}indexWriter.optimize();indexWriter.close();
long endTime = new Date().getTime();System.out.println("It took " + (endTime - startTime) + " milliseconds to create an index for the files in the directory " + fileDir.getPath());}}
我們注意到類 IndexWriter 的構(gòu)造函數(shù)需要三個參數(shù),第一個參數(shù)指定了所創(chuàng)建的索引要存放的位置,他可以是一個 File 對象,也可以是一個 FSDirectory 對象或者 RAMDirectory 對象。第二個參數(shù)指定了 Analyzer 類的一個實(shí)現(xiàn),也就是指定這個索引是用哪個分詞器對文擋內(nèi)容進(jìn)行分詞。第三個參數(shù)是一個布爾型的變量,如果為 true 的話就代表創(chuàng)建一個新的索引,為 false 的話就代表在原來索引的基礎(chǔ)上進(jìn)行操作。接著程序遍歷了目錄下面的所有文本文檔,并為每一個文本文檔創(chuàng)建了一個 Document 對象。然后把文本文檔的兩個屬性:路徑和內(nèi)容加入到了兩個 Field 對象中,接著在把這兩個 Field 對象加入到 Document 對象中,最后把這個文檔用 IndexWriter 類的 add 方法加入到索引中去。這樣我們便完成了索引的創(chuàng)建。接下來我們進(jìn)入在建立好的索引上進(jìn)行搜索的部分。
搜索文檔
Query
這是一個抽象類,他有多個實(shí)現(xiàn),比如TermQuery, BooleanQuery, PrefixQuery. 這個類的目的是把用戶輸入的查詢字符串封裝成Lucene能夠識別的Query。
Term
Term是搜索的基本單位,一個Term對象有兩個String類型的域組成。生成一個Term對象可以有如下一條語句來完成:Term term = new Term(“fieldName”,”queryWord”); 其中第一個參數(shù)代表了要在文檔的哪一個Field上進(jìn)行查找,第二個參數(shù)代表了要查詢的關(guān)鍵詞。
TermQuery
TermQuery是抽象類Query的一個子類,它同時也是Lucene支持的最為基本的一個查詢類。生成一個TermQuery對象由如下語句完成: TermQuery termQuery = new TermQuery(new Term(“fieldName”,”queryWord”)); 它的構(gòu)造函數(shù)只接受一個參數(shù),那就是一個Term對象。
IndexSearcher
IndexSearcher是用來在建立好的索引上進(jìn)行搜索的。它只能以只讀的方式打開一個索引,所以可以有多個IndexSearcher的實(shí)例在一個索引上進(jìn)行操作。
Hits
Hits是用來保存搜索的結(jié)果的。
介紹完這些搜索所必須的類之后,我們就開始在之前所建立的索引上進(jìn)行搜索了,清單2給出了完成搜索功能所需要的代碼。
如何添加一個文檔到索引文
Document document = new Document();
document.add(new Field("content",textReader));
document.add(new Field("path",textFiles[i].getPath(), Field.Store.YES, Field.Index.ANALYZED_NO_NORMS));
indexWriter.addDocument(document);
//最后不要忘記了關(guān)閉
indexWriter.close();
首先第一行創(chuàng)建了類 Document 的一個實(shí)例,它由一個或者多個的域(Field)組成。你可以把這個類想象成代表了一個實(shí)際的文檔,比如一個 HTML 頁面,一個 PDF 文檔,或者一個文本文件。而類 Document 中的域一般就是實(shí)際文檔的一些屬性。比如對于一個 HTML 頁面,它的域可能包括標(biāo)題,內(nèi)容,URL 等。我們可以用不同類型的 Field 來控制文檔的哪些內(nèi)容應(yīng)該索引,哪些內(nèi)容應(yīng)該存儲。如果想獲取更多的關(guān)于 Lucene 的域的信息,可以參考 Lucene 的幫助文檔。代碼的第二行和第三行為文檔添加了兩個域,每個域包含兩個屬性,分別是域的名字和域的內(nèi)容。在我們的例子中兩個域的名字分別是"content"和"path"。分別存儲了我們需要索引的文本文件的內(nèi)容和路徑。最后一行把準(zhǔn)備好的文檔添加到了索引當(dāng)中。
從索引中刪除文檔
類IndexReader負(fù)責(zé)從一個已經(jīng)存在的索引中刪除文檔。
File indexDir = new File("C:\\luceneIndex");
IndexReader ir = IndexReader.open(indexDir);
ir.delete(1);
ir.delete(new Term("path","C:\\file_to_index\lucene.txt"));
ir.close();
第二行用靜態(tài)方法 IndexReader.open(indexDir) 初始化了類 IndexReader 的一個實(shí)例,這個方法的參數(shù)指定了索引的存儲路徑。類 IndexReader 提供了兩種方法去刪除一個文檔,如程序中的第三行和第四行所示。第三行利用文檔的編號來刪除文檔。每個文檔都有一個系統(tǒng)自動生成的編號。第四行刪除了路徑為"C:\\file_to_index\lucene.txt"的文檔。你可以通過指定文件路徑來方便的刪除一個文檔。值得注意的是雖然利用上述代碼刪除文檔使得該文檔不能被檢索到,但是并沒有物理上刪除該文檔。Lucene 只是通過一個后綴名為 .delete 的文件來標(biāo)記哪些文檔已經(jīng)被刪除。既然沒有物理上刪除,我們可以方便的把這些標(biāo)記為刪除的文檔恢復(fù)過來,如清單 3 所示,首先打開一個索引,然后調(diào)用方法 ir.undeleteAll() 來完成恢復(fù)工作。
恢復(fù)已刪除文檔
File indexDir = new File("C:\\luceneIndex");
IndexReader ir = IndexReader.open(indexDir);
ir.undeleteAll();
ir.close();
如何物理上刪除文檔
File indexDir = new File("C:\\luceneIndex");
Analyzer luceneAnalyzer = new StandardAnalyzer();
IndexWriter indexWriter = new IndexWriter(indexDir,luceneAnalyzer,false);
indexWriter.optimize();
indexWriter.close();
第三行創(chuàng)建了類 IndexWriter 的一個實(shí)例,并且打開了一個已經(jīng)存在的索引。第 4 行對索引進(jìn)行清理,清理過程中將把所有標(biāo)記為刪除的文檔物理刪除。
提高索引性能
利用 Lucene,在創(chuàng)建索引的工程中你可以充分利用機(jī)器的硬件資源來提高索引的效率。當(dāng)你需要索引大量的文件時,你會注意到索引過程的瓶頸是在往磁盤上寫索引文件的過程中。為了解決這個問題, Lucene 在內(nèi)存中持有一塊緩沖區(qū)。但我們?nèi)绾慰刂?Lucene 的緩沖區(qū)呢?幸運(yùn)的是,Lucene 的類 IndexWriter 提供了三個參數(shù)用來調(diào)整緩沖區(qū)的大小以及往磁盤上寫索引文件的頻率。
1.合并因子(mergeFactor)
這個參數(shù)決定了在 Lucene 的一個索引塊中可以存放多少文檔以及把磁盤上的索引塊合并成一個大的索引塊的頻率。比如,如果合并因子的值是 10,那么當(dāng)內(nèi)存中的文檔數(shù)達(dá)到 10 的時候所有的文檔都必須寫到磁盤上的一個新的索引塊中。并且,如果磁盤上的索引塊的隔數(shù)達(dá)到 10 的話,這 10 個索引塊會被合并成一個新的索引塊。這個參數(shù)的默認(rèn)值是 10,如果需要索引的文檔數(shù)非常多的話這個值將是非常不合適的。對批處理的索引來講,為這個參數(shù)賦一個比較大的值會得到比較好的索引效果。
2.最小合并文檔數(shù)
這個參數(shù)也會影響索引的性能。它決定了內(nèi)存中的文檔數(shù)至少達(dá)到多少才能將它們寫回磁盤。這個參數(shù)的默認(rèn)值是10,如果你有足夠的內(nèi)存,那么將這個值盡量設(shè)的比較大一些將會顯著的提高索引性能。
3.最大合并文檔數(shù)
這個參數(shù)決定了一個索引塊中的最大的文檔數(shù)。它的默認(rèn)值是 Integer.MAX_VALUE,將這個參數(shù)設(shè)置為比較大的值可以提高索引效率和檢索速度,由于該參數(shù)的默認(rèn)值是整型的最大值,所以我們一般不需要改動這個參數(shù)。
int mergeFactor = 10;
int minMergeDocs = 10;
int maxMergeDocs = Integer.MAX_VALUE;
IndexWriter indexWriter = new IndexWriter(indexDir,luceneAnalyzer,true);
indexWriter.mergeFactor = mergeFactor;
indexWriter.minMergeDocs = minMergeDocs;
indexWriter.maxMergeDocs = maxMergeDocs;
下面我們來看一下這三個參數(shù)取不同的值對索引時間的影響,注意參數(shù)值的不同和索引之間的關(guān)系。我們?yōu)檫@個實(shí)驗(yàn)準(zhǔn)備了 10000 個測試文檔。表 1 顯示了測試結(jié)果。
表1:測試結(jié)果
![]()
通過表 1,你可以清楚地看到三個參數(shù)對索引時間的影響。在實(shí)踐中,你會經(jīng)常的改變合并因子和最小合并文檔數(shù)的值來提高索引性能。只要你有足夠大的內(nèi)存,你可以為合并因子和最小合并文檔數(shù)這兩個參數(shù)賦盡量大的值以提高索引效率,另外我們一般無需更改最大合并文檔數(shù)這個參數(shù)的值,因?yàn)橄到y(tǒng)已經(jīng)默認(rèn)將它設(shè)置成了最大。
作者:hoojo
出處:
blog:http://blog.csdn.net/IBM_hoojo
http://hoojo.cnblogs.com
本文版權(quán)歸作者和博客園共有,歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責(zé)任的權(quán)利。
版權(quán)所有,轉(zhuǎn)載請注明出處 本文出自:
