在猿帮帮中,我们希望可以从互联网中获取资源,为用户提供便捷的搜索和推荐功能,以期使用户获得更好的体验并解决初期数据库中问题和答案数目过少,无法有效吸引用户的问题。

功能模块设计

Clipboard Image.png

在猿帮帮中,我们希望拓展传统的Q&A网站的功能,因此我们为问答模块提供了三个数据源:用户在线回答,历史问题资源以及网络中的相关资源。上图是我们的系统结构(问答功能),左边部分属于本文所述搜索推荐模块,主要包括三个子模块:网络爬虫,资源库和搜索引擎。网络爬虫的主要功能是从互联网中爬取与相关的知识并以一定的格式存储到资源库中,推荐引擎用于根据用户提问信息,从资源库中搜索相关答案并将结果返回答题模块。

在我们当前版本中,网络爬虫爬取的数据来自CSDN和OSChina两个网站的博客,爬取的数据非常丰富,包括博客题目,标签,作者,内容等。在数据存储方面,基于我们目前的需求,我们对博客内容选择四个字段描述{id,title,tags,url},当用户进行提问的时候,我们的推荐引擎根据用户问题和title字段进行模糊查询,返回结果后为用户提供博客url。简单的数据存储格式弊端在于当我们需要优化推荐引擎的时候,由于信息量不足无法提取丰富的特征,与此同时,考虑到今后的数据库更新和维护,我们在代码中预留了四个字段{author,comments,content,isUsable}。

具体设计

yuanbangbangsearch.jpg

在搜索推荐模块,我们的具体设计依赖于spring boot框架,通过位于中心的WebCrawlerController屏蔽内部爬虫细节,同时结合对两个Service的调用,控制本模块的运转。在网络爬虫的实现中,我们使用WebMagic网络爬虫框架,爬取策略为:使用两个线程池A和B,A线程池用于基于baseurl进行全网搜索,获取所有用户名,并保存在队列中;B线程池用于从队列中获取用户名,并生成该用户博客列表url,根据该博客列表逐一进入博客爬取其中的内容。例如,当我们爬取CSDN网站时,我们使用两个线程实例化CSDNGateWay和CSDNPageProcessor两个对象,CSDNGateWay用于向从给定基url和site domain对全网进行递归搜索,并提取用户名,关键代码如下:

    private String blogFlag = "/article/details/";
    public CSDNGateWay() {
        baseUrl = "http://blog.csdn.net/";
        alllinksRex = "href=";
        linksRex = null;
        site = Site.me().setDomain("blog.csdn.net").addStartUrl("http://blog.csdn.net");
        site.setSleepTime(1);
    }

    public void Process(Page page){
    List<String> alllinks = page.getHtml().links().all();
        //System.out.println(alllinks);
        for(int i=0;i<alllinks.size();i++){
            if(alllinks.get(i).contains(blogFlag) && alllinks.get(i).contains(baseUrl)){
                String name = alllinks.get(i).split(baseUrl)[1].split("/")[0];
                if(!UrlQueue.allNames.contains(name)){
                    UrlQueue.allNames.add(name);
                }
         }
     }

CSDNPageProcessor用于针对某一个用户博客列表,递归爬取所有博客内容,其关键在于html文件的提取和博客url的生成和验证,关键代码如下:

public CSDNPageProcessor(String url){
        site = Site.me().setDomain("blog.csdn.net");
        site.setSleepTime(1);

        blogFlag="/article/details/";     
        linksRex="//div[@class='list_item article_item']/div[@class='article_title']/h1/span/a/@href"; //链接列表过滤表达式
        titlesRex="//div[@class='list_item article_item']/div[@class='article_title']/h1/span/a/text()";//title列表过滤表达式

        contentRex="div.article_content";                                                               //内容过滤表达式
        titleRex="//div[@class='details']/div[@class='article_title']/h1/span/a/text()";                //title过滤表达式
        tagsRex="//div[@class='article_l']/span/a/text()";                                              //tags过滤表达式

        this.url=url;

        if(!url.contains(blogFlag)){
            name = url.split("/")[url.split("/").length - 1];
        }else{
                String[] embedd = url.split("/");
                int i = 0;
                for( ; i < embedd.length ; i++){
                        if(embedd[i].equals("article")){
                                break;
                        }
                }
                name = embedd[i-1];
        }//Extract name section

        //http://blog.csdn.net/cxhzqhzq/article/list/2
        PagelinksRex="http://blog\\.csdn\\.net/"+name+"/article/list/\\d+"; 
        //System.out.println(url);
    }

针对同数据库的交互过程和模糊查询匹配过程,我们使用两个Service实现。WebCrawlerService封装数据库操作,基于spring boot的强大功能;WebSearchService封装根据用户提问搜索数据库操作;具体定义了如下service:

public interface WebCrawlerService {

    public WebResource createWebResource(WebResource webResource);

    public  WebResource fineWebResourceById(int webid);

    public WebResource findWebResourceByTitle(String title);

    public List<WebResource> getAll();

    public List<WebResource> getAllByAuthor();

    public List<WebResource> getAllByTags(List<String> tags);

    public List<WebResource> findByTitle(String title);

}

public interface WebSearchService {

    public List<WebResource> searchByTitle(String title);

    public List<WebResource> searchByContent(String author);

}

最后,我们来看本模块的核心WebCrawlerController。这个类整合后台爬虫和Service,允许前端以rest形式访问其中的方法,同时,其中的方法以json格式返回数据。直接为前端提供搜索结果的函数为getWebResources,该函数调用WebSearchService,并返回20个搜索结果。具体代码如下:

    @RequestMapping(value = "", method = RequestMethod.GET)
    @ResponseBody
    public List<WebResourceDTO> getWebResources(@RequestParam(value = "title", required = false) String title){
        List<WebResource> webResources1 = webSearchService.searchByTitle(title);
        List<WebResource> webResources = new ArrayList<WebResource>();
        for(int i=0; i<webResources1.size(); i++ ){
            //System.out.println(webResources1.get(i).getTitle());
            webResources.add(webResources1.get(i));
            if(i > 20){
                break;
            }
        }
        List<WebResourceDTO> webResourceDTOs = Lists.newArrayList(Lists.transform(webResources,
                WebCrawlerTransform.WEBRESOURCE_TO_WEBRESOURCEDTO_FUNCTION));

        return webResourceDTOs;
    }

搜索与推荐

推荐引擎是我们需要核心研究的点,但是我们的第一版几乎没有在这里做出任何有趣的事情。在之后的版本中,我们的重点会放在这个模块,希望做出基于用户技术提问的精准推荐和问答搜索。目前,我们只是做了一个简单的demo,具体使用jieba分词工具对用户问题进行分词,然后对于每一个词,我们根据分词结果在数据库中对博客题目进行模糊查询,并返回数据。这里,我们做了一个小trick,我们认为对于一个普通的问题而言,词语位置靠近句子中央,其重要性相对比较高;通常,一个问题的结构可以是“请问当XX出现XX问题,我应该怎么办?”,也可以是“为什么XX在XX的时候出现error?”,我们在搜索过程中从语句中央的词汇开始向两边蔓延式搜索,这样当我们在WebCrawlerController返回结果时候,优先返回中央位置的词汇搜索结果。具体代码如下:

public List<WebResource> searchByTitle(String title) {
    JiebaSegmenter segmenter = new JiebaSegmenter();
        List<SegToken> list = segmenter.process(title, SegMode.SEARCH);
        List<WebResource> lw = new ArrayList<WebResource>();
        lw.addAll(webResourceRepository.getByTitle("%" + list.get(list.size()/2).word + "%"));
        for(int i=1;i<list.size()/2+1;i++){
            if (list.get(i).word.length()>=2){
                //System.out.println(list.get(i).word);
                if(list.size()/2+i<list.size())
                    lw.addAll(webResourceRepository.getByTitle("%" + list.get(list.size()/2+i).word + "%"));
                lw.addAll(webResourceRepository.getByTitle("%" + list.get(list.size()/2-i).word + "%"));
            }
        }
    return lw;
}

迭代与重构过程

我们的开发大致分为三个迭代周期和阶段:

  1. 开发初期爬虫demo,使用的是WebMagic框架的PipeLine接口进行输出。爬虫爬取策略为保持两个线程池A和B,A线程池用于爬取网站中包含的所有链接url(在baseurl的基础上),B线程池用于正则过滤A线程池输出的url,提取关于博客的url直接获取其中的html标签下的各项内容。
  2. 这一阶段我们发现A线程池提取的url数目太多,B线程池来不及处理,导致内存溢出。因此,我们重构了爬虫,并改为了以用户名为核心的爬取策略。与此同时,我们需要考虑存放在数据库的表结构,我们设计了七个字段几乎包含了所有可以爬下来的信息。
  3. 这一阶段,我们发现数据库的表过于庞杂,很多字段在当前没有用;同时,博客内容是html文本格式,会大量占据空间。我们决定修改数据库表结构,抽取当前使用的字段,即使是我们以后需要向用户展示该资源,我们在查询数据库后根据url进行实时爬取;最后,我们添加了基本的搜索和推荐功能。

未来工作

我们希望一直将这个项目做下去,不仅作为我们的一个作业,更作为我们提高工程实践能力一个重要机会。本模块中,未来工作的重点在于:

  1. 搜索推荐模块的研究,特征提取与推荐算法的选择,实现和验证。
  2. 考虑定期爬取数据以及数据库的更新维护,考虑当数据量巨大时如何分表以及向分布式存储过度。
  3. 考虑拓展爬虫范围,并重新整理重构代码,增加可读性。

登录发表评论 注册

反馈意见