--- reviewed_by: rd: pm: --- # 政府採購標案 API — `tw.openfun~api~procurement` > **給 AI 閱讀的使用指引。** > 人類可在 https://data.openfun.tw/datasets/tw.openfun~api~procurement 看到資料集說明。 > 詳細資料知識見 [knowledge.md](knowledge.md)。 --- ## ⚠️ 開始之前(AI agent 必讀,摘要也請保留這段) 資料集 slug:tw.openfun~api~procurement Base URL:https://pcc-api.openfun.app 認證:不需要(此 API 完全公開,無需 Token 或帳號) 查詢參數語言:英文(`query`、`unit_id`、`job_number` 等) 分頁參數:`page`(頁碼,從 1 開始),回應含 `total_records`、`total_pages` 最簡查詢範例:curl "https://pcc-api.openfun.app/api/searchbycompanyname?query=開放文化基金會" > **⚠️ curl 帶中文參數可能靜默失敗**:在某些環境(macOS / Linux)下,curl 帶中文的 `query=` 參數不會自動 URL encode,導致 API 回傳非 JSON 內容並造成解析錯誤。**凡是 `query` 含中文,一律改用 Python:** > > ```python > import urllib.request, urllib.parse, json > params = urllib.parse.urlencode({'query': '台北跨年', 'page': 1}) > with urllib.request.urlopen(f'https://pcc-api.openfun.app/api/searchbytitle?{params}') as r: > data = json.load(r) > print(data['total_records'], data['total_pages']) > ``` 禁止抓取 HTML 頁面(政府採購網有 bot 保護,WebFetch 讀 HTML 頁面可能失敗)。 授權標注:使用此資料產出的內容需標注「資料來源:歐噴資料庫(data.openfun.tw)/行政院公共工程委員會」 --- ## 資料來源與更新頻率 | 項目 | 說明 | |------|------| | 原始來源 | 行政院公共工程委員會([政府電子採購網 pcc.gov.tw](https://web.pcc.gov.tw)) | | API 服務 | 由歐噴維護(pcc-api.openfun.app) | | 涵蓋範圍 | 政府電子採購網所有公告(招標、決標、廢標等) | | 更新頻率 | 每日更新 | | 授權 | 政府資料開放授權條款-第 1 版 | --- ## 這份資料集能回答什麼問題 **可以回答:** - 某家公司(依名稱或統一編號)得過哪些政府標案? - 某個機關發過哪些採購公告? - 某個標案的詳細資訊(決標廠商、金額、決標方式等)? - 特定日期有哪些採購公告? - 前瞻基礎建設等特別預算花在哪些標案? - 某關鍵字相關的標案有哪些? **無法回答:** - 即時採購招標通知(資料有一定延遲) - 地方政府自行辦理、未上政府電子採購網的標案 - 採購以外的政府支出(補助、移轉支付等) --- ## API 端點總覽 | 端點 | 說明 | 必填參數 | |------|------|---------| | `GET /api/getinfo` | 資料狀況(最新/最舊時間、總公告數) | 無 | | `GET /api/searchbycompanyname` | 依公司名稱搜尋相關標案 | `query` | | `GET /api/searchbycompanyid` | 依統一編號搜尋相關標案 | `query` | | `GET /api/searchbytitle` | 依標案名稱關鍵字搜尋 | `query` | | `GET /api/listbydate` | 列出特定日期的所有公告 | `date`(YYYYMMDD) | | `GET /api/unit` | 列出所有機關代碼與名稱 | 無 | | `GET /api/listbyunit` | 列出特定機關的所有標案 | `unit_id` | | `GET /api/tender` | 查詢特定標案的完整公告記錄 | `unit_id` + `job_number` | | `GET /api/searchallspecialbudget` | 列出所有特別預算名稱 | 無 | | `GET /api/searchbyspecialbudget` | 搜尋特定特別預算的標案 | `query` | --- ## 常用查詢範例 ### 查廠商得標記錄 ```bash # 依公司名稱搜尋(模糊比對) curl "https://pcc-api.openfun.app/api/searchbycompanyname?query=開放文化基金會" # 依統一編號搜尋(精確) curl "https://pcc-api.openfun.app/api/searchbycompanyid?query=38552170" # 第 2 頁 curl "https://pcc-api.openfun.app/api/searchbycompanyname?query=中鋼&page=2" # 加上額外欄位(預設只回傳摘要,columns[] 可要求更多欄位) # 注意:含 [] 的 URL 必須加 -g 旗標,否則 curl 會把 [] 當 glob pattern 報錯 curl -g "https://pcc-api.openfun.app/api/searchbycompanyname?query=開放文化基金會&columns[]=已公告資料:決標方式&columns[]=機關資料:聯絡人" ``` ### 查標案 ```bash # 依標案名稱關鍵字搜尋 curl "https://pcc-api.openfun.app/api/searchbytitle?query=開放政府國家行動方案" # 查特定標案的完整公告(需 unit_id + job_number,可從搜尋結果取得) curl "https://pcc-api.openfun.app/api/tender?unit_id=A.41&job_number=ndc109050" ``` ### 關鍵字搜尋跨頁取全部結果(Python,推薦) curl 帶中文會靜默失敗,跨頁查詢建議用 Python 一次取完: ```python import urllib.request, urllib.parse, json def search_all_pages(keyword, filter_unit=None): """搜尋標案名稱,自動翻頁取全部結果,可選填機關名稱過濾。""" all_records = [] page = 1 while True: params = urllib.parse.urlencode({'query': keyword, 'page': page}) with urllib.request.urlopen( f'https://pcc-api.openfun.app/api/searchbytitle?{params}' ) as r: data = json.load(r) for rec in data['records']: if filter_unit is None or filter_unit in rec.get('unit_name', ''): all_records.append(rec) if page >= data['total_pages']: break page += 1 return all_records # 範例:取得所有台北市跨年相關標案 records = search_all_pages('跨年', filter_unit='臺北市') for r in records: print(r['date'], r['unit_name'], r['brief']['title']) ``` ### 批次取得廠商得標金額(正確做法) 搜尋結果的金額欄位常為 null,需逐筆呼叫 `/api/tender` 取真實金額。注意限速: ```python import urllib.request, urllib.parse, json, time, re from collections import defaultdict HEADERS = {'User-Agent': 'Mozilla/5.0'} def fetch(url, delay=0.5): """加 User-Agent + delay,避免 403/429。""" time.sleep(delay) req = urllib.request.Request(url, headers=HEADERS) with urllib.request.urlopen(req, timeout=20) as r: return json.load(r) def parse_amount(s): """'29,500,000元' → 29500000""" if not s: return 0 return int(re.sub(r'[^\d]', '', s)) # 步驟一:搜尋取全部公告(逐頁,加 delay) keyword = '社團法人' all_records = [] page = 1 params = urllib.parse.urlencode({'query': keyword, 'page': page}) first = fetch(f'https://pcc-api.openfun.app/api/searchbycompanyname?{params}') total_pages = first['total_pages'] all_records.extend(first['records']) for page in range(2, total_pages + 1): params = urllib.parse.urlencode({'query': keyword, 'page': page}) data = fetch(f'https://pcc-api.openfun.app/api/searchbycompanyname?{params}') all_records.extend(data['records']) # 步驟二:過濾決標公告,依廠商名稱統計案件數 company_cases = defaultdict(list) AWARD_TYPES = {'決標公告', '更正決標公告'} for rec in all_records: rec_type = rec.get('brief', {}).get('type', '') if rec_type not in AWARD_TYPES: continue for name in rec.get('brief', {}).get('companies', {}).get('names', []): if keyword in name: company_cases[name].append(rec.get('tender_api_url', '')) # 步驟三:對前 N 名逐筆取金額(限速,勿並發) top_n = sorted(company_cases, key=lambda k: len(company_cases[k]), reverse=True)[:10] company_total = {} for name in top_n: total = 0 for url in company_cases[name][:50]: # 每家最多取 50 筆 try: data = fetch(url, delay=0.5) for rec in data.get('records', []): if rec.get('brief', {}).get('type') == '決標公告': amt = parse_amount(rec.get('detail', {}).get('決標資料:總決標金額', '')) total += amt except Exception as e: print(f' error: {e}') company_total[name] = total print(f'{name}: {total//10000:.0f} 萬元 ({len(company_cases[name])} 案)') ``` ### 查機關 **`/api/unit` 回傳格式**:`{unit_id: unit_name}` 的**扁平 dict**,不是 records 陣列。例: ```json {"4": "立法院", "A.41": "國家發展委員會", ...} ``` **`/api/listbyunit` 分頁欄位**:使用 `total_page`(**單數**),與其他端點的 `total_pages`(複數)**不同**。 ```bash # 取得所有機關代碼(回傳 flat dict,不是 records[]) curl "https://pcc-api.openfun.app/api/unit" # 列出某機關所有標案(注意:分頁用 total_page 不是 total_pages) curl "https://pcc-api.openfun.app/api/listbyunit?unit_id=4" curl "https://pcc-api.openfun.app/api/listbyunit?unit_id=4&page=2" ``` **依機關名稱查詢並取全部標案(Python)**: ```python import urllib.request, json, time # Step 1: /api/unit 回傳 flat dict {unit_id: unit_name} with urllib.request.urlopen('https://pcc-api.openfun.app/api/unit') as r: units = json.load(r) # flat dict,不是 records 陣列 # 依名稱找 unit_id(精確比對) target_name = '立法院' unit_id = next((k for k, v in units.items() if v == target_name), None) print(f'{target_name} unit_id = {unit_id}') # → "4" # Step 2: listbyunit 用 total_page(單數!),不是 total_pages all_records = [] page = 1 while True: time.sleep(0.3) url = f'https://pcc-api.openfun.app/api/listbyunit?unit_id={unit_id}&page={page}' with urllib.request.urlopen(url) as r: data = json.load(r) all_records.extend(data['records']) if page >= data['total_page']: # ⚠️ total_page(單數),非 total_pages break page += 1 print(f'共 {len(all_records)} 筆') ``` ### 查特別預算 ```bash # 先取得所有特別預算名稱 curl "https://pcc-api.openfun.app/api/searchallspecialbudget" # 再搜尋特定特別預算的標案 curl "https://pcc-api.openfun.app/api/searchbyspecialbudget?query=前瞻基礎建設" ``` ### 查特定日期公告 ```bash # 日期格式:YYYYMMDD(整數) curl "https://pcc-api.openfun.app/api/listbydate?date=20240101" ``` ### 查資料整體狀況 ```bash curl "https://pcc-api.openfun.app/api/getinfo" # 回傳:{ "最新資料時間": "...", "最舊資料時間": "...", "公告數": N } ``` --- ## 欄位說明 ### 關鍵欄位補充說明 搜尋類端點(`searchby*`、`listby*`)的回應格式: ```json { "query": "搜尋詞", "page": 1, "total_records": 304, "total_pages": 4, "took": 0.123, "records": [ ... ] } ``` 每筆 `record` 的關鍵欄位: | 欄位 | 說明 | |------|------| | `date` | 公告日期(YYYYMMDD) | | `filename` | 公告檔名(唯一識別碼) | | `unit_id` | 機關代碼 | | `unit_name` | 機關名稱 | | `job_number` | 標案代碼 | | `brief.type` | 公告類型(招標/決標/廢標等) | | `brief.title` | 標案名稱 | | `brief.companies` | **搜尋/列表結果中的所有相關廠商(含投標廠商,不限得標者)**;格式為 dict,含 `ids`(統編陣列)、`names`(名稱陣列),**不是 list**,不可直接 iterate。**實際得標廠商**在 `/api/tender` 回傳的 `detail['決標品項:第1品項:得標廠商1:得標廠商']`(多品項則 `第N品項`,多家得標則 `得標廠商M`)。 | | `tender_api_url` | 查此標案完整資料的 API URL | | `detail` | 完整欄位(需用 `columns[]` 參數或呼叫 `/api/tender` 取得) | --- ## 注意事項與限制 1. **`tender_api_url`**:搜尋結果中每筆 record 都有此欄位,直接 fetch 即可取得完整標案資料,不需手動組 URL。 2. **`columns[]` 與 curl**:`columns[]` 參數含有 `[]`,curl 預設會將 URL 中的 `[]` 解讀為 glob range(如 `[A-Z]`),導致錯誤(exit code 49)。使用時必須加 `-g`(`--globoff`)旗標關閉 glob: ```bash curl -g "https://pcc-api.openfun.app/api/searchbycompanyname?query=中鋼&columns[]=已公告資料:決標金額" ``` 3. **`/api/tender`**:回傳同一標案的所有相關公告,格式是 `{"unit_name": "...", "records": [...]}` 包裝,**不是直接 list**。每筆 record 的公告類型在 `brief.type`(不是頂層 `type`),金額在 `detail` dict 中: - `決標公告`:`detail['決標資料:總決標金額']`(字串,含逗號,如 `"30,000,000元"`) - `招標公告`:`detail['採購資料:預算金額']` ```python import urllib.request, json, re with urllib.request.urlopen('https://pcc-api.openfun.app/api/tender?unit_id=3.79.66&job_number=1130036') as r: data = json.load(r) records = data.get('records', []) # 外層是 {"unit_name":..., "records":[...]} for rec in records: rec_type = rec.get('brief', {}).get('type', '') # ⚠️ type 在 brief 裡,不是頂層 if rec_type == '決標公告': detail = rec.get('detail', {}) amt_str = detail.get('決標資料:總決標金額', '') or '' amt = int(re.sub(r'[^\d]', '', amt_str)) if amt_str else 0 print(amt) # e.g. 29500000 ``` 4. **搜尋結果的金額欄位可能是 null**:`searchbycompanyname`/`searchbytitle` 加 `columns[]=決標資料:總決標金額` 取回的 `detail` 中,**金額值經常是 null**(部分類型公告不回傳)。若需要準確金額,必須對每筆取 `tender_api_url` 呼叫 `/api/tender` 取 `detail`。 5. **速率限制(429)**:此 API 有嚴格速率限制,**批次呼叫(包括 `/api/listbyunit`、`/api/tender`)必須全程循序執行,不可任何並發**(即使只有 2 個 worker 也會觸發 IP 層級封鎖,且封鎖持續數分鐘)。每次請求之間加 `time.sleep(0.3)` 以上(推薦 0.5)。 6. **搜尋結果上限 10,000 筆**:`searchbycompanyname` 等端點最多回傳 10,000 筆(100 頁 × 100 筆),超過此數量的查詢無法取得完整資料。可用更精確的關鍵字或加其他篩選縮小範圍。 7. **`date` 格式**:`listbydate` 的 `date` 參數為整數格式(`20240101`),非 ISO 日期字串。 8. **廠商比對**:`searchbycompanyname` 為模糊比對;`searchbycompanyid` 依統一編號精確比對,較可靠。 9. **小額採購不在資料集**:依政府採購法第 19 條,僅達公告金額以上之採購須公開招標公告;未達門檻的小額採購不會出現在本資料集。 --- ## 快速參考 | 項目 | 說明 | |------|------| | Base URL | `https://pcc-api.openfun.app` | | 認證 | 不需要(完全公開) | | 查詢參數語言 | 英文(`query`、`unit_id`、`job_number`、`date`、`page`) | | 分頁 | `?page=1`,回應含 `total_records`、`total_pages` | | 完整文件 | https://pcc-api.openfun.app/swagger.json | | 主要搜尋方式 | 公司名稱 / 統一編號 / 標案名稱 / 機關 / 日期 / 特別預算 | | 搜尋回應格式 | JSON,含 `total_records`、`total_pages`、`records[]` | | tender 回應格式 | JSON,含 `unit_name`、`records[]`(非直接 list) |