Django,自定义模板过滤器 - 正则表达式问题

2 投票
4 回答
2130 浏览
提问于 2025-04-15 11:47

我正在尝试在Django中实现一个WikiLink模板过滤器,这个过滤器会查询数据库模型,根据页面是否存在来给出不同的响应,类似于维基百科中的红色链接。这个过滤器不会引发错误,但也不会对输入做任何处理。

WikiLink的定义是:[[ThisIsAWikiLink | 这是替代文本]]

下面是一个不查询数据库的工作示例:

from django import template
from django.template.defaultfilters import stringfilter
from sites.wiki.models import Page
import re

register = template.Library()

@register.filter
@stringfilter
def wikilink(value):
    return re.sub(r'\[\[ ?(.*?) ?\| ?(.*?) ?\]\]', r'<a href="/Sites/wiki/\1">\2</a>', value)
wikilink.is_safe = True

这个输入value)是一个多行字符串,里面包含HTML和很多WikiLinks。

期望的输出是将[[ThisIsAWikiLink | 这是替代文本]]替换为:

  • <a href="/Sites/wiki/ThisIsAWikiLink">这是替代文本</a>

    或者如果"ThisIsAWikiLink"在数据库中不存在:

  • <a href="/Sites/wiki/ThisIsAWikiLink/edit" class="redlink">这是替代文本</a>

然后返回这个值。

下面是无法工作的代码(根据评论/回答进行了编辑):

from django import template
from django.template.defaultfilters import stringfilter
from sites.wiki.models import Page
import re

register = template.Library()

@register.filter
@stringfilter
def wikilink(value):
    m = re.match(r'\[\[ ?(.*?) ?\| ?(.*?) ?\]\]', value)

    if(m):
        page_alias = m.group(2)
        page_title = m.group(3)
        try:
            page = Page.objects.get(alias=page_alias)
            return re.sub(r'(\[\[)(.*)\|(.*)(\]\])', r'<a href="Sites\/wiki\/\2">\3</a>', value)
        except Page.DoesNotExist:
             return re.sub(r'(\[\[)(.*)\|(.*)(\]\])', r'<a href="Sites\/wiki\/\2\/edit" class="redlink">\3</a>', value)
    else:
        return value
wikilink.is_safe = True

这段代码需要做的事情是:

  • 提取value中的所有WikiLinks
  • 查询Page模型,看看页面是否存在
  • 将所有WikiLinks替换为正常链接,样式根据每个维基页面的存在情况而定。
  • 返回修改后的value

更新的问题是: 什么正则表达式(方法)可以返回一个包含WikiLinks的Python列表,这些链接可以被修改并用于替换原始匹配项(在被修改后)。

编辑:

我想做类似这样的事情:

def wikilink(value):
    regex = re.magic_method(r'\[\[ ?(.*?) ?\| ?(.*?) ?\]\]', value)

    foreach wikilink in regex:
         alias = wikilink.group(0)
         text = wikilink.group(1)

         if(alias exists in Page):
              regex.sub("<a href="+alias+">"+ text +"</a>")
         else:
              regex.sub("<a href="+alias+" class='redlink'>"+ text +"</a>")

    return value

4 个回答

1

代码:

import re

def page_exists(alias):
    if alias == 'ThisIsAWikiLink':
        return True

    return False

def wikilink(value):
    if value == None:
        return None

    for alias, text in re.findall('\[\[\s*(.*?)\s*\|\s*(.*?)\s*\]\]',value):
        if page_exists(alias):
            value = re.sub('\[\[\s*%s\s*\|\s*%s\s*\]\]' % (alias,text), '<a href="/Sites/wiki/%s">%s</a>' % (alias, text),value)            
        else:
            value = re.sub('\[\[\s*%s\s*\|\s*%s\s*\]\]' % (alias,text), '<a href="/Sites/wiki/%s/edit/" class="redtext">%s</a>' % (alias, text), value)

    return value

示例结果:

>>> import wikilink
>>> wikilink.wikilink(None)
>>> wikilink.wikilink('')
''
>>> wikilink.wikilink('Test')
'Test'
>>> wikilink.wikilink('[[ThisIsAWikiLink | This is the alt text]]')
'<a href="/Sites/wiki/ThisIsAWikiLink">This is the alt text</a>'
>>> wikilink.wikilink('[[ThisIsABadWikiLink | This is the alt text]]')
'<a href="/Sites/wiki/ThisIsABadWikiLink/edit/" class="redtext">This is the alt text</a>'
>>> wikilink.wikilink('[[ThisIsAWikiLink | This is the alt text]]\n[[ThisIsAWikiLink | This is another instance]]')
'<a href="/Sites/wiki/ThisIsAWikiLink">This is the alt text</a>\n<a href="/Sites/wiki/ThisIsAWikiLink">This is another instance</a>'
>>> wikilink.wikilink('[[ThisIsAWikiLink | This is the alt text]]\n[[ThisIsAWikiLink | This is another instance]]')

一般评论:

  • findall 是你需要的神奇正则表达式函数
  • page_exists 改成你想要运行的任何查询
  • 这个方法容易受到HTML注入攻击(正如上面提到的Dave W. Smith所说)
  • 每次循环都重新编译正则表达式效率不高
  • 每次都查询数据库效率也不高

我觉得用这种方法很快就会遇到性能问题。

3

这个问题可以通过一小部分单元测试很快解决。

可以单独测试的过滤器部分(需要稍微调整一下代码):

  • 判断值是否包含你想要的模式
  • 如果有匹配的页面,会生成什么字符串
  • 如果没有匹配的页面,会生成什么字符串

这样可以帮助你找出问题出在哪里。你可能会发现需要重新调整正则表达式,以处理“|”周围的可选空格。

另外,乍一看,你的过滤器似乎存在安全隐患。你声称结果是安全的,但你没有过滤掉像脚本标签这样的恶意内容。

4

如果你的字符串里除了维基链接还有其他文本,你的过滤器就不管用了,因为你用的是 re.match,而不是 re.searchre.match 只会在字符串的开头匹配,而 re.search 可以在字符串的任何地方匹配。你可以看看这个链接了解更多关于 匹配和搜索的区别

另外,你的正则表达式使用了贪婪的 *,所以如果一行里有多个维基链接,它就不管用了。你可以用 *? 来让它变得不那么贪婪:

re.search(r'\[\[(.*?)\|(.*?)\]\]', value)

补充:

关于如何修复你的代码,我建议你使用 re.sub 和一个回调函数。这样做的好处有:

  • 如果同一行里有多个维基链接,它能正确工作。
  • 只需要遍历一次字符串就可以了。你不需要先找维基链接,再进行替换。

下面是一个实现的简单示例:

import re

WIKILINK_RE = re.compile(r'\[\[(.*?)\|(.*?)\]\]')

def wikilink(value):
  def wikilink_sub_callback(match_obj):
    alias = match_obj.group(1).strip()
    text = match_obj.group(2).strip()
    if(alias exists in Page):
      class_attr = ''
    else:
      class_attr = ' class="redlink"'
    return '<a href="%s"%s>%s</a>' % (alias, class_attr, text)

  return WIKILINK_RE.sub(wikilink_sub_callback, value)

撰写回答