in

如何爬取Algolia全文搜索

如何爬取Algolia全文搜索

Algolia为网络上许多流行网站的搜索系统提供支持,这使其成为流行的网络抓取目标。 在本网络抓取教程中,我们将了解如何使用 Python 抓取 Algolia 搜索。我们将看一下alternativeto.net的真实示例——一个软件元数据和推荐的 Web 数据库。通过这个例子,我们将看到 Algolia 是如何工作的,以及我们如何编写一个通用的网络抓取工具来从任何由 Algolia 支持的网站上抓取数据。

项目设置

我们将使用几个 Python 包进行网络抓取:

  • httpx作为我们的 HTTP 客户端。
  • parsel作为我们的 HTML 解析器(仅在奖励章节中使用)

两者都可以通过pip控制台命令安装:

$ pip install httpx parsel

什么是Algolia?

Algolia提供搜索索引 API 服务,因此任何网站都可以在后端工作量很少的情况下实现搜索系统。 它非常适合网络抓取,因为我们可以使用 Algolia 编写适用于任何网站的通用抓取工具。在本教程中,我们将做到这一点!

为了编写我们的抓取工具,我们需要了解 Algolia 的工作原理。为此,让我们从现实生活中的示例Alternativeto.net开始。 如果我们访问该网站并在搜索框中输入一些内容,我们可以看到后端请求正在发送到 Algolia 的 API:

使用浏览器开发工具界面(F12 键),我们可以看到提交搜索时发出的后台 POST 类型请求

我们注意到的第一件事是 URL 本身 – 它包含一些密钥,例如应用程序名称和 API 令牌,并发送带有我们的查询详细信息的 JSON 文档:

搜索 Algolia 时网络检查器的屏幕截图

现在我们知道了这一点,我们可以很容易地在 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 密钥。

Written by 河小马

河小马是一位杰出的数字营销行业领袖,广告中国论坛的重要成员,其专业技能涵盖了PPC广告、域名停放、网站开发、联盟营销以及跨境电商咨询等多个领域。作为一位资深程序开发者,他不仅具备强大的技术能力,而且在出海网络营销方面拥有超过13年的经验。