0%

微信公众号文章爬取之:微信自动化

本文转载自:陈文管的博客-微信公众号文章爬取之:微信自动化 本文内容详细介绍微信公众号历史文章自动化浏览脚本的实现,配合服务端对公众号文章数据爬取来实现微信公众号文章数据的采集。服务端爬取实现见:微信公众号文章爬取之:服务端数据采集。 背景:在团队的学习方面需要每周收集开发方面的博客文章,汇总输出每周的技术周报。周报小组成员收集的文章大多数是来自微信公众号,公众号的内容相对网页博客内容质量还是比较高的。既然数据的来源是确定的,收集汇总的流程是确定的,那么就把这个流程自动化,把人工成本降低到0。

一、方案选取

1、数据源选取

主要是爬取的数据来源选取,网上资料看的较多是爬取搜狗微信的内容,但是第三方平台(包括新榜、清博等 )的公众号文章数据更新做不到实时,而且数据也不全,还要和各种反爬措施斗智斗勇,浪费时间精力的事情划不来。最直接的方式当然是直接爬取微信公众号历史文章里面的内容。 在前期预研主要参考的资料是知乎专栏:微信公众号内容的批量采集与应用 。 上面的方案是借助阿里巴巴开源的AnyProxy工具,AnyProxy作为一个中间人在微信客户端和服务器之间的交互过程中做数据截获和转发。获取到公众号文章的实际链接地址之后转发到自己的服务器进行保存,整个数据采集的自动化程度较大取决于微信客户端的自动化浏览实现。

2、自动化方案选取

如果是比较简单的安卓应用自动化操作的实现,一般直接使用AccessibilityService就行,UIAutomator也是基于AccessibilityService来实现的,但是AccessibilityService不支持WebView的操作,因为微信公众号历史文章页面是用WebView来加载的,要实现自动化必须同时支持安卓原生和WebView两个上下文环境的操作。 经过现有的几个自动化方案实现对比,最便利又具备极佳扩展性的方案就是使用Appium

  • Appium是开源的移动端自动化测试框架;
  • 支持Native App、Hybird App、Web App;
  • 支持Android、iOS、Firefox OS;
  • 跨平台,可以在Mac,Windows以及Linux系统上;
  • 用Appium自动化测试不需要重新编译App;
  • 支持Java、python、ruby、C#、Objective C、PHP等主流语言;

更多资料参考:Android自动化测试框架

公众号文章爬取系统架构图

公众号文章爬取系统架构图

二、Appium安装配置(Mac)

Appium程序的安装,我这边不是使用brew命令安装的方式,直接从BitBucket下载Appium安装包,也可以从Github上下载。这边使用BitBucket 1.5.3版本。 Appium1.5.0之后的版本,需要在终端安装doctor,在终端输入命令:npm install -g appium-doctor,安装完毕之后,在终端输入命令:appium-doctor,查看所需的各个配置是否都已经安装配置完毕。下面是我这边在终端输出得到的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
info AppiumDoctor Appium Doctor v.1.4.3
info AppiumDoctor ### Diagnostic starting ###
info AppiumDoctor  ✔ The Node.js binary was found at: /Users/chenwenguan/.nvm/versions/node/v8.9.3/bin/node
info AppiumDoctor  ✔ Node version is 8.9.3
info AppiumDoctor  ✔ Xcode is installed at: /Library/Developer/CommandLineTools
info AppiumDoctor  ✔ Xcode Command Line Tools are installed.
info AppiumDoctor  ✔ DevToolsSecurity is enabled.
info AppiumDoctor  ✔ The Authorization DB is set up properly.
WARN AppiumDoctor  ✖ Carthage was NOT found!
info AppiumDoctor  ✔ HOME is set to: /Users/chenwenguan
WARN AppiumDoctor  ✖ ANDROID_HOME is NOT set!
WARN AppiumDoctor  ✖ JAVA_HOME is NOT set!
WARN AppiumDoctor  ✖ adb could not be found because ANDROID_HOME is NOT set!
WARN AppiumDoctor  ✖ android could not be found because ANDROID_HOME is NOT set!
WARN AppiumDoctor  ✖ emulator could not be found because ANDROID_HOME is NOT set!
WARN AppiumDoctor  ✖ Bin directory for $JAVA_HOME is not set
info AppiumDoctor ### Diagnostic completed, 7 fixes needed. ###
info AppiumDoctor 
info AppiumDoctor ### Manual Fixes Needed ###
info AppiumDoctor The configuration cannot be automatically fixed, please do the following first:
WARN AppiumDoctor - Please install Carthage. Visit https://github.com/Carthage/Carthage#installing-carthage for more information.
WARN AppiumDoctor - Manually configure ANDROID_HOME.
WARN AppiumDoctor - Manually configure JAVA_HOME.
WARN AppiumDoctor - Manually configure ANDROID_HOME and run appium-doctor again.
WARN AppiumDoctor - Add '$JAVA_HOME/bin' to your PATH environment
info AppiumDoctor ###
info AppiumDoctor 
info AppiumDoctor Bye! Run appium-doctor again when all manual fixes have been applied!
info AppiumDoctor

上面打叉的都是没配置好的,在终端输入命令安装Carthage :brew install carthage

输入命令查看JDK安装路径:/usr/libexec/java_home -V

1
2
1.8.0_60, x86_64: "Java SE 8" /Library/Java/JavaVirtualMachines/jdk1.8.0_60.jdk/Contents/Home
/Library/Java/JavaVirtualMachines/jdk1.8.0_60.jdk/Contents/Home

需要把上面的路径配置到环境变量中,ANDROID_HOME就是Android SDK的安装路径。

输入命令打开配置文件: open ~/.bash_profile,在文件中添加如下内容:

1
2
3
export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_60.jdk/Contents/Home  
export PATH=$JAVA_HOME/bin:$PATH  
export ANDROID_HOME=/Users/chenwenguan/Library/Android/sdk

输入命令让配置立即生效:source ~/.bash_profile

更多安装配置资料可参考:Mac上搭建Appium环境过程以及遇到的问题

TIPS

在首次使用Appium时可能会出现一个错误:

1
Could not detect Mac OS X Version from sw_vers output: '10.13.2

在终端输入命令:

1
grep -rl "Could not detect Mac OS X Version from sw_vers output" /Applications/Appium.app/

得到如下结果:

1
2
3
4
/Applications/Appium.app//Contents/Resources/node_modules/appium-support/lib/system.js
/Applications/Appium.app//Contents/Resources/node_modules/appium-support/build/lib/system.js
/Applications/Appium.app//Contents/Resources/node_modules/appium/node_modules/appium-support/lib/system.js
/Applications/Appium.app//Contents/Resources/node_modules/appium/node_modules/appium-support/build/lib/system.js

打开上面四个路径下的文件,添加当前的Appium版本参数,具体内容可参考:在Mac OS 10.12 上安装配置appium

三、具体代码实现

预研资料主要参考这篇博文:Appium 微信 webview 的自动化技术 自动化实现的原理就是通过ID或者模糊匹配找到相应的控件,之后对这个控件做点击、滑动等操作。如果要对微信WebView做自动化,必须能够获取到WebView里面的对象,如果是Android原生的控件可以通过AndroidStudio里面的Android Device Monitor来查看控件的id、类名等各种属性。

1、Android原生控件属性参数值的获取

在AndroidStudio打开Monitor工具:Tools->Android->Android Device Monitor 按照下图的步骤查看控件的ID等属性,后续在代码实现中会用到。

Android Device Monitor

Android Device Monitor

2、WebView属性参数值的获取

如果是在安卓真机上,需要打开WebView的调试模式才能读取到WebView的各个属性,在微信里面可以在任意聊天窗口输入debugx5.qq.com,这是微信x5内核调试页面,在信息模块中勾选打开TBS内核Inspector调试功能。

微信X5内核调试页面

微信X5内核调试页面

之后还要在真机上安装Chrome浏览器,如果是在虚拟机上无需做此操作。 接下来在Chrome浏览器中输入:chrome://inspect ,我这边使用的是虚拟机,真机上也一样,进入到公众号历史文章页面,这边就会显示相应可检视的WebView页面,点击inspect,进入到Developer Tools页面。

chrome inspect页面

chrome inspect页面

如果进入到Developer Tools页面显示一片空白,是因为chrome inspect需要加载 https://chrome-devtools-frontend.appspot.com 上的资源,所以需要翻墙,把appstop.com 加入翻墙代理白名单,或者直接全局应用翻墙VPN,具体可参考:使用chrome remote debug时打开inspect时出现一片空白 下面是美团技术团队历史文章列表的详细结构信息,具体的文章列表项在weui-panel->weui-panel__bd appmsg_history_container->js_profile_history_container->weui_msg_card_list路径下。

Chrome inspect查看WebView详细内容

Chrome inspect查看WebView详细内容

继续展开节点查看文章详细结构信息,这边可以看到每篇文章的ID都是以“WXAPPMSG100″开头的,类名都是“weui_media_box”开头,一开始的实现是通过模糊匹配ID来查找历史文章列表项数据,但在测试过程中出现来一个异常,后来发现,如果是纯文本类型的文章,也就是只有一段话的文章,它是没有ID的,所以不能通过ID来模糊匹配。

公众号历史文章列表项详细结构

公众号历史文章列表项详细结构

之后就把现有的四种公众号文章类型都找来出来,找它们的共性,虽然ID不一定有,但是class类型值一定有,四种类型值如下,这样就可以通过class类型值来匹配查找数据了。

1
2
3
4
 * weui_media_box appmsg js_appmsg : 文章
* weui_media_box text js_appmsg : 文本
* weui_media_box img js_appmsg : 图片
* weui_media_box appmsg audio_msg_primary js_appmsg playing : 语音

3、具体代码实现

整体自动化是按照如下顺序:通讯录页面->点击公众号进入公众号列表页面->公众号列表项选择一个点击->公众号页面->公众号消息页面->点击“全部消息”进入公众号历史文章页面->根据设置的时间类型(一周之内、一个月之内、一年之内或者全部)逐个点击历史文章列表项,完毕之后返回公众号列表页面,继续下一个公众号浏览的操作;

1)初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private static AndroidDriver getDriver() throws MalformedURLException {
        DesiredCapabilities capability = new DesiredCapabilities();
        capability.setCapability("platformName", "emulator-5554");
        capability.setCapability("platformVersion", "4.4.4");
        capability.setCapability("deviceName", "MuMu");
        /**
         * 真机上platformName使用"Android"
         */
        /*
        capability.setCapability("platformName", "Android");
        capability.setCapability("platformVersion", "6.0");
        capability.setCapability("deviceName", "FRD-AL00");
        */
        capability.setCapability("unicodeKeyboard","True");
        capability.setCapability("resetKeyboard","True");
        capability.setCapability("app", "");
        capability.setCapability("appPackage", "com.tencent.mm");
        capability.setCapability("appActivity", ".ui.LauncherUI");
        capability.setCapability("fastReset", false);
        capability.setCapability("fullReset", false);
        capability.setCapability("noReset", true);
        capability.setCapability("newCommandTimeout", 2000);
        /**
         * 必须加这句,否则webView和native来回切换会有问题
         */
        capability.setCapability("recreateChromeDriverSessions", true);
        /**
         * 关键是加上这段
         */
        ChromeOptions options = new ChromeOptions();
        options.setExperimentalOption("androidProcess", "com.tencent.mm:tools");
        capability.setCapability(ChromeOptions.CAPABILITY, options);
        String url = "http://127.0.0.1:4723/wd/hub";
        mDriver = new AndroidDriver<>(new URL(url), capability);
        return mDriver;
    }

如果是虚拟机则platformName使用具体的虚拟机名称,如果是真机使用“Android”,platformVersion和deviceName可以使用工程安装APK之后查看详细信息,对应的参数就是显示的系统版本和设备名称。

设备信息

设备信息

URL参数是在Appium里面设置的,确保”http://127.0.0.1:4723/wd/hub”字符串中的服务器地址和端口与Appium设置一致。

Appium URL参数设置

Appium URL参数设置

2)列表滑动和元素获取

不管是WebView还是Android原生ListView的滑动都需要在Android原生上下文环境下操作driver.context(“NATIVE_APP”); 滑动操作都可以通过如下代码实现,通过滑动前后的PageSource对比可以知道列表是否已经滑动到底部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
     /**
     * 滑动列表加载下一页数据
     *
     * @param driver
     * @return
     * @throws InterruptedException
     */
    private static boolean isScrollToBottom(AndroidDriver driver) throws InterruptedException {
        int width = driver.manage().window().getSize().width;
        int height = driver.manage().window().getSize().height;
        String beforeswipe = driver.getPageSource();
        driver.swipe(width / 2, height * 3 / 4, width / 2, height / 4, 1000);
        /**
         * 设置8s超时,网络较差情况下时间过短加载不出内容
         */
        mDriver.manage().timeouts().implicitlyWait(8000, TimeUnit.MILLISECONDS);
        String afterswipe = driver.getPageSource();
        /**
         * 已经到底部
         */
        if (beforeswipe.equals(afterswipe)) {
            return true;
        }
        return false;
    }

TIPS: 如果是Android原生的ListView读取到的数据是在屏幕上显示的数据,超过屏幕的数据是获取不到的,如果是WebView的列表获取的数据是所有已加载的数据,不管是否在屏幕显示范围内。 获取公众号列表数据逻辑代码如下,”com.tencent.mm:id/a0y”是具体的公众号名称TextView的ID。

1
List<WebElement> elementList = mDriver.findElementsById("com.tencent.mm:id/a0y");

获取历史文章列表数据逻辑代码如下,div是节点,上面说到公众号四种类型的文章都是以’weui_media_box’类名开头的,通过模糊匹配class类名以’weui_media_box’开始的元素来过滤出所有的公众号文章列表项。

1
List<WebElement> msgHistory = driver.findElements(By.xpath("//div[starts-with(@class,'weui_media_box')]"));
3)元素定位方式

如果一定需要模糊匹配就使用By.xpath()的方式,因为Android APK应用如果有增加或减少了布局字符串资源或者控件,编译之后生成的ID可能会不一样,这边说的ID是指通过Android Device Monitor查看的布局ID,不是实际的布局代码控件id,布局控件id除非命名改动,否则不会变化。所以不同版本的微信客户端生成的ID很可能会不一样,如果要批量实现自动化最好使用模糊匹配的方式,但By.xpath()方式查找定位元素是遍历页面的所有元素,会比较耗时,也容易出现异常。 在测试过程中执行

1
driver.findElement(By.xpath("//android.widget.ImageView[@content-desc='返回']")).click();

时候经常出现如下错误,改为

1
driver.findElementById("com.tencent.mm:id/ht").click();

异常消失,猜测原因就是因为By.xpath()方法查找比较耗时导致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
org.openqa.selenium.WebDriverException: An unknown server-side error occurred while processing the command. (WARNING: The server did not provide any stacktrace information)
Command duration or timeout: 1.41 seconds
Build info: version: '2.44.0', revision: '76d78cf', time: '2014-10-23 20:02:37'
System info: host: 'wenguanchen-MacBook-Pro.local', ip: '30.85.214.6', os.name: 'Mac OS X', os.arch: 'x86_64', os.version: '10.13.2', java.version: '1.8.0_112-release'
Driver info: io.appium.java_client.android.AndroidDriver
Capabilities [{appPackage=com.tencent.mm, noReset=true, dontStopAppOnReset=true, deviceName=emulator-5554, fullReset=false, platform=LINUX, deviceUDID=emulator-5554, desired={app=, appPackage=com.tencent.mm, recreateChromeDriverSessions=true, noReset=true, dontStopAppOnReset=true, deviceName=MuMu, fullReset=false, appActivity=.ui.LauncherUI, platformVersion=4.4.4, automationName=Appium, unicodeKeyboard=true, fastReset=false, chromeOptions={args=[], extensions=[], androidProcess=com.tencent.mm:tools}, platformName=Android, resetKeyboard=true}, platformVersion=4.4.4, webStorageEnabled=false, automationName=Appium, takesScreenshot=true, javascriptEnabled=true, unicodeKeyboard=true, platformName=Android, resetKeyboard=true, app=, networkConnectionEnabled=true, recreateChromeDriverSessions=true, warnings={}, databaseEnabled=false, appActivity=.ui.LauncherUI, locationContextEnabled=false, fastReset=false, chromeOptions={args=[], extensions=[], androidProcess=com.tencent.mm:tools}}]
Session ID: 592813d6-7c6e-4a3c-8183-e5f93d1d3bf0
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at org.openqa.selenium.remote.ErrorHandler.createThrowable(ErrorHandler.java:204)
at org.openqa.selenium.remote.ErrorHandler.throwIfResponseFailed(ErrorHandler.java:156)
at org.openqa.selenium.remote.RemoteWebDriver.execute(RemoteWebDriver.java:599)
at io.appium.java_client.DefaultGenericMobileDriver.execute(DefaultGenericMobileDriver.java:27)
at io.appium.java_client.AppiumDriver.execute(AppiumDriver.java:1)
at io.appium.java_client.android.AndroidDriver.execute(AndroidDriver.java:1)
at org.openqa.selenium.remote.RemoteWebDriver.findElement(RemoteWebDriver.java:352)
at org.openqa.selenium.remote.RemoteWebDriver.findElementByXPath(RemoteWebDriver.java:449)
at io.appium.java_client.DefaultGenericMobileDriver.findElementByXPath(DefaultGenericMobileDriver.java:99)
at io.appium.java_client.AppiumDriver.findElementByXPath(AppiumDriver.java:1)
at io.appium.java_client.android.AndroidDriver.findElementByXPath(AndroidDriver.java:1)
at org.openqa.selenium.By$ByXPath.findElement(By.java:357)
at org.openqa.selenium.remote.RemoteWebDriver.findElement(RemoteWebDriver.java:344)
at io.appium.java_client.DefaultGenericMobileDriver.findElement(DefaultGenericMobileDriver.java:37)
at io.appium.java_client.AppiumDriver.findElement(AppiumDriver.java:1)
at io.appium.java_client.android.AndroidDriver.findElement(AndroidDriver.java:1)
at com.example.AppiumAutoScan.getArticleDetail(AppiumAutoScan.java:335)
at com.example.AppiumAutoScan.launchBrowser(AppiumAutoScan.java:96)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:45)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:42)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:68)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:47)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
at org.junit.runners.ParentRunner.run(ParentRunner.java:300)
at org.junit.runner.JUnitCore.run(JUnitCore.java:157)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:117)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:42)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:262)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:84)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

如果容易出现如下异常,则是因为页面的内容还未加载完毕,可以通过

1
mDriver.manage().timeouts().implicitlyWait(8000, TimeUnit.MILLISECONDS);

方法设置下超时等待时间,等待页面内容加载完毕,具体超时时间可自己调试看看设置一个合适的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
org.openqa.selenium.StaleElementReferenceException: stale element reference: element is not attached to the page document
  (Session info: webview=33.0.0.0)
  (Driver info: chromedriver=2.20.353124 (035346203162d32c80f1dce587c8154a1efa0c3b),platform=Mac OS X 10.13.2 x86_64) (WARNING: The server did not provide any stacktrace information)
Command duration or timeout: 2.41 seconds
For documentation on this error, please visit: http://seleniumhq.org/exceptions/stale_element_reference.html
Build info: version: '2.44.0', revision: '76d78cf', time: '2014-10-23 20:02:37'
System info: host: 'wenguanchen-MacBook-Pro.local', ip: '30.85.214.81', os.name: 'Mac OS X', os.arch: 'x86_64', os.version: '10.13.2', java.version: '1.8.0_112-release'
Driver info: io.appium.java_client.android.AndroidDriver
Capabilities [{appPackage=com.tencent.mm, noReset=true, dontStopAppOnReset=true, deviceName=emulator-5554, fullReset=false, platform=LINUX, deviceUDID=emulator-5554, desired={app=, appPackage=com.tencent.mm, recreateChromeDriverSessions=true, noReset=true, dontStopAppOnReset=true, deviceName=MuMu, fullReset=false, appActivity=.ui.LauncherUI, platformVersion=4.4.4, automationName=Appium, unicodeKeyboard=true, fastReset=false, chromeOptions={args=[], extensions=[], androidProcess=com.tencent.mm:tools}, platformName=Android, resetKeyboard=true}, platformVersion=4.4.4, webStorageEnabled=false, automationName=Appium, takesScreenshot=true, javascriptEnabled=true, unicodeKeyboard=true, platformName=Android, resetKeyboard=true, app=, networkConnectionEnabled=true, recreateChromeDriverSessions=true, warnings={}, databaseEnabled=false, appActivity=.ui.LauncherUI, locationContextEnabled=false, fastReset=false, chromeOptions={args=[], extensions=[], androidProcess=com.tencent.mm:tools}}]
Session ID: b5e933e1-0ddf-421d-9144-e423a7bb25b1
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at org.openqa.selenium.remote.ErrorHandler.createThrowable(ErrorHandler.java:204)
at org.openqa.selenium.remote.ErrorHandler.throwIfResponseFailed(ErrorHandler.java:156)
at org.openqa.selenium.remote.RemoteWebDriver.execute(RemoteWebDriver.java:599)
at io.appium.java_client.DefaultGenericMobileDriver.execute(DefaultGenericMobileDriver.java:27)
at io.appium.java_client.AppiumDriver.execute(AppiumDriver.java:1)
at io.appium.java_client.android.AndroidDriver.execute(AndroidDriver.java:1)
at org.openqa.selenium.remote.RemoteWebElement.execute(RemoteWebElement.java:268)
at io.appium.java_client.DefaultGenericMobileElement.execute(DefaultGenericMobileElement.java:27)
at io.appium.java_client.MobileElement.execute(MobileElement.java:1)
at io.appium.java_client.android.AndroidElement.execute(AndroidElement.java:1)
at org.openqa.selenium.remote.RemoteWebElement.getText(RemoteWebElement.java:152)
at com.example.AppiumAutoScan.getArticleDetail(AppiumAutoScan.java:294)
at com.example.AppiumAutoScan.launchBrowser(AppiumAutoScan.java:110)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:45)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:42)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:68)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:47)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
at org.junit.runners.ParentRunner.run(ParentRunner.java:300)
at org.junit.runner.JUnitCore.run(JUnitCore.java:157)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:117)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:42)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:262)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:84)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

更多元素定位方法可参考官网:

1
[http://selenium-python.readthedocs.io/locating-elements.html#locating-by-id](http://selenium-python.readthedocs.io/locating-elements.html#locating-by-id)
4)chromedriver相关问题

在2017年6月微信热更新升级了X5内核之后,真机上切换到WebView上下文环境就出问题了,具体见这篇博文的评论Appium 微信 webview 的自动化技术Appium 微信小程序,driver.context (“WEBVIEW_com.tencent.mm:tools”) 切换 webview 报错 看评论是通过降低chromedriver版本的方式来避免异常,但是在试过降低版本到20之后还是不行,更新到最新的版本也不行,于是放弃在真机上实现自动化,在模拟器中跑起来的速度也还可以接受。 在真机上跑的时候,切换到WebView上下文环境,程序控制台输出no such session异常,异常信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
org.openqa.selenium.remote.SessionNotFoundException: no such session
  (Driver info: chromedriver=2.21.371459 (36d3d07f660ff2bc1bf28a75d1cdabed0983e7c4),platform=Mac OS X 10.13.2 x86_64) (WARNING: The server did not provide any stacktrace information)
Command duration or timeout: 14 milliseconds
Build info: version: '2.44.0', revision: '76d78cf', time: '2014-10-23 20:02:37'
System info: host: 'wenguanchen-MacBook-Pro.local', ip: '192.168.1.102', os.name: 'Mac OS X', os.arch: 'x86_64', os.version: '10.13.2', java.version: '1.8.0_112-release'
Driver info: io.appium.java_client.android.AndroidDriver
Capabilities [{appPackage=com.tencent.mm, noReset=true, dontStopAppOnReset=true, deviceName=55CDU16C07009329, fullReset=false, platform=LINUX, deviceUDID=55CDU16C07009329, desired={app=, appPackage=com.tencent.mm, recreateChromeDriverSessions=True, noReset=true, dontStopAppOnReset=true, deviceName=FRD-AL00, fullReset=false, appActivity=.ui.LauncherUI, platformVersion=6.0, automationName=Appium, unicodeKeyboard=true, fastReset=false, chromeOptions={args=[], extensions=[], androidProcess=com.tencent.mm:tools}, platformName=Android, resetKeyboard=true}, platformVersion=6.0, webStorageEnabled=false, automationName=Appium, takesScreenshot=true, javascriptEnabled=true, unicodeKeyboard=true, platformName=Android, resetKeyboard=true, app=, networkConnectionEnabled=true, recreateChromeDriverSessions=True, warnings={}, databaseEnabled=false, appActivity=.ui.LauncherUI, locationContextEnabled=false, fastReset=false, chromeOptions={args=[], extensions=[], androidProcess=com.tencent.mm:tools}}]
Session ID: e2e50190-398b-4fa2-bc66-db1097201e3f
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at org.openqa.selenium.remote.ErrorHandler.createThrowable(ErrorHandler.java:204)
at org.openqa.selenium.remote.ErrorHandler.throwIfResponseFailed(ErrorHandler.java:162)
at org.openqa.selenium.remote.RemoteWebDriver.execute(RemoteWebDriver.java:599)
at io.appium.java_client.DefaultGenericMobileDriver.execute(DefaultGenericMobileDriver.java:27)
at io.appium.java_client.AppiumDriver.execute(AppiumDriver.java:272)
at org.openqa.selenium.remote.RemoteWebDriver.getPageSource(RemoteWebDriver.java:459)
at com.example.AppiumAutoScan.getArticleDetail(AppiumAutoScan.java:238)
at com.example.AppiumAutoScan.launchBrowser(AppiumAutoScan.java:78)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:45)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:42)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:68)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:47)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
at org.junit.runners.ParentRunner.run(ParentRunner.java:300)
at org.junit.runner.JUnitCore.run(JUnitCore.java:157)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:117)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:42)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:262)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:84)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

在Appium端输出的异常信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[debug] [AndroidDriver] Found webviews: ["WEBVIEW_com.tencent.mm:tools","WEBVIEW_com.tencent.mm"]
[debug] [AndroidDriver] Available contexts: ["NATIVE_APP","WEBVIEW_com.tencent.mm:tools","WEBVIEW_com.tencent.mm"]
[debug] [AndroidDriver] Connecting to chrome-backed webview context 'WEBVIEW_com.tencent.mm:tools'
[debug] [Chromedriver] Changed state to 'starting'
[Chromedriver] Set chromedriver binary as: /Applications/Appium.app/Contents/Resources/node_modules/appium/node_modules/appium-android-driver/node_modules/appium-chromedriver/chromedriver/mac/chromedriver
[Chromedriver] Killing any old chromedrivers, running: pkill -15 -f "/Applications/Appium.app/Contents/Resources/node_modules/appium/node_modules/appium-android-driver/node_modules/appium-chromedriver/chromedriver/mac/chromedriver.*--port=9515"
[Chromedriver] No old chromedrivers seemed to exist
[Chromedriver] Spawning chromedriver with: /Applications/Appium.app/Contents/Resources/node_modules/appium/node_modules/appium-android-driver/node_modules/appium-chromedriver/chromedriver/mac/chromedriver --url-base=wd/hub --port=9515 --adb-port=5037
[Chromedriver] [STDOUT] Starting ChromeDriver 2.21.371459 (36d3d07f660ff2bc1bf28a75d1cdabed0983e7c4) on port 9515
Only local connections are allowed.
[JSONWP Proxy] Proxying [GET /status] to [GET http://127.0.0.1:9515/wd/hub/status] with no body
[Chromedriver] [STDERR] [warn] kq_init: detected broken kqueue; not using.: Undefined error: 0
[JSONWP Proxy] Got response with status 200: "{\"sessionId\":\"\",\"stat...
[JSONWP Proxy] Proxying [POST /session] to [POST http://127.0.0.1:9515/wd/hub/session] with body: {"desiredCapabilities":{"ch...
[JSONWP Proxy] Got response with status 200: {"sessionId":"166cee263fc87...
[debug] [Chromedriver] Changed state to 'online'
[MJSONWP] Responding to client with driver.setContext() result: null
[HTTP] <-- POST /wd/hub/session/82b9d81c-f725-473d-8d55-ddbc1f92c100/context 200 903 ms - 76 
[HTTP] --> GET /wd/hub/session/82b9d81c-f725-473d-8d55-ddbc1f92c100/context {}
[MJSONWP] Calling AppiumDriver.getCurrentContext() with args: ["82b9d81c-f725-473d-8d55-d...
[MJSONWP] Responding to client with driver.getCurrentContext() result: "WEBVIEW_com.tencent.mm:tools"
[HTTP] <-- GET /wd/hub/session/82b9d81c-f725-473d-8d55-ddbc1f92c100/context 200 2 ms - 102 
[HTTP] --> GET /wd/hub/session/82b9d81c-f725-473d-8d55-ddbc1f92c100/source {}
[MJSONWP] Driver proxy active, passing request on via HTTP proxy
[JSONWP Proxy] Proxying [GET /wd/hub/session/82b9d81c-f725-473d-8d55-ddbc1f92c100/source] to [GET http://127.0.0.1:9515/wd/hub/session/166cee263fc8757cbcb5576a52f7229e/source] with body: {}
[JSONWP Proxy] Got response with status 200: "{\"sessionId\":\"166cee263...
[JSONWP Proxy] Replacing sessionId 166cee263fc8757cbcb5576a52f7229e with 82b9d81c-f725-473d-8d55-ddbc1f92c100
[HTTP] <-- GET /wd/hub/session/82b9d81c-f725-473d-8d55-ddbc1f92c100/source 200 8 ms - 220

如果要替换chromedriver的版本,可以从Appium上输出的Log信息找到chromedriver的路径,在终端依次执行如下命令打开chromedriver所在的文件夹。

1
2
cd /Applications/Appium.app/Contents/Resources/node_modules/appium/node_modules/appium-android-driver/node_modules/appium-chromedriver/chromedriver/mac/
open .

相应的chromedriver和Chrome版本对应信息和下载地址可以参考: selenium之 chromedriver与chrome版本映射表

5)程序使用的JAR包

自动化脚本程序要跑起来需要两个压缩包,java-client-3.1.0.jar 和 selenium-server-standalone-2.44.0.jar ,试过使用这两个JAR包的最新版本,会有一些奇奇怪怪的问题,这两个版本的JAR包够用了。 java-client-3.1.0.jar 可以从Appium官网下载:

1
[http://appium.io/downloads.html](http://appium.io/downloads.html)

selenium-server-standalone-2.44.0.jar 可以从selenium官网下载:

1
[http://selenium-release.storage.googleapis.com/index.html](http://selenium-release.storage.googleapis.com/index.html)
6)虚拟机

我这边使用的是网易MuMu虚拟机,基于Android 4.4.4平台,在我自己的Mac上跑着没问题,同一个版本安装到公司的Mac上就跑不起来,一打开就崩。后面虚拟机自动升级到了Android6.0.1,脚本跑了就有异常,而且每次打开的时候经常卡死在加载页面,system so库报异常。所以最好还是基于Android4.X的版本上运行脚本,Mac上没有一个通用稳定的虚拟机,自己下几个看看是否能用,个人测试各类型的虚拟机结果如下: 1)网易MuMu:在Mac上还是比较好用的,但是最新的版本是6.0.1,初始化经常卡死,无法回退到4.4.4平台版本,脚本在Android6.0平台上切换到WebView的上下文环境异常,升级ChromeDriver版本和Appium版本也无法解决此问题。 2)GenyMotion:微信安装之后无法打开,一直闪退,页面滑动在Mac上巨难操作。 3)天天模拟器:下载的DMG安装文件根本无法打开。 4)夜神模拟器:还是比较好用的,但是Appium adb无法连上虚拟机,从Log来看一直在重启adb, 最后程序中断。 5)逍遥安卓:没有Mac版本。 6)BlueStack:无法安装,安装过程中异常退出,多次重试还是一样。 综上,如果是在Mac上运行虚拟机,目前测试有效的是网易MuMu 基于Android 4.4.4 平台的版本,其他版本和虚拟机都有各种问题。 另:附上Android WebView 历史版本下载地址(需要翻墙):

1
[https://cn.apkhere.com/app/com.google.android.webview](https://cn.apkhere.com/app/com.google.android.webview)

WebView 和对应的ChromeDriver版本见Appium GitHub chromedriver说明文档:

1
[https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/web/chromedriver.md](https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/web/chromedriver.md)
7)编译IDE

不做Android开发的可以下载Eclipse IDE,在Eclipse下运行Java程序还比较方便,拷贝工程源码中的三份文件即可

1
2
3
java-client-3.1.0.jar 
selenium-server-standalone-2.44.0.jar  
AppiumWeChatAuto/appiumauto/src/main/java/com/example/AppiumAutoScan.java

Eclipse IDE下载地址:

1
[http://www.eclipse.org/downloads/packages/](http://www.eclipse.org/downloads/packages/)

Java版本和对应的Eclipse IDE版本参考:

1
[http://wiki.eclipse.org/Eclipse/Installation](http://wiki.eclipse.org/Eclipse/Installation)
8)GitHub工程源码

源码GitHub地址:

1
[https://github.com/wenguan0927/AppiumWeChatAuto](https://github.com/wenguan0927/AppiumWeChatAuto)

运行Android工程查看设备信息的时候Edit Configurations切换到app,运行自动化脚本的时候切换到AppiumAutoScan。支持按最近一周,一个月,一年或爬取所有历史文章,checkTimeLimit()传入不同限制时间类型的参数即可。

四、参考资料

Appium 官方文档:http://appium.io/docs/cn/about-appium/intro/

Appium 常用API

Appium自动化测试–使用Chrome调试模式获取App混合应用H5界面元素

Appium 微信 webview 的自动化技术

Appium Girls 学习手册

Appium:轻松玩转app+webview混合应用自动化测试

Appium 微信小程序,driver.context (“WEBVIEW_com.tencent.mm:tools”) 切换 webview 报错

Appium 事件监听

妙用AccessibilityService黑科技实现微信自动加好友拉人进群聊

Appium自动化测试Android

Windows下部署Appium教程(Android App自动化测试框架搭建)

微信、手Q、Qzone之x5内核inspect调试解决方案

selenium之 chromedriver与chrome版本映射表

(Android开发自测)在Mac OS 10.12 上安装配置appium

辅助功能 AccessibilityService笔记