轉載請注明來源, 原文鏈接 :
https://www.cnblogs.com/Laplacedoge/p/11828622.html
講真的, 手機看漫畫翻頁總是會手殘碰到頁面上的廣告好吧, 再碰上站點的帶寬還很低, 無疑是雪上加霜, 要是指定漫畫的主頁URL就能給我返回整本漫畫的所有圖片並且整理好存放在指定目錄就好了...
這促使我產生了使用Python 3來實現, 做一個 ComicReaper(漫畫收割者) 的想法!
本文所用漫畫鏈接 : http://www.manhuadb.com/manhua/2317
總體流程
那就開始吧
做一些准備工作
導入將會使用到Python的兩個庫, re 與 urllib
1 # 導入正則表達式 2 import re 3 # 導入 urllib.request 4 import urllib.request
先用字符串存儲兩個鏈接, 一個是本次漫畫網站站點的域名URL, 另一個是當前我們要爬取的漫畫主頁URL
再定義一個 header 字典, 用於存儲我們的 User-Agent 和 Referer Referrer (由於早期HTTP規范的拼寫錯誤, 為了保持向后兼容就將錯就錯了)
1 url_domainame = r'https://www.manhuadb.com' 2 url_host = r'https://www.manhuadb.com/manhua/2317' 3 header = { 4 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:70.0) Gecko/20100101 Firefox/70.0', 5 'Referer' : '' 6 }
首部字段 User-Agent
首部字段 User-Agent 告訴服務器當前創建請求的瀏覽器是什么(有的網站會針對不同的瀏覽器提供不同的頁面, 比如如果是手機瀏覽器提出的請求, 服務器就向客戶端提供網站的手機版頁面)
比如說同樣是請求 GitHub 的主頁, 左邊是使用筆記本電腦瀏覽器請求的頁面, 右邊是在安卓手機上請求的
首部字段 Referer
首部字段 Referer 告訴服務器當前請求的頁面是從哪個Web頁面發起的(一般情況下 Referer 字段用於防盜鏈)
有的網站不允許直接訪問站內的URL, 只能通過從主頁點擊鏈接來進行跳轉, 或者...我們在請求之前構建請求頭把 User-Agent 字段設置為主頁或發起頁即可
獲取章節目錄
一次性獲取所有的章節信息將會是一個不錯的選擇, 因為發起一次請求的代價很高(當網速較慢或者網站帶寬較低時, 延時很高)
我們要獲取當前漫畫所有章節的標題與URL(標題用於后期存儲時文件夾的命名, URL用於跳轉到當前章節的開始頁面)並且打包成字典存儲在列表中
對在瀏覽器中按下 [F12] 鍵打開開發者工具來對漫畫的章節頁面進行分析
我們可以看到頁面中有很多章節, 也就是章節跳轉鏈接, 每個鏈接的<a>標簽中正好具有我們需要的標題和URL, 分別是<a>標簽的 title 屬性與 href 屬性, 我們將使用字典來存儲它
先不慌着前進, 考慮到整個HTML中有非常多的鏈接, 那么也就意味着頁面中具有大量的<a>標簽, 如果我們只是單純地從HTML中過濾出<a>標簽, 這樣我們會得到大量我們並不需要的<a>標簽, 這是不明智的, 我們必須只過濾出章節跳轉鏈接的<a>標簽, 仔細觀察, 發現章節跳轉鏈接的<a>標簽們都具有一個特點, 那就是它們都具有 class 屬性並且屬性值為 "fixed-a-es" , 這就找到了一個可以定位章節<a>標簽的依據, 把這一點加入到我們的正則表達式的匹配規則中去
現在就可以定義一個正則表達式匹配字符串了(什么是正則表達式?)(在線正則表達式練習) :
pat = r'<a class="fixed-a-es" href="(.*?)" title="(.*?)"'
為什么要這么寫 :
- 在Python中, 在字符串常量的開頭加一個 'r' 表示本字符串中的 '\' 字符將不會用來作轉義字符使用, 保留了它原本的含義, 也就是反斜杠字符
- 在正則表達式中, '.' 字符用於匹配任何字符(當匹配時具有 're.S' 標志時此話成立, 否則只能匹配任意但除了 '\n' 以外的字符)
- 在正則表達式中, '*' 字符用於描述它左邊的匹配字符的出現次數為0次或若干次
- 在正則表達式中, '(.*?)' 的組合用來表示一個貪婪匹配(並且會被捕捉到), 至於什么是貪婪匹配, 可以看這位博主的這篇文章
使用這個正則表達式, 就可以匹配到 title 屬性與 href 屬性的屬性值中的雙引號里面的內容了
具體實現是 chapterIndexReaper 函數, 主要用來"收割"當前漫畫的所有章節並存儲為字典列表
代碼如下 :
1 #獲取一本漫畫的所有章節的目錄 2 def chapterIndexReaper(url_host, header): 3 # 定義一個臨時字典, 用於臨時存儲一個章節的標題與url 4 dic_temp = { 5 'Title' : '', 6 'Url' : '' 7 } 8 # 章節字典列表, 存儲當前漫畫的所有章節字典 9 set_dic = [] 10 # 構建Request對象 11 req = urllib.request.Request(url = url_host, headers = header) 12 # 讀取所請求的req並用utf-8編碼來進行解碼, 所得到的的字符串賦值給html 13 html = urllib.request.urlopen(req).read().decode('utf-8') 14 # 爬取漫畫章節標題與url的正則表達式 15 pat = r'<a class="fixed-a-es" href="(.*?)" title="(.*?)"' 16 # 使用pat在html中進行進行匹配(re.S參數是為了讓"."除了能夠匹配本身規定的字符, 17 # 另外也能匹配"\n"), 返回一個結果列表res 18 res = re.findall(pat, html, re.S) 19 for i in res: 20 dic_temp['Title'] = i[1] 21 dic_temp['Url'] = url_head + i[0] 22 # 向當前的章節字典列表的后面追加新的章節, 注意, 此處要使用淺拷貝 23 # (因為dic_temp是一個臨時變量, 需要創建它的副本並追加到set_dic中去, 24 # 否則當dic_temp刷新時set_dic中的元素會相應發生改變) 25 set_dic.append(dic_temp.copy()) 26 return set_dic
獲取章節全部頁面的URL目錄
我們有了所有章節的跳轉鏈接, 現在需要做的是獲取章節里面的所有頁面, 因為每一個頁面都包含一張我們所需要爬取的圖片
對第一章的頁面URL進行分析 :
第一頁(也是章節跳轉鏈接) :
https://www.manhuadb.com/manhua/2317/2860_52789.html
第二頁 :
https://www.manhuadb.com/manhua/2317/2860_52789_p2.html
第三頁 :
https://www.manhuadb.com/manhua/2317/2860_52789_p3.html
想必都能看出規律, 交給 pageIndexReaper 來生成並打包它即可 :
1 #獲取一個章節的所有頁碼的目錄 2 def pageIndexReaper(url, number): 3 set_page = [url] 4 url = url.replace('.html', '') 5 for i in range(2, number + 1): 6 set_page.append(url + '_p' + str(i) + '.html') 7 return set_page
這時候可能就有小朋友會問了 : "pageIndexReaper(url, number) 的 number 參數怎么來的? "
的確, 雖然我們能夠確定漫畫有多少個章節, 但我們卻不能確定每章有多少頁(每章的頁面數量是不確定的), 這時候我們就需要在頁面上尋找有沒有能夠提供總頁面數量的標簽了
正好這里有一個 :
分析其 HTML :
丟給 totalPageNumberReaper 去解決吧 :
1 #爬取一個章節的頁碼數 2 pat_totalPageNumber = r'<li class="breadcrumb-item active" aria-current="page".*?共 (.*?) 頁' 3 def totalPageNumberReaper(url, html = None): 4 if html == None: 5 hdr = header 6 hdr['Referer'] = url_host 7 req = urllib.request.Request(url = url, headers = hdr) 8 html = urllib.request.urlopen(req).read().decode('utf-8') 9 res = re.search(pat_totalPageNumber, html, re.S) 10 return int(res.group(1))
這里的 totalPageNumberReaper 函數擁有兩種調用方案 :
- 最原始的方法, 給出指定章節頁面的URL, 經過構建請求頭, 請求並讀取然后解碼, 最后再使用正則表達式匹配到章節總頁碼數
- 利用現有的 HTML 直接開始匹配章節總頁碼數(實在是因為請求一次頁面非常耗時間)
Laplacedoge說 : 能實時看到進度的爬蟲才是好爬蟲! 加上進度條來讓你掌控全局!
1 #進度條顯示 2 def showProgressBar(value, tip): 3 if value < 1: 4 print('\r', ' [{} : {:.2%}]'.format(tip, value), end = '', flush = True) 5 else: 6 print('\r', ' [{} : {:.2%}]'.format(tip, value), end = '' + '\n' + '[Done]' + '\n', flush = True)
事實上Python有很多第三方進度條庫, 各種實用而且炫酷的都有(比如受人稱贊的tqdm庫)
下載圖片並存儲到指定指定目錄
傳給 GO 函數漫畫的主頁URL以及在准備存儲在系統中的位置, 把剩下的事情交給 GO 去做吧!
1 #開始 2 pat_image = r'<img class="img-fluid show-pic" src="(.*?)"' 3 def GO(url_comic, path_save): 4 # 獲取所有章節目錄 5 index_chapter = chapterIndexReaper(url_comic, header) 6 # 對所有章節進行遍歷 7 for chapter in index_chapter: 8 # 獲取當前章節的頁面總數量 9 number = totalPageNumberReaper(chapter['Url']) 10 # 獲取當前章節的頁面URL目錄 11 index_page = pageIndexReaper(chapter['Url'], number) 12 # 編輯當前章節的路徑名, 即將以當前章節名為文件名在 path_save 路徑下創建一個文件夾 13 path_current = path_save + '/' + chapter['Title'] 14 # 使用os模塊創建目錄並給出777的權限(最開放的權限) 15 # 檢測章節目錄是否已經創建 16 if not os.path.exists(path_current): 17 os.makedirs(path_current) 18 os.chmod(path_current, 0o777) 19 # 用於標記當前頁碼 20 page_current = 1 21 print('\n[{} : 共{}頁]'.format(chapter['Title'], number)) 22 # 遍歷當前章節的所有頁面 23 for page in index_page: 24 # 編輯當前頁面圖片的路徑名 25 path_currentImage = path_current + '/' + chapter['Title'] + '_' + str(page_current) + '.jpg' 26 # 使用os模塊判斷是否存在當前照片 27 if os.path.exists(path_currentImage): #若是當前圖片存在 28 showProgressBar(page_current / number, 'Downloading ' + chapter['Title']) #顯示當前進度 29 page_current = page_current + 1 30 # 不存在就請求並存儲這張照片 31 else: 32 hdr = header 33 hdr['Referer'] = chapter['Url'] 34 req = urllib.request.Request(url = page, headers = hdr) 35 html = urllib.request.urlopen(req).read().decode('utf-8') 36 # 爬取當前頁面圖片 37 url_currentImage = re.search(pat_image, html, re.S).group(1) 38 # 下載當前圖片到指定目錄下 39 urllib.request.urlretrieve(url_currentImage, path_currentImage) 40 os.chmod(path_currentImage, 0o777) 41 # 顯示當前進度 42 showProgressBar(page_current / number, 'Downloading ' + chapter['Title']) 43 # 當前頁碼 + 1 44 page_current = page_current + 1 45 # 延時 1s, 太快會出大問題 46 time.sleep(1) 47 time.sleep(1)
正如之前的<a>標簽的情況一樣, 這張圖片使用的<img>標簽也在頁面中大量存在, 使用下圖中黃框的 class 屬性來對<img>標簽進行定位確保唯一, 紅框中的URL是我們要提取的內容
1 pat_image = r'<img class="img-fluid show-pic" src="(.*?)"'
最終爬取效果
在桌面的指定存儲路徑下 :
所有章節文件夾均以它們在原網頁上的標題命名 :
每一張圖片是章節 + 圖片標號的命名方式, 可在代碼中修改 :
關於斷點續爬功能
首先說說為什么需要這個功能?
事實上要知道, 如果在爬取的過程中斷網了, 那將會變成一件非常揪心的事情——這意味着你要全部從頭開始重新爬(會覆蓋原來已經爬取的內容)
所以斷點續爬提供了存在圖片自動檢測功能, 讓您爬得舒心, 用得安心