用Python向部落格園釋出新文章
最近在開發一個部落格系統,經常把寫的東西放在自己網站的部落格上(之前寫在Onenote),然後我在部落格園也申請了一個部落格,就有了同樣一篇文章,我需要複製貼上排版分別提交兩次的情況。於是我就想能不能在我的網站內提交後直接把這篇文章同步提交至部落格園甚至是其他第三方部落格呢,所以花點時間實現了這個功能。本文寫的比較細,面向對這一塊瞭解不多的同學,大神就一笑置之吧。
一、分析HTTP請求
所有瀏覽器行為,本質都是向web伺服器發起http請求,伺服器收到請求後,根據請求內容返回結果,瀏覽器經過渲染後最終呈現給使用者。
登入部落格園,進入後臺,新建一篇隨筆,可以看到,編輯頁面的url為https://i.cnblogs.com/EditPosts.aspx?opt=1,把標題和內容隨便寫一寫,F12開啟Chrome控制檯,由於文章釋出後部落格園會有重定向,所以把Preserver log勾選上,這樣重新整理網頁歷史記錄也不會消失。
點擊發布按鈕,出來一大堆東西
第一個結果看名字推測是驗證使用者是否登入,我們直接點第二個結果:
發現這就是一個常規的POST請求,顯然這大概率是我們要找的目標,繼續看看它提交了什麼資料
除了圖片上的欄位,還有一段很長的欄位,欄位名為__VIEWSTATE
可以看到,除了__VIEWSTATE和__VIEWSTATEGENERATOR我們完全不知道是什麼之外,下面幾個欄位看名字就可以推測作用
我們先不管具體的作用,注意到POST請求的url和我們編輯文章的url是同一個地址,推測這裡直接使用form表單提交的可能性較大,回到頁面看看http結構
在頁面中確實找到了form表單,並且下面恰好就有一個隱藏input,就是我們剛才看到的__VIEWSTATE。
確定了是form表單後,事情就變得簡單了,找到並確認提交的欄位作用如下:
__VIEWSTATE:部落格園生成欄位
__VIEWSTATEGENERATOR:部落格園生成欄位
Editor$Edit$txbTitle:文章標題
Editor$Edit$EditorBody:文章內容
Editor$Edit$Advanced$txbTag:文章標籤
Editor$Edit$Advanced$txbExcerpt:文章摘要
Editor$Edit$Advanced$ckbPublished:on 文章是否釋出
Editor$Edit$Advanced$chkDisplayHomePage:on 顯示在我的部落格首頁
Editor$Edit$Advanced$chkComments:on 允許評論
Editor$Edit$Advanced$chkMainSyndication:on 顯示在RSS中
Editor$Edit$Advanced$chkPinned:on 置頂
Editor$Edit$Advanced$txbEntryName:友好地址名
Editor$Edit$Advanced$rblPostType:文章型別 (1-隨筆 2-文章 3-新聞 4-日記)
Editor$Edit$Advanced$tbEnryPassword:閱讀密碼
Editor$Edit$lkbPost:釋出
這些就是主要欄位,值得注意的是Editor$Edit$lkbPost的值,可以是“釋出”,也可以是“存為草稿”,功能就不言自明瞭
分析完提交文章的請求過程,再來看看部落格園的響應內容:
響應狀態碼為302,代表頁面重定向,重定向到localtion的地址,這裡地址有個值得注意點,就是postid=11510913,不出所料是新文章的id,後續可能會有用。
好了,說了這麼一圈,其實整個http請求異常簡單:
使用者使用POST方式向https://i.cnblogs.com/EditPosts.aspx?opt=1提交資料,如果成功會返回一個重定向的地址,這個地址包含了一個新文章的id。下面開始用程式碼來實現吧。
二、 模擬登入
雖然在分析HTTP請求的過程中一直沒有談到登入,但部落格園肯定是要在登入狀態下才能發文的,通常可以採用兩種方式來實現模擬使用者登入行為。
2.1 基於Cookie
何為Cookie?可以舉一個並不十分恰當的例子。我們去高鐵站坐高鐵,要經過取票、刷票進站這麼一個流程,閘機會通過驗證高鐵票的真偽、出行時間、人臉認證來判斷是否放行。在這個例子中,高鐵票就是Cookie,web伺服器首先在我們登入時給了我們一個Cookie(取票),然後我們下次訪問頁面時就會帶著這個Cookie一起提交請求(驗票),伺服器一看,哦這傢伙帶著我給它發的通行證,再一瞧通行證是不是假的,有沒有過期,驗證後都沒問題就可以知道是哪一個使用者在訪問它,進而給使用者提供相應的服務。
瞭解Cookie之後,我們就知道這是伺服器發的身份證,我們只要在訪問頁面時候把Cookie一起帶上,伺服器就會認為你已經登入了。那麼如何拿到Cookie呢,其實Cookie就在HTTP的請求頭裡面:
很長的一段,沒關係全部複製出來肯定不會錯。
下面開始我們的第一段程式碼
import requests def get_login_session(cookie): headers = { 'referer': 'https://i.cnblogs.com/', 'cookie': cookie } session = requests.session() session.headers.update(headers) return session
get_login_session方法接收一個cookie,返回一個session,其實session就是requests的另一層封裝,它會自動把你處理像Cookie呀一類的請求。我們在這個方法內給session傳遞了兩個請求頭,一個是cookie,另一個是referer,cookie就不用多說了,referer是由於不少網站會用這個欄位來判斷你是不是機器人,出於經驗主義我把它加上來了,但是如果不加是否有效,你們可以自行驗證一下。
如果對session甚至是requests還有疑問的同學,可以查閱官方文件http://2.python-requests.org/zh_CN/latest/user/advanced.html#advanced
2.2 賬號密碼登入
使用Cookie模擬登入,在程式碼層面來看確實十分簡單,但是對於普通使用者來說,他未必能夠理解Cookie並找到它,更多人能記住的僅僅是自己的賬號密碼,所以理應要有賬號密碼登入的功能。如果你理解了本文的第一部分,就會發現登入本質上還是一個POST請求,而且更簡單、提交的欄位更少。需要特別說明的一點是,部落格園有一個驗證機制,登入的時候大概率會彈出一個滑塊驗證碼,只有驗證通過後才會讓你登入。針對這個問題,以我的認知,requests目前是沒有辦法解決的,但真的要做,也不是全無辦法,我們可以採用selenium來實現模擬登入,過滑動驗證碼的方案百度上也有很多(本想貼我以前看過的一篇文章,無奈沒找到~),模擬拿到Cookie後即可,這裡我就不詳講了,如果大家確實感興趣,後續我在專門寫一篇過部落格園驗證碼的文章。
三、 requests構建HTTP請求
現在我們拿到登入後的session,要做的只是提交一篇新文章的POST請求,先上程式碼
from bs4 import BeautifulSoup def post_article(session,title,summary,content,**kwargs): ''' 向部落格園提交新文章 :param session:登入的session :param title: 文章標題 :param summary: 文章摘要 :param content: 文章內容 :param kwargs: 自定義form表單內容 :return: Response ''' url = 'https://i.cnblogs.com/EditPosts.aspx?opt=1' wb_data = session.get(url,allow_redirects=False) soup = BeautifulSoup(wb_data.txt,'lxml') __VIEWSTATE = soup.find(id='__VIEWSTATE')['value'] __VIEWSTATEGENERATOR = soup.find(id='__VIEWSTATEGENERATOR')['value'] data = {'Editor$Edit$lkbPost': '', 'Editor$Edit$Advanced$ckbPublished': 'on', 'Editor$Edit$Advanced$chkDisplayHomePage': 'on', 'Editor$Edit$Advanced$chkComments': 'on', 'Editor$Edit$Advanced$chkMainSyndication': 'on', 'Editor$Edit$Advanced$txbEntryName': '', 'Editor$Edit$Advanced$txbExcerpt': summary, 'Editor$Edit$Advanced$txbTag': '', 'Editor$Edit$Advanced$tbEnryPassword': '', '__VIEWSTATE': __VIEWSTATE, '__VIEWSTATEGENERATOR': __VIEWSTATEGENERATOR, 'Editor$Edit$txbTitle': title, 'Editor$Edit$EditorBody': content} data.update(kwargs) response = session.post(url,data=data,allow_redirects=False) return response
程式碼內的註釋應該很明白了,額外說幾點。第一點是由於__VIEWSTATE和__VIEWSSTATEGENERATOR欄位是部落格園生成的,所以我首先是用get請求,使用BeautifulSoup解析返回頁面並找到__VIEWSTATE和__VIEWSSTATEGENERATOR,然後再構建data進行post提交。第二個點是由於先前我們已經注意到,返回的是一個302重定向頁面,而requests是預設自動幫我們做重定向的,由於我們在後續的步驟中需要最原始的響應來幫助我們作判斷,所以我們使用allow_redirects=False禁用了重定向。最後一點是post_article方法還支援以鍵值對的方式傳遞任意引數,這些引數最終會更新到data並提交至部落格園,所以我們可以在呼叫方法時控制提交文章的一些選項,比如post_article(session,title,summary,content,Editor$Edit$Advanced$txbTag="Python")。
四、 獲取判斷返回結果
實際上,一般情況下呼叫post_article方法的後你的文章已經發布出去了,如果你想判斷是否真的成功了,那麼我們可以繼續。
在第一部分我們知道了如果釋出文章成功,那麼伺服器首先返回的是一個狀態碼為302的重定向頁面,如果釋出失敗了,比如當我發表標題重複的文章又或者觸碰了其他部落格園規則,這時候伺服器返回的就是一個狀態碼為200的普通頁面。所以我們可以根據返回物件的status或者Localtion來做一層判斷
location = response.headers.get('Location') if location: return True
五、其他
值得一提的是,部落格園文章的內容是基於html語言的,如果直接把普通文字提交到部落格園,那麼文章的排版肯定會十分混亂,所以對文章內容需要進行特別處理,由於我在寫的部落格系統,儲存的文章內容本身就是基於html語言的,所以我這也就沒有處理需求,在本文就不展開講了。
新建文章也不僅僅只有我列出的那一部分欄位,如果我沒有列出來的,可以在form表單下的input標籤。
五、完整示例
#!/usr/bin/env python # -*- coding:utf-8 -*- from bs4 import BeautifulSoup import requests def get_login_session(cookie): headers = { 'referer': 'https://i.cnblogs.com/', 'cookie': cookie } session = requests.session() session.headers.update(headers) return session def post_article(session,title,summary,content,**kwargs): ''' 向部落格園提交新文章 :param session:登入的session :param title: 文章標題 :param summary: 文章摘要 :param content: 文章內容 :param kwargs: 自定義form表單內容 :return: Response ''' url = 'https://i.cnblogs.com/EditPosts.aspx?opt=1' wb_data = session.get(url,allow_redirects=False) soup = BeautifulSoup(wb_data.text,'lxml') __VIEWSTATE = soup.find(id='__VIEWSTATE')['value'] __VIEWSTATEGENERATOR = soup.find(id='__VIEWSTATEGENERATOR')['value'] data = {'Editor$Edit$lkbPost': '', 'Editor$Edit$Advanced$ckbPublished': 'on', 'Editor$Edit$Advanced$chkDisplayHomePage': 'on', 'Editor$Edit$Advanced$chkComments': 'on', 'Editor$Edit$Advanced$chkMainSyndication': 'on', 'Editor$Edit$Advanced$txbEntryName': '', 'Editor$Edit$Advanced$txbExcerpt': summary, 'Editor$Edit$Advanced$txbTag': '', 'Editor$Edit$Advanced$tbEnryPassword': '', '__VIEWSTATE': __VIEWSTATE, '__VIEWSTATEGENERATOR': __VIEWSTATEGENERATOR, 'Editor$Edit$txbTitle': title, 'Editor$Edit$EditorBody': content} data.update(kwargs) response = session.post(url,data=data,allow_redirects=False) return response if __name__ == "__main__": cookie = input('請輸入部落格園Cookie: ') session = get_login_session(cookie) response = post_article(session,'測試標題','測試摘要','測試內容') location = r.headers.get('Location') if location: print('文章釋出成功') else: soup = BeautifulSoup(r.text, 'lxml') ErrorPanel = soup.find('div', {'class': 'ErrorPanel'}) if ErrorPanel: print(ErrorPanel.get_text()) print('文章釋出失敗')
&n