前段时间看了些爬虫的知识,然后又看到selenium,Appium,在Appium环境设置过程中,意外地看到这个帖子adb命令模拟按键事件 KeyCode,然后结合相关搜索结果,完成了“QQ点赞这个想法”这个想法。
Windows 10、Python2.7.13、Android SDK、Android手机QQ 7.0.0.3135
Usage: input [
程序中用了其中的如下命令:
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
(来源:通过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命令模拟点击屏幕等对手机进行操作(本程序的点击操作使用的坐标都是计算的控件的中心坐标)。
0、adb shell wm size命令,输出的字符中,包含了屏幕分辨率,也可用正则表达式解析出来,从而根据屏幕分辨率使用比例进行模拟滑动等操作。
1、获取当前与用户交互的Activity:通过adb shell命令查看当前与用户交互的activity,也可从输出中解析出活动,可用于判断是否进入了QQ这个程序的指定界面。(本程序没添加该功能)
2、Android中的一个am命令,可用于启动程序,不过我的手机没root,测试了几下不成功。也没加这个功能。0、正则表达式解析的模式字符串从哪里找?
程序是在Windows上运行,所以要先使用adb pull命令将手机中的XML文件复制到PC中,然后在ui automator viewer中找到想处理的控件,通过bounds属性值在XML文件中查找标签,然后分析控件标签的特征字符串,构建正则表达式。
(如果能使用am命令启动程序,或许还可以直接在手机上运行代码,而且文件也不用复制到PC中,执行的shell命令也是在手机中执行。只需一个Python环境如QPython即可)
1、 联系人界面,如何确定选中的控件?
XML的标签中有属性selected,对于选中的控件,其值为true。
2、分组控件的处理,联系人列表是否已经展开
通过uiautomator viewer查看分组控件属性如下:
通过观察,第一个子控件即
CheckBox
,若分组展开,其
checked
属性值为
true
,所以可通过这个判断以决定是否展开该分组下联系人列表。
4、判断光标是否超出了屏幕范围,而并非是最后一个好友这种情况。
这种情况也可能发生,当屏幕下方可视范围内显示的不是完整的好友控件时,可能光标会在屏幕外并继续向下移动,但是这时正则表达式匹配不到已选中的控件,所以返回None。其中一种方法是:最开始先点击屏幕下方的联系人,折叠所有好友列表,然后向上滑动屏幕,这使“好友、群、…”那一列位于界面上“联系人”标题所在的控件下方,这样的话,光标向下移动时,列表会自动向上移动,使选中的联系人控件在屏幕可见范围内。
然而也可能遇到一些特殊情况,使得运行过程中可能出现意外情况。
这种情况:
有的人使用了QQ厘米秀这个功能,而且,遮挡的位置不同,但只要这个厘米秀人物在点赞图标附近,就会点到这个厘米秀人物进入别的页面。本是打算进入个人资料卡后先向上滑动屏幕,但是滑动的少的话还是会点击到这个人物,滑动得多的话,有的人的点赞图标又滑动到屏幕外去了。本程序未对这种情况做完善处理。
# _*_ 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()