의료 데이터 파싱은 프록시 선택에 특별한 접근이 필요한 작업입니다. 의료 포털, 임상 연구 데이터베이스 및 제약 리소스는 자동화된 데이터 수집에 대한 고급 보호 시스템을 사용합니다. 이 기사에서는 안전한 의료 정보 파싱을 위한 프록시 설정 방법, 차단을 피하고 필요한 데이터를 효율적으로 수집하는 방법을 살펴보겠습니다.
의료 사이트가 파싱을 차단하는 이유
의료 포털과 데이터베이스는 여러 가지 이유로 자동화된 정보 수집에 매우 민감합니다. 첫째, 많은 사이트가 상업적으로 운영되며 유료 구독을 통해 데이터에 대한 접근을 판매합니다. 자동 파싱은 이용 약관 및 라이선스 계약을 위반할 수 있습니다.
둘째, 의료 데이터는 종종 법률에 의해 보호되는 기밀 정보를 포함하고 있습니다(HIPAA 미국, GDPR 유럽). 리소스 소유자는 이러한 데이터에 대한 접근을 통제하고 무단 배포를 방지해야 합니다. 따라서 그들은 고급 보호 시스템을 사용합니다:
- 속도 제한 — 특정 시간 내에 하나의 IP 주소에서 보낼 수 있는 요청 수를 제한합니다(보통 분당 10-50 요청)
- 지문 인식 — 브라우저 특성, HTTP 헤더, 리소스 로딩 순서 분석
- CAPTCHA — 의심스러운 활동 시 작동하는 reCAPTCHA v3와 같은 시스템
- IP 차단 — 데이터 센터의 IP 주소를 일시적 또는 영구적으로 차단
- Cloudflare 및 유사 서비스 — CDN 수준에서 봇으로부터 보호
세 번째 이유는 서버에 대한 부하입니다. 의료 데이터베이스는 종종 수백만 개의 레코드를 포함하고 있으며, 대량 파싱은 인프라에 상당한 부하를 초래할 수 있습니다. 따라서 관리자는 자동화된 데이터 수집을 방지하기 위해 봇에 특유한 행동 패턴을 추적합니다: 요청 간 동일한 간격, 페이지 순차적 탐색, JavaScript 및 쿠키의 부재.
중요: 의료 데이터 파싱을 시작하기 전에 사이트의 이용 약관과 적용 가능한 법률을 반드시 검토하십시오. 일부 데이터는 저작권으로 보호되거나 환자의 개인 정보를 포함할 수 있습니다. 귀하의 활동이 합법적이며 제3자의 권리를 침해하지 않도록 확인하십시오.
의료 데이터에 적합한 프록시 유형 선택하기
프록시 유형 선택은 의료 데이터 파싱의 성공에 매우 중요합니다. 다양한 출처는 서로 다른 접근 방식을 요구합니다. 주요 프록시 유형과 그 적용 가능성을 살펴보겠습니다:
| 프록시 유형 | 장점 | 단점 | 언제 사용해야 하는가 |
|---|---|---|---|
| 데이터 센터 프록시 | 높은 속도 (100+ Mbps), 저렴한 비용, 안정적인 연결 | 탐지하기 쉬우며, 종종 보호된 사이트에서 차단됨 | 엄격한 보호가 없는 공개 데이터베이스 (PubMed, WHO) |
| 주거지 프록시 | 실제 가정 사용자 IP, 차단 위험 낮음, Cloudflare 통과 가능 | 비용이 더 높고, 속도가 변동적이며, 불안정할 수 있음 | 보호된 상업 데이터베이스 (Elsevier, Springer), Cloudflare가 있는 사이트 |
| 모바일 프록시 | 최대 신뢰 (모바일 운영자 IP), 거의 차단되지 않음 | 가장 비쌈, 제한된 지리적 범위, 느릴 수 있음 | 특히 보호된 리소스, 주거지 프록시가 효과가 없을 때 |
| ISP 프록시 | 데이터 센터 속도 + 주거지 신뢰, 정적 IP | 중간 비용, 제한된 가용성 | 하나의 IP에서 장기 파싱이 필요할 때 안정성이 요구됨 |
대부분의 의료 데이터 파싱 작업에는 주거지 프록시를 사용하는 것이 좋습니다. 이들은 비용과 효율성 간의 최적의 균형을 제공합니다. 데이터 센터 프록시는 보호가 없는 공개 출처에만 적합합니다. 모바일 프록시는 다른 유형이 효과가 없을 때 극단적인 경우에만 사용하는 것이 좋습니다.
특정 출처에 대한 선택 권장 사항
- PubMed, PubMed Central — 데이터 센터 프록시로 충분하지만 초당 3 요청으로 제한
- ClinicalTrials.gov — 데이터 센터 프록시, 공식 API 있음
- Elsevier, Springer, Wiley — 주거지 프록시 필수, 고급 지문 인식 사용
- DrugBank, RxList — 주거지 프록시, 파싱에 대한 적극적인 보호
- FDA, EMA 데이터베이스 — 데이터 센터 프록시 적합하지만 느린 파싱 속도
주요 의료 데이터 출처 및 그 보호
의료 데이터는 여러 출처에 분산되어 있으며, 각 출처는 고유한 특성과 보호 수준을 가지고 있습니다. 이러한 특성을 이해하면 파싱 전략을 올바르게 설정하는 데 도움이 됩니다.
공개 국가 데이터베이스
PubMed/PubMed Central — 가장 큰 의료 출판 데이터베이스로, 3500만 개 이상의 레코드를 포함합니다. 미국 국립 의학 도서관(NLM)은 데이터에 접근하기 위한 공식 API E-utilities를 제공합니다. 웹 인터페이스의 직접 파싱은 가능하지만, 하나의 IP에서 초당 3 요청으로 제한됩니다. 이 한도를 초과하면 24시간 동안 일시적으로 차단됩니다.
ClinicalTrials.gov — 임상 연구 데이터베이스로, 220개국에서 400,000개 이상의 연구 정보를 포함합니다. 프로그래밍 접근을 위한 API도 제공합니다. 웹 인터페이스는 속도 제한으로 보호되며, 하나의 IP에서 5분에 최대 100 요청이 가능합니다. 기본적인 봇 보호가 사용되지만 Cloudflare는 없습니다.
FDA 의약품 데이터베이스 — FDA에서 승인한 의약품 데이터베이스입니다. 웹 인터페이스 및 API openFDA를 통한 공개 접근이 가능합니다. 제한 사항: 익명 사용자에게는 분당 240 요청, API 키를 가진 사용자에게는 분당 1000 요청이 허용됩니다. 차단은 드물지만, 공격적인 파싱 시 발생할 수 있습니다.
상업적 과학 출판사
Elsevier (ScienceDirect) — 가장 큰 과학 문헌 출판사 중 하나입니다. Cloudflare, 브라우저 지문 인식, 사용자 행동 분석과 같은 다단계 보호를 사용합니다. 자동 다운로드 패턴을 감지합니다: 기사에 대한 순차적 접근, JavaScript의 부재, 비정상적인 User-Agent. 파싱이 감지되면 계정 수준에서 IP를 차단하고 전체 기관을 차단할 수 있습니다. 회전 및 브라우저 완전 에뮬레이션이 포함된 주거지 프록시 사용이 필수입니다.
Springer Nature — 유사한 보호를 사용하며, 페이지 스크롤 속도 및 마우스 움직임을 추가로 추적합니다. 봇 감지를 위해 머신 러닝을 사용합니다. 하나의 IP에서 시간당 10-15개 기사 이하로 파싱하는 것이 권장되며, 요청 간 랜덤 지연을 두어야 합니다.
Wiley Online Library — 덜 공격적인 보호를 제공하지만 여전히 프록시 사용이 필요합니다. 하나의 IP에서 시간당 약 50 요청을 허용하며 차단되지 않습니다. 활동 추적을 위해 세션 쿠키를 사용합니다.
제약 데이터베이스
DrugBank — 포괄적인 의약품 데이터베이스입니다. 무료 버전은 웹 인터페이스로 제한되며, 상업 버전은 API 및 데이터 덤프를 제공합니다. 웹 버전은 Cloudflare 및 속도 제한으로 보호되며, 분당 최대 20 요청이 가능합니다. 쿠키 및 JavaScript의 부재로 자동화를 감지합니다.
RxList, Drugs.com — 소비자를 위한 인기 있는 의약품 안내서입니다. Cloudflare를 사용하며 파싱에 대해 적극적으로 대응합니다. 데이터 센터 IP는 거의 즉시 차단됩니다. 주거지 프록시와 느린 파싱 속도(분당 5-10 페이지)가 필요합니다.
장기 파싱을 위한 IP 회전 설정
올바른 IP 주소 회전은 의료 데이터 파싱의 성공에 중요한 요소입니다. 두 가지 주요 접근 방식이 있습니다: 요청 수준 회전 및 시간 기반 회전.
요청 수준 회전
이 접근 방식에서는 각 요청이 새로운 IP 주소를 통해 전송됩니다. 이는 차단 위험을 최대한 줄이지만, 쿠키를 통해 세션을 추적하는 사이트에서 문제를 일으킬 수 있습니다. 세션 상태를 유지할 필요가 없는 목록 및 카탈로그 파싱에 적합합니다.
대부분의 주거지 프록시 제공자는 특별한 엔드포인트를 통해 자동 회전을 제공합니다. 예를 들어, 회전 프록시 엔드포인트를 사용할 경우 각 새로운 TCP 연결은 새로운 IP를 받습니다. 이는 Python의 requests와 같은 라이브러리와 함께 자동으로 작동합니다. 기본적으로 각 요청에 대해 새로운 연결이 생성됩니다.
시간 기반 회전 (스티키 세션)
스티키 세션은 특정 시간(보통 5-30분) 동안 하나의 IP 주소를 사용한 후 자동으로 변경됩니다. 이는 인증이 필요하거나 쿠키를 통해 세션 상태를 추적하는 사이트에 유용합니다. 하나의 IP로 여러 페이지를 파싱하여 실제 사용자 행동을 모방한 후 IP가 자동으로 변경됩니다.
의료 사이트에는 10-15분의 스티키 세션을 사용하는 것이 권장됩니다. 이 시간 동안 10-20 페이지를 파싱할 수 있으며(지연에 따라 다름), 이후 IP가 변경되고 "새로운 세션"이 시작됩니다. 이는 자연스럽게 보이며 감지 위험을 줄입니다.
IP 주소 풀의 크기
장기 파싱을 위해서는 사용 가능한 IP 주소의 풀 크기가 중요합니다. 일주일 동안 동일한 100개의 IP 세트를 사용하는 경우, 사이트는 패턴을 감지하고 모든 주소를 차단할 수 있습니다. 주거지 프록시는 일반적으로 수백만 개의 IP에 대한 접근을 제공하므로 동일한 주소의 재사용 가능성을 거의 배제합니다.
데이터 센터 프록시를 사용할 경우, 평균적인 양(월 10,000-50,000 페이지)의 파싱을 위해 최소 500-1000 IP의 풀을 갖는 것이 좋습니다. 대규모 파싱(수십만 페이지)의 경우, 주거지 프록시와 그들의 방대한 IP 풀을 사용하는 것이 좋습니다.
다양한 출처에 대한 회전 팁:
- PubMed — 회전은 필요하지 않으며, 속도 제한을 준수하는 1 IP로 충분함
- 상업 출판사 — 10-15분의 스티키 세션, 15-20 페이지마다 새로운 IP
- 제약 데이터베이스 — 각 요청마다 회전 또는 5분의 스티키 세션
- Cloudflare가 있는 사이트 — 스티키 세션 필수, 요청 수준 회전은 작동하지 않음
프록시를 이용한 파싱을 위한 Python 코드 예제
인기 있는 Python 라이브러리를 사용하여 의료 데이터 파싱을 위한 프록시 설정의 실제 예제를 살펴보겠습니다. 기본 예제부터 시작하여 점차 복잡하게 만들어 보겠습니다.
requests 라이브러리를 이용한 기본 설정
import requests
from time import sleep
import random
# 프록시 설정 (귀하의 데이터로 교체)
PROXY_HOST = "proxy.example.com"
PROXY_PORT = "8080"
PROXY_USER = "username"
PROXY_PASS = "password"
proxies = {
'http': f'http://{PROXY_USER}:{PROXY_PASS}@{PROXY_HOST}:{PROXY_PORT}',
'https': f'http://{PROXY_USER}:{PROXY_PASS}@{PROXY_HOST}:{PROXY_PORT}'
}
# 실제 브라우저를 모방하기 위한 헤더
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'gzip, deflate, br',
'DNT': '1',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1'
}
# PubMed에 대한 요청 예제
url = "https://pubmed.ncbi.nlm.nih.gov/?term=diabetes"
try:
response = requests.get(url, proxies=proxies, headers=headers, timeout=30)
print(f"상태 코드: {response.status_code}")
print(f"콘텐츠 길이: {len(response.content)}")
# 요청 간 지연 추가 (PubMed에 필수)
sleep(random.uniform(1.0, 3.0))
except requests.exceptions.RequestException as e:
print(f"오류: {e}")
회전 및 재시도 로직을 포함한 고급 설정
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from time import sleep
import random
class ProxyRotator:
def __init__(self, proxy_list):
"""
proxy_list: 프록시가 포함된 사전의 목록
[{'http': 'http://user:pass@host:port', 'https': '...'}, ...]
"""
self.proxy_list = proxy_list
self.current_index = 0
def get_next_proxy(self):
"""목록에서 다음 프록시를 가져옵니다."""
proxy = self.proxy_list[self.current_index]
self.current_index = (self.current_index + 1) % len(self.proxy_list)
return proxy
def create_session_with_retries():
"""오류 발생 시 자동 재시도가 포함된 세션을 생성합니다."""
session = requests.Session()
# 자동 재시도 설정
retry_strategy = Retry(
total=3, # 최대 3회 시도
backoff_factor=1, # 시도 간 지연: 1, 2, 4초
status_forcelist=[429, 500, 502, 503, 504], # 재시도할 상태 코드
allowed_methods=["GET", "POST"]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("http://", adapter)
session.mount("https://", adapter)
return session
def scrape_with_rotation(urls, proxy_rotator):
"""프록시 회전을 포함한 URL 목록을 파싱합니다."""
session = create_session_with_retries()
results = []
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.9',
}
for url in urls:
# 각 요청에 대해 새로운 프록시를 가져옵니다.
proxy = proxy_rotator.get_next_proxy()
try:
response = session.get(
url,
proxies=proxy,
headers=headers,
timeout=30
)
if response.status_code == 200:
results.append({
'url': url,
'status': 'success',
'content_length': len(response.content)
})
print(f"✓ 성공: {url}")
else:
results.append({
'url': url,
'status': 'failed',
'error': f"상태 코드: {response.status_code}"
})
print(f"✗ 실패: {url} (상태: {response.status_code})")
except requests.exceptions.RequestException as e:
results.append({
'url': url,
'status': 'error',
'error': str(e)
})
print(f"✗ 오류: {url} ({e})")
# 요청 간 랜덤 지연 추가 (중요!)
sleep(random.uniform(2.0, 5.0))
return results
# 사용 예제
proxy_list = [
{
'http': 'http://user1:pass1@proxy1.example.com:8080',
'https': 'http://user1:pass1@proxy1.example.com:8080'
},
{
'http': 'http://user2:pass2@proxy2.example.com:8080',
'https': 'http://user2:pass2@proxy2.example.com:8080'
}
]
rotator = ProxyRotator(proxy_list)
urls_to_scrape = [
"https://pubmed.ncbi.nlm.nih.gov/?term=diabetes",
"https://pubmed.ncbi.nlm.nih.gov/?term=cancer",
"https://pubmed.ncbi.nlm.nih.gov/?term=covid"
]
results = scrape_with_rotation(urls_to_scrape, rotator)
JavaScript가 있는 사이트를 위한 Selenium 사용
많은 현대 의료 사이트는 콘텐츠 로딩을 위해 JavaScript를 사용합니다. 이러한 경우 headless 브라우저가 필요합니다:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time
def create_proxy_driver(proxy_host, proxy_port, proxy_user, proxy_pass):
"""프록시와 함께 Chrome WebDriver를 생성합니다."""
chrome_options = Options()
# Headless 모드 (GUI 없음)
chrome_options.add_argument('--headless')
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--disable-dev-shm-usage')
# 프록시 설정
chrome_options.add_argument(f'--proxy-server=http://{proxy_host}:{proxy_port}')
# 자동화 비활성화 (감지 우회를 위해 중요)
chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
chrome_options.add_experimental_option('useAutomationExtension', False)
# User-Agent
chrome_options.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36')
driver = webdriver.Chrome(options=chrome_options)
# 인증이 필요한 프록시의 경우 확장 프로그램을 사용하거나 capabilities를 통해 설정해야 합니다 (더 복잡한 방법)
return driver
def scrape_with_selenium(url, driver):
"""JavaScript 로딩을 기다리며 페이지를 파싱합니다."""
driver.get(url)
# 요소 로딩 대기 (예: 검색 결과)
try:
wait = WebDriverWait(driver, 10)
results = wait.until(
EC.presence_of_element_located((By.CLASS_NAME, "results-article"))
)
# 데이터 추출
articles = driver.find_elements(By.CLASS_NAME, "results-article")
data = []
for article in articles:
try:
title = article.find_element(By.CLASS_NAME, "docsum-title").text
authors = article.find_element(By.CLASS_NAME, "docsum-authors").text
data.append({
'title': title,
'authors': authors
})
except:
continue
return data
except Exception as e:
print(f"요소 대기 중 오류 발생: {e}")
return []
# 사용 예제
proxy_host = "proxy.example.com"
proxy_port = "8080"
proxy_user = "username"
proxy_pass = "password"
driver = create_proxy_driver(proxy_host, proxy_port, proxy_user, proxy_pass)
try:
url = "https://pubmed.ncbi.nlm.nih.gov/?term=diabetes"
results = scrape_with_selenium(url, driver)
for result in results:
print(f"제목: {result['title']}")
print(f"저자: {result['authors']}\n")
finally:
driver.quit()
요청 속도 제어 및 속도 제한 우회
속도 제한은 의료 사이트가 파싱을 방지하기 위한 주요 보호 수단 중 하나입니다. 요청 속도를 올바르게 설정하는 것은 차단 없이 장기 파싱을 위해 매우 중요합니다.
안전한 속도 정의
첫 번째 단계는 특정 사이트의 한계를 정의하는 것입니다. 이는 실험적으로 수행할 수 있으며, 속도를 점차 증가시켜 429(요청이 너무 많음) 오류 또는 차단이 발생할 때까지 진행합니다. 대부분의 의료 사이트에 대한 안전한 값은 다음과 같습니다:
- PubMed — 초당 최대 3 요청 (공식 권장 사항)
- ClinicalTrials.gov — 분당 20 요청이 안전하며, 5분에 최대 100 요청이 허용됨
- 상업 출판사 — 하나의 IP에서 시간당 10-15 요청
- 제약 데이터베이스 — 분당 5-10 요청
Python에서 속도 제한기 구현
import time
from collections import deque
class RateLimiter:
def __init__(self, max_calls, period):
"""
max_calls: 최대 호출 수
period: 초 단위의 시간 기간
예: RateLimiter(3, 1) = 초당 3 요청
"""
self.max_calls = max_calls
self.period = period
self.calls = deque()
def __call__(self, func):
"""함수 호출 속도를 제한하는 데코레이터"""
def wrapper(*args, **kwargs):
now = time.time()
# 기간을 초과한 오래된 호출 제거
while self.calls and self.calls[0] < now - self.period:
self.calls.popleft()
# 한도에 도달하면 대기
if len(self.calls) >= self.max_calls:
sleep_time = self.period - (now - self.calls[0])
if sleep_time > 0:
print(f"속도 제한에 도달했습니다. {sleep_time:.2f}s 대기 중입니다.")
time.sleep(sleep_time)
# 대기 후 초기화
self.calls.clear()
# 호출 시간을 기록합니다.
self.calls.append(time.time())
# 함수를 실행합니다.
return func(*args, **kwargs)
return wrapper
# 사용 예제
@RateLimiter(max_calls=3, period=1) # 초당 3 요청
def fetch_pubmed_page(url):
response = requests.get(url, headers=headers, proxies=proxies)
return response
# 이제 함수는 자동으로 속도 제한을 준수합니다.
for i in range(10):
result = fetch_pubmed_page(f"https://pubmed.ncbi.nlm.nih.gov/?term=test&page={i}")
print(f"페이지 {i} 가져오기 완료")
적응형 속도 제한
보다 발전된 접근 방식은 서버의 응답에 따라 속도를 적응적으로 변경하는 것입니다. 429 또는 503 오류가 발생하면 자동으로 속도를 줄입니다:
import time
import random
class AdaptiveRateLimiter:
def __init__(self, initial_delay=1.0, max_delay=60.0):
self.current_delay = initial_delay
self.initial_delay = initial_delay
self.max_delay = max_delay
self.success_count = 0
def wait(self):
"""다음 요청 전 대기합니다."""
# 자연스러움을 위해 랜덤성을 추가합니다.
actual_delay = self.current_delay * random.uniform(0.8, 1.2)
time.sleep(actual_delay)
def on_success(self):
"""성공적인 요청 시 호출됩니다."""
self.success_count += 1
# 10번의 성공적인 요청 후 약간 속도를 높입니다.
if self.success_count >= 10:
self.current_delay = max(
self.initial_delay,
self.current_delay * 0.9
)
self.success_count = 0
def on_rate_limit(self):
"""429 또는 유사한 오류 발생 시 호출됩니다."""
# 지연을 두 배로 늘리되 최대 한도를 초과하지 않도록 합니다.
self.current_delay = min(
self.current_delay * 2,
self.max_delay
)
self.success_count = 0
print(f"속도 제한에 도달했습니다! 지연을 {self.current_delay:.2f}s로 증가시킵니다.")
def on_error(self):
"""기타 오류 발생 시 호출됩니다."""
# 지연을 약간 늘립니다.
self.current_delay = min(
self.current_delay * 1.5,
self.max_delay
)
self.success_count = 0
# 사용 예제
limiter = AdaptiveRateLimiter(initial_delay=2.0, max_delay=30.0)
for url in urls_to_scrape:
limiter.wait()
try:
response = requests.get(url, proxies=proxies, headers=headers)
if response.status_code == 200:
limiter.on_success()
# 데이터 처리
elif response.status_code == 429:
limiter.on_rate_limit()
# 나중에 다시 시도
else:
limiter.on_error()
except requests.exceptions.RequestException:
limiter.on_error()
의료 사이트를 위한 올바른 헤더 및 User-Agent
의료 사이트는 HTTP 헤더를 분석하여 봇을 감지합니다. 잘못된 또는 누락된 헤더는 품질 좋은 프록시를 사용하더라도 차단의 일반적인 원인입니다.
필수 헤더
각 요청에 반드시 포함되어야 하는 최소한의 헤더 세트는 다음과 같습니다:
headers = {
# User-Agent — 반드시 최신 브라우저
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
# Accept — 브라우저가 수용하는 콘텐츠 유형
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
# Accept-Language — 사용자 언어
'Accept-Language': 'en-US,en;q=0.9',
# Accept-Encoding — 압축 지원
'Accept-Encoding': 'gzip, deflate, br',
# Connection — 연결 유지
'Connection': 'keep-alive',
# Upgrade-Insecure-Requests — HTTPS로 자동 전환
'Upgrade-Insecure-Requests': '1',
# DNT — Do Not Track (선택 사항, 하지만 현실감을 더함)
'DNT': '1',
# Sec-Fetch-* 헤더 (현대 브라우저에 중요)
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'none',
'Sec-Fetch-User': '?1',
# Cache-Control
'Cache-Control': 'max-age=0'
}
User-Agent 회전
동일한 User-Agent를 사용하는 것은 의심스러울 수 있습니다. 여러 최신 브라우저 간의 회전을 권장합니다:
import random
USER_AGENTS = [
# Windows의 Chrome
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
# Mac의 Chrome
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
# Windows의 Firefox
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
# Mac의 Firefox
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0',
# Mac의 Safari
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15',
# Windows의 Edge
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0'
]
def get_random_headers():
"""랜덤 User-Agent가 포함된 헤더를 가져옵니다."""
return {
'User-Agent': random.choice(USER_AGENTS),
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
'DNT': '1'
}
# 사용 예
for url in urls:
headers = get_random_headers()
response = requests.get(url, headers=headers, proxies=proxies)
폼을 위한 Referer 및 Origin
검색 폼이나 POST 요청을 보낼 때는 반드시 Referer 및 Origin 헤더를 추가해야 합니다:
# 검색 폼에 대한 POST 요청
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'gzip, deflate, br',
'Content-Type': 'application/x-www-form-urlencoded',
'Origin': 'https://example.com',
'Referer': 'https://example.com/search',
'Connection': 'keep-alive'
}
# 폼 데이터가 포함된 POST 요청
data = {
'query': 'diabetes',
'page': '1'
}
response = requests.post(
'https://example.com/search',
headers=headers,
data=data,
proxies=proxies
)
일반적인 문제와 해결 방법
의료 데이터 파싱 중에는 특정 문제가 발생할 수 있습니다. 가장 흔한 문제와 그 해결 방법을 살펴보겠습니다.
문제: Cloudflare가 모든 요청을 차단함
증상: "브라우저 확인 중"이라는 텍스트가 포함된 페이지 또는 Cloudflare 언급이 있는 403 Forbidden 오류를 받습니다.
해결 방법:
- 데이터 센터 대신 주거지 프록시를 사용하십시오 — Cloudflare는 기본적으로 데이터 센터 IP를 차단합니다.
- Selenium 또는 Puppeteer로 전환하십시오 — headless 브라우저가 Cloudflare 검사를 더 잘 통과합니다.
- Python용 cloudscraper 라이브러리를 사용하십시오 — 기본 Cloudflare 보호를 자동으로 우회합니다.
- 쿠키 및 JavaScript를 활성화하십시오 — Cloudflare는 이들의 존재를 확인합니다.
- TLS 지문 인식을 추가하십시오 — curl_cffi를 사용하여 TLS 수준에서 실제 브라우저를 모방합니다.
문제: 429 Too Many Requests 오류 발생
증상: 몇 번의 성공적인 요청 후 서버가 429 오류를 반환하기 시작합니다.
해결 방법:
- 요청 간 지연을 늘리십시오 — 3-5초부터 시작해 보십시오.
- IP 회전을 활성화하십시오 — 각 요청을 새로운 IP로 보내면 속도 제한이 해제됩니다.
- 429 응답의 Retry-After 헤더를 확인하십시오 — 대기해야 할 초를 나타냅니다.
- 재시도 시 지연을 지수적으로 증가시키십시오 — 1초, 2초, 4초, 8초 등.
문제: 프록시가 느리거나 자주 끊김
증상: 타임아웃 오류, 페이지 로딩이 매우 느림, 연결 끊김.
해결 방법:
- 요청의 타임아웃을 30-60초로 늘리십시오 — 주거지 프록시는 느릴 수 있습니다.
- 지리적으로 가까운 프록시를 사용하십시오 — 유럽 사이트를 파싱할 경우 유럽 IP를 사용하십시오.
- 프록시 제공자의 품질을 확인하십시오 — 저렴한 프록시는 종종 불안정합니다.
- 재시도 로직을 추가하십시오 — 연결 오류 시 요청을 자동으로 반복하십시오.
- 연결 풀링을 사용하십시오 — requests.Session()을 통해 TCP 연결을 재사용하십시오.
문제: 사이트가 인증 또는 구독을 요구함
증상: 기사 전체 텍스트에 대한 접근이 제한되며 로그인 필요.
해결 방법:
- 기관 접근을 사용하십시오 — 많은 대학 및 병원이 구독을 보유하고 있습니다.
- Open Access 버전이 있는지 확인하십시오 — 많은 기사가 무료로 제공됩니다.
- 파싱 대신 API를 사용하십시오 — 일부 출판사는 연구자를 위한 API를 제공합니다.
- 메타데이터(제목, 저자, 초록)만 파싱하십시오 — 이는 일반적으로 무료로 제공됩니다.
문제: JavaScript 콘텐츠가 로드되지 않음
증상: HTML에 필요한 데이터가 없고 로딩 스피너 또는 빈 컨테이너만 보입니다.
해결 방법:
- Selenium/Puppeteer로 전환하십시오 — 이들은 JavaScript를 실행합니다.
- API 엔드포인트를 찾으십시오 — 브라우저에서 DevTools를 열고 Network 탭에서 데이터가 포함된 XHR 요청을 찾으십시오.
- requests-html 사용 — JavaScript를 처리할 수 있는 라이브러리입니다.