专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

抓取网易云音乐评论,哪一句触动你的心

音乐,是给灵魂的献礼。

一个好的耳机,好像是程序员的标配。当然有时候不光是为了听音乐,只是想告诉别人:忙碌中,莫挨老子…

音乐软件有很多,为什么说网易云音乐呢?因为我用的是这个。没有什么其他交易,当然我都要爬她了,她肯定不会很爽,所以大家还是悄悄的比较好。

网易云音乐有网页版,所以分析接口还是比较简单的。如果没有网页版,就要抓包了,最近发现了一款超级好用的抓包工具,http和https都可以抓

Proxyman Mac版的,免费的,比青花瓷好用太多了。

一 获取链接

获取评论的url长这样,需要把歌曲id拼接在后面
http://music.163.com/api/v1/resource/comments/R_SO_4_

歌曲id可以点击分享的时候,复制链接
https://music.163.com/song?id=1350364644

把id拼后面
http://music.163.com/api/v1/resource/comments/R_SO_4_1350364644
这就是完整的获取评论的url。

观察发现还有一个参数offset,用来翻页

  • offset = 0 的时候 返回 20个热评 + 10 条评论
  • offset = 1 的时候 无热评 从 第二条显示10 条评论,所以每次要加10

链接有了,接下来就是用代码来实现了。

二 获取歌曲id

其实我是先写好了代码,最后才获取歌曲的id的,不过还是先从简单的说吧。 拿到这个链接,获取id
https://music.163.com/song?id=1350364644
首先想到的是split(),还是太年轻啊,有时候链接像下面这样,如果登陆了会有userid。
https://music.163.com/song?id=1350364644&userid=110
据我仔细的观察,id好像始终在第一个。
这个时候只能用正则表达式了。

1. 正则表达式

正则表达式,不光是 ^([0-9]{0,})$,还有 先行断言(lookahead)和后行断言(lookbehind)

具体分为

  • 正向先行断言 后面必须有 (?=pattern)
  • 负向先行断言 后面不能有 (?!pattern)
  • 正向后行断言 前面必须要有 (?<=pattern)
  • 负向后行断言 前面不能有 (?<!pattern)

下面这个就是 正向后行断言 ,意思是前面必须有?id=的一段数字
(?<=\?id=)(\d+)
所以把复制的链接传进来就可以取到id了

Pattern pb = Pattern.compile("(?<=\\?id=)(\\d+)");
Matcher matcher = pb.matcher(songUrl);
Assert.isTrue(matcher.find(), "未找到歌曲id!");
String songId = matcher.group();

三 Http请求工具类

在日常的开发中,有时候需要发送http或者https请求。而http请求的工具类也有很多种写法。
当然,很多公司可能封装好了工具类,下面这个是我自己参考了一些别人的写法, 然后写的一个可以发送httphttpsgetpost请求的工具类。使用也很简单。

依赖如下

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.10</version>
</dependency>

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpmime</artifactId>
    <version>4.5.7</version>
</dependency>

package com.ler.pai.util;

import com.alibaba.fastjson.JSONObject;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

/**
 * http 工具类
 *
 * @author lww
 */
@Slf4j
public class HttpUtils {

    private static final String ENCODE = "UTF-8";

    private HttpUtils() {
    }

    /*
       <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.10</version>
        </dependency>

        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpmime</artifactId>
            <version>4.5.7</version>
        </dependency>
     */

    /**
     * 向指定URL发送GET方法的请求 http
     *
     * @param url     发送请求的URL
     * @param param   请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
     * @param headers 可为null
     * @return URL 所代表远程资源的响应结果
     */
    public static String sendGetHttp(String url, String param, Map<String, String> headers) {
        HttpGet httpGet = new HttpGet(StringUtils.isBlank(param) ? url : url + "?" + param);
        headers = initHeader(headers);
        //设置header
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            httpGet.setHeader(entry.getKey(), entry.getValue());
        }
        String content = null;
        try (CloseableHttpClient closeableHttpClient = HttpClientBuilder.create().build()) {
            CloseableHttpResponse httpResponse = closeableHttpClient.execute(httpGet);
            HttpEntity entity = httpResponse.getEntity();
            content = EntityUtils.toString(entity, ENCODE);
        } catch (IOException e) {
            log.error("HttpRequest_getForm_e:{}", e);
        }
        return content;
    }

    /**
     * 向指定URL发送GET方法的请求 https
     *
     * @param url     发送请求的URL
     * @param param   请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
     * @param headers 可为null
     * @return URL 所代表远程资源的响应结果
     */
    public static String sendGetHttps(String url, String param, Map<String, String> headers) {
        HttpGet httpGet = new HttpGet(StringUtils.isBlank(param) ? url : url + "?" + param);
        headers = initHeader(headers);
        //设置header
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            httpGet.setHeader(entry.getKey(), entry.getValue());
        }
        String content = null;
        try (CloseableHttpClient closeableHttpClient = sslHttpClientBuild()) {
            CloseableHttpResponse httpResponse = closeableHttpClient.execute(httpGet);
            HttpEntity entity = httpResponse.getEntity();
            content = EntityUtils.toString(entity, ENCODE);
        } catch (IOException e) {
            log.error("HttpRequest_getForm_e:{}", e);
        }
        return content;
    }

    /**
     * 向指定 URL 发送POST方法的请求 form参数 http
     *
     * @param url     发送请求的 URL
     * @param param   请求参数,请求参数可以 ?name1=value1&name2=value2 拼在url后,也可以放在param中。
     * @param headers 可为null
     * @return 所代表远程资源的响应结果
     */
    public static String sendPostFormHttp(String url, Map<String, String> param, Map<String, String> headers) {
        HttpPost httpPost = new HttpPost(url);
        headers = initHeader(headers);
        headers.put("Content-Type", "application/x-www-form-urlencoded");
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            httpPost.setHeader(entry.getKey(), entry.getValue());
        }
        String content = null;
        List<NameValuePair> pairList = new ArrayList<>();
        if (param != null) {
            for (Map.Entry<String, String> entry : param.entrySet()) {
                pairList.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
            }
        }
        try (CloseableHttpClient closeableHttpClient = HttpClientBuilder.create().build()) {
            httpPost.setEntity(new UrlEncodedFormEntity(pairList, ENCODE));
            CloseableHttpResponse httpResponse = closeableHttpClient.execute(httpPost);
            HttpEntity entity = httpResponse.getEntity();
            content = EntityUtils.toString(entity, ENCODE);
        } catch (IOException e) {
            log.error("HttpRequest_getForm_e:{}", e);
        }
        return content;
    }

    /**
     * 向指定 URL 发送POST方法的请求 form参数 https
     *
     * @param url     发送请求的 URL
     * @param param   请求参数,请求参数可以 ?name1=value1&name2=value2 拼在url后,也可以放在param中。
     * @param headers 可为null
     * @return 所代表远程资源的响应结果
     */
    public static String sendPostFormHttps(String url, Map<String, String> param, Map<String, String> headers) {
        HttpPost httpPost = new HttpPost(url);
        headers = initHeader(headers);
        headers.put("Content-Type", "application/x-www-form-urlencoded");
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            httpPost.setHeader(entry.getKey(), entry.getValue());
        }
        String content = null;
        List<NameValuePair> pairList = new ArrayList<>();
        if (param != null) {
            for (Map.Entry<String, String> entry : param.entrySet()) {
                pairList.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
            }
        }
        try (CloseableHttpClient closeableHttpClient = sslHttpClientBuild()) {
            httpPost.setEntity(new UrlEncodedFormEntity(pairList, ENCODE));
            CloseableHttpResponse httpResponse = closeableHttpClient.execute(httpPost);
            HttpEntity entity = httpResponse.getEntity();
            content = EntityUtils.toString(entity, ENCODE);
        } catch (IOException e) {
            log.error("HttpRequest_getForm_e:{}", e);
        }
        return content;
    }

    /**
     * 发送post,参数为json字符串 放在body中 requestBody http
     *
     * @param url     url
     * @param params  参数
     * @param headers 可为null
     */
    public static String sendPostJsonHttp(String url, JSONObject params, Map<String, String> headers) {
        HttpPost httpPost = new HttpPost(url);
        headers = initHeader(headers);
        headers.put("Content-Type", "application/json");
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            httpPost.setHeader(entry.getKey(), entry.getValue());
        }
        StringEntity stringEntity = new StringEntity(params.toString(), ENCODE);
        httpPost.setEntity(stringEntity);
        String content = null;
        try (CloseableHttpClient closeableHttpClient = HttpClientBuilder.create().build()) {
            CloseableHttpResponse httpResponse = closeableHttpClient.execute(httpPost);
            HttpEntity entity = httpResponse.getEntity();
            content = EntityUtils.toString(entity, ENCODE);
            closeableHttpClient.close();
        } catch (IOException e) {
            log.error("HttpUtil_sendPostJsonHttp_e:{}", e);
        }
        return content;
    }

    /**
     * 发送post,参数为json字符串 放在body中 requestBody https
     *
     * @param url     url
     * @param params  参数
     * @param headers 可为null
     */
    public static String sendPostJsonHttps(String url, JSONObject params, Map<String, String> headers) {
        HttpPost httpPost = new HttpPost(url);
        headers = initHeader(headers);
        headers.put("Content-Type", "application/json");
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            httpPost.setHeader(entry.getKey(), entry.getValue());
        }
        StringEntity stringEntity = new StringEntity(params.toString(), ENCODE);
        httpPost.setEntity(stringEntity);
        String content = null;
        try (CloseableHttpClient closeableHttpClient = sslHttpClientBuild()) {
            CloseableHttpResponse httpResponse = closeableHttpClient.execute(httpPost);
            HttpEntity entity = httpResponse.getEntity();
            content = EntityUtils.toString(entity, ENCODE);
            closeableHttpClient.close();
        } catch (IOException e) {
            log.error("HttpUtil_sendPostJsonHttps_e:{}", e);
        }
        return content;
    }

    private static Map<String, String> initHeader(Map<String, String> headers) {
        if (headers == null) {
            headers = new HashMap<>(16);
        }
        headers.put("accept", "*/*");
        headers.put("connection", "Keep-Alive");
        headers.put("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
        return headers;
    }

    public static CloseableHttpClient sslHttpClientBuild() {
        Registry<ConnectionSocketFactory> socketFactoryRegistry =
                RegistryBuilder.<ConnectionSocketFactory>create()
                        .register("http", PlainConnectionSocketFactory.INSTANCE)
                        .register("https", trustAllHttpsCertificates()).build();
        //创建ConnectionManager,添加Connection配置信息
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
        CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(connectionManager).build();
        return httpClient;
    }

    private static SSLConnectionSocketFactory trustAllHttpsCertificates() {
        SSLConnectionSocketFactory socketFactory = null;
        TrustManager[] trustAllCerts = new TrustManager[1];
        TrustManager tm = new Mitm();
        trustAllCerts[0] = tm;
        SSLContext sc;
        try {
            sc = SSLContext.getInstance("TLS");
            sc.init(null, trustAllCerts, null);
            socketFactory = new SSLConnectionSocketFactory(sc, NoopHostnameVerifier.INSTANCE);
        } catch (NoSuchAlgorithmException | KeyManagementException e) {
            log.error("HttpUtil_trustAllHttpsCertificates_e:{}", e);
        }
        return socketFactory;
    }

    static class Mitm implements TrustManager, X509TrustManager {

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return null;
        }

        @Override
        public void checkServerTrusted(X509Certificate[] certs, String authType) {
            //don't check
        }

        @Override
        public void checkClientTrusted(X509Certificate[] certs, String authType) {
            //don't check
        }
    }
}

四 无代码不BB

获取评论的代码。注释很清楚了。(贴了代码有种被看光光的感觉,害羞啊)

package com.ler.pai.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.ler.pai.service.MusicService;
import com.ler.pai.util.HttpUtils;
import com.ler.pai.vo.CommentsInfoVO;
import com.ler.pai.vo.CommentsVO;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

/**
 * @author lww
 * @date 2020-01-30 18:09
 */
@Service
@Slf4j
public class MusicServiceImpl implements MusicService {

    private Pattern pb = Pattern.compile("(?<=\\?id=)(\\d+)");

    private static final String COMMENT_URL = "http://music.163.com/api/v1/resource/comments/R_SO_4_";

    @Override
    public String getComment(String songUrl) {
        log.info("MusicServiceImpl_getComment_songUrl:{}", songUrl);
        Matcher matcher = pb.matcher(songUrl);
        Assert.isTrue(matcher.find(), "未找到歌曲id!");
        //获取歌曲id
        String songId = matcher.group();
        //拼接
        String url = COMMENT_URL + songId;
        // offset = 0 有 20个热评 + 10 条评论
        // offset = 1 无热评 从 第二条显示10 条评论
        String sb = "?offset=0";
        //发送请求
        String s = HttpUtils.sendPostFormHttps(url + sb, null, null);
        //解析
        CommentsInfoVO vo = JSONObject.parseObject(s, CommentsInfoVO.class);
        //获取总评论数
        Long total = vo.getTotal();
        Assert.isTrue(total != null, "资源不存在!");
        //计算页数
        Long page = (total % 10 == 0 ? total / 10 : total / 10 + 1);
        //用于存放评论
        StringBuilder res = new StringBuilder(1024);
        int i = 0;
        while (i < page) {
            //先把链接里 页数置空
            sb = sb.replace(i + "", "");
            //解析评论
            CommentsInfoVO commentsInfoVO = JSONObject.parseObject(s, CommentsInfoVO.class);
            List<CommentsVO> hotComments = commentsInfoVO.getHotComments();
            //热评 拼装数据
            if (hotComments != null) {
                for (CommentsVO hotComment : hotComments) {
                    res.append("========================").append("\n");
                    res./*append(hotComment.getUser().getNickname()).append(" : ").*/append(hotComment.getContent()).append("\n");
                }
            }
            List<CommentsVO> comments = commentsInfoVO.getComments();
            //评论 拼装数据
            for (CommentsVO comment : comments) {
                res.append("========================").append("\n");
                res./*append(comment.getUser().getNickname()).append(" : ").*/append(comment.getContent()).append("\n");
            }
            i += 10;
            if (i > 50) {
                //避免爬取太多
                break;
            }
            //要获取下一页,需要加10
            sb = "?offset=" + i;
            //发送请求 然后会再次进入循环,再次解析
            s = HttpUtils.sendPostFormHttps(url + sb, null, null);
        }
        return res.toString();
    }
}

陶峻汐 的《和解》执行的结果如下:

========================

很多时候 我们内心的痛苦 都是因为自己放不过自己 然而 当我们迟迟不肯与自己和解 不肯与过往和解 不肯与生活和解的时候 在那些数不清的黑夜里 另一个自己总是伺机跑出来扰乱 带给我们 纠缠 挣扎和矛盾

她是我的病 注定成不了我的人

尼采:“不需时刻敏感,迟钝有时即为美德”

暧昧上头的那几秒真的像极了爱情 可惜人生就是一个圆 上头有多快乐 下头就有多难过

我想和你一起睡 但不想睡你

劝大家,不要去接近心中有多年执念伤痕累累的人,不要把自己想象成拯救他的圣人。你的温暖和爱,只能换来他一句谢谢你和对不起。 同理,也不要在自己伤疤未好前用别人疗伤。感情多珍重,对己对他人。

离别后 我裹上并不讨喜的白色外衣 再次投入黑色的怀抱 欲盖弥彰

学会和过去和解吧,日子可是过的将来的 学会和自己和解吧,揪着自己的头发也要把自己从泥地里拔起来 和生活的不公和解吧,消化委屈和遭受的恶意。让自己变得再有力量一点强大一点 但多数情况下,我只会跟不快乐和解,替难过找借口说服自己,可有时候我还是会被反噬进去,掉进无底洞里

——日签晚安

谢谢大家的支持[可爱]

人啊 总要学会自己和自己和解 就像我说的算了吧不是因为我不想要了 而是我再怎么努力也没用 不如自我和解

写在最后

最近在构思一个开源项目,主要是整合一些第三方公共API接口的例子。
比如腾讯的公共API,百度的API等等。做一个简单的调用例子。

完整项目地址

由于现在登不了Github,代码在码云

现在已经搭了一个简单的项目,里面有以下接口

  • 高德地图 根据关键字查询地址
  • 高德地图 查询天气(为简化开发,只有 北京,杭州的 天气查询)
  • 高德地图 根据ip获取地址
  • 网易云音乐 获取歌曲评论
  • 短链生成

欢迎大家来共同维护,密钥等可以用个人申请的来测试。
不要泄露公司使用的或者自己的不可公开的。
注意保护个人数据安全。

文章永久链接:https://tech.souyunku.com/33769

未经允许不得转载:搜云库技术团队 » 抓取网易云音乐评论,哪一句触动你的心

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们