轉載:python + requests實現的接口自動化框架詳細教程

轉自https://my.oschina.net/u/3041656/blog/820023html

摘要: python + requests實現的接口自動化框架詳細教程java

前段時間因爲公司測試方向的轉型,由原來的web頁面功能測試轉變成接口測試,以前大多都是手工進行,利用postman和jmeter進行的接口測試,後來,組內有人講原先web自動化的測試框架移駕成接口的自動化框架,使用的是java語言,但對於一個學java,卻在學python的我來講,以爲python比起java更簡單些,因此,我決定本身寫python的接口自動化測試框架,因爲本人也是剛學習python,這套自動化框架目前已經基本完成了,因而進行一些總結,便於之後回顧溫習,有許多不完善的地方,也遇到了許多的問題,但願大神們多多指教下面我就進行今天的主要內容吧。(初學者的成功之路,哈哈哈~~)python

一、首先,咱們先來理一下思路。mysql

正常的接口測試流程是什麼?nginx

腦海裏的反應是否是這樣的:web

肯定測試接口的工具 —> 配置須要的接口參數 —> 進行測試 —> 檢查測試結果(有的須要數據庫輔助) —> 生成測試報告(html報告)sql

那麼,咱們就根據這樣的過程來一步步搭建咱們的框架。在這個過程當中,咱們須要作到業務和數據的分離,這樣才能靈活,達到咱們寫框架的目的。只要好好作,必定能夠成功。這也是我當初對本身說的。數據庫

接下來,咱們來進行結構的劃分。json

個人結構是這樣的,你們能夠參考下:api

 

​​​​​​  common:存放一些共通的方法

  result:執行過程當中生成的文件夾,裏面存放每次測試的結果

  testCase:用於存放具體的測試case

  testFile:存放測試過程當中用到的文件,包括上傳的文件,測試用例以及     數據庫的sql語句

  caselist:txt文件,配置每次執行的case名稱

                                       config:配置一些常量,例如數據庫的相關信息,接口的相關信息等

                                        readConfig: 用於讀取config配置文件中的內容

                                        runAll:用於執行case

 

 既然總體結構有了劃分,接下來就該一步步的填充整個框架了,首先,咱們先來看看config.ini和readConfig.py兩個文件,從他們入手,我的以爲比較容易走下去噠。

咱們來看下文件的內容是什麼樣子的:

[DATABASE] host = 50.23.190.57 username = xxxxxx password = ****** port = 3306 database = databasename  [HTTP] # 接口的url baseurl = http://xx.xxxx.xx port = 8080 timeout = 1.0  [EMAIL] mail_host = smtp.163.com mail_user = xxx@163.com mail_pass = ********* mail_port = 25 sender = xxx@163.com receiver = xxxx@qq.com/xxxx@qq.com subject = python content = "All interface test has been complited\nplease read the report file about the detile of result in the attachment." testuser = Someone on_off = 1

相信你們都知道這樣的配置文件,沒錯,全部一成不變的東西,咱們均可以放到這裏來。哈哈,怎麼樣,不錯吧。

如今,咱們已經作好了固定的「倉庫」。來保存咱們平時不動的東西,那麼,咱們要怎麼把它拿出來爲我所用呢?這時候,readConfig.py文件出世了,它成功的幫咱們解決了這個問題,下面就讓咱們來一睹它的廬山真面目吧。

import os import codecs import configparser proDir = os.path.split(os.path.realpath(__file__))[0] configPath = os.path.join(proDir, "config.ini") class ReadConfig: def __init__(self): fd = open(configPath) data = fd.read() # remove BOM if data[:3] == codecs.BOM_UTF8: data = data[3:] file = codecs.open(configPath, "w") file.write(data) file.close() fd.close() self.cf = configparser.ConfigParser() self.cf.read(configPath) def get_email(self, name): value = self.cf.get("EMAIL", name) return value def get_http(self, name): value = self.cf.get("HTTP", name) return value def get_db(self, name): value = self.cf.get("DATABASE", name) return value

怎麼樣,是否是看着很簡單啊,咱們定義的方法,根據名稱取對應的值,是否是so easy?!固然了,這裏咱們只用到了get方法,還有其餘的例如set方法,有興趣的同窗能夠本身去探索下,也能夠看看小編我本身的關於讀取配置文件的博文http://www.javashuo.com/article/p-kugnqdud-bq.html,這裏咱們就不在累述了。

話很少說,咱們先來看下common到底有哪些東西。

既然配置文件和讀取配置文件咱們都已經完成了,也看到了common裏的內容,接下來就能夠寫common裏的共通方法了,從哪一個下手呢?今天,咱們就來翻「Log.py」的牌吧,由於它是比較獨立的,咱們單獨跟他打交道,也爲了之後它能爲咱們服務打下良好基礎。

這裏呢,我想跟你們多說兩句,對於這個log文件呢,我給它單獨啓用了一個線程,這樣在整個運行過程當中,咱們在寫log的時候也會比較方便,看名字你們也知道了,這裏就是咱們對輸出的日誌的全部操做了,主要是對輸出格式的規定,輸出等級的定義以及其餘一些輸出的定義等等。總之,你想對log作的任何事情,均可以放到這裏來。咱們來看下代碼,沒有比這個更直接有效的了。

import logging from datetime import datetime import threading

首先,咱們要像上面那樣,引入須要的模塊,才能進行接下來的操做。

class Log: def __init__(self): global logPath, resultPath, proDir proDir = readConfig.proDir resultPath = os.path.join(proDir, "result") # create result file if it doesn't exist if not os.path.exists(resultPath): os.mkdir(resultPath) # defined test result file name by localtime logPath = os.path.join(resultPath, str(datetime.now().strftime("%Y%m%d%H%M%S"))) # create test result file if it doesn't exist if not os.path.exists(logPath): os.mkdir(logPath) # defined logger self.logger = logging.getLogger() # defined log level self.logger.setLevel(logging.INFO) # defined handler handler = logging.FileHandler(os.path.join(logPath, "output.log")) # defined formatter formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') # defined formatter handler.setFormatter(formatter) # add handler self.logger.addHandler(handler)

,如今,咱們建立了上面的Log類,在__init__初始化方法中,咱們進行了log的相關初始化操做。具體的操做內容,註釋已經寫得很清楚了(英文有點兒差,你們看得懂就行,嘿嘿……),這樣,log的基本格式已經定義完成了,至於其餘的方法,就靠你們本身發揮了,畢竟每一個人的需求也不一樣,咱們就只寫廣泛的共用方法啦。接下來,就是把它放進一個線程內了,請看下面的代碼:

class MyLog: log = None mutex = threading.Lock() def __init__(self): pass  @staticmethod def get_log(): if MyLog.log is None: MyLog.mutex.acquire() MyLog.log = Log() MyLog.mutex.release() return MyLog.log

看起來是否是沒有想象中的那樣複雜啊,哈哈哈,就是這樣簡單,python比java簡單了許多,這也是我爲何選擇它的緣由,雖然小編我也是剛剛學習,還有不少不懂的地方。關於python中線程的學習,不懂的同窗能夠點擊這裏http://www.javashuo.com/article/p-fcqcdsax-bg.html,進行學習哦。但願你們跟我一同進步。好了,至此log的內容也結束了,是否是感受本身棒棒噠~其實,不管何時,都不要感到懼怕,要相信「世上無難事只怕有心人」。

下面,咱們繼續搭建,此次要作的,是configHttp.py的內容。沒錯,咱們開始配置接口文件啦!(終於寫到接口了,是否是很開心啊~)

下面是接口文件中主要部分的內容,讓咱們一塊兒來看看吧。

import requests import readConfig as readConfig from common.Log import MyLog as Log localReadConfig = readConfig.ReadConfig() class ConfigHttp: def __init__(self): global host, port, timeout host = localReadConfig.get_http("baseurl") port = localReadConfig.get_http("port") timeout = localReadConfig.get_http("timeout") self.log = Log.get_log() self.logger = self.log.get_logger() self.headers = {} self.params = {} self.data = {} self.url = None self.files = {} def set_url(self, url): self.url = host + url def set_headers(self, header): self.headers = header def set_params(self, param): self.params = param def set_data(self, data): self.data = data def set_files(self, file): self.files = file # defined http get method def get(self): try: response = requests.get(self.url, params=self.params, headers=self.headers, timeout=float(timeout)) # response.raise_for_status() return response except TimeoutError: self.logger.error("Time out!") return None # defined http post method def post(self): try: response = requests.post(self.url, headers=self.headers, data=self.data, files=self.files, timeout=float(timeout)) # response.raise_for_status() return response except TimeoutError: self.logger.error("Time out!") return None

這裏咱們就挑重點來講吧。首先,能夠看到,小編此次是用python自帶的requests來進行接口測試的,相信有心的朋友已經看出來了,python+requests這個模式是很好用的,它已經幫咱們封裝好了測試接口的方法,用起來很方便。這裏呢,我就拿get和post兩個方法來講吧。(平時用的最多的就是這兩個方法了,其餘方法,你們能夠仿照着自行擴展)

  • get方法

        接口測試中見到最多的就是get方法和post方法,其中,get方法用於獲取接口的測試,說白了,就是說,使用get的接口,都不會對後臺數據進行更改,並且get方法在傳遞參數後,url的格式是這樣的:http://接口地址?key1=value1&key2=value2,是否是看起來很眼熟啊~(反正我看着它很眼熟~\(≧▽≦)/~啦啦啦),那咱們要怎麼使用它呢,請繼續往下看。

對於requests提供的get方法,有幾個經常使用的參數:

url:顯而易見,就是接口的地址url啦

headers:定製請求頭(headers),例如:content-type = application/x-www-form-urlencoded

params:用於傳遞測試接口所要用的參數,這裏咱們用python中的字典形式(key:value)進行參數的傳遞。

timeout:設置接口鏈接的最大時間(超過該時間會拋出超時錯誤)

如今,各個參數咱們已經知道是什麼意思了,剩下的就是往裏面填值啦,是否是機械式的應用啊,哈哈,小編我就是這樣機械般的學習的啦~

舉個栗子:

url=‘http://api.shein.com/v2/member/logout’
header={‘content-type’: application/x-www-form-urlencoded}
param={‘user_id’: 123456,‘email’: 123456@163.com}
timeout=0.5
requests.get(url, headers=header, params=param, timeout=timeout)
  • post方法

        與get方法相似,只要設置好對應的參數,就能夠了。下面就直接舉個栗子,直接上代碼吧:

url=‘http://api.shein.com/v2/member/login’
header={‘content-type’: application/x-www-form-urlencoded} data={‘email’: 123456@163.com,‘password’: 123456} timeout=0.5 requests.post(url, headers=header, data=data, timeout=timeout)

怎麼樣,是否是也很簡單啊。這裏咱們須要說明一下,post方法中的參數,咱們不在使用params進行傳遞,而是改用data進行傳遞了。哈哈哈,終於說完啦,下面咱們來探(了)討(解)下接口的返回值。

依然只說經常使用的返回值的操做。

text:獲取接口返回值的文本格式

json():獲取接口返回值的json()格式

status_code:返回狀態碼(成功爲:200)

headers:返回完整的請求頭信息(headers['name']:返回指定的headers內容)

encoding:返回字符編碼格式

url:返回接口的完整url地址

以上這些,就是經常使用的方法啦,你們可自行取之。

關於失敗請求拋出異常,咱們可使用「raise_for_status()」來完成,那麼,當咱們的請求發生錯誤時,就會拋出異常。在這裏提醒下各位朋友,若是你的接口,在地址不正確的時候,會有相應的錯誤提示(有時也須要進行測試),這時,千萬不能使用這個方法來拋出錯誤,由於python本身在連接接口時就已經把錯誤拋出,那麼,後面你將沒法測試指望的內容。並且程序會直接在這裏當掉,以錯誤來計。(別問我怎麼知道的,由於我就是測試的時候發現的)

好了。接口文件也講完了,是否是感受離成功不遠了呢?嗯,若是各位已經看到了這裏,那麼恭喜你們,下面還有很長的路要走~哈哈哈,就是這麼任性。(畢竟小編我爲了讓各位和我差很少的小白可以更容易理解,也是使出了體內的洪荒之力啦)

慢慢地長嘆一口氣,繼續下面的內容。。。

快,我想學(看)習(看)common.py裏的內容。

import os from xlrd import open_workbook from xml.etree import ElementTree as ElementTree from common.Log import MyLog as Log localConfigHttp = configHttp.ConfigHttp() log = Log.get_log() logger = log.get_logger() # 從excel文件中讀取測試用例 def get_xls(xls_name, sheet_name): cls = [] # get xls file's path xlsPath = os.path.join(proDir, "testFile", xls_name) # open xls file file = open_workbook(xlsPath) # get sheet by name sheet = file.sheet_by_name(sheet_name) # get one sheet's rows nrows = sheet.nrows for i in range(nrows): if sheet.row_values(i)[0] != u'case_name': cls.append(sheet.row_values(i)) return cls # 從xml文件中讀取sql語句 database = {} def set_xml(): if len(database) == 0: sql_path = os.path.join(proDir, "testFile", "SQL.xml") tree = ElementTree.parse(sql_path) for db in tree.findall("database"): db_name = db.get("name") # print(db_name) table = {} for tb in db.getchildren(): table_name = tb.get("name") # print(table_name) sql = {} for data in tb.getchildren(): sql_id = data.get("id") # print(sql_id) sql[sql_id] = data.text table[table_name] = sql database[db_name] = table def get_xml_dict(database_name, table_name): set_xml() database_dict = database.get(database_name).get(table_name) return database_dict def get_sql(database_name, table_name, sql_id): db = get_xml_dict(database_name, table_name) sql = db.get(sql_id) return sql

上面就是咱們common的兩大主要內容了,什麼?還不知道是什麼嗎?讓我告訴你吧。

  1. 咱們利用xml.etree.Element來對xml文件進行操做,而後經過咱們自定義的方法,根據傳遞不一樣的參數取得不(想)同(要)的值。
  2. 利用xlrd來操做excel文件,注意啦,咱們是用excel文件來管理測試用例的。

聽起來會不會有點兒懵,小編剛學時也很懵,看文件就好理解了。

excel文件:

xml文件:

至於具體的方法,我就再也不一點點講解了,總以爲你們都懂(小編剛學,望諒解),只是我我的須要詳細記錄,之後容易溫習。還有不會的小夥伴,能夠參見這兩篇文章學習下:excel:http://www.javashuo.com/article/p-ntpzovfd-ck.html  xml:http://www.javashuo.com/article/p-typheaez-kp.html

接下來,咱們看看數據庫和發送郵件吧(也可根據須要,不寫該部份內容)

先看老朋友「數據庫」吧。

小編此次使用的是MySQL數據庫,因此咱們就以它爲例吧。

import pymysql import readConfig as readConfig from common.Log import MyLog as Log localReadConfig = readConfig.ReadConfig() class MyDB: global host, username, password, port, database, config host = localReadConfig.get_db("host") username = localReadConfig.get_db("username") password = localReadConfig.get_db("password") port = localReadConfig.get_db("port") database = localReadConfig.get_db("database") config = { 'host': str(host), 'user': username, 'passwd': password, 'port': int(port), 'db': database } def __init__(self): self.log = Log.get_log() self.logger = self.log.get_logger() self.db = None self.cursor = None def connectDB(self): try: # connect to DB self.db = pymysql.connect(**config) # create cursor self.cursor = self.db.cursor() print("Connect DB successfully!") except ConnectionError as ex: self.logger.error(str(ex)) def executeSQL(self, sql, params): self.connectDB() # executing sql self.cursor.execute(sql, params) # executing by committing to DB self.db.commit() return self.cursor def get_all(self, cursor): value = cursor.fetchall() return value def get_one(self, cursor): value = cursor.fetchone() return value def closeDB(self): self.db.close() print("Database closed!")

這就是完整的數據庫的文件啦。由於小編的需求對數據庫的操做不是很複雜,因此這些已基本知足要求啦。注意下啦,在此以前,請朋友們先把pymysql裝起來!pymysql裝起來!pymysql裝起來!(重要的事情說三遍),安裝的方法很簡單,因爲小編是使用pip來管理python包安裝的,因此只要進入python安裝路徑下的pip文件夾下,執行如下命令便可:

pip install pymysql

哈哈哈,這樣咱們就能夠利用python連接數據庫啦~(鼓個掌,慶祝下)

小夥伴們發現沒,在整個文件中,咱們並無出現具體的變量值哦,爲何呢?沒錯,由於前面咱們寫了config.ini文件,全部的數據庫配置信息都在這個文件內哦,是否是感受很方便呢,之後就算變動數據庫了,也只要修改config.ini文件的內容就能夠了,結合前面測試用例的管理(excel文件),sql語句的存放(xml文件),還有接下來咱們要說的,businessCommon.py和存放具體case的文件夾,那麼咱們就已經將數據和業務分開啦,哈哈哈,想一想之後修改測試用例內容,sql語句神馬的工做,不再用每一個case都修改,只要改幾個固定的文件,是否是頓時開心了呢?(嗯,想笑就大聲的笑吧)

迴歸上面的configDB.py文件,內容很簡單,相信你們都能看得懂,就是鏈接數據庫,執行sql,獲取結果,最後關閉數據庫,沒有什麼不同的地方。還有不明白的朋友能夠查看這個連接進行學習:http://www.runoob.com/python/python-mysql.html

該談談郵件啦,你是否是也遇到過這樣的問題:每次測試完以後,都須要給開發一份測試報告。那麼,對於我這樣的懶人,是不肯意總是找人家開發的,因此,我就想,每次測試完,咱們可讓程序本身給開發人員發一封email,告訴他們,測試已經結束了,而且把測試報告以附件的形式,經過email發送給開發者的郵箱,這樣豈不是爽哉!

因此,configEmail.py應運而生。噹噹噹當……請看:

import os import smtplib from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from datetime import datetime import threading import readConfig as readConfig from common.Log import MyLog import zipfile import glob localReadConfig = readConfig.ReadConfig() class Email: def __init__(self): global host, user, password, port, sender, title, content host = localReadConfig.get_email("mail_host") user = localReadConfig.get_email("mail_user") password = localReadConfig.get_email("mail_pass") port = localReadConfig.get_email("mail_port") sender = localReadConfig.get_email("sender") title = localReadConfig.get_email("subject") content = localReadConfig.get_email("content") self.value = localReadConfig.get_email("receiver") self.receiver = [] # get receiver list for n in str(self.value).split("/"): self.receiver.append(n) # defined email subject date = datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.subject = title + " " + date self.log = MyLog.get_log() self.logger = self.log.get_logger() self.msg = MIMEMultipart('mixed') def config_header(self): self.msg['subject'] = self.subject self.msg['from'] = sender self.msg['to'] = ";".join(self.receiver) def config_content(self): content_plain = MIMEText(content, 'plain', 'utf-8') self.msg.attach(content_plain) def config_file(self): # if the file content is not null, then config the email file if self.check_file(): reportpath = self.log.get_result_path() zippath = os.path.join(readConfig.proDir, "result", "test.zip") # zip file files = glob.glob(reportpath + '\*') f = zipfile.ZipFile(zippath, 'w', zipfile.ZIP_DEFLATED) for file in files: f.write(file) f.close() reportfile = open(zippath, 'rb').read() filehtml = MIMEText(reportfile, 'base64', 'utf-8') filehtml['Content-Type'] = 'application/octet-stream' filehtml['Content-Disposition'] = 'attachment; filename="test.zip"' self.msg.attach(filehtml) def check_file(self): reportpath = self.log.get_report_path() if os.path.isfile(reportpath) and not os.stat(reportpath) == 0: return True else: return False def send_email(self): self.config_header() self.config_content() self.config_file() try: smtp = smtplib.SMTP() smtp.connect(host) smtp.login(user, password) smtp.sendmail(sender, self.receiver, self.msg.as_string()) smtp.quit() self.logger.info("The test report has send to developer by email.") except Exception as ex: self.logger.error(str(ex)) class MyEmail: email = None mutex = threading.Lock() def __init__(self): pass  @staticmethod def get_email(): if MyEmail.email is None: MyEmail.mutex.acquire() MyEmail.email = Email() MyEmail.mutex.release() return MyEmail.email if __name__ == "__main__": email = MyEmail.get_email() 

這裏就是完整的文件內容了,不過惋惜的是,小編我遇到一個問題,至今未解,在這裏提出,但願大神給出解決辦法!跪求啦!

問題:使用163免費郵箱服務器進行郵件的發送,可是,每次發送郵件,都會被163郵件服務器退信,拋出的錯誤碼是:554 

官方說明以下:

可是,however,but……小編在整合email進本框架以前寫的發送email的小demo是能夠正常發送郵件的。這個問題困擾着我,目前仍沒有解決,望大神賜教。

關於python對email的操做,上面代碼看不太明白的朋友,請移步這裏繼續學習:http://www.javashuo.com/article/p-xhgzaupj-kp.html

離成功不遠了,簡單說明下HTMLTestRunner.py文件,這個文件呢,也不是小編寫的,小編只是它的搬運工,哈哈哈,這個文件是從網上下載的,大神寫好的,用於生成html格式的測試報告,什麼?想知道生成測試報告的樣子?好,這就知足好奇的你:

看上去不錯吧,嗯,聰明的大家,也能夠本身去探索下這個文件,修改修改,變成你本身的style哦~

好了,重頭戲來了,就是咱們的runAll.py啦。請看主角登場。

這是咱們整個框架運行的入口,上面內容完成後,這是最後一步啦,寫完它,咱們的框架就算是完成了。(鼓掌,撒花~)

import unittest import HTMLTestRunner def set_case_list(self): fb = open(self.caseListFile) for value in fb.readlines(): data = str(value) if data != '' and not data.startswith("#"): self.caseList.append(data.replace("\n", "")) fb.close() def set_case_suite(self): self.set_case_list() test_suite = unittest.TestSuite() suite_model = [] for case in self.caseList: case_file = os.path.join(readConfig.proDir, "testCase") print(case_file) case_name = case.split("/")[-1] print(case_name+".py") discover = unittest.defaultTestLoader.discover(case_file, pattern=case_name + '.py', top_level_dir=None) suite_model.append(discover) if len(suite_model) > 0: for suite in suite_model: for test_name in suite: test_suite.addTest(test_name) else: return None return test_suite def run(self): try: suit = self.set_case_suite() if suit is not None: logger.info("********TEST START********") fp = open(resultPath, 'wb') runner = HTMLTestRunner.HTMLTestRunner(stream=fp, title='Test Report', description='Test Description') runner.run(suit) else: logger.info("Have no case to test.") except Exception as ex: logger.error(str(ex)) finally: logger.info("*********TEST END*********") # send test report by email if int(on_off) == 0: self.email.send_email() elif int(on_off) == 1: logger.info("Doesn't send report email to developer.") else: logger.info("Unknow state.") 

上面我貼出了runAll裏面的主要部分,首先咱們要從caselist.txt文件中讀取須要執行的case名稱,而後將他們添加到python自帶的unittest測試集中,最後執行run()函數,執行測試集。關於python的unittest,須要學的內容仍是不少的,因此這裏小編就不細講了,朋友們能夠移步這裏進行詳細的學習:https://docs.python.org/3/library/unittest.html  和  http://www.cnblogs.com/hero-blog/p/4128575.html  固然這裏只是小編給出的學習連接,你們能夠本身Google一下,方法刷刷刷的就出來了。

終於呢,整個接口自動化框架已經講完了,你們是否是看明白了呢?什麼?以前的以前貼出的目錄結構中的文件還有沒說到的?嘿嘿,,,相信不用小編多說,你們也大概知道了,剩下文件夾的做用了。嗯~思索萬千,仍是決定簡單談談吧。直接上圖,簡單明瞭:

   

result文件夾會在首次執行case時生成,而且之後的測試結果都會被保存在該文件夾下,同時每次測試的文件夾都是用系統時間命名,裏面包含了兩個文件,log文件和測試報告。

testCase文件夾下,存放咱們寫的具體的測試case啦,上面這些就是小編寫的一些。注意嘍,全部的case名稱都要以test開頭來命名哦,這是由於,unittest在進行測試時會自動匹配testCase文件夾下面全部test開頭的.py文件

   testFile文件夾下,放置咱們測試時用來管理測試用例的excel文件和用於數據庫查詢的sql語句的xml文件哦。

最後就是caselist.txt文件了,就讓大家瞄一眼吧:

凡是沒有被註釋掉的,都是要被執行的case名稱啦。在這裏寫上你要執行的case名稱就能夠啦。

呼~長舒一口氣,終於完成了整個過程,嗯,相信堅持看下來的大家必定會有所收穫的。在這裏,我要鄭重說一句:上文中提到的有關郵箱的問題,但願知道的大神賜教啊!!!

小編在最後弱弱的告訴你們哦:本博爲原創博文,轉載請標明出處哦。喜歡的朋友也能夠動動小手,給小編我點個贊吧,我會繼續努力學習,與你們共同成長噠!

 用的是Python3
相關文章
相關標籤/搜索