Mobil Programlama

Android

XML Bilgilerini Ayrıştırmak (Parsing)

Lisans: Creative Commons 11.12.2020 tarihinde güncellendi
Bakabileceğiniz Etiketler: Eğitmen: Geleceği Yazanlar Ekibi

XML (Extensible Markup Language - Genişletilebilir İşaretleme Dili), hem insanlar hem de makineler tarafından kolayca okunabilen ve İnternet üzerinde bilgi paylaşmak için çok sık kullanılan bir işaretleme dili formatıdır. Haber siteleri ya da bloglar gibi sıkça güncellenen içeriğe sahip web siteleri genellikle bir XML kaynağı sağlar ve böylece harici yazılımlar buralardaki içerik değişikliklerini bir arada görebilir. XML gönderme ve XML ayrıştırma (parsing) işlemleri, ağ bağlantılı uygulamalarda sıkça yapılan bir iştir. Bu eğitim içeriğinde sizlere XML dosyalarını nasıl ayrıştıracağınızı (parse) ve verilerini nasıl kullanacağınızı göstereceğiz.

 

Bir Ayrıştırıcı (Parser) seçmek

Google, Android için verimliliği ve sürdürülebilirliği açısından XmlPullParser'ı kullanmanızı öneriyor. Tarih sırasına göre Android içinde bu arayüzü kullanmak için iki sınıf tanımlanmıştır:

Buradaki iki seçenek de çalışmak için uygundur. Buradaki örnekte Xml.newPullParser()'dan gelen ExpatPullParser sınıfını kullandık.

 

Haber kaynağını analiz etmek

Bir haber kaynağı ayrıştırmanın ilk adımı, öncelikle onun hangi alanlarına ihtiyacınız olduğunu belirlemek. Ayrıştırıcı (parser), seçtiğiniz alandaki verileri ayrıştırır ve diğerlerini atlar.

Örnek uygulama içindeki bir bölümü incelersek konu daha iyi anlaşılacaktır. Stackoverflow.com'a gönderilen her içerik bir entry etiketi içinde (içinde de birkaç iç içe etiket bulunur) besleme kaynağında yer alır. Aşağıda "android" ile etiketlenmiş en yeni soruları içeren bir haber kaynağı örneğini bulabilirsiniz:

 



<?xml version="1.0" encoding="utf-8"?> 
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:creativeCommons="http://backend.userland.com/creativeCommonsRssModule" ...">     
<title type="text">newest questions tagged android - Stack Overflow</title>
...
    <entry>
    ...
    </entry>
    <entry>
        <id>http://stackoverflow.com/q/9439999</id>
        <re:rank scheme="http://stackoverflow.com">0</re:rank>
        <title type="text">Where is my data file?</title>
        <category scheme="http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest/tags" term="android"/>
        <category scheme="http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest/tags" term="file"/>
        <author>
            <name>cliff2310</name>
            <uri>http://stackoverflow.com/users/1128925</uri>
        </author>
        <link rel="alternate" href="http://stackoverflow.com/questions/9439999/where-is-my-data-file"  target="_blank" />
        <published>2012-02-25T00:30:54Z</published>
        <updated>2012-02-25T00:30:54Z</updated>
        <summary type="html">
            <p>I have an Application that requires a data file...</p>

        </summary>
    </entry>
    <entry>
    ...
    </entry>
...
</feed>

 

Örnek uygulama entry etiketindeki verileri ve onun içindeki title, link ve summary etiketindeki verilerini ayrıştırır.

 

Ayrıştırıyı ilklendirilmek (Instantiate the parser)

Bundan sonraki adım ayrıştırıcının ilklendirilmesi (instantiate) ve ayrıştırma işleminin başlatılması. Aşağıdaki kod parçasında ayrıştırıcı, isim uzaylarını (namespace) işlemeden ilklendiriliyor ve veri girişi için o an sağlanan InputStream kullanılıyor. Ayrıştırıcı nextTag() metoduna yaptığı bir çağrıyla ayrıştırma işlemini başlatıyor ve readFeed() metodunda da uygulamanın ilgi alanına giren verileri ayıklayıp işlemeye başlıyor:

public class StackOverflowXmlParser {
    // namespace kullanmıyoruz
    private static final String ns = null;
   
    public List parse(InputStream in) throws XmlPullParserException, IOException {
        try {
            XmlPullParser parser = Xml.newPullParser();
            parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
            parser.setInput(in, null);
            parser.nextTag();
            return readFeed(parser);
        } finally {
            in.close();
        }
    }
 ... 
}

 

Haber kaynağını okumak

Haber kaynağını işlemek için asıl işi readFeed() metodu yapıyor. Bu metot, özyineli (recursive) bir şekilde çalıştıracağı haber kaynağı işleme işini başlatmak için "entry" etiketiyle etiketlenmiş elementleri arar. Bulamadığı elementi atlar. Tüm haber kaynağı işlendikten sonra readFeed(), haber kaynağından ayıkladığı entry'lerden oluşan bir List nesnesi döndürür. Ardından bu List nesnesi (entries), ayrıştırıcı tarafından döndürülür:



private List readFeed(XmlPullParser parser) throws XmlPullParserException, IOException {
    List entries = new ArrayList();

    parser.require(XmlPullParser.START_TAG, ns, "feed");
    while (parser.next() != XmlPullParser.END_TAG) {
        if (parser.getEventType() != XmlPullParser.START_TAG) {
            continue;
        }
        String name = parser.getName();
        // Starts by looking for the entry tag
        if (name.equals("entry")) {
            entries.add(readEntry(parser));
        } else {
            skip(parser);
        }
    }  
    return entries;
}

 

XML'i ayrıştırmak

XML haber kaynağı (feed) ayrıştırmak için gerekli adımlar şunlardır:

  1. Haber kaynağını analiz etmek bölümünde açıklandığı gibi uygulamanızda ilgilendiğiniz etiketleri öncelikle bir belirleyin. Buradaki örnek, entry etiketini ve onun altındaki title, link ve summary gibi etiketleri ayıklıyor.
  2. Şu metotları oluşturun:
    1. İlgilendiğiniz her etiket için bir "read" metodu oluşturun. Örneğin readEntry(), readTitle() ve bunun gibi. Ayrıştırıcı (parser), etiketleri InputStream'den okuyacağı için buradan gelen verilerin içinde entry, title, link veya summary olarak isimlendirilmiş bir etiket gördüğünde, etiket için uygun metodu çağıracak. Öteki türlü uygun bir metot bulamazsa etiketi atlar.
    2. Her farklı etiket tipi için veriyi ayıklayacak ve ayrıştırıcının sonraki etikete geçmesini sağlayacak metotlar oluşturun. Örneğin:
      1. title ve summary etiketleri için readText() metodunu oluşturun ki ayrıştırıcı çağırabilsin. Bu metot, parser.getText() metodunu çağırarak o etiketler için veri ayıklaması yapar.
      2. link etiketi için ayrıştırıcının çağırabileceği bir readLink() metodu oluşturun. Saptadığı ilk şartlarınıza uyan bağlantıya (link) ilişkin verileri ayıklasın. Ardından parser.getAttributeValue() metodunu kullanarak bağlantının değerini ayıklasın.
      3. entry etiketi için ayrıştırıcının çağırabileceği bir readEntry() metodu oluşturun. Bu metot entry'nin iç içe tüm etiketlerini ayrıştırsın ve title, link ve summary gibi öğeleri olan bir Entry nesnesi döndürsün.
    3. Özyineli (recursive) olan bir yardımcı skip() metodu oluşturun. Bu konudaki daha ayrıntılı bilgiyi Gerekmeyen etiketleri atlamak başlığında bulabilirsiniz.

Neyse ki örneğimizde oluşturmanız gereken tüm bu metotları bulabilirsiniz :). Aşağıdaki kod parçasında ayrıştırıcının entry, link, title ve summary gibi alanları nasıl ayıkladığını görebilirsiniz:

public static class Entry {
    public final String title;
    public final String link;
    public final String summary;

    private Entry(String title, String summary, String link) {
        this.title = title;
        this.summary = summary;
        this.link = link;
    }
}

// Bir Entry'nin içeriğini ayrıştırır. Eğer title, summary veya link 
// etiketiyle karşılaşırsa onları işlemek için ilgili "read" metoduna
// yönlendirir. Karşılaşmazsa etiketi atlar.
private Entry readEntry(XmlPullParser parser) throws XmlPullParserException, IOException {
    parser.require(XmlPullParser.START_TAG, ns, "entry");
    String title = null;
    String summary = null;
    String link = null;
    while (parser.next() != XmlPullParser.END_TAG) {
        if (parser.getEventType() != XmlPullParser.START_TAG) {
            continue;
        }
        String name = parser.getName();
        if (name.equals("title")) {
            title = readTitle(parser);
        } else if (name.equals("summary")) {
            summary = readSummary(parser);
        } else if (name.equals("link")) {
            link = readLink(parser);
        } else {
            skip(parser);
        }
    }
    return new Entry(title, summary, link);
}

// haber kaynağındaki title etiketlerini işler
private String readTitle(XmlPullParser parser) throws IOException, XmlPullParserException {
    parser.require(XmlPullParser.START_TAG, ns, "title");
    String title = readText(parser);
    parser.require(XmlPullParser.END_TAG, ns, "title");
    return title;
}

// haber kaynağındaki link etiketlerini işler
private String readLink(XmlPullParser parser) throws IOException, XmlPullParserException {
    String link = "";
    parser.require(XmlPullParser.START_TAG, ns, "link");
    String tag = parser.getName();
    String relType = parser.getAttributeValue(null, "rel");  
    if (tag.equals("link")) {
        if (relType.equals("alternate")){
            link = parser.getAttributeValue(null, "href");
            parser.nextTag();
        } 
    }
    parser.require(XmlPullParser.END_TAG, ns, "link");
    return link;
}

// haber kaynağındaki summary etiketlerini işler
private String readSummary(XmlPullParser parser) throws IOException, XmlPullParserException {
    parser.require(XmlPullParser.START_TAG, ns, "summary");
    String summary = readText(parser);
    parser.require(XmlPullParser.END_TAG, ns, "summary");
    return summary;
}

// title ve summary etiketlerinin text değerlerini ayıklar
private String readText(XmlPullParser parser) throws IOException, XmlPullParserException {
    String result = "";
    if (parser.next() == XmlPullParser.TEXT) {
        result = parser.getText();
        parser.nextTag();
    }
    return result;
}
  ...
}

 

XML ayrıştırmadaki adımlardan biri de aşağıda açıklandığı gibi ihtiyacınız olmayan etiketlerin atlanmasıdır. Ayrıştırıcımızın skip() metoduna bakalım:

private void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
    if (parser.getEventType() != XmlPullParser.START_TAG) {
        throw new IllegalStateException();
    }
    int depth = 1;
    while (depth != 0) {
        switch (parser.next()) {
        case XmlPullParser.END_TAG:
            depth--;
            break;
        case XmlPullParser.START_TAG:
            depth++;
            break;
        }
    }
 }

Ayrıştırıcımızın skip() metodunun nasıl çalıştığına biraz daha ayrıntılı bakalım:

  • O anki olayı bir START_TAG değilde hemen exception fırlatıyor.
  • START_TAG'i ve onunla END_TAG içererek eşleyen tüm olayları tüketir.
  • Doğru END_TAG'de durduğunu ve orijinal START_TAG'ten sonra karşılaştığı ilk etiket olmadığını garantilemek için etiketlerin iç içe derinliğini (depth) takip eder.

Bu metot sayesinde o anki elementin iç içe elementleri varsa depth değişkeni ayrıştırıcı orijinal START_TAG ve ona ait END_TAG arasındaki tüm olayları "tüketmeden" 0 olmaz.

 

XML verisini tüketmek

Örnek uygulama, bir XML haber kaynağını AsyncTask içinde çekip ayrıştırıyor. Bu sayede bu çekip ayrıştırma işlemi "main thread" olarak isimlendirilen UI thread'in üstünden alınmış oluyor. İşleme işi bittiğinde uygulama Activity'yi (NetworkActivity) güncelleyebiliyor.

Aşağıda gösterildiği gibi loadPage() metodu şunu yapar:

  • XML haber kaynağının adresini temsil eden URL'i bir string değişkene atar.
  • Eğer kullanıcı ayarları ve ağ bağlantısı müsade ederse new DownloadXMLTask().execute(url) metodunu çalıştırır. Bu işlem yeni bir DownloadXmlTask nesnesi (bir AsyncTask alt sınıfı) örnekler ve onun execute() metodunu çalıştırır. Bu execute() metodu haber kaynağını indirip ayrıştırır ve sonucunu bir string olarak UI'da gösterir.
public class NetworkActivity extends Activity {
    public static final String WIFI = "Wi-Fi";
    public static final String ANY = "Any";
    private static final String URL = "http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest";
   
    // bir Wi-Fi bağlantısı var/yok
    private static boolean wifiConnected = false; 
    // bir mobil bağlantı var/yok
    private static boolean mobileConnected = false;
    // ekran yenilemek gerekiyor/gerekmiyor
    public static boolean refreshDisplay = true; 
    public static String sPref = null;

    ...
      
    // stackoverflow.com'dan haber kaynağını (feed)
    // indirmek için AsyncTask kullanır
    public void loadPage() {  
      
        if((sPref.equals(ANY)) && (wifiConnected || mobileConnected)) {
            new DownloadXmlTask().execute(URL);
        }
        else if ((sPref.equals(WIFI)) && (wifiConnected)) {
            new DownloadXmlTask().execute(URL);
        } else {
            // hata göster
        }  
    }

Aşağıda bulunan AsyncTask'ın alt sınıfı DownloadXmlTask, şu AsyncTask metotlarını gerçekler:

  • doInBackground() metodu loadXmlFromNetwork() metodunu çalıştırır. Haber kaynağı URL'ini bir parametre olarak gönderir ve loadXmlFromNetwork() metodu da onu indirip işler. Bittiğinde sonucu bir string nesnesi olarak döndürür.

  • onPostExecute() metodu dönen string sonucu alır ve UI'da gösterir.

// stackoverflow.com'dan haber kaynağı (feed) indirmek için kullanılan bir Asynctask implementasyonu
private class DownloadXmlTask extends AsyncTask {
    @Override
    protected String doInBackground(String... urls) {
        try {
            return loadXmlFromNetwork(urls[0]);
        } catch (IOException e) {
            return getResources().getString(R.string.connection_error);
        } catch (XmlPullParserException e) {
            return getResources().getString(R.string.xml_error);
        }
    }

    @Override
    protected void onPostExecute(String result) {  
        setContentView(R.layout.main);
        // WebVieew aracılığıyla gelen HTML stringi gösterilir.
        WebView myWebView = (WebView) findViewById(R.id.webview);
        myWebView.loadData(result, "text/html", null);
    }
}

Kodlarına aşağıdaki bloktan ulaşabileceğiniz loadXmlFromNetwork() metodu DownloadXmlTask tarafından çağrılır ve şunu yapar:

  1. Bir tane StackOverflowXmlParser nesnesi örnekler. Ayrıca bir tane List değişkeni oluşturur (entries) - Entiry nesnelerinden oluşan. Bunu XML haber kaynağı ve onun alanlarından oluşan verileri bir arada tutmak için kullanır.
  2. Haber kaynağını çeken ve bir InputStream olarak döndüren downloadUrl() metodunu çağırır.
  3. StackOverflowXmlParser'ı kullanarak gelen InputStream'ı ayrıştırır. Böylece StackOverflowXmlParser, entries ismindeki List nesnesini haber kaynağının verileriyle doldurur.
  4. entries listesini işler ve haber kaynağı verilerini HTML biçiminde bir araya getirir.
  5. AsyncTask'ın onPostExecute() metoduna bir HTML stringi döndürür ve böylece bu içerik kullanıcı arayüzünde kullanıcıya gösterilir.
// stackoverflow'dan XML'i indirir, sonra onu 
// ayrıştırır ve HTML sözdizimine uygun bir şekilde
// bir araya getirip String olarak döndürür.
private String loadXmlFromNetwork(String urlString) throws XmlPullParserException, IOException {
    InputStream stream = null;
    // ayrıştırıcıyı ilklendirelim
    StackOverflowXmlParser stackOverflowXmlParser = new StackOverflowXmlParser();
    List entries = null;
    String title = null;
    String url = null;
    String summary = null;
    Calendar rightNow = Calendar.getInstance(); 
    DateFormat formatter = new SimpleDateFormat("MMM dd h:mmaa");
        
    // kullanıcının özet metnini ekle/ekleme tercihine bakalım
    SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
    boolean pref = sharedPrefs.getBoolean("summaryPref", false);
        
    StringBuilder htmlString = new StringBuilder();
    htmlString.append("<h3>" + getResources().getString(R.string.page_title) + "</h3>");
    htmlString.append("<em>" + getResources().getString(R.string.updated) + " " + 
            formatter.format(rightNow.getTime()) + "</em>");
        
    try {
        stream = downloadUrl(urlString);        
        entries = stackOverflowXmlParser.parse(stream);
    // InputStream'ın kullanıcı onu kullandıktan sonra 
    // kapatıldığından emin olalım
    } finally {
        if (stream != null) {
            stream.close();
        } 
     }
    
    // StackOverflowXmlParser (entries adında) Entry nesnelerinden 
    // oluşan bir List döndürür. 
    // Her Entry nesnesi haber kaynağındaki bir 
    // gönderiyi ifade eder. 
    // Bu bölümde entries listesi, entry'ler HTML sözdizimine 
    // uygun biçimde birleştirilerek işlenir 
    // Her entry UI'da gösterilir

    for (Entry entry : entries) {       
        htmlString.append("<p><a href='");
        htmlString.append(entry.link);
        htmlString.append("'>" + entry.title + "</a></p>");

        // eğer kullanıcı özet metni tercisini
	// ayarladıysa onu da gösterilecek
	// metne ekleyelim
        if (pref) {
            htmlString.append(entry.summary);
        }
    }
    return htmlString.toString();
}

// haber kaynağının string olarak gelen URL'ini alır, 
// bir bağlantı açar ve sonucu InputStream olarak 
// alır ve döndürür
private InputStream downloadUrl(String urlString) throws IOException {
    URL url = new URL(urlString);
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    conn.setReadTimeout(10000 /* milliseconds */);
    conn.setConnectTimeout(15000 /* milliseconds */);
    conn.setRequestMethod("GET");
    conn.setDoInput(true);
    // sorguyu başlatır
    conn.connect();
    return conn.getInputStream();
}

 

Bu sayfadaki parçalar Android Open Source Project kapsamında oluşturulmuş ve paylaşılmış içeriğin küçük değişiklikler yapılmış hâlidir ve Creative Commons 2.5 Attribution License'ta belirlenen koşullara göre kullanılmıştır.

Bu eğitim içeriğinin orijinal hâline buradan ulaşabilirsiniz: Parsing XML Data