使用正则表达式来爬虫

前言

在上面我们简单的了解了一下正则表达式的使用。其中最后的一个例子中我们讲的是使用正则表达式来匹配html中标签。我们或许可以使用这个功能来干一件大事——使用Java来爬一个简单的虫。这个是不需要任何库的那种,就是用Java中的IO流和强大的正则表达式。不过,我们需要爬取什么样子的网站呢?一开始就想到了我最爱的网站bilibili。那么说干就干吧。来爬一下bilibili

爬取哔哩哔哩的失败

URL url = new URL("http://www.bilibili.com");
url.openStream();

虽然上面的代码是没有写全的。不过现在代码已经是出错的了。要么是b站返给我一个极短的没有用的html代码,要么就是直接给我一个403 Error,403错误代表的是访问权限不足,拒绝访问。

有着python爬虫经验的我,一下子就想到了这特喵的肯定是User-Agent的问题,不过如果配置Java中的User-Agent却是一个问题,而且我爬b站好像没啥用啊。我突然想到我玻璃的封面的图片的来源地——萌娘资源站。每次我去里面搞图片的时候都是一张一张的右键另存为,这很是头疼!!经过我的访问,我发现这个网站竟然没有任何的反爬措施,果然小网站就是小网站啊,就决定是你了!!

P.S.: 后来我查阅了资料,找到了Java中设置User-Agent的方式,果然好烦啊!爬虫还是python好。。。

try {
    URL url = new URL("https://www.bilibili.com");
    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
    connection.setRequestProperty("User-Agent",
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
                    "(KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36");
    BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
    String line = null;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }

} catch (IOException e) {
    e.printStackTrace();
}

爬取单页所有图片

虽然我先把这个网址连的所有的图片全都爬取下来,但是咋还是得一步一步的来。首先下载一张图片这种操作应该就不用写了,所以就从爬取单页的所有的图片开始入手吧。

于是我从这个网页开始入手http://moe.005.tv/78680.html分析网页的结构。

首先,我们需要爬取的是图片。HTML应该都是学过的,图片的地址就是在<img>标签的src属性下。通过查看源代码和使用开发者检查工具,我发现了我想要爬取的内容源代码是这样子的。

    <img alt="初音未来" src="http://www.005.tv/uploads/allimg/190902/66-1ZZ2150225T5.jpg" style="width: 600px; height: 705px;" /><br />
    ID=76539261<br />
    <br />
    <img alt="初音未来" src="http://www.005.tv/uploads/allimg/190902/66-1ZZ215025Wc.jpg" style="width: 600px; height: 920px;" /><br />
    ID=76539275<br />
    <br />
    <img alt="初音未来" src="http://www.005.tv/uploads/allimg/190902/66-1ZZ2150310O3.jpg" style="width: 600px; height: 849px;" /><br />
    ID=76539289<br />
    <br />
    <img alt="初音未来" src="http://www.005.tv/uploads/allimg/190902/66-1ZZ2150322Z9.png" style="width: 600px; height: 839px;" /><br />
    ID=76539340<br />
    <br />
    <img alt="初音未来" src="http://www.005.tv/uploads/allimg/190902/66-1ZZ21503334W.jpg" style="width: 600px; height: 848px;" /><br />
    ID=76539357<br />
    <br />
    <img alt="初音未来" src="http://www.005.tv/uploads/allimg/190902/66-1ZZ2150419601.jpg" style="width: 600px; height: 424px;" /><br />
    ID=76539479<br />
    <br />
    <img alt="初音未来" src="http://www.005.tv/uploads/allimg/190902/66-1ZZ2150434115.jpg" style="width: 600px; height: 414px;" /><br />
    ID=76539479<br />
    <br />
    <img alt="初音未来" src="http://www.005.tv/uploads/allimg/190902/66-1ZZ2150449D4.jpg" style="width: 600px; height: 424px;" /><br />
    ID=76540552<br />
    <br />
    <img alt="初音未来" src="http://www.005.tv/uploads/allimg/190902/66-1ZZ2150501231.jpg" style="width: 600px; height: 896px;" /><br />
    ID=76546567<br />
    <br />
    <img alt="初音未来" src="http://www.005.tv/uploads/allimg/190902/66-1ZZ2150513612.jpg" style="width: 600px; height: 600px;" /><br />
    ID=76549801<br />
    <br />
    <img alt="初音未来" src="http://www.005.tv/uploads/allimg/190902/66-1ZZ2150523104.jpg" style="width: 600px; height: 847px;" /><br />
    ID=76557430<br />
    <br />
    <img alt="初音未来" src="http://www.005.tv/uploads/allimg/190902/66-1ZZ2150533627.png" style="width: 600px; height: 891px;" /><br />
    ID=76563726<br />
    <br />
    <img alt="初音未来" src="http://www.005.tv/uploads/allimg/190902/66-1ZZ21505431R.jpg" style="width: 600px; height: 718px;" /><br />
    ID=76573133<br />

可以看到是一个单标签。其中我们真正需要的内容就是里面的src的网址,有了这些网址就相当于有了图片。

爬取网页的源代码

不过首先我们需要的是来下载这个网站的源代码。之前也说过了这个网页是没有反爬虫措施的,所以我们可以直接进行访问下载。

private String getUrlContent() {
    BufferedReader br = null;
    StringBuilder sb = new StringBuilder();
    try {
        br = new BufferedReader(new InputStreamReader(url.openStream()));
        String line = null;
        while ((line = br.readLine()) != null) {
            sb.append(line);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (br != null) {
                br.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return sb.toString();
}

上面就是一个简单的IO流的操作,其实应该是写成一个工具类的,毕竟后面的代码中还要用到这个方法。不过已经写完了那就无所谓了。

使用正则表达式匹配链接

上面的网页的源代码已经获得了,那么就需要使用到之前所学的正则表达式的知识匹配我们要的字符串了。

这里我写的表达式为<img[^>]*src=\"(.+?)\".*?/>

我将我要获取的连接放在了一个捕获组中(.+?)那么匹配玩之后我们就可以直接获取这个链接了。

关于上面的表达式其实还有一个问题,为什么我写的表达式这么简洁,不会匹配到其他的没有用的图片吗?还真的不会,我发现其他的无关的图片虽然前面多事满足的,但是结尾都是没有/的就是<img ...>。所有的我需要的图片都是<img ... />类型的,所以说这就很舒服的嘛!

private List<String> getImgUrl() {
    List<String> list = new ArrayList<String>();
    String urlContent = getUrlContent();
    Pattern pattern = Pattern.compile("<img[^>]*src=\"(.+?)\".*?/>");
    Matcher matcher = pattern.matcher(urlContent);
    while (matcher.find()) {
        String urlImg = matcher.group(1);
        if (urlImg.contains("dilidili")) {
            break;
        }
        if (urlImg.matches("http://.+?.(jpg|png)")) {
            list.add(urlImg);
        }
    }
    return list;
}

上面我使用了一个List来存放这些图片的链接。不过需要注意的是,链接中有dilidili的链接都是无法访问的,所以说直接剔除就好了。(莫非dilidili凉了??)

下载图片

上面已经获取到了所有的图片的连接了,下面只要运用之前学的IO流的知识来下载图片就行了。

public void downloadImg() throws Exception {
    List<String> list = getImgUrl();
    int i = 1;
    BufferedOutputStream bos = null;
    BufferedInputStream bis = null;
    for (String img : list) {
        String suffix = img.substring(img.length() - 4);
        File file = new File(dest, "" + i + suffix);
        bos = new BufferedOutputStream(new FileOutputStream(file));
        bis = new BufferedInputStream(new URL(img).openStream());
        byte[] buffer = new byte[1024];
        int cot;
        while ((cot = bis.read(buffer))!=-1) {
            bos.write(buffer,0, cot);
        }
        bos.flush();
        System.out.println("Download Complete: The "+i+" Img");
        i++;
    }
    bis.close();
    bos.close();
}

首先是文件名字的问题,既然网站上也没有名字,那我就直接使用数字命名大法吧,问题应该不大。

还有一个问题就是文件的后缀名的问题,经过上面的观察,这些图片不是jpg格式就是png格式,那么我只需要取最后的四位就可以得到图片的类型了。

后面就是IO流的操作了,这里就不说了。

爬取一整页的图片

经过我的观察这个网站中的源代码的格式基本上都是一模一样的。(那肯定的啊,没有框架写个锤子的网站)经过上面的努力,现在给我们任何一页的的链接,我们就可以下载这一页中的所有的图片。

于是现在我们的任务就是获取一整页中所有页的链接。还是之前获取图片的链接那样一样的操作。观察一下网页中我们需要的部分的源代码。

        <a href="http://moe.005.tv/78680.html" target="_blank"><span class="zt_pic" style="background-image:url(http://www.005.tv/uploads/190902/66-1ZZ2151605B7.jpg);"><img src="http://www.005.tv/uploads/190902/66-1ZZ2151605B7.jpg"><em>初音未来插画图片</em></span></a>
        <span class="zt_dep">
          <strong><a href="http://moe.005.tv/78680.html">初音未来插画图片</a></strong>
          <dl>
            <dt>2019-09-02</dt>
            <dd><i class="iconfont"></i>68</dd>
          </dl>
          <p>初音未来插画图片...</p>
        </span>
      </li><li>
        <a href="http://moe.005.tv/78638.html" target="_blank"><span class="zt_pic" style="background-image:url(http://www.005.tv/uploads/190822/66-1ZR2143A1X1.jpg);"><img src="http://www.005.tv/uploads/190822/66-1ZR2143A1X1.jpg"><em>【今日P站】热门插画0822</em></span></a>
        <span class="zt_dep">
          <strong><a href="http://moe.005.tv/78638.html">【今日P站】热门插画0822</a></strong>
          <dl>
            <dt>2019-08-22</dt>
            <dd><i class="iconfont"></i>3704</dd>
          </dl>
          <p>【今日插画】P站热门插画0822...</p>
        </span>
      </li><li>

上面是网页的源代码的一部分。观察一下,我们需要的链接在两个地方都出现了,上面的<a href="">中有我们需要的链接,下面的<strong>中的<a href="">也有我们需要的内容。不过下面的a中还有我们更想要的东——图片的名字。这么说或许有点儿不妥,应该说是文件夹的名字,这里图片是没有名字的。

爬取网页的源代码的操作和上面就是一模一样的,所以这里不再赘述。

使用正则表达式匹配链接

这里我使用的正则表达式是<strong><a href=\"(http://moe.005.tv/\\d{5}\\.html)\">(.*?)</a></strong>

这里因为我需要的有网页的链接还有名字这两个属性,所以我使用了两个捕获组,而且为了防止误匹配,其实链接的变化只是后面的五个数字,所以我直接精确的进行匹配,不搞那些.*了。

private void getLink() {
    List<String> list = new ArrayList<>();
    String urlContent = getUrlContent();
    Pattern pattern = Pattern.compile("<strong><a href=\"(http://moe.005.tv/\\d{5}\\.html)\">(.*?)</a></strong>");
    Matcher matcher = pattern.matcher(urlContent);
    while (matcher.find()){
        linkList.add(matcher.group(1));
        nameList.add(matcher.group(2));
    }
}

这里用了两个List,不过没法返回两个List。我就将这两个LIst作为私有属性而不是函数的返回值。

下载整页图片

上面我们已经获取了每一页的链接以及名字,我们就可以利用上面写好了的下面单页用的类进行下载了。这真的棒棒哒!

public void downloadImg() {
    getLink();
    for (int i = 0; i < linkList.size(); i++) {
        SpiderImg splitImg = new SpiderImg(linkList.get(i), dest+"/"+nameList.get(i));
        try {
            System.out.println("=====Download Start: "+nameList.get(i)+"=====");
            System.out.println();
            splitImg.downloadImg();
            System.out.println("=====Download Complete: "+nameList.get(i)+"=====");
            System.out.println();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

下载全站??

上面我们已经完成了一页的下载,那么下面我们的目标是???下载全站的资源??额,,,这个确实是有点儿过分了。不过我们可以想想如果硬盘和时间允许的情况下是否是可以的。

观察一下每一整页的链接。

第一页http://moe.005.tv/moeimg/tb/list_3_1.html

第二页http://moe.005.tv/moeimg/tb/list_3_2.html

……

第五十页http://moe.005.tv/moeimg/tb/list_3_50.html

观察一下第五十页的源代码,基本没有任何的变化。emm,,我感觉我真的可以下载全站。总共有126页。。。emm,算了算了。

public class SplitMorePage {
    public static void main(String[] args) {
        int n = 10;
        for (int i = 1; i <= n; i++) {
            System.out.println("!!!!!Now Downloading The " + i + " Page, Sum of " + n + "Page!!!!!");
            String url = "http://moe.005.tv/moeimg/tb/list_3_" + i + ".html";
            SpiderPage spider = new SpiderPage(url, "D:/JavaSpider/Page3_" + i);
            spider.downloadImg();
            System.out.println("!!!!!The " + i + " Page Download Complete,Sum of " + n + "Page!!!!!");
        }
    }
}

我下载了几页试了一下,发现已经下载了一千多张图片了,也占用了很大的空间。emm,,一百多页,,算了算了不爬了!

使用Python

上面使用Java爬虫确实是有点儿繁琐的用了将近二百行的代码。说起爬虫这种玩意,可以说是Python的天下啊。那么我就简单的使用python来还原一下上面的Java一样的操作。

这里使用的是urllib.request库,没有使用requests库。

爬取单页所有的图片

from urllib import request
import re
import os

def getUrlContent(url):
    headers = {
        'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36'
    }

    req = request.Request(url, headers = headers)
    resp = request.urlopen(req)
    urlContent = resp.read().decode('utf-8')
    return urlContent

def getImgUrlList(urlContent):
    pattern = re.compile(r'<img[^>]*src="(.+?)".*?/>')
    imgUrlList = pattern.findall(urlContent)
    return imgUrlList


def downloadImg(imgUrl, cot, dest):
    suffix = imgUrl[-3:]
    request.urlretrieve(imgUrl, dest+str(cot)+'.'+suffix)


def downloadImgs(imgUrlList, dest):
    if not os.path.exists(dest):
        os.makedirs(dest)
    for cot,imgUrl in enumerate(imgUrlList):
        if 'dilidili' in imgUrl:
            continue
        else:
            downloadImg(imgUrl, cot+1, dest)
            cot += 1

def download(url, dest):
    urlContent = getUrlContent(url)
    imgUrlList = getImgUrlList(urlContent)
    downloadImgs(imgUrlList, dest)

if __name__ == '__main__':
    url = 'http://moe.005.tv/78680.html'
    dest = 'img/'
    download(url, dest)

可以看到python的代码明显简单多了,而且还可以简单的指定User-Agent。关键是图片的下载只需要一行代码request.urlretrieve(url, file),不像Java还要使用复杂的IO流操作。(其实Java也可以,只是我没有使用Commons IO罢了,python的函数如此简洁好用,只是因为已经包装好了而已)

爬取一整页的图片

import spiderImg
from urllib import request
import re
import os

def getPageUrlList(urlContent):
    pattern = re.compile(r'<strong><a href="(http://moe.005.tv/\d{5}\.html)">(?:<b>)*(.*?)(?:</b>)*</a></strong>')
    pageUrlList = pattern.findall(urlContent)
    return pageUrlList

def downloadPages(pageUrlList, dest):
    if not os.path.exists(dest):
        os.makedirs(dest)
    for pageUrl in pageUrlList:
        url = pageUrl[0]
        name = pageUrl[1]
        path = dest + name + '/'
        if not os.path.exists(path):
            os.makedirs(path)
        spiderImg.download(url, path)

def download(url, dest):
    urlContent = spiderImg.getUrlContent(url)
    pageUrlList = getPageUrlList(urlContent)
    downloadPages(pageUrlList, dest)

if __name__ == '__main__':
    url = 'http://moe.005.tv/moeimg/list_2_11.html'
    dest = 'img/'
    download(url, dest)

上面的正则表达式中似乎是多了一个两个<b>,这是因为我发现有的标题使用了<b>倒是创建文件夹的时候失败了。于是我才加上了两个非捕获组防止<和>被放到文件名中,倒是创建文件夹失败。

爬取多页图片

import spiderPage
import os

if __name__ == '__main__':
    for i in range(29,32):
        url = 'http://moe.005.tv/moeimg/tb/list_3_'+str(i)+'.html'
        dest = 'img/page_3_'+str(i)+'/'
        if not (os.path.exists(dest)):
            os.makedirs(dest)
        try:
            spiderPage.download(url, dest)
        except Exception as e:
            print('Exception: '+e)

总结

上面可以说只是学完了正则表达式之后的一个简单的应用,写出来的也是一个极简极low的一个爬虫程序,不过这个极简极low的爬虫程序还真的解决了我的一个小问题。我再也不用,手动的右键另存为,另存为,花了半天才存了五十张图片了!!棒棒哒!


一枚小菜鸡