发送带有数据框的电子邮件作为附件

Sending email with dataframe as an attached file

我正在尝试创建一个可以发送电子邮件的功能,该电子邮件带有作为 csv 文件附加的数据框。附加文件通常需要先将文件保存到磁盘,所以我不知道是否有任何直接的方法可以解决这个问题?

我已经创建了一个可以将数据帧附加为 HTML 的函数,还有一个可以将附件作为电子邮件发送的函数,但是没有可以直接将数据帧作为附件发送的函数

常规设置

from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import smtplib
import os
_server = 'MY_SERVER'
_port = 9999
_sender = 'my.email@domain.com'

def create_simple_mail(to, title):
    mail = MIMEMultipart()
    mail['Subject'] = title
    mail['To'] = to
    mail['From'] = _sender

发送数据帧为html

def mail_dataframe_as_html(to, msg, title, df):
    mail = create_simple_mail(to, msg, title)
    html_str = msg
    html_str += '<tr></tr><tr></tr>'
    html_str += df.to_html()
    mail.attach(MIMEText(html_str, 'html'))
    smtp_connection = smtplib.SMTP(_server, _port, timeout=120)
    smtp_connection.sendmail(_sender, to, mail.as_string())

发送附件

def attach_file_to_mail(mail,f):
    with open(f, "rb") as fil:
        part = MIMEApplication(fil.read(), Name=os.path.basename(f))
        part['Content-Disposition'] = 'attachment; filename="%s"' % os.path.basename(f)
        mail.attach(part)
    return mail

def mail_html(to, title, html, attachments=None):
    mail = create_simple_mail(to=to, msg=None, title=title)
    mail.attach(MIMEText(html, 'html'))
    if attachments is not None:
        for f in attachments:
            mail = attach_file_to_mail(mail,f)
    smtp_connection = smtplib.SMTP(_server, _port, timeout=120)
    smtp_connection.sendmail(_sender, to, mail.as_string())

试试 pandas.DataFrame.to_csv
示例。
发送带有 pandas 数据框的邮件作为 .csv 附件:

from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import smtplib
import os
import pandas as pd
from datetime import datetime

_server = 'smtp.example.com'
_port = 587
_sender = 'some_sender@example.com'
_pass = 'pass_value'

def create_simple_mail(to, title):
    mail = MIMEMultipart()
    mail['Subject'] = title
    mail['To'] = to
    mail['From'] = _sender
    return mail

def attach_file_to_mail(mail,f):
    with open(f, "rb") as fil:
        part = MIMEApplication(fil.read(), Name=os.path.basename(f))
        part['Content-Disposition'] = 'attachment; filename="%s"' % os.path.basename(f)
        mail.attach(part)
    return mail

def mail_html(to, title, html, attachments=None):
    mail = create_simple_mail(to=to, title=title)
    mail.attach(MIMEText(html, 'html'))
    if attachments is not None:
        for f in attachments:
            mail = attach_file_to_mail(mail,f)
    
    smtp_connection = smtplib.SMTP(_server, _port, timeout=120)
    

    # I tested with TLS server connection
    smtp_connection.ehlo()
    smtp_connection.starttls()
    smtp_connection.ehlo()
    smtp_connection.login(_sender, _pass)

    smtp_connection.sendmail(_sender, to, mail.as_string())


if __name__ == "__main__":

    df_data = {'num': [1, 2, 3],
        'name': ['some val 1','some val 2','some val 3'],
        'year': [2001, 2002, 2003],
        
        }

    df = pd.DataFrame(df_data, columns = ['num', 'name','year'])

    now = datetime.now() 
    date_time = now.strftime("%d_%m_%Y__%H_%M_%S")
    file_name = f'{date_time}.csv'
    df.to_csv(file_name, index = False)

    mail_html('some@example.com','Some title','<b>html text</b>',[file_name])

    # If need
    os.remove(file_name)

结果:

我想出了一个非常巧妙的解决方案,使用 __enter____exit__ 来清理可以附加到电子邮件的临时生成文件。

import os
import uuid

class CreateAttachments:
    def __init__(self, dfs, file_names=None):
        self._attachments = dfs

        # Create generic file names if none was specified
        if file_names is None:
            self._names = [f'attached_df{i}.csv' for i in range(len(dfs))]

        # Ensure all file names ends with the .csv extension
        else:
            self._names = [fn if fn.endswith('.csv') else fn + '.csv' for fn in file_names]

            # If less file names than attachments were provided, generic names are appended
            if len(self._names) < len(self._attachments):
                self._names += [f'attached_df{i}.csv' for i in range(len(self._attachments) - len(self._names))]

        # Create a temporary folder for the files
        self._file_location = 'C:/Users/%s/%s/' % (os.getenv('username'), str(uuid.uuid4()))

    def __enter__(self):
        # If by the odd chance the temporary folder already exists, we abort the program
        if os.path.isdir(self._file_location):
            raise IsADirectoryError("Directory already exists. Aborting")

        # Create temporary folder
        os.makedirs(self._file_location)

        # For each attachment, save the dataframe to a .csv in the temporary folder
        attach_paths = []
        for attach, name in zip(self._attachments, self._names):
            tmp_path = self._file_location + name
            attach.to_csv(tmp_path)
            attach_paths.append(tmp_path)
        
        # Save all paths to class, as they are needed in the __exit__ function
        self.paths = attach_paths
        
        # Return paths to use for attachments
        return self.paths

    def __exit__(self, exc_type, exc_val, exc_tb):
        # Remove the .csv files for each attachment
        for path in self.paths:
            os.remove(path)
        
        # Remove the temporary folder
        os.removedirs(self._file_location)

使用上面的代码,我现在可以像这样创建附件:

import pandas as pd

df1 = pd.DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
df2 = pd.DataFrame([[2, 4, 6], [3, 6, 9], [4, 8, 12]])

html_str = '''<table style="width:100%">'''
html_str += '<tr> <font size="4"><b>Some HTML example</b> </font size> </tr>'
html_str += '<br> This is a test mail.'
with CreateAttachments([df1, df2]) as f:
    mail_html(to='receiver@domain.com',
              html=html_str,
              title='Test email with attachments',
              attachments=f)