搭建一个弹幕新闻网站
本項目僅供學習使用, 請勿用來進行商業用途
本期知識點:
前言
你是否在 刷B站 或 刷知乎 時覺得不夠暢? 是否想在一個平臺上同時獲取多個平臺的有趣內容?
這個網站將為你打開一扇快速通道
先來看效果
制作網站的緣由是我在刷新聞時的突發奇想, 純屬個人愛好, 項目源碼: https://github.com/zhanghao19/LetMeSee
網站的核心框架選擇的是Flask, 優勢是便捷, 且官方文檔中有詳細的入門教程: 快速上手flask
文章的描述順序也是筆者搭建的流程
1>前端
彈幕新聞的重點在于展示, 其原理簡單來說就像"往杯子里倒水"一樣
1.1>網站框架
這里網站的框架指的是彈幕所顯示在的地方, 我使用的是之前在學習Django的時候搭建的一個框架
以原網站作為基礎框架, 使用jinja的繼承功能來使我們的主要內容融入基礎框架
你可以使用任意一款你喜歡的網站模板, 來作為放置彈幕的容器, 參考網站: Bootstrap
下載好你的模板, 參考以下代碼中的block位置, 對自己的模板進行靜態抽取:
<!-- Web/App/templates/base.html --> <!DOCTYPE html> <html lang="zh-cn"> <head><meta charset="utf-8"><title id="title">{% block title %}{% endblock %}</title><link rel="stylesheet" href="../static/css/reset.css"><link rel="stylesheet" href="../static/css/base.css"><!-- 上面的是模板自帶的靜態文件, 下面是為項目需要準備的 -->{% block link %}{% endblock %} </head> <body> <!-- header start --> <header id="header"><div class="mw1200 header-contain clearfix"><!-- logo start --><h1 class="logo"><a href="javascript:void(0);" class="logo-title">Python</a></h1><!-- logo end --><!-- nav start --><nav class="nav"><ul class="menu"><!-- 這里是導航條上的一些選項-->{% block nav %}{% endblock %}</ul></nav><!-- nav end --></div> </header> <!-- header end --> <!-- mian start --> <main id="main"> <!-- 彈幕的容器 --> {% block main %}{% endblock %} </main> <!-- main end --> <!-- footer start --> <footer id="footer"...> <!-- footer end --> <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script> {% block script %}{% endblock %} </body> </html>1.2>網站內容
這里的內容是彈幕的主體, 可以放在大部分的網站模板上使用
下面的代碼包含, 彈幕的容器, 彈幕列表, 彈幕詳情頁
<!-- Web/App/templates/barrage.html --> {% extends 'base.html' %} {% block title %}LetMeSee-彈幕新聞網{% endblock %} {% block link %}<link rel="stylesheet" href="../static/css/barrage.css"><!-- 解決圖片加載失敗的問題 --><meta name="referrer" content="no-referrer" /> {% endblock %}{% block nav %} <li><a href="/">全部</a></li> <li><a href="/baidu/">新聞</a></li> <li><a href="/bilibili/">B站</a></li> <li><a href="/zhihu/">知乎</a></li> {% endblock %} {% block main %}<div class="box"><div class="barrage-container-wrap clearfix"><div class="barrage-container"><!-- 彈幕主體 --></div><div class="expand"><img src="../static/img/list.png" alt="expand" title="彈幕列表"></div></div></div><!-- 彈幕列表 start --><div class="barrage-list"><div class="list-header">彈幕列表<img src="../static/img/close.png" alt="close" class="close-btn" title="關閉"></div><ul>{% for barrage in barrages %}<!-- for循環展示彈幕 --><li class="barrage-list-item" data-id="{{ barrage.BID }}"><!-- truncate_text過濾器,過長字符串末尾顯示為... -->{{ barrage.BText | truncate_text }}</li>{% endfor %}</ul></div><!-- 彈幕列表 end --><!-- 彈幕詳情 start --><div class="barrage-detail-panel"><div class="list-header">彈幕詳情<img src="../static/img/close.png" alt="close" class="close-btn" title="關閉"></div><h3 class="title"></h3><p class="author"></p><img src="../static/img/loading.gif" alt="彈幕封面" class="cover"><a class="source"><--查看源網頁--></a></div><!-- 彈幕列表 彈幕詳情 --> {% endblock %} {% block script %}<script type="text/javascript">//js和html文件是分開的,傳遞數據需要先定義好參數,再執行js。參考:https://blog.csdn.net/m0_38061194/article/details/78891125var Server = {barrages:{{ barrages|safe }}};</script><script src="../static/js/barrage.js"></script><script src="../static/js/barrage_list.js"></script><script src="../static/js/barrage_details.js"></script> {% endblock %}自定義的過濾器truncate_text如下, 作用是過長字符串末尾顯示為…
# Web/App/my_filters/truncate_text.py def truncate_text(text):if len(text) > 19:new_text = text[0:17] + "..."return new_textelse:return text整理一下上面代碼在頁面中實現的框架, 如圖(同色表示同級):
barrage-container-wrap是彈幕容器的底層畫布, barrage-container是盛放彈幕的容器
barrege-list和barrage-detail是觸發點擊事件才顯示的.
1.3>JS部分
1.3.1>彈幕主體
網上有很多中彈幕的設計方式, 個人認為區別點在于彈幕的不重疊, 本次使用的方式是通過分組定位來實現彈幕不重疊.
//Web/App/static/js/barrage.js //彈幕的實現 (function () {/*******定義參數********/let barrageColorArray = {baidu : '#5519EB', bilibili: '#ff53e0', zhihu: '#0099cc'};let barrageBoxWrap = document.querySelector('.barrage-container-wrap');let barrageBox = document.querySelector('.barrage-container');//容器的寬高度let contentWidth = ~~window.getComputedStyle(barrageBoxWrap).width.replace('px', '');let boxHeight = ~~window.getComputedStyle(barrageBox).height.replace('px', '');//當前窗口可以垂直展示多少個彈幕, 30代表彈幕字體大小let howManyBarrageY = Math.round(boxHeight / 30);//定義一個包含彈幕的寬和高度范圍的數組let heightArray = [];//將每個可用的高度,放入數組, 以便在創建數組時使用for (let i = 30; i < boxHeight - 10; i += 30) {heightArray.push(i)}/*******創建彈幕**********/function createBarrage(item, index, forTime) {if (index >= howManyBarrageY) {//如果索引達到高度數組的長度,則需重置索引到0,因此取余數index = index % howManyBarrageY;}let divNode = document.createElement('div'); //彈幕的標簽let divChildNode = document.createElement('div'); //提示文本的標簽divNode.innerHTML = item.BText; //將彈幕內容插入標簽中, innerHTML表示這個標簽中的字符內容divNode.classList.add('barrage-item'); //追加classbarrageBox.appendChild(divNode); //彈幕的標簽作為彈幕容器的子代標簽divChildNode.innerHTML = '點擊查看詳情'; //鼠標懸停展示的內容divChildNode.classList.add('barrage-link');divNode.appendChild(divChildNode); //提示文本的標簽作為彈幕標簽的子代標簽//***設置彈幕的初始位置***//以容器的寬度為基準隨機生成每條彈幕的左側偏移值let barrageOffsetLeft = getRandom(contentWidth * forTime, contentWidth * (forTime + 0.618));//以容器的高度為基準隨機生成每條彈幕的上方偏移值let barrageOffsetTop = heightArray[index];//通過彈幕類型選擇顏色let barrageColor = barrageColorArray[item.BType];//執行初始化滾動//fun.call()傳入的第一個參數作為之后的this,詳解:https://codeplayer.vip/p/j7sj5initBarrage.call(divNode, {left: barrageOffsetLeft,top: barrageOffsetTop,color: barrageColor,barrageId: item.BID,});}/*******初始化彈幕移動(速度,延遲)*********/function initBarrage(obj) {//初始化位置顏色this.style.left = obj.left + 'px';this.style.top = obj.top + 'px';this.style.color = obj.color;//添加屬性this.distance = 0; //移動速度基準值this.width = ~~window.getComputedStyle(this).width.replace('px', ''); //彈幕的長度this.offsetLeft = obj.left;this.timer = null;this.timeOut = null;//彈幕子節點,即提示信息,span標簽let barrageChileNode = this.children[0];barrageChileNode.style.left = (this.width - barrageTipWidth) / 2 + 'px';//定義span標簽的位置//運動barrageAnimate(this);//鼠標懸停停止this.onmouseenter = function () {cancelAnimationFrame(this.timer);//彈幕停止移動function showDetailPopups() {//顯示提示****此處用于展示詳情窗口barrageChileNode.style.display = 'block';}//設置延遲顯示this.timeOut = setTimeout(showDetailPopups, 1000);};//鼠標移走this.onmouseleave = function () {//鼠標移走,隱藏提示barrageChileNode.style.display = 'none';barrageAnimate(this);//彈幕繼續移動clearTimeout(this.timeOut)};//打開彈幕對應的目標頁面this.onclick = function () {let url = "/detail/",data = {barrage_id:obj.barrageId};$.ajax({type : "get",async : false, //同步請求url : url,data : data,dataType: "json",success:function(barrage){showDetailPanel(barrage)// console.log(barrage)},error: function() {alert("失敗,請稍后再試!");}});};}/*******輔助彈幕移動*********///彈幕動畫function barrageAnimate(obj) {move(obj);if (Math.abs(obj.distance) < obj.width + obj.offsetLeft) {//滿足以上條件說明彈幕在可見范圍內obj.timer = requestAnimationFrame(function () {//在頁面重繪之前會調用這個回調函數-->讓彈幕繼續移動barrageAnimate(obj);});} else {//超出可見范圍,取消回調函數的調用-->讓彈幕停止移動cancelAnimationFrame(obj.timer);//刪除節點obj.parentNode.removeChild(obj);}}//回流:增刪元素會引起回流,重繪:改變樣式會引起重繪//彈幕移動function move(obj) {obj.distance -= 2; //移動速度為一次1像素//transform可以對元素進行翻轉、移動、傾斜等操作,這里主要使用了移動元素的效果obj.style.transform = 'translateX(' + obj.distance + 'px)';}//隨機獲取區間內的一個值function getRandom(start, end) {return start + (Math.random() * (end - start)); //Math.random()隨機獲取一個0~1之間的值}/*******初始化事件**********/ //整個事件的入口//獲取彈幕數據集let barrageArray = Server.barrages;//循環彈幕數組所需的切片次數, 彈幕總數/垂直可以顯示的彈幕數=彈幕播放組數let howManyGroupBarrages = Math.ceil(barrageArray.length / howManyBarrageY);for (let i = 0; i < howManyGroupBarrages; i++) {//對彈幕數組切片,取出一部分要顯示的彈幕,一直循環到取完let eachBarrageArray = barrageArray.slice(howManyBarrageY * i, howManyBarrageY * (i + 1));for (let item of eachBarrageArray) {//遍歷每個彈幕, 并傳入彈幕元素的索引,和循環次數(用作定位)createBarrage(item, eachBarrageArray.indexOf(item), i + 1);}} })();上面的代碼主要完成的了彈幕的生成, 簡單來講就是:生成->分組->定位, 下面這張圖能更清楚的表達邏輯:
PS: 彈幕不重疊還可以使用時間延遲的方式來實現, 有興趣的同學可以參考文章:不碰撞彈幕的研究與實現
1.3.2>彈幕列表
//Web/App/static/js/barrage_list.js let barrageList = document.querySelector('.barrage-list'),barrageDetailPanel = document.querySelector('.barrage-detail-panel'); //彈幕列表的實現 (function () {let expandBtn = document.querySelector('.expand');expandBtn.onclick = function () {//點擊展開再次點擊關閉if (barrageList.style.display === "none") {barrageList.style.display = "block";}else {barrageList.style.display = "none";}//關閉詳情頁顯示列表頁barrageDetailPanel.style.display = 'none'};let barrageItems = document.getElementsByClassName('barrage-list-item'); //li的集合for (let item of barrageItems){let barrageId = item.getAttribute('data-id');//點擊單項打開詳情頁item.onclick = function () {let url = "/detail/",data = {barrage_id:barrageId};//ajax請求, 攜帶參數barrage_id$.ajax({type : "get",async : false, //同步請求url : url,data : data,dataType: "json",success:function(barrage){showDetailPanel(barrage)},error: function() {alert("失敗,請稍后再試!");}});};} })();1.3.3>彈幕詳情
//Web/App/static/js/barrage_details.js //展示彈幕詳情頁 function showDetailPanel(obj) {let barrageTitle = document.querySelector('.title'),barrageAuthor = document.querySelector('.author'),barrageCover = document.querySelector('.cover'),barrageURL = document.querySelector('.source');//關閉列表頁顯示詳情頁barrageDetailPanel.style.display = 'block';barrageList.style.display = "none";//設置詳情頁的參數barrageTitle.innerHTML = obj.BText;barrageAuthor.innerHTML = '--' + obj.BAuthor;barrageCover.setAttribute('src', obj.BCover);barrageURL.onclick = function () {window.open(obj.BUrl);}; }//close button event let closeBtns = document.querySelectorAll('.close-btn'); for (let closeBtn of closeBtns){closeBtn.onclick = function () {barrageDetailPanel.style.display = "none";barrageList.style.display = "none";}; }1.4>其它靜態文件
CSS
https://github.com/zhanghao19/LetMeSee/tree/master/Web/App/static/css
Image
https://github.com/zhanghao19/LetMeSee/tree/master/Web/App/static/css
2>后端
2.1>用flask構建網站
# Web/App/views/first_blue.py import random from pymongo import MongoClient from flask import Blueprint, render_template, request, jsonify# Blueprint(藍圖),提供快速注冊端口,方便快捷. # https://dormousehole.readthedocs.io/en/latest/blueprints.html#blueprints first_blue = Blueprint('index', __name__) # 創建一個藍圖對象coll = MongoClient(host="localhost", port=27017).Spider.LetMeSee# 從數據庫中獲取數據 baidu_barrages = [i for i in coll.find({'BType': 'baidu'},{'_id': 0, 'BID': 1, 'BText': 1, 'BUrl': 1, 'BType': 1})]bilibili_barrages = [i for i in coll.find({'BType': 'bilibili'},{'_id': 0, 'BID': 1, 'BText': 1, 'BUrl': 1, 'BType': 1})]zhihu_barrages = [i for i in coll.find({'BType': 'zhihu'},{'_id': 0, 'BID': 1, 'BText': 1, 'BUrl': 1, 'BType': 1})]@first_blue.route('/') def index():# 拼接兩個彈幕列表barrages = baidu_barrages + bilibili_barrages + zhihu_barragesrandom.shuffle(barrages) # 打亂列表的順序# 渲染模板, 傳遞數據return render_template('barrage.html', barrages=barrages)@first_blue.route('/baidu/') def baidu():return render_template('barrage.html', barrages=baidu_barrages)@first_blue.route('/bilibili/') def bilibili():return render_template('barrage.html', barrages=bilibili_barrages)@first_blue.route('/zhihu/') def zhihu():return render_template('barrage.html', barrages=zhihu_barrages)@first_blue.route('/detail/') def barrage_details():# 獲取ajax請求攜帶的data中的barrage_idbarrage_id = request.args.get('barrage_id')# 通過barrage_id取匹配數據庫里的項barrage = coll.find_one({'BID': barrage_id},{'_id': 0, 'WriteTime': 0})print(barrage, barrage_id, type(barrage_id))# 以json的形式返回響應return jsonify(barrage) # Web/App/views/__init__.py from .first_blue import first_blue from Web.App.my_filters.truncate_text import truncate_textdef init_view(app):# 在應用對象上注冊這個藍圖對象app.register_blueprint(first_blue)# 指定jinja引擎env = app.jinja_env# 加載自定義過濾器env.filters["truncate_text"] = truncate_text # Web/App/__init__.py from flask import Flaskfrom Web.App.views import init_viewdef create_app():# 創建一個應用對象app = Flask(__name__)# 調用該方法,以初始化路由init_view(app)return app # manage.py from flask_script import Managerfrom Web.App import create_appapp = create_app() manager = Manager(app=app)if __name__ == '__main__':manager.run() # 使flask能夠像django一樣使用命令啟動, "python manage.py runserver -r -d"參考文檔: 快速上手flask / Blueprint / jsonify
參考視頻: 黑馬程序員-6節課入門Flask框架web開發視頻
ps: 我也是看這個視頻學的flask, 老師講解的很棒!
2.2>爬蟲
2.2.1>百度新聞
# Spider/spider_mode/baidu_spider.py import requests from datetime import datetime from lxml import etreefrom pymongo import MongoClientcoll = MongoClient(host="localhost", port=27017).Spider.LetMeSeeresp = requests.get('https://news.baidu.com/') # 請求頁面 html = etree.HTML(resp.text) # 創建xpath對象 barrage = [] item = {}title_ls = html.xpath('//*[@id="pane-news"]//a//text()') # 提取標題 url_ls = html.xpath('//*[@id="pane-news"]//a/@href') # 提取鏈接for n in range(len(title_ls)):item['BID'] = f'{n + 86000}' # iditem['BText'] = title_ls[n]item['BUrl'] = url_ls[n]item['BType'] = 'baidu'item['BCover'] = r'D:\Fire\PycharmProject\LetMeSee\Web\App\static\img\loading.gif' # 封面item['BAuthor'] = '未知作者' # 作者item['WriteTime'] = datetime.utcnow() # 寫入時間, 用于設置過期時間coll.insert_one(dict(item)) print('百度新聞--爬取完成!')2.2.2>B站榜單
# Spider/spider_mode/bilibili_spider.py from datetime import datetime import json import requests import refrom pymongo import MongoClientcoll = MongoClient(host="localhost", port=27017).Spider.LetMeSeeresp = requests.get('https://www.bilibili.com/ranking') # 請求頁面 # 使用正則獲取源碼中存放在script標簽中的數據 data_url = re.findall('window.__INITIAL_STATE__=(.*);\(function', resp.text)[0] data_loaded = json.loads(data_url) # 使用loads方法從 字符串 變成 字典 rankList = data_loaded['rankList'] # 排行榜中100個視頻的信息item ={} for i in range(len(rankList)):item['BID'] = f'{i + 81000}' # iditem['BText'] = rankList[i]['title'] # 標題item['BAuthor'] = rankList[i]['author'] # 作者item['BUrl'] = 'https://www.bilibili.com/video/' + rankList[i]['bvid'] # 拼接的視頻av號item['BType'] = 'bilibili'item['BCover'] = rankList[i]['pic'] # 封面item['WriteTime'] = datetime.utcnow() # 寫入時間, 用于設置過期時間coll.insert_one(dict(item)) print('B站榜單--爬取完成!')2.2.3>知乎熱榜
# Spider/spider_mode/zhihu_spider.py import json from datetime import datetimeimport requests from lxml import etree from pymongo import MongoClient# 用戶登錄后的cookies,直接F12->Network復制Request Headers的cookie即可, 這里只是自己建了一個放cookies的文件 from util.zhihu_cookies import Cookiescoll = MongoClient(host="localhost", port=27017).Spider.LetMeSeeheaders = {'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8','cache-control': 'max-age=0','cookie': Cookies, # 也可以直接將cookies直接copy到這里'upgrade-insecure-requests': '1','user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36'} resp = requests.get('https://www.zhihu.com/hot', headers=headers) # 請求頁面html = etree.HTML(resp.text) # 創建xpath對象data = html.xpath('//*[@id="js-initialData"]/text()')[0] # 提取數據集 data_loaded = json.loads(data) # 使用loads方法從 字符串 變成 字典 hotList = data_loaded["initialState"]["topstory"]["hotList"] # 提取目標數據'hotList'item ={} for i in range(len(hotList)):item['BID'] = f'{i + 83000}' # iditem['BText'] = hotList[i]["target"]["titleArea"]["text"] # 標題item['BAuthor'] = hotList[i]["target"]["metricsArea"]["text"] # 標題item['BUrl'] = hotList[i]["target"]["link"]["url"] # 拼接的視頻av號item['BType'] = 'zhihu'item['BCover'] = hotList[i]["target"]["imageArea"]["url"] # 封面item['WriteTime'] = datetime.utcnow() # 寫入時間, 用于設置過期時間coll.insert_one(dict(item)) print('知乎熱榜--爬取完成!')2.3>運行爬蟲
爬蟲文件都可以直接運行, 為了節省不必要的時間, 所以將它們整理到一個文件中運行, 如下:
# Spider/runSpider.py from pymongo import MongoClient import os# 創建數據庫對象 coll = MongoClient(host="localhost", port=27017).Spider.LetMeSee coll.drop() # 清空LetMeSee, 目的是使數據保持最新 # 設置延遲刪除字段, 單位為秒 coll.create_index([('WriteTime', 1)], expireAfterSeconds=43200)os.system(r"python D:\Fire\PycharmProject\LetMeSee\Spider\spider_mode\bilibili_spider.py") os.system(r"python D:\Fire\PycharmProject\LetMeSee\Spider\spider_mode\baidu_spider.py") os.system(r"python D:\Fire\PycharmProject\LetMeSee\Spider\spider_mode\zhihu_spider.py")3>總結
好了以上就是本次分享的全部內容了, 目前項目的規模不算大, 但有很大的擴展性, 后續如果有更多點子會再更新這個項目.
總結
以上是生活随笔為你收集整理的搭建一个弹幕新闻网站的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: swarm集群搭建教程
- 下一篇: 检测本地连接并自动连接宽带连接.cmd