4.JTest 重量級(jí)的商業(yè)工具
目前版本7.0.7,有for eclipse的插件.網(wǎng)址是http://www.parasoft.com/
不推薦使用,不過(guò)功能強(qiáng)大,可以進(jìn)行代碼檢查,可以自動(dòng)生成單元測(cè)試和進(jìn)行單元測(cè)試.(不過(guò)就是太慢了,而且生成的單元測(cè)試沒(méi)太大用途)
使用感覺(jué):
安裝上插件后,對(duì)自己的項(xiàng)目進(jìn)行檢查,發(fā)現(xiàn)警告太多了,有點(diǎn)發(fā)蒙的感覺(jué).不過(guò)把警告看一遍,覺(jué)得都很有道理,有些也確實(shí)是一些錯(cuò)誤.
當(dāng)然PMD和CheckStyle的規(guī)范太嚴(yán)格,最后還是配置了一下.
通過(guò)改正警告,感覺(jué)還是不錯(cuò),至少可以說(shuō)自己的代碼可以通過(guò)工具的檢測(cè)了.
當(dāng)然基礎(chǔ)代碼和項(xiàng)目代碼還是不一樣的,基礎(chǔ)代碼往往比較復(fù)雜,所以和普通項(xiàng)目代碼的規(guī)范應(yīng)該有所不同.有些規(guī)則只能用在普通代碼上,用在基礎(chǔ)類代碼上往往沒(méi)法處理.
其他
代碼查錯(cuò)推薦使用Findbugs和PMD,代碼書寫規(guī)范推薦使用CheckStyle進(jìn)行檢查.這樣不僅能查出一些基本的錯(cuò)誤,也能提高項(xiàng)目的代碼質(zhì)量.對(duì)提高自己的代碼水平也是非常好.
推薦項(xiàng)目組建立統(tǒng)一的規(guī)則,代碼復(fù)查的時(shí)候就使用這些工具,省時(shí)省力.
實(shí)乃居家旅行,殺人越貨必備之工具也.(因?yàn)榭隙ㄓ腥艘R你,呵呵,也是你找"差"的工具)
根據(jù)XML中CDATA類型的規(guī)范可以知道:"&"和"<"不需要也不能被轉(zhuǎn)換. ">" 如果出現(xiàn)在"]]>" 的內(nèi)容而不是表示結(jié)束時(shí),必須被轉(zhuǎn)義為>
但是這樣就存在一個(gè)問(wèn)題,如果我需要輸入"]]>",正確的處理是保存為"]]>",但是如果我想輸入"]]>",那么應(yīng)該如何保存哪? 我想了很久,除非加空格或者采用特殊的辦法,否則是沒(méi)有辦法解決的.
1.如果我們不考慮輸入"]]>"的問(wèn)題,來(lái)考慮一下"]]>"的處理,看看各種XML解析器是如何處理的?
xml解析器的測(cè)試包含2個(gè)部分:設(shè)置cdata類型的數(shù)據(jù)和讀出cdata類型的數(shù)據(jù).
首先我們寫一個(gè)測(cè)試的例子,計(jì)劃使用JDom 1.0和Dom4j來(lái)測(cè)試一下:
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); } } |
編譯并運(yùn)行上面的代碼結(jié)果,我們可以看到JDom無(wú)法設(shè)置Cdata的值為"]]>",會(huì)報(bào)異常.從xml字符串讀出cdata的結(jié)果也沒(méi)有把字串"]]>"翻譯為"]]>".
接著再來(lái)測(cè)試Dom4J:
package com.xml.test; import java.io.StringReader; import org.dom4j.*; import org.dom4j.io.SAXReader; import org.dom4j.tree.DefaultCDATA; /** * 測(cè)試XML的CData數(shù)據(jù)類型. * * @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()); } } |
根據(jù)上面的測(cè)試我們可以得出結(jié)論:很多xml解析器沒(méi)有正確解析cdata的數(shù)據(jù),(jdom和dom4j用的人比較多),不要太相信這些解析器.
2.我們?cè)賮?lái)看看閱讀RSS的RSS閱讀器吧,例如FeedDemon和POTU,我們準(zhǔn)備了一個(gè)CData類型的description字段,來(lái)進(jìn)行測(cè)試.
內(nèi)容:
<?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> |
結(jié)果:
1.POTU沒(méi)有做任何處理
2.FeedDemon做了處理,不過(guò)同時(shí)也把其他的> <等等都翻譯了,這就更不對(duì)了..
本來(lái)我是打算在RSS里使用CDATA類型的description字段的,經(jīng)過(guò)幾番試驗(yàn)和測(cè)試,最后決定還是使用普通的description字段了,不在使用CDATA了.
CDATA? 雞肋乎? 呵呵
關(guān)鍵字:rss,freemarker,rss.xml,webwork2
RSS在網(wǎng)絡(luò)上大行其道,各種網(wǎng)站都加上RSS支持,我最近也研究了一下,給我的文章也加上了RSS訂閱.
RSS目前用的也有幾個(gè)版本,很是混亂,下面以RSS2.0為例來(lái)說(shuō)明.
網(wǎng)絡(luò)上有個(gè)rsslibj庫(kù),是用來(lái)生成rss支持文件的,不過(guò)已經(jīng)好久沒(méi)有更新了,它是用xml的方式生成的.本文的例子不用到任何xml解析器,不過(guò)當(dāng)然要知道最后生成的XML文件的格式才行,關(guān)于RSS規(guī)范,可以瀏覽一下 http://blogs.law.harvard.edu/tech/rss .
在計(jì)劃生成RSS文件的時(shí)候,順便搜索了一下JIRA和Confluence的程序,發(fā)現(xiàn)它們分別是用模板方式和JSP動(dòng)態(tài)頁(yè)面來(lái)展示的.于是我也想到兩種方式:
1.用FreeMarker生成靜態(tài)文件,適用于更新不是很頻繁的內(nèi)容.
2.用JSP動(dòng)態(tài)展示,適合更新頻率高,種類繁多的內(nèi)容.
還是以本站的新聞舉例,其中的新聞信息類參考 http://www.jscud.com/srun/news/viewhtml/3_2005_8/76.htm ,此處不在列出.
(一) 先說(shuō)FreeMarker方式.
根據(jù)RSS的規(guī)范,得到模板如下:
<?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> |
其中的網(wǎng)址和網(wǎng)站名稱可以根據(jù)自己的實(shí)際情況修改.
我每次取出最新的20條文章來(lái)生成RSS,不過(guò)內(nèi)容比較多,生成的RSS文件比較大,看到有的網(wǎng)站的description只是放了文章摘要的內(nèi)容,這樣文件就小多了.總之是根據(jù)自己的需求設(shè)計(jì)吧.
其中用到的RssUtil函數(shù)庫(kù)的函數(shù)如下(日期的函數(shù)參考上一篇文章):
/** * 把]]>替換為]]> * @param content 內(nèi)容 * @return 格式化后的內(nèi)容 */ public static String formatRssCData(String content) { String result = StringFunc.replace(content,"\\]\\]>","]]>"); return result; } /** * 格式化為xml需要的字符串 * @param field 內(nèi)容 * @return 格式化后的串 */ public static String formatString2XML(String field) { return StringFunc.str2TextXML(field); } public static String getNowDateTime() { return formatRssDate(DateTime.getNowTimestamp()); } |
利用FreeMarker生成靜態(tài)文件的代碼如下:
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; |
新聞系統(tǒng)中調(diào)用重新生成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動(dòng)態(tài)方式
相對(duì)靜態(tài)方式而言,簡(jiǎn)單的多,不過(guò)效率上可能就不太好了.
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的方式簡(jiǎn)單多了,上面的jsp里面還演示了ww:bean的使用 :)
上面的類里面引用了很多其他的工具類,這里不一一列出,可以自己實(shí)現(xiàn)它們,都是很簡(jiǎn)單的類. :)
rss中日期格式要求遵守rfc822規(guī)范,其中是這么寫的:
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是可以省略的,后面的時(shí)間是要求有時(shí)區(qū)的.
示例如下(以在中國(guó)的中文操作系統(tǒng)機(jī)器為例):
1.Tue, 16 Aug 2005 15:33:33 GMT
2.Tue, 16 Aug 2005 23:33:33 +0800
其實(shí)這個(gè)rfc822應(yīng)該也是電子郵件內(nèi)容格式的規(guī)范,找一個(gè)郵件看看內(nèi)容,也可以看出,郵件的時(shí)間格式也是遵循這個(gè)規(guī)范的.
要輸入第一種格式,使用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意思是代表"中國(guó)時(shí)間",但是一經(jīng)搜索,發(fā)現(xiàn)CST代表了好幾個(gè)時(shí)區(qū),太讓人混亂了.而在RTF822里面,CST僅代表美國(guó)中部時(shí)間.所以如果使用SimpleDateFormat,要設(shè)置時(shí)區(qū)以GMT表示,否則容易讓人迷惑而且不知道是那個(gè)時(shí)區(qū).
假設(shè)你在中國(guó),想根據(jù)當(dāng)?shù)貢r(shí)間輸入復(fù)合當(dāng)?shù)貢r(shí)間的字符串,讓人一看就能明白文章的日期,那么就使用第二種格式.(我推薦使用第二種方式,當(dāng)然你的頻道主要給外國(guó)朋友瀏覽登除外)
上面說(shuō)到和郵件有關(guān),于是我們看看JavaMail包里面的javax.mail.internet.MailDateFormat,可以用來(lái)格式化日期:
(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)); |
輸出結(jié)果為:
Tue, 16 Aug 2005 23:33:33 +0800 (CST)
如果設(shè)置了時(shí)區(qū)為GMT,則輸出:
Tue, 16 Aug 2005 15:33:33 +0000 (GMT)
可以看到相對(duì)RTF822而言,好像多了一個(gè)后面的時(shí)區(qū)的說(shuō)明及其括號(hào).不知道這到底是怎么回事?
在硬盤上查找一番,發(fā)現(xiàn)在JIRA程序和Confluence中的RSS里都使用了這個(gè)日期格式.
注意到這個(gè)不同,我瀏覽了一下outlook Express里面的郵件,發(fā)現(xiàn)兩種時(shí)間格式的郵件都存在,真是讓人迷惑,或許都可以吧,呵呵 :)
如果不想使用MailDateFormat的格式,那么就自己寫一個(gè)類來(lái)實(shí)現(xiàn)吧,例如
public class RssDateFormat extends MailDateFormat { public RssDateFormat() { applyPattern("EEE, d MMM yyyy HH:mm:ss ’XXXXX’"); } } |
至此,我的RSS中的日期終于正確而且讓我滿意了. :)
關(guān)鍵字:lucene,html parser,全文檢索,IndexReader,Document,Field,IndexWriter,Term,HTMLPAGE
無(wú)論是建立索引還是分析內(nèi)容,都是為了用戶的搜索服務(wù).
在Lucene中,如果需要使用搜索,需要使用Searcher類,這是一個(gè)抽象類,它有2個(gè)子類:IndexSearcher和MultiSearcher.
IndexSearcher是對(duì)一個(gè)索引進(jìn)行搜索,如果你需要對(duì)多個(gè)索引進(jìn)行搜索,可以使用MultiSearcher.下面的內(nèi)容只介紹了IndexSearcher.
搜索涉及到幾個(gè)問(wèn)題:分頁(yè),組合條件,根據(jù)條件過(guò)濾,排序等等.
分頁(yè):分頁(yè)在記錄列表的地方都會(huì)遇到,這里不在贅述,我也實(shí)現(xiàn)過(guò)一個(gè)保存分頁(yè)結(jié)果和顯示結(jié)果的類,用于自己的實(shí)際工作,下面也會(huì)用到保存分頁(yè)結(jié)果的類,代碼如下:
package com.jscud.support; /** * 分頁(yè)顯示用的參數(shù). * * @author scud(飛云小俠) http://www.jscud.com * */ public class DivPageInfo { //開始記錄數(shù) private int recStart; //結(jié)束記錄數(shù) private int recEnd; //總頁(yè)數(shù) private int pageCount; //當(dāng)前頁(yè) private int page; //記錄總數(shù) private int recCount; //每頁(yè)記錄數(shù) private int perPageRows; public int getNicePageCount() { return getNicePageNum(pageCount); } //get,set等,不在列出 //...... /** * 得到友好的頁(yè)數(shù)數(shù)字,頁(yè)數(shù)為0時(shí),返回1. * * @return 得到友好的頁(yè)數(shù) */ public static int getNicePageNum(int nPage) { if (nPage == 0) { return 1; } else { return nPage; } } } |
顯示分頁(yè)結(jié)果的類就需要大家根據(jù)自己使用的框架來(lái)具體實(shí)現(xiàn)了.我使用的是WebWork.
組合條件:在Lucene中,搜索的條件可以組合的很復(fù)雜,相關(guān)的類有BooleanQuery, FilteredQuery, MultiTermQuery, PhrasePrefixQuery, PhraseQuery, PrefixQuery, RangeQuery, SpanQuery, TermQuery 等等,從而可以組合出很復(fù)雜的條件用于查詢.
另外QueryParser可以根據(jù)用戶輸入的字符串和設(shè)定的解析器和字段設(shè)置等,可以自動(dòng)產(chǎn)生新的組合條件用于查詢,例如用戶輸入"john AND black",QueryParser可以自己分析出用戶是需要查詢字段中同時(shí)包含"john"和"black"的結(jié)果.
過(guò)濾條件:有時(shí)候根據(jù)具體的用戶需求,有些記錄對(duì)于一些用戶是不可見的,此時(shí)就要使用過(guò)濾器來(lái)防止不合法的用戶看到不應(yīng)該看到的記錄.過(guò)濾器同時(shí)也可以根據(jù)一些具體的條件來(lái)過(guò)濾掉一些用戶不想看到的記錄.如果需要實(shí)現(xiàn)自己的filter,只要參考QueryFilter,DateFilter實(shí)現(xiàn)Filter即可.
排序:有時(shí)候,可能需要根據(jù)某個(gè)字段進(jìn)行排序,例如按照時(shí)間排序.當(dāng)然更多的時(shí)候是按照搜索結(jié)果的符合度進(jìn)行排序,lucene默認(rèn)的排序就是按照符合度來(lái)進(jìn)行排序的.
進(jìn)行搜索的代碼如下,根據(jù)自己的需要進(jìn)行代碼的修改:
/** * 進(jìn)行搜索. * * 參數(shù)依次為:搜索內(nèi)容(支持lucene語(yǔ)法),當(dāng)前頁(yè),每頁(yè)記錄數(shù),分頁(yè)信息對(duì)象 * */ 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(); //分頁(yè)檢索 DivPageInfo.divPage(hits.length(), perpage, page, pageinfo); //取出當(dāng)前頁(yè)的記錄 return docs; |
代碼中出現(xiàn)了一個(gè)新的類Hits,Hits是lucene的搜索結(jié)果集,是lazy load的結(jié)果集,只有你真正訪問(wèn)它,它才去裝載真正的數(shù)據(jù).
代碼中還出現(xiàn)了一個(gè)LuceneDocument,這是為了在頁(yè)面中顯示而寫的一個(gè)輔助類,因?yàn)閘ucene的Document是final的,無(wú)法進(jìn)行擴(kuò)展,而要顯示時(shí)間字段必須要調(diào)用DateField中的函數(shù),這樣在頁(yè)面中顯示就不太直觀了,所以寫了這個(gè)輔助類,代碼如下:
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; /** * 對(duì)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對(duì)結(jié)果集進(jìn)行了顯示,代碼如下:
<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 轉(zhuǎn)載請(qǐng)注明來(lái)源/作者
關(guān)鍵字:lucene,html parser,全文檢索,IndexReader,Document,Field,IndexWriter,Term,HTMLPAGE
在使用lucene對(duì)相關(guān)內(nèi)容進(jìn)行索引時(shí),會(huì)遇到各種格式的內(nèi)容,例如Html,PDF,Word等等,那么我們?nèi)绾螐倪@么文檔中得到我們需要的內(nèi)容哪?例如Html的內(nèi)容,一般我們不需要對(duì)Html標(biāo)簽建立索引,因?yàn)槟遣皇俏覀冃枰阉鞯膬?nèi)容.這個(gè)時(shí)候,我們就需要從Html內(nèi)容中解析出我們所需要的內(nèi)容.對(duì)于PDF,Word文檔,也是類似的要求.
總之,我們只需要從內(nèi)容中提取出我們需要的文本來(lái)建立索引,這樣用戶就能搜索到需要的內(nèi)容,然后訪問(wèn)對(duì)應(yīng)的資源即可.
Lucene本身帶的例子中有一個(gè)解析Html的代碼,不過(guò)不是純JAVA的,所以在網(wǎng)上我又找到了另外一個(gè)Html解析器,網(wǎng)址如下:http://htmlparser.sourceforge.net.
對(duì)PDF解析的相關(guān)項(xiàng)目有很多,例如PDFBox.在PDFBox里面提出pdf的文本內(nèi)容只需要一句話即可:
Document doc = LucenePDFDocument.getDocument( file ); |
/** //設(shè)置編碼:根據(jù)實(shí)際情況修改 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); //暫時(shí)不處理 meta OrFilter lastFilter = new OrFilter(); try //中場(chǎng)退出了 Node[] nodes = nodeList.toNodeArray(); String result = combineNodeText(nodes); //合并節(jié)點(diǎn)的有效內(nèi)容 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; /** * 搜索時(shí)解析Html后返回的頁(yè)面模型. * * @author scud(飛云小俠) http://www.jscud.com * */ public class SearchHtmlPage { /**標(biāo)題*/ private String title; /**內(nèi)容*/ 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; } } |
關(guān)鍵字:lucene,html parser,全文檢索,IndexReader,Document,Field,IndexWriter,Term,HTMLPAGE
Lucene是一個(gè)全文檢索的引擎,目前有Java和.Net 等幾個(gè)版本.Java版本的網(wǎng)址是http://lucene.apache.org.相關(guān)的一個(gè)項(xiàng)目是車東的WebLucene: http://sourceforge.net/projects/weblucene.
首先,基于一個(gè)簡(jiǎn)單的新聞系統(tǒng),要想做全文檢索.新聞系統(tǒng)的管理等在這里不在具體提出,下面列出新聞對(duì)象的類:
注:程序用會(huì)到一些工具類,不在此列出,用戶可以自己實(shí)現(xiàn).
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; /** * 一個(gè)新聞. * * @author scud(飛云小俠) http://www.jscud.com * */ public class NewsItem { private int nid; //新聞編號(hào) private int cid; //類別編號(hào) private String title;//標(biāo)題 private int showtype; //內(nèi)容類型:目前支持url和html private String content;//內(nèi)容 private String url;//對(duì)應(yīng)網(wǎng)址,如果內(nèi)容類型是url的話 private Timestamp addtime; //增加時(shí)間 private int click; //點(diǎn)擊數(shù) //對(duì)應(yīng)的get,set函數(shù),較多不在列出,可以使用工具生成 //...... /** * 按照類型格式化 */ 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 ""; } /** * 靜態(tài)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; } /** * 靜態(tài)Html文件的路徑 */ public String getHtmlFilePath() { int nYear = DateTime.getYear_Date(getAddtime()); int nMonth = DateTime.getMonth_Date(getAddtime()); String sGeneFilePath = getCid() + "_" + nYear + "_" + nMonth; return sGeneFilePath; } } |
可以看到,我們需要對(duì)標(biāo)題和內(nèi)容進(jìn)行檢索,為了這個(gè)目的,我們首先需要來(lái)研究一下lucene.
在Lucene中,如果要進(jìn)行全文檢索,必須要先建立索引然后才能進(jìn)行檢索,當(dāng)然實(shí)際工作中還會(huì)有刪除索引和更新索引的工作.
在此之前,介紹一個(gè)最基本的類(摘抄自http://www.tkk7.com/cap/archive/2005/07/17/7849.html):
Analyzer 文件的分析器(聽起來(lái)別扭,還是叫Analyzer好了)的抽象,這個(gè)類用來(lái)處理分詞(對(duì)中文尤其重要,轉(zhuǎn)換大小寫(Computer->computer,實(shí)現(xiàn)查詢大小寫無(wú)關(guān)),轉(zhuǎn)換詞根(computers->computer),消除stop words等,還負(fù)責(zé)把其他格式文檔轉(zhuǎn)換為純文本等.
在lucene中,一般會(huì)使用StandardAnalyzer來(lái)分析內(nèi)容,它支持中文等多字節(jié)語(yǔ)言,當(dāng)然可以自己實(shí)現(xiàn)特殊的解析器.StandardAnalyzer目前對(duì)中文的處理是按照單字來(lái)處理的,這是最簡(jiǎn)單的辦法,但是也有缺點(diǎn),會(huì)組合出一些沒(méi)有意義的結(jié)果來(lái).
首先我們來(lái)了解建立索引,建立索引包含2種情況,一種是給一條新聞建立索引,另外的情況是在開始或者一定的時(shí)間給批量的新聞建立索引,所以為了通用,我們寫一個(gè)通用的建立索引的函數(shù):
(一般一類的索引都放在一個(gè)目錄下,這個(gè)配置可以在函數(shù)中定義,也可以寫在配置文件中,通過(guò)參數(shù)傳遞給函數(shù).)
/** * 生成索引. * * @param doc 目標(biāo)文檔 * @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,它可以新建索引或者追加索引,但是需要自己判斷.判斷是通過(guò)IndexReader這個(gè)類來(lái)實(shí)現(xiàn)的,函數(shù)如下:
/** * 檢查索引是否存在. * @param indexDir * @return */ public static boolean indexExist(String indexDir) { return IndexReader.indexExists(indexDir); } |
如果每次都是新建索引的話,會(huì)把原來(lái)的記錄刪除,我在使用的時(shí)候一開始就沒(méi)有注意到,后來(lái)觀察了一下索引文件,才發(fā)現(xiàn)這個(gè)問(wèn)題.
還可以看到,建立索引是給用戶的Document對(duì)象建立索引,Document表示索引中的一條文檔記錄.那么我們?nèi)绾谓⒁粋€(gè)文檔那?以新聞系統(tǒng)為例,代碼如下:
/** * 生成新聞的Document. * * @param aNews 一條新聞. * * @return lucene的文檔對(duì)象 */ 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())); //對(duì)Html進(jìn)行解析,如果不是html,則不需要解析.或者根據(jù)格式調(diào)用自己的解析方法 String content = parseHtmlContent(aNews.getContent()); doc.add(Field.UnStored("content", content)); doc.add(Field.Keyword("addtime", aNews.getAddtime())); //可以加入其他的內(nèi)容:例如新聞的評(píng)論等 doc.add(Field.UnStored("other", "")); //訪問(wèn)url String newsUrl = "/srun/news/viewhtml/" + aNews.getHtmlFilePath() + "/" + aNews.getNid() + ".htm"; doc.add(Field.UnIndexed("visiturl", newsUrl)); return doc; } |
通過(guò)上面的代碼,我們把一條新聞轉(zhuǎn)換為lucene的Document對(duì)象,從而進(jìn)行索引工作.在上面的代碼中,我們又引入了lucene中的Field(字段)類.Document文檔就像數(shù)據(jù)庫(kù)中的一條記錄,它有很多字段,每個(gè)字段是一個(gè)Field對(duì)象.
從別的文章摘抄一段關(guān)于Field的說(shuō)明(摘抄自http://www.tkk7.com/cap/archive/2005/07/17/7849.html):
[quote]
類型 Analyzed Indexed Stored 說(shuō)明
Field.Keyword(String,String/Date) N Y Y 這個(gè)Field用來(lái)儲(chǔ)存會(huì)直接用來(lái)檢索的比如(編號(hào),姓名,日期等)
Field.UnIndexed(String,String) N N Y 不會(huì)用來(lái)檢索的信息,但是檢索后需要顯示的,比如,硬件序列號(hào),文檔的url地址
Field.UnStored(String,String) Y Y N 大段文本內(nèi)容,會(huì)用來(lái)檢索,但是檢索后不需要從index中取內(nèi)容,可以根據(jù)url去load真實(shí)的內(nèi)容
Field.Text(String,String) Y Y Y 檢索,獲取都需要的內(nèi)容,直接放index中,不過(guò)這樣會(huì)增大index
Field.Text(String,Reader) Y Y N 如果是一個(gè)Reader, lucene猜測(cè)內(nèi)容比較多,會(huì)采用Unstored的策略.
[/quote]
我們可以看到新聞的編號(hào)是直接用來(lái)檢索的,所以是Keyword類型的字段,新聞的標(biāo)題是需要檢索和顯示用的,所以是Text類型,而新聞的內(nèi)容因?yàn)槭荋tml格式的,所以在經(jīng)過(guò)解析器的處理用,使用的UnStored的格式,而新聞的時(shí)間是直接用來(lái)檢索的,所以是KeyWord類型.為了在新聞索引后用戶可以訪問(wèn)到完整的新聞頁(yè)面,還設(shè)置了一個(gè)UnIndexed類型的訪問(wèn)地址字段.
(對(duì)Html進(jìn)行解析的處理稍后在進(jìn)行講解)
為一條新聞建立索引需要兩個(gè)步驟:獲取Document,傳給makeIndex函數(shù),代碼如下:
public static void makeNewsInfoIndex(NewsItem aNews) { if (null == aNews) { return; } makeIndex(makeNewsSearchDocument(aNews),indexDir); } |
建立索引的工作就進(jìn)行完了,只要在增加新聞后調(diào)用 makeNewsInfoIndex(newsitem); 就可以建立索引了.
如果需要?jiǎng)h除新聞,那么也要?jiǎng)h除對(duì)應(yīng)的索引,刪除索引是通過(guò)IndexReader類來(lái)完成的:
/** * 刪除索引. * @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; |
刪除索引需要一個(gè)條件,類似數(shù)據(jù)庫(kù)中的字段條件,例如刪除一條新聞的代碼如下:
public static void deleteNewsInfoIndex(int nid) { Term aTerm = new Term("nid", String.valueOf(nid)); deleteIndex(aTerm,indexDir); } |
通過(guò)新聞的ID,就可以刪除一條新聞.
如果需要更新新聞,如何更新索引哪? 更新索引需要先刪除索引然后新建索引2個(gè)步驟,其實(shí)就是把上面的代碼組合起來(lái),例如更新一條新聞:
public static void updateNewsInfoIndex(NewsItem aNews) { if (null == aNews) { return; } deleteNewsInfoIndex(aNews.getNid()); makeNewsInfoIndex(aNews); } |
至此,索引的建立更新和刪除就告一段落了.其中批量更新新聞的代碼如下:
(批量更新應(yīng)該在訪問(wèn)人數(shù)少或者后臺(tái)程序在夜間執(zhí)行)
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的全文檢索,在很多地方需要解析或者說(shuō)分析Html內(nèi)容或者Html頁(yè)面,Lucene本身的演示程序中也提供了一個(gè)Html Parser,但是不是純Java的解決方案.于是到處搜索,在網(wǎng)上找到了一個(gè)"HTMLParser".
網(wǎng)址是: http://htmlparser.sourceforge.net ,當(dāng)前版本為1.5.
下載下來(lái),試用一番,感覺(jué)不錯(cuò),完全能滿足lucene解析Html的需求.
過(guò)幾天貼出lucene進(jìn)行全文檢索的代碼.(檢索本站的文章等).
試用代碼如下,供大家參考:
package com.jscud.test; import java.io.BufferedReader; import org.htmlparser.Node; import com.jscud.util.LogMan; //一個(gè)日志記錄類 /** 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); //訪問(wèn)外部資源,相對(duì)慢 } /** //設(shè)置編碼 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); //暫時(shí)不處理 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(); /** /** }
|