uiautomator2 操作手机
目录
[TOC]

一: 需求:
手机插入电脑端后,可以从电脑端控制手机进行微信公众号的信息爬取

二: 选择uiautomator2调研的原因?
uiautomator2底层是Google提供的用来做安卓自动化测试的UiAutomator。 UiAutomator功能很强:

可以对第三方App进行测试
获取屏幕上任意一个App的任意一个控件属性,并对其进行任意操作
测试逻辑能够用python编写:将uiautomator中的功能开放出来,然后再将这些http接口封装成Python库
UiAutomator的功能可以满足操控手机,获取组件进而提取想要的信息;uiautomator2是一个python库。基于这些方面的考虑,选择了uiautomator2进行实际调研
三: 环境搭建
3.1 安装python库
uiautomator2(主要库)
weditor(直观获取组件层次)
jupyter(notebook笔记方便调试记录)
3.2 配置adb环境
下载adb

ADB和Fastboot for Windows: https://dl.google.com/android...
ADB和Fastboot for Mac: https://dl.google.com/android...
ADB和Fastboot for Linux: https://dl.google.com/android...
配置环境变量

windows:控制面板->系统与安全->系统->高级系统设置->环境变量,将adb路径添加到PATH中
linux: 编辑.bashrc, 添加export PATH=$PATH:adb路径, 然后执行source .bashrc
四: 使用
4.1 连接手机

  1. 检测手机是否正确配置
    打开手机的开发者功能,并开启调试功能
    使用usb线连接电脑时,usb用途选择“传输文件”
    在命令行中使用adb devices查看是否成功检测到
  2. 获取手机连接后的识别码
    import os

cmd = "adb devices"
r = os.popen(cmd)
text = r.read() # adb devices命令的执行结果
r.close()

row = text.split("\n")[1]
if not bool(row):

print("未检测到设备")
raise Exception("未检测到设备")

else:

con_str = row.split('\t')[0]

print(con_str) # 获取识别码

  1. 使用uiautomator2连接手机
    import uiautomator2 as u2

d = u2.connect(con_str)
print(d.info)
4.2 操作微信搜索公众号

  1. 微信版本信息
    d.app_info("com.tencent.mm")
  2. 打开微信

    输入中文的时候需要使用fastinputIme

    d.set_fastinput_ime(True)

先关掉微信重新启动

d.app_stop("com.tencent.mm")
d.app_start("com.tencent.mm") # 微信

  1. 获取状态栏大小(后续滑动的时候会用到)
    status_bar_selector = d.xpath('//*[@resource-id="com.android.systemui:id/status_bar"]')
    status_bar_x, status_bar_y, status_bar_width, status_bar_height = status_bar_selector.get().rect
    print(status_bar_x, status_bar_y, status_bar_width, status_bar_height)
  2. 查找公众号

    设定需要爬取的公众号变量

    spider_name = "进击的Coder"

定位添加按钮"+"

selector_add_button = d.xpath('//com.tencent.mm.ui.mogic.WxViewPager//android.widget.ListView//android.widget.FrameLayout/android.widget.LinearLayout[1]/android.widget.RelativeLayout[2]/android.widget.ImageView')
selector_add_button.click()

定位"添加朋友"

d(text="添加朋友").click()

选择"公众号"

d(text="公众号").click()

输入要搜索的公众号名字,并搜索

d.set_fastinput_ime(True)
d.send_keys(spider_name, clear=True)
time.sleep(1)

在输入法中要打开OSError: FastInputIME started failed

try:

d.send_action("search")

except OSError:

raise Exception("请手动切换输入法")
  1. 从搜索结果中选择
    time.sleep(2)

    点击对应名字的公众号进入

    d.xpath('//*[@text="{}"]'.format(spider_name)).click()

  2. 判断是否关注

    判断是否关注

    need_subscribe=d.xpath('//*[@text="关注公众号"]').all()
    if bool(need_subscribe):
    print("没有关注该公众号")
    # 关注后会自动进入该公众号
    d.xpath('//*[@text="关注公众号"]').click()
    # 等页面加载, 然后退回列表页面
    time.sleep(2)
    d.xpath('//android.support.v7.widget.LinearLayoutCompat').click()
    else:
    print("已经关注该公众号")
    4.3 处理列表

  3. 层次结构
    ListView组件下有多个LinearLayout
    LinearLayout下有0/1/多个ViewGroup
    ViewGroup是一条消息所占据的组件
    组件层次结构图示
  4. 思路
    因为列表页面只加载屏幕能够显示的元素, 所以不能直接获取列表的全部, 需要依次滚动.
    先处理当前显示的元素, 提取出ListView, 然后提取出ListView下的所有LinearLayout组件

每一个LinearView通过deal_linear_layout函数进行处理

提取出LinearLayout下的所有ViewGroup组件

每一个ViewGroup通过deal_view_group函数进行处理
在进行向下细分处理时, 传入当前组件是否是同类型最后最后一个的标识

如果LinearLayout是最后一个

如果没有ViewGroup, 返回该LinearLayout的rect属性
如果有ViewGroup:

如果该ViewGroup是最后一个, 返回rect属性
如果不是, 点击进入文章详细页面, 进行提取操作, 继续循环
根据deal_list函数的返回进行滑动操作

  1. 处理显示的列表页面
    def deal_list():
    """ 处理某时刻的列表"""
    xpath_linear_layouts = '//*[@resource-id="android:id/list"]/android.widget.LinearLayout'
    selector_linear_layouts = d.xpath(xpath_linear_layouts)
    count_linear_layout = len(selector_linear_layouts.all())
    result = None
    for index in range(1, count_linear_layout+1):

     result = deal_linear_layout(
         '{}[{}]'.format(xpath_linear_layouts, index),
         index==count_linear_layout)

    return result

  2. 处理单个LinearLayout组件
    def deal_linear_layout(xpath_linear_layout, is_last_linear):
    """处理单个linear_layout"""
    xpath_group_layouts = '{}/android.view.ViewGroup'.format(xpath_linear_layout)
    selector_group_layouts = d.xpath(xpath_group_layouts)
    count_group_layout = len(selector_group_layouts.all())
    result = None
    if count_group_layout == 0:

     print("没找到ViewGroup", is_last_linear)
     if is_last_linear:
         result = d.xpath(xpath_linear_layout).get().rect

    else:

     print("找到了ViewGroup, 开始处理组消息", count_group_layout)
     for index in range(1, count_group_layout+1):
         result = deal_view_group(
             '{}[{}]'.format(xpath_group_layouts, index),
             is_last_linear and index==count_group_layout)

    return result

  3. 处理单个GroupView组件
    def deal_view_group(xpath_group_layout, is_last_group):
    """处理单个group_layout"""
    if is_last_group:

     # 返回最后一个groupview的rect
     selector_last_group_layout = d.xpath(xpath_group_layout)
     last_group_layout = selector_last_group_layout.get()
     return last_group_layout.rect

    else:

     selector_group_layout = d.xpath(xpath_group_layout)
     # 进入消息详情页面,提取消息具体信息
     # selector_group_layout.click()
     return None
  4. 处理当前列表并滑动操作(重复执行)
    last_layout_rect = deal_list()

sx=360
sy=last_layout_rect[1]
ex=360
ey=status_bar_height
d.drag(sx, sy, ex, ey)

  1. 结尾判断(未完善)
    注: 列表是否进行到头的判断还没有添加
    4.4 处理文章详情
  2. 标题

    获取标题名

    title_selector = d.xpath('//*[@resource-id="activity-name"]')
    if len(title_selector.all()) != 0:
    ele = title_selector.get()
    print("获取到了标题:", ele.attrib.get("content-desc") if bool(ele.attrib.get("content-desc")) else ele.attrib.get("text"))
    else:
    print("未找到标题")

  3. meta数据

    版权logo

    copyright_selector = d.xpath('//*[@resource-id="copyright_logo"]')
    if len(copyright_selector.all()) != 0:
    ele = copyright_selector.get()
    print("获取到了版权", ele.attrib.get("content-desc") if bool(ele.attrib.get("content-desc")) else ele.attrib.get("text"))
    else:
    print("未找到版权")

作者

author_selector = d.xpath('//*[@resource-id="js_name"]')
if len(author_selector.all()) != 0:

ele = author_selector.get()
print("获取到了作者", ele.attrib.get("content-desc") if bool(ele.attrib.get("content-desc")) else ele.attrib.get("text"))

else:

print("未找到作者")

时间

time_selector = d.xpath('//*[@resource-id="publish_time"]')
if len(time_selector.all()) != 0:

ele = time_selector.get()
print("获取到了时间", ele.attrib.get("content-desc") if bool(ele.attrib.get("content-desc")) else ele.attrib.get("text"))

else:

print("未找到时间")
  1. 正文(未完善)
    views_selector = d.xpath('//*[@resource-id="js_content"]/android.view.View')
    if len(views_selector.all()) !=0:
    for view in views_selector.all():

     view_string = view.attrib.get("content-desc") if bool(view.attrib.get("content-desc")) else view.attrib.get("text")
     if bool(view_string):
         print(view_string.strip())

    else:
    print("结尾了")
    也需要同处理列表相同的操作,边滑动边处理

  2. 返回列表页
    back_selector = d.xpath('//*[@resource-id="com.tencent.mm:id/dn"]')
    if len(back_selector.all()) == 0:
    print("未找到返回键")
    else:
    print("返回")
    back_selector.click()
    五: 调研结果
    5.1 碰到的问题
    组件识别问题:存在在一些机型上有些界面无法分析组件关系,影响下一步的点击操作。
    输入法切换问题:输入要搜索的内容后,需要回车确认。因为无法识别手机自带输入法的“搜索”按钮,所以需要切换uiautomator2库自带的输入法。部分型号手机无法使用命令切换。
    5.2 手机型号&微信版本
    手机型号 安卓型号 微信版本 切换输入法 识别组件
    oppo A5 8.1.0 7.0.9 否 部分不能识别
    搜索结果界面无法识别
    oppo A5 8.1.0 7.0.10 否 可
    红米6 9 7.0.12 可 可
    红米6 8.1.0 7.0.6 可 部分不能识别
    搜索结果界面无法识别
    红米note5 9 7.0.10 可 可
    5.3 目前结论
    组件识别问题:需要微信版本7.0.10以上
    输入法切换问题:红米系列手机可自动切换输入法; oppo A5不能自动切换输入法,需要提示用户手动切换

瑞0908
318 声望74 粉丝

一个一个解决


引用和评论

0 条评论