【爬虫框架-3】闭包的用法

接上次,但是回顾一下闭包,为什么呢,这个平时基本见不到,但是多次尝试,这个解法结合实例方法却是一次通过。

当然走类静态方法也可以,需要重新构造一下类。这也是我的第一版做法,把类相关的信息传过去,消费的时候冲洗构造实例。 同时还要维护一个实例缓存,避免每次都重新从meta参数转实例。

一、回顾一下闭包

1.数据隐藏与状态保持(轻量级的类)

如果你需要记录一些状态(比如计数),通常会写一个类。但如果逻辑很简单,用类有点"杀鸡用牛刀",闭包是更好的选择。

场景:统计代码运行了多少次。

code****Python

plain 复制代码
def make_counter():
    count = 0  # 这是被"记忆"的变量
    
    def adder():
        nonlocal count # 关键!告诉 Python 我们要修改外部变量,而不是定义新的
        count += 1
        return count
    
    return adder

# 实例化一个计数器
c1 = make_counter()

print(c1())  # 输出 1
print(c1())  # 输出 2
print(c1())  # 输出 3

# 实例化另一个,互不干扰
c2 = make_counter()
print(c2())  # 输出 1

对比用类(Class)的写法:

code****Python

plain 复制代码
class Counter:
    def __init__(self):
        self.count = 0
    def __call__(self):
        self.count += 1
        return self.count

# 闭包写法比类写法更简洁,且不需要理解 self
2.延迟执行(Lazy Evaluation)

有时候我们定义了逻辑,但不想立刻执行,而是想把逻辑和参数打包好,等需要的时候再执行。

这在你的 spider.py 原始代码中体现得很明显:

code****Python

plain 复制代码
# 原始代码逻辑简化版
def get_task(url):
    print("准备任务...")
    
    def task():
        print(f"正在下载: {url}") # url 被打包在里面了
    
    return task

# 1. 此时只生成了任务,没有下载
t = get_task("www.baidu.com") 

print("干点别的...")

# 2. 需要的时候才执行
t()
3.闭包里的坑:nonlocal 关键字

在闭包中,读取 外部变量是没问题的(如例1),但如果要修改外部变量(如例2的计数器),必须使用 nonlocal。

错误示范:

code****Python

plain 复制代码
def outer():
    x = 10
    def inner():
        x = 20 # 这里的 x 被 Python 认为是 inner 内部定义的新局部变量,而不是修改外部的 x
        print("内部:", x)
    inner()
    print("外部:", x)

outer()
# 输出:
# 内部: 20
# 外部: 10  <-- 外部的 x 并没有被改变!

正确示范:

code****Python

plain 复制代码
def outer():
    x = 10
    def inner():
        nonlocal x # 声明:我要操作的是外面那层函数的 x
        x = 20
    inner()
    print("外部:", x)

outer()
# 输出:
# 外部: 20 <-- 成功修改
4.闭包的终极形态:装饰器(Decorator)

你经常看到的 @boost,@staticmethod,其实本质上就是闭包!

装饰器就是:接受一个函数,返回一个闭包。

让我们手写一个最简单的装饰器,用来计算函数运行时间:

code****Python

plain 复制代码
import time

# 这是一个装饰器函数
def timer(func):
    # func 是被装饰的函数(比如下面的 work)
    
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs) # 执行真正的函数
        end = time.time()
        print(f"函数 {func.__name__} 耗时: {end - start} 秒")
        return result
    
    return wrapper # 返回闭包

# 使用装饰器
@timer
def work(n):
    time.sleep(n)
    return "完成"

# 等价于:work = timer(work)

print(work(1)) 
# 输出:
# 函数 work 耗时: 1.00xxxxx 秒
# 完成

二、举个栗子

plain 复制代码
def _get_crawl_consumer_function(self):
    # 外部环境:self (Engine实例) 就在这里
    # self._worker 也是在这里通过 self 访问到的
    
    def consumer_function(url, ...):
        # 闭包内部:
        # 虽然这函数由 funboost 在根本不知道 Engine 存在的角落里调用
        # 但它依然"记得" self,所以能调用 self._worker.process_crawl
        return self._worker.process_crawl(...)
        
    return consumer_function
plain 复制代码
这里的参数是怎么来的?
生成阶段:
当你调用 _get_crawl_consumer_function() 时,它不接收 url 这些参数。它只是生产并返回了 consumer_function 这个函数对象。这个函数对象"只要"被调用,就需要传入 url 等参数。
调用阶段(由框架触发):
funboost 框架在后台监听 Redis 消息队列。当 Redis 里来了一条消息(比如 {"url": "baidu.com", "meta": {}})时,框架内部会这样调用你生成的函数:
code
Python
# 假设 msg_dict 是从 Redis 取出的字典
# funboost 内部伪代码:

# 1. 拿到你之前返回的那个闭包函数
func = engine._get_crawl_consumer_function() 

# 2. 这里的 func 就是 consumer_function
# 3. 框架把消息里的字段拆开传进去
func(url="baidu.com", callback_name="parse", ...)

好了 ,基本就能这么解决了

更多文章,敬请关注gzh: 零基础爬虫第一天~

next

相关推荐
齐齐大魔王9 小时前
python爬虫学习进程(四)
爬虫·python·学习
毕设源码-钟学长10 小时前
【开题答辩全过程】以 基于Python爬虫的二手房信息爬取及分析为例,包含答辩的问题和答案
开发语言·爬虫·python
Glommer12 小时前
Akamai 逆向思路
javascript·爬虫·逆向
知识浅谈12 小时前
传统爬虫太耗时?AI一键生成企业级爬虫架构
人工智能·爬虫
工业互联网专业14 小时前
基于爬虫的个性化书籍推荐系统_flask+spider
爬虫·python·flask·毕业设计·源码·课程设计
sugar椰子皮14 小时前
【爬虫框架-4】统计的用法
爬虫
想个名字太难1 天前
网络爬虫入门程序
java·爬虫·maven
Data_agent1 天前
1688按图搜索1688商品(拍立淘)API ,Python请求示例
爬虫·python·算法·图搜索算法
深蓝电商API1 天前
爬虫+大模型结合:让AI自动写XPath和清洗规则
人工智能·爬虫