LittleQ

爱好:写代码

最近开发公司内部系统里面大量要使用到数据的展示,所以最后就研究了一下DataTables这个Js的插件,功能确实很强大,总结一下如何使用这个插件以及使用过程中碰到的一些问题。
我会先做一个简单的事例,然后逐渐扩展功能达到系统想要的要求,环境准备:

环境 版本
OS Ubuntu 16.04 64bits
python2.7
Web框架 Flask 0.11.1
模板引擎 Jinjia2 2.8
前端框架 ACE
Js插件 DataTables

初始环境搭建

把需要的东西都下载下来即可,如果嫌麻烦,可以直接用第三方的cdn,国内的话推荐:Bootstrap官方推荐,但是Ace这个框架的东西你还是要下载的,毕竟里面包含的东西太多了,github上直接clone到本地即可Ace Github地址:下载之后把assets整个文件夹拷过来即可,由于是演示,所以这个项目可能会又其他一些演示,这里我使用蓝图对不同的演示进行分模块,基本的项目结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.
├── README.md
└── web
├── config.py
├── datatables
│   ├── __init__.py
│   ├── templates
│   │   └── data_table
│   │   └── index.html
│   └── views.py
├── __init__.py
├── run.py
├── static
│   └── assets
│   ├── css
│   ├── font-awesome
│   ├── fonts
│   ├── images
│   ├── js
│   └── swf
└── templates
└── base.html

数据都是用Ajax异步请求获取,其中异步获取数据包括两个部分:

  • 异步获取表格的头部列信息,即表格头部一般是之前不知道的,这种情况尤其适用于BI平台这种专门展示数据的获取数据方式。
  • 异步获取表格的填充数据信息,这个毫无疑问了,后端分页必须这么做

后端

后端主要就两个接口,由于只是演示,所以数据不是从数据库里面查的,但是是一样的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from flask import Blueprint, render_template, jsonify, request

__author__ = 'anonymous'

data_table = Blueprint('data_table', __name__, template_folder='templates')


@data_table.route('/')
def index():
return render_template('data_table/index.html')


@data_table.route('/cols', methods=['GET', 'POST'])
def cols():
"""
获取表格需要展示列信息
:return:
"""
return jsonify(ret=True, columns=['col1', 'col2', 'col3', 'col4', 'col5'])


@data_table.route('/data', methods=['GET', 'POST'])
def data():
"""
获取表格需要展示数据
:return:
"""
# 生成一个100行 5列的二维数组
ao_data = map(lambda row: map(lambda col: '(%s,%s)' % (row, col), range(1, 6)), range(1, 101))

start = int(request.form.get('iDisplayStart'))
end = int(request.form.get('iDisplayStart')) + int(request.form.get('iDisplayLength'))

return jsonify({
"sEcho": request.form.get('sEcho'),
"iTotalRecords": 100,
"iTotalDisplayRecords": 100,
"aaData": ao_data[start:end]
})

**备注:**默认是解析aaData里面的数据进行填充,当然这个也可以在前端设置的。由于是后端获取数据进行分页,并且是用POST方式传输参数,所以可以用request.form.get()来获取各种dataTables的参数,又很多,将几个主要的:

参数 含义
sEcho 点击标记位,每次查询这个数会自增,需要原封不动的给传回去,不然不能分页
iTotalRecords 总记录条数,显示用
iTotalDisplayRecords 过滤之后的条数
aaData 填充表格的数据,二维数组
iDisplayStart 数据开始位置,分页用
iDisplayLength 每页显示数据条数,分页用
sSortDir_[index] 第index列数据顺序,例如 sSortDir_0=’desc’表示最后返回的数据按第一列升序排序
sSearch 搜索,即过滤条件

所以如果你是从数据库查询数据的话,可以把这些参数解析了然后拼出你要的sql,这样就可以按条件查询出你要的结果了,不过这个搜索过滤条件就比较随意,这里我就假定是对第一列进行搜索,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
select 
col1,
col2,
col3,
col4,
col5
from
user
where
col1 like ${sSearch}
order by
col1 desc
limit
${iDisplayLength} offset ${iDisplayStart};

这样后端的工作就基本完成了。

前端

前端的代码就一个文件,主要是用到了Jinjia2,不熟悉这个语法的可以去官网大概看一下,非常好上手,就是一个模板替换引擎,有一个公共的基础模板,里面引入了一些外部的文件和样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
{% extends 'base.html' %}

{% block title %}
DataTables演示
{% endblock %}

{% block body %}
<div>
<div class="row">
<div class="col-xs-12">
<!-- PAGE CONTENT BEGINS -->
<div class="row">
<div class="col-xs-12">
<h3 class="header smaller lighter blue">
表格大标题
</h3>

<div class="clearfix">
<div class="pull-right tableTools-container"></div>
</div>
<div class="table-header">
我是表头
</div>

<!-- div.dataTables_borderWrap -->
<div>
<table id="dynamic-table"
class="table table-striped table-bordered table-hover">
<thead>
<tr>
</tr>
</thead>

<tbody>
</tbody>
</table>
</div>
</div>
</div>

<div id="modal-table" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header no-padding">
<div class="table-header">
<button type="button" class="close" data-dismiss="modal"
aria-hidden="true">
<span class="white">&times;</span>
</button>
Results for "Latest Registered Domains
</div>
</div>

<div class="modal-body no-padding">
<table class="table table-striped table-bordered table-hover no-margin-bottom no-border-top">
<thead>

</thead>

<tbody>

</tbody>
</table>
</div>

<div class="modal-footer no-margin-top">
<button class="btn btn-sm btn-danger pull-left" data-dismiss="modal">
<i class="ace-icon fa fa-times"></i>
Close
</button>

<ul class="pagination pull-right no-margin">
<li class="prev disabled">
<a href="#">
<i class="ace-icon fa fa-angle-double-left"></i>
</a>
</li>

<li class="next">
<a href="#">
<i class="ace-icon fa fa-angle-double-right"></i>
</a>
</li>
</ul>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>

</div>
<!-- /.page-content -->
</div>
</div> <!-- 单独一个完整报表展示-->
{% endblock %}

{% block js %}
{{ super() }}

<!-- DataTables Js-->
<script src="{{ url_for('static', filename='assets/js/jquery.dataTables.min.js') }}"></script>
<script src="{{ url_for('static', filename='assets/js/jquery.dataTables.bootstrap.min.js') }}"></script>
<script src="{{ url_for('static', filename='assets/js/dataTables.buttons.min.js') }}"></script>
<script src="{{ url_for('static', filename='assets/js/buttons.flash.min.js') }}"></script>
<script src="{{ url_for('static', filename='assets/js/buttons.html5.min.js') }}"></script>
<script src="{{ url_for('static', filename='assets/js/buttons.print.min.js') }}"></script>
<script src="{{ url_for('static', filename='assets/js/buttons.colVis.min.js') }}"></script>
<script src="{{ url_for('static', filename='assets/js/dataTables.select.min.js') }}"></script>

<script type="text/javascript">
var table = $('#dynamic-table')
var aoColumns = []
$.ajax({
contentType: "application/json; charset=utf-8",
url: "{{ url_for('data_table.cols') }}",
type: 'post',
success: function (resp) {
$.each(resp.columns, function (i, v) {
table.find('thead > tr').append('<th>' + v + '</th>')
aoColumns.push({"title": v})
});
showTables(table, aoColumns)
}
});

function showTables(dom_table, cols) {

//initiate dataTables plugin
var myTable = dom_table.DataTable({
bAutoWidth: false,
"aoColumns": cols,
"aaSorting": [],

//"bProcessing": true,
"bServerSide": true,
"sAjaxSource": "{{ url_for('data_table.data') }}",
"fnServerData": function (sSource, aoData, fnCallback) {
$.ajax({
'dataType': 'json',
'type': 'POST',
'url': sSource,
'data': aoData,
'success': fnCallback
});
},
//,
//"sScrollY": "200px",
//"bPaginate": false,

//"sScrollX": "100%",
//"sScrollXInner": "120%",
//"bScrollCollapse": true,
//Note: if you are applying horizontal scrolling (sScrollX) on a ".table-bordered"
//you may want to wrap the table inside a "div.dataTables_borderWrap" element

//"iDisplayLength": 50


select: {
style: 'multi'
}
});


$.fn.dataTable.Buttons.defaults.dom.container.className = 'dt-buttons btn-overlap btn-group btn-overlap';

new $.fn.dataTable.Buttons(myTable, {
buttons: [
{
"extend": "colvis",
"text": "<i class='fa fa-search bigger-110 blue'></i> <span class='hidden'>Show/hide columns</span>",
"className": "btn btn-white btn-primary btn-bold",
columns: ':not(:first):not(:last)'
},
{
"extend": "copy",
"text": "<i class='fa fa-copy bigger-110 pink'></i> <span class='hidden'>Copy to clipboard</span>",
"className": "btn btn-white btn-primary btn-bold"
},
{
"extend": "csv",
"text": "<i class='fa fa-database bigger-110 orange'></i> <span class='hidden'>Export to CSV</span>",
"className": "btn btn-white btn-primary btn-bold"
},
{
"extend": "excel",
"text": "<i class='fa fa-file-excel-o bigger-110 green'></i> <span class='hidden'>Export to Excel</span>",
"className": "btn btn-white btn-primary btn-bold"
},
{
"extend": "pdf",
"text": "<i class='fa fa-file-pdf-o bigger-110 red'></i> <span class='hidden'>Export to PDF</span>",
"className": "btn btn-white btn-primary btn-bold"
},
{
"extend": "print",
"text": "<i class='fa fa-print bigger-110 grey'></i> <span class='hidden'>Print</span>",
"className": "btn btn-white btn-primary btn-bold",
autoPrint: false,
message: 'This print was produced using the Print button for DataTables'
}
]
});
myTable.buttons().container().appendTo($('.tableTools-container'));

//style the message box
var defaultCopyAction = myTable.button(1).action();
myTable.button(1).action(function (e, dt, button, config) {
defaultCopyAction(e, dt, button, config);
$('.dt-button-info').addClass('gritter-item-wrapper gritter-info gritter-center white');
});


var defaultColvisAction = myTable.button(0).action();
myTable.button(0).action(function (e, dt, button, config) {

defaultColvisAction(e, dt, button, config);


if ($('.dt-button-collection > .dropdown-menu').length == 0) {
$('.dt-button-collection')
.wrapInner('<ul class="dropdown-menu dropdown-light dropdown-caret dropdown-caret" />')
.find('a').attr('href', '#').wrap("<li />")
}
$('.dt-button-collection').appendTo('.tableTools-container .dt-buttons')
});

////

setTimeout(function () {
$($('.tableTools-container')).find('a.dt-button').each(function () {
var div = $(this).find(' > div').first();
if (div.length == 1) div.tooltip({container: 'body', title: div.parent().text()});
else $(this).tooltip({container: 'body', title: $(this).text()});
});
}, 500);


myTable.on('select', function (e, dt, type, index) {
if (type === 'row') {
$(myTable.row(index).node()).find('input:checkbox').prop('checked', true);
}
});
myTable.on('deselect', function (e, dt, type, index) {
if (type === 'row') {
$(myTable.row(index).node()).find('input:checkbox').prop('checked', false);
}
});


/////////////////////////////////
//table checkboxes
$('th input[type=checkbox], td input[type=checkbox]').prop('checked', false);

//select/deselect all rows according to table header checkbox
$('#dynamic-table > thead > tr > th input[type=checkbox], #dynamic-table_wrapper input[type=checkbox]').eq(0).on('click', function () {
var th_checked = this.checked;//checkbox inside "TH" table header

$('#dynamic-table').find('tbody > tr').each(function () {
var row = this;
if (th_checked) myTable.row(row).select();
else myTable.row(row).deselect();
});
});

//select/deselect a row when the checkbox is checked/unchecked
$('#dynamic-table').on('click', 'td input[type=checkbox]', function () {
var row = $(this).closest('tr').get(0);
if (this.checked) myTable.row(row).deselect();
else myTable.row(row).select();
});


$(document).on('click', '#dynamic-table .dropdown-toggle', function (e) {
e.stopImmediatePropagation();
e.stopPropagation();
e.preventDefault();
});


//And for the first simple table, which doesn't have TableTools or dataTables
//select/deselect all rows according to table header checkbox
var active_class = 'active';
$('#simple-table > thead > tr > th input[type=checkbox]').eq(0).on('click', function () {
var th_checked = this.checked;//checkbox inside "TH" table header

$(this).closest('table').find('tbody > tr').each(function () {
var row = this;
if (th_checked) $(row).addClass(active_class).find('input[type=checkbox]').eq(0).prop('checked', true);
else $(row).removeClass(active_class).find('input[type=checkbox]').eq(0).prop('checked', false);
});
});

//select/deselect a row when the checkbox is checked/unchecked
$('#simple-table').on('click', 'td input[type=checkbox]', function () {
var $row = $(this).closest('tr');
if ($row.is('.detail-row ')) return;
if (this.checked) $row.addClass(active_class);
else $row.removeClass(active_class);
});


/********************************/
//add tooltip for small view action buttons in dropdown menu
$('[data-rel="tooltip"]').tooltip({placement: tooltip_placement});

//tooltip placement on right or left
function tooltip_placement(context, source) {
var $source = $(source);
var $parent = $source.closest('table')
var off1 = $parent.offset();
var w1 = $parent.width();

var off2 = $source.offset();
//var w2 = $source.width();

if (parseInt(off2.left) < parseInt(off1.left) + parseInt(w1 / 2)) return 'right';
return 'left';
}


/***************/
$('.show-details-btn').on('click', function (e) {
e.preventDefault();
$(this).closest('tr').next().toggleClass('open');
$(this).find(ace.vars['.icon']).toggleClass('fa-angle-double-down').toggleClass('fa-angle-double-up');
});
/***************/


/**
//add horizontal scrollbars to a simple table
$('#simple-table').css({'width':'2000px', 'max-width': 'none'}).wrap('<div style="width: 1000px;" />').parent().ace_scroll(
{
horizontal: true,
styleClass: 'scroll-top scroll-dark scroll-visible',//show the scrollbars on top(default is bottom)
size: 2000,
mouseWheelLock: true
}
).css('padding-top', '12px');
*/


}
</script>
{% endblock %}

这里主要就说说后面的js获取数据这一块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
var table = $('#dynamic-table')
var aoColumns = []
$.ajax({
contentType: "application/json; charset=utf-8",
url: "{{ url_for('data_table.cols') }}",
type: 'post',
success: function (resp) {
$.each(resp.columns, function (i, v) {
table.find('thead > tr').append('<th>' + v + '</th>')
aoColumns.push({"title": v})
});
showTables(table, aoColumns)
}
});

function showTables(dom_table, cols) {

//initiate dataTables plugin
var myTable = dom_table.DataTable({
bAutoWidth: false,
"aoColumns": cols,
"aaSorting": [],

//"bProcessing": true,
"bServerSide": true,
"sAjaxSource": "{{ url_for('data_table.data') }}",
"fnServerData": function (sSource, aoData, fnCallback) {
$.ajax({
'dataType': 'json',
'type': 'POST',
'url': sSource,
'data': aoData,
'success': fnCallback
});
},
//,
}
}

第一个ajax从后端获取列的名字,然后动态插入表头元素:

1
table.find('thead > tr').append('<th>' + v + '</th>')

并且生成表头aoColumns供后面的myTable初始化用,格式就是{title:col_name}的一个数组,采用后端分页,所以需要设置

1
"bServerSide": true

及获取数据的源

1
"sAjaxSource": "{{ url_for('data_table.data') }}"

然后就是前后端数据交互:

1
2
3
4
5
6
7
8
9
"fnServerData": function (sSource, aoData, fnCallback) {
$.ajax({
'dataType': 'json',
'type': 'POST',
'url': sSource,
'data': aoData,
'success': fnCallback
});
},

这样就完成了一个简单的前后端交互表格的服务器端分页的例子,访问http://127.0.0.1:5000/data_table完成后的效果如下:

DataTables样例效果

DataTables一些配置解释

这里又一份非常详细的参数说明,不太清楚的可以先看看这个,这个是别人总结的,可以参考一下这些参数的含义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
$('#dataTable_ID1').dataTable({
//"aaSorting" : [[1, "asc"]], //默认的排序方式,第1+1列,升序排列
"aLengthMenu" : [5, 10, 25, 50, 100], //更改显示记录数选项(默认:[10,25,50,100])
"bAutoWidth" : false, // 禁用自适应宽度(默认:true)
//"bDeferRender":false,//推迟创建表元素每个元素,直到它们都创建完成(默认:false)
"bDestroy" : true,//重新初始化表格,未匹配到表格则新建 (默认:false)
"bFilter" : false,// 不适用搜索框过滤(默认:true)
//"bInfo" : true, //显示页脚信息,左下角显示记录数(默认:true)
//"bJQueryUI" : false, //不使用使用 jQury的UI theme(默认:false)
"bLengthChange" : true,//显示每页几条数据的显示框(默认:true)
//"bPaginate" : true, //显示(应用)分页器,不开启全显示(默认:true)
"bProcessing" : true,//加载进度提示(默认:false)
//"bScrollInfinite" : true, //启动初始化滚动条(默认:false)
//"bRetrieve":false,//使用指定的选择器检索表格,注意,如果表格已经被初始化,该参数会直接返回已经被创建的对象,并不会顾及你传递进来的初始化参数对象的变化,将该参数设置为true说明你确认已经明白这一点,如果你需要的话,bDestroy可以用来重新初始化表格(默认:false)
"bServerSide" : true,//启动服务器端数据导入(默认:false)
"bSort" : true,//启用字段可排序(默认:true) TODO:单个列排序可禁用
//"bStateSave" : true,//开启状态缓存,如分页信息,展示长度,开启后在ajax刷新纪录的时候不会将个性化设定重置为初始化状态,如: 会导致默认的aaSorting设置失效(默认:false)
//"bScrollCollapse" : true, //开启高度自适应,当数据条数不够分页数据条数的时候,插件高度随数据条数而改变
//"bScrollAutoCss":true,//指明滚动的标题元素是否被允许设置内边距和外边距等(默认:true)
//"bScrollCollapse":false,//当垂直滚动被允许的时候,不强制强制表格视图在任何时候都是给定的高度(默认:false)
//"bSortCellsTop":false,//允许使用底部的单元格,true为顶部(默认:false)
//"iCookieDuration":7200,//cookie储存时长(单位:s)(默认:7200)
//"iDeferLoading":null,//延时加载(type:int)(默认:null)
//"iDisplayLength":10,//每页显示几条数据(默认:10)
//"iDisplayStart":0,//当前页开始的记录序号(默认:0)
//"iScrollLoadGap":100,//当前页面还有多少条数据可供滚动时自动加载新的数据(默认:100)
"sDom": '<"top"l>rt<"bottom_left"i><"bottom_right"p><"clear">',//布局定义
//格式指定:包括分页,显示多少条数据和搜索等
//The following options are allowed:
// 'l' - 左上角按个下拉框,10个,20个,50个,所有的哪个
// 'f' - 快速过滤框
// 't' - 表格本身
// 'i' - 分页信息
// 'p' - 分页按钮
// 'r' - 现在正在加载中……
//The following constants are allowed:
// 'H' - jQueryUI theme "header" classes ('fg-toolbar ui-widget-header ui-corner-tl ui-corner-tr ui-helper-clearfix')
// 'F' - jQueryUI theme "footer" classes ('fg-toolbar ui-widget-header ui-corner-bl ui-corner-br ui-helper-clearfix')
//The following syntax is expected:
// '<' and '>' - div 元素
// '<"class" and '>' - 给div加clasa
// '<"#id" and '>' - 给div加上id
//Examples:
// '<"wrapper"flipt>'
// '<lf<t>ip>'
//例子:
//'<"top"i>rt<"bottom"flp><"clear">'
//解析结果:
// <div class="top">
// i
// </div>
// rt
// <div class="bottom">
// flp
// </div>
// <div class="clear"></div>
"sPaginationType" : "full_numbers",//全页数显示 || "two_button":显示两个按钮(默认:two_button)
"sAjaxSource" : mediaHost+'/wxUsers/getDataTable1',
//"sAjaxDataProp" : "aaDataName",//指定返回的数据对象名称(默认:aaData)
//"sScrollX" : 720, //DataTables的宽,可以是css设置,或者一个数字(单位:px),大于则开启水平滚动(默认:"blank string - i.e. disabled")
//"sScrollY" : 480, //DataTables的高,可以是css设置,或者一个数字(单位:px),大于则开启垂直滚动(默认:"blank string - i.e. disabled")
//"sCookiePrefix" : "SpryMedia_DataTables_",//指定cookie前缀(默认:"SpryMedia_DataTables_")


//初始化过滤状态
//"oSearch":{
// "sSearch":"value",
// "bRegex":false, //value不当成正则式
// "bSmart":true //灵活匹配策略
//},

//数据表列值
"aoColumns" : [ {
"mDataProp" : "data_properties0",
"sClass" : "center",
"bSortable" : false
//"sDefaultContent":"",//此列默认值为"",防数据无值报错
//"bVisible" : false //不显示此列
}, {
"mDataProp" : "data_properties1",
"sClass" : "center",
"bSortable" : false
}, {
"mDataProp" : "data_properties2",
"sClass" : "center",
"bSortable" : false
}, {
"mDataProp" : "data_properties2",
"sClass" : "center",
"bSortable" : false
},
],

//国际化配置
"oLanguage" : {
"sProcessing" : "正在加载数据,请稍后...",
"sLengthMenu" : "每页显示 _MENU_ 条记录",
"sZeroRecords" : "没有数据!",
"sEmptyTable" : "表中无数据存在!",
"sInfo" : "当前显示 _START_ 到 _END_ 条,共 _TOTAL_ 条记录",
"sInfoEmpty" : "显示0到0条记录",
"sInfoFiltered" : "数据表中共有 _MAX_ 条记录",
//"sInfoPostFix": "",
//"sSearch": "搜索:",
//"sUrl": "",
//"sLoadingRecords": "载入中...",
//"sInfoThousands": ",",
"oPaginate" : {
"sFirst" : "首页",
"sPrevious" : "上一页",
"sNext" : "下一页",
"sLast" : "末页"
}
//"oAria": {
// "sSortAscending": ": 以升序排列此列",
// "sSortDescending": ": 以降序排列此列"
//}
},
/**
*
* @param nRow 当前行内容
* @param aaData 当前数据对象
* @param iDisplayIndex 当前行索引,从0开始
* @param iDisplayIndexOfAadata 当前对象所在对象数组的索引,从0开始
* @returns {*}
*/
"fnRowCallback" : function(nRow, aaData, iDisplayIndex, iDisplayIndexOfAadata) {

//修改第一列为多选框内容
var firstTDHtml = '<label>firstTDHtml</label>';
$('td:eq(0)', nRow).html(firstTDHtml);

//修改第二列为序号
var secondTDHtml = iDisplayIndex+1;
$('td:eq(1)', nRow).html(secondTDHtml);

return nRow;
},
"fnDrawCallback" : function(oSettings) {
// jAlert( 'DataTables 重绘了' );
},
"fnFooterCallback" : function(nFoot, aData, iStart, iEnd, aiDisplay) {
// jAlert("FooterCallback");
},
});

网上下载某个js库,官网下载的源码一般都带有样例,但是好多html里面引用的js都是使用的cdn网址,类似于这样:

1
<script src="https://cdn.bootcss.com/d3/3.5.2/d3.min.js"></script>

但是好多优秀的JS插件都是国外的,所以很多作者的项目里面样例都是使用的国外的cdn,例如

1
https://cdnjs.com/

但是由于某些特殊的你懂得原因,上面这个网占经常没办法访问,或者访问不了,所以我只能替换成国内的cdn,比如我上面最开始写的那个就是,现在问题来了,我想把example下面的所有html文件里面的这个js都替换掉,即把:

1
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.2/d3.min.js" charset="utf-8"></script>

替换成:

1
<script src="https://cdn.bootcss.com/d3/3.5.2/d3.min.js"></script>

其实使用一条Linux命令就可以了:

1
sed 's/old_xxx/new_xxx/g' <file_name>
1
2
cd examples
sed -i 's#https://cdn.bootcss.com/d3/3.5.2/d3.min.js#https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.2/d3.min.js#g' *.html

上面需要稍作解释:

  • -i

如果不加-i参数,那么sed会把结果输出到终端,加了之后替换的结果会写回到原文件

  • s#old_xxx#new_xxx#g

原替换语法是s/old_xxx/new_xxx/g,但是如果原本的内容里面就有/,要么转义这个字符,或者为了不破外可读性,降低出错概率,直接用#做分隔符更好

  • *.html

由于要替换所有的,所以最后面的文件名参数直接就写成*.html,这样就会把所有的html文件里面的内容替换并写回原文件了。

今天在做Flask模块的实体类ORM声明时,发现会报错,花了一上午查了好多资料才解决,记录一下解决方案,查资料的时候貌似碰到这个问题的人还挺多的.
报错信息为:

InvalidRequestError: When initializing mapper Mapper|Parent|parent, expression ‘childs’ failed to locate a name (“name ‘Child’ is not defined”). If this is a class name, consider adding this relationship() to the Parent class after both dependent classes have been defined.

什么意思呢?就是在在ParentChild一对多的映射时,映射找不到这个类,这两个类属于不同的注册模块,当然也就不在一个文件中了,结构如下:

1
2
3
4
5
6
7
8
9
10
11
➜  web 
├── parent
│   ├── __init__.py
│   ├── models.py
│   ├── templates
│   └── views.py
└── child
   ├── __init__.py
   ├── models.py
   ├── templates
   └── views.py

部分关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Parent(db.Model):

__tablename__ = 'parent'
__bind_key__ = 'test'

id = db.Column(db.Integer, primary_key=True)
childs = db.relationship('Child', lazy='dynamic')

class Child(db.Model):

__tablename__ = 'child'
__bind_key__ = 'test'

id = db.Column(db.Integer, primary_key=True)
parent_id = db.Column(db.Integer, db.ForeignKey('parent.id'))

这是一个很简单的一对多映射,其他的类都可以调用db.create_all()创建,但是我调用这个命令确并不会创建Child表,实在想不明白这个类特殊在哪,于是网上查阅说可以强制导入model的定义:

1
2
3
from web.child.models import *
db.create_all()
db.session.commit()

然后去mysql里面看,确实可以创建表了,于是启动项目,结果登陆系统还是报一样的错,仍然是映射关系出问题,于是把谷歌翻了个变,终于找到了一个解决方法.
通常来说,db.create_all()无法创建所有表会有以下几个原因:

  1. model未继承db.Model
  2. views中未引入该表的model
  3. create_all()与models定义不在一个文件

对于一个多模块的系统来说,第3个原因不太可能,因为几乎不可能把models定义和create_all()放到一个文件里面。但是其他模块都没啥问题,所以问题不会出在这。
第一个显然不是,我的所有实体类都是继承了db.Model,所以当我把问题定位在2时,仔细一想好像真是这样,因为我虽然新增了一个模块,但是只是先把模块的models定义好了,views.py里面其实啥也没写,自然没有引用到新定义的models
于是在views.py文件里面加了一句web/child/views.py:

1
from web.child.models import *

然后执行:

1
2
3
db.drop_all()
db.create_all()
db.session.commit()

然后登陆系统,果然没有出现映射问题了。

出去玩了一趟,回来发现有的软件无法切换中文输入法了,总的来说就是浏览器还有系统自带的一些应用好像没啥影响,但是我安装的第三方然简就有的不行了,包括我又重装了输入法等还是不行,最后的解决办法可能和输入法模块的路径有关,不知道为啥找不到了。
会出现问题的软件一般为WPS, IDEA, PyCharm这些,所以只好在Pycharm启动文件里面去手动指定了,我的安装路径启动文件为:

1
/usr/dev/pycharm-2016.2/bin/pycharm.sh

修改文件,在顶端加入以下内容:

1
2
3
export GTK_IM_MODULE=fcitx
export QT_IM_MODULE=fcitx
export XMODIFIERS=@im=fcitx

然后重启Pycharm就可以切换搜狗输入法了。

但是有时候WPS的中文输入法也不起作用,这个时候用这个方法去改/usr/bin/wps,然后打开文档也不起作用,最后实验了发现,要想用wps编辑输入中文,必须是从应用那先打开wps软件,然后从软件中打开一个文档才行,如果直接用右键,选择wps打开xls的文档是不行的。

**备注:**如果你之前可以,但是照这个方法弄了以后还不行,那么你需要运行一下fcitx诊断命令:

1
fcitx-diagnose

一般可以发现问题所在,如果没啥异常,那么你需要完全卸载fcitx,然后重新装一遍就好了,卸载命令如下:

1
2
sudo apt-get purge  fcitx
sudo apt-get autoremove

然后重新安装一下fcitx,为了保险起见,你可以把搜狗也卸载了重装。

今天在是使用Flask的Flask-login搭建一个系统的时候,在登陆views.py视图文件里面引用models.py实体类的时候出现了一个错误:

1
ImportError: cannot import name db

看看项目的文件结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
➜  financial_bi git:(dev) ✗ tree -L 3
.
├── config.py
├── instances
│   └── dev_init.sql
├── README.md
├── requirements.txt
├── run.py
└── web
├── common
│   └── __init__.py
├── __init__.py
├── security
│   ├── __init__.py
│   ├── models.py
│   ├── templates
│   └── views.py
├── static
│   ├── assets
│   └── favicon.ico
└── templates
└── layout

9 directories, 15 files

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from web.security.views import security

app = Flask('web', static_folder='static')
app.config.from_pyfile('../config.py')

db = SQLAlchemy(app)


def create_app():
# 注册模块
app.register_blueprint(security, url_prefix='')

configure_flask_login(app)

return app

看到这里确实有问题,我们来梳理一下这个引用关系:
循环依赖图
我们对照着这个图分析一下怎么产生循环依赖的:

1
2
3
1. __init__.py 先import web/security/views.py,然后声明变量db
2. web/security/views.py依赖web/security/models.py的User实体类
3. web/security/models.py依赖__init__.py中声明的变量db

结果就导致了这个问题,所以我们要想解决这个问题,可以在__init__.py中把变量db的声明顺序放到import web/security/views.py之后,如此一来__init__.pydb变量就不会依赖其他模块了.
所以我们得出一个技巧,在用蓝图注册模块的时候,把引用放到工厂函数里面去,像下面这样:

1
2
3
4
5
6
def create_app():
# 注册模块
from web.security.views import security

app.register_blueprint(security, url_prefix='')

这样就不会有循环依赖的问题了。

要不是为了用Flask开发系统,要不是涉及到各种表,估计也没人愿意用ORM框架吧,简单的查询估计直接写sql,用MySQLdb了。但是当你的系统大了,功能复杂了,表多了之后,这个东西还是很有必要的。如果你做了一个还算不太复杂的系统,里面各种表,你还手写sql,那我敬你是一条汉子,毕竟我们都喜欢踏实肯干的小伙子啊,请务必把简历发到我们公司HR的邮箱。好了,前面都是扯淡的,现在我要一本正经的说重点了,Flask的ORM框架也不止一个,为啥多说人都推荐用SQLALChemy这个ORM框架呢?其实原因很简单:因为它非常的灵活,这直接导致了它的功能非常的强大,当然,间接导致了这个框架学习成本很高。其实主要是参数很多,很多人不知道什么场景用什么参数,所以才有这篇笔记,我也不知道这些参数都是干啥的,但是作者既然留了这么多参数,必然是有场景需要,不然人家吃饱了撑得?

SQLALChemy用法

虽然是因为Flask而去用这个框架,但是我这里主要讲SQLALChemy怎么用,并没有涉及到Flask.前面说了一堆,无非就是想告诉你,这个框架真的很牛逼,用的人很多,出了问题你也不是第一个踩坑的,毕竟每个坑下面躺着不少你的前辈,有了问题也好解决。直接说有些空洞,先上个简单的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from sqlalchemy import Integer, ForeignKey, String, Column
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from sqlalchemy.orm import sessionmaker

engine = create_engine('sqlite:///:memory:', echo=False)
Base = declarative_base()


class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
name = Column(String)

def __init__(self, name=None):
self.name = name

def __repr__(self):
return '<User:id={0},name={1}>'.format(self.id, self.name)

addresses = relationship('Address')


class Address(Base):
__tablename__ = 'address'
id = Column(Integer, primary_key=True)
email = Column(String)
user_id = Column(Integer, ForeignKey('user.id'))

def __init__(self, email=None, user_id=None):
self.email = email
self.user_id = user_id

def __repr__(self):
return '<Address:id={0},email={1},user_id={2}>'.format(self.id, self.email, self.user_id)


if __name__ == '__main__':

Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()

session.add_all([
User(name='san.zhang'),
User(name='si.li'),
User(name='wu.wang')
])

session.add_all([
Address(email='123@qq.com', user_id=1),
Address(email='456@qq.com', user_id=2),
Address(email='789@qq.com', user_id=2)
])
session.commit()

print '查询1:%s' % session.query(User).all()
print '查询2:%s' % session.query(User).filter_by(id=1).first().addresses
print '查询3:%s' % session.query(User).filter_by(id=2).first().addresses
print '查询4:%s' % session.query(User).filter_by(id=3).first().addresses

解释一下:

  • 第10行我直接创建了一个内存数据库,这东西用法就是一个数据库链接,用过jdbc的都应该知道我在说什么,不知道的就当你知道了。具体的链接不同数据库不同,我就举个MySQL的例子:
1
SQLALCHEMY_DATABASE_URI = 'mysql://<user_name>:<passward>@<host>:<port>/<database>'
  • echo:这个参数就是是否打印出执行的sql,其实orm框架到最后也是执行sql(好像是废话哈),所以你如果平时调试的时候想看这个底层执行的sql是不是符合你的逻辑,你可以把这个参数设置成True,这样在控制台就会打印出你每次执行的代码对应的sql

  • 实体类
    这里创建了两个实体类User,Address,分别对应数据库里面的两个表,这就完成了简单的ORM关系映射,把实体类和表对应起来了,注意这里有个映射关系,一个用户可能对应多个地址,这里还有个很重要的东西relationship('Address').这个东西很复杂,其实SQLALChemy的复杂主要就是体现在这个地方,关系映射这个东西不太好理解,后面会细讲

  • 数据库操作
    这个其实比较简单了,需要一个操作句柄,session,修改操作只有在session.commit()之后才会真正提交到数据库中,类似与Spring里面的事务管理.

具体使用上就比较简单了,先创建了几个对象,然后调用add_all()把数据写到数据库里面,后面就可以查询了

关系映射relationship

前面也说了,这个东西复杂主要就是复杂在relationship这个类的使用上,总的来说,关系型数据库当然少不了关系这个桥梁,而ORM框架里面的这个R就显得很重要了,网上的一些教程吧,说的很含糊,我自己学的时候看了很多,那些人以为自己理解了,讲清楚了,但是说出来的话模棱两可,对于新手来说具有很大的迷惑性,我只能自己验证一下:

表关联

首先强调一点,在实体类里面用relationship()申明的属性并不是这个类对应的数据库里面的真实字段,也就是说上面的User类里面有addresses这个属性,但是数据库表user里面并没有addresses字段,这个只是ORM框架替你做了一个映射,你可以通过User里面的属性拿到和它关联的地址,但是你没办法通过Address类来获取它对应的用户信息,这就是关系映射,看看运行结果:

1
2
3
4
查询1:[<User:id=1,name=san.zhang>, <User:id=2,name=si.li>, <User:id=3,name=wu.wang>]
查询2:[<Address:id=1,email=123@qq.com,user_id=1>]
查询3:[<Address:id=2,email=456@qq.com,user_id=2>, <Address:id=3,email=789@qq.com,user_id=2>]
查询4:[]

我这里把输出sql的开关设置成Flase了,所以比较整洁,只有输出结果,如果打开的话,就会有对应的查询语句.

延迟加载

这个东西比较有意思,我们把User类里面的东西该成下面这样:

1
addresses = relationship('Address', lazy='dynamic')

我们不仅加了关系映射,还加了一个属性lazy='dynamic',这个是个啥意思呢?其实就是延迟加载,想象一个场景,如果一个人对应了1w条地址信息,那么每当我们查一个用户的地址,访问addresses字段时,就会返回那1w条记录,但是正常情况下这1w条记录里面并不都是你要的,如果加上了上面这个属性,意思就是你访问addresses属性不再返回数据了,而是返回一个查询对象,你可以在这个查询对象上应用过滤啊,条件啥的,找到你真正要的那部分数据,这样性能上也会更好,不用每次把所有的数据都返回,具体可以看下面的例子,上面的查询语句输出结果如下:

1
2
3
4
5
6
7
8
9
10
查询1:[<User:id=1,name=san.zhang>, <User:id=2,name=si.li>, <User:id=3,name=wu.wang>]
查询2:SELECT address.id AS address_id, address.email AS address_email, address.user_id AS address_user_id
FROM address
WHERE :param_1 = address.user_id
查询3:SELECT address.id AS address_id, address.email AS address_email, address.user_id AS address_user_id
FROM address
WHERE :param_1 = address.user_id
查询4:SELECT address.id AS address_id, address.email AS address_email, address.user_id AS address_user_id
FROM address
WHERE :param_1 = address.user_id AND address.email = :email_1

可以看到,并没有输出我们查询到的结果,而是返回了查处对象,那么如果要获取查询结果怎么办呢?其实也很简单,既然是查询对象,那像上面一样调用first(),all()就可以了,把查询语句改一下:

1
2
3
4
print '查询1:%s' % session.query(User).all()
print '查询2:%s' % session.query(User).filter_by(id=1).first().addresses.all()
print '查询3:%s' % session.query(User).filter_by(id=2).first().addresses.all()
print '查询4:%s' % session.query(User).filter_by(id=2).first().addresses.filter_by(email='456@qq.com').all()

输出结果为:

1
2
3
4
查询1:[<User:id=1,name=san.zhang>, <User:id=2,name=si.li>, <User:id=3,name=wu.wang>]
查询2:[<Address:id=1,email=123@qq.com,user_id=1>]
查询3:[<Address:id=2,email=456@qq.com,user_id=2>, <Address:id=3,email=789@qq.com,user_id=2>]
查询4:[<Address:id=2,email=456@qq.com,user_id=2>]

注意第四个查询语句,我们在获取addresses属性之后调用了过滤函数,最后的结果确实只返回了一个,所以说这个延迟加载的确很方便,可以在查询结果返回前过滤掉不用的数据

反向查询

我们知道,通过User类可以访问到addresses属性,其实返回的就是Address对象,那么问题来了,如果想通过Address来反查某个地址下面的用户怎么办,当然你可以查询到这些user_id再去user表里面查对应的user信息,但是这样很麻烦,sqlalchemy也提供了一种反查机制,backref参数,类似于在Address中添加了user属性,同样的把User实体类改一下:

1
addresses = relationship('Address', backref=db.backref('user', lazy='joined'), lazy='dynamic')

注意到最外层的lazy='dynamic'是针对User查询多个地址时延迟加载策略,db.backref('user', lazy='joined')这个虽然定义在了User类里面,但是此处的作用是定义一个反向引用关系,这样就可通过Address来访问到对应的User类了,并且里面也定了一个策略lazy='joined',这是告诉Address查找对应的User时采用连接查询,如果不加,那就会采用子查询的方式来查询。可以测试一下,需要打开echo=True设置,这里就查一个吧,因为要输出查询语句,所以输出信息有点儿多,假设查询语句为:

1
print '查询1:%s' % db.session.query(Address).filter_by(id=2).first().user

看看相信的输出结果:

1
2
3
4
5
6
2016-08-20 15:45:29,194 INFO sqlalchemy.engine.base.Engine SELECT address.id AS address_id, address.email AS address_email, address.user_id AS address_user_id, user_1.id AS user_1_id, user_1.name AS user_1_name 
FROM address LEFT OUTER JOIN user AS user_1 ON user_1.id = address.user_id
WHERE address.id = ?
LIMIT ? OFFSET ?
2016-08-20 15:45:29,194 INFO sqlalchemy.engine.base.Engine (2, 1, 0)
查询1:<User:id=2,name=si.li>

看到没有,用的是LEFT OUTER JOIN来查询的,那如果去掉lazy='joined'呢?看看输出结果:

1
2
3
4
5
6
7
8
9
10
2016-08-20 15:47:33,246 INFO sqlalchemy.engine.base.Engine SELECT address.id AS address_id, address.email AS address_email, address.user_id AS address_user_id 
FROM address
WHERE address.id = ?
LIMIT ? OFFSET ?
2016-08-20 15:47:33,246 INFO sqlalchemy.engine.base.Engine (2, 1, 0)
2016-08-20 15:47:33,247 INFO sqlalchemy.engine.base.Engine SELECT user.id AS user_id, user.name AS user_name
FROM user
WHERE user.id = ?
2016-08-20 15:47:33,247 INFO sqlalchemy.engine.base.Engine (2,)
查询1:<User:id=2,name=si.li>

看到没有,这个里面有两个sql,先查address表,可以获取到user_id字段,然后再用这些去user表里面查,所以这里的lazy='joined'相当于反向查询的惰性加载。
还有个奇特的用法,就是自引用,就是类似于无限目录这种机制:

多对对映射

很多时候用ORM框架就是因为业务结构太复杂,复杂性一般体现在表与表之间很多都是多对多映射,如果两个对象是多对多的话怎么映射这个关系呢?难道是在两个实体类里面都用relationship互相声明吗?当然不用,SQLALChecmy里面提供了一个更简单的方法,中间表关联法,还是以UserAddress来说明:
新来增加一张中间表

1
2
3
4
5
user_address = db.Table(
'user_address',
db.Column('User', db.Integer, db.ForeignKey('user.id')),
db.Column('Address', db.Integer, db.ForeignKey('address.id'))
)

这个就不用实体类了,直接用中间表了,然后修改User表的定义:

1
2
addresses = relationship('Address', secondary=user_address, backref=db.backref('user', lazy='joined'),
lazy='dynamic')

然后修改Address实体类定义:

1
2
3
4
5
6
7
8
9
10
class Address(db.Model):
__tablename__ = 'address'
id = Column(Integer, primary_key=True)
email = Column(String)

def __init__(self, email=None):
self.email = email

def __repr__(self):
return '<Address:id={0},email={1}>'.format(self.id, self.email)

去掉了之前在这里面定义的外键user_id,然后我们来测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if __name__ == '__main__':
db.create_all()

add_list = []
for item in ['123@qq.com', '456@qq.com', '789@qq.com']:
address = Address()
address.email = item

add_list.append(address)
db.session.add(address)

user = User()
user.addresses = add_list[:2]
db.session.add(user)

user = User()
user.addresses = add_list[1:]

db.session.commit()

print '查询1:%s' % User.query.first().addresses.all()
print '查询2:%s' % Address.query.filter_by(email='456@qq.com').first().user

创建了3个地址,每个用户拥有2个地址,并且这两个用户都有中间的地址,看看查询结果:

1
2
查询1:[<Address:id=1,email=123@qq.com>, <Address:id=2,email=456@qq.com>]
查询2:[<User:id=1,name=None>, <User:id=2,name=None>]

可以看到反查,正查都可以差多多对多的结果。

最近开始用Flask做一个报表系统,为了方便组织代码,网上了解了一下Flask Blueprint这个东西,算是一个入门,新手容易碰到的问题。
关于蓝图的介绍网上也很多了,我也不多讲,主要是上代码,然后有常见的问题

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
➜  script git:(3a429f0) tree -L 4
.
├── my_site
│   ├── app1
│   │   ├── __init__.py
│   │   ├── templates
│   │   │   └── index.html
│   │   └── views.py
│   ├── app2
│   │   ├── __init__.py
│   │   ├── templates
│   │   │   └── index.html
│   │   └── views.py
│   ├── app3
│   │   ├── __init__.py
│   │   ├── templates
│   │   │   └── index.html
│   │   └── views.py
│   └── __init__.py
├── README.md
└── run.py

项目的目录结构如上面所示,项目根目录为scirpt,真正的项目为my_site目录,app1,app2,app3为三个不同的应用,对应为3个不同模块.
env文件夹为virtualenv虚拟python环境安装目录.

准备工作

安装虚拟Python环境,这个不多说,网上教程一堆,因为我用的Pycharm IDE,所以在Pycharm里面设置成我的安装环境就行。具体方法:File-->Settings-->Project-->Project Interpreter,选你安装虚拟环境的地址即可。

模块

一般自己写着玩或者写个小网站,就几个访问URL,都写在一个views.py里面当然没啥问题,但是项目大了,越来越复杂的话,肯定不可能都写在一个文件里面,极难维护,所以一般会按功能分成不同模块,下面以app1模块为例讲解一下:

  • my_site/app1/views.py
1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from flask import Blueprint, render_template

__author__ = 'anonymous'

app1 = Blueprint('app1', __name__, template_folder='templates')


@app1.route('/index')
def index():
print '访问app1'
return render_template('index.html')

这就相当于完成了一个模块了,模块里访问的index.html模板文件为:

  • my_site/app1/templates/index.html
1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>应用1</title>
</head>
<body>
测试1
</body>
</html>

完成了模块之后还需要注册模块.

  • my_site/__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from flask import Flask

__author__ = 'anonymous'

app = Flask(__name__, instance_relative_config=True)

app.config.from_object('config')

from my_site.app1.views import app1
from my_site.app2.views import app2
from my_site.app3.views import app3

app.register_blueprint(app1, url_prefix='/app1')
app.register_blueprint(app2, url_prefix='/app2')
app.register_blueprint(app3, url_prefix='/app3')

这里注册了三个模块,为了演示,有三个模块,内容就不多虽说了,views.py的内容都是一样的,但是index.html稍作区分,以标志我们访问的是哪一个页面.

运行

然后在项目的根目录下创建一个run.py文件作为主启动文件:

1
2
3
4
5
6
7
8
9
10
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from my_site import app

__author__ = 'anonymous'

if __name__ == '__main__':
app.run(host='localhost', debug=True)

pass

然后直接用IDE运行run.py文件就行。

访问

然后通过浏览器访问http://localhost:5000/app1/index.html,结果返回的值为:

1
测试3

问题有点儿奇怪,通过设置断点,请求确实进入了app1/views.py文件,但是返回的却是app3/index.html的内容,上网查了一下,发现这个问题好像还挺常见的,官方也没把这个问题定义成bug,这个就涉及到Flaskrender_template()函数在查找模板的时候是如何处理模板的了,默认是在项目的目录下查找template文件夹,如果没找到,就去模块下找,最后会把所有模块下找到的模板文件的路径加到一个字典文件里面,因为字典是无序的,所以具体会返回哪个页面得看字典的hash算法了,核心模块代码如下:

  • python2.7/site-packages/flask/templating.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def get_source(self, environment, template):
if self.app.config['EXPLAIN_TEMPLATE_LOADING']:
return self._get_source_explained(environment, template)
return self._get_source_fast(environment, template)

def render_template(template_name_or_list, **context):
"""Renders a template from the template folder with the given
context.

:param template_name_or_list: the name of the template to be
rendered, or an iterable with template names
the first one existing will be rendered
:param context: the variables that should be available in the
context of the template.
"""
ctx = _app_ctx_stack.top
ctx.app.update_template_context(context)
return _render(ctx.app.jinja_env.get_or_select_template(template_name_or_list),
context, ctx.app)

针对这个情况,有两种解决办法:

  • 官方解决方案:

在每个模块的template文件夹下面的模板文件不要重名,怎么做呢?很简单,就是以模块名再建一个文件夹,把所有的模板文件放到这个文件夹下面,最后的结构可能就变成了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
├── config.py
└─── my_site
   ├── app1
   │   ├── __init__.py
   │   ├── templates
   │   │   └── app1
   │   │   └── index.html
   │   └── views.py
   └── app2
      ├── __init__.py
      ├── templates
      │   └── app2
      │   └── index.html
      └── views.py

所以对应的views.py文件中需要改成:

1
return render_template('app1/index.html')
  • 第三方解决方案

既然已经知道问题出在处理模板的文件templating.py文件中,所以在对应额地方加上处理逻辑即可,github上的一位道友给出的解决方案,修改python2.7/site-packages/flask/templating.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def render_template(template_name_or_list, **context):
"""Renders a template from the template folder with the given
context.
:param template_name_or_list: the name of the template to be
rendered, or an iterable with template names
the first one existing will be rendered
:param context: the variables that should be available in the
context of the template.
"""
ctx = _app_ctx_stack.top
ctx.app.update_template_context(context)

template = None

if _request_ctx_stack.top is not None and \
_request_ctx_stack.top.request.blueprint is not None and \
isinstance(template_name_or_list, string_types):
bp = ctx.app.blueprints[_request_ctx_stack.top.request.blueprint]
if bp.jinja_loader is not None:
try:
template = bp.jinja_loader.load(ctx.app.jinja_env,
template_name_or_list,
ctx.app.jinja_env.globals)
except TemplateNotFound:
pass

if template is None:
template = ctx.app.jinja_env\
.get_or_select_template(template_name_or_list)

return _render(template, context, ctx.app)

然后再访问就可以了。
**注意:**两种方式都可以,但是推荐使用第一种官方的方案,因为如果改源码,虽然可以正常运行,但是换个环境,或者项目重新给别人部署,如果不加说明,不知道的人不会去改Flask代码,那么这个项目则会出错。

很多时候需要测试一下我们的sql逻辑是否有问题,或者想在本地调试一个东西,线上的Hadoop跑的太慢,要视资源和进群调度情况而定,所以本地调试无疑是效率最高的方式,记录一下本地配置Hive的过程。

系统环境

1
2
3
4
5
6
7
OS: Ubuntu 16.04 LTS 64bit
JDK: 1.7.0_40
Hadoop:hadoop-2.6.4.tar.gz
MySQL: 5.7.13
Hive: hive-2.1.0
➜ Blog git:(master) ✗ whoami
anonymous

安装步骤

首先你需要安装配置好Jdk,Hadoop,MySQL这三个东西,前面两个可以参见上一篇笔记Ubuntu 16 04 Hadoop本地安装配置

安装MySQL

MySQL安装比较简单:

1
sudo apt install -y mysql-server

设置对应的密码就行,其他的默认就可以,比较简单就不细讲了.主要讲后面的。

创建Hive用户

登陆MySQL,创建Hive用户:

1
2
3
mysql -uroot -proot
create user 'hive' identified by 'hive';
grant all privileges on *.* to 'hive'@'localhost' identified by 'hive';

**注意:**第一个hive是创建的用户名为hive,identified by后面的那个hive是密码。
然后用刚创建的用户登陆,并创建数据库:

1
2
mysql -uhive -phive
mysql> create database hive;

安装Hive

去Apache官网下载就行Hive官网地址,我这里下载的是最新的版本apache-hive-2.1.0-bin.tar.gz:

1
2
sudo tar -xvf apache-hive-2.1.0-bin.tar.gz -C /usr/dev/
sudo chown -R anonymous /usr/dev/apache-hive-2.1.0-bin/

然后需要配置Hive,一下操作默认都是在/usr/dev/apache-hive-2.1.0-bin目录中操作的.

1
cp conf/hive-default.xml.template conf/hive-site.xml

然后修改hive-site.xml文件中对应位置的内容,根据实际情况修改,比如数据库的名字,用户名,密码等,我的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<property>
<name>hive.metastore.warehouse.dir</name>
<value>/usr/dev/apache-hive-2.1.0-bin/warehouse</value>
<description>location of default database for the warehouse</description>
</property>
<property>
<name>javax.jdo.option.ConnectionURL</name>
<value>jdbc:mysql://localhost:3306/hive?characterEncoding=UTF-8&amp;createDatabaseIfNotExist=true</value>
<description>
JDBC connect string for a JDBC metastore.
To use SSL to encrypt/authenticate the connection, provide database-specific SSL flag in the connection URL.
For example, jdbc:postgresql://myhost/db?ssl=true for postgres database.
</description>
</property>
<property>
<name>javax.jdo.option.ConnectionDriverName</name>
<value>com.mysql.jdbc.Driver</value>
<description>Driver class name for a JDBC metastore</description>
</property>
<property>
<name>javax.jdo.option.ConnectionUserName</name>
<value>hive</value>
<description>Username to use against metastore database</description>
</property>
<property>
<name>javax.jdo.option.ConnectionPassword</name>
<value>hive</value>
<description>password to use against metastore database</description>
</property>

MySQL主要用来存储元数据,即一些表的信息,默认的是Derby,现在我们改为用MySQL,所以还需要把Jdbc驱动包拷贝到Hive的库目录下,如果本地没有可以去MySQL官网下载,不过如果你用过JetBrain系列的IDE,例如IDEA,PyCharm,可以在该用户目录下的.IntelliJIdea14目录下找到这个jar包,把这个jar包拷贝到Hive库目录下即可:

1
locate mysql-connector-java

根据查找内容找到jar包所在目录,可能会有很多地方有这个jar包,随便拷一个就行,拷贝该jar包即可,以我的为例:

1
cp /home/anonymous/.m2/repository/mysql/mysql-connector-java/5.1.16/mysql-connector-java-5.1.16.jar /usr/dev/apache-hive-2.1.0-bin/lib

到这里终于可以启动Hive试一试了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
./bin/hive
Logging initialized using configuration in jar:file:/usr/dev/apache-hive-2.1.0-bin/lib/hive-common-2.1.0.jar!/hive-log4j2.properties Async: true
Exception in thread "main" java.lang.RuntimeException: org.apache.hadoop.hive.ql.metadata.HiveException: org.apache.hadoop.hive.ql.metadata.HiveException: MetaException(message:Hive metastore database is not initialized. Please use schematool (e.g. ./schematool -initSchema -dbType ...) to create the schema. If needed, don't forget to include the option to auto-create the underlying database in your JDBC connection string (e.g. ?createDatabaseIfNotExist=true for mysql))
at org.apache.hadoop.hive.ql.session.SessionState.start(SessionState.java:578)
at org.apache.hadoop.hive.ql.session.SessionState.beginStart(SessionState.java:518)
at org.apache.hadoop.hive.cli.CliDriver.run(CliDriver.java:705)
at org.apache.hadoop.hive.cli.CliDriver.main(CliDriver.java:641)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at org.apache.hadoop.util.RunJar.run(RunJar.java:221)
at org.apache.hadoop.util.RunJar.main(RunJar.java:136)
Caused by: org.apache.hadoop.hive.ql.metadata.HiveException: org.apache.hadoop.hive.ql.metadata.HiveException: MetaException(message:Hive metastore database is not initialized. Please use schematool (e.g. ./schematool -initSchema -dbType ...) to create the schema. If needed, don't forget to include the option to auto-create the underlying database in your JDBC connection string (e.g. ?createDatabaseIfNotExist=true for mysql))
at org.apache.hadoop.hive.ql.metadata.Hive.registerAllFunctionsOnce(Hive.java:226)

启动不了,看错误信息里面有提示,好像是元数据库没有初始化,就照着搞一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
./bin/schematool -initSchema -dbType mysql
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/usr/dev/apache-hive-2.1.0-bin/lib/log4j-slf4j-impl-2.4.1.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/usr/dev/hadoop-2.6.4/share/hadoop/common/lib/slf4j-log4j12-1.7.5.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.apache.logging.slf4j.Log4jLoggerFactory]
Metastore connection URL: jdbc:mysql://localhost:3306/hive?createDatabaseIfNotExist=true
Metastore Connection Driver : com.mysql.jdbc.Driver
Metastore connection User: hive
Starting metastore schema initialization to 2.1.0
Initialization script hive-schema-2.1.0.mysql.sql
Initialization script completed
schemaTool completed

好,再启动一下试试:

1
2
3
4
5
6
7
8
9
10
11
12
./bin/hive
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/usr/dev/apache-hive-2.1.0-bin/lib/log4j-slf4j-impl-2.4.1.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/usr/dev/hadoop-2.6.4/share/hadoop/common/lib/slf4j-log4j12-1.7.5.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.apache.logging.slf4j.Log4jLoggerFactory]

Logging initialized using configuration in jar:file:/usr/dev/apache-hive-2.1.0-bin/lib/hive-common-2.1.0.jar!/hive-log4j2.properties Async: true
Exception in thread "main" java.lang.IllegalArgumentException: java.net.URISyntaxException: Relative path in absolute URI: ${system:java.io.tmpdir%7D/$%7Bsystem:user.name%7D
at org.apache.hadoop.fs.Path.initialize(Path.java:206)
at org.apache.hadoop.fs.Path.<init>(Path.java:172)
.......

尼玛,还是不行,继续排查,同样,看错误信息貌似是配置文件里面的${system:java.io.tmpdir这个出问题了,解决方法:替换掉conf/hive-site.xml里面的所有的${system:java.io.tmpdir}为固定值,比如我设置的就是/usr/dev/apache-hive-2.1.0-bin/tmp,只列出两个,有好几处,都得替换掉:

1
2
3
4
5
6
7
8
9
10
<property>
<name>hive.exec.local.scratchdir</name>
<value>/usr/dev/apache-hive-2.1.0-bin/tmp/${system:user.name}</value>
<description>Local scratch space for Hive jobs</description>
</property>
<property>
<name>hive.downloaded.resources.dir</name>
<value>/usr/dev/apache-hive-2.1.0-bin/tmp/${hive.session.id}_resources</value>
<description>Temporary local directory for added resources in the remote file system.</description>
</property>

好,再启动试一下:

1
2
3
4
5
6
7
8
9
10
./bin/hive
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/usr/dev/apache-hive-2.1.0-bin/lib/log4j-slf4j-impl-2.4.1.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/usr/dev/hadoop-2.6.4/share/hadoop/common/lib/slf4j-log4j12-1.7.5.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.apache.logging.slf4j.Log4jLoggerFactory]

Logging initialized using configuration in jar:file:/usr/dev/apache-hive-2.1.0-bin/lib/hive-common-2.1.0.jar!/hive-log4j2.properties Async: true
Hive-on-MR is deprecated in Hive 2 and may not be available in the future versions. Consider using a different execution engine (i.e. tez, spark) or using Hive 1.X releases.
hive>

终于启动成功了,试一下基本的命令:

1
2
3
4
hive> show databases;
OK
Failed with exception java.io.IOException:java.lang.IllegalArgumentException: java.net.URISyntaxException: Relative path in absolute URI: ${system:user.name%7D
Time taken: 0.99 seconds

尼玛,又不行,同样的,把conf/hive-site.xml里面的${system:user.name}全部替换成${user.name},然后重新试一下:

1
2
3
4
5
6
7
hive> show tables;
OK
Time taken: 0.981 seconds
hive> show databases;
OK
default

到此终于搞定了。试下创建一张表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
hive> create database dw_subject;
OK
Time taken: 0.233 seconds
hive> show databases;
OK
default
dw_subject
Time taken: 0.017 seconds, Fetched: 2 row(s)
hive> use dw_subject;
OK
Time taken: 0.021 seconds
hive> create table table_test (
> id bigint comment '自增id',
> name string comment '姓名'
> ) comment '测试表'
> ROW FORMAT DELIMITED
> FIELDS TERMINATED BY '\t'
> LINES TERMINATED BY '\n';
OK
Time taken: 0.315 seconds
hive> show create table table_test;
OK
CREATE TABLE `table_test`(
`id` bigint COMMENT '??id',
`name` string COMMENT '??')
COMMENT '???'
ROW FORMAT SERDE
'org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe'
WITH SERDEPROPERTIES (
'field.delim'='\t',
'line.delim'='\n',
'serialization.format'='\t')
STORED AS INPUTFORMAT
'org.apache.hadoop.mapred.TextInputFormat'
OUTPUTFORMAT
'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION
'hdfs://localhost:9000/user/anonymous/warehouse/dw_subject.db/table_test'
TBLPROPERTIES (
'COLUMN_STATS_ACCURATE'='{\"BASIC_STATS\":\"true\"}',
'numFiles'='0',
'numRows'='0',
'rawDataSize'='0',
'totalSize'='0',
'transient_lastDdlTime'='1469260899')
Time taken: 0.156 seconds, Fetched: 23 row(s)

这里可以看到有个问题,中文注释乱码了,怎么解决呢?网上有相关的教程,需要修改源码,重新编译hive-exec-2.1.0.jar这个包,暂时能用了,本机基本上是测试用,中文乱码也不影响使用,有空再研究怎么去乱码。

Hive中文乱码

乱码的问题网上也都一大片,可能每个人的问题还不一样,以我的为例,我的问题还不是乱码,是?,所有的中文都显示成?了。联系到有时候用MySQL没指定编码也会出现问题,我查看了一下hive的元数据表:

1
2
3
4
5
6
7
8
9
10
| COLUMNS_V2 | CREATE TABLE `COLUMNS_V2` (
`CD_ID` bigint(20) NOT NULL,
`COMMENT` varchar(256) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,
`COLUMN_NAME` varchar(767) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
`TYPE_NAME` varchar(4000) DEFAULT NULL,
`INTEGER_IDX` int(11) NOT NULL,
PRIMARY KEY (`CD_ID`,`COLUMN_NAME`),
KEY `COLUMNS_V2_N49` (`CD_ID`),
CONSTRAINT `COLUMNS_V2_FK1` FOREIGN KEY (`CD_ID`) REFERENCES `CDS` (`CD_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 |

可能老版本的是叫COLUMNS,这个差不多,视版本而定,可以看到存注释的字段确实不是用的utf8编码:

1
COMMENT varchar(256) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,

然后重启hive客户端,然后执行desc命令,发现还是问号,于是把表删了,然后把建表语句重新执行之后,再执行desc命令,果然好了:

1
2
3
4
5
ve> desc test.window_analysis;
OK
user_id bigint 用户id
reg_time timestamp 注册时间
Time taken: 0.091 seconds, Fetched: 2 row(s)

但是show create table xxx语句的还是乱码。

解决show create table乱码

首先要去官网下载源码自己编译Hive源码,

线上集群测试太慢,有时候需要在本地跑一些计算或者测试某个逻辑,主要做调试用,所以在本地也装一个Hive测试用,但是装Hive需要先安装Hadoop.

准备工作

开发环境为:

1
2
3
4
5
6
OS: Ubuntu 16.04 LTS 64bit
JDK: 1.7.0_40
ssh server:1:7.2p2-4ubuntu1
Hadoop:hadoop-2.6.4.tar.gz
➜ Blog git:(master) ✗ whoami
anonymous

**注意:**当前用户为anonymous,下面所有涉及到用户的地方,需要对应修改为你自己的用户名。

安装步骤

具体的安装步骤可能有些多,具体过程如下:

ssh登陆配置

先安装ssh server程序:

1
2
3
➜  sudo apt update
➜ sudo apt install openssh-server -y
➜ ssh localhost

输入密码之后可以登陆则没有问题,然后使用exit命令注销当前用户,直接忽略下面的异常问题,
如果第一步出现下面的错误,按照下面的方法解决即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
忽略:1 http://dl.google.com/linux/chrome/deb stable InRelease
获取:2 http://archive.ubuntukylin.com:10006/ubuntukylin xenial InRelease [3,192 B]
命中:3 http://cn.archive.ubuntu.com/ubuntu xenial InRelease
错误:2 http://archive.ubuntukylin.com:10006/ubuntukylin xenial InRelease
由于没有公钥,无法验证下列签名: NO_PUBKEY 8D5A09DC9B929006
命中:4 http://cn.archive.ubuntu.com/ubuntu xenial-updates InRelease
获取:5 http://cn.archive.ubuntu.com/ubuntu xenial-backports InRelease [92.2 kB]
命中:6 http://security.ubuntu.com/ubuntu xenial-security InRelease
命中:7 http://dl.google.com/linux/chrome/deb stable Release
正在读取软件包列表... 完成
W: GPG 错误:http://archive.ubuntukylin.com:10006/ubuntukylin xenial InRelease: 由于没有公钥>,无法验证下列签名: NO_PUBKEY 8D5A09DC9B929006
E: 仓库 “http://archive.ubuntukylin.com:10006/ubuntukylin xenial InRelease” 没有数字签名。
N: 无法安全地用该源进行更新,所以默认禁用该源。
N: 参见 apt-secure(8) 手册以了解仓库创建和用户配置方面的细节。

需要按如下方案解决,手动添加上面报错的签名8D5A09DC9B929006:

1
➜  sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 8D5A09DC9B929006

生成密钥

每次登陆都需要输密码很不方便,设置免密码登陆,记得注销当前用户exit:

1
2
3
➜  cd ~/.ssh
➜ sh-keygen -t rsa
➜ cat id_rsa.pub >> authorized_keys

**注意:**生成密钥的时候一路回车,不要输入任何东西,如果进行到这一步,我们就可以切回原来的系统了,然后使用:

1
ssh localhost

安装JDK

这个安装也很简单,解压然后配置环境变量即可:

1
sudo tar -xvf jdk-7u40-linux-x64.tar.gz -C /usr/dev

然后编辑~/.bashrc文件:

1
2
3
4
export JAVA_HOME=/usr/dev/jdk1.7.0_40
export JRE_HOME=${JAVA_HOME}/jre
export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib
export PATH=${JAVA_HOME}/bin:$PATH

然后更新一下文件:

1
➜  source ~/.bashrc

测试JDK是否配置正确:

1
2
3
4
➜  java -version
java version "1.7.0_40"
Java(TM) SE Runtime Environment (build 1.7.0_40-b43)
Java HotSpot(TM) 64-Bit Server VM (build 24.0-b56, mixed mode)

安装Hadoop

确认JDK安装正确之后,下面就可以来安装Hadoop了,进入到压缩包所在目录:

1
2
➜  sudo tar -xvf hadoop-2.6.4.tar.gz -C /usr/dev/
➜ sudo chown -R anonymous /usr/dev/hadoop-2.6.4/

看看是否正确安装了:

1
2
3
4
5
6
7
8
➜  cd /usr/dev/hadoop-2.6.4
➜ ./bin/hadoop version
Hadoop 2.6.4
Subversion https://git-wip-us.apache.org/repos/asf/hadoop.git -r 5082c73637530b0b7e115f9625ed7fac69f937e6
Compiled by jenkins on 2016-02-12T09:45Z
Compiled with protoc 2.5.0
From source with checksum 8dee2286ecdbbbc930a6c87b65cbc010
This command was run using /usr/dev/hadoop-2.6.4/share/hadoop/common/hadoop-common-2.6.4.jar

可以识别到版本就说明没啥问题了。
为了方便以后使用,可以再配置一下环境变量:

1
2
export HADOOP_HOME=/usr/dev/hadoop-2.6.4
export PATH=${HADOOP_HOME}/bin:$PATH

**注意:**后面的操作如果没有特殊指明,都是在hadoop的安装目录/usr/dev/hadoop-2.6.4下进行的操作

单机配置

单机配置比较简单,我们以一个简单的单词统计来看看怎么配置:

1
2
3
4
5
6
➜  cd /usr/dev/hadoop-2.6.4
➜ mkdir input
➜ cp etc/hadoop/*.xml input/
➜ ./bin/hadoop jar share/hadoop/mapreduce/hadoop-mapreduce-examples-2.6.4.jar grep input/ output 'dfs[a-z.]+'
➜ cat output/*
1 dfsadmin

伪分布式

伪分布式需要修改两个文件etc/hadoop/core-site.xml以及etc/hadoop/hdfs-site.xml,修改对应位置,改为如下内容:

  • etc/hadoop/core-site.xml
1
2
3
4
5
6
7
8
9
10
11
<configuration>
<property>
<name>hadoop.tmp.dir</name>
<value>file:/usr/dev/hadoop-2.6.4/tmp</value>
<description>Abase for other temporary directories.</description>
</property>
<property>
<name>fs.defaultFS</name>
<value>hdfs://localhost:9000</value>
</property>
</configuration>
  • etc/hadoop/hdfs-site.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<configuration>
<property>
<name>dfs.replication</name>
<value>1</value>
</property>
<property>
<name>dfs.namenode.name.dir</name>
<value>file:/usr/dev/hadoop-2.6.4/tmp/dfs/name</value>
</property>
<property>
<name>dfs.datanode.data.dir</name>
<value>file:/usr/dev/hadoop-2.6.4/tmp/dfs/data</value>
</property>
</configuration>

格式化节点:

1
2
3
4
5
6
7
8
9
➜  ./bin/hdfs namenode -format
............省略一堆调试信息............
16/07/19 16:23:54 INFO common.Storage: Storage directory /usr/dev/hadoop-2.6.4/tmp/dfs/name has been successfully formatted.
16/07/19 16:23:54 INFO namenode.NNStorageRetentionManager: Going to retain 1 images with txid >= 0
16/07/19 16:23:54 INFO util.ExitUtil: Exiting with status 0
16/07/19 16:23:54 INFO namenode.NameNode: SHUTDOWN_MSG:
/************************************************************
SHUTDOWN_MSG: Shutting down NameNode at anonymous/127.0.1.1
************************************************************/

**注意:**如果消息最后不是been successfully formatted.以及返回值不是Exiting with status 0则说明执行失败。

开启NameNodeDataNode守护进程:

1
2
3
4
5
6
➜  ./sbin/start-dfs.sh
Starting namenodes on [localhost]
localhost: Error: JAVA_HOME is not set and could not be found.
localhost: Error: JAVA_HOME is not set and could not be found.
Starting secondary namenodes [0.0.0.0]
0.0.0.0: Error: JAVA_HOME is not set and could not be found.

出现这个错误,并不是由于jdk没设置好,不然前面的也不能运行,只好手动修改文件etc/hadoop/hadoop-env.sh,原来的地方是export JAVA_HOME=${JAVA_HOME}改成~/.bashrc里面的内容:

1
export JAVA_HOME=/usr/dev/jdk1.7.0_40

记得要source ~/.bashrc,然后再试一下:

1
2
3
4
5
6
➜  ./sbin/start-dfs.sh
Starting namenodes on [localhost]
localhost: starting namenode, logging to /usr/dev/hadoop-2.6.4/logs/hadoop-hadoop-namenode-anonymous.out
localhost: starting datanode, logging to /usr/dev/hadoop-2.6.4/logs/hadoop-hadoop-datanode-anonymous.out
Starting secondary namenodes [0.0.0.0]
0.0.0.0: starting secondarynamenode, logging to /usr/dev/hadoop-2.6.4/logs/hadoop-hadoop-secondarynamenode-anonymous.out

看看进程:

1
2
3
4
5
➜  jps
19407 SecondaryNameNode
19239 DataNode
19104 NameNode
19574 Jps

**注意:**没有NameNode或者DataNodeNameNode则不对,正常启动之后可以在Web页面http://localhost:50070查看:

伪分布式实例

最开始演示了一个单机模式的单词统计,这里来一个统计hdfs文件统计的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜  ./bin/hdfs dfs -mkdir -p /user/hadoop
➜ ./bin/hdfs dfs -mkdir input
➜ ./bin/hdfs dfs -put etc/hadoop/*.xml input/
➜ ./bin/hdfs dfs -ls input/
Found 8 items
-rw-r--r-- 1 anonymous supergroup 4436 2016-07-19 19:37 input/capacity-scheduler.xml
-rw-r--r-- 1 anonymous supergroup 1051 2016-07-19 19:37 input/core-site.xml
-rw-r--r-- 1 anonymous supergroup 9683 2016-07-19 19:37 input/hadoop-policy.xml
-rw-r--r-- 1 anonymous supergroup 1105 2016-07-19 19:37 input/hdfs-site.xml
-rw-r--r-- 1 anonymous supergroup 620 2016-07-19 19:37 input/httpfs-site.xml
-rw-r--r-- 1 anonymous supergroup 3523 2016-07-19 19:37 input/kms-acls.xml
-rw-r--r-- 1 anonymous supergroup 5511 2016-07-19 19:37 input/kms-site.xml
-rw-r--r-- 1 anonymous supergroup 690 2016-07-19 19:37 input/yarn-site.xml

➜ ./bin/hadoop jar share/hadoop/mapreduce/hadoop-mapreduce-examples-2.6.4.jar grep input/ output 'dfs[a-z.]+'

查看hdfs文件的统计结果可以使用:

1
2
3
4
5
➜  ./bin/hdfs dfs -cat output/*
1 dfsadmin
1 dfs.replication
1 dfs.namenode.name.dir
1 dfs.datanode.data.dir

结果也可以取回本地,当然记得先把本地的文件夹删了:

1
2
3
➜  rm -r output
➜ ./bin/hdfs dfs -get output output
➜ cat output/*

配置Yarn

有时候我们还需要一个任务或资源的调度器,当然如果是本地的任务,这个功能其实没啥用,反而会是任务运行变慢,这里仅当学习使用:

1
➜  cp etc/hadoop/mapred-site.xml.template etc/hadoop/mapred-site.xml

编辑下面两个文件etc/hadoop/mapred-site.xmletc/hadoop/yarn-site.xml:

  • etc/hadoop/mapred-site.xml
1
2
3
4
5
6
<configuration>
<property>
<name>mapreduce.framework.name</name>
<value>yarn</value>
</property>
</configuration>
  • etc/hadoop/yarn-site.xml
1
2
3
4
5
6
7
<configuration>
<!-- Site specific YARN configuration properties -->
<property>
<name>yarn.nodemanager.aux-services</name>
<value>mapreduce_shuffle</value>
</property>
</configuration>

启动资源管理器

1
2
➜  ./sbin/start-yarn.sh
➜ ./sbin/mr-jobhistory-daemon.sh start historyserver

集群资源管理器的访问地址为:http://localhost:8088/cluster,另外第二条命令是为了看历史任务的。

关闭资源你管理器

1
2
➜  ./sbin/stop-yarn.sh
➜ ./sbin/mr-jobhistory-daemon.sh stop historyserver

公司的日志一般会有专门的日志收集系统,但是上传到hdfs上目录太多,一般都是按机房,按小时分割日志文件的。路径类似于下面这样:

1
2
3
4
/user/xxx/l-xxxx1.pay.cn1/20160717/log.20160717-18.gz
/user/xxx/l-xxxx1.pay.cn1/20160717/log.20160717-19.gz
/user/xxx/l-xxxx2.pay.cn1/20160717/log.20160717-18.gz
/user/xxx/l-xxxx3.pay.cn1/20160717/log.20160717-19.gz

即日志文件会按小时打包log.20160717-xx.gz,另外日志可能会标注机房l-xxxx[1-9].pay.cn[1-9]不同机器,这样会对应很多个目录,这样就无法通过在Hive里面建一个表指向一个固定的hdfs路径来分析日志数据。我们必须通过一个方法把这些日志转化到一个路径,然后把数据load到Hive表中,然后就可以对日志的数据做一些分析,或者使用了。

Hadoop Streaming

这里使用到的是Hadoop原生的Streaming,毕竟我们的目的也不是很负复杂,就是一个数据收集汇总的过程,当然这中间也可以做一些简单的处理,例如过滤掉不需要的日志,毕竟日志不比MySQL里面的结构化数据,日志的量级一般都很大,都装到Hive表里面数据量大分析的时候也需要更多的计算资源,也更慢

由于服务器上的hadoop是2.2.0版本的,所以我这里使用的是hadoop-streaming-2.2.0.jar。主要使用shell,python简单介绍一下如何使用这个。

1
2
3
4
5
$HADOOP_HOME/bin/hadoop  jar $HADOOP_HOME/hadoop-streaming.jar \
-input myInputDirs \
-output myOutputDir \
-mapper /bin/cat \
-reducer /bin/wc

上面是具体的在shell里面执行的时候参数的指定,这里最简单的用法就是执行输入目录或者输入文件,以我上面的例子为例,涉及到多个机房,日志还按小时分割打包,可以写成

1
-input /user/xxx/l-xxxx*.pay.cn*/20160717/log.20160717-*.gz

即路径可以使用通配符*指定,然后还要指定一个汇总日志输出路径:

1
-output /user/xxx/logs/xxxx/output/20160717/log_parse 

这里主要是想讲一下mapperreducer文件的编写,采用python实现:
先看一下mapper.py文件的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/env python
# coding=utf-8

import re
import sys

reg_pattern = r'(\d+-\d+-\d+ \d+:\d+:\d+.\d+).*GwRouteBizImpl\s*-\s*(\w*)\{(.*)\}.*'
re_gx = re.compile(reg_pattern)

for line in sys.stdin:
line = line.strip().replace('\t', ' ').replace('\r', ' ')

if re_gx.match(line):
print '%s' % re.sub(reg_pattern, r'\1\t\2\t\3', line)

这就是一个最简单的过滤日志,并简单做一些字段的提取,因为我们采用\t分割,所以需要把每一行的特殊字符去掉,然后用正则匹配满足格式的日志,如果不满足,这条日志就直接过滤掉了。
**注意:**使用Hadoop原生的Streaming程序,只能处理标准输入输出,另外如果对输入不做任何处理,可以直接使用系统自带的/bin/cat,不用单独写mapper或者reducer,当然你也可以使用python写一个,什么也不干,原样输出reducer.py:

1
2
3
4
5
import sys

for line in sys.stdin:
line = line.strip()
print '%s\t' % line

具体使用就很简单了,在shell里面调用:

sudo -u${HADOOP_USER} ${HADOOP_HOME}/bin/hadoop jar ${HADOOP_HOME}/share/hadoop/tools/lib/hadoop-streaming-2.2.0.jar -D stream.non.zero.exit.is.failure=false -mapper “python mapper.py” -reducer “python reducer.py” -input ${HADOOP_LOG_FILE} -output ${HADOOP_OUTPUT_DIR} -file reducer.py -file mapper.py

**备注:**如果mapper,reducer不是使用系统的shell命令,那么就需要加上-file参数来把我们用其他程序写的代码分发到所有节点。另外还有一个地方需要注意,要确保-output output_dir路径不存在,你需要在调用之前调用删除命令:

1
sudo -u${HADOOP_USER} ${HADOOP_HOME}/bin/hadoop fs -rm -r output_dir

还有一些常用的参数设置:

1
-jobconf <key>=<value>

例如:

1
-jobconf mapreduce.job.queue.name=xxx -jobconf mapreduce.job.name=xxx

常用操作

对于日志这种,其实是多路输入,合并到一路输出,中间只是把一行内容变成一行内容,用特定字符分隔开来而已,所以并没有reduce过程,如果我们用了上面那个参数,最后会又有一个reducer,特别慢,关键是有可能内存不够,所以可以取消掉

1
-jobconf mapred.reduce.tasks=0

这样就不会有reducer了,会快很多。

0%