【Django 天天生鲜项目04】搜索(搜索引擎、分词包的使用)、购物车

关键:全文检索框架搜索引擎分词包、Ajax请求的使用...

仅作为个人笔记!

目录

1.搜索

1.1. haystack框架和whoosh引擎安装

1.2.索引文件的生成

1.3.全文检索的使用

1.4.更改分词方式 

2.购物车

2.3.购物车前端Ajax请求


 

1.搜索

在首页、详情页和列表页中,都有搜索框,如何实现搜索呢?

关键:全文检索框架搜索引擎、分词包的使用、

 

全文检索不同于特定字段的模糊查询,使用全文检索的效率更高,并且能够对于中文进行分词处理。

 

搜索引擎:可以对表中的某些字段进行关键词分析,建立关键词对应的索引数据;

全文检索框架:帮助用户使用搜索引擎;

本处使用 haystack框架 和 whoosh引擎 。

【Django 天天生鲜项目04】搜索(搜索引擎、分词包的使用)、购物车_第1张图片

 

1.1. haystack框架和whoosh引擎安装

  • haystack:全文检索的框架,支持whoosh、solr、Xapian、Elasticsearc四种全文检索引擎,点击查看官方网站。
  • whoosh:纯Python编写的全文搜索引擎,虽然性能比不上sphinx、xapian、Elasticsearc等,但是无二进制包,程序不会莫名其妙的崩溃,对于小型的站点,whoosh已经足够使用,点击查看whoosh文档。
  • jieba:一款免费的中文分词包,如果觉得不好用可以使用一些收费产品。

 

(1)在虚拟环境中依次安装需要的包

pip install django-haystack
pip install whoosh
pip install jieba

(2)在settings.py文件中注册应用haystack,并做配置

# 注册
INSTALLED_APPS = (
    ...
    'haystack',
)

...
# 全文检索框架配置
HAYSTACK_CONNECTIONS = {
    'default': {
        # 使用whoosh引擎(配置路径)
        'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
        # 'ENGINE': 'haystack.backends.whoosh_cn_backend.WhooshEngine',  # 配置好jieba中文分词包后用这个
        # 设置索引文件生成的路径
        'PATH': os.path.join(BASE_DIR, 'whoosh_index'),
    }
}

# 当添加、修改、删除数据时,自动生成索引
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
# 指定搜索结果每页显示的条数
HAYSTACK_SEARCH_RESULTS_PER_PAGE = 2

 

1.2.索引文件的生成

要搜索商品表中的数据,就需要搜索引擎根据表的某些字段来建立关键词对应的索引数据。然后才能让搜索引擎来搜索对应的数据。

根据模型类(如:GoodsSKU)的表的数据生成索引的数据。

  • 根据模型类新建索引类

    在goods应用目录下新建一个search_indexes.py文件(文件名固定),在其中定义一个商品索引类。
    from haystack import indexes
    # 导入你的模型类
    from goods.models import GoodsSKU
    
    
    # 指定对于某个类的某些数据建立索引
    # 建议的索引类名格式:模型类名+Index
    class GoodsSKUIndex(indexes.SearchIndex, indexes.Indexable):
        # 索引字段 use_template=True:指定根据表中的哪些字段建立索引文件,这个'指定说明'放在一个文件中
        text = indexes.CharField(document=True, use_template=True)
    
        def get_model(self):
            # 返回你的模型类
            return GoodsSKU
    
        # 建立索引的数据
        def index_queryset(self, using=None):
            return self.get_model().objects.all()

     

  • 在文件中,指定根据表中的哪些字段建立索引文件 

    templates下面新建目录 search/indexes/模型类所在的应用名 (如:search/indexes/goods),在此目录下面新建一个文件 模型类名小写_text.txt(如:goodssku_text.txt)并编辑内容如下:
    # 指定根据表中的哪些字段建立索引数据
    {{ object.name }} # 根据商品的名称建立索引
    {{ object.desc }} # 根据商品的简介建立索引
    {{ object.goods.detail }} # 根据商品的详情建立索引

     

  • 使用命令生成索引文件。

    在虚拟环境中,cd到项目的路径,执行
    python manage.py rebuild_index

    就会按照配置,生成目录whoosh_index,并在目录下生成索引数据。

 

1.3.全文检索的使用

(1)搜索框的前端,点击进行提交时,会通过 haystack 搜索数据。

            

(2)action提交后的处理应该让搜索引擎来完成,会通过 haystack 搜索数据,故在项目的url中配置。注意提交的地址应该和配置的url一致。

urlpatterns = [
   ... 
    path('search', include('haystack.urls')),  # 全文检索框架
]

(3)全文检索结果。

搜索出结果后,haystack会把搜索出的结果传递给templates/search目录下的search.html(没有这个文件可以先自己建),传递的上下文包括:

  • query:搜索关键字
  • page:当前页的page对象 –>遍历page对象,获取到的是SearchResult类的实例对象,对象的属性object才是模型类的对象。
  • paginator:分页paginator对象

通过HAYSTACK_SEARCH_RESULTS_PER_PAGE 可以控制每页显示数量。

以下可以快速的查看搜索结果:

......

搜索的关键字:{{ query }}
当前页的Page对象:{{ page }}
    {% for item in page %}
  • {{ item.object }}
  • {% endfor %}
分页paginator对象:{{ paginator }}
... 浏览器显示结果如下: 搜索的关键字:草莓 当前页的Page对象: 草莓 500g 分页paginator对象:

项目中templates/search目录下的search.html关键代码:

    
    {% for item in page %}
  • {{ item.object.name }}

    ¥{{ item.object.price }} {{ item.object.price}}/{{ item.object.unite }}
  • {% endfor %}
{% if page.has_previous %} <上一页 {% endif %} {% for pindex in paginater.page_range %} {% if pindex == page.number %} {{ pindex }} {% else %} {{ pindex }} {% endif %} {% endfor %} {% if page.has_next %} 下一页> {% endif %}

 

1.4.更改分词方式 

默认的引擎在对中文进行关键词分析的时候,可能支持的不是很好。可以使用jieba这个分词包,对中文的处理更好。

(1)安装jieba分词模块:

pip install jieba

(2)更改whoosh引擎的默认词语分析类

  • 找到虚拟环境下的haystack目录,比如:

    /home/python/.virtualenvs/dailyfresh/lib/python3.5/site-packages/haystack/backends 
     
  • 在haystack目录中创建ChineseAnalyzer.py文件
     
    import jieba
    from whoosh.analysis import Tokenizer, Token
    
    class ChineseTokenizer(Tokenizer):
        def __call__(self, value, positions=False, chars=False,
                     keeporiginal=False, removestops=True,
                     start_pos=0, start_char=0, mode='', **kwargs):
            t = Token(positions, chars, removestops=removestops, mode=mode, **kwargs)
            seglist = jieba.cut(value, cut_all=True)  # 关键是这句
            for w in seglist:
                t.original = t.text = w
                t.boost = 1.0
                if positions:
                    t.pos = start_pos + value.find(w)
                if chars:
                    t.startchar = start_char + value.find(w)
                    t.endchar = start_char + value.find(w) + len(w)
                yield t
    
    def ChineseAnalyzer():
        return ChineseTokenizer()
    

     

  • 在haystack目录,复制whoosh_backend.py文件,改为如下名称,以便用来更改词语分析类:

    whoosh_cn_backend.py
     
  • 打开复制出来的新文件,引入中文分析类,内部采用jieba分词、更改词语分析类
    ...
    from .ChineseAnalyzer import ChineseAnalyzer
    ...
    
    # 查找词语分析类:analyzer=StemmingAnalyzer()
    # 并改为:       analyzer=ChineseAnalyzer()
    # 如下:
    ...
        schema_fields[field_class.index_fieldname] = TEXT(stored=True, analyzer=ChineseAnalyzer(), field_boost=field_class.b    oost, sortable=True)
    ...
  • 修改settings.py文件中的配置项
    # 全文检索框架配置
    HAYSTACK_CONNECTIONS = {
        'default': {
            # 使用whoosh引擎(配置路径)
            # 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
            'ENGINE': 'haystack.backends.whoosh_cn_backend.WhooshEngine',  # 配置好jieba中文分词包后用这个
            # 设置索引文件生成的路径
            'PATH': os.path.join(BASE_DIR, 'whoosh_index'),
        }
    }
    
    # 当添加、修改、删除数据时,自动生成索引
    HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'

     

  • 重新生成索引文件:在项目所在目录使用命令
    python manage.py rebuild_index

     

这样,使用新的分词类后,即使是商品详情中包含的词语,也能搜索到结果。

 

2.购物车

要完成购物车功能,要确定(尤其是前后端分开开发时):

  • 前端是否传递数据,传递什么数据,什么格式
  • 前端访问的方式(get  post
  • 返回给前端的什么数据,什么格式

 

2.1.商品详情页js代码

在商品的详情页,包含购买商品的数量、加入购物车等功能。

【Django 天天生鲜项目04】搜索(搜索引擎、分词包的使用)、购物车_第2张图片

在js中,绑定相关按钮等的点击事件进行处理。比如增减商品数、计算总价格等

        // 更新总价        
        update_goods_amount()
        // 计算商品的总价
        function update_goods_amount() {
            // 获取商品的单价和数量
            price = $('.show_pirze').children('em').text()
            count = $('.num_show').val()
            // 计算商品的总价
            price = parseFloat(price)
            count = parseInt(count)
            amount = price*count
            // 设置商品的总价,设置两位小数
            $('.total').children('em').text(amount.toFixed(2)+'元')
        }

        //增商品的数量(减同理
        $('.add').click(function () {
            // 获取商品数目并+1
            count = $('.num_show').val()
            count = parseInt(count)+parseInt('1')
            // 重新设置商品的数目
            $('.num_show').val(count)
            update_goods_amount()
        })
        ...
        // 手动输入商品的数量
        $('.num_show').blur(function () {
            count = $(this).val()
            // 校验count是否合法(能否转为数字、去除空格...)
            if (isNaN(count) || count.trim().length==0 || parseInt(count) <=0){
                count = 1
            }
            $(this).val(parseInt(count))
            update_goods_amount()
        })

 

注:点击事件中,若加1的操作结果并不对,检查代码也没问题。使用alert输出后发现被多次执行了。原来是出现了累加绑定的问题。解决办法可以参考这个博文试试:https://blog.csdn.net/GSCurry/article/details/71857127。

  • 使用前先解除绑定:$("#id").unbind("click")
  • 使用jQuery的one()方法。该方法为元素绑定一个一次性的事件处理函数,这个事件处理函数只会被执行一次。
  • 配合off()方法解除绑定。该方法为元素绑定一个的事件处理函数,再次给改元素添加相同事件时不会累加绑定。

 

2.2.购物车添加后台视图

购物车设计

是否传递数据、什么格式什么数据?

访问方式get? post?

返回给前端什么格式什么数据?

redis存储购物车记录:

  • 用户点击加入购物车时需要添加购物车记录(添加);
  • 使用购物车中数据和访问购物车页面时需要获取购物车记录(获取);
  • 存储购物车记录的格式:一个用户的购物车记录用一条数据保存,用hash类型(属性:值,'cart_用户id':{'sku_id1':商品数目, 'sku_id2':商品数目, ...})记录skuid和数量;

在前端点击加入购物车之后,通常页面的整体是不进行刷新操作的。所以采用的方式如下

  • 采用ajax 的 post请求
  • 前端需要传递的参数:商品id(sku_id)、 商品数量(count)

附:传参的几种方式

  • get传参:cart/add?sku_id=1?count=3 ;值涉及获取,可采用get;
  • post传参:参数会放在一个字典 {'sku_id':1, 'count':3 } ;若涉及到数据的修改(增删改),采用post ;
  • url传参:url配置时捕获参数;

 

cart/view.py中相关视图

...
# /cart/add
class CartAddView(View):
    """购物车记录添加"""
    def post(self, request):
        user = request.user
        if not user.is_authenticated:
            return JsonResponse({'res': 0, 'errmsg': '请先登录'})

        # 接收数据
        sku_id = request.POST.get('sku_id')
        count = request.POST.get('count')

        if not all([sku_id, count]):
            return JsonResponse({'res': 1, 'errmsg': '数据不完整'})

        # 校验添加的商品数量
        try:
            count = int(count)
        except Exception as e:
            return JsonResponse({'res': 2, 'errmsg': '商品数目出错'})

        try:
            sku = GoodsSKU.objects.get(id=sku_id)
        except GoodsSKU.DoesNotExist:
            return JsonResponse({'res': 3, 'errmsg': '商品不存在'})

        # 业务处理:添加购物车记录(已经有的累加、没有的添加)
        conn = get_redis_connection('default')
        cart_key = 'cart_%d' % user.id

        # 先尝试获取sku_id的值 : hget cart_key 属性
        # 如果sku_id在hash中不存在,hget会返回None
        cart_count = conn.hget(cart_key, sku_id)
        if cart_count:
            # 累加购物车中商品的数目
            count += int(cart_count)

        # 校验商品的库存
        if count > sku.stock:
            return JsonResponse({'res': 4, 'errmsg': '商品库存不足'})

        # 设置hash中sku_id对应的值
        # hset:如果sku_id已经存在,更新数据, 如果sku_id不存在,添加数据
        conn.hset(cart_key, sku_id, count)
        # 计算用户购物车商品的条目数
        total_count = conn.hlen(cart_key)

        # 返回应答
        return JsonResponse({'res':5, 'total_count':total_count, 'message':'添加成功'})

 

 

2.3.购物车前端Ajax请求

购物车记录的添加在Ajax中提交,在刚刚的视图中进行处理后返回。

注意,如果是表单post提交,csrf验证比较好处理,加上{% csrf_token %}即可Ajax提交怎么处理csrf验证呢?

(1)在前端同加上{% csrf_token %}

(2)访问页面,然后查看源代码,发现对应的隐藏域,类似这样

(3)在Ajax的js中,获取它的值,并加入到要传递的参数中

csrf = $('input[name="csrfmiddlewaretoken"]').val()
params = {'sku_id':sku_id, 'count':count, 'csrfmiddlewaretoken':csrf}

这样,就可以通过验证。

        // 获取add_cart div元素左上角的坐标
        var $add_x = $('#add_cart').offset().top;
        var $add_y = $('#add_cart').offset().left;
        // 获取show_count div元素左上角的坐标
        var $to_x = $('#show_count').offset().top;
        var $to_y = $('#show_count').offset().left;


        $('#add_cart').click(function(){
            // 获取商品的id和数量
            sku_id = $(this).attr('sku_id') // 获取自定义属性用attr
            count = $('.num_show').val()
            // 获取csrf隐藏域
            csrf = $('input[name="csrfmiddlewaretoken"]').val()
            params = {'sku_id':sku_id, 'count':count, 'csrfmiddlewaretoken':csrf}
            
            // 发起ajax post请求,访问/cart/add, 传递参数
            $.post('/cart/add', params, function (data) {
                if (data.res == 5){
                    // 添加成功,并显示动画
                    $(".add_jump").css({'left':$add_y+80,'top':$add_x+10,'display':'block'})
                    $(".add_jump").stop().animate({
                        'left': $to_y+7,
                        'top': $to_x+7},
                        "fast", function() {
                            $(".add_jump").fadeOut('fast',function(){
                                // 设置用户购物车中商品的条目数
                                $('#show_count').html(data.total_count);
                            });
                    });
                }
                else {
                    // 添加失败
                    alert(data.errmsg)
                }
            })
        })

2.4.购物车页面显示

用一个独立的页面来显示购物车。

【Django 天天生鲜项目04】搜索(搜索引擎、分词包的使用)、购物车_第3张图片

相关基本视图如下:

# /cart/
class CartInfoView(LoginRequiredMixin, View):
    """购物车页面显示"""
    def get(self, request):
        """显示"""
        # 获取登录的用户
        user = request.user
        # 获取用户购物车中商品的信息(保存在redis)
        conn = get_redis_connection('default')
        cart_key = 'cart_%d' % user.id
        # 记录的格式:{'商品id':商品数量, ...}
        cart_dict = conn.hgetall(cart_key)

        skus = []
        # 保存用户购物车中商品的总数目和总价格
        total_count = 0
        total_price = 0
        # 遍历获取商品的信息
        for sku_id, count in cart_dict.items():
            # 根据商品的id获取商品的信息
            sku = GoodsSKU.objects.get(id=sku_id)
            # 计算商品的小计
            amount = sku.price * int(count)
            # 动态给sku对象增加一个属性amount, 保存商品的小计
            sku.amount = amount
            # 动态给sku对象增加一个属性count, 保存购物车中对应商品的数量
            sku.count = int(count)  # 注意转为int
            # 添加
            skus.append(sku)

            # 累加计算商品的总数目和总价格
            total_count += int(count)
            total_price += amount

        # 组织上下文
        context = {'total_count': total_count,
                   'total_price': total_price,
                   'skus': skus}
        # 使用模板
        return render(request, 'cart.html', context)

 

购物车商品全选

使用js实现购物车页面显示的全选、选部分商品时相应的处理。在cart.html中添加js,改变相应的checkbox的事件。

JQuery选择器参考:https://www.w3school.com.cn/jquery/jquery_ref_selectors.asp、:checkbox、:checked、


    

 

购物车记录更新

购物车页面中,可以对数量进行增减等操作。采用Ajax post请求提交给后台,传递的参数:商品的id(sku_id)。在后台的视图中进行处理。

相关视图跟基本视图差不多(校验登录、接收数据、检验数据、业务处理、返回json...),注意Ajax返回值。前端购物车商品数量的增加的Ajax如下,减少的类似: 

(注意:默认发起的ajax请求都是异步的,不会等回调函数执行,需要时要先设置ajax请求为同步,再设置回异步)

    // 计算商品的小计
    function update_goods_amount(sku_ul) {
        count = sku_ul.find('.num_show').val()
        price = sku_ul.children('.col05').text()
        amount = parseInt(count)*parseFloat(price)
        // 设置商品的小计
        sku_ul.children('.col07').text(amount.toFixed(2)+'元')
    }
...

    // 更新购物车中商品的数量
    error_update = false
    total = 0
    function update_remote_cart_info(sku_id, count){
        csrf = $('input[name="csrfmiddlewaretoken"]').val()
        //组织参数
        params = {'sku_id': sku_id, 'count': count, 'csrfmiddlewaretoken': csrf}

        // 设置ajax请求为同步
        $.ajaxSettings.async = false
        // 发起Ajax post请求,访问/cart/update, 传递参数:sku_id count
        // 注意:默认发起的ajax请求都是异步的,不会等回调函数执行,需要时要先设置ajax请求为同步,再设置回异步
        $.post('/cart/update', params, function (data) {
            if(data.res == 5){
                // 更新成功
                error_update = false
                total = data.total_count
            }
            else{
                error_update = true
                alert(data.errmsg)
            }
        })
        // 回调函数执行结束,设置ajax请求为异步
        $.ajaxSettings.async = true
    }

    // 购物车商品数量的增加
    $('.add').click(function () {
        // 获取商品的id和商品的数量
        sku_id = $(this).next().attr('sku_id')
        count = $(this).next().val()
        count = parseInt(count)+1
        //  更新购物车记录
        update_remote_cart_info(sku_id, count)
        // 判断更新是否成功
        if(error_update ==  false){
            // 重新设置商品的数目
            $(this).next().val(count)
            // 计算商品的小计
            update_goods_amount($(this).parents('ul'))
            // 获取商品对应的checkbox的选中状态,如果被选中,更新页面信息
            is_checked = $(this).parents('ul').find(':checkbox').prop('checked')
            if (is_checked){
                // 更新页面信息
                update_page_info()
            }
            // 更新页面上购物车商品的总件数
            $('.total_count').children('em').text(total)
        }
    })

    // 购物车商品数量的减少
    ......

    // 记录用户输入之前商品的数量
    pre_count = 0
    $('.num_show').focus(function () {
        pre_count = $(this).val()
    })

    // 手动输入购物车中的商品数量
    $('.num_show').blur(function () {
        sku_id = $(this).attr('sku_id')
        count = $(this).val()
        if (isNaN(count) || count.trim().length==0 || parseInt(count)<=0){
            // 设置商品的数目为用户输入之前的数目
            $(this).val(pre_count)
            return
        }
        update_remote_cart_info(sku_id, count)
        if(error_update ==  false){
            $(this).val(count)
            update_goods_amount($(this).parents('ul'))
            // 获取商品对应的checkbox的选中状态,如果被选中,更新页面信息
            is_checked = $(this).parents('ul').find(':checkbox').prop('checked')
            if (is_checked){
                update_page_info()
            }
            $('.total_count').children('em').text(total)
        }
        else {
            // 设置商品的数目为用户输入之前的数目
            $(this).val(pre_count)
        }
    })

 

购物车记录删除 

点击删除将购物车商品删除。

同样采用ajax post请求、 前端需要传递的参数:商品的id(sku_id)。

view.py视图关键code:

        ...
        # 业务处理:删除购物车记录
        conn = get_redis_connection('default')
        cart_key = 'cart_%d' % user.id

        # 删除 hdel
        conn.hdel(cart_key, sku_id)
        。。。

cart.html提交的相关Ajax请求,关键是回调成功后,执行.remove():

// 删除购物车中的记录
    $('.cart_list_td').children('.col08').children('a').click(function () {
        // 获取对应商品的id
        sku_id = $(this).parents('ul').find('.num_show').attr('sku_id')
        csrf = $('input[name="csrfmiddlewaretoken"]').val()
        params = {'sku_id':sku_id, 'csrfmiddlewaretoken':csrf}
        // 获取商品所在的ul元素
        sku_ul = $(this).parents('ul')
        // 发起ajax post请求, 访问/cart/delete, 传递参数:sku_id
        $.post('/cart/delete', params, function (data) {
            if (data.res == 3){
                // 删除成功,移除页面上商品所在的ul元素
                sku_ul.remove()
                // 获取sku_ul中商品的选中状态
                is_checked = sku_ul.find(':checkbox').prop('checked')
                if (is_checked){
                    update_page_info()
                }
                // 重新设置页面上购物车中商品的总件数
                $('.total_count').children('em').text(data.total_count)
            }
            else{
                alert(data.errmsg)
            }
        })
    })

 

01 框架、数据表设计、项目框架笔记

02 注册、登录、用户中心 (itsdangerous模块加密、celery异步、 Django 的验证系统、redis作为缓存等)

03 FastDFS文件存储-首页-详情页-列表页

04 搜索(搜索引擎、分词包的使用)、购物车

05 订单(Mysql事务、并发处理、支付宝支付、评论)

06 项目部署(uwsgi服务器、Nginx服务器)

仅作为个人笔记 !

-----end-----

 

 

你可能感兴趣的:(Django实战)