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
英文地址,請注明出處:http://en.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;是系統調度的基本單位。 管…