注册

java就能写爬虫还要python干嘛?

爬虫学得好,牢饭吃得饱!!!切记!!!



相信大家多少都会接触过爬虫相关的需求吧,爬虫在绝大多数场景下,能够帮助客户自动的完成部分工作,极大的减少人工操作。目前更多的实现方案可能都是以python为实现基础,但是作为java程序员,咱们需要知道的是,以java 的方式,仍然可以很方便、快捷的实现爬虫。下面将会给大家介绍两种以java为基础的爬虫方案,同时提供案例供大家参考。



一、两种方案


传统的java实现爬虫方案,都是通过jsoup的方式,本文将采用一款封装好的框架【webmagic】进行实现。同时针对一些特殊的爬虫需求,将会采用【selenium-java】的进行实现,下面针对两种实现方案进行简单介绍和演示配置方式。


1.1 webmagic


官方文档:webmagic.io/


1.1.1 简介


使用webmagic开发爬虫,能够非常快速的实现简单且逻辑清晰的爬虫程序。


四大组件



  • Downloader:下载页面
  • PageProcessor:解析页面
  • Scheduler:负责管理待抓取的URL,以及一些去重的工作。通常不需要自己定制。
  • Pipeline:获取页面解析结果,数持久化。

Spider



  • 启动爬虫,整合四大组件

1.1.2 整合springboot


webmagic分为核心包和扩展包两个部分,所以我们需要引入如下两个依赖:



<properties>
<webmagic.version>0.7.5</webmagic.version>
</properties>

<!--WebMagic-->
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-core</artifactId>
<version>${webmagic.version}</version>
</dependency>
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-extension</artifactId>
<version>${webmagic.version}</version>
</dependency>

到此为止,我们就成功的将webmagic引入进来了,具体使用,将在后面的案例中详细介绍。


1.2 selenium-java


官网地址:http://www.selenium.dev/


1.2.1 简介


selenium是一款浏览器自动化工具,它能够模拟用户操作浏览器的交互。但前提是,我们需要在使用他的机器(windows/linux等)安装上它需要的配置。相比于webmigc的安装,它要繁琐的多了,但使用它的原因,就是为了解决一些webmagic做不到的事情。


支持多种语言:java、python、ruby、javascript等。其使用代码非常简单,以java为例如下:


package dev.selenium.hello;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

public class HelloSelenium {
public static void main(String[] args) {
WebDriver driver = new ChromeDriver();

driver.get("https://selenium.dev");

driver.quit();
}
}

1.2.2 安装


无论是在windows还是linux上使用selenium,都需要两个必要的组件:



  • 浏览器(chrome)
  • 浏览器驱动 (chromeDriver)

需要注意的是,要确保上述两者的版本保持一致。


下载地址


chromeDriver:chromedriver.storage.googleapis.com/index.html


windows


windows的安装相对简单一些,将chromeDriver.exe下载至电脑,chrome浏览器直接官网下载相应安装包即可。严格保证两者版本一致,否则会报错。


在后面的演示程序当中,只需要通过代码指定chromeDriver的路径即可。


linux


linux安装才是我们真正的使用场景,java程序通常是要部署在linux环境的。所以我们需要linux的环境下安装chrome和chromeDriver才能实现想要的功能。


首先要做的是判断我们的linux环境属于哪种系统,是ubuntucentos还是其他的种类,相应的shell脚本都是不同的。


我们采用云原生的环境,所有的服务均以容器的方式部署,所以要在每一个服务端容器内部安装chrome和chromeDiver。我们使用的是Alpine Linux,一个轻量级linux发行版,非常适合用来做Docker镜像。


我们可以通过apk --help去查看相应的命令,我直接给出安装命令:


# Install Chrome for Selenium
RUN apk add gconf
RUN apk add chromium
RUN apk add chromium-chromedriver

上面的内容,可以放在DockerFile文件中,在部署的时候,会直接将相应组件安装在容器当中。


需要注意的是,在Alpine Linux中自带的浏览器是chromiumchromium-chromedriver,且版本相应较低,但是足够我们的需求所使用了。


/ # apk search chromium
chromium-68.0.3440.75-r0
chromium-chromedriver-68.0.3440.75-r0

1.2.3 整合springboot


我们只需要在爬虫模块引入依赖就好了:


<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
</dependency>

二、三个案例


下面通过三个简单的案例,给大家实际展示使用效果。


2.1 爬取省份街道


使用webmagic进行省份到街道的数据爬取。注意,本文只提供思路,不提供具体爬取网站信息,请同学们自己根据使用选择。


接下来搭建webmagic的架子,其中有几个关键点:



  • 创建页面解析类,实现PageProcessor。

import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.processor.PageProcessor;

/**
* 页面解析
*
* @author wjbgn
* @date 2023/8/15 17:25
**/

public class TestPageProcessor implements PageProcessor {
@Override
public void process(Page page) {

}

@Override
public Site getSite() {
return site;
}

/**
* 初始化Site配置
*/

private Site site = Site.me()
// 重试次数
.setRetryTimes(3)
//编码
.setCharset(StandardCharsets.UTF_8.name())
// 超时时间
.setTimeOut(10000)
// 休眠时间
.setSleepTime(1000);
}


  • 实现PageProcessor后,要重写其方法process(Page page),此方法是我们实现爬取的核心(页面解析)。通常省市区代码分为6级,所以常见的网站均是按照层级区分,我们是从省份开始爬取,即从第三层开始爬取。

    • 初始化变量

    @Override
    public void process(Page page) {
    // 市级别
    Integer type = 3;
    // 初始化结果明细
    RegionCodeDTO regionCodeDTO = new RegionCodeDTO();
    // 带有父子关系的结果集合
    List<Map<String, Object>> list = new ArrayList();
    // 页面所有元素集合
    List<String> all = new ArrayList<>();
    // 页面中子页面的链接地址
    List<String> urlList = new ArrayList<>();
    }


    • 根据不同级别,获取相应页面不同的元素

    if (CollectionUtil.isEmpty(all)) {
    // 爬取所有的市,编号,名称
    all = page.getHtml().css("table.citytable").css("tr").css("a", "text").all();
    // 爬取所有的城市下级地址
    urlList = page.getHtml().css("table.citytable").css("tr").css("a", "href").all()
    .stream().distinct().collect(Collectors.toList());
    if (CollectionUtil.isEmpty(all)) {
    // 区县级别
    type = 4;
    all = page.getHtml().css("table.countytable").css("tr.countytr").css("td", "text").all();
    // 获取区
    all.addAll(page.getHtml().css("table.countytable").css("tr.countytr").css("a", "text").all());

    urlList = page.getHtml().css("table.countytable").css("tr").css("a", "href").all()
    .stream().distinct().collect(Collectors.toList());
    if (CollectionUtil.isEmpty(all)) {
    // 街道级别
    type = 5;
    all = page.getHtml().css("table.towntable").css("tr").css("a", "text").all();
    urlList = page.getHtml().css("table.towntable").css("tr").css("a", "href").all()
    .stream().distinct().collect(Collectors.toList());
    if (CollectionUtil.isEmpty(all)) {
    // 村,委员会
    type = 6;
    List<String> village = new ArrayList<>();
    all = page.getHtml().css("table").css("tr.villagetr").css("td", "text").all();
    for (int i = 0; i < all.size(); i++) {
    if (i % 3 != 1) {
    village.add(all.get(i));
    }
    }
    all = village;
    }
    }
    }
    }


    • 定义一个实体类RegionCodeDTO,用来存放临时获取的code,url以及父子关系等内容:

    public class RegionCodeDTO {

    private String code;

    private String parentCode;

    private String name;

    private Integer type;

    private String url;

    private List<RegionCodeDTO> regionCodeDTOS;
    }


    • 接下来对页面获取的内容(code、name、type)进行组装和临时存储,添加到children中:

    // 初始化子集
    List<RegionCodeDTO> children = new ArrayList<>();
    // 初始化临时节点数据
    RegionCodeDTO region = new RegionCodeDTO();
    // 解析页面结果集all当中的数据,组装到region 和 children当中
    for (int i = 0; i < all.size(); i++) {
    if (i % 2 == 0) {
    region.setCode(all.get(i));
    } else {
    region.setName(all.get(i));
    }
    if (StringUtils.isNotEmpty(region.getCode()) && StringUtils.isNotEmpty(region.getName())) {
    region.setType(type);
    // 添加子集到集合当中
    children.add(region);
    // 重新初始化
    region = new RegionCodeDTO();
    }
    }


    • 组装页面链接,并将页面链接组装到children当中。

    // 循环遍历页面元素获取的子页面链接
    for (int i = 0; i < urlList.size(); i++) {
    String url = null;
    if (StringUtils.isEmpty(urlList.get(0))) {
    continue;
    }
    // 拼接链接,页面的子链接是相对路径,需要手动拼接
    if (urlList.get(i).contains(provinceEnum.getCode() + "/")) {
    url = provinceEnum.getUrlPrefixNoCode();
    } else {
    url = provinceEnum.getUrlPrefix();
    }
    // 将链接放到临时数据子集对象中
    if (urlList.get(i).substring(urlList.get(i).lastIndexOf("/") + 1, urlList.get(i).indexOf(".html")).length() == 9) {
    children.get(i).setUrl(url + page.getUrl().toString().substring(page.getUrl().toString().indexOf(provinceEnum.getCode() + "/") + 3
    , page.getUrl().toString().lastIndexOf("/")) + "/" + urlList.get(i));
    } else {
    children.get(i).setUrl(url + urlList.get(i));
    }
    }


    • 将children添加到结果对象当中

    // 将子集放到集合当中
    regionCodeDTO.setRegionCodeDTOS(children);


    • 在下面的代码当中将进行两件事儿:

      • 处理下一页,通过page的addTargetRequests方法,可以进行下一页的跳转,此方法参数可以是listString和String,即支持多个页面跳转和单个页面的跳转。
      • 将数据传递到Pipeline,用于数据的存储,Pipeline的实现将在后面具体说明。



    // 定义下一页集合
    List<String> nextPage = new ArrayList<>();
    // 遍历上面的结果子集内容
    regionCodeDTO.getRegionCodeDTOS().forEach(regionCodeDTO1 -> {
    // 组装下一页集合
    nextPage.add(regionCodeDTO1.getUrl());
    // 定义并组装结果数据
    Map<String, Object> map = new HashMap<>();
    map.put("regionCode", regionCodeDTO1.getCode());
    map.put("regionName", regionCodeDTO1.getName());
    map.put("regionType", regionCodeDTO1.getType());
    map.put("regionFullName", regionCodeDTO1.getName());
    map.put("regionLevel", regionCodeDTO1.getType());
    list.add(map);
    // 推送数据到pipeline
    page.putField("list", list);
    });
    // 添加下一页集合到page
    page.addTargetRequests(nextPage);


  • 当本次process方法执行完后,将会根据传递过来的链接地址,再次执行process方法,根据前面定义的读取页面元素流程的代码,将不符合type=3的内容,所以将会进入到下一级4的爬取过程,5、6级别原理相同。

    image.png


  • 创建Pipeline,用于编写数据持久化过程。经过上面的逻辑,已经将所需内容全部获取到,接下来将通过pipline进行数据存储。首先定义pipeline,并实现其process方法,获取结果内容,具体存储数据的代码就不展示了,需要注意的是,此处pipeline没有通过spring容器托管,需要调用业务service需要使用SpringUtils进行获取:
    public class RegionDataPipeline implements Pipeline{


    @Override
    public void process(ResultItems resultItems, Task task) {
    // 获取service
    IXXXXXXXXXService service = SpringUtils.getBean(IXXXXXXXXXService.class);
    // 获取内容
    List<Map<String, String>> list = (List<Map<String, String>>) resultItems.getAll().get("list");
    // 解析数据,转换为对应实体类
    // service.saveBatch
    }


  • 启动爬虫
    //启动爬虫
    Spider.create(new RegionCodePageProcessor(provinceEnum))
    .addUrl(provinceEnum.getUrl())
    .addPipeline(new RegionDataPipeline())
    //此处不能小于2
    .thread(2).start()



2.2 爬取网站静态图片


爬取图片是最常见的需求,我们通常爬取的网站都是静态的网站,即爬取的内容都在网页上面渲染完成的,我们可以直接通过获取页面元素进行抓取。


可以参考下面的文章,直接拉取网站上的图片:juejin.cn/post/705138…


针对获取到的图片网络地址,直接使用如下方式进行下载即可:


url = new URL(imageUrl);
//打开连接
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
//设置请求方式为"GET"
conn.setRequestMethod("GET");
//超时响应时间为10秒
conn.setConnectTimeout(10 * 1000);
//通过输入流获取图片数据
InputStream is = conn.getInputStream();

2.3 爬取网站动态图片


在2.2中我们可以很快地爬取到对应的图片,但是在另外两种场景下,我们获取图片将会不适用上面的方式:



  • 需要拼图,且多层的gis相关图片,此种图片将会在后期进行复杂的图片处理(按位置拼接瓦片,多层png图层叠加),才能获取到我们想要的效果。
  • 动态js加载的图片,直接无法通过css、xpath获取。

所以在这种情况下我们可以使用开篇介绍的selenium-java来解决,本文使用的仅仅是截图的功能,来达到我们需要的效果。具体街区全屏代码如下所示:


public File getItems() {
// 获取当前操作系统
String os = System.getProperty("os.name");
String path;
if (os.toLowerCase().startsWith("win")) {
//windows系统
path = "driver/chromedriver.exe";
} else {
//linux系统
path = "/usr/bin/chromedriver";
}
WebDriver driver = null;
// 通过判断 title 内容等待搜索页面加载完毕,间隔秒
try {
System.setProperty("webdriver.chrome.driver", path);
ChromeOptions chromeOptions = new ChromeOptions();
chromeOptions.addArguments("--headless");
chromeOptions.addArguments("--no-sandbox");
chromeOptions.addArguments("--disable-gpu");
chromeOptions.addArguments("--window-size=940,820");
driver = new ChromeDriver(chromeOptions);
// 截图网站地址
driver.get(UsaRiverConstant.OBSERVATION_POINT_URL);
// 休眠用于网站加载
Thread.sleep(15000);
// 截取全屏
File screenshotAs = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
return screenshotAs;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
driver.quit();
}
}

如上所示,我们获取的是整个页面的图片,还需要对截取的图片进行相应的剪裁,保留我们需要的区域,如下所示:


public static void cutImg(InputStream inputStream, int x, int y, int width, int height, OutputStream outputStream) {//图片路径,截取位置坐标,输出新突破路径
InputStream fis = inputStream;
try {
BufferedImage image = ImageIO.read(fis);
//切割图片
BufferedImage subImage = image.getSubimage(x, y, width, height);
Graphics2D graphics2D = subImage.createGraphics();
graphics2D.drawImage(subImage, 0, 0, null);
graphics2D.dispose();
//输出图片
ImageIO.write(subImage, "png", outputStream);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

三、小结


通过如上两个组件的简单介绍,足够应付在java领域的大多数爬取场景。从页面数据、到静态网站图片,在到动态网站的图片截取。本文以提供思路为主,原理请参考相应的官方文档。


爬虫学得好,牢饭吃得饱!!!切记!!!


作者:我犟不过你
来源:juejin.cn/post/7267532912617177129

0 个评论

要回复文章请先登录注册