0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pinterest動画の「保存したい」を技術で解決:スクレイピングからCDN解析まで、実践的な実装メモ

0
Posted at

Pinterest動画の「保存したい」を技術で解決:スクレイピングからCDN解析まで、実践的な実装メモ

こんにちは、SNSのメディアアーカイブを趣味でやっているエンジニアです。

先日、デザインのインスピレーション収集プロジェクトで「Pinterest」にピン留めされた動画コンテンツをローカルに保存する必要が出てきました。「え、Pinterestの動画ってダウンロードできるの?」と思われるかもしれませんが、実は適切なアプローチを取れば、技術的には十分解析可能です。

ただし、最初に断っておきます。本記事で紹介する手法は、あくまで個人の学習・研究・デザイン参考目的での利用を前提としています。Pinterestの利用規約や著作権法は必ずご自身で確認し、適法な範囲でご活用ください。クリエイターの作品を無断で再配布・商用利用することは絶対に避けましょう。


Pinterest動画の「見えない構造」を技術的に分解する

InstagramやTikTokと違い、Pinterestの動画配信にはいくつかの「ちょっと変わった」特徴があります。

  1. GraphQL APIの採用
    動画メタデータは、ページ読み込み時に埋め込まれたJSONか、GraphQLエンドポイント経由で取得されます。単純なHTMLパースだけでは不十分なケースが。

  2. v.pinimg.com CDNの多重構造
    動画本体はv.pinimg.com配下のCDNで配信されており、解像度ごとに異なるURLが用意されています。

  3. Lazy Load + 動的読み込み
    スクロールに応じて動画が動的に読み込まれるため、静的なrequestsだけではコンテンツを取得できないことがあります。

  4. Pin IDとボード権限の絡み
    公開ピンと非公開ピンで取得フローが異なり、認証トークンが必要なケースも。

これらの課題を「手動で」突破するのは現実的ではありません。そこで、私が実際に運用している**「最小依存・保守性重視」の解析アプローチ**を紹介します。
Something went wrong


核心部分:動画メタデータの取得ロジック(Python例)

まず、Pinterestのピンページから動画URLを抽出する基本的な流れです。requestsBeautifulSoup、そして正規表現を組み合わせて使います。

import requests
import re
import json
from bs4 import BeautifulSoup

def extract_pinterest_video_url(pin_url: str) -> dict:
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
        'Accept-Language': 'ja-JP,ja;q=0.9,en;q=0.8',
        'Referer': 'https://www.pinterest.com/'
    }
    
    # 1. Pinページを取得
    resp = requests.get(pin_url, headers=headers)
    resp.raise_for_status()
    
    # 2. 埋め込みJSONから動画情報を抽出
    # Pinterestは<script id="initial-state">に状態を埋め込んでいる
    soup = BeautifulSoup(resp.text, 'html.parser')
    script_tag = soup.find('script', id='initial-state')
    
    if not script_tag:
        raise ValueError("initial-stateが見つかりません")
    
    # 3. JSONをパースして動画URLを探索
    try:
        initial_state = json.loads(script_tag.string)
        # 構造は変更される可能性があるため、柔軟に探索
        video_data = _traverse_video_info(initial_state)
        return video_data
    except json.JSONDecodeError:
        # Fallback: 正規表現でvideo_urlを検索
        return _fallback_regex_extract(resp.text)

def _traverse_video_info(obj):
    """再帰的にvideo/urlフィールドを探索する簡易実装"""
    if isinstance(obj, dict):
        if 'video' in obj and isinstance(obj['video'], dict):
            video = obj['video']
            if 'video_url' in video:
                return {
                    'video_url': video['video_url'],
                    'thumbnail': video.get('poster'),
                    'duration': video.get('duration')
                }
        for v in obj.values():
            result = _traverse_video_info(v)
            if result:
                return result
    elif isinstance(obj, list):
        for item in obj:
            result = _traverse_video_info(item)
            if result:
                return result
    return None

def _fallback_regex_extract(html: str) -> dict:
    """正規表現によるフォールバック抽出"""
    # video_urlのパターンを検索(簡易版)
    match = re.search(r'"video_url"\s*:\s*"([^"]+\.mp4[^"]*)"', html)
    if match:
        return {'video_url': match.group(1).replace('\\u0026', '&')}
    raise ValueError("動画URLを抽出できませんでした")

💡 ポイント: Pinterestは頻繁にフロントエンドの構造を変更するため、initial-stateのような実装詳細に依存しすぎないよう、複数の抽出方法を用意しておくのが吉です。
pinterest_pic (2)low.png


CDN URLの解決と高解像度取得のコツ

Pinterestの動画URLは、しばしば以下のような形式になっています:

https://v.pinimg.com/videos/mc/720p/ab/cd/ef/abcdef123456.mp4

ここで注目すべきは720pの部分。URLを少し編集するだけで、異なる解像度を取得できることがあります。

def get_available_resolutions(base_url: str) -> list:
    """URLパターンから利用可能な解像度を推定"""
    resolutions = ['236p', '480p', '720p', '1080p']
    available = []
    
    for res in resolutions:
        candidate = re.sub(r'/(?:\d+p)/', f'/{res}/', base_url)
        # HEADリクエストで存在確認(実際の運用ではレートリミットに注意)
        try:
            resp = requests.head(candidate, timeout=3)
            if resp.status_code == 200:
                available.append((res, candidate))
        except:
            continue
    return available

※ 実際のプロダクションでは、並列リクエストの制限・キャッシュ・エラーハンドリングを適切に実装してください。また、CDNの仕様変更に備えて、取得したURLは可能な限り早くダウンロードする設計が望ましいです。


「毎回スクリプトを動かすのは面倒」→ Webツール化の選択

上記のような処理を毎回Pythonスクリプトで実行するのは、デザイナーやマーケターの皆様にはハードルが高いですよね。そこで、私はこのロジックをWebインターフェースにラップし、URLを貼り付けるだけでダウンロードできるようにしました。

今回ご紹介している Pinterest動画ダウンローダー は、まさにそのような「技術的バックエンド + 直感的UI」を組み合わせたツールです。

技術スタックの裏側(少しだけ公開)

  • フロントエンド: Vue 3 + Composition API。URL入力・解析状態・解像度選択をリアクティブに管理。Pinのサムネイルをプリフェッチして表示速度を最適化。
  • バックエンド: Python(FastAPI) + asyncio。動画解析を非同期で処理し、同時リクエストへの対応を強化。
  • キャッシュ戦略: 同じPinのメタデータはRedisに短期間キャッシュ。ユーザー体験の向上とサーバー負荷軽減を両立。
  • セキュリティ: 入力値のサニタイズ、CORS制限、レートリミット(1IP/分間10リクエスト)により悪用を防止。
  • プライバシー: 動画データの一時保存はメモリ内のみ。ダウンロード完了後即破棄。アクセスログにも動画URLを記録しません。

特に「サーバー側に動画を保存しない」設計は重要で、ストレージコストと法的リスクの両方を抑えつつ、ユーザーに「自分の責任でダウンロード」してもらう姿勢を明確にしています。


実際の使い流れ(技術者目線で深掘り)

  1. URLの正規化とバリデーション
    PinterestのURLには?utm_source=...&pin_id=...のようなパラメータが付いていることがあります。ツール内部ではurllib.parseでクエリを整理し、pinterest.com/pin/またはpin.it/パターンにマッチするか検証しています。

  2. ショートURLの展開処理
    pin.it/xxxxのようなショートURLは、HTTPリダイレクトを追随して本来のPin URLに解決。requestsallow_redirects=Trueを活用しつつ、リダイレクトループの検出も実装。

  3. フォールバック機構の多層化
    initial-stateからの抽出→GraphQL APIへの直接問い合わせ→ヘッドレスブラウザによるレンダリング、を順次試行。これにより「ページ構造が変わって動かない」というトラブルを最小限に抑えています。

  4. 解像度選択とフォーマット変換
    取得した動画がWebM形式の場合、ユーザーの要望に応じてMP4に変換。バックエンドでffmpegを非同期実行し、進捗をWebSocketでフロントに通知しています。


開発中にハマったポイント(実体験・失敗談)

① initial-stateのJSON構造が環境によって異なる

最初は「PCブラウザと同じ構造がスマホでも取れる」と思い込んでいたのですが、実際はUser-Agentによって埋め込みデータが微妙に異なるケースがありました。対策として、ツールでは複数のUser-Agentパターンを試行し、最も豊富なメタデータが取得できるものを選択するロジックを実装。

② CDNの署名パラメータ有効期限

PinterestのCDN URLには?token=...のような署名パラメータが含まれることがあり、有効期限が短い(数時間)場合があります。対策として、メタデータ取得→ダウンロード開始を可能な限り短時間で完結させる設計に。ユーザーには「解析完了後はお早めにダウンロードください」と優しく案内しています。

③ 日本語エラーメッセージの「優しさ」バランス

「JSONパースエラー」と言われても一般ユーザーには伝わらない。かといって「何か問題が発生しました」だけだと技術者は困る。本ツールでは、エラーコード+平易な説明+技術者向けヒントの3層構造でエラー表示を実装。Qiita読者の皆様なら、この「使い手への配慮」、共感していただけるかと。


法的・倫理的なラインについて(ここはしっかり書きます)

技術的に「できる」ことと、「してよい」ことは別問題です。本ツールの利用にあたっては、以下の点を強く意識しています。

推奨される使い方

  • デザインのインスピレーション収集のための個人アーカイブ
  • 自分が作成したコンテンツのバックアップ
  • 学術研究・市場調査での一時的な参考利用(再配布なし)

避けるべき使い方

  • ダウンロードした動画をSNSやポートフォリオに無断で再アップロード
  • クリエイターの作品を商用目的で無断利用
  • 大量ダウンロードによるPinterestサーバーへの負荷攻撃

ツールページにも明記していますが、ユーザー自身の行動に対する法的責任はユーザー本人にあります。技術者は「できること」を提供するだけでなく、「どう使うべきか」のガイドラインも併せて提示する責任があると考えています。


Qiita読者への特別ヒント:自作ツールに「機能追加」するなら

もし本記事を読んで「自分でも作ってみよう」と思われた方へ、追加で実装すると喜ばれる機能を3つご紹介します。

  1. ボード単位の一括取得
    研究用途では「特定のボードにピン留めされた動画をまとめて取得したい」というニーズがあります。ボードURLからPin一覧を抽出→キューに投入→完了順にダウンロード、というフローを実装すると、プロユースに一気に近づきます。
// 簡易例:ボードURLからピン一覧を取得するフロントエンド処理
async function fetchBoardPins(boardUrl) {
  const pins = [];
  let bookmark = null;
  
  while (true) {
    const api = `https://api.pinterest.com/v5/boards/${boardId}/pins${bookmark ? `?bookmark=${bookmark}` : ''}`;
    const resp = await fetch(api, {headers: {'Authorization': `Bearer ${token}`}});
    const data = await resp.json();
    pins.push(...data.items);
    
    if (!data.bookmark) break;
    bookmark = data.bookmark;
    
    // レートリミット配慮
    await new Promise(r => setTimeout(r, 500));
  }
  return pins;
}
  1. メタデータのJSON/CSV出力
    動画タイトル・投稿者・ピン日・ボード名などを構造化データで出力できるようにすると、分析用途での活用が格段に便利になります。

  2. ブラウザ拡張機能との連携
    Pinterest閲覧中に「この動画保存したい」と思った瞬間に、拡張機能からツールにURLを送信。UXが劇的に向上します。chrome.runtime.sendMessageとFastAPIのエンドポイントを組み合わせるだけなので、実装コストも低め。


おわりに:技術は「創造の補助」のために

Pinterest動画のダウンロードというテーマは、一見「便利ツール」の域を出ないように見えるかもしれません。しかし、裏側を覗くと、GraphQL・CDN解析・非同期処理・UI/UX設計・法遵守…と、フルスタックエンジニアの腕が試される要素が詰まっています。

今回ご紹介した Pinterest動画ダウンローダー は、そうした技術的課題を「誰でも使える形」に落とし込んだ一つの答えです。もちろん完璧ではなく、Pinterestの仕様変更には随時対応が必要ですが、オープンな技術スタックで構築しているため、コミュニティからのフィードバックも歓迎しています。

📌 最後に一言
技術の可能性を追求する一方で、クリエイターへのリスペクトを忘れずに。
「インスピレーションを得る」と「権利を守る」のバランスを取ることこそ、プロフェッショナルの腕の見せ所だと信じています。

何か質問や技術的な議論があれば、コメント欄で気軽にどうぞ。
それでは、よいピン留めライフを!📌✨


参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?