最近工作上稍微闲点,这一周利用下班时间写了一个小工具,其实功能挺简单但也小折腾了会。

工具名称: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上提交的代码质量有待提高。

一些资源

前面提到写这个工具的过程中其实发掘了一些很不错的工具和服务,这里推荐给大家:

写在最后

今天写完才发现,原来Amazon官方就有一个插件叫Send to Kindle,而且支持各种浏览器,很好很强大,需要的同学直接用官方的吧,这么晚码字很辛苦,没有功劳也有苦劳,如果觉得不错给个推荐吧~

写这个工具最大的收获就是:有想法就去做,just do it!

【开源一个小工具】一键将网页内容推送到Kindle的更多相关文章

  1. 提高Scrum站会效率的一个小工具

    博客搬到了fresky.github.io - Dawei XU,请各位看官挪步.最新的一篇是:提高Scrum站会效率的一个小工具.

  2. Windows PE 第一章 熟悉OD(顺便破解一个小工具)

    熟悉OD(顺便破解一个小工具) 上一节了解了OD的简单使用,这次就练习下,目标是破解一款小软件(入门练手用的,没有壳什么的). 首先我们来看一下这个小软件: 我们的目的是输入任何字符串都可以成功注册, ...

  3. EasyDarwin开源流媒体服务器如何实现按需推送直播的

    --本文转自EasyDarwin开源团队成员邵帅的博客:http://blog.csdn.net/ss00_2012/article/details/51441753 我们使用EasyDarwin的推 ...

  4. 微信小程序:模板消息推送提示{“errcode”:41030,”errmsg”:”invalid page hint: [gP1eXXXXXX]”}

    在开发小程序 模板消息定时推送功能时,在开发版测试程序功能运行正常,但提交到线上后提示报错{“errcode”:41030,”errmsg”:”invalid page hint: [gP1eXXXX ...

  5. springboot搭建一个简单的websocket的实时推送应用

    说一下实用springboot搭建一个简单的websocket 的实时推送应用 websocket是什么 WebSocket是一种在单个TCP连接上进行全双工通信的协议 我们以前用的http协议只能单 ...

  6. 访问github太慢?我写了一个开源小工具一键变快

    前言 GitHub应该是广大开发者最常去的站点,这里面有大量的优秀项目,是广大开发者寻找资源,交友学习的好地方.尤其是前段时间GitHub公布了一项代码存档计划--Arctic Code Vault, ...

  7. 分享一个小工具:Excel表高速转换成JSON字符串

    在游戏项目中一般都须要由策划制作大量的游戏内容,当中非常大一部分是使用Excel表来制作的.于是程序就须要把Excel文件转换成程序方便读取的格式. 之前项目使用的Excel表导入工具都是通过Offi ...

  8. x01.TextProc: 两三分钟完成的一个小工具

    在工作中,遇到这么个问题,需要将 Excel 表中类似 2134-1234-4456 的商品编号输入到单位的程序中,而程序只认 213412344456 这种没有 ‘-’ 的输入.数量比较多,一笔一笔 ...

  9. 一个小工具 TcpTextListener

    项目地址 :    https://github.com/kelin-xycs/TcpTextListener 这是一个 可以 监听 Tcp (Http) 传输数据 的 小工具 . 不是 抓包 .不要 ...

随机推荐

  1. Linux Command Line Basics

    Most of this note comes from the Beginning the Linux Command Line, Second Edition by Sander van Vugt ...

  2. jboss性能优化

    jboss     linux jboss 部署时优化设置: 在/conf/web.xml中通过参数指定: <session-config>          <session-ti ...

  3. 修改/etc/profile导致常用命令不可用的解决办法

    原因:/etc/profile文件修改有误 解决办法: 用/usr/bin/vim /etc/profile进入,进去后修改正确/etc/profile,然后重启机器让该文件生效即可.

  4. photoshop工具使用的简单介绍

    photoshop工具使用的简单介绍 我所用PhotoShop版本号是cs6,这里对其主要功能做一个简单介绍. 第一部分: 首先,ps的界面主要分为了6部分: 一.最上面的一行的菜单栏,菜单中有:文件 ...

  5. DNS(企业级)

    构建DNS(企业级) 1.硬件选型 CPU:12C以上配置 内存:16G 网络:千兆 2.初始化系统配置 关闭 iptables service iptables stop chkconfig ipt ...

  6. java编程思想-java中的并发(一)

    一.基本的线程机制 并发编程使我们可以将程序划分为多个分离的.独立运行的任务.通过使用多线程机制,这些独立任务中的每一个都将由执行线程来驱动. 线程模型为编程带来了便利,它简化了在单一程序中同时jia ...

  7. SSL/TLS协议工作流程

    我看了CloudFlare的说明(这里和这里),突然意识到这是绝好的例子,可以用来说明SSL/TLS协议的运行机制.它配有插图,很容易看懂. 下面,我就用这些图片作为例子,配合我半年前写的<SS ...

  8. sql 分页的两种写法

    string Strsql = string.Format(@"select ee.DOCUMENTNO,ee.APPLICANTNAME,ee.COMPANY,ee.REQUESTTIME ...

  9. C#----使用WindowsMediaPlayer 同时播放多个声音

    使用Windows Media Player 其实就是使用组件AxWindowsMediaPlayer. 添加两个引用:Interop.WMPLib.dll和AxInterop.WMPLib.dll. ...

  10. C#中操作XML文件

    1.添加结点:XmlNode xmldoc.Load("..\\..\\App.config"); //根元素 XmlElement root = xmldoc.DocumentE ...