[ 준비 ]
크롤링할 사이트
이번에 크롤링할 사이트는 flaticon이라는 사이트 입니다.
저는 주로 android 기본 Vector Asset을 많이 사용하는데요!
아무래도 아이콘이 다양하지 않다 보니 여기에 마음에 드는 아이콘이 없을 때 flaticon에서 아이콘을 많이 받아서 쓰곤 합니다.
링크는 아래에도 한 번에 정리해 드리겠습니다.
URL 저장
url 저장은 이전 글에서 하던 방식대로 local.properties에 해주겠습니다.
build.gradle(:app) dependencies 추가
Jsoup, Coroutines을 사용하기 위해 아래 두둘의 코드를 추가해줍니다.
AndroidManifest.xml에 권한 추가
인터넷 사용을 위해 INTERNET권한도 manifest.xml에 추가해줍니다.
[크롤링할 Elements 보기]
먼저 저희가 크롤링하고자 하는 flaticon사이트에서 android관련 아이콘을 검색했다고 가정합시다. 🤖
검색 후에는 개발자 도구에 들어가서 검색을 하면 됩니다. 개발자 도구는 f12를 누르면 들어갈 수 있습니다.
여기서 아래 사진처럼 빨간 화살표로 되어있는 아이콘을 누르거나 Command + shift + c를 누르면 원하는 Element를 선택할 수 있습니다.
저희는 아이콘을 크롤링할 예정이니 아이콘을 선택합니다. 그럼 모든 icon들을 포함하고 있는 element는 section태그의 class 가 "search-result icons-search-result"인 항목입니다. 보통 웹에서 클래스를 '.'으로 id를 '#'으로 표현한다는 점을 사용하면 아래 콘솔을 통해 쉽게 찾을 수 있습니다. 예를 들어 저희가 선택한 section태그를 id로 찾아보겠습니다.
이런 식으로 Element들을 파악 후 규칙을 찾아서 크롤링하면 됩니다.
크롤링에 꼭 크롤링하고자 하는 지점까지 Element를 타고 들어가지 않아도 됩니다. 저는 다음과 같이 엘리먼트들을 선택하였습니다.
section.search-result.icons-search-result > ul.icons > div.icon--holder > a.view.link-icon-detail
저는 위에서 선택한 a태그 안에는 data-id, title, href 3가지 attribute와 하위에 img태그 안에 src attribute를 크롤링하도록 하겠습니다.
[기본 설명]
data class
우선 간단히 아이콘의 이름, 이미지 주소, 이미지를 커스텀해서 받는 사이트 이렇게 3가지를 가지는 클래스를 만들어 주도록 하겠습니다.
나중에 데이터를 좀 더 보기 쉽게 표현하기 위하여 toString함수를 재정의 하였습니다.
그리고 크롤링을 진행하다 보니 같은 검색어에 같은 페이지를 검색해도 계속 다른 결과를 보여주도록 되어있다는 걸 알게 되었습니다. 😱
그래서 웹에서 element를 살펴보니 flaticon사이트에서 사용하는 고유 키?로 생각되는 attribute가 있어서 실제 받는 부분에서는 HashMap으로 받으면서 <Key, IconInformation> 형식으로 받았습니다.
OnQueryTextListener (SearchView 리스너)
submit버튼을 사용하기 위하여 isSubmitButtionEnabled를 true로 설정하고 setOnQueryTextListener도 만들어준 리스너로 등록합니다.
검색 시에 텍스트가 없으면 Toast메시지를 띄우고 아무것도 하지 않고 텍스트가 있으면 검색을 수행하도록 하였습니다.
추가로 searchView에서는 submit버튼을 클릭했을 경우만 검색을 하도록 하였습니다. 다음 글에서 onQueryTextChange()와 함께 사용하는 글을 올리도록 하겠습니다.
Coroutines (스코프 설정)
크롤링 시 UI처리를 하는 메인 스레드를 사용하다가 ANR(Application Not Responding)이 발생할 수 있음으로 RXJava나 Coroutines을 사용해야 합니다. 여기서는 단순히 크롤링 후 성공 시 텍스트만 보여줄 예정이라 간단히 Dispatchers.Main으로 메인 스코프 안에서 크롤링하는 함수는 IO 스코프를 사용하도록 하였습니다. 크롤링하는 함수가 모두 끝나면 크롤링 정보를 textView에 표시하기 위하여 크롤링 함수에 suspend키워드를 달아주었습니다.
(사실 Coroutines에 관한 글을 먼저 쓰고 진행할까 했는데 저도 아직 공부 중이라 이후에 정리해서 올려드리겠습니다.😅)
전체 코드를 보시면 아시겠지만 제가 코루틴 관련해서 로그를 남겨놨는데 액티비티는 그대로 실행되고 Main 스코프에서 IO 스코프인 getIconInformation() 함수 작업이 모두 끝나면 차례로 끝나는 것을 보실 수 있습니다.
getIconInformation (크롤링 함수)
드디어 크롤링하는 부분입니다. 😭
위에서 미리 크롤링하고자 하는 부분을 정했다면 이제 Jsoup을 통해서 바로 크롤링할 수 있습니다.
저 같은 경우는 url에서 검색할 부분을 %s 페이지를 %d로 적어두고 String.format으로 page와 searchText를 넣어주었습니다.
[실행화면]
다음 포스팅에서는 불러온 src를 기준으로 Glide라이브러리를 활용하여 recyclerView에 보여주도록 추가해 볼 겁니다.
아직 테스트 단계라서 로딩이나 불러오는 페이지를 무조건 1로 설정을 하였는데 이러한 부분도 적용해서 올려보도록 하겠습니다. 🔥🔥🔥
[코드]
전체 코드는 제 github jsoup_crawling branch에서 clone 하시면 됩니다. 🧑🏻💻
IconInformation.kt
data class IconInformation(
val name:String,
val src:String,
val siteUrl:String
){
override fun toString(): String {
return "ICON : {\n" +
"\t$name\n" +
"\t$src\n" +
"\t$siteUrl}\n\n"
}
}
MainActivity.kt
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.appcompat.widget.SearchView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.ddns.yline.imagesearch.databinding.ActivityMainBinding
import net.ddns.yline.imagesearch.item.IconInformation
import org.jsoup.HttpStatusException
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.select.Elements
class MainActivity : AppCompatActivity() {
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
private val iconList = hashMapOf<String, IconInformation>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
setListener()
}
private fun setListener(){
binding.apply {
searchIcon.also {
it.isSubmitButtonEnabled = true
it.setOnQueryTextListener(OnQueryTextListener())
}
}
}
// 이미지를 가져오고 textview에표시하는 함수
private fun getItemList(searchText: String){
// 메인 코루틴 스코프
CoroutineScope(Dispatchers.Main).launch {
Log.d("coroutine Dispatchers.Main start", "test")
getImageInformation(iconList, searchText, 1)
Log.d("coroutine Dispatchers.Main end", "test")
Toast.makeText(applicationContext, iconList.size.toString(), Toast.LENGTH_SHORT).show()
// 크롤링한 정보 표시
binding.textviewTest.text = iconList.toString()
}
}
inner class OnQueryTextListener:SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
Log.d("coroutine activity start", "test")
// 이미지를 가져오고 textview에표시
if(query == null){
Toast.makeText(applicationContext, "검색한 단어가 없습니다.", Toast.LENGTH_SHORT).show()
return false
}else{
// 임시로 크롤링한 데이터 매번 지우기
iconList.clear()
getItemList(query)
}
Log.d("coroutine activity end", "test")
return true
}
// 지금은 사용 안함
override fun onQueryTextChange(newText: String?): Boolean {
return false
}
}
}
// 이미지 크롤링 하는 함수
suspend fun getImageInformation(iconList:HashMap<String, IconInformation>, searchText:String, page:Int):Boolean = withContext(Dispatchers.IO){
Log.d("coroutine Dispatchers.IO start", "test")
// 페이지 끝 여부
var isEnd = false
try {
// https://www.flaticon.com/search/%d?word=%s&license=selection&style=all&order_by=4&type=icon
// 주어진 검색어와 페이지를 검색
val jsoup = Jsoup.connect(String.format(BuildConfig.URL_ICON_SITE, page, searchText))
val doc:Document = jsoup.get()
// 크롤링 하고자 하는 엘리먼트들을 저장
val elements:Elements = doc
.select("section.search-result.icons-search-result")
.select("ul.icons")
.select("div.icon--holder")
.select("a.view.link-icon-detail")
//만일 해당 엘리먼트가 없으면 페이지에 마지막에 도달한 것으로 간주
if(elements.size == 0) isEnd = true
else{
for(element in elements){
element.run {
iconList[attr("data-id")] = IconInformation(
attr("title"),
attr("href"),
select("img").attr("src")
)
}
}
}
}
// status오류
catch (httpStatusException : HttpStatusException){
isEnd = true
Log.e("getImageInformation", httpStatusException.message.toString())
httpStatusException.printStackTrace()
}
// 기타 오류
catch (exception:Exception){
isEnd = true
Log.e("getImageInformation", exception.message.toString())
exception.printStackTrace()
}
Log.d("coroutine Dispatchers.IO end", "test")
isEnd
}
제가 잘못 알고 있거나 잘못된 부분이 있을 경우 알려주시고 추가로 궁금한 점 있으신 분들도 댓글이나 메일 주시면 성실히 답변해 드리겠습니다.🧑🏻💻
감사합니다~😄
'Android' 카테고리의 다른 글
[ 안드로이드 - Kotlin ] retrofit을 사용한 공공데이터 포털 데이터 받기 ( 코틀린 ) (1) | 2021.09.16 |
---|---|
[ 안드로이드 - Kotlin ] API KEY 관리 (0) | 2021.09.12 |