Reclaiming Kahoot quizzes
Kahoot's user-friendly web interface facilitates the creation of Kahoot quizzes. Furthermore, quizzes can be generated directly from an Excel file, rather than in csv format, but in xlsx format.
Typically, our teaching assistants create their quizzes through the web interface. However, one significant limitation of this approach is the difficulty in exporting quizzes. While Kahoot offers an API, it necessitates the purchase of additional tiers to access it. Recently, a teaching assistant contacted me, requesting the export of several quizzes. Our primary objective is to identify methods for exporting our content. It is important to note that automation offers boundless possibilities, and we aim to expedite the collection of our exports.
Intended Output Format
We need a CSV table output that can be directly imported into Kahoot's Excel template. The structure of the table is as follows:
| Question | Option 1 | Option 2 | Option 3 | Option 4 | Correct Answers | Time |
|---|---|---|---|---|---|---|
| Question 1 | Q1 o1 | Q1 o2 | Q1 o3 | Q1 o4 | 1,2,3,4 | 30 |
| Question 2 | Q2 o1 | 1 | 20 | |||
| Question 3 | Q3 o1 | Q3 o2 | Q3 o3 | 2 | 10 |
Extracting Data from Webpage Content Utilizing XPath
Upon examining the source code of Kahoot's web interface, we observe the absence of predefined ID values or class names. This is likely due to the utilization of a build tool. Nevertheless, the content structure remains consistent, unless a new version of the interface is then deployed. Consequently, we can leverage XPath (W3C spec) for traversing the XML document.
XPath serves as a method for expressing specific tags within an XML file. Given that HTML is also an XML format, we can directly apply XPath values to our desired content elements.
Locating an HTML element containing our text in modern browsers is straightforward: Right-click > Inspect element
Once the HTML element is identified, we can extract its XPath value from the Web Inspector: Right-click > Copy > XPath
The document.evaluate method evaluates a provided XPath expression. Although it is a complex method, we will only utilize a few parameters:
document.evaluate(
xpathExpression,
contextNode,
namespaceResolver,
resultType,
result,
);
// The use of xpathExpression and resultType is enough for our case:
document.evaluate(xpathExpression, document, null, resultType, null);
Upon locating Kahoot Library and selecting the Kahoot game we wish to export, we can observe a page containing all the questions in the game. From this page, we can expand all the questions by clicking the button on top right to expand all answers. We have the option to copy and paste all the questions from this page or we can gather all the information using scripting with XPath, resulting in a csv-like output.
First, we need to determine the number of questions, which corresponds to the count of <li> tags:
const questionCount = document.evaluate(
'count(//*[@id="main-content-container"]/div[3]/div[2]/div[2]/div/section[1]/ul/li)',
document,
null,
XPathResult.NUMBER_TYPE,
null,
).numberValue;
For every iteration of a question, we obtain a question text, a maximum of four answers, a list of correct answers, and a time limit. If we inspect the XPath values of subsequent question texts, we can observe that they can only differ by an order index value. We denote each questions with index variable i:
const questionText = document.evaluate(
`//*[@id="main-content-container"]/div[3]/div[2]/div[2]/div/section[1]/ul/li[${i}]/div/div[1]/button/span`,
document,
null,
XPathResult.STRING_TYPE,
null,
).stringValue;
Kahoot's import template supports a maximum of four answer options, which is our current limitation. For questions with more than four options, this tutorial can be further extended. Kahoot allows questions to have multiple answers, so we need to check the truth values of each answer option.
let answers = [];
let correctAnswers = [];
for (let j = 1; j <= 4; j++) {
console.info(`Processing answer ${j} of ${4}`);
let answer = document.evaluate(
`//*[@id="main-content-container"]/div[3]/div[2]/div[2]/div/section[1]/ul/li[${i}]/div/div[2]/div[${j}]/div/div[1]/span`,
document,
null,
XPathResult.STRING_TYPE,
null,
).stringValue;
answers.push(answer);
if (answer !== "") {
let answerColor = document
.evaluate(
`//*[@id="main-content-container"]/div[3]/div[2]/div[2]/div/section[1]/ul/li[${i}]/div/div[2]/div[${j}]/div/div[2]/span`,
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null,
)
.singleNodeValue.querySelector("svg path")
.getAttribute("style");
if (answerColor.includes("fill: rgb(38, 137, 12);")) {
correctAnswers.push(j);
}
}
}
In this loop, we iterated all available options for a question based on its SVG image. If the SVG image contains a green fill, the corresponding question option is designated as true.
Finally, we obtain the time value from the existing page, similar to the previous retrievals:
let time = parseInt(
document.evaluate(
`//*[@id="main-content-container"]/div[3]/div[2]/div[2]/div/section[1]/ul/li[${i}]/div/div[1]/div/div/div[2]/span`,
document,
null,
XPathResult.STRING_TYPE,
null,
).stringValue,
10,
);
Once we have questionText, answers, time, and correctAnswers prepared for a question, we combine them into tab-separated values and append them to the questions array.
let line = `${questionText}\t${answers.join(
"\t",
)}\t${time}\t${correctAnswers.join(",")}`;
questions.push(line);
Once the questions array is prepared, we can print it to the console for copying and pasting into Kahoot's Excel template file:
console.info("\n" + questions.join("\n"));
TL;DR
As a concluding remark, please find below the complete code that you can copy and paste into your console:
// Use it when you see all questions with all answers on the screen.
let questionCount = document.evaluate(
'count(//*[@id="main-content-container"]/div[3]/div[2]/div[2]/div/section[1]/ul/li)',
document,
null,
XPathResult.NUMBER_TYPE,
null,
).numberValue;
let questions = [];
for (let i = 1; i <= questionCount; i++) {
console.info(`Processing questionText ${i} of ${questionCount}`);
let questionText = document.evaluate(
`//*[@id="main-content-container"]/div[3]/div[2]/div[2]/div/section[1]/ul/li[${i}]/div/div[1]/button/span`,
document,
null,
XPathResult.STRING_TYPE,
null,
).stringValue;
let answers = [];
let correctAnswers = [];
for (let j = 1; j <= 4; j++) {
console.info(`Processing answer ${j} of ${4}`);
let answer = document.evaluate(
`//*[@id="main-content-container"]/div[3]/div[2]/div[2]/div/section[1]/ul/li[${i}]/div/div[2]/div[${j}]/div/div[1]/span`,
document,
null,
XPathResult.STRING_TYPE,
null,
).stringValue;
answers.push(answer);
if (answer !== "") {
let answerColor = document
.evaluate(
`//*[@id="main-content-container"]/div[3]/div[2]/div[2]/div/section[1]/ul/li[${i}]/div/div[2]/div[${j}]/div/div[2]/span`,
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null,
)
.singleNodeValue.querySelector("svg path")
.getAttribute("style");
if (answerColor.includes("fill: rgb(38, 137, 12);")) {
correctAnswers.push(j);
}
}
}
let time = parseInt(
document.evaluate(
`//*[@id="main-content-container"]/div[3]/div[2]/div[2]/div/section[1]/ul/li[${i}]/div/div[1]/div/div/div[2]/span`,
document,
null,
XPathResult.STRING_TYPE,
null,
).stringValue,
10,
);
let line = `${questionText}\t${answers.join(
"\t",
)}\t${time}\t${correctAnswers.join(",")}`;
questions.push(line);
}
console.info("\n" + questions.join("\n"));