谢乾坤 | Kingname

给时光以生命。

在写爬虫的时候,经常会使用xpath进行数据的提取,对于如下的代码:

1
<div id="test1">大家好!</div>

使用xpath提取是非常方便的。假设网页的源代码在selector中:

1
data = selector.xpath('//div[@id="test1"]/text()').extract()[0]

就可以把“大家好!”提取到data变量中去。

然而如果遇到下面这段代码呢?

1
<div id="test2">美女,<font color=red>你的微信是多少?</font><div>

如果使用:

1
data = selector.xpath('//div[@id="test2"]/text()').extract()[0]

只能提取到“美女,”;

如果使用:

1
data = selector.xpath('//div[@id="test2"]/font/text()').extract()[0]

又只能提取到“你的微信是多少?”

可是我本意是想把“美女,你的微信是多少?”这一整个句子提取出来。

这还不是最糟糕的,还有第三段代码:

1
<div id="test3">我左青龙,<span id="tiger">右白虎,<ul>上朱雀,<li>下玄武。</li></ul>老牛在当中,</span>龙头在胸口。<div>

而且内部的标签还不固定,如果我有一百段这样类似的html代码,又如何使用xpath表达式,以最快最方便的方式提取出来?

我差一点就去用正则表达式替换了。还好我去Stack Overflow上面提了问。于是很快就有人给我解答了。

使用xpath的string(.)

以第三段代码为例:

1
2
data = selector.xpath('//div[@id="test3"]')
info = data.xpath('string(.)').extract()[0]

这样,就可以把“我左青龙,右白虎,上朱雀,下玄武。老牛在当中,龙头在胸口”整个句子提取出来,赋值给info变量。

文章首发地址:http://kingname.info

今天在Github更新代码的时候,不小心把Gmail私钥文件更新上去了。即便我立刻删除了这个文件,可是在版本历史里面仍然可以看到这个文件的内容。这可把我吓坏了。

Google一圈以后,终于找到了解决办法。把某个文件的历史版本全部清空。

首先cd 进入项目文件夹下,然后执行以下代码:

1
2
3
4
5
6
7
8
9
10
11
git filter-branch --force --index-filter 'git rm --cached --ignore-unmatch 文件名' --prune-empty --tag-name-filter cat -- --all

git push origin master --force

rm -rf .git/refs/original/

git reflog expire --expire=now --all

git gc --prune=now

git gc --aggressive --prune=now

虽然不知道他们的作用是什么,不过真的解决了我的问题。看起来,以前我说我熟练掌握git,真是自不量力。

文章首发地址:http://kingname.info

这篇文章不会教你在技术角度上使用log,而是告诉你为什么要使用log日志功能。

为什么要使用Log

使用微信控制你的电脑这篇文章中,我写好了电脑端的程序,使用py2exe生成可执行文件,并把它们发送给我的朋友让他们进行测试。但是他们把_config.ini设置好以后,运行程序就看到一个黑色窗口一闪而过。或者有些人一开始看到程序能正常登陆邮箱,但是准备执行命令的时候,窗口自动关闭。

由于没有日志记录程序的运行状态,我根据他们的描述很难定位到错误在哪个地方。于是折腾了一个下午。

这个时候,我觉得添加一个日志的功能迫在眉睫。

哪些地方应该用Log

目前网上能找到的关于如何使用日志的文章,全部都是从技术角度讲怎么使用log:在XX地方应该先imort logging,然后basicconfig设定XX内容。可是我现在的问题是:

  • 应该在程序的哪些地方添加日志的输出?
  • 输出什么内容?
  • 如何输出才能以方便我的监控程序的运行情况?

于是我只有自己摸索。因此,以下内容是我自己摸索出来的野路子,可能会有错漏。希望有经验的朋友能给我指正,非常感谢。

这些地方应该用Log

使用使用微信控制你的电脑文章中涉及到的例子

程序入口代码如下:

1
2
3
4
5
6
7
if __name__=='__main__':
init()
print u'等待接收命令'
logging.info(u'初始化完成。')
while 1:
time.sleep(int(time_limit)) #每5分钟检查一次邮箱
accp_mail()

以上代码表示程序运行以后,首先执行init()函数,于是如果init()初始化没有什么问题,成功执行完成以后,就应该在日志中输出“初始化完成”,然后进入接收邮件的循环。如果程序窗口运行以后一闪而过,而且生成的日志中没有初始化完成这样的字眼,那就说明问题出在初始化上面。

然而初始化函数里面代码也有很多,又如何知道是初始化程序里面的什么地方出问题了呢?

所以,再初始化函数里面,也应该有一定的日志记录。

再看初始化函数的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
def init():
global username,password,host,boss_email,time_limit

try:
f = open('_config.ini','r')
except IOError,e:
logging.error(e)
exit()

info = f.readlines()
try:
host = re.search('host:(.*?)\n',info[0],re.S).group(1)
username = re.search('username:(.*?com)',info[1],re.S).group(1)
password = re.search('password:(.*?)\n',info[2],re.S).group(1)
boss_email = re.search('boss_email:(.*?com)',info[3],re.S).group(1)
time_limit = re.search('time_limit:(.*?)\n',info[4],re.S).group(1)
except Exception,e:
logging.error(e)

logging.info(u'打开配置文件成功。。。')


#将命令生成字典,便于查询
command_start = info.index('<command>\n')
command_end = info.index('</command>\n')
for each in info[command_start+1:command_end]:
command = each.split('=')
command_dict[command[0]] = command[1]

logging.info(command_dict)

open_start = info.index('<open_file>\n')
open_end = info.index('</open_file>\n')
for each in info[open_start+1:open_end]:
open_file = each.split('=')
open_dict[open_file[0]] = open_file[1][:-1]

logging.info(open_dict)

f.close()

在这段代码中,我使用try except命令捕获异常,如果发生异常,就使用logging.error将错误写入日志中。例如当_config.ini被改名了或者被删除的时候,程序就会报错,而通过日志就能发现这个错误。

经过上面的代码,如果在初始化的过程中出错,就可以很快确定问题出在什么地方。

初始化完成以后,进入邮件接收阶段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def accp_mail():
logging.info(u'开始检查邮箱')
try:
pp = poplib.POP3_SSL(host)
pp.set_debuglevel(1)
pp.user(username)
pp.pass_(password)
ret = pp.list()
logging.info(u'登录邮箱成功。')
except Exception,e:
logging.error(e)
exit()

logging.info(u'开始抓取邮件。')
try:
down = pp.retr(len(ret[1]))
logging.info(u抓取邮件成功。'')
except Exception,e:
logging.error(e)
exit()

logging.info(u'开始抓取subject和发件人')
try:
subject = re.search("Subject: (.*?)',",str(down[1]).decode('utf-8'),re.S).group(1)
sender = re.search("'X-Sender: (.*?)',",str(down[1]).decode('utf-8'),re.S).group(1)
logging.info(u'抓取subject和发件人成功')
except Exception,e:
logging.error(e)
exit()

if subject != 'pass':
if sender == boss_email:
DealCommand(subject)
pp.quit()

以上这段代码,对邮箱的登录与邮件的读取均作了监控,一旦有某个环节出了问题,就会体现在日志中。

通过在登录环节的try except返回的错误日志,发现有很多朋友无法登录邮箱,而密码用户名都没有错,从而推断是没有在新浪邮箱的账户控制里面打开客服端接收POP3和SMTP的功能。

再来看DealCommand()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def DealCommand(subject):
logging.info(u'开始处理命令。')
send_mail('pass','slave')
if subject in command_dict:
logging.info(u'执行命令')
try:
command = command_dict[subject]
os.system(command)
send_mail('Success','boss')
logging.info(u'执行命令成功')
except Exception,e:
logging.error(e)
send_mail('error','boss',e)
elif subject in open_dict:
logging.info(u'打开文件')
try:
open_file = open_dict[subject]
win32api.ShellExecute(0, 'open', open_file, '','',1)
send_mail('Success','boss')
logging.info(u'打开文件成功')
except Exception,e:
logging.error(e)
send_mail('error','boss',e)
else:
send_mail('error','boss','no such command')

执行命令的地方可能会出错,于是果断使用try except捕获错误。并使用日志记录。

最后是send_mail()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def send_mail(subject,flag,body='Success'):

msg = MIMEText(body,'plain','utf-8')#中文需参数‘utf-8’,单字节字符不需要
msg['Subject'] = subject
msg['from'] = username
logging.info('开始配置发件箱。')
try:
handle = smtplib.SMTP('smtp.sina.com', 25)
handle.login(username,password)
logging.info('发件箱配置成功')
except Exception,e:
logging.error(e)
exit()

logging.info(u'开始发送邮件'+ 'to' + flag)
if flag == 'slave':
try:
handle.sendmail(username,username, msg.as_string())
logging.info(u'发送邮件成功')
except Exception,e:
logging.error(e)
exit()
elif flag == 'boss':
try:
handle.sendmail(username,boss_email, msg.as_string())
logging.info(u'发送邮件成功')
except Exception,e:
logging.error(e)
exit()

handle.close()
logging.info(u'发送邮件结束'+flag)

这里对邮件发件的部分需要特别仔细的错误捕获,然后记录进入日志中。

完整的代码见:https://github.com/kingname/MCC.git中的auto.py

总结

需要使用日志记录的地方大致有一下几处:

  • 所有输入输出处,无论是从文件输入还是从网络等其他地方输入
  • 执行命令处
  • 调用函数处

PS

这里我对一般信息的记录使用了info,实际上,一般用作调试的话,是使用debug更多。

需要用户输入的地方,总会有想不到的错误,多小心都不为过。例如,用户可能会把time_limit设定为一个全角数字。而本文中就没有捕获这种问题到日志中。所以如果不放心的话,还可以更进一步的细化日志。

在上一篇文章使用AWS亚马逊云搭建Gmail转发服务(二)中,我们已经介绍了如何把邮件转发程序部署在服务器上。但是这样还不够。还需要实时监控程序的运行状态。于是,给程序增加日志记录功能是非常重要的。

日志

这里使用Python的logging库,实现日志记录功能。

1
2
3
4
5
6
7
8
import logging

logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s %(filename)s %(levelname)s %(message)s',
datefmt='%Y %m %d %H:%M:%S', #日期格式:年月日时分秒
filename='mail_note.log', #文件名
filemode='a') #以最佳的方式添加日志
mail_log = logging.getLogger('maillog')

以上代码的作用是导入logging库功能。然后配置logging的输出格式。
各行代码的作用已经注释。

要使用日志的时候,通过以下代码:

1
logging.info('日志内容')

同类的还有:

1
2
3
4
mail_log.debug('内容')
mail_log.warning('内容')
mail_log.error('内容')
……

logging库的功能还有很多,这里只是简单的介绍一下,更多的功能可以查阅相关的资料。

Flask

现在日志已经生成。又如何通过Flask查看呢?由于我的前端不行。因此这里就不使用精细的模板了。Flask的部署就不叙述了,各位可以参考Flask官方文档http://dormousehole.readthedocs.org/en/latest/

这里我只演示一个非常简单的日志输出功能。编写gmail_flask.py,请看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#-*-coding:utf-8-*-
from flask import Flask
app = Flask(__name__)

@app.route('/')
def index():
f = open('mail_note.log','rb') #以读文件的方式打开mail_note.log文件
content = f.readlines()#按行读取日志
s = ''
for each in content:
s += each
s += '</p>'#输出日志
f.close()
return s

if __name__ == '__main__':
app.run(host='0.0.0.0') #开发外网访问

这个功能是把日志按行输出到网页上。
现在测试一下功能:
在终端窗口输入:

1
2
screen
python gmail_helper.py

然后Ctrl+A+D返回,再输入:

1
2
screen
python gmail_flask.py

然后访问服务器的5000端口查看效果。如图是我的服务器返回信息:

这里出现了Google的很多信息,这是由于Gmail的API库文件discovery.py里面也有用到日志功能。这个时候这里调用根logging,就会把discovery.py里面logging.info输出的信息写出来。这个时候怎么办呢?我对logging不是很熟悉,还请熟悉logging模块的朋友指点迷津。

我使用了一个变通的办法:

修改gmail_flask.py文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#-*-coding:utf-8-*-
from flask import Flask
app = Flask(__name__)

@app.route('/')
def index():
f = open('mail_note.log','rb') #以读文件的方式打开mail_note.log文件
content = f.readlines()#按行读取日志
s = ''
for each in content:
if 'gmail_helper.py' in each: #判定信息来自gmail_helper.py而不是discovery.py
s += each
s += '</p>'#输出日志
f.close()
return s

if __name__ == '__main__':
app.run(host='0.0.0.0') #开发外网访问

效果如下图:

源代码已更新到Github,请戳->https://github.com/kingname/MCC/blob/master/ghelper_with_log

我的日志会通过博客进行开放,地址请戳:

http://flask.kingname.info:5000

使用AWS亚马逊云搭建Gmail转发服务(二)中,我们最后运行了邮件转发程序。本以为程序就可以正常工作了,于是我关闭了Putty窗口。几个小时后回来,发现程序早就终止运行了。

原来,在一般情况下,当一个session结束时,这个session里面运行的进程也会同时结束。这可不能达到我们要的效果。于是screen命令登场了。

使用screen命令,可以让程序在断开session的时候继续运行。要打开screen,只需要在终端输入screen这个命令即可。请看下面演示:

1
2
3
cd wwwproject/ghelper
screen
python gmail_helper.py

这样就在一个screen里面运行了邮件转发程序。那么如何退出呢?

键盘上Ctrl+A+D三个键一起按。这样就返回到了进入screen之前的终端界面。而邮件转发程序仍然在后台默默的运行。现在可以关闭putty,然后放心的去睡觉了。

那重新SSH登录服务器以后,想关闭这个邮件转发程序怎么办?

两个方法:

方法一,直接结束Python进程。

方法二,在终端窗口输入:

1
screen -ls

终端窗口返回:

1
2
3
4
5

ubuntu@ip-172-31-15-35:~$ screen -ls
There is a screen on:
7956.pts-0.ip-172-31-15-35 (01/01/2015 12:16:10 PM) (Detached)
1 Socket in /var/run/screen/S-ubuntu.

注意这里的7956就是pid,于是输入:

1
screen -r 7956

就能回到Python的运行窗口了。于是,Ctrl+C结束程序运行。

有了screen命令,再也不怕关闭session后程序结束运行了。

在上一篇文章使用AWS亚马逊云搭建Gmail转发服务(一)中,我们介绍了如何在亚马逊AWS的免费主机EC2中使用Gmai API从而接收邮件的操作。在这篇文章中,将要讲解如何制作一个邮件转发服务。

我之前有写一篇文章,使用微信控制你的电脑其中有讲解如何使用Python的smtplib库实现发送邮件。于是Gmail邮件转发的思路就出来了:

程序定期检查Gmail邮箱,如果发现有新的邮件,就将新邮件的标题,发送人,还有邮件正文提取出来,并使用 MIMEText构造一个邮件的object 然后再用国内邮箱发送给自己的主邮箱。

这里涉及到三个邮箱:Gmail,国内邮箱一(发送),国内邮箱二(接收)。其中国内邮箱二是我的常用邮箱,我将它和微信绑定,因此一旦有新邮件,就会收到提醒。国内邮箱一是一个备胎邮箱,他的作用就是一个转发而已。

可能有人会问为什么不用Gmail直接转发?因为我觉得很有可能不久以后,Gmail发送的邮件,国内邮箱收不到。

那么我们将使用微信控制你的电脑这篇文章中涉及到的auto.py进行修改,编写一个ghelper_sender.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#-*-coding:utf-8 -*-

import smtplib
import sys
from email.mime.text import MIMEText

reload(sys)
sys.setdefaultencoding('utf-8')

username = "[email protected]"# 接收邮箱
password = "123abc"# 接收邮箱密码
mailbox = "[email protected]" #国内邮箱二

def send_mail(subject,body='Success'):
msg = MIMEText(body,'plain','utf-8')#中文需参数‘utf-8’,单字节字符不需要
msg['Subject'] = subject
msg['from'] = username
handle = smtplib.SMTP('smtp.sina.com', 25)
handle.login(username,password)
handle.sendmail(username,mailbox, msg.as_string())
handle.close()

这里需要注意,msg[‘from’] 的值必须是国内邮箱一,而不是发邮件到Gmail邮箱的那个地址,否则会报错。

现在打开上一篇文章中的ghelper_api.py,在最开头添加:

1
from ghelper_sender import send_mail

导入send_mail函数。

由于ghelper_api.py 中,上一篇文章中通过

1
2
a = ListMessagesWithLabels(gmail_service,'me')[0]['id']
b = GetMessage(gmail_service,'me',a)

获得你邮件的正文和发件人,现在再获取邮件的标题:

1
subject = b['payload']['headers'][12]['value']

这里我把发件人和邮件正文添加到一起,于是得到以下代码:

1
2
3
4
5
6
7
8
9
10
a = ListMessagesWithLabels(gmail_service,'me')[0]['id']
b = GetMessage(gmail_service,'me',a)
content = b['snippet']
sender = b['payload']['headers'][3]['value']
subject = b['payload']['headers'][12]['value']
body = sender + '\n' + content
send_mail(subject,body)
print subject
print body
print '============================'

这样就能发送一次邮件了。接收到的效果如图:


但是这显然不是我们需要的效果。我们希望这个程序能够自动运行,自动查看邮箱。于是,编写一个函数isnew()来检测是否有新的邮件发送过来,如果有就获取新邮件并发送。

设定一个全局变量last_email用于保存已读的最后一封邮件,令他的初始化为当前邮箱里面最新的一封邮件的id.接下来每过一段时间,程序就检查邮箱里面邮件的id,如果和last_email相同,就什么都不做;如果不相同,则程序就读取邮件,直到读取到某一封邮件的id和last_email相同为止。再令last_email的值为当前最新的id.代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def init():
global last_email
last_email = ListMessagesWithLabels(gmail_service,'me')[0]['id']

def isnew():
global last_email
themail = ListMessagesWithLabels(gmail_service,'me')
for each in themail:
if each['id'] != last_email:
tosend(each['id'])
else:
break
last_email = themail[0]['id']

def tosend(id):
b = GetMessage(gmail_service,'me',id)
content = b['snippet']
sender = b['payload']['headers'][3]['value']
subject = b['payload']['headers'][12]['value']
body = sender + '\n' + content
send_mail(subject,body)
print subject
print body
print '============================'

设定一个主函数,并把上面的代码放在主函数里面,并循环执行:

1
2
3
4
5
if __name__=='__main__':
init()
while 1:
time.sleep(3600) #每一小时检查一次邮箱
isnew()

完整的代码我放在了Github上面,请戳->https://github.com/kingname/MCC/tree/master/ghelper

在下一篇文章中,将介绍Python的logging库,并将每次的发送记录通过日志记录下来。同时用Flask搭建一个网站,从而方便直观的检查这个程序的运行情况。

故事背景

2014年12月28号开始,Gmail被伟大的墙从协议上封禁,POP3、SMTP、IAMP全部阵亡。于是不仅网页不能打开Gmail,连邮件客服端都不能使用Gmail收发邮件了。

Gmail在国内的用户相当的广泛,难道就真的不用了吗?当然不是。虽然使用VPN可以翻出长城,但是开着VPN做其他事情又不太方便。于是,一种Gmail的转发服务变得重要起来。

这篇文章将详细介绍如何使用亚马逊云AWS的免费主机EC2,配合Gmail的API来编写一个Gmail的转发程序。程序在设定时间内访问Gmail收件箱,发现新邮件以后,就通过另一个邮箱转发到国内邮箱中。每一次转发记录到一个日志文件中,并使用Flask搭建网站来,从而直观的检查接收发送记录。

AWS的免费主机EC2的申请不是本文的重点,网上有很多教程,故略去不讲。
Flask环境的搭建不是本文重点,网上有很多教程,故略去不讲。

本篇先讲解Gmail API的使用,下一篇讲解如何制作转发程序。

授权之路

既然要是用Gmail的API,那就要开通Gmail的授权。Google的官方英文教程请戳->Run a Gmail App in Python

打开Gmail API

访问https://console.developers.google.com/project,单击“建立档案”选项,新建一个项目。我这里新建的项目叫做“gmail”,如下图:

单击新建的档案“gmail”,在左侧点击“API和验证”,选择“API”,然后再右侧中间搜索框中输入Gmail,找到后打开。如下图:

然后点击左侧“凭证”,选择“建立新的用户端ID”

这个时候注意一定要选择第三项,才能正确生成json文件。选择第三项,并填写完一些信息后,做如下选择,并点击“建立用户端ID”

接下来,下载json文件。

验证机器

在服务器上新建ghelper文件夹:

1
2
mkdir ghelper
cd ghelper

然后安装Google API Python Client库。建议使用pip安装而不是easy_install,因为pip安装的库文件可以卸载,而easy_install安装的库文件不能卸载。

1
sudo pip install --upgrade google-api-python-client

为了使代码中的run.tools()能够正常执行,还需要安装gflags:

1
sudo pip install python-gflags

将json文件上传到AWS服务器上,我放在了~/wwwproject/ghelper目录下面,并且重命名为client_secret.json,这样代码就不需要进行修改了。同时在本目录下面新建ghelper_api.py文件,文件内容为官方指南中的验证机器的代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import httplib2

from apiclient.discovery import build
from oauth2client.client import flow_from_clientsecrets
from oauth2client.file import Storage
from oauth2client.tools import run

# Path to the client_secret.json file downloaded from the Developer Console
CLIENT_SECRET_FILE = 'client_secret.json'

# Check https://developers.google.com/gmail/api/auth/scopes for all available scopes
OAUTH_SCOPE = 'https://www.googleapis.com/auth/gmail.readonly'

# Location of the credentials storage file
STORAGE = Storage('gmail.storage')

# Start the OAuth flow to retrieve credentials
flow = flow_from_clientsecrets(CLIENT_SECRET_FILE, scope=OAUTH_SCOPE)
http = httplib2.Http()

# Try to retrieve credentials from storage or run the flow to generate them
credentials = STORAGE.get()
if credentials is None or credentials.invalid:
credentials = run(flow, STORAGE, http=http)

# Authorize the httplib2.Http object with our credentials
http = credentials.authorize(http)

# Build the Gmail service from discovery
gmail_service = build('gmail', 'v1', http=http)

# Retrieve a page of threads
threads = gmail_service.users().threads().list(userId='me').execute()

# Print ID for each thread
if threads['threads']:
for thread in threads['threads']:
print 'Thread ID: %s' % (thread['id'])

运行ghelper_api.py,进入Google验证阶段。

1
python ghelper_api.py

在红线处按回车键就可以进入输入模式。输入gmail和密码以后,移动光标到“Sign in”回车,然后进入如下页面:

输入你的信息,验证通过以后会让你进入开启浏览器的javascript功能。可是Linux服务器哪来的浏览器?这个时候按键盘的Ctrl + Z来取消。

继续输入:

1
python ghelper_api.py --noauth_local_webserver

会提示离线验证,如果仍然失败的话,就继续Ctrl+Z然后再输入上面的代码,很快就会让你离线验证:

复制他给出的网址,并在自己电脑上登录后,复制他给出的代码并粘贴回服务器上。验证通过。

使用API

打开API Reference,查看Gmail API的用法。

这里用Users.messages的list和get方法来演示API的使用。

先查看list的说明:

Lists the messages in the user’s mailbox.

列出邮箱里的信息。这里实际上列出来的是每一封邮件的id,于是,使用这个id,通过get就能获得邮件的内容。

通过查看list和get的使用范例:

list:
https://developers.google.com/gmail/api/v1/reference/users/messages/list
get:
https://developers.google.com/gmail/api/v1/reference/users/messages/get

构造出以下的完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#-*-coding:utf-8 -*-
import httplib2

from apiclient.discovery import build
from oauth2client.client import flow_from_clientsecrets
from oauth2client.file import Storage
from oauth2client.tools import run
from apiclient import errors
import base64
import email


# Path to the client_secret.json file downloaded from the Developer Console
CLIENT_SECRET_FILE = 'client_secret.json'

# Check https://developers.google.com/gmail/api/auth/scopes for all available scopes
OAUTH_SCOPE = 'https://www.googleapis.com/auth/gmail.readonly'

# Location of the credentials storage file
STORAGE = Storage('gmail.storage')

# Start the OAuth flow to retrieve credentials
flow = flow_from_clientsecrets(CLIENT_SECRET_FILE, scope=OAUTH_SCOPE)
http = httplib2.Http()

# Try to retrieve credentials from storage or run the flow to generate them
credentials = STORAGE.get()
if credentials is None or credentials.invalid:
credentials = run(flow, STORAGE, http=http)

# Authorize the httplib2.Http object with our credentials
http = credentials.authorize(http)

# Build the Gmail service from discovery
gmail_service = build('gmail', 'v1', http=http)

# Retrieve a page of threads
# threads = gmail_service.users().threads().list(userId='me').execute()

# # Print ID for each thread
# if threads['threads']:
# for thread in threads['threads']:
# print 'Thread ID: %s' % (thread['id'])

def ListMessagesWithLabels(service, user_id, label_ids=[]):
"""List all Messages of the user's mailbox with label_ids applied.

Args:
service: Authorized Gmail API service instance.
user_id: User's email address. The special value "me"
can be used to indicate the authenticated user.
label_ids: Only return Messages with these labelIds applied.

Returns:
List of Messages that have all required Labels applied. Note that the
returned list contains Message IDs, you must use get with the
appropriate id to get the details of a Message.
"""
try:
response = service.users().messages().list(userId=user_id,
labelIds=label_ids).execute()
messages = []
if 'messages' in response:
messages.extend(response['messages'])

while 'nextPageToken' in response:
page_token = response['nextPageToken']
response = service.users().messages().list(userId=user_id,
labelIds=label_ids,
pageToken=page_token).execute()
messages.extend(response['messages'])

return messages
except errors.HttpError, error:
print 'An error occurred: %s' % error

def GetMessage(service, user_id, msg_id):
"""Get a Message with given ID.

Args:
service: Authorized Gmail API service instance.
user_id: User's email address. The special value "me"
can be used to indicate the authenticated user.
msg_id: The ID of the Message required.

Returns:
A Message.
"""
try:
message = service.users().messages().get(userId=user_id, id=msg_id).execute()

print 'Message snippet: %s' % message['snippet']

return message
except errors.HttpError, error:
print 'An error occurred: %s' % error

a = ListMessagesWithLabels(gmail_service,'me')[0]['id']
b = GetMessage(gmail_service,'me',a)
print b['snippet']
print b['payload']['headers'][3]['value']

通过观察GetMessage返回的数据,可以看到,返回的是一个字典dict,邮件的内容在key为snippet的里面。发件人在[‘payload’][‘headers’][3][‘value’]里面,如图:

代码在服务器上运行效果如图:

至此,Gmail API在AWS服务器上的部署完成。下一篇文章将会介绍如何使用Python轮询Gmail的收件箱,并在有新邮件的时候转发到国内邮箱。

在看《Dive into Python》的单元测试时,发现用作例子的“阿拉伯数字-罗马数字”的转换算法非常的巧妙,现在发上来和大家分享一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
romanNumeralMap = (('M',1000),
('CM',900),
('D',500),
('CD',400),
('C',100),
('XC',90),
('L',50),
('XL',40),
('X',10),
('IX',9),
('V',5),
('IV',4),
('I',1))
def toRoman(n):
result = ""
for numeral, integer in romanNumeralMap:
while n >= integer:
result += numeral
n -= integer
return result

def fromRoman(s):
result = 0
index = 0
for numeral, integer in romanNumeralMap:
while s[index:index+len(numeral)] == numeral:
result += integer
index += len(numeral)
return result

print toRoman(1356)
print fromRoman('MCMLXXII')

这个算法的聪明之处,就在于他通过一个romanNumeralMap,把罗马数字与阿拉伯数字里面的“边界值”做出一一对应。这个边界刚刚好是罗马数字组合之间的转换。例如,I,II,III都可以通过第一个边界值组合获得;V,VI,VII,VIII可以通过V和I的组合获得。而对于一些特殊的值,则直接列出来。例如IV。通过这个边界值的组合,就能实现所需求的转换。这就类似于在一些机读卡上,需要填写1到100的数字,他会使用0,1,2,4,7这样以来:

1
2
3
4
5
3 = 1 + 2;
5 = 4 + 1;
6 = 4 + 2;
8 = 7 + 1;
9 = 7 + 2.

首先看一下toRoman()函数,把阿拉伯数字转换成罗马数字。它使用Python连接字符串的操作符号 + 来使“边界值”连接到一起。例如用作例子的n = 1356,程序遍历romanNumeralMap,寻找n对应的罗马数字,如果找不到,那就找刚刚比n小一点的数字对应的罗马字符。遍历在能使n 在romanNumeralMap有对应值时结束。

找到刚刚比1356小的那个值对应的罗马数字,也就是1000,M
再继续找刚刚比n = 1356 - 1000 = 356小的数,也就是100,C;
又继续找比n = 356 - 100 = 256小的数,还是100,也就是C;
再找比n = 256 - 100 = 156小的数,仍然是100,C;
继续找比n = 156 - 100 = 56 小的数,50,L;
继续找比n = 56 - 50 = 6小的数,5,V;
继续找n = 6 - 5 = 1对于的数,1,I。 结束。

所以1356对应的值为MCCCLVI。 这样的操作很类似于在十进制里面,一个数字1356 = 1000 + 300 + 50 + 6,只是阿拉伯数字里面6是一个单独的符号,而罗马数字里面VI是个V + I的组合而已。

下面再说说fromRoman()函数,把罗马数字转换成阿拉伯数字。这个函数在理解上面可能比toRoman()稍稍要困难一点。

还是用例子来说明,MCMLXXII转换成阿拉伯数字。
其中如下代码

1
s[index:index+len(numeral)]

作用是把字符串s中,从第index位到第index+ len(numeral)位(不包含第index + len(numeral)位自身)的字符提取出来。比如:

>>> a = 'helloworld'
>>> print a[2:5]
llo

即s的第2,3,4位被取出。

回到对s = ‘MCMLXXII’的处理。

首先map中第一个罗马字符是M,只有一位,就把s 的第0位拿出来对比,发现s的第0位刚刚好是M,于是得到一个1000,index变为1,则之后从s的第一位开始。简单的说,相当于s 变成了s = 'CMLXXII'

接下来,经过一些无效的值以后,轮换到CM,发现CM为两位,就取出s的前两位,也就是CM,发现在s中刚刚好有CM,于是得到900. index再加2,则实际上s就相当于变成了LXXII

继续经过一些无效值以后,轮换到了L,发现s当前的1位为L,于是在map中有对应的值50.然后index加1,s相当于变成了XXII

接下来到了X,发现s当前的1位为X,在map中有对应的值10.然后index 再加1,s变成了XII

虽然这个时候人已经知道是12了,但是计算机还是不知道,于是继续一个X,s变为II

然后出现一个I,s变为I

终于程序找到了一个直接相等的值I,于是转换结束。

所以MCMLXXII对于的阿拉伯数字是1000+900+50+10+10+1+1 = 1972

这个方法,把一个罗马数字从高位开始逐次剥离最高位,从而渐渐的把数字缩小。

最近正在学习算法。因为越来越发现现在做的东西,如果仅仅实现功能的话,性能会出现瓶颈。希望我以后能写出更好的算法。

A totally amazing!!!

源代码请戳->https://github.com/kingname/MCC

实际上使用任何可以发送邮件的东西都可以。但是因为微信比较普及,所以就用微信的发送邮件功能做一个测试吧~~

文件结构

程序由两部分构成:

  • _config.ini为配置文件,用于配置主人邮箱,奴隶邮箱和手工添加需要执行的命令
  • auto.py为程序的主体文件,相关的实现代码均在里面

软件原理

本程序需要使用两个邮箱,我给他们取名字为【主人邮箱】和【奴隶邮箱】。建议奴隶邮箱使用小号。主人邮箱使用大号,我是使用的我的QQ邮箱作为主人邮箱,临时申请的一个新浪邮箱作为奴隶邮箱。目前奴隶邮箱使用新浪邮箱测试通过,其他邮箱未做测试。各位有兴趣的朋友可以测试一下并反馈给我,非常感谢~

本程序使用Python的poplib提供的函数,周期性读取奴隶邮箱最新的一封邮件,如果这封邮件是主人邮箱发送的,并且标题在_config.ini文件中有定义,则执行本标题定义的操作。

例如,_config.ini文件中有如下定义:

music=D:\backup\Music\Intro.mp3

主人邮箱发送一份邮件,标题为music,电脑就会调用默认播放器,播放D盘中的这个名叫Intro.mp3的音乐。如果这个Intro.mp3本身只有1秒钟,且没有内容,而音乐播放器设置为随机播放,就间接地实现了打开播放器随机播放音乐的目的。

目前程序可以实现两类功能:
运行命令与打开文件。

运行命令

其中运行命令的原理是:

1
os.system(command)

理论上任何在CMD命令提示符下可以执行的命令,在这里都可以执行。_config.ini中默认提供了两个样例,一个关闭计算机:

1
shutdown=shutdown -f -s -t 10 -c closing...

另一个是列出当前目录:

1
dir=dir

等号左侧为此命令的名字,也就是在邮件中可以发送的标题内容,等号右侧为命令本身。注意等号左右均不能有空格。

打开文件

打开文件的原理是:

1
win32api.ShellExecute(0, 'open', open_file, '','',1)

其中,open_file为文件在电脑中的位置。函数调用Windows的API来运行程序,效果和用鼠标双击相同。

运行流程

程序运行以后,先加载_config.ini,配置主人邮箱和奴隶邮箱,并确定扫描频率(time_limit)为多少秒检查一次邮箱。同时使用字典将命令的名称和命令本身添加到内存中。接下来的操作如下:

使用主人邮箱发送相应的命令名称以后,就能触发电脑的相关操作。

程序配置

打开_config.ini文件:

  • host填写奴隶邮箱的pop3服务器,例如新浪的pop3服务器为

      pop.sina.com
    
  • username为奴隶邮箱的邮箱号

  • password为奴隶邮箱的密码

  • boss_email为主人邮箱号

  • time_limit控制程序检查邮箱的评论,默认为300秒,也就是5分钟

  • <command>与</command>之间为命令区,此处可以使用任何能在CMD命令提示符中执行的命令格式为:

      名字=命令
    

注意=左右不能出现空格

  • <open_file></open_file>之间为可以打开的文件。任何在电脑上可以使用鼠标双击打开的程序、文件均可把其地址写在此处。格式为:

     名字=地址
    

注意=左右不能出现空格

编译程序

使用py2exe编译。进入代码目测,执行以下代码:

python mysetup.py py2exe

Todo

接下来的版本升级中

  • 会添加更多的操作进去
  • 开发图像界面,使配置更方便
  • 动态调整检查频率
  • 通过邮件的内容返回命令的执行状态
  • 通过邮件内容返回文件列表
  • 解决打开的文件功能在文件名和路径不能有汉字的bug

致谢

感谢知乎用户 @印如意fitz的启发与思路提供。

在Python的正则表达式中,有一个参数为re.S。它表示“.”(不包含外侧双引号,下同)的作用扩展到整个字符串,包括“\n”。看如下代码:

1
2
3
4
5
6
7
8
9
import re
a = '''asdfsafhellopass:
234455
worldafdsf
'''
b = re.findall('hello(.*?)world',a)
c = re.findall('hello(.*?)world',a,re.S)
print 'b is ' , b
print 'c is ' , c

运行结果如下:

1
2
b is  []
c is ['pass:\n\t234455\n\t']

正则表达式中,“.”的作用是匹配除“\n”以外的任何字符,也就是说,它是在一行中进行匹配。这里的“行”是以“\n”进行区分的。a字符串有每行的末尾有一个“\n”,不过它不可见。

如果不使用re.S参数,则只在每一行内进行匹配,如果一行没有,就换下一行重新开始,不会跨行。而使用re.S参数以后,正则表达式会将这个字符串作为一个整体,将“\n”当做一个普通的字符加入到这个字符串中,在整体中进行匹配。

在re.py库的介绍中有以下语句:

“.” Matches any character except a newline.

S DOTALL “.” matches any character at all, including the newline.

这里特别感谢评论中叫做Style的朋友指出了我的错误。

0%