基于 Appium 的 App 爬取实战

除了运行 Appium 的基本条件外,还要一个日志输出库

安装:  pip install loguru

思路分析

首先我们观察一下整个 app5 的交互流程,其首页分条显示了电影数据, 每个电影条目都包括封面,标题, 类别和评分 4 个内容, 点击一个电影条目, 就可以看到这个电影的详细介绍,包括标题,类别,上映时间,评分,时长,电影简介等内容

可见详情页远比首页内容丰富, 我们需要依次点击每个电影条目,抓取看到的所有内容,把所有电影条目的信息都抓取下来后回退到首页

另外,首页一开始只显示 10 个电影条目,需要上拉才能显示更多数据,一共 100 条数据,所以为了爬取所有数据,我们需要在适当的时候模拟手机上拉的操作,已加载更多的数据

综上,这里总结出基本爬取流程

遍历现有的电影条目,依次模拟点击每个电影条目,进入详情页

爬取详情页的数据,爬取完毕后模拟点击回退按钮的操作,返回首页

当首页的所有电影条目即将爬取完毕时,模拟上拉操作,加载更多数据

在爬取过程中,将已经爬取的数据记录下来,以免重复爬取

100 条数据爬取完毕后,终止爬取

基本实现

在编写代码的过程中,我们用 Appium 观察现有的  App 的源代码,以便编写节点的提取规则。 首先启动 Appium 服务,然后启动 Session , 打开电脑端的调试窗口

首先观察一些首页各个电影条目对应的 UI 树是怎样的。 通过观察可以发现,每个电影条目都是一个 android.widget.LinearLayout 节点, 该节点带有一个属性 resoutce-id 为 com.goldze.mvvmhabit:id/item , 条目内部的标题是一个 android.widget.TextView 节点,该节点带有一个属性 resource-id , 属性值是 com.goldze.mvvmhabit:id/tv_title, 我们可以选中所有的电影条目节点,同时记录电影标题去重

去重的目的: 因为对已经被渲染出来但是没有呈现在屏幕上的节点,我们是无法获取其信息的。在不断上拉爬取的过程中,我们同一时刻只能获取屏幕中能看到的所有电影条目的节点,被滑动出屏幕外的节点已经获取不到了。所有需要记录一下已经爬取的电影条目节点,以便下次滑动完毕后可以接着上一次爬取。由于此案例中的电影标题不存在重复,因此我们就用它来实现记录和去重

接下来做一些初始化声明

from appium import webdriver
from appium.options.android import UiAutomator2Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import NoSuchElementExceptionSERVER = 'http://localhost:4723/wd/hub'
DESIRED_CAPABILITIES = {'platformName': 'Android','deviceName': 'LIO_AN00','appPackage': 'com.goldze.mvvmhabit','appActivity': '.ui.MainActivity','noReset': True
}
PACKAGE_NAME = DESIRED_CAPABILITIES['appPackage']
TOTAL_NUMBER = 100

这里我们首先声明了 SERVER 变量, 即 Appium 在本地启动的服务地址。 接着声明了 DESITED_CAPABILITIES , 这就是 Appium 启动示例 App 的配置参数,其中 deviceName 需要更改成自己手机的 model 名称, 可以使用 adb devices –l  通过 cmd 获取。另外,这里额外声明了一个变量 PACKAGE_NAME 即包名, 这是为后续编写获取节点的逻辑准备的。 最后声明 TOTAL_NUMBER 为 100 , 代表电影条目的总数为 100 , 之后以此为判断终止爬取

接下来我么声明 driver 对象, 并初始化一些必要的对象和变量

driver = webdriver.Remote(SERVER, options=UiAutomator2Options().load_capabilities(DESIRED_CAPABILITIES))
wait = WebDriverWait(driver, 30)
window_size = driver.get_window_size()
window_width, window_height = window_size.get('width'), window_size.get('height')

这里的 wait 变量就是一个 WebDriverWait 对象, 调用它的 until 方法可以实现如果查找到目标接节点就立即返回,如果等待 30 秒还查找不到目标节点就抛出异常。 我们还声明了 window_width, window_height 变量, 分别代表屏幕的宽高

初始化工作完成,下面爬取首页的所有电影条目

def scrape_index():items = wait.until(EC.presence_of_all_elements_located((By.XPATH, f'//android.widget.LinearLayout[@resource-id="{PACKAGE_NAME}:id/item"]')))return items

这里实现了一个 scrape_index 方法, 使用 XPath 选择对应的节点, 开头的 // 代表匹配根节点的所有子孙节点,即所有符合后面条件的节点都会被筛选出来, 这里对节点名称 android.widget.LinearLayout 和 属性 resource-id 进行了组合匹配。 在外层调用了 wait 变量的 until 方法,最后的结果就是如果符合条件的节点加载出来看, 就立即把这个节点赋值为 items 变量,并返回 items ,否则抛出超时异常

所以在正常情况下,使用 scrape_index 方法可以获得首页上呈现的所有电影条目数据

接下来就可以定义一个 main 方法来调用 scrape_index 方法了 

from loguru import logger
def main():elements = scrape_index()for element in elements:element_data = scrape_detail(element)logger.debug(f'scraped data {element_data}')if __name__ == '__main__':main()

这里在 main 方法中首先调用 scrape_index 方法提取了当前首页的所有节点,然后遍历这些节点,并想通过一个 scrape_detail 方法提取每部电影的详情信息,最后返回并输出日志

那么问题明确了,scrape_detail 方法如何实现?大致思考一下,可以想到该方法需要做到如下三件事

模拟点击 element , 即首页的电影条目节点

进入详情页后爬取电影信息

点击回退按钮后返回首页

所以这个方法实现为

def scrape_detail(element):logger.debug(f'scraping {element}')element.click()wait.until(EC.presence_of_element_located((By.ID, f'{PACKAGE_NAME}:id/detail')))title = wait.until(EC.presence_of_element_located((By.ID, f'{PACKAGE_NAME}:id/title'))).get_attribute('text')categories = wait.until(EC.presence_of_element_located((By.ID, f'{PACKAGE_NAME}:id/categories_value'))).get_attribute('text')score = wait.until(EC.presence_of_element_located((By.ID, f'{PACKAGE_NAME}:id/score_value'))).get_attribute('text')minute = wait.until(EC.presence_of_element_located((By.ID, f'{PACKAGE_NAME}:id/minute_value'))).get_attribute('text')published_at = wait.until(EC.presence_of_element_located((By.ID, f'{PACKAGE_NAME}:id/published_at_value'))).get_attribute('text')drama = wait.until(EC.presence_of_element_located((By.ID, f'{PACKAGE_NAME}:id/drama_value'))).get_attribute('text')driver.back()return {'title': title,'categories': categories,'score': score,'minute': minute,'published_at': published_at,'drama': drama }

实现该方法需要先弄清楚详情页每个及诶蒂娜对应的节点名称, 属性都是怎样的,于是再次打开调试窗口,点击一个电影标题进入详情页, 查看器 DOM 树

可以观察到整个详情页对应一个 android.widget.ScrollView 节点,其包含的 resource-id 属性值为 com.goldze.mnnmhabit:id/detail 。详情页上的标题,类别,评分,时长,上映时间,剧情简介页都有各自的节点名称和 resource-id , 这里就不展开描述了, 从 Appium 的 Source 面板即可查看

在 scrape_detail 方法中,首先调用了 element 的click 方法进入对应的详情页,然后等待整个详情页的信息(即 com.goldze.mnnmhabit:id/detail )加载出来,之后顺次爬取了标题,类别,评分,时长,上映时间,剧情简介,爬取完毕后抹蜜点击回退按钮,最后将所有爬取的内容构成一个字典返回

其实现在,我们已经可以成功获取首页最开始加载的几条电影信息了,执行一下代码

部分输出内容

2024-08-16 16:05:22.177 | DEBUG    | __main__:scrape_detail:32 - scraping <appium.webdriver.webelement.WebElement (session="c9f0c1dc-d98a-45bc-b65f-60c5b3831219", element="00000000-0000-0015-7fff-ffff00000011")>
2024-08-16 16:05:24.149 | DEBUG    | __main__:main:62 - scraped data {'title': '霸王别姬', 'categories': '剧情、爱情', 'score': '9.5', 'minute': '171分钟', 'published_at': '1993-07-26', 'drama': '影片借一出《霸王别姬》的京戏,牵扯出三个人之间一段随时代风云变幻的爱恨情仇。段小楼(张丰毅 饰)与程蝶衣(张国荣 饰)是一对打小一起长大的师兄弟,两人一个演生,一个饰旦,一向配合天衣无缝,尤其一出《霸王别姬》,更是誉满京城,为此,两人约定合演一辈子《霸王别姬》。但两人对戏剧与人生关系的理解有本质不同,段小楼深知戏非人生,程蝶衣则是人戏不分。段小楼在认为该成家立业之时迎娶了名妓菊仙(巩俐 饰),致使程蝶衣认定菊仙是可耻的第三者,使段小楼做了叛徒,自此,三人围绕一出《霸王别姬》生出的爱恨情仇战开始随着时代风云的变迁不断升级,终酿成悲剧。'}

上拉加载更多内容

现在在上面代码的基础上,加入上拉加载更多数据的逻辑,因此需要判断什么时候上拉加载数据。想想我们平时在浏览器浏览数据的时候是怎么操作的? 一般是在即将看完的时候上拉,那这里页一样,可以让程序在遍历到位于偏下方的电影条目的时候开始上拉。例如,当爬取的节点对应的电影条目差不多位于页面高度的 80% 时,就触发上拉加载,将 main 方法改写如下

def main():elements = scrape_index()for element in elements:element_location = element.locationelement_y = element_location.get('y')if element_y / window_height > 0.5:logger.debug(f'scroll up')scroll_up()element_data = scrape_detail(element)logger.debug(f'scraped data {element_data}')

这里遍历是判断了  element 的位置,获取了其 y 的坐标值,当该值小于页面高度的 80% 时,触发上拉加载,加载的方法是 scroll_up 其定义如下

def scroll_up():driver.swipe(window_width * 0.5, window_height * 0.8, window_width * 0.5, window_height * 0.5, 1000)

方法 driver.swipe(start_x, start_y, end_x, end_y, 时间)

start_x, start_y : 开始上拉的 横纵坐标

end_x, end_y:上拉到的位置的横纵坐标

时间:上拉用时多久

去重,终止和保存数据

在本节开始部分我们曾提到,需要额外添加根据标题进行去重和判断终止的逻辑,所以在遍历首页中每个电影条目的时候还需要提取一下标题,然后将其存入一个全局变量中

def get_element_title(element):try:element_title = element.find_element(by=By.ID, value=f'{PACKAGE_NAME}:id/tv_title').get_aribute('text')return element_titleexcept NoSuchElementException:return None

这里定义了一个 get_element_title 方法,该方法接收一个 element 参数, 即首页电影条目对应的节点对象,然后提取其标题文本并返回,最后将 main 方法修改如下

scraped_titles = []
def main():while len(scraped_titles) < TOTAL_NUMBER:elements = scrape_index()for element in elements:element_title = get_element_title(element)if not element_title or element_title in scraped_titles:continueelement_location = element.locationelement_y = element_location.get('y')if element_y / window_height > 0.5:logger.debug(f'scroll up')scroll_up()element_data = scrape_detail(element)scraped_titles.append(element_title)logger.debug(f'scraped data {element_data}')

这里在 main 方法里添加了 while 循环, 入股哦爬取的电影条目数量尚未达到数量 TOTAL_NUMBER, 就接着爬取, 直到爬取完毕。 其中就调用 get_element_title 方法提取了电影标题,然后将已经爬取的电仪标题存储在全局变量 scraped_titles 中, 如果经判断, 当前节点对应的电影已经爬取过了, 就跳过, 否则接着爬取,爬取完毕后将标题存到 scraped_titles 变量里,这样就实现了去重

保存数据

最后,可以再添加一个保存数据的逻辑,将爬取的数据保存到本地 movie 文件夹中, 数据以 JSON 形式保存,代码如下

import os
import jsonOUTPUT_FOLDER = 'movie'
os.path.exists(OUTPUT_FOLDER) or os.makedirs(OUTPUT_FOLDER)def save_date(element_data):with open(f'{OUTPUT_FOLDER}/{element_data.get("title")}.json', 'w', encoding='utf-8') as f:f.write(json.dumps(element_data, ensure_ascii=False, indent=2))logger.debug(f'saved as file {element_data.get("title")}.json')

最后在 main 方法中添加调用逻辑即可

完整代码

from appium import webdriver
from appium.options.android import UiAutomator2Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import NoSuchElementException
from loguru import logger
import os
import jsonSERVER = 'http://localhost:4723/wd/hub'
DESIRED_CAPABILITIES = {'platformName': 'Android','deviceName': 'LIO_AN00','appPackage': 'com.goldze.mvvmhabit','appActivity': '.ui.MainActivity','noReset': True
}
OUTPUT_FOLDER = 'movie'
os.path.exists(OUTPUT_FOLDER) or os.makedirs(OUTPUT_FOLDER)
PACKAGE_NAME = DESIRED_CAPABILITIES['appPackage']
TOTAL_NUMBER = 100scraped_titles = []
driver = webdriver.Remote(SERVER, options=UiAutomator2Options().load_capabilities(DESIRED_CAPABILITIES))
wait = WebDriverWait(driver, 30)
window_size = driver.get_window_size()
window_width, window_height = window_size.get('width'), window_size.get('height')def scrape_index():items = wait.until(EC.presence_of_all_elements_located((By.XPATH, f'//android.widget.LinearLayout[@resource-id="{PACKAGE_NAME}:id/item"]')))return itemsdef scrape_detail(element):logger.debug(f'scraping {element}')element.click()wait.until(EC.presence_of_element_located((By.ID, f'{PACKAGE_NAME}:id/detail')))title = wait.until(EC.presence_of_element_located((By.ID, f'{PACKAGE_NAME}:id/title'))).get_attribute('text')categories = wait.until(EC.presence_of_element_located((By.ID, f'{PACKAGE_NAME}:id/categories_value'))).get_attribute('text')score = wait.until(EC.presence_of_element_located((By.ID, f'{PACKAGE_NAME}:id/score_value'))).get_attribute('text')minute = wait.until(EC.presence_of_element_located((By.ID, f'{PACKAGE_NAME}:id/minute_value'))).get_attribute('text')published_at = wait.until(EC.presence_of_element_located((By.ID, f'{PACKAGE_NAME}:id/published_at_value'))).get_attribute('text')drama = wait.until(EC.presence_of_element_located((By.ID, f'{PACKAGE_NAME}:id/drama_value'))).get_attribute('text')driver.back()return {'title': title,'categories': categories,'score': score,'minute': minute,'published_at': published_at,'drama': drama}def scroll_up():print(window_height)print(window_height * 0.8)print(window_height * 0.5)driver.swipe(window_width * 0.5, window_height * 0.8, window_width * 0.5, window_height * 0.5, 1000)def get_element_title(element):try:element_title = element.find_element(by=By.ID, value=f'{PACKAGE_NAME}:id/tv_title').get_attribute('text')return element_titleexcept NoSuchElementException:return Nonedef save_date(element_data):with open(f'{OUTPUT_FOLDER}/{element_data.get("title")}.json', 'w', encoding='utf-8') as f:f.write(json.dumps(element_data, ensure_ascii=False, indent=2))logger.debug(f'saved as file {element_data.get("title")}.json')def main():while len(scraped_titles) < TOTAL_NUMBER:elements = scrape_index()for element in elements:element_title = get_element_title(element)if not element_title or element_title in scraped_titles:continueelement_location = element.locationelement_y = element_location.get('y')if element_y / window_height > 0.5:logger.debug(f'scroll up')scroll_up()element_data = scrape_detail(element)scraped_titles.append(element_title)save_date((element_data))logger.debug(f'scraped data {element_data}')if __name__ == '__main__':main()

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/52134.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Godot《躲避小兵》实战之创建玩家场景

项目设置完之后&#xff0c;我们就可以开始处理玩家控制的角色。 这里我们将玩家放在一个单独的场景当中&#xff0c;这样做的好处是在游戏的其他部分做出来之前&#xff0c;我们就可以对其进行单独测试。 节点结构 场景是一个节点树结构&#xff0c;因此一个场景需要有一个…

WordPress美化节日灯笼插件,适合春节的时候使用

源码介绍&#xff1a; WordPress美化节日灯笼插件&#xff0c;适合每年过年的时候安在网站上使用&#xff0c;这款插件可以备用着&#xff0c;一款WordPress节日灯笼美化插件&#xff0c;可以给网页自动加一个灯笼效果使用说明&#xff1a;到网站WP后台 - 插件 - 安装插件 - 上…

[C#]基于winform结合photocartoon算法实现人物卡通化源码实现

【官方框架】 https://github.com/minivision-ai/photo2cartoon 简介 人像卡通风格渲染的目标是&#xff0c;在保持原图像ID信息和纹理细节的同时&#xff0c;将真实照片转换为卡通风格的非真实感图像。我们的思路是&#xff0c;从大量照片/卡通数据中习得照片到卡通画的映射…

企业级web应用服务器tomcat

目录 一、Web技术 1.1 HTTP协议和B/S 结构 1.2 前端三大核心技术 1.2.1 HTML 1.2.2 CSS&#xff08;Cascading Style Sheets&#xff09;层叠样式表 1.2.3 JavaScript 二、tomcat的功能介绍 2.1 安装 tomcat 环境准备 2.1.1 安装java环境 2.1.2 安装并启动tomcat …

vscode提升:JSON 中不允许有注释

解决方案 &#xff1a; 运行&#xff1a; json with comment 参考链接&#xff1a; https://blog.csdn.net/eqizhihui/article/details/134014010 人工智能学习网站 https://chat.xutongbao.top

基于飞腾平台的Hbase的安装配置

【写在前面】 飞腾开发者平台是基于飞腾自身强大的技术基础和开放能力&#xff0c;聚合行业内优秀资源而打造的。该平台覆盖了操作系统、算法、数据库、安全、平台工具、虚拟化、存储、网络、固件等多个前沿技术领域&#xff0c;包含了应用使能套件、软件仓库、软件支持、软件适…

iOS Native与JS通信:JSBridge

文章目录 一、简介二、JS 调用 Native1.使用 URL Schemea.UIWebViewb.WKWebView 2.使用 JavaScriptCore (iOS 7)3.使用 WKWebView 和 WKScriptMessageHandler (iOS 8) 三、Native 调用 JS1.使用 UIWebView2.使用 WKWebView3.使用 JavaScriptCore (iOS 7) 一、简介 对于移动应用…

深入浅出:你需要了解的用户数据报协议(UDP)

文章目录 **UDP概述****1. 无连接性****2. 尽最大努力交付****3. 面向报文****4. 多种交互通信支持****5. 较少的首部开销** **UDP报文的首部格式****详细解释每个字段** **UDP的多路分用模型****多路分用的实际应用** **检验和的计算方法****伪首部的详细内容****检验和计算步…

Python 数据分析之Numpy学习(一)

Python 数据分析之Numpy学习&#xff08;一&#xff09; 一、Numpy的引入 1.1 矩阵/向量的按位运算 需求&#xff1a;矩阵的按位相加 [0,1,4] [0,1,8] [0,2,12] 1.1.1 利用python实现矩阵/向量的按位运算 # 1.通过列表实现 list1 [0, 1, 4] list2 [0, 1, 8]# 列表使用…

iOS 18 Beta 5:苹果的细腻之笔,绘制用户体验新画卷

在苹果的世界里&#xff0c;每一次系统更新都是对用户体验进行的一次精心雕琢。 随着iOS 18 Beta 5的上线&#xff0c;苹果带来了一系列令人耳目一新的功能&#xff0c;同时也在系统的每个细微之处展现了对完美的追求。 Safari浏览器的“干扰控制”功能 在今天信息充斥的数字…

SpringBoot接入高德地图猎鹰轨迹服务API

SpringBoot接入高德地图猎鹰轨迹服务API 一、AP文档 猎鹰轨迹服务API文档 二、页面图 1、需登录账号&#xff0c;申请对应的应用key值 三、代码部分&#xff1a; 1、控制层 RestController RequestMapping("/gdTrack") public class TrackController {private …

搜维尔科技:【研究】Haption Virtuose外科手术触觉视觉学习系统的开发和评估

Haption面临挑战 除此之外&#xff0c;外科医生有时会对骨组织进行非常复杂的手术&#xff0c;其中一个例子是人工耳蜗的手术植入。重要的是要避免神经或血管等危险结构受伤&#xff0c;并尽可能轻柔地进行手术。在外科医生能够安全、无差错地进行此类手术之前&#xff0c;需要…

Flink常见数据源使用教程(DataStream API)

前言 一个 Flink 程序,其实就是对 DataStream 的各种转换。具体来说,代码基本上都由以下几部分构成,如下图所示: 获取执行环境(execution environment)读取数据源(source)定义基于数据的转换操作(transformations)定义计算结果的输出位置(sink)触发程序执行(exec…

鸿蒙HarmonOS实战开发: CMake脚本编写构建NDK工程

NDK工程构建 HarmonyOS NDK默认使用CMake作为构建系统&#xff0c;随包提供了符合HarmonyOS工具链的基础配置文件ohos.toolchain.cmake&#xff0c;用于预定义CMake变量来简化开发者配置。 常用的NDK工程构建方式有&#xff1a; 从源码构建 源码构建也有不同方式&#xff1a;…

写给大模型新人的经验,刷到少走三年弯路!

这篇文章&#xff0c;我将结合自己在大模型领域的经验&#xff0c;给大家详细聊聊新人应该如何转行大模型赛道&#xff1f; 比如大模型都有哪些方向&#xff1f;各方向的能力要求和岗位匹配&#xff1f;新手转行大模型常踩的坑和常见的误区&#xff1f;以及入行大模型最顺滑的…

什么是HW,企业要通过什么方式进行HW安全保障?

一、什么是HW 网络安全形势近年出现新变化&#xff0c;网络安全态势变得越来越复杂&#xff0c;黑客攻击入侵、勒索病毒等网络安全事件愈演愈烈&#xff0c;严重威胁到我国的网络空间安全。同时&#xff0c;国内不少关键信息基础设施的建设管理单位安全意识不够、安全投入不足…

怎么管控终端电脑上的移动端口

管控终端电脑上的移动端口&#xff0c;尤其是USB等移动端口&#xff0c;是确保企业数据安全和提升网络管理效率的重要手段。 一、使用注册表编辑器禁用USB端口&#xff08;适用于Windows系统&#xff09; 打开注册表编辑器&#xff1a; 同时按下“WinR”组合键&#xff0c;打…

24年上半年天融信营收缩减1.8亿,亏损2.06亿

吉祥知识星球http://mp.weixin.qq.com/s?__bizMzkwNjY1Mzc0Nw&mid2247485367&idx1&sn837891059c360ad60db7e9ac980a3321&chksmc0e47eebf793f7fdb8fcd7eed8ce29160cf79ba303b59858ba3a6660c6dac536774afb2a6330#rd 《网安面试指南》http://mp.weixin.qq.com/s?…

哪家轻量化PDM系统更适合中小企业?国内PDM系统推荐

在数字化转型的大潮中&#xff0c;中小企业面临着前所未有的挑战与机遇。产品数据管理PDM系统作为支持企业产品研发、制造和维护全生命周期的重要工具&#xff0c;其重要性日益凸显。然而&#xff0c;传统PDM系统往往因成本高、实施复杂而让中小企业望而却步。因此&#xff0c;…

java使用itext 直接生成pdf

itext 使用 需求背景itext 的使用依赖简单示例基础设置&#xff08;页面大小、边距、字体等&#xff09;段落内部&#xff0c;特殊设置关键字 字体或颜色生成动态表格页脚展示页数其他设置密码添加水印&#xff08;背景图&#xff09;目录Header, Footer分割 PDF合并 PDF 需求背…