🧬 연구 허브

Skill

paper-pdf-downloader 코드 참조

논문 PDF 를 받는다. 6단계로 물러나며 시도한다.

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_scraper

User-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/xxx

5단계: 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.xxx

Elsevier (ScienceDirect)

class ElsevierHandler:
    domains = ['sciencedirect.com', 'elsevier.com']
    # PII 추출 → /science/article/pii/{PII}/pdfft

환경 변수

변수설명필수
UNPAYWALL_EMAILUnpaywall API 이메일Yes
ENTREZ_EMAILPubMed 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. 모든 소스에서 실패

해결 순서:

  1. DOI/PMID가 올바른지 확인
  2. 기관 네트워크에서 시도
  3. /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      # 시도 목록

지원하는 소스 요약

소스설명비용
UnpaywallOpen Access PDF무료
CrossRef출판사 메타데이터무료
DOI Resolver리다이렉트 처리무료
PMCPubMed Central무료
WileyURL 패턴 변환무료 (기관 인증 필요)
ElsevierPII 기반무료 (기관 인증 필요)

관련 파일

파일경로설명
Python 패키지~/projects/skills/paper_pdf_downloader/메인 모듈
Content 래퍼프로젝트 scripts/download-paper-pdf.pyNode.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: 초기 버전 작성