SM.MS是个不错的图床工具,上传的图片资源虽然不能批量一键下载,但官方还是提供了一个获取图片列表的API:https://sm.ms/api/v2/upload_history,header中只需要提供一个Authorization认证码即可,好长时间没写python了,想练练手,就不用API了

之前爬取需要登陆的网站我都是通过Selenium完成,尝试换个姿势,通过requests.Session实现?首先分析下SM.MS网站的登陆流程:

使用fiddler抓包查看表单请求内容:

唯一的障碍点其实就是g-recaptcha-response字段值的获取(我本想通过execjs模块执行JS代码),sm.ms通过<script src="https://recaptcha.google.cn/recaptcha/api.js?render=6LdArYchAAAAADs9BJEP0Ud-MpC54mNN90Bp0BvK"></script>获得了一个grecaptcha对象,然后调用grecaptcha.execute()得到了g-recaptcha-response字段值,然而execjs主要是一个在非浏览器环境中执行JS代码的工具,因此涉及到浏览器特有对象(如window、document)的代码在这个环境下通常是无法正常工作的

好吧,姿势更换失败,还是只能通过selenium(+chrome)爬取SM.MS图片数据了,但是为了尽量寻求较优的实现方案,我只会用selenium完成登陆操作,之后就可以拿到网站cookie,再用requests库携cookie请求个人图片资源列表、下载图片数据,毕竟如果是做大规模图片爬取的话,使用selenium速度实在太慢了,肯定得用并发URL请求下载资源,而且很多境外服务器一次网络请求都有可能是以秒为单位,单线程下载不得等到地老天荒啊,对于python来说,爬虫属于网络IO密集型任务,因此直接用线程模块即可或者上线程池

其实要确定请求头参数有个更好的办法,直接F12找到网络请求链接右键Copy as cURL(还可以顺便cmd执行一下看看能否成功):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
curl "https://sm.ms/home/picture" ^
-v ^
-H "authority: sm.ms" ^
-H "accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7" ^
-H "accept-language: zh-CN,zh;q=0.9" ^
-H "cookie: PHPSESSID=xxx; smms=yyy" ^
-H "sec-ch-ua: ^\^"Not_A Brand^\^";v=^\^"8^\^", ^\^"Chromium^\^";v=^\^"120^\^", ^\^"Google Chrome^\^";v=^\^"120^\^"" ^
-H "sec-ch-ua-mobile: ?0" ^
-H "sec-ch-ua-platform: ^\^"Windows^\^"" ^
-H "sec-fetch-dest: document" ^
-H "sec-fetch-mode: navigate" ^
-H "sec-fetch-site: none" ^
-H "sec-fetch-user: ?1" ^
-H "upgrade-insecure-requests: 1" ^
-H "user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" ^
--compressed

执行request.get(url, cookie)报错:OpenSSL.SSL.Error: [(‘SSL routines’, ‘ssl3_get_server_certificate’, ‘certificate verify failed’)],意思是对于sm.ms服务器发过来的证书验证失败了,解决办法就是在get方法中传递verify参数以指定用于验证的CA根证书,从sm.ms网站下载用于验证的根证书(链),crt格式,然后用XCA转换为单个pem格式证书:

获取到所有图片资源的URL列表,就可以通过get请求下载二进制图片了,经过一系列操作,我得到了一个用字典构建的图片数据库db,其键值对定义如下:<img_unique_key : [img_name, url, size, upload_date, download_flag, local_path]>,其中download_flag指示其是否已被下载到本地,只有为否的图片url才需要提交给线程池通过request执行下载,local_path是图片的本地下载路径。通过线程池下载实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
urls = {k:v[1] for k,v in db.items() if v[-2]==False} #find all pictures not downloaded
def download_url(url, timeout):
r = requests.get(url, timeout=timeout)
if r.ok:
return r.content
return None
with concurrent.futures.ThreadPoolExecutor(max_workers=36) as workers:
future_download = {workers.submit(download_url, url, timeout=6):key for key,url in urls.items()}
for task in as_completed(future_download):
img_key = future_download(task)
try:
img_byte = task.result()
except Exception as e:
print(f'Error: download {db[img_key][1]} failed for {e}')
continue
if img_byte is None:
continue
db[img_key][-2] = True
save_img_to_local(img_byte, local_path)
db[img_key][-1] = local_path
save_db_to_local(db)

最后我又将图片数据库db导出为一个简易html页面:

完整代码见github仓库smms_download,支持增量更新