您可以在没有BeautifulSoup 或Selenium 的情况下抓取所有项目信息 - 您只需要requests。话虽这么说,它不是超级直截了当,所以我会尝试分解它:
当您访问一个 URL 时,您的浏览器会向外部资源发出许多请求。这些资源托管在服务器上(或者,现在,在几个不同的服务器上),它们构成了浏览器正确呈现网页所需的所有文件/数据。仅举几例,这些资源可以是图像、图标、脚本、HTML 文件、CSS 文件、字体、音频等。仅供参考,在我的浏览器中加载 www.google.com 会向各种资源发出 36 个请求。
您发出请求的第一个资源将始终是实际网页本身,即类似 HTML 的文件。然后,浏览器通过查看该 HTML 来确定它需要向哪些其他资源发出请求。
例如,假设网页包含一个包含我们要抓取的数据的表格。我们应该问自己的第一件事是“那张桌子是怎么出现在那个页面上的?”。我的意思是,有不同的方式可以用元素/html标签填充网页。这是一种这样的方式:
- 服务器从我们的浏览器接收到
page.html的请求
资源
- 该资源包含一个表,而该表需要数据,因此
服务器与数据库通信以检索数据
表
- 服务器获取该表数据并将其烘焙到 HTML 中
- 最后,服务器将 HTML 文件提供给您
- 您收到的是带有烘焙表格数据的 HTML。没有办法
您可以与前面提到的数据库进行通信 -
这很好,可取
- 您的浏览器呈现 HTML
当像这样抓取页面时,使用BeautifulSoup 是标准程序。您知道您要查找的数据已写入 HTML,因此BeautifulSoup 将能够看到它。
这是另一种可以用元素填充网页的方式:
- 服务器从我们的浏览器接收到
page.html的请求
资源
- 该资源需要另一个资源 - 一个脚本,它的工作是
在以后的某个时间点用数据填充表格
- 服务器向您提供该 HTML 文件(它不包含实际的
表数据)
当我说“稍后的时间点”时,该时间间隔可以忽略不计,对于使用实际浏览器查看页面的实际人类来说实际上是不明显的。然而,服务器只为我们提供了一个“基本”的 HTML。它只是一个空模板,它依赖于脚本来填充它的表。该脚本向 Web API 发出请求,Web API 用实际的表数据回复。所有这些都需要有限的时间,并且只有在脚本资源加载后才能开始。
当像这样抓取页面时,你不能使用BeautifulSoup,因为它只会看到“bare-bones”模板 HTML。这通常是您使用Selenium 模拟真实浏览会话的地方。
要返回您的 roblox 页面,此页面是第二种类型。
我建议的方法(这是我最喜欢的方法,在我看来,应该是您始终首先尝试的方法),只需找出潜在的 Web API 脚本正在发出请求,然后模仿请求得到你想要的数据。这是我最喜欢的方法的原因是因为这些 Web API 通常提供 JSON,这很容易解析。它超级干净,因为您只需要一个第三方模块 (requests)。
第一步是记录浏览器对资源的所有流量/请求。我将使用 Google Chrome,但其他现代浏览器可能具有类似功能:
- 打开谷歌浏览器并导航到目标页面
(
https://www.roblox.com/catalog/?Category=2&Subcategory=2&SortType=4)
- 按 F12 打开 Chrome 开发者工具菜单
- 点击“网络”标签
- 点击“过滤器”按钮(图标为漏斗形),然后
将过滤器选择从“All”更改为“XHR”(
XMLHttpRequest 或
XHR 资源是与服务器交互的对象。我们想
只查看 XHR 资源,因为它们可能与
网络 API)
- 单击圆形“录制”按钮(或按 CTRL + E)启用
日志记录 - 启用后图标应变为红色
- 按 CTRL + R 刷新页面并开始记录流量
- 刷新页面后,您应该会看到资源日志开始
填上。这是我们浏览器请求的所有资源的列表 -
我们只会看到 XHR 对象,因为我们设置了过滤器(如果
您很好奇,您可以将过滤器切换回“全部”以查看
对资源的所有请求的列表)
单击列表中的一项。右侧应打开一个面板,其中包含多个选项卡。单击“标头”选项卡以查看请求 URL、请求和响应标头以及任何 cookie(查看“Cookies”选项卡以获得更漂亮的视图)。如果请求 URL 包含任何查询字符串参数,您还可以在此选项卡中以更漂亮的格式查看它们。这是它的样子(对不起,大图):
此选项卡告诉我们关于模仿我们的请求的所有信息。它告诉我们应该在哪里提出请求,以及应该如何制定我们的请求才能被接受。 Web API 将拒绝格式错误的请求 - 并非所有 Web API 都关心相同的标头字段。例如,一些 Web API 非常关心“User-Agent”标头,但在我们的例子中,这个字段不是必需的。我知道的唯一原因是因为我复制并粘贴了请求标头,直到 Web API 不再拒绝我的请求 - 在我的解决方案中,我将使用最低限度来发出有效请求。
但是,我们实际上需要弄清楚这些 XHR 对象中的哪一个负责与正确的 Web API 对话——返回我们想要抓取的实际信息的那个。从列表中选择任何 XHR 对象,然后单击“预览”选项卡以查看 Web API 返回的数据的解析版本。假设是 Web API 将 JSON 返回给我们 - 您可能需要先展开和折叠树结构一段时间才能找到您要查找的内容,但是一旦您这样做了,您就会知道这个 XHR 对象是要求我们需要模仿。我碰巧知道我们感兴趣的数据在名为“details”的 XHR 对象中。以下是“预览”选项卡中扩展 JSON 的一部分:
如您所见,我们从该 Web API (https://catalog.roblox.com/v1/catalog/items/details) 获得的响应包含我们想要抓取的所有有趣数据!
这就是事情变得有点深奥的地方,并且特定于这个特定的网页(到目前为止,您可以使用所有东西通过 Web API 从其他页面抓取内容)。当您访问 https://www.roblox.com/catalog/?Category=2&Subcategory=2&SortType=4 时会发生以下情况:
- 您的浏览器会获取一些持续存在的 cookie,并生成 CSRF/XSRF 令牌并将其烘焙到页面的 HTML 中
- 最终,XHR 对象之一(以
"items?") 向 Web API 发出 HTTP GET 请求(需要 cookie!)
https://catalog.roblox.com/v1/search/items?category=Collectibles&limit=60&sortType=4&subcategory=Collectibles
(注意查询字符串参数)
响应是 JSON。它包含一个项目描述符列表,如下所示:
然后,一段时间后,另一个 XHR 对象(“详细信息”)向 Web API https://catalog.roblox.com/v1/catalog/items/details 发出 HTTP POST 请求(请参阅第一个和第二个屏幕截图)。这个请求只有在它包含正确的 cookie 和前面提到的 CSRF/XSRF 令牌时才会被 Web API 接受。此外,此请求还需要一个包含我们要抓取其信息的资产 ID 的有效负载 - 未能提供此信息也会导致拒绝。
所以,这有点棘手。一个 XHR 对象的请求取决于另一个 XHR 对象的响应。
所以,这是脚本。它首先创建一个requests.Session 来跟踪cookie。我们定义了一个字典params(实际上只是我们的查询字符串)——您可以更改这些值以满足您的需要。它现在的编写方式是从“收藏品”类别中提取前 60 个项目。然后,我们使用正则表达式从 HTML 正文中获取 CSRF/XSRF 令牌。我们根据 params 获取前 60 个项目的 id,并生成最终 Web API 请求将接受的字典/有效负载。我们发出最终请求,创建项目列表(字典),并打印查询的第一项的键和值。
def get_csrf_token(session):
import re
url = "https://www.roblox.com/catalog/"
response = session.get(url)
response.raise_for_status()
token_pattern = "setToken\\('(?P<csrf_token>[^\\)]+)'\\)"
match = re.search(token_pattern, response.text)
assert match
return match.group("csrf_token")
def get_assets(session, params):
url = "https://catalog.roblox.com/v1/search/items"
response = session.get(url, params=params, headers={})
response.raise_for_status()
return {"items": [{**d, "key": f"{d['itemType']}_{d['id']}"} for d in response.json()["data"]]}
def get_items(session, csrf_token, assets):
import json
url = "https://catalog.roblox.com/v1/catalog/items/details"
headers = {
"Content-Type": "application/json;charset=UTF-8",
"X-CSRF-TOKEN": csrf_token
}
response = session.post(url, data=json.dumps(assets), headers=headers)
response.raise_for_status()
items = response.json()["data"]
return items
def main():
import requests
session = requests.Session()
params = {
"category": "Collectibles",
"limit": "60",
"sortType": "4",
"subcategory": "Collectibles"
}
csrf_token = get_csrf_token(session)
assets = get_assets(session, params)
items = get_items(session, csrf_token, assets)
first_item = items[0]
for key, value in first_item.items():
print(f"{key}: {value}")
return 0
if __name__ == "__main__":
import sys
sys.exit(main())
输出:
id: 76692143
itemType: Asset
assetType: 8
name: Chaos Canyon Sugar Egg
description: This highly collectible commemorative egg recalls that *other* classic ROBLOX level, the one that was never quite as popular as Crossroads.
productId: 11837951
genres: ['All']
itemStatus: []
itemRestrictions: ['Limited']
creatorType: User
creatorTargetId: 1
creatorName: ROBLOX
lowestPrice: 400
purchaseCount: 7714
favoriteCount: 2781
>>>