DOI 또는 PubMed ID를 사용하여 논문 PDF를 다운로드합니다. 여러 소스를 순차적으로 시도하며, OA(Open Access) PDF를 우선 검색합니다.
사용법
import os, sys
SKILLS_HOME = os.environ.get('SKILLS_HOME', os.path.expanduser('~/projects/skills'))
sys.path.insert(0, SKILLS_HOME)
from paper_pdf_downloader import PaperPDFDownloader, download_pdf
# 방법 1: 편의 함수 사용
result = download_pdf(
identifier='$ARGUMENTS', # DOI, PMID, 또는 URL
download_dir='./papers',
unpaywall_email='your@email.com'
)
# 방법 2: 클래스 사용
downloader = PaperPDFDownloader(
download_dir='./papers',
unpaywall_email='your@email.com' # 또는 환경변수 UNPAYWALL_EMAIL
)
# DOI로 다운로드
result = downloader.download('10.1371/journal.pone.0200001')
# PMID로 다운로드
result = downloader.download_by_pmid('28800616')
# 결과 확인
if result.success:
print(f"다운로드 완료: {result.filepath}")
print(f"소스: {result.source}")
else:
print(f"실패: {result.error}")봇 감지 우회 기법
macOS Python 3.14 + LibreSSL 호환성 (v2026-02)
macOS Python 3.14 + LibreSSL 2.8.3 환경에서는 SSL 인증서 검증 오류가 발생합니다. 래퍼 스크립트(download-paper-pdf.py)에서 자동으로 처리됩니다:
# SSL 인증서 검증 비활성화
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
# requests 세션의 verify=False 기본값 설정
import requests as _requests
_orig_request = _requests.Session.request
def _patched_request(self, *args, **kwargs):
kwargs.setdefault('verify', False)
return _orig_request(self, *args, **kwargs)
_requests.Session.request = _patched_request
# cloudscraper → 일반 requests.Session 대체
# (cloudscraper의 자체 SSL 핸들링이 LibreSSL과 호환되지 않음)
import cloudscraper as _cloudscraper
def _fake_create_scraper(**kwargs):
session = _requests.Session()
session.headers.update({...})
return session
_cloudscraper.create_scraper = _fake_create_scraperUser-Agent 로테이션
요청마다 랜덤한 User-Agent를 사용합니다.
Referer 헤더 위장
학술 검색 엔진에서 온 것처럼 위장합니다.
자연스러운 요청 간격
min_request_delay: float = 0.5 # 최소 대기
max_request_delay: float = 2.0 # 최대 대기차단 감지
봇 차단 응답을 감지하고 적절히 처리합니다.
blocking_indicators = [
'cloudflare', 'access denied', 'forbidden', 'blocked',
'captcha', 'verification', 'security check', 'bot detection',
'just a moment', 'please wait', 'checking your browser',
'enable javascript', 'browser check'
]폴백 체인 (6단계)
PDF를 찾을 때까지 순차적으로 시도합니다.
1단계: Unpaywall API (Open Access)
GET https://api.unpaywall.org/v2/{doi}?email={email}- 오픈 액세스 논문의 PDF URL 반환
- 무료이며 안정적
2단계: CrossRef API
GET https://api.crossref.org/works/{doi}- 출판사 URL, 라이선스 정보 획득
- PDF 링크가 있으면 직접 다운로드
- 없으면 출판사 URL을 출판사 핸들러로 전달
3단계: DOI Resolver
GET https://doi.org/{doi}
→ 리다이렉트 → 출판사 페이지- DOI 리다이렉트 추적
- 최종 URL에서 PDF 링크 추출
4단계: Wiley 핸들러
Wiley Online Library 전용 처리:
원본: https://onlinelibrary.wiley.com/doi/10.xxxx/xxx
시도1: https://onlinelibrary.wiley.com/doi/epdf/10.xxxx/xxx
시도2: https://onlinelibrary.wiley.com/doi/pdf/10.xxxx/xxx5단계: Elsevier 핸들러
ScienceDirect 전용 처리:
1. PII (Publisher Item Identifier) 추출
2. https://www.sciencedirect.com/science/article/pii/{PII}/pdfft
3. XML/HTML 파싱으로 PDF 링크 추출6단계: PMC (PubMed Central)
PMID → PMC ID 변환 후:
GET https://www.ncbi.nlm.nih.gov/pmc/articles/{pmcid}/pdf/출판사별 처리
Wiley
class WileyHandler:
domains = ['wiley.com', 'onlinelibrary.wiley.com']
# URL 패턴 변환: /doi/10.xxx → /doi/epdf/10.xxx, /doi/pdf/10.xxxElsevier (ScienceDirect)
class ElsevierHandler:
domains = ['sciencedirect.com', 'elsevier.com']
# PII 추출 → /science/article/pii/{PII}/pdfft환경 변수
| 변수 | 설명 | 필수 |
|---|---|---|
UNPAYWALL_EMAIL | Unpaywall API 이메일 | Yes |
ENTREZ_EMAIL | PubMed Entrez API 이메일 | PMID 사용 시 |
의존성 설치
pip install --break-system-packages -r ~/projects/skills/paper_pdf_downloader/requirements.txt주요 패키지:
cloudscraper- Cloudflare 우회 (macOS에서는 requests.Session으로 자동 대체)beautifulsoup4- HTML 파싱requests- HTTP 클라이언트python-dotenv- 환경 변수
로깅
로그 파일 위치
| 모드 | 로그 파일 |
|---|---|
| Content (일반 논문) | logs/pdf-{jobId}.log |
| MetaPaper (메타분석) | logs/meta-pdf-{paperId}.log |
다운로드 실패 로그
{download_dir}/download_failures.jsonl{
"timestamp": "2025-02-03T10:00:00",
"identifier": "10.1234/example",
"identifier_type": "doi",
"doi": "10.1234/example",
"error_message": "PDF not found from any source",
"attempts": [
{"source": "unpaywall", "success": false},
{"source": "crossref", "success": false},
{"source": "doi_resolver", "success": false}
],
"download_time": 15.3
}Node.js 연동 (래퍼 스크립트)
프로젝트 scripts/download-paper-pdf.py가 Node.js API에서 호출됩니다.
Content 모드 (일반 논문)
python3 scripts/download-paper-pdf.py \
--doi "10.1234/example" \
--output "/path/to/output.pdf" \
--job-id "job-id" \
--content-id "content-id" \
--db-path "/path/to/dev.db"PdfDownloadJob테이블로 상태 추적- 실패 시 자동 브라우저 캡처 fallback (
/browser-pdf-capture) - Stuck Job 감지: 10분 이상 PENDING/DOWNLOADING 상태면 FAILED로 자동 전환
MetaPaper 모드 (메타분석)
python3 scripts/download-paper-pdf.py \
--doi "10.1234/example" \
--output "/path/to/output.pdf" \
--job-id "meta-paperId" \
--content-id "paperId" \
--db-path "/path/to/dev.db" \
--table "MetaPaper" \
--pdf-url-prefix "/uploads/meta-papers".status.json파일로 상태 추적- 5분 타임아웃 (DOWNLOADING 상태 초과 시 FAILED)
자동 폴백 흐름
1. PDF 수집 클릭 → download-paper-pdf.py 실행
2. 6단계 폴백 체인 시도
3. 모두 실패 → PdfDownloadButton이 자동 브라우저 캡처 시작
4. browser-capture-pdf.py 실행 (DOI → 웹페이지 → PDF 캡처)
5. 성공 시 DB 업데이트 + 상태 파일 갱신트러블슈팅
1. SSL 인증서 검증 실패 (macOS Python 3.14)
원인: Python 3.14 + LibreSSL 2.8.3의 SSL 인증서 검증 비호환 해결: 래퍼 스크립트(download-paper-pdf.py)에 SSL 우회 포함됨 (자동)
2. 403 Forbidden 에러
원인: 봇 감지에 걸림 해결:
- 자동으로 재시도 → 실패 시 자동 브라우저 캡처
- 수동:
/browser-pdf-capture스킬 사용
3. Stuck Job (다운로드 재시도 불가)
원인: Python 스크립트 크래시로 PdfDownloadJob이 PENDING/DOWNLOADING에서 stuck 해결: v2026-02에서 10분 타임아웃 자동 FAILED 처리 추가됨
4. 타임아웃
downloader = PaperPDFDownloader(
request_timeout=60, # 기본 30초
download_timeout=120, # 기본 60초
)5. PDF 검증 실패
원인: HTML 페이지가 반환됨 (로그인 필요 등) 해결: 기관 VPN 연결 또는 오픈 액세스 버전 확인
6. Unpaywall API 실패
export UNPAYWALL_EMAIL="your@email.com"7. 모든 소스에서 실패
해결 순서:
- DOI/PMID가 올바른지 확인
- 기관 네트워크에서 시도
/browser-pdf-capture스킬로 브라우저 캡처 시도
API 응답 구조
DownloadResult
@dataclass
class DownloadResult:
success: bool # 성공 여부
filepath: str = None # 저장된 파일 경로
source: str = None # 다운로드 소스 (unpaywall, crossref, etc.)
identifier: str = None # 입력 식별자
identifier_type: str = None # doi, pmid, url
doi: str = None # 정규화된 DOI
file_size: int = None # 파일 크기 (bytes)
download_time: float = None # 소요 시간 (초)
error_message: str = None # 에러 메시지
attempts: list = None # 시도 목록지원하는 소스 요약
| 소스 | 설명 | 비용 |
|---|---|---|
| Unpaywall | Open Access PDF | 무료 |
| CrossRef | 출판사 메타데이터 | 무료 |
| DOI Resolver | 리다이렉트 처리 | 무료 |
| PMC | PubMed Central | 무료 |
| Wiley | URL 패턴 변환 | 무료 (기관 인증 필요) |
| Elsevier | PII 기반 | 무료 (기관 인증 필요) |
관련 파일
| 파일 | 경로 | 설명 |
|---|---|---|
| Python 패키지 | ~/projects/skills/paper_pdf_downloader/ | 메인 모듈 |
| Content 래퍼 | 프로젝트 scripts/download-paper-pdf.py | Node.js 연동 |
| Content API | 프로젝트 src/app/api/papers/[id]/pdf-download/route.ts | 일반 논문 API |
| MetaPaper API | 프로젝트 src/app/api/meta-analysis/[id]/papers/[paperId]/pdf-download/route.ts | 메타분석 API |
| Skill 정의 | ~/projects/skills/skills/paper-pdf-downloader/SKILL.md | 이 문서 |
업데이트 이력
- 2026-02-13: SSL 우회(macOS Python 3.14), cloudscraper 대체, Stuck Job 감지, 로깅, MetaPaper 지원 문서화
- 2025-02-03: 봇 감지 우회 기법, 폴백 체인, 트러블슈팅 가이드 추가
- 2025-01: 초기 버전 작성