创建数据管道的经验教训
创建数据管道的经验教训
Photo by Suzy Hazelwood: https://www.pexels.com/photo/robot-toy-riding-a-scooter-2882361/
让我们想象一下,您是一家开发电动滑板车共享系统的初创公司的一员。这家初创公司的目标是在全球人口最多的城市开展业务。
那么最大的运营挑战是什么?让消费者可以使用电动滑板车,对吗?你可能在想,“当然,呃!”但这涉及到能够预测这些电动滑板车在使用后将“降落”在哪里。这就是它变得棘手的地方。
会使情况变得更加复杂的一些方面可能是:
- 在丘陵城市,用户倾向于使用踏板车上坡,然后步行下坡。
- 早上,居民区普遍向市中心移动。
- 每当开始下雨时,电动滑板车的使用量就会急剧下降。
- 每当带着背包年轻游客的飞机降落时,机场附近都需要大量的滑板车。
我收到了一个名为 Ganz 的虚构公司的案例场景,在这种情况下,需要根据天气和该大城市的航班到达情况来预测电动滑板车的使用情况。
为了使这些信息有用,需要实时收集并可供公司中的每个人使用。所以这就是为什么在我们的数据科学课程的数据工程部分,我们被要求 创造 并自动化云中的数据管道 .
在云中创建和自动化数据管道的步骤
Image source: WBS Coding School
- 从网络上抓取数据
- 使用 API 收集数据
- 使用 Pandas 数据框创建数据库模型
- 在本地 MySQL 上存储数据
- 使用 AWS 设置云数据库
- 将您的脚本移动到 Lambda
- 自动化管道
从网络上抓取数据
为了获取有关“大城市”的信息,我们需要能够从网页(例如 wiki)中抓取城市名称、国家/地区、纬度、经度和人口等信息。
如果使用以下方法来获取此信息的一种方法:漂亮的汤。
Beautiful Soup 是一个 Python 库,用于从 HTML 和 XML 文件中提取数据。它与您最喜欢的解析器一起使用,提供导航、搜索和修改解析树的惯用方式。它通常可以节省程序员数小时或数天的工作时间。资源: https://beautiful-soup-4.readthedocs.io/en/latest/
如果您想知道解析器是什么?我相信这个定义可以帮助理解它的功能:
为了让机器理解以人类可读形式编写的代码,必须将其转换为机器语言。此任务通常由翻译器(解释器或编译器)执行。解析器通常用作翻译器的一个组件,它将线性文本组织成易于操作的结构(解析树)。资源: https://www.techopedia.com/definition/3854/parser
我从网上抓取信息中吸取的教训:
-
如果您不了解 HTML,请学习如何理解和识别“标签”。这是 免费资源 你可以这样做。
-
使用检查功能读取您尝试抓取的网页(鼠标右键单击)。然后突出显示您要废弃的概念,并记下此信息所在的标签。
-
学习如何使用美丽的汤。这是一个链接到 文件
-
导航网页结构的另一种方法是使用以下内容:
从 BS4 进口 美丽汤
汤 = BeautifulSoup(html_doc, 'html.parser')打印(汤。美化())
回到案例研究,我们被要求废弃柏林和法兰克福的 wiki 页面。这是通过以下代码完成的:
target_cities = ['柏林','法兰克福']
国家 = []
状态 = []
City_State = []
城市 = []
地铁 = []
对于目标城市中的城市:
Country.append(get_html_wiki_text_label('Country', city))
States.append(get_html_wiki_text_label('State', city))
City_State.append(get_html_wiki_text_label('City/State', city))
Urban.append(get_html_wiki_text_label('Urban', city))
Metro.append(get_html_wiki_text_label('Metro', city)) df = pd.DataFrame({
“国家”:国家,
“国家”:国家,
'City_State':City_State,
“城市”:城市,
“米”:米
}
) df[['Countries', 'States', 'City_State', 'Urban', 'Metro']]].columns
使用 API 收集数据
让我们从回答什么是 API 开始:
API 是 使两个软件组件能够使用一组定义和协议相互通信的机制 .资源: https://aws.amazon.com/what-is/api/
这个 同一个链接 提供大量信息,以防您想了解更多信息。
获取天气信息的一种方法是使用免费天气预报,例如: 开放天气 .然而,为了从 API 获取信息,您需要创建一个帐户,验证您的电子邮件,然后您将拥有 API 密钥。
让我们再次考虑柏林的示例,并尝试使用 API 密钥获取天气信息。
#记得在开始之前导入 将熊猫导入为 pd
导入请求
从日期时间导入日期时间
进口pytz API_key = 'APIKey'
tz = pytz.timezone('欧洲/柏林')
现在 = datetime.now().astimezone(tz) weather_dict = {'city': [],
'国家': [],
'预测时间':[],
'外表': [],
'detailed_outlook': [],
'温度': [],
'温度感觉像':[],
'云':[],
'雨': [],
'雪': [],
'风速': [],
'wind_deg': [],
'湿度': [],
'压力': [],
'信息检索_at':[]} 城市 = ['柏林', '法兰克福']
对于城市中的城市:
网址 = (f"[ http://api.openweathermap.org/data/2.5/forecast?q={city}&appid={API_key}&units=metric](https://api.openweathermap.org/data/2.5/forecast?q=%7Bcity%7D&appid=%7BAPI_key%7D&units=metric) ")
响应 = requests.get(url)
json = response.json() 对于 json['list'] 中的 i:
weather_dict['city'].append(json['city']['name'])
weather_dict['country'].append(json['city']['country'])
weather_dict['forecast_time'].append(i['dt_txt'])
weather_dict['outlook'].append(i['weather'][0]['main'])
weather_dict['detailed_outlook'].append(i['weather'][0]['description'])
weather_dict['temperature'].append(i['main']['temp'])
weather_dict['temperature_feels_like'].append(i['main']['feels_like'])
weather_dict['clouds'].append(i['clouds']['all'])
尝试:
weather_dict['rain'].append(i['rain']['3h'])
除了:
weather_dict['rain'].append('0')
尝试:
weather_dict['snow'].append(i['snow']['3h'])
除了:
weather_dict['snow'].append('0')
weather_dict['wind_speed'].append(i['wind']['speed'])
weather_dict['wind_deg'].append(i['wind']['deg'])
weather_dict['湿度'].append(i['main']['湿度'])
weather_dict['pressure'].append(i['main']['pressure'])
weather_dict['information_retrieved_at'].append(now.strftime("%d/%m/%Y %H:%M:%S")) df = pd.DataFrame(weather_dict)
要收集您可以使用的航班信息 快速 API ,一个市场,您可以在其中获得免费请求的配额。从他们的市场你可以使用 航空数据盒 访问免费着陆航班信息。
如果您不想收费,请选择他们的基本计划并转到“端点部分以测试您的 API 并获取您需要在 API 上“调用”的代码。
终点部分将允许您按航班状态、出发日期进行搜索;还有更多选择。
Source: https://rapidapi.com/aedbx-aedbx/api/aerodatabox/
每个不同的端点都有不同的代码。记得选择 python / requests:
再次以柏林为例,此代码将获取有关机场位置的信息:
tz = pytz.timezone('欧洲/柏林')
现在 = datetime.now().astimezone(tz) 网址 = "[ https://aerodatabox.p.rapidapi.com/airports/search/term](https://aerodatabox.p.rapidapi.com/airports/search/term) " 查询字符串 = {"q":"柏林","limit":"10"} 标题 = {
"X-RapidAPI-Key": "APIKEY",
“X-RapidAPI-Host”:“aerodatabox.p.rapidapi.com”
} a = requests.request("GET", url, headers=headers, params=querystring) 打印(a.文本) json = a.json() 数据 = {
'icao' : [],
'iata' : [],
'姓名' : [],
'简称' : [],
'市政名称' : [],
'国家代码' : [],
'location_lat' : [],
'location_lon':[]
} 对于 json['items'] 中的项目:
数据['icao'].append(item['name'])
数据['iata'].append(item['iata'])
数据['name'].append(item['name'])
数据['shortName'].append(item['shortName'])
数据['municipalityName'].append(item['municipalityName'])
数据['countryCode'].append(item['countryCode'])
数据['location_lat'].append(item['location']['lat'])
数据['location_lon'].append(item['location']['lon']) df = pd.DataFrame(数据)
此代码将为您提供有关到达的信息:
tz = pytz.timezone('欧洲/柏林')
现在 = datetime.now().astimezone(tz) flight_dict = {'arrival_icao': [],
'arrival_time_local' : [],
'arrival_terminal' : [],
'出发城市' : [],
'departure_icao': [],
'departure_time_local' : [],
'航空公司': [],
'航班号' : [],
'data_retrived_on':[]
} def tomorrows_flight_arrivals(icao_list): 今天 = datetime.now().astimezone(timezone('Europe/Berlin')).date()
明天 = (今天 + timedelta(days=1)) list_for_df = [] icao_list 中的国际民航组织:
次 = [["08:00","09:00"],["09:00","12:00"]] 时间:
网址 = f"[ https://aerodatabox.p.rapidapi.com/flights/airports/icao/{icao}/{tomorrow}T{time[0]}/{tomorrow}T{time[1](https://aerodatabox.p.rapidapi.com/flights/airports/icao/%7Bicao%7D/%7Btomorrow%7DT%7Btime%5B0%5D%7D/%7Btomorrow%7DT%7Btime%5B1) ]}"
querystring = {"withLeg":"true","direction":"Arrival","withCancelled":"false","withCodeshared":"true","withCargo":"false","withPrivate":"false" }
标题 = {
'x-rapidapi-host': "aerodatabox.p.rapidapi.com",
'x-rapidapi-key':“e71ae2bda5mshb8d30afac2b8113p157923jsn81a8c60bc3fb”
}
response = requests.request("GET", url, headers=headers, params=querystring)
flight_json = response.json() 对于 flight_json['arrivals'] 中的航班:
航班字典 = {}
flight_dict['arrival_icao'] = icao
flight_dict['arrival_time_local'] = flight['arrival'].get('scheduledTimeLocal', None)
flight_dict['arrival_terminal'] = flight['arrival'].get('terminal', None)
flight_dict['departure_city'] = flight['departure']['airport'].get('name', None)
flight_dict['departure_icao'] = flight['departure']['airport'].get('icao', None)
flight_dict['departure_time_local'] = flight['departure'].get('scheduledTimeLocal', None)
flight_dict['airline'] = flight['airline'].get('name', None)
flight_dict['flight_number'] = flight.get('number', None)
flight_dict['data_retrieved_on'] = datetime.now().astimezone(timezone('Europe/Berlin')).date()
list_for_df.append(flights_dict) 返回 pd.DataFrame(list_for_df) icaos = ['EDDL', 'EGLL']
df = tomorrows_flight_arrivals(icaos)
使用 API 收集信息的经验教训:
- 了解如何进行“调用 API”,即如何请求信息。这个 关联 解释如何这样做。
- 了解 API 密钥上的参数。
- 为了从 Python 上的 API 请求信息,您需要导入“requests”库。这是链接到 文件 .
- 大多数 API 将使用 JSON 对象“发出响应”,但也有 其他格式 .
- 使用 JSON 查看器了解您收到的响应。我发现 这个 乐于助人。
- JSON 查看器将帮助您识别有用的信息,以便您可以专门提取这些信息,否则数据清理将更加困难。
使用 Pandas 创建数据框并将数据存储在本地 MySQL 上
我将这两个步骤合并在一起,因为我们希望能够首先在本地自动创建数据框到 MySQL 中。
为了连接 python 和 MySQL,您需要使用 SQL炼金术 .这意味着您将需要安装 add 然后将以下导入添加到笔记本顶部:
!pip 安装 SQLAlchemy !pip 安装 PyMySQL 从 sqlalchemy 导入 create_engine
导入 pymysql
现在让我们创建一个数据库,我们将在其中存储从 wiki Berlin 页面中删除的信息。
数据库 = 'NAME_of_database' 用户名 = 'Your_MySQL_username' 密码 = 'Your_MySQL_Password' sqlEngine = create_engine(f'mysql+pymysql://{username}:{password}[ @127](https://twitter.com/127) .0.0.1/{数据库}', pool_recycle=3600) dbConnection = sqlEngine.connect() 表名 = 'wiki_scraper' 尝试: frame = df.to_sql(tableName, dbConnection, if_exists='append', index = False); 除了 ValueError 作为 vx: 打印(vx) 例外情况除外: 打印(例如) 别的: print("数据已成功推送到表 %s。"%tableName); 最后: dbConnection.close()
要为天气和航班信息创建数据库,您只需将之前的代码添加到您拥有天气和/或航班代码的同一笔记本中,并更改您想用它创建的表格名称.
使用 Pandas 创建数据库并首先将其保存在本地的经验教训:
- 不要忘记您的 MySQL 帐户的密码。
- 使用 Pandas 推送数据并自动创建数据库比手动创建数据库更容易、更干净。
- 如果您的代码正常工作,那么您无法将数据本地推送到 MySQL 有几个原因。
- 如果您确实有问题,可能是因为您的 MySQL 用户名或密码不正确。
使用 AWS 设置云数据库
像往常一样的第一步是 报名 对于 AWS 账户,并确保您对其进行验证。
我们将使用 RDS 服务:
Amazon 关系数据库服务 (Amazon RDS) 是 一组托管服务,使在云中设置、操作和扩展数据库变得简单 .资源: AWS
Source: RDS AWS
在 AWS 云上创建数据库的步骤是:
- 在您的 AWS 控制台上搜索 RDS
- 选择“创建数据库”
- 选择“标准创建”
Source: AWS 中的 RDS
4.选择MySQL和最新版本
Source: AWS 中的 RDS
5. 选择“免费层”模板
Source: AWS 中的 RDS
6. 为您的实例命名并创建用户名和密码(不要忘记这一点!)
Source: AWS 中的 RDS
7. 如果您选择“免费套餐”,这些值将保留列出:
Source: AWS 中的 RDS
8. 不要更改存储值或连接性和 VPC(它们与默认值相同)
Source: AWS 中的 RDS
9. 在本例中,我们允许公共访问,我们将创建一个新的 VPC 安全名称。
Source: AWS 中的 RDS
10.在数据库身份验证上选择“密码身份验证”选项
11.忽略“附加配置”
12.选择“创建数据库”
如果创建此数据库需要超过几分钟的时间,请不要惊慌。但请注意,到目前为止,您刚刚创建了一个实例,然后需要创建数据库和表来存储有关城市、天气和航班的信息。
当您单击实例的名称时,您将如下所示:
创建新连接时,您需要复制“端点”下的信息并将其传递到您的 MySQL 中。像这样:
为确保您不会遇到任何连接问题,请确保您允许所有流量进入您的实例。
然后添加新规则
设置云 MySQL 实例的经验教训:
- 确保你一步一步走,不要分心
- 确保通过单击从 MySQL 创建的新连接来检查您建立的连接是否有效
将您的脚本移动到 Lambda
为了将数据插入云数据库,我建议您使用 AWS Lambda。
要了解 AWS Lambda 是什么,我建议您观看此内容 视频 .
首先,您需要创建一个角色。这些是您需要遵循的步骤:
- 在您的控制台中,搜索“IAM”
- 点击“角色”,然后点击“创建角色”
- 选择“AWS服务”
- 选择“Lambda”作为用例
- 选择下一步
- 点击“AdministratorAccess”,然后下一步
- 设置名称然后创建角色
然后您将创建 Lambda 函数。为此,请按照下列步骤操作:
- 搜索 Lambda 服务并点击它
- 选择创建功能
- 选择“从头开始创作”
- 然后也按照以下步骤操作:
5.然后点击“创建函数”
6. 你会做到以下几点。删除 index.js 下的代码并添加您的 lambda 函数。
您的 lambda 函数将如下所示:
def lambda_handler(事件,上下文):
然后您将需要添加完整的代码,例如:
导入json
将熊猫导入为 pd
导入请求
从 bs4 导入 BeautifulSoup
从 sqlalchemy 导入 create_engine def get_html_wiki_text_label(关键字,位置):
响应 = requests.get(f'[ https://en.wikipedia.org/wiki/{location}'](https://en.wikipedia.org/wiki/%7Blocation%7D') )
汤 = BeautifulSoup(response.content, 'html.parser')
我 = 0
对于 soup.select("th.infobox-label") 中的项目:
if(item.get_text()中的关键字):
return soup.select("td.infobox-data")[i].get_text()
我=我+1 def lambda_handler(事件,上下文):
target_cities = ['柏林','法兰克福']
国家 = []
状态 = []
City_State = []
城市 = []
地铁 = []
对于目标城市中的城市:
Country.append(get_html_wiki_text_label('Country', city))
States.append(get_html_wiki_text_label('State', city))
City_State.append(get_html_wiki_text_label('City/State', city))
Urban.append(get_html_wiki_text_label('Urban', city))
Metro.append(get_html_wiki_text_label('Metro', city)) df = pd.DataFrame({
“国家”:国家,
“国家”:国家,
'City_State':City_State,
“城市”:城市,
“米”:米
}
)
数据库 = 'Name_of_database' 用户名 = 'User_name_created_for_RDS'
密码 = 'Password_created_for_RDS'
主机 = 'Host_address_of_your_MySQL'
sqlEngine = create_engine(f'mysql+pymysql://{username}:{password}@{host}/{database}', pool_recycle=3600)
dbConnection = sqlEngine.connect()
表名 = 'wiki_city'
尝试:
frame = df.to_sql(tableName, dbConnection, if_exists='append', index = False);
除了 ValueError 作为 vx:
打印(vx)
例外情况除外:
打印(例如)
别的:
print("数据已成功推送到表 %s。"%tableName);
最后:
dbConnection.close() 返回 {
“状态代码”:200,
'body': json.dumps('来自 Lambda 的你好!')
}
打印(事件)
添加到完整代码后,单击部署以保存它。
7.然后点击test创建测试
8. 添加层,以便此实例可以理解您正在编写的代码
您需要使用的其他层来自 Keith 创建的关于 Klayers 的 GitHub 存储库。
9. 让我们添加 SQLAlchemy 层
10. 去这个 关联 并考虑您正在处理的区域,选择适当的包。就我而言,我使用伦敦作为地区。 (请注意,您需要在同一区域下创建所有实例)
11.点击html
12. 寻找 SQLAlchemy 并复制上面写着“arn...”之类的内容
13. 使用 ARN 选项添加新层
14. 你的函数有超时的风险,所以为了避免这种情况,请按照以下步骤操作:
添加更多时间,例如 3 分钟。
15.点击test,在执行结果上查看结果
This means your lambda function is working properly.
您需要为天气表和航班创建一个新的 Lambda 函数,重复上述步骤。
这是您的 Lambda 函数收集天气信息的样子:
导入json
将熊猫导入为 pd
导入请求
从 bs4 导入 BeautifulSoup
从 sqlalchemy 导入 create_engine
导入 pymysql API_key = 'APIweatherKey'
weather_dict = {'city': [],
'国家': [],
'预测时间':[],
'外表': [],
'detailed_outlook': [],
'温度': [],
'温度感觉像':[],
'云':[],
'雨': [],
'雪': [],
'风速': [],
'wind_deg': [],
'湿度': [],
'压力': []} def lambda_handler(事件,上下文):
城市 = ['柏林', '法兰克福']
对于城市中的城市:
网址 = (f"[ http://api.openweathermap.org/data/2.5/forecast?q={city}&appid={API_key}&units=metric](https://api.openweathermap.org/data/2.5/forecast?q=%7Bcity%7D&appid=%7BAPI_key%7D&units=metric) ")
响应 = requests.get(url)
json = response.json()
对于 json['list'] 中的 i:
weather_dict['city'].append(json['city']['name'])
weather_dict['country'].append(json['city']['country'])
weather_dict['forecast_time'].append(i['dt_txt'])
weather_dict['outlook'].append(i['weather'][0]['main'])
weather_dict['detailed_outlook'].append(i['weather'][0]['description'])
weather_dict['temperature'].append(i['main']['temp'])
weather_dict['temperature_feels_like'].append(i['main']['feels_like'])
weather_dict['clouds'].append(i['clouds']['all'])
尝试:
weather_dict['rain'].append(i['rain']['3h'])
除了:
weather_dict['rain'].append('0')
尝试:
weather_dict['snow'].append(i['snow']['3h'])
除了:
weather_dict['snow'].append('0')
weather_dict['wind_speed'].append(i['wind']['speed'])
weather_dict['wind_deg'].append(i['wind']['deg'])
weather_dict['湿度'].append(i['main']['湿度'])
weather_dict['pressure'].append(i['main']['pressure']) df = pd.DataFrame(weather_dict)
数据库 = 'Name_of_database' 用户名 = 'User_name_created_for_RDS'
密码 = 'Password_created_for_RDS'
主机 = 'Host_address_of_your_MySQL'
sqlEngine = create_engine(f'mysql+pymysql://{username}:{password}@{host}/{database}', pool_recycle=3600)
dbConnection = sqlEngine.connect()
表名 = '天气城市'
尝试:
frame = df.to_sql(tableName, dbConnection, if_exists='append', index = False);
除了 ValueError 作为 vx:
打印(vx)
例外情况除外:
打印(例如)
别的:
print("数据已成功推送到表 %s。"%tableName);
最后:
dbConnection.close()
这将是用于收集机场信息的 Lambda 函数:
导入json
将熊猫导入为 pd
导入请求
从 bs4 导入 BeautifulSoup
从 sqlalchemy 导入 create_engine 网址 = "[ https://aerodatabox.p.rapidapi.com/airports/search/term](https://aerodatabox.p.rapidapi.com/airports/search/term) " 查询字符串 = {"q":"柏林","limit":"10"} 标题 = {
"X-RapidAPI-Key": "API_key",
“X-RapidAPI-Host”:“aerodatabox.p.rapidapi.com”
} a = requests.request("GET", url, headers=headers, params=querystring) json = a.json() 数据 = {
'icao' : [],
'iata' : [],
'姓名' : [],
'简称' : [],
'市政名称' : [],
'国家代码' : [],
'location_lat' : [],
'location_lon':[]
} def lambda_handler(事件,上下文):
对于 json['items'] 中的项目:
数据['icao'].append(item['name'])
数据['iata'].append(item['iata'])
数据['name'].append(item['name'])
数据['shortName'].append(item['shortName'])
数据['municipalityName'].append(item['municipalityName'])
数据['countryCode'].append(item['countryCode'])
数据['location_lat'].append(item['location']['lat'])
数据['location_lon'].append(item['location']['lon']) df = pd.DataFrame(数据)
数据库 = 'Name_of_database' 用户名 = 'User_name_created_for_RDS'
密码 = 'Password_created_for_RDS'
主机 = 'Host_address_of_your_MySQL'
sqlEngine = create_engine(f'mysql+pymysql://{username}:{password}@{host}/{database}', pool_recycle=3600)
dbConnection = sqlEngine.connect()
表名 = 'flights_icao'
尝试:
frame = df.to_sql(tableName, dbConnection, if_exists='append', index = False);
除了 ValueError 作为 vx:
打印(vx)
例外情况除外:
打印(例如)
别的:
print("数据已成功推送到表 %s。"%tableName);
最后:
dbConnection.close()
这将是收集到达信息的功能:
对于最后一个 Lambda 函数,您需要使用 ARN 选项添加另一个层:
在云 AWS 上建立数据库的经验教训:
- 每一步都很重要,花时间在每一步上,尽量不要分散注意力
- 还要注意您正在创建所有内容的区域。可能是您在没有注意到的情况下从该地区改变,然后这可能会产生问题。
- 创建 Lambda 函数时,越简单越好。最好逐步进行,最后使用您之前创建的其他变量调用 Lambda 函数。
自动化管道
在这个阶段,您希望能够告诉您的实例,当 X 发生时执行此操作。因此,对于我们的示例,当我们想要“实时”跟踪天气和航班信息时,我们的 事件 那将 扳机 例如,我们的 Lambda 函数将是每分钟。
我建议你看 这个视频 有关如何设置 Amazon Event Bridge 的说明。
以下是有关如何创建它的步骤:
- 搜索事件桥并单击创建规则
- 定义一个名称,对于这个例子,我们将使用时间表作为我们的模式
- 选择以固定速率运行的计划选项
- 然后在target type上,选择AWS Service,搜索Lambda function,然后搜索你要关联的lambda function,点击next
- 暂时忽略标签
- 查看所有信息,然后单击创建规则
- 一旦创建,您将能够返回到您的 Lambda 函数并监控信息,并且在您的 MySQL 上,您将能够验证每分钟添加信息的方式。
从流水线自动化中吸取的经验教训:
- 如果一开始您看不清楚活动是否完全自动化,请不要着急,特别是如果安排的时间是每 5 分钟一次。
- 最好的部分是查看实际数据库如何获取所需信息,并且一切正常。
- 是的,有些步骤很长,但是一旦你掌握了它,即使你也会自动化。
尾注
我真的希望你能发现这很有帮助。把你的评论、意见和问题写给我。我很高兴将来能读到你。
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明