Java网络爬虫怎么写?一起来学吧(上篇)

如何使用 Java 进行网页抓取。

微信搜索关注《Java学研大本营》,加入读者群,分享更多精彩

Web 抓取、Web 采集或数据提取是一种从网页或其他在线资源中提取目标数据的技术。Web Scraping 如果操作得当,可以成为执行各种任务的强大工具,例如用于索引内容的搜索引擎 Web 数据爬行、价格比较机器人、使用社交媒体数据收集的市场研究以及开发人员的功能测试。

本文将讨论我们如何利用 Java 开始网络抓取,我们将探索静态与动态抓取、常见错误、性能优化和最佳实践。

概念:它是如何工作的?

Web Scraping 以其非常粗糙的形式非常容易理解。目标是将驻留在某个网页上的数据获取到我们的程序代码中,以便我们可以对数据运行一些转换逻辑,保存它或执行任何业务逻辑。然而,最终目标是使数据在程序中可用以利用它。

我们知道所需的信息在网页的 HTML 代码中的某个地方,所以我们只需要获取 HTML 作为对 Web 请求的响应,然后我们就可以从那里获取所需的数据(听起来很简单!!,不是真的。)

代码设置

为了设置代码,您需要在您的计算机上安装 Java,我在撰写本文时使用的是 Java 11,但任何高于 11 的 Java 版本都应该可以正常工作。

第 1 步:使用您选择的 IDE 创建一个新的 Java maven 项目。

第 2 步:将以下代码粘贴到您的pom.xml文件中的标记下。

<!-- https://mvnrepository.com/artifact/org.jsoup/jsoup -->
<dependency>
    <groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <version>1.14.3</version>
</dependency>

如果您正在寻找不同的版本,您可以在此处找到所有版本。

为什么是 Jsoup?

Jsoup 库提供了一个很好的 API,用于解析对活动 DOM 的 String HTML 响应,以便您可以查询、过滤、修改和提取由其类、标识符、标签等指定的元素。就像您在 JS/Jquery 中所做的那样。

Finding Elements
getElementById(String id)
getElementsByTag(String tag)
getElementsByClass(String className)
getElementsByAttribute(String key) (and related methods)
Element siblings: siblingElements(), firstElementSibling(), lastElementSibling(); nextElementSibling(), previousElementSibling()
Graph: parent(), children(), child(int index)
Element Data
attr(String key) to get and attr(String key, String value) to set attributes
attributes() to get all attributes
id(), className() and classNames()
text() to get and text(String value) to set the text content
html() to get and html(String value) to set the inner HTML content
outerHtml() to get the outer HTML value
data() to get data content (e.g. of script and style tags)
tag() and tagName()

您可以在 Jsoup 的官方文档中阅读更多相关信息。

第3步:抓取数据

出于本文的目的,我们将使用虚假网站crapeme.live。但基础知识可以应用于任何网站。

目标:获取每个商品的名称、价格、购买链接。

让我们开始 !!

写一个简单的包装类来做同样的事情

import Enums.IdentifierType;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
// Enum for our wrapper function. you can omit this and pass a //string or just create separate method
public enum IdentifierType {
    ATTRIBUTE,
    ID,
    CLASS,
    TAG
}
public class StaticScraper {

    public Document getDocumentFromURL(URL resourceUrl) throws IOException {
        return  Jsoup.connect(resourceUrl.toString()).get();
    }

    public List<Element> getElementsByIdentifier(Document document ,String identifier, IdentifierType identifiertype){
        List<Element> elements = new ArrayList<>();

        switch (identifiertype){
            case ID:
                elements.add(document.getElementById(identifier));
                return elements;
            case TAG:
                return document.getElementsByTag(identifier);
            case ATTRIBUTE:
                return document.getElementsByAttribute(identifier);
            case CLASS:
                return document.getElementsByClass(identifier);
            default:
                System.out.println("Not a valid Identifier type");
        }
        
        return elements;
    }

正如我们在开发工具(打开 cntl+shift+i)中看到的那样,产品卡具有 css 类作为产品。我们现在可以根据 css 类选择卡片,如下所示。

主类。

public class Main {
    public static  void main(String args[]) throws IOException {

        // create a new instance of scraper
        StaticScraper scraper = new StaticScraper();

        String urlToScrape = "https://scrapeme.live/shop/";
        // get the HTML document from the target url
        Document htmlDocument = scraper.getDocumentFromURL(new URL(urlToScrape));

        String classNameForProductCard = "product";
        List<Element> productCards = scraper.getElementsByIdentifier(htmlDocument,classNameForProductCard, IdentifierType.CLASS);

        System.out.println(productCards);
    }
}

运行时它将打印所有产品的

标签列表,我们已经将数据细化为我们想要的产品列表。现在让我们进一步完善它以仅打印价格和产品名称。

我们可以通过查看名称元素和价格元素的标识符进一步过滤产品名称及其价格的

标签。

我们可以观察到

标签下的产品标题具有 css 类“woocommerce-loop-product__title”,价格信息具有 CSS 类“woocommerce-Price-amount amount”。使用信息,我们可以提取所需的信息,如下所示。

public static  void main(String args[]) throws IOException {

    StaticScraper scraper = new StaticScraper();

    String urlToScrape = "https://scrapeme.live/shop/";
    Document htmlDocument = scraper.getDocumentFromURL(new URL(urlToScrape));

    String classNameForProductCard = "product";
    List<Element> productCards = scraper.getElementsByIdentifier(htmlDocument,classNameForProductCard, IdentifierType.CLASS);
// extract and print the desired information

    for(Element productCard: productCards){
        String productNameClassName = "woocommerce-loop-product__title";
        String productPriceClassName = "woocommerce-Price-amount amount";
        String extractedProductName =  productCard.getElementsByClass(productNameClassName).text();
        String extractedProductPrice = productCard.getElementsByClass(productPriceClassName).text();
        System.out.println(extractedProductName + " - "+ extractedProductPrice);
    }
}

当你运行它时,你可以看到如下输出。您已成功抓取商品和价格信息。太酷了!!

以下链接

到目前为止,我们只从第一页抓取了产品数据。总共 16 个产品,可以通过突出显示的输出进行验证。现在我们想从所有页面中抓取产品。这将需要我们跟踪链接。这有两个步骤,首先,获取所有页面链接的列表,访问/抓取链接并获取到下一页的链接并重复该过程。

获取页面链接

为了获取页面链接,我们采用了与获取产品卡类似的方法。

// this gives us all <a> tag with the link
String pageLinkCSSQuery = ".page-numbers>li>a";
Document currPageHtml = getDocumentFromURL(new URL(currUrl));
List<Element> pageLinks =  currPageHtml.select(pageLinkCSSQuery);
// extracting actual link 
for(Element link: pageLinks){
   String linkUrl = link.attr("href");
}

抓取页面

这里的逻辑非常简单,访问/抓取首页链接并获取所有下一页链接,现在访问下一页链接并重复相同的过程。

注意:因为链接是重复的,我们要避免多次访问同一个链接,因为我们会跟踪访问过的链接,如果该链接已经访问过,我们就不会再次访问它。

// Crawl function API
public List<Element> Crawl(URL initialUrl, int maxVisits, String pageLinkSelectorQuery) throws IOException {

    List<Element> scrapedElements = new ArrayList<>();
    Set<String> visitedPages = new HashSet<>();
    crawlPages(initialUrl.toString(),visitedPages,maxVisits,pageLinkSelectorQuery,scrapedElements);
    return scrapedElements;
}
// Helper function to recursively visit pages
private void crawlPages(String currUrl, Set<String> visited, int maxVisits, String pageLinkSelectorQuery, List<Element> elements) throws IOException {

    if(visited.size()==maxVisits){
        return ;
    }
    // mark the url visited
    visited.add(currUrl);

    // get page links
    Document currPageHtml = getDocumentFromURL(new URL(currUrl));
    List<Element> pageLinks =  currPageHtml.select(pageLinkSelectorQuery);

    // populate elements
    String classNameForProductCard = "product";
    List<Element> productCards = getElementsByIdentifier(currPageHtml,classNameForProductCard, IdentifierType.CLASS);

    // add curr page elements to elements list    
     elements.addAll(productCards);

    for(Element link: pageLinks){
        String nextUrl = link.attr("href");
        if(!visited.contains(nextUrl)){
            crawlPages(nextUrl,visited,maxVisits,    pageLinkSelectorQuery,elements);
        }
    }
    return;
}

可以用多种方式编写递归函数来优化参数和返回对象,出于演示目的,这是一个可以解决问题的简单函数。

我们可以像以前一样从浏览器开发者工具中找到产品链接的唯一标识符。

我们可以看到页面链接在类名 .page-number 下,然后是 lst 标签,然后是 a 标签。有效地将“.page-numbers>li>a”作为 css 选择器查询。

从 Main 方法调用爬网功能。

// get the page links from a css selector query
String pageLinkCSSQuery = ".page-numbers>li>a";
int maxVisits = 4;
String firstPageUrl = "https://scrapeme.live/shop/page/1/";
List<Element> elements = scraper.Crawl(new URL(firstPageUrl),maxVisits,pageLinkCSSQuery);

for(Element element: elements){
    String productNameClassName = "woocommerce-loop-product__title";
    String productPriceClassName = "woocommerce-Price-amount amount";
    String extractedProductName =  element.getElementsByClass(productNameClassName).text();
    String extractedProductPrice = element.getElementsByClass(productPriceClassName).text();
    System.out.println(extractedProductName + " - "+ extractedProductPrice);
}
System.out.println("Total Products Scraped from crawling "+maxVisits +" pages: " +elements.size());

静态抓取的局限性!!

虽然以这种方式抓取数据非常棒,因为返回的 HTML 填充了所有数据并且不涉及任何 JS 渲染。但是就像所有现代网站一样,它们使用 JS 动态修改 DOM,而 Jsoup 只是一个解析器,它没有 JS 引擎来渲染 JS。这就是无头浏览器来拯救我们的地方,它们具有浏览器的全部功能。

考虑一个简单的例子,我们请求这样的文档。

<!DOCTYPE html>
<html lang="en">
<head>
<title>Example Page</title>
</head>
<body>
<h1 id="myheading"> Hello World ! </h1
<script>
document.getElementById("myheading").innerText = "Hello World ! Scraping is great";
</script>
</body>
</html>

正如我们所知,一旦 script 标签加载,它就会改变 h1 中的文本,但由于 Jsoup 和静态抓取技术无法渲染 JS,它会显示Hello World !代替你好世界!刮痧很棒。

克服这个问题的唯一方法是渲染 JS。这就是无头浏览器来拯救的地方。

Java 中有许多这样的浏览器实现,例如 Selenium、Playwright、HtmlUnit等等。它们都提供 JS 渲染逻辑,以便在响应中返回完全渲染的 HTML DOM。

推荐书单

《项目驱动零起点学Java》

《项目驱动零起点学Java》共分 13 章,围绕 6 个项目和 258 个代码示例,分别介绍了走进Java 的世界、变量与数据类型、运算符、流程控制、方法、数组、面向对象、异常、常用类、集合、I/O流、多线程、网络编程相关内容。《项目驱动零起点学Java》总结了马士兵老师从事Java培训十余年来经受了市场检验的教研成果,通过6 个项目以及每章的示例和习题,可以帮助读者快速掌握Java 编程的语法以及算法实现。扫描每章提供的二维码可观看相应章节内容的视频讲解。

《项目驱动零起点学Java》贯穿6个完整项目,经过作者多年教学经验提炼而得,项目从小到大、从短到长,可以让读者在练习项目的过程中,快速掌握一系列知识点。

马士兵,马士兵教育创始人,毕业于清华大学,著名IT讲师,所讲课程广受欢迎,学生遍布全球大厂,擅长用简单的语言讲授复杂的问题,擅长项目驱动知识的综合学习。马士兵教育获得在线教育“名课堂”奖、“最受欢迎机构”奖。

赵珊珊,从事多年一线开发,曾为国税、地税税务系统工作。拥有7年一线教学经验,多年线上、线下教育的积累沉淀,培养学员数万名,讲解细致,脉络清晰。

《项目驱动零起点学Java》(马士兵,赵珊珊)【摘要 书评 试读】- 京东图书京东JD.COM图书频道为您提供《项目驱动零起点学Java》在线选购,本书作者:,出版社:清华大学出版社。买图书,到京东。网购图书,享受最低优惠折扣!icon-default.png?t=N3I4https://item.jd.com/13607758.html

精彩回顾

部署Spring Boot应用程序

Java Spring Boot 3.0.0 RC1 震撼登场!

微信搜索关注《Java学研大本营》

访问【IT今日热榜】,发现每日技术热点

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值