关于 instagram-post-downloader
instagram-post-downloader 是用于下载 Instagram 公开帖子图片的技能,面向需要保存原始分辨率图片的用户。它支持单图、轮播图和批量 URL,并可将轮播图片合并为 PDF;Reels 仅支持封面图下载。
主要功能
- 01下载单图和轮播图片
- 02批量处理帖子 URL
- 03选择可用的高分辨率图片
- 04将轮播图合并为 PDF
- 05保存帖子元数据
适用场景
- 01保存公开帖子的图片原图
- 02批量归档多个 Instagram 帖子
- 03将轮播图整理为单独图片和 PDF
- 04下载 Reel 封面图片
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mohitagw15856/ pm-claude-skills --skill instagram-post-downloader安装后在 Claude Code 中输入/instagram-post-downloader确认已加载。
详细介绍
Instagram 帖子下载器技能
从 Instagram 的 CDN 下载原始分辨率的帖子内容——无截图、无压缩。支持单张图片、轮播(多张幻灯片的帖子)和 Reels 封面图片。对于轮播帖子,会生成单独的幻灯片文件以及一个拼接后的 PDF。支持一次批量处理多个 URL。
前置条件 — 域名允许列表
此技能要获取任何媒体内容,必须先将 Instagram 的 CDN 域名添加到 Claude Code 的允许列表中:
设置 → 功能 → 域名允许列表 → 添加:
*.cdninstagram.com
否则,所有 CDN 获取请求都会被拦截。如果 Claude 尝试从 cdninstagram.com 获取内容时出现权限错误,请按此操作解决。
必需输入
如果用户没有预先提供,Claude 会询问以下内容:
| 输入 | 必需 | 备注 |
|---|---|---|
| Instagram 帖子 URL | 是 | 每行一个,或以逗号分隔。格式为 https://www.instagram.com/p/XXXX/ 或 https://www.instagram.com/reel/XXXX/ |
| 输出目录 | 否 | 默认为当前工作目录下的 ./instagram-downloads/ |
| 是否拼接轮播 PDF | 否 | 默认为是——除单独的幻灯片外,还会生成 carousel.pdf |
| 文件名前缀 | 否 | 可选前缀,添加在幻灯片文件名之前,例如 brand_ → brand_slide_01.jpg |
批量输入示例:
https://www.instagram.com/p/ABC123/
https://www.instagram.com/p/DEF456/
https://www.instagram.com/p/GHI789/
输出结构
对于处理的每个 URL,Claude 都会创建一个以帖子说明文字命名的文件夹(取前 40 个字符并清理——空格替换为下划线,特殊字符移除)。如果没有可用的说明文字,则使用帖子 shortcode 命名文件夹。
单张图片帖子
instagram-downloads/
└── this_is_the_caption_first_40_chars/
├── image.jpg
└── metadata.txt
轮播帖子
instagram-downloads/
└── carousel_caption_first_40_chars/
├── slide_01.jpg
├── slide_02.jpg
├── slide_03.jpg
├── slide_04.jpg
├── carousel.pdf ← all slides stitched in order
└── metadata.txt
批量运行(3 个 URL)
instagram-downloads/
├── first_post_caption_sanitised/
│ ├── image.jpg
│ └── metadata.txt
├── second_post_carousel_caption/
│ ├── slide_01.jpg
│ ├── slide_02.jpg
│ ├── carousel.pdf
│ └── metadata.txt
└── third_post_caption_here/
├── image.jpg
└── metadata.txt
metadata.txt 格式
Post URL: https://www.instagram.com/p/XXXX/
Shortcode: XXXX
Type: carousel | single_image | reel
Slide count: 4 (carousel only)
Caption: [full caption text]
Username: @username
Fetched at: 2026-05-27T14:32:00Z
CDN URLs:
slide_01.jpg https://scontent.cdninstagram.com/v/...
slide_02.jpg https://scontent.cdninstagram.com/v/...
完成摘要(打印到终端)
Instagram Post Downloader — Batch Complete
==========================================
URLs processed: 3
Posts saved: 3
Total files: 11 (9 images + 2 PDFs)
Skipped: 0
Output dir: /Users/you/project/instagram-downloads/
Results:
✓ this_is_the_caption_first_40_chars/ 1 image
✓ carousel_caption_first_40_chars/ 4 slides → carousel.pdf
✓ third_post_caption_here/ 1 image
Claude 应如何执行此技能
第 1 步 — 收集并验证输入
- 接受用户提供的 URL。如果用户粘贴的是逗号分隔列表,则按逗号拆分;如果是每行一个,则按换行符拆分。
- 验证每个 URL 是否匹配
instagram.com/p/、instagram.com/reel/或instagram.com/tv/。继续操作前,标出格式错误的 URL。 - 确认输出目录。如果用户未提供,则使用
./instagram-downloads/并告知用户。 - 仅当用户尚未表明 PDF 拼接偏好时,才询问其偏好。默认选择是。
第 2 步 — 对每个 URL:获取帖子页面
获取 Instagram 帖子的页面 HTML:
GET https://www.instagram.com/p/{shortcode}/?__a=1&__d=dis
Instagram 经常更改其 API。请按顺序使用以下回退方案:
尝试 A — JSON 端点:
https://www.instagram.com/p/{shortcode}/?__a=1&__d=dis
解析 JSON 响应。查找 graphql.shortcode_media 或 data.shortcode_media。
尝试 B — 嵌入页面(最可靠):
https://www.instagram.com/p/{shortcode}/embed/captioned/
获取此页面的 HTML,并从 <script> 标签中提取 og:image 元标签,以及任何嵌入的 window.__additionalDataLoaded 或 window.__StaticData JSON 数据块。
尝试 C — oEmbed 端点:
https://api.instagram.com/oembed/?url=https://www.instagram.com/p/{shortcode}/&omitscript=true
此端点会返回 thumbnail_url——适用于单张图片,但对于轮播帖子,只会返回第一帧。
所有请求都需包含的标头:
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36
Accept-Language: en-US,en;q=0.9
Accept: text/html,application/xhtml+xml,application/json
第 3 步 — 提取 CDN 图片 URL
从获取的数据中提取所有高分辨率 CDN URL。Instagram CDN URL 遵循以下模式:
https://scontent.cdninstagram.com/v/...jpg?...
https://scontent-lax3-1.cdninstagram.com/v/...jpg?...
https://instagram.fXXX1-1.fbcdn.net/v/...jpg?...
对于单张图片帖子:
- 提取单个
display_url,或选择最大的display_resources条目(选取config_width最高的条目)。
对于轮播帖子:
- 在 JSON 中查找
edge_sidecar_to_children.edges[]。每个边都有自己的node.display_url和node.display_resources[]。 - 按顺序遍历所有边。此顺序决定幻灯片编号。
- 从每张幻灯片的
display_resources数组中选择分辨率最高的版本。
对于 Reels:
- 封面图片可以按单张图片相同的方式提取。
- 视频文件本身需要借助第三方工具(见“额外内容”部分)。
如果 JSON 提取失败,则退而抓取页面 HTML 中的 <meta property="og:image"> 标签——这至少能提供一个图片 URL(第一张幻灯片或唯一的图片)。
步骤 4 — 清理文件夹名称
根据帖子说明生成文件夹名称:
- 取说明的前 40 个字符。
- 删除所有非字母数字、空格或连字符的字符。
- 将空格和连字符替换为下划线。
- 将结果转换为小写。
- 删除开头和结尾的下划线。
- 如果结果为空(例如说明全是表情符号),则使用帖子 shortcode。
import re
def sanitise_folder_name(caption: str, shortcode: str) -> str:
truncated = caption[:40]
cleaned = re.sub(r'[^a-zA-Z0-9 \-]', '', truncated)
underscored = re.sub(r'[\s\-]+', '_', cleaned).strip('_').lower()
return underscored if underscored else shortcode
步骤 5 — 创建输出文件夹结构
import os
base_dir = "./instagram-downloads"
folder_name = sanitise_folder_name(caption, shortcode)
post_dir = os.path.join(base_dir, folder_name)
os.makedirs(post_dir, exist_ok=True)
如果已存在同名文件夹(例如重复运行同一 URL),则追加 shortcode 以避免冲突:folder_name_SHORTCODE。
步骤 6 — 下载每个图片文件
对于每个 CDN URL,使用流式 GET 请求下载文件:
import requests
def download_file(url: str, dest_path: str) -> bool:
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
"Referer": "https://www.instagram.com/",
}
response = requests.get(url, headers=headers, stream=True, timeout=30)
response.raise_for_status()
with open(dest_path, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
return True
文件命名:
- 单张图片:
image.jpg - 轮播图幻灯片:
slide_01.jpg、slide_02.jpg……(用 0 补齐至 2 位;若超过 99 张,则补齐至 3 位)
根据 Content-Type 标头或 URL 扩展名检测文件格式。Instagram 照片通常为 JPEG,在某些情况下也可能提供 WebP — 请保留实际扩展名。
步骤 7 — 拼接轮播图 PDF(如适用)
下载所有幻灯片后,使用 Pillow 将它们拼接为一个 PDF:
from PIL import Image
def stitch_to_pdf(image_paths: list[str], output_path: str) -> None:
"""
Combine a list of image files into a single multi-page PDF.
Each image becomes one page. Page size matches the image dimensions.
"""
images = []
for path in sorted(image_paths): # sort ensures slide_01, slide_02, ... order
img = Image.open(path).convert("RGB")
images.append(img)
if not images:
return
first = images[0]
rest = images[1:]
first.save(
output_path,
format="PDF",
save_all=True,
append_images=rest,
resolution=150.0,
)
将文件保存为帖子文件夹中的 carousel.pdf。如果尚未安装 Pillow,请先运行 pip install Pillow,或指导用户执行此操作。
技能启动时检查依赖项:
try:
from PIL import Image
except ImportError:
print("Pillow not installed. Run: pip install Pillow")
print("PDF stitching will be skipped. Individual slides will still be downloaded.")
skip_pdf = True
步骤 8 — 编写 metadata.txt
在帖子文件夹中写入 metadata.txt 文件,包含所有提取的元数据:
from datetime import datetime, timezone
def write_metadata(post_dir, post_url, shortcode, post_type, caption, username, cdn_urls):
lines = [
f"Post URL: {post_url}",
f"Shortcode: {shortcode}",
f"Type: {post_type}",
]
if post_type == "carousel":
lines.append(f"Slide count: {len(cdn_urls)}")
lines += [
f"Caption: {caption}",
f"Username: @{username}",
f"Fetched at: {datetime.now(timezone.utc).isoformat()}",
"CDN URLs:",
]
for filename, url in cdn_urls.items():
lines.append(f" {filename:<16} {url}")
with open(os.path.join(post_dir, "metadata.txt"), "w", encoding="utf-8") as f:
f.write("\n".join(lines) + "\n")
步骤 9 — 打印完成摘要
处理完所有 URL 后,在终端中打印摘要表(格式见上文“输出结构”部分)。包括:
- 尝试处理的 URL 总数
- 成功保存的帖子数
- 写入的文件总数(分别统计图片和 PDF)
- 所有跳过的 URL 及原因
步骤 10 — 妥善处理错误
| 错误情形 | 操作 |
|---|---|
| URL 不是 Instagram URL | 跳过并显示消息:"Skipped — not an Instagram URL: [url]" |
| 帖子为私密内容或需要登录 | 跳过并显示消息:"Skipped — post is private or login required: [url]" |
| CDN 请求返回 403/404 | 如果有备用 CDN URL,则尝试使用;否则跳过该幻灯片并在元数据中注明 |
| 未安装 Pillow | 跳过 PDF 拼接,仅保存幻灯片,并在摘要中注明 |
| 网络超时 | 5 秒后重试一次;若仍失败,则跳过并记录 |
| 文件夹名称冲突 | 在文件夹名称后追加 shortcode 后缀 |
| 触发速率限制(429) | 等待 10 秒后重试;若重试仍失败,则记录 |
附加内容 — 下载 Instagram Reels(视频)
此技能涵盖图片和轮播图 PDF。对于 Reels 视频文件,Claude Code 无法直接下载视频,需要借助第三方工具,因为 Instagram 的视频 CDN 使用带签名的 URL 和额外的身份验证令牌。
Reels 的推荐方法:
使用 yt-dlp,这是一款持续维护的开源工具:
# Install
pip install yt-dlp
# Download a Reel
yt-dlp "https://www.instagram.com/reel/XXXX/" -o "%(title)s.%(ext)s"
# Download to a specific folder
yt-dlp "https://www.instagram.com/reel/XXXX/" \
-o "./instagram-downloads/%(uploader)s_%(id)s.%(ext)s"
# Download best quality
yt-dlp -f "bestvideo+bestaudio" "https://www.instagram.com/reel/XXXX/"
如果用户提出要求,Claude 可以通过 Bash 运行此命令。对于公开的 Reels,yt-dlp 会自动提取身份验证令牌。
完整脚本模板
Claude 应主动提出将其编写为独立脚本(instagram_downloader.py),供用户单独运行:
#!/usr/bin/env python3
"""
Instagram Post Downloader
Fetches high-res images from public Instagram posts and carousels.
Requires: pip install requests Pillow
"""
import os
import re
import sys
import json
import time
import requests
from datetime import datetime, timezone
from pathlib import Path
try:
from PIL import Image
PILLOW_AVAILABLE = True
except ImportError:
PILLOW_AVAILABLE = False
print("Warning: Pillow not installed. PDF stitching disabled. Run: pip install Pillow")
HEADERS = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Accept-Language": "en-US,en;q=0.9",
"Referer": "https://www.instagram.com/",
}
def extract_shortcode(url: str) -> str:
match = re.search(r"instagram\.com/(?:p|reel|tv)/([A-Za-z0-9_-]+)", url)
if not match:
raise ValueError(f"Cannot extract shortcode from URL: {url}")
return match.group(1)
def fetch_post_data(shortcode: str) -> dict:
"""Try multiple endpoints to get post JSON data."""
# Attempt A: JSON endpoint
try:
url = f"https://www.instagram.com/p/{shortcode}/?__a=1&__d=dis"
r = requests.get(url, headers=HEADERS, timeout=15)
if r.status_code == 200:
data = r.json()
media = (data.get("graphql", {}).get("shortcode_media") or
data.get("data", {}).get("shortcode_media"))
if media:
return media
except Exception:
pass
# Attempt B: Embed page
try:
url = f"https://www.instagram.com/p/{shortcode}/embed/captioned/"
r = requests.get(url, headers=HEADERS, timeout=15)
html = r.text
# Look for JSON blob in script tags
matches = re.findall(r'window\.__additionalDataLoaded\([^,]+,(\{.+?\})\);', html)
for blob in matches:
try:
data = json.loads(blob)
media = (data.get("graphql", {}).get("shortcode_media") or
data.get("data", {}).get("shortcode_media"))
if media:
return media
except json.JSONDecodeError:
continue
except Exception:
pass
return {}
def get_cdn_urls(media: dict) -> list[tuple[str, str]]:
"""Return list of (filename, cdn_url) tuples."""
results = []
media_type = media.get("__typename", "")
if media_type == "GraphSidecar":
edges = media.get("edge_sidecar_to_children", {}).get("edges", [])
for i, edge in enumerate(edges, start=1):
node = edge.get("node", {})
resources = node.get("display_resources", [])
url = (max(resources, key=lambda r: r.get("config_width", 0)).get("src")
if resources else node.get("display_url", ""))
if url:
ext = "jpg" if "jpg" in url.lower() else "webp"
filename = f"slide_{i:02d}.{ext}"
results.append((filename, url))
else:
resources = media.get("display_resources", [])
url = (max(resources, key=lambda r: r.get("config_width", 0)).get("src")
if resources else media.get("display_url", ""))
if url:
ext = "jpg" if "jpg" in url.lower() else "webp"
results.append((f"image.{ext}", url))
return results
def sanitise_folder_name(caption: str, shortcode: str) -> str:
truncated = caption[:40] if caption else ""
cleaned = re.sub(r"[^a-zA-Z0-9 \-]", "", truncated)
underscored = re.sub(r"[\s\-]+", "_", cleaned).strip("_").lower()
return underscored if underscored else shortcode
def download_file(url: str, dest_path: str) -> bool:
r = requests.get(url, headers=HEADERS, stream=True, timeout=30)
r.raise_for_status()
with open(dest_path, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
return True
def stitch_pdf(image_paths: list[str], output_path: str) -> None:
if not PILLOW_AVAILABLE:
return
images = [Image.open(p).convert("RGB") for p in sorted(image_paths)]
if images:
images[0].save(output_path, format="PDF", save_all=True,
append_images=images[1:], resolution=150.0)
def process_url(post_url: str, base_dir: str, stitch_pdf_flag: bool) -> dict:
result = {"url": post_url, "status": "ok", "files": [], "error": None}
try:
shortcode = extract_shortcode(post_url)
media = fetch_post_data(shortcode)
caption = ""
username = ""
if media:
caption_edges = media.get("edge_media_to_caption", {}).get("edges", [])
caption = caption_edges[0]["node"]["text"] if caption_edges else ""
owner = media.get("owner", {})
username = owner.get("username", "")
folder_name = sanitise_folder_name(caption, shortcode)
post_dir = os.path.join(base_dir, folder_name)
if os.path.exists(post_dir):
post_dir = f"{post_dir}_{shortcode}"
os.makedirs(post_dir, exist_ok=True)
cdn_urls = get_cdn_urls(media) if media else []
if not cdn_urls:
# Fallback: oEmbed
oembed_url = f"https://api.instagram.com/oembed/?url={post_url}&omitscript=true"
r = requests.get(oembed_url, headers=HEADERS, timeout=10)
if r.status_code == 200:
thumb = r.json().get("thumbnail_url", "")
if thumb:
cdn_urls = [("image.jpg", thumb)]
username = r.json().get("author_name", "")
downloaded_paths = []
cdn_map = {}
for filename, url in cdn_urls:
dest = os.path.join(post_dir, filename)
download_file(url, dest)
downloaded_paths.append(dest)
cdn_map[filename] = url
result["files"].append(filename)
if stitch_pdf_flag and len(downloaded_paths) > 1 and PILLOW_AVAILABLE:
pdf_path = os.path.join(post_dir, "carousel.pdf")
stitch_pdf(downloaded_paths, pdf_path)
result["files"].append("carousel.pdf")
post_type = "carousel" if len(cdn_urls) > 1 else "single_image"
write_metadata(post_dir, post_url, shortcode, post_type, caption, username, cdn_map)
result["files"].append("metadata.txt")
except Exception as e:
result["status"] = "error"
result["error"] = str(e)
return result
def write_metadata(post_dir, post_url, shortcode, post_type, caption, username, cdn_map):
lines = [
f"Post URL: {post_url}",
f"Shortcode: {shortcode}",
f"Type: {post_type}",
]
if post_type == "carousel":
lines.append(f"Slide count: {len([k for k in cdn_map if 'slide' in k])}")
lines += [
f"Caption: {caption}",
f"Username: @{username}",
f"Fetched at: {datetime.now(timezone.utc).isoformat()}",
"CDN URLs:",
]
for fn, url in cdn_map.items():
lines.append(f" {fn:<18} {url}")
with open(os.path.join(post_dir, "metadata.txt"), "w", encoding="utf-8") as f:
f.write("\n".join(lines) + "\n")
def main(urls: list[str], base_dir: str = "./instagram-downloads", stitch: bool = True):
os.makedirs(base_dir, exist_ok=True)
results = []
for url in urls:
url = url.strip()
if not url:
continue
print(f"Processing: {url}")
r = process_url(url, base_dir, stitch)
results.append(r)
time.sleep(1) # polite delay between requests
# Summary
ok = [r for r in results if r["status"] == "ok"]
err = [r for r in results if r["status"] == "error"]
total_files = sum(len(r["files"]) for r in ok)
print("\nInstagram Post Downloader — Batch Complete")
print("==========================================")
print(f"URLs processed: {len(results)}")
print(f"Posts saved: {len(ok)}")
print(f"Total files: {total_files}")
print(f"Errors: {len(err)}")
print(f"Output dir: {os.path.abspath(base_dir)}\n")
for r in results:
if r["status"] == "ok":
print(f" OK {r['url']}")
else:
print(f" ERR {r['url']} — {r['error']}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python instagram_downloader.py <url1> [url2] ...")
sys.exit(1)
main(sys.argv[1:])
质量检查
标记任务完成前,请逐项确认:
- 已确认域名允许列表 — 在任何抓取尝试之前添加
*.cdninstagram.com - 开始处理前,已验证所有提供的 URL 均为 Instagram URL
- CDN URL 均为可用的最高分辨率版本(选择最大的
config_width) - 文件夹名称已清理 — 不含特殊字符和空格,最多从说明文字中截取 40 个字符
- 已处理文件夹重名 — 如果文件夹已存在,则附加 shortcode
- 轮播图幻灯片按顺序编号并使用零填充(
slide_01、slide_02、……) - PDF 按正确顺序包含所有幻灯片(按幻灯片索引排序,而非字母顺序)
- 每个帖子文件夹中都已写入 metadata.txt,其中包含完整 CDN URL
- 启动时检查 Pillow 依赖 — 不可用时优雅回退
- 已打印批次完成摘要,包含文件数量和任何错误
- 已捕获并报告私密帖子的错误 — 不得静默跳过
- 已处理速率限制 — 请求之间至少间隔 1 秒
- 不存储凭据或 Cookie — 此技能仅处理公开帖子
反模式
- 不要尝试下载私密帖子或登录墙后的内容 — 此技能仅适用于公开帖子
- 不要忽略 429 速率限制响应 — 重试前务必等待退避时间
- 处理多个账号时,不要将所有下载内容保存到同一个平面文件夹 — 按来源使用命名子文件夹
- 轮播帖子不得跳过 PDF 拼接 — 仅提供单张幻灯片而不生成合并后的 PDF,输出是不完整的
- 如果 Instagram 返回登录墙,不要继续操作 — 应清楚说明限制,而不是静默返回错误
示例触发语句
- “帮我下载这篇 Instagram 帖子:https://www.instagram.com/p/ABC123/”
- “把那个轮播图保存到我的下载文件夹”
- “你能抓取这篇 Instagram 帖子的所有幻灯片并制作成 PDF 吗?”
- “下载这 5 篇 Instagram 帖子”[后接 URL 列表]
- “趁这篇 IG 帖子还没被删除,把它存档”
- “我需要这个轮播图的全分辨率图片”
- “下载这个 Instagram URL 中的图片并将它们拼接成 PDF”
- “批量下载这些 Instagram 帖子”[后接 URL]
- “将这个 Instagram 轮播图的幻灯片分别保存为 JPEG”
- “帮我获取这张 Instagram 图片的高分辨率版本”
关于 Instagram 反爬措施的说明
Instagram 会主动更改页面结构和 API 端点。如果三次抓取尝试均失败:
- 嵌入页面方法(
/embed/captioned/)历来最稳定 — 从这里开始尝试。 - CDN URL 会过期。抓取后立即下载 — 不要存储 URL 后再稍后下载。
- 即使某些帖子在技术上是公开的,Instagram 也可能为其返回登录墙。如果发生这种情况,此技能无法在没有身份验证的情况下继续(身份验证不在支持范围内)。
- 如果 Instagram 返回 429,请等待 10–30 秒后重试。对于较长的列表,请减少批次大小。
此技能仅针对公开帖子设计,不支持登录、会话或私密内容。
最初受 Frank 和 Diana Dovgopol(Write, Prompt, Scale)的一项技能启发 — 经改编并扩展后纳入此库。