使用JAVA爬取北京豆瓣租房小组租房信息

最近租的房子到期需要租房,为了获取租房信息,尝试爬取北京豆瓣租房小组的租房信息,本文主要介绍了如何使用Java爬虫根据关键词爬取北京豆瓣租房小组上的租房信息,使用OKhttp作为网络请求的客户端,使用Jsoup解析网页的html,最终的结果使用csv存储,首先看效果: 使用时,首先需要输入检索的关键词,按回车开始爬取信息:

avatar avatar avatar avatar avatar

大体思路是首先初始化租房列表页面的url列表,然后爬取每个列表页面,过滤title,如果title不包含关键词,则记录详情页面的超链接,全部租房列表页面访问完毕后,开多线程访问详情页面,爬取内容,查看内容是否包含关键词。

一、信息爬取

第一步要做的是将要爬取的租房列表页url加入列表,使用chrome查看网页上的分页超链接,如下:

avatar avatar

可以看到每页信息的地址为:

https://www.douban.com/group/26926/discussion?start=0

其中参数start,从0开始,每次递增25。 因此,初始化时,将要爬取的url加入一个队列:

avatar

初始化时,从start为0开始初始化,为了保证最新发布的消息在前面,使用队列存储。 之后,开始访问每个列表页面,爬取上面的租房链接信息,优先查看本身超链接的title,如果包含需要搜索的关键字,则不用再去详情页爬取详细内容,直接加入结果列表。列表页中租房信息超链接信息如下:

avatar

由于Jsoup并不支持xpath,支持使用select选择器获取元素,我们首先获取全部“class=title”的td节点,然后遍历每个td节点,取出其中“a”超链接的标题和链接("href")信息,示例代码如下:

String html = StringUtil.responseToString(response);
        Document document = Jsoup.parse(html);
        //获取class=title的td戒牒
        Elements elements = document.select("td[class=title]");
        if (elements != null && elements.size() > 0) {
            for (Element element : elements) {
                //获取其中的超链接信息
                Elements aElements = element.getElementsByTag("a");
                String title = aElements.text();
                String href = aElements.attr("href");
                String detailUrl = UrlUtil.getAbsoluteUrl(pageUrl, href);
                if (checkString(title) && !sCheckedRes.containsValue(detailUrl)) {
                    sCheckedRes.put(title, detailUrl);
                } else {
                    if (!sDetailUrls.containsValue(detailUrl) && !sVisitedUrls.contains(detailUrl)) {
                        sDetailUrls.put(title, detailUrl);
                    }
                }
            }
        }

检查title信息,如果包含需要搜索的关键词,则直接加入结果列表,如果title不包含关键词,则将详情页面的超链接加入url列表。 全部租房列表页面爬取完毕后,得到详情页的url列表,开启多线程爬取详情页面,使用jsoup的选择语法来获取详细描述信息,使用chrome查看租房详细描述信息如下:

avatar

位于“class=topic-content”的div节点下的p节点下,因此可以使用"div[class=topic-content] > p"来获取信息,示例代码如下:

 private static void visitedDetailUrl(String title, String detailUrl) {
    System.out.println("正在爬取详情页面--->" + detailUrl);
    sVisitedUrls.add(detailUrl);
    Response response = NetClient.doGet(detailUrl);
    if (response != null && response.isSuccessful()) {
        String html = StringUtil.responseToString(response);
        Document document = Jsoup.parse(html);
        Elements elements = document.select("div[class=topic-content] > p");
        String content = elements.text();
        if (checkString(content) && !sCheckedRes.containsValue(detailUrl)) {
            sCheckedRes.put(title, detailUrl);
        }
    }
    response.body().close();
}

二、信息保存

通过爬虫爬取的信息需要保存到本地进行持久化存储,可以保存到Sqlite数据库,这里为了阅读方便,直接保存到了csv文件,保存链接和链接的标题信息,保存csv中,不同项之间使用','隔开,代码如下:

public static void writeCSV(String filePath, HashMap<String, String> data, boolean append, String charset) {
    File file = new File(filePath);
    if (file != null && file.exists() && file.isFile()) {
        BufferedWriter bfw = null;
        try {
            bfw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, append), charset));
            Set<Entry<String, String>> entrySet = data.entrySet();
            for (Iterator<Entry<String, String>> iterator = entrySet.iterator(); iterator.hasNext(); ) {
                Entry<String, String> entry = iterator.next();
                String key = entry.getKey();
                String value = entry.getValue();
                bfw.write(key + "," + value);
                bfw.write("\n");
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (bfw != null) {
                    bfw.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

三、注意的问题

1、豆瓣机器人验证问题 在爬取豆瓣小组的过程中,频繁的爬取会造成服务器返回403错误,可以休眠一段时间后继续爬取。 有时,爬取速度过快,豆瓣会认为是机器人,从而跳转到如下页面:

avatar

使用chrome查看信息如下:

avatar

手动提交验证码后,使用chrome或者fiddler抓包可以看到发起了一个post请求,提交的信息如下:

avatar

其中,"captcha-solution"为验证码的值,"captcha-id"为前一个页面中的隐藏的"captcha-id"的值,识别验证码可以采用开源的识别方案,这里,为了简便,我们下载验证码后,使用系统自带的图片查看器显示图片,让用户手动输入验证码。使用select选择器获取验证码链接和"captcha-id",验证码链接位于"alt=captcha"的img中,"captcha-id"位于"name=captcha-id"的input中,示例代码如下:

private static void processRobotCheckPage() {
    String checkUrl = "https://www.douban.com/misc/sorry";
    System.out.println("处理异常情况--->" + checkUrl);
    Response response = NetClient.doGet(checkUrl);
    if (response != null) {
        String html = StringUtil.responseToString(response);
        Document document = Jsoup.parse(html);
        Elements img = document.select("img[alt=captcha]");
        String src = img.attr("src");
        String id = document.select("input[name=captcha-id]").attr("value");
        HashMap<String, String> params = new HashMap<String, String>();
        // params.put( "ck", "5txN" );
        Response imgResponse = NetClient.doGet(src);
        if (imgResponse != null && imgResponse.isSuccessful()) {
            FileUtil.saveInputStreamToFile(CAPTCHA_PATH, imgResponse.body().byteStream());
        }
        ImageUtil.showPic(CAPTCHA_PATH);
        System.out.println("请输入图形验证码:");
        String code = sScanner.next();
        params.put("captcha-id", id);
        params.put("captcha-solution", code);
        Response checkResponse = NetClient.doPost(checkUrl, params);
        if (checkResponse != null && 403 != checkResponse.code()) {
            System.out.println("验证成功!");
        } else {
            System.out.println("验证失败--->" + checkResponse.code());
        }
        response.body().close();
        imgResponse.body().close();
    }
}

2、防止重复爬取URL 在此爬取过程中不会出现url重复,造成重复爬取循环的问题,避免这种问题的做法是,将每个爬取过的url加入一个列表,爬取页面发现新的超链接时,首先判断该链接是否被访问过。 3、为了正常品爬取网页,需要加入一些头部信息:

   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;
}

4、由于头部信息中,加入了"Accept-Encoding : gzip,defate,br",导致接收到的response进行了gzip压缩,可以使用GZIPInputStream转换成正常的InputStream:

avatar

5、登录问题 有时爬取过多或者爬取频率过快,会导致豆瓣认证是否是人的行为,需要登录来验证,登录逻辑同问题1中的豆瓣机器人验证的解决办法,同样的也是需要验证码信息,以及用户名密码信息,发起post请求登录。

全部示例代码如下: 示例代码