Web scraping

1

Cześć,

zacząłem się uczyć web scrapingu z wykorzystaniem biblioteki BeautifulSoup i natrafilem na pewien problem. Strona z której chcę pobrać dane to inforstrefa.com. Konkretnie chodzi mi o listę depesz opublikowanych dla danej spółki wraz z datą i czasem publikacji. Wygląda to tak:

screenshot-20190714114643.png

W pierwszej kolejności dla daty, czasu i tekstu stworzyłem odwołanie do właściwej lokalizacji na stronie i usunąłem znaczniki HTML:

#ekstrahowanie daty i usuwanie znaczników HTML z daty
data = daty9[0].find_all("td", {"class":"divider"})
ndata = str(data)
cleandata = BeautifulSoup(ndata, "lxml").get_text()
czas = daty9[0].find_all("td", {"class":"time"})
nczas = str(czas)
cleantime = BeautifulSoup(nczas, "lxml").get_text()
tekst = daty9[0].find_all("td", {"class":"text"})
ntekst = str(tekst)
cleantext = BeautifulSoup(ntekst, "lxml").get_text()

Następnie stworzyłem pętle tak aby wyświetlić wszystkie komunikaty:

for i in daty9:
    print(cleandata)
    print(cleantime)
    print(cleantext)

W rezultacie otrzymuję trzy odrębne zbiory:
screenshot-20190714115447.png

Co zrobić, aby wynik końcowy był w formie: data1, czas1, komunikat 1 itd., czyli tak jak poniżej (w tym celu zmieniłem find_all, na find, ale nie wiem jak przełożyć to na cały zbiór)

screenshot-20190714115706.png

2

W web scrapingu nie chodzi o to by uzyskać osobne zbiory bo nie będziesz ich miał jak poskładać. Zamiast tego idziesz po kolei po wierszach swojej tabeli, kontrolując stan. W twoim przypadku dla takiego wiersza:

<tr>
    <td class="divider" colspan="3">13/07/2019</td>
</tr>

Zbierasz i zpamiętujesz datę i idziesz dalej (continue), a dla takiego:

<tr>
    <td class="time">...</td>

    <td class="text">...</td>
</tr>

Zbierasz czas i tekst i wypisujesz razem z zapamiętaną datą. Razem:

soup = BeautifulSoup(response.text)
table = soup.find("table", class_="table-data")

current_date, current_time, current_text = None, None, None
for tr in table.find_all("tr"):
    divider = tr.find('td', class_='divider')
    if divider:
        current_date = divider.text
        continue
    time = tr.find('td', class_='time')
    if time:
        current_time = time.text
    text = tr.find('td', class_='text')
    if text:
        current_text = text.text
    print(u"{}\n{}\n{}\n\n".format(current_date, current_time, current_text))

Może to zawieść jeśli trafi się jakiś wybrakowany wiersz w tabeli (np. posiada czas, a nie posiada tekstu) - obsługę takich przypadków pozostawiam jako ćwiczenie ;)

0
Spearhead napisał(a):

W web scrapingu nie chodzi o to by uzyskać osobne zbiory bo nie będziesz ich miał jak poskładać. Zamiast tego idziesz po kolei po wierszach swojej tabeli, kontrolując stan. W twoim przypadku dla takiego wiersza:

<tr>
    <td class="divider" colspan="3">13/07/2019</td>
</tr>

Zbierasz i zpamiętujesz datę i idziesz dalej (continue), a dla takiego:

<tr>
    <td class="time">...</td>

    <td class="text">...</td>
</tr>

Zbierasz czas i tekst i wypisujesz razem z zapamiętaną datą. Razem:

soup = BeautifulSoup(response.text)
table = soup.find("table", class_="table-data")

current_date, current_time, current_text = None, None, None
for tr in table.find_all("tr"):
    divider = tr.find('td', class_='divider')
    if divider:
        current_date = divider.text
        continue
    time = tr.find('td', class_='time')
    if time:
        current_time = time.text
    text = tr.find('td', class_='text')
    if text:
        current_text = text.text
    print(u"{}\n{}\n{}\n\n".format(current_date, current_time, current_text))

Może to zawieść jeśli trafi się jakiś wybrakowany wiersz w tabeli (np. posiada czas, a nie posiada tekstu) - obsługę takich przypadków pozostawiam jako ćwiczenie ;)

Wielkie dzięki za pomoc. Mam jeszcze pytanie o iterację po wszystkich pozostałych stronach z wynikami. W tym przypadku wszystkie komunikaty znajdują się na 13 stronach. Nie chcę ustawiać stałej wartości, bo w przypadku innych spółek liczba będzie inna i przez to rozwiązanie nie będzie aplikowalne.

Przyszło mi do głowy, aby zdefiniować nową zmienną która zawierałaby wszystkie strony, a następnie napisać pętle, której liczba iteracji byłaby równa zakresowi wartości w zmiennej. Zacząłem od przypisania wartości do zmiennej:

page_nr = soup.find_all('ul', class_= 'pagination')[-1].text

Jednak to nie działa, bo otrzymuję coś takiego:
screenshot-20190714165943.png

i niestety, ale utknąłem. Próbowałem znaleźć coś też na stackoverflow, ale większość rozwiązań opiera się o zdefiniowany zakres.

1

Parsowanie jest rekurencyjnie. W twoim przypadku masz przycisk "Dalej", więc za każdym razem jak sparsujesz stronę sprawdzasz, czy ten przycisk istnieje i jeśli tak, parsujesz kolejną. Z grubsza kod wygląda tak:

import requests
import bs4

def parse(url):
    r = requests.get('http://infostrefa.com/' + url)
    soup = bs4.BeautifulSoup(r.text)

    # sprasuj soup jak poprzednio

    next = soup.find('a', class_='nav-next')
    if next:
        parse(next['href'])

parse('infostrefa/pl/wiadomosci/espi/1')

Niestety, Python nie nadaje się do takich rekurencji ze względu na ograniczenie głębokości stosu, więc trzeba to przepisać aby zlikwidować rekurencję. Ponieważ zawsze sprawdzamy tylko jeden link, kod może wyglądać mniej więce tak:

import requests
import bs4

next_page = 'infostrefa/pl/wiadomosci/espi/1'
while True:
    r = requests.get('http://infostrefa.com/' + next_page)
    soup = bs4.BeautifulSoup(r.text)

    # sprasuj soup jak poprzednio

    next = soup.find('a', class_='nav-next')
    if next:
        next_page = next['href']
    else:
        break

Jeżeli natomiast chciałbyś parsować wszystkie linki w paginacji (czyli te widoczne na stronie jako 1, 2, 3 itd.) rekurencyjnie to musiałbyś dodatkowo trzymać w jakimś zbiorze które linki już odwiedziłeś. Przykładowo, po wejściu na http://infostrefa.com/infostrefa/pl/wiadomosci/espi/3?source=espi (strona 3) dalej masz w paginacji link do http://infostrefa.com/infostrefa/pl/wiadomosci/espi/2?source=espi (strona 2), którą już widziałeś, więc musisz ją odfiltrować. Ponownie polecam użycie scrapy, które robi tego typu rzeczy za ciebie.

0
Spearhead napisał(a):

Parsowanie jest rekurencyjnie. W twoim przypadku masz przycisk "Dalej", więc za każdym razem jak sparsujesz stronę sprawdzasz, czy ten przycisk istnieje i jeśli tak, parsujesz kolejną. Z grubsza kod wygląda tak:

import requests
import bs4

def parse(url):
    r = requests.get('http://infostrefa.com/' + url)
    soup = bs4.BeautifulSoup(r.text)

    # sprasuj soup jak poprzednio

    next = soup.find('a', class_='nav-next')
    if next:
        parse(next['href'])

parse('infostrefa/pl/wiadomosci/espi/1')

Niestety, Python nie nadaje się do takich rekurencji ze względu na ograniczenie głębokości stosu, więc trzeba to przepisać aby zlikwidować rekurencję. Ponieważ zawsze sprawdzamy tylko jeden link, kod może wyglądać mniej więce tak:

import requests
import bs4

next_page = 'infostrefa/pl/wiadomosci/espi/1'
while True:
    r = requests.get('http://infostrefa.com/' + next_page)
    soup = bs4.BeautifulSoup(r.text)

    # sprasuj soup jak poprzednio

    next = soup.find('a', class_='nav-next')
    if next:
        next_page = next['href']
    else:
        break

Jeżeli natomiast chciałbyś parsować wszystkie linki w paginacji (czyli te widoczne na stronie jako 1, 2, 3 itd.) rekurencyjnie to musiałbyś dodatkowo trzymać w jakimś zbiorze które linki już odwiedziłeś. Przykładowo, po wejściu na http://infostrefa.com/infostrefa/pl/wiadomosci/espi/3?source=espi (strona 3) dalej masz w paginacji link do http://infostrefa.com/infostrefa/pl/wiadomosci/espi/2?source=espi (strona 2), którą już widziałeś, więc musisz ją odfiltrować. Ponownie polecam użycie scrapy, które robi tego typu rzeczy za ciebie.

Po podstawieniu mój kod wygląda następująco

import requests
import bs4

next_page = 'infostrefa/pl/wiadomosci/espi/1'
while True:
    r = requests.get('http://infostrefa.com/' + next_page)
    soup = bs4.BeautifulSoup(r.text)

    table = soup.find("table", class_="table-data")

    current_date, current_time, current_text = None, None, None
    for tr in table.find_all("tr"):
        divider = tr.find('td', class_='divider')
        if divider:
            current_date = divider.text
            continue
        time = tr.find('td', class_='time')
        if time:
            current_time = time.text
        text = tr.find('td', class_='text')
        if text:
            current_text = text.text
        print(u"{}\n{}\n{}\n\n".format(current_date, current_time, current_text))

    next = soup.find('a', class_='nav-next')
    if next:
        next_page = next['href']
    else:
        break

Jednak po sparsowaniu pierwszej strony kazuje mi się błąd:
screenshot-20190715052924.png

Z kolei jak w linii 6. tj. r=request.get zamienię link tak aby dane były pobierane z zadanego okresu a nie najnowsze, to po sparsowaniu pierwszej strony otrzymuję:
screenshot-20190715053159.png

0

Jednak po sparsowaniu pierwszej strony kazuje mi się błąd:
screenshot-20190715052924.png

Po zrobieniu print(url) widać, że o ile pierwszy url jest złożony dobrze, to kolejny wygląda tak http://infostrefa.com//infostrefa/pl/wiadomosci/espi/2 zamiast http://infostrefa.com/infostrefa/pl/wiadomosci/espi/2. Na szybko i byle jak można to naprawić tak:

next_page = '/infostrefa/pl/wiadomosci/espi/1'
while True:
    url = 'http://infostrefa.com' + next_page

Tak naprawdę jednak generalnie nie należy samemu manipulować url-ami - jak widać prowadzi to do różnych błędów. Zamiast tego należałoby skorzystać albo z urljoij, albo biblioteki takiej jak furl. Scrapy (wciąż namawiam) ma response.follow i response.urljoin.

Z kolei jak w linii 6. tj. r=request.get zamienię link tak aby dane były pobierane z zadanego okresu a nie najnowsze, to po sparsowaniu pierwszej strony otrzymuję:
screenshot-20190715053159.png

Ale to byś musiał podać na co zamieniasz, tak to nie mam jak debugować. Spróbuj dodać printy w strategicznych miejscach i obserwować czy wszędzie dane są jak trzeba.

0
Spearhead napisał(a):

Jednak po sparsowaniu pierwszej strony kazuje mi się błąd:
screenshot-20190715052924.png

Po zrobieniu print(url) widać, że o ile pierwszy url jest złożony dobrze, to kolejny wygląda tak http://infostrefa.com//infostrefa/pl/wiadomosci/espi/2 zamiast http://infostrefa.com/infostrefa/pl/wiadomosci/espi/2. Na szybko i byle jak można to naprawić tak:

next_page = '/infostrefa/pl/wiadomosci/espi/1'
while True:
    url = 'http://infostrefa.com' + next_page

Tak naprawdę jednak generalnie nie należy samemu manipulować url-ami - jak widać prowadzi to do różnych błędów. Zamiast tego należałoby skorzystać albo z urljoij, albo biblioteki takiej jak furl. Scrapy (wciąż namawiam) ma response.follow i response.urljoin.

Z kolei jak w linii 6. tj. r=request.get zamienię link tak aby dane były pobierane z zadanego okresu a nie najnowsze, to po sparsowaniu pierwszej strony otrzymuję:
screenshot-20190715053159.png

Ale to byś musiał podać na co zamieniasz, tak to nie mam jak debugować. Spróbuj dodać printy w strategicznych miejscach i obserwować czy wszędzie dane są jak trzeba.

Pisząc o zamianie URL miałem na myśli URL, który zawiera zdefiniowany filtr daty (zależy mi na depeszach z okresu 01.2015 - 12.2018). Poniżej wklejam cały kod

next_page = 'infostrefa/pl/wiadomosci/espi/1'

while True:
    r = requests.get('http://infostrefa.com/infostrefa/pl/wiadomosci/szukaj/1?dateFrom=2015-01-01&dateTo=2018-12-31&company=77&source=espi&category=wszystko,tematy-dnia,o-rynku,analizy,pap-o-spolkach,rekomendacje,raporty-dm,gpw-knf,rynki-zagraniczne' + next_page)
    soup = bs4.BeautifulSoup(r.text)

    table = soup.find("table", class_="table-data")

    current_date, current_time, current_text = None, None, None
    for tr in table.find_all("tr"):
        divider = tr.find('td', class_='divider')
        if divider:
            current_date = divider.text
            continue
        time = tr.find('td', class_='time')
        if time:
            current_time = time.text
        text = tr.find('td', class_='text')
        if text:
            current_text = text.text
        print(u"{}\n{}\n{}\n\n".format(current_date, current_time, current_text))

    next = soup.find('a', class_='nav-next')
    if next:
        next_page = next['href']
    else:
        break

Co masz na myśli pisząc "strategiczne miejsca"? Może głupie pytanie, ale to moje pierwsze kroki.

0

Co masz na myśli pisząc "strategiczne miejsca"? Może głupie pytanie, ale to moje pierwsze kroki.

Wypisujesz zawartość głównych zmiennych, czy ich zawartość odpowiada oczekiwaniom. Na przykład wypiszmy sobie url-e, do których uderzamy:

next_page = 'infostrefa/pl/wiadomosci/espi/1'

while True:
    r = requests.get('http://infostrefa.com/infostrefa/pl/wiadomosci/szukaj/1?dateFrom=2015-01-01&dateTo=2018-12-31&company=77&source=espi&category=wszystko,tematy-dnia,o-rynku,analizy,pap-o-spolkach,rekomendacje,raporty-dm,gpw-knf,rynki-zagraniczne' + next_page)

    print(r.url)
    # ...

Zobaczymy, że kod próbuje wbijać na taki url:

http://infostrefa.com/infostrefa/pl/wiadomosci/szukaj/1?dateFrom=2015-01-01&dateTo=2018-12-31&company=77&source=espi&category=wszystko,tematy-dnia,o-rynku,analizy,pap-o-spolkach,rekomendacje,raporty-dm,gpw-knf,rynki-zagraniczne/infostrefa/pl/wiadomosci/szukaj/2?company=77&dateFrom=2015-01-01&dateTo=2018-12-31&source=espi&category=wszystko,tematy-dnia,o-rynku,analizy,pap-o-spolkach,rekomendacje,raporty-dm,gpw-knf,rynki-zagraniczneinfostrefa/pl/wiadomosci/espi/1

Wklejamy w przeglądarkę i widać, że jest niepoprawny. Poprawny otrzymujemy po kliknięciu na przycisk "dalej" i wygląda tak:

http://infostrefa.com/infostrefa/pl/wiadomosci/szukaj/2?company=77&dateFrom=2015-01-01&dateTo=2018-12-31&source=espi&category=wszystko,tematy-dnia,o-rynku,analizy,pap-o-spolkach,rekomendacje,raporty-dm,gpw-knf,rynki-zagraniczneinfostrefa/pl/wiadomosci/espi/1

Z tego wniosek, że nasze konstruowanie url-i jest do bani. Zamiast chamskiego dodawania zatem warto by użyć innych metod, jak chociażby zalinkowanego już wcześniej urljoin czy biblioteki furl. Z grubsza wychodzi coś takiego (nie sprawdzone)

import urlparse 

import requests
import bs4


page_to_scrape = 'http://infostrefa.com/infostrefa/pl/wiadomosci/szukaj/1?company=77&dateFrom=2015-01-01&dateTo=2018-12-31&source=espi&category=wszystko,tematy-dnia,o-rynku,analizy,pap-o-spolkach,rekomendacje,raporty-dm,gpw-knf,rynki-zagraniczneinfostrefa/pl/wiadomosci/espi/1'

while True:
    r = requests.get(page_to_scrape)

    soup = bs4.BeautifulSoup(r.text)
    table = soup.find("table", class_="table-data")

    current_date, current_time, current_text = None, None, None
    for tr in table.find_all("tr"):
        divider = tr.find('td', class_='divider')
        if divider:
            current_date = divider.text
            continue
        time = tr.find('td', class_='time')
        if time:
            current_time = time.text
        text = tr.find('td', class_='text')
        if text:
            current_text = text.text
        print(u"{}\n{}\n{}\n\n".format(current_date, current_time, current_text))

    next = soup.find('a', class_='nav-next')
    if next:
        next_page = urlparse.urljoin(page_to_scrape, next['href'])
    else:
        break

1 użytkowników online, w tym zalogowanych: 0, gości: 1