返回介绍

数据报表

发布于 2025-04-20 18:52:17 字数 10796 浏览 0 评论 0 收藏

Web 开发者的日常工作之一是给运营、产品、客服以及开发团队创建一些数据报表,本节将展示 3 个有用的例子。

发送带有样式和附件的邮件

常见需求之一是给运营和产品同事定期发送一些用于运营和产品决策的统计邮件。邮件通常是以表格为主体,一般也会带有一个或者多个附件。数据报表中的数据种类太多的话,如果不格式化处理,读起来很痛苦,也不容易从中获取有用信息。其实我们可以让邮件带有 CSS 样式,让读邮件也成为一件愉快的事情。

现在有这样一个需求:运营人员维护了一些核心用户池(如发布东西图文、海淘经验等),希望每周一看到上一周这些核心用户的一些数据。下面的例子展示了 10 个发布图文的核心用户上周每天的发布量(email_with_mako.py),它做了如下 4 件事:

  • 把数据写进 csv 文件。
  • 使用 Mako 把数据渲染成邮件内容。
  • 生成邮件。
  • 发送带附件的邮件。

我们先定义用到的假数据和 main 函数:

rows_data=[
    [34, 72, 38, 30, 75, 48, 75],
    [6, 24, 1, 84, 54, 62, 60],
    [28, 79, 97, 13, 85, 93, 93],
    [27, 71, 40, 17, 18, 79, 90],
    [88, 25, 33, 23, 67, 1, 59],
    [24, 100, 20, 88, 29, 33, 38],
    [6, 57, 88, 28, 10, 26, 37],
    [52, 78, 1, 96, 26, 45, 47],
    [60, 54, 81, 66, 81, 90, 80],
    [70, 5, 46, 14, 71, 19, 66],
]
col_headers=['日期', '周一', '周二', '周三',
            '周四', '周五', '周六', '周日']
row_headers=['用户{}'.format(i) for i in range(1, 11)]
     
def main():
    csv_file=os.path.join(HERE, 'statistics.csv')
    tmpl_directories=[os.path.join(HERE, 'tmpl')]
    write_csv(csv_file, col_headers, rows_data)
    data={'rows_data':rows_data, 'row':col_headers,
         'row_headers':row_headers}
    content=mako_render(data, 'statistics.txt', directories=tmpl_directories)
    sendmail(content, '核心用户运营数据', [csv_file], nick_from='豆瓣网')

现在开始分解 main 函数中未实现的功能。先看生成 csv 附件的函数:

import csv
    
    
def write_csv(csv_file, headers, rows):
    f=open(csv_file, 'wt')
    writer=csv.writer(f)
    writer.writerow(headers)
    for index, row in enumerate(rows):
        writer.writerow([row_headers[index]]+row)
    f.close()

再看使用 Mako 模板渲染的函数:

from mako.template import Template
from mako.lookup import TemplateLookup
    
def mako_render(data, mako_file, directories=['tmpl']):
    mylookup=TemplateLookup(directories=directories, input_encoding='utf-8',
                           output_encoding='utf-8',
                           default_filters=['decode.utf_8'])
    mytemplate=Template('<%include file="{}"/>'.format(mako_file),
                       lookup=mylookup, input_encoding='utf-8',
                       default_filters=['decode.utf_8'],
                       output_encoding='utf-8')
    content=mytemplate.render(**data)
    return content

Mako 默认会把传入的字符串作为函数 unicode 的参数执行,由于文本中有中文,会出现 UnicodeDecodeError 错误,所以一定要使用“default_filters=['decode.utf_8']”。

接下来引入构造和发送邮件的模块:

import smtplib
from email.header import Header as_Header
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.utils import parseaddr, formataddr

其中 email 负责构造邮件,smtplib 负责发送邮件。还需要定义发送邮件相关的常量:

SMTP_SERVER='smtp.qq.com'
SMTP_PORT=587
FROM_ADDR='xxx@qq.com'
PASSWORD='password'
TO_ADDRS=['yy@gmail.com']

使用标准的 25 端口连接 SMTP 服务器是明文传输,发送过程中可能会被窃听。推荐选择 STARTTLS 协议的 587 端口,它能够让明文的通信连接直接成为加密连接(使用 SSL 或 TLS 加密)。

构造一个邮件通常需要定义发送者(From)、接收者(To)、主题(Subject),以及附件(可以有多个),所以构造邮件的函数比较复杂:

def gen_msg(content, subject, attachments, nick_from=None, nick_to='运营'):
    if nick_from is None:
       nick_from=FROM_ADDR
    msg=MIMEMultipart()
    msg['From']=_format_addr('{}<{}>'.format(nick_from, FROM_ADDR))
    msg['To']=_format_addr('{}<{}>'.format(nick_to, TO_ADDRS))
    msg['Subject']=Header(subject)
    msg.attach(MIMEText(content, 'html', 'utf-8'))
    
    for attachment in attachments:
        attach=MIMEText(open(attachment, 'rb').read(), 'base64', 'utf-8')
        attach['Content-Type']='application/octet-stream'
        attach['Content-Disposition']='attachment;filename="{}"'.format(
            os.path.basename(attachment))
        msg.attach(attach)
    return msg
    
    
def Header(name):# noqa
    return_Header(name, 'utf-8').encode()
    
    
def _format_addr(s):
    name, addr=parseaddr(s)
    return formataddr((Header(name), addr))

上面使用了_format_addr 格式化邮件地址来支持中文<addr@example.com>这样的带有昵称的邮件地址。用函数 Header 伪装成 Header 类后就可以使用 Header(name) 代替较长的 Header(name, 'utf-8').encode() 了。

最后一步就是发送邮件了:

def sendmail(content, subject, attachments, nick_from=None):
    msg=gen_msg(content, subject, attachments, nick_from)
    server=smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
    server.starttls()
    server.login(FROM_ADDR, PASSWORD)
    server.sendmail(FROM_ADDR, TO_ADDRS, msg.as_string())
    server.quit()

现在各邮件服务商对邮件的使用样式管得比较严,最开始在邮件的 HTML 内容中加 link 标签就可以,现在这种方式普遍不可用了。另外的方法是把样式文件当作附件,这样就可以在邮件内容中引用附件来显示样式,缺点是附件中多出来的样式文件,会让需求方很迷惑。本例是直接把样式写进了 HTML 的 style 标签中,为了节省篇幅,使用 Bootstrap 框架的样式来演示,现在已经下载了 bootstrap.min.css,但实际工作中不应该把整个 Bootstrap 样式文件都插入到内容中,这会增加差不多 100 KB 的内容。应该只保留少量用到的样式。

邮件模板使用 include 添加 CSS 样式:

<style>
    <%include file="bootstrap.min.css"/>
</style>
<table class="table table-bordered table-hover">
    <tr>
    % for column in row:
      <th>${column}</th>
    % endfor
    % for index, rows in enumerate(rows_data):
       <tr>
      % for column in [row_headers[index]]+rows:
          <td>${column}</td>
      % endfor
      </tr>
    % endfor
</table>

发送一封邮件后,在 Mac 自带的邮件客户端 Mail 里就可以看到收到的邮件的效果了,如图 11.1 所示。

图 11.1 收到的邮件

创建 xlsx 文件

.xlsx 是 Microsoft Office Excel 从 2007 版本开始使用的文档扩展名。除非报表中只包含简单的字符串和数字,否则不建议使用 csv 格式,因为虽然 csv 格式对开发者友好也容易实现,但是 csv 对于使用 Windows 系统的非技术人员来说并不友好,常见的就是乱码问题。本节将向大家展示创建两个复杂的 xlsx 文件例子。

创建 xlsx 时使用了 XlsxWriter,我们先安装它:

> pip install XlsxWriter

包含带样式和 Sparkline 图表

从 Excel 2010 开始,Excel 增加 Sparkline 功能。它是一种信息体积小、数据密度高的图表,是研究数据走势的一个重要依据。本例将使用上例的需求和假数据来完成(core_user_report.py):

from__future__import unicode_literals
import xlsxwriter
    
from email_with_mako import rows_data, col_headers, row_headers
    
# 创建一个叫作 core_user_report.xlsx 的文件
workbook=xlsxwriter.Workbook('core_user_report.xlsx')
# 添加一个工作表,名字为“核心用户”
worksheet=workbook.add_worksheet('核心用户')
# add_format 用来创建样式,支持的样式列表详见
bold=workbook.add_format({'bold':True, 'align':'center'}) http://xlsxwriter.
    readthedocs.org/format.html
    
format1=workbook.add_format({'bg_color':'#FFC7CE',
                           'font_color':'#9C0006'})
format2=workbook.add_format({'bg_color':'#C6EFCE',
                           'font_color':'#006100'})
    
for index, header in enumerate(row_headers, 2):
    col='A{}'.format(index)
    worksheet.write(col, header, bold) #设置竖轴
    
for row, row_data in enumerate(data):
    worksheet.write_row(row+1, 1, row_data)
# 条件格式化,在左上角 B2 到右下角 H11 的区域内,如果值大于等于 50,使用样式 format1,
    否则使用样式 format2
worksheet.conditional_format('B2:H11',{'type':'cell',
                                       'criteria':'>=',
                                       'value':50,
                                       'format':format1})
worksheet.conditional_format('B2:H11',{'type':'cell',
                                       'criteria':'<',
                                       'value':50,
                                       'format':format2})
# I 列是 Sparkline 图表所在的列,加大它的宽度
worksheet.set_column('I:I', 20)
for i in range(2, 12):
    # 根据每一排的 7 天数据生成 Sparkline 图表
    worksheet.add_sparkline(
        'I{}'.format(i),
        {'range':'核心用户!B{0}:H{0}'.format(i), 'markers':True,
        'series_color':'#E965E0'})
workbook.close() # 需要显式地关闭 workbook

执行脚本后在当前目录下就生成了 core_user_report.xlsx 文件。但由于 Mac 自带的 Numbers 不支持 Sparkline 功能,会忽略 Sparkline 图表,图 11.2 所示的是从 Windows 上打开文件的效果。

图 11.2 从 Windows 上打开文件的效果

因为 xlsxwriter 要求值的类型是 unicode,在一开始使用了“from__future__import unicode_literals”,这样相当于自动把字符串转换为 unicode。

包含多工作表和图表

下面的例子将统计上周每天一些活动页面的 PV/UV,两种指标放在不同工作表中,并分别生成折线图和柱状图(pv_uv.py)。

from__future__import unicode_literals
    
import xlsxwriter
    
workbook=xlsxwriter.Workbook('pv_uv.xlsx')
    
worksheet1=workbook.add_worksheet('pv')
worksheet2=workbook.add_worksheet('uv')
# 这是创建的第三个工作表,只为演示,没有数据
worksheet3=workbook.add_worksheet('analysis')
    
chart1=workbook.add_chart({'type':'line'}) # PV 使用折线图
chart2=workbook.add_chart({'type':'column'}) # UV 使用柱状图
   
worksheet1.set_tab_color('#4271ae') # 设置工作表的 Tab 颜色
worksheet2.set_tab_color('#c82829')
worksheet3.set_tab_color('green')
   
bold=workbook.add_format({'bold':1})
    
pv_data=[[20233, 27855, 30126, 22737, 23331, 34791, 18075],
         [31001, 34483, 33221, 29448, 27082, 31534, 18035],
         [30771, 34543, 30001, 26257, 30778, 22168, 27469],
         [34605, 31545, 26359, 32073, 29603, 32025, 32674],
         [24035, 32267, 19562, 24721, 19573, 31712, 28171]]
    
uv_data=[[15509, 13787, 14492, 14093, 14008, 10630, 10363],
         [11727, 15526, 15865, 12235, 14798, 10056, 11561],
         [12699, 13125, 15009, 9606, 9555, 17222, 17231],
         [16110, 13798, 10435, 11363, 11862, 10981, 10113],
         [9306, 17165, 9803, 14932, 13226, 13047, 17671]]
    
cols=['专题页{}'.format(i) for i in range(1, 6)]
headings=['专题页', '周一', '周二', '周三', '周四', '周五', '周六', '周日']
     
for sheet, data in ((worksheet1, pv_data),
                    (worksheet2, uv_data)):
    sheet.write_row('A1', headings, bold)
    sheet.write_column('A2', cols, bold)
    for index, row in enumerate(data, 2):
    sheet.write_row('B{}'.format(index), row)
   
for sheet, chart in ((worksheet1, chart1),
                    (worksheet2, chart2)):
    for index in range(1, 6):
        chart.add_series({ # 给图表添加数据
             'name':'活动页{}'.format(index),
             'categories':'={}!$B$1:$H$1'.format(sheet.name),
             'values':'={0}!$B${1}:$H${1}'.format(
                 sheet.name, index+1),
        })
   
chart1.set_title({'name':'上周活动页 PV'}) # 设置标题
chart1.set_x_axis({'name':'PV'}) # 设置 x 轴
chart1.set_y_axis({'name':'数量'}) # 设置 y 轴
chart1.set_style(10) # Excel 2007 开始包含了默认的 48 种 Chart 样式,效果可参见
     chart_styles.xlsx
    
worksheet1.insert_chart('A8', chart1,{'x_offset':25, 'y_offset':10})
    
   
chart2.set_title({'name':'上周活动页 UV'})
chart2.set_x_axis({'name':'UV'})
chart2.set_y_axis({'name':'数量'})
chart2.set_style(42)
worksheet2.insert_chart('A8', chart2,{'x_offset':25, 'y_offset':10})
    
workbook.close()

上面的 categories 都使用了“={}!$B$1:$H$1'.format(sheet.name)”,相当于寻找工作表中对应坐标或者坐标集合的值,也就是 B1-H1 这 7 个坐标的结果。

效果图如图 11.3 和图 11.4 所示。

图 11.3 效果图 1

图 11.4 效果图 2

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。