java課程設計團隊博客《基於學院的搜索引擎》

JAVA課程設計

基於學院網站的搜索引擎

對學院網站用爬蟲進行抓取、建索(須要中文分詞)、排序(可選)、搜索、數據摘要高亮、分頁顯示。Web界面。

1、團隊介紹


2、項目git地址

碼雲地址

3、項目git提交記錄截圖

4、項目主要使用技術

  • Httplcient
  • Jsoup
  • 多線程
  • 數據庫dao模式
  • Lucene+IKAnanyzer
  • Javascript /jQuery
  • Bootstrap
  • Web

5、項目其他特色

  • 容錯處理完善
  • 界面美觀
  • 有配置文件
  • 數據量大的時候查詢速度依舊快

6、項目功能架構圖與主要功能流程圖

7、項目運行截圖

用爬蟲把數據爬取解析後存到數據庫裏面



把數據庫裏面的內容建索生成的索引文件



設計的前端界面

設計的logo,如今是2018年,也是狗年,而後就把2018變成狗。 這個是網站上直接下載下來的,不過也是找了很久

附上連接
javascript

咱們再搜索框輸入咱們要查詢的東西

而後展現搜索獲得的結果

這個是gif動圖,由於色彩太豐富了,致使錄製的時候,看起來顏色有點變

8、項目總體流程

1.爬蟲+數據庫

對爬蟲瞭解也不是很深,粗略的講一下大致思路。
因爲知識積累尚淺,平時通常採用這3種方式爬取基本的網頁。
①觀察url的規律,有些url多是id=xxx,這個xxx是從1開始遞增的,這個時候咱們就是能夠去遍歷。具體規律看實際,這邊只是一個思路
②查看網頁源碼,看看其屬性class什麼的,看看是否有規律,若是有,那就按照這個來,這個用Beautifulsoul4的時候常常用,叫作CSS 選擇器。咱們在寫 CSS 時,標籤名不加任何修飾,類名前加點.,id名前加 #,在這裏咱們也能夠利用相似的方法來篩選元素。由於感受寫正則匹配太麻煩(實際上是不太精通),喜歡這種懶人式的操做。
還有的話就是用xpath語法來獲取你想要的東西,之前有用過python中的scrapy框架,裏面就有xpath語法,剛剛查了一下,用java寫的爬蟲中也有。反正如今瀏覽器xpath路徑已經給你弄好,複製粘貼修改一下就ok。html

③獲取屬性爲<a href="xxx">的連接,能夠用jsoup或者其餘語言的其餘解析器,也能夠用正則去匹配。最後進行篩選,再去請求,再去匹配....


首先觀察網站結構,先大致翻看url結構,發現每篇正文都是info/xxxx/xxxx.htm,因而有了第一個思路,對xxxx進行遍歷,獲得每個url。再利用jsoup去進行解析,存進數據庫。



而後又查看一下導航欄的源碼,發現有驚喜,就是主幹url的class都是menu0_0_

想到這就是繼續深刻,猜想副幹那些也有這種規律。果不其然

綜合上述方法,權衡利弊後,選擇第二種方法。


觀察學院網站發現,學院網站有基本的三層,第一層就是導航欄,請求menu0_0_,大概有幾十個url

而後點擊進去,到第二層,相似於文章的目錄同樣,這邊的話上第一步的主幹url請求之後,咱們用選擇器選擇class="c124907"的連接

請求第二層的url,點擊,到達第三層,這個時候咱們就是在正文中提取本身想要的信息了,好比選擇title,正文內容啊,.contentstyle124904等等
前端




這邊的話還碰到個小坑,剛剛開始爬取的時候,大概只是爬取到了300多條連接,想一想以爲有點少,再翻看目錄的話發現,還有分頁的沒有考慮到,因而,把分頁的給弄下來
,分頁的弄下來後,發現url數仍是有點少,繼續看,發現從當前頁開始只顯示7頁的連接,其餘剩餘的這個從源碼又爬取不到

觀察這些頁數的連接後,能夠發現?a2t=19&a2p=3&a2c=10&urltype=tree.TreeTempUrl&wbtreeid=1114 ,a2t表明的是總頁數, a2p表明的是當前頁數,webtreeid這個不一樣頁面id不一樣
因而正則去匹配,當a2t大於7的時候,把這個a2p裏面的參數本身弄一個循環來添加。
java

到最後的話裏面有重複的,要去一下重

以上的操做都是基於多線程來完成的。


獲取到了所有url後(帶有info正文的,一些可有可無的url去掉)。
而後去請求這些url,裏面也是有些多是無效的,出現404 not found 啊什麼的,利用jsoup,若是解析不到特定的字段,捨棄這個url。
這邊的話利用多線程去請求正文標題等內容,請求完成之後就直接插入數據庫了python

數據庫的話採用dao模式

創建一個爬蟲的類
mysql

2 檢索部分

剛剛開始作這個搜索引擎的時候是想直接用select * from xxx where xxxxx like '%xx%' 這個語法的來返回查詢結果的,和老師交流了一下,改用Lucene+IkAnalyzer進行分詞索引,而後查詢。若是用 like語法的話,這個搜索引擎就失去了大半部分意義了。由於搜索引擎講究的是高效,在數據量小的時候採用sql語法查詢和在索引中進行查詢差異不是太大,但數據量大的時候差異就出來了。在網上看到過一個案例, lucene在查找100W 數據的時間 控制在0.02秒左右,相反的在Sql中100W的全文檢索的話須要10秒左右。有一個正採用lucene開發小型的搜索引擎的人測試,sql 100w之內的數據 仍是勉強能OK . lucene 就不限制了.(目前 正在作的一個搜索引擎 數據量5T 將近5億數據 時間維持在0.4秒之內)jquery

首先,咱們要來理解lucene是什麼?能幹什麼?
Lucene 是一個高效的,基於Java 的全文檢索庫
全文檢索的定義:全文檢索就是先建立索引,而後根據索引來進行搜索的過程,就叫全文檢索。
全文檢索首先對要搜索的數據或者是文檔進行分詞,而後造成索引,經過查詢索引來查詢文檔。原理和查字典同樣,對於一個不認識的字,咱們先在偏旁部首表中找到偏旁,而後跳到全部帶這個偏旁的字裏面,再來搜尋這個字。再跳到這個詞指定的頁數,獲得咱們想要的結果。創建索引的話就是把查字典的過程給逆過來。對於一大段內容,先利用分詞器IKAnalyzer進行分詞(拆分紅單獨的字或詞,去除標點,停詞等),而後創建索引。搜索的話就和查字典同樣了


這邊用processon弄了一個簡單的圖git

咱們來看一下索引的內部結構,好比咱們有10條數據庫的記錄,建成索引文檔後docid編號爲1-10web

將其裏面的內容進行建成索引文檔
每一個字符串都指向包含此字符串的文檔(Document)鏈表,此文檔鏈表稱爲倒排表算法

好比咱們輸入」計算機學院畢業生」進行查詢,
這個時候IKAnalyzer會對其進行分詞,拆分紅計算機 算 學 學院 院 ..etc.
取出包含畢業、計算機、學院的文檔鏈表進行合併,獲得包含三者的文檔,而後包含倆個的,一個的文檔鏈表。

那麼返回了那麼多的結果,如何判斷哪些是和咱們的想要的結果最接近的呢??
Term Frequency (tf):這個詞在文檔中出現的次數
Document Frequency (df):有多少文檔包含這個Term

一個文檔中包含了不少的詞(Term),其中找出詞在文檔中重要性的過程稱爲計算出詞的權重(Weight),若是一個詞在一個文檔中出現次數越多說明越重要,但在其餘的文檔中出現的次數也越多,那麼重要性就會下降,好比

判斷Term之間的關係從而獲得文檔相關性的過程,也即向量空間模型的算法(VSM)

咱們把文檔看做一系列詞(Term),每個詞(Term)都有一個權重(Term weight),不一樣的詞(Term)根據本身在文檔中的權重來影響文檔相關性的打分計算。因而咱們把全部此文檔中詞(term)的權重(term weight) 看做一個向量。
Document = {term1, term2, …… ,term N}
Document Vector = {weight1, weight2, …… ,weight N}
一樣咱們把查詢語句看做一個簡單的文檔,也用向量來表示。
Query = {term1, term 2, …… , term N}
Query Vector = {weight1, weight2, …… , weight N}
咱們把全部搜索出的文檔向量及查詢向量放到一個N維空間中,每一個詞(term)是一維。

咱們通常認爲兩個向量之間的夾角越小,相關性越大。
因此咱們計算夾角的餘弦值做爲相關性的打分,夾角越小,餘弦值越大,打分越高,相關性越大。

打分的計算表達式爲

3. 先後端

先後端也沒有太多東西,就是一個美化後的表單+jquery進行分頁和bootstrap+jquery修飾美化和數據交互,用表單接收到用戶的數據後傳到後端,而後再根據關鍵字從索引文件中查詢出來,在前端進行展現

第一次用eclipsee,出現的問題就是eclipsee啓動tomcat訪問不到主頁
查閱了相關資料後得知 在eclipsee中啓動tomacat後,它去啓動的web項目並非tomcat文件夾下的webapp下web工程,而是eclipsee中本身的一個文件夾下的web工程。
附上解決連接

而後第二個問題是,如何把我查詢到的數據傳到jsp中,第一次用jsp,不太熟,後面查詢到jsp裏面也是能夠寫java代碼的,這個就是很是6了

直接把獲取到的數據存到crawl裏面去,而後再弄個list 存儲crawl對象,return返回

而後jsp頁面接收

還有一個就是採用jquery進行分頁,大致思路就是,從查詢獲得的結果裏面統計獲取到幾條結果,而後自定義每條顯示幾個結果,向上取整獲得分頁的數目
,若是分頁數目爲1就不分頁,不爲1的話就是採用append方法添加連接,咱們定義一個參數來接受,好比用戶點倒第二頁,那麼參數分別爲用戶要查詢的字段和要看的第幾頁,好比word=xxx&page=2,類推。

字段的話name就是至關於word,a就是至關於page=,變量命名是剛剛開始測試用的,也沒有去改。
效果如前圖。

9、項目關鍵代碼

try {
            Document doc=Jsoup.connect("http://cec.jmu.edu.cn/").get();
            Elements links = doc.select(".menu0_0_");  
            for (Element link : links) {  
                lis1.add(oriurl+link.attr("href"));
            }  
        } catch (IOException e1) {
            e1.printStackTrace();
        }


try {
            CloseableHttpResponse response = httpClient.execute(httpget, context);
            try {
                HttpEntity entity = response.getEntity();
                Document doc=Jsoup.parse(EntityUtils.toString(entity,"utf8"));
                Elements links=doc.select(".c124907");    
                for (Element link : links) {  
                    lis1.add(url +link.attr("href"));
                }   
                String pattern ="\\?a2t=([0-9]{1,})&a2p=[0-9]{1,}&a2c=10&urltype=tree.TreeTempUrl&wbtreeid=([0-9]{1,})";  
                Elements links1=doc.select("a[href]"); 
                for (Element link1 : links1) {
                    String line=link1.attr("href");
                    Pattern r = Pattern.compile(pattern);
                    Matcher m = r.matcher(line);
                    int i=0;
                    if (m.find( )) {
//                       System.out.println("Found value: " + m.group(0) );
                        int j=Integer.parseInt(m.group(1));
                        if(j>7){
                            for(int k=1;k<j+1;k++){
                                lis.add("?a2t="+String.valueOf(j)+"&a2p="+String.valueOf(k)+"&a2c=10&urltype=tree.TreeTempUrl&wbtreeid="+m.group(2));
                                }
                            }
                        else{
                        lis.add(m.group(0));
                        }


CloseableHttpResponse response = httpClient.execute(httpget, context);
            try {
                HttpEntity entity = response.getEntity();
                Document doc=Jsoup.parse(EntityUtils.toString(entity,"utf8"));
                Elements links=doc.select(".c124907");    
                for (Element link : links) {         
                    lis.add(link.attr("href"));
                    
                }


try {
                HttpEntity entity = response.getEntity();
                Document doc=Jsoup.parse(EntityUtils.toString(entity,"utf8"));
                String title = doc.select(".contentstyle124904").text();


Crawl crawl=new Crawl(httpget.getURI().toString(),doc.title().toString(),title);
                CrawlDaoImpl test=new CrawlDaoImpl();
                try {
                    if(bool){
                    test.add(crawl);
                    System.out.println(httpget.toString()+"添加成功");
                    }
                    
                    else{
                        System.out.println("添加失敗");


jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.username=root
jdbc.password=root
jdbc.driver=com.mysql.jdbc.Driver


@Override
    public Crawl findById(int id) throws SQLException {
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        Crawl p = null;
        String sql = "select url,abs,description from crawl where id=?";
        try{
            conn = DBUtils.getConnection();
            ps = conn.prepareStatement(sql);
            ps.setInt(1, id);
            rs = ps.executeQuery();
            if(rs.next()){
                p = new Crawl();
                p.setId(id);
                p.setUrl(rs.getString(1));
                p.setAbs(rs.getString(2));
                p.setDescription(rs.getString(3));
            }
        }catch(SQLException e){
            e.printStackTrace();
            throw new SQLException("*");
        }finally{
            DBUtils.close(rs, ps, conn);
        }
        return p;
    }


public class IndexManager {
    @Test
    public void createIndex() throws Exception {
        // 採集數據
        CrawlDao dao = new CrawlDaoImpl();
        List<Crawl> list = dao.findAll();
        // 將採集到的數據封裝到Document對象中
        List<Document> docList = new ArrayList();
        Document document;
        for (Crawl crawl : list) {
            document = new Document();
            // store:若是是yes,則說明存儲到文檔域中
            Field id = new IntField("id", crawl.getId(), Store.YES);
            Field url = new StoredField("url", crawl.getUrl());
            Field abs = new StoredField("abs", crawl.getAbs());
            Field description = new TextField("description",
                    crawl.getDescription(), Store.YES);
            document.add(id);
            document.add(url);
            document.add(abs);
            document.add(description);
            docList.add(document);
        }
        // 建立分詞器,標準分詞器
        // Analyzer analyzer = new StandardAnalyzer();
        // 使用ikanalyzer
        Analyzer analyzer = new IKAnalyzer();
        // 建立IndexWriter
        IndexWriterConfig cfg = new IndexWriterConfig(Version.LUCENE_4_10_3,
                analyzer);
        // 指定索引庫的地址
        File indexFile = new File("C:\\test1\\aaa\\");
        Directory directory = FSDirectory.open(indexFile);
        IndexWriter writer = new IndexWriter(directory, cfg);
        // 經過IndexWriter對象將Document寫入到索引庫中
        for (Document doc : docList) {
            writer.addDocument(doc);
        }
        writer.close();
    }


public class IndexSearch {
    
    List<Crawl> lis1=new ArrayList();
    public List doSearch(Query query) throws InvalidTokenOffsetsException {
        // 建立IndexSearcher
        // 指定索引庫的地址
        try {
            File indexFile = new File("C:\\test1\\aaa\\");
            Directory directory = FSDirectory.open(indexFile);
            IndexReader reader = DirectoryReader.open(directory);
            IndexSearcher searcher = new IndexSearcher(reader);
            // 經過searcher來搜索索引庫
            // 第二個參數:指定須要顯示的頂部記錄的N條
            TopDocs topDocs = searcher.search(query, 20);
            // 根據查詢條件匹配出的記錄總數
            int count = topDocs.totalHits;
//          ScoreDoc[] scoreDocs = topDocs.scoreDocs;
            String filed="description";  
//          TopDocs top=searcher.search(query, 100);  
            QueryScorer score=new QueryScorer(query,filed);//傳入評分  
            SimpleHTMLFormatter fors=new SimpleHTMLFormatter("<span style=\"color:red;\">", "</span>");//定製高亮標籤  
            Highlighter  highlighter=new Highlighter(fors,score);//高亮分析器  
//           highlighter.setMaxDocCharsToAnalyze(10);//設置高亮處理的字符個數  
            for(ScoreDoc sd:topDocs.scoreDocs){  
                Document doc=searcher.doc(sd.doc);  
                String description=doc.get(filed);  
           //Lucene中分詞的全部信息咱們均可以從TokenStream流中獲取.  
                TokenStream token=TokenSources.getAnyTokenStream(searcher.getIndexReader(), sd.doc, "description", new IKAnalyzer(true));//獲取tokenstream  
                Fragmenter  fragment=new SimpleSpanFragmenter(score);  //根據這個評分新建一個對象     
                highlighter.setTextFragmenter(fragment);  //必須選取最合適的    
                highlighter.setTextFragmenter(new SimpleFragmenter());//設置每次返回的字符數 
                String str=highlighter.getBestFragment(token, description);//獲取高亮的片斷,能夠對其數量進行限制  
                Crawl crawl = new Crawl();
                crawl.setDescription(str);
                crawl.setAbs(doc.get("abs"));
                crawl.setUrl(doc.get("url"));
                lis1.add(crawl);
            }       
            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return lis1;
        
    }


<div id="test"> <img src="./img/logo.png" height="300" width="250"/></div>

    <form action="./query2.jsp" method="GET">
        <div class="search-wrapper">
            <div class="input-holder">
                <input type="text" class="search-input" placeholder="" name="name"/>
                <button class="search-icon" onclick="searchToggle(this, event);"><span></span></button>
            </div>
            <span class="close" onclick="searchToggle(this, event);"></span>
            <div class="result-container">

            </div>
        </div>
    </form>


<script src="js/jquery-1.11.0.min.js" type="text/javascript"></script>
<script type="text/javascript">
    function searchToggle(obj, evt){
        var container = $(obj).closest('.search-wrapper');

        if(!container.hasClass('active')){
              container.addClass('active');
              evt.preventDefault();
        }
        else if(container.hasClass('active') && $(obj).closest('.input-holder').length == 0){
              container.removeClass('active');
        
              container.find('.search-input').val('');
        
              container.find('.result-container').fadeOut(100, function(){$(this).empty();});
        }
    }

    function submitFn(obj, evt){
        value = $(obj).find('.search-input').val().trim();

        _html = "Yup yup! Your search text sounds like this: ";
        if(!value.length){
            _html = "Yup yup! Add some text friend :D";
        }
        else{
            _html += "<b>" + value + "</b>";
        }

        $(obj).find('.result-container').html('<span>' + _html + '</span>');
        $(obj).find('.result-container').fadeIn(100);

        evt.preventDefault();
    }
</script>


<script type="text/javascript">


$(function(){
    var Count = "<%=i %>";//記錄條數
    var tmp = "<%=test %>";
    var PageSize=5;//設置每頁示數目
    var PageCount=Math.ceil(Count/PageSize);//計算總頁數
    var currentPage =1;//當前頁,默認爲1。
    //造個簡單的分頁按鈕
    for(var i=1;i<=PageCount;i++){
        if(PageCount==1){
        }//若是頁數爲1的話,那麼咱們就是不分頁

        else{
        var pageN='<li style=\"font-size:30px\"><a href="?name='+tmp+'&a='+i+'">'+i+'</a></li>';
        
        $('.pagination').append(pageN);
    }

    }
    
    //顯示默認頁(第一頁)

});  
</script>


<%

String d =request.getParameter("a");    
//out.print(d+"<br>");
int b=0;
int k=0;
if(i!=0&&d==null){
    for(Crawl crawl: lis){
        if(5>k&&k>=0){
            out.print("<h3><p class=\"text-center\"><a href=\""+crawl.getUrl()+"\">"+crawl.getAbs()+"</a></p></h3>"); 
            out.print("<p class=\"text-center\">"+crawl.getDescription()+"<br>");  
            out.print("<br>");
        }
        k=k+1;
    }
    
    
}
else{
if(d!=null){
int c=Integer.valueOf(d);
//out.print(c);
for(Crawl crawl: lis){
    if(c*5>b&&b>=(c-1)*5){
        if(crawl.getDescription()==null){
            out.print("");
        }
        else{
            out.print("<h3><p class=\"text-center\"><a href=\""+crawl.getUrl()+"\">"+crawl.getAbs()+"</a></p></h3>"); 
            out.print("<p class=\"text-center\">"+crawl.getDescription()+"<br>"); 
        out.print("<br>");

        }
    }
    b=b+1;  
}
}
}
%>


10、尚待改進或者新的想法

變量的命名不太規範

能夠嘗試着去作一個只有修改部分參數,就能夠去爬取別的網站的搜索引擎,甚至更大


團隊成員任務分配

姓名

任務

袁德興

利用Lucene和IKanalyzer進行檢索,部分先後端內容與模塊銜接

陳芳毅

採用httpclient和jsoup,進行爬取和解析,部分數據庫內容

韓燁

採用數據庫的dao模式將jsoup解析後的內容進行存儲,部分前端和logo的設計

劉兵

採用bootstrap和jsp等進行前端界面的設計和後端代碼實現

張晨曦

採用jquery和jsp等進行前端界面的設計和後端代碼的實現

11、本次課設中大佬們博客內容提供的幫助很是大,衷心的感謝。

httpclient官方文檔
lucene學習教程
lucene學習5分鐘
lucene學習
lucene4入門實例
lucene高亮
jsp教程
jquery教程
bootstrap教程

掃描下方二維碼關注我公衆號

或者微信搜索:凡哥共享

相關文章
相關標籤/搜索