- src/dist 산출물 분리 원칙 정리(.gitignore, .gitattributes) - 루트 및 주요 폴더(config/scripts/prompts/tests/src, 런타임 폴더 5종)에 안내용 README.md 추가 - CHANGELOG.md, LICENSE, docs/ops/05-release-and-versioning.md 추가 - docs/README.md 문서 지도 갱신
205 KiB
유사 프로젝트 벤치마킹과 채택 구조
이 문서의 역할: DMF_Crawler 를 스크래치에서 만들기 전에, 실제로 존재가 확인된 GitHub 저장소 37개 이상을 범주별로 해부하여 "무엇을 베끼고 무엇을 버릴지"를 확정하고, 그 근거 위에서 최종 디렉터리 구조와 모듈 경계(fetch / parse / diff / store / report / notify / orchestrate)를 SSOT 로 못박는 문서다.
0. 한눈에 보기
이 문서에서 내린 결론(상세 근거는 각 섹션에):
- 한국 식약처(MFDS) DMF 전용 오픈소스는 존재하지 않는다. 30회 이상의 검색과 68회의 실제 페이지 fetch 로도
nedrug/MFDS/DMF를 대상으로 한 크롤러 저장소를 하나도 찾지 못했다. 가장 근접한 것이Q00/data.go.kr-crawling(★4, DUR 품목정보 API → xlsx)뿐이다. 즉 fetch/parse 계층은 우리가 직접 쓴다. 베낄 것은 "구조"이지 "코드"가 아니다. - 1차 데이터 소스는 스크래핑이 아니라 공공 API 로 확정한다.
식품의약품안전처_원료의약품등록(DMF)현황OpenAPI 엔드포인트https://apis.data.go.kr/1471000/MdcDmfInfoService01/getMdcDmfList01가DMF_PERMIT_NO,INGR_KOR_NAME,ENTP_NAME,MNFCTR_NAME,MNFCTR_PLACE,MANUF_COUNTRY_CODE_NM,DMF_PERMIT_DATE를 JSON/XML 로 준다. 공고 게시판(nedrug.mfds.go.kr/bbs/117, 총 710건)은 API 가 놓치는 "변경/취하" 서사를 보완하는 2차 소스다. - 아키텍처의 정본 레퍼런스는
mrueda/nomenclator-delta다. 스페인 보건부 의약품 목록의 월간 델타를 추적하는 프로젝트로,수집(collection) → 정규화(normalization) → 디핑(diffing) → 검증(validation)이라는 4단계 모듈 분리와data/(스냅샷 + 변경 이력) 레이아웃이 우리 요구사항과 1:1 로 대응한다. 모듈 경계는 이걸 그대로 채택한다. - 범용 변경감지 도구(changedetection.io ★33.5k, urlwatch ★3.1k, huginn ★49.9k)는 "도입"하지 않고 "설계 개념만" 가져온다. 이들은 텍스트 블록 diff 도구지 레코드 키 기반 diff 도구가 아니다. 우리에게 필요한 것은
DMF_PERMIT_NO를 키로 한 added/removed/changed 판정이고, 그 형태는larsyencken/csvdiff(★131,_index/added/removed/changedJSON 구조)가 정답이다. 다만 csvdiff 는 2021-02-18 아카이브되었으므로 JSON 스키마만 채택하고 구현은 자체 작성한다. - 운영 방식은 "Windows Task Scheduler(schtasks) + 상주 워치독" 2단 구조로 간다.
michalzobec/autorunsalerts가 검증한 패턴 — SYSTEM 컨텍스트 스캔 태스크와 사용자 컨텍스트 토스트 태스크를 분리 — 를 그대로 채택한다. 토스트는 SYSTEM/서비스 세션에서 뜨지 않기 때문이다(BurntToast 문서가 명시). - 서비스화는 WinSW(★14.3k,
v2.12.0안정)를 1순위로 한다.<onfailure action="restart" delay="10 sec"/>+<resetfailure>1 hour</resetfailure>+<startmode>Automatic</startmode>+<log mode="roll">이 XML 한 장에 선언적으로 들어가고 .NET 외 런타임 의존이 없다. NSSM(★1.2k, v2.24 / 2014-08-31)은 대안, pywin32 서비스는 기각(복구 액션이 트리거되지 않는 알려진 결함 — pywin32 issue #1563). - AI CLI headless 호출 규약은
claude -p/gemini -p/codex exec3종의 공통 패턴을agy -p로 사상한다: (1) 프롬프트를 파일로 빼서-p "$(cat prompt.txt)", (2)--output-format json+ JSON Schema 로 구조화 출력 강제, (3) stdout 을 파일로 리다이렉트 후 파싱, (4) 종료코드로 분기.agy자체의 플래그는 raw dump 에서 확인되지 않았으므로 ⚠️ 미검증이며 부트스트랩 단계에서agy --help로 실측해야 한다. - 리포트는 xlsxwriter 로 만들고, 탭 간 연동은
write_url(row, col, 'internal:Sheet2!A1')로 구현한다. openpyxl 대신 xlsxwriter 를 쓰는 이유는add_table()/conditional_format()/ 내부 하이퍼링크가 한 API 로 깔끔하게 나오기 때문이다(Bwhiz/Auto-Excel-Reports는 openpyxl 예시이나 서식 품질 요구가 우리보다 낮다). - 알림은 Apprise(★17.2k) 한 겹으로 추상화한다.
windows://(pywin32 필요, 250자 제한, 같은 PC 한정),tgram://bottoken/ChatID,slack://,mailto://를 URL 문자열 하나로 갈아끼울 수 있다. 다만 리치 토스트(버튼/이미지)는win11toast(★333) 로 직접 호출하는 이중 경로를 둔다. - 최종 디렉터리 구조는
src/dmf_crawler/{fetch,parse,diff,store,report,notify,orchestrate}+data/{raw,snapshots,history}+ops/{winsw,tasks,watchdog}+config/sources.yaml이다. 각 경로의 출처는 §10 에 저장소별로 명기했다.
1. 목차
- 0. 한눈에 보기
- 1. 목차
- 2. 조사 방법과 신뢰도 표기 규칙
- 3. 전체 저장소 인덱스 (실존 확인 37건)
- 4. (a) 한국 식약처 / 공공데이터 크롤러
- 5. (b) FDA / openFDA / 규제 데이터
- 6. (c) 규제 변경 감지 · 인텔리전스
- 7. (d) 범용 변경 감지 도구
- 8. (e) 크롤링 → 엑셀/시트 리포트 파이프라인
- 9. (f) AI CLI headless 자동화
- 10. (g) Windows 서비스화 · 워치독 · 토스트
- 11. (h) awesome 리스트 및 기타 참고
- 12. 채택 결정 표 (채택 / 부분채택 / 기각)
- 13. 최종 디렉터리 구조 제안
- 14. 모듈 경계 제안과 입출력 계약
- 15. 데이터 소스 실측 정보 (DMF API / 공고 게시판)
- 부록 A. 출처 목록
- 부록 B. 미해결 질문 / 실측 필요 항목
2. 조사 방법과 신뢰도 표기 규칙
원본 리서치는 WebSearch 33회 + WebFetch 68회로 수행되었다(WebSearch 예산 200/200 소진으로 #31, #32, #33 검색은 미수행). 본 문서는 그 raw dump 를 손실 없이 정제한 것이다.
| 표기 | 의미 |
|---|---|
| [F#n] | raw dump 의 [FETCH #n] — 해당 페이지를 실제로 열어 확인함 (verified_by_fetch=true) |
| [S#n] | raw dump 의 [SEARCH #n] — 검색 결과 링크로만 등장. 페이지를 직접 열지 않음 |
| ⚠️ 미검증 | 존재/수치를 직접 확인하지 못함. 삭제하지 않고 남기되 구현 전 실측 필요 |
스타 수·커밋 수는 2026-09-02 조사 시점 기준이며, GitHub 페이지에 "last commit date" 가 텍스트로 노출되지 않은 경우 커밋 총수로 대체 기록했다(원 dump 가 그렇게 기록했으므로 그대로 보존).
3. 전체 저장소 인덱스 (실존 확인 37건)
아래는 WebFetch 로 저장소 페이지를 실제로 열어 확인한 항목이다. 범주 기호는 원 조사 항목 (a)~(h) 를 따른다.
| # | 저장소 | URL | ★ | Fork | 언어 | 최근 활동 | 무엇을 하는가 | 이 프로젝트에서 정확히 무엇을 베낄 것인가 |
|---|---|---|---|---|---|---|---|---|
| 1 | Q00/data.go.kr-crawling |
https://github.com/Q00/data.go.kr-crawling | 4 | 0 | Python | development 브랜치 55 commits | 건강정보·의약품 크롤링, gevent 멀티스레딩 표방 | config.py.example 로 API 키를 코드 밖으로 빼는 패턴, column.py 로 응답 필드명↔한글명 매핑을 생성해서 파일로 떨구는 아이디어, page = int(totalCount/100) + 1 페이지네이션 공식 |
| 2 | jjscan/data.go.kr-1 |
https://github.com/jjscan/data.go.kr-1 | 0 | 0 | R | 미표기 | data.go.kr MFDS DURPrdlstInfoService/getUsjntTabooInfoList 수집 |
실패 사례 카탈로그: 연결 실패→폴링 재시도, totalCount == 0 로 빈 응답 판정, 351,010건 단일스레드 17시간 병목. 우리 재시도/공백판정 로직의 근거 |
| 3 | WooilJeong/PublicDataReader |
https://github.com/WooilJeong/PublicDataReader | 597 | 113 | Python | 168 commits | 공공데이터포털/KOSIS/ECOS 등 조회 파이썬 라이브러리 | 공공 API 래퍼의 패키지 레이아웃과 provider 별 모듈 분리. 단 식약처/의약품 커버리지는 없음(문서 명시) → 의존하지 않고 구조만 참고 |
| 4 | NomaDamas/k-skill |
https://github.com/NomaDamas/k-skill | 7.4k | — | — | — | 한국 특화 스킬 모음. docs/features/mfds-food-safety.md + scripts/mfds_food_safety.py |
API 키를 사용자 머신이 아니라 프록시 서버 환경변수(DATA_GO_KR_API_KEY, FOODSAFETYKOREA_API_KEY)에 두는 분리 원칙. 우리는 로컬 단독이므로 .env 로 대체하되 "키를 코드/리포지토리에 절대 넣지 않는다"는 규범만 채택 |
| 5 | FDA/openfda |
https://github.com/FDA/openfda | 705 | 166 | Python | — | FDA 공식. Luigi 파이프라인으로 공개 데이터셋 → JSON → Elasticsearch | openfda/ 패키지 + schemas/ + config/ + scripts/ 4분할 레이아웃. 데이터셋별 파이프라인 파일 분리(NSDE, CAERS, Substance, Device Clearance, Device PMA, Device Event) |
| 6 | jbremz/FDA-Analysis |
https://github.com/jbremz/FDA-Analysis | 5 | 2 | Python | — | (은퇴한) Drugs@FDA 사이트를 Scrapy 로 22,000+ 제품 스크래핑 후 pandas 분석 | "스파이더 디렉터리 / 원시 CSV / 분석 스크립트 / 노트북" 4분할. 우리 data/raw ↔ notebooks/ 분리의 근거 |
| 7 | logiover/fda-data-scraper |
https://github.com/logiover/fda-data-scraper | 0 | 0 | 미표기 | 1 commit | openFDA 9개 데이터셋 → JSON/CSV/XLSX/JSONL/XML/HTML | 출력 포맷을 하나의 CLI 인자로 스위칭하는 설계. 우리도 `--format xlsx |
| 8 | coderxio/OpenFDA |
https://github.com/coderxio/OpenFDA | 3 | 3 | Python | — | openFDA drug NDC 데이터셋을 DB 에 적재, CherryPy 로 서빙 | docker-compose.yml + docker-compose.override.yml 로 최초적재/운영 구성 분리. 우리는 Docker 미사용이나 "초기 백필 실행"과 "일일 증분 실행"을 다른 엔트리포인트로 분리하는 개념을 채택 |
| 9 | Tanguy9862/AI-Powered-FDA-Drug-Scraper |
https://github.com/Tanguy9862/AI-Powered-FDA-Drug-Scraper | 3 | 0 | Python | — | Drugs.com 신약 승인 페이지 스크래핑 → LangChain+GPT-4o-mini 분류 (1,770건) | scraper.py / classification.py / utils.py 3파일 분리 — 수집과 LLM 후처리를 절대 한 파일에 섞지 않는다. 회사명 표기 정규화(약 1000→700종)의 필요성 근거 |
| 10 | anton-semerenko/pharma-radar |
https://github.com/anton-semerenko/pharma-radar | 0 | 0 | Python | 2 commits | Claude(Opus급) 에이전트가 매일 06:00 규제/경쟁 인텔리전스 브리핑 생성 → Telegraph + Telegram | 가장 유사한 프로젝트. ① prompts/system_prompt.md 로 에이전트 방법론을 파일로 분리 ② config/sources.yaml 로 소스 계층·루브릭 정의 ③ src/deliver.py 는 표준 라이브러리만 사용 ④ "≥2개 독립 출처 또는 1개 공식 규제 1차 출처"라는 검증 정책 ⑤ 매일 06:00 스케줄 — 우리 요구사항과 동일 |
| 11 | mrueda/nomenclator-delta |
https://github.com/mrueda/nomenclator-delta | 1 | 0 | Python | Updated Aug 9, 2026 | 스페인 보건부 Nomenclátor 의약품 목록의 월간 델타(추가/삭제/변경) 비교 | 아키텍처 정본. src/nomenclator_delta/(collection·normalization·diffing·validation), data/(스냅샷+변경이력), site/(정적 앱), docs-site/, tests/. CLI 는 python3 -m nomenclator_delta validate data / python3 -m nomenclator_delta dist 형태의 서브커맨드 |
| 12 | Mzands2622/Zanalytix |
https://github.com/Mzands2622/Zanalytix | 0 | 0 | Python | Updated Mar 9, 2026 / 1 commit | 60+ 제약사 파이프라인 페이지 스크래핑 → GPT-4o 로 old vs new 비교, 우선순위 1~5 부여 | ① 소스별 파서 모듈을 1파일 1소스로 쪼개는 규칙({company}_pipeline.py, fetch_{company}_html() / process_{company}_html() 시그니처 통일) ② 날짜 스냅샷을 JSON 으로 보관 ③ 변경건에 우선순위 점수를 매겨 알림 대상 선별 |
| 13 | suriyadeepan/WebScraping-for-Healthcare |
https://github.com/suriyadeepan/WebScraping-for-Healthcare | 8 | 1 | Python | 44 commits | DrugBank·ClinicalTrials.gov·EMC(SMPC/PIL)·HPRA·MHRA 등 규제/의약 데이터 수집 | phscrape 패키지 안에서 소스별 모듈이 동일한 fetch() / crawl_k() 인터페이스를 노출하는 규약 |
| 14 | dgtlmoon/changedetection.io |
https://github.com/dgtlmoon/changedetection.io | 33.5k | 2.0k | Python | 2,448 commits / release 0.55.8 (13 Jul 09:26) |
웹페이지 변경 감지·알림 SaaS/셀프호스트 | 개념만: CSS/XPath/JSONPath/jq 로 감시 범위를 좁히는 필터 체인, word/line/character 3단계 diff 시각화, 타임존 인식 스케줄(요일·시간 제한), Apprise 알림, {{diff}}/{{diff_added}}/{{diff_removed}} 템플릿 토큰 |
| 15 | thp/urlwatch |
https://github.com/thp/urlwatch | 3.1k | 354 | Python | 974 commits / 릴리스 없음 | URL·셸 명령 출력의 변경을 감시하고 unified diff 로 통지 | urls.yaml 잡 정의 스키마(url/name/method/data/headers/cookies/encoding/filter/ignore_connection_errors), job_defaults 로 공통 설정 상속, 28종 내장 필터 체인 개념, diff_filter 로 diff 결과 자체를 후처리하는 발상 |
| 16 | huginn/huginn |
https://github.com/huginn/huginn | 49.9k | 4.3k | Ruby | 4,134 commits | 에이전트가 웹을 읽고 이벤트를 만들어 유향 그래프로 전파 | WebsiteAgent 의 mode: all / on_change / merge 3분류 — 우리 diff 모듈의 출력 모드와 정확히 대응. 기본 스케줄 every_12h. extract 설정이 선언적 추출 스펙을 데이터로 표현 |
| 17 | larsyencken/csvdiff |
https://github.com/larsyencken/csvdiff | 131 | 31 | Python | 2021-02-18 아카이브 | 두 CSV 를 키 기준으로 비교해 added/removed/changed 산출 | JSON 출력 스키마를 그대로 채택: _index(키 컬럼 배열), added, removed, changed(키별 field-level from/to). CLI 옵션 --style=summary|pretty, --output, --ignore-columns, --significance 도 우리 CLI 에 이식 |
| 18 | ecprice/newsdiffs |
https://github.com/ecprice/newsdiffs | 506 | 136 | Python (Django) | — | 뉴스 기사 변경 이력 추적 프레임워크 | parsers/ 아래 BaseParser 상속 사이트별 서브클래스 구조, 진행 로그와 에러 로그를 분리 기록(/tmp/newsdiffs_logging per-run vs /tmp/newsdiffs/logging_errs cumulative) |
| 19 | simonw/git-scraper-template |
https://github.com/simonw/git-scraper-template | 132 | 10 | — | — | GitHub Actions 로 URL 을 주기적으로 받아 변경 시 커밋(Git scraping) | "스냅샷을 버전관리에 커밋해 변경 이력 자체를 만든다"는 발상. 우리는 GitHub Actions 대신 로컬 Task Scheduler + 로컬 git 리포로 data/snapshots 를 커밋 |
| 20 | Bwhiz/Auto-Excel-Reports |
https://github.com/Bwhiz/Auto-Excel-Reports | 1 | 0 | Python | — | openpyxl 로 엑셀 리포트 생성 + GitHub Actions cron + SMTP 발송 | report_script.py(생성) / auto_mail.py(배포) 분리, cron 0 0 * * *, 자격증명은 GitHub Secrets → 우리는 .env |
| 21 | HasData/playwright-scraping |
https://github.com/HasData/playwright-scraping | 15 | 4 | Python/Node.js | 5 commits | Playwright 스크래핑 레시피 모음 | 디렉터리 분류 자체가 체크리스트: basics/ scraping/ selectors/ interactions/ save_data/ auth/ browser/ errors/ debug/. 특히 errors/(재시도·타임아웃)와 debug/(video/trace 녹화)를 우리 fetch 모듈 설계 항목으로 채택 |
| 22 | jshchnz/claude-code-scheduler |
https://github.com/jshchnz/claude-code-scheduler | 510 | 37 | TypeScript | — | AI CLI 를 OS 네이티브 스케줄러에 등록해 claude -p 자동 실행 |
① OS별 스케줄러 어댑터 분리(src/schedulers/base.ts, darwin.ts, linux.ts, windows.ts, index.ts) ② 스케줄 정의를 JSON 파일로(.claude/schedules.json) ③ 로그를 태스크 ID별 파일로(~/.claude/logs/<task-id>.log). Windows 는 Task Scheduler 사용 |
| 23 | addyosmani/gemini-cli-tips |
https://github.com/addyosmani/gemini-cli-tips | 2.4k | 105 | — | — | Gemini CLI 팁 모음 | headless gemini -p "...", stdin 파이프, --format=json, GEMINI_SYSTEM_MD 환경변수로 시스템 프롬프트 교체 — agy 의 동등 플래그를 찾을 때의 탐색 체크리스트 |
| 24 | winsw/winsw |
https://github.com/winsw/winsw | 14.3k | 1.7k | C# | v3 브랜치 841 commits / 최신 v3.0.0-alpha.11(29 Jan 02:20), 안정 v2.12.0(28 Jan 16:22) |
임의 실행파일을 Windows 서비스로 감싸는 래퍼 | XML 한 장으로 서비스 정의: <executable>, <arguments>, <onfailure action="restart" delay="10 sec"/>, <resetfailure>1 hour</resetfailure>, <startmode>Automatic</startmode>, <delayedAutoStart>true</delayedAutoStart>, <log mode="roll">, <env name= value=>, <workingdirectory>, <priority>, <serviceaccount> |
| 25 | kirillkovalenko/nssm |
https://github.com/kirillkovalenko/nssm | 1.2k | 169 | C++ | v2.24 (2014-08-31) | NSSM — 애플리케이션을 NT 서비스로 실행, 실패 시 재시작 | WinSW 대안. CLI 로 AppDirectory/AppParameters/AppStdout/AppStderr/AppThrottle/AppRestartDelay/AppRotateFiles/Start 설정 (§10.2 에 전체 명령 보존) |
| 26 | larsekje/PythonWindowsServices |
https://github.com/larsekje/PythonWindowsServices | 1 | 0 | Python | — | NSSM 으로 파이썬 스크립트를 서비스로 돌리는 PoC | /logs, /scripts, /windows_service 3분할. **"NSSM 은 로그 파일을 자동 생성하지 않으므로 사전 생성 필요"**라는 실전 함정 |
| 27 | HaroldMills/Python-Windows-Service-Example |
https://github.com/HaroldMills/Python-Windows-Service-Example | 21 | 11 | Python | — | pywin32 + PyInstaller 로 파이썬 서비스 빌드 | 반면교사. example_service.exe install / start 를 관리자 권한 프롬프트에서 실행해야 함. PyInstaller 가 Python 3.5 까지만 지원한다는 오래된 주석(2016) → 이 경로는 기각 근거 |
| 28 | mhammond/pywin32 (issue #1563) |
https://github.com/mhammond/pywin32/issues/1563 | — | — | Python | — | 서비스 크래시 시 Windows 복구 액션이 트리거되지 않는 문제 | pywin32 서비스 기각의 결정적 근거: SvcRun() 이 예외를 던지거나 sys.exit() 해도 pywin32 정리 코드가 SERVICE_STOPPED 를 보고해버려 복구 액션이 발동하지 않는다. 우회책이 os.kill(os.getpid(), signal.SIGABRT) 수준 |
| 29 | Windos/BurntToast |
https://github.com/Windos/BurntToast | 1.7k | 126 | PowerShell | v1.1.0 | Windows 10/Server 2019+ 토스트 알림 PowerShell 모듈 | Install-Module -Name BurntToast, New-BurntToastNotification -Text ..., New-BTButton, -AppLogo. 핵심 제약: SYSTEM/서비스 계정에서는 데스크톱 세션 요구 때문에 동작 제한 → 워치독 2단 분리의 근거 |
| 30 | michalzobec/autorunsalerts |
https://github.com/michalzobec/autorunsalerts | 0 | — | PowerShell | — | autoruns 설정 변경을 감지해 토스트로 알림 | 운영 패턴 정본. ① AutorunsAlert(SYSTEM, 60분마다): 스캔→state.json 과 비교→audit.log 기록 ② AutorunsAlertToast(사용자 컨텍스트, 15분마다): 플래그 확인 후 토스트. 파일 구성 autorunsalert.ps1/autorunstoast.ps1/configuration.json/state.json/audit.log/install.ps1/uninstall.ps1 |
| 31 | DatGuy1/Windows-Toasts |
https://github.com/DatGuy1/Windows-Toasts | 142 | 9 | Python | — | WinRT 기반 파이썬 토스트 | python -m pip install windows-toasts. Toast(), WindowsToaster('Python'), text_fields, on_activated 콜백. duration 이 short/long 만 지원(pywin32 대비 제약) |
| 32 | GitHub30/win11toast |
https://github.com/GitHub30/win11toast | 333 | 24 | Python | — | Windows 10/11 토스트 (WinRT) | pip install win11toast. toast(), notify()(논블로킹), toast_async(), buttons=[...], on_click='https://...', image=, duration='long'. 함정: 스크립트 실행 시 현재 디렉터리가 C:\Windows\system32 이므로 os.chdir() 필요 |
| 33 | ysfchn/toasted |
https://github.com/ysfchn/toasted | 31 | 2 | Python | — | 리치 토스트(이미지/select/input/progress) | Progress(value="{value}", status="...") 로 진행률 토스트 — 백필 실행처럼 오래 걸리는 작업의 진행 표시에 사용 가능 |
| 34 | caronc/apprise |
https://github.com/caronc/apprise | 17.2k | 652 | Python | 1,178 commits | 100+ 알림 서비스를 URL 문자열 하나로 통합 | pip install apprise; apobj.add('...') / apobj.notify(body=, title=). URL: windows://(pywin32 필요, ?duration=5, 250자 제한, 타 PC 전송 불가), slack://TokenA/TokenB/TokenC/Channel, tgram://bottoken/ChatID, mailto:///mailtos:// |
| 35 | 786raees/task-scheduler-python |
https://github.com/786raees/task-scheduler-python | 2 | 0 | Python | 2 commits | win32com.client 로 Windows Task Scheduler 제어 |
create_task()(실행경로/인자/ISO 8601 트리거), get_all_tasks(), toggle_task(), run_task(), delete_task() — 설치 스크립트에서 schtasks 문자열 조립 대신 COM 으로 다루는 대안 |
| 36 | lorien/awesome-web-scraping |
https://github.com/lorien/awesome-web-scraping | 8.1k | 934 | — | 640 commits | 스크래핑 라이브러리/도구/API 큐레이션 | python.md / javascript.md / php.md / ruby.md / golang.md / cli.md / manuals.md 언어별 분할. 변경감지·스케줄링·엑셀 섹션은 없음(확인함) → 라이브러리 선정 참고용으로만 |
| 37 | testing-in-production/gemini-jobs |
https://github.com/testing-in-production/gemini-jobs | — | — | — | — | (블로그가 언급한 cron+Gemini CLI 예제 저장소) | HTTP 404 — 존재하지 않음. ⚠️ 미검증. 참조 금지 |
3.1 검색 결과에만 등장한 저장소 (⚠️ 미검증 — 페이지를 직접 열지 않음)
버리지 않고 남긴다. 필요 시 실측 후 승격.
| 저장소 | URL | 범주 | 검색에서 파악된 내용 |
|---|---|---|---|
DarpitPatel/OpenFDA |
https://github.com/DarpitPatel/OpenFDA | (b) | openFDA API 를 파이썬으로 스크래핑해 txt + CSV 출력 [S#3] |
shaayohn/fda-drug-aproval-data-scraping |
https://github.com/shaayohn/fda-drug-aproval-data-scraping | (b) | Drugs@FDA 에서 특정 기준의 승인 상세를 가져옴 [S#6] |
tsbischof/fda |
https://github.com/tsbischof/fda | (b) | FDA 510(k) 및 관련 문서 스크래퍼·집계, predicate device 분석 [S#19] |
sheetalkalburgi/web-scraping |
https://github.com/sheetalkalburgi/web-scraping | (b)(c) | BeautifulSoup 로 FDA + Health Canada 사이트 스크래핑 [S#19] |
Norbaeocystin/FDA |
https://github.com/Norbaeocystin/FDA | (b) | FDA 의약품 승인 데이터 스크래핑·분석 [S#19] |
vshah1016/pharma_scraper |
https://github.com/vshah1016/pharma_scraper | (b) | Biopharmcatalyst PDUFA 캘린더 → CSV [S#19][S#27] |
arpitamangal/pharma-scrape-and-analysis |
https://github.com/arpitamangal/pharma-scrape-and-analysis | (b) | 대체 브랜드 식별, FDA 40개 카테고리 분류 [S#8] |
rOpenHealth/openfda |
https://github.com/rOpenHealth/openfda | (b) | R 패키지. jsonlite/magrittr 로 openFDA 접근 [S#23] |
roivant/openfda |
https://github.com/roivant/openfda | (b) | openFDA 관련 포크 [S#23] |
betagouv/api-medicaments |
https://github.com/betagouv/api-medicaments | (a 유사) | 프랑스 ANSM 공식 의약품 공개 DB API [S#13] |
kawsarlog/AmerisourceBergen |
https://github.com/kawsarlog/AmerisourceBergen | (b) | ★4, Python, Updated Aug 5, 2023. AmerisourceBergen 제품 가격 추출 자동화 [F#19] |
MohammedAhmed-01/DataDoseProject |
https://github.com/MohammedAhmed-01/DataDoseProject | (b) | ★0, Jupyter Notebook, Updated Mar 22, 2026. 성분 검증 + OpenFDA 라벨 보강 + DDI 탐지 파이프라인 [F#19] |
Dagiayy/kara-medical-telegram-data-platform |
https://github.com/Dagiayy/kara-medical-telegram-data-platform | (e) | ★0, Python, Updated Aug 23, 2026. Telegram 스크래핑 + PostgreSQL + Dagster 오케스트레이션 [F#19] |
bdmorris238/pharmaco-database-project |
https://github.com/bdmorris238/pharmaco-database-project | (b) | ★0, PLpgSQL, Updated Aug 15, 2025. 제약 시장 분석용 관계형/DW 설계 [F#19] |
khushihajiyani-dotcom/drug-spending-analysis |
https://github.com/khushihajiyani-dotcom/drug-spending-analysis | (b) | ★0, SQLite, Updated May 3, 2026. 캐나다 주별 약품 지출 분석 2020-2024 [F#19] |
Tanguy9862/new-drug-approvals-dashboard |
https://github.com/Tanguy9862/new-drug-approvals-dashboard | (e) | 위 #9 의 자매 프로젝트. Dash 로 실시간 대시보드 [F#42] |
patrickloeber/llm-data-scrapers |
https://github.com/patrickloeber/llm-data-scrapers | (h) | LLM 용 데이터 수집 오픈소스 도구 목록 [S#18] |
ManiMozaffar/linkedIn-scraper |
https://github.com/ManiMozaffar/linkedIn-scraper | (e) | Playwright + FastAPI, 결과를 DB 와 Telegram 채널로 [S#17] |
dineshk-qa/playwright.slack.reporter |
https://github.com/dineshk-qa/playwright.slack.reporter | (e) | Playwright 결과를 Slack 웹훅으로 리포팅 [S#17] |
god233012yamil/Excel-Automation-Using-Python |
https://github.com/god233012yamil/Excel-Automation-Using-Python | (e) | openpyxl 엑셀 자동화 예제 모음 [S#7] |
prabudevarajan/Task-Reminder-Automation-Python-Excel-CSV-Email-Alerts |
https://github.com/prabudevarajan/Task-Reminder-Automation-Python-Excel-CSV-Email-Alerts | (e) | Tkinter UI + Excel/CSV 저장 + 15/7/3/1일 전 이메일 리마인더 + daily scheduler + 로깅 [S#7] |
jithurjacob/Windows-10-Toast-Notifications |
https://github.com/jithurjacob/Windows-10-Toast-Notifications | (g) | win10toast 원본. 커스텀 아이콘, threaded 알림 [S#9] |
jacobcolbert/Windows-10-Toast-Notifications |
https://github.com/jacobcolbert/Windows-10-Toast-Notifications | (g) | 위 포크 [S#9] |
NakedPowerShell/BurntToast |
https://github.com/NakedPowerShell/BurntToast | (g) | BurntToast 포크 [S#25] |
Badgerati/Hook |
https://github.com/Badgerati/Hook | (g) | PowerShell 모듈. 서비스 상태 감시 후 BurntToast 팝업 [S#25] |
mattwolfe/changedetection |
https://github.com/mattwolfe/changedetection | (d) | changedetection.io 포크 [S#5] |
huginn/huginn_agent |
https://github.com/huginn/huginn_agent | (d) | Huginn 에이전트를 Gem 으로 만드는 베이스 [S#12] |
SublimeText/Pywin32 |
https://github.com/SublimeText/Pywin32 | (g) | pywin32 번들 [S#16] |
WinSW-Windows (org) |
https://github.com/WinSW-Windows | (g) | WinSW 관련 조직 계정 [S#15] |
google-gemini/gemini-cli Discussion #3215 |
https://github.com/google-gemini/gemini-cli/discussions/3215 | (f) | "Headless execution" 논의 스레드 [S#20] |
realpython/list-of-python-api-wrappers |
https://github.com/realpython/list-of-python-api-wrappers | (h) | 파이썬 API 래퍼 목록 [S#23] |
noirquant/awesome-web-scraping |
https://github.com/noirquant/awesome-web-scraping | (h) | lorien 포크 [S#8] |
jjwangnlp/awesome-web-scraping |
https://github.com/jjwangnlp/awesome-web-scraping | (h) | lorien 포크 [S#8] |
luminati-io/Awesome-Web-Scraping |
https://github.com/luminati-io/Awesome-Web-Scraping | (h) | HTTP 라이브러리·브라우저 자동화·프록시 서비스 포함 목록 [S#8] |
spinov001-art/awesome-web-scraping-2026 |
https://github.com/spinov001-art/awesome-web-scraping-2026 | (h) | 130+ 도구, Python/JS/Go/Rust, 안티디텍션·프록시·클라우드, 주간 갱신 [S#8] |
duyet/awesome-web-scraper |
https://github.com/duyet/awesome-web-scraper | (h) | 스크래퍼/크롤러 모음 [S#8] |
firecrawl/firecrawl |
https://github.com/firecrawl/firecrawl/releases | (d) | 릴리스 페이지가 검색에 노출 [S#24] |
diakes/coupang_crawler_python |
https://github.com/diakes/coupang_crawler_python | (a) | 쿠팡 크롤러 — 무관 [S#1] |
FareedKhan-dev/best-llm-finder-pipeline |
https://github.com/FareedKhan-dev/best-llm-finder-pipeline | (c) | Agentic RAG / 멀티에이전트 파이프라인 [S#18] |
architkaila/Fine-Tuning-LLMs-for-Medical-Entity-Extraction |
https://github.com/architkaila/Fine-Tuning-LLMs-for-Medical-Entity-Extraction | (c) | Llama2/StableLM PEFT·LoRA 로 약물명·부작용 추출 [S#18] |
guilopgar/Medication-Detection-LLM |
https://github.com/guilopgar/Medication-Detection-LLM | (c) | 소셜미디어 텍스트에서 약물 언급 탐지 [S#18] |
FDA (org) |
https://github.com/FDA | (b) | FDA 공식 GitHub 조직 [S#4] |
3.2 Gist (⚠️ 코드 조각, 저장소 아님)
| Gist | URL | 내용 |
|---|---|---|
drmalex07/10554232 |
https://gist.github.com/drmalex07/10554232 | pywin32 서비스 예제. class HelloWorldSvc(win32serviceutil.ServiceFramework), _svc_name_, _svc_display_name_, SvcStop, SvcDoRun, win32event.CreateEvent, win32serviceutil.HandleCommandLine [F#40] |
seanherron/5997278 |
https://gist.github.com/seanherron/5997278 | drugs@fda scraper [S#3][S#19] |
nmpowell/dc8e7187948788c5c126f01755252164 |
https://gist.github.com/nmpowell/dc8e7187948788c5c126f01755252164 | 기존 Windows Task Scheduler 태스크와 상호작용하는 파이썬 스크립트 [S#29] |
hygull/32a742339a416dcfa2990504c848c1a9 |
https://gist.github.com/hygull/32a742339a416dcfa2990504c848c1a9 | Windows 10 토스트 생성 [S#9] |
HainanZhao/92b43e68850189bfee8f39a2c2581ca6 |
https://gist.github.com/HainanZhao/92b43e68850189bfee8f39a2c2581ca6 | "Gemini CLI Job" 으로 반복 작업 자동화 [S#20] |
4. (a) 한국 식약처 / 공공데이터 크롤러
4.1 결론: DMF 전용 오픈소스는 없다
WebSearch #1, #2, #11, #13, #21, #22, #30 을 통해 다음 질의를 던졌으나 nedrug/MFDS/DMF 전용 크롤러 저장소는 하나도 나오지 않았다:
github nedrug 식약처 크롤링 의약품 crawler pythongithub 공공데이터포털 식약처 의약품 API python wrapper mfdsgithub 원료의약품 DMF 등록 식약처 크롤러 파이썬github 의약품안전나라 e약은요 API 파이썬 오픈API 크롤링 selenium 의약품 허가"nedrug.mfds.go.kr" github python selenium requests 크롤링 프로젝트github Korea MFDS drug approval scraper "nedrug" OR "mfds" pythongithub 공공데이터포털 data.go.kr 파이썬 라이브러리 PublicDataReader 식약처 의약품
검색 엔진이 반환한 것은 대부분 식약처 공식 포털 페이지와 블로그 튜토리얼이었다. 따라서 fetch/parse 계층은 우리가 최초 구현자라고 전제하고 설계한다.
4.2 Q00/data.go.kr-crawling [F#1][F#62][F#64]
| 항목 | 값 |
|---|---|
| URL | https://github.com/Q00/data.go.kr-crawling |
| 설명 | "건강정보, 의약품 크롤링, 멀티쓰레딩 gevent" |
| ★ / Fork | 4 / 0 |
| 언어 | Python |
| 커밋 | development 브랜치 55 commits |
| 토픽 | crawling, gevent, Python, python-lock, threading |
파일 구조:
apis/
async_data_crawler.py
go_data_crwaler.py # 오타 그대로 (crwaler)
column.py
url.py
config.py.example
requirements.txt
README.md
.gitignore
go_data_crwaler.py 실측 내용 [F#64] — 호출하는 data.go.kr MFDS 엔드포인트:
getDurPrdlstInfoList(메인 품목 목록)getSeobangjeongPartitnAtentInfoListgetEfcyDplctInfoListgetOdsnAtentInfoListgetMdctnPdAtentInfoListgetCpctyAtentInfoListgetPwnmTabooInfoListgetSpcifyAgrdeTabooInfoListgetUsjntTabooInfoList
핵심 코드 라인(원문 인용):
- 인증:
config.go_data_api_key - 파라미터 조립:
params.update({'typeName' : column.typeName[addUrl]}) - 페이지네이션:
params_str2 += '&pageNo=' + str(i+1) - 총 페이지 계산:
page = int(totalCount/100) + 1 - 출력:
wb.save(column.typeName[addUrl]+'.xlsx')
중요한 반증: 저장소 설명과 토픽은 gevent 를 내세우지만, 실제 go_data_crwaler.py 에는 gevent 사용이 없고 동기 requests.get() 만 쓴다. 저장소 설명을 믿지 말고 코드를 읽어야 한다는 교훈.
url.py 실측 내용 [F#62] — 이 파일은 크롤링이 아니라 API 명세 스크래핑을 한다:
- 대상:
https://www.data.go.kr/pubn/lab/gui/IrosDevGuide/selectReqResPrmList.do - POST payload:
publicDataDetailPk: "uddi:9a60503c-b31c-4879-9028-a4250f0f6998",paramtrSe: "2",oprtinSeqNo: 15920 - 응답 JSON 의
RESULT_RE_LIST를 순회(인덱스 8은 건너뜀)하며 파라미터명과 한글명을 추출 - 결과를
column.py파일로 써낸다 - import: gevent, base64, requests, BeautifulSoup, json
베낄 것: 공공데이터포털의 "요청/응답 파라미터 목록" 화면을 긁어 필드 사전을 코드로 자동 생성하는 발상. DMF API 의 영문 필드명(
DMF_PERMIT_NO등)을 한글 헤더로 바꿔야 하는 우리 리포트 요구와 정확히 맞물린다. →config/field_map.yaml을 손으로 쓰되, 생성 스크립트를scripts/gen_field_map.py로 둔다.
4.3 jjscan/data.go.kr-1 [F#46]
| 항목 | 값 |
|---|---|
| URL | https://github.com/jjscan/data.go.kr-1 |
| ★ / Fork | 0 / 0 |
| 언어 | R |
| 파일 | DURPrdlstInfoService.R, README.md |
| 대상 API | DURPrdlstInfoService / getUsjntTabooInfoList (병용금기정보조회) |
응답 구조 실측: numOfRows: 100, pageNo: 895(예), totalCount: 351010. XML 응답을 xmlSApply / xpathSApply 로 파싱 후 CSV 로 내보냄. HTTP 는 httr 패키지.
README 가 기록한 구현 난제 4가지(우리가 그대로 대비해야 할 항목):
- 연결 실패 — 폴링(재시도) 메커니즘으로 해결
- 널 데이터 처리 —
totalCount == 0검증으로 탐지 - R 세션 크래시 — 간헐적, 미해결
- 성능 병목 — 단일 스레드로 약 350,000건에 17시간. Rmpi 로 MPI 병렬화하여 해결
베낄 것: (1) 재시도 정책의 필요성, (2)
totalCount == 0을 "데이터 없음"의 공식 판정 기준으로 삼기, (3) 전체 백필은 반드시 병렬/증분으로 설계 — DMF 현황 전량 백필 시에도 동일한 함정이 있다.
4.4 WooilJeong/PublicDataReader [F#37]
| 항목 | 값 |
|---|---|
| URL | https://github.com/WooilJeong/PublicDataReader |
| 설명 | "공공 데이터 조회를 위한 오픈소스 파이썬 라이브러리" |
| ★ / Fork | 597 / 113 |
| 언어 | Python |
| 커밋 | 168 |
| 설치 | pip install PublicDataReader --upgrade |
지원 provider: FRED, 공공데이터포털(국토교통부 실거래가/건축물대장/건축인허가/주택인허가/토지임야/토지소유, 소상공인시장진흥공단 상권정보, 한국자산관리공사 공매물건, 국세청 사업자등록 확인, 한국부동산원), KOSIS, ECOS, 서울시 교통, V-World, KB부동산.
결정적 사실: 식약처/MFDS/의약품 커버리지가 없다(문서에 명시). 따라서 의존성으로 채택하지 않는다. 다만 "provider 별 모듈 + 공통 조회 인터페이스" 라는 패키지 레이아웃은 우리 fetch/ 의 소스별 어댑터 설계 참고가 된다.
4.5 NomaDamas/k-skill — docs/features/mfds-food-safety.md [F#36]
| 항목 | 값 |
|---|---|
| URL | https://github.com/NomaDamas/k-skill/blob/main/docs/features/mfds-food-safety.md |
| 부모 저장소 ★ | 7.4k |
| 문서 주제 | "식품 안전 체크 가이드" |
| 스크립트 경로 | scripts/mfds_food_safety.py |
| 참조 데이터 소스 | ① 공공데이터포털 "부적합 식품" 엔드포인트 ② 식품안전나라 회수·판매중지 API |
| API 키 처리 | DATA_GO_KR_API_KEY, FOODSAFETYKOREA_API_KEY 를 "프록시 운영 서버" 환경변수에 두고, 사용자는 k-skill-proxy 의 /v1/mfds/food-safety/search 로 접근 |
| 안전 규범 | "이 helper 는 직접 진단을 하지 않는다" — 자동 판정보다 전문가 상담 우선 |
베낄 것: ① 키를 사용자 머신 코드에 박지 않는 분리 원칙(우리는 로컬 단독이므로
.env+.gitignore) ② "자동화가 판정하지 않고 근거만 제시한다"는 안전 규범 — DMF 변경 탐지 결과도 "규제 판단"이 아니라 "차이 보고"로 문구를 통일한다.
4.6 이 범주에서 확인되지 않은 것 (⚠️ 미검증)
nedrug.mfds.go.kr를 WebFetch 로 열었을 때https://nedrug.mfds.go.kr/searchDmf는 에러 페이지("The requested page cannot be found")를 반환했다 [F#63]. DMF 검색 화면의 실제 URL·파라미터는 브라우저로 실측해야 한다.- data.go.kr 의 DMF OpenAPI 는 페이지 자체는 열렸다 [F#29]. §15 참조.
5. (b) FDA / openFDA / 규제 데이터
한국 DMF 에 직접 쓸 코드는 없지만, "규제 데이터셋을 주기적으로 받아 정규화하고 저장한다" 는 문제를 가장 오래 푼 집단이 여기다. 레이아웃과 파이프라인 분할을 여기서 가져온다.
5.1 FDA/openfda (공식) [F#2]
| 항목 | 값 |
|---|---|
| URL | https://github.com/FDA/openfda |
| 설명 | "openFDA is a research project to provide open APIs, raw data downloads, documentation and examples, and a developer community for an important collection of FDA public datasets." |
| ★ / Fork | 705 / 166 |
| 언어 | Python |
| 최상위 디렉터리 | api/faers, config, dependencies, openfda, schemas, scripts + Dockerfile, docker-compose.yml, requirements.txt, setup.py |
| 파이프라인 기술 | Luigi — "Python pipelines written with Luigi for processing public FDA data sets (drugs, foods, medical devices, and other) into a JSON format that can be loaded into Elasticsearch." |
| 데이터셋 | NSDE, CAERS, Substance Data, Device Clearance, Device PMA, Device Event 파이프라인 명시 |
| 실행 | docker-compose up → Elasticsearch + API 컨테이너(포트 8000). "the API container starts right away, it will not serve any data until some or all of the pipelines above have finished running." |
| 전제 조건 | Elasticsearch 7, Python 3.10, Node 16+ |
베낄 것
config/(설정) ·schemas/(데이터 계약) ·scripts/(운영 스크립트) ·<pkg>/(코드) 4분할. 우리 트리에 그대로 반영한다.schemas/를 별도 최상위로 두는 것 — 응답 스키마와 스냅샷 스키마를 코드와 분리해 버전 관리하면, 식약처가 필드를 바꿨을 때 diff 로 즉시 감지된다.- "데이터 적재 전에는 API 가 아무것도 서빙하지 않는다"는 명시 — 우리도 스냅샷이 2개 미만이면 리포트를 만들지 않고 "기준선 수립" 상태로 종료해야 한다.
기각할 것: Luigi / Elasticsearch / Docker. 단일 Windows PC 에 하루 1회 소량 데이터. 오버엔지니어링이다.
5.2 jbremz/FDA-Analysis [F#3]
| 항목 | 값 |
|---|---|
| URL | https://github.com/jbremz/FDA-Analysis |
| 설명 | "Scraping and analysis of the (now retired) Drugs@FDA site - with scrapy and pandas" |
| ★ / Fork | 5 / 2 |
| 언어 | Python |
| 파일 | FDA Spider/(Scrapy 프로젝트, 메인 스파이더는 FDASpider), masterDrugList2.csv(원시 산출물), FDA_Data_Analysis.py(pandas 분석), Drugs@FDA Analysis.ipynb(결론 노트북), .ipynb_checkpoints/ |
| 규모 | "over 22,000 different products" — British Medical Journal 의뢰로 데이터 품질·누락 보고 평가 |
베낄 것: "스파이더 / 원시 CSV / 분석 스크립트 / 결론 노트북" 4단 분리. 특히 원시 산출물을 저장소에 남겨 재현 가능하게 한 점. 우리는
data/raw/YYYY-MM-DD/에 원시 응답을 그대로 남긴다.경고 신호: 대상 사이트(Drugs@FDA 구 버전)가 은퇴하면서 이 프로젝트도 죽었다. 우리도 nedrug 게시판 구조 변경에 대비해 파서를 소스별로 격리하고, 파싱 실패를 즉시 알림으로 올려야 한다.
5.3 logiover/fda-data-scraper [F#4]
| 항목 | 값 |
|---|---|
| URL | https://github.com/logiover/fda-data-scraper |
| 설명 | "FDA data scraper — openFDA drug/device/food recalls, adverse events & 510(k) clearances as JSON/CSV" |
| ★ / Fork | 0 / 0 (master 1 commit) |
| 라이선스 | MIT |
| 파일 | /examples(CLI, API, JavaScript, Python 사용 예), .gitignore, LICENSE, README.md |
지원 9개 데이터셋: ① Drug recalls (enforcement) ② Drug adverse events (20M+ records) ③ Drug labels (258K records) ④ Device recalls ⑤ Device adverse events (24M+ records) ⑥ Device 510(k) clearances ⑦ Food recalls ⑧ Food adverse events ⑨ Animal & veterinary adverse events
출력 포맷: "JSON, CSV, Excel (XLSX), JSONL, XML or HTML"
자동화 기능: 일일 스케줄 실행, 완료 웹훅, Google Sheets/Excel 연동, 클라우드 스토리지 내보내기(S3, GCS), "Zapier, Make, n8n or Pipedream" 호환. 실행 경로는 Apify Console / Apify CLI / API(curl) / apify-client(JS·Python) 4가지.
베낄 것: 출력 포맷을 데이터 파이프라인이 아니라 최종 어댑터에서 스위칭하는 설계.
report모듈이 동일한DiffResult를 받아 xlsx/csv/json 중 하나로 렌더링한다.기각할 것: Apify 플랫폼 의존. 로컬 실행이 요구사항이다.
5.4 coderxio/OpenFDA [F#18]
| 항목 | 값 |
|---|---|
| URL | https://github.com/coderxio/OpenFDA |
| 설명 | "Python scripts for capturing OpenFDA data in a database." |
| ★ / Fork | 3 / 3 |
| 언어 | Python |
| 파일 | openfda/, .gitignore, README.md, docker-compose.override.yml, docker-compose.yml |
| 데이터 | drug NDC 데이터셋 (https://open.fda.gov/apis/drug/ndc/download/) 을 압축 해제 후 drug-ndc.json 으로 이름 바꿔 ./data/ 에 배치 |
| 실행 (venv) | venv 생성 → requirements 설치 → python app/load_data.py → python app/serve_data.py |
| 실행 (Docker) | "First run to load DB: docker-compose up --build" → 운영은 별도 compose 파일 → 정리는 docker-compose down -v |
| 서버 | CherryPy |
베낄 것:
load_data(적재) 와serve_data(제공) 의 엔트리포인트 분리, 그리고 "최초 실행"과 "이후 실행"이 다른 명령이라는 명시. 우리 CLI 에서dmf backfill과dmf daily로 구현한다. 또한 원시 다운로드물을./data/에 고정 파일명으로 두는 규칙.
5.5 Tanguy9862/AI-Powered-FDA-Drug-Scraper [F#42]
| 항목 | 값 |
|---|---|
| URL | https://github.com/Tanguy9862/AI-Powered-FDA-Drug-Scraper |
| 설명 | "Python-based web scraper leveraging generative AI with LangChain and GPT-4o-mini to extract and classify FDA drug approval data" |
| ★ / Fork | 3 / 0 |
| 언어 | Python |
| 파일 | new_drug_approvals_scraper/(패키지), img_readme/, scraper.py, classification.py, utils.py, __init__.py, requirements.txt, setup.py, .gitignore, LICENSE, README.md |
| 대상 | https://www.drugs.com/newdrugs.html |
| 규모 | "over 1,770 records" |
| 정규화 | 회사명 표기 변형을 접미사·약어·협업 서술 표준화로 약 1000종 → 700종으로 축소 |
| 자매 | https://github.com/Tanguy9862/new-drug-approvals-dashboard (Dash 대시보드) |
베낄 것
scraper.py/classification.py/utils.py분리 — 수집(결정론)과 LLM 분류(비결정론)를 절대 같은 파일에 두지 않는다. 우리 구조에서는fetch/(결정론) 와enrich/(agy 호출) 로 대응.- 업체명 정규화가 필수 작업이라는 실증. DMF
ENTP_NAME/MNFCTR_NAME도 "(주)"·"주식회사"·영문/한글 혼용 때문에 같은 문제를 겪는다.parse/normalize.py에 정규화 규칙 테이블을 둔다.
5.6 FDA DMF 목록 자체에 대해 확인된 것 / 확인 실패한 것
- FDA 는 DMF 목록을 분기별로 갱신한다. 검색 결과가 인용한 문장: "The list of DMFs, which is updated quarterly, contains DMFs received by June 30, 2026, for which acknowledgment letters were sent before July 19, 2026." [S#6]
- 컬럼(검색 결과 기준, ⚠️ 미검증): DMF 번호, submitter, file type, acknowledgment date, review division [S#27]
https://www.fda.gov/drugs/drug-master-files-dmfs/list-drug-master-files-dmfs와https://www.fda.gov/drugs/drug-master-files-dmfs는 둘 다 WebFetch 에서 HTTP 404 를 반환했다 [F#48][F#53]. 실제 xls/xlsx/zip 다운로드 URL 은 확인하지 못했다. ⚠️ 미검증.- 이 프로젝트는 한국 DMF 가 대상이므로 FDA DMF 는 범위 밖이다. 다만 "분기 갱신 스프레드시트를 받아 이전 분기와 비교" 라는 문제 형태는 우리와 동형이므로 참고로만 남긴다.
6. (c) 규제 변경 감지 · 인텔리전스
이 범주가 우리 프로젝트와 문제 정의가 가장 가깝다. 세 저장소가 각각 다른 축을 보여준다.
6.1 anton-semerenko/pharma-radar — 에이전트 파이프라인의 정본 [F#6]
| 항목 | 값 |
|---|---|
| URL | https://github.com/anton-semerenko/pharma-radar |
| 설명 | "Autonomous LLM agent delivering a daily, source-verified pharma regulatory & competitive intelligence briefing to Telegram." |
| ★ / Fork | 0 / 0 |
| 언어 | Python |
| 커밋 | main 브랜치 2 commits |
| 라이선스 | MIT |
디렉터리 구조 (원문 그대로):
├── prompts/system_prompt.md (agent methodology)
├── config/sources.yaml (source hierarchy & rubrics)
├── src/deliver.py (Telegraph + Telegram publishing)
└── examples/sample_digest.md (sample output)
감시 소스
- 규제기관: FDA, EMA, WHO, 우크라이나 당국(МОЗ, ДЕЦ, Держлікслужба)
- 업계지: Endpoints News, FiercePharma, STAT News
- 기업 IR 페이지 및 등록부
에이전트 로직 (원문 인용): Claude(Opus급)를 agentic loop 로 사용하여 "retrieves broadly, verifies every item against ≥2 independent sources or one official regulatory primary, attributes every number, and states what each development means."
검증 정책: 2개 독립 출처 또는 1개 공식 규제 1차 출처가 있어야 항목에 포함. 단일 2차 출처는 불충분으로 간주.
출력 구조: 4개 루브릭 — Regulatory & Approvals / Clinical & Pipeline / Market·Access & Ukraine / Competitive & Corporate — 에 Forward Agenda 와 Signals to Watch 섹션 추가. 각 항목은 4~7문장 + 출처 명시.
배포·스케줄: 롤링 Telegraph 페이지에 게시 + Telegram 푸시. 매일 06:00 스케줄드 태스크로 무인 실행. 배포 계층은 파이썬 표준 라이브러리만 사용.
베낄 것 (5가지, 전부 채택)
prompts/를 최상위 디렉터리로. 에이전트 프롬프트는 코드가 아니라 자산이다.prompts/dmf_summarize.md,prompts/dmf_classify.md로 분리하고 버전 관리한다.config/sources.yaml로 소스 계층·루브릭을 데이터로 선언. 소스를 추가할 때 코드를 고치지 않는다.- 배포(notify) 계층은 의존성 최소화. pharma-radar 는 표준 라이브러리만 썼다. 우리는 Apprise 하나만 허용하고 그 외 SDK 는 금지한다.
- 검증 정책을 문서로 명시. 우리 버전: "DMF 변경 건은 ① 공공 API 응답과 ② 공고 게시판 중 최소 1개의 공식 1차 출처로 뒷받침되어야 리포트에 '확정' 으로 표기한다. 한쪽만 있으면 '관찰 중(pending)' 으로 표기한다."
- 매일 06:00 + 무인 + 스케줄드 태스크 — 우리 요구사항과 동일한 운영 형태가 실제로 굴러가고 있다는 존재 증명.
주의: ★0 / 2 commits 로 성숙도는 매우 낮다. 구조만 참고하고 코드 재사용은 하지 않는다.
6.2 mrueda/nomenclator-delta — 모듈 경계의 정본 [F#30]
| 항목 | 값 |
|---|---|
| URL | https://github.com/mrueda/nomenclator-delta |
| 설명 | "Compare monthly changes in medicines and health products from the Ministerio de Sanidad's Nomenclátor de Facturación." |
| ★ / Fork | 1 / 0 |
| 언어 | Python |
| 최근 갱신 | Updated Aug 9, 2026 [F#19] |
| 라이선스 | MIT |
디렉터리 구조 (원문 그대로):
src/nomenclator_delta/ (collection, normalization, diffing, validation)
data/ (snapshots and change history)
site/ (Spanish static application)
docs-site/ (documentation and Pages build)
tests/ (unit and integration tests)
데이터 소스: 스페인 보건부 "Nomenclátor de facturación" — 국가보건시스템 의약품 공개 DB
델타 계산: "compares consecutive monthly releases" 하고 "finds changes by Código Nacional, medicine name, active ingredient, or laboratory." 즉 국가코드(고유 키) + 3개 부가 축으로 변경을 찾는다. added/removed/changed 를 식별한다고 README 가 기술.
출력: 백엔드 없는 정적 브라우저 앱 (GitHub Pages 호스팅)
운영: "a monthly update runbook" 문서가 존재 — 유지보수자가 정기 릴리스를 다루는 절차서
CLI:
python3 -m nomenclator_delta validate data
python3 -m nomenclator_delta dist
베낄 것 (이 프로젝트의 골격)
collection / normalization / diffing / validation4단 모듈 분리를 그대로 채택. 우리 명칭으로는fetch / parse(normalize) / diff / validate.data/에 "스냅샷"과 "변경 이력"을 함께 둔다. 스냅샷은 시점의 진실, 변경 이력은 시점 간의 진실. 둘 다 보존해야 재계산이 가능하다.validate를 별도 CLI 서브커맨드로. 파이프라인을 돌리기 전에 데이터 무결성을 먼저 검사한다. 우리는dmf validate data로 동일하게 만든다.docs-site/(문서) 와site/(산출물 뷰어) 를 분리. 우리는 산출물이 xlsx 이므로site/대신reports/를 둔다.- runbook 문서를 리포지토리 안에.
docs/ops/runbook.md로 채택.python3 -m <pkg> <subcommand>형태의 진입점. Windows 에서도python -m dmf_crawler daily가 배치 파일보다 견고하다.
6.3 Mzands2622/Zanalytix — LLM 기반 변경 감지의 실물 [F#31]
| 항목 | 값 |
|---|---|
| URL | https://github.com/Mzands2622/Zanalytix |
| 설명 | "AI-powered pipeline for scraping and tracking pharmaceutical clinical pipeline data with LLM-based change detection." |
| ★ / Fork | 0 / 0 |
| 언어 | Python |
| 커밋 | main 브랜치 1 commit / Updated Mar 9, 2026 |
파일 구조: 60+ 개의 회사별 파서 모듈(abbvie_pipeline.py, pfizer_pipeline.py …) + 코어:
db.py(DB 관리)function_app.py,master_scheduler.py(API 계층 / 스케줄러)login.py,sign_up.py(인증)notifications.py(알림)cleanup_text.py,treatment_visualizer.py(유틸)
수집 흐름 (원문): "HTML Scraping (Zyte API) --> 60+ Company Parsers (BeautifulSoup)" → 표준화 파이프라인. 각 회사 모듈은 두 함수를 반드시 구현:
fetch_{company}_html()— 파이프라인 페이지 획득process_{company}_html()— 구조화 객체로 파싱
LLM 변경 감지 (원문): "GPT-4o compares old vs new treatment data, assigns priority (1-5), categorizes changes" — 비교 큐를 통해 수행. 날짜별 스냅샷을 JSON 으로 유지해 시계열 분석 가능.
저장 구조: Revised_MasterTable 이 treatment 를 키로, Treatment_Data 에 타임스탬프 스냅샷을 담는다. 별도 Stream 테이블이 변경 이벤트별 GPT-4o 응답과 메타데이터를 추적.
스케줄·알림: "Calendar-based scraping schedules with recurrence rules and auto-extension" 이 Azure Functions 를 트리거. 매칭된 변경은 사용자 저장 선호에 따라 "Twilio SMS/Calls, Email" 로 라우팅.
베낄 것 (3가지)
- 소스별 파서 모듈의 함수 시그니처를 강제 통일 (
fetch_*/process_*). 우리는fetch(source_id) -> RawPayload,parse(RawPayload) -> list[DmfRecord]프로토콜로 명문화한다.- 변경 이벤트에 우선순위 점수(1~5)를 부여하고, 그 점수로 알림 대상/채널을 결정. DMF 에서는 예: 신규 등록=3, 등록 취하=5, 제조소 주소 변경=2, 오탈자 수정=1.
- 원본 스냅샷 테이블과 "변경 이벤트 + LLM 응답" 테이블을 분리. 우리 SQLite 스키마에
snapshots/changes/agent_runs3테이블로 반영.기각할 것: Azure Functions, Zyte API, Twilio, 로그인/회원가입. 로컬 단일 사용자에겐 전부 불필요.
6.4 suriyadeepan/WebScraping-for-Healthcare [F#22]
| 항목 | 값 |
|---|---|
| URL | https://github.com/suriyadeepan/WebScraping-for-Healthcare |
| 설명 | "Scraping the internet for extracting healthcare and pharma data." |
| ★ / Fork | 8 / 1 |
| 언어 | Python |
| 커밋 | main 브랜치 44 commits |
| 라이선스 | GPL-3.0 |
구조:
├── data/
├── mhra/
├── notebooks/
├── phscrape/
├── twitter/
├── .gitignore
├── LICENSE (GPL-3.0)
├── README.md
├── requirements.txt
└── tests.py
수집 대상: Drug Bank, ClinicalTrials.gov, COVID-19 API, Twitter(#remdesivir), EMC(Electronic Medicines Compendium — SMPC/PIL 추출), HPRA(아일랜드), MHRA(영국).
phscrape 모듈이 노출하는 함수: drugbank.fetch(), clinicaltrials.fetch(), emc.crawl_k(), hpra.crawl_k().
베낄 것: 소스별 서브모듈이 동일 인터페이스(
fetch()/crawl_k())를 노출하는 규약. 위 Zanalytix 와 동일한 결론에 독립적으로 도달했다는 점이 이 패턴의 타당성을 보강한다.주의: GPL-3.0 이므로 코드를 복사하면 안 된다. 설계만 참고.
6.5 상업 도구 및 업계 동향 (배경, 코드 없음)
- Clarivate 의 Biopharma Regulatory Compliance 서비스 [S#4]
- Vistaar 의 Regulatory Intelligence Database 소개 [S#4]
- IntuitionLabs: "AI and the Future of Regulatory Affairs in the U.S. Pharmaceutical Industry", "Open Source Pharma: Tools & Trends in Drug Development" [S#4]
- 2026년 1월, FDA 와 EMA 가 공동으로 "Guiding Principles for Good AI Practice in Drug Development" 를 발표 — 규제 당국이 새로운 도구 패러다임에 개방적임을 시사 [S#4]. ⚠️ 미검증(검색 요약문 기준)
7. (d) 범용 변경 감지 도구
7.0 이 범주에 대한 총평 — 도입이 아니라 설계 차용
| 도구 | 감지 단위 | 우리 요구와의 불일치 |
|---|---|---|
| changedetection.io | 페이지 텍스트 블록 | DMF 는 레코드 집합이다. "이 페이지의 3줄이 바뀜"이 아니라 "등록번호 XXX 가 신규/변경/취하"를 알아야 한다 |
| urlwatch | URL·명령 출력의 unified diff | 위와 동일. 또한 xlsx 리포트 생성 기능이 없다 |
| huginn | 이벤트 그래프 | Ruby/Rails + DB. Windows 단일 PC 에 얹기엔 과중 |
| csvdiff | 레코드 키 기반 added/removed/changed | ✅ 맞음. 단 2021 아카이브 |
결정: 우리 diff 모듈은 csvdiff 의 출력 스키마를 채택하고, 필터/알림/스케줄 개념은 urlwatch·changedetection.io 에서 가져오되, 도구 자체는 도입하지 않는다.
7.1 dgtlmoon/changedetection.io [F#7][F#55][F#61]
| 항목 | 값 |
|---|---|
| URL | https://github.com/dgtlmoon/changedetection.io |
| 설명 | "Best and simplest tool for website change detection, web page monitoring, and website change alerts. Perfect for tracking content changes, price drops, restock alerts, and website defacement monitoring—all for free or enjoy our SaaS plan!" |
| ★ / Fork | 33.5k / 2.0k |
| 언어 | Python |
| 커밋 | master 2,448 commits |
| 최신 릴리스 | 0.55.8 (13 Jul 09:26) |
콘텐츠 필터·감지
- CSS Selectors, XPath (1.0 & 2.0)
- JSONPath, jq 필터 (API 모니터링용)
- HTML 페이지 내 embedded JSON 추출
- Visual Selector 도구
모니터링
- word / line / character 레벨 diff 시각화
- 인터랙티브 브라우저 스텝 (로그인, 폼 채우기, 버튼 클릭)
- PDF 변경 추적
- 커스터마이즈 가능한 체크 간격
알림: Apprise 통합 — Discord, Email, Slack, Telegram, webhooks 등 90+ 서비스, Jinja2 템플릿
스케줄: 타임존 인식, 요일·시간 제한 지원
고급: LiteLLM 연동 AI 변경 감지·요약, REST API, Chrome 확장, 프록시(Bright Data 포함)
설치
# Docker Compose
docker compose up -d
# Docker standalone
docker run -d --restart always -p "127.0.0.1:5000:5000" -v datastore-volume:/datastore --name changedetection.io dgtlmoon/changedetection.io
# pip
pip3 install changedetection.io
changedetection.io -d /path/to/empty/data/dir -p 5000
접속: http://127.0.0.1:5000
REST API (x-api-key 헤더) [F#27] — Settings > API 에서 키 발급
# 감시 목록
curl -X GET "http://localhost:5000/api/v1/watch" \
-H "x-api-key: YOUR_API_KEY"
# 감시 생성
curl -X POST "http://localhost:5000/api/v1/watch" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"title": "My Monitor",
"time_between_check": {"hours": 1}
}'
# 단건 조회
curl -X GET "http://localhost:5000/api/v1/watch/{uuid}" \
-H "x-api-key: YOUR_API_KEY"
# 수정
curl -X PUT "http://localhost:5000/api/v1/watch/{uuid}" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"notification_muted": true}'
# 삭제
curl -X DELETE "http://localhost:5000/api/v1/watch/{uuid}" \
-H "x-api-key: YOUR_API_KEY"
# 이력
curl -X GET "http://localhost:5000/api/v1/watch/{uuid}/history" \
-H "x-api-key: YOUR_API_KEY"
# 최신 스냅샷
curl -X GET "http://localhost:5000/api/v1/watch/{uuid}/history/latest" \
-H "x-api-key: YOUR_API_KEY"
# 이전↔최신 비교
curl -X GET "http://localhost:5000/api/v1/watch/{uuid}/difference/previous/latest?format=htmlcolor" \
-H "x-api-key: YOUR_API_KEY"
# 전역 알림 URL 등록
curl -X POST "http://localhost:5000/api/v1/notifications" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"notification_urls": ["mailto:admin@example.com"]}'
추가 동작: 강제 재확인은 GET watch 엔드포인트에 ?recheck=1, 일시정지/음소거는 ?paused=paused / ?muted=muted. 연결 URL 형식은 로컬 http://localhost:5000/api/v1/, 호스티드 https://<your-domain>/api/v1/.
알림 템플릿 토큰 [F#55] (정확히 인용):
{{base_url}}
{{current_snapshot}}
{{diff}}
{{diff_full}}
{{diff_added}}
{{diff_removed}}
{{watch_url}}
{{triggered_text}}
예시 본문:
{
'myKey': 1234,
'url': '{{watch_url|tojson}}'
}
주의: {{current_snapshot}}, {{diff}}, {{diff_full}} 은 길어서 서비스 길이 제한(Discord 2,000자)을 넘길 수 있다. "your notification body contains at least something" 을 보장할 것.
베낄 것
{{diff_added}}/{{diff_removed}}로 알림 본문을 조립하는 토큰 설계 → 우리 notify 모듈의 템플릿 변수명을 이것과 동일하게 맞춘다(added,removed,changed,report_path,run_date).- 알림 본문 길이 상한을 서비스별로 강제하는 규칙. Apprise
windows://는 250자 제한이므로 토스트에는 요약 카운트만, 상세는 xlsx 링크로.- 타임존 인식 + 요일/시간 제한 스케줄 개념. 우리는 매일 06:00 고정이지만, 공휴일 스킵 옵션을
config/schedule.yaml로 열어둔다.- 강제 재확인(
?recheck=1) 에 대응하는 수동 실행 커맨드dmf daily --force.
7.2 thp/urlwatch [F#5][F#26][F#50][F#60]
| 항목 | 값 |
|---|---|
| URL | https://github.com/thp/urlwatch |
| 설명 | "Watch (parts of) webpages and get notified when something changes via e-mail, on your phone or via other means. Highly configurable." |
| ★ / Fork | 3.1k / 354 |
| 언어 | Python |
| 커밋 | master 974 commits |
| 릴리스 | "There aren't any releases here" — 공식 릴리스 없음 [F#60] |
| 문서 | https://urlwatch.readthedocs.io/ |
| 홈페이지 | https://thp.io/2008/urlwatch/ |
| 토픽 | automation, monitor, python, webpage |
URL 잡 형식 [F#26]:
name: "urlwatch homepage"
url: "https://thp.io/2008/urlwatch/"
URL 잡 옵션: url(필수), name, method(기본 GET), data(POST/PUT 페이로드), headers, cookies, encoding, filter, ignore_connection_errors.
셸 잡 형식:
name: "What is in my Home Directory?"
command: "ls -al ~"
설정 저장/실행: 잡은 urls.yaml 에 저장하고 urlwatch --edit 로 편집. urlwatch --list 로 인덱스 번호와 함께 목록 표시. 각 잡은 --- 만 있는 줄로 구분. 메인 설정 파일에 job_defaults 섹션을 두어 전 잡에 공통 설정 적용 가능(키 반복 제거).
내장 필터 28종 [F#50] (정확히 인용):
beautify, css, csv2text, element-by-class, element-by-id, element-by-style,
element-by-tag, format-json, grep, grepi, hexdump, html2text, pdf2text,
pretty-xml, ical2text, ocr, re.sub, re.findall, reverse, sha1sum, shellpipe,
sort, remove-duplicate-lines, strip, striplines, xpath, jq
필터 체인 예시:
url: https://example.net/css.html
filter:
- css: ul#groceries > li.unchecked
- html2text
diff 관련: diff_filter 는 "applied to the diff result before reporting the changes" 이며, --test-diff-filter 로 캐시된 과거 데이터로 테스트 가능. diff_tool 은 Filters 페이지에 문서화되어 있지 않음(⚠️ 미검증 — Configuration 페이지 확인 필요).
베낄 것
config/sources.yaml의 스키마를 urlwatch 잡 스키마에 맞춘다:name,url,method,headers,params,encoding,ignore_connection_errors,filter(체인).job_defaults상속 개념 — 타임아웃·User-Agent·재시도 횟수를 소스마다 반복하지 않는다.- 필터를 이름 붙은 체인으로 선언하고 코드가 아니라 설정으로 조합. 우리는
strip,sort,re.sub,jq정도만 있으면 충분하다.ignore_connection_errors— nedrug 게시판이 일시 장애일 때 파이프라인 전체를 죽이지 않는 플래그. 채택.--test-diff-filter처럼 과거 캐시로 diff 로직을 테스트하는 서브커맨드 →dmf diff --replay 2026-09-01 2026-09-02.경고: 릴리스가 없는 저장소다. 의존성으로 채택하지 않는다.
7.3 huginn/huginn [F#8][F#65][F#68]
| 항목 | 값 |
|---|---|
| URL | https://github.com/huginn/huginn |
| 설명 | "Create agents that monitor and act on your behalf. Your agents are standing by!" |
| ★ / Fork | 49.9k / 4.3k |
| 언어 | Ruby (Rails) |
| 커밋 | master 4,134 commits |
| 라이선스 | MIT |
에이전트 타입(확인된 것): WeatherAgent, WebsiteAgent, EmailAgent, TwitterAgent, 그리고 HipChat / FTP / IMAP / Jabber / JIRA / MQTT 커넥터, JavaScript 실행 에이전트, 위치 추적.
동작 원리: "Huginn's Agents create and consume events, propagating them along a directed graph." "send digest email with things that you care about at specific times during the day"
WebsiteAgent 실측 [F#68]:
- 정의: "The Website Agent scrapes a website, XML document, or JSON feed and creates Events based on the results."
- mode 옵션 3종:
all— 모든 추출 결과에 대해 이벤트 생성on_change— 이전 결과와 다를 때만 이벤트 생성merge— 기존 페이로드를 유지하며 새 값으로 갱신
- 기본 스케줄:
every_12h - extract 설정 예시:
"extract": {
"url": { "css": "#comic img", "value": "@src" },
"title": { "css": "#comic img", "value": "@alt" },
"hovertext": { "css": "#comic img", "value": "@title" }
}
CSS selector, XPath, JSON path, regex 를 문서 타입에 따라 지원.
주의: Huginn wiki 의 Agent-Types 페이지는 로딩 오류로 ChangeDetectorAgent, DeDuplicationAgent, DigestAgent, EmailDigestAgent, SchedulerAgent, ShellCommandAgent 의 설명을 확보하지 못했다 [F#65]. ⚠️ 미검증.
베낄 것
mode: all / on_change / merge3분류를 diff 모듈의 출력 모드로 그대로 채택.
all= 전체 스냅샷 시트on_change= 변경 건만 담은 시트 (일일 리포트의 메인)merge= 마스터 테이블 갱신 (누적 현황 시트)extract를 선언적 데이터로 표현하는 방식 — 게시판 파싱 규칙을config/sources.yaml안에{"필드": {"css": "...", "value": "..."}}형태로 넣는다.기각: Ruby/Rails 스택 전체.
7.4 larsyencken/csvdiff — diff 출력 스키마의 정본 [F#32]
| 항목 | 값 |
|---|---|
| URL | https://github.com/larsyencken/csvdiff |
| 설명 | "Generate a diff between two tabular datasets expressed in CSV files." |
| ★ / Fork | 131 / 31 |
| 언어 | Python |
| 상태 | 2021년 2월 18일 아카이브 (read-only, 유지보수 종료) |
| 라이선스 | BSD-3-Clause |
CLI:
csvdiff --style=summary KEY file1.csv file2.csv
csvdiff --style=summary id a.csv b.csv
옵션: --style(summary, pretty), --output(JSON 출력 파일), --ignore-columns(제외 컬럼 콤마 목록), --significance(수치 비교 정밀도, 음수는 자릿수)
JSON 출력 구조 (이것을 채택한다):
_index— 키 컬럼 배열added— 새 행 전체 필드removed— 삭제된 행 전체 필드changed— 변경 행. 키별로 필드 레벨 "from/to"
Python API:
import csvdiff
patch = csvdiff.diff_files('a.csv', 'b.csv', ['id'])
patch = csvdiff.diff_records(records_a, records_b, ['id'])
patch 적용 메서드도 제공.
채택 결정: JSON 스키마와 CLI 옵션 이름은 채택, 라이브러리는 미채택(아카이브). 우리
diff/모듈이 동일 구조를 만들어낸다:{ "_index": ["DMF_PERMIT_NO"], "added": [ { "DMF_PERMIT_NO": "...", "INGR_KOR_NAME": "...", ... } ], "removed": [ { "DMF_PERMIT_NO": "...", ... } ], "changed": { "20250001": { "MNFCTR_PLACE": { "from": "구주소", "to": "신주소" } } } }
--ignore-columns는 필수다 — 조회수·수집시각 같은 노이즈 필드를 diff 에서 빼야 오탐이 사라진다.
7.5 ecprice/newsdiffs [F#43]
| 항목 | 값 |
|---|---|
| URL | https://github.com/ecprice/newsdiffs |
| 설명 | "A website and framework that tracks changes in online news articles over time." |
| ★ / Fork | 506 / 136 |
| 언어 | Python (Django) |
아키텍처:
- 스냅샷 저장: 기사별 git 저장소가 아니라 디렉터리 기반. 셋업 시 생성되는
articles디렉터리에 저장 - diff 생성: 스크래퍼가 주기적으로 실행되어 버전을 캡처하고 스냅샷 간 변경 탐지
- 스케줄링: cron 또는 루프
while true; do python website/manage.py scraper; sleep 60m; done - 파서 프레임워크:
parsers/디렉터리에 모듈식,BaseParser를 상속한 사이트별 서브클래스 - 로깅: 진행 로그
/tmp/newsdiffs_logging(per-run), 에러 로그/tmp/newsdiffs/logging_errs(cumulative)
베낄 것: per-run 로그와 누적 에러 로그의 분리. 우리는
logs/runs/YYYY-MM-DD.log(실행별) 와logs/errors.log(누적)로 구현한다. 워치독이 감시할 대상은 후자다. 또한BaseParser상속 구조 — 소스가 늘어날 때 파서만 추가하면 되는 형태.
7.6 simonw/git-scraper-template [F#47]
| 항목 | 값 |
|---|---|
| URL | https://github.com/simonw/git-scraper-template |
| 설명 | "Template repository for setting up a new Git scraper using GitHub Actions." |
| ★ / Fork | 132 / 10 |
동작: GitHub Actions 가 ./download.sh(curl) 로 URL 내용을 받아 변경이 있으면 저장소에 커밋. 기본 스케줄 24시간마다. 파일: .github/workflows/scrape.yml, README.md, download.sh, scrape.sh(템플릿 생성 시 만들어짐). 파이썬 스크래퍼는 scrape.yml 의 주석 블록을 풀고 requirements.txt 를 두면 지원.
관련 개념 [S#24]: "Git scrapers can grab data periodically, commit it to a repository if changed, creating a commit log of changes to information over time." Simon Willison 의 git-history 도구가 이렇게 수집한 데이터를 분석하는 데 쓰인다.
베낄 것:
data/snapshots/를 로컬 git 리포지토리로 만들어 매일 커밋한다. 이러면 (1) 변경 이력이 공짜로 생기고 (2)git diff로 언제든 재검증 가능하며 (3) 스냅샷 파일이 무한히 쌓이지 않는다. 원격 push 는 하지 않는다(사내 데이터).
8. (e) 크롤링 → 엑셀/시트 리포트 파이프라인
8.1 Bwhiz/Auto-Excel-Reports [F#16]
| 항목 | 값 |
|---|---|
| URL | https://github.com/Bwhiz/Auto-Excel-Reports |
| 설명 | "Scripts and workflows to automate the generation and distribution of Excel reports using Python's openpyxl library" + GitHub Actions 로 "scheduled and event-triggered report generation and email distribution" |
| ★ / Fork | 1 / 0 |
| 언어 | Python |
| 파일 | .github/workflows/, assets/, report_script.py, auto_mail.py, requirements.txt, .gitignore, README.md |
핵심:
- 리포트 생성: openpyxl 로 워크북 생성·서식
- 스케줄: GitHub Actions cron — 예시 워크플로가
schedule: - cron: '0 0 * * *'(매일 실행) - 배포:
auto_mail.py가 SMTP 로 발송, 발신자 자격증명·수신자 주소는 환경변수 - 보안: GitHub Secrets 로 "securely handle sensitive information like email credentials"
베낄 것:
report_script.py(생성)와auto_mail.py(배포) 파일 분리 — 리포트를 만들 수 있는데 배포에서 실패하는 경우와, 애초에 리포트를 못 만드는 경우를 로그에서 구분할 수 있어야 한다. 우리는report/와notify/로 모듈 분리. 기각: GitHub Actions(로컬 실행 요구), SMTP(1차 채널은 Windows 토스트).
8.2 XlsxWriter — 탭 연동 리포트의 실제 API [F#58]
우리 요구사항 "탭(시트)별로 연동된 보기 좋은 xlsx" 를 만족시키는 정확한 API 는 다음과 같다. (출처: https://xlsxwriter.readthedocs.io/worksheet.html)
내부 하이퍼링크 — write_url()
write_url(row, col, url[, cell_format[, string[, tip]]])
# 현재 워크시트의 셀로 링크
worksheet.write_url('A1', 'internal:Sheet2!A1')
# 다른 워크시트의 셀로 링크
worksheet.write_url('A2', 'internal:Sheet2!A1:B2')
# 시트명에 공백이 있으면 작은따옴표로 감싼다
worksheet.write_url('A3', "internal:'Sales Data'!A1")
표 — add_table()
add_table(first_row, first_col, last_row, last_col, options)
worksheet.add_table('B3:F7', { ... })
# 또는 행-열 표기
worksheet.add_table(2, 1, 6, 5, { ... })
컬럼 너비 — set_column()
set_column(first_col, last_col, width, cell_format, options)
worksheet.set_column(0, 0, 20) # A열 너비 20
worksheet.set_column(1, 3, 30) # B-D열 너비 30
worksheet.set_column('E:E', 20) # E열 너비 20
worksheet.set_column('F:H', 30) # F-H열 너비 30
조건부 서식 — conditional_format()
conditional_format(first_row, first_col, last_row, last_col, options)
worksheet.conditional_format('B3:K12', {'type': 'cell',
'criteria': '>=',
'value': 50,
'format': format1})
확인 실패: autofilter() 와 freeze_panes() 는 위 fetch 에서 문서 내용을 확보하지 못했다. ⚠️ 미검증 — 다만 XlsxWriter 에 두 메서드가 존재한다는 것은 널리 알려져 있으므로 구현 시 문서 재확인 필요.
채택: 리포트 엔진은 XlsxWriter. 근거는 위 4개 API 가 한 라이브러리에서 나오고, 특히
write_url('internal:...')이 "탭 간 연동"의 유일한 정공법이기 때문이다. 리포트 시트 설계(초안)
시트명 내용 연동 요약실행일자, 신규/변경/취하 건수, 각 카운트 셀이 해당 시트로 internal 링크 → 신규/변경/취하 신규added레코드.add_table+autofilter각 행의 등록번호 → 전체현황해당 행변경changed레코드. from/to 2열 병기, 조건부 서식으로 변경 필드 강조동일 취하removed레코드동일 전체현황최신 스냅샷 전량(merge 모드) — 실행로그소스별 HTTP 상태, 건수, 소요시간, 에러 —
8.3 HasData/playwright-scraping [F#41]
| 항목 | 값 |
|---|---|
| URL | https://github.com/HasData/playwright-scraping |
| 설명 | "Web scraping and browser automation using Playwright in both Python and Node.js. It includes scripts for common tasks such as scraping data, interacting with web elements, handling authentication, and managing errors." |
| ★ / Fork | 15 / 4 |
| 커밋 | main 5 commits |
Python/ 과 NodeJS/ 가 동일한 하위 구조:
basics/ (브라우저 실행, headless 모드, 멀티 탭)
scraping/ (텍스트, 링크, 이미지, Shadow DOM, 대기)
selectors/ (CSS, XPath, role 기반, text 기반)
interactions/ (클릭, 폼, 드롭다운, 페이지네이션, 스크롤)
save_data/ (JSON, CSV, PDF, 다운로드, 스크린샷)
auth/ (basic auth, 쿠키)
browser/ (user agent, 프록시, 디바이스 에뮬레이션)
errors/ (재시도 로직, 타임아웃 처리)
debug/ (video/trace 녹화, 일시정지, 콘솔 검사)
시연 대상: Amazon, WooCommerce 상품 데이터, 요소 선택, 폼 상호작용, 페이지네이션, 무한 스크롤.
베낄 것: 이 디렉터리 목록을 fetch 모듈의 요구사항 체크리스트로 사용한다. 특히
errors/— 재시도·타임아웃은 처음부터 설계에 넣는다debug/— 실패 시 trace/스크린샷 자동 저장. nedrug 게시판 구조가 바뀌었을 때 원인 파악의 유일한 단서가 된다.data/debug/YYYY-MM-DD/에 저장.browser/— User-Agent 설정. 공공기관 사이트는 기본 UA 를 차단하는 경우가 있다.의사결정: DMF API 호출은
requests로 충분하다. Playwright 는 nedrug 게시판(/bbs/117) 파싱에만 조건부로 도입한다(정적 HTML 이면 requests + BeautifulSoup 로 끝낸다). ⚠️ 미검증 — 게시판이 JS 렌더링인지 실측 필요.
8.4 검색 결과에만 있는 파이프라인 예시들 [S#7][S#17][S#29]
| 프로젝트 | 요점 |
|---|---|
god233012yamil/Excel-Automation-Using-Python |
openpyxl 로 엑셀 읽기/쓰기/수정 예제 모음 |
prabudevarajan/Task-Reminder-Automation-... |
pandas + openpyxl + schedule 라이브러리. Excel/CSV 에 태스크 저장, 15/7/3/1일 전 이메일, daily scheduler + 로깅 |
ManiMozaffar/linkedIn-scraper |
Playwright 봇 + FastAPI. 결과를 DB 와 Telegram 채널에 저장 |
dineshk-qa/playwright.slack.reporter |
Playwright 결과(통과/실패/flaky 수)를 Slack 웹훅으로 |
nmpowell gist |
기존 Windows Task Scheduler 태스크와 상호작용하는 파이썬 스크립트 |
schtasks 로 일일 태스크를 만드는 표준 구문 [S#29]:
schtasks /create /tn "Task Name" /tr path_to_bat_file/run.bat /sc DAILY /st 16:00
일반적 접근: 파이썬을 호출하는 배치 파일을 만들고 그것을 Task Scheduler 에 등록.
채택: 우리 06:00 실행 등록 명령의 기본형은 다음과 같다(⚠️ 실측 필요 — 계정/권한/
/ru/rl옵션).schtasks /create /tn "DMF_Crawler_Daily" /tr "D:\workspace\DMF_Crawler\ops\tasks\run_daily.cmd" /sc DAILY /st 06:00 /rl HIGHEST /f
8.5 대안 경로: 786raees/task-scheduler-python [F#45]
| 항목 | 값 |
|---|---|
| URL | https://github.com/786raees/task-scheduler-python |
| 설명 | "The Task Scheduler Python project provides a convenient way to interact with the Windows Task Scheduler using Python and the win32com.client library." |
| ★ / Fork | 2 / 0 |
| 언어 | Python |
| 커밋 | main 2 commits |
| 파일 | .vscode/, app/, .gitignore, LICENSE(MIT), README.md, main.py, requirements.txt |
| 의존성 | pip install pywin32 |
TaskScheduler 클래스 메서드:
create_task()— 실행 경로, 인자, ISO 8601 트리거 시각get_all_tasks()— 태스크 열거toggle_task()— 활성/비활성 토글run_task()— 즉시 실행delete_task()— 삭제
부분채택: 설치/제거 스크립트에서
schtasks문자열을 조립하는 대신 COM 으로 다루면 태스크 존재 여부 확인·재등록·상태 조회가 훨씬 안정적이다. 우리ops/tasks/install_tasks.py의 구현 방식으로 이 API 형태를 참고한다. 단 코드 복사는 하지 않는다(★2, 2 commits).
9. (f) AI CLI headless 자동화
중요한 전제 정정: 이 프로젝트가 실제로 사용할 CLI 는 Google Antigravity CLI (
agy) 이며 headless 모드는agy -p다. 그러나 raw dump 는agy를 조사하지 않았다 —claude -p,gemini -p,codex exec세 가지만 조사되었다. 따라서 아래 내용은 "agy 에 사상해야 할 공통 규약"으로 읽어야 하며,agy의 실제 플래그는 전부 ⚠️ 미검증이다.
9.1 claude -p (Claude Code headless) — 가장 상세히 문서화된 레퍼런스 [F#25]
출처: https://code.claude.com/docs/en/headless
기본 사용
claude -p "Find and fix the bug in auth.py" --allowedTools "Read,Edit,Bash"
claude -p "What does the auth module do?"
-p(=--print) 로 비대화형 실행- 종료 코드: 성공 0, 실패 시 0 이 아닌 값. 잘못된 플래그는 실행 전 stderr 로 보고. 실행 중 발생한 실패(예: 인증 누락)는 결과로 stdout 에 출력
-p와 자주 조합:--continue,--allowedTools,--output-format--bg는 거부됨.--cloud+ 태스크 설명도 거부.--cloud+ 세션 ID +-p는 클라우드 세션에 메시지를 큐잉하고 종료
--bare 모드 (CI/스크립트 권장)
claude --bare -p "Summarize README.md" --allowedTools "Read"
- hooks, skills, custom commands, subagents, plugins, MCP servers, auto memory, CLAUDE.md 자동 탐색을 건너뛴다 → 모든 머신에서 동일 결과
- bare 모드에서는 OAuth 자격증명/시스템 키체인을 읽지 않는다.
ANTHROPIC_API_KEY를 환경변수로 설정하거나--settingsJSON 에apiKeyHelper제공 - bare 모드 기본 도구: Bash, file read, file edit
- 컨텍스트 주입 플래그:
| 로드할 것 | 플래그 |
|---|---|
| 시스템 프롬프트 추가 | --append-system-prompt, --append-system-prompt-file |
| 설정 | --settings <file-or-json> |
| MCP 서버 | --mcp-config <file-or-json> |
| 커스텀 에이전트 | --agents <json> |
| 플러그인 | --plugin-dir <path>, --plugin-url <url> |
- 문서 주석: "
--bareis the recommended mode for scripted and SDK calls, and will become the default for-pin a future release." - 경고:
--bare없이는 신뢰하지 않은 폴더에서도 프로젝트.claude/settings.json의 hooks 를 실행하고.mcp.json의 서버에 연결한다.-p세션은 워크스페이스 신뢰 대화상자도, 서버별 승인 프롬프트도 표시하지 않는다.
stdin 파이프
cat build-error.txt | claude -p 'concisely explain the root cause of this build error' > output.txt
- 파이프 stdin 상한 10MB. 초과 시 명확한 에러와 함께 비정상 종료. 더 큰 입력은 파일로 쓰고 경로를 프롬프트에 참조
- stdin 을 읽을 수 없으면 stderr 에 경고 후 커맨드라인 프롬프트로 계속. v2.1.211 이전에는 Windows 에서 읽을 수 없는 stdin 이 세션을 크래시시키거나 출력 없이 조용히 종료시켰다
빌드 스크립트 통합 예 (Windows 이식성 고려한 이스케이프)
{
"scripts": {
"lint:claude": "git diff main | claude -p \"you are a typo linter. for each typo in this diff, report filename:line on one line and the issue on the next. return nothing else.\""
}
}
구조화 출력 — 이 프로젝트의 핵심 규약
--output-format값:text(기본),json(result·session ID·metadata 포함),stream-json(개행 구분 JSON 스트리밍)
claude -p "Summarize this project" --output-format json
claude -p "Extract the main function names from auth.py" \
--output-format json \
--json-schema '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}'
- 구조화 결과는 응답의
structured_output필드에 담긴다. 텍스트 결과는result필드 - 스키마가 유효하지 않으면
Error: --json-schema is not a valid JSON Schema+ 검증기 진단 출력.format키워드(예:"format": "email")는 허용되지만 주석으로만 취급하고 강제하지 않는다. v2.1.205 이전에는 잘못된 스키마를 조용히 무시하고 비구조화 텍스트를 반환했다 --output-format json사용 시 응답 페이로드에total_cost_usd와 모델별 비용 내역 포함(클라이언트 측 추정치)
jq 로 파싱:
# 텍스트 결과 추출
claude -p "Summarize this project" --output-format json | jq -r '.result'
# 구조화 출력 추출
claude -p "Extract function names from auth.py" \
--output-format json \
--json-schema '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}' \
| jq '.structured_output'
스트리밍
claude -p "Explain recursion" --output-format stream-json --verbose --include-partial-messages
claude -p "Write a poem" --output-format stream-json --verbose --include-partial-messages | \
jq -rj 'select(.type == "stream_event" and .event.delta.type? == "text_delta") | .event.delta.text'
- 스트림 마지막 줄은 최종 응답 텍스트·비용·세션 메타데이터를 담은
result메시지 - 소비자가 느리게 읽으면 큐 배출까지 대기, 최대 30초(v2.1.214 이전엔 약 2초라 대용량 응답 끝이 잘렸다)
- 서브에이전트 메시지는
parent_tool_use_id로 구분(메인은null).--forward-subagent-text또는CLAUDE_CODE_FORWARD_SUBAGENT_TEXT로 서브에이전트 텍스트·사고 블록도 방출(v2.1.211+)
프로세스 수명
- 백그라운드 Bash 태스크는 최종 결과 반환 + stdin 닫힘 약 5초 후 종료. v2.1.163 이전에는 종료하지 않는 백그라운드 프로세스가
claude -p를 무한정 붙잡았다 - 백그라운드 서브에이전트/워크플로는 5초 유예에서 면제되어 완료까지 대기. v2.1.182 부터 연속 유휴 대기 최대 10분으로 상한.
CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS로 조정,0이면 무제한 - SIGTERM 으로 중단하면 종료 코드 143. 진행 중이던 턴은 미완료로 남고 결과가 기록되지 않는다. 턴을 끝내려면 SIGINT 를 보내거나 SDK
interrupt()호출. SIGTERM 시 실행 중 Bash 명령의 프로세스 트리를 종료하고SessionEnd훅을 실행 후 종료
agy 로 사상할 규약 (⚠️ 플래그명은 실측 필요)
개념 claude gemini codex agy (미검증) 비대화형 프롬프트 -p/-pcodex execagy -pJSON 출력 --output-format json--format=json(문서 미확인) ?스키마 강제 --json-schema없음 없음 ?권한 우회 --dangerously-skip-permissions— --ask-for-approval never?쓰기 허용 --allowedTools— --sandbox workspace-write?전자동 — — --full-auto?시스템 프롬프트 --append-system-prompt-fileGEMINI_SYSTEM_MD환경변수— ?컨텍스트 최소화 --bare— --ephemeral?반드시 지킬 것: (1) 프롬프트는 파일에 두고
$(cat ...)로 주입, (2) 출력은 파일로 리다이렉트 후 파싱, (3) 종료 코드로 성공/실패 분기, (4) 타임아웃을 반드시 걸 것, (5) 실패해도 파이프라인 전체가 죽지 않게 — AI 요약은 선택적 보강이지 필수 경로가 아니다.
9.2 실전 호출 예시 — drew.tech [F#21]
출처: https://drew.tech/posts/claude-code-as-a-cron-job
claude \
--dangerously-skip-permissions \
--output-format json \
--json-schema "$(cat /tmp/schema.json)" \
-p "$(cat /tmp/prompt.txt)" \
> /tmp/output.json
| 플래그 | 용도 |
|---|---|
--dangerously-skip-permissions |
권한 프롬프트 우회 |
--output-format json |
구조화 JSON 반환 |
--json-schema |
제공된 스키마로 출력 검증 |
-p |
파일 또는 stdin 에서 프롬프트 수용 |
출력 처리: stdout 을 /tmp/output.json 으로 리다이렉트 → sandbox.readFileToBuffer() 로 읽기 → JSON 파싱 후 structured_output 필드 추출 → Zod 스키마로 검증.
함정: "snapshots preserve auth state" — MCP·통합은 자동화 시작 전에 스냅샷에 미리 설정해두어야 한다. PATH/환경변수 이슈는 이 글에서 언급되지 않았다.
이 스니펫이 우리
orchestrate모듈의 원형이다. 4개 요소 — 스키마 파일, 프롬프트 파일, 출력 파일, 권한 우회 플래그 — 를 그대로ops/agent/run_agy.cmd에 옮긴다.
9.3 jshchnz/claude-code-scheduler [F#9][F#39][F#44][F#57][F#66]
| 항목 | 값 |
|---|---|
| URL | https://github.com/jshchnz/claude-code-scheduler |
| 설명 | "Put Claude on autopilot. Schedule code reviews, security audits, and anything else - Claude Code runs them automatically, even while you sleep." |
| ★ / Fork | 510 / 37 |
| 언어 | TypeScript |
| 요구사항 | "Claude Code v1.0.33+" |
파일 구조:
.claude-plugin/ 플러그인 설정
commands/ CLI 명령 구현
src/ 소스
skills/scheduler/ 스케줄러 스킬 모듈
examples/ 사용 예시
dist/ 빌드 산출물
package.json, tsconfig.json, vitest.config.ts
src/ 하위 [F#57]:
__tests__/ cron/ history/ logs/ schedulers/ utils/ vcs/
config.ts index.ts types.ts
src/schedulers/ 하위 [F#66] (정확히 인용):
base.ts
darwin.ts
index.ts
linux.ts
windows.ts
동작: 태스크를 OS 네이티브 스케줄러에 등록하고, 예정 시각에 claude -p "your command" 를 실행. 출력은 ~/.claude/logs/<task-id>.log 에 로깅.
OS 지원: "macOS (launchd), Linux (crontab), Windows (Task Scheduler)"
설정 형식: .claude/schedules.json(프로젝트) 또는 ~/.claude/schedules.json(전역). 속성: id, name, trigger(cron 표현식), execution(command, timeout, skipPermissions), 선택적 worktree.
지원 플래그: --dangerously-skip-permissions(파일 편집·명령 실행 자율 수행). --output-format 은 README 에 명시되지 않음.
실제 스케줄 JSON 전문 [F#44] (examples/daily-review.json, 원문 그대로):
{
"version": 1,
"tasks": [
{
"id": "daily-code-review",
"name": "Daily Code Review",
"description": "Review commits from the previous day for code quality and potential issues",
"enabled": true,
"trigger": {
"type": "cron",
"expression": "0 9 * * 1-5",
"timezone": "local"
},
"execution": {
"command": "Review all commits from yesterday. Check for: 1) Code quality issues, 2) Security vulnerabilities, 3) Performance concerns, 4) Missing tests. Summarize findings and suggest improvements.",
"workingDirectory": ".",
"timeout": 300
},
"tags": ["code-quality", "daily"],
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
}
],
"settings": {
"defaultTimezone": "local",
"logRetentionDays": 30,
"maxExecutionHistory": 100
}
}
examples/ 에는 daily-review.json 과 weekly-audit.json 두 파일이 있다 [F#39].
Windows 에서 스케줄된 Claude 태스크 확인 [S#28]:
schtasks /query /tn "ClaudeSchedule*"
플러그인은 태스크 생성 시 래퍼(wrapper) 셸 스크립트를 생성해 Claude Code 를 프롬프트와 함께 호출하고, 그 래퍼를 Task Scheduler 에 등록한다. Task Scheduler 는 시스템 레벨 프로세스이므로 앱을 열지 않아도 실행된다.
베낄 것 (3가지, 전부 채택)
src/schedulers/{base,darwin,linux,windows}어댑터 분리 → 우리는 Windows 만 필요하지만ops/tasks/에 동일한 인터페이스를 두어 나중에 서버 이관이 가능하게 한다.- 스케줄 정의를 JSON/YAML 파일로. 위 JSON 의 필드 구성(
id,name,enabled,trigger.{type,expression,timezone},execution.{command,workingDirectory,timeout},tags,settings.{defaultTimezone,logRetentionDays,maxExecutionHistory})을 거의 그대로config/schedule.yaml로 옮긴다. 특히timeout(초)과logRetentionDays는 필수다.- 태스크 ID 별 로그 파일(
logs/<task-id>.log). 우리는logs/runs/<task-id>-YYYYMMDD.log.
9.4 Gemini CLI [F#35][S#20]
addyosmani/gemini-cli-tips (★2.4k / Fork 105):
# 원샷
gemini -p "Your prompt here"
# stdin 파이프
echo "Count to 10" | gemini
- "output a single response and exit" — 대화형 REPL 진입 없음
--format=json으로 프로그램적 소비. "parse the JSON to get the answer or any tool actions details"- 시스템 프롬프트 교체:
export GEMINI_SYSTEM_MD="/path/to/custom_system.md"
- 위치: "It transforms Gemini CLI from an interactive assistant into a backend service or utility that other programs can call."
- 관련 문서/논의: https://github.com/google-gemini/gemini-cli/discussions/3215 (Headless execution), https://geminicli.com/docs/issue-and-pr-automation/
주의: 블로그가 소개한 예제 저장소 github.com/testing-in-production/gemini-jobs 는 404 다 [F#17].
9.5 Codex CLI [S#26]
codex exec는 대화형 TUI 없이 실행 — CI/CD, Git hooks, cron job, 스크립트 자동화용- 단일 에이전트 세션을 시작해 태스크를 완료까지 실행, 진행 상황은 stderr 로 스트리밍, 최종 에이전트 메시지는 stdout 으로, 그리고 종료. 승인 프롬프트 없음
- 기본 샌드박스는 read-only. 파일 수정이 필요하면
--sandbox workspace-write, 무인 실행은--ask-for-approval never - GitHub Actions 예시:
codex exec --full-auto를cron: '0 8 * * 1-5'스케줄로 - 배치 패턴: bash 루프로
codex exec --full-auto --ephemeral를 태스크 목록마다 별도 세션으로 실행
agy 에 대한 시사점: 세 CLI 모두 "기본은 안전(읽기 전용/승인 요구), 자동화는 명시적 플래그로 해제" 구조다.
agy도 동일할 가능성이 높으므로, 부트스트랩 시agy --help로 (a) 비대화형 프롬프트 플래그, (b) 쓰기 권한 플래그, (c) 승인 우회 플래그, (d) JSON 출력 플래그 4가지를 반드시 식별해docs/research/에 기록해야 한다.
9.6 Claude Code Desktop 스케줄드 태스크 — 우리가 쓰지 않을 경로 [F#49]
출처: https://code.claude.com/docs/en/desktop-scheduled-tasks
세 가지 스케줄링 옵션 비교(문서 표 원문):
| Cloud (routines) | Desktop | /loop |
|
|---|---|---|---|
| 실행 위치 | 클라우드, 기본 Anthropic 관리 | 사용자 머신 | 사용자 머신 |
| 머신 켜짐 필요 | No | Yes | Yes |
| 열린 세션 필요 | No | No | Yes |
| 재시작 후 지속 | Yes | Yes | 만료 전이면 --resume 시 복원 |
| 로컬 파일 접근 | No (fresh clone) | Yes | Yes |
| MCP 서버 | 태스크별 커넥터 | 설정 파일 + 커넥터 | 세션 상속 |
| 권한 프롬프트 | No (자율 실행) | 태스크별 설정 가능 | 세션 상속 |
| 스케줄 커스터마이즈 | CLI /schedule |
Yes | Yes |
| 최소 간격 | 1시간 | 1분 | 1분 |
주요 제약:
- 로컬 태스크는 앱이 열려 있고 컴퓨터가 깨어 있을 때만 발화한다. 컴퓨터가 자면 실행은 스킵
- Settings → Desktop app → General 의 Keep computer awake 로 유휴 절전 방지 가능. 노트북 뚜껑을 닫으면 여전히 잠듦
- 놓친 실행: 앱 시작/기기 깨어남 시 지난 7일 내 놓친 실행을 확인해 가장 최근에 놓친 1회만 캐치업 실행하고 나머지는 폐기. "A task scheduled for 9am might run at 11pm if your computer was asleep all day."
- 태스크마다 몇 분의 결정론적 지연 오프셋이 붙어 API 트래픽을 분산
- 프롬프트 파일:
~/.claude/scheduled-tasks/<task-name>/SKILL.md(YAML frontmatter 로name/description, 본문이 프롬프트). 스케줄·폴더·모델·활성 상태는 이 파일에 없음 - 태스크가 실행 중
update_scheduled_taskMCP 도구로 자기 스케줄/프롬프트를 수정 가능
기각 결정: Desktop 스케줄드 태스크는 앱이 열려 있어야 한다는 치명적 제약 때문에 "재부팅 후에도 자동 복구되는 서비스" 요구를 만족하지 못한다. Windows Task Scheduler + 워치독으로 간다. 다만 채택할 개념 2가지: ① 놓친 실행 캐치업 로직 — 06:00 에 PC 가 꺼져 있었다면 부팅 후 1회만 따라잡는다(중복 실행 금지). ② 프롬프트를 YAML frontmatter + 본문 마크다운 파일로 관리.
10. (g) Windows 서비스화 · 워치독 · 토스트
10.1 winsw/winsw — 서비스화 1순위 [F#10][F#23][F#59]
| 항목 | 값 |
|---|---|
| URL | https://github.com/winsw/winsw |
| 설명 | "A wrapper executable that can run any executable as a Windows service, in a permissive license." |
| ★ / Fork | 14.3k / 1.7k |
| 언어 | C# |
| 커밋 | v3 브랜치 841 commits |
| 최신 릴리스 | v3.0.0-alpha.11 — "29 Jan 02:20" |
| 최신 안정 | v2.12.0 — "28 Jan 16:22" |
| 배포 | GitHub Releases + NuGet/Maven(2.x) |
설치 절차 (README 원문 요약): WinSW.exe 또는 WinSW.zip 을 받아 → myapp.xml 작성 → winsw install myapp.xml → winsw start myapp.xml → winsw status myapp.xml. 또는 WinSW.exe 를 myapp.exe 로 리네임하고 myapp.xml 을 나란히 두면 자동 발견된다.
XML 설정 요소 전문 [F#23]:
<!-- 실패 시 동작: restart / reboot / none. delay 단위: sec/secs/min/mins/hour/hours/day/days -->
<onfailure action="restart" delay="10 sec"/>
<onfailure action="restart" delay="20 sec"/>
<onfailure action="reboot" />
<!-- Windows SCM 이 실패 카운트를 리셋하는 시점. 기본 1 day -->
<resetfailure>1 hour</resetfailure>
<!-- Automatic | Manual. 기본 Automatic -->
<startmode>Automatic</startmode>
<!-- Automatic 일 때 지연 시작 -->
<delayedAutoStart>true</delayedAutoStart>
<!-- 로그 모드: append(기본) | reset | ignore | roll -->
<log mode="roll"></log>
<!-- 시작/정지 실행 파일과 인자 -->
<executable>catalina.sh</executable>
<startarguments>jpda run</startarguments>
<stopexecutable>catalina.sh</stopexecutable>
<stoparguments>stop</stoparguments>
<!-- 환경변수 -->
<env name="HOME" value="c:\\abc" />
<!-- 작업 디렉터리 -->
<workingdirectory>C:\\application</workingdirectory>
<!-- 프로세스 우선순위: idle | belownormal | normal | abovenormal | high | realtime -->
<priority>idle</priority>
<!-- 서비스 계정. gMSA 는 username 끝에 $ 를 붙이고 password 생략 -->
<serviceaccount>
<username>DomainName\\UserName</username>
<password>Pa55w0rd</password>
<allowservicelogon>true</allowservicelogon>
</serviceaccount>
설정 XML 은 %Name% 형태의 환경변수 확장을 지원한다.
샘플 파일: https://github.com/winsw/winsw/blob/v3/samples/minimal.xml (필수 옵션만), https://github.com/winsw/winsw/blob/v3/samples/complete.xml (전체 옵션)
채택 (1순위) — 근거:
<onfailure action="restart" delay="10 sec"/>를 여러 개 나열해 1차/2차 재시도 지연을 다르게 줄 수 있다. 요구사항 "서비스가 죽으면 복구" 를 선언적으로 만족.<resetfailure>1 hour</resetfailure>로 "1시간 정상 동작하면 실패 카운트 리셋" — 무한 재시작 루프 방지.<log mode="roll">로 로그 로테이션 내장.<startmode>Automatic</startmode>+<delayedAutoStart>true</delayedAutoStart>로 재부팅 후 자동 복구 요구를 만족. 지연 시작은 부팅 직후 네트워크 미준비 상태를 회피.- 파이썬 런타임 의존이 없다(C# 단일 exe).
우리 XML 초안 (
ops/winsw/dmf-watchdog.xml):<service> <id>DMFCrawlerWatchdog</id> <name>DMF Crawler Watchdog</name> <description>DMF_Crawler 일일 파이프라인 감시 및 복구 안내</description> <executable>D:\workspace\DMF_Crawler\.venv\Scripts\python.exe</executable> <arguments>-m dmf_crawler watchdog</arguments> <workingdirectory>D:\workspace\DMF_Crawler</workingdirectory> <startmode>Automatic</startmode> <delayedAutoStart>true</delayedAutoStart> <onfailure action="restart" delay="10 sec"/> <onfailure action="restart" delay="60 sec"/> <onfailure action="restart" delay="300 sec"/> <resetfailure>1 hour</resetfailure> <log mode="roll"></log> <env name="PYTHONUTF8" value="1"/> <priority>belownormal</priority> </service>⚠️ 미검증 —
<id>,<name>,<description>,<startarguments>vs<arguments>의 정확한 요구 여부는samples/minimal.xml로 실측 필요.
10.2 NSSM — 서비스화 2순위 [F#24][F#51][F#14]
kirillkovalenko/nssm (★1.2k / Fork 169, C++, 버전 2.24 / 2014-08-31). README 는 http://nssm.cc/ 를 공식 문서로 지목하며, 이 저장소는 커뮤니티 포크로 보인다(공식 소스 미러 여부 불확실 — ⚠️ 미검증). 핵심 문장: NSSM "can start any application as an NT service and will restart the service if it fails for any reason."
CLI 사용법 전문 [F#24] (출처: https://nssm.cc/usage):
nssm install <servicename> <application> [<options>]
nssm set <servicename> Application C:\path\to\app.exe
nssm set <servicename> AppDirectory C:\startup\directory
nssm set <servicename> AppParameters argument1 argument2
nssm set <servicename> AppStdout C:\path\to\output.log
nssm set <servicename> AppStderr C:\path\to\error.log
nssm set <servicename> Start SERVICE_AUTO_START
nssm set <servicename> Start SERVICE_DELAYED_AUTO_START
종료 액션 (애플리케이션 종료 시 반응): Restart(자동 재실행) / Ignore(중지 상태 유지) / Exit(서비스 중지). 레지스트리 경로: HKLM\System\CurrentControlSet\Services\<servicename>\Parameters\AppExit
재시작 스로틀링 (CPU 루프 방지 — 임계 밀리초 내 종료 시 재시작 지연):
nssm set <servicename> AppThrottle 1500
재시작 지연 (재시작 간 강제 간격):
nssm set <servicename> AppRestartDelay 3000
로그 로테이션:
nssm set <servicename> AppRotateFiles 1
제거:
nssm remove <servicename> confirm
전체 구성 예시:
nssm install MyService "C:\Program Files\app.exe"
nssm set MyService AppDirectory C:\Program Files
nssm set MyService AppParameters --config settings.ini
nssm set MyService AppStdout C:\logs\output.log
nssm set MyService AppStderr C:\logs\error.log
nssm set MyService Start SERVICE_AUTO_START
nssm set MyService AppThrottle 2000
nssm set MyService AppRestartDelay 5000
larsekje/PythonWindowsServices 의 실전 지식 [F#14] (★1 / Fork 0, Python. 파일: /logs, /scripts, /windows_service, .gitignore, readme.md, requirements.txt):
nssm install "SERVICE_NAME" "PATH_TO_PYTHON.exe" "PATH_TO_PYTHON_SCRIPT.py"
nssm start SERVICE_NAME
요구사항: NSSM 이 시스템 PATH 에 있을 것, requirements.txt 로 파이썬 환경 구성, 그리고 "NSSM 이 로그 파일을 자동 생성하지 않으므로 로그 파일을 미리 만들어 둘 것".
부분채택 (폴백): WinSW 설치가 실패하거나 .NET 런타임 문제가 생기면 NSSM 으로 전환한다.
AppThrottle(15002000ms) +5000ms) 조합이 WinSW 의AppRestartDelay(3000onfailure delay와 등가다. 버전이 2014년이라는 점이 감점 요인.
10.3 pywin32 서비스 — 기각 [F#15][F#40][F#52][S#16]
HaroldMills/Python-Windows-Service-Example (★21 / Fork 11, Python, MIT). 파일: .gitignore, LICENSE, README.md, example_service.py, example_service.spec.
- 의존성: "The service should be built in a Python environment that includes the
pywin32andpyinstallerpackages." - 빌드:
pyinstaller example_service.spec(저장소 루트에서) - 설치:
build\example_service에서example_service.exe install→example_service.exe start - "The commands must be issued from a command prompt that was run as administrator."
- 제거:
example_service.exe stop→example_service.exe remove(관리자 권한) - 치명적 주석: "as of this writing (2016-03-37), PyInstaller supports Python versions only through 3.5"
drmalex07/10554232 gist 의 표준 골격 [F#40]:
class HelloWorldSvc (win32serviceutil.ServiceFramework):
_svc_name_ = "HelloWorld-Service"
_svc_display_name_ = "HelloWorld Service"
def __init__(self, args):
win32serviceutil.ServiceFramework.__init__(self, args)
self.stop_event = win32event.CreateEvent(None, 0, 0, None)
socket.setdefaulttimeout(60)
self.stop_requested = False
SvcStop 은 self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) 후 정지 플래그를 세운다. SvcDoRun 이 self.main() 을 호출하고, 메인 루프는 if self.stop_requested: 로 탈출한다.
if __name__ == '__main__':
win32serviceutil.HandleCommandLine(HelloWorldSvc)
스레드 코멘트: 이 기본 패턴은 타임아웃 에러를 만날 수 있으며, 개선판은 len(sys.argv) 로 커맨드라인 설치와 실제 서비스 실행을 구분한다.
기각의 결정적 근거 — pywin32 issue #1563 [F#52]:
요청 내용:
win32serviceutil.ServiceFramework서비스가 Windows 복구 액션(자동 재시작 등)을 트리거하는 방식으로 종료되기를 원한다. 복구 액션은 서비스가SERVICE_STOPPED를 보고하지 않고 끝날 때 발동한다. 문제:SvcRun()이 예외를 던지거나sys.exit()를 호출하면, pywin32 의 정리 코드가 자동으로 서비스 상태를SERVICE_STOPPED로 설정해버려 복구 액션이 트리거되지 않는다. 현재 우회책:os.kill(os.getpid(), signal.SIGABRT)— 보고자 본인도 "극단적 조치"라고 인정. 요청:SvcRun()이 자동 정리(특히SetServiceStatus()호출)를 건너뛰도록 플래그를 걸 수 있게 해달라.
관련 [S#16]: 크래시한 서비스의 재시작은 ChangeServiceConfig2 로 실패 액션 딕셔너리를 설정해 구성할 수 있다. win32service 모듈로 SCM 에 연결해 서비스를 열거하고 상태(Running/Stopped/Paused)를 확인하며 시작·중지·일시정지·재시작할 수 있다.
결정: pywin32 로 서비스를 직접 구현하지 않는다. 서비스 껍데기는 WinSW/NSSM 에 맡기고, pywin32 는 워치독이 서비스 상태를 조회할 때만(
win32service열거/상태 조회) 사용한다.
10.4 michalzobec/autorunsalerts — 운영 패턴 정본 [F#34]
| 항목 | 값 |
|---|---|
| URL | https://github.com/michalzobec/autorunsalerts |
| 설명 | "Simple toast notifications for changes to autoruns configurations on windows" |
| ★ | 0 |
| 언어 | PowerShell |
2개 스케줄드 태스크 구조:
AutorunsAlert(SYSTEM 컨텍스트, 60분마다) —autorunsc.exe로 현재 상태 스캔 →state.json의 이전 기준선과 비교 → 탐지된 모든 변경을audit.log에 기록AutorunsAlertToast(사용자 컨텍스트, 15분마다) — 스캐너가 세운 알림 플래그를 확인하고 토스트를 띄우며audit.log로 연결
파일:
autorunsalert.ps1— 메인 스캔 스크립트autorunstoast.ps1— 토스트 전달 스크립트configuration.json— 공유 설정 변수state.json— 비교용 이전 스캔 기준선audit.log— 변경 이력 및 조사 기록install.ps1/uninstall.ps1— 설치/제거
원문 결론: "This separation ensures alerts appear in the user's active session rather than the system session where detection occurs."
채택 (핵심 운영 패턴). 우리 대응:
autorunsalerts DMF_Crawler AutorunsAlert(SYSTEM, 60분)DMF_Crawler_Daily(Task Scheduler, 매일 06:00) — 수집·diff·리포트 생성AutorunsAlertToast(User, 15분)DMF_Crawler_Notify(User 컨텍스트, 15분) —state/notify_queue.json확인 후 토스트state.jsondata/state.json(마지막 성공 실행 시각, 마지막 스냅샷 해시)audit.loglogs/audit.log(누적 변경 이력)configuration.jsonconfig/settings.yamlinstall.ps1/uninstall.ps1ops/tasks/install.ps1/uninstall.ps1이 2단 분리가 필수인 이유: 아래 10.5 참조.
10.5 Windos/BurntToast — 그리고 SYSTEM 컨텍스트 제약 [F#33][S#25]
| 항목 | 값 |
|---|---|
| URL | https://github.com/Windos/BurntToast |
| 설명 | "PowerShell Module for displaying Toast Notifications on Windows 10 and Windows Server 2019 and above." |
| ★ / Fork | 1.7k / 126 |
| 언어 | PowerShell |
| 최신 릴리스 | v1.1.0 — Urgent 스위치(긴급 알림), 버튼 색상 커스터마이즈 추가 |
| 라이선스 | MIT |
Install-Module -Name BurntToast
New-BurntToastNotification -Text "제목", "본문"
New-BTButton # 인터랙티브 버튼
# -AppLogo 로 앱 브랜딩
결정적 제약 (원문): "The module targets user-context notifications and has limitations when running from SYSTEM or service accounts due to the Windows notification framework's desktop session requirements."
관련 사례 [S#25]:
Badgerati/Hook— 서비스 상태를 감시하다 중지되면 BurntToast 팝업(MongoDB 서비스 예시)- Windows 업데이트 알림 패턴 (cyberdrain.com), 재부팅 알림 가이드 (dearing.dev), PDQ 블로그
채택: 워치독의 사용자 알림은 BurntToast(PowerShell) 또는 win11toast(Python) 로 하되, 반드시 사용자 컨텍스트 태스크에서 실행한다. 서비스(SYSTEM)에서 직접 토스트를 띄우려는 시도는 하지 않는다.
10.6 파이썬 토스트 라이브러리 3종 비교
| 라이브러리 | ★ | Fork | 설치 | 기반 | 특징 | 함정 |
|---|---|---|---|---|---|---|
GitHub30/win11toast [F#12][F#67] |
333 | 24 | pip install win11toast |
WinRT | toast(), notify()(논블로킹), toast_async(), buttons, on_click, image, duration |
실행 시 CWD 가 C:\Windows\system32 이므로 os.chdir() 필요. app_id 파라미터는 문서에 없음 |
DatGuy1/Windows-Toasts [F#11] |
142 | 9 | python -m pip install windows-toasts |
WinRT (pywin32 아님) | Toast(), WindowsToaster(), text_fields, on_activated |
duration 이 short/long 만 (pywin32 대비 제약) |
ysfchn/toasted [F#20] |
31 | 2 | python -m pip install toasted |
WinRT | Windows 가 제공하는 모든 요소 지원 — 이미지, select, input, progress | Python 버전 요구사항 미문서화 |
win11toast 코드 [F#12][F#67]:
from win11toast import toast
toast('Hello Python🐍')
toast('Hello Python', 'Click to open url', on_click='https://www.python.org')
toast('Hello', 'Click a button', buttons=['Approve', 'Dismiss', 'Other'])
toast('Hello', 'Hello from Python', image='https://example.com/image.png')
toast('Hello Python🐍', duration='long')
from win11toast import notify
notify('Hello Python', 'Click to open url', on_click='https://www.python.org')
from win11toast import toast_async
async def main():
await toast_async('Hello Python', 'Click to open url',
on_click='https://www.python.org')
버튼은 프로토콜 활성화 지원: {'activationType': 'protocol', 'arguments': 'https://google.com', 'content': 'Open Google'} → 클릭 시 {'arguments': 'https://google.com', 'user_input': {}} 반환. 라이선스 MIT. 선행 프로젝트로 winsdk_toast, Windows-Toasts, MarcAlx/notification.py 를 인정.
Windows-Toasts 코드 [F#11]:
from windows_toasts import Toast, WindowsToaster
toaster = WindowsToaster('Python')
newToast = Toast()
newToast.text_fields = ['Hello, world!']
newToast.on_activated = lambda _: print('Toast clicked!')
toaster.show_toast(newToast)
toasted 코드 [F#20]:
from toasted import Toast, Progress, Text
import asyncio
async def main():
toast = Toast()
toast.elements = [
Text("File downloader"),
Progress(value="{value}", status="Downloading files...")
]
await toast.show(dict(value=75/100))
win10toast (jithurjacob/Windows-10-Toast-Notifications) [S#9] — pip 설치 가능, 커스텀 아이콘·스레드 알림 지원. 가장 널리 쓰이지만 오래된 라이브러리.
채택:
win11toast를 1순위(별 수 가장 많고on_click으로 xlsx 파일 열기 연결 가능),toasted를 백필 진행률 표시용 2순위.os.chdir()함정은 워치독 스크립트 첫 줄에 반드시 반영.
10.7 Apprise — 알림 추상화 계층 [F#38][F#56]
| 항목 | 값 |
|---|---|
| URL | https://github.com/caronc/apprise |
| 설명 | "Push Notifications that work with just about every platform!" |
| ★ / Fork | 17.2k / 652 |
| 언어 | Python |
| 커밋 | master 1,178 commits |
| 설치 | pip install apprise |
import apprise
apobj = apprise.Apprise()
apobj.add('mailto://myuserid:mypass@gmail.com')
apobj.notify(body='notification text', title='my title')
URL 형식:
| 서비스 | URL |
|---|---|
| Windows Toast | windows:// |
| Slack | slack://TokenA/TokenB/TokenC/Channel |
| Telegram | tgram://bottoken/ChatID |
mailto://userid:pass@domain.com, mailtos://(보안) |
windows:// 상세 [F#56]:
- 필요 의존성:
pip install pywin32 - 파라미터:
duration— "Optionally set the duration of the popup message in seconds. By default this value is set to12". 예:windows://?duration=5 - 제약: "this notification can not be sent from one PC to another." — 같은 시스템에만 전송 가능
- 메시지 사양: 아이콘 지원, 텍스트 포맷, 메시지당 최대 250자
채택: 알림 채널을
config/settings.yaml의 URL 문자열 리스트로 선언하고 Apprise 로 일괄 전송한다. 100+ 서비스를 코드 수정 없이 갈아끼울 수 있다.notify: urls: - "windows://?duration=8" # - "tgram://<bottoken>/<ChatID>" # - "mailto://user:pass@company.co.kr" max_body_chars: 250 # windows:// 제약에 맞춤단, 리치 토스트(버튼/이미지/클릭 시 xlsx 열기)는 Apprise 로 불가능하므로
notify/toast.py에서win11toast를 직접 호출하는 이중 경로를 유지한다.
10.8 Windows 워치독 최종 설계 (종합)
[Task Scheduler]
├─ DMF_Crawler_Daily 매일 06:00, 최고 권한
│ → run_daily.cmd → python -m dmf_crawler daily
│ 성공: state.json 갱신 + notify_queue.json 에 요약 push
│ 실패: logs/errors.log 기록 + notify_queue.json 에 실패 push
│
├─ DMF_Crawler_Notify 15분마다, 사용자 컨텍스트(로그온 시)
│ → python -m dmf_crawler notify-drain
│ notify_queue.json 을 비우며 win11toast 로 표시
│
└─ DMF_Crawler_Catchup 로그온 시 1회
→ python -m dmf_crawler daily --catchup
state.json 의 마지막 성공일이 오늘 이전이면 1회만 따라잡기
[WinSW 서비스: DMFCrawlerWatchdog] (선택, 상시 감시가 필요할 때)
→ python -m dmf_crawler watchdog
· Task Scheduler 태스크 3개의 존재/활성 상태를 주기 확인 (win32com/win32service)
· 마지막 성공 실행이 26시간을 넘으면 notify_queue.json 에 경보 push
· 자기 자신이 죽으면 WinSW <onfailure action="restart"> 가 복구
왜 서비스와 스케줄드 태스크를 둘 다 쓰는가: 스케줄드 태스크는 "정시에 한 번 도는 일"에 최적이고, 서비스는 "죽었는지 지켜보는 일"에 최적이다. autorunsalerts 가 검증한 SYSTEM/User 분리에, WinSW 의 onfailure 자동 복구를 얹은 형태다.
11. (h) awesome 리스트 및 기타 참고
11.1 lorien/awesome-web-scraping [F#13]
| 항목 | 값 |
|---|---|
| URL | https://github.com/lorien/awesome-web-scraping |
| 설명 | "List of libraries, tools and APIs for web scraping and data processing." |
| ★ / Fork | 8.1k / 934 |
| 커밋 | master 640 commits |
구조:
python.md Python 패키지
javascript.md JavaScript 패키지
php.md PHP 패키지
ruby.md Ruby 패키지
golang.md Go 패키지
cli.md 커맨드라인 도구
manuals.md 교육 자료·서적
README 링크: https://github.com/lorien/awesome-web-scraping/blob/master/README.md
직접 확인한 사실: 이 저장소에는 변경 감지·스케줄링·엑셀/리포팅 섹션이 없다. 큐레이션 목록이지 모니터링 도구가 아니다. 캡차 해결 서비스, 프록시 마켓플레이스 참조, Telegram 커뮤니티 링크, CONTRIBUTING.md 를 포함.
활용 범위:
python.md를 라이브러리 선정 시 참고. 그 이상은 없다.
11.2 awesome 파생 리스트 (⚠️ 미검증)
| 저장소 | 요점 |
|---|---|
noirquant/awesome-web-scraping |
lorien 포크 |
jjwangnlp/awesome-web-scraping |
lorien 포크 |
luminati-io/Awesome-Web-Scraping |
HTTP 라이브러리·브라우저 자동화·프록시 서비스 포함 |
spinov001-art/awesome-web-scraping-2026 |
"130+ web scraping tools — Python, JavaScript, Go, Rust. Anti-detection, proxies, cloud platforms. Updated weekly. Includes free API alternatives." |
duyet/awesome-web-scraper |
"A collection of awesome web scaper, crawler." |
patrickloeber/llm-data-scrapers |
"A list of useful Open Source tools and scrapers to gather data for LLMs" |
realpython/list-of-python-api-wrappers |
파이썬 API 래퍼 목록 |
awesome-pharma-data 같은 제약 전용 awesome 리스트는 존재가 확인되지 않았다. 대신 GitHub Topics 페이지가 그 역할을 한다:
활용:
pharmaceutical-data토픽에는 저장소가 7개뿐이다(2026-09 기준). 이 분야에 오픈소스가 거의 없다는 사실 자체가, 우리가 직접 만들어야 한다는 결론을 강화한다.
11.3 벤더/블로그 자료 (코드 없음, 배경 지식)
| 자료 | URL | 요점 |
|---|---|---|
| PageCrawl.io: 오픈소스 변경감지 도구 비교 | https://pagecrawl.io/blog/open-source-website-change-detection-tools | 도구를 목적형 모니터(changedetection.io, urlwatch) 와 범용 자동화 플랫폼(Huginn, n8n) 으로 이분. 선택 기준: JavaScript 렌더링, 노이즈 필터링, 알림 폭, 실패 가시성, 유지보수 부담 |
| GIGAZINE: changedetection.io 리뷰 | https://gigazine.net/gsc_news/en/20260517-changedetection-io | 셀프호스트 모니터링 도구 리뷰 |
| alternativeto: urlwatch / changedetection.io 대안 | https://alternativeto.net/software/urlwatch , https://alternativeto.net/software/changedetection-io/ | Huginn 이 changedetection.io 의 최선 대안으로 언급 |
| Simon Willison: Git scraping | https://simonwillison.net/2020/Oct/9/git-scraping/ | "track changes over time by scraping to a Git repository". git-history 도구 |
| Oxylabs / Flipnode / JC Chouinard / Biztory / Oreate AI | (부록 A 참조) | Python 스크래퍼 + Windows Task Scheduler 자동화 튜토리얼 |
| PDQ: BurntToast | https://www.pdq.com/blog/display-toast-notifications-with-powershell-burnt-toast-module/ | "perfect for script completion alerts, Pomodoro timers, or nudging users to reboot" |
| Infonautics / usro.net / ehmiiz.se | (부록 A 참조) | WinSW 로 프로그램을 서비스화하는 단계별 가이드, PowerShell 스크립트 서비스화 |
| Codex Knowledge Base (danielvaughan.com) 4편 | (부록 A 참조) | codex exec 헤드리스·배치·CI·스케줄드 에이전트 |
| SmartScope / MindStudio / wmedia.es / hidekazu-konishi / StackNotice / Usagebar / DevShelfHub / HeyClaude / LikeOne / BuildThisNow | (부록 A 참조) | Claude Code 헤드리스·cron 가이드 다수 |
| Level Up Coding: Claude Code Routines | https://levelup.gitconnected.com/claude-code-routines-the-cron-replacement-i-didnt-know-i-needed-6f53cf476577 | Routines 를 cron 대체로 |
| MCP Market: Windows Task Scheduler Skill | https://mcpmarket.com/tools/skills/windows-task-scheduler | Claude Code 로 Windows Task Scheduler 잡을 만들고 관리하는 스킬 |
| Apify 스크래퍼들 | https://apify.com/labrat011/fda-orange-book-scraper/api , https://apify.com/fortuitous_pirate/openfda-scraper/api/python , https://apify.com/benthepythondev/openfda-drug-intelligence/api/python | FDA Orange Book / openFDA 상용 스크래퍼. Drugs@FDA 28,000+ 신청(NDA/ANDA/BLA) |
| John Snow Labs / PharmaCompass / pharmaexcipients / fdapals / dmf-list.backgroundscheck.info | (부록 A 참조) | FDA DMF 디렉터리 상용 데이터 |
| 한국어 크롤링 블로그 | velog naverPillCrawling, samslow.github.io 식품안전나라 크롤링 가이드, velog 공공데이터 포털API사용하기 |
Selenium/BeautifulSoup 기반 국내 의약품 크롤링 사례 |
11.4 라이선스 주의 표
코드를 참고할 때 반드시 확인해야 할 항목이다.
| 저장소 | 라이선스 | 코드 복사 가능? |
|---|---|---|
anton-semerenko/pharma-radar |
MIT | 가능(출처 표기) |
mrueda/nomenclator-delta |
MIT | 가능(출처 표기) |
logiover/fda-data-scraper |
MIT | 가능 |
huginn/huginn |
MIT | 가능 |
GitHub30/win11toast |
MIT | 가능 |
Windos/BurntToast |
MIT | 가능 |
HaroldMills/Python-Windows-Service-Example |
MIT | 가능 |
786raees/task-scheduler-python |
MIT | 가능 |
larsyencken/csvdiff |
BSD-3-Clause | 가능(고지 유지) |
suriyadeepan/WebScraping-for-Healthcare |
GPL-3.0 | 불가 — 설계만 참고 |
| 나머지 | 미확인 | ⚠️ 복사 전 확인 필수 |
12. 채택 결정 표 (채택 / 부분채택 / 기각)
12.1 저장소·도구별 결정
| 후보 | 결정 | 이유 (1~2문장) |
|---|---|---|
mrueda/nomenclator-delta |
채택 (구조 정본) | collection/normalization/diffing/validation 4단 분리와 data/(스냅샷+이력) 레이아웃이 우리 문제와 1:1 대응한다. CLI 서브커맨드 형태(python -m pkg validate data)까지 그대로 가져온다. |
anton-semerenko/pharma-radar |
채택 (에이전트 계층 정본) | prompts/ + config/sources.yaml + 의존성 최소 deliver 계층, 그리고 "≥2 독립 출처 또는 1 공식 1차 출처" 검증 정책. 매일 06:00 무인 실행이라는 동일한 운영 형태의 존재 증명. |
larsyencken/csvdiff |
부분채택 (스키마만) | _index/added/removed/changed JSON 구조와 --ignore-columns/--significance 옵션은 우리 diff 요구에 정확히 맞다. 단 2021-02-18 아카이브라 라이브러리 의존은 금지하고 자체 구현한다. |
huginn/huginn |
부분채택 (개념만) | mode: all/on_change/merge 3분류를 diff 출력 모드로 채택. Ruby/Rails 스택 전체는 Windows 단일 PC 에 과중하므로 기각. |
dgtlmoon/changedetection.io |
부분채택 (개념만) | 알림 템플릿 토큰({{diff_added}} 등), 필터 체인, 타임존 스케줄, 본문 길이 제한 규칙을 차용. 도구 자체는 텍스트 블록 diff 라서 레코드 키 기반 판정을 못 한다. |
thp/urlwatch |
부분채택 (설정 스키마만) | urls.yaml 잡 스키마와 job_defaults 상속, ignore_connection_errors, --test-diff-filter 개념을 config/sources.yaml 에 이식. 릴리스가 하나도 없는 저장소라 의존성 채택은 기각. |
Mzands2622/Zanalytix |
부분채택 (패턴만) | 소스별 파서 함수 시그니처 통일, 변경 우선순위 1~5 점수, 스냅샷/변경 테이블 분리를 채택. Azure Functions·Zyte·Twilio·로그인 기능은 전부 기각. |
FDA/openfda |
부분채택 (레이아웃만) | config/·schemas/·scripts/·<pkg>/ 4분할을 채택. Luigi/Elasticsearch/Docker 는 하루 1회 소량 데이터에 과잉이므로 기각. |
winsw/winsw |
채택 (서비스화 1순위) | <onfailure action="restart"> 다단 지연 + <resetfailure> + <startmode>Automatic</startmode> + <delayedAutoStart> 로 "재부팅 후 자동 복구"를 XML 한 장으로 만족한다. ★14.3k, 안정판 v2.12.0. |
kirillkovalenko/nssm / nssm.cc |
부분채택 (폴백) | AppThrottle/AppRestartDelay/AppExit Restart 로 동등한 복구를 제공하지만 버전이 2014년(v2.24)이라 2순위. WinSW 가 실패할 때만 전환. |
pywin32 서비스 (HaroldMills/..., gist drmalex07) |
기각 | issue #1563 — SvcRun() 예외/sys.exit() 시 pywin32 가 SERVICE_STOPPED 를 보고해 Windows 복구 액션이 트리거되지 않는다. 우회책이 SIGABRT 수준이면 운영에 못 쓴다. PyInstaller 도 Python 3.5 까지라는 낡은 제약. |
michalzobec/autorunsalerts |
채택 (운영 패턴 정본) | SYSTEM 스캔 태스크 + 사용자 컨텍스트 토스트 태스크 분리, state.json/audit.log/configuration.json/install.ps1 파일 구성을 그대로 매핑한다. |
Windos/BurntToast |
부분채택 | 사용자 컨텍스트 알림용 대안 경로로 유지. "SYSTEM/서비스 계정에서 제약" 이라는 문서가 2단 태스크 분리의 근거가 되었다는 점이 더 큰 기여. |
GitHub30/win11toast |
채택 (토스트 1순위) | ★333 으로 파이썬 토스트 중 최다. on_click='<xlsx 경로>' 로 리포트 바로 열기가 가능하고 buttons/image/duration 을 모두 지원한다. |
ysfchn/toasted |
부분채택 | Progress() 요소로 장시간 백필의 진행률 토스트를 띄울 때만 사용. |
DatGuy1/Windows-Toasts |
기각 | win11toast 대비 기능이 좁고(duration short/long), 별도 채택 이유가 없다. |
caronc/apprise |
채택 (알림 추상화) | 알림 채널을 URL 문자열로 선언해 Telegram/Slack/Email 로 코드 수정 없이 확장. windows:// 는 250자 제한이 있으므로 리치 토스트는 win11toast 이중 경로로 보완. |
| XlsxWriter | 채택 (리포트 엔진) | write_url('internal:Sheet2!A1') 이 "탭 간 연동" 요구를 만족하는 유일한 정공법이고, add_table/conditional_format/set_column 이 같은 API 안에 있다. |
openpyxl (Bwhiz/Auto-Excel-Reports) |
기각 (엔진으로서) | 생성/배포 파일 분리 아이디어만 채택. 서식·내부링크 품질 요구가 우리 쪽이 높아 XlsxWriter 가 낫다. |
jshchnz/claude-code-scheduler |
채택 (스케줄 설정 스키마) | schedules.json 필드 구성(trigger.expression, execution.timeout, settings.logRetentionDays)과 OS별 스케줄러 어댑터 분리, 태스크 ID별 로그 파일을 채택. TypeScript 구현체 자체는 미사용. |
| Claude Code Desktop 스케줄드 태스크 | 기각 | "앱이 열려 있고 컴퓨터가 깨어 있어야 발화" 라는 제약이 "재부팅 후 자동 복구" 요구와 충돌. 단 놓친 실행 캐치업 1회 규칙은 채택. |
| Claude Code Routines (cloud) | 기각 | 로컬 파일 접근 불가(fresh clone), 최소 간격 1시간. 로컬 xlsx 생성이 목적인 우리와 맞지 않는다. |
claude -p / gemini -p / codex exec |
부분채택 (규약만) | 세 CLI 의 공통 호출 규약(프롬프트 파일 → JSON 출력 → 파일 리다이렉트 → 종료코드 분기)을 agy -p 에 사상한다. agy 실제 플래그는 실측 필요. |
786raees/task-scheduler-python |
부분채택 | win32com.client 로 태스크를 CRUD 하는 API 형태만 참고. 코드 복사는 안 함(★2, 2 commits). |
simonw/git-scraper-template |
부분채택 (개념) | data/snapshots/ 를 로컬 git 리포로 두고 매일 커밋해 변경 이력을 무상으로 얻는다. GitHub Actions 부분은 기각(로컬 실행 요구). |
HasData/playwright-scraping |
부분채택 (체크리스트) | 디렉터리 목록을 fetch 모듈 요구사항 체크리스트로 사용. Playwright 도입 자체는 게시판이 JS 렌더링일 때만 조건부. |
ecprice/newsdiffs |
부분채택 | per-run 로그와 누적 에러 로그 분리, BaseParser 상속 구조를 채택. Django 스택은 기각. |
WooilJeong/PublicDataReader |
기각 (의존성으로) | 식약처/의약품 커버리지가 없다고 문서에 명시. provider 별 모듈 레이아웃만 참고. |
Q00/data.go.kr-crawling |
부분채택 | config.py.example 분리, 필드 사전 자동 생성, page = int(totalCount/100)+1 공식. 저장소 설명(gevent)과 실제 코드(동기 requests)가 다르므로 코드 신뢰 금지. |
jjscan/data.go.kr-1 |
부분채택 (교훈만) | 재시도 폴링, totalCount == 0 공백 판정, 대량 백필 병목(350k건 17시간) 경고를 설계에 반영. R 코드는 무관. |
NomaDamas/k-skill |
부분채택 (규범만) | API 키 분리 원칙과 "자동화가 직접 진단하지 않는다"는 안전 규범을 채택. |
Tanguy9862/AI-Powered-FDA-Drug-Scraper |
부분채택 | scraper/classification/utils 3분리와 업체명 정규화 필요성. LangChain/GPT 의존은 agy 로 대체. |
suriyadeepan/WebScraping-for-Healthcare |
부분채택 (설계만) | 소스별 동일 인터페이스 규약만. GPL-3.0 이므로 코드 복사 금지. |
jbremz/FDA-Analysis |
부분채택 | 스파이더/원시CSV/분석/노트북 4분할. 대상 사이트 은퇴로 프로젝트가 죽은 것은 파서 격리의 필요성 경고. |
coderxio/OpenFDA |
부분채택 | load_data/serve_data 엔트리포인트 분리 → dmf backfill / dmf daily. |
logiover/fda-data-scraper |
부분채택 | 출력 포맷을 최종 어댑터에서 스위칭하는 설계. Apify 플랫폼 의존은 기각. |
lorien/awesome-web-scraping |
부분채택 (참고용) | python.md 를 라이브러리 선정 참고로만. 변경감지/스케줄/엑셀 섹션이 없음을 확인했다. |
testing-in-production/gemini-jobs |
기각 | HTTP 404. 존재하지 않는다. |
12.2 기술 스택 최종 결정
| 계층 | 채택 | 대안(폴백) | 기각한 것 |
|---|---|---|---|
| 언어/런타임 | Python 3.11+ (venv) | — | — |
| HTTP | requests |
httpx |
Scrapy(단일 API 호출에 과잉) |
| HTML 파싱 | beautifulsoup4 + lxml |
Playwright(JS 렌더링 시) | Selenium |
| 데이터 프레임 | pandas |
— | — |
| 저장 | SQLite (sqlite3 표준 라이브러리) + JSON 스냅샷 파일 |
— | PostgreSQL, Elasticsearch |
| diff | 자체 구현 (csvdiff 스키마) | — | csvdiff 라이브러리(아카이브), changedetection.io |
| 리포트 | XlsxWriter |
— | openpyxl, Google Sheets API |
| 알림 | apprise + win11toast |
BurntToast(PowerShell) | win10toast, Windows-Toasts |
| 스케줄 | Windows Task Scheduler (schtasks / win32com.client) |
— | cron, GitHub Actions, Claude Desktop tasks |
| 서비스 | WinSW v2.12.0 | NSSM 2.24 | pywin32 ServiceFramework |
| AI CLI | Google Antigravity CLI agy -p |
(미사용 시 파이프라인은 정상 동작) | claude/gemini/codex (규약만 차용) |
| 설정 | YAML (config/*.yaml) + .env |
— | 하드코딩, 레지스트리 |
| 버전관리 | 로컬 git (data/snapshots 포함) |
— | 원격 push |
13. 최종 디렉터리 구조 제안
각 줄 끝의 ← 는 어느 저장소의 어느 레이아웃을 근거로 했는지를 표시한다.
D:\workspace\DMF_Crawler\
│
├─ README.md 프로젝트 개요·빠른 시작
├─ pyproject.toml 패키지 메타/의존성 ← FDA/openfda(setup.py), Tanguy9862(setup.py)
├─ requirements.txt 고정 의존성 (운영 재현용) ← Q00/data.go.kr-crawling, larsekje/PythonWindowsServices
├─ .env.example API 키 템플릿(실제 .env 는 .gitignore) ← Q00 의 config.py.example, NomaDamas/k-skill 의 키 분리 원칙
├─ .gitignore .env, data/raw, logs, reports 제외
│
├─ config\ ★ 설정은 전부 데이터. 코드에 상수 금지 ← FDA/openfda 의 config/, pharma-radar 의 config/
│ ├─ sources.yaml 소스 정의(엔드포인트·파라미터·필터·job_defaults) ← pharma-radar sources.yaml + urlwatch urls.yaml 스키마
│ ├─ schedule.yaml 태스크 정의(cron, timezone, timeout, logRetentionDays) ← claude-code-scheduler schedules.json
│ ├─ settings.yaml 전역 설정(알림 URL, 재시도, 임계값, 우선순위 규칙) ← autorunsalerts configuration.json
│ └─ field_map.yaml API 영문 필드 ↔ 리포트 한글 헤더 매핑 ← Q00 의 column.py 자동 생성 발상
│
├─ schemas\ ★ 데이터 계약을 코드와 분리해 버전 관리 ← FDA/openfda 의 schemas/
│ ├─ dmf_record.schema.json 정규화된 DMF 레코드 스키마
│ ├─ diff_result.schema.json diff 산출물 스키마(_index/added/removed/changed) ← csvdiff JSON 구조
│ └─ agent_output.schema.json agy --output-format json 에 넘길 JSON Schema ← drew.tech 의 --json-schema 패턴
│
├─ prompts\ ★ 에이전트 프롬프트는 자산이지 코드가 아니다 ← pharma-radar 의 prompts/system_prompt.md
│ ├─ system.md agy 시스템 프롬프트(YAML frontmatter + 본문) ← Claude Desktop 의 SKILL.md 형식
│ ├─ summarize_changes.md 변경 건 요약 프롬프트
│ └─ classify_priority.md 변경 우선순위(1~5) 분류 프롬프트 ← Zanalytix 의 priority 1-5
│
├─ src\
│ └─ dmf_crawler\ ★ python -m dmf_crawler <subcommand> ← nomenclator-delta 의 python3 -m nomenclator_delta
│ ├─ __init__.py
│ ├─ __main__.py CLI 진입점(argparse 서브커맨드)
│ ├─ cli.py daily / backfill / diff / report / notify-drain / validate / watchdog / doctor
│ ├─ settings.py config/*.yaml + .env 로딩, 경로 상수
│ │
│ ├─ fetch\ ★ 수집 (결정론) ← nomenclator-delta 의 collection
│ │ ├─ __init__.py
│ │ ├─ base.py BaseFetcher — 재시도·타임아웃·UA·디버그 덤프 ← newsdiffs BaseParser, playwright-scraping errors//browser//debug/
│ │ ├─ dmf_api.py data.go.kr getMdcDmfList01 페이지네이션 수집
│ │ ├─ nedrug_board.py nedrug.mfds.go.kr/bbs/117 공고 게시판 수집
│ │ └─ registry.py source_id → Fetcher 매핑 ← Zanalytix 의 fetch_{company}_html() 규약
│ │
│ ├─ parse\ ★ 정규화 ← nomenclator-delta 의 normalization
│ │ ├─ __init__.py
│ │ ├─ dmf_api.py JSON/XML → DmfRecord
│ │ ├─ nedrug_board.py HTML → BoardPost (css/xpath 규칙은 sources.yaml 에서) ← huginn WebsiteAgent 의 extract 선언
│ │ └─ normalize.py 업체명·주소·성분명 표기 정규화 ← Tanguy9862 의 회사명 1000→700 정규화
│ │
│ ├─ diff\ ★ 차이 판정 ← nomenclator-delta 의 diffing
│ │ ├─ __init__.py
│ │ ├─ engine.py 키 기반 added/removed/changed 산출 ← csvdiff diff_records()
│ │ ├─ modes.py all / on_change / merge ← huginn WebsiteAgent mode
│ │ └─ priority.py 변경 유형별 우선순위 1~5 부여 ← Zanalytix
│ │
│ ├─ store\ ★ 저장
│ │ ├─ __init__.py
│ │ ├─ db.py SQLite: snapshots / changes / runs / agent_runs ← Zanalytix db.py, Revised_MasterTable+Stream 분리
│ │ ├─ snapshots.py data/snapshots/ 읽기·쓰기·해시
│ │ └─ gitlog.py 스냅샷 디렉터리 자동 커밋 ← simonw/git-scraper-template
│ │
│ ├─ report\ ★ 리포트 ← Bwhiz report_script.py
│ │ ├─ __init__.py
│ │ ├─ xlsx.py XlsxWriter 멀티시트 + internal 링크 + add_table + conditional_format
│ │ ├─ sheets.py 시트별 빌더(요약/신규/변경/취하/전체현황/실행로그)
│ │ └─ styles.py 서식 상수(폰트·색·너비)
│ │
│ ├─ notify\ ★ 알림 ← Bwhiz auto_mail.py, pharma-radar src/deliver.py
│ │ ├─ __init__.py
│ │ ├─ queue.py state/notify_queue.json push/drain ← autorunsalerts 의 플래그 파일 패턴
│ │ ├─ toast.py win11toast (os.chdir 처리 포함)
│ │ └─ apprise_sink.py Apprise URL 리스트 전송, 250자 트리밍
│ │
│ ├─ orchestrate\ ★ 오케스트레이션
│ │ ├─ __init__.py
│ │ ├─ pipeline.py fetch→parse→diff→store→report→notify 순서 제어·부분 실패 허용
│ │ ├─ agy.py agy -p 서브프로세스 호출(프롬프트 파일·스키마·타임아웃·종료코드) ← drew.tech 스니펫
│ │ ├─ catchup.py 놓친 실행 1회 따라잡기 ← Claude Desktop missed-runs 규칙
│ │ └─ watchdog.py 태스크 존재·최근 성공 시각 감시 ← autorunsalerts + Badgerati/Hook
│ │
│ └─ util\
│ ├─ logging.py per-run 로그 + 누적 에러 로그 분리 ← newsdiffs
│ ├─ retry.py 지수 백오프 ← jjscan/data.go.kr-1 의 폴링 재시도
│ └─ hashing.py 스냅샷 해시
│
├─ ops\ ★ 운영 자산 ← FDA/openfda 의 scripts/, larsekje 의 windows_service/
│ ├─ winsw\
│ │ ├─ WinSW.exe (v2.12.0 배포본, .gitignore 대상)
│ │ ├─ dmf-watchdog.xml 서비스 정의 ← winsw samples/minimal.xml
│ │ └─ install.ps1 winsw install / start
│ ├─ tasks\
│ │ ├─ install.ps1 schtasks 3종 등록 ← autorunsalerts install.ps1
│ │ ├─ uninstall.ps1 제거 ← autorunsalerts uninstall.ps1
│ │ ├─ run_daily.cmd python -m dmf_crawler daily 래퍼 ← claude-code-scheduler 의 wrapper 생성 패턴
│ │ ├─ run_notify.cmd python -m dmf_crawler notify-drain
│ │ └─ run_catchup.cmd python -m dmf_crawler daily --catchup
│ ├─ agent\
│ │ ├─ bootstrap_agy.ps1 agy 미설치 시 설치·인증·프롬프트 창 표시
│ │ └─ run_agy.cmd agy -p "$(cat prompt)" --output-format json > out.json ← drew.tech
│ └─ doctor.ps1 환경 진단(파이썬, agy, WinSW, 태스크, 권한)
│
├─ data\ ★ .gitignore 대상 중 snapshots 만 로컬 git 추적 ← nomenclator-delta 의 data/
│ ├─ raw\ 원시 응답 원본 보존 (YYYY-MM-DD\<source>.json|html) ← jbremz masterDrugList2.csv, coderxio ./data/
│ ├─ snapshots\ 정규화된 시점 스냅샷 (YYYY-MM-DD.json) ← nomenclator-delta, Zanalytix 날짜별 JSON
│ ├─ history\ 일자별 diff 결과 (YYYY-MM-DD.diff.json)
│ ├─ debug\ 실패 시 HTML/스크린샷/trace ← playwright-scraping debug/
│ └─ dmf.sqlite3 운영 DB
│
├─ state\
│ ├─ state.json 마지막 성공 실행 시각·스냅샷 해시 ← autorunsalerts state.json
│ └─ notify_queue.json 대기 중 알림(사용자 컨텍스트 태스크가 소비)
│
├─ reports\ ★ 산출물 xlsx (YYYY-MM-DD_DMF_리포트.xlsx) ← nomenclator-delta 의 site/ 자리
│
├─ logs\ ← newsdiffs 로그 분리, claude-code-scheduler 의 task-id 별 로그
│ ├─ runs\ <task-id>-YYYYMMDD.log (per-run)
│ ├─ errors.log 누적 에러 (워치독 감시 대상)
│ └─ audit.log 누적 변경 이력 ← autorunsalerts audit.log
│
├─ tests\ ← nomenclator-delta tests/, claude-code-scheduler src/__tests__/
│ ├─ unit\
│ ├─ integration\
│ └─ fixtures\ 고정 응답 샘플(회귀 테스트용)
│
├─ notebooks\ 탐색·검증용 ← jbremz 의 .ipynb, suriyadeepan 의 notebooks/
│
├─ scripts\ 1회성 유틸 ← FDA/openfda scripts/
│ ├─ gen_field_map.py 공공데이터포털 명세에서 field_map.yaml 생성 ← Q00 의 url.py → column.py
│ └─ inspect_board.py nedrug 게시판 구조 실측
│
└─ docs\
├─ research\ ★ 본 문서 등 리서치 SSOT
│ ├─ 01-*.md
│ └─ 02-benchmark-github-projects.md
├─ ops\
│ ├─ runbook.md 일일 운영 절차·장애 대응 ← nomenclator-delta 의 monthly update runbook
│ └─ install.md 최초 설치 순서
└─ decisions\ ADR (Architecture Decision Record)
13.1 이 구조가 만족하는 요구사항 대조표
| 요구사항 | 이 구조에서 어디가 담당하는가 |
|---|---|
| 매일 06:00 크롤링 | ops/tasks/install.ps1 → DMF_Crawler_Daily → run_daily.cmd → orchestrate/pipeline.py |
| 신규·변경·취하 탐지 | diff/engine.py + diff/modes.py, 결과는 data/history/*.diff.json |
| 탭별 연동 xlsx | report/xlsx.py + report/sheets.py → reports/YYYY-MM-DD_DMF_리포트.xlsx |
| AI CLI headless | orchestrate/agy.py + prompts/*.md + schemas/agent_output.schema.json |
| agy 자동 부트스트랩 | ops/agent/bootstrap_agy.ps1 (미설치 시 설치, 필요 시 프롬프트 창) |
| 재부팅 후 자동 복구 | WinSW <startmode>Automatic</startmode> + <delayedAutoStart> + Task Scheduler 등록 지속 |
| 서비스 사망 시 알림 | orchestrate/watchdog.py → state/notify_queue.json → DMF_Crawler_Notify(사용자 컨텍스트) → notify/toast.py |
| 놓친 실행 복구 | DMF_Crawler_Catchup(로그온 시) → orchestrate/catchup.py |
| 재현 가능성 | data/raw 원본 보존 + data/snapshots git 커밋 + tests/fixtures |
14. 모듈 경계 제안과 입출력 계약
14.0 설계 원칙 (근거 저장소 명시)
- 결정론과 비결정론을 섞지 않는다 —
fetch/parse/diff는 순수 결정론. LLM 호출은orchestrate/agy.py한 곳에만. ←Tanguy9862의 scraper/classification 분리 - 소스별 어댑터는 동일 시그니처를 강제한다 ←
Zanalytix의fetch_{company}_html()/process_{company}_html(),suriyadeepan의fetch()/crawl_k() - 각 단계는 파일에 산출물을 남긴다 — 중간 산출물이 없으면 재현도, 부분 재실행도 불가능 ←
nomenclator-delta의data/ - AI 는 선택적 보강이다 — agy 가 실패해도 xlsx 는 나와야 한다 ←
pharma-radar의 표준 라이브러리 배포 계층 - 부분 실패를 허용한다 — 소스 하나가 죽어도 나머지는 진행 ← urlwatch 의
ignore_connection_errors
14.1 데이터 타입 정의 (공통 계약)
# src/dmf_crawler/types.py
from dataclasses import dataclass, field
from datetime import date, datetime
from typing import Any, Literal
SourceId = Literal["dmf_api", "nedrug_board"]
ChangeKind = Literal["added", "removed", "changed"]
@dataclass(frozen=True)
class RawPayload:
"""fetch 계층의 유일한 산출물. 파싱하지 않은 원본."""
source_id: SourceId
fetched_at: datetime
url: str
status_code: int
content_type: str # "application/json" | "text/html" | "application/xml"
body: bytes
meta: dict[str, Any] = field(default_factory=dict) # pageNo, totalCount 등
raw_path: str | None = None # data/raw/YYYY-MM-DD/<source>_<page>.json 로 저장된 경로
@dataclass(frozen=True)
class DmfRecord:
"""parse 계층의 산출물. 정규화된 DMF 1건."""
dmf_permit_no: str # DMF_PERMIT_NO ← 진짜 유일 키
ingr_kor_name: str # INGR_KOR_NAME 성분명
entp_name: str # ENTP_NAME 업체명 (정규화 적용됨)
mnfctr_name: str # MNFCTR_NAME 제조소명
mnfctr_place: str # MNFCTR_PLACE 제조소 소재지
manuf_country: str # MANUF_COUNTRY_CODE_NM 제조국가명
dmf_permit_date: date # DMF_PERMIT_DATE 발급일자
source_id: SourceId
raw: dict[str, Any] = field(default_factory=dict) # 원본 필드 전량 보존
@dataclass(frozen=True)
class BoardPost:
"""공고 게시판 1건."""
seq: int # 연번
title: str # 제목
view_count: int # 조회건수
registrant: str # 등록자
registered_on: date # 등록일자
detail_url: str
attachments: list[str] = field(default_factory=list)
@dataclass(frozen=True)
class FieldChange:
field_name: str
before: Any
after: Any
@dataclass(frozen=True)
class ChangeEvent:
kind: ChangeKind
key: str # dmf_permit_no
priority: int # 1~5 ← Zanalytix
record_after: DmfRecord | None
record_before: DmfRecord | None
fields: list[FieldChange] = field(default_factory=list)
@dataclass(frozen=True)
class DiffResult:
"""csvdiff 호환 구조."""
index: list[str] # == ["dmf_permit_no"] ← csvdiff _index
base_date: date # 이전 스냅샷 날짜
head_date: date # 이번 스냅샷 날짜
added: list[DmfRecord]
removed: list[DmfRecord]
changed: list[ChangeEvent]
ignored_columns: list[str] = field(default_factory=list)
@dataclass
class RunContext:
"""파이프라인 전체를 관통하는 실행 컨텍스트."""
run_id: str # "20260902-060000"
run_date: date
started_at: datetime
dry_run: bool = False
force: bool = False
catchup: bool = False
errors: list[str] = field(default_factory=list) # 부분 실패 누적
14.2 모듈별 입출력 계약
| 모듈 | 입력 | 출력 | 부수효과 | 실패 시 |
|---|---|---|---|---|
| fetch | source_id, config/sources.yaml, RunContext |
list[RawPayload] |
data/raw/YYYY-MM-DD/ 에 원본 저장, 실패 시 data/debug/ 에 덤프 |
ignore_connection_errors: true 면 빈 리스트 + ctx.errors 에 기록하고 계속. false 면 예외 |
| parse | list[RawPayload] |
list[DmfRecord] 또는 list[BoardPost] |
없음 (순수 함수) | 레코드 단위 실패는 스킵 + 카운트. 전체 실패율 20% 초과 시 예외 |
| diff | list[DmfRecord](head), list[DmfRecord](base), ignore_columns |
DiffResult |
없음 (순수 함수) | base 스냅샷이 없으면 DiffResult(added=전량, ...) 가 아니라 "기준선 수립" 모드로 종료 |
| store | list[DmfRecord], DiffResult, RunContext |
snapshot_path, diff_path |
data/snapshots/*.json, data/history/*.diff.json, SQLite 테이블, git commit |
DB 트랜잭션 롤백 후 예외 전파(이건 치명적) |
| report | DiffResult, list[DmfRecord](전체현황), RunContext |
report_path (xlsx 절대경로) |
reports/*.xlsx 생성 |
예외 전파. 단 notify 는 "리포트 생성 실패" 알림으로 계속 |
| notify | DiffResult 요약, report_path, ctx.errors |
None |
state/notify_queue.json 에 push, Apprise 전송 |
절대 예외를 전파하지 않는다(알림 실패로 파이프라인을 죽이지 않음). logs/errors.log 에만 기록 |
| orchestrate | CLI 인자, 설정 | 프로세스 종료 코드 | state/state.json 갱신, logs/runs/*.log |
종료 코드 0(성공) / 1(부분 실패) / 2(치명적 실패) |
14.3 함수 시그니처 (프로토콜)
# src/dmf_crawler/fetch/base.py
from typing import Protocol, Iterable
class Fetcher(Protocol):
source_id: str
def fetch(self, ctx: RunContext) -> Iterable[RawPayload]:
"""페이지네이션을 내부에서 처리하고 RawPayload 를 순차 yield 한다.
재시도·타임아웃·User-Agent 는 BaseFetcher 가 담당한다."""
...
# src/dmf_crawler/parse/base.py
class Parser(Protocol):
source_id: str
def parse(self, payloads: Iterable[RawPayload]) -> list[DmfRecord]:
...
# src/dmf_crawler/diff/engine.py
def compute_diff(
head: list[DmfRecord],
base: list[DmfRecord],
*,
key: str = "dmf_permit_no",
ignore_columns: list[str] | None = None,
) -> DiffResult:
"""csvdiff 와 동일한 의미론. key 로 조인하고 나머지 필드를 비교한다.
ignore_columns 에 든 필드는 changed 판정에서 제외한다."""
...
# src/dmf_crawler/diff/priority.py
def assign_priority(event: ChangeEvent) -> int:
"""1~5. 규칙은 config/settings.yaml 의 priority_rules 에서 읽는다."""
...
# src/dmf_crawler/report/xlsx.py
def build_report(
diff: DiffResult,
full_snapshot: list[DmfRecord],
ctx: RunContext,
out_path: str,
) -> str:
"""XlsxWriter 로 멀티시트 리포트를 만들고 절대경로를 반환한다."""
...
# src/dmf_crawler/notify/queue.py
def push(item: dict) -> None: ...
def drain() -> list[dict]: ...
# src/dmf_crawler/orchestrate/agy.py
def run_agy(
prompt_path: str,
schema_path: str | None = None,
*,
timeout_sec: int = 300,
cwd: str | None = None,
) -> dict | None:
"""agy -p 를 서브프로세스로 호출한다.
실패·타임아웃 시 None 을 반환하고 예외를 던지지 않는다(선택적 보강 원칙)."""
...
14.4 파이프라인 시퀀스
python -m dmf_crawler daily
│
├─ 0. settings 로드 (config/*.yaml + .env) 실패 → exit 2
├─ 1. RunContext 생성, logs/runs/<run_id>.log 열기
├─ 2. catchup 판정 (state.json 의 last_success 확인) 이미 오늘 성공 → exit 0
│
├─ 3. for source in sources.yaml:
│ fetch() → data/raw/ ─────────── 실패 & ignore_connection_errors → ctx.errors 추가 후 continue
│ parse() → list[DmfRecord] ────── 실패율 20% 초과 → exit 2
│
├─ 4. 레코드 병합 + normalize() → head_snapshot
├─ 5. store.snapshots.write(head_snapshot) → data/snapshots/YYYY-MM-DD.json
│ store.gitlog.commit()
│
├─ 6. base = store.snapshots.read(직전 날짜)
│ base 없음 → "기준선 수립" 토스트 후 exit 0
│
├─ 7. diff.compute_diff(head, base, ignore_columns=settings.ignore_columns)
│ diff.priority.assign_priority(each)
│ store.db.save_changes() → SQLite changes 테이블
│ → data/history/YYYY-MM-DD.diff.json
│
├─ 8. (선택) orchestrate.agy.run_agy(prompts/summarize_changes.md, schemas/agent_output.schema.json)
│ → 실패 시 None. 요약 없이 계속.
│
├─ 9. report.build_report() → reports/YYYY-MM-DD_DMF_리포트.xlsx 실패 → ctx.errors 추가
│
├─ 10. notify.queue.push({kind, counts, report_path, errors})
│ notify.apprise_sink.send() 실패해도 무시
│
└─ 11. state.json 갱신(last_success), 로그 닫기
exit 0 (ctx.errors 없음) / exit 1 (부분 실패)
14.5 SQLite 스키마 초안
Zanalytix 의 Revised_MasterTable(스냅샷) / Stream(변경+LLM 응답) 분리를 따른다.
-- 실행 이력
CREATE TABLE IF NOT EXISTS runs (
run_id TEXT PRIMARY KEY, -- '20260902-060000'
run_date TEXT NOT NULL, -- '2026-09-02'
started_at TEXT NOT NULL,
finished_at TEXT,
exit_code INTEGER,
error_count INTEGER DEFAULT 0,
report_path TEXT
);
-- 시점 스냅샷 (레코드 단위)
CREATE TABLE IF NOT EXISTS snapshots (
run_id TEXT NOT NULL,
snapshot_date TEXT NOT NULL,
dmf_permit_no TEXT NOT NULL,
ingr_kor_name TEXT,
entp_name TEXT,
mnfctr_name TEXT,
mnfctr_place TEXT,
manuf_country TEXT,
dmf_permit_date TEXT,
source_id TEXT NOT NULL,
raw_json TEXT NOT NULL, -- 원본 필드 전량
PRIMARY KEY (snapshot_date, dmf_permit_no)
);
CREATE INDEX IF NOT EXISTS idx_snapshots_key ON snapshots(dmf_permit_no);
-- 변경 이벤트
CREATE TABLE IF NOT EXISTS changes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id TEXT NOT NULL,
base_date TEXT NOT NULL,
head_date TEXT NOT NULL,
kind TEXT NOT NULL CHECK (kind IN ('added','removed','changed')),
dmf_permit_no TEXT NOT NULL,
priority INTEGER NOT NULL CHECK (priority BETWEEN 1 AND 5),
fields_json TEXT, -- [{"field_name":..,"before":..,"after":..}]
notified_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_changes_run ON changes(run_id);
CREATE INDEX IF NOT EXISTS idx_changes_key ON changes(dmf_permit_no);
-- AI 에이전트 호출 기록 (선택적 보강)
CREATE TABLE IF NOT EXISTS agent_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id TEXT NOT NULL,
prompt_path TEXT NOT NULL,
schema_path TEXT,
exit_code INTEGER,
duration_ms INTEGER,
output_json TEXT,
error_text TEXT
);
14.6 config/sources.yaml 초안
urlwatch 잡 스키마 + pharma-radar sources.yaml + huginn extract 선언을 합친 형태.
# job_defaults 는 urlwatch 의 개념 (모든 소스에 상속)
job_defaults:
timeout_sec: 30
retries: 3
backoff_sec: 2
user_agent: "DMF_Crawler/1.0 (+internal use)"
ignore_connection_errors: false
encoding: utf-8
sources:
- id: dmf_api
name: "식품의약품안전처_원료의약품등록(DMF)현황"
kind: api
url: "https://apis.data.go.kr/1471000/MdcDmfInfoService01/getMdcDmfList01"
method: GET
params:
serviceKey: "${DATA_GO_KR_API_KEY}" # .env 에서 주입
type: json
numOfRows: 100
pageNo: 1
pagination:
page_param: pageNo
size_param: numOfRows
page_size: 100
total_field: totalCount # totalCount == 0 이면 '데이터 없음' ← jjscan 교훈
formula: "int(totalCount/numOfRows) + 1" # ← Q00 의 공식
key_field: DMF_PERMIT_NO
fields:
- DMF_PERMIT_NO
- INGR_KOR_NAME
- ENTP_NAME
- MNFCTR_NAME
- MNFCTR_PLACE
- MANUF_COUNTRY_CODE_NM
- DMF_PERMIT_DATE
- id: nedrug_board
name: "의약품안전나라 원료의약품등록(DMF) 정보 게시판"
kind: html
url: "https://nedrug.mfds.go.kr/bbs/117"
method: GET
ignore_connection_errors: true # 게시판 장애로 파이프라인을 죽이지 않는다
# huginn WebsiteAgent 의 extract 선언 방식
extract:
seq: { css: "table tbody tr td:nth-child(1)" }
title: { css: "table tbody tr td:nth-child(2) a", value: "text" }
detail_url: { css: "table tbody tr td:nth-child(2) a", value: "@href" }
view_count: { css: "table tbody tr td:nth-child(3)" }
registrant: { css: "table tbody tr td:nth-child(4)" }
registered_on: { css: "table tbody tr td:nth-child(5)" }
filter: # urlwatch 필터 체인 개념
- strip
pagination:
page_size_options: [10, 20, 30, 40, 50]
total_posts_hint: 710 # 2026-09 실측 [F#28]
# ⚠️ 미검증: 위 CSS 선택자는 실제 DOM 으로 반드시 재확인해야 함
14.7 config/settings.yaml 초안
run:
timezone: "Asia/Seoul"
daily_at: "06:00"
timeout_sec: 1800 # ← claude-code-scheduler execution.timeout
log_retention_days: 30 # ← claude-code-scheduler settings.logRetentionDays
max_execution_history: 100
diff:
key: DMF_PERMIT_NO
ignore_columns: # ← csvdiff --ignore-columns
- fetched_at
- view_count
# 우선순위 규칙 ← Zanalytix priority 1-5
priority_rules:
removed: 5 # 등록 취하 — 가장 중요
added: 3 # 신규 등록
changed:
MNFCTR_NAME: 4 # 제조소 변경
MNFCTR_PLACE: 2 # 소재지 변경
ENTP_NAME: 4 # 업체 변경
INGR_KOR_NAME: 4 # 성분명 변경
_default: 1
notify:
urls:
- "windows://?duration=8" # ← apprise windows:// (pywin32 필요, 250자 제한)
# - "tgram://<bottoken>/<ChatID>"
# - "mailto://user:pass@company.co.kr"
max_body_chars: 250
toast:
engine: win11toast
open_report_on_click: true
min_priority_to_notify: 2 # 우선순위 1은 조용히 로그만
watchdog:
stale_after_hours: 26 # 마지막 성공이 26시간 넘으면 경보
check_interval_sec: 900
agent:
enabled: true
binary: "agy" # ⚠️ 미검증: 실제 실행파일명·플래그 실측 필요
print_flag: "-p"
output_format_flag: "--output-format json"
timeout_sec: 300
prompts:
summarize: "prompts/summarize_changes.md"
classify: "prompts/classify_priority.md"
schema: "schemas/agent_output.schema.json"
fail_open: true # 에이전트 실패 시 파이프라인 계속
15. 데이터 소스 실측 정보 (DMF API / 공고 게시판)
이 절은 raw dump 에서 확인된 한국 DMF 데이터 소스의 1차 사실을 손실 없이 보존한다. 벤치마킹 결과를 실제로 꽂을 자리이므로 본 문서에 포함한다.
15.1 식품의약품안전처_원료의약품등록(DMF)현황 OpenAPI [F#29]
출처: https://www.data.go.kr/data/15057075/openapi.do
| 항목 | 값 |
|---|---|
| API 명 | 식품의약품안전처_원료의약품등록(DMF)현황 |
| 서비스 URL | https://apis.data.go.kr/1471000/MdcDmfInfoService01 |
| 엔드포인트 | https://apis.data.go.kr/1471000/MdcDmfInfoService01/getMdcDmfList01 |
| 형식 | XML / JSON |
| 비용 | 무료 |
| 트래픽 제한(개발) | 10,000 calls |
| 최종 수정 | 2025년 9월 19일 |
| 제공기관 | 식품의약품안전처 |
요청 파라미터
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
serviceKey |
string | Yes | 데이터 포털에서 발급받은 인증키 |
pageNo |
integer | No | 페이지 번호 (기본 1) |
numOfRows |
integer | No | 페이지당 결과 수 (기본 3) |
entp_name |
string | No | 업체/제조사명 |
ingr_kor_name |
string | No | 성분명(한글) |
type |
string | No | 응답 형식: xml 또는 json |
응답 필드
| 필드 | 의미 |
|---|---|
DMF_PERMIT_NO |
등록번호 — 유일 키 |
INGR_KOR_NAME |
성분명 |
ENTP_NAME |
업체명 |
MNFCTR_NAME |
제조소명 |
MNFCTR_PLACE |
제조소 소재지 |
MANUF_COUNTRY_CODE_NM |
제조국가명 |
DMF_PERMIT_DATE |
발급일자 |
resultCode |
상태 코드 |
resultMsg |
상태 메시지 |
totalCount |
전체 결과 수 |
동작 가능한 호출 예 (numOfRows 기본값 3 에 주의 — 반드시 명시할 것)
# scripts/probe_dmf_api.py — 최초 실측용
import os
import requests
SERVICE_URL = "https://apis.data.go.kr/1471000/MdcDmfInfoService01/getMdcDmfList01"
def fetch_page(page_no: int, num_of_rows: int = 100) -> dict:
params = {
"serviceKey": os.environ["DATA_GO_KR_API_KEY"], # 디코딩된 키 사용
"type": "json",
"pageNo": page_no,
"numOfRows": num_of_rows,
}
resp = requests.get(
SERVICE_URL,
params=params,
timeout=30,
headers={"User-Agent": "DMF_Crawler/1.0 (+internal use)"},
)
resp.raise_for_status()
return resp.json()
def fetch_all(num_of_rows: int = 100) -> list[dict]:
first = fetch_page(1, num_of_rows)
body = first.get("body", first)
total = int(body.get("totalCount", 0))
if total == 0: # ← jjscan/data.go.kr-1 의 공백 판정 규칙
return []
pages = int(total / num_of_rows) + 1 # ← Q00/data.go.kr-crawling 의 공식
rows = list(body.get("items", []))
for page in range(2, pages + 1):
b = fetch_page(page, num_of_rows).get("body", {})
rows.extend(b.get("items", []))
return rows
if __name__ == "__main__":
records = fetch_all()
print(f"총 {len(records)}건")
if records:
print(records[0])
⚠️ 미검증 사항: 응답 JSON 의 실제 중첩 구조(response.body.items.item 인지 body.items 인지), serviceKey 의 인코딩/디코딩 키 구분, resultCode 정상값. 최초 실행 시 위 스크립트로 실측하고 schemas/dmf_record.schema.json 을 확정한다.
15.2 의약품안전나라 DMF 공고 게시판 [F#28]
출처: https://nedrug.mfds.go.kr/bbs/117
| 항목 | 값 |
|---|---|
| 게시판 | 원료의약품등록(DMF) 정보 |
| 테이블 컬럼 | 연번 / 제목 / 조회건수 / 등록자 / 등록일자 |
| 페이지네이션 | 처음 | 이전 | 1-10 | 다음 | 마지막, 총 710건 |
| 페이지당 표시 | 10, 20, 30, 40, 50 선택 |
| 검색 폼 | "제목" 검색 필드 + 검색/초기화 버튼 |
| 게시물 제목 패턴 | "등록대상 원료의약품(DMF) 등록 공고" + 특정 날짜 범위, 주 단위로 게시 |
| 표시된 샘플 범위 | 2021년 2월 ~ 2020년 11월 |
| 확인 사항 | 고급 필터나 "변경/취하" 전용 게시물 타입은 화면에 보이지 않음 |
중요: 게시판에는 "변경"/"취하" 를 별도 구분하는 UI 가 없다. 즉 신규/변경/취하 판정은 게시글 제목·첨부파일 내용이 아니라 API 스냅샷 diff 로 하는 것이 정공법이다. 게시판은 (a) 공고 게시 사실의 근거 링크, (b) API 반영 지연 시의 조기 신호 로 쓴다.
관련 확인:
- 한국보건산업진흥원(KHIDI) 제약산업정보포털에도 "등록대상 원료의약품(DMF) 등록 공고(7월 둘째주)" 같은 동일 공고가 게시된다 [S#11] — 보조 소스 후보
[지침]원료의약품 등록(DMF) 처리 절차문서 [S#11]- 식품의약품안전평가원 KDMF 페이지: https://www.nifds.go.kr/brd/m_87/list.do [S#11]
- 원료의약품 등록 현황(신규등록, 변경등록, 연차보고)은 식약처 홈페이지 전자민원창구에서도 확인 가능 [S#11] — ⚠️ 미검증, 별도 소스가 될 수 있음
실측 실패: https://nedrug.mfds.go.kr/searchDmf 는 "The requested page cannot be found. The page you are looking for has been changed or is currently unavailable." 에러 페이지를 반환했다 [F#63]. DMF 검색 화면의 실제 경로는 브라우저로 재확인 필요.
15.3 관련 식약처/공공데이터 소스 (보조·참고)
15.4 이 소스들을 벤치마크 구조에 꽂는 방법
config/sources.yaml
├─ dmf_api (1차, 필수) → fetch/dmf_api.py → parse/dmf_api.py → DmfRecord
└─ nedrug_board (2차, 선택) → fetch/nedrug_board.py → parse/nedrug_board.py → BoardPost
ignore_connection_errors: true
diff/engine.py
key = DMF_PERMIT_NO
base = data/snapshots/<어제>.json
head = data/snapshots/<오늘>.json
→ added / removed / changed
검증 정책 (← pharma-radar 의 "≥2 독립 출처 또는 1 공식 1차 출처")
· API 에만 나타난 변경 → 리포트에 '확정' (공식 1차 출처)
· 게시판에만 나타난 공고 → 리포트에 '관찰 중(pending)'
· 양쪽 모두 일치 → 리포트에 '확정 + 공고 링크'
부록 A. 출처 목록
raw dump 에 등장한 모든 URL 이다. "확인" 열의 의미: F#n = WebFetch 로 실제 열어봄 / S#n = 검색 결과 링크로만 등장 / 404 = 열었으나 존재하지 않음.
A.1 GitHub 저장소·페이지
A.2 GitHub Topics
A.3 Gist
| 제목 | URL | 확인 |
|---|---|---|
| pywin32 서비스 예제 (drmalex07) | https://gist.github.com/drmalex07/10554232 | F#40 |
| drugs@fda scraper (seanherron) | https://gist.github.com/seanherron/5997278 | S#3 |
| drugs@fda scraper (단축) | https://gist.github.com/5997278 | S#19 |
| Windows Task Scheduler 상호작용 스크립트 (nmpowell) | https://gist.github.com/nmpowell/dc8e7187948788c5c126f01755252164 | S#29 |
| Windows 10 토스트 생성 (hygull) | https://gist.github.com/hygull/32a742339a416dcfa2990504c848c1a9 | S#9 |
| Gemini CLI Job (HainanZhao) | https://gist.github.com/HainanZhao/92b43e68850189bfee8f39a2c2581ca6 | S#20 |
A.4 한국 식약처 / 공공데이터
A.5 FDA (미국) 및 상용 DMF 데이터
A.6 공식 문서 (도구·라이브러리)
A.7 블로그·튜토리얼·기사
부록 B. 미해결 질문 / 실측 필요 항목
B.1 최우선 (설계를 바꿀 수 있는 것)
agy(Google Antigravity CLI) 의 실제 CLI 표면을 실측한다. raw dump 는agy를 전혀 조사하지 않았다.agy --help로 다음 4가지를 확인하고docs/research/에 기록: ① 비대화형 프롬프트 플래그(-p?), ② JSON 출력 플래그, ③ JSON Schema 강제 옵션 존재 여부, ④ 승인/샌드박스 우회 플래그. 없으면config/settings.yaml의agent.*를 전면 수정해야 한다.agy설치 경로·인증 방식·환경변수를 확인한다.claude는ANTHROPIC_API_KEY(bare 모드),gemini는GEMINI_SYSTEM_MD등을 쓴다.agy의 대응물이 무엇인지, 그리고 스케줄드 태스크(비대화형 세션)에서 인증이 유지되는지 — drew.tech 이 지적한 "snapshots preserve auth state" 문제와 동형.agy미설치 시 자동 부트스트랩이 가능한지. 요구사항에 "agy 가 없으면 자동 설치하고 필요하면 Windows 프롬프트 창을 띄운다"가 있으나, 무인 설치 경로(패키지 관리자? 설치 스크립트?)가 확인되지 않았다.- DMF OpenAPI 응답의 실제 JSON 중첩 구조.
response.body.items.item인지body.items인지.§15.1의probe_dmf_api.py로 실측 후schemas/dmf_record.schema.json확정. serviceKey의 인코딩/디코딩 키 구분. data.go.kr 은 두 종류를 발급한다.requests의params=로 넘길 때 어느 쪽이 맞는지 실측.resultCode정상값과 에러 코드 목록. 재시도 대상 에러와 즉시 실패 에러를 구분해야 한다.- DMF 현황 전체 레코드 수(
totalCount).jjscan/data.go.kr-1이 351,010건에 17시간을 썼다. DMF 가 몇 건인지에 따라 백필 전략(병렬 여부)이 달라진다. nedrug.mfds.go.kr/bbs/117이 정적 HTML 인지 JS 렌더링인지. 정적이면requests+ BeautifulSoup, 아니면 Playwright 도입.scripts/inspect_board.py로 실측.- 게시판 페이지네이션 파라미터. 총 710건, 페이지당 10/20/30/40/50 선택 가능하다는 것만 확인됨. 실제 쿼리스트링(
page=?pageNo=? POST?)은 미확인. §14.6의 CSS 선택자 전량.table tbody tr td:nth-child(n)는 추정치다. 실제 DOM 으로 반드시 교체.
B.2 데이터 소스 (2차)
https://nedrug.mfds.go.kr/searchDmf의 올바른 경로. WebFetch 가 에러 페이지를 반환했다 [F#63]. DMF 검색 UI 가 실제로 존재하는지, 있다면 URL 은 무엇인지.- 식약처 전자민원창구의 "원료의약품 등록 현황(신규등록, 변경등록, 연차보고)" [S#11] 이 별도 소스로 쓸 만한지. 특히 "변경등록"이 명시적으로 구분되어 있다면 diff 없이도 변경 사유를 얻을 수 있다.
- KHIDI 제약산업정보포털 공고가 nedrug 게시판보다 빠른지/늦은지. 빠르면 조기 신호 소스로 추가.
식품의약품안전처_의약품 제품 허가정보API 의 엔드포인트 URL 과 파라미터. 페이지에 명시되지 않았다 [F#54].CANCEL_DATE/CANCEL_NAME/CHANGE_DATE필드가 있으므로 취하/변경 판정의 보조 근거가 될 수 있다.- DMF API 의 갱신 주기. "Last modified September 19, 2025" 만 확인됨. 일 단위인지 주 단위인지에 따라 06:00 실행의 의미가 달라진다(주 단위면 대부분의 실행이 no-change).
ENTP_NAME/MNFCTR_NAME의 표기 흔들림 실태.Tanguy9862는 회사명 1000종→700종 정규화가 필요했다. 우리도 샘플 1,000건으로 실태 조사 후parse/normalize.py규칙 작성.
B.3 Windows 운영
- WinSW
samples/minimal.xml의 필수 요소 확인.§10.1의 XML 초안에서<id>/<name>/<description>이 필수인지,<arguments>와<startarguments>를 언제 쓰는지. - WinSW v2.12.0 과 v3.0.0-alpha.11 중 어느 것을 쓸지. alpha 는 위험하지만
docs/xml-config-file.md는 v3 브랜치 문서다. v2 에서 동일 요소가 지원되는지 확인 필요 — 특히<delayedAutoStart>,<priority>,<resetfailure>. - WinSW 실행에 .NET 런타임이 필요한지, 어느 버전인지. Windows 11 기본 탑재로 충분한지.
schtasks /create의 정확한 옵션./ru(실행 사용자),/rp(비밀번호),/rl HIGHEST,/np(비밀번호 저장 안 함),/it(대화형)의 조합. 특히 사용자 컨텍스트 토스트 태스크는/it가 필요한지.- 사용자가 로그오프 상태일 때 06:00 태스크가 도는지.
/ru SYSTEM이면 돌지만 토스트는 못 띄운다 — 그래서 큐 파일 방식을 택했으나, 실제 동작 검증 필요. win11toast의os.chdir()함정 재현. "실행 시 CWD 가C:\Windows\system32" [F#67] 가 Task Scheduler 실행 시에도 발생하는지.- Apprise
windows://가 pywin32 를 통해 실제로 토스트를 띄우는지, 250자 제한이 어떻게 잘리는지. - PC 절전/최대절전 시 Task Scheduler 의 "작업을 실행하기 위해 절전 모드 해제" 옵션을 켤 것인지. Claude Desktop 문서가 지적한 "컴퓨터가 자면 실행이 스킵된다" 문제의 Windows 네이티브 해법.
B.4 리포트
- XlsxWriter
autofilter()/freeze_panes()의 정확한 시그니처. [F#58] 에서 확보 실패. 문서 재확인 필요. - 한글 시트명에
write_url('internal:...')가 정상 동작하는지. 문서는 "Worksheet names with spaces should be single quoted" 만 언급. 한글 + 공백 조합 실측 필요. - 엑셀에서 열었을 때 internal 링크가 실제로 클릭되는지 (Excel 버전별, LibreOffice 호환성).
- 리포트 파일명 규칙 확정.
YYYY-MM-DD_DMF_리포트.xlsx로 할 경우 한글 파일명이win11toast의on_click으로 열릴 때 문제가 없는지.
B.5 diff / 데이터 모델
DMF_PERMIT_NO가 진짜 불변 유일 키인지. 재발급·번호 변경 사례가 있으면 diff 가 대량 오탐을 낸다. 과거 스냅샷 2개를 확보해 검증.- "취하"가 API 에서 어떻게 표현되는지. 레코드가 사라지는가(→
removed), 아니면 상태 필드가 바뀌는가(→changed)? 전자라면 API 일시 장애 시 전량removed오탐이 발생하므로, 레코드 수가 전일 대비 N% 이상 감소하면 diff 를 중단하는 안전장치가 필수다. ignore_columns에 넣어야 할 노이즈 필드 실태. 조회수 외에 무엇이 매일 바뀌는지 1주일 관측.- 우선순위 규칙(
§14.7priority_rules)의 타당성을 실무자에게 확인.
B.6 검색 예산 소진으로 미완인 조사
WebSearch 예산(200/200)이 소진되어 다음 3개 질의가 수행되지 않았다. 필요 시 WebFetch + Bing/DuckDuckGo 로 보완:
github 식약처 공고 크롤링 텔레그램 알림 스케줄러 파이썬 회수 판매중지[S#31 미수행]github xlsxwriter multi-sheet report internal hyperlink summary sheet python generator repository[S#32 미수행]github Playwright python Windows Task Scheduler headless daily scrape report "pythonw" OR "schtasks" repository[S#33 미수행]
추가로 확인하지 못한 것:
urlwatch의diff_tool옵션 — Filters 페이지에 없었다 [F#50]. Configuration 페이지 확인 필요.- Huginn 의
ChangeDetectorAgent,DeDuplicationAgent,DigestAgent,EmailDigestAgent,SchedulerAgent,ShellCommandAgent설명 — wiki 로딩 실패 [F#65]. jshchnz/claude-code-scheduler의src/schedulers/windows.ts실제 구현 — schtasks 를 어떻게 호출하는지 [F#66 은 파일명만 확인].- FDA DMF 목록 스프레드시트의 실제 다운로드 URL — fda.gov 두 페이지가 모두 404 [F#48][F#53].
FDA/openfda,changedetection.io,huginn,urlwatch등의 정확한 최근 커밋 날짜 — GitHub 페이지에서 텍스트로 노출되지 않아 커밋 총수로 대체 기록했다.
B.7 라이선스·규범
- 참고한 저장소 중 라이선스 미확인 항목(
§11.4표의 "미확인" 행)을 코드 참고 전에 확인. - 식약처 공공데이터 이용약관 — 저장·재배포·사내 공유 범위. data.go.kr 활용 신청 시 "운영" 등급으로 승격하려면 활용 사례 등록이 필요하다 [F#54].
- 크롤링 빈도 예의(rate limit) — 개발 등급 10,000 calls 제한 [F#29] 안에서 일일 실행이 몇 콜을 쓰는지 계산하고, 게시판 스크래핑에는 요청 간 지연을 둘 것.