Python爬虫进阶:JS逆向实战破解图片网站加密签名

发布时间:2026/8/17 14:46:41
Python爬虫进阶:JS逆向实战破解图片网站加密签名 1. 项目概述与核心挑战最近在整理一些视觉素材时发现“美之图”这类网站上有大量高质量的图片资源非常适合用于设计参考或个人收藏。直接用浏览器一张张保存效率太低于是自然想到了用Python写个爬虫。但实际操作起来才发现事情没那么简单。这类网站的前端防护做得相当到位图片链接、翻页参数甚至列表数据都经过了JavaScript混淆和加密直接发送HTTP请求拿到的HTML源码里几乎找不到我们想要的有效信息。这就是典型的“JS逆向”场景——你需要像侦探一样去分析前端JavaScript代码的执行逻辑找到数据最终是如何被生成和渲染的然后模拟这个过程在Python中复现关键算法从而拿到真实的数据接口。这个项目不仅考验你的Python爬虫基本功更考验你分析前端代码、理解网络请求和逆向JavaScript加密逻辑的能力。整个过程就像在解一个有趣的谜题非常适合有一定Python基础想从简单爬虫进阶到处理动态网页、对抗反爬策略的开发者。接下来我会详细拆解我是如何一步步找到突破口并最终成功爬取到目标图片的。整个过程涉及网络抓包、JS代码调试、参数逆向和最终的爬虫架构设计我会把每个环节的思考、踩过的坑以及最终验证有效的方案都分享出来。2. 逆向前的侦察网络请求与静态分析动手写代码之前充分的侦察是成功的一半。盲目地开始写解析逻辑很可能是在做无用功。2.1 初探网站结构与数据加载方式首先我用浏览器推荐Chrome或Edge的开发者工具打开目标网站。第一件事是禁用JavaScript然后刷新页面。果不其然页面变成了一片空白或者只显示一个框架没有任何图片内容。这立刻证实了我们的猜想所有关键数据图片列表、链接都是通过JavaScript动态加载的服务器返回的初始HTML只是一个“空壳”。接下来保持JavaScript启用重新加载页面。在图片加载出来的同时我迅速打开浏览器开发者工具的“网络”Network面板。为了聚焦我先清空记录然后勾选上“保留日志”Preserve log并选择“XHR/Fetch”过滤器。重新滚动页面或点击翻页观察网络面板中出现了哪些新的请求。很快我发现了一个关键请求。它的URL模式通常包含“api”、“getList”、“data”等关键词响应类型Type是xhr或fetch并且响应内容Preview是结构清晰的JSON数据里面包含了图片的ID、标题、缩略图URL有时甚至包含原始大图的URL。这个请求就是我们最终要模拟的目标接口。2.2 关键请求的深度剖析找到疑似数据接口后需要对其进行深度分析。右键点击该请求选择“Copy - Copy as cURL (bash)”。这个命令包含了请求的所有细节URL、请求头Headers、请求方法Method有时还包括请求体Payload。1. URL参数分析仔细查看URL特别是问号?后面的查询参数Query String Parameters。常见的参数有page: 页码。limit或size: 每页数量。t或_t: 时间戳用于防止缓存。sign或token: 加密签名这是JS逆向最常见的难点。它通常由其他参数如页码、时间戳加上一个固定的密钥key通过某种加密算法如MD5, SHA1, Base64 自定义运算生成。服务器端会用同样的算法验证这个签名不匹配则拒绝请求。2. 请求头Headers分析一些网站会校验特定的请求头。除了常见的User-Agent需要模拟成真实浏览器还需要特别注意Referer: 来源页有时必须正确设置。Cookie: 用户会话标识。对于公开图片列表可能不需要但如果网站有登录态或风控可能需要获取有效的Cookie。X-Requested-With: 可能是XMLHttpRequest。一些自定义的头部如X-Sign,X-Token等。3. 请求体Payload分析如果请求方法是POST那么需要查看“载荷”Payload标签页。它可能是form-data格式类似page1limit20也可能是raw格式通常是JSON字符串如{page:1, sign:abc123}。同样里面可能包含加密字段。在我的这次分析中目标接口是一个GET请求URL里包含page、limit和一个名为sign的参数。page和limit是明文的但sign是一长串毫无规律的字符串显然是加密后的结果。我们的核心任务就是找到生成这个sign参数的JavaScript代码。注意不同网站的反爬策略差异巨大。有的签名简单有的非常复杂甚至结合了环境检测如浏览器指纹。对于初学者建议从参数较少、签名逻辑相对清晰的网站练手。3. 核心战场JavaScript逆向与签名破解这是整个项目最具挑战性也最有趣的部分。我们需要在纷繁复杂的JavaScript代码中定位到生成加密参数的那几行关键逻辑。3.1 定位加密函数入口在开发者工具的“网络”面板中找到那个携带sign参数的关键请求。点击它在右侧的“标头”Headers选项卡最下方找到“发起程序”Initiator这一行。这里会显示是哪个JavaScript文件发起了这个请求。点击那个.js文件名可以直接跳转到“源代码”Sources面板对应的位置。跳转过去后通常看到的是一段经过压缩minify和混淆obfuscate的代码所有变量名都变成了a, b, c, d几乎没有可读性。这时我们需要一些技巧搜索关键词在源代码面板中按CtrlF搜索。可以尝试搜索sign、encrypt、MD5、SHA、encode、参数名如page等。如果运气好可能直接定位到相关代码段。XHR断点在“源代码”面板右侧找到“XHR/提取断点”XHR/Fetch Breakpoints点击“添加断点”Add breakpoint输入包含接口URL关键词如getList的字符串。然后刷新页面或触发翻页当浏览器发起匹配的请求时代码执行会自动暂停。此时调用栈Call Stack会显示当前执行到的函数我们可以一步步向上回溯找到生成参数的函数。Hook技巧这是一种更高级的方法。在控制台Console中注入一段代码用于拦截对特定属性如window.sign或方法如XMLHttpRequest.prototype.send的调用从而在签名生成时捕获其参数和结果。这需要一定的JS功底。经过一番搜索和断点调试我最终在发起请求的代码附近找到了一个类似sign: getSign(page, limit)的调用。顺藤摸瓜找到了getSign函数的定义。3.2 分析并复现加密逻辑找到getSign函数后发现它并不简单。它内部调用了其他函数进行了一系列字符串拼接、转换和加密操作。典型的流程可能是将page和limit按特定格式拼接如page1limit20key某个固定值。对这个拼接后的字符串进行MD5哈希计算。将MD5结果再进行一次Base64编码或者取其中间某一段。为了在Python中复现我必须彻底弄懂这个流程。我会在浏览器控制台里用相同的输入参数手动调用这个getSign函数验证输出是否与网络请求中的sign值一致。然后逐行分析函数内部的每一步操作。一个常见的坑JavaScript和Python在处理字符串和加密时可能有细微差别。例如字符串编码JS默认使用UTF-16吗Python的hashlib.md5()需要传入bytes类型你需要确保字符串编码一致通常使用.encode(utf-8)。Base64差异JS的btoa和Python的base64.b64encode结果可能因为换行符、填充符而不同需要仔细比对。时间戳JS的Date.now()返回毫秒级时间戳Python的int(time.time()*1000)可以对应。经过分析我确认目标网站的签名算法是sign md5(‘page’ page ‘size’ limit ‘t’ timestamp ‘secret_key’ KEY).toUpperCase()。其中KEY是一个隐藏在JS代码中的固定字符串通过搜索secret、key等关键词找到。3.3 将JS逻辑翻译为Python代码逻辑清晰后翻译成Python代码就水到渠成了。这里需要用到hashlib库进行MD5计算。import hashlib import time def generate_sign(page, limit, secret_key找到的密钥): # 获取当前时间戳毫秒 timestamp int(time.time() * 1000) # 按JS中的格式拼接字符串 raw_str fpage{page}size{limit}t{timestamp}secret_key{secret_key} # MD5哈希并转为大写十六进制字符串 md5_hash hashlib.md5(raw_str.encode(utf-8)).hexdigest().upper() return md5_hash, timestamp # 通常时间戳也需要作为参数传递 # 测试 sign, ts generate_sign(1, 20) print(f生成的sign: {sign}) print(f对应的时间戳: {ts})在浏览器控制台用相同参数运行JS的getSign函数对比两者输出确保完全一致。这是逆向成功与否的黄金标准。4. 爬虫架构设计与代码实现逆向成功后构建一个健壮、高效的爬虫就相对直接了。我将爬虫分为几个模块请求器、解析器、下载器和主调度器。4.1 请求器模拟浏览器请求请求器的核心是构造出能够通过网站验证的HTTP请求。我们将逆向得到的签名函数集成进来。import requests import time from urllib.parse import urlencode class ImageSpider: def __init__(self, secret_key): self.session requests.Session() # 设置一个合理的浏览器User-Agent self.session.headers.update({ 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, Referer: https://www.target-website.com/ # 替换为目标网站主页 }) self.secret_key secret_key self.base_url https://api.target-website.com/getList # 替换为找到的接口地址 def generate_params(self, page, limit): 生成请求参数包含签名 timestamp int(time.time() * 1000) # 根据逆向结果构造参数字典 params { page: page, size: limit, t: timestamp, # 其他可能的固定参数 } # 生成签名 sign self._make_sign(params) params[sign] sign return params def _make_sign(self, params_dict): 具体的签名算法实现 # 这里根据逆向逻辑实现例如 # 1. 将参数按特定顺序拼接成字符串 param_list [f{k}{v} for k, v in sorted(params_dict.items())] param_str .join(param_list) # 2. 拼接密钥 raw_str param_str self.secret_key # 3. 计算MD5并大写 return hashlib.md5(raw_str.encode(utf-8)).hexdigest().upper() def fetch_list(self, page1, limit20): 获取图片列表页的JSON数据 params self.generate_params(page, limit) try: # 注意是GET请求参数在URL中 resp self.session.get(self.base_url, paramsparams, timeout10) resp.raise_for_status() # 如果状态码不是200抛出异常 return resp.json() # 直接解析JSON except requests.exceptions.RequestException as e: print(f请求列表页失败 (第{page}页): {e}) return None except ValueError as e: print(f解析JSON失败 (第{page}页): {e}) print(f响应文本: {resp.text[:200]}) # 打印前200字符帮助调试 return None4.2 解析器与下载器处理数据与文件存储列表接口返回的JSON数据通常结构清晰。我们需要从中提取出图片的标题和真正的图片URL。import os from concurrent.futures import ThreadPoolExecutor, as_completed class ImageSpider(ImageSpider): # 继承上面的类 def parse_list(self, json_data): 从列表JSON中解析出图片信息 image_list [] # 这个结构需要根据实际接口返回调整 # 例如数据可能在 json_data[data][list] 里 if json_data and json_data.get(code) 200: # 假设成功码是200 items json_data.get(data, {}).get(list, []) for item in items: image_info { id: item.get(id), title: item.get(title, ).strip().replace(/, _), # 处理标题避免文件名非法字符 # 缩略图和小图URL可能不同这里需要根据实际情况取大图URL # 有时大图URL需要另一个接口这里假设list里直接有 url: item.get(imageUrl) or item.get(url) or item.get(src) } if image_info[url]: # 确保有有效的URL image_list.append(image_info) return image_list def download_image(self, image_info, save_dir./downloads): 下载单张图片 if not image_info[url]: return False # 创建保存目录 os.makedirs(save_dir, exist_okTrue) # 生成文件名避免重复 file_ext os.path.splitext(image_info[url])[1] or .jpg # 如果扩展名不常见或没有可以尝试从Content-Type判断 safe_title .join([c for c in image_info[title] if c.isalpha() or c.isdigit() or c in ( , -, _)]).rstrip() filename f{image_info[id]}_{safe_title}{file_ext} if safe_title else f{image_info[id]}{file_ext} filepath os.path.join(save_dir, filename) try: # 下载图片 resp self.session.get(image_info[url], streamTrue, timeout15) resp.raise_for_status() # 检查Content-Type是否是图片 content_type resp.headers.get(content-type, ) if image not in content_type: print(f警告: {image_info[url]} 返回的内容类型不是图片: {content_type}) # 可以选择不保存或继续 # 写入文件 with open(filepath, wb) as f: for chunk in resp.iter_content(chunk_size8192): if chunk: f.write(chunk) print(f下载成功: {filename}) return True except Exception as e: print(f下载失败 {image_info[url]}: {e}) return False def download_batch(self, image_info_list, max_workers5): 使用线程池批量下载图片 with ThreadPoolExecutor(max_workersmax_workers) as executor: future_to_img {executor.submit(self.download_image, img): img for img in image_info_list} for future in as_completed(future_to_img): img future_to_img[future] try: future.result() # 这里可以获取结果但我们只关心是否异常 except Exception as e: print(f线程下载出错 {img.get(title)}: {e})4.3 主调度器串联整个流程最后用一个主函数把所有模块串联起来实现翻页爬取。def main(): # 1. 初始化爬虫传入逆向得到的密钥 spider ImageSpider(secret_keyYOUR_SECRET_KEY_HERE) save_directory ./beauty_images start_page 1 end_page 10 # 计划爬取的页数 per_page 20 for page in range(start_page, end_page 1): print(f\n开始处理第 {page} 页...) # 2. 获取列表数据 list_data spider.fetch_list(pagepage, limitper_page) if not list_data: print(f第 {page} 页获取失败跳过。) continue # 3. 解析图片列表 images spider.parse_list(list_data) if not images: print(f第 {page} 页未解析到图片数据可能结构有变或已无数据。) break # 如果连续几页没数据可以考虑终止 print(f第 {page} 页解析到 {len(images)} 张图片。) # 4. 批量下载本页图片 spider.download_batch(images, save_dirsave_directory) # 5. 礼貌性延迟避免请求过快 time.sleep(1) print(\n所有任务完成) if __name__ __main__: main()5. 实战中遇到的坑与解决方案在实际操作中不可能一帆风顺。下面是我遇到的一些典型问题及解决方法。5.1 签名算法突然变更这是最头疼的情况。可能某一天爬虫突然失效返回“签名错误”。这说明网站更新了签名算法。应对策略监控与告警在爬虫脚本中加入对响应状态的检查。如果连续多次收到特定的错误码如code: 403message: ‘invalid sign’就触发告警如发送邮件、记录日志。重新逆向按照第3章的步骤重新抓包、定位、分析新的签名函数。通常算法核心可能不变只是拼接顺序或密钥变了。代码可配置化将签名生成函数独立出来并且使其易于修改。可以将密钥、拼接模板等写成配置文件这样更新时只需修改配置而无需改动核心代码。5.2 数据接口返回假数据或空数据有时接口能正常返回200状态码但data字段是空的或者图片URL是统一的占位图。可能原因和排查请求头不全检查是否漏掉了某些必要的请求头如Referer,Origin, 或某些自定义头部。用复制来的cURL命令在Python里重放请求对比差异。Cookie失效或需要特定Cookie有些网站的风控策略会检查Cookie。尝试在浏览器中登录如果需要然后将有效的Cookie字符串复制到爬虫的session.headers.update({Cookie: ...})中。注意Cookie有有效期。IP被封短时间内请求过于频繁导致IP被暂时封锁。解决方案是使用代理IP池并在请求间增加随机延迟。import random time.sleep(random.uniform(1, 3)) # 随机延迟1-3秒参数格式错误检查sign的生成是否完全正确时间戳单位秒/毫秒是否对参数名是page还是pageNum是否和网站一致。5.3 图片URL是临时的或需要二次请求有些网站为了防盗链列表接口返回的图片URL是临时的、有过期时间的或者只是一个图片ID需要再用这个ID请求另一个详情接口才能拿到真实地址。解决方案分析详情接口点击网站上的某张图片查看其大图加载时触发的网络请求。很可能存在一个如getImageDetail?idxxx的接口。修改解析逻辑在parse_list函数中不再直接提取imageUrl而是提取图片ID。然后新增一个fetch_detail方法用ID去请求详情接口从返回的JSON中提取真实的高清图URL。注意请求顺序先批量获取所有ID再并发请求详情接口最后再并发下载图片。避免同步请求导致的效率低下。5.4 法律与道德风险规避爬取图片必须遵守法律法规和网站的robots.txt协议。尊重版权明确爬取图片的用途。用于个人学习、研究是合理的但未经授权用于商业用途、大量分发或训练AI模型可能侵权。控制速率在爬虫中添加显著的延迟如time.sleep(2)避免对目标网站服务器造成压力这既是道德要求也能降低被封IP的风险。查看robots.txt访问https://目标网站/robots.txt查看是否禁止爬取目标路径如/api/,/getList。虽然技术上可以绕过但遵守它是良好的网络公民行为。用户代理标识在User-Agent中可以考虑加入联系方式如MyResearchBot/1.0 (contactexample.com)以示友好。6. 性能优化与代码健壮性提升一个基础的爬虫写完后我们可以从以下几个方面让它变得更强大、更稳定。6.1 引入异步处理对于IO密集型的网络爬虫尤其是大量图片下载使用异步IOasyncioaiohttp可以极大提升效率避免在等待网络响应时阻塞线程。# 简化的异步示例框架 import aiohttp import asyncio async def fetch_and_download(session, url, save_path): try: async with session.get(url) as resp: if resp.status 200: content await resp.read() with open(save_path, wb) as f: f.write(content) return True except Exception as e: print(fError downloading {url}: {e}) return False async def main_async(): connector aiohttp.TCPConnector(limit10) # 控制并发连接数 timeout aiohttp.ClientTimeout(total30) async with aiohttp.ClientSession(connectorconnector, timeouttimeout, headersheaders) as session: tasks [] # ... 生成所有下载任务 for img_info in all_images: task fetch_and_download(session, img_info[url], img_info[path]) tasks.append(task) # 并发执行所有任务 results await asyncio.gather(*tasks, return_exceptionsTrue)6.2 增加持久化与断点续传爬取大量数据时脚本可能中途因网络或错误中断。我们可以将已成功爬取的页面或图片ID记录到文件如JSON或SQLite数据库中下次启动时先读取记录跳过已处理的内容。import json class ProgressTracker: def __init__(self, state_fileprogress.json): self.state_file state_file self.state self._load_state() def _load_state(self): try: with open(self.state_file, r) as f: return json.load(f) except FileNotFoundError: return {last_page: 0, downloaded_ids: []} def save_state(self, page, downloaded_id): self.state[last_page] page self.state[downloaded_ids].append(downloaded_id) with open(self.state_file, w) as f: json.dump(self.state, f) def is_downloaded(self, img_id): return img_id in self.state[downloaded_ids] # 在主循环中使用 tracker ProgressTracker() for page in range(tracker.state[last_page] 1, end_page 1): # ... 获取列表 for img in images: if tracker.is_downloaded(img[id]): continue # 跳过已下载的 if download_success: tracker.save_state(page, img[id])6.3 完善的日志与错误处理在生产环境中详细的日志至关重要。使用Python的logging模块替代print可以方便地控制日志级别、输出到文件。import logging logging.basicConfig( levellogging.INFO, format%(asctime)s - %(name)s - %(levelname)s - %(message)s, handlers[ logging.FileHandler(spider.log), logging.StreamHandler() ] ) logger logging.getLogger(__name__) # 在代码中使用 try: data spider.fetch_list(page) except requests.exceptions.Timeout: logger.error(f请求第{page}页超时) except Exception as e: logger.exception(f处理第{page}页时发生未知错误) # 会记录完整的异常堆栈6.4 应对反爬升级动态参数与环境检测一些高级反爬会检测浏览器环境比如检查navigator对象属性、WebGL渲染器、字体列表等。纯requests库无法提供这些环境。解决方案使用Selenium或Playwright这些工具可以控制一个真实的浏览器完美通过环境检测。但代价是资源消耗大、速度慢。适合用于获取最初的关键令牌Token或Cookie然后用requests会话维持后续请求。分析核心检测点通过JS逆向找到网站具体检测了哪些属性。有时可能只是检查一个简单的全局变量是否存在如window._isHuman。如果检测逻辑不复杂可以在Python中用requests-html或pyppeteer等无头浏览器轻量级模拟或者在requests会话中通过注入JS代码来设置相应的请求头或Cookie。整个逆向爬虫的过程是一个不断与目标网站“斗智斗勇”和学习的过程。没有一劳永逸的方案核心在于掌握分析问题的思路和工具的使用。从静态分析到动态调试从模拟请求到处理异常每一步都需要耐心和细心。当你成功破解签名、看到数据如预期般流淌下来时那种成就感是无可替代的。希望这份详细的记录能为你下一次的JS逆向之旅铺平道路。记住保持好奇保持耐心合规爬取。