Python: 调用 Python 对象时超过了最大递归深度

67
我已经建立了一个爬虫,需要运行在大约五百万个页面上(通过增加url ID),然后解析包含所需信息的页面。
使用运行在200K个URL上的算法并保存好的和坏的结果后,我发现浪费了很多时间。我可以看到有一些返回的被减数,可以用来检查下一个有效的URL。
你可以快速看到这些被减数(前几个“good IDs”的小例子)。
510000011 # +8
510000029 # +18
510000037 # +8
510000045 # +8
510000052 # +7
510000060 # +8
510000078 # +18
510000086 # +8
510000094 # +8
510000102 # +8
510000110 # etc'
510000128
510000136
510000144
510000151
510000169
510000177
510000185
510000193
510000201

在爬取约200K个URL后,只得到了14K个好结果,我知道我正在浪费时间,需要优化它。所以我运行了一些统计并构建了一个函数,将检查URL,同时使用8\18\17\8(返回顶部的减数)等递增ID。
这是该功能 -
def checkNextID(ID):
    global numOfRuns, curRes, lastResult
    while ID < lastResult:
        try:
            numOfRuns += 1
            if numOfRuns % 10 == 0:
                time.sleep(3) # sleep every 10 iterations
            if isValid(ID + 8):
                parseHTML(curRes)
                checkNextID(ID + 8)
                return 0
            if isValid(ID + 18):
                parseHTML(curRes)
                checkNextID(ID + 18)
                return 0
            if isValid(ID + 7):
                parseHTML(curRes)
                checkNextID(ID + 7)
                return 0
            if isValid(ID + 17):
                parseHTML(curRes)
                checkNextID(ID + 17)
                return 0
            if isValid(ID+6):
                parseHTML(curRes)
                checkNextID(ID + 6)
                return 0
            if isValid(ID + 16):
                parseHTML(curRes)
                checkNextID(ID + 16)
                return 0
            else:
                checkNextID(ID + 1)
                return 0
        except Exception, e:
            print "somethin went wrong: " + str(e)

基本上它所做的是- checkNextID(ID)获取第一个包含数据减8的ID,因此第一次迭代将匹配第一个“if isValid”子句(isValid(ID + 8)将返回True)。 lastResult是保存最后已知的URL ID的变量,因此我们将运行直到numOfRuns为止。
isValid()是一个函数,它获取ID +其中一个减数并返回True,如果URL包含我需要的内容并将URL的soup对象保存到名为' curRes '的全局变量中,则返回True,如果URL不包含我需要的数据则返回False。 parseHTML是一个函数,它获取soup对象(curRes),解析我需要的数据,然后将数据保存到csv中,然后返回True。
如果isValid()返回True,我们将调用parseHTML(),然后尝试检查下一个ID +减数(通过调用checkNextID(ID + subtrahends),如果没有一个返回我要找的结果,我将增加1并再次检查,直到我找到下一个有效的URL。
您可以在这里看到其余代码
运行代码后,我得到了大约950个好的结果,突然出现了异常 -
"something went wrong:maximum recursion depth exceeded while calling a Python object"
我在WireShark上看到脚本卡在id-510009541(我用510000003启动了我的脚本),在我注意到错误并停止它之前,脚本尝试几次获取该ID的URL。
我非常兴奋地发现,与我的旧脚本相比,使用更少的HTTP请求,速度快25倍至40倍,非常精确,我错过了1000个好结果中的1个结果,这对我来说是可以接受的,运行5M次是不可能的,我的旧脚本运行了30小时,只得到了14-15K个结果,而我的新脚本在5-10分钟内给我提供了960个结果左右。
我读过关于堆栈限制的文章,但我正在尝试在Python中实现的算法必须有解决方案(我不能回到我的旧“算法”,它永远不会结束)。
谢谢!

4
每个递归算法都可以转化成等价的迭代算法,最简单的方法是在算法层面处理栈(例如,在深度优先遍历二叉树时将节点推入堆栈而不是对其进行递归),有时存在更简单(更自然)的迭代算法能够完成相同的任务。 - user395760
Thomas K,请原谅我,我还在学习如何使用stackoverflow,我会仔细查看所有得到的答案。 - YSY
6个回答

63

Python在递归方面的支持不够强,这是由于它缺少TRE(Tail Recursion Elimination)。

这意味着每次调用递归函数都会创建一个函数调用栈,由于栈深度有限(默认为1000),你可以通过sys.getrecursionlimit检查到(当然你也可以使用sys.setrecursionlimit更改它,但这并不推荐),当程序达到这个限制时会崩溃。

正如其他答案已经给出了更好的解决方案(即将递归替换为简单循环),如果你仍然想使用递归,则有另一种解决方案,即使用实现TRE的许多python方案之一,例如这个方案

N.B: 我的回答旨在让你更深入地了解为什么会出现错误,而我并不建议你使用TRE,因为如我已经解释的,在你的情况下,使用循环会更好和更易于阅读。


58
你可以通过以下方式增加堆栈的容量:
import sys
sys.setrecursionlimit(10000)

3
我有一台参数相当不错的27英寸iMac,但这导致出现了“总线错误:10”并且Python停止运行。 - Deepend
如果您无法控制递归部分,那么这是一个不错的解决方案。在这种情况下,您可以尝试将递归限制设置为更高的值。这对我很有效。 - evermean

19

这将递归转化为循环:

def checkNextID(ID):
    global numOfRuns, curRes, lastResult
    while ID < lastResult:
        try:
            numOfRuns += 1
            if numOfRuns % 10 == 0:
                time.sleep(3) # sleep every 10 iterations
            if isValid(ID + 8):
                parseHTML(curRes)
                ID = ID + 8
            elif isValid(ID + 18):
                parseHTML(curRes)
                ID = ID + 18
            elif isValid(ID + 7):
                parseHTML(curRes)
                ID = ID + 7
            elif isValid(ID + 17):
                parseHTML(curRes)
                ID = ID + 17
            elif isValid(ID+6):
                parseHTML(curRes)
                ID = ID + 6
            elif isValid(ID + 16):
                parseHTML(curRes)
                ID = ID + 16
            else:
                ID = ID + 1
        except Exception, e:
            print "somethin went wrong: " + str(e)

我认为还应该像在递归中那样调用 isValid(ID + 1),这样我也会检查 ID+1。 否则: 如果 isValid(ID + 1): 解析HTML(curRes) ID = ID + 1 - YSY
也许吧,但是你的代码中没有这个检查,所以我没有添加它。 - Dan D.
1
我所说的“check”是指isValid(ID+1),这在你的代码中并没有出现;而循环末尾的checkNextID(ID + 1)ID=ID+1; continue是一样的,但continue是多余的,所以我用ID = ID + 1代替了它。 - Dan D.

8

您可以增加递归深度和线程堆栈大小。

import sys, threading
sys.setrecursionlimit(10**7) # max depth of recursion
threading.stack_size(2**27)  # new thread will get stack of such size

2

与其使用递归,代码中的 checkNextID(ID + 18) 等部分可以替换为 ID+=18,然后如果删除所有的 return 0 实例,它应该会像一个简单的循环一样工作。然后在结尾处放置一个 return 0,并使您的变量非全局化。


-1

使用try和except,但不要在except中打印错误,只需在except语句中再次运行您的函数。


2
当前您的回答写得不够清晰,请[编辑]以添加更多细节,帮助其他人理解如何解决问题。您可以在帮助中心找到有关编写良好答案的更多信息。 - Community

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