This post was published in 2021-08-30. Obviously, expired content is less useful to users if it has already pasted its expiration date.
Table of Contents
预加载
通过预先生成所有链接对应的缓存文件,可以让Wordpress页面的第一次访问就很快。
或者可以看这篇文章的介绍:🔗 [Preload cache - WP Rocket Knowledge Base] https://docs.wp-rocket.me/article/8-preload-cache
WordPress插件提供的preload功能基本上都是用php实现的,本文的preload代码是用python3写的,所以你需要准备一种你喜欢的缓存方案(不一定要wordpress插件,nginx-FastCGI也可以).
原版代码
是这篇文章的python3实现:
备注:
✪ 这是一个短时间快速生成WordPress全站html缓存的程序,建议开启Mysql或者MariaDB的 query_cache ,能够显著缩短运行时间。
✪ 运行本代码前需要清空缓存文件。
✪ 比较适用于:个人站点(只有你一个人发表文章,所以你知道什么时候可以运行这个程序);不太适用于:有多名文章发表管理员的大型站点。
✪ 不同缓存方案(redis, memcached, mysql query cache)的性能对比:🔗 [Memcached和Redis测试(preload.py) - Truxton's blog] https://truxton2blog.com/preload-py-memcached-redis-benchmark/
✪ 相比Wordpress插件(比如wp-rocket, wp super cache, wp fastest cache...)自带的php preload程序:
优点是:多线程/多进程,可自定义preload内容(防止遗漏),
缺点是:脱离了Wordpress体系,对web hosting平台不友好,暂时无法针对单篇文章进行缓存更新。
✪ 理论上适用于绝大多数基于HTML的缓存插件以及FastCGI,但需要关闭缓存插件自带的preload功能,防止功能重叠。
✪ 需要 $ pip3 install lxml requests .
✪ 需要替换代码里的example.com .
✪ 不同的sitemap插件生成的xml文件名称可能不同,本站使用的是Yoast SEO .
✪ 默认代码在本地运行,能够访问127.0.0.1:443 .
✪ 第70行 ThreadPoolExecutor(max_workers=2) 需要根据cpu核心数进行调整。我个人一般倾向于 调整到cpu利用率正好超过90% .
✪ 我的服务器只有性能极弱的1核cpu,所以我使用了2个线程;如果你有多核cpu,也许你可以尝试一下 ProcessPoolExecutor .
✪ 正则表达式在其他wordpress主题上可能需要做调整,并不一定通用,具体表现如下:
1. 如果你不在widget里添加“monthly archives“,那么你不需要第56~61行判断月份的代码。
2. 如果你发现这个程序无法在你的网站上获取分页,那么你需要修改代码第61行的regex:
pattern_pagination = re.compile(r'(?<=<a class="next page-numbers" href=")[^"]+(?=">)')
,以及第22行的substring判断代码:
if 'link rel="next" href=' in response_body:
,让它和你的网站的分页链接相匹配。
from concurrent.futures import ThreadPoolExecutor
import time
import requests
from lxml import etree
import re
from functools import partial
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
def download(pattern_pagination, pattern_embed, url):
print(url)
url = url.replace('example.com', '127.0.0.1:443')
headers = {'Host': 'example.com'}
response = requests.get(url, headers=headers, verify=False)
response_body = response.text
# pattern_pagination = re.compile(r'(?<=<link rel="next" href=")[^"]+(?=" \/>)') # 查找分页
# python in 比 regex 更快
if 'link rel="next" href=' in response_body:
results_pagination = pattern_pagination.findall(response_body)
if len(results_pagination) != 0:
download(pattern_pagination=pattern_pagination, pattern_embed=pattern_embed, url=results_pagination[0])
# pattern_embed = re.compile(r'(?<=data-src=")[^"]+example[^"]+\/embed\/') # 查找嵌入页面
# python in 比 regex 更快
if '/embed/' in response_body:
results_embed = pattern_embed.findall(response_body)
for val in results_embed:
download(pattern_pagination=pattern_pagination, pattern_embed=pattern_embed, url=val)
return 1
def get_sitemap(url):
xmlList = []
r = requests.get(url)
root = etree.fromstring(r.content)
for sitemap in root:
children = sitemap.getchildren()
xmlList.append(children[0].text)
return xmlList
if __name__ == '__main__':
start = time.time()
urlList = []
urlList.extend(get_sitemap("https://example.com/post-sitemap.xml"))
urlList.extend(get_sitemap("https://example.com/page-sitemap.xml"))
urlList.extend(get_sitemap("https://example.com/category-sitemap.xml"))
urlList.extend(get_sitemap("https://example.com/post_tag-sitemap.xml"))
# add month archive
response_body = requests.get("https://127.0.0.1:443", headers={'Host': 'example.com'}, verify=False).text
pattern_month_archive = re.compile(
r'(?<=href=[\'\"])https:\/\/example.com\/\d+\/\d+\/(?=[\'\"])') # 查找月份archives
results_month_archive = pattern_month_archive.findall(response_body)
for val in results_month_archive:
urlList.append(val)
# 去重
urlList = list(dict.fromkeys(urlList))
# 先编译好
pattern_pagination = re.compile(r'(?<=<a class="next page-numbers" href=")[^"]+(?=">)') # 查找分页
pattern_embed = re.compile(r'(?<=data-src=")[^"]+example[^"]+\/embed\/') # 查找embed页面
pool = ThreadPoolExecutor(max_workers=2)
dl_func = partial(download, pattern_pagination, pattern_embed)
results = list(pool.map(dl_func, urlList))
end = time.time()
print(time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(start)))
print(time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(end)))
print('time in seconds:{:.3f}'.format(end - start))
进一步优化
上面的代码还有很多优化余地;但是优化的越多,代码就越不具有通用性,所以接下来仅仅叙述一些优化思路:
2023年4月12日紧急更新:我对pycurl和requests的性能对比还是没有定数,目前的结论是“pycurl的总体性能和requests几乎完全一样”,测试结果:🔗 [preload.py使用pycurl和requests的性能对比 - Truxton's blog] https://truxton2blog.com/compare-pycurl-requests-in-preload-py/
可能存在争议的内容:
✪ 用 PycURL 代替 requests :能够大幅度提升本程序的性能。在我的服务器上,运行 requests 的代码,(preload.py)cpu平均占用约15%;换成 PycURL 以后,cpu平均占用只有1.3%左右了. 但是一定要注意PycURL使用的ssl库:如果编译PycURL时链接的是 gnutls 而不是 openssl/nss ,则会大幅度损失性能,导致PycURL的cpu占用率达到15%左右(和requests没什么区别了)。
为了避免反复尝试不同编译参数带来的麻烦,推荐使用conda安装PycURL:
$ conda install -c anaconda pycurl
也可以自行编译libcurl --with-openssl:🔗 [PycURL的性能取决于安装方法和编译依赖 - Truxton's blog] https://truxton2blog.com/pycurl-performance-differs/
✪ 依次删除缓存文件:通常情况下的逻辑是“先删除所有缓存文件,然后再运行preload.py”,但这样可能会带来一个潜在的问题:执行preload.py程序的时候cpu占用非常高,如果恰好访问了一个缓存文件丢失的链接,那么这个网页请求就会由php处理,速度可能会慢好几倍。
如果你使用的是类似FastCGI这样的缓存系统,由于FastCGI的路径是可以计算的(绝大多数Wordpress缓存插件的缓存路径也可以计算,有些基于MD5有些基于SHA256...可以查看源代码),所以你可以修改代码,让程序的执行逻辑变为“删除一个页面的缓存文件,通过preload生成新的缓存文件,然后删除下一个页面的缓存文件...”。这样做的好处是:preload程序执行期间访问网站的速度不会大幅度降低。
FastCGI缓存的计算方式可以参考:【半成品】NGINX FastCGI purge and preload for single URL
✪ 如果你开启了 query_cache ,建议在preload.py程序开头部分执行 RESET QUERY CACHE ,清理query cache缓存碎片,进一步加快preload.py的执行速度(前提是你需要给query cache分配足够的内存空间)。以本站点为例,生成300个html页面需要大约30MB的mariadb query cache空间。(2022-11-31)