mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-10 17:43:00 +08:00
refactor: remove video crawler
This commit is contained in:
@@ -335,92 +335,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "【点播】滑动消费",
|
||||
"android": {
|
||||
"actions": [
|
||||
{
|
||||
"method": "video_crawler",
|
||||
"params": {
|
||||
"feed": {
|
||||
"sleep_random": [
|
||||
0,
|
||||
5,
|
||||
0.6,
|
||||
5,
|
||||
15,
|
||||
0.2,
|
||||
15,
|
||||
50,
|
||||
0.2
|
||||
],
|
||||
"target_count": 10,
|
||||
"target_labels": [
|
||||
{
|
||||
"regex": true,
|
||||
"scope": [
|
||||
0,
|
||||
0.5,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"text": "^广告$"
|
||||
},
|
||||
{
|
||||
"regex": true,
|
||||
"scope": [
|
||||
0,
|
||||
0.5,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"text": "^图文$"
|
||||
},
|
||||
{
|
||||
"regex": true,
|
||||
"scope": [
|
||||
0,
|
||||
0.5,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"text": "^特效\\|"
|
||||
},
|
||||
{
|
||||
"regex": true,
|
||||
"scope": [
|
||||
0,
|
||||
0.5,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"text": "^模板\\|"
|
||||
},
|
||||
{
|
||||
"regex": true,
|
||||
"scope": [
|
||||
0,
|
||||
0.5,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"text": "^购物\\|"
|
||||
}
|
||||
]
|
||||
},
|
||||
"live": {
|
||||
"sleep_random": [
|
||||
20,
|
||||
20
|
||||
],
|
||||
"target_count": 0
|
||||
},
|
||||
"timeout": 600
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "返回主界面,并打开本地时间戳",
|
||||
"android": {
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"name": "抓取抖音视频信息",
|
||||
"variables": {
|
||||
"device": "${ENV(SerialNumber)}"
|
||||
},
|
||||
"android": [
|
||||
{
|
||||
"serial": "$device"
|
||||
}
|
||||
]
|
||||
},
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "启动 app",
|
||||
"android": {
|
||||
"actions": [
|
||||
{
|
||||
"method": "app_launch",
|
||||
"params": "com.ss.android.ugc.aweme"
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "ui_foreground_app",
|
||||
"assert": "equal",
|
||||
"expect": "com.ss.android.ugc.aweme",
|
||||
"msg": "app [com.ss.android.ugc.aweme] should be in foreground"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "滑动消费 feed 至少 10 个,live 至少 3 个;滑动过程中,70% 随机间隔 0-5s,30% 随机间隔 5-10s",
|
||||
"android": {
|
||||
"actions": [
|
||||
{
|
||||
"method": "video_crawler",
|
||||
"params": {
|
||||
"feed": {
|
||||
"sleep_random": [
|
||||
0,
|
||||
5,
|
||||
0.7,
|
||||
5,
|
||||
10,
|
||||
0.3
|
||||
],
|
||||
"target_count": 5,
|
||||
"target_labels": [
|
||||
{
|
||||
"regex": true,
|
||||
"scope": [
|
||||
0,
|
||||
0.5,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"target": 1,
|
||||
"text": "^广告$"
|
||||
},
|
||||
{
|
||||
"regex": true,
|
||||
"scope": [
|
||||
0,
|
||||
0.5,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"target": 1,
|
||||
"text": "^图文$"
|
||||
},
|
||||
{
|
||||
"regex": true,
|
||||
"scope": [
|
||||
0,
|
||||
0.5,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"text": "^特效\\|"
|
||||
},
|
||||
{
|
||||
"regex": true,
|
||||
"scope": [
|
||||
0,
|
||||
0.5,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"text": "^模板\\|"
|
||||
},
|
||||
{
|
||||
"regex": true,
|
||||
"scope": [
|
||||
0,
|
||||
0.5,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"text": "^购物\\|"
|
||||
}
|
||||
]
|
||||
},
|
||||
"live": {
|
||||
"sleep_random": [
|
||||
15,
|
||||
20
|
||||
],
|
||||
"target_count": 3
|
||||
},
|
||||
"timeout": 600
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "exit",
|
||||
"android": {
|
||||
"actions": [
|
||||
{
|
||||
"method": "app_terminate",
|
||||
"params": "com.ss.android.ugc.aweme"
|
||||
}
|
||||
]
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "ui_foreground_app",
|
||||
"assert": "not_equal",
|
||||
"expect": "com.ss.android.ugc.aweme",
|
||||
"msg": "app [com.ss.android.ugc.aweme] should not be in foreground"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
//go:build localtest
|
||||
|
||||
package uitest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp"
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
|
||||
)
|
||||
|
||||
func TestAndroidVideoCrawlerTest(t *testing.T) {
|
||||
testCase := &hrp.TestCase{
|
||||
Config: hrp.NewConfig("抓取抖音视频信息").
|
||||
WithVariables(map[string]interface{}{
|
||||
"device": "${ENV(SerialNumber)}",
|
||||
}).
|
||||
SetAndroid(uixt.WithSerialNumber("$device")),
|
||||
TestSteps: []hrp.IStep{
|
||||
hrp.NewStep("启动 app").
|
||||
Android().
|
||||
ScreenShot(uixt.WithScreenShotOCR(true), uixt.WithScreenShotUpload(true)).
|
||||
AppLaunch("com.ss.android.ugc.aweme").
|
||||
Sleep(5).
|
||||
Validate().
|
||||
AssertAppInForeground("com.ss.android.ugc.aweme"),
|
||||
hrp.NewStep("滑动消费 feed 至少 10 个,live 至少 3 个;滑动过程中,70% 随机间隔 0-5s,30% 随机间隔 5-10s").
|
||||
Android().
|
||||
VideoCrawler(map[string]interface{}{
|
||||
"timeout": 600,
|
||||
"feed": map[string]interface{}{
|
||||
"target_count": 5,
|
||||
"target_labels": []map[string]interface{}{
|
||||
{"text": "^广告$", "scope": []float64{0, 0.5, 1, 1}, "regex": true, "target": 1},
|
||||
{"text": "^图文$", "scope": []float64{0, 0.5, 1, 1}, "regex": true, "target": 1},
|
||||
{"text": `^特效\|`, "scope": []float64{0, 0.5, 1, 1}, "regex": true},
|
||||
{"text": `^模板\|`, "scope": []float64{0, 0.5, 1, 1}, "regex": true},
|
||||
{"text": `^购物\|`, "scope": []float64{0, 0.5, 1, 1}, "regex": true},
|
||||
},
|
||||
"sleep_random": []float64{0, 5, 0.7, 5, 10, 0.3},
|
||||
},
|
||||
"live": map[string]interface{}{
|
||||
"target_count": 3,
|
||||
"sleep_random": []float64{15, 20},
|
||||
},
|
||||
}),
|
||||
hrp.NewStep("exit").
|
||||
Android().
|
||||
AppTerminate("com.ss.android.ugc.aweme").
|
||||
Validate().
|
||||
AssertAppNotInForeground("com.ss.android.ugc.aweme"),
|
||||
},
|
||||
}
|
||||
|
||||
if err := testCase.Dump2JSON("demo_android_video_crawler.json"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err := hrp.Run(t, testCase)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -121,27 +121,6 @@ func TestAndroidExpertTest(t *testing.T) {
|
||||
).
|
||||
Validate().
|
||||
AssertOCRExists("推荐", "进入抖音失败"),
|
||||
// 点播赛道
|
||||
hrp.NewStep("【点播】滑动消费").
|
||||
Android().
|
||||
VideoCrawler(map[string]interface{}{
|
||||
"timeout": 600,
|
||||
"feed": map[string]interface{}{
|
||||
"target_count": 10,
|
||||
"target_labels": []map[string]interface{}{
|
||||
{"text": "^广告$", "scope": []float64{0, 0.5, 1, 1}, "regex": true},
|
||||
{"text": "^图文$", "scope": []float64{0, 0.5, 1, 1}, "regex": true},
|
||||
{"text": `^特效\|`, "scope": []float64{0, 0.5, 1, 1}, "regex": true},
|
||||
{"text": `^模板\|`, "scope": []float64{0, 0.5, 1, 1}, "regex": true},
|
||||
{"text": `^购物\|`, "scope": []float64{0, 0.5, 1, 1}, "regex": true},
|
||||
},
|
||||
"sleep_random": []float64{0, 5, 0.6, 5, 15, 0.2, 15, 50, 0.2},
|
||||
},
|
||||
"live": map[string]interface{}{
|
||||
"target_count": 0,
|
||||
"sleep_random": []float64{20, 20},
|
||||
},
|
||||
}),
|
||||
// localtime 时间戳界面
|
||||
hrp.NewStep("返回主界面,并打开本地时间戳").
|
||||
Android().
|
||||
@@ -275,27 +254,6 @@ func TestIOSExpertTest(t *testing.T) {
|
||||
).
|
||||
Validate().
|
||||
AssertOCRExists("推荐", "进入抖音失败"),
|
||||
// 点播赛道
|
||||
hrp.NewStep("【点播】滑动消费").
|
||||
IOS().
|
||||
VideoCrawler(map[string]interface{}{
|
||||
"timeout": 600,
|
||||
"feed": map[string]interface{}{
|
||||
"target_count": 10,
|
||||
"target_labels": []map[string]interface{}{
|
||||
{"text": "^广告$", "scope": []float64{0, 0.5, 1, 1}, "regex": true},
|
||||
{"text": "^图文$", "scope": []float64{0, 0.5, 1, 1}, "regex": true},
|
||||
{"text": `^特效\|`, "scope": []float64{0, 0.5, 1, 1}, "regex": true},
|
||||
{"text": `^模板\|`, "scope": []float64{0, 0.5, 1, 1}, "regex": true},
|
||||
{"text": `^购物\|`, "scope": []float64{0, 0.5, 1, 1}, "regex": true},
|
||||
},
|
||||
"sleep_random": []float64{0, 5, 0.6, 5, 15, 0.2, 15, 50, 0.2},
|
||||
},
|
||||
"live": map[string]interface{}{
|
||||
"target_count": 0,
|
||||
"sleep_random": []float64{20, 20},
|
||||
},
|
||||
}),
|
||||
// localtime 时间戳界面
|
||||
hrp.NewStep("返回主界面,并打开本地时间戳").
|
||||
IOS().
|
||||
|
||||
@@ -320,92 +320,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "【点播】滑动消费",
|
||||
"ios": {
|
||||
"actions": [
|
||||
{
|
||||
"method": "video_crawler",
|
||||
"params": {
|
||||
"feed": {
|
||||
"sleep_random": [
|
||||
0,
|
||||
5,
|
||||
0.6,
|
||||
5,
|
||||
15,
|
||||
0.2,
|
||||
15,
|
||||
50,
|
||||
0.2
|
||||
],
|
||||
"target_count": 10,
|
||||
"target_labels": [
|
||||
{
|
||||
"regex": true,
|
||||
"scope": [
|
||||
0,
|
||||
0.5,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"text": "^广告$"
|
||||
},
|
||||
{
|
||||
"regex": true,
|
||||
"scope": [
|
||||
0,
|
||||
0.5,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"text": "^图文$"
|
||||
},
|
||||
{
|
||||
"regex": true,
|
||||
"scope": [
|
||||
0,
|
||||
0.5,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"text": "^特效\\|"
|
||||
},
|
||||
{
|
||||
"regex": true,
|
||||
"scope": [
|
||||
0,
|
||||
0.5,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"text": "^模板\\|"
|
||||
},
|
||||
{
|
||||
"regex": true,
|
||||
"scope": [
|
||||
0,
|
||||
0.5,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"text": "^购物\\|"
|
||||
}
|
||||
]
|
||||
},
|
||||
"live": {
|
||||
"sleep_random": [
|
||||
20,
|
||||
20
|
||||
],
|
||||
"target_count": 0
|
||||
},
|
||||
"timeout": 600
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "返回主界面,并打开本地时间戳",
|
||||
"ios": {
|
||||
|
||||
@@ -62,7 +62,6 @@ const (
|
||||
ACTION_SwipeToTapApp ActionMethod = "swipe_to_tap_app" // swipe left & right to find app and tap
|
||||
ACTION_SwipeToTapText ActionMethod = "swipe_to_tap_text" // swipe up & down to find text and tap
|
||||
ACTION_SwipeToTapTexts ActionMethod = "swipe_to_tap_texts" // swipe up & down to find text and tap
|
||||
ACTION_VideoCrawler ActionMethod = "video_crawler"
|
||||
ACTION_ClosePopups ActionMethod = "close_popups"
|
||||
ACTION_EndToEndDelay ActionMethod = "live_e2e"
|
||||
ACTION_InstallApp ActionMethod = "install_app"
|
||||
@@ -740,17 +739,6 @@ func (dExt *DriverExt) DoAction(action MobileAction) (err error) {
|
||||
return dExt.Driver.StartCamera()
|
||||
case ACTION_StopCamera:
|
||||
return dExt.Driver.StopCamera()
|
||||
case ACTION_VideoCrawler:
|
||||
params, ok := action.Params.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid video crawler params: %v(%T)", action.Params, action.Params)
|
||||
}
|
||||
data, _ := json.Marshal(params)
|
||||
configs := &VideoCrawlerConfigs{}
|
||||
if err := json.Unmarshal(data, configs); err != nil {
|
||||
return errors.Wrapf(err, "invalid video crawler params: %v(%T)", action.Params, action.Params)
|
||||
}
|
||||
return dExt.VideoCrawler(configs)
|
||||
case ACTION_ClosePopups:
|
||||
return dExt.ClosePopupsHandler()
|
||||
case ACTION_EndToEndDelay:
|
||||
|
||||
@@ -59,7 +59,6 @@ type ScreenResult struct {
|
||||
Texts OCRTexts `json:"texts"` // dumped raw OCRTexts
|
||||
Icons UIResultMap `json:"icons"` // CV 识别的图标
|
||||
Tags []string `json:"tags"` // tags for image, e.g. ["feed", "ad", "live"]
|
||||
Video *Video `json:"video,omitempty"`
|
||||
Popup *PopupInfo `json:"popup,omitempty"`
|
||||
|
||||
SwipeStartTime int64 `json:"swipe_start_time"` // 滑动开始时间戳
|
||||
@@ -94,15 +93,13 @@ type cacheStepData struct {
|
||||
screenShots []string
|
||||
// cache step screenshot ocr results, key is image path, value is ScreenResult
|
||||
screenResults ScreenResultMap
|
||||
// cache feed/live video stat
|
||||
videoCrawler *VideoCrawler
|
||||
e2eDelay []timeLog
|
||||
// cache e2e delay
|
||||
e2eDelay []timeLog
|
||||
}
|
||||
|
||||
func (d *cacheStepData) reset() {
|
||||
d.screenShots = make([]string, 0)
|
||||
d.screenResults = make(map[string]*ScreenResult)
|
||||
d.videoCrawler = nil
|
||||
d.e2eDelay = nil
|
||||
}
|
||||
|
||||
@@ -331,7 +328,6 @@ func (dExt *DriverExt) saveScreenShot(raw *bytes.Buffer, fileName string) (strin
|
||||
|
||||
func (dExt *DriverExt) GetStepCacheData() map[string]interface{} {
|
||||
cacheData := make(map[string]interface{})
|
||||
cacheData["video_stat"] = dExt.cacheStepData.videoCrawler
|
||||
cacheData["screenshots"] = dExt.cacheStepData.screenShots
|
||||
|
||||
cacheData["screenshots_urls"] = dExt.cacheStepData.screenResults.getScreenShotUrls()
|
||||
|
||||
@@ -418,7 +418,6 @@ func (dExt *DriverExt) GetScreenResult(options ...ActionOption) (screenResult *S
|
||||
screenResult.Texts = imageResult.OCRResult.ToOCRTexts()
|
||||
screenResult.UploadedURL = imageResult.URL
|
||||
screenResult.Icons = imageResult.UIResult
|
||||
screenResult.Video = &Video{LiveType: imageResult.LiveType, ViewCount: imageResult.LivePopularity}
|
||||
|
||||
if actionOptions.ScreenShotWithClosePopups && imageResult.ClosePopupsResult != nil {
|
||||
screenResult.Popup = &PopupInfo{
|
||||
|
||||
@@ -1,467 +0,0 @@
|
||||
package uixt
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/code"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/json"
|
||||
)
|
||||
|
||||
type TargetLabel struct {
|
||||
Text string `json:"text"`
|
||||
Scope Scope `json:"scope"`
|
||||
Regex bool `json:"regex"`
|
||||
Target int `json:"target"` // target count for current label
|
||||
}
|
||||
|
||||
type FeedConfig struct {
|
||||
TargetCount int `json:"target_count"`
|
||||
TargetLabels []TargetLabel `json:"target_labels"`
|
||||
SleepRandom []interface{} `json:"sleep_random"`
|
||||
}
|
||||
|
||||
type LiveConfig struct {
|
||||
TargetCount int `json:"target_count"`
|
||||
TargetLabels []TargetLabel `json:"target_labels"`
|
||||
SleepRandom []interface{} `json:"sleep_random"`
|
||||
}
|
||||
|
||||
type VideoCrawlerConfigs struct {
|
||||
Timeout int `json:"timeout"` // seconds
|
||||
|
||||
Feed FeedConfig `json:"feed"`
|
||||
Live LiveConfig `json:"live"`
|
||||
}
|
||||
|
||||
type VideoCrawler struct {
|
||||
driverExt *DriverExt
|
||||
configs *VideoCrawlerConfigs
|
||||
timer *time.Timer
|
||||
|
||||
// used to help checking if swipe success
|
||||
failedCount int64
|
||||
|
||||
FeedCount int `json:"feed_count"`
|
||||
FeedStat map[string]int `json:"feed_stat"` // 分类统计 feed 数量:视频/图文/广告/特效/模板/购物
|
||||
LiveCount int `json:"live_count"`
|
||||
LiveStat map[string]int `json:"live_stat"` // 分类统计 live 数量:秀场/游戏/电商/多人
|
||||
}
|
||||
|
||||
func (vc *VideoCrawler) isFeedTargetAchieved() bool {
|
||||
targetStat := make(map[string]int)
|
||||
for _, targetLabel := range vc.configs.Feed.TargetLabels {
|
||||
targetStat[targetLabel.Text] = targetLabel.Target
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Int("current_total", vc.FeedCount).
|
||||
Interface("current_stat", vc.FeedStat).
|
||||
Int("target_total", vc.configs.Feed.TargetCount).
|
||||
Interface("target_stat", targetStat).
|
||||
Msg("display feed crawler progress")
|
||||
|
||||
// check total feed count
|
||||
if vc.FeedCount < vc.configs.Feed.TargetCount {
|
||||
return false
|
||||
}
|
||||
|
||||
// check each feed type's count
|
||||
for _, targetLabel := range vc.configs.Feed.TargetLabels {
|
||||
if vc.FeedStat[targetLabel.Text] < targetLabel.Target {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (vc *VideoCrawler) isLiveTargetAchieved() bool {
|
||||
targetStat := make(map[string]int)
|
||||
for _, targetLabel := range vc.configs.Live.TargetLabels {
|
||||
targetStat[targetLabel.Text] = targetLabel.Target
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Int("current_total", vc.LiveCount).
|
||||
Interface("current_stat", vc.LiveStat).
|
||||
Int("target_total", vc.configs.Live.TargetCount).
|
||||
Interface("target_stat", targetStat).
|
||||
Msg("display live crawler progress")
|
||||
|
||||
// check total live count
|
||||
if vc.LiveCount < vc.configs.Live.TargetCount {
|
||||
return false
|
||||
}
|
||||
|
||||
// check each live type's count
|
||||
for _, targetLabel := range vc.configs.Live.TargetLabels {
|
||||
if vc.LiveStat[targetLabel.Text] < targetLabel.Target {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (vc *VideoCrawler) isTargetAchieved() bool {
|
||||
return vc.isFeedTargetAchieved() && vc.isLiveTargetAchieved()
|
||||
}
|
||||
|
||||
func (vc *VideoCrawler) exitLiveRoom() error {
|
||||
log.Info().Msg("press back to exit live room")
|
||||
err := vc.driverExt.Driver.PressBack()
|
||||
time.Sleep(time.Duration(3) * time.Second)
|
||||
if vc.driverExt.TapByOCR("退出直播间") == nil {
|
||||
log.Info().Msg("clicked the button to exit the live room successfully")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
const (
|
||||
FOUND_FEED_SUCCESS = "found feed success"
|
||||
FOUND_LIVE_SUCCESS = "found live success"
|
||||
)
|
||||
|
||||
func (dExt *DriverExt) VideoCrawler(configs *VideoCrawlerConfigs) (err error) {
|
||||
if dExt.plugin == nil {
|
||||
return errors.New("miss plugin for video crawler")
|
||||
}
|
||||
|
||||
// set default sleep random strategy if not set
|
||||
if configs.Feed.SleepRandom == nil {
|
||||
configs.Feed.SleepRandom = []interface{}{1, 5}
|
||||
}
|
||||
if configs.Live.SleepRandom == nil {
|
||||
configs.Live.SleepRandom = []interface{}{10, 15}
|
||||
}
|
||||
|
||||
crawler := &VideoCrawler{
|
||||
driverExt: dExt,
|
||||
configs: configs,
|
||||
|
||||
failedCount: 0,
|
||||
FeedCount: 0,
|
||||
FeedStat: make(map[string]int),
|
||||
LiveCount: 0,
|
||||
LiveStat: make(map[string]int),
|
||||
}
|
||||
defer func() {
|
||||
dExt.cacheStepData.videoCrawler = crawler
|
||||
}()
|
||||
|
||||
// flag,仅当 flag 为 false 时,并处于内流时,才执行退出直播间逻辑
|
||||
isFeed := true
|
||||
|
||||
// loop until target count achieved or timeout
|
||||
// the main loop is feed crawler
|
||||
crawler.timer = time.NewTimer(time.Duration(configs.Timeout) * time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-crawler.timer.C:
|
||||
log.Warn().Msg("timeout in feed crawler")
|
||||
return errors.Wrap(code.TimeoutError, "feed crawler timeout")
|
||||
case <-dExt.interruptSignal:
|
||||
log.Warn().Msg("interrupted in feed crawler")
|
||||
return errors.Wrap(code.InterruptError, "feed crawler interrupted")
|
||||
default:
|
||||
if err = crawler.clearCurrentVideo(); err != nil {
|
||||
log.Error().Err(err).Msg("clear cache failed")
|
||||
}
|
||||
|
||||
// swipe to next feed video
|
||||
log.Info().Msg("swipe to next feed video")
|
||||
swipeStartTime := time.Now()
|
||||
if err = dExt.SwipeUpUtil(crawler.failedCount, WithOffsetRandomRange(-10, 10)); err != nil {
|
||||
log.Error().Err(err).Msg("feed swipe up failed")
|
||||
return err
|
||||
}
|
||||
swipeFinishTime := time.Now()
|
||||
|
||||
// get app event trackings
|
||||
// retry 3 times if get feed failed, abort if fail 3 consecutive times
|
||||
fetchVideoStartTime := time.Now()
|
||||
currentVideo, err := crawler.getCurrentVideo()
|
||||
if err != nil || currentVideo.Type == "" {
|
||||
crawler.failedCount++
|
||||
if crawler.failedCount >= 3 {
|
||||
// failed 3 consecutive times
|
||||
return errors.Wrap(code.TrackingGetError,
|
||||
"get current feed video failed 3 consecutive times")
|
||||
}
|
||||
log.Warn().
|
||||
Int64("failedCount", crawler.failedCount).
|
||||
Msg("get current feed video failed")
|
||||
|
||||
// check and handle popups
|
||||
if err := crawler.driverExt.ClosePopupsHandler(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// retry
|
||||
continue
|
||||
}
|
||||
fetchVideoFinishTime := time.Now()
|
||||
|
||||
// 直播预览流线上概率
|
||||
livePreviewProb := crawler.getLivePreviewProb()
|
||||
|
||||
switch currentVideo.Type {
|
||||
case VideoType_PreviewLive:
|
||||
isFeed = true
|
||||
// 直播预览流
|
||||
var skipEnterLive bool
|
||||
if crawler.isLiveTargetAchieved() {
|
||||
log.Info().Interface("video", currentVideo).
|
||||
Msg("live count achieved, skip entering live room")
|
||||
skipEnterLive = true
|
||||
} else if rand.Float64() <= livePreviewProb {
|
||||
log.Info().Interface("livePreviewProb", livePreviewProb).Msg("skip entering preview")
|
||||
skipEnterLive = true
|
||||
}
|
||||
|
||||
if !skipEnterLive {
|
||||
time.Sleep(1 * time.Second)
|
||||
// enter live room
|
||||
entryPoint := PointF{
|
||||
X: float64(dExt.WindowSize.Width / 2),
|
||||
Y: float64(dExt.WindowSize.Height / 2),
|
||||
}
|
||||
|
||||
log.Info().Msg("tap screen center to enter live room")
|
||||
if err := crawler.driverExt.TapAbsXY(entryPoint.X, entryPoint.Y,
|
||||
WithOffsetRandomRange(-20, 20)); err != nil {
|
||||
log.Error().Err(err).Msg("tap live video failed")
|
||||
continue
|
||||
}
|
||||
currentVideo.Type = VideoType_Live
|
||||
} else {
|
||||
// skip entering live room
|
||||
// only mock simulation play duration
|
||||
sleepTime := math.Min(float64(currentVideo.SimulationPlayDuration), float64(currentVideo.RandomPlayDuration))
|
||||
currentVideo.PlayDuration = int64(sleepTime)
|
||||
}
|
||||
fallthrough
|
||||
|
||||
case VideoType_Live:
|
||||
// 直播
|
||||
crawler.LiveCount++
|
||||
log.Info().Interface("video", currentVideo).Msg(FOUND_LIVE_SUCCESS)
|
||||
|
||||
// wait 3s for live loading
|
||||
time.Sleep(3 * time.Second)
|
||||
// take screenshot and get screen texts by OCR
|
||||
screenResult, err := crawler.driverExt.GetScreenResult(
|
||||
WithScreenShotOCR(true),
|
||||
WithScreenShotUpload(true),
|
||||
WithScreenShotLiveType(true),
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("get screen result failed")
|
||||
}
|
||||
|
||||
// add live type
|
||||
if screenResult.ImageResult != nil &&
|
||||
screenResult.ImageResult.LiveType != "" &&
|
||||
screenResult.ImageResult.LiveType != "NoLive" {
|
||||
currentVideo.LiveType = screenResult.ImageResult.LiveType
|
||||
}
|
||||
|
||||
// simulation watch live video
|
||||
simulationPlayDuration := math.Min(float64(currentVideo.PlayDuration), 300000)
|
||||
sleepStrict(swipeFinishTime, int64(simulationPlayDuration))
|
||||
|
||||
screenResult.Video = currentVideo
|
||||
screenResult.Resolution = dExt.WindowSize
|
||||
screenResult.SwipeStartTime = swipeStartTime.UnixMilli()
|
||||
screenResult.SwipeFinishTime = swipeFinishTime.UnixMilli()
|
||||
screenResult.TotalElapsed = time.Since(swipeFinishTime).Milliseconds()
|
||||
screenResult.FetchVideoStartTime = fetchVideoStartTime.UnixMilli()
|
||||
screenResult.FetchVideoFinishTime = fetchVideoFinishTime.UnixMilli()
|
||||
screenResult.FetchVideoElapsed = fetchVideoFinishTime.Sub(fetchVideoStartTime).Milliseconds()
|
||||
|
||||
var exitLive bool
|
||||
if crawler.isLiveTargetAchieved() {
|
||||
log.Info().Interface("live", currentVideo).
|
||||
Msg("live count achieved, exit live room")
|
||||
exitLive = true
|
||||
} else if rand.Float64() <= livePreviewProb {
|
||||
log.Info().Interface("livePreviewProb", livePreviewProb).Msg("exit live room by preview live chance")
|
||||
exitLive = true
|
||||
}
|
||||
|
||||
// isFeed:通过预览流进入内流失败的情况下,防止使用退出直播间逻辑,影响:首次进入内流,至少会消费两个直播间才能退出
|
||||
if !isFeed && exitLive && currentVideo.Type == VideoType_Live {
|
||||
err = crawler.exitLiveRoom()
|
||||
if err != nil {
|
||||
if errors.Is(err, code.TimeoutError) || errors.Is(err, code.InterruptError) {
|
||||
return err
|
||||
}
|
||||
log.Error().Err(err).Msg("run live crawler failed, continue")
|
||||
}
|
||||
} else {
|
||||
isFeed = false
|
||||
}
|
||||
|
||||
default:
|
||||
isFeed = true
|
||||
// 点播 || 图文 || 广告 || etc.
|
||||
crawler.FeedCount++
|
||||
log.Info().Interface("video", currentVideo).Msg(FOUND_FEED_SUCCESS)
|
||||
|
||||
screenResult := &ScreenResult{
|
||||
Resolution: dExt.WindowSize,
|
||||
Video: currentVideo,
|
||||
|
||||
// log swipe timelines
|
||||
SwipeStartTime: swipeStartTime.UnixMilli(),
|
||||
SwipeFinishTime: swipeFinishTime.UnixMilli(),
|
||||
FetchVideoStartTime: fetchVideoStartTime.UnixMilli(),
|
||||
FetchVideoFinishTime: fetchVideoFinishTime.UnixMilli(),
|
||||
FetchVideoElapsed: fetchVideoFinishTime.Sub(fetchVideoStartTime).Milliseconds(),
|
||||
}
|
||||
dExt.cacheStepData.screenResults[time.Now().String()] = screenResult
|
||||
|
||||
// simulation watch feed video
|
||||
simulationPlayDuration := math.Min(float64(currentVideo.PlayDuration), 600000)
|
||||
sleepStrict(swipeFinishTime, int64(simulationPlayDuration))
|
||||
screenResult.TotalElapsed = time.Since(swipeFinishTime).Milliseconds()
|
||||
}
|
||||
|
||||
// check if target count achieved
|
||||
if crawler.isTargetAchieved() {
|
||||
log.Info().Msg("target count achieved, exit crawler")
|
||||
return nil
|
||||
}
|
||||
|
||||
// reset failed count
|
||||
crawler.failedCount = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type VideoType string
|
||||
|
||||
const (
|
||||
VideoType_Feed VideoType = "FEED"
|
||||
VideoType_PreviewLive VideoType = "PREVIEW-LIVE" // 直播预览流
|
||||
VideoType_Live VideoType = "LIVE"
|
||||
VideoType_Image VideoType = "IMAGE"
|
||||
VideoType_AD VideoType = "AD"
|
||||
)
|
||||
|
||||
type Video struct {
|
||||
Type VideoType `json:"type" required:"true"` // 视频类型, feed/preview-live/live/image
|
||||
DataType string `json:"data_type"` // 数据源对应的事件名称
|
||||
|
||||
// Feed 视频基础数据
|
||||
CacheKey string `json:"cache_key,omitempty"` // cachekey
|
||||
VideoID string `json:"video_id,omitempty"` // 视频 video ID
|
||||
URL string `json:"feed_url,omitempty"` // 实际播放的视频 url
|
||||
UserName string `json:"user_name"` // 视频作者
|
||||
Duration int64 `json:"duration,omitempty"` // 视频时长(ms)
|
||||
Caption string `json:"caption,omitempty"` // 视频文案
|
||||
// 作者信息
|
||||
UserID string `json:"user_id"` // 作者用户名
|
||||
FollowerCount int64 `json:"follower_count"` // 作者粉丝数
|
||||
// 视频热度数据
|
||||
ViewCount int64 `json:"view_count,omitempty"` // feed 观看数
|
||||
LikeCount int64 `json:"like_count,omitempty"` // feed 点赞数
|
||||
CommentCount int64 `json:"comment_count,omitempty"` // feed 评论数
|
||||
CollectCount int64 `json:"collect_count,omitempty"` // feed 收藏数
|
||||
ForwardCount int64 `json:"forward_count,omitempty"` // feed 转发数
|
||||
ShareCount int64 `json:"share_count,omitempty"` // feed 分享数
|
||||
|
||||
// timelines
|
||||
PublishTimestamp int64 `json:"publish_timestamp,omitempty"` // feed 发布时间戳
|
||||
PreloadTimestamp int64 `json:"preload_timestamp,omitempty"` // feed 预加载时间戳
|
||||
|
||||
// Live 视频基础数据
|
||||
LiveStreamID string `json:"live_stream_id,omitempty"` // 直播流 ID
|
||||
LiveStreamURL string `json:"live_stream_url,omitempty"` // 直播流地址
|
||||
LiveType string `json:"live_type,omitempty"` // 直播间类型
|
||||
// 网络数据
|
||||
ThroughputKbps int64 `json:"throughput_kbps,omitempty"` // 网速
|
||||
// 视频热度数据
|
||||
AudienceCount int64 `json:"audience_count,omitempty"` // 直播间人数
|
||||
|
||||
// 图文数据
|
||||
ImageUrls []string `json:"image_urls,omitempty"` // 图片对应的 url 列表
|
||||
|
||||
// 记录仿真决策信息
|
||||
PlayDuration int64 `json:"play_duration"` // 播放时长(ms),取自 Simulation/Random
|
||||
SimulationPlayProgress float64 `json:"simulation_play_progress"` // 仿真播放比例(完播率)
|
||||
SimulationPlayDuration int64 `json:"simulation_play_duration"` // 仿真播放时长(ms)
|
||||
RandomPlayDuration int64 `json:"random_play_duration"` // 随机播放时长(ms)
|
||||
}
|
||||
|
||||
func (vc *VideoCrawler) clearCurrentVideo() error {
|
||||
if !vc.driverExt.plugin.Has("ClearCurrentVideo") {
|
||||
return errors.New("plugin missing ClearCurrentVideo method")
|
||||
}
|
||||
|
||||
_, err := vc.driverExt.plugin.Call("ClearCurrentVideo")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "call plugin ClearCurrentVideo failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (vc *VideoCrawler) getCurrentVideo() (video *Video, err error) {
|
||||
if !vc.driverExt.plugin.Has("GetCurrentVideo") {
|
||||
return nil, errors.New("plugin missing GetCurrentVideo method")
|
||||
}
|
||||
|
||||
resp, err := vc.driverExt.plugin.Call("GetCurrentVideo")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "call plugin GetCurrentVideo failed")
|
||||
}
|
||||
|
||||
if resp == nil {
|
||||
return nil, errors.New("video not found")
|
||||
}
|
||||
|
||||
feedBytes, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return nil, errors.New("json marshal video info failed")
|
||||
}
|
||||
|
||||
video = &Video{}
|
||||
err = json.Unmarshal(feedBytes, video)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "json unmarshal video info failed")
|
||||
}
|
||||
|
||||
if video.Type == VideoType_Live || video.Type == VideoType_PreviewLive {
|
||||
video.RandomPlayDuration = getSimulationDuration(vc.configs.Live.SleepRandom)
|
||||
} else {
|
||||
video.RandomPlayDuration = getSimulationDuration(vc.configs.Feed.SleepRandom)
|
||||
}
|
||||
|
||||
// get simulation play duration
|
||||
if video.SimulationPlayDuration != 0 {
|
||||
video.PlayDuration = video.SimulationPlayDuration
|
||||
} else {
|
||||
video.PlayDuration = video.RandomPlayDuration
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str("type", string(video.Type)).
|
||||
Str("dataType", video.DataType).
|
||||
Msg("get current video success")
|
||||
return video, nil
|
||||
}
|
||||
|
||||
func (vc *VideoCrawler) getLivePreviewProb() float64 {
|
||||
if vc.driverExt.Device.System() == "ios" {
|
||||
return 0.5326
|
||||
} else if vc.driverExt.Device.System() == "android" {
|
||||
return 0.3414
|
||||
}
|
||||
return -1
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
//go:build localtest
|
||||
|
||||
package uixt
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVideoCrawler(t *testing.T) {
|
||||
setupAndroid(t)
|
||||
|
||||
driverExt.Driver.AppLaunch("com.ss.android.ugc.aweme")
|
||||
configs := &VideoCrawlerConfigs{
|
||||
Timeout: 600,
|
||||
Feed: FeedConfig{
|
||||
TargetCount: 5,
|
||||
TargetLabels: []TargetLabel{
|
||||
{Text: `^广告$`, Scope: Scope{0, 0.5, 1, 1}, Regex: true},
|
||||
{Text: `^图文$`, Scope: Scope{0, 0.5, 1, 1}, Regex: true, Target: 2},
|
||||
{Text: `^特效\|`, Scope: Scope{0, 0.5, 1, 1}, Regex: true},
|
||||
{Text: `^模板\|`, Scope: Scope{0, 0.5, 1, 1}, Regex: true},
|
||||
{Text: `^购物\|`, Scope: Scope{0, 0.5, 1, 1}, Regex: true},
|
||||
},
|
||||
SleepRandom: []interface{}{0, 5, 0.7, 5, 10, 0.3},
|
||||
},
|
||||
Live: LiveConfig{
|
||||
TargetCount: 3,
|
||||
SleepRandom: []interface{}{15, 20},
|
||||
},
|
||||
}
|
||||
err := driverExt.VideoCrawler(configs)
|
||||
checkErr(t, err)
|
||||
}
|
||||
@@ -337,15 +337,6 @@ func (s *StepMobile) SleepRandom(params ...float64) *StepMobile {
|
||||
return &StepMobile{step: s.step}
|
||||
}
|
||||
|
||||
func (s *StepMobile) VideoCrawler(params map[string]interface{}) *StepMobile {
|
||||
s.obj().Actions = append(s.obj().Actions, uixt.MobileAction{
|
||||
Method: uixt.ACTION_VideoCrawler,
|
||||
Params: params,
|
||||
Options: nil,
|
||||
})
|
||||
return &StepMobile{step: s.step}
|
||||
}
|
||||
|
||||
func (s *StepMobile) EndToEndDelay(options ...uixt.ActionOption) *StepMobile {
|
||||
s.obj().Actions = append(s.obj().Actions, uixt.MobileAction{
|
||||
Method: uixt.ACTION_EndToEndDelay,
|
||||
@@ -651,7 +642,7 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err
|
||||
|
||||
// automatic handling of pop-up windows on each step finished
|
||||
if !step.IgnorePopup && !s.IgnorePopup() {
|
||||
if err2 := uiDriver.ClosePopups(); err2 != nil {
|
||||
if err2 := uiDriver.ClosePopupsHandler(); err2 != nil {
|
||||
log.Error().Err(err2).Str("step", step.Name).Msg("auto handle popup failed")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user