Python安全工具開發(一) :分散式爬蟲初探

本文由江sir原創投稿一葉知安,已向作者發放一定獎勵。

江sir個人博客:blogsir.com.cn

投稿參見:一葉知安徵稿[歡迎踴躍投稿

前言

寫的一款百度爬蟲採集工具,自己平時用的都是window下的一些exe工具,但在自己linux系統里還是有點不太方便,就自己寫了一個百度爬蟲工具,之後做一些批量掃描測試的時候可以發揮很大用處了

多線程類沒啥可說的,寫好多了,都可以直接拿來用的,詳細講解下spider()函數

網頁分析

在網頁分析時主要是注意一下幾點

  • 每頁的網頁鏈接格式,一般都有固定的鏈接格式,如百度的每頁搜索結果鏈接是只取兩個個參數的結果是這樣,每頁10條

    https://www.baidu.com/s?wd=ctf&pn=10n

  • F12對當前頁面分析每個鏈接的特點,百度搜索有點坑,你會發現百度都是通過一個長長的鏈接302跳轉來訪問的,隨便選取一個鏈接都是這種

    <a target="_blank" href="http://www.baidu.com/link?url=GI9K125i3rnLbxL2-kKs-2g2OZt-oDTJZZIFjndQHXGiDubfIEpvNxnnCc1h5ags" class="c-showurl" stylex="text-decoration:none;">www.secbox.cn/tag/<b>ctf</b> </a>n

    特徵就是class=」c-showurl」 屬性值,用bs庫去獲取所有有這個屬性的tagres = soup.find_all(name="a", attrs={class:c-showurl})
  • 訪問跳轉鏈接獲取實際網站url,title之類的信息

上面就是我們寫一個百度爬蟲需要做的所有事,也沒多少內容

爬蟲實戰

我習慣先寫一個簡單腳本測試下爬蟲,先把第一頁的所有網站url爬取下來,如果沒問題,在搬到多線程中,如下簡單爬蟲腳本

target = https://www.baidu.com/s?wd=%s&pn=%s%(ctf,10)nheaders = {User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0}nhtml = requests.get(target,headers=headers)nsoup = bs(html.text,"lxml")nres = soup.find_all(name="a", attrs={class:c-showurl})n# print resnfor r in res:ntry:n h = requests.get(r[href],headers=headers,timeout=3)nif h.status_code == 200:n url = h.urln title = re.findall(r<title>(.*?)</title>,h.content)[0]n title = title.decode(utf-8)#解碼成unicode,否則add_row會轉換出錯nprint url,titlen urls.append((pn,url,title))nprint urlsnelse:ncontinuenexcept:ncontinuen

下面是具體多線程百度爬蟲類

#!/usr/bin/env pythonn# -*- coding: utf-8 -*-n# @Date : 2017-03-19 15:06:48n# @Author : 江sir (qq:2461805286)n# @Link : http://www.blogsir.comn# @Version : v1.0nnnimport requestsnfrom bs4 import BeautifulSoup as bsnimport threading nimport renfrom Queue import Queuenfrom prettytable import PrettyTable nimport argparsenimport timenimport sysnnnnthread_count = 3 #修改線程npage = 5 #可修改抓取頁數nnurls = []nnx = PrettyTable([page,url,title])nx.align["title"] = "1" # Left align city namesnx.padding_width = 1nnpage = (page+1) * 10nnnclass ichunqiu(threading.Thread):ndef __init__(self, queue):n threading.Thread.__init__(self)n self.Q = queue n self.headers = {User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0}nndef run(self):nwhile 1:ntry:n t = self.Q.get(True, 1)n# print tn self.spider(t)nexcept Exception,e: #調試最好列印出錯信息,否則,spider函數出錯也無法定位錯誤,多次遇到這個問題了,靠列印才解決nprint enbreaknndef spider(self,target):n pn = int(target.split(=)[-1])/10 +1n# print pnn# print targetn html = requests.get(target,headers=self.headers)n# print htmln soup = bs(html.text,"lxml")n res = soup.find_all(name="a", attrs={class:c-showurl})n# print resnfor r in res:ntry:n h = requests.get(r[href],headers=self.headers,timeout=3)nif h.status_code == 200:n url = h.urln title = re.findall(r<title>(.*?)</title>,h.content)[0]n title = title.decode(utf-8)#解碼成unicode,否則add_row會轉換出錯nprint url,titlen urls.append((pn,url,title))nelse:ncontinuenexcept:ncontinuennnndef Load_Thread(queue):nreturn [ichunqiu(queue) for i in range(thread_count)]nndef Start_Thread(threads):nprint thread is start...nfor t in threads:n t.setDaemon(True)n t.start()nfor t in threads:n t.join()nprint thread is end...nndef main():n start = time.time()n parser = argparse.ArgumentParser()n parser.add_argument(-s)n parser.add_argument(-f)n arg = parser.parse_args()n# print argn word = arg.sn output = arg.f n# word = inurl:login.actionn# output = test.txtn queue = Queue()nfor i in range(0,page,10):n target = https://www.baidu.com/s?wd=%s&pn=%s%(word,i)n queue.put(target)nn thread_list = Load_Thread(queue)n Start_Thread(thread_list)nnnif output:nwith open(output,a) as f:nfor record in urls:n f.write(record[1]+n)nprint urls,len(urls)nfor record in urls:n x.add_row(list(record))nnprint xnprint 共爬取數據%s%len(urls)nprint time.time()-startnnif __name__ == __main__:n main()n

掃的結果如上,現在爬』inurl:login.action』 竟然還能撈到幾個伺服器,2333,最大可以爬取500多條數據,寫爬蟲的過程中又發現不少問題,每次寫都能收穫很多,這次主要是編碼的問題,PrettyTable在add_row時編碼錯誤,挺奇怪,然後在存儲MySQL資料庫時也出現了編碼錯誤,於是下工夫花了半天把python編解碼的問題有複習了一篇,本來想單獨寫成一個python編解碼那些事,想想算了,弄一起得了

字元編解碼那些事

Python默認字元串採用的是ascii編碼方式,如下所示:

python -c "import sys; print sys.getdefaultencoding()"nasciin

可以通過#coding:utf-8 指定頁面默認編碼為utf-8(ps:但系統默認還是ascii)

字元串的編解碼都是以unicode為中間編碼,無法直接完成轉換,python會自動按其系統默認編碼方式解碼為unicode,再編碼成另一中編碼格式

比如:

#coding:utf-8ns = 中文nprint s.decode(gbk)n

一些報錯信息如下

  1. 報錯為UnicodeDecodeError: gbk codec cant decode bytes in position 2-3: illegal multibyte sequence

即頁面指定為utf-8,但decode()會將gbk轉換為unicode,所有就報錯了,報錯類型為UnicodeDecodeError

  1. 如果去掉#coding:utf-8 報錯為SyntaxError: Non-ASCII character xe4 in file /home/sublime/test/exception/2.py on line 2, but no encoding declared; see PEP 263 -- Defining Python Source Code Encodings for details

The problem is that your code is trying to use the ASCII encoding, but the pound symbol is not an ASCII character. Try using UTF-8 encoding. You can start by putting # -- coding: utf-8 -- at the top of your .py file.(大概就是頁面嘗試去使用ascii編碼,但發現頁面存在非ascii字元,因此報了一個語法錯誤)

  1. 如果指定頁面編碼為ascii

#!/usr/bin/pythonn# -*- coding: ascii -*-ns = 中文n

那麼報錯就是SyntaxError: ascii codec cant decode byte 0xe4 in position 5: ordinal not in range(128) 即ascii無法編碼超過128的字元,這個和上一個很相似,但報錯確不一樣

另外發現一個有趣的事

>>> s=中文n>>> snxe4xb8xadxe6x96x87n>>> s.decode(utf8)nu中文n

linux終端下終端python交互環境默認也是utf-8格式,不需要指定#coding:utf-8即可編碼中文字元

linux終端和sublime默認使用unicode,可以顯示utf-8和unicode編碼,但window下終端使用gbk編碼,utf和unicode顯示亂碼,這點要注意,出錯是編解碼轉換出錯,亂碼是終端或顯示器不支持該編碼格式,無法顯示

小技巧

1.如果想知道一個字元串是什麼編碼,可以print [字元串] 來看二進位碼,一般有如下兩種

[u』目標區服』] unicode編碼

[『xe7x9bxaexe6xa0x87xe5x8cxbaxe6x9cx8d』] utf-8編碼

另一個例子

#!/usr/bin/pythonn# -*- coding: utf-8 -*-nimport sysn# reload(sys)n# sys.setdefaultencoding(utf-8) ns = 中文nnprint [s]nprint s.encode(gbk)n

此時報錯為UnicodeDecodeError: ascii codec cant decode byte 0xe4 in position 0: ordinal not in range(128) 猜測雖然指定了當前頁面為utf-8,但因為直接encode()轉換程序會自動先按照系統默認的編碼(此時還是ascii) decode一次成unicode,再從unicode編碼為gbk, 因為s編碼為utf-8,明顯解碼出錯

有兩種解決辦法

1 手動解碼 print s.decode(『utf-8』).encode(『gbk』)

2 改變系統默認編碼,即加入這兩句

reload(sys)nsys.setdefaultencoding(utf-8) n

分散式爬蟲

在將多線程版本改寫成分散式的百度爬蟲,主要用的可跨平台的multiprocessing.managers的BaseManager模塊,這個模塊的主要功能就是將task_queue和result_queue兩個隊列註冊成函數暴露到網上去,Master節點監聽埠,讓Worker子節點去連接,不同主機之間就可以通過註冊的函數來共享同步資源,Master節點主要負責發送任務和獲取結果,Worker就獲取任務隊列的任務開始跑,並將獲取的結果存儲到資料庫獲取返回回來

spider_Master 文件: 注釋寫的很明白了,Master節點創建了task_queue和result_queue隊列,通過BaseManager模塊和Worker子節點交互,將結果存儲到資料庫中

BaseManager一些常用函數

connect(self)n | Connect manager object to the server processn | n | get_server(self)n | Return server object with serve_forever() method and address attributen | n | join(self, timeout=None)n | Join the manager process (if it has been spawned)n | n | start(self, initializer=None, initargs=())n | Spawn a server process for this manager objectn | n | ----------------------------------------------------------------------n | Class methods defined here:n | n | register(cls, typeid, callable=None, proxytype=None, exposed=None, method_to_typeid=None, create_method=True) from __builtin__.typen | Register a typeid with the manager typen

spider_Worker 節點主要調用spider()函數對任務進行處理,方法都類似,子節點每獲取一個鏈接就傳回Master, 另外需要注意的是Master文件只能運行一個,但Worker節點可以同時運行多個並行同步處理task任務隊列

spider_Master.py

#coding:utf-8nnfrom multiprocessing.managers import BaseManagernfrom Queue import Queue nimport timenimport argparsenimport MySQLdbnimport sysnnpage = 2nword = inurl:login.actionnoutput = test.txtnnpage = (page+1) * 10nnhost = 127.0.0.1nport = 500nurls = []nnclass Master():ndef __init__(self):n self.task_queue = Queue() #server需要先創建兩個共享隊列,worker端不需要n self.result_queue = Queue()nndef start(self):n BaseManager.register(get_task_queue,callable=lambda:self.task_queue) #在網路上註冊一個get_task_queue函數,即把兩個隊列暴露到網上,worker端不需要callable參數n BaseManager.register(get_result_queue,callable=lambda:self.result_queue)nn manager = BaseManager(address=(host,port),authkey=sir)n manager.start() #master端為start,即開始監聽埠,worker端為connectnn task = manager.get_task_queue() #master和worker都是從網路上獲取task隊列和result隊列,不能在創建的兩個隊列n result = manager.get_result_queue()nnnprint put tasknfor i in range(0,page,10):n target = https://www.baidu.com/s?wd=%s&pn=%s%(word,i)nprint put task %s%targetn task.put(target)nnprint try get resultnwhile True:ntry:n url = result.get(True,5) #獲取數據時需要超時長一些nprint url n urls.append(url)nexcept:nbreakn manager.shutdown()nnif __name__ == __main__:n start = time.time()n server = Master()n server.start()nprint 共爬取數據%s條%len(urls)nprint time.time()-startnwith open(output,a) as f:nfor url in urls:n f.write(url[1]+n)nn conn = MySQLdb.connect(localhost,root,root,Struct,charset=utf8)n cursor = conn.cursor()nfor record in urls:n sql = "insert into s045 values(%s,%s,%s)"%(record[0],record[1],str(record[2]))n cursor.execute(sql)n conn.commit()n conn.close()n

spider_Worker

#coding:utf-8nnnimport renimport Queuenimport timenimport requestsnfrom multiprocessing.managers import BaseManagernfrom bs4 import BeautifulSoup as bsnnnhost = 127.0.0.1nport = 500nnnnclass Worder():ndef __init__(self):n self.headers = {User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0}nndef spider(self,target,result):n urls = []n pn = int(target.split(=)[-1])/10 +1n# print pnn# print targetn html = requests.get(target,headers=self.headers)n soup = bs(html.text,"lxml")n res = soup.find_all(name="a", attrs={class:c-showurl})nfor r in res:ntry:n h = requests.get(r[href],headers=self.headers,timeout=3)nif h.status_code == 200:n url = h.urln# print urln time.sleep(1)n title = re.findall(r<title>(.*?)</title>,h.content)[0]n# print url,titlen title = title.decode(utf-8)nprint send spider url:,url n result.put((pn,url,title))nelse:ncontinuenexcept:ncontinuenn# return urlsnnndef start(self):n BaseManager.register(get_task_queue)n BaseManager.register(get_result_queue)nnprint Connect to server %s%hostn m = BaseManager(address=(host,port),authkey=sir)n m.connect()nn task = m.get_task_queue()n result = m.get_result_queue()nnprint try get queuenwhile True:ntry:n target = task.get(True,1)nprint run pages %s%targetn res = self.spider(target,result)n# print resnnexcept:nbreak nnnnnif __name__ == __main__:n w = Worder()n w.start()n

這個分散式爬蟲還有待優化,發現跑的速度還沒多線程快,繼續會更新優化python爬蟲相關內容

關於加入一葉之安讀者微信群的問題

我們開通了微信群後,有大量的讀者加入,現一群已滿,所以創建了二群,你還可以加微信:Guest_Killer_0nliscimoom829Snake_90邀請進入。

Ps:已加入一群的小夥伴們就別再申請了^_^ 感謝大家的支持!

關於小密圈

前幾天,我們開設了『一葉知安小密圈』,為了避免潛水黨、醬油黨亂入,影響了整個學習、交流的氛圍。我們啟用了收費進入的方式。價格是小密圈最低定價 「50/人」。很快就有很多小夥伴加入,氛圍很好。附上部分截圖。

有想加入這個圈子的,可以掃描下方二維碼進入。


推薦閱讀:

跟黃哥學python序列文章之python二分查找演算法
Scrapy爬圖片(二)
用Python玩GTA 5—使用OpenCV讀取遊戲面面

TAG:Python | 黑客Hacker | 网页爬虫 |