Avant d'écrire notre script, vérifions que l'installation de Puppeteer s'est bien déroulée :
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")
Ici, nous ouvrons une fenêtre de navigateur, créons une nouvelle page, accédons à notre URL cible, puis fermons le navigateur. Par souci de simplicité et pour faciliter le débogage visuel, j'ouvre la fenêtre de navigateur en mode non headless et en plein écran.
Examinons maintenant la structure du site web et extrayons progressivement la liste de données précédente :
Au premier coup d’œil, vous avez peut-être remarqué que la structure du site web est assez complexe. Les noms de classes sont générés de manière aléatoire et très peu d’éléments HTML sont identifiés de manière unique.
Heureusement pour nous, en parcourant les éléments parents des données ciblées, nous trouvons l’attribut « data-testid ». Une recherche rapide dans le document HTML permet de confirmer que cet attribut identifie de manière unique l’élément que nous ciblons.
Par conséquent, pour extraire le nom et le pseudonyme du profil, nous allons extraire l’élément « div » dont l’attribut « data-testid » est défini sur « UserName ». Le code ressemblera à ceci :
// 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])
Comme le nom et l'identifiant du profil ont le même parent, le résultat final apparaîtra concaténé. Pour y remédier, nous utilisons la méthode « split » pour séparer les données.
Nous appliquons ensuite la même logique pour extraire la biographie du profil. Dans ce cas, la valeur de l'attribut « data-testid » est « 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)
Le résultat final est décrit par la propriété « textContent » de l'élément HTML.
En passant à la section suivante des données du profil, nous trouvons la localisation, le site web et la date d’inscription sous la même structure.
// 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)
Pour obtenir le nombre d'abonnés et d'abonnements, nous devons adopter une approche légèrement différente. Consultez la capture d'écran ci-dessous :
Il n'y a pas d'attribut « data-testid » et les noms de classe sont toujours générés de manière aléatoire. Une solution serait de cibler les éléments d'ancrage, car ils fournissent un attribut « href » unique.
// 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)
Pour que le code fonctionne avec n'importe quel profil Twitter, nous avons défini le sélecteur CSS de manière à cibler les éléments d'ancrage dont l'attribut « href » se termine respectivement par « /following » ou « /followers ».
Passons maintenant à la liste des tweets : nous pouvons à nouveau identifier facilement chacun d'entre eux à l'aide de l'attribut « data-testid », comme indiqué ci-dessous :
Le code est identique à ce que nous avons fait jusqu'à présent, à l'exception de l'utilisation de la méthode « querySelectorAll » et de la conversion du résultat en un tableau JavaScript :
// 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)
Cependant, même si le sélecteur CSS est sans aucun doute correct, vous avez peut-être remarqué que la liste obtenue est presque toujours vide. Cela s’explique par le fait que les tweets sont chargés quelques secondes après le chargement de la page.
La solution simple à ce problème consiste à ajouter un temps d'attente supplémentaire après avoir accédé à l'URL cible. Une option consiste à tester différentes durées fixes en secondes, tandis qu'une autre consiste à attendre qu'un sélecteur CSS spécifique apparaisse dans le DOM :
await page.waitForSelector('div[aria-label^="Timeline: "]')
Ici, nous demandons donc à notre script d'attendre qu'un élément « div » dont l'attribut « aria-label » commence par « Timeline: » soit visible sur la page. Le snippet précédent devrait désormais fonctionner parfaitement.
Pour continuer, nous pouvons identifier les données concernant l’auteur du tweet comme précédemment, en utilisant l’attribut « data-testid ».
Dans l'algorithme, nous allons parcourir la liste des éléments HTML et appliquer la méthode « querySelector » à chacun d'entre eux. De cette façon, nous pouvons mieux nous assurer que les sélecteurs que nous utilisons sont uniques, car le champ d'application ciblé est beaucoup plus restreint.
// 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)
Les données concernant l'auteur apparaîtront également concaténées ici ; pour nous assurer que le résultat a du sens, nous appliquons la méthode « split » à chaque section.
Le contenu textuel du tweet est assez simple :
const tweetText = t.querySelector('div[data-testid="tweetText"]')
Pour les photos du tweet, nous allons extraire une liste d’éléments « img », dont les éléments parents sont des éléments « div » dont l’attribut « data-testid » est défini sur « tweetPhoto ». Le résultat final sera l’attribut « src » de ces éléments.
const tweetPhotos = t.querySelectorAll('div[data-testid="tweetPhoto"] > img')
const tweetPhotosArray = Array.from(tweetPhotos)
const photos = tweetPhotosArray.map(p => p.getAttribute('src'))
Et enfin, la section des statistiques du tweet. Le nombre de réponses, de retweets et de mentions « J'aime » est accessible de la même manière, via la valeur de l'attribut « aria-label », après avoir identifié l'élément grâce à l'attribut « data-testid ».
Pour obtenir le nombre de vues, nous ciblons l'élément d'ancrage dont l'attribut « aria-label » se termine par la chaîne « Views. View Tweet analytics ».
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") : ''
Comme le résultat final contiendra également des caractères, nous utilisons la méthode « split » pour extraire et renvoyer uniquement la valeur numérique. L'extrait de code complet permettant d'extraire les données des tweets est présenté ci-dessous :
// 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)
Une fois le script exécuté, votre terminal devrait afficher quelque chose comme ceci :
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'
}
]