Java爬虫

一 、 爬虫简介

  1. http://www.lete.com , 乐贷网其实就是爬虫的简单应用 ,发送一个商品连接 , 获取商品信息
  2. 目标
  1. 爬取京东所有商品的信息
  2. 封装在自己的Item实体类中
  1. 分析:
  1. 京东允许爬虫爬取数据么?
  1. 京东是允许爬虫的 , 没有反爬虫技术
  1. 爬虫产品:
  1. httpClient :但是httpClient抓取的是整个页面 , 整夜字符串的处理、解析比较繁琐 , 数据的定位非常不准确 。
  2. htmlUnit : 也获取整个页面 , 抓取页面也可以包含二次提交 , 数据定位也比较准确 , 但是爬取过程不稳定 , 在爬取过程中 需要断点续爬代码的编写 。
  3. jsoup: 是一款比较稳定 , 定位准确 , 包含二次提交的java爬虫技术 。
  4. python也可以做爬虫 , 使用beautifulSoup技术 ,底层原理与jsoup是一样的 。 只是语言不同。

jsoup

  1. 抓取整个页面
  2. 抓取整个网站(以京东为列 , 抓取从首页能获取所有的连接地址)
  3. 抓取页面中某一个定位的数据
  4. 抓取二次提交ajax(如 : price)
  5. 抓取其他的jsonp数据 (如: 商品描述)
  6. 以上五种问题 , 如果都能解决 ,那么使用jsoup爬取任何网站都是可行的 。

案例

  1. 整个页面
  1. 与httpclient无异
/**
     * 爬取网页
     * @throws IOException 
     * */
    @Test
    public void testt_01() throws IOException {
        String url = "http://www.jd.com"; 
        Connection connect = Jsoup.connect(url);
        Response execute = connect.execute();
        System.out.println(execute.body());
    }
  1. 整个网站
  1. 抓取绝大部分的连接地址
  2. 观察网站的连接大部分都是使用的a标签 , 连接在href中
  3. 使用jsoup定位a标签 , 获取所有a标签 , 然后获取href的值
/**
     * 爬取整个网站
     * @throws IOException 
     * */
    @Test
    public void test_02() throws IOException {
        String url = "http://www.jd.com";
        Document document = Jsoup.connect(url).get();
        //寻找a标签
        Elements elementsByTag = document.getElementsByTag("a");
        for(Element element :elementsByTag) {
            String href = element.attr("href");
            String val = element.val();
            System.out.println("连接地址:"+href + "---"+val);
        }
    }
  1. 定位信息
/**
         * 爬取一个网页中的信息
         * 定位具体标签中的数据
         * @throws IOException 
         * */
        @Test
        public void test_03() throws IOException {
            String url= "http://item.jd.com/4329035.html";

            //get请求 获取的是返回结构的document树
            //excute获取的是返回的所有数据
            Document doc = Jsoup.connect(url).get();
            //选择器与jQ中的选择器使用一致
            //为了 定位准确 , 使用父子选择器 , 确定唯一的定位
            Element select = doc.select("ul li .p-img a").get(0);
            System.out.println(select.attr("href"));
        }
  1. json二次提交获取信息
  1. 需要自己 寻找页面中发起 ajax的请求地址
/**
     * 抓取二次提交
     * 商品价格是页面加载之后又通过ajax获取的
     * @throws IOException 
     * */
    @Test
    public void test_04() throws IOException {
        String url = "http:///prices/mgets?skuIds=J_5089253";
        Response response = Jsoup.connect(url).ignoreContentType(true).execute();
        String  json = response.body();
        System.out.println(json);
        ObjectMapper mp = new ObjectMapper();
        JsonNode jn = mp.readTree(json);
        //[{"op":"8388.00","m":"9999.00","id":"J_5089253","p":"8388.00"}]
        //直接获取到的是数组  ,需要获取到第一个元素
        String price = jn.get(0).get("p").asText();
        System.out.println(price);
    }
  1. jsonp数据
/**
     * 获取jsonp请求数据
     * @throws IOException 
     * */
    @Test
    public void test_05() throws IOException {
        String url = "http://d.3.cn/desc/4329035";
        String jsonDesc = Jsoup.connect(url).ignoreContentType(true).execute().body();
        System.out.println(jsonDesc);
        String data = jsonDesc.substring(jsonDesc.indexOf("(")+1, jsonDesc.lastIndexOf(")"));
        System.out.println(data);
        ObjectMapper mp = new ObjectMapper();
        JsonNode jn = mp.readTree(data);
        String  desc = jn.get("date").asText();
        System.out.println(desc);

    }
  1. 爬取京东商品信息
/**
 * 爬取京东商品的所有商品信息
 * @author outman 
 * 2018 - 1 - 31 - 17:48
 * 步骤: 
 * 1. 先获取所有的商品三级分类链接
 * 2. 访问商品分类链接后获取一个分类下所有商品的链接(可能存在分页的情况)
 * 3. 访问商品链接后获取商品信息 
 * 
 * 过程中要十分注意异常的处理
 * 在爬取过程中一旦出现异常 , 后续的过程也将受到影响 , 导致整个数据错乱
 * */
public class JDCrawler {
    private static SqlSession session ; 
    static {
         //获取一个数据流
        InputStream in;
        try {
            in = Resources.getResourceAsStream("mybatis-config.xml");
            //创建一个工厂
            SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
            //创建一个会话
            session = factory.openSession(true);//true表示自动提交 , 默认为false , 需要手动提交
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    /**
     * 入口函数
     * @throws Exception 
     * */
    public static void main (String[] args) throws Exception {
        //测试
        //http://www.jd.com/allSort.aspx  商品 分类页面
//      getItemCatUrls("http://www.jd.com/allSort.aspx");
        //list.jd.com/list.html?cat=12379,13302,13313 某一分类下的商品展示页面
//      getItemsPageUrls("http://list.jd.com/list.html?cat=12379,13302,13313");
        //http://list.jd.com/list.html?cat=12379,13302,13313&page=2  商品展示页面
//      getItemUrls("http://list.jd.com/list.html?cat=12379,13302,13313&page=2");
        //item.jd.com/12017077901.html  商品信息页面
//      getItem("http://item.jd.com/12017077901.html");
        // 12017077901某一个商品的ID
//      getPrice(new Long("12017077901"));

        //完整测试
        List<String> itemCatUrls = getItemCatUrls("http://www.jd.com/allSort.aspx");
        for(String itemCaturl :itemCatUrls) {
            System.out.println("商品分类链接:"+itemCaturl);
            List<String> itemsPageUrls = getItemsPageUrls(itemCaturl);
            for(String itemsPageUrl : itemsPageUrls) {
                System.out.println("商品展示页面链接:"+itemsPageUrl);
                List<String> itemUrls = getItemUrls(itemsPageUrl);
                for(String itemUrl : itemUrls) {
                    System.out.println("商品链接:"+itemUrls);
                    Item item = getItem(itemUrl);
                    saveItem(item);
                    System.out.println(item);
                }
            }
        }
    }
    /**
     * 获取京东商品的所有分类链接
     * @throws Exception  
     * */
    public static List<String> getItemCatUrls(String url) throws Exception{
        //记录数据数量
        Integer hrefPreNum  = 0  ;
        List<String> itemCatUrls =  new ArrayList<String>();
        //这里选择抛出异常 , 这里如果抛出异常 , 说明url有问题  , 或者网络有问题 , 后续的操作没有任何意义
        Document doc = Jsoup.connect(url).get();
        Elements eles = doc.select("dl dd a");
        for(Element ele : eles) {
            String href = ele.attr("href");
            hrefPreNum += 1;
            if(href.startsWith("//list.jd.com/")) {
                itemCatUrls.add("http:"+href);
//              System.out.println(href);
            }
        }
        System.out.println("获取到的总三级分类链接量:"+hrefPreNum);
        System.out.println("数据清洗后的数量:"+itemCatUrls.size());

        return itemCatUrls;
    }
    /**
     * 获取三级分类下所有商品页面的链接
     * 商品展示可能存在分页的情况
     * 所以在获取所有的商品链接之前需要先获取 所有的商品分类页
     * */
    public static List<String> getItemsPageUrls(String url){
        List<String> itemsPages = new ArrayList<String>();
        //从商品展示页面获取分页信息
        String num;
        try {//抛出异常 , 如果 出现异常则继续执行 , 丢失一点信息是正常的
            num = Jsoup.connect(url).get().select("#J_topPage span i").get(0).text();
            Long numL = new  Long(num);
            for(int i = 1 ; i<=numL ; i++) {
                String pageUrl = url+"&page="+i;
//              System.out.println(pageUrl);
                itemsPages.add(pageUrl);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return itemsPages;
    }
    /**
     * 获取每个商品分类页面的商品链接
     * */
    public static List<String> getItemUrls(String url){
        List<String> itemUrls = new ArrayList<String>();
        try {
            Elements eles = Jsoup.connect(url).get().select(" li div .p-img a");
            for(Element ele : eles) {
                String itemUrl = ele.attr("href");
                itemUrls.add("http:"+itemUrl);
            }
        } catch (Exception e) {
            System.out.println("获取商品展示页面的商品链接出错:"+url);
        }
        return itemUrls;
    }
    /**
     * 访问商品链接 , 获取商品数据
     * */
    public static Item getItem (String url) {
        Item item = new Item();
        Long id = null;
        try {
            Document doc = Jsoup.connect(url).get();
            //获取id   //item.jd.com/12016709876.html
            id = new Long(url.substring(url.lastIndexOf("/")+1, url.indexOf(".html")));
            //获取title
            String title = doc.select("#name h1").get(0).text();
            //获取卖点  获取到的 值为"" 说明页面时是通过ajax方式请求  需要json格式的数据
//          String sellPoint = doc.select("#p-ad").get(0).text();
            String sellPoint = getSellPoint(id);
            //获取价格  价格是通过ajax二次请求的
//          Long price = new Long(doc.select(".dd .p-price .price").get(0).text());
            Long price = getPrice(id);
            //获取图片
//          String img = doc.select("#spec-n1 img").attr("src");
            String img = getImg(url);
//          System.out.println(img);
            //获取商品详情
//          String desc = doc.select("J-detail-content").get(0).text();
            String desc = getDesc(id);
            //封装属性
            item.setId(id);
            item.setTitle(title);
            item.setSellPoint(sellPoint);
            item.setPrice(price);
            item.setImg(img);
            item.setDesc(desc);
            System.out.println(item);
        } catch (Exception e) {
            // TODO Auto-generated catch block
            System.out.println("获取商品信息失败");
        }
        return item;
    }
    /**
     * 爬取卖点
     * 由于商品价格是页面加载完成之后 , 有通过ajax获取的 , 所以单独爬取json格式的数据
     * 通过页面分析 得到卖点的url
     * http://ad.3.cn/ads/mgets?skuids=AD_ +12017077901
     * */
    public static String getSellPoint(Long id) {

        String sellPoint = null;
        try {
            Response resp = Jsoup.connect("http://ad.3.cn/ads/mgets?skuids=AD_"+id).ignoreContentType(true).execute();
            ObjectMapper mapper = new ObjectMapper();
             sellPoint = mapper.readTree(resp.body()).get(0).get("ad").asText();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            System.out.println("获取卖点失败");
        }
        return sellPoint;
    }
    /**
     * 爬取商品价格
     * 由于商品价格是页面加载完成之后 , 有通过ajax获取的 , 所以单独爬取
     * 通过页面分析 得到商品价格的链接 ///prices/get?skuid=id
     * */
    public static Long getPrice(Long id) {
        Long price  = null;
        try {
            Response resp = Jsoup.connect("http:///prices/get?skuid="+id).ignoreContentType(true).execute();
            ObjectMapper mapper = new ObjectMapper();
            JsonNode jsonNode = mapper.readTree(resp.body()).get(0);
            price = jsonNode.get("m").asLong();
//          System.out.println(price);
        } catch (Exception e) {
            System.out.println("获取价格失败");
        }
        return price;
    }
    /**
     * 获取商品图片 
     * 通过分析页面 , 得到图片的请求地址
     * */
    public static String getImg(String url) {
        String img = "";
        Document doc;
        try {
            doc = Jsoup.connect(url).get();
            //获取页面大图的地址
            String bigsrc = doc.select("#spec-n1 img").attr("src");
//          System.out.println("大图地址:"+bigsrc);
            //获取小图地址
            Elements smallsrcs = doc.select("#spec-list div ul li img");
            for(Element ele : smallsrcs) {
                String src = ele.attr("src");
//              System.out.println("小图地址:"+src);
                //将小图地址替换成大图
                String newSrc = src.replace("n5", "n1");
                img+=newSrc+";";
//              System.out.println(newSrc);
            }
        } catch (Exception e) {
            // TODO Auto-generated catch block
            System.out.println("获取图片失败");
        }
        img = img.substring(0 , img.length()-1);
        return img;
    }
    /**
     * 爬取商品详情
     * 商品详情是页面加载完成之后 , 通过jsonp获取的 , 需要单独获取
     * http://dx.3.cn/desc/10316672107
     * */
    public static String getDesc(Long id) {
        String desc  = null;
        try {
            Response resp = Jsoup.connect("http://dx.3.cn/desc/"+id).ignoreContentType(true).execute();
            ObjectMapper mapper = new ObjectMapper();
            String body = resp.body();
            body = body.substring(body.indexOf("(")+1, body.lastIndexOf(")"));
            desc = mapper.readTree(body).get("content").asText();

        } catch (Exception e) {
            // TODO Auto-generated catch block
            System.out.println("获取不到"+id+"的商品描述");
        }
        return desc;

    }

    /**
     * 数据入库
     * */
    public static void saveItem(Item item) {
        session.insert("ItemMapper.saveItem" , item);
    }
}

爬虫的注意事项

  1. 网络不稳定 , 最好使用完整的严谨 的逻辑(断点续爬)
  2. 爬虫代码量不大(逻辑种类不多) , 最重要 的是页面结构的分析
  3. 网站改版导致爬虫的代码更新 。
  4. 反爬虫技术
  1. 频繁修改样式关键字(最简单的反爬虫机制)
  2. nginx就可以反爬虫 (使用nginx黑名单)
  1. jsoup的连接 请求头和浏览器请求头不一样
  1. jsoup可以用代码模拟请求头—伪装请求头 参考: http://jilongliang.iteye.com/blog/2048459
  1. 查看访问频率 , 如果频率过高 , 则封ip一段时间

问题

  1. 数据是会每天更新或添加的 ,怎样在原有的基础上爬取最新的数据