花了两周整理完一份行业报告,从截图到排版再到加各种水印,折腾了好几轮。发到小红书上,隔天就刷到一模一样的内容——水印裁得干干净净,联系方式换成别人的,还挂了 9.9 在卖。找平台举报,回复是“证据不足”。白干了。

手动把每页关键信息打码再切片?三十多页的 PDF,光截图就让人手抽筋,更别提还得一页页检查有没有漏掉角落里的微信号。这不是个案。知识付费圈子里被搬得最惨的,永远是花时间做预览图的那批人。我见过最离谱的案例,对方连我排版里的拼写错误都照搬,只是把联系方式涂了。那种时候你觉得自己是在“做内容”吗?不,是在给对方免费提供原料。

当时第一反应是加更密集的水印。但小红书对营销图限流,水印太重直接不给你曝光。打码也不行——手动一页页糊马赛克,费时不说,漏掉一个二维码就等于白忙。

真正让我决定写脚本的契机,是某次手动处理一份 47 页的笔记。到第 39 页时眼睛花了,把某段关键数据放了过去。第二天,有人截图那段数据去引流,文案写的是“完整版找我”。那天晚上我翻了翻 Pillow 的 ImageDrawImageFilter,发现给图片打随机位置的遮挡块,再按固定网格切分,这套流程完全可以用几十行代码自动化。核心逻辑就是三步:加载原图 → 按规则覆盖遮挡层(文字块、二维码区、图表关键轴)→ 等分成 3×3 或 4×3 切片输出。遮挡块的位置、大小、透明度全部随机,避免被反向还原。

from PIL import Image, ImageDraw, ImageFilter
import random

def add_obfuscation(img, areas):
    overlay = ImageDraw.Draw(img)
    for area in areas:
        x1 = area[0] + random.randint(-20, 20)
        y1 = area[1] + random.randint(-20, 20)
        x2 = area[2] + random.randint(-20, 20)
        y2 = area[3] + random.randint(-20, 20)
        overlay.rectangle([x1, y1, x2, y2], fill=(random.randint(180,220),)*3)
    return img

def slice_grid(img, rows=3, cols=3):
    w, h = img.size
    cell_w, cell_h = w // cols, h // rows
    tiles = []
    for r in range(rows):
        for c in range(cols):
            box = (c*cell_w, r*cell_h, (c+1)*cell_w, (r+1)*cell_h)
            tiles.append(img.crop(box))
    return tiles

参数调了一整个晚上。遮挡块灰度值低于 180 太扎眼,高于 230 又容易被后期去噪还原。切片尺寸不能刚好等分——部分场景需要让中间几格重叠 5% 边距,防止被 AI 拼接缝合。跑通之后拿那份 47 页的笔记试了一次,生成 141 张切片图(3 行 × 3 列 × 47 页),耗时 8.3 秒。手动干至少两小时。

当然,自动化不是万能。有些资料页的二维码恰好压在两张切片边界上,遮挡块没盖全。后来加了预先扫描图片中高对比度矩形区域的逻辑(大概率是二维码或条形码),强制覆盖两层不同形状的遮挡。这套逻辑被我拆成一个独立函数 cover_qr_zone(),配合 PIL 的 ImageFilter.GaussianBlur(radius=15) 做二次模糊,算是把“防恢复”做到了我能接受的底线。

切片跑通了,但真正让我卡了两天的不是技术,是“怎么切才能让人想点进来”

第一次跑出来的 141 张图,我发给朋友看。他说:“这不就是马赛克拼图吗?谁要啊。”

对。如果每张切片都把核心内容糊死,用户划过去直接划走了。你防住了搬运,也防住了点击。小红书那个信息流里,一张图片停留时间不到 0.5 秒。你必须在 0.5 秒内让人判断出“这资料有用”,但又拿不走全文。这个度,纯靠人眼看图调参数,我大概调了五轮才找到感觉。

遮挡位置:不是随机,是“诱惑”

纯随机遮挡块是最蠢的做法——可能把标题、图表关键轴标签、公式推导第一步全盖了,剩下些边角料。用户看不懂,自然不会点。正确的做法是盖住结论和完整数据,保留结构和诱饵。

比如一份 40 页的行业报告,我每页只遮挡三样东西:表格里的具体数值(留表头、留行名,让用户知道“这页有增长率对比”,但看不到数字);结论性语句的后半句(“基于以上分析,我们认为……”之后全糊,但前面留全);二维码、微信号(这个必须盖两层)。剩下那些段落标题、图表框架、过渡句,全部保留。用户看一眼就知道这一页在讲什么,甚至能脑补出大概逻辑,就是拿不到那个关键数字和联系方式。这个心理缺口,就是点击的动力。

实现上我写了一个 selective_cover() 函数,接收一个坐标列表,每个坐标带一个“保护等级”参数。等级 1(标题、图表框架)不遮挡;等级 2(过渡句)随机加 30% 透明度灰色遮罩;等级 3(数值、结论、二维码)强制两层遮挡加高斯模糊。调参那晚我盯着一个饼图看了十分钟——盖住扇形区的百分比标签,但保留色块和名称,用户知道“蓝色代表 A 市场”,但不知道具体占比。这才叫有效遮挡。

切片方式:让 AI 缝合失败

第一版切片用的是完美等分——3 行 3 列,每张图大小一模一样。后来发现有些 AI 缝合工具(比如 OpenCV 的 stitch 模块)能根据边缘像素相似度自动拼回去,尤其是纯色背景的文档截图,拼接成功率接近 80%。

解决办法是让切片之间重叠并且错位。我改了 slice_grid(),传入一个重叠率参数(默认 0.05,即 5% 重叠),并且每行切片在水平方向上随机偏移 10 到 30 像素。这样切出来的图片,边缘像素不完全对齐,缝合算法在特征点匹配阶段就会报错——它找不到足够多的匹配点。实测同一组图,完美等分切法能被缝合,加重叠加错位之后缝合失败率 100%。

代价是总切片数量增加了约 15%(因为重叠区域多切了几刀),47 页也从 141 张涨到 163 张,耗时多了不到 2 秒,能接受。

防搬运组合拳:三层保险

光靠遮挡和切片不够。我见过有人把切片图一张张下载再自己裁剪拼接的——虽然费劲,但真要偷你资料的人不在乎这点时间。所以我又加了两个东西。

随机水印:每张切片上随机位置加一行半透明文字,内容是当前切片的页码加随机字符串(比如“P12-7kf9”)。水印字体用 ImageFont.truetype() 加载一个极细的等线体,颜色跟背景相近(低对比度),不影响阅读但后期很难批量擦除——因为每张图的水印位置和字符串都不同,没法用固定遮罩一次性清掉。

切片顺序打乱:生成所有切片后,用一个种子随机打乱文件名前缀。用户下载后看到的顺序是乱的,想拼回原始页面布局得手动对每张图编号。大部分普通用户没这个耐心。

局部模糊:在二维码和微信号区域,不盖纯色方块,而是用 ImageFilter.GaussianBlur(radius=12) 做模糊。纯色方块一键去色就能还原(灰度图加阈值分割),但模糊后的区域信息被不可逆地混合了,基本没法恢复。这个坑我是在测试时踩的——某次用手机相册的“增强”功能,居然把纯色方块下的二维码轮廓给锐化出来了,扫了个半残。换成高斯模糊后,怎么锐化都没用。

这三层走完,一份资料包的预览图才算“能发”。这套逻辑写进代码只有不到 200 行,但参数调了差不多三个晚上。最终效果是:我把那 47 页的行业报告预览图发到小红书,三天后有人私信问我“这个报告怎么买”,也有几个人说“我试了试拼接,拼到一半放弃了太乱了”。后者就是我想看到的反应。

Python script obfuscation slicing preview

前文聊的是“为什么要防”和“防到什么程度”,现在直接上脚本

我用的还是 Pillow,没上 OpenCV——不是不能,是为了让复制这段代码的人少装一个库就能跑。

先说打码。大部分人想到的是直接画矩形,但矩形位置你得手填坐标,换一张截图就得改数字。我的方案是:先用 pytesseract 扫一遍图片里的文字区域,把包含“微信”、“VX”、“二维码”这些关键词的矩形框找出来,然后对这些区域做模糊处理。代码大概长这样:

from PIL import Image, ImageFilter
import pytesseract

def auto_blur_sensitive(img_path):
    img = Image.open(img_path)
    data = pytesseract.image_to_data(img, output_type=pytesseract.Output.DICT)
    for i in range(len(data['text'])):
        if data['text'][i].lower() in ['微信', 'vx', 'wechat']:
            x, y, w, h = data['left'][i], data['top'][i], data['width'][i], data['height'][i]
            crop = img.crop((x, y, x+w, y+h))
            blurred = crop.filter(ImageFilter.GaussianBlur(radius=15))
            img.paste(blurred, (x, y))
    return img

当然,遇到那种“二维码贴在角落、旁边没文字”的图,OCR 就歇菜了。这时候我备了一招:硬编码坐标白名单。在配置里写一组 [(100,200,150,250), ...],扫到这些区域直接干,不依赖文字识别。两种策略二选一,脚本启动时加个 --mode auto--mode manual 就行。

切片这块我走了弯路。一开始傻乎乎按固定高度切,结果遇到跨页的流程图,中间一刀把人家逻辑线切断了,预览图看起来像残废。后来改成“按内容边界切”——检测图片中的空白行或色块跳变,在那些位置下刀。Pillow 的 Image.getpixel() 配合行扫描能搞定:

def smart_slice(img, min_height=200):
    width, height = img.size
    cuts = []
    for y in range(height):
        row_colors = [img.getpixel((x, y)) for x in range(width)]
        if all(c == (255,255,255) for c in row_colors):
            cuts.append(y)
    pass

效率呢?一张 1920×1080 的截图,白色线条检测耗时不到 0.3 秒。47 页行业报告跑了大概 14 秒,比纯固定高度切多了 3 秒,但出来的切片不会把关键图表拦腰斩断。

水印我坚持随机。固定位置的水印等于没水印——人家截个图就裁掉了。我的写法是每张切片生成时,在四个象限里随机选一个,再随机偏移 10 到 50 像素,文字透明度设成 30%。这样即便有人拿到所有切片,想拼回去也得手动去掉每张图上位置不同的水印,工作量翻倍。

最后提醒一句:脚本跑完后记得人工抽检几张。Pillow 在处理某些压缩率高的 JPEG 时,getpixel() 返回的 RGB 值可能带噪点,导致白色行检测漏判。我吃过一次亏,某次批量处理完发现有两页没切片,因为那页的截图背景是浅灰色(#F8F8F8)而不是纯白。改阈值的时候顺手加了个宽容度参数 threshold=240,一切正常了。

这脚本写完我丢给两个朋友试用,反馈是“能跑,但第一次配 pytesseract 路径搞了十分钟”。

Python Pillow image obfuscation code

踩坑记录:打码太狠没人看,打码太少被白嫖

第一次跑通脚本的时候我挺兴奋的,直接把整页文字全部模糊掉——覆盖率 100%,心想这总没人能盗了吧。结果发小红书,三小时零互动。朋友说你这图跟抹了浆糊似的,谁看得见内容?用户点进来是想瞄一眼值不值得买,你全糊住人家凭什么下单。

第二次我学乖了,只遮关键数据行和结论段落,留七成正文可见。曝光确实翻倍了,但三天后隔壁群就有人拼出了完整版——他把六张切片按顺序拼回一张图,打码的位置就那么几处,用 PS 的内容识别填充随手就补上了。

打码密度和切片顺序是第一个坑。我后来改成随机跳序输出切片文件名:用 uuid4() 生成哈希前缀,打乱后上传。每张切片之间留 5% 的裁切重叠区域,但重叠部分用的模糊半径不一样——左半张用半径 15 的高斯模糊,右半张用半径 8。这样就算有人想拼接,相邻切片边缘对不上,得花大量人工对齐。

水印我也翻过车。一开始用半透明浮层,透明度设 10%,位置固定在左下角。结果发现手机版小红书会自动裁切边缘,水印直接没了。后来改用多层水印:一层浅色文字铺满全图(透明度 5%),两层小号水印随机撒在四个象限,再用 ImageDraw.text() 在图片的 EXIF 区域写入隐藏标识(通过 piexif 库写 UserComment 字段)。表面看就是一张普通预览图,但如果你右键保存然后用 exiftool 读,能查到我的自定义标记。这招对批量搬运工特别有效——他们通常只改文件名不清理元数据。

阈值问题折腾了我一晚上。Pillow 的 Image.getpixel() 在读取高压缩 JPEG 时,纯白区域可能抖成 (254, 254, 254) 甚至 (252, 253, 254)。我写的白色行检测最初写死 == (255, 255, 255),结果 50 张图漏切了 8 张。改成 all(c > 245 for c in row_colors) 后稳定了。

def is_blank_row(row_pixels, threshold=245):
    return all(p > threshold for p in row_pixels)

cuts = []
for y in range(height):
    row = [img.getpixel((x, y)) for x in range(width)]
    if is_blank_row([max(rgb) for rgb in row]):
        cuts.append(y)
merged = []
i = 0
while i < len(cuts):
    start = cuts[i]
    while i + 1 < len(cuts) and cuts[i+1] - cuts[i] < 5:
        i += 1
    merged.append((start, cuts[i]))
    i += 1

说句实在的——打码这事没有银弹。我试过动态密度:根据图片内容算熵值,信息量大的区域打码密度自动调高到 70%,空白区域只遮 10%。但算力成本上来了,一张图要多跑 0.8 秒。后来折中:对文字区域用 OCR 框定坐标后只遮关键词(比如“价格”“方案”“源码”),图表区域整块打码。脚本里加了个 --aggressive 参数,默认关掉,只有对付那种明知道会被二次转卖的爆款笔记才开。

切片打码搞完了,图也存了一堆,然后呢?

直接发原图肯定不行——你发什么人家就存什么,凭什么找你买完整版。预览图的目的是“看着有用,但拿不到全的”。我自己分三步走:发切片、留钩子、转到微信。

发笔记的时候埋好“缺口”

小红书对引流管得松还是紧,看命。2024 年那阵子封得凶,评论区带“微信”两个字直接限流。现在稍微好点,但也不能硬来。我一般正文里只放 3 到 4 张打码切片,最后一张图留个箭头指向评论区,图上写“完整版在评论区”。然后自己在评论区发一条:“需要完整资料包的姐妹私信我发‘资料’就好,自动回复。” 这条评论用小号置顶,大号别动——万一被举报,丢的是小号。

自动回复怎么设?小红书创作中心里有个“自动回复”功能,关键词匹配就能触发。我设的关键词是“资料包”、“完整版”、“PDF”,回复内容是一段话加一张带个人微信号的图。注意,图上的微信号用“vx: xxx”这种写法,别写全称,容易被 OCR 扫到。文字回复里写“请查看私信图片”,别直接在文字里扔微信号。

还有一种更稳的方式:建个小红书群聊。在笔记里挂群聊入口,说“群里已上传完整版资料包,进群自取”。群聊里可以发文件,也能发带有水印的完整版 PDF。用户进了群,你就有触达渠道了。群公告里挂你的微信号,写上“群文件失效请加微信领取”。这招的好处是——平台只监控笔记和私信,群聊内部相对宽松,但别发二维码,群聊里发二维码被截图的概率很高。

私域承接:别让用户等,等就跑了

用户私信你“资料”,自动回复弹了图片。但图片里的微信号,你得保证他扫了能加上。我踩过一个坑:用了个临时微信号,结果当天加人太多被风控了,新好友请求直接收不到。后来换成企业微信,加人上限高,而且可以设置“自动通过好友”和“欢迎语”。欢迎语里直接放完整版资料的百度网盘链接(带提取码),再附一句“还有更多行业模板持续更新,需要什么资料随时问我”。

定价呢?我卖的不是单个 PDF。单个卖 19.9,用户嫌贵。后来改成“资料包合集加社群年费”的模式:前 50 份免费送(其实是诱饵,让用户觉得占了便宜),然后拉进微信群,群里每两周更新一次资料,年费 99。愿意付 99 的人,才是真正的高价值用户。单个资料包 19.9,转化率大概 5%。社群年费 99,转化率虽然降到 2%,但客单价翻了 5 倍,而且后续不用再重复打码切片,省了很多事。

风险提示:别把鸡蛋放一个篮子里

小红书账号是有生命周期的。我见过一个号做到 5 万粉,发了一条带微信号的私信,直接永封。所以我的策略是:用矩阵号。主号只发内容,不引流。引流用小号,小号每天只回 20 条私信,超了第二天再说。这样就算小号被封,主号还在,换个引流号继续干。

这个流程里最花时间的不是写代码,是维护社群和更新资料。自动打码切片解放了你的双手,但“持续输出有价值的内容”这件事,机器帮不了你。别想着躺赚,没人买你的资料包是因为你只发了三张图,而是因为那三张图让人相信“这人手里有真东西”。

Xiaohongshu preview image private domain traffic

回头看我那套打码切片的代码,最初就是写死的

目录路径写死、打码区域全靠肉眼估、文件名直接落一个时间戳——这套流程我跑了不到三次就想摔键盘。每次换一套资料包,至少得翻四个变量,文件夹还得自己一个一个 mkdir,搞完眼睛都快瞎了。

所以后来加了个命令行参数入口。用 argparse--input--output--opacity(打码透明度,默认 0.6)、--grid(切片行列数,比如 3x4)。跑一次长这样:

python slice_mosaic.py --input ./素材 --output ./预览图 --opacity 0.7 --grid 3x4

参数传错了也不慌,加了 --dry-run 模式,只打印文件列表和切片尺寸,不动图。这个模式帮我省了好几次误操作——有一次把原图目录和输出目录设成同一个,幸亏先 dry-run 看了一眼。

再往后加了文件整理功能。输入目录里如果有多个 PDF 或图片文件夹,脚本会自动读文件名里的日期前缀(比如 2026-05_行业报告.pdf),按年月归类输出。不想按日期?那就加个 --tag 参数,传个主题词进去,输出目录名直接用这个 tag。写完之后我拿三批不同主题的资料包试了试,python slice_mosaic.py --input ./素材 --tag 金融模板,跑完自动生成 ./output_previews/金融模板/ 文件夹,里面按原文件名切好的预览图整整齐齐。

有些功能我一直想加但暂时没时间写——比如自动给预览图加一圈边框(类似拍立得白边),或者根据图片主色调自动生成封面文字底色。再比如多平台适配:小红书预览图是 3:4 竖版,知乎首图是 1:1 方形,B 站专栏头图是 16:9。同一个资料包输出三套尺寸,脚本里加个 --platform 参数就行,就是改切片后的拼接比例。

最让我觉得值的不是“防搬运”功能,而是那个批量处理。原来每套资料包手动打码切片,一套就得耗掉我 20 分钟,现在终端里敲一行命令,我就能甩手干别的了。听着电脑在那边吭哧吭哧地出图,我这边微信消息都回了好几轮。工具嘛,写出来就是给懒人用的,人一懒下来,才有空琢磨怎么把资料包卖得更贵。