Files
Aislo/resources/knowledge/original/_pipeline/collect_cost_sources.py
T
eomsangdon c998afd3b6 docs(knowledge): 값형 원본 스냅샷 저장·실무 자재 실측 반영
- 계획 변경(사용자): 값형도 original/원가계산/에 원본 스냅샷 보존
  - 자재단가/ 나라장터 27,485행 CSV+JSON, 환율_한국은행ECOS/ 연초~일별 760행,
    유가_오피넷/ 전국평균+7일 JSON — collect_cost_sources.py values 모드 신설
- 실무 실측(울진 간선임도 46종·영월 58종): 관급 별도계상 16·조달가 15·물가지/견적 ~15
  - 실무 대비표 슬롯 = 조달가격/물가정보/물가자료/적산정보/견적단가 관측
  - 원가_기초데이터_원천.md §5 신설 — 유료지 실수요 = 공사당 사급 잡자재 15종 내외
- 노임 PDF 값 추출 가능 확인(직종코드 앵커), 품셈 기계경비 장(8장) 존재 확인
2026-08-14 16:54:02 +09:00

190 lines
9.0 KiB
Python

# -*- coding: utf-8 -*-
"""원가 기초단가 원천 수집 스크립트 (미결 No.17 / 수집지침 W8 예정)
수집 대상 → 저장 위치 (original/원가계산/):
1. 건설업 노임 : 대한건설협회 건설임금 게시판 → 노임단가_건설업_대한건설협회/
2. 제조업 노임 : 중소기업중앙회 조사·통계 게시판 → 노임단가_제조업_중소기업중앙회/
3. 건설공사 표준품셈 : 한국표준품셈정보원(kseis) 자료실 → 건설공사_표준품셈/
4. 자재단가 스냅샷 : 나라장터 가격정보현황서비스 API → 자재단가/ (CSV+JSON, 수집일 파일명)
5. 환율 스냅샷 : 한국은행 ECOS API 731Y001 → 환율_한국은행ECOS/ (CSV, 연초~수집일 일별)
6. 유가 스냅샷 : 오피넷 API → 유가_오피넷/ (JSON, 수집일)
사용법:
python collect_cost_sources.py docs # 1~3 문서 다운로드 (게시판 최신글 자동 탐색은 미구현 —
# 반기마다 아래 URL 상수를 최신 게시글로 갱신 후 실행)
python collect_cost_sources.py values # 4~6 값형 API 원본 스냅샷 저장 (키 = knowledge/.secrets.local.md)
주의: 문서 URL(게시글 seq·uuid)은 반기마다 바뀐다. 갱신 절차는 knowledge/01_수집지침.md W8 예정.
값형(4~6)은 원본 스냅샷을 폴더에 보존하고, 프로그램용 데이터셋은 여기서 별도 추출해 구성한다
(2026-08-14 사용자 결정). 표준시장단가는 수집 제외 (100억 미만 공사 미적용·품셈 방식과 별도 트랙).
"""
import csv
import json
import os
import re
import ssl
import sys
import urllib.request
from datetime import date
from pathlib import Path
BASE = Path(__file__).resolve().parent.parent / "원가계산"
SECRETS = Path(__file__).resolve().parent.parent.parent / ".secrets.local.md"
UA = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
# ── 반기 갱신 대상 URL (2026 상반기/2026년판 기준) ──
DOC_SOURCES = [
# (폴더, 파일명, URL, referer)
("노임단가_건설업_대한건설협회", "2026상반기_건설업_임금실태조사_대한건설협회.pdf",
"https://www.cak.or.kr/download.do?uuid=a0e92670-b6b7-4764-a15f-40c34febeaa0.pdf",
"https://www.cak.or.kr/lay1/S1T16C41/sublink.do"),
("노임단가_제조업_중소기업중앙회", "2026상반기_중소제조업_직종별_임금조사_중소기업중앙회.pdf",
None, # kbiz는 view 페이지에서 download.do 링크 추출 필요 — VIEW_URL 사용
"https://www.kbiz.or.kr/ko/contents/bbs/view.do?mnSeq=325&seq=163372"),
("건설공사_표준품셈", "2026년_건설공사_표준품셈.pdf",
"https://www.kseis.co.kr/bbs/data/dataFileDown.do?bbs_seq=64699193400142&file_no=1",
"https://www.kseis.co.kr/bbs/data/dataDetail.do?bbs_seq=64699193400142&pgno=1"),
("건설공사_표준품셈", "2026년_건설공사_표준품셈_개정사항.pdf",
"https://www.kseis.co.kr/bbs/data/dataFileDown.do?bbs_seq=64699193400142&file_no=2",
"https://www.kseis.co.kr/bbs/data/dataDetail.do?bbs_seq=64699193400142&pgno=1"),
]
G2B_OPS = [
("getPriceInfoListFcltyCmmnMtrilEngrk", "토목"),
("getPriceInfoListFcltyCmmnMtrilBildng", "건축"),
("getPriceInfoListFcltyCmmnMtrilMchnEqp", "기계설비"),
("getPriceInfoListFcltyCmmnMtrilElctyIrmc", "전기정보통신"),
]
_ctx = ssl.create_default_context()
_ctx.check_hostname = False
_ctx.verify_mode = ssl.CERT_NONE # 일부 기관 인증서 체인 불량 — 무결성은 매직바이트로 검사
def fetch(url, referer=None, timeout=120):
h = dict(UA)
if referer:
h["Referer"] = referer
req = urllib.request.Request(url, headers=h)
with urllib.request.urlopen(req, timeout=timeout, context=_ctx) as r:
return r.read()
def read_key(pattern, name):
m = re.search(pattern, SECRETS.read_text(encoding="utf-8"))
if not m:
sys.exit(f"{name} 키를 .secrets.local.md에서 찾지 못함")
return m.group(1)
def read_g2b_key():
return read_key(r"인증키\(Encoding형[^`]*`([^`]+)`", "공공데이터포털")
def collect_docs():
for folder, fname, url, referer in DOC_SOURCES:
dest = BASE / folder / fname
if url is None: # kbiz: view 페이지에서 download 링크 추출
html = fetch(referer).decode("utf-8", "replace")
links = re.findall(r'(/[^"\']*download\.do\?[^"\']+)', html)
if not links:
print(f"SKIP {fname}: 다운로드 링크 못 찾음")
continue
url = "https://www.kbiz.or.kr" + links[0].replace("&", "&")
data = fetch(url, referer)
if not data[:4] == b"%PDF" and not data[:4] == b"PK\x03\x04" and not data[:8].startswith(b"\xd0\xcf\x11\xe0"):
print(f"FAIL {fname}: PDF/HWP 아님 ({data[:8].hex()}) — URL 갱신 필요")
continue
dest.write_bytes(data)
print(f"saved {folder}/{fname} ({len(data):,} B)")
def collect_values():
"""값형 3종 원본 스냅샷: 자재단가(CSV+JSON)·환율(CSV)·유가(JSON)."""
today = date.today().isoformat()
collect_snapshot(BASE / "자재단가")
# 환율: 연초~오늘 일별, 5통화
ecos = read_key(r"ECOS[^`]*\n- 인증키: `([^`]+)`", "ECOS")
year = today[:4]
rows = []
for code, name in [("0000001", "원/미국달러"), ("0000002", "원/일본엔100"),
("0000003", "원/유로"), ("0000012", "원/영국파운드"), ("0000053", "원/위안")]:
url = (f"https://ecos.bok.or.kr/api/StatisticSearch/{ecos}/json/kr/1/400/"
f"731Y001/D/{year}0101/{today.replace('-', '')}/{code}")
j = json.loads(fetch(url, timeout=60).decode("utf-8", "replace"))
for r in j.get("StatisticSearch", {}).get("row", []):
rows.append({"통화": name, "항목코드": code, "일자": r["TIME"], "환율": r["DATA_VALUE"]})
out = BASE / "환율_한국은행ECOS" / f"환율_일별_{year}0101_{today}.csv"
out.parent.mkdir(exist_ok=True)
with open(out, "w", newline="", encoding="utf-8-sig") as f:
w = csv.DictWriter(f, fieldnames=["통화", "항목코드", "일자", "환율"])
w.writeheader()
w.writerows(rows)
print(f"saved {out.name} ({len(rows):,} rows)")
# 유가: 오늘 전국 평균 + 유종별 최근 7일
opinet = read_key(r"오피넷[^`]*`([^`]+)`", "오피넷")
data = {"수집일": today}
j = json.loads(fetch(f"https://www.opinet.co.kr/api/avgAllPrice.do?out=json&code={opinet}", timeout=60))
data["전국평균"] = j.get("RESULT", {}).get("OIL", [])
data["최근7일"] = {}
for prod in ["B027", "D047"]: # 휘발유, 자동차용경유
j = json.loads(fetch(f"https://www.opinet.co.kr/api/avgRecentPrice.do?out=json&code={opinet}&prodcd={prod}", timeout=60))
data["최근7일"][prod] = j.get("RESULT", {}).get("OIL", [])
out = BASE / "유가_오피넷" / f"유가_전국평균_{today}.json"
out.parent.mkdir(exist_ok=True)
out.write_text(json.dumps(data, ensure_ascii=False, indent=1), encoding="utf-8")
print(f"saved {out.name}")
def collect_snapshot(outdir=None):
key = read_g2b_key()
today = date.today().isoformat()
rows, fieldnames = [], []
for op, label in G2B_OPS:
page, got, total = 1, 0, 1
while got < total:
url = (f"http://apis.data.go.kr/1230000/ao/PriceInfoService/{op}"
f"?serviceKey={key}&pageNo={page}&numOfRows=999&type=json")
j = json.loads(fetch(url, timeout=60).decode("utf-8", "replace"))
body = j.get("response", {}).get("body", {})
total = int(body.get("totalCount") or 0)
items = body.get("items", [])
if isinstance(items, dict):
items = items.get("item", [])
if not items:
break
for it in items:
it["분야"] = label
rows.append(it)
for k in it:
if k not in fieldnames:
fieldnames.append(k)
got += len(items)
page += 1
print(f"{label}: {got}/{total}")
outp = Path(outdir or BASE / "자재단가")
outp.mkdir(exist_ok=True)
out = outp / f"나라장터_시설공통자재_{today}.csv"
with open(out, "w", newline="", encoding="utf-8-sig") as f:
w = csv.DictWriter(f, fieldnames=fieldnames)
w.writeheader()
w.writerows(rows)
(outp / f"나라장터_시설공통자재_{today}.json").write_text(
json.dumps(rows, ensure_ascii=False), encoding="utf-8")
print(f"saved {out.name} ({len(rows):,} rows, +json)")
if __name__ == "__main__":
mode = sys.argv[1] if len(sys.argv) > 1 else "docs"
if mode == "docs":
collect_docs()
elif mode == "values":
collect_values()
elif mode == "snapshot":
collect_snapshot(sys.argv[2] if len(sys.argv) > 2 else None)
else:
sys.exit("mode: docs | values | snapshot [출력폴더]")