FastAPI-3:快速入門

来源:https://www.cnblogs.com/testing-/p/18233400
-Advertisement-
Play Games

目錄一、背景介紹二、爬蟲代碼2.1 展示爬取結果2.2 爬蟲代碼講解三、可視化代碼3.1 讀取數據3.2 數據清洗3.3 可視化3.3.1 IP屬地分析-柱形圖3.3.2 評論時間分析-折線圖3.3.3 點贊數分佈-箱線圖3.3.4 評論內容-情感分佈餅圖3.3.5 評論內容-詞雲圖四、技術總結五、 ...


3 快速入門

第二章是python基礎,故不做介紹。

FastAPI是一個現代、快速(高性能)的網路框架,用於使用基於標準Python 類型提示的Python 3.6+構建API。
FastAPI的創建者是Sebastián Ramírez。

FastAPI由Sebastián Ramírez於2018年發佈。與大多數Python Web框架相比,它在很多方面都更加現代化--充分利用了過去幾年中添加到Python 3 中的功能。本章將快速介紹FastAPI的主要功能,重點是您首先需要瞭解的內容:如何處理Web請求和響應。

3.1 FastAPI簡介

與其他網路框架一樣,FastAPI 可以幫助您構建網路應用程式。每個框架的設計都是為了通過功能、遺漏和預設設置來簡化某些操作。顧名思義,FastAPI的目標是開髮網絡API,不過您也可以將其用於傳統的網路內容應用程式。

FastAPI具有以下優勢:

  • 性能:在某些情況下與Node.js和Go一樣快,與Python框架不同。

  • 快速開發:容易上手,簡單清晰。

  • 代碼質量更高:類型提示和模型有助於減少錯誤。

  • 自動生成文檔和測試頁面:比手工編輯OpenAPI說明容易得多。

FastAPI 使用以下功能:

  • Python 類型提示
  • 用於網路機制的 Starlette,包括非同步支持
  • 用於數據定義和驗證的 Pydantic
  • 用於利用和擴展其他工具的特殊集成

這種組合為網路應用程式(尤其是 RESTful 網路服務)提供了令人滿意的開發環境。

3.2 FastAPI應用

讓我們編寫一個小巧的 FastAPI 應用程式--只有一個端點的網路服務。現在,我們處於我所說的網路層,只處理網路請求和響應。首先,安裝我們要用到的基本 Python 包:

  • FastAPI 框架: pip install fastapi
  • Uvicorn 網路伺服器: pip install uvicorn
  • HTTPie 文本網路客戶端: pip install httpie
  • 請求同步網路客戶端軟體包: pip install requests
  • HTTPX 同步/非同步網路客戶端軟體包: pip install httpx

雖然curl是最著名的文本網路客戶端,但我認為HTTPie更容易使用。此外,它預設使用JSON編碼和解碼,更適合 FastAPI。在本章後面,你會看到一張截圖,其中包含訪問特定端點所需的curl命令行語法。

hello.py:

from fastapi import FastAPI

app = FastAPI()

@app.get("/hi")
def greet():
    return "Hello? World?"

  • app是頂級FastAPI對象,代表整個網路應用程式。
  • @app.get("/hi") 是一個路徑裝飾器。它告訴 FastAPI 以下內容:
    • 在此伺服器上對URL"/hi"的請求應被指向以下函數。
    • 此裝飾器僅適用於 HTTP GET verb。您也可以使用其他 HTTP 動詞(PUT、POST 等)來響應"/hi"URL,每個動詞都有一個單獨的函數。
  • def greet()是一個路徑函數--HTTP 請求和響應的主要連接點。

下一步是在網路伺服器中運行該網路應用程式。FastAPI本身不包含網路伺服器,但推薦使用Uvicorn。您可以通過兩種方式啟動Uvicorn和FastAPI 網路應用程式:外部啟動或內部啟動。

從外部通過命令行啟動Uvicorn 的方法:

$ uvicorn hello:app --reload

其中hello指的是hello.py文件,app是其中的FastAPI變數名。內部啟動如下:

from fastapi import FastAPI

app = FastAPI()

@app.get("/hi")
def greet():
    return "Hello? World?"

if __name__ == "__main__":
    import uvicorn
    uvicorn.run("hello:app", reload=True, host=r'0.0.0.0', port=8008)

無論哪種情況,如果hello.py發生變化,reload都會告訴 Uvicorn 重新啟動網路伺服器。在本章中,我們將經常使用自動重載功能。預設情況下都會使用你機器上的 8000 埠(名為 localhost)。如果你想使用其他方法,外部方法和內部方法都有主機和埠參數。
現在伺服器有了一個端點(/hi),可以隨時接受請求了。讓我們用多個網路客戶端進行測試:

  • 對於瀏覽器,在頂部位置欄鍵入URL。

  • HTTPie
$ http localhost:8008/hi
HTTP/1.1 200 OK
content-length: 15
content-type: application/json
date: Thu, 06 Jun 2024 01:41:28 GMT
server: uvicorn

"Hello? World?"

# 使用 -b 參數跳過響應頭,只列印正文。
$ http -b localhost:8008/hi
"Hello? World?"

# 使用 -v 獲取完整的請求頭和響應。
# http -v localhost:8008/hi
GET /hi HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:8008
User-Agent: HTTPie/3.2.2



HTTP/1.1 200 OK
content-length: 15
content-type: application/json
date: Thu, 06 Jun 2024 01:51:23 GMT
server: uvicorn

"Hello? World?"

  • Requests 或 HTTPX
>>> import requests
>>> r = requests.get("http://localhost:8008/hi")
>>> r.json()
'Hello? World?'
>>> import httpx
>>> r = httpx.get("http://localhost:8008/hi")
>>> r.json()
'Hello? World?'

3.3 HTTP請求

上面第一個HTTPie請求包含以下內容:

  • 動詞 (GET) 和路徑 (/hi)
  • 查詢參數(此處為"無",有的話跟在問號後面)
  • 其他 HTTP 頭信息
  • 正文內容(無請求)

FastAPI 將這些信息歸納為方便的定義:Header、Path、Query、Body。

FastAPI從HTTP請求的各個部分提供數據的方式是其最大的特點之一,也是對大多數Python Web框架的改進。您需要的所有參數都可以直接在path函數中聲明和提供,使用前面列表中的定義 (Path、Query 等),以及您編寫的函數。這使用了一種名為"依賴註入"的技術,我們將繼續討論這種技術,併在第 6 章中進一步闡述。

3.3.1 URL Path

讓我們在前面的應用程式中添加一個名為 who 的參數,向某人發送 Hello? 我們將嘗試不同的方法來傳遞這個新參數:

from fastapi import FastAPI

app = FastAPI()

@app.get("/hi/{who}")
def greet(who):
    return f"Hello? {who}?"

if __name__ == "__main__":
    import uvicorn
    uvicorn.run("hello:app", reload=True, host=r'0.0.0.0', port=8008)

在 URL 中(@app.get 之後)添加 {who},會告訴 FastAPI 在 URL 中的該位置期待一個名為 who 的變數。然後,FastAPI 將其賦值給下麵 greet() 函數中的 who 參數。這顯示了路徑裝飾器和路徑函數之間的協調。
註意不要在此處使用 Python f-string 來表示修改後的 URL 字元串 ("/hi/{who}")。

from fastapi import FastAPI

app = FastAPI()

@app.get("/hi/{who}")
def greet(who):
    return f"Hello? {who}?"

if __name__ == "__main__":
    import uvicorn
    uvicorn.run("hello:app", reload=True, host=r'0.0.0.0', port=8008)

執行:


# http -b localhost:8008/hi/test
"Hello? test?"


# http -b localhost:8008/hi
{
    "detail": "Not Found"
}

參考資料

3.3.2查詢參數

查詢參數是 URL中?後面的name=value字元串,用&字元分隔。

from fastapi import FastAPI

app = FastAPI()

@app.get("/hi")
def greet(who):
    return f"Hello? {who}?"

if __name__ == "__main__":
    import uvicorn
    uvicorn.run("hello:app", reload=True, host=r'0.0.0.0', port=8008)

執行:


# http localhost:8008/hi?who=Mom
HTTP/1.1 200 OK
content-length: 13
content-type: application/json
date: Thu, 06 Jun 2024 02:58:24 GMT
server: uvicorn

"Hello? Mom?"


# http localhost:8008/hi
HTTP/1.1 422 Unprocessable Entity
content-length: 89
content-type: application/json
date: Thu, 06 Jun 2024 02:58:31 GMT
server: uvicorn

{
    "detail": [
        {
            "input": null,
            "loc": [
                "query",
                "who"
            ],
            "msg": "Field required",
            "type": "missing"
        }
    ]
}

# 註意兩個等號為GET, 冒號表示HTTP頭,一個等號為POST,
# http -b localhost:8008/hi who==Mom
"Hello? Mom?"


3.3.3 POST

from fastapi import FastAPI, Body

app = FastAPI()

@app.post("/hi")
def greet(who:str = Body(embed=True)):
    return f"Hello? {who}?"

if __name__ == "__main__":
    import uvicorn
    uvicorn.run("hello:app", reload=True, host=r'0.0.0.0', port=8000)

執行:


# /root/code/fastapi/example^C
root@www:~/code/fastapi/example# http -v localhost:8000/hi who=Mom
POST /hi HTTP/1.1
Accept: application/json, */*;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 14
Content-Type: application/json
Host: localhost:8000
User-Agent: HTTPie/3.2.2

{
    "who": "Mom"
}


HTTP/1.1 200 OK
content-length: 13
content-type: application/json
date: Thu, 06 Jun 2024 03:25:34 GMT
server: uvicorn

"Hello? Mom?"

需要使用 Body(embed=True) 來告 FastAPI,這次我們從JSON格式的請求正文中獲取who的值。embed 部分意味著它看起來應該像 {"who": "Mom"},而不僅僅是 "Mom"。

3.3.4 HTTP 頭信息

from fastapi import FastAPI, Header

app = FastAPI()

@app.get("/hi")
def greet(who:str = Header()):
    return f"Hello? {who}?"

if __name__ == "__main__":
    import uvicorn
    uvicorn.run("hello:app", reload=True, host=r'0.0.0.0', port=8000)

執行:

# http -v localhost:8000/hi who:Mom
GET /hi HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:8000
User-Agent: HTTPie/3.2.2
who: Mom



HTTP/1.1 200 OK
content-length: 13
content-type: application/json
date: Thu, 06 Jun 2024 06:09:55 GMT
server: uvicorn

"Hello? Mom?"

3.3.5 多重請求數據

你可以在同一個路徑函數中使用多個方法。也就是說,你可以從 URL、查詢參數、HTTP 主體、HTTP 頭文件、cookie等獲取數據。你還可以編寫自己的依賴函數,以特殊方式處理和組合這些數據,例如用於分頁或身份驗證。

3.3.6哪種方法最好?

以下是一些建議:

  • 在URL中傳遞參數時,遵循 RESTful 準則是標準做法。
  • 查詢字元串通常用於提供可選參數,如分頁。
  • body通常用於較大的輸入,如整個或部分模型。

在每種情況下,如果您在數據定義中提供了類型提示,Pydantic就會自動對您的參數進行類型檢查。這將確保參數的存在和正確性。

3.4 HTTP響應

預設情況下FastAPI會將返回的內容轉換為JSON;HTTP響應的頭行內容類型為:application/json。

3.4.1 狀態代碼

FastAPI預設返回200狀態代碼;異常會產生4xx代碼。在路徑裝飾器中,指定在一切順利的情況下應返回的 HTTP 狀態代碼可以改寫返回(異常會生成自己的代碼並覆蓋它)。

from fastapi import FastAPI, Body

app = FastAPI()

@app.post("/hi")
def greet(who:str = Body(embed=True)):
    return f"Hello? {who}?"

if __name__ == "__main__":
    import uvicorn
    uvicorn.run("hello:app", reload=True, host=r'0.0.0.0', port=8000)

執行:

~# http localhost:8000/happy
HTTP/1.1 200 OK
content-length: 4
content-type: application/json
date: Thu, 06 Jun 2024 06:39:00 GMT
server: uvicorn

":)"


# http localhost:8000/happy3
HTTP/1.1 404 Not Found
content-length: 22
content-type: application/json
date: Thu, 06 Jun 2024 06:40:32 GMT
server: uvicorn

{
    "detail": "Not Found"
}

3.4.2 Headers

from fastapi import FastAPI, Body, Response

app = FastAPI()

@app.get("/header/{name}/{value}")
def header(name: str, value: str, response:Response):
    response.headers[name] = value
    return "normal body"

if __name__ == "__main__":
    import uvicorn
    uvicorn.run("hello:app", reload=True, host=r'0.0.0.0', port=8000)

執行:

# http localhost:8000/header/marco/polo
HTTP/1.1 200 OK
content-length: 13
content-type: application/json
date: Thu, 06 Jun 2024 06:48:10 GMT
marco: polo
server: uvicorn

"normal body"

3.4.3 響應類型

響應類型(從 fastapi.responses 中導入這些類)包括以下內容:

  • JSONResponse(預設值)
  • HTMLResponse
  • PlainTextResponse
  • RedirectResponse
  • FileResponse
  • StreamingResponse

關於後兩種,我將在第15章中詳細介紹。對於其他輸出格式(也稱為 MIME 類型),可以使用通用的 Response 類,它需要以下內容:

  • content:字元串或位元組

  • media_type:字元串 MIME 類型

  • status_code:HTTP 整數狀態代碼

  • headers:字元串

3.4.4 類型轉換

路徑函數可以返回任何內容,預設情況下(使用 JSONResponse),FastAPI 會將其轉換為 JSON 字元串並返回,同時返回與之匹配的 HTTP 響應頭 Content-Length 和 Content-Type。這包括任何 Pydantic 模型類。
但它是如何做到這一點的呢?如果您使用過 Python json 庫,您可能會發現,當給定某些數據類型(如日期時間)時,它會引發異常。FastAPI 使用名為 jsonable_encoder() 的內部函數將任何數據結構轉換為 "JSONable" Python 數據結構,然後調用 json.dumps() 將其轉換為 JSON 字元串。


import datetime
import json
import pytest
from fastapi.encoders import jsonable_encoder

@pytest.fixture
def data():
    return datetime.datetime.now()

def test_json_dump(data):
    with pytest.raises(Exception):
        _ = json.dumps(data)

def test_encoder(data):
    out = jsonable_encoder(data)
    assert out
    json_out = json.dumps(out)
    assert json_out

3.4.4 模型類型和響應模型

不同的類可能有許多相同的欄位,只是一個專門用於用戶輸入,一個用於輸出,還有一個用於內部使用。產生這些變體的原因可能包括以下幾點:

  • 從輸出中移除一些敏感信息,比如去識別個人醫療數據,如果你遇到《健康保險可攜性和責任法案》(HIPAA)的要求。
  • 為用戶輸入添加欄位(如創建日期和時間)。

下列展示了一個假定案例中的三個相關類:

  • TagIn 是定義用戶需要提供的內容(在本例中,只是一個名為 tag 的字元串)的類。
  • Tag 是由 TagIn 生成的,並增加了兩個欄位:created(創建時間)和 secret(內部字元串,可能存儲在資料庫中,但永遠不會向外界公開)。
  • TagOut 是定義可以返回給用戶(通過查詢或搜索終端)的內容的類。它包含來自原始 TagIn 對象及其派生 Tag 對象的標簽欄位,以及為 Tag 生成的創建欄位,但不是秘密欄位。
from datetime import datetime
from pydantic import BaseClass

class TagIn(BaseClass):
    tag: str

class Tag(BaseClass):
    tag: str
    created: datetime
    secret: str

class TagOut(BaseClass):
    tag: str
    created: datetime

您可以通過不同方式從FastAPI路徑函數返回預設JSON以外的數據類型。其中一種方法是在路徑裝飾器中使用 response_model 參數,讓 FastAPI 返回其他類型的數據。FastAPI 將放棄任何在返回對象中存在但不在 response_model 指定的對象中的欄位。

假設你編寫了一個名為 service/tag.py 的新服務模塊,其中包含 create() 和 get() 函數,為該網路模塊提供了調用的內容。這些低層堆棧細節在這裡並不重要。重要的是底部的 get_one() 路徑函數,以及路徑裝飾器中的 response_model=TagOut。這會自動將內部 Tag 對象轉換為經過消毒的 TagOut 對象。

import datetime
from fastapi import FastAPI
from model.tag import TagIn, Tag, TagOut
import service.tag as service

app = FastAPI()

@app.post('/')
def create(tag_in: TagIn) -> TagIn:
    tag: Tag = Tag(tag=tag_in.tag, created=datetime.utcnow(),
        secret="shhhh")
    service.create(tag)
    return tag_in

@app.get('/{tag_str}', response_model=TagOut)
def get_one(tag_str: str) -> TagOut:
    tag: Tag = service.get(tag_str)
    return tag

儘管我們返回的是 Tag,但response_model會將其轉換為 TagOut。

3.5 自動化文檔

上面3.3.3 POST的自動文檔http://localhost:8000/docs。

FastAPI會根據您的代碼生成一個OpenAPI規範,並包含這個頁面來顯示和測試您的所有端點。這隻是其秘訣之一。
單擊綠色框右側的向下箭頭,打開它進行測試。

點擊右側的 "試用 "按鈕。現在你會看到一個區域,可以在正文部分輸入數值(。

點擊 "字元串"。將其更改為 "Cousin Eddie"(保留雙引號)。然後單擊底部的藍色 "執行 "按鈕。

3.6複雜數據

這些示例只展示瞭如何向端點傳遞單個字元串。許多端點,尤其是 GET 或 DELETE 端點,可能根本不需要參數,或者只需要幾個簡單的參數,如字元串和數字。但在創建(POST)或修改(PUT 或 PATCH)資源時,我們通常需要更複雜的數據結構。第 5 章展示了 FastAPI 如何使用 Pydantic 和數據模型來簡潔地實現這些功能。

3.7小結

在本章中,我們使用 FastAPI 創建了一個只有一個端點的網站。多個網路客戶端對其進行了測試:網路瀏覽器、HTTPie 文本程式、Requests Python 包和 HTTPX Python 包。從簡單的 GET 調用開始,請求參數通過 URL 路徑、查詢參數和 HTTP 標頭進入伺服器。然後,HTTP 主體被用來向 POST 端點發送數據。隨後,本章展示瞭如何返回各種 HTTP 響應類型。最後,自動生成的表單頁面為第四個測試客戶端提供了文檔和實時表單。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 以用戶為中心的性能指標是理解和改進站點體驗的關鍵點 一、以用戶為中心的性能指標 1. 指標是用來幹啥的? 指標是用來衡量性能和用戶體驗的 2. 指標類型 感知載入速度:網頁可以多快地載入網頁中的所有視覺元素並將其渲染到屏幕上 載入響應速度:頁面載入和執行組件快速響應用戶互動所需的 JavaScrip... ...
  • 隨著Web應用變得越來越複雜,而jQuery的功能卻顯得過於簡單,難以應對這些複雜的需求。比如,對於一些需要大量動態交互的應用程式,jQuery的功能並不足夠強大。此外,由於jQuery所寫應用的代碼結構較為混亂,其中包含了大量的全局變數和函數,例如,全局變數"$"和"jQuery"都指向了jQue... ...
  • 基於React的SSG靜態站點渲染方案 靜態站點生成SSG - Static Site Generation是一種在構建時生成靜態HTML等文件資源的方法,其可以完全不需要服務端的運行,通過預先生成靜態文件,實現快速的內容載入和高度的安全性。由於其生成的是純靜態資源,便可以利用CDN等方案以更低的成 ...
  • 結構化開發方法 基本思想:自頂向下,逐步求精,過程抽象,模塊化技術 概念: 結構化程式設計:按照一定的原則與原理,組織編寫正確且易讀的程式的軟體技術。 結構化分析設計:數據流圖、數據字典、模塊結構圖。 優勢:合理性(管理複雜性的有效手段:分解,抽象,層次)、正確性(依據規約,完成任務) 程式 & 抽 ...
  • 01-什麼是Spring IOC 和DI ? IOC : 控制翻轉 , 它把傳統上由程式代碼直接操控的對象的調用權交給容 器,通過容器來實現對 象組件的裝配和管理。所謂的“控制反轉”概念就是對組件對象控制權的轉 移,從程式代碼本身 轉移到了外部容器。 DI : 依賴註入,在我們創建對象的過程中,把對 ...
  • 前言 大家好,我是老馬。很高興遇到你。 我們希望實現最簡單的 http 服務信息,可以處理靜態文件。 如果你想知道 servlet 如何處理的,可以參考我的另一個項目: 手寫從零實現簡易版 tomcat minicat 手寫 nginx 系列 如果你對 nginx 原理感興趣,可以閱讀: 從零手寫實 ...
  • 源碼: <?php error_reporting(0); if ($_SERVER['REQUEST_METHOD'] !== 'POST') { header("HTTP/1.1 405 Method Not Allowed"); exit(); } else { if (!isset($_PO ...
  • 在 Mac 上安裝多個 Python 版本可通過幾種不同方法實現。 1 Homebrew 1.1 安裝 Homebrew 若安裝過,跳過該步。 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/ ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...