Kotlin

BlogViewer (feat. JSoup Crawling)

kakaroo 2022. 4. 3. 13:39
반응형

article logo

 

Tistroy 에 게시된 글을 crawling 을 통해 cardview 형태로 만들어보겠습니다.

 

전체적인 UI는 아래 게시글을 재사용하겠습니다.

https://kakaroo.tistory.com/57

 

NewsFeed - JSoup / Nested RecyclerView

관심있는 뉴스 정보를 매시간마다 서버로 모아 퇴근시에 한번에 보는 어플리케이션을 만들어 보려고 합니다. 서버의 필요성은 현재 찾지 못해서, 현재 시간대의 기사를 검색할 때마다 보여주는

kakaroo.tistory.com

 

Tistory 게시글의 소스는 아래와 같습니다.

 

Viewer에 구성될 요소들은 카테고리, 카테고리 URL, 게시글 제목, 게시글 날짜, 게시글 URL, 게시글 본문 입니다.

티스토리의 게시글들을 PageNo 또는 검색어로 검색하거나, 전체 글을 카테고리 별로 분류해서 게시하려 합니다.

 

 

<Category recycler view 구성>

<LinearLayout
    android:orientation="vertical"
    android:background="#FFFFFF"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginBottom="16dp">

    <LinearLayout
        android:orientation="horizontal"
        android:background="#FFFFFF"
        android:layout_weight="1"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/tv_category"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="4"
            android:layout_margin="4dp"
            android:textSize="18sp"
            android:textStyle="bold"
            android:textColor="#000000" />

        <TextView
            android:id="@+id/tv_topicNum"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="0.1"
            android:layout_marginTop="10dp"
            android:layout_marginRight="12dp"
            android:textColor="#000000"
            android:textSize="12sp" />
    </LinearLayout>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_articles"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

 

<Category class>

data class Category(var name: String, var articles: ArrayList<Article>)

 

 

<Article view>

<androidx.cardview.widget.CardView
    android:id="@+id/card_view"
    android:layout_margin="4dp"
    android:layout_gravity="center"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    card_view:cardCornerRadius="4dp">

        <LinearLayout
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_margin="2dp">

        <LinearLayout
            android:orientation="horizontal"
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:layout_margin="2dp">

                <ImageView
                    android:id="@+id/iv_thumb"
                    android:layout_width="70dp"
                    android:layout_height="60dp"
                    android:src="@drawable/news_thumb_jpg"/>

                <LinearLayout
                    android:orientation="vertical"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:layout_margin="2dp">

                <TextView
                    android:id="@+id/tv_title"
                    android:layout_width="40dp"
                    android:layout_height="40dp"
                    android:layout_marginTop="0dp"
                    android:layout_marginLeft="2dp"
                    android:layout_marginRight="2dp"
                    android:layout_marginBottom="0dp"
                    android:ellipsize="marquee"
                    android:textStyle="bold"
                    android:lines="3"
                    android:layout_gravity="right"
                    android:text="Kotlin? Java?"
                    android:textSize="10dp"/>

                <TextView
                    android:id="@+id/tv_date"
                    android:layout_width="42dp"
                    android:layout_height="16dp"
                    android:layout_marginTop="2dp"
                    android:layout_marginLeft="8dp"
                    android:layout_marginRight="0dp"
                    android:layout_marginBottom="4dp"
                    android:ellipsize="marquee"
                    android:singleLine="true"
                    android:layout_gravity="right"
                    android:text="2022.04.01"
                    android:textSize="8dp"/>

                </LinearLayout>

        </LinearLayout>

                <TextView
                    android:id="@+id/tv_summary"
                    android:layout_width="110dp"
                    android:layout_height="60dp"
                    android:layout_marginLeft="4dp"
                    android:layout_marginTop="4dp"
                    android:layout_marginRight="8dp"
                    android:layout_marginBottom="2dp"
                    android:ellipsize="end"
                    android:maxLines="4"
                    android:text="인라인(inline) 키워드는 자바에서는 제공하지 않는 코틀린만의 키워드입니다. 이러한 인라인 키워드를 이용하여 함수를 만들고 이를 잘 활용한다면"
                    android:textSize="12dp" />

        </LinearLayout>

</androidx.cardview.widget.CardView>>

 

<Article data class>

data class Article(val idx: Int, val categoryName: String, var title: String, var date: String,
                   val categoryUrl: String, val articleUrl: String, val imgUrl: String, var summary: String)

 

각각의 포스팅은 아래와 같이 구성이 됩니다.

<article class="article-type-common article-type-thumbnail">
	<a href="/69" class="link-article">
		<p class="thumbnail"  has-thumbnail="1" style="background-image:url('https://blog.kakaocdn.net/dn/nja5F/btryciPbvtV/z3WgxG3Y4WLTGKqnL1FeRK/img.png')" >
		<img src="https://blog.kakaocdn.net/dn/nja5F/btryciPbvtV/z3WgxG3Y4WLTGKqnL1FeRK/img.png" class="img-thumbnail" role="presentation">
		</p>
	</a>

	<div class="article-content">
		<a href="/69" class="link-article">
			<strong class="title">inline 함수</strong>
			<p class="summary">인라인(inline) 키워드는 자바에서는 제공하지 않는 코틀린만의 키워드입니다. 이러한 인라인 키워드를 이용하여 함수를 만들고 이를 잘 활용한다면 다양한 이득을 얻을 수 있..</p>
		</a>
		<div class="box-meta">
			<a href="/category/Kotlin" class="link-category">Kotlin</a>
			<span class="date">2022.04.01</span>
			<span class="reply">
				<s_rp_count></s_rp_count>
			</span>
		</div>
	</div>
</article>

 

JSoup을 넘겨줄 tag class를 정의합니다.

data class ArticlesTag(var articleTag: String = Common.SELECTOR_ARTICLE_SYNTAX,
                       var categoryTag: String = Common.SELECTOR_CATEGORY_SYNTAX,
                       var articleUrlTag: String = Common.SELECTOR_ARTICLUEURL_SYNTAX,
                       var titleTag: String = Common.SELECTOR_TITLE_SYNTAX,
                       var dateTag: String = Common.SELECTOR_DATE_SYNTAX,
                       var imageTag: String = Common.SELECTOR_IMAGE_SYNTAX,
                       var summaryTag: String = Common.SELECTOR_SUMMARY_SYNTAX) {
    init {
        if(articleTag == "") this.articleTag = Common.SELECTOR_ARTICLE_SYNTAX
        if(categoryTag == "") this.categoryTag = Common.SELECTOR_CATEGORY_SYNTAX
        if(articleUrlTag == "") this.articleUrlTag = Common.SELECTOR_ARTICLUEURL_SYNTAX
        if(titleTag == "") this.titleTag = Common.SELECTOR_TITLE_SYNTAX
        if(dateTag == "") this.dateTag = Common.SELECTOR_DATE_SYNTAX
        if(imageTag == "") this.imageTag = Common.SELECTOR_IMAGE_SYNTAX
        if(summaryTag == "") this.summaryTag = Common.SELECTOR_SUMMARY_SYNTAX
    }
}

 

JSoup에서는 해당 tag를 가지고 parsing을 합니다.

val contentElements: Elements = doc.select(articlesTag.articleTag)
for ((i, elem) in contentElements.withIndex()) {
    val category = elem.select(articlesTag.categoryTag).text()
    val articleUrl = elem.select(articlesTag.articleUrlTag).first().attr(Common.HREF_TAG)
    val title = elem.select(articlesTag.titleTag).text()
    val date = elem.select(articlesTag.dateTag).text()
    val categoryUrl = elem.select(articlesTag.categoryTag).first().attr(Common.HREF_TAG)
    val imageUrl = elem.select(articlesTag.imageTag).attr(Common.SRC_TAG)
    val summary = elem.select(articlesTag.summaryTag).text()

    //mCategorySet.add(category)
    mArticleList.add(Article(i, category, title, date, url+categoryUrl, url+articleUrl, imageUrl, summary))

 

 

viewer 메인화면

 


티스토리 마다 CSS 를 다르게 사용하기 때문에 그에 따라 HTML tag도 다르게 됩니다.

JSoup을 통해 분류할 tag 는 설정으로 사용자가 직접 설정할 수 있게 하겠습니다.

 

<PreferenceScreen 설정>

<PreferenceCategory app:title="@string/category_header"
    app:iconSpaceReserved="false"
    app:key="url_category_key">

    <EditTextPreference
        app:key="url_input_key"
        app:title="@string/blog_url"
        app:useSimpleSummaryProvider="false"
        app:summary=""
        app:iconSpaceReserved="false"/>
</PreferenceCategory>


<PreferenceCategory app:title="@string/category_tag"
    app:iconSpaceReserved="false"
    app:key="tag_category_key">

    <EditTextPreference
        app:key="article_tag_key"
        app:title="@string/article_tag_name"
        app:useSimpleSummaryProvider="false"
        app:summary="article"
        app:iconSpaceReserved="false"/>

    <EditTextPreference
        app:key="category_tag_key"
        app:title="@string/category_tag_name"
        app:useSimpleSummaryProvider="false"
        app:summary="a.link-category"
        app:iconSpaceReserved="false"/>

    <EditTextPreference
        app:key="articleLink_tag_key"
        app:title="@string/link_tag_name"
        app:useSimpleSummaryProvider="false"
        app:summary="a.link-article"
        app:iconSpaceReserved="false"/>

    <EditTextPreference
        app:key="title_tag_key"
        app:title="@string/title_tag_name"
        app:useSimpleSummaryProvider="false"
        app:summary=".title"
        app:iconSpaceReserved="false"/>

    <EditTextPreference
        app:key="date_tag_key"
        app:title="@string/date_tag_name"
        app:useSimpleSummaryProvider="false"
        app:summary=".date"
        app:iconSpaceReserved="false"/>

    <EditTextPreference
        app:key="image_tag_key"
        app:title="@string/image_tag_name"
        app:useSimpleSummaryProvider="false"
        app:summary="img.img-thumbnail"
        app:iconSpaceReserved="false"/>

    <EditTextPreference
        app:key="summary_tag_key"
        app:title="@string/summary_tag_name"
        app:useSimpleSummaryProvider="false"
        app:summary=".summary"
        app:iconSpaceReserved="false"/>

</PreferenceCategory>

<PreferenceCategory app:title="@string/category_page"
    app:iconSpaceReserved="false"
    app:key="page_category_key">

    <EditTextPreference
        app:key="page_max_num_key"
        app:title="@string/article_maxnum"
        app:useSimpleSummaryProvider="false"
        app:summary="50"
        app:iconSpaceReserved="false"/>

</PreferenceCategory>

tag 설정화면

 


HTML tag에 따른 설정방법 도움말도 ImageView 로 하나 만들어 보았습니다.

도움말 이미지

 

해당 이미지를 Dialog에 생성하고, 이미지가 작기 때문에 zoon-in 기능도 넣어보겠습니다.

 

<custom_dialog.xml>

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/customDialog"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tv_hint1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hint1_text" />

    <TextView
        android:id="@+id/tv_hint2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hint2_text" />

    <View
        android:background="@color/black"
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:layout_marginLeft="2dp"
        android:layout_marginRight="2dp"/>

    <com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
        android:id="@+id/imgView_Hint"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:src="@drawable/settings_hint"
        android:layout_marginTop="2dp"
        android:layout_marginBottom="2dp"
        android:layout_marginLeft="2dp"
        android:layout_marginRight="2dp"/>

</LinearLayout>

 

zoon-in 이 가능한 image view를 사용하기 위해 SubsamplingScaleImageView library를 추가해줍니다.

implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'

 

코드에는 아래와 같이 리소스만 적용해주면 간단하게 확대가 가능한 image view를 만들수 있습니다.

//Image zoom in-out
val imageView: SubsamplingScaleImageView = mDialog.findViewById(R.id.imgView_Hint)
imageView.setImage(ImageSource.resource(R.drawable.settings_hint))

 

반응형

'Kotlin' 카테고리의 다른 글

확장함수 (Extension Function)  (0) 2022.04.14
코루틴(Coroutine)  (0) 2022.04.03
inline 함수  (0) 2022.04.01
enum class, sealed class  (0) 2022.03.31
sequence  (0) 2022.03.27