Python Module/Package:把 test repo 的爬蟲整理成 Package
12. Python Module / Package — 從 test repo 出發
📣 「你 EP01 clone 的 test repo 裡已經有好幾個 .py 檔。接下來我們用這些檔案來理解什麼是 module、什麼是 package,然後讓它長大。」
12-1. 先看你的 test repo 現在長什麼樣
~/test/
├── .gitignore
├── .python-version ← pyenv / uv 設定用
├── README.md
├── main.py ← 程式進入點
├── pyproject.toml ← 專案設定檔
├── uv.lock ← 套件版本鎖定
├── finmind.py ← FinMind API 抓股價
├── ptt_crawler.py ← PTT 爬蟲
├── hahow_crawler.py ← Hahow API 爬蟲
├── test.py ← 簡單 requests 測試
├── tqdm_test.py ← 進度條範例
├── ptt.ipynb ← Jupyter 爬蟲範例
└── test.ipynb ← Jupyter 測試
這些 .py 檔全部散在同一層。目前能跑,但隨著專案變大,你會遇到問題。
12-2. Module 是什麼
module(模組)= 一個 .py 檔
你 test repo 裡的每一個 .py 檔都是一個 module:
finmind.py→ 一個 module,裡面有crawler_finmind()函式ptt_crawler.py→ 一個 module,裡面有ptt_crawler()函式main.py→ 一個 module,裡面有main()函式
直接執行 module:
cd ~/test
python main.py # 印出 Hello from test!
python finmind.py # 抓 0050 股價
python ptt_crawler.py # 爬 PTT Stock 版
從別的檔案 import module:
# 在同一個資料夾裡,可以互相 import
import finmind # 匯入整個 module
from ptt_crawler import ptt_crawler # 只匯入某個函式
📣 「module 就是『把可重用的程式碼裝進一個 .py 檔』。你寫的每一個 .py 都是 module。」
12-3. if __name__ == "__main__" 的用途
看 test repo 裡的 main.py:
# main.py
def main():
print("Hello from test!")
if __name__ == "__main__":
main()
| 情境 | __name__ 的值 |
main() 會跑嗎 |
|---|---|---|
python main.py(直接執行) |
"__main__" |
✅ 會跑 |
import main(被別人 import) |
"main" |
❌ 不會跑 |
問題:finmind.py 和 ptt_crawler.py 底部沒有用 if __name__,直接寫了執行程式碼:
# finmind.py 最後一行
crawler_finmind(stock_id="0050") # 被 import 時也會跑!
📣 「如果你
import finmind,它會直接跑爬蟲——這不是你想要的。所以好的 module 要用if __name__ == '__main__'把『自己跑的程式碼』包起來。」
12-4. 現在的問題:test repo 不是 Package
目前 test repo 的所有 .py 檔散在同一層,這會造成幾個問題:
| 問題 | 說明 |
|---|---|
| 只能在同資料夾 import | cd ~/test 才能 import finmind,在別的地方不行 |
| 不能 pip install | 別人拿到你的 code,沒辦法用 pip 安裝 |
| 沒有結構 | 爬蟲、清洗、存儲全混在一起,檔案多了會很亂 |
沒有 __init__.py |
Python 不認為這是一個 package |
解法:把散落的 module 組織成一個 Package。
12-5. Package 是什麼
package(套件)= 一個含 __init__.py 的資料夾
把 test repo 的爬蟲們整理成 package 的樣子:
❌ 現在(散的 module) ✅ 整理後(package)
test/ test/
├── finmind.py ├── crawlers/ ← 資料夾 = package
├── ptt_crawler.py │ ├── __init__.py ← 加這個!
├── hahow_crawler.py │ ├── finmind.py
├── main.py │ ├── ptt_crawler.py
└── ... │ └── hahow_crawler.py
├── main.py
└── pyproject.toml
加了 __init__.py 之後就能這樣用:
from crawlers import finmind
from crawlers.ptt_crawler import ptt_crawler
| Module | Package | |
|---|---|---|
| 是什麼 | 一個 .py 檔 |
一個含 __init__.py 的資料夾 |
| 裝什麼 | 函式 / 變數 / 類別 | 多個 module |
| import | import finmind |
from crawlers import finmind |
12-6. __init__.py 的兩個作用
作用一:標記——有 __init__.py,Python 才把這個資料夾當成 package。
作用二:控制匯出——把深層的函式 import 上來,使用者不用記內部結構。
# crawlers/__init__.py
from .finmind import crawler_finmind
from .ptt_crawler import ptt_crawler
from .hahow_crawler import crawler_hahow_course
沒有往上提: from crawlers.ptt_crawler import ptt_crawler (要知道在哪個檔案)
有往上提: from crawlers import ptt_crawler (只要知道在 crawlers)
📣 「
__init__.py就像大樓的大廳服務台——把各樓層的服務集中在大廳,訪客不用自己找。」
12-7. import 機制怎麼運作
你寫:from crawlers import finmind
│
▼
Python 依序去這些地方找名叫 crawlers 的 package:
① 當前工作目錄
② 已安裝的套件目錄(site-packages) ← pip install / uv add 裝的在這
③ 標準函式庫
│
找到了 → 先執行 __init__.py,再找 finmind
找不到 → ModuleNotFoundError
📣 「這就解釋了為什麼
import有時會報ModuleNotFoundError——Python 在那串路徑裡找不到它。用pip install -e .或uv add,就是把 package 登記到搜尋路徑裡。」
12-8. pyproject.toml:專案的設定檔
test repo 已經有 pyproject.toml——這就是告訴工具「這個專案叫什麼、需要什麼套件」的地方:
# test/pyproject.toml(已經存在的)
[project]
name = "test"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"beautifulsoup4>=4.13.4",
"pandas>=2.3.1",
"requests>=2.32.4",
"tqdm>=4.67.1",
]
| 欄位 | 意思 |
|---|---|
name |
專案名稱 |
version |
版本號 |
requires-python |
最低 Python 版本 |
dependencies |
這個專案需要的套件(uv add 會自動更新這裡) |
📣 「pyproject.toml 是現代 Python 的標準設定檔,取代舊的 setup.py。uv、pip、pipenv 都讀這個檔案。」
12-9. 真實專案怎麼做:看 de-project
DataEngCamp/de-project 就是一個完整的 package 範例:
de-project/
├── pyproject.toml
├── .python-version
├── uv.lock
├── data_ingestion/ ← Package!
│ ├── __init__.py ← 有這個
│ ├── crawlers.py ← 爬蟲 module
│ ├── celery_task.py ← 非同步任務
│ ├── producer.py ← 訊息發布
│ └── mysql_module.py ← 資料庫
├── airflow/
│ └── dags/
└── docker-compose files...
跟你的 test repo 比較:
| test repo(現在) | de-project(目標) | |
|---|---|---|
| 結構 | .py 散在同一層 | .py 收進 data_ingestion/ 資料夾 |
__init__.py |
❌ 沒有 | ✅ 有 |
| 可 pip install | ❌ | ✅ |
| pyproject.toml | ✅ 有 | ✅ 有 |
📣 「兩個專案做同一件事(爬蟲),差別只在結構。學完 Package 的概念,你就知道 de-project 為什麼這樣組織了。」
13. Lab:把 test repo 的爬蟲整理成 Package
📂 在
~/test資料夾裡操作。
Step 1:先跑 test repo 的 module(確認現在能用)
cd ~/test
python main.py # Hello from test!
python finmind.py # 抓 0050 股價
python ptt_crawler.py # 爬 PTT Stock 版
✅ 預期:三個都能跑,輸出正常
Step 2:試試在 Python 裡 import
python
>>> import finmind # 會直接跑爬蟲(因為底部沒包 if __name__)
>>> from ptt_crawler import ptt_crawler
>>> result = ptt_crawler("Gossiping")
>>> print(result)
>>> exit()
📣 「注意
import finmind直接跑了爬蟲——這就是沒有if __name__的問題。」
Step 3:建立 crawlers package
# 在 test/ 裡建 crawlers 資料夾
mkdir crawlers
# 把爬蟲檔案複製進去(保留原檔)
cp finmind.py crawlers/
cp ptt_crawler.py crawlers/
cp hahow_crawler.py crawlers/
# 建立 __init__.py(關鍵!)
touch crawlers/__init__.py
現在結構變成:
test/
├── crawlers/
│ ├── __init__.py ← 新建的
│ ├── finmind.py
│ ├── ptt_crawler.py
│ └── hahow_crawler.py
├── main.py
├── finmind.py ← 原檔還在
└── ...
Step 4:編輯 __init__.py
# crawlers/__init__.py
from .finmind import crawler_finmind
from .ptt_crawler import ptt_crawler
from .hahow_crawler import crawler_hahow_course
💡
.finmind前面的.是相對匯入——代表「同一個資料夾裡的 finmind.py」。
Step 5:用 package 的方式 import
cd ~/test
python
>>> from crawlers import crawler_finmind
>>> from crawlers import ptt_crawler
>>> result = ptt_crawler("Tech_Job")
>>> print(result)
>>> exit()
✅ 預期:可以從 crawlers 匯入函式,而且不會自動跑爬蟲(因為我們只 import 了函式)
Step 6:看 de-project 的 package(對照)
cd ~/de-project
ls data_ingestion/
cat data_ingestion/__init__.py
📣 「de-project 的
data_ingestion/跟你剛才建的crawlers/結構一模一樣——有__init__.py、有多個 module。只是它的 module 更複雜(有 Celery、MySQL 等)。」
Step 7:確認 pyproject.toml
test repo 的 pyproject.toml 已經寫好了,dependencies 裡有 beautifulsoup4、pandas、requests——這些就是 crawlers 裡面的 .py 用到的套件。
cat pyproject.toml
📣 「pyproject.toml 的 dependencies 要跟你 code 裡 import 的套件對應。你用了 requests 就要列在 dependencies 裡,不然別人裝你的專案會缺套件。」
Step 2 補充:看 FinMind package 結構
cd ../finmind
find . -type f | grep -v __pycache__ | grep -v egg-info
✅ 預期:
./finmind/__init__.py ./finmind/extract.py ./finmind/transform.py ./finmind/load.py ./pyproject.toml ./tests/test_transform.py
⚠️ 注意這裡有兩層
finmind:
- 外層
finmind/(你現在所在)= 專案資料夾,裝著pyproject.toml、tests/- 內層
finmind/(含__init__.py)= 真正的套件,import 時打的就是這個名字這是 Python 專案的標準長相:專案資料夾和套件常常同名。
import finmindimport 的是內層那個。
📸 截圖 Q3:VS Code 檔案樹顯示 finmind package 結構
- 紅框①標出外層專案資料夾與內層
finmind/套件資料夾- 紅框②標出
__init__.py- 紅框③標出
pyproject.toml
Step 3:看 __init__.py 和 pyproject.toml(對照 Part 4)
用 VS Code 打開 finmind/__init__.py 和 pyproject.toml:
📸 截圖 Q4:init.py 內容(標出
from finmind.extract import ...往上提 +run_pipeline()) 📸 截圖 Q5:pyproject.toml 內容(標出[project] name、dependencies、packages.find)
📣 「這就是 12-4 講的『大廳服務台』和 12-6 講的『現代安裝描述檔』的實作。把 extract / transform / load 三個 module 往上提,再用
run_pipeline()串成一條 E→T→L pipeline。」
Step 4:用 pipenv 安裝這個 package(可編輯模式)
pipenv install -e .
📣 「對照 12-5:
-e是 editable,pip 在 site-packages 放一個指標指回這個資料夾,改 code 不用重裝。裝完後,import finmind在電腦任何地方都有效。」
📸 截圖 Q6:pipenv install -e . 的輸出(標出「Installing -e .」+「-e 是 editable」)
✅ 預期:安裝成功,Pipfile 多了
finmind = {editable = true, path = "."}
Step 5:在任何地方都能 import
pipenv run python -c "from finmind import run_pipeline; print(run_pipeline('/tmp/stock.csv'), '筆已寫入')"
cat /tmp/stock.csv
✅ 預期:印出
4 筆已寫入,CSV 有 4 筆(5 筆原始,1 筆缺值被清掉)
📸 截圖 Q7:run_pipeline 結果 + stock.csv 內容(標出「4 筆」+ CSV 內容)
📣 「5 筆原始,1 筆缺值被 transform 清掉,剩 4 筆。這就是一條最小的 ETL pipeline,只是現在是模組化的 package——這就是『工程化』的第一步。」
Step 6:跑測試(pytest)
pipenv install -e ".[dev]"
pipenv run pytest -v
📣 「
.[dev]就是 12-6 講的 optional-dependencies——把測試用的 pytest 一起裝進來。」
📸 截圖 Q8:pytest 結果(標出「3 passed」+ 三個測試名稱)
✅ 預期:
tests/test_transform.py::test_clean_drops_none_close PASSED tests/test_transform.py::test_clean_strips_comma_and_casts_float PASSED tests/test_transform.py::test_run_pipeline_writes_rows PASSED 3 passed
📣 「三個測試:①缺值清除 ②逗號轉 float ③整條 pipeline。以後改了 code,跑一次 pytest 就知道有沒有破壞東西。這是『能跑』升級成『能讓人放心』的關鍵。」
Step 7:這個 finmind 是「參考答案」,不用 push
⚠️ 你剛跑的 finmind 是 clone 下來的老師範例,不是你的 repo——不要 push 它(你也沒權限)。它是「標準答案」:每集老師會更新它,你
git pull就能跟上。
cd ~/serial-course
git pull
📣 「老師的 finmind 是你的對照組——卡住時看它怎麼寫。你自己的工程化任務在課後作業:把今天學的 package 寫法,套回你自己 repo 裡的個人 MVP。」

送件鴿飛出去了