技術堆疊:python + scrapy + tor
為什么要單獨開這么一篇隨筆,主要還是在上一篇隨筆"一個小爬蟲的整體解決方案"(https://www.cnblogs.com/qinyulin/p/13219838.html)中沒有著重介紹Scrapy,包括后面幾天也對代碼做了Review,優化了一些性能,覺得還是應該把自己的勞動成果打個標,也怕后面需要的時候記不住,所以還是規規矩矩的寫一篇隨筆用來記錄,話不多說,上干貨,
Scrapy框架,我的理解就是在Spider中請求url,在這個請求程序中,我們會用到中間件,用來劫持網路請求,對請求Request進行一些頭部資訊、代理的封裝,然后在回傳物件Response中也可以做一些處理,把獲取到的網頁,通過bs4決議標簽元素,轉換成自己需要的資訊,當拿到資訊的時候,可以把資訊包裝成物件,通過pipe管道進行資料清理,然后再進行資料存盤(可以存本地檔案,也可以呼叫API存資料庫),具體的原理可以參考下面的鏈接https://blog.csdn.net/qq_34120459/article/details/86711728,然而在實際做的程序當中,我一次性要對于產品的評論爬成千上萬條,而且還要針對失敗后的斷點續爬,所以我就放棄了資料管道清洗方式,把所有的業務邏輯都放在Spider里面進行,總的來說,這樣做有悖于Scrapy的資料扭轉原理,但是沒辦法,和很多人交流過,貌似目前的方式至少是可行的,
首先來一個爬蟲Spider的代碼縮略圖:

這里的Init函式主要是用來做一些初始化作業,詳細代碼如下:
1 #初始化函式 2 def __init__(self,saveType=1): 3 self.keywords = []#關鍵詞陣列 4 self.totalObj = {}#所有關鍵詞結果的物件 5 # self.over = True #這個引數暫時沒用,觀察了之后可以洗掉 6 self.startTime = time.strftime("%b_%d_%Y_%H_%M_%S", time.localtime()) #開始時間,用來寫入json檔案名 7 self.saveType = saveType#來源型別,用來區分是手動還是自動 8 self.Count = 10000#定義每個Asin爬取的最大評論數量 9 self.resCount = 0#最終發送給服務器的請求條數 10 self.getCount = 0#用來計算keywords的索引是否完成了所有查詢,每次成功之后索引+1 11 self.successArr = []#成功發送的Asin陣列 12 self.loseArr = []#失敗發送的Asin陣列 13 14 #########下面的代碼是用來讀取之前的評論資料,每次完成之后會吧資料存在這個JSON檔案里面,如果中途中斷下次會讀取上一次的資料, 15 review_list_file = open("data/review_detail_crawler_all.json","w+") 16 self.review_list = review_list_file.readlines()
接著進入入口函式start_requests,因為我在爬蟲的時候需要定位到US,所以先模擬了一個表單請求,然后setSession和getAsinKeywords都是從服務器拿資料并進行處理,這里是自己的業務代碼就不貼了,
1 #爬蟲入口函式 2 def start_requests(self): 3 data =https://www.cnblogs.com/qinyulin/p/ { 4 "locationType":"LOCATION_INPUT", 5 "zipCode": "10001", 6 "storeContext": "hpc", 7 "deviceType": "web", 8 "pageType": "Detail", 9 "actionSource": "glow", 10 } 11 yield scrapy.FormRequest("https://www.amazon.com/gp/delivery/ajax/address-change.html", method="POST",formdata=https://www.cnblogs.com/qinyulin/p/data, headers={'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'},dont_filter=True, callback=self.setSession,errback=self.ceshi)
主要說一下parse_post_data這個函式,主要是根據獲取到的網頁資訊,格式化并生成標簽樹,通過bs4插件拿到資料,注釋寫得比較清楚了,代碼如下:
1 #成功回呼函式,先是判斷是否被封,如果被封就呼叫Tor代理更換IP,如果沒被封就跟著決議, 2 def parse_post_data(self, response): 3 asin = response.meta["asin"] 4 title = BeautifulSoup(response.text, 'lxml').title 5 title = title.string if not title is None else "" 6 if "Robot Check" in title: 7 print("IP被封了Spider") 8 yield from self.renew_connection(asin) 9 else: 10 #抓取該單品從開賣到現在的所有 Review 11 dom = BeautifulSoup(response.text, 'lxml') 12 try: 13 #獲取到評論串列 14 ids = dom.find(id="cm_cr-review_list").select(".review") 15 #如果評論串列長度為0則說明爬完了, 16 if len(ids) == 0: 17 self.totalObj[asin]["over"] = True 18 logger.warning("{}沒有下一頁終止爬取資料".format(asin)) 19 #回圈獲取到的評論串列,取出資料,因為是第一次做,里面獲取的方法有點雜, 20 #因為有太多未知的錯誤,所以用了try方法來賦值,里面的引數沒注釋,可以結合API看, 21 for id in ids: 22 if self.totalObj[asin]["over"] == True or len(self.totalObj[asin]["list"]) >= self.Count: 23 break 24 obj = {} 25 obj["page"] = self.totalObj[asin]["pageNumber"] 26 obj["reviewId"] = id.attrs["id"] 27 28 obj["title"] = id.select( 29 ".review-title span:first-child")[0].string 30 obj["username"] = id.select(".a-profile-name")[0].string 31 obj["content"] = id.select(".review-text-content")[0].get_text() 32 try: 33 obj["reviewDate"] = id.select(".review-date")[0].get_text() 34 except Exception as e: 35 obj["reviewDate"] = "" 36 37 try: 38 obj["voteNum"] = 0 if not len(id.select(".cr-vote-text")) else id.select(".cr-vote-text")[0].string 39 except Exception as e: 40 obj["voteNum"] = 0 41 try: 42 obj["score"] = id.select(".a-icon-alt")[0].string 43 except Exception as e: 44 obj["score"] = 0 45 46 #如果遇到reviewId評論,說明之前已經爬取過,就把當前的over狀態置為True 47 if obj["reviewId"] == self.totalObj[asin]["reviewId"]: 48 self.totalObj[asin]["over"] = True 49 logger.warning("{}回傳reviewId終止爬取{}".format(asin,obj["reviewId"])) 50 else: 51 self.totalObj[asin]["list"].append(obj) 52 print(obj) 53 pass 54 except Exception as e: 55 pass 56 #上面是決議了當前頁面的資料,下面的代碼是用來判斷是否爬完, 57 try: 58 #這里是用來判斷當前Asin評論的總頁碼 59 if self.totalObj[asin]["totalStr"] == "": 60 try: 61 totalStr = dom.select("#filter-info-section .a-size-base")[0].string 62 self.totalObj[asin]["totalStr"] = totalStr 63 except Exception as e: 64 pass 65 else: 66 totalStr = self.totalObj[asin]["totalStr"] 67 #totalStr示例:Showing 1-20 of 2,442 reviews 68 countArr = totalStr.split(' ') 69 fNum = countArr[1].split('-')[1]#當前數量20 70 tNum = countArr[3]#總數量2442 71 #如果當前數量大于等于總數量,表示已經爬完, 72 if int(fNum.replace(",","")) >= self.Count: 73 self.totalObj[asin]["over"] = True 74 logger.warning("{}大于{}條資料終止爬取".format(asin,self.Count)) 75 if fNum == tNum: 76 #最后一頁,把當前asin的結束標志置為True 77 self.totalObj[asin]["over"] = True 78 logger.warning("{}最后一頁{}終止爬取資料".format(asin,fNum)) 79 print("這是{}第{}頁".format(asin,self.totalObj[asin]["pageNumber"])) 80 #防止log資訊太多,10條錄入一次, 81 if self.totalObj[asin]["pageNumber"] % 10 == 0: 82 logger.warning("這是{}第{}頁".format(asin,self.totalObj[asin]["pageNumber"])) 83 except Exception as e: 84 pass 85 86 #如果當前Asin的over為false,說明沒有遇到reviewId和還有下一頁,則把當前Asin的頁碼加1繼續爬, 87 if self.totalObj[asin]["over"] == False: 88 self.totalObj[asin]["pageNumber"] += 1 89 yield from self.getViewForAsin(asin) 90 else: 91 #如果當前Asin的over為True,說明當前Asin已經爬完,把當前Asin的資料發送到服務器, 92 #而且根據keywords陣列進行下一個Asin的爬取,如果爬取Asin的長度大于等于keywords長度, 93 #說明整個爬取程序已經完成,不進行任何操作,進入close流程, 94 logger.warning("{}完成了一次請求,準備發送資料.".format(asin)) 95 yield from self.sendSingelData(asin) 96 97 self.getCount += 1 98 if self.getCount <= len(self.keywords) - 1: 99 yield from self.getViewForAsin(self.keywords[self.getCount])
最后就是close函式的代碼:
1 #爬蟲關閉的鉤子函式 2 def close(self, reason, spider): 3 # 打開公共設定檔案,讀取search_asin_index值,并讀取對應的search_asin_index檔案的文本,轉換成陣列并組合成發送的資料 4 if self.resCount == len(self.keywords): 5 logger.warning("全部產品的評論發送完成,共發送{}次".format(self.resCount)) 6 else: 7 logger.warning("此次爬蟲未全部爬完資料,共發送{}次".format(self.resCount)) 8 logger.warning("成功的產品有{}".format(self.successArr)) 9 logger.warning("失敗的產品有{}".format(self.loseArr)) 10 self.file = open('data/review_detail_crawler_all.json'.format(self.startTime,time.strftime("%H_%M_%S", time.localtime())), 'wb') 11 self.file.write(json.dumps(self.totalObj).encode()) 12 self.file.close() 13 pass
在確定用Scrapy框架之前,我也是去體驗了一把requests庫和selenium,requests的話做一些簡單的爬蟲需求還是可以,不能規模化;selenium的話主要還是通過模擬用戶行為來進行資料的爬取,主要用于自動化測驗的場景,在一些需要復雜操作的爬蟲還是可以的,但是如果用在大規模爬取專案是非常耗時的,所以我最侄訓是選取了Scrapy框架,而且在后期因為要頻繁爬取,容易觸發亞馬遜的反爬策略,所以又研究了Tor網路,這個是用來隱藏服務器IP,通過代理去進行爬蟲請求,這個從最終的代碼量來看其實不大,但是在研究的程序也是備受煎熬,因為在百分百成功之前的每一步都是在反思為什么不行,從拿到需求到熟悉框架,到最后完成上線,然后再review優化代碼差不多1個月時間,特別是最開始在了解了框架的原理,但是卻不能解決自己的需求反復去尋求解決方案是最難熬的,不過當在一次次試錯后找到最后的解決方案,還是很有成就感,
其實Scrapy的東西還很多,我用到的只是很小的一部分,希望在后面有迭代需求的時候可以再繼續研究,下面貼出專案中用到的一些名詞和對應的網址,也希望看到這篇文章的小伙伴如果在研究Scrapy遇到問題,可以進行留言或者私信交流,學無止境,我們一直在路上,
Python教程(我推薦廖雪峰的):https://www.liaoxuefeng.com/wiki/1016959663602400
Scrapy官網:https://scrapy.org/
requests:https://requests.readthedocs.io/en/master/
基礎爬蟲+selenium教程(我就是看著這位小伙伴的連載入的門):https://www.cnblogs.com/Albert-Lee/p/6238866.html
bs4:https://beautifulsoup.readthedocs.io/zh_CN/v4.4.0/
tor:
linux版本:https://medium.com/@mimizhang55555/%E5%9C%A8scrapy%E4%B8%AD%E4%BD%BF%E7%94%A8tor%E4%BB%A3%E7%90%86-20a0f07c14b2
win版本:https://www.cnblogs.com/kylinlin/archive/2016/03/04/5242266.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/42923.html
標籤:Linux
上一篇:hadoop集群啟動報錯
下一篇:uboot怎么設定內核的啟動地址
