django-haystack全文檢索詳細教程


前幾天要用Django-haystack來實現搜索功能,網上一搜中文資源少之又少,雖說有官方文檔,但相信對於我們這些英語差的同學來說要看懂真的是一件難事。特別是關於高級部分,特地找了個英語專業的來翻譯,也沒能看出個名堂來,專業性實在是太強了,導致完全看不懂。。。

但是,對於一些小站點的開發來說,下面我要給大家講的完全足夠用了,只不過有時候確實麻煩點。好了,言歸正傳。為了節約時間,簡單設置部分從網上找了一篇博客,但是這邊文章沒有解釋配置的作用,而我會給你們詳細解釋並拓展。

轉載部分來自:點擊打開鏈接

 

一:使用的工具

  • haystack是django的開源搜索框架,該框架支持Solr,Elasticsearch,Whoosh, *Xapian*搜索引擎,不用更改代碼,直接切換引擎,減少代碼量。
  • 搜索引擎使用Whoosh,這是一個由純Python實現的全文搜索引擎,沒有二進制文件等,比較小巧,配置比較簡單,當然性能自然略低。
  • 中文分詞Jieba,由於Whoosh自帶的是英文分詞,對中文的分詞支持不是太好,故用jieba替換whoosh的分詞組件。
  • 其他:Python 2.7 or 3.4.4, Django 1.8.3或者以上,Debian 4.2.6_3

二:配置說明

 

現在假設我們的項目叫做Project,有一個myapp的app,簡略的目錄結構如下。

  • Project
    • Project
      • settings.py
    • blog
      • models.py

models.py的內容假設如下:

 

  1.  
    from django.db import models
  2.  
    from django.contrib.auth.models import User
  3.  
     
  4.  
     
  5.  
    class Note(models.Model):
  6.  
    user = models.ForeignKey(User)
  7.  
    pub_date = models.DateTimeField()
  8.  
    title = models.CharField(max_length= 200)
  9.  
    body = models.TextField()
  10.  
     
  11.  
    def __str__(self):
  12.  
    return self.title

 

1. 首先安裝各工具

pip install whoosh django-haystack jieba

2. 添加 Haystack 到Django的 INSTALLED_APPS

配置Django項目的settings.py里面的INSTALLED_APPS添加Haystack,例子:

 

 

  1.  
    INSTALLED_APPS = [
  2.  
    'django.contrib.admin',
  3.  
    'django.contrib.auth',
  4.  
    'django.contrib.contenttypes',
  5.  
    'django.contrib.sessions',
  6.  
    'django.contrib.sites',
  7.  
     
  8.  
    # Added. haystack先添加,
  9.  
    'haystack',
  10.  
    # Then your usual apps... 自己的app要寫在haystakc后面
  11.  
    'blog',
  12.  
    ]

3. 修改 你的 settings.py,以配置引擎

本教程使用的是Whoosh,故配置如下:

 

  1.  
    import os
  2.  
    HAYSTACK_CONNECTIONS = {
  3.  
    'default': {
  4.  
    'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
  5.  
    'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
  6.  
    },
  7.  
    }
其中顧名思義, ENGINE為使用的引擎必須要有,如果引擎是 Whoosh,則 PATH必須要填寫,其為Whoosh 索引文件的存放文件夾。
其他引擎的配置見 官方文檔

 

 

4.創建索引

 

如果你想針對某個app例如mainapp做全文檢索,則必須在mainapp的目錄下面建立search_indexes.py文件,文件名不能修改。內容如下:


  1.  
    import datetime
  2.  
    from haystack import indexes
  3.  
    from myapp.models import Note
  4.  
     
  5.  
    class NoteIndex(indexes.SearchIndex, indexes.Indexable): #類名必須為需要檢索的Model_name+Index,這里需要檢索Note,所以創建NoteIndex
  6.  
    text = indexes.CharField(document= True, use_template=True) #創建一個text字段
  7.  
     
  8.  
    author = indexes.CharField(model_attr= 'user') #創建一個author字段
  9.  
     
  10.  
    pub_date = indexes.DateTimeField(model_attr= 'pub_date') #創建一個pub_date字段
  11.  
     
  12.  
    def get_model(self): #重載get_model方法,必須要有!
  13.  
    return Note
  14.  
     
  15.  
    def index_queryset(self, using=None): #重載index_..函數
  16.  
    """Used when the entire index for model is updated."""
  17.  
    return self.get_model().objects.filter(pub_date__lte=datetime.datetime.now())

 

為什么要創建索引?索引就像是一本書的目錄,可以為讀者提供更快速的導航與查找。在這里也是同樣的道理,當數據量非常大的時候,若要從這些數據里找出所有的滿足搜索條件的幾乎是不太可能的,將會給服務器帶來極大的負擔。所以我們需要為指定的數據添加一個索引(目錄),在這里是為Note創建一個索引,索引的實現細節是我們不需要關心的,至於為它的哪些字段創建索引,怎么指定,正是我要給大家講的,也是網上所不曾提到的。

 

每個索引里面必須有且只能有一個字段為 document=True,這代表haystack 和搜索引擎將使用此字段的內容作為索引進行檢索(primary field)。其他的字段只是附屬的屬性,方便調用,並不作為檢索數據。直到我自己完成一個搜索器,也沒有用到這些附屬屬性,所以我索性就都刪掉了,大家學習的時候也可以先注釋掉不管。具體作用我也不明白,反正我沒用上。

注意:如果使用一個字段設置了document=True,則一般約定此字段名為text,這是在SearchIndex類里面一貫的命名,以防止后台混亂,當然名字你也可以隨便改,不過不建議改。

並且,haystack提供了use_template=Truetext字段,這樣就允許我們使用數據模板去建立搜索引擎索引的文件,說得通俗點就是索引里面需要存放一些什么東西,例如 Note 的 title 字段,這樣我們可以通過 title 內容來檢索 Note 數據了,舉個例子,假如你搜索 python ,那么就可以檢索出含有title含有 python 的Note了,怎么樣是不是很簡單?數據模板的路徑為templates/search/indexes/yourapp/note_text.txt(推薦在項目根目錄創建一個templates,並在settings.py里為其引入,使得django會從這個templates里尋找模板,當然,只要放在任何一個你的Django能搜索到的tempaltes下面就好,關於這點我想不屬於我們討論的范疇),templates/search/indexes/blog/note_text.txt文件名必須為要索引的類名_text.txt,其內容為

 

  1.  
    {{ object.title }}
  2.  
    {{ object.user.get_full_name }}
  3.  
    {{ object.body }}

這個數據模板的作用是對 Note.titleNote.user.get_full_name, Note.body這三個字段建立索引,當檢索的時候會對這三個字段做全文檢索匹配。上面已經解釋清楚了。

 

5.在URL配置中添加SearchView,並配置模板

 

urls.py中配置如下url信息,當然url路由可以隨意寫。

(r'^search/', include('haystack.urls')),

其實 haystack.urls的內容為

 

  1.  
    from django.conf.urls import url
  2.  
    from haystack.views import SearchView
  3.  
     
  4.  
    urlpatterns = [
  5.  
    url( r'^$', SearchView(), name='haystack_search'),
  6.  
    ]

SearchView()視圖函數默認使用的html模板路徑為 templates/search/search.html(再說一次推薦在根目錄創建templates,並在settings.py里設置好)
所以需要在 templates/search/下添加 search.html文件,內容為

 

 

  1.  
    <h2>Search</h2>
  2.  
     
  3.  
    <form method="get" action=".">
  4.  
    <table>
  5.  
    {{ form.as_table }}
  6.  
    <tr>
  7.  
    <td> </td>
  8.  
    <td>
  9.  
    <input type="submit" value="Search">
  10.  
    </td>
  11.  
    </tr>
  12.  
    </table>
  13.  
     
  14.  
    {% if query %}
  15.  
    <h3>Results</h3>
  16.  
     
  17.  
    {% for result in page.object_list %}
  18.  
    <p>
  19.  
    <a href="{{ result.object.get_absolute_url }}">{{ result.object.title }}</a>
  20.  
    </p>
  21.  
    {% empty %}
  22.  
    <p>No results found.</p>
  23.  
    {% endfor %}
  24.  
     
  25.  
    {% if page.has_previous or page.has_next %}
  26.  
    <div>
  27.  
    {% if page.has_previous %} <a href="?q={{ query }}&page={{ page.previous_page_number }}">{% endif %}« Previous{% if page.has_previous %}</a>{% endif %}
  28.  
    |
  29.  
    {% if page.has_next %} <a href="?q={{ query }}&page={{ page.next_page_number }}">{% endif %}Next »{% if page.has_next %}</a>{% endif %}
  30.  
    </div>
  31.  
    {% endif %}
  32.  
    {% else %}
  33.  
    {# Show some example queries to run, maybe query syntax, something else? #}
  34.  
    {% endif %}
  35.  
    </form>

很明顯,它自帶了分頁。

 

然后為大家解釋一下這個文件。首先可以看到模板里使用了的變量有 form,query,page 。下面一個個的說一下。

form,很明顯,它和django里的form類是差不多的,可以渲染出一個搜索的表單,相信用過Django的Form都知道,所以也不多說了,不明白的可以去看Django文檔,當然其實我倒最后也沒用上,最后是自己寫了個<form></form>,提供正確的參數如name="seach",method="get"以及你的action地址就OK了。。。如果需要用到更多的搜索功能如過濾的話可能就要自定義Form類了(而且通過上面的例子可以看到,默認的form也是提供一個簡單的過濾器的,可以供你選擇哪些model是需要檢索的,如果一個都不勾的話默認全部搜索,當然我們也是可以自己利用html來模擬這個form的,所以想要實現model過濾還是很簡單的,只要模擬一下這個Form的內容就好了),只有這樣haystack才能夠構造出相應的Form對象來進行檢索,其實和django的Form是一樣的,Form有一個自我檢查數據是否合法的功能,haystack也一樣,關於這個此篇文章不做多說,因為我也不太明白(2333)。具體細節去看文檔,而且文檔上關於View&Form那一節還是比較通俗易懂的,詞匯量要求也不是很高,反正就連我都看懂了一些。。。

query嘛,就是我們搜索的字符串。

關於page,可以看到page有object_list屬性,它是一個list,里面包含了第一頁所要展示的model對象集合,那么list里面到底有多少個呢?我們想要自己控制個數怎么辦呢?不用擔心,haystack為我們提供了一個接口。我們只要在settings.py里設置:

 

  1.  
    #設置每頁顯示的數目,默認為20,可以自己修改
  2.  
    HAYSTACK_SEARCH_RESULTS_PER_PAGE = 8

然后關於分頁的部分,大家看名字應該也能看懂吧。

 

如果想要知道更多的默認context帶的變量,可以自己看看源碼views.py里的SearchView類視圖,相信都能看懂。

那么問題來了。對於一個search頁面來說,我們肯定會需要用到更多自定義的 context 內容,那么這下該怎么辦呢?最初我想到的辦法便是修改haystack源碼,為其添加上更多的 context 內容,你們是不是也有過和我一樣的想法呢?但是這樣做即笨拙又愚蠢,我們不僅需要注意各種環境,依賴關系,而且當服務器主機發生變化時,難道我們還要把 haystack 也復制過去不成?這樣太愚蠢了!突然,我想到既然我不能修改源碼,難道我還不能復用源碼嗎?之后,我用看了一下官方文檔,正如我所想的,通過繼承SeachView來實現重載 context 的內容。官方文檔提供了2個版本的SearchView,我最開始用的是新版的,最后出錯了,也懶得去找錯誤是什么引起的了,直接使用的了舊版本的SearchView,只要你下了haystack,2個版本都是給你安裝好了的。於是我們在myapp目錄下再創建一個search_views.py 文件,位置名字可以自己定,用於寫自己的搜索視圖,代碼實例如下:

 

  1.  
    from haystack.views import SearchView
  2.  
    from .models import *
  3.  
     
  4.  
    class MySeachView(SearchView):
  5.  
    def extra_context(self): #重載extra_context來添加額外的context內容
  6.  
    context = super(MySeachView,self).extra_context()
  7.  
    side_list = Topic.objects.filter(kind= 'major').order_by('add_date')[:8]
  8.  
    context[ 'side_list'] = side_list
  9.  
    return context

 

 

然后再修改urls.py將search請求映射到MySearchView:

 

     url(r'^search/', search_views.MySeachView(), name='haystack_search'),

講完了上下文變量,再讓我們來講一下模板標簽,haystack為我們提供了 {% highlight %}和 {% more_like_this %} 2個標簽,這里我只為大家詳細講解下 highlight的使用。

 

你是否也想讓自己的檢索和百度搜索一樣,將匹配到的文字也高亮顯示呢? {% highlight %} 為我們提供了這個功能(當然不僅是這個標簽,貌似還有一個HighLight類,這個自己看文檔去吧,我英語差,看不明白)。

Syntax:

 
         
{% highlight <text_block> with <query> [css_class "class_name"] [html_tag "span"] [max_length 200] %}

 

大概意思是為 text_block 里的 query 部分添加css_class,html_tag,而max_length 為最終返回長度,相當於 cut ,我看了一下此標簽實現源碼,默認的html_tag 值為 span ,css_class 值為 highlighted,max_length 值為 200,然后就可以通過CSS來添加效果。如默認時:

 

  1.  
    span.highlighted {
  2.  
    color: red;
  3.  
    }


 

Example:

 

  1.  
    # 使用默認值
  2.  
    {% highlight result.summary with query %}
  3.  
     
  4.  
    # 這里我們為 {{ result.summary }}里所有的 {{ query }} 指定了一個<div></div>標簽,並且將class設置為highlight_me_please,這樣就可以自己通過CSS為{{ query }}添加高亮效果了,怎么樣,是不是很科學呢
  5.  
    {% highlight result.summary with query html_tag "div" css_class "highlight_me_please" %}
  6.  
     
  7.  
    # 這里可以限制最終{{ result.summary }}被高亮處理后的長度
  8.  
    {% highlight result.summary with query max_length 40 %}


 

好了,到目前為止,如果你掌握了上面的知識的話,你已經會制作一個比較令人滿意的搜索器了,接下來就是創建index文件了。

 

6.最后一步,重建索引文件

 

使用python manage.py rebuild_index或者使用update_index命令。

好,下面運行項目,進入該url搜索一下試試吧。

每次數據庫更新后都需要更新索引,所以haystack為大家提供了一個接口,只要在settings.py里設置:

 

  1.  
    #自動更新索引
  2.  
    HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'


 

三:下面要做的,使用jieba分詞

 
1 將文件 whoosh_backend.py(該文件路徑為 python路徑/lib/python2.7.5/site-packages/haystack/backends/whoosh_backend.py)拷貝到app下面,並重命名為 whoosh_cn_backend.py,例如 blog/whoosh_cn_backend.py

修改為如下
 
  1.  
    from jieba.analyse import ChineseAnalyzer #在頂部添加
  2.  
     
  3.  
    schema_fields[field_class.index_fieldname] = TEXT(stored= True, analyzer=ChineseAnalyzer(),field_boost=field_class.boost, sortable=True) #注意先找到這個再修改,而不是直接添加

2 在 settings.py中修改引擎,如下
 
  1.  
    import os
  2.  
    HAYSTACK_CONNECTIONS = {
  3.  
    'default': {
  4.  
    'ENGINE': 'blog.whoosh_cn_backend.WhooshEngine', #blog.whoosh_cn_backend便是你剛剛添加的文件
  5.  
    'PATH': os.path.join(BASE_DIR, 'whoosh_index'
  6.  
    },
  7.  
    }
 
3 重建索引,在進行搜索中文試試吧。
 
 
 

四.highlight補充

終於寫完了!!!淚奔!!!最后給大家看看我的

怎么樣,還行吧?眼尖的人會發現,為什么標題里的高等沒有被替換成...,而段落里的數學之前的內容卻被替換成了...,標題本來就很短,想象一下,若是高等數學被顯示成了數學,是不是丟失了最重要的信息呢?高等這么重要的字眼都被省略了,很顯然是不行的,畢竟我是個高等生。那么怎么辦呢?我沒有選擇去看文檔,可能文檔的HighLight類就是用來干這個的吧,但是我選擇了讀highlight 標簽的源碼,最終還是讓我實現了。

我們需要做的是復制粘貼源碼,然后進行修改,而不是選擇直接改源碼,創建一個自己的標簽。為大家奉上。添加myapp/templatetags/my_filters_and_tags.py 文件和 myapp/templatetags/highlighting.py 文件,內容如下(源碼分別位於haystack/templatetags/lighlight.py 和 haystack/utils/lighlighting.py 中):

my_filter_and_tags.py:

 

  1.  
    # encoding: utf-8
  2.  
    from __future__ import absolute_import, division, print_function, unicode_literals
  3.  
     
  4.  
    from django import template
  5.  
    from django.conf import settings
  6.  
    from django.core.exceptions import ImproperlyConfigured
  7.  
    from django.utils import six
  8.  
     
  9.  
    from haystack.utils import importlib
  10.  
     
  11.  
    register = template.Library()
  12.  
     
  13.  
    class HighlightNode(template.Node):
  14.  
    def __init__(self, text_block, query, html_tag=None, css_class=None, max_length=None, start_head=None):
  15.  
    self.text_block = template.Variable(text_block)
  16.  
    self.query = template.Variable(query)
  17.  
    self.html_tag = html_tag
  18.  
    self.css_class = css_class
  19.  
    self.max_length = max_length
  20.  
    self.start_head = start_head
  21.  
     
  22.  
    if html_tag is not None:
  23.  
    self.html_tag = template.Variable(html_tag)
  24.  
     
  25.  
    if css_class is not None:
  26.  
    self.css_class = template.Variable(css_class)
  27.  
     
  28.  
    if max_length is not None:
  29.  
    self.max_length = template.Variable(max_length)
  30.  
     
  31.  
    if start_head is not None:
  32.  
    self.start_head = template.Variable(start_head)
  33.  
     
  34.  
    def render(self, context):
  35.  
    text_block = self.text_block.resolve(context)
  36.  
    query = self.query.resolve(context)
  37.  
    kwargs = {}
  38.  
     
  39.  
    if self.html_tag is not None:
  40.  
    kwargs[ 'html_tag'] = self.html_tag.resolve(context)
  41.  
     
  42.  
    if self.css_class is not None:
  43.  
    kwargs[ 'css_class'] = self.css_class.resolve(context)
  44.  
     
  45.  
    if self.max_length is not None:
  46.  
    kwargs[ 'max_length'] = self.max_length.resolve(context)
  47.  
     
  48.  
    if self.start_head is not None:
  49.  
    kwargs[ 'start_head'] = self.start_head.resolve(context)
  50.  
     
  51.  
    # Handle a user-defined highlighting function.
  52.  
    if hasattr(settings, 'HAYSTACK_CUSTOM_HIGHLIGHTER') and settings.HAYSTACK_CUSTOM_HIGHLIGHTER:
  53.  
    # Do the import dance.
  54.  
    try:
  55.  
    path_bits = settings.HAYSTACK_CUSTOM_HIGHLIGHTER.split( '.')
  56.  
    highlighter_path, highlighter_classname = '.'.join(path_bits[:-1]), path_bits[-1]
  57.  
    highlighter_module = importlib.import_module(highlighter_path)
  58.  
    highlighter_class = getattr(highlighter_module, highlighter_classname)
  59.  
    except (ImportError, AttributeError) as e:
  60.  
    raise ImproperlyConfigured("The highlighter '%s' could not be imported: %s" % (settings.HAYSTACK_CUSTOM_HIGHLIGHTER, e))
  61.  
    else:
  62.  
    from .highlighting import Highlighter
  63.  
    highlighter_class = Highlighter
  64.  
     
  65.  
    highlighter = highlighter_class(query, **kwargs)
  66.  
    highlighted_text = highlighter.highlight(text_block)
  67.  
    return highlighted_text
  68.  
     
  69.  
     
  70.  
    @register.tag
  71.  
    def myhighlight(parser, token):
  72.  
    """
  73.  
    Takes a block of text and highlights words from a provided query within that
  74.  
    block of text. Optionally accepts arguments to provide the HTML tag to wrap
  75.  
    highlighted word in, a CSS class to use with the tag and a maximum length of
  76.  
    the blurb in characters.
  77.  
     
  78.  
    Syntax::
  79.  
     
  80.  
    {% highlight <text_block> with <query> [css_class "class_name"] [html_tag "span"] [max_length 200] %}
  81.  
     
  82.  
    Example::
  83.  
     
  84.  
    # Highlight summary with default behavior.
  85.  
    {% highlight result.summary with request.query %}
  86.  
     
  87.  
    # Highlight summary but wrap highlighted words with a div and the
  88.  
    # following CSS class.
  89.  
    {% highlight result.summary with request.query html_tag "div" css_class "highlight_me_please" %}
  90.  
     
  91.  
    # Highlight summary but only show 40 characters.
  92.  
    {% highlight result.summary with request.query max_length 40 %}
  93.  
    """
  94.  
    bits = token.split_contents()
  95.  
    tag_name = bits[ 0]
  96.  
     
  97.  
    if not len(bits) % 2 == 0:
  98.  
    raise template.TemplateSyntaxError(u"'%s' tag requires valid pairings arguments." % tag_name)
  99.  
     
  100.  
    text_block = bits[ 1]
  101.  
     
  102.  
    if len(bits) < 4:
  103.  
    raise template.TemplateSyntaxError(u"'%s' tag requires an object and a query provided by 'with'." % tag_name)
  104.  
     
  105.  
    if bits[2] != 'with':
  106.  
    raise template.TemplateSyntaxError(u"'%s' tag's second argument should be 'with'." % tag_name)
  107.  
     
  108.  
    query = bits[ 3]
  109.  
     
  110.  
    arg_bits = iter(bits[ 4:])
  111.  
    kwargs = {}
  112.  
     
  113.  
    for bit in arg_bits:
  114.  
    if bit == 'css_class':
  115.  
    kwargs[ 'css_class'] = six.next(arg_bits)
  116.  
     
  117.  
    if bit == 'html_tag':
  118.  
    kwargs[ 'html_tag'] = six.next(arg_bits)
  119.  
     
  120.  
    if bit == 'max_length':
  121.  
    kwargs[ 'max_length'] = six.next(arg_bits)
  122.  
     
  123.  
    if bit == 'start_head':
  124.  
    kwargs[ 'start_head'] = six.next(arg_bits)
  125.  
     
  126.  
    return HighlightNode(text_block, query, **kwargs)

lighlighting.py:

 

 

  1.  
    # encoding: utf-8
  2.  
     
  3.  
    from __future__ import absolute_import, division, print_function, unicode_literals
  4.  
     
  5.  
    from django.utils.html import strip_tags
  6.  
     
  7.  
     
  8.  
    class Highlighter(object):
  9.  
    #默認值
  10.  
    css_class = 'highlighted'
  11.  
    html_tag = 'span'
  12.  
    max_length = 200
  13.  
    start_head = False
  14.  
    text_block = ''
  15.  
     
  16.  
    def __init__(self, query, **kwargs):
  17.  
    self.query = query
  18.  
     
  19.  
    if 'max_length' in kwargs:
  20.  
    self.max_length = int(kwargs[ 'max_length'])
  21.  
     
  22.  
    if 'html_tag' in kwargs:
  23.  
    self.html_tag = kwargs[ 'html_tag']
  24.  
     
  25.  
    if 'css_class' in kwargs:
  26.  
    self.css_class = kwargs[ 'css_class']
  27.  
     
  28.  
    if 'start_head' in kwargs:
  29.  
    self.start_head = kwargs[ 'start_head']
  30.  
     
  31.  
    self.query_words = set([word.lower() for word in self.query.split() if not word.startswith('-')])
  32.  
     
  33.  
    def highlight(self, text_block):
  34.  
    self.text_block = strip_tags(text_block)
  35.  
    highlight_locations = self.find_highlightable_words()
  36.  
    start_offset, end_offset = self.find_window(highlight_locations)
  37.  
    return self.render_html(highlight_locations, start_offset, end_offset)
  38.  
     
  39.  
    def find_highlightable_words(self):
  40.  
    # Use a set so we only do this once per unique word.
  41.  
    word_positions = {}
  42.  
     
  43.  
    # Pre-compute the length.
  44.  
    end_offset = len(self.text_block)
  45.  
    lower_text_block = self.text_block.lower()
  46.  
     
  47.  
    for word in self.query_words:
  48.  
    if not word in word_positions:
  49.  
    word_positions[word] = []
  50.  
     
  51.  
    start_offset = 0
  52.  
     
  53.  
    while start_offset < end_offset:
  54.  
    next_offset = lower_text_block.find(word, start_offset, end_offset)
  55.  
     
  56.  
    # If we get a -1 out of find, it wasn't found. Bomb out and
  57.  
    # start the next word.
  58.  
    if next_offset == -1:
  59.  
    break
  60.  
     
  61.  
    word_positions[word].append(next_offset)
  62.  
    start_offset = next_offset + len(word)
  63.  
     
  64.  
    return word_positions
  65.  
     
  66.  
    def find_window(self, highlight_locations):
  67.  
    best_start = 0
  68.  
    best_end = self.max_length
  69.  
     
  70.  
    # First, make sure we have words.
  71.  
    if not len(highlight_locations):
  72.  
    return (best_start, best_end)
  73.  
     
  74.  
    words_found = []
  75.  
     
  76.  
    # Next, make sure we found any words at all.
  77.  
    for word, offset_list in highlight_locations.items():
  78.  
    if len(offset_list):
  79.  
    # Add all of the locations to the list.
  80.  
    words_found.extend(offset_list)
  81.  
     
  82.  
    if not len(words_found):
  83.  
    return (best_start, best_end)
  84.  
     
  85.  
    if len(words_found) == 1:
  86.  
    return (words_found[0], words_found[0] + self.max_length)
  87.  
     
  88.  
    # Sort the list so it's in ascending order.
  89.  
    words_found = sorted(words_found)
  90.  
     
  91.  
    # We now have a denormalized list of all positions were a word was
  92.  
    # found. We'll iterate through and find the densest window we can by
  93.  
    # counting the number of found offsets (-1 to fit in the window).
  94.  
    highest_density = 0
  95.  
     
  96.  
    if words_found[:-1][0] > self.max_length:
  97.  
    best_start = words_found[: -1][0]
  98.  
    best_end = best_start + self.max_length
  99.  
     
  100.  
    for count, start in enumerate(words_found[:-1]):
  101.  
    current_density = 1
  102.  
     
  103.  
    for end in words_found[count + 1:]:
  104.  
    if end - start < self.max_length:
  105.  
    current_density += 1
  106.  
    else:
  107.  
    current_density = 0
  108.  
     
  109.  
    # Only replace if we have a bigger (not equal density) so we
  110.  
    # give deference to windows earlier in the document.
  111.  
    if current_density > highest_density:
  112.  
    best_start = start
  113.  
    best_end = start + self.max_length
  114.  
    highest_density = current_density
  115.  
     
  116.  
    return (best_start, best_end)
  117.  
     
  118.  
    def render_html(self, highlight_locations=None, start_offset=None, end_offset=None):
  119.  
    # Start by chopping the block down to the proper window.
  120.  
    #text_block為內容,start_offset,end_offset分別為第一個匹配query開始和按長度截斷位置
  121.  
    text = self.text_block[start_offset:end_offset]
  122.  
     
  123.  
    # Invert highlight_locations to a location -> term list
  124.  
    term_list = []
  125.  
     
  126.  
    for term, locations in highlight_locations.items():
  127.  
    term_list += [(loc - start_offset, term) for loc in locations]
  128.  
     
  129.  
    loc_to_term = sorted(term_list)
  130.  
     
  131.  
    # Prepare the highlight template
  132.  
    if self.css_class:
  133.  
    hl_start = '<%s class="%s">' % (self.html_tag, self.css_class)
  134.  
    else:
  135.  
    hl_start = '<%s>' % (self.html_tag)
  136.  
     
  137.  
    hl_end = '</%s>' % self.html_tag
  138.  
     
  139.  
    # Copy the part from the start of the string to the first match,
  140.  
    # and there replace the match with a highlighted version.
  141.  
    #matched_so_far最終求得為text中最后一個匹配query的結尾
  142.  
    highlighted_chunk = ""
  143.  
    matched_so_far = 0
  144.  
    prev = 0
  145.  
    prev_str = ""
  146.  
     
  147.  
    for cur, cur_str in loc_to_term:
  148.  
    # This can be in a different case than cur_str
  149.  
    actual_term = text[cur:cur + len(cur_str)]
  150.  
     
  151.  
    # Handle incorrect highlight_locations by first checking for the term
  152.  
    if actual_term.lower() == cur_str:
  153.  
    if cur < prev + len(prev_str):
  154.  
    continue
  155.  
     
  156.  
    #分別添上每個query+其后面的一部分(下一個query的前一個位置)
  157.  
    highlighted_chunk += text[prev + len(prev_str):cur] + hl_start + actual_term + hl_end
  158.  
    prev = cur
  159.  
    prev_str = cur_str
  160.  
     
  161.  
    # Keep track of how far we've copied so far, for the last step
  162.  
    matched_so_far = cur + len(actual_term)
  163.  
     
  164.  
    # Don't forget the chunk after the last term
  165.  
    #加上最后一個匹配的query后面的部分
  166.  
    highlighted_chunk += text[matched_so_far:]
  167.  
     
  168.  
    #如果不要開頭not start_head才加點
  169.  
    if start_offset > 0 and not self.start_head:
  170.  
    highlighted_chunk = '...%s' % highlighted_chunk
  171.  
     
  172.  
    if end_offset < len(self.text_block):
  173.  
    highlighted_chunk = '%s...' % highlighted_chunk
  174.  
     
  175.  
    #可見到目前為止還不包含start_offset前面的,即第一個匹配的前面的部分(text_block[:start_offset]),如需展示(當start_head為True時)便加上
  176.  
    if self.start_head:
  177.  
    highlighted_chunk = self.text_block[:start_offset] + highlighted_chunk
  178.  
    return highlighted_chunk



 

添加上這2個文件之后,便可以使用自己的標簽 {% mylighlight %}了,使用時記得Load哦!

 

Syntax:

 
        
{% myhighlight <text_block> with <query> [css_class "class_name"] [html_tag "span"] [max_length 200] [start_head True] %}

 

可見我只是多添加了一個選項 start_head ,默認為False,如果設置為True 則不會省略。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM