Web scraping html class, Zugriff

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Benutzeravatar
Strawk
User
Beiträge: 227
Registriert: Mittwoch 15. Februar 2017, 11:42
Wohnort: Aachen
Kontaktdaten:

Hallo Nutzer!
Im Rahmen von web scraping mit Python möchte ich auf bestimmte Klassen des HTML-Codes zugreifen. Ich habe das schon ausführlich gegoogelt; nichts fruchtet. Hier mein bisheriger Code:

Code: Alles auswählen

# import necessary modules
import requests, bs4, os

# create folder
os.makedirs('forum_messages', exist_ok=True)

# 1st request
res = requests.get('https://www.phpbb.de/community/')

# gain all html-code
soup = bs4.BeautifulSoup(res.text, 'html.parser')

# CSS-selector
linkElems = soup.select('div', class_='topictitle')

for elem in linkElems:
    print(elem)
Erbitte Hilfe!
Grüße
Strawk
Ich programmiere erfolglos, also bin ich nicht.
Sirius3
User
Beiträge: 17712
Registriert: Sonntag 21. Oktober 2012, 17:20

Die Klasse `topictitle` gibt es auf der von Dir verlinkten Seite auch nicht.
Benutzeravatar
__blackjack__
User
Beiträge: 13006
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Strawk: Von `res.text` würde ich hier abraten und lieber `res.content` verwenden. Es kommt häufiger vor, dass die (implizite) Kodierung nicht mit der tatsächlichen im HTML-Dokument übereinstimmt und die Angabe im HTML-Dokument stimmt in der Regel, wird aber nicht ausgewertet wenn man dem Parser schon dekodierten Text übergibt.

Edit: Der Kommentar und der `select()`-Aufruf passen nicht, denn das was Du da übergibst würde man `find_all()` übergeben und nicht `select()`. Ein CSS-Selektor wäre 'div.topictitle'.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Benutzeravatar
Strawk
User
Beiträge: 227
Registriert: Mittwoch 15. Februar 2017, 11:42
Wohnort: Aachen
Kontaktdaten:

Hallo Nutzer,

danke für die hilfreichen Tipps. Ziel: alle Nachrichten aus einem Unterforum downloaden. Programm Stand jetzt:

Code: Alles auswählen

# import necessary modules
import requests, bs4, os

# create folder
os.makedirs('forum_messages', exist_ok=True)

# 1st request
res = requests.get('https://www.phpbb.de/community/viewforum.php?f=145')

# gain all html-code
soup = bs4.BeautifulSoup(res.content, 'html.parser')

# correct comment?
linkElems = soup.select('a.topictitle')

for elem in linkElems:
    # edit link-string
    elemedit = str(elem)
    elemedit = elemedit[44:]
    elemedit = elemedit[:19]
    elemedit = elemedit.replace('&', '&')
    pos = elemedit.rfind("\"")
    elemedit = elemedit[:pos]
    elem = elemedit
    
    # 2nd request (plus link-branch)
    res = requests.get('https://www.phpbb.de/community/viewtopic?' + elem)
    # print('https://www.phpbb.de/community/viewtopic.php?' + elem)
    
    # gain all html-code
    soup = bs4.BeautifulSoup(res.content, 'html.parser')
    
    # correct comment?
    linkElems = soup.select('a.content')
    for elem in linkElems:
        print(elem)
Ich erhalte (noch) keine Ergebnisse. Bitte Hilfe.
Grüße
Strawk
Ich programmiere erfolglos, also bin ich nicht.
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich bekomme auch eine Fehlermeldung fuer eine deiner zusammengebastelten URLs: https://www.phpbb.de/community/viewtopic?f=145&t=232547 - No route found for "GET /viewtopic"

Wenn man statt da mit str und irgendwelchen Indizes gleich einfach das Attribut abfragt, dann geht's auch. Und a.content gibbet auch nicht, das war ja schon mal div.content etwas weiter oben hier in der Disukssion...

Code: Alles auswählen

# import necessary modules
import requests, bs4, os

# create folder
#os.makedirs('forum_messages', exist_ok=True)

# 1st request
res = requests.get('https://www.phpbb.de/community/viewforum.php?f=145')

# gain all html-code
soup = bs4.BeautifulSoup(res.content, 'html.parser')

# correct comment?
linkElems = soup.select('a.topictitle')

for elem in linkElems:
    # edit link-string
    href = elem["href"]
    url = 'https://www.phpbb.de/community{}'.format(href[1:])
    print(url)
    res = requests.get(url)
    # gain all html-code
    soup = bs4.BeautifulSoup(res.content, 'html.parser')
    # correct comment?
    linkElems = soup.select('div.content')
    for elem in linkElems:
        print(elem)
Benutzeravatar
__blackjack__
User
Beiträge: 13006
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Strawk: Was auf jeden Fall falsch ist: Das umwandeln eines Elements in eine Zeichenkette um da dann mit Zeichenkettenoperationen drauf zu operieren um Teile von Attributwerten daraus zu extrahieren. Greif über das Element-Objekt direkt auf die Attributwerte zu.

Bei einer URL für `requests` würde ich die Parameter auch nicht selbst als Zeichenkette da rein basteln sondern die entsprechenden Argumente dafür verwenden.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Benutzeravatar
Strawk
User
Beiträge: 227
Registriert: Mittwoch 15. Februar 2017, 11:42
Wohnort: Aachen
Kontaktdaten:

Hallo!

Das sieht schon ganz prima aus. :)

Danke!

Jetzt wäre das Ziel, jede Nachricht als .txt im Ordner "forum_messages" zu speichern. Ich hatte an die Variable i gedacht, welche inkrementiert, sodass die .txt "1.txt", "2.txt", "3.txt" usw. heißen. Eleganter wäre freilich ein Dateiname aus den ersten drei Wörtern der Nachricht o.ä.

Hat da jemand einen guten Tipp?

Grüße
Strawk

Code: Alles auswählen

# import necessary modules
import requests, bs4, os

# create folder
os.makedirs('forum_messages', exist_ok=True)

# 1st request
res = requests.get('https://www.phpbb.de/community/viewforum.php?f=145')

# gain all html-code
soup = bs4.BeautifulSoup(res.content, 'html.parser')

# correct comment?
linkElems = soup.select('a.topictitle')

for elem in linkElems:
    # edit link-string
    href = elem["href"]
    url = 'https://www.phpbb.de/community{}'.format(href[1:])
    # print(url)
    res = requests.get(url)
    # gain all html-code
    soup = bs4.BeautifulSoup(res.content, 'html.parser')
    # correct comment?
    linkElems = soup.select('div.content')
    i = 1
    for elem in linkElems:
        print(elem)
        print('\n')
        
        # open for writing
        pageFile = open(os.path.join('forum_messages/', + i + '.txt'), 'wb')
    
        # write into files
        for chunk in res.iter_content(100000):
            pageFile.write(chunk)
        pageFile.close()
        i += 1
Ich programmiere erfolglos, also bin ich nicht.
Benutzeravatar
__blackjack__
User
Beiträge: 13006
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Strawk: Du kommentierst zu viel. Kommentare sollten nicht beschreiben *was* der Code macht, denn das beschreibt der Code bereits, sondern *warum* er das (so) macht. Sofern das nicht offensichtlich ist.

Ein Kommentar der vor dem Import sagt, das da die notwendigen Module importiert werden, bringt dem Leser beispielsweise überhaupt keinen Mehrwert.

Namen sollten nicht Abgekürzt werden wenn es nicht allgemein geläufige Abkürzungen sind. `res` sollte also `response` heissen, damit man nicht rätseln muss ob das `res` für `response`, `result`, `resource`, oder sonstwas steht. `elem` hiesse dann `element` wobei das ziemlich generisch ist. Der Leser möchte da vielleicht wissen *was* für ein Element das ist.

Namenskonvention ist klein_mit_unterstrichen für alles ausser Konstanten (KOMPLETT_GROSS) und Klassen (MixedCase).

Bei der inneren Schleife wird das mit den Namen dann verwirrend. Willst Du da Links selektieren (welche?) oder den Inhalt der Beiträge? Im letzteren Fall ist der Name `linkElem` sehr irreführend.

Code: Alles auswählen

import os
from urlparse import urljoin

import bs4
import requests


def main():
    os.makedirs('forum_messages', exist_ok=True)

    html_parser = 'html.parser'
    base_url = 'https://www.phpbb.de/community/'
    response = requests.get(
        urljoin(base_url, 'viewforum.php'), params={'f': '145'}
    )
    soup = bs4.BeautifulSoup(response.content, html_parser)
    for topic_link_element in soup.select('a.topictitle'):
        response = requests.get(urljoin(base_url, topic_link_element['href']))
        soup = bs4.BeautifulSoup(response.content, html_parser)
        for content_element in soup.select('div.content'):
            print(content_element)


if __name__ == '__main__':
    main()
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Benutzeravatar
__blackjack__
User
Beiträge: 13006
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Strawk: Du schreibst da wahrscheinlich lauter leere Dateien, denn `Response.iter_content()` wird nichts liefern wenn Du den Inhalt schon anderweitig ausgelesen hast. Falls doch schreibst Du die komplette Webseite für jeden Beitrag auf der Webseite.

Wenn das was Du da zeigst überhaupt kompilieren würde.

Als Dateiname, oder zumindest als Teil davon würde sich die Beitrags-ID anbieten, denn die ist eindeutig, im Gegensatz zum Titel oder gar nur den den ersten drei Worten des Titels.

*.txt ist irgendwie nicht die richtige Endung für Dateien die HTML(-Fragmente) enthalten.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Benutzeravatar
Strawk
User
Beiträge: 227
Registriert: Mittwoch 15. Februar 2017, 11:42
Wohnort: Aachen
Kontaktdaten:

Hallo!

Das verstehe ich. Werde mich adäquat bemühen.

Es fehlt noch der heiße Tipp zum Speichern der Ergebnisse in .txt-Dateien.

Grüße
Strawk

(da hat sich was überschnitten)
Ich programmiere erfolglos, also bin ich nicht.
Benutzeravatar
Strawk
User
Beiträge: 227
Registriert: Mittwoch 15. Februar 2017, 11:42
Wohnort: Aachen
Kontaktdaten:

Hallo Profis!

Sorry, da hatte sich was überschnitten.

Ich habe den Code überarbeitet und versucht, alle eure Tipps umzusetzen. Bitte Beurteilung, aber bitte erklärend darlegend und motivierend und nicht demotivierend. Danke.

Code: Alles auswählen

import requests, bs4, os

# create folder to take/contain the messages in html-format
os.makedirs('forum_messages', exist_ok=True)

# request main page/site
response = requests.get('https://www.phpbb.de/community/viewforum.php?f=145')

# gain all html-code
soup = bs4.BeautifulSoup(response.content, 'html.parser')

# select topics
topics = soup.select('a.topictitle')

# initialize counter for filename per single message
i = 1

# iterate through each topic
for topic in topics:
    # edit link-string
    href = topic["href"]
    url = 'https://www.phpbb.de/community{}'.format(href[1:])
    # print(url)
    response = requests.get(url)
    # gain all html-code
    soup = bs4.BeautifulSoup(response.content, 'html.parser')

    # select messages
    messages = soup.select('div.content')
    
    # iterate through each message
    for message in messages:
               
        # open single file for writing message into it
        pageFile = open(os.path.join('forum_messages/', str(i) + '.html'), 'wb')
    
        # transform message to be writable
        byteelement = bytearray(str(message), 'utf-8')
        
        # write message into single file
        pageFile.write(byteelement)
        
        # close single file
        pageFile.close()
        
        # increment counter for name of single file
        i += 1
Grüße
Strawk
Ich programmiere erfolglos, also bin ich nicht.
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich melde mich wenn ich meinen Didaktik Kurs abgeschlossen habe. Man will ja nicht unabsichtlich demotivieren.
Benutzeravatar
__blackjack__
User
Beiträge: 13006
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Strawk: Ich würde den Style Guide for Python Code ans Herz legen. Die Namensschreibweise(n) hatte ich ja schon einmal angesprochen. Die Importe nocht nicht: die sollten nicht alle in einer Anweisung zusammengefasst sein und sind üblicherweise nach Standardbibliothek und Modulen/Paketen von Drittanbietern getrennt.

Die Kommentare hatte ich auch schon erwähnt. Weg mit den nutzlosen, redundanten. Die bringen dem Leser keinen Mehrwert, bergen aber die Gefahr das entweder jetzt oder wenn der Code mal geändert wird, Kommentar und Code nicht mehr zusammen passen und der Leser dann nicht weiss wer recht hat, Kommentar oder Code.

Vom Sprachgefühl würde ich sagen bei `# gain all html-code` ist das `gain` falsch und zwischen html und code gehört kein Bindestrich.

Die einzigen Kommentarzeilen die ein bisschen Sinn machen sind die beim `i` — aber auch nur weil man da erklären muss wofür `i` überhaupt steht. Wenn man hier einen erklärenden Namen verwendet, können auch hier die Kommentare weg.

Daten und Code sollten sich nicht wiederholen. Wenn man etwas Ändern will, zum Beispiel das Verzeichnis in dem die Beiträge gespeichert werden, die Basis-URL des Forums, oder der Parser der für das HTML verwendet werden soll, muss man das an jeder Stelle im Code ändern, mit der Gefahr das man etwas übersieht, oder nicht jede Änderung äquivalent macht. Bei Daten kann man Konstanten definieren, und bei Code Schleifen oder Funktionen schreiben um Wiederholungen zu vermeiden.

Wegwerfen des ersten Zeichens der Links und formatieren in eine Zeichenkette ist fragiler als `urlparse.urljoin()` zu verwenden.

`os.path.join()` ist dazu da plattformunabhängig Pfadteile zusammenzusetzen — das klappt aber nicht wenn man dann doch wieder plattformunabhängige Pfadtrenner hart in die Daten schreibt.

Dateien öffnet man am besten zusammen mit der ``with``-Anweisung um sicherzustellen das die Datei beim verlassen des ``with``-Blocks geschlossen wird, egal warum der verlassen wird.

Wenn man die Datei nicht im Binärmodus öffnet, braucht man sich nicht in einem Extraschritt selbst um die Kodierung der Daten zu kümmern.

Die erzeugten Dateinamen sind ungünstig wenn man sie alphabetisch sortiert, was viele Werkzeuge machen, denn dann sind sie nicht in der richtigen numerischen Reihenfolge. Das kann man umgehen, in dem man die Zahlen links mit 0en auffüllt. Besser wäre IMHO aber wie schon in einem vorherigen Beitrag geschrieben, die Beitrags-ID aus dem Forum zu verwenden, denn die ist Eindeutig und kann nicht mehr geändert werden, oder zumindest ist das wesentlich unwahrscheinlicher als die laufende Nummer (wenn Beiträge gelöscht werden) oder der Beitragstitel der a) nicht eindeutig ist und b) geändert werden kann.

Last but not least: Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steckt üblicherweise in einer Funktion die `main()` heisst. Das verringert die Gefahr das Funktionen undurchsichtig über globalen Zustand zusammenhängen, und man kann das ganze so schreiben, das man das Modul importieren kann, ohne das gleich das Programm abläuft. So das man einzelne Funktionen manuell, zum Beispiel in einer Python-Shell, oder automatisiert testen kann, wiederverwenden kann. Ausserdem erwarten diverse Werkzeuge, zum Beispiel Sphinx, das man Module ohne Seiteneffekte importieren kann.

Ungetestet:

Code: Alles auswählen

import os
from urlparse import urljoin

import bs4
import requests

FORUM_BASE_URL = 'https://www.phpbb.de/community/'
MESSAGES_PATH = 'forum_messages'


def get_soup(url):
    return bs4.BeautifulSoup(requests.get(url).content, 'html.parser')


def main():
    os.makedirs(MESSAGES_PATH, exist_ok=True)

    message_count = 0
    soup = get_soup(urljoin(FORUM_BASE_URL, 'viewforum.php?f=145'))
    for topic in soup.select('a.topictitle'):
        soup = get_soup(urljoin(FORUM_BASE_URL, topic['href']))
        for message in soup.select('div.content'):
            message_count += 1
            # 
            # TODO Use post id in or as filename for a consistent
            #   mapping between filename and forum post.
            # 
            filename = os.path.join(
                MESSAGES_PATH, '{:06d}.html'.format(message_count)
            )
            with open(filename, 'w', encoding='utf-8') as message_file:
                message_file.write(str(message))


if __name__ == '__main__':
    main()
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Antworten