记一次Java爬虫

240 阅读6分钟

作为一个java互联网金融开发从业者,一直对爬虫有很大的兴趣,之前也学过Python语言,但是拿是很久之前的事情了。 虽然Python语言语法不难,再要捡起来也需要一些时间。 就想着为什么不能用java个爬虫呢?java可是世界上第二好的语言。

说起来就动手。

一、准备阶段

webmagic官方文档
github

二、豆瓣top250

下载源码后,根据文档跑了几个源码例子,webmagic简直简单清晰明了。 于是乎准备自己着手一个爬虫, 由于最近一直在看电影,所有挑了个软柿子捏一捏。 豆瓣top250 就开始了。

1、实体类(Movie)

这里我需要的信息不多,所以只有电影名字,电影简介及电影评分。

public class Movie {

    private String name;
    private String comment;
    private double score;
}

2、编写MoviceService 关键得实现PageProcessor接口中的process方法和getSite方法

①. site

 Site site = Site.me()
            .setRetryTimes(3)
            .setSleepTime(1000)
            .setCharset("UTF-8")
            .addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36");

setRetryTimes:爬虫重试间隔

setSleepTime:相当于Thread.sleep(1000)

addHeader:这个千万不能少,一般网页都是通过ua区别机器还是正常访问情况。

②. 最最关键的process核心方法

打开豆瓣250网站 豆瓣250熟练的F12打开控制台,Ctrl+Shift+C 快速定位到我需要的标签位置。这里我选用了Xpath选择器。快速有简单。

//*[@id="content"]/div/div[1]/ol/li[1]/div/div[2]/div[1]/a/span[1]

紧接着出现了第一个问题, 我要的是这一页中所有的电影的数据。这就不得不找排列规律了。我开始了用肉眼盯着一个个标签对比,眼睛都快瞎了。突然的灵机一动,我copy出两个标签,对比一下那个地方不同不就可以了。

//*[@id="content"]/div/div[1]/ol/li[1]/div/div[2]/div[1]/a/span[1]
//*[@id="content"]/div/div[1]/ol/li[2]/div/div[2]/div[1]/a/span[1]

经过对比,发现只有li标签不同。抽取代码如下:

List<String> m_names = page.getHtml().xpath("//*[@id=\"content\"]/div/div[1]/ol/li/div/div[2]/div[1]/a/span[1]/text()").all();
List<String> comments = page.getHtml().xpath("//*[@id=\"content\"]/div/div[1]/ol/li/div/div[2]/div[2]/p[2]/span/text()").all();
List<String> scores = page.getHtml().xpath("//*[@id=\"content\"]/div/div[1]/ol/li/div/div[2]/div[2]/div/span[2]/text()").all();

③ 翻页

这里豆瓣的翻页是get请求,并且是没有加密限制的。所以参数很容易找出来:

public static void main(String[] args) {
        String url = "https://movie.douban.com/top250?start=";
        for (int i = 0; i < 10; i++) {
            Spider.create(new MovieService())
                    .addUrl(url + i * 25)
                    .thread(5)
                    .run();
        }
    }

④、入库

这里自己写了个preparedStatement,直接用原生jdbc插入的数据库

public class MovieDao {

    private Connection connection;
    private PreparedStatement preparedStatement;

    public MovieDao(){
        try {
            Class.forName("com.mysql.jdbc.Driver");
            String url = "jdbc:mysql://47.94.174.237/movie?useUnicode=true&characterEncoding=utf8";
            String username = "我是用户名";
            String password = "我是密码.";
            connection = DriverManager.getConnection(url, username, password);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public void add(Movie movie){
        try {
            String sql = "insert into movie(m_name, comment, score) value(?,?,?)";
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1, movie.getName());
            preparedStatement.setString(2, movie.getComment());
            preparedStatement.setDouble(3,movie.getScore());
            preparedStatement.execute();
            System.out.println("插入成功<"+movie.getName()+"::"+movie.getComment());
        } catch (SQLException e) {
            System.out.println("插入失败");
            e.printStackTrace();
        }finally {
            try {
                if (connection != null){
                    connection.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
            try {
                if (preparedStatement != null){
                    preparedStatement.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

⑤ 爬虫开始

感觉自己像个黑客(捂脸)...

⑥ 爬虫结束

三、 网易云音乐评论

在这我以为还能跟豆瓣一样简单,爬虫也不过如此。

1、数据来源

同样写了个site利用xpath选择器拿到标签地址,然后翻页找翻页参数。发现,不行啊,这是post请求... 所以失败了。查看源码,github上有人也提出了这类问题。

 Request request = new Request(url);
 request.setMethod(HttpConstant.Method.POST);

于是修改代码,还是不行???

一番谷歌百度后,原来是这两个参数在搞鬼。

于是又一番谷歌百度后...

2、解密

这里解密算法啥的我就不详细说了(其实是看的一知半解,说不出来),参见知乎 综合了几个人的代码后:

package com.cxm.util;

import org.springframework.util.Base64Utils;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;

public class MusicEncrypt {
    /***
     * 密钥
     */
    private static String sKey = "0CoJUm6Qyw8W8jud";
    /**
     * 偏移量
     */
    private static String ivParameter = "0102030405060708";
    private static String context = "{rid: \"R_SO_4_25641368\",offset: \"0\",total: \"true\",limit: \"20\",csrf_token: \"\"}";

    /**
     * aes加密
     * @param content 加密内容
     * @param sKey 偏移量
     * @return
     */
    public static String AESEncrypt(String content,String sKey) {
        try {
            byte[] encryptedBytes;
            byte[] byteContent = content.getBytes("UTF-8");
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            SecretKeySpec secretKeySpec = new SecretKeySpec(sKey.getBytes(), "AES");
            IvParameterSpec iv = new IvParameterSpec(ivParameter.getBytes());
            cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, iv);
            encryptedBytes = cipher.doFinal(byteContent);
            return new String(Base64Utils.encode(encryptedBytes), "UTF-8");
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (InvalidAlgorithmParameterException e) {
            e.printStackTrace();
        } catch (BadPaddingException e) {
            e.printStackTrace();
        } catch (IllegalBlockSizeException e) {
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return null;
    }

    public static String rsaEncrypt() {
        String secKey = "257348aecb5e556c066de214e531faadd1c55d814f9be95fd06d6bff9f4c7a41f831f6394d5a3fd2e3881736d94a02ca919d952872e7d0a50ebfa1769a7a62d512f5f1ca21aec60bc3819a9c3ffca5eca9a0dba6d6f7249b06f5965ecfff3695b54e1c28f3f624750ed39e7de08fc8493242e26dbc4484a01c76f739e135637c";
        return secKey;
    }

    /**
     * @param songId     歌曲ID
     * @param paging     是否第一页 true 第一页  其余传入false
     * @param nowPageNum 当前页数
     * @return
     */
    public static String makeContent(String songId, String paging, int nowPageNum) {
        int offset;
        if (nowPageNum < 1) {
            offset = 20;
        }
        offset = (nowPageNum - 1) * 20;
        String baseContent = "{rid: \"R_SO_4_%s\",offset: \"%d\",total: \"%s\",limit: \"20\",csrf_token: \"\"}";
        return String.format(baseContent, songId, offset, paging);
    }

    /**
     * 获取评论的2个参数设置
     *
     * @param content
     * @return
     */
    public static Map<String, Object> makePostParam(String content) {
        Map<String, Object> map = new HashMap<>();
        map.put("params", MusicEncrypt.AESEncrypt((AESEncrypt(content, sKey)), "FFFFFFFFFFFFFFFF"));
        map.put("encSecKey", MusicEncrypt.rsaEncrypt());
        return map;
    }

    /**
     * 直接调用此方法
     * @param songId
     * @param paging
     * @param nowPageNum
     * @return
     */
    public static Map<String, Object> makePostParam(String songId, String paging, int nowPageNum) {
        return makePostParam(makeContent(songId, paging, nowPageNum));
    }
}

2、爬虫代码

这里是ajax返回的json数据。所以用的是JsonPathSelector选择器。

List<String> contentList = new JsonPathSelector("$.comments.[*].content").selectList(page.getRawText());
List<String> timeList = new JsonPathSelector("$.comments.[*].time").selectList(page.getRawText());
List<String> nicknameList = new JsonPathSelector("$.comments.[*].user.nickname").selectList(page.getRawText());

为了不被网易发现...

 for (int i = 1; i <= 5930; i++) {
                System.err.println("开始查询页数:" + i);
                String url = "https://music.163.com/weapi/v1/resource/comments/R_SO_4_474567580?csrf_token=f6618e7d1b51e227d863ffc579a27c95";
                Request request = new Request(url);
                Map<String, Object> makePostParam = MusicEncrypt.makePostParam("474567580", "false", i);
                request.setRequestBody(HttpRequestBody.form(makePostParam, "utf8"));
                request.setMethod(HttpConstant.Method.POST);
                Spider.create(new CommentService())
//                    .addUrl("https://music.163.com/song?id=474567580")
//                        .setDownloader(httpClientDownloader)
                        .addRequest(request)
                        .thread(3)
                        .run();
                Thread.sleep((new Random().nextInt(1000) + 1000));
            }

好吧, 还是被发现了,封了我一天的ip... 但是这不可能阻止我学习欲望。

加入代理ip,为此我还专门去爬取了代理ip网站的ip。真是差火,没一个能用的。白嫖是不可能的了。 买来ip后,继续爬取。

 HttpClientDownloader httpClientDownloader = new HttpClientDownloader();
 httpClientDownloader.setProxyProvider(SimpleProxyProvider.from(new Proxy(ProxyHost,ProxyPort,ProxyUser,ProxyPass)));

 Spider.create(new CommentService())
                        .setDownloader(httpClientDownloader); 

我的天, 看着一条条数据写入磁盘,别提心里多舒服了。

2、结果

在我的代码跑了一个小时后, 发现,已经拿不到数据了。 翻看日记发现,从250页开始后面对数据都没有了。一度怀疑是ip封了,回家后马上换网络继续爬取。 还是同一个问题,250页之后都没了。于是我开始倒着爬试试,从最后一页往前爬,发现,同样,只能爬取最后250页数据。 这是为什么?

换ip,换歌,能换的都换了。 最后手动翻页到250页,发现,wdnmd,第250页之后到倒数250页之间的数据是不展示的,不展示我咋拿...

简直了,这是一道无解题

网易,你厂的程序员还真会玩...

最后,特别不甘心,琢磨了两天,居然是这样的一个结果,如果谁有其他的办法,请一定告诉我。