返回
编程
分类

个人比较喜欢看日剧,我们首先需要下载包含有感兴趣数据的网页

日期: 2020-01-02 07:56 浏览次数 : 155

Python爬虫: "追新番"网站资源链接爬取,python爬虫

作者:hwj3747
转载请注明

本内容为《用Python写网络爬虫》书籍内容,有兴趣的读者可以购买本书,本章的代码皆可在Python3中运行。
为了抓取网站,我们首先需要下载包含有感兴趣数据的网页,该过程一般称为爬去(crawling)。爬去一个网站有很多种方法,而选用那种方法更加合适,则取决于目标网站的结构。我们首先探讨如何安全地下载网页,然后介绍3种爬去网站的常见方法:

“追新番”网站

追新番网站提供最新的日剧和日影下载地址,更新比较快。

个人比较喜欢看日剧,因此想着通过爬取该网站,做一个资源地图

可以查看网站到底有哪些日剧,并且随时可以下载。

简介

在看动漫追番的时候,发现每一季度的新番都是被优酷,爱奇艺,哔哩哔哩,PPTV等各大视频厂商买了版权,导致我在手机上要装各种软件,并且很多番更是直接被广电禁掉了,很烦。于是乎,我找到了一个山寨的网站:风车动漫,里面资源倒是挺多,(当然,广告弹窗什么的也很多)可惜没有APP端。刚好最近学习了爬虫技术,于是我就想,能不能用爬虫技术帮他搞一个APP端呢?说干就干,刚好好久没写代码了,就当是练练手,于是我制作了一个简易版的APP,不会设计界面,界面有点丑,并且还是有很多问题没解决就是了。
目前完成了新番展示页面,即展示周一到周日每日新番表,番剧详情页面,播放页面效果如下:

必威官网亚洲体育 1

ezgif-2-b3fd223a68.gif

基本功能算是实现了,但是还有很多问题,能力有限,还没解决。

  • 爬取网站地图;
  • 遍历每个网页的数据库ID;
  • 跟踪网页链接。

资源地图

必威官网亚洲体育 ,爬取的资源地图如下:

在linux系统上通过 ls | grep keywords 可以轻松找到想要的资源(windows直接搜索就行啦)

必威官网亚洲体育 2

前期准备

  • 技术准备:需要掌握Android开发技术,以及一点点的前端HTML,CSS,JS技术。
  • 基本框架就是我以前写过的[Android MVP+Retrofit+dagger2+RxAndroid框架整合])(其实这么小的项目,根本不需要这么重的框架,但是拿过来练手,熟悉下框架还是不错的)
  • jsoup:好像Java做爬虫都是用的这个包。
  • contextmenu:就是右边周一到周日的选择菜单,看他动效还不错就拿过来用了,详细使用方法点进去GitHub就看得到了。
  • glide :这个没什么好说的,就是加载图片的库了。
  • 谷歌浏览器 :因为要做爬虫,所以分析前端的HTML代码的工具也要有,这里推荐Chrome浏览器,好用!
  • 注意,本文主要讲述爬虫方面如何爬数据,至于Android端的实现都只是一些基本的页面,所以就不一一赘述了。

下载网页

要想爬取网页,我们首先需要将其下载下来。下面的示例脚本使用Python3的urllib.request模块下载URL。

import urllib.request
def download(url):
    return urllib.request.urlopen(url).read()
if __name__ == '__main__':
    print(download('http://www.baidu.com'))

当传入URL参数时,print会输出download函数获取的网址源码。不过,这个代码片段存在一个问题,即当下载网页时,我们可能会遇到一些无法控制的错误,比如请求的页面可能不存在。此时,urllib会抛出异常,然后退出脚本。安全起见,下面在给出一个更健壮的版本,可以捕获这些异常。

def download(url):
    """捕获错误的下载函数"""
    print("Downloading:", url)
    try:
        html = urllib.request.urlopen(url).read()
    except urllib.request.URLError as e:
        print("download error:", e.reason)
        html = None
    return html

现在,当出现下载错误时,该函数能够捕获到异常,然后返回None。

爬取脚本开发

番剧列表页面

首先进入风车动漫这个网站,找到这个地方,我们只需要获取周一到周日的番剧列表就行了。

必威官网亚洲体育 3

5.PNG

然后用谷歌浏览器F12查看源代码,得到:

必威官网亚洲体育 4

6.PNG

我们发现它的结构是这样的:最上层一个div标签 class为tists,包含7个ul标签,每个u标签l包含若干个li标签,这个li标签里面就是每个番剧里的信息了,只包含番剧名,番剧链接以及当前第几话的信息。
知道这些后,我们就可以用爬虫来获取这些数据了。
首先我们建立一个用来存放番剧简要信息的实体类:

public class BangumiEntity {
    String title;//番剧标题
    String number;//当前是第几话
    String url;//番剧的链接
}

然后初始化,用Jsoup获取网站连接:

            String url = "http://www.fengchedm.com/";
            Connection conn = Jsoup.connect(url);
            // 修改http包中的header,伪装成浏览器进行抓取
            conn.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:32.0) Gecko/    20100101 Firefox/32.0");
            Document doc = null;
            try {
                doc = conn.get();
            } catch (IOException e) {
                e.printStackTrace();
            }

先获取最上层class为tists的这个div标签

Elements noteList = doc.select("div.tists");

然后获取其下的ul标签列表

 Elements ul = noteList.select("ul");

然后用同样的方法遍历ul标签下的li标签,获取其中的数据,因为这里只有两个a标签,第一个是第几话,第二个是番剧名,所以我就用first和last获取了,没在用数组了。.而attr方法可以用来获取标签内的某个属性,比如这里的a标签里的href属性,并且加上abs:可以取得到完整的路径,因为很多网站写路径的时候都是用的相对路径,最后,用二维数组保存这个信息:

ArrayList<ArrayList<BangumiEntity>> arrayList=new ArrayList<ArrayList<BangumiEntity>>();
for (Element ulElement : ul) {
                Elements li=ulElement.select("li");
                ArrayList<BangumiEntity> bangumiEntities=new ArrayList<BangumiEntity>();
                for (Element liElement : li) {
                    BangumiEntity bangumiEntity=new BangumiEntity();
                    bangumiEntity.setNumber(liElement.select("a").first().text());
                    bangumiEntity.setTitle(liElement.select("a").last().text());
                    bangumiEntity.setUrl(liElement.select("a").last().attr("abs:href"));
                    bangumiEntities.add(bangumiEntity);
                }
                arrayList.add(bangumiEntities);
            }

接下来要做的就是把这个arrayList的数据展示到页面上了,页面我是用的RecycleView+CarView实现,具体见源码。

重新下载

下载时遇到的错误经常是临时性的,比如服务器过载时返回的 503 Service Unavailable 错误。对于此类错误,我们可以尝试重新下载,因为这个服务器问题现在可能已解决。不过,我们不需要对所有错误都尝试重新下载。如果服务器返回的是 404 Not Found错误,则说明该网页目前并不存在,再次尝试同样的请求一般也不会出现不同的结果。互联网工程任务组(Internet Engineering Task Force)定义了HTTP错误的完整列表,详情请点击,从该文档中,我们可以了解4xx错误发生在请求存在问题时,而5xx错误则发生在服务端存在问题时。所以,我们只需要确保download在发生5xx错误时重试下载即可。下面是支持重试下载功能的新版本代码。

def download(url, num_retries=2):
    """下载函数,也会重试5xx错误。参数二为重试次数,默认为2次"""
    print("Downloading", url)
    try:
        html = urllib.request.urlopen(url).read()
    except urllib.request.URLError as e:    
        print("Download error:", e.reason)
        html = None
        if num_retries > 0:
            if hasattr(e, 'code') and 500 <= e.code < 600:
                #重试 5xx http错误
                html = download3(url, num_retries-1)
    return html

现在,当download函数遇到5xx错误码时,将会递归调用函数自身进行重试。此外,该函数还增加了一个参数,用于设定重试下载次数,其默认值为两次。我们在这里限制网页下载的尝试次数,是因为服务器错误可能暂时还没有解决。想要测试该函数,可以尝试下载http://httpstat.us/500,该网站会始终返回500错误码。

1. 确定爬取策略

进入多个日剧,可以查看到每个剧的网址都是如下形式:

必威官网亚洲体育 5

可以看出,每个日剧网页都对应一个编号。

因此我们可以通过遍历编号来爬取。

番剧详情页面(1)

在前面的页面上,我们点击某一番剧进入页面,如下:

必威官网亚洲体育 6

1.PNG

用F12查看源代码,得到这一部分的HTML代码:

必威官网亚洲体育 7

2.PNG

接下来分析一下,这段HTML的结构是这样的:

  • 番剧名:class为spay的div标签,里面的a标签
  • 封面链接:class为tpic l的div标签下的img标签
  • 作者,状态等详细信息: class为alex的div标签下的span标签数组
    同样先新建一个实体对象用来保存这些信息:
public class BangumiInfoEntity {
    String name;//名字
    String cover;//封面
    String all;//全集
    String autor;//作者
    String type;//类型
    String state;//状态
    String version;//版本
}

用前面得到的URL访问这个页面:

getItemInfo(String url){
 Connection conn = Jsoup.connect(url);
            // 修改http包中的header,伪装成浏览器进行抓取
            conn.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:32.0) Gecko/    20100101 Firefox/32.0");
            Document doc = null;
            try {
                doc = conn.get();
            } catch (IOException e) {
                e.printStackTrace();
            }
}

然后就可以用爬虫来解析的这段代码了,还是和上面一样,先是获取spay div下的a标签得到番剧名,然后获取tpic l div(注意:这里两个class用.链接)下的img标签用attr方法获取src属性得到封面图片链接最后获取alex div 下的span数组,遍历得到番剧的详情信息。

BangumiInfoEntity bangumiInfoEntity =new BangumiInfoEntity();
bangumiInfoEntity.setName(doc.select("div.spay").select("a").text());
bangumiInfoEntity.setCover(doc.select("div.tpic.l").select("img").attr("src"));
Elements noteList = doc.select("div.alex").select("span");
bangumiInfoEntity.setAll(noteList.get(0).text());
bangumiInfoEntity.setState(noteList.get(1).text());
bangumiInfoEntity.setAutor(noteList.get(2).text());
bangumiInfoEntity.setVersion(noteList.get(3).text());
bangumiInfoEntity.setType(noteList.get(4).text());

设置用户代理

默认情况下,urllib使用Python-urllib/3.5作为用户代理下载网页内容,其中3.5是Python的版本号。如果能使用可辨识的用户代理则更好,这样可以避免我们的网络爬虫碰到一些问题。此外,也许是因为曾经经历过质量不佳的Python网络爬虫造成的服务器过载。一些网站还会封禁这个默认的用户代理。比如,在使用Python默认用户代理的情况下,访问http://www.meetup.com/,目前会返回的提示是Forbidden。
因此,为了下载更加可靠,我们需要控制用户代理的设定。下面的代码对download函数进行了修改,设定了一个默认的用户代理"wswp"(即Web Scraping with Python的首字母缩写)。

def download4(url, user_agent='wswp', num_retries=2):
    """包括用户代理支持的下载函数"""
    print("Downloading:", url)
    headers = {'User-agent': user_agent}
    request = urllib.request.Request(url, headers=headers)
    try:
        html = urllib.request.urlopen(request).read()
    except urllib.request.URLError as e:
        print("Download error:", e.reason)
        html = None
        if num_retries > 0:
            if hasattr(e, 'code') and 500 <= e.code < 600:
                # 重试 5xx http错误
                html = download4(url, user_agent, num_retries-1)
    return html

现在,我们拥有了一个灵活的下载函数,可以在后续示例中得到复用。该函数能够捕获异常、重试下载并设置用户代理。

2. 获取日剧的名字

打开其中一个日剧的网页,查看标题的源代码如下:

必威官网亚洲体育 8

可以看到,标题的标签ID为"pdtname", 我们只要获取该标签的文本即可获取日剧名字

通过beautifulSoup的接口,获取该标签内容(去除了名字中多余东西)

 1     # try get tv name
 2     tag_name = soup.find(id='pdtname')
 3     if None == tag_name:
 4         print('tv_{:0>4d}: not exist.'.format(num))
 5         return None
 6 
 7     # remove signs not need
 8     name = tag_name.get_text().replace(' ', '') 
 9     try:
10         name = name.replace(re.search('【.*】', name).group(0), '') 
11         name = name.replace(re.search('(.*)', name).group(0), '') 
12         name = name.replace('《', '') 
13         name = name.replace('》', '') 
14         name = name.replace('/', '') 
15     except :
16         pass

 

3. 获取资源链接

在每个日剧页面中同时也包含了资源链接的地址,查看源代码如下:

必威官网亚洲体育 9

可以看到资源链接使用了一个表块,并且表块的ID为"ajax_tbody"

其中每一集都是表的行元素,每一行又包含了几列来显示资源的各个信息

我们通过遍历表的元素来获取每一集的资源链接

    # try get tv resources list
    tag_resources = soup.find(id='ajax_tbody')
    if None == tag_resources:
        print('tv_{:0>4d}: has no resources.'.format(num))
        return None

    # walk resources
    for res in tag_resources.find_all('tr'):

        # get link tag
        tag_a = res.find('a')
        info = res.find_all('td')
        print('resource: ', tag_a.get_text())

        # get download link
        downlink = get_resources_link(session, tag_a.get('href'))

        # record resouces
        tv.resources.append([tag_a.get_text(), info[2].get_text(), downlink, ''])
        delay(1)

 

4. 获取下载链接

点击其中一个资源,进入下载链接页面,查看源代码如下

必威官网亚洲体育 10

可以看到电驴的下载链接标签ID为"emule_url",因此我们只需要获取该标签的文本就可以了(磁力链接类似)

不过首先我们还需要先获取该下载页面,整体操作代码如下

def get_resources_link(session, url):
    ''' get tv resources download link  '''

    global domain
    res_url = domain + url

    # open resources page
    resp = session.get(res_url, timeout = 10)
    resp.raise_for_status()

    soup = page_decode(resp.content, resp.encoding)

    tag_emule = soup.find(id='emule_url')
    return tag_emule.get_text() if tag_emule != None else ''

 

5. 将资源下载链接保存到本地

其中,由于爬取所有日剧的下载链接比较耗时,前面做了判断可以只爬取标题,日后根据序号再爬取下载链接

def save_tv(tv):
    ''' save tv infomation on disk '''

    filename = os.path.join(os.path.abspath(save_dir), '{:0>4d}_{}.txt'.format(tv.num, tv.name))

    global only_catalog
    if only_catalog == True:
        with open(filename, 'a+') as f:
            pass
    else:
        with open(filename, 'w') as f:
            for info in tv.resources:
                f.write(os.linesep.join(info))
                f.write('========' + os.linesep)

以上,就是整个爬取脚本的开发过程。

 

欢迎关注我的代码仓库:

以后还会开发其余网站的爬取脚本。

 

番剧详情页面(2)

我们已经获取到了番剧的详细信息,接下来,我们要获取的就是番剧下的资源信息了,页面如下:

必威官网亚洲体育 11

3.PNG

用F12查看源代码,得到这一部分的HTML代码:

必威官网亚洲体育 12

4.PNG

接下来分析一下,这段HTML的结构是这样的:所有的资源都是在tabs的div下,在其下面,资源来源在menu0的ul标签下,资源的集数在main0的div标签下,一一对应。并且,main0的div下用若干的li标签,每个li标签用a标签包裹每集的集数以及链接。
新建一个实体类,用来保存集数以及链接的信息:

public class BangumiEpisodeEntity {
    String num;//第几话
    String url;//地址
}

同样的,用番剧列表页面得到的链接访问这个页面:

getItemList(String url){
Connection conn = Jsoup.connect(url);
          // 修改http包中的header,伪装成浏览器进行抓取
          conn.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:32.0) Gecko/    20100101 Firefox/32.0");
          Document doc = null;
          try {
              doc = conn.get();
          } catch (IOException e) {
              e.printStackTrace();
          }
}

最后,用一个title的一维数组保存资源来源信息,用一个child的二维数组保存资源每一集的信息。先获取class为menu0下的ul标签数组,遍历将里面class为on的li标签存入title数组。然后获取class为main0下的div数组,再遍历下面的li标签,分别获取li标签下的a标签的text以及href属性,得到剧集名,以及剧集链接。

ArrayList<String> title=new ArrayList();
ArrayList<ArrayList<BangumiEpisodeEntity>> child=new ArrayList<ArrayList<BangumiEpisodeEntity>>();
            Elements noteList = doc.select("div.tabs");
            Elements ul = noteList.select("ul.menu0");
            Elements div = noteList.select("div.main0");
            for (Element ulElement : ul) {
                title.add(ulElement.select("li.on").text());
            }
            for(Element divElement:div){
                Elements li=divElement.select("li");
                ArrayList<BangumiEpisodeEntity> bangumiEpisodeEntities=new ArrayList<BangumiEpisodeEntity>();
                for (Element liElement : li) {
                    BangumiEpisodeEntity bangumiEpisodeEntity=new BangumiEpisodeEntity();
                    bangumiEpisodeEntity.setNum(liElement.select("a").text());
                    bangumiEpisodeEntity.setUrl(liElement.select("a").attr("abs:href"));
                    bangumiEpisodeEntities.add(bangumiEpisodeEntity);
                }
                child.add(bangumiEpisodeEntities);
            }

番剧详情页面的UI部分,上半部分用的是glide加载图片,下半部分用的是ExpandableListView展示各个资源来源下的番剧信息。

网站地图爬虫

在第一个简单的爬虫中,我们将使用示例网站robot.txt文件中发现的网站地图来下载所有网页。为了解析网站地图,我们将会使用一个简单的正则表达式,从<loc>标签中提取出URL。

import re
from common import download

def crawl_sitemap(url):
    #下载sitemap文件
    sitemap = download(url).decode('utf-8')
    #抓取站点地图链接
    links = re.findall('<loc>(.*?)</loc>', sitemap)
    # 下载每一个链接
    for link in links:
        html = download(link)
        # scrape html here
        # ...

现在,运行网站地图爬虫,从示例网站中下载所有国家页面。

>>> crawl_sitemap('http://example.webscraping.com/sitemap.xml')
ownloading: http://example.webscraping.com/sitemap.xml
Downloading: http://example.webscraping.com/view/Afghanistan-1
Downloading: http://example.webscraping.com/view/Aland-Islands-2
Downloading: http://example.webscraping.com/view/Albania-3
Downloading: http://example.webscraping.com/view/Algeria-4
...

可以看出,上述运行结果和我们的预期一致,不过正如前文所述,我们无法依靠Sitemap文件提供每个网页的链接。下一节中,我们将会介绍另一个简单的爬虫,该爬虫不在依赖于Sitemap文件。

附录

整体代码:

  1 #!/usr/bin/python3
  2 # -*- coding:utf-8 -*-
  3 
  4 import os
  5 import sys
  6 import re
  7 import requests
  8 from bs4 import BeautifulSoup
  9 from time import sleep
 10 
 11 # website domain
 12 domain = 'http://www.zhuixinfan.com/'
 13 
 14 # spide infomation save directory
 15 save_dir = './tvinfo/'
 16 
 17 # only tv catalog
 18 only_catalog = False
 19 
 20 class TVInfo:
 21     ''' TV infomation class'''
 22 
 23     def __init__(self, num, name):
 24         self.num = num
 25         self.name = name
 26         self.resources = []
 27 
 28 
 29 def delay(seconds):
 30     ''' sleep for secondes '''
 31 
 32     while seconds > 0:
 33         sleep(1)
 34         seconds = seconds - 1
 35 
 36 def page_decode(content, encoding):
 37     ''' decode page '''
 38 
 39     # lxml may failed, then try html.parser
 40     try:
 41         soup = BeautifulSoup(content, 'lxml', from_encoding=encoding)
 42     except:
 43         soup = BeautifulSoup(content, 'html.parser', from_encoding=encoding)
 44 
 45     return soup
 46 
 47 def open_home_page(session):
 48     ''' open home page first as humain being '''
 49 
 50     global domain
 51     home_url = domain + 'main.php'
 52     
 53     # open home page
 54     resp = session.get(home_url, timeout = 10)
 55     resp.raise_for_status()
 56 
 57     # do nothing
 58 
 59 def get_resources_link(session, url):
 60     ''' get tv resources download link  '''
 61     
 62     global domain
 63     res_url = domain + url
 64 
 65     # open resources page
 66     resp = session.get(res_url, timeout = 10)
 67     resp.raise_for_status()
 68 
 69     soup = page_decode(resp.content, resp.encoding)
 70 
 71     tag_emule = soup.find(id='emule_url')
 72     return tag_emule.get_text() if tag_emule != None else ''
 73 
 74 
 75 def spider_tv(session, num):
 76     ''' fetch tv infomaion '''
 77 
 78     global domain
 79     tv_url = domain + 'viewtvplay-{}.html'.format(num)
 80     
 81     # open tv infomation page
 82     resp = session.get(tv_url, timeout = 10)
 83     resp.raise_for_status()
 84 
 85     soup = page_decode(resp.content, resp.encoding)
 86 
 87     # try get tv name
 88     tag_name = soup.find(id='pdtname')
 89     if None == tag_name:
 90         print('tv_{:0>4d}: not exist.'.format(num))
 91         return None
 92     
 93     # try get tv resources list
 94     tag_resources = soup.find(id='ajax_tbody')
 95     if None == tag_resources:
 96         print('tv_{:0>4d}: has no resources.'.format(num))
 97         return None
 98 
 99     # remove signs not need
100     name = tag_name.get_text().replace(' ', '')
101     try:
102         name = name.replace(re.search('【.*】', name).group(0), '')
103         name = name.replace(re.search('(.*)', name).group(0), '')
104         name = name.replace('《', '')
105         name = name.replace('》', '')
106         name = name.replace('/', '')
107     except :
108         pass
109 
110     print('tv_{:0>4d}: {}'.format(num, name))
111 
112     tv = TVInfo(num, name)
113 
114     global only_catalog
115     if only_catalog == True:
116         return tv
117 
118     # walk resources
119     for res in tag_resources.find_all('tr'):
120 
121         # get link tag
122         tag_a = res.find('a')
123         info = res.find_all('td')
124         print('resource: ', tag_a.get_text())
125 
126         # get download link
127         downlink = get_resources_link(session, tag_a.get('href'))
128 
129         # record resouces
130         tv.resources.append([tag_a.get_text(), info[2].get_text(), downlink, ''])
131         delay(1)
132     
133     return tv
134 
135 
136 def save_tv(tv):
137     ''' save tv infomation on disk '''
138 
139     filename = os.path.join(os.path.abspath(save_dir), '{:0>4d}_{}.txt'.format(tv.num, tv.name)) 
140     
141     global only_catalog
142     if only_catalog == True:
143         with open(filename, 'a+') as f:
144             pass
145     else:
146         with open(filename, 'w') as f:
147             for info in tv.resources: 
148                 f.write(os.linesep.join(info))
149                 f.write('========' + os.linesep)
150 
151 def main():
152     
153     start = 1
154     end = 999
155 
156     if len(sys.argv) > 1:
157         start = int(sys.argv[1])
158     
159     if len(sys.argv) > 2:
160         end = int(sys.argv[2])
161 
162     global only_catalog
163     s = input("Only catalog ?[y/N] ")
164     if s == 'y' or s == 'Y':
165         only_catalog = True
166 
167     # headers: firefox_58 on ubuntu
168     headers = {
169         'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:58.0)' 
170                 + ' Gecko/20100101 Firefox/58.0',
171         'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
172         'Accept-Language': 'zh-CN,en-US;q=0.7,en;q=0.3',
173         'Accept-Encoding': 'gzip, deflate',
174         }
175     
176     # create spider session
177     with requests.Session() as s:
178 
179         try:
180             s.headers.update(headers)
181             open_home_page(s)
182             for num in range(start, end+1):
183                 delay(3)
184                 tv = spider_tv(s, num)
185                 if tv != None:
186                     save_tv(tv)
187 
188         except Exception as err:
189             print(err)
190             exit(-1)
191     
192 if __name__ == '__main__':
193     main()

 

: 追新番网站资源链接爬取,python爬虫 “追新番”网站 追新番网站提供最新的日剧和日影下载地址,更新比较快。 个人比较喜欢...

播放页面

这个部分就比较头大了,我原本的想法是获取视频的真实地址,然后直接用播放器播放,岂不是美滋滋。然而,我仔细研究了一下他的html页面,发现他视频播放时这样做的:

必威官网亚洲体育 13

5.PNG

就是说他的视频播放使用js代码动态注入的,然后用flash播放的,对于我这个对JS只了解皮毛的来说研究不透。对于如何获取视频的真实地址,希望有大佬能讲一下这方面的思路。
最后我就只能用webview直接加载视频播放页面了,但是新的问题就又来了,用webview倒是能加载,但是因为是山寨网站,网站下面一堆广告,看起来很不爽。我就又研究了一下,他的广告加载方式。。结果广告也是用js动态注入的,看不明白。于是我就用了一个很挫的方法解决了这个广告问题,因为广告都是在页面的上方,所以我只保留播放器上面的div不就得了。具体实现是这样的

WebView wv;
wv.setWebViewClient(new WebViewClient() {
            @Override
            public void onPageFinished(WebView view, String url) {
                                view.loadUrl("javascript:function setTop(){var x=document.getElementsByTagName("div");" +
                        "for (var i=8;i<x.length;i++){x[i].style.display="none";}}setTop();");
            }
        });
wv.setWebChromeClient(new WebChromeClient() {
            @Override
            public void onProgressChanged(WebView view, int newProgress) {
                view.loadUrl("javascript:function setTop(){var x=document.getElementsByTagName("div");" +
                        "for (var i=8;i<x.length;i++){x[i].style.display="none";}}setTop();");
            }
        });

在webview加载过程和加载完成后注入js代码,查找所有div标签,然后把播放器一下所有的div标签都屏蔽掉。

最后附上项目github地址:github

ID遍历爬虫

本节中,我们将利用网站结构的弱点,更加轻松地访问所有内容。下面是一些示例国家的URL。

  • Downloading: http://example.webscraping.com/view/Afghanistan-1
  • Downloading: http://example.webscraping.com/view/Aland-Islands-2
  • Downloading: http://example.webscraping.com/view/Albania-3
    可以看出,这些URL只是在结尾出有所区别。包括国家名(作为页面别名)和ID。在URL中包含页面别名是非常普遍的做法。可以对搜索引擎优化起到帮助作用。一般情况下,Web服务器会忽略这个字符串,只使用ID来匹配数据库中的相关记录。下面我们将其移除,加载http://example.webscraping.com/view/1,测试示例网站中的链接是否仍然可用。如果我们加载成功,证明该方法是有用的。下载,我们就可以忽略别名。只遍历ID来下载所有国家的页面。下面是使用了该技巧的代码片段。
# -×- coding: utf-8 -*-
import itertools
from common import download
def iteration():
    for page in itertools.count(1):
        url = 'http://example.webscraping.com/view/-{}'.format(page)
        html = download(url)
        if html is None:
            # 尝试下载此网站时收到的错误
            # 所以假设已达到最后一个国家ID,并可以停止下载
            break
        else:
            # 成功 - 能够刮结果
            # ...
            pass

在这段代码中,我们对ID进行遍历,直到出现下载错误时停止,我们假设此时已到达最后一个国家的页面。不过,这种实现方式存在一个缺陷,那就是某些记录可能已被删除,数据库ID之间并不是连续的。此时,只要访问到某个间隔点,爬虫就会立即退出。下面是这段代码的改进版本,在该版本中连续发生多次下载错误后才退出程序。

def iteration2():
    max_errors = 5 # 允许最大连续下载错误数
    num_errors = 0 # 当前连续下载错误数
    for page in itertools.count(1):
        url = 'http://example.webscraping.com/view/-{}'.format(page)
        html = download(url)
        if html is None:
            # 尝试下载此网页时出错
            num_errors += 1
            if num_errors == max_errors:
                # 达到最大错误数时,退出
                break
            # 所以假设已达到最后一个ID,并可以停止下载
        else:
            # 成功 - 能够刮到结果
            # ...
            num_errors = 0

上面代码中实现的爬虫需要连续5次下载错误才会停止遍历,这样就很大程度上降低了遇到被删除记录时过早停止遍历的风险。
在爬取网站时,遍历ID是一个很便捷的方法,但是和网站地图爬虫一样,这种方法也无法保证始终可用。比如,一些网站会检查页面别名是否满足预期,如果不是,则会返回404 Not Found 错误。而另一些网站则会使用非连续大数作为ID,或是不使用数值作为ID,此时遍历就难以发挥作用了。例如,Amazon使用ISBN作为图书ID,这种编码包好至少10位数字。使用ID对Amazon的图书进行遍历需要测试数十亿次,因此这种方法肯定不是抓取该网站内容最高效的方法。

链接爬取

到目前为止,我们已经利用示例网站的结构特点实现了两个简单爬虫,用于下载所有的国家页面。只要这两种技术可用,就应当使用其进行爬取,因为这两种方法最小化了需要下载的网页数量。不过,对于另一些网站,我们需要让爬虫表现的更像普通用户,跟踪链接,访问感兴趣的内容。
通过跟踪所有链接的方式,我们可以很容易地下载种鸽网站的页面。但是,这种方法会下载大量我们并不需要的网页。例如,我们想要从一个在线论坛中抓取用户账号详情页,那么此时我们只需要下载账号页,而不需要下载讨论帖的页面。本节中的链接爬虫将使用正则表达式来确定需要下载那些页面。下面时这段代码的初始版本。

import re
from common import download
def link_crawler(seed_url, link_regex):
    """从指定的种子网址按照link_regex匹配的链接进行抓取"""
    crawal_queue = [seed_url] # 要下载的URL队列
    while crawal_queue:
        url = crawal_queue.pop()
        html = download(url).decode('utf-8')
        # 使用过滤器来匹配我们的正则表达式
        for link in get_links(html):
            if re.match(link_regex, link):
                # 将这个链接添加到爬网队列
                crawal_queue.append(link)
def get_links(html):
    """从HTML返回一个链接列表"""
    # 从网页提取所有链接的正则表达式
    webpage_regex = re.compile('<a[^>]+href=["'](.*?)["']', re.IGNORECASE)
    # 来自网页的所有链接的列表
    return webpage_regex.findall(html)

要运行这段代码,只需要调用link_crawler函数,并传入两个参数,要爬取的网站URL和用于跟踪链接的正则表达式。对于示例网站,我们想要爬取的是国家列表索引页和国家页面。其中,索引页链接格式如下。

  • http://example.webscraping.com/index/1
  • http://example.webscraping.com/index/2
    国家页链接格式如下。
  • http://example.webscraping.com/view/Afghanistan-1
  • http://example.webscraping.com/view/Aland-Islands-2
    因此,我们可以用/(index|view)/这个简单的正则表达式来匹配这两类网页。当爬虫使用这些输入参数运行时会发生什么呢?你会发现我们得到了如下的下载错误。
>>> link_crawler('http://example.webscraping.com', '/(index|view)')
Downloading: http://example.webscraping.com
Downloading: /index/1
Traceback (most recent call last):
 ...
ValueError: unknown url type: '/index/1'

可以看出,问题处在下载/index/1时,该链接只有网页的路径部分,而没有协议和服务器部分,也就是说这是一个相对链接。由于浏览器知道你正在浏览哪个网页,所以在浏览器浏览时,相对链接是能够正常工作的。但是,urllib是无法获知上下文的。为了让urllib能够定位网页,我们需要将链接转换为决定链接的形式,以便包含定位网页的所有细节。如你所愿,Python中确实有用来实现这一功能的模块,该模块称为urlparse。下面是link_crawler的改进版本,使用了urlparse模块来创建绝对路径。

import urllib.parse
def link_crawler(seed_url, link_regex):
    """从指定的种子网址按照link_regex匹配的链接进行抓取"""
    crawal_queue = [seed_url] # 要下载的URL队列
    while crawal_queue:
        url = crawal_queue.pop()
        html = download(url).decode('utf-8')
        for link in get_links(html):
            # 检查链接是否匹配预期正则表达式
            if re.match(link_regex, link):
                # 形式绝对链接
                link = urllib.parse.urljoin(seed_url, link)
                crawal_queue.append(link)

当你运行这段代码时,会发现虽然网页下载没有出现错误,但是同样的地点总是会被不断下载到。这是因为这些地点相互之间存在链接。比如,澳大利亚链接到了南极洲,而南极洲也存在到澳大利亚的链接,此时爬虫就会在它们之间不断循环下去。要想避免重复爬取相同的链接,我们需要记录哪些链接已经被爬取过。下面是修改后的link_crawler函数,已具备存储已发现URL的功能,可以避免重复下载。

def link_crawler(seed_url, link_regex):
    """从指定的种子网址按照link_regex匹配的链接进行抓取"""
    crawal_queue = [seed_url] # 要下载的URL队列
    seen = set(crawal_queue) # 跟踪以前看过的URL
    while crawal_queue:
        url = crawal_queue.pop()
        html = download(url).decode('utf-8')
        for link in get_links(html):
            # 检查链接是否匹配预期正则表达式
            if re.match(link_regex, link):
                # 形式绝对链接
                link = urllib.parse.urljoin(seed_url, link)
                # 检查是否已经看过该链接
                if link not in seen:
                    seen.add(link)
                    crawal_queue.append(link)

当运行该脚本时,它会爬取所有地点,并且能够如期停止。最终,我们得到了一个可用的爬虫!

高级功能

现在,让我们为链接爬虫添加了一些功能,使其在爬取其它网站时更加有用。

解析robots.txt

首先,我们需要解析robots.txt文件,以避免下载禁止爬取的URL。使用Python自带的robotparser模块,就可以轻松完成这项工作,如下图的代码所示。

>>> import robotparser
>>> rp = robotparser.RobotFileParser()
>>> rp.set_url('http://example.webscraping.com/robots.txt')
>>> rp.read()
>>> url = 'http://example.webscraping.com'
>>> user_agent = 'BadCrawler'
>>> rp.can_fetch(user_agent, url)
False
>>> user_agent = 'GoodCrawler'
>>> rp.can_fetch(user_agent, url)
True

rotbotparser模块首先加载robots.txt文件,然后通过can_fetch()函数确定指定的用户代理是否允许访问网页。在本例中,当用户代理设置为‘BadCrawler’时,robotparser模块会返回结果表明无法获取网页,这和示例网站robots.txt的定义一样。
为了将该功能集成到爬虫中,我们需要在crawl循环中添加该检查。

while crawal_queue:
        url = crawal_queue.pop()
        # 检查网址传递的robots.txt限制
        if rp.can_fetch(user_agent, url):
            ...
        else:
            print("Blocked by robots.txt", url)

支持代理

有时我们需要使用代理访问某个网站。比如,Netflix屏蔽了美国以外的大多数国家。使用urllib.request支持代理并没有想象中的那么容易(可以尝试使用更友好的Python HTTP模块 requests来实现该功能,点击跳转文档地址。下面是使用urllib.request支持代理的代码。

proxy = ...
opener = urllib.request.build_opener()
proxy_params = {urllib.parse.urlparse(url).scheme: proxy}
opener.add_handler(urllib.request.ProxyHandler(proxy_params))
response = opener.open(request)

下面是集成了该功能的新版本 download 函数:

# -*- coding: utf-8 -*-
import urllib.request
import urllib.parse
def download5(url, user_agent='wswp',proxy=None, num_retries=2):
    """支持代理功能的下载函数"""
    print("Downloading:", url)
    headers = {'User-agent': user_agent}
    request = urllib.request.Request(url, headers=headers)
    opener = urllib.request.build_opener()
    if proxy:
        proxy_params = {urllib.parse.urlparse(url).scheme: proxy}
        opener.add_handler(urllib.request.ProxyHandler(proxy_params))
    try:
        html = opener.open(request).read()
    except urllib.request.URLError as e:
        print("Download error:", e.reason)
        html = None
        if num_retries > 0:
            if hasattr(e, 'code') and 500 <= e.code < 600:
                # 重试 5xx HTTP 错误
                html = download5(url, user_agent, proxy, num_retries-1)
    return html

下载限速

如果我们爬取网站的速度过快,就会面临被封禁或是造成服务器过载的风险。为了降低这些风险,我们可以在两次下载之间添加延时,从而对爬虫限速。下面是实现了该功能的类的代码。

class Throttle:
    def  __init__(self, delay):
        self.delay = delay
        self.domains = {}

    def wait(self, url):
        domain = url.parse.urlparse(url).netloc
        last_accessed = self.domains.get(domain)
        if self.delay > 0 and last_accessed is not None:
            sleep_secs = self.delay - (datetime.now() - last_accessed).seconds
            if sleep_secs > 0:
                time.sleep(sleep_secs)
        self.domains[domain] = datetime.now()

     def get_links(html):
        webpage_regex = re.compile('<a[^>]+href=["'](.*?)["']', re.IGNORECASE)
        return webpage_regex.findall(html)

Throttle 类记录了每个域名上次访问的时间,如果当前时间距离上次访问时间小于指定延时,则执行睡眠操作。我们可以在每次下载之前调用Throttle对爬虫进行限速。

throttles = Throttle(delay)
...
throttle.wait(url)
result = download(url, headers, proxy = proxy, num_retries=num_retries)

避免爬虫陷阱

目前,我们的爬虫会跟踪所有之前没有访问过的链接。但是,一些网站会动态生成页面内容,这样就会出现无限多的网页。比如,网站有一个在线日历功能,提供了可以访问下个月和下一年的链接,那么下个月的页面中同样会包含访问下个月的链接,这样页面就会无止境地链接下去。这种情况被称为爬虫陷阱。
想要避免陷入爬虫陷阱,一个简单的方法是记录到达当前网页经过了多少个链接,也就是深度。当到达最大深度时,爬虫就不在向队列中添加该网页中的链接了。要实现这一功能,我们需要修改seen变量。该变量原先只记录访问过的网页链接,现在修改为一个字典,增加了页面深度的记录。

def link_crawler(.... max_depth=2):
    max_depth = 2
    seen = {}
    ...
    depth = seen[url]
    if depth != max_depth:
            for link in links:
                if link not in seen:
                    seen[link] = depth + 1
                    crawl_queue.append(link)

现在有了这一功能,我们就有信心爬虫最终一定能够完成。如果想要禁用该功能,只需将max_depth设为一个负数即可,此时当前深度永远不会与之相等。

最终版本

这个高级链接爬虫的完整源代码可以在此下载。要测试这段代码,我们可以将用户代理设置为BadCrawler,也就是本章前文所述的被robots.txt屏蔽了的那个用户代理。从下面的运行结果中可以看出,爬虫果然被屏蔽了,代码启动后马上就会结束。

>>> seed_url = 'http://example.webscraping.com/index'
>>> link_regex = '/(index|view)'
>>> link_crawler(seed_url, link_regex, user_agent='BadCrawlar')
Blocked by robots.txt: http://example.webscraping.com/

现在,让我们使用默认的用户代理,并将最大深度设置为1,这样只有主页上的链接才会被下载。

>>> link_crawler(seed_url, link_regex, max_depth=1)

和预期一样,爬虫在下载完国家列表的第一页之后就停止了。

最终链接爬虫的代码如下:

import re
import urllib.parse
import urllib.request
import urllib.robotparser
import time
from datetime import datetime
import queue


def link_crawler(seed_url, link_regex=None, delay=5, max_depth=-1, 
        max_urls=-1, headers=None, user_agent='wswp', proxy=None, num_retries=1):
    """从指定的种子网址按照link_regex匹配的链接进行抓取"""
    crawal_queue = queue.deque([seed_url]) # 仍然需要抓取的网址队列
    seen = {seed_url: 0} # 已经看到的网址以及深度
    num_urls = 0 # 跟踪已下载了多少个URL
    rp = get_robots(seed_url)
    throttle = Throttle(delay)
    headers = headers or {}
    if user_agent:
        headers['User-agent'] = user_agent

    while crawal_queue:
        url = crawal_queue.pop()
        # 检查网址传递的robots.txt限制
        if rp.can_fetch(user_agent, url):
            throttle.wait(url)
            html = download(url, headers, proxy=proxy, num_retries=num_retries)
            links = []

            depth = seen[url]
            if depth != max_depth:
                # 仍然可以进一步爬行
                if link_regex:
                    # 过滤符合我们的正则表达式的链接
                    links.extend(link for link in get_links(html) if re.match(link_regex, link))

                for link in links:
                    link = normalize(seed_url, link)
                    # 检查是否已经抓取这个链接
                    if link not in seen:
                        seen[link] = depth + 1
                        # 检查链接在同一域内
                        if same_domain(seed_url, link):
                            # 成功! 添加这个新链接到队列里
                            crawal_queue.append(link)

            # 检查是否已达到下载的最大值
            num_urls += 1
            if num_urls == max_urls:
                break
        else:
            print("Blocked by robots.txt:", url) # 链接已被robots.txt封锁

class Throttle:
    """Throttle通过睡眠在请求之间下载同一个域"""
    def __init__(self, delay):
        """每个域的下载之间的延迟量"""
        self.delay = delay
        # 上次访问域时的时间戳
        self.domains = {}

    def wait(self, url):
        domain = urllib.parse.urlparse(url).netloc
        last_accessed = self.domains.get(domain)

        if self.delay > 0 and last_accessed is not None:
            sleep_secs = self.delay - (datetime.now() - last_accessed).seconds
            if sleep_secs > 0:
                time.sleep(sleep_secs)
        self.domains[domain] = datetime.now()

def download(url, headers, proxy, num_retries, data=None):
    print("Downloading:", url)
    request = urllib.request.Request(url, data, headers)
    opener = urllib.request.build_opener()
    if proxy:
        proxy_params = {urllib.parse.urlparse(url).scheme: proxy}
        opener.add_handler(urllib.request.ProxyHandler(proxy_params))
    try:
        response = opener.open(request)
        html = response.read()
        code = response.code
    except urllib.request.URLError as e:
        print("Download error:", e.reason)
        html = ''
        if hasattr(e, 'code'):
            code = e.code
            if num_retries > 0 and 500 <= code < 600:
                # 重试 5xx HTTP 错误
                return download(url, headers, proxy, num_retries-1, data)
        else:
            code = None
    return html

def normalize(seed_url, link):
    """通过删除散列和添加域来规范化此URL"""
    link, _ = urllib.parse.urldefrag(link) # 删除散列以避免重复
    return urllib.parse.urljoin(seed_url, link)

def same_domain(url1, url2):
    """如果两个网址属于同一域,则返回True"""
    return urllib.parse.urlparse(url1).netloc == urllib.parse.urlparse(url2).netloc

def get_robots(url):
    """初始化此域的机器人解析器"""
    rp = urllib.robotparser.RobotFileParser()
    rp.set_url(urllib.parse.urljoin(url, '/robots.txt'))
    rp.read()
    return rp

def get_links(html):
    """从HTML返回一个链接列表"""
    # 从网页提取所有链接的正则表达式
    webpage_regex = re.compile('<a[^>]+href=["'](.*?)["']', re.IGNORECASE)
    html = html.decode('utf-8')
    # 来自网页的所有链接的列表
    return webpage_regex.findall(html)