Как парсить XML на Android

На сегодняшний день роль XML (eXtensible Markup Language — расширяемый язык разметки) в программировании огромна: этот формат используется повсеместно во многих языках программирования. В частности, при разработке приложений на Android XML отвечает за то, что пользователь увидит на экране своего смартфона (интерфейс приложения, строковые ресурсы, векторные изображения и т.д.). Кроме того, данный формат зачастую используется для хранения и передачи различных данных.

Именно из-за того, что XML можно использовать как хранилище данных, встаёт вопрос: как с этими данными взаимодействовать, как получать их из XML и отправлять обратно.

Ранее мы говорили о том, что в Android 8.0 Oreo для хранения конфигураций Wi-Fi сетей теперь используется другой файл (подробнее об этом можно почитать здесь). Поскольку этот файл конфигурации как раз нужного нам формата XML, попробуем распарсить его и вытащить их него нужные данные.

Сама структура XML-файла конфигурации выглядит следующим образом:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<WifiConfigStoreData>
  <int name="Version" value="1" />
  <NetworkList>
    <Network>
      <WifiConfiguration>
        <string name="ConfigKey">&quot;Kafe Mongol&quot;WPA_PSK</string>
        <string name="SSID">&quot;Kafe Mongol&quot;</string>
        <null name="BSSID" />
        <string name="PreSharedKey">&quot;mongol1234&quot;</string>
        <null name="WEPKeys" />
        <int name="WEPTxKeyIndex" value="0" />
        <boolean name="HiddenSSID" value="false" />
        <boolean name="RequirePMF" value="false" />
        <byte-array name="AllowedKeyMgmt" num="1">02</byte-array>
        <byte-array name="AllowedProtocols" num="1">03</byte-array>
        <byte-array name="AllowedAuthAlgos" num="1">01</byte-array>
        <byte-array name="AllowedGroupCiphers" num="1">0f</byte-array>
        <byte-array name="AllowedPairwiseCiphers" num="1">06</byte-array>
        <boolean name="Shared" value="true" />
        <int name="Status" value="2" />
        <null name="FQDN" />
        <null name="ProviderFriendlyName" />
        <null name="LinkedNetworksList" />
        <string name="DefaultGwMacAddress">6c:19:8f:f5:e3:6e</string>
        <boolean name="ValidatedInternetAccess" value="true" />
        <boolean name="NoInternetAccessExpected" value="false" />
        <int name="UserApproved" value="0" />
        <boolean name="MeteredHint" value="false" />
        <boolean name="UseExternalScores" value="false" />
        <int name="NumAssociation" value="4" />
        <int name="CreatorUid" value="1000" />
        <string name="CreatorName">android.uid.system:1000</string>
        <string name="CreationTime">time=08-30 12:51:06.957</string>
        <int name="LastUpdateUid" value="1000" />
        <string name="LastUpdateName">android.uid.system:1000</string>
        <int name="LastConnectUid" value="1000" />
        <boolean name="IsLegacyPasspointConfig" value="false" />
        <long-array name="RoamingConsortiumOIs" num="0" />
      </WifiConfiguration>
      <NetworkStatus>
        <string name="SelectionStatus">NETWORK_SELECTION_ENABLED</string>
        <string name="DisableReason">NETWORK_SELECTION_ENABLE</string>
        <null name="ConnectChoice" />
        <long name="ConnectChoiceTimeStamp" value="-1" />
        <boolean name="HasEverConnected" value="true" />
      </NetworkStatus>
      <IpConfiguration>
        <string name="IpAssignment">DHCP</string>
        <string name="ProxySettings">NONE</string>
      </IpConfiguration>
    </Network>
...
  </NetworkList>
  <PasspointConfigData>
    <long name="ProviderIndex" value="0" />
    </PasspointConfigData>
</WifiConfigStoreData>

Нужные нам данные хранятся внутри тега <WifiConfiguration>, каждый тег обозначает тип значения, затем внутри в виде атрибутов устанавливаются его название и собственно значение.

Для извлечения данных в Android есть полезный интерфейс XmlPullParser. С его помощью можно в цикле while пройтись построчно по каждому тегу в документе и проверить его содержимое. Каждый проход по циклу представляет собой определённое событие, метод getEventType() возвращает тип этого события, например: START_DOCUMENT, END_TAG и др. Переход к следующему тегу осуществляется с помощью метода next().

Извлечём некоторые данные из XML файла. Получившийся код выглядит следующим образом:

private void parseWithXmlPullParser() {
  LayoutInflater inflater = getLayoutInflater();
  CardView cardView = null;
  LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
      ViewGroup.LayoutParams.WRAP_CONTENT);
  params.setMargins(50, 50, 50, 50);

  try {
    XmlPullParser xmlPullParser = getResources().getXml(R.xml.wifi_config_store);
    while (xmlPullParser.getEventType() != XmlPullParser.END_DOCUMENT) {

      switch (xmlPullParser.getEventType()) {
        case XmlPullParser.START_DOCUMENT: {
          if (BuildConfig.DEBUG) {
            Log.d("LOG_TAG", "START_DOCUMENT");
          }
          break;
        }
        // начало тега
        case XmlPullParser.START_TAG: {
          if (BuildConfig.DEBUG) {
            Log.d("LOG_TAG", "START_TAG: имя тега = "
                + xmlPullParser.getName()
                + ", уровень = "
                + xmlPullParser.getDepth()
                + ", число атрибутов = "
                + xmlPullParser.getAttributeCount());
          }

          if (xmlPullParser.getName().equals("WifiConfiguration")) {
            cardView = (CardView) inflater.inflate(R.layout.wifi_info, null);
            cardView.setLayoutParams(params);
          }

          if (xmlPullParser.getName().equals("string")) {
            if (xmlPullParser.getAttributeValue(null, "name").equals("SSID")) {
              TextView textViewSSID = cardView.findViewById(R.id.textSsid);
              textViewSSID.setText(textViewSSID.getText() + " " + xmlPullParser.nextText());
              break;
            }

            if (xmlPullParser.getAttributeValue(null, "name").equals("PreSharedKey")) {
              TextView textViewPreShareKey = cardView.findViewById(R.id.textPreShareKey);
              textViewPreShareKey.setText(
                  textViewPreShareKey.getText() + " " + xmlPullParser.nextText());
              break;
            }
          }

          if (xmlPullParser.getName().equals("boolean")) {
            if (xmlPullParser.getAttributeValue(null, "name").equals("HiddenSSID")) {
              TextView textViewHiddenSSID = cardView.findViewById(R.id.textHiddenSsid);
              textViewHiddenSSID.setText(
                  textViewHiddenSSID.getText() + " " + xmlPullParser.getAttributeValue(null,
                      "value"));
              break;
            }
          }
          break;
        }
        // конец тега
        case XmlPullParser.END_TAG:
          if (BuildConfig.DEBUG) {
            Log.d("LOG_TAG", "END_TAG: имя тега = " + xmlPullParser.getName());
          }

          if (xmlPullParser.getName().equals("WifiConfiguration")) {
            linearLayout.addView(cardView);
          }

          break;
        // содержимое тега
        case XmlPullParser.TEXT:
          if (BuildConfig.DEBUG) {
            Log.d("LOG_TAG", "текст = " + xmlPullParser.getText());
          }
          break;

        default:
          break;
      }
      xmlPullParser.next();
    }
  } catch (Throwable e) {
    e.printStackTrace();
  }
}

В данном случае нас интересует событие START_TAG. Когда это событие наступает, нужно найти нужные данные по типу и имени, и затем забрать из этого тега значение. В результате выполнения цикла выводим всё на экран.

Как видно, реализация довольно проста, разработчику достаточно только понимать, как устроен XML-документ.

Однако это не единственный способ извлечь данные из XML. В Android есть также класс DocumentBuilder, который позволяет получать экземпляры DOM документов из XML DOM (Document Object Model) — это программный интерфейс, благодаря которому можно получать доступ к данным HTML или XML.

Ключевым методом здесь будет являться getElementsByTagName(), который возвращает список тегов с нужным именем, затем внутри этого списка ищется нужный по атрибуту name. Реализация выглядит следующим образом.

private void parseWithDOM() {
  LayoutInflater inflater = getLayoutInflater();
  CardView cardView = null;
  LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
      ViewGroup.LayoutParams.WRAP_CONTENT);
  params.setMargins(50, 50, 50, 50);

  try {
    InputStream is = getAssets().open("wifi_config_store.xml");
    DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
    DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
    Document doc = documentBuilder.parse(is);
    Element element = doc.getDocumentElement();
    element.normalize();

    NodeList nList = doc.getElementsByTagName("WifiConfiguration");
    for (int i = 0; i < nList.getLength(); i++) {
      cardView = (CardView) inflater.inflate(R.layout.wifi_info, null);
      cardView.setLayoutParams(params);
      TextView textViewSSID = cardView.findViewById(R.id.textSsid);
      TextView textViewPreSharedKey = cardView.findViewById(R.id.textPreShareKey);
      TextView textViewHiddenSSID = cardView.findViewById(R.id.textHiddenSsid);

      Node node = nList.item(i);

      if (node.getNodeType() == Node.ELEMENT_NODE) {
        Element elementNode = (Element) node;
        NodeList childrenList = elementNode.getElementsByTagName("string");

        for (int j = 0; j < childrenList.getLength(); j++) {
          Element childNode = (Element) childrenList.item(j);

          if (childNode.getAttribute("name").equals("SSID")) {
            textViewSSID.setText(textViewSSID.getText() + " " + childNode.getChildNodes().item(0).getNodeValue());
          }

          if (childNode.getAttribute("name").equals("PreSharedKey")) {
            textViewPreSharedKey.setText(textViewPreSharedKey.getText() + " " + childNode.getChildNodes().item(0).getNodeValue());
          }
        }

        NodeList hiddenList = elementNode.getElementsByTagName("boolean");
        for (int k = 0; k < hiddenList.getLength(); k++) {
          Element childNode = (Element) hiddenList.item(k);

          if (childNode.getAttribute("name").equals("HiddenSSID")) {
            textViewHiddenSSID.setText(textViewHiddenSSID.getText() + " " + childNode.getAttribute("value"));
          }
        }
      }

      linearLayout.addView(cardView);
    }
  } catch (IOException | ParserConfigurationException | SAXException e) {
    e.printStackTrace();
  }
}

В результате мы получили аналогичный список, как и при первом случае.

Здесь были приведены не все способы распарсить XML, существует множество различных пользовательских библиотек, предоставляющих такой же функционал. Всё зависит от того, какой способ будет удобнее разработчику в конкретной ситуации.

Нашли ошибку в тексте?

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *