將本身在CSDN上的文章下載到本地並上傳到掘金

CSDN 算是一個老牌技術網站了,不少喜歡寫文章的人,一開始都是在 CSDN上發佈,可是可能因爲某些緣由,有的人想把本身在 CSDN上的文章放到其餘的網站上(嗯,好比掘金),可是因爲在 CSDN上發佈的文章數量不少,一篇篇複製粘貼下來理論上是可行的,就是手痠了點。前端

不過,做爲技術型體力勞動者人才,重複一種動做幾十甚至上百遍未免有點丟失 biger,想起前段時間我花費了 大量時間 翻譯的 Puppeteer,至今還沒體現出其價值來,因而決定就用它了。git

本文的可運行示例代碼已經上傳到 github了,須要的請自取,順手 star哦~github


下載

想要獲取到文章的標題和內容信息,第一個想到的就是文章的詳情頁,標題就一行,沒那麼多道道還好說,可是內容就要複雜點了,若是直接分析內容元素的 DOM結構固然可行,但未免有點麻煩,若是直接獲取內容的字符串,例如使用 textContent這種方法,又會丟失語義,沒辦法得到內容的層級結構數組

不過我轉念一想,既然這文章是本身的,那麼徹底能夠進入文章的編輯頁啊,編輯框內的內容不就是文章的原始內容嗎,我寫文章都是用 md編輯器,那麼編輯框裏的內容就是 md源文件,正是我想要的東西。瀏覽器

想要進入後臺編輯頁,必需要登陸,因而先登陸, CSDN的登陸頁連接是 https://passport.csdn.net/account/login,登陸分爲帳號密碼登陸和第三方登陸,就按最簡單的來,因此我選擇了帳號密碼登陸,若是你以前一直是第三方登陸,沒有帳號密碼,那麼你能夠選擇如今去建立一個,或者換一種寫法,就用第三方登陸,我這裏演示帳號密碼登陸的流程 第三方登陸你本身想辦法

await page.goto('https://passport.csdn.net/account/login', { timeout })
// 切換到帳號密碼登陸
await page.click('.login-code__open')
await page.waitForSelector('#username')
await sleep()
// 在表單中輸入 帳號 密碼
await page.type('#username', 'Your Name')
await page.type('#password', 'Your pwd')
// 登陸
const loginBtn = await page.$('.logging')
// 點擊登陸按鈕
await loginBtn.click()
await page.waitForNavigation()
console.log('csdn登陸成功')
複製代碼

登陸成功後,先別急着去編輯頁,由於還沒肯定編輯頁連接,先把你本身全部發布的文章詳情頁的連接拿到,就拿我本身來舉慄吧。cookie

每一個人創做者都有這樣一個頁面,例如 blog.csdn.net/DeepLies,這個頁面羅列了你全部的文章:網絡

在這個頁面的最底部,有個能夠翻頁的區域:app

從這裏能夠知道本身的文章共能夠分爲多少個翻頁,同時也能夠獲得每一個翻頁的地址連接,例如,點擊下面的頁碼 2,就會去往 blog.csdn.net/DeepLies/ar…,這個連接前面部分是固定的,就叫它 base url 吧,只有最後那個數字會改變,這個數字就對應相應翻頁的地址編輯器

// 前往文章列表頁 第 1 頁,從第1頁獲取全部的翻頁信息
  await page.goto('https://blog.csdn.net/DeepLies/article/list/1', { timeout })
  // 獲取最底部的翻頁區域,獲取全部翻頁頁面
  await page.waitForSelector('#pageBox li')
  sleep()
  // 獲取全部頁碼 li內的文本,做用是根據這些文本中的數字找到最大頁碼
  const pageNumLiText = await page.$$eval('#pageBox li', eles => Array.from(eles).map(ele=>ele.textContent))
  // 從獲取到的頁碼中計算出最大頁碼,最大頁碼也就等於翻頁的總頁面數
  const maxPageNum = Math.max.apply(Math, pageNumLiText.reduce((total, d) => {
    if (!isNaN(Number(d))) return total.concat(+d)
    return total
  }, []))
  await browserManage.closeCtrlPage(page)

  // 翻頁的 共同 url字符串
  const baseUrl = 'https://blog.csdn.net/DeepLies/article/list/'
  // listPage 暫存了全部的翻頁連接
  let listPage = []
  for (let i = 1; i < maxPageNum + 1; i++) {
    listPage.push(baseUrl + i)
  }
複製代碼

因而,目前爲止,咱們能夠拿到全部的翻頁,而後每一個翻頁上有對應的文章列表,根據這個元素就能夠獲得文章的 articleId,每篇文章詳情頁和編輯頁的 base url都是同樣的,文章詳情頁(例如 https://blog.csdn.net/DeepLies/article/details/81511722)的)的 base urlhttps://blog.csdn.net/DeepLies/article/details,文章編輯頁(例如 https://mp.csdn.net/mdeditor/81511722)的 base urlhttps://mp.csdn.net/mdeditor,可見,只要拿到文章的 id,就能獲得其詳情頁和編輯頁的地址,遍歷全部的翻頁,就能獲得全部的文章的 articleIdpost

// 前往每個翻頁
await evPage.goto(url, { timeout })
await evPage.waitForSelector('.article-item-box')
// 獲取到當前翻頁的全部文章的id,這裏是經過截取文章詳情頁的地址字段獲得的 articleId
const pageDetailIds = await evPage.$$eval('.article-item-box h4 a', eles => eles.map(e => e.href.match(/\/(\d+)$/)[1]))
複製代碼

根據文章的 articleId獲得文章的編輯頁地址,就能進入編輯頁直接從編輯框中得到文章 md正文,從標題框中得到文章的 標題完美結束

不過,還有個問題,文章正文中除了通常的文字以外,還有圖片連接,按理說,這個圖片連接是網絡連接,直接引用是沒問題的,butCSDN 不只 給圖片加了水印,並且 還加了防盜鏈!

這是水印:

這是防盜鏈:

嗯,水印的事情我看了一圈,加上去容易可是弄下來就比較複雜了,能夠搞可是不太好搞,不是本文主要探究的事情 至於防盜鏈就更狠了,我試了下,這不只僅是加個 Referer或者帶個 Cookie就能解決的事情 不過這都不要緊,過程不重要,結果纔是想要的,我不是要引用這個圖片,我是要下載它,管它什麼水印、防盜鏈,物理攻擊一次性解決,絕招:截圖大法

// 對圖片進行截圖操做
await pageHandle.screenshot({ path: path.resolve(__dirname, `../article/${title}/${fileName}`), clip })
複製代碼

反正無論怎麼說,這圖片確定是能夠被看到的,既然能夠被看到,那就能夠被截圖,puppeteer 提供的 screenshot 真乃搞定防盜鏈圖片的利器也,之後你們有搞不定的防盜鏈圖片,就把它截下來,固然,因爲是截圖,因此若是是 gif格式的動圖,那就沒辦法了,你手動滑稽下載吧

至於水印,仔細看了下,根據CSDN給圖片加水印的方法,其實只要把圖片連接最後面 ?後面全部的參數所有去掉,留下來的連接就是沒有加水印的圖片連接 例如正常文章中的圖片連接是 https://img-blog.csdn.net/20171220155632166?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvRGVlcExpZXM=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast,而這個圖片的無水印版連接是 https://img-blog.csdn.net/20171220155632166,因此只須要將圖片後面的參數所有去掉,顯示在瀏覽器中的圖片就是無水印版本的圖片,而後再結合上面的截圖,就 get

這裏須要注意的是,原先文章內的防盜鏈連接是用不了的,你把圖片下載到本地後,要把文章中對應的圖片連接換成本地的(固然,是用代碼換),不然你下載下來後文不對圖有什麼意義

這裏有幾個坑須要注意一下

  • 1號坑

若是你想使用文章本來的標題當作下載下來文章的文件名,那麼有些含有特殊字符的標題要特殊對待一下,由於某些特殊字符,例如 /:*?"<>|,這些是不能做爲文件名保存的,這不是代碼的問題,是操做系統的問題,因此碰到這些字符就要額外處理一下:

const clearFilePath = filePath => {
  // 我這裏把特殊字符統統換成 !
  return filePath.replace(/[\/:*?"<>|]/g, '!')
}
複製代碼
  • 2號坑

使用 screenshot方法截圖的時候,若是有的文章圖片一半在屏幕視野內,一半在屏幕視野外,就像這樣:

則對這種圖片進行截屏的時候,可能會出現把滾動條截進去的狀況,最後截圖是這樣的:

這其實很好理解,用眼睛來看的話,圖片原本就是被滾動條擋住的,截圖的時候滾動天然就要被一同截進來,不過下面一半眼睛是看不到,爲何也能被截進來,甚至那些徹底在視野外的也都能完整的進行截取?可能這種實現就是所謂的一半從實際狀況一半從需求方面考慮了吧。

先別管爲何這麼設計,但既然找到緣由,解決便可,這裏我採用的解決手段也很簡單,既然有滾動條,那我把滾動條隱藏不就好了:

document.body.style.overflow = 'hidden'
複製代碼

直接給 body設個 overflow的樣式,這樣就沒有滾動條了,並且前面也說了,屏幕外的圖片也能夠被截取,被屏幕 overflow:hidden的元素也能夠截取,因此沒必要擔憂圖片也這個樣式給 hidden掉了, 外國的東西就是好,人性化

  • 3號坑

上一條提到,使用 screenshot這個方法截圖,大部分狀況下截圖的結果,和你手動開瀏覽器截圖是同樣的,那麼若是有什麼東西(沒錯,說的就是廣告)擋在了你所要截圖的圖片上面,那毫無疑問也會被截取下來,因此須要將廣告屏蔽掉,最簡單的方式就是 display: none

目前這個時間點,在我目前這個電腦上,只有一條廣告影響到了截圖,我就直接把它 display: none掉了,可是廣告這種東西變化莫測,說不定哪天就換了一個位置換了一個形式,那就須要使用者自行尋找並 display: none之了。

上傳

經過前面的步驟,就能夠將 CSDN上的文章下載到本地了,不過我還想給弄到掘金上,由於我總以爲前端的文章在掘金上曝光量更大,上傳這一步我決定換個方法,上面使用 puppeteer用得太累了,想要調接口直接把文章傳上去,然而,我發現掘金並不打算給我找個機會:想要使用上傳接口不只僅須要帶 cookie,還須要帶 token 什麼仇什麼怨

cookie好解決,可是 token這個東西很差弄,我深深地思考了下,考慮到前面用 puppeteer差很少也熟手了,仍是繼續用下去吧 puppeteer 真香

首先,仍是老規矩,想上傳確定是要登陸的,掘金登陸也分帳號密碼和第三方登陸兩種,這裏仍是用最簡單的帳號密碼登陸

await page.goto('https://juejin.im/', { timeout })
// 點擊 .login元素,把登陸彈窗弄出來
await page.click('.login')
await page.waitForSelector('.auth-form')
await sleep()
// 輸入帳號密碼
await page.type('input[name=loginPhoneOrEmail]', 'Your Name')
await page.type('input[name=loginPassword]', 'Your pwd')
const loginBtn = await page.$('.auth-form .btn')
// 點擊 登陸 按鈕
await loginBtn.click()
await page.waitForNavigation()
console.log('掘金登陸成功')
複製代碼

讀取須要上傳的文章,有兩種方法,一種是等前面下載完了,再讀取本地文件進行上傳;另一種就是一邊下載一邊上傳,直接利用下載時獲取的文章信息便可,這樣就少了讀取本地文件這一步,這裏採用後一種

這一步的難點在於,要從正文中獲取到全部的圖片連接(由於下載到本地了,因此這裏就是圖片的本地路徑),並把圖片連接對應的本地圖片文件進行上傳,而後再獲取到對應上傳後的網絡路徑,替換掉本地圖片路徑,就組成了一篇能夠直接上傳的正文

因爲不清楚一篇文章中到底有多少的圖片,也不清楚這些圖片都放在什麼地方,因此須要全文檢索,這裏採用的手段以下:

  • 將文章正文當作是一個長字符串,將正文中的每一個圖片連接(例如 ![img](http://example.com/img.png))都當作是這個長字符串的分隔符,這樣正文就被圖片連接分割成了 n
  • 根據上一步,將圖片連接和剩下的正文分開,分別存儲到兩個變量中,例如 var contentArr = [content1, content2, content3...]var imgArr = [pic1, pic2...],這裏把圖片連接當作是正文的分隔符,因此正文數組的長度確定是比圖片數組長度大 1的。
    const contentArr = content.split(/!\[.*?\]\(.*?\)/)
    const imgArr = []
    content.replace(/!\[.*?\]\((.+?)\)/g, (m1, m2) => imgArr.push(m2))
    複製代碼
  • 上傳 imgArr中的數據項,這些數據項目前都是本地圖片路徑,上傳後便可在掘金的編輯框內獲得相應的網絡路徑,獲得圖片網絡地址的數組 imgJuejinUrlArr
    const relativePath = `article/${articleTitle}/${fileBaseName}`
    const uploadInput = await page.$('.image-file-selector', { hidden: true })
    // 遮罩層出現,說明開始上傳
    await uploadInput.uploadFile(relativePath)
    複製代碼

  • 按照順序依次拼接 contentArrimgJuejinUrlArr 內的數據項,獲得上傳正文
    const juejinContent = contentArr.reduce((t, c, i) => {
      return t.concat(c, imgJuejinUrlArr[i] || '')
    }, []).join('')
    複製代碼
  • 上傳文章

原理就是上面那樣,代碼可能稍微有些複雜,須要調試的時間可能會有點長

總結

當我準備用代碼實現本文功能的時候,我以爲或許有點複雜,一時半會應該不太能搞定,怎麼也得兩三天的時間吧 可是實在是沒想到,棘手程度遠超預期,利用週末和下班後那點時間,斷斷續續一直搞了一個多星期才最終搞定, 因爲對 puppeteer不是很熟,掉了不少坑,又由於開始時沒預料到這個功能那麼複雜,因此對代碼結構沒什麼規劃, 直到寫到一半纔不得不把代碼重構了一遍,另外,因爲此功能時調用瀏覽器模擬對網站的實際操做, 還須要考慮到網速、超時、佔用資源等實際因素,都要一一解決。 本意是想解放勞動力,優雅而快速地搞定實際需求,誰知寫代碼花費的時間徹底足以我手工人肉把我在 csdn上的文章下載下來並上傳到掘金好幾遍了!天殺的

另外,目前這個時間點,此項目可正常運行,可是爬蟲(姑且認爲我寫的是一個爬蟲吧)這個東西,特別是本文項目這種與網頁元素結構緊密聯繫的爬蟲,可能只是由於目標頁面換了個元素結構、換了個類名、換了個展現邏輯、換了個接口……就不能用了,因此若是碰到這種問題,請自行解決一下,大致處理邏輯是不變的,修正這種小問題就很簡單了。

相關文章
相關標籤/搜索