第11讲:Reqeusts + PyQuery + PyMongo 基本案例实战
在前面我們已經(jīng)學習了多進程、requests、正則表達式、pyquery、PyMongo 等的基本用法,但我們還沒有完整地實現(xiàn)一個爬取案例。本課時,我們就來實現(xiàn)一個完整的網(wǎng)站爬蟲案例,把前面學習的知識點串聯(lián)起來,同時加深對這些知識點的理解。
1.準備工作
在本節(jié)課開始之前,我們需要做好如下的準備工作:
- 安裝好 Python3(最低為 3.6 版本),并能成功運行 Python3 程序。
- 了解 Python 多進程的基本原理。
- 了解 Python HTTP 請求庫 requests 的基本用法。
- 了解正則表達式的用法和 Python 中正則表達式庫 re 的基本用法。
- 了解 Python HTML 解析庫 pyquery 的基本用法。
- 了解 MongoDB 并安裝和啟動 MongoDB 服務(wù)。
- 了解 Python 的 MongoDB 操作庫 PyMongo 的基本用法。
以上內(nèi)容在前面的課時中均有講解,如果你還沒有準備好,那么我建議你可以再復習一下這些內(nèi)容。
2.爬取目標
這節(jié)課我們以一個基本的靜態(tài)網(wǎng)站作為案例進行爬取,需要爬取的鏈接為:https://static1.scrape.cuiqingcai.com/,這個網(wǎng)站里面包含了一些電影信息,界面如下:
首頁是一個影片列表,每欄里都包含了這部電影的封面、名稱、分類、上映時間、評分等內(nèi)容,同時列表頁還支持翻頁,點擊相應(yīng)的頁碼我們就能進入到對應(yīng)的新列表頁。
如果我們點開其中一部電影,會進入電影的詳情頁面,比如我們點開第一部《霸王別姬》,會得到如下頁面:
這里顯示的內(nèi)容更加豐富、包括劇情簡介、導演、演員等信息。
我們這節(jié)課要完成的目標是:
- 用 requests 爬取這個站點每一頁的電影列表,順著列表再爬取每個電影的詳情頁。
- 用 pyquery 和正則表達式提取每部電影的名稱、封面、類別、上映時間、評分、劇情簡介等內(nèi)容。
- 把以上爬取的內(nèi)容存入 MongoDB 數(shù)據(jù)庫。
- 使用多進程實現(xiàn)爬取的加速。
那么我們現(xiàn)在就開始吧。
3.爬取列表頁
爬取的第一步肯定要從列表頁入手,我們首先觀察一下列表頁的結(jié)構(gòu)和翻頁規(guī)則。在瀏覽器中訪問 https://static1.scrape.cuiqingcai.com/,然后打開瀏覽器開發(fā)者工具,觀察每一個電影信息區(qū)塊對應(yīng)的 HTML,以及進入到詳情頁的 URL 是怎樣的,如圖所示:
可以看到每部電影對應(yīng)的區(qū)塊都是一個 div 節(jié)點,它的 class 屬性都有 el-card 這個值。每個列表頁有 10 個這樣的 div 節(jié)點,也就對應(yīng)著 10 部電影的信息。
我們再分析下從列表頁是怎么進入到詳情頁的,我們選中電影的名稱,看下結(jié)果:
可以看到這個名稱實際上是一個 h2 節(jié)點,其內(nèi)部的文字就是電影的標題。h2 節(jié)點的外面包含了一個 a 節(jié)點,這個 a 節(jié)點帶有 href 屬性,這就是一個超鏈接,其中 href 的值為 /detail/1,這是一個相對網(wǎng)站的根 URL https://static1.scrape.cuiqingcai.com/ 路徑,加上網(wǎng)站的根 URL 就構(gòu)成了 https://static1.scrape.cuiqingcai.com/detail/1,也就是這部電影詳情頁的 URL。這樣我們只需要提取這個 href 屬性就能構(gòu)造出詳情頁的 URL 并接著爬取了。
接下來我們來分析下翻頁的邏輯,我們拉到頁面的最下方,可以看到分頁頁碼,如圖所示:
頁面顯示一共有 100 條數(shù)據(jù),10 頁的內(nèi)容,因此頁碼最多是 10。接著我們點擊第 2 頁,如圖所示:
可以看到網(wǎng)頁的 URL 變成了 https://static1.scrape.cuiqingcai.com/page/2,相比根 URL 多了 /page/2 這部分內(nèi)容。網(wǎng)頁的結(jié)構(gòu)還是和原來一模一樣,所以我們可以和第 1 頁一樣處理。
接著我們查看第 3 頁、第 4 頁等內(nèi)容,可以發(fā)現(xiàn)有這么一個規(guī)律,每一頁的 URL 最后分別變成了 /page/3、/page/4。所以,/page 后面跟的就是列表頁的頁碼,當然第 1 頁也是一樣,我們在根 URL 后面加上 /page/1 也是能訪問的,只不過網(wǎng)站做了一下處理,默認的頁碼是 1,所以顯示第 1 頁的內(nèi)容。
好,分析到這里,邏輯基本就清晰了。
如果我們要完成列表頁的爬取,可以這么實現(xiàn):
- 遍歷頁碼構(gòu)造 10 頁的索引頁 URL。
- 從每個索引頁分析提取出每個電影的詳情頁 URL。
現(xiàn)在我們寫代碼來實現(xiàn)一下吧。
首先,我們需要先定義一些基礎(chǔ)的變量,并引入一些必要的庫,寫法如下:
import requests import logging import re import pymongo from pyquery import PyQuery as pq from urllib.parse import urljoinlogging.basicConfig(level=logging.INFO,format='%(asctime)s - %(levelname)s: %(message)s')BASE_URL = 'https://static1.scrape.cuiqingcai.com' TOTAL_PAGE = 10這里我們引入了 requests 用來爬取頁面,logging 用來輸出信息,re 用來實現(xiàn)正則表達式解析,pyquery 用來直接解析網(wǎng)頁,pymongo 用來實現(xiàn) MongoDB 存儲,urljoin 用來做 URL 的拼接。
接著我們定義日志輸出級別和輸出格式,完成之后再定義 BASE_URL 為當前站點的根 URL,TOTAL_PAGE 為需要爬取的總頁碼數(shù)量。
定義好了之后,我們來實現(xiàn)一個頁面爬取的方法吧,實現(xiàn)如下:
def scrape_page(url):logging.info('scraping %s...', url)try:response = requests.get(url)if response.status_code == 200:return response.textlogging.error('get invalid status code %s while scraping %s', response.status_code, url)except requests.RequestException:logging.error('error occurred while scraping %s', url, exc_info=True)考慮到我們不僅要爬取列表頁,還要爬取詳情頁,所以在這里我們定義一個較通用的爬取頁面的方法,叫作 scrape_page,它接收一個 url 參數(shù),返回頁面的 html 代碼。
這里我們首先判斷狀態(tài)碼是不是 200,如果是,則直接返回頁面的 HTML 代碼,如果不是,則會輸出錯誤日志信息。另外,這里實現(xiàn)了 requests 的異常處理,如果出現(xiàn)了爬取異常,則會輸出對應(yīng)的錯誤日志信息。這時我們將 logging 的 error 方法的 exc_info 參數(shù)設(shè)置為 True 則可以打印出 Traceback 錯誤堆棧信息。
好了,有了 scrape_page 方法之后,我們給這個方法傳入一個 url,正常情況下它就可以返回頁面的 HTML 代碼了。
在這個基礎(chǔ)上,我們來定義列表頁的爬取方法吧,實現(xiàn)如下:
def scrape_index(page):index_url = f'{BASE_URL}/page/{page}'return scrape_page(index_url)方法名稱叫作 scrape_index,這個方法會接收一個 page 參數(shù),即列表頁的頁碼,我們在方法里面實現(xiàn)列表頁的 URL 拼接,然后調(diào)用 scrape_page 方法爬取即可得到列表頁的 HTML 代碼了。
獲取了 HTML 代碼后,下一步就是解析列表頁,并得到每部電影的詳情頁的 URL 了,實現(xiàn)如下:
def parse_index(html):doc = pq(html)links = doc('.el-card .name')for link in links.items():href = link.attr('href')detail_url = urljoin(BASE_URL, href)logging.info('get detail url %s', detail_url)yield detail_url在這里我們定義了 parse_index 方法,它接收一個 html 參數(shù),即列表頁的 HTML 代碼。接著我們用 pyquery 新建一個 PyQuery 對象,完成之后再用 .el-card .name 選擇器選出來每個電影名稱對應(yīng)的超鏈接節(jié)點。我們遍歷這些節(jié)點,通過調(diào)用 attr 方法并傳入 href 獲得詳情頁的 URL 路徑,得到的 href 就是我們在上文所說的類似 /detail/1 這樣的結(jié)果。由于這并不是一個完整的 URL,所以我們需要借助 urljoin 方法把 BASE_URL 和 href 拼接起來,獲得詳情頁的完整 URL,得到的結(jié)果就是類似 https://static1.scrape.cuiqingcai.com/detail/1 這樣完整的 URL 了,最后 yield 返回即可。
這樣我們通過調(diào)用 parse_index 方法傳入列表頁的 HTML 代碼就可以獲得該列表頁所有電影的詳情頁 URL 了。
好,接下來我們把上面的方法串聯(lián)調(diào)用一下,實現(xiàn)如下:
def main():for page in range(1, TOTAL_PAGE + 1):index_html = scrape_index(page)detail_urls = parse_index(index_html)logging.info('detail urls %s', list(detail_urls))if __name__ == '__main__':main()這里我們定義了 main 方法來完成上面所有方法的調(diào)用,首先使用 range 方法遍歷一下頁碼,得到的 page 是 1~10,接著把 page 變量傳給 scrape_index 方法,得到列表頁的 HTML,賦值為 index_html 變量。接下來再將 index_html 變量傳給 parse_index 方法,得到列表頁所有電影的詳情頁 URL,賦值為 detail_urls,結(jié)果是一個生成器,我們調(diào)用 list 方法就可以將其輸出出來。
好,我們運行一下上面的代碼,結(jié)果如下:
2020-03-08 22:39:50,505 - INFO: scraping https://static1.scrape.cuiqingcai.com/page/1... 2020-03-08 22:39:51,949 - INFO: get detail url https://static1.scrape.cuiqingcai.com/detail/1 2020-03-08 22:39:51,950 - INFO: get detail url https://static1.scrape.cuiqingcai.com/detail/2 2020-03-08 22:39:51,950 - INFO: get detail url ...由于輸出內(nèi)容比較多,這里只貼了一部分。
可以看到,在這個過程中程序首先爬取了第 1 頁列表頁,然后得到了對應(yīng)詳情頁的每個 URL,接著再接著爬第 2 頁、第 3 頁,一直到第 10 頁,依次輸出了每一頁的詳情頁 URL。這樣,我們就成功獲取到所有電影詳情頁 URL 啦。
4.爬取詳情頁
現(xiàn)在我們已經(jīng)成功獲取所有詳情頁 URL 了,那么下一步當然就是解析詳情頁并提取出我們想要的信息了。
我們首先觀察一下詳情頁的 HTML 代碼吧,如圖所示:
經(jīng)過分析,我們想要提取的內(nèi)容和對應(yīng)的節(jié)點信息如下:
- 封面:是一個 img 節(jié)點,其 class 屬性為 cover。
- 名稱:是一個 h2 節(jié)點,其內(nèi)容便是名稱。
- 類別:是 span 節(jié)點,其內(nèi)容便是類別內(nèi)容,其外側(cè)是 button 節(jié)點,再外側(cè)則是 class 為 categories 的 div 節(jié)點。
- 上映時間:是 span 節(jié)點,其內(nèi)容包含了上映時間,其外側(cè)是包含了 class 為 info 的 div 節(jié)點。但注意這個 div 前面還有一個 class 為 info 的 div 節(jié)點,我們可以使用其內(nèi)容來區(qū)分,也可以使用 nth-child 或 nth-of-type 這樣的選擇器來區(qū)分。另外提取結(jié)果中還多了「上映」二字,我們可以用正則表達式把日期提取出來。
- 評分:是一個 p 節(jié)點,其內(nèi)容便是評分,p 節(jié)點的 class 屬性為 score。
- 劇情簡介:是一個 p 節(jié)點,其內(nèi)容便是劇情簡介,其外側(cè)是 class 為 drama 的 div 節(jié)點。
看上去有點復雜,但是不用擔心,有了 pyquery 和正則表達式,我們可以輕松搞定。
接著我們來實現(xiàn)一下代碼吧。
剛才我們已經(jīng)成功獲取了詳情頁的 URL,接下來我們要定義一個詳情頁的爬取方法,實現(xiàn)如下:
def scrape_detail(url):return scrape_page(url)這里定義了一個 scrape_detail 方法,它接收一個 url 參數(shù),并通過調(diào)用 scrape_page 方法獲得網(wǎng)頁源代碼。由于我們剛才已經(jīng)實現(xiàn)了 scrape_page 方法,所以在這里我們不用再寫一遍頁面爬取的邏輯了,直接調(diào)用即可,這就做到了代碼復用。
另外你可能會問,這個 scrape_detail 方法里面只調(diào)用了 scrape_page 方法,沒有別的功能,那爬取詳情頁直接用 scrape_page 方法不就好了,還有必要再單獨定義 scrape_detail 方法嗎?
答案是有必要,單獨定義一個 scrape_detail 方法在邏輯上會顯得更清晰,而且以后如果我們想要對 scrape_detail 方法進行改動,比如添加日志輸出或是增加預處理,都可以在 scrape_detail 里面實現(xiàn),而不用改動 scrape_page 方法,靈活性會更好。
好了,詳情頁的爬取方法已經(jīng)實現(xiàn)了,接著就是詳情頁的解析了,實現(xiàn)如下:
def parse_detail(html):doc = pq(html)cover = doc('img.cover').attr('src')name = doc('a > h2').text()categories = [item.text() for item in doc('.categories button span').items()]published_at = doc('.info:contains(上映)').text()published_at = re.search('(\d{4}-\d{2}-\d{2})', published_at).group(1) \if published_at and re.search('\d{4}-\d{2}-\d{2}', published_at) else Nonedrama = doc('.drama p').text()score = doc('p.score').text()score = float(score) if score else Nonereturn {'cover': cover,'name': name,'categories': categories,'published_at': published_at,'drama': drama,'score': score}這里我們定義了 parse_detail 方法用于解析詳情頁,它接收一個 html 參數(shù),解析其中的內(nèi)容,并以字典的形式返回結(jié)果。每個字段的解析情況如下所述:
- cover:封面,直接選取 class 為 cover 的 img 節(jié)點,并調(diào)用 attr 方法獲取 src 屬性的內(nèi)容即可。
- name:名稱,直接選取 a 節(jié)點的直接子節(jié)點 h2 節(jié)點,并調(diào)用 text 方法提取其文本內(nèi)容即可得到名稱。
- categories:類別,由于類別是多個,所以這里首先用 .categories button span 選取了 class 為 categories 的節(jié)點內(nèi)部的 span 節(jié)點,其結(jié)果是多個,所以這里進行了遍歷,取出了每個 span 節(jié)點的文本內(nèi)容,得到的便是列表形式的類別。
- published_at:上映時間,由于 pyquery 支持使用 :contains 直接指定包含的文本內(nèi)容并進行提取,且每個上映時間信息都包含了「上映」二字,所以我們這里就直接使用 :contains(上映) 提取了 class 為 info 的 div 節(jié)點。提取之后,得到的結(jié)果類似「1993-07-26 上映」這樣,但我們并不想要「上映」這兩個字,所以我們又調(diào)用了正則表達式把日期單獨提取出來了。當然這里也可以直接使用 strip 或 replace 方法把多余的文字去掉,但我們?yōu)榱司毩曊齽t表達式的用法,使用了正則表達式來提取。
- drama:直接提取 class 為 drama 的節(jié)點內(nèi)部的 p 節(jié)點的文本即可。
- score:直接提取 class 為 score 的 p 節(jié)點的文本即可,但由于提取結(jié)果是字符串,所以我們需要把它轉(zhuǎn)成浮點數(shù),即 float 類型。
上述字段提取完畢之后,構(gòu)造一個字典返回即可。
這樣,我們就成功完成了詳情頁的提取和分析了。
最后,我們將 main 方法稍微改寫一下,增加這兩個方法的調(diào)用,改寫如下:
def main():for page in range(1, TOTAL_PAGE + 1):index_html = scrape_index(page)detail_urls = parse_index(index_html)for detail_url in detail_urls:detail_html = scrape_detail(detail_url)data = parse_detail(detail_html)logging.info('get detail data %s', data)這里我們首先遍歷了 detail_urls,獲取了每個詳情頁的 URL,然后依次調(diào)用了 scrape_detail 和 parse_detail 方法,最后得到了每個詳情頁的提取結(jié)果,賦值為 data 并輸出。
運行結(jié)果如下:
2020-03-08 23:37:35,936 - INFO: scraping https://static1.scrape.cuiqingcai.com/page/1... 2020-03-08 23:37:36,833 - INFO: get detail url https://static1.scrape.cuiqingcai.com/detail/1 2020-03-08 23:37:36,833 - INFO: scraping https://static1.scrape.cuiqingcai.com/detail/1... 2020-03-08 23:37:39,985 - INFO: get detail data {'cover': 'https://p0.meituan.net/movie/ce4da3e03e655b5b88ed31b5cd7896cf62472.jpg@464w_644h_1e_1c', 'name': '霸王別姬 - Farewell My Concubine', 'categories': ['劇情', '愛情'], 'published_at': '1993-07-26', 'drama': '影片借一出《霸王別姬》的京戲,牽扯出三個人之間一段隨時代風云變幻的愛恨情仇。段小樓(張豐毅 飾)與程蝶衣(張國榮 飾)是一對打小一起長大的師兄弟,兩人一個演生,一個飾旦,一向配合天衣無縫,尤其一出《霸王別姬》,更是譽滿京城,為此,兩人約定合演一輩子《霸王別姬》。但兩人對戲劇與人生關(guān)系的理解有本質(zhì)不同,段小樓深知戲非人生,程蝶衣則是人戲不分。段小樓在認為該成家立業(yè)之時迎娶了名妓菊仙(鞏俐 飾),致使程蝶衣認定菊仙是可恥的第三者,使段小樓做了叛徒,自此,三人圍繞一出《霸王別姬》生出的愛恨情仇戰(zhàn)開始隨著時代風云的變遷不斷升級,終釀成悲劇。', 'score': 9.5} 2020-03-08 23:37:39,985 - INFO: get detail url https://static1.scrape.cuiqingcai.com/detail/2 2020-03-08 23:37:39,985 - INFO: scraping https://static1.scrape.cuiqingcai.com/detail/2... 2020-03-08 23:37:41,061 - INFO: get detail data {'cover': 'https://p1.meituan.net/movie/6bea9af4524dfbd0b668eaa7e187c3df767253.jpg@464w_644h_1e_1c', 'name': '這個殺手不太冷 - Léon', 'categories': ['劇情', '動作', '犯罪'], 'published_at': '1994-09-14', 'drama': '里昂(讓·雷諾 飾)是名孤獨的職業(yè)殺手,受人雇傭。一天,鄰居家小姑娘馬蒂爾德(納塔麗·波特曼 飾)敲開他的房門,要求在他那里暫避殺身之禍。原來鄰居家的主人是警方緝毒組的眼線,只因貪污了一小包毒品而遭惡警(加里·奧德曼 飾)殺害全家的懲罰。馬蒂爾德 得到里昂的留救,幸免于難,并留在里昂那里。里昂教小女孩使槍,她教里昂法文,兩人關(guān)系日趨親密,相處融洽。 女孩想著去報仇,反倒被抓,里昂及時趕到,將女孩救回。混雜著哀怨情仇的正邪之戰(zhàn)漸次升級,更大的沖突在所難免……', 'score': 9.5} 2020-03-08 23:37:41,062 - INFO: get detail url https://static1.scrape.cuiqingcai.com/detail/3 ...由于內(nèi)容較多,這里省略了后續(xù)內(nèi)容。
可以看到,我們已經(jīng)成功提取出每部電影的基本信息,包括封面、名稱、類別,等等。
5.保存到 MongoDB
成功提取到詳情頁信息之后,下一步我們就要把數(shù)據(jù)保存起來了。在上一課時我們學習了 MongoDB 的相關(guān)操作,接下來我們就把數(shù)據(jù)保存到 MongoDB 吧。
在這之前,請確保現(xiàn)在有一個可以正常連接和使用的 MongoDB 數(shù)據(jù)庫。
將數(shù)據(jù)導入 MongoDB 需要用到 PyMongo 這個庫,這個在最開始已經(jīng)引入過了。那么接下來我們定義一下 MongoDB 的連接配置,實現(xiàn)如下:
MONGO_CONNECTION_STRING = 'mongodb://localhost:27017' MONGO_DB_NAME = 'movies' MONGO_COLLECTION_NAME = 'movies'client = pymongo.MongoClient(MONGO_CONNECTION_STRING) db = client['movies'] collection = db['movies']在這里我們聲明了幾個變量,介紹如下:
- MONGO_CONNECTION_STRING:MongoDB 的連接字符串,里面定義了 MongoDB 的基本連接信息,如 host、port,還可以定義用戶名密碼等內(nèi)容。
- MONGO_DB_NAME:MongoDB 數(shù)據(jù)庫的名稱。
- MONGO_COLLECTION_NAME:MongoDB 的集合名稱。
這里我們用 MongoClient 聲明了一個連接對象,然后依次聲明了存儲的數(shù)據(jù)庫和集合。
接下來,我們再實現(xiàn)一個將數(shù)據(jù)保存到 MongoDB 的方法,實現(xiàn)如下:
def save_data(data):collection.update_one({'name': data.get('name')}, {'$set': data}, upsert=True)在這里我們聲明了一個 save_data 方法,它接收一個 data 參數(shù),也就是我們剛才提取的電影詳情信息。
在方法里面,我們調(diào)用了 update_one 方法,
- 第 1 個參數(shù)是查詢條件,即根據(jù) name 進行查詢;
- 第 2 個參數(shù)是 data 對象本身,也就是所有的數(shù)據(jù),這里我們用 $set 操作符表示更新操作;
- 第 3 個參數(shù)很關(guān)鍵,這里實際上是 upsert 參數(shù),如果把這個設(shè)置為 True,則可以做到存在即更新,不存在即插入的功能,更新會根據(jù)第一個參數(shù)設(shè)置的 name 字段,所以這樣可以防止數(shù)據(jù)庫中出現(xiàn)同名的電影數(shù)據(jù)。
注:實際上電影可能有同名,但該場景下的爬取數(shù)據(jù)沒有同名情況,當然這里更重要的是實現(xiàn) MongoDB 的去重操作。
好的,那么接下來我們將 main 方法稍微改寫一下就好了,改寫如下:
def main():for page in range(1, TOTAL_PAGE + 1):index_html = scrape_index(page)detail_urls = parse_index(index_html)for detail_url in detail_urls:detail_html = scrape_detail(detail_url)data = parse_detail(detail_html)logging.info('get detail data %s', data)logging.info('saving data to mongodb')save_data(data)logging.info('data saved successfully')重新運行,我們看下輸出結(jié)果:
2020-03-09 01:10:27,094 - INFO: scraping https://static1.scrape.cuiqingcai.com/page/1... 2020-03-09 01:10:28,019 - INFO: get detail url https://static1.scrape.cuiqingcai.com/detail/1 2020-03-09 01:10:28,019 - INFO: scraping https://static1.scrape.cuiqingcai.com/detail/1... 2020-03-09 01:10:29,183 - INFO: get detail data {'cover': 'https://p0.meituan.net/movie/ce4da3e03e655b5b88ed31b5cd7896cf62472.jpg@464w_644h_1e_1c', 'name': '霸王別姬 - Farewell My Concubine', 'categories': ['劇情', '愛情'], 'published_at': '1993-07-26', 'drama': '影片借一出《霸王別姬》的京戲,牽扯出三個人之間一段隨時代風云變幻的愛恨情仇。段小樓(張豐毅 飾)與程蝶衣(張國榮 飾)是一對打小一起長大的師兄弟,兩人一個演生,一個飾旦,一向配合天衣無縫,尤其一出《霸王別姬》,更是譽滿京城,為此,兩人約定合演一輩子《霸王別姬》。但兩人對戲劇與人生關(guān)系的理解有本質(zhì)不同,段小樓深知戲非人生,程蝶衣則是人戲不分。段小樓在認為該成家立業(yè)之時迎娶了名妓菊仙(鞏俐 飾),致使程蝶衣認定菊仙是可恥的第三者,使段小樓做了叛徒,自此,三人圍繞一出《霸王別姬》生出的愛恨情仇戰(zhàn)開始隨著時代風云的變遷不斷升級,終釀成悲劇。', 'score': 9.5} 2020-03-09 01:10:29,183 - INFO: saving data to mongodb 2020-03-09 01:10:29,288 - INFO: data saved successfully 2020-03-09 01:10:29,288 - INFO: get detail url https://static1.scrape.cuiqingcai.com/detail/2 2020-03-09 01:10:29,288 - INFO: scraping https://static1.scrape.cuiqingcai.com/detail/2... 2020-03-09 01:10:30,250 - INFO: get detail data {'cover': 'https://p1.meituan.net/movie/6bea9af4524dfbd0b668eaa7e187c3df767253.jpg@464w_644h_1e_1c', 'name': '這個殺手不太冷 - Léon', 'categories': ['劇情', '動作', '犯罪'], 'published_at': '1994-09-14', 'drama': '里昂(讓·雷諾 飾)是名孤獨的職業(yè)殺手,受人雇傭。一天,鄰居家小姑娘馬蒂爾德(納塔麗·波特曼 飾)敲開他的房門,要求在他那里暫避殺身之禍。原來鄰居家的主人是警方緝毒組的眼線,只因貪污了一小包毒品而遭惡警(加里·奧德曼 飾)殺害全家的懲罰。馬蒂爾德 得到里昂的留救,幸免于難,并留在里昂那里。里昂教小女孩使槍,她教里昂法文,兩人關(guān)系日趨親密,相處融洽。 女孩想著去報仇,反倒被抓,里昂及時趕到,將女孩救回。混雜著哀怨情仇的正邪之戰(zhàn)漸次升級,更大的沖突在所難免……', 'score': 9.5} 2020-03-09 01:10:30,250 - INFO: saving data to mongodb 2020-03-09 01:10:30,253 - INFO: data saved successfully ...在運行結(jié)果中我們可以發(fā)現(xiàn),這里輸出了存儲 MongoDB 成功的信息。
運行完畢之后我們可以使用 MongoDB 客戶端工具(例如 Robo 3T )可視化地查看已經(jīng)爬取到的數(shù)據(jù),結(jié)果如下:
這樣,所有的電影就被我們成功爬取下來啦!不多不少,正好 100 條。
6.多進程加速
由于整個的爬取是單進程的,而且只能逐條爬取,速度稍微有點慢,有沒有方法來對整個爬取過程進行加速呢?
在前面我們講了多進程的基本原理和使用方法,下面我們就來實踐一下多進程的爬取吧。
由于一共有 10 頁詳情頁,并且這 10 頁內(nèi)容是互不干擾的,所以我們可以一頁開一個進程來爬取。由于這 10 個列表頁頁碼正好可以提前構(gòu)造成一個列表,所以我們可以選用多進程里面的進程池 Pool 來實現(xiàn)這個過程。
這里我們需要改寫下 main 方法的調(diào)用,實現(xiàn)如下:
import multiprocessingdef main(page):index_html = scrape_index(page)detail_urls = parse_index(index_html)for detail_url in detail_urls:detail_html = scrape_detail(detail_url)data = parse_detail(detail_html)logging.info('get detail data %s', data)logging.info('saving data to mongodb')save_data(data)logging.info('data saved successfully')if __name__ == '__main__':pool = multiprocessing.Pool()pages = range(1, TOTAL_PAGE + 1)pool.map(main, pages)pool.close()pool.join()這里我們首先給 main 方法添加一個參數(shù) page,用以表示列表頁的頁碼。接著我們聲明了一個進程池,并聲明 pages 為所有需要遍歷的頁碼,即 1~10。最后調(diào)用 map 方法,第 1 個參數(shù)就是需要被調(diào)用的方法,第 2 個參數(shù)就是 pages,即需要遍歷的頁碼。
這樣 pages 就會被依次遍歷。把 1~10 這 10 個頁碼分別傳遞給 main 方法,并把每次的調(diào)用變成一個進程,加入到進程池中執(zhí)行,進程池會根據(jù)當前運行環(huán)境來決定運行多少進程。比如我的機器的 CPU 有 8 個核,那么進程池的大小會默認設(shè)定為 8,這樣就會同時有 8 個進程并行執(zhí)行。
運行輸出結(jié)果和之前類似,但是可以明顯看到加了多進程執(zhí)行之后,爬取速度快了非常多。我們可以清空一下之前的 MongoDB 數(shù)據(jù),可以發(fā)現(xiàn)數(shù)據(jù)依然可以被正常保存到 MongoDB 數(shù)據(jù)庫中。
7.總結(jié)
到現(xiàn)在為止,我們就完成了全站電影數(shù)據(jù)的爬取并實現(xiàn)了存儲和優(yōu)化。
這節(jié)課我們用到的庫有 requests、pyquery、PyMongo、multiprocessing、re、logging 等,通過這個案例實戰(zhàn),我們把前面學習到的知識都串聯(lián)了起來,其中的一些實現(xiàn)方法可以好好思考和體會,也希望這個案例能夠讓你對爬蟲的實現(xiàn)有更實際的了解。
本節(jié)代碼:https://github.com/Python3WebSpider/ScrapeStatic1。
總結(jié)
以上是生活随笔為你收集整理的第11讲:Reqeusts + PyQuery + PyMongo 基本案例实战的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 第19讲:Pyppeteer 爬取实战
- 下一篇: 第09讲:爬虫解析利器 PyQuery