用Scrapy框架開發的一個爬蟲項目

来源:https://www.cnblogs.com/qinyulin/archive/2020/07/02/13223677.html
-Advertisement-
Play Games

技術棧:python + scrapy + tor 為什麼要單獨開這麼一篇隨筆,主要還是在上一篇隨筆"一個小爬蟲的整體解決方案"(https://www.cnblogs.com/qinyulin/p/13219838.html)中沒有著重介紹Scrapy,包括後面幾天也對代碼做了Review,優化了 ...


  技術棧: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 = {
 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=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

 

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 通過runlike去查看一個容器的docker run啟動參數 ...
  • 前言 在編程中異常報錯是不可避免的。特別是在學習某個語言初期,看到異常報錯就抓耳撓腮,常常開玩笑說編程1分鐘,改bug1小時。今天就讓我們來看看什麼是異常和怎麼合理的處理異常吧! 異常與error介紹 下麵還是先讓我們來看一下基本概念吧! ​ 異常指程式運行過程中出現的非正常現象,例如用戶輸入錯誤、 ...
  • 打開VMware虛擬網路編輯器,裡面沒有VMnet0(沒有紅圈的),此處只是我電腦里被我改後顯示出來的,原本沒有。 1、選擇 “右鍵屬性->相容性-->以管理員身份運行此程式”,應用,確認。 2、確認後退出,重新打開“虛擬機編輯器”,就解決OK啦。 ...
  • Linux 命令詳解- CD,LS,PWD 命令 今天這三個命令是用於 Linux 中的文件管理。 cd 命令:用於切換當前工作目錄至 dirName(目錄參數)。 語法: cd [dirName] 參數: " ~ " 代位符 代表的是個人目錄的地址,個人目錄地址隨著用戶身份的改變而變化。 # 在普 ...
  • 更換Ubuntu國內鏡像源: 由於Ubuntu官方軟體倉庫伺服器位於國外,導致我們國內訪問體驗非常糟糕,下載網速只有200-300k左右,令人淚目。 所以我們需要將系統預設下載地址配置為國內的鏡像源,以此提高用戶體驗。 準備工作: 備份源文件: cp /etc/apt/sources.list /e ...
  • 高密度MRAM具有非常低的功率,高的讀取速度,非常高的數據保留能力和耐久性,適用於廣泛的應用。單元面積僅為0.0456平方微米,讀取速度為10ns,讀取功率為0.8mA/MHz/b,在低功耗待機模式(LPSB)下,其在25C時的泄漏電流小於55mA,相當於每比特的漏電流僅為1.7E-12A。對於32 ...
  • 1>拉取鏡像 docker pull elasticsearch:7.6.2 2>創建數據掛在目錄,以及配置ElasticSearch集群配置文件 mkdir /docker/ES mkdir /docker/ES/data1 mkdir /docker/ES/data2 mkdir /docker ...
  • Docker安裝單機版ELK日誌收集系統 概述 現在Elasticsearch是比較火的, 很多公司都在用. 而Docker也正如火如荼, 所以我就使用了Docker來安裝ELK, 這裡會詳細介紹下安裝的細節以及需要註意的地方. 先來強調一下, Elasticsearch和Kibana必須用相同版本 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...