Bevor wir unser Skript schreiben, überprüfen wir, ob die Installation von Puppeteer erfolgreich war:
import puppeteer from 'puppeteer';
async function scrapeTwitterData(twitter_url: string): Promise<void> {
// Launch Puppeteer
const browser = await puppeteer.launch({
headless: false,
args: ['--start-maximized'],
defaultViewport: null
})
// Create a new page
const page = await browser.newPage()
// Navigate to the target URL
await page.goto(twitter_url)
// Close the browser
await browser.close()
}
scrapeTwitterData("https://twitter.com/netflix")
Hier öffnen wir ein Browserfenster, erstellen eine neue Seite, navigieren zu unserer Ziel-URL und schließen dann den Browser. Der Einfachheit und der visuellen Fehlerbehebung halber öffne ich das Browserfenster im maximierten Modus im Nicht-Headless-Modus.
Schauen wir uns nun die Struktur der Website an und extrahieren wir die oben genannte Liste von Daten schrittweise:
Auf den ersten Blick ist Ihnen vielleicht aufgefallen, dass die Struktur der Website ziemlich komplex ist. Die Klassennamen werden zufällig generiert und nur sehr wenige HTML-Elemente sind eindeutig identifiziert.
Zu unserem Glück finden wir beim Durchsuchen der übergeordneten Elemente der Ziel-Daten das Attribut „data-testid“. Eine schnelle Suche im HTML-Dokument bestätigt, dass dieses Attribut das von uns angestrebte Element eindeutig identifiziert.
Um den Profilnamen und den Handle zu extrahieren, extrahieren wir daher das „div“-Element, bei dem das Attribut „data-testid“ auf „UserName“ gesetzt ist. Der Code sieht wie folgt aus:
// Extract the profile name and handle
const profileNameHandle = await page.evaluate(() => {
const nameHandle = document.querySelector('div[data-testid="UserName"]')
return nameHandle ? nameHandle.textContent : ""
})
const profileNameHandleComponents = profileNameHandle.split('@')
console.log("Profile name:", profileNameHandleComponents[0])
console.log("Profile handle:", '@' + profileNameHandleComponents[1])
Da sowohl der Profilname als auch der Profil-Handle denselben übergeordneten Element haben, erscheint das Endergebnis verkettet. Um dies zu beheben, verwenden wir die „split“-Methode, um die Daten zu trennen.
Anschließend wenden wir dieselbe Logik an, um die Biografie des Profils zu extrahieren. In diesem Fall lautet der Wert für das Attribut „data-testid“ „UserDescription“:
// Extract the user bio
const profileBio = await page.evaluate(() => {
const location = document.querySelector('div[data-testid="UserDescription"]')
return location ? location.textContent : ""
})
console.log("User bio:", profileBio)
Das Endergebnis wird durch die Eigenschaft „textContent“ des HTML-Elements beschrieben.
Wenn wir zum nächsten Abschnitt der Profildaten übergehen, finden wir den Standort, die Website und das Beitrittsdatum unter derselben Struktur.
// Extract the user location
const profileLocation = await page.evaluate(() => {
const location = document.querySelector('span[data-testid="UserLocation"]')
return location ? location.textContent : ""
})
console.log("User location:", profileLocation)
// Extract the user website
const profileWebsite = await page.evaluate(() => {
const location = document.querySelector('a[data-testid="UserUrl"]')
return location ? location.textContent : ""
})
console.log("User website:", profileWebsite)
// Extract the join date
const profileJoinDate = await page.evaluate(() => {
const location = document.querySelector('span[data-testid="UserJoinDate"]')
return location ? location.textContent : ""
})
console.log("User join date:", profileJoinDate)
Um die Anzahl der Follower und der Personen, denen gefolgt wird, zu ermitteln, benötigen wir einen etwas anderen Ansatz. Sehen Sie sich den folgenden Screenshot an:
Es gibt kein „data-testid“-Attribut und die Klassennamen werden weiterhin zufällig generiert. Eine Lösung wäre, die Anker-Elemente anzusprechen, da diese ein eindeutiges „href“-Attribut bieten.
// Extract the following count
const profileFollowing = await page.evaluate(() => {
const location = document.querySelector('a[href$="/following"]')
return location ? location.textContent : ""
})
console.log("User following:", profileFollowing)
// Extract the followers count
const profileFollowers = await page.evaluate(() => {
const location = document.querySelector('a[href$="/followers"]')
return location ? location.textContent : ""
})
console.log("User followers:", profileFollowers)
Um den Code für jedes Twitter-Profil nutzbar zu machen, haben wir den CSS-Selektor so definiert, dass er die Ankerelemente anspricht, deren „href“-Attribut jeweils mit „/following“ oder „/followers“ endet.
Wenn wir uns nun der Liste der Tweets zuwenden, können wir jeden einzelnen wieder leicht anhand des Attributs „data-testid“ identifizieren, wie unten hervorgehoben:
Der Code unterscheidet sich nicht von dem, was wir bis zu diesem Punkt gemacht haben, mit der Ausnahme, dass wir die Methode „querySelectorAll“ verwenden und das Ergebnis in ein JavaScript-Array konvertieren:
// Extract the user tweets
const userTweets = await page.evaluate(() => {
const tweets = document.querySelectorAll('article[data-testid="tweet"]')
const tweetsArray = Array.from(tweets)
return tweetsArray
})
console.log("User tweets:", userTweets)
Auch wenn der CSS-Selektor zweifellos korrekt ist, ist Ihnen vielleicht aufgefallen, dass die resultierende Liste fast immer leer ist. Das liegt daran, dass die Tweets erst einige Sekunden nach dem Laden der Seite geladen werden.
Die einfache Lösung für dieses Problem besteht darin, eine zusätzliche Wartezeit einzufügen, nachdem wir zur Ziel-URL navigiert sind. Eine Möglichkeit ist, mit einer festen Anzahl von Sekunden zu experimentieren, eine andere besteht darin, zu warten, bis ein bestimmter CSS-Selektor im DOM erscheint:
await page.waitForSelector('div[aria-label^="Timeline: "]')
Hier weisen wir unser Skript also an, zu warten, bis ein „div“-Element, dessen „aria-label“-Attribut mit „Timeline:“ beginnt, auf der Seite sichtbar ist. Und nun sollte der vorherige Codeausschnitt einwandfrei funktionieren.
Weiter geht es: Wir können die Daten zum Autor des Tweets wie zuvor mithilfe des Attributs „data-testid“ ermitteln.
Im Algorithmus durchlaufen wir die Liste der HTML-Elemente und wenden auf jedes einzelne die „querySelector“-Methode an. Auf diese Weise können wir besser sicherstellen, dass die von uns verwendeten Selektoren eindeutig sind, da der Zielbereich viel kleiner ist.
// Extract the user tweets
const userTweets = await page.evaluate(() => {
const tweets = document.querySelectorAll('article[data-testid="tweet"]')
const tweetsArray = Array.from(tweets)
return tweetsArray.map(t => {
const authorData = t.querySelector('div[data-testid="User-Names"]')
const authorDataText = authorData ? authorData.textContent : ""
const authorComponents = authorDataText.split('@')
const authorComponents2 = authorComponents[1].split('·')
return {
authorName: authorComponents[0],
authorHandle: '@' + authorComponents2[0],
date: authorComponents2[1],
}
})
})
console.log("User tweets:", userTweets)
Die Daten zum Autor werden auch hier verkettet angezeigt. Um sicherzustellen, dass das Ergebnis sinnvoll ist, wenden wir auf jeden Abschnitt die Methode „split“ an.
Der Textinhalt des Tweets ist ziemlich einfach:
const tweetText = t.querySelector('div[data-testid="tweetText"]')
Für die Fotos des Tweets extrahieren wir eine Liste von „img“-Elementen, deren übergeordnete Elemente „div“-Elemente sind, bei denen das Attribut „data-testid“ auf „tweetPhoto“ gesetzt ist. Das Endergebnis ist das „src“-Attribut dieser Elemente.
const tweetPhotos = t.querySelectorAll('div[data-testid="tweetPhoto"] > img')
const tweetPhotosArray = Array.from(tweetPhotos)
const photos = tweetPhotosArray.map(p => p.getAttribute('src'))
Und schließlich der Statistikabschnitt des Tweets. Die Anzahl der Antworten, Retweets und Likes ist auf die gleiche Weise zugänglich, nämlich über den Wert des „aria-label“-Attributs, nachdem wir das Element mit dem „data-testid“-Attribut identifiziert haben.
Um die Anzahl der Aufrufe zu ermitteln, zielen wir auf das Anker-Element ab, dessen „aria-label“-Attribut mit der Zeichenfolge „Views. View Tweet analytics“ endet.
const replies = t.querySelector('div[data-testid="reply"]')
const repliesText = replies ? replies.getAttribute("aria-label") : ''
const retweets = t.querySelector('div[data-testid="retweet"]')
const retweetsText = retweets ? retweets.getAttribute("aria-label") : ''
const likes = t.querySelector('div[data-testid="like"]')
const likesText = likes ? likes.getAttribute("aria-label") : ''
const views = t.querySelector('a[aria-label$="Views. View Tweet analytics"]')
const viewsText = views ? views.getAttribute("aria-label") : ''
Da das Endergebnis auch Zeichen enthält, verwenden wir die „split“-Methode, um nur den numerischen Wert zu extrahieren und zurückzugeben. Der vollständige Codeausschnitt zum Extrahieren von Tweet-Daten ist unten dargestellt:
// Extract the user tweets
const userTweets = await page.evaluate(() => {
const tweets = document.querySelectorAll('article[data-testid="tweet"]')
const tweetsArray = Array.from(tweets)
return tweetsArray.map(t => {
// Extract the tweet author, handle, and date
const authorData = t.querySelector('div[data-testid="User-Names"]')
const authorDataText = authorData ? authorData.textContent : ""
const authorComponents = authorDataText.split('@')
const authorComponents2 = authorComponents[1].split('·')
// Extract the tweet content
const tweetText = t.querySelector('div[data-testid="tweetText"]')
// Extract the tweet photos
const tweetPhotos = t.querySelectorAll('div[data-testid="tweetPhoto"] > img')
const tweetPhotosArray = Array.from(tweetPhotos)
const photos = tweetPhotosArray.map(p => p.getAttribute('src'))
// Extract the tweet reply count
const replies = t.querySelector('div[data-testid="reply"]')
const repliesText = replies ? replies.getAttribute("aria-label") : ''
// Extract the tweet retweet count
const retweets = t.querySelector('div[data-testid="retweet"]')
const retweetsText = retweets ? retweets.getAttribute("aria-label") : ''
// Extract the tweet like count
const likes = t.querySelector('div[data-testid="like"]')
const likesText = likes ? likes.getAttribute("aria-label") : ''
// Extract the tweet view count
const views = t.querySelector('a[aria-label$="Views. View Tweet analytics"]')
const viewsText = views ? views.getAttribute("aria-label") : ''
return {
authorName: authorComponents[0],
authorHandle: '@' + authorComponents2[0],
date: authorComponents2[1],
text: tweetText ? tweetText.textContent : '',
media: photos,
replies: repliesText.split(' ')[0],
retweets: retweetsText.split(' ')[0],
likes: likesText.split(' ')[0],
views: viewsText.split(' ')[0],
}
})
})
console.log("User tweets:", userTweets)
Nach Ausführung des gesamten Skripts sollte Ihr Terminal etwa Folgendes anzeigen:
Profile name: Netflix
Profile handle: @netflix
User bio:
User location: California, USA
User website: netflix.com/ChangePlan
User join date: Joined October 2008
User following: 2,222 Following
User followers: 21.3M Followers
User tweets: [
{
authorName: 'best of the haunting',
authorHandle: '@bestoffhaunting',
date: '16 Jan',
text: 'the haunting of hill house.',
media: [
'https://pbs.twimg.com/media/FmnGkCNWABoEsJE?format=jpg&name=360x360',
'https://pbs.twimg.com/media/FmnGkk0WABQdHKs?format=jpg&name=360x360',
'https://pbs.twimg.com/media/FmnGlTOWABAQBLb?format=jpg&name=360x360',
'https://pbs.twimg.com/media/FmnGlw6WABIKatX?format=jpg&name=360x360'
],
replies: '607',
retweets: '37398',
likes: '170993',
views: ''
},
{
authorName: 'Netflix',
authorHandle: '@netflix',
date: '9h',
text: 'The Glory Part 2 premieres March 10 -- FIRST LOOK:',
media: [
'https://pbs.twimg.com/media/FmuPlBYagAI6bMF?format=jpg&name=360x360',
'https://pbs.twimg.com/media/FmuPlBWaEAIfKCN?format=jpg&name=360x360',
'https://pbs.twimg.com/media/FmuPlBUagAETi2Z?format=jpg&name=360x360',
'https://pbs.twimg.com/media/FmuPlBZaEAIsJM6?format=jpg&name=360x360'
],
replies: '250',
retweets: '4440',
likes: '9405',
views: '656347'
},
{
authorName: 'Kurtwood Smith',
authorHandle: '@tahitismith',
date: '14h',
text: 'Two day countdown...more stills from the show to hold you over...#That90sShow on @netflix',
media: [
'https://pbs.twimg.com/media/FmtOZTGaEAAr2DF?format=jpg&name=360x360',
'https://pbs.twimg.com/media/FmtOZTFaUAI3QOR?format=jpg&name=360x360',
'https://pbs.twimg.com/media/FmtOZTGaAAEza6i?format=jpg&name=360x360',
'https://pbs.twimg.com/media/FmtOZTGaYAEo-Yu?format=jpg&name=360x360'
],
replies: '66',
retweets: '278',
likes: '3067',
views: ''
},
{
authorName: 'Netflix',
authorHandle: '@netflix',
date: '12h',
text: 'In 2013, Kai the Hatchet-Wielding Hitchhiker became an internet sensation -- but that viral fame put his questionable past squarely on the radar of authorities. \n' +
'\n' +
'The Hatchet Wielding Hitchhiker is now on Netflix.',
media: [],
replies: '169',
retweets: '119',
likes: '871',
views: '491570'
}
]