保存極客時(shí)間文章到本地
背景需求
自己極客時(shí)間買(mǎi)的專(zhuān)欄。很多文章很好,也很有深度,想要反復(fù)閱讀。所以想打印成紙質(zhì)版,閱讀更方便。
實(shí)踐過(guò)程
目標(biāo)就是將網(wǎng)頁(yè)正文內(nèi)容保存到PDF
想到的方法:
- 后端爬蟲(chóng) 直接放棄,話(huà)說(shuō)這么有價(jià)值的東西。要是爬蟲(chóng)能簡(jiǎn)單就爬到數(shù)據(jù),還做什么計(jì)算機(jī)技術(shù)知識(shí)內(nèi)容?
- 用chrome保存到PDF 簡(jiǎn)單粗暴,成功率高。就是內(nèi)容多了費(fèi)事,所以上按鍵精靈。
- 前端JS爬蟲(chóng) 有跨域、登錄驗(yàn)證等其他問(wèn)題,復(fù)雜度過(guò)高
- 保存文章到本地 極客空間的網(wǎng)頁(yè)內(nèi)容一看就是markdown生成的,所以再反轉(zhuǎn)回去存入markdown文件。相當(dāng)于拿到原文件就可以隨便整了
網(wǎng)頁(yè)直接保存到PDF文件
chrome有好多插件可以將網(wǎng)頁(yè)保存到PDF
- 捕捉網(wǎng)頁(yè)截圖 - FireShot —— 比較麻煩,不能簡(jiǎn)單的保存正文
- Print Friendly & PDF —— 很方便,會(huì)自動(dòng)過(guò)濾掉無(wú)用的內(nèi)容,而且也很方便刪除
- 先將單獨(dú)文章用
Print Friendly & PDF配合按鍵精靈單獨(dú)保存到pdf中 - 用
Adobe Acrobat X Pro將單獨(dú)文件合并成一個(gè)單獨(dú)的PDF文件,不錯(cuò)還帶書(shū)簽
遇到問(wèn)題
1.文件是按照名字排序的,但實(shí)際文章不是按名字順序的,所以合并后的文章順不對(duì)。。。
解:python文章按時(shí)間排序,(保存的時(shí)候是按文章順序排序保存的)
# dirPath 目錄路徑,只處理目錄下的文件
# sort 0 默認(rèn)按名稱(chēng)排序;1,按時(shí)間正序;2,按時(shí)間逆序
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
給文章名加上編號(hào),并刪除不要的字符,使其按名字排序后為文章的發(fā)表順序
def rename(path, pattern, replace, iExt=True):
"將path路徑下的文件按rxeg正則表達(dá)式重命名,iExt是否忽略擴(kuò)展名"
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自己添加的東西,而且字體變成了黑體,字間距也不合適
解:無(wú)解
要修改PDF簡(jiǎn)直不可能,搜半天沒(méi)一個(gè)點(diǎn)好辦法。轉(zhuǎn)為worl、html、再修改也是不可能的,各種亂七八糟的東西。無(wú)奈只能放棄。。。
將文章直接保持到本地Markdown
工具:Chrome瀏覽器 和 Tampermonkey 油猴插件
步驟
- 觀察網(wǎng)站HTML接口,找到關(guān)鍵點(diǎn),編寫(xiě)油猴腳本
// ==UserScript==
// @name bcjksjmd
// @namespace ssqf.site
// @version 0.1
// @description 保存極客時(shí)間的文字為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==
/*
元素說(shuō)明
第一個(gè)h1即標(biāo)題
緊接著的div1是作者
下一個(gè)div2 是正文
div2.1 開(kāi)頭圖片
div2.2 音頻 可能不存在,不存在就往前移
div2.3 正文
div2.3.1-1p宣傳連接生成
div2.3.1-2p宣傳圖
div2.4 版權(quán)
再下來(lái)div3是評(píng)論框
div3評(píng)論內(nèi)容
圖片 自定義標(biāo)簽
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 = "";
// 等待網(wǎng)頁(yè)加載完成再執(zhí)行。但由于網(wǎng)頁(yè)是用js動(dòng)態(tài)生成的,所以這個(gè)不行。
// 這種情況有討論就是等待一個(gè)關(guān)鍵元素,感覺(jué)太麻煩
// https://stackoverflow.com/questions/12897446/userscript-to-wait-for-page-to-load-before-executing-code-techniques
// window.addEventListener('load', (event) => {
// GetArticleInfos();
// });
// 延遲執(zhí)行,簡(jiǎn)單粗暴
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();
}
// 移除廣告每頁(yè)可能不太一樣
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();
}
}
//網(wǎng)頁(yè)中的高亮和代碼用code和table處理的,需要簡(jiǎn)化使其可以正確轉(zhuǎn)換會(huì)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 += "\n\n"
if (audio != "") {
mdTxt += "" + "\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;
}
//處理正文內(nèi)容
function formatContent(ctt) {
var html = $(ctt).html();
// turndown一個(gè)將html轉(zhuǎn)為markdown的js庫(kù) https://github.com/domchristie/turndown
var turndownService = new TurndownService({ codeBlockStyle: 'fenced' ,headingStyle:"atx"})
return turndownService.turndown(html)
}
//保存文件
//由于跨域問(wèn)題不能傳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, //成功跳到下一頁(yè)
dataType: "text"
});
}
//跳轉(zhuǎn)到下一頁(yè)
function nextPage(){
//h1的父的父的弟的5子即為下一頁(yè)按鈕
var nextBtn = $("h1").first().parent().parent().next().children().eq(5);
if (nextBtn.lenght >0){
nextBtn.click();
setTimeout(function () {
GetArticleInfos();
}, tm)
}
}
})();
- 保存到本地markdown文件
- 直接用油猴保存——js不可能簡(jiǎn)單通過(guò)瀏覽器操作本地文件,放棄
- ajax發(fā)送到本地服務(wù),本地服務(wù)保存
保存文件到本地的服務(wù)
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)
}
- 開(kāi)啟保存服務(wù),瀏覽到第一篇文章,刷新頁(yè)面。過(guò)段時(shí)間文件就會(huì)出現(xiàn)在本地。如果中間網(wǎng)頁(yè)出錯(cuò)刷新一下會(huì)繼續(xù)
- markdown中還是有一些格式不對(duì)的,用python寫(xiě)個(gè)腳本處理處理就好。如:圖片文件保存到本地,標(biāo)題不合適等。
拿到完整的md文件就好了么?轉(zhuǎn)為一個(gè)好看的PDF,又一個(gè)折磨的過(guò)程開(kāi)始了。。。
將Markdown文件轉(zhuǎn)為PDF文件
- 文檔轉(zhuǎn)換當(dāng)然用
PanDoc,然而發(fā)現(xiàn)這這個(gè)坑也不淺,是一個(gè)龐大的工程 - 用vscode的
markdown pdf還不錯(cuò),就是有頁(yè)眉頁(yè)腳,沒(méi)有目錄書(shū)簽,邊距太小,文章開(kāi)始不在下一頁(yè)是連續(xù)的,還要繼續(xù)配置折騰
==未完待續(xù)==
結(jié)束句
本來(lái)是想學(xué)東西,結(jié)果被帶偏,搞了幾天還是沒(méi)有達(dá)到自己的預(yù)期。有這個(gè)時(shí)間文章都可能看了一遍了。這就是我還是個(gè)低端碼農(nóng)的原因吧。以上路就不知道跑哪里去。。。
參考
-
跨域
-
pandoc 使用