作者:張波
連結:http://blog.codingroad.com/python-get-and-analysis-pubg-mobile-data.html
已獲作者轉載授權
首先,神槍鎮樓
背景
最近老闆愛上了吃雞(手遊:全軍出擊),經常拉著我們開黑,只能放棄午休的時間,陪老闆在沙漠裡奔波。 上週在在微信遊戲頻道看戰績的時候突發奇想,是不是可以透過這個方式抓取到很多戰鬥資料,然後分析看看有什麼規律。
秀一波戰績,開黑情況下我們團隊吃雞率非常高,近100場吃雞次數51次
簡單評估了一下,覺得可行,咱就開始。
Step 1 分析資料介面
第一步當然是把這些戰績資料採集下來,首先我們需要瞭解頁面背後的故事。去看看頁面是如何獲取戰鬥資料的。
使用Charles抓包
抓包實現
在Mac下推薦使用工具Charles來從協議層抓取手機上的流量,原理就是在Mac上開啟一個代*理*伺服器,然後將手機的網路代*理設定為Mac,這樣手機上的所有流量都會經過我們的代*理*伺服器了。 大致流程如下:
https加密流量的處理
在實際操作的時候發現微信所有的流量都走了HTTPS,導致我們的抓到的都是加密資料,對我們沒有任何參考意義。 經過研究,可以透過在手機和電腦都安裝Charles根證書的方式來實現對Https流量的分析,具體操作可以參考:
-
charles mac下https抓包和iphone https抓包
-
解決Charles無法正常抓包iOS 11中的Https請求
安裝證書後,我們的流量大致是這樣子的
經過上述的配置,我們已經可以讀取到https的請求和響應資料了,如下圖所示。
windows下用findler可以實現相同的功能
其實這就是一個非常典型的中間人場景
資料介面
接下來就根據這些資料來找出我們需要的介面了,經過分析,主要涉及三個介面
-
獲取使用者資訊介面
-
獲取使用者戰績串列介面
-
獲取使用者指定戰績詳細資訊介面
下麵我們一個一個看
1. 獲取使用者資訊介面
-
request
API | /cgi-bin/gamewap/getpubgmdatacenterindex |
---|---|
方法 | GET |
引數 | openid、pass_ticket |
cookie | key pass_ticket、uin、pgv_pvid、sd_cookie_crttime、sd_userid |
-
response
{
“user_info”: {
“openid”: “oODfo0pjBQkcNuR4XLTQ321xFVws”,
“head_img_url”:“http://wx.qlogo.cn/mmhead/Q3auHgzwzM5hSWxxxxxUQPwW9ibxxxx9DlxLTsKWk97oWpDI0rg/96”,
“nick_name”: “望”,
“role_name”: “xxxx”,
“zone_area_id”: 0,
“plat_id”: 1
},
“battle_info”: {
“total_1”: 75,
“total_10”: 336,
“total_game”: 745,
“total_kill”: 1669
},
“battle_list”: [{
“map_id”: 1,
“room_id”: “6575389198189071197”,
“team_id”: 57,
“dt_event_time”: 1530953799,
“rank_in_ds”: 3,
“times_kill”: 1,
“label”: “前五”,
“team_type”: 1,
“award_gold”: 677,
“mode”: 0
}],
“appitem”: {
“AppID”: “wx13051697527efc45”,
“IconURL”:“https://mmocgame.qpic.cn/wechatgame/mEMdfrX5RU0dZFfNEdCsMJpfsof1HE0TP3cfZiboX0ZPxqh5aZnHjxPFXUGgsXmibe/0”,
“Name”: “絕地求生 全軍出擊”,
“BriefName”: “絕地求生 全軍出擊”,
“Desc”: “官方正版絕地求生手遊”,
“Brief”: “槍戰 | 808.2M”,
“WebURL”: “https://game.weixin.qq.com/cgi-bin/h5/static/detail_v2/index.html?wechat_pkgid=detail_v2&appid;=wx13051697527efc45&show;_bubble=0”,
“DownloadInfo”: {
“DownloadURL”: “https://itunes.apple.com/cn/app/id1304987143”,
“DownloadFlag”: 5
},
“Status”: 0,
“AppInfoFlag”: 45,
“Label”: [],
“AppStorePopUpDialogConfig”: {
“Duration”: 1500,
“Interval”: 172800,
“ServerTimestamp”: 1531066098
},
“HasEnabledChatGroup”: false,
“AppType”: 0,
“game_tag_list”: [“絕地求生”, “正版還原”, “好友開黑”, “百人對戰”, “超大地圖”],
“recommend_reason”: “正版絕地求生,荒野射擊”,
“size_desc”: “808.2M”
},
“is_guest”: true,
“is_blocked”: false,
“errcode”: 0,
“errmsg”: “ok”
}
-
分析
openid是使用者的惟一標識。
2. 獲取使用者戰績串列介面
-
request
API /cgi-bin/gamewap/getpubgmbattlelist 方法 GET 引數 openid、pass_ticket、plat_id、after_time、limit cookie key pass_ticket、uin、pgv_pvid、sd_cookie_crttime、sd_userid -
response
{
“errcode”: 0,
“errmsg”: “ok”,
“next_after_time”: 1528120556,
“battle_list”: [{
“map_id”: 1,
“room_id”: “6575389198111172597”,
“team_id”: 57,
“dt_event_time”: 1530953799,
“rank_in_ds”: 3,
“times_kill”: 1,
“label”: “前五”,
“team_type”: 1,
“award_gold”: 677,
“mode”: 0
}, {
“map_id”: 1,
“room_id”: “6575336498940384115”,
“team_id”: 11,
“dt_event_time”: 1530941404,
“rank_in_ds”: 5,
“times_kill”: 2,
“label”: “前五”,
“team_type”: 1,
“award_gold”: 632,
“mode”: 0
}],
“has_next”: true
}
-
分析
-
這個介面用after_time來進行分頁,遍歷獲取時可以根據介面響應的has_next和next_after_time來判斷是否還有下一頁的資料。
-
串列裡面的room_id是每一場battle的惟一標識。
3. 獲取使用者戰績詳情介面
-
request
API | /cgi-bin/gamewap/getpubgmbattledetail |
---|---|
方法 | GET |
引數 | openid、pass_ticket、room_id |
cookie | key pass_ticket、uin、pgv_pvid、sd_cookie_crttime、sd_userid |
-
request
{
“errcode”: 0,
“errmsg”: “ok”,
“base_info”: {
“nick_name”: “柚茶”,
“head_img_url”: “http://wx.qlogo.cn/mmhead/xxxx/96”,
“dt_event_time”: 1528648165,
“team_type”: 4,
“rank”: 1,
“player_count”: 100,
“role_sex”: 1,
“label”: “大吉大利”,
“openid”: “oODfo0s1w5lWjmxxxxxgQkcCljXQ”
},
“battle_info”: {
“award_gold”: 622,
“times_kill”: 6,
“times_head_shot”: 0,
“damage”: 537,
“times_assist”: 3,
“survival_duration”: 1629,
“times_save”: 0,
“times_reborn”: 0,
“vehicle_kill”: 1,
“forward_distance”: 10140,
“driving_distance”: 5934,
“dead_poison_circle_no”: 6,
“top_kill_distance”: 223,
“top_kill_distance_weapon_use”: 2924130819,
“be_kill_user”: {
“nick_name”: “小旭”,
“head_img_url”:“http://wx.qlogo.cn/mmhead/ibLButGMnqJNFsUtStNEV8tzlH1QpwPiaF9kxxxxx66G3ibjic6Ng2Rcg/96”,
“weapon_use”: 20101000001,
“openid”: “oODfo0qrPLExxxxc0QKjFPnPxyI”
},
“label”: “大吉大利”
},
“team_info”: {
“user_list”: [{
“nick_name”: “ooo”,
“times_kill”: 6,
“assist_count”: 3,
“survival_duration”: 1638,
“award_gold”: 632,
“head_img_url”:“http://wx.qlogo.cn/mmhead/Q3auHgzwzM4k4RXdyxavNxxxxUjcX6Tl47MNNV1dZDliazRKRg”,
“openid”: “oODfo0xxxxf1bRAXE-q-lEezK0k”
}, {
“nick_name”: “我吃炒肉”,
“times_kill”: 2,
“assist_count”: 2,
“survival_duration”: 1502,
“award_gold”: 583,
“head_img_url”:“http://wx.qlogo.cn/mmhead/sTJptKvBQLKd5SAAjOF0VrwiapUxxxxFffxoDUcrVjYbDf9pNENQ”,
“openid”: “oODfo0gIyDxxxxZpUrSrpapZSDT0”
}]
},
“is_guest”: true,
“is_blocked”: false
}
分析
-
這個介面響應了戰鬥的詳細資訊,包括殺*敵數、爆*頭數、救人數、跑動距離等等,足夠我們分析了。
-
這個介面還響應了是被誰殺死的以及組團成員的openid,利用這個特性我們這可無限深度的發散爬取更多使用者的資料。
至於cookie中的息pass_ticket等資訊肯定是用於許可權認證的,在上述的幾次請求中這些資訊都沒有變化,所以我們不需要深研其是怎麼算出來的,只需要抓包提取到預設資訊後填到程式碼裡面就可以用了。
Step 2 爬取資料
介面已經確定下來了,接下來就是去抓取足夠量的資料了。
使用requests請求介面獲取資料
url = ‘https://game.weixin.qq.com/cgi-bin/gamewap/getpubgmdatacenterindex?openid=%s&plat;_id=0&uin;=&key;=&pass;_ticket=%s’ % (openid, settings.pass_ticket)
r = requests.get(url=url, cookies=settings.def_cookies, essay-headers=settings.def_essay-headers, timeout=(5.0,5.0))
tmp = r.json()
wfile = os.path.join(settings.Res_UserInfo_Dir, ‘%s.txt’ % (rediskeys.user(openid)))
with codecs.open(wfile, ‘w’, ‘utf-8’) as wf:
wf.write(simplejson.dumps(tmp, indent=2, sort_keys=True, ensure_ascii=False))
參照這種方式我們可以很快把另外兩個介面寫好。
使用redis來標記已經爬取過的資訊
在上述介面中我們可能從使用者A的入口進去找到使用者B的openid,然後從使用者B的入口進去又找到使用者A的openid,為了避免重覆採集,所以我們需要記錄下哪些資訊是我們採集過的。 核心程式碼片斷:
# rediskeys.user_battle_list 根據openid獲取存在redis中的key值
def user_battle_list(openid):
return ‘ubl_%s’ % (openid)
# 在提取battle list之前,首先判斷這用使用者的資料是否已經提取過了
if settings.DataRedis.get(rediskeys.user_battle_list(openid)):
return True
# 在提取battle list之後,需要在redis中記錄使用者資訊
settings.DataRedis.set(rediskeys.user_battle_list(openid), 1)
使用celery來管理佇列
celery是一個非常好用的分散式佇列管理工具,我這次只打算在我自己的電腦上執行,所以並沒有用到分散式的功能。 我們建立三個task和三個queue
task_queues = (
Queue(‘queue_get_battle_info’, exchange=Exchange(‘priority’, type=‘direct’), routing_key=‘gbi’),
Queue(‘queue_get_battle_list’, exchange=Exchange(‘priority’, type=‘direct’), routing_key=‘gbl’),
Queue(‘queue_get_user_info’, exchange=Exchange(‘priority’, type=‘direct’), routing_key=‘gui’),
)
task_routes = ([
(‘get_battle_info’, {‘queue’: ‘queue_get_battle_info’}),
(‘get_battle_list’, {‘queue’: ‘queue_get_battle_list’}),
(‘get_user_info’, {‘queue’: ‘queue_get_user_info’}),
],)
然後在task中控制API請求和Redis資料實現完整的任務邏輯,如:
@app.task(name=‘get_battle_list’)
def get_battle_list(openid, plat_id=None, after_time=0, update_time=None):
# 判斷是否已經取過使用者戰績串列資訊
if settings.DataRedis.get(rediskeys.user_battle_list(openid)):
return True
if not plat_id:
try:
# 提取使用者資訊
us = handles.get_user_info_handles(openid)
plat_id=us[‘plat_id’]
except Exception as e:
print ‘can not get user plat_id’, openid, traceback.format_exc()
return False
# 提取戰績串列
battle_list = handles.get_battle_list_handle(openid, plat_id, after_time=0, update_time=None)
# 為每一場戰鬥建立非同步獲取詳情任務
for room_id in battle_list:
if not settings.DataRedis.get(rediskeys.user_battle(openid, room_id)):
get_battle_info.delay(openid, plat_id, room_id)
return True
開始抓取
因為我們是發散是爬蟲,所以需要給程式碼一個使用者的入口,所以需要手動建立一個使用者的採集任務
from tasks.all import get_battle_list
my_openid = ‘oODfo0oIErZI2xxx9xPlVyQbRPgY’
my_platid = ‘0’
get_battle_list.delay(my_openid, my_platid, after_time=0, update_time=None)
有入口之後我們就用celery來啟動worker去開始爬蟲
# 啟動獲取使用者詳情worker
celery –A tasks.all worker –c 5 —queue=queue_get_user_info —loglevel=info –n get_user_info@%h
# 啟動獲取戰績串列worker
celery –A tasks.all worker –c 5 —queue=queue_get_battle_list —loglevel=info –n get_battle_list@%h
# 啟動獲取戰績詳情worker
celery –A tasks.all worker –c 30 —queue=queue_get_battle_info —loglevel=info –n get_battle_info@%h
這樣我們的爬蟲就可以愉快的跑起來了。再透過celery-flower來檢視執行情況。
celery flower –A tasks.all —broker=redis://:$REDIS_PASS@$REDIS_HOST:$REDIS_PORT/10
透過flower,我們可以看到執行的效率還是非常不錯的。在執行過程中會發現get_battle_list跑太快,導致get_battle_info即使開了30個併發都還會積壓很多,所以需要適時的去停一下這些worker。 在我們抓到20萬條資訊之後就可以停下來了。
Step 3 資料分析
1. 平均使用者日線上時長2小時
從分佈圖上看大部分使用者都在1小時以上,最猛的幾個人超過8小時。
註:我這裡統計的是每一局的存活時間,實際線上時長會比我這個更長。
2. 女性角色被救次數高於男性
終於知道為什麼有那麼多人妖了,原來在遊戲裡面可以佔便宜啊。
3. 女性角色救人次數高於男性
給了大家一個帶妹上分的好理由。
4. 週五大家最忙
估計週五大家都要忙著交差和寫周報了。
5. 晚上22點是遊戲高峰
凌晨還有那麼多人玩,你們不睡覺嗎?
6. 最遠擊*殺距離639米
我看了一下98K、SKS和AWP的有效射程,大致都在800米以內,所以這個值可信度還是可以的。 反過來看抖音上的那些超遠距離擊*殺應該都是擺拍的。
7. 能拿到「救死扶傷」稱號才是最高榮耀
從分佈情況可以看出來,救死扶傷比十殺還要難。能拿到救死扶傷稱號的大部分都是女性角色,再一次證明玩遊戲要帶妹。 回歸到這個遊戲的本質,那就是生存遊戲,沒什麼比活下來更重要的了。
結尾
這次爬蟲主要是利用了微信遊戲頻道可以檢視陌生人資料的場景才能提取到這麼多資料。我們可以透過同樣的手段來分析王者榮耀和其它遊戲的資料,有興趣的同學可以嘗試一下。 最後再說一下,UMP9是把好槍,配2倍鏡非常爽。
作者:張波:先後從事過測試、運維、開發和需求分析工作,主要開發語言是 Python,現任一家小型公司技術團隊負責人。
●編號477,輸入編號直達本文
●輸入m獲取文章目錄
演演算法與資料結構
更多推薦《18個技術類微信公眾號》
涵蓋:程式人生、演演算法與資料結構、駭客技術與網路安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。