Instagramスクレイピング完全ガイド:プロキシを使った公開データ収集

Instagramの公開データをプロキシ経由で収集する方法を解説。レート制限、ログイン壁、デバイスフィンガープリンティングへの対処法から、Pythonでの実装例、倫理的スクレイピングのベストプラクティスまで網羅します。

Instagramスクレイピング完全ガイド:プロキシを使った公開データ収集

重要な法的注意事項: 本記事は、一般公開されているデータへの正当なアクセス方法のみを解説しています。Instagramの利用規約、および適用法(米国のCFAA、EUのGDPRなど)を遵守してください。ログイン認証の自動化、非公開データへのアクセス、またはサービスへの妨害行為は推奨しません。公式APIの利用を常に優先してください。

Instagramスクレイピングの現状と課題

Instagramスクレイピングは、多くの開発者にとって魅力的なデータソースですが、Meta(旧Facebook)は強力な対策を講じています。ソーシャルリスニングツールや市場調査プラットフォームを構築する場合、Instagramの公開データは貴重な洞察を提供しますが、その収集には技術的なハードルがあります。

Instagramがスクレイピングを困難にしている主な理由は以下の通りです:

  • レート制限: IPアドレスごとに厳格なリクエスト制限があり、超過するとHTTP 429エラーが返されます
  • ログイン壁: 多くのコンテンツがログイン必須となり、匿名アクセス可能な範囲が縮小
  • アンチボットシステム: 不自然なアクセスパターンを検出し、CAPTCHAやブロックを表示
  • デバイスフィンガープリンティング: ブラウザ、OS、画面解像度などの組み合わせでボットを識別
  • 行動分析: マウス移動、スクロール速度、クリック間隔などの人間らしい行動を分析

これらの対策により、従来の単純なHTMLスクレイピング手法はほぼ機能しなくなりました。現在では、モバイルAPIのリバースエンジニアリングと、Instagramスクレイピングプロキシの適切な使用が不可欠です。

ログインなしでアクセス可能なデータ

Instagramの利用規約に従い、ログインせずにアクセスできる公開データに限定することが重要です。以下のデータは、ログインなしでアクセス可能な範囲です:

公開プロフィールページ

ユーザー名、プロフィール画像、バイオグラフィ、フォロワー数、投稿数などの基本情報。ただし、投稿の詳細を表示するにはログインが必要な場合があります。

ハッシュタグページ

特定のハッシュタグが付いた公開投稿の一部。人気投稿と最新投稿のサンプルが表示されますが、完全なリストにはアクセスできません。

ロケーションページ

特定の場所に関連する公開投稿。ジオタグ付きコンテンツのサンプリングに利用可能です。

Reelsフィード

公開設定のReels動画のサムネイルと基本メタデータ。ただし、継続的なスクロールにはログインが必要です。

ログイン壁で保護されるデータ: ストーリーズ、DM、非公開アカウントの投稿、完全なフィード履歴、詳細なエンゲージメント分析。これらへのアクセスを試みることは規約違反となります。

なぜデータセンタープロキシではなく住宅用プロキシなのか

Instagramスクレイピングにおいて、住宅用プロキシの使用は必須に近い選択肢です。理由は明確です:

特性 データセンタープロキシ 住宅用プロキシ
IPの起源 クラウドホスティング事業者のIPブロック 実際のISPから割り当てられた住宅IP
Instagramの検出 高確率でフラグ付け 一般ユーザーとほぼ区別不可能
ブロック率 非常に高い(70%以上) 低い(5-15%)
コスト 安価 中程度〜高価
速度 高速 中程度(変動あり)
推奨用途 テスト・開発環境のみ 本番スクレイピング

Instagramは、AWS、Google Cloud、DigitalOceanなどの主要クラウドプロバイダーのIPレンジをデータベース化しており、これらからのアクセスを自動的にフラグ付けします。一方、住宅用プロキシは実際の家庭からのインターネット接続を経由するため、Instagramのシステムには「本物のユーザー」として認識されます。

モバイルプロキシ(4G/5G)はさらに高い信頼性を提供しますが、コストが高いため、重要なプロジェクトに限定して使用することをお勧めします。

Pythonでの実装:requests + ローテーション住宅用プロキシ

以下に、ProxyHatの住宅用プロキシプールを使用してInstagramの公開データを収集するPythonコードを示します。適切なヘッダー設定、ユーザーエージェントのローテーション、セッション分離を実装しています。

基本セットアップ

import requests
import random
import time
from fake_useragent import UserAgent

# ProxyHat接続設定
PROXY_GATEWAY = "gate.proxyhat.com"
PROXY_PORT = 8080
PROXY_USER = "your_username"
PROXY_PASS = "your_password"

def get_proxy_url(country=None, session_id=None):
    """国指定とセッションIDを含むプロキシURLを生成"""
    username_parts = [PROXY_USER]
    
    if country:
        username_parts.append(f"country-{country}")
    if session_id:
        username_parts.append(f"session-{session_id}")
    
    username = "-".join(username_parts)
    return f"http://{username}:{PROXY_PASS}@{PROXY_GATEWAY}:{PROXY_PORT}"

# ユーザーエージェントローテーション
ua = UserAgent(platforms=['mobile', 'desktop'])

def get_random_headers():
    """リアルなリクエストヘッダーを生成"""
    return {
        "User-Agent": ua.random,
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.9",
        "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",
    }

公開プロフィールのスクレイピング

def scrape_public_profile(username, max_retries=3):
    """
    公開プロフィールページから基本情報を抽出
    注意: __a=1エンドポイントは現在制限されています
    """
    url = f"https://www.instagram.com/{username}/"
    
    for attempt in range(max_retries):
        try:
            # 各リクエストで新しいセッションIDを使用(IPローテーション)
            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
            }
            
            session = requests.Session()
            session.proxies = proxies
            session.headers.update(get_random_headers())
            
            # 適切な遅延を入れる
            time.sleep(random.uniform(2, 5))
            
            response = session.get(url, timeout=30)
            
            if response.status_code == 200:
                # HTMLからメタデータを抽出
                # Instagramは現在SSRで基本情報を埋め込んでいます
                return parse_profile_html(response.text, username)
                
            elif response.status_code == 429:
                print(f"Rate limited. Waiting before retry...")
                time.sleep(60 * (attempt + 1))
                continue
                
            elif response.status_code == 404:
                print(f"Profile not found: {username}")
                return None
                
            else:
                print(f"Unexpected status: {response.status_code}")
                
        except requests.exceptions.RequestException as e:
            print(f"Request failed: {e}")
            time.sleep(30)
            
    return None

def parse_profile_html(html, username):
    """HTMLレスポンスからプロフィール情報を抽出"""
    import re
    import json
    
    # 共有データを探す
    pattern = r'window\._sharedData = ({.*?});</script>'
    match = re.search(pattern, html)
    
    if match:
        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"Failed to parse profile data: {e}")
    
    return None

Node.jsでの実装例

const axios = require('axios');
const { HttpsProxyAgent } = require('https-proxy-agent');

const PROXY_CONFIG = {
  gateway: 'gate.proxyhat.com',
  port: 8080,
  username: 'your_username',
  password: 'your_password'
};

function createProxyAgent(country = null, sessionId = null) {
  let username = PROXY_CONFIG.username;
  if (country) username += `-country-${country}`;
  if (sessionId) username += `-session-${sessionId}`;
  
  const proxyUrl = `http://${username}:${PROXY_CONFIG.password}@${PROXY_CONFIG.gateway}:${PROXY_CONFIG.port}`;
  return new HttpsProxyAgent(proxyUrl);
}

const USER_AGENTS = [
  'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1',
  'Mozilla/5.0 (Linux; Android 13; SM-S908B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile Safari/537.36',
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36'
];

async function fetchInstagramProfile(username) {
  const sessionId = `ig_${Date.now()}_${Math.floor(Math.random() * 9000) + 1000}`;
  const agent = createProxyAgent('US', sessionId);
  
  const config = {
    method: 'get',
    url: `https://www.instagram.com/${username}/`,
    httpsAgent: agent,
    headers: {
      'User-Agent': USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)],
      'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
      'Accept-Language': 'en-US,en;q=0.9',
      'Accept-Encoding': 'gzip, deflate, br',
      'Connection': 'keep-alive'
    },
    timeout: 30000
  };
  
  try {
    const response = await axios(config);
    return parseProfileData(response.data, username);
  } catch (error) {
    if (error.response?.status === 429) {
      console.log('Rate limited - implement backoff');
    }
    throw error;
  }
}

module.exports = { fetchInstagramProfile };

Instagram固有の技術的課題

__a=1 JSONエンドポイントの現状

以前は?__a=1パラメータを付与することで、ページのJSON形式データを取得できました。しかし、Instagramは2023年以降、このエンドポイントへのアクセスを厳格に制限しています。現在は以下の対策が必要です:

  • HTMLレスポンスからwindow._sharedDataオブジェクトを抽出
  • 追加のGraphQLクエリには認証が必要な場合が多い
  • モバイルWeb版のエンドポイントを使用するアプローチ

GraphQLクエリとヘッダー

Instagramの内部APIにアクセスする場合、特定のヘッダーが必要です:

GRAPHQL_HEADERS = {
    "x-ig-app-id": "936619743392459",  # Instagram Web App ID
    "x-requested-with": "XMLHttpRequest",
    "x-csrftoken": "",  # セッションから取得が必要
    "content-type": "application/x-www-form-urlencoded",
}

# 注意: x-csrftokenを取得するには、まずページを訪問して
# Cookieから値を抽出する必要があります
# これはログインなしのセッションでも必要です

HTTPS証明書ピンニング

Instagramのモバイルアプリは証明書ピンニングを実装していますが、Webスクレイピングでは直接影響しません。ただし、中間者攻撃検出のために、プロキシ接続が正常に機能していることを確認してください。

HTMLスクレイピングからモバイルAPIへの移行

InstagramのHTML構造は頻繁に変更されるため、可能な限り構造化されたエンドポイントを使用することをお勧めします。ただし、モバイルAPIのリバースエンジニアリングは法的リスクを伴うため、公開Webページからの情報抽出に留めることが重要です。

レート制限と信頼性のベストプラクティス

自己規制の実装

import time
from collections import deque

class RateLimiter:
    """Instagramスクレイピング用のレートリミッター"""
    
    def __init__(self, requests_per_hour=100, min_interval=10):
        self.requests_per_hour = requests_per_hour
        self.min_interval = min_interval
        self.request_times = deque()
        self.last_request = 0
    
    def wait_if_needed(self):
        """必要に応じて待機"""
        now = time.time()
        
        # 最小間隔を確保
        time_since_last = now - self.last_request
        if time_since_last < self.min_interval:
            time.sleep(self.min_interval - time_since_last)
        
        # 1時間あたりのリクエスト数を制限
        hour_ago = now - 3600
        while self.request_times and self.request_times[0] < hour_ago:
            self.request_times.popleft()
        
        if len(self.request_times) >= self.requests_per_hour:
            wait_time = self.request_times[0] + 3600 - now + 60
            print(f"Hourly limit reached. Waiting {wait_time:.0f} seconds...")
            time.sleep(wait_time)
        
        self.request_times.append(time.time())
        self.last_request = time.time()

# 使用例
limiter = RateLimiter(requests_per_hour=80, min_interval=15)

for username in target_usernames:
    limiter.wait_if_needed()
    profile = scrape_public_profile(username)
    # データを処理...

エラーハンドリングとリトライ戦略

  • 429エラー: 指数バックオフで再試行(60秒、120秒、240秒)
  • 403エラー: IPがブロックされている可能性。別のプロキシIPに切り替え
  • CAPTCHA: 自動回避を試みず、手動介入またはスキップ
  • タイムアウト: プロキシの品質問題の可能性。別のロケーションを試行

倫理的スクレイピングのガイドライン

Instagramスクレイピングを行う際は、以下の倫理的ガイドラインを厳守してください:

robots.txtの尊重

Instagramのrobots.txtを確認し、禁止されているパスへのアクセスを避けてください。現在、Instagramはほぼ全てのパスをボットに禁止しています。

自己規制レート制限

プラットフォームに負荷をかけないよう、1時間あたりのリクエスト数を厳格に制限してください。目安は1時間あたり50-100リクエスト以下です。

ログイン自動化の回避

ログインプロセスの自動化は、利用規約違反であり、セキュリティリスクも高いため絶対に避けてください。アカウント停止のリスクがあります。

データの適切な取り扱い

  • 収集したデータを販売しない
  • 個人のプライバシーを尊重
  • GDPR、CCPAなどの規制を遵守
  • データの保存期間を最小限に

公式APIの優先

Instagram Graph APIやBasic Display APIが要件を満たす場合は、そちらを優先してください。公式APIは安定しており、法的リスクもありません。

重要: 商用利用や大規模データ収集を計画している場合、必ず弁護士に相談し、Instagramの最新の利用規約を確認してください。本記事の内容は技術的な教育目的であり、法的助言ではありません。

Key Takeaways

  • 住宅用プロキシは必須: データセンターIPはInstagramに即座にフラグ付けされるため、本番環境では住宅用プロキシを使用
  • 公開データのみを対象: ログイン壁の背後にあるデータへのアクセスは規約違反
  • 自己規制が成功の鍵: 1時間あたり50-100リクエスト以下に制限し、リクエスト間隔を10秒以上空ける
  • 技術は常に変化: Instagramの対策は進化するため、定期的な手法の見直しが必要
  • 公式APIを検討: 長期的な安定性が必要な場合は、Instagram Graph APIの使用を検討
  • 法的コンプライアンス: 利用規約、GDPR、CCPAを常に意識

Instagramスクレイピングは技術的に可能ですが、適切なプロキシインフラと倫理的アプローチが不可欠です。ProxyHatの住宅用プロキシプールは、Instagramの公開データ収集に必要な信頼性とIP多様性を提供します。詳細については料金プランをご確認いただくか、Webスクレイピングのユースケースページで他の活用例をご覧ください。

始める準備はできましたか?

AIフィルタリングで148か国以上、5,000万以上のレジデンシャルIPにアクセス。

料金を見るレジデンシャルプロキシ
← ブログに戻る