Python3網路爬蟲:漫畫下載,動態載入、反爬蟲這都不叫事!
1
前言
經過上兩篇文章的學習,爬蟲三步走:發起請求、解析資料、儲存資料,已經掌握,算入門爬蟲了嗎?
很多人學習python,不知道從何學起。
很多人學習python,掌握了基本語法過後,不知道在哪裡尋找案例上手。
很多已經做案例的人,卻不知道如何去學習更加高深的知識。
那麼針對這三類人,我給大家提供一個好的學習平臺,免費領取視訊教程,電子書籍,以及課程的原始碼!
QQ群:101677771
不,還遠遠不夠!只掌握這些,還只能算門外漢級別。
今天,就來帶大家繼續學習,怎麼爬的更優雅!
按照慣例,還是從實戰出發,今天咱們就爬個圖片,盤點那些遇到的問題,和優雅的解決方案。
本文男女老少皆宜,什麼妹子圖、肌肉男,學會了本文的方法,一切盡收囊中!
2
實戰背景
咱不來吸睛勁爆的圖片下載,咱來點清淡的家常菜。
動漫之家漫畫下載!
這個實戰,你會遇到動態載入、初級反爬,會了本文的方法,你還怕爬不到心心念的"美圖"嗎?
3
漫畫下載
咱不下載整站資源,就挑一本下載,別給伺服器太大壓力。
挑來挑去,找了本動漫之家排名靠前的一本《妖神記》,說實話,看了漫畫第一章的內容,濃濃的火影氣息。
URL:https://www.dmzj.com/info/yaoshenji.html
想下載這本動漫,我們需要儲存所有章節的圖片到本地。我們先捋捋思路:
-
拿到所有章節名和章節連結
-
根據章節連結章節裡的所有漫畫圖片
-
根據章節名,分類儲存漫畫
看似簡單,實際做起來,可能遇到各種各樣的問題,讓我們一起優雅的解決這些問題吧!
獲取章節名和章節連結
一個網頁,是由很多div
元素組成的,比如這個樣子。
不同的div
存放不同的內容,如上圖,有存放標題Jack Cui的div
,有存放選單的div
,有存放正文內容的div
,有存放版權資訊的div
。
瞧,不難發現,只要拿到class
屬性為zj_list
的div
標籤,就能拿到章節名和章節連結,都存放在這個div
標籤下的a
標籤中。
再仔細觀察一番,你會發現,div
標籤下還有個ul
標籤,ul
標籤是距離a
標籤最近的標籤。
用上一篇文章講解的BeautifulSoup,實際上直接匹配最近的class
list_con_li
的ul
標籤即可。編寫如下程式碼:
import requests
from bs4 import BeautifulSoup
target_url = "https://www.dmzj.com/info/yaoshenji.html"
r = requests.get(url=target_url)
bs = BeautifulSoup(r.text, 'lxml')
list_con_li = bs.find('ul', class_="list_con_li")
comic_list = list_con_li.find_all('a')
chapter_names = []
chapter_urls = []
for comic in comic_list:
href = comic.get('href')
name = comic.text
chapter_names.insert(0, name)
chapter_urls.insert(0, href)
print(chapter_names)
print(chapter_urls)
瞧,章節名和章節連結搞定了!
沒有難度啊?別急,難的在後面。
獲取漫畫圖片地址
我們只要分析在一個章節裡怎麼獲取圖片,就能批量的在各個章節獲取漫畫圖片。
我們先看第一章的內容。
URL:https://www.dmzj.com/view/yaoshenji/41917.html
開啟第一章的連結,你會發現,連結後面自動添加了#@page=1。
你翻頁會發現,第二頁的連結是後面加了#@page=2,第三頁的連結是後面加了#@page=3,以此類推。
但是,這些並不是圖片的地址,而是這個展示頁面的地址,要下載圖片,首先要拿到圖片的真實地址。
審查元素找圖片地址,你會發現,這個頁面不能右鍵!
這就是最最最最低階的反爬蟲手段,這個時候我們可以通過鍵盤的F12調出審查元素視窗。
有的網站甚至把F12都禁掉,這種也是很低階的反爬蟲手段,騙騙剛入門的手段而已。
面對這種禁止看頁面原始碼的初級手段,一個優雅的通用解決辦法是,在連線前加個view-source:
。
view-source:https://www.dmzj.com/view/yaoshenji/41917.html
用這個連結,直接看的就是頁面原始碼。
更簡單的辦法是,將滑鼠焦點放在瀏覽器位址列,然後按下F12依然可以調出除錯視窗。
這個漫畫網站,還是可以通過F12審查元素,調出除錯視窗的。
我們可以在瀏覽器除錯視窗中的Network裡找到這個頁面載入的內容,例如一些css檔案啊、js檔案啊、圖片啊,等等等。
要找圖片的地址,直接在這裡找,別在html頁面裡找,html資訊那麼多,一條一條看得找到猴年馬月。
在Network中可以很輕鬆地找到我們想要的圖片真實地址,除錯工具很強大,Headers可以看一些請求頭資訊,Preview可以瀏覽返回資訊。
搜尋功能,過濾功能等等,應有盡有,具體怎麼用,自己動手點一點,就知道了!
好了,拿到了圖片的真實地址,我們看下連結:
https://images.dmzj.com/img/chapterpic/3059/14237/14395217739069.jpg
這就是圖片的真實地址,拿著這個連結去html頁面中搜索,看下它存放在哪個img
標籤裡了,搜尋一下你會發現,瀏覽器中的html頁面是有這個圖片連結的。
但你是用view-source:
開啟這個頁面,你會發現你搜索不到這個圖片連結。
view-source:https://www.dmzj.com/view/yaoshenji/41917.html
記住,這就說明,這個圖片是動態載入的!
使用view-source:
方法,就是看頁面原始碼,並不管動態載入的內容。這裡面沒有圖片連結,就說明圖片是動態載入的。
是不是判斷起來很簡單?
遇到動態載入不要慌,使用JavaScript動態載入,無外乎兩種方式:
-
外部載入
-
內部載入
外部載入就是在html頁面中,以引用的形式,載入一個js,例如這樣:
<script type="text/javascript" src="https://cuijiahua.com/call.js"></script>
這段程式碼得意思是,引用cuijiahua.com域名下的call.js檔案。
內部載入就是Javascript指令碼內容寫在html內,例如這個漫畫網站。
這時候,就可以用搜索功能了,教一個搜尋小技巧。
https://images.dmzj.com/img/chapterpic/3059/14237/14395217739069.jpg
圖片連結是這個,那就用圖片的名字去掉字尾,也就是14395217739069
在瀏覽器的除錯頁面搜尋,因為一般這種動態載入,連結都是程式合成的,搜它準沒錯!
<script type="text/javascript">
var arr_img = new Array();
var page = '';
eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('g f=\'{"e":"h","i":"0","l":"k\\/3\\/5\\/2\\/j.4\\r\\6\\/3\\/5\\/2\\/d.4\\r\\6\\/3\\/5\\/2\\/7.4\\r\\6\\/3\\/5\\/2\\/8.4\\r\\6\\/3\\/5\\/2\\/c.4\\r\\6\\/3\\/5\\/2\\/b.4\\r\\6\\/3\\/5\\/2\\/a.4\\r\\6\\/3\\/5\\/2\\/9.4\\r\\6\\/3\\/5\\/2\\/m.4\\r\\6\\/3\\/5\\/2\\/v.4\\r\\6\\/3\\/5\\/2\\/A.4\\r\\6\\/3\\/5\\/2\\/n.4\\r\\6\\/3\\/5\\/2\\/B.4\\r\\6\\/3\\/5\\/2\\/x.4\\r\\6\\/3\\/5\\/2\\/y.4","w":"p","o":"1","q":"\\s\\u \\t\\z"}\';',38,38,'||14237|chapterpic|jpg|3059|nimg|14395217891719|14395217893745|14395217913416|14395217908431|14395217904781|1439521790086|1439521788936|id|pages|var|41917|hidden|14395217739069|img|page_url|14395217918734|14395217931135|chapter_order|15|chapter_name||u7b2c01|u91cd|u8bdd|14395217923415|sum_pages|14395217940216|14395217943921|u751f|14395217926321|1439521793602'.split('|'),0,{}))
</script>
不出意外,你就能看到這段程式碼,14395217739069
就混在其中!
看不懂Javascript,怎麼辦啊?
沒關係,說實話,我看著也費勁兒。
那咱們就找找規律,分析分析,看看能不能優雅的解決這個動態載入問題,我們再看這個圖片連結:
https://images.dmzj.com/img/chapterpic/3059/14237/14395217739069.jpg
連結中的數字是不是眼熟?
這不就是這幾個數字合成的嗎?
好了,我有個大膽的想法!直接把這些長的數字搞出來,合成下連結試試看。
import requests
from bs4 import BeautifulSoup
import re
url = 'https://www.dmzj.com/view/yaoshenji/41917.html'
r = requests.get(url=url)
html = BeautifulSoup(r.text, 'lxml')
script_info = html.script
pics = re.findall('\d{13,14}', str(script_info))
chapterpic_hou = re.findall('\|\|(\d{5})', str(script_info))[0]
chapterpic_qian = re.findall('\|jpg\|(\d{4})', str(script_info))[0]
for pic in pics:
url = 'https://images.dmzj.com/img/chapterpic/' + chapterpic_qian + '/' + chapterpic_hou + '/' + pic + '.jpg'
print(url)
執行程式碼,你可以得到如下結果:
踏破鐵鞋無覓處,得來全不費工夫!
比對一下你會發現,這些,還真就是漫畫圖片的連結!
但是有個問題,這麼合成的的圖片連結不是按照漫畫順序的,這下載下來漫畫圖片都是亂的啊!不優雅!
這個網站也是人寫的嘛!是人,就好辦!慣性思維,要是你,是不是小數放在前面,大數放在後面?這些長的數字裡,有13位的,有14位的,並且都是以14開頭的數字,那我就賭它末位補零後的結果,就是圖片的順序!
import requests
from bs4 import BeautifulSoup
import re
url = 'https://www.dmzj.com/view/yaoshenji/41917.html'
r = requests.get(url=url)
html = BeautifulSoup(r.text, 'lxml')
script_info = html.script
pics = re.findall('\d{13,14}', str(script_info))
for idx, pic in enumerate(pics):
if len(pic) == 13:
pics[idx] = pic + '0'
pics = sorted(pics, key=lambda x:int(x))
chapterpic_hou = re.findall('\|\|(\d{5})', str(script_info))[0]
chapterpic_qian = re.findall('\|jpg\|(\d{4})', str(script_info))[0]
for pic in pics:
if pic[-1] == '0':
url = 'https://images.dmzj.com/img/chapterpic/' + chapterpic_qian + '/' + chapterpic_hou + '/' + pic[:-1] + '.jpg'
else:
url = 'https://images.dmzj.com/img/chapterpic/' + chapterpic_qian + '/' + chapterpic_hou + '/' + pic + '.jpg'
print(url)
程式對13位的數字,末位補零,然後排序。
在跟網頁的連結按順序比對,你會發現沒錯!就是這個順序!
不用讀懂Javascript合成連結程式碼,直接分析測試,夠不夠優雅?
下載圖片
萬事俱備,只欠東風!
使用其中一個圖片連結,用程式碼下載試試。
import requests
from urllib.request import urlretrieve
dn_url = 'https://images.dmzj.com/img/chapterpic/3059/14237/14395217739069.jpg'
urlretrieve(dn_url,'1.jpg')
通過urlretrieve
方法,就可以下載,這是最簡單的下載方法。第一個引數是下載連結,第二個引數是下載後的檔案儲存名。
不出意外,就可以順利下載這張圖片!
但是,意外發生了!
出現了HTTP Error,錯誤程式碼是403。
403表示資源不可用,這是又是一種典型的反扒蟲手段。
別慌,我們再分析一波!
開啟這個圖片連結:
https://images.dmzj.com/img/chapterpic/3059/14237/14395217739069.jpg
這個地址就是圖片的真實地址,在瀏覽器中開啟,可能直接無法開啟,或者能開啟,但是一重新整理就又不能打開了!
如果開啟章節頁面後,再開啟這個圖片連結就又能看到圖片了。
章節URL:
https://www.dmzj.com/view/yaoshenji/41917.html
記住,這就是一種典型的通過Referer的反扒爬蟲手段!
Referer可以理解為來路,先開啟章節URL連結,再開啟圖片連結。開啟圖片的時候,Referer的資訊裡儲存的是章節URL。
動漫之家網站的做法就是,站內的使用者訪問這個圖片,我就給他看,從其它地方過來的使用者,我就不給他看。
是不是站內使用者,就是根據Referer進行簡單的判斷。
這就是很典型的,反爬蟲手段!
解決辦法也簡單,它需要啥,咱給它就完了。
import requests
from contextlib import closing
download_header = {
'Referer': 'https://www.dmzj.com/view/yaoshenji/41917.html'
}
dn_url = 'https://images.dmzj.com/img/chapterpic/3059/14237/14395217739069.jpg'
with closing(requests.get(dn_url, headers=download_header, stream=True)) as response:
chunk_size = 1024
content_size = int(response.headers['content-length'])
if response.status_code == 200:
print('檔案大小:%0.2f KB' % (content_size / chunk_size))
with open('1.jpg', "wb") as file:
for data in response.iter_content(chunk_size=chunk_size):
file.write(data)
else:
print('連結異常')
print('下載完成!')
使用closing
方法可以設定Headers資訊,這個Headers資訊裡儲存Referer來路,就是第一章的URL,最後以寫檔案的形式,儲存這個圖片。
下載完成!就是這麼簡單!
4
漫畫下載
將程式碼整合在一起,下載整部漫畫。編寫程式碼如下:
import requests
import os
import re
from bs4 import BeautifulSoup
from contextlib import closing
from tqdm import tqdm
import time
"""
Author:
Jack Cui
Wechat:
https://mp.weixin.qq.com/s/OCWwRVDFNslIuKyiCVUoTA
"""
# 建立儲存目錄
save_dir = '妖神記'
if save_dir not in os.listdir('./'):
os.mkdir(save_dir)
target_url = "https://www.dmzj.com/info/yaoshenji.html"
# 獲取動漫章節連結和章節名
r = requests.get(url = target_url)
bs = BeautifulSoup(r.text, 'lxml')
list_con_li = bs.find('ul', class_="list_con_li")
cartoon_list = list_con_li.find_all('a')
chapter_names = []
chapter_urls = []
for cartoon in cartoon_list:
href = cartoon.get('href')
name = cartoon.text
chapter_names.insert(0, name)
chapter_urls.insert(0, href)
# 下載漫畫
for i, url in enumerate(tqdm(chapter_urls)):
download_header = {
'Referer': url
}
name = chapter_names[i]
# 去掉.
while '.' in name:
name = name.replace('.', '')
chapter_save_dir = os.path.join(save_dir, name)
if name not in os.listdir(save_dir):
os.mkdir(chapter_save_dir)
r = requests.get(url = url)
html = BeautifulSoup(r.text, 'lxml')
script_info = html.script
pics = re.findall('\d{13,14}', str(script_info))
for j, pic in enumerate(pics):
if len(pic) == 13:
pics[j] = pic + '0'
pics = sorted(pics, key=lambda x:int(x))
chapterpic_hou = re.findall('\|(\d{5})\|', str(script_info))[0]
chapterpic_qian = re.findall('\|(\d{4})\|', str(script_info))[0]
for idx, pic in enumerate(pics):
if pic[-1] == '0':
url = 'https://images.dmzj.com/img/chapterpic/' + chapterpic_qian + '/' + chapterpic_hou + '/' + pic[:-1] + '.jpg'
else:
url = 'https://images.dmzj.com/img/chapterpic/' + chapterpic_qian + '/' + chapterpic_hou + '/' + pic + '.jpg'
pic_name = '%03d.jpg' % (idx + 1)
pic_save_path = os.path.join(chapter_save_dir, pic_name)
with closing(requests.get(url, headers = download_header, stream = True)) as response:
chunk_size = 1024
content_size = int(response.headers['content-length'])
if response.status_code == 200:
with open(pic_save_path, "wb") as file:
for data in response.iter_content(chunk_size=chunk_size):
file.write(data)
else:
print('連結異常')
time.sleep(10)
大約40分鐘,漫畫即可下載完成!
還是那句話,我們要做一個友好的爬蟲。寫爬蟲,要謹慎,勿給伺服器增加過多的壓力,滿足我們的獲取資料的需求,這就夠了。
你好,我也好,大家好才是真的好。