通過Python爬蟲抓取漫畫圖片

bb01.jpg

無聊瀏覽某漫畫網站(你懂的。-_-),每次翻頁時都需要重新請求整個頁面,頁面雜七雜八的內容過多,導致頁面加載過程耗時略長。于是決定先把圖片先全部保存到本地。本文的主要內容,就是講解如何通過一個爬蟲程序,自動將所有圖片抓取到本地。

先來看網頁大概張什么樣。:)

漫畫封面列表界面:

1.jpg

每部漫畫點擊進去的界面

2.jpg

漫畫列表每一頁有幾十部漫畫,網站暫時有15頁列表,每部漫畫頁數有10+頁到近200頁不等,所以所有漫畫包含的圖片總數還是比較可觀的,通過手動將每張圖右鍵另存基本不可能完成。接下來將一步一步展示,如何用Python實現一個簡單爬蟲的程序,將網頁上所有漫畫全保存到本地。Python用的是2.7版本。

<h4>首先,何為網絡爬蟲?</h4>

網絡爬蟲(又被稱為網頁蜘蛛,網絡機器人),是一種按照一定的規則,自動地抓取萬維網信息的程序或者腳本。

一般而言,簡單的爬蟲實現主要分為兩步:

  • 獲取指定網頁html源碼;
  • 通過正則表達式提取出源碼中的目標內容。

<h3>本文程序實現大體步驟:</h3>

  1. 獲取每頁漫畫列表的url;
  2. 獲取每頁漫畫列表中每一部漫畫的url;
  3. 獲取每部漫畫每一頁圖片的url;
  4. 通過圖片的url將圖片保存到本地。

(總之就是通過for循環遍歷的過程。)

最后會附本文Demo完整源碼。

<h3>獲取每頁漫畫列表的url</h3>

程序最開始處理的頁面為:http://www.xeall.com/shenshi,即漫畫列表的首頁。紳士,你懂的。。

先創建一個文件Gentleman.py,引入需要用到的庫:

#coding:utf-8

import urllib2
import re
import os
import zlib

定義一個類,和初始化函數:

class Gentleman:
    def __init__(self, url, path):
        exists = os.path.exists(path)
        if not exists:
            print "文件路徑無效."
            exit(0)

        self.base_url = url
        self.path = path

url為漫畫列表首頁url

path為圖片保存在本地的路徑,做個錯誤檢測,判斷本地是否存在對應的路徑

接下來定義 get_content 方法,獲取指定url的html源碼內容:

def get_content(self, url):
    # 打開網頁
    try:
        request = urllib2.Request(url)
        response = urllib2.urlopen(request, timeout=20)

        # 將網頁內容解壓縮
        decompressed_data = zlib.decompress(response.read(), 16 + zlib.MAX_WBITS)

        # 網頁編碼格式為 gb2312
        content = decompressed_data.decode('gb2312', 'ignore')
        # print content
        return content
    except Exception, e:
        print e
        print "打開網頁: " + url + "失敗."
        return None

urllib2.Request() 通過指定的url構建一個請求,urllib2.urlopen() 獲取請求返回的結果,timeout設為20秒無響應則做超時處理。urllib2.urlopen() 有可能出現打開網頁錯誤的情況,所以做了異常處理,保證在返回40X50X之類的時候程序不會退出。后續相關的操作也都會做異常的處理。

原網頁為壓縮過的,請求到之后必須先進行解壓縮處理。

通過 charset=gb2312 可知解碼所需格式。

判斷網頁是否為壓縮的編碼類型,可以通過打印語句:

response.info().get('Content-Encoding')

打印結果為:

'gzip'

函數的返回結果為頁面html源碼文本。

跑起來試試:

url = "http://www.xeall.com/shenshi"
save_path = "/Users/moshuqi/Desktop/cartoon"

gentleman = Gentleman(url, save_path)
content = gentleman.get_content(url)

print content

可以看到打印結果:

3.jpg

接下來分析源碼文件,看如何從中獲取到每一頁漫畫列表的url。

頁面上有個選擇頁的控件:

4.jpg

通過分析對應控件的源碼,可知具體每一頁所對應的url:

5.jpg

控件的 namesldd ,通過搜索全文發現只有這一處“sldd”字段,所以該字段可用來做標識。

option value 的值即為對應頁的url。

定義 get_page_url_arr 方法,獲取每一頁的url,返回一個數組:

def get_page_url_arr(self, content):
    pattern = re.compile('name=\'sldd\'.*?>(.*?)</select>', re.S)
    result = re.search(pattern, content)
    option_list = result.groups(1)

    pattern = re.compile('value=\'(.*?)\'.*?</option>', re.S)
    items = re.findall(pattern, option_list[0])

    arr = []
    for item in items:
        page_url = self.base_url + '/' + item
        arr.append(page_url)

    print "total pages: " + str(len(arr))
    return arr

傳入的 content 為頁面源碼,首先通過 sldd 獲取到其中 option 的內容。采用正則表達式將內容提取出來。(關于正則表達式,不熟悉的同學推薦看這本 【正則表達式必知必會】,很薄的一本書,半天時間大概翻一遍基本就能用來處理大部分常見問題了)

正則表達式用了Python re模塊,具體方法的使用請自行百度, 這里只大概說一下思路。

首先用到的匹配模式:

'name=\'sldd\'.*?>(.*?)</select>'

'' 的轉義字符,匹配先找到以 name='sldd' 開頭的字符,.?* 是一個非貪婪匹配,用來匹配 name='sldd' 之后到最近的一個 > 之間的內容。(.?)* 意義和前一個類似,加上 () 表示為分組,可以在匹配結果中訪問,即我們需要識別出的內容。結尾的 select 標簽即為識別內容最后的標記。

運行后 option_list 識別到的內容應為:

<option value='p1.html' selected>1</option>
<option value='p2.html'>2</option>
<option value='p3.html'>3</option>
<option value='p4.html'>4</option>
<option value='p5.html'>5</option>
<option value='p6.html'>6</option>
<option value='p7.html'>7</option>
<option value='p8.html'>8</option>
<option value='p9.html'>9</option>
<option value='p10.html'>10</option>
<option value='p11.html'>11</option>
<option value='p12.html'>12</option>
<option value='p13.html'>13</option>
<option value='p14.html'>14</option>
<option value='p15.html'>15</option>

再正對 option_list 的內容進行識別,獲取到每個 option 的值,匹配模式為:

'value=\'(.*?)\'.*?</option>'

取到的值和一開始 base_url 連接拼接起來即為每一頁的url。

測試一下:

arr = gentleman.get_page_url_arr(content)
print arr

打印結果:

[u'http://www.xeall.com/shenshi/p1.html', u'http://www.xeall.com/shenshi/p2.html', u'http://www.xeall.com/shenshi/p3.html', u'http://www.xeall.com/shenshi/p4.html', u'http://www.xeall.com/shenshi/p5.html', u'http://www.xeall.com/shenshi/p6.html', u'http://www.xeall.com/shenshi/p7.html', u'http://www.xeall.com/shenshi/p8.html', u'http://www.xeall.com/shenshi/p9.html', u'http://www.xeall.com/shenshi/p10.html', u'http://www.xeall.com/shenshi/p11.html', u'http://www.xeall.com/shenshi/p12.html', u'http://www.xeall.com/shenshi/p13.html', u'http://www.xeall.com/shenshi/p14.html', u'http://www.xeall.com/shenshi/p15.html']

<h3>獲取每一頁包含的漫畫的url</h3>

例如,先對第一頁做處理http://www.xeall.com/shenshi/p1.html

打開網頁,找到這部分所對應的源碼:

6.jpg

瀏覽器上展示這部分的源碼很長,而且沒有換行,我們可以將文本拷貝到本地的編輯器上再進行分析。

可以看到內容比較長,以下兩張圖分別是開頭和結尾的截圖,紅色圈出的內容可作為識別這段文本的開頭和結尾。

7.jpg
8.jpg

定義 get_cartoon_arr 方法,獲取每一頁包含的漫畫的url,返回一個數組:

def get_cartoon_arr(self, url):
    content = self.get_content(url)
    if not content:
        print "獲取網頁失敗."
        return None

    pattern = re.compile('class="piclist listcon".*?>(.*?)</ul>', re.S)
    result = re.search(pattern, content)
    cartoon_list = result.groups(1)

    pattern = re.compile('href="/shenshi/(.*?)".*?class="pic show"', re.S)
    items = re.findall(pattern, cartoon_list[0])

    arr = []
    for item in items:
        # print item
        page_url = self.base_url + '/' + item
        arr.append(page_url)

    return arr

匹配模式:

'class="piclist listcon".*?>(.*?)</ul>'

識別出漫畫列表的內容,內容太多就不打印了。

下圖為每一部漫畫所包含的信息,例如展示的第一部漫畫為圈出的部分。

9.jpg

我們只需要提取到 href 中的信息,可以分別用 /shenshi/class="pic show" 作為開頭結尾,提取出 “10444.html” 鏈接信息。

匹配模式為:

'href="/shenshi/(.*?)".*?class="pic show"'

將提取的結果和 base_url 連接拼接起來即為漫畫的url。

測試代碼:

arr = gentleman.get_cartoon_arr("http://www.xeall.com/shenshi/p1.html")
print arr

打印結果:

[u'http://www.xeall.com/shenshi/10444.html', u'http://www.xeall.com/shenshi/10440.html', u'http://www.xeall.com/shenshi/10423.html', u'http://www.xeall.com/shenshi/10414.html', u'http://www.xeall.com/shenshi/10406.html', ...]

<h3>創建Cartoon類</h3>

用來專門處理每一部漫畫的類,要處理的細節較多,所以專門封裝成一個類來實現。

類的初始化函數,參數 url 為漫畫地址

#coding:utf-8

import urllib2
import re
import zlib
import os

class Cartoon:
    def __init__(self, url):
        self.base_url = "http://www.xeall.com/shenshi"
        self.url = url

Cartoon類中也會包含 get_content 函數,和之前的實現方式一樣,這里就不列出來了。當然,好的實現方式應該避免重復代碼,我懶得整理直接拷過來了。- -

定義獲取漫畫名的方法 get_title ,因為漫畫名后面要用來作為保存每部漫畫的文件夾名稱。

def get_title(self, content):
    pattern = re.compile('name="keywords".*?content="(.*?)".*?/', re.S)
    result = re.search(pattern, content)

    if result:
        title = result.groups(1)
        return title[0]
    else:
        print "獲取標題失敗。"
        return None

我們用這部漫畫做測試http://www.xeall.com/shenshi/10444.html

測試代碼:

title = cartoon.get_title(content)
print title

打印結果:

紳士漫畫:CURE UP↑↑秘密的寶島 (魔法少女同人志)

接下來需要獲取到翻頁對應的url,翻頁部分見下圖:

11.jpg

定義獲取漫畫每一頁的url方法 get_page_url_arr

def get_page_url_arr(self, content):
    pattern = re.compile('class="pagelist">(.*?)</ul>', re.S)
    result = re.search(pattern, content)
    page_list = result.groups(1)

    pattern = re.compile('<a href=\'(.*?)\'>.*?</a>', re.S)
    items = re.findall(pattern, page_list[0])

    arr = []
    for item in items:
        page_url = self.base_url + "/" + item
        arr.append(page_url)

    # pagelist中還包含了上一頁和下一頁,根據網頁格式可知分別在開始和結束,所以去掉首尾元素避免重復
    arr.pop(0)
    arr.pop(0)
    arr.pop(len(arr) - 1)
    return arr

打開網頁源碼:

10.jpg

可以看到圈出部分為每一頁按鈕對應的信息。

通過匹配模式獲取到這部分內容;

'class="pagelist">(.*?)</ul>'

對這部分內容進行匹配,獲取每一頁的url:

'<a href=\'(.*?)\'>.*?</a>'

看下圖會發現,前兩個鏈接為 href='#' ,最后一個為下一頁鏈接,重復了。

12.jpg

所以得到數組把這3個元素剔除掉。(至于為什么把 href='#' 都去掉,因為用這個鏈接后面會出錯,具體原因懶得追究,反正去掉之后也就少了第一頁的封面而已。)

arr.pop(0)
arr.pop(0)
arr.pop(len(arr) - 1)

測試代碼:

arr = cartoon.get_page_url_arr(content)
print arr

打印結果:

[u'http://www.xeall.com/shenshi/10444_2.html', u'http://www.xeall.com/shenshi/10444_3.html', u'http://www.xeall.com/shenshi/10444_4.html', ...]

<h3>獲取每頁漫畫圖片的url</h3>

例如針對之前獲取到數組的第一個元素:http://www.xeall.com/shenshi/10444_2.html

打開網頁,查看源碼:
![Uploading 14_014335.jpg . . .]

13.jpg

找到頁面圖片對應的代碼,通過搜索 img alt 可知文件中只有一處,所以可以用這個字段用來標識。

定義獲取圖片url的函數 get_pic_url

def get_pic_url(self, page_url):
    content = self.get_content(page_url)
    if not content:
        return None

    pattern = re.compile('<img alt.*?src="(.*?)".*?/>', re.S)
    result = re.search(pattern, content)

    if result:
        pic = result.groups(1)
        return pic[0]
    else:
        print "獲取圖片地址失敗。"
        print "url: " + page_url
        return None

匹配模式為:

'<img alt.*?src="(.*?)".*?/>'

這里最初使用了后面的 p 標簽結束符作為結束標識,后來發現極少數部分頁面用的不是 p 而是 br,因而導致圖片url獲取失敗。

測試代碼:

url = cartoon.get_pic_url("http://www.xeall.com/shenshi/10444_2.html")
print url

打印結果:

http://tu.zzbzwy.com/xeall/uploadfile/gx02/160904/ww02.jpg

點擊打開看到直接就是一張圖片

14.jpg

<h3>將圖片保存到本地</h3>

定義保存函數的方法 save_pic

def save_pic(self, pic_url, path):
    req = urllib2.Request(pic_url)
    req.add_header('User-Agent', 'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36')
    req.add_header('GET', pic_url)

    try:
        print "save pic url:" + pic_url
        resp = urllib2.urlopen(req, timeout=20)
        data = resp.read()
        # print data

        fp = open(path, "wb")
        fp.write(data)
        fp.close
        print "save pic finished."
    except Exception, e:
        print e
        print "save pic: " + pic_url + " failed."

pic_url 為圖片鏈接,path 為圖片保存到本地的路徑。

這遇到了個坑,Request 請求若不設置 User-Agent 信息會導致返回 403 Forbidden,服務器可能對訪問做了些限制。

最后Cartoon類定義一個對外的 save 方法,將漫畫所有圖片保存到本地

def save(self, path):
    dir_path = path + "/" + self.title
    self.create_dir_path(dir_path)

    for i in range(0, len(self.page_url_arr)):
        page_url = self.page_url_arr[i]
        pic_url = self.get_pic_url(page_url)
        if pic_url == None:
            continue

        pic_path = dir_path + "/" + str(i + 1) + ".jpg"
        self.save_pic(pic_url, pic_path)

    print self.title + " fetch finished."

給漫畫創建創建本地文件夾,文件夾名稱為漫畫名

def create_dir_path(self, path):
    # 以漫畫名創建文件夾
    exists = os.path.exists(path)
    if not exists:
        print "創建文件夾"
        os.makedirs(path)
    else:
        print "文件夾已存在"

完成的初始化函數:

 def __init__(self, url):
    self.base_url = "http://www.xeall.com/shenshi"
    self.url = url

    content = self.get_content(self.url)
    if not content:
        print "Cartoon init failed."
        return

    self.title = self.get_title(content)
    self.page_url_arr = self.get_page_url_arr(content)           

下載一部完整漫畫的測試代碼:

url = "http://www.xeall.com/shenshi/10444.html"
save_path = "/Users/moshuqi/Desktop/cartoon"

cartoon = Cartoon(url)
cartoon.save(save_path)

運行起來,看看置頂文件夾結果:

15.jpg

<h3>將Cartoon類和Gentlman類結合,爬取所有漫畫圖片</h3>

Gentleman 完整的初始化函數:

def __init__(self, url, path):
    exists = os.path.exists(path)
    if not exists:
        print "文件路徑無效."
        exit(0)

    self.base_url = url
    self.path = path
    content = self.get_content(url)

    self.page_url_arr = self.get_page_url_arr(content)

外部調用的接口方法,遍歷所有頁和所有漫畫:

def hentai(self):
    # 遍歷每一頁的內容
    for i in range(0, len(self.page_url_arr)):
        # 獲取每一頁漫畫的url
        cartoon_arr = self.get_cartoon_arr(self.page_url_arr[i])
        print "page " + str(i + 1) + ":"
        print cartoon_arr
        for j in range(0, len(cartoon_arr)):
            cartoon = Cartoon(cartoon_arr[j])
            cartoon.save(self.path)
        print "======= page " + str(i + 1) + " fetch finished ======="

最終的結果跑起來:

url = "http://www.xeall.com/shenshi"
save_path = "/Users/moshuqi/Desktop/cartoon"

gentleman = Gentleman(url, save_path)
gentleman.hentai()       

打印輸出:

16.jpg

可以看到置頂的文件夾里不斷生成新的漫畫文件夾和圖片。

<h3>圖片爬取結果</h3>

17.jpg

18.jpg

19.jpg

程序連續跑了幾個小時,一共抓取500+部漫畫,1W5+張圖片,文件總大小4G+。大概如上圖所示。- -

<h3>其他</h3>

程序里還做了些其他處理。偶爾會出現圖片請求失敗,導致一部漫畫缺少幾頁,對于這種情況的漫畫做處理時,通過判斷只對缺失的頁做請求。

重新跑程序時,每部漫畫都會先拿服務器上的總頁數與本地頁數做對比,若大于或等于(為什么不是等于,因為Mac系統會在文件夾里生成.DS_store文件。。)則說明該部漫畫已爬取完成,不再重新做處理。

總之就是避免重新運行程序時避免重復數據的爬取。處理在Demo代碼里面,已加上注釋說明。

<h3>最后。</h3>

本文只是展示了網絡爬蟲基本使用的大體思路而已,程序可以優化的地方還很多,如爬取時通過多線程,或用現成的爬蟲框架之類等等等。讀者請自行思考完善。當然,如果你想留郵箱的話。。不先去start下?:)

本文完整源碼

原文地址

完。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,559評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,442評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,581評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,922評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,096評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,639評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,374評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,591評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,789評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,322評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,554評論 2 379

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,739評論 25 708
  • 在之前一篇抓取漫畫圖片的文章里,通過實現一個簡單的Python程序,遍歷所有漫畫的url,對請求所返回的html源...
    msq3閱讀 12,843評論 14 88
  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,175評論 4 61
  • 到三姨跟瘋了一樣要跳崖,有幾十層樓那么高,坐電梯上去的,下面是大海,結果跳下來沒事,只是扭了筋骨。三姨家養的貓跑來...
    一與_閱讀 220評論 0 0
  • 不愿起床的我 奮然起身 拉開窗簾 就這樣沐浴在光明
    蔥頭越閱讀 249評論 0 1