為避免尬聊,我用Python爬取了一千多張斗圖!
前幾天和女神聊天的時候實在是太尬了,因為沒有足夠的斗圖表情包,整個聊天的氣氛都帶動不起來,所以抑郁不得志!
為了追到心目中的完美女神,我爬了一千多張斗圖表情包,只為下一次聊天的時候,主動權都在我的手上。
考慮到有些小伙伴可能在python基礎不是很好,因此,啃書君決定先幫各位補補基礎知識,大佬可以直接看實戰(zhàn)內容。本次實戰(zhàn)內容是爬?。憾穲D吧。
如果你不想看前面這些基礎知識,可以直接拉到文章尾部的實戰(zhàn)篇。
面向對象
python從設計開始就是一門面向對象的的語言,因此使用python創(chuàng)建一個類與對象是非常簡單的一件事情。
如果你以前沒有接觸過面向對象的編程語言,那么你需要了解一些面向對象語言的一些基本特征,接下來就來感受python的面向對象語言。
面向對象簡介
類(Class):用來描述具有相同的屬性和方法的對象的集合。它定義了該集合中每個對象所共有的屬性和方法。對象是類的實例。 類變量:類變量在整個實例化的對象中是公共的。類變量定義在類中,且在函數體外。 數據成員:類變量或實例變量,用于處理類及其實例對象的相關數據。 方法重載:如果從父類繼承的方法,無法滿足子類的需求,可以對其進行改寫,這個過程叫做覆蓋,也稱為方法的重載。 實例變量:定義在方法中的變量,只作用于當前實例的類。 繼承:即一個派生類,繼承基類(父類)的字段與方法。 實例化:創(chuàng)建一個實例,類的具體對象。 方法:類中定義的函數 對象:通過類定義的數據結構實例。對象包括兩個數據成員(類變量和實例變量)和方法。
創(chuàng)建類與對象
類相當于一個模板,模板里面可以有多個函數,函數用于實現功能。
對象其實是根據模板創(chuàng)建的一個實例,通過創(chuàng)建的實例可以執(zhí)行類中的函數。
# 創(chuàng)建類
class Foo(object):
# 創(chuàng)建類中的函數
def bar(self):
# todo
pass
# 根據Foo類創(chuàng)建對象obj
obj = Foo()
class是關鍵字,代表類 object代碼父類,所有類都繼承object類 創(chuàng)建對象,類名稱后加上括號即可
面向對象的三大特性
封裝
封裝,顧名思義就是將內容封裝到某個地方,以后再去調用被封裝在某處的內容。
所以,在使用面向對象的封裝特性時,需要:
將內容封裝到某處 從某處調用被封裝的內容
class Foo(object):
# 構造方法,根據類創(chuàng)建對象時自動執(zhí)行
def __init__(self, name, age):
self.name = name
self.age = age
# 根據類Foo創(chuàng)建對象
# 自動啟動Foo類的__init__方法
obj1 = Foo('Jack', 18)
obj2 = Fo('Rose', 20)
obj1 = Foo('Jack', 18)將Jack和18分別封裝到obj1(self)的name和age屬性中,obj2也是同樣的道理。
self是一個形式參數,當執(zhí)行obj1 = Foo('Jack', 18),self等于obj1,因此,每個對象都有name和age屬性。
通過對象調用封裝內容
class Foo(object):
def __init__(self, name, age):
self.name = name
self.age = age
obj1 = Foo('Jack', 18)
print(obj1.name) # 調用obj1的name屬性
print(obj1.age) # 調用obj1的age屬性
obj2 = Foo('Jack', 18)
print(obj2.name) # 調用obj2的name屬性
print(obj2.age) # 調用obj2的age屬性
通過self間接調用封裝的內容
class Foo(object):
def __init__(self, name, age):
self.name = name
self.age = age
def detail(self):
print(self.name)
print(self.age)
obj1 = Foo('Jack', 18)
obj1.detail()
obj2 = Foo('Rose', 20)
obj2.detail()
obj1.detail()python默認會將obj1傳給self參數,即:obj1.detail(obj1),所以,此時方法內部的self=obj1,即self.name相當于obj1.name。
感受面向對象的簡便
對于面向對象封裝來說,其實就是使用構造方法將內容封裝到對象中,然后通過對象直接或者self間接獲取封裝內容。
接下來,我們就來體驗一下面向對象的簡便性。
def kanchai(name, age, gender):
print "%s,%s歲,%s,上山去砍柴" %(name, age, gender)
def qudongbei(name, age, gender):
print "%s,%s歲,%s,開車去東北" %(name, age, gender)
def dabaojian(name, age, gender):
print "%s,%s歲,%s,最愛大保健" %(name, age, gender)
kanchai('小明', 10, '男')
qudongbei('小明', 10, '男')
dabaojian('小明', 10, '男')
kanchai('老李', 90, '男')
qudongbei('老李', 90, '男')
dabaojian('老李', 90, '男')
函數式編程
class Foo(object):
def __init__(self, name, age ,gender):
self.name = name
self.age = age
self.gender = gender
def kanchai(self):
print "%s,%s歲,%s,上山去砍柴" %(self.name, self.age, self.gender)
def qudongbei(self):
print "%s,%s歲,%s,開車去東北" %(self.name, self.age, self.gender)
def dabaojian(self):
print "%s,%s歲,%s,最愛大保健" %(self.name, self.age, self.gender)
xiaoming = Foo('小明', 10, '男')
xiaoming.kanchai()
xiaoming.qudongbei()
xiaoming.dabaojian()
laoli = Foo('老李', 90, '男')
laoli.kanchai()
laoli.qudongbei()
laoli.dabaojian()
如果使用函數式編程,需要在每一個執(zhí)行函數的時候都要傳入相同的參數,如果參數多的話,就每一次都要復制粘貼,非常不方便;而對于面向對象來說,只需要在創(chuàng)建對象時,將所需要的參數封裝到對象中,然后通過對象調用即可獲取封裝的內容。
繼承
繼承就是讓類和類之間存在父子關系,子類可以直接訪問父類的靜態(tài)屬性與方法。在python中,新建的類可以繼承一個或多個父類,父類可以稱為基類或超類,新建的類稱為派生類或子類。
class ParentClass1: #定義父類1
pass
class ParentClass2: #定義父類2
pass
class SubClass1(ParentClass1):
# 單繼承,基類是ParentClass1,派生類是SubClass
pass
class SubClass2(ParentClass1,ParentClass2):
# python支持多繼承,用逗號分隔開多個繼承的類
pass
print(SubClass1.__bases__) # 查看所有繼承的父類
print(SubClass2.__bases__)
# ===============
# (<class '__main__.Father1'>,)
# (<class '__main__.Father1'>, <class '__main__.Father2'>)
繼承的規(guī)則
1、子類繼承父類的成員變量與方法
2、子類不繼承父類的構造方法
3、子類不能刪除父類成員,但是可以重新定義父類成員
4、子類可以增加自己的成員。
具體代碼,如下所示:
class Person(object):
def __init__(self, name, age, sex):
self.name = 'jasn'
self.age = 18
self.sex = sex
def talk(self):
print('I want to say someting to you')
class Chinese(Person):
def __init__(self, name, age, sex, language):
Person.__init__(self, name, age, sex) # 用父類的name, age, sex覆蓋掉子類的屬性
self.age = age # 覆蓋掉父類的age屬性,取值為子類實例傳入的age參數
self.language = 'Chinese'
def talk(self):
print('我說的是普通話')
Person.talk(self)
obj = Chinese('nancy', 30, 'male', '普通話')
print(obj.name)
print(obj.age)
print(obj.language)
obj.talk()
運行結果,如下:
jasn
30
Chinese
我說的是普通話
I want to say someting to you
因為,Chinese類覆蓋了Person類,在開始的時候,我們將父類的屬性覆蓋了子類的屬性,比如說name屬性,子類沒有去覆蓋父類,因此,即使子類傳來了name屬性值,但依舊還是輸出父類的name屬性。
繼承的作用
1、實現代碼(功能)重用,降低代碼冗余
2、增強軟件的可擴充性
3、提高軟件的維護性
繼承與抽象的概念
面向對象的兩個重要概念:抽象與分類。
class animal(): # 定義父類
country = 'china' # 這個叫類的變量
def __init__(self,name,age):
self.name = name # 這些又叫數據屬性
self.age = age
def walk(self): # 類的函數,方法,動態(tài)屬性
print('%s is walking'%self.name)
def say(self):
pass
class people(animal): # 子類繼承父類
pass
class pig(animal): # 子類繼承父類
pass
class dog(animal): # 子類繼承父類
pass
aobama=people('aobama',60) # 實例化一個對象
print(aobama.name)
aobama.walk()
上面的代碼可以這樣理解:我們將人、狗、豬抽象為動物,人、狗、豬都繼承動物類。
python中super()的作用和原理
super()在類的繼承里面非常常用,它解決了子類調用父類方法的一些問題。下面我們來看一下,它優(yōu)化了什么問題。
class Foo(object):
def bar(self, message):
print(message)
obj1 = Foo()
obj1.bar('hello')
當存在繼承關系的時候,有時候需要在子類中調用父類方法,此時最簡單的方法就是把對象調用轉換成類調用,需要注意的是這時self參數需要顯示傳遞。
具體代碼,如下所示:
class FooParent(object):
"""docstring for FooParent"""
def bar(self, message):
print(message)
class FooChild(FooParent):
"""docstring for FooChild"""
def bar(self, message):
FooParent.bar(self, message)
foochild = FooChild()
foochild.bar('hello')
這樣的繼承方式其實是存在缺陷的,比如說,我修改了父類的名稱,那么子類中將要涉及多處修改。
因此python就引入了super()機制,具體代碼如下所示:
class FooParent(object):
def bar(self, message):
print(message)
class FooChild(FooParent):
def bar(self, message):
super(FooChild, self).bar(message)
obj = FooChild()
obj.bar('hello')
多態(tài)
關于python多態(tài)的知識,因本次實戰(zhàn)內容中并沒有使用到,因此我就不再敘述了,小伙伴們可以自行查找資料去了解。
什么是生產者與消費者模式
比如有兩個進程A與B,它們共享一個固定大小的緩沖區(qū),A進程生產數據放入緩沖區(qū);B進程從緩沖區(qū)取出數據進行計算,那么這里的A進程就相當于生產者,B進程相當于消費者。
為什么要使用生產者與消費者模式
在進程的世界里,生產者就是生產數據的進程,消費者就是使用(處理)數據的進程。同樣的道理,如果消費者的處理能力大于生產者,那么消費者就必須等待生產者。同樣的道理,如果生產者的處理能力大于消費者能力,那么生產者就必須等待消費者。
實現了生產者與消費者的解耦和,平衡了生產力與消費力,因為二者不能直接溝通,而是通過隊列進行溝通。
生產者消費者模式
生產者消費者模式是通過一個容器來解決生產者和消費者的強耦合問題。
生產者與消費者不直接通信,而是通過阻塞隊列進行通信,因此,生產者生產完數據之后不用等待消費者處理,直接扔給阻塞隊列,消費者不找生產者要數據,而是去阻塞隊列中找數據。阻塞隊列就類似于緩沖區(qū),平衡了生產者與消費者的能力。
multiprocess-Queue實現
具體代碼,如下所示:
from multiprocessing import Process, Queue
import time, random
from threading import Thread
import queue
# 生產者
def producer(name, food, q):
for i in range(4):
time.sleep(random.randint(1, 3)) # 模擬產生數據的時間
f = '%s 生產了 %s %s個' % (name, food, i + 1)
print(f)
q.put(f)
# 消費者
def consumer(name, q):
while True:
food = q.get()
if food is None:
print('%s 獲取到一個空' % name)
break
f = '%s 消費了 %s' % (name, food)
print(f)
time.sleep(random.randint(1, 3))
if __name__ == '__main__':
q = Queue() # 創(chuàng)建隊列
# 模擬生產者,產生數據
p1 = Process(target=producer, args=('p1', '包子', q))
p1.start()
p2 = Process(target=producer, args=('p2', '燒餅', q))
p2.start()
c1 = Process(target=consumer, args=('c1', q))
c1.start()
c2 = Process(target=consumer, args=('c2', q))
c2.start()
p1.join()
p2.join()
q.put(None)
q.put(None)
Thread-Queue實現
上面的代碼是由多進程實現的,接下來就考慮一下多線程實現該功能。
具體代碼,如下所示:
import random
import time
from threading import Thread
import queue
def producer(name, count, q):
for i in range(count):
food = f'{name} 生產第{i}個包子'
print(food)
q.put(food)
def consumer(name, q):
while True:
time.sleep(random.randint(1, 3))
if q.empty():
break
print(f'{name} 消費了 {q.get()}')
if __name__ == '__main__':
q = queue.Queue()
print(q.empty())
for i in range(1, 4):
p = Thread(target=producer, args=(f'生產者{i}', 10, q))
p.start()
for i in range(1, 6):
c = Thread(target=consumer, args=(f'消費者{i}', q))
c.start()
生產者消費者模式特點
保證生產者不會在緩沖區(qū)滿的時候繼續(xù)向緩沖區(qū)放入數據,而消費者也不會在緩沖區(qū)空的時候,消耗數據。
當緩沖區(qū)滿的時候,生產者會進入休眠狀態(tài),當下次消費者開始消耗緩沖區(qū)數據時,生產者才會被喚醒,開始往緩沖區(qū)添加數據;當緩沖區(qū)空的時候,消費者會進入休眠狀態(tài),直到生產者往緩沖區(qū)添加數據時才會被喚醒。
基礎知識總結
到這里,我基本上就將本次實戰(zhàn)需要用到的基礎知識都教給大家了,相當于拋磚引玉。拋出我這塊磚,引出小伙伴們的玉。本次的基礎知識主要分為兩大模塊,第一個是面向對象的知識,第二個則是線程相關的知識,小伙伴們需要盡可能去熟悉,才能寫出更加高效健壯的爬蟲demo。
實戰(zhàn)篇
工具庫使用
本次爬蟲所需要的工具庫我先列舉出來
import requests
from lxml import etree
import threading
from queue import Queue
import re
缺少哪些就自行安裝。
抓取目標
本次實戰(zhàn)所要抓取的網站是斗圖吧。網址如下:
https://www.doutub.com/
我們需要抓取的內容是該網站下的斗圖表情包。

瞬間讓你成為斗圖高手。啥也別說了,干就完事。
網頁分析

定睛一看,好家伙,居然有26頁的表情包,這不起飛?
首先來分析一下不同頁面url的地址變化。
# 第一頁
https://www.doutub.com/img_lists/new/1
# 第二頁
https://www.doutub.com/img_lists/new/2
# 第三頁
https://www.doutub.com/img_lists/new/3
看到這種變化的方式之后難道你不先竊喜一下。
頁面url地址已經搞定,那接下來要弄清楚的就是每一張表情包的url地址了。

這不是很容易就被聰明的你發(fā)現了嗎?這些鏈接我們采用xpath將其提取出來即可。
生產者的實現
首先,我們先創(chuàng)建兩個隊列,一個用于存儲每一頁的url地址,另一個便用于存儲圖片鏈接。
具體代碼,如下所示:
# 建立隊列
page_queue = Queue() # 頁面url
img_queue = Queue() # 圖片url
for page in range(1, 27):
url = f'https://www.doutub.com/img_lists/new/{page}'
page_queue.put(url)
通過上面的代碼,便將每一頁的url地址放入了page_queue。
接下來再通過創(chuàng)建一個類,將圖片url放入img_queue中。
具體代碼如下所示:
class ImageParse(threading.Thread):
def __init__(self, page_queue, img_queue):
super(ImageParse, self).__init__()
self.page_queue = page_queue
self.img_queue = img_queue
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36'
}
def run(self):
while True:
if self.page_queue.empty():
break
url = self.page_queue.get()
self.parse_img(url)
def parse_img(self, url):
response = requests.get(url, headers=self.headers).content.decode('utf-8')
html = etree.HTML(response)
img_lists = html.xpath('//div[@class="expression-list clearfix"]')
for img_list in img_lists:
img_urls = img_list.xpath('./div/a/img/@src')
img_names = img_list.xpath('./div/a/span/text()')
for img_url, img_name in zip(img_urls, img_names):
self.img_queue.put((img_url, img_name))
消費者的實現
其實消費者很簡單,我們只需要不斷的從img_page中獲取到圖片的url鏈接并不停的進行訪問即可。直到兩個隊列中有一個隊列為空即可退出。
class DownLoad(threading.Thread):
def __init__(self, page_queue, img_queue):
super(DownLoad, self).__init__()
self.page_queue = page_queue
self.img_queue = img_queue
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36'
}
def run(self):
while True:
if self.page_queue.empty() and self.img_queue.empty():
break
img_url, filename = self.img_queue.get()
fix = img_url.split('.')[-1]
name = re.sub(r'[??.,。!!*\\/|]', '', filename)
# print(fix)
data = requests.get(img_url, headers=self.headers).content
print('正在下載' + filename)
with open('../image/' + name + '.' + fix, 'wb') as f:
f.write(data)
最后,再讓創(chuàng)建好的兩個線程跑起來
for x in range(5):
t1 = ImageParse(page_queue, img_queue)
t1.start()
t2 = DownLoad(page_queue, img_queue)
t2.start()
t1.join()
t2.join()
最后結果

一共抓取了1269張圖片。
從今往后誰還能比得上你?就這?這不有爬蟲就行!
我們的文章到此就結束啦,如果你喜歡今天的Python 實戰(zhàn)教程,請持續(xù)關注菜鳥學Python。
左手Python,右手Java,升職就業(yè)不愁啦!
推薦閱讀:
入門: 最全的零基礎學Python的問題 | 零基礎學了8個月的Python | 實戰(zhàn)項目 |學Python就是這條捷徑
干貨:爬取豆瓣短評,電影《后來的我們》 | 38年NBA最佳球員分析 | 從萬眾期待到口碑撲街!唐探3令人失望 | 笑看新倚天屠龍記 | 燈謎答題王 |用Python做個海量小姐姐素描圖 |碟中諜這么火,我用機器學習做個迷你推薦系統(tǒng)電影
趣味:彈球游戲 | 九宮格 | 漂亮的花 | 兩百行Python《天天酷跑》游戲!
AI: 會做詩的機器人 | 給圖片上色 | 預測收入 | 碟中諜這么火,我用機器學習做個迷你推薦系統(tǒng)電影
小工具: Pdf轉Word,輕松搞定表格和水印! | 一鍵把html網頁保存為pdf!| 再見PDF提取收費! | 用90行代碼打造最強PDF轉換器,word、PPT、excel、markdown、html一鍵轉換 | 制作一款釘釘低價機票提示器! |60行代碼做了一個語音壁紙切換器天天看小姐姐!|
年度爆款文案
點閱讀原文,領AI全套資料


