簡述
在上一節「偷個懶,公號摳腚早報80%自動化——3.Flask速成大法」中,快速地把 Flask的基本語法擼了一遍,本節直接開衝,用Flask來寫下摳腚男孩的後臺。
PS:筆者沒有真正參加過前後端開發,此文都是現學現賣,你可以理解成小白文章, 有錯的地方,還望海涵,批評或建議歡迎在評論區留言,謝謝~
1、從業務邏輯中提煉API介面
先捋下整個業務流程吧
- 1.早上定時8點執行爬蟲指令碼爬取新聞(刪表建新表)。
- 2.查詢當日爬取到的新聞,把覺得有意思的新聞新增到篩選池中。
- 3.對篩選池中的新聞進行二次篩選,在這一步可以新增或者修改篩選池新聞。
- 4.取篩選池中的前15條早報,附上日期插入日報池中。
- 5.傳入日期,通過模板生成微信群文字版。
- 6.傳入日期,通過模板生成微信公眾號版。
- 7.傳入日期,通過模板生成新聞詳情列表頁。
(PS:下述部分可以直接跳過,不過我還是建議看看概念性的東西)
① 業務邏輯思維導圖
抽象出業務邏輯,把相同的東西先放一起,然後通過思維導圖的形式表現出來。
② 功能——業務邏輯思維導圖
「業務邏輯」和「功能模組」呈現的內容結合,一個model對應多個業務邏輯。 模組的劃分依據:功能與業務的關係,功能和功能間不能有關係,功能儘可能實現一對多。 筆者的專案過於簡單,圖跟業務邏輯思維導圖差不了多少,直接略過。
③ 基本功能模組關係
找出功能——業務邏輯思維導圖中的對應關係,功能模組按照人和事來劃分。
事不能理解為使用者的行為,「事」就是單純的事,不是使用者行為,「人」就是使用者,
「事」就是指事物,「事件」是人和事之間的關係。不能主動發出請求的都歸屬於事
。
你去星巴克喝咖啡 = 事件 = 人和事之間的關係。事是事物,不是事件,我給你發簡訊,
你接收簡訊,這是兩個事件。
- 我是人,簡訊是事物,我發簡訊是事件
- 你是人,簡訊是事物,你收簡訊是事件
如果商家能主動發起請求,那就是人,即一個東西具備主動性,它就是人。 (這裡的人之間沒有啥關聯,所以沒有線~)
④ 功能模組介面UML(設計API)
只考慮功能模組,設計介面去解決問題,注意耦合把控,太高不能拆分,太低失去化模組意義。
目前所需的API就上面這些,後面按需擴充套件即可。
2、手撕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":[]
}
複製程式碼
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%自動化——3.Flask速成大法》 已經講解過了,把程式碼傳伺服器上,安裝配置nginx和uwsgi,配置完後,即可通過伺服器公網ip進行訪問。 當然你可以坐下域名解析,指向伺服器,直接通過域名訪問。(貌似個人域名備案變把以前嚴格了,前不久在騰訊雲 備案一個域名,寫的CoderPig的程式設計技術小站,客服說不能出現程式設計字眼,還有什麼商業性的都不行~)
行吧,關於摳腚男孩的簡陋後臺,基本雛形就完成了,有些粗糙,又不是不能用。
( 順帶以此圖,緬懷沒有下個系統版本更新的堅果Pro 2S)後續根據需求,以及自己掌握更多 新的姿勢後再來一點點優化把。
下一節就是本系列的最後一節的了,手撕一個APP來調這些介面,敬請期待~
參考文獻:
- 《App後臺開發運維和架構實踐》曾健生 編著——中的2.1 從App業務邏輯中提煉API介面。
Tips:公號目前只是堅持發早報,在慢慢完善,有點心虛,只敢貼個小圖,想看早報的可以關注下~