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不在{1,2,3}中,返回参数错误结果:param error
补充:
- 也可请求:http://119.23.242.165:8888/resource/movieplus?id={id}&kw={kw}
movieplus中加入了西刺代理中提供的免费代理IP,并且15分钟更新一次,确保使用的是最新的代理IP,但实测因为是免费代理IP可用率的原因,导致结果并不理想。
请求示例
返回结果展示
前言
大概有两个多月没有更新博客了,主要是因为这段时间去深圳实习了。在实习的过程中接触到了Python
的Web
框架之一——Tornado
框架,我的主要工作就是利用Tornado
框架开发爬虫程序的API
接口。关于Tornado的概述:
Tornado
和现在的主流Web
服务器框架(包括大多数Python
的框架)有着明显的区别:它是非阻塞式服务器,而且速度相当快。得利于其 非阻塞的方式和对epoll
的运用,Tornado
每秒可以处理数以千计的连接,因此Tornado
是实时Web
服务的一个 理想框架。
因为个人喜欢看电影,而且对电影的画质有要求,所以一般看电影都是在网上找资源通过下载链接或种子文件进行下载。年后,我打算写一个API
接口,用来一键获取相关电影的下载链接。
我看了一下我常用的下载电影的无非就是那几个网站,所以我的目的就是针对那几个网站写一个爬虫程序,当对API
接口发起请求时,爬虫程序就爬取相关页面的数据(资源名,下载链接等)以JSON
的数据格式返回。
分析(以爬取BT天堂为例)[末尾贴有BT天堂网站的完整程序]
1. 分析电影资源网站的搜索URL
这里我以三个电影网站为例,因为这三个网站的资源几乎足以满足我找片源了(正规片)。分别是高清电影网
、BT天堂
、TorrentKitty种子库
。
如在下面三个网站找关键字“功夫”的片源。
- 高清电影网(gaoqing.la):http://gaoqing.la/?s=%E9%92%A2%E9%93%81%E4%BE%A0
- BT天堂(www.bttt.la):https://www.bttt.la/s.php?q=%E5%8A%9F%E5%A4%AB&PageNo=1
- TorrentKitty(bt.sou177.com):https://bt.sou177.com/index.php?r=files/index&kw=%E5%8A%9F%E5%A4%AB
因为针对中文字符或者另外有意义的字符,包含在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)
2. 获取BT天堂搜索结果中的所有页面
为了获得所有的搜索结果,自然需要获取到当前所有结果的所有页面数量,我们从“末页”
当中可以得到当前搜索结果的最大页数
。
# 获取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
格式的数据,在FireFox
和Safari
浏览器中对JSON
格式自带解析功能,在Chrome
和Microsoft
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