Внутренний элемент фильтра из дерева через линзу

Я постоянно признаю, что плохо разбираюсь в объективах, но разве учиться на примерах — это не хорошо? Я хочу взять HTML, разобрать его с помощью taggy-lens, а затем удалить все элементы script изнутри. Вот моя попытка:

#!/usr/bin/env stack
-- stack --resolver lts-7.1 --install-ghc runghc --package text --package lens --package taggy-lens --package string-class --package classy-prelude

{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}

import ClassyPrelude
import Control.Lens hiding (children, element)
import Data.String.Class (toText, fromText, toString)
import Data.Text (Text)
import Text.Taggy.Lens
import qualified Text.Taggy.Lens as Taggy
import qualified Text.Taggy.Renderer as Renderer

somehtmlSmall :: Text
somehtmlSmall =
    "<!doctype html><html><body>\
    \<div id=\"article\"><div>first</div><div>second</div><script>this should be removed</script><div>third</div></div>\
    \</body></html>"

renderWithoutScriptTag :: Text
renderWithoutScriptTag =
    let mArticle :: Maybe Taggy.Element
        mArticle =
            (fromText somehtmlSmall) ^? html .
            allAttributed (ix "id" . only "article")
        mArticleFiltered =
            fmap
                (\el ->
                      el ^.. to universe . traverse .
                      filtered (\n -> n ^. name /= "script"))
                mArticle
    in maybe "" (toText . concatMap Renderer.render) mArticleFiltered

main :: IO ()
main = print renderWithoutScriptTag

Отметьте этот файл как исполняемый и просто запустите его, и вы увидите:

➜  tmp  ./scraping-question.hs
"<div id=\"article\"><div>first</div><div>second</div><script>this should be removed</script><div>third</div></div><div>first</div><div>second</div><div>third</div>"

Итак, это не сработало. Я хотел бы:

  • есть рабочее решение
  • понять рабочее решение

Был бы особенно благодарен, если бы вы помогли мне понять, что не так с моим. Спасибо!


person Konstantine Rybnikov    schedule 30.09.2016    source источник
comment
Я тоже не очень разбираюсь в объективах и не могу ответить на вопрос, но я бы рекомендовал немного разобрать ваш код. Напишите функцию, которая принимает Taggy.Element и возвращает отфильтрованное Taggy.Element. Затем напишите отдельную функцию, обрабатывающую преобразование из/в текст вокруг этого, что, вероятно, можно сделать довольно прямо через призму.   -  person dfeuer    schedule 01.10.2016
comment
(Изящный трюк со стеком shebang — очень полезно для переполнения стека!)   -  person duplode    schedule 05.10.2016


Ответы (1)


Корень вашей проблемы - universe, который сглаживает дерево DOM в список. Если вы снова посмотрите на вывод, вы увидите, что фильтрация работает нормально, но древовидная структура потеряна, и поэтому вы получаете немодифицированный элемент статьи (со всеми дочерними элементами внутри), за которым следуют дочерние узлы без элемента сценария.

Один комбинатор Control.Lens.Plated, который может делать то, что вам нужно, это transform, который преобразует "каждый элемент в дереве снизу вверх":

transform :: Plated a => (a -> a) -> a -> a

В частности, вы можете использовать его для рекурсивной фильтрации дочерних узлов:

renderWithoutScriptTag :: Text
renderWithoutScriptTag =
    let mArticle :: Maybe Taggy.Element
        mArticle =
            (fromText somehtmlSmall) ^? html .
            allAttributed (ix "id" . only "article")
        mArticleFiltered =
            fmap
                (transform (children %~ filter (\n ->
                    n ^? element . name /= Just "script")))
                mArticle
    in maybe "" (toText . Renderer.render) mArticleFiltered
person duplode    schedule 05.10.2016
comment
Благодарю вас! Если вы не возражаете, добавьте это для обзора в документацию SO stackoverflow.com/documentation/haskell/6962/ (может быть не виден, пока не будет принят) - person Konstantine Rybnikov; 05.10.2016
comment
@KostiantynRybnikov Я совсем не против - используйте его по своему усмотрению. - person duplode; 05.10.2016