12

如何生成一本书(web2pdf)

 3 years ago
source link: http://saitjr.com/others/web2pdf.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

个人看书有些习惯,非技术书在 Kindle 上看,技术书在电脑或 iPad 上以 pdf 格式看。但经常看到一些写得好的博客,这对我来说很不友好。自制力太弱了,让我打开一个网页,五分钟以后就能看到已经打开二十个了…对于 pdf 来说,结合 MarginNotes 学习是非常不错的选择,那么就动手把 web 转成 pdf 吧。

环境信息

macOS Catalina

Chrome 78.0.3904.108

puppeteer 3.1.0

node 12.16.1

全文操作基于 Chrome

常见的有以下几种场景,按自定义程度不同排序:

  1. 页面较少,并且整个网页直接下载(网页无登录)
  2. 需要剔除一些网页元素(比如广告,不需要的控件等)
  3. 页面多,需要自动爬取目录自动获取链接
  4. 有登录操作
  5. 完全自定义页面样式(比如作者样式花哨,想用 Gitbook 样式等)

除此之外,还有两个可选操作:

  1. 自动生成目录
  2. 合并多个页面到一个 pdf

直接下载

最近在学习 macOS 自动化,刚好看到一个不错的网站( The macOS Automation Sites ),其中 AppleScript 的入门文章挺有意思。所以后面大多数案例会以这个网站为例。

除此之外,再忍不住安利一下(不能白嫖)之前下载的灯塔大佬的 Go 语言设计与实现 ,真是吾辈楷模。

整个页面内容如下图,只有绿框的正文内容是必要的,目录、相关阅读、导航栏等信息都可以删除(目录可以用来获取余下文章的链接)。

VVJ3UrY.jpg!web

先从最基础的直接下载开始。

打印

最简单的方式,直接用 Chrome CMD+P 打印页面,Destination 为 Save as PDF 即可。

Headless Chrome

页面多的情况下,不可能一个一个点,最简单的自动化方式是挑选一门熟悉的脚本语言,然后执行下载命令。那么,问题是 Save as PDF 对应的命令是什么?

Headless Chrome is shipping in Chrome 59. It’s a way to run the Chrome browser in a headless environment. Essentially, running Chrome without chrome! It brings all modern web platform features provided by Chromium and the Blink rendering engine to the command line.

A headless browser is a great tool for automated testing and server environments where you don’t need a visible UI shell. For example, you may want to run some tests against a real web page, create a PDF of it, or just inspect how the browser renders an URL.

通俗的说,Chrome 59 以后发布了 Headless Chrome,Headless 即没有 GUI 交互页面,这对测试、爬虫等场景非常友好,省去了诸多不必要的渲染。在此之前,大多是基于 PhantomJS 的,现在官方发布了 Headless Chrome 无疑是福音(而且 PhantomJS 是基于旧版本的 WebKit)。

官网 的第二个案例,就是 Create a PDF

对于 AppleScript 教程的下载,最简单的方式:

#!/usr/bin/env ruby
# encoding: utf-8

# 手动复制每个链接,或者自动解析出来的链接
list = [
  ""https://macosxautomation.com/applescript/firsttutorial/index.html,
  "https://macosxautomation.com/applescript/firsttutorial/01.html",
  "https://macosxautomation.com/applescript/firsttutorial/02.html"
]

save_dir = File.expand_path("~/Desktop/pdf/")
list.each_with_index do |url, index|
  # Chrome 地址,也可以像文档中那样取一个别名
  # alias chrome="/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome"
  chrome = '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome'
  save_path = File.join(save_dir, "#{index}.pdf")
  # 执行脚本
  system "#{chrome} --headless --disable-gpu --print-to-pdf=#{save_path} #{url}"
end

Tips: 如果不经常导出 pdf,直接在 Chrome Console 手动执行 JQuery 获取目录链接也是可以的(比手动一个个复制快,也比临时写脚本解析页面快):

// 注入 JQ
var script = document.createElement('script');
script.src = "https://ajax.googleapis.com/ajax/libs/jquery/1.6.3/jquery.min.js";
document.getElementsByTagName('head')[0].appendChild(script);
// 根据目录所在节点,输出所有链接
var list = $(".navbox a"); list.each(i => {
  console.log(list[i].href)
})

需要修改页面元素(Puppeteer)

从之前导出的 pdf 看,有几个问题:

  1. 有很多不必要的元素(导航栏、相关阅读)
  2. 正文只占到页面的 3/4,还有 1/4 由于导航栏占用,即使隐藏也会出现留白
  3. 有页眉页脚

这些需求需要对 headless Chrome 做更定制操作。对此,Chrome 团队推出了 Puppeteer(Node 库),可以更精细操作 headless Chrome。

Puppeteer is a Node library developed by the Chrome team. It provides a high-level API to control headless (or full) Chrome.

Puppeteer 直接下载

再回到上一个例子,如果用 Puppeteer 来实现:

const puppeteer = require('puppeteer')
const gen = module.exports = {}
gen.fromURL = async (url, path) => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto(url, { waitUntil: 'networkidle2' });
  await page.pdf({path, format: 'A4'});
  await browser.close();
}

// 调用
const path = require('path');
const gen = require('./gen');
(async () => {
  const url = 'https://macosxautomation.com/applescript/firsttutorial/index.html'
  const filePath = path.join(process.env.HOME, '/Desktop/pdf/as-01.pdf')
  await gen.fromURL(url, filePath)
})();

page.pdf 方法需要传入存储路径,以及一些 OptionsPDFOptions 对象),这些 Options 可以在 Chrome Print 的时候看到。比如:

format: 'A4'
margin: {x, x, x, x}
printBackground: true/false
displayHeaderFooter: true/false

除了在 Print 能看到的,还有 Print 没有的:

headerTemplate/footerTemplate
width/height

修改样式并下载

按之前的想法,页面导航、目录这些都是多余的。通过 Inspect 可以找到这些元素的选择器,而且我们需要修改内容的宽度,剔除多余的留白,css 如下:

#header, #navpath, .note, #sidebar, .more, #globalnav, #globalsearch {
  display: none;
}
#content {
  width: auto;
}

注入 css,这需要用到 pageaddStyleTag(option: string) 方法。 option 可以是 urlpathcontent ,分别对应着 css 链接,本地 css 文件和 css 内容。考虑到可读性,我选择了本地文件,一个网站对应一个样式:

gen.fromCustomStyle = async (url, path, styleFilePath) => {
  // 新增 addStyleTag 即可
  await page.addStyleTag({path: styleFilePath})
}

// 调用
(async() => {
  const style = path.join(__dirname, '../style/applescript.css')
  await gen.fromCustomStyle(url, filePath, style)
})();

最后生成的效果很好,没有多余的元素:

En6zqa3.jpg!web

到此,已经覆盖了绝大多数博客导出为 pdf 的场景。所以先来收个尾:如何将多个页面的 pdf 合并成一个。

页面合并

macOS 的 Finder 已经提供了 Quick Action,选中多个 pdf 右键就可以(Quick Action 好神奇,自己能写吗?当然可以,Automator 大法好)。

fyui6bB.jpg!web

有登录操作

绝大多数的登录问题都可以通过传入本地 Cookie 搞定,在 puppeteer 中也有 page.setCookie 的 API:

gen.fromCustomCookie = async (url, path, cookie) => {
  // 新增 setCookie 即可
  await page.setCookie(cookie)
}

// 调用
(async() => {
  const cookie = { name: 'xxx', value: 'xxxx', url }
  await gen.fromCustomCookie(url, filePath, cookie)
})();

完全自定义页面样式

这种常见于:

page.goto

第二种情况在付费网站中很常见(博客大多都是静态站),拿极客时间举例,文章是通过 https://xxx/serv/v1/article 接口返回的,response 是:

{
  "data": {
    "neighbors": {
      "left": {
        "article_title": "加餐 | 一个前端工程师到底需要掌握哪些技能?",
      }
    },
    "article_content": "<p>你好,我是winter。<\/p><p>最初我答应“极客时间”的时候,其实心里想的是:反正我要做程序员教育,做一个专栏就当整理自己的知识也好。<\/p><p>你可以在各种文档和标准中找到它们或者它们的变体。有一些工程领域相关的知识,来自我工作中的实践,有一些也算是首创,但是我不认为这些知识属于我,我只是发现了它们。<\/p><!-- [[[read_end]]] --><p>所以我认为,知识是免费的,承载它们的教育产品才是收费的。<\/p>"
  }
}

最为关键的 title 和 content 都在接口中返回了。距离生成 pdf 还差一个模板,决定用 ejs (顺手温习了一下 erb 和 smarty,又想起了看 Workpress 源码的那段日子)。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK