如何掌握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 Scraping API & Data Extraction - ZenRows

为了避免每次都请求 HTML,我们可以将其存储在 HTML 文件中并从那里加载 BeautifulSoup。对于一个简单的演示,我们可以手动完成此操作。一种简单的方法是查看页面源代码,将其复制并粘贴到文件中。必须像爬虫一样在不登录的情况下访问该页面。

在此处获取 HTML 可能看起来是一个简单的任务,但事实并非如此。我们不会在这篇博文中介绍它,但它值得一个完整的指南。我们的建议是使用这种静态方法,因为许多网站会在几次请求后将您重定向到登录页面。其他一些会显示验证码,在最坏的情况下,您的 IP 将被禁止。

with open("test.html") as fp: 
    soup = BeautifulSoup(fp, "html.parser") 
 
print(soup.title.string) # Web Scraping API & Data Extraction - ZenRows

一旦我们从文件静态加载,我们就可以进行尽可能多的测试,而不会出现任何网络或阻塞问题。

编码前探索

在开始编码之前,我们必须了解页面的内容和结构。为此,我们知道的更简单的方法是使用浏览器检查目标页面。我们将使用 Chrome 的 DevTools,但其他浏览器也有类似的工具。

例如,我们可以打开亚马逊上的任何产品页面,快速浏览一下就会向我们显示产品的名称、价格、可用性和许多其他字段。在复制所有这些选择器之前,我们建议花几分钟时间查找隐藏的输入、元数据和网络请求。

请注意使用 Chrome DevTools 或类似方法执行此操作。一旦 Javascript 和网络请求(可能)修改了内容,您就会看到内容。这很烦人,但有时我们必须探索原始 HTML 以避免运行 Javascript。如果我们找到所有内容,我们就不需要运行无头浏览器 – 即 Puppeteer,从而节省时间和内存消耗。

免责声明:我们不会在每个示例的代码片段中都包含 URL 请求。他们看起来都像第一个。请记住,如果您要多次测试 HTML 文件,请将其存储在本地。

隐藏输入

隐藏输入允许开发人员包含最终用户无法看到或修改的输入字段。许多表单使用这些来包含内部 ID 或安全令牌。

亚马逊的产品中,我们可以看到还有更多。有些会在其他地方或以其他格式提供,但有时它们是独一无二的。不管怎样,隐藏输入的名称往往比类更稳定。

amazon-input-hidden

元数据

虽然某些内容通过 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 或其他身份验证方法。或者他们可以立即禁止您,因为这不是常规用户路径。当心。

auction-xhr-requests

再看一下图像。

您可以获得的所有数据都已清理并格式化,随时可以提取。然后还有更多。地理位置、内部 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 = ['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://twitter.com/webscraperio']

如果您不熟悉正则表达式,第二个有点棘手。简而言之,他们将尝试匹配给定搜索模式的任何文本。

在这种情况下,它将尝试匹配一些字符(主要是字母和数字),然后是 [@],然后是字符 – 域 – [点],最后是两到四个字符 – 互联网顶级域或 TLD。例如,它会发现[email protected].

请注意,此正则表达式不是完整的正则表达式,因为它不会匹配组合的 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 数据帧中。这是一个简单的示例,但您应该像来自数据集一样操作所有数据。

我们首先找到一个表并循环遍历所有行(“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 的示例一样,有时使用这种方法更方便。例如,日期通常以“类似机器”的格式显示,这在抓取时更有帮助。

netflix-schema

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' ...]

隐藏的电子商务产品信息

结合一些已经见过的技术,我们的目标是提取不可见的产品信息。我们的第一个示例是 Shopify 电子商务Spigen

如果你愿意的话,可以先自己看一下。

提示:寻找品牌🤐。

我们将能够可靠地提取它,而不是从产品名称或面包屑中提取,因为我们不能说它们是否总是相关的。

spigen-schema-metadata

你找到他们了吗?在本例中,他们使用“itemprop”并包含来自 schema.org 的产品和报价。我们可以通过查看表格或“添加到购物车”按钮来判断产品是否有库存。但没有必要,我们可以信赖itemprop="availability"。至于品牌,与 YouTube 使用的片段相同,但将属性名称更改为“品牌”。

brand = soup.find('meta', itemprop='brand') 
print(brand['content']) # Tesla

另一个 Shopify 示例:nomz。我们想要提取评分计数和平均值,可以在 HTML 中访问,但有些隐藏。使用 CSS 隐藏平均评分。

有一个屏幕阅读器专用标签,上面有平均值和附近的计数。这两个包含文本,没什么大不了的。但我们知道我们可以做得更好。

如果你检查一下来源,这很容易。产品架构将是您首先看到的。应用 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 文件或数据库,无论您需要什么。嵌套字段不容易导出为任何这些格式。

我们已经占用了您足够的时间。吸收所有这些新信息,并将其运用到日常工作中。与此同时,我们将致力于制定以下指南来克服所有这些障碍!

结论

我们希望您参加三堂课:

  1. CSS 选择器很好,但还有其他选择。
  2. 有些内容是隐藏的 – 或不存在 – 但可以通过元数据访问。
  3. 尽量避免加载 Javascript 和无头浏览器以提高性能。

每一种方法都有优点和缺点、不同的方法以及很多很多的替代方案。编写完整的指南将是一本很长的书,而不是一篇博客文章。

类似文章