美文网首页爬虫
聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎

聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎

作者: 江湖十年 | 来源:发表于2018-07-07 14:07 被阅读28次

    了解搜索网站大体功能

    搜索网站首页

    image.png

    搜索网站搜素结果页

    image.png

    es完成搜索建议-搜索建议字段保存

    事实上 elasticsearch 自身是支持搜索建议的,当我们在输入框输入内容的时候,可以通过 elasticsearch 的接口完成智能提示,甚至可以发现,搜索框中输入的是错误的单词, java 写成了 jaav,elasticsearch 甚至完成了纠错,可以智能提示可能需要搜索的内容

    image.png

    elasticsearch 搜索建议自动补全文档:https://www.elastic.co/guide/en/elasticsearch/reference/5.3/search-suggesters-completion.html

    查看官方文档

    image.png

    所以首先需要定义的就是这个新增 mapping,在 ArticleSpider 项目中,mapping 是通过 model 去定义的,现在需要解决的首个问题就是如何在 model 中新增这个 字段,并且将其 type 设为 completion

    修改之前写的 es_types.py 文件,给 ArticleType model 增加 suggest 字段

    # ArticleSpider/models/es_types.py
    
    from datetime import datetime
    from elasticsearch_dsl import DocType, Date, Nested, Boolean, \
        analyzer, Completion, Keyword, Text, Integer
    from elasticsearch_dsl.connections import connections
    from elasticsearch_dsl.analysis import CustomAnalyzer as _CustomAnalyzer
    
    
    # 指明连接的服务器
    connections.create_connection(hosts=['localhost'])
    
    
    class CustomAnalyzer(_CustomAnalyzer):
        """
            自定义 CustomAnalysis,解决报错问题
        """
        def get_analysis_definition(self):
            return {}
    
    
    # filter=['lowercase'] 参数做大小写转换
    ik_analyzer = CustomAnalyzer('ik_max_word', filter=['lowercase'])
    
    
    class ArticleType(DocType):
        """
            伯乐在线文章类型
        """
        # 新增 suggest 字段,为 Completion 类型
        # 理论上是可以向下面这样写的,在 Completion 内部指明 analyzer
        # 但是由于 elasticsearch_dsl 的源码有点问题,直接这样写的话
        # 运行程序,init 的时候会报错
        # suggest = Completion(analyzer='ik_max_word')
        # 现在解决方案是自定义一个 CustomAnalyzer
        suggest = Completion(analyzer=ik_analyzer)
        title = Text(analyzer='ik_max_word')  # 需要进行分词,所以定义成 text
        create_date = Date()
        url = Keyword()  # 无需分词
        url_object_id = Keyword()
        front_img_url = Keyword()
        front_img_path = Keyword()
        praise_nums = Integer()
        comment_nums = Integer()
        fav_nums = Integer()
        tags = Text(analyzer='ik_max_word')
        content = Text(analyzer='ik_max_word')
    
        class Meta:
            index = 'jobbole'
            doc_type = 'article'
    
    
    if __name__ == '__main__':
        ArticleType.init()  # 可以直接生成 mapping
    
    

    运行文件

    image.png

    查看 mapping 结果

    image.png

    已经根据 elasticsearch 的文档生成了 mapping,那么爬虫每爬取一条数据,保存到 elasticsearch 的时候,要如何生成 suggest 字段这个值呢?

    image.png image.png

    那么大概知道怎么一回事,这个 input 的分词是如何生成的呢?
    事实上,可以通过 GET _analyze 这个接口来完成的

    image.png

    所以在爬虫爬取过来数据,在将数据插入 elasticsearch 的时候,就需要对数据进行处理,后续在做搜索建议的时候才有可能成功

    所以要在之前定义的 item JobBoleArticleLoadItemsave_to_es 方法中来生成 搜索建议值

    # ArticleSpider/items.py
    
    from ArticleSpider.models.es_types import ArticleType
    
    # 获取 es 的连接
    from elasticsearch_dsl.connections import connections
    es = connections.create_connection(ArticleType._doc_type.using)
    
    def gen_suggests(index, info_tuple):
        """
            根据传递进来的字符串和权重生成搜索建议数组
        Args:
            index: 索引名称
            info_tuple: 包含 text(内容)、weight(权重) 的元组
        Returns:
            suggests: 搜索建议数组
        """
        # used_words 设定为 set 目的是为了去重
        # 假如第一次 传过来一个字符串 "python爬虫",属于 "title" 这个字段
        # 第二次传过来同样字符串 "python爬虫",属于 "tags" 这个字段,这两个
        # 字段 suggest 的权重是不同的,第一次 title 已经解析了 "python爬虫"
        # 这个词,权重为 10,tags 权重为 3,那么第二次 tags 解析 "python爬虫"
        # 这个词的时候,如果不去重,就会把之前的权重给改掉
        # 所以这里为了只取第一次分析的结果,后面遇到相同的词不进行修改,以第一次为准
        used_words = set()
        suggests = []
        print('-=-=-=-=', info_tuple)
        for text, weight in info_tuple:
            if text:
                # 传递过来非空字符串才进行处理
                # 调用 es 的 analyze 接口分析字符串,将字符串分词以及大小写转换
                # 返回的 words 就是处理后的数据
                words = es.indices.analyze(index=index, analyzer='ik_max_word', params={'filter': ['lowercase']}, body=text)
                # len(r['token']) > 1 过滤掉经过分词分成的单个字的词
                analyzerd_words = set([r['token'] for r in words['tokens'] if len(r['token']) > 1])
                new_words = analyzerd_words - used_words
            else:
                new_words = set()
    
            if new_words:
                suggests.append({'input': list(new_words), 'weight': weight})
        return suggests
    
    
    class JobBoleArticleLoadItem(scrapy.Item):
        ...
    
        def save_to_es(self):
            """
                将数据存入 Elasticsearch
            """
            # 将 item 转换为 es 数据
            article = ArticleType()
            article.title = self['title']
            article.create_date = self['create_date']
            article.content = remove_tags(self['content'])
            article.front_img_url = self['front_img_url']
            if 'front_img_path' in self:
                article.front_img_path = self['front_img_path']
            article.praise_nums = self['praise_nums']
            article.fav_nums = self['fav_nums']
            article.comment_nums = self['comment_nums']
            article.url = self['url']
            article.tags = self['tags']
            article.meta.id = self['url_object_id']
    
            # article.suggest = [{'input': [], 'weight': 2}]
            article.suggest = gen_suggests(ArticleType._doc_type.index, ((article.title, 10), (article.tags, 7)))
    
            article.save()
    
    

    运行 jobbole spider,断点调试

    image.png

    查看数据

    image.png image.png

    这样,就完成了 搜索建议字段的保存,接下来就可以运行 spider 源源不断的向 es 中写入数据了

    django实现elasticsearch的搜索建议

    • 准备工作
    • 创建虚拟环境 lcv_search
    mkvirtualenv lcv_search
    
    • 进入虚拟环境,安装依赖包
    pip install Django==1.11
    pip install elasticsearch-dsl==5.2.0
    

    创建项目 LcvSearch

    image.png

    打开项目,新建的 search app 已经自动添加进来了

    image.png

    启动项目,可以正常运行

    image.png image.png

    项目根目录下创建 static/ 目录用于存放静态文件,将 html 文件放到 templates/ 目录下

    image.png

    settings.py 配置

    # LcvSearch/settings.py
    
    TEMPLATES = [
        {
            ...
            'DIRS': [os.path.join(BASE_DIR, 'templates')],
            ...
    ]
    ...
    
    STATICFILES_DIRS = [
        os.path.join(BASE_DIR, 'static')
    ]
    
    

    urls.py 配置

    # LcvSearch/urls.py
    
    from django.views.generic import TemplateView
    
    urlpatterns = [
        url(r'^admin/', admin.site.urls),
        url(r'^$', TemplateView.as_view(template_name='index.html'), name='index'),
    ]
    
    

    替换 HTML 中静态文件引用

    启动项目

    image.png image.png
    • 搜索建议的原理是如何实现的?

    打开百度,搜索框搜索关键字就会发现,实际上,当我们输入关键词的时候,前端页面通过 js 已经自动向后端发送了请求,并将搜索结果返回,就实现了搜索建议

    image.png
    • elasticsearch 如何实现模糊搜索

    elasticsearch 为我们提供了 fuzzy 模糊搜索

    image.png image.png image.png image.png
    
    # 模糊搜索
    GET jobbole/_search
    {
      "query": {
        "fuzzy": {"title": "linu"}
      },
      "_source": ["title"]
    }
    
    
    # 模糊搜索
    GET jobbole/_search
    {
      "query": {
        "match": {"title": "linu"}
      },
      "_source": ["title"]
    }
    
    
    
    GET _search
    {
      "query": {
        "fuzzy": {
          "title": {
            "value": "linu",
            "fuzziness": 1,
            "prefix_length": 0
          }
        }
      }
    }
    
    
    
    # 其中 fuzziness 指明编辑距离
    # 编辑距离(Edit Distance),又称Levenshtein距离,是指两个字串之间,由一个转成另一个所需的最少编辑操作次数。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。一般来说,编辑距离越小,两个串的相似度越大。
    
    # prefix_length 指明需要搜索的字符串前面不需要参与变换的词的长度
    
    

    明白了什么是 fuzzy 搜索,下面演示如何通过 suggest 完成自动补全

    image.png
    
    POST jobbole/_search?pretty
    {
        "suggest": {
            "my-suggest" : {
                "text" : "linux",
                "completion" : {
                    "field" : "suggest",
                    "fuzzy" : {
                        "fuzziness" : 2
                    }
                }
            }
        }
    }
    
    

    现在将 ArticleSpider 项目中的 ArticleSpider/models/es_types.py 中的代码拷贝到 LcvSearch 项目中的 search/models.py 文件,这也是为什么之前在 ArticleSpider 项目中写的时候为什么尽量写成和 Django 的 Model 一样的原因,拿过来即可以使用

    # search/models.py
    
    from datetime import datetime
    from elasticsearch_dsl import DocType, Date, Nested, Boolean, \
        analyzer, Completion, Keyword, Text, Integer
    from elasticsearch_dsl.connections import connections
    from elasticsearch_dsl.analysis import CustomAnalyzer as _CustomAnalyzer
    
    
    # 指明连接的服务器
    connections.create_connection(hosts=['localhost'])
    
    
    class CustomAnalyzer(_CustomAnalyzer):
        """
            自定义 CustomAnalysis,解决报错问题
        """
        def get_analysis_definition(self):
            return {}
    
    
    # filter=['lowercase'] 参数做大小写转换
    ik_analyzer = CustomAnalyzer('ik_max_word', filter=['lowercase'])
    
    
    class ArticleType(DocType):
        """
            伯乐在线文章类型
        """
        # 新增 suggest 字段,为 Completion 类型
        # 理论上是可以向下面这样写的,在 Completion 内部指明 analyzer
        # 但是由于 elasticsearch_dsl 的源码有点问题,直接这样写的话
        # 运行程序,init 的时候会报错
        # suggest = Completion(analyzer='ik_max_word')
        # 现在解决方案是自定义一个 CustomAnalyzer
        suggest = Completion(analyzer=ik_analyzer)
        title = Text(analyzer='ik_max_word')  # 需要进行分词,所以定义成 text
        create_date = Date()
        url = Keyword()  # 无需分词
        url_object_id = Keyword()
        front_img_url = Keyword()
        front_img_path = Keyword()
        praise_nums = Integer()
        comment_nums = Integer()
        fav_nums = Integer()
        tags = Text(analyzer='ik_max_word')
        content = Text(analyzer='ik_max_word')
    
        class Meta:
            index = 'jobbole'
            doc_type = 'article'
    
    
    if __name__ == '__main__':
        ArticleType.init()  # 可以直接生成 mapping
    
    

    编写 view

    # search/views.py
    
    import json
    from django.shortcuts import render
    from django.http import HttpResponse
    from django.views.generic.base import View
    from .models import ArticleType
    
    
    # Create your views here.
    class SearchSuggest(View):
    
        def get(self, request):
            key_words = request.GET.get('s', '')
            re_datas = []
            if key_words:
                s = ArticleType.search()
                s = s.suggest('my-suggest', key_words, completion={
                    "field": "suggest", "fuzzy": {
                        "fuzziness": 2
                    },
                    "size": 10
                })
                suggestions = s.execute_suggest()
                for match in getattr(suggestions, 'my-suggest')[0].options:
                    source = match._source
                    re_datas.append(source['title'])
            return HttpResponse(json.dumps(re_datas), content_type='application/json')
    
    

    配置 urls.py

    # LcvSearch/urls.py
    
    from search.views import SearchSuggest
    
    urlpatterns = [
        ...
        # 处理搜索建议
        url(r'^suggest/$', SearchSuggest.as_view(), name='suggest'),
    ]
    
    

    启动项目

    image.png

    测试搜索建议功能已经实现了

    image.png

    django实现elasticsearch的搜索功能

    编写 view

    # search/views.py
    
    import json
    from datetime import datetime
    from django.shortcuts import render
    from django.http import HttpResponse
    from django.views.generic.base import View
    from .models import ArticleType
    from elasticsearch import Elasticsearch
    
    # 初始化一个 Elasticsearch 的连接
    cline = Elasticsearch(hosts=['127.0.0.1'])
    
    ...
    class SearchView(View):
    
        def get(self, request):
            key_words = request.GET.get('q', '')
            page = request.GET.get('p', '1')
            try:
                page = int(page)
            except:
                page = 1
    
            start_time = datetime.now()
            response = cline.search(
                index="jobbole",
                body={
                    "query": {
                        "multi_match": {
                            "query": key_words,
                            "fields": ["tags", "title", "content"]
                        }
                    },
                    "from": (page-1)*10,
                    "size": 10,
                    "highlight": {
                        "pre_tags": ['<span class="keyWord">'],
                        "post_tags": ['</span>'],
                        "fields": {
                            "title": {},
                            "content": {},
                        }
                    }
                }
            )
            end_time = datetime.now()
            last_seconds = (end_time - start_time).total_seconds()
    
            # 获取总数量
            total_nums = response['hits']['total']
            if page % 10 > 0:
                page_nums = int(total_nums / 10) + 1
            else:
                page_nums = int(total_nums / 10)
            hit_list = []
            for hit in response['hits']['hits']:
                hit_dict = {}
                if 'title' in hit['highlight']:
                    hit_dict['title'] = ''.join(hit['highlight']['title'])
                else:
                    hit_dict['title'] = hit['_source']['title']
                if 'content' in hit['highlight']:
                    hit_dict['content'] = ''.join(hit['highlight']['content'])[:500]
                else:
                    hit_dict['content'] = hit['_source']['content'][:500]
                hit_dict['create_date'] = hit['_source']['create_date']
                hit_dict['url'] = hit['_source']['url']
                hit_dict['score'] = hit['_score']
    
                hit_list.append(hit_dict)
    
            return render(request, 'result.html', {
                'all_hits': hit_list,
                'key_words': key_words,
                'total_nums': total_nums,
                'page': page,
                'page_nums': page_nums,
                'last_seconds': last_seconds,
            })
    
    

    配置 urls.py

    # LcvSearch/urls.py
    
    from search.views import SearchSuggest, SearchView
    
    urlpatterns = [
        ...
        url(r'^search/$', SearchView.as_view(), name='search'),
    ]
    
    

    将前端 HTML 页面中的变量替换为 view 视图中传递过来的变量

    启动项目

    image.png image.png

    相关文章

      网友评论

        本文标题:聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎

        本文链接:https://www.haomeiwen.com/subject/nhrsuftx.html