인스타그램 공개 데이터 스크래핑: 프록시 활용 완벽 가이드

인스타그램 공개 데이터를 수집하기 위한 프록시 설정, Python 구현 예제, 그리고 레지덴셜 프록시가 필수적인 이유까지 상세히 다룹니다. 중급 Python 개발자를 위한 실전 가이드.

인스타그램 공개 데이터 스크래핑: 프록시 활용 완벽 가이드

중요 고지: 이 가이드는 공개적으로 접근 가능한 데이터에 대한 합법적인 수집만을 다룹니다. 인스타그램 이용약관(Terms of Service)을 준수하고, 미국의 CFAA(Computer Fraud and Abuse Act), 유럽의 GDPR 등 관련 법률을 반드시 확인하시기 바랍니다. 로그인 자동화, 비공개 계정 데이터 접근, 스팸 활동은 절대 권장하지 않습니다.

인스타그램 스크래핑이 왜 어려운가

인스타그램은 세계에서 가장 정교한 안티봇 시스템 중 하나를 운영합니다. 단순 HTTP 요청으로 데이터를 수집하려는 시도는 대부분 몇 분 내로 차단됩니다. 그 이유를 이해하는 것이 성공적인 데이터 파이프라인 구축의 첫걸음입니다.

레이트 리밋과 요청 제한

인스타그램은 IP 주소별, 계정별로 엄격한 요청 제한을 적용합니다. 로그인하지 않은 상태에서는 시간당 약 200-500개의 요청만 허용되며, 이를 초과하면 429 Too Many Requests 응답과 함께 IP가 일시적으로 차단됩니다. 차단 지속 시간은 1시간에서 24시간까지 다양하며, 반복 위반 시 영구 차단으로 이어질 수 있습니다.

로그인 월과 인증 게이트

2020년 이후 인스타그램은 많은 공개 콘텐츠에 대해 로그인을 요구하기 시작했습니다. 특히 해시태그 페이지, 위치 페이지, 릴스 피드는 로그인 없이 접근이 제한됩니다. 이는 데이터 수집의 난이도를 크게 높였으며, 로그인 자동화는 이용약관 위반이자 법적 위험이 있으므로 피해야 합니다.

디바이스 핑거프린팅

인스타그램은 단순히 IP만 확인하지 않습니다. User-Agent, Accept-Language, 화면 해상도, 캔버스 핑거프린트, WebGL 렌더러, 폰트 목록, 타임존 등 수십 가지 브라우저 특성을 조합해 방문자를 식별합니다. 이러한 핑거프린트가 일관되지 않거나 '헤드리스 브라우저' 패턴으로 감지되면 즉시 차단됩니다.

안티봇 챌린지

의심스러운 활동이 감지되면 인스타그램은 다양한 챌린지를 트리거합니다:

  • JavaScript 챌린지: 브라우저 환경 검증 코드 실행 요구
  • CAPTCHA: 이미지 선택 또는 텍스트 인식
  • 전화번호 인증: 계정 연결 전화번호 입력 요구
  • 로그인 의무화: 콘텐츠 접근 전 로그인 페이지로 리다이렉트

로그인 없이 접근 가능한 데이터

인스타그램의 공개 데이터 접근 정책은 지속적으로 변화하지만, 현재 로그인 없이 수집 가능한 주요 데이터 유형은 다음과 같습니다:

데이터 유형 접근 가능성 제한 사항
공개 프로필 페이지 부분 가능 최근 게시물 일부만 표시, 스크롤 시 로그인 요구
개별 게시물 페이지 가능 댓글은 일부만 표시
해시태그 페이지 제한됨 로그인 게이트가 자주 표시됨
위치 페이지 제한됨 대부분 로그인 필요
릴스 피드 불가능 로그인 필수
스토리 불가능 로그인 필수

공개 프로필 페이지에서 수집할 수 있는 데이터에는 사용자명, 프로필 이미지, 바이오, 팔로워/팔로잉 수, 게시물 수, 최근 게시물 미리보기 등이 포함됩니다. 하지만 전체 게시물 목록을 가져오려면 스크롤이 필요하며, 이 시점에서 로그인 요구가 나타납니다.

레지덴셜 프록시가 필수인 이유

인스타그램 스크래핑에서 레지덴셜 프록시 선택은 선택이 아닌 필수입니다. 데이터센터 프록시를 사용하면 거의 확실하게 차단됩니다.

데이터센터 IP 차단 메커니즘

인스타그램은 IP 주소의 ASN(Autonomous System Number)을 확인하여 데이터센터 IP를 식별합니다. AWS, Google Cloud, DigitalOcean, Hetzner 등 주요 클라우드 제공업체의 IP 대역은 이미 블랙리스트에 등록되어 있습니다. 데이터센터 IP에서 요청이 들어오면:

  1. 즉시 403 Forbidden 응답
  2. 로그인 페이지로 강제 리다이렉트
  3. JavaScript 챌린지 트리거

레지덴셜 프록시의 우위

레지덴셜 프록시는 실제 가정용 인터넷 회선에서 할당된 IP 주소를 사용합니다. ISP(Internet Service Provider)가 발급한 진짜 사용자 IP이므로 인스타그램의 데이터센터 필터를 우회할 수 있습니다. 또한:

  • IP 회전: 요청마다 다른 레지덴셜 IP 사용 가능
  • 지역 다양성: 전 세계 다양한 국가/도시에서 오는 요청 시뮬레이션
  • 신뢰도: 실제 사용자 트래픽과 혼재되어 식별 어려움

핵심 인사이트: 인스타그램은 데이터센터 IP를 '봇'으로 간주합니다. 반면 레지덴셜 IP는 일반 사용자로 취급되며, 적절한 레이트 리밋만 지키면 장기간 안정적인 데이터 수집이 가능합니다.

모바일 프록시 고려사항

인스타그램은 모바일 우선 플랫폼이므로 4G/5G 모바일 프록시도 탁월한 선택입니다. 모바일 IP는 대규모 NAT 뒤에 있어 수천 명의 실제 사용자와 IP를 공유하므로, 안티봇 시스템이 이를 차단하기 매우 어렵습니다. 다만 비용이 높다는 단점이 있습니다.

Python으로 인스타그램 스크래핑 구현하기

이제 실제 코드로 레지덴셜 프록시를 활용한 인스타그램 공개 데이터 수집을 구현해보겠습니다. requests 라이브러리와 ProxyHat 레지덴셜 프록시를 사용합니다.

기본 설정 및 프록시 구성

import requests
import time
import random
from fake_useragent import UserAgent
from urllib.parse import quote

# ProxyHat 레지덴셜 프록시 설정
PROXY_GATEWAY = "gate.proxyhat.com"
PROXY_PORT = 8080
PROXY_USER = "your_username"  # ProxyHat 사용자명
PROXY_PASS = "your_password"  # ProxyHat 비밀번호

def get_proxy_url(country=None, session_id=None):
    """국가 및 세션 지정이 가능한 프록시 URL 생성"""
    username = PROXY_USER
    
    if country:
        username = f"{username}-country-{country}"
    if session_id:
        username = f"{username}-session-{session_id}"
    
    return f"http://{username}:{PROXY_PASS}@{PROXY_GATEWAY}:{PROXY_PORT}"

# 회전하는 User-Agent 생성
ua = UserAgent()

def get_realistic_headers():
    """실제 브라우저와 유사한 헤더 생성"""
    return {
        "User-Agent": ua.random,
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.5",
        "Accept-Encoding": "gzip, deflate, br",
        "DNT": "1",
        "Connection": "keep-alive",
        "Upgrade-Insecure-Requests": "1",
        "Sec-Fetch-Dest": "document",
        "Sec-Fetch-Mode": "navigate",
        "Sec-Fetch-Site": "none",
        "Sec-Fetch-User": "?1",
        "Cache-Control": "max-age=0",
    }

프로필 페이지 스크래핑

def scrape_profile(username, max_retries=3):
    """공개 프로필 페이지 스크래핑"""
    url = f"https://www.instagram.com/{username}/"
    
    for attempt in range(max_retries):
        try:
            # 각 요청마다 새로운 세션과 IP 사용
            session = requests.Session()
            session_id = f"ig_{int(time.time())}_{random.randint(1000, 9999)}"
            proxy_url = get_proxy_url(country="US", session_id=session_id)
            
            proxies = {
                "http": proxy_url,
                "https": proxy_url
            }
            
            headers = get_realistic_headers()
            
            # 요청 간 랜덤 지연 (인간적인 패턴)
            time.sleep(random.uniform(2, 5))
            
            response = session.get(
                url,
                headers=headers,
                proxies=proxies,
                timeout=30,
                allow_redirects=True
            )
            
            # 레이트 리밋 감지
            if response.status_code == 429:
                wait_time = 60 * (attempt + 1)
                print(f"Rate limited. Waiting {wait_time}s...")
                time.sleep(wait_time)
                continue
            
            # 로그인 리다이렉트 감지
            if "login" in response.url:
                print("Login wall detected. Trying different approach...")
                return None
            
            if response.status_code == 200:
                return parse_profile_html(response.text, username)
            
        except requests.exceptions.RequestException as e:
            print(f"Request failed: {e}")
            time.sleep(10)
    
    return None

def parse_profile_html(html, username):
    """HTML에서 프로필 데이터 추출"""
    import re
    import json
    
    # _sharedData 객체에서 JSON 추출
    pattern = r'window\._sharedData\s*=\s*({.+?});'
    match = re.search(pattern, html)
    
    if not match:
        print("Could not find shared data")
        return None
    
    try:
        data = json.loads(match.group(1))
        user_data = data.get('entry_data', {}).get('ProfilePage', [{}])[0].get('graphql', {}).get('user', {})
        
        return {
            'username': username,
            'full_name': user_data.get('full_name'),
            'biography': user_data.get('biography'),
            'followers': user_data.get('edge_followed_by', {}).get('count'),
            'following': user_data.get('edge_follow', {}).get('count'),
            'posts': user_data.get('edge_owner_to_timeline_media', {}).get('count'),
            'is_private': user_data.get('is_private'),
            'is_verified': user_data.get('is_verified'),
            'profile_pic_url': user_data.get('profile_pic_url_hd'),
        }
    except (json.JSONDecodeError, KeyError, IndexError) as e:
        print(f"Parsing error: {e}")
        return None

# 사용 예시
if __name__ == "__main__":
    profile = scrape_profile("instagram")
    if profile:
        print(json.dumps(profile, indent=2, ensure_ascii=False))

세션 격리와 IP 회전 전략

대규모 수집을 위해서는 세션 격리가 필수적입니다. 각 수집 대상마다 독립된 세션과 IP를 사용해야 합니다:

class InstagramScraper:
    def __init__(self, proxy_user, proxy_pass):
        self.proxy_user = proxy_user
        self.proxy_pass = proxy_pass
        self.request_count = 0
        self.max_requests_per_ip = 50  # IP당 최대 요청 수
        
    def rotate_session(self, country="US"):
        """새로운 세션과 IP로 회전"""
        session_id = f"scraper_{int(time.time())}_{random.randint(10000, 99999)}"
        self.current_session = session_id
        self.session_requests = 0
        
        return {
            "http": f"http://{self.proxy_user}-country-{country}-session-{session_id}:{self.proxy_pass}@gate.proxyhat.com:8080",
            "https": f"http://{self.proxy_user}-country-{country}-session-{session_id}:{self.proxy_pass}@gate.proxyhat.com:8080"
        }
    
    def should_rotate(self):
        """IP 회전 필요 여부 확인"""
        return self.session_requests >= self.max_requests_per_ip
    
    def make_request(self, url, country="US"):
        """자동 회전이 포함된 요청"""
        if not hasattr(self, 'current_session') or self.should_rotate():
            self.proxies = self.rotate_session(country)
            print(f"Rotated to new session: {self.current_session}")
        
        headers = get_realistic_headers()
        
        try:
            response = requests.get(
                url,
                headers=headers,
                proxies=self.proxies,
                timeout=30
            )
            self.session_requests += 1
            
            # 레이트 리밋 시 즉시 회전
            if response.status_code == 429:
                print("Rate limit hit, rotating IP...")
                self.proxies = self.rotate_session(country)
                return None
            
            return response
        
        except requests.exceptions.RequestException as e:
            print(f"Request error: {e}")
            self.proxies = self.rotate_session(country)
            return None

인스타그램 특화 기술 이슈

JSON 엔드포인트의 변화

과거에는 ?__a=1 쿼리 파라미터를 추가하면 JSON 형식의 데이터를 쉽게 얻을 수 있었습니다. 하지만 2021년 이후 이 엔드포인트는 대부분 차단되었거나 로그인을 요구합니다:

# 과거 방식 (현재 작동하지 않음)
url = f"https://www.instagram.com/{username}/?__a=1"

# 현재는 HTML에서 _sharedData를 추출해야 함
# 또는 GraphQL 쿼리 사용 (로그인 필요)

GraphQL 쿼리와 필수 헤더

인스타그램의 내부 API는 GraphQL을 사용합니다. 이를 직접 호출하려면 특정 헤더가 필요합니다:

def get_graphql_headers(csrf_token=None):
    """GraphQL API용 헤더"""
    headers = get_realistic_headers()
    headers.update({
        "x-ig-app-id": "936619743392459",  # Instagram Web App ID
        "x-requested-with": "XMLHttpRequest",
        "x-csrftoken": csrf_token or "",
        "Content-Type": "application/x-www-form-urlencoded",
    })
    return headers

# 주의: GraphQL 쿼리는 로그인 상태에서만 완전히 작동
# 비로그인 상태에서는 제한된 데이터만 접근 가능

HTTPS 인증서 고정(Pinning)

인스타그램 모바일 앱은 SSL 인증서 고정을 사용합니다. 앱 트래픽을 가로채려면 Frida나 Objection 같은 도구로 인증서 검증을 우회해야 합니다. 이는 고급 기법이며 법적/윤리적 문제가 복잡하므로 공개 웹 인터페이스를 통한 수집을 권장합니다.

모바일 API 리버스 엔지니어링

일부 스크래퍼는 인스타그램 모바일 앱의 API를 리버스 엔지니어링하여 사용합니다. 이는 다음을 포함합니다:

  • 앱 APK/IPA 디컴파일
  • API 엔드포인트 및 파라미터 분석
  • 서명 알고리즘 역추적

이러한 방식은 기술적으로 복잡할 뿐 아니라 이용약관 위반이며 법적 위험이 있습니다. 공개 웹 데이터 수집으로 제한하는 것을 강력히 권장합니다.

Node.js 구현 예제

Node.js 환경에서의 구현도 유사한 원칙을 따릅니다:

const axios = require('axios');
const { SocksProxyAgent } = require('socks-proxy-agent');

// ProxyHat 레지덴셜 프록시 설정
const PROXY_CONFIG = {
  host: 'gate.proxyhat.com',
  port: 8080,
  auth: {
    username: 'your_username-country-US-session-node1',
    password: 'your_password'
  }
};

const proxyAgent = new SocksProxyAgent(
  `http://${PROXY_CONFIG.auth.username}:${PROXY_CONFIG.auth.password}@${PROXY_CONFIG.host}:${PROXY_CONFIG.port}`
);

const realisticHeaders = () => ({
  'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
  'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
  'Accept-Language': 'en-US,en;q=0.5',
  'Accept-Encoding': 'gzip, deflate, br',
  'DNT': '1',
  'Connection': 'keep-alive',
});

async function scrapeInstagramProfile(username) {
  const url = `https://www.instagram.com/${username}/`;
  
  try {
    const response = await axios.get(url, {
      headers: realisticHeaders(),
      httpsAgent: proxyAgent,
      timeout: 30000,
    });
    
    if (response.status === 200) {
      // _sharedData 추출
      const match = response.data.match(/window\._sharedData\s*=\s*({.+?});/);
      if (match) {
        const data = JSON.parse(match[1]);
        const userData = data?.entry_data?.ProfilePage?.[0]?.graphql?.user;
        
        return {
          username: userData?.username,
          fullName: userData?.full_name,
          followers: userData?.edge_followed_by?.count,
          posts: userData?.edge_owner_to_timeline_media?.count,
        };
      }
    }
  } catch (error) {
    if (error.response?.status === 429) {
      console.log('Rate limited. Implementing backoff...');
    } else {
      console.error('Request failed:', error.message);
    }
  }
  
  return null;
}

// 사용
scrapeInstagramProfile('instagram').then(console.log);

레이트 리밋 관리와 안정성 확보

지속 가능한 스크래핑을 위해서는 스스로 레이트 리밋을 설정해야 합니다:

권장 요청 패턴

  • 요청 간격: 최소 2-5초 랜덤 지연
  • IP당 요청: 시간당 100개 미만
  • 일일 수집량: 대상당 1,000개 미만
  • 백오프 전략: 429 응답 시 지수 백오프 적용

지수 백오프 구현

import time
import random

def exponential_backoff(attempt, base_delay=1, max_delay=300):
    """지수 백오프 with 지터"""
    delay = min(base_delay * (2 ** attempt), max_delay)
    jitter = random.uniform(0, delay * 0.1)
    return delay + jitter

def request_with_backoff(url, max_retries=5):
    for attempt in range(max_retries):
        try:
            response = make_request(url)
            
            if response and response.status_code == 200:
                return response
            
            if response and response.status_code == 429:
                wait = exponential_backoff(attempt)
                print(f"Rate limited. Waiting {wait:.1f}s (attempt {attempt + 1})")
                time.sleep(wait)
            else:
                time.sleep(random.uniform(2, 5))
                
        except Exception as e:
            wait = exponential_backoff(attempt)
            print(f"Error: {e}. Retrying in {wait:.1f}s")
            time.sleep(wait)
    
    return None

윤리적 스크래핑 가이드라인

데이터 수집은 책임감 있게 수행해야 합니다. 다음 원칙을 준수하세요:

robots.txt 준수

인스타그램의 robots.txt를 확인하면 대부분의 경로가 크롤링을 금지하고 있습니다. 이는 플랫폼의 의도를 명확히 보여줍니다:

# instagram.com/robots.txt (요약)
User-agent: *
Disallow: /

이 가이드는 공개적으로 접근 가능한 제한된 데이터에 대한 기술적 방법론을 설명합니다. 대규모 무차별 수집은 권장하지 않습니다.

공식 API 사용 고려

Meta는 인스타그램 Graph API를 제공합니다. 비즈니스 계정, 미디어, 인사이트 데이터에 접근할 수 있습니다:

  • 장점: 합법적, 안정적, 문서화됨
  • 단점: 승인 필요, 제한된 엔드포인트, 할당량 존재

상업적 용도라면 공식 Instagram Graph API 사용을 먼저 검토하세요.

절대 피해야 할 행위

  • 로그인 자동화: 계정 생성/로그인 스크립트는 이용약관 위반
  • 비공개 계정 접근: 팔로우 승인 없이 비공개 데이터 접근 시도
  • 스팸/악용: 수집한 데이터를 스팸, 괴롭힘, 허위 정보에 활용
  • 과도한 요청: 서비스에 부하를 주는 무차별 수집
  • 개인정보 침해: GDPR, CCPA 등 개인정보보호법 위반

주요 요약

  • 레지덴셜 프록시 필수: 데이터센터 IP는 즉시 차단되므로 레지덴셜 또는 모바일 프록시만 사용
  • 로그인 없이 제한적 접근: 공개 프로필 일부 데이터만 수집 가능, 해시태그/위치 페이지는 대부분 로그인 필요
  • 핑거프린트 관리: User-Agent, 헤더, 요청 패턴을 실제 사용자와 유사하게 구성
  • 자체 레이트 리밋: IP당 시간당 100개 미만, 요청 간 2-5초 지연 권장
  • 세션 격리: 각 수집 세션에 독립된 IP와 식별자 사용
  • 윤리적 준수: 이용약관과 법률을 존중하며, 공식 API 사용을 우선 고려

인스타그램 공개 데이터 수집은 기술적으로 가능하지만, 상당한 주의와 전문성이 필요합니다. ProxyHat 레지덴셜 프록시를 활용하면 안정적인 IP 회전으로 지속 가능한 데이터 파이프라인을 구축할 수 있습니다. 소셜 리스닝, 시장 조사, 경쟁사 분석 등 합법적인 용도로 책임감 있게 활용하세요.

시작할 준비가 되셨나요?

AI 필터링으로 148개국 이상에서 5천만 개 이상의 레지덴셜 IP에 액세스하세요.

가격 보기레지덴셜 프록시
← 블로그로 돌아가기