昨天摸了一天的鱼,下午快下班的时候,突然来活了,说要爬取钓友之家的钓场数据!什么?爬虫?之前一直没写过啊啊!为了保住自己的饭碗,赶紧打开百度,开始了自己第一个爬虫程序之旅!
什么是爬虫?
答:简单来讲,爬虫就是一个探测机器,它的基本操作就是模拟人的行为去各个网站溜达,点点按钮,查查数据,或者把看到的信息背回来。就像一只虫子在一幢楼里不知疲倦地爬来爬去。
了解了爬虫的概念之后,我随便翻了几篇博客,发现大部分的人都是用jsoup
来实现爬虫的。简单的瞄了一下代码,发现jsoup
使用起来还蛮简单,于是我就决定使用jsoup
来编写我的第一个爬虫程序!
我接到的任务需求是这样的,爬取钓鱼之家上常州每个钓场的名称、区域、是否收费、收费规则以及描述等数据。这里我了博客写起来方便,我简化一下需求,查询常州所有钓场的数据。
我们打开钓友之家网站,点击钓场,进入如下页面:
随便找个城市点击进去,这里我们按照需求,选择常州,进去之后,我们进入钓场列表页面:
发现我们需要的钓场名称、收费方式以及描述等数据都能在列表页找到!但是钓场所在区域却没办法直接获取,我们点击进入详情
发现依然没有区域,这可怎么办?
接着,我突然灵机一动,又回到列表页:
列表页有按照区域查询的筛选条件,我们可以先获取所有的区域,再按照每个区域查询出钓场列表,那么每次查询出来的列表都属于我们该次查询时指定的区域!不得不佩服自己的机智!
jsoup
依赖我想SpringBoot大家应该都很熟悉了,怎么新建项目在这里我就不多说了。
项目新建完成之,我们引入jsoup
相关依赖
<dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.14.3</version> </dependency>
在编写代码之前,jsoup中有几个方法需要介绍一下:
Document document = Jsoup.connect(url).get();
通过该方法可以获取我们的整个页面,我们页面中的所有元素都包含在这个对象里。Element
对象,就是我们html页面上的DOM节点Elements
列表,DOM节点的集合。Document
, Element
, Elements
对象中的方法都与javascript中的DOM节点方法一样。我们进入列表页面,F12
打开调试,选择Elements分析页面数据:
从中,我们发现我们可以使用 标签的class与标签种类来定位到我们的区域。
@Test void contextLoads() throws Exception { //这个就是博客中的java反射的url String url= "https://www.diaoyu.com/diaochang/changzhou/"; Document document = Jsoup.connect(url).get(); // select表示使用css选择器 // .sort-select li 是一个关系选择器,表示选择class是sort-select的所有li子元素 Elements elements = document.select(".sort-select li"); System.out.println(elements); }
运行测试:
我们发现,按照document.select(".sort-select li")
选择器筛选出来的条件有三个,对应列表页面的三个查询条件,想要获得区域信息,我们只要拿第一个就可以了。
以下是我们最终代码:
新建一个pojo类,来存放区域信息:
package com.xdw.pojo; import lombok.Data; import java.io.Serializable; @Data public class Area implements Serializable { // 城市名称 private String cityName; // 省 private String province; // 区域名称 private String areaName; // 区域对应链接 private String href; }
编写获取区域代码:
package com.xdw.utils; import com.xdw.pojo.Area; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import org.springframework.util.CollectionUtils; import java.util.ArrayList; import java.util.List; @SuppressWarnings("all") public class JsoupUtils { public static List<Area> getAreaList(Document document, String cityName, String provinceName) { List<Area> areaList = new ArrayList<>(); // select表示使用css选择器 // .sort-select li 是一个关系选择器,表示选择class是sort-select的所有li子元素 // first()表示选择第一个li,即我们的区域信息 Element areaElement = document.select(".sort-select li").first(); // 选择li标签下的dd标签,我们需要的区域名称以及对应的检索链接都在这个DOM节点下 Elements ddList = areaElement.select("dd"); if(!CollectionUtils.isEmpty(ddList)) { for (Element d : ddList) { Area area = new Area(); area.setAreaName(d.text()); // 链接位于dd标签下的a标签中 area.setHref(d.select("a").attr("abs:href")); area.setCityName(cityName); area.setProvince(provinceName); areaList.add(area); } } return areaList; } }
编写测试代码:
@Test void contextLoads() throws Exception { //这个就是博客中的java反射的url String url= "https://www.diaoyu.com/diaochang/changzhou/"; Document document = Jsoup.connect(url).get(); List<Area> areaList = JsoupUtils.getAreaList(document, "常州市", "江苏省"); for (Area area : areaList) { System.out.println(area); } }
运行测试:
区域列表获取成功。
进入钓场列表页面,f12
进入浏览器调试页面:
下面开始编写我们的代码:
新建一个钓场类:
package com.xdw.pojo; import cn.afterturn.easypoi.excel.annotation.Excel; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class Fishery { // 钓场名称 private String name; // 区域名称 private String areaName; // 城市名称 private String cityName; // 省名称 private String province; // 收费方式 private String priceType; // 价格 private String price; // 钓场描述 private String descript; }
在JsoupUtils
中新增如下方法,用来获取钓场信息:
public static List<Fishery> getFisheryListByArea(Area area) { List<Fishery> fisheryList = new ArrayList<>(); String url = area.getHref(); try { //先获得的是整个页面的html标签页面 Document doc = Jsoup.connect(url).get(); // 交集选择器,选出钓场元素所在DOM Elements elements = doc.select("ul.article-list").select("li"); if(!CollectionUtils.isEmpty(elements)) { for (Element element : elements) { Fishery fishery = new Fishery(); // 设置钓场区域名称 fishery.setAreaName(area.getAreaName()); fishery.setCityName(area.getCityName()); fishery.setProvince(area.getProvince()); // 获取钓场名称 String name = element.select("dl dt a").text(); fishery.setName(name); // 设置钓场描述 fishery.setDescript(element.select("dl dd.article-intro").text()); // 获取收费类型 Elements priceElement = element.select("dl dd.article-charge"); if(CollectionUtils.isEmpty(priceElement)) { fishery.setPriceType("免费"); } else { fishery.setPriceType("收费"); String price = priceElement.select("span").text(); fishery.setPrice(price); } fisheryList.add(fishery); } } } catch (Exception e) { e.printStackTrace(); } return fisheryList; }
编写测试类:
@Test void contextLoads() throws Exception { //这个就是博客中的java反射的url String url= "https://www.diaoyu.com/diaochang/changzhou/"; Document document = Jsoup.connect(url).get(); List<Area> areaList = JsoupUtils.getAreaList(document, "常州市", "江苏省"); List<Fishery> fisheryList = new ArrayList<>(); if(!CollectionUtils.isEmpty(areaList)) { for (Area area : areaList) { fisheryList.addAll(JsoupUtils.getFisheryListByArea(area)); } } for (Fishery fishery : fisheryList) { System.out.println(fishery); } }
运行测试:
我们发现成功获取了钓场数据。
问题:刚刚我们只是获得每个区域的第一页数据,我们如何获取后面页的数据呢?
我们发现,当列表页到了尾页时,下一页按钮就会消失,所以我们可以在获取钓场数据方法中添加一个循环,直到没有下一页,才跳出循环!
完善我们的getFisheryListByArea
方法如下:
public static List<Fishery> getFisheryListByArea(Area area) { List<Fishery> fisheryList = new ArrayList<>(); String url = area.getHref(); try { boolean flag = true; while(flag) { //先获得的是整个页面的html标签页面 Document doc = Jsoup.connect(url).get(); // 交集选择器,选出钓场元素所在DOM Elements elements = doc.select("ul.article-list").select("li"); if(!CollectionUtils.isEmpty(elements)) { for (Element element : elements) { Fishery fishery = new Fishery(); // 设置钓场区域名称 fishery.setAreaName(area.getAreaName()); fishery.setCityName(area.getCityName()); fishery.setProvince(area.getProvince()); // 获取钓场名称 String name = element.select("dl dt a").text(); fishery.setName(name); // 设置钓场描述 fishery.setDescript(element.select("dl dd.article-intro").text()); // 获取收费类型 Elements priceElement = element.select("dl dd.article-charge"); if(CollectionUtils.isEmpty(priceElement)) { fishery.setPriceType("免费"); } else { fishery.setPriceType("收费"); String price = priceElement.select("span").text(); fishery.setPrice(price); } fisheryList.add(fishery); } } // 获取页面信息 Elements pageElements = doc.select(".page a"); // 没有页面,说明只有一页 if(CollectionUtils.isEmpty(pageElements)) { flag = false; } else if(!pageElements.text().contains("下一页")) { // 没有下一页,直接跳出循环 flag = false; } else { for (Element pe : pageElements) { if(pe.text().equals("下一页")) { // 存在下一页,获取页面信息 url = pe.attr("abs:href"); } } } if(!flag) { break; } } } catch (Exception e) { e.printStackTrace(); } return fisheryList; }
至此,我们的爬虫大功告成!
我擦了擦额头的汗水,庆幸保住了的饭碗!