写在前面
本人业余时间会写写CSDN的博客,查看下博客数据,展现量、阅读量什么的。在“作品数据-单篇文章分析”菜单中可以看到每篇文章的总体展现量、阅读量,要是想看每篇文章每日的访问量需要再次点击列表后边的“查看详情”显示的曲线图,一个一个点击着实有些麻烦,所以想通过调用接口的方式返回数据,把每篇文章的每日数据存起来,再设置个定时任务,就解放双手了。
找到的参考代码都是python的,没有java的,自己编码后在这里记录一下。
接口选择
点击“单篇文章分析”,可以得到每篇文章的总体展现、阅读量,那么使用某篇文章的阅读量减去昨天此文章的阅读量,就是每日的访问量了,也不需要调用每篇文章的数据的接口,新建的文章也都能自动获取到了。
head分析
在每次请求接口的时候都会在请求头中传入cookie;x-ca-key;x-ca-nonce;x-ca-signature;x-ca-signature-headers这五个参数。简单的分析不难得出x-ca-key和x-ca-signature-headers是两个固定值,cookie是登录后的标识,会保持数天不变。而x-ca-signature和x-ca-signature-headers 每次请求的时候都不同,这就有点难顶了
进一步分析
进一步分析下上述5个参数如何生成的,cookie没什么好说的。
通过查看js源代码,搜索x-ca-key的值,看到下边这个方法
X-Ca-Key是固定的
X-Ca-Nonce最终通过De函数生成
X-Ca-Signature由方法名称、url、参数、固定值等计算获得
X-Ca-Nonce生成方法
private static List<String> chats = new ArrayList<>();
static {
//生成 [a, b, c, d, e, f, g, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
for (int i = 0; i < 7; i++) {
char c = (char) (i + 97);
chats.add(String.valueOf(c));
}
for (int i = 0; i < 10; i++) {
char c = (char) (i + 48);
chats.add(String.valueOf(c));
}
}
/**
* 获取一次性访问key,参考js方法:
* De = function() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (function(e) {
var t = 16 * Math.random() | 0,
n = "x" === e ? t: 3 & t | 8;
return n.toString(16)
}))}
*
* @return
*/
private static String onceKey() {
List<String> strs = new ArrayList<>();
Random rd = new Random();
for (int i = 0; i < 30; i++) {
strs.add(chats.get(rd.nextInt(chats.size())));
}
return String.format("%s%s%s%s%s%s%s%s-%s%s%s%s-4%s%s%s-9%s%s%s-%s%s%s%s%s%s%s%s%s%s%s%s", strs.toArray());
}
X-Ca-Signature生成方法
这里特别需要注意的是在生成x-ca-signature时,POST请求和GET请求并不相同,我们需要区分处理。最明显的两个区别是请求方法Method不同,content_type不同,下面POST方法并没有写完
private static String sign(String fullUrl, String method, String onceKey) throws Exception {
final String ekey = "9znpamsyl2c7cdrr9sas0le9vbc3r6ba";
final String xcakey = "203803574"; // 开发工具 network 请求头 x-ca-key
String[] wholdUrl = fullUrl.split("\\?");
String url, params = "";
if ("get".equals(method)) {
url = wholdUrl[0];
params = wholdUrl[1];
} else {
url = wholdUrl[0];
}
String _url = url + (!"".equals(params) ? "?" + params : "");
String to_enc = "";
if ("get".equals(method)) {
to_enc = String.format("GET\napplication/json, text/plain, */*\n\n\n\nx-ca-key:%s\nx-ca-nonce:%s\n%s", xcakey, onceKey, _url);
} else {
// to_enc = String.format("POST\n%s\n\n{content_type}\n\nx-ca-key:%s\nx-ca-nonce:%s\n%s"
// , accept, content_type, xcakey, onceKey, _url);
}
return getSHA256StrJava(to_enc, ekey);
}
private static String getSHA256StrJava(String content, String secret) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
mac.init(secret_key);
// System.out.println("key: " + secret + " | 内容是:\n" + content);
byte[] binaryData = mac.doFinal(content.getBytes());
return Base64.encodeBase64String(binaryData);
}
执行接口调用
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.8.1</version>
</dependency>
public JSONObject getAllMyArticle() throws Exception {
String host = "https://bizapi.csdn.net";
String path = "/blog/phoenix/console/v1/data/single-article-list?action=down&page=1&size=40&type=date";
String onceKey = onceKey();
String sign = sign(path, "get", onceKey);
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(host + path)
.addHeader("Accept", "application/json, text/plain, */*")
// .addHeader("Accept-Encoding", "gzip, deflate, br") //若有设置,response为乱码
.addHeader("Accept-Language", "zh-CN,zh;q=0.9")
.addHeader("Connection", "keep-alive")
.addHeader("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36")
.addHeader("origin", "https://mp.csdn.net")
.addHeader("referer", "https://mp.csdn.net/mp_blog/analysis/article/single")
.addHeader("sec-ch-ua", "\"Google Chrome\";v=\"105\", \"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"105\"")
.addHeader("sec-ch-ua-mobile", "?0")
.addHeader("sec-ch-ua-platform", "\"Windows\"")
.addHeader("sec-fetch-dest", "empty")
.addHeader("sec-fetch-mode", "cors")
.addHeader("sec-fetch-site", "same-site")
.addHeader("uri-name", "feige")
//登录后的cokie
.addHeader("cookie", cookie)
.addHeader("x-ca-key", "203803574") //根据浏览器开发者工具请求头设置
.addHeader("x-ca-nonce", onceKey) //请求1次后作废
.addHeader("x-ca-signature", sign) // 根据url、排序参数、onceKey、加密key,使用HmacSHA256算法生成的摘要
.addHeader("x-ca-signature-headers", "x-ca-key,x-ca-nonce") //根据浏览器开发者工具请求头设置
.build();
Response response = client.newCall(request).execute();
response.header("content-type", "application/json;charset=utf-8");
// 出现错误可以将此打开获取报错内容
// System.out.println(response);
// System.out.println(response.header("X-Ca-Error-Message"));
String responseData = response.body().string();
// System.out.println(responseData);
return JSONObject.parseObject(responseData);
}
这里特别注意,生成X-Ca-Signature传入的path是不带域名的,且url的参数部分需要按照字母顺序升序排列,header里其他参数使用浏览器的的,否则容易出错
X-Ca-Signature签名排错方法
打开response的输出,获取header中X-Ca-Error-Message会返回具体的错误信息。如下图会提示正确的签名生成方式,将自己的重新调整就好,因为HTTP Header中无法表示换行,因此返回信息中的换行符都被替换成#,反过来传参时# 号使用 \n 替换
总结
需要注意cookie会过期问题。工具本质上不难,就是调用CSDN的接口。难点主要是在于请求头中x-ca-nonce和x-ca-signature这两个参数比较难搞。搞定了这两个参数后面的调用逻辑就比较简单。比如本人使用今天获取的数据与前一天的数据做差,计算出每篇文章昨日访问量数据。
版权归原作者 as350144 所有, 如有侵权,请联系我们删除。