Java 爬虫

Java 爬虫本文介绍了 Java 主流的爬虫框架 包括 Jsoup 的基本使用 Selenium 的网页操作及动态页面处理 以及 WebMagic 的整体架构和自定义组件

大家好,欢迎来到IT知识分享网。

 

java主流的爬虫框架

2、Selenium

  • 一款web自动化工具,在浏览器中模拟用户操作,更接近真实用户的操作,常用于web自动化测试、爬取动态页面;
  • 因为要启动浏览器、在浏览器中操作,运行速度慢、效率低,通常会搭配 headless browser 无界面浏览器使用,以提升执行效率;
  • 支持多种语言:C#、JavaScript、Java、Python、Ruby;
  • 支持多种浏览器:chrome、firefox、edge、ie、safari、opera。
     

3、Web Magic

  • 一款国人开源的java爬虫类库,文档齐全、组件丰富、功能强大,封装了一套流程化的爬虫操作。
  • 使用jsoup、以及基于jsoup开发的xsoup作为html解析工具,可集成Selenium爬取动态网页,可处理json响应。
  • 可递归爬取页面内的链接,支持多线程、分布式,适合爬取相关内容、整个网站。

 

jsoup

基本使用
<dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.13.1</version> </dependency> 
Document document = Jsoup.connect(url) //默认 30000 ms .timeout(30000) //可设置代理 .proxy("127.0.0.1", 8888) //可设置header,支持逐个设置、map方式设置,部分请求头也可以调用对应的方法直接设置 .header("key1", "value1") .header("key2", "value2") .userAgent("") .referrer("") .headers(headerMap) //可设置cookie,,支持逐个设置、map方式设置 .cookie("key1", "value1") .cookie("key2", "value2") .cookies(cookieMap) //可设置请求体 .requestBody("") //指定请求方式,默认GET .method(Connection.Method.GET) //执行请求,返回 Connection.Response。自动会校验http状态码,如果返回的状态码 <200 || >=400,会直接抛出 HttpStatusException .execute() //解析响应得到 Document .parse(); // GET、POST请求,官方已经封装好了以上三行 .method().execute().parse(),可以直接简写为 get()、post() // .get(); //支持多种方式获取元素 Element username = document.getElementById("username"); Elements nameList = document.getElementsByAttribute("name"); Elements aList = document.getElementsByTag("a"); Elements imgList = document.select("div > a > img"); //处理元素信息 String html = username.html(); String text = username.text(); aList.forEach(a -> System.out.println(a.absUrl("href"))); imgList.forEach(img -> System.out.println(img.attr("src"))); 

 

工具类
import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; import org.jsoup.Connection; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import java.net.Proxy; import java.util.Map; / * jsoup工具类 */ @Slf4j public class JsoupUtil { 
     private JsoupUtil() { 
     } / * 获取Document * * @param url 网页地址 * @param proxy 代理 * @param method 请求方式 * @param timeout 超时时间,ms * @param headers 请求头配置 * @param cookies 携带的cookie信息 * @return Document */ public static Document getDocument(String url, Proxy proxy, Connection.Method method, Integer timeout, Map<String, String> headers, Map<String, String> cookies) { 
     if (StringUtils.isBlank(url)) { 
     throw new RuntimeException("获取document-参数错误:url不能为空"); } Connection connection = Jsoup.connect(url); if (proxy != null) { 
     connection.proxy(proxy); } if (method != null) { 
     connection.method(method); } if (timeout != null) { 
     connection.timeout(timeout); } if (MapUtils.isNotEmpty(headers)) { 
     connection.headers(headers); } if (MapUtils.isNotEmpty(cookies)) { 
     connection.cookies(cookies); } //如果返回的状态码 <200 || >=400,会直接抛出异常 try { 
     return connection.execute().parse(); } catch (Exception e) { 
     log.error("获取document-执行异常,url={}", url, e); throw new RuntimeException("获取document异常"); } } / * 获取Document * * @param url 网页地址 * @param proxy 代理 * @param headers 请求头配置 * @param cookies 携带的cookie信息 * @return Document */ public static Document getDocument(String url, Proxy proxy, Map<String, String> headers, Map<String, String> cookies) { 
     return getDocument(url, proxy, null, null, headers, cookies); } / * 获取Document * * @param url 网页地址 * @param proxy 代理 * @return Document */ public static Document getDocument(String url, Proxy proxy) { 
     return getDocument(url, proxy, null, null, null, null); } / * 获取Document * * @param url 网页地址 * @return Document */ public static Document getDocument(String url) { 
     return getDocument(url, null, null, null, null, null); } } 

 

常见问题

2、UnsupportedMimeTypeException 不支持的资源类型

jsoup.UnsupportedMimeTypeException: Unhandled content type. Must be text/, application/xml, or application/+xml

jsoup默认只接收网页文本,如果请求的是图片、音视频之类的其它类型资源,会报错,可以设置忽略 contentType

Jsoup.connect(url) .ignoreContentType(true) 

 

Selenium

基本使用

2、pom.xml

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

3、使用示例

import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.chrome.ChromeOptions; import java.time.Duration; @Slf4j public class SeleniumTest { 
     public static void main(String[] args) { 
     // 设置浏览器驱动路径,不同浏览器,属性名不同 System.setProperty(ChromeDriverService.CHROME_DRIVER_EXE_PROPERTY, "D:/chromedriver/chromedriver.exe"); // 自定义浏览器参数 ChromeOptions chromeOptions = new ChromeOptions(); //允许向远程主机发起请求。默认只允许请求本地资源(localhost),向远程服务器发起请求时需要设置此参数,不然会报403禁止访问 chromeOptions.addArguments("--remote-allow-origins=*") //禁用自动化软件控制标识。一些网站会检测 window.navigator.webdriver 来反爬虫,不设置此项时可能会报403禁止访问 .addArguments("--disable-blink-features=AutomationControlled") // 不使用沙箱模式 .addArguments("--no-sandbox") // 禁用浏览器插件,可以提高执行效率 .addArguments("--disable-plugins") // 无界面模式,可以提高执行效率 .addArguments("--headless") // 用户代理,headless模式下需要设置此参数,不然可能会报403禁止访问 .addArguments("--user-agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'") // 禁用GPU加速 .addArguments("--disable-gpu") //如果页面含有大量图片,可能因为加载图片太多导致超时报错,可以禁用加载图片 //.addArguments("blink-settings=imagesEnabled=false") // http代理 //.addArguments("--proxy-server=127.0.0.1:8888") // 使用无痕模式 // .addArguments("--incognito") // 窗口最大化,防止网页排版不同造成元素获取、截图等方面的问题 .addArguments("--start-maximized"); // 一些参数也可以通过对应方法来设置,比如:chromeOptions.setHeadless(true); //创建浏览器驱动、启动浏览器 WebDriver webDriver = new ChromeDriver(chromeOptions); webDriver.manage().timeouts() // 网页加载超时 .pageLoadTimeout(Duration.ofSeconds(30)) // 隐式等待时间(获取不到指定元素时的等待时间),默认0 不等待,获取不到指定元素时直接报错 .implicitlyWait(Duration.ofSeconds(30)); log.info("webDriver已启动,开始爬取数据"); try { 
     // 获取网页 webDriver.get("https://www.baidu.com"); // WebElement 封装了元素操作,支持id、name、tagName、className、cssSelector、xpath等方式获取元素 WebElement input = webDriver.findElement(By.id("kw")); WebElement submitBtn = webDriver.findElement(By.id("su")); // WebElement byName = webDriver.findElement(By.name("xxx")); // List<WebElement> byTagName = webDriver.findElements(By.tagName("xxx")); // List<WebElement> byClassName = webDriver.findElements(By.className("xxx")); // List<WebElement> byCssSelector = webDriver.findElements(By.cssSelector("xxx")); // 模拟键盘输入 input.sendKeys("chy"); // 模拟点击 submitBtn.click(); log.info("爬取数据完毕"); } catch (Exception e) { 
     log.error("爬取数据执行异常", e); } finally { 
     // 退出、关闭浏览器 webDriver.quit(); log.info("webDriver已关闭"); } } } 

如果没有通过 webDriver.quit() 之类的正常方式关闭 webDriver,比如点击IDE的停止按钮终止爬虫程序、杀死IDE、发生异常后没执行到 webDriver.quit() 等,浏览器进程下会挂着多个 webDriver 子进程,可能导致爬取时报403禁止访问,可以在任务管理器中 kill 掉这些 webDriver 子进程后重试。

implicitlyWait() 隐式等待不是万能的,往往只适用于 webDriver.get(url) 加载网页后等待元素,如果只是ajax或点击按钮之类、刷新局部dom,依然可能出现获取不到元素的情况,可以 Thread.sleep() + 重试。

 

frame嵌套

如果内嵌了 <iframe>,webDriver.findElement() 可以获取到 iframe 自身,但获取不到 iframe 中的元素,需要先 webDriver.switchTo() 切换到对应的 frame 中

//切换到指定frame,参数可以是String(frame的id、name),或者frame对应的WebElement webDriver.switchTo().frame("ifm"); //切换到父frame webDriver.switchTo().parentFrame(); //切到主frame,即主页面 webDriver.switchTo().defaultContent(); 

 

常见问题

1、多次执行 webDriver.get(),导致从之前网页获取的元素失效

// 爬取列表页 webDriver.get("https://xxx/list.html"); List<WebElement> articleElements = webDriver.findElements(By.cssSelector("#articleList > li > a")); // List<String> detailUrls = articleElements.stream().map(x -> x.getAttribute("href")).collect(Collectors.toList()); // 遍历爬取详情页 for (WebElement articleElement : articleElements ) { 
     String detailUrl = articleElement.getAttribute("href"); webDriver.get(detailUrl); // ... } 

webDriver.get("https://xxx/list.html") 加载列表页,解析得到 List<WebElement> articleElements;循环遍历到第一个元素时 webDriver.get(detailUrl) 加载了其它网页 Document,webDriver 中原来的列表页 Document 被覆盖了、失效了,从列表中获取的 articleElements 之类的 WebElement 也就找不到、失效了,所以循环遍历从 articleElements 中获取第二个元素时,会报 404 找不到。

解决方案:如果循环中要 webDriver.get() 加载新的网页,就不要用 List<WebElement> 去遍历,可以先映射保存为其它类型的 list,用其它类型的 list 去遍历。

List<String> detailUrls = articleElements.stream().map(x -> x.getAttribute("href")).collect(Collectors.toList()); for (String detailUrl : detailUrls) { 
     webDriver.get(detailUrl); // ... } 

 

截图

ChromeDriver、FirefoxDriver等具体Driver除了继承实现 WebDriver 接口,还会继承实现其它的类、接口,提供额外功能,比如

  • 继承实现 JavascriptExecutor 接口,提供执行js脚本的能力,chromeDriver.executeScript("alert('ok');");
  • 继承实现 TakesScreenshot 接口,提供截图能力,File screenshotAs = chromeDriver.getScreenshotAs(OutputType.FILE);

Selenium截图实质是调用 TakesScreenshot#getScreenshotAs,ChromeDriver、FirefoxDriver等具体的 WebDriver 以及 WebElement 都继承了 TakesScreenshot,可以直接调用,如果声明为 WebDriver 则需要强转一下。

chromeDriver.get("https://www.baidu.com"); //全屏截图 File screenshotAs = chromeDriver.getScreenshotAs(OutputType.FILE); // File screenshotAs = ((ChromeDriver) webDriver).getScreenshotAs(OutputType.FILE); // File screenshotAs = ((TakesScreenshot) webDriver).getScreenshotAs(OutputType.FILE); //针对某个元素截图 WebElement webElement = chromeDriver.findElement(By.id("head_wrapper")); File screenshotAs = webElement.getScreenshotAs(OutputType.FILE); try { 
     FileUtils.copyFile(screenshotAs, new File("D:/image/screenshot.png")); } catch (IOException e) { 
     log.error("截图失败", e); } 

 

Web Magic

整体架构

在这里插入图片描述
四大组件

  • Scheduler:负责管理待爬取的url,以及url去重。默认使用内存队列来存储待爬取的url、使用HashSet对url去重,也支持使用redis进行分布式管理。
  • Downloader:负责获取网页,默认使用了HttpClient作为下载工具。
  • PageProcessor:负责解析网页,抽取需要的数据,以及发现新链接。使用jsoup作为html解析工具,并基于jsoup开发了解析xpath的xsoup。
  • Pipeline:负责处理抽取的数据,可以进行计算、持久化到文件、数据库等。内置输出到控制台、保存到文件两种处理方式,默认为输出到控制台。
     

内置对象

  • Request:对URL的封装,一个Request对应一个URL地址,此外还包含一个map成员extra,可以传递一些自定义的数据。
  • Page:封装Downloader下载得到网页、json、xml之类的文本内容,提供解析网页、保存数据的一些方法。
  • ResultItems:相当于一个Map,用于保存PageProcessor提取的数据,供Pipeline使用。它的API与Map很类似,值得注意的是它有一个字段skip,若设置为true,则该页面提取的数据不会被Pipeline处理。

 

基本使用

1、pom.xml

<webmagic.version>0.9.0</webmagic.version> 
<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> 

 

2、使用示例

import org.apache.commons.lang3.StringUtils; import us.codecraft.webmagic.Page; import us.codecraft.webmagic.Site; import us.codecraft.webmagic.Spider; import us.codecraft.webmagic.downloader.HttpClientDownloader; import us.codecraft.webmagic.pipeline.ConsolePipeline; import us.codecraft.webmagic.pipeline.JsonFilePipeline; import us.codecraft.webmagic.processor.PageProcessor; import us.codecraft.webmagic.proxy.Proxy; import us.codecraft.webmagic.proxy.SimpleProxyProvider; import us.codecraft.webmagic.scheduler.QueueScheduler; import java.util.List; public class MyPageProcessor implements PageProcessor { 
     / * 网站爬取相关配置,会自动给每个请求都设置这些参数,时间单位ms */ private Site site = Site.me() .addHeader("key1", "value1") .addHeader("key2", "value2") .addCookie("name1", "value1") .addCookie("name2", "value2") .setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36") //下载网页超时时间,默认5000 .setTimeOut(10000) //爬取间隔时间,默认5000 .setSleepTime(1000) //重试次数,默认0 .setRetryTimes(3); @Override public void process(Page page) { 
     //选取元素,支持css选择器、xpath、正则匹配。css()也可以写为$() //Page内置了ResultItems,ResultItems内置了Map,page.putField(key,value)实质是将键值对保存到map中 page.putField("links", page.getHtml().css("div > a", "href").all()); page.putField("links", page.getHtml().$("div > a", "href").all()); page.putField("name", page.getHtml().xpath("//h1[@class='public']/strong/a/text()").get()); //all()返回List<String>;get()实质是all().get(0) 返回list的第1个元素,如果list为空则直接返回null;toString()实质是调用get() //match()判断是否有匹配 page.putField("links", page.getHtml().css("div > a", "href").all()); page.putField("link", page.getHtml().css("div > a", "href").get()); page.putField("link", page.getHtml().css("div > a", "href").toString()); page.putField("hasLink", page.getHtml().css("div > a", "href").match()); //page.getUrl()获取当前网页对应的url //正则匹配时可以将要提取部分对应的正则表达式放在()中,有()时会自动提取()指定部分,没有()时默认提取整个正则表达式匹配的内容 String username = page.getUrl().regex("https://xxx\\.com/(\\w+)/.*").get(); if (StringUtils.isBlank(username)) { 
     //pipeline处理结果时跳过当前网页。只是给当前page打了一个标识,不会中止当前方法中后续代码的执行 page.setSkip(true); } //将需要递归爬取的url添加到待处理url集合中,后续自动爬取 //page.links()获取当前网页中的所有链接(a标签的href属性值),会自动将相对路径的url转换为绝对路径 List<String> allLinkList = page.getHtml().links().regex("(https://xxx\\.com/\\w+/\\w+)").all(); page.addTargetRequests(allLinkList); } @Override public Site getSite() { 
     return this.site; } public static void main(String[] args) { 
     HttpClientDownloader httpClientDownloader = new HttpClientDownloader(); httpClientDownloader.setThread(7); //设置代理池 Proxy proxy = new Proxy("127.0.0.1", 8888); httpClientDownloader.setProxyProvider(SimpleProxyProvider.from(proxy)); Spider.create(new MyPageProcessor()) //要爬取的url,可以是入口url,也可以是从数据、文件等地方读取的url .addUrl("https://xxx/xxx") //默认为new HttpClientDownloader(),可通过Downloader设置代理 .setDownloader(httpClientDownloader) //添加结果处理,支持多个,常见的比如:ConsolePipeline输出到控制台(默认),JsonFilePipeline保存为json文件 .addPipeline(new ConsolePipeline()) .addPipeline(new JsonFilePipeline()) //指定调度器,默认为QueueScheduler 使用内存队列存储待爬取的url .setScheduler(new QueueScheduler()) //线程数,默认1。此方法会通过 Executors.newFixedThreadPool(n) 创建指定线程数的线程池 .thread(7) //启动爬虫。run()会阻塞当前线程,start()、runAsync()是异步启动,当前线程继续执行 .run(); } } 

官方提供的示例:https://github.com/code4craft/webmagic/tree/master/webmagic-samples

不推荐多次调用 page.putField("key", value); 放置提取的各个数据,尽量把要提取的数据封装为实体类,统一约定好data之类的key,page.putField("data", entity); 直接放置实体,Pipeline统一 Xxx xxx = resultItems.get("data"); 获取实体进行处理。

 

threadPool.execute(new Runnable() { 
     @Override public void run() { 
     try { 
     processRequest(request); onSuccess(request); } catch (Exception e) { 
     onError(request, e); logger.error("process request " + request + " error", e); } finally { 
     pageCount.incrementAndGet(); signalNewUrl(); } } }); 

Spider是逐个处理url(Downloader下载 -> PageProcessor解析数据 -> Pipeline处理结果),只在最外层捕获了

 

爬取非GET请求

参考:http://webmagic.io/docs/zh/posts/ch4-basic-page-processor/post.html

 

Xsoup语法
解析json响应

如果爬取的是接口,响应json,可以通过web magic提供的JsonPathSelector、Json来解析、提取数据

//page.getRawText()是获取响应的原始文本数据,比如json page.putField("title", new JsonPathSelector("$.data.title").select(page.getRawText())); page.putField("content", new JsonPathSelector("$.data.content").select(page.getRawText())); //高版本提供的json数据解析方式,推荐。page.getJson()会将rawText转换为Json page.putField("name", page.getJson().jsonPath("$.name").get()); page.putField("name", page.getJson().removePadding("callback").jsonPath("$.name").get()); 

 

自定义SpiderMonitor:爬虫监控

Web Magic 封装了 SpiderMonitor、SpiderStatusMXBean 用于监控爬虫状态

public interface SpiderStatusMXBean { 
     //爬虫名称,即爬虫的uuid,唯一标识一个爬虫 public String getName(); public String getStatus(); public int getThread(); public int getTotalPageCount(); //待爬取的url数量 public int getLeftPageCount(); public int getSuccessPageCount(); public int getErrorPageCount(); public List<String> getErrorPages(); //启动爬虫,实质是调用 spider.start() public void start(); //终止爬虫,实质是调用 spider.stop() public void stop(); public Date getStartTime(); public int getPagePerSecond(); } 

自定义的爬虫监控 CustomSpiderMonitor

import us.codecraft.webmagic.Spider; import us.codecraft.webmagic.SpiderListener; import us.codecraft.webmagic.monitor.SpiderMonitor; import us.codecraft.webmagic.monitor.SpiderStatusMXBean; import javax.annotation.Nullable; import javax.management.JMException; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; / * 自定义的爬虫监控 * * @author chy */ public class CustomSpiderMonitor extends SpiderMonitor { 
     / * 爬虫监控实例 */ private static final CustomSpiderMonitor INSTANCE = new CustomSpiderMonitor(); private CustomSpiderMonitor() { 
     super(); } / * 获取爬虫监控实例 * * @return CustomSpiderMonitor */ public static CustomSpiderMonitor instance() { 
     return INSTANCE; } / * 注册爬虫(添加爬虫监控) * * @param spiders 爬虫列表 * @return CustomSpiderMonitor * @throws JMException registerMBean失败 */ @Override public synchronized CustomSpiderMonitor register(Spider... spiders) throws JMException { 
     for (Spider spider : spiders) { 
     MonitorSpiderListener monitorSpiderListener = new MonitorSpiderListener(); if (spider.getSpiderListeners() == null) { 
     List<SpiderListener> spiderListeners = new ArrayList<>(); spiderListeners.add(monitorSpiderListener); spider.setSpiderListeners(spiderListeners); } else { 
     spider.getSpiderListeners().add(monitorSpiderListener); } SpiderStatusMXBean spiderStatusMXBean = super.getSpiderStatusMBean(spider, monitorSpiderListener); super.registerMBean(spiderStatusMXBean); super.getSpiderStatuses().add(spiderStatusMXBean); } return this; } / * 获取所有爬虫的运行状态 * * @return List<SpiderStatusMXBean> */ @Override public List<SpiderStatusMXBean> getSpiderStatuses() { 
     return super.getSpiderStatuses(); } / * 获取指定爬虫的运行状态 * * @param spider 爬虫 * @return SpiderStatusMXBean */ @Nullable public SpiderStatusMXBean getSpiderStatus(Spider spider) { 
     return this.getSpiderStatus(spider.getUUID()); } / * 通过爬虫的UUID获取爬虫运行状态 * * @param uuid 爬虫的UUID * @return SpiderStatusMXBean */ @Nullable public SpiderStatusMXBean getSpiderStatus(String uuid) { 
     for (SpiderStatusMXBean spiderStatus : super.getSpiderStatuses()) { 
     if (spiderStatus.getName().equals(uuid)) { 
     return spiderStatus; } } return null; } / * 获取指定状态的爬虫运行情况 * * @param spiderStatusEnum 爬虫状态枚举 * @return List<SpiderStatusMXBean> */ public List<SpiderStatusMXBean> getSpiderStatus(Spider.Status spiderStatusEnum) { 
     return super.getSpiderStatuses().stream() .filter(spiderStatus -> spiderStatusEnum.name().equals(spiderStatus.getStatus())) .collect(Collectors.toList()); } } 

 

爬虫枚举 SpiderEnum

import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; import us.codecraft.webmagic.Spider; import us.codecraft.webmagic.pipeline.ConsolePipeline; import javax.annotation.Nullable; / * 爬虫枚举 * * @author chy */ @Getter @AllArgsConstructor(access = AccessLevel.PRIVATE) public enum SpiderEnum { 
     / * 百度爬虫 */ BAIDU_SPIDER("baidu", Spider.create(new BaiduPageProcessor()) .setUUID("baidu") .addUrl("https://www.baidu.com") .addPipeline(new BaiduPipeline()) .thread(7)), / * 知乎爬虫 */ ZHIHU_SPIDER("zhihu", Spider.create(new ZhihuPageProcessor()) .setUUID("zhihu") .addUrl("https://www.zhihu.com") .addPipeline(new ZhihuPipeline()) .thread(7)), ; / * 唯一标识一个爬虫 */ private final String uuid; / * 爬虫配置 */ private final Spider spider; / * 通过uuid获取对应的爬虫实例 * * @param uuid 爬虫的uuid * @return Spider */ @Nullable public static Spider getSpiderByUuid(String uuid) { 
     for (SpiderEnum spiderEnum : values()) { 
     if (spiderEnum.uuid.equals(uuid)) { 
     return spiderEnum.getSpider(); } } return null; } } 

 

爬虫 controller

@PutMapping("start/{uuid}") public Boolean start(@PathVariable("uuid") String uuid) { 
     //校验爬虫状态 List<SpiderStatusMXBean> runningSpiderStatusList = CustomSpiderMonitor.instance().getSpiderStatus(Spider.Status.Running); if (CollectionUtils.isNotEmpty(runningSpiderStatusList)) { 
     log.info("当前已有运行中的爬虫,请先终止运行中的爬虫,或等运行中的爬虫执行完毕,runningSpiderStatusList={}", runningSpiderStatusList); return false; } //根据uuid获取对应爬虫的配置 Spider spider = SpiderEnum.getSpiderByUuid(uuid); if (spider == null) { 
     log.error("指定爬虫不存在,uuid={}", uuid); return false; } //注册爬虫监控 try { 
     CustomSpiderMonitor.instance().register(spider); } catch (JMException e) { 
     log.error("注册爬虫监控失败,uuid={}", uuid, e); return false; } //异步启动爬虫 spider.start(); return true; } @PutMapping("stop/{uuid}") public Boolean stop(@PathVariable("uuid") String uuid) { 
     SpiderStatusMXBean spiderStatus = CustomSpiderMonitor.instance().getSpiderStatus(uuid); if (spiderStatus == null) { 
     log.error("爬虫不存在,uuid={}", uuid); return false; } if (!Spider.Status.Running.name().equals(spiderStatus.getStatus())) { 
     log.error("爬虫状态不是运行中,不能进行终止操作,uuid={}", uuid); return false; } spiderStatus.stop(); return true; } @GetMapping("status/{uuid}") public SpiderStatusMXBean getStatus(@PathVariable("uuid") String uuid) { 
     return CustomSpiderMonitor.instance().getSpiderStatus(uuid); } @GetMapping("status") public List<SpiderStatusMXBean> listStatus() { 
     return CustomSpiderMonitor.instance().getSpiderStatuses(); } 

 

自定义Scheduler:url管理

常见的Scheduler

  • QueueScheduler:使用内存队列保存待抓取URL,如果待抓取的url较多,则内存占用大。
  • FileCacheQueueScheduler:使用文件保存抓取URL,可以在关闭程序并下次启动时,从之前抓取到的URL继续抓取,需指定路径,会建立.urls.txt和.cursor.txt两个文件。
  • RedisScheduler:使用Redis保存抓取队列,可进行多台机器同时合作抓取。

除了 RedisScheduler 是使用 Redis 的set进行去重,其它 Scheduler 都是使用 HashSetDuplicateRemover 进行去重。URL较多时,使用HashSetDuplicateRemover 会比较占用内存,也可以尝试 BloomFilterDuplicateRemover,但可能会漏抓页面。

可以自行继承实现 DuplicateRemovedScheduler,如果接入了爬虫监控,还需要实现 MonitorableScheduler 接口。

 

自定义Pipeline:结果处理

保存到文件示例

@Slf4j public class ArticleFilePipeline implements Pipeline { 
     private static final File TARGET_FILE = new File("article.txt"); @Override public void process(ResultItems resultItems, Task task) { 
     ArticlePo articlePo = resultItems.get("data"); String line = String.format("%s,%s,%s\n", articlePo.getId(), articlePo.getAuthor(), articlePo.getTitle()); try { 
     FileUtils.write(TARGET_FILE, line, StandardCharsets.UTF_8, true); } catch (IOException e) { 
     log.error("写入文件失败,line={}", line, e); } } } 

 

保存到DB示例

@Component public class ArticleDbPipeline implements Pipeline { 
     @Resource private ArticleDao articleDao; @Override public void process(ResultItems resultItems, Task task) { 
     ArticlePo articlePo = resultItems.get("data"); articleDao.insert(articlePo); } } 

 

自定义Downloader

常见的比如结合 Selenium 爬取动态页面

 

获取本地代理地址


免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/112660.html

(0)
上一篇 2026-01-16 20:11
下一篇 2026-01-16 20:20

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信