Websites crawlen en scrapen met Python: verlopen domeinen en gebroken links vinden met Scrapy
Inleiding
Deze tutorial gaat over het crawlen en scrapen van het web met Python en Scrapy, met als doel het vinden van verlopen domeinen en gebroken links. Dit is een van de vele dingen die je kunt doen met crawlers en scrapers. Bij toepassingen en onderzoek in machine learning is het vaak gebruikelijk om links te crawlen en inhoud van websites te scrapen, vooral voor inhoudsanalyse en community discovery algoritmen. Bijvoorbeeld, websites die naar elkaar linken zijn vaak op een of andere manier aan elkaar gerelateerd en behoren meestal tot dezelfde inhoudsgemeenschap. Met het gratis en open source Scrapy-pakket in Python kan de code in deze gids de inhoud van een lijst met websites scrapen, links van deze websites extraheren, en deze op hun beurt crawlen, terwijl ze links opslaan die fouten retourneren. Vervolgens wordt het type fout geanalyseerd. De inhoud om te scrapen en het aantal links om te crawlen groeit exponentieel naarmate meer links en domeinen worden gecrawld en aan de wachtrij worden toegevoegd, zoals hieronder conceptueel weergegeven: Dit is dezelfde werkwijze die zoekmachines gebruiken bij het indexeren van inhoud op het web, waarbij miljoenen instanties en duizenden machines continu aan het crawlen en scrapen zijn, deze worden 'spiders' genoemd. De Google-spider is de bekendste. Je kunt veel met de informatie die de spider verzamelt. Bij de code hieronder, als een link een fout retourneert tijdens het crawlen, zal de code de fout analyseren die wordt geretourneerd. Dit kunnen HTTP-fouten zijn (404), maar ook fouten gerelateerd aan serverconfiguratiefouten en DNS / nameserver fouten. Op deze manier kun je bijvoorbeeld websites met gebroken links opsporen, wat kan aangeven dat ze niet langer actief worden onderhouden. Je kunt ook domeinen ontdekken met backlinks die beschikbaar zijn om te registreren. De resultaten worden opgeslagen in een bestand en kunnen ook in een database worden opgeslagen. * Let op: dit is nog in ontwikkeling, de code en annotatie van elk codeblok worden regelmatig bijgewerkt naarmate de codebase voor scrapingtechnieken verder wordt ontwikkeld.
Crawlen, scrapen en deep learning
Websites doorzoeken en content verzamelen van het internet is in de context van machine learning vooral relevant bij onderzoek naar contentclassificatie. Bijvoorbeeld, door gebruik te maken van technieken uit de natuurlijke taalverwerking samen met voorgetrainde modellen in het Engels, kunnen we miljoenen artikelen van Engelse uitgevers analyseren. Dit helpt om zaken te ontdekken zoals de politieke voorkeur en de 'sentiment' van de uitgever, en wat voor soort content (nieuws) in de loop der tijd populair is geweest.
Een ander vaak bestudeerd onderwerp is het backlink-profiel van websites. Zoals eerder genoemd, horen websites die naar elkaar linken vaak bij dezelfde community. Backlinks en forward links worden bijvoorbeeld gebruikt in deep learning-algoritmes samen met hoofcomponentenanalyse om clusters van websites te ontdekken die hetzelfde publiek bedienen en over dezelfde onderwerpen gaan.
Noodzakelijke bibliotheken
Eerst importeren we de benodigde bibliotheken. De belangrijkste bibliotheek is Scrapy, een gratis open source scraping framework in Python. Deze tool is in 2008 geïntroduceerd en wordt sindsdien actief ontwikkeld.
Twisted kan worden benut om DNS-oplosfouten te identificeren, wat kan aangeven of een domein waarschijnlijk beschikbaar is voor registratie. De bibliotheek tldextract helpt om de tld van een link te halen (bijvoorbeeld '.com' in 'https://github.com/scrapy/scrapy').
Je kunt deze bibliotheken installeren met de volgende commando’s:
- pip install scrapy
- pip install twisted
- pip install tldextract
Gebruik de volgende code om in Python deze en andere benodigde bibliotheken te laden:
from namecheap import Api
from twisted.internet.error import DNSLookupError
from scrapy.linkextractors import LinkExtractor
import CustomLinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from scrapy.item import Item, Field
from scrapy.spidermiddlewares.httperror import HttpError
import os
import time
from urlparse import urlparse
import datetime
import sys
import tldextract
import shutil
Stel de directory in waar je de resultaten wil opslaan in een bestand:
try:
shutil.rmtree("/home/ubuntu/crawls/crawler1/")
except Exception:
sys.exc_clear()
os.makedirs("/home/ubuntu/crawls/crawler1/")
Geef aan welke informatie je wil opslaan als je content van de webpagina schraapt waarop een link verwijst:
class DmozItem(Item):
domaincrawl = Field()
current_url_id = Field()
domain_id = Field()
refer_domain = Field()
Stel een lijst samen van websites waar je wil starten met crawlen. Je kunt bijvoorbeeld een lijst van 50 websites samenstellen en de crawler deze websites asynchroon laten schrapen. Voeg ook de nodige voorvoegsels zoals 'http' toe.
with open('/home/ubuntu/crawler_sites.txt', 'r') as file:
starturl = file.readline().strip()
extracted = tldextract.extract(starturl)
filename = "{}.{}".format(extracted.domain, extracted.suffix)
filename = filename[0:20]
if starturl[0:4] != "http":
starturl = "http://"+starturl
Stel de globale parameters in die tijdens de weg bijgewerkt worden. Scrapy zorgt voor de meeste complexe uitdagingen, zoals geheugengebruik wanneer het aantal links om te crawlen exponentieel groeit, en links die worden opgeslagen in een database zodat links en pagina's slechts eenmaal worden gecrawld. Hierdoor kun je miljoenen links crawlen, zelfs op een computer met weinig geheugen.
current_url_idcount = 0
current_url_printtreshold = 50
domain_avail = 0
domain_count = 0
time_treshold = 120 ### in seconden
processed_dupes = {}
blocked = []
time1=datetime.datetime.now()
Sla ook de shelluitvoer van de Python-kernel op als Scrapy onderweg op een fout stuit.
f = open('/home/ubuntu/logs/crawler1/crawler1/lastfeed.txt', 'w')
f.write("FEED CRAWLER1")
f.close()
Nu definiëren we de MySpider klasse. Dit, samen met Crawlspider, is een belangrijke klasse van het Scrapy-framework. Hier specificeer je de regels van de crawler, of 'spin'. Bijvoorbeeld, je wilt alleen .com-domeinen crawlen. Je past dus een filter toe op de links in het crawling-proces, waar de spider zich aan houdt:
class MySpider(CrawlSpider):
name = 'crawler1'
start_urls = [
starturl,
]
extracted = tldextract.extract(starturl)
print extracted
extractedsuffix2 = extracted.suffix[-3:]
Nu specificeren we de regels. We willen alleen .com, .net, .org, .edu en .gov-domeinen crawlen. We willen ook links/domeinen met 'forum' erin vermijden, zodat de crawler niet vastzit op forums met duizenden threads en berichten. We kunnen zo veel regels toevoegen als we willen op basis van sleutelwoorden.
if extracted.suffix == "com" or extracted.suffix == "net" or extracted.suffix == "org":
rules = (
Rule(LinkExtractor(allow=("\.com", "\.net", "\.org", "\.edu", "\.gov"), deny=('forum', ),
unique=True),
callback="parse_obj",
process_request='add_errback',
follow=True,
process_links='check_for_semi_dupe'
),
)
Een andere belangrijke klasse is de pipeline-klasse, die specificeert hoe de geschrapte content wordt verwerkt (bijvoorbeeld, je wilt alleen links en headers bewaren van de content die is geschrapt). We komen later op deze klasse terug.
Als onderdeel van de MySpider-klasse controleren we op dubbele links, zodat links niet opnieuw worden gecrawld.
def check_for_semi_dupe(self, links):
for link in links:
extracted = tldextract.extract(link.url)
just_domain = "{}.{}".format(extracted.domain, extracted.suffix)
url_indexed = 0
if just_domain not in processed_dupes:
processed_dupes[just_domain] = datetime.datetime.now()
else:
url_indexed = 1
timediff_in_sec = int((datetime.datetime.now() - processed_dupes[just_domain]).total_seconds())
if just_domain in blocked:
continue
elif url_indexed == 1 and timediff_in_sec time_treshold:
blocked.append(just_domain)
continue
else:
yield link
Nu verwerken we de reactie die we krijgen bij het crawlen van een link die door de eerder gespecificeerde filters komt. Als de link een geldige HTTP-reactie (200) retourneert, volgt het de link, crawlt de daaropvolgende content en haalt er links uit, en crawlt vervolgens deze links. De domeinen worden opgeslagen in een .csv-bestand. Dit bestand kan op een later moment gebruikt worden, bijvoorbeeld in nieuwe crawling- en scraping-instanties, om te zorgen dat dezelfde domeinen niet opnieuw gecrawld worden.
- Opmerking: er zijn enkele lelijke globale variabelen gedefinieerd in de onderstaande functie - deze kunnen op een meer Pythonische manier herschreven worden.
def parse_obj(self, response):
download_size = len(response.body)
global current_url_idcount
current_url_idcount = current_url_idcount + 1
global current_url_printtreshold
if current_url_idcount == current_url_printtreshold:
try:
global domain_avail
domain_avail = sum(1 for line in open(
# note that the first row is also counted, which contain the headers
"/home/ubuntu/scrapy/output/%s.csv" %filename )) - 1
except Exception:
sys.exc_clear()
global domain_count
referring_url = response.request.headers.get('Referer', None)
De output wordt hieronder afgedrukt naar het uitgiftebestand. Dit is voor debug-doeleinden, je kunt dit wellicht weglaten.
with open('/home/ubuntu/logs/crawler1/crawler1/lastfeed.txt', 'a') as outfile:
print outfile, "pcrawl: " + str(current_url_idcount) + " dcheck: " + str(domain_count) + " davail: " + str(domain_avail) + " pps: " + str(p_per_sec) + " dlsize: " + str(download_size) + " refurl: " + referring_url
print "pcrawl: " + str(current_url_idcount) + " dcheck: " + str(domain_count) + " davail: " + str(domain_avail) + " pps: " + str(p_per_sec) + " dlsize: " + str(download_size) + " refurl: " + referring_url
global current_url_printtreshold
current_url_printtreshold = current_url_idcount + 50
Nu analyseren we de crawl-fouten die we tegenkomen. De meeste crawlingtoepassingen zijn alleen geïnteresseerd in de links die werken en content leveren, maar niet-crawlebare links zijn ook interessant. Als een website verwijst naar een niet-functionerende website, of een ontbrekende pagina op een website, kan dat verschillende redenen hebben. Bijvoorbeeld dat de website niet langer actief wordt onderhouden. Het kan ook zijn dat de pagina waar de link naar verwijst is verwijderd (wat weer controversiële content kan signaleren), of dat het doel-domein niet langer geregistreerd is en beschikbaar is voor iemand die in dat domeinnaam geïnteresseerd is. Verschillende redenen kunnen van belang zijn voor de persoon die de crawler en scraper gebruikt. We slaan de foutreden op in een bestand, en proberen specifiek te identificeren als de domeinnaam beschikbaar is voor registratie door te achterhalen of de fout een 404 (Niet beschikbaar) fout is of een DNS-fout.
def add_errback(self, request):
return request.replace(errback=self.errback_httpbin)
def errback_httpbin(self, failure):
self.logger.error(repr(failure))
global current_url_idcount
current_url_idcount = current_url_idcount + 1
global current_url_printtreshold
try:
global domain_avail
domain_avail = sum(1 for line in open("/home/ubuntu/scrapy/output/%s.csv" %filename)) - 1
except Exception:
sys.exc_clear()
if current_url_idcount == current_url_printtreshold:
global domain_count
with open('/home/ubuntu/logs/crawler1/crawler1/lastfeed.txt', 'a') as outfile:
print outfile, "pcrawl: " + str(current_url_idcount) + " dcheck: " + str(domain_count) + " davail: " + str(domain_avail)
print "pcrawl: " + str(current_url_idcount) + " dcheck: " + str(
domain_count) + " davail: " + str(domain_avail)
global current_url_printtreshold
current_url_printtreshold = current_url_idcount + 50
if failure.check(HttpError):
response = failure.value.response
response2 = str(response)
response3 = response2[:4]
Als de fout een 503-error is, heeft dit te maken met de configuratie van het Domain Name System (DNS) / nameservers van het domein. Dit wijst er waarschijnlijk op dat het domein niet langer geregistreerd is - het kan bijvoorbeeld verlopen zijn. Het kan ook zijn dat de DNS niet is ingesteld, of onjuist is ingesteld. We slaan deze informatie op evenals de verwijzende url.
if response3 ==
global domain_count
domain_count = domain_count + 1
extracted = tldextract.extract(response.url)
newstr4 = "{}.{}".format(extracted.domain, extracted.suffix)
referring_url = response.request.headers.get('Referer', None)
item = DmozItem(domaincrawl=newstr4, current_url_id = current_url_idcount, domain_id = domain_count, refer_domain = referring_url)
De kruiscontrole hieronder controleert de specifieke DNS-fout, maar dit is niet waterdicht. Daarom is de waarde ingesteld op 0. Zoals opgemerkt in de conclusie, is een meer betrouwbare manier het integreren van een externe API in de onderstaande code. Geretourneerde en opgeleverde items worden verwerkt door de Scrapy's pijplijn, waar we in de volgende sectie mee te maken krijgen.
CROSS_CHECK_DNS = 0
iF CROSS_CHECK_DNS == 1:
rules = (
Rule(LinkExtractor(),
process_request='add_errback'),
)
def add_errback(self, request):
return request.replace(errback=self.errback_httpbin)
def errback_httpbin(self, failure):
self.logger.error(repr(failure))
if failure.check(DNSLookupError):
request = failure.request
self.logger.error('DNSLookupError on %s', request.url)
item = DmozItem(current_url = request.url)
print item
print "TEST" +"item"
return item
yield item
Conclusie
De code in deze tutorial is een eerste stap in web crawlen en scrapen met Python. Het richt zich vooral op het vinden van verlopen domeinen en gebroken links. Door de Scrapy-bibliotheek te gebruiken, begin je vanaf een vooraf bepaalde lijst van domeinen, scrapet deze, slaat gevonden links op, en crawlt en scrapet deze op hun beurt. Voor mensen die geïnteresseerd zijn in het gebruiken van Python voor data-analyse, kan het artikel Analyzing data using Polars in Python: an introduction nuttige inzichten bieden in het verwerken van de data die is verzameld door web scrapen. Dit proces gaat door totdat de crawler handmatig wordt gestopt (of, in theorie, als er geen links meer te crawlen zijn op het web, of - in netwerktermen - als de 'hoofdcomponent' volledig is gecrawld). Het slaat informatie over elke link op in een database.
De crawler in deze setup richt zich op links die een DNS-gerelateerde fout teruggeven wanneer ze worden gecrawld. In de meeste gevallen betekent dit dat het domein waarnaar de link verwijst, beschikbaar is voor registratie, maar niet altijd. Zoals eerder opgemerkt, kan het ook wijzen op een misconfiguratie van de DNS of naamservers. Om de laatste reden uit te sluiten, kun je een API van bijvoorbeeld een domeinregistrar integreren om het domein te controleren. Dit kan worden opgenomen in de eerder genoemde errorfunctie.
De code kan voor verschillende doeleinden worden verbeterd. Je kunt ervoor kiezen om alleen bepaalde soorten links te crawlen (bepaald in de Regels parameters), bepaalde domeinen met specifieke extensies, of links die alleen in bepaalde contexten voorkomen. Misschien wil je ook de gescrapete inhoud opslaan en verwerken. Scrapy heeft een andere component daarvoor, de pipeline, dat deel uitmaakt van de 'middleware' infrastructuur van Scrapy. Bijvoorbeeld, je kunt natuurlijke taalverwerking toepassen op de inhoud om grammaticale entiteiten te extraheren of om gemeenschappen of betekenissen te ontdekken. Ook kun je alle gecrawlde links opslaan om een netwerk analyse uit te voeren. Dit zijn enkele van de belangrijke onderwerpen in machine learning.
Delen