---
# System prepended metadata

title: Flask學習筆記

---

# 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跳出就好
```