1. 缘由
接到朋友求助,能否帮他将云盘上的资料下载下来;资料都是些文本文档,按照目录结构组织,当然也希望下载到本地后能够按照目录划分。
在拿到账号和密码后,我登录上去,云盘里的内容大致如下:
2. 方案分析
2.1 需要解决问题
从要求来看,需要解决的点主要如下:
自动登录:给定账号、密码、url自动登录。
-
登陆后去除提示框。当时登录发现每次登录会出现一个提示框,需要点击继续使用之后才能继续。如下:
资源地址按照云盘的文件夹组织,以便后续下载文件按文件夹放置。
按照云盘结构下载存储资源。
2.2 解决方案
(1). 自动登录
首先要实现自动登录,当然要祭出selenium神器了,只需要几行python代码就可以自动登录。
(2). 资源解析
由于使用python练习过爬虫,爬过图片、文档资源连接,但都是使用request、urllib等完成的,且目标网站资源简单,地址格式都类似,只需简单拼接借号。但这个云盘资源地址的资源不是直接展示的,是依赖每次鼠标点击文件夹资源,触发js然后get一个地址,切换到另一个文件夹资源;对文档资源js触发get下载操作。所以,解析资源的操作使用selenium会方便很多,css_selector、xpath方式均可。
(3). 资源下载及存储
还有个问题就是本地存储目录结构比照照云盘目录格结构,这个实现方案有2种:
- 先下载所有文件(浏览器设置的默认下载位置),然后依据解析的“文件夹-资源文件”格式,移动响应文件到文件夹。
- 也可以使用selenium不断更换浏览器下载存储地址,批次地下载对应文件夹相关的文件。但是,每个文件夹就必须要新开一个chrome实例,特别耗费资源,也容易被反爬。
3. 方案实现
3.1 自动登录
selenium实现自动登录非常简单,只需要简单分析下网站的登录框,模拟人填入相应的账号密码、点击提交按钮即可。
# 实例化浏览器对象
browser = webdriver.Chrome()
# 最大化浏览器
browser.maximize_window()
# 这里设置智能等待10s
browser.implicitly_wait(10)
# 网址
browser.get('访问的网址') # 相当于你打开浏览器输入地址、enter
# 用户名和密码
username="用户名"
passwd="密码"
# 找到登录位置填入用户信息
elem=browser.find_element_by_id("userName") # 发现的该云盘的登录框中用户输入框ID
elem.send_keys(username) # 填入用户名
elem=browser.find_element_by_id("password") # 发现的该云盘的登录框中密码输入框ID
elem.send_keys(passwd) # 填入密码
elem=browser.find_element_by_id("login-btn") # 找到登录按钮id
elem.click() # 点击提交
3.2 去掉提示框
最开始因为不熟悉前端的一些东西,导致每次登录都解析不出资源,命名分析了页面元素,但是用尽各种xpath、css_selector选择器都还是无法解析出相关html元素。
经历过艰难的填坑之后,才发现可能是iframe的问题。挣扎之后的解决方案:
(1). 关闭提示框
首先需要解决的是除提示框:我用的比较粗暴的办法,通过selenium提供的find_element_by_link_text方法,找到继续使用网页版,触发点击完成关闭提示框。
(2). 切换iframe
仅仅是关闭提示框,仍然无法解析相关资源,通过分析多个页面切换的html元素,发现仅在首次登录的时候会在默认iframe,当开始访问具体资源文件夹时,所有的资源相关内容都在id名为mainFrame的iframe。
找到问题之后,解决方案就很简单了:自动登录-->关闭提示框-->切换mainFrame ……代码如下:
# 点击继续使用,去掉遮罩层
browser.find_element_by_link_text(u'继续使用网页版').click()
# 点击访问技术文档内容,目的是简单,不去解析"技术文档"所在地址再点击
browser.get('访问技术文档地址')
# 切换到id为mainFrame的iframe上,才能获取到文件夹列表内容
browser.switch_to.frame("mainFrame") # 用id来定位
3.3 解析资源
应该说,每个网站的布局、获取资源分时都不同,需要具体问题具体分析。这部分内容没什么共性,唯一的共性就是如何找到你想要的元素,提取出自己需要的内容。
对于这个下载要求来讲,无外乎完成如下功能:
- 访问一个文件夹,解析当前页面所有文件夹地址、文档资源地址
- 按照文件夹 -- 子文件件/文档资源组成以文件夹为key,文档地址或子文件夹地址为value的字典/map结构完成资源地址存储。
网页结构分析也不再详细说,对于该云盘资源,关键点在于,页面结构如下:
html-->body-->...->list(每个页面混合子文件夹、文档资源)-->th/tr-->input
其中: - tr包含多个属性,其中type属性指定了资源类型,type是folder则对应文件夹资源,type是file则对应文件夹资源。
- tr之下还有多个input标签,input[1]指示文件夹/资源文件名,input[2]则是对应的folderId或者fileID。
fileName_folder_fileUri = []
browser.get(targetUrl) # 切换到想要解析的资源页面
element = browser.find_element_by_id('listContent') # 先定位到id是listContent的元素,如果有id的以id定位最方便快捷准确
trElements = element.find_elements_by_tag_name('tr') # 依据tag为tr找到所有的tr元素
listuri = []
for trElement in trElements: # 遍历每个tr元素,准备解析具体的文件夹或者文件资源
# print trElement
if trElement.get_attribute('type') == 'folder': # 判断该tr是文件夹元素
inputElements = trElement.find_elements_by_tag_name('input') # 解析input元素
folderName = inputElements[1].get_attribute('value').rstrip() # 解析文件夹名
folderUrl = baseFolderUrl+inputElements[2].get_attribute('value') # 解析文件夹ID
listuri.append(folderName+'#'+folderUrl) # 自定义组装方式,准备先存储到本地
print "folderName=%s, folderUrl=%s" % (folderName,folderUrl)
elif trElement.get_attribute('type') == 'file': # 如果是文件资源
inputElements = trElement.find_elements_by_tag_name('input') # 定位该tr下的所有input元素
filerName = inputElements[1].get_attribute('value').rstrip() # 解析文件名
fileUrl = baseFileUrl+inputElements[2].get_attribute('value') # 解析文件地址
fileName_folder_fileUri.append(filerName+'#'+path+'#'+fileUrl) # 组装成文件名#文件夹名#文件资源地址
3.4 递归解析资源
由于资源方式是父子文件夹,文件夹嵌套、且单个文件夹同时包含文件夹、文件资源,因此还需能递归调用实现遍历所有的资源地址。代码如下:
def getResourceRecursively(browser,baseFolderUrl, baseFileUrl, targetUrl, path):
print u'-----------------递归解析资源------------------------------------'
print "get url: %s ..." % targetUrl
print "all file are : %s ..." % path
fileName_folder_fileUri = []
browser.get(targetUrl)
element = browser.find_element_by_id('listContent')
trElements = element.find_elements_by_tag_name('tr')
listuri = []
for trElement in trElements:
# print trElement
if trElement.get_attribute('type') == 'folder':
inputElements = trElement.find_elements_by_tag_name('input')
folderName = inputElements[1].get_attribute('value').rstrip()
folderUrl = baseFolderUrl+inputElements[2].get_attribute('value')
listuri.append(folderName+'#'+folderUrl)
print "folderName=%s, folderUrl=%s" % (folderName,folderUrl)
elif trElement.get_attribute('type') == 'file':
inputElements = trElement.find_elements_by_tag_name('input')
filerName = inputElements[1].get_attribute('value').rstrip()
fileUrl = baseFileUrl+inputElements[2].get_attribute('value')
fileName_folder_fileUri.append(filerName+'#'+path+'#'+fileUrl)
# print "filename=%s, fileUrl=%s" % (filerName,fileUrl)
# 待当前页面所有文件夹uri获取到之后,递归调用获取子目录资源
for val in listuri:
# 递归调用,对当前页面比如解析到3个文件夹地址,则3个文件夹地址都需调用,如果进去的文件夹还有文件夹,就继续递归
sub_fileName_folder_fileUri = getResourceRecursively(browser,baseFolderUrl, baseFileUrl, val.split('#')[1], os.path.join(path,val.split('#')[0]))
#print sub_fileName_folder_fileUri
fileName_folder_fileUri.extend(sub_fileName_folder_fileUri)
print u'-----------------end------------------------------------'
# 如果当前页面所有文件夹资源遍历完毕,或者只有文件资源,递归结束条件结束,返回解析到文件资源
return fileName_folder_fileUri
参数说明:
- baseFolderUrl:文件夹资源基本串,该网站使用的基本串+folderID(我们解析出来的)方式
- baseFileUrl:同理,只是文件资源基本串+fileId
- targetUrl:每次递归需要解析的页面地址
解析过程大致如下,类似深度优先遍历的过程,先对一个目录遍历到底,再逐个从底层返回:
4.下载资源
之前说到,下载方式要么一堆解析到的资源一次性下载,然后按照自己组装解析文件夹 -- 子文件夹/文件资源对应关系,使用python的os模块完成文件挪动,之前的想法是将这种文件夹层级关系组装为json格式,方便处理。
但是实际操作会发现,浏览器下载文件时,对于中文文件名空格等总会加上些乱七八糟的字符,如:
这对于文件名匹配可不是好事,解析时都是正常的文件名,下载后的这种必然匹配不上,因此放弃。
另外,考虑到一次下载完资源,如果中途出现失败也比较麻烦,如图:
转而采用配置chrome默认下载目录的方式,不过考虑到文件资源众多,文件夹数量众多,而且想使用selenium配置chrome下载目录,必须每次配置都新启动一个实例才会生效。
考虑到上述情况,解决方案如下:
- 将资源切分多个子文件,逐个子文件下载
-
采用配置浏览器下载目录方式,每隔新配置的实例只下载一个目录下的所有文件资源连接。当然,配置下载目录前会先创建对应目录。
# 先解析资源文件,得到所有资源list
with open('fileUri1_1301_1375.txt') as f:
records = f.readlines()
#uris = [x.split('#')[2] for x in f.readlines()]
fileuris = [x.split('#')[2].decode('utf-8') for x in records]
folders = [x.split('#')[1].decode('utf-8') for x in records]
# 将资源地址解析封装成资源连接为key,应该存储的文件夹为value的dict,所有资源组成list返回
uris = list(map(lambda x, y : [x, y], fileuris, folders))
# 资源总数
uri_count = len(uris)
print u'总计解析到%d个资源链接。' % uri_count
# 组成文件夹--文件资源list的dict结构
folder_fileuri = {}
# 初始化字典,key为文件夹名
for folder in folders:
folder_fileuri[folder] = []
print u'初始化完成!总计文件夹个数:%d' % len(folder_fileuri)
# 在将list解析为文件夹为key,对应的文件资源地址list链接为value组成的dict,转为json是为了方便写入文件
for uri in uris:
folder_fileuri[uri[1]].append(uri[0])
jsondata = json.dumps(folder_fileuri, encoding="UTF-8", ensure_ascii=False, sort_keys=False, indent=4)
下载资源实现:配置下载目录,逐个资源get请求即可
def downloadFileList(fileList, downLoadPath):
# 声明浏览器对象,配置下载默认下载地址参数
options = webdriver.ChromeOptions()
prefs = {'profile.default_content_settings.popups': 0, 'download.default_directory': downLoadPath}
options.add_experimental_option('prefs', prefs)
browser = webdriver.Chrome(chrome_options=options)
# 最大化浏览器
#browser.maximize_window()
# 这里设置智能等待10s
browser.implicitly_wait(10)
# 网址
browser.get('首页地址')
# 用户名和密码
username="用户名"
passwd="密码"
# 找到登录位置填入用户信息
elem=browser.find_element_by_id("userName")
elem.send_keys(username)
elem=browser.find_element_by_id("password")
elem.send_keys(passwd)
elem=browser.find_element_by_id("login-btn")
elem.click()
# 点击继续使用,去掉遮罩层
browser.find_element_by_link_text(u'继续使用网页版').click()
# 点击访问技术文档内容
browser.get('访问技术文档地址')
# 切换到id为mainFrame的iframe上,才能获取到文件夹列表内容
browser.switch_to.frame("mainFrame") # 用id来定位
# 逐个下载
for fileuri in fileList:
print u'下载%s到目录:%s ...' % (fileuri, downLoadPath)
browser.get(fileuri)
创建文件夹逻辑:
##
# @Brief 创建目录
#
# @Param 待创建目录
# @return 无返回
#
# @Details 测试目录存在与否,无则创建目录
#
def createDirIfNull(dir):
if(not os.path.exists(dir.strip())):
print u'目录%s不存在,现在创建...' % dir
调用下载的主逻辑:
# 遍历下载,下载到不同文件夹
folder_coumt = 1
for folder in folder_fileuri: # folder_fileuri就是每次读取资源文件解析成的文件夹为key,对应文件夹下文件资源链接list为value的dict
print u'开始下载%s文件夹对应的资源...' % folder
createDirIfNull(folder)
downloadFileList(folder_fileuri[folder], folder)
if(folder_coumt%2 == 0):
print u'还有%s个文件夹所属资源需要下载, 暂停5s...' % (str(len(folder_fileuri)-folder_coumt))
time.sleep(2)
folder_coumt = folder_coumt + 1
time.sleep(15)
# 下载完成
print u'所有文件下载完毕!'
下载后的成果:
5.总结
虽然实现了下载功能,但是仍然有几个问题:
(1). 反爬问题:
好在下载过程中并未出现,其实应该配合代理IP地址,每次访问通过代理IP请求下载,能够避免很多问题。
(2). 代码结构:
主逻辑部分较为混乱,也没花心思摆弄,改为类实现应该能省去很多参数传递问题。
(3). chromedriver
selenium需要chromedriver,这是个可执行文件,下载下来放到python安装目录或者当前项目目录即可,只有python能找到。至于如何匹配自己的chrome版本,可以参考地址:https://www.cnblogs.com/xqtesting/p/8334997.html