Scrapy Core API 深度解析:基于 Crawler 的扩展与中间件开发指南
本指南围绕 docs/topics/api.rst 所记载的 Scrapy 核心 API 展开,面向扩展(Extensions)与中间件(Middlewares)开发者,系统讲解 Crawler API、Settings API、SpiderLoader API、Signals API、Stats Collector API 与 Engine API 的接口设计与调用关系。阅读本文后,你将掌握在 Scrapy 项目中使用 Crawler 对象访问全部核心组件、编写自定义扩展/中间件、管理多爬虫运行,以及通过源码级细节(如 scrapy/crawler.py、scrapy/settings/init.py)理解各 API 底层行为的能力。
Crawler API:Scrapy 核心 API 的统一入口
Scrapy API 的主入口是 scrapy/crawler.py 中的 Crawler 对象。文档明确:Scrapy 的各个组件(详见 components.rst)在初始化时通过 from_crawler(即 scrapy/utils/misc.py 中的 build_from_crawler)获得它。Crawler 提供对 Scrapy 所有核心组件的访问,是组件访问核心功能并将其挂钩进 Scrapy 的唯一途径——它扮演了"组件总线"的角色。
Crawler 的构造函数签名如下(来自源码):
def __init__(self, spidercls, settings=None, init_reactor=False):
要点:
spidercls必须是Spider的子类。若传入的是Spider实例,会直接抛出ValueError(源码中显式检查isinstance(spidercls, Spider));settings可以是一个字典或Settings对象,None时内部会构造一个空的Settings;- 构造时
Crawler会复制一份 settings 作为自己的self.settings,并调用spidercls.update_settings(self.settings),使蜘蛛类中定义的custom_settings得以应用。
延迟初始化的核心属性(自 2.18 起行为变化)
engine、extensions、logformatter、request_fingerprinter 与 stats 这几个属性属于"晚绑定"属性(late attributes),它们的值在爬取开始时才被赋值。在这些属性赋值之前读取会抛出 RuntimeError。
源码中通过 _LateAttribute 描述符实现(scrapy/crawler.py):值实际存放在下划线前缀的私有属性中(如 _engine),在爬取开始前读取公开属性会抛出 RuntimeError,报错信息会提示"该属性在爬取开始时才被赋值,例如只能在 spider_opened 信号处理器及之后使用"。
api.rst 中的版本说明(versionchanged:: 2.18.0)明确指出:在 2.18.0 之前,这些属性在被赋值前值为 None。如果你正在维护面向旧版本的扩展,需要注意这一兼容性差异——依赖"读 crawler.engine 得到 None"的旧代码在新版本会收到异常而非 None。
从 scrapy/crawler.py 的 _apply_settings() 可以看到这些属性的实际装配顺序:
- 加载 add-ons(
self.addons.load_settings),并处理废弃的蜘蛛属性(download_delay→DOWNLOAD_DELAY、max_concurrent_requests→CONCURRENT_REQUESTS_PER_DOMAIN); - 依据
STATS_CLASS创建stats; - 依据
LOG_FORMATTER创建logformatter; - 依据
REQUEST_FINGERPRINTER_CLASS创建request_fingerprinter; - 检查/安装 Twisted reactor(由
TWISTED_REACTOR_ENABLED等设置驱动); - 创建
ExtensionManager并赋值给extensions; self.settings.freeze()冻结设置。
Crawler 各属性的用途
| 属性 | 说明 |
|---|---|
request_fingerprinter |
该爬虫的请求指纹构建器。扩展与中间件用它为请求构建短小、唯一的标识符。指纹相关概念详见 request-response.rst 中的请求指纹章节 |
settings |
该爬虫的设置管理器,供扩展与中间件访问 Scrapy 设置。入门见 settings.rst,API 见 scrapy.settings.Settings 类 |
signals |
该爬虫的信号管理器,扩展与中间件通过它把自身功能挂钩进 Scrapy。入门见 signals.rst,API 见 SignalManager 类 |
stats |
该爬虫的统计收集器,用于记录组件自身行为产生的统计量,或读取其他扩展采集的统计。入门见 stats.rst,API 见 StatsCollector 类 |
logformatter |
该爬虫的日志格式化器,扩展与中间件用其构造关于爬取事件的消息。API 见 scrapy.logformatter.LogFormatter 类 |
extensions |
追踪已启用扩展的扩展管理器。多数扩展无需直接访问它。扩展入门见 extensions.rst |
engine |
执行引擎,协调调度器、下载器与蜘蛛之间的核心爬取逻辑。部分扩展可能想访问引擎以检视或修改下载器与调度器行为,但这属于高级用法,且该 API 尚未稳定 |
spider |
当前正在爬取的蜘蛛。它是构造 crawler 时提供的蜘蛛类的实例,在 crawl 方法给出的参数应用之后创建 |
从 Crawler 获取组件运行时实例
2.12 版本起,Crawler 提供了一组 get_* 方法,用于在运行时获取已启用的组件实例(找不到时返回 None)。它们都基于统一的 _get_component 实现(按类做 isinstance 匹配),但多数方法要求引擎已经创建:
get_addon(cls):返回指定 add-on 类(或其子类)的运行时实例;get_extension(cls):返回指定扩展类的运行时实例(2.12+);get_downloader_middleware(cls):返回指定下载器中间件类的运行时实例;get_item_pipeline(cls):返回指定 Item Pipeline 类的运行时实例;get_spider_middleware(cls):返回指定蜘蛛中间件类的运行时实例。
从源码看,后四者内部直接遍历引擎的相关容器(如 engine.downloader.middleware.middlewares、engine.scraper.itemproc.middlewares、engine.scraper.spidermw.middlewares、extensions.middlewares)。因此文档与源码都强调:这些方法只能在爬取引擎创建之后调用,例如在 engine_started 或 spider_opened 信号处理器中调用;在引擎未创建时会抛出 RuntimeError。而 get_addon 没有该限制(add-on 在 _apply_settings() 早期即已加载)。
启动与停止爬取
Crawler 提供同步(基于 Twisted Deferred)与异步(基于 asyncio)两套控制 API:
crawl(*args, **kwargs):用给定参数实例化蜘蛛类并使执行引擎运转,一个实例只能调用一次(重复调用会抛RuntimeError)。返回一个在爬取完成时触发的 Deferred。内部流程(见 scrapy/crawler.py):创建蜘蛛 → 应用设置 → 创建引擎 →engine.open_spider_async()→engine.start_async();若中途抛出CloseSpider,则以相应 reason 关闭引擎;crawl_async(*args, **kwargs):2.14 版本新增的 asyncio 版本,爬取结束时协程完成;stop():发起优雅停止并返回 Deferred。已废弃,请改用stop_async();stop_async():2.14 版本新增,优雅停止爬虫并在完全停止时完成。
扩展管理器与 EXTENSIONS 设置
Crawler 引用的扩展管理器(Extension Manager)负责加载并跟踪所有已安装的扩展,它通过 EXTENSIONS 设置配置。该设置是一个字典,包含所有可用扩展及其顺序值,配置方式与下载器中间件的配置类似(数字越小优先级越高,None 表示禁用)。ExtensionManager 在 scrapy/extension.py 中实现,并由 build_from_crawler 以 crawler 为参数构造——这正是扩展能拿到 from_crawler 回调中 crawler 的原因。
多爬虫运行辅助类:Runner 与 Process
当需要手动控制爬取过程(而非依赖 scrapy crawl 命令)时,可以使用 scrapy/crawler.py 中的四组辅助类。它们都共享一个基类 CrawlerRunnerBase,基类构造函数会解析 settings、通过 get_spider_loader(settings) 构建蜘蛛加载器,并维护 _crawlers 集合与 bootstrap_failed 状态。
共同能力(来自基类):
crawlers属性:返回由crawl()启动并被该类管理的Crawler集合;create_crawler(crawler_or_spidercls):接受三种输入——已构造的Crawler(把 runner 的 settings 作为默认值合并进去)、Spider子类、或项目内蜘蛛名(通过 spider loader 查找)。注意不能传蜘蛛实例,否则抛ValueError。
CrawlerRunner(Deferred 风格)与 AsyncCrawlerRunner(协程风格)
CrawlerRunner 是在已搭好的 Twisted reactor 中跟踪、管理并运行爬虫的便捷类。它提供 Deferred 风格的 API:
crawl(crawler_or_spidercls, *args, **kwargs):运行一个爬虫并返回在爬取结束时触发的 Deferred;stop():同时停止所有正在进行的爬取任务,返回在所有任务结束后触发的 Deferred;join():在所有受管crawlers完成执行时触发。
AsyncCrawlerRunner(2.14+)提供现代协程 API:crawl() 返回 asyncio.Task,stop()、join() 为可等待的协程。它既支持已安装的 Twisted reactor,也支持已安装的 asyncio 事件循环(取决于 TWISTED_REACTOR_ENABLED)。需要特别注意其 reactor 前置条件(源码中的显式检查):
TWISTED_REACTOR_ENABLED=True时:要求 reactor 已安装,且必须是twisted.internet.asyncioreactor.AsyncioSelectorReactor;TWISTED_REACTOR_ENABLED=False时:要求 reactor 未被安装,但必须已有 asyncio 事件循环在运行。
CrawlerRunner 不支持 TWISTED_REACTOR_ENABLED=False(构造函数直接抛 RuntimeError)。
CrawlerProcess 与 AsyncCrawlerProcess
CrawlerProcess 在 CrawlerRunner 之上增加了"启动 Twisted reactor 并处理关闭信号(如 Ctrl-C)"的能力,同时负责配置顶层日志。文档建议:如果你没有在自己的应用中运行另一个 reactor,CrawlerProcess 是比 CrawlerRunner 更合适的选择。
构造函数可传 install_root_handler 参数(默认 True)控制是否安装根日志处理器。
其核心方法:
process = CrawlerProcess(settings) # 需传 Settings 对象
process.crawl(spidercls_or_name, *args, **kwargs) # 注册爬虫(可多次)
process.start(stop_after_crawl=True,
install_signal_handlers=True) # 阻塞运行
start() 会:
- 启动 Twisted reactor,并依据
REACTOR_THREADPOOL_MAXSIZE调整线程池大小; - 依据
TWISTED_DNS_RESOLVER安装 DNS 解析器(源码中同时处理了已废弃的DNS_RESOLVER及其优先级告警); stop_after_crawl=True(默认)时通过join()在所有爬虫结束后停止 reactor;False时 reactor 保持运行直到外部停止;install_signal_handlers=True(默认)时安装 Scrapy 的优雅关闭/强制杀进程两级信号处理器——第一次收到信号优雅关闭(再按一次可取消),第二次立即强制终止。
AsyncCrawlerProcess 是 AsyncCrawlerRunner 的对应版本,start() 依据 TWISTED_REACTOR_ENABLED 分别走 Twisted 路径(_start_twisted)或纯 asyncio 事件循环路径(_start_asyncio,见源码中详尽的状态机注释,覆盖正常完成、stop_after_crawl=False、按一次/两次 Ctrl-C 等多种工作流)。
从脚本手动运行爬虫的完整示例见 docs/intro/install.rst 中"从脚本运行"一节。CLI 命令
scrapy crawl底层正是通过CrawlerProcess实现(见 scrapy/commands/crawl.py)。
Settings API:带优先级的设置管理器
SETTINGS_PRIORITIES:设置来源与优先级
scrapy/settings/init.py 定义了默认的设置优先级字典。每个条目代表一个设置入口,含识别用的代码名与整型优先级;数值越大,在 Settings 类中设置与读取时优先级越高:
SETTINGS_PRIORITIES = {
"default": 0,
"command": 10,
"addon": 15,
"project": 20,
"spider": 30,
"cmdline": 40,
}
这六类来源的含义分别对应:default(scrapy/settings/default_settings.py 内置默认值)、command(如命令内部临时设置)、addon(add-on 贡献的设置)、project(项目 settings.py)、spider(蜘蛛的 custom_settings / 蜘蛛属性)、cmdline(scrapy crawl 命令行 -s 传入)。完整讲解见 settings.rst。
get_settings_priority(priority) 是一个小助手函数(scrapy/settings/init.py):若传入字符串,则在上述字典中查表返回整型值;若本身就是整数则原样返回。
获取项目设置
scrapy.utils.project.get_project_settings() 返回当前项目的 Settings 对象(源码见 scrapy/utils/project.py)。其内部逻辑为:
- 若未设置
SCRAPY_SETTINGS_MODULE环境变量,则根据SCRAPY_PROJECT(默认"default")初始化 scrapy 环境; - 构造
Settings()(自带全部全局默认设置,见下文); - 从
SCRAPY_SETTINGS_MODULE指定的模块以project优先级加载设置; - 将环境变量中
SCRAPY_CHECK、SCRAPY_PROJECT、SCRAPY_PYTHON_SHELL、SCRAPY_SETTINGS_MODULE以project优先级写入。
CrawlerRunner/CrawlerProcess 构造函数也接受 dict、Settings 或 None 作为 settings 入参,内部统一转换为 Settings。
Settings 与 BaseSettings:字典式行为 + 优先级 + 冻结
Settings 与 BaseSettings 都实现了可变映射(MutableMapping)接口,但会为每个 (key, value) 对同时保存优先级,并支持冻结(不可变)。
Settings 是 BaseSettings 的直接子类,唯一的额外行为在构造函数中:实例化时会先把全局默认设置(scrapy/settings/default_settings.py,通过 setmodule 以 "default" 优先级加载)填入自身,并把所有字典类型的默认值提升为 BaseSettings 实例以支持逐键优先级,最后再合并用户传入的 values(默认 "project" 优先级)。因此访问不存在的键时 __getitem__ 返回 None,而 get(name, default) 会在值为 None 时回退到默认值。
核心方法一览(均定义于 scrapy/settings/init.py):
| 方法 | 说明 |
|---|---|
get(name, default=None) |
取设置值,不做类型转换;会针对已废弃的 CONCURRENT_REQUESTS_PER_IP、DNS_RESOLVER 发告警 |
getbool/getint/getfloat(name, default) |
按布尔/整型/浮点读取。getbool 接受 1/'1'/True/'True' 为真,0/'0'/False/'False'/None 为假,非法值抛 ValueError |
getlist(name, default=None) |
列表读取:原值是 list 则返回副本,字符串按 , 分割,空串返回空列表(对应 'one,two' 环境变量场景) |
getdict(name, default=None) |
字典读取:字符串会被 json.loads 解析 |
getdictorlist |
兼容 dict/list 的读取,字符串先尝试 JSON,失败回退为逗号分隔列表 |
getwithbase(name) |
取字典型设置与其 _BASE 后缀默认值组合后的 BaseSettings |
get_component_priority_dict_with_base(name) |
针对组件优先级字典(如中间件/扩展配置)的组合读取,会把键解析为导入路径去重后再还原 |
set(name, value, priority="project") |
存储键值对并给定优先级;已存在时仅在 priority >= 现有优先级时覆盖 |
setdefault / update(values, priority) |
默认值语义 / 批量更新。update 支持 dict、可迭代、JSON 字符串,且传入 BaseSettings 时按各键原有优先级更新 |
setmodule(module, priority) |
把模块内所有全大写变量以指定优先级载入 |
delete(name) / pop(name) |
删除指定设置(优先级不足时不删除) |
getpriority(name) |
返回设置的数值优先级,不存在返回 None |
maxpriority() |
返回所有设置中的最高优先级 |
copy() / frozencopy() / freeze() |
深拷贝 / 冻结副本 / 冻结当前实例。冻结后任何修改尝试都会抛 TypeError |
copy_to_dict() |
复制为普通 dict(scrapy shell 中打印设置时很有用) |
此外还有供 add-on/扩展在不改变原设置优先级的前提下操作的组件字典工具:add_to_list、remove_from_list、replace_in_component_priority_dict、set_in_component_priority_dict、setdefault_in_component_priority_dict。
值得注意的是 set() 的文档化约定:设置应在 Crawler 应用它们之前填充(即 crawl/crawl_async 之前),否则不会生效——因为 _apply_settings() 末尾会调用 settings.freeze(),从此设置对象进入不可变状态。
SpiderLoader API:自定义蜘蛛加载器
当需要自定义蜘蛛发现与加载逻辑时,可在项目设置 SPIDER_LOADER_CLASS 中指定自定义加载器路径。它必须实现 SpiderLoaderProtocol(scrapy/spiderloader.py)规定的接口:
from_settings(cls, settings):类方法,返回给定 settings 下的实例;load(spider_name):按名称返回蜘蛛类,找不到必须抛KeyError;list():返回项目所有可用蜘蛛名列表;find_by_request(request):返回可处理给定请求的蜘蛛名列表。
框架自带的默认实现 SpiderLoader(同文件 scrapy/spiderloader.py):
- 从
SPIDER_MODULES读取蜘蛛模块列表,用walk_modules_iter递归遍历并用iter_spider_classes找出蜘蛛类; - 若某模块导入失败:
SPIDER_LOADER_WARN_ONLY为真则仅发RuntimeWarning,否则直接抛出; load()未命中时抛KeyError(f"Spider not found: {spider_name}");_check_name_duplicates()会检查跨模块的同名蜘蛛并发出UserWarning;find_by_request()本质是调用cls.handles_request(request)过滤蜘蛛。
DummySpiderLoader 是一个不加载任何蜘蛛的"哑"加载器:list() 返回空、load() 恒抛 KeyError。get_spider_loader(settings) 工厂函数(同文件 scrapy/spiderloader.py)根据 SPIDER_LOADER_CLASS 加载类并调用其 from_settings(settings.frozencopy())——注意传入的是冻结副本,防止加载器修改项目设置。
Signals API:信号管理器
SignalManager(scrapy/signalmanager.py)封装了 PyDispatcher,并以 crawler 作为默认 sender。蜘蛛回调、中间件、扩展在构造时拿到 crawler 后,便可用 crawler.signals.connect(...) 挂钩各类事件。可用信号清单见 signals.rst。
核心方法:
connect(receiver, signal, **kwargs):把接收函数连接到某信号,默认sender为自身持有的 sender(通常是 crawler);disconnect(receiver, signal, **kwargs):取消连接,参数同connect;send_catch_log(signal, **kwargs):发送信号,捕获并记录处理器抛出的异常;关键字参数会被透传给所有已连接的处理器;send_catch_log_deferred(signal, **kwargs):send_catch_log的异步版本,返回一个在所有信号处理器完成后触发的 Deferred。已废弃,请使用send_catch_log_async;send_catch_log_async(signal, **kwargs):2.14 起提供的协程版本,支持异步信号处理器(会等待 Deferred/协程处理器执行完毕);disconnect_all(signal, **kwargs):断开给定信号上的全部接收者;wait_for(signal):协程,等待下一个信号发生(对应文档中"懒加载 start_requests"的应用场景,见 spiders.rst 相关示例),内部实现为注册一个临时处理器并在信号到来后自行断开。
Stats Collector API:统计采集接口
scrapy/statscollectors.py 提供了若干统计采集器,它们都继承并实现 StatsCollector 定义的 API。爬虫运行时输出的 Scrapy stats 摘要即来自该模块。
面向扩展/中间件的采集方法(公开 API)
get_value(key, default=None):返回指定统计键的值,不存在时返回default;get_stats():把当前正在运行的蜘蛛的全部统计以字典形式返回;set_value(key, value):为指定键设置值;set_stats(stats):用传入的字典整体覆盖当前统计;inc_value(key, count=1, start=0):将键值增加count,若键未设置则先初始化为start再累加(等价d.setdefault(key, start) + count);max_value(key, value):仅当当前值小于value(或键尚未设置)时写入;min_value(key, value):仅当当前值大于value(或键尚未设置)时写入;clear_stats():清空全部统计。
供实现自定义采集器的方法
以下方法不属于统计采集公共 API,而是自定义统计采集器时使用的钩子:
open_spider():为统计采集打开蜘蛛;close_spider():关闭蜘蛛。调用之后,不能再访问或采集更多统计(源码中MemoryStatsCollector正是在_persist_stats阶段把最后一批统计按蜘蛛名归档到spider_stats)。
框架内置采集器与选择建议:
MemoryStatsCollector:默认采集器,在内存中保留每个蜘蛛最近一次爬取的统计,可通过spider_stats(以蜘蛛名为键的字典)访问;DummyStatsCollector:什么都不做但非常高效,可通过STATS_CLASS设置为它来彻底关闭统计以提升性能(不过源码注释也提醒:相比解析页面等抓取负载,统计开销通常微不足道);StatsCollector:基类,close_spider中受STATS_DUMP设置控制是否打印统计转储。
另注意:StatsCollector 通过 __getattribute__ 把 spider 位置参数形式(已废弃用法)统一包装为带 _warn_spider_arg 装饰的缓存方法,因此各方法签名都兼容可选的 spider 参数并会给出废弃告警。这些方法由引擎在蜘蛛生命周期中自动调用(见 scrapy/core/scraper.py 与扩展对 stats 的使用)。
Engine API:执行引擎的只读访问面
最后,api.rst 还暴露了引擎的最小子集:scrapy.core.engine.ExecutionEngine 的两个成员(完整实现见 scrapy/core/engine.py):
needs_backout():返回布尔值,表示引擎当前是否处于需要"退避"(停止从队列中取新请求)的状态——通常用于调度层面判断下载器积压是否超过阈值;scheduler:引擎所持有的调度器对象。
引擎是"调度器—下载器—蜘蛛"三者间核心爬取逻辑的协调者(架构总览见 architecture.rst)。api.rst 将其标记为供扩展检视/修改下载器与调度器行为的高级接口,且尚未稳定——这意味着第三方代码应尽量避免依赖引擎内部细节,优先使用前文介绍的公开信号、统计与组件访问 API。引擎在 scrapy/core/engine.py 中还提供 open_spider_async()、close_spider_async()、start_async()、stop_async() 等方法,它们是 Crawler.crawl_async() 内部实际调用的底层协程。
小结与上手建议
把上述 API 串起来的典型扩展开发路径是:
- 在扩展的
from_crawler中拿到crawler; - 用
crawler.settings读取自定义配置(通过get*类型转换方法),用crawler.signals.connect订阅engine_started、spider_opened、spider_closed等信号; - 在信号处理器内用
crawler.get_extension/crawler.get_downloader_middleware等get_*方法检索其他已启用组件,用crawler.stats记录行为统计,用crawler.logformatter构造日志消息; - 通过
EXTENSIONS设置启用扩展,在 tests 目录(如 test_extension_*.py、test_crawler_runners.py)中参照官方测试验证行为。
多爬虫场景下,优先使用 CrawlerProcess/AsyncCrawlerProcess 一站式管理 reactor/事件循环与关闭信号;若你的应用已自持 reactor,再退而使用 CrawlerRunner/AsyncCrawlerRunner。而全新的 asyncio 原生代码应优先采用 2.14 起引入的协程 API(crawl_async/stop_async/AsyncCrawlerProcess 等),同时留意 api.rst 与源码注释中标注的各废弃项与版本边界。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00