當前位置:
首頁 > 知識 > 關於Python爬蟲種類、法律、輪子的一二三

關於Python爬蟲種類、法律、輪子的一二三


Linux編程

點擊右側關注,免費入門到精通!

作者丨linkin


https://segmentfault.com/a/1190000016998351

Welcome to the D-age

對於網路上的公開數據,理論上只要由服務端發送到前端都可以由爬蟲獲取到。但是Data-age時代的到來,數據是新的黃金,毫不誇張的說,數據是未來的一切。基於統計學數學模型的各種人工智慧的出現,離不開數據驅動。數據採集、清洗是最末端的技術成本,網路爬蟲也是基礎採集腳本。但是有幾個值得關注的是:


對於實時變化的網路環境,爬蟲的持續有效性如何保證
數據採集、清洗規則的適用範圍
數據採集的時間與質量

--效率


爬與反爬的恩怨
爬蟲的法律界限



法律的邊界,技術無罪



對於上面幾個關注點,我最先關注的便是爬蟲的法律界限 ,我曾經諮詢過一個律師:



Q: 老師,我如果用爬蟲爬取今日頭條這種類型網站的千萬級公開數據,算不算違法呢?
A: 爬取的公開數據不得進行非法使用或者商業利用



簡單的概括便是爬蟲爬取的數據如果進行商業出售或者有獲利的使用,便構成了「非法使用」。而一般的爬蟲程序並不違法,其實這是從法律專業的一方來解讀,如果加上技術層面的維度,那麼應該從這幾方面考慮:


爬取的數據量
爬取數據的類型(數據具有巨大的商業價值,未經對方許可,任何人不得非法獲取其數據並用於經營行為)
爬取的數據用途 (同行競爭?出售?經營?分析?實驗?...)
是否遵循網站的

robots

.txt

 即 機器人協議
爬取行為是否會對對方網站造成不能承受的損失(大量的爬取請求會把一個小型網站拖垮)


其實爬蟲構成犯罪的案例是開始增多的,相關新聞:



當爬蟲遇上法律會有什麼風險?


https://www.sohu.com/a/256579233_161795



程序員爬蟲竟構成犯罪?


https://baijiahao.baidu.com/s?

id=1609682215455337498&wfr=spider&for=pc



爬蟲相關法律知識


https://www.cnblogs.com/nick477931661/p/9139137.html

如果你的上級或公司要求你爬取某些網站的大量公開數據,你會怎麼辦呢?可以參考第2條新聞。法律矛盾點關鍵在於前面考慮的前三點,如果是個人隱私數據,是不能爬取的,如果是非公開數據,是不能爬取的,而對於其他大量的公開數據爬取,看人家查不查的到你,要不要起訴你。技術在你的手上,非法與否在於你怎麼去用。最好的爬取道德原則是:


減少並發請求
延長請求間隔
不進行公開出售數據
遵循網站 robots協議



當然,反爬最有效的便(目的均在於攔截爬蟲進入網站數據範圍)是:


要求用戶密碼+驗證碼
加密數據

js

混淆

css

混淆
針對

IP

請求頻率封鎖
針對

cookie

session

單個賬戶請求頻率封鎖單日請求次數
對關鍵數據進行拆分合併
對爬蟲投毒(返回假數據)
完善

robots

.txt


識別點擊九宮圖中沒有包含

xxx

的圖片等(終極驗證碼)
設置黑白名單、

IP

用戶組等



工欲善其事



針對網站的公開數據進行爬取,我們一般都要先對網站數據進行分析,定位,以確定其採集規則,如果網站設置了訪問許可權,那麼便不屬於我們的爬蟲採集範圍了:) 分析好採集規則,寫好了採集數據持久化(存入資料庫、導出為word、excel、csv、下載等)的相關代碼,整個爬蟲運行正常。那麼怎樣才能提高採集速度呢?


多進程採集
多線程採集
非同步協程採集
多進程 + 多線程採集
多進程 + 非同步協程採集
分散式採集



非同步爬蟲是同步爬蟲的升級版,在同步爬蟲中,無論你怎麼優化代碼,同步IO的阻塞是最大的致命傷。同步阻塞會讓採集任務一個個排著長隊領票等待執行。而非同步採集不會造成IO阻塞,充分利用了IO阻塞任務的等待時間去執行其他任務。


在IO 模型中,只有IO多路復用(I/O multiplexing){在內核處理IO請求結果為可讀或可寫時調用回調函數} 不阻塞 「內核拷貝IO請求數據到用戶空間」這個過程,實現非同步IO操作。



同步爬蟲


一般的同步爬蟲,我們可以寫一個,(以爬取圖片網站圖片為例) 


http://www.quanjing.com/creative/SearchCreative.aspx?id=7 ,我們來看看其下載該網址所有圖片所花費的時間:



以下代碼為後面多個常式的共同代碼:


#coding:utf-8


import

 time

from

 lxml 

import

 etree

import

 urllib.request 

as

 request

#目標網址


url = 

"http://www.quanjing.com/creative/SearchCreative.aspx?id=7"

def

 

download_one_pic

(url:str,name:str,suffix:str=

"jpg"

)

:


    

#下載單張圖片


    path = 

"."

.join([name,suffix])
    response = request.urlopen(url)
    wb_data = response.read()
    

with

 open(path,

"wb"

as

 f:
        f.write(wb_data)

def

 

download_many_pic

(urls:list)

:


    

#下載多張圖片


    start = time.time()
    

for

 i 

in

 urls:
        ts = str(int(time.time() * 

1000

))
        download_one_pic(i, ts)
    end = time.time()
    print(

u"下載完成,%d張圖片,耗時:%.2fs"

 % (len(urls), (end - start)))

def

 

get_pic_urls

(url:str)

->list:


    

#獲取頁面所有圖片鏈接


    response = request.urlopen(url)
    wb_data = response.read()
    html = etree.HTML(wb_data)
    pic_urls = html.xpath(

"//a[@class="item lazy"]/img/@src"

)
    

return

 pic_urls

def

 

allot

(pic_urls:list,n:int)

->list:


    

#根據給定的組數,分配url給每一組


    _len = len(pic_urls)
    base = int(_len / n)
    remainder = _len % n
    groups = [pic_urls[i * base:(i + 

1

) * base] 

for

 i 

in

 range(n)]
    remaind_group = pic_urls[n * base:]
    

for

 i 

in

 range(remainder):
        groups[i].append(remaind_group[i])
    

return

 [i 

for

 i 

in

 groups 

if

 i]



同步爬蟲:


def

 

crawler

()

:


    

#同步下載


    pic_urls = get_pic_urls(url)
    download_many_pic(pic_urls)



執行同步爬蟲,


crawler()



輸出(時間可能不一樣,取決於你的網速):


下載完成,196張圖片,耗時

:49.04s




在同一個網路環境下,排除網速時好時壞,可以下載多幾次取平均下載時間,在我的網路環境下,我下載了5次,平均耗時約55.26s



多進程爬蟲



所以為了提高採集速度,我們可以寫一個多進程爬蟲(以爬取圖片網站圖片為例): 


為了對應多進程的進程數n,我們可以將圖片鏈接列表分成n組,多進程爬蟲:


from

 multiprocessing.pool 

import

 Pool

def

 

multiprocess_crawler

(processors:int)

:


    

#多進程爬蟲


    pool = Pool(processors)
    pic_urls = get_pic_src(url)
    

#對應多進程的進程數processors,我們可以將圖片鏈接列表分成processors組


    url_groups = allot(pic_urls,processors)
    

for

 i 

in

 url_groups:
        pool.apply_async(func=download_many_pic,args=(i,))
    pool.close()
    pool.join()



執行爬蟲,進程數設為4,一般是cpu數量:


multiprocess_crawler(4)



輸出:


下載完成,49張圖片,耗時

:18.22s


下載完成,49張圖片,耗時

:18.99s


下載完成,49張圖片,耗時

:18.97s


下載完成,49張圖片,耗時

:19.51s




可以看出,多進程比原先的同步爬蟲快許多,整個程序耗時19.51s,為什麼不是同步爬蟲的55s/4 ≈ 14s呢?因為進程間的切換需要耗時。 如果把進程數增大,那麼:


進程數

:10

 , 耗時:12

.3s


進程數

:30

 , 耗時:2

.81s


進程數

:40

 , 耗時:11

.34s




對於多進程爬蟲來說,雖然實現非同步爬取,但也不是越多進程越好,進程間切換的開銷不僅會讓你崩潰,有時還會讓你的程序崩潰。一般用進程池Pool維護,Pool的processors設為CPU數量。進程的數量設置超過100個便讓我的程序崩潰退出。使用進程池可以保證當前在跑的進程數量控制為設置的數量,只有池子沒滿才能加新的進程進去。



多線程爬蟲



多線程版本可以在單進程下進行非同步採集,但線程間的切換開銷也會隨著線程數的增大而增大。當線程間需要共享變數內存時,此時會有許多不可預知的變數讀寫操作發生,python為了使線程同步,給每個線程共享變數加了全局解釋器鎖GIL。而我們的爬蟲不需要共享變數,因此是線程安全的,不用加鎖。多線程版本:


import

 random

from

 threading 

import

 Thread

def

 

run_multithread_crawler

(pic_urls:list,threads:int)

:


    begin = 

0


    start = time.time()
    

while

 

1

:
        _threads = []
        urls = pic_urls[begin:begin+threads]
        

if

 

not

 urls:
            

break


        

for

 i 

in

 urls:
            ts = str(int(time.time()*

10000

))+str(random.randint(

1

,

100000

))
            t = Thread(target=download_one_pic,args=(i,ts))
            _threads.append(t)
        

for

 t 

in

 _threads:
            t.setDaemon(

True

)
            t.start()
        

for

 t 

in

 _threads:
            t.join()
        begin += threads
    end = time.time()
    print(

u"下載完成,%d張圖片,耗時:%.2fs"

 % (len(pic_urls), (end - start)))

def

 

multithread_crawler

(threads:int)

:


    pic_urls = get_pic_src(url)
    run_multithread_crawler(pic_urls,threads)



並發線程數太多會讓我們的系統開銷越大,使程序花費時間越長,同時也會增大目標網站識別爬蟲機器行為的幾率。因此設置好一個適當的線程數以及爬取間隔是良好的爬蟲習慣。 執行多線程爬蟲,設置線程數為50


multithreads_crawler(50)



輸出:


下載完成,196張圖片,耗時

:3.10s




增大線程數,輸出:


線程數

:50

,耗時

:3.10s


線程數

:60

,耗時

:3.07s


線程數

:70

,耗時

:2.50s


線程數

:80

,耗時

:2.31s


線程數

:120

,耗時

:3.67s




可以看到,線程可以有效的提高爬取效率,縮短爬取時間,但必須是一個合理的線程數,越多有時並不是越好的,一般是幾十到幾百個之間,數值比多進程進程數大許多。



非同步協程爬蟲

Python3.5引入了async/await 非同步協程語法。詳見PEP492 https://www.python.org/dev/peps/pep-0492/ 由於asyncio提供了基於socket的非同步I/O,支持TCP和UDP協議,但是不支持應用層協議HTTP,所以需要安裝非同步http請求的aiohttp模塊 單進程下的非同步協程爬蟲:


import

 asyncio

from

 asyncio 

import

 Semaphore

from

 aiohttp 

import

 ClientSession,TCPConnector

async

 

def

 

download

(session:ClientSession,url:str,name:str,sem:Semaphore,suffix:str=

"jpg"

)

:


    path = 

"."

.join([name,suffix])
    

async

 

with

 sem:
        

async

 

with

 session.get(url) 

as

 response:
            wb_data = 

await

 response.read()
            

with

 open(path,

"wb"

as

 f:
                f.write(wb_data)

async

 

def

 

run_coroutine_crawler

(pic_urls:list,concurrency:int)

:


    

# 非同步協程爬蟲,最大並發請求數concurrency


    tasks = []
    sem = Semaphore(concurrency)
    conn =TCPConnector(limit=concurrency)
    

async

 

with

 ClientSession(connector=conn) 

as

 session:
        

for

 i 

in

 pic_urls:
            ts = str(int(time.time() * 

10000

)) + str(random.randint(

1

100000

))
            tasks.append(asyncio.create_task(download(session,i,ts,sem)))
        start = time.time()
        

await

 asyncio.gather(*tasks)
        end = time.time()
        print(

u"下載完成,%d張圖片,耗時:%.2fs"

 % (len(pic_urls), (end - start)))

def

 

coroutine_crawler

(concurrency:int)

:


    pic_urls = get_pic_src(url)
    loop = asyncio.get_event_loop()
    loop.run_until_complete(run_coroutine_crawler(pic_urls,concurrency))
    loop.close()



執行非同步協程爬蟲,設置最大並發請求數為100:


coroutine_crawler(100)



輸出:


下載完成,196張圖片,耗時

:2.27s




可以看出,非同步多協程的下載請求效率並不比多線程差,由於磁碟IO讀寫阻塞,所以還可以進一步優化,使用aiofiles。 針對比較大的多媒體數據下載,非同步磁碟IO可以使用aiofiles,以上述例子download可以改為:


import

 aiofiles

async

 

def

 

download

(session:ClientSession,url:str,name:str,sem:Semaphore,suffix:str=

"jpg"

)

:


    path = 

"."

.join([name,suffix])
    

async

 

with

 sem:
        

async

 

with

 session.get(url) 

as

 response:
           

async

 

with

 aiofiles.open(path,

"wb"

as

 fd:
            

while

 

1

:
                wb_data_chunk = 

await

 response.content.read(

1024

)
                

if

 

not

 wb_data_chunk:
                    

break


                

await

 fd.write(wb_data_chunk)



多進程 + 多線程 爬蟲



實際採集大量數據的過程中,往往是多種手段來實現爬蟲,這樣可以充分利用機器CPU,節省採集時間。 下面使用多進程(進程數為CPU數,4)+ 多線程 (線程數設為50)來對例子進行更改(上面各個例子導入的模塊默認使用):


def 

mixed_process_thread_crawler

(

processors:

int

,threads:

int

):
    pool 

= Pool(processors)
    pic_urls = get_pic_src(url)
    url_groups = allot(pic_urls,processors)
    

for

 

group

 

in

 url_groups:
        pool.apply_async(run_multithread_crawler,args=(

group

,threads))
    pool.close()
    pool.

join

()



執行爬蟲:


mixed_process_thread_crawler(4,50)



輸出:


下載完成,49張圖片,耗時

:2.73s


下載完成,49張圖片,耗時

:2.76s


下載完成,49張圖片,耗時

:2.76s


下載完成,49張圖片,耗時

:2.76s




採集時間與非同步協程和多線程並無多大的差異,可以使用更大數據量做實驗區分。因為多進程+多線程,CPU切換上下文也會造成一定的開銷,所以進程數與線程數不能太大,並發請求的時間間隔也要考慮進去。



多進程 + 非同步協程 爬蟲



使用多進程(進程數為CPU數,4)+ 非同步協程(最大並發請求數設為50)來對例子進行更改(上面各個例子導入的模塊默認使用):


def _coroutine_crawler(pic_urls:list,concurrency:

int

):
    loop = asyncio.get_event_loop()
    loop.run_until_complete(run_coroutine_crawler(pic_urls, concurrency))
    loop.close()

def 

mixed_process_coroutine_crawler

(

processors:

int

,concurrency:

int

):
    pool 

= Pool(processors)
    pic_urls = get_pic_src(url)
    url_groups = allot(pic_urls, processors)
    

for

 

group

 

in

 url_groups:
        pool.apply_async(_coroutine_crawler, args=(

group

, concurrency))
    pool.close()
    pool.

join

()



執行爬蟲 :


mixed_process_coroutine_crawler(4,50)



輸出:


下載完成,49張圖片,耗時

:2.56s


下載完成,49張圖片,耗時

:2.54s


下載完成,49張圖片,耗時

:2.56s


下載完成,49張圖片,耗時

:2.62s




效果與多進程 + 多線程 爬蟲差不多,但是CPU減少了切換線程上下文的開銷,而是對每一個協程任務進行監視回調喚醒。使用IO多路復用的底層原理實現。



分散式採集



關於分散式採集將會單獨寫一章,使用Map-Reduce+redis來實現分散式爬蟲。



輪子們,你們辛苦了



現實生活中的爬蟲不止上面那些,但是基本的骨架是一樣的,對於特定的網站需要制定特定的採集規則,所以通用的數據採集爬蟲很難實現。所以針對某個網站的數據採集爬蟲是需要定製的,但是在不同之中包含著許多的相同、重複性的過程,比如說採集流程,或者對請求頭部的偽造,數據持久化的處理等,採集框架應運而生。Scrapy就是目前比較成熟的一個爬蟲框架。它可以幫助我們大大減少重複性的代碼編寫,可以更好的組織採集流程。而我們只需要喝一杯咖啡,編寫自己的採集規則,讓Scrapy去給我們管理各種各樣的爬蟲,做些累活。如果你是一個爬蟲愛好者,那麼scrapy是你的不錯選擇。由於好奇scrapy的實現流程,所以我才開始打開他的源碼學習。 有些人覺得scrapy太重,他的爬蟲只需要簡單的採集,自己寫一下就可以搞定了。但如果是大量的爬蟲採集呢?怎麼去管理這些爬蟲呢?怎樣才能提高採集效率呀? Scrapy helps~!! 另外還有另一個Python採集框架:pyspider。國人編寫的,cool~ 感謝那些辛苦工作的輪子們,你們辛苦了~



本文所用代碼 均在GitHub上,地址:



https://github.com/01ly/article-codes/blob/master/articel_code_20181113.py

 推薦↓↓↓ 






??

16個技術公眾號

】都在這裡!


涵蓋:程序員大咖、源碼共讀、程序員共讀、數據結構與演算法、黑客技術和網路安全、大數據科技、編程前端、Java、Python、Web編程開發、Android、iOS開發、Linux、資料庫研發、幽默程序員等。

萬水千山總是情,點個 「

好看

」 行不行

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Python開發 的精彩文章:

TAG:Python開發 |