recuperar enlaces de la página web usando python y BeautifulSoup


Respuestas:


193

Aquí hay un fragmento corto usando la clase SoupStrainer en BeautifulSoup:

import httplib2
from bs4 import BeautifulSoup, SoupStrainer

http = httplib2.Http()
status, response = http.request('http://www.nytimes.com')

for link in BeautifulSoup(response, parse_only=SoupStrainer('a')):
    if link.has_attr('href'):
        print(link['href'])

La documentación de BeautifulSoup es bastante buena y cubre varios escenarios típicos:

https://www.crummy.com/software/BeautifulSoup/bs4/doc/

Editar: Tenga en cuenta que usé la clase SoupStrainer porque es un poco más eficiente (memoria y velocidad), si sabe de antemano lo que está analizando.


13
+1, usar el colador de sopa es una gran idea porque te permite evitar muchos análisis innecesarios cuando lo único que buscas son los enlaces.
Evan Fosmark el

44
/usr/local/lib/python2.7/site-packages/bs4/__init__.py:128: UserWarning: The "parseOnlyThese" argument to the BeautifulSoup constructor has been renamed to "parse_only."
Aviso

27
En la versión 3.2.1 de BeautifulSoup no hay has_attr. En cambio, veo que hay algo llamado has_keyy funciona.

2
Actualización para python3
john doe

77
desde bs4 importa BeautifulSoup. (no de BeautifulSoup importa BeautifulSoup ..) corrección necesaria.
Rishabh Agrahari

67

Para completar, la versión BeautifulSoup 4, haciendo uso de la codificación proporcionada por el servidor también:

from bs4 import BeautifulSoup
import urllib.request

parser = 'html.parser'  # or 'lxml' (preferred) or 'html5lib', if installed
resp = urllib.request.urlopen("http://www.gpsbasecamp.com/national-parks")
soup = BeautifulSoup(resp, parser, from_encoding=resp.info().get_param('charset'))

for link in soup.find_all('a', href=True):
    print(link['href'])

o la versión de Python 2:

from bs4 import BeautifulSoup
import urllib2

parser = 'html.parser'  # or 'lxml' (preferred) or 'html5lib', if installed
resp = urllib2.urlopen("http://www.gpsbasecamp.com/national-parks")
soup = BeautifulSoup(resp, parser, from_encoding=resp.info().getparam('charset'))

for link in soup.find_all('a', href=True):
    print link['href']

y una versión que usa la requestsbiblioteca , que tal como está escrita funcionará tanto en Python 2 como en 3:

from bs4 import BeautifulSoup
from bs4.dammit import EncodingDetector
import requests

parser = 'html.parser'  # or 'lxml' (preferred) or 'html5lib', if installed
resp = requests.get("http://www.gpsbasecamp.com/national-parks")
http_encoding = resp.encoding if 'charset' in resp.headers.get('content-type', '').lower() else None
html_encoding = EncodingDetector.find_declared_encoding(resp.content, is_html=True)
encoding = html_encoding or http_encoding
soup = BeautifulSoup(resp.content, parser, from_encoding=encoding)

for link in soup.find_all('a', href=True):
    print(link['href'])

La soup.find_all('a', href=True)llamada encuentra todos los <a>elementos que tienen un hrefatributo; los elementos sin el atributo se omiten.

BeautifulSoup 3 detuvo el desarrollo en marzo de 2012; los nuevos proyectos realmente deberían usar BeautifulSoup 4, siempre.

Tenga en cuenta que debe dejar la decodificación del HTML de bytes a BeautifulSoup . Puede informar a BeautifulSoup del conjunto de caracteres que se encuentra en los encabezados de respuesta HTTP para ayudar en la decodificación, pero esto puede ser incorrecto y contradictorio con una <meta>información de encabezado que se encuentra en el propio HTML, razón por la cual lo anterior usa el método de clase interna BeautifulSoup EncodingDetector.find_declared_encoding()para asegurarse de que Estas sugerencias de codificación incorporadas ganan a un servidor mal configurado.

Con requests, el response.encodingatributo predeterminado es Latin-1 si la respuesta tiene un tipo text/*MIME, incluso si no se devuelve ningún conjunto de caracteres. Esto es coherente con los RFC de HTTP, pero es doloroso cuando se usa con el análisis HTML, por lo que debe ignorar ese atributo cuando no charsetse establece en el encabezado Content-Type.


¿Hay algo como StrainedSoup para bs4? (No lo necesito ahora, pero me pregunto si es posible que desee agregar eso)
Antti Haapala

@AnttiHaapala: ¿ SoupStrainerquieres decir? No fue a ninguna parte, todavía es parte del proyecto .
Martijn Pieters

¿Hay alguna razón por la que este código no pase "features =" al constructor BeautifulSoup? BeautifulSoup me da una advertencia sobre el uso de un analizador predeterminado.
MikeB

1
@MikeB: cuando escribí esta respuesta, BeautifulSoup aún no emitió una advertencia si no lo hiciste.
Martijn Pieters

50

Otros han recomendado BeautifulSoup, pero es mucho mejor usar lxml . A pesar de su nombre, también es para analizar y raspar HTML. Es mucho, mucho más rápido que BeautifulSoup, e incluso maneja HTML "roto" mejor que BeautifulSoup (su reclamo de fama). También tiene una API de compatibilidad para BeautifulSoup si no desea aprender la API lxml.

Ian Blicking está de acuerdo .

Ya no hay ninguna razón para usar BeautifulSoup, a menos que esté en Google App Engine o algo en lo que no se permita nada que no sea Python.

lxml.html también admite selectores CSS3, por lo que este tipo de cosas es trivial.

Un ejemplo con lxml y xpath se vería así:

import urllib
import lxml.html
connection = urllib.urlopen('http://www.nytimes.com')

dom =  lxml.html.fromstring(connection.read())

for link in dom.xpath('//a/@href'): # select the url in href for all a tags(links)
    print link

23
BeautifulSoup 4 se usará lxmlcomo el analizador predeterminado si está instalado.
Martijn Pieters

28
import urllib2
import BeautifulSoup

request = urllib2.Request("http://www.gpsbasecamp.com/national-parks")
response = urllib2.urlopen(request)
soup = BeautifulSoup.BeautifulSoup(response)
for a in soup.findAll('a'):
  if 'national-park' in a['href']:
    print 'found a url with national-park in the link'

Esto resolvió un problema que tenía con mi código. ¡Gracias!
RJ

10

El siguiente código es para recuperar todos los enlaces disponibles en una página web usando urllib2y BeautifulSoup4:

import urllib2
from bs4 import BeautifulSoup

url = urllib2.urlopen("http://www.espncricinfo.com/").read()
soup = BeautifulSoup(url)

for line in soup.find_all('a'):
    print(line.get('href'))

8

Bajo el capó, BeautifulSoup ahora usa lxml. Solicitudes, lxml y comprensiones de listas hacen una combinación asesina

import requests
import lxml.html

dom = lxml.html.fromstring(requests.get('http://www.nytimes.com').content)

[x for x in dom.xpath('//a/@href') if '//' in x and 'nytimes.com' not in x]

En la lista de compilación, "if '//' y 'url.com' no están en x" es un método simple para eliminar la lista de url de las URL de navegación 'internas' del sitio, etc.


1
Si se trata de una nueva publicación, ¿por qué la publicación original no incluye: 1. solicitudes 2.lista comp 3. lógica para eliminar enlaces internos y basura del sitio? Intente y compare los resultados de las dos publicaciones, mi compilación de listas hace un trabajo sorprendentemente bueno al eliminar los enlaces basura.
cheekybastard

El OP no solicitó esas características y la parte que solicitó ya se ha publicado y resuelto utilizando exactamente el mismo método que publica. Sin embargo, eliminaré el voto negativo ya que la comprensión de la lista agrega valor para las personas que quieren esas características y las mencionas explícitamente en el cuerpo de la publicación. Además, puede usar el representante :)
dotancohen

4

solo para obtener los enlaces, sin B.soup y regex:

import urllib2
url="http://www.somewhere.com"
page=urllib2.urlopen(url)
data=page.read().split("</a>")
tag="<a href=\""
endtag="\">"
for item in data:
    if "<a href" in item:
        try:
            ind = item.index(tag)
            item=item[ind+len(tag):]
            end=item.index(endtag)
        except: pass
        else:
            print item[:end]

para operaciones más complejas, por supuesto, BSoup sigue siendo preferido.


77
¿Y si, por ejemplo, hay algo intermedio <ay href? ¿Decir rel="nofollow"o onclick="..."incluso una nueva línea? stackoverflow.com/questions/1732348/…
dimo414

¿Hay alguna manera de filtrar solo algunos enlaces con esto? como decir que solo quiero enlaces que tengan "Episodio" en el enlace?
nwgat

4

Este script hace lo que buscas, pero también resuelve los enlaces relativos a enlaces absolutos.

import urllib
import lxml.html
import urlparse

def get_dom(url):
    connection = urllib.urlopen(url)
    return lxml.html.fromstring(connection.read())

def get_links(url):
    return resolve_links((link for link in get_dom(url).xpath('//a/@href')))

def guess_root(links):
    for link in links:
        if link.startswith('http'):
            parsed_link = urlparse.urlparse(link)
            scheme = parsed_link.scheme + '://'
            netloc = parsed_link.netloc
            return scheme + netloc

def resolve_links(links):
    root = guess_root(links)
    for link in links:
        if not link.startswith('http'):
            link = urlparse.urljoin(root, link)
        yield link  

for link in get_links('http://www.google.com'):
    print link

Esto no hace lo que ti debe hacer; si resolve_links () no tiene una raíz, entonces nunca devuelve ninguna URL.
MikeB

4

Para encontrar todos los enlaces, en este ejemplo usaremos el módulo urllib2 junto con el re.module * Una de las funciones más poderosas en el módulo re es "re.findall ()". Mientras que re.search () se usa para encontrar la primera coincidencia de un patrón, re.findall () encuentra todas las coincidencias y las devuelve como una lista de cadenas, y cada cadena representa una coincidencia *

import urllib2

import re
#connect to a URL
website = urllib2.urlopen(url)

#read html code
html = website.read()

#use re.findall to get all the links
links = re.findall('"((http|ftp)s?://.*?)"', html)

print links

3

¿Por qué no usar expresiones regulares?

import urllib2
import re
url = "http://www.somewhere.com"
page = urllib2.urlopen(url)
page = page.read()
links = re.findall(r"<a.*?\s*href=\"(.*?)\".*?>(.*?)</a>", page)
for link in links:
    print('href: %s, HTML text: %s' % (link[0], link[1]))

1
Me encantaría poder entender esto, ¿dónde puedo averiguar eficientemente qué (r"<a.*?\s*href=\"(.*?)\".*?>(.*?)</a>", page)significa? ¡Gracias!
user1063287

9
Realmente una mala idea. HTML roto en todas partes.
Ufoguy

2
¿Por qué no utilizar expresiones regulares para analizar html: stackoverflow.com/questions/1732348/…
allcaps

@ user1063287, la web está llena de tutoriales de expresiones regulares. Vale la pena tu tiempo para leer un par. Si bien los RE pueden ser realmente complicados, el que estás preguntando es bastante básico.
alexis

3

Los enlaces pueden estar dentro de una variedad de atributos para que pueda pasar una lista de esos atributos para seleccionar

por ejemplo, con el atributo src y href (aquí estoy usando el operador comienza con ^ para especificar que cualquiera de estos valores de atributos comience con http. Puede personalizar esto según sea necesario

from bs4 import BeautifulSoup as bs
import requests
r = requests.get('https://stackoverflow.com/')
soup = bs(r.content, 'lxml')
links = [item['href'] if item.get('href') is not None else item['src'] for item in soup.select('[href^="http"], [src^="http"]') ]
print(links)

Atributo = selectores de valor

[attr ^ = valor]

Representa elementos con un nombre de atributo de atributo cuyo valor está prefijado (precedido) por valor.


1

He aquí un ejemplo usando @ars respuesta aceptada y los BeautifulSoup4, requestsy wgetmódulos para manejar las descargas.

import requests
import wget
import os

from bs4 import BeautifulSoup, SoupStrainer

url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/eeg-mld/eeg_full/'
file_type = '.tar.gz'

response = requests.get(url)

for link in BeautifulSoup(response.content, 'html.parser', parse_only=SoupStrainer('a')):
    if link.has_attr('href'):
        if file_type in link['href']:
            full_path = url + link['href']
            wget.download(full_path)

1

Encontré la respuesta de @ Blairg23 funcionando, después de la siguiente corrección (cubriendo el escenario donde no funcionó correctamente):

for link in BeautifulSoup(response.content, 'html.parser', parse_only=SoupStrainer('a')):
    if link.has_attr('href'):
        if file_type in link['href']:
            full_path =urlparse.urljoin(url , link['href']) #module urlparse need to be imported
            wget.download(full_path)

Para Python 3:

urllib.parse.urljoin tiene que usarse para obtener la URL completa en su lugar.


1

El propio analizador de BeatifulSoup puede ser lento. Puede ser más factible usar lxml, que es capaz de analizar directamente desde una URL (con algunas limitaciones que se mencionan a continuación).

import lxml.html

doc = lxml.html.parse(url)

links = doc.xpath('//a[@href]')

for link in links:
    print link.attrib['href']

El código anterior devolverá los enlaces tal como están, y en la mayoría de los casos serían enlaces relativos o absolutos de la raíz del sitio. Dado que mi caso de uso era solo extraer un cierto tipo de enlaces, a continuación hay una versión que convierte los enlaces a URL completos y que opcionalmente acepta un patrón global como *.mp3. Sin embargo, no manejará puntos simples y dobles en las rutas relativas, pero hasta ahora no tuve la necesidad de hacerlo. Si necesita analizar fragmentos de URL que contengan ../o ./luego, urlparse.urljoin puede ser útil.

NOTA : El análisis directo de URL de lxml no maneja la carga httpsy no realiza redireccionamientos, por lo que por esta razón la siguiente versión está usando urllib2+ lxml.

#!/usr/bin/env python
import sys
import urllib2
import urlparse
import lxml.html
import fnmatch

try:
    import urltools as urltools
except ImportError:
    sys.stderr.write('To normalize URLs run: `pip install urltools --user`')
    urltools = None


def get_host(url):
    p = urlparse.urlparse(url)
    return "{}://{}".format(p.scheme, p.netloc)


if __name__ == '__main__':
    url = sys.argv[1]
    host = get_host(url)
    glob_patt = len(sys.argv) > 2 and sys.argv[2] or '*'

    doc = lxml.html.parse(urllib2.urlopen(url))
    links = doc.xpath('//a[@href]')

    for link in links:
        href = link.attrib['href']

        if fnmatch.fnmatch(href, glob_patt):

            if not href.startswith(('http://', 'https://' 'ftp://')):

                if href.startswith('/'):
                    href = host + href
                else:
                    parent_url = url.rsplit('/', 1)[0]
                    href = urlparse.urljoin(parent_url, href)

                    if urltools:
                        href = urltools.normalize(href)

            print href

El uso es el siguiente:

getlinks.py http://stackoverflow.com/a/37758066/191246
getlinks.py http://stackoverflow.com/a/37758066/191246 "*users*"
getlinks.py http://fakedomain.mu/somepage.html "*.mp3"

lxmlsolo puede manejar una entrada válida, ¿cómo puede reemplazarla BeautifulSoup?
alexis

@alexis: Creo que lxml.htmles un poco más indulgente que el lxml.etree. Si su entrada no está bien formada, puede establecer explícitamente el analizador BeautifulSoup: lxml.de/elementsoup.html . Y si vas con BeatifulSoup, entonces BS3 es una mejor opción.
ccpizza

0
import urllib2
from bs4 import BeautifulSoup
a=urllib2.urlopen('http://dir.yahoo.com')
code=a.read()
soup=BeautifulSoup(code)
links=soup.findAll("a")
#To get href part alone
print links[0].attrs['href']

0

Puede haber muchos enlaces duplicados junto con enlaces externos e internos. Para diferenciar entre los dos y simplemente obtener enlaces únicos usando conjuntos:

# Python 3.
import urllib    
from bs4 import BeautifulSoup

url = "http://www.espncricinfo.com/"
resp = urllib.request.urlopen(url)
# Get server encoding per recommendation of Martijn Pieters.
soup = BeautifulSoup(resp, from_encoding=resp.info().get_param('charset'))  
external_links = set()
internal_links = set()
for line in soup.find_all('a'):
    link = line.get('href')
    if not link:
        continue
    if link.startswith('http'):
        external_links.add(link)
    else:
        internal_links.add(link)

# Depending on usage, full internal links may be preferred.
full_internal_links = {
    urllib.parse.urljoin(url, internal_link) 
    for internal_link in internal_links
}

# Print all unique external and full internal links.
for link in external_links.union(full_internal_links):
    print(link)
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.