王者营地爬虫复盘

Posted by Pismery Liu on Tuesday, September 10, 2019

TOC

王者营地数据爬取

这两周应大佬吩咐去王者营地 app 爬取相关数据,在这里复盘一下自己在爬取过程中的思考。

目标

爬取 app 中每局的数据至 excel ,包含战局比分,事件时间名称,以及最后死亡时间;大致效果如下:

执行过程

在此之前,没有任何的爬虫经验,对于爬虫仅有的认知是如果目标程序是通过 http 协议通信,则可以通过数据抓包工具,抓取 http 数据包分析内容,找到想要的数据;而后通过模拟 http 请求来爬取需要的数据;

于是自己大概捋了一下执行过程:

  1. 了解并学会使用一个抓包工具;
  2. 尝试学习并完成一个简单的 python 爬虫,先爬一爬,热热身;
  3. 开始抓包分析;
  4. 爬取结果;

捋好思路就开始干了,了解了一下抓包工具,发现了 Fiddle,就立刻决定用它了。原因无他,看到过身边的同事在用;于是开始学习工具,找到了一篇写的不错的入门文章,半小时就能上手了。

接着了解了一下 python 爬虫小程序,模仿教程写了下面的第一个 python 爬虫小程序了;

## 获取指定网页的 html 数据
import urllib.request
data  = urllib.request.urlopen("https://read.douban.com/provider/all").read()
data = data.decode("utf-8")

## 正则表达式分析获取 html 文件内容
import re
pat = '<div class="name">(.*?)</div>'
data = re.compile(pat).findall(data)

## 将数据写入文件中
fs = open("e:/document/1.txt","w")
for i in range(0,len(data)):
    fs.write(data[i]+"\n")
     
fs.flush()

开始抓包,发现 app 抓包并不像浏览器抓包那样简单,花了两天时间折腾环境,尝试抓包依旧没有任何进展;转化一下思路,app 中有微信转发功能,将战局转发至微信,而后通过浏览器打开,这样就能够处理了。

浏览器打开的页面效果如下:

从页面中可以轻松找到战局比分,事件发生时间,事件名称等信息,且所有事件信息都是已经再 html 中生成好了,可通过正则表达式直接分析 html 获取。

而获取死亡时间需要操作步骤如下:

  1. 点击我方详细数据,获取死亡时间
  2. 点击敌方详细数据,获取死亡时间
  3. 比对获取最后死亡时间

问题来了,如何模拟点击,此前听说过 selenium 自动化测试,印象中它能够完成这样的功能;于是经过两个晚上的现学现用,发现虽然能够达到目的,但是操作过于繁琐,爬取效率很低,决定放弃,重新另找思路; html 中死亡时间的数据,需要点击「详细信息」才会更新,如何能够不通过点击获取到数据? 想到能不能像分析 http 数据包一样,分析页面 js 逻辑,看看最后数据是如何获取的,再模拟来获取即可。于是就开始了 js 源码的分析。

分析 js

首先通过 Chrome debug 窗口的 EventListener 找到点击事件的 js 代码入口

经过关键字搜索查找,终于找到了获取死亡时间的 js 函数

分析这个函数,只要弄清楚参数 t 和 eventInfo 的数据结构以及来源则可以自己实现逻辑了。然后查看了一下 localStorage 中的存储,意外发现了数据内容,

接着就要分析如何拿到 key 值,通过检索关键字 「analyze_」 发现了相应代码,从代码可以看出 key 只需要获取 url 的参数拼接即可完成。

最后分析 value 的内容,看看如何获取指定的死亡时间; 从上面找到的 getDeadTime() 函数,可以知道死亡时间是从 eventInfo.battle.killInfo 中获取;对比 localStorage 数据也发现了相应的结构;并且根据页面数据比对,发现了事件数据即为 keyEventArr;这样更加确认了找到的 killInfo 就是需要的死亡事件数据。

死亡时间数据已确认拿到,最后一步,怎么区分死亡时间所属事件? 从 getDeadTime() 方法发现是通过 objectId 匹配死亡时间与事件的,自然想到如何获取 obejctId,经过一系列 debug 依旧没有进度;而后想到了通过事件时间来匹配,毕竟每个事件发生时间是不会重复的;观察数据发现 localStorage 中的时间是一串数字,我们获取的时间经过了转换;继而开始找时间转换的函数,发现就在 getDeadTime() 中的 getTimeStr() 方法中。简单搜索就找到了相应的代码了。至此,这个数据链路也就疏通了;

最后,捋一下编码思路,就可以开始撸码了。

  1. 通过 selenium 用 Chrome 打开链接;
  2. 获取 localStorage key 值;
  3. 获取 localStorage value 值;
  4. 通过 selenium 获取战局比分,事件时间,事件名称;
  5. 通过事件时间与 localStorage value 值匹配获取死亡时间;
  6. 将数据写入 excel;

代码

import time
import re
import os
import math
import json

from selenium import webdriver
from urllib import parse
from openpyxl import Workbook
from openpyxl.utils import get_column_letter

class GameData:
    gameNo = 0
    blueScore = 0
    redScore = 0
    eventTime = []
    eventName = []
    killTime = []

    def __init__(self,gn,b,r,et,en,kt):
        self.gameNo = gn
        self.blueScore = b
        self.redScore = r
        self.eventTime = et
        self.eventName = en
        self.killTime = kt

def getUrlParm(driver,url):
    return parse.parse_qs(parse.urlparse(url).query)

def getLocalStorage(params):
    return r'analyze_"{}"'.format(params['gamesvrentity'][0] + params['relaysvrentity'][0] + params['gameseq'][0]+params['playerid'][0])

def getKeyEventList(localStorage):
    command = r"return localStorage.getItem('{}')".format(localStorage)
    eventList = json.loads(driver.execute_script(command))
    return eventList['keyEventArr']

def getEventKillTime(eventList,eventTime):
    result = '-'
    for event in eventList:
        if (event['eventType'] == 'battle'):
            if(convertTime(event['battle']['startTime']) == eventTime):
                result = getMaxTimeInKillInfo(event['battle']['killInfo'])
            
    return result

def getMaxTimeInKillInfo(killInfos):
    maxTime = 0
    for killInfo in killInfos:
        if (maxTime < int(killInfo['time'])):
            maxTime = int(killInfo['time'])
    
    return convertTime(maxTime)

def getData(driver,url):
    blueScore = driver.execute_script('return document.getElementsByClassName(\"blue\")[0].innerHTML')
    redScore = driver.execute_script("return document.getElementsByClassName(\"red\")[0].innerHTML")
    
    f = 'function a(url) {var result = [];elements = document.getElementsByClassName(url);for(var i = 0;i < elements.length;i++) {result[i] = elements[i].innerText;}return result;}'
    
    script = f + 'return a(\"time_txt\")'
    eventTime = driver.execute_script(script)

    script = f + 'return a(\"txt\")'
    eventName = driver.execute_script(script)

    killTime = []
    urlParam = getUrlParm(driver,url)
    keyEventList = getKeyEventList(getLocalStorage(urlParam))
    for et in eventTime:
        killTime.append(getEventKillTime(keyEventList,et))

    return GameData(urlParam['gameseq'][0],blueScore,redScore,eventTime,eventName,killTime)

def createExcel(gamedataList):
    wb = Workbook()
    ws = wb.active
    writeTitle(ws)
    for i in range(0,len(gamedataList)): 
        writeGameData(ws,gamedataList[i])
    wb.save(filename  = 'result.xlsx')

def writeTitle(ws):
    ws.title = '战报分析'
    ws.cell(row = 1,column = 1,value = '战局')
    ws.cell(row = 1,column = 2,value = '战局比分(blue:red)')
    ws.cell(row = 1,column = 3,value = '事件时间')
    ws.cell(row = 1,column = 4,value = '事件名称')
    ws.cell(row = 1,column = 5,value = '死亡时间')

    ws.column_dimensions['A'].width = 17
    ws.column_dimensions['B'].width = 19.5
    ws.column_dimensions['D'].width = 17

def writeGameData(ws,gamedata):
    beginIndex = ws.max_row + 1
    for i in range(0,len(gamedata.eventTime)):
        ws.cell(row = beginIndex + i,column = 1,value = gamedata.gameNo)
        ws.cell(row = beginIndex + i,column = 2,value = gamedata.blueScore+':'+gamedata.redScore)
        ws.cell(row = beginIndex + i,column = 3,value = gamedata.eventTime[i])
        ws.cell(row = beginIndex + i,column = 4,value = gamedata.eventName[i])
        ws.cell(row = beginIndex + i,column = 5,value = gamedata.killTime[i])

def getURL(): 
    data = open('.\\source.txt','r',encoding='UTF-8').read()
    pat = r'(https://.*&playerid=[0-9a-zA-Z]{9})'
    return re.compile(pat).findall(data)


def convertTime(t):
    t = int(t) / 1000
    e = math.floor(t / 60)
    n = math.floor(t - 60 * e)
    r = ""

    r = "0" + str(e) if e < 10 else str(e)
    r += ":0" + str(n) if n < 10 else ":" + str(n)

    return r


if __name__ == "__main__":
    urlList = getURL()
    driver = webdriver.Chrome()
    driver.implicitly_wait(5)

    gameDataList = []
    for url in urlList: 
        driver.get(url)
        time.sleep(4)
        gameDataList.append(getData(driver,url))

    createExcel(gameDataList)
    driver.close()