# Flask學習筆記 [官方](https://flask.palletsprojects.com/en/1.1.x/) 感覺不會用查這裡比較準 **大綱** [TOC] ## 網站基礎架構 上到下的順序 **瀏覽器** -使用者使用- **前端**(產生畫面)ex: js/css/http -由http發送要求到後端- **後端** ex: python/flask -發送要求到資料庫- **資料庫** ex: MongoDB/mySQL ### Web API 相關術語 * **HTTP** - 網際網路之間互相傳送訊息 * **URL** - 網址 * **JSON** - 一種輕量級的資料結構,用來傳輸由屬性值或者序列性的值組成的資料物件 * **REST** - 一種全球資訊網軟體架構風格,便於不同軟體/程式在網路中互相傳遞資訊。 ## virtualenv 要先架設虛擬環境 沒有用也可以動 有設會比較好,因為多文件開發,會需要不同的編譯環境,不同專案因需要不同版本環境而互相影響,就會"碰" ### Linux ``` $virtualenv flask $flask/bin/pip install flask ``` ### Windows上開始虛擬環境(名字是env) 好像又不能用了:( 參考來源[How to set up a virtual environments for Windows](https://programwithus.com/learn/python/pip-virtualenv-windows) ``` cd project //在project資料夾底下 virtualenv env \\env\Scripts\activate.bat //creates a batch file cd env cd Scripts activate pip install flask $deactivate //離開 ``` ### 換conda看看(要先載anaconda,一樣是在Windows) ``` conda create -n 專案名字 python=3.6 conda activate 專案名字 install flask(第一次用就好) ``` 結果我每次打開他都會幫我先`conda activate base` deactivate ``` conda deactivate ``` ## 開始用Flask ### 基本的 ```python= #!flask/bin/python from flask import Flask #載入flask app = Flask(__name__) #初始化 Flask 類別成為 instance #__name__ 這邊是用來定位目前載入資料夾的位置,用來判別 template__folder 或 static_folder 資料夾位置。 @app.route('/') #路由和處理函式配對 def index(): #回應網站首頁連線 return "Hello, World!" #網站首頁回應內容 if __name__ == '__main__': #啟動網站伺服器 app.run(debug=True) #debug=True ctrl+s可以直接改網站 ``` ### 執行 ``` $python app.py * Serving Flask app "app" (lazy loading) * Environment: production WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Debug mode: on * Restarting with stat * Debugger is active! * Debugger PIN: 263-040-831 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) 127.0.0.1 - - [23/Oct/2020 02:12:38] "GET / HTTP/1.1" 200 - 127.0.0.1 - - [23/Oct/2020 02:12:52] "GET / HTTP/1.1" 200 - 127.0.0.1 - - [23/Oct/2020 02:12:57] "GET / HTTP/1.1" 200 - ``` 127.0什麼的是連線指令等等的東西,包括案重新整理什麼的。 server端永遠不會停止,所以可以一直接受前端的訊息 ctrl+c之後再重新整理,就會出現無法連上網站 Warning的部分只要在跑的時候設定development環境,就不會再跳出來 (在powershell上設) ``` $env:FLASK_ENV = "development" ``` #### 更好的執行方法 拿掉最後的`if __name__ == '__main__': app.run(debug=True) ` 因為容易會有沒有原因的異常錯誤,或是會執行多次部分程式 可以改成 ``` $ export FLASK_APP=your_file.py $ flask run ``` 我是在vscode裡面的powershell跑的(在env前面要加錢錢符號才能動,但是我不懂錢錢符號是甚麼意思 ``` $env:FLASK_APP = "a" #檔名是a.py flask run ``` run後面可以加的參數 * –reload # 修改 py 檔後,Flask server 會自動 reload * –debugger # 如果有錯誤,會在頁面上顯示是哪一行錯誤 * –host # 可以指定允許訪問的主機IP,0.0.0.0 為所有主機的意思 * –port # 自訂網路埠號的參數 舉例 ``` flask run --reload --debugger --host 127.0.0.1 --port 5000 ``` 我如果像很多教學一樣用http://0.0.0.0:80/ 都會打不開... [官方推薦的方法](https://flask.palletsprojects.com/en/1.1.x/cli/) ### 寫成不同個py檔(Blueprints) #### 插播:import的用法 一個py檔就是一個module ```python= import [module] ``` 是直接把某個檔案裡所有東西抓來用,所以要用裡面某個函式時要加點(ex:`random.randint(0, 5)`) ps.relative import不能直接用import ```python= from [module] import [name1, name2, ...] ``` 把檔案裡的函式拿來用,所以不需要加點 (ex:`randint(0, 5)`) ```python= import [module] as [new_name] ``` 跟第一個一樣,只是名字換了新的 (ex:`new_name.randint(0, 5)`) 剩下不想寫了[import](https://medium.com/pyladies-taiwan/python-%E7%9A%84-import-%E9%99%B7%E9%98%B1-3538e74f57e3) 注意: 如果要用相對位置,要寫一個__init__.py在母資料夾 小心import的deadlock(兩個module互相import就會出現) #### 2個檔案example 以main.py跟a.py試試看 ```python= #main.py from flask import Flask import a #把a.py讀進來 app = Flask(__name__) a.init_app(app) #在a.裡面def一個大函式,這一行之後裡面的東西都可以用 @app.route('/') def index(): return 'hi!' if __name__ == "__main__": app.run(debug=True) ``` ```python= #a.py def init_app(app): @app.route('/a') def aaa(): return 'i am a' @app.route('/b') def bbb(): return 'i am b' ``` 在網頁上打http://127.0.0.1:5000/a就可以跑出"i am a" #### 龐大架構要用Blueprints (template是一個html檔,先放著以後再看[個人網頁模板教學](http://nckuacc.github.io/lastwork/)) 假設資料夾長這樣 ``` /flask ├── /view │ └── api.py └── main.py ``` main.py放在 flask/main.py ```python= #main.py from flask import Flask from flask import Blueprint from view.api import app2 #view底下的api.app中,拿出app2 app = Flask(__name__) @app.route('/') def index(): return "Hello index" app.register_blueprint(app2) #藍圖抓進來用 ``` api.py放在 flask/view/api.py ```python= from flask import Blueprint app2 = Blueprint('app2', __name__) #一個創建起來的藍圖 @app2.route('/app2') def show(): return "Hello Blueprint app2" ``` 在網頁上打http://127.0.0.1:5000/app2就可以跑出"Hello Blueprint app2" ### jsonify 我自己寫的 ```python= import flask from flask import jsonify #使用jsoonify app.config["JSON_AS_ASCII"] = False # 為了跑出中文 app = flask.Flask(__name__) app.config["DEBUG"] = True #創一個json family = { "living_city": "Tainan", "members": [ { "name": "David", }, { "name": "Susan", }, { "name": "Kevin", }, { "name": "Kathleen", } ] } @app.route('/', methods=['GET']) def home(): return "<h1>Hello !</h1>" @app.route('/show', methods=['GET']) def familly(): return jsonify(family) #inp是可以傳入的參數 @app.route('/show/<inp>', methods=['GET']) def inputt(inp): return jsonify(inp) app.run() ``` ### 傳送json 寫一個send,在python send.py之後會把json傳到指定網址 ```python= #send.py import requests me = {'last_name': 'Luo', 'first_name ': 'Xuan-Yu'} m = requests.get('http://127.0.0.1:5001/get/json', json=me) #如果要用post可以改成m = requests.post~~~~~~ #下面是debug用的 decoded_result = m.json() print('in send') print(decoded_result) ``` 在app.py中的函式 ```python= @app.route('/get/json', methods=['GET']) def get_json(): json_data = request.json #print是debug用的 print('in web') print(json_data) return jsonify(json_data) ``` request.json 取得接受到的json檔 ### Flask-sqlalchemy 先安裝sqlalchemy ``` pip install flask-sqlalchemy ``` 在檔案一開始要import ```python= from flask_sqlalchemy import SQLAlchemy ``` 連到哪個sql?(跨謀,先放著)是改後面那串 ``` app.config['SQLALCHEMY_DATABASE_URI'] = [DB_TYPE]+[DB_CONNECTOR]://[USERNAME]:[PASSWORD]@[HOST]:[PORT]/[DB_NAME] ``` 應該會長這樣 ps: sqlite:後三個斜線是相對位置,四個斜線是絕對位置(要設定母資料夾) ```python= app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///tmp/test.db' ``` 試試看能不能建立一個空的database ```python= from flask import Flask from flask import request from flask_sqlalchemy import SQLAlchemy app = Flask(__name__) db = SQLAlchemy() #在括號裡面打app的話代表甚麼都不initialize,跟只有括號有一點差別,目前是都可用 app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db' db.init_app(app) # ???? @app.route('/', methods=['GET']) def index(): return "Hello!" @app.route('/create', methods=['GET']) def indexx(): db.create_all() return 'ok' ``` 直接執行就會在tmp資料夾底下創一個test.db(若資料夾不存在就會出現Internal Server Error) [QUICKSTART](https://flask-sqlalchemy.palletsprojects.com/en/2.x/quickstart/) [SQLAlchemy的API](https://flask.palletsprojects.com/en/1.1.x/search/?q=SQLAlchemy%28app%29) #### 建立database與基本使用 寫一個class記錄要放甚麼東西 ```python= from flask_sqlalchemy import SQLAlchemy from flask import Flask, request import datetime import time app = Flask(__name__) # MySql datebase app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False #不會跑出警告 app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:///cars.db" db = SQLAlchemy(app) class Cars(db.Model): __tablename__ = "車車" plate_num = db.Column(db.String(30), primary_key=True) # 車牌 arriving_time = db.Column(db.DateTime, nullable=False) # 預計抵達時間 driver = db.Column(db.String(10), nullable=False) # 駕駛人 def __init__(self, plate_num, arriving_time, driver): self.plate_num = plate_num self.arriving_time = arriving_time self.driver = driver @app.route('/') def index(): # Create data db.create_all() return 'ok' @app.route('/add', methods=['GET']) def add(): c1 = Cars('FFF-353', datetime.datetime(2020, 11, 6, 21, 19, 15),'王小寶') c2 = Cars('BBB-353',datetime.datetime(2020, 11, 6, 21, 50, 15), '王大寶') c = [c1, c2] db.session.add_all(c) db.session.commit() return 'ok' @app.route('/find', methods=['GET']) def find(): query = Cars.query.filter_by(plate_num='AAA-303').first() #找車牌AAA-303的車 # print(query.plate_num) if query is None: #沒有找到的時候 return 'not found' else: #找到就印出駕駛人名字 return query.driver @app.route('/change', methods=['GET']) def change(): query = Cars.query.filter_by(plate_num='BBB-353').first() query.car_type = 1 #改變找到的query中的某個值 db.session.commit() return 'changed' @app.route('/delete/<inp>', methods=['GET']) def delete(inp): #記得要在函式的input參數這裡宣告從網址傳入的值 query = Cars.query.filter_by(plate_num=inp).first() db.session.delete(query) #刪掉找到的query db.session.commit() return 'deleted' ``` ps.如果重複新增同樣primary key的資料就會在網頁跑出internal sever error,以上面的例子來說,不能一直刷新/add這個頁面 #### Table的互相連結 如果要連接兩個table就要再寫一個table互相連結彼此 假設是a、b之間用atob連接 ```python= #in a db_a_atob = db.relationship("atob", backref="a") #in b db_b_atob = db.relationship("atob", backref="b") #in atob #要加上foreign key aid = db.Column(db.Integer, db.ForeignKey('a.aid'), nullable=False) bid = db.Column(db.Integer, db.ForeignKey('b.bid'), nullable=False) ``` 連結好之後 假如說要查第幾個連結的值 ```python= query = atob.query.filter_by(id=1).first() #取得第一筆資料 print (query.a.element) ``` 就會印出a的element(被連接的),query.a的a是backref ### 與html做連結 * server端 每個函式return那行加入render template,開頭也要import,template要放在同資料夾的templates底下 假如說從database取得某個table的所有資料並傳到html中 (positions和cars都是要傳入html中的資料,Positions和Cars是創建database的class名稱) ```python= @app.route('/') def index(): positions = Positions.query.all() cars = Cars.query.all() return render_template('index.html', positions=positions, cars=cars) ``` * 網頁端 以剛剛傳入的positions和cars的資料來說,如果要show出每一個資料 (位置是創建database時的database tablename) ```htmlembedded= {% for 位置 in positions %} <!-- 要顯示的內容--> {% endfor %} ``` 要取得值的話用{{ tablename.data }}(要在上面寫的迴圈之內) 不管是在show或是id等等取值一樣 ex: ```htmlembedded= <button type="button" class="form-control" id="pos_buttton" button_id="posInput{{ 位置.pid }}">{{ 位置.name }}</button> ``` button_id會變成Input+第n個數據的pid,然後顯示第n數據的name ## 遇到的問題 #### 如果執行(./xxxxx.py)出來的網址沒有用ctrl+c關閉 先輸入指令 ``` ps -fA | grep python ``` 看第二欄的pid再用 ``` kill -9 pid ``` 去結束就可以繼續使用 #### 把GET直接改成POST會出現Method Not Allowed ```python= @app.route('/', methods=['POST']) #這裡 ``` 可以改成 ```python= @app.route('/', methods=['GET','POST']) ``` 是因為是用這個路徑去到達這個網址,所以如果只允許POST就會出錯 那應該是要把POST應用在接收資料上,GET用來進入網站。 ## 其他補充: chmod a+x app.py (a+x a是所有用戶 +x是執行權限) ### POST and GET 有點像GET是明信片,POST是信封的感覺 * GET 以 Query String(一種Key/Vaule的編碼方式)加在要寄送的地址(URL)後面 會有安全性問題 * POST ### JSON格式 陣列可以用 [ ] 來寫入資料 物件可以用 { } 來寫入資料 name / value 是成對的,中間透過 (:) 來區隔 flask jsonify的參考:[在flask中使用jsonify和json.dumps的区别](https://blog.csdn.net/Duke_Huan_of_Qi/article/details/76064225) ### 更改位置 在app.run裡面改 ```python= app.run(host="127.0.0.1", port=5001) #會連到http://127.0.0.1:5001/ ``` ### SQL SQL 是應用程式與資料庫之間溝通的語言,大概就是用SQL下指令去對database幹嘛幹嘛 row- 一筆資料 column - 資料的一個部分 create的時候要定義好資料型態 NOT NULL資料不可為空 UNIQUE資料都不相同 CHECK有限制條件 Primary Key 可以是原本資料內的一個欄位,或是一個人造欄位 (與原本資料沒有關係的欄位),用確認每一筆資料 ### ORM Object Relational Mapping 用於實現物件導向程式語言裡不同類型系統的資料之間的轉換。 就是可以在程式語言中使用的虛擬資料庫 ## 在powershell上的指令 建virtual env相關 ``` > conda create -n project_name python=3.6 > conda env list //看哪些env > conda env remove -n project_name //刪除這個env > pip list //看有什麼套件 > conda activate project_name //進入env > conda deactivate //離開env ``` 切換到python終端機 ``` > python //就會變成>>>在用ctrl+z跳出就好 ```