<filename>seoanalyzer/page.py<gh_stars>0
import hashlib
import json
import os
import re

from bs4 import BeautifulSoup
from collections import Counter
import lxml.html as lh
from string import punctuation
from urllib.parse import urlsplit
from urllib3.exceptions import HTTPError
from seoanalyzer.http import http
from seoanalyzer.stemmer import stem

# This list of English stop words is taken from the "Glasgow Information
# Retrieval Group". The original list can be found at
# http://ir.dcs.gla.ac.uk/resources/linguistic_utils/stop_words
STOP_WORDS = frozenset([
    "a", "about", "above", "across", "after", "afterwards", "again", "against",
    "all", "almost", "alone", "along", "already", "also", "although", "always",
    "am", "among", "amongst", "amoungst", "amount", "an", "and", "another",
    "any", "anyhow", "anyone", "anything", "anyway", "anywhere", "are",
    "around", "as", "at", "back", "be", "became", "because", "become",
    "becomes", "becoming", "been", "before", "beforehand", "behind", "being",
    "below", "beside", "besides", "between", "beyond", "bill", "both",
    "bottom", "but", "by", "call", "can", "cannot", "cant", "co", "con",
    "could", "couldnt", "cry", "de", "describe", "detail", "do", "done",
    "down", "due", "during", "each", "eg", "eight", "either", "eleven", "else",
    "elsewhere", "empty", "enough", "etc", "even", "ever", "every", "everyone",
    "everything", "everywhere", "except", "few", "fifteen", "fify", "fill",
    "find", "fire", "first", "five", "for", "former", "formerly", "forty",
    "found", "four", "from", "front", "full", "further", "get", "give", "go",
    "had", "has", "hasnt", "have", "he", "hence", "her", "here", "hereafter",
    "hereby", "herein", "hereupon", "hers", "herself", "him", "himself", "his",
    "how", "however", "hundred", "i", "ie", "if", "in", "inc", "indeed",
    "interest", "into", "is", "it", "its", "itself", "keep", "last", "latter",
    "latterly", "least", "less", "ltd", "made", "many", "may", "me",
    "meanwhile", "might", "mill", "mine", "more", "moreover", "most", "mostly",
    "move", "much", "must", "my", "myself", "name", "namely", "neither",
    "never", "nevertheless", "next", "nine", "no", "nobody", "none", "noone",
    "nor", "not", "nothing", "now", "nowhere", "of", "off", "often", "on",
    "once", "one", "only", "onto", "or", "other", "others", "otherwise", "our",
    "ours", "ourselves", "out", "over", "own", "part", "per", "perhaps",
    "please", "put", "rather", "re", "same", "see", "seem", "seemed",
    "seeming", "seems", "serious", "several", "she", "should", "show", "side",
    "since", "sincere", "six", "sixty", "so", "some", "somehow", "someone",
    "something", "sometime", "sometimes", "somewhere", "still", "such",
    "system", "take", "ten", "than", "that", "the", "their", "them",
    "themselves", "then", "thence", "there", "thereafter", "thereby",
    "therefore", "therein", "thereupon", "these", "they",
    "third", "this", "those", "though", "three", "through", "throughout",
    "thru", "thus", "to", "together", "too", "top", "toward", "towards",
    "twelve", "twenty", "two", "un", "under", "until", "up", "upon", "us",
    "very", "via", "was", "we", "well", "were", "what", "whatever", "when",
    "whence", "whenever", "where", "whereafter", "whereas", "whereby",
    "wherein", "whereupon", "wherever", "whether", "which", "while", "whither",
    "who", "whoever", "whole", "whom", "whose", "why", "will", "with",
    "within", "without", "would", "yet", "you", "your", "yours", "yourself",
    "yourselves",
    'ab',
    'aber',
    'abermaliges',
    'abermals',
    'abgerufen',
    'abgerufene',
    'abgerufener',
    'abgerufenes',
    'abgesehen',
    'acht',
    'aehnlich',
    'aehnliche',
    'aehnlichem',
    'aehnlichen',
    'aehnlicher',
    'aehnliches',
    'aehnlichste',
    'aehnlichstem',
    'aehnlichsten',
    'aehnlichster',
    'aehnlichstes',
    'aeusserst',
    'aeusserste',
    'aeusserstem',
    'aeussersten',
    'aeusserster',
    'aeusserstes',
    'ähnlich',
    'ähnliche',
    'ähnlichem',
    'ähnlichen',
    'ähnlicher',
    'ähnliches',
    'ähnlichst',
    'ähnlichste',
    'ähnlichstem',
    'ähnlichsten',
    'ähnlichster',
    'ähnlichstes',
    'alle',
    'allein',
    'alleine',
    'allem',
    'allemal',
    'allen',
    'allenfalls',
    'allenthalben',
    'aller',
    'allerdings',
    'allerlei',
    'alles',
    'allesamt',
    'allg',
    'allg.',
    'allgemein',
    'allgemeine',
    'allgemeinem',
    'allgemeinen',
    'allgemeiner',
    'allgemeines',
    'allgemeinste',
    'allgemeinstem',
    'allgemeinsten',
    'allgemeinster',
    'allgemeinstes',
    'allmählich',
    'allzeit',
    'allzu',
    'als',
    'alsbald',
    'also',
    'am',
    'an',
    'and',
    'andauernd',
    'andauernde',
    'andauerndem',
    'andauernden',
    'andauernder',
    'andauerndes',
    'ander',
    'andere',
    'anderem',
    'anderen',
    'anderenfalls',
    'anderer',
    'andererseits',
    'anderes',
    'anderm',
    'andern',
    'andernfalls',
    'anderr',
    'anders',
    'anderst',
    'anderweitig',
    'anderweitige',
    'anderweitigem',
    'anderweitigen',
    'anderweitiger',
    'anderweitiges',
    'anerkannt',
    'anerkannte',
    'anerkannter',
    'anerkanntes',
    'anfangen',
    'anfing',
    'angefangen',
    'angesetze',
    'angesetzt',
    'angesetzten',
    'angesetzter',
    'ans',
    'anscheinend',
    'ansetzen',
    'ansonst',
    'ansonsten',
    'anstatt',
    'anstelle',
    'arbeiten',
    'auch',
    'auf',
    'aufgehört',
    'aufgrund',
    'aufhören',
    'aufhörte',
    'aufzusuchen',
    'augenscheinlich',
    'augenscheinliche',
    'augenscheinlichem',
    'augenscheinlichen',
    'augenscheinlicher',
    'augenscheinliches',
    'augenscheinlichst',
    'augenscheinlichste',
    'augenscheinlichstem',
    'augenscheinlichsten',
    'augenscheinlichster',
    'augenscheinlichstes',
    'aus',
    'ausdrücken',
    'ausdrücklich',
    'ausdrückliche',
    'ausdrücklichem',
    'ausdrücklichen',
    'ausdrücklicher',
    'ausdrückliches',
    'ausdrückt',
    'ausdrückte',
    'ausgenommen',
    'ausgenommene',
    'ausgenommenem',
    'ausgenommenen',
    'ausgenommener',
    'ausgenommenes',
    'ausgerechnet',
    'ausgerechnete',
    'ausgerechnetem',
    'ausgerechneten',
    'ausgerechneter',
    'ausgerechnetes',
    'ausnahmslos',
    'ausnahmslose',
    'ausnahmslosem',
    'ausnahmslosen',
    'ausnahmsloser',
    'ausnahmsloses',
    'außen',
    'ausser',
    'ausserdem',
    'außerhalb',
    'äusserst',
    'äusserste',
    'äusserstem',
    'äussersten',
    'äusserster',
    'äusserstes',
    'author',
    'autor',
    'baelde',
    'bald',
    'bälde',
    'bearbeite',
    'bearbeiten',
    'bearbeitete',
    'bearbeiteten',
    'bedarf',
    'bedürfen',
    'bedurfte',
    'been',
    'befahl',
    'befiehlt',
    'befiehlte',
    'befohlene',
    'befohlens',
    'befragen',
    'befragte',
    'befragten',
    'befragter',
    'begann',
    'beginnen',
    'begonnen',
    'behalten',
    'behielt',
    'bei',
    'beide',
    'beidem',
    'beiden',
    'beider',
    'beiderlei',
    'beides',
    'beim',
    'beinahe',
    'beisammen',
    'beispielsweise',
    'beitragen',
    'beitrugen',
    'bekannt',
    'bekannte',
    'bekannter',
    'bekanntlich',
    'bekanntliche',
    'bekanntlichem',
    'bekanntlichen',
    'bekanntlicher',
    'bekanntliches',
    'bekennen',
    'benutzt',
    'bereits',
    'berichten',
    'berichtet',
    'berichtete',
    'berichteten',
    'besonders',
    'besser',
    'bessere',
    'besserem',
    'besseren',
    'besserer',
    'besseres',
    'bestehen',
    'besteht',
    'bestenfalls',
    'bestimmt',
    'bestimmte',
    'bestimmtem',
    'bestimmten',
    'bestimmter',
    'bestimmtes',
    'beträchtlich',
    'beträchtliche',
    'beträchtlichem',
    'beträchtlichen',
    'beträchtlicher',
    'beträchtliches',
    'betraechtlich',
    'betraechtliche',
    'betraechtlichem',
    'betraechtlichen',
    'betraechtlicher',
    'betraechtliches',
    'betreffend',
    'betreffende',
    'betreffendem',
    'betreffenden',
    'betreffender',
    'betreffendes',
    'bevor',
    'bez',
    'bez.',
    'bezgl',
    'bezgl.',
    'bezueglich',
    'bezüglich',
    'bietet',
    'bin',
    'bis',
    'bisher',
    'bisherige',
    'bisherigem',
    'bisherigen',
    'bisheriger',
    'bisheriges',
    'bislang',
    'bisschen',
    'bist',
    'bitte',
    'bleiben',
    'bleibt',
    'blieb',
    'bloss',
    'böden',
    'boeden',
    'brachte',
    'brachten',
    'brauchen',
    'braucht',
    'bräuchte',
    'bringen',
    'bsp',
    'bsp.',
    'bspw',
    'bspw.',
    'bzw',
    'bzw.',
    'ca',
    'ca.',
    'circa',
    'da',
    'dabei',
    'dadurch',
    'dafuer',
    'dafür',
    'dagegen',
    'daher',
    'dahin',
    'dahingehend',
    'dahingehende',
    'dahingehendem',
    'dahingehenden',
    'dahingehender',
    'dahingehendes',
    'dahinter',
    'damalige',
    'damaligem',
    'damaligen',
    'damaliger',
    'damaliges',
    'damals',
    'damit',
    'danach',
    'daneben',
    'dank',
    'danke',
    'danken',
    'dann',
    'dannen',
    'daran',
    'darauf',
    'daraus',
    'darf',
    'darfst',
    'darin',
    'darüber',
    'darüberhinaus',
    'darueber',
    'darueberhinaus',
    'darum',
    'darunter',
    'das',
    'daß',
    'dass',
    'dasselbe',
    'Dat',
    'davon',
    'davor',
    'dazu',
    'dazwischen',
    'dein',
    'deine',
    'deinem',
    'deinen',
    'deiner',
    'deines',
    'dem',
    'demgegenüber',
    'demgegenueber',
    'demgemaess',
    'demgemäss',
    'demnach',
    'demselben',
    'den',
    'denen',
    'denkbar',
    'denkbare',
    'denkbarem',
    'denkbaren',
    'denkbarer',
    'denkbares',
    'denn',
    'dennoch',
    'denselben',
    'der',
    'derart',
    'derartig',
    'derartige',
    'derartigem',
    'derartigen',
    'derartiger',
    'derem',
    'deren',
    'derer',
    'derjenige',
    'derjenigen',
    'derselbe',
    'derselben',
    'derzeit',
    'derzeitig',
    'derzeitige',
    'derzeitigem',
    'derzeitigen',
    'derzeitiges',
    'des',
    'deshalb',
    'desselben',
    'dessen',
    'dessenungeachtet',
    'desto',
    'desungeachtet',
    'deswegen',
    'dich',
    'die',
    'diejenige',
    'diejenigen',
    'dies',
    'diese',
    'dieselbe',
    'dieselben',
    'diesem',
    'diesen',
    'dieser',
    'dieses',
    'diesseitig',
    'diesseitige',
    'diesseitigem',
    'diesseitigen',
    'diesseitiger',
    'diesseitiges',
    'diesseits',
    'dinge',
    'dir',
    'direkt',
    'direkte',
    'direkten',
    'direkter',
    'doch',
    'doppelt',
    'dort',
    'dorther',
    'dorthin',
    'dran',
    'drauf',
    'drei',
    'dreißig',
    'drin',
    'dritte',
    'drüber',
    'drueber',
    'drum',
    'drunter',
    'du',
    'duerfte',
    'duerften',
    'duerftest',
    'duerftet',
    'dunklen',
    'durch',
    'durchaus',
    'durchweg',
    'durchwegs',
    'dürfen',
    'durfte',
    'dürfte',
    'durften',
    'dürften',
    'durftest',
    'dürftest',
    'durftet',
    'dürftet',
    'eben',
    'ebenfalls',
    'ebenso',
    'ect',
    'ect.',
    'ehe',
    'eher',
    'eheste',
    'ehestem',
    'ehesten',
    'ehester',
    'ehestes',
    'eigen',
    'eigene',
    'eigenem',
    'eigenen',
    'eigener',
    'eigenes',
    'eigenst',
    'eigentlich',
    'eigentliche',
    'eigentlichem',
    'eigentlichen',
    'eigentlicher',
    'eigentliches',
    'ein',
    'einbaün',
    'eine',
    'einem',
    'einen',
    'einer',
    'einerlei',
    'einerseits',
    'eines',
    'einfach',
    'einführen',
    'einführte',
    'einführten',
    'eingesetzt',
    'einig',
    'einige',
    'einigem',
    'einigen',
    'einiger',
    'einigermaßen',
    'einiges',
    'einmal',
    'einmalig',
    'einmalige',
    'einmaligem',
    'einmaligen',
    'einmaliger',
    'einmaliges',
    'eins',
    'einseitig',
    'einseitige',
    'einseitigen',
    'einseitiger',
    'einst',
    'einstmals',
    'einzig',
    'empfunden',
    'ende',
    'entgegen',
    'entlang',
    'entsprechend',
    'entsprechende',
    'entsprechendem',
    'entsprechenden',
    'entsprechender',
    'entsprechendes',
    'entweder',
    'er',
    'ergänze',
    'ergänzen',
    'ergänzte',
    'ergänzten',
    'ergo',
    'erhält',
    'erhalten',
    'erhielt',
    'erhielten',
    'erneut',
    'eröffne',
    'eröffnen',
    'eröffnet',
    'eröffnete',
    'eröffnetes',
    'erscheinen',
    'erst',
    'erste',
    'erstem',
    'ersten',
    'erster',
    'erstere',
    'ersterem',
    'ersteren',
    'ersterer',
    'ersteres',
    'erstes',
    'es',
    'etc',
    'etc.',
    'etliche',
    'etlichem',
    'etlichen',
    'etlicher',
    'etliches',
    'etwa',
    'etwaige',
    'etwas',
    'euch',
    'euer',
    'eure',
    'eurem',
    'euren',
    'eurer',
    'eures',
    'euretwegen',
    'fall',
    'falls',
    'fand',
    'fast',
    'ferner',
    'fertig',
    'finde',
    'finden',
    'findest',
    'findet',
    'folgend',
    'folgende',
    'folgendem',
    'folgenden',
    'folgender',
    'folgendermassen',
    'folgendes',
    'folglich',
    'for',
    'fordern',
    'fordert',
    'forderte',
    'forderten',
    'fort',
    'fortsetzen',
    'fortsetzt',
    'fortsetzte',
    'fortsetzten',
    'fragte',
    'frau',
    'frei',
    'freie',
    'freier',
    'freies',
    'fuer',
    'fuers',
    'fünf',
    'für',
    'fürs',
    'gab',
    'gaenzlich',
    'gaenzliche',
    'gaenzlichem',
    'gaenzlichen',
    'gaenzlicher',
    'gaenzliches',
    'gängig',
    'gängige',
    'gängigen',
    'gängiger',
    'gängiges',
    'ganz',
    'ganze',
    'ganzem',
    'ganzen',
    'ganzer',
    'ganzes',
    'gänzlich',
    'gänzliche',
    'gänzlichem',
    'gänzlichen',
    'gänzlicher',
    'gänzliches',
    'gar',
    'gbr',
    'geb',
    'geben',
    'geblieben',
    'gebracht',
    'gedurft',
    'geehrt',
    'geehrte',
    'geehrten',
    'geehrter',
    'gefallen',
    'gefälligst',
    'gefällt',
    'gefiel',
    'gegeben',
    'gegen',
    'gegenüber',
    'gegenueber',
    'gehabt',
    'gehalten',
    'gehen',
    'geht',
    'gekommen',
    'gekonnt',
    'gemacht',
    'gemaess',
    'gemäss',
    'gemeinhin',
    'gemocht',
    'genau',
    'genommen',
    'genug',
    'gepriesener',
    'gepriesenes',
    'gerade',
    'gern',
    'gesagt',
    'gesehen',
    'gestern',
    'gestrige',
    'getan',
    'geteilt',
    'geteilte',
    'getragen',
    'getrennt',
    'gewesen',
    'gewiss',
    'gewisse',
    'gewissem',
    'gewissen',
    'gewisser',
    'gewissermaßen',
    'gewisses',
    'gewollt',
    'geworden',
    'ggf',
    'ggf.',
    'gib',
    'gibt',
    'gilt',
    'gleich',
    'gleiche',
    'gleichem',
    'gleichen',
    'gleicher',
    'gleiches',
    'gleichsam',
    'gleichste',
    'gleichstem',
    'gleichsten',
    'gleichster',
    'gleichstes',
    'gleichwohl',
    'gleichzeitig',
    'gleichzeitige',
    'gleichzeitigem',
    'gleichzeitigen',
    'gleichzeitiger',
    'gleichzeitiges',
    'glücklicherweise',
    'gluecklicherweise',
    'gmbh',
    'gottseidank',
    'gratulieren',
    'gratuliert',
    'gratulierte',
    'groesstenteils',
    'grösstenteils',
    'gruendlich',
    'gründlich',
    'gut',
    'gute',
    'guten',
    'hab',
    'habe',
    'haben',
    'habt',
    'haette',
    'haeufig',
    'haeufige',
    'haeufigem',
    'haeufigen',
    'haeufiger',
    'haeufigere',
    'haeufigeren',
    'haeufigerer',
    'haeufigeres',
    'halb',
    'hallo',
    'halten',
    'hast',
    'hat',
    'hätt',
    'hatte',
    'hätte',
    'hatten',
    'hätten',
    'hattest',
    'hattet',
    'häufig',
    'häufige',
    'häufigem',
    'häufigen',
    'häufiger',
    'häufigere',
    'häufigeren',
    'häufigerer',
    'häufigeres',
    'hen',
    'her',
    'heraus',
    'herein',
    'herum',
    'heute',
    'heutige',
    'heutigem',
    'heutigen',
    'heutiger',
    'heutiges',
    'hier',
    'hierbei',
    'hiermit',
    'hiesige',
    'hiesigem',
    'hiesigen',
    'hiesiger',
    'hiesiges',
    'hin',
    'hindurch',
    'hinein',
    'hingegen',
    'hinlanglich',
    'hinlänglich',
    'hinten',
    'hintendran',
    'hinter',
    'hinterher',
    'hinterm',
    'hintern',
    'hinunter',
    'hoch',
    'höchst',
    'höchstens',
    'http',
    'hundert',
    'ich',
    'igitt',
    'ihm',
    'ihn',
    'ihnen',
    'ihr',
    'ihre',
    'ihrem',
    'ihren',
    'ihrer',
    'ihres',
    'ihretwegen',
    'ihrige',
    'ihrigen',
    'ihriges',
    'im',
    'immer',
    'immerhin',
    'immerwaehrend',
    'immerwaehrende',
    'immerwaehrendem',
    'immerwaehrenden',
    'immerwaehrender',
    'immerwaehrendes',
    'immerwährend',
    'immerwährende',
    'immerwährendem',
    'immerwährenden',
    'immerwährender',
    'immerwährendes',
    'immerzu',
    'important',
    'in',
    'indem',
    'indessen',
    'Inf.',
    'info',
    'infolge',
    'infolgedessen',
    'information',
    'innen',
    'innerhalb',
    'innerlich',
    'ins',
    'insbesondere',
    'insgeheim',
    'insgeheime',
    'insgeheimer',
    'insgesamt',
    'insgesamte',
    'insgesamter',
    'insofern',
    'inzwischen',
    'irgend',
    'irgendein',
    'irgendeine',
    'irgendeinem',
    'irgendeiner',
    'irgendeines',
    'irgendetwas',
    'irgendjemand',
    'irgendjemandem',
    'irgendwann',
    'irgendwas',
    'irgendwelche',
    'irgendwen',
    'irgendwenn',
    'irgendwer',
    'irgendwie',
    'irgendwo',
    'irgendwohin',
    'ist',
    'ja',
    'jaehrig',
    'jaehrige',
    'jaehrigem',
    'jaehrigen',
    'jaehriger',
    'jaehriges',
    'jährig',
    'jährige',
    'jährigem',
    'jährigen',
    'jähriges',
    'je',
    'jede',
    'jedem',
    'jeden',
    'jedenfalls',
    'jeder',
    'jederlei',
    'jedes',
    'jedesmal',
    'jedoch',
    'jeglichem',
    'jeglichen',
    'jeglicher',
    'jegliches',
    'jemals',
    'jemand',
    'jemandem',
    'jemanden',
    'jemandes',
    'jene',
    'jenem',
    'jenen',
    'jener',
    'jenes',
    'jenseitig',
    'jenseitigem',
    'jenseitiger',
    'jenseits',
    'jetzt',
    'jung',
    'junge',
    'jungem',
    'jungen',
    'junger',
    'junges',
    'kaeumlich',
    'kam',
    'kann',
    'kannst',
    'kaum',
    'käumlich',
    'kein',
    'keine',
    'keinem',
    'keinen',
    'keiner',
    'keinerlei',
    'keines',
    'keineswegs',
    'klar',
    'klare',
    'klaren',
    'klares',
    'klein',
    'kleinen',
    'kleiner',
    'kleines',
    'koennen',
    'koennt',
    'koennte',
    'koennten',
    'koenntest',
    'koenntet',
    'komme',
    'kommen',
    'kommt',
    'konkret',
    'konkrete',
    'konkreten',
    'konkreter',
    'konkretes',
    'könn',
    'können',
    'könnt',
    'konnte',
    'könnte',
    'konnten',
    'könnten',
    'konntest',
    'könntest',
    'konntet',
    'könntet',
    'kuenftig',
    'kuerzlich',
    'kuerzlichst',
    'künftig',
    'kürzlich',
    'kürzlichst',
    'laengst',
    'lag',
    'lagen',
    'langsam',
    'längst',
    'längstens',
    'lassen',
    'laut',
    'lediglich',
    'leer',
    'legen',
    'legte',
    'legten',
    'leicht',
    'leider',
    'lesen',
    'letze',
    'letzte',
    'letzten',
    'letztendlich',
    'letztens',
    'letztere',
    'letzterem',
    'letzterer',
    'letzteres',
    'letztes',
    'letztlich',
    'lichten',
    'liegt',
    'liest',
    'links',
    'mache',
    'machen',
    'machst',
    'macht',
    'machte',
    'machten',
    'mag',
    'magst',
    'mal',
    'man',
    'manch',
    'manche',
    'manchem',
    'manchen',
    'mancher',
    'mancherlei',
    'mancherorts',
    'manches',
    'manchmal',
    'mann',
    'margin',
    'massgebend',
    'massgebende',
    'massgebendem',
    'massgebenden',
    'massgebender',
    'massgebendes',
    'massgeblich',
    'massgebliche',
    'massgeblichem',
    'massgeblichen',
    'massgeblicher',
    'mehr',
    'mehrere',
    'mehrerer',
    'mehrfach',
    'mehrmalig',
    'mehrmaligem',
    'mehrmaliger',
    'mehrmaliges',
    'mein',
    'meine',
    'meinem',
    'meinen',
    'meiner',
    'meines',
    'meinetwegen',
    'meins',
    'meist',
    'meiste',
    'meisten',
    'meistens',
    'meistenteils',
    'meta',
    'mich',
    'mindestens',
    'mir',
    'mit',
    'miteinander',
    'mitgleich',
    'mithin',
    'mitnichten',
    'mittels',
    'mittelst',
    'mitten',
    'mittig',
    'mitunter',
    'mitwohl',
    'mochte',
    'möchte',
    'möchten',
    'möchtest',
    'moechte',
    'moeglich',
    'moeglichst',
    'moeglichste',
    'moeglichstem',
    'moeglichsten',
    'moeglichster',
    'mögen',
    'möglich',
    'mögliche',
    'möglichen',
    'möglicher',
    'möglicherweise',
    'möglichst',
    'möglichste',
    'möglichstem',
    'möglichsten',
    'möglichster',
    'morgen',
    'morgige',
    'muessen',
    'muesst',
    'muesste',
    'muss',
    'müssen',
    'musst',
    'müßt',
    'musste',
    'müsste',
    'mussten',
    'müssten',
    'nach',
    'nachdem',
    'nacher',
    'nachher',
    'nachhinein',
    'nächste',
    'nacht',
    'naechste',
    'naemlich',
    'nahm',
    'nämlich',
    'naturgemaess',
    'naturgemäss',
    'natürlich',
    'ncht',
    'neben',
    'nebenan',
    'nehmen',
    'nein',
    'neu',
    'neue',
    'neuem',
    'neuen',
    'neuer',
    'neuerdings',
    'neuerlich',
    'neuerliche',
    'neuerlichem',
    'neuerlicher',
    'neuerliches',
    'neues',
    'neulich',
    'neun',
    'nicht',
    'nichts',
    'nichtsdestotrotz',
    'nichtsdestoweniger',
    'nie',
    'niemals',
    'niemand',
    'niemandem',
    'niemanden',
    'niemandes',
    'nimm',
    'nimmer',
    'nimmt',
    'nirgends',
    'nirgendwo',
    'noch',
    'noetigenfalls',
    'nötigenfalls',
    'nun',
    'nur',
    'nutzen',
    'nutzt',
    'nützt',
    'nutzung',
    'ob',
    'oben',
    'ober',
    'oberen',
    'oberer',
    'oberhalb',
    'oberste',
    'obersten',
    'oberster',
    'obgleich',
    'obs',
    'obschon',
    'obwohl',
    'oder',
    'oefter',
    'oefters',
    'off',
    'offenkundig',
    'offenkundige',
    'offenkundigem',
    'offenkundigen',
    'offenkundiger',
    'offenkundiges',
    'offensichtlich',
    'offensichtliche',
    'offensichtlichem',
    'offensichtlichen',
    'offensichtlicher',
    'offensichtliches',
    'oft',
    'öfter',
    'öfters',
    'oftmals',
    'ohne',
    'ohnedies',
    'online',
    'paar',
    'partout',
    'per',
    'persoenlich',
    'persoenliche',
    'persoenlichem',
    'persoenlicher',
    'persoenliches',
    'persönlich',
    'persönliche',
    'persönlicher',
    'persönliches',
    'pfui',
    'ploetzlich',
    'ploetzliche',
    'ploetzlichem',
    'ploetzlicher',
    'ploetzliches',
    'plötzlich',
    'plötzliche',
    'plötzlichem',
    'plötzlicher',
    'plötzliches',
    'pro',
    'quasi',
    'reagiere',
    'reagieren',
    'reagiert',
    'reagierte',
    'recht',
    'rechts',
    'regelmäßig',
    'reichlich',
    'reichliche',
    'reichlichem',
    'reichlichen',
    'reichlicher',
    'restlos',
    'restlose',
    'restlosem',
    'restlosen',
    'restloser',
    'restloses',
    'richtiggehend',
    'richtiggehende',
    'richtiggehendem',
    'richtiggehenden',
    'richtiggehender',
    'richtiggehendes',
    'rief',
    'rund',
    'rundheraus',
    'rundum',
    'runter',
    'sage',
    'sagen',
    'sagt',
    'sagte',
    'sagten',
    'sagtest',
    'sagtet',
    'samt',
    'sämtliche',
    'sang',
    'sangen',
    'sattsam',
    'schätzen',
    'schätzt',
    'schätzte',
    'schätzten',
    'scheinbar',
    'scheinen',
    'schlechter',
    'schlicht',
    'schlichtweg',
    'schließlich',
    'schlussendlich',
    'schnell',
    'schon',
    'schreibe',
    'schreiben',
    'schreibens',
    'schreiber',
    'schwerlich',
    'schwerliche',
    'schwerlichem',
    'schwerlichen',
    'schwerlicher',
    'schwerliches',
    'schwierig',
    'sechs',
    'sect',
    'sehe',
    'sehen',
    'sehr',
    'sehrwohl',
    'seht',
    'sei',
    'seid',
    'seien',
    'seiest',
    'seiet',
    'sein',
    'seine',
    'seinem',
    'seinen',
    'seiner',
    'seines',
    'seit',
    'seitdem',
    'seite',
    'seiten',
    'seither',
    'selbe',
    'selben',
    'selber',
    'selbst',
    'selbstredend',
    'selbstredende',
    'selbstredendem',
    'selbstredenden',
    'selbstredender',
    'selbstredendes',
    'seltsamerweise',
    'senke',
    'senken',
    'senkt',
    'senkte',
    'senkten',
    'setzen',
    'setzt',
    'setzte',
    'setzten',
    'sich',
    'sicher',
    'sicherlich',
    'sie',
    'sieben',
    'siebte',
    'siehe',
    'sieht',
    'sind',
    'singen',
    'singt',
    'so',
    'sobald',
    'sodaß',
    'soeben',
    'sofern',
    'sofort',
    'sog',
    'sogar',
    'sogleich',
    'solange',
    'solc',
    'solc hen',
    'solch',
    'solche',
    'solchem',
    'solchen',
    'solcher',
    'solches',
    'soll',
    'sollen',
    'sollst',
    'sollt',
    'sollte',
    'sollten',
    'solltest',
    'solltet',
    'somit',
    'sondern',
    'sonst',
    'sonstig',
    'sonstige',
    'sonstigem',
    'sonstiger',
    'sonstwo',
    'sooft',
    'soviel',
    'soweit',
    'sowie',
    'sowieso',
    'sowohl',
    'später',
    'spielen',
    'startet',
    'startete',
    'starteten',
    'statt',
    'stattdessen',
    'steht',
    'steige',
    'steigen',
    'steigt',
    'stellenweise',
    'stellenweisem',
    'stellenweisen',
    'stets',
    'stieg',
    'stiegen',
    'such',
    'suchen',
    'tages',
    'tat',
    'tät',
    'tatsächlich',
    'tatsächlichen',
    'tatsächlicher',
    'tatsächliches',
    'tatsaechlich',
    'tatsaechlichen',
    'tatsaechlicher',
    'tatsaechliches',
    'tausend',
    'teile',
    'teilen',
    'teilte',
    'teilten',
    'tief',
    'titel',
    'toll',
    'total',
    'trage',
    'tragen',
    'trägt',
    'trotzdem',
    'trug',
    'tun',
    'tust',
    'tut',
    'txt',
    'übel',
    'über',
    'überall',
    'überallhin',
    'überaus',
    'überdies',
    'überhaupt',
    'überll',
    'übermorgen',
    'üblicherweise',
    'übrig',
    'übrigens',
    'ueber',
    'ueberall',
    'ueberallhin',
    'ueberaus',
    'ueberdies',
    'ueberhaupt',
    'uebermorgen',
    'ueblicherweise',
    'uebrig',
    'uebrigens',
    'um',
    'ums',
    'umso',
    'umstaendehalber',
    'umständehalber',
    'unbedingt',
    'unbedingte',
    'unbedingter',
    'unbedingtes',
    'und',
    'unerhoert',
    'unerhoerte',
    'unerhoertem',
    'unerhoerten',
    'unerhoerter',
    'unerhoertes',
    'unerhört',
    'unerhörte',
    'unerhörtem',
    'unerhörten',
    'unerhörter',
    'unerhörtes',
    'ungefähr',
    'ungemein',
    'ungewoehnlich',
    'ungewoehnliche',
    'ungewoehnlichem',
    'ungewoehnlichen',
    'ungewoehnlicher',
    'ungewoehnliches',
    'ungewöhnlich',
    'ungewöhnliche',
    'ungewöhnlichem',
    'ungewöhnlichen',
    'ungewöhnlicher',
    'ungewöhnliches',
    'ungleich',
    'ungleiche',
    'ungleichem',
    'ungleichen',
    'ungleicher',
    'ungleiches',
    'unmassgeblich',
    'unmassgebliche',
    'unmassgeblichem',
    'unmassgeblichen',
    'unmassgeblicher',
    'unmassgebliches',
    'unmoeglich',
    'unmoegliche',
    'unmoeglichem',
    'unmoeglichen',
    'unmoeglicher',
    'unmoegliches',
    'unmöglich',
    'unmögliche',
    'unmöglichen',
    'unmöglicher',
    'unnötig',
    'uns',
    'unsaeglich',
    'unsaegliche',
    'unsaeglichem',
    'unsaeglichen',
    'unsaeglicher',
    'unsaegliches',
    'unsagbar',
    'unsagbare',
    'unsagbarem',
    'unsagbaren',
    'unsagbarer',
    'unsagbares',
    'unsäglich',
    'unsägliche',
    'unsäglichem',
    'unsäglichen',
    'unsäglicher',
    'unsägliches',
    'unse',
    'unsem',
    'unsen',
    'unser',
    'unsere',
    'unserem',
    'unseren',
    'unserer',
    'unseres',
    'unserm',
    'unses',
    'unsre',
    'unsrem',
    'unsren',
    'unsrer',
    'unsres',
    'unstreitig',
    'unstreitige',
    'unstreitigem',
    'unstreitigen',
    'unstreitiger',
    'unstreitiges',
    'unten',
    'unter',
    'unterbrach',
    'unterbrechen',
    'untere',
    'unterem',
    'unteres',
    'unterhalb',
    'unterste',
    'unterster',
    'unterstes',
    'unwichtig',
    'unzweifelhaft',
    'unzweifelhafte',
    'unzweifelhaftem',
    'unzweifelhaften',
    'unzweifelhafter',
    'unzweifelhaftes',
    'usw',
    'usw.',
    'vergangen',
    'vergangene',
    'vergangener',
    'vergangenes',
    'vermag',
    'vermögen',
    'vermutlich',
    'vermutliche',
    'vermutlichem',
    'vermutlichen',
    'vermutlicher',
    'vermutliches',
    'veröffentlichen',
    'veröffentlicher',
    'veröffentlicht',
    'veröffentlichte',
    'veröffentlichten',
    'veröffentlichtes',
    'verrate',
    'verraten',
    'verriet',
    'verrieten',
    'version',
    'versorge',
    'versorgen',
    'versorgt',
    'versorgte',
    'versorgten',
    'versorgtes',
    'viel',
    'viele',
    'vielen',
    'vieler',
    'vielerlei',
    'vieles',
    'vielleicht',
    'vielmalig',
    'vielmals',
    'vier',
    'voellig',
    'voellige',
    'voelligem',
    'voelligen',
    'voelliger',
    'voelliges',
    'voelligst',
    'vollends',
    'völlig',
    'völlige',
    'völligem',
    'völligen',
    'völliger',
    'völliges',
    'völligst',
    'vollstaendig',
    'vollstaendige',
    'vollstaendigem',
    'vollstaendigen',
    'vollstaendiger',
    'vollstaendiges',
    'vollständig',
    'vollständige',
    'vollständigem',
    'vollständigen',
    'vollständiger',
    'vollständiges',
    'vom',
    'von',
    'vor',
    'voran',
    'vorbei',
    'vorgestern',
    'vorher',
    'vorherig',
    'vorherige',
    'vorherigem',
    'vorheriger',
    'vorne',
    'vorüber',
    'vorueber',
    'wachen',
    'waehrend',
    'waehrenddessen',
    'waere',
    'während',
    'währenddessen',
    'wann',
    'war',
    'wär',
    'wäre',
    'waren',
    'wären',
    'warst',
    'wart',
    'warum',
    'was',
    'weder',
    'weg',
    'wegen',
    'weil',
    'weiß',
    'weit',
    'weiter',
    'weitere',
    'weiterem',
    'weiteren',
    'weiterer',
    'weiteres',
    'weiterhin',
    'weitestgehend',
    'weitestgehende',
    'weitestgehendem',
    'weitestgehenden',
    'weitestgehender',
    'weitestgehendes',
    'weitgehend',
    'weitgehende',
    'weitgehendem',
    'weitgehenden',
    'weitgehender',
    'weitgehendes',
    'welche',
    'welchem',
    'welchen',
    'welcher',
    'welches',
    'wem',
    'wen',
    'wenig',
    'wenige',
    'weniger',
    'wenigstens',
    'wenn',
    'wenngleich',
    'wer',
    'werde',
    'werden',
    'werdet',
    'weshalb',
    'wessen',
    'weswegen',
    'wichtig',
    'wie',
    'wieder',
    'wiederum',
    'wieso',
    'wieviel',
    'wieviele',
    'wievieler',
    'wiewohl',
    'will',
    'willst',
    'wir',
    'wird',
    'wirklich',
    'wirklichem',
    'wirklicher',
    'wirkliches',
    'wirst',
    'wo',
    'wobei',
    'wodurch',
    'wofuer',
    'wofür',
    'wogegen',
    'woher',
    'wohin',
    'wohingegen',
    'wohl',
    'wohlgemerkt',
    'wohlweislich',
    'wolle',
    'wollen',
    'wollt',
    'wollte',
    'wollten',
    'wolltest',
    'wolltet',
    'womit',
    'womoeglich',
    'womoegliche',
    'womoeglichem',
    'womoeglichen',
    'womoeglicher',
    'womoegliches',
    'womöglich',
    'womögliche',
    'womöglichem',
    'womöglichen',
    'womöglicher',
    'womögliches',
    'woran',
    'woraufhin',
    'woraus',
    'worden',
    'worin',
    'wuerde',
    'wuerden',
    'wuerdest',
    'wuerdet',
    'wurde',
    'würde',
    'wurden',
    'würden',
    'wurdest',
    'würdest',
    'wurdet',
    'würdet',
    'www',
    'x',
    'z.B.',
    'zahlreich',
    'zahlreichem',
    'zahlreicher',
    'zB',
    'zb.',
    'zehn',
    'zeitweise',
    'zeitweisem',
    'zeitweisen',
    'zeitweiser',
    'ziehen',
    'zieht',
    'ziemlich',
    'ziemliche',
    'ziemlichem',
    'ziemlichen',
    'ziemlicher',
    'ziemliches',
    'zirka',
    'zog',
    'zogen',
    'zu',
    'zudem',
    'zuerst',
    'zufolge',
    'zugleich',
    'zuletzt',
    'zum',
    'zumal',
    'zumeist',
    'zumindest',
    'zunächst',
    'zunaechst',
    'zur',
    'zurück',
    'zurueck',
    'zusammen',
    'zusehends',
    'zuviel',
    'zuviele',
    'zuvieler',
    'zuweilen',
    'zwanzig',
    'zwar',
    'zwei',
    'zweifelsfrei',
    'zweifelsfreie',
    'zweifelsfreiem',
    'zweifelsfreien',
    'zweifelsfreier',
    'zweifelsfreies',
    'zwischen',
    'zwölf',
])

TOKEN_REGEX = re.compile(r'(?u)\b\w\w+\b')

HEADING_TAGS_XPATHS = {
    'h1': '//h1',
    'h2': '//h2',
    'h3': '//h3',
    'h4': '//h4',
    'h5': '//h5',
    'h6': '//h6',
}

ADDITIONAL_TAGS_XPATHS = {
    'title': '//title/text()',
    'meta_desc': '//meta[@name="description"]/@content',
    'viewport': '//meta[@name="viewport"]/@content',
    'charset': '//meta[@charset]/@charset',
    'canonical': '//link[@rel="canonical"]/@href',
    'alt_href': '//link[@rel="alternate"]/@href',
    'alt_hreflang': '//link[@rel="alternate"]/@hreflang',
}

IMAGE_EXTENSIONS = set(['.img', '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.svg', '.webp', '.avif',])


class Page():
    """
    Container for each page and the core analyzer.
    """

    def __init__(self, url='', base_domain='', analyze_headings=False, analyze_extra_tags=False):
        """
        Variables go here, *not* outside of __init__
        """

        self.base_domain = urlsplit(base_domain)
        self.parsed_url = urlsplit(url)
        self.url = url
        self.analyze_headings = analyze_headings
        self.analyze_extra_tags = analyze_extra_tags
        self.title = ''
        self.description = ''
        self.keywords = {}
        self.warnings = []
        self.translation = bytes.maketrans(punctuation.encode('utf-8'), str(' ' * len(punctuation)).encode('utf-8'))
        self.links = []
        self.total_word_count = 0
        self.wordcount = Counter()
        self.bigrams = Counter()
        self.trigrams = Counter()
        self.stem_to_word = {}
        self.content_hash = None

        if analyze_headings:
            self.headings = {}
        if analyze_extra_tags:
            self.additional_info = {}

    def talk(self):
        """
        Returns a dictionary that can be printed
        """

        context = {
            'page': self.url,
            'title': self.title,
            'description': self.description,
            'word_count': self.total_word_count,
            'keywords': self.sort_freq_dist(self.keywords, limit=5),
            'bigrams': self.bigrams,
            'trigrams': self.trigrams,
            'warnings': self.warnings,
            'content_hash': self.content_hash
        }

        if self.analyze_headings:
            context['headings'] = self.headings
        if self.analyze_extra_tags:
            context['additional_info'] = self.additional_info

        return context

    def populate(self, bs):
        """
        Populates the instance variables from BeautifulSoup
        """

        try:
            self.title = bs.title.text
        except AttributeError:
            self.title = 'No Title'

        descr = bs.findAll('meta', attrs={'name': 'description'})

        if len(descr) > 0:
            self.description = descr[0].get('content')

        keywords = bs.findAll('meta', attrs={'name': 'keywords'})

        if len(keywords) > 0:
            self.warn(f'Keywords should be avoided as they are a spam indicator and no longer used by Search Engines: {keywords}')

    def analyze_heading_tags(self, bs):
        """
        Analyze the heading tags and populate the headings
        """

        try:
            dom = lh.fromstring(str(bs))
        except ValueError as _:
            dom = lh.fromstring(bs.encode('utf-8'))
        for tag, xpath in HEADING_TAGS_XPATHS.items():
            value = [heading.text_content() for heading in dom.xpath(xpath)]
            if value:
                self.headings.update({tag: value})

    def analyze_additional_tags(self, bs):
        """
        Analyze additional tags and populate the additional info
        """

        try:
            dom = lh.fromstring(str(bs))
        except ValueError as _:
            dom = lh.fromstring(bs.encode('utf-8'))
        for tag, xpath in ADDITIONAL_TAGS_XPATHS.items():
            value = dom.xpath(xpath)
            if value:
                self.additional_info.update({tag: value})

    def analyze(self, raw_html=None):
        """
        Analyze the page and populate the warnings list
        """

        if not raw_html:
            valid_prefixes = []

            # only allow http:// https:// and //
            for s in ['http://', 'https://', '//',]:
                valid_prefixes.append(self.url.startswith(s))

            if True not in valid_prefixes:
                self.warn(f'{self.url} does not appear to have a valid protocol.')
                return

            if self.url.startswith('//'):
                self.url = f'{self.base_domain.scheme}:{self.url}'

            if self.parsed_url.netloc != self.base_domain.netloc:
                self.warn(f'{self.url} is not part of {self.base_domain.netloc}.')
                return

            try:
                page = http.get(self.url)
            except HTTPError as e:
                self.warn(f'Returned {e}')
                return

            encoding = 'ascii'

            if 'content-type' in page.headers:
                encoding = page.headers['content-type'].split('charset=')[-1]

            if encoding.lower() not in ('text/html', 'text/plain', 'utf-8'):
                # there is no unicode function in Python3
                # try:
                #     raw_html = unicode(page.read(), encoding)
                # except:
                self.warn(f'Can not read {encoding}')
                return
            else:
                raw_html = page.data.decode('utf-8')

        self.content_hash = hashlib.sha1(raw_html.encode('utf-8')).hexdigest()

        # remove comments, they screw with BeautifulSoup
        clean_html = re.sub(r'<!--.*?-->', r'', raw_html, flags=re.DOTALL)

        soup_lower = BeautifulSoup(clean_html.lower(), 'html.parser') #.encode('utf-8')
        soup_unmodified = BeautifulSoup(clean_html, 'html.parser') #.encode('utf-8')

        texts = soup_lower.findAll(text=True)
        visible_text = [w for w in filter(self.visible_tags, texts)]

        self.process_text(visible_text)

        self.populate(soup_lower)

        self.analyze_title()
        self.analyze_description()
        self.analyze_og(soup_lower)
        self.analyze_a_tags(soup_unmodified)
        self.analyze_img_tags(soup_lower)
        self.analyze_h1_tags(soup_lower)

        if self.analyze_headings:
            self.analyze_heading_tags(soup_unmodified)
        if self.analyze_extra_tags:
            self.analyze_additional_tags(soup_unmodified)

        return True

    def word_list_freq_dist(self, wordlist):
        freq = [wordlist.count(w) for w in wordlist]
        return dict(zip(wordlist, freq))

    def sort_freq_dist(self, freqdist, limit=1):
        aux = [(freqdist[key], self.stem_to_word[key]) for key in freqdist if freqdist[key] >= limit]
        aux.sort()
        aux.reverse()
        return aux

    def raw_tokenize(self, rawtext):
        return TOKEN_REGEX.findall(rawtext.lower())

    def tokenize(self, rawtext):
        return [word for word in TOKEN_REGEX.findall(rawtext.lower()) if word not in STOP_WORDS]

    def getngrams(self, D, n=2):
        return zip(*[D[i:] for i in range(n)])

    def process_text(self, vt):
        page_text = ''

        for element in vt:
            if element.strip():
                page_text += element.strip().lower() + u' '

        tokens = self.tokenize(page_text)
        raw_tokens = self.raw_tokenize(page_text)
        self.total_word_count = len(raw_tokens)

        bigrams = self.getngrams(raw_tokens, 2)

        for ng in bigrams:
            vt = ' '.join(ng)
            self.bigrams[vt] += 1

        trigrams = self.getngrams(raw_tokens, 3)

        for ng in trigrams:
            vt = ' '.join(ng)
            self.trigrams[vt] += 1

        freq_dist = self.word_list_freq_dist(tokens)

        for word in freq_dist:
            root = stem(word)
            cnt = freq_dist[word]

            if root not in self.stem_to_word:
                self.stem_to_word[root] = word

            if root in self.wordcount:
                self.wordcount[root] += cnt
            else:
                self.wordcount[root] = cnt

            if root in self.keywords:
                self.keywords[root] += cnt
            else:
                self.keywords[root] = cnt

    def analyze_og(self, bs):
        """
        Validate open graph tags
        """
        og_title = bs.findAll('meta', attrs={'property': 'og:title'})
        og_description = bs.findAll('meta', attrs={'property': 'og:description'})
        og_image = bs.findAll('meta', attrs={'property': 'og:image'})

        if len(og_title) == 0:
            self.warn(u'Missing og:title')

        if len(og_description) == 0:
            self.warn(u'Missing og:description')

        if len(og_image) == 0:
            self.warn(u'Missing og:image')

    def analyze_title(self):
        """
        Validate the title
        """

        # getting lazy, create a local variable so save having to
        # type self.x a billion times
        t = self.title

        # calculate the length of the title once
        length = len(t)

        if length == 0:
            self.warn(u'Missing title tag')
            return
        elif length < 10:
            self.warn(u'Title tag is too short (less than 10 characters): {0}'.format(t))
        elif length > 70:
            self.warn(u'Title tag is too long (more than 70 characters): {0}'.format(t))

    def analyze_description(self):
        """
        Validate the description
        """

        # getting lazy, create a local variable so save having to
        # type self.x a billion times
        d = self.description

        # calculate the length of the description once
        length = len(d)

        if length == 0:
            self.warn(u'Missing description')
            return
        elif length < 140:
            self.warn(u'Description is too short (less than 140 characters): {0}'.format(d))
        elif length > 255:
            self.warn(u'Description is too long (more than 255 characters): {0}'.format(d))

    def visible_tags(self, element):
        if element.parent.name in ['style', 'script', '[document]']:
            return False

        return True

    def analyze_img_tags(self, bs):
        """
        Verifies that each img has an alt and title
        """
        images = bs.find_all('img')

        for image in images:
            src = ''
            if 'src' in image:
                src = image['src']
            elif 'data-src' in image:
                src = image['data-src']
            else:
                src = image

            if len(image.get('alt', '')) == 0:
                self.warn('Image missing alt tag: {0}'.format(src))

    def analyze_h1_tags(self, bs):
        """
        Make sure each page has at least one H1 tag
        """
        htags = bs.find_all('h1')

        if len(htags) == 0:
            self.warn('Each page should have at least one h1 tag')

    def analyze_a_tags(self, bs):
        """
        Add any new links (that we didn't find in the sitemap)
        """
        anchors = bs.find_all('a', href=True)

        for tag in anchors:
            tag_href = tag['href']
            tag_text = tag.text.lower().strip()

            if tag_text in ['click here', 'page', 'article']:
                self.warn('Anchor text contains generic text: {0}'.format(tag_text))

            if self.base_domain.netloc not in tag_href and ':' in tag_href:
                continue

            modified_url = self.rel_to_abs_url(tag_href)

            url_filename, url_file_extension = os.path.splitext(modified_url)

            # ignore links to images
            if url_file_extension in IMAGE_EXTENSIONS:
                continue

            # remove hash links to all urls
            if '#' in modified_url:
                modified_url = modified_url[:modified_url.rindex('#')]

            self.links.append(modified_url)

    def rel_to_abs_url(self, link):
        if ':' in link:
            return link

        relative_path = link
        domain = self.base_domain.netloc

        if domain[-1] == '/':
            domain = domain[:-1]

        if len(relative_path) > 0 and relative_path[0] == '?':
            if '?' in self.url:
                return f'{self.url[:self.url.index("?")]}{relative_path}'

            return f'{self.url}{relative_path}'

        if len(relative_path) > 0 and relative_path[0] != '/':
            relative_path = f'/{relative_path}'

        return f'{self.base_domain.scheme}://{domain}{relative_path}'

    def warn(self, warning):
        self.warnings.append(warning)

<filename>seoanalyzer/page.py<gh_stars>0
import hashlib
import json
import os
import re

from bs4 import BeautifulSoup
from collections import Counter
import lxml.html as lh
from string import punctuation
from urllib.parse import urlsplit
from urllib3.exceptions import HTTPError
from seoanalyzer.http import http
from seoanalyzer.stemmer import stem

# This list of English stop words is taken from the "Glasgow Information
# Retrieval Group". The original list can be found at
# http://ir.dcs.gla.ac.uk/resources/linguistic_utils/stop_words
STOP_WORDS = frozenset([
    "a", "about", "above", "across", "after", "afterwards", "again", "against",
    "all", "almost", "alone", "along", "already", "also", "although", "always",
    "am", "among", "amongst", "amoungst", "amount", "an", "and", "another",
    "any", "anyhow", "anyone", "anything", "anyway", "anywhere", "are",
    "around", "as", "at", "back", "be", "became", "because", "become",
    "becomes", "becoming", "been", "before", "beforehand", "behind", "being",
    "below", "beside", "besides", "between", "beyond", "bill", "both",
    "bottom", "but", "by", "call", "can", "cannot", "cant", "co", "con",
    "could", "couldnt", "cry", "de", "describe", "detail", "do", "done",
    "down", "due", "during", "each", "eg", "eight", "either", "eleven", "else",
    "elsewhere", "empty", "enough", "etc", "even", "ever", "every", "everyone",
    "everything", "everywhere", "except", "few", "fifteen", "fify", "fill",
    "find", "fire", "first", "five", "for", "former", "formerly", "forty",
    "found", "four", "from", "front", "full", "further", "get", "give", "go",
    "had", "has", "hasnt", "have", "he", "hence", "her", "here", "hereafter",
    "hereby", "herein", "hereupon", "hers", "herself", "him", "himself", "his",
    "how", "however", "hundred", "i", "ie", "if", "in", "inc", "indeed",
    "interest", "into", "is", "it", "its", "itself", "keep", "last", "latter",
    "latterly", "least", "less", "ltd", "made", "many", "may", "me",
    "meanwhile", "might", "mill", "mine", "more", "moreover", "most", "mostly",
    "move", "much", "must", "my", "myself", "name", "namely", "neither",
    "never", "nevertheless", "next", "nine", "no", "nobody", "none", "noone",
    "nor", "not", "nothing", "now", "nowhere", "of", "off", "often", "on",
    "once", "one", "only", "onto", "or", "other", "others", "otherwise", "our",
    "ours", "ourselves", "out", "over", "own", "part", "per", "perhaps",
    "please", "put", "rather", "re", "same", "see", "seem", "seemed",
    "seeming", "seems", "serious", "several", "she", "should", "show", "side",
    "since", "sincere", "six", "sixty", "so", "some", "somehow", "someone",
    "something", "sometime", "sometimes", "somewhere", "still", "such",
    "system", "take", "ten", "than", "that", "the", "their", "them",
    "themselves", "then", "thence", "there", "thereafter", "thereby",
    "therefore", "therein", "thereupon", "these", "they",
    "third", "this", "those", "though", "three", "through", "throughout",
    "thru", "thus", "to", "together", "too", "top", "toward", "towards",
    "twelve", "twenty", "two", "un", "under", "until", "up", "upon", "us",
    "very", "via", "was", "we", "well", "were", "what", "whatever", "when",
    "whence", "whenever", "where", "whereafter", "whereas", "whereby",
    "wherein", "whereupon", "wherever", "whether", "which", "while", "whither",
    "who", "whoever", "whole", "whom", "whose", "why", "will", "with",
    "within", "without", "would", "yet", "you", "your", "yours", "yourself",
    "yourselves",
    'ab',
    'aber',
    'abermaliges',
    'abermals',
    'abgerufen',
    'abgerufene',
    'abgerufener',
    'abgerufenes',
    'abgesehen',
    'acht',
    'aehnlich',
    'aehnliche',
    'aehnlichem',
    'aehnlichen',
    'aehnlicher',
    'aehnliches',
    'aehnlichste',
    'aehnlichstem',
    'aehnlichsten',
    'aehnlichster',
    'aehnlichstes',
    'aeusserst',
    'aeusserste',
    'aeusserstem',
    'aeussersten',
    'aeusserster',
    'aeusserstes',
    'ähnlich',
    'ähnliche',
    'ähnlichem',
    'ähnlichen',
    'ähnlicher',
    'ähnliches',
    'ähnlichst',
    'ähnlichste',
    'ähnlichstem',
    'ähnlichsten',
    'ähnlichster',
    'ähnlichstes',
    'alle',
    'allein',
    'alleine',
    'allem',
    'allemal',
    'allen',
    'allenfalls',
    'allenthalben',
    'aller',
    'allerdings',
    'allerlei',
    'alles',
    'allesamt',
    'allg',
    'allg.',
    'allgemein',
    'allgemeine',
    'allgemeinem',
    'allgemeinen',
    'allgemeiner',
    'allgemeines',
    'allgemeinste',
    'allgemeinstem',
    'allgemeinsten',
    'allgemeinster',
    'allgemeinstes',
    'allmählich',
    'allzeit',
    'allzu',
    'als',
    'alsbald',
    'also',
    'am',
    'an',
    'and',
    'andauernd',
    'andauernde',
    'andauerndem',
    'andauernden',
    'andauernder',
    'andauerndes',
    'ander',
    'andere',
    'anderem',
    'anderen',
    'anderenfalls',
    'anderer',
    'andererseits',
    'anderes',
    'anderm',
    'andern',
    'andernfalls',
    'anderr',
    'anders',
    'anderst',
    'anderweitig',
    'anderweitige',
    'anderweitigem',
    'anderweitigen',
    'anderweitiger',
    'anderweitiges',
    'anerkannt',
    'anerkannte',
    'anerkannter',
    'anerkanntes',
    'anfangen',
    'anfing',
    'angefangen',
    'angesetze',
    'angesetzt',
    'angesetzten',
    'angesetzter',
    'ans',
    'anscheinend',
    'ansetzen',
    'ansonst',
    'ansonsten',
    'anstatt',
    'anstelle',
    'arbeiten',
    'auch',
    'auf',
    'aufgehört',
    'aufgrund',
    'aufhören',
    'aufhörte',
    'aufzusuchen',
    'augenscheinlich',
    'augenscheinliche',
    'augenscheinlichem',
    'augenscheinlichen',
    'augenscheinlicher',
    'augenscheinliches',
    'augenscheinlichst',
    'augenscheinlichste',
    'augenscheinlichstem',
    'augenscheinlichsten',
    'augenscheinlichster',
    'augenscheinlichstes',
    'aus',
    'ausdrücken',
    'ausdrücklich',
    'ausdrückliche',
    'ausdrücklichem',
    'ausdrücklichen',
    'ausdrücklicher',
    'ausdrückliches',
    'ausdrückt',
    'ausdrückte',
    'ausgenommen',
    'ausgenommene',
    'ausgenommenem',
    'ausgenommenen',
    'ausgenommener',
    'ausgenommenes',
    'ausgerechnet',
    'ausgerechnete',
    'ausgerechnetem',
    'ausgerechneten',
    'ausgerechneter',
    'ausgerechnetes',
    'ausnahmslos',
    'ausnahmslose',
    'ausnahmslosem',
    'ausnahmslosen',
    'ausnahmsloser',
    'ausnahmsloses',
    'außen',
    'ausser',
    'ausserdem',
    'außerhalb',
    'äusserst',
    'äusserste',
    'äusserstem',
    'äussersten',
    'äusserster',
    'äusserstes',
    'author',
    'autor',
    'baelde',
    'bald',
    'bälde',
    'bearbeite',
    'bearbeiten',
    'bearbeitete',
    'bearbeiteten',
    'bedarf',
    'bedürfen',
    'bedurfte',
    'been',
    'befahl',
    'befiehlt',
    'befiehlte',
    'befohlene',
    'befohlens',
    'befragen',
    'befragte',
    'befragten',
    'befragter',
    'begann',
    'beginnen',
    'begonnen',
    'behalten',
    'behielt',
    'bei',
    'beide',
    'beidem',
    'beiden',
    'beider',
    'beiderlei',
    'beides',
    'beim',
    'beinahe',
    'beisammen',
    'beispielsweise',
    'beitragen',
    'beitrugen',
    'bekannt',
    'bekannte',
    'bekannter',
    'bekanntlich',
    'bekanntliche',
    'bekanntlichem',
    'bekanntlichen',
    'bekanntlicher',
    'bekanntliches',
    'bekennen',
    'benutzt',
    'bereits',
    'berichten',
    'berichtet',
    'berichtete',
    'berichteten',
    'besonders',
    'besser',
    'bessere',
    'besserem',
    'besseren',
    'besserer',
    'besseres',
    'bestehen',
    'besteht',
    'bestenfalls',
    'bestimmt',
    'bestimmte',
    'bestimmtem',
    'bestimmten',
    'bestimmter',
    'bestimmtes',
    'beträchtlich',
    'beträchtliche',
    'beträchtlichem',
    'beträchtlichen',
    'beträchtlicher',
    'beträchtliches',
    'betraechtlich',
    'betraechtliche',
    'betraechtlichem',
    'betraechtlichen',
    'betraechtlicher',
    'betraechtliches',
    'betreffend',
    'betreffende',
    'betreffendem',
    'betreffenden',
    'betreffender',
    'betreffendes',
    'bevor',
    'bez',
    'bez.',
    'bezgl',
    'bezgl.',
    'bezueglich',
    'bezüglich',
    'bietet',
    'bin',
    'bis',
    'bisher',
    'bisherige',
    'bisherigem',
    'bisherigen',
    'bisheriger',
    'bisheriges',
    'bislang',
    'bisschen',
    'bist',
    'bitte',
    'bleiben',
    'bleibt',
    'blieb',
    'bloss',
    'böden',
    'boeden',
    'brachte',
    'brachten',
    'brauchen',
    'braucht',
    'bräuchte',
    'bringen',
    'bsp',
    'bsp.',
    'bspw',
    'bspw.',
    'bzw',
    'bzw.',
    'ca',
    'ca.',
    'circa',
    'da',
    'dabei',
    'dadurch',
    'dafuer',
    'dafür',
    'dagegen',
    'daher',
    'dahin',
    'dahingehend',
    'dahingehende',
    'dahingehendem',
    'dahingehenden',
    'dahingehender',
    'dahingehendes',
    'dahinter',
    'damalige',
    'damaligem',
    'damaligen',
    'damaliger',
    'damaliges',
    'damals',
    'damit',
    'danach',
    'daneben',
    'dank',
    'danke',
    'danken',
    'dann',
    'dannen',
    'daran',
    'darauf',
    'daraus',
    'darf',
    'darfst',
    'darin',
    'darüber',
    'darüberhinaus',
    'darueber',
    'darueberhinaus',
    'darum',
    'darunter',
    'das',
    'daß',
    'dass',
    'dasselbe',
    'Dat',
    'davon',
    'davor',
    'dazu',
    'dazwischen',
    'dein',
    'deine',
    'deinem',
    'deinen',
    'deiner',
    'deines',
    'dem',
    'demgegenüber',
    'demgegenueber',
    'demgemaess',
    'demgemäss',
    'demnach',
    'demselben',
    'den',
    'denen',
    'denkbar',
    'denkbare',
    'denkbarem',
    'denkbaren',
    'denkbarer',
    'denkbares',
    'denn',
    'dennoch',
    'denselben',
    'der',
    'derart',
    'derartig',
    'derartige',
    'derartigem',
    'derartigen',
    'derartiger',
    'derem',
    'deren',
    'derer',
    'derjenige',
    'derjenigen',
    'derselbe',
    'derselben',
    'derzeit',
    'derzeitig',
    'derzeitige',
    'derzeitigem',
    'derzeitigen',
    'derzeitiges',
    'des',
    'deshalb',
    'desselben',
    'dessen',
    'dessenungeachtet',
    'desto',
    'desungeachtet',
    'deswegen',
    'dich',
    'die',
    'diejenige',
    'diejenigen',
    'dies',
    'diese',
    'dieselbe',
    'dieselben',
    'diesem',
    'diesen',
    'dieser',
    'dieses',
    'diesseitig',
    'diesseitige',
    'diesseitigem',
    'diesseitigen',
    'diesseitiger',
    'diesseitiges',
    'diesseits',
    'dinge',
    'dir',
    'direkt',
    'direkte',
    'direkten',
    'direkter',
    'doch',
    'doppelt',
    'dort',
    'dorther',
    'dorthin',
    'dran',
    'drauf',
    'drei',
    'dreißig',
    'drin',
    'dritte',
    'drüber',
    'drueber',
    'drum',
    'drunter',
    'du',
    'duerfte',
    'duerften',
    'duerftest',
    'duerftet',
    'dunklen',
    'durch',
    'durchaus',
    'durchweg',
    'durchwegs',
    'dürfen',
    'durfte',
    'dürfte',
    'durften',
    'dürften',
    'durftest',
    'dürftest',
    'durftet',
    'dürftet',
    'eben',
    'ebenfalls',
    'ebenso',
    'ect',
    'ect.',
    'ehe',
    'eher',
    'eheste',
    'ehestem',
    'ehesten',
    'ehester',
    'ehestes',
    'eigen',
    'eigene',
    'eigenem',
    'eigenen',
    'eigener',
    'eigenes',
    'eigenst',
    'eigentlich',
    'eigentliche',
    'eigentlichem',
    'eigentlichen',
    'eigentlicher',
    'eigentliches',
    'ein',
    'einbaün',
    'eine',
    'einem',
    'einen',
    'einer',
    'einerlei',
    'einerseits',
    'eines',
    'einfach',
    'einführen',
    'einführte',
    'einführten',
    'eingesetzt',
    'einig',
    'einige',
    'einigem',
    'einigen',
    'einiger',
    'einigermaßen',
    'einiges',
    'einmal',
    'einmalig',
    'einmalige',
    'einmaligem',
    'einmaligen',
    'einmaliger',
    'einmaliges',
    'eins',
    'einseitig',
    'einseitige',
    'einseitigen',
    'einseitiger',
    'einst',
    'einstmals',
    'einzig',
    'empfunden',
    'ende',
    'entgegen',
    'entlang',
    'entsprechend',
    'entsprechende',
    'entsprechendem',
    'entsprechenden',
    'entsprechender',
    'entsprechendes',
    'entweder',
    'er',
    'ergänze',
    'ergänzen',
    'ergänzte',
    'ergänzten',
    'ergo',
    'erhält',
    'erhalten',
    'erhielt',
    'erhielten',
    'erneut',
    'eröffne',
    'eröffnen',
    'eröffnet',
    'eröffnete',
    'eröffnetes',
    'erscheinen',
    'erst',
    'erste',
    'erstem',
    'ersten',
    'erster',
    'erstere',
    'ersterem',
    'ersteren',
    'ersterer',
    'ersteres',
    'erstes',
    'es',
    'etc',
    'etc.',
    'etliche',
    'etlichem',
    'etlichen',
    'etlicher',
    'etliches',
    'etwa',
    'etwaige',
    'etwas',
    'euch',
    'euer',
    'eure',
    'eurem',
    'euren',
    'eurer',
    'eures',
    'euretwegen',
    'fall',
    'falls',
    'fand',
    'fast',
    'ferner',
    'fertig',
    'finde',
    'finden',
    'findest',
    'findet',
    'folgend',
    'folgende',
    'folgendem',
    'folgenden',
    'folgender',
    'folgendermassen',
    'folgendes',
    'folglich',
    'for',
    'fordern',
    'fordert',
    'forderte',
    'forderten',
    'fort',
    'fortsetzen',
    'fortsetzt',
    'fortsetzte',
    'fortsetzten',
    'fragte',
    'frau',
    'frei',
    'freie',
    'freier',
    'freies',
    'fuer',
    'fuers',
    'fünf',
    'für',
    'fürs',
    'gab',
    'gaenzlich',
    'gaenzliche',
    'gaenzlichem',
    'gaenzlichen',
    'gaenzlicher',
    'gaenzliches',
    'gängig',
    'gängige',
    'gängigen',
    'gängiger',
    'gängiges',
    'ganz',
    'ganze',
    'ganzem',
    'ganzen',
    'ganzer',
    'ganzes',
    'gänzlich',
    'gänzliche',
    'gänzlichem',
    'gänzlichen',
    'gänzlicher',
    'gänzliches',
    'gar',
    'gbr',
    'geb',
    'geben',
    'geblieben',
    'gebracht',
    'gedurft',
    'geehrt',
    'geehrte',
    'geehrten',
    'geehrter',
    'gefallen',
    'gefälligst',
    'gefällt',
    'gefiel',
    'gegeben',
    'gegen',
    'gegenüber',
    'gegenueber',
    'gehabt',
    'gehalten',
    'gehen',
    'geht',
    'gekommen',
    'gekonnt',
    'gemacht',
    'gemaess',
    'gemäss',
    'gemeinhin',
    'gemocht',
    'genau',
    'genommen',
    'genug',
    'gepriesener',
    'gepriesenes',
    'gerade',
    'gern',
    'gesagt',
    'gesehen',
    'gestern',
    'gestrige',
    'getan',
    'geteilt',
    'geteilte',
    'getragen',
    'getrennt',
    'gewesen',
    'gewiss',
    'gewisse',
    'gewissem',
    'gewissen',
    'gewisser',
    'gewissermaßen',
    'gewisses',
    'gewollt',
    'geworden',
    'ggf',
    'ggf.',
    'gib',
    'gibt',
    'gilt',
    'gleich',
    'gleiche',
    'gleichem',
    'gleichen',
    'gleicher',
    'gleiches',
    'gleichsam',
    'gleichste',
    'gleichstem',
    'gleichsten',
    'gleichster',
    'gleichstes',
    'gleichwohl',
    'gleichzeitig',
    'gleichzeitige',
    'gleichzeitigem',
    'gleichzeitigen',
    'gleichzeitiger',
    'gleichzeitiges',
    'glücklicherweise',
    'gluecklicherweise',
    'gmbh',
    'gottseidank',
    'gratulieren',
    'gratuliert',
    'gratulierte',
    'groesstenteils',
    'grösstenteils',
    'gruendlich',
    'gründlich',
    'gut',
    'gute',
    'guten',
    'hab',
    'habe',
    'haben',
    'habt',
    'haette',
    'haeufig',
    'haeufige',
    'haeufigem',
    'haeufigen',
    'haeufiger',
    'haeufigere',
    'haeufigeren',
    'haeufigerer',
    'haeufigeres',
    'halb',
    'hallo',
    'halten',
    'hast',
    'hat',
    'hätt',
    'hatte',
    'hätte',
    'hatten',
    'hätten',
    'hattest',
    'hattet',
    'häufig',
    'häufige',
    'häufigem',
    'häufigen',
    'häufiger',
    'häufigere',
    'häufigeren',
    'häufigerer',
    'häufigeres',
    'hen',
    'her',
    'heraus',
    'herein',
    'herum',
    'heute',
    'heutige',
    'heutigem',
    'heutigen',
    'heutiger',
    'heutiges',
    'hier',
    'hierbei',
    'hiermit',
    'hiesige',
    'hiesigem',
    'hiesigen',
    'hiesiger',
    'hiesiges',
    'hin',
    'hindurch',
    'hinein',
    'hingegen',
    'hinlanglich',
    'hinlänglich',
    'hinten',
    'hintendran',
    'hinter',
    'hinterher',
    'hinterm',
    'hintern',
    'hinunter',
    'hoch',
    'höchst',
    'höchstens',
    'http',
    'hundert',
    'ich',
    'igitt',
    'ihm',
    'ihn',
    'ihnen',
    'ihr',
    'ihre',
    'ihrem',
    'ihren',
    'ihrer',
    'ihres',
    'ihretwegen',
    'ihrige',
    'ihrigen',
    'ihriges',
    'im',
    'immer',
    'immerhin',
    'immerwaehrend',
    'immerwaehrende',
    'immerwaehrendem',
    'immerwaehrenden',
    'immerwaehrender',
    'immerwaehrendes',
    'immerwährend',
    'immerwährende',
    'immerwährendem',
    'immerwährenden',
    'immerwährender',
    'immerwährendes',
    'immerzu',
    'important',
    'in',
    'indem',
    'indessen',
    'Inf.',
    'info',
    'infolge',
    'infolgedessen',
    'information',
    'innen',
    'innerhalb',
    'innerlich',
    'ins',
    'insbesondere',
    'insgeheim',
    'insgeheime',
    'insgeheimer',
    'insgesamt',
    'insgesamte',
    'insgesamter',
    'insofern',
    'inzwischen',
    'irgend',
    'irgendein',
    'irgendeine',
    'irgendeinem',
    'irgendeiner',
    'irgendeines',
    'irgendetwas',
    'irgendjemand',
    'irgendjemandem',
    'irgendwann',
    'irgendwas',
    'irgendwelche',
    'irgendwen',
    'irgendwenn',
    'irgendwer',
    'irgendwie',
    'irgendwo',
    'irgendwohin',
    'ist',
    'ja',
    'jaehrig',
    'jaehrige',
    'jaehrigem',
    'jaehrigen',
    'jaehriger',
    'jaehriges',
    'jährig',
    'jährige',
    'jährigem',
    'jährigen',
    'jähriges',
    'je',
    'jede',
    'jedem',
    'jeden',
    'jedenfalls',
    'jeder',
    'jederlei',
    'jedes',
    'jedesmal',
    'jedoch',
    'jeglichem',
    'jeglichen',
    'jeglicher',
    'jegliches',
    'jemals',
    'jemand',
    'jemandem',
    'jemanden',
    'jemandes',
    'jene',
    'jenem',
    'jenen',
    'jener',
    'jenes',
    'jenseitig',
    'jenseitigem',
    'jenseitiger',
    'jenseits',
    'jetzt',
    'jung',
    'junge',
    'jungem',
    'jungen',
    'junger',
    'junges',
    'kaeumlich',
    'kam',
    'kann',
    'kannst',
    'kaum',
    'käumlich',
    'kein',
    'keine',
    'keinem',
    'keinen',
    'keiner',
    'keinerlei',
    'keines',
    'keineswegs',
    'klar',
    'klare',
    'klaren',
    'klares',
    'klein',
    'kleinen',
    'kleiner',
    'kleines',
    'koennen',
    'koennt',
    'koennte',
    'koennten',
    'koenntest',
    'koenntet',
    'komme',
    'kommen',
    'kommt',
    'konkret',
    'konkrete',
    'konkreten',
    'konkreter',
    'konkretes',
    'könn',
    'können',
    'könnt',
    'konnte',
    'könnte',
    'konnten',
    'könnten',
    'konntest',
    'könntest',
    'konntet',
    'könntet',
    'kuenftig',
    'kuerzlich',
    'kuerzlichst',
    'künftig',
    'kürzlich',
    'kürzlichst',
    'laengst',
    'lag',
    'lagen',
    'langsam',
    'längst',
    'längstens',
    'lassen',
    'laut',
    'lediglich',
    'leer',
    'legen',
    'legte',
    'legten',
    'leicht',
    'leider',
    'lesen',
    'letze',
    'letzte',
    'letzten',
    'letztendlich',
    'letztens',
    'letztere',
    'letzterem',
    'letzterer',
    'letzteres',
    'letztes',
    'letztlich',
    'lichten',
    'liegt',
    'liest',
    'links',
    'mache',
    'machen',
    'machst',
    'macht',
    'machte',
    'machten',
    'mag',
    'magst',
    'mal',
    'man',
    'manch',
    'manche',
    'manchem',
    'manchen',
    'mancher',
    'mancherlei',
    'mancherorts',
    'manches',
    'manchmal',
    'mann',
    'margin',
    'massgebend',
    'massgebende',
    'massgebendem',
    'massgebenden',
    'massgebender',
    'massgebendes',
    'massgeblich',
    'massgebliche',
    'massgeblichem',
    'massgeblichen',
    'massgeblicher',
    'mehr',
    'mehrere',
    'mehrerer',
    'mehrfach',
    'mehrmalig',
    'mehrmaligem',
    'mehrmaliger',
    'mehrmaliges',
    'mein',
    'meine',
    'meinem',
    'meinen',
    'meiner',
    'meines',
    'meinetwegen',
    'meins',
    'meist',
    'meiste',
    'meisten',
    'meistens',
    'meistenteils',
    'meta',
    'mich',
    'mindestens',
    'mir',
    'mit',
    'miteinander',
    'mitgleich',
    'mithin',
    'mitnichten',
    'mittels',
    'mittelst',
    'mitten',
    'mittig',
    'mitunter',
    'mitwohl',
    'mochte',
    'möchte',
    'möchten',
    'möchtest',
    'moechte',
    'moeglich',
    'moeglichst',
    'moeglichste',
    'moeglichstem',
    'moeglichsten',
    'moeglichster',
    'mögen',
    'möglich',
    'mögliche',
    'möglichen',
    'möglicher',
    'möglicherweise',
    'möglichst',
    'möglichste',
    'möglichstem',
    'möglichsten',
    'möglichster',
    'morgen',
    'morgige',
    'muessen',
    'muesst',
    'muesste',
    'muss',
    'müssen',
    'musst',
    'müßt',
    'musste',
    'müsste',
    'mussten',
    'müssten',
    'nach',
    'nachdem',
    'nacher',
    'nachher',
    'nachhinein',
    'nächste',
    'nacht',
    'naechste',
    'naemlich',
    'nahm',
    'nämlich',
    'naturgemaess',
    'naturgemäss',
    'natürlich',
    'ncht',
    'neben',
    'nebenan',
    'nehmen',
    'nein',
    'neu',
    'neue',
    'neuem',
    'neuen',
    'neuer',
    'neuerdings',
    'neuerlich',
    'neuerliche',
    'neuerlichem',
    'neuerlicher',
    'neuerliches',
    'neues',
    'neulich',
    'neun',
    'nicht',
    'nichts',
    'nichtsdestotrotz',
    'nichtsdestoweniger',
    'nie',
    'niemals',
    'niemand',
    'niemandem',
    'niemanden',
    'niemandes',
    'nimm',
    'nimmer',
    'nimmt',
    'nirgends',
    'nirgendwo',
    'noch',
    'noetigenfalls',
    'nötigenfalls',
    'nun',
    'nur',
    'nutzen',
    'nutzt',
    'nützt',
    'nutzung',
    'ob',
    'oben',
    'ober',
    'oberen',
    'oberer',
    'oberhalb',
    'oberste',
    'obersten',
    'oberster',
    'obgleich',
    'obs',
    'obschon',
    'obwohl',
    'oder',
    'oefter',
    'oefters',
    'off',
    'offenkundig',
    'offenkundige',
    'offenkundigem',
    'offenkundigen',
    'offenkundiger',
    'offenkundiges',
    'offensichtlich',
    'offensichtliche',
    'offensichtlichem',
    'offensichtlichen',
    'offensichtlicher',
    'offensichtliches',
    'oft',
    'öfter',
    'öfters',
    'oftmals',
    'ohne',
    'ohnedies',
    'online',
    'paar',
    'partout',
    'per',
    'persoenlich',
    'persoenliche',
    'persoenlichem',
    'persoenlicher',
    'persoenliches',
    'persönlich',
    'persönliche',
    'persönlicher',
    'persönliches',
    'pfui',
    'ploetzlich',
    'ploetzliche',
    'ploetzlichem',
    'ploetzlicher',
    'ploetzliches',
    'plötzlich',
    'plötzliche',
    'plötzlichem',
    'plötzlicher',
    'plötzliches',
    'pro',
    'quasi',
    'reagiere',
    'reagieren',
    'reagiert',
    'reagierte',
    'recht',
    'rechts',
    'regelmäßig',
    'reichlich',
    'reichliche',
    'reichlichem',
    'reichlichen',
    'reichlicher',
    'restlos',
    'restlose',
    'restlosem',
    'restlosen',
    'restloser',
    'restloses',
    'richtiggehend',
    'richtiggehende',
    'richtiggehendem',
    'richtiggehenden',
    'richtiggehender',
    'richtiggehendes',
    'rief',
    'rund',
    'rundheraus',
    'rundum',
    'runter',
    'sage',
    'sagen',
    'sagt',
    'sagte',
    'sagten',
    'sagtest',
    'sagtet',
    'samt',
    'sämtliche',
    'sang',
    'sangen',
    'sattsam',
    'schätzen',
    'schätzt',
    'schätzte',
    'schätzten',
    'scheinbar',
    'scheinen',
    'schlechter',
    'schlicht',
    'schlichtweg',
    'schließlich',
    'schlussendlich',
    'schnell',
    'schon',
    'schreibe',
    'schreiben',
    'schreibens',
    'schreiber',
    'schwerlich',
    'schwerliche',
    'schwerlichem',
    'schwerlichen',
    'schwerlicher',
    'schwerliches',
    'schwierig',
    'sechs',
    'sect',
    'sehe',
    'sehen',
    'sehr',
    'sehrwohl',
    'seht',
    'sei',
    'seid',
    'seien',
    'seiest',
    'seiet',
    'sein',
    'seine',
    'seinem',
    'seinen',
    'seiner',
    'seines',
    'seit',
    'seitdem',
    'seite',
    'seiten',
    'seither',
    'selbe',
    'selben',
    'selber',
    'selbst',
    'selbstredend',
    'selbstredende',
    'selbstredendem',
    'selbstredenden',
    'selbstredender',
    'selbstredendes',
    'seltsamerweise',
    'senke',
    'senken',
    'senkt',
    'senkte',
    'senkten',
    'setzen',
    'setzt',
    'setzte',
    'setzten',
    'sich',
    'sicher',
    'sicherlich',
    'sie',
    'sieben',
    'siebte',
    'siehe',
    'sieht',
    'sind',
    'singen',
    'singt',
    'so',
    'sobald',
    'sodaß',
    'soeben',
    'sofern',
    'sofort',
    'sog',
    'sogar',
    'sogleich',
    'solange',
    'solc',
    'solc hen',
    'solch',
    'solche',
    'solchem',
    'solchen',
    'solcher',
    'solches',
    'soll',
    'sollen',
    'sollst',
    'sollt',
    'sollte',
    'sollten',
    'solltest',
    'solltet',
    'somit',
    'sondern',
    'sonst',
    'sonstig',
    'sonstige',
    'sonstigem',
    'sonstiger',
    'sonstwo',
    'sooft',
    'soviel',
    'soweit',
    'sowie',
    'sowieso',
    'sowohl',
    'später',
    'spielen',
    'startet',
    'startete',
    'starteten',
    'statt',
    'stattdessen',
    'steht',
    'steige',
    'steigen',
    'steigt',
    'stellenweise',
    'stellenweisem',
    'stellenweisen',
    'stets',
    'stieg',
    'stiegen',
    'such',
    'suchen',
    'tages',
    'tat',
    'tät',
    'tatsächlich',
    'tatsächlichen',
    'tatsächlicher',
    'tatsächliches',
    'tatsaechlich',
    'tatsaechlichen',
    'tatsaechlicher',
    'tatsaechliches',
    'tausend',
    'teile',
    'teilen',
    'teilte',
    'teilten',
    'tief',
    'titel',
    'toll',
    'total',
    'trage',
    'tragen',
    'trägt',
    'trotzdem',
    'trug',
    'tun',
    'tust',
    'tut',
    'txt',
    'übel',
    'über',
    'überall',
    'überallhin',
    'überaus',
    'überdies',
    'überhaupt',
    'überll',
    'übermorgen',
    'üblicherweise',
    'übrig',
    'übrigens',
    'ueber',
    'ueberall',
    'ueberallhin',
    'ueberaus',
    'ueberdies',
    'ueberhaupt',
    'uebermorgen',
    'ueblicherweise',
    'uebrig',
    'uebrigens',
    'um',
    'ums',
    'umso',
    'umstaendehalber',
    'umständehalber',
    'unbedingt',
    'unbedingte',
    'unbedingter',
    'unbedingtes',
    'und',
    'unerhoert',
    'unerhoerte',
    'unerhoertem',
    'unerhoerten',
    'unerhoerter',
    'unerhoertes',
    'unerhört',
    'unerhörte',
    'unerhörtem',
    'unerhörten',
    'unerhörter',
    'unerhörtes',
    'ungefähr',
    'ungemein',
    'ungewoehnlich',
    'ungewoehnliche',
    'ungewoehnlichem',
    'ungewoehnlichen',
    'ungewoehnlicher',
    'ungewoehnliches',
    'ungewöhnlich',
    'ungewöhnliche',
    'ungewöhnlichem',
    'ungewöhnlichen',
    'ungewöhnlicher',
    'ungewöhnliches',
    'ungleich',
    'ungleiche',
    'ungleichem',
    'ungleichen',
    'ungleicher',
    'ungleiches',
    'unmassgeblich',
    'unmassgebliche',
    'unmassgeblichem',
    'unmassgeblichen',
    'unmassgeblicher',
    'unmassgebliches',
    'unmoeglich',
    'unmoegliche',
    'unmoeglichem',
    'unmoeglichen',
    'unmoeglicher',
    'unmoegliches',
    'unmöglich',
    'unmögliche',
    'unmöglichen',
    'unmöglicher',
    'unnötig',
    'uns',
    'unsaeglich',
    'unsaegliche',
    'unsaeglichem',
    'unsaeglichen',
    'unsaeglicher',
    'unsaegliches',
    'unsagbar',
    'unsagbare',
    'unsagbarem',
    'unsagbaren',
    'unsagbarer',
    'unsagbares',
    'unsäglich',
    'unsägliche',
    'unsäglichem',
    'unsäglichen',
    'unsäglicher',
    'unsägliches',
    'unse',
    'unsem',
    'unsen',
    'unser',
    'unsere',
    'unserem',
    'unseren',
    'unserer',
    'unseres',
    'unserm',
    'unses',
    'unsre',
    'unsrem',
    'unsren',
    'unsrer',
    'unsres',
    'unstreitig',
    'unstreitige',
    'unstreitigem',
    'unstreitigen',
    'unstreitiger',
    'unstreitiges',
    'unten',
    'unter',
    'unterbrach',
    'unterbrechen',
    'untere',
    'unterem',
    'unteres',
    'unterhalb',
    'unterste',
    'unterster',
    'unterstes',
    'unwichtig',
    'unzweifelhaft',
    'unzweifelhafte',
    'unzweifelhaftem',
    'unzweifelhaften',
    'unzweifelhafter',
    'unzweifelhaftes',
    'usw',
    'usw.',
    'vergangen',
    'vergangene',
    'vergangener',
    'vergangenes',
    'vermag',
    'vermögen',
    'vermutlich',
    'vermutliche',
    'vermutlichem',
    'vermutlichen',
    'vermutlicher',
    'vermutliches',
    'veröffentlichen',
    'veröffentlicher',
    'veröffentlicht',
    'veröffentlichte',
    'veröffentlichten',
    'veröffentlichtes',
    'verrate',
    'verraten',
    'verriet',
    'verrieten',
    'version',
    'versorge',
    'versorgen',
    'versorgt',
    'versorgte',
    'versorgten',
    'versorgtes',
    'viel',
    'viele',
    'vielen',
    'vieler',
    'vielerlei',
    'vieles',
    'vielleicht',
    'vielmalig',
    'vielmals',
    'vier',
    'voellig',
    'voellige',
    'voelligem',
    'voelligen',
    'voelliger',
    'voelliges',
    'voelligst',
    'vollends',
    'völlig',
    'völlige',
    'völligem',
    'völligen',
    'völliger',
    'völliges',
    'völligst',
    'vollstaendig',
    'vollstaendige',
    'vollstaendigem',
    'vollstaendigen',
    'vollstaendiger',
    'vollstaendiges',
    'vollständig',
    'vollständige',
    'vollständigem',
    'vollständigen',
    'vollständiger',
    'vollständiges',
    'vom',
    'von',
    'vor',
    'voran',
    'vorbei',
    'vorgestern',
    'vorher',
    'vorherig',
    'vorherige',
    'vorherigem',
    'vorheriger',
    'vorne',
    'vorüber',
    'vorueber',
    'wachen',
    'waehrend',
    'waehrenddessen',
    'waere',
    'während',
    'währenddessen',
    'wann',
    'war',
    'wär',
    'wäre',
    'waren',
    'wären',
    'warst',
    'wart',
    'warum',
    'was',
    'weder',
    'weg',
    'wegen',
    'weil',
    'weiß',
    'weit',
    'weiter',
    'weitere',
    'weiterem',
    'weiteren',
    'weiterer',
    'weiteres',
    'weiterhin',
    'weitestgehend',
    'weitestgehende',
    'weitestgehendem',
    'weitestgehenden',
    'weitestgehender',
    'weitestgehendes',
    'weitgehend',
    'weitgehende',
    'weitgehendem',
    'weitgehenden',
    'weitgehender',
    'weitgehendes',
    'welche',
    'welchem',
    'welchen',
    'welcher',
    'welches',
    'wem',
    'wen',
    'wenig',
    'wenige',
    'weniger',
    'wenigstens',
    'wenn',
    'wenngleich',
    'wer',
    'werde',
    'werden',
    'werdet',
    'weshalb',
    'wessen',
    'weswegen',
    'wichtig',
    'wie',
    'wieder',
    'wiederum',
    'wieso',
    'wieviel',
    'wieviele',
    'wievieler',
    'wiewohl',
    'will',
    'willst',
    'wir',
    'wird',
    'wirklich',
    'wirklichem',
    'wirklicher',
    'wirkliches',
    'wirst',
    'wo',
    'wobei',
    'wodurch',
    'wofuer',
    'wofür',
    'wogegen',
    'woher',
    'wohin',
    'wohingegen',
    'wohl',
    'wohlgemerkt',
    'wohlweislich',
    'wolle',
    'wollen',
    'wollt',
    'wollte',
    'wollten',
    'wolltest',
    'wolltet',
    'womit',
    'womoeglich',
    'womoegliche',
    'womoeglichem',
    'womoeglichen',
    'womoeglicher',
    'womoegliches',
    'womöglich',
    'womögliche',
    'womöglichem',
    'womöglichen',
    'womöglicher',
    'womögliches',
    'woran',
    'woraufhin',
    'woraus',
    'worden',
    'worin',
    'wuerde',
    'wuerden',
    'wuerdest',
    'wuerdet',
    'wurde',
    'würde',
    'wurden',
    'würden',
    'wurdest',
    'würdest',
    'wurdet',
    'würdet',
    'www',
    'x',
    'z.B.',
    'zahlreich',
    'zahlreichem',
    'zahlreicher',
    'zB',
    'zb.',
    'zehn',
    'zeitweise',
    'zeitweisem',
    'zeitweisen',
    'zeitweiser',
    'ziehen',
    'zieht',
    'ziemlich',
    'ziemliche',
    'ziemlichem',
    'ziemlichen',
    'ziemlicher',
    'ziemliches',
    'zirka',
    'zog',
    'zogen',
    'zu',
    'zudem',
    'zuerst',
    'zufolge',
    'zugleich',
    'zuletzt',
    'zum',
    'zumal',
    'zumeist',
    'zumindest',
    'zunächst',
    'zunaechst',
    'zur',
    'zurück',
    'zurueck',
    'zusammen',
    'zusehends',
    'zuviel',
    'zuviele',
    'zuvieler',
    'zuweilen',
    'zwanzig',
    'zwar',
    'zwei',
    'zweifelsfrei',
    'zweifelsfreie',
    'zweifelsfreiem',
    'zweifelsfreien',
    'zweifelsfreier',
    'zweifelsfreies',
    'zwischen',
    'zwölf',
])

TOKEN_REGEX = re.compile(r'(?u)\b\w\w+\b')

HEADING_TAGS_XPATHS = {
    'h1': '//h1',
    'h2': '//h2',
    'h3': '//h3',
    'h4': '//h4',
    'h5': '//h5',
    'h6': '//h6',
}

ADDITIONAL_TAGS_XPATHS = {
    'title': '//title/text()',
    'meta_desc': '//meta[@name="description"]/@content',
    'viewport': '//meta[@name="viewport"]/@content',
    'charset': '//meta[@charset]/@charset',
    'canonical': '//link[@rel="canonical"]/@href',
    'alt_href': '//link[@rel="alternate"]/@href',
    'alt_hreflang': '//link[@rel="alternate"]/@hreflang',
}

IMAGE_EXTENSIONS = set(['.img', '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.svg', '.webp', '.avif',])


class Page():
    """
    Container for each page and the core analyzer.
    """

    def __init__(self, url='', base_domain='', analyze_headings=False, analyze_extra_tags=False):
        """
        Variables go here, *not* outside of __init__
        """

        self.base_domain = urlsplit(base_domain)
        self.parsed_url = urlsplit(url)
        self.url = url
        self.analyze_headings = analyze_headings
        self.analyze_extra_tags = analyze_extra_tags
        self.title = ''
        self.description = ''
        self.keywords = {}
        self.warnings = []
        self.translation = bytes.maketrans(punctuation.encode('utf-8'), str(' ' * len(punctuation)).encode('utf-8'))
        self.links = []
        self.total_word_count = 0
        self.wordcount = Counter()
        self.bigrams = Counter()
        self.trigrams = Counter()
        self.stem_to_word = {}
        self.content_hash = None

        if analyze_headings:
            self.headings = {}
        if analyze_extra_tags:
            self.additional_info = {}

    def talk(self):
        """
        Returns a dictionary that can be printed
        """

        context = {
            'page': self.url,
            'title': self.title,
            'description': self.description,
            'word_count': self.total_word_count,
            'keywords': self.sort_freq_dist(self.keywords, limit=5),
            'bigrams': self.bigrams,
            'trigrams': self.trigrams,
            'warnings': self.warnings,
            'content_hash': self.content_hash
        }

        if self.analyze_headings:
            context['headings'] = self.headings
        if self.analyze_extra_tags:
            context['additional_info'] = self.additional_info

        return context

    def populate(self, bs):
        """
        Populates the instance variables from BeautifulSoup
        """

        try:
            self.title = bs.title.text
        except AttributeError:
            self.title = 'No Title'

        descr = bs.findAll('meta', attrs={'name': 'description'})

        if len(descr) > 0:
            self.description = descr[0].get('content')

        keywords = bs.findAll('meta', attrs={'name': 'keywords'})

        if len(keywords) > 0:
            self.warn(f'Keywords should be avoided as they are a spam indicator and no longer used by Search Engines: {keywords}')

    def analyze_heading_tags(self, bs):
        """
        Analyze the heading tags and populate the headings
        """

        try:
            dom = lh.fromstring(str(bs))
        except ValueError as _:
            dom = lh.fromstring(bs.encode('utf-8'))
        for tag, xpath in HEADING_TAGS_XPATHS.items():
            value = [heading.text_content() for heading in dom.xpath(xpath)]
            if value:
                self.headings.update({tag: value})

    def analyze_additional_tags(self, bs):
        """
        Analyze additional tags and populate the additional info
        """

        try:
            dom = lh.fromstring(str(bs))
        except ValueError as _:
            dom = lh.fromstring(bs.encode('utf-8'))
        for tag, xpath in ADDITIONAL_TAGS_XPATHS.items():
            value = dom.xpath(xpath)
            if value:
                self.additional_info.update({tag: value})

    def analyze(self, raw_html=None):
        """
        Analyze the page and populate the warnings list
        """

        if not raw_html:
            valid_prefixes = []

            # only allow http:// https:// and //
            for s in ['http://', 'https://', '//',]:
                valid_prefixes.append(self.url.startswith(s))

            if True not in valid_prefixes:
                self.warn(f'{self.url} does not appear to have a valid protocol.')
                return

            if self.url.startswith('//'):
                self.url = f'{self.base_domain.scheme}:{self.url}'

            if self.parsed_url.netloc != self.base_domain.netloc:
                self.warn(f'{self.url} is not part of {self.base_domain.netloc}.')
                return

            try:
                page = http.get(self.url)
            except HTTPError as e:
                self.warn(f'Returned {e}')
                return

            encoding = 'ascii'

            if 'content-type' in page.headers:
                encoding = page.headers['content-type'].split('charset=')[-1]

            if encoding.lower() not in ('text/html', 'text/plain', 'utf-8'):
                # there is no unicode function in Python3
                # try:
                #     raw_html = unicode(page.read(), encoding)
                # except:
                self.warn(f'Can not read {encoding}')
                return
            else:
                raw_html = page.data.decode('utf-8')

        self.content_hash = hashlib.sha1(raw_html.encode('utf-8')).hexdigest()

        # remove comments, they screw with BeautifulSoup
        clean_html = re.sub(r'<!--.*?-->', r'', raw_html, flags=re.DOTALL)

        soup_lower = BeautifulSoup(clean_html.lower(), 'html.parser') #.encode('utf-8')
        soup_unmodified = BeautifulSoup(clean_html, 'html.parser') #.encode('utf-8')

        texts = soup_lower.findAll(text=True)
        visible_text = [w for w in filter(self.visible_tags, texts)]

        self.process_text(visible_text)

        self.populate(soup_lower)

        self.analyze_title()
        self.analyze_description()
        self.analyze_og(soup_lower)
        self.analyze_a_tags(soup_unmodified)
        self.analyze_img_tags(soup_lower)
        self.analyze_h1_tags(soup_lower)

        if self.analyze_headings:
            self.analyze_heading_tags(soup_unmodified)
        if self.analyze_extra_tags:
            self.analyze_additional_tags(soup_unmodified)

        return True

    def word_list_freq_dist(self, wordlist):
        freq = [wordlist.count(w) for w in wordlist]
        return dict(zip(wordlist, freq))

    def sort_freq_dist(self, freqdist, limit=1):
        aux = [(freqdist[key], self.stem_to_word[key]) for key in freqdist if freqdist[key] >= limit]
        aux.sort()
        aux.reverse()
        return aux

    def raw_tokenize(self, rawtext):
        return TOKEN_REGEX.findall(rawtext.lower())

    def tokenize(self, rawtext):
        return [word for word in TOKEN_REGEX.findall(rawtext.lower()) if word not in STOP_WORDS]

    def getngrams(self, D, n=2):
        return zip(*[D[i:] for i in range(n)])

    def process_text(self, vt):
        page_text = ''

        for element in vt:
            if element.strip():
                page_text += element.strip().lower() + u' '

        tokens = self.tokenize(page_text)
        raw_tokens = self.raw_tokenize(page_text)
        self.total_word_count = len(raw_tokens)

        bigrams = self.getngrams(raw_tokens, 2)

        for ng in bigrams:
            vt = ' '.join(ng)
            self.bigrams[vt] += 1

        trigrams = self.getngrams(raw_tokens, 3)

        for ng in trigrams:
            vt = ' '.join(ng)
            self.trigrams[vt] += 1

        freq_dist = self.word_list_freq_dist(tokens)

        for word in freq_dist:
            root = stem(word)
            cnt = freq_dist[word]

            if root not in self.stem_to_word:
                self.stem_to_word[root] = word

            if root in self.wordcount:
                self.wordcount[root] += cnt
            else:
                self.wordcount[root] = cnt

            if root in self.keywords:
                self.keywords[root] += cnt
            else:
                self.keywords[root] = cnt

    def analyze_og(self, bs):
        """
        Validate open graph tags
        """
        og_title = bs.findAll('meta', attrs={'property': 'og:title'})
        og_description = bs.findAll('meta', attrs={'property': 'og:description'})
        og_image = bs.findAll('meta', attrs={'property': 'og:image'})

        if len(og_title) == 0:
            self.warn(u'Missing og:title')

        if len(og_description) == 0:
            self.warn(u'Missing og:description')

        if len(og_image) == 0:
            self.warn(u'Missing og:image')

    def analyze_title(self):
        """
        Validate the title
        """

        # getting lazy, create a local variable so save having to
        # type self.x a billion times
        t = self.title

        # calculate the length of the title once
        length = len(t)

        if length == 0:
            self.warn(u'Missing title tag')
            return
        elif length < 10:
            self.warn(u'Title tag is too short (less than 10 characters): {0}'.format(t))
        elif length > 70:
            self.warn(u'Title tag is too long (more than 70 characters): {0}'.format(t))

    def analyze_description(self):
        """
        Validate the description
        """

        # getting lazy, create a local variable so save having to
        # type self.x a billion times
        d = self.description

        # calculate the length of the description once
        length = len(d)

        if length == 0:
            self.warn(u'Missing description')
            return
        elif length < 140:
            self.warn(u'Description is too short (less than 140 characters): {0}'.format(d))
        elif length > 255:
            self.warn(u'Description is too long (more than 255 characters): {0}'.format(d))

    def visible_tags(self, element):
        if element.parent.name in ['style', 'script', '[document]']:
            return False

        return True

    def analyze_img_tags(self, bs):
        """
        Verifies that each img has an alt and title
        """
        images = bs.find_all('img')

        for image in images:
            src = ''
            if 'src' in image:
                src = image['src']
            elif 'data-src' in image:
                src = image['data-src']
            else:
                src = image

            if len(image.get('alt', '')) == 0:
                self.warn('Image missing alt tag: {0}'.format(src))

    def analyze_h1_tags(self, bs):
        """
        Make sure each page has at least one H1 tag
        """
        htags = bs.find_all('h1')

        if len(htags) == 0:
            self.warn('Each page should have at least one h1 tag')

    def analyze_a_tags(self, bs):
        """
        Add any new links (that we didn't find in the sitemap)
        """
        anchors = bs.find_all('a', href=True)

        for tag in anchors:
            tag_href = tag['href']
            tag_text = tag.text.lower().strip()

            if tag_text in ['click here', 'page', 'article']:
                self.warn('Anchor text contains generic text: {0}'.format(tag_text))

            if self.base_domain.netloc not in tag_href and ':' in tag_href:
                continue

            modified_url = self.rel_to_abs_url(tag_href)

            url_filename, url_file_extension = os.path.splitext(modified_url)

            # ignore links to images
            if url_file_extension in IMAGE_EXTENSIONS:
                continue

            # remove hash links to all urls
            if '#' in modified_url:
                modified_url = modified_url[:modified_url.rindex('#')]

            self.links.append(modified_url)

    def rel_to_abs_url(self, link):
        if ':' in link:
            return link

        relative_path = link
        domain = self.base_domain.netloc

        if domain[-1] == '/':
            domain = domain[:-1]

        if len(relative_path) > 0 and relative_path[0] == '?':
            if '?' in self.url:
                return f'{self.url[:self.url.index("?")]}{relative_path}'

            return f'{self.url}{relative_path}'

        if len(relative_path) > 0 and relative_path[0] != '/':
            relative_path = f'/{relative_path}'

        return f'{self.base_domain.scheme}://{domain}{relative_path}'

    def warn(self, warning):
        self.warnings.append(warning)

# This list of English stop words is taken from the "Glasgow Information # Retrieval Group". The original list can be found at # http://ir.dcs.gla.ac.uk/resources/linguistic_utils/stop_words Container for each page and the core analyzer. Variables go here, *not* outside of __init__ Returns a dictionary that can be printed Populates the instance variables from BeautifulSoup Analyze the heading tags and populate the headings Analyze additional tags and populate the additional info Analyze the page and populate the warnings list # only allow http:// https:// and // # there is no unicode function in Python3 # try: #     raw_html = unicode(page.read(), encoding) # except: # remove comments, they screw with BeautifulSoup #.encode('utf-8') #.encode('utf-8') Validate open graph tags Validate the title # getting lazy, create a local variable so save having to # type self.x a billion times # calculate the length of the title once Validate the description # getting lazy, create a local variable so save having to # type self.x a billion times # calculate the length of the description once Verifies that each img has an alt and title Make sure each page has at least one H1 tag Add any new links (that we didn't find in the sitemap) # ignore links to images # remove hash links to all urls