利用selenium解析及下载资源

1. 缘由

    接到朋友求助,能否帮他将云盘上的资料下载下来;资料都是些文本文档,按照目录结构组织,当然也希望下载到本地后能够按照目录划分。
在拿到账号和密码后,我登录上去,云盘里的内容大致如下:


QQ截图20180630112917.jpg

2. 方案分析

2.1 需要解决问题

    从要求来看,需要解决的点主要如下:

  • 自动登录:给定账号、密码、url自动登录。

  • 登陆后去除提示框。当时登录发现每次登录会出现一个提示框,需要点击继续使用之后才能继续。如下:


    QQ截图20180630131458.jpg
  • 资源地址按照云盘的文件夹组织,以便后续下载文件按文件夹放置。

  • 按照云盘结构下载存储资源。

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:每次递归需要解析的页面地址

解析过程大致如下,类似深度优先遍历的过程,先对一个目录遍历到底,再逐个从底层返回:


QQ截图20180630131228.jpg

4.下载资源

    之前说到,下载方式要么一堆解析到的资源一次性下载,然后按照自己组装解析文件夹 -- 子文件夹/文件资源对应关系,使用python的os模块完成文件挪动,之前的想法是将这种文件夹层级关系组装为json格式,方便处理。


QQ截图20180630133605.jpg

    但是实际操作会发现,浏览器下载文件时,对于中文文件名空格等总会加上些乱七八糟的字符,如:


QQ截图20180630132203.jpg

这对于文件名匹配可不是好事,解析时都是正常的文件名,下载后的这种必然匹配不上,因此放弃。
另外,考虑到一次下载完资源,如果中途出现失败也比较麻烦,如图:


QQ截图20180630132906.jpg

    转而采用配置chrome默认下载目录的方式,不过考虑到文件资源众多,文件夹数量众多,而且想使用selenium配置chrome下载目录,必须每次配置都新启动一个实例才会生效。
    考虑到上述情况,解决方案如下:

  • 将资源切分多个子文件,逐个子文件下载
  • 采用配置浏览器下载目录方式,每隔新配置的实例只下载一个目录下的所有文件资源连接。当然,配置下载目录前会先创建对应目录。


    QQ截图20180630133953.jpg
#  先解析资源文件,得到所有资源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'所有文件下载完毕!'

下载后的成果:


QQ截图20180630140215.jpg

5.总结

    虽然实现了下载功能,但是仍然有几个问题:

(1). 反爬问题:

好在下载过程中并未出现,其实应该配合代理IP地址,每次访问通过代理IP请求下载,能够避免很多问题。

(2). 代码结构:

主逻辑部分较为混乱,也没花心思摆弄,改为类实现应该能省去很多参数传递问题。

(3). chromedriver

selenium需要chromedriver,这是个可执行文件,下载下来放到python安装目录或者当前项目目录即可,只有python能找到。至于如何匹配自己的chrome版本,可以参考地址:https://www.cnblogs.com/xqtesting/p/8334997.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,431评论 25 707
  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明先生_x阅读 15,967评论 3 119
  • 月没西山, 晨光熹微。 宿鸟携风振翅, 池面树影微叠。 虽清波渺渺, 无有千尺深, 不似桃花潭。 命里相遇都成客,...
    恶竹阅读 147评论 0 1
  • 有的人可以打麻将三天三夜,有的人可以打电玩三天三夜,有的人可以读书三天三夜……都辛苦,都不辛苦,乐在其中,自然会废...
    大胡子逸舟阅读 482评论 0 1
  • 规律 你的成(发)长(展),终究还是要经过生根,发芽,生长,绽放。自然的规律,哪怕你是万年的常青...
    夏雨凡星阅读 161评论 0 0