Hi I'm Shendi
最近有小程序爬虫需求,于是在这里简略记录下来经过
方案
爬取数据的办法通常有几种,一种是自动化脚本。即模拟用户操作,在移动设备上可以通过无障碍来获取元素数据
,另一种是通过接口获取数据。
通过监听移动设备数据(中间人)
能直接通过接口爬取数据是最好的,所以最开始使用的就是监听数据的形式
具体流程就是在移动设备设置代理,让数据经过电脑的代理服务器,并在移动设备安装证书,这样就可以监听到数据了
不过这个方法尝试失败了,因为在Android9以后,用户证书不再被信任,而要安装系统证书需要Root...尝试一番太过麻烦就放弃了。
通过无障碍抓取数据
本着可见皆可爬的原则,觉得无障碍爬取一定是没有问题,所以在稍微尝试了一番后,编写了无障碍APP爬取,果然是可以爬的,然后我就开始编写后续所有的代码...
其中有图片爬取,不过没关系,可以自动化操作,点击图片,长按保存让其进入相册,然后在相册中读取最新的图片就可以了。并且小程序保存会自动输出保存的图片路径,也可以通过无障碍获取这个地址进行操作。
等我编写完,我发现,有一个数据是乱的(正常的文字,但是连起来狗屁不通,与界面上显示的不一致),我开始仔细分析哪里的问题...思索无果放弃了。
现在对于要完成这个目标的选择有:
- 尝试反编译小程序源码
- 直接通过OCR读取屏幕转文字,配合无障碍完成整个爬取,这是没有任何办法时的最后的办法
反编译小程序代码
但总是要爬的,所以开始反编译,因为微信可以电脑上打开小程序,所以反编译简单了起来,只要使用工具就可以了,反编译成功了,获取到了小程序源码。
反编译过程可以看这个:https://blog.csdn.net/weixin_54261528/article/details/145663372
反编译后,我又想到是否可以直接在电脑上监听接口数据,因为小程序是电脑微信打开的,我使用mitmproxy监听,的确监听到了,这样token就可以拿到了。
不过其中的一些关键数据是加密的,正好有源码,查看源码,一个个寻找...
最终,这个小程序使用RSA加密了数据,私钥都在小程序代码中,所以我很容易解密了。接口包含timestamp,token,random,sign,这四个关键参数,根据源代码显示的,将前三个字符串拼接,通过md5生成的sign就可以通过接口验证了。
现在一切都通了,好像很顺利,但是我解密的数据是HTML实体编码的,我解码后发现,跟无障碍遇到的情况一样,跟显示的文字完全对不上!我仔细查找源代码,以为有什么地方漏看了,少了一个解码步骤...
除了一个加载自定义字体的操作,没有任何东西了,所以,是不是字体的问题?了解后,果然,字体反爬是一种常见的手段,但是如何用字体将实体编码解析成文本?这可就头疼了。
自定义字体的自动化映射获取
最后的问题就是处理掉这个字体的问题,整个爬取就大功告成了
我通过源代码,下载了自定义的字体文件,这个字体可以理解为一个编码对应一个图片...所以,能想到的就是编写一个映射,将编码与正确文字对应...但手动编写也太浪费时间了,并且如果字很多呢?
字体在线编辑可以用这个看字体编码与字体对应:https://kekee000.github.io/fonteditor/
所以我第一时间想到,能不能使用OCR识别来全自动生成映射?只要把字体分割成一个个的,然后一个个识别就可以了...于是编写了以下代码(Python)
python
import io
import os
import ddddocr
from PIL import ImageDraw, ImageFont, Image
from fontTools.ttLib import TTFont
# 将字体文件生成映射的python代码,通过ocr自动识别。
class UniversalFontRecognition(object):
def __init__(self, font_path):
self.font_path = font_path
self.ocr = ddddocr.DdddOcr(show_ad=False)
def font_to_xml(self, xml_path=None):
"""将 TTF 字体文件转换为 XML 格式"""
if not xml_path:
filename_with_ext = os.path.basename(self.font_path)
filename_only = os.path.splitext(filename_with_ext)[0]
xml_path = f'{filename_only}.xml'
font = TTFont(self.font_path)
font.saveXML(xml_path)
def font_to_img(self, char_list, img_size=100, font_ratio=0.7):
"""将字体字符渲染成图片,并使用 OCR 识别"""
normal_dict = {}
for char in char_list:
char_code = chr(char) # 直接转换 Unicode
img = Image.new('RGB', (img_size, img_size), (255, 255, 255)) # 修正背景色
draw = ImageDraw.Draw(img)
font = ImageFont.truetype(self.font_path, int(img_size * font_ratio))
# 获取文本边界
bbox = draw.textbbox((0, 0), char_code, font=font)
if bbox is None:
continue # 跳过无法获取尺寸的字符
x, y = bbox[2] - bbox[0], bbox[3] - bbox[1] # 计算宽高
draw.text(((img_size - x) // 2, (img_size - y) // 2), char_code, font=font, fill=(0, 0, 0))
# 将图片保存为字节流
img_bytes = io.BytesIO()
img.save(img_bytes, format='JPEG')
image_bytes = img_bytes.getvalue()
# 使用 OCR 识别
word = self.ocr.classification(image_bytes)
normal_dict[char] = word[0] if word and len(word) > 0 else ''
return normal_dict
def crack(self):
"""解析 TTF 字体文件并生成 Unicode → 真实字符的映射"""
with open(self.font_path, 'rb') as fr:
font_bytes = fr.read()
with TTFont(io.BytesIO(font_bytes)) as font_parse:
u_d = font_parse.getBestCmap() # 获取 Unicode → 字体编码的映射
return self.font_to_img(list(u_d.keys()))
# === 使用示例 ===
ufr = UniversalFontRecognition(r"font/sfont.ttf")
# 1. 可选:导出 XML,查看字体编码信息
ufr.font_to_xml()
# 2. 生成映射字典
font_map = ufr.crack()
# 3. 打印映射结果
print(font_map)
需要先安装以下依赖,然后通过python直接运行。
pip install fonttools pillow ddddocr
效果也是非常不错,但我在查看过程中,发现有些字母大小写识别总是小写,所以手动更改一下映射
最终还是遇到了一个问题,是解密后解码的文字莫名其妙有一个字符乱码,我定位到问题是在解密后的数据就带上了乱码...最终是因为,RSA解密使用的字符串拼接方式,导致了乱码,所以改成直接字节方式就可以了
java
public static byte[] decryptLongBinary(String encryptedData, PrivateKey privateKey) throws Exception {
byte[] encryptedBytes = Base64.getDecoder().decode(encryptedData);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
int keySize = 128; // 1024bit RSA, 128 bytes per block
int chunkSize = keySize; // Each decryption chunk is 128 bytes
int inputLength = encryptedBytes.length;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// Split the data and decrypt chunk by chunk
for (int i = 0; i < inputLength; i += chunkSize) {
int len = Math.min(inputLength - i, chunkSize);
byte[] decryptedChunk = cipher.doFinal(encryptedBytes, i, len);
baos.write(decryptedChunk);
}
return baos.toByteArray();
}
就这样,目标完成。
END