薅羊毛 | 揭秘闲鱼方案,一部手机,实现随时随地薅羊毛

image

阅读文本大概需要 15 分钟。

1、目标场景

上一篇文章 过后,很多人在后台给我留言,说上次的方案不够智能,每次操作都要连上电脑操作,一旦离开 PC,就没法愉快地薅羊毛了。问我有没有便捷的方案?

答案是肯定的。万能的闲鱼有大神已经实现了。那他们是如何实现的?

本篇文章以「全民小视频」为例,在只有一部手机的情况下,利用 Android 单元测试脚本「UIAutomator2」来实现这一操作。

2、准备工作

由于需要编写 Android 单元测试脚本,所以需要在 PC 端配置好 Android 开发环境,并准备一部已「root」的 Android 设备,提前下载好 Android 开发工具:Android Studio。

ps:以上操作是必备的。对 Android 开发不熟悉的童鞋可以自行 Google 一下。

另外,使用源文件直接调用测试脚本需要对当前应用进行「签名」,所以需要提前使用 AS 创建一个签名文件「也可以直接使用源码中提供的签名文件:deal」,然后在 app/gradle.build 文件中配置好 Debug 和 Release 模式的签名。

android {
  # 签名文件
  signingConfigs {
        release {
            keyAlias 'xag'
            keyPassword 'xingag'
            storeFile file('./../deal')
            storePassword 'xingag'
        }
    }

   # 配置签名
   buildTypes {
        debug {
            signingConfig signingConfigs.release
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        release {
            signingConfig signingConfigs.release
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

最后在 app/gradle.build 文件中添加单元测试脚本依赖库:uiautomator。

dependencies {    
# 新增uiautomator依赖    
implementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.3'
}

ps:由于很多 App 厂商针对模拟器会做反作弊操作,所以最好使用一部真机来测试。

3、编写脚本

我们首先使用 AS 创建一个新的项目。

image

接着在测试脚本目录下「app/src/androidTest/java/」新建一个类 GetDeal,让它使用 AndroidJUnit4 的方式运行。

@RunWith(AndroidJUnit4.class)
public class GetDeal{
}

在 @BeforClass 和 @Before 注解的方法中初始化「UiDevice」,使用UiDevice 类获取到当前设备的宽和高。

# 只会被调用一次
@BeforeClass
public static void init()
{
   # 初始化UiDevice
    mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
}

//每一个测试case被调用之前都会运行
@Before
public void start()
{
    //获取当前设备屏幕高度、宽度
    device_width = mDevice.getDisplayWidth();
    device_height = mDevice.getDisplayHeight();
}

然后,使用 @Test 注解创建一个测试 Case。

由于 UIAutomator2 中可以通过 InstrumentationRegistry 拿到「上下文 Context」,获取到目标应用的包名后,就可以顺利使用 Intent 的方式打开目标 App。

# 全民小视频包名
private String mPackageName = "com.baidu.minivideo";

# 获取到上下文
Context mContext = InstrumentationRegistry.getContext();

# 指明意图
Intent myIntent = mContext.getPackageManager().getLaunchIntentForPackage(sPackageName);  

# 启动全民小视频app
mContext.startActivity(myIntent);

与其他测试框架一样,第一次打开 App 都需要做一个显式等待操作,直到首页元素加载完全。

获取元素的属性值需要借助 android sdk 文件夹自带的工具:「uiautomatorviewer」。

image
# 等待首页中的某个元素完全加载出来
# 超时时间:10s
boolean result = mDevice.wait(Until.hasObject(By.res("com.baidu.minivideo:id/fragment_index_recycler")), 10 * 1000);

if (result)
{
  //后面的操作
}else
{
  Log.d("xag", "超时了~");
}

接着,我们获取到首页列表元素的第一个子元素,执行点击操作,进入到视频播放页面。

//主页列表元素
UiObject2 rv = mDevice.findObject(By.res("com.baidu.minivideo:id/fragment_index_recycler"));

//列表元素下面的子元素列表
if (rv.getChildCount() > 0)
{
    //获取第一项元素,点击进入
    UiObject2 first_video_element = rv.getChildren().get(0);
    first_video_element.click();

为了应对某些 App 的反作弊行为,这里需要对每一条视频的播放时间取一个随机数据。

/***
   * 产生随机数
   * @param min
   * @param max
   * @return
*/
public static int geneRandom(int min, int max)
{
    Random random = new Random();
    return random.nextInt(max) % (max - min + 1) + min;
}

int wait_time = NumUtils.geneRandom(10, 40);
Log.d("xag", "这个视频播放时间:" + wait_time + "s");

使用 uiautomatorviewer 工具可以获取到当前视频的一些元素属性,然后再使用 UiDevice 类获取到视频的基本信息,包含:视频标题、视频作者、播放时长。

/***
   * 获取当前视频信息
   * @param play_time 视频播放时间
*/
private VideoItem get_current_video_info(int play_time)
{
    # 视频作者
    UiObject2 author_element = mDevice.findObject(By.res("com.baidu.minivideo:id/detail_author_name"));

    # 视频标题
    UiObject2 content_element = mDevice.findObject(By.res("com.baidu.minivideo:id/detail_title"));

    String author = (null == author_element) ? "" : author_element.getText();
    String content = (null == content_element) ? "" : content_element.getText();

    VideoItem item = new VideoItem(play_time, author, content);

   return item;
 }

当一个视频播放时长到了之后,我们利用 UiDevice 的 swipe 函数模拟向上滑动到下一个视频播放界面。

同样,这里对起始坐标和结束坐标做一定数据内的「随机」处理,保证每次滑动的起始坐标、结束坐标都不一样。

/***
   * 下一个视频
   */
private void play_next_video()
{
    //手机按下的坐标和抬手的坐标
    int top = NumUtils.geneRandom(20, 150);
    int bottom = device_height - top;

    int top_x = NumUtils.geneRandomWithOffset(device_width / 2, 10);
    int bottom_x = NumUtils.geneRandomWithOffset(device_width / 2, 10);

    Log.d("xag", "滑动底部坐标:" + bottom_x + "/" + bottom + ";顶部坐标:" + top_x + "/" + top);

    # 获取到下一个视频界面
    mDevice.swipe(bottom_x, bottom, top_x, top, step);

    mDevice.waitForIdle(timeout);
    }

循环执行以上操作,就可以实现从打开一个 App,到切换页面、滑动页面等一系列常见操作。

4、调用测试脚本

最后一个步骤是通过一个 App 应用调用上面编写的测试脚本。

首先,我们需要在默认项目中的布局文件:activity_main.xml 中加入一个按钮控件。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="10dip"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/start_qmxsp_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="全民小视频"
        android:textSize="20sp" />

</LinearLayout>

接着在主界面 MainActivity 中添加点击的监听事件。

# 按钮
start_qmxsp_tv = findViewById(R.id.start_qmxsp_tv);

# 为按钮添加监听事件
start_qmxsp_tv.setOnClickListener(this);

# 回调
@Override
public void onClick(View v)
{
        switch (v.getId())
        {
            case R.id.start_qmxsp_tv:
                # 回调测试case
                openQmxspMet();
                break;
            default:
                break;
        }
}

通过「am instrument」语法,加上测试 Case 所在的包名及测试 Case 类名、方法名生成一条命令。

# 测试Case类所在的包名
String pkgName = "com.xingag.qmxsp";

// 测试Case的类名
String clsName = "GetDeal";

// 测试Case的方法名
String mtdName = "start_qmxsp";

// 生成一条命令
String command = "am instrument -w -r -e debug false -e class "
                + pkgName + "." + clsName + "#" + mtdName + " "
                + pkgName + ".test/android.support.test.runner.AndroidJUnitRunner";

然后利用「Runtime.getRuntime().exec("su")」获取到设备 Root 权限之后,就可以执行上面的命令了。

//获取root权限
process = Runtime.getRuntime().exec("su"); 
pw = new PrintWriter(process.getOutputStream());/
/执行命令
pw.println(command);pw.flush();result = process.waitFor();

需要注意的是,执行上面的命令是一个「耗时」的操作,必须放在子线程中执行。

new Thread()
        {
            @Override
            public void run()
            {
                super.run();
                String command = generateCommand("com.xingag.qmxsp", "GetDeal", "start_qmxsp");
                CMDUtils.CMD_Result rs = CMDUtils.runCMD(command, true, true);
            }
        }.start();

最后,可以直接运行项目后,会在手机上生成一个应用;或者可以通过 AS 工具生成一个带有签名的应用 APK,然后再利用手机助手安装到手机上。

image

5、结果结论

打开应用,然后点击界面上的按钮,紧接着会执行测试脚本,按照测试 Case 中写好的脚本步骤执行一系列操作。

image

本篇只是以全民小视频一个 App 为例,使用 UIAutomator2 脚本实现全自动薅羊毛的操作。

如果想要实现其他平台,只需编写对应的测试 Case,按顺序依次执行测试脚本,就可以实现一部手机随时随地薅多个平台羊毛的操作。

我已经将全部源码,包含一个测试 apk 上传到后台上,公众号回复「 **薅羊毛2 **」即可获得。

如果你觉得文章还不错,请大家点赞分享下。你的肯定是我最大的鼓励和支持。

推荐阅读:

薅羊毛 | 让Python每天帮你薅一个早餐钱

本文首发于公众号「 AirPython 」,公众号后台回复「 薅羊毛2 」即可获取完整代码。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容