【開源一個小工具】一鍵將網頁內容推送到Kindle


最近工作上稍微閑點,這一周利用下班時間寫了一個小工具,其實功能挺簡單但也小折騰了會。

工具名稱:Simple Send to Kindle

Github地址https://github.com/zhanjindong/SimpleSendToKindle

功能:Windows下一個簡單的將網頁內容推送到Kindle的工具。

寫這個工具的是滿足自己的需求。自從買了Kindle paperwhite 2,它就成了我使用率最高的一個電子設備。相信很多Kindle擁有者和我一樣都有這樣一個需求:就是白天網上看到了一些好文章沒時間看,就想把它推送到Kindle上,晚上睡覺前躺在床上慢慢看。之前我一直用的是一個叫KindleMii的工具,但是發現經常推送的內容圖片丟失了,Chrome應用商店里有一個叫做Send to Kindle的工具但是裝了之后不知道什么原因用不了,於是我就想不如自己動手寫一個,名字就叫Simple Send to Kindle。

原理

原理很簡單,就是通過Chrome擴展程序將網頁鏈接發送給本地的一個Java寫的程序,這個程序將網頁內容下載下來並轉換為Kindle的mobi格式,然后再通過kindle的郵箱發送給Kindle設備。

工具的核心功能是利用Amazon提供的一個叫kindlegen的程序生成mobi文件,大家也可以離線使用這個工具將網頁內容生成各種Kindle支持的格式,另外一個核心是Chrome擴展和本地程序的Native Messaging,這個浪費了我挺長時間,后面會簡單介紹下。

 

如何使用

1、用mvn assembly打包,打包后目錄如下:

2、工具可以放到任何地方,然后執行setup.bat這個腳本。

3、安裝Chrome擴展。在Chrome里輸入chrome://extension就可以進入擴展管理:點加載正在開發的擴展程序,選擇ext下的Chrome目錄就可以以開發者模式加載擴展程序了,可以看到每個擴展都有一個唯一標識ID,這個后面配置會用到。

加載成功就可以在瀏覽器地址欄右邊看到這個logo了:

4、工具已經安裝成功了下面進行一些簡單配置就可以了:

1)打開SimpleSendToKindle.json這個文件:將allowed_origins里面的內容修改為上面Chrome擴展的ID。

2)sstk.properties里面是一些工具的通用配置:

#整個服務的超時時間 sstk.service.timeout = 120000 #網頁內容或圖片的下載超時時間 sstk.download.timeout = 15000 #是否刪除臨時目錄 sstk.download.deleteTmpDir = false mail.smtp.starttls.enable=true mail.smtp.socketFactory.port=25 mail.smtp.host=smtp.126.com mail.host=smtp.126.com mail.smtp.auth=true mail.transport.protocol=smtp mail.userName=XXX mail.password=iflytek mail.from=XXX@126.com mail.to=XXX@kindle.cn #debug sstk.debug.sendMail = false

主要配置的就是郵箱這塊,mail.to配置是你的Kindle郵箱,mail.from是用來發送的郵箱,我這里用的是126,其他郵箱也都支持smtp,有Kindle的同學都知道要想Kindle收到郵件發送的內容必須將發送油箱添加到Amazon認可的郵箱列表中。

都配置好后看到你想要推送的頁面,只要輕輕點擊下就Ok了。

稍等片刻,查看你的Kindle,效果如下:

 

 

遇到的一些問題

工具雖然簡單,但是從思路到成型,過程也遇到了一些問題,這里跟大家分享下,有興趣的同學可以接着往下看。

實現思路

有了想法后首先要想的就是實現思路,一開始想用JavaScript寫,最后只要安裝一個Chrome擴展程序就可以了,這樣肯定是Simple的,但是最后還是放棄這個想法,一來我對JS基本不會,二來寫這個工具的目的是為了滿足自己的需求,怎么快怎么來,什么技術熟悉就用什么,所以最后還是決定用Chrome擴展和Java程序通信這種方式。但這過程發現了一些很有用的工具,我在最后會推薦給大家。

 

Chrome擴展開發

我一直用的都是chrome,所以想到了開發Chrome下的插件(Chrome下叫Extension擴展)。那首先要解決的就是如何開發Chrome插件?開發chrome擴展很簡單,官方有一個入門例子非常簡單,一看就懂http://chrome.liuyixi.com/getstarted.html。這里推薦園子里的一篇文章:Chrome插件(Extensions)開發攻略

 

Chrome擴展和本地程序通信

官方術語叫做Native Messaging具體技術細節這里不啰嗦了,有興趣的同學可以網上搜下,這里指簡單介紹下。chrome擴展在Windows下是通過HKEY_CURRENT_USER\Software\Google\Chrome\NativeMessagingHosts\這個注冊表下面的內容和一個.json的清單文件來找到你的Native App的。上面的setup.bat就是用來寫入注冊表的,SimpleSendToKind.json就是清單文件:

@echo off reg add HKEY_CURRENT_USER\Software\Google\Chrome\NativeMessagingHosts\so.zjd.sstk /ve /t REG_SZ /d %~dp0\SimpleSendToKindle.json /f

setup.bat將so.zjd.sstk這個“程序”注冊到chrome關心的注冊表下,Chrome通過它找到標識應用程序信息的清單文件:

{ "name":"so.zjd.sstk", "description":"Simple Send to Kindle(by zjd.so)", "path":"startup.exe",
        "type":"stdio", "allowed_origins":[ "chrome-extension://jnihbngmnjbmchfhcdfabofamnfcljaf/" ] }

path是本地程序的路徑,除了注意程序的權限問題外,還要注意這里path里面如果有路徑分隔符必須是雙斜杠“//”。

Chrome是通過系統的標准輸入輸出和本地程序進行通信,具體協議如下:

Chrome 瀏覽器在單獨的進程中啟動每一個原生消息通信宿主,並使用標准輸入(stdin)與標准輸出(stdout)與之通信。向兩個方向發送消息時使用相同的格式:每一條消息使用 JSON 序列化,以 UTF-8 編碼,並在前面附加 32 位的消息長度(使用本機字節順序)。

協議其實很簡單,但是這塊卻浪費了我好長時間,我用Java死活無法讀取Chrome寫入標准輸入的內容,總是報下面的錯誤:

一開始懷疑自己的寫的代碼有問題,網上搜了半天有說是JDK的問題,我重裝還是不行。后來我發現Chrome傳給程序其實有兩個參數,一個windwos的句柄,一個Chrome擴展的ID:

arg 0:--parent-window=3349886 arg 1:chrome-extension://oojaanpmaapemaihjbebgojmblljbhhh/

所以我就想Java能不能直接從Windows句柄讀數據,因為Java確實提供了一個FileDescriptor類,但折騰了半天發現原生的Java並不支持這么干。最后沒辦法下,想出了非常丑陋的解決辦法,利用C#來做下中轉,所以才多了個startup.exe,C#代碼寫的很順利,這也讓我對Java是累感不愛啊。

 1 using System;  2 using System.Collections.Generic;  3 using System.Linq;  4 using System.Text;  5 using System.IO;  6 using System.Diagnostics;  7 
 8 namespace Startup  9 {  10     class Program  11  {  12         static void Main(string[] args)  13  {  14             try
 15  {  16                 if (!Directory.Exists(System.AppDomain.CurrentDomain.BaseDirectory + "\\log"))  17  {  18                     Directory.CreateDirectory(System.AppDomain.CurrentDomain.BaseDirectory + "\\log");  19  }  20 
 21                 if (args.Length == 0)  22  {  23                     WriteStandardStreamOut("Missing parameter.");  24                     Log2File("Missing parameter.");  25                     return;  26  }  27 
 28                 string url = ReadStandardStreamIn();  29                 Log2File("Running SimpleSendToKindle.jar with url:" + url);  30                 string ret = RunJar(url);  31                 Log2File("Completed with return msg:" + ret);  32                 WriteStandardStreamOut("{\"text\":\"" + ret + "\"}");  33  }  34             catch (Exception ex)  35  {  36                 Log2File("Error:" + ex.ToString());  37                 WriteStandardStreamOut("{\"text\":\"" + "Error." + ex.Message + "\"}");  38  }  39  }  40 
 41         static string RunJar(string arg)  42  {  43             ProcessStartInfo startInfo = new ProcessStartInfo()  44  {  45                 WorkingDirectory = System.AppDomain.CurrentDomain.BaseDirectory,  46                 UseShellExecute = false,//要重定向 IO 流,Process 對象必須將 UseShellExecute 屬性設置為 False。
 47                 CreateNoWindow = true,  48                 RedirectStandardOutput = true,  49                 //RedirectStandardInput = false,
 50                 WindowStyle = ProcessWindowStyle.Normal,  51                 FileName = "java.exe",  52                 Arguments = @" -Dfile.encoding=utf-8 -jar SimpleSendToKindle.jar " + arg,  53  };  54             //啟動進程
 55             using (Process process = Process.Start(startInfo))  56  {  57  process.Start();  58                 //process.WaitForExit();
 59                 using (StreamReader reader = process.StandardOutput)  60  {  61                     return reader.ReadToEnd();  62  }  63  }  64  }  65 
 66         static void Log2File(string s)  67  {  68             FileStream fs = new FileStream(System.AppDomain.CurrentDomain.BaseDirectory + @"log/startup.log", FileMode.Append);  69             StreamWriter sw = new StreamWriter(fs, Encoding.UTF8);  70  sw.WriteLine(s);  71  sw.Close();  72  fs.Close();  73  }  74 
 75         static string ReadStandardStreamIn()  76  {  77             using (Stream stdin = Console.OpenStandardInput())  78  {  79                 int length = 0;  80                 byte[] bytes = new byte[4];  81                 stdin.Read(bytes, 0, 4);  82                 length = System.BitConverter.ToInt32(bytes, 0);  83 
 84                 byte[] msgBytes = new byte[length];  85                 stdin.Read(msgBytes, 0, length);  86 
 87                 string decodeMsg = Microsoft.JScript.GlobalObject.decodeURI(System.Text.Encoding.UTF8.GetString(msgBytes));  88                 return decodeMsg;  89  }  90  }  91 
 92         static void WriteStandardStreamOut(string msg)  93  {  94             int length = msg.Length;  95             byte[] lenBytes = System.BitConverter.GetBytes(length);  96             byte[] msgBytes = System.Text.Encoding.UTF8.GetBytes(msg);  97             byte[] wrapBytes = new byte[4 + length];  98             Array.Copy(lenBytes, 0, wrapBytes, 0, 4);  99             Array.Copy(msgBytes, 0, wrapBytes, 4, length); 100 
101             using (Stream stdout = Console.OpenStandardOutput()) 102  { 103                 stdout.Write(wrapBytes, 0, wrapBytes.Length); 104  } 105  } 106  } 107 }
View Code 

 

Chrome擴展獲取當前頁面的url

園子里那個例子里是在content_script.js里用document.URL,但是我發現這有個問題,每次必須重新加載頁面,不然這個值好像全局就一個。發現用chrome.tabs.getSelected這個事件監聽更好些:

chrome.tabs.getSelected(null,function(tab) { var port = null; var nativeHostName = "so.zjd.sstk"; port = chrome.runtime.connectNative(nativeHostName); port.onMessage.addListener(function(msg) { //console.log("Received " + msg); 
        $("#message").text(msg.text); }); port.onDisconnect.addListener(function onDisconnected(){ //console.log("connetct native host failure:" + chrome.runtime.lastError.message);
        port = null; //$("#message").text("Finished!");
 }); port.postMessage(encodeURI(tab.url)) });
popup.js

 

圖片解析

其實右鍵將網頁另存為為html后就能利用kindlegen生成mobi文件了,或者利用Amazon的郵箱服務直接將html文件發送給Kindle,也能自動轉換成mobi。但是之所以要寫這個工具的原因就是kindlegen也好,kindle郵箱服務也好都不會去主動下載頁面里的圖片,kindlegen需要你將頁面里圖片或其他資源的地址轉換成相對路徑,然后將資源統一放在一個文件家里。

所以處理也很簡單解析頁面img元素內容,自己將圖片下載下來然后將src替換成相對路徑就OK了,需要注意的就是網頁圖片引用的幾種方式:http://www.test.com/dir1/dir2/test.html

./images/mem/figure9.png  →  http://www.test.com/dir1/dir2/images/mem/figure9.png
images/mem/figure9.png    →  http://www.test.com/dir1/dir2/images/mem/figure9.png
/images/mem/figure9.png   →  http://www.test.com/images/mem/figure9.png
../../images/mem/figure9.png → http://www.test.com/figure.png

.表示當前目錄

..表示上級目錄

代碼大致如下:

private String processRelativeUrl(String url) {
        if (url.startsWith("http://")) {
            return url;
        }
        String pageUrl = this.page.getUrl();
        int relative = 0;
        int index = 0;
        if (url.startsWith("/")) {
            relative = -1;
        } else {
            while (true) {
                index = 0;
                if (url.startsWith("./")) {// 當前目錄
                    index = url.indexOf("./");
                    url = url.substring(index + 2);
                    continue;
                } else if (url.startsWith("../")) {// 上級目錄
                    relative++;
                    index = url.indexOf("../");
                    url = url.substring(index + 3);
                    continue;
                } else {// 當前目錄
                    break;
                }
            }
        }
        if (relative == -1) {
            index = pageUrl.indexOf('/', 7);
            pageUrl = pageUrl.substring(0, index);
            url = url.substring(1);
        } else {
            for (int i = 0; i <= relative; i++) {
                index = pageUrl.lastIndexOf("/");
                if (index == -1) {
                    break;
                }
                pageUrl = pageUrl.substring(0, index);
            }
        }
        url = pageUrl + "/" + url;

        return url;
    }
View Code

本來是打算也處理CSS的,結果發現CSS反而會導致生成的mobi格式錯亂就算了。

 

頁面亂碼

有的網頁的meta元素並不規范會導致kindlegen生成的mobi文件亂碼,比如:

<meta charset="UTF-8">

需要處理下:

<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>

 

一些網站防止惡意抓取的問題

有些網站的頁面為了防止網絡爬蟲惡意抓取內容會對HTTP請求的User-Agent進行簡單驗證,這種情況簡單模擬下瀏覽器的UA就可以繞過了,這也說明了惡意的抓取確實很難杜絕,前幾天園子里好像還有人提到這個。這里有個疑問:到底什么樣的行為算惡意抓取,就我本人來說肯定不會有任何惡意。

 

存在的問題

寫的比較匆忙,還存在很多問題:

1、Chrome插件沒界面、沒用戶體驗,只是為了實現功能;

2、需要C#程序來做中轉,這個太惡心了,結果工具一點也不simple;

3、有的中文網頁會導致生成的mobi文件亂碼,肯定是網頁編碼方便的問題,有時間再看看;

4、生成的mobi文件比較大,可以考慮對內容進行裁剪;

5、不支持將頁面選中的內容推送到Kindle;

6、如果頁面有代碼或排版不好,顯示比較亂,可讀性比較差;

7、未考慮Kindle不支持的圖片格式,其實大部分情況就哪幾種圖片;

8、Linux平台支持,其實kindlegen有linux下的版本,Chrome擴展本身在什么平台下都能用。

另外才關注開源沒多久,Github上提交的代碼質量有待提高。

 

一些資源

前面提到寫這個工具的過程中其實發掘了一些很不錯的工具和服務,這里推薦給大家:

 

寫在最后

今天寫完才發現,原來Amazon官方就有一個插件叫Send to Kindle,而且支持各種瀏覽器,很好很強大,需要的同學直接用官方的吧,這么晚碼字很辛苦,沒有功勞也有苦勞,如果覺得不錯給個推薦吧~

寫這個工具最大的收獲就是:有想法就去做,just do it!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM