以下内容仅用于技术交流,严禁用于恶意下载腾讯动漫资源
最近在研究破解反爬虫机制,正好发现腾讯动漫的js反爬机制有点意思,这里就就以此为例介绍一下如何破解反爬虫
思路分析
首先我们随便打开一个漫画,看看里面的内容,以 中国惊奇先生 为例,打开控制台(网站上禁止右键,因此可以使用快捷键 alt + commond + i 打开控制台)选择network面板,可以看到如下数据包
查看 https://ac.qq.com/ComicView/index/id/511915/cid/43 请求的response可以看到这里是返回了一个html,而最左边我框柱的请求: https://manhua.qpic.cn/manhua_detail/0/23_09_41_a6e25b26a841828110792db68802b052a_113865916.jpg/0 就是图片链接;而在两个请求中间只加载了一个css,并没有ajax请求来获取图片链接,因此我们查找一下图片地址看看html中是否返回了图片链接,由于考虑到可能存在url拼接的情况,这里我截取中间部分进行全局搜索
可以发现在咱们访问的原始页面中有此图片的链接,但并没有看到后续图片的链接,于是我试着在网页上往下滑动加载更多的图片,然后再看下控制台
可以发现直接开始加载图片了,中间并没有发送ajax请求获取新的图片地址。然后搜索一下新加载的图片链接,也没有发现哪里能找到,因此推测图片链接做了js加密。
我们先分析一下页面返回的Html内容,大致浏览一下结构会发现这部分极其扎眼
可能DATA属性就是图片加密后的内容,我们全局搜索一下"DATA"(注意区分大小写)
可以看到DATA只存在于https://ac.gtimg.com/media/js/ac.page.chapter.view_v2.6.0.js?v=20200914 中。我们点击"pretty-print"美化一下,然后把每个用到"DATA"的地方看一下,可以看到在这个js中"DATA"出现了37次,但只有一次——也就是我上图中的地方——是给DATA赋值的,那么我们重点研究这里。先我们再全局搜一下"_v",可以发现_v只在当前js出现,且多为取值操作,唯一一次意义不明的情况在"var DATA=_v"的上一行
可以看到这里是一个加密的js方法(这里需要熟悉js常用语法,爬虫需要的知识面很广呀!!!)
eval(function(p, a, c, k, e, r) {
e = function(c) {
return (c < a ? "" : e(parseInt(c / a))) + ((c = c % a) > 35 ? String.fromCharCode(c + 29) : c.toString(36))
}
;
if (!"".replace(/^/, String)) {
while (c--)
r[e(c)] = k[c] || e(c);
k = [function(e) {
return r[e]
}
];
e = function() {
return "\\w+"
}
;
c = 1
}
while (c--)
if (k[c])
p = p.replace(new RegExp("\\b" + e(c) + "\\b","g"), k[c]);
return p
}("p y(){i=\"J+/=\";O.D=p(c){s a=\"\",b,d,h,f,g,e=0;C(c=c.z(/[^A-G-H-9\\+\\/\\=]/g,\"\");e<c.k;)b=i.l(c.m(e++)),d=i.l(c.m(e++)),f=i.l(c.m(e++)),g=i.l(c.m(e++)),b=b<<2|d>>4,d=(d&15)<<4|f>>2,h=(f&3)<<6|g,a+=7.5(b),w!=f&&(a+=7.5(d)),w!=g&&(a+=7.5(h));v a=u(a)};u=p(c){C(s a=\"\",b=0,d=17=8=0;b<c.k;)d=c.o(b),Q>d?(a+=7.5(d),b++):R<d&&S>d?(8=c.o(b+1),a+=7.5((d&F)<<6|8&r),b+=2):(8=c.o(b+1),x=c.o(b+2),a+=7.5((d&15)<<12|(8&r)<<6|x&r),b+=3);v a}}s B=I y(),T=W['K'+'L'].M(''),N=W['n'+'P'+'e'],j,t,q;N=N.U(/\\d+[a-V-Z]+/g);j=N.k;X(j--){t=Y(N[j])&10;q=N[j].z(/\\d+/g,'');T.11(t,q.k)}T=T.13('');14=16.E(B.D(T));", 62, 70, "|||||fromCharCode||String|c2||||||||||_keyStr|len|length|indexOf|charAt||charCodeAt|function|str|63|var|locate|_utf8_decode|return|64|c3|Base|replace|||for|decode|parse|31|Za|z0|new|ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789|DA|TA|split||this|onc|128|191|224||match|zA||while|parseInt||255|splice||join|_v||JSON|c1".split("|"), 0, {}))
我们随便找个网站进行解密(在网站上搜索 “js eval 解密”即可),这里我用的是 beautifyconverter,解密之后的js如下:
function Base() {
_keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
this.decode = function(c) {
var a = "",
b, d, h, f, g, e = 0;
for (c = c.replace(/[^A-Za-z0-9\+\/\=]/g, ""); e < c.length;) b = _keyStr.indexOf(c.charAt(e++)), d = _keyStr.indexOf(c.charAt(e++)), f = _keyStr.indexOf(c.charAt(e++)), g = _keyStr.indexOf(c.charAt(e++)), b = b << 2 | d >> 4, d = (d & 15) << 4 | f >> 2, h = (f & 3) << 6 | g, a += String.fromCharCode(b), 64 != f && (a += String.fromCharCode(d)), 64 != g && (a += String.fromCharCode(h));
return a = _utf8_decode(a)
};
_utf8_decode = function(c) {
for (var a = "", b = 0, d = c1 = c2 = 0; b < c.length;) d = c.charCodeAt(b), 128 > d ? (a += String.fromCharCode(d), b++) : 191 < d && 224 > d ? (c2 = c.charCodeAt(b + 1), a += String.fromCharCode((d & 31) << 6 | c2 & 63), b += 2) : (c2 = c.charCodeAt(b + 1), c3 = c.charCodeAt(b + 2), a += String.fromCharCode((d & 15) << 12 | (c2 & 63) << 6 | c3 & 63), b += 3);
return a
}
}
var B = new Base(),
T = W['DA' + 'TA'].split(''),
N = W['n' + 'onc' + 'e'],
len, locate, str;
N = N.match(/\d+[a-zA-Z]+/g);
len = N.length;
while (len--) {
locate = parseInt(N[len]) & 255;
str = N[len].replace(/\d+/g, '');
T.splice(locate, str.length)
}
T = T.join('');
_v = JSON.parse(B.decode(T));
分析这段js可以知道大致逻辑为根据W[‘DATA’]、W[nonce]经过计算得到_v。而我们通过debug可以知道_v中包含了所有的图片链接
ok,现在我们的目标变成了找到W[‘DATA’]和W[nonce],DATA我们上面已经找到了,就在页面返回的html中,现在我们全局搜索一下"nonce"
会发现只有一个结果,ok,我们把上面解密后的js中的W[DATA]和W[nonce]替换一下,然后在控制台中运行一下
可以发现运行会直接报错
这说明这个参数肯定有问题。然后想到在解密后的JS中使用W[‘DA’ + ‘TA’]代替W[‘DATA’]、W[‘n’ + ‘onc’ + ‘e’]代替W[‘nonce’],那么会不会DATA和nonce其实也是用拼接的方式得到的呢,我们分别对nonce进行分割然后搜索,可以发现有两处十分可疑
我们分别把这两个值填充到解密后的js中执行一下
很明显可以知道咱们的第二段js就是咱们的目标,ok,到这里我们的解密工作已经完成。
爬取思路
爬取流程
- 访问某个漫画的地址,如https://ac.qq.com/Comic/comicInfo/id/511915,获得所有章节列表
- 遍历章节列表,如https://ac.qq.com/ComicView/index/id/511915/cid/43,从其响应中获得DATA,与nonce
- 用上面解密后的js对这DATA与nonce进行计算的到当前章节所有图片的额链接
- 保存图片
Coding
下面让我们来写代码吧
- 首先我们访问漫画地址获得所有章节
/**
* @param url 漫画Url
* @return 每个章节的地址
* @throws Exception
*/
private List<String> getChapterUrls(String url) throws Exception {
String s = sendRequest(url);
Document document = Jsoup.parse(s);
Elements hrefs = document.getElementsByAttributeValueMatching("href", "/ComicView/index/id/");
List<String> chapterUrls = hrefs.stream().map(element -> URL_PREFIX + element.attr("href")).collect(Collectors.toList());
return chapterUrls;
}
- 然后遍历访问每个章节的Url,从响应中获得DATA和nonce
/**
* @param htmlContent 章节url的html文本
* @return TencentChapter 每章内容信息
*/
private TencentChapter getPics(String htmlContent) {
Document document = Jsoup.parse(htmlContent);
Elements scripts = document.getElementsByTag("script");
String data = "";
String nonce = "";
for (Element script : scripts) {
//获取DATA
if (StringUtils.contains(script.html(), "DATA")) {
data = StringUtils.substringBetween(script.html(), "'", "'");
}
//获取nonce
if (StringUtils.containsAny(script.html(), "window\\[.*?\\].*?eval")) {
String str = StringUtils.substringBefore(script.html(), " = ");
str = StringUtils.replace(str, "\"+\"", "");
if (StringUtils.equalsIgnoreCase(str, "window[\"nonce\"]")) {
nonce = StringUtils.substringBetween(script.html(), "] = ", ";");
nonce = StringUtils.replace(nonce, "!!document.getElementsByTagName('html')", "true");
nonce = StringUtils.replace(nonce, "(!window.Array)", "false");
nonce = StringUtils.replace(nonce, "!!document.children", "true");
nonce = executeJS(nonce);
}
}
}
if (StringUtils.isAnyBlank(data, nonce)) {
// log.error("data or nonce get failed,data:{},nonce:{}", data, nonce);
return null;
}
//通过data和nonce计算出网站返回的json(其中包含图片)
String jsonData = jsDecode(data, nonce);
JSONObject sourceJson = JSON.parseObject(jsonData);
String title = sourceJson.getJSONObject("comic").getString("title");
String chapterSeq = sourceJson.getJSONObject("chapter").getString("cSeq");
String chapterName = sourceJson.getJSONObject("chapter").getString("cTitle");
JSONArray pictures = sourceJson.getJSONArray("picture");
List<String> urls = pictures.stream().map(j -> ((JSONObject) j).getString("url")).collect(Collectors.toList());
//创建一个对象用户保存漫画相关信息
TencentChapter chapter = new TencentChapter();
chapter.setTitle(title);
chapter.setChapterName(chapterName);
chapter.setChapterSeq(chapterSeq);
chapter.setPics(urls);
return chapter;
}
ps.需要注意,由于nonce的值是一段js,需要执行之后才能得到结果,因此我们需要在java中执行才能获取到真实值。又由于nonce的计算可能需要使用到window、document等对象,而java执行js的时候是没有这些对象的,因此我在上面的代码中对一些值进行了替换。executJS方法如下
private String executeJS(String jsContent) {
try {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("JavaScript");
CompiledScript script = ((Compilable) engine).compile(jsContent);
Object object = script.eval();
return object.toString();
} catch (Exception e) {
LOGGER.error("jsContent:{}", jsContent);
e.printStackTrace();
}
return "";
}
- 把解密后的js转化为java语言
private String jsDecode(String data, String nonce) {
List<String> dataList = converter(data);
List<String> nonceList = matchNonce(nonce);
for (int i = nonceList.size() - 1; i >= 0; i--) {
String non = nonceList.get(i);
int locate = getNumbers(non) & 255;
String str = non.replaceAll("\\d+", "");
dataList.subList(locate, locate + str.length()).clear();
}
data = dataList.stream().collect(Collectors.joining(""));
String decode = decode(data);
return decode;
}
private String decode(String source) {
source = source.replaceAll("[^A-Za-z0-9\\+\\/\\=]", "");
String keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
String a = "";
int b, d, h, f, g, e = 0;
for (int i = 0; i < source.length() && e < source.length(); i++) {
b = keyStr.indexOf(source.charAt(e));
e++;
d = keyStr.indexOf(source.charAt(e));
e++;
f = keyStr.indexOf(source.charAt(e));
e++;
g = keyStr.indexOf(source.charAt(e));
e++;
b = b << 2 | d >> 4;
d = (d & 15) << 4 | f >> 2;
h = (f & 3) << 6 | g;
a += (char) b;
if (64 != f) {
a += (char) d;
}
if (64 != g) {
a += (char) h;
}
}
return a;
}
private List<String> matchNonce(String nonce) {
List<String> allMatches = new ArrayList<>();
Matcher m = Pattern.compile("\\d+[a-zA-Z]+").matcher(nonce);
while (m.find()) {
allMatches.add(m.group());
}
return allMatches;
}
private int getNumbers(String s) {
String[] n = s.split(""); //array of strings
StringBuffer f = new StringBuffer(); // buffer to store numbers
for (int i = 0; i < n.length; i++) {
if ((n[i].matches("[0-9]+"))) {// validating numbers
f.append(n[i]); //appending
} else {
//parsing to int and returning value
return Integer.parseInt(f.toString());
}
}
return 0;
}
private List<String> converter(String data) {
char[] chars = data.toCharArray();
List<String> result = new ArrayList<>();
for (char aChar : chars) {
result.add(Character.toString(aChar));
}
return result;
}
- 下载保存相关图片
@Test
public void test() throws Exception {
//通过漫画获取总章节数
List<String> chapterUrls = getChapterUrls(CARTOOON_URL);
for (String chapterUrl : chapterUrls) {
String htmlContent = sendRequest(chapterUrl);
try {
TencentChapter tencentChapter = getPics(htmlContent);
if (tencentChapter == null) {
continue;
}
int i = 1;
for (String pic : tencentChapter.getPics()) {
File cartoonBaseDir = new File(DIR_PATH + tencentChapter.getTitle());
if (!cartoonBaseDir.exists()) {
cartoonBaseDir.mkdir();
}
File chapterDir = new File(DIR_PATH + tencentChapter.getTitle() + "/" + tencentChapter.getChapterSeq() + "-" + tencentChapter.getChapterName());
if (!chapterDir.exists()) {
chapterDir.mkdir();
}
String imagePath = chapterDir.getPath() + "/" + i++ + ".jpg";
File imageFile = new File(imagePath);
if(imageFile.exists()){
continue;
}
downloadFileFromUrl(pic, imagePath);
}
LOGGER.info("chapter :{} download sucessful", tencentChapter.getChapterName());
} catch (Exception e) {
LOGGER.error("something error:{}", e.getMessage());
}
}
}
ok,到这这里整个代码就完成了,可以下载整本漫画,并分章节按目录保存。后续还会分享更多的反爬虫破解技术,如果大家有什么加密很有趣的网站也可以让我研究一下呀
转载自原文链接, 如需删除请联系管理员。
原文链接:反爬虫破解——破解腾讯动漫js加密,转载请注明来源!