美文网首页Python
用Python向Kindle推送电子书

用Python向Kindle推送电子书

作者: 小温侯 | 来源:发表于2018-07-21 23:12 被阅读284次

    使用Kindle的同学应该知道Amazon官方有个Send To Kindle的插件可以方便的把文档推送到你的设备上,可惜的是,这个插件只能用美亚的账号登陆,何不自己做一个?

    我从昨天睡觉之前萌生了这个想法,到现在做出来原型整整用了一天,惭愧不已,看来还需多加练习。

    简单说下原理

    完整的源码我会放在最后,具体的细节实现可以看源码。这里我就简单的说下我的思路以及过程中遇到的值得记录一下的”难点“。

    过程其中很简单,就是用SMTP协议把文档以邮件的形式发送给Kindle邮箱。那么,我们需要做的其实只有:

    • 一个UI用来收集Kindle邮箱、推送邮箱、推送邮箱密码和要推送的文档
    • 使用SMTP协议

    邮件服务商

    SMTP协议是应用层的网络协议,由TCP协议支持,换句话说,它是在保证了可靠传输的基础上通过一定的”暗号“交接来传递邮件,过程大概是:打招呼(hello),确认交流方式(加密吗?用什么加密协议?),身份认证(采用base64编码的用户名和密码),传递内容,结束。具体的指令可能会因为邮件服务商所用的加密协议有所不同,但过程基本就是这样。这样的文章网上一搜一大堆:SMTP协议--在cmd下利用命令行发送邮件,有兴趣的可以自己打一打指令,对理解协议有好处。

    但有一点我想说的是这些文章中使用的都是端口25(SMTP的默认端口,传输不加密),而经过我的测试,QQ、163甚至Gmail都已经不再开放25号端口了,用的最多的是TLS和SSL的端口进行加密传输。在SMTP交互过程中,对应的指令变化就是在auth login之前要先发送starttls指令,同时在连接服务器的时候使用端口587。

    这里我要吐槽一下QQ和163邮箱所谓的授权码,我不知道是出于什么原因要让第三方邮件客户端使用这个授权码。它真的让你的邮箱更安全了吗?在使用SMTP协议时,QQ和163在验证身份时都要求提供采用base64编码的授权码。Hmm...

    汉字转拼音库

    在使用smtplib库时,我发现当附件名是中文的时候,收件方收到的邮件中附件会变成一个bin文件。在尝试调整编码无果之后,我想到了一个把中文都转化为拼音的方案。接着去搜了一下还真有一个第三方库:Pinyin

    安装这个库的方法是,将整个github库下载到本地,解压缩,用cmd切换到有setup.py那个目录,然后执行:python setup.py install

    其他

    除了上述两点,本项目中还有几个值得一提的问题或者说我学到的新知识:

    • Python中将键盘鼠标的操作和函数绑定:Events and Bindings,它可以用来实现一个带有超链接效果的Label。
    • 使用文件对话框选取一个文件
    • 怎样清空一个Entry插件

    都在代码里了。

    目前使用的一些约束

    因为只是一个初版,难免有些考虑不到的地方或者说bug,考虑到这只是个练手的项目,我应该也不会继续完善它了,目前我能想到的一些约束包括:

    • 只支持三个邮件服务商:QQ、163和Gmail。目前会对用户输入的推送邮箱和密码做一些简单的校验,但是仅限于判断其是不是以qq.com, 163.comgmail.com结尾,用例如13@3gmail.com就是校验不出来的。kindle邮箱也有类似的问题。同时如果邮箱或密码错误,也不能返回相应的错误消息。
    • 推送的文件大小不能超过50MB,这个其实不是bug,超过50MB的文件即使推送了也会被Amazon退回。同样文件的类型我也不能控制,如果用户选择了kindle不支持的文件类型,软件仍然会推送,只不过同样会被Amazon退回。
    • 在正常流程下,点击发送按钮之后程序会”停滞“很长时间,多长取决于文件的大小。考虑应该添加一个类似于进度条的东西用来缓解用户等待时产生的”焦虑感“。
    • 目前一次只能推送一个文件。考虑将”选择文件“那个Frame做成一个可以进行CRUD操作的LIstbox,但要校验总文件大小不能超过50MB。
    • 还有一些诸如邮箱必须开启SMTP服务,推送邮箱必须处于Amazon账户信任列表里的条件不属于本软件范畴,但是确实必要的,我就不一一列出来的。总之,如果你手动发邮件能成功推送文档,那么软件也可以, 亲测有效。

    完整代码

    效果图

    SentToKindle效果图.jpg

    smtp.py

    import smtplib
    from email.mime.text import MIMEText
    from email.mime.multipart import MIMEMultipart
    from email.header import Header
     
    def SendToKindle(mail_host, mail_user, mail_pass, receiver, fullpath, bookname):
        message = MIMEMultipart()
        message['From'] = Header("SentToKindle", 'utf-8')
        message['To'] =  receiver 
        message['Subject'] = Header('convert')
    
        att = MIMEText(open(fullpath, 'rb').read(), 'base64', 'utf-8')
        att["Content-Type"] = 'application/octet-stream'
        att["Content-Disposition"] = 'attachment; filename=%s' % bookname
        message.attach(att)
    
        smtpObj = smtplib.SMTP(mail_host, 587)
        smtpObj.ehlo()
        smtpObj.starttls()
        smtpObj.login(mail_user, mail_pass)
        smtpObj.sendmail(mail_user, [receiver], message.as_string())
        smtpObj.quit()
    

    ui.py

    import tkinter as tk
    import tkinter.messagebox
    import tkinter.filedialog
    from tkinter import END
    import webbrowser
    import os.path
    
    import smtp
    import pinyin
    
    mail_host = ''
    mail_user = ''
    mail_pass = ''
    receiver  = ''
    fullpath  = ''
    bookname  = ''
    
    class SentToKindleUI(object):
        def __init__(self, object):
            # 推送信息
            self.lf_sendinfo = tk.LabelFrame(object, width=256, height=144, text='推送信息')  
            self.lf_sendinfo.grid(row=0, column=0, sticky='w',padx=10)
            
            self.label_sendinfo_kindlemail = tk.Label(self.lf_sendinfo, width=12, text='Kindle邮箱:')
            self.label_sendinfo_kindlemail.place(x=5,y=2)
            self.label_sendinfo_entry1 = tk.Entry(self.lf_sendinfo, relief='solid')
            self.label_sendinfo_entry1.place(x=100, y=2)
    
            self.label_sendinfo_sendmail = tk.Label(self.lf_sendinfo, width=12, text='推送邮箱:')
            self.label_sendinfo_sendmail.place(x=5,y=30)
            self.label_sendinfo_entry2 = tk.Entry(self.lf_sendinfo, relief='solid')
            self.label_sendinfo_entry2.place(x=100, y=30)
    
            self.label_sendinfo_password = tk.Label(self.lf_sendinfo, width=12, text='推送邮箱密码:')
            self.label_sendinfo_password.place(x=5,y=58)
            self.label_sendinfo_entry3 = tk.Entry(self.lf_sendinfo, relief='solid',show='*')
            self.label_sendinfo_entry3.place(x=100, y=58)
    
            # 校验三个Entries的内容
            def label_sendinfo_bt_click():
                global mail_host, receiver, mail_user, mail_pass
                receiver  = self.label_sendinfo_entry1.get()
                mail_user = self.label_sendinfo_entry2.get()
                mail_pass = self.label_sendinfo_entry3.get()
    
                # 检查kindle邮箱
                if receiver.endswith('kindle.com') or receiver.endswith('kindle.cn'):
                    pass
                else:
                    tk.messagebox.showinfo(title='HI', message='Kindle邮箱必须以kindle.com或kindle.cn结尾。')
                    self.label_sendinfo_entry1.delete(0, END)
                    return
    
                # 检查推送邮箱    
                if mail_user.endswith('gmail.com'):
                    mail_host = 'smtp.gmail.com'
                elif mail_user.endswith('163.com'):
                    mail_host = 'smtp.163.com'
                elif mail_user.endswith('qq.com'):
                    mail_host = 'smtp.qq.com'
                else:
                    tk.messagebox.showinfo(title='HI', message='目前仅支持QQ、163和Gmail邮箱作为推送邮箱。')
                    self.label_sendinfo_entry2.delete(0, END)
                    self.label_sendinfo_entry3.delete(0, END)
                    return
    
                # 如果能进行到这,说明内容校验都没问题
                tk.messagebox.showinfo(title='HI', message='输入没有问题!')
    
    
            varCheck = tk.IntVar()
    
            def label_sendinfo_checkbutton_click():
                if varCheck.get() == 1:
                    self.label_sendinfo_entry3.config(show='')
                else:
                    self.label_sendinfo_entry3.config(show='*')
    
            self.label_sendinfo_checkbutton = tk.Checkbutton(self.lf_sendinfo,
                text = '显示密码',
                variable = varCheck,
                onvalue = 1,
                offvalue = 0,
                command = label_sendinfo_checkbutton_click
                )
            self.label_sendinfo_checkbutton.place(x=90,y=86)
    
            self.label_sendinfo_bt = tk.Button(self.lf_sendinfo,
                text='校验',
                width=8,
                command=label_sendinfo_bt_click
                )
            self.label_sendinfo_bt.place(x=175,y=86)
    
            # 文件选择
            self.lf_file = tk.LabelFrame(object, width=256, height=128, text='文件选择')  
            self.lf_file.grid(row=1, column=0, sticky='w', padx=10)
    
            self.lf_file_label = tk.Label(self.lf_file, 
                width=34, 
                text='已选择:(空)',
                anchor='w', 
                justify='left',
                wraplength=240
                )
            
    
            def lf_file_bt_click():
                global bookname, fullpath
                SupportedFiletypes = [('所有文件','*.*'), ('mobi文件','*.mobi'), ('文本文件','*.txt'), ('pdf文件','*.pdf')] 
                filename = tk.filedialog.askopenfilename(filetypes=SupportedFiletypes)
    
                if filename != '':
                    filesize = os.path.getsize(filename)/float(1024*1024)  # MB
                    if float(filesize) > 50.00:
                        tk.messagebox.showinfo(title='HI', message='文件大小不得超过50MB。')
                        self.lf_file_label.config(text = '已选择:(空)')
                        return
                    self.lf_file_label.config(text = '已选择: '+ filename)
                    fullpath = filename
                    bookname = pinyin.get(os.path.basename(fullpath), format="numerical")
    
            self.lf_file_bt = tk.Button(self.lf_file,
                text = '选择文件',
                command=lf_file_bt_click
                )
            self.lf_file_bt.place(x=2, y=2)
            self.lf_file_label.place(x=2, y=42)
    
            # 描述信息
            self.lf_desc = tk.LabelFrame(object, width=256, height=96, text='说明')  
            self.lf_desc.grid(row=2, column=0, sticky='w', padx=10)
    
            def callback(event):
                webbrowser.open_new(r"https://journal.ethanshub.com/post/category/gong-cheng-shi/-python-kindledian-zi-shu-tui-song#toc_4")
    
            self.tmp = "目前一些使用的约束"
            self.lf_desc_label = tk.Label(self.lf_desc, 
                fg='blue',
                cursor='hand2',
                width=34, 
                text=self.tmp, 
                anchor='w', 
                justify='left',
                wraplength=250
                )
            self.lf_desc_label.place(x=2, y=2)
            self.lf_desc_label.bind("<Button-1>", callback)
            
            # 按钮
            self.lf_button = tk.Frame(object, width=256, height=96)  
            self.lf_button.grid(row=3, column=0, sticky='w', padx=10)
    
            def lf_button_bt1_click():
                global mail_host, mail_user, mail_pass, receiver, bookname
                
                smtp.SendToKindle(mail_host, mail_user, mail_pass, receiver, fullpath, bookname)
    
            self.lf_button_bt1 = tk.Button(self.lf_button,
                text='发送',
                width=12,
                height=2,
                command=lf_button_bt1_click
                )
            self.lf_button_bt1.place(x=20,y=5)
    
            self.lf_button_bt2 = tk.Button(self.lf_button,
                text='取消',
                width=12,
                height=2,
                command=self.lf_sendinfo.quit
                )
            self.lf_button_bt2.place(x=123,y=5)
    
    # 初始化窗口
    root = tk.Tk()
    root.title('Sent to Kindle')
    
    width = 276
    height = 432
    screenwidth = root.winfo_screenwidth()  
    screenheight = root.winfo_screenheight()  
    size = '%dx%d+%d+%d' % (width, height, (screenwidth - width)/2, (screenheight - height)/2)
    root.geometry(size)
    
    SentToKindleUI(root)
    root.mainloop()
    

    main.py

    import ui as myUI
    
    # 初始化窗口
    root = myUI.tk.Tk()
    root.title('Sent to Kindle')
    
    width = 276
    height = 432
    screenwidth = root.winfo_screenwidth()  
    screenheight = root.winfo_screenheight()  
    size = '%dx%d+%d+%d' % (width, height, (screenwidth - width)/2, (screenheight - height)/2)
    root.geometry(size)
    
    myUI.SentToKindleUI(root)
    root.mainloop()
    

    相关文章

      网友评论

        本文标题:用Python向Kindle推送电子书

        本文链接:https://www.haomeiwen.com/subject/hubkmftx.html