作为一个喜欢折腾的独立开发者,我的GitHub上躺着无数个写到一半就放弃的项目。但最近,有个小工具我坚持做完了,还真的上线跑了起来——一个帮助设计师朋友下载高清素材的小工具。
做这个项目的初衷很简单:朋友需要,我正好会。但真正动手后才发现,一个看似简单的"下载器",背后涉及的技术点远比想象中复杂。这篇文章就记录一下过程中的一些思考。
一、为什么是Flask?
决定做这个项目时,我面临第一个选择:用什么框架?
Django太重,Node.js我不太熟,FastAPI当时还在早期。最后选了Flask,理由如下:
- 轻量:项目功能单一,不需要ORM、后台管理等复杂功能
- 灵活:可以自由选择第三方库
- 学习成本低:一个文件就能跑起来
项目结构很简单:
flickr-downloader/
├── app.py 主程序
├── requirements.txt 依赖
├── static/ CSS、JS文件
├── templates/ HTML模板
└── utils/ 工具函数
├── parser.py 解析逻辑
├── proxy.py 代理相关
└── cache.py 缓存管理
二、解析逻辑的演进
2.1 第一版:正则表达式硬核解析
最开始,我天真的以为Flickr的视频链接就在页面源码里躺着等我。于是写了这样的代码:
import re
def parse_v1(url):
html = requests.get(url).text
自以为聪明的正则
pattern = r'https:\\/\\/live\.staticflickr\.com\\/video\\/[^"]+\.mp4'
urls = re.findall(pattern, html)
return [u.replace('\\', '') for u in urls]
跑起来发现:有时候能抓到,有时候抓不到,完全没有规律。
2.2 第二版:寻找数据源头
后来发现,Flickr的数据是通过JSON格式嵌入在<script>标签里的。改进后的代码:
import json
import re
def parse_v2(url):
html = requests.get(url).text
找到包含视频信息的script标签
match = re.search(r'<script[^>]+data-component-props="([^"]+)"', html)
if match:
data_str = match.group(1).replace('"', '"')
data = json.loads(data_str)
递归查找所有包含视频URL的字段
return extract_video_urls(data)
备用方案:另一种数据结构
match = re.search(r'photoInfo\s=\s({.+?});', html)
if match:
data = json.loads(match.group(1))
return extract_video_urls(data)
这里有个坑:JSON字符串是HTML转义过的,需要先反转义。

2.3 第三版:处理反爬
解决了数据提取,新的问题来了:IP被封。
写了个简单的代理轮换:
import random
import requests
class ProxyManager:
def __init__(self):
self.proxies = []
self.current = 0
def add_proxy(self, proxy):
self.proxies.append(proxy)
def get(self):
if not self.proxies:
return None
随机选择一个代理
proxy = random.choice(self.proxies)
return {
'http': f'http://{proxy}',
'https': f'http://{proxy}'
}
def remove(self, proxy):
if proxy in self.proxies:
self.proxies.remove(proxy)
三、前端那些事
我是个后端,前端水平停留在"能看懂"的程度。但这个小工具的前端,我坚持自己写。
3.1 设计原则
- 极简:只有一个输入框和一个按钮
- 反馈明确:解析中、成功、失败都有清晰提示
- 移动端适配:朋友可能在手机上看素材
3.2 技术实现
用了最基础的Bootstrap + 原生JavaScript,没有框架:
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-8">
<h1 class="text-center mb-4">Flickr视频解析</h1>
<div class="input-group mb-3">
<input type="url" id="urlInput" class="form-control"
placeholder="输入Flickr视频链接">
<button class="btn btn-primary" id="parseBtn">解析</button>
</div>
<div id="result" class="mt-4"></div>
</div>
</div>
</div>
<script>
document.getElementById('parseBtn').onclick = async () => {
const url = document.getElementById('urlInput').value;
const resultDiv = document.getElementById('result');
if (!url.includes('flickr.com')) {
resultDiv.innerHTML = '<div class="alert alert-danger">请输入正确的Flickr链接</div>';
return;
}
resultDiv.innerHTML = '<div class="alert alert-info">解析中,请稍候...</div>';
try {
const res = await fetch('/parse', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({url})
});
const data = await res.json();
if (data.success) {
let html = '<div class="list-group">';
data.videos.forEach(v => {
html += `<a href="${v.url}" class="list-group-item list-group-item-action" target="_blank">
下载 ${v.quality} 格式
</a>`;
});
html += '</div>';
resultDiv.innerHTML = html;
} else {
resultDiv.innerHTML = `<div class="alert alert-danger">${data.error}</div>`;
}
} catch (err) {
resultDiv.innerHTML = '<div class="alert alert-danger">网络错误,请重试</div>';
}
};
</script>
四、部署过程中的坑
4.1 编码问题
服务器是Ubuntu,默认编码是ASCII。解析包含中文的页面时报错:
解决方案
import sys
reload(sys)
sys.setdefaultencoding('utf8') Python2
Python3中这样处理
import locale
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
4.2 超时设置
有些视频页面加载慢,需要设置合理的超时:
不要用默认超时
response = requests.get(url, timeout=(3.05, 10))
添加重试机制
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
session = requests.Session()
retry = Retry(total=3, backoff_factor=1)
adapter = HTTPAdapter(max_retries=retry)
session.mount('http://', adapter)
session.mount('https://', adapter)
4.3 内存泄漏
最初版本有个严重问题:缓存只增不减。后来加了过期机制:
from collections import OrderedDict
from time import time
class TimedCache(OrderedDict):
def __init__(self, maxsize=100, timeout=3600):
super().__init__()
self.maxsize = maxsize
self.timeout = timeout
def __getitem__(self, key):
val = super().__getitem__(key)
if time() - val['time'] > self.timeout:
del self[key]
raise KeyError(key)
return val['data']
def __setitem__(self, key, val):
if len(self) >= self.maxsize:
self.popitem(last=False)
super().__setitem__(key, {'data': val, 'time': time()})
五、上线后的维护
5.1 日志监控
写了个简单的日志分析脚本:
import re
from collections import Counter
def analyze_logs(logfile):
with open(logfile) as f:
logs = f.readlines()
统计请求来源
referers = []
for line in logs:
match = re.search(r'"referer": "([^"]+)"', line)
if match:
referers.append(match.group(1))
print(Counter(referers).most_common(10))
5.2 异常报警
用企业微信机器人做了简单的报警:
import requests
def send_alert(msg):
webhook = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx'
data = {
"msgtype": "text",
"text": {"content": f"【解析工具异常】{msg}"}
}
requests.post(webhook, json=data)
六、一点感悟
做这个项目最大的收获不是技术本身,而是:
- 先上线,再优化:第一版代码很烂,但能跑。跑起来才有改进的动力
- 用户反馈很重要:朋友说"这个按钮不好找",我第二天就改了布局
- 技术要为需求服务:不用追求最新技术栈,能解决问题就行
项目地址在这里,欢迎体验:Flickr视频解析工具
如果你也在做类似的小工具,或者有什么问题想交流,欢迎留言。一起进步。
浙公网安备 33010602011771号