保存极客时间文章到本地
背景需求
自己极客时间买的专栏。很多文章很好,也很有深度,想要反复阅读。所以想打印成纸质版,阅读更方便。
实践过程
目标就是将网页正文内容保存到PDF
想到的方法:
- 后端爬虫 直接放弃,话说这么有价值的东西。要是爬虫能简单就爬到数据,还做什么计算机技术知识内容?
- 用chrome保存到PDF 简单粗暴,成功率高。就是内容多了费事,所以上按键精灵。
- 前端JS爬虫 有跨域、登录验证等其他问题,复杂度过高
- 保存文章到本地 极客空间的网页内容一看就是markdown生成的,所以再反转回去存入markdown文件。相当于拿到原文件就可以随便整了
网页直接保存到PDF文件
chrome有好多插件可以将网页保存到PDF
- 捕捉网页截图 - FireShot —— 比较麻烦,不能简单的保存正文
- Print Friendly & PDF —— 很方便,会自动过滤掉无用的内容,而且也很方便删除
- 先将单独文章用
Print Friendly & PDF
配合按键精灵
单独保存到pdf中 - 用
Adobe Acrobat X Pro
将单独文件合并成一个单独的PDF文件,不错还带书签
遇到问题
1.文件是按照名字排序的,但实际文章不是按名字顺序的,所以合并后的文章顺不对。。。
解:python文章按时间排序,(保存的时候是按文章顺序排序保存的)
# dirPath 目录路径,只处理目录下的文件
# sort 0 默认按名称排序;1,按时间正序;2,按时间逆序
def getSortFile(dirPath,sort=0):
fileList = os.listdir(dirPath)
if sort != 0:
r = False if sort == 1 else True
fileList = sorted(fileList, key=lambda x: os.path.getmtime(
os.path.join(dirPath, x)), reverse=r)
return fileList
给文章名加上编号,并删除不要的字符,使其按名字排序后为文章的发表顺序
def rename(path, pattern, replace, iExt=True):
"将path路径下的文件按rxeg正则表达式重命名,iExt是否忽略扩展名"
pRe = re.compile(pattern)
for root, _, files in os.walk(path):
# print("root:{0},dirs:{1}".format(root,dirs))
for file in files:
newFile = ""
if iExt:
nameInfo = os.path.splitext(file)
newFile = pRe.sub(replace, nameInfo[0])+nameInfo[1]
else:
newFile = pRe.sub(replace, file)
newPath = os.path.join(root, newFile)
oldPath = os.path.join(root, file)
print("{0} rename to {1}".format(oldPath, newPath))
os.rename(oldPath, newPath)
2.PDF里有一些Print Friendly & PDF
自己添加的东西,而且字体变成了黑体,字间距也不合适
解:无解
要修改PDF简直不可能,搜半天没一个点好办法。转为worl、html、再修改也是不可能的,各种乱七八糟的东西。无奈只能放弃。。。
将文章直接保持到本地Markdown
工具:Chrome浏览器
和 Tampermonkey
油猴插件
步骤
- 观察网站HTML接口,找到关键点,编写油猴脚本
// ==UserScript==
// @name bcjksjmd
// @namespace ssqf.site
// @version 0.1
// @description 保存极客时间的文字为markdown
// @author tako
// @match https://time.geekbang.org/*
// @grant none
// @require https://code.jquery.com/jquery-3.4.0.min.js
// @require https://unpkg.com/turndown/dist/turndown.js
// @run-at document-idle
// ==/UserScript==
/*
元素说明
第一个h1即标题
紧接着的div1是作者
下一个div2 是正文
div2.1 开头图片
div2.2 音频 可能不存在,不存在就往前移
div2.3 正文
div2.3.1-1p宣传连接生成
div2.3.1-2p宣传图
div2.4 版权
再下来div3是评论框
div3评论内容
图片 自定义标签
TI = tilte image
A = audio
V = video
*/
(function () {
'use strict';
var tm = 5 * 1000
//var title = "";
var author = "";
var date = "";
var h1Title = ""
var headImg = "";
var audio = "";
var teller = "";
var content = "";
// 等待网页加载完成再执行。但由于网页是用js动态生成的,所以这个不行。
// 这种情况有讨论就是等待一个关键元素,感觉太麻烦
// https://stackoverflow.com/questions/12897446/userscript-to-wait-for-page-to-load-before-executing-code-techniques
// window.addEventListener('load', (event) => {
// GetArticleInfos();
// });
// 延迟执行,简单粗暴
setTimeout(function () {
GetArticleInfos();
}, tm)
//获取文字必要的信息并以josn 字符串形式返回
function GetArticleInfos() {
//title = $("title").text();
var audioElem = $("audio");
var isAudio = (audioElem.length > 0) ? true : false;
//var h1 = $("h1:frist");
var h1 = $("h1").first();
var authorDiv = h1.next();
var mainDiv = authorDiv.next();
var div2ch1 = mainDiv.children();
var imgDiv = div2ch1.eq(0);
var audioDiv
var contentDiv
if (isAudio) {
audioDiv = div2ch1.eq(1);
contentDiv = div2ch1.eq(2).children().eq(0);
} else {
contentDiv = div2ch1.eq(1).children().eq(0);
}
h1Title = h1.text();
var authorch = authorDiv.children();
author = authorch.eq(0).text();
date = authorch.eq(1).text();
headImg = imgDiv.find("img").attr("src");
if (isAudio) {
audio = audioDiv.find("audio").attr("src");
teller = audioDiv.find("span").first().text();
}
// 移除广告每页可能不太一样
var adLink = contentDiv.children().last();
var adImg = adLink.prev();
if (adLink.find("img").length == 1) {
adLink.remove();
} else {
if (adLink.find("a").length == 1 && adImg.find("img").length == 1) {
adLink.remove();
adImg.remove();
}
}
//网页中的高亮和代码用code和table处理的,需要简化使其可以正确转换会markdown
var code = contentDiv.find("code")
code.each(function (i) {
var parentElem = $(this).parent()
if (parentElem.is("pre")) { //代码块
var trList = $(this).find("tr")
var codeTxt = ""
trList.each(function (i) {
var trTxt = $(this).text()
if (trTxt.length > 0) {
codeTxt += trTxt + "\n"
}
})
$(this).empty();
$(this).text(codeTxt);
} else {
var txt = $(this).text();
$(this).empty();
$(this).text(txt);
}
});
content = formatContent(contentDiv);
var mdTxt = ""
mdTxt += "# " + h1Title + "\n\n"
mdTxt += "作者:" + author +" 日期:" + date + "\n\n"
mdTxt += "![TI](" + headImg + ")\n\n"
if (audio != "") {
mdTxt += "![A](" + audio + ")" + "\n\n"
mdTxt += teller + "\n\n"
}
mdTxt += content
// var jsonStr = JSON.stringify(infos);
console.log("markdown text:" + mdTxt);
var fileName = h1Title.replace(/[/\\?*<>:"|]/g,""); //文件名中的特殊字符提出掉
SaveInfoToFile(fileName+".md",mdTxt)
return mdTxt;
}
//处理正文内容
function formatContent(ctt) {
var html = $(ctt).html();
// turndown一个将html转为markdown的js库 https://github.com/domchristie/turndown
var turndownService = new TurndownService({ codeBlockStyle: 'fenced' ,headingStyle:"atx"})
return turndownService.turndown(html)
}
//保存文件
//由于跨域问题不能传json,就只能用from-data方式
function SaveInfoToFile(filename,data) {
$.ajax({
type: "POST",
url: "http://localhost:8282/savemd",
// data: JSON.stringify({filePath:path,fileData:data}),
// contentType:"application/json",
data: {fileName:filename,fileData:data},
success: nextPage, //成功跳到下一页
dataType: "text"
});
}
//跳转到下一页
function nextPage(){
//h1的父的父的弟的5子即为下一页按钮
var nextBtn = $("h1").first().parent().parent().next().children().eq(5);
if (nextBtn.lenght >0){
nextBtn.click();
setTimeout(function () {
GetArticleInfos();
}, tm)
}
}
})();
- 保存到本地markdown文件
- 直接用油猴保存——js不可能简单通过浏览器操作本地文件,放弃
- ajax发送到本地服务,本地服务保存
保存文件到本地的服务
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
)
const servAddr = ":8282"
const dir = "D:\\保存目录\\" //需要目录存在
func main() {
log.Printf("Start savemd serv!\n")
http.HandleFunc("/savemd", saveMD)
log.Fatalf("HTTP Serv error:%v\n", http.ListenAndServe(servAddr, nil))
}
func saveMD(w http.ResponseWriter, r *http.Request) {
fileName := r.FormValue("fileName")
fileData := r.FormValue("fileData")
filePath := dir + fileName
if filePath == "" {
log.Printf("filePath is empty!\n")
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(fmt.Sprintf("request error:filePath is empty!")))
return
}
err := ioutil.WriteFile(filePath, []byte(fileData), 0644)
if err != nil {
log.Printf("ioutil write file[%s] error:%v\n", filePath, err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf("server error:%v", err)))
return
}
log.Printf("Save [%s] ok\n", filePath)
}
- 开启保存服务,浏览到第一篇文章,刷新页面。过段时间文件就会出现在本地。如果中间网页出错刷新一下会继续
- markdown中还是有一些格式不对的,用python写个脚本处理处理就好。如:图片文件保存到本地,标题不合适等。
拿到完整的md文件就好了么?转为一个好看的PDF,又一个折磨的过程开始了。。。
将Markdown文件转为PDF文件
- 文档转换当然用
PanDoc
,然而发现这这个坑也不浅,是一个庞大的工程 - 用vscode的
markdown pdf
还不错,就是有页眉页脚,没有目录书签,边距太小,文章开始不在下一页是连续的,还要继续配置折腾
==未完待续==
结束句
本来是想学东西,结果被带偏,搞了几天还是没有达到自己的预期。有这个时间文章都可能看了一遍了。这就是我还是个低端码农的原因吧。以上路就不知道跑哪里去。。。
参考
-
跨域
- 不要再问我跨域的问题了
-
pandoc 使用
- Pandoc 的使用和遇到的问题