Python爬虫防封禁方法集合
抓取应该是从 HTML 中提取内容。听起来很简单,但是却有很多障碍。第一个是获取上述 HTML。为此,我们将使用Python 来避免检测。这可能需要绕过反机器人系统。使用 Python 或任何其他工具进行网页抓取而不被阻止并不是在公园散步。
网站倾向于保护他们的数据和访问。防御系统可以采取许多可能的行动。请继续关注我们,了解如何减轻其影响或使用Requests或Playwright直接绕过机器人检测。
注意:大规模测试时,切勿直接使用您的家庭 IP。一个小错误或失误,你就会被禁止。
先决条件
为了使代码正常工作,您需要安装 Python 3。有些系统已经预装了它。之后,通过运行安装所有必需的库pip install
。
pip install requests playwright npx playwright install
IP速率限制
最基本的安全系统是禁止或限制来自同一IP 的请求。这意味着普通用户不会在几秒钟内请求一百个页面,因此该连接将被标记为危险。
import requests response = requests.get('http://httpbin.org/ip') print(response.json()['origin']) # xyz.84.7.83
IP 速率限制的工作方式与 API 速率限制类似,但通常没有有关它们的公开信息。我们无法确定可以安全地提出多少请求。
我们的互联网服务提供商为我们分配了 IP,我们无法影响或屏蔽该 IP。解决办法就是改变它。我们不能修改一台机器的IP,但是我们可以使用不同的机器。数据中心可能有不同的 IP,但这不是真正的解决方案。
代理是。他们接受传入的请求并将其转发到最终目的地。它在那里不进行任何处理。但这足以掩盖我们的 IP 并绕过封锁,因为目标网站将看到代理的 IP。
轮换代理
有免费代理,尽管我们不推荐它们。它们可能适用于测试,但并不可靠。我们可以使用其中一些进行测试,正如我们将在一些示例中看到的那样。
现在我们有了不同的IP,我们的家庭连接也安全无虞。好的。但如果他们屏蔽了代理的 IP 怎么办?我们又回到了最初的位置。
我们不会详细介绍免费代理。只需使用列表中的下一个即可。经常更换它们,因为它们的使用寿命通常很短。
另一方面,付费代理服务提供 IP 轮换。我们的服务将工作相同,但网站会看到不同的 IP。在某些情况下,他们会针对每个请求或每隔几分钟轮换一次。无论如何,它们更难被禁止。当这种情况发生时,我们很快就会获得一个新的 IP。
import requests proxies = {'http': 'http://190.64.18.177:80'} response = requests.get('http://httpbin.org/ip', proxies=proxies) print(response.json()['origin']) # 190.64.18.162
我们知道这些;这意味着机器人检测服务也知道它们。一些大公司会阻止来自已知代理 IP 或数据中心的流量。对于这些情况,有一个更高的代理级别:住宅。
住宅代理价格更高,有时带宽有限,但它们为我们提供普通人使用的 IP。这意味着我们的移动提供商明天可以为我们分配该 IP。或者朋友昨天也有过。他们与实际的最终用户没有区别。
我们可以刮任何我们想要的东西,对吗?默认情况下便宜的,必要时昂贵的。不,还没有。我们只过了第一道坎,后面还有更多的坎。我们必须看起来像合法用户,以避免被标记为机器人或抓取工具。
用户代理标头
下一步是检查我们的请求标头。最广为人知的是User-Agent(简称 UA),但还有更多。UA 遵循我们稍后会看到的格式,许多软件工具(例如 GoogleBot)都有自己的格式。
如果我们直接使用 Python 请求或 cURL,目标网站将收到以下内容。
import requests response = requests.get('http://httpbin.org/headers') print(response.json()['headers']['User-Agent']) # python-requests/2.25.1
curl http://httpbin.org/headers # { ... "User-Agent": "curl/7.74.0" ... }
许多网站不会检查 UA,但这对于这样做的网站来说是一个巨大的危险信号。我们必须伪造它。幸运的是,大多数库都允许自定义标头。以下是使用请求的示例:
import requests headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36"} response = requests.get('http://httpbin.org/headers', headers=headers) print(response.json()['headers']['User-Agent']) # Mozilla/5.0 ...
要获取当前的用户代理,请像代码片段一样访问httpbin并复制它。使用相同的 UA 请求所有 URL 也可能会触发一些警报,从而使解决方案变得更加复杂。
理想情况下,我们将拥有所有当前可能的用户代理,并像处理 IP 那样轮换它们。既然这几乎是不可能的,我们至少可以拥有一些。有一些用户代理列表可供我们选择。
import requests import random user_agents = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148', 'Mozilla/5.0 (Linux; Android 11; SM-G960U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.72 Mobile Safari/537.36' ] user_agent = random.choice(user_agents) headers = {'User-Agent': user_agent} response = requests.get('https://httpbin.org/headers', headers=headers) print(response.json()['headers']['User-Agent']) # Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) ...
请记住,浏览器经常更改版本,并且此列表可能会在几个月内过时。如果我们要使用用户代理轮换,可靠的来源至关重要。我们可以手动完成或使用服务提供商。
我们又近了一步,但标头中仍然存在一个缺陷:反机器人系统也知道这一技巧并检查其他标头以及用户代理。
全套标头
每个浏览器,甚至版本,都会发送不同的标头。检查 Chrome 和 Firefox 的运行情况:
{ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "en-US,en;q=0.9", "Host": "httpbin.org", "Sec-Ch-Ua": ""Chromium";v="92", " Not A;Brand";v="99", "Google Chrome";v="92"", "Sec-Ch-Ua-Mobile": "?0", "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "none", "Sec-Fetch-User": "?1", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36" }
{ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "en-US,en;q=0.5", "Host": "httpbin.org", "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "none", "Sec-Fetch-User": "?1", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:90.0) Gecko/20100101 Firefox/90.0" }
它意味着你认为它意味着
import requests import random headers_list = [{ 'authority': 'httpbin.org', 'cache-control': 'max-age=0', 'sec-ch-ua': '"Chromium";v="92", " Not A;Brand";v="99", "Google Chrome";v="92"', 'sec-ch-ua-mobile': '?0', 'upgrade-insecure-requests': '1', 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36', 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'sec-fetch-site': 'none', 'sec-fetch-mode': 'navigate', 'sec-fetch-user': '?1', 'sec-fetch-dest': 'document', 'accept-language': 'en-US,en;q=0.9', } # , {...} ] headers = random.choice(headers_list) response = requests.get('https://httpbin.org/headers', headers=headers) print(response.json()['headers'])
什么。先前包含五个用户代理的数组是不完整的。我们需要一个包含每个用户代理的完整标头集的数组。为了简洁起见,我们将显示一个包含一项的列表。已经够久了。
在这种情况下,从 httpbin 复制结果是不够的。理想的情况是直接从源复制它。最简单的方法是使用 Firefox 或 Chrome DevTools 或浏览器中的等效工具。
转到“网络”选项卡,访问目标网站,右键单击请求并“复制为 cURL”。然后将 cURL 语法转换为 Python并将标题粘贴到列表中。
import requests import random headers_list = [{ 'authority': 'httpbin.org', 'cache-control': 'max-age=0', 'sec-ch-ua': '"Chromium";v="92", " Not A;Brand";v="99", "Google Chrome";v="92"', 'sec-ch-ua-mobile': '?0', 'upgrade-insecure-requests': '1', 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36', 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'sec-fetch-site': 'none', 'sec-fetch-mode': 'navigate', 'sec-fetch-user': '?1', 'sec-fetch-dest': 'document', 'accept-language': 'en-US,en;q=0.9', } # , {...} ] headers = random.choice(headers_list) response = requests.get('https://httpbin.org/headers', headers=headers) print(response.json()['headers'])
我们可以添加 Referer 标头以提高安全性,例如 Google 或同一网站的内部页面。它会掩盖我们总是直接请求 URL 而无需交互的事实。
但要小心,因为添加引荐来源网址会更改更多标头。您不希望您的 Python 请求脚本因此类错误而被阻止。
cookie
我们忽略了上面的cookie,因为它们值得单独的部分。Cookie 可以帮助您绕过一些反机器人程序或阻止您的请求。它们是我们需要正确理解的强大工具。
例如,cookie 可以跟踪用户会话并在登录后记住该用户。网站为每个新用户分配一个 cookie 会话。有很多方法可以做到这一点,但我们会尽力简化。然后,用户的浏览器将在每个请求中发送该 cookie,以跟踪用户的活动。
怎么会有这个问题呢?我们使用轮换代理,因此每个请求可能具有来自不同地区或国家的不同 IP。反机器人程序可以看到该模式并阻止它,因为这不是用户浏览的自然方式。
另一方面,一旦您绕过反机器人解决方案,它就会发送有价值的 cookie。如果会话看起来合法,防御系统不会检查两次。查看如何绕过 Cloudflare以获取更多信息。
cookie 会帮助我们的 Python 请求脚本避免机器人检测吗?或者他们会伤害我们并阻止我们?答案在于我们的实施。
对于简单的情况,不发送 cookie 可能效果最好。无需维持会话。
对于更高级的案例和反机器人软件,会话 cookie 可能是获取和抓取最终内容的唯一方法。始终考虑会话请求和 IP 必须匹配。
如果我们希望在 XHR 调用后在浏览器中生成内容,也会发生同样的情况。我们需要使用无头浏览器。初始加载后,JavaScript 将尝试使用 XHR 调用获取一些内容。如果没有 cookie,我们就无法在受保护的站点上进行该调用。
我们将如何使用无头浏览器(特别是 Playwright)来避免检测?继续阅读!
无头浏览器
一些反机器人系统只会在浏览器解决 JavaScript 挑战后才会显示内容。我们不能使用 Python Requests 来模拟浏览器的行为。我们需要一个具有 JavaScript 执行功能的浏览器来运行并通过挑战。
Selenium、Puppeteer和 Playwright 是最常用和最知名的库。出于性能原因避免使用它们会更好,并且它们会使抓取速度变慢。但有时,别无选择。
我们将了解如何运行 Playwright。下面的代码片段显示了一个简单的脚本,该脚本访问打印发送的标头的页面。输出仅显示用户代理,但由于它是真正的浏览器,标头将包含整个集合(Accept、Accept-Encoding 等)。
import json from playwright.sync_api import sync_playwright with sync_playwright() as p: for browser_type in [p.chromium, p.firefox, p.webkit]: browser = browser_type.launch() page = browser.new_page() page.goto('https://httpbin.org/headers') jsonContent = json.loads(page.inner_text('pre')) print(jsonContent['headers']['User-Agent']) browser.close() # Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/93.0.4576.0 Safari/537.36 # Mozilla/5.0 (X11; Linux x86_64; rv:90.0) Gecko/20100101 Firefox/90.0 # Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15
这种方法有其自身的问题:看看用户代理。Chromium 包括Headless Chrome,它会告诉目标网站它是一个无头浏览器。他们可能会据此采取行动。
回到标题部分:我们可以添加自定义标题来覆盖默认标题。将上一个片段中的行替换为此行并粘贴有效的用户代理:
browser.new_page(extra_http_headers={'User-Agent': '...'})
这只是无头浏览器的入门级。无头检测本身就是一个领域,许多人正在研究它。有些是为了检测它,有些是为了避免被阻止。例如,您可以使用实际浏览器和无头浏览器访问Pixelscan 。要被认为“始终如一”,你需要努力工作。
请看下面的屏幕截图,是与 Playwright 一起访问 Pixelscan 时拍摄的。看到UA了吗?我们伪造的没有问题,但他们可以通过检查导航器 JavaScript API 来检测我们在撒谎。
我们可以传递user_agent
,Playwright 会为我们设置 JavaScript 中的 User-Agent 和 header。好的!
page = browser.new_page(user_agent='...')
您可以轻松地将剧作家秘密添加到脚本中,以应对更高级的情况,并使检测变得更加困难。它可以处理标头和浏览器 JavaScript API 之间的不一致等问题。
总之,实现 100% 覆盖很复杂,但大多数时候您并不需要它。站点始终可以执行一些更复杂的检查:WebGL、触摸事件或电池状态。
除非您尝试抓取需要绕过反机器人解决方案(例如 Akamai)的网站,否则您不需要这些额外的功能。对于这些情况,额外的努力将是强制性的。老实说,要求很高。
地理限制或地理封锁
您是否尝试过在美国境外观看 CNN?
这就是所谓的地理封锁。只有美国境内的联系人才能观看 CNN 直播。为了绕过这个问题,我们可以使用虚拟专用网络(VPN)。然后我们可以像往常一样浏览,但由于 VPN,该网站将看到本地 IP。
当抓取具有地理封锁的网站时,也会发生同样的情况。代理有一个等价物:地理定位代理。一些代理提供商允许我们从国家/地区列表中进行选择。例如,激活后,我们将仅获得来自美国的本地 IP。
行为模式
如今,仅仅阻止 IP 和用户代理是不够的。它们在几小时甚至几分钟内就会变得难以管理和陈旧。只要我们使用干净的 IP 和真实世界的用户代理执行请求,我们就基本上是安全的。涉及的因素较多,但大多数请求应该是有效的。
然而,大多数现代反机器人软件使用机器学习和行为模式,而不仅仅是静态标记(IP、UA、地理位置)。这意味着如果我们总是以相同的顺序执行相同的操作,我们就会被检测到。
- 转到主页。
- 点击“商店”按钮。
- 向下滚动。
- 转到第 2 页。
- …
几天后,启动相同的脚本可能会导致每个请求被阻止。许多人都可以执行相同的操作,但机器人有一些让它们显而易见的东西:速度。
使用软件,我们将按顺序执行每个步骤,而实际用户需要花一秒钟的时间,然后单击,使用鼠标滚轮缓慢向下滚动,将鼠标移动到链接,然后单击。
也许没有必要假装这一切,但要意识到可能出现的问题并知道如何面对它们。
我们必须思考我们想要什么。也许我们不需要第一个请求,因为我们只需要第二页。我们可以使用它作为入口点,而不是主页。
并保存一个请求。它可以扩展到每个域数百个 URL。无需按顺序访问每个页面;向下滚动,单击下一页,然后重新开始。
为了抓取搜索结果,一旦我们识别了分页的 URL 模式,我们只需要两个数据点:项目数和每页项目数。大多数时候,该信息出现在首页或请求上。
import requests from bs4 import BeautifulSoup response = requests.get('https://scrapeme.live/shop/') soup = BeautifulSoup(response.content, 'html.parser') pages = soup.select('.woocommerce-pagination a.page-numbers:not(.next)') print(pages[0].get('href')) # https://scrapeme.live/shop/page/2/ print(pages[-1].get('href')) # https://scrapeme.live/shop/page/48/
一项请求显示有 48 页。我们现在可以将它们排队。与其他技术相结合,我们将从该页面抓取内容并添加剩余的 47 个内容。要通过绕过反机器人系统来抓取它们,我们可以:
- 打乱页面顺序以避免模式检测。
- 使用不同的 IP 和用户代理,因此每个请求看起来都像是一个新的请求。
- 在某些呼叫之间添加延迟。
- 随机使用 Google 作为引荐来源网址。
我们可以编写一些混合所有这些的片段,但现实生活中最好的选择是使用一个工具来完成所有这些,例如Scrapy、pyspider、node-crawler (Node.js) 或Colly (Go)。
片段的想法是单独理解每个问题。但对于大型的、现实的项目,我们自己处理所有事情就太复杂了。
验证码
即使是准备得最充分的请求也可能会被捕获并显示验证码。如今,通过 Anti Captcha 和 2Captcha 等解决方案可以解决验证码问题,但这会浪费时间和金钱。最好的解决办法是避免它们。第二个最好的方法是忘记该请求并重试。
例外是显而易见的:网站在第一次访问时总是显示验证码。如果没有办法绕过它,我们就必须解决它。然后使用会话cookies来避免再次受到挑战。
这听起来可能违反直觉,但等待一秒钟并使用不同的 IP 和标头集重试相同的请求将比解决验证码更快。亲自尝试一下并告诉我们体验😉。
做一名优秀的网络公民
我们可以使用多个网站进行测试,但在大规模进行相同操作时要小心。努力做一名优秀的互联网公民,不要造成 DDoS。限制每个域的交互。亚马逊每秒可以处理数千个请求,但并非所有目标站点都能处理。
我们总是在谈论“只读”浏览模式。访问页面并读取其内容。切勿出于恶意提交表单或执行主动操作。
如果我们采取更积极的方法,其他几个因素也会很重要:写入速度、鼠标移动、无需点击的导航、同时浏览多个页面等。机器人防护软件对主动操作特别积极。出于安全原因应该如此。
我们不会讨论这一部分,但这些操作将为他们提供阻止请求的新理由。再说一次,好公民不会尝试大量登录。我们谈论的是抓取,而不是恶意活动。
有时网站会让数据收集变得更加困难,也许不是故意的。但使用现代前端工具,CSS 类可能每天都会发生变化,从而破坏精心准备的脚本。有关更多详细信息,请阅读我们之前关于如何在 Python 中抓取数据的文章。
结论
我们希望您记住那些唾手可得的成果:
- IP 轮换代理。
- 针对具有挑战性目标的住宅代理。
- 全套标头,包括用户代理。
- 当需要 JavaScript 挑战时,绕过 Playwright 的机器人检测,也许添加隐形模块。
- 避免可能将您标记为机器人的模式。还有更多,可能还有更多我们没有涉及到的。但通过这些技术,您应该能够大规模地爬行和抓取。毕竟,如果您知道如何操作,则可以在不被 Python 阻止的情况下进行网页抓取。