又404,看我手撸网页快照爬虫
目录
日常中经常会碰到收藏的链接隔段时间就 404 打不开的情况,可能是作者或网站自己删除了,也有可能是因为不可抗拒因素被和谐。如果我们在收藏链接的时候直接将网页快照保存下来,那就万事大吉了。
背景
想要保存网页快照,最简单的方法就是 Ctrl+S 另存为网页文件,不过这种方式保存下来会有多个文件,而且网页的样式可能会发生变化。
Chrome插件
在 Chrome 中则有许多的插件,比如 SingleFile 可以将整个网页保存为一个文件,又比如 GoFullPage 可以对整个网页进行滚动截屏保存为图片。
不过这两个插件虽然易用,但网页保存之后却不易进行检索查找,特别是当收藏的内容增多以后;而网页直接保存在本地电脑,也存在丢失风险。
开源项目
WebArchive
既然 Chrome 的插件无法满足需求,那就找找看有没有开源的工具,发现一个神奇的网站 Archive:https://archive.org
Internet Archive is a non-profit library of millions of free books, movies, software, music, websites, and more.
可惜这个网站远在欧洲波兰,对国内访问极不友好,抓取网页效果很差,只能作罢。
ArchiveBox
进一步发现了一个开源工具 ArchiveBox,可以部署在自己服务器,看官方网站介绍也是十分牛逼:
Open source self-hosted web archiving. Takes URLs/browser history/bookmarks/Pocket/Pinboard/etc., saves HTML, JS, PDFs, media, and more…
而且还提供了Demo试用:
然而就在我十分激动地部署完成后,真正使用才发现一些大问题:
- 抓取速度很慢,提交一个链接后需要等几分钟~几十分钟才能把网页保存下来;
- 存储数据很大,一个网页会同时以多种格式保存(html, png, pdf等),而单个网页的 pdf 文件竟然有几十上百MB;
- 很多网页上的图片会以 Lazy Load的形式加载,网页保存后这些图片并没有正常保存下来;
于是又只能作罢。不过不黑不吹,ArchiveBox的文档非常详细,其中就包含了同类产品的对比,列举了现有的一些其他 Archive 项目。
方案
既然现有的轮子不能满足我们的需求,作为程序猿当然是选择自己造一个轮子。这个轮子可以简单拆分为两部分:
- 给一个链接,抓取网页快照,尽可能保留页面原始样式;
- 保存后的快照可以搜索,方便查看阅读;
对于第一个目标,我们可以采用 Selenium + Chrome 来打开并加载网页后进行截图和保存为PDF,由于 Chrome 支持 headless 模式,整个过程可以自动化运行在 Linux 服务器上。
对于第二个目标,我们可以采用开源的 CMS 系统,比如 WordPress,在网页抓取完成后直接通过 API 将快照保存并展示出来。
实现
网页抓取
环境部署
首先准备一下开发环境,Linux 系统为 Ubuntu Server 22.04,Python 版本 3.10, 安装 selenium 库:
$ pip install selenium
下载 Chrome 安装包和对应的 chrome driver:
// chrome driver
$ wget https://chromedriver.storage.googleapis.com/106.0.5249.61/chromedriver_linux64.zip
$ unzip chromedriver_linux64.zip
$ chmod +x chromedriver
// google chrome
$ wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
$ sudo dpkg -i google-chrome-stable_current_amd64.deb
// 安装缺少的依赖
$ sudo apt install -f
// 安装中文字体
$ sudo apt install fonts-wqy-microhei
可以先测试一下:
// 打印网页
$ google-chrome-stable --headless --disable-gpu --dump-dom https://www.baidu.com
// 保存为pdf
$ google-chrome-stable --headless --disable-gpu --print-to-pdf https://www.baidu.com
自动化抓取
首先使用 selenium 初始化 chrome driver,并设置 headless 模式:
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
CHROME_DRIVER = "./chromedriver"
def init_driver():
service = Service(executable_path=CHROME_DRIVER)
options = Options()
options.add_argument("--headless")
driver = webdriver.Chrome(service=service, options=options)
return driver
然后就可以加载网页,并获取页面标题:
def get_title(driver):
title = driver.title
if not title:
og_title = driver.find_element(by=By.CSS_SELECTOR, value='[property="og:title"]')
title = og_title.get_attribute("content")
return title
driver = init_driver()
driver.get("https://www.baidu.com/")
title = get_title(driver)
print(title)
可以截图保存,由于 screenshot
只会截取当前屏幕显示的内容,对于内容很长的页面我们采用一个小技巧:将浏览器窗口尺寸高度调整为页面长度,再进行截图就可以轻松搞定。注意:这种方式只能在 headless 模式下生效!
WIDTH = 700
HEIGHT = 200
def save_as_img(driver, path):
driver.set_window_size(WIDTH, HEIGHT)
height = driver.execute_script("return document.body.parentNode.scrollHeight")
driver.set_window_size(WIDTH, height + HEIGHT)
body = driver.find_element(by=By.TAG_NAME, value="body")
body.screenshot(path)
最后还可以采用 cpd 指令保存为 PDF,注意这里返回的数据是 base64 编码:
def save_as_pdf(driver, path):
pdf = driver.execute_cdp_cmd("Page.printToPDF", {"printBackground": True})
with open(path, "wb") as fw:
fw.write(base64.b64decode(pdf["data"]))
对于微信公众号文章里的图片是 Lazy Load,所以需要特殊处理一下:
def fix_wxmp_images(driver):
if "mp.weixin.qq.com" in driver.current_url:
spans = driver.find_elements(by=By.CLASS_NAME, value="js_img_placeholder")
for span in spans:
driver.execute_script("arguments[0].style.display='none'", span)
images = driver.find_elements(by=By.TAG_NAME, value="img")
conditions = []
for img in images:
data_src = img.get_attribute("data-src")
if data_src:
driver.execute_script("arguments[0].src=arguments[1]", img, data_src)
driver.execute_script("arguments[0].style.display='block'", img)
conditions.append(EC.visibility_of(img))
try:
WebDriverWait(driver, 15).until(EC.all_of(*conditions))
except Exception as e:
pass
快照保存
网页快照保存为图片和PDF后还需要汇总收藏,以便将来搜索查阅。我们直接采用 WordPress 来做这个事情,搭建一个 WordPress 对于大家来说肯定是小菜一碟,所以这里主要讲讲如何通过 API 来将网页快照保存到 wordpress 上。
WordPress 相关的 API 可以参考官方文档, 而调用接口的权限认证原生只支持 cookie,并不是一个很好的方式,因此建议安装插件来扩展认证功能,比如 JWToken:
def get_jwt_token():
data = {
"username": WORDPRESS["username"],
"password": WORDPRESS["password"],
}
r = requests.post(WORDPRESS["host"] + "/jwt-auth/v1/token", data=data)
result = r.json()
return result.get("token")
之后就可以上传文件:
def upload_file(jwt_token, fpath, title):
headers = {
"Authorization": "Bearer %s" % jwt_token,
}
params = {
"title": title,
"slug": os.path.basename(fpath),
}
with open(fpath, "rb") as fobj:
r = requests.post(WORDPRESS["host"] + "/wp/v2/media", params=params, files={"file": fobj}, headers=headers)
result = r.json()
guid = result.get("guid", {}).get("raw")
return guid
上传成功后获取到资源的guid,可以用在创建文章的content中:
def create_post(jwt_token, title, excerpt, content, slug, date=None):
if not date:
date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
params = {
"date": date,
"status": "publish",
"title": title,
"excerpt": excerpt,
"content": content,
"slug": slug,
"format": "standard",
}
headers = {
"Authorization": "Bearer " + jwt_token,
}
r = requests.post(WORDPRESS["host"] + "/wp/v2/posts", params=params, headers=headers)
result = r.json()
post_id = result.get("id")
return post_id
成品
至此,基本的功能就完成了,按照此方案最终实现了一个网页快照收藏工具,感兴趣可以查看一下 https://archive.jiehua.fun:233/ 。
评论