本文主要讲述了如何爬取链家房源信息,并使用MySql持久化存储信息,同时,分析了链家的登录、处理验证码等过程。
爬取的整个流程如下:
链家登录
通过Fiddler抓包可以看到,PC网页版链接登录时,会发起两个请求:
第一个请求为get请求,请求这个地址时,会获取到json数据
解析其中的"data",在登录时会用到。
第二个请求为post请求,为登录请求,请求post发送的数据为:
其中的lt即为第一个请求获取到的data,可以看到,密码采用明文传输,登陆的返回值如下:
通过判断"success"是否等于1来判断是否登录成功。
获取房源信息的URL
链家二手房源的url的构成为:
http://bj.lianjia.com/ershoufang/ + 区域名称 + /pg + 页数
区域名称为各区对应的汉语拼音。
public static final String[] DISTRICTS = {"dongcheng", "xicheng", "haidian", "fengtai", "shijingshan", "tongzhou"
, "changping", "daxing", "yizhuangkaifaqu", "shunyi", "fangshan", "mentougou", "pingu", "huairou"
, "miyun", "yanqing", "yanjiao"};
其中的页数,可以去访问各个区域的第一页获取:
总的页数位于"class=page-box house-list-page-box"的div节点的"page-data"属性,是一个json,解析其中的"totalPage"即可获取当前区域的总页数,构造出当前区域的所有URL。
爬取房源信息
访问房源链接,房源信息如下:
需要获取的房源信息主要为标题、位置信息、房屋描述信息、小区名称、单价、总价、标签(满五唯一)等,构造bean如下:
public class HouseBean {
//详细url
private String detailUrl;
//总价
private float totalPrice;
//单价
private float unitPrice;
//标签 例如满五唯一
private String tag;
//房屋信息 | 2室1厅 | 101.77平米 | 南 北 | 简装 | 有电梯
private String description;
//位置信息
private String positionInfo;
//小区
private String community;
//面积
private float area;
//标题
private String title;
//区域
private String district;
......
}
使用chrome查看网页的源码,可以看到房源的详细信息位于"class=clear"的div下:
使用Jsoup的选择器获取详细的节点,例如获取小区名称,"class=houseInfo"的div节点下的直接"a"节点的内容即为小区名称:
因此,select路径可以写为:
element.select("div[class=houseInfo] > a").first().text()
整个房源信息的解析如下:
private static HouseBean getHouseBean(Element element, String url) {
if (element != null) {
HouseBean houseBean = new HouseBean();
String title = element.select("div[class=title] > a").first().text();
houseBean.setTitle(title);
houseBean.setDetailUrl(element.select("div[class=title] > a").first().attr("href"));
houseBean.setTotalPrice(Float.valueOf(element.select("div[class=totalPrice] > span").first().text()));
String unitPriceStr = element.select("div[class=unitPrice] > span").first().text();
houseBean.setUnitPrice(Float.valueOf(unitPriceStr.substring(unitPriceStr.indexOf("价") + 1, unitPriceStr.indexOf("元"))));
houseBean.setDescription(element.select("div[class=houseInfo]").first().text());
houseBean.setArea(CommonUtil.getAreaFromDescription(houseBean.getDescription()));
houseBean.setCommunity(element.select("div[class=houseInfo] > a").first().text());
houseBean.setPositionInfo(element.select("div[class=positionInfo]").text());
houseBean.setTag(element.select("div[class=tag]").first().text());
houseBean.setDistict(CommonUtil.getDistrictFromUrl(url));
return houseBean;
}
return null;
}
为了保证爬取速度,使用多线程来爬取详细的房源信息,使用线程池,将所有的链接加入线程池,之后,调用线程池的"shutdown"的方法停止接收新的任务,每隔一段时间检查一下任务是否执行完毕。
for (final String url : sUrls) {
ThreadPoolManger.getInstance()
.execute(() -> getDetailInfo(url));
}
等待所有线程执行完毕:
public void await(int unitTime, int checkTimes) {
// 执行一次,不再接收新的任务,继续执行之前的任务
ThreadPoolManger.getInstance().getThreadPool().shutdown();
try {
while (!ThreadPoolManger.getInstance().getThreadPool().awaitTermination(unitTime, TimeUnit.SECONDS) && checkTimes < 100) {
// 每隔一定时间检查一次线程是否执行完毕
System.out.println("尚未全部爬取完毕!");
checkTimes++;
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
ThreadPoolManger.getInstance().getThreadPool().shutdownNow();
e.printStackTrace();
}
ThreadPoolManger.getInstance().getThreadPool().shutdownNow();
}
最终爬取到了17712条数据,链家首页显示的总共有24253套房源。
MySql持久化存储
借助Spring对JDBC的支持,将爬取到的链接以及房源信息存储到数据库,如下图:
网络客户端
网络客户端使用的是HttpClient 4.5.3,在构造HttpRequst的时候,加入浏览器的头部信息,
private static HashMap<String, String> genDefaultPcHeaders() {
HashMap<String, String> headers = new HashMap<String, String>();
headers.put("Connection", "keep-alive");
headers.put("Accept", "*/*");
headers.put("User-Agent",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36");
headers.put("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
headers.put("Accept-Encoding", "gzip, deflate, br");
headers.put("Accept-Language", "zh-CN,zh;q=0.8");
return headers;
}
private static void addHeaders(HttpRequest request, Map<String, String> headers) {
if (headers == null || request == null || headers.size() == 0) {
return;
}
Set<Map.Entry<String, String>> set = headers.entrySet();
Iterator<Map.Entry<String, String>> iterator = set.iterator();
for (; iterator.hasNext(); ) {
Map.Entry<String, String> entry = iterator.next();
request.addHeader(entry.getKey(), entry.getValue());
}
}
在Https处理方面,忽略所有的Https证书,方法如下: 1、实现"javax.net.ssl.X509TrustManager"接口:
package com.wz.common.network;
import javax.net.ssl.X509TrustManager;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
public class X509TrustManagerImpl implements X509TrustManager {
public void checkClientTrusted(X509Certificate[] certificates, String authType) throws CertificateException {
// TODO Auto-generated method stub
}
public void checkServerTrusted(X509Certificate[] certificates, String authType) throws CertificateException {
// TODO Auto-generated method stub
}
public X509Certificate[] getAcceptedIssuers() {
// TODO Auto-generated method stub
X509Certificate[] certificates = new X509Certificate[]{};
return certificates;
}
}
注意,在"getAcceptedIssuers()"方法中,需要返回一个空的"X509Certificate"数组,而不是直接返回null,直接返回null在请求https时,会发生异常。 2、实现"HostnameVerifier"接口,直接返回"true" 3、在构造HttpClient实例时,将他们关联:
HttpClientBuilder builder = HttpClients.custom();
//HTTPS设置
SSLContext sslContext = null;
try {
sslContext = SSLContext.getInstance("TLS");
TrustManager tm = new X509TrustManagerImpl();
sslContext.init(null, new TrustManager[]{tm}, new java.security.SecureRandom());
builder.setSSLContext(sslContext);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
}
builder.setSSLHostnameVerifier(new HostnameVerifierImpl());
在Cookie方面,HttpClient会在请求时,自动带上Cookie,完成会话保持。
链家爬虫异常处理
当爬取过于频繁时,链家会将当前地址重定向到
"http://captcha.lianjia.com?redirect=当前地址"
,如下图:开始时,怎么也无法获取到"302"的Http返回码,HttpClient直接跳转到了验证码页面,返回"200",查阅资料发现,在HttpClient的设置中:
private static RequestConfig genRequestConfig() {
RequestConfig.Builder builder = RequestConfig.custom();
builder.setConnectionRequestTimeout(CONNECTION_REQUEST_TIMEOUT)
.setConnectTimeout(CONNECT_TIMEOUT)
.setSocketTimeout(SOCKET_TIMEOUT);
builder.setCookieSpec(CookieSpecs.STANDARD);
builder.setCircularRedirectsAllowed(true);
builder.setRedirectsEnabled(false);
return builder.build();
}
其中的"setRedirectsEnabled(bolean redirectsEnabled)"需要设置为false,不然HttpClient会直接跳转到重定向的地址,无法通过返回码判断是否重定向到了验证码页面。
使用Fiddler抓包可以发现,在访问验证码页面的同时,链家网页会同时请求http://captcha.lianjia.com/human获取验证码的一些信息,链家的验证码是展示四个图片,选择倒置的图片,这里的处理策略是获取图片信息,展示出来,识别出倒置图片的序号,获取的验证码信息如下:
其中,images为四张验证码图片,为图片base64编码之后的信息,uuid在之后的验证请求中会用到。
通过Fiddler抓包可以看到,验证请求采用的post请求,请求的地址为"http://captcha.lianjia.com/human",请求的数据如下图:
其中,uuid为刚才请求验证码信息中的uuid,_csrf为网页中隐藏的信息,查看源码:
此信息位于"name=_csrf"的input下,并且属性设置为隐藏。bitvalue为选择的图片序号,4张图片上往下,从左往右依次是"1,2,4,8",排列顺序也是上图json中的从上到下的顺序。post请求的结果如下:
失败结果:
成功结果:
项目源码位于:
源代码
爬取到的房源信息,导出excel:
房源信息汇总