朋友遇到一点麻烦,我自告奋勇帮忙。事情是这样的:
- 他们的业务系统中,数据来自一个邮箱;
- 每一个邮件包含一条记录;
- 这些记录是纯文本的,字段之间由一些特殊字符分隔;
- 他们需要从邮箱中批量取出每一封邮件,放到一个excel文件中。
这些对python来说,真是小菜一碟。(事后证明,还是有些小坑,让我头疼了好一会儿。)
因为是初学者,没有必要从python2起步,我直接用了python3。
首先是收信。邮箱不支持pop3取信,好在支持IMAP。查了一下,python3有专门的库可以做到。
然后是要用正则表达式处理文本。
生成excel需要用到什么什么第三方库,找了一下,没下下来。干脆就简单点,生成csv文件吧。
==============
1 def main(): 2 M = imaplib.IMAP4_SSL("my-host.com","993") 3 t=0 4 try: 5 try: 6 M.login(‘my-username‘,‘my-password‘) 7 except Exception as e: 8 print(‘login error: %s‘ % e) 9 M.close() 10 11 M.select(‘INBOX‘,False) 12 13 # result, message = M.select() 14 # tips: 如果想找Essh邮件的话,使用 15 # type, data = M.search(None, ‘(SUBJECT "Essh")‘) 16 # 里面要用一个括号,代表是一个查询条件,可以同时指定多个查询条件,例如FROM xxxx SUBJECT "aaa", 17 # 注意,命令要用括号罩住(痛苦的尝试) 18 typ, data = M.search(None, ‘ALL‘) 19 20 msgList = data[0].split() 21 print("total mails:" + str(len(msgList))) 22 last = msgList[len(msgList) - 1] 23 # first = msgList[0] 24 # M.store(first, ‘-FLAGS‘, ‘(\Seen)‘) 25 # M.store("1:*", ‘+FLAGS‘, ‘\\Deleted‘) #Flag all Trash as Deleted 26 output=PATH+‘\output.csv‘ 27 fp2=open(output, ‘w‘) 28 29 last_id=read_config() 30 count=0 31 for idx in range(int(last_id), len(msgList)): 32 print("curr id: "+str(idx)+‘\n‘) 33 type,data=M.fetch(msgList[idx],‘(RFC822)‘) 34 deal_mail(data, fp2) 35 count=count+1 36 if count>500: 37 break 38 39 write_config(idx) 40 # print(str(idx)) 41 print("OK!") 42 M.logout() 43 except Exception as e: 44 print(‘imap error: %s‘ % e) 45 M.close()
这是main()部分。主要是连接IMAP服务器、取信、调用处理函数。
我发现,IMAP提供的接口比较怪异。不管怎么说,没怎么掉坑,网上的资料都很齐全。关于搜索的语法以及删除和置为已读/未读的命令都放在注释里。
这里面的逻辑是:首先获取上次处理的序号last_id,从此处开始,处理500条信件。然后将新的last_id写入配置文件中,下次读取。
在写这个程序的时候,遇到的最多的麻烦是关于str和bytes类型的。因为网上许多代码都是来自python2,所以在python3中就遇到多次提示:
cannot use a string pattern on a bytes-like object
write() argument must be str, not bytes
error: a bytes-like object is required, not ‘str‘
error: string argument without an encoding
error: cannot use a string pattern on a bytes-like object
等等。。。一个头两个大。比如,我要把半角逗号替换成全角逗号,这个最简单不过的功能,就试了半天:
这个错:
content=content.replace(‘,‘, ‘,‘)
error: a bytes-like object is required, not ‘str‘
这个也错:
content=content.replace(‘,‘, bytes(‘,‘))
error: string argument without an encoding
最终这个才对了:
content=content.replace(bytearray(‘,‘,‘GBK‘), ‘,‘.encode(‘GBK‘))
可是当我继续要把半角双引号变成全角双引号时,情况又不一样:
matchObj = re.match( r‘.*<body>(.*)</body>.*‘, content.decode(‘GBK‘), re.M|re.I|re.S) if matchObj: found=matchObj.group(1) #邮件正文 aa=found.split(‘#$‘) #分解为一个个field aa[9]=aa[9].replace(‘"‘, ‘“‘) #我靠前面的写法用不着了! content=content.replace(bytearray(‘,‘,‘GBK‘), ‘,‘.encode(‘GBK‘))
我汗。。。总之肯定里面有什么东西还不大明白,导致走了许多弯路。记录下来,利已助人吧。
以下是全部源码
#-*- coding:UTF-8 -*- import imaplib, string, email import os import re CONFIG_FILE=‘last_id.txt‘ PATH=os.path.split(os.path.realpath(__file__))[0] def main(): M = imaplib.IMAP4_SSL("my-host.com","993") t=0 try: try: M.login(‘my-username‘,‘my-password‘) except Exception as e: print(‘login error: %s‘ % e) M.close() M.select(‘INBOX‘,False) # result, message = M.select() # tips: 如果想找Essh邮件的话,使用 # type, data = M.search(None, ‘(SUBJECT "Essh")‘) # 里面要用一个括号,代表是一个查询条件,可以同时指定多个查询条件,例如FROM xxxx SUBJECT "aaa", # 注意,命令要用括号罩住(痛苦的尝试) typ, data = M.search(None, ‘ALL‘) msgList = data[0].split() print("total mails:" + str(len(msgList))) last = msgList[len(msgList) - 1] # first = msgList[0] # M.store(first, ‘-FLAGS‘, ‘(\Seen)‘) # M.store("1:*", ‘+FLAGS‘, ‘\\Deleted‘) #Flag all Trash as Deleted output=PATH+‘\output.csv‘ fp2=open(output, ‘w‘) last_id=read_config() count=0 for idx in range(int(last_id), len(msgList)): print("curr id: "+str(idx)+‘\n‘) type,data=M.fetch(msgList[idx],‘(RFC822)‘) deal_mail(data, fp2) count=count+1 if count>500: break write_config(idx) # print(str(idx)) print("OK!") M.logout() except Exception as e: print(‘imap error: %s‘ % e) M.close() def main2(): path=os.path.split(os.path.realpath(__file__))[0] input=path+‘\input2.txt‘ output=path+‘\output.csv‘ fp=open(input, ‘rb‘) fp2=open(output, ‘w‘) if True: line=fp.read() pharse_content(fp2, line) def get_mime_version(msg): if msg != None: return email.utils.parseaddr(msg.get(‘mime-version‘))[1] else: empty_obj() def get_message_id(msg): if msg != None: return email.utils.parseaddr(msg.get(‘Message-ID‘))[1] else: empty_obj() # 读config文件,获取上次最大id,从这个id开始读邮件 def read_config(): if os.path.isfile(PATH+"\\"+CONFIG_FILE): _fp=open(PATH+"\\"+CONFIG_FILE) id=_fp.read() _fp.close() else: id=0 return id # 将本次处理的邮件的最大id写入config,以便下次读取 def write_config(id): _fp=open(PATH+"\\"+CONFIG_FILE, ‘w‘) _fp.write(str(id)) _fp.close() def deal_mail(data, fp2): msg=email.message_from_string(data[0][1].decode(‘GBK‘)) messageid = get_message_id(msg) print(messageid) content=msg.get_payload(decode=True) #print(content) pharse_content(fp2, content, messageid) def pharse_content(fp2, content, messageid): #将半角的 , 换成全角的 , # content=content.replace(‘,‘, ‘,‘) # error: a bytes-like object is required, not ‘str‘ # content=content.replace(‘,‘, bytes(‘,‘)) # error: string argument without an encoding content=content.replace(bytearray(‘,‘,‘GBK‘), ‘,‘.encode(‘GBK‘)) # print(content.decode(‘GBK‘)) # strinfo=re.compile(‘,‘) # content=strinfo.sub(‘,‘, content) # error: cannot use a string pattern on a bytes-like object matchObj = re.match( r‘.*<body>(.*)</body>.*‘, content.decode(‘GBK‘), re.M|re.I|re.S) if matchObj: found=matchObj.group(1) #邮件正文 aa=found.split(‘#$‘) #分解为一个个field # 获取申诉涉及号码。匹配模式:申诉问题涉及号码:18790912404; mobileObj=re.match(r‘.*申诉问题涉及号码:(.*);‘, aa[9], re.M|re.I|re.S) if mobileObj: mobile=mobileObj.group(1) else: mobile=‘‘ # bb 是结果数组,对应生成的csv文件的列 aa[9]=aa[9].replace(‘"‘, ‘“‘) #我靠前面的写法用不着了! content=content.replace(bytearray(‘,‘,‘GBK‘), ‘,‘.encode(‘GBK‘)) bb=[‘‘]*40 #40个元素的数组,对应40个列 bb[3]=aa[0] #D列 bb[4]=aa[4] #E bb[5]=mobile #F bb[6]=aa[5] #G bb[7]=aa[2] #H bb[8]=aa[1] #I bb[9]=aa[3] #J bb[11]=aa[6] #L bb[12]=aa[6] #M bb[22]=‘网站‘ #W 申诉来源。此处可自行修改为指定类型 bb[36]=‘"‘+aa[9]+‘"‘ #AK,两侧加 "" 是为了保证多行文字都放进一个单元格中 DELI=‘,‘ # fp2.write("AAAAA,"+DELI.join(bb)+"\\n") fp2.write(DELI.join(bb)+"\n") else: print("No match!!") main()