Algolia为网络上许多流行网站的搜索系统提供支持,这使其成为流行的网络抓取目标。 在本网络抓取教程中,我们将了解如何使用 Python 抓取 Algolia 搜索。我们将看一下alternativeto.net的真实示例——一个软件元数据和推荐的 Web 数据库。通过这个例子,我们将看到 Algolia 是如何工作的,以及我们如何编写一个通用的网络抓取工具来从任何由 Algolia 支持的网站上抓取数据。
项目设置
我们将使用几个 Python 包进行网络抓取:
两者都可以通过pip
控制台命令安装:
$ pip install httpx parsel
什么是Algolia?
Algolia提供搜索索引 API 服务,因此任何网站都可以在后端工作量很少的情况下实现搜索系统。 它非常适合网络抓取,因为我们可以使用 Algolia 编写适用于任何网站的通用抓取工具。在本教程中,我们将做到这一点!
理解和抓取 Algolia 搜索
为了编写我们的抓取工具,我们需要了解 Algolia 的工作原理。为此,让我们从现实生活中的示例Alternativeto.net开始。 如果我们访问该网站并在搜索框中输入一些内容,我们可以看到后端请求正在发送到 Algolia 的 API:
使用浏览器开发工具界面(F12 键),我们可以看到提交搜索时发出的后台 POST 类型请求
我们注意到的第一件事是 URL 本身 – 它包含一些密钥,例如应用程序名称和 API 令牌,并发送带有我们的查询详细信息的 JSON 文档:
现在我们知道了这一点,我们可以很容易地在 Python 中复制这种行为:
from urllib.parse import urlencode import httpx params = { "x-algolia-agent": "Algolia for JavaScript (4.13.1); Browser (lite)", "x-algolia-api-key": "88489cdf3a8fbfe07a2f607bf1568330", "x-algolia-application-id": "ZIDPNS2VB0", } search_url = "https://zidpns2vb0-dsn.algolia.net/1/indexes/fullitems/query?" + urlencode(params) search_data = { # for more see: https://www.algolia.com/doc/api-reference/search-api-parameters/ "query": "Spotify", "page": 1, "distinct": True, "hitsPerPage": 20, } response = httpx.post(search_url, json=search_data) print(response.json())
我们得到了结果的第一页以及分页元数据,我们可以使用它来同时检索剩余的页面。为此,让我们使用异步编程以极快的速度同时下载所有页面:
import asyncio import json from typing import List from urllib.parse import urlencode import httpx params = { "x-algolia-agent": "Algolia for JavaScript (4.13.1); Browser (lite)", "x-algolia-api-key": "88489cdf3a8fbfe07a2f607bf1568330", "x-algolia-application-id": "ZIDPNS2VB0", } search_url = "https://zidpns2vb0-dsn.algolia.net/1/indexes/fullitems/query?" + urlencode(params) async def scrape_search(query: str) -> List[dict]: search_data = { # for more see: https://www.algolia.com/doc/api-reference/search-api-parameters/ "query": query, "page": 1, "distinct": True, "hitsPerPage": 20, } async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as session: # scrape first page for total number of pages response_first_page = await session.post(search_url, json=search_data) data_first_page = response_first_page.json() results = data_first_page["hits"] total_pages = data_first_page["nbPages"] # scrape remaining pages concurrently other_pages = [ session.post(search_url, json={**search_data, "page": i}) for i in range(2, total_pages + 1) ] for response_page in asyncio.as_completed(other_pages): page_data = (await response_page).json() results.extend(page_data["hits"]) return results print(asyncio.run(scrape_search("spotify")))
我们上面写的这个简短的爬虫可以与任何 Algolia 驱动的搜索系统一起使用!要对此进行测试,您可以在一些流行的 Algolia 支持的网站上进行练习。
奖励:寻找代币
在我们的爬虫中,我们简单地硬编码了我们在网络检查器中发现的 Algolia API 和网络应用程序密钥,虽然它们不太可能经常更改,但如果我们正在构建一个高正常运行时间的实时网络爬虫,我们可能希望以编程方式发现它们。 由于 Algolia 是一个前端插件,所有需要的键都可以在 HTML 或 javascript 主体中找到。例如,键可以<input>
作为变量放置在隐藏节点或 javascript 资源中。 通过一点解析魔法和模式匹配,我们可以尝试提取这些键:
import re from urllib.parse import urljoin import httpx from parsel import Selector HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36", "Accept-Encoding": "gzip, deflate, br", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", "Connection": "keep-alive", "Accept-Language": "en-US,en;q=0.9,lt;q=0.8,et;q=0.7,de;q=0.6", } def search_keyword_variables(html: str): """Look for Algolia keys in javascript keyword variables""" variables = re.findall(r'(\w*algolia\w*?):"(.+?)"', html, re.I) api_key = None app_id = None for key, value in variables: key = key.lower() if len(value) == 32 and re.search("search_api_key|search_key|searchkey", key): api_key = value if len(value) == 10 and re.search("application_id|appid|app_id", key): app_id = value if api_key and app_id: print(f"found algolia details: {app_id=}, {api_key=}") return app_id, api_key def search_positional_variables(html: str): """Look for Algolia keys in javascript position variables""" found = re.findall(r'"(\w{10}|\w{32})"\s*,\s*"(\w{10}|\w{32})"', html) return sorted(found[0], reverse=True) if found else None def find_algolia_keys(url): """Scrapes url and embedded javascript resources and scans for Algolia APP id and API key""" response = httpx.get(url, headers=HEADERS) sel = Selector(response.text) # 1. Search in input fields: app_id = sel.css("input[name*=search_api_key]::attr(value)").get() search_key = sel.css("input[name*=search_app_id]::attr(value)").get() if app_id and search_key: print(f"found algolia details in hidden inputs {app_id=} {search_key=}") return { "x-algolia-application-id": app_id, "x-algolia-api-key": search_key, } # 2. Search in website scripts: scripts = sel.xpath("//script/@src").getall() # prioritize scripts with keywords such as "app-" which are more likely to contain environment keys: _script_priorities = ["app", "settings"] scripts = sorted(scripts, key=lambda script: any(key in script for key in _script_priorities), reverse=True) print(f"found {len(scripts)} script files that could contain algolia details") for script in scripts: print("looking for algolia details in script: {script}", script=script) resp = httpx.get(urljoin(url, script), headers=HEADERS) if found := search_keyword_variables(resp.text): return { "x-algolia-application-id": found[0], "x-algolia-api-key": found[1], } if found := search_positional_variables(resp.text): return { "x-algolia-application-id": found[0], "x-algolia-api-key": found[1], } print(f"could not find algolia keys in {len(scripts)} script details") ## input find_algolia_keys("https://www.heroku.com/search") ## kw variables find_algolia_keys("https://incidentdatabase.ai/apps/discover/") find_algolia_keys("https://fontawesome.com/search") ## positional variables find_algolia_keys("https://alternativeto.net/")
在上面的抓取算法中,我们正在扫描主页以查找可能位于以下位置的 Algolia API 密钥:
- 网站使用的脚本文件中的 Javascript 关键字变量。
- 位置 javascript 变量,我们知道 Algolia 网络 ID 的长度为 10 个字符,API 密钥的长度为 32 个字符。
- 隐藏在页面 HTML 中的输入表单。
该算法应该是无需动手即可找到 Algolia 密钥的良好开端!
常问问题
网络抓取 Algolia 合法吗?
是的。Algolia 搜索索引是公开可用的,我们不会提取任何个人或私人信息。以缓慢、尊重的速度抓取 Algolia 搜索将属于道德抓取定义。
是什么导致“期望值(接近 1:1)”错误?
当我们的 POST 请求的搜索正文格式不正确(应该是有效的 json)或者标头Content-Type
丢失或不正确时会导致此错误,而应将其设置为application/json
.
是什么导致“{“message”:“indexName is not valid”,“status”:400}”错误?
有些网站使用多个索引,需要通过请求体的“IndexName”关键字参数明确指定查询的索引。就像其他细节一样,我们可以在我们的 devtools 网络检查器(F12 键)中看到它。这个值不太可能改变,所以我们可以将它编码到您的网络抓取工具中。
概括
在这个简短的教程中,我们了解了如何抓取 Algolia 嵌入式搜索系统。我们为alternativeto.net编写了一个快速抓取器,可以同时抓取所有搜索结果。我们通过查看基本令牌扫描来查找那些 Algolia API 密钥。