€216,000. 이 기준선 미만의 서비스를 구매하는 유럽 시의회는 EU의 중앙 등록부인 TED에 입찰 공고를 게시할 의무가 없습니다.
이는 소규모 입찰자가 현실적으로 수주할 수 있는 계약들이 다른 곳에 존재함을 의미합니다. 바로 해당 구매를 진행하는 시의회, 지역, 공공사업자 또는 부처의 자체 포털입니다.
과제
유럽연합 집행위원회 집계에 따르면, EU 내 25만 개 이상의 공공 기관이 공사, 공급 및 서비스에 연간 약 2조 6천억 유로를 지출합니다. EU 지침은 기준 금액을 초과하는 계약에만 적용되며, 2026년 1월 1일부터 중앙 정부 공급 및 서비스는 €140,000, 지방 및 지역 당국은 €216,000, 공사는 €5,404,000로 설정되어 있습니다. 이 기준선 미만에서는 국가별 규칙이 적용되며 TED 게시 여부는 자율입니다.
입찰 모니터링이 스크래핑 작업이 되는 이유가 바로 여기에 있습니다. 입찰 컨설팅 기업이나 건설사를 위해 시스템을 구축한다면, 기준 금액 초과 영역은 쉬운 부분에 불과합니다. 나머지는 기계가 읽을 수 있도록 설계되지 않은 수많은 포털의 롱테일입니다.
북미 지역의 공공 부문 입찰 데이터를 판매하는 BidPrime은 2026년 5월 미국 버전의 이러한 상황을 "수천 개의 워크플로"라고 표현했습니다. 자동화된 접근을 제한하는 포털, 계정이나 세션이 필요한 조달 시스템, 공고와 별도로 저장되는 첨부 파일, 예고 없이 변경되는 URL 등이 이에 해당합니다. 이 목록의 어떤 항목도 미국에만 국한되지 않습니다.
하지만 입찰 모니터링에서 치명적인 실패는 요청을 거부하는 포털이 아닙니다. 그런 오류는 로그에 기록됩니다. 진짜 문제는 아무런 데이터도 없이 정상(green)으로 반환되는 실행 결과입니다.
시의회 포털은 만료된 세션에 대해 HTTP 200과 함께 안내 페이지를 반환할 수 있습니다. 결과 테이블이 있어야 할 자리에 유지보수 안내문이 렌더링될 수도 있습니다. 어제 제출한 검색 쿼리가 1페이지만 다시 반환할 수도 있는데, 이는 2페이지가 해당 요청을 보낸 쿠키에만 유효했기 때문입니다. 파서는 행을 0개 발견하고 "오늘 새로운 입찰 없음"으로 기록합니다. 그리고 놓친 가격 정보와 달리, 놓친 입찰 공고는 되돌릴 수 없습니다. 누락을 발견했을 때는 이미 제출 기한이 지나 공고가 입찰자에게 무의미해진 뒤입니다.
접근 방식: 모든 fetch가 무엇을 확인했는지 증명하도록 만들기
스크래핑할 필요가 없는 데이터부터 시작하세요. TED의 API는 키 없이도 게시된 공고에 대한 익명 접근을 제공하므로, 기준액 이상의 공고는 해당 API에서 가져오면 됩니다. 영국의 경우 GOV.UK에 따르면 2025년 2월 24일부터 모든 발주 기관이 Find a Tender를 중앙 플랫폼으로 사용하여 조달 정보를 게시하고 있습니다. 특정 포털용 파서를 작성하기 전에 중앙 등록 시스템이 이미 다루고 있는 범위를 먼저 확인하세요.
그 외의 경우에는 다음 세 가지 규칙이 핵심 역할을 합니다.
1. 비어 있는 경우를 포함해 실제 유효한 응답을 정의하세요. 정상적인 결과 페이지에는 고유한 컬럼 헤더(해당 포털의 표기 방식에 따른 "Closing date")가 포함됩니다. 결과가 없는 정상적인 날에는 포털 고유의 빈 상태 안내 문구가 표시됩니다. 그 외의 결과는 데이터가 0건인 것이 아니라 누락(gap)입니다. 두 문자열을 request의 validate.data.accept 목록에 모두 지정하면, 세션 만료 페이지가 발생했을 때 데이터가 없는 날로 오인되어 통과되지 않고 request 실패로 처리됩니다. FourA 엔진은 자체적으로 감지한 검증 페이지를 성공으로 간주하지 않도록 기본 차단하지만, 모든 지자체 포털의 타임아웃 문구까지 알 수는 없습니다(이 부분은 사용자가 직접 지정해야 합니다). 일치 여부는 대소문자를 구분하는 단순 부분 문자열로 확인하므로 직접 타이핑하기보다 페이지의 텍스트를 그대로 복사하세요. 이 작동 방식은 validate 규칙으로 성공 여부를 판별하기 시작했을 때 설명한 바 있으며, 시계열 관점에서의 동일한 문제는 차단 페이지가 정상 데이터로 보일 때에서 다루었습니다.
2. 포털에 표시되는 건수를 확인하세요. 대부분의 검색 결과 목록에는 일치하는 공고 수("1-20 of 143")가 표시됩니다. 해당 숫자를 파싱하여 저장된 행 수와 비교하세요. 두 값이 다르다면 HTTP status code와 무관하게 수집 작업이 완료되지 않은 것입니다.
3. 경로를 한 번 찾은 후 재사용하세요. 페이지네이션 및 세션 기반 포털은 후속 페이지 요청이 동일한 IP와 동일한 브라우저에서 들어올 것으로 기대합니다. FourA와 같은 플랫폼에서는 Auto 모드가 첫 번째 페이지에서 동작하는 경로를 찾은 뒤, 성공한 시도의 세션 정보(proxy ID, User-Agent, cookie)를 반환합니다. 이후의 모든 페이지는 프로덕션 경로인 해당 출구의 Single 모드를 통해 처리됩니다. Auto 모드는 경로를 찾는 역할만 수행합니다.
import math, re, requests
API = "https://api.foura.ai/api"
H = {"X-API-Key": "pk_live_..."}
LIST = "https://tenders.example-council.gov/open?page={}"
PER_PAGE = 20
# A real answer is the results table or the portal's own empty-state line.
# An expired session or a maintenance notice served at 200 is neither.
CHECK = {"data": {"accept": ["Closing date", "No open tenders"]}}
def fetch(endpoint, body):
r = requests.post(f"{API}/{endpoint}", headers=H, json=body).json()
if r.get("error"):
raise RuntimeError(r["error"]) # a gap to refill, not a quiet day
return r
# Auto finds a route that works and hands back its session...
first = fetch("auto", {"url": LIST.format(1), "validate": CHECK})
proxy, ua = first["session"]["proxy"], first["session"]["userAgent"]
pages = [first["data"]]
# ...and Single replays it on the same exit for every other page.
m = re.search(r"\d+-\d+ of (\d+)", pages[0])
total = int(m.group(1)) if m else 0
for n in range(2, math.ceil(total / PER_PAGE) + 1):
pages.append(fetch("single", {
"method": "GET", "url": LIST.format(n), "proxy": proxy,
"headers": [["User-Agent", ua]], "validate": CHECK,
})["data"])
포털이 검색 상태를 서버에 유지하는 경우 동일한 headers 목록에 세션 cookie를 전송하십시오. Single은 리디렉션 홉마다 모든 Set-Cookie 헤더를 반환합니다. 입찰 문서는 보통 PDF 형식의 다운로드 링크 뒤에 있으며, Single의 리디렉션 제어 및 원시 버퍼 모드를 사용하면 깨진 텍스트 대신 바이트 단위로 문서를 가져옵니다. 한 가지 습관을 더 들이자면 각 포털을 자체 일정에 맞춰 폴링하는 것입니다. 이 포털들은 소규모 지방 의회가 운영하는 작은 서버이므로, 오후 내내 20개의 요청을 분산하는 것보다 한 번에 20개의 요청을 한꺼번에 보내는 것이 훨씬 더 위험해 보입니다.
결과
- 입찰 공고 캡처 건수 25% 증가. 스크래핑 서비스 기업인 GroupBWT는 수동 벤치마크 대비 50개 이상의 조달 포털을 모니터링하는 고객에 대해 이를 보고했습니다. "더 많은 입찰 공고가 존재해서가 아니라 누락을 방지했기 때문입니다."
- 임계값 초과 항목에 대한 스크래퍼 불필요. TED의 공고는 키가 필요 없는 API에서 제공되므로 파서 작업 시간 전체를 API가 없는 포털에 온전히 투입할 수 있습니다.
- 공고가 열려 있는 동안 누락 감지. 수락 규칙을 통과하지 못한 페이지는 발생 당일 오류를 발생시키며, 작업은 빈 데이터를 저장하는 대신 재시도합니다.
- 페이지별이 아닌 포털별 1회 경로 탐색. Auto의 검색은 한 번만 실행되며, 이후의 모든 후속 페이지는 탐색된 출구에 대한 Single 호출로 처리됩니다.
첫 번째 항목만 측정된 결과이며, 나머지 세 항목은 이 구성이 변경하는 사항을 설명합니다.
핵심 요약
입찰 모니터링에서 저장할 수 있는 가장 비용이 큰 행은 "오늘 새로운 항목 없음"입니다. 나중에 수정할 수 없는 유일한 행이기 때문입니다. 가격은 내일 다시 확인할 수 있습니다. 하지만 스크래퍼가 만료된 세션 페이지를 읽는 동안 마감된 입찰 공고는 다시 돌아오지 않습니다.
따라서 작업이 0이라는 결과를 반환하려면 그에 맞는 엄격한 검증을 거쳐야 합니다. 테이블, 포털 자체의 빈 상태 문구, 또는 일치하는 카운트를 표시하지 못하는 목록 페이지는 아직 아무런 정보도 알려주지 않은 것이며, 이를 유효한 응답으로 처리하면 입찰자는 결국 낙찰 공고를 보고 나서야 계약의 존재를 알게 됩니다.