scrape.js 3.4 KB

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