掌握 Python 网页抓取:从零到大师
网站抓取远不止使用一些 CSS 选择器提取内容。本指南总结了我们多年的经验。借助这些新技巧和理念,您将能够可靠、快速、高效地抓取数据,并获取一些您原本以为不存在的额外字段。
先决条件
要使代码运行,您需要安装 python3。某些系统已预装。之后,运行 来安装所有必要的库pip install
。
pip install requests beautifulsoup4 pandas
使用请求库 (requests) 可以轻松地从 URL 获取 HTML。然后将内容传递给 BeautifulSoup,我们就可以开始获取数据并使用选择器进行查询了。我们不会详细介绍。简而言之,您可以使用CSS 选择器来获取页面元素和内容。有些选择器需要不同的语法,但我们稍后会讲解。
import requests
from bs4 import BeautifulSoup
response = requests.get("https://zenrows.com")
soup = BeautifulSoup(response.content, 'html.parser')
print(soup.title.string) # Web Data Automation Made Easy - ZenRows
为了避免每次都请求 HTML,我们可以将其存储在一个 HTML 文件中,然后从中加载 BeautifulSoup。为了进行简单的演示,我们可以手动执行此操作。一个简单的方法是查看页面源代码,然后将其复制并粘贴到一个文件中。访问页面时,务必确保没有登录,就像爬虫一样。
在这里获取 HTML 代码看似简单,但事实并非如此。我们不会在本篇博文中介绍它,但它值得一个完整的指南。我们建议使用这种静态方法,因为许多网站会在几次请求后将您重定向到登录页面。有些网站会显示验证码,在最坏的情况下,您的 IP 会被封禁。
with open("test.html") as fp:
soup = BeautifulSoup(fp, "html.parser")
print(soup.title.string) # Web Data Automation Made Easy - ZenRows
一旦我们从文件静态加载,我们就可以进行尽可能多的测试,而不会出现任何网络或阻塞问题。
编码前先探索
在开始编码之前,我们必须了解页面的内容和结构。为此,我们知道更简单的方法是使用浏览器检查目标页面。我们将使用 Chrome 的 DevTools,但其他浏览器也有类似的工具。
例如,我们可以打开亚马逊上的任何产品页面,快速浏览一下就能看到产品的名称、价格、库存情况以及许多其他字段。在复制所有这些选择器之前,我们建议花几分钟时间查找隐藏的输入、元数据和网络请求。
小心不要使用 Chrome DevTools 或类似工具执行此操作。一旦 JavaScript 和网络请求(可能)修改了内容,您就会看到它。这很麻烦,但有时我们必须探索原始 HTML 以避免运行 JavaScript。如果我们找到了所有内容,就不需要运行无头浏览器(例如 Puppeteer),从而节省时间和内存消耗。
免责声明:我们不会在每个示例的代码片段中包含 URL 请求。它们看起来都与第一个示例类似。请记住,如果您要多次测试,请将 HTML 文件存储在本地。
隐藏输入
隐藏输入允许开发者添加终端用户无法查看或修改的输入字段。许多表单使用隐藏输入来包含内部 ID 或安全令牌。
在亚马逊产品中,我们可以看到更多隐藏输入。有些会在其他地方或格式中提供,但有时它们是独一无二的。无论如何,隐藏输入的名称往往比类别更稳定。
元数据
虽然有些内容可以通过 UI 看到,但使用元数据提取可能更容易。您可以获取YouTube 视频中数字格式的观看次数和 YYYY-mm-dd 格式的发布日期。这两个信息可以通过可见部分的方法获取,但没有必要。花几分钟时间练习这些技巧是值得的。
interactionCount = soup.find('meta', itemprop="interactionCount")
print(interactionCount['content']) # 8566042
datePublished = soup.find('meta', itemprop="datePublished")
print(datePublished['content']) # 2014-01-09
XHR 请求
有些网站会加载空模板,并通过 XHR 请求获取所有数据。在这种情况下,仅检查原始 HTML 是不够的。我们需要检查网络,特别是 XHR 请求。
拍卖就是这样。在表单中填写任意城市并进行搜索。它会将你重定向到一个结果页面,其中包含一个框架页面,同时它会根据你输入的城市执行一些查询。
这迫使我们使用可以执行 JavaScript 并拦截网络请求的无头浏览器,但我们也看到了它的好处。有时您可以直接调用 XHR 端点,但它们通常需要 Cookie 或其他身份验证方法。或者,由于这不是常规用户路径,它们可能会立即禁止您。请务必小心。
我们挖到宝了。再看看图片。
所有您拥有的数据,都已清理并格式化,随时可供提取。此外,还有更多数据,例如地理位置、内部 ID、无格式的数字价格、制造年份等等。
提取可靠内容的秘诀和技巧
暂时放下你的冲动。用 CSS 选择器实现所有功能固然可行,但还有很多其他选择器。先看看这些选择器,然后在直接使用之前再三考虑。我们并不是说那些选择器不好,而我们的选择器很棒。别误会。
我们正在努力为您提供更多工具和想法。这样,每次的决定权都在您手中。
获取内部链接
现在,我们将开始使用 BeautifulSoup 获取有意义的内容。这个库允许我们通过 ID、类、伪选择器等方式获取内容。我们将仅介绍其一小部分功能。
此示例将从页面中提取所有内部链接。为简单起见,只有以斜杠开头的链接才会被视为内部链接。在更完整的情况下,我们应该检查域名和子域名。
internalLinks = [
a.get('href') for a in soup.find_all('a')
if a.get('href') and a.get('href').startswith('/')]
print(internalLinks)
一旦我们获得了所有这些链接,我们就可以进行去重并将它们排队,以便将来进行抓取。这样做,我们将构建一个完整的网站爬虫,而不仅仅是针对一个页面。由于这是一个完全不同的问题,我们想提及它,并准备一篇博客文章来讨论它的使用和可扩展性。需要抓取的页面数量可能会像滚雪球一样迅速增长。
需要注意的是:自动运行此程序时请务必谨慎。几秒钟内可能会收到数百个链接,这会导致对同一网站的请求过多。如果处理不当,可能会触发验证码或被封禁。
提取社交链接和电子邮件
另一个常见的抓取任务是提取社交链接和电子邮件。“社交链接”没有确切的定义,因此我们将根据域名获取。对于电子邮件,有两种选择:“mailto”链接和检查全文。
我们将使用抓取测试站点进行此演示。
第一个代码片段将获取所有链接,类似于上一个。然后循环遍历所有链接,检查是否存在任何社交域名或“mailto”。如果存在,则将该 URL 添加到列表中并最终打印出来。
links = [a.get('href') for a in soup.find_all('a')]
to_extract = ["facebook.com", "twitter.com", "mailto:"]
social_links = []
for link in links:
for social in to_extract:
if link and social in link:
social_links.append(link)
print(social_links)
# ['mailto:****@webscraper.io',
# 'https://www.facebook.com/webscraperio/',
# 'https://twitter.com/webscraperio']
如果你不熟悉正则表达式,那么第二个方法会更棘手一些。简而言之,它们会尝试匹配给定搜索模式的任何文本。
在这种情况下,它会尝试匹配一些字符(主要是字母和数字),然后是 [@],接着是字符 - 域名 - [点],最后是两到四个字符 - 互联网顶级域名或 TLD。例如,它会找到test@example.com
。
请注意,此正则表达式并不完整,因为它无法匹配诸如 之类的组合 TLD co.uk
。
我们可以在整个内容(HTML)中运行此表达式,也可以仅在文本中运行。我们使用 HTML 来完成补全,但由于电子邮件显示在文本和 href 中,因此我们会复制它。
emails = re.findall(
r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}",
str(soup))
print(emails) # ['****@webscraper.io', '****@webscraper.io']
自动解析表格
HTML 表格早已存在,但它们仍然用于显示表格内容。我们可以利用这一点,因为它们通常结构化且格式良好。
以维基百科最畅销专辑列表为例,我们将所有值提取到一个数组和一个 Pandas DataFrame 中。这是一个简单的例子,但你应该像操作数据集一样操作所有数据。
我们首先找到一个表格,并循环遍历所有行(“tr”)。对于每一行,找到单元格(“td”或“th”)。以下几行将从维基百科表格中删除注释和可折叠内容,但并非绝对必要。然后,将单元格中剥离的文本附加到行,并将行附加到最终输出。打印结果以检查一切是否正常。
table = soup.find("table", class_="sortable")
output = []
for row in table.findAll("tr"):
new_row = []
for cell in row.findAll(["td", "th"]):
for sup in cell.findAll('sup'):
sup.extract()
for collapsible in cell.findAll(
class_="mw-collapsible-content"):
collapsible.extract()
new_row.append(cell.get_text().strip())
output.append(new_row)
print(output)
# [
# ['Artist', 'Album', 'Released', ...],
# ['Michael Jackson', 'Thriller', '1982', ...]
# ]
另一种方法是直接使用pandas
并导入 HTML,如下所示。它会为我们处理所有事情:第一行将匹配标题,其余部分将作为正确类型的内容插入。read_html
返回一个数组,因此我们取出第一项,然后删除没有内容的列。
一旦进入数据框,我们就可以执行任何操作,例如按销售额排序,因为 Pandas 会将某些列转换为数字。或者将所有索赔销售额相加。虽然在这里用处不大,但你明白我的意思了。
import pandas as pd
table_df = pd.read_html(str(table))[0]
table_df = table_df.drop('Ref(s)', 1)
print(table_df.columns) # ['Artist', 'Album', 'Released' ...
print(table_df.dtypes) # ... Released int64 ...
print(table_df['Claimed sales*'].sum()) # 422
print(table_df.loc[3])
# Artist Pink Floyd
# Album The Dark Side of the Moon
# Released 1973
# Genre Progressive rock
# Total certified copies... 24.4
# Claimed sales* 45
从元数据而不是 HTML 中提取
如前所述,有一些方法可以不依赖视觉内容来获取关键数据。让我们以Netflix 的《猎魔人》为例。我们将尝试获取演员信息。很简单,对吧?一句话就够了。
actors = soup.find(class_="item-starring").find(
class_="title-data-info-item-list")
print(actors.text.split(','))
# ['Henry Cavill', 'Anya Chalotra', 'Freya Allan']
如果我告诉你有十四位演员,你会怎么做?你会试着把他们都找出来吗?如果你想自己尝试一下,就别往下翻了。我会等你的。
还没找到?记住,眼前看到的可不止这些。你知道其中三个;在原始 HTML 中搜索它们。说实话,下面还有另一个地方展示了全部演员阵容,但尽量避开。
Netflix 包含一个 Schema.org 代码片段,其中包含演员列表和许多其他数据。与 YouTube 示例一样,有时使用这种方法更方便。例如,日期通常以“类似机器”的格式显示,这在抓取数据时更有帮助。
import json
ldJson = soup.find("script", type="application/ld+json")
parsedJson = json.loads(ldJson.contents[0])
print([actor['name'] for actor in parsedJson['actors']])
# [... 'Jodhi May', 'MyAnna Buring', 'Joey Batey' ...]
在其他时候,如果我们不想渲染 JavaScript,这是一种实用的方法。我们将使用Instagram Billie Eilish 的个人资料来演示。它们是已知的拦截器。访问几个页面后,您将被重定向到登录页面。抓取 Instagram 数据时要小心,并使用本地 HTML 进行测试。
我们将在以后的文章中介绍如何避免这些阻止或重定向。敬请关注!
通常的做法是搜索一个类,在我们的例子中是“Y8-fY”。我们不建议使用这些类,因为它们可能会发生变化。它们看起来像是自动生成的。许多现代网站都使用这种 CSS,每次更改都会自动生成,这意味着我们不能依赖它们。
B 计划:"header ul > li"
对吧?它会起作用。但我们需要 JavaScript 渲染来实现,因为它在第一次加载时不存在。如前所述,我们应该尽量避免这种情况。
看一下源 HTML:标题和描述包含关注者、关注内容和帖子数量。由于它们是字符串格式,这可能会有点问题,但我们可以克服。如果我们只需要这些数据,就不需要无头浏览器了。太棒了!
metaDescription = soup.find("meta", {'name': 'description'})
print(metaDescription['content'])
# 87.9m Followers, 0 Following, 493 Posts ...
隐藏的电子商务产品信息
结合一些已介绍的技术,我们的目标是提取不可见的产品信息。我们的第一个示例是 Shopify 电商平台Spigen。
如果您愿意的话,可以先自己看一下。
提示:寻找品牌🤐。
我们将能够可靠地提取它,而不是从产品名称或面包屑中提取,因为我们无法判断它们是否总是相关的。
你找到他们了吗?在本例中,他们使用了“itemprop”,并包含了来自schema.org的产品和优惠信息。我们或许可以通过查看表单或“添加到购物车”按钮来判断产品是否有货。但其实没必要,我们可以放心使用itemprop="availability"
。至于品牌,代码片段与YouTube相同,但将属性名称改为“品牌”。
brand = soup.find('meta', itemprop="brand")
print(brand['content']) # Tesla
另一个 Shopify 示例:nomz。我们希望提取评分计数和平均值,这些值可以在 HTML 中访问,但会有所隐藏。平均评分使用 CSS 隐藏起来。
屏幕阅读器上有一个标签,旁边是平均值和计数。这两个标签包含文本,没什么大不了的。但我们知道我们可以做得更好。
如果你检查一下源代码,就会发现这很简单。你首先看到的是 Product 的 schema。运用 Netflix 示例中的知识,获取第一个“ld+json”块,解析 JSON,然后所有内容就都可用了!
import json
ldJson = soup.find("script", type="application/ld+json")
parsedJson = json.loads(ldJson.contents[0])
print(parsedJson["aggregateRating"]["ratingValue"]) # 4.9
print(parsedJson["aggregateRating"]["reviewCount"]) # 57
print(parsedJson["weight"]) # 0.492kg -> extra, not visible in UI
最后,同样重要的是,我们将利用数据属性,这在电子商务中也很常见。在检查Marucci Sports Wood Bats时,我们可以看到每个产品都有一些有用的数据点。数字格式的价格、ID、产品名称和类别。我们可能需要的所有数据都在这里。
products = []
cards = soup.find_all(class_="card")
for card in cards:
products.append({
'id': card.get('data-entity-id'),
'name': card.get('data-name'),
'category': card.get('data-product-category'),
'price': card.get('data-product-price')
})
print(products)
# [
# {
# "category": "Wood Bats, Wood Bats/Professional Cuts",
# "id": "1945",
# "name": "6 Bat USA Professional Cut Bundle",
# "price": "579.99"
# },
# {
# "category": "Wood Bats, Wood Bats/Pro Model",
# "id": "1804",
# "name": "M-71 Pro Model",
# "price": "159.99"
# },
# ...
# ]
剩余的障碍
好了!你已经获取了该页面的所有数据。现在你需要将其复制到第二个页面,然后再复制到第三个页面。规模很重要,避免被封禁也很重要。
但您还必须转换并存储这些数据:CSV 文件或数据库,无论您需要什么格式。嵌套字段不易导出为任何格式。
我们已经占用了您太多时间了。请吸收所有这些新信息,并将其运用到您的日常工作中。同时,我们将致力于编写以下指南,帮助您克服所有这些障碍!
结论
我们希望您能学到以下三点:
- CSS 选择器很好,但还有其他选择。
- 某些内容是隐藏的(或不存在的),但可以通过元数据访问。
- 尽量避免加载 Javascript 和无头浏览器以提高性能。
这些方法各有优缺点,方法各异,而且替代方案也多种多样。写一篇完整的指南,嗯,相当于一本厚厚的书,而不是一篇博客文章。
如果您了解更多网站抓取技巧或对应用这些技巧有疑问,请联系我们。
记住,我们讲解了数据抓取,但还有更多内容:爬取、避免被屏蔽、内容转换和存储、基础架构扩展等等。敬请期待!
不要忘记查看本系列的其余帖子。
如果您喜欢该内容,请分享。
鏂囩珷鏉ユ簮锛�https://dev.to/anderrv/mastering-web-scraping-in-python-from-zero-to-hero-4fj4