最近在開發一個博客系統,經常把寫的東西放在自己網站的博客上(之前寫在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('文章發布失敗')