为什么这个Python方法会泄漏内存?

8

这种方法会遍历数据库中的术语列表,检查这些术语是否在传递为参数的文本中,如果有一个,则用搜索页面链接替换它,并将该术语作为参数传递。

术语数量很高(约100000个),因此该过程相当缓慢,但这没关系,因为它是作为cron任务执行的。然而,它会导致脚本内存消耗激增,我找不到原因:

class SearchedTerm(models.Model):

[...]

@classmethod
def add_search_links_to_text(cls, string, count=3, queryset=None):
    """
        Take a list of all researched terms and search them in the 
        text. If they exist, turn them into links to the search
        page.

        This process is limited to `count` replacements maximum.

        WARNING: because the sites got different URLS schemas, we don't
        provides direct links, but we inject the {% url %} tag 
        so it must be rendered before display. You can use the `eval`
        tag from `libs` for this. Since they got different namespace as
        well, we enter a generic 'namespace' and delegate to the 
        template to change it with the proper one as well.

        If you have a batch process to do, you can pass a query set
        that will be used instead of getting all searched term at
        each calls.
    """

    found = 0

    terms = queryset or cls.on_site.all()

    # to avoid duplicate searched terms to be replaced twice 
    # keep a list of already linkified content
    # added words we are going to insert with the link so they won't match
    # in case of multi passes
    processed = set((u'video', u'streaming', u'title', 
                     u'search', u'namespace', u'href', u'title', 
                     u'url'))

    for term in terms:

        text = term.text.lower()

        # no small word and make
        # quick check to avoid all the rest of the matching
        if len(text) < 3 or text not in string:
            continue

        if found and cls._is_processed(text, processed):
            continue

        # match the search word with accent, for any case
        # ensure this is not part of a word by including 
        # two 'non-letter' character on both ends of the word
        pattern = re.compile(ur'([^\w]|^)(%s)([^\w]|$)' % text, 
                            re.UNICODE|re.IGNORECASE)

        if re.search(pattern, string):
            found += 1

            # create the link string
            # replace the word in the description 
            # use back references (\1, \2, etc) to preserve the original
            # formatin
            # use raw unicode strings (ur"string" notation) to avoid
            # problems with accents and escaping

            query = '-'.join(term.text.split())
            url = ur'{%% url namespace:static-search "%s" %%}' % query
            replace_with = ur'\1<a title="\2 video streaming" href="%s">\2</a>\3' % url

            string = re.sub(pattern, replace_with, string)

            processed.add(text)

            if found >= 3:
                break

    return string

您可能也需要这段代码:
class SearchedTerm(models.Model):

[...]

@classmethod
def _is_processed(cls, text, processed):
    """
        Check if the text if part of the already processed string
        we don't use `in` the set, but `in ` each strings of the set
        to avoid subtring matching that will destroy the tags.

        This is mainly an utility function so you probably won't use
        it directly.
    """
    if text in processed:
        return True

    return any(((text in string) for string in processed))

我只有两个对象的引用可能是嫌疑人:termsprocessed。但我看不出它们不被垃圾回收的任何原因。

编辑:

我认为我应该说这个方法是在Django模型方法中调用的。我不知道这是否相关,但这是代码:

class Video(models.Model):

[...]

def update_html_description(self, links=3, queryset=None):
    """
        Take a list of all researched terms and search them in the 
        description. If they exist, turn them into links to the search
        engine. Put the reset into `html_description`.

        This use `add_search_link_to_text` and has therefor, the same 
        limitations.

        It DOESN'T call save().
    """
    queryset = queryset or SearchedTerm.objects.filter(sites__in=self.sites.all())
    text = self.description or self.title
    self.html_description = SearchedTerm.add_search_links_to_text(text, 
                                                                  links, 
                                                                  queryset)

我可以想象自动Python正则表达式缓存会占用一些内存。但它应该只做一次,而且在每次调用update_html_description时内存消耗都会增加。

问题不仅在于它消耗了大量内存,而且它没有释放:每次调用都会占用大约3%的内存,最终填满并崩溃脚本,显示“无法分配内存”。


2
在像Python这样的垃圾回收语言中,几乎不可能出现内存泄漏。严格来说,内存泄漏是指没有变量引用的内存。在C ++中,如果在类中分配内存,但没有声明析构函数,则可能会出现内存泄漏。你这里所说的只是高内存消耗问题。 - Byron Whitlock
好的。那么我遇到了一个高内存消耗的问题,每次调用后都会变得越来越高。但是既然它是一个方法,而且在完成后我没有对任何东西保持引用,为什么还会有东西占用内存呢? - Bite code
你确定这个调用是导致内存消耗的原因了吗? - Karoly Horvath
是的:删除它,内存就会保持不变。目前我没有运行它,因为它对网站不是关键性的。 - Bite code
我尝试使用gc模块来了解更多信息,但是gc.get_object()会在5分钟内用数据饱和我的屏幕 :-( - Bite code
显示剩余4条评论
4个回答

3

一旦调用整个queryset,它就会完全加载到内存中,这将消耗大量内存。如果结果集非常大,您应该获取结果的分块,这可能会增加对数据库的访问次数,但意味着内存消耗要少得多。


1
我对整个查询集都在内存中没有问题。这不多,最多100000个字符串包装在模型对象中。每次调用后,它应该被垃圾回收。问题是每次调用都会累积地消耗内存。第一次调用占用3%的RAM。下一次调用6%,依此类推。 - Bite code
关于此事更新了问题。 - Bite code

2

我完全找不到问题的原因,但现在我通过调用包含此方法调用的脚本(使用subprocess)来隔离臭名昭著的代码片段。内存会增加,但当Python进程结束后,内存会恢复正常。

说起来真是糟糕。

但现在这就是我的全部内容了。


1

确保你没有在 DEBUG 模式下运行。


2
这是一个生产服务器,DEBUG被设置为False。但是很好发现,这确实会导致内存泄漏,你的回答迫使我检查了一下。 - Bite code

-1
我认为我应该说这个方法是在Django模型方法内部调用的。
@classmethod
为什么? 为什么这是“类级别”的?
为什么这些不是普通的方法,可以有普通的作用域规则,并且在正常情况下会被垃圾回收?
换句话说(以答案的形式)
去掉 @classmethod。

@classmethod 是一个类方法,它被调用在 update_html_description(self, links=3, queryset=None) 这个实例方法中。这里没有任何混淆。 - Bite code
@e-satis。我相信没有混淆。也没有必要 - S.Lott
我添加了它们所属的类的名称以澄清事情:SearchedTerm有一个类方法,可以将任何文本链接化,而Video实例使用此方法来更新其html_description。 add_search_links_to_text之所以是类方法,当然是因为它是一种实用方法,不是用于操作SearchedTerm实例的。 - Bite code
2
既然你有124K的声望,我刚刚测试了一下代码,将所有类方法转换为实例方法。但是这并没有改变什么。考虑到你的回答相当激进且错误,并且我在SO上已经看到你做过几次这样的事情,所以我给你一个-1。 - Bite code
1
在我的上一条评论中谈到了聊天提案。放松点,伙计。如果你这样紧张,会得心脏病的。 - Bite code
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接