pobrać linki ze strony internetowej za pomocą Pythona i BeautifulSoup

Odpowiedzi:

193

Oto krótki fragment wykorzystujący klasę SoupStrainer w 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'])

Dokumentacja BeautifulSoup jest w rzeczywistości całkiem dobra i obejmuje kilka typowych scenariuszy:

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

Edycja: Zwróć uwagę, że użyłem klasy SoupStrainer, ponieważ jest nieco bardziej wydajna (pod względem pamięci i szybkości), jeśli wiesz, co analizujesz z wyprzedzeniem.

ars
źródło
13
+1, użycie sitka do zupy to świetny pomysł, ponieważ pozwala ominąć wiele niepotrzebnych analiz, gdy jedyne, czego szukasz, to linki.
Evan Fosmark,
4
/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."
Uwaga
27
W wersji 3.2.1 BeautifulSoup nie ma has_attr. Zamiast tego widzę, że coś się nazywa has_keyi działa.
2
Aktualizacja dla python3
john doe
7
z bs4 import BeautifulSoup. (nie z BeautifulSoup import BeautifulSoup ..) wymagana korekta.
Rishabh Agrahari
67

Ze względu na kompletność wersja BeautifulSoup 4, również wykorzystująca kodowanie dostarczone przez serwer:

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'])

lub wersja 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']

oraz wersja korzystająca z requestsbiblioteki , która jak napisano będzie działać zarówno w Pythonie 2, jak i 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'])

soup.find_all('a', href=True)Wywołanie znajdzie wszystkie <a>elementy, które mają hrefatrybut; elementy bez atrybutu są pomijane.

BeautifulSoup 3 zatrzymał rozwój w marcu 2012; nowe projekty naprawdę powinny używać BeautifulSoup 4, zawsze.

Zauważ, że powinieneś pozostawić dekodowanie HTML z bajtów do BeautifulSoup . Możesz poinformować BeautifulSoup o zestawie znaków znajdującym się w nagłówkach odpowiedzi HTTP, aby pomóc w dekodowaniu, ale może to być błędne i sprzeczne z <meta>informacjami nagłówka znalezionymi w samym kodzie HTML, dlatego powyższe używa metody wewnętrznej klasy BeautifulSoup, EncodingDetector.find_declared_encoding()aby upewnić się, że takie wbudowane wskazówki dotyczące kodowania wygrywają z błędnie skonfigurowanym serwerem.

W requestsprzypadku response.encodingatrybutu wartość domyślna to Latin-1, jeśli odpowiedź ma text/*typ MIME, nawet jeśli nie został zwrócony żaden zestaw znaków. Jest to zgodne ze specyfikacjami HTTP RFC, ale jest bolesne, gdy jest używane z analizą HTML, więc należy zignorować ten atrybut, jeśli charsetw nagłówku Content-Type ustawiono no .

Martijn Pieters
źródło
Czy jest coś takiego jak StrainedSoup dla bs4? (Nie potrzebuję tego teraz, ale po prostu się zastanawiam, czy mógłbyś to dodać)
Antti Haapala
@AnttiHaapala: SoupStrainermasz na myśli? To nie idź nigdzie, nadal jest częścią projektu .
Martijn Pieters
Czy jest jakiś powód, dla którego ten kod nie przekazuje „features =” do konstruktora BeautifulSoup? BeautifulSoup ostrzega mnie o używaniu domyślnego parsera.
MikeB
1
@MikeB: kiedy napisałem tę odpowiedź, BeautifulSoup nie zgłosiła jeszcze ostrzeżenia, jeśli tego nie zrobiłeś.
Martijn Pieters
50

Inni polecili BeautifulSoup, ale znacznie lepiej jest użyć lxml . Pomimo swojej nazwy służy również do analizowania i zgrywania kodu HTML. Jest dużo, dużo szybszy niż BeautifulSoup, a nawet obsługuje „zepsuty” HTML lepiej niż BeautifulSoup (ich roszczenia do sławy). Ma również interfejs API zgodności dla BeautifulSoup, jeśli nie chcesz uczyć się lxml API.

Ian Blicking zgadza się .

Nie ma już powodu, aby używać BeautifulSoup, chyba że korzystasz z Google App Engine lub czegoś, w czym coś, co nie jest czysto Python, jest niedozwolone.

lxml.html obsługuje również selektory CSS3, więc tego rodzaju rzeczy są trywialne.

Przykład z lxml i xpath wyglądałby tak:

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
aehlke
źródło
23
BeautifulSoup 4 użyje lxmljako domyślnego parsera, jeśli zostanie zainstalowany.
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'
Andrew Johnson
źródło
To rozwiązało problem, który miałem z kodem. Dziękuję Ci!
RJ
10

Poniższy kod służy do pobierania wszystkich linków dostępnych na stronie internetowej przy użyciu urllib2i 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'))
Sentient07
źródło
8

Pod maską BeautifulSoup używa teraz lxml. Żądania, lxml i listy składają się na zabójczą kombinację.

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]

W zestawieniu list „if” // ”i„ url.com ”not in x” to prosta metoda przeglądania listy adresów URL „wewnętrznych” adresów URL nawigacji witryn itp.

bezczelny drań
źródło
1
Jeśli jest to repost, dlaczego oryginalny post nie zawiera: 1. żądań 2. kompilacji listy 3. logiki czyszczenia wewnętrznych i śmieciowych linków? Spróbuj porównać wyniki dwóch postów, moja kompozycja listy zaskakująco dobrze radzi sobie z szorowaniem niepotrzebnych linków.
cheekybastard
OP nie pytał o te funkcje, a część, o którą prosił, została już opublikowana i rozwiązana przy użyciu tej samej metody, co Ty. Jednak usuwam głos przeciwny, ponieważ rozumienie listy dodaje wartości dla osób, które chcą tych funkcji, i wyraźnie wspominasz o nich w treści postu. Możesz również użyć rep :)
dotancohen
4

tylko po to, aby uzyskać linki, bez B.soup i 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]

dla bardziej złożonych operacji, oczywiście BSoup jest nadal preferowana.

ghostdog74
źródło
7
A jeśli na przykład jest coś pomiędzy <aa href? Powiedz rel="nofollow"lub onclick="..."po prostu nową linijkę? stackoverflow.com/questions/1732348/…
dimo414
czy istnieje sposób na odfiltrowanie tylko niektórych linków z tym? na przykład chcę tylko linki, które mają „Odcinek” w linku?
nwgat
4

Ten skrypt robi to, czego szukasz, ale także rozwiązuje względne linki do bezwzględnych linków.

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
Ricky Wilson
źródło
To nie robi tego, co ma zrobić; jeśli solution_links () nie ma katalogu głównego, to nigdy nie zwraca żadnych adresów URL.
MikeB
4

Aby znaleźć wszystkie linki, w tym przykładzie użyjemy modułu urllib2 razem z re.module * Jedną z najpotężniejszych funkcji w module re jest „re.findall ()”. Podczas gdy re.search () służy do znalezienia pierwszego dopasowania dla wzorca, re.findall () znajduje wszystkie dopasowania i zwraca je jako listę ciągów, z których każdy reprezentuje jedno dopasowanie *

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
Mayur Ingle
źródło
3

Dlaczego nie użyć wyrażeń regularnych:

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]))
ahmadh
źródło
1
chciałbym móc to zrozumieć, gdzie mogę skutecznie dowiedzieć się, co (r"<a.*?\s*href=\"(.*?)\".*?>(.*?)</a>", page)to znaczy? dzięki!
user1063287
9
Naprawdę zły pomysł. Wszędzie uszkodzony HTML.
Ufoguy
2
Dlaczego nie użyć wyrażeń regularnych do analizowania html: stackoverflow.com/questions/1732348/ ...
allcaps
@ user1063287, sieć jest pełna samouczków dotyczących wyrażeń regularnych. Warto poświęcić trochę czasu na przeczytanie kilku. Chociaż RE mogą być naprawdę zagmatwane, ten, o który pytasz, jest dość prosty.
Alexis
3

Łącza mogą mieć różne atrybuty, więc możesz przekazać listę tych atrybutów do wyboru

na przykład z atrybutem src i href (tutaj używam operatora rozpoczyna się od ^, aby określić, że każda z tych wartości atrybutów zaczyna się od http.

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)

Atrybut = selektory wartości

[atr ^ = wartość]

Reprezentuje elementy z nazwą atrybutu attr, którego wartość jest poprzedzona (poprzedzona) wartością.

QHarr
źródło
1

Oto przykład z użyciem @ars Zaakceptowanych odpowiedź i BeautifulSoup4, requestsoraz wgetmoduły do obsługi plików do pobrania.

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)
Blairg23
źródło
1

Znalazłem odpowiedź, którą @ Blairg23 działała, po następującej poprawce (obejmującej scenariusz, w którym nie działał poprawnie):

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)

W przypadku Pythona 3:

urllib.parse.urljoin należy użyć zamiast tego w celu uzyskania pełnego adresu URL.

AkanKsha Bhardwaj
źródło
1

Parser BeatifulSoup może działać wolno. Bardziej realne może być użycie lxml, który jest w stanie analizować bezpośrednio z adresu URL (z pewnymi ograniczeniami wymienionymi poniżej).

import lxml.html

doc = lxml.html.parse(url)

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

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

Powyższy kod zwróci linki bez zmian, aw większości przypadków będą to linki względne lub bezwzględne względem katalogu głównego witryny. Ponieważ moim przypadkiem użycia było wyodrębnianie tylko określonego typu linków, poniżej znajduje się wersja, która konwertuje linki na pełne adresy URL i która opcjonalnie akceptuje wzorzec glob, taki jak *.mp3. Nie obsługuje jednak pojedynczych i podwójnych kropek na ścieżkach względnych, ale do tej pory nie było takiej potrzeby. Jeśli chcesz przeanalizować fragmenty adresów URL zawierające urlparse.urljoin../ lub ./wtedy, może się przydać.

UWAGA : Bezpośrednie parsowanie adresu URL lxml nie obsługuje ładowania z httpsi nie wykonuje przekierowań, więc z tego powodu poniższa wersja używa 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

Sposób użycia jest następujący:

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"
ccpizza
źródło
lxmlobsługuje tylko prawidłowe dane wejściowe, w jaki sposób można je zastąpić BeautifulSoup?
Alexis
@alexis: Myślę, że lxml.htmljest nieco bardziej wyrozumiały niż lxml.etree. Jeśli dane wejściowe nie są poprawnie sformułowane, możesz jawnie ustawić parser BeautifulSoup: lxml.de/elementsoup.html . A jeśli zdecydujesz się na BeatifulSoup, lepszym wyborem będzie BS3.
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']
Tilak Patidar
źródło
0

Może istnieć wiele zduplikowanych linków razem z linkami zewnętrznymi i wewnętrznymi. Aby rozróżnić te dwa elementy i po prostu uzyskać unikalne linki za pomocą zestawów:

# 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)
Alexander
źródło