3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[Kotlin] JSoupを使ってHTML解析する方法

Last updated at Posted at 2021-03-09

##はじめに##

JSoupをYahoo NewsのHTMLから内容を取得する方法を説明します。
ちなみにKotlinのようなスッキリ感はないがJAVAでも同様です。

先ずはMavenやGradleでJsoupを導入

    <dependency>
        <groupId>org.jsoup</groupId>
        <artifactId>jsoup</artifactId>
        <version>1.13.1</version>
    </dependency>

必要ではないが、僕は”これがあれば便利”の拡張機能とNULL POINTER防止の為を以下を作成した。
JSoupのAPIはElements.first(), Elements.last(), Element.selectFirst()などはNULL POINTER発生します。
そのまま使うと細かくチェックしないとNPE発生するので厄介です。

JSoupDoc.kt
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.util.*
import java.util.function.Consumer
import kotlin.streams.toList

class JSoupDoc(private val document: Optional<Document>) {
    companion object {
        const val CONNECTION_TIMEOUT: Int = 150000
        var errorHandler: Consumer<Throwable> = Consumer { }

        @JvmStatic
        fun fromUrl(url: String): JSoupDoc {
            val htmlDoc = kotlin.runCatching {
                Jsoup.connect(url).ignoreHttpErrors(true).timeout(CONNECTION_TIMEOUT).get()
            }.fold(
                onSuccess = { Optional.ofNullable(it) },
                onFailure = {
                    errorHandler.accept(it)
                    Optional.empty()
                }
            )
            return JSoupDoc(htmlDoc)
        }
    }

    val isEmpty = document.isEmpty
    val isPresent = document.isPresent
    val body: Optional<Element> = document.map { it.body() }

    fun listElement(query: String): List<Element> {
        return document.map { it.select(query).stream().toList() }.orElse(listOf())
    }
}

fun Element.selectFirstOpt(query: String): Optional<Element> {
    return Optional.ofNullable(this.select(query).first())
}

fun Element.textWithout(text: String): String = this.text().replace(text, "")

fun Element.href(): String = this.attr("href")

fun Element.absHref(): String = this.attr("abs:href")

fun Element.firstText(query: String): String = this.selectFirstOpt(query).map { it.text() }.orElse("")

fun Element.firstHref(query: String): String = this.selectFirstOpt(query).map { it.href() }.orElse("")

fun Element.firstAbsHref(query: String): String = this.selectFirstOpt(query).map { it.absHref() }.orElse("")

##例1:Yahoo News##
###取得したい内容を定義###

例として、news.yahoo.co.jp/ranking/access/newsの内容を取得するプログラムをつくります。
p14.png

パッと見れば一つのニュースでは以下の構成

  • タイトル
  • 内容の連結
  • 新聞の元
  • 時間
  • 画像
NewsItem.kt
    class NewsItem(
        val title: String,
        val url: String,
        val source: String,
        val date: String,
        val imageUrl: String
    )

###ChromeのToolsを使ってHTML分析###
p11.png

p12.png

まず、一つのニュース内容は全部newsFeed_item_linkというaの中にいます(赤の部分)。
この中で、欲しい内容は何処にいるかを探します(緑の部分)。
それぞれの唯一無二の特徴を探します。

  • タイトル ー newsFeed_item_titleというdiv内
  • 内容の連結 ー 最初のaのhref
  • 新聞の元 ー newsFeed_item_mediaというspan内
  • 時間 ー newsFeed_item_dateというtime内
  • 画像の連結 ー 唯一のimg内のsrc

###Coding###

YahooNewsParser.kt
object YahooNewsParser {
    fun get() {
        val doc = JSoupDoc.fromUrl("https://news.yahoo.co.jp/ranking/access/news")
        val newsList: List<NewsItem> = doc.listElement("a.newsFeed_item_link").map { parseSingleItem(it) }

        val gson = GsonBuilder()
            .setPrettyPrinting()
            .create()
        gson.toJson(newsList, FileWriter(Paths.get("news.json").toFile()));
    }

    private fun parseSingleItem(element: Element): NewsItem {
        val url = element.href()
        val title = element.firstText("div.newsFeed_item_title")
        val source = element.firstText("span.newsFeed_item_media")
        val date = element.firstText("time.newsFeed_item_date")
        val imageUrl = element.selectFirstOpt("img")
            .map { it.attr("src") }.orElse("").substringBefore("?")
        return NewsItem(title, url, source, date, imageUrl)
    }
}

###結果###
p13.png

##例2:楽天市場##
###取得したい内容とHTML分析###
p21.png
一つの内容 ー dui-card searchresultitemというdiv

  • タイトル ー content titleというdiv > h2 > aの文字
  • 内容の連結 ー content titleというdiv > h2 > aのhref
  • 価格 ー content description priceというdiv > spanの文字
  • 商店 ー content merchant _ellipsisというdiv > aの文字
  • 画像の連結 ー imageというdiv > a > imgのsrc

###Coding###

RakutenShoppingParser.kt
object RakutenShoppingParser {
    class ShopItem(
        val title: String,
        val url: String,
        val price: String,
        val shop: String,
        val imageUrl: String
    )

    fun get(keyWord: String) {
        val doc = JSoupDoc.fromUrl("https://search.rakuten.co.jp/search/mall/$keyWord/")
        val itemList: List<ShopItem> =
            doc.listElement("div[class=dui-card searchresultitem]").map { parserSingleItem(it) }

        val gson = GsonBuilder()
            .setPrettyPrinting()
            .create()
        gson.toJson(itemList, FileWriter(Paths.get("items.json").toFile()));
    }

    private fun parserSingleItem(element: Element): ShopItem {
        val titleElement = element.selectFirstOpt("div[class=content title] h2 a")
        val title = titleElement.map { it.text() }.orElse("")
        val url = titleElement.map { it.href() }.orElse("")
        val price = element.firstText("div[class=content description price] span")
        val shop = element.firstText("div[class=content merchant _ellipsis] a")
        val imageUrl = element.selectFirstOpt("div.image a img").map { it.attr("src") }
            .orElse("").substringBefore("?")
        return ShopItem(title, url, price, shop, imageUrl)
    }
}

###結果###
RakutenShoppingParser.get("Kotlin")
p22.png

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?