网页抓取的注意事项
无论是网页抓取的新手、老手,还是好奇的玩家,这些技巧都至关重要。抓取看似入门容易,事实也确实如此。但它会把你带入一个个“兔子洞”。不知不觉中,你就被一个网站屏蔽了,你的代码简直是110%的意大利面条,根本无法扩展到其他四个网站。
你去过那里吗?✋ 我十年前就去过那里——没什么不好意思(好吧,只是有点不好意思)。继续听我们讲几分钟,我们会帮你找到通往兔子洞的路。🕳️
旋转 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
第二种最常见的反爬虫机制是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"
发送 User-Agent 本身并没有问题,实际上我们建议这样做。问题在于发送哪一个。许多 HTTP 客户端会发送自己的 User-Agent(例如 cURL、Python 中的请求或 JavaScript 中的 Axios),这可能会引起怀疑。你能想象你的服务器收到数百个带有"curl/7.74.0"
UA 的请求吗?你至少会心存疑虑。
解决方案通常是找到有效的 UA,例如上面 iPhone 上的那个,然后使用它们。但这也可能对你不利。短时间内收到数千个版本完全相同的请求?
因此,下一步是设置并使用多个有效且现代的 User-Agent。并保持列表更新。与 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)
DO 研究目标内容
在开始开发之前,请先查看源代码。许多网站提供比 CSS 选择器更易于管理的数据抓取方式。一种标准的数据展示方法是通过丰富的代码片段,例如通过 Schema.org JSON 或itemprop
数据属性。其他一些网站会使用隐藏的输入数据(例如 ID、类别、产品代码)用于内部用途,您可以利用这些功能。实际操作远不止于此。
一些其他网站在首次加载后依赖 XHR 请求来获取数据。而且数据结构化了!对我们来说,更简单的方法是打开 DevTools 浏览网站,并检查“HTML”和“网络”选项卡。几分钟后,你就能清晰地了解数据,并决定如何提取数据。这些技巧并非总是有效,但使用它们可以省去不少麻烦。例如,元数据通常比 HTML 或 CSS 类的更改更少,这使得它更可靠,并且更易于长期维护。
我们用 Python 中的示例和代码写了关于编码前探索的内容;查看更多信息。
并行化请求
换了设备并扩大规模后,旧的单文件顺序脚本将无法满足需求。你可能需要对其进行“专业化”。对于小目标和几个 URL,逐个获取可能就足够了。但当扩展到数千个不同的域名时,它就无法正常工作了。
实现这种扩展的第一步是同时获取多个 URL,并且不会因为响应缓慢而停止整个抓取过程。从 50 行脚本到 Google 规模的规模是一个巨大的飞跃,但第一步是可以实现的。您需要的主要工具包括:并发和队列。
并发
主要思路是同时发送多个请求,但要设置一个限制。然后,一旦收到响应,就立即发送一个新的请求。假设限制是 10 个。这意味着在任何给定时间,都会有 10 个 URL 处于运行状态,直到没有 URL 为止,这就引出了下一步。
我们编写了有关使用并发的指南(Python 和 Javascript 中的示例)。
队列
队列是一种数据结构,允许添加待处理的项目。你可以从单个 URL 开始抓取,获取 HTML 并提取所需的链接。将这些链接添加到队列中,它们就会开始运行。继续执行此操作,你就构建了一个可扩展的爬虫。虽然有些地方有所缺失,例如 URL 去重(避免重复抓取同一个 URL)或无限循环。但解决这个问题的简单方法是设置最大抓取页面数量,并在达到最大数量后停止。
我们有一篇文章,其中有一个使用 Python从种子 URL 进行抓取的示例。
虽然距离 Google 的规模还很远(显然),但用这种方法你可以访问数千个页面。更准确地说,你可以为每个域名设置不同的设置,以避免单个目标页面过载。至于具体设置,就交给你了😉
不要用无头浏览器做所有事情
毫无疑问,Selenium、Puppeteer 和 Playwright 都很棒,但它们并非灵丹妙药。它们会带来资源开销,并减慢数据抓取的速度。那么为什么要使用它们呢?JavaScript 渲染内容绝对需要它们,而且在很多情况下都很有用。但问问自己,如果你的情况也是如此。
大多数网站都会在第一个 HTML 请求中以某种方式提供数据。因此,我们建议你反过来做。先用你常用的工具和语言(cURL、Python 中的请求、JavaScript 中的 Axios 等等)测试一下纯 HTML。检查你需要的内容:文本、ID、价格。这里要小心,因为有时你在浏览器上看到的数据可能经过了编码(例如,在纯 HTML 中显示为 "")。复制粘贴可能不起作用。😅
在某些情况下,您会找不到信息,因为首次加载时它并不存在,例如在Angular.io中。没关系,在这种情况下,无头浏览器会派上用场。或者像上面 Auction 中展示的那样,使用 XHR 抓取。
如果你找到了这些信息,就尝试编写提取器。快速的 hack 或许足够测试一下。一旦你确定了所有想要的内容,下一步就是将通用的爬虫代码与目标网站的自定义代码区分开来。
- 使用 Python 的“请求”:2.41 秒
- 一位剧作家使用 Chromium 每次请求打开一个新浏览器:11.33 秒
- 使用 Chromium 共享浏览器和所有 URL 的上下文的剧作家:7.13 秒
虽然它并非 100% 结论性,也并非统计上准确,但它确实展现了差异。在最佳情况下,使用 Playwright 的速度会慢 3 倍,而且共享上下文并不总是一个好主意。我们甚至还没讨论 CPU 和内存消耗。
不要将代码与目标耦合
有些操作与您正在抓取的网站无关:获取 HTML、解析 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"
😕
解决这个问题没有简单的办法,只能拥有一套完整的标头。而且要足够多,每个 User-Agent 对应一个。不是 Chrome 一个,iPhone 一个,不对。每个 User-Agent 一个。🤯
有些人试图通过使用无头浏览器来避免这种情况,但我们已经解释了为什么最好避免使用它们。而且,你对他们也并非一无所知。他们会发送适用于该浏览器版本的完整标头。如果你修改了其中任何一个,其余部分可能就会失效。如果你在 Chrome 中使用 Puppeteer 并覆盖 UA 以使用 iPhone 版本……你可能会有意外的收获。真正的 iPhone 不会发送"Sec-Ch-Ua"
,但 Puppeteer 会,因为你覆盖了 UA 但没有删除那个。
有些网站会提供User-Agent 列表。但是,要获取数百个 User-Agent 的完整集合非常困难,而这正是抓取复杂网站数据所需的规模。
# ...
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% 的准确率。如果它在你想要的域名上有效,那就足够了!不要急于达到完美,因为你可能并不需要它。
如有疑问、问题或建议,请随时联系我们。
感谢阅读!你觉得内容有用吗?请传播并分享。👈
鏂囩珷鏉ユ簮锛�https://dev.to/anderrv/dos-and-donts-of-web-scraping-4lnl