娃在幼兒園吃了些啥小作?Python自動(dòng)爬取幼兒園伙食菜單亭姥,通過微信公眾號(hào)展示

前言

家里寶寶目前幼兒園小班,談話的時(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)容包括兩部分:

  1. 日期選擇器
    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>
  1. 菜單表格
    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)擊“每日菜單”:


訂閱號(hào)

點(diǎn)擊圖文消息:


圖文消息

原文鏈接疮胖,可橫向滾動(dòng):
點(diǎn)擊閱讀原文
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末环戈,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子澎灸,更是在濱河造成了極大的恐慌院塞,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件性昭,死亡現(xiàn)場(chǎng)離奇詭異拦止,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)糜颠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門汹族,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人其兴,你說我怎么就攤上這事顶瞒。” “怎么了元旬?”我有些...
    開封第一講書人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵榴徐,是天一觀的道長(zhǎng)守问。 經(jīng)常有香客問我,道長(zhǎng)箕速,這世上最難降的妖魔是什么酪碘? 我笑而不...
    開封第一講書人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮盐茎,結(jié)果婚禮上兴垦,老公的妹妹穿的比我還像新娘。我一直安慰自己字柠,他們只是感情好探越,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著窑业,像睡著了一般钦幔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上常柄,一...
    開封第一講書人閱讀 49,741評(píng)論 1 289
  • 那天鲤氢,我揣著相機(jī)與錄音,去河邊找鬼西潘。 笑死卷玉,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的喷市。 我是一名探鬼主播相种,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼品姓!你這毒婦竟也來了寝并?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤腹备,失蹤者是張志新(化名)和其女友劉穎衬潦,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體植酥,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡别渔,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了惧互。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡喇伯,死狀恐怖喊儡,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情稻据,我是刑警寧澤艾猜,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布买喧,位于F島的核電站,受9級(jí)特大地震影響匆赃,放射性物質(zhì)發(fā)生泄漏淤毛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一算柳、第九天 我趴在偏房一處隱蔽的房頂上張望低淡。 院中可真熱鬧,春花似錦瞬项、人聲如沸蔗蹋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽猪杭。三九已至,卻和暖如春妥衣,著一層夾襖步出監(jiān)牢的瞬間皂吮,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來泰國(guó)打工税手, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蜂筹,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓冈止,卻偏偏與公主長(zhǎng)得像狂票,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子熙暴,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • 全文鏈接 第一章 創(chuàng)建一個(gè)blog應(yīng)用第二章 使用高級(jí)特性來增強(qiáng)你的blog第三章 擴(kuò)展你的blog應(yīng)用第四章上 ...
    夜夜月閱讀 8,965評(píng)論 37 34
  • 我見過蒼翠欲滴周霉, 也見過萬紫千紅掂器; 見過春暖花開, 也見過秋水長(zhǎng)天俱箱; 見過朦朧煙雨国瓮, 也見過萬里晴空; 見過清風(fēng)見...
    十一小姐呀閱讀 709評(píng)論 0 2
  • 16-summer#12.28#打卡 真是年少不懂事啊,上學(xué)時(shí)總覺得老師叫我們背的課文枯燥難背跟衅。寫得都是什么啊孵睬,要...
    寫故事的阿夏閱讀 604評(píng)論 0 0
  • 我今天情緒屬于穩(wěn)定性低落的狀態(tài)。 可能是早晨倩倩抱了我一下伶跷,只是一小會(huì)掰读,就覺得有一種心酸要奪眶而出秘狞。今天沒有哭,到...
    毛絨毯子卷大穎閱讀 255評(píng)論 0 0
  • 當(dāng)我看著你遠(yuǎn)去的身影蹈集,當(dāng)手中的花掉落一地烁试,當(dāng)天上的云朵分離,當(dāng)大雁飛回時(shí)拢肆,當(dāng)情字溢滿心里减响,當(dāng)你送我的禮物被藏起,當(dāng)...
    穆炎閱讀 378評(píng)論 1 4