GraphQL 正在成为处理动态网站中主要数据集的一种越来越流行的方式。数据量大的网站通常会使用 graphql 作为 javascript 驱动的前端的后端,它会在用户浏览网页时动态加载数据。这样做的主要缺点是抓取 graphql 网站更加困难,因为它需要浏览器模拟或逆向工程网站的后端功能。 在本文中,我们将了解什么是 Graphql 以及我们如何在网络抓取中处理它。我们将了解查询构建基础知识、如何检查网络目标和 graphql 查询以进行网络抓取。
什么是 GraphQL
Graphql 是一种用于基于图形的数据集的查询语言。图形在数据繁重的网站中越来越受欢迎,因为它是一种更高效、更灵活的数据传输方式。与 REST 服务相比,graphql 客户端可以显式请求数据集的特定部分,这是 Web 开发人员所期望的功能,因为它允许更高效的数据传输和更快、更灵活的开发。 当谈到网络抓取时,我们也可以分享同样的好处——通过减少查询,我们可以只抓取我们需要的细节,这将节省我们抓取的资源和带宽。 由 graphql 支持的网站的一些示例是: https : //wallmart.com/、https ://www.airbnb.com/、https : //www.coursera.org/、https : //opensea.io/ 等 抓取 Graphql 支持的网站通常有点困难,因为数据协议更复杂并且难以理解和复制。那么让我们来看看是什么让 graphql 协议如此特别,以及我们如何在网络抓取中利用它。
理解 Graphql
为了成功地抓取 GraphQL 支持的网站,我们需要了解两件事:一点 graphql 查询语言和一点网站逆向工程。让我们从 graphql 基础知识开始,以及我们如何在 Python 中应用它们。 Graphql 客户端通常通过包含两到三个关键字的 JSON 文档传输查询:强制query
和可选operationName
以及variables
:
{ "operationName": "QueryName", "query": "query QueryName($variable: VariableType) { field1 field2 filedObject() { field3 field4 } }", "variables": { "variable": "europe" } }
query 关键字必须包含一个有效的 graphql 查询,它是相当简单的函数,如语法。括号中的值表示可选的变量(例如MyQuery($some_number: Float)
),大括号中的值表示我们希望返回的字段。 让我们用一个包含一些虚构产品的示例 graphql 演示服务器来尝试一下。为此,让我们启动docker
并启动一个简单的演示服务器:
$ docker run -p 8000:8000 scrapecrow/graphql-products-example:latest Running strawberry on `http://0.0.0.0:8000/graphql`
如果我们在浏览器中打开调试工作室链接 ( http://0.0.0.0:8000/graphql
),我们可以使用前端帮助我们为这个演示数据集构建一些查询。让我们从返回价格大于100 的所有产品的产品查询开始:
# define query query MyQuery($priceGt: Float) { # we can only query queries server supports, in this example server contains # "products" query which can take many different filter parameters products(priceGt: $priceGt) { # here we define which fields we want returned name price # take note that "object" fields need to be further expanded to include which sub-fields to retrieve locations { name } } }
在这里,我们构建了一个非常简单的查询来获取产品名称、价格和位置详细信息。如果我们启动浏览器的网络检查器(F12
在 chrome 中并选择Network
选项卡)并查看当我们单击发送查询时发生的情况,我们可以看到正在发出一个简单的 json 请求:
浏览器客户端将带有我们的查询和变量的请求作为 JSON 文档发送到服务器,服务器返回带有查询结果或错误的 JSON 文档 – 非常简单! 让我们在 Python 中复制这个请求以完全掌握 graphql 客户端行为(我们将使用requests包)
import json import requests query = """ query MyQuery($priceGt: Float) { products(priceGt: $priceGt) { name price locations { name } } } """ json_data = { 'query': query, 'variables': { 'priceGt': 100, }, 'operationName': 'MyQuery', } response = requests.post('http://0.0.0.0:8000/graphql', json=json_data) print(json.dumps(response.json(), indent=2))
如果我们运行这个脚本,它会打印出我们在调试前端看到的相同 JSON 结果:
{ "data": { "products": [ { "name": "Nintendo gaming console [mark 1]", "price": 111.22, "locations": [ { "name": "London" } ] }, { "name": "Sony microwave [limited edition]", "price": 277.52, "locations": [ { "name": "Bangkok" } ] } ... } }
所以总而言之,我们可以将 graphql 查询作为 JSON POST 请求发送,query
它的有效负载中必须包含关键字。
抓取 Graphql
现在我们了解了 graphql 以及如何发出简单的 graphql 请求,让我们来看看它在网络抓取的上下文中是如何工作的。 在我们可以将 graphql 查询发送到我们的抓取目标之前,我们首先需要反省可用的查询键和变量。有两种方法可以做到这一点:内省和逆向工程。
深入理解 Graphql
Introspection 是一个漂亮的 graphql 特性,它告诉 graphql 客户端有关可用图形数据集的信息:支持哪些查询,数据集对象包含哪些字段以及可以为数据过滤提供哪些变量。 不幸的是,许多公共服务器禁用了 graphql 自省以防止数据抓取或数据库攻击——但是,我们仍然可以尝试! 有几种简单的方法可以执行 graphql 自省。第一个是我们可以运行一个简单的 graphql 查询,它引用特殊__schema
变量:
import json import requests query = """ query IntrospectionQuery { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name description locations args { ...InputValue } } } } fragment FullType on __Type { kind name description fields(includeDeprecated: true) { name description args { ...InputValue } type { ...TypeRef } isDeprecated deprecationReason } inputFields { ...InputValue } interfaces { ...TypeRef } enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } possibleTypes { ...TypeRef } } fragment InputValue on __InputValue { name description type { ...TypeRef } defaultValue } fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } } } } """ json_data = { 'query': query, } response = requests.post('http://0.0.0.0:8000/graphql', json=json_data) print(json.dumps(response.json(), indent=2))
此查询将以 JSON 格式返回数据集的架构,其中包含有关可用查询、对象和字段的信息。 然而,人类阅读 JSON 模式可能是一项非常艰巨的任务,因此我们可以使用一些内省 GUI 工具来大大简化这个过程。让我们来看看我们如何使用 graphql 开发 GUI 来编写用于网络抓取的代码。
使用 Apollo Studio 进行自省
流行的内省工具由主要的 graphql 平台Apollo提供。这个平台在网络抓取中经常遇到,所以我们可以使用他们自己的工作室工具来逆向工程 graphql 查询。 在这个例子中,我们将使用 SpaceX 开放的 graphql 数据库:https://spacex.land。让我们转到此页面,看看我们的 Web 检查器中发生了什么:
在这里,我们看到正在向 SpaceX 的 graphql 服务器发出 graphql 请求: https: //api.spacex.land/graphql 让我们尝试通过访问 https://studio.apollographql.com/并创建一个新图表项目:
如果自省是可能的,Apollo Studio 将为我们提供一个方便的 graphql 调试环境,我们可以在其中构建和测试我们的查询:
一旦我们对我们的查询感到满意,我们就可以在您的网络抓取工具中复制它们,就像我们在上一节中所做的那样:
# scrape all SpaceX launch details import json import requests query = """ query Launches { launches { launch_date_utc mission_name mission_id rocket { rocket { id name company } } details links { article_link } } } """ json_data = { 'query': query, } response = requests.post('https://api.spacex.land/graphql', json=json_data) print(json.dumps(response.json(), indent=2))
在上面的代码中,我们可以在单个请求中检索所有启动数据!GraphQL 服务器非常强大,通常允许在不分页的情况下检索大块数据集,从而使网络爬虫逻辑更加简单和易于维护。
挑战
不幸的是,graphql 自省很少公开访问,因此要构建用于网络抓取的 graphql 查询,我们需要运用一些逆向工程知识。这通常涉及一些常见的挑战:
- 锁定查询。 为了保护自己免受网络抓取,网站通常只响应已知查询。换句话说,有时查询哈希是在请求标头/正文中提供的,或者在评估之前由服务器检查。这意味着我们只能查询列入白名单的值,因此在我们的网络抓取工具代码中分析网站时,复制我们在网络检查器中看到的查询非常重要。
- 标头很重要。 另一种保护形式是标题锁定。网站通常会使用各种秘密令牌(应该首先查看诸如 之类的标头
Authorization
,带前缀的标头)来验证该查询来自网站 javascript 客户端,而不是某些网络抓取工具。X-
幸运的是,这些密钥中的大多数通常驻留在网页的 HTML 正文中,可以轻松找到并添加到网络抓取程序代码中。
总而言之,当网络抓取 graphql 时,最好的做法是复制网络浏览器正在执行的确切行为,然后可以仔细调整查询以优化网络抓取。
概括
在本介绍教程中,我们熟悉了网络抓取 graphql。我们深入了解了查询语言本身以及如何在 Python 中执行它。我们还介绍了如何使用各种内省工具来分析网站的 graphql 功能,以及 graphql 爬虫面临的一些挑战。 Graphql 正在成为一种越来越流行的动态加载数据的选择,并且通过一些努力,它就像任何其他动态数据技术一样可抓取!