整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

使用 Python 获取 QQ 群投票数据

使用 Python 获取 QQ 群投票数据

者: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可以。主要代码如下:

  1. #!/bin/env python

  2. # -*- coding:utf-8 -*-

  3. from selenium import webdriver

  4. from selenium.webdriver.chrome.options import Options

  5. presets=[

  6. {"key":"1080 x 1920","name":"Nexus 5 Portrait","width":1080,"height":1920},

  7. ]

  8. mobile_emulation={

  9. "deviceMetrics": { "width": 360, "height": 640, "pixelRatio": 3.0 },

  10. "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" }

  11. chrome_options=Options

  12. chrome_options.add_experimental_option("mobileEmulation", mobile_emulation)

  13. driver=webdriver.Chrome(chrome_options=chrome_options)

这样获得的driver就可以正常浏览投票页面了

还有个值得注意的是当投票人数比较多的情况下,不会一次加载完成的。每当鼠标滚动到最下方,就会加载一些数据。我最开始本打算用selenium进行点击拖动来加载数据,后来查询到可以用js控制右侧的滚动条来实现

这里比较偷懒,尝试拖动三次,其实应该在首页获取投票人数,第一次加载100人,看看QQ的js,找到之后一次加载多少数据后,得到循环的次数...

  1. # 滑动界面获取全部投票

  2. try:

  3. for _ in range(3):

  4. js="var q=document.body.scrollTop=10000"

  5. driver.execute_script(js)

  6. time.sleep(2)

  7. except:

  8. 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:

准备工作

首先制作这个游戏,我们需要的技术为:

  • 前端
    • Socket.io
    • Webpack
  • 后端
    • Node
    • Socket.io
    • express
    • ...

并且你需要对以下技术有一定了解:

  • Canvas
  • 面向对象
  • ES6
  • Node
  • Promise

其实本来想使用denots来开发的,但是因为我对这两项技术都是半生不熟的阶段,所以就不拿出来献丑了。

游戏架构

后端服务需要做的是:

  • 存储生成的游戏对象,并且将其发送给前端。
  • 接收前端的玩家操作,给游戏对象进行数据处理

前端需要做的是:

  • 接收后端发送的数据并将其渲染出来。
  • 将玩家的操作发送给服务器

这也是典型的状态同步方式开发游戏。

后端服务搭建开发

因为前端是通过后端的数据驱动的,所以我们就先开发后端。

搭建起一个Express服务

首先我们需要下载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

我们就可以得出以上的文件啦。

解释一下分别是什么东西:

  • public 存储一些资源
  • src 开发代码
    • core 核心代码
    • objects 玩家、道具等对象
    • client 前端代码
    • servers 后端代码
    • shared 前后端共用常量

编写基本代码

然后我们在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/gamecore/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"
    }
    //..
}

这里的两个命令devstart都使用到了cross-envnodemon,这里解释一下:

  • cross-env 设置环境变量,这里可以看到这个后面还有一个NODE_ENV=development/production,判断是否是开发模式。
  • nodemon 这个的话说白了就是监听文件变化并重置Node服务。

启动服务看一下吧

执行以下命令开启开发模式。

> npm run dev

可以看到我们成功的启动服务了,监听到了3000端口。

在服务中,我们搭载了socket服务,那要怎么测试是否有效呢?

所以我们现在简单的搭建一下前端吧。

Webpack搭建前端文件

我们在开发前端的时候,用到模块化的话会开发更加丝滑一些,并且还有生产环境的打包压缩,这些都可以使用到Webpack

我们的打包有两种不同的环境,一种是生产环境,一种是开发环境,所以我们需要两个webpack的配置文件。

当然傻傻的直接写两个就有点憨憨了,我们将其中重复的内容给解构出来。

我们在根目录下创建webpack.common.jswebpack.dev.jswebpack.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文件了,接下来我们将它分配给developmentproduction中,其中production将会压缩jscss以及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"
  },
//...
}

接下来,我们试试devstart的效果吧。

可以看到使用npm run dev命令后不仅启动了服务还打包了前端文件。

再试试npm run start

也可以看到先打包好了文件再启动了服务。

我们来看看打包后的文件。

测试Socket是否有效

先让我装一下前端的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,这样入口文件就可以知道什么时候连接成功了。

我们现在就去前端页面中看一下吧。

可以很清楚的看到,前后端都有连接成功的相关提示。

创建游戏对象

我们现在来定义一下游戏中的游戏对象吧。

首先游戏中将会有四种不同的游戏对象:

  • Player 玩家人物
  • Prop 道具
  • Bullet 子弹

我们来一一将其实现吧。

首先他们都属于物体,所以我给他们都定义一个父类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;

上面这个类是所有游戏对象都要继承的类,它定义了游戏世界里每一个元素的基本属性。

接下来就是playerPropBullet的定义了。

// 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