Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >教程 | 比Python快100倍,利用spaCy和Cython实现高速NLP项目

教程 | 比Python快100倍,利用spaCy和Cython实现高速NLP项目

作者头像
小小科
发布于 2018-07-31 08:09:41
发布于 2018-07-31 08:09:41
1.6K00
代码可运行
举报
文章被收录于专栏:北京马哥教育北京马哥教育
运行总次数:0
代码可运行

来源:机器之心

ID:almosthuman2014

Cython 是一个工具包,可以使你在 Python 中编译 C 语言,这就是为什么 numpy 和 pandas 很快的原因,Cython 就是 Python 的超集。在本文中,作者将为我们介绍他的 GitHub 项目 NeuralCoref v3.0,详解如何利用 spaCy 和 Cython 以约 100 倍于 Python 的速度实现 NLP 项目。

相关 Jupyter Notebook 地址:https://github.com/huggingface/100-times-faster-nlp

去年我们发布 Python 包 coreference resolution package 后,我们收到了来自社区的精彩反馈,并且人们开始在很多应用中使用它,其中一些与我们原来的对话用例迥异。

我们发现,尽管对话信息的处理速度非常好,但对于长的新闻文章来说,处理速度可能会非常慢。

我决定详细研究这一问题,最终成果即 NeuralCoref v3.0,它在相同准确率的情况下比老版本快 100 倍左右(每秒几千字),同时兼顾 Python 库的易用性和兼容性。

NeuralCoref v3.0 :https://github.com/huggingface/neuralcoref/

我想在这篇文章中分享一些关于这个项目的经验,特别是:

  • 如何用 Python 设计一个高速模块;
  • 如何利用 spaCy 的内部数据结构来有效地设计超高速 NLP 函数。

所以我在这里有点作弊,因为我们会谈论 Python,但也谈论一些 Cython 的神奇作用。但是,你知道吗?Cython 是 Python 的超集,所以不要让它吓跑你!

你现在的 Python 程序已经是 Cython 程序。

有几种情况下你可能需要加速,例如:

  • 你正在使用 Python 开发一个 NLP 的生产模块;
  • 你正在使用 Python 计算分析大型 NLP 数据集;
  • 你正在为深度学习框架,如 PyTorch / TensorFlow,预处理大型训练集,或者你的深度学习批处理加载器中的处理逻辑过于繁重,这会降低训练速度。

再强调一遍:我同步发布了一个 Jupyter Notebook,其中包含我在本文中讨论的例子。试试看!

Jupyter Notebook:https://github.com/huggingface/100-times-faster-nlp

加速第一步:剖析

首先要知道的是,你的大多数代码在纯 Python 环境中可能运行的不错,但是如果你多用点心,其中一些瓶颈函数可能让你的代码快上几个数量级。

因此,你首先应该分析你的 Python 代码并找出瓶颈部分的位置。使用如下的 cProfile 是一种选择:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import cProfile
 import pstats
 import myslowmodule
 cProfile.run('myslowmodule.run()', 'restats')
 p = pstats.Stats('restats')
 p.sortstats('cumulative').printstats(30)

如果你使用神经网络,你可能会发现瓶颈部分是几个循环,并且涉及 Numpy 数组操作。

那么,我们如何加速这些循环代码?

在 Python 中使用一些 Cython 加速循环

让我们用一个简单的例子来分析这个问题。假设我们有一大堆矩形,并将它们存储进一个 Python 对象列表,例如 Rectangle 类的实例。我们的模块的主要工作是迭代这个列表,以便计算有多少矩形的面积大于特定的阈值。

我们的 Python 模块非常简单,如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
from random import random

class Rectangle:
 def __init__(self, w, h):
 self.w = w
 self.h = h
 def area(self):
 return self.w * self.h

def check_rectangles(rectangles, threshold):
 n_out = 0
 for rectangle in rectangles:
 if rectangle.area() > threshold:
 n_out += 1
 return n_out

def main():
 n_rectangles = 10000000
 rectangles = list(Rectangle(random(), random()) for i in range(n_rectangles))
 n_out = check_rectangles(rectangles, threshold=0.25)
 print(n_out)

check_rectangles 函数是瓶颈部分!它对大量的 Python 对象进行循环,这可能会很慢,因为 Python 解释器在每次迭代时都会做大量工作(寻找类中的求面积方法、打包和解包参数、调用 Python API ...)。

Cython 将帮助我们加速循环。

Cython 语言是 Python 的超集,它包含两种对象:

  • Python 对象是我们在常规 Python 中操作的对象,如数字、字符串、列表、类实例...
  • Cython C 对象是 C 或 C ++ 对象,比如 double、int、float、struct、vectors。这些可以由 Cython 在超快速的底层代码中编译。

快速循环只是 Cython 程序(只能访问 Cython C 对象)中的一个循环。

设计这样一个循环的直接方法是定义 C 结构,它将包含我们在计算过程中需要的所有要素:在我们的例子中,就是矩形的长度和宽度。

然后,我们可以将矩形列表存储在这种结构的 C 数组中,并将这个数组传递给我们的 check_rectangle 函数。此函数现在接受一个 C 数组作为输入,因此通过 cdef 关键字而不是 def 将其定义为 Cython 函数(请注意,cdef 也用于定义 Cython C 对象)。

下面是我们的 Python 模块的快速 Cython 版:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
from cymem.cymem cimport Pool
from random import random

cdef struct Rectangle:
 float w
 float h

cdef int check_rectangles(Rectangle* rectangles, int n_rectangles, float threshold):
 cdef int n_out = 0
 # C arrays contain no size information => we need to give it explicitly
 for rectangle in rectangles[:n_rectangles]:
 if rectangles[i].w * rectangles[i].h > threshold:
 n_out += 1
 return n_out

def main():
 cdef:
 int n_rectangles = 10000000
 float threshold = 0.25
 Pool mem = Pool()
 Rectangle* rectangles = <Rectangle*>mem.alloc(n_rectangles, sizeof(Rectangle))
 for i in range(n_rectangles):
 rectangles[i].w = random()
 rectangles[i].h = random()
 n_out = check_rectangles(rectangles, n_rectangles, threshold)
 print(n_out)

我们在这里使用了原生 C 指针数组,但你也可以选择其他选项,特别是 C ++ 结构,如向量、对、队列等。在这个片段中,我还使用了 cymem 的便利的 Pool()内存管理对象,以避免必须手动释放分配的 C 数组。当 Pool 由 Python 当做垃圾回收时,它会自动释放我们使用它分配的内存。

spaCy API 的 Cython Conventions 是 Cython 在 NLP 中的实际运用的一个很好的参考。

spaCy:https://spacy.io

Cython Conventions:https://spacy.io/api/cython#conventions

让我们试试这个代码吧!

有很多方法可以测试、编译和发布 Cython 代码!Cython 甚至可以直接用在 Python 这样的 Jupyter Notebook 中。

Jupyter Notebook:http://cython.readthedocs.io/en/latest/src/reference/compilation.html#compiling-notebook

首先使用 pip install cython 安装 Cython

在 Jupyter 的第一次测试

使用 %load_ext Cython 将 Cython 插件加载到 Jupyter notebook 中。

现在,你可以使用黑魔术命令 %% cython 编写像 Python 代码一样的 Cython 代码。

如果在执行 Cython 单元时遇到编译错误,请务必检查 Jupyter 终端输出以查看完整的信息。

大多数情况下,在 %% cython 编译为 C ++(例如,如果你使用 spaCy Cython API)或者 import numpy(如果编译器不支持 NumPy)之后,你会丢失 - + 标记。

正如我在开始时提到的,查看这篇文章的同步 Jupyter Notebook,该 Notebook 包含本文讨论的所有示例。

编写、使用和发布 Cython 代码

Cython 代码写在 .pyx 文件中。这些文件由 Cython 编译器编译为 C 或 C ++ 文件,然后通过系统的 C 编译器编译为字节码文件。Python 解释器可以使用字节码文件。

你可以使用 pyximport 直接在 Python 中加载 .pyx 文件:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
>>> import pyximport; pyximport.install()
>>> import my_cython_module

你还可以将你的 Cython 代码构建为 Python 包,并将其作为常规 Python 包导入/发布,详见下方地址。这可能需要一些时间才能开始工作,尤其在全平台上。如果你需要一个有效示例,spaCy』s install script 是一个相当全面的例子。

导入教程:http://cython.readthedocs.io/en/latest/src/tutorial/cython_tutorial.html#

Before we move to some NLP, let』s quickly talk about the def, cdef and cpdef keywords, because they are the main things you need to grab to start using Cython.

在我们转向 NLP 之前,让我们先快速讨论一下 def、cdef 和 cpdef 关键字,因为它们是你开始使用 Cython 需要掌握的主要内容。

你可以在 Cython 程序中使用三种类型的函数:

  • Python 函数,用常用的关键字 def 定义。它们可作为输入和输出的 Python 对象。也可以在内部同时使用 Python 和 C / C ++ 对象,并可以调用 Cython 和 Python 函数。
  • 用 cdef 关键字定义的 Cython 函数。它们可以作为输入,在内部使用并输出 Python 和 C / C ++对象。这些函数不能从 Python 空间访问(即 Python 解释器和其他可导入 Cython 模块的纯 Python 模块),但可以由其他 Cython 模块导入。
  • 用 cpdef 关键字定义的 Cython 函数就像 cdef 定义的 Cython 函数一样,但它们也提供了一个 Python 封装器,因此可以从 Python 空间(以 Python 对象作为输入和输出)以及其他 Cython 模块(以 C / C ++ 或 Python 对象作为输入)中调用它们。

cdef 关键字有另一种用途,即在代码中定义 Cython C / C ++ 对象。除非用这个关键字定义对象,否则它们将被视为 Python 对象(因此访问速度很慢)。

使用 Cython 与 spaCy 来加速 NLP

这些东西又好又快,但是...... 我们现在还没有融入 NLP!没有字符串操作、没有 unicode 编码,也没有我们在自然语言处理中幸运拥有的微妙联系。

官方的 Cython 文档甚至建议不要使用 C 字符串:

一般来说:除非你知道自己在做什么,否则应尽可能避免使用 C 字符串,而应使用 Python 字符串对象。

那么我们如何在使用字符串时在 Cython 中设计快速循环?

spaCy 会帮我们的。

spaCy 解决这个问题的方式非常聪明。

将所有字符串转换为 64 位哈希码

spaCy 中的所有 unicode 字符串(token 的文本、其小写文本、引理形式、POS 键标签、解析树依赖关系标签、命名实体标签...)都存储在叫 StringStore 的单数据结构中,它们在里面由 64 位散列索引,即 C uint64_t。

StringStore 对象实现了 Python unicode 字符串和 64 位哈希码之间的查找表。

它可以通过 spaCy 任意处及任意对象访问(请参阅上图),例如 nlp.vocab.strings、doc.vocab.strings 或 span.doc.vocab.string。

当某个模块需要对某些 token 执行快速处理时,仅使用 C 级别的 64 位哈希码而不是字符串。调用 StringStore 查找表将返回与哈希码相关联的 Python unicode 字符串。

但是,spaCy 做的远不止这些,它使我们能够访问文档和词汇表的完全覆盖的 C 结构,我们可以在 Cython 循环中使用这些结构,而不必自定义结构。

spaCy 的内部数据结构

与 spaCy Doc 对象关联的主要数据结构是 Doc 对象,该对象拥有已处理字符串的 token 序列(「单词」)以及 C 对象中的所有称为 doc.c 的标注,它是一个 TokenC 结构数组。

TokenC 结构包含我们需要的关于每个 token 的所有信息。这些信息以 64 位哈希码的形式存储,可以重新关联到 unicode 字符串,就像我们刚刚看到的那样。

要深入了解这些 C 结构中的内容,只需查看刚创建的 SpaCy 的 Cython API doc。

我们来看看一个简单的 NLP 处理示例。

使用 spaCy 和 Cython 进行快速 NLP 处理

假设我们有一个需要分析的文本数据集

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import urllib.request
import spacy

with urllib.request.urlopen('https://raw.githubusercontent.com/pytorch/examples/master/word_language_model/data/wikitext-2/valid.txt') as response:
text = response.read()
nlp = spacy.load('en')
doc_list = list(nlp(text[:800000].decode('utf8')) for i in range(10))

我在左边写了一个脚本,它生成用于 spaCy 解析的 10 份文档的列表,每个文档大约 170k 字。我们也可以生成每个文档 10 个单词的 170k 份文档(比如对话数据集),但创建速度较慢,因此我们坚持使用 10 份文档。

我们想要在这个数据集上执行一些 NLP 任务。例如,我们想要统计数据集中单词「run」作为名词的次数(即用 spaCy 标记为「NN」词性)。

一个简单明了的 Python 循环就可以做到:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
def slow_loop(doc_list, word, tag):
 n_out = 0
 for doc in doc_list:
 for tok in doc:
 if tok.lower_ == word and tok.tag_ == tag:
 n_out += 1
 return n_out

def main_nlp_slow(doc_list):
 n_out = slow_loop(doc_list, 'run', 'NN')
 print(n_out)

但它也很慢!在我的笔记本电脑上,这段代码需要大约 1.4 秒才能得到结果。如果我们有一百万份文件,则需要一天以上才能给出结果。

我们可以使用多线程,但在 Python 中通常不是很好的解决方案,因为你必须处理 GIL。另外,请注意,Cython 也可以使用多线程!而且这实际上可能是 Cython 最棒的部分,因为 GIL 被释放,我们可以全速运行。Cython 基本上直接调用 OpenMP。

现在我们尝试使用 spaCy 和部分 Cython 加速我们的 Python 代码。

首先,我们必须考虑数据结构。我们将需要一个 C 数组用于数据集,指针指向每个文档的 TokenC 数组。我们还需要将我们使用的测试字符串(「run」和「NN」)转换为 64 位哈希码。

当我们所需的数据都在 C 对象中时,我们可以在数据集上以 C 的速度进行迭代。

下面是如何使用 spaCy 在 Cython 中编写的示例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
%%cython -+
import numpy # Sometime we have a fail to import numpy compilation error if we don't import numpy
from cymem.cymem cimport Pool
from spacy.tokens.doc cimport Doc
from spacy.typedefs cimport hash_t
from spacy.structs cimport TokenC

cdef struct DocElement:
 TokenC* c
 int length

cdef int fast_loop(DocElement* docs, int n_docs, hash_t word, hash_t tag):
 cdef int n_out = 0
 for doc in docs[:n_docs]:
 for c in doc.c[:doc.length]:
 if c.lex.lower == word and c.tag == tag:
 n_out += 1
 return n_out

def main_nlp_fast(doc_list):
 cdef int i, n_out, n_docs = len(doc_list)
 cdef Pool mem = Pool()
 cdef DocElement* docs = <DocElement*>mem.alloc(n_docs, sizeof(DocElement))
 cdef Doc doc
 for i, doc in enumerate(doc_list): # Populate our database structure
 docs[i].c = doc.c
 docs[i].length = (<Doc>doc).length
 word_hash = doc.vocab.strings.add('run')
 tag_hash = doc.vocab.strings.add('NN')
 n_out = fast_loop(docs, n_docs, word_hash, tag_hash)
 print(n_out)

代码有点长,因为我们必须在调用 Cython 函数之前在 main_nlp_fast 中声明并填充 C 结构。(如果你在代码中多次使用低级结构,使用 C 结构包装的 Cython 扩展类型来设计我们的 Python 代码是比每次填充 C 结构更优雅的选择。这就是大多数 spaCy 的结构,它是一种结合了快速,低内存以及与外部 Python 库和函数接口的简便性的非常优雅的方法。)

但它也快很多!在我的 Jupyter Notebook 中,这个 Cython 代码的运行时间大约为 20 毫秒,比我们的纯 Python 循环快大约 80 倍。

Jupyter Notebook cell 中编写的模块的绝对速度同样令人印象深刻,并且可以为其他 Python 模块和函数提供本地接口:在 30ms 内扫描约 1,700 万字意味着我们每秒处理高达 8000 万字。

我们这就结束了使用 Cython 进行 NLP 的快速介绍。我希望你喜欢它。

Cython 还有很多其他的东西可讲,但这会让我们远离主题。从现在开始,最好的地方可能就是 Cython tutorials 的概述和适用于 NLP 的 spaCy’s Cython page。

原文链接:https://medium.com/huggingface/100-times-faster-natural-language-processing-in-python-ee32033bdced

*声明:推送内容及图片来源于网络,部分内容会有所改动,版权归原作者所有,如来源信息有误或侵犯权益,请联系我们删除或授权事宜。

- END -

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-07-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 马哥Linux运维 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
必看!深入理解linux系统的目录结构就靠本文了
本文由马哥教育面授班24期学员推荐,转载自互联网,作者为郭東,内容略经小编改编和加工,观点跟作者无关,最后感谢作者的辛苦贡献与付出。 Linux和Windows操作系统中的文件系统些不同,在学习使用linux之前,能够了解这个不同之处助于后续的学习。本文先对Windows和Linux上面文件系统的一些概念进行区分,然后介绍一些Linux文件系统相关的原理,最后较为详细地介绍了Linux系统的目录结构。 一、Linux和Windows文件系统 ---- 下面分别简单介绍一下启动Windows和Linux
小小科
2018/05/02
2.3K0
必看!深入理解linux系统的目录结构就靠本文了
【Linux 基础篇】Linux 目录结构速查表
当谈论到Linux系统管理时,了解常见的目录结构是非常重要的。Linux操作系统采用一种层次结构的目录布局,每个目录都有其特定的用途和功能。在本篇博客中,我们将介绍Linux目录的速查表,帮助您更好地理解和导航Linux文件系统。
繁依Fanyi
2023/10/12
4190
linux文件系统
Linux是一个基于Unix的操作系统,具有强大的文件系统功能。Linux文件系统是在硬盘上组织和存储数据的一种结构,通过文件系统可以管理文件、目录、权限等信息。在Linux中,文件系统被组织成一个树形结构,称为文件系统层次结构(Filesystem Hierarchy Standard,FHS),该标准规定了Linux操作系统中各级目录的名称和用途,使得Linux文件系统具有统一性和规范性。
玖叁叁
2023/04/08
9.1K0
【Linux核心宝典】Linux 系统目录结构详解 - 01
Linux系统目录是Linux操作系统中最重要的部分之一,它承载着Linux系统的文件和目录结构。如同Windows系统中的文件夹,Linux系统目录用于组织和管理系统的文件和应用程序。不同的是,Linux系统目录具有更加灵活和复杂的结构,因此了解和掌握Linux系统目录的结构和操作对于Linux用户来说至关重要。
程序员洲洲
2024/06/07
1850
【Linux核心宝典】Linux 系统目录结构详解 - 01
Linux的文件系统(3)
文件系统(File system)泛指储存在计算机上的文件和目录。文件系统可以有不同的格式,叫做文件系统类型(file system types)。这些格式决定信息是如何被储存为文件和目录。某些文件系统类型储存重复数据,某些文件系统类型加快硬盘驱动器的存取速度。
py3study
2020/01/14
3.1K0
Linux 文件系统详解
在LINUX系统中有一个重要的概念:一切都是文件。 其实这是UNIX哲学的一个体现,而Linux是重写UNIX而来,所以这个概念也就传承了下来。在UNIX系统中,把一切资源都看作是文件,包括硬件设备。UNIX系统把每个硬件都看成是一个文件,通常称为设备文件,这样用户就可以用读写文件的方式实现对硬件的访问。
黄规速
2022/04/14
10.8K0
Linux 文件系统详解
linux文件系统
Linux 文件系统是 Linux 操作系统中的重要组成部分,它是文件和目录的组织方式,为用户提供了一个良好的管理和访问文件的方式。Linux 文件系统具有以下特点:
堕落飞鸟
2023/04/01
8.9K0
Linux操作系统完整目录结构概览
Linux,作为一种开源的操作系统,其文件系统设计展现了极高的灵活性和效率。在每一个 Linux 系统中,都存在一套标准的目录结构,这套结构不仅映射了操作系统的运作机制,同时也为管理员与用户提供了一种便捷的文件管理方式。对于 Linux 的用户和管理者而言,掌握这些目录的知识是极其重要的一环。🧑‍💻
Lethehong
2025/03/20
1470
Linux操作系统完整目录结构概览
每日一博 - 导航Linux文件系统:根目录的子目录层次结构
Linux操作系统的根目录(/)是整个文件系统的起点,它包含了许多重要的子目录,每个子目录都有特定的作用和用途。以下是主要的根目录子目录及其作用:
小小工匠
2023/09/14
3580
每日一博 - 导航Linux文件系统:根目录的子目录层次结构
Linux根目录——详情介绍
在默认登陆的情况下是【/root】路径,我们使用【cd ..】的命令来返回到根目录下。
红目香薰
2023/10/11
3.2K0
Linux根目录——详情介绍
Linux常见根目录详释
在Linux系统中,根目录(/)是整个文件系统的顶级目录,所有其他目录和文件都位于其下。
久绊A
2025/03/12
3280
Linux文件
在Linux下,一切皆文件。这是我们嵌入式Linux开发与应用这门课的老师经常挂在嘴边的一句话。足以体现出在Linux操作系统中,对于一切资源的管理都是对文件的操作。
zy010101
2022/05/05
7K0
Linux文件
《Linux操作系统编程》 第五章 文件和文件系统: 了解文件和文件系统的概念和特性,掌握Linux文件系统的基本操作
​ 掌握:文件的访问权限及表示方式,目录操作(当前路径、读取目录项),属性获取以及文件类型。
猫头虎
2024/04/08
2680
《Linux操作系统编程》 第五章 文件和文件系统: 了解文件和文件系统的概念和特性,掌握Linux文件系统的基本操作
Linux之文件系统介绍
在上一篇Linux系列文章:Linux之硬件资源管理,主要介绍了查看硬件资源,配置硬件资源,磁盘管理及格式化,磁盘挂载,交换分区等基本命令。以下,主要介绍Linux文件系统相关命令。
可可的测试小栈
2020/04/15
1.7K0
二.Linux文件及目录管理
声明:本文为原创,作者为 对弈,转载时请保留本声明及附带文章链接:http://www.duiyi.xyz/linux%e6%96%87%e4%bb%b6%e5%8f%8a%e7%9b%ae%e5%bd%95%e7%ae%a1%e7%90%86/
对弈
2019/09/04
1.6K0
二.Linux文件及目录管理
Linux 系统目录结构
在 Linux 系统中,有几个目录是比较重要的,平时需要注意不要误删除或者随意更改内部文件。
用户8682940
2021/12/02
2.2K0
Linux文件系统目录结构详解
对于每一个Linux学习者来说,了解Linux文件系统的目录结构,是学好Linux的至关重要的一步.,深入了解linux文件目录结构的标准和每个目录的详细功能,对于我们用好linux系统只管重要,下面我们就开始了解一下linux目录结构的相关知识。
全栈程序员站长
2022/08/27
3.1K0
Linux文件系统目录结构详解
Linux文件系统浅析
在计算机出现之前其实就有文件系统的概念了,此时的文件系统指的是用于管理(存储和检索)纸质文件的系统,而在计算机发明之后,文件系统逐渐指的是管理存储介质的系统,它通过简单的接口给用户,方便用户使用存储设备。
wenzid
2021/06/06
3.1K0
Linux及文件系统基本介绍
用户1170933
2018/01/05
1.7K0
Linux及文件系统基本介绍
深入探究Linux树状目录结构
Linux 作为一款广泛使用的开源操作系统,其目录结构采用了树状设计,这种结构清晰、有条理,便于用户和系统进行文件管理与操作。
池央
2025/01/16
2030
深入探究Linux树状目录结构
相关推荐
必看!深入理解linux系统的目录结构就靠本文了
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验