AI & Data Dreamers練功房
寶箱片場規則
前傳・開工前環境安裝

Python Module/Package:把 test repo 的爬蟲整理成 Package

← 回資料館・在 GitHub 看原始檔(de-course)・裝到一半卡住?左下角送件鴿或 Discord #觀眾服務台 喊一聲

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 finmind import 的是內層那個。

📸 截圖 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。」