使用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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章