如何掌握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 或安全令牌。
在亚马逊的产品中,我们可以看到还有更多。有些会在其他地方或以其他格式提供,但有时它们是独一无二的。不管怎样,隐藏输入的名称往往比类更稳定。
元数据
虽然某些内容通过 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 = ['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 的示例一样,有时使用这种方法更方便。例如,日期通常以“类似机器”的格式显示,这在抓取时更有帮助。
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。
如果你愿意的话,可以先自己看一下。
提示:寻找品牌🤐。
我们将能够可靠地提取它,而不是从产品名称或面包屑中提取,因为我们不能说它们是否总是相关的。
你找到他们了吗?在本例中,他们使用“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 文件或数据库,无论您需要什么。嵌套字段不容易导出为任何这些格式。
我们已经占用了您足够的时间。吸收所有这些新信息,并将其运用到日常工作中。与此同时,我们将致力于制定以下指南来克服所有这些障碍!
结论
我们希望您参加三堂课:
- CSS 选择器很好,但还有其他选择。
- 有些内容是隐藏的 – 或不存在 – 但可以通过元数据访问。
- 尽量避免加载 Javascript 和无头浏览器以提高性能。
每一种方法都有优点和缺点、不同的方法以及很多很多的替代方案。编写完整的指南将是一本很长的书,而不是一篇博客文章。