美文网首页爬虫
聚焦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