目標:使用Python爬取豆瓣電影並保存MongoDB數據庫中
我們先來看一下通過瀏覽器的方式來篩選某些特定的電影:
我們把URL來復制出來分析分析:
https://movie.douban.com/tag/#/?sort=T&range=0,10&tags=%E7%94%B5%E5%BD%B1,%E7%88%B1%E6%83%85,%E7%BE%8E%E5%9B%BD,%E9%BB%91%E5%B8%AE
有3個字段是非常重要的:
1.sort=T
2.range=0,10
3.tags=%E7%94%B5%E5%BD%B1,%E7%88%B1%E6%83%85,%E7%BE%8E%E5%9B%BD,%E9%BB%91%E5%B8%AE
具體分析方法如下:
1.sort:表示排序方式,可以看到它有3中排序方式
根據上圖可以直到每個字母表示的含義:
T:熱度排序,
R:時間排序,
S:評價排序:
2.range=0,10;表示一個范圍,具體是什么范圍呢?
range參數我們也搞定了,它就是表示評分區間!
默認評分區間是:0-10
3.tags:同樣的原理,這是一個標簽
我們選中的標簽有:電影,愛情,美國,黑幫4個標簽,但是在tags里面我們看到的不是這寫漢字,而是被編碼過的形式!
%E7%94%B5%E5%BD%B1,%E7%88%B1%E6%83%85,%E7%BE%8E%E5%9B%BD,%E9%BB%91%E5%B8%AE)
那如何知道這些字符是表示什么呢?
我們可以到網上進行解碼看看正不正確?
4.那么還有沒有可以選擇的參數呢?
我們還有2個參數可以選擇!
playbale=1:表示可播放
unwatched=1:表示還沒看過的
至此,我們就已經把URL中的查詢參數全都弄明白了!
但是,又有一個問題了,當我們在瀏覽器中點擊"加載更多"按鈕時,這個地址欄中的URL並沒有發生變化,但是電影信息可以加載出來了!這是為什么?
如果知道AJAX加載技術的讀者可能知道這個原理,實際上就是異步加載,服務器不需要刷新整個網頁,只需要刷新局部網頁就可以把數據展示到網頁中,這樣不僅可以加快速度,也可以減少服務器的壓力.
重點來了:
抓包結果:
看看瀏覽器地址欄的URL與Request URL有什么不一樣的地方?
我們在瀏覽器地址欄中看到的URL是:
https://movie.douban.com/tag/#/?
sort=S&range=5,10&tags=%E7%94%B5%E5%BD%B1,%E7%88%B1%E6%83%85,%E7%BE%8E%E5%9B%BD&playable=1&unwatched=1
實際瀏覽器發送的Request URL是:
https://movie.douban.com/j/new_search_subjects?
sort=S&range=5,10&tags=%E7%94%B5%E5%BD%B1,%E7%88%B1%E6%83%85,%E7%BE%8E%E5%9B%BD&playable=1&unwatched=1&start=0
除了被紅色標記的地方不同之外,其他地方都是一樣的!那我們發送請求的時候應該是用哪一個URL呢?
在上面我就已提到了,在豆瓣電影中,是采用異步加載的方式來加載數據的,也就是說在加載數據的過程中,地址欄中的URL是一直保持不變的,那我們還能用這個URL來發送請求嗎?當然不能了!
既然不能用地址欄中的URL來發送請求,那我們就來分析一下瀏覽器實際發送的Request URL:
我們把這個URL復制到瀏覽器中看看會發生什么情況!
我們可以看到這個URL的響應結果恰恰就是我們想要的數據,采用json格式.在Python中,我們可以利用一些工具把它轉換成字典格式,來提取我們想要的數據.
距離我們成功還有一小步:
在這個URL中,我們看到還有一個參數:start,這個是干嘛的呢?
這個數值表示偏移量,來控制每一次加載的偏移位置是在哪里!比如我們把它設置成20,表示一次請求的電影數量.那么得到的結果如下:
到這里,該案例的思路,難點就已經全都捋清楚了,剩下的就是代碼的事情了!
項目結構:
完整的代碼如下:
settings.py
1 User_Agents =[ 2 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50', 3 'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50', 4 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0', 5 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0.1) Gecko/20100101 Firefox/4.0.1', 6 'Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; en) Presto/2.8.131 Version/11.11', 7 'Opera/9.80 (Windows NT 6.1; U; en) Presto/2.8.131 Version/11.11', 8 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11', 9 ]
mongoHelper.py
1 import pymongo 2 3 4 class MongoDBHelper: 5 """數據庫操作""" 6 7 def __init__(self, collection_name=None): 8 # 啟動mongo 9 self._client = pymongo.MongoClient('localhost', 27017) 10 # 使用test數據庫 11 self._test = self._client['test'] 12 # 創建指定的集合 13 self._name = self._test[collection_name] 14 15 def insert_item(self, item): 16 """插入數據""" 17 self._name.insert_one(item) 18 19 def find_item(self): 20 """查詢數據""" 21 data = self._name.find() 22 return data 23 24 25 def main(): 26 mongo = MongoDBHelper('collection') 27 mongo.insert_item({'a': 1}) 28 29 30 if __name__ == '__main__': 31 main()
douban.py
1 import logging 2 import random 3 import string 4 import requests 5 import time 6 from collections import deque 7 from urllib import parse 8 9 from settings import User_Agents 10 from MongDBHelper import MongoDBHelper 11 12 13 class DoubanSpider(object): 14 """豆瓣爬蟲""" 15 def __init__(self): 16 # 基本的URL 17 self.base_url = 'https://movie.douban.com/j/new_search_subjects?' 18 self.full_url = self.base_url + '{query_params}' 19 # 從User-Agents中選擇一個User-Agent 20 self.headers = {'User-Agent': random.choice(User_Agents)} 21 # 影視形式(電影, 電視劇,綜藝) 22 self.form_tag = None # 類型 23 self.type_tag = None # 地區 24 self.countries_tag = None # 特色 25 self.genres_tag = None 26 self.sort = 'T' # 排序方式,默認是T,表示熱度 27 self.range = 0, 10 # 評分范圍 28 self.playable = '' 29 self.unwatched = '' 30 # 連接數據庫,集合名為douban_movies 31 self.db = MongoDBHelper('douban_movies') 32 33 def get_query_parameter(self): 34 """獲取用戶輸入信息""" 35 # 獲取tags參數 36 self.form_tag = input('請輸入你想看的影視形式(電影|電視劇|綜藝...):') 37 self.type_tag = input('請輸入你想看的影視類型(劇情|愛情|喜劇|科幻...):') 38 self.countries_tag = input('請輸入你想看的影視地區(大陸|美國|香港...):') 39 self.genres_tag = input('請輸入你想看的影視特色(經典|冷門佳片|黑幫...):') 40 41 def get_default_query_parameter(self): 42 """獲取默認的查詢參數""" 43 # 獲取 sort, range, playable, unwatched參數 44 self.range = input('請輸入評分范圍[0-10]:') 45 self.sort = input('請輸入排序順序(熱度:T, 時間:R, 評價:S),三選一:').upper() 46 self.playable = input('請選擇是否可播放(默認不可播放):') 47 self.unwatched = input('請選擇是否為我沒看過(默認是沒看過):') 48 49 def encode_query_data(self): 50 """對輸入信息進行編碼處理""" 51 if not (self.form_tag and self.type_tag and self.countries_tag and self.genres_tag): 52 all_tags = '' 53 else: 54 all_tags = [self.form_tag, self.type_tag, self.countries_tag, self.genres_tag] 55 query_param = { 56 'sort': self.sort, 57 'range': self.range, 58 'tags': all_tags, 59 'playable': self.playable, 60 'unwatched': self.unwatched, 61 } 62 63 # string.printable:表示ASCII字符就不用編碼了 64 query_params = parse.urlencode(query_param, safe=string.printable) 65 # 去除查詢參數中無效的字符 66 invalid_chars = ['(', ')', '[', ']', '+', '\''] 67 for char in invalid_chars: 68 if char in query_params: 69 query_params = query_params.replace(char, '') 70 # 把查詢參數和base_url組合起來形成完整的url 71 self.full_url = self.full_url.format(query_params=query_params) + '&start={start}' 72 73 def download_movies(self, offset): 74 """下載電影信息 75 :param offset: 控制一次請求的影視數量 76 :return resp:請求得到的響應體""" 77 full_url = self.full_url.format(start=offset) 78 resp = None 79 try: 80 resp = requests.get(full_url, headers=self.headers) 81 except Exception as e: 82 # print(resp) 83 logging.error(e) 84 return resp 85 86 def get_movies(self, resp): 87 """獲取電影信息 88 :param resp: 響應體 89 :return movies:爬取到的電影信息""" 90 if resp: 91 if resp.status_code == 200: 92 # 獲取響應文件中的電影數據 93 movies = dict(resp.json()).get('data') 94 if movies: 95 # 獲取到電影了, 96 print(movies) 97 return movies 98 else: 99 # 響應結果中沒有電影了! 100 # print('已超出范圍!') 101 return None 102 else: 103 # 沒有獲取到電影信息 104 return None 105 106 def save_movies(self, movies, id): 107 """把請求到的電影保存到數據庫中 108 :param movies:提取到的電影信息 109 :param id: 記錄每部電影 110 """ 111 if not movies: 112 print('save_movies() error: movies為None!!!') 113 return 114 115 all_movies = self.find_movies() 116 if len(all_movies) == 0: 117 # 數據庫中還沒有數據, 118 for movie in movies: 119 id += 1 120 movie['_id'] = id 121 self.db.insert_item(movie) 122 else: 123 # 保存已經存在數據庫中的電影標題 124 titles = [] 125 for existed_movie in all_movies: 126 # 獲取數據庫中的電影標題 127 titles.append(existed_movie.get('title')) 128 129 for movie in movies: 130 # 判斷數據庫中是否已經存在該電影了 131 if movie.get('title') not in titles: 132 id += 1 133 movie['_id'] = id 134 # 如果不存在,那就進行插入操作 135 self.db.insert_item(movie) 136 else: 137 print('save_movies():該電影"{}"已經在數據庫了!!!'.format(movie.get('title'))) 138 139 def find_movies(self): 140 """查詢數據庫中所有的電影數目 141 :return: 返回數據庫中所有的電影 142 """ 143 all_movies = deque() 144 data = self.db.find_item() 145 for item in data: 146 all_movies.append(item) 147 return all_movies 148 149 150 def main(): 151 """豆瓣電影爬蟲程序入口""" 152 # 1. 初始化工作,設置請求頭等 153 spider = DoubanSpider() 154 # 2. 與用戶交互,獲取用戶輸入的信息 155 spider.get_query_parameter() 156 ret = input('是否需要設置排序方式,評分范圍...(Y/N):') 157 if ret.lower() == 'y': 158 spider.get_default_query_parameter() 159 # 3. 對信息進行編碼處理,組合成有效的URL 160 spider.encode_query_data() 161 id = offset = 0 162 while True: 163 # 4. 下載影視信息 164 reps = spider.download_movies(offset) 165 # 5.提取下載的信息 166 movies = spider.get_movies(reps) 167 # 6. 保存數據到MongoDB數據庫 168 # spider.save_movies(movies, id) 169 offset += 20 170 id = offset 171 # 控制訪問速速 172 time.sleep(5) 173 174 175 if __name__ == '__main__': 176 main()
小結:在本次案例中,主要的難點有:查詢參數的組合那部分和了解異步加載的原理從而找到真正的URL!查詢參數的設置主要用到urlencode()方法,當我們不要把ASCII字符編碼的時候,我們要設置safe參數為string.printable,這樣只要把一些非ASCII字符編碼就好了,同樣quote()也是用來編碼的,也有safe參數;那么本例中為什么要使用urlencode()方法呢?主要是通過觀察URL是key=value的形式,所以才選用它!當我們把數據插入到數據庫中時,如果是有相同的名字的電影,我們就不插入,這樣也是處於對性能的考慮,合理利用資源!