Appium爬取安卓版⽹易云⾳乐单曲评论(热门评论,近期热评,所有评论)python Appium 爬取安卓版⽹易云⾳乐单曲评论(热门评论,近期热评,所有评论)
程序概述
使⽤appium 爬取⽹易云⾳乐歌曲评论。
该博客内容只是对使⽤appium 爬取⽹易云⾳乐评论的⼀个概述。如果您对该程序感兴趣可以关注我,欢迎⼤家交流经验
⽹易云⾳乐的评论可以通过⽹页版进⾏爬取。但是⽬前⽹页版的评论内容有以下问题:
1、就是当翻页到500页左右的时候,服务端就不再返回正确的数据。返回的都是重复数据,知道最后500页才正确。
2、⽹页端的api⽬前⽐较陈旧,热门评论显⽰不全,近期热评不显⽰。
使⽤ appium 爬取评论能解决以上两个问题,但是⽬前依旧存在的问题:
1、由于app客户端返回的评论数据是加密的(需逆向⼯程分析解密),⽆法通过抓包直接分析返回的json数据。只能在界⾯上查⾃⼰所需要的元素。这样整个程序处理的速度⽐较慢。
2、由于需要滑动界⾯,滑动的界⾯可能会切开⼀个完整的评论(完整的评论包括,昵称,点赞数,内容,时间,可能包括回复内容)。这样可能获取的数据存在不完整的情况。可能丢失回复的内容。
3、依托appium 、模拟器、或者真机,程序的稳定性⽐较差。
环境搭建
请⾃⾏使⽤搜索引擎python Appium环境搭建,这⾥不再赘述。需要以下环境:
1、Python3.6 以上
2、JDK1.8以上
3、安卓SDK
4、Appium(可⽤桌⾯版,⽐较⽅便)
5、安卓模拟器或安卓真机。模拟器:雷电3.44,分辨率 宽:1080 ⾼:1920 DPI:160-240 (DPI 设置很重要,可以控制页⾯能够显⽰的评论数,最好是160左右)
6、mysql
7、⽹易云安卓客户端,V5.8.3
代码实现
这⾥讲述⼤概的实现流程。
配置⽂件
包含以下主要内容
1、数据库的连接配置
2、appium启动安卓程序的配置
3、appium需要点击和查的元素的路径和节点名称
数据库连接代码
HOST = '127.0.0.1'
USER = 'root'
PASSWD = 'root'
PROT = 3306
DBNAME = 'music_comments'
CHARSET = 'utf8mb4'
启动安卓程序配置代码(根据模拟器或真机需要调整,以下为雷电模安卓拟器的实现)
APPIUM_ADDR = "127.0.0.1:4723/wd/hub"
DESIRED_CAPS = {
"platformName": "Android",
# "platformVersion": "6.0.1", #mumu模拟器
"platformVersion": "5.1.1",
"appPackage": "comease.cloudmusic",
"appActivity": ".activity.MainActivity",
"deviceName": "emulator",
# "deviceName": "127.0.0.1:7555", mumu模拟器的地址
"noReset": True,
"unicodeKeyboard": True,  # 输⼊中⽂。
"resetKeyboard": True  # 输⼊中⽂。
}
查节点元素的配置
通过xpath查⽅法或id查确定元素的内容
# # find_ele_str
SEARCH_BTN_XPATH_S = "//android.widget.TextView[@content-desc='搜索']"  # 搜索按钮路径
INPUT_BOX_ID_S = 'comease.cloudmusic:id/search_src_text'  # 输⼊框路径
SEARCH_RES_TAB_CLASSNAME_M = 'android.support.v7.app.ActionBar$Tab'  # 搜索结果的tab页
SEARCH_RES_SONG_MORE_INFO_ID_S = 'comease.cloudmusic:id/a'  # 搜索结果的歌曲的三个点
MORE_INFO_OF_COMMENT_XPATH_S = "//android.widget.TextView[contains(@text,'评论')]"  # 更多信息⾥的评论按钮
RETURN_CUR_PAGE_TOP_ID_S = "comease.cloudmusic:id/m_"  # 返回评论界⾯的置顶
VIEW2VIEW_XPATH_M = "//android.view.View[2]/*"  # 评论总数
TITLE_SONG_NAME_ID_S = "comease.cloudmusic:id/a8p"  # 歌曲名字
TITLE_SINGER_NAME_ID_S = "comease.cloudmusic:id/a8q"  # 歌⼿名字
LISTVIEW_XPATH_M = "//android.widget.ListView/android.widget.RelativeLayout"  # 所有评论的LISTVIEW
COMMENT_TYPE_ID_S = "comease.cloudmusic:id/a8y"  # 获取歌曲类别的。数据库中commentType 0 代表最新评论,1 代表精彩评论,2 代表近期热评
NICK_NAME_ID_S = "comease.cloudmusic:id/a80"  # 昵称
COMMENT_TIME_ID_S = "comease.cloudmusic:id/a8s"  # 评论时间
LIKE_CNT_ID_S = "comease.cloudmusic:id/a7z"  # 点赞数
CONTENT_TEXT_ID_S = "comease.cloudmusic:id/a81"  # 评论内容
REPLY_LINK_ID_S = "comease.cloudmusic:id/aip"  # 回复内容链接
BE_REPLY_CONTENT_ID_S = "comease.cloudmusic:id/a8_"  # 回复内容
MORE_HOT_COMMENT_ID_S = "comease.cloudmusic:id/b5m"  # 更多精彩评论
FROM_MORE_RETURN_MAIN_COMMENT = "//android.view.View[2]/android.widget.ImageButton"  # 返回箭头
RELPLY_LINEARLAYOUT_XPATH_S = ".//android.widget.LinearLayout/android.widget.LinearLayou赵雨航
t"  # 回复内容可点击的
RELPLY_LINEARLAYOUT_TEXT_XPATH_S = ".//android.widget.LinearLayout/android.widget.LinearLayout//android.widget.TextView" #回复内容⽂本数量该⽅法适⽤回复少于3条的内容
实现全局界⾯刷新的等待
因为需要实现界⾯的等待,但是appium的隐式等待显⽰等待不能满⾜全部需求,因为评论元素在⼀个view内是重复出现的。所以如果只是使⽤⾃带两个等待可能会出现数据丢失的问题。所以需要⾃定义⼀个等待⽅法。该⽅法表⽰当滑动屏幕完成后,当前界⾯如果在⼀定的条件下未发⽣变化,可认为界⾯整体加载完毕。⼀次性判断加载整体界⾯加载完毕的⽅式也可以节省⼤量的时间。以下是实现该⽅法的思路
def wait_full_view(self, interval, comp_times, timeout):
"""
刷新整个view可以设置刷新间隔
:param interval: 刷新间隔-秒单位
:param comp_times: 如果连续刷新该次数的driver.page_source相同得话则认为界⾯加载完毕
:param timeout: 最长得等待时间,如果超出该时间则认为界⾯加载完毕-秒单位
:return:
"""
starttime = int(time.time() * 1000)
comp_count = 0
page_init = self.driver.page_source
while True:
page_fir = page_init
time.sleep(interval)
page_sec = self.driver.page_source
if page_fir == page_sec:
comp_count += 1
if page_fir != page_sec:
comp_count = 0
page_init = page_sec
endtime = int(time.time() * 1000)
if comp_count >= comp_times or (endtime - starttime) >= timeout * 1000:
break
查评论分类的⽅法
如果我们要按照分类进⾏爬取,就必须知道评论的类型是属于热门,近期热评还是近期评论
所以需要知道评论类别的分界线,⽅便爬取
def __aline_type_frame(self, app_control, type_str):  # 对齐评论类型与顶栏
print("将尝试齐评论类型与顶栏")
find_res = False
self.__return_top(app_control)
x = app_control.x * 0.5
y = app_control.y * 0.5
top_frame = app_control.find_one_element(By.ID, config.RETURN_CUR_PAGE_TOP_ID_S)
swip_y_end = top_('y') + top_('height')
while True:
app_control.wait_full_view(0.4, 3, 5)
# comments_type_name = app_control.find_one_element(By.ID, "comease.cloudmusic:id/a8u")            try:
maybe_hidde = app_control.driver.find_element(By.ID, config.COMMENT_TYPE_ID_S)
except Exception:
maybe_hidde = None
if maybe_hidde:
s = 3500  # 时间速度
if maybe_hidde.location['y'] > y:
app_control.swipe_up_down(650, 0.75, 0.65, 1)
s = 6000  # 距离让⼦弹飞⼀会
app_control.wait_full_view(0.4, 3, 5)
comments_type_name = app_control.find_one_element(By.ID, config.COMMENT_TYPE_ID_S)                if comments_type_name and comments_[:4] == type_str:
swip_y_begin = comments_type_name.location['y']
app_control.driver.swipe(x, swip_y_begin, x, swip_y_end, s)
find_res = True
break
if comments_type_name and comments_[:4] == '最新评论':
break
app_control.swipe_up_down(750, 0.75, 0.30, 1)
return find_res
爬取评论的⽅法
def __get_commmet(self, app_control, comment_type=None, entry_reply_link=False):
"""
因为滑屏的原因,将⼀个完整的评论,时间,昵称,内容等有可能进⾏了切分。虽然提供了简单的去重逻辑但是,
不保证完全去除成功,并且有很⼩的概率删除⼀个⼈评论的内容相同的。该⽅法应该放在对齐⽅法的后⾯
:param app_control: app_control类
:param comment_type: 评论类型
:param entry_reply_link: 是否进⼊回复评论连接
:return:
"""
dbctl = OperateDB()
swipe_count = 0
distinct_pool = []  # 初始值定为50 为滑动的两页的数量
driver_source_pool = []  # 存放整体页⾯。⽤来跳出循环
type_flag_list = []  # ⽤来存放到的评论分类
洪士雅
while True:
perhaps_flag = False  # 判断两个页⾯是不是衔接失败。如果driver_source_pool中不存在页⾯的第⼀个元素则认为衔接失败反向滑屏            if len(driver_source_pool) >= 5:
driver_source_pool._str_md5(app_control.driver.page_source))
driver_source_pool.pop(0)
else:
driver_source_pool._str_md5(app_control.driver.page_source))
if len(driver_source_pool) >= 5 and len(set(driver_source_pool)) == 1:
海市蜃楼歌曲print('滑动屏幕多次,界⾯未发⽣变化,不再继续滑动')
break
relativelayout_list = app_control.find_all_elements(By.XPATH, config.LISTVIEW_XPATH_M)
destination_el = relativelayout_list[2]  # ⽤于scoll 这个为1 能保证数据的完整性。滑动到第⼆个元素的距离
origin_el = relativelayout_list[-2]  # 从最后⼀个元素开始滑动
relativelayout_index = 0
胡歌纹身
for relativeLayout in relativelayout_list:
# 到第⼀个类别分界线。⼀般是指对其后的第⼀个
one_type = _element_text(relativeLayout, By.ID, config.COMMENT_TYPE_ID_S)
if one_type:
type_flag_list.append(one_type)
if len(type_flag_list) >= 2:  # 如果到第⼆个的话则中断循环
break
nick_name = _element_text(relativeLayout, By.ID, config.NICK_NAME_ID_S)
comment_time = _element_text(relativeLayout, By.ID, config.COMMENT_TIME_ID_S)
like_cnt = _element_text(relativeLayout, By.ID, config.LIKE_CNT_ID_S)
content_text = _element_text(relativeLayout, By.ID, config.CONTENT_TEXT_ID_S)
be_reply = _element_text(relativeLayout, By.ID, config.BE_REPLY_CONTENT_ID_S)
be_reply_count = _element_text(relativeLayout, By.ID, config.REPLY_LINK_ID_S)
# print(be_reply_count)
be_reply_count = utils.turn_reply_cnt(be_reply_count)
if be_reply_count is None:
reply_layout = app_control.elements_exist(relativeLayout, By.XPATH,
config.RELPLY_LINEARLAYOUT_TEXT_XPATH_S)
if reply_layout is not None:
be_reply_count = len(reply_layout)
like_cnt = un_likecount_str(like_cnt)
if nick_name is None or comment_time is None or content_text is None:
河北电视台杨阳continue
one_full_comment = utils.str_join_md5(nick_name, comment_time, like_cnt, content_text,
be_reply)  # 将字符串结果组合然后进⾏MD5
if len(distinct_pool) >= config.DISTINCT_POOL and relativelayout_index == 1:
if one_full_comment not in distinct_pool:
perhaps_flag = True
break
# print(len(distinct_pool),relativelayout_index,perhaps_flag)
if one_full_comment in distinct_pool:
distinct_pool.append(one_full_comment)
print('该条记录存在缓存池,不再放⼊到数据库中')
else:
distinct_pool.append(one_full_comment)
print('昵称:%s; 时间:%s; 点赞数:%s; 内容:%s...' % (nick_name, comment_time, like_cnt, content_text[:20]))
sql = "INSERT INTO `music_comments`.`comment_content`(`COMMENT_ID`, `TASK_ID`, `NICK_NAME`, " \
"`COMMENT_TIME`, `LIKE_COUNT`, `COMMENT_TEXT`, `BE_REPLIED_TEXT`, `BE_REPLIED_COUNT`, " \
"`COMMENT_TIME_PROC`, `COMMENT_TYPE`, `SWIPE_COUNT`, `CREATE_TIME`) " \
"VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s);"
comment_id = int(time.time() * 1000)
comment_info_list = (
comment_id, self.task_id, nick_name, comment_time, like_cnt, content_text, be_reply,
be_reply_count, utils.turn_time(comment_time), comment_type, swipe_count,
w())
dbctl.add_data(sql, comment_info_list)
if entry_reply_link:  # 进⼊回复的评论
self.__entry_reply_comment(dbctl, app_control, relativeLayout, comment_id)
我的骄傲if len(distinct_pool) > config.DISTINCT_POOL:
distinct_pool.pop(0)
relativelayout_index += 1
if len(type_flag_list) >= 2:
break
elif not perhaps_flag:
app_control.el_scroll(origin_el, destination_el, 4000)
# app_control.swipe_up_down(450, 0.75, 0.30, 1)  # 这个不太好⽤,先弃⽤吧
swipe_count += 1
else:
app_control.swipe_up_down(random.randint(650, 800), 0.30, 0.60, 1)
print('可能衔接中断,开始上滑')
app_control.wait_full_view(0.35, 3, 5)  # ⼤概每次⽹络请求会返回20条评论,在之后界⾯有⼀段需要加载的的时间,
# 就是界⾯显⽰正在加载,这时⽆法使⽤显⽰等待或者隐式等待,通过判断整体界⾯在特定条件下是否有变化来判断界⾯是否加载完毕程序实现功能
1 、对爬取所有评论时出现中断实现断点续爬
2、单独爬取不同类别的评论,爬取所有⽹易云⾳乐评论的所有热门评论,近期热评,近期评论,所有评论
3、爬取评论被回复的内容。可以爬取评论的被回复的所有内容
4、将爬取下来的数据存储到mysql 数据库
爬取⽅法和数据样例: