Python基于Flask構(gòu)建一個(gè)URL書簽管理工具
1. 引言
在信息爆炸的時(shí)代,我們每天都會(huì)接觸到大量有價(jià)值的網(wǎng)頁(yè)和在線資源。瀏覽器自帶的書簽功能雖然方便,但存在諸多局限性:難以跨設(shè)備同步、搜索功能薄弱、缺乏分類和組織能力、無(wú)法添加詳細(xì)的備注信息等。據(jù)統(tǒng)計(jì),普通互聯(lián)網(wǎng)用戶平均擁有數(shù)百個(gè)書簽,但其中超過(guò)60%的書簽很少被再次訪問(wèn),部分原因是難以在需要時(shí)快速找到。
基于Web的URL書簽管理工具應(yīng)運(yùn)而生,它解決了傳統(tǒng)書簽管理的痛點(diǎn),提供了更強(qiáng)大的功能:
- 集中存儲(chǔ):所有書簽存儲(chǔ)在云端,隨時(shí)隨地訪問(wèn)
- 強(qiáng)大的搜索:支持全文搜索、標(biāo)簽搜索、分類搜索
- 智能分類:支持文件夾、標(biāo)簽、收藏等級(jí)等多維度組織
- 跨平臺(tái)同步:在任何設(shè)備上都能訪問(wèn)完整的書簽庫(kù)
- 社交功能:分享書簽、發(fā)現(xiàn)他人收藏的有用資源
本文將詳細(xì)介紹如何使用Python的Flask框架構(gòu)建一個(gè)功能完整的URL書簽管理工具。我們將從基礎(chǔ)架構(gòu)開(kāi)始,逐步實(shí)現(xiàn)用戶認(rèn)證、書簽CRUD、搜索過(guò)濾、數(shù)據(jù)導(dǎo)入導(dǎo)出等核心功能,最終打造一個(gè)生產(chǎn)可用的Web應(yīng)用。
2. 技術(shù)棧與架構(gòu)設(shè)計(jì)
2.1 技術(shù)棧選擇
后端技術(shù):
- Flask:輕量級(jí)Web框架,靈活且易于擴(kuò)展
- SQLAlchemy:Python SQL工具包和ORM
- Flask-Login:用戶會(huì)話管理
- Flask-WTF:表單處理和驗(yàn)證
- Bcrypt:密碼哈希加密
前端技術(shù):
- Jinja2:模板引擎
- Bootstrap 5:響應(yīng)式UI框架
- Font Awesome:圖標(biāo)庫(kù)
- jQuery:DOM操作和AJAX
數(shù)據(jù)庫(kù):
- SQLite(開(kāi)發(fā)環(huán)境)
- PostgreSQL(生產(chǎn)環(huán)境)
2.2 系統(tǒng)架構(gòu)

2.3 數(shù)據(jù)庫(kù)設(shè)計(jì)
系統(tǒng)主要包含以下數(shù)據(jù)模型:
- User:用戶信息
- Bookmark:書簽核心信息
- Tag:標(biāo)簽系統(tǒng)
- BookmarkTag:書簽與標(biāo)簽的關(guān)聯(lián)表
關(guān)系模型可以用以下公式表示:

3. 項(xiàng)目設(shè)置與環(huán)境配置
3.1 創(chuàng)建項(xiàng)目結(jié)構(gòu)
首先創(chuàng)建標(biāo)準(zhǔn)的Flask項(xiàng)目目錄結(jié)構(gòu):
bookmark_manager/
├── app/
│ ├── __init__.py
│ ├── models.py
│ ├── routes.py
│ ├── forms.py
│ ├── templates/
│ │ ├── base.html
│ │ ├── index.html
│ │ ├── auth/
│ │ │ ├── login.html
│ │ │ └── register.html
│ │ └── bookmarks/
│ │ ├── list.html
│ │ ├── add.html
│ │ ├── edit.html
│ │ └── detail.html
│ ├── static/
│ │ ├── css/
│ │ ├── js/
│ │ └── images/
│ └── utils/
│ ├── __init__.py
│ ├── helpers.py
│ └── validators.py
├── migrations/
├── tests/
├── config.py
├── requirements.txt
└── run.py
3.2 環(huán)境配置與依賴安裝
創(chuàng)建requirements.txt文件:
Flask==2.3.3 Flask-SQLAlchemy==3.0.5 Flask-Login==0.6.3 Flask-WTF==1.1.1 Flask-Migrate==4.0.4 WTForms==3.0.1 email-validator==2.0.0 bcrypt==4.0.1 requests==2.31.0 beautifulsoup4==4.12.2 python-dotenv==1.0.0
安裝依賴:
pip install -r requirements.txt
3.3 配置文件
創(chuàng)建config.py配置文件:
# config.py
import os
from datetime import timedelta
class Config:
"""基礎(chǔ)配置類"""
# 安全密鑰
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'
# 數(shù)據(jù)庫(kù)配置
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///bookmarks.db'
SQLALCHEMY_TRACK_MODIFICATIONS = False
# 會(huì)話配置
PERMANENT_SESSION_LIFETIME = timedelta(days=7)
# 文件上傳配置
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB max file size
# 應(yīng)用配置
BOOKMARKS_PER_PAGE = 20
ENABLE_URL_PREVIEW = True
# 郵件配置(可選)
MAIL_SERVER = os.environ.get('MAIL_SERVER')
MAIL_PORT = int(os.environ.get('MAIL_PORT') or 587)
MAIL_USE_TLS = True
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
class DevelopmentConfig(Config):
"""開(kāi)發(fā)環(huán)境配置"""
DEBUG = True
SQLALCHEMY_ECHO = True
class ProductionConfig(Config):
"""生產(chǎn)環(huán)境配置"""
DEBUG = False
# 生產(chǎn)環(huán)境必須設(shè)置SECRET_KEY
SECRET_KEY = os.environ.get('SECRET_KEY')
# 生產(chǎn)環(huán)境使用PostgreSQL
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
class TestingConfig(Config):
"""測(cè)試環(huán)境配置"""
TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
# 配置映射
config = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'testing': TestingConfig,
'default': DevelopmentConfig
}
4. 數(shù)據(jù)庫(kù)模型設(shè)計(jì)
4.1 核心模型實(shí)現(xiàn)
創(chuàng)建app/models.py文件:
# app/models.py
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin
from datetime import datetime
import bcrypt
from sqlalchemy import event
from sqlalchemy.ext.hybrid import hybrid_property
db = SQLAlchemy()
# 書簽標(biāo)簽關(guān)聯(lián)表(多對(duì)多關(guān)系)
bookmark_tags = db.Table('bookmark_tags',
db.Column('bookmark_id', db.Integer, db.ForeignKey('bookmark.id'), primary_key=True),
db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'), primary_key=True),
db.Column('created_at', db.DateTime, default=datetime.utcnow)
)
class User(UserMixin, db.Model):
"""用戶模型"""
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, nullable=False, index=True)
email = db.Column(db.String(120), unique=True, nullable=False, index=True)
password_hash = db.Column(db.String(128), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
last_login = db.Column(db.DateTime)
is_active = db.Column(db.Boolean, default=True)
# 關(guān)系
bookmarks = db.relationship('Bookmark', backref='owner', lazy='dynamic',
cascade='all, delete-orphan')
tags = db.relationship('Tag', backref='owner', lazy='dynamic',
cascade='all, delete-orphan')
def set_password(self, password):
"""設(shè)置密碼哈希"""
self.password_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
def check_password(self, password):
"""驗(yàn)證密碼"""
return bcrypt.checkpw(password.encode('utf-8'), self.password_hash.encode('utf-8'))
def get_bookmarks_count(self):
"""獲取用戶書簽數(shù)量"""
return self.bookmarks.count()
def get_tags_count(self):
"""獲取用戶標(biāo)簽數(shù)量"""
return self.tags.count()
def __repr__(self):
return f'<User {self.username}>'
class Bookmark(db.Model):
"""書簽?zāi)P?""
__tablename__ = 'bookmark'
id = db.Column(db.Integer, primary_key=True)
url = db.Column(db.String(2048), nullable=False, index=True)
title = db.Column(db.String(512), nullable=False, index=True)
description = db.Column(db.Text)
favicon = db.Column(db.String(512))
is_public = db.Column(db.Boolean, default=False)
click_count = db.Column(db.Integer, default=0)
created_at = db.Column(db.DateTime, default=datetime.utcnow, index=True)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# 外鍵
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False, index=True)
# 關(guān)系
tags = db.relationship('Tag', secondary=bookmark_tags, lazy='dynamic',
backref=db.backref('bookmarks', lazy='dynamic'))
@hybrid_property
def domain(self):
"""從URL提取域名"""
from urllib.parse import urlparse
parsed_url = urlparse(self.url)
return parsed_url.netloc
def increment_click_count(self):
"""增加點(diǎn)擊計(jì)數(shù)"""
self.click_count += 1
db.session.commit()
def to_dict(self):
"""轉(zhuǎn)換為字典(用于API)"""
return {
'id': self.id,
'url': self.url,
'title': self.title,
'description': self.description,
'domain': self.domain,
'is_public': self.is_public,
'click_count': self.click_count,
'created_at': self.created_at.isoformat(),
'tags': [tag.name for tag in self.tags]
}
def __repr__(self):
return f'<Bookmark {self.title}>'
class Tag(db.Model):
"""標(biāo)簽?zāi)P?""
__tablename__ = 'tag'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), nullable=False, index=True)
color = db.Column(db.String(7), default='#6c757d') # Bootstrap secondary color
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# 外鍵
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False, index=True)
# 唯一約束:同一用戶的標(biāo)簽名必須唯一
__table_args__ = (db.UniqueConstraint('user_id', 'name', name='unique_tag_per_user'),)
@hybrid_property
def bookmarks_count(self):
"""獲取標(biāo)簽關(guān)聯(lián)的書簽數(shù)量"""
return self.bookmarks.count()
def __repr__(self):
return f'<Tag {self.name}>'
# 數(shù)據(jù)庫(kù)事件監(jiān)聽(tīng)器
@event.listens_for(Bookmark, 'before_update')
def update_timestamp(mapper, connection, target):
"""在書簽更新時(shí)自動(dòng)更新updated_at時(shí)間戳"""
target.updated_at = datetime.utcnow()
4.2 數(shù)據(jù)庫(kù)初始化
創(chuàng)建app/__init__.py文件:
# app/__init__.py
from flask import Flask
from flask_login import LoginManager
from flask_migrate import Migrate
from config import config
# 擴(kuò)展初始化
login_manager = LoginManager()
login_manager.login_view = 'auth.login'
login_manager.login_message_category = 'info'
def create_app(config_name='default'):
"""應(yīng)用工廠函數(shù)"""
app = Flask(__name__)
app.config.from_object(config[config_name])
# 初始化擴(kuò)展
from app.models import db
db.init_app(app)
login_manager.init_app(app)
Migrate(app, db)
# 用戶加載回調(diào)
@login_manager.user_loader
def load_user(user_id):
from app.models import User
return User.query.get(int(user_id))
# 注冊(cè)藍(lán)圖
from app.routes import main_bp, auth_bp, bookmarks_bp, api_bp
app.register_blueprint(main_bp)
app.register_blueprint(auth_bp, url_prefix='/auth')
app.register_blueprint(bookmarks_bp, url_prefix='/bookmarks')
app.register_blueprint(api_bp, url_prefix='/api')
# 錯(cuò)誤處理
register_error_handlers(app)
# 上下文處理器
@app.context_processor
def inject_globals():
from app.models import Tag
from flask_login import current_user
if current_user.is_authenticated:
user_tags = Tag.query.filter_by(user_id=current_user.id).all()
else:
user_tags = []
return dict(user_tags=user_tags)
return app
def register_error_handlers(app):
"""注冊(cè)錯(cuò)誤處理器"""
@app.errorhandler(404)
def not_found_error(error):
return render_template('errors/404.html'), 404
@app.errorhandler(500)
def internal_error(error):
db.session.rollback()
return render_template('errors/500.html'), 500
5. 用戶認(rèn)證系統(tǒng)
5.1 認(rèn)證表單
創(chuàng)建app/forms.py文件:
# app/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, TextAreaField, SelectMultipleField
from wtforms.validators import DataRequired, Email, Length, EqualTo, ValidationError, URL
from app.models import User
class LoginForm(FlaskForm):
"""登錄表單"""
username = StringField('用戶名', validators=[DataRequired(), Length(1, 64)])
password = PasswordField('密碼', validators=[DataRequired()])
remember_me = BooleanField('記住我')
class RegistrationForm(FlaskForm):
"""注冊(cè)表單"""
username = StringField('用戶名', validators=[
DataRequired(),
Length(1, 64, message='用戶名長(zhǎng)度必須在1-64個(gè)字符之間')
])
email = StringField('郵箱', validators=[
DataRequired(),
Email(),
Length(1, 120)
])
password = PasswordField('密碼', validators=[
DataRequired(),
Length(6, 128, message='密碼長(zhǎng)度至少6個(gè)字符'),
EqualTo('password2', message='兩次輸入的密碼必須一致')
])
password2 = PasswordField('確認(rèn)密碼', validators=[DataRequired()])
def validate_username(self, field):
"""驗(yàn)證用戶名是否已存在"""
if User.query.filter_by(username=field.data).first():
raise ValidationError('該用戶名已被使用')
def validate_email(self, field):
"""驗(yàn)證郵箱是否已存在"""
if User.query.filter_by(email=field.data).first():
raise ValidationError('該郵箱已被注冊(cè)')
class BookmarkForm(FlaskForm):
"""書簽表單"""
url = StringField('URL', validators=[
DataRequired(),
URL(message='請(qǐng)輸入有效的URL地址'),
Length(1, 2048)
])
title = StringField('標(biāo)題', validators=[
DataRequired(),
Length(1, 512)
])
description = TextAreaField('描述', validators=[Length(0, 2000)])
tags = StringField('標(biāo)簽', validators=[Length(0, 500)])
is_public = BooleanField('公開(kāi)書簽')
class EditBookmarkForm(BookmarkForm):
"""編輯書簽表單"""
pass
class SearchForm(FlaskForm):
"""搜索表單"""
query = StringField('搜索', validators=[DataRequired(), Length(1, 200)])
class ImportBookmarksForm(FlaskForm):
"""導(dǎo)入書簽表單"""
bookmarks_file = StringField('書簽文件')
bookmarks_text = TextAreaField('書簽文本', validators=[Length(0, 10000)])
5.2 認(rèn)證路由
創(chuàng)建認(rèn)證相關(guān)的路由:
# app/routes.py - 認(rèn)證部分
from flask import render_template, redirect, url_for, flash, request, current_app
from flask_login import login_user, logout_user, current_user, login_required
from app import db
from app.models import User, Bookmark, Tag
from app.forms import LoginForm, RegistrationForm
# 認(rèn)證藍(lán)圖
auth_bp = Blueprint('auth', __name__)
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
"""用戶登錄"""
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user is None or not user.check_password(form.password.data):
flash('無(wú)效的用戶名或密碼', 'danger')
return redirect(url_for('auth.login'))
if not user.is_active:
flash('賬戶已被禁用,請(qǐng)聯(lián)系管理員', 'warning')
return redirect(url_for('auth.login'))
login_user(user, remember=form.remember_me.data)
user.last_login = datetime.utcnow()
db.session.commit()
flash(f'歡迎回來(lái),{user.username}!', 'success')
# 重定向到next參數(shù)指定的頁(yè)面
next_page = request.args.get('next')
if not next_page or not next_page.startswith('/'):
next_page = url_for('main.index')
return redirect(next_page)
return render_template('auth/login.html', title='登錄', form=form)
@auth_bp.route('/register', methods=['GET', 'POST'])
def register():
"""用戶注冊(cè)"""
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = RegistrationForm()
if form.validate_on_submit():
user = User(
username=form.username.data,
email=form.email.data
)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('注冊(cè)成功!請(qǐng)登錄。', 'success')
return redirect(url_for('auth.login'))
return render_template('auth/register.html', title='注冊(cè)', form=form)
@auth_bp.route('/logout')
@login_required
def logout():
"""用戶登出"""
logout_user()
flash('您已成功登出。', 'info')
return redirect(url_for('main.index'))
6. 書簽管理功能
6.1 書簽CRUD操作
創(chuàng)建書簽管理路由:
# app/routes.py - 書簽管理部分
from flask import jsonify, send_file
from urllib.parse import urlparse
import requests
from bs4 import BeautifulSoup
from sqlalchemy import or_, func
# 書簽藍(lán)圖
bookmarks_bp = Blueprint('bookmarks', __name__)
@bookmarks_bp.route('/')
@bookmarks_bp.route('/page/<int:page>')
@login_required
def list_bookmarks(page=1):
"""書簽列表"""
per_page = current_app.config['BOOKMARKS_PER_PAGE']
# 獲取查詢參數(shù)
tag_name = request.args.get('tag')
search_query = request.args.get('q')
sort_by = request.args.get('sort', 'created_at')
order = request.args.get('order', 'desc')
# 構(gòu)建基礎(chǔ)查詢
query = Bookmark.query.filter_by(user_id=current_user.id)
# 標(biāo)簽過(guò)濾
if tag_name:
query = query.join(Bookmark.tags).filter(Tag.name == tag_name)
# 搜索過(guò)濾
if search_query:
search_filter = or_(
Bookmark.title.ilike(f'%{search_query}%'),
Bookmark.description.ilike(f'%{search_query}%'),
Bookmark.url.ilike(f'%{search_query}%')
)
query = query.filter(search_filter)
# 排序
if sort_by == 'title':
order_by = Bookmark.title.asc() if order == 'asc' else Bookmark.title.desc()
elif sort_by == 'clicks':
order_by = Bookmark.click_count.asc() if order == 'asc' else Bookmark.click_count.desc()
else: # created_at
order_by = Bookmark.created_at.asc() if order == 'asc' else Bookmark.created_at.desc()
query = query.order_by(order_by)
# 分頁(yè)
bookmarks = query.paginate(
page=page,
per_page=per_page,
error_out=False
)
return render_template('bookmarks/list.html',
bookmarks=bookmarks,
tag_name=tag_name,
search_query=search_query,
sort_by=sort_by,
order=order)
@bookmarks_bp.route('/add', methods=['GET', 'POST'])
@login_required
def add_bookmark():
"""添加書簽"""
form = BookmarkForm()
if form.validate_on_submit():
# 創(chuàng)建書簽
bookmark = Bookmark(
url=form.url.data,
title=form.title.data,
description=form.description.data,
is_public=form.is_public.data,
user_id=current_user.id
)
# 處理標(biāo)簽
if form.tags.data:
tag_names = [tag.strip() for tag in form.tags.data.split(',') if tag.strip()]
for tag_name in tag_names:
tag = Tag.query.filter_by(name=tag_name, user_id=current_user.id).first()
if not tag:
tag = Tag(name=tag_name, user_id=current_user.id)
db.session.add(tag)
bookmark.tags.append(tag)
db.session.add(bookmark)
db.session.commit()
flash('書簽添加成功!', 'success')
return redirect(url_for('bookmarks.list_bookmarks'))
# 預(yù)填充URL(如果通過(guò)參數(shù)傳遞)
url = request.args.get('url', '')
if url and not form.url.data:
form.url.data = url
# 自動(dòng)獲取標(biāo)題
if current_app.config['ENABLE_URL_PREVIEW']:
try:
response = requests.get(url, timeout=5)
soup = BeautifulSoup(response.content, 'html.parser')
title = soup.find('title')
if title:
form.title.data = title.get_text().strip()
except:
pass
return render_template('bookmarks/add.html', title='添加書簽', form=form)
@bookmarks_bp.route('/edit/<int:bookmark_id>', methods=['GET', 'POST'])
@login_required
def edit_bookmark(bookmark_id):
"""編輯書簽"""
bookmark = Bookmark.query.filter_by(id=bookmark_id, user_id=current_user.id).first_or_404()
form = EditBookmarkForm(obj=bookmark)
# 預(yù)填充標(biāo)簽
if not form.tags.data and bookmark.tags.count() > 0:
form.tags.data = ', '.join([tag.name for tag in bookmark.tags])
if form.validate_on_submit():
bookmark.url = form.url.data
bookmark.title = form.title.data
bookmark.description = form.description.data
bookmark.is_public = form.is_public.data
# 更新標(biāo)簽
bookmark.tags = []
if form.tags.data:
tag_names = [tag.strip() for tag in form.tags.data.split(',') if tag.strip()]
for tag_name in tag_names:
tag = Tag.query.filter_by(name=tag_name, user_id=current_user.id).first()
if not tag:
tag = Tag(name=tag_name, user_id=current_user.id)
db.session.add(tag)
bookmark.tags.append(tag)
db.session.commit()
flash('書簽更新成功!', 'success')
return redirect(url_for('bookmarks.list_bookmarks'))
return render_template('bookmarks/edit.html', title='編輯書簽', form=form, bookmark=bookmark)
@bookmarks_bp.route('/delete/<int:bookmark_id>', methods=['POST'])
@login_required
def delete_bookmark(bookmark_id):
"""刪除書簽"""
bookmark = Bookmark.query.filter_by(id=bookmark_id, user_id=current_user.id).first_or_404()
db.session.delete(bookmark)
db.session.commit()
flash('書簽已刪除。', 'success')
return redirect(url_for('bookmarks.list_bookmarks'))
@bookmarks_bp.route('/<int:bookmark_id>')
@login_required
def view_bookmark(bookmark_id):
"""查看書簽詳情"""
bookmark = Bookmark.query.filter_by(id=bookmark_id, user_id=current_user.id).first_or_404()
# 增加點(diǎn)擊計(jì)數(shù)
bookmark.increment_click_count()
return render_template('bookmarks/detail.html', bookmark=bookmark)
@bookmarks_bp.route('/click/<int:bookmark_id>')
@login_required
def click_bookmark(bookmark_id):
"""點(diǎn)擊書簽(重定向到實(shí)際URL)"""
bookmark = Bookmark.query.filter_by(id=bookmark_id, user_id=current_user.id).first_or_404()
# 增加點(diǎn)擊計(jì)數(shù)
bookmark.increment_click_count()
return redirect(bookmark.url)
6.2 工具函數(shù)
創(chuàng)建工具函數(shù)輔助書簽管理:
# app/utils/helpers.py
import requests
from bs4 import BeautifulSoup
from urllib.parse import urlparse, urljoin
from flask import current_app
import time
def fetch_url_metadata(url):
"""
獲取URL的元數(shù)據(jù)(標(biāo)題、描述等)
參數(shù):
url (str): 目標(biāo)URL
返回:
dict: 包含元數(shù)據(jù)的字典
"""
try:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
response = requests.get(url, timeout=10, headers=headers)
response.raise_for_status()
soup = BeautifulSoup(response.content, 'html.parser')
# 提取標(biāo)題
title = soup.find('title')
title_text = title.get_text().strip() if title else ''
# 提取描述
description = ''
meta_desc = soup.find('meta', attrs={'name': 'description'})
if meta_desc and meta_desc.get('content'):
description = meta_desc['content'].strip()
# 提取favicon
favicon = extract_favicon(soup, url)
return {
'title': title_text,
'description': description,
'favicon': favicon,
'success': True
}
except Exception as e:
current_app.logger.error(f"獲取URL元數(shù)據(jù)失敗: {e}")
return {
'title': '',
'description': '',
'favicon': '',
'success': False,
'error': str(e)
}
def extract_favicon(soup, base_url):
"""
從HTML中提取favicon URL
參數(shù):
soup: BeautifulSoup對(duì)象
base_url (str): 基礎(chǔ)URL
返回:
str: favicon URL
"""
favicon = None
# 查找link標(biāo)簽中的favicon
icon_link = soup.find('link', rel=lambda x: x and 'icon' in x.lower())
if icon_link and icon_link.get('href'):
favicon = urljoin(base_url, icon_link['href'])
# 如果沒(méi)找到,嘗試默認(rèn)路徑
if not favicon:
parsed_url = urlparse(base_url)
favicon = f"{parsed_url.scheme}://{parsed_url.netloc}/favicon.ico"
return favicon
def parse_bookmarks_file(file_content, file_type='html'):
"""
解析書簽文件(支持HTML、JSON等格式)
參數(shù):
file_content (str): 文件內(nèi)容
file_type (str): 文件類型
返回:
list: 書簽列表
"""
bookmarks = []
if file_type == 'html':
# 解析瀏覽器導(dǎo)出的HTML書簽文件
soup = BeautifulSoup(file_content, 'html.parser')
# 查找所有<a>標(biāo)簽(書簽)
for link in soup.find_all('a'):
url = link.get('href', '').strip()
title = link.get_text().strip()
if url and title:
bookmarks.append({
'url': url,
'title': title,
'description': '',
'tags': []
})
return bookmarks
def generate_tag_cloud(tags, min_font=12, max_font=24):
"""
生成標(biāo)簽云數(shù)據(jù)
參數(shù):
tags: 標(biāo)簽查詢集
min_font (int): 最小字體大小
max_font (int): 最大字體大小
返回:
list: 包含字體大小的標(biāo)簽列表
"""
if not tags:
return []
# 獲取標(biāo)簽使用頻率
tag_counts = []
for tag in tags:
tag_counts.append((tag, tag.bookmarks_count))
if not tag_counts:
return []
# 計(jì)算字體大小
counts = [count for _, count in tag_counts]
min_count = min(counts)
max_count = max(counts)
tag_cloud = []
for tag, count in tag_counts:
if max_count == min_count:
# 所有標(biāo)簽使用頻率相同
font_size = (min_font + max_font) / 2
else:
# 線性插值計(jì)算字體大小
font_size = min_font + (count - min_count) * (max_font - min_font) / (max_count - min_count)
tag_cloud.append({
'tag': tag,
'font_size': font_size,
'count': count
})
return tag_cloud
7. 搜索與過(guò)濾功能
高級(jí)搜索實(shí)現(xiàn)
# app/routes.py - 搜索功能
@bookmarks_bp.route('/search')
@login_required
def search_bookmarks():
"""搜索書簽"""
query = request.args.get('q', '').strip()
tag_filter = request.args.get('tag', '')
date_from = request.args.get('date_from', '')
date_to = request.args.get('date_to', '')
is_public = request.args.get('is_public', '')
if not any([query, tag_filter, date_from, date_to, is_public]):
return redirect(url_for('bookmarks.list_bookmarks'))
# 構(gòu)建查詢
search_query = Bookmark.query.filter_by(user_id=current_user.id)
# 關(guān)鍵詞搜索
if query:
search_terms = query.split()
for term in search_terms:
term_filter = or_(
Bookmark.title.ilike(f'%{term}%'),
Bookmark.description.ilike(f'%{term}%'),
Bookmark.url.ilike(f'%{term}%')
)
search_query = search_query.filter(term_filter)
# 標(biāo)簽過(guò)濾
if tag_filter:
search_query = search_query.join(Bookmark.tags).filter(Tag.name == tag_filter)
# 日期范圍過(guò)濾
if date_from:
try:
from_date = datetime.strptime(date_from, '%Y-%m-%d')
search_query = search_query.filter(Bookmark.created_at >= from_date)
except ValueError:
pass
if date_to:
try:
to_date = datetime.strptime(date_to, '%Y-%m-%d')
search_query = search_query.filter(Bookmark.created_at <= to_date)
except ValueError:
pass
# 公開(kāi)狀態(tài)過(guò)濾
if is_public:
is_public_bool = is_public.lower() == 'true'
search_query = search_query.filter(Bookmark.is_public == is_public_bool)
# 執(zhí)行查詢
bookmarks = search_query.order_by(Bookmark.created_at.desc()).all()
return render_template('bookmarks/search_results.html',
bookmarks=bookmarks,
query=query,
tag_filter=tag_filter,
date_from=date_from,
date_to=date_to,
is_public=is_public)
@bookmarks_bp.route('/api/suggest')
@login_required
def suggest_bookmarks():
"""書簽搜索建議(用于自動(dòng)完成)"""
query = request.args.get('q', '').strip()
if not query or len(query) < 2:
return jsonify([])
# 搜索匹配的書簽
search_filter = or_(
Bookmark.title.ilike(f'%{query}%'),
Bookmark.description.ilike(f'%{query}%'),
Bookmark.url.ilike(f'%{query}%')
)
suggestions = Bookmark.query.filter(
search_filter,
Bookmark.user_id == current_user.id
).limit(10).all()
results = []
for bookmark in suggestions:
results.append({
'id': bookmark.id,
'title': bookmark.title,
'url': bookmark.url,
'description': bookmark.description[:100] + '...' if len(bookmark.description) > 100 else bookmark.description
})
return jsonify(results)
8. 數(shù)據(jù)導(dǎo)入導(dǎo)出
導(dǎo)入導(dǎo)出功能
# app/routes.py - 導(dǎo)入導(dǎo)出功能
import json
from datetime import datetime
import csv
from io import StringIO
@bookmarks_bp.route('/import', methods=['GET', 'POST'])
@login_required
def import_bookmarks():
"""導(dǎo)入書簽"""
form = ImportBookmarksForm()
if form.validate_on_submit():
imported_count = 0
error_count = 0
# 處理文件上傳
if form.bookmarks_file.data:
# 這里處理文件上傳邏輯
pass
# 處理文本輸入
if form.bookmarks_text.data:
try:
# 嘗試解析為JSON
bookmarks_data = json.loads(form.bookmarks_text.data)
for item in bookmarks_data:
try:
# 創(chuàng)建書簽
bookmark = Bookmark(
url=item.get('url', ''),
title=item.get('title', ''),
description=item.get('description', ''),
is_public=item.get('is_public', False),
user_id=current_user.id
)
# 處理標(biāo)簽
tags = item.get('tags', [])
if isinstance(tags, str):
tags = [tag.strip() for tag in tags.split(',')]
for tag_name in tags:
tag = Tag.query.filter_by(name=tag_name, user_id=current_user.id).first()
if not tag:
tag = Tag(name=tag_name, user_id=current_user.id)
db.session.add(tag)
bookmark.tags.append(tag)
db.session.add(bookmark)
imported_count += 1
except Exception as e:
error_count += 1
current_app.logger.error(f"導(dǎo)入書簽失敗: {e}")
db.session.commit()
flash(f'成功導(dǎo)入 {imported_count} 個(gè)書簽,失敗 {error_count} 個(gè)。', 'success')
return redirect(url_for('bookmarks.list_bookmarks'))
except json.JSONDecodeError:
flash('JSON格式錯(cuò)誤,請(qǐng)檢查輸入。', 'danger')
else:
flash('請(qǐng)?zhí)峁灁?shù)據(jù)。', 'warning')
return render_template('bookmarks/import.html', form=form)
@bookmarks_bp.route('/export')
@login_required
def export_bookmarks():
"""導(dǎo)出書簽"""
format_type = request.args.get('format', 'json')
# 獲取用戶的所有書簽
bookmarks = Bookmark.query.filter_by(user_id=current_user.id).all()
if format_type == 'json':
# JSON格式導(dǎo)出
export_data = []
for bookmark in bookmarks:
export_data.append({
'url': bookmark.url,
'title': bookmark.title,
'description': bookmark.description,
'is_public': bookmark.is_public,
'tags': [tag.name for tag in bookmark.tags],
'created_at': bookmark.created_at.isoformat(),
'click_count': bookmark.click_count
})
response = jsonify(export_data)
response.headers['Content-Type'] = 'application/json'
response.headers['Content-Disposition'] = f'attachment; filename=bookmarks_{datetime.now().strftime("%Y%m%d")}.json'
return response
elif format_type == 'csv':
# CSV格式導(dǎo)出
output = StringIO()
writer = csv.writer(output)
# 寫入表頭
writer.writerow(['URL', 'Title', 'Description', 'Tags', 'Public', 'Created At', 'Clicks'])
# 寫入數(shù)據(jù)
for bookmark in bookmarks:
tags = ', '.join([tag.name for tag in bookmark.tags])
writer.writerow([
bookmark.url,
bookmark.title,
bookmark.description or '',
tags,
'Yes' if bookmark.is_public else 'No',
bookmark.created_at.strftime('%Y-%m-%d %H:%M:%S'),
bookmark.click_count
])
response = current_app.response_class(
output.getvalue(),
mimetype='text/csv',
headers={'Content-Disposition': f'attachment; filename=bookmarks_{datetime.now().strftime("%Y%m%d")}.csv'}
)
return response
else:
flash('不支持的導(dǎo)出格式。', 'danger')
return redirect(url_for('bookmarks.list_bookmarks'))
9. API接口設(shè)計(jì)
RESTful API實(shí)現(xiàn)
# app/routes.py - API部分
from flask import jsonify, request
from app.models import Bookmark, Tag
from app.utils.helpers import fetch_url_metadata
# API藍(lán)圖
api_bp = Blueprint('api', __name__)
@api_bp.route('/bookmarks', methods=['GET'])
@login_required
def api_list_bookmarks():
"""API:獲取書簽列表"""
page = request.args.get('page', 1, type=int)
per_page = min(request.args.get('per_page', 20, type=int), 100)
tag = request.args.get('tag', '')
search = request.args.get('search', '')
query = Bookmark.query.filter_by(user_id=current_user.id)
if tag:
query = query.join(Bookmark.tags).filter(Tag.name == tag)
if search:
search_filter = or_(
Bookmark.title.ilike(f'%{search}%'),
Bookmark.description.ilike(f'%{search}%')
)
query = query.filter(search_filter)
pagination = query.order_by(Bookmark.created_at.desc()).paginate(
page=page, per_page=per_page, error_out=False
)
return jsonify({
'bookmarks': [bookmark.to_dict() for bookmark in pagination.items],
'total': pagination.total,
'pages': pagination.pages,
'current_page': page
})
@api_bp.route('/bookmarks', methods=['POST'])
@login_required
def api_create_bookmark():
"""API:創(chuàng)建書簽"""
data = request.get_json()
if not data or not data.get('url'):
return jsonify({'error': 'URL是必填字段'}), 400
# 檢查書簽是否已存在
existing = Bookmark.query.filter_by(url=data['url'], user_id=current_user.id).first()
if existing:
return jsonify({'error': '該書簽已存在'}), 409
bookmark = Bookmark(
url=data['url'],
title=data.get('title', ''),
description=data.get('description', ''),
is_public=data.get('is_public', False),
user_id=current_user.id
)
# 處理標(biāo)簽
tags = data.get('tags', [])
for tag_name in tags:
tag = Tag.query.filter_by(name=tag_name, user_id=current_user.id).first()
if not tag:
tag = Tag(name=tag_name, user_id=current_user.id)
db.session.add(tag)
bookmark.tags.append(tag)
db.session.add(bookmark)
db.session.commit()
return jsonify(bookmark.to_dict()), 201
@api_bp.route('/bookmarks/<int:bookmark_id>', methods=['GET'])
@login_required
def api_get_bookmark(bookmark_id):
"""API:獲取單個(gè)書簽"""
bookmark = Bookmark.query.filter_by(id=bookmark_id, user_id=current_user.id).first_or_404()
return jsonify(bookmark.to_dict())
@api_bp.route('/bookmarks/<int:bookmark_id>', methods=['PUT'])
@login_required
def api_update_bookmark(bookmark_id):
"""API:更新書簽"""
bookmark = Bookmark.query.filter_by(id=bookmark_id, user_id=current_user.id).first_or_404()
data = request.get_json()
if 'title' in data:
bookmark.title = data['title']
if 'description' in data:
bookmark.description = data['description']
if 'is_public' in data:
bookmark.is_public = data['is_public']
# 更新標(biāo)簽
if 'tags' in data:
bookmark.tags = []
for tag_name in data['tags']:
tag = Tag.query.filter_by(name=tag_name, user_id=current_user.id).first()
if not tag:
tag = Tag(name=tag_name, user_id=current_user.id)
db.session.add(tag)
bookmark.tags.append(tag)
db.session.commit()
return jsonify(bookmark.to_dict())
@api_bp.route('/bookmarks/<int:bookmark_id>', methods=['DELETE'])
@login_required
def api_delete_bookmark(bookmark_id):
"""API:刪除書簽"""
bookmark = Bookmark.query.filter_by(id=bookmark_id, user_id=current_user.id).first_or_404()
db.session.delete(bookmark)
db.session.commit()
return jsonify({'message': '書簽已刪除'})
@api_bp.route('/metadata')
@login_required
def api_get_metadata():
"""API:獲取URL元數(shù)據(jù)"""
url = request.args.get('url')
if not url:
return jsonify({'error': 'URL參數(shù)是必需的'}), 400
metadata = fetch_url_metadata(url)
return jsonify(metadata)
10. 前端界面與用戶體驗(yàn)
10.1 基礎(chǔ)模板
創(chuàng)建基礎(chǔ)模板templates/base.html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}書簽管理器{% endblock %}</title>
<!-- Bootstrap 5 CSS -->
<link rel="external nofollow" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" rel="external nofollow" >
<!-- 自定義CSS -->
<link href="{{ url_for('static', filename='css/style.css') }}" rel="external nofollow" rel="stylesheet">
{% block extra_css %}{% endblock %}
</head>
<body>
<!-- 導(dǎo)航欄 -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="{{ url_for('main.index') }}" rel="external nofollow" >
<i class="fas fa-bookmark"></i> 書簽管理器
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
{% if current_user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('bookmarks.list_bookmarks') }}" rel="external nofollow" >
<i class="fas fa-list"></i> 我的書簽
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('bookmarks.add_bookmark') }}" rel="external nofollow" rel="external nofollow" rel="external nofollow" >
<i class="fas fa-plus"></i> 添加書簽
</a>
</li>
{% endif %}
</ul>
<ul class="navbar-nav">
{% if current_user.is_authenticated %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" rel="external nofollow" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
<i class="fas fa-user"></i> {{ current_user.username }}
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ url_for('bookmarks.import_bookmarks') }}" rel="external nofollow" >導(dǎo)入書簽</a></li>
<li><a class="dropdown-item" href="{{ url_for('bookmarks.export_bookmarks') }}" rel="external nofollow" >導(dǎo)出書簽</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('auth.logout') }}" rel="external nofollow" >退出</a></li>
</ul>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.login') }}" rel="external nofollow" >登錄</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.register') }}" rel="external nofollow" >注冊(cè)</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<!-- 主要內(nèi)容 -->
<main class="container mt-4">
<!-- 閃存消息 -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
<!-- 頁(yè)腳 -->
<footer class="bg-light mt-5 py-4">
<div class="container text-center">
<p class="text-muted mb-0">
© 2024 書簽管理器. 使用 Flask 構(gòu)建.
</p>
</div>
</footer>
<!-- JavaScript -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
{% block extra_js %}{% endblock %}
</body>
</html>
10.2 書簽列表模板
創(chuàng)建templates/bookmarks/list.html:
{% extends "base.html" %}
{% block title %}我的書簽 - 書簽管理器{% endblock %}
{% block content %}
<div class="row">
<!-- 側(cè)邊欄 -->
<div class="col-md-3">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">篩選</h5>
</div>
<div class="card-body">
<!-- 搜索表單 -->
<form method="GET" class="mb-3">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="搜索書簽..."
value="{{ search_query or '' }}">
<button class="btn btn-outline-primary" type="submit">
<i class="fas fa-search"></i>
</button>
</div>
</form>
<!-- 標(biāo)簽云 -->
<h6 class="mt-4">標(biāo)簽</h6>
<div class="tag-cloud">
{% for tag in user_tags %}
<a href="{{ url_for('bookmarks.list_bookmarks', tag=tag.name) }}" rel="external nofollow"
class="badge bg-secondary text-decoration-none me-1 mb-1">
{{ tag.name }} ({{ tag.bookmarks_count }})
</a>
{% endfor %}
</div>
<!-- 排序選項(xiàng) -->
<h6 class="mt-4">排序</h6>
<div class="btn-group-vertical w-100">
<a href="{{ url_for('bookmarks.list_bookmarks', sort='created_at', order='desc') }}" rel="external nofollow"
class="btn btn-outline-secondary btn-sm text-start">
最新添加
</a>
<a href="{{ url_for('bookmarks.list_bookmarks', sort='title', order='asc') }}" rel="external nofollow"
class="btn btn-outline-secondary btn-sm text-start">
標(biāo)題 A-Z
</a>
<a href="{{ url_for('bookmarks.list_bookmarks', sort='clicks', order='desc') }}" rel="external nofollow"
class="btn btn-outline-secondary btn-sm text-start">
最多點(diǎn)擊
</a>
</div>
</div>
</div>
</div>
<!-- 主內(nèi)容區(qū) -->
<div class="col-md-9">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>
{% if tag_name %}
標(biāo)簽: "{{ tag_name }}"
{% elif search_query %}
搜索: "{{ search_query }}"
{% else %}
我的書簽
{% endif %}
<small class="text-muted">({{ bookmarks.total }} 個(gè))</small>
</h2>
<a href="{{ url_for('bookmarks.add_bookmark') }}" rel="external nofollow" rel="external nofollow" rel="external nofollow" class="btn btn-primary">
<i class="fas fa-plus"></i> 添加書簽
</a>
</div>
<!-- 書簽列表 -->
{% if bookmarks.items %}
<div class="row">
{% for bookmark in bookmarks.items %}
<div class="col-lg-6 mb-4">
<div class="card h-100 bookmark-card">
<div class="card-body">
<h5 class="card-title">
<a href="{{ url_for('bookmarks.click_bookmark', bookmark_id=bookmark.id) }}" rel="external nofollow"
target="_blank" class="text-decoration-none">
{{ bookmark.title }}
</a>
{% if bookmark.is_public %}
<span class="badge bg-success ms-1">公開(kāi)</span>
{% endif %}
</h5>
<p class="card-text text-muted small">
<a href="{{ bookmark.url }}" rel="external nofollow" class="text-muted" target="_blank">
{{ bookmark.domain }}
</a>
</p>
{% if bookmark.description %}
<p class="card-text">{{ bookmark.description|truncate(150) }}</p>
{% endif %}
<!-- 標(biāo)簽 -->
{% if bookmark.tags.count() > 0 %}
<div class="mb-2">
{% for tag in bookmark.tags %}
<span class="badge bg-light text-dark me-1">{{ tag.name }}</span>
{% endfor %}
</div>
{% endif %}
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
點(diǎn)擊: {{ bookmark.click_count }} |
添加: {{ bookmark.created_at.strftime('%Y-%m-%d') }}
</small>
<div class="btn-group">
<a href="{{ url_for('bookmarks.view_bookmark', bookmark_id=bookmark.id) }}" rel="external nofollow"
class="btn btn-sm btn-outline-secondary">
<i class="fas fa-eye"></i>
</a>
<a href="{{ url_for('bookmarks.edit_bookmark', bookmark_id=bookmark.id) }}" rel="external nofollow"
class="btn btn-sm btn-outline-primary">
<i class="fas fa-edit"></i>
</a>
<form method="POST"
action="{{ url_for('bookmarks.delete_bookmark', bookmark_id=bookmark.id) }}"
class="d-inline"
onsubmit="return confirm('確定要?jiǎng)h除這個(gè)書簽嗎?');">
<button type="submit" class="btn btn-sm btn-outline-danger">
<i class="fas fa-trash"></i>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- 分頁(yè) -->
{% if bookmarks.pages > 1 %}
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
{% if bookmarks.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('bookmarks.list_bookmarks', page=bookmarks.prev_num, tag=tag_name, q=search_query) }}" rel="external nofollow" >
上一頁(yè)
</a>
</li>
{% endif %}
{% for page_num in bookmarks.iter_pages() %}
{% if page_num %}
<li class="page-item {% if page_num == bookmarks.page %}active{% endif %}">
<a class="page-link" href="{{ url_for('bookmarks.list_bookmarks', page=page_num, tag=tag_name, q=search_query) }}" rel="external nofollow" >
{{ page_num }}
</a>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">…</span></li>
{% endif %}
{% endfor %}
{% if bookmarks.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('bookmarks.list_bookmarks', page=bookmarks.next_num, tag=tag_name, q=search_query) }}" rel="external nofollow" >
下一頁(yè)
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="text-center py-5">
<i class="fas fa-bookmark fa-4x text-muted mb-3"></i>
<h4 class="text-muted">暫無(wú)書簽</h4>
<p class="text-muted">開(kāi)始添加你的第一個(gè)書簽吧!</p>
<a href="{{ url_for('bookmarks.add_bookmark') }}" rel="external nofollow" rel="external nofollow" rel="external nofollow" class="btn btn-primary">
<i class="fas fa-plus"></i> 添加書簽
</a>
</div>
{% endif %}
</div>
</div>
{% endblock %}
11. 完整應(yīng)用集成
11.1 應(yīng)用啟動(dòng)文件
創(chuàng)建run.py文件:
# run.py
import os
from app import create_app
from app.models import db, User, Bookmark, Tag
app = create_app(os.getenv('FLASK_CONFIG') or 'default')
@app.shell_context_processor
def make_shell_context():
"""為Flask shell添加上下文"""
return {
'db': db,
'User': User,
'Bookmark': Bookmark,
'Tag': Tag
}
@app.cli.command()
def init_db():
"""初始化數(shù)據(jù)庫(kù)命令"""
db.create_all()
print('數(shù)據(jù)庫(kù)初始化完成。')
@app.cli.command()
def create_test_data():
"""創(chuàng)建測(cè)試數(shù)據(jù)"""
from datetime import datetime, timedelta
import random
# 創(chuàng)建測(cè)試用戶
user = User.query.filter_by(username='testuser').first()
if not user:
user = User(username='testuser', email='test@example.com')
user.set_password('password')
db.session.add(user)
db.session.commit()
print('創(chuàng)建測(cè)試用戶: testuser/password')
# 創(chuàng)建測(cè)試書簽
sample_bookmarks = [
{
'url': 'https://www.python.org',
'title': 'Python官方網(wǎng)站',
'description': 'Python編程語(yǔ)言的官方網(wǎng)站',
'tags': ['編程', 'Python', '開(kāi)發(fā)']
},
{
'url': 'https://flask.palletsprojects.com',
'title': 'Flask文檔',
'description': 'Flask Web框架的官方文檔',
'tags': ['Web開(kāi)發(fā)', 'Flask', 'Python']
},
{
'url': 'https://stackoverflow.com',
'title': 'Stack Overflow',
'description': '程序員問(wèn)答社區(qū)',
'tags': ['編程', '問(wèn)答', '社區(qū)']
}
]
for data in sample_bookmarks:
# 檢查書簽是否已存在
existing = Bookmark.query.filter_by(url=data['url'], user_id=user.id).first()
if not existing:
bookmark = Bookmark(
url=data['url'],
title=data['title'],
description=data['description'],
user_id=user.id,
created_at=datetime.utcnow() - timedelta(days=random.randint(1, 30))
)
# 添加標(biāo)簽
for tag_name in data['tags']:
tag = Tag.query.filter_by(name=tag_name, user_id=user.id).first()
if not tag:
tag = Tag(name=tag_name, user_id=user.id)
db.session.add(tag)
bookmark.tags.append(tag)
db.session.add(bookmark)
db.session.commit()
print('測(cè)試數(shù)據(jù)創(chuàng)建完成。')
if __name__ == '__main__':
app.run(debug=True)
11.2 環(huán)境變量配置
創(chuàng)建.env文件:
# .env FLASK_APP=run.py FLASK_CONFIG=development SECRET_KEY=your-secret-key-here DATABASE_URL=sqlite:///bookmarks.db # 郵件配置(可選) MAIL_SERVER=smtp.gmail.com MAIL_PORT=587 MAIL_USERNAME=your-email@gmail.com MAIL_PASSWORD=your-app-password
12. 部署與生產(chǎn)環(huán)境配置
12.1 生產(chǎn)環(huán)境配置
創(chuàng)建wsgi.py用于生產(chǎn)部署:
# wsgi.py
import os
from app import create_app
app = create_app(os.getenv('FLASK_CONFIG') or 'production')
if __name__ == '__main__':
app.run()
12.2 部署說(shuō)明
使用Gunicorn部署:
pip install gunicorn gunicorn -w 4 -b 0.0.0.0:8000 wsgi:app
使用Docker部署:
FROM python:3.9-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . ENV FLASK_CONFIG=production EXPOSE 8000 CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "wsgi:app"]
13. 總結(jié)
本文詳細(xì)介紹了如何使用Flask構(gòu)建一個(gè)功能完整的URL書簽管理工具。我們實(shí)現(xiàn)了:
- 用戶認(rèn)證系統(tǒng):安全的注冊(cè)、登錄和會(huì)話管理
- 書簽CRUD操作:完整的書簽增刪改查功能
- 標(biāo)簽系統(tǒng):靈活的書簽分類和組織
- 搜索過(guò)濾:強(qiáng)大的全文搜索和多維度過(guò)濾
- 數(shù)據(jù)導(dǎo)入導(dǎo)出:支持JSON和CSV格式
- RESTful API:為移動(dòng)應(yīng)用和第三方集成提供接口
- 響應(yīng)式界面:基于Bootstrap 5的現(xiàn)代化UI
這個(gè)書簽管理工具不僅解決了傳統(tǒng)瀏覽器書簽的痛點(diǎn),還提供了許多高級(jí)功能,如智能標(biāo)簽、搜索建議、URL元數(shù)據(jù)提取等。代碼遵循了良好的軟件工程實(shí)踐,包括模塊化設(shè)計(jì)、錯(cuò)誤處理、安全防護(hù)等。
通過(guò)這個(gè)項(xiàng)目,我們展示了Flask框架的強(qiáng)大功能和靈活性,以及如何構(gòu)建一個(gè)生產(chǎn)級(jí)別的Web應(yīng)用程序。讀者可以根據(jù)自己的需求進(jìn)一步擴(kuò)展功能,如添加書簽分享、協(xié)作功能、瀏覽器擴(kuò)展集成等。
到此這篇關(guān)于Python基于Flask構(gòu)建一個(gè)URL書簽管理工具的文章就介紹到這了,更多相關(guān)Python Flask構(gòu)建URL書簽管理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python輕松獲取網(wǎng)絡(luò)時(shí)間和本地時(shí)間技巧揭秘
這篇文章主要為大家介紹了Python輕松獲取網(wǎng)絡(luò)時(shí)間和本地時(shí)間技巧揭秘,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01
Python 創(chuàng)建子進(jìn)程模塊subprocess詳解
這篇文章主要介紹了Python 創(chuàng)建子進(jìn)程模塊subprocess詳解,本文詳細(xì)講解了subprocess模塊的方法、參數(shù)、使用實(shí)例等,需要的朋友可以參考下2015-04-04
Django 中自定義 Admin 樣式與功能的實(shí)現(xiàn)方法
這篇文章主要介紹了Django 中自定義 Admin 樣式與功能的實(shí)現(xiàn)方法,本文圖文并茂給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-07-07
基于Python編寫一個(gè)有趣的年會(huì)抽獎(jiǎng)系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了如何使用Python編寫一個(gè)簡(jiǎn)易的抽獎(jiǎng)系統(tǒng),順便幫助大家鞏固一下對(duì)Python語(yǔ)法和框架的理解,感興趣的小伙伴可以了解下2023-12-12
python使用typing模塊加強(qiáng)代碼的可讀性(實(shí)戰(zhàn)演示)
Python是一門弱類型的語(yǔ)言,很多時(shí)候我們可能不清楚函數(shù)參數(shù)類型或者返回值類型,很有可能導(dǎo)致一些類型沒(méi)有指定方法,typing模塊可以很好的解決這個(gè)問(wèn)題。下面通過(guò)本文給大家介紹python使用typing模塊加強(qiáng)代碼的可讀性,感興趣的朋友一起看看吧2021-12-12
查看jupyter notebook每個(gè)單元格運(yùn)行時(shí)間實(shí)例
這篇文章主要介紹了查看jupyter notebook每個(gè)單元格運(yùn)行時(shí)間實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-04-04
Python中免驗(yàn)證跳轉(zhuǎn)到內(nèi)容頁(yè)的實(shí)例代碼
在本篇文章里小編給大家整理的是一篇關(guān)于Python中免驗(yàn)證跳轉(zhuǎn)到內(nèi)容頁(yè)的實(shí)例代碼,有興趣的朋友們可以學(xué)習(xí)分享下。2020-10-10

