1. 程式人生 > >Java--汽車之家論壇反爬蟲破解

Java--汽車之家論壇反爬蟲破解

問口碑的人比較多,寫了一下思路,請點選這裡

現在論壇的反爬蟲也改成了字型對映,所以本篇破解方式已經不適用了,新的破解方式可以看我的口碑破解方法. ---2018-1-9

目前論壇可以用 , 口碑的不能用 . 最近的口碑破解有時間分享 ---2017.11.16

公司給的任務 ,需要爬取汽車之家論壇的內容, 由於文章的內容有一些反爬蟲的機制, 所以並不好直接爬取. 在網上搜了一些解決辦法後, 看到了"星光海豚"寫的解決的方法.  反爬蟲破解系列-汽車之家利用css樣式代替文字破解方法  .

他的部落格裡有具體的破解原理, 原理很簡單, 就是實現起來比較麻煩. 由於他是用Python實現的, 而我需要用的Java, 所以

我就在根據他的原理用Java實現了一下, 大部分都是參考的他的原理和思路. 再次感謝"星光海豚".

用到了一個Jsoup包來解析網頁

這是阿里雲映象的座標

<dependency>
			<!-- jsoup HTML parser library @ https://jsoup.org/ -->
			<groupId>org.jsoup</groupId>
			<artifactId>jsoup</artifactId>
			<version>1.10.3</version>
		</dependency>



程式碼如下

package test;

import java.io.IOException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;

public class DecodeAutoHome {
	public String getArticle(String url) throws IOException, URISyntaxException {
		// 文章的文字內容
		String article = null;

		// 獲取網頁的Document物件
		Document doc = Jsoup.connect(url).get();
		// 獲取文章的內容
		Element content = doc.getElementsByClass("rconten").get(0);

		String articleHtml = content.html();

		// #############################################################
		// 處理js混淆部分

		// 獲取混淆的js程式碼
		String script = content.getElementsByTag("script").get(0).html();

		/*
		 * 1.判斷混淆 無引數 返回常量 函式 function Ad_() { function _A() { return 'Ad__'; };
		 * if (_A() == 'Ad__') { return '8'; } else { return _A(); } }
		 * 
		 * 需要將Ad_()替換為8
		 */
		// 取出每個"混淆 無引數 返回常量 函式" 格式的函式
		String regex1 = "function\\s*(\\w+)\\(\\)\\s*\\{\\s*" + "function\\s+\\w+\\(\\)\\s*\\{\\s*"
				+ "return\\s+[\'\"]([^\'\"]+)[\'\"];\\s*" + "\\};\\s*"
				+ "if\\s*\\(\\w+\\(\\)\\s*==\\s*[\'\"]([^\'\"]+)[\'\"]\\)\\s*\\{\\s*"
				+ "return\\s*[\'\"]([^\'\"]+)[\'\"];\\s*" + "\\}\\s*else\\s*\\{\\s*" + "return\\s*\\w+\\(\\);\\s*"
				+ "\\}\\s*" + "\\}";
		Pattern p1 = Pattern.compile(regex1);
		Matcher m1 = p1.matcher(script);
		// 找出所有匹配的字串,存入List中
		List<String> l1 = new ArrayList<String>();
		// 遍歷每一條函式
		while (m1.find()) {
			// Matcher.group(int i) 此方法可以獲取正則表示式中第i個括號裡的內容.
			l1.add(m1.group());
		}
		for (String str : l1) {
			Pattern p11 = Pattern.compile(regex1);
			Matcher m11 = p11.matcher(str);
			while (m11.find()) {
				String name = m11.group(1);// 獲取第一個括號裡的內容
				String b = m11.group(2);// 獲取第二個括號裡的內容
				String c = m11.group(3);// 獲取第三個括號裡的內容
				String value = "";
				if (b.equals(c)) {
					value = m11.group(4);
				} else {
					value = m11.group(2);
				}
				script = script.replaceAll(name + "\\(\\)", value);// 給name的正則加上括號
			}

		}

		// 2.判斷混淆 無引數 返回函式 常量
		/*
		 * function wu_() { function _w() { return 'wu_'; }; if (_w() == 'wu__')
		 * { return _w(); } else { return '5%'; } } 需要將wu_()替換為5%
		 */

		// 取出每一個"混淆 無引數 返回函式 常量" 格式的函式

		String regex2 = "function\\s*(\\w+)\\(\\)\\s*\\{\\s*" + "function\\s*\\w+\\(\\)\\s*\\{\\s*"
				+ "return\\s*[\'\"]([^\'\"]+)[\'\"];\\s*" + "\\};\\s*"
				+ "if\\s*\\(\\w+\\(\\)\\s*==\\s*[\'\"]([^\'\"]+)[\'\"]\\)\\s*\\{\\s*" + "return\\s*\\w+\\(\\);\\s*"
				+ "\\}\\s*else\\s*\\{\\s*" + "return\\s*[\'\"]([^\'\"]+)[\'\"];\\s*" + "\\}\\s*" + "\\}";
		Pattern p2 = Pattern.compile(regex2);
		Matcher m2 = p2.matcher(script);
		// 找出所有匹配的字串,存入List中
		List<String> l2 = new ArrayList<String>();
		while (m2.find()) {
			l2.add(m2.group());
		}
		for (String str : l2) {
			Pattern p21 = Pattern.compile(regex2);
			Matcher m21 = p21.matcher(str);
			while (m21.find()) {
				String name = m21.group(1);// 獲取第一個括號裡的內容
				String b = m21.group(2);
				String c = m21.group(3);
				String value = "";
				if (!b.equals(c)) {
					value = m21.group(4);
				} else {
					value = m21.group(2);
				}
				script = script.replaceAll(name + "\\(\\)", value);
			}
		}

		// 3.var 引數等於返回值函式
		/*
		 * 類似於此的混淆 var ZA_ = function(ZA__) { 'return ZA_'; return ZA__; };
		 */
		// 需要替換ZA__(',5_198')為,5_198

		// 從js中,利用正則,提取上面這種格式的變數
		String regex3 = "var\\s*([^=]+)\\s*=\\s*function\\(\\w+\\)\\{\\s*" + "[\'\"]return\\s*\\w+\\s*[\'\"];\\s*"
				+ "return\\s+\\w+;\\s*" + "\\};";
		Pattern p3 = Pattern.compile(regex3);
		Matcher m3 = p3.matcher(script);
		// 找出所有匹配的字串,存入List中
		List<String> l3 = new ArrayList<String>();
		while (m3.find()) {
			l3.add(m3.group());
		}
		for (String str : l3) {
			Pattern p31 = Pattern.compile(regex3);
			Matcher m31 = p31.matcher(str);
			while (m31.find()) {
				String name31 = m31.group(1);
				// 再次利用正則,在js中擷取ZA__(',5_198')
				String regex32 = name31 + "\\(([^\\)]+)\\)";
				Pattern p32 = Pattern.compile(regex32);
				Matcher m32 = p32.matcher(script);
				while (m32.find()) {
					String value32 = m32.group(1);
					script = script.replaceAll(regex32, value32);
				}
			}

		}
		// System.out.println(script);
		// 4.var 無引數 返回常量 函式
		/*
		 * 類似於此結構 var jW_ = function() { 'return jW_'; return '34;'; };
		 */
		// 需要替換jW_()為34;
		String regex4 = "var\\s*([^=]+)=\\s*function\\(\\)\\s*\\{\\s*" + "[\'\"]return\\s*\\w+\\s*[\'\"];\\s*"
				+ "return\\s*[\'\"]([^\'\"]+)[\'\"];\\s*" + "\\};";
		Pattern p4 = Pattern.compile(regex4);
		Matcher m4 = p4.matcher(script);
		// 找出所有匹配的字串,存入List中
		List<String> l4 = new ArrayList<String>();
		while (m4.find()) {
			l4.add(m4.group());
			/*
			 * String name4 = m4.group(1);//獲取第一個括號裡的內容 String value4 =
			 * m4.group(2);//獲取第一個括號裡的內容 script =
			 * script.replaceAll(name4+"\\(\\)", value4);
			 */
		}
		for (String str4 : l4) {
			Pattern p41 = Pattern.compile(regex4);
			Matcher m41 = p41.matcher(str4);
			while (m41.find()) {
				String name41 = m41.group(1);// 獲取第一個括號裡的內容
				String value41 = m41.group(2);// 獲取第一個括號裡的內容
				script = script.replaceAll(name41 + "\\(\\)", value41);
			}
		}
		// 5.無引數 返回常量 函式
		/*
		 * 類似於此結構 function HB_() { 'return HB_'; return '_;'; }
		 */
		// 需要將HB_()替換為_;
		String regex5 = "function\\s*(\\w+)\\(\\)\\s*\\{\\s*" + "[\'\"]return\\s*[^\'\"]+[\'\"];\\s*"
				+ "return\\s*[\'\"]([^\'\"]+)[\'\"];\\s*" + "\\}\\s*";
		Pattern p5 = Pattern.compile(regex5);
		Matcher m5 = p5.matcher(script);
		// 找出所有匹配的字串,存入List中
		List<String> l5 = new ArrayList<String>();
		while (m5.find()) {
			l5.add(m5.group());

		}
		// 遍歷每一條匹配到的字串
		for (String str5 : l5) {
			Pattern p51 = Pattern.compile(regex5);
			Matcher m51 = p51.matcher(str5);
			while (m51.find()) {
				String name51 = m51.group(1);// 獲取第一個括號裡的內容,也就是HB_
				String value51 = m51.group(2);// 獲取第二個括號裡的內容,也就是-;
				String regex51 = name51 + "\\(\\)";// 組合HB_()的正則表示式
				script = script.replaceAll(regex51, value51);
			}

		}
		// 6.無引數 返回常量 函式 中間無混淆程式碼
		/*
		 * 類似於此結構的 function do_() { return ''; } 需要將do_()替換為
		 */
		String regex6 = "function\\s*(\\w+)\\(\\)\\s*\\{\\s*" + "return\\s*[\'\"]([^\'\"]*)[\'\"];\\s*" + "\\}\\s*";
		Pattern p6 = Pattern.compile(regex6);
		Matcher m6 = p6.matcher(script);
		// 找出所有匹配的字串,存入List中
		List<String> l6 = new ArrayList<String>();
		while (m6.find()) {
			l6.add(m6.group());
		}
		for (String str6 : l6) {
			Pattern p61 = Pattern.compile(regex6);
			Matcher m61 = p61.matcher(str6);
			while (m61.find()) {
				String name61 = m61.group(1);
				String value6 = m61.group(2);
				String regex61 = name61 + "\\(\\)";
				script = script.replaceAll(regex61, value6);
			}
		}

		// System.out.println(script);
		// 7.字串拼接時使無參常量函式
		/*
		 * 類似於下面這種結構 (function() { 'return rv_'; return '%' })() 將上面這個替換為%
		 */

		String regex7 = "\\(function\\(\\)\\s*\\{\\s*" + "[\'\"]return[^\'\"]+[\'\"];\\s*"
				+ "return\\s*([\'\"][^\'\"]*[\'\"]);?\\s*" + "\\}\\)\\(\\)";
		Pattern p7 = Pattern.compile(regex7);
		Matcher m7 = p7.matcher(script);
		// 找出所有匹配的字串,存入List中
		List<String> l7 = new ArrayList<String>();
		while (m7.find()) {
			l7.add(m7.group());

		}
		// 遍歷每一個匹配的字串,並替換為實際的值
		for (String str7 : l7) {
			Pattern p71 = Pattern.compile(regex7);
			Matcher m71 = p71.matcher(str7);
			while (m71.find()) {
				String value7 = m71.group(1);
				script = script.replace(str7, value7);

			}
		}

		// 8.字串拼接時使用返回引數的函式
		/*
		 * 類似於以下這種結構 (function(eR__) { 'return eR_'; return eR__; })('%9')
		 * 將整體替換為%9
		 */

		String regex8 = "\\(function\\(\\w+\\)\\s*\\{\\s*" + "[\'\"]return[^\'\"]+[\'\"];\\s*" + "return\\s*\\w+;\\s*"
				+ "\\}\\)\\(([\'\"][^\'\"]*[\'\"])\\)";
		Pattern p8 = Pattern.compile(regex8);
		Matcher m8 = p8.matcher(script);
		// 找出所有匹配的字串,存入List中
		List<String> l8 = new ArrayList<String>();
		while (m8.find()) {
			l8.add(m8.group());

		}
		// 遍歷每一個匹配的字串,並替換為實際的值
		for (String str8 : l8) {
			Pattern p81 = Pattern.compile(regex8);
			Matcher m81 = p81.matcher(str8);
			while (m81.find()) {
				String value8 = m81.group(1);
				script = script.replace(str8, value8);

			}
		}

		// .獲取所有var pz_='8'格式的變數
		Pattern p = Pattern.compile("var \\w+=\'.*?\'");
		Matcher m = p.matcher(script);
		while (m.find()) {
			if (m.group().contains("<")) {
				continue;
			}
			// System.out.println(m.group());
			String varName = m.group().split(" ")[1].split("=")[0];
			String varValue = m.group().split(" ")[1].split("=")[1].replaceAll("'", "");
			script = script.replaceAll(varName, varValue);
		}

		// 將js中所有的空格,+,',都去掉
		script = script.replaceAll("[\\s+']", "");
		// ((?:%\w\w|[A-Za-z\d])+)
		Pattern p10 = Pattern.compile("((?:%\\w\\w)+)");
		Matcher m10 = p10.matcher(script);
		String[] words = {};
		while (m10.find()) {
			//System.out.println(m10.group());
			// 將在js端decodeURIComponent編碼的字串,用下面這種方法解碼.
			String result = new java.net.URI(m10.group()).getPath();
			words = result.split("");
			// System.out.println(Arrays.toString(words));
		}
		// 從 字串密集區域後面開始尋找索引區域,連續匹配十次以上,確保是索引
		Pattern p11 = Pattern.compile("([\\d,]+(;[\\d,]+)+){10,}");
		Matcher m11 = p11.matcher(script);
		String[] indexs = {};
		while (m11.find()) {
			indexs = m11.group().split("[;,]");
		}

		//System.out.println(Arrays.toString(indexs));

		// js中的混淆解決完成
		// #########################################################################
		// 開始替換<span>標籤
		// 獲取<span>裡class屬性的數字
		Pattern p12 = Pattern.compile("<span\\s*class=[\'\"]hs_kw(\\d+)_([^\'\"]+)[\'\"]></span>");
		Matcher m12 = p12.matcher(articleHtml);
		while (m12.find()) {
			// 將String型別的數字轉為int型別
			int num = Integer.parseInt(m12.group(1));
			// 以class屬性中的數字為下標,尋找對應的索引號.
			String index = indexs[num];
			// 將String型別的索引號轉為int型別
			int indexWord = Integer.parseInt(index);
			// 獲取每個<span>對應的文字
			String word = words[indexWord];
			articleHtml = articleHtml.replace(m12.group(), word);
		}
		Document artcileDoc = Jsoup.parse(articleHtml);
		article = artcileDoc.text().replaceAll(" ", "");// 獲取文字,去空格

		return article;
	}

	// 測試方法
	public static void main(String[] args) throws IOException, URISyntaxException {
		DecodeAutoHome d = new DecodeAutoHome();
		String url = "http://club.autohome.com.cn/bbs/thread-c-3980-65944945-1.html";
		String article = d.getArticle(url);
		System.out.println(article);
	}
}



輸出結果

昌河Q35帶你來到浙南最美時尚體育小鎮――泰順百丈   “我愛我的家鄉,我愛這片土地,
所以我眼中總是飽含著熱淚”。――,今天我就開著昌河Q35帶諸位看官老爺逛逛我的家鄉――泰順百丈。
當然,作為一個業餘的車評人,我會告訴大家昌河Q35最專業的用車感受。昌河Q35有個姐姐叫紳寶X35,
樣子都長的不錯,可惜今天姐姐不在,先給大家介紹一下妹妹。當你上一輛車的時候,最直觀的感受就是加速,
如果說發動機是一輛車的心臟,那變速箱無疑就是它的靈魂。昌河Q35全系搭載均為一臺型號為A151的1.5L自然吸
氣發動機,最大功率為116馬力(85千瓦),峰值扭矩為148牛·米,傳動系統匹配的是5擋手動變速箱和4速手自一體
變速箱,此車型搭載的是4速手自一體變速箱。昌河Q35的動力系統與紳寶X35完全相同。Q35的定位非常年輕活潑,
但是年輕往往是衝勁有餘,後勁不足,所以0到40的加速有點猛,到80中規中矩,到100,我只能呵呵了。當然,
在高速上Q35還是很容易的就可以讓你吃到罰單的,一段高速跑下來,感覺這個車好開,容易上手,懸掛不硬,轉
向不神經質,只要你緩緩加油,變速箱也都能領會,不過給你過多的頓挫感,但是,我真的要說但是,沒有一點駕
駛樂趣。比較適合居家過日子,狂野你就別指望了。都說去西藏一定要自駕,因為風景都在路上,來到泰順何嘗不
是,隨手一拍,這鬱鬱蔥蔥的美麗公路都可以給你做屏保。再說,泰順也稱之為溫州的青藏高原。在西藏,是眼睛的
天堂身體的地獄,而在泰順,也是如此,因為這裡的新鮮空氣和氧離子含量會讓吸慣了霧霾和尾氣的你很不適應,甚
至醉倒。泰順沿線路邊都有農家樂,味道正宗價格便宜,最重要的是,健康。過了這條隧道,再有15分鐘車程,就能到
達我們今天行程目的地―百丈,為什麼此刻我有心跳聲,因為那裡有我刻骨銘心的夢想,
因為不曾完成。跑山路的時候我就後悔了,為什麼我不選一臺手動的呢,老司機都會懂的哦轉眼間,
已經到這個浙南最美小鎮百丈了,滿眼的湖光山色,藍天白雲,厭倦了城市喧囂的我,只想在此一屋二人一日三餐,逍遙餘生
。傳說宋朝初年從飛雲湖下游的平陽坑灘腳上溯至該地需要經過99灘。清朝泰順《分疆錄》一書則記載“百丈謠”曰“百丈百灘,
一灘一丈,迢迢羅陽,如在天上”,於是“百丈”之名。建鎮於1935年,是當地唯一的自然鎮,自古就是水運埠頭,建國前後曾
是浙、閩兩省七縣的物資吞吐口岸,是有名的百年商埠,被稱為“小上海”,輝煌一時。後為解決500萬溫州群眾的飲水問題,
這裡就成了大水缸,浙江省第二大人工湖。歷經十年沉寂後,而今迎來全新發展,變身為時尚體育小鎮,成為賽艇運動員與遊客的天堂。
百丈入口處,時尚體育小鎮的主題與Q35時尚動感不謀而合。昌河和百丈的發展是非常相似的,有歷史、有故事、有變革,有許多分
分合合,歷經沉浮後顛覆了過往,都以新貌出現在世人前面,是希望還是瓶頸,且笑看風雲。百丈是名副其實的紅色歷史文化古鎮。
1935年11月至1937年1月,中共南坑洋區委、壽泰線蘇維埃政府就在百丈鎮黃坑地區建立以夏明君為主席的南坑洋蘇維埃政府。那段歷
史值得銘記。這裡是百丈鎮新建的紅色文化主題公園。在《戰狼2》燃爆的八月,紅色的確非常給力。緩緩駛入小鎮,運動氣息撲面而
來。Q35的整體造型顛覆了我對昌河的一貫認知,就像這裡曾經是深山密林,而今一條條彩道可以讓你騎行健身,也可以漫步湖邊。在
這裡,道路即賽道。百丈的客運碼頭,可以乘船遊湖。訓練中的運動員,誰曾想過,原來那個人比狗少的荒寂小鎮,因為眾多國家賽
艇運動員的入駐,又變得生機勃勃了。湖下是曾經的老百丈,現在來自全國的青年賽艇運動員正在水面上為榮譽揮灑著汗水。青山綠
水中皮划艇訓練,美醉了吧2017全國青年賽艇錦標賽在百丈鎮的直升機停機坪舉行。賽事剪影賽艇的下水點矯健的運動員在搬運比賽
用的賽艇。我在水中劈波斬浪,也欲與天公試比高長期的戶外訓練被晒的黝黑的面板勾勒出的線條讓我很是羨慕,我在健身房這麼久
都沒練出來。頒獎儀式現場拿了獎牌的運動員笑的很開心,一分汗水一分回報,當然還是天賦最重要。除了賽艇,百丈鎮還有曲棍球
訓練基地。百丈的第一家海鮮鋪,食材新鮮,以往百丈當地人買菜都是靠外面車子拉進來的,很多時候沒菜吃了也要等上兩三天,現在
好了在家門口就可以買到新鮮的食材了。而且,最關鍵的是,老闆娘好漂亮,人也很好,大家下次去的時候可以關顧一下,在此我就不
發老闆娘美圖了,留點神祕感等你。看完賽事,還有正事,那就自己動手洗車,然後奉上車輛細節。加了透鏡的車燈凌厲有神。大面積
的霧燈符合SUV的定位,看上去很狂野。輪轂的造型很運動噴了紅色卡鉗,當然裝飾的作用大些。尾燈與北汽紳寶X35的差異化較為明顯。
這個屁股看上去還是很耐操的,就是菊花小了點,很容易夾手。這裡做了個懸浮式腰線的設計,我覺得是提升了車輛的檔次感。接縫處
做工有所進步,但還有待提高。全系標配的行李架是個加分項,如果沒有行李架,這車完全就不是這感覺了。避震,底盤懸掛偏軟,但
韌性和路面的反饋還是不錯的。車輛的底盤還算工整,此處應當有掌聲,我可是趴在60度的路面上拍的。後背箱的容積和工整度都不錯
。第二排座椅可以按比例放倒,日常使用足夠了。後備箱內有隔層,可以放些雜物。來張大鵬展翅,我們一起看內飾吧。昌河Q35與紳寶
X35的內飾,除了車標外,其他幾乎沒什麼分別,整體佈局配色都很不錯,但是塑料感偏強,當然這個價位的車子你不可能要求它有賓士
的做工。空調出風口的造型是不錯,但使用起來總覺得還是常規的方便門板上的仿皮設計還是能營造出一些豪華感的。從這個角度看,
如果不是中間那個很LOW的顯示屏,我都覺得自己是在開一輛跑車。方向盤的手感還是不錯的,轉向力度也輕,適合女生駕駛。配備車身
穩定控制系統。這個地方我真的要好好批評一下了,為什麼做的一點阻尼都沒有,鬆鬆垮垮,根本找不到感覺。Q35這四速變速箱是來自
愛信的嗎?平路開開還可以,但是山路爬坡,我真寧可手動,而且檔位的清晰度真不好,掛倒擋容易掛到空擋去,掛D擋又容易掛到2擋去,
可能是我最近健身練得力氣變大了,有點控制不住吧。又一裝逼神器,真越野的話,我怕會被我掰斷。被架子和放硬幣的,不能擱太高的杯
子,不然剎車時候容易甩出來。扶手箱空間不算大,剛好讓我放個錢包和眼鏡盒,不過,主要是我的錢包比較大裝逼到牙齒,看到這個我
好想給你加個T。座椅是真心舒服,軟硬適中。腰部頭部臀部支撐都很到位。關鍵部位還打孔。後排的中間也是有頭枕的,而且這種造型的
頭枕不會讓脖子受累。這個後排空間不能說寬敞,但也不至於侷促,符合緊湊定位。後配配有兒童安全鎖的。前大燈前燈夜間照明效果還
算不錯,但是與近光燈交集的地方會有重影,看著不爽。尾燈歷經幾天的糾結與修改終於把這篇帖子寫完了,因為在寫這篇帖子的時候,我
做了十年來最重要的決定,我要辭職了,回到百丈去創業。十年中,我無數次的問自己:“十年的體制內生活,你厭倦嗎?”我不厭倦,是憎
恨,辭職是這十年來最讓我向往也是最開心興奮的決定了,我的前半生是在無數的檔案、報告和會議中度過的,而後半生,我想換一種活法了。

注意 :汽車之家還在不斷的更新,所以程式碼是有時效性的.