尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Python递归、lambda与random:从原理到生产级协同实战

Python递归、lambda与random:从原理到生产级协同实战 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, dataNone): 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在n500时已超限: {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, n1): 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 RossumPython之父在2009年博客中直言“TCO会破坏栈追踪stack trace让调试变得地狱般困难。”这意味着即使你写出尾递归形式def factorial_tail(n, acc1): 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_depth1而非在函数内自增 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的引用而非创建时的值。循环结束时i4所有lambda都指向这个最终值。修复方案不是“用ii默认参数”而是理解闭包变量在lambda定义时未求值执行时才求值。# 正确方案1用默认参数固化值 funcs [] for i in range(5): funcs.append(lambda xi: x) # xi在定义时求值并绑定 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 xi: x中i在lambda定义时即for循环的每次迭代中被求值其值作为默认参数x的初始值固化。这是Python唯一的“立即求值并绑定”机制。3.2 lambda vs def何时必须用lambdalambda的核心优势是无状态、无副作用、纯表达式。它只能包含表达式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(fCalling {func.__name__} with {args}, {kwargs}) result func(*args, **kwargs) print(f{func.__name__} returned {result}) return result return wrapper def with_retry(max_attempts3, delay1): 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_attempts2)(lambda url: fMock 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, seed12345): self.state seed def next_int(self, min_val0, max_val100): # 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(seed42) 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) # 因为seed12345choice总是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(length16): 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(length16): 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_delayminbase_delay-jitter_range, maxbase_delayjitter_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(fRequesting {url} after {delay:.3f}s jitter...) time.sleep(delay) # 这里放真实的requests.get()调用 return fMock response for {url} # 使用示例 jitter RequestJitter(base_delay2.0, jitter_range0.8) for i in range(5): jitter.wait_and_request(fhttps://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(fJSON嵌套深度超限 ({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_depth20) urls parser.parse_urls( sample_api_response, url_keyurl, children_keyrelated ) 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_workersself.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, fERROR: {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 fResponse from {url} # 集成使用 if __name__ __main__: # 1. 解析URL parser SafeJSONParser(max_depth20) 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_delay1.5
返回列表