把多家品牌的「機油等效對照表」(PDF / Excel)統整成一個 SQLite 資料庫, 作為日後查詢 app 的單一資料來源。
2026_oil_equivalent/
├── raw/ # 原始檔案(按來源分資料夾,唯讀)
│ ├── lubmarine/ # Total Lubmarine 競品對照表
│ ├── q8oils/ # Q8 Oils 對照表
│ └── safra/ # Safra Química 對照表
├── parsers/ # 各來源的專屬 parser
│ ├── base.py # 共用 dataclass: Product / Equivalence / ParseResult
│ ├── parse_matrix_pdf.py # 通用「橫式矩陣 PDF」抽取器(pdfplumber)
│ ├── parse_lubmarine_xlsx.py
│ ├── parse_q8_matrix_pdf.py
│ └── parse_safra_matrix_pdf.py
├── staging/ # parser 輸出的中繼 CSV(人類可審)
├── db/equivalents.sqlite # 統一資料庫(查詢 app 直接讀)
├── manifest.json # 已處理檔案 hash,避免重複 ingest
├── ingest.py # 主流程
└── requirements.txt
products(id, brand, product_name, category, description, source_file, source_date)
equivalents(product_a_id, product_b_id, confidence, source_file)
-- confidence: 'official' (Lubmarine 原廠對照) | 'closest' (PDF 矩陣)把資料統一成「產品節點 + 對等關係邊」的圖結構, 查詢任一品牌的型號即可反查所有等效品。
python -m venv .venv
.venv\Scripts\activate # Windows
pip install -r requirements.txtpython ingest.py # 處理 raw/ 下所有未見過或更動過的檔
python ingest.py --dry-run # 只產生 staging CSV,不寫入 DB
python ingest.py --reingest # 砍掉 DB 與 manifest 重新建ingest.py 會:
- 掃描
raw/下所有檔案、計算 sha256 - 與
manifest.json比對,跳過已處理且未變動的檔 - 依
ROUTES表把檔案路由到對應的 parser - parser 產出中繼 CSV 到
staging/(供人工檢查) - upsert 進
db/equivalents.sqlite - 更新
manifest.json
- 把新檔丟進
raw/<品牌>/ python ingest.py --dry-run先產生中繼 CSV,目視檢查staging/是否合理python ingest.py寫入 DB
- 把新檔丟進
raw/<新品牌>/ - 先用
pdfplumber.extract_tables()或openpyxl在 REPL 中觀察結構 - 在
parsers/新增一支 parser:- 若是「橫式矩陣 PDF」→ 直接呼叫
parse_matrix_pdf.parse()並提供BRAND_COLUMNS/SPEC_COLUMNS/ref_brand設定(參考parse_q8_matrix_pdf.py、parse_safra_matrix_pdf.py) - 若是直式 Excel → 仿
parse_lubmarine_xlsx.py - 其他格式 → 自行回傳
ParseResult(products=[...], equivalences=[...])
- 若是「橫式矩陣 PDF」→ 直接呼叫
- 在
ingest.py的ROUTES表加上 glob → parser 對應 python ingest.py --dry-run驗證、再正式 ingest
- 直接覆蓋
raw/<品牌>/<檔名> - 跑
python ingest.py,hash 變動會自動觸發重 parse 與 upsert - 注意:不會自動刪除舊資料的 product/edge。
若刪除了某些列,建議用
--reingest整批重建
| 來源 | products | equivalences |
|---|---|---|
| Lubmarine xlsx | 1868 | 1712(official) |
| Q8 PDF | 727 | 631(closest,Q8↔他牌) |
| Safra PDF | 458 | 1930(closest,11 牌全配對) |
| DB 總計 | 2973(去重後) | 4124 |
import sqlite3
conn = sqlite3.connect('db/equivalents.sqlite')
q = """
SELECT p2.brand, p2.product_name, e.confidence
FROM products p1
JOIN equivalents e ON e.product_a_id=p1.id OR e.product_b_id=p1.id
JOIN products p2 ON p2.id = CASE WHEN e.product_a_id=p1.id
THEN e.product_b_id ELSE e.product_a_id END
WHERE p1.brand=? AND p1.product_name LIKE ?
"""
for row in conn.execute(q, ('SHELL', '%VALVATA 460%')):
print(row)- 查詢 app:可用 Streamlit/FastAPI 直接讀
db/equivalents.sqlite - 品名正規化:目前不同來源拼法可能不一(如
5W-40vsSAE 5W40), 可在parsers/base.py加normalize_product_name()統一 - 多跳查詢:以
equivalents邊做 BFS 可推導「A↔B↔C」傳遞性等效 (注意 confidence 會逐跳衰減,建議限制 hop 數) - 新增掃描影像 PDF 來源:若日後遇到掃描影像 PDF(pdfplumber 抽不到表), 再導入 OCR(Tesseract);目前三份檔案皆有可抽取文字