• 隐藏侧边栏
  • 展开分类目录
  • 关注微信公众号
  • 我的GitHub
  • QQ:1753970025
Chen Jiehua

又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/ 。

码字很辛苦,转载请注明来自ChenJiehua《又404,看我手撸网页快照爬虫》

评论