news 2026/7/6 9:00:27

Python+Playwright构建Twitter数据采集框架:从原理到实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python+Playwright构建Twitter数据采集框架:从原理到实战

1. 项目概述:为什么选择Python+Playwright来“盯”住Twitter?

做数据分析和市场研究的朋友,估计都动过从Twitter(现在叫X)上抓点数据的念头。无论是追踪某个话题的热度、分析竞品动态,还是研究用户情绪,Twitter都是一个信息金矿。但这事儿吧,说简单也简单,说难也难。简单在于,你打开浏览器就能看;难在于,想规模化、自动化、稳定地获取结构化数据,那就是另一回事了。

传统的路子,比如用requests库模拟请求,在Twitter这种前端高度动态化、反爬机制严密的平台面前,越来越吃力。你得处理一大堆动态加载的JavaScript、应对频繁变化的API接口、还要搞定登录状态维持和验证码,一套组合拳下来,开发维护成本高得吓人。所以,我转向了浏览器自动化这条路,而Playwright就是当前这个领域里,我认为最趁手的工具。

为什么是Playwright,而不是Selenium或者Puppeteer?简单说,Playwright是微软出品,原生支持异步,速度飞快;它能为Chromium、Firefox和WebKit三大浏览器引擎提供统一的API,写一份代码能跑三个浏览器,兼容性测试和稳定性都更有保障;最关键的是,它对现代Web应用(单页应用、动态内容)的支持非常好,能自动等待元素加载,处理弹窗、iframe都更优雅。用Python来驱动Playwright,结合Python丰富的数据处理生态(pandas, numpy, sqlalchemy等),构建一个数据采集框架就非常顺理成章了。

这个框架的目标很明确:稳定、高效、可维护地从Twitter采集公开数据。它不是为了“暴力”爬取,而是模拟一个真实用户的浏览行为,合规地获取我们所需的信息。接下来,我会把这个框架从设计思路到代码实现,再到踩坑优化的全过程,毫无保留地拆解给你看。

2. 框架核心设计:模块化与稳健性优先

在动手写第一行代码之前,设计思路决定了后续是事半功倍还是事倍功半。我的核心设计原则就两条:模块化稳健性

2.1 模块化架构拆解

一个健壮的采集框架不能把所有逻辑都塞在一个脚本里。我把它分成了几个核心模块,各司其职:

  1. 核心驱动层 (Core Driver):这是框架的引擎。职责是管理Playwright浏览器实例的启动、配置和关闭。包括设置代理(如果需要)、用户数据目录(保存登录状态)、浏览器类型(一般用Chromium足矣)、以及启动参数(如无头模式、窗口大小、忽略HTTPS错误等)。这一层要保证浏览器环境的单一和可控。

  2. 身份与会话管理模块 (Session Manager):处理最令人头疼的登录和会话维持。Twitter的登录可能有多种形式(用户名密码、手机验证码、双重认证)。这个模块需要能识别当前登录状态,在会话失效时按照预设策略进行重登录。一个关键技巧是复用用户数据目录,让浏览器记住Cookie和LocalStorage,可以极大减少需要主动登录的频率。

  3. 页面交互与导航模块 (Navigator):封装所有与Twitter页面进行交互的操作。比如打开个人主页、搜索关键词、滚动加载更多推文、点击“显示更多回复”等。这个模块的方法应该尽可能模拟人类操作,加入随机延迟,避免操作过快触发反爬。

  4. 数据解析与提取模块 (Parser):这是价值所在。从复杂的HTML页面中,精准地提取出我们需要的数据字段。一条推文可能包含:推文ID、作者、发布时间、正文内容、转发数、点赞数、回复数、引用推文、媒体链接(图片、视频)、话题标签等。这里需要精心编写CSS选择器或XPath,并考虑页面结构可能发生的变动,所以解析逻辑要有一定的容错性。

  5. 数据存储模块 (Storage):定义采集到的数据如何落地。可以是简单的CSV/JSON文件,也可以是MySQL/PostgreSQL数据库,或者MongoDB这类文档数据库。模块设计上应该支持灵活扩展,方便切换存储后端。

  6. 任务调度与监控模块 (Scheduler/Monitor):如果你需要定时采集或监控多个目标,这个模块就必不可少。它可以管理采集任务队列,设置采集频率,并在任务失败或出现异常(如被限制访问)时发出警报或执行备用方案。

注意:模块化不是过度设计。对于小型、一次性任务,你可能只需要核心驱动层和解析模块。但对于需要长期运行、维护的项目,清晰的模块划分能让你在修改登录逻辑、更换解析规则或调整存储方式时,不至于牵一发而动全身。

2.2 稳健性设计要点

Twitter不会欢迎自动化爬虫,所以我们的代码必须“举止得体”。

  • 速率限制 (Rate Limiting):在请求之间插入随机延迟。不要用固定的sleep(2),而是用random.uniform(1.5, 4)这样的随机等待,让行为模式更接近真人。对于滚动加载,也要控制频率。
  • 错误处理与重试机制:网络波动、元素加载失败、页面结构变化都是常态。每个关键操作(如打开页面、查找元素、点击)都必须用try...except包裹,并设计合理的重试逻辑。例如,元素查找失败后,可以先等待更长时间再重试,重试3次仍失败则记录日志并执行降级方案(如跳过当前条目)。
  • 指纹伪装 (Fingerprinting):Playwright本身可以通过设置user-agentviewport等来模拟不同设备。更进一步,可以定期更换这些参数,甚至使用Playwright提供的context来隔离不同的“浏览器指纹”环境。
  • 状态检测:代码需要能检测是否被限制。例如,检查页面是否出现了“出错了,稍后再试”或“速率限制”等提示语,一旦发现,立即进入“冷却期”,暂停采集一段时间,或者切换账号/代理。
  • 日志系统:详细的日志是调试和监控的生命线。记录每个关键步骤的开始、结束、状态,以及所有遇到的错误和异常。这能帮助你在问题发生时快速定位。

3. 实战构建:从零搭建采集框架

理论说再多,不如一行代码。我们开始动手,构建一个最小可行产品(MVP)版本的框架。

3.1 环境准备与依赖安装

首先,确保你的Python环境是3.7或以上版本。然后,我们安装核心依赖:

# 安装playwright的python库 pip install playwright # 安装Playwright所需的浏览器驱动(Chromium, Firefox, WebKit) playwright install chromium

我通常只安装Chromium,因为它最常用,性能也最好。同时,我们还会用到一些辅助库,按需安装:

pip install pandas # 用于数据处理和保存为CSV pip install sqlalchemy psycopg2-binary # 如果你要用PostgreSQL存储 pip install python-dotenv # 用于管理配置文件(如账号密码)

项目目录结构可以这样组织:

twitter_crawler/ ├── core/ │ ├── __init__.py │ ├── driver.py # 核心驱动层 │ └── config.py # 配置文件读取 ├── manager/ │ ├── __init__.py │ └── session.py # 会话管理 ├── navigator/ │ ├── __init__.py │ └── actions.py # 页面交互 ├── parser/ │ ├── __init__.py │ └── tweet_parser.py # 推文解析 ├── storage/ │ ├── __init__.py │ └── csv_handler.py # 存储处理器 ├── utils/ │ ├── __init__.py │ ├── logger.py # 日志工具 │ └── helpers.py # 通用辅助函数 ├── tasks/ │ └── search_task.py # 示例采集任务 ├── .env # 环境变量(勿提交git) ├── requirements.txt └── main.py # 主入口

3.2 核心模块代码实现

我们挑几个最核心的模块来看看代码怎么写。

1. 核心驱动层 (core/driver.py)

这个模块负责创建和管理浏览器上下文。

# core/driver.py import asyncio from playwright.async_api import async_playwright, Browser, BrowserContext import random from typing import Optional from ..utils.logger import get_logger logger = get_logger(__name__) class BrowserDriver: def __init__(self, headless: bool = True, user_data_dir: Optional[str] = None, proxy: Optional[str] = None): self.headless = headless self.user_data_dir = user_data_dir self.proxy = proxy self._browser: Optional[Browser] = None self._context: Optional[BrowserContext] = None self._playwright = None async def start(self): """启动浏览器和上下文""" self._playwright = await async_playwright().start() launch_options = { 'headless': self.headless, 'args': [ '--disable-blink-features=AutomationControlled', # 隐藏自动化特征 '--no-sandbox', '--disable-dev-shm-usage' ] } if self.user_data_dir: launch_options['user_data_dir'] = self.user_data_dir self._browser = await self._playwright.chromium.launch(**launch_options) context_options = { 'viewport': {'width': 1920, 'height': 1080}, 'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' } if self.proxy: context_options['proxy'] = {'server': self.proxy} self._context = await self._browser.new_context(**context_options) # 注入脚本,屏蔽webdriver属性 await self._context.add_init_script(""" Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); """) logger.info("Browser driver started successfully.") return self._context async def close(self): """关闭浏览器和playwright""" if self._browser: await self._browser.close() if self._playwright: await self._playwright.stop() logger.info("Browser driver closed.") @property def context(self): if not self._context: raise RuntimeError("Browser context not started. Call `start()` first.") return self._context

2. 页面交互模块 (navigator/actions.py)

这里封装搜索和滚动操作。

# navigator/actions.py import asyncio import random from playwright.async_api import Page from typing import List from ..utils.logger import get_logger logger = get_logger(__name__) class TwitterNavigator: def __init__(self, page: Page): self.page = page async def search_keyword(self, keyword: str, filter_type: str = "top"): """ 搜索关键词 :param keyword: 搜索词 :param filter_type: 筛选类型,可选 'top'(热门), 'latest'(最新), 'people'(用户)等 """ search_url = f"https://twitter.com/search?q={keyword}&src=typed_query&f={filter_type}" await self.page.goto(search_url, wait_until="networkidle") await self.page.wait_for_selector('article', timeout=15000) # 等待推文加载 logger.info(f"Navigated to search results for: {keyword}") # 随机等待,模拟阅读 await asyncio.sleep(random.uniform(2, 5)) async def scroll_and_collect_tweets(self, max_scrolls: int = 10) -> List[dict]: """ 滚动页面,收集推文数据。 这是一个简化示例,实际解析在Parser模块。 :param max_scrolls: 最大滚动次数 :return: 推文数据列表 """ tweets_data = [] scroll_attempt = 0 while scroll_attempt < max_scrolls: # 获取当前页面所有推文article元素 tweet_elements = await self.page.query_selector_all('article') logger.info(f"Scroll {scroll_attempt + 1}: Found {len(tweet_elements)} tweet articles.") # 这里应该调用Parser模块来解析每个element # 示例: for element in tweet_elements: data = await parser.parse_tweet(element) # 我们先简单记录一下 for idx, element in enumerate(tweet_elements): # 模拟解析出一些信息 tweet_info = { 'position': f"{scroll_attempt}_{idx}", 'element': element # 实际应传递element给parser } tweets_data.append(tweet_info) # 执行滚动 scroll_height_before = await self.page.evaluate("document.documentElement.scrollHeight") await self.page.evaluate("window.scrollTo(0, document.documentElement.scrollHeight)") await asyncio.sleep(random.uniform(2, 4)) # 等待新内容加载 # 检查是否滚动到底部 scroll_height_after = await self.page.evaluate("document.documentElement.scrollHeight") if scroll_height_after == scroll_height_before: logger.info("Reached the bottom of the page or no new content loaded.") break scroll_attempt += 1 logger.info(f"Finished scrolling. Total tweets captured: {len(tweets_data)}") return tweets_data

3. 数据解析模块 (parser/tweet_parser.py)

这是最需要精细处理的部分,因为Twitter的页面结构可能变化。

# parser/tweet_parser.py from playwright.async_api import ElementHandle import re from datetime import datetime from typing import Optional, Dict, Any from ..utils.logger import get_logger logger = get_logger(__name__) class TweetParser: @staticmethod async def parse_tweet(element: ElementHandle) -> Optional[Dict[str, Any]]: """ 从一个推文article元素中解析出结构化数据。 """ try: # 1. 提取推文正文 # 注意:选择器需要根据Twitter实际页面结构调整,这里仅为示例 content_element = await element.query_selector('div[data-testid="tweetText"]') if not content_element: # 可能是一条纯媒体或转发推文,尝试其他选择器或跳过 logger.debug("Could not find tweet text, might be a media/retweet.") return None content = await content_element.inner_text() # 2. 提取作者信息 author_element = await element.query_selector('a[role="link"][aria-label]') # 注意:这个选择器不稳定 author_name = await author_element.inner_text() if author_element else None author_handle_element = await element.query_selector('span[data-testid="User-Name"]') author_handle = await author_handle_element.inner_text() if author_handle_element else None # 3. 提取时间戳 time_element = await element.query_selector('time') datetime_str = await time_element.get_attribute('datetime') if time_element else None published_at = datetime.fromisoformat(datetime_str.replace('Z', '+00:00')) if datetime_str else None # 4. 提取互动数据(转发、点赞、回复) # 这些按钮的aria-label属性通常包含数字 reply_button = await element.query_selector('[data-testid="reply"]') retweet_button = await element.query_selector('[data-testid="retweet"]') like_button = await element.query_selector('[data-testid="like"]') def extract_count_from_aria(button: Optional[ElementHandle]) -> int: if not button: return 0 aria_label = await button.get_attribute('aria-label') if aria_label: # 从类似“123 次转发”的文本中提取数字 match = re.search(r'(\d+(?:\.\d+)?[KM]?)', aria_label.replace(',', '')) if match: num_str = match.group(1) # 处理K, M缩写 multipliers = {'K': 1000, 'M': 1000000} if num_str[-1] in multipliers: return int(float(num_str[:-1]) * multipliers[num_str[-1]]) return int(num_str) return 0 reply_count = extract_count_from_aria(reply_button) retweet_count = extract_count_from_aria(retweet_button) like_count = extract_count_from_aria(like_button) # 5. 提取推文ID (从推文链接中) tweet_link_element = await element.query_selector('a[href*="/status/"]') tweet_url = await tweet_link_element.get_attribute('href') if tweet_link_element else None tweet_id = None if tweet_url: match = re.search(r'/status/(\d+)', tweet_url) tweet_id = match.group(1) if match else None # 6. 提取话题标签和提及 hashtags = re.findall(r'#(\w+)', content) mentions = re.findall(r'@(\w+)', content) tweet_data = { 'tweet_id': tweet_id, 'author_name': author_name, 'author_handle': author_handle, 'content': content, 'published_at': published_at.isoformat() if published_at else None, 'reply_count': reply_count, 'retweet_count': retweet_count, 'like_count': like_count, 'hashtags': ','.join(hashtags), 'mentions': ','.join(mentions), 'tweet_url': f"https://twitter.com{i tweet_url}" if tweet_url else None, 'scraped_at': datetime.utcnow().isoformat() } logger.debug(f"Parsed tweet: {tweet_id}") return tweet_data except Exception as e: logger.error(f"Error parsing tweet element: {e}", exc_info=True) return None

实操心得:Twitter的HTML结构和># tasks/search_task.py import asyncio import pandas as pd from core.driver import BrowserDriver from navigator.actions import TwitterNavigator from parser.tweet_parser import TweetParser from storage.csv_handler import save_to_csv from utils.logger import setup_logging import random async def main(): # 0. 设置日志 setup_logging() logger = get_logger(__name__) # 1. 初始化驱动 # 使用用户数据目录可以保持登录状态 driver = BrowserDriver( headless=False, # 首次调试建议设为False,观察浏览器行为 user_data_dir="./twitter_profile", # 指定一个目录保存会话 proxy=None # 如有需要,可设置代理,例如:'http://127.0.0.1:7890' ) context = await driver.start() page = await context.new_page() # 2. 初始化导航器和解析器 navigator = TwitterNavigator(page) parser = TweetParser() all_tweets = [] keyword = "Python" try: # 3. 执行搜索 await navigator.search_keyword(keyword, filter_type="latest") # 4. 滚动并解析 tweet_elements_list = [] scroll_attempt = 0 max_scrolls = 5 # 首次测试,先滚动5次 while scroll_attempt < max_scrolls: # 获取当前屏的推文元素 current_tweet_elements = await page.query_selector_all('article') logger.info(f"Scroll {scroll_attempt+1}: Found {len(current_tweet_elements)} articles.") for element in current_tweet_elements: # 解析每个推文元素 tweet_data = await parser.parse_tweet(element) if tweet_data: all_tweets.append(tweet_data) # 滚动 await page.evaluate("window.scrollTo(0, document.body.scrollHeight)") await asyncio.sleep(random.uniform(3, 6)) # 随机等待,模拟阅读 scroll_attempt += 1 logger.info(f"Total tweets collected: {len(all_tweets)}") # 5. 保存数据 if all_tweets: df = pd.DataFrame(all_tweets) save_to_csv(df, f"twitter_search_{keyword}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv") logger.info(f"Data saved to CSV. Sample:\n{df.head()}") else: logger.warning("No tweets were collected.") except Exception as e: logger.error(f"An error occurred during the task: {e}", exc_info=True) finally: # 6. 清理资源 await page.close() await driver.close() if __name__ == "__main__": asyncio.run(main())

运行这个脚本,你就能看到浏览器自动打开(如果headless=False),搜索“Python”,滚动页面,并将抓取到的推文数据保存到CSV文件中。这就是整个框架的雏形。

4. 深度优化与高级技巧

基础框架跑通只是第一步。要想让它真正稳定、高效、长期运行,还需要大量的优化和细节处理。

4.1 应对反爬策略:从“形似”到“神似”

Twitter的反爬系统非常智能,它不仅仅检测请求频率,更会分析行为模式。

  • 人类行为模拟

    • 随机化:除了请求间隔,鼠标移动轨迹、滚动速度、点击位置都可以加入随机性。Playwright提供了page.mouse.move(x, y)等API,可以模拟非直线的鼠标移动。
    • 不完美操作:真人有时会误点、会短暂停留。可以在代码中偶尔加入对非目标元素的点击(并立刻取消),或者在某个元素上悬停(hover)随机时间。
    • 浏览多样性:不要只执行单一任务。可以在采集间隙,随机访问一下趋势页面、个人资料页,模拟真实的浏览行为。
  • 上下文隔离与轮换

    • 如果采集量很大,考虑使用多个浏览器上下文(BrowserContext),每个上下文有独立的Cookie、缓存和指纹。可以轮换使用这些上下文来分散风险。
    • 结合代理IP池,为不同的上下文分配不同的IP地址。
  • 指纹深度伪装

    • 使用context.add_init_script注入更复杂的JS,覆盖更多的navigatorwindow属性,如plugins,languages,hardwareConcurrency等。
    • 定期更换user-agentviewport大小、时区、语言偏好等。

4.2 性能优化:更快更省资源

  • 异步并发:Playwright的Python API是异步的(async/await)。我们可以利用asyncio.gather并发处理多个页面或任务。例如,同时打开多个标签页采集不同关键词的数据。但要严格控制并发度,过高会立刻被识别。
    async def crawl_multiple_keywords(keywords): async with async_playwright() as p: browser = await p.chromium.launch(headless=True) tasks = [] for kw in keywords: context = await browser.new_context() page = await context.new_page() task = crawl_single_keyword(page, kw) # 一个封装的采集函数 tasks.append(task) await asyncio.gather(*tasks) await browser.close()
  • 请求拦截与过滤:并非所有网络请求都需要。使用page.route可以拦截请求,阻止图片、样式表、字体甚至某些非必要的API请求加载,大幅提升页面加载速度和减少带宽消耗。
    async def block_media(route): if route.request.resource_type in ["image", "stylesheet", "font"]: await route.abort() else: await route.continue_() await page.route("**/*", block_media)
  • 智能等待与超时控制:不要一味使用固定的sleep。Playwright提供了丰富的等待条件:page.wait_for_selector,page.wait_for_function,page.wait_for_load_state。结合这些,可以更精确地等待必要元素出现,减少无效等待时间。

4.3 数据质量与完整性保障

  • 增量采集:利用推文ID或发布时间,记录上次采集的最后一条推文。下次采集时,可以设定条件只采集比这个时间点更新的数据,避免重复。
  • 数据去重:在存储前,根据推文ID进行去重。推文可能在不同滚动批次中出现。
  • 字段校验与清洗:对解析出的数据进行校验。比如,发布时间格式是否合法?数字字段是否被正确解析(处理“1.2万”这种格式)?内容中的换行符、多余空格是否需要清理?
  • 关联数据采集:有时你需要的不只是推文本身。你可能还需要点开推文详情页,抓取完整的回复列表;或者进入用户主页,抓取用户信息。这需要框架具备更复杂的页面跳转和状态管理能力。

5. 常见问题排查与实战避坑指南

在实际运行中,你一定会遇到各种各样的问题。这里记录了一些典型问题和我的解决方案。

5.1 登录状态无法保持

  • 现象:每次运行脚本都需要重新登录。
  • 排查:检查user_data_dir参数是否设置且路径正确。确保在关闭浏览器时,没有以“隐身模式”或清理Cookie的方式启动。
  • 解决:确保BrowserContext使用的是同一个user_data_dir。首次运行时,用headless=False手动登录一次,后续运行即可复用该目录。注意,Twitter可能会定期要求验证,需要人工干预。

5.2 元素找不到或超时

  • 现象page.wait_for_selector超时,或query_selector返回None
  • 排查
    1. 选择器问题:页面结构可能已更新。用浏览器开发者工具重新检查元素,确认选择器是否依然有效。优先使用>async def safe_query(page, selector, timeout=10000, retries=3): for i in range(retries): try: element = await page.wait_for_selector(selector, timeout=timeout) return element except Exception as e: logger.warning(f"Attempt {i+1} failed for selector '{selector}': {e}") await asyncio.sleep(2) logger.error(f"All {retries} attempts failed for selector '{selector}'.") return None

      5.3 被限制访问或弹出验证码

      • 现象:页面出现“出错了,稍后再试”、“速率限制”或验证码。
      • 排查:行为模式过于规律,请求频率过高,或IP被标记。
      • 解决
        • 立即进入冷却:一旦检测到这类页面,脚本应立即暂停所有活动,进入长时间休眠(如30分钟到数小时)。
        • 降低频率:大幅增加操作间的随机延迟,减少每日采集总量。
        • 切换身份:如果有多个账号和IP,切换到备用方案。
        • 验证码处理:对于简单验证码,可以考虑集成OCR服务(如Tesseract,但效果有限)。更常见的做法是报警并等待人工处理。可以设置脚本发送邮件或钉钉消息通知你。

      5.4 数据解析不全或错位

      • 现象:CSV文件里的数据混乱,比如作者名跑到内容栏。
      • 排查:解析逻辑对页面结构变动的适应性差,或者选择器不够精确,一次匹配到了多个元素。
      • 解决
        1. 范围限定:在推文article元素内部进行查找(element.query_selector),而不是在整个页面(page.query_selector),避免跨推文匹配。
        2. 多重验证:解析后,检查关键字段(如推文ID、内容)是否为空,如果为空则丢弃或标记为解析失败。
        3. 定期更新解析器:将解析用的选择器作为配置项,放在外部文件(如JSON)中。当Twitter改版时,只需更新配置文件,而无需修改核心代码。

      构建这样一个框架,更像是一个持续对抗和适应的过程。没有一劳永逸的方案,只有通过精心设计、持续观察和不断调整,才能让它在合规的前提下,稳定地为你获取有价值的数据。记住,尊重目标网站的robots.txt,控制采集频率,只采集公开数据,这是长期运行的基础。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/6 8:57:46

AI服务集成OAuth2:Spring Authorization Server实战与高并发优化

1. 项目概述&#xff1a;当AI实体侦测遇上OAuth2 最近在做一个挺有意思的项目&#xff0c;核心是把一个AI驱动的智能实体侦测服务&#xff0c;无缝集成到各种第三方应用里去。简单来说&#xff0c;这个服务能通过摄像头或图片流&#xff0c;实时识别出画面里是“人”、“车”、…

作者头像 李华
网站建设 2026/7/6 8:54:42

从零构建高可用加密即时通讯系统:WebSocket网关与端到端加密实践

1. 项目概述&#xff1a;从零构建一个能抗能打的通讯系统最近几年&#xff0c;无论是企业内部协作&#xff0c;还是各类社交、游戏应用&#xff0c;对即时通讯&#xff08;IM&#xff09;的需求都在爆炸式增长。用户不再满足于简单的文字收发&#xff0c;对消息的实时性、可靠性…

作者头像 李华
网站建设 2026/7/6 8:51:10

基于Django的大数据旅游数据分析与推荐系统

一、 技术栈与背景意义本系统旨在利用大数据技术分析海量旅游数据&#xff0c;为用户提供个性化的旅游推荐。系统采用前后端分离架构&#xff0c;后端基于Django框架&#xff0c;前端使用Vue.js&#xff0c;并整合了多种大数据处理与分析组件。1.1 技术栈概览后端框架: Django …

作者头像 李华
网站建设 2026/7/6 8:50:01

UVa 627 The Net

题目描述 给定一个由路由器组成的网络&#xff0c;每个路由器都有一个“路由表”&#xff0c;记录它能直接到达的其他路由器&#xff08;称为“可见”路由器&#xff09;。信息在网络中传递时&#xff0c;每经过一个路由器计为一次跳数。需要为若干查询&#xff08;从源路由器到…

作者头像 李华
网站建设 2026/7/6 8:47:40

AI Agent开发实战指南:从零构建智能体,掌握LangChain与工具调用

&#x1f680; 30款热门AI模型一站整合&#xff0c;DeepSeek/GLM/Qwen 随心用&#xff0c;限时 5 折。 &#x1f449; 点击领海量免费额度 大家好&#xff0c;我是专注于AI应用开发的技术博主。最近在项目落地和社区交流中&#xff0c;发现很多开发者对大模型和AI Agent的开…

作者头像 李华
网站建设 2026/7/6 8:47:01

25-AGENTS.md最佳实践

25 AGENTS.md 最佳实践 概述 前面三篇文章详细介绍了 AGENTS.md 的机制、高级用法和配置灵活性。这一篇,我们将回归到最本质的问题:怎样写出能让 AI 真正高效工作的 AGENTS.md? 本文不是理论的堆砌,而是从真实踩坑经验中提炼出来的实战规则。这些规则来自大量 AGENTS.m…

作者头像 李华