千萬不要被所謂「元類是99%的python程序員不會用到的特性」這類的說辭嚇住。由於每一箇中國人,都是天生的元類使用者html
學懂元類,你只須要知道兩句話:python
在python世界,擁有一個永恆的道,那就是「type」,請記在腦海中,type就是道。如此廣袤無垠的python生態圈,都是由type產生出來的。git
道生一,一輩子二,二生三,三生萬物。程序員
1
2
3
4
5
6
7
8
9
10
11
|
# 建立一個Hello類,擁有屬性say_hello ----二的起源
class Hello():
def say_hello(self, name='world'):
print('Hello, %s.' % name)
# 從Hello類建立一個實例hello ----二生三
hello = Hello()
# 使用hello調用方法say_hello ----三生萬物
hello.say_hello()
|
輸出效果:github
1
|
Hello, world.
|
這就是一個標準的「二生三,三生萬物」過程。 從類到咱們能夠調用的方法,用了這兩步。web
那咱們不禁自主要問,類從何而來呢?回到代碼的第一行。
class Hello實際上是一個函數的「語義化簡稱」,只爲了讓代碼更淺顯易懂,它的另外一個寫法是:sql
1
2
3
4
|
def fn(self, name='world'): # 假如咱們有一個函數叫fn
print('Hello, %s.' % name)
Hello = type('Hello', (object,), dict(say_hello=fn)) # 經過type建立Hello class ---- 神祕的「道」,能夠點化一切,此次咱們直接從「道」生出了「二」
|
這樣的寫法,就和以前的Class Hello寫法做用徹底相同,你能夠試試建立實例並調用數據庫
1
2
3
4
5
|
# 從Hello類建立一個實例hello ----二生三,徹底同樣
hello = Hello()
# 使用hello調用方法say_hello ----三生萬物,徹底同樣
hello.say_hello()
|
輸出效果:編程
1
|
Hello, world. ----調用結果徹底同樣。
|
咱們回頭看一眼最精彩的地方,道直接生出了二:數組
Hello = type(‘Hello’, (object,), dict(say_hello=fn))
這就是「道」,python世界的起源,你能夠爲此而驚歎。
注意它的三個參數!暗合人類的三大永恆命題:我是誰,我從哪裏來,我要到哪裏去。
值得注意的是,三大永恆命題,是一切類,一切實例,甚至一切實例屬性與方法都具備的。理所應當,它們的「創造者」,道和一,即type和元類,也具備這三個參數。但日常,類的三大永恆命題並不做爲參數傳入,而是以以下方式傳入
1
2
3
4
5
6
7
8
|
class Hello(object){
# class 後聲明「我是誰」
# 小括號內聲明「我來自哪裏」
# 中括號內聲明「我要到哪裏去」
def say_hello(){
}
}
|
通常來講,元類均被命名後綴爲Metalass。想象一下,咱們須要一個能夠自動打招呼的元類,它裏面的類方法呢,有時須要say_Hello,有時須要say_Hi,有時又須要say_Sayolala,有時須要say_Nihao。
若是每一個內置的say_xxx都須要在類裏面聲明一次,那將是多麼可怕的苦役! 不如使用元類來解決問題。
如下是建立一個專門「打招呼」用的元類代碼:
1
2
3
4
5
|
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)
|
記住兩點:
一、元類是由「type」衍生而出,因此父類須要傳入type。【道生一,因此一必須包含道】
二、元類的操做都在 __new__中完成,它的第一個參數是將建立的類,以後的參數便是三大永恆命題:我是誰,我從哪裏來,我將到哪裏去。 它返回的對象也是三大永恆命題,接下來,這三個參數將一直陪伴咱們。
在__new__中,我只進行了一個操做,就是
1
|
attrs['say_'+name] = lambda self,value,saying=name: print(saying+','+value+'!')
|
它跟據類的名字,建立了一個類方法。好比咱們由元類建立的類叫「Hello」,那建立時就自動有了一個叫「say_Hello」的類方法,而後又將類的名字「Hello」做爲默認參數saying,傳到了方法裏面。而後把hello方法調用時的傳參做爲value傳進去,最終打印出來。
那麼,一個元類是怎麼從建立到調用的呢?
來!一塊兒根據道生1、一輩子2、二生3、三生萬物的準則,走進元類的生命週期吧!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
# 道生一:傳入type
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)
# 一輩子二:建立類
class Hello(object, metaclass=SayMetaClass):
pass
# 二生三:建立實列
hello = Hello()
# 三生萬物:調用實例方法
hello.say_Hello('world!')
|
輸出爲
1
|
Hello, world!
|
注意:經過元類建立的類,第一個參數是父類,第二個參數是metaclass
普通人出生都不會說話,但有的人出生就會打招呼說「Hello」,「你好」,「sayolala」,這就是天賦的力量。它會給咱們面向對象的編程省下無數的麻煩。
如今,保持元類不變,咱們還能夠繼續建立Sayolala, Nihao類,以下:
1
2
3
4
5
6
7
8
9
|
# 一輩子二:建立類
class Sayolala(object, metaclass=SayMetaClass):
pass
# 二生三:建立實列
s = Sayolala()
# 三生萬物:調用實例方法
s.say_Sayolala('japan!')
|
輸出
1
|
Sayolala, japan!
|
也能夠說中文
1
2
3
4
5
6
7
8
9
|
# 一輩子二:建立類
class Nihao(object, metaclass=SayMetaClass):
pass
# 二生三:建立實列
n = Nihao()
# 三生萬物:調用實例方法
n.say_Nihao('中華!')
|
輸出
1
|
Nihao, 中華!
|
再來一個小例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
# 道生一
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)
|
如今咱們打印一下L
1
2
3
|
print(L)
>>> [1]
|
而普通的list沒有add()方法
1
2
3
4
|
L2 = list()
L2.add(1)
>>>AttributeError: 'list' object has no attribute 'add'
|
太棒了!學到這裏,你是否是已經體驗到了造物主的樂趣?
python世界的一切,盡在掌握。
年輕的造物主,請隨我一塊兒開創新世界。
咱們選擇兩個領域,一個是Django的核心思想,「Object Relational Mapping」,即對象-關係映射,簡稱ORM。
這是Django的一大難點,但學完了元類,一切變得清晰。你對Django的理解將更上一層樓!
另外一個領域是爬蟲領域(黑客領域),一個自動搜索網絡上的可用代理,而後換着IP去突破別的人反爬蟲限制。
這兩項技能很是有用,也很是好玩!
1
2
3
4
5
6
7
8
|
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類實例化時將獲得兩個參數,name和column_type,它們將被綁定爲Field的私有屬性,若是要將Field轉化爲字符串時,將返回「Field:XXX」 , XXX是傳入的name名稱。
1
2
3
4
5
6
7
8
9
|
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實例初始化時,時自動調用父類的初始化方式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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 # 保存屬性和列的映射關係
attrs['__table__'] = name # 假設表名和類名一致
return type.__new__(cls, name, bases, attrs)
|
它作了如下幾件事
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
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))
|
1
2
3
4
5
6
|
class User(Model):
# 定義類的屬性到列的映射:
id = IntegerField('id')
name = StringField('username')
email = StringField('email')
password = StringField('password')
|
這時
id= IntegerField(‘id’)就會自動解析爲:
Model.__setattr__(self, ‘id’, IntegerField(‘id’))
由於IntergerField(‘id’)是Field的子類的實例,自動觸發元類的__new__,因此將IntergerField(‘id’)存入__mappings__並刪除這個鍵值對。
當你初始化一個實例的時候並調用save()方法時候
1
2
|
u = User(id=12345, name='Batman', email='batman@nasa.org', password='iamback')
u.save()
|
這時先完成了二生三的過程:
接下來完成了三生萬物的過程:
經過u.save()模擬數據庫存入操做。這裏咱們僅僅作了一下遍歷__mappings__操做,虛擬了sql並打印,在現實狀況下是經過輸入sql語句與數據庫來運行。
輸出結果爲
1
2
3
4
5
6
7
|
Found model: User
Found mapping: name ==> <StringField:username>
Found mapping: password ==> <StringField:password>
Found mapping: id ==> <IntegerField:id>
Found mapping: email ==> <StringField:email>
SQL: insert into User (username,password,id,email) values (Batman,iamback,12345,batman@nasa.org)
ARGS: ['Batman', 'iamback', 12345, 'batman@nasa.org']
|
- 年輕的造物主,你已經和我一塊兒體驗了由「道」演化「萬物」的偉大曆程,這也是Django中的Model版塊核心原理。
- 接下來,請和我一塊兒進行更好玩的爬蟲實戰(嗯,你如今已是初級黑客了):網絡代理的爬取吧!
請確保已安裝requests和pyquery這兩個包。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
# 文件: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後面,試完刪除
1
2
3
|
if(__name__ == '__main__'):
rs = get_page('https://www.baidu.com')
print('result:\r\n', rs)
|
把這一段粘在get_page.py後面,試完刪除
1
2
3
4
5
6
7
8
9
10
11
12
|
if(__name__ == '__main__'):
from pyquery import PyQuery as pq
start_url = 'http://www.proxy360.cn/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)
|
接下來進入正題:使用元類批量抓取代理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
|
from getpage import get_page
from pyquery import PyQuery as pq
# 道生一:建立抽取代理的metaclass
class ProxyMetaclass(type):
"""
元類,在FreeProxyGetter類中加入
__CrawlFunc__和__CrawlFuncCount__
兩個參數,分別表示爬蟲函數,和爬蟲函數的數量。
"""
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)
# 一輩子二:建立代理獲取類
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.66ip.cn/{}.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.proxy360.cn/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.goubanjia.com/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__':
# 二生三:實例化ProxyGetter
crawler = ProxyGetter()
print(crawler.__CrawlName__)
# 三生萬物
for site_label in range(crawler.__CrawlFuncCount__):
site = crawler.__CrawlName__[site_label]
myProxies = crawler.get_raw_proxies(site)
|
怎麼樣?是否是和以前建立ORM的__mappings__過程極爲類似?
分別從三個免費代理網站抓取了頁面上顯示的所有代理。
若是對yield用法不熟悉,能夠查看:
廖雪峯的python教程:生成器
略
那麼。。。怎麼利用批量代理,衝擊別人的網站,套取別人的密碼,狂發廣告水貼,定時騷擾客戶? 呃!想啥呢!這些本身悟!若是悟不到,請聽下回分解!
請記住揮動工具的口訣: