crawl4ai--bitcointalk爬虫实战项目

📌 项目目标

本项目旨在自动化抓取 Bitcointalk 论坛中指定板块的帖子数据(包括主贴和所有回复),并提取出结构化信息如标题、作者、发帖时间、用户等级、活跃度、Merit 等,以便进一步分析或使用。

本项目只供科研学习使用

核心环境:

py==3.9,  Crawl4AI==0.6.3,beautifulsoup4==4.12.3

  • 爬虫框架crawl4ai(基于异步爬虫 + 浏览器模拟)

  • HTML 解析:BeautifulSoup (bs4)

  • 异步进度显示tqdm.asyncio

  • 存储:JSON 文件格式

两个py代码文件 bitcointalk_crawler.py  和  main.py即可运行

直接给出完整代码:

bitcointalk_crawler.py :

import osfrom bs4 import BeautifulSoup
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig, JsonCssExtractionStrategy, BrowserConfig, CacheMode
import json
import re
from pathlib import Pathfrom tqdm.asyncio import tqdm as async_tqdm
# 保存路径
SAVE_DIR = "../bitcointalk/"
os.makedirs(SAVE_DIR, exist_ok=True)# url = "https://bitcointalk.org/index.php?board=77"  # Bitcoin > Bitcoin Discussion > Press
# board = "Bitcoin Discussion_Press"
# board_url = "https://bitcointalk.org/index.php?board=74"   # Bitcoin > Bitcoin Discussion > Legal
# board = "Bitcoin Discussion_Legal"board_url = "https://bitcointalk.org/index.php?board=6"   #  Bitcoin > Development_Technical_Discussion子板块的url
board = "Bitcoin Development_Technical_Discussion"  # 保存文件的名称关键词
bitcointalk_page = 346  # 设置每个子板块爬取的页数
##使用时只修改以上三个参数即可完美运行# board_url = "https://bitcointalk.org/index.php?board=8"   #  Economy > Trading Discussion
# board = " Economy Trading Discussion"URL_path=SAVE_DIR+board+"_bitcointalk_urls.json"
DONE_URLS_FILE = SAVE_DIR+board+"bitcointalk_done_urls.json"
RESULTS_FILE = SAVE_DIR+board+"bitcointalk_results.json"
# JavaScript:点击“下一页”并等待刷新
js_click_next_and_wait = """
(async () => {const getTopicTitles = () => {return Array.from(document.querySelectorAll('.tborder a')).map(a => a.textContent.trim()).join('||');};const initialTitles = getTopicTitles();const nextButton = Array.from(document.querySelectorAll('#bodyarea #toppages .prevnext a.navPages')).find(a => a.textContent.trim() === '»');if (nextButton) nextButton.click();while (true) {await new Promise(resolve => setTimeout(resolve, 200));const currentTitles = getTopicTitles();if (currentTitles !== initialTitles) break;}
})();
"""# schema 提取规则
schema = {"name": "BitcointalkList","baseSelector": "#bodyarea .tborder a[href*='topic=']","fields": [{"name": "title", "selector": "a", "type": "text", "transform": "strip"},{"name": "url", "selector": "a", "type": "attr:href"},],
}# 翻页爬取url
async def crawl_bitcointalk_dynamic_list(board_url, max_pages: int = 3):print("开始收集url")browser_config = BrowserConfig(headless=True, java_script_enabled=True)async with AsyncWebCrawler(config=browser_config) as crawler:all_urls = []# url = "https://bitcointalk.org/index.php?board=77"   Bitcoin Discussion > Press# url = "https://bitcointalk.org/index.php?board=74"   Bitcoin Discussion > Legalsession_id = "bitcointalk_session"for page in range(max_pages):offset = page * 40page_url = f"{board_url}.{offset}"urls = []config = CrawlerRunConfig(cache_mode=CacheMode.BYPASS,css_selector="#bodyarea .tborder .windowbg a[href*='topic=']",extraction_strategy=JsonCssExtractionStrategy(schema),# js_code=js_click_next_and_wait if page > 0 else None,# js_only=page > 0,session_id=session_id,)result = await crawler.arun(url=page_url, config=config)# print("首页结果:", result.markdown)# print("首页结果:", result)if result.success:html_content = result.html  # 假设这里是原始 HTML 字符串urls = re.findall(r'href="(https://bitcointalk\.org/index\.php\?topic=\d+\.0)"', html_content)for url in urls:all_urls.append(url)else:print(f"⚠️ 第 {page + 1} 页抓取失败")print(f"✅ 目前共 {len(all_urls)} 个url")with open(URL_path, "w", encoding="utf-8") as f:json.dump(all_urls, f, ensure_ascii=False, indent=2)print(f"爬取完毕✅ 共 {len(all_urls)} 个url")
# 更新 URL 为下一页return all_urls# 进入每个帖子页面抓取详细内容async def crawl_bitcointalk_post_detail_with_replies(url: str) -> dict:"""输入一个 Bitcointalk 帖子 URL,返回结构化的主贴与回复数据"""result_data = {"url": url,"time": "unknown","title": "","content": "","read_count": -1,"author": "unknown","rank": "unknown","activity": 0,"merit": 0,"replies_count": 0,"replies": []}schema = {"name": "Bitcointalk Thread","baseSelector": ".bordercolor .msgcl1",  # 每个帖子(主贴 + 回复) .bordercolor"fields": [{"name": "author","selector": ".poster_info > b > a","type": "text",},{"name": "author_inf","selector": ".poster_info .smalltext","type": "text",},{"name": "time","selector": ".td_headerandpost .smalltext",#quickModForm > table.bordercolor > tbody > tr:nth-child(1) > td > table > tbody > tr > td > table > tbody > tr:nth-child(1) > td.td_headerandpost"type": "text",},{"name": "content","selector": ".td_headerandpost .post","type": "text",},],}browser_config = BrowserConfig(headless=True, java_script_enabled=True)crawler_config = CrawlerRunConfig(cache_mode=CacheMode.BYPASS,extraction_strategy=JsonCssExtractionStrategy(schema),delay_before_return_html=1,magic=True,simulate_user=True,override_navigator=True,)try:# 启动爬虫async with AsyncWebCrawler(config=browser_config) as crawler:result = await crawler.arun(url=url,config=crawler_config,# js_code=js_click_next_and_wait if page > 0 else None,# js_only=page > 0,)# print(result)soup = BeautifulSoup(result.cleaned_html, "html.parser")# print(soup)# 帖子标题# 查找包含帖子标题和阅读次数的 <td>topic_td = soup.find("td", string=re.compile(r"Topic:\s*\s*.+?\(Read\s+\d+\s+times\)"))# print(topic_td)if topic_td:# 假设你已经获得了 td 的文本text = topic_td.get_text(strip=True)# 匹配标题和阅读次数match = re.search(r"Topic:\s*(.+?)\s*\(Read\s+(\d+)\s+times\)", text)if match:title = match.group(1)  # 帖子标题read_count = int(match.group(2))  # 阅读次数# print("标题:", title)# print("阅读次数:", read_count)else:print("❌ 无法匹配标题和阅读次数")else:title = "unknown"read_count = -1# 保存结果result_data["title"] = titleresult_data["read_count"] = read_countraw_posts = json.loads(result.extracted_content)# print(raw_posts)print(f"✅ 成功提取 {len(raw_posts)} 条帖子")posts = []main_content = raw_posts[0].get("content", "")for  i, raw_post in enumerate(raw_posts):post = {}author_inf = raw_post.get("author_inf", "")rank_match = re.search(r"^(Sr\. Member|Hero Member|Legendary|Full Member|Member|Newbie)", author_inf)activity_match = re.search(r"Activity:\s*(\d+)", author_inf)merit_match = re.search(r"Merit:\s*(\d+)", author_inf)post["author"] = raw_post.get("author", "")post["rank"] = rank_match.group(1) if rank_match else "unknown"post["activity"] = int(activity_match.group(1)) if activity_match else 0post["merit"] = int(merit_match.group(1)) if merit_match else 0post["time"] = raw_post.get("time", "unknown")# 如果是回复,并包含主贴内容,就移除主贴部分if i > 0 and main_content in raw_post.get("content", ""):cleaned_text = raw_post.get("content", "").replace(main_content, "").strip()post["content"] = cleaned_textelse:post["content"] = raw_post.get("content", "")# print(f"作者: {post['author']}, 时间: {post['time']}, 等级: {post['rank']}, 活动: {post['activity']}, Merit: {post['merit']}, 内容: {post['content'][:50]}...,")posts.append(post)# 主贴 + 回复整合if raw_posts:main_post = posts[0]result_data.update({"author": main_post["author"],"time": main_post["time"],"rank": main_post["rank"],"activity": main_post["activity"],"merit": main_post["merit"],"content": main_post["content"],"replies_count": len(posts) - 1,  # 回复数量"replies": posts[1:]})# print(result_data)return result_dataexcept Exception as e:print(f"❌ 抓取失败:{e}")return result_dataasync def load_urls(URL_path,board_url,pages):if os.path.exists(URL_path):print(f"✅ url文件已存在,跳过爬取url: {URL_path}")with open(URL_path, "r", encoding="utf-8") as f:ALL_URLS = json.load(f)else:ALL_URLS = await crawl_bitcointalk_dynamic_list(board_url, max_pages=pages)  #获取帖子url并保存到文件return ALL_URLSdef load_done_urls():if Path(DONE_URLS_FILE).exists():with open(DONE_URLS_FILE, "r", encoding="utf-8") as f:return set(json.load(f))return set()def save_done_urls(done_urls: set):with open(DONE_URLS_FILE, "w", encoding="utf-8") as f:json.dump(list(done_urls), f, ensure_ascii=False, indent=2)def append_post(post: dict):if not Path(RESULTS_FILE).exists():with open(RESULTS_FILE, "w", encoding="utf-8") as f:json.dump([post], f, ensure_ascii=False, indent=2)else:with open(RESULTS_FILE, "r+", encoding="utf-8") as f:data = json.load(f)data.append(post)f.seek(0)json.dump(data, f, ensure_ascii=False, indent=2)f.truncate()async def crawl_bitcointalk_by_keywords(pages=bitcointalk_page, board_url=board_url):ALL_URLS = await load_urls(URL_path,board_url,pages)all_done_urls = load_done_urls()new_done_urls = set()print(f"🔍 Bitcointalk - urls - start")for URL in async_tqdm(ALL_URLS, desc="📡 正在异步爬取"):if URL in all_done_urls:print(f"⏩ 已完成跳过:{URL}")continuetry:print(f"📥 正在抓取内容:{URL}")final_post = await crawl_bitcointalk_post_detail_with_replies(URL)# ✅ 实时保存append_post(final_post)new_done_urls.add(URL)# ✅ 实时保存进度save_done_urls(all_done_urls.union(new_done_urls))print(f"✅ 已保存:{URL}")except Exception as e:print(f"❌ 错误跳过:{URL} - {e}")continueprint("🎉 全部关键词抓取完毕")

main.py:

import asynciofrom bitcointalk_crawler import crawl_bitcointalk_by_keywordsasync def main():keywords = ["bitcoin", "crypto"]# 爬取 Bitcointalkprint("开始爬取 Bitcointalk...")await crawl_bitcointalk_by_keywords() # # 爬取 Twitter# print("开始爬取 Twitter...")# await crawl_twitter_by_keywords(keywords)## # 爬取 Reddit# print("开始爬取 Reddit...")# reddit_data = await crawl_reddit_by_keywords(keywords, pages)# save_data("Reddit", reddit_data)if __name__ == "__main__":asyncio.run(main())

爬取结果:

URLS_path保存所有帖子的 URL 列表的 JSON 文件
DONE_URLS_FILE已经爬取完成的 URL 列表,防止重复抓取
RESULTS_FILE保存结构化帖子内容的结果文件

🔁 爬取流程总览

第一步:获取帖子列表 URL

函数:crawl_bitcointalk_dynamic_list(board_url, max_pages)

  1. 访问指定板块的 URL(例如技术讨论区)。

  2. 模拟翻页抓取前 max_pages 页的帖子链接。

  3. 通过 CSS selector 提取帖子标题及 URL。

  4. 使用正则进一步筛选帖子链接。

  5. 将结果保存至 URL_path 指定的文件中。

🔗 示例结果:

[ "https://bitcointalk.org/index.php?topic=123456.0", "https://bitcointalk.org/index.php?topic=234567.0" ]


第二步:提取主贴与回复详细内容

函数:crawl_bitcointalk_post_detail_with_replies(url)

对每个帖子 URL:

  1. 使用爬虫打开页面并等待加载完成。

  2. 提取原始 HTML,并用 BeautifulSoup 解析出标题与阅读数。

  3. 使用 JSON CSS 提取策略,批量提取每个楼层的内容:

    • 作者

    • 作者信息(等级、Merit、Activity)

    • 发布时间

    • 帖子正文内容

  4. 将第一个帖子识别为主贴,后续为回复。

  5. 主贴与所有回复打包为结构化字典。

📌 关键正则解析逻辑

提取阅读数和标题:

match = re.search(r"Topic:\s*(.+?)\s*\(Read\s+(\d+)\s+times\)", text)

提取用户等级 / 活跃度 / Merit:

rank_match = re.search(r"^(Sr\. Member|Hero Member|Legendary|Full Member|Member|Newbie)", author_inf) activity_match = re.search(r"Activity:\s*(\d+)", author_inf) merit_match = re.search(r"Merit:\s*(\d+)", author_inf)


✅ 成功与失败处理机制

  • 成功后保存:append_post()save_done_urls() 实时写入文件

  • 如果请求或解析失败,则打印错误并继续下一个 URL(不会中断全流程)

  • 日志中提供清晰提示(✅、❌、⏩ 等符号)

📚 示例数据输出

保存在 RESULTS_FILE 中的 JSON 数组,每个元素是一个完整帖子的结构化数据,便于后续 NLP、分类、情感分析等处理。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/89325.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/89325.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

调用 System.gc() 的弊端及修复方式

弊端分析不可控的执行时机System.gc() 仅是 建议 JVM 执行垃圾回收&#xff0c;但 JVM 可自由忽略该请求&#xff08;尤其是高负载时&#xff09;。实际回收时机不确定&#xff0c;无法保证内存及时释放。严重的性能问题Stop-The-World 停顿&#xff1a;触发 Full GC 时会暂停所…

git merge 和 git rebase 的区别

主要靠一张图&#xff1a;区别 git merge git checkout feature git merge master此时在feature上git会自动产生一个新的commit 修改的是当前分支 feature。 git rebase git checkout feature git rebase master&#xff08;在feature分支上执行&#xff0c;修改的是master分支…

Java学习--JVM(2)

JVM提供垃圾回收机制&#xff0c;其也是JVM的核心机制&#xff0c;其主要是实现自动回收不再被引用的对象所占用的内存&#xff1b;对内存进行整理&#xff0c;防止内存碎片化&#xff1b;以及对内存分配配进行管理。JVM 通过两种主要算法判断对象是否可回收&#xff1a;引用计…

用大模型(qwen)提取知识三元组并构建可视化知识图谱:从文本到图谱的完整实现

引言 知识图谱作为一种结构化的知识表示方式&#xff0c;在智能问答、推荐系统、数据分析等领域有着广泛应用。在信息爆炸的时代&#xff0c;如何从非结构化文本中提取有价值的知识并进行结构化展示&#xff0c;是NLP领域的重要任务。知识三元组&#xff08;Subject-Relation-O…

(附源码)基于 Go 和 gopacket+Fyne 的跨平台网络抓包工具开发实录

基于 Go 和 gopacket Fyne 的跨平台网络抓包工具开发实录 一、项目背景 在网络安全、协议分析、运维排查等场景中&#xff0c;抓包工具是不可或缺的利器。Wireshark 虽然功能强大&#xff0c;但对于部分初学者或有定制需求的开发者来说&#xff0c;学习曲线较陡&#xff0c;且…

Langchain和Faiss搭建本地知识库对比

对比 对比维度及优缺点分析对比维度LangChain&#xff08;封装 FAISS&#xff09;直接使用 FAISS易用性✅ 高&#xff0c;提供高级封装&#xff0c;简化开发流程❌ 中等&#xff0c;需要熟悉 FAISS API学习成本✅ 低&#xff0c;适合快速开发❌ 高&#xff0c;需要掌握 FAISS 的…

Java常用命令汇总

JDK 工具命令jps&#xff08;Java Virtual Machine Process Status Tool&#xff09;命令示例&#xff1a;jps -l 应用场景&#xff1a;列出当前系统中所有Java进程的PID和主类名&#xff0c;常用于快速定位Java应用的进程ID。javac&#xff08;Java Compiler&#xff09;命令示…

Llama 2:开放基础模型与微调聊天模型

温馨提示&#xff1a; 本篇文章已同步至"AI专题精讲" Llama 2&#xff1a;开放基础模型与微调聊天模型 摘要 在本研究中&#xff0c;我们开发并发布了 Llama 2&#xff0c;一组预训练和微调的大型语言模型&#xff08;LLMs&#xff09;&#xff0c;其规模从 70 亿参…

ThinkPHP 8 在 Apache 下启用伪静态

ThinkPHP 8 在 Apache 下启用伪静态&#xff0c;需要配置 .htaccess 文件并确保 Apache 支持 URL 重写。以下是详细设置步骤&#xff1a;1. 启用 Apache 重写模块首先确保 Apache 的 mod_rewrite 模块已启用。编辑 Apache 配置文件&#xff08;通常是 /etc/apache2/apache2.con…

Android开发中Retrofit使用方法与底层原理详解

Retrofit 是 Android 开发中一个 类型安全、基于注解、高度解耦 的 RESTful HTTP 客户端库&#xff0c;由 Square 公司开发。它极大地简化了 Android 应用与 Web 服务进行网络交互的过程。 核心价值&#xff1a; 声明式 API 定义&#xff1a; 使用 Java/Kotlin 接口和注解描述 …

基于FPGA的IIC控制EEPROM读写(2)

基于FPGA的IIC控制EEPROM读写 文章目录基于FPGA的IIC控制EEPROM读写一、EEPROM简介二、代码实现——个人理解1、状态机2、仿真效果3、上板验证4、代码top.viic_master.vuart三、代码实现——复用性较高的IIC模块1、框架设计2、状态机设计3、仿真效果4、上板验证5、代码top.viic…

C# 界面程序在23H2型号系统中无法退出

20250716记录 环境&#xff1a;c# winform问题描述&#xff1a;主界面退出直接使用了Environment.Exit(0); 程序假死&#xff0c;无法关闭解决措施&#xff1a;//使用 this.Close();以下代码目标&#xff1a;执行完程序自身后&#xff0c;删除指定文件&#xff08;可用于程序文…

Kafka——集群核心参数配置

引言在分布式系统中&#xff0c;Kafka 凭借其高吞吐量、低延迟和强大的扩展性&#xff0c;成为数据管道和流处理的首选解决方案。然而&#xff0c;要充分发挥 Kafka 的性能和稳定性&#xff0c;正确配置集群参数至关重要。为什么参数配置如此重要&#xff1f;Kafka 的参数配置直…

单臂路由实现VLAN互通实验

实验拓扑图实验需求&#xff1a;按照图示为 PC3 和 PC4 配置 IP 地址和网关PC3 属于 Vlan10&#xff0c;PC4 属于 Vlan20&#xff0c;配置单臂路由实现 Vlan10 和 Vlan20 三层互通PC3 和 PC4 可以互通实验步骤&#xff1a;1.PC 配置 IP 地址2.PC3 属于 Vlan10&#xff0c;PC4 属…

基于渐进式迁移学习网络(PTLN)​的小样本故障诊断模型

目录 一、研究背景与挑战​ ​二、创新方法:渐进式迁移学习网络(PTLN)​​ ​1. 核心架构​编辑 ​2. 训练优化​ 三、核心代码 四、实验结果与优势​ ​1. 数据集​ ​2. 性能对比​ ​3. 关键验证​ 五、工程价值与未来方向​ 六、补充信息​ 一、研究背景与挑…

网络原理 —— HTTP

通过网络初识&#xff0c;我们认识了网络的协议栈&#xff0c;TCP/IP 分为五层&#xff1a;应用层&#xff0c;传输层&#xff0c;网络层&#xff0c;数据链路层&#xff0c;物理层。也介绍了其中的关键协议。而这些协议的理解&#xff0c;是我们写网络代码的基础。 应用层&…

docker--安装--原理

安装 链接 启动之后&#xff0c;docker状态查看&#xff1a; sudo systemctl status docker 添加普通用户到docker用户组&#xff1a; sudo usermod -aG docker $USER# 重启或者使用以下命令刷新组权限&#xff1a;newgrp docker 原理

Java并发第一篇(从零开始:一文读懂Java并发编程核心基础)

从零开始&#xff1a;一文读懂Java并发编程核心基础一. 为什么需要并发编程&#xff1f;二. 并发编程的“另一面”&#xff1a;挑战与代价2.1 频繁的上下文切换2.2 线程安全问题&#xff08;如&#xff1a;死锁&#xff09;三. 夯实基础&#xff1a;必须掌握的核心概念与操作3.…

【删库跑路】一次删除pip的所有第三方库

进入命令行&#xff0c;先list看下库存pip list导出所有的第三方库至一文件列表pip freeze >requirements.txt按照列表卸载所有库pip uninstall -r requirements.txt -y再list看下&#xff0c;可见库存已清空

python 【技术面试题和HR面试题】➕列表操作、条件判断、循环、函数定义编程题

1.技术面试题 &#xff08;1&#xff09;解释Linux中的进程、线程和守护进程的概念&#xff0c;以及如何管理它们&#xff1f; 答&#xff1a; 进程 概念&#xff1a;程序运行的实例&#xff0c;有独立资源&#xff08;如内存&#xff09;&#xff0c;是系统调度的基本单位。 管…