Scrapy 抓取起點中文網(上):實現登錄和認證
本小節將完成一個有趣的 Scrapy 爬蟲項目,主要實現的主要功能如下:
- 實現網站登錄,得到相應的個人信息,比如用戶信息,我的書架等,并實現一鍵刪除書架上所有書籍的功能;
本節和接下來的一節都是實戰環節,用于梳理 Scrapy 爬蟲框架的基本使用,也為鞏固和加深前面所學知識。
1. 基于 Cookie 的自動登錄
如果是想基于基本的 API 方式登錄,我們會面臨兩大難點:
- 手機驗證碼校驗 ,如下圖所示:
- 滑動驗證碼校驗,如下圖所示:
繞過這些校驗的方法超過了本教程的知識范圍,故我們不再次詳細討論。好在起點網支持自動登錄過程,也就是 Cookie 登錄:
第一次手動登錄起點,選擇自動登錄后,起點網站返回的 Cookie 信息就會保存至本地。下次再訪問起點網時,通過請求帶上該 Cookie 信息就能正確識別用戶,實現自動登錄過程。Cookie 存在本地,就存在被代碼讀取的可能。通常而言,我們來使用 Python 中的 browsercookie 庫可以獲取瀏覽器的 cookie,目前它只支持 Chrome 和 FireFox 兩種瀏覽器。不過對于 Chrome 80.X 版本的瀏覽器,其中的 cookie 信息被加密了,我們無法按照早期的操作進行 cookie 讀取。
不過網上這個博客給出了一個解密 Cookie 的代碼,我們拿過來簡單改造下,做成一個輔助模塊:
# 參考文檔:https://blog.csdn.net/u012552769/article/details/105001108
import sqlite3
import urllib3
import os
import json
import sys
import base64
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
def dpapi_decrypt(encrypted):
import ctypes
import ctypes.wintypes
class DATA_BLOB(ctypes.Structure):
_fields_ = [('cbData', ctypes.wintypes.DWORD),
('pbData', ctypes.POINTER(ctypes.c_char))]
p = ctypes.create_string_buffer(encrypted, len(encrypted))
blobin = DATA_BLOB(ctypes.sizeof(p), p)
blobout = DATA_BLOB()
retval = ctypes.windll.crypt32.CryptUnprotectData(
ctypes.byref(blobin), None, None, None, None, 0, ctypes.byref(blobout))
if not retval:
raise ctypes.WinError()
result = ctypes.string_at(blobout.pbData, blobout.cbData)
ctypes.windll.kernel32.LocalFree(blobout.pbData)
return result
def aes_decrypt(encrypted_txt):
with open(os.path.join(os.environ['LOCALAPPDATA'],
r"Google\Chrome\User Data\Local State"), encoding='utf-8', mode="r") as f:
jsn = json.loads(str(f.readline()))
encoded_key = jsn["os_crypt"]["encrypted_key"]
encrypted_key = base64.b64decode(encoded_key.encode())
encrypted_key = encrypted_key[5:]
key = dpapi_decrypt(encrypted_key)
nonce = encrypted_txt[3:15]
cipher = Cipher(algorithms.AES(key), None, backend=default_backend())
cipher.mode = modes.GCM(nonce)
decryptor = cipher.decryptor()
return decryptor.update(encrypted_txt[15:])
def chrome_decrypt(encrypted_txt):
if sys.platform == 'win32':
try:
if encrypted_txt[:4] == b'x01x00x00x00':
decrypted_txt = dpapi_decrypt(encrypted_txt)
return decrypted_txt.decode()
elif encrypted_txt[:3] == b'v10':
decrypted_txt = aes_decrypt(encrypted_txt)
return decrypted_txt[:-16].decode()
except WindowsError:
return None
else:
raise WindowsError
def get_cookies_from_chrome(domain, key_list):
sql = f'SELECT name, encrypted_value as value FROM cookies where host_key like "%{domain}%"'
filename = os.path.join(os.environ['USERPROFILE'], r'AppData\Local\Google\Chrome\User Data\default\Cookies')
con = sqlite3.connect(filename)
con.row_factory = sqlite3.Row
cur = con.cursor()
cur.execute(sql)
cookie_dict = {}
for row in cur:
if row['value'] is not None:
name = row['name']
value = chrome_decrypt(row['value'])
if value is not None and name in key_list:
cookie_dict[name] = value
return cookie_dict
Tips:上述這段代碼不用糾結細節,前面函數的主要是替
get_cookies_from_chrome()
函數服務的,而該函數的輸入要搜索的網站以及提取相應網站 cookie 信息中的某個具體字段,返回相應的結果。
本人 Python 3.8.2 安裝的是 win32 版本,該段代碼親測有效。來看看起點中文網給讀者生成的 cookie 數據,我們調用上面的獲取 cookie 信息的代碼來從中提取相應數據:
print(get_cookies_from_chrome('qidian.com', '_csrfToken'))
print(get_cookies_from_chrome('qidian.com', 'e1'))
print(get_cookies_from_chrome('qidian.com', 'e2'))
執行上述代碼我們可以得到如下結果:
PS C:\Users\spyinx> & "D:/Program Files (x86)/python3/python.exe" c:/Users/spyinx/Desktop/test_cookie.py
{'_csrfToken': 'YJklLmhMNpEfuSmqZZGaK72D4sUVJty52gyKwXXX'}
{'e1': '%7B%22pid%22%3A%22qd_p_qidian%22%2C%22eid%22%3A%22qd_A08%22%2C%22l1%22%3A1%7D'}
{'e2': '%7B%22pid%22%3A%22qd_p_qidian%22%2C%22eid%22%3A%22qd_A10%22%2C%22l1%22%3A1%7D'}
這說明我們通過前面的代碼能爭取獲取到起點網保存在 Chrome 瀏覽器中的 cookie 信息。因此,前面的代碼將作為我們讀取起點用戶登錄 Cookie 的重要輔助模塊。
Tips:這個測試只能在裝了 Chrome 瀏覽器的 Windows 系統上進行測試,或者是 Linux 的桌面版
我們首先來創建一個起點爬蟲:
PS C:\Users\Administrator\Desktop> scrapy startproject qidian_spider
接下里我們來看看要提取的我的書架的信息:
對應的 items.py 的內容如下:
# https://docs.scrapy.org/en/latest/topics/items.html
import scrapy
class QidianSpiderItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
# 分組
category = scrapy.Field()
# 小說名
name = scrapy.Field()
# 最新章節
latest_chapter = scrapy.Field()
# 作者
author = scrapy.Field()
# 更新時間
update_time = scrapy.Field()
# 閱讀進度
progress_status = scrapy.Field()
接下來,在爬蟲部分需要請求該頁面然后提取相應的數據,我們的爬蟲代碼如下:
"""
獲取用戶書架數據
"""
import json
from urllib import parse
from scrapy import Request
from scrapy.spiders import Spider
from .get_cookie import get_cookies_from_chrome
from ..items import QidianSpiderItem
class BookCaseSpider(Spider):
name = "bookcase"
# 構造函數
def __init__(self):
# 最重要的就是這個獲取起點的cookie數據了,這里保存了之前用戶登錄的cookie信息
self.cookie_dict = get_cookies_from_chrome(
"qidian.com",
["_csrfToken", "e1", "e2", "newstatisticUUID", "ywguid", "ywkey"]
)
def start_requests(self):
url = "https://my.qidian.com/bookcase"
# http請求時附加上cookie信息
yield Request(url=url, cookies=self.cookie_dict)
def parse(self, response):
item = QidianSpiderItem()
books = response.xpath('//table[@id="shelfTable"]/tbody/tr')
for book in books:
category = book.xpath('td[@class="col2"]/span/b[1]/a[1]/text()').extract_first()
name = book.xpath('td[@class="col2"]/span/b[1]/a[2]/text()').extract_first()
latest_chapter = book.xpath('td[@class="col2"]/span/a/text()').extract_first()
update_time = book.xpath('td[3]/text()').extract_first()
author = book.xpath('td[@class="col4"]/a/text()').extract_first()
progress_status = book.xpath('td[@class="col5"]/a/text()').extract_first()
item['category'] = category
item['name'] = name
item['latest_chapter'] = latest_chapter
item['update_time'] = update_time
item['author'] = author
item['progress_status'] = progress_status
print(f'get item = {item}')
yield item
最重要的方法就是那個獲取 cookie 信息的方法了,正是靠著這個 cookie,我們才能獲取相應的用戶書架的網頁并提取相應的書籍信息。接下來,簡單實現一個 item pipeline 用于保存書架上的書籍信息,該代碼位于 scrapy/pipelines.py
文件中,默認的 pipelines 都寫會在這里:
# 源碼位置:scrapy/pipelines.py
# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html
import json
# useful for handling different item types with a single interface
from itemadapter import ItemAdapter
class QidianSpiderPipeline:
def open_spider(self, spider):
self.file = open("bookcase.json", 'w+', encoding='utf-8')
def close_spider(self, spider):
self.file.close()
def process_item(self, item, spider):
data = json.dumps(dict(item), ensure_ascii=False)
self.file.write(f"{data}\n")
return item
最后別忘了在 settings.py 中添加這個 item pipeline:
ITEM_PIPELINES = {
'qidian_spider.pipelines.QidianSpiderPipeline': 300,
}
我們運行下這個爬蟲,看看是否能抓到我們想要的數據:
PS C:\Users\Administrator\Desktop> scrapy crawl bookcase
最后的結果如下:
這樣,我們就成功實現了用戶登錄后的訪問動作。接下來我們在這個基礎上進一步擴展,實現清除書架上所有的書籍,類似于淘寶的一鍵清除購物車。
2. 刪除起點網用戶的所有書架
首先我們隨便添加一個書籍到書架上,然后進行清楚,請看下圖,通過 Chrome 開發者工具我們可以找到刪除書架上書籍的 URL 請求以及相應攜帶參數:
該請求一共有三個參數:
- _csrfToken:可以從 cookie 中獲??;
- bids:書籍編號,可以從這一行的 html 元素中提??;
- gid:發現是固定的100;
于是我們在請求到書架上的書籍信息時,解析得到書籍編號,然后對應發送刪除該書籍的請求,對應的代碼如下:
from .get_cookie import get_cookies_from_chrome
from ..items import QidianSpiderItem
# 刪除書籍信息 https://my.qidian.com/ajax/BookShelf/DelBook?_csrfToken=YJklLmhEFpEfuSmqZZGaK72D4sUVJty52gyKw0TJ&bids=1022282526&gid=-100
class BookCaseSpider(Spider):
name = "bookcase"
# 構造函數
def __init__(self):
self.cookie_dict = get_cookies_from_chrome(
"qidian.com",
["_csrfToken", "e1", "e2", "newstatisticUUID", "ywguid", "ywkey"]
)
def start_requests(self):
url = "https://my.qidian.com/bookcase"
# http請求時附加上cookie信息
yield Request(url=url, cookies=self.cookie_dict)
def parse(self, response):
item = QidianSpiderItem()
books = response.xpath('//table[@id="shelfTable"]/tbody/tr')
for book in books:
# ...
# 刪除該書籍信息
query_data = {
'bids': book.xpath('td[6]/div[@class="ui-datalist"]/div[@class="ui-datalist-datalist"]/a[1]/@data-id').extract_first(),
'gid': '-100',
'_csrfToken': self.cookie_dict['_csrfToken']
}
url = "https://my.qidian.com/ajax/BookShelf/DelBook?{}".format(parse.urlencode(query_data))
print('對應刪除url請求={}'.format(url))
yield Request(url=url, method='get', cookies=self.cookie_dict, callback=self.parse_delete_book)
def parse_delete_book(self, response):
"""
刪除結果:{"code":0,"data":{"1022354901":{"code":0,"message":"操作成功"}},"msg":"成功"}
"""
data = response.text
print('刪除響應:{}'.format(data))
if isinstance(data, str):
data = json.loads(data)
print('msg = {}'.format(data['msg']))
是不是非常簡單?來看看最后運行的效果:
是不是很有意思?基于這樣的操作,我們想想淘寶一鍵清除購物車功能,是不是也能這樣實現?還有每次明星的戀情有變,連夜刪除上千條微博,導致手指酸痛,我們是否能提供一鍵清除微博的功能,解決他們的痛點?這些事情是不是想想就很激動?還等什么,心動不如行動,這個就作為課后作業吧,希望你能獨立完成淘寶的一鍵清除購物車代碼。
3. 小結
本節中,我們基于 Scrapy 框架講解了下如何簡單地實現自動登錄操作,避開滑動驗證碼的二次驗證。然后,在登錄的基礎上實現刪除用戶的所有書架操作。