说到自动化报告,很容易想起R里面的knitrknitr很强大,可以在报告模板中嵌入源代码,编译后就可以生成一份报告,而且还支持LaTeX,LyX,HTML,Markdown以及reStructuredText。不过我一直没有用过,毕竟在我想弄自动化报告的时候,想到的不是knitr而是Jinja2。

在生信的标准化分析当中,分析完成后都需要提供一份分析报告。之前很多这种报告都是人手工写的,不仅容易出错而且还特别费时间,而且类似word和pdf这种格式的报告,不方便展示动态的可视化内容。后来有许多公司出现了HTML格式的报告,这类报告一些都是自动生成的。可能不同公司生成报告的方法都不大一样,这里我想介绍下我是怎么用Jinja2来生成HTML报告的。

Jinja2简介

Jinja2其实是Python中的一种模板语言,上手迅速,渲染速度快。有下面几个特性: - 沙箱中执行 - 强大的 HTML 自动转义系统保护系统免受 XSS - 模板继承 - 及时编译最优的 python 代码 - 可选提前编译模板的时间 - 易于调试。异常的行数直接指向模板中的对应行。 - 可配置的语法

官方罗列出的一些特性,我看着也费劲。其实我的总结就是简单灵活,能识别Python代码。

在编写模板的时候,我经常用到Jinja2下面的一些API - 变量 - 控制结构-if for macro block include - 模板继承

下面这个是base.html的内容,里面规定了HTML的基本框架,可以说是所有模板的模板,后面其他模板的生成了,可以直接引用里面的框架进行填充。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html lang="en">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    {% block head %}
    <link rel="stylesheet" href="style.css" />
    <title>{% block title %}{% endblock %} - My Webpage</title>
    {% endblock %}
</head>
<body>
    <div id="content">{% block content %}{% endblock %}</div>
    <div id="footer">
        {% block footer %}
        &copy; Copyright 2008 by <a href="http://domain.invalid/">you</a>.
        {% endblock %}
    </div>
</body>

上面的{% block %} ... {% endblock%}定义了4个block,这些block可以在子模板中重新定义,可以看到block也是可以嵌套的。

子模板chirld.html

{% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block head %}
    {{ super() }}
    <style type="text/css">
        .important { color: #336699; }
    </style>
{% endblock %}
{% block content %}
    <h1>Index</h1>
    <p class="important">
      Welcome on my awesome homepage.
    </p>
    <!-- if 示例-->
    {% if page > 2 %}    
    <p>This is not the first page.</p>
    {% endif %}

    <!-- for 示例 -->
    <ul>
    {% for user in users %}
      <li>{{ user.username }}</li>  <!-- 这里 {{ variable }} 表示引用的变量 -->
    {% endfor %}
    </ul>

    <!-- 引用示例 -->
    {% include "child2.html" %}

{% endblock %}

子模板中首行先是用{% extends "base.html" %}}继续了base.html模板中的内容。之后对父模板中的block进行填充, 不过我需要注意下{% block head %}里面有个{{ super() }},这句话的作用就是保留父模板{% block head %}的内容,同时也可以写上自己填充的内容。{% include "child2.html" %}是导入了child2.html中的渲染内容。上面没有展示,宏可以看成是一个函数,把一些常用的操作放在宏里面,需要使用的时候可以使用import将宏导入后使用。

读取目录结构

一般标准分析的结果存放目录都是固定的,我们可以先用Python读取结果目录的结构,根据目录中是否生成特定的文件和图片来动态的渲染我们的报告模板。而且标准分析经常也是分成几个部分,我们可以把每个分析点分别写一个模板,然后根据特定文件或目录是否生成来判断是否include这个分析点的模板。将报告模块化,可以更加方便的进行维护和多次利用。比如在NGS分析中,经常会在开始查看数据质量如何,那我们就准备一个数据质控的模板,这个模板在不同的报告中也可以使用。

读取目录结构可以使用下面的代码

import os
import sys 
import pprint

pp = pprint.PrettyPrinter()
init_dict = {'dirs':{},'files':[]}

def path_to_dict(path, init_dict):
    d = init_dict
    for x in os.listdir(path):
        if os.path.isdir(os.path.join(path, x)):
            print 'dir:', x
            if x not in d['dirs']:
                d['dirs'][x] = {'dirs':{},'files':[]}
            d['dirs'][x]= path_to_dict(os.path.join(path,x), d['dirs'][x])
        else:
            print 'file:', x
            d['files'].append(x)

    return d

mydict = path_to_dict(sys.argv[1], init_dict)
pp.pprint(mydict)

最后返回一个嵌套的字典,列出目录中所有的文件和子目录。后面我们在进行渲染的时候,就可以把这个嵌套的字典传给Jinja2的模板,根据我们在模板中设置条件,来生成所需的报告。

这里先建几个目录和几个空文件:

mkdir -p dirA dirA/dirB1/dirC
mkdir -p dirA dirA/dirB2
touch dirA/dirB2/file2.txt
touch dirA/dirB1/file1.txt
touch dirA/dirB1/dirC/file3.txt

这个目录的结构是:

├── dirA
│   ├── dirB1
│   │   ├── dirC
│   │   │   └── file3.txt
│   │   └── file1.txt
│   └── dirB2
│       └── file2.txt
└── test.py

这里用了递归的方法来获得目录结构,最后可以得到该目录的字典如下:

{'dirs': {'dirA': {'dirs': {'dirB1': {'dirs': {'dirC': {'dirs': {},
                                                        'files': ['file3.txt']}},
                                      'files': ['file1.txt']},
                            'dirB2': {'dirs': {}, 'files': ['file2.txt']}},
                   'files': []},
          'templates': {'dirs': {}, 'files': ['base.html', 'report.html']}},
 'files': ['test.py']}

把目录下的内容按照是文件夹还是文件,分别放在dirsfiles键下面。

报告的渲染

首先新建dirB1.htmldirB2.html两个文件,以dirB1.html为例:

{% if 'dirB1' in dirA_tree['dirs'] %}
<h2>This is dirB1 demo.</h2>
<p>There is file1.txt in this folder.</p>
{% else %}
<h2>This is dirB1 demo is missing.</h2>
<p>Nothing left.</p>
{% endif %}

主要就是根据文件是否存在而输出相应的内容。测序报告的模板文件report.html

{% extends 'base.html' %} 

{% block content %}

<!-- 为dirA_tree赋值,用了set关键字 -->
{% set dirA_tree = dir_tree['dirs']['dirA'] %}
{% for dir in dirA_tree['dirs'] %}
{% if dir == 'dirB1' %}
    {% include 'dirB1.html' %}
{% elif dir == 'dirB2' %}
    {% include 'dirB2.html' %}
{% endif %}
{% endblock %}

先是用extends引用母模板,之后在{% block content %}中添加自定义的内容。这里自定义的内容是按照文件夹是否存在而进行引用。

模板创建完毕后就可以进行渲染:

import os
from jinja2 import Environment, FileSystemLoader

# 设置模板文件的目录
temp_dir = os.path.join(os.path.abspath('.'), 'templates')
# 读取目录的结果
dir_tree = path_to_dict(os.path.abspath('.'), init_dict)
# 设置jinja2的环境配置
env = Environment(
                      loader = FileSystemLoader(temp_dir),
                      extensions = ['jinja2.ext.do']
                      )

# 读取模板文件
report = env.get_template('report.html')
# 将字典传递到模板中,进行渲染
html = report.render(dir_tree = dir_tree)

with open('Report.html', 'w') as handle:
  handle.write(html.encode('utf-8'))

最后将渲染的结果保存, 可以打开Report.html看看结果。

如果遇到一些复杂的处理,可以将处理的逻辑代码先保存起来,使用的时候再进行导入,可以像Python的函数一样使用,Jinja2里面提供了的功能来实现这样的目的。我们可以写一个如何将Python列表输出成html格式的表格。新建一个宏模板table.html

{% macro make_table(filelist) %}
<table>
    <tr>
        {% for cell in filelist[0] %}
            <th>{{ cell }}</th>
        {% endfor %}
    </tr>
    {% for row in filelist[1:] %}
    <tr>
        {% for cell in row %}
        <td>{{ cell }}</td>
        {% endfor %}
    </tr>
    {% endfor %}
</table>

{% endmacro %}

dirB2.html中引用和使用

{% import "table.html" as T %}
{{ T.make_table(tophat) }}

最后生成的Report.html效果图

其他的一些思考

1. 报告的样式

上面的示例我们只是简单地把从目录结构的读取到报告的生成做了一个简单的介绍。并没有过多的注重报告的样式。以HTML格式提供报告有一个很大的优势就是我们可以通过htmlCSSjavascript来控制报告的展示形式。不过这需要我们提前把相关的样式和框架设计好,再编写模板。

2. 动态的可视化展示

对数据进行动态可视化的展示,可以更加直观,发现数据中的动态变化。Python Jinja2中添加动态展示也非常方便。我们可以利用丰富的javascript插件(d3.js,charts.js等)来实现这个功能。例如pyecharts提供的echarts.js API,可以把想展示的数据先用pyecharts渲染成html,再嵌入到模板中。

3. 报告格式的转换

在一些特殊的情况下,可能还需要其他格式的报告,比如PDF,word等。可以利用pandoc来进行转换,不过在实际使用上,这些转换可能会有一些格式的上变化,最好先进行测试。如果要转换为PDF的话,那推荐使用WeasyPrint,能够识别大部分的CSS样式,生成的PDF比较美观。

4. 对markdown的支持

markdown格式如今也非常流行,借助Python中的一些markdown解析库,我们也可以在报告中将markdown格式的文本渲染成html嵌入到报告中。

上面的内容也只是我在目前想到利用Python进行自动化报告的一点总结,肯定会有很多地方不够完善。本文中出现的所有代码和示例都可在我的GitHub中找到。

参考

Jinja2中文文档