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

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 []  [...]

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

The commands and default sources are:
      text  (Default: touchscreen)
      keyevent [--longpress]  ... (Default: keyboard)
      tap   (Default: touchscreen)
      swipe     [duration(ms)] (Default: touchscreen)
      press (Default: trackball)
      roll   (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 ,该命令点击屏幕指定的坐标处,本程序主要用于点开联系人列表、点击列表中的好友进入个人资料卡界面、点击点赞图标。

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


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

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

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

Available subcommands:

help: displays help message

runtest: executes UI automation tests
    runtest  [options]
    :  < -c  | -e class  >
      : 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.
      : 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  : 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 程序流程图

Python+ADB实现Android手机QQ自动点赞_第1张图片

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查看分组控件属性如下:
Python+ADB实现Android手机QQ自动点赞_第2张图片
分组列表控件及其子控件中XML代码:


通过观察,第一个子控件即 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})\][^>]*>]*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("]*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 = "]*(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\]\">]*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="]* 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 = "]*selected=\"true\"[^>]*>[^<]*(){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()


你可能感兴趣的:(Python,ADB)