前言
家里寶寶目前幼兒園小班,談話的時(shí)候經(jīng)常問她的也就是今天玩了什么顾稀、學(xué)了什么达罗、吃了什么等。這其中最容易檢驗(yàn)答案的就是“吃了什么”静秆,因?yàn)橛杏變簣@官方主頁的每日伙食菜單可以對(duì)照粮揉。其他的只能在家長(zhǎng)群里交流了解。
但幼兒園官網(wǎng)的菜單有個(gè)明顯缺點(diǎn)——只能查看當(dāng)前一個(gè)星期的抚笔,沒法查看歷史數(shù)據(jù)扶认。正好現(xiàn)在接觸Python比較多,于是很自然地有了爬取菜單保存到數(shù)據(jù)庫的想法殊橙。那既然保存了吧辐宾,就再搭個(gè)web服務(wù)展示出來吧;既然要弄網(wǎng)頁吧膨蛮,那干脆放到公眾號(hào)里吧叠纹。于是有了這個(gè)“幼兒園伙食菜單”迷你項(xiàng)目。
主要呢自己也能通過“幼兒園伙食菜單”項(xiàng)目接觸下bootstrap和微信公眾號(hào)平臺(tái)敞葛,復(fù)習(xí)下Django誉察。github地址:https://github.com/zhangjizhong-86/kinder_foods
效果預(yù)覽
最終成品是個(gè)看上去很簡(jiǎn)潔的一個(gè)網(wǎng)頁,默認(rèn)高亮顯示當(dāng)天菜單惹谐,也可以通過日期選擇器選擇日期持偏,顯示對(duì)應(yīng)日期的幼兒園伙食。
環(huán)境
- Debian 9 x64
- python 3.7.0
- mysql Ver 15.1 Distrib 10.1.37-MariaDB, for debian-linux-gnu (x86_64) using readline 5.2
- Django 2.0.7
項(xiàng)目目錄結(jié)構(gòu)
主要目錄和文件豺鼻,不重要的或沒用上的就不列出了综液。
kinder_foods
│ bosva_menu.log # 日志
│ bosva_menu.py # 采集腳本
│ manage.py # django腳本
│
├─menu # django
│ │ views.py # 視圖文件
│ │
│ ├─static
│ │ ├─css
│ │ │ bootstrap-datetimepicker.css等
│ │ ├─fonts
│ │ │ glyphicons-halflings-regular.eot等
│ │ └─js
│ │ │ bootstrap-datetimepicker.js等
│ │ └─locales
│ │ bootstrap-datetimepicker.zh-CN.js等
│ │
│ ├─templates
│ │ menu.html # 網(wǎng)頁模板
│
└─menu_server # django項(xiàng)目
│ settings.py
│ urls.py
數(shù)據(jù)采集
首先需將菜單從網(wǎng)頁上爬下來,bosva_menu.py完成這個(gè)任務(wù)儒飒,其中的函數(shù)說明:
- load_html: 爬取菜單谬莹,組織數(shù)據(jù)結(jié)構(gòu)
利用requesets和BeautifulSoup,沒什么好多說的,網(wǎng)上關(guān)于爬蟲的內(nèi)容很多附帽,這里也是很基礎(chǔ)的內(nèi)容:
def load_html(url):
r = requests.get(url)
html = r.content
soup = BeautifulSoup(html, "html.parser")
...(略)
- db_connect: 連接本地?cái)?shù)據(jù)庫
我比較習(xí)慣把數(shù)據(jù)庫連接過程單獨(dú)封裝在一個(gè)函數(shù)里埠戳,通過函數(shù)返回連接對(duì)象。
def db_connect(host='localhost', user='root', password='pass', db='test', charset='utf8'):
return pymysql.connect(host=host,
user=user,
password=password,
db=db,
charset=charset,
cursorclass=pymysql.cursors.DictCursor)
- to_database: 寫入數(shù)據(jù)庫
寫入時(shí)忽略已存在的數(shù)據(jù)(insert ignore ...)蕉扮,返回寫入的條數(shù)整胃。
def to_database(row, conn):
with conn.cursor() as cur:
insert_sql = '''insert ignore into kinder_foods values (%s,%s,%s,%s,%s)'''
rows_affected = cur.executemany(insert_sql, row)
conn.commit()
return rows_affected
- log: 記錄日志
簡(jiǎn)單的日志記錄函數(shù),當(dāng)然還是推薦使用Logger喳钟,這里偷懶了屁使。
def log(log, file):
with open(file, 'a', encoding='utf8') as f:
print('[' + datetime.now().strftime('%Y-%m-%d %H:%M:%S') + ']' + log, file=f)
代碼均只展示主要部分,省略異常處理等細(xì)節(jié)
定時(shí)執(zhí)行采集任務(wù)
網(wǎng)頁上的菜單每周更新奔则,但不確定確切時(shí)間蛮寂,干脆每天運(yùn)行bosva_menu.py吧(所以插入數(shù)據(jù)的時(shí)候使用insert ignore)
crontab -e
58 6 * * * cd /root/bosva_menu && /root/.pyenv/shims/python bosva_menu.py
每天的UTC6:58(北京時(shí)間14:58)執(zhí)行爬取腳本
Django項(xiàng)目
在當(dāng)前路徑下新建項(xiàng)目
django-admin.py startproject menu_server .
允許IP訪問
settings.py中將服務(wù)器的IP和域名加入ALLOWED_HOSTS
ALLOWED_HOSTS = ['XX.XX.XX.XX','domain-name.com']
模板(templates)
模板文件menu.html,使用bootstrap框架和bootstrap的日期選擇器易茬。
選擇日期后通過ajax更新菜單表格<tbody>酬蹋。
<body>里的內(nèi)容包括兩部分:
- 日期選擇器
bootstrap datetimepicker提供了一些樣例,稍加修改即可抽莱。
<div class="container">
<form action="" class="form-horizontal" role="form">
<fieldset>
<div class="form-group">
<label for="dtp_input2" class="col-md-2 control-label">選擇日期</label>
<div class="input-group date form_date col-md-5" data-date="" data-date-format="yyyy-mm-dd" data-link-field="dtp_input2" data-link-format="yyyy-mm-dd">
<input class="form-control" size="16" type="text" value="" readonly>
<span class="input-group-addon"><span class="glyphicon glyphicon-remove"></span></span>
<span class="input-group-addon"><span class="glyphicon glyphicon-calendar"></span></span>
</div>
<input type="hidden" id="dtp_input2" value="" /><br/>
</div>
</fieldset>
</form>
</div>
- 菜單表格
class=table-responsive使表格具備自寬度適應(yīng)功能范抓。
<tbody>標(biāo)簽下留白,留給ajax更新食铐。
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<th class="text-center">日期</th>
<th class="text-center">早點(diǎn)</th>
<th class="text-center">午餐</th>
<th class="text-center">午點(diǎn)</th>
<th class="text-center">體弱兒營(yíng)養(yǎng)菜</th>
</tr>
</thead>
<tbody id='menu'>
</tbody>
</table>
</div>
引用部分
引用bootstrap 3.3.7匕垫、bootstrap datetimepicker、jquery
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-datetimepicker.min.css" media="screen">
<script type="text/javascript" src="/static/js/jquery-3.3.1.min.js"></script>
<script type="text/javascript" src="/static/js/bootstrap.min.js"></script>
<script type="text/javascript" src="/static/js/bootstrap-datetimepicker.js" charset="UTF-8"></script>
<script type="text/javascript" src="/static/js/locales/bootstrap-datetimepicker.zh-CN.js" charset="UTF-8"></script>
以及適配移動(dòng)終端
加入viewport適配移動(dòng)終端虐呻,否則在高分屏上字體非常小年缎。更多viewport參考:https://www.cnblogs.com/2050/p/3877280.html
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" charset="utf-8" >
模板主要js腳本
- dom ready后執(zhí)行do_ajax,初始化日期選擇插件铃慷,定義日期改變事件觸發(fā)ajax,向后臺(tái)POST參數(shù)new_query_date
<script type="text/javascript">
$(document).ready(function(){
do_ajax(today());
$('.form_date').datetimepicker({
// 初始化日期選擇器
language: 'zh-CN',
weekStart: 1,
todayBtn: 1,
autoclose: 1,
todayHighlight: 1,
startView: 2,
minView: 2,
forceParse: 0
});
$('.form_date').datetimepicker().on('changeDate', function(e) {
// 選擇日期發(fā)生變化
let new_query_date = $('.form-control').val();
// 通過AJAX發(fā)送新的請(qǐng)求蜕该,參數(shù)為選擇的日期
do_ajax(new_query_date);
});
});
</script>
- do_ajax負(fù)責(zé)發(fā)起查詢犁柜,攜帶查詢?nèi)掌冢晒Λ@得返回值后更新menu的html
function do_ajax(query_date)
{
// AJAX POST堂淡,攜帶查詢?nèi)掌诓雒澹鶕?jù)返回JSON更新菜單tbody
$.post("/ajax/",
{'query_date': query_date, 'csrfmiddlewaretoken': '{{ csrf_token }}'},
function(data, status){
for (let query_date_return in data) {
menu = data[query_date_return];
if (status == 'success' & menu != null) {
let html;
for (let date in menu) {
if (query_date_return == date) { html += '<tr class="success"><th class="text-center">' + date + '<br>' + getWeekday(date) + '</th>'; } // 該日期與查詢?nèi)掌谝恢拢吡溜@示
else { html += '<tr><th class="text-center">' + date + '<br>' + getWeekday(date) + '</th>'; }
let meals = menu[date];
for (let meal in meals) {
let dishes = meals[meal];
html += '<td class="text-center">' + dishes + '</td>'; //菜品
}
html += '</tr>'
}
$("#menu").html(html);
} else { alert('這個(gè)日期沒有數(shù)據(jù)哦绢淀!'); } //查詢的日期沒有數(shù)據(jù)
}
});
}
視圖(views)
最主要的是ajax函數(shù)(ajax請(qǐng)求會(huì)被路由到這個(gè)函數(shù))萤悴,當(dāng)前端ajax請(qǐng)求傳遞查詢?nèi)掌诤螅瑥臄?shù)據(jù)庫查詢結(jié)果皆的,并組織成一個(gè)數(shù)據(jù)結(jié)構(gòu)覆履,以json形式返回。
def ajax(request):
if request.method == 'POST':
# 獲取頁面請(qǐng)求的日期,如果沒有硝全,則默認(rèn)為當(dāng)前日期(UTC+8)
# 頁面時(shí)區(qū)以瀏覽器自動(dòng)調(diào)整
query_date = request.POST.get('query_date', datetime.now(timezone(timedelta(hours=8))).strftime('%Y-%m-%d'))
query_date_menu = db_query(query_date)
for k, v in query_date_menu.items():
date_in_subquery = k
menu = v
if menu:
menu_sort = sort(menu)
query_date_menu[date_in_subquery] = menu_sort
data = json.dumps(query_date_menu) # 如果沒有查詢到相應(yīng)日期的菜單, menu==None -> data==null
return HttpResponse(data, content_type='application/json')
服務(wù)器后臺(tái)運(yùn)行
nohup python manage.py runserver 0.0.0.0:80 &
域名申請(qǐng)
網(wǎng)上搜索了一番栖雾,最終用優(yōu)惠在namesilo花$5.99申請(qǐng)了一個(gè)還算看得上眼的.com域名,配置域名解析到服務(wù)器IP地址伟众。
如果有需要域名的析藕,歡迎使用本人的優(yōu)惠注冊(cè)鏈接或使用優(yōu)惠碼“WORTHAMILLION”。
微信公眾號(hào)
個(gè)人用戶可以開通公眾號(hào)中的訂閱號(hào)凳厢,在管理-素材管理中新建圖文消息账胧,在原文鏈接中粘貼網(wǎng)頁地址,這樣在點(diǎn)擊閱讀原文后就能實(shí)現(xiàn)跳轉(zhuǎn)先紫。
如果網(wǎng)站沒有備案治泥,或端口不是標(biāo)準(zhǔn)端口(80),微信內(nèi)置瀏覽器會(huì)對(duì)頁面進(jìn)行轉(zhuǎn)換泡孩,頁面上的js和css會(huì)不正常车摄,所以建議申請(qǐng).com域名+部署在80端口。
設(shè)置添加公眾號(hào)仑鸥、按需回復(fù)和點(diǎn)擊菜單吮播,均回復(fù)新建的圖文消息。這部分都是網(wǎng)頁操作眼俊,就不詳述了意狠。
訂閱號(hào)效果
進(jìn)入訂閱號(hào),回復(fù)“菜單”或點(diǎn)擊“每日菜單”:
點(diǎn)擊圖文消息:
原文鏈接疮胖,可橫向滾動(dòng):