美文网首页
利用selenium解析及下载资源

利用selenium解析及下载资源

作者: 西瓜雪梨桔子汁 | 来源:发表于2018-06-30 14:03 被阅读0次

    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

    相关文章

      网友评论

          本文标题:利用selenium解析及下载资源

          本文链接:https://www.haomeiwen.com/subject/pkrtuftx.html