22 Sep 2017

Android 網頁資料擷取

前言

此文章將會對證交所網站進行資料擷取,將所要搜尋的股票票價顯示於 app。網頁資料擷取的步驟為連接網頁、取得網頁內容和網頁解析,這些都是耗時的工作,必需進行異步處理。爬蟲 (crawler) 是網頁資料擷取的延伸,可自動搜尋網路上的相關資料,是搜尋引擎的基本結構,常應用於比價網站等等。

步驟

1.連接網頁:以 URL 和 HttpURLConnection 連接網頁。

2.取得網頁內容:用串流 (Stream) 的概念取得網站內容。

3.網頁內容解析:排除網頁內容中不需要的字串進而取得所要的字串資料。

連接網頁

利用 HttpURLConnection 和 URL 進行網頁連接再設定 http 請求方法 (request methods) 對網頁進行請求。請求行為有 GET、POST、HEAD、OPTIONS、PUT、DELETE 和 TRACE。Http 的最常用的請求方法為 GET 和 POST,兩者差別如下:

  • GET

    1. 透過URL傳送資料。

    2. 傳送資料有長度限制,其長度限制取決於該瀏覽器或伺服器對URL的長度限制。

    3. 瀏覽器會儲存歷史記錄。

    4. 可被加到書籤。

    5. 傳送資料會顯示於URL,應避免以此方法傳送敏感資料。

    6. 速度較快。

  • POST

    1. 資料透過主體 (body) 傳送。

    2. 沒有長度限制。

    3. 瀏覽紀錄不被儲存。

    4. 不可加到書籤。

    5. 安全性較 GET 高。

    6. 數度較慢。

在請求方法的選擇上若無安全性上的考量且傳送資料不大時應選擇 GET,因為 GET 有較快地處理速度。由下圖可發現證交所網站的查詢個股功能是利用 POST 方法,因此本專案決定用 POST 方法。

HttpURLConnection 是一個抽象類別,其基本用法可參考官方網站。下段程式碼為 HttpURLConnection 和 URL 進行網頁連接。finally 區段的程式碼是一定會實行的,其中呼叫 disconnect 方法意味著不需要再對伺服器發起請求,因此斷開連線。

public String postData(String key,String value) {
    String htmlInf="";
    try{
        HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
        try {
            urlConnection.setChunkedStreamingMode(0);//不知道資料長度時呼叫,避免緩存耗盡
            urlConnection.setRequestMethod("POST");//預設是GET 用POST要改
            urlConnection.setDoOutput(true);//default is false,有輸出時須為true
            urlConnection.setDoInput(true);//default is true,有輸入時須為true

            String outputData = keyAndValue(key,value);//key和value字串的串接是要丟到網頁上的資料 ex: "stkNo=2300"
            writeHtmldata(urlConnection,outputData);//將資料丟到網頁
            htmlInf = readHtmlStream(urlConnection);//將所要的資料讀進app並輸出

        } finally {
            urlConnection.disconnect();//斷開連接
        }
    } catch (MalformedURLException e) {
        return "Exception:" + e.getMessage();
    } catch (IOException e) {
        return "Exception:" + e.getMessage();
    }
    return htmlInf;
}

取得網頁內容

取得所要的網頁內容之前須先傳送要查詢的個股代號與關鍵字到網站上,接下來網站會返回網頁內容。傳送、接收和輸出都需要倚靠串流進行處理。串流可想成資料在資料來源端 (Data Source) 與資料目的端 (Data Sink) 之間流動。輸入串流 (InputStream) 是將資料從來源端取出而輸出串流 (Output) 是將資料寫入目的端,資料的單位是位元組。下圖是輸出與輸入串流常用類別的繼承圖。

輸入串流以及輸出串流處理的是位元組資料,若要處理字元資料可使用字元處理類別,字元資料處理類別會自動將位元組與字元進行編碼轉換。Reader 類別可將字元資料取來源端出而 Writer 可將字元資料寫入目的端。下圖是 Reader 類別與 Writer 類別的繼承關係圖。

下段程式碼的功能是要將字元資料傳送到網頁上,因此使用字元處理類別。BufferedWriter 提供 Writer 資料緩衝區,此類別需包裹一個 OutputStreamWriter,之後將資料寫入記憶體的緩衝區,不需寫在硬碟上,因此增加處理速度,非常適合用於輸出資料到網頁上的情形。當資料傳送完畢後要 close,因此將關閉的方法寫在 finally 區段以確定程式碼確實會關閉串流以及 Writer。讀者也可嘗試用自動關閉資源的方法撰寫,此方法也可確保資源的關閉。

private void writeHtmldata(HttpURLConnection urlConnection,String outputData) throws IOException {
    OutputStream os = urlConnection.getOutputStream();
    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));//bufferwriter 處理的資料是字串 自動在字元跟位元組之間作轉換
    try {
        writer.write(outputData);
        writer.flush();
    }finally {
        writer.close();
        os.close();
    }
}

要得到資料需經過輸入以及輸出處理,輸入是將資料由來源端取出而輸出則是將資料寫到目的端。將資料從網路上取出時使用 BufferedInputStream,此類別需包裹一個 InputStream 類別,之後會將資料讀出至緩衝區。ByteArrayOutputStream 也是利用緩衝區的類別,不需要包裹 OutputStream 即可使用。

private String readHtmlStream(HttpURLConnection urlConnection) throws IOException {
    InputStream is = new BufferedInputStream(urlConnection.getInputStream());//讀進來時不需做位元組與字元轉換 不需要用reader
    OutputStream bo = new ByteArrayOutputStream();
    try {
        int i = is.read();
        while (i != -1) {
            bo.write(i);
            i = is.read();
        }
    } finally {
        bo.close();
        is.close();
    }
    return bo.toString();
}

網頁內容解析

下圖中可看出開盤價位於第一個有“開盤競價基準”的 table 標籤內,且在第二個<td>後。此專案基於以上的想法進行網頁分析。

private String getstockdata(String htmlInf) {
    String result="";
    Pattern ptn =Pattern.compile("table");
    Matcher mch = ptn.matcher(htmlInf);
    String subs="";
    int start=0,end=0;
    while(mch.find()){
        start = mch.start();
        subs=htmlInf.substring(end,start);
        if(subs.contains("開盤競價基準")){
            break;
        }
        end = mch.end();
    }
    result = subs.split("<td>")[2].replace("</td>","");
    return result;
}

完整程式碼

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.sarah.testhttpurlconnection.MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="請輸入個股代碼"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.204" />
    <EditText
        android:layout_width="119dp"
        android:layout_height="60dp"
        android:layout_marginLeft="8dp"
        app:layout_constraintLeft_toLeftOf="parent"
        android:layout_marginRight="8dp"
        app:layout_constraintRight_toRightOf="parent"
        android:id="@+id/editText"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_marginTop="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_marginBottom="8dp"
        app:layout_constraintVertical_bias="0.336"
        tools:layout_editor_absoluteY="133dp"
        tools:layout_editor_absoluteX="127dp" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="搜尋開盤競價基準"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_marginBottom="8dp"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_marginTop="8dp"
        app:layout_constraintVertical_bias="0.55"
        android:layout_marginLeft="8dp"
        app:layout_constraintLeft_toLeftOf="parent"
        android:layout_marginRight="8dp"
        app:layout_constraintRight_toRightOf="parent"
        android:onClick="search"/>

    <TextView
        android:id="@+id/textView"
        android:layout_width="99dp"
        android:layout_height="79dp"
        android:text=""
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_marginBottom="8dp"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_marginTop="8dp"
        android:layout_marginRight="8dp"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_marginLeft="8dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintVertical_bias="0.795"
        app:layout_constraintHorizontal_bias="0.501" />

</android.support.constraint.ConstraintLayout>

ConnectWeb.java

package com.example.sarah.testhttpurlconnection;

import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

/**
 * Created by sarah on 2017/9/20.
 */

public class ConnectWeb {
    private URL url;

    public ConnectWeb(URL url){
        this.url=url;
    }

    public String postData(String key,String value) {
        String htmlInf="";
        try{
            HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
            try {
                urlConnection.setChunkedStreamingMode(0);//不知道資料長度時呼叫,避免緩存耗盡
                urlConnection.setRequestMethod("POST");//預設是GET 用POST要改
                urlConnection.setDoOutput(true);//default id false
                urlConnection.setDoInput(true);//default is true

                String outputData = keyAndValue(key,value);
                writeHtmldata(urlConnection,outputData);
                htmlInf = readHtmlStream(urlConnection);

            } finally {
                urlConnection.disconnect();
            }
        } catch (MalformedURLException e) {
            return "Exception:" + e.getMessage();
        } catch (IOException e) {
            return "Exception:" + e.getMessage();
        }
        return htmlInf;
    }

    private String keyAndValue(String key,String value){
        StringBuilder outputDataBuilder = new StringBuilder();
        outputDataBuilder.append(key);
        outputDataBuilder.append("=");
        outputDataBuilder.append(value);
        return outputDataBuilder.toString();
    }

    private void writeHtmldata(HttpURLConnection urlConnection,String outputData) throws IOException {
        OutputStream os = urlConnection.getOutputStream();
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));//bufferwriter 處理的資料是字串 自動在字元跟位元組之間作轉換
        try {
            writer.write(outputData);
            writer.flush();
        }finally {
            writer.close();
            os.close();
        }
    }

    private String readHtmlStream(HttpURLConnection urlConnection) throws IOException {
        InputStream is = new BufferedInputStream(urlConnection.getInputStream());//讀進來時不需做位元組與字元轉換 不需要用reader
        OutputStream bo = new ByteArrayOutputStream();
        try {
            int i = is.read();
            while (i != -1) {
                bo.write(i);
                i = is.read();
            }
        } finally {
            bo.close();
            is.close();
        }
        return bo.toString();
    }
}

MainActivity.java

package com.example.sarah.testhttpurlconnection;

import android.os.AsyncTask;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class MainActivity extends AppCompatActivity {

    private TextView text;
    private EditText edit;
    private Button btn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        text = (TextView) findViewById(R.id.textView);
        edit = (EditText) findViewById(R.id.editText);
        btn = (Button) findViewById(R.id.button);
    }

    public void search(View view) {
        btn.setClickable(false);
        String edtext = edit.getText().toString();
        if (edtext.matches("[0-9]{1}[0-9]{3}"))
            new stackpricedown().execute(edtext);
        else {
            text.setText("請輸入正確股號格式");
            btn.setClickable(false);
        }
    }

    private class stackpricedown extends AsyncTask<String, Void, String> {

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            text.setText("資料下載中");
        }

        @Override
        protected String doInBackground(String... strings) {
            String result="",htmlInf;
            try {
                URL url = new URL("http://www.tse.com.tw/zh/stockSearch/stockSearch");
                ConnectWeb connectStock = new ConnectWeb(url);
                htmlInf = connectStock.postData("stkNo",strings[0]);
                if(htmlInf.contains("<!-- 終止上市 -->")){
                    return "終止上市";
                }
                else if (htmlInf.contains("各檔權證即時行情及基本資料")) {
                    result = getstockdata(htmlInf);23
                }else
                    result="查無資料\n請輸入正確股號";
            }catch(MalformedURLException e) {
                return e.getMessage();
            }
            return result;
        }

        @Override
        protected void onPostExecute(String s) {
            btn.setClickable(true);
            super.onPostExecute(s);
            text.setText(s);
        }
    }

    private String getstockdata(String htmlInf) {
        String result="";
        Pattern ptn =Pattern.compile("table");
        Matcher mch = ptn.matcher(htmlInf);
        String subs="";
        int start=0,end=0;
        while(mch.find()){
            start = mch.start();
            subs=htmlInf.substring(end,start);
            if(subs.contains("開盤競價基準")){
                break;
            }
            end = mch.end();
        }
        result = subs.split("<td>")[2].replace("</td>","");
        return result;
    }
}

Tags:
0 comments