Skip to main content
Glama
sourcing
by m2nho

sourcing

구글 맵에서 병원·클리닉의 WhatsApp 연락처를 수집하는 CLI.

설치

Windows

mise는 Windows 지원이 제한적이라 쓰지 않는다. uv가 Python 3.13을 알아서 받아온다 (.python-version을 읽는다).

PowerShell에서:

# uv 설치 (이미 있으면 건너뛴다)
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"

git clone https://github.com/m2nho/sourcing.git
cd sourcing

uv sync                              # Python 3.13까지 알아서 받아온다
uv run playwright install chromium   # 브라우저 (~200MB)

uv run pytest                        # 145개 통과하면 준비 완료

uv를 설치한 창에서는 PATH가 아직 갱신되지 않았을 수 있다. 터미널을 새로 열고 uv --version이 되는지 확인한다.

macOS · Linux

mise install                        # Python 3.13 (mise.toml)
uv sync
uv run playwright install chromium
uv run pytest

mise가 없으면 생략해도 된다 — uv sync가 Python을 알아서 잡는다.

Related MCP server: medical-hospital-mcp-server

사용

도시 하나를 제대로 훑기 (권장)

구글 맵은 뷰포트가 넓으면 같은 곳만 반복해서 돌려준다. 넓은 도시는 지구 단위로 나눠 돌리고 합쳐야 한다. 실측: 런던을 반경 10km로 한 번 돌리면 120곳, 8개 지구로 나누면 667곳이 나왔다.

scripts/sweep scripts/districts-london.txt "aesthetic clinic" GB london

지구 목록은 이름|위도,경도|반경km|셀km 형식이다(scripts/districts-london.txt 참고). 순차로 돌고 마지막에 자동으로 합친다. 이미 결과가 있는 지구는 건너뛰므로 중간에 멈춰도 다시 실행하면 이어서 간다.

한 곳만 돌리기

uv run sourcing "klinik" --region ID --lang id --center=-6.2,106.8167
uv run sourcing "aesthetic clinic" --region GB --center=51.5205,-0.1490 --radius-km 2 --cell-km 1.5

음수 좌표는 --center=처럼 =로 붙인다. 안 그러면 argparse가 옵션으로 오해한다.

--cell-km이 "검색 한 번이 몇 km를 볼지"다. 기본 4km이고 격자는 여기서 자동 계산된다. 클리닉이 밀집한 중심가는 1.5km까지 좁힌다.

--center 없이 실행하면 구글이 실행 위치 IP를 기준으로 검색한다. 특정 지역을 노리려면 좌표를 주거나 키워드에 지역명을 넣는다.

여러 결과 합치기

uv run sourcing-merge out/london-*.raw.jsonl --out out/london-ALL.xlsx

지구 경계에 걸친 곳은 중복되므로 반드시 합쳐서 하나의 엑셀로 쓴다. 같은 곳이 여러 번 나오면 근거가 더 강한 쪽을 남긴다.

자세한 판단 기준은 .claude/skills/lead-sourcing/SKILL.md에 있다.

WhatsApp 상태

구글 맵에는 WhatsApp 필드가 없다. 이 도구는 근거를 밝힌 세 단계로 표시한다.

상태

근거

confirmed

업체가 스스로 선언한 wa.me 링크에서 나온 번호 — 맵의 웹사이트 필드 또는 홈페이지에서 찾는다

verified

선언은 없지만 WhatsApp 프로필 조회로 확인된 번호. 프로필 이름이 상호와 맞는 것만

candidate

대표번호가 모바일 번호대 — 동남아에서는 대부분 WhatsApp이다. 추측이므로 wa_link를 채우지 않는다

unlikely

유선이거나 번호가 없다

웹사이트 훑기

기본으로 켜져 있다. 장소에 웹사이트가 있으면 그 페이지를 브라우저로 열어 wa.me / api.whatsapp.com 링크를 찾고, 찾으면 confirmed로 승격시킨다.

맵 리스팅만으로는 확정이 거의 안 나오기 때문이다 — 실측 709건 중 1건이었다. 업체가 WhatsApp을 대표 채널로 쓰더라도 맵의 웹사이트 칸에는 홈페이지 주소를 넣고, wa.me 링크는 그 홈페이지 안(푸터·플로팅 버튼)에 둔다.

번호가 여러 개 나오면 각각을 별개 레코드로 저장한다. 부서·지점별로 번호를 따로 두는 곳이 실제로 있다. 추가 번호는 place_cid에 #1, #2가 붙어 중복 제거와 재개가 그대로 작동한다.

정적 HTML이 아니라 브라우저로 렌더링해서 받는다. WhatsApp 버튼을 JS 위젯으로 삽입하는 사이트가 흔해서, 원본 HTML만 봐서는 링크가 보이지 않는다(실측: 한 클리닉에서 정적 3개 → 렌더링 후 4개).

본문에 그냥 적힌 번호는 쓰지 않는다. "WA:" 같은 라벨로 추정할 수는 있지만 그것은 추측이고, 이 단계의 목적은 추측이 아니라 선언을 읽는 것이다.

사이트당 한 번 요청하므로 수집 시간이 대략 두 배가 된다. --no-crawl로 끄면 맵 정보만 쓰고 훨씬 빠르지만 confirmed는 거의 나오지 않는다.

미국·캐나다에서는 candidate가 나오지 않는다

북미번호계획은 지역번호로 유선/모바일을 나누지 않아 모든 번호가 "구분 불가"로 분류된다. 그 유형을 후보로 올리면 전부 후보가 되어 아무것도 걸러주지 못하므로 (실측: 마이애미 클리닉 304건 중 267건), +1 번호는 confirmed만 리드가 된다. 즉 미국에서는 웹사이트 훑기가 사실상 유일한 경로다.

Claude Desktop · Codex에서 쓰기

MCP 서버로 노출돼 있다. 두 클라이언트 모두 stdio MCP를 쓰므로 같은 서버를 쓴다.

Claude Code는 저장소의 .mcp.json을 그대로 읽으므로 별도 설정이 필요 없다.

Claude Desktop은 claude_desktop_config.json에 아래를 넣는다 (macOS ~/Library/Application Support/Claude/, Windows %APPDATA%\Claude\):

{
  "mcpServers": {
    "sourcing": {
      "command": "uv",
      "args": ["--directory", "<이 저장소를 클론한 절대경로>", "run", "sourcing-mcp"]
    }
  }
}

Codex는 저장소의 .codex/config.toml을 그대로 쓴다 — 클론한 디렉터리에서 codex를 실행하면 바로 잡힌다. 전역으로 등록하려면 둘 중 하나를 쓴다.

codex mcp add sourcing -- uv --directory <클론한 절대경로> run sourcing-mcp

또는 ~/.codex/config.toml에 직접:

[mcp_servers.sourcing]
command = "uv"
args = ["--directory", "<클론한 절대경로>", "run", "sourcing-mcp"]

--directory로 프로젝트 경로를 명시해야 uv가 이 프로젝트의 가상환경을 찾는다.

Windows 경로는 JSON·TOML에서 역슬래시를 두 번 쓰거나 슬래시로 적는다: "C:\\Users\\me\\sourcing" 또는 "C:/Users/me/sourcing".

툴

툴

하는 일

start_collection

수집 시작. 즉시 반환한다 — 실제 수집은 20~40분 걸린다

check_collection

진행 상황과 현재까지의 집계

list_collections

이 세션의 작업 목록

cancel_collection

중단. 수집한 레코드는 남아 재개할 수 있다

get_leads

결과를 걸러서 읽는다. 레코드 전체가 아니라 필요한 만큼만

export_excel

엑셀을 다시 뽑는다. 취소된 작업·예전 데이터·수집 도중 중간 결과

check_site_whatsapp

사이트 한 곳만 확인. 몇 초면 끝난다

수집이 오래 걸리므로 작업 방식으로 만들었다. 툴이 40분을 붙들고 있으면 클라이언트가 타임아웃되고 그동안 대화도 막힌다. start_collection은 job_id만 주고 바로 돌아오며, 진행률은 JSONL 파일에서 읽는다 — 레코드마다 flush되므로 그 파일이 곧 실시간 진행률이다.

get_leads가 레코드 전체를 돌려주지 않는 것도 같은 이유다. 200건을 컨텍스트에 쏟으면 토큰만 태운다. 요약과 집계를 주고, 필요한 만큼만 잘라 읽게 한다.

동시에 하나의 수집만 돈다. 브라우저가 하나뿐이고 동시 접속은 차단 위험을 키운다.

출력 파일

수집이 끝나면 세 파일이 나온다.

파일

용도

*.xlsx

영업용 목록. 병원명·위치·전화번호·WhatsApp 링크·상태·근거 여섯 컬럼. 연락 가능한 곳만, 확정을 위로 정렬

*.csv

전체 레코드 원본. 모든 컬럼 보존

*.raw.jsonl

재개용 원장. 레코드마다 즉시 flush된다

근거 컬럼

확정 안에도 신뢰도가 다른 것들이 섞인다. 어디부터 걸지 정할 수 있도록 출처를 남긴다.

근거

의미

홈페이지+맵 일치

홈페이지의 wa.me가 맵 대표번호와 같다 — 가장 강한 근거

홈페이지 링크

홈페이지에서 찾았고 맵에는 없던 번호. 실측상 확정의 3분의 2가 여기

구글맵 링크

맵 웹사이트 필드가 wa.me였다. 드물다 (709건 중 1건)

맵 번호 추정

맵 대표번호가 모바일이라는 추정뿐. 검증되지 않았다

WhatsApp 프로필 조회

기본으로 켜져 있다. 수집한 번호를 wa.me에서 열어 프로필 이름이 뜨는지 본다. 등록된 비즈니스 계정이면 상호가 보이고, 미등록이거나 개인 계정이면 번호만 보인다. 메시지는 보내지 않는다 — 공개 페이지를 여는 것뿐이다.

이름이 상호와 맞으면 추측(candidate)이나 버려진 것(unlikely)을 verified로 올린다. 이미 선언이 있는 confirmed는 그대로 두고 프로필 이름만 참고로 남긴다. 이름이 안 맞으면 등급을 올리지 않되 이름은 기록한다 — 남의 번호일 수 있다는 신호다.

실측(런던 아이스테틱 120곳):

프로필 확인율

confirmed 20건 표본

80%

candidate 15건 표본

53%

버려졌던 unlikely 45건

9% (4건 회수)

candidate의 절반은 실제로 등록돼 있지 않았다. 이 단계가 없으면 추측과 확인을 구분할 수 없다.

이름이 안 뜬다고 미등록인 것은 아니다. 비즈니스 프로필 없이 개인 계정으로 쓰면 번호만 보인다 — 이 수치는 늘 하한이다.

--no-verify로 끄면 건당 3초를 아낀다.

한계

  • 구글 맵 검색 하나는 100~120건에서 잘린다. 지역 전수에 가깝게 가려면 --center/--radius-km/--grid로 격자를 쪼개야 한다.

  • 자동화된 스크래핑이라 구글이 확인 절차를 요구할 수 있다. 그때는 --headful로 실행해 창에서 직접 통과하면 세션이 프로필에 남는다.

  • 중단해도 *.raw.jsonl에 즉시 기록되므로 같은 명령을 다시 실행하면 이어서 받는다.

  • reviews(리뷰 수) 컬럼은 현재 채워지지 않는다. 패널 HTML을 캡처하는 시점에 평점 블록의 리뷰 수 컨테이너가 비어 있어 파서가 가져올 값이 없다. 알려진 한계이며 후속 작업 대상이다 — CRM에 빈 값으로 들어가도 버그가 아니다.

개발

uv run pytest

maps.py(브라우저 계층)를 뺀 나머지는 전부 네트워크 없이 테스트된다. 구글이 DOM을 바꾸면 parse.py의 셀렉터 상수와 tests/fixtures/만 갱신하면 된다.

Available Tools

7 tools
cancel_collectionB

돌고 있는 수집을 중단한다. 그때까지 수집한 레코드는 그대로 남는다.

Inputs: job_id (required).

check_collectionA

수집 작업의 진행 상황과 현재까지의 집계를 돌려준다.

Inputs: job_id (required).

check_site_whatsappA

웹사이트 한 곳을 열어 선언된 WhatsApp 번호를 찾는다. 몇 초면 끝난다.

업체가 href에 넣어둔 wa.me / api.whatsapp.com 링크만 읽는다. 본문에 적힌 맨 번호는 WhatsApp인지 알 수 없으므로 쓰지 않는다.

Inputs: url (required).

export_excelA

수집 결과를 엑셀(.xlsx)로 다시 뽑는다.

수집이 정상으로 끝나면 엑셀은 자동 생성된다. 이 툴은 그러지 못한 경우를 위한 것이다 — cancel_collection으로 중단해 마무리 코드에 도달하지 못했거나, 예전에 모은 데이터를 지금 형식으로 다시 만들고 싶을 때.

재개 원장(.raw.jsonl)에서 만들므로 수집 도중에도 현재까지의 결과를 뽑을 수 있다.

Args: job_id: start_collection이 준 작업 id. csv_path: 결과 CSV 경로. job_id 대신 쓸 수 있고, 이전 세션의 결과에 쓴다. out_path: 저장할 .xlsx 경로. 비우면 CSV와 같은 이름으로 나란히 만든다.

Inputs: job_id, csv_path, out_path.

get_leadsA

수집 결과를 읽는다. 레코드 전체가 아니라 필요한 만큼만 돌려준다.

엑셀 파일(.xlsx)은 수집이 끝나면 CSV와 나란히 자동 생성된다. 병원명·위치· 전화번호·WhatsApp 링크·상태·근거 여섯 컬럼이며 연락 가능한 곳만 담는다.

Args: job_id: start_collection이 준 작업 id. csv_path 대신 쓸 수 있다. csv_path: 결과 CSV 경로. 이전 세션의 결과를 읽을 때 쓴다. status: 'confirmed'(업체가 선언한 확정 번호) / 'candidate'(모바일 번호 추측) / 'unlikely'. 비우면 전부. limit: 최대 반환 건수.

Inputs: limit, job_id, status, csv_path.

list_collectionsB

이 세션에서 시작한 수집 작업들.

Inputs: no named parameters.

start_collectionA

구글 맵에서 병원·클리닉을 수집하고 WhatsApp 연락처를 찾는 작업을 시작한다.

즉시 반환한다. 실제 수집은 20~40분 걸리므로 check_collection으로 진행을 확인하라.

Args: keyword: 검색어. 현지어가 훨씬 잘 나온다 (인니 'klinik', 베트남 'phòng khám'). 실측상 종합병원('rumah sakit')보다 클리닉('klinik')의 수확률이 5배 높다. region: 전화번호 정규화 기준 ISO 국가코드 (ID, VN, PH, US ...). lat: 검색 중심 위도. 도시명을 좌표로 바꿔서 넘겨라. lng: 검색 중심 경도. radius_km: 격자가 덮을 반경. grid: 한 변의 타일 수. 구글 맵은 검색당 약 120건에서 잘리므로 넓은 지역은 격자로 쪼갠다. limit: 수집할 최대 장소 수. 0이면 제한 없음. lang: 구글 맵 UI 언어. crawl: 웹사이트를 훑어 WhatsApp 링크를 찾을지. 끄면 훨씬 빠르지만 confirmed가 거의 나오지 않는다 (실측 0.5% 대 47%).

Inputs: lat (required), lng (required), region (required), keyword (required), +5 more.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.9/5.0

    Scored across 7 tools

    Disambiguation4/5

    The async job lifecycle tools (start/check/list/cancel_collection) are clearly distinct, as are get_leads and export_excel. The only mild confusion is the shared 'check_' prefix on check_collection (job progress) and check_site_whatsapp (single website lookup), and list_collections overlapping somewhat with check_collection in purpose, but descriptions resolve these adequately.

    Naming Consistency4/5

    All tools follow a verb_noun snake_case pattern (start_collection, cancel_collection, get_leads, export_excel), which is predictable and readable. Minor deviations: list_collections uses plural while other collection tools use singular, and check_site_whatsapp is a compound noun rather than a simple object.

    Tool Count5/5

    Seven tools is well-scoped for the stated purpose of bulk Google Maps lead collection. Each tool earns its place: four for the async job lifecycle, one for reading results, one for single-site WhatsApp verification, and one for Excel export recovery.

    Completeness5/5

    The tool set fully covers the collection lifecycle: start, check progress, list jobs, cancel (with preserved partial results), read leads, verify a single site, and export to Excel even after cancellation. Cross-session access via csv_path is a thoughtful touch that prevents dead ends.

    Maintenance

    ActivityStale
    ResponsivenessOutdated data

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables comprehensive LinkedIn profile search, data extraction, and contact enrichment using Google Search, Apollo.io, and AI-powered analysis. Includes automated data mining workflows with database storage and CSV export capabilities.
      3
      ISC
    • F
      license
      Not graded
      quality
      C
      maintenance
      Enables hospital search and detail lookup to discover medical institutions and verify campuses, departments, contacts, introductions, and operating entities.
      1
      -