Python+ADB实现Android手机QQ自动点赞

原创 2017年05月10日 22:37:49

1、 前言

前段时间看了些爬虫的知识,然后又看到selenium,Appium,在Appium环境设置过程中,意外地看到这个帖子adb命令模拟按键事件 KeyCode,然后结合相关搜索结果,完成了“QQ点赞这个想法”这个想法。

2、  环境

Windows 10、Python2.7.13、Android SDK、Android手机QQ 7.0.0.3135

3、 分析

3.1     相关命令

3.1.1adb shell input命令

Usage: input [<source>] <command> [<arg>...]

The sources are:
      keyboard
      mouse
      joystick
      touchnavigation
      touchpad
      trackball
      dpad
      stylus
      gamepad
      touchscreen

The commands and default sources are:
      text <string> (Default: touchscreen)
      keyevent [--longpress] <key code number or name> ... (Default: keyboard)
      tap <x> <y> (Default: touchscreen)
      swipe <x1> <y1> <x2> <y2> [duration(ms)] (Default: touchscreen)
      press (Default: trackball)
      roll <dx> <dy> (Default: trackball)

程序中用了其中的如下命令:

0、 adb shell input keyevent keycode命令,用于模拟在Android手机上按的一些特殊键(更多值还是看这个链接adb命令模拟按键事件 KeyCode),例如:

keycode为KEYCODE_BACK,执行命令相当于按了返回键;

keycode为KEYCODE_DPAD_UP或KEYCODE_DPAD_DOWN用于模拟上下键,按上下键可使屏幕上的控件出现被选中的背景色(就是按下分组名时控件背景闪现的灰色);

keycode为KEYCODE_MOVE_HOME时,可回到屏幕顶端(如在QQ空间向下翻了之后,连续以这个值为keycode,执行两次该命令,就可回到顶部);在联系人列表中,若没有控件被选中(即有灰色背景色),以KEYCODE_MOVE_HOME为keycode值执行命令,就可以让刚才选中过的控件被选中,再次执行则会选中列表中最顶端控件,QQ 7.0中,则会选中“特别关心”这个控件。

1、 adb shell input tap <x> <y>,该命令点击屏幕指定的坐标处,本程序主要用于点开联系人列表、点击列表中的好友进入个人资料卡界面、点击点赞图标。

2、 adb shell input swipe <x1> <y1> <x2> <y2> [duration(ms)],用于模拟在两个点之间滑动,最后一个参数是毫秒为单位的持续时间。


3.1.2 adb shell uiautomator dump [path]filename.xml命令

(来源:通过adb获取安卓应用屏幕所有控件信息

查看帮助:
C:\Users\Dragon>adb shell uiautomator
Usage: uiautomator <subcommand> [options]

Available subcommands:

help: displays help message

runtest: executes UI automation tests
    runtest <class spec> [options]
    <class spec>: <JARS> < -c <CLASSES> | -e class <CLASSES> >
      <JARS>: a list of jar files containing test classes and dependencies. If
        the path is relative, it's assumed to be under /data/local/tmp. Use
        absolute path if the file is elsewhere. Multiple files can be
        specified, separated by space.
      <CLASSES>: a list of test class names to run, separated by comma. To
        a single method, use TestClass#testMethod format. The -e or -c option
        may be repeated. This option is not required and if not provided then
        all the tests in provided jars will be run automatically.
    options:
      --nohup: trap SIG_HUP, so test won't terminate even if parent process
               is terminated, e.g. USB is disconnected.
      -e debug [true|false]: wait for debugger to connect before starting.
      -e runner [CLASS]: use specified test runner class instead. If
        unspecified, framework default runner will be used.
      -e <NAME> <VALUE>: other name-value pairs to be passed to test classes.
        May be repeated.
      -e outputFormat simple | -s: enabled less verbose JUnit style output.

dump: creates an XML dump of current UI hierarchy
    dump [--verbose][file]
      [--compressed]: dumps compressed layout information.
      [file]: the location where the dumped XML should be stored, default is
      /sdcard/window_dump.xml

events: prints out accessibility events until terminated
该命令会将当前的Activity信息以XML文件的形式存在手机中,XML文件中存放了各标签的一些属性,如本程序中用到的class、resource-id、checked、selected、bounds属性,其中bounds属性的值形式为[x1,y1][x2,y2],[x1,y1]为控件的左上角坐标,[x2,y2]为控件的右下角坐标。本程序就是利用对得到的XML文件的操作,使用正则表达式解析出控件坐标,从而使用上面提到的adb shell input命令模拟点击屏幕等对手机进行操作(本程序的点击操作使用的坐标都是计算的控件的中心坐标)。

3.1.3 其它相关命令

0、adb shell wm size命令,输出的字符中,包含了屏幕分辨率,也可用正则表达式解析出来,从而根据屏幕分辨率使用比例进行模拟滑动等操作。

1、获取当前与用户交互的Activity:通过adb shell命令查看当前与用户交互的activity,也可从输出中解析出活动,可用于判断是否进入了QQ这个程序的指定界面。(本程序没添加该功能)

2、Android中的一个am命令,可用于启动程序,不过我的手机没root,测试了几下不成功。也没加这个功能。

3.2 程序流程图


3.3 一些问题的处理

0、正则表达式解析的模式字符串从哪里找?

程序是在Windows上运行,所以要先使用adb pull命令将手机中的XML文件复制到PC中,然后在ui automator viewer中找到想处理的控件,通过bounds属性值在XML文件中查找标签,然后分析控件标签的特征字符串,构建正则表达式。

(如果能使用am命令启动程序,或许还可以直接在手机上运行代码,而且文件也不用复制到PC中,执行的shell命令也是在手机中执行。只需一个Python环境如QPython即可)

1、 联系人界面,如何确定选中的控件?

XML的标签中有属性selected,对于选中的控件,其值为true。

2、分组控件的处理,联系人列表是否已经展开

通过uiautomator viewer查看分组控件属性如下:

分组列表控件及其子控件中XML代码:
<node … resource-id="com.tencent.mobileqq:id/group_item_layout" … selected="true" bounds="[0,509][1080,647]">
<node … resource-id="com.tencent.mobileqq:id/name" class="android.widget.CheckBox" … checked="false" … selected="true" bounds="[32,533][64,623]"/>
通过观察,第一个子控件即CheckBox,若分组展开,其checked属性值为true,所以可通过这个判断以决定是否展开该分组下联系人列表。
3、判断是否是最后一个好友
光标经过最后一个好友之后,会再向下移动,移出好友列表之外,此时通过正则表达式匹配选中的控件会返回None,通过这个可判断是否已经是最后一个好友。(但中间可能发生一些意外情况,所以使用了一个变量作为计数器,每遇到返回None,计数器减1,计数器为0后,程序退出)

4、判断光标是否超出了屏幕范围,而并非是最后一个好友这种情况。

这种情况也可能发生,当屏幕下方可视范围内显示的不是完整的好友控件时,可能光标会在屏幕外并继续向下移动,但是这时正则表达式匹配不到已选中的控件,所以返回None。其中一种方法是:最开始先点击屏幕下方的联系人,折叠所有好友列表,然后向上滑动屏幕,这使“好友、群、…”那一列位于界面上“联系人”标题所在的控件下方,这样的话,光标向下移动时,列表会自动向上移动,使选中的联系人控件在屏幕可见范围内。

然而也可能遇到一些特殊情况,使得运行过程中可能出现意外情况。

3.4 未处理好的问题

这种情况:


有的人使用了QQ厘米秀这个功能,而且,遮挡的位置不同,但只要这个厘米秀人物在点赞图标附近,就会点到这个厘米秀人物进入别的页面。本是打算进入个人资料卡后先向上滑动屏幕,但是滑动的少的话还是会点击到这个人物,滑动得多的话,有的人的点赞图标又滑动到屏幕外去了。本程序未对这种情况做完善处理。

4、源代码

(本程序必须手动让手机进入QQ程序,并且要借助USB数据线,有一些局限性)
# _*_ coding: utf-8 _*_
# @Time : 2017/5/10 22:57
# @Author : 0x3E6
# @File : android_qq_praise.py
import os
import re

class QQPraise():
    def __init__(self):
        self.screen_center_x=0
        self.screen_center_y=0
        self.fd=None
        self.contact_UI_content=None
        self.profile_crad_content=None
        self.pattern=None
        self.points=None
        self.x=0
        self.y=0
        self.num=0

    def entry_point(self):
        self.prepare()
        i=0
        while 1:
            type = self.find_type_of_selected_widget()
            if type == "friend_widget":
                self.parse_coordinate()
                os.system("adb shell input tap %s %s" % (self.x, self.y))
                self.praise_and_return()
            elif type == "group_widget":
                self.expand_group()
            else:
                i+=1
                if i==10:
                    break
            if self.cursor_out_of_screen():
                os.system("adb shell input keyevent KEYCODE_PAGE_DOWN")
            os.system("adb shell input keyevent KEYCODE_DPAD_DOWN")

    def prepare(self):
        # 先切换到QQ主界面的中间联系人列表界面
        self.load_layout_xml("contacts_list.xml",1)
        self.pattern = "bounds=\"\[(\d{1,4}),(\d{1,4})\]\[(\d{1,4}),(\d{1,4})\][^>]*><node[^>]*text=\"联系人\" resource-id=\"com\.tencent\.mobileqq:id/name\""
        contact_widget=self.parse_UI(self.contact_UI_content)
        if contact_widget:
            os.system("adb shell input tap %s %s" % (self.x, self.y))
        # 选择好友列表
        self.load_layout_xml("contacts_list.xml",1)
        points=re.search("<node[^>]*text=\"好友\"[^>]*bounds=\"\[(\d{1,4}),(\d{1,4})\]\[(\d{1,4}),(\d{1,4})\]",self.contact_UI_content).groups()

        x=int(points[0])+(int(points[2])-int(points[0]))/2
        y=int(points[1])+(int(points[3])-int(points[1]))/2
        os.system("adb shell input tap %s %s" % (x,y))
        # 折叠好友列表
        os.system("adb shell input tap %s %s" % (self.x,self.y))
        # 获取屏幕分辨率计算屏幕中心
        f=os.popen("adb shell wm size")
        screen_width,screen_height=re.search("(\d{3,4})x(\d{3,4})",f.read()).groups()
        center=(int(screen_width)/2,int(screen_height)/2)
        self.screen_center_x=center[0]
        self.screen_center_y=center[1]
        # 向上划一下,将“好友、群、...”那一行移动到最上面
        os.system("adb shell input swipe %s %s %s %s" % (center[0],center[1],center[0],0))
        # 发送KEYCODE_MOVE_HOME将光标定位到“特别关心”分组
        os.system("for /L %%i in (1,1,2) do adb shell input keyevent KEYCODE_MOVE_HOME")

    def load_layout_xml(self, xml_name,option):
        # os.system("adb shell uiautomator dump /storage/sdcard0/friend_profile_card.xml")
        os.system("adb shell uiautomator dump /storage/sdcard0/%s" % xml_name)
        os.system("adb pull /storage/sdcard0/%s ./%s" % (xml_name, xml_name))
        self.fd = open(xml_name, "r")
        if option==1:
            self.contact_UI_content = self.fd.read()
        elif option==2:
            self.profile_crad_content=self.fd.read()
        self.fd.close()

    def parse_UI(self,content):
        result=re.search(self.pattern,content)
        if result:
            if len(result.groups())==5:
                self.points=result.groups()[1:]
                return result.groups()[0]
            elif len(result.groups())==4:
                self.points=result.groups()
                self.parse_coordinate()
                return True
            elif len(result.groups())==1:
                return result.groups()[0]
        else:
            return False

    def parse_coordinate(self):
        min_x = int(self.points[0])
        min_y = int(self.points[1])
        max_x = int(self.points[2])
        max_y = int(self.points[3])
        self.x = min_x + (max_x - min_x) / 2
        self.y = min_y + (max_y - min_y) / 2

    def find_type_of_selected_widget(self):
        self.load_layout_xml("contacts_list.xml",1)
        self.pattern = "<node[^>]*(id/group|LinearLayout)[^>]*selected=\"true\"[^>]*bounds=\"\[(\d{1,4}),(\d{1,4})\]\[(\d{1,4}),(\d{1,4})\]\"[^>]*>"
        contact_widget=self.parse_UI(self.contact_UI_content)
        if contact_widget:
            if "LinearLayout" == contact_widget:
                # print "好友控件", "坐标", type.groups()[1:]
                return "friend_widget"
            elif "id/group" == contact_widget:
                # print "分组控件", "坐标", type.groups()[1:]
                return "group_widget"
        else:
            return None

    def expand_group(self):
        coordinate = (int(self.points[0]), int(self.points[1]), int(self.points[2]), int(self.points[3]))
        self.pattern = "\[%s,%s\]\[%s,%s\]\"><node[^>]*checked=\"([^\"]*)\"" % coordinate
        checked = self.parse_UI(self.contact_UI_content)
        if checked == 'false':
            self.parse_coordinate()
            os.system("adb shell input tap %s %s" % (self.x, self.y))

    def praise_and_return(self):
        self.num+=1
        # 若有的好友资料卡中赞的位置被厘米秀挡住,需要取消下面这一行的注释,但这并不能解决所有问题
        os.system("adb shell input swipe %s %s %s %s" % (self.screen_center_x,self.screen_center_y,self.screen_center_x,self.screen_center_y*2/3))
        self.load_layout_xml("friend_profile_card.xml",2)
        # 找点赞位置
        self.pattern="<node [^<]*点击可赞[^>]* bounds=\"\[(\d{1,4}),(\d{1,4})\]\[(\d{1,4}),(\d{1,4})\].*"
        result=self.parse_UI(self.profile_crad_content)
        if result:
            os.system("FOR /L %%v IN (1,1,10) DO adb shell input tap %s %s" % (self.x, self.y))
            os.system("adb shell input keyevent KEYCODE_BACK && adb shell input keyevent KEYCODE_MOVE_HOME")

    def cursor_out_of_screen(self):
        self.pattern = "<node[^>]*selected=\"true\"[^>]*>[^<]*(</node>){6}"
        is_out = re.search(self.pattern, self.contact_UI_content)
        if is_out:
            return True
        else:
            return False

def main():
    praiser=QQPraise()
    praiser.entry_point()

if __name__ == "__main__":
    main()


版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

Docker_入门?只要这篇就够了!(纯干货适合0基础小白)

与sgy一起开启你的Docker之路 关键词: Docker; mac; Docker中使用gdb无法进入断点,无法调试; 写在前面 这篇博客适合谁? 对于Docker并不了解,只是有一点模糊的...

想要清肠排毒,就喝汁己青汁!

保持肠胃畅通,对于我们的身体是非常重要的。积累毒素会影响我们气色和皮肤。只有身体排毒好了,才能让身体更加轻盈人也更加精神。 日常多加注意一些小细节可帮助你减少毒素积累。 多喝水 早晨最好空腹喝水...

史上最简单的 MySQL 教程(三)「 MySQL 数据库」

MySQL 数据库MySQL 数据库是一种C\S结构的软件,即分为:客户端和服务端。若想访问服务器,必须通过客户端;服务器应该一直运行,客户端则在需要使用的时候运行。

Node.js开发入门—使用对话框ngDialog

做网站经常会遇到弹出对话框获取用户输入或弹出对话框让用户确认某个操作之类的情景,基于AngularJS的扩展模块ngDialog可以帮我们优雅地完成这类事情。
  • foruok
  • foruok
  • 2015-09-06 07:15
  • 13225

ACM竞赛路上亲爱的坑们

写在前边:这些梗都是敝人自己做题和比赛时曾经坑过自己的地方,特别在这里记录一下,所有的链接都是本博客中的题解链接(有大致题意说明和代码),原题请到OJ上自行寻找。目的是提升自身姿势。欢迎大佬们给我提出...

经验分享-前端与后端的接口、HTML分离

在WEB项目中 前后端不分离多人开放效率还不及一个人开发效率来的高,今天分享一个概念

java实现二维码([带]logo)的绘制和解析(zxing by google)

二维条码/二维码(2-dimensional bar code)是用某种特定的几何图形按一定规律在平面(二维方向上)分布的黑白相间的图形记录数据符号信息的;在代码编制上巧妙地利用构成计算机内部逻辑基础...

Java基础之(三十七)Java多线程编程<二>

控制线程join线程Thread提供了一个线程等待另一个线程完成的方法:join方法。当在某个程序执行流中调用其他线程的join方法,调用join方法的那个线程将被阻塞,直到被join方法加入的joi...

std::map 如何使用结构体作为自定义键值

在使用map时,有时候我们需要自定义键值,才能符合程序的需要。 比如我们需要使用自定义的结构体来作为map的键值.
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)