上一篇文章《Python | 資料分析實戰Ⅰ》中,實現了對資料的簡單爬取,在文末也遺留了了一些問題。
- 拉鉤網對於同一ip的大量請求行為肯定會進行封禁,所以需要準備代理池。
- 為了實現高自動化,需要對一系列可能出現的異常情況進行處理,斷點處理,確保程式不掛。
- 為了提高效率,加入多執行緒。
- 資料持久化,在持久化之前需要先進行清洗。
在這篇文章中,我們主要對以上幾個問題進行思考,並採取一些解決方式。
這篇文章主要包括:
- 構建一個簡易的代理池
- 記錄異常日誌
- 多執行緒
- 資料持久化
簡易代理池
假如在對一個網站進行大量訪問爬取時,略有反爬措施的網站一定會檢測到你這個異常IP並進行封禁。如果你還沒遇到這類情況,要麼就是你爬取的資料量還太少,網站完全不care;要麼是網站的基本安全措施不完善。
我在寫這篇文章時,還沒較大規模地爬取過一個網站,所以構建簡易代理池暫時沒有考慮效率方面的問題,主要是用來練手,學習之用。
現在的普遍情況是:免費,不穩定;穩定。不免費。所以大部分商業級爬蟲開發者都是會付費購買有人專門維護的代理池,我們這種玩玩的,圖個意思就好。不過我也希望,能有大佬發起一個開源專案,讓有時間的同學能一起維護使用一個代理池。
我的思路很簡單,首先爬取一些提供免費代理的網站,然後對這些代理進行測試篩選,將可用的一部分儲存在本地,在需要的時候進行呼叫。
首先構建一個IpProxy
類,在這個類中有三個主要方法,__init__(self)
初始化方法,get_ip(self)
用來抓取某網站的免費代理,validate_proxy(self,pool)
對免費代理進行驗證,然後暫存在檔案中(我是覺得沒有必要在本地持久化,因為這些代理存活率低,往往不久就會失效,所以在跑主爬蟲前,有必要即時跑一下這個代理爬蟲,獲取最新的可用代理)。
思路很簡單,也沒什麼技術含量,下面是主要程式碼實現部分。
class IpProxy():
def __init__(self):
self.ip_pool=[]
self.ip_pool_after_validate=[]
self.url="http://www.xicidaili.com/nn/"
self.headers={
"Host":"www.xicidaili.com",
"User-Agent":"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
"Upgrade-Insecure-Requests":"1"
}
logging.basicConfig(filename=os.path.join(os.getcwd(), 'validateProxy.txt'), level=logging.INFO)
logging.basicConfig(filename=os.path.join(os.getcwd(), 'log_proxy.txt'), level=logging.ERROR)
def get_ip(self):
#暫時只爬取一頁嘗試
try:
result = requests.get(self.url, headers=self.headers)
except:
logging.error("獲取免費代理失敗")
raise
result.encoding="utf-8"
content=result.content
bs=BeautifulSoup(content,"html.parser")
trs=bs.find_all("tr")
for tr in trs[1:]:
try:
ip={}
tds=tr.find_all("td")
ip["address"] = tds[1].text
ip["port"] = tds[2].text
ip["type"] = tds[5].text.lower()
self.ip_pool.append(ip)
# print(ip)
# print(self.ip_pool)
except:
logging.error("將tag為:"+tds+"構造為資料字典失敗")
pass
return self.ip_pool
def validate_proxy(self,pool):
proxies={
"http":"",
"https":""
}
for item in pool:
ip=item["type"]+"://"+item["address"]+":"+item["port"]
if item["type"]=="http":
proxies["https"]=""
proxies["http"]=ip
try:
res = requests.get("http://www.baidu.com", proxies=proxies)
if res == None:
continue
else:
print(ip+"可用")
logging.info(ip)
self.ip_pool_after_validate.append(ip)
except:
print(ip+"不是可用的代理")
pass
else:
proxies["http"]=""
proxies["https"]=ip
try:
res = requests.get("http://www.baidu.com", proxies=proxies)
if res == None:
continue
else:
print(ip + "可用")
logging.info(ip)
self.ip_pool_after_validate.append(ip)
except:
print(ip+"不是可用的代理")
pass
return self.ip_pool_after_validate
複製程式碼
輸出到檔案中:
可以看到是能正常工作的,速度對我來說其實還可以,可以接受。如果想優化一下的話,這裡有一個解決點:在程式碼中可以看到我是用代理ip去訪問了一下百度看能不能返回狀態200,能科學上網的時候,百度都是被用來測試網路是否連通的(笑,這是一種效率比較低的做法,同學可以去試試使用telnetlib
模組。
異常日誌
程式碼出現異常,那是再正常不過的事兒了。讓我們看看程式碼中哪些地方比較容易出現異常。
代理爬蟲中:
- 請求頁面返回資料時
- 構造ip資料時
- 使用某不可用代理訪問百度時
主爬蟲中:
- 請求頁面時
- 解析資料介面時
- 某一頁資料未正常獲取時
持久化時:
- 資料庫連線時
- 資料插入失敗時
使用os
+logging
模組來將資訊列印到日誌檔案中。具體請看程式碼,這這裡就具體演示了。
多執行緒
使用多執行緒來同時執行幾隻不同的主爬蟲,比如同時Java
和Python
的崗位資訊,或者做一些其他的操作。
為了物件導向,首先我們構建一個CrawThread
類。其中主要的兩個方法是run
準備執行的函式,get_result
獲取返回值。
程式碼如下:
class CrawThread(threading.Thread):
def __init__(self,url,java_job):
threading.Thread.__init__(self)
self.url=url
self.job=java_job
self.all_page_info=[]
def run(self):
print("新執行緒開始")
for x in range(2, 4):
data = {
"first": "false",
"pn": x,
"kd": "Java"
}
try:
current_page_info = self.job.getJob(self.url, data)
print(current_page_info)
self.all_page_info.append(current_page_info)
print("第%d頁已經爬取成功" % x)
time.sleep(5)
except:
print("第%d頁爬取失敗,已記錄" % x)
logging.error("第%d頁爬取失敗,已記錄" % x)
pass
def getResult(self):
return self.all_page_info
複製程式碼
主函式中調取:
if __name__ == '__main__':
java_url="https://www.lagou.com/jobs/positionAjax.json?px=default&city=%E4%B8%8A%E6%B5%B7&needAddtionalResult=false&isSchoolJob=0"
java_job = Job()
java_all_page_info=[]
python_url="https://www.lagou.com/jobs/positionAjax.json?px=default&city=%E4%B8%8A%E6%B5%B7&needAddtionalResult=false"
python_job=Job()
python_all_page_info=[]
java_t=CrawThread(java_url,java_job)
java_t.start()
java_t.join()
java_all_page_info =java_t.getResult()
# print(java_all_page_info)
# print(type(java_all_page_info))
addPosition(java_all_page_info)
# python_t=CrawThread(python_url,python_job)
# python_all_page_info=python_t.start()
複製程式碼
持久化
為了便於後面的資料分析,所爬取的資料肯定是需要持久化到資料庫的。我使用的是MySQL
,這裡可以使用pymysql
模組。
我這裡選取了這幾項資料進行採集。
接下來的操作也沒什麼需要著重強調的,但是在實際操作中,我發現了一個坑:
我一開始將我的職位表名命名為:position,結果在執行sql語句時一直出錯。一條簡單的插入語句,找來找去沒發現錯誤,後來想到可能position是MySQL的保留字?便將表名更改,遂執行通過。
db_username="root"
db_password="root"
logging.basicConfig(filename=os.path.join(os.getcwd(), 'log_db.txt'), level=logging.ERROR)
def addPosition(positions):
db = pymysql.Connect(
host="127.0.0.1",
port=3306,
user=db_username,
passwd=db_password,
db='lagou_position',
charset='utf8'
)
try:
cursor = db.cursor()
except:
print("連線資料庫失敗")
logging.error("連線資料庫失敗")
print(len(positions))
for x in range(0,len(positions)):
for position in positions[x]:
print(position)
temp = []
temp.append(position["companyId"])
temp.append(position["positionAdvantage"])
temp.append(position["salary"])
temp.append(position["positionName"])
temp.append(position["companySize"])
temp.append(position["workYear"])
temp.append(position["education"])
temp.append(position["jobNature"])
temp.append(position["industryField"])
temp.append(position["city"])
temp.append(position["companyFullName"])
temp.append(position["firstType"])
temp.append(position["secondType"])
try:
sql = "INSERT INTO java_position(companyId,positionAdvantage,salary,positionName,companySize,workYear,education,jobNature,industryField,city,companyFullName,firstType,secondType) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)"
cursor.execute(sql,temp)
db.commit()
except:
print("插入資料出現錯誤:")
logging.error("插入資料出現錯誤")
db.rollback()
pass
複製程式碼
此時測試爬取幾張頁面,發現資料已經被採集到資料庫中了。