用算法和爬虫搞副业:一个成都斜杠程序员的CV实战手记

Tomcat饲养员
2025-12-23 08:48
阅读 2108

上周五晚上十一点半,我瘫在椅子上盯着 VSCode 里一堆红得发亮的报错,心里直嘀咕:“这破项目再搞不定,下个月房租怕是要靠泡面续命了。”

没错,这就是我——一个坐标成都、白天写业务代码、晚上接外包副业的斜杠程序员。生活节奏是舒服,但钱包节奏就……你懂的。最近一个朋友介绍了个小活儿:帮一家本地宠物店做“自动识别流浪猫品种”的小程序。听起来简单?实则暗坑无数。今天就来聊聊这个从零开始的计算机视觉(CV)实战项目,顺便吐吐槽、晒晒代码。


为啥又搞 CV?被需求逼的

事情起源于上个月。那家宠物店老板想做个公益功能:路人拍张流浪猫照片上传,系统自动识别品种+是否绝育(通过耳标判断),然后推送附近的领养信息。听起来很有爱,对吧?但问题来了——他们没数据、没标注、没预算。

“我们有图!”老板说,“抖音、小红书、微博上一大堆!”

于是,爬虫 + 算法 的组合拳,成了唯一出路。

说实话,我主业是后端+全栈,CV 只是大学时摸过 OpenCV,工作后基本靠现学。但谁让成都的副业市场卷呢?不学点新东西,连外包单子都抢不到。于是咬牙开干:先爬数据,再训模型,最后部署上线。


第一步:爬虫不是你想爬,想爬就能爬

很多人以为爬虫就是 requests + BeautifulSoup 走天下。现实是:反爬机制越来越狠,尤其是小红书和微博这种平台,IP 封得比火锅底料还快。

我一开始用 Scrapy 写了个基础爬虫,目标是关键词“流浪猫”、“猫咪救助”下的图片。结果跑了一小时,IP 被封了三次,还触发了验证码滑块——那一刻我真的想砸键盘。

后来换了策略:

  1. 代理池轮换:用了免费代理 + 自建轻量级代理池(基于 Redis 队列)
  2. User-Agent 随机化:从真实浏览器 UA 列表里随机选
  3. 请求间隔随机化:避免固定频率被识别为机器人
  4. 图片直链提取:绕过页面渲染,直接解析 JSON 接口(比如小红书的 /api/sns/web/v1/note/...

关键代码片段如下(简化版):

import requests
import random
import time
from fake_useragent import UserAgent

ua = UserAgent()
proxies = get_random_proxy()  # 从代理池获取

headers = {
    "User-Agent": ua.random,
    "Referer": "https://www.xiaohongshu.com/"
}

def fetch_image_urls(keyword, page=1):
    url = f"https://edith.xiaohongshu.com/api/sns/web/v1/search/notes?keyword={keyword}&page={page}"
    try:
        resp = requests.get(url, headers=headers, proxies=proxies, timeout=10)
        data = resp.json()
        image_urls = []
        for note in data['data']['notes']:
            if 'images' in note:
                image_urls.extend(note['images'])
        return image_urls
    except Exception as e:
        print(f"爬取失败: {e}")
        return []

💡 经验之谈:别硬刚反爬!能走 API 就别解析 HTML,能模拟 App 请求就别用浏览器自动化(除非万不得已)。省下的时间够你多调两轮超参。

最终,我爬了约 1.2 万张图片,涵盖中华田园猫、英短、美短、布偶等主流品种。当然,也混进了不少狗、风景、甚至自拍……数据清洗成了下一关。


数据清洗:脏数据比 Bug 更可怕

原始数据里,有 30% 是无效图(非猫、模糊、截图等)。手动筛?不可能。我写了个多阶段过滤脚本:

  1. 用预训练模型初筛:加载 torchvision 的 ResNet50(ImageNet 预训练),只保留预测类别为“cat”(class_id=281)的图片。
  2. 去重:用 perceptual hash(pHash)计算图像指纹,相似度 >95% 的视为重复。
  3. 分辨率过滤:低于 300x300 的直接丢弃。

这部分代码用了 OpenCV + PIL + torchvision,跑在本地 Docker 容器里,省得污染我的主力开发环境(VSCode 插件已经装到 87 个,不能再乱了)。

清洗后剩下约 6000 张有效图,按品种分文件夹。但问题又来了:类别极度不平衡。中华田园猫占 70%,布偶猫只有 200 张。直接训?模型肯定偏科。

解决方案:过采样 + 数据增强

  • 对少数类(如布偶、暹罗)做旋转、裁剪、色彩抖动
  • 使用 torchvision.transforms 构建动态增强 pipeline
  • 训练时启用 WeightedRandomSampler,让每个 batch 中各类别比例接近 1:1
from torch.utils.data import WeightedRandomSampler

# 计算每个类别的权重
class_counts = [4200, 800, 600, 200, 200]  # 示例
weights = 1. / torch.tensor(class_counts, dtype=torch.float)
samples_weights = weights[target_labels]

sampler = WeightedRandomSampler(samples_weights, len(samples_weights))
train_loader = DataLoader(dataset, batch_size=32, sampler=sampler)

模型选型:别一上来就上 YOLOv8

很多新手一听说 CV,立刻想到 YOLO、Detectron2 这些大杀器。但在这个项目里,任务只是图像分类(不是检测,也不是分割),而且部署环境是微信小程序后端(资源有限)。

我试了三种方案:

模型 参数量 准确率(val) 推理速度(CPU) 是否适合部署
ResNet18 11M 82.3% 45ms
EfficientNet-B0 5.3M 84.1% 38ms ✅✅
ViT-Tiny 5.7M 80.7% 120ms

结论很明显:EfficientNet-B0 性价比最高。轻量、准确、推理快,还支持 ONNX 导出。

训练过程用了 PyTorch Lightning,省去大量 boilerplate 代码。关键配置:

model:
  backbone: efficientnet_b0
  num_classes: 5
  pretrained: true

training:
  epochs: 50
  lr: 0.001
  scheduler: CosineAnnealingLR
  augmentations:
    - RandomHorizontalFlip
    - ColorJitter(brightness=0.2, contrast=0.2)
    - RandomRotation(degrees=15)

跑了 12 小时(RTX 3060 笔记本),最终 val 准确率 86.4%,混淆矩阵显示“中华田园猫”偶尔会被误判为“美短”,但业务可接受——毕竟用户要的是“大致品种”,不是基因检测。


部署上线:别让运维背锅

模型训好了,怎么上线?客户要求:API 响应 <1s,月成本 <200 元

我放弃了 Flask + Gunicorn 的老套路(太重),改用 FastAPI + Uvicorn + ONNX Runtime

  • 模型导出为 ONNX 格式,体积从 15MB 压到 8MB
  • ONNX Runtime 推理速度比原生 PyTorch 快 2.3 倍(实测)
  • 用 Docker 打包,部署到腾讯云轻量服务器(成都区,延迟低)

Dockerfile 特意精简:

FROM python:3.9-slim
RUN pip install onnxruntime==1.15.1 fastapi uvicorn pillow
COPY app.py model.onnx ./ 
EXPOSE 8000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

上线当天,测试同学提了个 bug:“上传黑猫,返回‘未知品种’”。查日志发现是光照太暗导致特征提取失败。临时加了个亮度校正预处理

def adjust_brightness(img: Image.Image) -> Image.Image:
    hsv = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2HSV)
    h, s, v = cv2.split(hsv)
    v = cv2.add(v, 30)  # 提亮
    v = np.clip(v, 0, 255)
    final_hsv = cv2.merge((h, s, v))
    return Image.fromarray(cv2.cvtColor(final_hsv, cv2.COLOR_HSV2RGB))

搞定!现在系统每天处理 300+ 请求,准确率稳定在 85% 左右。老板说下周要加“耳标检测”功能……唉,副业真是永无止境。


复盘:算法不是万能,但没算法万万不能

这次项目让我深刻体会到:CV 落地 = 30% 算法 + 70% 工程 + 100% 耐心

  • 爬虫是数据源头,但法律边界要小心(只用于公益、不商用、不存原始图)
  • 模型不必最先进,但要最适合场景(轻量、快、稳)
  • 部署要考虑成本和维护性,别为了炫技堆复杂架构

顺便吐槽一句:产品经理总以为“AI 能自动搞定一切”,殊不知光是清洗那 6000 张图,我就熬了两个通宵。不过看到流浪猫被成功领养的消息推送出去时,又觉得值了。


给想搞 CV 副业的朋友几点建议

  1. 从小场景切入:别一上来就想做人脸识别、自动驾驶。像“猫品种识别”这种垂直需求,反而更容易变现。
  2. 善用预训练模型:Hugging Face、TorchVision 里的模型,90% 的场景够用。
  3. 爬虫要合规:遵守 robots.txt,控制请求频率,别惹官司。
  4. 重视数据质量:Garbage in, garbage out。宁可少而精,不要多而烂。
  5. 工具链要顺手:我的 VSCode 里装了 Python Docstring Generator、Pylance、Docker、Remote - SSH……效率翻倍。

最后,如果你也在成都,接外包、搞副业、或者单纯想聊技术,欢迎来找我喝杯盖碗茶。反正这边生活节奏慢,代码可以慢慢调,bug 可以慢慢修——只要 deadline 别卡在周五晚上就行 😅

(完)

评论 0

最热最新
暂无评论
Tomcat饲养员Lv.1
0
影响力
0
文章
0
粉丝