4
5

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 1 year has passed since last update.

Android強化月間 - Androidアプリ開発の知見を共有しよう -

Jetpack Composeでスクレイピングしてみた

Last updated at Posted at 2023-10-07

はじめに

どうも、フリーランスエンジニアをしているモブ太郎です。
このの記事では、Jetpack Composeで「Jsoup」というライブラリを使用して、スクレイピングをしたいと思います。
細かい説明は省いていますので、参考程度、こういったものがあるんだな程度に見ていってください。

完成形

image.png

Jetpack Composeとは

Jetpack Composeは、ネイティブUIをビルドする際に推奨されるAndroidの最新ツールキットのことです。
Jetpack Composeを導入する理由は以下になります。

  • コードを削減
    少ないコードで多くの機能が可能になり、あらゆる種類のバグが避けられるので、コードがシンプルで維持しやすくなります。
  • 直観的
    UIを記述するだけで、残りの部分はComposeで処理されます。
    アプリの状態が変更されると、UIが自動的に更新されます。
  • 開発を加速させる
    あらゆる既存のコードと互換性があり、必要に応じて利用できます。
    ライブプレビューとAndroid Studioのフルサポートにより、反復処理を高速化できます。
  • パワフル
    AndroidプラットフォームAPIに直接アクセスし、マテリアルデザイン、ダークテーマ、アニメーションなどを組み込んだ美しいアプリを作成できます。

参照

Jsoupとは

URLを指定してhtmlの情報を取得し、jQueryっぽく要素にアクセスできるライブラリです。

実装

プロジェクト作成

  1. Android Studioを起動して「New Project」をクリック。
    image.png
  2. 「Empty Compose Activity」を選択し、「Next」をクリック。
    image.png
  3. Nameを設定し、「Finish」をクリック。
    image.png
  4. プロジェクト作成完了!
    image.png

必要なライブラリの追加

build.gradle(:app)に以下ライブラリを追加

// Jsoup スクレイピング用
implementation 'org.jsoup:jsoup:1.16.1'
// Coil 画像読み込み用
implementation 'io.coil-kt:coil-compose:2.4.0'

上記を追加したら一旦ビルドします。
SDKのバージョンがうんぬんかんぬんなどのエラーがでると思います。
私の場合は、以下の内容で修正してエラーを解決しました。

build.gradle(:app)
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

android {
    namespace 'com.example.kamenriderscrapingapp'
    compileSdk 33 // 32 -> 33

    defaultConfig {
        applicationId "com.example.kamenriderscrapingapp"
        minSdk 28
        targetSdk 33 // 32 -> 33
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        vectorDrawables {
            useSupportLibrary true
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures {
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion '1.4.2' // 1.1.1 -> 1.4.2
    }
    packagingOptions {
        resources {
            excludes += '/META-INF/{AL2.0,LGPL2.1}'
        }
    }
}

dependencies {

    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
    implementation 'androidx.activity:activity-compose:1.3.1'
    implementation "androidx.compose.ui:ui:$compose_ui_version"
    implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
    implementation 'androidx.compose.material:material:1.1.1'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
    androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
    debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
    debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"

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

    // Coil
    implementation 'io.coil-kt:coil-compose:2.4.0'
}
build.gradle(KamenRiderScrapingApp)
buildscript {
    ext {
        compose_ui_version = '1.3.3' // 1.1.1 -> 1.3.3
    }
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
    id 'com.android.application' version '7.3.1' apply false
    id 'com.android.library' version '7.3.1' apply false
    id 'org.jetbrains.kotlin.android' version '1.8.10' apply false // 1.6.10 -> 1.8.10
}

インターネットアクセスへの許可

インターネットに接続するためには以下の内容を追記しないと接続できません。

<uses-permission android:name="android.permission.INTERNET" />
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!-- インターネットアクセスへの許可 -->
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.KamenRiderScrapingApp"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.KamenRiderScrapingApp">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <meta-data
                android:name="android.app.lib_name"
                android:value="" />
        </activity>
    </application>

</manifest>

仮面ライダーデータクラス

KamenRider.kt
data class KamenRider(
    val imageUrl: String,
    val title: String,
    val era: String,
)

仮面ライダーの情報を保持するためのデータクラスです。

仮面ライダー一覧画面

KamenRiderScreen.kt
@Composable
fun KamenRiderScreen() {
    var kamenRiderList by remember { mutableStateOf(emptyList<KamenRider>()) }
    thread {
        kamenRiderList = kamenRiderScraping()
    }
    Scaffold { paddingValue ->
        LazyColumn(modifier = Modifier.padding(paddingValue)) {
            items(kamenRiderList.sortedBy { it.era }) { item ->
                KamenRiderInfoItem(item)
            }
        }
    }
}

@Composable
fun KamenRiderInfoItem(item: KamenRider) {
    Box(
        modifier = Modifier
            .background(Color.White)
            .border(
                width = 1.dp,
                color = Color.DarkGray,
            ),
        contentAlignment = Alignment.TopCenter,
    ) {
        Row(
            modifier = Modifier.fillMaxWidth()
        ) {
            // 画像表示
            AsyncImage(
                model = item.imageUrl,
                contentDescription = null,
                contentScale = ContentScale.Crop,
                modifier = Modifier
                    .width(100.dp)
                    .height(150.dp)
                    .padding(5.dp)
            )

            Column(modifier = Modifier.fillMaxWidth()) {
                // 年代
                Text(
                    text = item.era,
                    maxLines = 3,
                    fontWeight = FontWeight.Bold,
                    color = Color.Black,
                    fontSize = 20.sp,
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(5.dp),
                )
                // 作品名
                Text(
                    text = item.title,
                    maxLines = 3,
                    fontWeight = FontWeight.Bold,
                    color = Color.Black,
                    fontSize = 15.sp,
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(5.dp),
                )
            }
        }
    }
}

private fun kamenRiderScraping(): ArrayList<KamenRider> {
    /* 仮面ライダーポータルサイトをスクレイピングするための関数 */

    val targetUrl = "https://www.kamen-rider-official.com/series/"

    val kamenRiderList: ArrayList<KamenRider> = arrayListOf()
    try {
        if (!isConnect(targetUrl)) {
            Log.e("Error", "正常に接続できませんでした。")
            return arrayListOf()
        }

        val doc: Document = Jsoup.connect(targetUrl).get()
        val elements: Elements = doc.select(".styles_era_order_color__D0ePc")
        elements.forEach { element ->
            element.select(".styles_flex_content__Bygaa").forEach { item ->
                kamenRiderList.add(
                    KamenRider(
                        imageUrl = "https://www.kamen-rider-official.com" + item.select(".styles_series__Card__Img__85aon").attr("src"),
                        era = item.select(".styles_series__Card__Year__n5X_o").text(),
                        title = item.select(".styles_series__Card__Name__fg02u").text()
                    )
                )
            }
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }
    return kamenRiderList
}

private fun isConnect(targetUrl: String): Boolean {
    /* 接続可能なURLかチェックするための関数 */

    val url = URL(targetUrl)
    val connection = url.openConnection() as HttpURLConnection
    // 接続チェック
    connection.requestMethod = "GET"
    connection.instanceFollowRedirects = false
    connection.connect()
    val statusCode: Int = connection.responseCode
    connection.disconnect()
    return statusCode == HttpURLConnection.HTTP_OK
}

スクレイピングを行い、一覧を表示するためのクラスです。

メインクラス

MainActivity.kt
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            KamenRiderScrapingAppTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    // 仮面ライダー一覧画面
                    KamenRiderScreen()
                }
            }
        }
    }
}

さいごに

もともとスクレイピングに興味があったのですが、Pythonでスクレイピングするよりも今自分がハマっているAndroidでスクレイピングがしたい!
という思いで「Jsoup」なるものを見つけ、今回の記事を書きました。

他にもAndroidでpythonを動かすことができるライブラリもあるみたいなので、それも記事にできたらなと思います。

ここまで見てくだっさてありがとうございます。mm

4
5
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
4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?