偷個懶,公號摳腚早報80%自動化——4.用Flask搭個簡易(陋)後臺

coder-pig發表於2019-02-26

簡述

在上一節「偷個懶,公號摳腚早報80%自動化——3.Flask速成大法」中,快速地把 Flask的基本語法擼了一遍,本節直接開衝,用Flask來寫下摳腚男孩的後臺。

偷個懶,公號摳腚早報80%自動化——4.用Flask搭個簡易(陋)後臺

PS:筆者沒有真正參加過前後端開發,此文都是現學現賣,你可以理解成小白文章, 有錯的地方,還望海涵,批評或建議歡迎在評論區留言,謝謝~

偷個懶,公號摳腚早報80%自動化——4.用Flask搭個簡易(陋)後臺


1、從業務邏輯中提煉API介面

先捋下整個業務流程吧

  • 1.早上定時8點執行爬蟲指令碼爬取新聞(刪表建新表)。
  • 2.查詢當日爬取到的新聞,把覺得有意思的新聞新增到篩選池中。
  • 3.對篩選池中的新聞進行二次篩選,在這一步可以新增或者修改篩選池新聞。
  • 4.取篩選池中的前15條早報,附上日期插入日報池中。
  • 5.傳入日期,通過模板生成微信群文字版。
  • 6.傳入日期,通過模板生成微信公眾號版。
  • 7.傳入日期,通過模板生成新聞詳情列表頁。

偷個懶,公號摳腚早報80%自動化——4.用Flask搭個簡易(陋)後臺

(PS:下述部分可以直接跳過,不過我還是建議看看概念性的東西)

① 業務邏輯思維導圖

抽象出業務邏輯,把相同的東西先放一起,然後通過思維導圖的形式表現出來。

偷個懶,公號摳腚早報80%自動化——4.用Flask搭個簡易(陋)後臺

② 功能——業務邏輯思維導圖

「業務邏輯」和「功能模組」呈現的內容結合,一個model對應多個業務邏輯。 模組的劃分依據:功能與業務的關係功能和功能間不能有關係功能儘可能實現一對多。 筆者的專案過於簡單,圖跟業務邏輯思維導圖差不了多少,直接略過。

③ 基本功能模組關係

找出功能——業務邏輯思維導圖中的對應關係,功能模組按照人和事來劃分。 事不能理解為使用者的行為,「」就是單純的事,不是使用者行為,「」就是使用者, 「」就是指事物,「事件」是人和事之間的關係。不能主動發出請求的都歸屬於事。 你去星巴克喝咖啡 = 事件 = 人和事之間的關係。事是事物,不是事件,我給你發簡訊, 你接收簡訊,這是兩個事件。

  • 我是人,簡訊是事物,我發簡訊是事件
  • 你是人,簡訊是事物,你收簡訊是事件

如果商家能主動發起請求,那就是人,即一個東西具備主動性,它就是人。 (這裡的人之間沒有啥關聯,所以沒有線~)

偷個懶,公號摳腚早報80%自動化——4.用Flask搭個簡易(陋)後臺

④ 功能模組介面UML(設計API)

只考慮功能模組,設計介面去解決問題,注意耦合把控,太高不能拆分,太低失去化模組意義。

偷個懶,公號摳腚早報80%自動化——4.用Flask搭個簡易(陋)後臺

目前所需的API就上面這些,後面按需擴充套件即可。


2、手撕API介面——前

① 專案結構

先是專案的結構,直接使用上一節說的簡單通用的結構:

偷個懶,公號摳腚早報80%自動化——4.用Flask搭個簡易(陋)後臺

結構簡述:

  • app:整個專案的包目錄。
  • models:資料模型。
  • static:靜態檔案,css,JavaScript,圖示等。
  • templates:模板檔案。
  • views:檢視檔案。
  • config.py:配置檔案。
  • venv:虛擬環境。
  • manage.py:專案啟動控制檔案。
  • requirements.txt:專案啟動控制檔案。

② 定義資料模型

定義三種型別的資料:源新聞,篩選新聞、早報、欄位大同小異,程式碼如下:

# models\news.py

from app import db

__all__ = ['OriginNews', 'ChooseNews', 'MorningNews']


class OriginNews(db.Model):
    __tablename__ = 'news_origin'
    __table_args__ = {"useexisting": True}
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    title = db.Column(db.Text)
    url = db.Column(db.Text)
    create_time = db.Column(db.Text)

    def to_dict(self):
        return {"id": self.id, "title": self.title, "url": self.url, "create_time": self.create_time}


class ChooseNews(db.Model):
    __tablename__ = 'news_choose'
    __table_args__ = {"useexisting": True}
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    title = db.Column(db.Text)
    url = db.Column(db.Text)
    create_time = db.Column(db.Text)

    def to_dict(self):
        return {"id": self.id, "title": self.title, "url": self.url, "create_time": self.create_time}


class MorningNews(db.Model):
    __tablename__ = 'news_morning'
    __table_args__ = {"useexisting": True}
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    title = db.Column(db.Text)
    url = db.Column(db.Text)
    create_time = db.Column(db.Text)
    add_time = db.Column(db.Text)

    def to_dict(self):
        return {"id": self.id, "title": self.title, "url": self.url, "create_time": self.create_time,
                "add_time": self.add_time}
複製程式碼

③ 編輯config.py檔案

新增sqlalchemy相關的配置,如下:

SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:Jay12345@127.0.0.1:3306/news'
SQLALCHEMY_TRACK_MODIFICATIONS = True
複製程式碼

④ app目錄下建立__init__.py檔案

在這裡完成Flask,SQLAlchemy物件的例項化,以及相關資料庫的建立:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config.from_object('config')
db = SQLAlchemy(app)


from app.models.news import *
db.create_all()
複製程式碼

⑤ 建立檢視

從業務邏輯思維導圖那裡就知道,不只是做早報,還有表情包,沙雕圖等,為了 便於後面方便擴充套件,利用藍圖來分離模組。直接在views目錄下建立一個news.py。

# 建立藍圖
ns = Blueprint('news', __name__)

# flask例項註冊藍圖
app.register_blueprint(ns, url_prefix='/news')
複製程式碼

檢視檔案建立,現在大部分的介面返回的資料都是Json字串,如果每次返回資料都要 我們自行去拼接字串,顯得過於繁瑣,可以包裝下jsonify,把字典型別的資料直接 轉換成Json字串返回。程式碼如下:

class JsonResponse(Response):
    @classmethod
    def force_type(cls, response, environ=None):
        if isinstance(response, dict):
            response = jsonify(response)
        return super(JsonResponse, cls).force_type(response, environ)

app.response_class = JsonResponse
複製程式碼

約定下返回的Json資料格式:

{
    "code":"200",
    "msg":"請求成功",
    "data":[]
}
複製程式碼

3、手撕API介面——中

準備工作做的差不多了,接著開始著手編寫API介面,分幾類進行編寫,先是和資料庫增刪改查有關的:

① 資料庫增刪改查相關的介面

# 查詢新聞,判斷是否傳入nid來判斷是單條查詢還是多條,
# kind為表序號:1來源表,2篩選表,3早報表
@ns.route("/show", methods=['GET'])
def news_show():
    req_args = request.args
    if 'kind' in req_args:
        kind = int(req_args['kind'])
        # 如果有nid引數,說明是查詢單條,否則是查詢全部
        if 'nid' in req_args:
            nid = request.args['nid']
            if kind == 1:
                news = OriginNews.query.filter_by(id=nid).first()
            elif kind == 2:
                news = ChooseNews.query.filter_by(id=nid).first()
            elif kind == 3:
                news = ChooseNews.query.filter_by(id=nid).first()
            else:
                return make_response({'code': '200', 'msg': '無效的kind引數', 'data': []})
            return make_response({'code': '200', 'msg': '請求成功', 'data': news.to_dict()})
        else:
            if 'count' in req_args and 'page' in req_args:
                count = int(req_args['count'])
                page = int(req_args['page'])
                news_list = []
                if kind == 1:
                    for n in OriginNews.query.filter().offset(page * count).limit(count):
                        news_list.append(n.to_dict())
                elif kind == 2:
                    for n in ChooseNews.query.filter().offset(page * count).limit(count):
                        news_list.append(n.to_dict())
                elif kind == 3:
                    for n in MorningNews.query.filter().offset(page * count).limit(count):
                        news_list.append(n.to_dict())
                else:
                    return make_response({'code': '200', 'msg': '無效的kind引數', 'data': []})
                return make_response({'code': '200', 'msg': '請求成功', 'data': news_list})
            else:
                return make_response({'code': '200', 'msg': '缺少count或page引數', 'data': []})
    else:
        return make_response({'code': '200', 'msg': '缺少kind引數', 'data': []}


# 查詢新聞條數
# kind為表序號:1來源表,2篩選表,3早報表
@ns.route("/<int:kind>/count", methods=['GET'])
def news_count(kind):
    if kind == 1:
        count = OriginNews.query.filter().count()
    elif kind == 2:
        count = ChooseNews.query.filter().count()
    elif kind == 3:
        count = MorningNews.query.filter().count()
    else:
        return make_response({'code': '201', 'msg': '錯誤的引數型別', 'data': []})
    resp = make_response({'code': '200', 'msg': '請求成功', 'data': {'count': count}})
    return resp


# 刪除某條新聞,傳入引數nid代表新聞id
# kind為表序號:1來源表,2篩選表,3早報表
@ns.route("/destroy", methods=['DELETE'])
def news_delete():
    req_args = request.form
    if 'kind' in req_args:
        kind = int(req_args['kind'])
        if 'nid' in req_args:
            nid = int(req_args['nid'])
            if kind == 1:
                news = OriginNews.query.filter_by(id=nid).first()
                db.session.delete(news)
                db.session.commit()
            elif kind == 2:
                db.session.delete(ChooseNews.query.filter_by(id=nid).first())
                db.session.commit()
            elif kind == 3:
                db.session.delete(MorningNews.query.filter_by(id=nid).first())
                db.session.commit()
            else:
                return make_response({'code': '200', 'msg': '無效的kind引數', 'data': []})
            return make_response({'code': '200', 'msg': '請求成功', 'data': []})
        else:
            return make_response({'code': '200', 'msg': '缺少mid 引數', 'data': []})
    else:
        return make_response({'code': '200', 'msg': '缺少kind引數', 'data': []})
    
# 更新篩選池裡的新聞(有的更新,沒的插入)
@ns.route("/update", methods=['POST'])
def add_news():
    req_args = request.form
    if 'news' in req_args:
        news_dict = json.loads(req_args['news'])
        news = ChooseNews.query.filter_by(id=news_dict['nid']).first()
        # 沒有資料是插入,有資料是修改
        if news is None:
            news = ChooseNews()
            news.id = news_dict['nid']
            news.title = news_dict['title']
            news.url = news_dict['url']
            news.create_time = news_dict['create_time']
            db.session.add(news)
            db.session.commit()
            return make_response({'code': '200', 'msg': '插入成功', 'data': []})
        else:
            news.id = news_dict['nid']
            news.title = news_dict['title']
            news.url = news_dict['url']
            news.create_time = news_dict['create_time']
            db.session.commit()
            return make_response({'code': '200', 'msg': '更新成功', 'data': []})
    else:
        return make_response({'code': '200', 'msg': '缺少news引數', 'data': []})
        
# 把篩選池的新聞插入到日報池中(限制15條)
@ns.route("/insert_morning", methods=['POST'])
def add_morning_news():
    for n in ChooseNews.query.filter().limit(15):
        n_dict = n.to_dict()
        morning_news = MorningNews()
        morning_news.id = n_dict.get('id')
        morning_news.title = n_dict.get('title')
        morning_news.url = n_dict.get('url')
        morning_news.create_time = n_dict.get('create_time')
        morning_news.add_time = time.strftime("%Y%m%d")
        db.session.add(morning_news)
    db.session.commit()
    return make_response({'code': '200', 'msg': '請求成功', 'data': []})
複製程式碼

② 啟動爬蟲的介面

需要通過命令列來啟動爬蟲,爬蟲的執行比較耗時,而Flask的服務預設是同步的。 只有爬蟲執行完畢才會響應客戶端,顯然是非常不合理的。這裡用執行緒池來實現 最簡單的非同步操作,請求後直接響應,後臺去執行爬蟲。

executor = ThreadPoolExecutor(max_workers=2)

# 執行新聞爬蟲
def spider():
    os.system("python PenpaiSpider.py")
    os.system("python WeiboSpider.py")
    print("爬蟲執行完畢...")

# 執行爬取新聞的爬蟲
@ns.route("/spider", methods=['GET'])
def run_spider():
    os.system("python DBHelper.py")
    executor.submit(spider)
    return make_response({'code': '200', 'msg': '請求成功', 'data': []})
複製程式碼

③ 生成微信轉發文字的介面

就是簡單的字串拼接:

# 生成複製模板文字
@ns.route("/show_copy_model", methods=['GET'])
def show_copy_model():
    req_args = request.args
    if 'date' in req_args:
        date = req_args['date']
        text_model = "『摳腚早報速讀』| 第%s期\n\n要聞速讀\n\n" % date[2:]
        news = MorningNews.query.filter_by(add_time=date).all()
        for i in range(len(news)):
            text_model += str(i + 1)
            text_model += "、%s。\n\n" % news[i].title
        return make_response({'code': '200', 'msg': '請求成功', 'data': text_model[:-1]})
    else:
        return make_response({'code': '200', 'msg': '缺少date引數', 'data': []})
複製程式碼

④ 公眾號文章編輯複製樣式的介面

就是微信公號編寫文章時的內容,利用flask內建的jinja2模板來動態生成。這裡有一點要注意: render_template()函式雖然返回的是html,但是請求介面後瀏覽器顯示的是HTML程式碼而非HTML 頁面,而且還有亂碼。這裡需要在響應頭中把「content-type」設定為「text/html; charset=utf-8」。 但是問題來了,筆者對於前端一竅不通(從我寫的新聞列表頁就知道了...)一個最簡單的做法就是開啟 瀏覽器的開發者工具,複製下網頁原始碼,調整下程式碼以及排版,找出每天新聞對應的程式碼,利用 迴圈來構造,抽取後的部分html程式碼如下:

{% for i in news_list %}
<p style="max-width: 100%; min-height: 1em;"><br></p>
<p style="max-width: 100%; min-height: 1em;">{{loop.index}}、{{i.title}}。</p>
{% endfor %}
複製程式碼

接著在檢視函式中為模板傳入新聞資訊,動態生成頁面:

# 生成微信複製模板
@ns.route("/create_wc_model", methods=['GET'])
def show_wc_model():
    req_args = request.args
    if 'date' in req_args:
        date = req_args['date']
        news = MorningNews.query.filter_by(add_time=date).all()
        resp = make_response(render_template('news.html', news_list=news))
        resp.headers['content-type'] = 'text/html; charset=utf-8'
        return resp
    else:
        return make_response({'code': '200', 'msg': '缺少date引數', 'data': []})
複製程式碼

⑤ 新聞列表詳情頁的介面

和上面那個一樣玩法,定義模板news_list.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>『摳腚早報速讀』| 第{{news_list[0].add_time[2:]}}期</title>
</head>
<body>
{% for news in news_list %}
<div style="height: 48px"><a href="{{news.url}}" target="_blank" style="color:black;text-decoration:none;">{{loop.index}}、{{news.title}}</a></div>
{% endfor %}
</div>
</body>
</html>
複製程式碼

同樣在意圖中傳入新聞資訊:

# 生成新聞列表頁
@ns.route("/create_news_list", methods=['GET'])
def show_news_list():
    req_args = request.args
    if 'date' in req_args:
        date = req_args['date']
        news = MorningNews.query.filter_by(add_time=date).all()
        resp = make_response(render_template('news_list.html', news_list=news))
        resp.headers['content-type'] = 'text/html; charset=utf-8'
        return resp
    else:
        return make_response({'code': '200', 'msg': '缺少date引數', 'data': []})
複製程式碼

⑥ 異常處理

對應常見的404和500錯誤,直接返回不好,這裡簡單的處理下。

@app.errorhandler(404)
def error_404(e):
    return make_response({'code': '404', 'msg': '404錯誤', 'data': []})


@app.errorhandler(500)
def error_404(e):
    return make_response({'code': '500', 'msg': '500錯誤', 'data': []})
複製程式碼

4、手撕API介面——後

行吧,API介面編寫完畢,接著用PostMan模擬下請求:

偷個懶,公號摳腚早報80%自動化——4.用Flask搭個簡易(陋)後臺

增刪改查的結果就不演示了,只展示早報復制文字,公號編輯,以及新聞詳情列表頁介面的請求結果,依次如下:

偷個懶,公號摳腚早報80%自動化——4.用Flask搭個簡易(陋)後臺

偷個懶,公號摳腚早報80%自動化——4.用Flask搭個簡易(陋)後臺

偷個懶,公號摳腚早報80%自動化——4.用Flask搭個簡易(陋)後臺

行吧,接著把專案部署到伺服器上,怎麼部署在上一節《偷個懶,公號摳腚早報80%自動化——3.Flask速成大法》 已經講解過了,把程式碼傳伺服器上,安裝配置nginx和uwsgi,配置完後,即可通過伺服器公網ip進行訪問。 當然你可以坐下域名解析,指向伺服器,直接通過域名訪問。(貌似個人域名備案變把以前嚴格了,前不久在騰訊雲 備案一個域名,寫的CoderPig的程式設計技術小站,客服說不能出現程式設計字眼,還有什麼商業性的都不行~)


行吧,關於摳腚男孩的簡陋後臺,基本雛形就完成了,有些粗糙,又不是不能用。

偷個懶,公號摳腚早報80%自動化——4.用Flask搭個簡易(陋)後臺

( 順帶以此圖,緬懷沒有下個系統版本更新的堅果Pro 2S)後續根據需求,以及自己掌握更多 新的姿勢後再來一點點優化把。

偷個懶,公號摳腚早報80%自動化——4.用Flask搭個簡易(陋)後臺

下一節就是本系列的最後一節的了,手撕一個APP來調這些介面,敬請期待~


參考文獻

  • 《App後臺開發運維和架構實踐》曾健生 編著——中的2.1 從App業務邏輯中提煉API介面。

Tips:公號目前只是堅持發早報,在慢慢完善,有點心虛,只敢貼個小圖,想看早報的可以關注下~

偷個懶,公號摳腚早報80%自動化——4.用Flask搭個簡易(陋)後臺


相關文章