補充 B:搞懂「同時做很多事」— 同步/非同步、多執行緒、多行程、分散式
這份補充把整個 crawler 專案底層在做的事講清楚。你會發現,前面學的
.delay()、worker 的concurrency、--pool、多台 worker,其實都是「怎麼同時處理很多件事」的不同層次。全部用我們的 repo 當例子。
為什麼要學這個
想像你要抓台股全部 1000 多支股票的歷史股價。如果一支抓完才抓下一支,每支等 API 回應 1 秒,就要等 1000 秒。太慢了。
「怎麼讓很多件事一起進行」有好幾種層次,從最輕到最重是:
同步 → 非同步 → 多執行緒 → 多行程 → 分散式
(一次一件)(不等它做完)(一個行程多條線)(多個行程)(多台機器)
我們的 crawler 專案剛好把後面幾種都用上了。下面一個一個拆。
先分清楚兩個最容易搞混的詞
- 並發(concurrency):交錯處理很多件事,不一定真的同時。像一個廚師同時顧兩道菜——其實是在切換:趁一鍋在燉的空檔,去炒另一鍋。
- 並行(parallelism):真的同時執行。像兩個廚師、各站一個爐台,同一秒都在炒。
一句話:並發是「看起來同時」,並行是「真的同時」。多執行緒、非同步比較偏並發;多行程、分散式才是真並行。記住這個區分,下面就好懂了。
1. 同步 vs 非同步 —— 差在「要不要等它做完」
同步(synchronous):呼叫一件事,程式會停在那裡等它做完,才往下走。 非同步(asynchronous):呼叫一件事,程式丟出去就繼續往下走,不等它做完。
比喻:同步=在餐廳櫃檯點完餐,站在原地盯著廚房做完、拿到餐才肯走——這段時間你什麼都不能做。非同步=點完餐拿個號碼牌回座位,餐好了店家叫號——.delay() 就是拿號碼牌,RabbitMQ 就是掛在廚房前的那疊單子。差別不在「餐做多快」,在你要不要站在那裡等。
用我們的 repo 看
# 同步:直接呼叫函式,程式會卡在這裡等整支爬蟲跑完(含等 API)
crawler_finmind(stock_id="2330")
# 非同步:.delay() 把任務丟進 RabbitMQ 就立刻回來,不等它做完
crawler_finmind.delay(stock_id="2330")
這就是第 1 章的核心。時間軸長這樣(抓 3 支、每支等 API 3 秒):
同步:
你的程式 |--抓2330(3s)--|--抓0050(3s)--|--抓2317(3s)--| 共 9 秒,全程卡住
非同步(.delay 丟給 Celery):
你的程式 |送2330|送0050|送2317| ← 幾乎瞬間送完就繼續
worker |--抓2330--|
|--抓0050--| ← 交給 worker 併發處理
|--抓2317--|
producer.py 用 for 迴圈 .delay() 送 100 個任務、幾乎瞬間跑完,就是非同步——它只負責「丟」,不等「做完」。
.delay() 和 apply_async():同一件事的兩種寫法
非同步派送在 Celery 裡有兩種寫法,你在這個 repo 兩種都會看到:
# 寫法一:.delay() —— 最簡單,適合「只要把任務丟出去」
crawler_finmind.delay(stock_id="2330")
# 寫法二:apply_async() —— 完整版,可以加一堆選項
crawler_finmind.apply_async(kwargs={"stock_id": "2330"})
這兩個都是非同步(丟出去就回來、不等做完),差別只在「能不能加進階設定」:
.delay(x=1)其實就是.apply_async(kwargs={"x": 1})的簡寫,方便但沒有額外參數可調。.apply_async(...)是完整版,可以指定:queue="twse":把任務送到指定佇列(第 3 章多佇列分流就是靠這個,.delay()做不到)countdown=10:10 秒後才執行eta=某個時間:指定在某個時間點執行retry=True、link=...:重試策略、串接後續任務
repo 裡的 producer_multi_queue.py 就是因為要指定佇列,所以改用 .s(...).apply_async(queue=...) 而不是 .delay():
# 因為要指定 queue,不能用 .delay(),改用 signature + apply_async
crawler_finmind.s(stock_id="2330").apply_async(queue="twse")
一句話:要簡單就 .delay(),要控制(指定佇列、延遲、重試)就 apply_async()。 兩者都是非同步。
補充:Python 還有另一種非同步寫法叫
asyncio(async def/await),那是「單一程式內」的非同步。我們的專案不是用asyncio,而是用 Celery 把任務丟給別的 worker 去做,達到「呼叫端不用等」的效果。兩者精神相同(都是「不卡住等待」),但實作層次不同。
2. 多執行緒 multi-threading —— 一個行程內,多條執行緒,共享記憶體
多執行緒:在同一個行程裡開很多條執行緒(thread),它們共享同一塊記憶體。
比喻:一個廚師(一個行程),但他很會安排——趁一鍋湯在燉(等待),就去切菜、擺盤。看起來一個人同時在弄好幾道菜。這是並發。
Python 有個關鍵限制:GIL
CPython 有一個「全域直譯器鎖(GIL)」,代表同一時間只有一條執行緒能真正在跑 Python 程式碼。所以多執行緒在 Python 裡:
- 適合 I/O 密集(一直在等網路、等硬碟):一條執行緒在等 API 回應時,GIL 會放開,讓另一條執行緒去跑。等於「等待的空檔被塞滿」,很有效。
- 不適合 CPU 密集(一直在算):因為 GIL 卡著,多條執行緒還是輪流跑,快不起來。
用我們的 repo 看
我們的爬蟲大部分時間在等 FinMind 回應(第 2 章講過,這是 I/O 密集),所以非常適合這種「單行程、高並發」的做法。Celery 提供對應的 pool:
# threads pool:一個 worker 行程內開很多執行緒
uv run python -m celery -A crawler.worker worker --pool=threads --concurrency=50
# gevent pool:更輕量的「協程」版本(概念類似,但比執行緒還省),實務上爬蟲最常用
uv run python -m celery -A crawler.worker worker --pool=gevent --concurrency=200
一個 worker 就能同時「等」幾十上百個 API 回應,記憶體成本很低。(gevent 嚴格講是協程/綠色執行緒,不是作業系統執行緒,但你可以先把它理解成「超輕量的多執行緒」,都是為了在單行程內塞滿 I/O 等待。)
3. 多行程 multi-processing —— 多個行程,各自獨立,真正用滿多核
多行程:開很多個獨立的行程(process),每個有自己的記憶體、自己的 Python 直譯器。因為各自獨立,繞過了 GIL,可以真正在多顆 CPU 核心上同時跑——這是真正的並行。
比喻:多請幾個廚師(多個行程),每人站一個爐台,同一秒都在炒菜。
用我們的 repo 看
這正是 Celery 的預設行為。 還記得第 1 章啟動 worker 時,banner 上那行嗎:
.> concurrency: 8 (prefork)
prefork 就是多行程 pool,concurrency: 8 代表這個 worker fork 出 8 個子行程,可以真正同時跑 8 個任務。預設數量等於你的 CPU 核心數。
# prefork(預設):開 4 個子行程,適合「一直在算」的 CPU 密集任務
uv run python -m celery -A crawler.worker worker --pool=prefork --concurrency=4
代價:每個子行程都是一個真正的作業系統行程,吃記憶體、啟動成本高,所以數量受核心數和 RAM 限制,不可能像 gevent 那樣開幾百個。
Celery 的 pool 選項一覽(prefork 之外還有誰)
前面出現過 prefork、threads、gevent 三種,這裡把常見的 pool 一次排開:
| pool | 底層是什麼 | 屬於哪一層(廚房版) | 併發模型 | 合理併發 | 適合 | 注意 |
|---|---|---|---|---|---|---|
prefork(預設) |
多個子行程(fork) | 多行程——多請幾個廚師,每人一個爐台 | 並行(繞過 GIL) | ≈ CPU 核心數 | CPU 密集 | 吃記憶體、啟動慢;容器內要留意 RAM 上限 |
threads |
作業系統執行緒 | 多執行緒——一個廚師開好幾條工作線 | 並發(受 GIL) | 數十 | I/O 密集 | 標準庫就有、不用裝套件,最無腦的 I/O 選項 |
gevent |
協程(綠色執行緒) | 協程——可視為「更輕的多執行緒」:還是一個廚師,但超會利用空檔,幾百鍋湯同時燉 | 並發(受 GIL) | 數百 | I/O 密集(爬蟲首選) | 要先 uv add gevent;靠 monkey-patch 把阻塞 I/O 換成非阻塞,極少數 C 擴充套件不相容 |
eventlet |
協程(gevent 的同類替代品) | 協程——同 gevent | 並發(受 GIL) | 數百 | I/O 密集 | 與 gevent 同定位、二選一即可;近年維護趨緩,新專案建議選 gevent |
solo |
單行程單線,任務在主行程裡直接跑 | 都不是——一次只做一道菜的實習生 | 無併發 | 1 | 除錯、教學 | 一次只跑一個任務、log 最乾淨;查單一任務的問題時很好用 |
註:協程不是作業系統的執行緒,它是「同一條執行緒裡」由程式自己安排的快速切換(所以更省),但目的和多執行緒一樣——把 I/O 等待的空檔塞滿。分類上可以記成「多執行緒陣營的輕量版」。
一句話選型:在算的用 prefork、在等的用 gevent、想看清楚單一任務怎麼跑用 solo。我們的爬蟲整天在等 FinMind → gevent。
多行程 vs 多執行緒:並排比一次(最容易混淆的一組)
前面比過「同步 vs 非同步」(差在要不要等),這一組比的是「人手怎麼開」。兩個詞只差一個字,差別卻是本質的:
| 多執行緒 threading | 多行程 multiprocessing | |
|---|---|---|
| 記憶體 | 同一行程內共享 | 各自獨立、不共享 |
| 資料交換 | 直接讀寫同一個變數(方便,但要小心互搶) | 得靠佇列/管線傳遞(要序列化,成本高) |
| GIL 影響 | 受限——同一時間只有一條在跑 Python | 每個行程有自己的 GIL,互不卡 |
| 併發 or 並行 | 並發(輪流跑+等待重疊) | 並行(多核真同時) |
| 啟動成本 | 低(很輕量) | 高(複製出一整個行程) |
| 一個掛掉 | 可能拖垮整個行程(大家同住一間房) | 只死自己,其他行程照跑 |
| 適合 | I/O 密集:等網路、等硬碟 | CPU 密集:運算、壓縮、跑模型 |
| Celery 對應 | --pool=threads / gevent(協程同陣營) |
--pool=prefork(預設) |
三個最常見的誤解,順手拆掉:
- 「執行緒開得多,應該比較快?」——在 Python,CPU 密集時多執行緒不會變快(GIL 讓它們輪流跑);只有「在等」的時間能被重疊利用。
- 「多行程之間改個全域變數溝通?」——不行。子行程是複製出去的,各改各的副本,彼此看不到。這也是為什麼 Celery 任務之間不靠變數傳資料,而是靠 broker 和資料庫。
- 廚房版記法:多執行緒=一個廚師很會利用燉湯的空檔切菜;多行程=多個廚師每人一個爐台。人數才是兩者的本質差別,「會不會利用空檔」是執行緒的強項、「多顆爐同時開火」是行程的強項。
4. 分散式 Distributed —— 跨機器,靠 broker 協調
分散式:把工作分給多台機器(或多個獨立行程),它們不共享記憶體,靠**網路 + 一個中介(broker)**互相協調。
比喻:一間連鎖餐廳開了很多分店,共用同一套中央訂單系統。訂單進中央系統,哪間分店有空就接來做。
用我們的 repo 看
這是整個 Celery 架構的重點。多個 worker 連到同一個 RabbitMQ,一起消費任務:
# 機器 A(或終端機 A)
uv run python -m celery -A crawler.worker worker -l info -n w1@%h
# 機器 B(或終端機 B)
uv run python -m celery -A crawler.worker worker -l info -n w2@%h
只要這些 worker 都連到同一個 broker,它們就自動組成一個「worker 池」分著做任務。這些 worker 可以在同一台機器、也可以在完全不同的機器——只要連得到同一個 RabbitMQ。
專案的 docker-compose-local.yml 裡的 worker_twse、worker_tpex 就是兩個獨立的 worker 容器,連同一個 broker,這已經是分散式的雛形了。
分散式和多行程的差別:多行程是「一台機器、多顆核心」;分散式是「多台機器」。分散式突破了單機的極限——你可以無限加機器。而且因為 producer 和 worker 被 broker 徹底隔開(第 1 章講的解耦),某台 worker 掛掉,任務還在 broker,別台會接手。
把四層疊起來看(我們的 repo 剛好全都有)
這四種不是互斥的,而是可以疊在一起。我們的專案從外到內是這樣:
分散式:多台機器 / 多個 worker ← 連同一個 RabbitMQ
└─ 每台機器上,一個 worker 行程
└─ 這個 worker 用某種 pool 開併發:
├─ prefork(多行程,用滿多核)
└─ 或 gevent / threads(單行程多並發,塞滿 I/O 等待)
└─ 而任務本身是被 .delay() 非同步派送進來的
換句話說:
| 你做的事 | 對應概念 | repo 裡是什麼 |
|---|---|---|
crawler_finmind.delay(...) |
非同步 | producer 派送任務 |
--pool=gevent --concurrency=200 |
多執行緒 / 並發 | 單 worker 塞滿 I/O 等待 |
--pool=prefork --concurrency=8 |
多行程 / 並行 | 單 worker 用滿多核 |
| 多開 worker / 多台機器連同一 broker | 分散式 | worker_twse、worker_tpex…… |
到底該選哪個?看你的任務是「CPU 密集」還是「I/O 密集」
這是最實用的決策依據:
| 任務類型 | 特徵 | 該用什麼 | 為什麼 |
|---|---|---|---|
| I/O 密集 | 大部分時間在等(網路、硬碟、DB) | 非同步 + gevent/threads 高併發 | CPU 在發呆,開再多也不搶 CPU,塞滿等待就好、成本低 |
| CPU 密集 | 大部分時間在算(運算、壓縮、模型) | 多行程 prefork,貼齊核心數 | GIL 讓執行緒沒用,要用多行程才能真的用滿多核 |
| 量超大、單機不夠 | 不管哪種,就是量太大 | 分散式,加機器 / 加 worker | 突破單機極限,水平擴充 |
我們的爬蟲屬於哪種? I/O 密集(一直在等 FinMind 回應)。所以最有效率的做法是 --pool=gevent 開高併發,或多開幾個 worker 分散式處理;不需要為了單機 CPU 拼命。這也解釋了第 9 章為什麼建議爬蟲用 gevent。
逐項驗證(用 repo 實際執行)
實驗 1:prefork(多行程)vs gevent(多並發)
準備一批任務(例如把 producer_crawler_finmind_print.py 的股票清單加到 20 支),分別用兩種 pool 跑,比較速度感受:
# A. prefork,開 4 個行程
uv run python -m celery -A crawler.worker worker -l info --pool=prefork --concurrency=4
# B. gevent,開 50 個協程(要先 uv add gevent)
uv run python -m celery -A crawler.worker worker -l info --pool=gevent --concurrency=50
因為爬蟲是 I/O 密集,B 通常能同時「等」更多支股票,整批更快做完。
實驗 2:分散式(多 worker 分工)
# 開兩個 worker
uv run python -m celery -A crawler.worker worker -l info -n w1@%h
uv run python -m celery -A crawler.worker worker -l info -n w2@%h
# 再跑一次 producer,到 Flower 看兩個 worker 各分到幾個任務
想一想(確認你懂了)
Q1:concurrency: 8 (prefork) 這行,代表 worker 用了上面哪一種?
多行程。prefork 就是 fork 出 8 個獨立子行程,能真正在多核上並行。這是 Celery 的預設 pool。
Q2:我們的爬蟲是 I/O 密集,為什麼用 gevent 開 200 併發,比 prefork 開 8 行程還划算?
因為爬蟲大部分時間在「等 API」,CPU 在發呆。gevent 的協程超輕量,一個行程就能同時掛著 200 個「正在等」的任務,記憶體成本極低;而 prefork 每個併發都是一個真行程,很吃記憶體,開不到那麼多。對「等待型」的工作,塞滿等待比用滿 CPU 更重要。
Q3:多行程和分散式,都是「很多個一起做」,差在哪?
多行程是「同一台機器、多顆核心」,受限於這台機器的核心數與記憶體;分散式是「多台機器」,靠 broker 協調,可以一直加機器、突破單機極限。而且分散式的 worker 彼此獨立,一台掛掉別台會接手。
Q4:GIL 對「多執行緒」有什麼影響?
GIL 讓同一時間只有一條執行緒能跑 Python 程式碼。所以多執行緒對 CPU 密集任務幫助不大(還是輪流跑),但對 I/O 密集很有用(一條在等網路時,GIL 放開讓另一條跑)。這就是為什麼「多執行緒適合 I/O、多行程適合 CPU」。
換你試試看
練習 1:看見多行程
用預設 prefork 啟動 worker,另開一個終端機執行 ps aux | grep celery(Mac/Linux)。你會看到不只一個 celery 行程——一個主行程加上好幾個子行程(數量約等於 concurrency)。這確認了 prefork 真的開了多個作業系統行程。
練習 2:對照 I/O 密集下兩種 pool 的吞吐
把 producer 的股票清單加到 20 支,分別用 --pool=prefork --concurrency=4 和 --pool=gevent --concurrency=50 跑,用手機碼錶大略計時整批做完的時間。想一想:為什麼 I/O 密集的任務,gevent 高併發會贏?
練習 3:把分散式跑起來
同時開 3 個 worker(不同 -n 名字),跑一次 producer,到 Flower 的 Workers 頁看任務怎麼被三個 worker 分掉。這就是分散式最直觀的樣子——你沒改任何程式碼,只是多開了幾個執行單位。
一頁總結
| 概念 | 一句話 | 餐廳比喻 | 並發還是並行 | repo 例子 | 適合 |
|---|---|---|---|---|---|
| 同步 | 呼叫後等它做完才往下 | 站在櫃檯等餐做完才走 | — | crawler_finmind(...) |
簡單、有先後依賴 |
| 非同步 | 丟出去就繼續,不等 | 拿號碼牌回座等叫號 | — | .delay(...) / .apply_async(...) |
不想被耗時工作卡住 |
| 多執行緒 | 一行程多條線、共享記憶體 | 一個廚師利用燉湯空檔切菜 | 偏並發(受 GIL) | --pool=threads/gevent |
I/O 密集 |
| 多行程 | 多個獨立行程、各用一核 | 多請廚師、每人一個爐台 | 並行 | --pool=prefork(預設) |
CPU 密集 |
| 分散式 | 多台機器靠 broker 協調 | 連鎖分店+中央訂單系統 | 並行 | 多個 worker 連同一 RabbitMQ | 量太大、要水平擴充 |
餐廳版一條龍:客人點餐拿號碼牌(.delay() 非同步)→ 單子掛上廚房(RabbitMQ)→ 店裡的廚師各自開工:一個超會利用空檔(gevent)、或多請幾個各站一爐(prefork)→ 生意做大就開分店、共用中央訂單系統(分散式)。
一條龍記法:.delay() 非同步把任務丟進 broker → 每個 worker 用 pool(prefork 用滿核心 / gevent 塞滿 I/O)在單機併發 → 多開 worker、多台機器就成了分散式。我們的 crawler 專案,這四層剛好全用上了。
附錄:術語速查表
這裡把前面各章出現過的技術名詞集中整理,看不懂哪個詞就翻到這裡查。「詳見」欄標出哪一章有完整說明。
Celery 核心
| 名詞 | 白話說明 | 詳見 |
|---|---|---|
| Celery | 分散式任務佇列框架,把耗時工作丟到背景 worker 執行 | 第 1 章 |
| Producer 生產者 | 發送任務的角色,呼叫 .delay() / apply_async() |
第 1 章 |
| Broker 訊息中介 | 任務排隊的中間站,本專案用 RabbitMQ | 第 1 章 |
| Worker 工作者 | 從 broker 取出任務、實際執行的行程 | 第 1 章 |
| Result Backend 結果後端 | 存放任務回傳結果的地方(選用),本專案未啟用 | 第 1 章 |
app(Celery app) |
整個 Celery 應用的核心實例,所有任務靠它註冊;本專案定義在 worker.py |
第 1 章 |
@app.task(裝飾器 / decorator) |
加在函式上,讓普通函式變成「可派送任務」 | 第 1 章 |
include |
Celery app 的參數,列出要載入哪些「模組(檔案)」的任務 | 第 1 章 |
.delay() |
非同步派送的簡寫,丟出去立刻回傳、不等執行 | 第 1 章 |
.apply_async() |
完整版非同步派送,可指定 queue、countdown、eta、重試等 |
第 3 章、補充上半 |
.s()(signature 簽章) |
把「任務 + 參數」綁成物件(還沒送出),方便再加 queue 等設定 | 第 3 章 |
| AsyncResult | .delay() 回傳的物件,像一張「取件單」,可查狀態 / 取結果(需 result backend) |
第 1 章 |
| concurrency 併發數 | 一個 worker 同時能跑幾個任務 | 第 1、6 章 |
| pool 執行池 | worker 用哪種方式開併發:prefork / threads / gevent / eventlet / solo | 第 9 章、補充上半 |
| prefork | 預設 pool,開多個子行程(多行程),適合 CPU 密集 | 第 1、6 章 |
| gevent | 協程 pool,單行程開大量並發,適合 I/O 密集 | 第 9 章、補充上半 |
-Q |
啟動 worker 時指定「只消費哪一條佇列」 | 第 3 章 |
-n / --hostname |
幫 worker 取名字,多 worker 時好辨識(%h 會換成主機名) |
第 1、3 章 |
| ack(確認) | worker 處理完回報 broker、broker 才刪掉訊息;acks_late 的細節在第 4 章 |
第 3 → 7 章 |
訊息佇列 / RabbitMQ
| 名詞 | 白話說明 | 詳見 |
|---|---|---|
| RabbitMQ | 本專案用的 broker,任務在這裡排隊;管理介面在 :15672(worker/worker) | 第 1 章 |
| 佇列 queue | 任務排隊的通道;預設佇列叫 celery,本專案還有 twse / tpex |
第 1、3 章 |
| pyamqp / AMQP | Celery 連 RabbitMQ 用的協定;連線字串格式 pyamqp://帳號:密碼@主機:埠/ |
第 1 章 |
| Flower | Celery 的網頁監控面板,看任務與 worker 狀態,在 :5555 | 第 1 章 |
| Ready / Unacked | RabbitMQ UI 上的訊息狀態:Ready = 排隊中還沒被拿、Unacked = 被拿走但還沒確認 | 第 3 章 |
資料庫
| 名詞 | 白話說明 | 詳見 |
|---|---|---|
| MySQL | 本專案存股價的關聯式資料庫,在 :3306 | 第 5 章 |
| mydb | 本專案使用的資料庫名稱 | 第 5 章 |
| phpMyAdmin | MySQL 的網頁管理介面,:8080,帳密 root/1234 | 第 5 章 |
| SQLAlchemy | Python 連資料庫、操作資料庫的工具庫 | 第 5 章 |
engine / create_engine |
SQLAlchemy 的連線引擎,代表「怎麼連到這個資料庫」 | 第 5 章 |
to_sql |
pandas 把整個 DataFrame 直接寫進資料表的方法 | 第 5 章 |
if_exists="append" |
表已存在就把資料附加上去(會一直疊加、造成重複) | 第 5 章 |
| 主鍵 primary key | 唯一辨識一筆資料的欄位 | 第 6 章 |
| 複合主鍵 composite key | 用多個欄位組合當主鍵,本專案用 stock_id + date |
第 6 章 |
| upsert | update + insert:主鍵有就更新、沒有就新增 | 第 6 章 |
on_duplicate_key_update |
MySQL 專用語法,主鍵重複時改成更新而非報錯,用來做 upsert | 第 6 章 |
| 冪等 idempotent | 同一個操作做幾次,結果都一樣 | 第 6 章 |
| transaction 交易 | 一組「要嘛全成功、要嘛全失敗」的資料庫操作;engine.begin() |
第 6 章 |
爬蟲與資料格式
| 名詞 | 白話說明 | 詳見 |
|---|---|---|
| API | 程式對外要資料的介面,本專案用 FinMind API | 第 2 章 |
| endpoint | API 的網址入口 | 第 2 章 |
HTTP GET / requests.get |
發出網路請求去抓資料 | 第 2 章 |
| query string | 網址問號後面帶的一串參數 | 第 2 章 |
JSON / resp.json() |
API 回傳的文字格式 / 把它轉成 Python dict | 第 2 章 |
| status_code 200 | HTTP 狀態碼,200 代表請求成功 | 第 2 章 |
| DataFrame / pandas | 表格型資料結構 / 處理它的套件 | 第 2 章 |
CSV / to_csv / utf-8-sig |
逗號分隔的表格檔 / 存檔方法 / 讓 Excel 開中文不亂碼的編碼 | 第 2、4 章 |
排程
| 名詞 | 白話說明 | 詳見 |
|---|---|---|
| APScheduler | Python 的排程套件,定時觸發函式 | 第 9 章 |
| BackgroundScheduler | 背景執行的排程器,需自己用 while True 保活 |
第 9 章 |
| BlockingScheduler | 會卡住主執行緒的排程器,適合「專職跑排程」 | 第 9 章 |
| cron / trigger | 用「秒 分 時 星期」格式定時的觸發方式 | 第 9 章 |
| coalesce | 錯過排程時只補跑一次,避免一次爆發 | 第 9 章 |
環境與工具
| 名詞 | 白話說明 | 詳見 |
|---|---|---|
| Docker / container / image | 容器技術 / 執行中的容器 / 容器的範本 | 第 1 章 |
| docker compose | 用一個檔案定義並一次啟動多個容器 | 第 1 章 |
| uv | 比 pip 快很多的 Python 套件 / 環境管理器 | 第 1 章 |
| 環境變數 | 從系統傳給程式的設定值,讓連線資訊不用寫死在程式碼裡 | 第 1 章 |
效能與併發觀念
| 名詞 | 白話說明 | 詳見 |
|---|---|---|
| I/O 密集 | 任務大部分時間在「等」(網路、硬碟、DB) | 第 2、6 章、補充上半 |
| CPU 密集 | 任務大部分時間在「算」 | 第 9 章、補充上半 |
| 並發 concurrency / 並行 parallelism | 「看起來同時」/「真的同時」 | 補充上半 |
| GIL | Python 的全域直譯器鎖,同時只有一條執行緒能跑 Python | 補充上半 |
專案裡的慣例
| 名詞 | 白話說明 | 詳見 |
|---|---|---|
_print 版 |
只印出、不寫資料庫的教學版本,用來先驗證流程再上正式版 | 第 2 章起 |
| 模組 vs 任務 | 一個「模組(檔案)」可含多個 @app.task;include 以檔案為單位掛入,任務以函式為單位註冊 |
第 1 章 |
| twse / tpex | 本專案自訂的兩條佇列名稱,分別代表上市 / 上櫃 | 第 3 章 |

送件鴿飛出去了