【开源一个小工具】一键将网页内容推送到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 =
#网页内容或图片的下载超时时间
sstk.download.timeout =
#是否删除临时目录
sstk.download.deleteTmpDir = false mail.smtp.starttls.enable=true
mail.smtp.socketFactory.port=
mail.smtp.host=smtp..com
mail.host=smtp..com
mail.smtp.auth=true
mail.transport.protocol=smtp
mail.userName=XXX
mail.password=iflytek
mail.from=XXX@.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 :--parent-window=
arg :chrome-extension://oojaanpmaapemaihjbebgojmblljbhhh/
所以我就想Java能不能直接从Windows句柄读数据,因为Java确实提供了一个FileDescriptor类,但折腾了半天发现原生的Java并不支持这么干。最后没办法下,想出了非常丑陋的解决办法,利用C#来做下中转,所以才多了个startup.exe,C#代码写的很顺利,这也让我对Java是累感不爱啊。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Diagnostics; namespace Startup
{
class Program
{
static void Main(string[] args)
{
try
{
if (!Directory.Exists(System.AppDomain.CurrentDomain.BaseDirectory + "\\log"))
{
Directory.CreateDirectory(System.AppDomain.CurrentDomain.BaseDirectory + "\\log");
} if (args.Length == )
{
WriteStandardStreamOut("Missing parameter.");
Log2File("Missing parameter.");
return;
} string url = ReadStandardStreamIn();
Log2File("Running SimpleSendToKindle.jar with url:" + url);
string ret = RunJar(url);
Log2File("Completed with return msg:" + ret);
WriteStandardStreamOut("{\"text\":\"" + ret + "\"}");
}
catch (Exception ex)
{
Log2File("Error:" + ex.ToString());
WriteStandardStreamOut("{\"text\":\"" + "Error." + ex.Message + "\"}");
}
} static string RunJar(string arg)
{
ProcessStartInfo startInfo = new ProcessStartInfo()
{
WorkingDirectory = System.AppDomain.CurrentDomain.BaseDirectory,
UseShellExecute = false,//要重定向 IO 流,Process 对象必须将 UseShellExecute 属性设置为 False。
CreateNoWindow = true,
RedirectStandardOutput = true,
//RedirectStandardInput = false,
WindowStyle = ProcessWindowStyle.Normal,
FileName = "java.exe",
Arguments = @" -Dfile.encoding=utf-8 -jar SimpleSendToKindle.jar " + arg,
};
//启动进程
using (Process process = Process.Start(startInfo))
{
process.Start();
//process.WaitForExit();
using (StreamReader reader = process.StandardOutput)
{
return reader.ReadToEnd();
}
}
} static void Log2File(string s)
{
FileStream fs = new FileStream(System.AppDomain.CurrentDomain.BaseDirectory + @"log/startup.log", FileMode.Append);
StreamWriter sw = new StreamWriter(fs, Encoding.UTF8);
sw.WriteLine(s);
sw.Close();
fs.Close();
} static string ReadStandardStreamIn()
{
using (Stream stdin = Console.OpenStandardInput())
{
int length = ;
byte[] bytes = new byte[];
stdin.Read(bytes, , );
length = System.BitConverter.ToInt32(bytes, ); byte[] msgBytes = new byte[length];
stdin.Read(msgBytes, , length); string decodeMsg = Microsoft.JScript.GlobalObject.decodeURI(System.Text.Encoding.UTF8.GetString(msgBytes));
return decodeMsg;
}
} static void WriteStandardStreamOut(string msg)
{
int length = msg.Length;
byte[] lenBytes = System.BitConverter.GetBytes(length);
byte[] msgBytes = System.Text.Encoding.UTF8.GetBytes(msg);
byte[] wrapBytes = new byte[ + length];
Array.Copy(lenBytes, , wrapBytes, , );
Array.Copy(msgBytes, , wrapBytes, , length); using (Stream stdout = Console.OpenStandardOutput())
{
stdout.Write(wrapBytes, , wrapBytes.Length);
}
}
}
}
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;
}
本来是打算也处理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上提交的代码质量有待提高。
一些资源
前面提到写这个工具的过程中其实发掘了一些很不错的工具和服务,这里推荐给大家:
- KDP(Amazon Kindle Direct Publishing):亚马逊提供的一个服务。
- HTML-to-MOBI:一个在线的将网页转换成mobi文件的服务,但是好像图片处理也有问题。
- 用JS将markdown转成mobi,epub等电子书格式。
- Java mobi metadata editor:一个小工具可以用来编辑mobi的元数据。
- kindle book development tool:貌似是一个收费的工具。
- Calibre:一个非常强大的免费电子书管理和生成工具,推荐这篇文章抓取网页内容生成Kindle电子书。
- RssToMobiService Github上一个抓取RSS生成mobi文件发送到Kindle的工具,很不错的。
写在最后
今天写完才发现,原来Amazon官方就有一个插件叫Send to Kindle,而且支持各种浏览器,很好很强大,需要的同学直接用官方的吧,这么晚码字很辛苦,没有功劳也有苦劳,如果觉得不错给个推荐吧~
写这个工具最大的收获就是:有想法就去做,just do it!
【开源一个小工具】一键将网页内容推送到Kindle的更多相关文章
- 提高Scrum站会效率的一个小工具
博客搬到了fresky.github.io - Dawei XU,请各位看官挪步.最新的一篇是:提高Scrum站会效率的一个小工具.
- Windows PE 第一章 熟悉OD(顺便破解一个小工具)
熟悉OD(顺便破解一个小工具) 上一节了解了OD的简单使用,这次就练习下,目标是破解一款小软件(入门练手用的,没有壳什么的). 首先我们来看一下这个小软件: 我们的目的是输入任何字符串都可以成功注册, ...
- EasyDarwin开源流媒体服务器如何实现按需推送直播的
--本文转自EasyDarwin开源团队成员邵帅的博客:http://blog.csdn.net/ss00_2012/article/details/51441753 我们使用EasyDarwin的推 ...
- 微信小程序:模板消息推送提示{“errcode”:41030,”errmsg”:”invalid page hint: [gP1eXXXXXX]”}
在开发小程序 模板消息定时推送功能时,在开发版测试程序功能运行正常,但提交到线上后提示报错{“errcode”:41030,”errmsg”:”invalid page hint: [gP1eXXXX ...
- springboot搭建一个简单的websocket的实时推送应用
说一下实用springboot搭建一个简单的websocket 的实时推送应用 websocket是什么 WebSocket是一种在单个TCP连接上进行全双工通信的协议 我们以前用的http协议只能单 ...
- 访问github太慢?我写了一个开源小工具一键变快
前言 GitHub应该是广大开发者最常去的站点,这里面有大量的优秀项目,是广大开发者寻找资源,交友学习的好地方.尤其是前段时间GitHub公布了一项代码存档计划--Arctic Code Vault, ...
- 分享一个小工具:Excel表高速转换成JSON字符串
在游戏项目中一般都须要由策划制作大量的游戏内容,当中非常大一部分是使用Excel表来制作的.于是程序就须要把Excel文件转换成程序方便读取的格式. 之前项目使用的Excel表导入工具都是通过Offi ...
- x01.TextProc: 两三分钟完成的一个小工具
在工作中,遇到这么个问题,需要将 Excel 表中类似 2134-1234-4456 的商品编号输入到单位的程序中,而程序只认 213412344456 这种没有 ‘-’ 的输入.数量比较多,一笔一笔 ...
- 一个小工具 TcpTextListener
项目地址 : https://github.com/kelin-xycs/TcpTextListener 这是一个 可以 监听 Tcp (Http) 传输数据 的 小工具 . 不是 抓包 .不要 ...
随机推荐
- 偶然发现的Unity3d,两点之间的距离计算。
无意间查了一下Vector3的API,发现了一个方法. magnitude Returen the length of vector(Read Only). 然后就试了一下这个方法. Vector3 ...
- CF 268E Playlist(贪心)
题目链接: 传送门 Playlist time limit per test:1 second memory limit per test:256 megabytes Description ...
- iOS自动处理键盘事件的第三方库:IQKeyboardManager
我们写界面要考虑很多用户体验问题,键盘事件的响应就是比较麻烦的一种.我们需要监听键盘事件,考虑点击背景收起键盘.考虑键盘遮挡输入框问题等等,而且每个界面都要做这么一套.这个库帮我们解决了这个事情. 这 ...
- python列表、元组、字典(四)
列表 如:[11,22,33,44,44].['TangXiaoyue', 'bruce tang'] 每个列表都具备如下功能: class list(object): ""&qu ...
- MongoDB系列一(索引及C#如何操作MongoDB)
索引总概况 db.test.ensureIndex({"username":1})//创建索引 db.test.ensureIndex({"username": ...
- BZOJ3697: 采药人的路径
传送门 不是那么裸的点分治. $f[i][0/1]$表示当前节点的一个子树中总权值和为$i$,且是否存在一个前缀使得其前缀和为$i$ $g[i][0/1]$表示当前节点的已遍历过的子树,其余一样. 对 ...
- 今天接触枚举类型,感觉是C里面应该才有的东西
遍历枚类型的方法: public static EActChannel getEnumByCode(int code) { for (EActChannel enm : EActChannel.val ...
- SaltStack配置管理之状态模块和jinja2(五)
官方文档 https://docs.saltstack.com/en/latest/topics/states/index.html 配置管理之SLS Salt State SLS描述文件(YAM ...
- 10月21日下午PHP常用函数
函数四要素:返回类型 函数名 参数列表 函数体 //最简单的函数定义方式 function Show() { echo "hello"; } Show();//输出结果为he ...
- Linux下which、whereis、locate、find 命令的区别
1.which 作用:查看可执行文件的位置(通过 PATH环境变量到该路径内查找可执行文件) 语法:which 可执行文件名称 示例: zsm@wilburUbun:/$ which passwd / ...