本項目僅供學習使用, 請勿用來進行商業用途
本期知識點:
- 使用JS製作彈幕的方法
- 使用分組定位來實現彈幕不重疊
- 使用flask構建網站
- 爬蟲: 百度新聞, B站榜單, 知乎熱榜
前言
你是否在 刷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/78891125
var 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_text
else:
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'); //追加class
barrageBox.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/j7sj5
initBarrage.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_barrages
random.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_id
barrage_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_text
def 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 Flask
from Web.App.views import init_view
def create_app():
# 創建一個應用對象
app = Flask(__name__)
# 調用該方法,以初始化路由
init_view(app)
return app
# manage.py
from flask_script import Manager
from Web.App import create_app
app = 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 etree
from pymongo import MongoClient
coll = MongoClient(host="localhost", port=27017).Spider.LetMeSee
resp = 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}' # id
item['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 re
from pymongo import MongoClient
coll = MongoClient(host="localhost", port=27017).Spider.LetMeSee
resp = 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}' # id
item['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 datetime
import requests
from lxml import etree
from pymongo import MongoClient
# 用戶登錄後的cookies,直接F12->Network複製Request Headers的cookie即可, 這裏只是自己建了一個放cookies的文件
from util.zhihu_cookies import Cookies
coll = MongoClient(host="localhost", port=27017).Spider.LetMeSee
headers = {'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}' # id
item['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>總結
好了以上就是本次分享的全部內容了, 目前項目的規模不算大, 但有很大的擴展性, 後續如果有更多點子會再更新這個項目.