— Python网页抓取教程:循序渐进 —
抓取网页入门其实挺简单的。在之前的文章中我们介绍了怎么用C#和JAVA两种方法来抓取网页,这一期给大家介绍一种更容易,也是使用最广泛的一种抓取方法,那就是Python。
说起Python,大家应该并不陌生,它是目前入门最简单的一种方法了,因为它是一种面向对象的语言。Python的类和对象比任何其他语言都更容易使用。此外,Python存在许多库,因而在Python中构建用于网页抓取的工具轻而易举。
在这篇Python网络抓取教程中,我们将分步骤讲解如何利用python来抓取目标数据。首先需要从页面源获取基于文本的数据,然后将其存储到文件中并根据设置的参数对输出进行排序。使用Python进行网页抓取时还有一些更高级功能的选项,这些将在最后概述,并提供一些使用上的建议。按照教程下面概述的步骤进行操作,您将能知道如何进行网页抓取。
Python网页抓取教程适用于所有操作系统。不同系统安装Python或开发环境时会略有不同,其它部分均无不同。
我们所说的网页抓取是什么?
网络抓取是收集公共数据的自动化过程。爬虫会在几秒钟内自动从目标网站中提取大量公共数据。
#构建网络爬虫:Python准备工作
在整个网络抓取教程中,将使用Python3.4以上版本,您可以此页面下载。
准确的说,我们使用了3.8.3,但任何3.4+版本都应该可以正常运行我们下面用到的代码。
对于Windows系统,安装Python时确保选中“PATH安装”。PATH安装将可执行项添加到默认的Windows命令提示符可执行项搜索中。然后Windows将识别诸如“pip”或“python”之类的命令,而无需用户将其指向可执行文件的目录(例如C:/tools/python/.../python.exe)。如果您已经安装了Python但没有勾选复选框,只需重新运行安装并选择修改。在第二页上选择“添加到环境变量”即可。
了解Python库
Python的一大优势在于可供选择的库很多。这些网页抓取用到的库现在已经用于数以万计的Python项目——仅在PyPI上,现在就有超过300,000个项目。您可以选择多种类型的Python网页抓取库:
●Requests
●Beautiful Soup
●lxml
●Selenium
01#Requests库
网页抓取首先向网站服务器发送HTTP请求(例如POST或GET ),该请求会返回一个包含所需数据的响应。但是,标准Python HTTP库难以使用,为了提高效率,需要大量代码行,这进一步加剧了已经存在的问题。
与其他HTTP库不同,Requests库通过减少代码行简化了发出此类请求的过程,使代码更易于理解和调试,而不会影响其有效性。使用pip命令就可以从终端内安装该库:
pip install requests
Requests库提供了发送HTTPGET和POST请求的简单方法。例如,发送HTTP Get请求的函数被恰当地命名为get():
import requests
response = requests.get("https://oxylabs.io/”)
print(response.text)
如果需要发布表单,可以使用post()方法轻松完成。表单数据可以作为字典发送,如下所示:
form_data = {'key1': 'value1', 'key2': 'value2'}
response = requests.post("https://oxylabs.io/ ", data=form_data)
print(response.text)
请求库还会使那些需要进行身份验证的代理变得非常容易使用。
proxies={'http': 'http://user:password@proxy.oxylabs.io'}
response = requests.get('http://httpbin.org/ip', proxies=proxies)
print(response.text)
但是这个库有一个局限性,它不解析提取的HTML数据,也就是说它不能将数据转换成更易读的格式进行分析。此外,它不能用于抓取纯JavaScript编写的网站。
02#Beautiful Soup
Beautiful Soup是一个Python库,它与解析器一起从HTML中提取数据,甚至可以将无效标记转换为解析树。但是,该库仅用于解析,不能以HTML文档/文件的形式从网络服务器请求数据。它主要与Python Requests库一起使用。需要注意的是,Beautiful Soup可以轻松查询和导航HTML,但仍需要解析器。以下示例演示了html.parser模块的使用,该模块是Python标准库的一部分。
#Part 1–使用Requests获取HTML
import requests
url='https://oxylabs.io/blog'
response = requests.get(url)
#Part 2–查找元素
from bs4 import BeautifulSoup
soup = BeautifulSoup(response.text, 'html.parser')
print(soup.title)
标题里的元素会输出如下:
<h1 class="blog-header">Oxylabs Blog</h1>
由于其导航、搜索和修改解析树方法均很简单,Beautiful Soup即使对于初学者也是十分不错的一个库,并且通常可以节省开发人员数小时的工作时间。例如,要输出此页面中的所有博客标题,就可以使用findAll()。在此页面上,会找到所有h2大小,且类属性为blog-card__content-title的博客标题。该信息可以配合findAll方法使用,如下所示:
blog_titles = soup.findAll('h2', attrs={"class":"blog-card__content-title"})
for title in blog_titles:print(title.text)
# Output:
# Prints all blog tiles on the page
BeautifulSoup还可以轻松使用CSS selectors。如果开发人员知道CSS selector,则无需学习find()或find_all()方法。以下是相同的示例,但使用的是CSS selectors:
blog_titles = soup.select('h2.blog-card__content-title')
for title in blog_titles:
print(title.text)
虽然能解析有问题的HTML是该库的主要功能之一,但它还提供了许多其它功能,包括检测页面编码,更进一步提高从HTML文件中提取数据的准确性。
更重要的是,它可以轻松配置,只需几行代码,即可提取任何自定义的公开可用数据或识别特定的数据类型。我们的Beautiful Soup教程包含有关此配置和其他配置的更多信息,以及该库的工作原理。
03#lxml
lxml是一个解析库。它是一个快速、强大且易于使用的库,适用于HTML和XML文件。此外,lxml是大量提取数据的理想选择。然而,与Beautiful Soup不同的是,这个库针对设计的不好的HTML可能会出现解析不了的情况。
可以使用以下pip命令从终端安装lxml库:
pip install lxml
这个库包含一个html模块来处理HTML。但是,lxml库首先需要HTML字符串。可以使用上一节中讨论的Requests库检索此HTML字符串。一旦HTML可用,就可以使用下面的fromstring方法构建树:
# After response = requests.get()
from lxml import html
tree = html.fromstring(response.text)
现在可以使用XPath查询此树。继续上一节中讨论的示例,要获取博客的标题,XPath将如下所示:
//h2[@class="blog-card__content-title"]/text()
可以将此XPath提供给tree.xpath()函数。这将返回与此XPath匹配的所有元素。注意XPath中的text()函数。该函数会提取h2元素内的文本。
blog_titles = tree.xpath('//h2[@class="blog-card__content-title"]/text()')
for title in blog_titles:
print(title)
假设您希望学习使用这个库并将其集成到您的网络抓取工作中,或者只是在您现有的专业知识基础上学习更多知识。您可以参见更详细的lxml教程。
04#Selenium
如上所述,一些网站是使用JavaScript编写的,JavaScript是一种允许开发者动态填充字段和菜单的语言。这给只能从静态网页中提取数据的Python库带来了问题。事实上,当涉及到JavaScript时,Requests库将无法使用。这个时候就是Selenium网络抓取的用武之地。
这个Python网络库是一个开源的浏览器自动化工具(网络驱动),它允许您自动执行诸如登录社交媒体平台之类的过程。Selenium广泛用于在应用程序上测试案例或测试脚本。它在网页抓取方面的优势源于它能够像任何浏览器一样通过运行JavaScript来呈现网页——标准的网络爬虫无法运行这种编程语言。目前Selenium已被开发人员广泛使用。
Selenium需要三个组件:
●浏览器–支持的浏览器有Chrome、Edge、Firefox和Safari。
●浏览器驱动程序-请参阅此页面以获取驱动程序的链接。
●Selenium安装包。
可以从终端安装selenium包:
pip install selenium
安装后,可以导入浏览器的相应类。导入后,必须创建类的对象。注意,这将需要可执行驱动程序的路径。Chrome浏览器示例如下:
from selenium.webdriver import Chrome
driver = Chrome(executable_path='/path/to/driver')
现在可以使用该get()方法在浏览器中加载任何页面。
driver.get('https://oxylabs.io/blog')
Selenium允许使用CSS Selectors和XPath来提取元素。以下示例使用CSS Selectors输出所有博客标题:
blog_titles = driver.get_elements_by_css_selector(' h2.blog-card__content-title')
for title in blog_tiles:
print(title.text)
driver.quit() # closing the browser
通过运行JavaScript,Selenium可以处理动态显示的任何内容,然后可用内置方法甚至Beautiful Soup对网页内容进行解析。此外,它还可以模仿用户的行为。
在网络抓取中使用Selenium的唯一缺点是它会减慢过程,因为它必须先为每个页面执行JavaScript代码,然后才能对其进行解析。因此,它不适合大规模的数据提取。但是,如果您希望小规模提取数据或者不在乎数据提取速度,那么Selenium是一个不错的选择。
支持网页抓取的Python库比较
对于这次的Python网页抓取教程,我们将使用三个重要的库——BeautifulSoup v4、Pandas和Selenium。请提前安装好这些库。如果您收到“NameError:name* is not defined”,则可能存在没安装成功的库。
#网络驱动程序和浏览器
每个网络爬虫都会使用浏览器,因为它需要连接到目标URL。出于测试目的,我们强烈建议使用常规浏览器(或不是无头浏览器),尤其是对于新手。查看编写的代码如何与应用程序交互可以进行简单的故障排除和调试,也有助于更好地理解整个过程。
无头浏览器可以在后面再使用,因为它们对于复杂的任务更有效。在本次网页抓取教程中,我们将使用Chrome浏览器,其实整个过程用Firefox浏览器也几乎相同。
首先,使用您喜欢的搜索引擎查找“Chrome(或Firefox)的网络驱动”。记下您浏览器的当前版本。下载与您的浏览器版本匹配的网络驱动程序。
如果适用,请选择所需的软件包,下载并解压缩。将驱动程序的可执行文件复制到任何易于访问的目录即可。操作是否正确,后面运行程序的时候就知道了。
为我们的Python网络爬虫寻找良好的编码环境
在我们进入本次网页抓取教程的编程部分之前,需要采取最后一步:使用良好的编码环境。有很多选择,从简单的文本编辑器(只需创建*.py文件并直接写下代码就足够了),到功能齐全的IDE(集成开发环境)。
如果您已经安装了Visual Studio Code,选择这个IDE将是最简单的选择。否则,我强烈建议新手使用PyCharm,因为它几乎没有入门门槛,并且有直观的用户界面。后面我们将使用PyCharm用于网页抓取教程。
在PyCharm中,右键单击项目区域并“新建->Python文件”。给它取个好听的名字!
Part 1 导入和使用库
是时候使用我们之前安装的所有包了:
import pandas as pd
from bs4 import BeautifulSoup
from selenium import webdriver
PyCharm可能会以灰色显示这些导入,因为它会自动标记未使用的库。不要接受PyCharm删除未使用的库的建议。
首先,定义我们的浏览器。根据我们在“网络驱动和浏览器”中选择的网络驱动,我们应该输入:
driver = webdriver.Chrome(executable_path='c:\path\to\windows\webdriver\executable.exe')
OR
driver = webdriver.Firefox(executable_path='/nix/path/to/webdriver/executable')
Part 2 选择一个网址
在执行我们第一次测试运行之前,选择一个URL。由于本次网页抓取教程旨在创建一个基本应用程序,我们强烈建议您选择一个简单的目标URL:
●避开隐藏在Javascript元素中的数据。这些数据有时需要通过执行特定操作来触发才能显示。从Javascript元素中抓取数据需要更复杂的Python使用方法及逻辑。
●避开抓取图像。图像可以直接用Selenium下载。
●在进行任何抓取活动之前,请确保您正在抓取的是公共数据,并且绝不会侵犯第三方权利。另外,不要忘记查看robots.txt文件获得指导。
选择您要访问的登录页面并将URL输入到driver.get('URL')参数中。Selenium要求提供连接协议。因此,始终需要将“http://”或“https://”附加到URL上。
driver.get('https://your.url/here?yes=brilliant')
尝试通过单击左下角的绿色箭头或右键单击编码环境并选择“运行”来进行测试运行。
如果您收到一条错误消息,指出文件丢失,请仔细检查驱动程序“webdriver.*”中提供的路径是否与可执行网络驱动的位置匹配。如果您收到版本不匹配的消息,请重新下载正确的可执行网络驱动。
Part 3 定义对象和构建列表
Python允许编码人员在不指定确切类型的情况下设计对象。可以通过简单地键入其标题并分配一个值来创建对象。
# Object is “results”, brackets make the object an empty list.
# We will be storing our data here.
results = []
Python中的列表是有序的、可变的并且允许复制列表中的成员。当然您也可以使用其他集合,例如集合或字典。但列表是最容易使用的。下面我们先来添加一些对象。
# Add the page source to the variable `content`.
content = driver.page_source
# Load the contents of the page, its source, into BeautifulSoup
# class, which analyzes the HTML as a nested data structure and allows to select
# its elements by using various selectors.
soup = BeautifulSoup(content)
我们回顾一下之前已经写好的代码:
import pandas as pd
from bs4 import BeautifulSoup
from selenium import webdriver
driver = webdriver.Chrome(executable_path='/nix/path/to/webdriver/executable')
driver.get('https://your.url/here?yes=brilliant')
results = []
content = driver.page_source
soup = BeautifulSoup(content)
重新运行应用程序,不应显示任何错误。如果出现任何问题,前面的章节中概述了一些可能的故障排除选项。
Part 4 使用Python网页抓取工具提取数据
这部分有趣而又困难——从HTML文件中提取数据。由于几乎在所有网页下,我们都会从页面的不同部分中提取需要的部分,并且我们希望将其存储到列表中,因此我们需要处理每个小的部分,然后将其添加到列表中:
# Loop over all elements returned by the `findAll` call. It has the filter `attrs` given
# to it in order to limit the data returned to those elements with a given class only.
for element in soup.findAll(attrs={'class': 'list-item'}):
...
“soup.findAll”可以接受各种参数。出于本教程的目的,我们仅使用“attrs”(属性)参数。它允许我们通过设置一个语句“如果属性等于X为真,则……”来缩小搜索范围。很容易就能找到和使用寻找的类,我们下面将会用到该参数。
在继续之前,让我们在真实的浏览器中访问所选的URL。然后使用CTRL+U(Chrome)打开页面源代码或右键单击并选择“查看页面源代码”。找到嵌套数据的“最近”类。另一种选择是按F12打开开发者工具来选择Element Picker。例如,它可以嵌套为:
<h4 class="title">
<a href="...">This is a Title</a>
</h4>
我们的属性“class”就是“title”。如果您选择了一个简单的目标,在大多数情况下,数据将以与上述示例类似的方式嵌套。获取复杂的目标数据可能需要更多尝试。让我们回到编码并添加我们在源代码中找到的类:
# Change ‘list-item’ to ‘title’.
for element in soup.findAll(attrs={'class': 'title'}):
...
我们的循环现在将遍历页面源中具有“title”类的所有对象。我们会处理每一个对象:
name = element.find('a')
让我们看看我们的循环是如何遍历HTML的:
<h4 class="title">
<a href="...">This is a Title</a>
</h4>
我们的第一个语句(在循环本身中)查找所有匹配标签的元素,其“class”属性包含“title”。然后我们在该类中执行另一个搜索。我们的第二次搜索查找文档中的所有标签(被包括在内,而像这样的部分匹配则不被包括在内)。最后,对象被分配给变量“name”。
然后,我们可以将对象名称分配给我们之前创建的列表数组“results”,但这样做会将整个标签及其内部的文本合并到一个元素中。在大多数情况下,我们只需要文本本身而不需要任何额外的标签。
# Add the object of “name” to the list “results”.
# `.text` extracts the text in the element, omitting the HTML tags.
results.append(name.text)
我们的循环将遍历整个页面源,找到上面列出的所有出现的类,然后将嵌套数据附加到我们的列表中:
import pandas as pd
from bs4 import BeautifulSoup
from selenium import webdriver
driver = webdriver.Chrome(executable_path='/nix/path/to/webdriver/executable')
driver.get('https://your.url/here?yes=brilliant')
results = []
content = driver.page_source
soup = BeautifulSoup(content)
for element in soup.findAll(attrs={'class': 'title'}):
name = element.find('a')
results.append(name.text)
请注意,循环后的两个语句是缩进的。循环需要缩进来表示嵌套。任何一致的缩进都将被视为合法。没有缩进的循环将输出“IndentationError”报错,并用“箭头”指出违规语句。
Part 5 导出数据
即使在运行我们的程序时没有出现语法或运行时的错误,仍然可能存在语义错误。您需要检查我们获得的数据是不是分配给指定对象并正确移动到数组的。
检查您获取的数据是否正确收集的最简单方法之一是使用“print”。由于数组有许多不同的值,因此通常使用一个简单的循环将每个条目分行进行输出:
for x in results:
print(x)
在这一点上,“print”和“for”是配合使用的。我们只是为了快速测试和调试目的进行循环。直接输出结果也是完全可行的:
print(results)
到目前为止,我们的代码应该是这样的:
driver = webdriver.Chrome(executable_path='/nix/path/to/webdriver/executable')
driver.get('https://your.url/here?yes=brilliant')
results = []
content = driver.page_source
soup = BeautifulSoup(content)
for a in soup.findAll(attrs={'class': 'class'}):
name = a.find('a')
if name not in results:
results.append(name.text)
for x in results:
print(x)
现在运行我们的程序应该不会报错,调试窗口中也应该会显示获取的数据。虽然“print”非常适合用于测试目的,但它对于解析和分析数据并不是很有用。
您可能已经注意到,到目前为止,“import pandas”仍然是灰色的。我们最终还是会充分利用库。建议现在删除“print”循环,因为接下来我们要做的事情与此类似,并且会将数据移动到csv文件。
df = pd.DataFrame({'Names': results})
df.to_csv('names.csv', index=False, encoding='utf-8')
我们的两个新语句依赖于pandas库。我们的第一个语句创建了一个变量“df”并将其对象转换为二维数据表。“Names”是我们列的名称,而“results”是我们要输出的列表。注意,pandas可以创建多个列,我们只是没有足够的列表来使用这些参数(目前)。
我们的第二个语句将变量“df”的数据移动到特定的文件类型(在本例中为“csv”)。我们的第一个参数为我们即将创建的文件分配一个名称和一个扩展名。添加扩展名是必要的,否则“pandas”将输出一个没有扩展名的文件,并且必须手动更改。“索引”可用于为列分配特定的起始编号。“编码”用于以特定格式保存数据。一般情况下使用UTF-8就足够了。
import pandas as pd
from bs4 import BeautifulSoup
from selenium import webdriver
driver = webdriver.Chrome(executable_path='/nix/path/to/webdriver/executable')
driver.get('https://your.url/here?yes=brilliant')
results = []
content = driver.page_source
soup = BeautifulSoup(content)
for a in soup.findAll(attrs={'class': 'class'}):
name = a.find('a')
if name not in results:
results.append(name.text)
df = pd.DataFrame({'Names': results})
df.to_csv('names.csv', index=False, encoding='utf-8')
现在所有导入的库应该都不是灰色的了,并且运行我们的应用程序可以将“names.csv”输出到我们的项目目录中。注意,“Guesed At Parser”警告仍然存在。我们可以通过安装第三方解析器来删除它,但对于本Python网页抓取教程而言,默认的HTML选项就可以了。
Part 6 更多清单
许多网页抓取操作需要获取多组数据。例如,仅提取电子商务网站上列出项目的标题几乎没用。为了收集有意义的信息并从中得出结论,至少需要两个数据点。
出于本教程的目的不同,我们将尝试一些稍微不同的代码。由于从同一个类中获取数据只是意味着一个额外的列表,我们应该尝试从不同的类中提取数据,但同时保持我们表的结构。
显然,我们需要另一个列表来存储我们的数据。
import pandas as pd
from bs4 import BeautifulSoup
from selenium import webdriver
driver = webdriver.Chrome(executable_path='/nix/path/to/webdriver/executable')
driver.get('https://your.url/here?yes=brilliant')
results = []
other_results = []
for b in soup.findAll(attrs={'class': 'otherclass'}):
# Assume that data is nested in ‘span’.
name2 = b.find('span')
other_results.append(name.text)
由于我们将从HTML的不同部分提取额外的数据点,因此我们需要一个额外的循环。如果需要,我们还可以添加另一个“if”条件来控制重复条目:
最后,我们需要改变我们的数据表的形成方式:
df = pd.DataFrame({'Names': results, 'Categories': other_results})
到目前为止,我们代码的最新迭代应该是这样的:
import pandas as pd
from bs4 import BeautifulSoup
from selenium import webdriver
driver = webdriver.Chrome(executable_path='/nix/path/to/webdriver/executable')
driver.get('https://your.url/here?yes=brilliant')
results = []
other_results = []
content = driver.page_source
for a in soup.findAll(attrs={'class': 'class'}):
name = a.find('a')
if name not in results:
results.append(name.text)
for b in soup.findAll(attrs={'class': 'otherclass'}):
name2 = b.find('span')
other_results.append(name.text)
df = pd.DataFrame({'Names': results, 'Categories': other_results})
df.to_csv('names.csv', index=False, encoding='utf-8')
现在可以试试看,如果一切顺利,运行此代码不会输出任何错误。在某些情况下,“pandas”会输出“ValueError:arrays must all be the same length”报错消息。简单来说,“results”和“other_results”列表的长度不相等,因此pandas无法创建二维表。
有多种方法可以解决该错误消息。从用“空”值填充最短列表到创建字典,再到创建两个系列并列出它们。我们选择第三种做法:
series1 = pd.Series(results, name = 'Names')
series2 = pd.Series(other_results, name = 'Categories')
df = pd.DataFrame({'Names': series1, 'Categories': series2})
df.to_csv('names.csv', index=False, encoding='utf-8')
请注意,数据不会匹配,因为列表长度不均匀,但如果需要两个数据点,创建两个系列是最简单的解决方法。我们的最终代码应该是这样的:
import pandas as pd
from bs4 import BeautifulSoup
from selenium import webdriver
driver = webdriver.Chrome(executable_path='/nix/path/to/webdriver/executable')
driver.get('https://your.url/here?yes=brilliant')
results = []
other_results = []
content = driver.page_source
soup = BeautifulSoup(content)
for a in soup.findAll(attrs={'class': 'class'}):
name = a.find('a')
if name not in results:
results.append(name.text)
for b in soup.findAll(attrs={'class': 'otherclass'}):
name2 = b.find('span')
other_results.append(name.text)
series1 = pd.Series(results, name = 'Names')
series2 = pd.Series(other_results, name = 'Categories')
df = pd.DataFrame({'Names': series1, 'Categories': series2})
df.to_csv('names.csv', index=False, encoding='utf-8')
运行它会创建一个名为“names”的csv文件,其中包含两列数据。
Part 7 使用Python进行网络抓取
我们的第一个网络抓取工具现在应该可以正常运行了。整个过程很基础,也很简单,所以执行一些重要的数据采集时需要编译更完善的代码。在进行更复杂的项目前,我强烈建议您尝试一些附加功能:
●通过创建可生成偶数长度列表的循环来创建匹配的数据提取。
●一次性抓取多个URL。有很多方法可以实现这样的功能。最简单的选择之一是简单地重复上面的代码并且每次更改URL。但是那样很费时间,也会很枯燥。可以构建一个循环和一组要访问的URL。
●另一种选择是创建多个数组来存储不同的数据集并将其输出到具有不同行的一个文件中。一次抓取几种不同类型的信息是电子商务数据获取的重要组成部分。
●一旦运行了令人满意的网络爬虫,您就不再需要在用浏览器查看,而是直接执行操作。获取Chrome或Firefox浏览器的无头版本,并使用它们来减少加载时间。
●创建爬取模式。想一想普通用户如何浏览互联网并尝试模拟他们的操作。当然这里会需要新的库。使用“import time”和“from random import randint”来创建页面之间的等待时间。添加“scrollto()”或使用特定的按键输入在浏览器中移动。在创建抓取模式时,很难列出所有可能的选项。
●创建监控流程。某些网站上的数据可能对时间(甚至用户)敏感。尝试创建一个持久的循环,以设定的时间间隔重新检查某些URL并抓取数据。确保您获取的数据始终是最新的。
●使用Python Requests库。Requests是网络抓取工具包中的重要组成部分,因为它允许优化发送到服务器的HTTP请求。
●最后,将代理集成到您的网络爬虫中。使用特定位置的请求源允许您获取可能无法访问的数据。
—— 总结 ——
看完我们的教程,您就可以自己写一些代码了。用Python构建网络爬虫、获取数据并从大量信息中得出结论其实是一个复杂但有趣的过程。
如果您想了解有关代理或高级数据采集工具如何工作的更多信息,或特定网络抓取案例,例如:网络抓取职位发布信息或构建黄页抓取工具的更多信息,请留意我们的微信,知乎和其它社交平台。
我们准备了不少优质的文章:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。