离开 Meetup.com 并在无 API 访问权限的情况下提取过往活动数据
过去几年,Meetup.com 提高收费标准早已不是什么秘密。这对于一些成员众多的大型用户群体来说影响不大,但对于那些没有收入来源的小型群体而言,无疑是致命打击。
我实在想不明白,成员不足 50 人的小型群体,如何负担得起每 6 个月超过 150 英镑的费用,更何况还要支付活动场地、茶点、服务器空间以及其他小型群体所需的各项开支。而且这还不是固定成本,Meetup.com 很可能会进一步提价。
我公司在曼彻斯特管理的本地 Drupal 用户群体就是受影响的群体之一。我们既没有足够的收入,成员数量也不足以支撑我们继续使用 Meetup.com 的服务。所以,在使用了 14 年之后,我们取消了订阅,这个群体也将在接下来的几周内从 Meetup 平台上消失。
但是,那 14 年的活动数据该怎么办呢?这些数据都存储在 Meetup.com 上,没有 API 访问权限根本无法获取,而获取 API 访问权限的成本又非常高。我原本想逐个打开这些活动,把数据复制粘贴到 CSV 文件中,但总觉得应该有更好的办法。
在这篇文章中,我将介绍我是如何深入研究 Meetup.com 网站,了解数据呈现方式的,以及如何用 PHP 半土半洋地实现了一个解决方案,将群体的过往活动数据下载为 CSV 文件。同时,在这个过程中,也会涉及到关于 Drupal 开发以及 Drupal11 升级等方面可能遇到的一些思路和挑战,希望这些内容能为 Drupal 模块开发相关工作带来一定的参考价值。
一、深入挖掘数据
我能在页面上看到过往活动的列表。当向下滚动页面时,过往活动会分块加载出来。需要注意的是,当你阅读这篇文章时,这个页面可能已经不存在了,但它曾经就在这个位置。
如果查看浏览器后台的请求,我发现数据是从 /gql2 路径加载的,请求携带了一个负载,从某种 API 层获取下一批数据。
我们发送到这个端点的负载包含以下数据。
{
"operationName": "getPastGroupEvents",
"variables": {
"urlname": "nwdrupal",
"beforeDateTime": "2026-01-30T08:12:37.288Z",
"after": "MzA3NjgxNTk3OjE3NDcxNTkyMDAwMDA="
},
"extensions": {
"persistedQuery": {
"version": 1,
"sha256Hash": "9463f7c9ab5b08db3f2172223c806fb48993508781cd939184d9151c75214e3a"
}
}
}
据我分析,这个调用包含以下几个重要部分:
- urlname——这是我们群体的名称,在本例中是“nwdrupal”。
- beforeDateTime——这是数据的截止日期,我们将查找在此日期之前的活动信息。这似乎是一个 ISO 8601 格式的日期时间字符串(这是最好的日期格式)。
- after——这是 API 的分页控制参数。这里的字符串是当前系列页面的 ID。
- sha256Hash——这个值是 Meetup 在某个地方生成的,API 响应需要这个值。我不确定这个字符串是与我的用户账户相关,还是一个通用的字符串,因为注销账户或切换群体似乎都不会改变它。
目前还不清楚 sha256Hash 的值是如何生成的,但对于这组数据来说,它基本上是相同的。实际上,我能够提取出这个字符串,并在数据导出过程中重复使用。我猜想(也有点希望)不同的群体这个字符串可能会不同,但这就是我页面上的值。
让我们看看能否将这个调用转换为标准的 CURL 命令。
curl -sSL -X POST https://www.meetup.com/gql2 -d '{"operationName":"getPastGroupEvents","variables":{"urlname":"nwdrupal","beforeDateTime":"2026-01-30T08:12:37.288Z","after":"MzA3NjgxNTk3OjE3NDcxNTkyMDAwMDA="},"extensions":{"persistedQuery":{"version":1,"sha256Hash":"9463f7c9ab5b08db3f2172223c806fb48993508781cd939184d9151c75214e3a"}}}'
成功啦!这个命令运行得很好,返回了一个 34KB 的 JSON 数据块,其中包含列表中的接下来 10 个活动信息。这里面有很多数据,但我们可以挑出重要的部分。
查看返回的 JSON 格式,我们可以看到活动数据。文件顶部的数据签名包含了一些关于我们群体、当前组织者用户账户以及分页数据项数量的信息。
{
"data": {
"groupByUrlname": {
"id": "4396922",
"organizer": {
"id": "11408007",
"isStarterOrganizer": false,
"__typename": "Member"
},
"events": {
"totalCount": 173,
"pageInfo": {
"endCursor": "MzA3NjgxNTk3OjE3NDcxNTkyMDAwMDA=",
"hasNextPage": true,
"__typename": "PageInfo"
},
..
这里还有一个重要的 endCursor 值,它指向数据集的下一页。利用这个值,我们可以请求下一页的数据,以此类推,直到获取所有过往活动数据。为什么分页指针是一个哈希字符串呢?我也不清楚,但使用这个项,我们可以向 Meetup 系统查询下一页的结果。
在 JSON 数据中还有活动数据,存储在 edges 数组中。这个数组包含了活动的所有信息,包括日期和时间、活动标题和描述,以及与成员出席和门票相关的多个字段。
{
"data": {
"groupByUrlname": {
..
},
"events": {
..
},
"edges": [
{
"node": {
"id": "311961727",
"title": "NWDUG monthly meetup January 2026",
"eventUrl": "https://www.meetup.com/nwdrupal/events/311961727/",
"description": "The Zoom details will be available to anyone who RSVPs to the event.\n\nWe endeavor to have guest speakers for each meeting, so if you have something you'd like to present, please let us know!\n\nWe will also have the usual news and events roundup. Our virtual get together are always warm and welcoming - we hope to see you there.",
"aeoDescription": null,
"isSaved": false,
..
这是列表中的第二页数据,所以要获取第一页的结果,我们只需稍微修改查询,去掉负载变量部分的 after 参数即可。
curl -sSL -X POST https://www.meetup.com/gql2 -d '{"operationName":"getPastGroupEvents","variables":{"urlname":"nwdrupal","beforeDateTime":"2026-01-29T08:24:58.294Z"},"extensions":{"persistedQuery":{"sha256Hash":"9463f7c9ab5b08db3f2172223c806fb48993508781cd939184d9151c75214e3a"}}}'
这个命令返回第一页的结果,同样包含 endCursor 参数。
这意味着要下载所有数据,我们只需先进行一次调用获取第一页,然后利用分页数据提取其余部分即可。
让我们编写一些代码来实现这个功能吧!
二、使用 PHP 提取过往活动数据
根据上述信息,我们可以通过几次对这个 JSON 端点的调用轻松提取过往活动数据。
首先要做的是创建一个小函数,它接受一个 JSON 负载,并使用内置的 CURL 函数调用端点。使用函数意味着我们可以多次调用这个功能,而不必在代码库中到处散布 CURL 命令。
function getMeetupData($jsonPayload) {
$curl_handle = curl_init();
curl_setopt($curl_handle, CURLOPT_URL, "https://www.meetup.com/gql2");
curl_setopt($curl_handle, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl_handle, CURLOPT_POST, 1);
curl_setopt($curl_handle, CURLOPT_POSTFIELDS, $jsonPayload);
$result = curl_exec($curl_handle);
if (curl_errno($curl_handle)) {
echo 'Error:' . curl_error($curl_handle);
}
curl_close($curl_handle);
return json_decode($result);
}
这个函数的返回值是从 JSON 结果中提取的数据。
顺便说一下,我是通过将上面的 CURL 命令输入到我的 CURL 命令工具中创建了这段代码,该工具直接将命令转换为你上面看到的 PHP 代码。
现在我们需要定义用于查询的负载。我使用 sprintf 函数创建了我们需要的负载模板。我发现以这种方式创建模板意味着我可以反复使用它们,而且还避免了复杂而丑陋的字符串拼接。这里有一个变量用于存储 sha256Hash 键,这是请求成功所必需的。
$sha = 'put the sha key from the calls in the browser here';
$urlname = 'put the group name parameter here';
$rootDataUrlTemplate = '{"operationName":"getPastGroupEvents","variables":{"urlname":"%s","beforeDateTime":"2026-01-29T08:24:58.294Z"},"extensions":{"persistedQuery":{"sha256Hash":"%s"}}}';
$rootDataUrl = sprintf($rootDataUrlTemplate, $urlname, $sha);
$pagesDataUrlTemplate = '{"operationName":"getPastGroupEvents","variables":{"urlname":"%s","beforeDateTime":"2026-01-29T08:24:58.294Z","after":"%s"},"extensions":{"persistedQuery":{"version":1,"sha256Hash":"%s"}}}';
接下来,我们需要进行初始请求,然后从数据中提取下一页的哈希值。我们只需调用 getMeetupData() 函数,并通过访问有效负载中的正确位置来提取数据。
$data = getMeetupData($rootDataUrl);
$nextPage = $data->data->groupByUrlname->events->pageInfo->endCursor;
我们希望将数据存储为 CSV 格式,所以在这里设置文件句柄。
$eventsCsv = 'events.csv';
$eventsCsvHandle = fopen($eventsCsv, 'w');
现在我们只需从 JSON 端点获取下一页的结果,提取我们需要的数据,然后将其写入 CSV 文件。然后,我们可以找到下一页的 endCursor 数据,并获取下一页的数据(如果存在)。
我在这里使用了 do/while 循环,因为它会先执行循环中的代码,然后再进行下一次迭代,这意味着我们可以在进入第二页之前从第一页的结果中提取数据。
// Get the page of results whilst the $nextPage variable is not null.
do {
foreach ($data->data->groupByUrlname->events->edges as $event) {
// Extract data from the JSON payload.
$id = $event->node->id;
$title = $event->node->title;
$description = $event->node->description;
$date = $event->node->dateTime;
// Write data to the CSV file.
$writeMe = [
$id,
$title,
$description,
$date,
];
fputcsv($eventsCsvHandle, $writeMe);
}
// Create the payload for the next page and call it to get the next page of results.
$pagesDataUrl = sprintf($pagesDataUrlTemplate, $urlname, $nextPage, $sha);
$data = getMeetupData($pagesDataUrl);
$nextPage = $data->data->groupByUrlname->events->pageInfo->endCursor;
} while ($nextPage !== NULL);
通过命令行运行这段代码大约花了 10 秒,现在我已经得到了该群体 173 个过往活动的 CSV 文件,出于历史原因,我可以将这些数据提取为 HTML 并上传到我们的网站上。
需要注意的是,这段代码中几乎没有错误检测机制,所以如果出现问题,它只会报错并停止运行。这不是一个用于与 Meetup.com 进行可靠集成的生产就绪系统,更像是一个一次性脚本,用于抓取数据后就不再运行。
此外,Drupal 群体在活动描述中很少添加任何格式。有些群体在群体描述中使用 markdown 格式,所以你可能需要将数据通过 markdown 工具进行转换,以处理这种格式。
不过,现在数据已经处于可用状态了。
三、总结
最初,我尝试使用 PHP 加载页面并以这种方式进行网页抓取。但问题是,原始页面只是某个 JavaScript 应用的前端,实际上包含的标记非常少,却有大量的 JSON 数据,我并不想提取这些数据。
在研究过往活动页面发出的请求时,我很容易就找到了被调用的端点,显然这个端点根本不需要任何身份验证(除了一个简单的哈希值),我可以直接从网站上拉取数据。实际上,我能够从网站上的几个不同群体中提取过往活动数据。
对于这是否算一个安全问题,我有些拿不定主意。虽然你可以轻松获取出席信息,包括用户名和个人资料照片,但这些内容在网站上本来就是公开的。
这篇文章旨在帮助那些需要从该平台提取过往数据的人,而不是作为攻击或滥用的手段。
如果你想保存群体的历史活动信息,希望这些内容对你有所帮助。如果你要离开 Meetup.com,并且在获取群体的历史信息方面需要帮助,请与我联系。如果你遇到困难,也可以加入相关的交流群体与那里的人交流。


