Python爬蟲初學(三)—— 模擬登錄知乎

模擬登錄知乎

這幾天在研究模擬登錄, 以知乎 - 與世界分享你的知識、經驗和見解為例。實現過程遇到不少疑問,借鑒了知乎xchaoinfo的代碼,萬分感激!

知乎登錄分為郵箱登錄和手機登錄兩種方式,通過瀏覽器的開發者工具查看,我們通過不同方式登錄時,網址是不一樣的。郵箱登錄的地址email_url = 'https://www.zhihu.com/login/email',手機登錄網址是phone_url = 'http://www.zhihu.com/login/phone_num'

1. 建立一個可以傳Cookie的opener

Cookie用來跟蹤用戶是否已經登錄的狀態信息。一旦網站認證了我們的登錄,就會將cookie存到瀏覽器中,里面包含了服務器生成的令牌、登錄有效時長、狀態跟蹤信息。當登陸有效時長達到,我們的登錄狀態就被清空,想要訪問其他需要登錄后才能訪問的頁面也就不能成功了。意思就是我們能保持登錄狀態不掉線,是因為服務器端核對了我們訪問時攜帶的cookie,核對成功后服務器端就認為我們時“已經登錄了”的狀態。urllib.requst.urlopen()可傳入的參數中,都不能攜帶cookie信息。http.cookiejarFileCookieJar可以幫到我們,FileCookieJar可以將cookie存到本地文件中。 其中FileCookieJar的子類LWPCookieJar,可以存Set-Cookie3類型的文件,而MozillaCookieJar子類是存為.txt格式的文件。這里我使用LWPCookieJar

filename = 'cookie'
cookie = http.cookiejar.LWPCookieJar(filename)

這樣就建立了一個可以保存cookie的實例對象,它還有一個方法load()可以從本地加載已存的cookie數據,這樣我們就可以攜帶著cookie(相當于帶了一塊令牌)訪問服務器,服務器核對成功后,就可以訪問那些登錄后才能訪問的頁面。

try:
    cookie.load(ignore_discard=True)
except IOError:
    print('Cookie未加載!')

其中參數ignore_discard=True表示即使cookies將被丟棄也把它保存下來,它還有另外一個參數igonre_expires表示當前數據覆蓋(overwritten)原文件。

現在建立一個可以處理cookie的opener。

headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 '
                         '(KHTML, like Gecko) Chrome/57.0.2987.98 Safari/537.36',
           "Host": "www.zhihu.com",
           "Referer": "https://www.zhihu.com/",
           }
# 創建一個可以處理cookies的opener
opener = request.build_opener(request.HTTPCookieProcessor(cookie))
# 給openner添加headers, addheaders屬性接受元組而非字典
opener.addheaders = [(key, value) for key, value in headers.items()]

接下來我們可以使用opener.open()來傳入url和data了。

2. 獲取登錄所需關鍵參數

模擬登錄知乎,除了要POST自己的賬號密碼,還有兩個動態生成的參數,一個是_xsrf,還有一個就是討厭的驗證碼了。我們輸入賬號密碼后,通過開發者工具找到一個名為email的json文件,請求方法是POST,可以看到其域名是https://www.zhihu.com/login/email.我用火狐的開發者工具,點開這個文件看到里面的參數。

與chrome對應的好像是Headers下面的Form Data,在Network里面勾選preserve log就可以看到。

post的數據查看

這里就把本人郵箱和密碼的給打碼了哈,嘿嘿。

獲取_xsrf

這個參數是動態變化了,所以不能獲取一次后就一勞永逸。從html的body里面搜索下_xsrf,然后用正則表達式匹配出來就行。

def get_xsrf():
    """
    獲取參數_xsrf
    """
    req = opener.open('https://www.zhihu.com')
    html = req.read().decode('utf-8')
    get_xsrf_pattern = re.compile(r'<input type="hidden" name="_xsrf" value="(.*?)"')
    # 這里會返回多個值,不過都是一樣的內容,取第一個就行
    _xsrf = re.findall(get_xsrf_pattern, html)[0]
    return _xsrf

獲取驗證碼

知乎登錄一般需要填入驗證碼,如果沒有post這個參數過去,是不能成功登錄的。

知乎的驗證碼開始把我給坑了,在html內容的頁面里搜索能看到驗證碼圖片的網址,但是實際用xxx.read().decode('utf-8')獲取到的網頁內容是沒有這個網址的,它被隱藏了!好狡詐。研究無果只好搜索,從知乎上這個問題xchaoinfo的回答找到答案,結果是這個圖片網址中的一串數字就是時間戳

驗證碼的鏈接

時間戳是指格林威治時間1970年01月01日00時00分00秒(北京時間1970年01月01日08時00分00秒)起至現在的總秒數。

通過time.time()可以查看當前時間戳。比如我當前是1471771678.5400066,看是不是和上面紅框中的很接近!通過過換算關系t = time.time() * 1000可以得到圖片鏈接的關鍵部分,其余部分都是不變的。驗證碼的完整地址為captcha_url = 'http://www.zhihu.com/captcha.gif?r=' + t + "&type=login"。好了,鏈接知道了,下載下來查看并手動輸入就行了。再把這個post過去應該就可以登錄成功了,好激動。

def get_captcha():
    """
    獲取驗證碼本地顯示
    返回你輸入的驗證碼
    """
    t = str(int(time.time() * 1000))
    # 驗證碼完整網址
    captcha_url = 'http://www.zhihu.com/captcha.gif?r=' + t + "&type=login"
    # 下載驗證碼圖片,用下面注釋掉的方法一樣的效果
    request.urlretrieve(captcha_url, 'cptcha.gif')
    # image_data = request.urlopen(captcha_url).read()
    # with open('cptcha.gif', 'wb') as f:
    #     f.write(image_data)
    # 用Pillow庫顯示圖片,免去手動去文件夾打開的麻煩
    im = Image.open('cptcha.gif')
    im.show()
    captcha = input('本次登錄需要輸入驗證碼: ')
    return captcha

3. 嘗試模擬登錄知乎

關鍵的兩個東西我們都獲取到了,現在登錄一下試試吧!要將登錄方式考慮進去,如果檢測到用于輸入手機號,則我們應該訪問手機登錄網址;否則就是郵箱登錄。

def login(username, password):
    """
    輸入自己的賬號密碼,模擬登錄知乎
    """
    # 檢測到11位數字則是手機登錄
    if re.match(r'\d{11}$', username):
        url = 'http://www.zhihu.com/login/phone_num'
        data = {'_xsrf': get_xsrf(),
                'password': password,
                'remember_me': 'true',
                'phone_num': username
                }
    else:
        url = 'https://www.zhihu.com/login/email'
        data = {'_xsrf': get_xsrf(),
                'password': password,
                'remember_me': 'true',
                'email': username
                }

    # 若不用驗證碼,直接登錄
    post_data = parse.urlencode(data).encode('utf-8')
    r = opener.open(url, post_data)
    result = r.read().decode('utf-8')
    # 打印返回的響應,r = 1代表響應失敗,msg里是失敗的原因
    # 要用驗證碼,post后登錄
    if (json.loads(result))["r"] == 1:
        data['captcha'] = get_captcha()
        post_data = parse.urlencode(data).encode('utf-8')
        r = opener.open(url, post_data)
        result = r.read().decode('utf-8')
        print((json.loads(result))['msg'])
    # 保存cookie到本地
    cookie.save(ignore_discard=True, ignore_expires=True)

關于那句print((json.loads(result))['msg']) ,opener攜帶數據post過去,請求網址得到的響應會返回登錄信息,該數據是json類型,剛開始我一直是登錄失敗的,返回這樣的玩意兒!

{
"r": 1,
"errcode": 1991829,

"data": {"captcha":"\u9a8c\u8bc1\u7801\u9519\u8bef"},

"msg": "\u9a8c\u8bc1\u7801\u9519\u8bef"

}

r為1表示登錄失敗,0表示登錄成功。msg里反映的是登錄失敗的原因是”驗證碼會話失效“。

這里一定注意,登錄是一個連貫的過程。這個過程我們總共有三次訪問網址,一定保證包括獲取動態參數,獲取驗證碼、最終模擬登陸都使用同一個opener。這也是登錄失敗的原因之一,因為剛開始獲取_xsrf和驗證碼時用的是urlopen()urllib標準庫并不很強大,可以嘗試requests庫,會讓這個過程變得簡單。

看下結果吧。如果顯示登錄成功,可以訪問以下個人資料的網址,這個如果登錄失敗了是不能查看的,會返回到登錄界面的網址。

個人資料網頁

好了,這次模擬登錄感覺與前兩次學習比起來難度加深了,好多問題還得靠搜索才能解決。加油吧。

這里貼上全部代碼。

import re
from urllib import parse, request
import http.cookiejar
from PIL import Image
import time
import json

# 建立LWPCookieJar實例,可以存Set-Cookie3類型的文件。
# 而MozillaCookieJar類是存為'.txt'格式的文件
cookie = http.cookiejar.LWPCookieJar('cookie')
# 若本地有cookie則不用再post數據了
try:
    cookie.load(ignore_discard=True)
except IOError:
    print('Cookie未加載!')

headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 '
                         '(KHTML, like Gecko) Chrome/57.0.2987.98 Safari/537.36',
           "Host": "www.zhihu.com",
           "Referer": "https://www.zhihu.com/",
           }
opener = request.build_opener(request.HTTPCookieProcessor(cookie))
# 給openner添加headers, addheaders方法接受元組而非字典
opener.addheaders = [(key, value) for key, value in headers.items()]


def get_xsrf():
    """
    獲取參數_xsrf
    """
    response = opener.open('https://www.zhihu.com')
    html = response.read().decode('utf-8')
    get_xsrf_pattern = re.compile(r'<input type="hidden" name="_xsrf" value="(.*?)"')
    _xsrf = re.findall(get_xsrf_pattern, html)[0]
    return _xsrf


def get_captcha():
    """
    獲取驗證碼本地顯示
    返回你輸入的驗證碼
    """
    t = str(int(time.time() * 1000))
    captcha_url = 'http://www.zhihu.com/captcha.gif?r=' + t + "&type=login"
    # 獲取驗證碼也用同一個opener
    image_data = opener.open(captcha_url).read()
    with open('cptcha.gif', 'wb') as f:
        f.write(image_data)
    im = Image.open('cptcha.gif')
    im.show()
    captcha = input('本次登錄需要輸入驗證碼: ')
    return captcha


def login(username, password):
    """
    輸入自己的賬號密碼,模擬登錄知乎
    """
    # 檢測到11位數字則是手機登錄
    if re.match(r'\d{11}$', username):
        url = 'http://www.zhihu.com/login/phone_num'
        data = {'_xsrf': get_xsrf(),
                'password': password,
                'remember_me': 'true',
                'phone_num': username
                }
    else:
        url = 'https://www.zhihu.com/login/email'
        data = {'_xsrf': get_xsrf(),
                'password': password,
                'remember_me': 'true',
                'email': username
                }

    # 若不用驗證碼,直接登錄
    post_data = parse.urlencode(data).encode('utf-8')
    r = opener.open(url, post_data)
    result = r.read().decode('utf-8')
    # 打印返回的響應,r = 1代表響應失敗,msg里是失敗的原因
    # 要用驗證碼,post后登錄
    if (json.loads(result))["r"] == 1:
        data['captcha'] = get_captcha()
        post_data = parse.urlencode(data).encode('utf-8')
        r = opener.open(url, post_data)
        result = r.read().decode('utf-8')
        print((json.loads(result))['msg'])
    # 保存cookie到本地
    cookie.save(ignore_discard=True, ignore_expires=True)


def isLogin():
    # 通過查看用戶個人信息來判斷是否已經登錄
    url = 'https://www.zhihu.com/settings/profile'
    # 獲得真實網址,可能重定向了
    actual_url = opener.open(url).geturl()
    if actual_url == 'https://www.zhihu.com/settings/profile':
        return True
    else:
        return False


if __name__ == '__main__':
    if isLogin():
        print('您已經登錄')
    else:
        account = input('輸入賬號:')
        secret = input('輸入密碼:')
        login(account, secret)

更新: 使用requests庫重寫程序

Date:2016.8.23

今天翻看了下requests的文檔,學了點urllib庫再看這個不算很難。我用requests最基本的函數重新實現以上功能,當然大部分代碼是重復的。

requests.get()類似urllib.request.urlopen()。如其名是以get方式請求的,接收url,字典形式的headerstimeoutallow_redirects等參數,當然還有requests.post(),可以傳入data參數,不像urllib一樣需要對字典形式的data進行編碼,requests它會自動處理并且data可以傳入json數據。

allow_redirects這是參數可選TrueFalse,默認True,若選False則表示禁止重定向,按我的理解即禁止自動跳轉。

看下兩個庫的區別,返回來的response可選textcontent,其中text以文本形式返回,content以二進制數據形式返回,比如我們請求的網址是圖片,就返回content,便可以以wb方式寫入文件了。看下這兩個庫在實現返回網頁內容的區別。對了返回的對象如response還有一個屬性是status_code訪問成功了當然就返回的200啦。

# requests返回html內容
response = requests.get('https://www.zhihu.com', headers=headers)
    html = response.text
# urllib返回html內容
req = urllib.request.Request(url)
response = urllib.request.urlopen(req)
html = response.read().decode('utf-8')

然后就是requests.Session()或者requests.session(),兩種寫法都可以。類型都是一個class 'requests.sessions.Session',看下源碼其實session返回的就是一個Session對象。requests.Session()會新建一個會話,可以把同一用戶的不同請求聯系起來,直到會話結束都會自動處理cookies,這比urllib方便多了。如果只使用requests.get()或者requests.post()每次訪問網頁都是獨立進行的,并沒有把當前用戶的多次訪問關聯起來,故而模擬登錄需要用到requests.Session()。然后再用新建的session使用post()get()等函數。如下。

session = requests.Session()
session.get(url, headers)
session.post(url, headers, data)

這只是requests的冰山一角!它強大著呢。號稱是HTTP FOR HUMAN。不過掌握以上基本的東西,足夠重寫模擬登錄知乎的程序。進一步學習請看requests文檔,那里有詳細介紹。

再放requests版本的。

import re
import requests
import http.cookiejar
from PIL import Image
import time
import json

headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 '
                         '(KHTML, like Gecko) Chrome/57.0.2987.98 Safari/537.36',
           "Host": "www.zhihu.com",
           "Referer": "https://www.zhihu.com/",
           }
# 建立一個會話,可以把同一用戶的不同請求聯系起來;直到會話結束都會自動處理cookies
session = requests.Session()
# 建立LWPCookieJar實例,可以存Set-Cookie3類型的文件。
# 而MozillaCookieJar類是存為'/.txt'格式的文件
session.cookies = http.cookiejar.LWPCookieJar("cookie")
# 若本地有cookie則不用再post數據了
try:
    session.cookies.load(ignore_discard=True)
except IOError:
    print('Cookie未加載!')


def get_xsrf():
    """
    獲取參數_xsrf
    """
    response = session.get('https://www.zhihu.com', headers=headers)
    html = response.text
    get_xsrf_pattern = re.compile(r'<input type="hidden" name="_xsrf" value="(.*?)"')
    _xsrf = re.findall(get_xsrf_pattern, html)[0]
    return _xsrf


def get_captcha():
    """
    獲取驗證碼本地顯示
    返回你輸入的驗證碼
    """
    t = str(int(time.time() * 1000))
    captcha_url = 'http://www.zhihu.com/captcha.gif?r=' + t + "&type=login"
    response = session.get(captcha_url, headers=headers)
    with open('cptcha.gif', 'wb') as f:
        f.write(response.content)
    # Pillow顯示驗證碼
    im = Image.open('cptcha.gif')
    im.show()
    captcha = input('本次登錄需要輸入驗證碼: ')
    return captcha


def login(username, password):
    """
    輸入自己的賬號密碼,模擬登錄知乎
    """
    # 檢測到11位數字則是手機登錄
    if re.match(r'\d{11}$', username):
        url = 'http://www.zhihu.com/login/phone_num'
        data = {'_xsrf': get_xsrf(),
                'password': password,
                'remember_me': 'true',
                'phone_num': username
                }
    else:
        url = 'https://www.zhihu.com/login/email'
        data = {'_xsrf': get_xsrf(),
                'password': password,
                'remember_me': 'true',
                'email': username
                }
    # 若不用驗證碼,直接登錄
    result = session.post(url, data=data, headers=headers)
    # 打印返回的響應,r = 1代表響應失敗,msg里是失敗的原因
    # loads可以反序列化內置數據類型,而load可以從文件讀取
    if (json.loads(result.text))["r"] == 1:
        # 要用驗證碼,post后登錄
        data['captcha'] = get_captcha()
        result = session.post(url, data=data, headers=headers)
        print((json.loads(result.text))['msg'])
        # 保存cookie到本地
    session.cookies.save(ignore_discard=True, ignore_expires=True)


def isLogin():
    # 通過查看用戶個人信息來判斷是否已經登錄
    url = "https://www.zhihu.com/settings/profile"
    # 禁止重定向,否則登錄失敗重定向到首頁也是響應200
    login_code = session.get(url, headers=headers, allow_redirects=False).status_code
    if login_code == 200:
        return True
    else:
        return False


if __name__ == '__main__':
    if isLogin():
        print('您已經登錄')
    else:
        account = input('輸入賬號:')
        secret = input('輸入密碼:')
        login(account, secret)

以上使用到的瀏覽器UA都是電腦端的,現在知乎出了倒立文字驗證碼。如果因此而導致登錄失敗,可以將瀏覽器UA改成Android端或者IOS端。


by sunhaiyu

2016.8.21

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內容