2016-07-02 14 views
27

sto cercando di risolvere un download automation script che fornisco pubblicamente in modo che chiunque può facilmente scaricare il World Values ​​con R.Come scaricare un file dietro un sondaggio semi-rotto javascript funzione di asp con R

In questa pagina web - http://www.worldvaluessurvey.org/WVSDocumentationWV4.jsp - il collegamento PDF "WVS_2000_Questionnaire_Root" si scarica facilmente in firefox e chrome.Non riesco a capire come automatizzare il download con httr o RCurl o qualsiasi altro pacchetto R. screenshot sotto il comportamento di internet di Chrome. Quel collegamento PDF deve seguire fino alla fonte ultima di http://www.worldvaluessurvey.org/wvsdc/DC00012/F00001316-WVS_2000_Questionnaire_Root.pdf ma se si fa clic direttamente su di essi, c'è un errore di connettività. io sono chiaro se questo è legato alla richiesta di intestazione Upgrade-Insecure-Requests:1 o il codice di stato intestazione di risposta 302

cliccando in giro il nuovo sito worldvaluessurvey.org con bicromato di potassio di ispezionare le finestre aperte elemento mi fa pensare ci fossero alcune decisioni di codifica hacky fatte qui, da qui il titolo semi-rotti:/

enter image description here

+2

wow, abbastanza impressionante per sacrificare quasi tutta la tua reputazione in una sola domanda! ;-) – agenis

+1

Heck; Sarei felice di supportare una risposta utile a questa domanda. Se non trovi una soluzione e la assegni nel tempo richiesto, fammelo sapere e inserirò altri 500 rappresentanti per assicurarti che rimangano in primo piano. Grazie per tutto il tuo lavoro nel rendere accessibili i set di dati pubblici, Anthony. –

+0

@ 42- grazie mille David, lo apprezzo. la risposta di headless browsing è buona, ma il poster ha ragione che sarebbe meglio solo all'interno di -R. sono preoccupato che qualcuno darà una buona risposta a 'RCurl' e poi il sondaggio sui valori del mondo la gente cambierà di nuovo il sito .. rischio professionale;) –

risposta

4

Utilizzando l'eccellente curlconverter per simulare il browser è possibile richiedere direttamente il pdf.

In primo luogo abbiamo imitare il browser richiesta iniziale GET (potrebbe non essere necessario un semplice GET e mantenendo il cookie può essere sufficiente):

library(curlconverter) 
library(httr) 
browserGET <- "curl 'http://www.worldvaluessurvey.org/WVSDocumentationWV4.jsp' -H 'Host: www.worldvaluessurvey.org' -H 'User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:49.0) Gecko/20100101 Firefox/49.0' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' -H 'Accept-Language: en-US,en;q=0.5' --compressed -H 'Connection: keep-alive' -H 'Upgrade-Insecure-Requests: 1'" 
getDATA <- (straighten(browserGET) %>% make_req)[[1]]() 

Il cookie JSESSIONID è disponibile presso getDATA$cookies$value

getPDF <- "curl 'http://www.worldvaluessurvey.org/wvsdc/DC00012/F00001316-WVS_2000_Questionnaire_Root.pdf' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' -H 'Accept-Encoding: gzip, deflate' -H 'Accept-Language: en-US,en;q=0.5' -H 'Connection: keep-alive' -H 'Cookie: JSESSIONID=59558DE631D107B61F528C952FC6E21F' -H 'Host: www.worldvaluessurvey.org' -H 'Referer: http://www.worldvaluessurvey.org/AJDocumentationSmpl.jsp' -H 'Upgrade-Insecure-Requests: 1' -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:49.0) Gecko/20100101 Firefox/49.0'" 
appIP <- straighten(getPDF) 
# replace cookie 
appIP[[1]]$cookies$JSESSIONID <- getDATA$cookies$value 
appReq <- make_req(appIP) 
response <- appReq[[1]]() 
writeBin(response$content, "test.pdf") 

Il le stringhe sono state strappate direttamente dal browser e curlconverter fa tutto il lavoro.

+0

sì! grazie –

+0

@AnthonyDamico: abbiamo bisogno di ricostituire il tuo rappresentante o proteggere il tuo nuovo rappresentante. Se vuoi che sponsorizzi le domande, fammelo sapere. e-mail, dal momento che ho cambiato il mio nome SO da DWin. –

+0

grazie mille! Se la mia prossima domanda viene visualizzata prima che la mia reputazione si ripristini, ti invierò una nota: D –

0

il tuo problema è probabilmente causato dal codice 302 di stato. Potrei spiegare che cos'è un codice 302, ma sembra che tu possa trarre beneficio da una spiegazione dell'intero processo di download:

Questo è ciò che accade quando un utente fa clic su quel link pdf.

  1. L'evento javascript onclick viene attivato per quel collegamento. Se fai clic con il pulsante destro del mouse sul link e fai clic su "Inspect Element", puoi vedere che c'è un evento onclick impostato su "DocDownload ('1316')". inline javascript onclick event.
  2. Se scriviamo DocDownload nella console javascript, il browser ci dice che DocDownload non esiste come funzione. enter image description here
  3. Questo perché il collegamento pdf si trova all'interno di un iframe all'interno della finestra enter image description here. La console dev in un browser accede solo le variabili/funzioni
+2

grazie. domanda è: come potrebbe essere scaricato il file con R? –

3

Guardando il codice per la funzione DocDownload, essi sono principalmente solo facendo un post per /AJDownload.jsp con params post delle ulthost: WVS, CndWAVE: 4, SAID: 0, DOID: (l'id doc qui), AJArchive: Archivio dati WVS. Non sono sicuro se alcuni di questi sono richiesti, ma probabilmente è meglio includerli comunque.

farlo in R utilizzando HTTR, sarebbe simile a questa

r <- POST("http://www.worldvaluessurvey.org/AJDownload.jsp", body = list("ulthost" = "WVS", "CndWAVE" = 4, "SAID" = 0, "DOID" = 1316, "AJArchive" = "WVS Data Archive")) 

L'AJDownload.asp endpoint restituirà una 302 (reindirizzamento all'URL REAL), e la biblioteca HTTR dovrebbe seguire automaticamente il reindirizzamento per tu. Attraverso prove ed errori, ho determinato che il server richiede sia intestazioni Content-Type che Cookie, altrimenti restituirà una risposta vuota 400 (OK). Dovrai ottenere un cookie valido, che puoi trovare ispezionando qualsiasi pagina caricata su quel server, e cercare l'intestazione con Cookie: JSESSIONID = ....., vorrai copiare l'intera intestazione

quindi, con quelli, sembra che

r <- POST("http://www.worldvaluessurvey.org/AJDownload.jsp", body = list("ulthost" = "WVS", "CndWAVE" = 4, "SAID" = 0, "DOID" = 1316, "AJArchive" = "WVS Data Archive"), add_headers("Content-Type" = "application/x-www-form-urlencoded", "Cookie" = "[PASTE COOKIE VALUE HERE]")) 

La risposta sta per essere dati in formato PDF binari, quindi sarà necessario per salvarlo in un file per essere in grado di fare qualsiasi cosa con esso.

bin <- content(r, "raw") 
writeBin(bin, "myfile.txt") 

EDIT:

Va bene, ha ottenuto un po 'di tempo per eseguire in realtà il codice. Ho anche scoperto i parametri minimi richiesti per le chiamate POST, che è solo il docid, il cookie JSESSIONID e l'intestazione Referer.

library(httr) 
download_url <- "http://www.worldvaluessurvey.org/AJDownload.jsp" 
frame_url <- "http://www.worldvaluessurvey.org/AJDocumentationSmpl.jsp" 
body <- list("DOID" = "1316") 

file_r <- POST(download_url, body = body, encode = "form", 
      set_cookies("JSESSIONID" = "0E657C37FF030B41C33B7D2B1DCAB3D8"), 
      add_headers("Referer" = frame_url), 
      verbose()) 

Questo ha funzionato sulla mia macchina e restituisce correttamente i dati binari PDF.

Questo è ciò che accade se imposto manualmente il cookie dal mio browser. Sto solo usando la parte JSESSIONID del cookie e nient'altro. Come ho detto prima, il JSESSIONID scadrà, probabilmente per età o inattività. success_image

+0

grazie ma ho provato il tuo esempio in modi diversi e nessuno di loro ha funzionato? 'library (httr); r <- POST ("http://www.worldvaluessurvey.org/AJDownload.jsp", body = list ("ulthost" = "WVS", "CndWAVE" = 4, "SAID" = 0, "DOID" = 1316 , "AJArchive" = "Archivio dati WVS")); x <- POST ("http://www.worldvaluessurvey.org/AJDownload.jsp", body = list ("ulthost" = "WVS", "CndWAVE" = 4, "SAID" = 0, "DOID" = 1316 , "AJArchive" = "Archivio dati WVS"), add_headers ("Content-Type" = "application/x-www-form-urlencoded", "JSESSIONID" = cookie (r) $ valore)); x $ content' –

+0

Ho aggiornato con il codice che ho eseguito localmente e lavorato. Dai una prova alle modifiche. Anche qualcosa da notare, il cookie JSESSIONID può e scadrà. Quindi vorrai assicurarti di usarne uno usato di recente. Per automatizzare ulteriormente in futuro è possibile richiedere una pagina (come la lista, o anche la home page) per prima, ed è possibile estrarre il cookie dalla risposta. –

+0

Mi dispiace, il tuo esempio non funziona ancora per me? potresti rivedere la tua risposta in modo da automatizzare il processo (incluso il richiamo di '$ $ cookie $ valore ') dall'inizio alla fine e viene eseguito in modo pulito in una nuova sessione R? –

6

Ho avuto a che fare con questo genere di cose in passato. La mia soluzione è stata quella di utilizzare uno headless browser per navigare e manipolare le pagine Web che contenevano le risorse che mi interessavano a livello di programmazione. Ho anche svolto attività piuttosto semplici come l'accesso e la compilazione e l'invio di moduli utilizzando questo metodo.

Vedo che si sta tentando di utilizzare un approccio R puro per scaricare questi file mediante il reverse engineering delle richieste GET/POST generate dal collegamento. Questo potrebbe funzionare, ma lascerebbe la tua implementazione molto vulnerabile a eventuali cambiamenti futuri nella progettazione del sito, come cambiamenti nel gestore di eventi JavaScript, reindirizzamenti URL o requisiti di intestazione.

Utilizzando un browser headless è possibile limitare l'esposizione all'URL di livello superiore e alcune query XPath minime che consentono la navigazione verso il collegamento di destinazione. Certo, questo lega ancora il tuo codice a dettagli non contrattuali e abbastanza interni del design del sito, ma è certamente meno un'esposizione. Questo è il rischio del web scraping.


Ho sempre usato la libreria Java HtmlUnit per la mia navigazione senza testa, che ho trovato ad essere abbastanza eccellente. Naturalmente, per sfruttare una soluzione basata su Java di Rland sarebbe necessario generare un processo Java, che richiederebbe (1) Java da installare sul computer dell'utente, (2) il $CLASSPATH da impostare correttamente per individuare i JAR HtmlUnit come come il tuo file personalizzato, il download della classe principale e (3) l'invocazione corretta del comando Java con argomenti corretti utilizzando uno dei metodi di R per eseguire il bombardamento su un comando di sistema. Inutile dire che questo è abbastanza complicato e disordinato.

Una soluzione di ricerca headless in puro R sarebbe piacevole, ma sfortunatamente, mi sembra che R non offra alcuna soluzione nativa per la navigazione senza headless. Il più vicino è RSelenium, che sembra essere solo un binding R alla libreria client Java del software di automazione del browser Selenium. Ciò significa che non funzionerà indipendentemente dal browser della GUI dell'utente e richiede comunque l'interazione con un processo Java esterno (sebbene in questo caso i dettagli dell'interazione siano opportunamente incapsulati sotto l'API di RSelenium).


Utilizzando HtmlUnit, ho creato una classe principale abbastanza generico Java che può essere utilizzato per scaricare un file facendo clic su un link in una pagina web. La parametrizzazione dell'applicazione è la seguente:

  • L'URL della pagina.
  • Una sequenza opzionale di espressioni XPath per consentire la discesa in un numero qualsiasi di fotogrammi nidificati a partire dalla pagina di livello superiore.Nota: in realtà lo analizzo dall'argomento dell'URL dividendolo su \s*>\s*, che mi piace come sintassi concisa. Ho usato il carattere > perché non è valido negli URL.
  • Una singola espressione XPath che specifica il collegamento di ancoraggio per fare clic.
  • Un nome file facoltativo sotto il quale salvare il file scaricato. Se omesso, sarà derivato da un'intestazione Content-Disposition il cui valore corrisponde al modello filename="(.*)" (questo era un caso insolito che ho riscontrato durante lo scraping delle icone qualche istante fa) o, in mancanza, il nome di base dell'URL di richiesta che ha attivato la risposta del flusso di file. . Il metodo di derivazione basename funziona per il tuo link di destinazione.

Ecco il codice:

package com.bgoldst; 

import java.util.List; 
import java.util.ArrayList; 

import java.io.File; 
import java.io.FileOutputStream; 
import java.io.InputStream; 
import java.io.OutputStream; 
import java.io.IOException; 

import java.util.regex.Pattern; 
import java.util.regex.Matcher; 

import com.gargoylesoftware.htmlunit.WebClient; 
import com.gargoylesoftware.htmlunit.BrowserVersion; 
import com.gargoylesoftware.htmlunit.ConfirmHandler; 
import com.gargoylesoftware.htmlunit.WebWindowListener; 
import com.gargoylesoftware.htmlunit.WebWindowEvent; 
import com.gargoylesoftware.htmlunit.WebResponse; 
import com.gargoylesoftware.htmlunit.WebRequest; 
import com.gargoylesoftware.htmlunit.util.NameValuePair; 
import com.gargoylesoftware.htmlunit.Page; 
import com.gargoylesoftware.htmlunit.html.HtmlPage; 
import com.gargoylesoftware.htmlunit.html.HtmlAnchor; 
import com.gargoylesoftware.htmlunit.html.BaseFrameElement; 

public class DownloadFileByXPath { 

    public static ConfirmHandler s_downloadConfirmHandler = null; 
    public static WebWindowListener s_downloadWebWindowListener = null; 
    public static String s_saveFile = null; 

    public static void main(String[] args) throws Exception { 

     if (args.length < 2 || args.length > 3) { 
      System.err.println("usage: {url}[>{framexpath}*] {anchorxpath} [{filename}]"); 
      System.exit(1); 
     } // end if 
     String url = args[0]; 
     String anchorXPath = args[1]; 
     s_saveFile = args.length >= 3 ? args[2] : null; 

     // parse the url argument into the actual URL and optional subsequent frame xpaths 
     String[] fields = Pattern.compile("\\s*>\\s*").split(url); 
     List<String> frameXPaths = new ArrayList<String>(); 
     if (fields.length > 1) { 
      url = fields[0]; 
      for (int i = 1; i < fields.length; ++i) 
       frameXPaths.add(fields[i]); 
     } // end if 

     // prepare web client to handle download dialog and stream event 
     s_downloadConfirmHandler = new ConfirmHandler() { 
      public boolean handleConfirm(Page page, String message) { 
       return true; 
      } 
     }; 
     s_downloadWebWindowListener = new WebWindowListener() { 
      public void webWindowContentChanged(WebWindowEvent event) { 

       WebResponse response = event.getWebWindow().getEnclosedPage().getWebResponse(); 

       //System.out.println(response.getLoadTime()); 
       //System.out.println(response.getStatusCode()); 
       //System.out.println(response.getContentType()); 

       // filter for content type 
       // will apply simple rejection of spurious text/html responses; could enhance this with command-line option to whitelist 
       String contentType = response.getResponseHeaderValue("Content-Type"); 
       if (contentType.contains("text/html")) return; 

       // determine file name to use; derive dynamically from request or response headers if not specified by user 
       // 1: user 
       String saveFile = s_saveFile; 
       // 2: response Content-Disposition 
       if (saveFile == null) { 
        Pattern p = Pattern.compile("filename=\"(.*)\""); 
        Matcher m; 
        List<NameValuePair> headers = response.getResponseHeaders(); 
        for (NameValuePair header : headers) { 
         String name = header.getName(); 
         String value = header.getValue(); 
         //System.out.println(name+" : "+value); 
         if (name.equals("Content-Disposition")) { 
          m = p.matcher(value); 
          if (m.find()) 
           saveFile = m.group(1); 
         } // end if 
        } // end for 
        if (saveFile != null) saveFile = sanitizeForFileName(saveFile); 
        // 3: request URL 
        if (saveFile == null) { 
         WebRequest request = response.getWebRequest(); 
         File requestFile = new File(request.getUrl().getPath()); 
         saveFile = requestFile.getName(); // just basename 
        } // end if 
       } // end if 

       getFileResponse(response,saveFile); 

      } // end webWindowContentChanged() 
      public void webWindowOpened(WebWindowEvent event) {} 
      public void webWindowClosed(WebWindowEvent event) {} 
     }; 

     // initialize browser 
     WebClient webClient = new WebClient(BrowserVersion.FIREFOX_45); 
     webClient.getOptions().setCssEnabled(false); 
     webClient.getOptions().setJavaScriptEnabled(true); // required for JavaScript-powered links 
     webClient.getOptions().setThrowExceptionOnScriptError(false); 
     webClient.getOptions().setThrowExceptionOnFailingStatusCode(false); 

     // 1: get home page 
     HtmlPage page; 
     try { page = webClient.getPage(url); } catch (IOException e) { throw new Exception("error: could not get URL \""+url+"\".",e); } 
     //page.getEnclosingWindow().setName("main window"); 

     // 2: navigate through frames as specified by the user 
     for (int i = 0; i < frameXPaths.size(); ++i) { 
      String frameXPath = frameXPaths.get(i); 
      List<?> elemList = page.getByXPath(frameXPath); 
      if (elemList.size() != 1) throw new Exception("error: frame "+(i+1)+" xpath \""+frameXPath+"\" returned "+elemList.size()+" elements on page \""+page.getTitleText()+"\" >>>\n"+page.asXml()+"\n<<<."); 
      if (!(elemList.get(0) instanceof BaseFrameElement)) throw new Exception("error: frame "+(i+1)+" xpath \""+frameXPath+"\" returned a non-frame element on page \""+page.getTitleText()+"\" >>>\n"+page.asXml()+"\n<<<."); 
      BaseFrameElement frame = (BaseFrameElement)elemList.get(0); 
      Page enclosedPage = frame.getEnclosedPage(); 
      if (!(enclosedPage instanceof HtmlPage)) throw new Exception("error: frame "+(i+1)+" encloses a non-HTML page."); 
      page = (HtmlPage)enclosedPage; 
     } // end for 

     // 3: get the target anchor element by xpath 
     List<?> elemList = page.getByXPath(anchorXPath); 
     if (elemList.size() != 1) throw new Exception("error: anchor xpath \""+anchorXPath+"\" returned "+elemList.size()+" elements on page \""+page.getTitleText()+"\" >>>\n"+page.asXml()+"\n<<<."); 
     if (!(elemList.get(0) instanceof HtmlAnchor)) throw new Exception("error: anchor xpath \""+anchorXPath+"\" returned a non-anchor element on page \""+page.getTitleText()+"\" >>>\n"+page.asXml()+"\n<<<."); 
     HtmlAnchor anchor = (HtmlAnchor)elemList.get(0); 

     // 4: click the target anchor with the appropriate confirmation dialog handler and content handler 
     webClient.setConfirmHandler(s_downloadConfirmHandler); 
     webClient.addWebWindowListener(s_downloadWebWindowListener); 
     anchor.click(); 
     webClient.setConfirmHandler(null); 
     webClient.removeWebWindowListener(s_downloadWebWindowListener); 

     System.exit(0); 

    } // end main() 

    public static void getFileResponse(WebResponse response, String fileName) { 

     InputStream inputStream = null; 
     OutputStream outputStream = null; 

     // write the inputStream to a FileOutputStream 
     try { 

      System.out.print("streaming file to disk..."); 

      inputStream = response.getContentAsStream(); 

      // write the inputStream to a FileOutputStream 
      outputStream = new FileOutputStream(new File(fileName)); 

      int read = 0; 
      byte[] bytes = new byte[1024]; 

      while ((read = inputStream.read(bytes)) != -1) 
       outputStream.write(bytes, 0, read); 

      System.out.println("done"); 

     } catch (IOException e) { 
      e.printStackTrace(); 
     } finally { 
      if (inputStream != null) { 
       try { 
        inputStream.close(); 
       } catch (IOException e) { 
        e.printStackTrace(); 
       } // end try-catch 
      } // end if 
      if (outputStream != null) { 
       try { 
        //outputStream.flush(); 
        outputStream.close(); 
       } catch (IOException e) { 
        e.printStackTrace(); 
       } // end try-catch 
      } // end if 
     } // end try-catch 

    } // end getFileResponse() 

    public static String sanitizeForFileName(String unsanitizedStr) { 
     return unsanitizedStr.replaceAll("[^\040-\176]","_").replaceAll("[/\\<>|:*?]","_"); 
    } // end sanitizeForFileName() 

} // end class DownloadFileByXPath 

seguito è una demo di me in esecuzione della classe principale sul mio sistema. Ho tagliato la maggior parte dell'output prolisso di HtmlUnit. Spiegherò in seguito gli argomenti della riga di comando.

ls; 
## bin/ src/ 
CLASSPATH="bin;C:/cygwin/usr/local/share/htmlunit-latest/*" java com.bgoldst.DownloadFileByXPath "http://www.worldvaluessurvey.org/WVSDocumentationWV4.jsp > //iframe[@id='frame1'] > //iframe[@id='frameDoc']" "//a[contains(text(),'WVS_2000_Questionnaire_Root')]"; 
## Jul 10, 2016 1:34:34 PM com.gargoylesoftware.htmlunit.IncorrectnessListenerImpl notify 
## WARNING: Obsolete content type encountered: 'application/x-javascript'. 
## Jul 10, 2016 1:34:34 PM com.gargoylesoftware.htmlunit.IncorrectnessListenerImpl notify 
## WARNING: Obsolete content type encountered: 'application/x-javascript'. 
## 
## ... snip ... 
## 
## Jul 10, 2016 1:34:45 PM com.gargoylesoftware.htmlunit.IncorrectnessListenerImpl notify 
## WARNING: Obsolete content type encountered: 'text/javascript'. 
## streaming file to disk...done 
## 
ls; 
## bin/ F00001316-WVS_2000_Questionnaire_Root.pdf* src/ 
  • CLASSPATH="bin;C:/cygwin/usr/local/share/htmlunit-latest/*" Qui ho impostato il $CLASSPATH per il mio sistema utilizzando un prefisso variabile di assegnazione (nota: stavo correndo nella shell bash Cygwin). Il file .class è stato compilato in bin e ho installato i JAR HtmlUnit nella struttura della directory di sistema Cygwin, che è probabilmente leggermente inusuale.
  • java com.bgoldst.DownloadFileByXPath Ovviamente questa è la parola di comando e il nome della classe principale da eseguire.
  • "http://www.worldvaluessurvey.org/WVSDocumentationWV4.jsp > //iframe[@id='frame1'] > //iframe[@id='frameDoc']" Questo è l'URL e le espressioni XPath del frame. Il tuo link di destinazione è annidato sotto due iframe, richiedendo quindi le due espressioni XPath. È possibile trovare gli attributi id nella sorgente, visualizzando il codice HTML grezzo o utilizzando uno strumento di sviluppo Web (Firebug è il mio preferito).
  • "//a[contains(text(),'WVS_2000_Questionnaire_Root')]" Infine, questa è l'espressione XPath effettiva per il collegamento di destinazione all'interno dell'iframe interno.

Ho omesso l'argomento del nome file. Come puoi vedere, il codice ha derivato correttamente il nome del file dall'URL della richiesta.


riconosco che questo è un sacco di problemi a passare attraverso per scaricare un file, ma per il web scraping in generale, credo davvero che l'unico approccio robusto e praticabile è quello di andare il tutto nove cantieri e utilizzare un motore di browser senza testa completo. Potrebbe essere meglio separare completamente il compito di scaricare questi file da Rland, e invece implementare l'intero sistema di scraping usando un'applicazione Java, magari integrata con alcuni script di shell per un front end più flessibile. A meno che tu non stia lavorando con URL di download progettati per richieste HTTP one-shot senza fronzoli da parte di client come curl, wget e R, l'utilizzo di R per il web scraping non è probabilmente una buona idea. Sono i miei due centesimi.

Problemi correlati