dfs_crawl函数易崩溃,主因是未处理网络异常、URL未归一化、缺乏深度控制及递归栈溢出;应改用显式栈结构,存(url, depth)元组,入栈前校验深度并归一化URL。

dfs_crawl 函数能跑通,但实际用起来大概率会崩——不是因为逻辑错,而是没处理网络异常、URL归一化、深度控制失效、递归栈溢出这四类硬伤。
递归实现 dfs_crawl 为什么容易卡死或崩溃
直接套用教科书式递归写法,比如每次拿到链接就 dfs_crawl(link, visited),问题立刻暴露:
- 没有
try/except包裹requests.get(),遇到超时、403、DNS失败就会中断整个爬取 - 相对路径 URL(如
/news/123、../about)没做urllib.parse.urljoin()归一化,直接拼进递归会导致 404 或跳站 - 深度参数只是传参,没在函数入口做
if depth > max_depth: return判断,子链接照样往下钻 - Python 默认递归限制约 1000 层,真实网站链路深一点(比如 5 层嵌套 + 每层 20 个链接),很快触发
RecursionError
用栈替代递归,手动控深度和去重
把递归改成显式栈(list 或 collections.deque),就能稳住调用栈、自由插检查点:
- 每个栈元素存
(url, depth)元组,入栈前先判断depth - 用
set存已访问 URL,但注意:必须对归一化后的 URL 去重,否则https://a.com/和https://a.com/index.html被当成两个 - 解析出的链接先用
urljoin(base_url, href)转成绝对地址,再过滤掉mailto:、javascript:、#锚点等无效协议 - 加个简单节流:每次请求后
time.sleep(0.1),避免被目标站封 IP
from urllib.parse import urljoin
import requests
from bs4 import BeautifulSoup
<p>def dfs_crawl_stack(start_url, max_depth=3):
visited = set()
stack = [(start_url, 0)]</p><pre class='brush:python;toolbar:false;'>while stack:
url, depth = stack.pop()
if depth >= max_depth or url in visited:
continue
visited.add(url)
print(f"[{depth}] {url}")
try:
resp = requests.get(url, timeout=5)
if resp.status_code != 200:
continue
soup = BeautifulSoup(resp.text, 'html.parser')
for a in soup.find_all('a', href=True):
next_url = urljoin(url, a['href'])
# 过滤明显无效链接
if next_url.startswith(('http', 'https')) and '#' not in next_url:
stack.append((next_url, depth + 1))
except (requests.RequestException, UnicodeDecodeError):
pass
BeautifulSoup 解析时常见的链接漏抓场景
只靠 soup.find_all('a', href=True) 会漏掉大量真实可爬链接:
立即学习“Python免费学习笔记(深入)”;
- 页面用
<link rel="next">做分页,这类导航链接不会被<a></a>捕获 - JS 渲染的链接(比如 React/Vue 路由)根本不在初始 HTML 里,
requests + BeautifulSoup直接看不见 - 有些站点把关键链接藏在
<script></script>标签里,用 JS 动态拼接,需正则提取或换execjs/playwright -
href值可能是空字符串、"#"、或带空格的脏数据,不清洗就直接urljoin可能生成非法 URL
深度控制不是“层数”而是“可达性”
所谓“爬 3 层”,不等于从首页点 3 次链接。真实场景中:
- 同一域名下,
https://a.com/page1→https://a.com/page2→https://a.com/page3是合理深度链 - 但
https://a.com/→https://b.com/→https://c.com/这种跨域跳转,即使 depth=2,也大概率该被截断——否则就是全网乱爬 - 建议加域名白名单:初始化时记录
urllib.parse.urlparse(start_url).netloc,后续只保留同域链接 - 如果真要跨域,应显式配置
allowed_domains = ['a.com', 'cdn.a.com'],而不是靠 depth 参数糊弄
深度优先不是写个递归就叫“实现了”,真正落地时,URL 归一化、异常兜底、栈管理、域名约束这四件事没做扎实,跑不出三个页面就会停。别省那几行 try 和 urljoin。


















