前情提要
基于上一篇的分析,实现一个Instagram下载网站,只要复制帖子的链接到网站,就可以支持图文的显示和下载。
![]()
TL;DR
项目地址:github.com/newwangzhic… 在线演示:instagram-downloader-ten.vercel.app/
流程
由于需要通过接口请求IG的数据,为了突破跨域的限制,需要服务器帮忙代理请求并将数据解析后返回给前端显示,所以项目采用Nextjs一把梭,以下是用户请求的基本流程:
![]()
1. shortcode解析
IG的分享链接一般是这样的www.instagram.com/p/DBwQQlRt_… ,其中的DBwQQlRt_2q就是shortcode;p/tv/reel/stories等代表帖子的类型。这里先讨论前三种,stories类型需要特殊处理,下章再表。
首先需要对IG的分享链接验证结构是否合法,http(s),www和searchParams部分都不是必须的;通过一条正则可以直接判断/^(https?:\/\/)?(www\.)?instagram\.com\/(p|reel|tv)\/[a-zA-Z0-9_\-]+(\/(\?[^#]*)?)?(#.*)?$/
验证合法后就可以提取出shortcode了,const matcher = /\/(?:p|reel|tv)\/([a-zA-Z0-9_\-]+)/,shortcode就保存在matcher[1]中
2. 服务端请求
提取到shortcode后就可以直接通过/api/graphql这个POST接口请求IG数据了,上一章有介绍;此处不表。这里直接贴出需要的header和body
js 代码解读复制代码// header
const headers = {
  Accept: '*/*',
  'Accept-Language': 'en-US,en;q=0.5',
  'Content-Type': 'application/x-www-form-urlencoded',
  'X-FB-Friendly-Name': 'PolarisPostActionLoadPostQueryQuery',
  'X-CSRFToken': 'RVDUooU5MYsBbS1CNN3CzVAuEP8oHB52',
  'X-IG-App-ID': '1217981644879628',
  'X-FB-LSD': 'AVqbxe3J_YA',
  'X-ASBD-ID': '129477',
  'Sec-Fetch-Dest': 'empty',
  'Sec-Fetch-Mode': 'cors',
  'Sec-Fetch-Site': 'same-origin',
  'User-Agent':
    'Mozilla/5.0 (Linux; Android 11; SAMSUNG SM-G973U) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/14.2 Chrome/87.0.4280.141 Mobile Safari/537.36'
}
// body
const requestData = {
  av: '0',
  __d: 'www',
  __user: '0',
  __a: '1',
  __req: '3',
  __hs: '19624.HYP:instagram_web_pkg.2.1..0.0',
  dpr: '3',
  __ccg: 'UNKNOWN',
  __rev: '1008824440',
  __s: 'xf44ne:zhh75g:xr51e7',
  __hsi: '7282217488877343271',
  __dyn:
    '7xeUmwlEnwn8K2WnFw9-2i5U4e0yoW3q32360CEbo1nEhw2nVE4W0om78b87C0yE5ufz81s8hwGwQwoEcE7O2l0Fwqo31w9a9x-0z8-U2zxe2GewGwso88cobEaU2eUlwhEe87q7-0iK2S3qazo7u1xwIw8O321LwTwKG1pg661pwr86C1mwraCg',
  __csr:
    'gZ3yFmJkillQvV6ybimnG8AmhqujGbLADgjyEOWz49z9XDlAXBJpC7Wy-vQTSvUGWGh5u8KibG44dBiigrgjDxGjU0150Q0848azk48N09C02IR0go4SaR70r8owyg9pU0V23hwiA0LQczA48S0f-x-27o05NG0fkw',
  __comet_req: '7',
  lsd: 'AVqbxe3J_YA',
  jazoest: '2957',
  __spin_r: '1008824440',
  __spin_b: 'trunk',
  __spin_t: '1695523385',
  fb_api_caller_class: 'RelayModern',
  fb_api_req_friendly_name: 'PolarisPostActionLoadPostQueryQuery',
  variables: JSON.stringify({
    shortcode: this.shortcode,
    fetch_comment_count: 'null',
    fetch_related_profile_media_count: 'null',
    parent_comment_count: 'null',
    child_comment_count: 'null',
    fetch_like_count: 'null',
    fetch_tagged_user_count: 'null',
    fetch_preview_comment_count: 'null',
    has_threaded_comments: 'false',
    hoisted_comment_id: 'null',
    hoisted_reply_id: 'null'
  }),
  server_timestamps: 'true',
  doc_id: '10015901848480474'
}
const body = new URLSearchParams(requestData).toString()
3. 解析出图文链接
接口返回的帖子相关的数据保存在data.xdt_shortcode_media中,称之为mediaData,如果shortcode是错误的或者请求出现限制(如私有帖),mediaData就可能是null或带有错误信息的string。
多个图片/视频
一个帖子可能是单图片/视频。也可能是多个。如果是多个图片/视频,那么所有资源保存在edge_sidecar_to_children中。其中mediaData.edge_sidecar_to_children.edges[i].node也是mediaData的结构。所以我们可以通过递归解析出所有的资源:
ts 代码解读复制代码public formatToResourceInfo(mediaData?: MediaData): ResourceInfo[] {
  if (!mediaData) {
    return []
  }
  const nodes = mediaData.edge_sidecar_to_children
  if (nodes) {
    return nodes.edges.map((node) => ({
      ...this.formatToResourceInfo(node.node)[0]
    }))
  }
  if (mediaData.is_video) {
    return [
      {
        filename: `ig-video-${dayjs().format('YYYY-MM-DDTHH:mm:ss')}.mp4`,
        width: mediaData.dimensions.width,
        height: mediaData.dimensions.height,
        url: mediaData.video_url,
        type: 'Video'
      }
    ]
  } else {
    const imageData = mediaData.display_resources.at(-1)
    return [
      {
        filename: `ig-img-${dayjs().format('YYYY-MM-DDTHH:mm:ss')}.jpeg`,
        width: imageData?.config_width ?? 0,
        height: imageData?.config_height ?? 0,
        url: imageData?.src ?? '',
        type: 'Image'
      }
    ]
  }
}
视频
mediaData.is_video直接告诉了你当前资源是否是视频,视频链接可以直接通过mediaData.video_url获取
图片
mediaData.is_video为false既表示当前资源是图片,其中display_resources数组保存了不同分辨率的图片链接。我们可以直接通过mediaData.display_resources.at(-1).src拿到最高清的一张
4. 显示预览
如果顺利,我们把解析好的数据返回给前端就可以顺利显示IG贴的图片和视频了
视频
视频是mp4格式。放到video中可以直接。
为了让设备可以有“下载”的按钮,可以将链接放在source中
tsx代码解读复制代码
IOS设备由于系统的限制,是不会显示下载,且无法直接保存到相册中的。只能退而求其次下载链接资源中,使用a标签创建一个下载链接
tsx 代码解读复制代码href} download>
  Long Press Save
图片
很不幸,图片有跨域限制,这代表如果直接把链接直接放在img标签中将无法显示。
同样,我们可以使用服务器帮助我们请求到图片,再通过base64字符串或流传输到前端。
那么有什么方法可以直接前端显示跨域图片吗?还真有!
首先,前端是无法直接显示有跨域限制的图片的。但是我们可以白嫖谷歌Translate的代理服务,它会帮忙代理图片,将header设置成非跨域的。只需要将图片链接转换一下格式就行了。
比如这个图片https://scontent-iad3-2.cdninstagram.com/v/t51.29350-15/465229499_861965882787711_606795556211070811_n.heic?stp=dst-jpg_e35&efg=eyJ2ZW5jb2RlX3RhZyI6ImltYWdlX3VybGdlbi4xMDM3eDEwMzcuc2RyLmYyOTM1MC5kZWZhdWx0X2ltYWdlIn0&_nc_ht=scontent-iad3-2.cdninstagram.com&_nc_cat=103&_nc_ohc=J7-gdDYU9-cQ7kNvgE-u_hO&_nc_gid=78f37573b1be4eaeb6b9c1a323aea5a3&edm=APs17CUBAAAA&ccb=7-5&oh=00_AYD4OLzRnF9umh6cplyabc_zxwQhsPrzSc2QuU-8tiAxaA&oe=6747FF23&_nc_sid=10d13b,是无法直接在我们的网站下使用img显示的。,但是我们将前半部分改造成https://scontent--iad3--2-cdninstagram-com.translate.goog/,经过代理的这张图片是没有跨域限制的,这样我们就可以直接预览和保存啦!改造后完整的链接:https://scontent--iad3--2-cdninstagram-com.translate.goog/v/t51.29350-15/465229499_861965882787711_606795556211070811_n.heic?stp=dst-jpg_e35&efg=eyJ2ZW5jb2RlX3RhZyI6ImltYWdlX3VybGdlbi4xMDM3eDEwMzcuc2RyLmYyOTM1MC5kZWZhdWx0X2ltYWdlIn0&_nc_ht=scontent-iad3-2.cdninstagram.com&_nc_cat=103&_nc_ohc=J7-gdDYU9-cQ7kNvgE-u_hO&_nc_gid=78f37573b1be4eaeb6b9c1a323aea5a3&edm=APs17CUBAAAA&ccb=7-5&oh=00_AYD4OLzRnF9umh6cplyabc_zxwQhsPrzSc2QuU-8tiAxaA&oe=6747FF23&_nc_sid=10d13b,可以试试。
转换的代码如下,感谢开源项目corsDown:
ts 代码解读复制代码export function toCorsUrl(url: string): string {
  const p = url.split('/')
  let t = ''
  for (let i = 0; i < p.length; i++) {
    if (i == 2) {
      t +=
        p[i].replaceAll('-', '--').replaceAll('.', '-') +
        atob('LnRyYW5zbGF0ZS5nb29n') +
        '/'
    } else {
      if (i != p.length - 1) {
        t += p[i] + '/'
      } else {
        t += p[i]
      }
    }
  }
  return encodeURI(t)
}
接下来
stories的解析和p/reel还略有不同,是通过用户id直接获取的。敬请期待下一章...
                                    
评论记录:
回复评论: