网页抓取的注意事项
对于那些刚接触网络抓取的人、普通用户或只是好奇的人来说:这些技巧是黄金。刮痧看似是一项容易上手的活动,事实确实如此。但它会带你掉进兔子洞。在您意识到这一点之前,您已被某个网站屏蔽,您无法将其扩展到另外四个网站。
轮换IP
最简单、最常见的反抓取技术就是通过IP封禁。服务器将向您显示第一页,但它会检测到来自同一 IP 的流量过多,并在一段时间后阻止它。那么你的刮刀将无法使用。您甚至无法从真正的浏览器访问该网页。关于网络抓取的第一课是永远不要使用您的实际 IP。
每个请求都会留下痕迹,即使您试图在代码中避免它。网络的某些部分是您无法控制的。但你可以使用代理来更改你的IP。服务器会看到一个IP,但它不会是你的。下一步,轮换 IP 或使用可以为您完成此操作的服务。这究竟意味着什么?
您可以每隔几秒或每个请求使用不同的 IP。目标服务器无法识别您的请求,也不会阻止这些 IP。您可以建立一个庞大的代理列表,并为每个请求随机选取一个。或者使用旋转代理来为您做到这一点。无论哪种方式。仅通过这一更改,您的抓取工具正确工作的机会就猛增。
import requests import random urls = ["http://ident.me"] # ... more URLs proxy_list = [ "54.37.160.88:1080", "18.222.22.12:3128", # ... more proxy IPs ] for url in urls: proxy = random.choice(proxy_list) proxies = {"http": f"http://{proxy}", "https": f"http://{proxy}"} response = requests.get(url, proxies=proxies) print(response.text) # prints 54.37.160.88 (or any other proxy IP)
请使用自定义用户代理
第二常见的反抓取机制是User-Agent。UA 是浏览器在请求中发送的用于识别自身身份的标头。它们通常是一个长字符串,声明浏览器的名称、版本、平台等等。以 iPhone 13 为例:
"Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1"
发送用户代理没有任何问题,实际上建议这样做。问题是送哪一张。许多 HTTP 客户端发送自己的(cURL、Python 中的请求或 Javascript 中的 Axios),这可能是可疑的。您能想象您的服务器通过 UA 收到数百个请求吗"curl/7.74.0"
?至少你会持怀疑态度。
解决方案通常是找到有效的 UA(例如上面 iPhone 中的 UA)并使用它们。但它也可能对你不利。短时间内有数千个完全相同版本的请求?
因此,下一步是拥有几个有效且现代的用户代理并使用它们。并保持列表更新。与 IP 一样,在代码中的每个请求中轮换 UA。
# ... same as above user_agents = [ "Mozilla/5.0 (iPhone ...", "Mozilla/5.0 (Windows ...", # ... more User-Agents ] for url in urls: proxy = random.choice(proxy_list) proxies = {"http": f"http://{proxy}", "https": f"http://{proxy}"} response = requests.get(url, proxies=proxies) print(response.text)
研究目标内容
在开始开发之前先看一下源代码。许多网站提供了比 CSS 选择器更易于管理的数据抓取方式。公开数据的标准方法是通过丰富的代码片段,例如通过 Schema.org JSON 或itemprop
数据属性。其他人将隐藏输入用于内部目的(即 ID、类别、产品代码),您可以利用。事情远不止表面上看到的那样。
其他一些站点在首次加载后依赖XHR 请求来获取数据。而且它是结构化的!对于我们来说,更简单的方法是在打开 DevTools 的情况下浏览站点并检查 HTML 和 Network 选项卡。您将在几分钟内拥有清晰的愿景并决定如何提取数据。这些技巧并不总是可用,但使用它们可以让您省去麻烦。例如,元数据的变化往往比 HTML 或 CSS 类少,因此从长远来看更可靠、更可维护。
我们写了关于在使用 Python 中的示例和代码进行编码之前进行探索;查看更多信息。
并行化请求
在切换设备并扩大规模之后,旧的单文件顺序脚本将不够用。您可能需要将其“专业化”。对于一个很小的目标和几个 URL,将它们一一获取可能就足够了。但随后将其扩展到数千个不同的域。它不会正常工作。
扩展的第一步是同时获取多个 URL,并且不要因响应缓慢而停止整个抓取。从 50 行脚本到 Google 规模是一个巨大的飞跃,但第一步是可以实现的。您需要的主要东西是:并发性和队列。
并发性
主要思想是同时发送多个请求,但有限制。然后,一旦收到回复,就发送新的回复。假设限制是十个。这意味着在任何给定时间都会有 10 个 URL 始终在运行,直到没有更多 URL,这使我们进入下一步。
我们编写了有关使用并发的指南(Python 和 Javascript 中的示例)。
队列
队列是一种数据结构,允许添加稍后处理的项目。您可以从单个 URL 开始抓取,获取 HTML 并提取您想要的链接。将它们添加到队列中,它们将开始运行。继续做同样的事情,你就构建了一个可扩展的爬虫。缺少一些要点,例如重复 URL 删除(不对同一个 URL 爬行两次)或无限循环。但解决这个问题的简单方法是设置抓取的最大页面数,并在到达那里后停止。
我们有一篇文章提供了一个使用 Python从种子 URL 中抓取的示例。
距离 Google 的规模(显然)还很远,但你可以用这种方法访问数千个页面。更准确地说,您可以为每个域设置不同的设置,以避免单个目标过载。我们将把它留给你😉
不要在所有事情上都使用无头浏览器
毫无疑问,Selenium、Puppeteer 和 Playwright 都很棒,但并不是灵丹妙药。它们会带来资源开销并减慢抓取过程。那么为什么要使用它们呢?Javascript 渲染内容 100% 需要,并且在许多情况下很有帮助。但问问自己是否属于这种情况。
大多数站点都会在第一个 HTML 请求上以某种方式提供数据。正因为如此,我们提倡反其道而行之。使用您最喜欢的工具和语言(cURL、Python 中的请求、Javascript 中的 Axios 等)首先测试纯 HTML 。检查您需要的内容:文本、ID、价格。这里要小心,因为有时您在浏览器上看到的数据可能会被编码(即,“在纯 HTML 中显示为”)。复制和粘贴可能不起作用。😅
在某些情况下,您找不到该信息,因为它在第一次加载时不存在,例如在Angular.io中。没问题,无头浏览器在这些情况下会派上用场。或者如上所示的XHR 抓取用于拍卖。
如果找到信息,请尝试编写提取器。快速破解可能足以进行测试。一旦确定了所需的所有内容,接下来的一点是将通用爬行代码与目标站点的自定义爬行代码分开。
我们使用三种不同的方法获取 HTML,对 10 个 URL 进行了小规模基准测试。
- 使用Python的“请求”:2.41秒
- 使用 chromium 的剧作家每次请求打开一个新浏览器:11.33 秒
- 使用 chromium 共享浏览器和所有 URL 上下文的剧作家:7.13 秒
它不是 100% 结论性的,也不是统计上准确的,但它显示了差异。在最好的情况下,我们谈论的是使用 Playwright 速度慢 3 倍,并且共享上下文并不总是一个好主意。我们甚至没有讨论 CPU 和内存消耗。
不要将代码与目标耦合
有些操作与您正在抓取的网站无关:获取 HTML、解析它、对新链接进行排队以进行爬网、存储内容等等。在理想的情况下,我们会将这些与依赖于目标站点的那些分开:CSS 选择器、URL 结构、DDBB 结构。
第一个脚本通常很纠结;没问题。但随着它的增长和新页面的添加,分离职责就变得至关重要。我们知道,说起来容易做起来难。但停下来思考对于开发可维护且可扩展的爬虫很重要。
我们发布了有关Python 分布式爬行的存储库和博客文章。它比我们迄今为止看到的要复杂一些。它使用外部软件(Celery用于异步任务队列,Redis作为数据库)。
长话短说,分离和抽象与目标站点相关的部分。在我们的示例中,我们通过为每个域创建一个文件来进行简化。在那里,我们指定了四件事:
- 如何获取HTML(请求VS无头浏览器)
- 过滤 URL 以排队等待抓取
- 要提取哪些内容(CSS 选择器)
- 数据存储位置(Redis 中的列表)
# ... def extract_content(url, soup): # ... def store_content(url, content): # ... def allow_url_filter(url): # ... def get_html(url): return headless_chromium.get_html(url, headers=random_headers(), proxies=random_proxies())
它距离大规模生产还很远。但代码重用很容易,添加新域也很容易。当添加更新的浏览器或标头时,可以很容易地修改旧的抓取工具以使用它们。
不要删除您的目标网站
您的额外负担对于亚马逊来说可能只是沧海一粟,但对于小型独立商店来说却是一种负担。请注意抓取的规模和目标的大小。
您可能可以同时在亚马逊上抓取数百个页面,而他们甚至不会注意到(尽管如此,请小心)。但许多网站运行在规格较差的单一共享计算机上,它们值得我们理解。调低这些站点的脚本功能。它可能会使代码复杂化,但如果响应时间增加就停止会很好。
另一点是检查并遵守他们的robots.txt。主要有两条规则:不要抓取不允许的页面并遵守Crawl-Delay
。该指令并不常见,但如果存在,则表示爬网程序在请求之间应等待的秒数。有一个Python模块可以帮助我们遵守robots.txt。
我们不会详细说明,但不会进行恶意活动(应该不用说,以防万一)。我们总是谈论在不违反法律或对目标站点造成损害的情况下提取数据。
不要混合来自不同浏览器的标头
最后一项技术适用于更高级别的反机器人解决方案。浏览器发送多个标头,其格式随版本而异。高级解决方案会检查这些内容并将它们与现实世界的标头集数据库进行比较。这意味着当您发送错误的邮件时,您会发出危险信号。或者更难注意到,因为没有发送正确的信息!访问httpbin以查看浏览器发送的标头。可能比你想象的要多,有些你甚至没有听说过!"Sec-Ch-Ua"
?😕
除了拥有一套实际的完整标头之外,没有简单的方法可以解决这个问题。并且拥有大量的它们,您使用的每个用户代理都有一个。不是一个适用于 Chrome,另一个适用于 iPhone,不。一。每。用户代理。🤯
有些人试图通过使用无头浏览器来避免这种情况,但我们已经说明了为什么最好避免它们。无论如何,你和他们之间并没有明确的关系。他们发送适用于该版本上的浏览器的整套标头。如果您修改其中任何一个,其余的可能无效。如果将 Chrome 与 Puppeteer 一起使用并覆盖 UA 以使用 iPhone ……您可能会感到惊讶。真正的 iPhone 不会发送"Sec-Ch-Ua"
,但 Puppeteer 会发送,因为您覆盖了 UA 但没有删除该发送。
一些站点提供用户代理列表。但数百个的完整集合很难获得,而这正是复杂场地刮削时所需的规模。
# ... header_sets = [ { "Accept-Encoding": "gzip, deflate, br", "Cache-Control": "no-cache", "User-Agent": "Mozilla/5.0 (iPhone ...", # ... }, { "User-Agent": "Mozilla/5.0 (Windows ...", # ... }, # ... more header sets ] for url in urls: # ... headers = random.choice(header_sets) response = requests.get(url, proxies=proxies, headers=headers) print(response.text)
我们知道最后一个有点挑剔。但一些反抓取解决方案可能非常挑剔,甚至比标头还要挑剔。有些人可能会检查浏览器甚至连接指纹——高级的东西。
结论
轮换 IP 并拥有良好的标头将允许您抓取和抓取大多数网站。仅在必要时使用无头浏览器并应用软件工程良好实践。
从小规模构建并以此为基础进行扩展,添加功能和用例。但在保持高成功率的同时,始终要牢记规模和可维护性。如果您时不时地被阻止,请不要绝望,并从每个案例中吸取教训。
大规模的网络抓取是一个充满挑战且漫长的旅程,但您可能不需要有史以来最好的系统。也不是100%的准确率。如果它适用于您想要的域,那就足够了!不要冻结试图达到完美,因为你可能不需要它。