title: golang爬取Instagram内容下载地址
tags: go
author: Clown95
前言
应该是全网首发吧???如果转载请添加原作者信息。
因为工作需要,需要获取一些小姐姐的图片和视频,然后就瞄上了ins。虽然可以利用一些插件保存图片和视频,但是这种方法不仅效率低,而且繁琐。因此就想通过程序来实现,搜索了一圈也没找到什么好的ins下载器,果断自己撸一个。
其实ins获取到它的内容并不难,它就是使用json传递了信息。但是如果想要获得全部内容就比较麻烦,它的内容是通过动态加载的,没有什么翻页的参数,所以想要获得到全部内容需要动一点脑筋。
分析
首先我们来分析一下如何获得ins的图片和视频地址,前提是你得有一个科学上网的梯子。
json地址参数分析
我们随机选择找一个小姐姐的主页
https://www.instagram.com/angelaqiqi_99/
通过浏览器的开发者工具,我们可以看到ins传递了一些json信息
我们选择最后一个json地址,并把这个它解码一下看看它有什么信息
https://www.instagram.com/graphql/query/?query_hash=44efc15d3c13342d02df0b5a9fa3d33f&variables={“id”:“436245394”,“first”:12,“after”:“QVFENGZuREJpVFhrakVkdEc0cGpUTmI2XzNzWi1fQ1NJYWM4MTh4M0MzMDNNc3VWM2hzVkRmUEZXMW54REV5ZjZfWl8tMlh4UmkySVpTTEtJcUs0OEVWWg==”}
我们通过这个地址可以看到,它与两个参数query_hash
和variables
,variables
里面又包含了三个参数分别是id
、first
和after
,并且通过这个名字我们可以知道,网站数据的变化跟它有关系。。
然后我们滚动网页,查看下一个json的地址有什么不一样的地方
我们还是来看一下解码后的地址:
https://www.instagram.com/graphql/query/?query_hash=44efc15d3c13342d02df0b5a9fa3d33f&variables={“id”:“436245394”,“first”:12,“after”:“QVFEX3VVWjZEcExrbmxWdUZfQUMzeUEzdnd1UTZPSU1FQllMRVNDSmdBeVJCRVlVVE80eS1rcmdKcmlUT0lwRENsZ0xac2k4cmxEVlR5R0FwZTNiX3ZKYQ==”}
通过对比我们发现,after
这个产生不一样,其他几个都一样。 接下来我们猜测一下这些参数的用途。
query_hash
是查询数据的hash值(这个值是固定的,ins会隔一段时间更换一次,可能是一个月,也可能是两个月)id
是博主的账号idfirst
是每次显示内容的数量after
根据名字猜测,在…之后,说明它是类似于索引的东西,主要用来控制数据加载的
图片和视频地址分析
现在我们已经知道了所有参数的用途,现在我们来具体的看下json的内容,我们以第二个地址为例
通过图片我们可以看到, display_url
它存放了一个图片的地址。is_video
通过意思我们可以知道它用来判断内容是否为视频。
我们在找一段is_video
为true
验证一下我们的想法。
果不其然,is_video
为true
的情况下,多了一个键video_url
,不用看都知道这是存放视频地址的, display_url
这时候是这个视频的封面图片。
知道了这三个键,那么我们在进行数据获取的就可以通过is_video
来屏蔽掉视频封面的图片。
内容加载分析
现在我们已经知道了图片和视频的存放地址,接下来才是重点,ins是如何加载更多内容的。前面我们猜测过after
这个参数是控制内容加载的。
现在我们已经知道了after
参数的用途,现在我们就需要找到它是在什么地方传递的。已知第一个after
的值为QVFENGZuREJpVFhrakVkdEc0cGpUTmI2XzNzWi1fQ1NJYWM4MTh4M0MzMDNNc3VWM2hzVkRmUEZXMW54REV5ZjZfWl8tMlh4UmkySVpTTEtJcUs0OEVWWg=="
我们在源文件里面搜索它,果然我们在script
里面搜索到了它,只不过它的键名是end_cursor
,看到这么名字你是不是已经猜到了什么,结束游标
,它就是我们要找的那个。
如果你仔细的看了json的内容,那么你应该对它有印象。
在json中page_info
里面包含了has_next_page
和end_cursor
。
has_next_page
判断是否有下一页end_cursor
当前页的结束游标
当然一个json里面包含了好几个page_info
,最主要的就是第一个page_info
,这个我已经验证过。当主要数据到底的时候,只有这个page_info
的end_cursor
内容为空。
现在我们已经知道了第一页的end_cursor
在script
里面,第二页的end_cursor
在第一条josn里面,以此类推,第三页的end_cursor
,在第二条josn里面。
实现
package main
import (
"bufio"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"regexp"
"strconv"
"time"
)
type Instargam struct {
Data struct {
User struct {
EdgeOwnerToTimelineMedia struct {
//Count int `json:"count"`
Edges []struct {
Node struct {
/*
DashInfo struct {
IsDashEligible bool `json:"is_dash_eligible"`
NumberOfQualities int `json:"number_of_qualities"`
VideoDashManifest string `json:"video_dash_manifest"`
} `json:"dash_info"`
EdgeMediaToComment struct {
PageInfo struct {
EndCursor string `json:"end_cursor"`
} `json:"page_info"`
} `json:"edge_media_to_comment"`
*/
DisplayURL string `json:"display_url"`
EdgeSidecarToChildren struct {
Edges []struct {
Node struct {
/*
DashInfo struct {
VideoDashManifest string `json:"video_dash_manifest"`
} `json:"dash_info"`
*/
DisplayURL string `json:"display_url"`
IsVideo bool `json:"is_video"`
VideoURL string `json:"video_url"`
} `json:"node"`
} `json:"edges"`
} `json:"edge_sidecar_to_children"`
IsVideo bool `json:"is_video"`
VideoURL string `json:"video_url"`
} `json:"node"`
} `json:"edges"`
PageInfo struct {
EndCursor string `json:"end_cursor"`
HasNextPage bool `json:"has_next_page"`
} `json:"page_info"`
} `json:"edge_owner_to_timeline_media"`
} `json:"user"`
} `json:"data"`
}
// 获取网页源代码
func GetHtml(Insurl string) (html string) {
// 解析代理地址
proxy, err := url.Parse("http://127.0.0.1:1087") //加载本地代理
//设置网络传输
netTransport := &http.Transport{
Proxy: http.ProxyURL(proxy),
MaxIdleConnsPerHost: 10,
ResponseHeaderTimeout: time.Second * time.Duration(5),
}
httpClient := &http.Client{
Timeout: time.Second * 10,
Transport: netTransport,
}
request, err := http.NewRequest("GET", Insurl, nil)
if err != nil {
log.Println(err)
}
request.Header.Add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36") //模拟浏览器User-Agent
request.Header.Set("Cookie", `ig_did=5448DAB0-E424-48F9-97DF-3A386834D9BE; mid=XsH7PQAEAAG55HTxRZVxBxvZICeY; ds_user_id=25850748707; shbid=2673; shbts=1590030929.7257333; csrftoken=OvLzaajy6C9peOrWQeTfo61GBRY0xLGz; sessionid=25850748707%3ATgB2CXKqw4BFUu%3A23; rur=FTW; urlgen="{\"35.221.222.156\": 15169\054 \"34.92.32.194\": 15169\054 \"34.92.235.238\": 15169}:1jcJdR:nlKH_w6YMze8RyJkjEbQ5Sc9UxA"`)
res, err := httpClient.Do(request)
if err != nil {
log.Println(err)
return
}
defer res.Body.Close()
//判断是否成功访问,如果成功访问StatusCode应该为200
if res.StatusCode != http.StatusOK {
log.Println(err)
return
}
content, _ := ioutil.ReadAll(res.Body)
return string(content)
}
// 通过主页获取第一个after的值
func GetAfterByHtml(homepage string) string {
regex := `end_cursor":"(.*?)"},"edges`
rp := regexp.MustCompile(regex)
after := rp.FindStringSubmatch(homepage)
return after[1]
}
// 通过主页获得博主的账号ID
func GetIdByHtml(homepage string) string {
regex := `{"id":"(.*?)","username`
rp := regexp.MustCompile(regex)
id := rp.FindStringSubmatch(homepage)
return id[1]
}
//获取博主的账号名
func GetUserName(homepage string) string {
l1 := len("https://www.instagram.com/")
l2 := len(homepage)
return homepage[l1:l2-1] + ".txt"
}
// 拼接查询地址
func SetQueryUrl(query_hash, id, first, after string) string {
url := fmt.Sprintf("https://www.instagram.com/graphql/query/?query_hash=%s&variables={\"id\":\"%s\",\"first\":%s,\"after\":\"%s\"}", query_hash, id, first, after)
return url
}
//json转Struct
func Json2Struct(strjson string) Instargam {
var ins Instargam
json.Unmarshal([]byte(strjson), &ins)
return ins
}
//通过json得到After
func GetAfter(ins Instargam) string {
return ins.Data.User.EdgeOwnerToTimelineMedia.PageInfo.EndCursor
}
// 判断网站是否加载到底
func IsEnd(ins Instargam) bool {
return ins.Data.User.EdgeOwnerToTimelineMedia.PageInfo.HasNextPage
}
// 通过json得到图片和视频下载地址
func GetDownloadUrl(savefile string, ins Instargam) {
for _, v := range ins.Data.User.EdgeOwnerToTimelineMedia.Edges {
var content string
if v.Node.IsVideo != true {
fmt.Println(v.Node.DisplayURL)
content = v.Node.DisplayURL + "\n"
} else {
fmt.Println(v.Node.VideoURL)
content += v.Node.VideoURL + "\n"
}
for _, v1 := range v.Node.EdgeSidecarToChildren.Edges {
if v1.Node.IsVideo != true {
fmt.Println(v1.Node.DisplayURL)
content = v1.Node.DisplayURL + "\n"
} else {
fmt.Println(v1.Node.VideoURL)
content += v1.Node.VideoURL + "\n"
}
}
WirteText(savefile, content)
}
}
// 写入txt文件
func WirteText(savefile string, txt string) {
f, err := os.OpenFile(savefile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0777)
if err != nil {
fmt.Println("os Create error: ", err)
return
}
defer f.Close()
bw := bufio.NewWriter(f)
bw.WriteString(txt)
bw.Flush()
}
func main() {
var homepage string
fmt.Scanln(&homepage)
query_hash := "44efc15d3c13342d02df0b5a9fa3d33f"
first := 12
//通过html获取第一条json地址
html := GetHtml(homepage)
after := GetAfterByHtml(html)
id := GetIdByHtml(html)
first_query_url := SetQueryUrl(query_hash, id, strconv.Itoa(first), after) // 第一条json地址
//通过json获取内容
jsonconent := GetHtml(first_query_url)
for {
ins := Json2Struct(jsonconent)
next_after := GetAfter(ins) //通过json获得after的值
next_query_url := SetQueryUrl(query_hash, id, strconv.Itoa(first), next_after)
GetDownloadUrl(GetUserName(homepage), ins)
jsonconent = GetHtml(next_query_url)
if !IsEnd(ins) { //如果页面加载到底,结束循环
break
}
}
}