使用 Flask 開發 Python Web 應用程式:投票系統實戰

在現代 Web 開發領域,Python 因其簡潔易讀的語法與豐富的生態系統,成為許多開發者的首選語言。而在眾多 Python Web 框架中,Flask 以其輕量級、彈性高與容易上手的特性,贏得廣大開發者的喜愛。本文將帶領你從零開始,使用 Flask 框架建立一個功能完整的投票系統應用程式。

Flask 簡介:輕量級 Web 框架的優勢

Flask 是一個輕量級的 Python Web 框架,它提供了建立 Web 應用程式的核心功能,同時在擴充套件性上也相當出色。與 Django 等全功能框架不同,Flask 遵循「微框架」的設計理念,只提供核心功能,讓開發者能夠根據專案需求自由選擇其他元件。

在我多年的 Web 開發經驗中,Flask 的這種設計理念讓我在小型到中型專案中能夠快速迭代,避免了不必要的複雜性。特別是對於初學者或是需要快速原型開發的情境,Flask 絕對是一個值得考慮的選擇。

開始使用 Flask:環境設定與基礎知識

安裝 Flask

在開始我們的投票系統專案前,首先需要安裝 Flask 及其相關依賴。我建議使用虛擬環境來管理專案依賴,這樣可以避免不同專案間的套件衝突。以下是設定步驟:

# 建立並啟動虛擬環境
python -m venv venv
source venv/bin/activate  # 在 Windows 上使用 venv\Scripts\activate

# 安裝 Flask
pip install flask

使用 pip 安裝必要套件

除了 Flask 核心套件外,我們的投票系統還需要安裝一些額外的套件:

# 安裝 Flask-SQLAlchemy 用於資料函式庫
pip install flask-sqlalchemy

# 安裝 Flask-WTF 用於表單處理
pip install flask-wtf

# 安裝 pytest 用於單元測試
pip install pytest

Survey:使用 Flask 開發簡易投票應用程式

在這個實作中,我們將建立一個名為「Survey」的投票應用程式,讓使用者能夠建立投票、檢視投票列表、參與投票並檢視結果。這個應用程式涵蓋了 Web 開發的基本要素,包括資料函式庫、表單處理、路由設計以及範本渲染。

基礎檔案結構

良好的專案結構是開發可維護應用程式的關鍵。以下是我們投票系統的基本檔案結構:

survey/
  ├── app.py              # 應用程式入口
  ├── config.py           # 設定檔案
  ├── models.py           # 資料函式庫
  ├── views.py            # 檢視函式
  ├── forms.py            # 表單類別
  ├── static/             # 靜態檔案(CSS、JavaScript)
  ├── templates/          # HTML 範本
  │   ├── base.html       # 基礎範本
  │   ├── index.html      # 首頁範本
  │   ├── create.html     # 建立投票範本
  │   ├── detail.html     # 投票詳情範本
  │   └── vote.html       # 投票頁面範本
  └── tests/              # 單元測試
      └── test_app.py     # 應用程式測試

這種結構將不同功能的程式碼分離開來,使得專案更加模組化,便於維護和擴充套件。

建構應用程式

首先,我們來建立應用程式的入口檔案 app.py

from flask import Flask
from config import Config

app = Flask(__name__)
app.config.from_object(Config)

from models import db
db.init_app(app)

from views import *

if __name__ == '__main__':
    app.run(debug=True)

接著,在 config.py 中定義設定:

class Config:
    SECRET_KEY = 'your-secret-key'
    SQLALCHEMY_DATABASE_URI = 'sqlite:///survey.db'
    SQLALCHEMY_TRACK_MODIFICATIONS = False

使用 Flask-SQLAlchemy 撰寫資料模型

Flask-SQLAlchemy 是 Flask 的擴充套件,它為 Flask 應用程式提供了 SQLAlchemy 的支援。SQLAlchemy 是 Python 中最流行的 ORM(物件關聯對映)工具,它允許我們使用 Python 類別來操作資料函式庫

定義資料模型

models.py 中,我們定義投票系統所需的資料模型:

from flask_sqlalchemy import SQLAlchemy
from datetime import datetime

db = SQLAlchemy()

class Question(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    choices = db.relationship('Choice', backref='question', lazy=True, cascade='all, delete-orphan')

    def __repr__(self):
        return f'<Question {self.title}>'

class Choice(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    text = db.Column(db.String(200), nullable=False)
    votes = db.Column(db.Integer, default=0)
    question_id = db.Column(db.Integer, db.ForeignKey('question.id'), nullable=False)

    def __repr__(self):
        return f'<Choice {self.text}>'

建立資料函式庫

在我們的應用程式中,資料函式庫已經在 models.py 中建立,並在 app.py 中初始化。這種分離的方式可以避免迴圈參照的問題。

建立投票系統資料模型

我們的投票系統有兩個主要模型:QuestionChoiceQuestion 模型代表一個投票問題,它包含標題和建立時間。Choice 模型代表投票選項,它包含選項文字、得票數以及與問題的關聯。

這種一對多的關係透過 db.relationshipdb.ForeignKey 來實作。db.relationshipQuestion 模型中定義,它建立了從問題到選項的反向參照,使得我們可以輕鬆地透過問題取得所有相關選項。

在資料函式庫立表格

要在資料函式庫立表格,我們需要在應用程式連貫的背景與環境中執行 db.create_all() 方法。我們可以在 app.py 中新增以下程式碼:

with app.app_context():
    db.create_all()

或者,你也可以建立一個單獨的指令碼來初始化資料函式庫

from app import app
from models import db

with app.app_context():
    db.create_all()
    print("Database tables created.")

查詢資料函式庫

使用 Flask-SQLAlchemy,我們可以方便地查詢資料函式庫下是一些常見的查詢操作:

# 取得所有問題
questions = Question.query.all()

# 取得特定 ID 的問題
question = Question.query.get(1)

# 按標題搜尋問題
questions = Question.query.filter(Question.title.contains('Flask')).all()

# 按建立時間排序問題
questions = Question.query.order_by(Question.created_at.desc()).all()

# 取得問題的所有選項
question = Question.query.get(1)
choices = question.choices

檢視函式:處理 HTTP 請求的核心

檢視函式是 Flask 應用程式的核心,它們接收 HTTP 請求並回傳回應。在我們的投票系統中,我們需要定義多個檢視函式來處理不同的功能。

views.py 中,我們定義以下檢視函式:

from flask import render_template, redirect, url_for, request, flash
from app import app
from models import db, Question, Choice
from forms import QuestionForm, VoteForm

@app.route('/')
def index():
    questions = Question.query.order_by(Question.created_at.desc()).all()
    return render_template('index.html', questions=questions)

@app.route('/question/new', methods=['GET', 'POST'])
def new_question():
    form = QuestionForm()
    if form.validate_on_submit():
        question = Question(title=form.title.data)
        for choice_text in form.choices.data:
            if choice_text.strip():
                choice = Choice(text=choice_text)
                question.choices.append(choice)
        db.session.add(question)
        db.session.commit()
        flash('Question created successfully!', 'success')
        return redirect(url_for('index'))
    return render_template('create.html', form=form)

@app.route('/question/<int:id>')
def show_question(id):
    question = Question.query.get_or_404(id)
    return render_template('detail.html', question=question)

@app.route('/question/<int:id>/edit', methods=['GET', 'POST'])
def edit_question(id):
    question = Question.query.get_or_404(id)
    form = QuestionForm(obj=question)
    
    if form.validate_on_submit():
        question.title = form.title.data
        # 處理選項更新邏輯...
        db.session.commit()
        flash('Question updated successfully!', 'success')
        return redirect(url_for('show_question', id=question.id))
    
    return render_template('create.html', form=form, question=question)

@app.route('/question/<int:id>/delete', methods=['POST'])
def delete_question(id):
    question = Question.query.get_or_404(id)
    db.session.delete(question)
    db.session.commit()
    flash('Question deleted successfully!', 'success')
    return redirect(url_for('index'))

@app.route('/question/<int:id>/vote', methods=['GET', 'POST'])
def vote_question(id):
    question = Question.query.get_or_404(id)
    form = VoteForm()
    form.choice_id.choices = [(choice.id, choice.text) for choice in question.choices]
    
    if form.validate_on_submit():
        choice = Choice.query.get(form.choice_id.data)
        choice.votes += 1
        db.session.commit()
        flash('Vote recorded successfully!', 'success')
        return redirect(url_for('show_question', id=question.id))
    
    return render_template('vote.html', question=question, form=form)

這些檢視函式處理了我們投票系統的各種功能,包括:

問題列表

index() 檢視函式取得所有問題並按建立時間降序排列,然後渲染 index.html 範本。

建立新投票

new_question() 檢視函式處理建立新投票的 GET 和 POST 請求。當使用者提交表單時,它建立新的 Question 和相關的 Choice 例項,並將它們儲存到資料函式庫

顯示投票詳情

show_question() 檢視函式取得指定 ID 的問題,並渲染 detail.html 範本顯示問題詳情和投票結果。

更新投票

edit_question() 檢視函式處理更新現有投票的 GET 和 POST 請求。它首先取得指定 ID 的問題,然後根據表單提交的資料更新問題和選項。

刪除投票

delete_question() 檢視函式處理刪除投票的 POST 請求。它取得指定 ID 的問題,從資料函式庫除它,然後重定向到問題列表。

投票表單與投票處理

vote_question() 檢視函式處理使用者投票的 GET 和 POST 請求。它首先取得指定 ID 的問題,並為表單設定選項。當使用者提交表單時,它增加選擇選項的票數,並重定向到問題詳情頁面。

範本:構建使用者介面

在 Flask 中,我們使用 Jinja2 範本引擎來渲染 HTML。Jinja2 允許我們在 HTML 中使用變數、條件陳述式和迴圈,使得範本更加動態和靈活。

基礎範本

首先,我們建立一個基礎範本 base.html,它包含了所有頁面分享的 HTML 結構和樣式:

<!DOCTYPE html>
<html lang="zh-TW">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}Survey App{% endblock %}</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2

## 為何Requests套件成為Python網路互動標準

在Python網路開發的世界中Requests套件已成為處理HTTP請求的黃金標準當我初次面對複雜的網路爬蟲專案時從標準函式庫llib2轉向Requests的決定讓我的程式碼量減少近一半同時增加了可讀性今天玄貓將深入解析這個強大工具的核心特性並透過例項展示它如何簡化我們與網路的互動

### urllib2與Requests的優雅差異

讓我們先看一個基本的對比例子這個例子極好地展示了為何Requests套件如此受歡迎

使用urllib2進行基本驗證的方式

```python
# -*- coding: utf-8 -*-
import urllib2
gh_url = 'https://api.github.com'

req = urllib2.Request(gh_url)
password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
password_manager.add_password(None, gh_url, 'user', 'pass')
auth_manager = urllib2.HTTPBasicAuthHandler(password_manager)
opener = urllib2.build_opener(auth_manager)
urllib2.install_opener(opener)
handler = urllib2.urlopen(req)
print handler.getcode()
print handler.headers.getheader('content-type')
# 輸出
# 200
# 'application/json'

而使用Requests實作相同功能:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import requests
r = requests.get('https://api.github.com', auth=('user', 'pass'))
print r.status_code
print r.headers['content-type']
# 輸出:
# 200
# 'application/json'

這個對比令人震撼。同樣是實作HTTP基本驗證,urllib2需要建立多個物件並進行繁瑣的設定,而Requests只需一行程式碼就能完成相同功能。在處理大型網路應用時,這種簡潔效能顯著提升開發速度和程式碼可維護性。

Requests的核心優勢

現代HTTP特性的無縫整合

Requests套件的設計理念是讓HTTP/1.1的所有優勢自動為開發者所用。在我設計某金融機構的API監控系統時,Requests的這些特性為我節省了大量工作:

  1. 連線重用 - 自動重用TCP連線,減少連線建立的開銷
  2. Keep-alive機制 - 維持連線活躍,避免反覆建立連線的成本
  3. 連線池管理 - 透過內建的urllib3提供高效的連線池

更重要的是,Requests讓開發者不必關注這些底層細節,它處理了:

  • 查詢字串的自動編碼
  • 表單資料的格式化
  • Cookie管理
  • 多種身分驗證機制
  • SSL驗證
  • 檔案上載

建立第一個請求

讓我們從最基本的網頁請求開始:

>>> import requests
>>> r = requests.get('http://google.com')

就這麼簡單!這兩行程式碼完成了一個完整的HTTP GET請求。變數r是一個回應物件,包含了伺服器回應的所有資訊:

  • 標頭資訊
  • 回應內容
  • 編碼類別
  • 狀態碼
  • URL資訊

Requests支援所有主要的HTTP方法:GET、POST、PUT、DELETE、HEAD,使用方式一致與直觀。

引數傳遞的簡化方式

在網路請求中,URL引數傳遞是常見需求。Requests使用params關鍵字讓這個過程變得簡單:

parameters = {'key1': 'value1', 'key2': 'value2'}
r = requests.get('url', params=parameters)

以GitHub API為例,我們可以這樣取得使用者資訊:

>>> r = requests.get('https://api.github.com/user', auth=('myemailid.mail.com', 'password'))
>>> r.status_code
200
>>> r.url
u'https://api.github.com/user'
>>> r.request
<PreparedRequest [GET]>

這裡我們使用了auth元組來提供基本驗證資訊,r.status_code的200結果表示請求成功。

回應內容的人工智慧處理

在網路互動中,伺服器回應的解碼和處理是關鍵挑戰。Requests提供了多種方式來處理不同類別的回應內容。

文字與二進位回應

Requests會自動處理多種Unicode字元集的解碼工作:

>>> import requests
>>> r = requests.get('https://google.com')
>>> r.content  # 原始二進位內容
'<!doctype html><html itemscope="" itemtype="http://schema.org/WebPage" .....'
>>> type(r.content)
<type 'str'>
>>> r.text  # 解碼後的文字內容
u'<!doctype html><html itemscope=""\ itemtype="http://schema.org/WebPage" lang="en-IN"><head><meta content="........
>>> type(r.text)
<type 'unicode'>

這裡r.content提供原始的二進位回應,而r.text則是Requests根據編碼轉換後的Unicode字串。

編碼管理

Requests允許我們查詢和修改它用於解碼的編碼方式:

>>> r.encoding  # 檢視當前使用的編碼
'ISO-8859-1'
>>> r.encoding = 'utf-8'  # 修改編碼

當我們修改r.encoding後,Requests會在下次存取r.text時使用新的編碼進行解碼。

如果r.encoding設為None,Requests會使用r.apparent_encoding的值,這是由chardet函式庫出來的編碼:

>>> r.encoding = None
>>> r.apparent_encoding
'ascii'

需要注意的是,r.apparent_encoding是隻讀屬性,嘗試修改它會引發AttributeError

>>> r.apparent_encoding = 'ISO-8859-1'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

Requests的強大之處在於它能夠使用自定義編碼。如果我們有自己註冊的編碼器,Requests可以無縫使用它們來處理回應內容。

傳送不同類別的請求內容

自定義標頭

在實際開發中,我們經常需要傳送帶有特定標頭的請求。Requests讓這個過程變得簡單:

>>> import json
>>> url = 'https://api.github.com/some/endpoint'
>>> payload = {'some': 'data'}
>>> headers = {'Content-Type': 'application/json'}
>>> r = requests.post(url, data=json.dumps(payload), headers=headers)

在這個例子中,我們傳送了一個POST請求,並指定了Content-Type標頭為application/json

同樣地,如果我們需要傳送帶有授權標頭的請求:

>>> url = 'some url'
>>> header = {'Authorization': 'some token'}
>>> r = requests.post(url, headers=header)

表單編碼資料

HTML表單提交是網頁互動的基礎。Requests可以輕鬆模擬表單提交:

>>> payload = {'key1': 'value1', 'key2': 'value2'}
>>> r = requests.post("some_url/post", data=payload)
>>> print(r.text)
{
...
"form": {
  "key2": "value2",
  "key1": "value1"
},
...
}

在這個例子中,字典payload被自動轉換為表單編碼的資料。如果我們要傳送非表單編碼的資料,應該使用字串而非字典。

上載多部分編碼檔案

檔案上載是另一個常見需求。Requests透過files引數支援多部分編碼的檔案上載:

>>> url = 'some api endpoint'
>>> files = {'file': open('plan.csv', 'rb')}
>>> r = requests.post(url, files=files)
>>> r.text
{
...
"files": {
  "file": "< some data ... >"
},
....
}

我們還可以指定內容類別和其他標頭:

>>> url = 'some url'
>>> files = {'file': ('plan.csv', open('plan.csv', 'rb'), 'application/csv', {'Expires': '0'})}
>>> r = requests.post(url, files=files)

甚至可以傳送字串作為檔案:

>>> url = 'some url'
>>> files = {'file': ('plan.csv', 'some, strings, to, send')}
>>> r = requests.post(url, files=files)

狀態碼處理與異常管理

HTTP狀態碼是瞭解請求結果的關鍵。Requests提供了簡單的方式來檢查狀態碼:

>>> r = requests.get('http://google.com')
>>> r.status_code
200

為了更直觀地處理狀態碼,Requests提供了內建的狀態碼查詢物件:

>>> r = requests.get('http://google.com')
>>> r.status_code == requests.codes.ok  # 檢查是否為200 OK
True

>>> r = requests.get('http://google.com/404')
>>> r.status_code == requests.codes.ok
False

當我們需要自動處理錯誤狀態碼時,可以使用raise_for_status()方法:

>>> bad_request = requests.get('http://google.com/404')
>>> bad_request.status_code
404
>>> bad_request.raise_for_status()
HTTPError: 404 Client Error: Not Found

而對於成功的請求,這個方法不會做任何事:

>>> good_request = requests.get('http://google.com')
>>> good_request.status_code
200
>>> good_request.raise_for_status()
>>> # 沒有異常,表示請求成功

玄貓的實戰心得

在我多年的網路開發經驗中,Requests一直是我的得力助手。曾經我需要同時監控上百個API端點的健康狀態,使用Requests的連線池和工作階段管理功能,讓整個系統的效能提升了30%以上。

相比於標準函式庫項,Requests的優勢在於它讓開發者專注於業務邏輯,而不是瑣碎的HTTP細節。無論是建立內部監控工具、與第三方API整合,還是開發網頁爬蟲,Requests都能顯著提升開發效率。

特別是在建構需要與多個REST服務互動的系統時,Requests的工作階段管理和連線池功能夠明顯改善應用程式的回應時間和資源使用效率。

實用技巧與最佳實踐

從我的經驗來看,以下是使用Requests時的一些最佳實踐:

  1. 使用工作階段物件:當需要多次請求同一網站時,使用Session物件可以重用TCP連線並保持Cookie,顯著提升效能。

  2. 設定超時:總是為請求設定合理的超時間,避免因網路問題而導致應用程式阻塞。

  3. 使用內容流:處理大型回應時,使用流式下載避免一次性將所有內容載入記憶體。

  4. 適當處理例外:使用try/except捕捉網路例外,確保應用程式的穩定性。

  5. 使用工作階段層級的標頭:對於需要在多個請求中使用相同標頭的情況,在工作階段層級設定標頭更為高效。

Requests套件的設計哲學是「讓HTTP對人類友好」,而它確實做到了。它隱藏了HTTP協定的複雜性,讓開發者能夠以更直觀的方式與網路世界互動。

無論你是網路爬蟲開發者、API整合工作者,還是後端工程師,掌握Requests套件都能顯著提升你的開發效率和程式碼品質。透過本文介紹的基礎知識,你已經具備了利用這個強大工具的能力。接下來,就是在實際專案中探索它更多進階功能的時候了。

Python的網路互動不必複雜,有了Requests,一切都變得優雅而簡單。

掌握 Requests 的進階互動技巧

在我多年的網路應用開發經驗中,Python 的 Requests 函式庫是我處理 HTTP 互動的首選工具。上次我們探討了 Requests 的基礎功能,今天讓我們深入挖掘這個強大函式庫階特性,這些功能在開發複雜網路應用時格外有用。

解析回應標頭的關鍵資訊

伺服器回應標頭包含了關於伺服器處理請求的重要資訊。透過 r.headers 屬性,我們可以輕鬆存取這些資訊:

r = requests.get('http://google.com')
r.headers
# 輸出結果類別似:
# CaseInsensitiveDict({'alternate-protocol': '80:quic', 'x-xss-protection': '1; mode=block', ...})

值得注意的是,根據 RFC 7230,HTTP 標頭名稱不區分大小寫,這讓我們能夠以大寫或小寫方式存取標頭:

r.headers['Content-Type']  # 使用大寫
# 輸出:'text/html; charset=ISO-8859-1'

r.headers.get('content-type')  # 使用小寫
# 輸出:'text/html; charset=ISO-8859-1'

這種不區分大小寫的特性為我們提供了更大的彈性,讓程式碼更加健壯。

在開發需要狀態維護的應用時,Cookie 管理是必不可少的技能。Requests 讓 Cookie 處理變得極為簡單:

# 從回應中取得 Cookie
url = 'http://somewebsite/some/cookie/setting/url'
r = requests.get(url)
cookie_value = r.cookies['some_cookie_name']  # 取得特定 Cookie

# 傳送自訂 Cookie
url = 'http://httpbin.org/cookies'
cookies = dict(cookies_are='working')
r = requests.get(url, cookies=cookies)
r.text  # 輸出:'{"cookies": {"cookies_are": "working"}}'

在一個我為金融科技公司開發的專案中,正確管理 Cookie 幫助我們實作了無縫的使用者身份驗證流程,大幅提升了使用者經驗。

追蹤請求重定向的完整歷程

瞭解請求的重定向歷程對於除錯和效能分析至關重要。Requests 提供了 history 屬性來追蹤這一過程:

r = requests.get('http://google.com')
r.url  # 顯示最終 URL
# 輸出:'http://www.google.co.in/?gfe_rd=cr&ei=rgMSVOjiFKnV8ge37YGgCA'

r.status_code  # 200

r.history  # 顯示重定向歷史
# 輸出:(<Response [302]>,)

在這個例子中,我們看到請求最初被重定向了(回傳 302 狀態碼)。如果不希望 Requests 自動處理重定向,可以設定 allow_redirects=False

r = requests.get('http://google.com', allow_redirects=False)
r.url  # 'http://google.com/'
r.status_code  # 302
r.history  # []

相反,如果使用 HEAD 方法但希望啟用重定向,可以設定 allow_redirects=True

r = requests.head('http://google.com', allow_redirects=True)
r.url  # 顯示重定向後的 URL
r.history  # 顯示重定向歷史

設定超時保障系統效能

在建構高用性系統時,避免長時間等待無回應的請求是關鍵。設定超時可以確保你的應用不會因為慢速回應而停滯:

try:
    requests.get('http://google.com', timeout=0.03)
except requests.exceptions.Timeout:
    print("請求超時!")

timeout 引數指定了等待回應的最長時間(以秒為單位)。這不僅適用於連線階段,也適用於讀取回應的過程。當玄貓在開發監控系統時,合理設定超時引數幫助我避免了因網路波動導致的系統假死問題。

超時可能發生在兩種情況:

  1. 嘗試連線到遠端伺服器時請求超時
  2. 伺服器在分配的時間內未能傳送完整回應時

例外處理機制

Requests 提供了清晰的例外處理機制,幫助我們識別和處理各種網路問題:

  • HTTPError:當 HTTP 回應無效時丟擲
  • ConnectionError:當網路出現問題(如連線被拒絕、DNS 失敗)時丟擲
  • Timeout:當請求超時丟擲
  • TooManyRedirects:當請求超過設定的最大重定向次數時丟擲

其他還有 MissingSchemaInvalidURLChunkedEncodingErrorContentDecodingError 等異常類別。

合理處理這些異常對於建構健壯的網路應用至關重要。我在實務中通常會為不同類別的異常設計專門的處理策略,確保應用能夠優雅地從各種網路問題中還原。

深入 Requests 的高階功能

使用 Session 物件在請求間保持引數

Session 物件是 Requests 中一個強大的功能,它允許我們在多個請求間保持某些引數,如 cookies、認證資訊等:

import requests

# 建立 Session 物件
session = requests.Session()

# 使用 Session 傳送請求,附帶 cookie
response = requests.get("https://google.co.in", cookies={"new-cookie-identifier": "1234abcd"})

# 檢視請求中的 Cookie
print(response.request.headers['Cookie'])  # 'new-cookie-identifier=1234abcd'

使用 Session 物件還可以為後續請求設定預設引數:

# 設定預設引數
session.params = {"key1": "value", "key2": "value2"}
session.auth = ('username', 'password')
session.headers.update({'foo': 'bar'})

# 傳送請求時可以覆寫預設引數
session.get('http://mysite.com/new/url', headers={'foo': 'new-bar'})

在一個需要頻繁與 API 互動的專案中,我發現使用 Session 物件不僅可以簡化程式碼,還能顯著提高效能,因為它重用了底層的 TCP 連線。

瞭解請求與回應的結構

要深入理解 Requests,我們需要了解請求和回應物件的結構。

請求物件包含以下主要引數:

  • method:HTTP 方法(GET、POST、PUT 等)
  • url:請求的 Web 地址
  • headers:請求標頭字典
  • files:用於多部分上載的檔案字典
  • data:請求體
  • json:JSON 格式的請求體(設定時會自動將 Content-Type 設為 application/json)
  • params:URL 引數字典
  • auth:認證元組 (username, password)
  • cookies:Cookie 字典
  • hooks:回呼鉤子字典

回應物件則包含伺服器的回應,以及我們最初建立的請求物件。

讓我們看一個例項:

response = requests.get('https://python.org')

# 檢視請求標頭
print(response.request.headers)

# 查看回應標頭
print(response.headers)

# 檢視其他回應屬性
print(response.status_code)  # 200
print(response.url)  # 'https://www.python.org/'
print(response.elapsed)  # 請求所花時間
print(response.reason)  # 'OK'

瞭解這些結構有助於我們更精確地控制 HTTP 互動,特別是在處理複雜 API 時。

使用 PreparedRequest 進行高階自定義

每個傳送到伺服器的請求實際上都會被轉換為 PreparedRequest 物件。回應物件的 request 屬性就是用於該請求的 PreparedRequest

這種機制讓我們能夠在傳送前精確控制請求的每個方面,這在需要複雜自定義的情境中特別有用。

在我的實務經驗中,瞭解請求和回應的內部結構幫助我解決了許多棘手的 API 整合問題,特別是在處理需要精確控制標頭和認證的安全敏感型應用時。

實用技巧與最佳實踐

從我多年使用 Requests 的經驗來看,以下是一些值得分享的最佳實踐:

  1. 總是設定超時:永遠不要傳送沒有超時引數的請求,這可能導致應用在網路問題時無限期掛起。

  2. 使用 Session 提高效能:對於需要多次請求同一伺服器的情境,始終使用 Session 物件以重用連線。

  3. 正確處理異常:設計全面的例外處理策略,確保應用能從各種網路問題中還原。

  4. 注意重定向行為:瞭解並適當控制重定向行為,特別是在處理敏感操作如表單提交時。

  5. 謹慎處理 Cookie:在涉及身份驗證的應用中,正確管理 Cookie 至關重要。

透過掌握這些技巧,你將能夠構建更健壯、更高效的網路應用。在下一篇文章中,我們將探討如何使用 Requests 處理更複雜的場景,如 OAuth 認證和非同步請求。

Requests 函式庫雅設計和豐富功能使它成為 Python 生態系統中的瑰寶。希望這篇探討能幫助你更有效地利用這個強大工具,就像它多年來幫助我一樣。

掌握Requests函式庫階技巧

在開發網路應用程式時,處理HTTP請求是一項核心工作。Python的Requests函式庫其直覺的API設計,已成為處理HTTP請求的首選工具。然而,許多開發者僅停留在基礎使用層面,未能充分發揮這個強大函式庫力。

在我多年的後端開發生涯中,Requests一直是我的得力助手。今天,玄貓將帶領大家深入探索Requests函式庫些鮮為人知但極為實用的進階功能,幫助你寫出更高效、更穩定的網路應用程式。

工作階段管理與引數控制

當我們需要傳送多個具有相似設定的請求,或是需要在請求間保持某些狀態(如cookies)時,Session物件就顯得尤為重要。

使用Session與準備請求

Session物件能夠保持跨請求的引數,這在複雜API互動中特別有用。以下是一個使用Session和準備請求的範例:

from requests import Request, Session

# 建立一個頭部字典
header = {'User-Agent': 'Mozilla/5.0'}

# 建立Session物件
session = Session()

# 建立請求物件
request1 = Request('GET', 'https://api.example.com/data', headers=header)

# 準備請求
prepare = session.prepare_request(request1)

# 傳送請求,並新增額外引數
response = session.send(prepare, stream=True, verify=True)

print(response.status_code)  # 輸出狀態碼

這種方式的妙處在於,我們可以建立一個基本請求,然後在傳送前新增更多引數。在我構建一個需要處理大量API呼叫的金融資料分析工具時,這種模式顯著減少了重複程式碼,提高了維護性。

Requests提供了多種準備方法,包括:

  • prepare_request - 準備完整請求
  • prepare_auth - 準備身份驗證
  • prepare_body - 準備請求主體
  • prepare_cookies - 準備cookies
  • prepare_headers - 準備標頭
  • prepare_hooks - 準備鉤子
  • prepare_method - 準備HTTP方法
  • prepare_url - 準備URL

這些方法讓我們能精確控制請求的每個方面,實作高度客製化。

SSL證書驗證

在現代網路應用中,安全性至關重要。Requests提供了簡單的SSL證書驗證機制,幫助確保通訊安全。

驗證SSL證書

使用verify引數可以控制SSL證書驗證行為:

# 驗證SSL證書
response = requests.get('https://python.org', verify=True)
print(response)  # <Response [200]>

# 嘗試存取無SSL證書的網站會失敗
try:
    response = requests.get('http://insecure-site.example', verify=True)
except requests.exceptions.ConnectionError as e:
    print(f"連線錯誤: {e}")

在預設情況下,verify=True,這意味著Requests會驗證SSL證書。如果網站沒有效的SSL證書,請求會失敗。

在某些情況下,例如開發環境或內部網路,我們可能需要跳過SSL驗證:

# 跳過SSL驗證(不建議在生產環境使用)
response = requests.get('https://internal-server.local', verify=False)

不過我必須強調,在生產環境中停用SSL驗證是危險的做法,可能導致中間人攻擊。在我為一家金融機構開發系統時,我們建立了一套完整的證書管理流程,確保所有API通訊都經過適當的SSL驗證。

內容流處理

處理大型檔案或持續資料流時,流式處理至關重要。Requests提供了強大的流式處理能力,讓我們能高效處理大量資料。

流式下載

當下載大型檔案時,一次性載入全部內容可能會耗盡記憶體。使用流式處理可以解決這個問題:

# 流式下載大型檔案
url = "https://pypi.python.org/packages/source/F/Flask/Flask-0.10.1.tar.gz"
response = requests.get(url, stream=True)

# 檢查檔案大小是否過大
if int(response.headers.get('content-length', 0)) < 10 * 1024 * 1024:  # 10MB
    content = response.content  # 小檔案直接下載
else:
    # 大檔案分塊下載
    with open('downloaded_file.tar.gz', 'wb') as f:
        for chunk in response.iter_content(chunk_size=8192):
            if chunk:  # 過濾keep-alive新區塊
                f.write(chunk)

stream=True引數告訴Requests只下載標頭,而不立即下載內容。這讓我們可以檢查內容大小,並決定如何處理。

iter_content方法讓我們能夠分塊處理內容,這對於大型檔案尤其重要。我曾經開發過一個需要處理GB級日誌檔案的系統,如果沒有流式處理,系統會因為記憶體不足而當機。

行迭代處理

對於需要按行處理的資料,我們可以使用iter_lines方法:

# 按行處理流式資料
response = requests.get('http://httpbin.org/stream/20', stream=True)
for line in response.iter_lines():
    if line:
        # 處理非空行
        processed_line = line.decode('utf-8')
        print(processed_line)

這在處理CSV檔案或串流API回應時特別有用。

重要提示:使用stream=True時,需要確保消耗完所有資料或手動關閉連線,否則連線會保持開啟:

response = requests.get('https://api.example.com/large-data', stream=True)
try:
    # 處理資料
    for chunk in response.iter_content(chunk_size=8192):
        pass
finally:
    # 確保連線關閉
    response.close()

Keep-alive功能

Requests根據urllib3,支援連線重用,這能顯著提升處理多個請求的效能。

在Session內,Keep-alive功能是自動的。每個在Session內發出的請求都會自動使用適當的連線:

session = requests.Session()

# 這些請求會重用相同的連線
response1 = session.get('https://api.github.com/user')
response2 = session.get('https://api.github.com/user/repos')
response3 = session.get('https://api.github.com/user/orgs')

這在API密集型應用中特別有用。在一個我參與的專案中,透過使用Session和Keep-alive,我們將API呼叫的平均延遲減少了近40%。

流式上載

Requests不僅支援流式下載,也支援流式上載,這對於上載大型檔案非常實用:

# 流式上載大型檔案
with open('massive-video.mp4', 'rb') as file:
    response = requests.post('https://upload.example.com/videos',
                           data=file,
                           headers={'Content-Type': 'video/mp4'})

檔案內容會被分塊讀取並上載,避免一次性將整個檔案載入記憶體。

分塊編碼請求

HTTP分塊傳輸編碼允許我們以一系列分塊傳送資料,Requests支援這種機制:

# 使用生成器傳送分塊編碼請求
def content_generator():
    yield "第一部分資料"
    yield "第二部分資料"
    yield "最後部分資料"

response = requests.post('https://httpbin.org/post',
                        data=content_generator())

這在需要即時生成上載內容或處理超大資料集時特別有用。我曾在一個資料分析平台中使用這種方式上載分析結果,避免了生成完整結果集可能導致的記憶體問題。

事件鉤子

Requests提供了事件鉤子機制,允許我們在請求過程中的特定點插入自定義處理函式。

# 定義回呼函式
def log_response(response, *args, **kwargs):
    print(f"URL: {response.url}")
    print(f"狀態碼: {response.status_code}")
    print(f"內容類別: {response.headers.get('content-type')}")
    return response  # 回傳回應,不改變其內容

# 使用鉤子傳送請求
response = requests.get('https://www.python.org/',
                       hooks={'response': log_response})

這種方式讓我們能夠在不修改核心程式碼的情況下擴充套件Requests的功能。在我的實務經驗中,我經常使用鉤子來實作請求日誌記錄、回應時間測量和錯誤處理。

串流API處理

現代Web應用越來越多地使用串流API來提供實時資料。Requests提供了強大的工具來處理這類別API:

import json
import requests

# 連線到串流API
response = requests.get('http://httpbin.org/stream/10', stream=True)

# 處理串流資料
for line in response.iter_lines():
    if line:
        # 解析JSON資料
        data = json.loads(line)
        print(f"接收到資料: {data}")

在處理Twitter串流API或其他實時資料源時,這種模式特別有用。不過需要注意的是,iter_lines()方法可能會導致接收資料的丟失,特別是當處理速度跟不上資料流速度時。

編碼處理

Requests會自動處理回應的編碼,但有時我們需要手動控制編碼:

# 傳送請求
response = requests.get('https://example.com')

# 檢視內容類別
print(response.headers['content-type'])  # 例如: 'text/html; charset=utf-8'

# 手動設定編碼(如果自動檢測不正確)
response.encoding = 'utf-8'

# 取得正確編碼的內容
text = response.text

在處理多語言內容或非標準編碼時,手動控制編碼非常重要。我曾遇到過一個網站回傳了錯誤的編碼資訊,導致中文內容顯示為亂碼,透過手動設定正確的編碼解決了問題。

HTTP動詞

Requests支援所有標準HTTP動詞,每個動詞都有其特定用途:

  1. GET - 取得資源,不修改伺服器狀態

    response = requests.get('https://api.example.com/users')
    
  2. POST - 建立新資源

    data = {'name': 'John', 'email': 'john@example.com'}
    response = requests.post('https://api.example.com/users', data=data)
    
  3. PUT - 更新資源(完全替換)

    data = {'name': 'John', 'email': 'new.email@example.com'}
    response = requests.put('https://api.example.com/users/123', data=data)
    
  4. DELETE - 刪除資源

    response = requests.delete('https://api.example.com/users/123')
    
  5. HEAD - 僅取得標頭資訊

    response = requests.head('https://www.python.org')
    print(response.headers)
    
  6. OPTIONS - 取得支援的HTTP方法

    response = requests.options('https://api.example.com/users')
    print(response.headers.get('allow'))  # 例如: 'GET, POST, HEAD, OPTIONS'
    
  7. PATCH - 部分更新資源

    data = {'email': 'new.email@example.com'}  # 只更新email
    response = requests.patch('https://api.example.com/users/123', data=data)
    

在設計RESTful API時,正確使用這些HTTP動詞至關重要。我曾見過許多API濫用POST方法進行所有操作,這違反了REST原則,並使API更難理解和維護。

使用連結標頭進行API導航

許多現代API使用連結標頭提供自描述功能,特別是在分頁資源中:

# 存取分頁API
url = "https://api.github.com/search/code?q=addClass+user:mozilla&page=1&per_page=4"
response = requests.head(url=url)

# 取得連結標頭
link_header = response.headers.get('link')
print(f"連結標頭: {link_header}")
# 輸出範例:
# <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=2&per_page

## Requests 身份驗證機制:從基礎到進階

在網路應用開發過程中身份驗證是確保資源安全的關鍵環節無論是開發 API 整合還是建立網路爬蟲瞭解各種身份驗證機制都是不可或缺的技能在我多年的開發經驗中發現 Python Requests 套件提供的身份驗證功能不僅強大而與使用便捷讓開發者能輕鬆實作各種驗證需求

本文將探討 Requests 支援的各種身份驗證方法從最基本的驗證機制到更複雜的 OAuth 流程並提供實用範例說明如何在實際專案中應用這些技術

### 身份驗證的重要性

在深入各種身份驗證方法前我想先談為何身份驗證如此重要在現代網路應用中身份驗證不僅是保護資源的第一道防線也是實作精細許可權控制的基礎一個設計良好的身份驗證系統能夠

1. 保護敏感資料不被未授權存取
2. 追蹤和記錄使用者行為
3. 提供個人化的使用者經驗
4. 防止惡意攻擊和濫用 API

接下來讓我們逐一探討 Requests 支援的各種身份驗證方法

## 基本身份驗證 (Basic Authentication)

基本身份驗證是最常見的 HTTP 驗證方式雖然簡單但仍被廣泛採用這種方法在 HTTP 1.0 規範中就已定義實作原理直觀明確

### 基本身份驗證的工作原理

基本身份驗證的流程相當簡單

1. 使用者提供使用者 ID 和密碼
2. 這些憑證使用 Base64 編碼注意不是加密!)
3. 編碼後的憑證透過 HTTP  Authorization 標頭傳送給伺服器
4. 伺服器驗證憑證並決定是否授予存取許可權

整個流程可以圖示如下

1. 客戶端請求資源:`GET /resource`
2. 伺服器回應需要驗證:`401 Unauthorized` 並附帶 `WWW-Authenticate: Basic realm="ExAir"` 標頭
3. 客戶端再次請求並附帶憑證:`GET /resource, Authorization: Basic YWRtaW46cGFzc3dvcmQ=`
4. 伺服器驗證成功後回應:`200 OK` 並提供請求的資源

### 使用 Requests 實作基本身份驗證

使用 Requests 進行基本身份驗證非常簡單

```python
import requests
from requests.auth import HTTPBasicAuth

# 方法一:使用 HTTPBasicAuth 物件
response = requests.get(
    'https://api.example.com/secure-resource',
    auth=HTTPBasicAuth('username', 'password')
)

# 方法二:使用簡寫形式(Requests 會自動使用基本驗證)
response = requests.get(
    'https://api.example.com/secure-resource',
    auth=('username', 'password')
)

# 檢查回應
if response.status_code == 200:
    print("驗證成功!")
    print(response.json())
else:
    print(f"驗證失敗,狀態碼: {response.status_code}")

在這個範例中,我展示了兩種實作基本驗證的方式。第二種方式更為簡潔,當 auth 引數接收到一個元組時,Requests 會自動使用 HTTPBasicAuth。

基本身份驗證的優缺點

雖然基本身份驗證使用簡單,但它存在一些嚴重的安全隱憂:

優點

  • 幾乎所有瀏覽器和伺服器都支援
  • 實作簡單直觀
  • 適合內部網路或低安全需求的應用

缺點

  • 憑證只是 Base64 編碼,不是加密,可以輕易解碼
  • 每次請求都會傳送完整憑證,增加被攔截風險
  • 容易受到跨站請求偽造 (CSRF) 攻擊
  • 沒有內建的許可權復原機制

在我開發一個內部工具時,曾使用基本驗證作為臨時解決方案,但在系統投入生產環境前,我們將其替換為更安全的摘要驗證和 HTTPS,以提高安全性。這提醒我們,基本驗證應結合 HTTPS 使用,與僅適用於安全需求較低的場景。

摘要身份驗證 (Digest Authentication)

摘要身份驗證是為瞭解決基本驗證的安全漏洞而設計的進階驗證方式。它同樣使用者 ID 和密碼,但採用了加密技術來保護憑證。

摘要身份驗證的工作原理

摘要驗證使用密碼學雜湊(通常是 MD5)來保護密碼,而不是像基本驗證那樣直接傳送編碼後的密碼。其運作流程如下:

  1. 客戶端請求資源
  2. 伺服器回應 401 狀態碼,並提供一個隨機生成的 nonce(僅使用一次的數字)
  3. 客戶端使用者名、密碼、nonce 等資訊計算一個雜湊值
  4. 客戶端傳送包含此雜湊值的新請求
  5. 伺服器使用相同的方法計算雜湊值並且客戶端提供的值比較
  6. 如果雜湊值比對,則授予存取許可權

整個流程可以圖示為:

  1. 客戶端:GET /resource
  2. 伺服器:401 Unauthorized, WWW-Authenticate: Digest nonce="XXXXX"
  3. 客戶端:GET /resource, Authorization: Digest nonce="XXXXX", response="YYYY"
  4. 伺服器:200 OK 並提供請求資源(如果驗證成功)

使用 Requests 實作摘要身份驗證

Requests 讓摘要驗證的實作變得簡單直觀:

import requests
from requests.auth import HTTPDigestAuth

response = requests.get(
    'https://api.example.com/secure-resource',
    auth=HTTPDigestAuth('username', 'password')
)

# 檢查回應
if response.status_code == 200:
    print("摘要驗證成功!")
    print(response.json())
else:
    print(f"驗證失敗,狀態碼: {response.status_code}")

摘要身份驗證的優缺點

優點

  • 比基本驗證更安全,密碼不會以明文形式傳輸
  • 使用 nonce 值可以防止重放攻擊
  • 與基本驗證一樣,被廣泛支援

缺點

  • 仍然容易受到中間人攻擊,尤其是在沒有 HTTPS 的情況下
  • 伺服器需要儲存明文密碼或可逆加密的密碼,以便計算正確的雜湊值
  • 比基本驗證在伺服器端實作更複雜

在一個需要提高安全性但無法立即採用 OAuth 的專案中,我曾成功使用摘要驗證作為過渡方案。它確實提供了比基本驗證更高的安全性,但仍應與 HTTPS 結合使用以獲得更好的保護。

Kerberos 身份驗證

Kerberos 是一種網路身份驗證協定,使用金鑰加密技術在不安全網路上提供強大的身份驗證。它最初由麻省理工學院(MIT)開發,現在廣泛用於企業網路環境,特別是與 Microsoft Active Directory 整合的系統。

Kerberos 的工作原理

Kerberos 的核心概念是「票據」(ticket)。整個系統涉及三個主要角色:

  1. 認證伺服器(Authentication Server, AS):驗證使用者身份並提供票據授予票據(TGT)
  2. 票據授予伺服器(Ticket Granting Server, TGS):使用 TGT 發放服務票據
  3. 主機(Host):提供實際服務的伺服器

完整的 Kerberos 認證過程相當複雜,簡化來說包含以下步驟:

  1. 使用者登入並向認證伺服器請求 TGT
  2. 認證伺服器驗證使用者身份,並提供加密的 TGT 和工作階段金鑰
  3. 使用者使用 TGT 向票據授予伺服器請求特定服務的票據
  4. 票據授予伺服器提供服務票據
  5. 使用者使用服務票據向服務伺服器請求服務
  6. 服務伺服器驗證票據並提供服務

這種複雜的流程設計確保了高度的安全性,因為:

  • 密碼從不以明文形式傳輸
  • 票據有時間限制,降低被盜用風險
  • 透過票據實作單點登入(SSO)

使用 Requests 實作 Kerberos 身份驗證

要在 Requests 中使用 Kerberos 認證,需要安裝額外的套件:

pip install requests-kerberos

然後,就可以使用它進行 Kerberos 驗證:

import requests
from requests_kerberos import HTTPKerberosAuth

# 建立 Kerberos 認證物件
kerberos_auth = HTTPKerberosAuth()

# 使用 Kerberos 認證傳送請求
response = requests.get(
    'https://intranet.example.com/secure-resource',
    auth=kerberos_auth
)

# 檢查回應
if response.status_code == 200:
    print("Kerberos 驗證成功!")
    print(response.json())
else:
    print(f"驗證失敗,狀態碼: {response.status_code}")

在企業環境中,我曾利用這種方式開發了一個自動化工具,能夠無縫連線到公司內部的 Kerberos 保護資源,大提高了團隊的工作效率。

Kerberos 身份驗證的優缺點

優點

  • 高度安全,密碼從不以明文傳輸
  • 支援單點登入(SSO)
  • 票據有時效性,降低被盜用風險
  • 廣泛用於企業環境,特別是 Windows 網域

缺點

  • 設定和維護複雜
  • 需要專門的基礎設施
  • 認證伺服器必須持續可用,否則使用者無法取得票據
  • 不適合公共網際網路服務

Kerberos 最適合企業內部網路環境,特別是已經使用 Active Directory 的組織。對於公共網站或服務,OAuth 通常是更合適的選擇。

OAuth 身份驗證

OAuth 是一個開放標準的授權協定,允許第三方應用在不需要使用者提供密碼的情況下,安全地存取使用者資源。它被廣泛應用於大型網路服務如 Google、Facebook、Twitter 等。

OAuth 有兩個主要版本:OAuth 1.0 和 OAuth 2.0。其中 OAuth 2.0 因簡化的流程和更好的擴充套件性而成為現今的主流。

OAuth 的基本概念

OAuth 涉及四個角色:

  1. 資源擁有者:通常是使用者,擁有受保護資源的存取權
  2. 客戶端:請求存取資源的應用程式
  3. 授權伺服器:驗證資源擁有者並發放存取令牌
  4. 資源伺服器:持有受保護資源的伺服器

OAuth 2.0 的基本流程(授權碼模式)如下:

  1. 客戶端將使用者重定向到授權伺服器
  2. 使用者在授權伺服器上進行認證,並決定是否授權客戶端
  3. 授權伺服器將授權碼回傳給客戶端
  4. 客戶端使用授權碼向授權伺服器請求存取令牌
  5. 授權伺服器驗證授權碼並回傳存取令牌
  6. 客戶端使用存取令牌存取資源伺服器上的受保護資源

使用 Requests 實作 OAuth 2.0 認證

Requests 本身不

Python Requests的認證機制全解析

在現代API開發中,安全性不再是可選項,而是必備要素。當我們使用Python的Requests函式庫PI互動時,瞭解不同的認證機制至關重要。在我過去負責金融科技專案時,選擇合適的認證方式常是整個系統安全架構的關鍵決策點。本文將帶你深入瞭解Python Requests支援的各種認證機制,從最基本的認證方式到更安全的OAuth協定。

OAuth 1.0:認證的演進之路

OAuth 1.0認證協定提出了一個革命性的想法:以安全的API呼叫握手取代傳統的密碼使用。這個協定由一群受到OpenID啟發的網頁開發者建立,目的是提供更安全的授權機制。

OAuth 1.0的關鍵角色與元件

在OAuth 1.0認證流程中,有幾個核心概念需要理解:

  • 消費者(Consumer): 能夠發出認證請求的HTTP客戶端
  • 服務提供者(Service Provider): 處理OAuth請求的HTTP伺服器
  • 使用者(User): 控制HTTP伺服器上受保護資源的個人
  • 消費者金鑰與密碼(Consumer Key and Secret): 用於請求認證與授權的識別符
  • 請求令牌與密碼(Request Token and Secret): 用於取得使用者授權的憑證
  • 存取令牌與密碼(Access Token and Secret): 用於存取使用者受保護資源的憑證

OAuth 1.0的工作流程

OAuth 1.0的認證流程可以分為以下步驟:

  1. 客戶端應用程式向服務提供者請求取得請求令牌
  2. 服務提供者發放請求令牌並將其回傳給客戶端
  3. 使用者被重定向到服務提供者的授權頁面,並帶有之前接收到的請求令牌作為引數
  4. 使用者授予許可使用消費者應用程式
  5. 服務提供者將使用者回傳到客戶端應用程式
  6. 應用程式接受已授權的請求令牌並換取存取令牌
  7. 使用者使用存取令牌獲得對應用程式的存取許可權

使用Requests實作OAuth 1.0認證

在Requests中使用OAuth 1.0需要額外安裝requests_oauthlib函式庫為它不包含在Requests模組中:

import requests
from requests_oauthlib import OAuth1

# 建立OAuth1身份驗證物件
auth = OAuth1('<consumer key>', '<consumer secret>',
              '<user oauth token>', '<user oauth token secret>')

# 使用OAuth1進行認證請求            
response = requests.get('https://demo.example.com/resource/path', auth=auth)

這段程式碼建立了一個OAuth1認證物件,然後在get請求中使用它進行認證。這種方法讓我們能夠安全地存取需要OAuth 1.0認證的API資源。

OAuth 2.0:現代認證標準

OAuth 2.0是OAuth 1.0的後繼版本,它被開發來克服前代的缺點。在現代網路服務中,OAuth 2.0被廣泛採用。由於其易用性和更高的安全性,它吸引了大量使用者。OAuth 2.0的優勢在於其簡單性和為不同類別的應用程式(如網頁、行動裝置和桌面應用)提供特定授權方法的能力。

OAuth 2.0的四種授權流程

OAuth 2.0提供了四種工作流程(也稱為授權類別):

  1. 授權碼授權(Authorization code grant): 主要用於網頁應用程式,便於授權和安全資源委派
  2. 隱式授權(Implicit grant): 用於行動應用程式的OAuth授權流程
  3. 資源擁有者密碼憑證授權(Resource owner password credentials grant): 用於使用可信任客戶端的應用程式
  4. 客戶端憑證授權(Client credentials grant): 用於機器對機器認證

OAuth 2.0的改進

OAuth 2.0帶來了多項能夠解決OAuth 1.0問題的功能:

  • 使用SSL取代簽名驗證API請求的可信度
  • 支援不同環境(從網頁到行動應用)的不同流程
  • 引入重新整理令牌概念以提高安全性

使用Requests實作OAuth 2.0

以下是在Requests中使用OAuth 2.0的範例:

from requests_oauthlib import OAuth2Session

# 建立OAuth2Session物件
client = OAuth2Session('<client id>', token='token')

# 傳送請求
response = client.get('https://demo.example.com/resource/path')

自定義認證機制

Requests還提供了根據使用者需求和靈活性編寫新的或自定義認證的能力。它配備了requests.auth.AuthBase類別,這是所有認證類別的基礎類別。實作自定義認證只需在requests.auth.AuthBase__call__()方法中實作即可。

下面是自定義認證的語法範例:

import requests

class CustomAuth(requests.auth.AuthBase):
    def __call__(self, r):
        # 在這裡實作自定義認證邏輯
        # 例如新增特定頭部、修改請求等
        return r

# 使用自定義認證
response = requests.get('https://demo.example.com/resource/path', auth=CustomAuth())

在我的實際專案中,曾經需要為特定金融API開發一個時間戳加密認證方案,自定義認證機制讓我能夠完全控制認證流程,確保符合嚴格的安全要求。

使用HTTPretty模擬HTTP請求

在開發與測試使用API的應用程式時,我們常會遇到API伺服器不可用或API內容變更的情況。這時,一個HTTP請求模擬工具就顯得非常重要。雖然HTTPretty與Requests沒有直接關聯,但作為一個強大的模擬工具,它能幫助我們解決上述問題。

HTTPretty簡介

HTTPretty是一個Python的HTTP客戶端模擬函式庫靈感來自Ruby社群廣為人知的FakeWeb。HTTPretty透過模仿請求和回應來重新實作HTTP協定。

HTTPretty在通訊端層面工作,這使它能夠與大多數HTTP客戶端函式庫工作,特別是經過測試的HTTP客戶端函式庫Requests、httplib2和urlib2。因此,我們可以輕鬆地模擬來自Requests函式庫動。

HTTPretty在以下兩種情況下特別有用:

  • API伺服器不可用時
  • API內容發生變更時

安裝HTTPretty

安裝HTTPretty非常簡單,可以直接從Python Package Index (PyPi)安裝:

pip install HTTPretty

在學習過程中,我們還會用到其他一些函式庫mock、sure和Requests:

pip install requests sure mock

這些套件的功能簡介:

  • mock: 一個測試函式庫許我們用模擬物件替換系統的部分元件
  • sure: 一個用於斷言的Python函式庫### HTTPretty的使用方法

使用HTTPretty時,通常需要遵循三個主要步驟:

  1. 啟用HTTPretty
  2. 向HTTPretty註冊統一資源定位器(URL)
  3. 停用HTTPretty

首先,我們需要啟用HTTPretty,這樣它才會應用猴子補丁(monkey patching),即動態替換通訊端模組的屬性。然後使用register_uri函式註冊URL。register_uri函式接受class、uri和body作為引數:

method: register_uri(class, uri, body)

在測試結束時,我們應該停用HTTPretty,以避免它影響其他程式的行為。

基本使用範例

import httpretty
import requests
from sure import expect

def example():
    httpretty.enable()
    httpretty.register_uri(httpretty.GET, "http://google.com/",
                          body="This is the mocked body",
                          status=201)
    response = requests.get("http://google.com/")
    expect(response.status_code).to.equal(201)
    httpretty.disable()

在這個例子中,我們使用httpretty.GET類別在register_uri函式中註冊URI值"http://google.com/"。然後,我們使用Requests取得該URI的資訊,並使用expect函式斷言預期的狀態碼。簡單來說,上述程式碼嘗試模擬URI並測試我們是否獲得了預期的狀態碼。

使用裝飾器簡化程式碼

由於啟用和停用HTTPretty的步驟在每次使用時都相同,我們可以使用裝飾器來簡化程式碼。裝飾器看起來像這樣:@httpretty.activate。前面的程式碼範例可以使用裝飾器重寫如下:

import httpretty
import requests
from sure import expect

@httpretty.activate
def example():
    httpretty.register_uri(httpretty.GET, "http://google.com/",
                          body="This is the mocked body",
                          status=201)
    response = requests.get("http://google.com/")
    expect(response.status_code).to.equal(201)

設定HTTP標頭

HTTP標頭欄位提供有關請求或回應的必要資訊。我們可以使用HTTPretty模擬任何HTTP回應標頭。要實作這一點,我們將它們作為關鍵字引數加入。需要注意的是,關鍵字引數的鍵始終為小寫,並使用下劃線(_)而不是破折號。

例如,如果我們想模擬回傳Content-Type的伺服器,可以使用content_type引數:

import httpretty
import requests
from sure import expect

@httpretty.activate
def setting_header_example():
    httpretty.register_uri(httpretty.GET,
                          "http://api.example.com/some/path",
                          body='{"success": true}',
                          status=200,
                          content_type='text/json')
    response = requests.get("http://api.example.com/some/path")
    expect(response.json()).to.equal({'success': True})
    expect(response.status_code).to.equal(200)

同樣,HTTPretty接受的所有關鍵字引數都會被轉換為RFC2616等效名稱。

處理HTTP回應

當我們使用HTTPretty模擬HTTP請求時,它會回傳一個httpretty.Response物件。我們可以透過回呼生成以下回應:

  • 輪循回應(Rotating Responses)
  • 流式回應(Streaming Responses)
  • 動態回應(Dynamic Responses)

輪循回應

輪循回應是當我們向具有相同URL和相同請求方法的伺服器傳送請求時,按給定順序接收到的回應。我們可以使用responses引數定義任意多的回應。

以下程式碼說明瞭輪循回應的模擬:

import httpretty
import requests
from sure import expect

@httpretty.activate
def rotating_responses_example():
    URL = "http://example.com/some/path"
    RESPONSE_1 = "This is Response 1."
    RESPONSE_2 = "This is Response 2."
    RESPONSE_3 = "This is Last Response."
    
    httpretty.register_uri(httpretty.GET,
                          URL,
                          responses=[
                              httpretty.Response(body=RESPONSE_1,
                                              status=201),
                              httpretty.Response(body=RESPONSE_2,
                                              status=202),
                              httpretty.Response(body=RESPONSE_3,
                                              status=203)
                          ])
                          
    # 第一次請求
    response1 = requests.get(URL)
    expect(response1.text).to.equal(RESPONSE_1)
    expect(response1.status_code).to.equal(201)
    
    # 第二次請求
    response2 = requests.get(URL)
    expect(response2.text).to.equal(RESPONSE_2)
    expect(response2.status_code).to.equal(202)
    
    # 第三次請求
    response3 = requests.get(URL)
    expect(response3.text).to.equal(RESPONSE_3)
    expect(response3.status_code).to.equal(203)

在這個範例中,我們定義了三個不同的回應,每次對同一URL的請求都會按順序回傳不同的回應。這在測試需要多次呼叫同一API端點並期望不同結果的場景中非常有用。