none
XPath как работать со связанными данными RRS feed

  • Вопрос

  • Столкнулся с такой проблемой. Использую для работы С# объект XmlDocument

    Имеется XML файл. Пример:

    <?xml version="1.0" encoding="utf-8"?>
    <Data>
    <Houses>
      <House ID="999999000" StreetID="276154">Дом без счетчиков 100%</House>
      <House ID="47328" StreetID="47310">26</House>
    </Houses>
    <Counters>
      <Counter ID="22289" HouseID="47310" />
    </Counters>
    </Data>

    Передо мной стоит задача найти все дома, к которым не привязаны счетчики.

    Данных довольно много и поэтому такой пример работает очень долго:

    foreach (XmlNode item in Doc.SelectNodes("Data/Houses/House"))
                    {
                        string id = item.Attributes["ID"].Value;
                        if (Doc.SelectNodes("Data/Counters/Counter[@HouseID=" + id + "]").Count == 0)
                        {
                          //Здесь точно видно, что переменная item и есть дом без счетчиков
                        }
                    }

    Есть ли способ найти связанные значения (Связь происходит по аттрибутам ID и HouseID элементов House и Counter соответсвенно) одним XPath запросом.

    12 августа 2014 г. 5:49

Ответы

  • Спасибо за такой подробный ответ. Даже несмотря на то, что решить задачу так и не удалось с помощью XPath, я почерпнул кое что новое. Это полезно.

    Я решил задачу "В лоб". Можно сказать обошел эту  проблему.

    Я создал две коллекции. Домов и счетчиков. (Создав соответствующие объекты) и сделал пересечение посредством обычного Linq.

    Что - то типа этого:

     public List<Houses> HousesGetEmptyHouses()
            {
                CountersManager cMgr = new CountersManager(Doc);
                HousesManager hMgr = new HousesManager(Doc);
                
                List<Houses> hColl =  hMgr.GetAll();
                List<Counters> cColl = cMgr.GetAll();
    
                hColl.RemoveAll(p => cColl.Find(s => s.HouseID == p.ID) != null);
                return hColl;
            }

    При наличии примерно 200 домов и 2500 счетчиков такое решение оказалось заметно эффективнее по скорости. Раз в 10, чем то, что я писал выше в первом вопросе.

    13 августа 2014 г. 10:21

Все ответы

  • В дотнете есть три API для работы с XML в памяти: XmlDocument, XPathDocument (позволяет только чтение, но не изменение документа), Linq to Xml (XDocument/XElement). Первый считается самым медленным, второй и третий быстрее. Последний самый современный и рекомендуется для всех новых проектов. Если версия фреймворка позволяет, переходите на него.

    Поверх любого из них можно создать XPathNavigator, который позволяет использовать компилированные xpath-выражения - они работают существенно быстрее. Но так как в данном случае xpath меняется динамически постоянно, это может не дать выигрыша.

    Можно попробовать чуть-чуть оптимизировать данный код. Например, укоротить путь поиска во втором xpath.

    XmlNode counters = Doc.SelectSingleNode("Data/Counters");
    
    foreach (XmlNode item in Doc.SelectNodes("Data/Houses/House"))
    {
        ...
        if (counters.SelectNodes("Counter[@HouseID=" + id + "]").Count == 0)
        ...
    }

    Также, если позволяют условия, использовать SelectSingleNode - выборка одного узла быстрее, чем всей коллекции.

    if (counters.SelectSingleNode("Counter[@HouseID=" + id + "]") == null)

    А вообще, xpath - это очень медленный способ работы с xml. Если получается добраться до нужного узла с помощью таких свойств, как ChildNodes, FirstChild, NextSibling и т. п., то это будет обычно быстрее.
    Отмечу, что в Linq to Xml такой способ является предпочтительным. Там xpath по умолчанию вообще не открыт.

    В приведённом XML используются атрибуты ID, что даёт возможность использовать метод GetElementById - это, по идее, должно работать быстрее всего.

    На всякий случай отмечу, что очень медленным является использование Descendant узлов (в xpath это выражается двумя слэшами - //). В данном случае этого нет, и это хорошо.

    Вот. Теперь, собственно, ответ.

    var items = Doc.SelectNodes("Data/Houses/House[@ID = /Data/Counters/Counter/@HouseID]");

    Так мы получим коллекцию домов со счетчиками. Вам же нужен, как я понял, противоположный результат - дома без счетчика. Увы, если условие поменять на != (не равно), то выберутся все дома. Что-то не соображу, как получить нужное.

    12 августа 2014 г. 12:55
  • Спасибо за такой подробный ответ. Даже несмотря на то, что решить задачу так и не удалось с помощью XPath, я почерпнул кое что новое. Это полезно.

    Я решил задачу "В лоб". Можно сказать обошел эту  проблему.

    Я создал две коллекции. Домов и счетчиков. (Создав соответствующие объекты) и сделал пересечение посредством обычного Linq.

    Что - то типа этого:

     public List<Houses> HousesGetEmptyHouses()
            {
                CountersManager cMgr = new CountersManager(Doc);
                HousesManager hMgr = new HousesManager(Doc);
                
                List<Houses> hColl =  hMgr.GetAll();
                List<Counters> cColl = cMgr.GetAll();
    
                hColl.RemoveAll(p => cColl.Find(s => s.HouseID == p.ID) != null);
                return hColl;
            }

    При наличии примерно 200 домов и 2500 счетчиков такое решение оказалось заметно эффективнее по скорости. Раз в 10, чем то, что я писал выше в первом вопросе.

    13 августа 2014 г. 10:21