如何使用Python抓取JavaScript动态网页内容
在使用 Python 抓取 JavaScript 呈现的网页时是否曾碰壁?
由于动态加载的数据,这肯定会很困难。更不用说有大量使用 React.js 或 Angular 等框架的 Web 应用程序,因此您的基于请求的抓取程序很可能在尝试执行时中断。
到目前为止,您可能已经意识到标准库和方法不足以抓取 JS 生成的内容。不用担心!在本教程中,您将获得完成工作的正确提示。
您准备好学习如何使用 Python 抓取 JavaScript 呈现的网页了吗?我们走吧:
为什么抓取 JavaScript 渲染的网页很困难?
当您向网页发送请求时,客户端会下载内容(这在 JS 网站中有所不同)。如果客户端支持 JavaScript,它将运行代码来填充呈现的 HTML 内容。
话虽如此…
这些页面并不会真正产生有价值的静态 HTML 内容。因此,纯 HTTP 请求是不够的,因为必须首先填充请求的内容。
这意味着您必须专门为每个目标网站编写代码。这就是抓取 JavaScript 内容如此困难的原因。
当然,还有其他选择。让我们看看呈现网页的不同方式:
- 静态渲染:这发生在构建时并提供快速的用户体验。主要缺点是必须为每个可能的 URL 生成单独的 HTML 文件。
众所周知,从静态网站抓取数据非常容易。
- 服务器呈现:它为服务器上的页面生成完整的 HTML 以响应导航。这避免了在客户端获取额外的数据,因为它是在浏览器获得响应之前处理的。
就像静态网站一样,我们可以通过发送简单的 HTTP 请求来提取信息,因为整个内容都从服务器返回。
- 客户端呈现:页面使用 JavaScript 直接在浏览器中呈现。逻辑、数据获取和路由是在客户端而不是服务器端处理的。
如今,许多现代应用程序结合了后两种方法,试图弥补它们的缺点。这通常被称为通用渲染。
React.js 和 Angular 等流行框架也支持它。例如,React 解析 HTML 并动态更新呈现的页面——这个过程称为 hydration。
如何抓取 JavaScript 生成的内容
有多种不同的方法可用。让我们探讨其中两个:
使用后端查询
有时,诸如 React 之类的框架会使用后端查询来填充页面。可以在您的应用程序中使用这些 API 调用,直接从服务器获取数据。
然而,这并不能保证。这意味着您首先需要检查浏览器请求以了解是否有可用的 API 后端。如果有,那么您可以使用与自定义查询相同的设置来获取数据。
使用脚本标签
那么,还有什么方法可以用来从网页中抓取 JavaScript 生成的内容呢?
您可以尝试将脚本标记中的隐藏数据用作 JSON 文件。但是,您应该知道这可能需要深入搜索,因为您将检查加载页面上的 HTML 标记。可以使用 BeautifulSoup Python 包提取 JS 代码。
Web 应用程序通常使用不同的身份验证方法来保护 API 端点,因此使用 API 来抓取 JS 呈现的页面可能具有挑战性。
如果静态内容中存在编码的隐藏数据,您可能无法对其进行解码。在这种情况下,您需要能够呈现用于抓取的 JavaScript 的软件。
您可以尝试基于浏览器的自动化工具,例如 Selenium、Playwright 或 Puppeteer。在本指南中,我们将测试 Python 中的 Selenium 是如何工作的(请注意,它也可用于 JavaScript 和 Node.js)。
如何使用 Selenium 构建网络爬虫
Selenium 主要用于网络测试。它能够像实际浏览器一样工作,这也使其成为网络抓取的最佳选择之一。
由于它也支持 JS,因此使用 Selenium 抓取 JavaScript 呈现的网页应该不是问题。
在本教程中,我们不会探索您可以使用的所有复杂方法。查看我们详尽的Selenium 指南以了解所有相关信息以及更多信息。但是,在这里我们将重点放在其他方面:
让我们尝试从Instacart中刮取Sprouts 的面包。
首先,网站在服务器上渲染一个模板页面;然后,它在客户端由 JavaScript 填充。
这是加载屏幕的样子:
填充 HTML 内容后,我们得到如下内容:
现在我们已经介绍了基础知识,让我们开始使用 Python 上的 Selenium 抓取 JavaScript 呈现的网页!
安装要求
Selenium 用于控制 Web 驱动程序实例。因此,您需要浏览器的驱动程序。为此,我们转到WebDriver Manager,它将自动下载所需的所有内容。数据将CSV
使用 Pandas 模块以某种格式存储。
然后我们必须使用以下命令安装软件包pip
:
pip install webdriver-manager selenium pandas
好吧!最后,我们可以开始抓取了。
我们将从导入必要的模块开始:
import time import pandas as pd from selenium import webdriver from selenium.webdriver import Chrome from selenium.webdriver.chrome.service import Service from selenium.webdriver.common.by import By from webdriver_manager.chrome import ChromeDriverManager
现在,让我们初始化 headless chrome 网络驱动程序:
# start by defining the options options = webdriver.ChromeOptions() options.headless = True # it's more scalable to work in headless mode # normally, selenium waits for all resources to download # we don't need it as the page also populated with the running javascript code. options.page_load_strategy = 'none' # this returns the path web driver downloaded chrome_path = ChromeDriverManager().install() chrome_service = Service(chrome_path) # pass the defined options and service objects to initialize the web driver driver = Chrome(options=options, service=chrome_service) driver.implicitly_wait(5)
之后,我们将连接到该网站:
url = "https://www.instacart.com/store/sprouts/collections/bread?guest=True" driver.get(url) time.sleep(10)
您会注意到我们添加了 10 秒的延迟。这样做是为了让驱动程序完全加载网站。
在我们继续从单个列表中提取数据之前,我们必须找出产品的存储位置。
它们被保存li
为 中的一个元素ul
,而 又是一个div
元素:
我们可以通过按子字符串过滤它们的类来对元素进行排序div
。
接下来,我们必须检查他们的class
属性是否有ItemsGridWithPostAtcRecommendations
文本。您可以为此使用 CSS 选择器:
content = driver.find_element(By.CSS_SELECTOR, "div[class*='ItemsGridWithPostAtcRecommendations'")
*=
在确定特定子字符串是否在属性中时,这将派上用场。li
由于父元素之外没有任何元素ul
,我们将从中提取元素content
:
breads = content.find_elements(By.TAG_NAME, "li")
接下来,我们将从每个元素中分别抓取 JS 生成的数据li
:
让我们从提取产品图像开始。img
您会注意到两件事:中只有一个元素li
,并且图像 URL 在属性中可见srcset
:
我们现在需要处理提取的数据。
经过一番挖掘,您可以看到图像存储在 CloudFront CDN 中。从那里,我们可以提取 URL。
,
通过[注意逗号后的空格]拆分整个元素并处理第一部分。我们将 URL 打断,/
并将以下部分链接在一起Cloudfront
:
def parse_img_url(url): # get the first url url = url.split(', ')[0] # split it by '/' splitted_url = url.split('/') # loop over the elements to find where 'cloudfront' url begins for idx, part in enumerate(splitted_url): if 'cloudfront' in part: # add the HTTP scheme and concatenate the rest of the URL # then return the processed url return 'https://' + '/'.join(splitted_url[idx:]) # as we don't know if that's the only measurement to take, # return None if the cloudfront couldn't be found return None
是时候使用函数提取 URL 了parse_img_url
:
img = element.find_element(By.TAG_NAME, "img").get_attribute("srcset") img = parse_img_url(img) img = parse_img_url(img)
如您所见,只有部分产品具有饮食属性。
是时候使用 CSS 选择器来提取元素span
内部的 s了div
。之后,我们将使用find_elements
Selenium 中的方法。如果它返回 None
,则意味着没有任何span
元素:
# A>B means the B elements where A is the parent element. dietary_attrs = element.find_elements(By.CSS_SELECTOR, "div[class*='DietaryAttributes']>span") # if there aren't any, then 'dietary_attrs' will be None and 'if' block won't work # but if there are any dietary attributes, extract the text from them if dietary_attrs: dietary_attrs = [attr.text for attr in dietary_attrs] else: # set the variable to None if there aren't any dietary attributes found. dietary_attrs = None
继续谈价格……
价格存储在属性中div
带有子字符串的元素中。由于它不是唯一的,我们将使用 CSS 选择器直接获取元素:ItemBCardDefault
class
span
在抓取网页上的价格时检查元素是否已加载总是一个好主意。
一个简单的方法就是find_elements
方法。
它返回一个空列表,这有助于构建数据提取 API:
# get the span elements where the parent is a 'div' element that # has 'ItemBCardDefault' substring in the 'class' attribute price = element.find_elements(By.CSS_SELECTOR, "div[class*='ItemBCardDefault']>span") # extract the price text if we could find the price span if price: price = price[0].text else: price = None
最后,我们将检索产品的名称和尺寸。
名称存储在唯一h2
元素中。至于大小,我们将再次依赖 CSS 选择器来完成这项工作:
现在一切都已经处理好了,我们必须添加以下代码:
name = element.find_element(By.TAG_NAME, "h2").text size = element.find_element(By.CSS_SELECTOR, "div[class*='Size']").text
name = element.find_element(By.TAG_NAME, "h2").text size = element.find_element(By.CSS_SELECTOR, "div[class*='Size']").text
最后,我们可以将所有这些包装在一个extract_data
函数中:
def extract_data(element): img = element.find_element(By.TAG_NAME, "img").get_attribute("srcset") img = parse_img_url(img) # A>B means the B elements where A is the parent element. dietary_attrs = element.find_elements(By.CSS_SELECTOR, "div[class*='DietaryAttributes']>span") # if there aren't any, then 'dietary_attrs' will be None and 'if' block won't work # but if there are any dietary attributes, extract the text from them if dietary_attrs: dietary_attrs = [attr.text for attr in dietary_attrs] else: # set the variable to None if there aren't any dietary attributes found. dietary_attrs = None # get the span elements where the parent is a 'div' element that # has 'ItemBCardDefault' substring in the 'class' attribute price = element.find_elements(By.CSS_SELECTOR, "div[class*='ItemBCardDefault']>span") # extract the price text if we could find the price span if price: price = price[0].text else: price = None name = element.find_element(By.TAG_NAME, "h2").text size = element.find_element(By.CSS_SELECTOR, "div[class*='Size']").text return { "price": price, "name": name, "size": size, "attrs": dietary_attrs, "img": img }
让我们用它来处理li
在 main content 中找到的所有元素div
。可以将结果存储在列表中并使用 Pandas 将它们转换为 DataFrame!
data = [] for bread in breads: extracted_data = extract_data(bread) data.append(extracted_data) df = pd.DataFrame(data) df.to_csv("result.csv", index=False)
你有它!一个从 JavaScript 呈现的网站中提取数据的 Selenium 抓取工具!容易,对吧?
使用 Python 从 JavaScript 呈现的网页中抓取的数据。
您可以在此GitHub 要点中找到指南中使用的完整代码。
使用Selenium的缺点
由于我们正在运行 Web 驱动程序实例,因此很难扩展应用程序。它们中的更多将需要更多资源,这将使生产环境超载。
您还应该记住,与基于请求的解决方案相比,使用网络驱动程序更耗时。因此,通常建议使用此类浏览器自动化工具,即 Selenium,作为最后的手段。
结论
今天您学习了如何从动态加载的网页中抓取 JavaScript 生成的内容。
我们介绍了 JS 呈现的网站是如何工作的。我们使用 Selenium 构建了一个动态数据提取工具。
让我们快速回顾一下:
-
安装 Selenium 和 WebDriver Manager。
-
连接到目标 URL。
-
使用 CSS 选择器或Selenium 支持的其他方法抓取相关数据。
-
将数据保存并导出为 CSV 文件以备后用。
当然,您始终可以构建自己的网络抓取工具。虽然,这个决定会带来很多困难。想想网站使用的众多反机器人保护措施。更不用说抓取几十个产品和/或网站是极其困难和耗时的。