摘要: 作为一个经常需要收集素材的程序员,我开发了一个在线的Flickr视频下载工具。本文将分享整个开发过程中的技术细节、遇到的坑以及解决方案,希望对同样在做工具类网站的朋友有帮助。

一、需求的诞生

上个月,我在做一个视频剪辑项目,需要从Flickr下载一些高清素材作为片头背景。Flickr上有大量CC协议的优质视频,但官方并没有提供直接的下载按钮。

试了几个网上的下载工具,要么收费,要么广告满天飞,还有的下载下来是带水印的低清版本。作为一个程序员,与其忍受这些,不如自己动手写一个。

于是,我开始了这个项目的开发。

二、技术架构设计

2.1 整体架构

项目采用前后端分离架构:

用户请求 → Nginx → Gunicorn → Flask应用 → 解析服务 → 返回结果
                   ↓
                Redis缓存

2.2 技术选型说明

  • 后端框架:Flask 2.0

    • 轻量灵活,适合小型API服务
    • 扩展丰富,可以按需引入功能
  • 前端:原生JavaScript + Bootstrap 5

    • 避免框架臃肿,加快首屏加载
    • Bootstrap保证基础样式可用
  • 缓存:Redis

    • 存储解析结果,减少重复请求
    • 存储IP请求频率记录
  • 部署:阿里云轻量应用服务器 + Docker
    flickr_pic (1) low

三、核心功能实现

3.1 视频地址解析模块

这是整个项目的核心。Flickr的视频地址并不是直接写在HTML里,而是通过JavaScript动态加载。

经过抓包分析,我发现视频信息藏在这样一个结构里:

import requests
import re
import json

class FlickrParser:
    def __init__(self):
        self.session = requests.Session()
        self.session.headers.update({
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
        })
    
    def parse(self, flickr_url):
         第一步:获取页面内容
        response = self.session.get(flickr_url)
        html = response.text
        
         第二步:提取JSON数据
         方法1:从script标签中提取
        pattern = r'<script type="application/ld\+json">(.?)</script>'
        scripts = re.findall(pattern, html, re.DOTALL)
        
        for script in scripts:
            try:
                data = json.loads(script)
                video_url = self._extract_video_from_json(data)
                if video_url:
                    return video_url
            except:
                continue
        
         方法2:从photoInfo变量中提取
        pattern = r'photoInfo\s=\s({.?});'
        match = re.search(pattern, html, re.DOTALL)
        if match:
            data = json.loads(match.group(1))
            return self._extract_video_urls(data)
        
        return None
    
    def _extract_video_urls(self, data):
        """递归提取视频URL"""
        urls = {}
        
        def find_videos(obj, path=''):
            if isinstance(obj, dict):
                 检查是否是视频URL
                if 'url' in obj and '.mp4' in str(obj.get('url', '')):
                    quality = obj.get('label', obj.get('width', 'unknown'))
                    urls[quality] = obj['url']
                
                 递归遍历
                for k, v in obj.items():
                    find_videos(v, f"{path}.{k}")
            
            elif isinstance(obj, list):
                for i, item in enumerate(obj):
                    find_videos(item, f"{path}[{i}]")
        
        find_videos(data)
        return urls

3.2 API接口设计

采用RESTful风格设计API:

from flask import Flask, request, jsonify
from flask_cors import CORS
import validators

app = Flask(__name__)
CORS(app)   允许跨域请求

@app.route('/api/parse', methods=['POST'])
def parse_video():
    """解析视频接口"""
    try:
        data = request.get_json()
        url = data.get('url', '').strip()
        
         参数验证
        if not url:
            return jsonify({'error': '请提供URL'}), 400
        
        if not validators.url(url):
            return jsonify({'error': 'URL格式不正确'}), 400
        
        if 'flickr.com' not in url:
            return jsonify({'error': '只支持Flickr链接'}), 400
        
         检查缓存
        cache_key = f"flickr:{hash(url)}"
        cached = redis_client.get(cache_key)
        if cached:
            return jsonify(json.loads(cached))
        
         解析视频
        parser = FlickrParser()
        result = parser.parse(url)
        
        if not result:
            return jsonify({'error': '未找到视频资源'}), 404
        
         存入缓存(24小时)
        response_data = {
            'success': True,
            'videos': result,
            'url': url
        }
        redis_client.setex(cache_key, 86400, json.dumps(response_data))
        
        return jsonify(response_data)
    
    except Exception as e:
        app.logger.error(f"解析失败: {str(e)}")
        return jsonify({'error': '服务器内部错误'}), 500

@app.route('/api/status')
def status():
    """健康检查接口"""
    return jsonify({
        'status': 'running',
        'version': '1.0.0'
    })

3.3 前端页面实现

为了提升用户体验,前端需要做到即时反馈:

// main.js
class FlickrDownloader {
    constructor() {
        this.form = document.getElementById('parseForm');
        this.input = document.getElementById('flickrUrl');
        this.button = document.getElementById('submitBtn');
        this.result = document.getElementById('result');
        this.error = document.getElementById('error');
        
        this.init();
    }
    
    init() {
        this.form.addEventListener('submit', (e) => {
            e.preventDefault();
            this.parse();
        });
    }
    
    async parse() {
        const url = this.input.value.trim();
        
        // 基础验证
        if (!url) {
            this.showError('请输入Flickr链接');
            return;
        }
        
        if (!url.includes('flickr.com')) {
            this.showError('请输入有效的Flickr链接');
            return;
        }
        
        // 显示加载状态
        this.setLoading(true);
        this.hideError();
        this.hideResult();
        
        try {
            const response = await fetch('/api/parse', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({ url })
            });
            
            const data = await response.json();
            
            if (!response.ok) {
                throw new Error(data.error || '解析失败');
            }
            
            if (data.success) {
                this.showResult(data.videos);
            } else {
                this.showError(data.error || '未找到视频');
            }
        } catch (error) {
            this.showError(error.message);
        } finally {
            this.setLoading(false);
        }
    }
    
    showResult(videos) {
        const videosList = Object.entries(videos).map(([quality, url]) => {
            return `
                <div class="video-item">
                    <span class="quality">${quality}</span>
                    <a href="${url}" target="_blank" class="btn-download">
                        点击下载
                    </a>
                </div>
            `;
        }).join('');
        
        this.result.innerHTML = `
            <h3>解析成功!</h3>
            <p>请选择视频质量下载:</p>
            <div class="video-list">
                ${videosList}
            </div>
        `;
        this.result.style.display = 'block';
    }
    
    showError(message) {
        this.error.textContent = message;
        this.error.style.display = 'block';
    }
    
    hideError() {
        this.error.style.display = 'none';
    }
    
    hideResult() {
        this.result.style.display = 'none';
    }
    
    setLoading(isLoading) {
        if (isLoading) {
            this.button.disabled = true;
            this.button.innerHTML = '<span class="spinner"></span> 解析中...';
        } else {
            this.button.disabled = false;
            this.button.textContent = '解析视频';
        }
    }
}

// 初始化
new FlickrDownloader();

四、性能优化措施

4.1 缓存策略

为了减轻对Flickr的压力,我实现了多层缓存:

class CacheManager:
    def __init__(self):
        self.redis_client = redis.Redis(
            host='localhost',
            port=6379,
            decode_responses=True
        )
    
    def get_video_info(self, url):
         1. 先从Redis查询
        cache_key = f"video:{hash(url)}"
        cached = self.redis_client.get(cache_key)
        
        if cached:
            return json.loads(cached)
        
         2. 缓存不存在,解析
        parser = FlickrParser()
        result = parser.parse(url)
        
        if result:
             存入缓存,24小时过期
            self.redis_client.setex(
                cache_key, 
                86400, 
                json.dumps(result)
            )
        
        return result
    
    def rate_limit(self, ip):
        """IP限流,每小时最多100次请求"""
        key = f"rate:{ip}:{datetime.now().strftime('%Y%m%d%H')}"
        count = self.redis_client.incr(key)
        
        if count == 1:
            self.redis_client.expire(key, 3600)
        
        return count <= 100

4.2 静态资源优化

 Nginx配置
location ~ \.(jpg|jpeg|png|gif|ico|css|js)$ {
    expires 30d;
    add_header Cache-Control "public, immutable";
}

 Gzip压缩
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

五、部署与运维

5.1 Docker部署

FROM python:3.9-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

 创建非root用户
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser

CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]

5.2 docker-compose配置

version: '3.8'

services:
  web:
    build: .
    ports:
      - "5000:5000"
    environment:
      - REDIS_HOST=redis
    depends_on:
      - redis
    restart: unless-stopped
  
  redis:
    image: redis:6-alpine
    volumes:
      - redis_data:/data
    restart: unless-stopped

volumes:
  redis_data:

5.3 日志监控

import logging
from logging.handlers import RotatingFileHandler

 配置日志
handler = RotatingFileHandler(
    'app.log',
    maxBytes=10485760,   10MB
    backupCount=5
)
handler.setFormatter(logging.Formatter(
    '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
))

app.logger.addHandler(handler)
app.logger.setLevel(logging.INFO)

六、项目总结

经过两周的开发调试,这个Flickr下载工具终于上线了。目前运行稳定,每天服务几百个用户。

一些数据:

  • 平均解析时间:1.2秒
  • 缓存命中率:65%
  • 成功率:92%

项目地址:https://twittervideodownloaderx.com/flickr_downloader_cn

七、经验教训

回顾整个开发过程,有几点体会:

  1. 先跑通核心流程:不要一开始就想把所有功能都做完,先让用户能用起来

  2. 重视异常处理:网络请求什么意外都可能发生,try-catch要写到位

  3. 日志很重要:没有日志,线上出了问题都不知道从哪里查

  4. 适度缓存:缓存可以提高性能,但也要考虑数据更新的及时性

  5. 关注用户体验:加载状态、错误提示这些细节决定用户愿不愿意用第二次

八、后续计划

接下来准备增加的功能:

  1. 批量解析:一次处理多个链接
  2. 历史记录:保存用户的解析记录
  3. 浏览器插件:不用打开网站就能用
  4. API开放:给其他开发者调用

希望我的经验对大家有帮助。如果你也在做类似的项目,欢迎交流讨论。

posted on 2026-02-27 09:10  yqqwe  阅读(1)  评论(0)    收藏  举报