老司機教你下載tumblr上視頻和圖片的正確姿勢


本文面向初學者。

很多同學問我:“我非常想學Python編程,但是找不到興趣點”。 還有的同學呢,找到了很好的興趣點,但是無從下手,“玄魂老師,我想下載tumblr上的視頻, 怎么下載,Python能實現嗎?你懂得(這里有一個淫笑的表情)”。

好吧,我表示對他所要表達的意思秒懂了,宅男都喜歡看別人開車。今天本人姑且裝一把老司機, 帶大家來分析下如何下載tumblr上的圖片和視頻。請大家准備好紙巾,哦不,是准備好開發工具, 我們開始寫代碼。

1.1 需求分析

下載一個站點上的圖片和視頻,無非就是寫一個簡易的爬蟲,這里我不去使用現有的爬蟲框架, 也可以很容易的完成任務。編寫指定頁面的爬蟲,需要對目標頁面的HTML結構進行分析,如果 是AJAX請求,需要進行多次的請求分析;如果存在身份驗證,則要進一步處理Cookie等數據。一般的 爬蟲的基本結構如下圖:

爬蟲基本結構

即使編寫一個最小的爬蟲,我們也要有一個任務調度器,用來生成任務條目到隊列中, 針對不同的任務類型,要編寫不同的下載器。不考慮分布式部署的情況下,每一個下載器(Downloader) 應該在一個線程中執行任務。今天我們的要編寫的爬蟲略微簡單,考慮到入門級同學,過多的 概念就不介紹了,以免發蒙。

如果你還不知道Tumblr是什么的話,請百度。 Tumblr(中文名:湯博樂) 成立於2007年,是目前全球最大的輕博客網站,也是輕博客網站的始祖。Tumblr(湯博樂)是一種介於傳統博客和微博之間的全新媒體形態,既注重表達,又注重社交,而且注重個性化設置,成為當前最受年輕人歡迎的社交網站之一。雅虎公司董事會2013年5月19日決定,以11億美元收購Tumblr。

這是一個高大上的網站,很多設計師,動圖愛好者的聚集地。不過目前需要翻牆訪問。

下面我們來看一下tumblr的個人空間。

tumblr個人頁面

如圖,每個tumblr的個人空間都是一個二級域名,你甚至可以綁定你自己的域名。在個人主頁上, 是一個微博式的消息列表,有文字,圖片,視頻等形式。消息的展現,是頁面上的JavaScript腳本 通過請求Tumblr的Api來獲取返回信息,然后添加到頁面上的。通過API,可以省掉很多麻煩,至少 我們不必分析整個頁面的html來提取需要的信息了。

tumblr個人頁面

下面我們看一下接口:

上面的代碼是一個接口模板,第一個參數是要訪問的用戶空間的用戶名;第二個參數是媒體類型, 圖片為“photo”,視頻為“video”;第三個參數為請求的資源數;第四個參數為從第幾個資源開始 請求。 下面我們構造一個photo的請求,看看返回的數據是什么樣的。

tumblrAPI

我們看到返回的數據是XML格式的數據,基本的層級為Tumblr>posts>post。圖片的URL在post的photo-url字段中,視頻與此類似,就不再演示了。 獲取到媒體資源的url之后,就可以進行下載了。

我們再構造一個video類型的請求。

tumblrAPI

video類型的資源的url,需要從player屬性中進行進一步匹配才能得出最后的結果。

在具體編碼之前,我們需要對可能遇到的技術難點進行一個評估,並找到解決方案。

1.2 技術點分析

1.2.1 如何發送http請求

這里推薦 request模塊(https://pypi.python.org/pypi/requests/)。在我們要實現的功能 中,直接使用request模塊的get方法就可以了。

1.2.2 如何處理xml數據

使用request模塊發送並接收數據之后,要處理返回的XML數據,因為我們只需要獲取photo-uri字段的 值就可以了,所以這里推薦使用xmltodict模塊(https://pypi.python.org/pypi/xmltodict)。 xmltodic模塊,將xml文檔處理成類似Json對象,方便我們對數據進行訪問。

1.2.3 如何實現Queue

python中自帶Queue模塊,可以滿足我們目前的隊列需求,由於python2.7和python3.0中 對queue模塊的命名進行的變更,編程的時候需要注意。如果考慮兼容兩個版本的話,可以 考慮引入six模塊(https://pypi.python.org/pypi/six)。six模塊是一個專門用於解決 從python2.x到python3.x的兼容性問題的模塊,它對python版本變更導致到部分模塊不能應用的問題 進行了內部處理,需要處理類似兼容問題的時候,可以考慮或者參考該模塊的實現方式。

1.2.4 如何實現多線程

關於Python多線程,請自行搜索相關文章進行學習,例子很多,這里就不詳細說明了。

1.2.5 如何處理json

考慮到Tumblr需要翻牆訪問,如果本機不使用VPN的話,可能需要配置代理,代理采用json配置方式。 處理.使用python內置的json模塊(https://docs.python.org/2/library/json.html)就可以了。

1.2.6 如何使用正則表達式

為了精確匹配url信息,我們需要使用正則表達式對xml數據的中字段值進行進一步處理,使用 內置的re模塊(https://docs.python.org/2/library/re.html)就可以了。

1.3 搭建程序基本框架

通過上面的分析,我們編寫一個下載Tumblr圖片和視頻的簡易爬蟲已經沒有技術障礙了,下面 我們搭建基本框架。

以下代碼非玄魂原創,參考自https://github.com/dixudx/tumblr-crawler,做了部分修改

 1 # 設置請求超時時間
 2 TIMEOUT = 10
 3 
 4 # 嘗試次數
 5 RETRY = 5
 6 
 7 # 分頁請求的起始點
 8 START = 0
 9 
10 # 每頁請求個數
11 MEDIA_NUM = 50
12 
13 # 並發線程數
14 THREADS = 10
15 
16 # 是否下載圖片
17 ISDOWNLOADIMG=True
18 
19 #是否下載視頻
20 ISDOWNLOADVIDEO=True
21 
22 #任務執行類
23 class DownloadWorker(Thread):
24     def __init__(self, queue, proxies=None):
25         Thread.__init__(self)
26         self.queue = queue
27         self.proxies = proxies
28 
29     def run(self):
30         while True:
31             medium_type, post, target_folder = self.queue.get()
32             self.download(medium_type, post, target_folder)
33             self.queue.task_done()
34 
35     def download(self, medium_type, post, target_folder):
36         pass
37 
38     def _handle_medium_url(self, medium_type, post):
39        pass
40 
41     def _download(self, medium_type, medium_url, target_folder):
42        pass
43 
44 #調度類
45 class CrawlerScheduler(object):
46 
47     def __init__(self, sites, proxies=None):
48         self.sites = sites
49         self.proxies = proxies
50         self.queue = Queue.Queue()
51         self.scheduling()
52 
53     def scheduling(self):
54         pass
55 
56 
57 
58     def download_videos(self, site):
59         pass
60 
61     def download_photos(self, site):
62         pass
63 
64     def _download_media(self, site, medium_type, start):
65         pass
66 
67 #程序入口
68 #初始化配置

 

首先,我們定義了一些全局變量,看注釋就明白用途了,不做過多解釋。

現在看上面的類和方法的定義。DownloadWorker類,執行具體的下載任務,因為每個下載任務 要在單獨的線程中完成,所以我們將DownloadWorker類繼承Thread類。DownloadWorker接收從CrawlerScheduler 傳遞過來的Queue,它會從queue中請求任務來執行。同時如果用戶配置了代理,在執行http請求的時候會使用代理。 run方法是線程啟動方法,它會不停的從queue中請求任務,執行任務。download方法,首先調用_handle_medium_url 方法,獲取當前任務的url,然后調用_download方法執行具體的下載任務。

CrawlerScheduler類,根據配置中需要處理的用戶名,創建任務隊列,初始化任務線程,啟動線程執行任務。 scheduling方法,創建並啟動工作線程,然后調用download_videos和download_photos方法。 download_videos和download_photos方法分別調用_download_media方法,創建具體的任務隊列。_download_media 方法,首先根據傳入的site創建對應的本地文件夾,然后請求Tumblr的接口,獲取用戶所有的圖片或者視頻數據壓入隊列。

除了上面的核心方法之外,我們創建兩個配置文件proxies.json和sites.txt文件。proxies.json用來配置 代理,默認為空。

{}

 

可以根據你使用的代理,進行具體的配置,比如:

{
  "http": "http://10.10.1.10:3128",
  "https": "127.0.0.1:8787"
}

或者

{
    "http": "socks5://user:pass@host:port",
    "https": "socks5://127.0.0.1:1080"
}

sites.txt文件用來配置我們要請求的用戶空間,只需要配置用戶名即可,例如:

want2580,luoli-qaq

1.4 具體實現

這里大家使用最新的python版本就可以了,安裝Python的時候一定要將pip一同安裝。 根據1.2節的分析,我們需要安裝如下模塊:

requests>=2.10.0
xmltodict
six
PySocks>=1.5.6

為了方便,可以將這些依賴放到一個requirements.txt文件中。

requerments.txt

然后執行命令:

pip install -r requirements.txt
 

基本環境准備完畢之后,在具體實現邏輯之前先引入依賴的模塊。

# -*- coding: utf-8 -*-

import os
import sys
import requests
import xmltodict
from six.moves import queue as Queue
from threading import Thread
import re
import json

 

下面我們先來完善CrawlerScheduler類的scheduling方法。

def scheduling(self):
        # 創建工作線程
        for x in range(THREADS):
            worker = DownloadWorker(self.queue,
                                    proxies=self.proxies)
            #設置daemon屬性,保證主線程在任何情況下可以退出
            worker.daemon = True
            worker.start()

        for site in self.sites:
            if ISDOWNLOADIMG:
                self.download_photos(site)
            if ISDOWNLOADVIDEO:
                self.download_videos(site)

 

 
        

根據全局變量THREADS定義的最大線程數,創建DownloadWorker對象,並調用start方法,啟動線程。 接下來根據傳入的sites,循環調用download_photos和download_videos方法。下面我們看download_photos和download_videos方法 的實現。

 def download_videos(self, site):
        self._download_media(site, "video", START)
        # 等待queue處理完一個用戶的所有請求任務項
        self.queue.join()
        print("視頻下載完成 %s" % site)

    def download_photos(self, site):
        self._download_media(site, "photo", START)
         # 等待queue處理完一個用戶的所有請求任務項
        self.queue.join()
        print("圖片下載完成 %s" % site)

這兩個方法,只是調用了_download_media方法,傳入各自的類型,和分頁請求的其實索引值,目前都是從0開始。 下面看核心的_download_media方法。

def _download_media(self, site, medium_type, start):
        #創建存儲目錄
        current_folder = os.getcwd()
        target_folder = os.path.join(current_folder, site)
        if not os.path.isdir(target_folder):
            os.mkdir(target_folder)

        base_url = "http://{0}.tumblr.com/api/read?type={1}&num={2}&start={3}"
        start = START
        while True:
            media_url = base_url.format(site, medium_type, MEDIA_NUM, start)
            response = requests.get(media_url,
                                    proxies=self.proxies)
            data = xmltodict.parse(response.content)
            try:
                posts = data["tumblr"]["posts"]["post"]
                for post in posts:
                    # select the largest resolution
                    # usually in the first element
                    self.queue.put((medium_type, post, target_folder))
                start += MEDIA_NUM
            except KeyError:
                break

_download_media方法,會在當前程序執行目錄創建以用戶名命名的子文件夾,用來存儲圖片和視頻文件。這里 使用os.getcwd()來獲取當前程序執行的覺得路徑,然后通過os.path.join(current_folder, site)構造目標文件夾 路徑,通過os.path.isdir(target_folder)來判斷是否已經存在該文件夾,如果沒有則通過os.mkdir(target_folder) 創建文件夾。

接下來,_download_media方法循環進行分頁請求,來獲取圖片或視頻資源信息。通過requests.get(media_url,proxies=self.proxies) 發送http get請求,通過response.content獲取返回的數據,然后利用xmltodict.parse(response.content)來 反序列化xml數據到data對象。調用 data["tumblr"]["posts"]["post"],獲取當前返回數據中的所有媒體資源。 然后循環調用self.queue.put((medium_type, post, target_folder))方法,將每一個post字段壓入隊列。此時壓入隊列的post包含了 一個圖片或者視頻的各項數據,需要在worker線程執行的時候進一步處理才能得到具體的url,后面我們繼續分析。

scheduling類的實現已經完成了,在scheduling類的scheduling方法中啟動了線程,每個worker對象的run方法 都會執行。

   def run(self):
        while True:
            medium_type, post, target_folder = self.queue.get()
            self.download(medium_type, post, target_folder)
            self.queue.task_done()

run 方法,通過self.queue.get()方法,從任務隊列中獲取一條任務,每個任務包含媒體類型(圖片或則視頻), 每個媒體的post信息以及下載文件保存的目標文件夾。run方法將這些信息傳入download方法。

 def download(self, medium_type, post, target_folder):
        try:
            medium_url = self._handle_medium_url(medium_type, post)
            if medium_url is not None:
                self._download(medium_type, medium_url, target_folder)
        except TypeError:
            pass

download方法首先通過_handle_medium_url方法獲取具體的資源的url,然后調用_download執行下載。

def _handle_medium_url(self, medium_type, post):
        try:
            if medium_type == "photo":
                return post["photo-url"][0]["#text"]

            if medium_type == "video":
                video_player = post["video-player"][1]["#text"]
                hd_pattern = re.compile(r'.*"hdUrl":("([^\s,]*)"|false),')
                hd_match = hd_pattern.match(video_player)
                try:
                    if hd_match is not None and hd_match.group(1) != 'false':
                        return hd_match.group(2).replace('\\', '')
                except IndexError:
                    pass
                pattern = re.compile(r'.*src="(\S*)" ', re.DOTALL)
                match = pattern.match(video_player)
                if match is not None:
                    try:
                        return match.group(1)
                    except IndexError:
                        return None
        except:
            raise TypeError("找不到正確的下載URL "
                            "請到 "
                            "https://github.com/xuanhun/tumblr-crawler"
                            "提交錯誤信息:\n\n"
                            "%s" % post)

_handle_medium_url方法,根據媒體的不同采用了不同的資源獲取方法。不過獲取圖片的方法這里還是有缺陷的, 因為用戶在一個post中可能會發送一組圖片,目前的方法只處理了第一張圖片。如果是視頻,先取出video-player 的文本內容,然后通過正則表達式匹配出視頻的url,具體匹配原理,你只要參考視頻請求返回的xml內容就明白了, 這里就不詳細分析了。下面我們看如何下載資源。

def _download(self, medium_type, medium_url, target_folder):
        medium_name = medium_url.split("/")[-1].split("?")[0]
        if medium_type == "video":
            if not medium_name.startswith("tumblr"):
                medium_name = "_".join([medium_url.split("/")[-2],
                                        medium_name])

            medium_name += ".mp4"

        file_path = os.path.join(target_folder, medium_name)
        if not os.path.isfile(file_path):
            print("Downloading %s from %s.\n" % (medium_name,
                                                 medium_url))
            retry_times = 0
            while retry_times < RETRY:
                try:
                    resp = requests.get(medium_url,
                                        stream=True,
                                        proxies=self.proxies,
                                        timeout=TIMEOUT)
                    with open(file_path, 'wb') as fh:
                        for chunk in resp.iter_content(chunk_size=1024):
                            fh.write(chunk)
                    break
                except:
                    # try again
                    pass
                retry_times += 1
            else:
                try:
                    os.remove(file_path)
                except OSError:
                    pass
                print("Failed to retrieve %s from %s.\n" % (medium_type,
                                                            medium_url))

_download方法先構造存儲文件的路徑,如果文件存在則不再反復下載,這樣可以保證在下載出現錯誤的情況下, 我們可以手動重啟程序,多次下載。通過

resp = requests.get(medium_url,
                                        stream=True,
                                        proxies=self.proxies,
                                        timeout=TIMEOUT)

發送get請求,獲取流數據,然后每次從流中請求1024bit數據寫入磁盤,直到流結束為止:

 with open(file_path, 'wb') as fh:
                        for chunk in resp.iter_content(chunk_size=1024):
                            fh.write(chunk)

至此所有核心代碼都完善完畢,是不是很簡單呢?確實很簡單,這就是Python的強大之處。最后我們完善下程序的入口。

def usage():
    print(u"未找到sites.txt文件,請創建.\n"
          u"請在文件中指定Tumblr站點名,並以逗號分割,不要有空格.\n"
          u"保存文件並重試.\n\n"
          u"例子: site1,site2\n\n"
          u"或者直接使用命令行參數指定站點\n"
          u"例子: python tumblr-photo-video-ripper.py site1,site2")


def illegal_json():
    print(u"文件proxies.json格式非法.\n"
          u"請參照示例文件'proxies_sample1.json'和'proxies_sample2.json'.\n"
          u"然后去 http://jsonlint.com/ 進行驗證.")


if __name__ == "__main__":
    sites = None

    proxies = None
    if os.path.exists("./proxies.json"):
        with open("./proxies.json", "r") as fj:
            try:
                proxies = json.load(fj)
                if proxies is not None and len(proxies) > 0:
                    print("You are using proxies.\n%s" % proxies)
            except:
                illegal_json()
                sys.exit(1)

    if len(sys.argv) < 2:
        #校驗sites配置文件
        filename = "sites.txt"
        if os.path.exists(filename):
            with open(filename, "r") as f:
                sites = f.read().rstrip().lstrip().split(",")
        else:
            usage()
            sys.exit(1)
    else:
        sites = sys.argv[1].split(",")

    if len(sites) == 0 or sites[0] == "":
        usage()
        sys.exit(1)

    CrawlerScheduler(sites, proxies=proxies)

 

我們定義了usage方法,提示用戶如何使用,illegal_json方法提示代理配置錯誤。 在程序的入口處,先判斷是否有代理配置,如果有則取出信息。sites的配置支持從文件和命令行參數傳入兩種方式。最后 初始化CrawlerScheduler,啟動抓取程序。


1.5 小結

至此,程序構造完畢,總共才200行左右的代碼,沒有什么能阻擋荷爾蒙的迸發,進軍吧,少年們,把擼雞雞的手解放出來擼擼代碼,你也能做老司機! 玩笑歸玩笑,希望這能激發你編程的欲望,自己動手敲一敲,會學到很多的,也許就愛上編程了呢。

tumblr

最后,全部完整代碼,我已經放到github上了(https://github.com/xuanhun/tumblr-crawler),如果你在微信中閱讀本文,點擊閱讀原文,可以跳轉過去。我認為我已經 講解的夠詳細了,同時源代碼已經給到了,你還是搞不定的話,只能說編程不適合你。當然,不能勉強所有人都喜歡 編程,我把這個代碼打包成了exe工具,作為宅男福利,作為對不勞而獲的懲罰,你需要在微信訂閱號(xuanhun521)本篇文章下面打賞>=10元,我會在微信訂閱號后台統一回復給你工具包和使用方法。

請關注微信訂閱號(xuanhun521,下方二維碼),回復“python”,可查看更多python基礎及黑客編程內容。問題討論請加qq群:Hacking (1群):303242737   Hacking (2群):147098303。

 

玄魂工作室-精彩不斷

 


免責聲明!

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



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