얼마 전, 같이 일하는 엔지니어 한 명이 주말에 혼자 짠 Python 스크립트를 보여줬어요. 시장 조사용으로 Pinterest에서 제품 영감 이미지를 모으려던 코드였죠. 실행 버튼을 눌렀는데 결과가… 핀 16개. 보드 2,000개가 넘는데 말이죠. 그는 화면을 한참 보다가 저를 돌아보며 그러더라고요. “Pinterest가 저를 놀리는 것 같아요.”
이건 그 친구만의 일이 아니에요. Python으로 Pinterest를 긁으려는 개발자가 가장 자주 부딪히는 벽이거든요. requests와 BeautifulSoup으로 Pinterest URL에 붙어 보면, 몇 개 안 되는 항목이거나 텅 빈 HTML 껍데기만 돌아와요. 이유는 단순해요. Pinterest는 JavaScript로 전부 렌더링되는 싱글 페이지 앱이라, 평범한 HTTP 요청으로는 실제 콘텐츠가 안 보여요. 이 글에서는 왜 이렇게 되는지, 어떤 방식이 진짜 통하는지(Playwright, 내부 API 인터셉트, 그리고 Thunderbit 같은 노코드 도구), 그리고 핀·보드·사용자 프로필·무한 스크롤·원본 해상도 이미지까지 단계별 코드로 어떻게 가져오는지 알려드릴게요. 운영 수준 스크래퍼를 만들든, 빠르게 데이터만 모으든, 필요한 내용이 다 들어 있어요.
Pinterest 스크래핑이란?
Pinterest 스크래핑은 핀 이미지, 제목, 설명, 보드 이름, 팔로워 수, URL 같은 데이터를 코드로 뽑아내는 작업이에요. 핀을 하나하나 둘러보며 저장하는 대신, 코드나 도구로 검색 결과·보드·사용자 프로필에서 구조화된 데이터를 한꺼번에 모으는 방식이죠.
2025년 말 기준 플랫폼에는 핀 2,400억 개 이상과 월간 활성 사용자 6억 1,900만 명이 있어요. 웹에서 가장 풍부한 시각 데이터 소스 중 하나죠. 기업 입장에선 이 데이터가 무척 값져요. 제품 트렌드를 추적하든, 경쟁사 콘텐츠를 벤치마킹하든, 인플루언서 리스트를 만들든 활용도가 높거든요.
왜 Python으로 Pinterest를 스크래핑할까?
이제 Pinterest는 결혼식 무드보드용 서비스가 아니에요. 어엿한 비즈니스 인텔리전스 플랫폼이죠. 주간 사용자의 85%가 브랜드 핀을 보고 뭔가를 산 적이 있고, 상위 검색의 96~97%는 브랜드 없는 검색이에요. 즉 사용자는 구매 의도를 갖고 들어오지만 특정 브랜드에 묶여 있지 않아요. 발견의 기회가 그만큼 크고, 그래서 많은 팀이 구조화된 Pinterest 데이터를 원해요.
팀별로 보면 이렇게 활용해요.
| 팀 | 필요한 데이터 | 비즈니스 가치 |
|---|---|---|
| 이커머스 운영 | 제품 이미지, 가격, 인기 있는 미적 트렌드 | 경쟁력 있는 가격 책정, 트렌드 기반 재고 운영 |
| 마케팅 | 보드 성과, 핀 참여도, 경쟁사 콘텐츠 | 콘텐츠 전략, 캠페인 벤치마킹 |
| 세일즈 / 리드 발굴 | 크리에이터 프로필, 팔로워 수, 연락처 정보 | 인플루언서 아웃리치, 파트너십 타깃팅 |
| 부동산 | 홈 스테이징 핀, 인테리어 트렌드, 공간 레이아웃 | 매물 사진 전략, 스테이징 가이드 |
| 콘텐츠 크리에이터 | 인기 주제, 잘 먹히는 형식, 시즌별 테마 | 콘텐츠 캘린더, 비주얼 스타일 리서치 |
핵심은 이거예요. Pinterest 공식 API는 제약이 많아요. 비즈니스 계정이 있어야 하고, 승인 과정에선 앱 데모 영상까지 내야 하며, 접근 데이터도 내 계정 것으로 한정돼요. 공개 보드, 검색 결과, 경쟁사 프로필을 보고 싶다면 스크래핑이 현실적인 대안이에요. 그래서 많은 팀이 Python을 고르고, 설정 없이 바로 결과만 필요하면 Thunderbit 같은 노코드 도구로 넘어가죠.
BeautifulSoup만으로는 Pinterest가 안 되는 이유와, 실제로 통하는 방식
requests + BeautifulSoup으로 긁었는데 16개만 나오거나 빈 페이지가 나왔다면, 착각이 아니에요. Pinterest는 React로 만들어졌고 콘텐츠를 전부 JavaScript로 렌더링해요. 평범한 HTTP 요청으로 URL을 가져오면 서버는 최소한의 HTML 뼈대만 줘요. <link>와 <script> 태그 몇 개, 그리고 React가 앱을 꽂을 빈 <div> 정도죠. 핀 카드, 이미지, 제목, 그리드 레이아웃은 전부 브라우저에서 JavaScript가 돈 뒤에야 주입돼요.
JavaScript 실행이 없으면 핀도 없어요.
그럼 뭐가 통할까요? 주요 방식은 이래요.
| 방식 | JS 처리 가능? | 전체 데이터 확보? | 복잡도 | 적합한 경우 |
|---|---|---|---|---|
requests + BeautifulSoup | 아니오 | 약 0~16개 | 낮음 | Pinterest에는 부적합 |
| Selenium / Playwright | 예 | 예, 스크롤 로직 포함 | 중간 | 완전한 제어, Python 파이프라인 |
| Pinterest 내부 API 인터셉트 | 예 | 예, 페이지네이션 JSON | 높음 | 최대 데이터, 브라우저 불필요 |
| 서드파티 Scraper API | 예 | 상황에 따라 다름 | 낮음 | 인프라 없이 대량 수집 |
| 노코드 도구(Thunderbit) | 예 | AI 구조화 | 매우 낮음 | 비기술 사용자, 빠른 결과 |
이 글의 Python 방식에서는 Playwright를 추천해요. JavaScript를 렌더링하고, 스크롤도 흉내 내고, 유지보수도 활발해요(GitHub 스타 78,600+, 채용 공고는 연 180% 성장). 벤치마크상 Selenium보다 35~45% 빠르고요. 노코드도 원하면 그 부분도 같이 다룰게요.
Pinterest 공식 API vs Python 스크래핑 vs 노코드: 어떤 방법을 선택할까
코드를 짜기 전에 한 번 물어볼 만해요. 정말 코드가 필요할까요? 아래 의사결정 표를 참고하세요.
| 기준 | Pinterest API | Python 스크래핑 | Thunderbit(노코드) |
|---|---|---|---|
| 승인 필요 여부 | 비즈니스 계정 + 데모 영상 | 없음 | 없음 |
| 공개 핀/보드 접근 | 제한적(내 데이터만) | 전체 가능 | 전체 가능 |
| 원본 해상도 이미지 다운로드 | 상황에 따라 다름 | 예, URL 파싱으로 가능 | 예, 이미지 추출로 가능 |
| 무한 스크롤 처리 | 해당 없음 | 예, 코드로 처리 | 자동 |
| 유지보수 필요성 | 낮음 | 높음(셀렉터가 깨짐) | 없음(AI가 적응) |
| Sheets/Airtable 내보내기 | 수동 | 사용자 정의 코드 | 기본 제공 |
| 설정 시간 | 몇 시간~며칠 | 30~60분 | 2분 |
마케터, 이커머스 운영 담당자, 또는 Python 스크립트를 짜거나 유지보수하지 않고 Pinterest 데이터를 스프레드시트로 받고 싶은 분이라면 Thunderbit의 AI Web Scraper가 가장 빠른 선택이에요. Pinterest 페이지를 열고 "AI Suggest Fields"를 누른 뒤 "Scrape"를 클릭하면 Google Sheets, Excel, Airtable, Notion으로 바로 내보낼 수 있어요. 하위 페이지 스크래핑 기능을 쓰면 개별 핀 링크까지 따라가 데이터를 자동으로 더 채울 수도 있고요. 코드를 한 줄도 안 짜본 팀원이 3분도 안 돼 Google Sheet에 핀 500개를 넣는 걸 직접 본 적도 있어요.
직접 제어가 필요하거나, Python 파이프라인에 스크래핑을 붙이고 싶거나, 만드는 과정 자체를 즐기는 분이라면 계속 읽어 주세요.
Pinterest 스크래핑을 위한 Python 환경 설정
- 난이도: 중급
- 소요 시간: 약 30~60분(코딩 및 테스트 포함)
- 준비물: Python 3.9+, Chrome 브라우저(테스트용), 터미널/명령줄 접근 권한
Playwright와 의존성 설치
먼저 프로젝트 폴더를 만들고 가상환경을 잡아요.
mkdir pinterest-scraper
cd pinterest-scraper
python -m venv venv
source venv/bin/activate # Windows에서는: venv\Scripts\activate
Playwright를 설치하고 Chromium 브라우저 바이너리를 내려받아요.
pip install playwright
playwright install chromium
데이터 내보내기에는 Python 내장 json, os, csv 모듈을 쓰니 별도 설치는 필요 없어요.
프로젝트 폴더 구조
처음부터 정리해 두길 추천해요.
pinterest-scraper/
├── scraper.py
├── config.py
├── output/
│ ├── pins.json
│ └── pins.csv
└── images/
├── board-name-1/
└── board-name-2/
config.py에는 user agent 문자열을 넣으세요. Pinterest는 기본 헤드리스 브라우저 시그니처를 막으니, 그럴듯한 값이어야 해요.
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"
1단계: Pinterest 검색 URL 만들기
검색어를 템플릿에 끼워 검색 URL을 구성해요.
query = "mid century modern furniture"
url = f"https://www.pinterest.com/search/pins/?q={query.replace(' ', '%20')}&rs=typed"
어떤 검색어에도 똑같이 적용돼요. rs=typed 파라미터는 추천이 아니라 직접 입력한 검색어임을 Pinterest에 알려줘요. 경우에 따라 결과 관련성에 영향을 주고요.
2단계: 헤드리스 브라우저를 실행하고 페이지 불러오기
핵심 Playwright 설정은 이래요. 맞춤 user agent를 꼭 챙기세요. 없으면 Pinterest가 요청을 막거나 로그인 화면을 띄울 가능성이 커요.
import asyncio
from playwright.async_api import async_playwright
from config import USER_AGENT
async def scrape_search(query, max_pins=100):
url = f"https://www.pinterest.com/search/pins/?q={query.replace(' ', '%20')}&rs=typed"
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page(
user_agent=USER_AGENT,
viewport={"width": 1920, "height": 1080}
)
await page.goto(url)
await asyncio.sleep(3) # JS가 초기 핀을 렌더링할 시간을 줌
이 코드가 돌면 페이지에는 보통 핀 25~50개 정도가 초기 로드돼 있어야 해요.
3단계: 페이지에서 핀 데이터 추출하기
Pinterest는 핀마다 data-test-id='pinWrapper'가 붙은 div로 감싸요. 안에는 핀 URL과 제목이 담긴 링크(<a>)가 있고(제목은 aria-label로 확인), 썸네일 URL이 든 <img>도 있어요.
results = []
pins = await page.query_selector_all("div[data-test-id='pinWrapper']")
for pin in pins:
link = await pin.query_selector("a")
if not link:
continue
title = await link.get_attribute("aria-label") or ""
href = await link.get_attribute("href") or ""
img = await pin.query_selector("img")
src = await img.get_attribute("src") if img else ""
results.append({
"title": title,
"url": f"https://www.pinterest.com{href}" if href.startswith("/") else href,
"image_url": src
})
이 시점의 results에는 첫 화면에 보이는 핀들이 들어 있어요. 더 가져오려면 스크롤이 필요하고요. 이제 가장 중요한 부분으로 넘어갈게요.
4단계: 결과를 JSON 또는 CSV로 저장하기
추출한 뒤엔 데이터를 파일로 저장해 두면 다루기 편해요.
import json
import csv
def save_json(data, filepath="output/pins.json"):
with open(filepath, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def save_csv(data, filepath="output/pins.csv"):
if not data:
return
with open(filepath, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=data[0].keys())
writer.writeheader()
writer.writerows(data)
CSV를 Excel에서 열 거라면 utf-8-sig 인코딩을 쓰세요. 글자 깨짐을 막아 줘요.
전체 Pinterest 보드와 사용자 프로필 스크래핑하기
이 부분이 기존 튜토리얼의 큰 공백이에요. 보드나 프로필 스크래핑을 깊이 다룬 경쟁 가이드를 하나도 못 찾았거든요. 그런데 포럼에선 가장 많이 요청되는 기능 중 하나예요. 사람들은 보드의 모든 핀을 내려받고, 이미지를 보드별 폴더로 정리하고, 팔로워 수나 보드 목록 같은 프로필 수준 데이터까지 원해요.
보드 URL에서 모든 핀 스크래핑하기
보드 URL은 https://www.pinterest.com/{username}/{board-name}/ 형태예요. DOM 구조는 검색 결과와 비슷해요. 핀은 여전히 div[data-test-id='pinWrapper']로 감싸지지만, 전부 로드하려면 스크롤이 필요해요.
async def scrape_board(board_url, max_pins=500):
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page(user_agent=USER_AGENT, viewport={"width": 1920, "height": 1080})
await page.goto(board_url)
await asyncio.sleep(3)
seen_ids = set()
all_pins = []
for scroll_round in range(100): # 안전 제한
pins = await page.query_selector_all("div[data-test-id='pinWrapper']")
new_count = 0
for pin in pins:
link = await pin.query_selector("a")
if not link:
continue
href = await link.get_attribute("href") or ""
if href in seen_ids:
continue
seen_ids.add(href)
new_count += 1
title = await link.get_attribute("aria-label") or ""
img = await pin.query_selector("img")
src = await img.get_attribute("src") if img else ""
all_pins.append({
"title": title,
"url": f"https://www.pinterest.com{href}" if href.startswith("/") else href,
"image_url": src
})
print(f"스크롤 {scroll_round + 1}: 고유 핀 {len(all_pins)}개 수집 완료")
if new_count == 0 or len(all_pins) >= max_pins:
break
prev_height = await page.evaluate("document.body.scrollHeight")
await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
await asyncio.sleep(2.5)
curr_height = await page.evaluate("document.body.scrollHeight")
if curr_height == prev_height:
break # 더 이상 콘텐츠 없음
await browser.close()
return all_pins
주의할 점 하나. 보드 페이지에는 가끔 저장 핀과 알고리즘 추천을 나누는 "More Ideas" 탭이 있어요. 보드에 실제로 저장된 핀만 원한다면, 이 구분선이 보일 때 스크롤을 멈추면 돼요.
사용자 프로필에서 보드, 팔로워 수, 핀 추출하기
프로필 URL은 https://www.pinterest.com/{username}/ 형태예요. 프로필 페이지에서 뽑을 수 있는 항목은 이래요.
- 팔로워/팔로잉 수:
div[data-test-id='follower-count']를 확인 - 보드 목록: 각 보드는
/{username}/{board-name}/로 연결되는 카드 - 총 핀 수: 프로필 헤더에 표시되는 경우가 있음
async def scrape_profile(username):
url = f"https://www.pinterest.com/{username}/"
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page(user_agent=USER_AGENT, viewport={"width": 1920, "height": 1080})
await page.goto(url)
await asyncio.sleep(3)
# 팔로워 수 추출
follower_el = await page.query_selector("div[data-test-id='follower-count']")
followers = await follower_el.inner_text() if follower_el else "N/A"
# 보드 링크 추출
board_links = await page.query_selector_all("a[href*='/" + username + "/']")
boards = []
for bl in board_links:
href = await bl.get_attribute("href") or ""
name = await bl.get_attribute("aria-label") or href.split("/")[-2]
if href.count("/") >= 3 and href != f"/{username}/":
boards.append({"name": name, "url": f"https://www.pinterest.com{href}"})
await browser.close()
return {"username": username, "followers": followers, "boards": boards}
프로필의 모든 보드에 든 핀까지 가져오려면 보드 목록을 반복하면서 보드마다 scrape_board()를 호출하면 돼요. 내려받은 이미지는 보드별 폴더로 자동 정리할 수 있고요.
운영 수준의 무한 스크롤 처리기 만들기
여기서 장난감 스크래퍼와 진짜 스크래퍼가 갈려요. 가장 큰 골칫거리는 — 포럼 글만 열두 개는 봤어요 — 스크래퍼가 16~25개쯤 긁고 멈추는 거예요. 스크롤이 부족했거나, for i in range(5): scroll()처럼 고정 횟수만 돌리고 운에 맡겼기 때문이죠.
이런 방식은 못 믿어요. Pinterest는 스크롤 이벤트마다 약 25개씩 새 콘텐츠를 불러와요. 다섯 번 스크롤하면 125개를 얻을 수도 있지만, 네트워크가 느리면 75개에 그치고, 배치가 작으면 150개가 될 수도 있어요. 더 똑똑한 패턴이 필요해요.
새 콘텐츠가 없을 때까지 스크롤하는 패턴
아래는 고유 핀 ID를 추적하고, 설정 가능한 타임아웃을 쓰며, 재시도 로직과 진행 출력까지 갖춘 견고한 스크롤 함수예요.
import time
import random
async def scroll_and_collect(page, max_pins=1000, max_scrolls=200, scroll_pause=2.5):
seen_ids = set()
all_pins = []
no_new_count = 0
for i in range(max_scrolls):
pins = await page.query_selector_all("div[data-test-id='pinWrapper']")
new_this_round = 0
for pin in pins:
link = await pin.query_selector("a")
if not link:
continue
href = await link.get_attribute("href") or ""
if href in seen_ids:
continue
seen_ids.add(href)
new_this_round += 1
title = await link.get_attribute("aria-label") or ""
img = await link.query_selector("img")
src = await img.get_attribute("src") if img else ""
all_pins.append({
"title": title,
"url": f"https://www.pinterest.com{href}" if href.startswith("/") else href,
"image_url": src
})
print(f" 스크롤 {i+1}: 새 핀 {new_this_round}개 | 총 고유 핀 {len(all_pins)}개")
if len(all_pins) >= max_pins:
print(f" max_pins 제한({max_pins})에 도달했습니다. 중지합니다.")
break
if new_this_round == 0:
no_new_count += 1
if no_new_count >= 3:
print(" 연속 3회 스크롤에서 새 핀이 없습니다. 콘텐츠 끝으로 판단합니다.")
break
else:
no_new_count = 0
prev_height = await page.evaluate("document.body.scrollHeight")
await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
await asyncio.sleep(scroll_pause + random.uniform(0.5, 1.5))
curr_height = await page.evaluate("document.body.scrollHeight")
if curr_height == prev_height and new_this_round == 0:
print(" 페이지 높이가 변하지 않았고 새 핀도 없습니다. 피드가 끝난 것으로 보입니다.")
break
return all_pins
이 설계가 잘 도는 이유는 이래요.
- href 기준 중복 제거: 핀 URL은 저마다 고유하니 ID처럼 써요. 스크롤 중 DOM이 다시 그려져도 같은 핀을 두 번 세지 않아요.
- 3회 연속 실패 규칙: 연속 세 번 스크롤했는데 새 핀이 없으면 멈춰요. 페이지는 아직 로딩 중인데 실제 새 콘텐츠는 없는 상황을 처리해 줘요.
- 랜덤 지연 추가: 스크롤 사이에 0.5~1.5초 랜덤 지연을 넣으면 사람처럼 보이고, 봇 차단 가능성도 줄어요.
- 최대 스크롤 제한: 문제가 생겼을 때 무한 루프를 막아요.
예외 상황 다루기
- "More Ideas" 구분선: 보드 페이지에서 Pinterest가 가끔 "More Ideas" 섹션을 끼워 넣어요. 보드의 실제 핀만 원하면 이 요소가 보일 때 스크롤을 멈추면 돼요.
- 긴 세션에서의 rate limiting: 핀이 수천 개인 보드를 오래 스크롤하면 Pinterest가 응답을 늦추기 시작할 수 있어요. 간헐적으로(연속 3회는 아니지만) 새 핀이 0개로 나오면 스크롤 간격을 5초 이상으로 늘려 보세요.
Pinterest 이미지 원본 해상도로 받기(썸네일 말고)
이 문제가 정말 사람 속을 긁어요. 핀을 잔뜩 긁어 이미지를 받았는데 전부 236px짜리 작은 썸네일인 경우가 많거든요. 포럼에선 “화질이 쓰레기다, 너무 작다”고 표현하기도 해요. 해법은 Pinterest 이미지 URL 구조를 이해하는 거예요.
Pinterest 이미지 URL 경로 이해하기
Pinterest 이미지는 모두 https://i.pinimg.com/{size}/{hash}.jpg 형식이에요. 여기서 {size} 세그먼트가 해상도를 정해요.
| 크기 경로 | 해상도 | 용도 |
|---|---|---|
/236x/ | 너비 236px | 기본 그리드 보기(기본적으로 받는 값) |
/474x/ | 너비 474px | 중간 해상도 |
/736x/ | 너비 736px | 핀 상세/확대 보기 |
/originals/ | 원본 업로드 해상도 | 전체 해상도 |
유틸 함수: Pinterest 이미지 URL을 고해상도로 업그레이드하기
아래 함수는 가능한 가장 높은 품질로 이미지 URL을 다시 쓰고, 실패하면 자동으로 다른 크기를 시도해요.
import requests as req
def upgrade_image_url(url, preferred_size="originals"):
"""Pinterest 이미지 URL을 가능한 가장 높은 해상도로 바꿉니다."""
sizes = ["originals", "736x", "474x", "236x"]
if preferred_size not in sizes:
preferred_size = "originals"
for size in sizes[sizes.index(preferred_size):]:
upgraded = url
for s in sizes:
upgraded = upgraded.replace(f"/{s}/", f"/{size}/")
try:
resp = req.head(upgraded, timeout=5, allow_redirects=True)
if resp.status_code == 200:
return upgraded
except Exception:
continue
return url # 모두 실패하면 원본 반환
중요 참고 사항(2025년 기준): /originals/ 경로는 점점 더 자주 HTTP 403 Forbidden을 돌려줘요. gallery-dl의 문서화된 이슈에서도 2025년 중반 기준 이 동작이 확인됐어요. 안정적으로 받을 수 있는 최고 해상도는 /736x/예요. 위 함수는 /originals/를 먼저 시도한 뒤 자동으로 /736x/로 내려가요.
이미지를 정리된 폴더에 다운로드하기
import os
import time
def download_images(pins, folder="images/default", delay=1.5):
os.makedirs(folder, exist_ok=True)
for i, pin in enumerate(pins):
img_url = upgrade_image_url(pin.get("image_url", ""), preferred_size="736x")
if not img_url:
continue
filename = f"pin_{i+1}.jpg"
filepath = os.path.join(folder, filename)
try:
resp = req.get(img_url, timeout=15)
if resp.status_code == 200:
with open(filepath, "wb") as f:
f.write(resp.content)
print(f" {filename} 다운로드 완료 ({len(resp.content) // 1024} KB)")
else:
print(f" {filename} 실패: HTTP {resp.status_code}")
except Exception as e:
print(f" {filename} 다운로드 중 오류: {e}")
time.sleep(delay + random.uniform(0.3, 0.8))
다운로드 사이에 속도 제한용 지연을 넣으세요. 저는 1.5~2.3초에 랜덤 지연을 더해 써요. 안 그러면 Pinterest가 몇백 요청 만에 IP를 막을 수 있어요.
스크래핑한 Pinterest 데이터 내보내기
CSV 또는 JSON으로 내보내기
기본은 앞서 다뤘어요. 더 큰 데이터셋(핀 1만 개 이상)이라면 JSON Lines 형식을 생각해 보세요. 한 줄에 JSON 객체 하나씩 저장하니 스트리밍과 후처리가 편해요.
def save_jsonl(data, filepath="output/pins.jsonl"):
with open(filepath, "w", encoding="utf-8") as f:
for item in data:
f.write(json.dumps(item, ensure_ascii=False) + "\n")
Google Sheets, Airtable, Notion으로 내보내기
Python에서 곧장 Google Sheets로 보내려면 gspread 라이브러리와 Google Cloud 서비스 계정이 필요해요. Airtable은 pyairtable, Notion은 notion-client를 쓰고요. 각각 API 키 설정이 필요하고, 파이프라인 복잡도도 꽤 올라가요.
아니면 — 편향이 좀 있지만 솔직히 가장 빠른 길이기도 한데 — Thunderbit로 Pinterest를 긁은 뒤 한 번 클릭으로 이 서비스들에 바로 내보낼 수 있어요. API 키도, 서비스 계정도, 추가 코드도 필요 없어요. Thunderbit Chrome 확장 프로그램이 내보내기를 기본 지원하거든요.
Pinterest 스크래핑 중 차단을 피하는 팁
ScrapeOps 기준 Pinterest의 봇 차단 회피 난이도는 10점 만점에 6점이에요. 아주 쉽진 않지만 제일 어려운 대상도 아니에요. 브라우저 지문, 행동 분석, IP 기반 rate limiting을 써요. 실제로 효과 있는 건 이래요.
- User agent 순환: 실제 Chrome user agent 문자열을 여러 개 준비해 세션마다 무작위로 골라요.
- 랜덤 지연 추가: 스크롤과 요청 사이에 2
5초, 거기에 jitter를 더하세요. 프록시 없이 갈 거면 1015초로 늘리는 게 좋아요. - 현실적인 viewport 사용:
viewport={"width": 1920, "height": 1080}로 두세요. 너무 작거나 특이한 크기는 피하고요. - 대량 수집에는 프록시 고려: 핀 수천 개를 긁는다면 residential proxy 회전을 검토하세요. 없으면 몇백 요청 후 IP 차단이 올 수 있어요.
robots.txt를 존중: Pinterest의robots.txt는 대부분의 자동 크롤러를 막고 약 180개의 disallow 규칙이 있어요. 준수 차원에서 염두에 두세요.- 로그인 상태 스크래핑은 피하기: 로그인하지 않은 공개 콘텐츠만 다루세요. 로그인 뒤 스크래핑은 법적·기술적 위험이 둘 다 커져요.
Thunderbit는 AI 엔진으로 봇 차단과 CAPTCHA를 자동 처리해요. 노코드라면 유지보수할 일이 하나 줄어드는 셈이죠.
Pinterest 스크래핑의 법적·윤리적 고려사항
이 부분은 글의 핵심은 아니지만 중요하니 짧게만 짚을게요.
Pinterest 서비스 약관(2a항)에는 “당사의 명시적 사전 허가 없이 자동화 수단으로 Pinterest의 데이터나 콘텐츠를 무단으로 스크래핑, 수집, 검색, 복사하거나 그 밖의 방식으로 접근하지 않는다”는 데 동의한다고 적혀 있어요. 다만 법원은 대체로 공개 데이터 스크래핑이 Computer Fraud and Abuse Act를 위반하지 않는다고 봐 왔어요. 예를 들어 hiQ v. LinkedIn과 Meta v. Bright Data(2024년 1월)에서는 로그인하지 않은 상태에서 공개로 보이는 데이터를 긁는 게 합법이라고 판결했고요.
기본 원칙은 이래요.
- 로그인하지 않은 상태에서 공개 콘텐츠만 스크래핑하기
- 스크래핑 데이터를 스팸이나 사용자 사칭에 쓰지 않기
- 이미지 저작권 존중하기 — 가능하면 메타데이터만 추출하고, 허가 없이 저작권 이미지를 상업적으로 재배포하지 않기
- 스크래핑 데이터를 상업적으로 쓸 계획이면 변호사와 상담하기
법적 쟁점을 더 깊이 보고 싶다면 웹 스크래핑 법적 쟁점 가이드를 참고하세요.
정리: 무엇을 배웠고, 다음엔 뭘 할까
이제 정적 스크래핑이 Pinterest에서 실패하는 이유(React SPA라 JavaScript 없이는 데이터가 안 보임), Playwright로 검색 결과·보드·사용자 프로필을 긁는 법, 16개에서 멈추지 않는 운영 수준 무한 스크롤 처리기 만드는 법, 그리고 작은 썸네일이 아니라 원본 해상도 이미지를 얻는 법까지 알게 됐어요.
핵심만 빠르게 정리하면 이래요.
requests+ BeautifulSoup은 Pinterest에서 거의 안 통해요. 시간 낭비하지 마세요.- Playwright가 이 작업에 가장 맞는 Python 도구예요. 빠르고, 지원도 활발하고, JS 렌더링을 자연스럽게 처리해요.
- 무한 스크롤에는 고정 횟수 반복이 아니라 중복 제거 기반 스크롤 루프가 필요해요.
- 원본 해상도 이미지는 URL 경로를 바꿔야 해요.
/736x/를 노리세요./originals/는 자주 403을 돌려줘요. - 보드와 프로필 스크래핑은 기존 튜토리얼에서 부족하지만, 올바른 셀렉터만 있으면 어렵지 않아요.
- 비개발자나 속도가 중요한 팀이라면 Thunderbit으로 Pinterest를 두 번 클릭에 긁고 Google Sheets, Excel, Airtable, Notion으로 내보낼 수 있어요. Python은 필요 없어요. Chrome 확장 프로그램으로 무료 체험해 보세요.
Python 파이프라인을 만든다면 이 글의 코드가 탄탄한 출발점이에요. 데이터만 필요하다면 Thunderbit가 더 빠른 지름길이고요. 어느 쪽이든 이제 핀 16개와 멍한 표정에 머물 필요는 없어요.
스크래핑과 데이터 추출을 더 알고 싶다면 Python으로 웹 스크래핑하는 방법, 최고의 자동 웹 스크래핑 도구, 웹사이트 데이터를 Excel로 스크래핑하는 방법 가이드를 확인해 보세요. Thunderbit 요금제를 살펴보거나 Thunderbit YouTube 채널에서 튜토리얼을 볼 수도 있어요.
FAQ
1. BeautifulSoup만으로 Pinterest를 스크래핑할 수 있나요?
단독으로는 안 통해요. Pinterest는 모든 콘텐츠를 JavaScript로 렌더링하니 requests + BeautifulSoup는 빈 HTML 껍데기만 보게 돼요. 페이지를 먼저 렌더링하는 Playwright나 Selenium 같은 헤드리스 브라우저가 필요하거나, JS 렌더링을 자동 처리하는 Thunderbit 같은 노코드 도구를 쓰면 돼요.
2. 한 번의 세션에서 Pinterest 핀을 얼마나 많이 스크래핑할 수 있나요?
스크롤 로직과 봇 차단 대응에 따라 달라요. 이 글의 운영 수준 무한 스크롤 처리기(중복 제거, 타임아웃, 재시도 로직 포함)를 쓰면 보드나 검색어당 핀 수백~수천 개를 안정적으로 모을 수 있어요. 아주 큰 보드라면 스크롤과 수집에 몇 분 정도는 잡아야 하고요.
3. 스크래핑한 Pinterest 이미지가 왜 이렇게 작게 나오나요?
Pinterest는 기본적으로 그리드 보기에서 /236x/ 썸네일을 줘요. 더 높은 해상도를 원하면 이미지 URL 경로를 /736x/나 /originals/로 바꾸세요. 다만 2025년 기준 /originals/는 403을 점점 더 자주 돌려주니, 안정적인 최대값은 /736x/예요.
4. Pinterest 스크래핑은 합법인가요?
공개로 볼 수 있는 데이터 스크래핑은 최근 판결들(예: hiQ v. LinkedIn, Meta v. Bright Data)에서 대체로 허용된다고 받아들여지지만, Pinterest 서비스 약관은 무단 자동화 접근을 금지해요. 공개 콘텐츠만 다루고, 스팸에 쓰지 않으며, 저작권을 존중하고, 상업 목적이라면 법률 자문을 받으세요.
5. Pinterest를 스크래핑할 수 있는 최고의 노코드 대안은 무엇인가요?
Thunderbit의 AI Web Scraper는 Pinterest 핀 데이터 — 제목, 이미지, URL, 설명 — 를 두 번 클릭으로 뽑고, Google Sheets, Excel, Airtable, Notion으로 바로 내보낼 수 있어요. JavaScript 렌더링, 무한 스크롤, 봇 차단도 자동 처리하니 코드를 짜거나 유지보수할 필요가 없어요.
더 알아보기


