爬取Instagram数据的那些事(已解决)

6,541 阅读6分钟

写于2018-12-27

因为需要做一个Ins相关的app,需要爬取ins的数据,总共试验了一个多礼拜最后宣告失败。。。

2019-1-8已成功

2019-5-20日更新,目前在shareData里已经没有rhx_gis这个字段了,暂时的解决方式是传入一个空字符串,后续待观察,估计接口会大改。

2019-6-20日更新:1、获取ProfilePageContainer.js方法变了,现在它是在一个script标签中2、目前请求头中不需要添加x-instagram-gis的加密字段

2020-06-20日更新:除了query接口,ins相关接口已经需要登录才能访问,暂时只能通过请求时加入cookie(有个字段sessionid),因此只能后端去请求数据了,纯前端无法解决。你可以使用instagram-web-api,instagram-private-api,单这些库都是nodejs环境的,游览器下无法使用。我们是把项目转成混合开发了,可以通过Instagram api 去登录,才可以使用这些接口

特此记录一下

前言

某天领导说,咱们app默认只能获取用户前12张照片,我查了一下,因为默认爬取首页,里面的照片数据确实只有12条。我把原因balalal一说,领导立刻很机智的回答我:小宣,你是不是会node,你来弄一下吧。
好吧,我会node

开始观察

首先打开Instagram的首页,选上我喜欢的霉霉。F12,看network.
提示:需要科学上网 这里出现了2个请求,咱们主要看第二个,点开preview,没问题数据出来了,其中display_url就是咱们需要的啦

获取初始数据

我大概说一下如何获取用户资料以及前12张照片,方法很简单,直接get请求用户的首页,找到其中的shareData即可,这里几乎网上大部分人都能找到,给大家一个示例代码。这里可以直接用前端去get,也可以放在后端。 下述代码是在前端直接获取的

let username='taylorswift'   
let url='https://www.instagram.com/' + username + '/'    
axios.get(url)
.then(res=>{
  let dataStr = JSON.stringify(res)   
  let datarep = dataStr.replace(/(\r\n)|(\n)/g, "")    
  let n = datarep.match(/(window._sharedData\s?)(=\s?)(.*?);<\/script>/)[3]    
  let shareData = JSON.parse(n.replace(/\\/g, ""))    
})    

这里的shareData大概是这样

这里面比较有用的就是rhx_gis大部分用户信息都在entry_data.ProfilePage[0].graphql.user
其中图片等数据在edge_owner_to_timeline_media.edges下,page_info下主要是分页接口需要的数据(重要)

分页接口

当你请求完首页时你会发现,edges下只有12张照片,我之前之所以没发现这个问题,是因为我爬我自己的账号,上面总共还没12张照片 这时候再打开network就会发现,你就会很轻松的发现有一个query的请求,这就是分页接口 首先寻找queryHash,把它复制,然后在所有页面以及js中寻找,最后发现它在ProfilePageContainer.js/68f09467caf1.js文件中, 但是你不能写死地址直接访问,因为:这tm是个不固定的网址,它会根据国家等信息变化。
但是这怎么能逃过我的火眼金睛,在element中找到一个link标签 当然从前端是无法处理的,当你想给请求头上加上cookie这些东西时候,就会被拦截,于是转而用node访问。
那就先写第一步的代码吧。

const cheerio = require("cheerio")   
const rp = require('request-promise')  
const requestOptions = { 
    resolveWithFullResponse: true,   
    proxy: 'http://localhost:1080',   //这里加个代理,科学上网
     headers: {
                "user-agent": userAgent,//这里放上你从游览器复制的userAgent
                "x-requested-with": "XMLHttpRequest"
            }
    }    
async function (username){    
  const res = await rp.get('https://www.instagram.com/' + username, requestOptions)    
  const $ = cheerio.load(res.body)    
  let datas = '', jsFilesUrl= ''
//这里需要先获取shareData中的一些信息备用  
 $('script').each(function(indexInArray, valueOfElement) {
        let htmlStr=$('script').eq(indexInArray).html()
        if (htmlStr.indexOf("window._sharedData") == 0) {
            datas = $('script').eq(indexInArray).html()
            datas = datas.substring(20).replace(/\;/g, "");
        }
        let src=$('script').eq(indexInArray).prop('src')
        if(src&&src.indexOf('ProfilePageContainer')){
            jsFiles = 'https://www.instagram.com' + src    
        }
    })    
const jsContent = await rp.get(jsFiles, requestOptions)   
let queryHashList = jsContent.body.match(/(queryId:\s?)(\S*),/g)    
let result = queryHashList.map(element => {    
    return element = element.substr(9, 32)  
})   
//获取参数    
let queryHash = result[2],    
      PageInfo = datas.entry_data.ProfilePage[0].graphql.user.edge_owner_to_timeline_media.page_info,     
      userId = datas.entry_data.ProfilePage[0].graphql.user.id,     
      gisCode = datas.rhx_gis,     
      csrftoken = datas.config['csrf_token'],
      userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)Chrome/71.0.3578.98 Safari/537.36'    
 //开始请求分页接口    
 let BASE_URL = 'https://www.instagram.com/graphql/query/?'    
 let options = {    
    resolveWithFullResponse: true,    
    proxy: 'http://localhost:1080',    
    headers: {    
          "x-instagram-gis": gisCode,    
          "x-requested-with": "XMLHttpRequest",    
          "user-agent": userAgent,    
       }    
    }   
 let variables = {    
     id: userId,    
     first: 12,    
     after: PageInfo['end_cursor']    
    }    
    let str = escape(JSON.stringify(variables))   
    let url = `${BASE_URL}query_hash=${queryHash}&variables=${str}`
    const nextPage = await rp.get(url, options)  
    console.log(nextPage)
    
    }
    ```

>现实依旧无情的打击了我,返回结果居然是403。我仿佛感觉到了facebook嘲笑的目光 
![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/12/27/167edfca357fec19~tplv-t2oaga2asx-image.image)
## 最新进展
我试遍了几乎github上的所有库,目前找到能用的只有一个[instagram-web-api](https://github.com/jlobos/instagram-web-api)
但是需要登录才能获取分页,结合express,核心代码如下: 
<br>
const Instagram = require('instagram-web-api')    
const accounts = require('./accountList.json')    
app.get('/api/ins/morepic', async(req, res) => {    
let after = req.query.after,     
  name = req.query.username    
  if ((name && name === '') || (after && after === '')) {   
    res.sendStatus(400)   
   }    
   let len = accounts.length    
let randIdx = parseInt(Math.random() * len)    
let account = accounts[randIdx]    
let client = new Instagram({    
    username: account.username,    
  password: account.password    
    }, { proxy: 'http://127.0.0.1:1080' })    
    await client.login()    
let photoData = await client.getPhotosByUsername({ username: name, after: after })   
    res.json(photoData)   
});

我注册了3个账号,然后随机登录,但是这样肯定是不行的,一旦访问量过大,肯定会触发Instagram的安全机制,所以我还在努力

更新2018-12-29

同事提出一个新思路,首先服务启动的时候,就登陆一个账号,取得一个session,然后通过websocket实时接收和发送数据。这样的话就相当于一个账号一直登录去查询数据。测试过确实也可以,但是如果出现错误,这个进程就会down掉(nodejs是单进程),这时候需要一个子进程去维护,记录错误,重启主进程。虽然有pm2这种工具,但是还是不太会用,需要继续研究

更新2019-1-3

目前发现ins的接口是需要MD5加密的,我使用了blueimp-md5这个包,示例代码如下

const md5 = require('blueimp-md5')

 let variables = {
        id: userId,
        first: 12,
        after: PageInfo['end_cursor']
    },
    gisCode=datas.rhx_gis//datas是第一次访问是爬取的shareData
    let needcrpyto = `${gisCode}:${JSON.stringify(variables)}`
    let skey = md5(needcrpyto)
    //然后将这个skey放入header的x-instagram-gis

现在有出现了一个问题,我第一次直接访问获取到的rhx_gis好像是错的,我把游览器中rhx_gis赋值,确实可以访问到,但是用爬取到的rhx_gis就是不行,估计和平台有关。通过游览器直接访问,加密算法是MD5,如果被断定为后台访问,加密算法就会变。

更新2019-1-8

目前发现是因为我第一次请求时请求头写错了,导致rhx_gis出错,所以以上代码基本上可以实现爬取分页数据。上面的代码已经修改正确,在此就不再赘述一遍了。

结语

通过这次爬虫,让我对nodejs的request了解更深了,也对网站前后端访问,以及如何进行加密验证也有了一点了解。大概总结一下爬取ins接口的过程。

  • 第一步,你需要get请求用户首页,获取window._shareData,需要保存的数据主要有rhx_gis,图片列表在edge_owner_to_timeline_media.edges下,page_info下主要是分页接口需要的数据(重要),这一步可以放在前端访问

  • 第二步,获取ProfilePageContainer.js路径下的那个js文件的内容,查找出queryId,这个值就是访问分页接口时的query_hash, 这一步其实也是前后端皆可,我是放到后端了,因为nodejs的cheerio比较好用

  • 第三步,组合数据,加密,你需要按照network里的格式组装 组装完成后,按照如下格式${gisCode}:${JSON.stringify(variables)},使用MD5加密,格式为32位小写。最后放入请求头的x-instagram-gis中。

  • 最后一步,请求分页接口即可,地址为'https://www.instagram.com/graphql/query/?'

其实整个接口最主要的就是user-agent,只要你填写游览器的useragent,一般不会有问题。cookie不是必须的,csrftoken也是一个烟雾弹而已。

谢谢大家。