1. 緣由
????接到朋友求助,能否幫他將云盤上的資料下載下來承冰;資料都是些文本文檔华弓,按照目錄結(jié)構(gòu)組織,當(dāng)然也希望下載到本地后能夠按照目錄劃分困乒。
在拿到賬號(hào)和密碼后寂屏,我登錄上去,云盤里的內(nèi)容大致如下:
2. 方案分析
2.1 需要解決問題
????從要求來看娜搂,需要解決的點(diǎn)主要如下:
自動(dòng)登錄:給定賬號(hào)迁霎、密碼、url自動(dòng)登錄百宇。
-
登陸后去除提示框考廉。當(dāng)時(shí)登錄發(fā)現(xiàn)每次登錄會(huì)出現(xiàn)一個(gè)提示框,需要點(diǎn)擊繼續(xù)使用之后才能繼續(xù)携御。如下:
資源地址按照云盤的文件夾組織昌粤,以便后續(xù)下載文件按文件夾放置。
按照云盤結(jié)構(gòu)下載存儲(chǔ)資源啄刹。
2.2 解決方案
(1). 自動(dòng)登錄
????首先要實(shí)現(xiàn)自動(dòng)登錄涮坐,當(dāng)然要祭出selenium神器了,只需要幾行python代碼就可以自動(dòng)登錄誓军。
(2). 資源解析
????由于使用python練習(xí)過爬蟲袱讹,爬過圖片、文檔資源連接昵时,但都是使用request捷雕、urllib等完成的,且目標(biāo)網(wǎng)站資源簡(jiǎn)單债查,地址格式都類似非区,只需簡(jiǎn)單拼接借號(hào)。但這個(gè)云盤資源地址的資源不是直接展示的盹廷,是依賴每次鼠標(biāo)點(diǎn)擊文件夾資源征绸,觸發(fā)js然后get一個(gè)地址,切換到另一個(gè)文件夾資源;對(duì)文檔資源js觸發(fā)get下載操作管怠。所以淆衷,解析資源的操作使用selenium會(huì)方便很多,css_selector渤弛、xpath方式均可祝拯。
(3). 資源下載及存儲(chǔ)
????還有個(gè)問題就是本地存儲(chǔ)目錄結(jié)構(gòu)比照照云盤目錄格結(jié)構(gòu),這個(gè)實(shí)現(xiàn)方案有2種:
- 先下載所有文件(瀏覽器設(shè)置的默認(rèn)下載位置)她肯,然后依據(jù)解析的“文件夾-資源文件”格式佳头,移動(dòng)響應(yīng)文件到文件夾。
- 也可以使用selenium不斷更換瀏覽器下載存儲(chǔ)地址晴氨,批次地下載對(duì)應(yīng)文件夾相關(guān)的文件康嘉。但是,每個(gè)文件夾就必須要新開一個(gè)chrome實(shí)例籽前,特別耗費(fèi)資源亭珍,也容易被反爬。
3. 方案實(shí)現(xiàn)
3.1 自動(dòng)登錄
????selenium實(shí)現(xiàn)自動(dòng)登錄非常簡(jiǎn)單枝哄,只需要簡(jiǎn)單分析下網(wǎng)站的登錄框肄梨,模擬人填入相應(yīng)的賬號(hào)密碼、點(diǎn)擊提交按鈕即可挠锥。
# 實(shí)例化瀏覽器對(duì)象
browser = webdriver.Chrome()
# 最大化瀏覽器
browser.maximize_window()
# 這里設(shè)置智能等待10s
browser.implicitly_wait(10)
# 網(wǎng)址
browser.get('訪問的網(wǎng)址') # 相當(dāng)于你打開瀏覽器輸入地址众羡、enter
# 用戶名和密碼
username="用戶名"
passwd="密碼"
# 找到登錄位置填入用戶信息
elem=browser.find_element_by_id("userName") # 發(fā)現(xiàn)的該云盤的登錄框中用戶輸入框ID
elem.send_keys(username) # 填入用戶名
elem=browser.find_element_by_id("password") # 發(fā)現(xiàn)的該云盤的登錄框中密碼輸入框ID
elem.send_keys(passwd) # 填入密碼
elem=browser.find_element_by_id("login-btn") # 找到登錄按鈕id
elem.click() # 點(diǎn)擊提交
3.2 去掉提示框
????最開始因?yàn)椴皇煜で岸说囊恍〇|西,導(dǎo)致每次登錄都解析不出資源瘪贱,命名分析了頁面元素纱控,但是用盡各種xpath、css_selector選擇器都還是無法解析出相關(guān)html元素菜秦。
????經(jīng)歷過艱難的填坑之后甜害,才發(fā)現(xiàn)可能是iframe的問題。掙扎之后的解決方案:
(1). 關(guān)閉提示框
首先需要解決的是除提示框:我用的比較粗暴的辦法球昨,通過selenium提供的find_element_by_link_text方法尔店,找到繼續(xù)使用網(wǎng)頁版,觸發(fā)點(diǎn)擊完成關(guān)閉提示框主慰。
(2). 切換iframe
????僅僅是關(guān)閉提示框嚣州,仍然無法解析相關(guān)資源,通過分析多個(gè)頁面切換的html元素共螺,發(fā)現(xiàn)僅在首次登錄的時(shí)候會(huì)在默認(rèn)iframe该肴,當(dāng)開始訪問具體資源文件夾時(shí),所有的資源相關(guān)內(nèi)容都在id名為mainFrame的iframe藐不。
????找到問題之后匀哄,解決方案就很簡(jiǎn)單了:自動(dòng)登錄-->關(guān)閉提示框-->切換mainFrame ……代碼如下:
# 點(diǎn)擊繼續(xù)使用秦效,去掉遮罩層
browser.find_element_by_link_text(u'繼續(xù)使用網(wǎng)頁版').click()
# 點(diǎn)擊訪問技術(shù)文檔內(nèi)容,目的是簡(jiǎn)單涎嚼,不去解析"技術(shù)文檔"所在地址再點(diǎn)擊
browser.get('訪問技術(shù)文檔地址')
# 切換到id為mainFrame的iframe上阱州,才能獲取到文件夾列表內(nèi)容
browser.switch_to.frame("mainFrame") # 用id來定位
3.3 解析資源
????應(yīng)該說,每個(gè)網(wǎng)站的布局法梯、獲取資源分時(shí)都不同苔货,需要具體問題具體分析。這部分內(nèi)容沒什么共性立哑,唯一的共性就是如何找到你想要的元素夜惭,提取出自己需要的內(nèi)容。
????對(duì)于這個(gè)下載要求來講刁憋,無外乎完成如下功能:
- 訪問一個(gè)文件夾滥嘴,解析當(dāng)前頁面所有文件夾地址、文檔資源地址
- 按照文件夾 -- 子文件件/文檔資源組成以文件夾為key至耻,文檔地址或子文件夾地址為value的字典/map結(jié)構(gòu)完成資源地址存儲(chǔ)。
????網(wǎng)頁結(jié)構(gòu)分析也不再詳細(xì)說镊叁,對(duì)于該云盤資源尘颓,關(guān)鍵點(diǎn)在于,頁面結(jié)構(gòu)如下:
html-->body-->...->list(每個(gè)頁面混合子文件夾晦譬、文檔資源)-->th/tr-->input
其中: - tr包含多個(gè)屬性疤苹,其中type屬性指定了資源類型,type是folder則對(duì)應(yīng)文件夾資源敛腌,type是file則對(duì)應(yīng)文件夾資源卧土。
- tr之下還有多個(gè)input標(biāo)簽,input[1]指示文件夾/資源文件名像樊,input[2]則是對(duì)應(yīng)的folderId或者fileID尤莺。
fileName_folder_fileUri = []
browser.get(targetUrl) # 切換到想要解析的資源頁面
element = browser.find_element_by_id('listContent') # 先定位到id是listContent的元素,如果有id的以id定位最方便快捷準(zhǔn)確
trElements = element.find_elements_by_tag_name('tr') # 依據(jù)tag為tr找到所有的tr元素
listuri = []
for trElement in trElements: # 遍歷每個(gè)tr元素生棍,準(zhǔn)備解析具體的文件夾或者文件資源
# 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) # 自定義組裝方式颤霎,準(zhǔn)備先存儲(chǔ)到本地
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 遞歸解析資源
????由于資源方式是父子文件夾,文件夾嵌套涂滴、且單個(gè)文件夾同時(shí)包含文件夾友酱、文件資源,因此還需能遞歸調(diào)用實(shí)現(xiàn)遍歷所有的資源地址柔纵。代碼如下:
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)
# 待當(dāng)前頁面所有文件夾uri獲取到之后缔杉,遞歸調(diào)用獲取子目錄資源
for val in listuri:
# 遞歸調(diào)用,對(duì)當(dāng)前頁面比如解析到3個(gè)文件夾地址搁料,則3個(gè)文件夾地址都需調(diào)用或详,如果進(jìn)去的文件夾還有文件夾系羞,就繼續(xù)遞歸
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------------------------------------'
# 如果當(dāng)前頁面所有文件夾資源遍歷完畢,或者只有文件資源鸭叙,遞歸結(jié)束條件結(jié)束觉啊,返回解析到文件資源
return fileName_folder_fileUri
????
參數(shù)說明:
- baseFolderUrl:文件夾資源基本串,該網(wǎng)站使用的基本串+folderID(我們解析出來的)方式
- baseFileUrl:同理沈贝,只是文件資源基本串+fileId
- targetUrl:每次遞歸需要解析的頁面地址
解析過程大致如下杠人,類似深度優(yōu)先遍歷的過程,先對(duì)一個(gè)目錄遍歷到底宋下,再逐個(gè)從底層返回:
4.下載資源
????之前說到嗡善,下載方式要么一堆解析到的資源一次性下載,然后按照自己組裝解析文件夾 -- 子文件夾/文件資源對(duì)應(yīng)關(guān)系学歧,使用python的os模塊完成文件挪動(dòng)罩引,之前的想法是將這種文件夾層級(jí)關(guān)系組裝為json格式,方便處理枝笨。
????但是實(shí)際操作會(huì)發(fā)現(xiàn)袁铐,瀏覽器下載文件時(shí),對(duì)于中文文件名空格等總會(huì)加上些亂七八糟的字符横浑,如:
這對(duì)于文件名匹配可不是好事剔桨,解析時(shí)都是正常的文件名,下載后的這種必然匹配不上徙融,因此放棄洒缀。
另外,考慮到一次下載完資源欺冀,如果中途出現(xiàn)失敗也比較麻煩树绩,如圖:
????轉(zhuǎn)而采用配置chrome默認(rèn)下載目錄的方式,不過考慮到文件資源眾多隐轩,文件夾數(shù)量眾多饺饭,而且想使用selenium配置chrome下載目錄,必須每次配置都新啟動(dòng)一個(gè)實(shí)例才會(huì)生效龙助。
????考慮到上述情況砰奕,解決方案如下:
- 將資源切分多個(gè)子文件,逐個(gè)子文件下載
-
采用配置瀏覽器下載目錄方式提鸟,每隔新配置的實(shí)例只下載一個(gè)目錄下的所有文件資源連接军援。當(dāng)然,配置下載目錄前會(huì)先創(chuàng)建對(duì)應(yīng)目錄称勋。
# 先解析資源文件胸哥,得到所有資源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,應(yīng)該存儲(chǔ)的文件夾為value的dict赡鲜,所有資源組成list返回
uris = list(map(lambda x, y : [x, y], fileuris, folders))
# 資源總數(shù)
uri_count = len(uris)
print u'總計(jì)解析到%d個(gè)資源鏈接空厌。' % uri_count
# 組成文件夾--文件資源list的dict結(jié)構(gòu)
folder_fileuri = {}
# 初始化字典庐船,key為文件夾名
for folder in folders:
folder_fileuri[folder] = []
print u'初始化完成!總計(jì)文件夾個(gè)數(shù):%d' % len(folder_fileuri)
# 在將list解析為文件夾為key嘲更,對(duì)應(yīng)的文件資源地址list鏈接為value組成的dict筐钟,轉(zhuǎn)為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)
下載資源實(shí)現(xiàn):配置下載目錄,逐個(gè)資源get請(qǐng)求即可
def downloadFileList(fileList, downLoadPath):
# 聲明瀏覽器對(duì)象赋朦,配置下載默認(rèn)下載地址參數(shù)
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()
# 這里設(shè)置智能等待10s
browser.implicitly_wait(10)
# 網(wǎng)址
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()
# 點(diǎn)擊繼續(xù)使用篓冲,去掉遮罩層
browser.find_element_by_link_text(u'繼續(xù)使用網(wǎng)頁版').click()
# 點(diǎn)擊訪問技術(shù)文檔內(nèi)容
browser.get('訪問技術(shù)文檔地址')
# 切換到id為mainFrame的iframe上,才能獲取到文件夾列表內(nèi)容
browser.switch_to.frame("mainFrame") # 用id來定位
# 逐個(gè)下載
for fileuri in fileList:
print u'下載%s到目錄:%s ...' % (fileuri, downLoadPath)
browser.get(fileuri)
創(chuàng)建文件夾邏輯:
##
# @Brief 創(chuàng)建目錄
#
# @Param 待創(chuàng)建目錄
# @return 無返回
#
# @Details 測(cè)試目錄存在與否宠哄,無則創(chuàng)建目錄
#
def createDirIfNull(dir):
if(not os.path.exists(dir.strip())):
print u'目錄%s不存在壹将,現(xiàn)在創(chuàng)建...' % dir
調(diào)用下載的主邏輯:
# 遍歷下載,下載到不同文件夾
folder_coumt = 1
for folder in folder_fileuri: # folder_fileuri就是每次讀取資源文件解析成的文件夾為key毛嫉,對(duì)應(yīng)文件夾下文件資源鏈接list為value的dict
print u'開始下載%s文件夾對(duì)應(yīng)的資源...' % folder
createDirIfNull(folder)
downloadFileList(folder_fileuri[folder], folder)
if(folder_coumt%2 == 0):
print u'還有%s個(gè)文件夾所屬資源需要下載, 暫停5s...' % (str(len(folder_fileuri)-folder_coumt))
time.sleep(2)
folder_coumt = folder_coumt + 1
time.sleep(15)
# 下載完成
print u'所有文件下載完畢诽俯!'
下載后的成果:
5.總結(jié)
????雖然實(shí)現(xiàn)了下載功能,但是仍然有幾個(gè)問題:
(1). 反爬問題:
好在下載過程中并未出現(xiàn)承粤,其實(shí)應(yīng)該配合代理IP地址暴区,每次訪問通過代理IP請(qǐng)求下載,能夠避免很多問題辛臊。
(2). 代碼結(jié)構(gòu):
主邏輯部分較為混亂颜启,也沒花心思擺弄,改為類實(shí)現(xiàn)應(yīng)該能省去很多參數(shù)傳遞問題浪讳。
(3). chromedriver
selenium需要chromedriver,這是個(gè)可執(zhí)行文件涌萤,下載下來放到python安裝目錄或者當(dāng)前項(xiàng)目目錄即可淹遵,只有python能找到。至于如何匹配自己的chrome版本负溪,可以參考地址:https://www.cnblogs.com/xqtesting/p/8334997.html