Alexey Suvorov dev blog

Мой разработческий блог

Archive for the ‘F#’ Category

Visual studio 12 F# interactive — mixed mode assembly error

leave a comment »

Давно и широко использую f# для автоматизации операций по настройке энвайрмента. Создать и накататьи на неё все update скрипты, или обработать лог файлы с помощью pattern matching. Запускаю я это прямо из студии в f# Interactive, но с переходом на 12 студию возникла проблема с загрузкой сборок не 4.0. Такая проблема уже была в 2012 студии, но как-то забылось за давностью.

Итак, студия описывала проблему совершенно однозначно:

Mixed mode assembly is built against version ‘v2.0.50727’ of the runtime and cannot be loaded in the 4.0 runtime without additional configuration information.

Решение тоже хорошо известно из интернетов:

  <startup useLegacyV2RuntimeActivationPolicy="true">
    <supportedRuntime version="v4.0"/>
  </startup>

Но поискав на диске «fsi.exe.config» и добавив кусок выше во все найденные файлы я не добился никакого результата. Вариантов было множество от копания в конфигурациях расширений IDE, до долгих поисков в интернете, но я подумал, что F# Interactive в студии точно не тред а процесс, поэтому просто запустил procexp.

Не буду томить долгими историями, но оказалось что моя студия использует FsiAnyCPU.exe, который был расположен в совсем уж неожиданной папке:
C:\Program Files (x86)\Microsoft SDKs\F#\3.0\Framework\v4.0
FsiAnyCPU process

UPD: в VS 2013 Update 1 у меня FSI переехал в C:\Program Files (x86)\Microsoft SDKs\F#\3.1\Framework\v4.0

Written by alexeysuvorov

05.11.2012 at 4:19 пп

Опубликовано в F#, F# interactive, VS 11

F# — парсинг XML экспортированного из Excel

leave a comment »

WCF сервисы, SQL и noSQL базы данных — это реалии крупных западных компаний. Местный малый бизнес до сих пор обменивается данными в Word документах, в лучшем случае в Excel. Правильный парсинг Excel должен начинаться как-то так:

Microsoft.Office.Interop.Excel.Application excelApp = new Microsoft.Office.Interop.Excel.Application();

Но на shared хостинге это решительно невозможно и тогда компромиссным решением может служить XML экспорт из Excel. Проблема этого подхода в том, что придётся делать всё с нуля. В силу привычки для парсинга я использую HtmlAgilityPack, но в данном случае возникла проблема с namespaces. Дело в том, что экспорт Excel в XML выглядит как то так:

<?xml version="1.0"?>
<?mso-application progid="Excel.Sheet"?>
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
 xmlns:o="urn:schemas-microsoft-com:office:office"
 xmlns:x="urn:schemas-microsoft-com:office:excel"
 xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
 xmlns:html="http://www.w3.org/TR/REC-html40">
...
 <Worksheet ss:Name="Battery">
  <Table ss:ExpandedColumnCount="6" ss:ExpandedRowCount="537" x:FullColumns="1"
   x:FullRows="1" ss:DefaultRowHeight="15">
   <Column ss:Width="45"/>
   <Column ss:AutoFitWidth="0" ss:Width="374.25"/>
   <Column ss:Width="38.25"/>
   <Column ss:Width="57.75" ss:Span="2"/>
   <Row ss:Height="30">
    <Cell ss:StyleID="s64"><Data ss:Type="String">X-XX-951</Data></Cell>
    <Cell ss:StyleID="s66"><Data ss:Type="String">***</Data></Cell>
    <Cell ss:StyleID="s66"><Data ss:Type="String">        2600,00</Data></Cell>
    <Cell ss:StyleID="s66"><Data ss:Type="String">        2250,00</Data></Cell>
    <Cell ss:StyleID="s66"><Data ss:Type="String">        1850,00</Data></Cell>
   </Row>
  </Table>
 </Worksheet>
...
</Workbook>

Все ячейки имеют namespace и xpath выражение должно быть построено с учётом этого факта. Альтернативой был saxon, но стиль в котором написан API выжимает слезу — вспоминаются институтские годы и .net 1.0. Так что меньшим злом оказалась обёртка над стандартным XmlDocument. XmlNamespaceManager должен передаваться в каждый запрос, так что имеет его сохранить. Код:

module XmlHelpers =
    let EnumerableToSeq<'a> (x:System.Collections.IEnumerable) =
        let enumerator = x.GetEnumerator()
        seq {
            while enumerator.MoveNext() do 
                yield (enumerator.Current :?> 'a)
        }

    type NsEnabledDocument (fileName:string) =
        let doc = new XmlDocument()
        do doc.Load(fileName)
        let nsManager = new XmlNamespaceManager(doc.NameTable)
        //все запросы будут выполнены с namespace manager-ом
        let doSelect sel p = sel(p,nsManager)
        
        member public this.AddNamespace ns nsVal =
            nsManager.AddNamespace(ns,nsVal)
            nsManager.PushScope()

        member public this.Select path = 
            doSelect doc.DocumentElement.SelectNodes path 
            |> EnumerableToSeq<XmlNode>

        member public this.SelectSingle path =
            doSelect doc.DocumentElement.SelectSingleNode path

Использовать как-то так:

let doc = NsEnabledDocument(file)
doc.AddNamespace "ss" "urn:schemas-microsoft-com:office:spreadsheet"
let nodes = doc.Select "//ss:Worksheet[@ss:Name='Battery']/ss:Table/ss:Row"

После получения документа и выборки ячеек надо понять что содержит строка, является ли она заголовком или содержит нужные данные. Получение типа строки по его содержимому:

let (|CountVal|DoubleVal|StringVal|) (s:string) =
    match s with
    | "****" -> CountVal ">30"
    | "***"  -> CountVal "11-30"
    | "**"   -> CountVal "4-10"
    | "*"    -> CountVal "1-3"
    | s ->
        let (b,d) = Double.TryParse(s,NumberStyles.Any,CultureInfo.CurrentCulture.NumberFormat)
        if b then DoubleVal(d)
        else StringVal(s)

Этот код полностью зависит от предметной области, в данном случае ячейка содержащая «*» — «****» означает примерное количество товара на складе.
Соответственно ячейки делятся на 4 типа: с данными не попадающие в другие категории (DataCell), ячейка с ценой (DoubleCell), ячейка с количеством (CountCell) и Xml не являющийся ячейкой (NotCell):

let (|DataCell|DoubleCell|CountCell|NotCell|) (n:XmlNode) 
    : Choice<string,double, string, unit> =
    if null = n then NotCell
    else
        match (n.Descendants() |> Seq.toList) with
        | (data : XmlNode) :: [] -> 
            match data.InnerText with 
            | CountVal x -> CountCell(x)
            | DoubleVal x -> DoubleCell(x)
            | StringVal x -> DataCell(x)
        | _ -> NotCell

Здесь .Descendants() — метод расширение

type System.Xml.XmlNode with
        member this.Descendants () =
            this.ChildNodes |> EnumerableToSeq<XmlNode>

Теперь код парсинга примитивно прост — мы задаём тип и последовательность ячеек которые должна содержать строка и конструируем готовый объект из уже типизированных объектов:

let PriceRowToObject (row:XmlNode) = 
    let cells = row.Descendants() |> FilterTextNodes |> Seq.toList
    match cells with
    | DataCell(a) :: DataCell(desc) :: CountCell(count) :: 
      DoubleCell(p1) :: DoubleCell(p2) :: DoubleCell(p3) :: _ -> 
        Some({Article = a; Count = count;  Desc = desc; PriceMax = p2; PriceMin = p3}: PriceItem)
    | _ -> None

Механизм не претендует на универсальность и требует перекомпиляции при изменении схему, но валиден в реалиях жёсткой схемы базы и структуры загружаемого документа.

Written by alexeysuvorov

17.06.2012 at 5:09 пп

Опубликовано в .net, F#

Простой web scraping на f#

6 комментариев

Достаточно законный вопрос почему такая избитая тема как web scraping и почему f#. 1 . на f# web scraping намного увлекательней чем на c#  2. хотелось попробовать насколько f# применим для разработки не демо примеров а что то реально делающих программ 3. У f# есть интерактивная консоль, что при ковырянии в недрах HTML становится просто спасением.

Touareg

Туарег на f#К делу, будем покупать VW Touareg. По моему самая оптимальная машина для суровой зимы и не менее суровых дорог. Предположим у нас миллион четыреста тысяч, большое желание и больше ничего нет. Есть ещё сайт auto.ru, но недавнее моё участие в покупке бу автомобиля выявило несколько недостатков: а. нужно постоянно заходить в правильный раздел б. нужно постоянно заполнять форму поиска, что особенно раздражает когда это нужно делать в дороге с мобильных устройств, по пути на осмотр очередного кандидата мы пользовались iPad и это было «не айс», а с какого нибудь смартфона я бы точно застрелился выполнять все эти операции. Итого требования: программа обходит страницу с предложениями соответствующими запросу, ищет новые предложения и если находит, то посылает письмо с параметрами нового предложения(ий) и заодно список всех предложений, удовлетворяющих запросу, чтобы можно было сравнить визуально.

Общие методы

auto.ru достаточно лояльно относится к сбору своего контента, так что извратов вроде эмуляции нажатия на кнопку и подсовывания cookies тут не будет, и всё что нас интересует можно получить по прямому url через GET. Так же письма мы будем слать через gmail, что потребует в конфиге настроек SMTP клиента указанных в комментариях

module WebUtil =
    let LoadPage (x:WebRequest) =
        use resp = x.GetResponse()
        let rs = resp.GetResponseStream()
        let rr = new StreamReader(rs,System.Text.Encoding.GetEncoding(1251))
        rr.ReadToEnd()

    let LoadPageByUrl (x:string) =
        let request = WebRequest.Create(x)
        LoadPage request

    let SentByMail (recepinet: string) (subj:string) (content: string) =
        let client = new SmtpClient()
        client.DeliveryMethod <- SmtpDeliveryMethod.Network
        use message = new MailMessage()
        message.To.Add(recepinet)
        message.Body <- content
        message.Subject <- subj
        message.IsBodyHtml <- true 
        client.Send(message)
    (*
    <system.net>
    <mailSettings>
      <smtp from="YourMail@gmail.com">
        <network host="smtp.gmail.com" port="587" enableSsl="true" 
                 password="$ecretPa$$w0rd" defaultCredentials="false" 
                 userName="YourMail@gmail.com" />
      </smtp>
    </mailSettings>
    </system.net>
    *)

Что здесь от f#? Функционально — ничего, стандартные методы платформы, но зато поразительно кратко, если бы не портянка сеттеров свойств MailMessage. Читаемость кода это очень персональное, но по моему мало что может сравниться с f# по читаемости.

Структуры данных

Т.к. нужно различать какие предложения были добавлены с момент последней проверки, результат предидущего запроса будет храниться в файле. Можно было бы правда хранить только дату последней проверки, но тогда было бы совсем не интересно и была бы упущена тема сереализации сложных объектов. Итак записи (records):

module CarParser =
    [<DataContract>]
    type Car = 
        {
            [<field: DataMember(Name="Year") >]
            Year: int;
            [<field: DataMember(Name="Price") >]
            Price: int;
            [<field: DataMember(Name="Model") >]
            Model: string;
            [<field: DataMember(Name="Engine") >]
            Engine: string;
            [<field: DataMember(Name="Url") >]
            Url: string;
        }

    [<DataContract>]
    type CarRequest =
        {
            [<field: DataMember(Name="Email") >]
            Email:string;
            [<field: DataMember(Name="RequestUrl") >]
            RequestUrl: string;
            [<field: DataMember(Name="Cars") >]
            Cars: Car list;
        }

Почему дополнительные атрибуты? Дело в том, что стандартная сериализация в XML через XmlSerializer не работает, потому что у f# records нет конструктора без параметров, который является обязательным. В данном случае спасёт DataContractSerializer, методы для сериализации и десереализации в файл выглядят так

open System;
open System.IO;
open System.Xml;
open System.Runtime.Serialization;
open System.Text.RegularExpressions;

module SerializationUtils = 
    let SerializeToFile (req:'T) (fileName: string) =
      let xmlSerializer = DataContractSerializer(typeof<'T>); 
      use fs = File.OpenWrite(fileName)
      xmlSerializer.WriteObject(fs, req)
  
    //T' will be calculated automatically
    let Deserialize<'T> (fileName:string) =
        let xmlSerializer = DataContractSerializer(typeof<'T>); 
        use fs = File.OpenRead(fileName)
        xmlSerializer.ReadObject(fs) :?> 'T

Парсинг контента

Если говорить о параметрах конкретной машины, то приоритет следующий: цена, двигатель — сколько налогов я буду платить и бензин или дизель, год — очень косвенно указывающий на состояние. Если меня всё устраивает, то можно посмотреть на фото перейдя по ссылке, этот момент тоже кстати интересно было бы переделать и указывать ссылку на свой сайт, который покажет мне фотографии и описание машины без всяких там реклам. Но вернёмся к более насущной задаче.
Разбор контента
Использовав HTMLAgilityPack (по моему это действительно классно — любые .net библиотеки доступны из f#) получаем таблицу с предложениями и дальше разбор — просто дело техники. Опять таки на f# парсинг выглядит очень коротко и понятно, я точно знаю, что хотя бы часть следующего реального проекта по сбору и анализу контента я буду делать на f# потому что его намного проще читать.

    
let private ParseCar (cnt: HtmlNode) =
    let columns = cnt.SelectNodes("td") |> Seq.toList
    let model = columns.[0].InnerText
    let txt = columns.[1].InnerText
    let price = txt |> (fun x -> Regex.Replace(x,"\\W",System.String.Empty)) |> Int32.Parse
    let url = columns.[0].ChildNodes 
            |> Seq.find (fun x -> x.Name.ToLower() = "a")
            |> (fun x-> x.Attributes) 
            |> Seq.find (fun x -> x.Name = "href")
            |> (fun x -> x.Value)
    let year = columns.[2].InnerText |> Int32.Parse
    let engine = columns.[3].InnerText
    let c: Car = { Year = year; Price = price; Model = model; Url = url; Engine = engine; }
    c

let private ParsePage (node: HtmlNode) (parseCar: HtmlNode -> Car) =
    node.SelectNodes("//div[@id='cars_sale']/table[@class='list']/descendant::tr[not(@class='header first')]")
    |> Seq.map parseCar

И несколько методов для обобщения полученных данных в запросы интересных наверное только каррированием и инициализацией записи копированием из старой записи. f# сам переопределяет функции CompareTo, Equals и GetHashCode, поэтому сравнение записей в данном случае работает корректно и можно писать x = y.

let private ParseCarNode x = ParsePage x ParseCar

let private GetCars (cntnt:string) (pars: HtmlNode -> seq<Car>) =
    let doc = new HtmlDocument()
    doc.LoadHtml(cntnt)
    pars doc.DocumentNode

let CreateCarRequest mail url =
    let cars = GetCars (LoadPageByUrl url) ParseCarNode
    { Email = mail; RequestUrl = url; Cars = cars |> List.ofSeq }

let UpdateCarList (oldRequest: CarRequest) =
    let newCars = GetCars (LoadPageByUrl oldRequest.RequestUrl) ParseCarNode
    let isContains y = Seq.tryFind (fun x -> x = y)
    let diff = newCars |> Seq.filter (fun x -> (oldRequest.Cars |> isContains x) = None)
    let res = { oldRequest with Cars = newCars |> List.ofSeq }
    //и результаты запроса и разница с предыдущим, очень приятный синтаксис
    (res,diff)

Итоги

За бортом остались функции форматирования e-mail сообщений и функция которая собирает это всё вместе и запускает по таймеру, но их реализация очевидна. Тестирование можно делать на c#, в частности тестирование корректности нахождения новых машин было реализовано с помощью Moles и именно там всплыли грабли описанные в этом посте.
Основные достоинства: 200 строк кода. Всё вместе. Все 5 файлов. Процентов на 30 меньше чем средний файл на c# в программах которые несут хоть какую нибудь функциональную нагрузку, а не просто вызывают методы фреймворка с другим порядком аргументов. Читаемость кода. Скорость разработки, в программах собирающих конетент очень большим плюсом является возможность выполнить код без компиляции. По моему мнению f# — это более понятный и натуральный способ разработки программ, знакомые и рутинные задачи снова становятся интересными.
Основные недостатки: основной недостаток — это конечно же кривые руки, потому что в программе нет ни логирования ни внятной обработки ошибок, что конечно же недопустимо (но правда тут мы просто машину покупаем а не программами торгуем).
В любом случае на f# можно и нужно писать real world программы и данная задача просто недостаточно сложна алгоритмически и логически чтобы показать все возможности и достоинства языка, но даже такие задачи получаются неплохо, а главное интересно и достаточно быстро, если не считать время на освоение языка.

PS: Осталось написать веб интерфейс и попросить с подписавшихся donation на оплату sms gateway, если авто.ру не забанит меня раньше 🙂

Written by alexeysuvorov

22.01.2011 at 11:11 пп

Опубликовано в .net, F#

Две вещи которых мне сразу не хватает в C# после F#

with one comment

Я не считаю себя превередливым человеком, но есть вещи в которых очень сложно себе отказать. Я думаю любой кто хотя бы пару дней посидел за f# со мной согласится.
|> и seq.map:

namespace Common.LangExt
{
    using System;
    using System.Collections.Generic;

    public static class λExtension
    {
        public static U ApplyFunction<T, U>(this T t, Func<T, U> f) { return f(t); }
        public static void ApplyFunction<T>(this T t, Action<T> f) { f(t); }

        public static IEnumerable<T> ForEach<U, T>(this IEnumerable<U> t, Func<U, T> f)
        {
            foreach (U x in t) yield return f(x);
        }
        public static void ForEach<U>(this IEnumerable<U> t, Action<U> f)
        {
            foreach (U x in t) f(x);
        }
    }
}

Для тех кто не брезгует символами, которых нет на клавиатуре — своё пространство имён

namespace Common.LangExt.Light
{
    usingCommon.LangExt;
    using System;
    using System.Collections.Generic;

    public static class λExtensionLight
    {
        public static U à<T, U>(this T t, Func<T, U> f)
        {
            return t.ApplyFunction(f);
        }
        public static void à<T>(this T t, Action<T> f)
        {
            t.ApplyFunction(f);
        }

        public static IEnumerable<T> ƒ<U, T>(this IEnumerable<U> t, Func<U, T> f)
        {
            return t.ForEach(f);
        }
        public static void ƒ<U>(this IEnumerable<U> t, Action<U> f)
        {
            t.ForEach(f);
        }
    }
}

Written by alexeysuvorov

14.01.2011 at 3:41 дп

Опубликовано в .net, C#, F#