千萬不要被所謂“元類是99%的python程序員不會用到的特性”這類的說辭嚇住。因?yàn)槊總€(gè)中國人,都是天生的元類使用者 學(xué)懂元類,你只需要知道兩句話: 道生一,一生二,二生三,三生萬物 我是誰?我從哪來里?我要到哪里去? 在python世界,擁有一個(gè)永恒的道,那就是“type”,請記在腦海中,type就是道。如此廣袤無垠的python生態(tài)圈,都是由type產(chǎn)生出來的。 道生一,一生二,二生三,三生萬物。 道 即是 type 一 即是 metaclass(元類,或者叫類生成器) 二 即是 class(類,或者叫實(shí)例生成器) 三 即是 instance(實(shí)例) 萬物 即是 實(shí)例的各種屬性與方法,我們平常使用python時(shí),調(diào)用的就是它們。 道和一,是我們今天討論的命題,而二、三、和萬物,則是我們常常使用的類、實(shí)例、屬性和方法,用hello world來舉例: # 創(chuàng)建一個(gè)Hello類,擁有屬性say_hello ----二的起源 class Hello(): def say_hello(self, name='world'): print('Hello, %s.' % name) # 從Hello類創(chuàng)建一個(gè)實(shí)例hello ----二生三 hello = Hello() # 使用hello調(diào)用方法say_hello ----三生萬物 hello.say_hello() 輸出效果: Hello, world. 這就是一個(gè)標(biāo)準(zhǔn)的“二生三,三生萬物”過程。 從類到我們可以調(diào)用的方法,用了這兩步。 那我們不由自主要問,類從何而來呢?回到代碼的第一行。 class Hello其實(shí)是一個(gè)函數(shù)的“語義化簡稱”,只為了讓代碼更淺顯易懂,它的另一個(gè)寫法是: def fn(self, name='world'): # 假如我們有一個(gè)函數(shù)叫fn print('Hello, %s.' % name) Hello = type('Hello', (object,), dict(say_hello=fn)) # 通過type創(chuàng)建Hello class ---- 神秘的“道”,可以點(diǎn)化一切,這次我們直接從“道”生出了“二” 這樣的寫法,就和之前的Class Hello寫法作用完全相同,你可以試試創(chuàng)建實(shí)例并調(diào)用 # 從Hello類創(chuàng)建一個(gè)實(shí)例hello ----二生三,完全一樣 hello = Hello() # 使用hello調(diào)用方法say_hello ----三生萬物,完全一樣 hello.say_hello() 輸出效果: Hello, world. ----調(diào)用結(jié)果完全一樣。 我們回頭看一眼最精彩的地方,道直接生出了二: Hello = type('Hello’, (object,), dict(say_hello=fn)) 這就是“道”,python世界的起源,你可以為此而驚嘆。 注意它的三個(gè)參數(shù)!暗合人類的三大永恒命題:我是誰,我從哪里來,我要到哪里去。 第一個(gè)參數(shù):我是誰。 在這里,我需要一個(gè)區(qū)分于其它一切的命名,以上的實(shí)例將我命名為“Hello” 第二個(gè)參數(shù):我從哪里來。在這里,我需要知道從哪里來,也就是我的“父類”,以上實(shí)例中我的父類是“object”——python中一種非常初級的類。 第三個(gè)參數(shù):我要到哪里去。在這里,我們將需要調(diào)用的方法和屬性包含到一個(gè)字典里,再作為參數(shù)傳入。以上實(shí)例中,我們有一個(gè)say_hello方法包裝進(jìn)了字典中。 值得注意的是,三大永恒命題,是一切類,一切實(shí)例,甚至一切實(shí)例屬性與方法都具有的。理所應(yīng)當(dāng),它們的“創(chuàng)造者”,道和一,即type和元類,也具有這三個(gè)參數(shù)。但平常,類的三大永恒命題并不作為參數(shù)傳入,而是以如下方式傳入 class Hello(object){ # class 后聲明“我是誰” # 小括號內(nèi)聲明“我來自哪里” # 中括號內(nèi)聲明“我要到哪里去” def say_hello(){ } } 造物主,可以直接創(chuàng)造單個(gè)的人,但這是一件苦役。造物主會先創(chuàng)造“人”這一物種,再批量創(chuàng)造具體的個(gè)人。并將三大永恒命題,一直傳遞下去。 “道”可以直接生出“二”,但它會先生出“一”,再批量地制造“二”。 type可以直接生成類(class),但也可以先生成元類(metaclass),再使用元類批量定制類(class)。 元類——道生一,一生二 一般來說,元類均被命名后綴為Metalass。想象一下,我們需要一個(gè)可以自動(dòng)打招呼的元類,它里面的類方法呢,有時(shí)需要say_Hello,有時(shí)需要say_Hi,有時(shí)又需要say_Sayolala,有時(shí)需要say_Nihao。 如果每個(gè)內(nèi)置的say_xxx都需要在類里面聲明一次,那將是多么可怕的苦役! 不如使用元類來解決問題。 以下是創(chuàng)建一個(gè)專門“打招呼”用的元類代碼: class SayMetaClass(type): def __new__(cls, name, bases, attrs): attrs['say_' name] = lambda self,value,saying=name: print(saying ',' value '!') return type.__new__(cls, name, bases, attrs) 記住兩點(diǎn): 元類是由“type”衍生而出,所以父類需要傳入type?!镜郎唬砸槐仨毎馈?div> 元類的操作都在 __new__中完成,它的第一個(gè)參數(shù)是將創(chuàng)建的類,之后的參數(shù)即是三大永恒命題:我是誰,我從哪里來,我將到哪里去。 它返回的對象也是三大永恒命題,接下來,這三個(gè)參數(shù)將一直陪伴我們。 在__new__中,我只進(jìn)行了一個(gè)操作,就是 attrs['say_' name] = lambda self,value,saying=name: print(saying ',' value '!') 它跟據(jù)類的名字,創(chuàng)建了一個(gè)類方法。比如我們由元類創(chuàng)建的類叫“Hello”,那創(chuàng)建時(shí)就自動(dòng)有了一個(gè)叫“say_Hello”的類方法,然后又將類的名字“Hello”作為默認(rèn)參數(shù)saying,傳到了方法里面。然后把hello方法調(diào)用時(shí)的傳參作為value傳進(jìn)去,最終打印出來。 那么,一個(gè)元類是怎么從創(chuàng)建到調(diào)用的呢? 來!一起根據(jù)道生一、一生二、二生三、三生萬物的準(zhǔn)則,走進(jìn)元類的生命周期吧! # 道生一:傳入type class SayMetaClass(type): # 傳入三大永恒命題:類名稱、父類、屬性 def __new__(cls, name, bases, attrs): # 創(chuàng)造“天賦” attrs['say_' name] = lambda self,value,saying=name: print(saying ',' value '!') # 傳承三大永恒命題:類名稱、父類、屬性 return type.__new__(cls, name, bases, attrs) # 一生二:創(chuàng)建類 class Hello(object, metaclass=SayMetaClass): pass # 二生三:創(chuàng)建實(shí)列 hello = Hello() # 三生萬物:調(diào)用實(shí)例方法 hello.say_Hello('world!') 輸出為 Hello, world! 注意:通過元類創(chuàng)建的類,第一個(gè)參數(shù)是父類,第二個(gè)參數(shù)是metaclass 普通人出生都不會說話,但有的人出生就會打招呼說“Hello”,“你好”,“sayolala”,這就是天賦的力量。它會給我們面向?qū)ο蟮木幊淌∠聼o數(shù)的麻煩。 現(xiàn)在,保持元類不變,我們還可以繼續(xù)創(chuàng)建Sayolala, Nihao類,如下: # 一生二:創(chuàng)建類 class Sayolala(object, metaclass=SayMetaClass): pass # 二生三:創(chuàng)建實(shí)列 s = Sayolala() # 三生萬物:調(diào)用實(shí)例方法 s.say_Sayolala('japan!') 輸出 Sayolala, japan! 也可以說中文 # 一生二:創(chuàng)建類 class Nihao(object, metaclass=SayMetaClass): pass # 二生三:創(chuàng)建實(shí)列 n = Nihao() # 三生萬物:調(diào)用實(shí)例方法 n.say_Nihao('中華!') 輸出 Nihao, 中華! 再來一個(gè)小例子: # 道生一 class ListMetaclass(type): def __new__(cls, name, bases, attrs): # 天賦:通過add方法將值綁定 attrs['add'] = lambda self, value: self.append(value) return type.__new__(cls, name, bases, attrs) # 一生二 class MyList(list, metaclass=ListMetaclass): pass # 二生三 L = MyList() # 三生萬物 L.add(1) 現(xiàn)在我們打印一下L print(L) >>> [1] 而普通的list沒有add()方法 L2 = list() L2.add(1) >>>AttributeError: 'list' object has no attribute 'add' 太棒了!學(xué)到這里,你是不是已經(jīng)體驗(yàn)到了造物主的樂趣? python世界的一切,盡在掌握。 年輕的造物主,請隨我一起開創(chuàng)新世界。 我們選擇兩個(gè)領(lǐng)域,一個(gè)是Django的核心思想,“Object Relational Mapping”,即對象-關(guān)系映射,簡稱ORM。 這是Django的一大難點(diǎn),但學(xué)完了元類,一切變得清晰。你對Django的理解將更上一層樓! 另一個(gè)領(lǐng)域是爬蟲領(lǐng)域(黑客領(lǐng)域),一個(gè)自動(dòng)搜索網(wǎng)絡(luò)上的可用代理,然后換著IP去突破別的人反爬蟲限制。 這兩項(xiàng)技能非常有用,也非常好玩! 挑戰(zhàn)一:通過元類創(chuàng)建ORM 準(zhǔn)備工作,創(chuàng)建一個(gè)Field類 class Field(object): def __init__(self, name, column_type): self.name = name self.column_type = column_type def __str__(self): return '<%s:%s>' % (self.__class__.__name__, self.name) 它的作用是 在Field類實(shí)例化時(shí)將得到兩個(gè)參數(shù),name和column_type,它們將被綁定為Field的私有屬性,如果要將Field轉(zhuǎn)化為字符串時(shí),將返回“Field:XXX” , XXX是傳入的name名稱。 準(zhǔn)備工作:創(chuàng)建StringField和IntergerField class StringField(Field): def __init__(self, name): super(StringField, self).__init__(name, 'varchar(100)') class IntegerField(Field): def __init__(self, name): super(IntegerField, self).__init__(name, 'bigint') 它的作用是 在StringField,IntegerField實(shí)例初始化時(shí),時(shí)自動(dòng)調(diào)用父類的初始化方式。 道生一 class ModelMetaclass(type): def __new__(cls, name, bases, attrs): if name=='Model': return type.__new__(cls, name, bases, attrs) print('Found model: %s' % name) mappings = dict() for k, v in attrs.items(): if isinstance(v, Field): print('Found mapping: %s ==> %s' % (k, v)) mappings[k] = v for k in mappings.keys(): attrs.pop(k) attrs['__mappings__'] = mappings # 保存屬性和列的映射關(guān)系 attrs['__table__'] = name # 假設(shè)表名和類名一致 return type.__new__(cls, name, bases, attrs) 它做了以下幾件事 創(chuàng)建一個(gè)新的字典mapping 將每一個(gè)類的屬性,通過.items()遍歷其鍵值對。如果值是Field類,則打印鍵值,并將這一對鍵值綁定到mapping字典上。 將剛剛傳入值為Field類的屬性刪除。 創(chuàng)建一個(gè)專門的__mappings__屬性,保存字典mapping。 創(chuàng)建一個(gè)專門的__table__屬性,保存?zhèn)魅氲念惖拿Q。 一生二 class Model(dict, metaclass=ModelMetaclass): def __init__(self, **kwarg): super(Model, self).__init__(**kwarg) def __getattr__(self, key): try: return self[key] except KeyError: raise AttributeError(''Model' object has no attribute '%s'' % key) def __setattr__(self, key, value): self[key] = value # 模擬建表操作 def save(self): fields = [] args = [] for k, v in self.__mappings__.items(): fields.append(v.name) args.append(getattr(self, k, None)) sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join([str(i) for i in args])) print('SQL: %s' % sql) print('ARGS: %s' % str(args)) 如果從Model創(chuàng)建一個(gè)子類User: class User(Model): # 定義類的屬性到列的映射: id = IntegerField('id') name = StringField('username') email = StringField('email') password = StringField('password') 這時(shí) id= IntegerField('id’)就會自動(dòng)解析為: Model.__setattr__(self, 'id’, IntegerField('id’)) 因?yàn)镮ntergerField('id’)是Field的子類的實(shí)例,自動(dòng)觸發(fā)元類的__new__,所以將IntergerField('id’)存入__mappings__并刪除這個(gè)鍵值對。 二生三、三生萬物 當(dāng)你初始化一個(gè)實(shí)例的時(shí)候并調(diào)用save()方法時(shí)候 u = User(id=12345, name='Batman', email='batman@nasa.org', password='iamback') u.save() 這時(shí)先完成了二生三的過程: 先調(diào)用Model.__setattr__,將鍵值載入私有對象 然后調(diào)用元類的“天賦”,ModelMetaclass.__new__,將Model中的私有對象,只要是Field的實(shí)例,都自動(dòng)存入u.__mappings__。 接下來完成了三生萬物的過程: 通過u.save()模擬數(shù)據(jù)庫存入操作。這里我們僅僅做了一下遍歷__mappings__操作,虛擬了sql并打印,在現(xiàn)實(shí)情況下是通過輸入sql語句與數(shù)據(jù)庫來運(yùn)行。 輸出結(jié)果為 Found model: User Found mapping: name ==> Found mapping: password ==> Found mapping: id ==> Found mapping: email ==> SQL: insert into User (username,password,id,email) values (Batman,iamback,12345,batman@nasa.org) ARGS: ['Batman', 'iamback', 12345, 'batman@nasa.org'] 年輕的造物主,你已經(jīng)和我一起體驗(yàn)了由“道”演化“萬物”的偉大歷程,這也是Django中的Model版塊核心原理。 接下來,請和我一起進(jìn)行更好玩的爬蟲實(shí)戰(zhàn)(嗯,你現(xiàn)在已經(jīng)是初級黑客了):網(wǎng)絡(luò)代理的爬取吧! 挑戰(zhàn)二:網(wǎng)絡(luò)代理的爬取 準(zhǔn)備工作,先爬個(gè)頁面玩玩 請確保已安裝requests和pyquery這兩個(gè)包。 # 文件:get_page.py import requests base_headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36', 'Accept-Encoding': 'gzip, deflate, sdch', 'Accept-Language': 'zh-CN,zh;q=0.8' } def get_page(url): headers = dict(base_headers) print('Getting', url) try: r = requests.get(url, headers=headers) print('Getting result', url, r.status_code) if r.status_code == 200: return r.text except ConnectionError: print('Crawling Failed', url) return None 這里,我們利用request包,把百度的源碼爬了出來。 試一試抓百度 把這一段粘在get_page.py后面,試完刪除 if(__name__ == '__main__'): rs = get_page('https://www.baidu.com') print('result:', rs) 試一試抓代理 把這一段粘在get_page.py后面,試完刪除 if(__name__ == '__main__'): from pyquery import PyQuery as pq start_url = 'http://www./Region/China' print('Crawling', start_url) html = get_page(start_url) if html: doc = pq(html) lines = doc('div[name='list_proxy_ip']').items() for line in lines: ip = line.find('.tbBottomLine:nth-child(1)').text() port = line.find('.tbBottomLine:nth-child(2)').text() print(ip ':' port) 接下來進(jìn)入正題:使用元類批量抓取代理 批量處理抓取代理 from getpage import get_page from pyquery import PyQuery as pq # 道生一:創(chuàng)建抽取代理的metaclass class ProxyMetaclass(type): ''' 元類,在FreeProxyGetter類中加入 __CrawlFunc__和__CrawlFuncCount__ 兩個(gè)參數(shù),分別表示爬蟲函數(shù),和爬蟲函數(shù)的數(shù)量。 ''' def __new__(cls, name, bases, attrs): count = 0 attrs['__CrawlFunc__'] = [] attrs['__CrawlName__'] = [] for k, v in attrs.items(): if 'crawl_' in k: attrs['__CrawlName__'].append(k) attrs['__CrawlFunc__'].append(v) count = 1 for k in attrs['__CrawlName__']: attrs.pop(k) attrs['__CrawlFuncCount__'] = count return type.__new__(cls, name, bases, attrs) # 一生二:創(chuàng)建代理獲取類 class ProxyGetter(object, metaclass=ProxyMetaclass): def get_raw_proxies(self, site): proxies = [] print('Site', site) for func in self.__CrawlFunc__: if func.__name__==site: this_page_proxies = func(self) for proxy in this_page_proxies: print('Getting', proxy, 'from', site) proxies.append(proxy) return proxies def crawl_daili66(self, page_count=4): start_url = 'http://www./{}.html' urls = [start_url.format(page) for page in range(1, page_count 1)] for url in urls: print('Crawling', url) html = get_page(url) if html: doc = pq(html) trs = doc('.containerbox table tr:gt(0)').items() for tr in trs: ip = tr.find('td:nth-child(1)').text() port = tr.find('td:nth-child(2)').text() yield ':'.join([ip, port]) def crawl_proxy360(self): start_url = 'http://www./Region/China' print('Crawling', start_url) html = get_page(start_url) if html: doc = pq(html) lines = doc('div[name='list_proxy_ip']').items() for line in lines: ip = line.find('.tbBottomLine:nth-child(1)').text() port = line.find('.tbBottomLine:nth-child(2)').text() yield ':'.join([ip, port]) def crawl_goubanjia(self): start_url = 'http://www./free/gngn/index.shtml' html = get_page(start_url) if html: doc = pq(html) tds = doc('td.ip').items() for td in tds: td.find('p').remove() yield td.text().replace(' ', '') if __name__ == '__main__': # 二生三:實(shí)例化ProxyGetter crawler = ProxyGetter() print(crawler.__CrawlName__) # 三生萬物 for site_label in range(crawler.__CrawlFuncCount__): site = crawler.__CrawlName__[site_label] myProxies = crawler.get_raw_proxies(site) 道生一:元類的__new__中,做了四件事: 將“crawl_”開頭的類方法的名稱推入ProxyGetter.__CrawlName__ 將“crawl_”開頭的類方法的本身推入ProxyGetter.__CrawlFunc__ 計(jì)算符合“crawl_”開頭的類方法個(gè)數(shù) 刪除所有符合“crawl_”開頭的類方法 怎么樣?是不是和之前創(chuàng)建ORM的__mappings__過程極為相似? 一生二:類里面定義了使用pyquery抓取頁面元素的方法 分別從三個(gè)免費(fèi)代理網(wǎng)站抓取了頁面上顯示的全部代理。 如果對yield用法不熟悉,可以查看:廖雪峰的python教程:生成器 二生三:創(chuàng)建實(shí)例對象crawler 略 三生萬物:遍歷每一個(gè)__CrawlFunc__ 在ProxyGetter.__CrawlName__上面,獲取可以抓取的的網(wǎng)址名。 觸發(fā)類方法ProxyGetter.get_raw_proxies(site) 遍歷ProxyGetter.__CrawlFunc__,如果方法名和網(wǎng)址名稱相同的,則執(zhí)行這一個(gè)方法 把每個(gè)網(wǎng)址獲取到的代理整合成數(shù)組輸出。 那么。。。怎么利用批量代理,沖擊別人的網(wǎng)站,套取別人的密碼,狂發(fā)廣告水貼,定時(shí)騷擾客戶? 呃!想啥呢!這些自己悟!如果悟不到,請聽下回分解! 年輕的造物主,創(chuàng)造世界的工具已經(jīng)在你手上,請你將它的威力發(fā)揮到極致! 請記住揮動(dòng)工具的口訣: 道生一,一生二,二生三,三生萬物 我是誰,我來自哪里,我要到哪里去
|