scrape.js 3.2 KB

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