利用Python爬取豆瓣電影


目標:使用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的形式,所以才選用它!當我們把數據插入到數據庫中時,如果是有相同的名字的電影,我們就不插入,這樣也是處於對性能的考慮,合理利用資源!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM