In [1]:
import json
import os
import nltk
import string
import re
import pandas as pd
from IPython.display import display
import numpy as np
import math
Der Crawler wurde mit Hilfe der Bibliothek 'scrapy' geschrieben. Die dazu implementierte Anwendung ist im Verzeichnis ~/sitespider zu finden und der eigentliche Crawler in der Datei ~/sitespider/sitespider/spiders/site_spider.py. Der Code dazu sieht wie folgt aus:
# -*- coding: utf-8 -*-
import scrapy
import re
import json
class SiteSpiderSpider(scrapy.Spider):
name = 'site_spider'
start_urls = [
'http://people.f4.htw-berlin.de/~zhangg/pages/teaching/pages/d01.html',
'http://people.f4.htw-berlin.de/~zhangg/pages/teaching/pages/d06.html',
'http://people.f4.htw-berlin.de/~zhangg/pages/teaching/pages/d08.html'
]
def parse(self, response):
id = response.url.split('/')[-1].split('.')[0]
with open("sites/%s.json" % id, 'w') as f:
data = {'id': id,
'text': "".join(line for line in response.css('body::text').extract()),
'url': response.url,
'back_links': [link.split('.')[0] for link in response.css('a::attr(href)').extract()]
}
json.dump(data, f)
for next_page in response.css('a::attr(href)').extract():
yield response.follow(next_page, callback=self.parse)
Die Klasse SiteSpiderSpider erbt von der Klasse scrapy.Spider. So kann die geschriebene Klasse später als Crawler benutzen werden.
Das Attribut name identifizieren den Crwaler und wird dazu benutzt, den Crawler später über die Konsole aufzurufen. Das Attribut start_urls legt fest, welche URLs vom Crawler initial angefragt werden sollen. Die Methode parse() nimmt dann anschließend die Rückgabe jeder Anfrage entgegen und bearbeitet diese. Da die Methode parse() die default Callback-Methode ist, muss diese nicht explizit angegeben werden.
Die parse()-Methode erzeugt einen String, der die Anfrage eindeutig beschreibt. Dieser Wert wird anschließend als Dateiname für die Datei verwendet, in der die Daten der Anfrage gespeichert werden. Gewähltes Fromat ist hierbei JSON. In der Datei selber wird die Id der Anfrage, der eigentliche Text ohne HTML-Tags, die URL und alle auf der Seite gefundenen Links als Array gespeichert. Anschließend wird für alle auf der Seite gefundenen Links derselbe Prozess mittels der Methode response.follow() angestoßen.
Der implementierte Crawler kann mittels des Konsolen-Befehls 'scrapy crawl site_spider' in dem Verzeichniss ~/sitespider aufgerufen werden. Die resultierenden Dateien sind in dem Unterordner ~/sitespider/sites/* zu finden
Folgend werden die PageRanks der heruntergeladenen Seiten berechnet.
Folgende Variablen werden verwendet:
In [2]:
directory = 'sitespider/sites'
files = [x[2] for x in os.walk(directory)][0]
pages = []
Im folgenden Code-Block werden die resultierenden Dateien einzeln ausgelesen und in der Variablen pages gespeichert.
In [3]:
for file in files:
with open("%s/%s" % (directory, file)) as json_data:
pages += [json.load(json_data)]
Die folgenden Konstenten sind vorgegeben bzw. müssen zuvor berechnet werden:
In [4]:
n = len(pages)
t = 0.05
d = 1 - t
δ = 0.04
PageRank-Funktion
Die Funktion calculate_page_rank() berechnet den PageRank für die übergebene Seite page_i. Folgende Formel wurde dabei implementiert:
In [5]:
def calculate_page_rank(page_i):
sum_result = 0
for page_j in pages:
if page_i['id'] in page_j['back_links']:
sum_result += page_j['rank'] / len(page_j['back_links'])
if len(page_j['back_links']) == 0:
sum_result += page_j['rank'] / n
return d * sum_result + t / n
Hilfe-Funktionen
Die Funktion initialize() initialisiert alle Seiten mit einem Start-PageRank, der sich durch die Division von eins durch die Anzahl aller Seiten berechnet. Zudem setzt sie den folgenden PageRank auf None.
In [6]:
def initialize():
for page in pages:
page['rank'] = 1/n
page['rank+1'] = None
Die Funktion termination_condition() gibt einen boolischen Wert zurück, der sagt, ob die Abbruchbedingung erfüllt ist oder nicht. Im Falle, dass es keine zwei Vergleichswerte gibt, also rank+1 gleich None ist, wird False zurückgegeben, ansonsten wird der Rückgabewert mit folgender Formel berechnet:
In [7]:
def termination_condition():
delta = 0
for page in pages:
if page['rank+1'] is None:
return False
else:
delta += abs(page['rank+1'] - page['rank'])
return delta <= δ
Die Funktion set_ranks() setzt, wenn rank+1 nicht None ist, für alle Seiten den älteren PageRank-Wert rank auf den aktuellen PageRank-Wert rank+1.
In [8]:
def set_ranks():
for page in pages:
if page['rank+1'] is not None:
page['rank'] = page['rank+1']
Im folgenden Code-Block wird zunächst die Methode initialize() aufgerufen, um alle Seiten mit einem initalen PageRank-Wert zu initialisieren. Anschließend wird so lange die While-Schleife durchlaufen, bis die Abbruchsbedingung erfüllt ist. Innerhalb der While-Schleife wird zunächst die Methode set_ranks() aufgerufen, um alle alten PageRanks auf den aktuellen Stand zu setzen. Anschließend wird jede Seite durchlaufen, wobei jeweils der nächste PageRank für die aktuelle Seite berechnet wird und in der Variablen rank+1 gespeichert wird.
In [9]:
initialize()
while not termination_condition():
set_ranks()
for page in pages:
page['rank+1'] = calculate_page_rank(page)
Die Resultate werden in der Datei rank.txt gespeichert. Dabei steht in der Datei zunächst die Id der Seite und anschließend der eigentliche PageRank-Wert.
In [10]:
with open('rank.txt', 'w') as f:
for page in pages:
f.write("%s: %s\n" % (page['id'], page['rank+1']))
Als Abschluß wird die Summe aller PageRanks berechnet, um zu prüfen, ob dieser Wert ungefähr 1 entspricht.
In [11]:
rank_sum = 0
for page in pages:
rank_sum += page['rank+1']
print(rank_sum)
Im folgenden Abschnitt wird ein TF-Index für alle Wörter, die in den Dokumenten (Seiten) auftauchen, berechnet.
Folgende Variablen werden verwendet:
In [12]:
tf_dict = {}
term_set = set()
stopwords = []
exclude = set(string.punctuation)
porter = nltk.PorterStemmer()
Gewichtete Term-Frequency
Die Funktion get_weighted_tf() brechnet mittels der folgenden Formel die gewichtete Term-Frequency für den übergebenen Term im Bezug auf das Dokument, mit der übergebenen Dokumenten-Id.
In [13]:
def get_weighted_tf(doc_id, term):
if term in tf_dict[doc_id]:
if tf_dict[doc_id][term] == 0:
return 0
else:
return 1 + math.log10(tf_dict[doc_id][term])
else:
return 0
Im folgenden Code-Block werden die Stopwörter aus der Datei stop_words.txt ausgelesen und in der Variablen stopwords gespeichert.
In [14]:
with open('stop_words.txt') as line:
stopwords += re.sub('[^a-zA-Z0-9,]', '', line.read()).split(',')
Um alle vorkommenden Wörter zu berechnen, werden alle Texte der Dokumente durchlaufen und mittels des nltk-Wörter-Tokinisierers tokinisiert. Jedes resultierende Wort wird anschließend analysiert und sofern es kein Zeichen oder ein Stopwort ist, dem term_set hinzugefügt.
In [15]:
for page in pages:
for term in nltk.word_tokenize(page['text']):
if term not in exclude and term not in stopwords:
term_set.add(porter.stem(term).lower())
Um den Term-Frequency-Indexes aufzustellen, wird jedes Dokument durchlaufen, wobei zunächst für jedes Wort aus dem term_set ein Wert von 0 initialisiert wird. Anschließend wird jedes Wort innherhalb des Dokumentes analysiert und, sofern es sich um kein Zeichen oder Stopwort handelt, der passende Zähler um eins erhöht.
In [16]:
for page in pages:
tf_dict[page['id']] = {}
for term in term_set:
tf_dict[page['id']][term] = 0
for term in nltk.word_tokenize(page['text']):
if term not in exclude and term not in stopwords:
tf_dict[page['id']][porter.stem(term).lower()] += 1
Das Resultat wird in der Datei index.txt gespeichert. Dabei stellt jede Zeile ein Wort und jede Spalte ein Dokument dar.
In [17]:
tf_df = pd.DataFrame(tf_dict)
tf_df.to_csv('index.txt', header=True, index=True, sep=';')
Folgend wird ein TF-Index für alle Wörter innerhalb der Dokumente (Seiten) berechnet.
Folgende Variablen werden verwendet:
In [18]:
df_dict = {}
Gewichtete Inverse-Document-Frequency
Die Funktion get_weighted_idf() berechnet mittels der folgenden Formel die gewichtete Inverse-Document-Frequency für den übergebenen Term.
In [19]:
def get_weighted_idf(term):
if term in df_dict:
if df_dict[term]['count'] == 0:
return 0
else:
return math.log10( n / df_dict[term]['count'])
else:
return 0
Gewichteter TF-IDF-Wert
Die Funktion get_weighted_tf_idf() brechnet mittels der folgenden Formel den gewichteten TF-IDF-Wert für den übergebenen Term im Bezug auf das Dokument mit der übergebenen Dokumenten-Id.
In [20]:
def get_weighted_tf_idf(doc_id, term):
return get_weighted_tf(doc_id, term) * get_weighted_idf(term)
Search
Die Funktion search() brechnet für jede Seite einen Wert, der aussagt, wie gut die jeweilge Seite zu den gesuchten Termen passt. Dabei berechnet sich der Wert ohne Einbeziehung des PageRanks wie folgt:
Mit Einbeziehung des PageRanks berechnet sich der Wert wie folgt:
In [21]:
def search(terms, page_rank=False):
result = {}
for page in pages:
result[page['id']] = 0
for term in terms:
result[page['id']] += get_weighted_tf_idf(page['id'], porter.stem(term).lower())
if page_rank:
result[page['id']] *= page['rank+1']
return result
Zur Berechnung der Document-Frequency werden alle Seiten durchlaufen, wobei jedes Wort des Textes der Seite betrachtet wird. Sofern das Wort kein Zeichen oder Stopwort ist und noch nicht im df_dict vorkommt, wird es neu mit der Document-Frequency 1 und der Dokumenten-Id angelegt. Existiert das Wort bereits im df_dict, wird nur dann die Document-Frequency um eins erhöht, wenn die aktuelle Dokumenten-Id noch nicht existiert.
In [22]:
for page in pages:
for term in nltk.word_tokenize(page['text']):
if term not in exclude and term not in stopwords:
if porter.stem(term).lower() in df_dict and page['id'] not in df_dict[porter.stem(term).lower()]['documents']:
df_dict[porter.stem(term).lower()]['count'] += 1
df_dict[porter.stem(term).lower()]['documents'] += [page['id']]
elif porter.stem(term).lower() not in df_dict:
df_dict[porter.stem(term).lower()] = {'count': 1,
'documents': [page['id']]
}
Gesucht wird nach folgenden Termen:
In [23]:
search_terms = [['token'],['index'],['classification'],['classification', 'token']]
Im folgenden Code-Block wird jeder Such-Term durchgegangen und der jeweilige Wert für jedes Dokument berechnet. Der Wert setzt sich aus dem TF-IDF-Wert zusammen. Anschließend wird das Ergebnis in der Datei pageranke_search.txt gespeichert.
In [24]:
with open('tfidf_search.txt', 'w') as f:
for search_term in search_terms:
f.write('Suchwort: %s\n\n' % ', '.join(search_term))
result = search(search_term)
for key in result:
f.write("%s: %s\n" % (key, result[key]))
f.write('\n\n')
Gesucht wird nach folgenden Termen:
In [25]:
search_terms = [['token'],['index'],['classification'],['classification', 'token']]
Im folgenden Code-Block wird jeder Such-Term durchgegangen und der jeweilige Wert für jede Seite berechnet. Der Wert setzt sich aus dem TF-IDF-Wert und PageRank zusammen. Anschließend wird das Ergebnis in der Datei pageranke_search.txt gespeichert.
In [26]:
with open('pageranke_search.txt', 'w') as f:
for search_term in search_terms:
f.write('Suchwort: %s\n\n' % ', '.join(search_term))
result = search(search_term, page_rank=True)
for key in result:
f.write("%s: %s\n" % (key, result[key]))
f.write('\n\n')