摘要: 作为一个经常需要收集素材的程序员,我开发了一个在线的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
七、经验教训
回顾整个开发过程,有几点体会:
-
先跑通核心流程:不要一开始就想把所有功能都做完,先让用户能用起来
-
重视异常处理:网络请求什么意外都可能发生,try-catch要写到位
-
日志很重要:没有日志,线上出了问题都不知道从哪里查
-
适度缓存:缓存可以提高性能,但也要考虑数据更新的及时性
-
关注用户体验:加载状态、错误提示这些细节决定用户愿不愿意用第二次
八、后续计划
接下来准备增加的功能:
- 批量解析:一次处理多个链接
- 历史记录:保存用户的解析记录
- 浏览器插件:不用打开网站就能用
- API开放:给其他开发者调用
希望我的经验对大家有帮助。如果你也在做类似的项目,欢迎交流讨论。

浙公网安备 33010602011771号