文链接:Optimize resource loading, from web.dev。翻译时有删改。
在上一篇文章中,我们讨论了关键渲染路径,了解了影响页面初始渲染效率的阻塞渲染 CSS 和 阻塞解析 JavaScript。
页面加载时,会伴随很多资源的引用。这些资源可能是为页面提供外观和布局的 CSS,也可能是提交页面交互效果的 JavaScript。
本文,我们将深入了解这 2 类资源的阻塞原理并具体学习一些优化手段。
从之前的学习,我们知道 CSS 属于阻塞渲染的资源,在浏览器完成下载和解析 CSS 成为 CSSOM 之前,会停止渲染工作。
之所以将 CSS 作为阻塞渲染的资源,是为了避免页面出现短暂的无样式闪现。类似下面这样:
有一个术语专门用于描述这个场景,叫 FOUC,全称是“Flash of Unstyled Content”。
因为 CSS 会作为阻塞渲染的资源处理,FOUC 现象你通常是看不到的,但要理解这个概念,就能明白浏览器为什么要这么做了。
从之前的学习,我们知道浏览器在遇到 <script> 元素时,会阻塞浏览器进一步的解析 ,优先下载、处理和执行 JavaScript,在处于其余部分的 HTML。
JavaScript 的阻塞解析,也是浏览器刻意为之的策略。这是因为 JavaScript 代码中可能会包含对 DOM 结构的访问和修改。
html复制代码<!-- This is a parser-blocking script: -->
<script src="/script.js"></script>
当 <script> 元素没有注明是 async/defer 的情况下,浏览器会先终止后续解析,优先下载(可选,适应于引用外部资源)、解析、执行 JavaScript 文件。直到这一过程结束,浏览器才继续后续内容的解析。
值得注意的是,JavaScript 的执行也有一个前提,就是当前没有正在处理的 CSS 资源,这也是刻意为之。
因为 CSS 解析时,并不会阻止浏览器解析 JavaScript,如果 JavaScript 中有类似 element.getComputedStyle() 代码调用,那么必然要等到样式解析完成才行,否则是不准确的。因此,JavaScript 的执行要等待 CSS 解析彻底完成。
预加载扫描器(preload scanner) 是浏览器的一个优化手段,它是主 HTML 解析器(primary HTML parser)之外的另一个 HTML 解析器,叫辅助 HTML 解析器(secondary HTML parser)。
预加载扫描器会在主 HTML 解析器发现资源之前查找并获取资源。比如:提前下载 <img> 元素中指定的资源。这个操作即便在 HTML 解析器被 JavaScript 和 CSS 阻塞也是如此。
不过预加载扫描器也有一些处理盲区。盲区之内引用的资源无法被识别,也就无法优化了。这些处理盲区包括:
以上场景都有一个共同特点,就是资源加载都是滞后的,自然就无法被预加载扫描器知道。
当然,针对这类场景我们还能使用 preload hint 手动提示来支持。不过这是下一篇的内容了,这里先不赘述。
CSS 影响页面的外观和布局效果,它是一种阻塞渲染的资源。本小节,我们就来看看如果优化 CSS,来改善页面加时间。
通过压缩 CSS 减少文件大小,从而加快下载速度。
demo:压缩前
css复制代码/* Unminified CSS: */
/* Heading 1 */
h1 {
font-size: 2em;
color: #000000;
}
/* Heading 2 */
h2 {
font-size: 1.5em;
color: #000000;
}
demo:压缩后
css复制代码/* Minified CSS: */
h1,h2{color:#000}h1{font-size:2em}h2{font-size:1.5em}
压缩是 CSS 最基本而且有效的一个优化手段,可以提高网站 FCP 甚至 LCP 指标。一般前端工程中是配置的打包工具(bundler)都内置了这个功能。
在浏览器渲染网页内容前,需要下载并解析所有样式。当然,这里花费的时间还包括当前页面未使用的样式。如果你使用的打包工具将所有 CSS 资源组合到一个文件中,那么你的用户可能会下载比实际当前渲染页面所需要的更多 CSS。
要发现当前页面未使用的 CSS,可以使用 Chrome DevTools 中的 Coverage 工具。
删除未使用的 CSS 会带来 2 个好处:
虽然看起来很方便,但您应该避免在 CSS 中使用 @import 声明:
css复制代码/* Don't do this: */
@import url('style.css');
与 HTML 中 <link> 元素的工作方式类似,CSS 中的 @import 声明能让你从样式表中导入外部 CSS 资源。
这两种方法之间的主要区别在于 HTML <link> 元素是 HTML 响应的一部分,因此比通过 @import 声明下载的 CSS 文件能更快比发现。
原因在于 @import 声明必须先下载包含 CSS 文件。这会产生所谓的请求链(request chain),它会延迟页面初始渲染所需的时间。另一个缺点,是使用 @import 声明加载的样式表无法被预加载扫描器发现,也会成为后期发现的渲染阻塞资源。
html复制代码<!-- Do this instead: -->
<link rel="stylesheet" href="style.css">
在大多数情况下,你可以使用 <link rel="stylesheet"> 元素替换 @import。<link> 元素是同时下载样式表的,这样能减少总体加载时间,这与 @import 声明连续下载样式表的策略也是不一样的。
所谓“关键 CSS”是指“首屏”页面内容所需要的样式。
下载 CSS 文件需要时间,这会增加页面的 FCP 指标。如果在文档 <head> 中内联关键样式可以消除对 CSS 资源的网络请求。剩余的 CSS 可以异步加载,或者附加在 <body> 元素的末尾。
html复制代码<head>
<title>Page Title</title>
<!-- ... -->
<style>h1,h2{color:#000}h1{font-size:2em}h2{font-size:1.5em}</style>
</head>
<body>
<!-- Other page markup... -->
<link rel="stylesheet" href="non-critical.css">
</body>
另一方面,内联大量 CSS 会向初始 HTML 响应增加更多字节。而通过 HTML 资源通常无法缓存很长时间(或根本不缓存),因此这个也需要我们基于自身情况做选择。
JavaScript 驱动了网络上的大部分交互,但也付出了代价。
传送太多的 JavaScript 可能会让页面加载时间太长、响应缓慢,交互也变慢了,这两种情况都会让用户抓狂。
当网页中使用的 <script> 元素不带 defer 或 async 时,当前代码的解析会阻止浏览器后续工作的进行,直到当前代码处理完成。同样的,内联脚本也会阻塞解析,直到脚本代码处理完成。
<script> 的 async 和 defer attribute 可以让你在加载外部脚本同时不阻塞 HTML 解析器的工作。不过,async 和 defer 策略上还是有一些不同。
来源自 html.spec.whatwg.org/multipage/s…
使用 async 加载的脚本在下载后立即解析并执行;而使用 defer 加载的脚本在 HTML 文档解析完成时执行——跟 DOMContentLoaded 事件一个时机。
另外,当网页中同时存在多个脚本时。 async 脚本可能无法保证执行执行,而 defer 脚本则会始终按照它们在源代码中出现的顺序执行。
值得注意的是:带有 type="module" 的脚本(包括内联脚本)效果跟 defer 一样;而通过脚本动态注入的 <script> 标签效果类似 async。
一般来说,你应该避免使用 JavaScript 来呈现任何关键内容或页面的 LCP 元素。这种做法称为“客户端渲染”,是单页应用程序 (SPA) 中广泛使用的一种技术。
通过 JavaScript 渲染的 HTML 标签是无法被预加载扫描器观察到的。这可能会延迟关键资源的下载,例如 LCP 图片。浏览器只会在脚本执行后才开始下载 LCP 图片,并添加到 DOM 中,这应该被避免。
此外,与直接在服务器响应请求返回相比,使用 JavaScript 渲染标签更有可能产生较长的任务。广泛使用 HTML 客户端渲染也会延迟交互,在页面 DOM 非常大的情况下尤其如此。
与 CSS 类似,压缩 JavaScript 可以减少脚本文件大小,加快下载速度,让浏览器更快解析和编译 JavaScript。
此外,JavaScript 的压缩可以比 CSS 等其他资源更好。当压缩 JavaScript 时,它不仅能删除空格、制表符和注释等内容,而且 JavaScript 中标识符也能被缩短,这个过程有时被称为丑化(uglification)。
js复制代码// Unuglified JavaScript source code:
export function injectScript () {
const scriptElement = document.createElement('script');
scriptElement.src = '/js/scripts.js';
scriptElement.type = 'module';
document.body.appendChild(scriptElement);
}
js复制代码// Uglified JavaScript production code:
export function injectScript(){const t=document.createElement("script");t.src="/js/scripts.js",t.type="module",document.body.appendChild(t)}
观察可以发现,你可以看到源代码中变量 scriptElement 被缩短成 t 了。当你脚本代码很多时,这种做法可以节省的体积相当可观,而且也不会影响网站的生产环境功能。
如果你有在用打包工具处理网站源码,JavaScript 的生产压缩默认就有。这类丑化工具——例如 Terser——也是高度可配置的,你可以调整丑化算法的激进程度来实现最大程度的压缩。然而,任何丑化工具的默认设置通常就足够使用的了。
本文在上一篇关键渲染路径的基础上,进一步讨论了浏览器针对 JavaScript/CSS 采用的不同阻塞策略的原因,继而给出优化 JavaScript、CSS 的不同手段。希望对各位正在阅读的朋友日后的工作带来一些帮助。
下一篇里,我们会继续探讨另一种在网页中进行手工优化的方式——资源提示(resource hints),敬请期待。
再见。
网络爬虫开发中,使用强大的库是至关重要的,而requests-html就是其中一颗璀璨的明星。本文将深度探讨requests-html的各个方面,包括基本的HTTP请求、HTML解析、JavaScript渲染、选择器的使用以及高级特性的应用。
首先,需要安装requests-html:
pip install requests-html
然后,进行简单的HTTP请求:
from requests_html import HTMLSession
session = HTMLSession()
response = session.get('https://example.com')
print(response.html.text)
requests-html内置了强大的HTML解析器和类似jQuery的选择器,使得数据提取变得非常便捷:
# 使用选择器提取标题
titles = response.html.find('h2')
for title in titles:
print(title.text)
对于需要JavaScript渲染的页面,requests-html也能轻松应对:
# JavaScript渲染
r = session.get('https://example.com', params={'q': 'python'})
r.html.render()
print(r.html.text)
1 异步JavaScript渲染
对于异步加载的JavaScript内容,requests-html提供了pyppeteer的支持:
# 异步JavaScript渲染
r = session.get('https://example.com')
r.html.render(sleep=1, keep_page=True)
print(r.html.text)
2 自定义Headers和Cookies
在请求中自定义Headers和Cookies是常见需求,requests-html为此提供了简单易用的方法:
# 自定义Headers和Cookies
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'}
cookies = {'example_cookie': 'value'}
r = session.get('https://example.com', headers=headers, cookies=cookies)
print(r.html.text)
1 抓取动态页面
通过requests-html,可以轻松抓取动态页面的数据:
# 抓取动态页面
r = session.get('https://example.com/dynamic-page')
r.html.render()
print(r.html.text)
2 表单提交
模拟用户行为,实现表单提交:
# 表单提交
payload = {'username': 'user', 'password': 'pass'}
r = session.post('https://example.com/login', data=payload)
print(r.html.text)
requests-html内置了类似于jQuery的选择器,让数据提取变得轻松:
# 使用选择器提取链接
links = response.html.find('a')
for link in links:
print(link.attrs['href'])
此外,通过更复杂的选择器和过滤器,可以更精准地定位和提取所需数据:
# 使用更复杂的选择器和过滤器
articles = response.html.find('article')
for article in articles:
title = article.find('h2', first=True).text
author = article.find('.author', first=True).text
print(f"Title: {title}, Author: {author}")
对于需要等待页面加载完成的情况,requests-html提供了wait参数:
# 等待页面加载完成
r = session.get('https://example.com/dynamic-content')
r.html.render(wait=2)
print(r.html.text)
此外,还可以利用render函数生成页面截图:
# 生成页面截图
r = session.get('https://example.com')
r.html.render(screenshot='screenshot.png')
在爬虫过程中,异常处理是不可或缺的一部分。requests-html提供了捕获异常和错误页面重试的选项:
# 异常处理和错误页面重试
try:
r = session.get('https://example.com/unstable-page')
r.html.render(retries=3, wait=2)
print(r.html.text)
except Exception as e:
print(f"Error: {e}")
在爬虫开发中,性能优化和并发请求是至关重要的。requests-html提供了一些功能和选项,能够更好地处理这些方面的问题。
1. 并发请求
并发请求是同时向多个目标发送请求,以提高效率。requests-html使用asyncio库支持异步请求,从而实现并发。以下是一个简单的例子:
from requests_html import AsyncHTMLSession
async def fetch(url):
async with AsyncHTMLSession() as session:
response = await session.get(url)
return response.html.text
urls = ['https://example.com/page1', 'https://example.com/page2', 'https://example.com/page3']
# 利用asyncio.gather实现并发请求
results = AsyncHTMLSession().run(lambda: [fetch(url) for url in urls])
for result in results:
print(result)
在这个例子中,asyncio.gather被用于同时运行多个异步请求。这种方式在大量页面需要抓取时可以显著提高效率。
2. 链接池
requests-html的Session对象内置了连接池,它能够维护多个持久化连接,减少请求时的连接建立开销。这对于频繁请求同一域名下的多个页面时尤为有用。以下是一个简单的使用示例:
from requests_html import HTMLSession
session = HTMLSession()
# 利用连接池发送多个请求
responses = session.get(['https://example.com/page1', 'https://example.com/page2', 'https://example.com/page3'])
for response in responses:
print(response.html.text)
这里,session.get()接受一个包含多个URL的列表,使用连接池维护这些请求的连接。
3. 缓存
requests-html允许使用缓存,以避免重复下载相同的内容。这对于频繁访问不经常更新的网页时很有用。以下是一个使用缓存的例子:
from requests_html import HTMLSession
session = HTMLSession()
# 使用缓存
response = session.get('https://example.com', cached=True)
print(response.html.text)
在这个例子中,cached=True表示启用缓存。
在本篇博客中,深入探讨了requests-html这一Python爬虫库,揭示了其强大而灵活的功能。通过详细的示例代码和实际应用场景,展示了如何使用该库进行HTTP请求、HTML解析、JavaScript渲染以及高级功能的应用。requests-html的异步支持使得并发请求变得轻而易举,通过连接池和缓存的利用,我们能够更好地优化性能,提高爬虫的效率。同时,库内置的强大选择器和灵活的数据提取方式让页面解析变得更为简单。
总体而言,requests-html为爬虫开发者提供了一个强大而友好的工具,使得从静态网页到动态渲染页面的抓取都变得更加便捷。通过学习本文,不仅能够熟练掌握requests-html的基本用法,还能深入理解其高级功能,为实际项目的开发提供更全面的解决方案。希望通过这篇博客,能够更加自信和高效地运用requests-html来应对各类爬虫任务。
、BeautifulSoup简介
BeautifulSoup是Python爬虫应用解析Html的利器,是Python三方模块bs4中提供的进行HTML解析的类,可以认为是一个HTML解析工具箱,对HTML报文中的标签具有比较好的容错识别功能。lxml是一款html文本解析器,BeautifulSoup构建对象时需要指定HTML解析器,推荐使用lxml。
BeautifulSoup和lxml安装命令:
1.pip install -i https://pypi.tuna.tsinghua.edu.cn/simple bs4
2.pip install -i https://pypi.tuna.tsinghua.edu.cn/simple lxml
加载BeautifulSoup:
1.from bs4 import BeautifulSoup
BeatifulSoap解析HTML报文的常用功能:
通过标签的contents属性,可以访问其下嵌套的所有下级HTML元素,这些该标签下的子标签对应的HTML元素放到一个contents 指向的列表中。
如:print(soup.body.contents)
可以访问标签对应的父、子、兄弟及祖先标签信息;
使用strings属性迭代访问除标签外的所有内容;
可以使用find、find_all、find_parent、find_parents等系列方法查找满足特定条件的标签;
使用select通过css选择器定位特定标签。
二、一些解析技巧
在HTML解析时,如果通过简单的tag、或单个tag属性(如id、class)或文本一次搜索或select定位是最简单的,而有些情况需要使用组合方法才能处理。
2.1、通过标签的多个属性组合定位或查找
经常有些要定位的标签有很多,按单个属性查找也有很多,得使用多个属性查找。如:
上面的html文本中有多个id为article_content的div标签,如果使用:
就会返回两条记录。这时候就可以使用多标签属性定位的如下4种语句:
以上四种方式是等价的,因为id可以用#来标记,class在查找时需要和Python关键字class区分,因此有上述不同方法,注意select的每个属性必须用中括号括起来,不同属性的中括号之间不能有空格,如果有空格表示的就不是查找同一标签的属性,空格后的属性表示前一个属性对应标签的子孙标签的属性。
2.2、利用tag标签关系定位内容
tag标签关系包括父子、兄弟、祖先等关系,有时要查找或定位的内容本身不是很好定位,但结合其他标签关系(主要是父子、祖先关系)则可以唯一确认。
案例:
这是博文中关于博主个人信息的部分报文:
以上报文中,如果要取博主的原创文章数和周排名,原创文章数和博主周排名的tag标签完全相同,二者都在span标签内,标签的属性及值都相同,只是span标签的父标签dt标签的兄弟标签dd标签的string的中文内容才能区分。对于这种情况,首先要通过祖先标签<div class="data-info d-flex item-tiling">定位到祖先标签,再在祖先标签内通过中文字符串定位到要访问属性的兄弟标签的子标签,然后通过该子标签找到其父标签的父标签,再通过该父标签的dt子标签的span子标签访问具体取值。
示例代码如下:
注意:上面的select使用的也是标签的属性来定位标签,并且两个中括号之间有空格,表明后一个要查找的标签在前一个属性对应标签的子孙标签范围内。
2.3、分析前去除程序代码避免干扰
在解析HTML报文时,绝大多数情况是需要分析有用的标签信息,但作为技术文章,大部分的博文中都有代码,这些代码可能会对分析进行干扰。如本文中的代码含有一些分析的HTML报文,如果获取本文的完整HTML内容,这些报文在非代码部分也会出现,此时要排除代码的影响,可以将代码先从分析内容中去除再来分析。
目前大多数技术平台的博文编辑器都支持对代码的标识,象markdown等编辑器代码的标签为code标检,如果有其他编辑器用不同标签的,只有确认了标签名,都可以按下面介绍的类似方式来处理。
处理步骤如下:
获取报文;
构建BeatifulSoap对象soup;
通过soup.code.extract()或soup.code.decompose()方式就从soup对象中去除了代码部分,decompose方法与extract方法的区别就是decompose直接删除对应对象数据而extract再删除时将删除对象单独返回。
三、小结
本文介绍了使用BeatifulSoap解析HTML报文的三个使用技巧,包括通过多属性组合查找或定位标签、通过结合多个标签关系来定位标签以及去除html报文中的代码标签来避免代码对解析的影响。
写字不易,敬请支持:
如果阅读本文于您有所获,敬请点赞、评论、收藏,谢谢大家的支持!
————————————————
版权声明:本文为转载文章,如有侵权,请联系作者删除。
*请认真填写需求信息,我们会在24小时内与您取得联系。