无需滚动页面即可收集完整的按钮列表(DevTools Google Chrome)

Collect the full list of buttons to follow without having to scroll the page (DevTools Google Chrome)

DevTools Google Chrome:

在此站点 (https://booyah.live/users/41874362/followers) 上,要加载完整的关注者列表,需要不断向下滚动页面以重新加载更多配置文件,但有时页面太重以至于浏览器崩溃,最终需要关闭。

有什么方法可以在不发生这种情况的情况下收集关注按钮吗?

我目前使用的脚本是:

setInterval(function(){
document.getElementById("layout-content").scrollTo(0, 50000000000000000000000000000000000000);

document.querySelectorAll('.components-button.components-button-size-mini.components-button-type-orange.desktop.components-button-inline').forEach(btn => btn.click());
}, 10)

我使用 setInterval 创建一个循环:

1 - Scrolling the page
2 - Loading more profiles
3 - Clicking the follow buttons

我的需求:

对于我正在进行的学习研究,我的想法是我的个人资料遵循一个最著名个人资料的所有个人资料关注者,以便分析有多少人在这个社交媒体上关注。

附加:

在 Leftium 提供的这个答案中,可以只遵循一个配置文件:

在 KCGD 给出的这个答案中,可以收集整个关注者列表,但在此收集期间没有关注配置文件,可以创建列表并保存数据,但不关注配置文件:

我试着联系他们,但他们还没有回来。这是一个好方法,但我无法将这两个答案结合起来,因此我可以关注所有配置文件,我考虑过收集 KCGD 响应的配置文件的可能性,我也会关注这些配置文件,但不仅是第一个,还有 Leftium.

的答案

是否可以利用 KCGD 的响应和每个响应创建的循环,已经关注所有配置文件,而不是像 Leftium 的响应中那样只关注第一个配置文件?

我尝试创建但没有成功。

崩溃是因为间隔太快

setInterval(function(){}, 10)

您正在尝试每隔 10 milliseconds 调用一次滚动和单击函数(即每隔 1 second 调用一次 100 函数)。这也会干扰服务器,因为他们在滚动时获取新用户。

如果您将间隔调整到至少 1000 milliseconds (1 second),您的脚本就可以运行。当然,这可能需要一段时间,但它会奏效。您还应该预料到页面可能会变得迟缓,特别是当页面已经加载了大量用户时,因为 Virtual Scrolling 未在此页面中实现。

即使放慢滚动速度,它仍然会使浏览器陷入困境,解决方案可能在 API 页面联系人中。为了获得用户的关注者,它会联系网站的 V3 API

https://booyah.live/api/v3/users/41874362/followers?cursor=[LAST 用户在 API RETURN]&count=100

获取将显示在页面中的所有用户。我写了一个脚本,可以一遍又一遍地联系 api 以获取所有关注者数据,只需 运行 它在页面的控制台中并在您想要导出数据时使用 print() 并且 copy/paste 将其放入 .json 文件

//WARNING: THIS SCRIPT USES RECURSION, i have no clue how long the followers list goes so use at your own risk

var followers = []; //data collected from api

function getFollowers(cursor){

    httpGet(`https://booyah.live/api/v3/users/41874362/followers?cursor=${cursor}&count=100`, function (data) { //returns data from API for given cursor (user at the end of last follower chunk)

        console.log("got cursor: "+cursor);

        var _followChunk = JSON.parse(String(data));

        console.log(_followChunk)

        followers.push(_followChunk.follower_list); //saves followers from chunk

        var last_user = _followChunk.follower_list[_followChunk.follower_list.length - 1]; //gets last user of chunk (cursor for the next chunk)


        setTimeout(function(){ //1 second timeout so that the API doesnt return "too many requests", not nessicary but you should probably leave this on

            getFollowers(last_user.uid); //get next chunk

        },1000)

    })

}

var print = function(){console.log(JSON.stringify(followers))};

getFollowers(0); //get initial set of followers (cursor 0)

function httpGet(theUrl, callback) {
    var xmlHttp = new XMLHttpRequest();
    xmlHttp.open("GET", theUrl, false); // false for synchronous request

    xmlHttp.setRequestHeader("Cache-Control", "no-store");
    xmlHttp.send(null);

    callback(xmlHttp.responseText);
};

如果您真的只需要按钮元素,那么唯一的方法是每次加载新关注者时一直向下滚动,因为页面会在您向下滚动时创建元素

浏览器崩溃,因为使用了太多的内存。当您向下滚动页面时,HTML DOM 树会扩展并下载更多头像图像。这些 HTML 和图像资源对于您的目标来说不是必需的。

可以通过直接调用(内部)Booyah API 来避免崩溃。这将更快并且消耗更少的资源,因为只传输文本。有两个 API 感兴趣的端点:

GET /api/v3/users/[USERID]/followers?cursor=0&count=100

获取关注某个用户的关注者列表:

  • [USERID]是被研究用户的ID(WEEDZAO的id)
  • cursor 是关注者列表中开始列出的位置。当页面首次加载时,这是 0。当您向下滚动时,以下 API 调用会增加此值 (101, 201, 301...)
  • count是多少个结果到return.
  • 由于这是一个 GET 调用,您可以在浏览器中打开此 URL。

POST /api/v3/users/[USERID]/followings

关注用户(与单击他们的 'Follow' 按钮相同)。

  • 这里[USERID]是要更新关注者列表的用户ID(你自己的ID)
  • 必须发送如下所示的有效载荷:{followee_uid: ID, source: 43}。我不确定 source 是什么。
  • 还必须包含 CSRF header。
  • 因为这是一个POST类型的调用,所以无法直接在您的浏览器中打开这种类型的URL。

DELETE /api/v3/users/[USERID]/followings

还有一个 API 可以取消关注用户。 (仅供参考)

如果您从浏览器外部调用这些 API,您可能需要发送 session 个 cookie。


此脚本将列出 WEEDZAO 的前 10 个关注者,然后关注列表中的第一个:

  • 您必须将 USERID 和 CSRF_TOKEN 替换为您自己的值。
  • 您可以copy/paste将此代码输入浏览器开发控制台。
  • 或者,您可以使用 Puppeteer 等网络抓取框架中的代码。

// Find these values in dev console "Network" tab:
var CSRF_TOKEN, USERID, USERID_TARGET, main;
USERID_TARGET = '41874362';
USERID =        '12345678';
CSRF_TOKEN = 'MTYy...p8wg';

main = async function() {
  var body, followers, json, options, payload, response, url;

  // Get list of first 10 followers
  console.log(url = `/api/v3/users/${USERID_TARGET}/followers?cursor=0&count=10`);
  response = (await fetch(url));
  json = (await response.json());
  followers = json.follower_list;
  console.table(followers);

  
  // Follow first member from list above
  console.log(url = `/api/v3/users/${USERID}/followings`);
  payload = JSON.stringify({
    followee_uid: followers[0].uid,
    source: 43
  });
  response = (await fetch(url, options = {
    method: 'POST',
    body: payload,
    headers: {
      'X-CSRF-Token': CSRF_TOKEN
    }
  }));
  body = (await response.text());
  return console.log(body);
};

main();

这是一个完全有效的解决方案,我已经在我自己的 Chrome 浏览器中使用新帐户进行了测试,成功关注了您目标帐户的所有关注者帐户。

更新 (2021-06-18)

我已经将我的解决方案更新为一个大大改进和更快的函数,用 async/await 重写。这个新功能将估计的 运行 时间从约 45 分钟减少到约 10 分钟。 10 分钟仍然很长,但考虑到您的目标用户拥有大量关注者,这是可以预料的。

经过几次迭代,最新的功能不仅提高了速度、性能和错误报告,而且还扩展了该功能的可能性。我在下面提供了几个关于如何完整使用该功能的解决方案的示例。

为了使我的回答更加简洁,我将从该解决方案中完全删除我的旧功能,但如果您愿意,您仍然可以在我的解决方案的编辑历史记录中引用它。

TL;DR

这是最终的、最快的、可行的解决方案。确保将 PUT_YOUR_CSRF_TOKEN_HERE 替换为您自己的 CSRF 令牌值。下面是有关如何找到您的 CSRF 令牌的详细说明。

您必须在 Booyah 网站上的控制台中 运行 这样做以避免 CORS 问题。

const csrf = 'PUT_YOUR_CSRF_TOKEN_HERE';
async function booyahGetAccounts(uid, type = 'followers', follow = 1) {
    if (typeof uid !== 'undefined' && !isNaN(uid)) {
        const loggedInUserID = window.localStorage?.loggedUID;
        if (uid === 0) uid = loggedInUserID;
        const unfollow = follow === -1;
        if (unfollow) follow = 1;
        if (loggedInUserID) {
            if (csrf) {
                async function getUserData(uid) {
                    const response = await fetch(`https://booyah.live/api/v3/users/${uid}`),
                          data = await response.json();
                    return data.user;
                }
                const loggedInUserData = await getUserData(loggedInUserID),
                      targetUserData = await getUserData(uid),
                      followUser = uid => fetch(`https://booyah.live/api/v3/users/${loggedInUserID}/followings`, { method: (unfollow ? 'DELETE' : 'POST'), headers: { 'X-CSRF-Token': csrf }, body: JSON.stringify({ followee_uid: uid, source: 43 }) }),
                      logSep = (data = '', usePad = 0) => typeof data === 'string' && usePad ? console.log((data ? data + ' ' : '').padEnd(50, '━')) : console.log('━'.repeat(50),data,'━'.repeat(50));
                async function getList(uid, type, follow) {
                    const isLoggedInUser = uid === loggedInUserID;
                    if (isLoggedInUser && follow && !unfollow && type === 'followings') {
                        follow = 0;
                        console.warn('You alredy follow your followings. `follow` mode switched to `false`. Followings will be retrieved instead of followed.');
                    }
                    const userData = await getUserData(uid),
                          totalCount = userData[type.slice(0,-1)+'_count'] || 0,
                          totalCountStrLength = totalCount.toString().length;
                    if (totalCount) {
                        let userIDsLength = 0;
                        const userIDs = [],
                              nickname = userData.nickname,
                              nicknameStr = `${nickname ? ` of ${nickname}'s ${type}` : ''}`,
                              alreadyFollowedStr = uid => `User ID ${uid} already followed by ${loggedInUserData.nickname} (Account #${loggedInUserID})`;
                        async function followerFetch(cursor = 0) {
                            const fetched = [];
                            await fetch(`https://booyah.live/api/v3/users/${uid}/${type}?cursor=${cursor}&count=100`).then(res => res.json()).then(data => {
                                const list = data[type.slice(0,-1)+'_list'];
                                if (list?.length) fetched.push(...list.map(e => e.uid));
                                if (fetched.length) {
                                    userIDs.push(...fetched);
                                    userIDsLength += fetched.length;
                                    if (follow) followUser(uid);
                                    console.log(`${userIDsLength.toString().padStart(totalCountStrLength)} (${(userIDsLength / totalCount * 100).toFixed(4)}%)${nicknameStr} ${follow ? 'followed' : 'retrieved'}`);
                                    if (fetched.length === 100) {
                                        followerFetch(data.cursor);
                                    } else {
                                        console.log(`END REACHED. ${userIDsLength} accounts ${follow ? 'followed' : 'retrieved'}.`);
                                        if (!follow) logSep(targetList);
                                    }
                                }
                            });
                        }
                        await followerFetch();
                        return userIDs;
                    } else {
                        console.log(`This account has no ${type}.`);
                    }
                }
                logSep(`${follow ? 'Following' : 'Retrieving'} ${targetUserData.nickname}'s ${type}`, 1);
                const targetList = await getList(uid, type, follow);
            } else {
                console.error('Missing CSRF token. Retrieve your CSRF token from the Network tab in your inspector by clicking into the Network tab item named "bug-report-claims" and then scrolling down in the associated details window to where you see "x-csrf-token". Copy its value and store it into a variable named "csrf" which this function will reference when you execute it.');
            }
        } else {
            console.error('You do not appear to be logged in. Please log in and try again.');
        }
    } else {
        console.error('UID not passed. Pass the UID of the profile you are targeting to this function.');
    }
}
booyahGetAccounts(41874362);

过程详解

作为函数 运行s,它会将进度记录到控制台,包括到目前为止已关注的用户数量,以及基于总用户数按百分比计算的进度您定位的个人资料拥有的关注者。

正在检索您的 CSRF 令牌

此过程中唯一需要手动操作的部分是检索您的 CSRF 令牌。不过这很简单。登录 Booyah 后,导航到 Chrome 控制台的“网络”选项卡并单击名为 bug-report-claims 的项目,然后一直向下滚动出现在右侧的详细信息 window。那里应该看到x-csrf-token。将此值作为字符串变量存储在您的控制台中作为 csrf,我的函数将在 运行 时引用它。这是使用 POST 方法关注用户所必需的。

这是它的样子:

解决方法

该功能将遍历所有用户,您的目标帐户以 100 个为一组(每个 GET 请求允许的最大数量)并全部关注他们。当每批结束时,自动递归触发下一批。

版本 3(最快最灵活,使用 async/awaitfetch()

My previous two solution versions ( …) can be referenced in this answer's edit history.

确保将 PUT_YOUR_CSRF_TOKEN_HERE 替换为您自己的 CSRF 令牌值。下面是有关如何找到您的 CSRF 令牌的详细说明。

您必须在 Booyah 网站上的控制台中 运行 这样做以避免 CORS 问题。

const csrf = 'PUT_YOUR_CSRF_TOKEN_HERE';
async function booyahGetAccounts(uid, type = 'followers', follow = 1) {
    if (typeof uid !== 'undefined' && !isNaN(uid)) {
        const loggedInUserID = window.localStorage?.loggedUID;
        if (uid === 0) uid = loggedInUserID;
        const unfollow = follow === -1;
        if (unfollow) follow = 1;
        if (loggedInUserID) {
            if (csrf) {
                async function getUserData(uid) {
                    const response = await fetch(`https://booyah.live/api/v3/users/${uid}`),
                          data = await response.json();
                    return data.user;
                }
                const loggedInUserData = await getUserData(loggedInUserID),
                      targetUserData = await getUserData(uid),
                      followUser = uid => fetch(`https://booyah.live/api/v3/users/${loggedInUserID}/followings`, { method: (unfollow ? 'DELETE' : 'POST'), headers: { 'X-CSRF-Token': csrf }, body: JSON.stringify({ followee_uid: uid, source: 43 }) }),
                      logSep = (data = '', usePad = 0) => typeof data === 'string' && usePad ? console.log((data ? data + ' ' : '').padEnd(50, '━')) : console.log('━'.repeat(50),data,'━'.repeat(50));
                async function getList(uid, type, follow) {
                    const isLoggedInUser = uid === loggedInUserID;
                    if (isLoggedInUser && follow && !unfollow && type === 'followings') {
                        follow = 0;
                        console.warn('You alredy follow your followings. `follow` mode switched to `false`. Followings will be retrieved instead of followed.');
                    }
                    const userData = await getUserData(uid),
                          totalCount = userData[type.slice(0,-1)+'_count'] || 0,
                          totalCountStrLength = totalCount.toString().length;
                    if (totalCount) {
                        let userIDsLength = 0;
                        const userIDs = [],
                              nickname = userData.nickname,
                              nicknameStr = `${nickname ? ` of ${nickname}'s ${type}` : ''}`,
                              alreadyFollowedStr = uid => `User ID ${uid} already followed by ${loggedInUserData.nickname} (Account #${loggedInUserID})`;
                        async function followerFetch(cursor = 0) {
                            const fetched = [];
                            await fetch(`https://booyah.live/api/v3/users/${uid}/${type}?cursor=${cursor}&count=100`).then(res => res.json()).then(data => {
                                const list = data[type.slice(0,-1)+'_list'];
                                if (list?.length) fetched.push(...list.map(e => e.uid));
                                if (fetched.length) {
                                    userIDs.push(...fetched);
                                    userIDsLength += fetched.length;
                                    if (follow) followUser(uid);
                                    console.log(`${userIDsLength.toString().padStart(totalCountStrLength)} (${(userIDsLength / totalCount * 100).toFixed(4)}%)${nicknameStr} ${follow ? 'followed' : 'retrieved'}`);
                                    if (fetched.length === 100) {
                                        followerFetch(data.cursor);
                                    } else {
                                        console.log(`END REACHED. ${userIDsLength} accounts ${follow ? 'followed' : 'retrieved'}.`);
                                        if (!follow) logSep(targetList);
                                    }
                                }
                            });
                        }
                        await followerFetch();
                        return userIDs;
                    } else {
                        console.log(`This account has no ${type}.`);
                    }
                }
                logSep(`${follow ? 'Following' : 'Retrieving'} ${targetUserData.nickname}'s ${type}`, 1);
                const targetList = await getList(uid, type, follow);
            } else {
                console.error('Missing CSRF token. Retrieve your CSRF token from the Network tab in your inspector by clicking into the Network tab item named "bug-report-claims" and then scrolling down in the associated details window to where you see "x-csrf-token". Copy its value and store it into a variable named "csrf" which this function will reference when you execute it.');
            }
        } else {
            console.error('You do not appear to be logged in. Please log in and try again.');
        }
    } else {
        console.error('UID not passed. Pass the UID of the profile you are targeting to this function.');
    }
}

用法

到 运行 函数(对于上述任一解决方案),只需使用所需的用户 ID 名称作为参数调用函数名称,在您的示例中为 41874362。函数调用如下所示:

booyahGetAccounts(41874362);

尽管该函数的功能非常灵活。 booyahGetAccounts() 接受三个参数,但只有第一个是必需的。

booyahGetAccounts(
    uid, // required, no default
    type = 'followers', // optional, must be 'followers' or 'followings' -> default: 'followers'
    follow = 1 // optional, must be 0, 1, or -1, -> default: 1 (boolean true)
)

第二个参数type允许您选择是否要处理目标用户的关注者或关注者(该用户关注的用户)。

第三个参数允许您选择是要 follow/unfollow returned 用户还是只检索他们的用户 ID。这默认为 1 (boolean true) 将关注 returned 用户,但如果您只想测试功能而不实际关注 returned 用户,将其设置为虚假值,例如 0false。使用 -1 将取消关注用户 returned.

此函数会从 window.localStorage 对象中智能地为您检索您自己的用户 ID,因此您无需自行检索。如果您想处理自己的关注者或追随者,只需将 0 作为主要 uid 参数值传递,该函数会将 uid 默认为您自己的用户 ID。

因为您不能重新关注您已经关注的用户,如果您尝试关注您的关注者,该功能将产生警告 You already follow your followings. 'follow' mode switched to 'false'. Followings will be retrieved instead of followed. 而不是 return 他们就像您已经设置的一样follow 参数到 false.

但是,处理您自己的列表可能非常有用。例如,如果你想关注你自己的所有粉丝,你可以这样做:

booyahGetAccounts(0); // `type` and `follow` parameters already default to the correct values here

另一方面,如果您战略性地使用 follow/unfollow 技术来增加您的关注者数量并且需要取消关注您的所有关注者,您可以这样做:

booyahGetAccounts(0, 'followers', -1);

通过将 follow 参数值设置为 -1,您指示该函数 运行 它的 followUser 函数在所有 returned 用户 ID 上使用DELETE 方法而不是 POST 方法,从而取消关注这些用户 returned 而不是关注他们。

Desired outcome Function call
Follow all your own followers booyahGetAccounts(0, 'followers');
Unfollow all your own followers booyahGetAccounts(0, 'followers', -1);
Unfollow all your own followings booyahGetAccounts(0, 'followings', -1);
Follow users that follow User ID #12345 booyahGetAccounts(12345, 'followers');
Follow users followed by User ID #12345 booyahGetAccounts(12345, 'followings');
Retrieve User IDs of accounts following User ID #12345 booyahGetAccounts(12345, 'followers', 0);
Retrieve User IDs of accounts followed by User ID #12345 booyahGetAccounts(12345, 'followings', 0);

其他说明

  • 为了提高这个函数的性能,因为它非常重,我用一个专用的 userIDsLength 变量替换了对 userIDs.length 的所有调用,我添加到使用 += 每次迭代而不是每次调用长度。同样,我将字符串化 followerCount 的长度存储在变量 followerCountStrLength 中,而不是在每次迭代时调用 followerCount.toString().length。因为这是一个相当繁重的功能,所以您的浏览器 window 可能会崩溃。但是,它最终应该会完成。

  • 如果页面似乎因闪烁和自动关闭控制台而崩溃,请首先尝试重新打开控制台而不刷新页面。在我的例子中,inspector偶尔会自己关闭,可能是因为功能耗尽,但是当我再次打开inspector的控制台时,功能仍然是运行ning.