示例代码(爬虫):
import logging
from typing import Optional
# 设置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# 尝试导入必要的库
try:
import requests
from bs4 import BeautifulSoup
from sqlalchemy import create_engine, Column, Integer, String, Text, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship
except ImportError:
logger.error("请安装必要的库: pip install requests beautifulsoup4 sqlalchemy")
exit(1)
# 创建数据库模型
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
full_name = Column(String(100), unique=True, nullable=False)
def __repr__(self):
return f"<User(id={self.id}, full_name='{self.full_name}')>"
class WebContent(Base):
__tablename__ = 'web_contents'
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
url = Column(String(500), nullable=False)
title = Column(String(200))
content = Column(Text)
user = relationship("User", back_populates="contents")
def __repr__(self):
return f"<WebContent(id={self.id}, url='{self.url}', title='{self.title}')>"
# 添加反向关系
User.contents = relationship("WebContent", order_by=WebContent.id, back_populates="user")
# 数据库连接和会话
def get_db_session():
"""创建数据库连接和会话"""
# 使用SQLite作为数据库
engine = create_engine('sqlite:///web_crawler.db')
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
return Session()
def get_or_create_user(user_full_name: str) -> User:
"""
获取或创建用户
Args:
user_full_name: 用户全名
Returns:
User: 用户对象
"""
session = get_db_session()
# 查找用户,如果不存在则创建
user = session.query(User).filter(User.full_name == user_full_name).first()
if not user:
user = User(full_name=user_full_name)
session.add(user)
session.commit()
logger.info(f"创建新用户: {user_full_name}")
else:
logger.info(f"找到现有用户: {user_full_name}")
return user
def crawl_webpage(user: User, url: str) -> Optional[WebContent]:
"""
爬取单个网页并存储内容
Args:
user: 用户对象
url: 要爬取的URL
Returns:
WebContent: 爬取的网页内容对象
"""
session = get_db_session()
# 检查URL是否已经爬取过
existing_content = session.query(WebContent).filter(
WebContent.user_id == user.id,
WebContent.url == url
).first()
if existing_content:
logger.info(f"URL已经爬取过: {url}")
return existing_content
# 爬取网页
logger.info(f"开始爬取URL: {url}")
try:
response = requests.get(url, timeout=30)
response.raise_for_status() # 如果请求失败则抛出异常
# 解析HTML
soup = BeautifulSoup(response.text, 'html.parser')
# 提取标题和内容
title = soup.title.string if soup.title else "无标题"
# 提取正文内容 (这里简化处理,实际应用中可能需要更复杂的提取逻辑)
paragraphs = soup.find_all('p')
content = "\n".join([p.get_text() for p in paragraphs])
# 存储到数据库
web_content = WebContent(
user_id=user.id,
url=url,
title=title,
content=content
)
session.add(web_content)
session.commit()
logger.info(f"成功爬取并存储URL: {url}, 标题: {title}")
return web_content
except Exception as e:
logger.error(f"爬取URL时出错: {url}, 错误: {str(e)}")
return None
def main():
"""主函数"""
# 用户名
user_full_name = "Paul Iusztin"
# 要爬取的URL (只爬取一个)
url = "https://mlabonne.github.io/blog/posts/2024-07-29_Finetune_Llama31.html" # 请替换为你想爬取的实际URL
# 获取或创建用户
user = get_or_create_user(user_full_name)
# 爬取网页
web_content = crawl_webpage(user, url)
logger.info(f"web_content===: {web_content}")
if web_content:
logger.info(f"爬取成功! 标题: {web_content.title}")
logger.info(f"内容预览: {web_content.content[:200]}...")
else:
logger.error("爬取失败!")
if __name__ == "__main__":
main()
这段代码给 User
模型添加了一个属性,叫做 contents
,用来表示与该用户关联的所有 WebContent
对象。具体来说,它使用了 SQLAlchemy
提供的 relationship()
函数,语法规则和功能说明如下:
-
基本语法说明
User.contents = relationship("WebContent", ...)
这是在给 User 类“动态”添加一个属性(contents),这个属性的值在查询时会返回所有与这个用户相关联的WebContent
对象。"WebContent"
:表示关联的目标模型是 WebContent(注意这里是字符串,SQLAlchemy 会延迟解析)。order_by=WebContent.id
:指明在获取关联数据时,以 WebContent 表中的 id 字段进行排序(默认是升序)。back_populates="user"
:表示在 WebContent 模型中,存在一个属性 user,与 User.contents 形成双向关联。也就是说,当你从 WebContent 访问 user 时,得到的就是与之关联的 User 对象,而从 User 访问 contents 时,得到的是该用户所有相关的 WebContent 对象。
-
通俗讲
想象你有一个图书馆系统,User 就像借书人,而 WebContent 就像他们借的书。User.contents
就相当于在借书人档案中记录“借书清单”。- 当你查看某个借书人的档案(User 对象)时,通过
.contents
属性可以看到他借了哪些书(WebContent 对象),而且这些书是按照书的编号(这里用的是 id)排序的。 - 同时,每本书记录中都有一个“借书人”的字段(在 WebContent 模型中,通过 back_populates=“user” 指定),这样你可以从书反查到具体是谁借的。
-
数值详细举例说明
假设我们有如下数据记录:
-
用户(User)记录
- 用户 A:
- id = 1
- full_name = “Alice”
- 用户 A:
-
网页内容(WebContent)记录
假设 Alice 关联了三个网页内容:- 网页内容 1:
- id = 10
- user_id = 1(表示属于 Alice)
- title = “Page One”
- 网页内容 2:
- id = 5
- user_id = 1
- title = “Page Two”
- 网页内容 3:
- id = 20
- user_id = 1
- title = “Page Three”
- 网页内容 1:
当你查询用户 Alice 的 contents 属性时,由于我们设置了
order_by=WebContent.id
,返回的列表会根据 id 从小到大排序,结果就是:alice.contents # 返回的顺序为:[网页内容 2 (id=5), 网页内容 1 (id=10), 网页内容 3 (id=20)]
此外,在 WebContent 的每个对象中,由于设置了
back_populates="user"
,你可以通过访问:some_webcontent.user # 这将返回对应的用户对象,也就是 Alice
-
总的来说,这行代码就是在建立一种双向关系,让你可以非常方便地从用户对象访问到该用户对应的所有网页内容(并按 id 排序),同时也能从每个网页内容反查到所属的用户。
同时,这种对应关系主要依赖于两个方面:
-
外键(ForeignKey)关系
在 WebContent 模型中,有一个字段user_id
,它通过ForeignKey('users.id')
指定了与 User 表中的id
字段相关联。这就告诉 SQLAlchemy,每条 WebContent 记录的user_id
对应的是 User 表中某个用户的id
,也就是说,当 WebContent 的user_id
为 1 时,它就关联到 User 表中id
为 1 的用户。 -
双向关联的声明(back_populates)
在 WebContent 模型中,我们写了:user = relationship("User", back_populates="contents")
而在 User 模型中,通过:
User.contents = relationship("WebContent", order_by=WebContent.id, back_populates="user")
双方互相声明了对方的属性名称。这意味着:
- 当你通过
user.contents
访问时,SQLAlchemy 会自动查找所有 WebContent 记录,其user_id
与该用户的id
匹配; - 当你从某个 WebContent 记录访问
web_content.user
时,SQLAlchemy 就会返回对应的 User 对象。
- 当你通过
这两部分协同工作,SQLAlchemy 根据 WebContent 中的外键 user_id
和双方在 relationship 中设置的 back_populates
属性,自动确定每个用户和网页内容之间的对应关系。