Chapter 13 Table相关插件
Table结构适配器 —— table_adapter
要将context中的变量适配成Table结构,除了手动调用TableWrapper进行包装之外,还可以使用table_adapter插件,使用该插件可以将context中的变量批量适配为table:
table_adapter.py
# coding: utf-8
"""
Docs goes here
"""
from girlfriend.workflow.gfworkflow import Job
from girlfriend.plugin.table import TableMeta
logger = None
logger_level = "info"
def workflow(options):
work_units = (
# make_data
Job(
name="make_data",
caller=make_data,
),
# table_adapter
Job(
name="table_adapter",
plugin="table_adapter",
args=[
TableMeta(
from_variable="user_list",
to_variable="user_table",
name=u"用户表",
titles=["id", u"编号", "name", u"姓名", "score", u"积分"],
table_type=None
),
TableMeta(
from_variable="goods_list",
to_variable="goods_table",
name=u"商品表",
titles=["id", u"编号", "name", u"名称", "price", u"价格"],
table_type=None
),
]
),
# print_table
Job(
name="print_table",
plugin="print_table",
args=[
"$user_table",
"$goods_table",
]
),
)
return work_units
def make_data(context):
context["user_list"] = [
(1, "Sam", 12),
(2, "Tom", 9),
(3, "James", 13)
]
context["goods_list"] = [
(1, "Apple", "12.01"),
(2, "orange", "9.81")
]
其中,每个要转换的对象使用一个TableMeta来表示,TableMeta的具体参数如下:
- from_variable: 要转换的变量
- to_variable: 转换后的变量名称
- name: table name
- titles: 表格标题,可以是Title对象列表,如果你懒的import,那么可以直接像上面那样写一个字符串列表。
- table_type: 表格对象类型,可以指定ListTable、DictTable或者ObjectTable,如果希望自己探测,那么可以忽略该项,或者指定为None。
长表与宽表的转换 —— column2title插件
在对关系型数据库进行建模时,我们通常不会去使用宽行的设计,但在做数据分析的时候,可能通过宽行的方式更加容易比较,比如,一组数据在表结构中是这样存储的:
id month name grade score
1 1 Jack 1 95
1 2 Jack 1 97
1 3 Jack 1 94
1 4 Jack 1 87
2 1 Sam 2 86
2 2 Sam 2 87
2 3 Sam 2 96
但在数据分析的时候,我们经常希望这样去做比较:
id name grade 1月份 2月份 3月份 4月份
1 Jack 1 95 97 94 87
2 Sam 2 86 87 96 0
使用column2title插件可以轻松完成这种转换:
column2title_test.py
# coding: utf-8
"""
Docs goes here
"""
from girlfriend.workflow.gfworkflow import Job
from girlfriend.data.table import Title, TableWrapper
logger = None
logger_level = "info"
def workflow(options):
work_units = (
# make_data
Job(
name="make_data",
caller=make_data,
),
# column2title
Job(
name="column2title",
plugin="column2title",
args={
"from_table": "score_table",
"to_table": "score_table_by_month",
"title_column": "month",
"value_column": "score",
"title_generator": lambda month:\
Title("month_%d" % month, u"%d月份" % month),
"new_title_sort": sorted,
"new_table_name": u"成绩表",
"default": 0,
"sum_title": Title("sum_score", u"总分"),
"avg_title": Title("avg_score", u"平均分")
}
),
# print_table
Job(
name="print_table",
plugin="print_table",
args=[
"$score_table",
"$score_table_by_month",
]
),
)
return work_units
def make_data(context):
data = [
(1, 1, "Jack", 1, 95),
(1, 2, "Jack", 1, 97),
(1, 3, "Jack", 1, 92),
(2, 1, "Betty", 5, 93),
(2, 2, "Betty", 5, 94),
]
context["score_table"] = TableWrapper(
u"成绩表",
(
Title("id", u"编号"),
Title("month", u"月份"),
Title("name", u"姓名"),
Title("grade", u"年级"),
Title("score", u"分数")
)
)(data)
print context["score_table"]
最终输出的结果:
+------+-------+------+-------+-------+-------+------+--------+
| 编号 | 姓名 | 年级 | 1月份 | 2月份 | 3月份 | 总分 | 平均分 |
+------+-------+------+-------+-------+-------+------+--------+
| 2 | Betty | 5 | 93 | 94 | 0 | 187 | 62 |
| 1 | Jack | 1 | 95 | 97 | 92 | 284 | 94 |
+------+-------+------+-------+-------+-------+------+--------+
column2title参数说明:
from_table: 要转换的表格变量名
to_table: 转换结果的表格变量名
title_column: 要转换为标题的列,这里是month
value_column: 要作为新列的值的列,这里是score。
title_generator: 标题生成器,接受一个值作为参数。
new_title_sort: 用于规定新生成标题的顺序,接受所有标题值列表作为参数,如果我们要标题按月份倒序排序,那么参数值就应该是 lambda month_lst: sorted(month_lst, reverse=True)
new_table_name: 新生成表格的名称
default: 默认值,比如Betty没有3月份的成绩,那么将会用这个指定的默认值替代。
sum_title: 如果你想顺便求个和,那么可以指定一个总和Title。默认为None,不会计算总和。
avg_title: 用于顺便求个平均值,使用场景如上。
由于column2title插件的运算量稍微大一些,并且会创建一些中间结构,所以,最好的实践是尽量只在筛选出的最终结果集上去使用它。
使用print_table打印表格
在之前的示例中,我们已经多次用到print_table插件,该插件的使用非常简单,只需要将保存在上下文中的表格变量名称传递给它即可,不再赘述。
使用html_table渲染html格式的表格
如果你将girlfriend集成在web应用中,或者你希望在邮件正文中插入表格,那么你就可以使用html_table插件。html_table能够自动将table对象转换成<table>
标签的形式,并且允许你自由创建样式。
html_table.py
# coding: utf-8
import webbrowser
from girlfriend.workflow.gfworkflow import Job
from girlfriend.plugin.table import HTMLTable
from girlfriend.data.table import Title, ListTable
logger = None
logger_level = "info"
def workflow(options):
work_units = (
# make_data
Job(
name="make_data",
caller=make_data,
),
# html_table
Job(
name="html_table",
plugin="html_table",
args=[
HTMLTable(
table="table",
variable="html_table",
property={
"table": "class='mytable' border=1",
"title-row": "",
"title-cell": "style='background-color:gray'",
"data-row": data_row_style,
"data-cell": "",
}
),
]
),
Job(
name="show_html_table",
caller=show_html_table
)
)
return work_units
def make_data(context):
context["table"] = ListTable(
"table",
(Title("id", u"编号"), Title("name", u"姓名")),
[
(1, u"小明"),
(2, u"小红"),
(3, u"小王"),
(4, u"小张")
]
)
def data_row_style(row_index, row):
if (row_index + 1) % 2 == 0:
return "style='color:red;'"
def show_html_table(ctx):
with open("/tmp/test.html", "w") as f:
f.write(ctx["html_table"].encode("gbk"))
webbrowser.open("file:///tmp/test.html")
每个HTMLTable对象代表了一个要进行转换的表格,其中table参数为要转换的表格变量,variable参数为转换后保存html的变量名,重点是property属性,该属性允许你为指定的html标签插入参数:
table: 即<table>标签
title-row: 标题行,即包含<th>元素的<tr>
title-cell: 即<th>元素
data-row: 数据行,即包含<td>元素的<tr>
data-cell: 数据单元格,即<td>元素
除了传递字符串格式的参数之外,还允许你传递函数,比如data-row指定的函数可以将偶数行的单元格字体指定为红色。每个类型的标签,接受的函数参数是不同的:
table: function(table) 接受要转换的table对象本身作为参数
title-row: function(table) 同上,接受table对象
title-cell: function(column_index, Title) 列索引(从0开始)和对应的Title对象
data-row: function(row_index, row) 行索引(从0开始)和对应的行对象
data-cell: function(row_index, column_index, field_name, value) 依次是行索引、列索引、对应的行属性名称、值。
你可以对这些参数进行检查,从而动态生成更为灵活的属性,比如超过某个阈值的单元格将显示为红色之类。
纵向连接表格
使用concat_table插件可以实现纵向连接表格,类似于SQL语句中的union。使用方法非常简单:
Job(
name="concat_table",
args={
tables=[
"user_table_1",
"user_table_2",
"user_table_3",
],
name=u"连接后的新表名称",
titles=[...] # 连接后的新表Title序列
}
)
横向连接表格
使用join_table插件可以实现横向连接表格,类似于关系数据库中的join查询,比如,当你有一份数据来自于MongoDB,另一份来自于Redis,那么你就可以使用join_table插件来连接两份数据。
join_table.py
# coding: utf-8
"""
Docs goes here
"""
from girlfriend.workflow.gfworkflow import Job
from girlfriend.data.table import ListTable, Title
logger = None
logger_level = "info"
def workflow(options):
work_units = (
# make_data
Job(
name="make_data",
caller=make_data,
),
# join_table
Job(
name="join_table",
plugin="join_table",
args={
"way": "inner",
"left": "user_table",
"right": "score_table",
"on": "id=id",
"fields": ["l.id", "l.name", "l.clazz", "r.c", "r.m", "r.e"],
"name": u"成绩详情",
"titles": [
Title("id", u"编号"),
Title("name", u"姓名"),
Title("clazz", u"班级"),
Title("c", u"语文"),
Title("m", u"数学"),
Title("e", u"英语")
],
}
),
# print_table
Job(
name="print_table",
plugin="print_table",
args=[
"$user_table",
"$score_table",
"$join_table.result"
]
),
)
return work_units
def make_data(context):
user_data = [
(1, "Sam", 1),
(2, "Jack", 1),
(3, "James", 2),
(4, "Tom", 2)
]
score_data = [
(1, 90, 80, 76),
(2, 92, 87, 78),
(3, 99, 82, 75),
(4, 88, 92, 91)
]
context["user_table"] = ListTable(
u"用户表",
(Title("id", u"编号"), Title("name", u"姓名"), Title("clazz", u"班级")),
user_data
)
context["score_table"] = ListTable(
u"分数表",
(Title("id", u"编号"), Title("c", u"语文"),
Title("m", u"数学"), Title("e", u"英语")),
score_data
)
最终输出结果:
+------+-------+------+
| 编号 | 姓名 | 班级 |
+------+-------+------+
| 1 | Sam | 1 |
| 2 | Jack | 1 |
| 3 | James | 2 |
| 4 | Tom | 2 |
+------+-------+------+
+------+------+------+------+
| 编号 | 语文 | 数学 | 英语 |
+------+------+------+------+
| 1 | 90 | 80 | 76 |
| 2 | 92 | 87 | 78 |
| 3 | 99 | 82 | 75 |
| 4 | 88 | 92 | 91 |
+------+------+------+------+
+------+-------+------+------+------+------+
| 编号 | 姓名 | 班级 | 语文 | 数学 | 英语 |
+------+-------+------+------+------+------+
| 1 | Sam | 1 | 90 | 80 | 76 |
| 2 | Jack | 1 | 92 | 87 | 78 |
| 3 | James | 2 | 99 | 82 | 75 |
| 4 | Tom | 2 | 88 | 92 | 91 |
+------+-------+------+------+------+------+
两张表根据ID字段可以关联起来。join_table插件参数:
way: join方式,共有三个值可取,分别为inner、left、right。同我们所熟悉的关系型数据库的join查询一致。
left: 左表
right: 右表
on: 连接条件,左边的连接字段名=右边的连接字段名,如果多个条件,使用逗号隔开。
fields: 最终连接后保留的字段,l表示左表的字段,r表示右表的字段。
name: 结果表的名称
titles: 结果表的标题序列
按条件分割表
很多时候我们需要将一张大表分割成许多小表来进行处理,比如我们从数据库中读出所有渠道的每日注册数据,但是我们希望在报表中各个渠道的负责人只看到自己所负责渠道的数据,这种情况下就需要将这张表的数据按渠道进行切割后发送。通过split_table插件可以帮助你迅速完成这个任务。
split_table.py
# coding: utf-8
"""
Docs goes here
"""
from girlfriend.data.table import ListTable, Title
from girlfriend.workflow.gfworkflow import Job
logger = None
logger_level = "info"
def workflow(options):
work_units = (
# make_data
Job(
name="make_data",
caller=make_data,
),
# split_table
Job(
name="split_table",
plugin="split_table",
args={
"table": "$register_table",
"split_condition": split_condition
}
),
# print_table
Job(
name="print_table",
caller=print_table,
),
)
return work_units
def make_data(context):
data = [
(1, "google", "seo", 100),
(2, "baidu", "seo", 120),
(3, "weixin", "mobile", 30),
(4, "papa", "mobile", 15)
]
context["register_table"] = ListTable(
u"注册数据",
[
Title("id", u"编号"),
Title("channel", u"渠道"),
Title("channel_type", u"渠道类型"),
Title("num", u"注册数")
],
data
)
def print_table(context):
splited_tables = context["split_table.result"]
for table in splited_tables.values():
print table.name
print
print table
print
def split_condition(row):
ch = row["channel_type"]
return ch, u"%s类渠道的注册数据" % ch
最终输出结果:
seo类渠道的注册数据
+------+--------+----------+--------+
| 编号 | 渠道 | 渠道类型 | 注册数 |
+------+--------+----------+--------+
| 1 | google | seo | 100 |
| 2 | baidu | seo | 120 |
+------+--------+----------+--------+
mobile类渠道的注册数据
+------+--------+----------+--------+
| 编号 | 渠道 | 渠道类型 | 注册数 |
+------+--------+----------+--------+
| 3 | weixin | mobile | 30 |
| 4 | momo | mobile | 15 |
+------+--------+----------+--------+
split_table的重点就是split_condition这个函数了,这个函数的作用是从行对象中提取属性作为分割表的依据。该函数需要返回两个值,第一个值是作为分隔表依据的key,返回相同key的记录会被归为同一张表,这里返回的是渠道类型,可以把它理解为grouby的条件;第二个值是对应表的名称。
split_table最终返回的结果是一个字典,key为split_condition所返回的分隔条件,value为对应的Table对象。