前言:
社交网络中用户生成的海量数据,社交网络数据的多样性和复杂性
如何高效地从海量的数据中获取和处理我们需要的信息资源?
该微博爬虫能够从社交网络平台中地提取文本、图片和用户之间的转发关系,并将这些数据结构化存储到图数据库中,为后续的数据分析和挖掘提供支持。
总体思路
在爬取静态的文本与图片时,首先使用libcurl库进行http请求,从指定URL下载网页内容,再使用Gumbo解析HTML内容以提取文本和图像URL,并字符串形式储存,Gumbo DOM树转换为Pugixml文档,使用XPath查找图像URL,将提取的文本内容保存到一个文件中,图像URL保存到另一个文件中。
在爬取动态的转发关系时,使用selenium库模拟用户操作,动态加载网页,beautifulsoup库解析网页内容,如果文本包含//,则被认为转发自其他用户,提取用户名,否则,被认为转发自原始微博,
最后将拥有用户id,转发时间,转发来源的csv用pandas库读取内容进行数据处理,neo4j储存微博用户与转发关系
设计方法
在设计方法上,本实验综合利用了C++的高效性和Python的灵活性。具体方法如下:
2.1工具与库的选择:为了实现高效的网页下载和HTML解析,选择了libcurl和Gumbo库;为了处理动态网页内容和模拟用户操作,选择了selenium库;为了解析HTML并提取特定数据,选择了BeautifulSoup库;为了处理CSV文件和进行数据分析,选择了pandas库;最后,为了实现数据的图数据库存储,选择了Neo4j。
2.2模块化设计:整个系统分为多个功能模块,包括爬取文本和图片模块、爬取转发关系模块、数据处理与存储模块。每个模块负责特定的任务,模块之间相互协作,共同完成整个数据爬取和存储流程。
3.1性能分析:
3.1.1高效性,算法需要能够高效地处理大量网页请求,特别是对动态加载内容的网页,需要快速响应和处理。
3.1.2资源利用,为了减少对系统资源的占用,包括CPU、内存和网络带宽的使用,以提高系统的整体性能。
3.2功能分析:
3.2.1准确性:算法必须准确地提取网页中的文本、图片URL和用户转发关系,确保数据的完整性和正确性
3.2.2鲁棒性:算法需要具备较强的鲁棒性,能够处理各种异常情况,如网页内容变化、网络连接中断等。
3.2.3扩展性:系统设计需要具备良好的扩展性,便于后续增加新的功能模块,例如爬取用户的点赞关系、评论内容等。
part1:用c++实现文本与图片的爬取,直接把我的代码粘过来,注释很详细
在爬取文本与图片时,首先初始化并且使用libcurl库从指定URL下载网页内容,再使用Gumbo解析HTML内容,提取文本和图像URL,并储存到字符串里面,Gumbo DOM树转换为Pugixml文档,使用XPath查找图像URL,将提取的文本内容保存到一个文件中,图像URL保存到另一个文件中。
//(爬取文本,转发关系)
#include <iostream>
#include <fstream>//用于文件操作
#include <string>
#include <curl/curl.h>//用于 libcurl 库,进行 HTTP 请求
#include <gumbo.h>//用于 Gumbo 库,进行 HTML 解析
#include <pugixml.hpp>//用于 pugixml 库,进行 XML 操作
#include <vector>//用于 STL 容器
#include <set>//用于 STL 容器
using namespace std;
size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp) {
((string*)userp)->append((char*)contents, size * nmemb);
return size * nmemb;//返回处理的数据总大小,告诉 libcurl 已经处理了多少数据。
}
string download_url(const string& url, const string& cookie_string) {
CURL* curl;//一个指向 CURL 句柄的指针,用于执行 HTTP 请求。
CURLcode res;// 用于存储 curl 操作的返回码
string readBuffer;//用于存储下载的内容。
//1,初始化 CURL
curl = curl_easy_init();//初始化一个 CURL 句柄,如果初始化成功,返回一个 CURL 句柄,否则返回 nullptr
//2,设置 CURL 选项
if (curl) {
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());//CURLOPT_URL: 设置要下载的 URL
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);//CURLOPT_WRITEFUNCTION: 设置写入数据的回调函数 WriteCallback。
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);//CURLOPT_WRITEDATA: 设置回调函数的用户数据,这里是 readBuffer,用于存储下载的数据。
curl_easy_setopt(curl, CURLOPT_COOKIE, cookie_string.c_str());
curl_easy_setopt(curl, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36");//CURLOPT_USERAGENT: 设置请求的 User-Agent 字符串,模拟浏览器
//3,执行 CURL 请求
res = curl_easy_perform(curl);//返回操作的结果码。
if (res != CURLE_OK) {
cout<< "下载URL内容错误: " << curl_easy_strerror(res) << endl;//转可读字符串
}
curl_easy_cleanup(curl);//清理
}
return readBuffer;
}
void search_for_text(GumboNode* node, string& text) {//GumboNode* node: 指向当前处理的 Gumbo 节点的指针;std::string& text: 用于存储提取出的文本内容的字符串引用。
if (node->type == GUMBO_NODE_TEXT) {//看是不是文本
text.append(node->v.text.text);//是的话追加到字符串text
}
else if (node->type == GUMBO_NODE_ELEMENT && node->v.element.tag != GUMBO_TAG_SCRIPT && node->v.element.tag != GUMBO_TAG_STYLE) {
GumboVector* children = &node->v.element.children;//获取该节点的子节点集合(GumboVector* children)遍历子节点集合,对每个子节点递归调用 search_for_text 函数。
for (unsigned int i = 0; i < children->length; ++i) {
search_for_text(static_cast<GumboNode*>(children->data[i]), text);
}
}
}
string extract_text_from_html(const string& html_content) {
GumboOutput* output = gumbo_parse(html_content.c_str());//调用 gumbo_parse 函数将 html_content 转换为 C 字符串并解析为 Gumbo DOM 树。output 是解析后的 Gumbo 树的根节点
if (!output) {
cout << "Gumbo解析失败" << endl;
return "";
}
string text;
search_for_text(output->root, text);
gumbo_destroy_output(&kGumboDefaultOptions, output);//销毁
return text;
}
void traverse_gumbo_node(GumboNode* gumbo_node, pugi::xml_node& pugi_node) {
if (gumbo_node->type != GUMBO_NODE_ELEMENT) return;//检查当前 Gumbo 节点是否是元素节点,如果不是则直接返回。
GumboElement* element = &gumbo_node->v.element;//获取当前节点的元素部分并将其存储在 element 中
const char* tag_name = gumbo_normalized_tagname(element->tag);//获取标准化的标签名,并将其存储在 tag_name 中
if (tag_name && tag_name[0]) {//检查标签名是否有效(非空且不为空字符串)
pugi::xml_node new_node = pugi_node.append_child(tag_name);
// 添加节点的属性
GumboVector* attributes = &element->attributes;
for (unsigned int i = 0; i < attributes->length; ++i) {//将每个属性添加到 new_node 中。属性的名称和值分别为 attribute->name 和 attribute->value。
GumboAttribute* attribute = (GumboAttribute*)attributes->data[i];
new_node.append_attribute(attribute->name) = attribute->value;
}
// 递归处理子节点
GumboVector* children = &element->children;
for (unsigned int i = 0; i < children->length; ++i) {
traverse_gumbo_node((GumboNode*)children->data[i], new_node);
}
}
}
void parse_html(const string& html_content, string& text, set<std::string>& images) {
GumboOutput* output = gumbo_parse(html_content.c_str());
if (!output) {
cout << "Gumbo解析失败" <<endl;
return;
}
pugi::xml_document doc;
// 创建一个 pugixml 文档对象 doc。调用 traverse_gumbo_node 函数,将 Gumbo 的 DOM 树转换为 pugixml 的 DOM 树。
traverse_gumbo_node(output->root, doc);
// 提取文本内容
text = extract_text_from_html(html_content);
// 提取图像URL
for (pugi::xpath_node xpath_node : doc.select_nodes("//img")) {
pugi::xml_node img_node = xpath_node.node();//XPath 表达式 //img 查找 pugixml 文档中所有的 <img> 标签。
images.insert(img_node.attribute("src").value());
}
gumbo_destroy_output(&kGumboDefaultOptions, output);//销毁 Gumbo 解析器的输出对象 output,释放其占用的内存
}
// 函数用于将文本内容保存到文件
void save_text_to_file(const string& text, const string& filename) {//const string& text:需要保存的文本内容。const string& filename:目标文件的文件名。
ofstream file(filename);//创建一个 ofstream 对象 file,并打开名为 filename 的文件进行输出操作。
if (file.is_open()) {
file << text;
file.close();
}
else {
cout << "打开文件错误: " << filename << endl;
}
}
// 函数用于将图像URL保存到文件
void save_images_to_file(const set<string>& images, const string& filename) {
ofstream file(filename);
if (file.is_open()) {
for (const string& image : images) {
file << image << endl;
}
file.close();
}
else {
cerr << "打开文件错误: " << filename << endl;
}
}
int main() {
string url = "https://s.weibo.com/weibo?q=%E5%88%98%E6%98%8A%E7%84%B6%20%E6%9C%95%E5%92%8C%E5%AC%9B%E5%AC%9B%E4%BD%95%E6%9B%BE%E6%9C%89%E8%BF%87%E5%AB%8C%E9%9A%99&topic_ad=";
string cookie_string = "SINAGLOBAL=5399883731581.21.1696810732360; ariaMouseten=null; UOR=mp.csdn.net,service.weibo.com,mail.qq.com; SUB=_2A25LYf0kDeRhGeFG7VAS9ynPwzyIHXVoH3DsrDV8PUNbmtANLRXWkW1NeTFXOlSZsbpSTgfXcGAbTof6v4B8rIFe; ALF=02_1720523380; ariaDefaultTheme=default; ariaFixed=true; ariaReadtype=1; ariaStatus=true; _s_tentry=-; Apache=6084155699997.418.1718066762994; ULV=1718066762995:15:11:2:6084155699997.418.1718066762994:1717920771762";
string html_content = download_url(url, cookie_string);
if (html_content.empty()) {
cerr << "从URL下载内容失败。" << endl;
return 1;
}
string text;
set<std::string> images;
parse_html(html_content, text, images);
// 保存文本内容到文件
save_text_to_file(text, "text_content.txt");
// 保存图像URL到文件
save_images_to_file(images, "image_urls.txt");
cout << "文本内容已保存到 text_content.txt" << std::endl;
cout << "图像URL已保存到 image_urls.txt" << std::endl;
return 0;
}
part2:python爬取转发关系
在爬取链接时,使用selenium库,beautifulsoup库爬取转发关系,如果文本包含//,则被认为转发自其他用户,提取用户名,否则,被认为转发自原始微博,最后将拥有用户id,转发时间,转发来源的csv用pandas库读取内容进行数据处理,neo4j储存微博用户与转发关系
#最终
from selenium import webdriver
from selenium.webdriver.edge.service import Service
from selenium.webdriver.edge.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
import re
import time
from datetime import datetime
import pandas as pd
from bs4 import BeautifulSoup
def initialize_browser(driver_path, options, headers):
print("Initializing browser...")
service = Service(driver_path)
browser = webdriver.Edge(service=service, options=options)
browser.execute_cdp_cmd('Network.enable', {})
browser.execute_cdp_cmd('Network.setExtraHTTPHeaders', {'headers': headers})
print("Browser initialized.")
return browser
def wait_for_page_load(browser, timeout, class_name):
print("Waiting for page to load...")
WebDriverWait(browser, timeout).until(
EC.presence_of_element_located((By.CLASS_NAME, class_name))
)
print("Page loaded.")
def get_page_source(browser):
print("Getting page source...")
return browser.page_source
def scroll_page(browser):
print("Scrolling page...")
last_scroll_height = browser.execute_script("return document.body.scrollHeight")
while True:
browser.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(5)
new_scroll_height = browser.execute_script("return document.body.scrollHeight")
if new_scroll_height == last_scroll_height:
break
last_scroll_height = new_scroll_height
print("Finished scrolling page.")
def parse_page(page_html):
print("Parsing page HTML...")
soup = BeautifulSoup(page_html, "html.parser")
original_poster = soup.find("header", class_="woo-box-flex").find("a", class_="ALink_default_2ibt1")
original_user_id = original_poster.get("aria-label")
repost_entries = soup.findAll("div", class_="wbpro-scroller-item")
print("Finished parsing page HTML.")
return original_user_id, repost_entries
def extract_repost_data(repost_entries, original_user_id, processed_user_ids):
print("Extracting repost data...")
scraped_data = []
for entry in repost_entries:
# 查找包含转发文本的容器
repost_text_container = entry.find("div", class_="text").find("span")
if repost_text_container:
# 提取容器中的纯文本内容,忽略所有链接
text_content = "".join([segment for segment in repost_text_container.strings if
segment not in repost_text_container.find_all("a")])
if "//" in text_content:
# 如果文本内容中有"//",表示这是转发的微博,查找用户名的链接
username_anchor = repost_text_container.find("a")
if username_anchor:
username_link = username_anchor.get("href")
repost_user = username_link.split("/")[-1] # 获取用户名
repost_info = f"转发自:{repost_user}"
else:
repost_info = f"转发自:未知用户" # 处理没有用户名链接的情况
else:
repost_info = f"转发自:{original_user_id}" # 直接转发自原始用户
# 查找当前用户的ID
user_anchor = entry.find("div", class_="text").find("a", class_="ALink_default_2ibt1")
if user_anchor:
current_user_id = user_anchor.get_text(strip=True)
if current_user_id not in processed_user_ids:
processed_user_ids.add(current_user_id)
# 查找发布时间信息
time_info = entry.find("div", string=re.compile(r'\d{2}-\d{1,2}-\d{1,2} \d{1,2}:\d{2}'))
if time_info:
post_timestamp = time_info.text.strip()
try:
# 解析时间格式
post_timestamp = datetime.strptime(post_timestamp, '%y-%m-%d %H:%M').strftime(
'%Y-%m-%d %H:%M:%S')
except ValueError:
print(f"时间格式错误:{post_timestamp}")
# 保存提取的数据
scraped_data.append([current_user_id, post_timestamp, repost_info])
print(f"用户ID: {current_user_id}, 时间: {post_timestamp}, 转发关系:{repost_info}")
print("Finished extracting repost data.")
return scraped_data
def save_to_csv(data, output_csv):
print("Saving data to CSV...")
try:
data_frame = pd.DataFrame(data, columns=['user_id', 'time', 'repost_source'])
data_frame.to_csv(output_csv, index=False, encoding='utf-8_sig')
print(f"数据已保存到 {output_csv}")
except Exception as save_error:
print("保存CSV文件时出错: ", str(save_error))
def main():
driver_path = 'D:\\python310\\Scripts\\msedgedriver.exe'
options = Options()
options.add_argument("--headless")
options.add_argument("--disable-gpu")
req_headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0",
"cookie": "SINAGLOBAL=5399883731581.21.1696810732360; UOR=mp.csdn.net,service.weibo.com,mail.qq.com; SUB=_2A25LYf0kDeRhGeFG7VAS9ynPwzyIHXVoH3DsrDV8PUNbmtANLRXWkW1NeTFXOlSZsbpSTgfXcGAbTof6v4B8rIFe; ALF=02_1720523380; ariaDefaultTheme=default; ariaFixed=true; ULV=1718067765857:16:12:3:6228001885489.596.1718067765856:1718066762995; XSRF-TOKEN=1eo5sramGTwy_UcD1QfWa7Wp; WBPSESS=eQZNrz-Wn46RlA5U6QxZX_XMdXGL03Js8_qrxltO2fuSlg9ZjBjrhdtXezA_v15N6m50CLzRQYeD5igzwC15uiMikucPdT9KLGvXo4V-zH6MrZ6iL0wpyjfDdMfGUq7NRQzXh4fOBTtIKuY8cvP8UQ==; ariaReadtype=1; ariaStatus=true"
}
target_url = "https://weibo.com/6377374546/OhqKDemDv#repost"
output_csv = 'pachong.csv'
processed_user_ids = set()
try:
browser = initialize_browser(driver_path, options, req_headers)
browser.get(target_url)
wait_for_page_load(browser, 20, "detail_wbtext_4CRf9")
scroll_page(browser)
page_html = get_page_source(browser)
original_user_id, repost_entries = parse_page(page_html)
scraped_data = extract_repost_data(repost_entries, original_user_id, processed_user_ids)
save_to_csv(scraped_data, output_csv)
except Exception as overall_error:
print("运行中错: ", str(overall_error))
finally:
browser.quit()
if __name__ == "__main__":
main()
part3导入neo4j图数据库,进行可视化
#pandas库读取CSV文件中的数据,然后使用Neo4j数据库来存储微博用户及其转发关系
import pandas as pd#pandas用于数据处理,neo4j用于操作Neo4j图数据库
from neo4j import GraphDatabase
# 读取 CSV 文件内容,pandas的read_csv函数读取文件内容,存储在DataFrame对象df中。
file_path = r'D:\python project\pythonProject\pachong.csv' # 修改为你的 CSV 文件路径""
df = pd.read_csv(file_path)
# Neo4j 配置
uri = "bolt://localhost:7687" # 修改为你的 Neo4j URI
user = "neo4j" # 修改为你的 Neo4j 用户名
password = "zyc679613" # 修改为你的 Neo4j 密码
# GraphDatabase.driver函数连接到Neo4j数据库
driver = GraphDatabase.driver(uri, auth=(user, password))
def create_user(tx, user_id):#用于创建用户节点。在Neo4j中,节点可以有多个属性,这里我们使用MERGE命令来确保如果节点已经存在,则不会重复创建相同的节点。
tx.run("MERGE (u:User {id: $user_id})", user_id=user_id)
def create_relationship(tx, user_id, source_id):#这个函数用于创建转发关系。在Neo4j中,关系用于连接两个节点。这里我们使用MERGE命令来确保如果关系已经存在,则不会重复创建相同的关系。
tx.run("""
MATCH (u1:User {id: $user_id}), (u2:User {id: $source_id})
MERGE (u1)-[:REPOSTED]->(u2)
""", user_id=user_id, source_id=source_id)
with driver.session() as session:#使用Neo4j的driver对象创建一个会话。driver.session()返回一个上下文管理器,用于执行Cypher查询。
# 创建用户节点
for user_id in df['user_id'].unique():#遍历DataFrame中不重复的用户ID
session.execute_write(create_user, user_id)#调用create_user函数创建用户节点,并传递用户ID作为参数。
# 创建转发关系
for index, row in df.iterrows():#遍历DataFrame中的每一行,获取用户ID和转发源。
source_id = row['repost_source'].split(":")[-1] # 从转发源字段中提取转发源ID。转发源字段的格式是"转发自:xxx",这里使用split函数将其拆分,并取最后一个部分作为转发源ID。
session.execute_write(create_relationship, row['user_id'], source_id)#调用create_relationship函数创建转发关系,并传递用户ID和转发源ID作为参数。
driver.close()
实验结果与分析
C++提取文本与图片的代码通过
提取图片的文件内容
提取文本的文件内容
python爬取转发关系的代码运行过程
python爬取转发关系的csv文件结果
csv文件导入neo4j的节点截图
Neo4j的转发关系截图
-
总结与心得体会
在完成这次社交网络爬虫系统设计与实现的过程中,我深刻体会到了整个项目从构思到实现的复杂性和挑战性。通过这个项目,不仅提高了我在网络爬虫、数据处理和图数据库等方面的技能,还让我对大规模数据处理和系统设计有了更深入的理解。
在设计阶段,我首先对社交网络数据的特点和目标进行了详细分析。社交网络上的数据不仅包括静态的文本和图片,还包含动态加载的转发关系、评论和点赞等互动信息。因此,系统需要具备处理不同类型网页内容的能力。基于此,我决定采用C++和Python结合的方法:C++负责静态网页的内容下载和解析,Python负责动态网页的内容提取和数据存储。
在项目实施过程中,我遇到了多个挑战。
处理动态网页内容的挑战,动态网页内容需要通过JavaScript加载,传统的静态爬虫无法获取这些数据。为了解决这一问题,我引入了selenium库,通过模拟浏览器操作,加载动态网页,并使用BeautifulSoup解析加载完成的网页内容。
数据量大且复杂的挑战,社交网络上的数据量庞大且格式多样,这对系统的性能和数据处理能力提出了很高的要求。为此,我采用了高效的libcurl库进行网页下载,并利用pandas进行数据清洗和处理,确保系统能够高效处理大规模数据。
反爬机制的挑战,很多社交网络平台都有反爬机制,限制频繁的爬虫行为。我通过设置合理的请求头和cookie,模拟真实用户的浏览行为,同时加入了随机延时机制,避免触发反爬机制。
最后我深刻体会到了合适工具选择的重要性,在本项目中,libcurl、Gumbo、selenium、BeautifulSoup、pandas和Neo4j的组合提供了强大的功能支持,使得系统能够高效地完成数据爬取和处理任务,当然其中包括时下载或者是选择相应的库文件都不是一件简单的事情,需要不断去尝试学习。同时我也感受到心态与坚持的魅力,中途有很多次想要放弃,项目其中也因为各种各样的问题停滞不前,,我想这种发现问题,解决问题的能力将伴随我未来的各种项目,希望我能攻克一个又一个问题。