前言
傳統(tǒng)的移動端爬蟲一般是基于webView,通過注入JS的方式配紫,獲取登錄后的cookie讓服務(wù)端使用無頭瀏覽器模擬登錄狀態(tài)爬取數(shù)據(jù)径密。
這種方式簡單有效,但是對于有做反爬(IP限制笨蚁,是否模擬器睹晒,是否處于異常環(huán)境)的網(wǎng)站趟庄,爬取難度大,甚至無法爬取伪很。
業(yè)務(wù)驅(qū)動技術(shù)戚啥,在移動端爬蟲的演進(jìn)過程中經(jīng)歷了四個階段
- cookie爬取(早期網(wǎng)站都沒有反爬的機(jī)制)
- cookie + 本地爬蕊笔浴(部分網(wǎng)站出現(xiàn)反爬猫十,無法通過服務(wù)端爬取,則本地獲取HTML解析)
- cookie + 本地爬取 + 截圖認(rèn)證(針對反爬嚴(yán)重呆盖,無法通過webView登錄采集的業(yè)務(wù)采用跳轉(zhuǎn)APP拖云,截圖OCR的方式爬取)
- Tensorflow + BroadCast Extension(通過錄屏獲取實(shí)時界面內(nèi)容应又,Tensorflow做圖像識別獲取關(guān)鍵頁面內(nèi)容宙项,無視反爬機(jī)制)
什么是BroadCast Upload Extension?
BroadCast Upload Extension在iOS10的時候推出,當(dāng)時只能in-APP BroadCast
株扛,即錄制當(dāng)前APP尤筐。
在WWDC2018中,蘋果發(fā)布的ReplayKit2中升級了這個擴(kuò)展洞就,做到了iOS System BroadCast
,即錄制iOS系統(tǒng)界面盆繁,不限制與某個APP,但此時需要從控制中心喚起錄屏旬蟋,任然有些麻煩油昂。
在iOS12中,iOS又推出了RPSystemBroadcastPickerView
類倾贰,可以在APP內(nèi)通過按鈕喚起控制中心的錄屏選擇界面冕碟。
客戶端流程:
架構(gòu):
- 錄屏插件:iOS原生錄屏插件,獲取最新的錄屏幀匆浙,傳遞給中間件鸣哀。
- 中間件:保存插件傳遞的最新一幀內(nèi)容,負(fù)責(zé)對幀對象的處理吞彤,消息的發(fā)送(心跳請求,幀請求)叹放。
- APP端:負(fù)責(zé)對收到的圖片對象進(jìn)行classify饰恕,根據(jù)結(jié)果進(jìn)行步驟匹配(是否需要服務(wù)端OCR,下一個關(guān)鍵頁面是什么)井仰。
? 插件端不停的將最新視頻幀給到中間件埋嵌,中間件在上一次post請求完畢后獲取最新的一幀轉(zhuǎn)換成圖片對象并發(fā)送,配置文件中設(shè)置字段控制上一次post請求完畢到下一次圖片轉(zhuǎn)換之間的dealy時間俱恶。
? 將幀處理成圖片后做一次圖片壓縮(TensorFlow對圖片進(jìn)行檢測前也會做一次壓縮雹嗦,這里提前做掉)范舀,保證post請求的大小和速度。
? 為了盡量低的內(nèi)存占用(錄屏插件最大可使用的內(nèi)存50MB了罪,超過就會崩)锭环,控制圖片轉(zhuǎn)換的頻率,壓縮圖片請求泊藕,將圖片轉(zhuǎn)換放到autoreleasepool中辅辩。
錄屏插件端
錄屏插件使用的是iOS系統(tǒng)自帶的BroadCast Upload Extension,可以在project - target + Application Extension
中添加娃圆。
添加后項(xiàng)目中會多一個BroadCast的target玫锋,自帶一個SampleHandler
類,用于接收系統(tǒng)錄屏插件的回調(diào)讼呢。
#import "SampleHandler.h"
@interface SampleHandler()
@end
@implementation SampleHandler
//開始錄屏
- (void)broadcastStartedWithSetupInfo:(NSDictionary<NSString *,NSObject *> *)setupInfo {
NSLog(@"APP錄屏開始");
}
//錄屏中切換APP iOS > 11.2
- (void)broadcastAnnotatedWithApplicationInfo:(NSDictionary *)applicationInfo {
NSLog(@"錄屏中切換APP");
}
//錄屏結(jié)束
- (void)broadcastFinished {
NSLog(@"APP錄屏結(jié)束");
}
//獲取錄屏幀信息
- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType {
switch (sampleBufferType) {
case RPSampleBufferTypeVideo://錄屏圖像信息回調(diào)
//在這里將獲取到的圖像信息傳遞給中間類處理
break;
case RPSampleBufferTypeAudioApp://錄屏音頻信息回調(diào)
// Handle audio sample buffer for app audio
break;
case RPSampleBufferTypeAudioMic://錄屏聲音輸入信息回調(diào)
// Handle audio sample buffer for mic audio
break;
default:
break;
}
}
@end
這一塊做的事非常少撩鹿,只是單純的將獲取到的視頻幀信息傳遞給中間類,刷新中間類保存的最后一幀信息悦屏。
中間件
中間件負(fù)責(zé)做的事情比較多节沦,狀態(tài)同步,圖片轉(zhuǎn)換還要考慮內(nèi)存占用問題窜管。
- CMSampleBufferRef轉(zhuǎn)UIImage對象
- 控制buffer轉(zhuǎn)image的頻率
- 通過HTTP請求的方式將圖片發(fā)送到主APP
- 發(fā)送心跳包告知APP插件存活
//
// MXSampleBufferManager.m
//
// buffer轉(zhuǎn)UIImage對象
// Created by joker on 2018/9/18.
// Copyright ? 2018 Scorpion. All rights reserved.
//
#import "MXSampleBufferManager.h"
#import <VideoToolbox/VideoToolbox.h>
#define clamp(a) (a>255?255:(a<0?0:a))
@implementation MXSampleBufferManager
+ (UIImage*)getImageWithSampleBuffer:(CMSampleBufferRef)sampleBuffer{
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
CVPixelBufferLockBaseAddress(imageBuffer,0);
size_t width = CVPixelBufferGetWidth(imageBuffer);
size_t height = CVPixelBufferGetHeight(imageBuffer);
uint8_t *yBuffer = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0);
size_t yPitch = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 0);
uint8_t *cbCrBuffer = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 1);
size_t cbCrPitch = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 1);
int bytesPerPixel = 4;
uint8_t *rgbBuffer = malloc(width * height * bytesPerPixel);
for(int y = 0; y < height; y++) {
uint8_t *rgbBufferLine = &rgbBuffer[y * width * bytesPerPixel];
uint8_t *yBufferLine = &yBuffer[y * yPitch];
uint8_t *cbCrBufferLine = &cbCrBuffer[(y >> 1) * cbCrPitch];
for(int x = 0; x < width; x++) {
int16_t y = yBufferLine[x];
int16_t cb = cbCrBufferLine[x & ~1] - 128;
int16_t cr = cbCrBufferLine[x | 1] - 128;
uint8_t *rgbOutput = &rgbBufferLine[x*bytesPerPixel];
int16_t r = (int16_t)roundf( y + cr * 1.4 );
int16_t g = (int16_t)roundf( y + cb * -0.343 + cr * -0.711 );
int16_t b = (int16_t)roundf( y + cb * 1.765);
rgbOutput[0] = 0xff;
rgbOutput[1] = clamp(b);
rgbOutput[2] = clamp(g);
rgbOutput[3] = clamp(r);
}
}
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(rgbBuffer, width, height, 8, width * bytesPerPixel, colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipLast);
CGImageRef quartzImage = CGBitmapContextCreateImage(context);
UIImage *image = [UIImage imageWithCGImage:quartzImage];
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
CGImageRelease(quartzImage);
free(rgbBuffer);
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
return image;
}
@end
主APP端
主APP端更多的是跟業(yè)務(wù)相關(guān)的操作
- 開啟HTTP Serve接收請求(GCDAsyncSocket)
- 獲取到圖片進(jìn)行TensorFlow識別散劫,輸出classify的結(jié)果。
- 根據(jù)識別結(jié)果和獲取的配置信息進(jìn)行match幕帆,目標(biāo)關(guān)鍵幀則上傳服務(wù)端OCR
- 錄屏插件存活檢測
模型怎么訓(xùn)練获搏?
參考鏈接:https://codelabs.developers.google.com/codelabs/tensorflow-for-poets/index.html#0
可以用官網(wǎng)提供的訓(xùn)練工程來簡單的訓(xùn)練模型。
demo工程中會帶一個訓(xùn)練過的微信的模型失乾。
使用效果
可以看到常熙,在模型訓(xùn)練好的情況下,實(shí)時錄屏的識別度是非常高的碱茁,配合服務(wù)端OCR可以獲取任何出現(xiàn)在屏幕上的內(nèi)容裸卫。