ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

2024主流平台内容采集工作流:Playwright自动化+结构化导出

2024主流平台内容采集工作流:Playwright自动化+结构化导出

1. 项目概述:这不是“爬虫教程”,而是一套面向内容工作者的数据采集工作流

你有没有遇到过这样的场景:某位知乎大V连续写了27篇关于AI绘画的深度专栏,你想系统性地存档研读,但手动一页页点开、复制、截图、保存PDF,光是前5篇就耗掉一个下午;雪球上一位行业分析师的季度复盘长文逻辑严密、数据扎实,但平台不提供导出功能,手机端阅读体验又差,想离线精读却无从下手;微博上某个摄影博主发布的300张高清样片和配套拍摄手记,散落在不同年份、不同话题下,用常规搜索根本串不起来——这些不是技术问题,而是信息获取效率的断层。这个项目标题里提到的“批量下载知乎专栏回答/文章/视频,下载雪球文章,导出文章数据,批量下载微博图片/视频/内容/文章PDF,导出微博数据”,本质上不是教你怎么写爬虫代码,而是构建一套可重复、可验证、可维护的内容资产沉淀系统。它面向的是内容编辑、行业研究员、自媒体创作者、学术资料整理者这类真实用户,解决的核心痛点是:在不依赖平台官方API(多数已关闭或权限极严)、不手动操作的前提下,把分散在多个中文主流内容平台上的高价值文本、图像、视频资源,结构化地抓取、清洗、归档,并生成便于后续分析或再创作的本地数据集。关键词里的“2024”不是凑数——这一年知乎全面升级了反爬策略,雪球对未登录状态的访问做了更细粒度的限流,微博则将图片CDN域名从wx1.sinaimg.cn切换为动态生成的wx[1-9].sinaimg.cn加随机路径,旧脚本基本全部失效。所以这个方案必须基于当前真实环境设计,所有步骤我都已在2024年6月用三台不同配置的机器(Mac M1、Windows 11 i5、Ubuntu 22.04)实测通过,不是理论推演。它不承诺“全自动零干预”,但能确保95%以上的操作在预设规则下稳定运行,剩下5%的异常情况有明确的排查路径和人工介入接口。

2. 整体架构设计与核心思路拆解:为什么放弃“万能爬虫”,选择“分平台定制+浏览器自动化+数据管道”三位一体

很多人看到“批量下载”第一反应就是写个Python爬虫,用requests+BeautifulSoup硬刚。我在2021年也这么干过,当时知乎的HTML结构还比较“友好”,用正则就能提取正文。但到了2024年,这条路已经彻底堵死。我试过三种主流思路,最终放弃了其中两种:

  • 纯HTTP请求方案(已淘汰):试图用requests模拟登录,携带Cookie去请求目标页面。问题在于:知乎的登录态校验现在嵌入了WebGL指纹、Canvas渲染特征、时序行为分析三重校验,单纯伪造User-Agent和Cookie会被秒判为机器人;雪球的登录接口返回的是加密的JWT token,且token有效期仅15分钟,每次刷新都需要完整走一遍OAuth2.0隐式授权流程,中间夹杂着至少4次重定向和2次JS执行;微博的图片CDN地址是前端JS动态拼接的,URL里包含时间戳哈希和设备ID,服务端根本不认你用Python构造的静态链接。实测下来,这种方案的失败率超过80%,且失败后无法定位是网络抖动、反爬拦截还是参数过期,调试成本极高。

  • 逆向JS方案(不推荐):用Playwright或Selenium加载页面后,直接执行页面内的加密函数(比如知乎的window._za对象里的签名方法)。这需要你持续跟踪平台JS代码更新——知乎在2024年Q1把核心签名逻辑从main.js拆到了vendor-xxxx.js和runtime-xxxx.js两个动态加载文件里,每次版本更新都得重新找入口点。我跟了三个月,平均每周要重写一次签名解析逻辑,纯粹是体力活,没有技术积累价值。

最终选定的方案是:以浏览器自动化为执行引擎,以平台特性为设计原点,以数据管道为交付标准。具体来说:

  1. 知乎部分:放弃解析HTML,直接用Playwright控制真实Chrome浏览器,模拟人类操作——点击“专栏”Tab、滚动加载更多、点击“展开全文”按钮、触发PDF打印对话框。关键点在于:我们不截取网页快照,而是监听浏览器发起的Network.requestWillBeSent事件,从中捕获所有带content-type: application/json的XHR请求,这些请求里就包含了原始Markdown格式的正文、作者信息、发布时间等结构化数据。这样既绕过了HTML渲染层的反爬,又拿到了比网页显示更干净的原始数据。

  2. 雪球部分:利用其PC端网页版对未登录用户仍开放文章阅读(仅限制评论和关注),但会插入大量广告和遮罩层。我们用Playwright先等待广告DOM节点加载完成,然后执行page.evaluate(() => { document.querySelectorAll('.ad-container, .mask-layer').forEach(el => el.remove()); })移除干扰元素,再用page.locator('article').innerHtml()精准提取正文。之所以不用截图或PDF导出,是因为雪球的文章排版含大量SVG图表和交互式表格,PDF会丢失矢量信息,而纯文本提取又会漏掉图表标题和数据注释。

  3. 微博部分:这是最复杂的。微博的图片和视频资源分散在三个层级:①博文正文里的<img>标签(缩略图);②点击图片后弹出的Lightbox里的高清图(URL在>import json from playwright.sync_api import sync_playwright def handle_json_request(route, request): if 'api/v4/columns' in request.url and 'posts' in request.url: # 拦截请求,获取原始响应 response = route.fetch() data = response.json() # 提取所有文章的id、title、content、created_time for post in data['data']: print(f"标题: {post['title']}, 发布时间: {post['created_time']}") # 继续转发请求,保证页面正常渲染 route.fulfill(response=response) else: route.continue_() with sync_playwright() as p: browser = p.chromium.launch(headless=False) page = browser.new_page() page.route('**/api/**', handle_json_request) # 全局拦截API请求 page.goto('https://zhuanlan.zhihu.com/p/xxxxx')

    这段代码在2024年6月实测中,成功捕获了知乎专栏所有文章的原始JSON数据,包括被折叠的长回答和视频嵌入信息,准确率100%。而Selenium要实现同样功能,需要注入自定义JS Hook,代码量翻三倍且稳定性差。

    提示:Playwright的route拦截是同步阻塞的,如果在handler里做耗时操作(如写文件),会导致页面卡顿。正确做法是把数据存入内存队列,另起线程异步处理。

    3.2 知乎专栏数据提取:绕过“折叠回答”的终极方案

    知乎对长回答的处理很狡猾:正文超过1500字会默认折叠,只显示前几句,后面是“展开阅读全文”按钮。很多脚本到这里就停了,只拿到开头几百字。真正的解决方案是:不点“展开”,而是直接请求原始API。我们在handle_json_request里捕获到的/api/v4/columns/{slug}/posts响应中,每个post对象都有一个content字段,这就是未经折叠的完整HTML。但要注意,这个HTML里还包含<script>标签和广告占位符,需要清洗。我的清洗函数如下:

    import re from bs4 import BeautifulSoup def clean_zhihu_content(html): # 移除所有script标签及其内容 html = re.sub(r'<script[^>]*>.*?</script>', '', html, flags=re.DOTALL | re.IGNORECASE) # 移除知乎广告容器 html = re.sub(r'<div[^>]*class="RichContent-ad"[^>]*>.*?</div>', '', html, flags=re.DOTALL | re.IGNORECASE) # 移除图片懒加载的data-src,替换为src html = re.sub(r'<img[^>]*data-src="([^"]+)"', r'<img src="\1"', html) # 用BeautifulSoup进一步清理 soup = BeautifulSoup(html, 'html.parser') # 移除所有style标签 for tag in soup.find_all('style'): tag.decompose() # 只保留p, h1-h3, img, ul, ol, li等语义化标签 allowed_tags = ['p', 'h1', 'h2', 'h3', 'img', 'ul', 'ol', 'li', 'br', 'blockquote'] for tag in soup.find_all(True): if tag.name not in allowed_tags: tag.unwrap() # 移除标签但保留内容 return str(soup)

    这个函数处理后的HTML,可以直接用pdfkit.from_string()生成PDF,或者用markdownify库转成Markdown。实测对一篇12000字的知乎专栏长文,清洗后体积缩小35%,但所有段落、列表、图片链接完整保留。

    3.3 雪球文章导出:如何应对“无痕模式”下的数据缺失

    雪球有个隐藏机制:在浏览器无痕模式下访问文章,页面会加载一个空白的<div class="article-content">,里面只有<p>加载中...</p>,真实内容由JS动态注入。普通Playwright脚本如果没处理好,就会拿到空内容。解决方案是:强制等待特定DOM节点出现,并注入防检测脚本。关键代码如下:

    # 等待文章主体加载完成(雪球的加载完成标志是出现class="article-title"的h1) page.wait_for_selector('h1.article-title', timeout=30000) # 注入防检测脚本:伪装成正常用户行为 page.add_init_script(""" // 覆盖navigator.webdriver属性 Object.defineProperty(navigator, 'webdriver', { get: () => false, }); // 覆盖plugins和mimeTypes Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5], }); """) # 移除广告层后提取内容 page.evaluate(""" document.querySelectorAll('.ad-container, .mask-layer, .article-footer').forEach(el => el.remove()); """) content_html = page.locator('article').inner_html()

    这里add_init_script注入的脚本,是绕过雪球前端反爬检测的关键。如果不加,page.locator('article').inner_html()大概率返回空字符串。这个技巧在2024年所有雪球文章导出场景中都验证有效。

    3.4 微博图片/视频下载:动态CDN域名的实时解析

    微博在2024年将图片CDN从固定域名升级为动态域名,例如一张图的真实URL可能是:

    https://wx3.sinaimg.cn/large/008vGfZKly1hxxxxx.jpg https://wx7.sinaimg.cn/mw1024/008vGfZKly1hyyyyy.jpg

    wx[1-9]是随机的,large/mw1024是尺寸标识,后面的008vGfZKly1hxxxxx是唯一ID。很多脚本直接用<img src="...">里的URL,结果下载下来全是404。正确做法是:在页面上下文中执行JS,获取Lightbox弹窗里的真实高清图URL。微博的图片查看器会把高清图URL存在># 在页面上下文中获取所有图片的真实高清URL image_urls = page.evaluate(""" () => { const urls = []; // 获取所有博文中的图片 document.querySelectorAll('article img').forEach(img => { if (img.dataset.src) { urls.push(img.dataset.src); } }); // 获取Lightbox里的图片(需先触发弹窗) document.querySelectorAll('article .pic-list img').forEach(img => { if (img.dataset.bsrc) { urls.push(img.dataset.bsrc); // bsrc是big source } }); return [...new Set(urls)]; // 去重 } """)

    对于视频,微博的MP4地址藏在<video>标签的src属性里,但这个属性是动态设置的。我们用同样的方式:

    video_urls = page.evaluate(""" () => { const urls = []; document.querySelectorAll('article video').forEach(video => { if (video.src && video.src.includes('weibo.com')) { urls.push(video.src); } }); return urls; } """)

    实测下来,这套方案对微博2024年Q2的所有图片和视频类型(包括GIF、MP4、M3U8)都适用,下载成功率99.2%。

    3.5 数据结构化与导出:为什么用SQLite而非CSV或JSON

    所有平台的数据最终要汇入一个统一数据库。很多人第一反应是导出CSV,但CSV在处理多对一关系时极其脆弱。比如一篇知乎文章可能有5条评论、3个标签、2个关联视频,CSV只能扁平化存储,导致数据冗余和查询困难。JSON虽然能嵌套,但无法高效查询。我最终选择SQLite,原因有三:

    1. 单文件便携:整个数据库就是一个.db文件,双击可用DB Browser打开,非技术人员也能查数据;
    2. 关系型支持:可以建articles、comments、tags、media四张表,用外键关联,避免数据重复;
    3. 全文检索:SQLite内置FTS5扩展,对文章标题和正文建全文索引,SELECT * FROM articles WHERE articles MATCH 'AI 绘画'毫秒级响应。

    建表SQL示例:

    -- 文章主表 CREATE TABLE articles ( id INTEGER PRIMARY KEY AUTOINCREMENT, platform TEXT NOT NULL, -- 'zhihu', 'xueqiu', 'weibo' url TEXT UNIQUE NOT NULL, title TEXT NOT NULL, content TEXT, author TEXT, publish_time DATETIME, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 媒体资源表(图片、视频) CREATE TABLE media ( id INTEGER PRIMARY KEY AUTOINCREMENT, article_id INTEGER NOT NULL, type TEXT CHECK(type IN ('image', 'video')) NOT NULL, url TEXT NOT NULL, local_path TEXT, -- 下载后的本地路径 size_bytes INTEGER, FOREIGN KEY (article_id) REFERENCES articles (id) ON DELETE CASCADE ); -- 启用FTS5全文索引 CREATE VIRTUAL TABLE articles_fts USING fts5(title, content, content='articles'); INSERT INTO articles_fts(articles_fts) VALUES('rebuild');

    这套结构让后续的数据分析变得极其简单。比如想统计“2024年所有平台关于‘大模型’的讨论热度”,只需:

    SELECT platform, COUNT(*) as count FROM articles WHERE articles_fts MATCH '大模型' GROUP BY platform;

    3.6 PDF导出的质量控制:字体、页眉页脚与目录生成

    导出PDF不是终点,而是内容再利用的起点。很多人用pdfkit生成的PDF,中文显示为方块,页眉页脚混乱,没有目录。我的解决方案是:

    1. 字体嵌入:在HTML模板里强制指定思源黑体(Noto Sans CJK),并用@font-face引入本地字体文件:
    <style> @font-face { font-family: 'Noto Sans CJK'; src: url('./fonts/NotoSansCJKsc-Regular.otf') format('opentype'); } body { font-family: 'Noto Sans CJK', sans-serif; } </style>
    1. 页眉页脚:用pdfkit的--header-html和--footer-html参数,传入独立的HTML文件,里面用<span class="page"></span>自动显示页码;
    2. 目录生成:在HTML里为<h1>、<h2>添加id属性,用JavaScript生成锚点链接,再用CSS的@page :first { margin-top: 0; }控制首页无页眉。

    实测生成的PDF,打开速度比网页快3倍,搜索中文关键词无延迟,打印效果媲美专业排版软件。

    4. 实操过程与核心环节实现:从零开始搭建完整工作流的详细步骤

    4.1 环境初始化:三步完成基础环境搭建

    第一步:安装Playwright和依赖

    # 创建虚拟环境(推荐Python 3.10+) python -m venv zhihu-scraper-env source zhihu-scraper-env/bin/activate # Linux/Mac # zhihu-scraper-env\Scripts\activate # Windows # 安装Playwright核心库 pip install playwright==1.42.0 # 下载Chromium浏览器(自动匹配版本) playwright install chromium # 安装其他必需库 pip install pdfkit beautifulsoup4 markdownify lxml PyPDF2 python-docx

    注意:pdfkit依赖系统级的wkhtmltopdf,Mac用户用brew install wkhtmltopdf,Windows用户需下载exe并加入PATH,Linux用户用apt-get install wkhtmltopdf。如果跳过这步,后续PDF导出会报错。

    第二步:配置全局参数文件config.py

    # config.py import os # 平台配置 PLATFORM_CONFIG = { 'zhihu': { 'base_url': 'https://zhuanlan.zhihu.com', 'timeout': 60000, # 60秒超时 'concurrent_requests': 3, # 知乎反爬严,限制并发 }, 'xueqiu': { 'base_url': 'https://xueqiu.com', 'timeout': 30000, 'concurrent_requests': 5, # 雪球反爬较松 }, 'weibo': { 'base_url': 'https://weibo.com', 'timeout': 120000, # 微博加载慢,超时设长 'concurrent_requests': 2, # 视频下载耗带宽,降低并发 } } # 输出路径 OUTPUT_DIR = os.path.join(os.getcwd(), 'output') os.makedirs(OUTPUT_DIR, exist_ok=True) ARTICLES_DB = os.path.join(OUTPUT_DIR, 'articles.db') PDF_DIR = os.path.join(OUTPUT_DIR, 'pdf') IMAGES_DIR = os.path.join(OUTPUT_DIR, 'images') VIDEOS_DIR = os.path.join(OUTPUT_DIR, 'videos') # 日志配置 LOG_LEVEL = 'INFO'

    这个配置文件把所有可变参数集中管理,后续修改平台URL或超时时间,只需改这里,不用动业务代码。

    第三步:初始化SQLite数据库

    # db_init.py import sqlite3 from config import ARTICLES_DB def init_db(): conn = sqlite3.connect(ARTICLES_DB) cursor = conn.cursor() # 创建articles表 cursor.execute(''' CREATE TABLE IF NOT EXISTS articles ( id INTEGER PRIMARY KEY AUTOINCREMENT, platform TEXT NOT NULL, url TEXT UNIQUE NOT NULL, title TEXT NOT NULL, content TEXT, author TEXT, publish_time DATETIME, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') # 创建media表 cursor.execute(''' CREATE TABLE IF NOT EXISTS media ( id INTEGER PRIMARY KEY AUTOINCREMENT, article_id INTEGER NOT NULL, type TEXT CHECK(type IN ('image', 'video')) NOT NULL, url TEXT NOT NULL, local_path TEXT, size_bytes INTEGER, FOREIGN KEY (article_id) REFERENCES articles (id) ON DELETE CASCADE ) ''') # 创建FTS5全文索引 cursor.execute(''' CREATE VIRTUAL TABLE IF NOT EXISTS articles_fts USING fts5( title, content, content='articles' ) ''') cursor.execute('INSERT INTO articles_fts(articles_fts) VALUES("rebuild")') conn.commit() conn.close() print(f"Database initialized at {ARTICLES_DB}") if __name__ == '__main__': init_db()

    运行python db_init.py,数据库就准备好了。这是整个工作流的基石,后续所有数据都写入这里。

    4.2 知乎专栏批量下载:从URL列表到PDF的全流程

    假设你要下载知乎专栏https://zhuanlan.zhihu.com/p/123456789的所有文章。流程如下:

    第一步:获取专栏所有文章URL

    # zhihu_collector.py from playwright.sync_api import sync_playwright import re import json from config import PLATFORM_CONFIG def get_all_post_urls(column_slug): """获取专栏所有文章的URL列表""" urls = [] with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() # 设置超时 page.set_default_timeout(PLATFORM_CONFIG['zhihu']['timeout']) # 访问专栏首页 page.goto(f"https://zhuanlan.zhihu.com/{column_slug}") # 滚动到底部,触发所有文章加载 last_height = page.evaluate("document.body.scrollHeight") while True: page.evaluate("window.scrollTo(0, document.body.scrollHeight)") page.wait_for_timeout(2000) # 等待2秒加载 new_height = page.evaluate("document.body.scrollHeight") if new_height == last_height: break last_height = new_height # 提取所有文章链接 links = page.eval_on_selector_all( 'a[href^="/p/"]', 'elements => elements.map(el => el.href)' ) # 过滤掉非文章链接(如首页、关于等) for link in links: if re.match(r'https://zhuanlan\.zhihu\.com/p/\d+', link): urls.append(link) browser.close() return list(set(urls)) # 去重 # 示例:获取专栏所有URL urls = get_all_post_urls('ai-art') print(f"Found {len(urls)} articles")

    这段代码会返回类似['https://zhuanlan.zhihu.com/p/123456789', ...]的列表。

    第二步:逐个下载文章并存入数据库

    # zhihu_downloader.py import sqlite3 from playwright.sync_api import sync_playwright import re from bs4 import BeautifulSoup from config import PLATFORM_CONFIG, ARTICLES_DB, PDF_DIR import os def clean_zhihu_content(html): # 此处复用前面定义的clean_zhihu_content函数 pass def save_article_to_db(platform, url, title, content, author, publish_time): """保存文章到SQLite数据库""" conn = sqlite3.connect(ARTICLES_DB) cursor = conn.cursor() cursor.execute(''' INSERT OR REPLACE INTO articles (platform, url, title, content, author, publish_time) VALUES (?, ?, ?, ?, ?, ?) ''', (platform, url, title, content, author, publish_time)) article_id = cursor.lastrowid conn.commit() conn.close() return article_id def download_zhihu_article(url): """下载单篇知乎文章""" with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() page.set_default_timeout(PLATFORM_CONFIG['zhihu']['timeout']) # 拦截API请求获取原始数据 def handle_route(route, request): if 'api/v4/columns' in request.url and 'posts' in request.url: response = route.fetch() data = response.json() # 提取文章信息 for post in data['data']: if post['url'] == url: title = post['title'] content = clean_zhihu_content(post['content']) author = post['author']['name'] publish_time = post['created_time'] # 保存到数据库 article_id = save_article_to_db('zhihu', url, title, content, author, publish_time) # 生成PDF pdf_path = os.path.join(PDF_DIR, f"zhihu_{article_id}.pdf") os.makedirs(PDF_DIR, exist_ok=True) import pdfkit options = { 'encoding': "UTF-8", 'page-size': 'A4', 'margin-top': '0.75in', 'margin-right': '0.75in', 'margin-bottom': '0.75in', 'margin-left': '0.75in', 'no-outline': None } pdfkit.from_string( f"<h1>{title}</h1><p>作者:{author}</p><p>发布时间:{publish_time}</p>{content}", pdf_path, options=options ) print(f"Saved PDF: {pdf_path}") return route.continue_() else: route.continue_() page.route('**/api/**', handle_route) page.goto(url) browser.close() # 批量下载 for url in urls[:10]: # 先试10篇 try: download_zhihu_article(url) except Exception as e: print(f"Failed to download {url}: {e}")

    运行后,output/pdf/目录下会出现zhihu_1.pdf、zhihu_2.pdf等文件,articles.db里也写入了对应记录。

    4.3 雪球文章导出:处理无登录态的挑战

    雪球不需要登录就能看文章,但需要处理广告和动态加载。代码如下:

    # xueqiu_downloader.py from playwright.sync_api import sync_playwright import sqlite3 from bs4 import BeautifulSoup from config import PLATFORM_CONFIG, ARTICLES_DB, PDF_DIR import os def extract_xueqiu_content(page): """提取雪球文章内容""" # 等待文章标题出现 page.wait_for_selector('h1.article-title', timeout=30000) # 移除广告层 page.evaluate(""" document.querySelectorAll('.ad-container, .mask-layer, .article-footer').forEach(el => el.remove()); """) # 提取文章主体 content_html = page.locator('article').inner_html() # 清洗HTML soup = BeautifulSoup(content_html, 'html.parser') # 移除所有script和style for tag in soup(['script', 'style']): tag.decompose() # 只保留语义化标签 allowed_tags = ['p', 'h1', 'h2', 'h3', 'img', 'ul', 'ol', 'li', 'br', 'blockquote'] for tag in soup.find_all(True): if tag.name not in allowed_tags: tag.unwrap() return str(soup) def download_xueqiu_article(url): """下载单篇雪球文章""" with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() page.set_default_timeout(PLATFORM_CONFIG['xueqiu']['timeout']) # 注入防检测脚本 page.add_init_script(""" Object.defineProperty(navigator, 'webdriver', {get: () => false}); Object.defineProperty(navigator, 'plugins', {get: () => [1,2,3]}); """) page.goto(url) # 提取内容 content = extract_xueqiu_content(page) title = page.title() # 从URL提取作者和时间(雪球URL格式:https://xueqiu.com/XXXXXX/YYYYYY) author = url.split('/')[3] publish_time = page.evaluate("document.querySelector('time').innerText") # 保存到数据库 conn = sqlite3.connect(ARTICLES_DB) cursor = conn.cursor() cursor.execute(''' INSERT OR REPLACE INTO articles (platform, url, title, content, author, publish_time) VALUES (?, ?, ?, ?, ?, ?) ''', ('xueqiu', url, title, content, author, publish_time)) article_id = cursor.lastrowid conn.commit() conn.close() # 生成PDF pdf_path = os.path.join(PDF_DIR, f"xueqiu_{article_id}.pdf") os.makedirs(PDF_DIR, exist_ok=True) import pdfkit pdfkit.from_string( f"<h1>{title}</h1><p>作者:{author}</p><p>发布时间:{publish_time}</p>{content}", pdf_path ) print(f"Saved Xueqiu PDF: {pdf_path}") browser.close() # 示例:下载雪球文章 download_xueqiu_article('https://xueqiu.com/1234567890/987654321')

    4.4 微博内容下载:图片、视频、文字的一站式采集

    微博的难点在于资源分散,我们用一个函数统一处理:

    # weibo_downloader.py import os import requests from playwright.sync_api import sync_playwright from config import PLATFORM_CONFIG, ARTICLES_DB, IMAGES_DIR, VIDEOS_DIR import sqlite3 def download_media(url, local_dir, media_type): """通用媒体下载函数""" os.makedirs(local_dir, exist_ok=True) filename = os.path.basename(url) local_path = os.path.join(local_dir, filename) # 避免重复下载 if os.path.exists(local_path): return local_path try: response = requests.get(url, timeout=60) response.raise_for_status() with open(local_path, 'wb') as f: f.write(response.content) return local_path except Exception as e: print(f"Failed to download {url}: {e}") return None def download_weibo_post(url): """下载单条微博(含文字、图片、视频)""" with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() page.set_default_timeout(PLATFORM_CONFIG['weibo']['timeout']) page.goto(url) # 提取文字内容 text_content = page.locator('article').text_content() title = f"微博_{url.split('/')[-1]}" # 获取图片URL image_urls = page.evaluate(""" () => { const urls = []; document.querySelectorAll('article img').forEach(img => { if (img.dataset.src) urls.push(img.dataset.src); }); document.querySelectorAll('article .pic-list img').forEach(img => { if (img.dataset.bsrc) urls.push(img.dataset.bsrc); }); return [...new Set(urls)]; } """) # 获取视频URL video_urls = page.evaluate(""" () => { const urls = []; document.querySelectorAll('article video').forEach(video => { if (video.src && video.src.includes('weibo.com')) { urls.push(video.src); } }); return urls; } """) # 保存文字到数据库 conn = sqlite3.connect(ARTICLES_DB) cursor = conn.cursor() cursor.execute(''' INSERT OR REPLACE INTO articles (platform, url, title, content, publish_time) VALUES (?, ?, ?, ?, ?) ''', ('weibo', url, title, text_content, '')) article_id = cursor.lastrowid conn.commit() # 下载图片 for img_url in image_urls[:5]: # 限制每条微博最多下5张图 local_path = download_media(img_url, IMAGES_DIR, 'image') if local_path: cursor.execute(''' INSERT INTO media (article_id, type, url, local_path, size_bytes) VALUES (?, ?, ?, ?, ?) ''', (article_id, 'image', img
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进