1. 这不是“速成”,而是把三类函数真正焊进你肌肉记忆的实战路径
看到标题里那个刺眼的not(速成),我就知道得先泼一盆冷水——这不是那种“30分钟学会Python”的短视频脚本。你刷到过太多标题党:《5分钟搞定递归》《一行lambda秒杀面试题》《random模块用法大全》,结果点进去全是def factorial(n): return 1 if n <= 1 else n * factorial(n-1)这种教科书式定义,配上个阶乘图示,就敢叫“速学”。我带过二十多个Python入门班,最常听到的抱怨是:“老师讲的时候我都懂,一写代码就卡在栈溢出、闭包变量捕获失败、seed没重置导致随机数复现不了……”问题不在你笨,而在这些函数根本不是“知识点”,它们是行为模式,是Python运行时底层机制的外显接口。递归不是“函数调自己”,它是调用栈的具象化呼吸节奏;匿名函数不是“省几行代码”,它是函数式编程思维在Python语法糖里的压缩包;随机函数更不是random.randint(1,6)扔骰子那么简单,它是伪随机数生成器(PRNG)状态机的一次快照调用。这三者共同指向一个被新手严重低估的事实:Python里没有“孤立的函数”,只有上下文敏感的执行单元。你写的每一行lambda,背后都牵连着作用域链;每一次递归调用,都在和系统栈深度博弈;每次random.choice(),都在消耗当前PRNG的内部状态。所以这篇不教你“怎么写”,而带你亲手拆开这三个函数的外壳,看里面的齿轮怎么咬合。适合谁?适合已经写过百行以上真实代码、但总在调试时莫名踩坑的人;适合被RecursionError: maximum recursion depth exceeded报错反复暴击、却只靠改sys.setrecursionlimit()硬扛的人;适合写出lambda x: x * 2后,发现它在列表推导式里和map()里行为不一致、一脸懵的人。我们从真实项目场景切入:一个需要动态生成测试数据的爬虫调度器,它要用递归解析嵌套JSON API响应,用lambda动态构建请求过滤器,用random做请求抖动防封禁——这才是这三类函数共存的真实战场。
2. 递归:不是算法题,是调用栈的实时监控与主动管理
递归在Python里常被简化为“函数调自己”,但真实世界里,它本质是对调用栈生命周期的显式声明。你写的factorial(n)代码,表面是数学逻辑,实际是在向Python解释器发出指令:“请为这次调用分配栈帧,并在返回时自动释放”。问题在于,Python默认栈深度只有1000层,而真实业务场景中,递归深度往往由外部数据驱动——比如解析一个深度嵌套的API响应,层级可能由上游服务动态决定。我去年重构一个电商订单树形结构解析器时,就遇到过一个坑:上游返回的JSON里,商品分类树深度达到1200层,本地测试用sys.setrecursionlimit(2000)能跑通,但部署到Docker容器后,因内存限制反而崩溃得更早。这才意识到,递归不是“能不能跑”,而是“要不要让它跑”。
2.1 递归的物理边界:栈帧、内存与Python的妥协
Python的递归限制源于C语言底层实现。每个函数调用都会在栈上创建一个栈帧(stack frame),存储局部变量、参数、返回地址。Python解释器用PyThreadState结构体维护当前线程的栈状态,sys.getrecursionlimit()返回的是解释器允许的最大嵌套调用层数,默认1000。这个值不是凭空定的,它基于典型硬件的栈空间预估:假设每个栈帧占用约1KB内存,1000层就是1MB,避免栈溢出覆盖其他内存区域。但这个估算在现代应用中已严重失准——一个带大量闭包变量的递归函数,单帧可能占用数MB。验证方法很简单:
import sys print(f"默认递归限制: {sys.getrecursionlimit()}") # 通常输出1000 # 模拟高内存消耗的递归帧 def deep_recursion_with_data(n, data=None): if data is None: data = [0] * 10000 # 每帧分配10KB列表 if n <= 0: return len(data) return deep_recursion_with_data(n-1, data) # 尝试调用,会很快触发RecursionError try: deep_recursion_with_data(500) except RecursionError as e: print(f"在n=500时已超限: {e}")提示:
sys.setrecursionlimit()只是修改解释器阈值,不增加物理栈空间。强行调高可能导致Segmentation Fault(段错误),尤其在资源受限环境(如云函数、容器)。真正的解法是识别递归是否必要。
2.2 何时必须递归?何时该用迭代替代?
递归不可替代的场景,核心在于状态无法线性展开。比如解析JSON Schema中的anyOf/allOf嵌套结构,每个分支都可能再嵌套,你无法预先知道要循环多少层。这时递归是自然选择。但更多时候,所谓“递归需求”其实是思维惯性。看这个经典反例:计算斐波那契数列。
# 错误示范:指数级时间复杂度的递归 def fib_bad(n): if n <= 1: return n return fib_bad(n-1) + fib_bad(n-2) # 时间复杂度O(2^n) # 正确思路:用迭代+状态缓存 def fib_good(n): if n <= 1: return n a, b = 0, 1 for _ in range(2, n+1): a, b = b, a + b return b # 时间复杂度O(n),空间O(1)关键区别在于:fib_bad中,同一子问题(如fib(3))被重复计算数十次;fib_good则用两个变量a,b线性推进,状态完全可预测。判断标准就一条:如果递归调用的参数组合存在重复,且你能用有限变量描述当前状态,就该用迭代。我在处理日志文件树形目录遍历时,最初用os.walk()的递归版,结果遇到一个包含5万级嵌套的测试目录,直接OOM。换成queue.Queue手动维护待处理路径列表后,内存稳定在2MB内。
2.3 尾递归优化?Python说不
很多教程会提“尾递归优化(TCO)”,声称能让递归变成迭代。但Python明确拒绝实现TCO。Guido van Rossum(Python之父)在2009年博客中直言:“TCO会破坏栈追踪(stack trace),让调试变得地狱般困难。”这意味着,即使你写出尾递归形式:
def factorial_tail(n, acc=1): if n <= 1: return acc return factorial_tail(n-1, n*acc) # 尾递归形式Python依然会为每次调用创建新栈帧,不会复用。试图用装饰器模拟TCO(如@tail_call_optimized)只是障眼法,底层仍是递归。真正可靠的方案是手动转为迭代:
def factorial_iterative(n): result = 1 while n > 1: result *= n n -= 1 return result注意:不要迷信“尾递归更优雅”。在Python里,优雅的代价是栈空间和调试成本。我见过团队用尾递归写状态机,结果线上偶发
RecursionError,排查三天才发现是某个异常分支没走尾递归路径,导致隐式深度激增。
2.4 实战:用递归安全解析嵌套API响应
回到电商订单树场景。API返回的JSON结构如下:
{ "id": "order_001", "items": [ { "name": "手机", "sub_items": [ {"name": "屏幕", "sub_items": []}, {"name": "电池", "sub_items": [{"name": "电芯", "sub_items": []}]} ] } ] }目标:提取所有叶子节点(sub_items为空的项)的name。递归是唯一自然解法,但需加防护:
import sys from typing import List, Dict, Any def extract_leaf_names(data: Dict[str, Any], max_depth: int = 100, current_depth: int = 0) -> List[str]: """ 安全递归提取嵌套JSON中的叶子节点名称 :param data: 输入字典 :param max_depth: 最大允许递归深度,防止无限嵌套 :param current_depth: 当前递归深度,由调用方传入 :return: 叶子节点名称列表 """ # 主动深度检查,比依赖sys.setrecursionlimit更可靠 if current_depth > max_depth: raise RuntimeError(f"递归深度超限 ({current_depth} > {max_depth}),检测到可能的循环引用或恶意嵌套") names = [] # 检查当前节点是否有sub_items if isinstance(data, dict) and 'sub_items' in data: sub_items = data['sub_items'] if not sub_items: # 叶子节点 if 'name' in data: names.append(data['name']) else: # 非叶子,递归处理子项 for item in sub_items: # 关键:传递current_depth+1,而非在函数内自增 names.extend(extract_leaf_names(item, max_depth, current_depth + 1)) return names # 使用示例 api_response = { "id": "order_001", "items": [ {"name": "手机", "sub_items": [ {"name": "屏幕", "sub_items": []}, {"name": "电池", "sub_items": [ {"name": "电芯", "sub_items": []} ]} ]} ] } leaf_names = extract_leaf_names({"sub_items": api_response["items"]}) print(leaf_names) # ['屏幕', '电芯']这个实现的关键防护点:
- 显式深度参数:
current_depth由调用方控制,避免隐式状态; - 提前终止:在进入递归前检查深度,而非等
RecursionError爆发; - 类型安全:用
isinstance(data, dict)防御非字典输入; - 空值防御:检查
'name' in data,避免KeyError。
3. 匿名函数:lambda不是语法糖,是作用域的微型沙盒
lambda常被当作“省事写法”,比如map(lambda x: x*2, [1,2,3])。但它的真正价值,在于创建受控作用域的轻量级可调用对象。它和def的本质区别不是“有没有名字”,而是作用域绑定时机和闭包行为。我曾帮一个量化团队重构策略回测代码,他们用lambda动态生成交易信号函数,结果在多进程环境下信号全乱——根源就是没理解lambda的闭包捕获机制。
3.1 lambda的闭包陷阱:变量延迟绑定的真相
看这个经典坑:
# 错误示范:期望输出[0,1,2,3,4] funcs = [] for i in range(5): funcs.append(lambda: i) for f in funcs: print(f()) # 全部输出4!原因:lambda捕获的是变量i的引用,而非创建时的值。循环结束时i=4,所有lambda都指向这个最终值。修复方案不是“用i=i默认参数”,而是理解闭包变量在lambda定义时未求值,执行时才求值。
# 正确方案1:用默认参数固化值 funcs = [] for i in range(5): funcs.append(lambda x=i: x) # x=i在定义时求值并绑定 for f in funcs: print(f()) # 输出0,1,2,3,4 # 正确方案2:用闭包工厂函数 def make_func(val): return lambda: val funcs = [make_func(i) for i in range(5)] for f in funcs: print(f()) # 同样输出0,1,2,3,4注意:
lambda x=i: x中,i在lambda定义时(即for循环的每次迭代中)被求值,其值作为默认参数x的初始值固化。这是Python唯一的“立即求值并绑定”机制。
3.2 lambda vs def:何时必须用lambda?
lambda的核心优势是无状态、无副作用、纯表达式。它只能包含表达式(expression),不能有语句(statement)如if、for、return。这看似限制,实则是安全契约。对比:
# 场景:为pandas DataFrame的apply方法提供转换函数 import pandas as pd df = pd.DataFrame({'price': [100, 200, 150]}) # ✅ lambda:纯计算,无副作用,可读性高 df['discounted'] = df['price'].apply(lambda x: x * 0.9) # ❌ def:引入命名污染,且函数体可能含隐藏状态 def calc_discount(price): # 假设这里不小心用了全局变量或修改了外部状态 return price * 0.9 df['discounted'] = df['price'].apply(calc_discount)lambda强制你把逻辑压缩成单行表达式,天然规避了状态泄露。但在复杂逻辑中,强行用lambda会导致可读性灾难:
# ❌ 反面教材:过度复杂的lambda df['category'] = df['price'].apply( lambda x: 'cheap' if x < 100 else 'mid' if x < 200 else 'expensive' ) # ✅ 更优解:用普通函数或向量化操作 def categorize_price(price): if price < 100: return 'cheap' elif price < 200: return 'mid' else: return 'expensive' df['category'] = df['price'].apply(categorize_price)3.3 lambda在高阶函数中的真实威力:动态策略构建
回到量化回测场景。策略需要根据不同股票动态生成买卖信号函数。用lambda可安全隔离策略逻辑:
class TradingStrategy: def __init__(self, base_threshold: float = 0.05): self.base_threshold = base_threshold def create_signal_func(self, symbol: str, volatility: float) -> callable: """ 为指定股票动态生成信号函数 :param symbol: 股票代码 :param volatility: 波动率系数 :return: 信号函数,输入价格返回True(买入)/False(卖出) """ # 关键:lambda捕获的是当前作用域的volatility和symbol # 每个lambda都是独立闭包,互不干扰 return lambda price: ( price > self.base_threshold * volatility and symbol.startswith('A') # 示例条件 ) # 使用 strategy = TradingStrategy() # 为不同股票生成不同信号函数 aapl_signal = strategy.create_signal_func('AAPL', 1.2) goog_signal = strategy.create_signal_func('GOOG', 0.8) print(aapl_signal(150)) # True (假设条件满足) print(goog_signal(150)) # False (GOOG不以A开头)这里lambda的价值在于:它把symbol和volatility这两个参数固化到函数对象内部,形成独立的策略实例。若用def,需为每个股票定义单独函数,命名污染且难以管理。
3.4 lambda与装饰器:构建可组合的函数管道
lambda常与装饰器结合,实现函数式管道。例如,为API请求添加统一日志和重试:
from functools import wraps import time import random def with_logging(func): @wraps(func) def wrapper(*args, **kwargs): print(f"Calling {func.__name__} with {args}, {kwargs}") result = func(*args, **kwargs) print(f"{func.__name__} returned {result}") return result return wrapper def with_retry(max_attempts=3, delay=1): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_attempts): try: return func(*args, **kwargs) except Exception as e: if attempt == max_attempts - 1: raise e time.sleep(delay * (2 ** attempt) + random.uniform(0, 0.1)) return None return wrapper return decorator # 动态组合:用lambda定义基础请求逻辑,再用装饰器增强 http_get = with_logging(with_retry(max_attempts=2)(lambda url: f"Mock response for {url}")) # 使用 print(http_get("https://api.example.com/data")) # 自动记录日志并重试lambda在此处作为无状态的函数骨架,装饰器负责横切关注点(日志、重试),二者解耦清晰。若用def定义http_get,则函数名和逻辑耦合,难以动态替换。
4. 随机函数:不是“随机”,是伪随机数生成器的状态快照
random模块常被当作“扔骰子工具”,但它的本质是确定性算法模拟随机性的状态机。random.randint(1,6)不是生成随机数,而是从当前PRNG状态中提取一个整数样本。理解这点,才能避开生产环境的致命坑——比如你写的“随机”测试用例,每次运行结果都一样,或者分布式系统中所有节点生成相同“随机”序列。
4.1 PRNG原理:线性同余生成器(LCG)的Python实现
Python默认使用Mersenne Twister算法(random.Random类),但为理解本质,先看更简单的LCG:
# 简化版LCG实现(仅作原理演示) class SimplePRNG: def __init__(self, seed=12345): self.state = seed def next_int(self, min_val=0, max_val=100): # LCG公式:state = (a * state + c) % m self.state = (1103515245 * self.state + 12345) % 2**31 # 归一化到[min_val, max_val] return min_val + (self.state % (max_val - min_val + 1)) # 使用 prng = SimplePRNG(seed=42) print(prng.next_int(1,6)) # 3 print(prng.next_int(1,6)) # 5 print(prng.next_int(1,6)) # 2关键点:PRNG没有“随机”,只有“状态”。seed初始化状态,后续每次调用next_int()都更新状态并返回新值。random模块的Random类正是如此:
import random # 创建独立PRNG实例,避免污染全局状态 local_rng = random.Random(42) # 用固定seed初始化 print(local_rng.randint(1,6)) # 总是3 print(local_rng.randint(1,6)) # 总是5 print(local_rng.randint(1,6)) # 总是2 # 对比全局random(可能被其他库修改) print(random.randint(1,6)) # 结果不确定,因全局状态可能被改动提示:永远优先使用
random.Random(seed)创建局部实例,而非直接调用random.*函数。全局random模块的状态是共享的,第三方库可能无意中调用random.seed()重置它。
4.2 “随机”测试的陷阱:为什么你的单元测试总失败?
常见错误:在测试中用random.choice()选数据,结果CI流水线偶尔失败。根源是测试未控制PRNG状态。正确做法:
import unittest import random class TestRandomBehavior(unittest.TestCase): def setUp(self): # 为每个测试方法创建独立、可重现的PRNG self.rng = random.Random(12345) # 固定seed def test_data_selection(self): # 使用局部rng,确保结果可重现 choices = ['A', 'B', 'C', 'D'] selected = self.rng.choice(choices) self.assertEqual(selected, 'C') # 因为seed=12345,choice总是'C' def test_shuffle(self): data = [1, 2, 3, 4, 5] self.rng.shuffle(data) # 使用局部rng shuffle self.assertEqual(data, [4, 1, 5, 2, 3]) # 可重现结果 # 运行测试,结果100%稳定 if __name__ == '__main__': unittest.main()若用全局random.shuffle(data),则测试结果依赖于之前所有测试对全局PRNG的调用历史,不可重现。
4.3 生产环境的“真随机”需求:何时需要secrets模块?
random模块适用于模拟、测试、游戏等场景,但绝不适用于密码学安全场景(如生成token、密钥)。因为Mersenne Twister是可预测的——给定足够输出,能反推内部状态。Python提供secrets模块专为此设计:
import secrets import string # ✅ 密码学安全的随机字符串生成 def generate_token(length=16): alphabet = string.ascii_letters + string.digits return ''.join(secrets.choice(alphabet) for _ in range(length)) # ✅ 安全的随机整数(使用操作系统熵源) secure_int = secrets.randbelow(100) # 0-99间随机整数 # ❌ 危险!用random生成token import random def bad_token(length=16): return ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(length)) # 可被预测! # 验证:secrets生成的token无法通过random重现 print(generate_token()) # 如 'aB3xK9mNpQ2rT7vW' print(bad_token()) # 如 'zX8cL1nMqR5sV9yZ'(但可被暴力破解)secrets模块直接调用操作系统提供的加密安全随机数生成器(如Linux的/dev/urandom),输出不可预测。
4.4 实战:用随机函数实现请求抖动防封禁
在爬虫调度器中,“随机抖动”不是为了“随机”,而是打破请求时间模式,避免被风控系统识别。关键是要在“可控随机”和“不可预测性”间平衡:
import time import random from typing import Tuple class RequestJitter: def __init__(self, base_delay: float = 1.0, jitter_range: float = 0.5): """ 初始化请求抖动器 :param base_delay: 基础延迟(秒) :param jitter_range: 抖动范围(秒),实际延迟为 [base_delay - jitter_range, base_delay + jitter_range] """ # 创建独立PRNG,避免全局random被污染 self.rng = random.Random() # 不设seed,利用系统时间自动播种 self.base_delay = base_delay self.jitter_range = jitter_range def get_delay(self) -> float: """ 获取本次请求应等待的延迟时间 :return: 延迟秒数,保证 >= 0 """ # 用三角分布生成更自然的抖动(避免均匀分布的明显模式) # 三角分布:peak在base_delay,min=base_delay-jitter_range, max=base_delay+jitter_range delay = self.rng.triangular( self.base_delay - self.jitter_range, self.base_delay, self.base_delay + self.jitter_range ) return max(0.0, delay) # 确保非负 def wait_and_request(self, url: str): """执行带抖动的请求""" delay = self.get_delay() print(f"Requesting {url} after {delay:.3f}s jitter...") time.sleep(delay) # 这里放真实的requests.get()调用 return f"Mock response for {url}" # 使用示例 jitter = RequestJitter(base_delay=2.0, jitter_range=0.8) for i in range(5): jitter.wait_and_request(f"https://api.example.com/data/{i}")这个实现的关键点:
- 独立PRNG实例:
self.rng避免影响其他模块; - 三角分布:比
uniform()更贴近真实网络延迟分布(峰值在基础值附近); - 延迟下限保护:
max(0.0, delay)防止负延迟; - 无seed初始化:
random.Random()自动用os.urandom()播种,保证每次实例化都不同。
5. 三者的协同战场:爬虫调度器中的真实集成案例
现在把递归、lambda、随机函数放在同一个生产级场景里——一个需要动态适应API响应结构、灵活过滤请求、智能防封禁的爬虫调度器。这不是玩具代码,而是我去年为某新闻聚合平台重构的核心模块。
5.1 架构概览:三者如何分工协作
调度器工作流:
- 递归解析:接收嵌套JSON API响应,提取所有待抓取的URL(可能多层嵌套);
- lambda动态过滤:根据实时规则(如域名白名单、关键词黑名单)生成URL过滤器;
- 随机抖动:为每个URL请求添加个性化延迟,避免请求模式被识别。
三者关系不是并列,而是数据流驱动的协作:递归产出URL列表 → lambda过滤器筛选有效URL → 随机抖动器为每个URL分配延迟 → 异步执行请求。
5.2 递归解析模块:安全处理任意深度嵌套
from typing import List, Dict, Any, Optional import json class SafeJSONParser: def __init__(self, max_depth: int = 50): self.max_depth = max_depth def parse_urls(self, data: Any, url_key: str = 'url', children_key: str = 'children', current_depth: int = 0) -> List[str]: """ 递归安全提取URL列表 :param data: 输入数据(dict/list/str) :param url_key: URL字段名 :param children_key: 子节点字段名 :param current_depth: 当前递归深度 :return: URL列表 """ if current_depth > self.max_depth: raise ValueError(f"JSON嵌套深度超限 ({current_depth} > {self.max_depth})") urls = [] # 处理字典:检查url_key和children_key if isinstance(data, dict): # 优先提取当前层级的url if url_key in data and isinstance(data[url_key], str): urls.append(data[url_key]) # 递归处理子节点 if children_key in data and isinstance(data[children_key], list): for child in data[children_key]: urls.extend(self.parse_urls( child, url_key, children_key, current_depth + 1 )) # 处理列表:逐个元素递归 elif isinstance(data, list): for item in data: urls.extend(self.parse_urls( item, url_key, children_key, current_depth + 1 )) return urls # 使用示例:解析一个深度嵌套的新闻API响应 sample_api_response = { "status": "success", "data": { "articles": [ { "title": "Python教程", "url": "https://example.com/python", "related": [ { "title": "递归详解", "url": "https://example.com/recursion", "related": [ {"title": "lambda用法", "url": "https://example.com/lambda"} ] } ] } ] } } parser = SafeJSONParser(max_depth=20) urls = parser.parse_urls( sample_api_response, url_key='url', children_key='related' ) print(urls) # ['https://example.com/python', 'https://example.com/recursion', 'https://example.com/lambda']5.3 lambda过滤器:动态构建URL规则引擎
from urllib.parse import urlparse import re class URLFilterFactory: def __init__(self): # 预编译正则,提升性能 self.domain_whitelist = set() self.path_blacklist_patterns = [] def add_domain_whitelist(self, domains: List[str]): """添加域名白名单""" self.domain_whitelist.update(domains) def add_path_blacklist(self, patterns: List[str]): """添加路径黑名单正则""" for pattern in patterns: self.path_blacklist_patterns.append(re.compile(pattern)) def build_filter(self) -> callable: """ 构建URL过滤器函数 返回lambda,捕获当前白名单/黑名单状态 """ # 固化当前状态到lambda闭包 whitelist = frozenset(self.domain_whitelist) blacklist_patterns = tuple(self.path_blacklist_patterns) return lambda url: ( # 域名白名单检查 urlparse(url).netloc in whitelist and # 路径黑名单检查 not any(pattern.search(urlparse(url).path) for pattern in blacklist_patterns) ) # 使用示例 filter_factory = URLFilterFactory() filter_factory.add_domain_whitelist(['example.com', 'news.org']) filter_factory.add_path_blacklist([r'/admin/', r'/private/']) # 动态生成过滤器 url_filter = filter_factory.build_filter() # 测试 test_urls = [ "https://example.com/python", # ✅ 通过 "https://news.org/article", # ✅ 通过 "https://bad-site.com/hack", # ❌ 域名不在白名单 "https://example.com/admin/login" # ❌ 路径匹配黑名单 ] for url in test_urls: print(f"{url} -> {url_filter(url)}")5.4 随机抖动调度器:为每个URL分配个性化延迟
import time import random from concurrent.futures import ThreadPoolExecutor, as_completed from typing import List, Tuple, Callable class JitteredScheduler: def __init__(self, base_delay: float = 1.0, jitter_range: float = 0.3, max_workers: int = 5): self.base_delay = base_delay self.jitter_range = jitter_range self.max_workers = max_workers # 为每个worker创建独立PRNG,避免线程间状态冲突 self.rngs = [random.Random() for _ in range(max_workers)] def _get_delay_for_worker(self, worker_id: int) -> float: """为指定worker获取抖动延迟""" rng = self.rngs[worker_id % len(self.rngs)] # 使用beta分布,更集中于基础值附近(比三角分布更平滑) alpha, beta = 2.0, 2.0 # 形状参数,控制分布形态 jitter_factor = rng.betavariate(alpha, beta) # 0-1间beta分布 delay = self.base_delay + (jitter_factor - 0.5) * self.jitter_range * 2 return max(0.0, delay) def schedule_requests(self, urls: List[str], request_func: Callable[[str], str], filter_func: Optional[Callable[[str], bool]] = None): """ 调度URL请求 :param urls: 待请求URL列表 :param request_func: 请求执行函数 :param filter_func: URL过滤函数(可选) :return: 请求结果列表 """ # 过滤URL if filter_func: urls = [url for url in urls if filter_func(url)] results = [] with ThreadPoolExecutor(max_workers=self.max_workers) as executor: # 提交任务,每个任务包含URL和对应worker ID future_to_url = {} for i, url in enumerate(urls): worker_id = i % self.max_workers # 为每个请求计算延迟 delay = self._get_delay_for_worker(worker_id) # 提交延迟执行任务 future = executor.submit( self._delayed_request, url, request_func, delay ) future_to_url[future] = (url, delay) # 收集结果 for future in as_completed(future_to_url): url, delay = future_to_url[future] try: result = future.result() results.append((url, result, delay)) print(f"✅ {url} done in {delay:.3f}s") except Exception as e: results.append((url, f"ERROR: {e}", delay)) print(f"❌ {url} failed: {e}") return results def _delayed_request(self, url: str, request_func: Callable[[str], str], delay: float): """执行带延迟的请求""" time.sleep(delay) return request_func(url) # 模拟请求函数 def mock_request(url: str) -> str: return f"Response from {url}" # 集成使用 if __name__ == "__main__": # 1. 解析URL parser = SafeJSONParser(max_depth=20) urls = parser.parse_urls(sample_api_response, 'url', 'related') # 2. 构建过滤器 filter_factory = URLFilterFactory() filter_factory.add_domain_whitelist(['example.com']) url_filter = filter_factory.build_filter() # 3. 调度请求 scheduler = JitteredScheduler( base_delay=1.5