最近在看爬蟲方面的知識,看到崔慶才所著的《Python3網絡爬蟲開發實戰》一書講的比較系統,果斷入手學習。下面根據書中的內容,簡單總結一下爬蟲的基礎知識,並且實際練習一下。詳細內容請見:https://cuiqingcai.com/5465.html(作者已把書的前幾章內容對外公開)。
在寫爬蟲程序之前需要了解的一些知識:
爬蟲基礎:我們平時訪問網頁就是對服務器發送請求(Request),然后得到響應(Response)的一個過程。爬蟲通過模仿瀏覽器,對網頁進行自動訪問。需要知道請求包含哪些內容,請求的方式有哪些,響應包含哪些內容。
網頁結構:網頁由HTML,CSS,JaveScript組成。需要知道其各自的作用是什么,還需要知道到哪個節點去獲取自己想要的信息。
其他:了解會話(Session),Cookie,代理(Proxy)的作用。
爬蟲流程:
- 爬取網頁(獲取網頁源代碼):可使用的庫有urllib,requests等;當然,現在很多網頁都是動態加載的,對於這些網頁,還需使用Selenium等庫
- 解析網頁(提取網頁中我們需要的信息):定位信息的方式有:正則表達式,XPath選擇器,CSS選擇器;可使用的庫有re,lxml,Beautiful Soup等
- 保存結果(將結果保存至文件或數據庫):文件有txt,json, csv等格式;數據庫可選擇MySQL,MongoDB等
在python中爬取網頁,我們一般用requests庫。下面是經常用到的一些語法:
導入requests庫: import requests
獲取響應: response=requests.get(url, headers)
獲取響應體: response.text
下面讓我們來實際操練一下:
實例目標:用requests庫爬取貓眼電影網上top100的電影(排名,圖片,電影名稱,上映時間,評分),用正則表達式進行解析,然后將結果保存至txt文件
實例網址:https://maoyan.com/board/4
首先,導入requests庫和re,json模塊:
import requests import re import json
其次,先定義一個爬取一個網頁的方法:
def get_one_page(url): headers={'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) \ AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36'} response=requests.get(url,headers=headers) if response.status_code==200: return response.text return None
這樣,在main()方法里,我們設定好url,就可以把該網頁源代碼打印出來:
def main(): url="https://maoyan.com/board/4" html=get_one_page(url) print(html)
接下來,我們來仔細查看這個源代碼,看看怎樣用正則表達式把我們需要的信息提取出來。首先用瀏覽器打開這個網頁,然后在瀏覽器里面選擇開發者工具,在Network里查看網頁源代碼。下面截取一部分:
<div class="content"> <div class="wrapper"> <div class="main"> <p class="update-time">2018-12-30<span class="has-fresh-text">已更新</span></p> <p class="board-content">榜單規則:將貓眼電影庫中的經典影片,按照評分和評分人數從高到低綜合排序取前100名,每天上午10點更新。相關數據來源於“貓眼電影庫”。</p> <dl class="board-wrapper"> <dd> <i class="board-index board-index-1">1</i> <a href="/films/1203" title="霸王別姬" class="image-link" data-act="boarditem-click" data-val="{movieId:1203}"> <img src="//ms0.meituan.net/mywww/image/loading_2.e3d934bf.png" alt="" class="poster-default" /> <img data-src="https://p1.meituan.net/movie/20803f59291c47e1e116c11963ce019e68711.jpg@160w_220h_1e_1c" alt="霸王別姬" class="board-img" /> </a> <div class="board-item-main"> <div class="board-item-content"> <div class="movie-item-info"> <p class="name"><a href="/films/1203" title="霸王別姬" data-act="boarditem-click" data-val="{movieId:1203}">霸王別姬</a></p> <p class="star"> 主演:張國榮,張豐毅,鞏俐 </p> <p class="releasetime">上映時間:1993-01-01</p> </div> <div class="movie-item-number score-num"> <p class="score"><i class="integer">9.</i><i class="fraction">6</i></p>
可以看到,電影的排名在一個dd節點下面:
<dd> <i class="board-index board-index-1">1</i>
因此,相應的正則表達式可以寫為:<dd>.*?board-index.*?>(.*?)</i>
接下來,我們發現圖片在一個a節點下面,但是有兩張圖片。經過檢查,第二個img節點下的data-src屬性是圖片的鏈接:
<img data-src="https://p1.meituan.net/movie/20803f59291c47e1e116c11963ce019e68711.jpg@160w_220h_1e_1c" alt="霸王別姬" class="board-img" />
因此,相應的正則表達式可以寫為:.*?data-src="(.*?)" (注:因為這個會接在之前的正則表達式之后,因此最前面寫上.*?即可。下同。)
再接下來,電影的名稱,在一個p節點下面,class為"name":
<p class="name"><a href="/films/1203" title="霸王別姬" data-act="boarditem-click" data-val="{movieId:1203}">霸王別姬</a></p>
相應的正則表達式可以寫為:.*?name.*?a.*?>(.*?)</a>
上映時間,在一個p節點下面,class為"releasetime":
<p class="releasetime">上映時間:1993-01-01</p>
相應的正則表達式可以寫為:.*?releasetime.*?>(.*?)</p>
評分,在一個p節點下面,class為"score":
<p class="score"><i class="integer">9.</i><i class="fraction">6</i></p>
相應的正則表達式可以寫為:.*?score.*?integer">(.*?)</i>.*?fraction">(.*?)</i>.*?</dd> (注:最后用dd節點收尾)
把這些正則表達式連接起來,然后就可以用findall()方法查找出所有符合條件的內容。完整的正則表達式如下:
<dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)".*?name.*?a.*?>(.*?)</a>.*?releasetime.*?>(.*?)</p>.*?score.*?integer">(.*?)</i>.*?fraction">(.*?)</i>.*?</dd>
下面,我們再定義一個解析網頁的方法:
def parse_one_page(html): pattern=re.compile('<dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)".*?name.*?a.*?>(.*?)</a>.*?releasetime.*?>(.*?)</p>.*?score.*?integer">(.*?)</i>.*?fraction">(.*?)</i>.*?</dd>', re.S) result=re.findall(pattern, html) return result
這里需要注意,在定義正則表達式的pattern時,必須加上re.S修飾符(匹配包括換行符在內的所有字符),否則碰到換行就無法進行匹配。
輸出的匹配結果如下:
[('1', 'https://p1.meituan.net/movie/20803f59291c47e1e116c11963ce019e68711.jpg@160w_220h_1e_1c', '霸王別姬', '上映時間:1993-01-01', '9.', '6'), ('2', 'https://p0.meituan.net/movie/283292171619cdfd5b240c8fd093f1eb255670.jpg@160w_220h_1e_1c', '肖申克的救贖', '上映時間:1994-10-14(美國)', '9.', '5'), ('3', 'https://p0.meituan.net/movie/54617769d96807e4d81804284ffe2a27239007.jpg@160w_220h_1e_1c', '羅馬假日', '上映時間:1953-09-02(美國)', '9.', '1'), ('4', 'https://p0.meituan.net/movie/e55ec5d18ccc83ba7db68caae54f165f95924.jpg@160w_220h_1e_1c', '這個殺手不太冷', '上映時間:1994-09-14(法國)', '9.', '5'), ('5', 'https://p1.meituan.net/movie/f5a924f362f050881f2b8f82e852747c118515.jpg@160w_220h_1e_1c', '教父', '上映時間:1972-03-24(美國)', '9.', '3'), ('6', 'https://p1.meituan.net/movie/0699ac97c82cf01638aa5023562d6134351277.jpg@160w_220h_1e_1c', '泰坦尼克號', '上映時間:1998-04-03', '9.', '5'), ('7', 'https://p0.meituan.net/movie/da64660f82b98cdc1b8a3804e69609e041108.jpg@160w_220h_1e_1c', '唐伯虎點秋香', '上映時間:1993-07-01(中國香港)', '9.', '2'), ('8', 'https://p0.meituan.net/movie/b076ce63e9860ecf1ee9839badee5228329384.jpg@160w_220h_1e_1c', '千與千尋', '上映時間:2001-07-20(日本)', '9.', '3'), ('9', 'https://p0.meituan.net/movie/46c29a8b8d8424bdda7715e6fd779c66235684.jpg@160w_220h_1e_1c', '魂斷藍橋', '上映時間:1940-05-17(美國)', '9.', '2'), ('10', 'https://p0.meituan.net/movie/230e71d398e0c54730d58dc4bb6e4cca51662.jpg@160w_220h_1e_1c', '亂世佳人', '上映時間:1939-12-15(美國)', '9.', '1')]
可以看出,上述的格式還是有些雜亂,讓我們修改一下解析網頁的方法,使其變為整齊的結構化數據:
def parse_one_page(html): pattern=re.compile('<dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)".*?name.*?a.*?>(.*?)</a>.*?releasetime.*?>(.*?)</p>.*?score.*?integer">(.*?)</i>.*?fraction">(.*?)</i>.*?</dd>', re.S) result=re.findall(pattern, html) for item in result: yield {"index": item[0], "movie_name": item[2],\ "pic": item[1], "release": item[3],\ "score": item[4]+item[5]}
現在匹配結果變成了字典格式:
{'index': '1', 'movie_name': '霸王別姬', 'pic': 'https://p1.meituan.net/movie/20803f59291c47e1e116c11963ce019e68711.jpg@160w_220h_1e_1c', 'release': '上映時間:1993-01-01', 'score': '9.6'} {'index': '2', 'movie_name': '肖申克的救贖', 'pic': 'https://p0.meituan.net/movie/283292171619cdfd5b240c8fd093f1eb255670.jpg@160w_220h_1e_1c', 'release': '上映時間:1994-10-14(美國)', 'score': '9.5'} {'index': '3', 'movie_name': '羅馬假日', 'pic': 'https://p0.meituan.net/movie/54617769d96807e4d81804284ffe2a27239007.jpg@160w_220h_1e_1c', 'release': '上映時間:1953-09-02(美國)', 'score': '9.1'} {'index': '4', 'movie_name': '這個殺手不太冷', 'pic': 'https://p0.meituan.net/movie/e55ec5d18ccc83ba7db68caae54f165f95924.jpg@160w_220h_1e_1c', 'release': '上映時間:1994-09-14(法國)', 'score': '9.5'} {'index': '5', 'movie_name': '教父', 'pic': 'https://p1.meituan.net/movie/f5a924f362f050881f2b8f82e852747c118515.jpg@160w_220h_1e_1c', 'release': '上映時間:1972-03-24(美國)', 'score': '9.3'} {'index': '6', 'movie_name': '泰坦尼克號', 'pic': 'https://p1.meituan.net/movie/0699ac97c82cf01638aa5023562d6134351277.jpg@160w_220h_1e_1c', 'release': '上映時間:1998-04-03', 'score': '9.5'} {'index': '7', 'movie_name': '唐伯虎點秋香', 'pic': 'https://p0.meituan.net/movie/da64660f82b98cdc1b8a3804e69609e041108.jpg@160w_220h_1e_1c', 'release': '上映時間:1993-07-01(中國香港)', 'score': '9.2'} {'index': '8', 'movie_name': '千與千尋', 'pic': 'https://p0.meituan.net/movie/b076ce63e9860ecf1ee9839badee5228329384.jpg@160w_220h_1e_1c', 'release': '上映時間:2001-07-20(日本)', 'score': '9.3'} {'index': '9', 'movie_name': '魂斷藍橋', 'pic': 'https://p0.meituan.net/movie/46c29a8b8d8424bdda7715e6fd779c66235684.jpg@160w_220h_1e_1c', 'release': '上映時間:1940-05-17(美國)', 'score': '9.2'} {'index': '10', 'movie_name': '亂世佳人', 'pic': 'https://p0.meituan.net/movie/230e71d398e0c54730d58dc4bb6e4cca51662.jpg@160w_220h_1e_1c', 'release': '上映時間:1939-12-15(美國)', 'score': '9.1'}
接下來要將結果寫入txt文件,這里定義一個寫入文件的方法:
def write_to_file(result): with open ("result.txt","a") as f: f.write(json.dumps(result, ensure_ascii=False)+'\n')
然后在main方法里將結果逐行寫入文件:
def main(): url="https://maoyan.com/board/4" html=get_one_page(url) result=parse_one_page(html) for i in result: write_to_file(i)
這里有幾個需要注意的地方:1,由於需要將結果逐行寫入,因此文件用"a"方式打開,a也就是append。
2,由於需要將結果逐行寫入,因此將結果寫入文件時最后加上換行符"\n"。
3,由於結果是字典格式,無法直接寫入文件,需要先用json.dumps方法把字典轉為字符串,但是這樣會導致中文亂碼。根據json.dumps方法的注釋,如果將ensure_ascii設為false,那么寫入的字符串可以包含非ASCII字符,否則,所有這些字符都會在JSON字符串中轉義。也就是說將參數ensure_ascii設為False可以使中文(UTF-8編碼)不經過轉義,也就不會亂碼。
至此,第一頁網頁就已經全部爬取成功了。但是一共有10頁這樣的網頁,我們打開第二個網頁和第三個網頁看一下。可以發現,第二個網頁的url變為:https://maoyan.com/board/4?offset=10,第三頁網頁的url則是:https://maoyan.com/board/4?offset=20。可以發現規律就是多了一個offset參數,那么我們把1~10頁的網頁爬取url設置從offset為0,一直到offset為90,就可以爬取所有網頁了。
由於我們在main方法里設定了爬取url,因此我們給main方法增加一個輸入參數,也就是offset偏移值,這樣,我們就能爬取我們想要的網頁了。最后,再增添一個循環語句,用於爬取各種offset的網頁,這樣,一個簡單的爬蟲程序就完成了。
我們再把代碼重新整合一下,並且由於現在貓眼多了反爬蟲,如果爬取速度過快,會沒有響應,因此,需要加上一個延時。
完整代碼如下:
import requests import re import json import time def get_one_page(url): try: headers={'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) \ AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36'} response=requests.get(url, headers=headers) if response.status_code==200: return response.text return None except requests.RequestException: print("Fail") def parse_one_page(html): pattern=re.compile('<dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)".*?name.*?a.*?>(.*?)</a>.*?releasetime.*?>(.*?)</p>.*?score.*?integer">(.*?)</i>.*?fraction">(.*?)</i>.*?</dd>', re.S) result=re.findall(pattern, html) for item in result: yield {"index": item[0], "movie_name": item[2],\ "pic": item[1], "release": item[3],\ "score": item[4]+item[5]} def write_to_file(result): with open ("result.txt","a") as f: f.write(json.dumps(result, ensure_ascii=False)+'\n') def main(offset): url="https://maoyan.com/board/4?offset={}".format(offset) html=get_one_page(url) result=parse_one_page(html) for i in result: write_to_file(i) if __name__=='__main__': for i in range(10): main(offset=i*10) time.sleep(1)