如何响应最近的私有 API 更改执行未经身份验证的 Instagram 网络抓取?

Posted

技术标签:

【中文标题】如何响应最近的私有 API 更改执行未经身份验证的 Instagram 网络抓取?【英文标题】:How to perform unauthenticated Instagram web scraping in response to recent private API changes? 【发布时间】:2018-09-22 01:00:06 【问题描述】:

几个月前,Instagram 开始通过删除大多数功能并拒绝接受针对大多数权限范围的新应用程序,使其公共 API 无法运行。 Further changes were made this week 进一步限制了开发者选项。

我们中的许多人已经转向 Instagram 的私有网络 API 来实现我们以前拥有的功能。一个杰出的ping/instagram_private_api 设法重建了大部分以前的功能,然而,随着本周公开宣布的更改,Instagram 还对其私有 API 进行了基本更改,需要使用魔术变量、用户代理和 MD5 哈希来进行网络抓取请求可能。这可以通过following the recent releases on the previously linked git repository 看到,继续获取数据所需的确切更改可以be seen here。

这些变化包括:

在请求之间保持用户代理和 CSRF 令牌。 向https://instagram.com/ 发出初始请求以从响应正文中获取rhx_gis 魔术键。 设置X-Instagram-GIS 标头,该标头是通过在将rhx_gis 键和查询变量通过MD5 哈希传递之前神奇地连接起来形成的。

小于此值将导致 403 错误。这些更改已成功实施in the above repository,但是,我在 JS 中的尝试仍然失败。在下面的代码中,我试图从用户时间轴中获取前 9 个帖子。确定这一点的查询参数是:

query_hash of 42323d64886122307be10013ad2dcc44(从用户的时间线获取媒体)。 variables.id 的任何用户 ID 作为字符串(从中获取媒体的用户)。 variables.first,要获取的帖子数,为整数。

以前,可以通过简单地从https://www.instagram.com/graphql/query/?query_hash=42323d64886122307be10013ad2dcc44&variables=%7B%22id%22%3A%225380311726%22%2C%22first%22%3A1%7D 获取此请求而无需进行上述任何更改,因为 URL 不受保护。

但是,我试图实现在上述存储库中成功编写的功能的尝试不起作用,我只收到来自 Instagram 的 403 响应。我在节点环境中使用 superagent 作为我的请求库。

/*
** Retrieve an arbitrary cookie value by a given key.
*/
const getCookieValueFromKey = function(key, cookies) 
        const cookie = cookies.find(c => c.indexOf(key) !== -1);
        if (!cookie) 
            throw new Error('No key found.');
        
        return (RegExp(key + '=(.*?);', 'g').exec(cookie))[1];
    ;

/*
** Calculate the value of the X-Instagram-GIS header by md5 hashing together the rhx_gis variable and the query variables for the request.
*/
const generateRequestSignature = function(rhxGis, queryVariables) 
    return crypto.createHash('md5').update(`$rhxGis:$queryVariables`, 'utf8').digest("hex");
;

/*
** Begin
*/
const userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/604.3.5 (Khtml, like Gecko) Version/11.0.1 Safari/604.3.5';

// Make an initial request to get the rhx_gis string
const initResponse = await superagent.get('https://www.instagram.com/');
const rhxGis = (RegExp('"rhx_gis":"([a-f0-9]32)"', 'g')).exec(initResponse.text)[1];

const csrfTokenCookie = getCookieValueFromKey('csrftoken', initResponse.header['set-cookie']);

const queryVariables = JSON.stringify(
    id: "123456789",
    first: 9
);

const signature = generateRequestSignature(rhxGis, queryVariables);

const res = await superagent.get('https://www.instagram.com/graphql/query/')
    .query(
        query_hash: '42323d64886122307be10013ad2dcc44',
        variables: queryVariables
    )
    .set(
        'User-Agent': userAgent,
        'X-Instagram-GIS': signature,
        'Cookie': `rur=FRC;csrftoken=$csrfTokenCookie;ig_pr=1`
    ));

我还应该尝试什么?是什么让我的代码失败了,而上面存储库中提供的代码工作得很好?

更新 (2018-04-17)

Instagram 在一周内至少第三次更新了他们的 API。此更改不再需要 CSRF 令牌构成散列签名的一部分。

上述问题已更新以反映这一点。

更新 (2018-04-14)

Instagram 再次更新了他们的私有 graphql API。据任何人都可以弄清楚:

用户代理不再需要包含在X-Instagram-Gis md5 计算中。

上述问题已更新以反映这一点。

【问题讨论】:

您是否尝试过添加x-requested-with 标头github.com/ping/instagram_private_api/blob/… 并将用户代理更改为普通浏览器? @inDream,是的,但它无关紧要,因为这些标头实际上从未出于此问题的目的添加(paramsNone)。此外,为了匹配 Python lib 的问题,UA 进行了更新,但只要它在请求之间保持一致,它也是无关紧要的。 @ReactingToAngularVues 我现在也在与这些变化作斗争。我有一个用于从 Instagram 保存媒体的 Chrome 扩展程序,因此我使用纯 javascript。不过我想我还是被卡住了,因为似乎无法访问“set-cookie”值。 有没有人知道他们从什么时候开始限制和抛出 429 响应? 大家好,我也在为 Instagram 更新而苦苦挣扎,我从这个链接 instagram.com/username/?__a=1 获得了个人资料详细信息和前 12 个媒体。但由于 instagram 新标头更改,它给出了 403 Forbidden 响应。我看到他们已经添加了如上所述的 X-instagram-GIS,但无法获得此处用于创建魔术字符串的变量,因为此链接没有变量。我们应该将用户名或 id 作为变量。我有 rhx_gis 和 csrf_token。 【参考方案1】:

要坚持的价值观

您没有在对 Instagram 的第一个查询中保留用户代理(要求):

const initResponse = await superagent.get('https://www.instagram.com/');

应该是:

const initResponse = await superagent.get('https://www.instagram.com/')
                     .set('User-Agent', userAgent);

这必须与csrftoken cookie 一起保存在每个请求中。

X-Instagram-GIS 标头生成

正如您的回答所示,您必须从两个属性生成 X-Instagram-GIS 标头,即在您的初始请求中找到的 rhx_gis 值,以及在您的下一个请求中的查询变量。这些必须是 md5 散列,如上面的函数所示:

const generateRequestSignature = function(rhxGis, queryVariables) 
    return crypto.createHash('md5').update(`$rhxGis:$queryVariables`, 'utf8').digest("hex");
;

【讨论】:

自从我发布这个问题以来,Instagram (再次)更改了他们的私有 API,因此我更新了我的帖子。但是,我仍然无法让这个工作正常进行。 您在向https://www.instagram.com/ 发出初始请求时是否使用相同的用户代理?因为我在你的例子中看不到这一点 @ReactingToAngularVues 请这样做:) 实际上,他们再次更新了 api,现在您甚至不需要 csrftoken 签名,只需要 rhx_gis 和变量...@PirateNinja 使用相对路径而不是变量,例如 https://www.instagram.com/durov/ 它会是/durov/(不要忘记斜线) @PirateNinja 是的!【参考方案2】:

因此,为了调用 instagram 查询,您需要生成 x-instagram-gis 标头。

要生成此标头,您需要计算下一个字符串“rhx_gis:path”的 md5 哈希。 rhx_gis 值存储在 instagram 页面源代码中的 window._sharedData 全局 js 变量中。

示例: 如果您尝试像这样获取用户信息请求 https://www.instagram.com/username/?__a=1 您需要添加http头x-instagram-gis来请求哪个值是MD5("rhx_gis:/username/")

这是经过测试并且可以 100% 工作的,所以请随时询问是否有问题。

【讨论】:

嘿,它不起作用,我尝试使用 md5("fc2e73d4fd7dddcd31d28bea5cb2df59:/username/?__a=1") @Stack 代替 /username/?__a=1 您需要使用 /username/ 以便 md5("fc2e73d4fd7dddcd31d28bea5cb2df59:/username/") 将给出值 00a89418c3a4f92d5407e36116117cd9 。您需要将此值放入 GET 请求“instagram.com/username/?__a=1”的 x-instagram-gis 标头中(不用说,您需要输入您的 instagram 用户名来代替用户名)。告诉我它是否适合你,我刚刚为我的帐户测试了同样的效果 这看起来真的很像黑客。虽然这可能现在可行,但可能不是一个长期的解决方案。 我现在可以抓取前 12 个图像,然后抓取下一个 12 个(或 50 个,这是我设置的),但是当我尝试访问下一个 12 个时,我得到一个 403。任何为什么会这样?请求应该不同吗?他们是否阻止了第三个请求? @堆栈 他们有每秒请求限制,因此您需要在代码中添加请求限制,否则您可能会被 instagram 临时阻止。尝试尝试请求率,以免出现 403 错误【参考方案3】:

嗯...我的机器上没有安装 Node,所以我无法确定,但在我看来,您缺少查询字符串中参数的关键部分,即 after 字段:

const queryVariables = JSON.stringify(
    id: "123456789",
    first: 4,
    after: "YOUR_END_CURSOR"
);

从那些queryVariables 取决于您的 MD5 哈希,然后,与预期的不匹配。试试看:我希望它会起作用。

编辑:

仔细阅读您的代码,不幸的是它没有多大意义。我推断您正在尝试从用户的提要中获取完整的图片流。

那么,您需要做的是不是像现在这样调用 Instagram 主页 (superagent.get('https://www.instagram.com/')),而是调用用户的信息流 (superagent.get('https://www.instagram.com/your_user'))。

注意:您需要硬编码您将在下面使用的相同用户代理(而且看起来不像...)。

然后,您需要提取查询 ID(它不是硬编码的,它每隔几个小时,有时几分钟就会更改一次;硬编码它是愚蠢的——但是,对于这个 POC,您可以保持硬编码) , 和 end_cursor。对于结束光标,我会选择这样的东西:

const endCursor = (RegExp('end_cursor":"([^"]*)"', 'g')).exec(initResponse.text)[1];

现在您拥有发出第二个请求所需的一切:

const queryVariables = JSON.stringify(
    id: "123456789",
    first: 9,
    after: endCursor
);

const signature = generateRequestSignature(rhxGis, csrfTokenCookie, queryVariables);

const res = await superagent.get('https://www.instagram.com/graphql/query/')
    .query(
        query_hash: '42323d64886122307be10013ad2dcc44',
        variables: queryVariables
    )
    .set(
        'User-Agent': userAgent,
        'Accept': '*/*',
        'Accept-Language': 'en-US',
        'Accept-Encoding': 'gzip, deflate',
        'Connection': 'close',
        'X-Instagram-GIS': signature,
        'Cookie': `rur=$rurCookie;csrftoken=$csrfTokenCookie;mid=$midCookie;ig_pr=1`
    ).send();

【讨论】:

after 字段不是必需的,它仅表示您可以从中获取图像的光标。 鉴于没有文档,您的文档不起作用,而这个文档起作用,但在我看来,这是非常需要的……重点是:两者之间没有其他重大变化除了 1. 缺少 after 字段之外的两种解决方案,2. 两个呼叫都没有使用相同的 UA。更改编号2,如果这不能解决,没有。 1 是您唯一的答案恕我直言。 根据上面 Alex 的回答,缺少的元素是初始调用中的相同 UA。 after 是一个完全可选的游标属性。 @Gianluca 我现在可以抓取前 12 张图像,然后抓取接下来的 12 张(或我设置的 50 张),但是当我尝试访问接下来的 12 张时,我得到了 403有什么理由会这样吗?请求应该不同吗?注意,这是我必须给 rhx_gis 的第二个请求。有变化吗? @WilliamHampshire 从第二个开始的每个调用都会输出一个新的ig_gis,基于rhx_gisnew_params,以及new_params 本身。所以,ig_gis 确实会随着每次调用而改变,rhx_gis 不会改变,而新的参数会改变【参考方案4】:

query_hash 不是一成不变的,会随着时间不断变化。

例如 ProfilePage 脚本包括以下脚本:

https://www.instagram.com/static/bundles/base/ConsumerCommons.js/9e645e0f38c3.js https://www.instagram.com/static/bundles/base/Consumer.js/1c9217689868.js

哈希位于上述脚本之一中,例如对于edge_followed_by

const res = await fetch(scriptUrl,  credentials: 'include' );
const rawBody = await res.text();
const body = rawBody.slice(0, rawBody.lastIndexOf('edge_followed_by'));
const hashes = body.match(/"\w32"/g);
// hashes[hashes.length - 2]; = edge_followed_by
// hashes[hashes.length - 1]; = edge_follow

【讨论】:

不。 query_hash 在每个方法的基础上都是常数。请注意,它在我上面链接的工作存储库中是硬编码的。 github.com/ping/instagram_private_api/blob/…

以上是关于如何响应最近的私有 API 更改执行未经身份验证的 Instagram 网络抓取?的主要内容,如果未能解决你的问题,请参考以下文章

在 Apollo 中处理未经身份验证的响应

保护未经身份验证的 REST API

Azure 授权 - 让未经身份验证的用户调用 api

从Salesforce查看文件时,Google Drive API消息“超出未经身份验证的使用限制。”

使用 JWT 进行护照身份验证:如何将护照的默认未经授权响应更改为我的自定义响应?

摘要身份验证返回未经授权的