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

簡述

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

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


一、從業務邏輯中提煉API接口

先捋下整個業務流程吧前端

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

(PS:下述部分能夠直接跳過,不過我仍是建議看看概念性的東西)python

① 業務邏輯思惟導圖

抽象出業務邏輯,把相同的東西先放一塊兒,而後經過思惟導圖的形式表現出來。mysql

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

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

③ 基本功能模塊關係

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

  • 我是人,短信是事物,我發短信是事件
  • 你是人,短信是事物,你收短信是事件

若是商家能主動發起請求,那就是人,即一個東西具有主動性,它就是人。 (這裏的人之間沒有啥關聯,因此沒有線~)數據庫

④ 功能模塊接口UML(設計API)

只考慮功能模塊,設計接口去解決問題,注意耦合把控,過高不能拆分,過低失去化模塊意義。編程

目前所需的API就上面這些,後面按需擴展便可。json


二、手撕API接口——前

① 項目結構

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

結構簡述:

  • 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":[]
}
複製代碼

三、手撕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': []})
複製代碼

四、手撕API接口——後

行吧,API接口編寫完畢,接着用PostMan模擬下請求:

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

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


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

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

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


參考文獻

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

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

相關文章
相關標籤/搜索