Daten (im Sinne von Datum) aus Webseiten extrahieren

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
kevla89
User
Beiträge: 4
Registriert: Samstag 22. März 2014, 01:30

Hallo zusammen,

kann mir jemand bei folgendem Code behilflich sein, bisscen kommentieren wäre sehr für das Verständnis beitragend, bin nicht fit in Python.
Es geht darum, dass Daten von Webseiten extrahiert werden sollen.

Vielen Dank
Kevin

Code: Alles auswählen

import bs4
import csv
from datetime import datetime
import requests
import re
import sys


DATE_OUTPUT_FORMAT = '%Y-%m-%d'

MONTHS = 'january,february,march,april,may,june,july,august,september,october,november,december'.split(',')
MONTHS_SHORT = [m[:3] for m in MONTHS]

NUM_RE = r'(?P<%s>\d{%s})'
YEAR_RE = NUM_RE % ('year', 4)
or_re = lambda x: '|'.join(x)
MONTH_RE = r'(?P<month>\d{1,2}|%s|%s)' % (or_re(MONTHS), or_re(MONTHS_SHORT))
DAY_RE = NUM_RE % ('day', '1,2')

//ab hier werden wohl die regulären Ausdrücke definiert
ISO_RE = re.compile(r'%s-%s-%s' % (YEAR_RE, MONTH_RE, DAY_RE))
SPOKEN_RE = re.compile(r'%s %s(?:,|, | | of )%s' % (MONTH_RE, DAY_RE, YEAR_RE))
DATES_RES = ISO_RE, SPOKEN_RE
assert ISO_RE.match('2000-01-01')
assert ISO_RE.match('2000-1-1')
assert not ISO_RE.match('000-01-01.2000-01-0')
assert ISO_RE.match('2000-feb-01')
assert SPOKEN_RE.match('february 1, 2000')
assert SPOKEN_RE.match('february 1,2000')
assert SPOKEN_RE.match('february 1 2000')
assert SPOKEN_RE.match('february 1 of 2000')

def sentences(content):
    if not content:
        return        
    for a in content.split('\n'):
        for s in a.split('. '):
            s = s.strip()
            if s.endswith('.'):
                s = s[:-1]
            yield s
_s = lambda x: list(sentences(x))
assert _s('') == []
assert _s('a') == ['a']
assert _s(' a ') == ['a']
assert _s('a.') == ['a']
assert _s('a a') == ['a a']
assert _s('a.a') == ['a.a']
assert _s('a. a') == ['a', 'a']
assert _s('a\na.') == ['a', 'a']

def parse_month(s):
    if s.isdigit():
        return int(s)
    if s in MONTHS:
        return MONTHS.index(s) + 1
    if s in MONTHS_SHORT:
        return MONTHS_SHORT.index(s) + 1

assert parse_month('1') == 1
assert parse_month('12') == 12
# we're not filtering yet
assert parse_month('13') == 13
assert parse_month('jan') == 1
assert parse_month('dec') == 12
assert parse_month('january') == 1
assert parse_month('december') == 12

def looks_like_date(year, month, day):
    return ((1000 <= year <= 3000) and
        (1 <= month <= 12) and
        (1 <= day <= 31))

def extract_dates(content):
    for sentence in sentences(content):
        for date_re in DATES_RES:
            for d in date_re.finditer(sentence.lower()):
                m = date_re.match(d.group(0))
                g = m.groupdict()
                year, month, day = int(g['year']), parse_month(g['month']), int(g['day'])
                if looks_like_date(year, month, day):
                    yield datetime(year, month, day), sentence
_extract = lambda x: list(extract_dates(x))
d = datetime(2000, 1, 1)
d2 = datetime(2000, 1, 2)
assert _extract("") == []
assert _extract("2000-01-01") == [(d, "2000-01-01")]
assert _extract("2000-1-1") == [(d, "2000-1-1")]
assert _extract(" 2000-01-01 ") == [(d, "2000-01-01")]
assert _extract("\t2000-01-01\t") == [(d, "2000-01-01")]
assert _extract("\n2000-01-01\n") == [(d, "2000-01-01")]
assert _extract("\r\n2000-01-01\r\n") == [(d, "2000-01-01")]
assert _extract("2000-01-01.2000-01-01") == [(d, "2000-01-01.2000-01-01")] * 2
assert _extract("2000-01-01. 2000-01-01") == [(d, "2000-01-01")] * 2
assert _extract("2000-01-01.2000-01-02") == [(d, "2000-01-01.2000-01-02"), (d2, "2000-01-01.2000-01-02")]
assert _extract("2000-01-01\n2000-01-01") == [(d, "2000-01-01")] * 2
assert _extract("In the year 2000-01-01") == [(d, "In the year 2000-01-01")]
assert _extract("2000-jan-01") == [(d, "2000-jan-01")]
assert _extract("2000-Jan-01") == [(d, "2000-Jan-01")]
assert _extract("January 1, 2000") == [(d, "January 1, 2000")]


def body_text(content):
    for parser in ["html5lib", "lxml", "html.parser"]:
        try:
            return bs4.BeautifulSoup(content).text
        except:
            pass

def main():
    if len(sys.argv) != 2:
        print "usage: python dates.py http://example.com/page/etc"
    url = sys.argv[1]
    response = requests.get(url)
    body = body_text(response.content)

    csvfile = csv.writer(sys.stdout)
    for date, sentence in extract_dates(body):
        csvfile.writerow((date.strftime(DATE_OUTPUT_FORMAT), sentence))

if __name__ == '__main__':
    main()
Zuletzt geändert von Anonymous am Samstag 22. März 2014, 01:59, insgesamt 1-mal geändert.
Grund: Code in Python-Code-Tags gesetzt.
BlackJack

@kevla89: Wo kommst Du denn nicht weiter? Hast Du das Tutorial in der Python-Dokumentation schon durchgearbeitet? Dort gibt es bei den Howto's auch eines zu regulären Ausdrücken.

Das der Autor den Testcode einfach so auf Modulebene zwischen die Funktionen geklatscht hat macht den Quelltext für meinen Geschmack unübersichtlich und schlechter lesbar, auf der anderen Seite gibt er Dir natürlich Anhaltspunkte was die Funktionen machen oder zumindest wie Argumente aussehen und was da jeweils als Ergebnis erwartet wird.

Die `body_text()`-Funktion enthält teilweise Unsinn, denn die kann man auch einfach so schreiben:

Code: Alles auswählen

def body_text(content):
    try:
        return bs4.BeautifulSoup(content).text
    except:
        return None
BlackJack

Ich hab's mal ein wenig überarbeitet. Die Tests in Funktionen gesteckt, die ganzen nichtssagenden, einbuchstabigen Namen durch etwas aussagekräftigeres ersetzt. Und ein paar Fehler beseitigt. Besonders sinnfrei erschien mir das hier:

Code: Alles auswählen

            for d in date_re.finditer(sentence.lower()):
                m = date_re.match(d.group(0))
                g = m.groupdict()
Den regulären Ausdruck noch einmal auf genau das Anwenden das er bereits gematcht hat, gibt natürlich wieder genau das selbe Ergebnis. `m` ist also total überflüssig. Was einem sehr wahrscheinlich aufgefallen wäre, wenn man sich dafür einen beschreibenden Namen ausgedacht hätte.

Bei den Testfunktionen habe ich es bei den reinen ``assert``\s belassen. In realem Code hätte ich `nose` oder `py.test` dazu genommen. Bei `test_extract_sentences()` habe ich den letzten Testfall geändert, denn das ursprünglich erwartete Ergebnis passt nicht zum Namen der Funktion. Dieser Test schlägt nun also fehl.

`parse_month()` reagiert auf falsche Eingaben.

`looks_like_date()` ist rausgeflogen. Wenn es kein gültiges Datum ist, dann wird der `datetime()`-Aufruf fehlschlagen. Darauf muss man sowieso reagieren, denn die `looks_like_date()`-Funktion lässt auch Werte durchgehen, die keine Datumsangaben sind.

Die `get_body_text()`-Funktion liefert nun ihrem Namen entsprechend tatsächlich nur den Text aus dem <body> und nicht mehr aus dem gesamten <html>-Dokument.

Code: Alles auswählen

#!/usr/bin/env python
import csv
import re
import sys
from calendar import month_name, month_abbr
from datetime import datetime as DateTime
from itertools import chain
from locale import getlocale, setlocale, LC_TIME

import bs4
import requests
 
DATE_OUTPUT_FORMAT = '%Y-%m-%d'


def create_month_names(locale='C'):
    try:
        old_locale = getlocale(LC_TIME)
        setlocale(LC_TIME, locale)
        return (
            [n.lower() for n in names] for names in [month_name, month_abbr]
        )
    finally:
        setlocale(LC_TIME, old_locale)


MONTHS, MONTHS_SHORT = create_month_names()
NUM_RE = r'(?P<%s>\d{%s})'
YEAR_RE = NUM_RE % ('year', 4)
MONTH_RE = (
    r'(?P<month>\d{1,2}|%s)'
        % '|'.join(re.escape(m) for m in chain(MONTHS, MONTHS_SHORT) if m)
)
DAY_RE = NUM_RE % ('day', '1,2')
 
ISO_RE = re.compile(r'%s-%s-%s' % (YEAR_RE, MONTH_RE, DAY_RE))
SPOKEN_RE = re.compile(r'%s %s(?:,|, | | of )%s' % (MONTH_RE, DAY_RE, YEAR_RE))
DATES_RES = ISO_RE, SPOKEN_RE


def test_iso_re():
    assert ISO_RE.match('2000-01-01')
    assert ISO_RE.match('2000-1-1')
    assert not ISO_RE.match('000-01-01.2000-01-0')
    assert ISO_RE.match('2000-feb-01')


def test_spoken_re():
    assert SPOKEN_RE.match('february 1, 2000')
    assert SPOKEN_RE.match('february 1,2000')
    assert SPOKEN_RE.match('february 1 2000')
    assert SPOKEN_RE.match('february 1 of 2000')
 

def extract_sentences(content):
    for lines in content.split('\n'):
        for sentence in lines.split('. '):
            sentence = sentence.strip()
            if sentence.endswith('.'):
                sentence = sentence[:-1]
            if sentence:
                yield sentence


def test_extract_sentences():
    
    def extract_to_list(content):
        return list(extract_sentences(content))

    assert extract_to_list('') == []
    assert extract_to_list('a') == ['a']
    assert extract_to_list(' a ') == ['a']
    assert extract_to_list('a.') == ['a']
    assert extract_to_list('a a') == ['a a']
    assert extract_to_list('a.a') == ['a.a']
    assert extract_to_list('a. a') == ['a', 'a']
    assert extract_to_list('a\na.') == ['a a']


def parse_month(string):
    try:
        result = int(string)
        if not 1 <= result <= 12:
            raise ValueError('month value {0} not in 1..12'.format(result))
        return result
    except ValueError:
        try:
            return MONTHS.index(string)
        except ValueError:
            try:
                return MONTHS_SHORT.index(string)
            except ValueError:
                pass  # Intentionally ignored.
    raise ValueError('{0!r} is not a valid month name'.format(string))


def test_parse_month():
    assert parse_month('1') == 1
    assert parse_month('12') == 12
    try:
        parse_month('13') == 13
    except ValueError:
        pass
    else:
        assert False
    assert parse_month('jan') == 1
    assert parse_month('dec') == 12
    assert parse_month('january') == 1
    assert parse_month('december') == 12


def extract_dates(content):
    for sentence in extract_sentences(content):
        for date_re in DATES_RES:
            for date_match in date_re.finditer(sentence.lower()):
                try:
                    year = int(date_match.group('year'))
                    month = parse_month(date_match.group('month'))
                    day = int(date_match.group('day'))
                    yield DateTime(year, month, day), sentence
                except ValueError:
                    pass  # Intentionally ignored.


def test_extract_dates():
    
    def _extract(content):
        return list(extract_dates(content))
    
    date_a = DateTime(2000, 1, 1)
    date_b = DateTime(2000, 1, 2)
    assert _extract('') == []
    assert _extract('2000-01-01') == [(date_a, '2000-01-01')]
    assert _extract('2000-1-1') == [(date_a, '2000-1-1')]
    assert _extract(' 2000-01-01 ') == [(date_a, '2000-01-01')]
    assert _extract('\t2000-01-01\t') == [(date_a, '2000-01-01')]
    assert _extract('\n2000-01-01\n') == [(date_a, '2000-01-01')]
    assert _extract('\r\n2000-01-01\r\n') == [(date_a, '2000-01-01')]
    assert (
        _extract('2000-01-01.2000-01-01')
            == [(date_a, '2000-01-01.2000-01-01')] * 2
    )
    assert _extract('2000-01-01. 2000-01-01') == [(date_a, '2000-01-01')] * 2
    assert (
        _extract('2000-01-01.2000-01-02') == [
            (date_a, '2000-01-01.2000-01-02'),
            (date_b, '2000-01-01.2000-01-02'),
        ]
    )
    assert _extract('2000-01-01\n2000-01-01') == [(date_a, '2000-01-01')] * 2
    assert (
        _extract('In the year 2000-01-01')
            == [(date_a, 'In the year 2000-01-01')]
    )
    assert _extract('2000-jan-01') == [(date_a, '2000-jan-01')]
    assert _extract('2000-Jan-01') == [(date_a, '2000-Jan-01')]
    assert _extract('January 1, 2000') == [(date_a, 'January 1, 2000')]
 
 
def get_body_text(content):
    return bs4.BeautifulSoup(content).body.text


def main():
    if len(sys.argv) != 2:
        print 'usage: {0} http://example.com/page/etc'.format(sys.argv[0])
    else:
        try:
            response = requests.get(sys.argv[1])
        except requests.RequestException as error:
            print error
            sys.exit(1)
        body = get_body_text(response.content)
        csv_file = csv.writer(sys.stdout)
        for date, sentence in extract_dates(body):
            csv_file.writerow((date.strftime(DATE_OUTPUT_FORMAT), sentence))
 

if __name__ == '__main__':
    main()
Benutzeravatar
darktrym
User
Beiträge: 785
Registriert: Freitag 24. April 2009, 09:26

Kommt mir das nur so vor dass der Code nicht besonders robust ist. Alleine die Funktion extrahiere Sätze kann ziemlich einfach aufs Glatteis geführt werden.
„gcc finds bugs in Linux, NetBSD finds bugs in gcc.“[Michael Dexter, Systems 2008]
Bitbucket, Github
BlackJack

@darktrym: Wie schon gesagt, macht die Funktion auch nicht wirklich genau das was der Name endeutet denn selbst wenn es nur Punkte gibt, und wir mal davon ausgehen das jeder ein Satzende markiert, muss die Funktion nicht den ganzen Satz liefern. Und *das* scheint sogar gewollt den ursprünglichen ``assert``\s nach zu urteilen.

Auf der anderen Seite kann man so ein allegmeines „extrahiere Datumsangaben” auch gar nicht robust umsetzen, dafür gibt es a) zu viele Möglichkeiten ein Datum zu schreiben, und b) sind die nicht alle von anderen Informationen unterscheidbar, die nach Datum aussehen, aber keines sind.
kevla89
User
Beiträge: 4
Registriert: Samstag 22. März 2014, 01:30

Wow danke Blackjack,
also offen gesagt kann ich Python gar nicht, nur eben ein bisschen Java, ich habe einen Freund um Hilfe gebeten und er hat es direkt komplett umgesetzt, nur eine regular Expression für das Deutsche Format wäre noch sinnvoll, aber das ist auch nicht so wichtig, die Sache ist eben nur, dass er es eben überraschenderweise in Python gemacht hat und nicht in Java und ich es jetzt nicht verstehe.
Eine Codedokumentation würde sehr sehr helfen, auch wenn es um die einfachsten Sachen geht, also via \\ bei einigen Zeilen einsetzen wie das bei Python so läuft.

Das wäre super:) ich bin am verzweifeln
LG
kevla89
User
Beiträge: 4
Registriert: Samstag 22. März 2014, 01:30

?
.
Antworten