🛠️

バスどこサイトをスクレイピング&読み上げスマホアプリ作成

11 min read

車でバス停に送る

車で家の人を周回バスのバス停まで送っていきます。
バス停で降ろしてさよならです。
周回バスは、GPS付でバスどこサイトで位置情報の一覧を見ることができます。
周回バスに乗り遅れると次のバスはもう来ないのでバスどこサイトを逐一チェックしながらバス停まで送っていきたいと考えていましたが、ブゥブゥー。
車を運転しながらスマホ操作はできませヌ。

考える&考える

ならば、バスどこサイトをスマホアプリで定周期でスクレイピングして位置情報の一覧を取得して合成音声で読み上げれば良い。
言うは易し。
スクレイピングは、トラブル続きの某銀行システムでスマホアプリのバックエンドでやったことがあるのでボヤっと理解している。
合成音声読み上げはスマホアプリで作ったことがあるのでOK牧場。
あぁ、できるじゃない。

プログラミング

Androidアプリ は、Javaで作成(Android Onlyです)。
スクレイピングは、Jsoup で実装。
合成音声は、TextToSpeech で実装。
プログラムは、MainActivity.java にダラダラと書く(自分用アプリなのでw)。
スプレイピングする部分は、キーになるタグに注意して作成。

// MainActivity.java
package dnaltd.co.jp.buslocate;

import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.media.AudioManager;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Message;
import android.speech.tts.TextToSpeech;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import android.speech.tts.TextToSpeech;
import android.view.Menu;
import android.view.MenuItem;
import android.view.WindowManager;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;

import static org.jsoup.Jsoup.connect;

public class MainActivity extends AppCompatActivity implements TextToSpeech.OnInitListener {
    // 20秒毎にスクレイピングして読み上げ
    private static final long DELAY_MILLIS = 20000;
    // TextToSpeech使用
    private TextToSpeech tts;

    private TextView textView;
    private String urlStr = "";//http://basdoco.net/m/bus/?u=123456";

    private AudioManager am;
    private SeekBar notifyVolSeekBar;
    private TextView notifyVolText;

    private List<String> dispList = new ArrayList<String>();
    protected Document doc = null;

    @Override
    public void onInit(int status) {
        if (status == TextToSpeech.SUCCESS) {
            // 言語をUSに設定
            Locale locale = Locale.US;
            if (tts.isLanguageAvailable(locale) >= TextToSpeech.LANG_AVAILABLE) {
                tts.setLanguage(locale);
            } else {
                Log.e("TTS", "Not support locale.");
            }
        } else {
            Log.e("TTS", "Init error.");
        }
    }

    @Override
    protected void onDestroy(){
        super.onDestroy();
        if (tts != null) {
            // リソースを解放
            tts.shutdown();
        }
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        //画面がスリープ状態にしない設定
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

        //プリファレンスの取得
        SharedPreferences pref = getSharedPreferences( "url_id_data", Context.MODE_PRIVATE );
        // "url_id" というキーで保存されている値を読み出す
        urlStr = pref.getString( "url_id", "" );

        textView = (TextView) findViewById(R.id.text_view);
        textView.setText("START");

        //ボリューム
        am = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
        int ringVolume = am.getStreamVolume(AudioManager.STREAM_MUSIC);
        //着信音量シークバー
        notifyVolSeekBar = (SeekBar)findViewById(R.id.ringVolSeekBar);
        //最大音量値をプログレス最大値に設定
        notifyVolSeekBar.setMax(am.getStreamMaxVolume(AudioManager.STREAM_MUSIC));
        //現在音量値をプログレス値に設定
        notifyVolSeekBar.setProgress(am.getStreamVolume(AudioManager.STREAM_MUSIC));
        //プログレス値を表示
        notifyVolText = (TextView)findViewById(R.id.ringVolText);
        notifyVolText.setText("音量:"+notifyVolSeekBar.getProgress());

        notifyVolSeekBar.setOnSeekBarChangeListener(
            new SeekBar.OnSeekBarChangeListener() {
                public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                    //TextViewに設定値を表示
                    notifyVolText.setText("音量:"+progress);
                    //着信音量設定
                    am.setStreamVolume(AudioManager.STREAM_MUSIC, progress, 0);
                }

                public void onStartTrackingTouch(SeekBar seekBar) {
                }
                public void onStopTrackingTouch(SeekBar seekBar) {
                }
            }
        );

        myHandler.sendEmptyMessageDelayed(0, DELAY_MILLIS);

        // TTSのインスタンス生成
        tts = new TextToSpeech(this, this);

        taskExe();
    }

    /**
     * タイマーハンドラー
     */
    private Handler myHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            taskExe();
            sendEmptyMessageDelayed(0, DELAY_MILLIS);
        }
    };

    private void taskExe(){

        AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>(){
            String html;

            @Override
            protected Void doInBackground(Void... params) {
                try {
                    if(!"".equals(urlStr)){
                        doc = Jsoup.connect(urlStr).get();
                    }
                }catch(IOException e){
                    e.printStackTrace();
                    return null;
                }
                return null;
            }

            @Override
            protected void onPostExecute(Void result) {

                // 言語をJAPANESEに設定
                Locale locale = Locale.US;
                if(tts.isLanguageAvailable(locale) >= TextToSpeech.LANG_AVAILABLE) {
                    tts.setLanguage(locale);
                }
                locale = Locale.JAPANESE;
                if(tts.isLanguageAvailable(locale) >= TextToSpeech.LANG_AVAILABLE) {
                    tts.setLanguage(locale);
                }

                if(tts.isSpeaking()) {
                    // 読上げ中ならストップ
                    tts.stop();
                }

                String text = "";

                if(doc != null){
                    //imageView.setImageBitmap(bmp);
                    Elements elements = doc.getElementsByTag("li");
                    //System.out.println("a1:" + elements.size());
                    for (Element element : elements) {
                        if(element.toString().indexOf("icon_bus_blink.gif") >= 0){
                            //バスブリンクGifありのliタグのHTMLが element.html() で取得できる
                            //System.out.println("バスブリンクありのliタグのHTML:" + element.html());

                            //<li><span>
                            //<img src="/mobile/common/img/sp/icon_bus_blink.gif" width="32" height="20" alt="バス" />
                            //<a href="/mobile/bus/routes/bus_stop/2841983?u=123456" >天神一丁目</a><span class="time">08:14</span>
                            //</span>
                            //</li>

                            Document docSub = Jsoup.parse(element.html());
                            //time classのテキストを取得 時刻を取得
                            Elements elementsTime = docSub.getElementsByClass("time");
                            //System.out.println("時刻:" + time.text());
                            String time = elementsTime.text();

                            //aタグのテキストを取得 位置を取得
                            Elements elementsLocate = docSub.getElementsByTag("a");
                            //System.out.println("位置:" + locate.text());
                            String locate = elementsLocate.text();

                            Pattern patternTime = Pattern.compile("[0-9][0-9]:[0-9][0-9]");

                            //判定するパターンを生成
                            Matcher m = patternTime.matcher(time);
                            if(!m.find()){
                                //時分のフォーマットが正しくない
                                text = "エラー01";

                                dispList.add(0, String.format("エラー01[%s %s]", time, locate));
                                if(dispList.size() > 3){
                                    dispList.remove(3);
                                }
                                //listText = String.format("エラー01[%s %s]", time, locate);
                            }else{
                                text = String.format("%s時%s分 %s",
                                        time.substring(0, 2),
                                        time.substring(3, 5),
                                        locate);
                                dispList.add(0, text);
                                if(dispList.size() > 3){
                                    dispList.remove(3);
                                }
                            }
                        }
                    }
                }
                if("".equals(text)){
                    text = "現在バスは走っていません";
                    // TODO: テスト用 //text = urlStr;
                    dispList.add(0, text);
                    if(dispList.size() > 3){
                        dispList.remove(3);
                    }
                }

                String disp = String.format("%s\n%s\n%s",
                        dispList.size() >= 1 ? dispList.get(0): "",
                        dispList.size() >= 2 ? dispList.get(1): "",
                        dispList.size() >= 3 ? dispList.get(2): "");

                tts.speak(text, TextToSpeech.QUEUE_ADD, null);
                textView.setText(disp);
            }
        };
        task.execute();
    }

    //2018.05追加分 設定画面を追加
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    //2018.05追加分 設定画面を追加
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.optionsMenu_01:
                Intent intent1 = new android.content.Intent(MainActivity.this, SimplePreferenceActivity.class);
                startActivity(intent1);
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }
}

表示画面

実際に動いていたときの画面のスクショがありませんでしたが、
こんな感じで動作していました。

Discussion

ログインするとコメントできます