Skip to content

블로그

Django에서 Jinja2를 Template Engine으로 사용하기

Django는 대표적인 Python 웹 프레임워크 입니다. 페이지 렌더링, ORM, Session 자체 Template Engine도 가지고 있지요.

그런데 개인적인 의견으로 Template Engine은 Jinja2가 더 강력한 것 같습니다. 다행히 Django에서 Jinja2도 지원을 하고 있습니다. 다만 약간의 추가적인 세팅이 필요하죠.

이 포스트에서 Django에서 Jinja2를 사용하는 방법을 차근차근 설명하고자 합니다.

우선 Jinja2를 설치합니다.

Terminal window
$ pip install Jinja2

Django 프로젝트의 settings.py 파일을 수정합니다.

TEMPLATES = [
{
'BACKEND': 'django.template.backends.jinja2.Jinja2',
'DIRS': [
'app/templates'
],
'APP_DIRS': True,
'OPTIONS': {
'extensions': [
'jinja2.ext.do',
'jinja2.ext.loopcontrols',
'jdj_tags.extensions.DjangoCompat',
],
},
},
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]

여기서 Jinja2 부분의 OPTION/extensions에 Jinja2에서 기본 제공하는 확장 기능들을 추가할 수 있습니다. jinja2.ext.dojinja2.ext.loopcontrols는 Jinja2의 기본 확장 기능입니다. Jinja2 Extensions에서 다른 확장 기능들도 확인해 보세요. jdj_tags.extensions.DjangoCompat는 Django Template Engine이 제공하는 Template Tag들을 Jinja2 버전으로 만들어 놓은 커스텀 확장 기능입니다. 이걸 사용하기 위해서 pip install jinja2-django-tags로 모듈을 설치합니다.

이렇게 하면 이제 Jinja2를 Template Engine으로 사용하게 됩니다. 그런데 Jinja2에서는 Custom Filter를 기존 Django 방식으로 할 수 없습니다.

필터 함수들을 가지고 있는 모듈 filters.py를 다음과 같이 정의합니다.

""" filters.py
"""
from django.utils.http import urlquote
# collection
def filter_getitem(value, arg):
return value.get(arg, '')
def filter_getat(value, arg):
try:
return value[int(arg)]
except Exception as e:
return None
def filter_notinappend(a, b):
if b not in a:
a.append(b)
return a
def filter_union(a, b):
return set(a) | set(b)
# math
def filter_divide(value, arg):
return float(value) / float(arg)
def filter_mod(value, arg):
return int(value) % int(arg)
def filter_min(a, b):
return min(a, b)
def filter_max(a, b):
return max(a, b)
# converting
def filter_int(value):
return int(value)
# type
def filter_type(value):
return str(type(value))
# str
def filter_endswith(value, arg):
return value.endswith(arg)
def filter_quote(a):
return urlquote(a)

filters.py의 필터들을 추가하기 위해 settings.py에 다음과 같은 코드를 추가합니다.

jinja2.filters.FILTERS.update({
"getitem": filters.filter_getitem,
"getat": filters.filter_getat,
"notinappend": filters.filter_notinappend,
"union": filters.filter_union,
"divide": filters.filter_divide,
"mod": filters.filter_mod,
"min": filters.filter_min,
"max": filters.filter_max,
"int": filters.filter_int,
"type": filters.filter_type,
"endswith": filters.filter_endswith,
"quote": filters.filter_quote,
})

이렇게 하면 html에서 Jinja2 태그들 및 새롭게 추가한 Custom Filter들도 사용할 수 있습니다.

예를 들어서 다음 html 코드는

{% raw %}<input type="number" value="{{ -100|max(0) }}">{% endraw %}

추가한 max 필터를 거쳐서 <input type="number" value="0">이 되는 것입니다.

Google과 Naver에서 동시 검색하는 Chrome Extension 만들기

Google과 Naver를 동시에 검색해 보고 싶어 졌습니다. 처음에는 iframe으로 그냥 띄우면 되지 않을까 했는데 문제가 생겼습니다. Google과 Naver와 같이 X-Frame-OptionsDENY 또는 SAMEORIGIN으로 설정되어 있는 사이트는 iframe으로 띄울 수가 없습니다.

그래서 생각한 것이 Chrome Extension으로 만들어 보는 것이였습니다. Chrome Extension 개발의 기본적인 사항은 Getting Started: Building a Chrome Extension에서 확인하시면 됩니다.

코드는 간단합니다. 우선 HTML 코드 입니다.

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Google과 Naver 동시 검색 확장앱</title>
<link rel="stylesheet" type="text/css" href="lib/bootstrap-4.0.0-beta/css/bootstrap.css">
<link rel="stylesheet" type="text/css" href="lib/font-awesome-4.7.0/css/font-awesome.min.css">
<link rel="stylesheet" href="browser.css">
<script type="text/javascript" src="lib/jquery-3.2.1.min.js"></script>
<script type="text/javascript" src="lib/popper.min.js"></script>
<script type="text/javascript" src="lib/bootstrap-4.0.0-beta/js/bootstrap.min.js"></script>
<script src="browser.js"></script>
</head>
<body>
<div id="layout_srch">
<div class="row">
<div class="col">
<a id="a_back_g" href="#"><i class="fa fa-arrow-left my-control" aria-hidden="true"></i></a>
</div>
<div class="col-10">
<form id="form_srch" class="form-inline">
<div id="dv_srch" class="form-group">
<input id="inp_query" class="form-control mr-2" value="">
<button id="btn_srch" class="btn btn-success">
<i class="fa fa-search" aria-hidden="true"></i>
</button>
</div>
<div class="form-check mb-2 mr-sm-2 mb-sm-0">
<label class="form-check-label">
<input id="ckb_mode" type="checkbox" class="form-check-input">
모바일 모드
</label>
</div>
</form>
</div>
<div class="col">
<a id="a_back_n" href="#"><i class="fa fa-arrow-left my-control" aria-hidden="true" style="float: right"></i></a>
</div>
</div>
</div>
<webview id="ifr_g" src="https://www.google.co.kr/search?q="></webview>
<webview id="ifr_n" src="https://search.naver.com"></webview>
</body>
</html>

상단에 검색 폼을 두고 왼쪽에는 구글 웹뷰, 오른쪽에는 네이버 웹뷰를 두었습니다.

상단의 검색 폼에서 질의를 입력하고 검색 버튼을 누르면 이 두 웹뷰를 각 URL에 질의를 추가하여 다시 로딩합니다. 이를 위한 JavaScript 코드는 다음과 같습니다.

$(function() {
var webview_g = $('#ifr_g');
var webview_n = $('#ifr_n');
var version = navigator.appVersion.substr(navigator.appVersion.lastIndexOf('Chrome/') + 7);
var match = /([0-9]*)\.([0-9]*)\.([0-9]*)\.([0-9]*)/.exec(version);
var majorVersion = parseInt(match[1]);
var buildVersion = parseInt(match[3]);
var AGENT_PC = webview_g.get(0).getUserAgent();
var AGENT_MOBILE = "Mozilla/5.0 (Linux; Android 7.0; SM-G930V Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36";
function search() {
var query = $('#inp_query').val();
webview_g.attr('src', "https://www.google.co.kr/search?q="+query);
webview_n.attr('src', "https://search.naver.com/search.naver?query="+query);
}
$('#btn_srch').click(search);
$('#inp_query').keypress(function(event) {
if (event.which == 13) {
document.querySelector('#btn_srch').click();
event.preventDefault();
}
});
$('#ckb_mode').change(function () {
var checked = $(this).prop('checked');
if (checked) {
webview_g.get(0).setUserAgentOverride(AGENT_MOBILE);
webview_n.get(0).setUserAgentOverride(AGENT_MOBILE);
} else {
webview_g.get(0).setUserAgentOverride(AGENT_PC);
webview_n.get(0).setUserAgentOverride(AGENT_PC);
}
search();
});
function adjust_height() {
var height = window.innerHeight - 60;
webview_g.css('height', height);
webview_n.css('height', height);
}
adjust_height();
window.onresize = adjust_height;
});

여기서 주의할 점은 웹뷰의 에이전트를 잘 정해줘야 한다는 것입니다. 에이전트가 잘 정의되어 있지 않으면 (구글, 네이버가 막아놔서) 검색이 되지 않을 수도 있습니다.

추가로 CSS도 공유합니다.

body {
margin: 0;
padding: 0;
min-width: 800px;
}
#layout_srch {
position: fixed;
top:0px;
right:0px;
left:0px;
height: 60px;
border-bottom: 1px solid #eee;
}
#layout_srch .my-control {
color: #fff;
margin: 10px;
padding: 10px;
}
#form_srch {
margin: 0px auto;
width: 600px;
padding: 10px;
}
#inp_query {
width: 400px;
}
#lb_mode {
color: #fff;
}
#dv_srch {
width: 80%;
margin-right: 10px;
}
#ifr_g {
position: fixed;
right: 50%;
top: 60px;
bottom: 0px;
width: 50%;
}
#ifr_n {
position: fixed;
right: 0px;
left: 50%;
top: 60px;
bottom: 0px;
width: 50%;
}

결과는 다음과 같습니다.

결과

프로젝트 파일 첨부합니다. 다운로드

분봉 수급 점수 계산하기

여기서 구하고자 하는 피쳐는 분봉 수급 점수입니다.

분봉 수급 점수: 1분동안 체결이 많이 이루어진 분봉들 중에서 매수세가 강한 분봉들의 비율로 [-1, 1]의 범위를 가짐

아래와 같은 1분봉차트가 있습니다. 분봉들이 아주 많아서 사람이 직접 점수를 계산하기는 어려울 수 있습니다.

분봉동시호가제외

분봉차트 CSV 파일

이를 파이썬으로 계산해 보겠습니다.

python3, numpy, pandas, matplotlib, mpl_finance 등을 필요로 합니다.

mpl_financehttps://github.com/matplotlib/mpl_finance 에서 받을 수 있습니다.

우선 필요한 라이브러리들 부터 임포트합니다.

import numpy as np
import pandas as pd
import matplotlib as mpl
import matplotlib.pyplot as plt
import mpl_finance
if __name__ == '__main__':
pass

이제 아래 코드로 main을 채웁니다.

df_chart = pd.read_csv('data/000660_m_in.csv') # 다운받은 데이터의 경로로 변경하십시오
# 고려할 분봉 수 결정
# df_chart = df_chart.iloc[-60*6*5:] # 약 5일치 분봉
# 동시호가 거래 없애기
df_chart['체결일'] = (df_chart['체결시간'] / 1000000).astype(int)
df_chart['체결시분'] = df_chart['체결시간'].mod(1000000).astype(int)
df_chart = df_chart[df_chart['체결시분'] < 153000]
# 거래량이 평균+표준편자*2 보다 많았던 분봉들만 남깁니다
df_chart = df_chart[df_chart['거래량'] > df_chart['거래량'].mean() + df_chart['거래량'].std() * 2]
# [수요 거래대금 + 공급 거래대금]을 구합니다
supdemand = ((df_chart['현재가'] - df_chart['시가']).abs() * df_chart['거래량']).sum()
# [수요 거래대금 - 공급 거래대금]을 구합니다
demand = ((df_chart['현재가'] - df_chart['시가']) * df_chart['거래량']).sum()
# [(수요 거래대금 - 공급 거래대금) / (수요 거래대금 + 공급 거래대금)]을 구합니다
score_demand = float(demand) / supdemand

여기서 score_demand분봉 수급 점수입니다. 이 점수가 1에 가까울수록 수요 거래대금이 컸다는 의미이고 -1에 가까울수록 공급 거래대금이 많았다는 의미입니다.

위의 1분봉 차트에 대해서 분봉 수급 점수는 0.1252 입니다. 양수이므로 수요 거래대금이 더 많았다는 뜻입니다.

물론 이 지표만으로 투자를 결정할 수는 없습니다. 앞으로 다양한 지표들을 개발하고 데이터를 기반으로 이유있는 투자를 하고자 합니다.

Matplotlib 라인차트

Python에서 초간단 라인차트를 그려보겠습니다. 누군가가 일주일 동안 BMI를 측정해보았다고 치고, 아래 코드는 x축을 날짜, y축을 BMI 값으로 가지는 차트를 그립니다.

import matplotlib.pyplot as plt
bmi = [24.6, 24.6, 24.9, 24.9, 24.9 ,24.9, 24.6]
plt.title("BMI Chart")
plt.ylabel("BMI")
plt.xticks(range(1, 8), ["Day 1", "Day 2", "Day 3", "Day 4", "Day 5", "Day 6", "Day 7"], fontsize=10)
plt.plot(range(1, 8), bmi, "o-", color="b", linewidth=1)
plt.ylim([23, 26])
plt.show()

title을 정하고, y축 레이블을 정하고, x축에 표시할 값들을 정해주고, 데이터를 뿌린다. 이 때, 값들을 점으로 표시하고 선으로 연결하도록 “o-” 옵션을 주고, 색깔을 blue로 하고, 선 굵기를 1로 했습니다. y축의 범위를 [23, 26]으로 정해서 선차트 위아래 어느정도 공간을 주고, show!

차트

Pandas DataFrame을 엑셀 파일로 내보내기

한글을 포함하는 Pandas DataFrame을 csv 파일로 변환할 상황이 생겼습니다.

import pandas as pd
df = pd.DataFrame()
# ...
df.to_csv('out.csv', encoding='utf-8')

이렇게 인코딩까지 지정하면 한글이 깨지지 않을 것이라 기대했는데… 깨집니다. 안깨지게 하려면 csv가 아니라 excel 파일로 변환하면 됩니다.

df.to_excel('out.xlsx', sheet_name='sheet1')

긴 URL이 있으면 워닝 (UserWarning: Ignoring URL)이 뜨는데, 아래와 같이 해결할 수 있습니다.

writer = pd.ExcelWriter('out.xlsx', engine='xlsxwriter', options={'strings_to_urls': False})
df.to_excel(writer)