用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标签。可以使用美丽的python软件包提取JS代码。
Web应用程序通常使用不同的身份验证方法保护API端点,因此使用API刮擦JS渲染页面可能具有挑战性。
如果静态内容中存在编码的隐藏数据,则可能无法解码它。在这种情况下,您需要可以渲染JavaScript进行刮擦的软件。
您可以尝试基于浏览器的自动化工具,例如硒,剧作家或Puppeteer。在本指南中,我们将测试Python中的硒是如何工作的(请注意,它也可用于JavaScript和Node.js)。
如何用硒构建网络刮板
硒主要用于网络测试。它像实际的浏览器一样工作的能力也将其作为网络刮擦的最佳选择之一。
由于它也支持JS,因此用Selenium刮擦JavaScript渲染的网页不应该是问题。
在本教程中,我们不会探索您可以使用的所有复杂方法。查看我们彻底的Selenium guide,以了解有关所有内容的所有信息。但是,在这里我们将重点介绍:
让我们尝试craping Sprouts' breads from Instacart。
首先,该网站在服务器上呈现模板页面;然后,它在客户端的一边被JavaScript填充。
这是加载屏幕的样子:
填充HTML内容后,我们得到了类似的东西:
现在我们已经介绍了基础知识,让我们开始使用python上的硒刮擦JavaScript渲染的网页!
安装要求
硒用于控制Web驱动程序实例。因此,您需要浏览器的驱动程序。为此,我们转到WebDriver Manager,它将自动下载所有需要的内容。数据将通过使用PANDAS模块以CSV
格式存储。
然后,我们必须使用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
现在,让我们初始化无头Chrome Web驱动程序:
# 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'")
在确保属性中是否有特定的子字符串时,*=
将派上用场。由于ul
parent之外没有任何li
元素,我们将从content
中提取这些元素:
breads = content.find_elements(By.TAG_NAME, "li")
接下来,我们将分别从每个li
元素中刮擦JS生成的数据:
让我们从提取产品图像开始。您会注意到两件事:li
中只有一个img
元素,并且在srcset
属性中可见图像URL:
我们现在需要处理提取的数据。
进行了一些挖掘后,您可以看到图像存储在CloudFront CDN中。从那里,我们可以提取URL。
将整个元素按,
[记下逗号之后的空间]并处理第一部分。我们用/
打破URL,然后将零件从Cloudfront
One链接在一起:
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
使用parse_img_url
函数提取URL的时间:
img = element.find_element(By.TAG_NAME, "img").get_attribute("srcset")
img = parse_img_url(img)
img = parse_img_url(img)
您可以看到,仅在某些产品上才有饮食属性。
是时候使用CSS选择器在div
元素内部提取span
。之后,我们将在硒中使用find_elements
方法。如果它返回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
继续价格...
价格存储在ItemBCardDefault
substring中的div
元素中。由于不是唯一的一个,我们将通过使用CSS选择器直接获得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
最后,我们可以将所有这些包装在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
}
让我们使用它来处理主要内容div
中发现的所有li
元素。可以将结果存储在列表中,并通过使用Pandas将其转换为数据框!
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渲染的网站中提取数据的硒刮板!容易,对吗?
,如果您仔细遵守此分步教程,则最终结果应该是什么样子:
使用Python从JavaScript渲染的网页上进行的刮擦数据。
您可以在此GitHub gist中找到指南中使用的完整代码。
使用硒的缺点
由于我们正在运行Web驱动程序实例,因此很难扩大应用程序。他们中的更多将需要更多的资源,这将超载生产环境。
您还应该记住,与基于请求的解决方案相比,使用Web驱动程序的 都更加耗时。因此,通常建议使用此类浏览器自动化工具,即硒,作为最后的手段。
结论
今天,您学会了如何从动态加载的网页中刮擦JavaScript生成的内容。
我们介绍了JS渲染的网站的工作方式。我们使用硒来构建动态数据提取工具。
让我们快速回顾一下:
- 安装Selenium和WebDriver Manager。
- 连接到目标URL。
- 使用CSS选择器Or another method Selenium supports刮擦相关数据。
- 将数据保存并导出为CSV文件以供以后使用。
当然,您总是可以构建自己的网络刮板。但是,该决定遇到了许多困难。只需想一想众多的Antibot保护网站所使用即可。更不用说刮擦数十种产品和/或网站非常困难且耗时。
那么,为什么不让专业人士脱掉您的手? ZenRows可让您使用简单的API调用来刮擦数据。它还可以自动处理反机器人测量。 今天免费尝试!
您发现内容有帮助吗?在Twitter,LinkedIn和Facebook上分享单词并在Twitter上共享。
本文最初发表在Zenrows上:How to Scrape JavaScript-Rendered Web Pages with Python