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 url
是 https://blog.csdn.net/DeepLies/article/details
,文章編輯頁(例如 https://mp.csdn.net/mdeditor/81511722
)的 base url
是 https://mp.csdn.net/mdeditor
,可見,只要拿到文章的 id
,就能獲得其詳情頁和編輯頁的地址,遍歷全部的翻頁,就能獲得全部的文章的 articleId
:post
// 前往每個翻頁
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正文,從標題框中得到文章的 標題。完美結束
不過,還有個問題,文章正文中除了通常的文字以外,還有圖片連接,按理說,這個圖片連接是網絡連接,直接引用是沒問題的,but
,CSDN
不只 給圖片加了水印,並且 還加了防盜鏈!
這是水印:
這是防盜鏈:
嗯,水印的事情我看了一圈,加上去容易可是弄下來就比較複雜了,能夠搞可是不太好搞,不是本文主要探究的事情 至於防盜鏈就更狠了,我試了下,這不只僅是加個 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
了
這裏須要注意的是,原先文章內的防盜鏈連接是用不了的,你把圖片下載到本地後,要把文章中對應的圖片連接換成本地的(固然,是用代碼換),不然你下載下來後文不對圖有什麼意義
這裏有幾個坑須要注意一下
若是你想使用文章本來的標題當作下載下來文章的文件名,那麼有些含有特殊字符的標題要特殊對待一下,由於某些特殊字符,例如 /:*?"<>|
,這些是不能做爲文件名保存的,這不是代碼的問題,是操做系統的問題,因此碰到這些字符就要額外處理一下:
const clearFilePath = filePath => {
// 我這裏把特殊字符統統換成 !
return filePath.replace(/[\/:*?"<>|]/g, '!')
}
複製代碼
使用 screenshot
方法截圖的時候,若是有的文章圖片一半在屏幕視野內,一半在屏幕視野外,就像這樣:
則對這種圖片進行截屏的時候,可能會出現把滾動條截進去的狀況,最後截圖是這樣的:
這其實很好理解,用眼睛來看的話,圖片原本就是被滾動條擋住的,截圖的時候滾動天然就要被一同截進來,不過下面一半眼睛是看不到,爲何也能被截進來,甚至那些徹底在視野外的也都能完整的進行截取?可能這種實現就是所謂的一半從實際狀況一半從需求方面考慮了吧。
先別管爲何這麼設計,但既然找到緣由,解決便可,這裏我採用的解決手段也很簡單,既然有滾動條,那我把滾動條隱藏不就好了:
document.body.style.overflow = 'hidden'
複製代碼
直接給 body
設個 overflow
的樣式,這樣就沒有滾動條了,並且前面也說了,屏幕外的圖片也能夠被截取,被屏幕 overflow:hidden
的元素也能夠截取,因此沒必要擔憂圖片也這個樣式給 hidden
掉了, 外國的東西就是好,人性化
上一條提到,使用 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)
複製代碼
contentArr
與 imgJuejinUrlArr
內的數據項,獲得上傳正文const juejinContent = contentArr.reduce((t, c, i) => {
return t.concat(c, imgJuejinUrlArr[i] || '')
}, []).join('')
複製代碼
原理就是上面那樣,代碼可能稍微有些複雜,須要調試的時間可能會有點長
當我準備用代碼實現本文功能的時候,我以爲或許有點複雜,一時半會應該不太能搞定,怎麼也得兩三天的時間吧 可是實在是沒想到,棘手程度遠超預期,利用週末和下班後那點時間,斷斷續續一直搞了一個多星期才最終搞定, 因爲對 puppeteer
不是很熟,掉了不少坑,又由於開始時沒預料到這個功能那麼複雜,因此對代碼結構沒什麼規劃, 直到寫到一半纔不得不把代碼重構了一遍,另外,因爲此功能時調用瀏覽器模擬對網站的實際操做, 還須要考慮到網速、超時、佔用資源等實際因素,都要一一解決。 本意是想解放勞動力,優雅而快速地搞定實際需求,誰知寫代碼花費的時間徹底足以我手工人肉把我在 csdn
上的文章下載下來並上傳到掘金好幾遍了!天殺的
另外,目前這個時間點,此項目可正常運行,可是爬蟲(姑且認爲我寫的是一個爬蟲吧)這個東西,特別是本文項目這種與網頁元素結構緊密聯繫的爬蟲,可能只是由於目標頁面換了個元素結構、換了個類名、換了個展現邏輯、換了個接口……就不能用了,因此若是碰到這種問題,請自行解決一下,大致處理邏輯是不變的,修正這種小問題就很簡單了。