爬虫笔记之自如房屋价格图片识别(价格字段css背景图片偏移显示)
一、前言
自如房屋详情页的价格字段用图片显示,特此破解一下以丰富一下爬虫笔记系列博文集。
二、分析 & 实现
先打开一个房屋详情页观察一下;
网页的源代码中没有直接显示价格字段,价格的显示是使用一张背景图,图上是0-9十个数字,然后网页上显示的时候价格的每一个数字对应着一个元素,元素的背景图就设置为这张图片,然后使用偏移定位到自己对应的数字:
就拿上面这个例子来说,它对应的背景图是:
这张图宽30*10=300px,每个数字宽度是30px,网页上价格每个元素实际显示的数字在图片中数字的下标映射公式为:
1
|
Math.abs(style_background-position_value) / 30
|
拿这个房屋价格代入:
1
2
3
4
|
第一个数字的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。
源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
|
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;
/**
* 自如的房租价格用图片显示,这是一个从图片中解析出价格的例子
*
*
*
* @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 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折腾半天,反爬机制对应的破解方案应该不具有通用性并且成本比较高这个反爬做得才有意义,否则爬虫方面投入很小的成本(时间 & 经济上的投入)就破解了那这反爬相当于白做哇。