scrape.mjs 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. import Path from 'node:path'
  2. import fs from 'node:fs'
  3. import {
  4. fetchString,
  5. fetchJson,
  6. RequestFailedError,
  7. } from '@overleaf/fetch-utils'
  8. import crypto from 'node:crypto'
  9. import { fileURLToPath } from 'node:url'
  10. const __dirname = Path.dirname(fileURLToPath(import.meta.url))
  11. const CACHE_IN = Path.join(
  12. Path.dirname(Path.dirname(Path.dirname(__dirname))),
  13. 'data',
  14. 'learnPages'
  15. )
  16. async function scrape(baseUrl, page) {
  17. const uri = new URL(baseUrl + '/learn-scripts/api.php')
  18. uri.search = new URLSearchParams({
  19. page,
  20. action: 'parse',
  21. format: 'json',
  22. redirects: true,
  23. }).toString()
  24. try {
  25. return await fetchString(uri)
  26. } catch (err) {
  27. if (err instanceof RequestFailedError) {
  28. console.error(err.response.status, page, err.response)
  29. } else {
  30. console.error(err)
  31. }
  32. }
  33. }
  34. function hash(blob) {
  35. return crypto.createHash('sha1').update(blob).digest('hex')
  36. }
  37. function getName(page) {
  38. let enc = encodeURIComponent(page)
  39. // There are VERY long titles in media wiki.
  40. // Add percent encoding and they exceed the filename size on my Ubuntu box.
  41. if (enc.length > 100) {
  42. enc = enc.slice(0, 100) + hash(page)
  43. }
  44. return enc
  45. }
  46. async function scrapeAndCachePage(baseUrl, page) {
  47. const path = Path.join(CACHE_IN, getName(page) + '.json')
  48. try {
  49. return JSON.parse(await fs.promises.readFile(path, 'utf-8'))
  50. } catch (e) {
  51. const blob = await scrape(baseUrl, page)
  52. const parsed = JSON.parse(blob).parse
  53. if (!parsed) {
  54. console.error(page, blob)
  55. throw new Error('bad contents')
  56. }
  57. await fs.promises.mkdir(CACHE_IN, { recursive: true })
  58. await fs.promises.writeFile(path, JSON.stringify(parsed, null, 2), 'utf-8')
  59. return parsed
  60. }
  61. }
  62. async function getAllPagesFrom(baseUrl, continueFrom) {
  63. // https://learn.overleaf.com/learn/Special:ApiSandbox#action=query&format=json&generator=allpages&gapfilterredir=nonredirects
  64. const uri = new URL(baseUrl + '/learn-scripts/api.php')
  65. uri.search = new URLSearchParams({
  66. action: 'query',
  67. format: 'json',
  68. generator: 'allpages',
  69. // Ignore pages with redirects. We do not want to check page content twice.
  70. gapfilterredir: 'nonredirects',
  71. // Bump the default page size of 10.
  72. gaplimit: 100,
  73. ...continueFrom,
  74. }).toString()
  75. let blob
  76. try {
  77. blob = await fetchJson(uri)
  78. } catch (err) {
  79. if (err instanceof RequestFailedError) {
  80. console.error(err.response.status, continueFrom, err.response)
  81. } else {
  82. console.error(err)
  83. throw err
  84. }
  85. }
  86. const nextContinueFrom = blob && blob.continue
  87. const pagesRaw = (blob && blob.query && blob.query.pages) || {}
  88. const pages = Object.values(pagesRaw).map(page => page.title)
  89. return { nextContinueFrom, pages }
  90. }
  91. async function getAllPages(baseUrl) {
  92. let continueFrom = {}
  93. let allPages = []
  94. while (true) {
  95. const { nextContinueFrom, pages } = await getAllPagesFrom(
  96. baseUrl,
  97. continueFrom
  98. )
  99. allPages = allPages.concat(pages)
  100. if (!nextContinueFrom) break
  101. continueFrom = nextContinueFrom
  102. }
  103. return allPages.sort()
  104. }
  105. async function getAllPagesAndCache(baseUrl) {
  106. const path = Path.join(CACHE_IN, 'allPages.txt')
  107. try {
  108. return JSON.parse(await fs.promises.readFile(path, 'utf-8'))
  109. } catch (e) {
  110. const allPages = await getAllPages(baseUrl)
  111. await fs.promises.mkdir(CACHE_IN, { recursive: true })
  112. await fs.promises.writeFile(path, JSON.stringify(allPages), 'utf-8')
  113. return allPages
  114. }
  115. }
  116. export default {
  117. getAllPagesAndCache,
  118. scrapeAndCachePage,
  119. }