前言
MediaCrawler 是最近冲上 Github 热搜的开源多社交平台爬虫。虽然现在已删库,但还好我眼疾手快,有幸还 Fork 了一份,乘着周末,简单分析了下小红书平台的相关代码。
爬虫难点
一般写爬虫,都需要面对以下几个问题
- 如果 app/网页需要登录,如何获取登录态(cookie/jwt)
- 大部分 app/网页都会对请求参数进行 sign,如果有,如何获取 sign 逻辑
- 绕过其它遇到的反爬措施
我将带着这三个问题,阅读 MediaCrawler 小红书代码,看看 MediaCrawler 是怎么处理的。
获取登录态
提供了三种方式
- QRCode (login_by_qrcode)
- 手机号 (login_by_mobile)
- Cookie (login_by_cookies)
登录相关代码都在 media_platform/xhs/login.py
文件中
QRCode 登录
实现代码为 login_by_qrcode
方法。代码为:
1 | async def login_by_qrcode(self): |
大致逻辑:
启动 headless 浏览器并且 headless 模式必须设为
False
, 因为不会把 QRCode 显示在终端或者通过信息转发服务转发到你的手机上通过
utils.find_login_qrcode
工具函数以及qrcode_img_selector
获取headless 浏览器所渲染页面中的 QRCode 图片元素如果没有获取到,则点击
login_button_ele
, 弹出登录对话框,然后再重复一次步骤2,如果依旧没有获取到,则退出爬虫,爬取失败。如果获取到了,则等待用户扫码完成登录。
其中可能会出现验证码的情况,此时会提示需要手动验证,没有实现自动验证,需要人工手动操作干预。
1
2
3
4
5
6
7async def check_login_state(self, no_logged_in_session: str) -> bool:
# ......
if "请通过验证" in await self.context_page.content():
utils.logger.info("[XHSLogin.check_login_state] A verification code appeared during the login process, please verify manually.")
# ......
手机号登录
实现代码为 login_by_mobile
方法。代码为:
1 | async def login_by_mobile(self): |
大致逻辑:
启动 headless 浏览器
点击
login_button_ele
, 弹出登录对话框获取phone
input_ele
,并填入手机号1
2
3login_container_ele = await self.context_page.wait_for_selector("div.login-container")
input_ele = await login_container_ele.query_selector("label.phone > input")
await input_ele.fill(self.login_phone)点击
send_btn_ele
, 发送验证码每隔 1 秒从 Redis 数据库中获取验证码。如果 120 秒后,依旧没有获取到,则退出爬虫,爬取失败
如果获取到了,则将验证码,填入验证码输入框(
sms_code_btn_ele
),并勾选同意隐私协议按钮(agree_privacy_ele
)以及提交按钮(submit_btn_ele
)因为依赖了 Redis 数据库组件,所以可以通过短信转发软件或者短信获取接口实现短信验证码输入的自动化,实现自动化手机号登录
- 代码中没有检测验证码的正确性。
- 代码中没有登录失败重试机制
Cookie登录
实现代码为 login_by_cookies
方法。就是将用户提供的 cookie(web_session)
信息放到browser_context
中
1 | async def login_by_cookies(self): |
Sign签名算法
小红书浏览器端接口有做sign
验签,MediaCrawler 生成 sign
相关参数的代码位于 media_platform/xhs/client.py
文件中的 _pre_headers
方法。代码如下:
1 | async def _pre_headers(self, url: str, data=None) -> Dict: |
- 没有逆向后用 Python 重新实现
window._webmsxyw
函数,而是通过self.playwright_page.evaluate("([url, data]) => window._webmsxyw(url,data)", [url, data])
直接主动调用浏览器运行时中的window._webmsxyw
生成encrypt_params
- 通过
self.playwright_page.evaluate("() => window.localStorage")
获取浏览器local_storage
对象 - 将
cookie
中的a1
,local_storage
中的b1
,encrypt_params
中的X-s
,encrypt_params
中的X_t
, 作为参数传给sign
函数 sign
函数最终返回签名后的值signs
- 将
signs
赋值给headers
所以主要签名逻辑就是 sign
函数,深入进去看下。代码位于 media_platform/xhs/help.py
文件。代码如下:
1 | def sign(a1="", b1="", x_s="", x_t=""): |
- 这个
sign
函数没有像window._webmsxyw
方法一样,选择通过self.playwright_page.evaluate
主动调用浏览器运行时中的sign
相关方法,而是自己用 Python 实现。实现代码不再解释,就是逆向 JS 逻辑后,翻译成了 Python。 - 对于为何选择自己用 Python 实现而不主动调用浏览器中的 JS 方法,我也咨询了下作者,作者表示,这里的代码冗余了
其它
反反爬虫
MediaCrawler 小红书爬虫,也做了一些反反爬虫措施,代码位于media_platform/xhs/core.py
中的 start
函数。
通过注入
stealthjs
脚本,来反headless 浏览器检测1
2# https://github.com/berstend/puppeteer-extra/tree/master/packages/extract-stealth-evasions
await self.browser_context.add_init_script(path="libs/stealth.min.js")通过添加
webId cookie
来防止出现captcha
滑块1
2
3
4
5
6
7
8await self.browser_context.add_cookies([
{
'name': "webId",
'value': "xxx123", # any value
'domain': ".xiaohongshu.com",
'path': "/"
}
])支持设置 ip 代理来更改 ip 地址
1
2
3
4if config.ENABLE_IP_PROXY:
ip_proxy_pool = await create_ip_pool(config.IP_PROXY_POOL_COUNT, enable_validate_ip=True)
ip_proxy_info: IpInfoModel = await ip_proxy_pool.get_proxy()
playwright_proxy_format, httpx_proxy_format = self.format_proxy_info(ip_proxy_info)
数据爬取
通过 httpx
库发起 http 请求,请求时带上cookie
以及 sign
相关参数。请求是直接请求的 API, 所以没有任何 html 解析逻辑,当请求成功后,就会对数据进行一些处理,然后将数据入库。相关主要逻辑位于 media_platform/xhs/core.py
以及 media_platform/xhs/client.py
。由于比较简单,不再展开。
结语
MediaCrawler 小红书爬虫是基于小红书浏览器端协议,实现了sign
参数的获取,以及登录态的获取。sign
参数的获取没有完全逆向 JS 逻辑并用 Python 实现,而是通过self.playwright_page.evaluate
主动调用了部分 JS 函数(window._webmsxyw
)。登录态的获取,也是基于 headless 浏览器实现,QRCode 登录需要人工操作;手机号登录可以通过短信转发软件或者短信接收接口实现自动化登录,但没有做验证码检验,验证失败重试。可以通过stealthjs
来反 headless 浏览器检测。
PS: 兄弟们,我先去爬小红书大胸翘臀女菩萨了,有时间再分析下抖音爬虫的大致逻辑~