者:solideogloria
來源:https://blog.yasking.org/a/python-and-selenium-qqvote.html
抽時間研究下QQ群投票信息的獲取,比較適合(高頻次/投票人數較多)的投票統計。不知道QQ群有沒有API可以獲取投票信息,反正找了一圈沒發現。那就想想看怎么辦
QQ客戶端肯定是沒辦法了...自然而然的想到網頁版也有群空間之類的地方,可以下載群文件,查看群相冊什么的。去碰碰運氣,然而只可以發起投票,不能獲取投票的人名稱什么的。比較難辦,最后翻翻用手機看看,天無絕人之路,在投票頁面可以復制投票地址。
類似這樣的URL: http://client.qun.qq.com/qqweb/m/qun/vote/detail.html?_lv=38105&_wv=1031&_bid=2035&src=3&groupuin=151496851&fid=93a8070900000000d38621575e800***
在瀏覽器中直接打開會出現"載入中,請稍后..."的字眼。這是因為沒有登陸,沒有權限
在這里登陸自己的QQ,再訪問就可以了
此時的選項是不可點擊的,因為網頁檢測你的瀏覽環境不是手機界面,并且是不可觸摸的
在Chrome下很好解決,只需進入響應式模式即可,如果是火狐,則需要進入火狐后再次點擊"加載觸摸事件",就可以點擊進入詳情頁
既然在瀏覽器端已經可以獲取到這些數據,那么應該就可以使用python和selenium來獲取數據
自然而然的想到如何讓Chrome和Firefox來進入這種模式呢?
單單換UA和窗口尺寸是不行的,因為不會加載觸摸事件。
Firefox driver我沒有找到這樣的操作,好在Chrome可以。主要代碼如下:
#!/bin/env python
# -*- coding:utf-8 -*-
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
presets=[
{"key":"1080 x 1920","name":"Nexus 5 Portrait","width":1080,"height":1920},
]
mobile_emulation={
"deviceMetrics": { "width": 360, "height": 640, "pixelRatio": 3.0 },
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.23 Mobile Safari/537.36" }
chrome_options=Options
chrome_options.add_experimental_option("mobileEmulation", mobile_emulation)
driver=webdriver.Chrome(chrome_options=chrome_options)
這樣獲得的driver就可以正常瀏覽投票頁面了
還有個值得注意的是當投票人數比較多的情況下,不會一次加載完成的。每當鼠標滾動到最下方,就會加載一些數據。我最開始本打算用selenium進行點擊拖動來加載數據,后來查詢到可以用js控制右側的滾動條來實現
這里比較偷懶,嘗試拖動三次,其實應該在首頁獲取投票人數,第一次加載100人,看看QQ的js,找到之后一次加載多少數據后,得到循環的次數...
# 滑動界面獲取全部投票
try:
for _ in range(3):
js="var q=document.body.scrollTop=10000"
driver.execute_script(js)
time.sleep(2)
except:
pass
其它的就沒什么好說的了,先登錄,然后訪問投票頁面。
完整的代碼在這里:python-qqvote
只獲取了投票第一項的數據,如果不知道投票選項,需要在投票首頁的地方獲取投票選項數量,比較懶...就這樣吧
QQ投票頁面的數據中只有用戶昵稱,比較可惜,不過可以在QQ群空間獲取QQ號和昵稱的對應關系。
獲取QQ群成員的代碼在這里:python-qun-people
參考:
https://sites.google.com/a/chromium.org/chromedriver/mobile-emulation
http://blog.csdn.net/winterto1990/article/details/48215941
題圖:pexels,CC0 授權。
閩南網]
- 英雄聯盟幸運召喚師現在已經開放,活動是上周五才剛開的,因為這次也是突然就開了,官方也沒有宣傳,所以應該很多小伙伴還不知道,以下是活動地址,最新一期的,可以進入頁面登錄賬號看看是不是本期幸運星。
- 【幸運召喚師】
- 1、活動地址:http://lol.qq.com/act/a20180224lucky/index.html
- 2、活動時間:10月26日~11月6日(預計)
- 3、關于幸運星獲得說明
- 如果進入后提示說自己不是本期幸運星,大家不要放棄,有小號的登錄看看,一般等級低的比等級高的容易獲得資格,此外,這個資格是官方隨機給的,所以也是只能憑運氣。
- 4、關于活動獎勵說明
- 這個活動就是抽皮膚折扣的,一次抽三種,但只能在頁面購買皮膚時才有優惠,如果是抽到折扣后,再到商城去買是不會打折的,而且必須先擁有英雄才能買皮膚。
- 以上就是幸運召喚師本次活動說明,想了解更多游戲資訊嗎?請繼續關注琵琶網吧,這里有最新最全的游戲資訊和游戲攻略等你來看。
信大家都是知道游戲的吧。
這玩意還是很有意思的,無論是超級瑪麗,還是魂斗羅,亦或者是王者榮耀以及陰陽師。
當然,這篇文章不涉及到那么牛逼的游戲,這里就簡單的做一個小游戲吧。
先給它取個名字,就叫“球球作戰”吧。
咳咳,簡單易懂嘛
任何人進入游戲輸入名字然后就可以連接進入游戲,控制一個小球。
你可以操作這個小球進行一些動作,比如:移動,發射子彈。
通過殺死其他玩家來獲取積分,并在排行榜上進行排名。
其實這類游戲有一個統一的名稱,叫做IO類游戲,在這個網站中有大量的這類游戲:https://iogames.space/
這個游戲的github地址:https://github.com/lionet1224/node-game
在線體驗: http://120.77.44.111:3000/
演示GIF:
首先制作這個游戲,我們需要的技術為:
并且你需要對以下技術有一定了解:
其實本來想使用deno和ts來開發的,但是因為我對這兩項技術都是半生不熟的階段,所以就不拿出來獻丑了。
后端服務需要做的是:
前端需要做的是:
這也是典型的狀態同步方式開發游戲。
因為前端是通過后端的數據驅動的,所以我們就先開發后端。
首先我們需要下載express,在根目錄下輸入以下命令:
// 創建一個package.json文件
> npm init
// 安裝并且將其置入package.json文件中的依賴中
> npm install express socket.io --save
// 安裝并置入package.json的開發依賴中
> npm install cross-env nodemon --save-dev
這里我們也可以使用cnpm進行安裝
然后在根目錄中瘋狂建文件夾以及文件。
image.png
我們就可以得出以上的文件啦。
解釋一下分別是什么東西:
然后我們在server.js中編寫啟動服務的相關代碼。
// server.js
// 引入各種模塊
const express = require('express')
const socketio = require('socket.io');
const app = express();
const Socket = require('./core/socket');
const Game = require('./core/game');
// 啟動服務
const port = process.env.PORT || 3000;
const server = app.listen(3000, () => {
console.log('Server Listening on port: ' + port)
})
// 實例游戲類
const game = new Game;
// 監聽socket服務
const io = socketio(server)
// 將游戲以及io傳入創建的socket類來統一管理
const socket = new Socket(game, io);
// 監聽連接進入游戲的回調
io.on('connect', item => {
socket.listen(item)
})
上面的代碼還引入了兩個其他文件core/game、core/socket。
這兩個文件中的代碼,我大致的編寫了一下。
// core/game.js
class Game{
constructor(){
// 保存玩家的socket信息
this.sockets = {}
// 保存玩家的游戲對象信息
this.players = {};
// 子彈
this.bullets = [];
// 最后一次執行時間
this.lastUpdateTime = Date.now();
// 是否發送給前端數據,這里將每兩幀發送一次數據
this.shouldSendUpdate = false;
// 游戲更新
setInterval(this.update.bind(this), 1000 / 60);
}
update(){
}
// 玩家加入游戲
joinGame(){
}
// 玩家斷開游戲
disconnect(){
}
}
module.exports = Game;
// core/socket.js
const Constants = require('../../shared/constants')
class Socket{
constructor(game, io){
this.game = game;
this.io = io;
}
listen(){
// 玩家成功連接socket服務
console.log(`Player connected! Socket Id: ${socket.id}`)
}
}
module.exports = Socket
在core/socket中引入了常量文件,我們來看看我在其中是怎么定義的。
// shared/constants.js
module.exports = Object.freeze({
// 玩家的數據
PLAYER: {
// 最大生命
MAX_HP: 100,
// 速度
SPEED: 500,
// 大小
RADUIS: 50,
// 開火頻率, 0.1秒一發
FIRE: .1
},
// 子彈
BULLET: {
// 子彈速度
SPEED: 1500,
// 子彈大小
RADUIS: 20
},
// 道具
PROP: {
// 生成時間
CREATE_TIME: 10,
// 大小
RADUIS: 30
},
// 地圖大小
MAP_SIZE: 5000,
// socket發送消息的函數名
MSG_TYPES: {
JOIN_GAME: 1,
UPDATE: 2,
INPUT: 3
}
})
Object.freeze() 方法可以凍結一個對象。一個被凍結的對象再也不能被修改;凍結了一個對象則不能向這個對象添加新的屬性,不能刪除已有屬性,不能修改該對象已有屬性的可枚舉性、可配置性、可寫性,以及不能修改已有屬性的值。此外,凍結一個對象后該對象的原型也不能被修改。freeze() 返回和傳入的參數相同的對象。- MDN
通過上面的四個文件的代碼,我們已經擁有了一個具備基本功能的后端服務結構了。
接下來就來將它啟動起來吧。
在package.json中編寫啟動命令。
// package.json
{
// ...
"scripts": {
"dev": "cross-env NODE_ENV=development nodemon src/servers/server.js",
"start": "cross-env NODE_ENV=production nodemon src/servers/server.js"
}
//..
}
這里的兩個命令dev和start都使用到了cross-env和nodemon,這里解釋一下:
執行以下命令開啟開發模式。
> npm run dev
可以看到我們成功的啟動服務了,監聽到了3000端口。
在服務中,我們搭載了socket服務,那要怎么測試是否有效呢?
所以我們現在簡單的搭建一下前端吧。
我們在開發前端的時候,用到模塊化的話會開發更加絲滑一些,并且還有生產環境的打包壓縮,這些都可以使用到Webpack。
我們的打包有兩種不同的環境,一種是生產環境,一種是開發環境,所以我們需要兩個webpack的配置文件。
當然傻傻的直接寫兩個就有點憨憨了,我們將其中重復的內容給解構出來。
我們在根目錄下創建webpack.common.js、webpack.dev.js、webpack.prod.js三個文件。
此步驟的懶人安裝模塊命令:
npm install @babel/core @babel/preset-env babel-loader css-loader html-webpack-plugin mini-css-extract-plugin optimize-css-assets-webpack-plugin terser-webpack-plugin webpack webpack-dev-middleware webpack-merge webpack-cli \--save-dev
// webpack.common.js
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
game: './src/client/index.js',
},
// 將打包文件輸出到dist文件夾
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
// 使用babel解析js
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ['@babel/preset-env'],
},
},
},
// 將js中的css抽出來
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
},
'css-loader',
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
// 將處理后的js以及css置入html中
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'src/client/html/index.html',
}),
],
};
上面的代碼已經可以處理css以及js文件了,接下來我們將它分配給development和production中,其中production將會壓縮js和css以及html。
// webpack.dev.js
const { merge } = require('webpack-merge')
const common = require('./webpack.common')
module.exports = merge(common, {
mode: 'development'
})
// webpack.prod.js
const { merge } = require('webpack-merge')
const common = require('./webpack.common')
// 壓縮js的插件
const TerserJSPlugin = require('terser-webpack-plugin')
// 壓縮css的插件
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
module.exports = merge(common, {
mode: 'production',
optimization: {
minimizer: [new TerserJSPlugin({}), new OptimizeCssAssetsPlugin({})]
}
})
上面已經定義好了三個不同的webpack文件,那么該怎么樣使用它們呢?
首先開發模式,我們需要做到修改了代碼就自動打包代碼,那么代碼如下:
// src/servers/server.js
const webpack = require('webpack')
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpackConfig = require('../../webpack.dev')
// 前端靜態文件
const app = express();
app.use(express.static('public'))
if(process.env.NODE_ENV === 'development'){
// 這里是開發模式
// 這里使用了webpack-dev-middleware的中間件,作用就是代碼改動就使用webpack.dev的配置進行打包文件
const compiler = webpack(webpackConfig);
app.use(webpackDevMiddleware(compiler));
} else {
// 上線環境就只需要展示打包后的文件夾
app.use(express.static('dist'))
}
接下來就在package.json中添加相對應的命令吧。
{
//...
"scripts": {
"build": "webpack --config webpack.prod.js",
"start": "npm run build && cross-env NODE_ENV=production nodemon src/servers/server.js"
},
//...
}
接下來,我們試試dev和start的效果吧。
可以看到使用npm run dev命令后不僅啟動了服務還打包了前端文件。
再試試npm run start。
也可以看到先打包好了文件再啟動了服務。
我們來看看打包后的文件。
先讓我裝一下前端的socket.io。
> npm install socket.io-client --save
然后編寫一下前端文件的入口文件:
// src/client/index.js
import { connect } from './networking'
Promise.all([
connect()
]).then(() => {
}).catch(console.error)
可以看到上面代碼我引入了另一個文件networking,我們來看一下:
// src/client/networking
import io from 'socket.io-client'
// 這里判斷是否是https,如果是https就需要使用wss協議
const socketProtocal = (window.location.protocol.includes('https') ? 'wss' : 'ws');
// 這里就進行連接并且不重新連接,這樣可以制作一個斷開連接的功能
const socket = io(`${socketProtocal}://${window.location.host}`, { reconnection: false })
const connectPromise = new Promise(resolve => {
socket.on('connect', () => {
console.log('Connected to server!');
resolve();
})
})
export const connect = onGameOver => {
connectPromise.then(()=> {
socket.on('disconnect', () => {
console.log('Disconnected from server.');
})
})
}
上面的代碼就是連接socket,將會自動獲取地址然后進行連接,通過Promise傳給index.js,這樣入口文件就可以知道什么時候連接成功了。
我們現在就去前端頁面中看一下吧。
可以很清楚的看到,前后端都有連接成功的相關提示。
我們現在來定義一下游戲中的游戲對象吧。
首先游戲中將會有四種不同的游戲對象:
我們來一一將其實現吧。
首先他們都屬于物體,所以我給他們都定義一個父類Item:
// src/servers/objects/item.js
class Item{
constructor(data = {}){
// id
this.id = data.id;
// 位置
this.x = data.x;
this.y = data.y;
// 大小
this.w = data.w;
this.h = data.h;
}
// 這里是物體每幀的運行狀態
update(dt){
}
// 格式化數據以方便發送數據給前端
serializeForUpdate(){
return {
id: this.id,
x: this.x,
y: this.y,
w: this.w,
h: this.h
}
}
}
module.exports = Item;
上面這個類是所有游戲對象都要繼承的類,它定義了游戲世界里每一個元素的基本屬性。
接下來就是player、Prop、Bullet的定義了。
// src/servers/objects/player.js
const Item = require('./item')
const Constants = require('../../shared/constants')
/**
* 玩家對象類
*/
class Player extends Item{
constructor(data){
super(data);
this.username = data.username;
this.hp = Constants.PLAYER.MAX_HP;
this.speed = Constants.PLAYER.SPEED;
// 擊敗分值
this.score = 0;
// 擁有的buffs
this.buffs = [];
}
update(dt){
}
serializeForUpdate(){
return {
...(super.serializeForUpdate()),
username: this.username,
hp: this.hp,
buffs: this.buffs.map(item => item.type)
}
}
}
module.exports = Player;
然后是道具以及子彈的定義。
// src/servers/objects/prop.js
const Item = require('./item')
/**
* 道具類
*/
class Prop extends Item{
constructor(){
super();
}
}
module.exports = Prop;
// src/servers/objects/bullet.js
const Item = require('./item')
/**
* 子彈類
*/
class Bullet extends Item{
constructor(){
super();
}
}
module.exports = Bullet
上面都是簡單的定義,隨著開發會逐漸添加內容。
上面的代碼雖然已經定義好了,但是還需要使用它,所以在這里我們來開發使用它們的方法。
在玩家輸入名稱加入游戲后,需要生成一個Player的游戲對象。
// src/servers/core/socket.js
class Socket{
// ...
listen(socket){
console.log(`Player connected! Socket Id: ${socket.id}`);
// 加入游戲
socket.on(Constants.MSG_TYPES.JOIN_GAME, this.game.joinGame.bind(this.game, socket));
// 斷開游戲
socket.on('disconnect', this.game.disconnect.bind(this.game, socket));
}
// ...
}
然后在game.js中添加相關邏輯。
// src/servers/core/game.js
const Player = require('../objects/player')
const Constants = require('../../shared/constants')
class Game{
// ...
update(){
const now = Date.now();
// 現在的時間減去上次執行完畢的時間得到中間間隔的時間
const dt = (now - this.lastUpdateTime) / 1000;
this.lastUpdateTime = now;
// 更新玩家人物
Object.keys(this.players).map(playerID => {
const player = this.players[playerID];
player.update(dt);
})
if(this.shouldSendUpdate){
// 發送數據
Object.keys(this.sockets).map(playerID => {
const socket = this.sockets[playerID];
const player = this.players[playerID];
socket.emit(
Constants.MSG_TYPES.UPDATE,
// 處理游戲中的對象數據發送給前端
this.createUpdate(player)
)
})
this.shouldSendUpdate = false;
} else {
this.shouldSendUpdate = true;
}
}
createUpdate(player){
// 其他玩家
const otherPlayer = Object.values(this.players).filter(
p => p !== player
);
return {
t: Date.now(),
// 自己
me: player.serializeForUpdate(),
others: otherPlayer,
// 子彈
bullets: this.bullets.map(bullet => bullet.serializeForUpdate())
}
}
// 玩家加入游戲
joinGame(socket, username){
this.sockets[socket.id] = socket;
// 玩家位置隨機生成
const x = (Math.random() * .5 + .25) * Constants.MAP_SIZE;
const y = (Math.random() * .5 + .25) * Constants.MAP_SIZE;
this.players[socket.id] = new Player({
id: socket.id,
username,
x, y,
w: Constants.PLAYER.WIDTH,
h: Constants.PLAYER.HEIGHT
})
}
disconnect(socket){
delete this.sockets[socket.id];
delete this.players[socket.id];
}
}
module.exports = Game;
這里我們開發了玩家的加入以及退出,還有Player對象的數據更新,以及游戲的數據發送。
現在后端服務已經有能力提供內容給前端了,接下來我們開始開發前端的界面吧。
上面的內容讓我們開發了一個擁有基本功能的后端服務。
接下來來開發前端的相關功能吧。
我們來看看后端發過來的數據是什么樣的吧。
先在前端編寫接收的方法。
// src/client/networking.js
import { processGameUpdate } from "./state";
export const connect = onGameOver => {
connectPromise.then(()=> {
// 游戲更新
socket.on(Constants.MSG_TYPES.UPDATE, processGameUpdate);
socket.on('disconnect', () => {
console.log('Disconnected from server.');
})
})
}
export const play = username => {
socket.emit(Constants.MSG_TYPES.JOIN_GAME, username);
}
// src/client/state.js
export function processGameUpdate(update){
console.log(update);
}
// src/client/index.js
import { connect, play } from './networking'
Promise.all([
connect()
]).then(() => {
play('test');
}).catch(console.error)
上面的代碼就可以讓我們進入頁面就直接加入游戲了,去頁面看看效果吧。
image.png
原文鏈接: https://mp.weixin.qq.com/s/hoc5YXVRDDV_7jGmrO5Vfg
*請認真填寫需求信息,我們會在24小時內與您取得聯系。