Как разработать “поисковый робот” с состоянием в Haskell?

Я изучаю Haskell после лет ООП.

Я пишу немой поисковый робот с немногими функциями и состоянием.
Я не уверен, как сделать его правильно в мире FP.

В мире ООП этот паук мог быть разработан как это (использованием):

Browser b = new Browser()
b.goto(“http://www.google.com/”)

String firstLink = b.getLinks()[0]

b.goto(firstLink)
print(b.getHtml())

Этот код загружает http://www.google.com/, затем “нажимает” на первую ссылку, контент загрузок второй страницы и затем печатает содержание.

class Browser {
   goto(url: String) : void // loads HTML from given URL, blocking
   getUrl() : String // returns current URL
   getHtml() : String // returns current HTML
   getLinks(): [String] // parses current HTML and returns a list of available links (URLs)

   private _currentUrl:String
   private _currentHtml:String
}

Это - possbile, чтобы иметь 2 или “браузеры” сразу с его собственным отдельным состоянием:

Browser b1 = new Browser()
Browser b2 = new Browser()

b1.goto(“http://www.google.com/”)
b2.goto(“http://www.stackoverflow.com/”)

print(b1.getHtml())
print(b2.getHtml())

ВОПРОС: покажите, как Вы разработали бы такую вещь в Haskell от scracth (подобный Браузеру API с возможностью иметь несколько независимых экземпляров)? Дайте фрагмент кода.

Примечание: Для простоты пропустите детали о getLinks () функция (его тривиальное и не интересные).
Также давайте предположим, что существует API-функция

getUrlContents :: String -> IO String

это открывает HTTP-соединение и возвращает HTML для данного URL.


ОБНОВЛЕНИЕ: почему иметь состояние (или может быть не)?

API может иметь больше функций, не только единственных "результатов загрузки-и-синтаксического-анализа".
Я не добавил их для предотвращения сложности.

Также это могло заботиться о HTTP о заголовке Referer и cookie путем отправки им с каждым запросом для эмуляции реального поведения браузера.

Рассмотрите следующий сценарий:

  1. Откройте http://www.google.com/
  2. Введите "haskell" в первую входную область
  3. Нажмите кнопку "Google Search"
  4. Нажмите на ссылку "2"
  5. Нажмите на ссылку "3"
  6. Распечатайте HTML текущей страницы (страница 3 результатов Google для "haskell")

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

Browser b = new Browser()
b.goto("http://www.google.com/")
b.typeIntoInput(0, "haskell")
b.clickButton("Google Search") // b.goto(b.finButton("Google Search"))
b.clickLink("2") // b.goto(b.findLink("2"))
b.clickLink("3")
print(b.getHtml())

Цель этого сценария состоит в том, чтобы получить HTML последней страницы после ряда операций. Другая менее видимая цель состоит в том, чтобы сохранить код компактным.

Если Браузер имеет состояние, он может отправить HTTP заголовок Referer и cookie при сокрытии всей механики в себе и предоставлении хорошего API.

Если Браузер не будет иметь никакого состояния, то разработчик, вероятно, раздаст весь текущий URL/HTML/cookie - и это добавляет шум к коду сценария.

Примечание: Я предполагаю, что существуют библиотеки снаружи для фрагментирования HTML в Haskell, но мое намерение не состояло в том, чтобы фрагментировать HTML, но изучить, как эти "черные помещенные в коробку" вещи могут быть разработаны правильно в Haskell.

19
задан oshyshko 21 February 2010 в 21:56
поделиться

4 ответа

Как вы описываете проблему, в состоянии нет необходимости вообще:

data Browser = Browser { getUrl :: String, getHtml :: String, getLinks :: [String]} 

getLinksFromHtml :: String -> [String] -- use Text.HTML.TagSoup, it should be lazy

goto :: String -> IO Browser
goto url = do
             -- assume getUrlContents is lazy, like hGetContents
             html <- getUrlContents url 
             let links = getLinksFromHtml html
             return (Browser url html links)

Возможно иметь 2 или «браузера» одновременно, с их собственное отдельное состояние:

Очевидно, вы можете иметь столько, сколько хотите, и они не могут мешать друг другу.

Теперь эквивалент ваших сниппетов. Первое:

htmlFromGooglesFirstLink = do
                              b <- goto "http://www.google.com"
                              let firstLink = head (links b)
                              b2 <- goto firstLink -- note that a new browser is returned
                              putStr (getHtml b2)

И второе:

twoBrowsers = do
                b1 <- goto "http://www.google.com"
                b2 <- goto "http://www.stackoverflow.com/"
                putStr (getHtml b1)
                putStr (getHtml b2)

ОБНОВЛЕНИЕ (ответ на ваше обновление):

Если у браузера есть состояние, он может отправлять HTTP-заголовок Referer и файлы cookie, скрывая при этом всю механику внутри себя и предоставляя приятный API.

Нет необходимости в состоянии, goto может просто принимать аргумент браузера. Во-первых, нам нужно расширить тип:

data Browser = Browser { getUrl :: String, getHtml :: String, getLinks :: [String], 
                         getCookies :: Map String String } -- keys are URLs, values are cookie strings

getUrlContents :: String -> String -> String -> IO String
getUrlContents url referrer cookies = ...

goto :: String -> Browser -> IO Browser
goto url browser = let
                     referrer = getUrl browser 
                     cookies = getCookies browser ! url
                   in 
                   do 
                     html <- getUrlContents url referrer cookies
                     let links = getLinksFromHtml html
                     return (Browser url html links)

newBrowser :: Browser
newBrowser = Browser "" "" [] empty

Если у браузера нет состояния, разработчик, скорее всего, передаст все текущие URL / HTML / файлы cookie - и это добавляет шума в код сценария.

Нет, вы просто передаете значения типа Browser. Например,

useGoogle :: IO ()
useGoogle = do
              b <- goto "http://www.google.com/" newBrowser
              let b2 = typeIntoInput 0 "haskell" b
              b3 <- clickButton "Google Search" b2
              ...

Или вы можете избавиться от этих переменных:

(>>~) = flip mapM -- use for binding pure functions

useGoogle = goto "http://www.google.com/" newBrowser >>~
            typeIntoInput 0 "haskell" >>=
            clickButton "Google Search" >>=
            clickLink "2" >>=
            clickLink "3" >>~
            getHtml >>=
            putStr

Выглядит достаточно хорошо? Обратите внимание, что браузер по-прежнему неизменен.

12
ответ дан 30 November 2019 в 04:48
поделиться

Не пытайтесь воспроизвести многим объектно-ориентированным.

Просто определите простой тип браузера , который содержит текущий URL (на IORef ради изменяемости) и некоторые функции IO для обеспечения доступа и модификации. функциональность.

Пример программы будет выглядеть так:

import Control.Monad

do
   b1 <- makeBrowser "google.com"
   b2 <- makeBrowser "stackoverflow.com"

   links <- getLinks b1

   b1 `navigateTo` (head links)

   print =<< getHtml b1
   print =<< getHtml b2

Обратите внимание, что если вы определите вспомогательную функцию типа o # f = fo , у вас будет синтаксис, более похожий на объект (например, b1 # getLinks ).

Полные определения типов:

data Browser = Browser { currentUrl :: IORef String }

makeBrowser  :: String -> IO Browser

navigateTo   :: Browser -> String -> IO ()
getUrl       :: Browser -> IO String
getHtml      :: Browser -> IO String
getLinks     :: Browser -> IO [String]
3
ответ дан 30 November 2019 в 04:48
поделиться

Функция getUrlContents уже делает то, что goto() и getHtml(), не хватает только функции, которая извлекает ссылки из загруженной страницы. Она может принимать строку (HTML страницы) и URL (для разрешения относительных ссылок) и извлекать все ссылки с этой страницы:

getLinks :: String -> String -> [String]

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

getFirstLinked :: String -> IO String
getFirstLinked url =
   do page <- getUrlContents url
      getUrlContents (head (getLinks page url))

Простая функция для загрузки всех ссылок с URL может выглядеть так:

allPages :: String -> IO [String]
allPages url =
   do page <- getUrlContent url
      otherpages <- mapM getUrlContent (getLinks page url)
      return (page : otherpages)

(Обратите внимание, что эта функция будет бесконечно повторять циклы в ссылках - функция для реального использования должна позаботиться о таких случаях)

Единственное "состояние", которое используется этими функциями - это URL, и оно просто передается соответствующим функциям в качестве параметра.

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

data BrowseInfo = BrowseInfo
     { getUrl     :: String
     , getProxy   :: ProxyInfo
     , getMaxSize :: Int
     }

Функции, использующие эту информацию, могли бы просто принимать параметр этого типа и использовать содержащуюся в нем информацию. Нет никакой проблемы в том, чтобы иметь много экземпляров этих объектов и использовать их одновременно, каждая функция будет просто использовать объект, который ей дан в качестве параметра.

3
ответ дан 30 November 2019 в 04:48
поделиться

Они одинаковы.

HTM использовался, поскольку некоторые системы не поддерживают четырехсимвольные расширения.
Это больше не так, и больше нет причин использовать HTM.

-121--2489297-

Попробуйте:

Time.parse(target_date) + curr_time.sec + curr_time.min * 60 + curr_time.hour * 60 * 60
=> Sat Apr 17 19:30:34 +0200 2010

Вы получите DateTime с датой от target_date и временем от curr_time.

-121--3060836-

показать, как бы вы разработали такую штуку в Haskell с нуля (похожий на браузер API с возможностью иметь несколько независимых экземпляров)? Пожалуйста, дайте фрагмент кода.

Я бы использовал один (Haskell) поток в каждой точке, чтобы все потоки работали в государственной монаде с рекордным типом любых ресурсов, которые им нужны, и чтобы результаты передавались обратно в главный поток по каналу.

Добавьте больше параллелизма! Это способ FP.

Если я правильно помню, здесь есть дизайн для банд потоков проверки связей, взаимодействующих по каналам:

Также, убедитесь, что не использовать Последовательностей, но Text или ByteStrings - они будут намного быстрее.

2
ответ дан 30 November 2019 в 04:48
поделиться
Другие вопросы по тегам:

Похожие вопросы: