卑微程序员用Django 3.0.3 集成whoosh站内搜索竟遇到这种事 ImportError: cannot import name 'six' from 'django.utils'

黄炜强 后端 2020-07-08

事出缘由

    今天下午,想尝试给我的django3.0.3版本的应用集成站内搜索引擎(因为之前是用模糊查询——ORM的filter实现的),所以我选择了Django Haystack + Whoosh搜索引擎 + jieba中文分词来实现站内搜索可以根据用户的搜索关键词对搜索结果进行排序以及高亮关键字。

“经始大业”

    1)安装依赖

pip install django_haystack
pip install whoosh
pip install jieba

    2) 配置settings.py

# django项目应用的settings.py

INSTALLEND_APPS = [
    # ....
    # 加入haystack
    'haystack'
]

# 配置haystack
HAYSTACK_CONNECTIONS = {
  'default': {
    # 设置搜索引擎, 文件是index app中whoosh_backend.py
    'ENGINE': 'index.whoosh_backend.WhooshEngine',
    # 索引存放路径为当前项目路径下的whoosh_index文件夹
    'PATH': os.path.join(BASE_DIR, 'whoosh_index'),
    'INCLUDE_SPELLING': True
  }
}

# 当数据库更新时,会自动更新索引, 这就很方便了
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'

    3)重新定义WhooshEngine和重写WhooshSearchBackend的类方法

from haystack.backends.whoosh_backend import *
from jieba.analyse import ChineseAnalyzer


class MyWhooshSearchBackend(WhooshSearchBackend):
    def build_schema(self, fields):
        schema_fields = {
            ID: WHOOSH_ID(stored=True, unique=True),
            DJANGO_CT: WHOOSH_ID(stored=True),
            DJANGO_ID: WHOOSH_ID(stored=True),
        }
        initial_key_count = len(schema_fields)
        content_field_name = ''
        for field_name, field_class in fields.items():
            if field_class.is_multivalued:
                if field_class.indexed is False:
                    schema_fields[field_class.index_fieldname] = IDLIST(stored=True, field_boost=field_class.boost)
                else:
                    schema_fields[field_class.index_fieldname] = KEYWORD(stored=True, commas=True, scorable=True, field_boost=field_class.boost)
            elif field_class.field_type in ['date', 'datetime']:
                schema_fields[field_class.index_fieldname] = DATETIME(stored=field_class.stored, sortable=True)
            elif field_class.field_type == 'integer':
                schema_fields[field_class.index_fieldname] = NUMERIC(stored=field_class.stored, numtype=int, field_boost=field_class.boost)
            elif field_class.field_type == 'float':
                schema_fields[field_class.index_fieldname] = NUMERIC(stored=field_class.stored, numtype=float, field_boost=field_class.boost)
            elif field_class.field_type == 'boolean':
                # Field boost isn't supported on BOOLEAN as of 1.8.2.                schema_fields[field_class.index_fieldname] = BOOLEAN(stored=field_class.stored)
            elif field_class.field_type == 'ngram':
                schema_fields[field_class.index_fieldname] = NGRAM(minsize=3, maxsize=15, stored=field_class.stored, field_boost=field_class.boost)
            elif field_class.field_type == 'edge_ngram':
                schema_fields[field_class.index_fieldname] = NGRAMWORDS(minsize=2, maxsize=15, at='start', stored=field_class.stored, field_boost=field_class.boost)
            else:
                schema_fields[field_class.index_fieldname] = TEXT(stored=True, analyzer=ChineseAnalyzer(),field_boost=field_class.boost, sortable=True)
            if field_class.document is True:
                content_field_name = field_class.index_fieldname
                schema_fields[field_class.index_fieldname].spelling = True        if len(schema_fields) <= initial_key_count:
            raise SearchBackendError("No fields were found inS any search_indexes. Please"                                     " correct this before attempting to search.")
        return (content_field_name, Schema(**schema_fields))


# 重新定义搜索引擎
class WhooshEngine(BaseEngine):
    # 将搜索引擎指向自定义的MyWhooshSearchBackend
    backend = MyWhooshSearchBackend
    query = WhooshSearchQuery

    4) 定义模型

# models.py
from django.db import models
# 创建产品信息表
class Product(models.Model):
    id = models.AutoField('序号', primary_key=True)
    name = models.CharField('名称', max_length=50)
    weight = models.CharField('重量', max_length=20)
    describe = models.CharField('描述', max_length=500)

    def __str__(self):
        return self.name

    别忘了数据迁移 python manager.py makemigrations && migrate

    5) 定义索引类

# index 的 search_indexes.py 索引类的文件名必须为search_index.py
from haystack import indexes
from .models import Product


# 类名必须是为模型名+Index
class ProductIndex(indexes.SearchIndex, indexes.Indexable):
    # document = True 代表 使用此字段的内容作为索引, use_template=True 代表搜索引擎使用索引模板进行搜索
    text = indexes.CharField(document=True, use_template=True)

      # 设置模型
    def get_model(self):
        return Product

    # 设置查询范围
    def index_queryset(self, using=None):
        return self.get_model().objects.all()

    6) 定义索引模板

    索引模板的路径是固定不变的,路径格式是 templates/search/indexes/项目应用路径/模型名称(小写)_text.txt,我们创建product的索引模板

# templates/search/indexes/index/product_text.txt
{{ object.name }}
{{ object.describe }}

    然后使用命令 python manager.py rebuild_index 创建索引就像这样在whoosh_index文件夹:

卑微程序员用Django 3.0.3 集成whoosh站内搜索竟遇到这种事 ImportError: cannot import name 'six' from 'django.utils'

然后就已经配置的差不多了,然后已经迫不及待的想要试一试了

    7)定义路由

此处省略,

    8)定义视图

# 视图函数views.py
from django.shortcuts import render
from django.core.paginator import *
from .models import *
from haystack.views import SearchView

class MySearchView(SearchView):
 # 模板文件template = 'search.html'# 重写响应方式# 如果请求参数q为空, 就返回模型Product的全部数据def create_response(self):
    if not self.request.GET.get('q', ''):
        show_all = True        
        product = Product2.objects.all().order_by('id')
        p = Paginator(product, 15, 7)   # 分页函数 这里是指每页显示15条数据,最后一页如果少于等于7条数据则加到上一页显示
        try: 
            num = int(self.request.GET.get('page', 1))
            page = p.page(num)
        except PageNotAnInteger:
            page = p.page(1)
        except EmptyPage:
            page = p.page(p.num_pages)
        return render(self.request, self.template, locals())
    else:
        show_all = False        qs = super(MySearchView, self).create_response()
        return qs

    9)定义模板文件

search.html

<!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <title>搜索引擎</title>    {# 导入CSS样式文件 #}    {% load static %}
    <link rel="stylesheet"  target="_blank" href="https://url.apipost.cn/url?%7B%25+static+"  rel="external nofollow" css/common.css" %}">    <link rel="stylesheet"  target="_blank" href="https://url.apipost.cn/url?%7B%25+static+"  rel="external nofollow" css/search.css" %}"></head><body><div class="header">    <div class="search-box">        <form action="" method="get">            <div class="search-keyword">                {# 搜索文本框必须命名为q #}                <input name="q" type="text" class="keyword">            </div>            <input type="submit" class="search-button" value="搜 索">        </form>    </div></div><div class="wrapper clearfix"><div class="listinfo">    <ul class="listheader">        <li class="name">产品名称</li>        <li class="weight">重量</li>        <li class="describe">描述</li>    </ul>    <ul class="ullsit">        {# 列出当前分页所对应的数据内容 #}        {% if show_all %}
            {% for item in page.object_list %}
            <li>                <div class="item">                    <div class="nameinfo">{{ item.name }}</div>                    <div class="weightinfo">{{item.weight}}</div>                    <div class="describeinfo">{{ item.describe }}</div>                </div>            </li>            {% endfor %}
        {% else %}
            {# 导入自带高亮功能, 将关键字进行高亮 #}            {% load highlight %}
            {% for item in page.object_list %}
            <li>                <div class="item">                    <div class="nameinfo">{% highlight item.object.name with query %}</div>                    <div class="weightinfo">{{item.object.weight}}</div>                    <div class="describeinfo">{% highlight item.object.describe with query %}</div>                </div>            </li>            {% endfor %}
        {% endif %}
    </ul>    {# 分页导航 #}    <div class="page-box">    <div class="pagebar" id="pageBar">    {# 上一页的路由地址 #}    {% if page.has_previous %}
        {% if query %}
            <a  target="_blank" href="https://url.apipost.cn/url?%7B%25+url+%27index%3Ahaystack%27%25%7D%3Fq%3D%7B%7B+query+%7D%7D%26amp%3Bpage%3D%7B%7B+page.previous_page_number+%7D%7D"  rel="external nofollow"  class="prev">上一页</a>        {% else %}
            <a  target="_blank" href="https://url.apipost.cn/url?%7B%25+url+%27index%3Ahaystack%27%25%7D%3Fpage%3D%7B%7B+page.previous_page_number+%7D%7D"  rel="external nofollow"  class="prev">上一页</a>        {% endif %}
    {% endif %}
    {# 列出所有的路由地址 #}    {% for num in page.paginator.page_range %}
        {% if num == page.number %}
            <span class="sel">{{ page.number }}</span>        {% else %}
            {% if query %}
                <a  target="_blank" href="https://url.apipost.cn/url?%7B%25+url+%27index%3Ahaystack%27+%25%7D%3Fq%3D%7B%7B+query+%7D%7D%26amp%3Bpage%3D%7B%7B+num+%7D%7D"  rel="external nofollow" >{{num}}</a>            {% else %}
                <a  target="_blank" href="https://url.apipost.cn/url?%7B%25+url+%27index%3Ahaystack%27+%25%7D%3Fpage%3D%7B%7B+num+%7D%7D"  rel="external nofollow" >{{num}}</a>            {% endif %}
        {% endif %}
    {% endfor %}
    {# 下一页的路由地址 #}    {% if page.has_next %}
        {% if query %}
            <a  target="_blank" href="https://url.apipost.cn/url?%7B%25+url+%27index%3Ahaystack%27+%25%7D%3Fq%3D%7B%7B+query+%7D%7D%26amp%3Bpage%3D%7B%7B+page.next_page_number+%7D%7D"  rel="external nofollow"  class="next">下一页</a>        {% else %}
            <a  target="_blank" href="https://url.apipost.cn/url?%7B%25+url+%27index%3Ahaystack%27+%25%7D%3Fpage%3D%7B%7B+page.next_page_number+%7D%7D"  rel="external nofollow"  class="next">下一页</a>        {% endif %}
    {% endif %}
    </div>    </div></div></div></body></html>

      10)样式文件

css/common.css

blockquote,body,button,dd,dl,dt,fieldset,form,h1,h2,h3,h4,h5,h6,hr,input,legend,li,ol,p,pre,td,textarea,th,ul{margin:0;padding:0}ol,ul{list-style:none}body{font:12px/1.5 Arial;-webkit-text-size-adjust:none}button,input,select{vertical-align:middle;font-size:100%;outline:0}fieldset,img{border:0 none}em{font-style:normal}i{font-style:normal}a{color:#333;text-decoration:none;outline:0}a:hover{color:#aaa}.clear{clear:both;display:block;height:0;visibility:hidden;font:0/0 arial}.clearfix:after{content:".";display:block;height:0;clear:both;visibility:hidden;font-size:0}button{cursor:pointer}.header{width:1200px;height:80px;padding:0;margin:10px auto 0}.search-box{position:relative;z-index:50;float:left;width:302px;padding:0 78px 0 0;margin:10px 0 0}.search-box .search-keyword{position:relative;padding:6px 10px;height:20px;overflow:hidden;zoom:1;border:2px solid #30c37e;background:#fff}.search-box .keyword{float:left;width:278px;height:20px;border:0 none;outline:0 none;font:14px/20px arial;color:#999}.search-box .search-button{position:absolute;right:0;top:0;width:80px;height:36px;border:0 none;background-color:#30c37e;color:#fff;font:16px/34px 'Microsoft YaHei',arial;overflow:hidden;cursor:pointer;outline:0 none}.page-box{position:relative;zoom:1}.pagebar{padding:0 0 40px;font-size:14px;font-family:arial;line-height:34px;text-align:center}.pagebar span{margin:0 3px 0 2px}.pagebar span.historySel,.pagebar span.sel{font-family:arial}.pagebar .historySel,.pagebar .sel,.pagebar a{display:inline-block;border:1px solid #e6e6e6;padding:0 7px;height:34px;min-width:20px;white-space:nowrap;margin:0 3px 0 2px;vertical-align:top}.pagebar a{color:#333}.pagebar .history,.pagebar .historyStart{color:#ccc}.pagebar .sel,.pagebar a:hover{background:#30c37e;color:#fff;border-color:#30c37e}.pagebar .historySel,.pagebar .historyStart:hover,.pagebar a.history:hover{background:#ccc;color:#fff;border-color:#ccc}.pagebar a:hover{text-decoration:none}.pagebar .prev{padding:0 11px 0 25px;position:relative}.pagebar .next{padding:0 24px 0 12px;position:relative}.pagebar .next i,.pagebar .prev i{position:absolute;width:7px;height:12px;font:0/0 arial;top:11px}.pagebar .prev i{left:12px;background-position:0 -76px}.pagebar .next i{right:12px;background-position:-11px -76px}.pagebar .prev:hover i{left:12px;background-position:-22px -76px}.pagebar .next:hover i{right:12px;background-position:-33px -76px}

css/search.css

.wrapper{width:1200px;padding:0;margin:20px auto 0}.page{padding:18px 0 60px;overflow:hidden;color:#222;font:14px/36px simsun}.page a{display:inline-block;min-width:12px;height:34px;margin:0 5px 0 0;padding:0 11px;border:1px solid #e6e6e6;background-color:#fff;vertical-align:middle;white-space:nowrap;text-align:center;color:#666}.page a,.page span{font-family:"Microsoft YaHei";line-height:34px}.page span{padding:0 6px 0 4px;font-family:simsun}.page .next,.page .prev{width:42px;background-color:#fff}.page .prev{padding-left:26px;padding-right:12px;background-position:12px 11px}.page .next{padding-left:12px;padding-right:26px;background-position:-57px 11px}.page .next:hover,.page .prev:hover{background-color:#76bbec}.page .prev:hover{background-position:12px -14px}.page .next:hover{background-position:-57px -14px}.page .now,.page a:hover{color:#fff;border-color:#30c37c;background-color:#30c37c;text-decoration:none}.page .next:hover,.page .prev:hover,.page a:hover{background-color:#30c37c}.page .now:hover{background-color:#30c37c}.listinfo{font-size:14px;overflow:hidden;width:960px;float:left}.listinfo{padding-bottom:40px}.listheader{height:50px;line-height:50px;background-color:#fbfbfd;color:#999}.listheader,.item{position:relative;padding-left:20px}.listheader,.item{padding-right:122px}.name,.nameinfo{float:left;width:47.685185%;position:relative;white-space:normal}.ullsit{overflow:hidden;clear:both}.ullsit li:nth-child(even){background-color:#fbfbfd}.item{clear:both;font-size:0;overflow:hidden}.item--even{background-color:#fbfbfd}.describeinfo,.weightinfo,.nameinfo{line-height:50px;height:50px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px}.describeinfo,.weightinfo,.describe,.weight{float:left;padding-left:20px;width:23.611111%}.c_tx_highlight{color:#31c27c}.pagebar{padding:40px 0 0}.side{float:right;width:220px}.module{margin:0 0 10px 0;border:1px solid #e6e6e6}.module-header{padding:7px 10px;border-bottom:1px solid #e6e6e6;background-color:#fafafa}.module-header h3{font:14px/23px 'Microsoft YaHei',arial}.module-header .more{float:right;margin:0 0 0 10px;font-family:simsun,'\u5B8B\u4F53';height:24px;line-height:24px;color:#666}.module-header .more:hover{color:#aaa}.module_2 p{line-height:20px;padding:10px 8px}.module_2 p a{padding:0 8px;white-space:nowrap;border-right:1px solid #ccc;color:#31c27c}.module_2 p a:hover{color:#0093aa}.module_2 p a:last-child{border:0}.side-list{padding:9px 5px 6px 10px;background:#fff}.side-list li{position:relative;height:24px;overflow:hidden;padding:3px 0 3px 24px;line-height:24px}.side-list .n1,.side-list .n2{position:absolute;top:6px;left:-3px;height:16px;font:italic 12px/16px arial;width:16px;text-align:center}.side-list .n1{background:#30c37e;color:#fff}.side-list .n2{color:#999}.side-list .pic{display:none;float:left;margin:3px 5px 0 0}.side-list .pic img{vertical-align:top}.side-list p{max-height:36px;overflow:hidden;zoom:1;word-break:break-all;word-wrap:break-word}.side-list p a{color:#666}.side-list p a:hover{color:#aaa}.side-list .price{display:none;overflow:hidden;zoom:1;margin:5px 0 0;color:#f33;line-height:18px;height:18px}.side-list .current{height:66px;line-height:18px}.side-list .current .pic,.side-list .current .price{display:block}.mod_intro{overflow:hidden;margin:20px 0 21px}.mod_intro__cover{float:left;margin-right:20px}.mod_intro__pic{width:94px;height:94px;vertical-align:middle}.mod_intro_singer__pic{border-radius:94px}.mod_intro__base{line-height:40px;font-size:16px;overflow:hidden}.mod_intro__title{float:left;white-space:nowrap;font-weight:400;font-size:100%}.mod_intro_singer__singer{margin-right:30px;max-width:300px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.mod_intro_singer__link{margin-right:20px}.mod_btn_green{border:1px solid #31c27c;background:#31c27c;color:#fff}.mod_btn,.mod_btn_green{border-radius:2px;font-size:14px;margin-right:6px;padding:0 23px;height:38px;line-height:38px;display:inline-block;white-space:nowrap;box-sizing:border-box;overflow:hidden}.mod_btn,.mod_btn_green:hover{color:#fff}.mod_btn_green__icon_play{padding-right:10px}.mod_btn_green__icon_play::before{content:"\e634";font-family:iconfont;font-size:12px;color:#fff}.mod_intro_singer__link strong{font-weight:400;margin-left:4px}span.highlighted{color:red}

卑微程序员是否能够安然通过?

卑微程序员用Django 3.0.3 集成whoosh站内搜索竟遇到这种事 ImportError: cannot import name 'six' from 'django.utils'

“榱栋崩折” ? ?

卑微程序员用Django 3.0.3 集成whoosh站内搜索竟遇到这种事 ImportError: cannot import name 'six' from 'django.utils'

卑微程序员用Django 3.0.3 集成whoosh站内搜索竟遇到这种事 ImportError: cannot import name 'six' from 'django.utils'

即使看起来很慌,但其实我内心是非常冷静的。 毕竟沉着冷静是一个高级程序员的基本职业素养。而且这个错误看起来也很容易理解,可以从错误堆栈中看出....\haystack\utils\__init__.py文件中 引用django.utils的six包不存在。随着错误堆栈中的位置查看可以看到确实有这么一条引用

卑微程序员用Django 3.0.3 集成whoosh站内搜索竟遇到这种事 ImportError: cannot import name 'six' from 'django.utils'

而django.utils包中确实不存在这个包, ?????????????????[震惊], 为什么会少呢? 我的Django 都已经是3.x的最新的版本,而haystack也已经到了2.8的版本。当时我的状态:

卑微程序员用Django 3.0.3 集成whoosh站内搜索竟遇到这种事 ImportError: cannot import name 'six' from 'django.utils'

让我们来看看这个:

卑微程序员用Django 3.0.3 集成whoosh站内搜索竟遇到这种事 ImportError: cannot import name 'six' from 'django.utils'

嘿嘿 ,不愧是我。从官方的资料看原来是 django 3.x的版本移除掉了私有python2兼容性api其中就包括了我们缺少的six ,那简单直接把six给装上不就行了,可以从django2.0版本中找到six模块, 或者使用命令安装 six

pip install six

然后把six.py放到django.utils包下,然后重新启动django 。

    成了?

卑微程序员用Django 3.0.3 集成whoosh站内搜索竟遇到这种事 ImportError: cannot import name 'six' from 'django.utils'

这次又少了什么东西?

卑微程序员用Django 3.0.3 集成whoosh站内搜索竟遇到这种事 ImportError: cannot import name 'six' from 'django.utils'

看一下这东西,我ca嘞这东西又是什么? 怎么这么。。。。。。。???? 眼熟?

卑微程序员用Django 3.0.3 集成whoosh站内搜索竟遇到这种事 ImportError: cannot import name 'six' from 'django.utils'

这个函数也被移除了, 从文档上来看这东西其实是six.py中的一个方法, 嘿嘿找到six.py 打开发现这个函数确实存在, 于是

卑微程序员用Django 3.0.3 集成whoosh站内搜索竟遇到这种事 ImportError: cannot import name 'six' from 'django.utils'

把six.py中的python_2_unicode_compatible()加入到django.utils.encoding.py文件中,打完收工,重启django

卑微程序员用Django 3.0.3 集成whoosh站内搜索竟遇到这种事 ImportError: cannot import name 'six' from 'django.utils'卑微程序员用Django 3.0.3 集成whoosh站内搜索竟遇到这种事 ImportError: cannot import name 'six' from 'django.utils'

此时错误堆栈也没有了, 项目路由也能正常访问

卑微程序员用Django 3.0.3 集成whoosh站内搜索竟遇到这种事 ImportError: cannot import name 'six' from 'django.utils'

    总结

       1, 问题是解决了,但是这种修改源代码的方式是不可取的,所以在开启项目之前要有足够的技术调研,和计划。就不要出现这种版本冲突

       2, 第二点还没想好, 也许会有更好的方式来解决目前的问题,但是花太多时间去解决版本冲突属实是很浪费时间,所以还是要做好调研。

Apipost 私有化火热进行中

评论