一、前言
自如房屋详情页的价格字段用图片显示,特此破解一下以丰富一下爬虫笔记系列博文集。
二、分析 & 实现
先打开一个房屋详情页观察一下;
网页的源代码中没有直接显示价格字段,价格的显示是使用一张背景图,图上是0-9十个数字,然后网页上显示的时候价格的每一个数字对应着一个元素,元素的背景图就设置为这张图片,然后使用偏移定位到自己对应的数字:
就拿上面这个例子来说,它对应的背景图是:
这张图宽30*10=300px,每个数字宽度是30px,网页上价格每个元素实际显示的数字在图片中数字的下标映射公式为:
Math.abs(style_background-position_value) / 30
拿这个房屋价格代入:
第一个数字的background-position:-30px,带入得1,对应背景图中的第1个数字(下标从0开始),即为1
第二个数字的background-position:-60px,带入得2,对应背景图中的第2个数字,即为9
第三个数字的background-position:-90px,带入得3,对应背景图中的第3个数字,即为3
第四个数字的background-position:-240px,带入得8,对应背景图中的第8个数字,即为0
拼接起来得到最终价格:1930,与页面上显示的价格吻合。
其实并没有那么复杂,每一位对应图片中的数字的下标并不需要自己根据css计算,这个对应下标是在详情页的接口中返回的:
price是个数组,第一个元素是背景图的小图,第二个元素是背景图的大图,第三个元素是价格字段对应背景图中的第几个数字,有这几个信息足够识别出价格字段了,先从背景图中将价格对应的数字图片割出来,然后识别出来按顺序拼接起来再转为数字即可。
下面是识别价格字段的一个小Demo,依赖了我之前写的一个字符图片识别的小工具:commons-simple-character-ocr。
源码:
package cc11001100.crawler.ziroom; import cc11001100.ocr.OcrUtil;
import cc11001100.ocr.clean.SingleColorFilterClean;
import cc11001100.ocr.split.ImageSplitImpl;
import cc11001100.ocr.util.ImageUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jsoup.Jsoup; import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map; import static com.alibaba.fastjson.JSON.parseObject;
import static java.util.stream.Collectors.joining; /**
* 自如的房租价格用图片显示,这是一个从图片中解析出价格的例子
*
*
* <a>http://www.ziroom.com/z/vr/250682.html</a>
*
* @author CC11001100
*/
public class ZiRoomPriceGrab { private static final Logger log = LogManager.getLogger(ZiRoomPriceGrab.class); private static SingleColorFilterClean singleColorFilterClean = new SingleColorFilterClean(0XFFA000);
private static ImageSplitImpl imageSplit = new ImageSplitImpl();
private static Map<Integer, String> dictionaryMap = new HashMap<>(); static {
dictionaryMap.put(-2132100338, "0");
dictionaryMap.put(-458583857, "1");
dictionaryMap.put(913575273, "2");
dictionaryMap.put(803609598, "3");
dictionaryMap.put(-1845065635, "4");
dictionaryMap.put(1128997321, "5");
dictionaryMap.put(-660564186, "6");
dictionaryMap.put(-1173287820, "7");
dictionaryMap.put(1872761224, "8");
dictionaryMap.put(-1739426700, "9");
} public static JSONObject getHouseInfo(String id, String houseId) {
String url = "http://www.ziroom.com/detail/info?id=" + id + "&house_id=" + houseId;
String respJson = downloadText(url);
if (respJson == null) {
throw new RuntimeException("response null, id=" + id + ", houseId=" + houseId);
}
return parseObject(respJson);
} private static int extractPrice(JSONObject houseInfo) throws IOException {
JSONArray priceInfo = houseInfo.getJSONObject("data").getJSONArray("price");
String priceRawImgUrl = "http:" + priceInfo.getString(0);
System.out.println("priceRawImgUrl: " + priceRawImgUrl);
JSONArray priceImgCharIndexArray = priceInfo.getJSONArray(2);
System.out.println("priceImgCharIndexArray: " + priceImgCharIndexArray);
BufferedImage img = downloadImg(priceRawImgUrl);
if (img == null) {
throw new RuntimeException("img download failed, url=" + priceRawImgUrl);
}
List<BufferedImage> priceCharImgList = extractNeedCharImg(img, priceImgCharIndexArray);
String priceStr = priceCharImgList.stream().map(charImg -> {
int charImgHashCode = ImageUtil.imageHashCode(charImg);
return dictionaryMap.get(charImgHashCode);
}).collect(joining());
return Integer.parseInt(priceStr);
} // 因为价格通常是4位数,而返回的图片有10位数(0-9),所以第一步就是将价格字符抠出来
// (或者也可以先全部识别为字符串然后从字符串中按下标选取)
private static List<BufferedImage> extractNeedCharImg(BufferedImage img, JSONArray charImgIndexArray) {
List<BufferedImage> allCharImgList = imageSplit.split(singleColorFilterClean.clean(img));
List<BufferedImage> needCharImg = new ArrayList<>();
for (int i = 0; i < charImgIndexArray.size(); i++) {
int index = charImgIndexArray.getInteger(i);
needCharImg.add(allCharImgList.get(index));
}
return needCharImg;
} private static byte[] downloadBytes(String url) {
for (int i = 0; i < 3; i++) {
long start = System.currentTimeMillis();
try {
byte[] responseBody = Jsoup.connect(url)
.userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36")
.ignoreContentType(true)
.execute()
.bodyAsBytes();
long cost = System.currentTimeMillis() - start;
log.info("request ok, tryTimes={}, url={}, cost={}", i, url, cost);
return responseBody;
} catch (Exception e) {
long cost = System.currentTimeMillis() - start;
log.info("request failed, tryTimes={}, url={}, cost={}, cause={}", i, url, cost, e.getMessage());
}
}
return null;
} private static String downloadText(String url) {
byte[] respBytes = downloadBytes(url);
if (respBytes == null) {
return null;
} else {
return new String(respBytes);
}
} private static BufferedImage downloadImg(String url) throws IOException {
byte[] imgBytes = downloadBytes(url);
if (imgBytes == null) {
return null;
}
return ImageIO.read(new ByteArrayInputStream(imgBytes));
} private static void init() {
// OcrUtil ocrUtil = new OcrUtil().setImageClean(new SingleColorFilterClean(0XFFA000));
// ocrUtil.init("H:/test/crawler/ziroom/raw/", "H:/test/crawler/ziroom/char/");
OcrUtil.genAndPrintDictionaryMap("H:/test/crawler/ziroom/char/", "dictionaryMap", filename -> filename.substring(0, 1));
} public static void main(String[] args) throws IOException {
// init(); JSONObject o = getHouseInfo("61718150", "60273500");
int price = extractPrice(o);
System.out.println("price: " + price); // 1930 // output:
// 2018-12-15 20:24:59.206 INFO cc11001100.crawler.ziroom.ZiRoomPriceGrab 103 downloadBytes - request ok, tryTimes=0, url=http://www.ziroom.com/detail/info?id=61718150&house_id=60273500, cost=559
// priceRawImgUrl: http://static8.ziroom.com/phoenix/pc/images/price/ba99db25b3be2abed93c50c7f55c332cs.png
// priceImgCharIndexArray: [6,3,8,1]
// 2018-12-15 20:24:59.538 INFO cc11001100.crawler.ziroom.ZiRoomPriceGrab 103 downloadBytes - request ok, tryTimes=0, url=http://static8.ziroom.com/phoenix/pc/images/price/ba99db25b3be2abed93c50c7f55c332cs.png, cost=146
// price: 1930 } }
三、总结
自如的房屋价格图片显示类似于新蛋的商品价格图片显示,此类反爬措施破解难度较低,比较致命的是破解方案具有通用性,这意味着随便找个图片识别的库怼上就行,所以还不如自研个比较复杂的js加密来反爬呢,你要想高效的爬取就得来分析js折腾半天,反爬机制对应的破解方案应该不具有通用性并且成本比较高这个反爬做得才有意义,否则爬虫方面投入很小的成本(时间 & 经济上的投入)就破解了那这反爬相当于白做哇。
相关资料:
2. commons-simple-character-ocr
.