【爬虫框架-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

相关推荐
王同学_1161 天前
爬虫辅助技术(css选择器、xpath、正则基础语法)
前端·css·爬虫
3824278271 天前
使用 webdriver-manager配置geckodriver
java·开发语言·数据库·爬虫·python
如旧呀1 天前
爬虫小知识
数据库·爬虫·mysql
liu****1 天前
Python简单爬虫实践案例
开发语言·爬虫·python
3824278271 天前
python3网络爬虫开发实战 第2版:并发限制
开发语言·爬虫·python
我可以将你更新哟1 天前
【爬虫】下载ffmpeg,爬取b站视频,把音频和视频合成一个视频
爬虫·ffmpeg·音视频
胡伯来了1 天前
08 - 数据收集 - 网页采集工具Selenium
爬虫·python·selenium·rag·网络采集
Cherry的跨界思维2 天前
25、AI时代的数字生存战:爬虫与反爬虫的数据争夺全面解析
人工智能·爬虫·机器学习·python爬虫·python办公自动化·python反爬虫
我可以将你更新哟2 天前
【爬虫】使用协程(asyncio)爬取旁边桌面图片并存入数据
爬虫