GITHUB 레포 딥다이브 · 2026-07-29 · BUILDERIO/GPT-CRAWLER · 사이트를 커스텀 GPT용 지식 파일로

BuilderIO/gpt-crawler 딥다이브
웹사이트를 크롤링해서 커스텀 GPT용 지식 파일 하나로

gpt-crawler는 하나(혹은 사이트맵 URL)를 출발점 삼아 사이트를 헤드리스 브라우저로 돌아다니며 본문 텍스트를 긁어모은 뒤, 그 결과를 JSON 지식 파일로 묶어주는 Node.js·TypeScript 도구다. 목적은 아주 좁고 명확하다 — 이렇게 만든 파일을 ChatGPT의 커스텀 GPT나 OpenAI Assistants API의 "지식(Knowledge)" 업로드 칸에 넣어, 특정 문서 사이트만 훤히 꿰고 있는 나만의 챗봇을 만드는 것.

내부는 크롤링 자동화 라이브러리 CrawleePlaywrightCrawler(실제 브라우저를 원격 조종해 페이지를 여는 크롤러)를 그대로 가져다 쓰고, 그 위에 "CSS/XPath 셀렉터로 본문만 뽑기"와 "토큰·용량 기준으로 결과 파일을 자동으로 쪼개기"라는 얇지만 실용적인 두 겹의 로직만 얹은 구조다. CLI 대화형 실행, 로컬 스크립트 실행, Express API 서버, Docker 컨테이너까지 네 가지 실행 방식을 모두 지원한다.

(저장소 BuilderIO/gpt-crawler · 언어 TypeScript · 라이선스 ISC · package.json 버전 1.5.1 · 핵심 의존성 Crawlee 3.6.x + Playwright 1.40.x(lockfile 기준) · 최신 커밋 2025-07-07, 해시 d2245d6(PR #399 머지) — 이 문서 작성 시점 기준 약 1년간 신규 커밋 없음)
목차
  1. 한 줄 정체
  2. 왜 뜨는가 — 그리고 지금은 어떤 상태인가
  3. 무엇을 해결하나
  4. 전체 구조
  5. 핵심 설계·동작 원리
  6. 기술 스택·의존성
  7. 설치·요구사항
  8. 실제 사용 흐름
  9. 함정·오해 방지
  10. 대안 비교
  11. 정리 — 누가 쓰면 좋은가

1한 줄 정체

"사이트를 대신 읽어와 GPT가 먹기 좋은 JSON으로 정리해주는 소형 크롤러"

gpt-crawler는 "웹사이트 → LLM이 먹을 수 있는 지식 파일" 변환기다. 사용자가 config.ts에 시작 URL·크롤링 범위·본문 셀렉터만 적어주면, 실제 브라우저(Chromium 등)를 띄워 그 사이트의 페이지들을 순서대로 열고, 지정한 부분의 텍스트만 뽑아 output.json 하나(또는 용량/토큰 한도에 따라 여러 개)로 저장한다.

에이전트도 아니고, 검색엔진도 아니다. AI 추론은 전혀 하지 않는다 — 순수하게 "브라우저 자동화 + 텍스트 추출 + 파일 분할"만 담당하는 파이프라인이고, 결과물을 실제로 "똑똑하게" 만드는 일은 그 JSON을 업로드받는 ChatGPT·Assistants API 쪽 몫이다. 이 역할 분리가 이 프로젝트를 이해하는 첫 번째 열쇠다.

한 줄 비유

도서관 전체를 발췘해 "한 권의 요약 노트"로 만들어주는 대필 작가

어떤 회사의 문서 사이트에 100페이지짜리 매뉴얼이 흩어져 있다고 하자. 사람이 매번 사이트를 뒤져 답을 찾는 대신, 이 100페이지를 미리 다 읽고 핵심만 한 권의 노트로 정리해 둔 대필 작가가 있다면 훨씬 빠르다.

gpt-crawler가 바로 그 대필 작가 역할이다. 브라우저로 페이지를 하나씩 "읽고"(크롤링), 광고·메뉴·푸터는 걷어내고 본문만 옮겨 적고(셀렉터 추출), 마지막엔 노트가 너무 두꺼워지지 않게 권 단위로 나눠 제본한다(토큰/용량 분할). 이 노트를 ChatGPT에게 건네주면 그때부터 ChatGPT가 그 노트를 읽고 질문에 답한다.

용어
커스텀 GPT (Custom GPT)
ChatGPT 위에서 사용자가 지시문·지식 파일을 얹어 만드는 "특화 챗봇". 예를 들어 "우리 회사 문서만 아는 GPT"를 만들 때, 그 문서를 담은 파일을 업로드하는 칸에 gpt-crawler의 출력물을 넣는다.
용어
헤드리스 브라우저 (headless browser)
화면(창)을 띄우지 않고 코드로 원격 조종되는 브라우저. 사람이 클릭하듯 페이지 이동·스크롤·요소 대기를 자동화할 수 있어, 단순 HTTP 요청으로는 못 읽는 자바스크립트 렌더링 사이트도 크롤링할 수 있다.

2왜 뜨는가 — 그리고 지금은 어떤 상태인가

2023년 커스텀 GPT 열풍과 함께 떴고, 지금은 "꾸준히 인용되는 참고 구현"으로 남아있다

이 저장소는 2023년 11월 OpenAI가 커스텀 GPT 기능을 발표한 직후 만들어졌다. "내 문서 사이트를 GPT에게 학습시키고 싶은데, 페이지를 일일이 복사-붙여넣기 하기는 너무 번거롭다"는 아주 구체적인 불편을 정확히 겨냥했고, README에는 Builder.io가 자사 개발 문서로 실제 커스텀 GPT를 만든 예시가 그대로 실려 있다. 짧고 명확한 문제 정의 덕에 "LLM 지식 파일 만들기"의 대표적인 참고 구현으로 자리 잡았다.

다만 정직하게 짚어야 할 부분이 있다. 이 문서 작성 시점(2026-07-29) 기준으로 마지막 커밋은 2025-07-07이고, package.json의 버전은 CHANGELOG상 2025-01-23의 1.5.1에서 멈춰 있다. 즉 "요즘 활발하게 기능이 추가되는 중"이라기보다는, 한 번 완성된 뒤 큰 변화 없이 안정적으로 참조되는 스테디셀러에 가깝다. "왜 지금 뜨는가"보다는 "왜 지금도 계속 언급되는가"로 질문을 바꿔 보는 게 정확하다.

① 문제 정의가 좁고 코드가 짧아 "학습용 표준 예제"로 오래 살아남았다

소스 코드가 src/ 아래 5개 파일, 수백 줄뿐이다. Crawlee·Playwright라는 잘 관리되는 라이브러리 위에 얇게 얹혀 있어서, 라이브러리 쪽이 계속 업데이트되는 한 이 얇은 래퍼 자체는 손댈 일이 적다. "짧아서 읽기 쉽고, 얇아서 안 망가지는" 코드가 시간이 지나도 계속 인용되는 이유다.

② RAG·에이전트 지식 수집 파이프라인의 "가장 쉬운 첫 예제"로 계속 소환된다

"웹사이트를 LLM 지식으로 바꾼다"는 요구는 커스텀 GPT를 넘어 RAG(검색 증강 생성) 파이프라인 전반에서 여전히 흔하다. gpt-crawler는 셀렉터·매치 패턴·토큰 분할이라는 이 문제의 핵심 개념을 가장 적은 코드로 보여주는 예제라서, 각종 블로그·강의·awesome 목록에서 두고두고 다시 소개된다.

③ 실행 방식이 다양해 상황별로 바로 써먹을 수 있다

CLI 대화형 실행, npm start 스크립트 실행, Express API 서버, Docker 컨테이너까지 네 가지 실행 경로를 다 갖췄다. "일단 한 번 돌려보고 싶다"부터 "다른 서비스에서 API로 호출하고 싶다"까지 진입 장벽이 낮다.

균형 잡힌 평가
"활발한 개발"이 아니라 "안정적인 참조"로 보는 게 맞다

이 레포는 화려한 신규 기능 추가보다는 고정된 문제를 잘 푸는 작은 도구로 존재 가치가 있다. 유지보수 활동이 적다고 해서 코드가 못 쓰게 된 건 아니다 — 다만 이슈·PR 응답이 느릴 수 있고, Crawlee·Playwright의 최신 기능을 이 레포가 곧바로 흡수하진 않는다는 점은 감안해야 한다. 아래 함정·오해 방지에서 구체적으로 다룬다.

3무엇을 해결하나

"여러 페이지 → 지식 파일" 변환의 세 가지 실무적 골칫거리

겉보기엔 "웹사이트 긁어와서 파일로 저장"이라는 단순한 일 같지만, 막상 손으로 하려면 세 가지 지점에서 막힌다. gpt-crawler는 정확히 이 세 가지에만 집중해서 해결한다.

① 페이지가 자바스크립트로 그려지면 단순 요청으론 못 읽는다

많은 문서 사이트는 React·Vue 같은 프레임워크로 페이지를 브라우저 안에서 그린다. 단순히 URL에 HTTP 요청을 보내 받은 HTML엔 실제 본문이 비어 있는 경우가 흔하다. gpt-crawler는 Crawlee의 PlaywrightCrawler진짜 브라우저를 띄워 렌더링이 끝난 뒤의 화면을 읽기 때문에 이 문제를 원천적으로 피한다.

② 본문만 골라내지 않으면 메뉴·광고·푸터가 지식을 오염시킨다

페이지 전체 텍스트를 그대로 넣으면 내비게이션 메뉴, 배너, 저작권 문구 같은 반복되는 잡음이 매 페이지마다 끼어들어 지식 파일의 신호 대 잡음비를 떨어뜨린다. gpt-crawler는 CSS 셀렉터(또는 XPath)로 "본문 컨테이너 하나"만 지정하게 해, 그 안의 텍스트만 뽑아낸다.

③ 커스텀 GPT·Assistants 업로드에는 파일 크기·토큰 한도가 있다

ChatGPT나 Assistants API의 지식 파일 업로드에는 파일 하나당 크기 제한이 있다. 사이트가 커서 결과 JSON이 너무 커지면 업로드 자체가 실패한다. gpt-crawler는 maxFileSize(메가바이트)·maxTokens(토큰 수) 설정으로 결과를 자동으로 여러 파일로 쪼개 이 문제를 피해간다.

비유

이사할 때 필요한 짐만 골라(셀렉터), 트럭 적재량에 맞춰 상자를 나눠 싣는(토큰/용량 분할) 과정과 비슷하다. 집 전체를 통째로 옮기려 하면(전체 텍스트) 트럭에 안 실리고(업로드 실패), 짐을 안 고르면(셀렉터 없음) 필요 없는 잡동사니까지 다 실려 정작 중요한 물건이 묻힌다.

4전체 구조

3개의 진입점(CLI·main·server)이 모두 core.ts의 crawl()+write()로 모인다

레포 구조는 아주 단순하다. "어떻게 실행되는가"를 결정하는 진입점 파일 3개(main.ts·cli.ts·server.ts)와, "실제로 무슨 일을 하는가"를 담당하는 엔진 파일 2개(config.ts·core.ts)로 나뉜다. 어느 진입점으로 들어오든 결국 core.tscrawl()write() 두 함수를 순서대로 호출하는 것으로 수렴한다.

┌──────────────────────────────────────────────────────────────┐ │ gpt-crawler 전체 구조 │ └──────────────────────────────────────────────────────────────┘ 진입점 3가지 (사용자가 고르는 실행 방식) ┌───────────┐ ┌───────────┐ ┌──────────────┐ │ main.ts │ │ cli.ts │ │ server.ts │ │ npm start │ │ 대화형 CLI │ │ Express API │ │ config.ts │ │ commander+ │ │ POST /crawl │ │ 값 그대로 │ │ inquirer │ │ 로 값 받음 │ └─────┬─────┘ └─────┬─────┘ └──────┬───────┘ │ │ │ └────────────────┼────────────────┘ ▼ ┌────────────────────┐ │ config.ts │ ← zod 스키마로 검증(configSchema) │ (url·match·selector│ │ ·maxPagesToCrawl 등)│ └──────────┬──────────┘ ▼ ┌────────────────────────────┐ │ core.ts │ │ crawl(config) → 브라우저로│ │ 페이지를 돌며 storage/에 │ │ JSON 조각 저장(pushData) │ │ │ │ write(config) → storage의 │ │ JSON 조각들을 읽어 토큰/용량│ │ 기준으로 합치고 쪼개 │ │ output-N.json 으로 저장 │ └──────────┬──────────────────┘ ▼ Crawlee PlaywrightCrawler (실제 브라우저 자동화 엔진)

이 구조가 주는 실용적인 교훈은 "진입점(실행 방식)과 엔진(크롤링 로직)을 분리"했다는 점이다. CLI로 쓰든 API 서버로 쓰든 실제 크롤링·분할 로직은 core.ts 한 곳에만 있으니, 이 파일 하나만 이해하면 이 프로젝트의 8할을 이해한 셈이다. 다음 절에서 그 안을 그대로 뜯어본다.

5핵심 설계·동작 원리

crawl() — 브라우저로 페이지를 돌며 긁기 / write() — 조각을 모아 토큰·용량 기준으로 쪼개기

src/core.ts 하나에 이 프로젝트의 실질적인 로직이 거의 다 들어있다. 크게 두 함수 — 크롤링을 실행하는 crawl()결과를 파일로 합치는 write() — 로 나뉜다. 대표 흐름 한 줄기를 처음부터 끝까지 손으로 따라가 보자.

① 크롤 시작 — 사이트맵인가, 단일 URL인가

crawl(config)는 가장 먼저 configSchema.parse(config)로 설정값을 검증한다(잘못된 타입이면 여기서 바로 에러). 그다음 config.url이 정규식 /sitemap.*\.xml$/에 걸리는지로 분기한다.

// src/core.ts 의 분기 로직 (설명을 위해 발췌·정리) const isUrlASitemap = /sitemap.*\.xml$/.test(config.url); if (isUrlASitemap) { // 사이트맵이면: 사이트맵에 나열된 URL 전체를 큐에 한꺼번에 등록 const listOfUrls = await downloadListOfUrls({ url: config.url }); await crawler.addRequests(listOfUrls); await crawler.run(); } else { // 일반 URL이면: 그 한 페이지만 큐에 넣고 시작 → 이후 링크를 따라가며 확장 await crawler.run([config.url]); }

url 하나로 "단일 시작점 크롤"과 "사이트맵 일괄 등록"을 모두 지원한다. sitemap.xml을 주면 enqueueLinks 없이도 사이트 전체 URL 목록을 한 번에 큐에 넣을 수 있어, 링크 구조가 복잡한 사이트에 특히 유용하다.

② 페이지 하나를 방문할 때 — requestHandler

PlaywrightCrawler에 등록된 requestHandler가 방문한 페이지마다 실행된다. 순서대로: (1) 제목을 읽고 진행 로그를 찍는다 → (2) config.selector가 있으면 그 요소가 나타날 때까지 기다린다(waitForSelector 또는 XPath면 waitForXPath) → (3) getPageHtml()로 텍스트를 뽑는다 → (4) pushData()storage/datasets/default/에 JSON 조각 하나를 저장한다 → (5) config.onVisitPage 콜백이 있으면 실행한다 → (6) enqueueLinks()match 패턴에 맞는 링크를 다음 크롤 대상 큐에 추가한다.

용어
셀렉터 (selector) — CSS vs XPath
페이지 안에서 "이 부분만" 가리키는 주소. CSS 셀렉터(예: .docs-container)는 클래스·태그로, XPath(예: //div[@id='content'], "/"로 시작)는 문서 트리 경로로 요소를 지정한다. gpt-crawler는 문자열이 "/"로 시작하면 자동으로 XPath로, 아니면 CSS로 처리한다.

여기서 짚고 넘어가야 할 세부 하나. getPageHtml()은 이름과 달리 실제로는 HTML 마크업이 아니라 순수 텍스트를 반환한다.

// src/core.ts
export function getPageHtml(page: Page, selector = "body") {
  return page.evaluate((selector) => {
    if (selector.startsWith("/")) {
      // XPath: 일치하는 첫 요소의 textContent(태그 없는 순수 텍스트)
      const elements = document.evaluate(selector, document, null, XPathResult.ANY_TYPE, null);
      let result = elements.iterateNext();
      return result ? result.textContent || "" : "";
    } else {
      // CSS: 일치하는 첫 요소의 innerText(화면에 보이는 대로의 텍스트)
      const el = document.querySelector(selector) as HTMLElement | null;
      return el?.innerText || "";
    }
  }, selector);
}

결과 JSON에는 이 값이 html이라는 필드 이름으로 저장된다(pushData({ title, url, html })). 필드 이름은 "html"인데 내용물은 텍스트라는 이 불일치는 코드를 처음 읽는 사람이 가장 자주 헷갈리는 지점이라 함정·오해 방지에서 다시 짚는다.

③ 크롤링 범위 제어 — match / exclude / resourceExclusions / cookie

enqueueLinks({ globs: config.match, exclude: config.exclude ?? [] })가 "어디까지 따라갈지"를 결정한다. match는 glob 패턴(예: "https://example.com/docs/**") 문자열 하나 또는 배열을 받는다. exclude는 그 반대로, 매칭되더라도 크롤 대상에서 뺄 패턴이다.

preNavigationHooks(페이지 이동 전에 실행되는 훅)에서 두 가지가 더 일어난다. 하나는 config.cookie가 있으면 그 값을 페이지 컨텍스트에 쿠키로 심는 것(쿠키 동의 배너를 미리 넘기는 용도로 흔히 쓴다). 다른 하나는 config.resourceExclusions(이미지·폰트·비디오 등 확장자 목록)가 있으면 그 확장자로 끝나는 요청을 page.route(...).abort("aborted")로 아예 막아버리는 것 — 크롤링 속도를 올리고 대역폭을 아끼는 최적화다.

비유

match/exclude"이 구역 안에서만, 이 골목은 빼고" 순찰 경로를 정해주는 것이고, resourceExclusions"사진첩·짐은 놔두고 문서만 챙겨오라"

④ 결과 합치기 — write()의 토큰·용량 기반 자동 분할

크롤이 끝나면 storage/datasets/default/*.json에 페이지 수만큼 조각 파일이 쌓여 있다. write(config)는 이 조각들을 순서대로 읽어 하나의 결과에 밀어 넣다가, 한도를 넘는 순간 파일을 잘라 다음 파일로 넘어가는 누적 알고리즘을 돌린다. 실제 로직은 이렇다.

// src/core.ts write() 안의 addContentOrSplit (설명을 위해 정리)
const contentString = JSON.stringify(data);
const tokenCount = isWithinTokenLimit(contentString, config.maxTokens || Infinity);
// tokenCount: 이 항목 하나의 토큰 수(숫자) — 단, 이 항목 자체가 이미
// maxTokens를 넘으면 숫자 대신 false 가 돌아온다

if (typeof tokenCount === "number") {
  if (estimatedTokens + tokenCount > config.maxTokens) {
    if (currentResults.length > 0) await writeBatchToFile(); // 지금까지 모은 걸 파일로 확정
    estimatedTokens = Math.floor(tokenCount / 2); // 다음 파일의 시작 추정치(근사치)
    currentResults.push(data);
  } else {
    currentResults.push(data);
    estimatedTokens += tokenCount;
  }
}
// tokenCount 가 false 였다면 위 if 블록에 아예 들어가지 않는다
// → 이 항목은 currentResults 에 추가되지 않고 그대로 버려진다

currentSize += getStringByteSize(contentString);
if (currentSize > maxBytes) await writeBatchToFile(); // 용량 기준 분할(별도 트리거)

풀어 말하면: 페이지 하나씩을 토큰 누적치바이트 누적치 두 자로 동시에 재면서, 둘 중 하나라도 한도(maxTokens / maxFileSize)를 넘는 순간 지금까지 모은 걸 output-1.json, output-2.json… 식으로 잘라 저장한다. maxTokens·maxFileSize를 아예 지정하지 않으면 두 한도 모두 사실상 무한대라 파일 하나(output-1.json)로만 끝난다.

코드에서 실제로 확인한 함정
maxTokens를 넘는 "한 페이지"는 결과에서 조용히 사라진다

만약 어떤 한 페이지의 본문 자체가 maxTokens 한도보다 크면, isWithinTokenLimit은 숫자가 아니라 false를 돌려준다. 코드는 이 경우를 if (typeof tokenCount === "number") 블록 밖에 두기 때문에, 그 페이지는 어떤 경고도 없이 그냥 결과 파일에서 빠진다.

큰 API 레퍼런스 페이지나 코드 예제가 잔뜩 있는 문서를 크롤링할 때 특히 주의해야 한다. 크롤 자체는 "성공"으로 끝나지만, 정작 가장 중요한 그 페이지가 빠진 채로 output.json이 만들어질 수 있다.

⑤ GPTCrawlerCore — API 서버가 쓰는 얇은 클래스 래퍼

core.ts 맨 아래엔 crawl()·write() 두 함수를 this.config로 감싼 GPTCrawlerCore 클래스가 있다. server.ts는 이 클래스 인스턴스를 만들어 crawler.crawl()crawler.write() 순서로 호출한다. 함수형으로 짠 로직을 API 핸들러에서 쓰기 좋게 클래스 인스턴스 하나로 감싼, 아주 얇은 어댑터일 뿐이다.

용어
Crawlee / PlaywrightCrawler
Crawlee는 Apify가 만든 Node.js 크롤링·스크래핑 프레임워크로, 큐 관리·재시도·동시성 제어·결과 저장을 대신 해준다. PlaywrightCrawler는 그중에서도 Playwright(브라우저 자동화 라이브러리)로 실제 브라우저를 띄워 페이지를 여는 크롤러 구현체다. gpt-crawler는 이 두 라이브러리를 그대로 가져다 쓰고, 그 위에 셀렉터 추출·파일 분할만 얹었다.

6기술 스택·의존성

Crawlee + Playwright가 엔진, 나머지는 진입점(CLI/Express)을 위한 얇은 도구들

버전의 진실원(source of truth)package.json의 선언값이 아니라 package-lock.json에 실제로 고정된 설치 버전이다. 아래 표의 버전은 이 저장소를 그대로 클론했을 때 package-lock.json에 기록된 실측값이다.

구성실제 설치 버전 (lockfile)역할
crawlee3.6.1 (선언 ^3.0.0)크롤링 프레임워크 — 큐·재시도·저장소 관리
playwright1.40.0 (선언 * — 와일드카드)실제 브라우저 자동화 엔진(Chromium 등)
gpt-tokenizer2.1.2OpenAI 토크나이저로 텍스트의 토큰 수 계산(isWithinTokenLimit)
zod3.22.4config.ts의 설정 스키마 정의·런타임 검증
express4.18.2server.ts의 API 서버 프레임워크
commander11.1.0cli.ts의 커맨드라인 인자 파서
inquirer9.2.xCLI에서 값이 비어 있을 때 대화형으로 되묻는 프롬프트
glob10.3.xwrite()에서 storage/datasets/default/*.json 조각 파일 목록 찾기
swagger-ui-express / swagger-autogen5.0.x / 2.23.xAPI 서버의 /api-docs 문서 자동 생성
TypeScript5.x (devDependency)전체 소스가 TypeScript. 빌드는 tscdist/에 컴파일
용어
zod
TypeScript용 스키마 검증 라이브러리. "이 값은 문자열이어야 한다", "이 필드는 선택값이다" 같은 규칙을 코드로 적어두면, 실행 중에 실제 값이 그 규칙을 어기는지 검사해준다. gpt-crawler는 config.tsconfigSchema로 크롤 설정을 검증한다.
주의
playwright 의존성이 "*"(와일드카드)로 선언돼 있다

package.json에는 "playwright": "*"라고 적혀 있어, 이론적으로는 어떤 버전이든 허용한다. 실제로는 package-lock.json이 1.40.0으로 고정하고 있어 npm ci를 쓰면 안전하지만, lockfile 없이 npm install만 새로 돌리는 환경에서는 예상보다 최신(혹은 호환되지 않는) Playwright가 깔릴 수 있다.

7설치·요구사항

Node.js + Playwright 브라우저 다운로드가 필수 — Docker로 통째로 감쌀 수도 있다
항목요구사항
Node.jsREADME 기준 16 이상. CI·Docker 이미지는 실제로 18을 사용(.github/workflows, Dockerfile)
브라우저 바이너리npm installpreinstall 훅이 npx playwright install을 자동 실행 — Chromium 등 브라우저 실행 파일을 별도로 내려받는다(수백 MB, 인터넷 필요)
로컬 실행git clonenpm iconfig.ts 수정 → npm start(내부적으로 tsc 빌드 후 dist/src/main.js 실행)
API 서버 모드npm run start:server — 기본 포트 3000(코드 기본값). .envAPI_PORT·API_HOST 재정의 가능(.env.example은 5000 예시)
Docker(기본)베이스 이미지 apify/actor-node-playwright-chrome:18 — Playwright·Chrome이 이미 설치된 이미지 위에 빌드하는 멀티스테이지 Dockerfile
Docker(containerapp/)Ubuntu 기반 이미지 안에 Docker까지 함께 설치하는 "완전 격리 실행" 스크립트 세트. run.sh가 이미지를 빌드하고 data/ 폴더를 마운트해 config.ts 주입·output.json 회수를 자동화
가벼운 편
특별한 하드웨어 요구사항은 없다

GPU도, 대용량 메모리도 필요 없다. 유일한 무게감은 Playwright가 내려받는 브라우저 바이너리와, 크롤링할 사이트 규모(페이지 수 × 브라우저 렌더링 시간)에서 온다. 개인 노트북에서도 문제없이 돌아가는 수준이다.

8실제 사용 흐름

config.ts 편집 → npm start → output.json → ChatGPT/Assistants 업로드
흐름 A — 가장 흔한 로컬 실행

config.ts를 고쳐서 한 번 돌리고 끝내는 방식

1) git clone https://github.com/builderio/gpt-crawler → 2) npm i(이때 Playwright 브라우저도 함께 설치됨) → 3) 루트의 config.ts를 열어 url·match·selector·maxPagesToCrawl·outputFileName을 자신의 사이트에 맞게 수정 → 4) npm start 실행. 내부적으로는 main.tsdefaultConfig를 그대로 읽어 crawl()write() 순서로 실행하고, 루트에 output-1.json(또는 분할 시 -2, -3…)이 생성된다.

흐름 B — 값을 코드에 안 적고 그때그때 물어보게

대화형 CLI (npm run start:cli)

npm run start:cli로 실행하면 cli.ts가 실행된다. --url·--match·--selector·--maxPagesToCrawl·--outputFileName 플래그로 값을 바로 넘길 수 있고, 필수값(url·match·selector)이 비어 있으면 inquirer가 터미널에서 하나씩 되물어 채워준다. config.ts를 편집하지 않고 반복 실행할 때 편하다.

흐름 C — 다른 서비스에서 API로 호출

Express API 서버 (npm run start:server)

npm run start:server로 서버를 띄운 뒤, POST /crawl에 config JSON을 body로 보내면 그 자리에서 크롤·분할까지 마친 결과 파일 내용을 응답으로 돌려받는다. /api-docs에서 Swagger로 API 명세를 바로 확인할 수 있다. 자체 서비스의 백엔드에서 "필요할 때마다 크롤을 트리거"하고 싶을 때 이 방식을 쓴다.

흐름 D — 완전히 격리된 환경에서 실행

Docker / containerapp

기본 Dockerfile로 이미지를 빌드해 컨테이너 하나로 실행하거나, containerapp/ 폴더의 run.sh를 실행하면 아예 처음부터 Ubuntu + Docker + Node를 갖춘 이미지를 새로 빌드해, data/config.ts를 컨테이너 안에 복사해 넣고 크롤을 돌린 뒤 data/output.json으로 결과를 돌려준다. 로컬 Node.js 환경을 건드리고 싶지 않을 때 쓰는 방식이다.

마지막 — 결과를 실제로 사용하기

output.json을 ChatGPT/Assistants에 업로드

ChatGPT의 "My GPTs → Create a GPT → Configure → Knowledge"에서 파일을 업로드하거나, OpenAI Assistants API(platform.openai.com/assistants)의 지식 파일로 올린다. 파일이 너무 크다는 오류가 나면 그제서야 되돌아가 config.tsmaxFileSizemaxTokens를 추가해 다시 크롤하는 게 README가 권하는 순서다.

9함정·오해 방지

코드를 직접 읽고 실행해 확인한, 문서만 봐서는 알기 어려운 지점들

① CLI의 -m 단축 플래그가 두 옵션에 겹쳐 있다 (실제로 재현 확인됨)

cli.ts-m, --match-m, --maxPagesToCrawl둘 다 -m으로 등록한다. commander는 이 경우 에러를 내지 않고, 먼저 등록된 --match-m 단축 플래그를 그대로 가져간다.gpt-crawler -m 30처럼 짧게 쓰면 최대 페이지 수가 아니라 match 값이 "30"으로 설정된다. 실제로 이 저장소의 commander@11.1.0으로 재현해 확인한 동작이다. 페이지 수를 지정하려면 반드시 긴 플래그 --maxPagesToCrawl을 써야 한다.

② 필드 이름은 "html"인데 실제로는 텍스트다

앞서 핵심 설계·동작 원리에서 봤듯, 결과 JSON의 html 필드에는 innerText/textContent(순수 텍스트)가 들어간다. 원본 마크업·링크·이미지 태그는 전혀 남지 않는다. "html이라 적혀 있으니 태그도 있겠지" 하고 후처리 코드를 짜면 곧바로 어긋난다.

③ README의 설정 타입 예시가 실제 스키마보다 적다

README에 소개된 Config 타입 예시에는 url·match·selector·maxPagesToCrawl·outputFileName·resourceExclusions·maxFileSize·maxTokens만 나온다. 그러나 실제 src/config.ts에는 cookie(쿠키 주입), onVisitPage(페이지마다 실행할 커스텀 콜백), waitForSelectorTimeout까지 더 있다. README만 읽으면 이 세 옵션의 존재 자체를 모르고 지나치기 쉽다.

법적·윤리적 주의
robots.txt를 자동으로 지키지 않고, 요청 속도 제한도 기본으로는 없다

코드 어디에도 robots.txt를 확인하거나 요청 사이 지연을 강제하는 로직이 없다. 즉 "이 크롤러를 쓰면 자동으로 예의 바르게 긁는다"는 보장이 없다. 대상 사이트의 이용 약관·robots.txt를 사람이 직접 확인하고, 필요하면 maxPagesToCrawl을 보수적으로 잡거나 크롤 대상 사이트 소유자의 허락을 받아야 한다. 저작권이 있는 콘텐츠를 그대로 지식 파일로 만들어 배포하는 것도 별개의 법적 쟁점이다.

④ "테스트 통과"가 실제 검증을 의미하지 않는다

package.jsontest 스크립트는 echo "No test specified, please create tests." && exit 0다. PR CI(pr.yml)가 npm run test를 돌리지만, 이건 항상 성공하는 빈 스크립트라 실제 자동화된 테스트는 없다. CI가 초록불이어도 크롤링 로직 자체가 검증됐다는 뜻은 아니다.

⑤ 기타 자잘한 불일치

package.json에는 express-fileupload가 의존성으로 선언돼 있지만 src/ 어디에서도 실제로 쓰이지 않는다(죽은 의존성). 라이선스는 "ISC"로, MIT과 성격은 비슷하지만(둘 다 매우 관대한 허용 라이선스) 정확히는 별개 라이선스명이니 사내 라이선스 검수 시 "ISC"라고 정확히 표기해야 한다.

10대안 비교

Firecrawl · Crawl4AI · Crawlee를 직접 쓰기 — "완성품 vs 재료"의 스펙트럼

gpt-crawler는 "셀렉터 + 토큰 분할"이라는 아주 좁은 문제만 푼다. 비슷한 목적을 가진 다른 선택지와 견주면 이 프로젝트의 위치가 더 또렷해진다.

도구성격gpt-crawler와 다른 점
gpt-crawler셀프호스트, 셀렉터 기반 소형 크롤러서버 없이 로컬에서 바로 실행. 설정 3~4줄로 시작하지만, 페이지 구조가 바뀌면 셀렉터를 직접 다시 찾아야 한다
Firecrawl크롤링 결과를 마크다운/구조화 데이터로 자동 정제해주는 API·오픈소스 서비스셀렉터를 직접 지정하지 않아도 "본문 추출"을 어느 정도 자동화해준다. 대신 호스팅형 사용 시 API 키·과금이 따르고, 셀프호스트도 별도 인프라가 필요하다
Crawl4AILLM/RAG 파이프라인을 겨냥한 파이썬 크롤링 라이브러리파이썬 생태계에 있고, 마크다운 변환·청크 분할·비동기 크롤 등 RAG에 특화된 기능이 더 풍부하다. 대신 러닝 커브가 gpt-crawler보다 높다
Crawlee를 직접 쓰기gpt-crawler가 내부에서 쓰는 프레임워크 그 자체셀렉터 추출·토큰 분할 로직을 스스로 짜야 하지만, 그만큼 크롤 큐·재시도·동시성·다양한 크롤러 타입(HTTP/Cheerio/Playwright)을 훨씬 세밀하게 제어할 수 있다
선택 기준
"바로 실행되는 완성품"이 필요하면 gpt-crawler, "맞춤 제어"가 필요하면 Crawlee

문서 사이트 하나를 빠르게 커스텀 GPT 지식으로 바꾸고 끝낼 거라면 gpt-crawler의 단순함이 유리하다. 반대로 여러 사이트를 정기적으로 재크롤링하거나, 마크다운 변환·중복 제거·임베딩까지 파이프라인을 직접 짜야 한다면 Firecrawl·Crawl4AI처럼 RAG를 더 염두에 둔 도구나, Crawlee를 직접 다루는 편이 장기적으로 낫다.

11정리 — 누가 쓰면 좋은가

"한 문서 사이트를 GPT 지식으로" 빠르게, 그리고 코드로 크롤링 파이프라인을 배우고 싶을 때

gpt-crawler를 한 문장으로 다시 요약하면: "Crawlee의 PlaywrightCrawler를 셀렉터 추출 + 토큰/용량 자동 분할이라는 두 겹의 얇은 로직으로 감싼, 커스텀 GPT 지식 파일 생성 전용 도구"다. AI 추론은 하지 않고, 그 대신 "사이트를 읽어서 파일로 만드는" 앞단 파이프라인 한 조각을 아주 잘 해낸다.

이런 사람에게 잘 맞는다

· 회사·개인 문서 사이트 하나를 커스텀 GPT나 Assistants API 지식으로 빠르게 옮기고 싶은 사람
· "웹 크롤링 → 셀렉터 추출 → 토큰 기준 파일 분할"이라는 패턴을 짧은 실제 코드로 배우고 싶은 초보 개발자
· Crawlee·Playwright를 처음 접하는데, 아주 얇은 실전 예제부터 읽고 싶은 사람

이런 상황엔 다른 도구를 먼저 보는 게 낫다

· 여러 사이트를 정기적으로 재크롤링하며 자동으로 최신 상태를 유지해야 하는 프로덕션 RAG 파이프라인 → Firecrawl·Crawl4AI 같은 RAG 특화 도구
· 크롤 큐·동시성·재시도 정책을 세밀하게 제어해야 하는 대규모 크롤 → Crawlee를 직접
· 대상 사이트가 로그인·CAPTCHA·강한 봇 차단을 쓰는 경우 → 이 저장소는 그런 회피 로직을 전혀 다루지 않는다

마지막으로, 함정·오해 방지에서 짚었듯 이 도구는 크롤링의 예의(robots.txt·속도 제한)를 대신 챙겨주지 않는다. 짧고 유용한 코드인 만큼, 그 짧음이 어디까지 책임지고 어디부터는 사용자가 직접 판단해야 하는지를 분명히 알고 쓰는 것이 이 도구를 안전하게 쓰는 법이다.