본문 바로가기
Native App/👽Android

web Crawling - Jsoup

by yewoneeee 2022. 6. 27.

Jsoup이란?

자바로 만들어진 HTML 파서로 URL, 파일, 문자열을 소스로 하여 HTML을 파싱할 수 있는 자바 라이브러리

 

Jsoup, Selenium 차이

 Jsoup은 백그라운드에서 HTTP Request/Response가 이루어지며, Request를 던졌을 때 웹 서버에서 응답한 결과를 받아온다. 따라서, 서버 사이드 랜더링(SSR, Server-Side Rendering)을 사용하는 웹 사이트는 서버에서 랜더링을 한 후 화면을 그리기 때문에 크롤링이 가능하지만, 클라이언트 사이드 랜더링(CSR, Client-Side Redering)을 사용하는 웹 사이트는 최소한의 페이지만 서버에서 랜더링하고 클라이언트(브라우저)에서 나머지 화면을 랜더링하기 때문에
HTTP Request로는 실제 브라우저에서 보여지는 화면을 스크랩 할 수 없다.
하지만, Selenium은 현재 브라우저에 출력된 페이지의 소스를 파싱 할 수 있다는 특성을 이용하여 CSR을 사용하는 웹 사이트도 크롤링 할 수 있다. 또한, XPath를 지원하고 Javascript 명령어를 실행할 수 있기 때문에 Jsoup보다 많은 데이터를 크롤링 할 수 있다.

 Selenium을 이용한 크롤러가 Jsoup보다 마냥 좋은 것은 아니다. Jsoup은 HTTP Request를 통해 웹서버에 직접 요청하기 때문에 빠른 응답을 받을 수 있다. 하지만, Selenium은 브라우저가 랜더링 된 후 페이지를 파싱하기 때문에 수집 속도가 느리다. 따라서, 동적 웹페이지가 아니라면 Jsoup을 이용하는 것이 좋다.

출처: https://heodolf.tistory.com/103

 

[크롤링] Selenium을 이용한 JAVA 크롤러 (1) - HTML 파싱

2020/02/25 - [Back-end/JAVA] - [크롤링] Jsoup을 이용한 JAVA 크롤러 (1) - HTML 파싱 2020/02/25 - [Back-end/JAVA] - [크롤링] Jsoup을 이용한 JAVA 크롤러 (2) - 파일 다운로드 0. 서론  이전 포스트에서 Js..

heodolf.tistory.com

동적 웹페이지가 아니기 때문에 selenium 대신 전에 사용했던 Jsoup을 다시 사용해봤다

 

웹 페이지에서 정보를 가져와서 리사이클러뷰에 출력하는 실습을 해봤다

 

 

사용 예제


파싱한 페이지: https://ba-on.com/product/list.html?cate_no=34 

 

모듈 설치

// build.gradle
// Glide
implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'

// Jsoup
implementation 'org.jsoup:jsoup:1.13.1'

 

인터넷 설정

// AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET"/>

 

크롤링 구현 함수

fun crawling(){
        Thread(Runnable{
            val url = "https://ba-on.com/product/list.html?cate_no=34"
            val doc = Jsoup.connect(url).get()
            val imgList = doc.select("div.thumbnail")
            val titleList = doc.select("div.description")

            for(i in imgList.indices){
                val item_img = "https:"+imgList[i].select("a img").attr("src")
                val item_title = titleList[i].select("div.description").select("a ul li span").text()
                //Log.d("MyActivity",item_img.toString())
                //Log.d("MyActivity",item_title)
                productList.add(Product(item_img,item_title))
            }

            runOnUiThread{
                val productRvAdapter=ProductRVAdapter(productList,this)
                binding.productListRv.adapter=productRvAdapter
                binding.productListRv.layoutManager=LinearLayoutManager(this,LinearLayoutManager.VERTICAL,false)
            }
        }).start()
    }

1. http 통신을 하기 때문에 메인 스레드와 비동기적으로 작동해야해서 thread를 만들어서 사용

 

 

2. 파싱하고자 하는 웹 페이지 연결

val url = "https://ba-on.com/product/list.html?cate_no=34"
val doc = Jsoup.connect(url).get()

 

 

3. 필요한 정보를 추출하기 위해 필요한 부분의 태그를 리스트로 가져옴

이미지 사진을 가져오기 위해 class명이 thumnatil인 div태그를 전부 가져옴

val imgList = doc.select("div.thumbnail")

 

상품명을 가져오기 위해 클래스명이 description인 div 태그 전부 가져옴

val titleList = doc.select("div.description")

 

 

4. 실제 이미지와 텍스트 가져오기

imgList 각 요소의 a태그 안 img 태그의 src 속성이 필요함

상대주소로 되어있기 때문에 "https:" 추가해줌

val item_img = "https:"+imgList[i].select("a img").attr("src")

 

titleList 각 요소의 a태그 안 ul 태그 안 li 태그 안 span 태그의 text가 필요함

val item_title = titleList[i].select("a ul li span").text()

 

 

5. recyclerView 어댑터 설정

runOnUiThread{
    val productRvAdapter=ProductRVAdapter(productList,this)
    binding.productListRv.adapter=productRvAdapter
    binding.productListRv.layoutManager=LinearLayoutManager(this,LinearLayoutManager.VERTICAL,false)
}

recyclerView 어댑터 설정은 크롤링 후에 실행되어야 하기 때문에 스레드 내에 runOnUiThread 사용해서 작성

 

 

6. 이미지는 glide 사용해서 출력

// recyclerView Adapter
inner class ViewHolder(val binding: ShoppingProductBinding,val context:Context): RecyclerView.ViewHolder(binding.root){
        fun bind(product:Product){
            Glide.with(context)
                .load(product.productSrc)
                .into(binding.itemIv)
            binding.itemTv.text=product.productName
        }
    }

Glide에서 context가 필요해서 ViewHolder의 매개변수로 context를 받도록 했음

 

실습 코드


data class

package com.example.jsouptest

data class Product(
    val productSrc: String,
    val productName: String
)

 

 

adapter 전체 코드

package com.example.jsouptest

import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.example.jsouptest.databinding.ShoppingProductBinding

class ProductRVAdapter(private var productList:ArrayList<Product>,val context:Context):RecyclerView.Adapter<ProductRVAdapter.ViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductRVAdapter.ViewHolder {
        val binding: ShoppingProductBinding = ShoppingProductBinding.inflate(LayoutInflater.from(parent.context),parent,false)
        return ViewHolder(binding,context)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(productList[position])
    }

    override fun getItemCount(): Int =productList.size

    inner class ViewHolder(val binding: ShoppingProductBinding,val context:Context): RecyclerView.ViewHolder(binding.root){
        fun bind(product:Product){
            Glide.with(context)
                .load(product.productSrc)
                .into(binding.itemIv)
            binding.itemTv.text=product.productName
        }
    }
}

 

 

전체 코드

package com.example.jsouptest

import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.jsouptest.databinding.ActivityShopBinding
import org.jsoup.Jsoup

class ShopActivity:AppCompatActivity() {
    lateinit var binding: ActivityShopBinding
    var productList = ArrayList<Product>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityShopBinding.inflate(LayoutInflater.from(this))
        setContentView(binding.root)
        onClickEvent()
        crawling()
    }

    fun onClickEvent(){
        binding.gotoMainActivityBtn.setOnClickListener {
            finish()
        }
    }

    fun crawling(){
        Thread(Runnable{
            val url = "https://ba-on.com/product/list.html?cate_no=34"
            val doc = Jsoup.connect(url).get()
            val imgList = doc.select("div.thumbnail")
            val titleList = doc.select("div.description")

            for(i in imgList.indices){
                val item_img = "https:"+imgList[i].select("a img").attr("src")
                val item_title = titleList[i].select("a ul li span").text()
                //Log.d("MyActivity",item_img.toString())
                //Log.d("MyActivity",item_title)
                productList.add(Product(item_img,item_title))
            }

            runOnUiThread{
                val productRvAdapter=ProductRVAdapter(productList,this)
                binding.productListRv.adapter=productRvAdapter
                binding.productListRv.layoutManager=LinearLayoutManager(this,LinearLayoutManager.VERTICAL,false)
            }
        }).start()
    }
}

 

 

 

실행 화면


 

근데 html 태그가 복잡해질수록 크롤링이 잘 안 돼서 다음엔 셀레니움을 써서 해볼 생각이다

'Native App > 👽Android' 카테고리의 다른 글

adb 명령어를 사용해서 안드로이드 기기 무선 페어링  (0) 2024.07.06
firebase 연동 정리  (0) 2023.01.14
kakao login api 사용 - 2  (0) 2022.06.19
kakao login api 사용 - 1  (0) 2022.06.16
#02 SharedPreferences  (0) 2022.06.16

댓글