■ はじめに
https://dk521123.hatenablog.com/entry/2020/07/20/232009
の続き。 Android で 英単語帳を作っているのだが 以下のサイトなどにあるように
https://atsueigo.com/vocabulary_google_image/
https://zinsoku.com/english-google-images/
英単語の暗記を一助のために、英単語から画像検索(※)を行いたい。 ※ 今回は、Googleの画像検索を使う。ただ、Yahooでも別の検索でも基本同じ 比較的すぐに実装できるかなっと思ったが、意外とはまって また、以下の「画像検索機能により学べた事項」のように 色々と学べることが多かった。
画像検索機能により学べた事項
1)Google画像検索の仕様・作り(著作権Freeなど) 2) 画像検索のHTML取得・解析処理(ちょっとしたスクレイピング)... Jsopライブラリ 3)2)を行う際の非同期処理 ... Corutinesライブラリ 4)非同期時のグルグル表示 ... ProgressBar 5)画像URLから画像を表示する際の処理 ... Picassoライブラリ etc...
目次
【1】画像検索機能 1)Google画像検索の著作権について 2)対象画像の取得方法について 【2】使用ライブラリ 【3】サンプル
【1】画像検索機能
画像検索機能には、以下を参考(ベース)に考えたが
https://qiita.com/ChanJun/items/e1b5a16a19b0c681ff90
以下の点で注意が必要 1)画像の著作権について 2)対象画像の取得方法について
1)Google画像検索の著作権について
で、クリエイティブ・コモンズ(Creative Commons、略称: CC)ライセンス っていう著作者が自ら著作物の再利用を許可する設定ができるらしいので、 やってみたら、以下のようなフォーマットになった。
Google画像検索のURLフォーマット
// ${keyword} = キーワード、hl=ja : 日本語サイト val url = "https://www.google.com/search?as_st=y&tbm=isch&hl=ja&as_q=${keyword}" + "&as_epq=&as_oq=&as_eq=&imgsz=&imgar=&imgc=&imgcolor=&imgtype=&cr=&" + "as_sitesearch=&safe=images&as_filetype=&tbs=sur%3Acl"
2)対象画像の取得方法について
https://qiita.com/ChanJun/items/e1b5a16a19b0c681ff90
でやっている正規表現のまま、画像のURLを取得すると 以下の2点で意図したデータを取得できなかったので そのデータは事前に省く必要がある。 ~~~~~~~~~ A)一発目は、Google検索自体の画像 B)http/https以外の画像データ ~~~~~~~~~ ※ 言葉だけではイメージ付きづらいと思うので、 PCのブラウザで画像検索をし、HTMLを表示し、 「<img 」で検索してみると理解しやすいかも。
A)一発目は、Google検索自体の画像
* Google検索のロゴが表示されるので、 そこは欲しい画像ではないので省く ~~~~~~~~~ <img src="https://www.google.com/logos/... ~~~~~~~~~
B)http/https以外の画像データ
* img src="data:image/gif;..." のように画像URLではないデータが 含まれるので、これも除外する ~~~~~~~~~ <img src="data:image/gif;base64,R0lG ..." ~~~~~~~~~
【2】使用ライブラリ
1)Google画像検索からの画像URL取得するためのライブラリ A)JSOUP ... Java製の HTML 解析・編集ライブラリ B)Coroutine(コルーチン) ... Kotlin 標準の非同期用ライブラリ ⇒ B)を使わないと、A)内でエラーにあるので使用。 2)画像URLから画像を表示する際のライブラリ C)Picasso(ピカソ) ... Android用画像ライブラリ
https://androidpedia.net/ja/tutorial/2172/---
# この辺のライブラリは、機会があれば別途掘り下げたい
【3】サンプル
* 以下の Github にアップしてあるので、そちらを参照。 => ここでは、一部をコメント付きで、紹介。
https://github.com/dk521123/EnglishCards
https://github.com/dk521123/EnglishCards/tree/develop/app/src/main/java/com/dk/englishcards/edit
build.gradle
dependencies { // JSOUP implementation 'org.jsoup:jsoup:1.13.1' // Coroutine(コルーチン) def coroutines_version = '1.3.9' implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" // Picasso(ピカソ) implementation 'com.squareup.picasso:picasso:2.71828' // Recyclerview implementation 'androidx.recyclerview:recyclerview:1.1.0' }
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.dk.englishcards" > <!-- 追加(インターネット接続するために必要) --> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <application・・・略・・・ </manifest>
ShowImagesActivity.kt
https://github.com/dk521123/EnglishCards/blob/develop/app/src/main/java/com/dk/englishcards/edit/ShowImagesActivity.kt
import android.content.Context import android.os.Bundle import android.os.Handler import android.os.Looper import android.widget.ProgressBar import android.widget.Toast import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.dk.englishcards.R import com.dk.englishcards.cards.EnglishCard import com.dk.englishcards.commons.BaseSubPageActivity import kotlinx.android.synthetic.main.activity_show_images.* import kotlinx.coroutines.* import org.jsoup.Jsoup import java.util.regex.Pattern class ShowImagesActivity : BaseSubPageActivity() { private lateinit var englishWord: String private val mainHandler = Handler(Looper.getMainLooper()) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_show_images) this.englishWord = intent.getStringExtra(EnglishCard.ENGLISH_FIELD) targetEnglishWordTextView.text = this.englishWord this.showImageList(false) doParallelTaskAsync(this, this.englishWord) } private fun showImageList(isShown: Boolean) { if (isShown) { imagesProgressbar.visibility = ProgressBar.INVISIBLE imagesRecyclerView.visibility = RecyclerView.VISIBLE } else { imagesProgressbar.visibility = ProgressBar.VISIBLE imagesRecyclerView.visibility = RecyclerView.INVISIBLE } } private fun doParallelTaskAsync(context: Context, englishWord: String) = GlobalScope.async { val task = RetrieveImagesTask() val imageUrlList = task.getImageUrls(englishWord) if (imageUrlList.isEmpty()) { Toast.makeText( applicationContext, "Failed to get images...", Toast.LENGTH_LONG ).show() } else { try { val imagesGridAdapter = ImageListRecyclerViewAdapter(imageUrlList) mainHandler.post(Runnable { showImageList(true) imagesRecyclerView.layoutManager = LinearLayoutManager(context) imagesRecyclerView.adapter = imagesGridAdapter imagesRecyclerView.setHasFixedSize(true) }) } catch (ex: java.lang.Exception) { ex.printStackTrace() Toast.makeText( applicationContext, "Failed to set images adapter...", Toast.LENGTH_LONG ).show() } } } inner class RetrieveImagesTask { // httpが記載されてある画像のみを取得するための定数 private val TOKEN_OF_HTTP_PROTOCAL = "http" fun getImageUrls(keyword: String): List<String> { val url = "https://www.google.com/search?as_st=y&tbm=isch&hl=ja&as_q=${keyword}" + "&as_epq=&as_oq=&as_eq=&imgsz=&imgar=&imgc=&imgcolor=&imgtype=&cr=&" + "as_sitesearch=&safe=images&as_filetype=&tbs=sur%3Acl" return try { val document = Jsoup.connect(url).get() val pattern = Pattern.compile( "<img.+?src=\"${TOKEN_OF_HTTP_PROTOCAL}(.+?)\".+?>") val targetHtml = document.html() val matcher = pattern.matcher(targetHtml) var imageUrlList = mutableListOf<String>() var index = 0 while (matcher.find()){ val imageUrl = TOKEN_OF_HTTP_PROTOCAL + matcher.group(1) println("Image URL[$index] : $imageUrl") index++ if (index == 1) { // Google画像検索のロゴ画像は除外する continue } imageUrlList.add(imageUrl) } imageUrlList } catch (ex: Exception) { ex.printStackTrace() emptyList() } } } }
ImageListRecyclerViewAdapter.kt
https://github.com/dk521123/EnglishCards/blob/develop/app/src/main/java/com/dk/englishcards/edit/ImageListRecyclerViewAdapter.kt
import android.view.* import android.widget.ImageView import androidx.recyclerview.widget.RecyclerView import com.dk.englishcards.R import com.squareup.picasso.Picasso import kotlinx.android.synthetic.main.image_list.view.* class ImageListRecyclerViewAdapter(private val imageUrlList: List<String>) : RecyclerView.Adapter<ImageListRecyclerViewAdapter.ImageListViewHolder>() { class ImageListViewHolder(val view: View) : RecyclerView.ViewHolder(view) { val image: ImageView = view.imageListImageView } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageListViewHolder { val layoutInflater = LayoutInflater.from(parent.context) val item = layoutInflater.inflate(R.layout.image_list, parent, false) return ImageListViewHolder(item) } override fun onBindViewHolder(holder: ImageListViewHolder, position: Int) { val url = this.imageUrlList[position] Picasso.get() .load(url) .resize(300, 300) .centerCrop() .into(holder.view.imageListImageView) } override fun getItemCount(): Int { return this.imageUrlList.size } }
activity_show_images.xml
https://github.com/dk521123/EnglishCards/blob/develop/app/src/main/res/layout/activity_show_images.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".edit.ShowImagesActivity"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:ignore="MissingConstraints"> <TextView android:id="@+id/targetEnglishWordTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:text="Target" android:textSize="36sp" /> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="vertical" tools:ignore="MissingConstraints"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/imagesRecyclerView" android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="vertical" /> </LinearLayout> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="horizontal" tools:ignore="MissingConstraints"> <ProgressBar android:id="@+id/imagesProgressbar" android:layout_height="wrap_content" android:layout_width="wrap_content" style="?android:attr/progressBarStyleLarge" tools:ignore="MissingConstraints" /> </LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout>
image_list.xml
lhttps://github.com/dk521123/EnglishCards/blob/develop/app/src/main/res/layout/image_list.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.cardview.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="8dp" card_view:cardElevation="2dp" android:foreground="?android:attr/selectableItemBackground"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_vertical" android:orientation="horizontal"> <ImageView android:id="@+id/imageListImageView" android:layout_width="350dp" android:layout_height="300dp" android:layout_margin="8dp" /> </LinearLayout> </androidx.cardview.widget.CardView>
関連記事
Kotlin / Realm で英単語帳を作る
https://dk521123.hatenablog.com/entry/2020/07/20/232009
画面コンポーネント / ImageView
https://dk521123.hatenablog.com/entry/2020/09/20/000000
画面コンポーネント / RecyclerView ~ 入門編 ~
https://dk521123.hatenablog.com/entry/2020/07/21/000000
【Python】スクレイピング ~ Beautiful Soup 4 ~
https://dk521123.hatenablog.com/entry/2020/06/01/000000