作为一个喜欢折腾的独立开发者,我的GitHub上躺着无数个写到一半就放弃的项目。但最近,有个小工具我坚持做完了,还真的上线跑了起来——一个帮助设计师朋友下载高清素材的小工具。

做这个项目的初衷很简单:朋友需要,我正好会。但真正动手后才发现,一个看似简单的"下载器",背后涉及的技术点远比想象中复杂。这篇文章就记录一下过程中的一些思考。

一、为什么是Flask?

决定做这个项目时,我面临第一个选择:用什么框架?

Django太重,Node.js我不太熟,FastAPI当时还在早期。最后选了Flask,理由如下:

  1. 轻量:项目功能单一,不需要ORM、后台管理等复杂功能
  2. 灵活:可以自由选择第三方库
  3. 学习成本低:一个文件就能跑起来

项目结构很简单:

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('&quot;', '"')
        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转义过的,需要先反转义。

flickr_pic (8) low

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 设计原则

  1. 极简:只有一个输入框和一个按钮
  2. 反馈明确:解析中、成功、失败都有清晰提示
  3. 移动端适配:朋友可能在手机上看素材

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)

六、一点感悟

做这个项目最大的收获不是技术本身,而是:

  1. 先上线,再优化:第一版代码很烂,但能跑。跑起来才有改进的动力
  2. 用户反馈很重要:朋友说"这个按钮不好找",我第二天就改了布局
  3. 技术要为需求服务:不用追求最新技术栈,能解决问题就行

项目地址在这里,欢迎体验:Flickr视频解析工具

如果你也在做类似的小工具,或者有什么问题想交流,欢迎留言。一起进步。

posted on 2026-02-28 10:22  yqqwe  阅读(2)  评论(0)    收藏  举报