Python3 爬蟲實戰 — 豆瓣電影TOP250

爬取時間:2019-09-27 爬取難度:★★☆☆☆☆ 請求鏈接:https://movie.douban.com/top250 以及每部電影詳情頁 爬取目標:爬取榜單上每一部電影詳情頁的數據,保存為 CSV 文件;下載所有電影海報到本地 涉及知識:請求庫 requests、解析庫 lxml、Xpath 語法、正則表達式、CSV 和二進位數據儲存、列表操作 完整程式碼:https://github.com/TRHX/Python3-Spider-Practice/tree/master/douban-top250 其他爬蟲實戰程式碼合集(持續更新):https://github.com/TRHX/Python3-Spider-Practice 爬蟲實戰專欄(持續更新):https://itrhx.blog.csdn.net/article/category/9351278


【1x00】循環爬取網頁模組

觀察豆瓣電影 Top 250,請求地址為:https://movie.douban.com/top250

每頁展示25條電影資訊,照例翻頁觀察 url 的變化:

第一頁:https://movie.douban.com/top250

第二頁:https://movie.douban.com/top250?start=25&filter=

第三頁:https://movie.douban.com/top250?start=50&filter=

一共有10頁,每次改變的是 start 的值,利用一個 for 循環,從 0 到 250 每隔 25 取一個值拼接到 url,實現循環爬取每一頁,由於我們的目標是進入每一部電影的詳情頁,然後爬取詳情頁的內容,所以我們可以使用 Xpath 提取每一頁每部電影詳情頁的 URL,將其賦值給 m_urls,並返回 m_urls,m_urls 是一個列表,列表元素就是電影詳情頁的 URL

def index_pages(number):      url = 'https://movie.douban.com/top250?start=%s&filter=' % number      index_response = requests.get(url=url, headers=headers)      tree = etree.HTML(index_response.text)      m_urls = tree.xpath("//li/div/div/a/@href")      return m_urls    if __name__ == '__main__':      for i in range(0, 250, 25):          movie_urls = index_pages(i)

【2x00】解析模組

定義一個解析函數 parse_pages(),利用 for 循環,依次提取 index_pages() 函數返回的列表中的元素,也就是每部電影詳情頁的 URL,將其傳給解析函數進行解析

def index_pages(number):  	expressions    def parse_pages(url):  	expressions    if __name__ == '__main__':      for i in range(0, 250, 25):          movie_urls = index_pages(i)          for movie_url in movie_urls:              results = parse_pages(movie_url)

詳細看一下解析函數 parse_pages(),首先要對接收到的詳情頁 URL 發送請求,獲取響應內容,然後再使用 Xpath 提取相關資訊

def parse_pages(url):      movie_pages = requests.get(url=url, headers=headers)      parse_movie = etree.HTML(movie_pages.text)

【2x01】Xpath 解析排名、電影名、評分資訊

其中排名、電影名和評分資訊是最容易匹配到的,直接使用 Xpath 語法就可以輕鬆解決:

# 排名  ranking = parse_movie.xpath("//span[@class='top250-no']/text()")    # 電影名  name = parse_movie.xpath("//h1/span[1]/text()")    # 評分  score = parse_movie.xpath("//div[@class='rating_self clearfix']/strong/text()")

【2x02】Xpath 解析參評人數

接下來準備爬取有多少人參與了評價,分析一下頁面:

如果只爬取這個 <span> 標籤下的數字的話,沒有任何提示資訊,別人看了不知道是啥東西,所以把 人評價 這三個字也爬下來的話就比較好了,但是可以看到數字和文字不在同一個元素標籤下,而且文字部分還有空格,要爬取的話就要把 class="rating_people" 的 a 標籤下所有的 text 提取出來,然後再去掉空格:

# 參評人數  # 匹配a節點  value = parse_movie.xpath("//a[@class='rating_people']")  # 提取a節點下所有文本  string = [value[0].xpath('string(.)')]  # 去除多餘空格  number = [a.strip() for a in string]    # 此時 number = ['1617307人評價']

這樣做太麻煩了,我們可以直接提取數字,得到一個列表,然後使用另一個帶有提示資訊的列表,將兩個列表的元素合併,組成一個新列表,這個新列表的元素就是提示資訊+人數

# 參評人數  value = parse_movie.xpath("//span[@property='v:votes']/text()")  # 合併元素  number = [" ".join(['參評人數:'] + value)]    # 此時 number = ['參評人數:1617307']

【2x03】正則表達式解析製片國家、語言

接下來嘗試爬取製片國家/地區、語言等資訊:

分析頁面可以觀察到,製片國家/地區和語言結構比較特殊,沒有特別的 class 或者 id 屬性,所包含的層次關係也太複雜,所以這裡為了簡便,直接採用正則表達式來匹配資訊,就沒有那麼複雜了:

# 製片國家/地區  value = re.findall('<span class="pl">製片國家/地區:</span>(.*?)<br/>', movie_pages.text)  country = [" ".join(['製片國家:'] + value)]    # 語言  value = re.findall('<span class="pl">語言:</span>(.*?)<br/>', movie_pages.text)  language = [" ".join(['語言:'] + value)]

【3x00】返回解析數據

其他剩下的資訊皆可利用以上方法進行提取,所有資訊提取完畢,最後使用 zip() 函數,將所有提取的對象作為參數,將對象中對應的元素打包成一個個元組,然後返回由這些元組組成的列表

return zip(ranking, name, score, number, types, country, language, date, time, other_name, director, screenwriter, performer, m_url, imdb_url)

【4x00】數據儲存模組

定義一個數據保存函數 save_results()

def save_results(data):      with open('douban.csv', 'a', encoding="utf-8-sig") as fp:          writer = csv.writer(fp)          writer.writerow(data)

注意:編碼方式要設置為 utf-8-sig,如果設置為 utf-8,則文件會亂碼,不設置編碼,則可能會報一下類似錯誤:

UnicodeEncodeError: 'gbk' codec can't encode character 'ub3c4' in position 9: illegal multibyte sequence

可以看到錯誤出現在 ub3c4 上,將該 Unicode 編碼轉換為中文為 도,發現正是排名第 19 的電影:熔爐 도가니,因為標題有韓文,所以在儲存為 CSV 文件時會報編碼錯誤,而將編碼設置為 utf-8-sig 就不會報錯,具體原因參見:《Python 中文日文漢字亂碼處理utf-8-sig》

接下來是保存電影的海報到本地:

# 保存電影海報  poster = parse_movie.xpath("//div[@id='mainpic']/a/img/@src")  response = requests.get(poster[0])  name2 = re.sub(r'[A-Za-z:s]', '', name[0])  poster_name = str(ranking[0]) + ' - ' + name2 + '.jpg'  dir_name = 'douban_poster'  if not os.path.exists(dir_name):      os.mkdir(dir_name)  poster_path = dir_name + '/' + poster_name  with open(poster_path, "wb")as f:      f.write(response.content)

解析電影詳情頁,使用 Xpath 提取海報的 URL,向該 URL 發送請求

圖片以 排名+電影名.jpg 的方式命名,但是由於提取的電影名部分含有特殊字元,比如排名第 10 的電影:忠犬八公的故事 Hachi: A Dog』s Tale,其中有個冒號,而 Windows 文件命名是不能包含這些字元的,所以我們直接去除電影名包含的英文字元、空白字元、特殊字元,只留下中文,程式碼實現: name2 = re.sub(r'[A-Za-z:s]', '', name[0])

定義一個文件夾名稱 douban_poster,利用 os 模組判斷當前是否存在該文件夾,若不存在就創建一個

最後以二進位形式保存海報到當前目錄的 douban_poster 文件夾下


【5x00】完整程式碼

# =============================================  # --*-- coding: utf-8 --*--  # @Time    : 2019-09-27  # @Author  : TRHX  # @Blog    : www.itrhx.com  # @CSDN    : https://blog.csdn.net/qq_36759224  # @FileName: douban.py  # @Software: PyCharm  # =============================================    import requests  from lxml import etree  import csv  import re  import time  import os    headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36'}      def index_pages(number):      url = 'https://movie.douban.com/top250?start=%s&filter=' % number      index_response = requests.get(url=url, headers=headers)      tree = etree.HTML(index_response.text)      m_urls = tree.xpath("//li/div/div/a/@href")      return m_urls      def parse_pages(url):      movie_pages = requests.get(url=url, headers=headers)      parse_movie = etree.HTML(movie_pages.text)        # 排名      ranking = parse_movie.xpath("//span[@class='top250-no']/text()")        # 電影名      name = parse_movie.xpath("//h1/span[1]/text()")        # 評分      score = parse_movie.xpath("//div[@class='rating_self clearfix']/strong/text()")        # 參評人數      value = parse_movie.xpath("//span[@property='v:votes']/text()")      number = [" ".join(['參評人數:'] + value)]      # value = parse_movie.xpath("//a[@class='rating_people']")      # string = [value[0].xpath('string(.)')]      # number = [a.strip() for a in string]      # print(number)        # 類型      value = parse_movie.xpath("//span[@property='v:genre']/text()")      types = [" ".join(['類型:'] + value)]        # 製片國家/地區      value = re.findall('<span class="pl">製片國家/地區:</span>(.*?)<br/>', movie_pages.text)      country = [" ".join(['製片國家:'] + value)]        # 語言      value = re.findall('<span class="pl">語言:</span>(.*?)<br/>', movie_pages.text)      language = [" ".join(['語言:'] + value)]        # 上映時期      value = parse_movie.xpath("//span[@property='v:initialReleaseDate']/text()")      date = [" ".join(['上映日期:'] + value)]        # 片長      value = parse_movie.xpath("//span[@property='v:runtime']/text()")      time = [" ".join(['片長:'] + value)]        # 又名      value = re.findall('<span class="pl">又名:</span>(.*?)<br/>', movie_pages.text)      other_name = [" ".join(['又名:'] + value)]        # 導演      value = parse_movie.xpath("//div[@id='info']/span[1]/span[@class='attrs']/a/text()")      director = [" ".join(['導演:'] + value)]        # 編劇      value = parse_movie.xpath("//div[@id='info']/span[2]/span[@class='attrs']/a/text()")      screenwriter = [" ".join(['編劇:'] + value)]        # 主演      value = parse_movie.xpath("//div[@id='info']/span[3]")      performer = [value[0].xpath('string(.)')]        # URL      m_url = ['豆瓣鏈接:' + movie_url]        # IMDb鏈接      value = parse_movie.xpath("//div[@id='info']/a/@href")      imdb_url = [" ".join(['IMDb鏈接:'] + value)]        # 保存電影海報      poster = parse_movie.xpath("//div[@id='mainpic']/a/img/@src")      response = requests.get(poster[0])      name2 = re.sub(r'[A-Za-z:s]', '', name[0])      poster_name = str(ranking[0]) + ' - ' + name2 + '.jpg'      dir_name = 'douban_poster'      if not os.path.exists(dir_name):          os.mkdir(dir_name)      poster_path = dir_name + '/' + poster_name      with open(poster_path, "wb")as f:          f.write(response.content)        return zip(ranking, name, score, number, types, country, language, date, time, other_name, director, screenwriter, performer, m_url, imdb_url)      def save_results(data):      with open('douban.csv', 'a', encoding="utf-8-sig") as fp:          writer = csv.writer(fp)          writer.writerow(data)      if __name__ == '__main__':      num = 0      for i in range(0, 250, 25):          movie_urls = index_pages(i)          for movie_url in movie_urls:              results = parse_pages(movie_url)              for result in results:                  num += 1                  save_results(result)                  print('第' + str(num) + '條電影資訊保存完畢!')                  time.sleep(3)

【6x00】數據截圖


【7x00】程式不足的地方

程式不足的地方:豆瓣電影有反爬機制,當程式爬取到大約 150 條數據的時候,IP 就會被封掉,第二天 IP 才會解封,可以考慮綜合使用多個代理、多個 User-Agent、隨機時間暫停等方法進行爬取

Tags: