Kapitel 1. Das Tidy Text Format

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Die Anwendung von Ordnungsprinzipien ist ein wirksames Mittel, um den Umgang mit Daten einfacher und effektiver zu gestalten, und das gilt nicht minder für den Umgang mit Text. Wie von Hadley Wickham (Wickham 2014) beschrieben, haben aufgeräumte Daten eine spezifische Struktur:

  • Jede Variable ist eine Spalte.

  • Jede Beobachtung ist eine Zeile.

  • Jede Art von Beobachtungseinheit ist eine Tabelle.

Wir definieren das Tidy-Text-Format daher als eine Tabelle mit einem Token pro Zeile. Ein Token ist eine sinnvolle Texteinheit, wie z. B. ein Wort, die wir für die Analyse verwenden möchten, und die Tokenisierung ist der Prozess der Aufteilung des Textes in Token. Diese Ein-Token-pro-Zeile-Struktur steht im Gegensatz zu der Art und Weise, wie Text in aktuellen Analysen oft gespeichert wird, z. B. als Zeichenketten oder in einer Dokument-Term-Matrix. Beim Tidy Text Mining ist das Token, das in jeder Zeile gespeichert wird, meist ein einzelnes Wort, kann aber auch ein n-Gramm, ein Satz oder ein Absatz sein. Im tidytext-Paket bieten wir Funktionen zur Tokenisierung von häufig verwendeten Texteinheiten wie diesen und zur Konvertierung in ein Ein-Term-pro-Zeile-Format.

Aufgeräumte Datensätze ermöglichen die Bearbeitung mit einem Standardsatz von "aufgeräumten" Tools, darunter beliebte Pakete wie dplyr (Wickham und Francois 2016), tidyr (Wickham 2016), ggplot2 (Wickham 2009) und broom (Robinson 2017). Indem die Ein- und Ausgaben in aufgeräumten Tabellen gehalten werden, können die Nutzer fließend zwischen diesen Paketen wechseln. Wir haben festgestellt, dass sich diese aufgeräumten Tools ganz natürlich auf viele Textanalysen und -untersuchungen anwenden lassen.

Gleichzeitig erwartet das tidytext-Paket nicht, dass der Nutzer die Textdaten während einer Analyse immer in einer aufgeräumten Form hält. Das Paket enthält Funktionen für tidy() Objekte (siehe das Broom-Paket [Robinson, s.o.]) aus beliebten Text Mining R-Paketen wie tm (Feinerer et al. 2008) und quanteda (Benoit und Nulty 2016). Dies ermöglicht zum Beispiel einen Arbeitsablauf, bei dem das Importieren, Filtern und Verarbeiten mit dplyr und anderen Tidy-Tools erfolgt, wonach die Daten in eine Dokument-Term-Matrix für Machine-Learning-Anwendungen umgewandelt werden. Die Modelle können dann wieder in eine aufgeräumte Form umgewandelt werden, um sie mit ggplot2 zu interpretieren und zu visualisieren.

Vergleich von Tidy Text mit anderen Datenstrukturen

Wie bereits erwähnt, definieren wir das aufgeräumte Textformat als eine Tabelle miteinem Token pro Zeile. Wenn Textdaten auf diese Weise strukturiert werden, entsprechen sie den Grundsätzen für aufgeräumte Daten und können mit einer Reihe von konsistenten Tools bearbeitet werden. Es lohnt sich, dies mit der Art und Weise zu vergleichen, wie Text in Textmining-Ansätzen oft gespeichert wird:

String

Text kann in R natürlich als Zeichenkette (d. h. als Zeichenvektor) gespeichert werden, und oft werden Textdaten zunächst in dieser Form in den Speicher eingelesen.

Corpus

Diese Arten von Objekten enthalten in der Regel Rohzeichenketten, die mit zusätzlichen Metadaten und Details versehen sind.

Dokument-Begriffs-Matrix

Dies ist eine spärliche Matrix, die eine Sammlung (d.h. einen Korpus) von Dokumenten mit einer Zeile für jedes Dokument und einer Spalte für jeden Begriff beschreibt. Der Wert in der Matrix ist in der Regel die Wortanzahl oder tf-idf (siehe Kapitel 3).

Mit der Erforschung von Korpus- und Dokument-Term-Matrix-Objekten wollen wir uns bis Kapitel 5 Zeit lassen und uns auf die Grundlagen der Umwandlung von Text in ein ordentliches Format konzentrieren.

Die Funktion unnest_tokens

Emily Dickinson schrieb zu ihrer Zeit einige schöne Texte.

text <- c("Because I could not stop for Death -",
          "He kindly stopped for me -",
          "The Carriage held but just Ourselves -",
          "and Immortality")

text
## [1] "Because I could not stop for Death -"   "He kindly stopped for me -"
## [3] "The Carriage held but just Ourselves -" "and Immortality"

Dies ist ein typischer Zeichenvektor, den wir vielleicht analysieren wollen. Um ihn in einen ordentlichen Textdatensatz zu verwandeln, müssen wir ihn zunächst in einen Datenrahmen packen.

library(dplyr)
text_df <- data_frame(line = 1:4, text = text)

text_df
## # A tibble: 4 × 2
##    line                                   text
##   <int>                                  <chr>
## 1     1   Because I could not stop for Death -
## 2     2             He kindly stopped for me -
## 3     3 The Carriage held but just Ourselves -
## 4     4                        and Immortality

Was bedeutet es, dass dieser Datenrahmen als "Tibble" ausgedruckt wurde? Ein Tibble ist eine moderne Klasse von Datenrahmen in R, die in den Paketen dplyr und tibble verfügbar ist und über eine praktische Druckmethode verfügt, Strings nicht in Faktoren umwandelt und keine Zeilennamen verwendet. Tibbles eignen sich hervorragend für die Verwendung mit Tidy-Tools.

Beachte, dass dieser Datenrahmen mit Text noch nicht mit der Tidy-Textanalyse kompatibel ist. Wir können keine Wörter herausfiltern oder zählen, welche am häufigsten vorkommen, da jede Zeile aus mehreren kombinierten Wörtern besteht. Wir müssen sie so umwandeln, dass sieein Token pro Dokument und Zeile enthält.

Hinweis

Ein Token ist eine sinnvolle Texteinheit, meist ein Wort, die wir für die weitere Analyse verwenden möchten. Die Tokenisierung ist der Prozess, bei dem der Text in Token zerlegt wird.

In diesem ersten Beispiel haben wir nur ein Dokument (das Gedicht), aber wir werden bald Beispiele mit mehreren Dokumenten untersuchen.

In unserem tidytext-Framework müssen wir den Text sowohl in einzelne Token zerlegen (ein Prozess, der Tokenisierung genannt wird) als auch in eine aufgeräumte Datenstruktur umwandeln. Dazu verwenden wir die Funktion tidytextunnest_tokens().

library(tidytext)

text_df %>%
  unnest_tokens(word, text)
## # A tibble: 20 × 2
##     line    word
##    <int>   <chr>
## 1      1 because
## 2      1       i
## 3      1   could
## 4      1     not
## 5      1    stop
## 6      1     for
## 7      1   death
## 8      2      he
## 9      2  kindly
## 10     2 stopped
## # ... with 10 more rows

Die beiden grundlegenden Argumente für unnest_tokens sind Spaltennamen. Zuerst haben wir den Namen der Ausgabespalte, die erstellt wird, wenn der Text nicht verschachtelt ist (in diesem Fallword), und dann die Eingabespalte, aus der der Text stammt (in diesem Falltext). Erinnere dich daran, dass text_dfeine Spalte mit dem Namen text hat, die die gewünschten Daten enthält.

Nach der Verwendung von unnest_tokens haben wir jede Zeile so aufgeteilt, dass es in jeder Zeile des neuen Datenrahmens ein Token (Wort) gibt; die Standard-Tokenisierung in unnest_tokens() ist für einzelne Wörter, wie hier gezeigt. Beachte auch:

  • Andere Spalten, wie zum Beispiel die Zeilennummer, aus der jedes Wort stammt, werden beibehalten.

  • Die Interpunktion wurde entfernt.

  • Standardmäßig wandelt unnest_tokens() die Token in Kleinbuchstaben um, damit sie leichter mit anderen Datensätzen verglichen oder kombiniert werden können. (Verwende das Argumentto_lower = FALSE, um dieses Verhalten abzuschalten).

Wenn wir die Textdaten in diesem Format haben, können wir sie mit den Standardwerkzeugen von Tidy, nämlich dplyr, tidyr und ggplot2, bearbeiten, verarbeiten und visualisieren (siehe Abbildung 1-1).

tmwr 0101
Abbildung 1-1. Ein Flussdiagramm einer typischen Textanalyse mit Hilfe der Tidy Data Prinzipien. Dieses Kapitel zeigt, wie du mit diesen Tools Texte zusammenfassen und visualisieren kannst.

Aufräumen mit den Werken von Jane Austen

Nehmen wir den Text von Jane Austens sechs abgeschlossenen und veröffentlichten Romanen aus dem janeaustenr-Paket (Silge 2016) und bringen wir ihn in ein ordentliches Format. Das janeaustenr-Paket stellt diese Texte in einem einzeiligen Format zur Verfügung, wobei eine Zeile in diesem Kontext einer gedruckten Zeile in einem physischen Buch entspricht. Beginnen wir damit und verwenden mutate(), um einelinenumber Menge zu kommentieren, um die Zeilen im Originalformat zu verfolgen, und eine chapter (unter Verwendung einer Regex), um herauszufinden, wo alle Kapitel sind.

library(janeaustenr)
library(dplyr)
library(stringr)

original_books <- austen_books() %>%
  group_by(book) %>%
  mutate(linenumber = row_number(),
         chapter = cumsum(str_detect(text, regex("^chapter [\\divxlc]",
                                                 ignore_case = TRUE)))) %>%
  ungroup()

original_books
## # A tibble: 73,422 × 4
##                     text                book linenumber chapter
##                    <chr>              <fctr>      <int>   <int>
## 1  SENSE AND SENSIBILITY Sense & Sensibility          1       0
## 2                        Sense & Sensibility          2       0
## 3         by Jane Austen Sense & Sensibility          3       0
## 4                        Sense & Sensibility          4       0
## 5                 (1811) Sense & Sensibility          5       0
## 6                        Sense & Sensibility          6       0
## 7                        Sense & Sensibility          7       0
## 8                        Sense & Sensibility          8       0
## 9                        Sense & Sensibility          9       0
## 10             CHAPTER 1 Sense & Sensibility         10       1
## # ... with 73,412 more rows

Um mit diesem Datensatz ordentlich arbeiten zu können, müssen wir ihn in dasEin-Token-pro-Zeile-Format umstrukturieren, was, wie wir bereits gesehen haben, mit der Funktionunnest_tokens() geschieht.

library(tidytext)
tidy_books <- original_books %>%
  unnest_tokens(word, text)

tidy_books
## # A tibble: 725,054 × 4
##                   book linenumber chapter        word
##                 <fctr>      <int>   <int>       <chr>
## 1  Sense & Sensibility          1       0       sense
## 2  Sense & Sensibility          1       0         and
## 3  Sense & Sensibility          1       0 sensibility
## 4  Sense & Sensibility          3       0          by
## 5  Sense & Sensibility          3       0        jane
## 6  Sense & Sensibility          3       0      austen
## 7  Sense & Sensibility          5       0        1811
## 8  Sense & Sensibility         10       1     chapter
## 9  Sense & Sensibility         10       1           1
## 10 Sense & Sensibility         13       1         the
## # ... with 725,044 more rows

Diese Funktion verwendet das Pakettokenizers, um jede Textzeile im ursprünglichen Datenrahmen in Token zu trennen. Standardmäßig werden Wörter in Token unterteilt, aber es gibt auch andere Optionen wie Zeichen, N-Gramme, Sätze, Zeilen, Absätze oder die Trennung um ein Regex-Muster.

Jetzt, wo die Daten im Ein-Wort-pro-Zeile-Format vorliegen, können wir sie mit Ordnungswerkzeugen wie dplyr bearbeiten. Bei der Textanalyse wollen wir oft Stoppwörter entfernen, d. h. Wörter, die für die Analyse nicht nützlich sind, z. B. extrem häufige Wörter wie "the", "of", "to" und so weiter im Englischen. Wir können Stoppwörter (die im tidytext-Datensatz stop_words gespeichert sind) mit einem anti_join() entfernen.

data(stop_words)

tidy_books <- tidy_books %>%
  anti_join(stop_words)

Der Datensatz stop_words im tidytext-Paket enthält Stoppwörter aus drei Lexika. Wir können sie alle zusammen verwenden, wie wir es hier getan haben, oderfilter(), um nur einen Satz von Stoppwörtern zu verwenden, wenn das für eine bestimmte Analyse besser geeignet ist.

Wir können auch dplyr's count() benutzen, um die häufigsten Wörter in allen Büchern zu finden.

tidy_books %>%
  count(word, sort = TRUE)
## # A tibble: 13,914 × 2
##      word     n
##     <chr> <int>
## 1    miss  1855
## 2    time  1337
## 3   fanny   862
## 4    dear   822
## 5    lady   817
## 6     sir   806
## 7     day   797
## 8    emma   787
## 9  sister   727
## 10  house   699
## # ... with 13,904 more rows

Da wir die Tidy-Tools verwendet haben, werden unsere Wortzahlen in einem Tidy-Datenframe gespeichert. So können wir die Daten direkt an das ggplot2-Paket übergeben, um zum Beispiel eine Visualisierung der häufigsten Wörter zu erstellen(Abbildung 1-2).

library(ggplot2)

tidy_books %>%
  count(word, sort = TRUE) %>%
  filter(n > 600) %>%
  mutate(word = reorder(word, n)) %>%
  ggplot(aes(word, n)) +
  geom_col() +
  xlab(NULL) +
  coord_flip()
tmwr 0102
Abbildung 1-2. Die häufigsten Wörter in den Romanen von Jane Austen

Beachte, dass wir mit der Funktion austen_books() mit genau dem Text begonnen haben, den wir analysieren wollten, aber in anderen Fällen müssen wir die Textdaten möglicherweise bereinigen, z. B. Copyright-Kopfzeilen oder Formatierungen entfernen. Beispiele für diese Art der Vorverarbeitung findest du in den Fallstudienkapiteln, insbesondere in "Vorverarbeitung".

Das gutenbergr Paket

Nachdem wir uns mit dem Paket janeaustenr mit dem Aufräumen von Text beschäftigt haben, stellen wir dir jetzt das Paketgutenbergr vor (Robinson 2016). Das Paket gutenbergr bietet Zugang zu den gemeinfreien Werken von , der Sammlung des Project Gutenberg. Das Paket enthält sowohl Werkzeuge zum Herunterladen von Büchern (wobei die wenig hilfreichen Kopf- und Fußzeileninformationen entfernt werden) als auch einen vollständigen Datensatz mit Metadaten des Project Gutenberg, mit dem sich interessante Werke finden lassen. In diesem Buch werden wir hauptsächlich die Funktion gutenberg_download() verwenden, die ein oder mehrere Werke von Project Gutenberg nach ID herunterlädt. Du kannst aber auch andere Funktionen verwenden, um Metadaten zu untersuchen, die Gutenberg-ID mit Titel, Autor, Sprache usw. zu verknüpfen oder Informationen über Autoren zu sammeln.

Tipp

Um mehr über gutenbergr zu erfahren, schau dir das Tutorial des Pakets bei rOpenSci an, wo es eines der rOpenSci-Pakete für den Datenzugang ist.

Wortfrequenzen

Eine häufige Aufgabe beim Textmining ist es, die Häufigkeit von Wörtern zu untersuchen, so wie wir es oben für Jane Austens Romane getan haben, und die Häufigkeit in verschiedenen Texten zu vergleichen. Wir können dies intuitiv und reibungslos mit Hilfe von ordentlichen Datenprinzipien tun. Die Werke von Jane Austen haben wir bereits, jetzt brauchen wir noch zwei weitere Texte, die wir miteinander vergleichen können. Zunächst schauen wir uns einige Science-Fiction- und Fantasy-Romane von H.G. Wells an, der im späten 19. und frühen 20. Jahrhundert lebte. Jahrhundert lebte. Wir nehmen Die Zeitmaschine, Der Krieg der Welten, Der unsichtbare Mann und Die Insel des Doktor Moreau. Wir können diese Werke über gutenberg_download() und die Project Gutenberg ID-Nummern der einzelnen Romane aufrufen.

library(gutenbergr)

hgwells <- gutenberg_download(c(35, 36, 5230, 159))
tidy_hgwells <- hgwells %>%
  unnest_tokens(word, text) %>%
  anti_join(stop_words)

Was sind die häufigsten Wörter in diesen Romanen von H.G. Wells?

tidy_hgwells %>%
  count(word, sort = TRUE)
## # A tibble: 11,769 × 2
##      word     n
##     <chr> <int>
## 1    time   454
## 2  people   302
## 3    door   260
## 4   heard   249
## 5   black   232
## 6   stood   229
## 7   white   222
## 8    hand   218
## 9    kemp   213
## 10   eyes   210
## # ... with 11,759 more rows

Nehmen wir uns nun einige bekannte Werke der Brontë-Schwestern vor, deren Leben sich mit dem von Jane Austen etwas überschnitt, die aber in einem ganz anderen Stil schrieben. Wir nehmen uns Jane Eyre, Wuthering Heights,The Tenant of Wildfell Hall,Villette undAgnes Grey vor. Wir werden wieder die Project Gutenberg ID-Nummern für jeden Roman verwenden und die Texte über gutenberg_download() aufrufen.

bronte <- gutenberg_download(c(1260, 768, 969, 9182, 767))
tidy_bronte <- bronte %>%
  unnest_tokens(word, text) %>%
  anti_join(stop_words)

Was sind die häufigsten Wörter in diesen Romanen der Brontë-Schwestern?

tidy_bronte %>%
  count(word, sort = TRUE)
## # A tibble: 23,051 × 2
##      word     n
##     <chr> <int>
## 1    time  1065
## 2    miss   855
## 3     day   827
## 4    hand   768
## 5    eyes   713
## 6   night   647
## 7   heart   638
## 8  looked   602
## 9    door   592
## 10   half   586
## # ... with 23,041 more rows

Interessant, dass "Zeit", "Augen" und "Hand" sowohl bei H.G. Wells als auch bei den Brontë-Schwestern in den Top 10 sind.

Berechnen wir nun die Häufigkeit der einzelnen Wörter in den Werken von Jane Austen, den Brontë-Schwestern und H.G. Wells, indem wir die Datenrahmen miteinander verbinden. Wir können spread und gather von tidyr verwenden, um unseren Datenrahmen so umzugestalten, dass er genau das ist, was wir für die Darstellung und den Vergleich der drei Romansätze brauchen.

library(tidyr)

frequency <- bind_rows(mutate(tidy_bronte, author = "Brontë Sisters"),
                       mutate(tidy_hgwells, author = "H.G. Wells"),
                       mutate(tidy_books, author = "Jane Austen")) %>%
  mutate(word = str_extract(word, "[a-z']+")) %>%
  count(author, word) %>%
  group_by(author) %>%
  mutate(proportion = n / sum(n)) %>%
  select(-n) %>%
  spread(author, proportion) %>%
  gather(author, proportion, `Brontë Sisters`:`H.G. Wells`)

Wir verwenden hier str_extract(), weil die UTF-8 kodierten Texte von Project Gutenberg einige Beispiele von Wörtern mit Unterstrichen enthalten, um sie hervorzuheben (wie Kursivschrift). Der Tokenizer hat diese als Wörter behandelt, aber wir wollen "any" nicht getrennt von "any" zählen, wie wir bei unserer ersten Datenexploration gesehen haben, bevor wir uns für str_extract() entschieden haben.

Zeichnen wir nun ein Diagramm(Abbildung 1-3).

library(scales)

# expect a warning about rows with missing values being removed
ggplot(frequency, aes(x = proportion, y = `Jane Austen`,
                      color = abs(`Jane Austen` - proportion))) +
  geom_abline(color = "gray40", lty = 2) +
  geom_jitter(alpha = 0.1, size = 2.5, width = 0.3, height = 0.3) +
  geom_text(aes(label = word), check_overlap = TRUE, vjust = 1.5) +
  scale_x_log10(labels = percent_format()) +
  scale_y_log10(labels = percent_format()) +
  scale_color_gradient(limits = c(0, 0.001),
                       low = "darkslategray4", high = "gray75") +
  facet_wrap(~author, ncol = 2) +
  theme(legend.position="none") +
  labs(y = "Jane Austen", x = NULL)
tmwr 0103
Abbildung 1-3. Vergleich der Worthäufigkeiten von Jane Austen, den Brontë-Schwestern und H.G. Wells

Wörter, die in diesen Diagrammen nahe an der Linie liegen, kommen in beiden Textgruppen ähnlich häufig vor, z. B. in den Texten von Austen und Brontë ("Miss", "Time" und "Day" am oberen Ende der Häufigkeit) oder in den Texten von Austen und Wells ("Time", "Day" und "Brother" am oberen Ende der Häufigkeit). Wörter, die weit von der Linie entfernt sind, sind Wörter, die in einer Gruppe von Texten häufiger vorkommen als in einer anderen. In der Austen-Brontë-Gruppe kommen zum Beispiel Wörter wie "Elizabeth", "Emma" und "Fanny" (alles Eigennamen) in den Austen-Texten, aber kaum in den Brontë-Texten vor, während Wörter wie "Arthur" und "Hund" in den Brontë-Texten, aber nicht in den Austen-Texten zu finden sind. Wenn du H.G. Wells mit Jane Austen vergleichst, fällt auf, dass Wells Wörter wie "Biest", "Gewehre", "Füße" und "schwarz" verwendet, die Austen nicht kennt, während Austen Wörter wie "Familie", "Freund", "Brief" und "lieb" verwendet, die Wells nicht kennt.

Insgesamt fällt in Abbildung 1-3 auf, dass die Wörter im Austen-Brontë-Panel näher an der Null-Linie liegen als im Austen-Wells-Panel. Beachte auch, dass sich die Wörter im Austen-Brontë-Panel auf niedrigere Frequenzen erstrecken; im Austen-Wells-Panel gibt es bei niedrigen Frequenzen einen leeren Raum. Diese Merkmale deuten darauf hin, dass Austen und die Brontë-Schwestern mehr ähnliche Wörter verwenden als Austen und H.G. Wells. Wir sehen auch, dass nicht alle Wörter in allen drei Textgruppen vorkommen und dass es weniger Datenpunkte im Panel für Austen und H.G. Wells gibt.

Mithilfe eines Korrelationstests wollen wir herausfinden, wie ähnlich und wie unterschiedlich diese Worthäufigkeiten sind. Wie stark korrelieren die Worthäufigkeiten zwischen Austen und den Brontë-Schwestern sowie zwischen Austen und Wells?

cor.test(data = frequency[frequency$author == "Brontë Sisters",],
         ~ proportion + `Jane Austen`)
##
##  Pearson's product-moment correlation
##
## data:  proportion and Jane Austen
## t = 119.64, df = 10404, p-value < 2.2e-16
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
##  0.7527837 0.7689611
## sample estimates:
##       cor
## 0.7609907
cor.test(data = frequency[frequency$author == "H.G. Wells",],
         ~ proportion + `Jane Austen`)
##
##  Pearson's product-moment correlation
##
## data:  proportion and Jane Austen
## t = 36.441, df = 6053, p-value < 2.2e-16
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
##  0.4032820 0.4446006
## sample estimates:
##      cor
## 0.424162

Wie wir in den Plots gesehen haben, sind die Worthäufigkeiten zwischen den Romanen von Austen und Brontë stärker korreliert als zwischen Austen und H.G. Wells .

Zusammenfassung

In diesem Kapitel haben wir untersucht, was wir unter aufgeräumten Daten verstehen, wenn es um Text geht, und wie die Prinzipien von aufgeräumten Daten auf die Verarbeitung natürlicher Sprache angewendet werden können. Wenn ein Text in einem Format mit einem Token pro Zeile organisiert ist, sind Aufgaben wie das Entfernen von Stoppwörtern oder das Berechnen von Worthäufigkeiten natürliche Anwendungen von vertrauten Operationen innerhalb des Tidy-Tool-Ökosystems. Das Ein-Token-pro-Zeile-Konzept kann von einzelnen Wörtern auf n-Gramme und andere sinnvolle Texteinheiten sowie auf viele andere Analyseprioritäten ausgeweitet werden, die wir in diesem Buch behandeln werden.

Get Text Mining mit R now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.