前情提要
基于上一篇的分析,实现一个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直接获取的。敬请期待下一章...
评论记录:
回复评论: