4.JTest 重量級的商業工具
目前版本7.0.7,有for eclipse的插件.網址是http://www.parasoft.com/
不推薦使用,不過功能強大,可以進行代碼檢查,可以自動生成單元測試和進行單元測試.(不過就是太慢了,而且生成的單元測試沒太大用途)
使用感覺:
安裝上插件后,對自己的項目進行檢查,發現警告太多了,有點發蒙的感覺.不過把警告看一遍,覺得都很有道理,有些也確實是一些錯誤.
當然PMD和CheckStyle的規范太嚴格,最后還是配置了一下.
通過改正警告,感覺還是不錯,至少可以說自己的代碼可以通過工具的檢測了.
當然基礎代碼和項目代碼還是不一樣的,基礎代碼往往比較復雜,所以和普通項目代碼的規范應該有所不同.有些規則只能用在普通代碼上,用在基礎類代碼上往往沒法處理.
其他
代碼查錯推薦使用Findbugs和PMD,代碼書寫規范推薦使用CheckStyle進行檢查.這樣不僅能查出一些基本的錯誤,也能提高項目的代碼質量.對提高自己的代碼水平也是非常好.
推薦項目組建立統一的規則,代碼復查的時候就使用這些工具,省時省力.
實乃居家旅行,殺人越貨必備之工具也.(因為肯定有人要罵你,呵呵,也是你找"差"的工具)
根據XML中CDATA類型的規范可以知道:"&"和"<"不需要也不能被轉換. ">" 如果出現在"]]>" 的內容而不是表示結束時,必須被轉義為>
但是這樣就存在一個問題,如果我需要輸入"]]>",正確的處理是保存為"]]>",但是如果我想輸入"]]>",那么應該如何保存哪? 我想了很久,除非加空格或者采用特殊的辦法,否則是沒有辦法解決的.
1.如果我們不考慮輸入"]]>"的問題,來考慮一下"]]>"的處理,看看各種XML解析器是如何處理的?
xml解析器的測試包含2個部分:設置cdata類型的數據和讀出cdata類型的數據.
首先我們寫一個測試的例子,計劃使用JDom 1.0和Dom4j來測試一下:
package com.jscud.test; public class XmlTestBase { public static String xmlpart = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"+ "<xml>" + "<test>"+ "<hello><![CDATA[ hello ]]> ]]></hello>" + "</test>" + "</xml>"; public static void print(String str) { System.out.println(str); } } |
package com.jscud.test; import java.io.*; import org.jdom.*; import org.jdom.input.SAXBuilder; import org.jdom.output.*; //@author scud http://www.jscud.com public class JDomXmlFileTest extends XmlTestBase { public static void main(String[] args) throws Exception { readDocument(); print("==========================="); createDocument(); } public static void readDocument() throws Exception { Reader reader = new StringReader(xmlpart); SAXBuilder builder = new SAXBuilder(); Document doc = builder.build(reader); Element aRoot = doc.getRootElement(); Element anode = aRoot.getChild("test").getChild("hello"); print(anode.getText()); } public static void createDocument() throws Exception { Document doc = new Document(); doc.setRootElement(new Element("root")); CDATA node = new CDATA("hello alt=]]>"); //throw Exception //node.setText("hello]]>"); Element ele = new Element("hello"); ele.setContent(node); Element root = doc.getRootElement(); root.getChildren().add(ele); XMLOutputter outputter = new XMLOutputter(); Format aFormat = Format.getCompactFormat(); aFormat.setEncoding("GB2312"); String sResult = outputter.outputString(doc.getRootElement().getChildren()); print(sResult); } } |
編譯并運行上面的代碼結果,我們可以看到JDom無法設置Cdata的值為"]]>",會報異常.從xml字符串讀出cdata的結果也沒有把字串"]]>"翻譯為"]]>".
接著再來測試Dom4J:
package com.xml.test; import java.io.StringReader; import org.dom4j.*; import org.dom4j.io.SAXReader; import org.dom4j.tree.DefaultCDATA; /** * 測試XML的CData數據類型. * * @author scud http://www.jscud.com * */ public class Dom4jXmlTest extends XmlTestBase { public static void main(String[] args) throws Exception { readDocument(); print("==========================="); createDocument(); } public static void createDocument() { Document document = DocumentHelper.createDocument(); Element root = document.addElement( "root" ); DefaultCDATA cdata = new DefaultCDATA("sample]]>"); DefaultCDATA cdata2 = new DefaultCDATA("sample]]>"); Element anode = root.addElement("cdata"); anode.add(cdata); print(anode.getText()); print(anode.asXML()); Element anode2 = root.addElement("cdata2"); anode2.add(cdata2); print(anode2.getText()); print(anode2.asXML()); } public static void readDocument() throws Exception { StringReader strreader = new StringReader(xmlpart); SAXReader reader = new SAXReader(); Document document = reader.read(strreader); Node node = document.selectSingleNode( "http://test/hello" ); print(node.getText()); print(node.getStringValue()); } } |
根據上面的測試我們可以得出結論:很多xml解析器沒有正確解析cdata的數據,(jdom和dom4j用的人比較多),不要太相信這些解析器.
2.我們再來看看閱讀RSS的RSS閱讀器吧,例如FeedDemon和POTU,我們準備了一個CData類型的description字段,來進行測試.
內容:
<?xml version="1.0" encoding="GB2312" ?> <rss version="2.0"> <channel> <title>Some Where</title> <link>http://www.jscud.com/</link> <description /> <item> <title>Test</title> <link>http://www.jscud.com</link> <author>scud</author> <pubDate>Mon, 22 Aug 2005 10:22:22 GMT</pubDate> <description><![CDATA[ <hr> ]]> ]]></description> </item> </channel> </rss> |
結果:
1.POTU沒有做任何處理
2.FeedDemon做了處理,不過同時也把其他的> <等等都翻譯了,這就更不對了..
本來我是打算在RSS里使用CDATA類型的description字段的,經過幾番試驗和測試,最后決定還是使用普通的description字段了,不在使用CDATA了.
CDATA? 雞肋乎? 呵呵
關鍵字:rss,freemarker,rss.xml,webwork2
RSS在網絡上大行其道,各種網站都加上RSS支持,我最近也研究了一下,給我的文章也加上了RSS訂閱.
RSS目前用的也有幾個版本,很是混亂,下面以RSS2.0為例來說明.
網絡上有個rsslibj庫,是用來生成rss支持文件的,不過已經好久沒有更新了,它是用xml的方式生成的.本文的例子不用到任何xml解析器,不過當然要知道最后生成的XML文件的格式才行,關于RSS規范,可以瀏覽一下 http://blogs.law.harvard.edu/tech/rss .
在計劃生成RSS文件的時候,順便搜索了一下JIRA和Confluence的程序,發現它們分別是用模板方式和JSP動態頁面來展示的.于是我也想到兩種方式:
1.用FreeMarker生成靜態文件,適用于更新不是很頻繁的內容.
2.用JSP動態展示,適合更新頻率高,種類繁多的內容.
還是以本站的新聞舉例,其中的新聞信息類參考 http://www.jscud.com/srun/news/viewhtml/3_2005_8/76.htm ,此處不在列出.
(一) 先說FreeMarker方式.
根據RSS的規范,得到模板如下:
<?xml version="1.0" encoding="UTF-8" ?> <rss version="2.0"> <channel> <title>JScud Develop</title> <link>http://www.jscud.com/</link> <language>zh-cn</language> <description >JScud Develop By Scud</description> <webMaster>xxx@21cn.com(scud)</webMaster> <lastBuildDate>${rssutil.formatRssDate(now)}</lastBuildDate> <#list newslist as onenews> <item> <title>${onenews.title?xml}</title> <link>http://www.jscud.com/srun/news/viewhtml/${onenews.htmlFilePath}/${onenews.nid}.htm</link> <pubDate>${rssutil.formatRssDate(onenews.addtime)}</pubDate> <description><![CDATA[ ${rssutil.formatRssCData(onenews.showContent)} ]]> </description> </item> </#list> </channel> </rss> |
其中的網址和網站名稱可以根據自己的實際情況修改.
我每次取出最新的20條文章來生成RSS,不過內容比較多,生成的RSS文件比較大,看到有的網站的description只是放了文章摘要的內容,這樣文件就小多了.總之是根據自己的需求設計吧.
其中用到的RssUtil函數庫的函數如下(日期的函數參考上一篇文章):
/** * 把]]>替換為]]> * @param content 內容 * @return 格式化后的內容 */ public static String formatRssCData(String content) { String result = StringFunc.replace(content,"\\]\\]>","]]>"); return result; } /** * 格式化為xml需要的字符串 * @param field 內容 * @return 格式化后的串 */ public static String formatString2XML(String field) { return StringFunc.str2TextXML(field); } public static String getNowDateTime() { return formatRssDate(DateTime.getNowTimestamp()); } |
利用FreeMarker生成靜態文件的代碼如下:
private Configuration freemarker_cfg = null; freemarker_cfg.setClassForTemplateLoading(this.getClass(), "/htmlskin"); freemarker_cfg.setDefaultEncoding("GBK"); return freemarker_cfg; public boolean geneFileByFreeMarker(String templateFileName, Map propMap, String filePath, File afile = new File(filePath + "/" + fileName); Writer out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(afile), propMap.put("baseurl", PropSet.getStringProp("url.root")); t.process(propMap, out); return true; |
新聞系統中調用重新生成RSS文件的代碼如下:
/** boolean shouldUpdate = false; //不更新,則返回 Map root = new HashMap(); root.put("newslist", newsList); geneFileByFreeMarker("/news/rss.ftl", root, PropSet.getStringProp("rss.rssdir"), PropSet return true; |
(二)JSP動態方式
相對靜態方式而言,簡單的多,不過效率上可能就不太好了.
webwork2的Action代碼如下:
newsList = 裝載新聞代碼 |
視圖Jsp如下:
<%@ page contentType="text/xml; charset=UTF-8"%> <%@ taglib uri="jscud" prefix="jscud" %> <%@ taglib uri="webwork" prefix="ww" %> <ww:bean name="’com.jscud.www.util.RSSUtil’" id="rssUtil" /> <?xml version="1.0" encoding="UTF-8" ?> <rss version="2.0"> <channel> <title>JScud Develop</title> <link>http://www.jscud.com/</link> <language>zh-cn</language> <description >JScud Develop By Scud</description> <webMaster>xxx@21cn.com(scud)</webMaster> <lastBuildDate><ww:property value="#rssUtil.nowDateTime" /></lastBuildDate> <ww:iterator value="newsList"> <item> <title><ww:property value="#rssUtil.formatString2XML(title)"/></title> <link>http://www.jscud.com/srun/news/viewhtml/<ww:property value="htmlFilePath" />/<ww:property value="nid" />.htm</link> <pubDate><ww:property value="#rssUtil.formatRssDate(addtime)" /></pubDate> <description><![CDATA[ <ww:property value="#rssUtil.formatRssCData(showContent)"/> ]]> </description> </item> </ww:iterator> </channel> </rss> |
jsp的方式簡單多了,上面的jsp里面還演示了ww:bean的使用 :)
上面的類里面引用了很多其他的工具類,這里不一一列出,可以自己實現它們,都是很簡單的類. :)
rss中日期格式要求遵守rfc822規范,其中是這么寫的:
date-time = [ day "," ] date time ; dd mm yy ; hh:mm:ss zzz day = "Mon" / "Tue" / "Wed" / "Thu" / "Fri" / "Sat" / "Sun" date = 1*2DIGIT month 2DIGIT ; day month year ; e.g. 20 Jun 82 month = "Jan" / "Feb" / "Mar" / "Apr" / "May" / "Jun" / "Jul" / "Aug" / "Sep" / "Oct" / "Nov" / "Dec" time = hour zone ; ANSI and Military hour = 2DIGIT ":" 2DIGIT [":" 2DIGIT] ; 00:00:00 - 23:59:59 zone = "UT" / "GMT" ; Universal Time ; North American : UT / "EST" / "EDT" ; Eastern: - 5/ - 4 / "CST" / "CDT" ; Central: - 6/ - 5 / "MST" / "MDT" ; Mountain: - 7/ - 6 / "PST" / "PDT" ; Pacific: - 8/ - 7 / 1ALPHA ; Military: Z = UT; ; A:-1; (J not used) ; M:-12; N:+1; Y:+12 / ( ("+" / "-") 4DIGIT ) ; Local differential ; hours+min. (HHMM)
可以看出,前面的星期X是可以省略的,后面的時間是要求有時區的.
示例如下(以在中國的中文操作系統機器為例):
1.Tue, 16 Aug 2005 15:33:33 GMT
2.Tue, 16 Aug 2005 23:33:33 +0800
其實這個rfc822應該也是電子郵件內容格式的規范,找一個郵件看看內容,也可以看出,郵件的時間格式也是遵循這個規范的.
要輸入第一種格式,使用SimpleDateFormat格式化即可,代碼如下
public static void test1(Date date) { SimpleDateFormat sdfTemp = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss z",Locale.US); SimpleTimeZone aZone = new SimpleTimeZone(8,"GMT"); sdfTemp.setTimeZone(aZone); System.out.println(sdfTemp.format(date)); } |
Tue, 16 Aug 2005 23:33:33 CST
這里的CST意思是代表"中國時間",但是一經搜索,發現CST代表了好幾個時區,太讓人混亂了.而在RTF822里面,CST僅代表美國中部時間.所以如果使用SimpleDateFormat,要設置時區以GMT表示,否則容易讓人迷惑而且不知道是那個時區.
假設你在中國,想根據當地時間輸入復合當地時間的字符串,讓人一看就能明白文章的日期,那么就使用第二種格式.(我推薦使用第二種方式,當然你的頻道主要給外國朋友瀏覽登除外)
上面說到和郵件有關,于是我們看看JavaMail包里面的javax.mail.internet.MailDateFormat,可以用來格式化日期:
(MyEclipse 3.8.4附帶的J2EE 1.3中的JavaMail包)
MailDateFormat mdf = new MailDateFormat(); SimpleTimeZone aZone = new SimpleTimeZone(8,"GMT"); //mdf.setTimeZone(aZone); System.out.println(mdf.format(date)); |
輸出結果為:
Tue, 16 Aug 2005 23:33:33 +0800 (CST)
如果設置了時區為GMT,則輸出:
Tue, 16 Aug 2005 15:33:33 +0000 (GMT)
可以看到相對RTF822而言,好像多了一個后面的時區的說明及其括號.不知道這到底是怎么回事?
在硬盤上查找一番,發現在JIRA程序和Confluence中的RSS里都使用了這個日期格式.
注意到這個不同,我瀏覽了一下outlook Express里面的郵件,發現兩種時間格式的郵件都存在,真是讓人迷惑,或許都可以吧,呵呵 :)
如果不想使用MailDateFormat的格式,那么就自己寫一個類來實現吧,例如
public class RssDateFormat extends MailDateFormat { public RssDateFormat() { applyPattern("EEE, d MMM yyyy HH:mm:ss ’XXXXX’"); } } |
至此,我的RSS中的日期終于正確而且讓我滿意了. :)
關鍵字:lucene,html parser,全文檢索,IndexReader,Document,Field,IndexWriter,Term,HTMLPAGE
無論是建立索引還是分析內容,都是為了用戶的搜索服務.
在Lucene中,如果需要使用搜索,需要使用Searcher類,這是一個抽象類,它有2個子類:IndexSearcher和MultiSearcher.
IndexSearcher是對一個索引進行搜索,如果你需要對多個索引進行搜索,可以使用MultiSearcher.下面的內容只介紹了IndexSearcher.
搜索涉及到幾個問題:分頁,組合條件,根據條件過濾,排序等等.
分頁:分頁在記錄列表的地方都會遇到,這里不在贅述,我也實現過一個保存分頁結果和顯示結果的類,用于自己的實際工作,下面也會用到保存分頁結果的類,代碼如下:
package com.jscud.support; /** * 分頁顯示用的參數. * * @author scud(飛云小俠) http://www.jscud.com * */ public class DivPageInfo { //開始記錄數 private int recStart; //結束記錄數 private int recEnd; //總頁數 private int pageCount; //當前頁 private int page; //記錄總數 private int recCount; //每頁記錄數 private int perPageRows; public int getNicePageCount() { return getNicePageNum(pageCount); } //get,set等,不在列出 //...... /** * 得到友好的頁數數字,頁數為0時,返回1. * * @return 得到友好的頁數 */ public static int getNicePageNum(int nPage) { if (nPage == 0) { return 1; } else { return nPage; } } } |
顯示分頁結果的類就需要大家根據自己使用的框架來具體實現了.我使用的是WebWork.
組合條件:在Lucene中,搜索的條件可以組合的很復雜,相關的類有BooleanQuery, FilteredQuery, MultiTermQuery, PhrasePrefixQuery, PhraseQuery, PrefixQuery, RangeQuery, SpanQuery, TermQuery 等等,從而可以組合出很復雜的條件用于查詢.
另外QueryParser可以根據用戶輸入的字符串和設定的解析器和字段設置等,可以自動產生新的組合條件用于查詢,例如用戶輸入"john AND black",QueryParser可以自己分析出用戶是需要查詢字段中同時包含"john"和"black"的結果.
過濾條件:有時候根據具體的用戶需求,有些記錄對于一些用戶是不可見的,此時就要使用過濾器來防止不合法的用戶看到不應該看到的記錄.過濾器同時也可以根據一些具體的條件來過濾掉一些用戶不想看到的記錄.如果需要實現自己的filter,只要參考QueryFilter,DateFilter實現Filter即可.
排序:有時候,可能需要根據某個字段進行排序,例如按照時間排序.當然更多的時候是按照搜索結果的符合度進行排序,lucene默認的排序就是按照符合度來進行排序的.
進行搜索的代碼如下,根據自己的需要進行代碼的修改:
/** * 進行搜索. * * 參數依次為:搜索內容(支持lucene語法),當前頁,每頁記錄數,分頁信息對象 * */ public static List search(String searchText, int page, int perpage, final DivPageInfo pageinfo) { List docs = new ArrayList(); if(!LuceneSearch.indexExist(indexDir)) { return docs; } Searcher searcher = null; //處理檢索條件 BooleanQuery query = new BooleanQuery(); //分頁檢索 DivPageInfo.divPage(hits.length(), perpage, page, pageinfo); //取出當前頁的記錄 return docs; |
代碼中出現了一個新的類Hits,Hits是lucene的搜索結果集,是lazy load的結果集,只有你真正訪問它,它才去裝載真正的數據.
代碼中還出現了一個LuceneDocument,這是為了在頁面中顯示而寫的一個輔助類,因為lucene的Document是final的,無法進行擴展,而要顯示時間字段必須要調用DateField中的函數,這樣在頁面中顯示就不太直觀了,所以寫了這個輔助類,代碼如下:
package com.jscud.www.support.search; import java.sql.Timestamp; import java.util.Date; import org.apache.lucene.document.DateField; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; /** * 對Lucene的Document的封裝,用于顯示目的. * * @author scud(飛云小俠) http://www.jscud.com * */ public class LuceneDocument { private Document doc; public LuceneDocument(Document doc) { this.doc = doc; } public static LuceneDocument getDocument(Document doc) { return new LuceneDocument(doc); } public String getValue(String name) { return doc.get(name); } public Field getField(String name) { return doc.getField(name); } public Timestamp getDateTime(String name) { String value = doc.get(name); return new Timestamp( DateField.stringToTime(value)); } public Date getDate(String name) { String value = doc.get(name); return DateField.stringToDate(value); } } |
使用WebWork對結果集進行了顯示,代碼如下:
<ww:iterator value="docs"> <tr > <td> <a href="<jscud:contextpath /><ww:property value="getValue('visiturl')" />" target="_blank" > <ww:property value="getValue('title')" escape="true" /> </a> (<jscud:datetime value="getDateTime('addtime')" />) </td> </tr> </ww:iterator> |
scud(飛云小俠) http://www.jscud.com 轉載請注明來源/作者
關鍵字:lucene,html parser,全文檢索,IndexReader,Document,Field,IndexWriter,Term,HTMLPAGE
在使用lucene對相關內容進行索引時,會遇到各種格式的內容,例如Html,PDF,Word等等,那么我們如何從這么文檔中得到我們需要的內容哪?例如Html的內容,一般我們不需要對Html標簽建立索引,因為那不是我們需要搜索的內容.這個時候,我們就需要從Html內容中解析出我們所需要的內容.對于PDF,Word文檔,也是類似的要求.
總之,我們只需要從內容中提取出我們需要的文本來建立索引,這樣用戶就能搜索到需要的內容,然后訪問對應的資源即可.
Lucene本身帶的例子中有一個解析Html的代碼,不過不是純JAVA的,所以在網上我又找到了另外一個Html解析器,網址如下:http://htmlparser.sourceforge.net.
對PDF解析的相關項目有很多,例如PDFBox.在PDFBox里面提出pdf的文本內容只需要一句話即可:
Document doc = LucenePDFDocument.getDocument( file ); |
/** //設置編碼:根據實際情況修改 HtmlPage visitor = new HtmlPage(myParser); myParser.visitAllNodesWith(visitor); title = visitor.getTitle(); body = combineNodeText(visitor.getBody().toNodeArray()); SearchHtmlPage result = new SearchHtmlPage(title, body); return result; /** myParser = Parser.createParser(content, "GBK"); NodeFilter textFilter = new NodeClassFilter(TextNode.class); //暫時不處理 meta OrFilter lastFilter = new OrFilter(); try //中場退出了 Node[] nodes = nodeList.toNodeArray(); String result = combineNodeText(nodes); //合并節點的有效內容 for (int i = 0; i < nodes.length; i++) String line = ""; line = linknode.getLink(); if (StringFunc.isTrimEmpty(line)) continue; result.append(" ").append(line); return result.toString(); |
package com.jscud.www.support.search; /** * 搜索時解析Html后返回的頁面模型. * * @author scud(飛云小俠) http://www.jscud.com * */ public class SearchHtmlPage { /**標題*/ private String title; /**內容*/ private String body; public SearchHtmlPage(String title, String body) { this.title = title; this.body = body; } public String getBody() { return body; } public void setBody(String body) { this.body = body; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } } |
關鍵字:lucene,html parser,全文檢索,IndexReader,Document,Field,IndexWriter,Term,HTMLPAGE
Lucene是一個全文檢索的引擎,目前有Java和.Net 等幾個版本.Java版本的網址是http://lucene.apache.org.相關的一個項目是車東的WebLucene: http://sourceforge.net/projects/weblucene.
首先,基于一個簡單的新聞系統,要想做全文檢索.新聞系統的管理等在這里不在具體提出,下面列出新聞對象的類:
注:程序用會到一些工具類,不在此列出,用戶可以自己實現.
package com.jscud.website.newsinfo.bean; import java.sql.Timestamp; import com.jscud.util.DateTime; import com.jscud.util.StringFunc; import com.jscud.website.newsinfo.NewsConst; /** * 一個新聞. * * @author scud(飛云小俠) http://www.jscud.com * */ public class NewsItem { private int nid; //新聞編號 private int cid; //類別編號 private String title;//標題 private int showtype; //內容類型:目前支持url和html private String content;//內容 private String url;//對應網址,如果內容類型是url的話 private Timestamp addtime; //增加時間 private int click; //點擊數 //對應的get,set函數,較多不在列出,可以使用工具生成 //...... /** * 按照類型格式化 */ public String getShowContent() { String sRes = content; if(showtype == NewsConst.ShowType_HTML) { } return sRes; } public String getTarget() { if(showtype == NewsConst.ShowType_URL) { return "_blank"; } else return ""; } /** * 靜態Html文件的路徑及其名字 */ public String getHtmlFileName() { int nYear = DateTime.getYear_Date(getAddtime()); int nMonth = DateTime.getMonth_Date(getAddtime()); String sGeneFileName = "/news/" + getCid() + "/" + nYear + "/" + nMonth +"/" + getNid() + ".htm"; return sGeneFileName; } /** * 靜態Html文件的路徑 */ public String getHtmlFilePath() { int nYear = DateTime.getYear_Date(getAddtime()); int nMonth = DateTime.getMonth_Date(getAddtime()); String sGeneFilePath = getCid() + "_" + nYear + "_" + nMonth; return sGeneFilePath; } } |
可以看到,我們需要對標題和內容進行檢索,為了這個目的,我們首先需要來研究一下lucene.
在Lucene中,如果要進行全文檢索,必須要先建立索引然后才能進行檢索,當然實際工作中還會有刪除索引和更新索引的工作.
在此之前,介紹一個最基本的類(摘抄自http://m.tkk7.com/cap/archive/2005/07/17/7849.html):
Analyzer 文件的分析器(聽起來別扭,還是叫Analyzer好了)的抽象,這個類用來處理分詞(對中文尤其重要,轉換大小寫(Computer->computer,實現查詢大小寫無關),轉換詞根(computers->computer),消除stop words等,還負責把其他格式文檔轉換為純文本等.
在lucene中,一般會使用StandardAnalyzer來分析內容,它支持中文等多字節語言,當然可以自己實現特殊的解析器.StandardAnalyzer目前對中文的處理是按照單字來處理的,這是最簡單的辦法,但是也有缺點,會組合出一些沒有意義的結果來.
首先我們來了解建立索引,建立索引包含2種情況,一種是給一條新聞建立索引,另外的情況是在開始或者一定的時間給批量的新聞建立索引,所以為了通用,我們寫一個通用的建立索引的函數:
(一般一類的索引都放在一個目錄下,這個配置可以在函數中定義,也可以寫在配置文件中,通過參數傳遞給函數.)
/** * 生成索引. * * @param doc 目標文檔 * @param indexDir 索引目錄 */ public static void makeIndex(Document doc, String indexDir) { List aList = new ArrayList(); aList.add(doc); makeIndex(aList, indexDir); } /** * 生成索引. * * @param doc 生成的document. * @param indexDir 索引目錄 */ public static void makeIndex(List docs, String indexDir) { if (null == docs) { return; } boolean indexExist = indexExist(indexDir); IndexWriter writer = null; //添加一條文檔 //索引完成后的處理 |
可以看到,建立索引用到類是IndexWrite,它可以新建索引或者追加索引,但是需要自己判斷.判斷是通過IndexReader這個類來實現的,函數如下:
/** * 檢查索引是否存在. * @param indexDir * @return */ public static boolean indexExist(String indexDir) { return IndexReader.indexExists(indexDir); } |
如果每次都是新建索引的話,會把原來的記錄刪除,我在使用的時候一開始就沒有注意到,后來觀察了一下索引文件,才發現這個問題.
還可以看到,建立索引是給用戶的Document對象建立索引,Document表示索引中的一條文檔記錄.那么我們如何建立一個文檔那?以新聞系統為例,代碼如下:
/** * 生成新聞的Document. * * @param aNews 一條新聞. * * @return lucene的文檔對象 */ public static Document makeNewsSearchDocument(NewsItem aNews) { Document doc = new Document(); doc.add(Field.Keyword("nid", String.valueOf(aNews.getNid()))); doc.add(Field.Text("title", aNews.getTitle())); //對Html進行解析,如果不是html,則不需要解析.或者根據格式調用自己的解析方法 String content = parseHtmlContent(aNews.getContent()); doc.add(Field.UnStored("content", content)); doc.add(Field.Keyword("addtime", aNews.getAddtime())); //可以加入其他的內容:例如新聞的評論等 doc.add(Field.UnStored("other", "")); //訪問url String newsUrl = "/srun/news/viewhtml/" + aNews.getHtmlFilePath() + "/" + aNews.getNid() + ".htm"; doc.add(Field.UnIndexed("visiturl", newsUrl)); return doc; } |
通過上面的代碼,我們把一條新聞轉換為lucene的Document對象,從而進行索引工作.在上面的代碼中,我們又引入了lucene中的Field(字段)類.Document文檔就像數據庫中的一條記錄,它有很多字段,每個字段是一個Field對象.
從別的文章摘抄一段關于Field的說明(摘抄自http://m.tkk7.com/cap/archive/2005/07/17/7849.html):
[quote]
類型 Analyzed Indexed Stored 說明
Field.Keyword(String,String/Date) N Y Y 這個Field用來儲存會直接用來檢索的比如(編號,姓名,日期等)
Field.UnIndexed(String,String) N N Y 不會用來檢索的信息,但是檢索后需要顯示的,比如,硬件序列號,文檔的url地址
Field.UnStored(String,String) Y Y N 大段文本內容,會用來檢索,但是檢索后不需要從index中取內容,可以根據url去load真實的內容
Field.Text(String,String) Y Y Y 檢索,獲取都需要的內容,直接放index中,不過這樣會增大index
Field.Text(String,Reader) Y Y N 如果是一個Reader, lucene猜測內容比較多,會采用Unstored的策略.
[/quote]
我們可以看到新聞的編號是直接用來檢索的,所以是Keyword類型的字段,新聞的標題是需要檢索和顯示用的,所以是Text類型,而新聞的內容因為是Html格式的,所以在經過解析器的處理用,使用的UnStored的格式,而新聞的時間是直接用來檢索的,所以是KeyWord類型.為了在新聞索引后用戶可以訪問到完整的新聞頁面,還設置了一個UnIndexed類型的訪問地址字段.
(對Html進行解析的處理稍后在進行講解)
為一條新聞建立索引需要兩個步驟:獲取Document,傳給makeIndex函數,代碼如下:
public static void makeNewsInfoIndex(NewsItem aNews) { if (null == aNews) { return; } makeIndex(makeNewsSearchDocument(aNews),indexDir); } |
建立索引的工作就進行完了,只要在增加新聞后調用 makeNewsInfoIndex(newsitem); 就可以建立索引了.
如果需要刪除新聞,那么也要刪除對應的索引,刪除索引是通過IndexReader類來完成的:
/** * 刪除索引. * @param aTerm 索引刪除條件 * @param indexDir 索引目錄 */ public static void deleteIndex(Term aTerm, String indexDir) { List aList = new ArrayList(); aList.add(aTerm); deleteIndex(aList, indexDir); } /** IndexReader reader = null; |
刪除索引需要一個條件,類似數據庫中的字段條件,例如刪除一條新聞的代碼如下:
public static void deleteNewsInfoIndex(int nid) { Term aTerm = new Term("nid", String.valueOf(nid)); deleteIndex(aTerm,indexDir); } |
通過新聞的ID,就可以刪除一條新聞.
如果需要更新新聞,如何更新索引哪? 更新索引需要先刪除索引然后新建索引2個步驟,其實就是把上面的代碼組合起來,例如更新一條新聞:
public static void updateNewsInfoIndex(NewsItem aNews) { if (null == aNews) { return; } deleteNewsInfoIndex(aNews.getNid()); makeNewsInfoIndex(aNews); } |
至此,索引的建立更新和刪除就告一段落了.其中批量更新新聞的代碼如下:
(批量更新應該在訪問人數少或者后臺程序在夜間執行)
public static void makeAllNewsInfoIndex(List newsList) { List terms = new ArrayList(); List docs = new ArrayList(); for (int i = 0; i < newsList.size(); i++) deleteIndex(terms,indexDir); |
最近在研究lucene的全文檢索,在很多地方需要解析或者說分析Html內容或者Html頁面,Lucene本身的演示程序中也提供了一個Html Parser,但是不是純Java的解決方案.于是到處搜索,在網上找到了一個"HTMLParser".
網址是: http://htmlparser.sourceforge.net ,當前版本為1.5.
下載下來,試用一番,感覺不錯,完全能滿足lucene解析Html的需求.
過幾天貼出lucene進行全文檢索的代碼.(檢索本站的文章等).
試用代碼如下,供大家參考:
package com.jscud.test; import java.io.BufferedReader; import org.htmlparser.Node; import com.jscud.util.LogMan; //一個日志記錄類 /** public class ParseHtmlTest public static void main(String[] args) throws Exception String content = readTextFile(aFile, "GBK"); test1(content); test2(content); test3(content); test4(content); test5(aFile); //訪問外部資源,相對慢 } /** //設置編碼 HtmlPage visitor = new HtmlPage(myParser); myParser.visitAllNodesWith(visitor); String textInPage = visitor.getTitle(); System.out.println(textInPage); /** HtmlPage visitor = new HtmlPage(myParser); myParser.visitAllNodesWith(visitor); String textInPage = visitor.getTitle(); System.out.println(textInPage); /** TextExtractingVisitor visitor = new TextExtractingVisitor(); myParser.visitAllNodesWith(visitor); String textInPage = visitor.getExtractedText(); System.out.println(textInPage); /** myParser = Parser.createParser(content, "GBK"); NodeFilter textFilter = new NodeClassFilter(TextNode.class); //暫時不處理 meta OrFilter lastFilter = new OrFilter(); nodeList = myParser.parse(lastFilter); Node[] nodes = nodeList.toNodeArray(); for (int i = 0; i < nodes.length; i++) String line = ""; line = linknode.getLink(); if (isTrimEmpty(line)) System.out.println(line); /** myParser = Parser.createParser(content, null); nodes = myParser.extractAllNodesThatAre(TextNode.class); //exception could be thrown here for (int i = 0; i < nodes.length; i++) } /** try String dataLine = ""; ins.close(); return sbStr.toString(); /** /** }
|