使用Tornado框架寫了一個用於獲取電影下載鏈接的API接口,歡迎大家使用 :-)


20180806更新:

由於我的阿里雲服務器到期了,所以這個API接口失效了。


請求方式

請求方式GET:
http://119.23.242.165:8888/resource/movie?id={id}&kw={kw}
 - id號用於指定電影網站,默認情況下同時返回id=1和id=2兩個網站的搜索結果
   - id=1:高清電影網(gaoqing.la)
   - id=2:BT天堂(www.bttt.la)
   - id=3:TorrentKitty(bt.sou177.com)
   - id={更多}:更多電影網站之後再慢慢加入
 - kw爲需要搜索的關鍵字,如
   - kw = 功夫:請求搜索關鍵字爲功夫的影視資源

示例:
 - 請求關鍵字爲‘功夫’的資源(默認id=1和id=2):http://119.23.242.165:8888/resource/movie?kw=功夫
 - 請求BT天堂中‘功夫’的資源:http://119.23.242.165:8888/resource/movie?kw=功夫&id=2
 - 若id不在{123}中,返回參數錯誤結果:param error

補充:
- 也可請求:http://119.23.242.165:8888/resource/movieplus?id={id}&kw={kw}
movieplus中加入了西刺代理中提供的免費代理IP,並且15分鐘更新一次,確保使用的是最新的代理IP,但實測因爲是免費代理IP可用率的原因,導致結果並不理想。

請求示例

  • 請求關鍵字爲‘功夫’的資源(默認id=1id=2):請求示例1
  • 請求BT天堂‘功夫’的資源:請求實例2
  • id不在{1,2,3}中,返回參數錯誤結果:請求示例3

返回結果展示

結果


前言

大概有兩個多月沒有更新博客了,主要是因爲這段時間去深圳實習了。在實習的過程中接觸到了PythonWeb框架之一——Tornado框架,我的主要工作就是利用Tornado框架開發爬蟲程序的API接口。關於Tornado的概述:

Tornado 和現在的主流 Web 服務器框架(包括大多數 Python
的框架)有着明顯的區別:它是非阻塞式服務器,而且速度相當快。得利於其 非阻塞的方式和對 epoll 的運用,Tornado
每秒可以處理數以千計的連接,因此 Tornado 是實時 Web 服務的一個 理想框架。

因爲個人喜歡看電影,而且對電影的畫質有要求,所以一般看電影都是在網上找資源通過下載鏈接或種子文件進行下載。年後,我打算寫一個API接口,用來一鍵獲取相關電影的下載鏈接。
我看了一下我常用的下載電影的無非就是那幾個網站,所以我的目的就是針對那幾個網站寫一個爬蟲程序,當對API接口發起請求時,爬蟲程序就爬取相關頁面的數據(資源名,下載鏈接等)以JSON的數據格式返回。


分析(以爬取BT天堂爲例)[末尾貼有BT天堂網站的完整程序]

1. 分析電影資源網站的搜索URL

這裏我以三個電影網站爲例,因爲這三個網站的資源幾乎足以滿足我找片源了(正規片)。分別是高清電影網BT天堂TorrentKitty種子庫
如在下面三個網站找關鍵字“功夫”的片源。

因爲針對中文字符或者另外有意義的字符,包含在URL中時需要字符進行URL編碼。

>>> from urllib.request import quote
>>> kw = '功夫'
>>> kw_utf = quote(kw)
>>> print(kw + ': ' + kw_utf)
功夫: %E5%8A%9F%E5%A4%AB

發現三個網站的搜索鏈接中,都是採用的將關鍵字包含在URL中的GET請求方式,以BT天堂爲例,帶有兩個參數,分別是關鍵字頁碼。故可以構造的請求鏈接如下:

from urllib.request import quote

# BT天堂搜索url
BTTT_SEARCH_URL = 'https://www.bttt.la/s.php?q={kw}&PageNo={page}'

# 搜索關鍵字和頁碼
kw = '功夫'
pageNo = 2

kw_utf = quote(kw)
bttt_index_url = BTTT_SEARCH_URL.format(kw=kw_utf, page=pageNo)

BTTT

2. 獲取BT天堂搜索結果中的所有頁面

爲了獲得所有的搜索結果,自然需要獲取到當前所有結果的所有頁面數量,我們從“末頁”當中可以得到當前搜索結果的最大頁數
page

# 獲取BT天堂搜索結果中的各頁面請求
async def get_bttt_res_page(self, search_url):  # 傳入搜索結果首頁的url
    result_reqs = list()
    home_page_req = self.CommonUtil.url2req(full_url=search_url, method='GET', headers=BTTT_HEADERS,
                                            validate_cert=False)  # 發現BT天堂缺少證書,故在請求的時候關閉證書
    home_page_resp = await self.async_client.fetch(home_page_req, raise_error=False)
    if home_page_resp.code == 200:  # 當首頁請求成功時,才能正確獲得所有頁面
        result_reqs.append(home_page_req)
        home_page_body = home_page_resp.body.decode('utf8')
        doc = HTML(home_page_body)
        if doc is None:
            raise self.StatusError.Succeed.EmptyResult
        last_page_link = doc.xpath('//form[@name="pagelist"]/li/a/@href')   # 獲取最後一頁的鏈接
        try:
            if last_page_link[-1]:  # 如果不存在多頁,則爲空
                max_page_num = int(last_page_link[-1].split('=')[-1])   # 獲取最大頁數
                for i in range(2, max_page_num + 1):    # 依次構造請求
                    index_url = BTTT_SEARCH_URL.format(kw=self.adic['kw'], page=i)
                    index_req = self.CommonUtil.url2req(full_url=index_url, method='GET', headers=BTTT_HEADERS,
                                                        validate_cert=False)
                    result_reqs.append(index_req)
        except:
            self.logging.info('bttt.la : Search result only 1 page.')
    return result_reqs

3. 獲得每一個頁面中各個影片的詳情鏈接

拿到了所有的頁面的請求返回結果的時候,我們需要提取出每一個頁面中的影片的詳情鏈接,爲下一步提取詳情頁面中的下載鏈接做準備。
詳情

# 提取BT天堂搜索關鍵字結果中的電影鏈接
def get_bttt_item_list(self, resps):
    movie_url_list = list() # 用於保存所有頁面的電影詳情鏈接
    for resp in resps:
        if resp.code == 200:    # 只保留成功請求的頁面
            index_body = resp.body.decode('utf8')
            doc = HTML(index_body)
            raw_item_link = doc.xpath('//div[@class="ml"]/div[@class="item cl"]/div[@class="litpic"]/a/@href')  # 提取電影詳情頁面的後綴鏈接
            movie_item_link = [BTTT_BASE_URL + each_link for each_link in raw_item_link]    # 拼接成完整鏈接
            movie_url_list.extend(movie_item_link)
    movie_reqs_list = list()    # 用於保存所有詳情頁面的請求
    if movie_url_list is not None:
        for each_url in movie_url_list:
            movie_content_req = self.CommonUtil.url2req(each_url, method='GET', headers=BTTT_HEADERS,
                                                        validate_cert=False)
            movie_reqs_list.append(movie_content_req)
    return movie_reqs_list

4. 獲得所有影片的下載鏈接

請求到每一部電影詳情頁面後,我們需要提取詳情頁面中的所有該影片的下載鏈接
下載

# 根據BT天堂的電影鏈接獲取電影的相關下載鏈接
BTTT_BASE_URL = 'https://www.bttt.la'
def parse_bttt_movie_content(self, resps):
    result_list = []  # 用於存儲所有搜索結果的全部下載鏈接
    for resp in resps:
        if resp.code == 200:
            content_body = resp.body.decode('utf8')
            doc = HTML(content_body)
            if doc is not None:
                movie_dict = {}
                try:
                    title = doc.xpath('//div[@class="title"]/h2')[0].xpath('string(.)')  # 提取關鍵字描述
                except:
                    title = self.adic['kw']
                movie_dict['FileName'] = title
                movie_dict['Link'] = resp.effective_url
                movie_download_list = []
                download_tags = doc.xpath('//div[@class="ml"]//div[@class="tinfo"]')    # 選中電影下載鏈接模塊
                for tag in download_tags:
                    if tag is not None:
                        link_dict = {}
                        download_link = BTTT_BASE_URL + tag.xpath('./a/@href')[0]  # 提取下載鏈接
                        download_desc = tag.xpath('./a/@title')[0].replace('BT種子下載', '')  # 提取關於下載文件的描述信息
                        link_dict['Information'] = download_desc
                        link_dict['DownloadLink'] = download_link
                        movie_download_list.append(link_dict)
                movie_dict['Resource'] = movie_download_list
                result_list.append(movie_dict)
    return result_list

總結-發現-問題

  • 由於一次關鍵字請求,在服務端會短時間內併發請求電影網站服務器幾百上千次,所以就很可能觸發網站的反爬機制。在高清電影網(gaoqing.la)中,根據判斷應該是針對IP進行限制訪問,所以我爬取了西刺代理來加入了代理IP,但是效果並不理想,所以很多時候導致高清電影網返回HTTP狀態碼503,在代碼中的處理結果是返回空列表。
  • BT天堂中,獲取到的並不是可以直接下載的磁力鏈接,根據網頁腳本分析而是要再次點擊下載按鈕填充表單提交數據才能下載,所以這裏我省略了。
  • 硬性條件的原因,我將我的代碼掛在我的阿里雲服務器上,慶幸電影網站沒有針對雲服務器IP進行限制。因爲我的是1M的帶寬,所以請求時間稍微需要更長一些,大概會在3s左右。
  • 返回的是JSON格式的數據,在FireFoxSafari瀏覽器中對JSON格式自帶解析功能,在ChromeMicrosoft
    Edge瀏覽器中使用時需要安裝JSON Formatter插件,否則會顯示JSON原始數據。
  • 寫到最後我發現我找了一些假的電影下載網站,所以打算之後再慢慢的加上去其他的網站所提供的下載鏈接。

[附]BT天堂部分的完整代碼

#!/usr/bin/env python 
# -*- coding: utf-8 -*- 
# @Author : Woolei
# @File : demo.py 

from lxml.etree import HTML
from util.TornadoBaseUtil import TornadoBaseHandler
from urllib.request import quote
from tornado import gen

# BT天堂基礎域名
BTTT_BASE_URL = 'https://www.bttt.la'
# BT天堂搜索url
BTTT_SEARCH_URL = 'https://www.bttt.la/s.php?q={kw}&PageNo={page}'
# BT天堂headers
BTTT_HEADERS = {'Host': 'www.bttt.la',
                'Referer': 'https://www.bttt.la/',
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36'}


class SearchHandler(TornadoBaseHandler):
    async def robust_get(self, id='', kw=''):
        item_list = await self.get_objects()
        result = {
            'data': item_list,
        }
        raise self.StatusError.Succeed(result)

    async def get_objects(self):
        bttt_index_url = BTTT_SEARCH_URL.format(kw=quote(self.adic['kw']), page=1)
        result = list()
        save_dict = {}
        self.logging.info('BT天堂 - ' + self.adic['kw'] + ' - ' + bttt_index_url)
        try:
            bttt_index_reqs = await self.get_bttt_res_page(search_url=bttt_index_url)
            bttt_index_tasks = [gen.convert_yielded(self.async_client.fetch(bttt_index_req, raise_error=False)) for
                                bttt_index_req in bttt_index_reqs]  # 把請求所有搜索結果頁面添加到隊列中
            bttt_index_resps = await gen.multi(bttt_index_tasks)
            bttt_movie_items_reqs = self.get_bttt_item_list(bttt_index_resps)
            bttt_content_tasks = [gen.convert_yielded(self.async_client.fetch(movie_items_req, raise_error=False))
                                  for
                                  movie_items_req in bttt_movie_items_reqs]  # 把搜索結果中的電影詳情頁請求添加到隊列中
            bttt_content_resps = await gen.multi(bttt_content_tasks)
            bttt_result = self.parse_bttt_movie_content(bttt_content_resps)  # 解析獲得下載鏈接結果
        except:
            bttt_result = []  # 請求出錯,則返回空結果
        save_dict['BT天堂(bttt.la)'] = bttt_result
        result.append(save_dict)
        return result

    # 獲取BT天堂搜索結果中的各頁面請求
    async def get_bttt_res_page(self, search_url):  # 傳入搜索結果首頁的url
        result_reqs = list()
        home_page_req = self.CommonUtil.url2req(full_url=search_url, method='GET', headers=BTTT_HEADERS,
                                                validate_cert=False)  # 發現BT天堂缺少證書,故在請求的時候關閉證書
        home_page_resp = await self.async_client.fetch(home_page_req, raise_error=False)
        if home_page_resp.code == 200:  # 當首頁請求成功時,才能正確獲得所有頁面
            result_reqs.append(home_page_req)
            home_page_body = home_page_resp.body.decode('utf8')
            doc = HTML(home_page_body)
            if doc is None:
                raise self.StatusError.Succeed.EmptyResult
            last_page_link = doc.xpath('//form[@name="pagelist"]/li/a/@href')  # 獲取最後一頁的鏈接
            try:
                if last_page_link[-1]:  # 如果不存在多頁,則爲空
                    max_page_num = int(last_page_link[-1].split('=')[-1])  # 獲取最大頁數
                    for i in range(2, max_page_num + 1):  # 依次構造請求
                        index_url = BTTT_SEARCH_URL.format(kw=self.adic['kw'], page=i)
                        index_req = self.CommonUtil.url2req(full_url=index_url, method='GET', headers=BTTT_HEADERS,
                                                            validate_cert=False)
                        result_reqs.append(index_req)
            except:
                self.logging.info('bttt.la : Search result only 1 page.')
        return result_reqs

    # 提取BT天堂搜索關鍵字結果中的電影鏈接
    def get_bttt_item_list(self, resps):
        movie_url_list = list()  # 用於保存所有頁面的電影詳情鏈接
        for resp in resps:
            if resp.code == 200:  # 只保留成功請求的頁面
                index_body = resp.body.decode('utf8')
                doc = HTML(index_body)
                raw_item_link = doc.xpath(
                    '//div[@class="ml"]/div[@class="item cl"]/div[@class="litpic"]/a/@href')  # 提取電影詳情頁面的後綴鏈接
                movie_item_link = [BTTT_BASE_URL + each_link for each_link in raw_item_link]  # 拼接成完整鏈接
                movie_url_list.extend(movie_item_link)
        movie_reqs_list = list()  # 用於保存所有詳情頁面的請求
        if movie_url_list is not None:
            for each_url in movie_url_list:
                movie_content_req = self.CommonUtil.url2req(each_url, method='GET', headers=BTTT_HEADERS,
                                                            validate_cert=False)
                movie_reqs_list.append(movie_content_req)
        return movie_reqs_list

    # 根據BT天堂的電影鏈接獲取電影的相關下載鏈接
    def parse_bttt_movie_content(self, resps):
        result_list = []  # 用於存儲所有搜索結果的全部下載鏈接
        for resp in resps:
            if resp.code == 200:
                content_body = resp.body.decode('utf8')
                doc = HTML(content_body)
                if doc is not None:
                    movie_dict = {}
                    try:
                        title = doc.xpath('//div[@class="title"]/h2')[0].xpath('string(.)')  # 提取關鍵字描述
                    except:
                        title = self.adic['kw']
                    movie_dict['FileName'] = title
                    movie_dict['Link'] = resp.effective_url
                    movie_download_list = []
                    download_tags = doc.xpath('//div[@class="ml"]//div[@class="tinfo"]')  # 選中電影下載鏈接模塊
                    for tag in download_tags:
                        if tag is not None:
                            link_dict = {}
                            download_link = BTTT_BASE_URL + tag.xpath('./a/@href')[0]  # 提取下載鏈接
                            download_desc = tag.xpath('./a/@title')[0].replace('BT種子下載', '')  # 提取關於下載文件的描述信息
                            link_dict['Information'] = download_desc
                            link_dict['DownloadLink'] = download_link
                            movie_download_list.append(link_dict)
                    movie_dict['Resource'] = movie_download_list
                    result_list.append(movie_dict)
        return result_list
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章