Weiter zum Inhalt

Web Scraping und Daten-Parsing in R | H-1B-Daten erkunden, Teil 1

Lerne, wie du Daten aus dem Web extrahierst, vorverarbeitest und mit R eine grundlegende explorative Datenanalyse durchführst.
Aktualisiert 18. Sept. 2026  · 15 Min. lesen

Mit KI erkunden

ChatGPTClaudePerplexity

Das Ziel dieses Tutorials ist, dir zu zeigen, wie du Daten zu H‑1B‑Visa per Web Scraping mit R sammelst. Anschließend lernst du, wie du JSON-Objekte parsest sowie die Daten speicherst und weiterverarbeitest, um eine grundlegende explorative Datenanalyse (EDA) des großen Datensatzes an H‑1B‑Anträgen durchzuführen.

Vielleicht findest du so heraus, wie du dich als Kandidat am besten positionierst – oder lernst neuen R‑Code!

Einführung

Letzte Woche hat DataCamps Blog „Can Data Help Your H-1B Visa Application“ einige Ergebnisse einer Analyse der H‑1B‑Daten über die Jahre vorgestellt. Jetzt bist du dran: Schnapp dir die Daten, analysiere sie selbst und finde heraus, was noch drinsteckt! Ted Kwartler führt dich mit einer Reihe von R‑Tutorials Schritt für Schritt durch.

Ein Freund von mir arbeitet in einer Kanzlei in Texas, die H‑1B‑Visa beantragt. Das H‑1B ist ein Nichteinwanderungsvisum in den USA, das es US‑Arbeitgebern erlaubt, ausländische Fachkräfte vorübergehend in Spezialberufen zu beschäftigen. Die Annahme ist offenbar extrem schwierig, da die Zahl der Visa begrenzt ist, sich aber Tausende bewerben. Das ist zwar anekdotisch, aber ich wollte mir die Daten selbst ansehen – in der Hoffnung, qualifizierten Kandidaten zu zeigen, dass die USA ein gastfreundlicher Ort sind!

Daten beschaffen: Web Scraping und Parsing

Ein DataCamp-Kollege hat mich auf diese Seite hingewiesen: eine einfache Website mit H‑1B‑Daten von 2012 bis 2016. Die Seite gibt an, 2 Mio. H‑1B‑Anträge in einer einzigen Tabelle zu organisieren.Ich habe mich entschieden, die Daten programmatisch zu sammeln (lies: per Web Scraping), denn Copy & Paste bis ans Lebensende war keine Option! Wie du siehst, zeigt das Bild unten einen Ausschnitt mit Bostons H‑1B‑Daten:

\"H1B

In diesem Tutorial nutzen wir unter anderem folgende Bibliotheken: jsonlite zum Parsen von JSON-Objekten, rvest zum „Ernten“ von HTML, pbapply – mein Favorit, weil es Fortschrittsbalken zu den Basis-apply‑Funktionen hinzufügt – und data.table, um R bei großen Data Frames Beine zu machen.

library(jsonlite)
library(rvest)
library(pbapply)
library(data.table)

Die Seitenstruktur erkunden

Wenn du die Seite erkundest, fällt dir auf, dass das Suchformular Vorschläge zum Autovervollständigen anbietet. Tippst du zum Beispiel ein „B“ in das City-Feld, erscheint ein Modal mit Vorschlägen wie unten gezeigt. Das folgende Bild zeigt die Optionen, wenn ich „B“ tippe:

\"Salary

Das bedeutet: Du kannst die Autovervollständigung als effiziente Möglichkeit nutzen, die Seite abzufragen. In Chrome kannst du neu laden, dann per Rechtsklick „Untersuchen“ wählen, im Entwicklertool zu „Network“ wechseln und schließlich auf der Seite „B“ tippen, um das Modal zu laden. Beim Durchsehen der Netzwerk-Links findest du eine PHP‑Abfrage, die ein JSON‑Objekt mit Städten zurückgibt – etwa diese. Ziel ist es, zunächst alle vorgeschlagenen Städte zu sammeln und diese Liste dann zu nutzen, um viele Seiten mit H‑1B‑Daten zu scrapen. Beim Blick auf die URL oben fällt dir auf, dass sie mit einem Buchstaben endet. Du kannst also paste0() mit der Basis‑URL http://h1bdata.info/cities.php?term= und letters verwenden. Die Basis wird für jeden Wert in letters recycelt. Das Objekt letters ist ein eingebauter R‑Vektor von „a“ bis „z“. Das Objekt json.cities ist ein Vektor von URLs von a bis z, die alle Autovervollständigungs‑Vorschläge als JSON enthalten.

json.cities<-paste0('http://h1bdata.info/cities.php?term=', letters)
JSON-Objekte parsenDas json.cities-Objekt ist ein Vektor aus 26 Links, die R einlesen muss. Mit lapply() oder pblapply() zusammen mit fromJSON parst R jedes JSON‑Objekt, um all.cities zu erstellen. Du verschachtelst das Ergebnis in unlist, sodass die Ausgabe ein einfacher String‑Vektor ist. Mit diesem Code hast du alle vorgeschlagenen Städte in einem Vektor, den du zum Aufbau der eigentlichen Datenseiten verwenden kannst.
all.cities<-unlist(pblapply(json.cities,fromJSON))
Um die Ladezeit einzelner Seiten zu verringern, kannst du zwei Parameter – Stadt und Jahr – in jede Seitenabfrage geben. Zum Beispiel Boston‑Daten 2012, dann Boston 2013 und so weiter. Eine großartige Funktion für Faktorkombinationen ist expand.grid(). Im Code unten siehst du, dass die Stadtinformationen all.cities übergeben werden und das Jahr per seq() von 2012 bis 2016. Die Funktion erzeugt über 5.000 Stadt‑Jahr‑Kombinationen. expand.grid() erzeugt programmatisch Boston 2012, Boston 2013, Boston 2014 usw., da jede Stadt und jedes Jahr eine eindeutige Kombination darstellen.
city.year<-expand.grid(city=all.cities,yr=seq(2012,2016))
Einige Städte wie Los Angeles bestehen aus zwei Wörtern und müssen für URLs kodiert werden. Die Funktion url_encode() ändert „Los Angeles“ in Los%20Angeles, damit die Adresse gültig ist. Du übergibst den gesamten Vektor, und url_encode() arbeitet zeilenweise:
city.year$city<-urltools::url_encode(as.character(city.year$city))
Zum Schluss nutzt du erneut paste0(), um die Basis‑URL mit den Stadt‑ und Jahres‑Kombinationen in city.year zu verketten. Ein Beispiel‑Link hier.
all.urls<-paste0('http://h1bdata.info/index.php?em=&job=&city=', city.year[,1],'&year=', city.year[,2])

Informationen von Seiten extrahieren

Nachdem du die vorherigen Schritte erledigt hast, kannst du eine eigene Funktion namens main schreiben, um die Daten jeder Seite zu sammeln. Der Workflow ist mit rvest ziemlich geradlinig. Zuerst wird eine URL übergeben und mit read_html() der Seiteninhalt geparst. Dann wird die einzige html_table der Seite aus dem restlichen HTML herausgegriffen. Die Funktion main konvertiert das Objekt x in eine data.table, damit es speichereffizient vorliegt. Bevor main endet, kannst du mit Sys.sleep noch eine Pause einbauen, damit du nicht als DDOS‑Angriff wahrgenommen wirst.

main<-function(url.x){
  x<-read_html(url.x)
  x<-html_table(x)
  x<-data.table(x[[1]])
  return(x)
  Sys.sleep(5)
}

Jetzt holen wir uns die Daten! Ich nutze gern den Fortschrittsbalken von pblapply(), um den Scraping‑Fortschritt im Blick zu behalten.
Du übergibst einfach all.urls und die Funktion main an pblapply(). R lädt sofort eine Seite, sammelt die Tabelle und hält die jeweilige data.table im Speicher. Jede URL wird der Reihe nach abgearbeitet und im Speicher gehalten.

all.h1b<-pblapply(all.urls, main)

Daten zu einer Data Table zusammenführen

Uff! Das hat Stunden gedauert! An diesem Punkt ist all.h1b eine Liste von Data Tables, eine pro Seite. Um die Liste in eine einzige Data Table zu vereinen, kannst du rbindlist verwenden. Das ist ähnlich zu do.call(rbind, all.h1b), nur deutlich schneller.

all.h1b<-rbindlist(all.h1b)

Speichere die Daten zum Schluss ab, damit du das nicht nochmal machen musst. Zum Glück habe ich hier eine Kopie gesichert.

write.csv(all.h1b,'h1b_data.csv', row.names=F)

Daten bereinigen

Auch wenn du die Daten selbst gescraped hast, sind ein paar zusätzliche Schritte nötig, um sie in ein gut handhabbares Format zu bringen.

Du nutzt lubridate, um Datumsangaben zu organisieren. Außerdem kommt stringr zum Einsatz, das komfortable Wrapper für String‑Manipulation bereitstellt.

library(lubridate)
library(stringr)

Das ist Geschmackssache, aber ich setze gern scipen=999. Pflicht ist das nicht, aber so verschwindet die wissenschaftliche Notation.

options(scipen=999)

Beim Scraping sind 1,8 Mio. der 2 Mio. H‑1B‑Datensätze erfasst worden. Aus meiner Sicht sind 1,8 Mio. gut genug. Also laden wir die Daten mit fread(): Diese Funktion ist wie read.csv, nur als „fast & friendly file finagler“ wesentlich effizienter.

h1b.data<-fread('h1b_data.csv')

Die Spaltennamen der gescrapten Daten sind in Großbuchstaben und enthalten Leerzeichen.

Sie per Namen zu referenzieren ist mühsam, daher benennst du sie zuerst um.

Für das Umbenennen brauchst du Funktionen auf beiden Seiten des Zuweisungsoperators (<-). Links verwendest du colnames() und übergibst den Data Frame. Rechts übergibst du einen String‑Vektor.

In diesem Beispiel nimmst du zunächst die Originalnamen und wandelst sie mit tolower() in Kleinbuchstaben um. In der zweiten Zeile wendest du gsub() an, eine Funktion für globale Ersetzungen.

Wenn gsub() ein Muster erkennt – hier ein Leerzeichen –, ersetzt es alle Vorkommen durch den zweiten Parameter, den Unterstrich. Abschließend sagst du gsub(), die Ersetzungen in names(h1b.data) vorzunehmen, also in den jetzt kleingeschriebenen Spaltennamen.

colnames(h1b.data)<-tolower(names(h1b.data))
colnames(h1b.data)<-gsub(' ', '_', names(h1b.data))  

Einer meiner ersten Schritte bei der Datenerkundung ist tail(). Diese Funktion gibt die letzten Zeilen zurück. Hier liefert tail() die letzten 8 Zeilen.

So bekommst du schnell ein Gefühl für Form und Vektoren der Daten.

tail(h1b.data, 8)

Als Nächstes prüfe ich immer die Klassen der Vektoren. Bei gescrapten Daten können numerische Werte oder Faktoren zu Text werden. Wenn du die Klassen jetzt korrigierst, ersparst du dir später Ärger!

Mit der Funktion apply() übergibst du h1b.data, dann 2 und die Funktion class. Da du 2 gewählt hast, prüft R die Klasse jeder Spalte und gibt sie in der Konsole aus. Mit apply() und 1 würdest du zeilenweise arbeiten – hier nicht hilfreich.

apply(h1b.data,2,class)

Oh oh!

Alle Spalten sind „character“ und müssen korrigiert werden. Ich zeige dir, wie du eine der Datumsspalten änderst, den Rest überlasse ich dir. Mit tail() siehst du dir die letzten 6 Zeilen der falsch typisierten Daten an.

Um die Datumsangaben zu korrigieren, müssen die Schrägstriche / in Bindestriche - geändert werden. Wieder hilft gsub(): Suche nach / und ersetze durch -.

tail(h1b.data$submit_date)
h1b.data$submit_date<-gsub('/', '-', h1b.data$submit_date)

Mit den Bindestrichen kannst du mdy() anwenden – das steht für „month, day, year“. So sind die Daten angeordnet. Wäre die Reihenfolge anders, würdest du die Buchstaben entsprechend anpassen.

Um sicherzugehen, dass die Spalte korrekt geändert wurde, prüfst du erneut das Tail und die Klasse. tail() sollte Daten wie „2016-03-11 UTC“ zeigen, und die Vektorklasse sollte „POSIXct“ statt „character“ sein.

h1b.data$submit_date<-mdy(h1b.data$submit_date)
tail(h1b.data$submit_date)
class(h1b.data$submit_date)

Für diese Analyse ist es sinnvoll, nur Monat und Jahr in neue Spalten zu extrahieren. Im folgenden Code siehst du zwei neue Spalten: $submit_month und $submit_yr.

In lubridate kannst du mit month() auf eine ganze Spalte den Monat aus dem Datum extrahieren. year() akzeptiert ebenso die Datumsspalte und erzeugt h1b.data$submit_yr. Mit head() solltest du jetzt zwei neue Spalten sehen.

h1b.data$submit_month<-month(h1b.data$submit_date, label=T)
h1b.data$submit_yr<-year(h1b.data$submit_date)
head(h1b.data)

Als Nächstes sehen wir uns $base_salary an. Dort steht ein Komma als Tausendertrennzeichen, weshalb R die Werte als Text betrachtet – das muss geändert werden. Wieder hilft gsub(), um das Komma zu entfernen und durch ein leeres Zeichen zu ersetzen. Danach wandelst du mit as.numeric() h1b.data$base_salary offiziell in numerische Werte um.

Mit head() kannst du dir einen Ausschnitt des neuen Vektors ansehen.

h1b.data$base_salary<-gsub(',','',h1b.data$base_salary)
h1b.data$base_salary<-as.numeric(h1b.data$base_salary)
head(h1b.data$base_salary)

Eine weitere sinnvolle Aufteilung ist nach Bundesstaat. In h1b.data$location sind Stadt und Staat durch ein Komma getrennt. Der Code unten nutzt str_split_fixed(), um die Ortsangabe am ersten Komma zu teilen. Übergebe einfach die Spalte, das Trennzeichen und die Anzahl der zurückzugebenden Spalten. Das resultierende Objekt state ist eine große Matrix mit derselben Zeilenzahl wie h1b.data und 2 Spalten.

state<-str_split_fixed(h1b.data$location,', ', 2)

Die nächsten zwei Zeilen binden die einzelnen Vektoren als $city und $state an h1b.data. Die Vektoren sind nicht perfekt, da Schreibweisen variieren können – etwa „Winston Salem“ versus „Winston‑Salem“. Für einfache EDA ist die Methode insgesamt gut genug, behalte aber im Hinterkopf, dass du in anderen Analysen Begriffe eventuell zusammenführen musst.

h1b.data$city<-state[,1]
h1b.data$state<-state[,2] 

Deine Daten erkunden: Erste Schritte

Wenn du dich für ein H‑1B‑Visum bewerben würdest, wolltest du wissen, in welchen Staaten die Chancen am größten sind. Die Funktion table() zählt kategoriale Variablen und lässt sich leicht auf h1b.data$state anwenden. In der zweiten Zeile erstellst du einen kleinen Data Frame, der die Staatskürzel und die gezählten H‑1B‑Werte enthält.

state.tally<-table(h1b.data$state)
state.tally<-data.frame(state=names(state.tally), h1b=as.vector(state.tally))

Mit state.tally und barplot() erzeugst du ein einfaches Balkendiagramm der H‑1B‑Werte je Staat. Der zweite Parameter names.arg definiert die Balkenbeschriftungen, und las=3 weist R an, die Labels vertikal zu platzieren. Einige unsaubere Ortsangaben durch das Komma‑Split sind zu sehen, aber die Aussage ist klar: Bewerber landen sehr wahrscheinlich in CA, NJ, NY oder TX.

barplot(state.tally$h1b,
        names.arg = names(table(h1b.data$state)),
        las=3)

\"H1B

Du siehst die H‑1B‑Summen nach Staat von 2012 bis 2016.

Als Nächstes versuchen wir, den Zusammenhang zwischen H‑1B‑Visa und einer externen Kennzahl zu verstehen. Der Einfachheit halber hat R einen eingebauten Datensatz namens state.x77. Das ist eine Matrix mit 50 Zeilen – je Staat eine – und Angaben wie Bevölkerung und Lebenserwartung aus dem US‑Zensus 1977.

Tipp: Verwende in deiner eigenen Analyse eine aktuellere Datenquelle.

Fürs Lernen ist state.x77 jetzt ein gutes Beispiel. Sieh dir den Datensatz mit head() an.

head(state.x77)

Verschmelzen wir diese Informationen mit den state.tally‑Daten zu einem größeren Data Frame, um Beziehungen zu verstehen. Erstelle dazu einen Data Frame state.data mit den Staatskürzeln state.abb und den alten Zensusdaten von 1977. Dann führst du mit merge state.tally und state.data zusammen.

Du kannst die Spalte state explizit als Join‑Schlüssel angeben. Im dritten Schritt inspizierst du per Index die Zeilen 15 bis 20.

state.data<-data.frame(state=state.abb,state.x77)
state.data<-merge(state.tally,
                  state.data,
                  by='state')
state.data[15:20,]

Eine grundlegende EDA‑Funktion ist cor: Sie gibt die Korrelation zwischen zwei Variablen aus.

Denk daran: Korrelationen reichen von −1 bis 1. 0 bedeutet keinen Zusammenhang. Werte nahe 1 bedeuten eine positive Korrelation (hoffentlich) wie R‑Programmierung und Einkommen. Negative Werte deuten auf einen gegenläufigen Zusammenhang hin – zum Beispiel R‑Programmierung und Sozialleben!

Dieser Code wendet cor auf die Bevölkerungszahl 1977 und die aktuellen H‑1B‑Summen an. Der zeitliche Versatz ist uns bewusst – der Code soll die Vorgehensweise illustrieren. Du kannst $Population natürlich gegen einen anderen Vektor im Data Frame tauschen.

cor(state.data$Population, state.data$h1b)

Eine weitere Möglichkeit, Zusammenhänge zu untersuchen, ist ein Scatterplot – besonders eine Scatterplot‑Matrix. Mit pairs() erstellst du diese schnell. Der Code unten nutzt eine Formel zur Definition der Beziehungen. Jede Spalte wird einzeln mit Pluszeichen verknüpft. Der Parameter data nimmt den Data Frame, und main setzt den Titel.

pairs(~ h1b + Population + Income,
      data = state.data,
      main='h1b relationships')

\"H1B

Du siehst, wie diese Scatterplot‑Matrix die H‑1B‑Summen den Variablen Population und Income gegenüberstellt.

Zwischen Population und h1b ist eine Beziehung zu erkennen. Das ist intuitiv, da bevölkerungsreichere Staaten mehr Jobchancen – und damit mehr Bedarf an H‑1B‑Visa – haben.

Um in einen einzelnen Plot aus der Matrix „hineinzuzoomen“, ruf einfach plot() mit zwei Variablen auf.

plot(state.data$Income,state.data$h1b,
     main = 'Income to H1B')

Nächste Schritte mit deinen H‑1B‑Daten

Wir haben die H‑1B‑Daten gerade erst angerissen! Der Datensatz ist sehr reichhaltig, und im nächsten Beitrag analysierst du die Gehaltsdaten, entfernst Ausreißer und erstellst aussagekräftigere Visualisierungen mit ggplot2.

Außerdem baust du auf diesen EDA‑Konzepten auf, indem du den H‑1B‑Status über die Zeit und die Top‑Arbeitgeber untersuchst. Eine der coolen Visualisierungen, die ich dir schon jetzt zeigen kann, ist ein Boxplot mit Gehaltsverteilungen nach H‑1B‑Status. Der Code dazu:

ggplot(h1b.data) +
geom_boxplot(aes(factor(case_status),base_salary,fill=as.factor(case_status))) +
  ylim(0,100000) +
  theme_gdocs() +
  scale_fill_gdocs() +
  theme(axis.text.x=element_blank())

\"H1B

Bleib dran für den nächsten Teil der Tutorialserie „Exploring H‑1B Data with R“! Schau dir in der Zwischenzeit unser R‑Data‑Frame‑Tutorial, den Importing Data in R‑Kurs oder unseren Data Manipulation in R with dplyr‑Kurs an. 

Themen
R
Datenwissenschaft
Python
Datenanalyse

R‑Kurse

Kurs

Web Scraping in R

4 Std.
15.2K
Hier lernst du, wie du mit R Daten von jeder Website effizient sammeln und herunterladen kannst.
Details anzeigenRight Arrow
Kurs Starten
Mehr anzeigenRight Arrow
Verwandt

Tutorial

Python Datenstrukturen Tutorial

Mach dich mit Python-Datenstrukturen vertraut: Lerne mehr über Datentypen und primitive sowie nicht-primitive Datenstrukturen wie Strings, Listen, Stapel usw.
Sejal Jaiswal's photo

Sejal Jaiswal

24 Min.

Tutorial

Loop-Schleifen in Python-Tutorial

Lerne, wie du For-Schleifen in Python umsetzt, um eine Sequenz oder die Zeilen und Spalten eines Pandas-DataFrame zu durchlaufen.
Aditya Sharma's photo

Aditya Sharma

5 Min.

Tutorial

Fibonacci-Folge in Python: Lerne und entdecke Programmiertechniken

Finde raus, wie die Fibonacci-Folge funktioniert. Schau dir die mathematischen Eigenschaften und die Anwendungen in der echten Welt an.
Laiba Siddiqui's photo

Laiba Siddiqui

6 Min.

Tutorial

Wie man Listen in Python aufteilt: Einfache Beispiele und fortgeschrittene Methoden

Lerne, wie du Python-Listen mit Techniken wie Slicing, List Comprehensions und itertools aufteilen kannst. Finde heraus, wann du welche Methode für die beste Datenverarbeitung nutzen solltest.
Allan Ouko's photo

Allan Ouko

11 Min.

Tutorial

Python Switch Case Statement: Ein Leitfaden für Anfänger

Erforsche Pythons match-case: eine Anleitung zu seiner Syntax, Anwendungen in Data Science und ML sowie eine vergleichende Analyse mit dem traditionellen switch-case.
Matt Crabtree's photo

Matt Crabtree

5 Min.

Tutorial

Python-Anweisungen IF, ELIF und ELSE

In diesem Tutorial lernst du ausschließlich Python if else-Anweisungen kennen.
Sejal Jaiswal's photo

Sejal Jaiswal

9 Min.

Mehr AnzeigenMehr Anzeigen