Salta el contingut

3. Peticions HTTP. API REST

El protocol HTPP

Hui en dia tot el núvol d'aplicacions i la web està gestionat, entre altres pel protocol HTTP i altres serveis que permeten estar interconnectats.

HTTP (Hiper Text Transfer Protocol) es basa en senzilles operacions de sol·licitud/resposta:

  1. Un client estableix una connexió amb un servidor i envia un missatge amb les dades de la sol·licitud. Aquesta petició s'anomena request i destaquen 4 tipus de peticions:
  2. GET, per a demanar algun recurs al servidor
  3. POST, per a desar algun recurs al servidor
  4. PUT, per a modificar algun recurs ja existent al resvidor
  5. DELETE, per a esborrar algun recurs
  6. El servidor respon amb un missatge similar, que conté l'estat de l'operació i el seu possible resultat. Aquesta resposta s'anomena response, formada, com hem dit per:
  7. Els possibles estat son un codi que indica el resultat de la petició. Infomració més detallada a la wikipedia
    1. 1XX Respostes informatives
    2. 2XX Peticions correctes
    3. 3XX Redireccions
    4. 4XX Errors provocats pel client
    5. 5XX Errors provocats dins del servidor
  8. El recurs sol·licitad. Habitualmente serà una pàgina web (HTML més la hipermèdia), un arxiu JSON amb dades, un arxiu PDF, etc.

Serveis API REST

Anem a definir una API REST de manera molt bàsica per a poder-ho entendre ràpidament:

  • S'anomena un servei tot allò que ens pot donar un servidor. Dades, pàgines, arxius, l'hora, etc.
  • API és l'acrònim de Application Programming Interfaces i indica tot allò que ens pot oferir un servidor, així com la sintaxi que hem de fer sevir per a demana-li les coses

    Dins del llenguatges de programació, la API son les llibreries que disposa i com hem de invocar a les funcions de dites llibreries.

  • Una API REST (Representational State Transfer) és un conjunt de regles i convencions arquitectòniques per dissenyar serveis web basats en el protocol HTTP. Cap destacar que REST implica:

  • Fer servir HTTP, i per tant identificar la seua API mitjançant:
    • Les URL on es demana la petició (ruta al servidor)
    • El tipus o mètode de la petició (GET, POST, etc.)
  • Que siga Stateless o sense memòria. Una petició serà atesa per ella mateixa, sense recordar res de les anteriors peticions.
  • Estat Representacional: es refereix a l'estat del recurs demanat en el moment de la petició, i ve expresat en XML o actualment en JSON
  • Seguretat (amb JWT) i hipermèdia (HAETOAS, amb enllaços als recursos relacionats)

Consumint recursos de internet. La llibreria request

És en aquest apartat on anem a estudiar com fer peticions a recursos de internet, mitjançant les peticions vistes anteriorment amb http. Aquestes peticinos s'engloben dins de la llibreria request. Els recursos poden ser be:

  • Una pàgina web
  • Una imatge o arxiu ubicat en algun lloc
  • Una API rest a la que demanarem/enviarem algun recurs

El fluxe de treball serà:

  1. Composar la URL de destí
  2. Afegir a la petició arguments o paràmetres
  3. Llançar la petició
  4. Arreplegar la resposta (response)
  5. Avaluar el codi d'estat per saber com ha anat
  6. Recuperar el cos de la resposta (JSON, Arxius, HTML, etc..)
Petició bàsica amb request
Python
1
2
3
4
5
6
7
8
import requests

resposta=requests.get('https://swapi.dev/api/films/1/')

print(type(resposta))
print(resposta.status_code)
print(resposta.headers)
print(resposta.json())

Com pot observar-se:

  • Es crea tot a partir del objecte requests. Aquest mètode te tants mètodes com tipus de peticions http, encara que sols veurem les 4 bàsiques. Rep com a paràmetre la url que volem fer la petició.
  • Ens retorna un objecte de tipus Response, el qual encapsula la resposta del servidor:
  • Response.status_code amb el codi html de com ha anat la petició
  • Response.headers la capçalera en la metainformació de la resposta. Per exemple:
    JSON
    {
        'Server': 'nginx/1.16.1', 
        'Date': 'Mon, 18 Sep 2023 08:11:28 GMT', 
        'Content-Type': 'application/json', 
        'Transfer-Encoding': 'chunked', '
        Connection': 'keep-alive', 
        'Vary': 'Accept, Cookie', 
        'X-Frame-Options': 'SAMEORIGIN', 
        'ETag': '"5af12d8e740a258e0c502b480ae78f2f"', 
        'Allow': 'GET, HEAD, OPTIONS', 'Strict-Transport-Security': 
        'max-age=15768000'
     }
    
  • Response.content el contingut de la resposta, directament en bytes. Si la resposta es text, s'interpreta directament.
  • Response.text la resposta en format text.
  • Response.json(), quan la resposta conté un json, ens permet parsejar-la directament i guarda-la dins d'un objecte:
    JSON
    1
    2
    3
    4
    5
    6
    7
    {
       'title': 'A New Hope', 
       'episode_id': 4, 
       'opening_crawl': "It is a period of civil war.\r\nRebel spaceships, striking\r\nfrom a hidden base, have won\r\ntheir first victory against\r\nthe evil Galactic Empire.\r\n\r\nDuring the battle, Rebel\r\nspies managed to steal secret\r\nplans to the Empire's\r\nultimate weapon, the DEATH\r\nSTAR, an armored space\r\nstation with enough power\r\nto destroy an entire planet.\r\n\r\nPursued by the Empire's\r\nsinister agents, Princess\r\nLeia races home aboard her\r\nstarship, custodian of the\r\nstolen plans that can save her\r\npeople and restore\r\nfreedom to the galaxy....", 
       'director': 'George Lucas', 
       ...
       }
    
Descarregant una imatge de la web
Python
from PIL import Image
from io import BytesIO
import requests

r=requests.get('https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Python-logo-notext.svg/701px-Python-logo-notext.svg.png')

# print(r.json()) Donaria error
imatge = Image.open(BytesIO(r.content))
imatge.show()
imatge.save("Python.png")

A l'anterior exemple:

  • La petició get() és a un recurs gràfic.
  • El content es passem per la classe BytesIO que ens permet tractar-lo com un stream de bytes, el que ens permet passar-ho a:
  • el mètode open() de la classe Image de la llibreria PIL (Python Image Library), dedicada a la manipulació d'imatges.
  • La imatge la podem obrir e inclòs guardar-la a un fitxer al nostre sistema.
Repte

Busca a la documentació com implementaries la descarrega d'un fitxer de qualssevol tipus, un pdf o un zip, i implementa-ho

Afegint paràmetres i recursos

De vegades (i molt sovint) voldrem enviar dades al servidor que ens aten la petició, per exemple identificar-nos, penjar un arxiu, marcar una compra a la cistella, etc. Existeixen dos maneres bàsiques de enviar eixa informació, per paràmetres o amb el cos de la petició

Enviar paràmetres

Les dades dins dels paràmetres s'envien afegint al recurs que volem les dades a enviar de la forma parametre=valor darrere d'un símbol d'interrogació (?). Cas d'haver més paràmetres, es concatenen amb l'ampersand (&). Per exemple si volem fer un login, la petició seria algo com http://www.elmeuservidor.com/login?user=pepito&password=123456.

La primera solució per a fer-ho així seria composar la url de manera artesanal:

Python
1
2
3
4
5
user="Pepito"
password="123456"
url="http://www.elmeuservidor.com/login?user="+user+"&password="+password

resposta=request.get(url)

La segona solució, recomanable. és fer servir eines pròpies de la llibreria per a elaborar-ho més fàcil:

Python
1
2
3
4
5
6
7
dades={
   "user":"Pepito"
   "password":"123456"
}
url="http://www.elmeuservidor.com/login"

response = requests.get(url, params=dades)

Si volem afegir dades, al cos de la petició, de la mateixa manera que es fa amb postman o similar, s'ha de fer canviant la crida:

Python
1
2
3
4
5
6
7
dades={
   "user":"Pepito"
   "password":"123456"
}
url="http://www.elmeuservidor.com/login"

response = requests.get(url, json=dades)

Teniu més informació disponible a la pàgina ofical https://requests.readthedocs.io/en/latest/.

Web Scraping

El web scraping, o rascat web en català, és una tècnica utilitzada per extreure informació de llocs web de manera automatitzada. En altres paraules, es tracta de processos automatitzats que naveguen per pàgines web, descarreguen el seu contingut i extreuen dades específiques d'interès. Aquestes dades poden incloure text, imatges, enllaços, preus, puntuacions i molt més, depenent de la naturalesa del lloc web i de la informació que es vulgui obtenir.

El web scraping és una eina valuosa per recopilar dades de la web quan no es disposa d'un accés directe a una API (Interfície de Programació d'Aplicacions) o quan es necessita automatitzar la recopilació d'informació. Tot i això, és important fer servir aquesta tècnica amb responsabilitat i respectar els termes d'ús dels llocs web, ja que alguns llocs poden prohibir o limitar l'ús del web scraping per tal de protegir les seves dades i continguts.

Com que necessitem descarregar contingut web, la llibreria request estudiat anteriorment casa perfectament per a descarregar els arxius HTML.

BeautifulSoup

Beautiful Soup és una biblioteca de Python que facilita l'extracció de dades de documents HTML i XML. Aquesta biblioteca proporciona eines per analitzar i "navegar" pels elements d'un document web, facilitant la tasca d'extreure informació específica. És especialment útil en conjunció amb tècniques de web scraping, ja que permet accedir als diferents components d'una pàgina web i extraure el contingut desitjat de manera més eficient.

Per a fer servir aquiesta llibreria, hem de instal·lar-la amb pip install beautifulsoup4 o mitjançant conda. L'estrctura bàsica d'un programa de scrapping és la següent:

Obtenir la sopa
Python
    import requests
    from bs4 import BeautifulSoup

    # URL de la pàgina a estudiar
    url = 'https://www.la_meua_web.es'

    # Fer la sol·licitud HTTP i obtenir el contingut de la pàgina
    response = requests.get(url)
    content = response.content

    # Crear un objecte BeautifulSoup per analitzar el contingut HTML
    soup = BeautifulSoup(content, 'html.parser')

Consideracions:

  • La dificultat del Scraping és el veure en quina part de la pàgina està el contingut a extreure
  • Moltes pàgines tenen mecanismes d'intentar detectar bots o programes de scraping, per això és bona idea el aplicar un user agent (navegador) distint.
Canviar navegador d'orige
Python
import requests
from fake_useragent import UserAgent

# Instanciar un objeto UserAgent
ua = UserAgent()

# Realizar varias solicitudes con User-Agents aleatorios
for _ in range(5):
      headers = {'User-Agent': ua.random}
      print(f"User-Agent: {headers['User-Agent']}")

      # Petició des d'aquest user-agent
      response = requests.get(url, headers=headers)

Això generarà alguna cosa com:

Bash
1
2
3
4
5
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/116.0
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/116.0
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36

Tot i que podem trobar tota la documentació a https://www.crummy.com/software/BeautifulSoup/bs4/doc/, anem a veure els principals elements que podem trobar per a accedir a les etiquetes d'una pàgina.

Atenció

El més important és recordar:

Text Only
1
2
3
- La estructura del DOM (**Documento Object Model**), fortament jerarquitzada (elements que contenen elements, amb estructura de pares-fills i germans)
- Cada element és d'un tipus, definit per la etiqueta del mateix. A banda de la etiqueta, podem veure de quina classe és (atribut `class=xxx`)
- Pot identificar-se elements accedint amb l'identificador del mateix, que si es compleix la especificaicó del HTML, deu ser únic (`id=yyy`).

Atenció

Hem partit d'un exemple al qual ens baixem l'HTML de la pàgina web a fer scrapping, però ara tenim que tindre en consideració el següent. El que mostra el navegador no sempre és el HTML, sinò que moltes vegades aquest HTML font s'amplia amb l'execució de JAvascript

Si descarreguem amb request la pàgina web tindrem el HTML pre-javascript. i a nosaltres ens interessa el HTML post-javascript, ja que molta informació pot ser el resultat de l'execució de javascript, amb consultes a BBDD o similar

Selenuim

Selènium https://www.selenium.dev és una potent suite d'eines per a la automatització de navegadors web. Dissenyat inicialment per a testing automatitzat de pàgines web, Selenium s'ha convertit en una eina essencial per als desenvolupadors i testers que necessiten interactuar amb navegadors web mitjançant codi.

Amb Selenium, els usuaris poden escriure scripts en diversos llenguatges de programació com Python, Java, C#, entre d'altres, per a realitzar tasques automatitzades a través de l'interfície gràfica d'un navegador web. Això inclou clicar en enllaços, emplenar formularis, navegar entre pàgines i molt més.

Selenium suporta diversos navegadors com Chrome, Firefox, Safari i Edge, el que fa que sigui una eina versàtil per a la creació de proves automatitzades o per a la realització de tasques repetitives en l'entorn web.

A més de la seva utilització en testing, Selenium també és àmpliament utilitzat en web scraping, monitoratge de pàgines web, i altres tasques d'automatització relacionades amb la interacció amb navegadors. La seva naturalesa flexible i la capacitat de controlar navegadors en temps real fan de Selenium una eina imprescindible per als desenvolupadors i professionals del control de qualitat que treballen amb aplicacions web.

Per fer la instal·lació de selenium conda install -c conda-forge selenium.

Si recordes hem arribat ací perquè volem la versió definitiva del HTML (post javascript). Llavors no ens queda altra que navegar per aquella pàgina que volem scrapejar, i Selenium ens permet emular dita navegació.

Selecció del navegador

Python
from selenium import webdriver


def getBrowser():
   browser=random.randint(1,3)
   if browser==1:
      driver = webdriver.Edge()
   elif browser==2:
      driver=webdriver.Chrome()
   elif browser==3:
      driver = webdriver.Safari()

   return driver

Aquest codi ens permet seleccionar un navegador distint (o no) cada cop que el demanem. Això és adequat per a evitar mecanismes anti-scrapping. També podrien ser canviar les IP d'orige, per a semblar consexions de llocs distints.

Un cop sel·leccionat el navegador, simplement deurem d'alçar la instància del mateix, dir-li la pàgina on volem a nar i revisar-la tota (anant de dalt a baix...)

Navegació per la pàgina www.marca.com

Python
driver = getBrowser()         # seleccionem un navegador
driver.get("www.marca.com")   # indiquem la web a visitar
driver.maximize_window()      # (opcional) maximitzem
time.sleep(0.5)               # esperem mig segon

# anem fent scroll fins al final de la web
iter=1
while True:
   # Què ens queda fins arribar baix de tot?
   scrollHeight = driver.execute_script("return document.documentElement.scrollHeight")
   # aleatori el bot cap avall, multiplicat per la iteració actual
   height=random.randint(300,400)*iter
   # Ens situem en dita posició
   driver.execute_script("window.scrollTo(0, " + str(Height) + ");")

   # Si ja hem arribat al final trenquem el bucle i eixim
   if height > scrollHeight:
      print('End of page')
      break
   time.sleep(0.5)
   iter+=1


# recuperem el interior del cos, quedant-nos amb el HTML
body = driver.execute_script("return document.body")
source = body.get_attribute('innerHTML')

# creem el DOM per a processar-lo amb BeautifulSoup
soup = BeautifulSoup(source, "html.parser")

# Fer scraping amb la "sopa"

BeautifulSoup

Anem a veure els mètodes més important que farem servir de la llibreria BeautifulSoup un cop recuperat l'HTML i parsejat. Partim una mica del següent DOM per a l'explicació

HTML
<body>
    <h1>Just testing</h1>
    <div class="block">
      <h2>Some links</h2>
      <p>Hi there!</p>
      <ul id="data">
        <li class="blue"><a href="https://example1.com">Example 1</a></li>
        <li class="red"><a href="https://example2.com">Example 2</a></li>
        <li class="gold"><a href="https://example3.com">Example 3</a></li>
      </ul>
    </div>
    <div class="block">
      <h2>Formulario</h2>
      <form action="" method="post">
        <label for="POST-name">Nombre:</label>
        <input id="POST-name" type="text" name="name">
        <input type="submit" value="Save">
      </form>
    </div>
    <div class="footer">
      This is the footer
      <span class="inline"><p>This is span 1</p></span>
      <span class="inline"><p>This is span 2</p></span>
      <span class="inline"><p>This is span 2</p></span>
    </div>
</body>

Observem que cada element conté:

HTML
<etiqueta class="classe" id="idETQ" atr1=valor1 atr2=valor2 > text </etiqueta>

i podem buscar-los com segueix:

  • Localitazar elements soup.find_all('etiqueta'). Retorna tots els elements del tipus d'etiqueta indicat. Per exemple soup.find_all('li') tornarà tots els elements de la llista.
  • Localitazar elements d'una classe:

  • soup.find_all('etiqueta',class_='classe'). Torna els elements de les etiquetes indicades d'una certa classe

  • soup.find_all(class_='classe'). Torna els elements indicades d'una certa classe, podent ser de distint tipus d'etiqueta
  • Localitzar un element identificat soup.find_all('etiqueta',id='identificacor')

Atenció

Tenir en compte:

  • Els mètodes find_all() retornent un conjunt d'elements (buit o no)
  • Els mètode find retorna:
    • None si no troba res (no existeix)
    • Un element, quan per exemple busquem per identificador, i el troba
    • El primer element en cas de trobar-ne diversos.

Espai a les búsquede

Tenir en compte que els mètodes find i find_all() efectuen la búsqueda a l'interior del element des del qual busquem:

  • soup.find_all('li') retornarà tots els elements que hi han al document, ja que busquem desde l'inici del DOM
  • element.find_all('div') buscarà sols dins de cada element.

Un cop ja tenim localitzat un element podem accedir, des d'ell a tot el següent. Suposem que el tenim referenciat amb la variable elem:

  • Tipus d'element o tagelem.name
    Python
    1
    2
    3
    elem = soup.find('ul', id='data')
    elem.name
    'ul'
    
  • Accedir a característiques o atributs d'ell → Una etiqueta pot tenir un llistat d'atributs o característiques. Podem accedir a ells com si d'un diccionari es tratara elem[atribut]:
Python
1
2
3
4
5
6
7
8
9
elem = soup.find('input', id='POST-name')

# elem és <input id="POST-name" name="melibea" type="json"/>

elem['id']     # retorna 'POST-name'

elem['name']   # retorna 'melibea'

elem['type']   # retorn 'json'

També podem recuperar-los tots amb elem.attrs:

Python
elem.attrs
{'id': 'POST-name', 'type': 'json', 'name': 'melibea'}

Accedir al contingut com a text

El contingut textual, per entendre-ho, ha de diferenciar-se que podem diferenciar entre els següents tipus d'elements:

  • Elements que contenen sols una línea de text
    XML
    <elem>Contingut textual</eleme>
    
  • Elements que contenen altres elements:

    XML
    1
    2
    3
    4
    5
    6
    <div class="footer">
    Peu de la pàgina
       <span class="inline"><p>This is span 1</p></span>
       <span class="inline"><p>This is span 2</p></span>
       <span class="inline"><p>This is span 2</p></span>
    </div>
    
    Tenim diverses opcions per a recuperar contingut:

  • elem.text → retorna un string amb tot el contingut textual. Apareixeran els bots de linia si els elements d'on extraguem el text estan en linies diferents, inclòs sagnats. Segons els tipus anteriors tindriem:

    Contingut textual

    o be

    '\n Peu de la pàgina\n This is span 1\nThis is span 2\nThis is span 2\n'

  • elem.strings → Retorna un llistat (array) amb els elements textuals, incloses bots de linia i blancs. Segons els tipus anteriors tindriem:

    ['Contingut textual']

    o be

    ['\n This is the footer\n ', 'This is span 1', '\n', 'This is span 2', '\n', 'This is span 2', '\n']

  • elem.stripped_strings → Retorna un llistat (array) amb els elements textuals, sense bots de linia i blancs. Segons els tipus anteriors tindriem:

    ['Contingut textual']

    o be

    ['This is the footer','This is span 1','This is span 2','This is span 2']

  • elem.string → retorna sols una cadena, quan l'element conté sols un únic element textual. Cas contrari (buida o més d'una) no retorna res

|Acció|funció| |---|-| |Contigut | `elem.contents`| |Contingut iterable | `elem.children`| |Accedir al pare del element | `elem.parent`| |Llista amb tots els pares | `elem.parents`| |El següent germa (dins d'element)|`elem.next_sibling`| |Iterable amb els següents germans |`elem.next_siblings`| |L'anterior germa (dins d'element)|`elem.previous_sibling`| |Iterable amb els anteriors germans |`elem.previous_siblings`| |Següent element (germà o no)|`elem.next_element`| |Anterior element (germà o no)|`elem.previous`|

Exemple de scraping

Primer que res hem d'analitzar el contingut de la pàgina web. A l'exemple anem a estudiar la web whatcar.com, que fa anàlisi de cotxes. Dins de la web, podem accedir a la pestanya de reviews, i ens trobem això:

![Contingut de la web](./img/Scrap01.png){width=75%}

Sembla que hi ha un enllaç per a cada marca. Per veure el tipus d'element, hem de fer una tasca d'anàlisi del codi font, mitjançant les eines de desenvolupador. Llavors ens trobem que:

![Contingut real de la web](./img/Scrap02.png){width=95%}
  • Per a cada marca que s'analitza està dins d'etiquetes <a class="FragmentReviewsMake-makeContainer-d9f354b1" ...>
  • Dins de cada marka hi ha un link a la pàgina de cada marca <a href='...'>

Arreplegant els links de les marques

Python
base_url="https://www.whatcar.com"
page="/reviews"

soup=get_soup_from_webPage(base_url+page)

class_marca='FragmentReviewsMake-makeContainer-d9f354b1'

soup=
marques=soup.find_all('a',class_=class_marca)

file = open("Brands.txt","w", encoding='utf-8')
for marca in marques:
     link_marca= marca['href']
     file.write(base_url+link_marca+"\n")

el resultat és un arxiu de text amb el següent contingut:

Text Only
1
2
3
4
5
https://www.whatcar.com/make/abarth
https://www.whatcar.com/make/alfa-romeo
https://www.whatcar.com/make/alpine
https://www.whatcar.com/make/aston-martin
https://www.whatcar.com/make/audi

Ara, de manera recursiva hauriem d'analitzar cadascuna de les marques, vegem primer la estructura de cada marca:

![Pàgina amb la marca](./img/Scrap03.png){width=100%}

i si explorem cadascuna de les targetes de cada model:

![Pàgina amb la marca](./img/Scrap04.png){width=100%}

podem trobar que dins mateix tenim un link <a href="/bmw/2-series/hatchback/review/n78" ...> </a>, del qual sols ens interessa el href..

Posteriorment hauriem de veure com accedir a la pàgina de cada model i buscar els elements amb informació interessant per veure com extreure-la.