Graph Operations¶
Час читання: 8 хвилин
Рівень: Середній
Математичні операції над графами та аналіз структури.
Огляд¶
GraphCrawler підтримує повний набір операцій над графами:
| Операція | Оператор | Метод |
|---|---|---|
| Об'єднання | +, \| | union() |
| Різниця | - | difference() |
| Перетин | & | intersection() |
| Симетрична різниця | ^ | symmetric_difference() |
| Підграф | <, <= | is_subgraph() |
| Надграф | >, >= | is_supergraph() |
Об'єднання графів (Union)¶
Оператор +¶
import graph_crawler as gc
graph1 = gc.crawl("https://site1.com", max_depth=2)
graph2 = gc.crawl("https://site2.com", max_depth=2)
# Об'єднання
merged = graph1 + graph2
print(f"Graph1: {len(graph1.nodes)} nodes")
print(f"Graph2: {len(graph2.nodes)} nodes")
print(f"Merged: {len(merged.nodes)} nodes")
Оператор |¶
Use Case: Моніторинг змін¶
# Краулінг різних розділів сайту
products = gc.crawl("https://shop.com/products", max_depth=3)
blog = gc.crawl("https://shop.com/blog", max_depth=3)
docs = gc.crawl("https://shop.com/docs", max_depth=3)
# Повна карта сайту
full_site = products + blog + docs
print(f"Total site structure: {len(full_site.nodes)} pages")
Різниця графів (Difference)¶
Оператор -¶
# Знайти нові сторінки
old_graph = gc.crawl("https://example.com") # Вчора
# ... час проходить ...
new_graph = gc.crawl("https://example.com") # Сьогодні
# Нові сторінки (є в new, немає в old)
new_pages = new_graph - old_graph
print(f"New pages: {len(new_pages.nodes)}")
for node in new_pages:
print(f" + {node.url}")
# Видалені сторінки (є в old, немає в new)
removed_pages = old_graph - new_graph
print(f"Removed pages: {len(removed_pages.nodes)}")
for node in removed_pages:
print(f" - {node.url}")
Use Case: Аудит контенту¶
import graph_crawler as gc
import json
from datetime import datetime
def audit_site(url, history_file='audit_history.json'):
"""Аудит змін на сайті."""
current = gc.crawl(url, max_depth=5)
# Завантажити попередній стан
try:
with open(history_file) as f:
history = json.load(f)
previous_urls = set(history['urls'])
except FileNotFoundError:
previous_urls = set()
current_urls = set(n.url for n in current)
report = {
'date': datetime.now().isoformat(),
'total_pages': len(current_urls),
'new_pages': list(current_urls - previous_urls),
'removed_pages': list(previous_urls - current_urls),
}
# Зберегти поточний стан
with open(history_file, 'w') as f:
json.dump({'urls': list(current_urls)}, f)
return report
report = audit_site("https://example.com")
print(f"New: {len(report['new_pages'])}")
print(f"Removed: {len(report['removed_pages'])}")
Перетин графів (Intersection)¶
Оператор &¶
Use Case: Аналіз конкурентів¶
import asyncio
import graph_crawler as gc
async def analyze_competitors(urls):
async with gc.AsyncCrawler(max_depth=2) as crawler:
graphs = await asyncio.gather(
*[crawler.crawl(url) for url in urls]
)
# Знайти спільні патерни URL
url_patterns = []
for g in graphs:
patterns = set()
for node in g:
# Витягти патерн (без домену)
path = node.url.split('/')[-1] if '/' in node.url else ''
patterns.add(path)
url_patterns.append(patterns)
# Спільні патерни у всіх конкурентів
common_patterns = url_patterns[0]
for p in url_patterns[1:]:
common_patterns &= p
return common_patterns
competitors = [
"https://shop1.com",
"https://shop2.com",
"https://shop3.com",
]
common = asyncio.run(analyze_competitors(competitors))
print(f"Common URL patterns: {common}")
Симетрична різниця (XOR)¶
Оператор ^¶
# Сторінки, які є тільки в одному з графів
unique = graph1 ^ graph2
print(f"Unique to either graph: {len(unique.nodes)}")
Порівняння графів¶
Підграф <, <=¶
# Перевірка чи graph1 є підграфом graph2
if graph1 < graph2:
print("graph1 is a strict subgraph of graph2")
if graph1 <= graph2:
print("graph1 is a subgraph of graph2 (or equal)")
Надграф >, >=¶
if graph1 > graph2:
print("graph1 is a strict supergraph of graph2")
if graph1 >= graph2:
print("graph1 is a supergraph of graph2 (or equal)")
Рівність ==¶
Use Case: Перевірка повноти краулінгу¶
# Краулінг sitemap
sitemap_graph = gc.crawl_sitemap("https://example.com")
# Краулінг через посилання
link_graph = gc.crawl("https://example.com", max_depth=10)
# Перевірка чи всі URL з sitemap знайдені
if sitemap_graph <= link_graph:
print("All sitemap URLs found via links!")
else:
missing = sitemap_graph - link_graph
print(f"Missing {len(missing.nodes)} URLs from sitemap:")
for node in missing:
print(f" - {node.url}")
Статистика графа¶
get_stats()¶
stats = graph.get_stats()
print(stats)
# {
# 'total_nodes': 150,
# 'scanned_nodes': 145,
# 'pending_nodes': 5,
# 'failed_nodes': 0,
# 'total_edges': 487,
# 'unique_domains': 1,
# 'avg_depth': 2.3,
# 'max_depth': 5,
# }
get_popular_nodes()¶
# Топ сторінок за вхідними посиланнями
popular = graph.get_popular_nodes(top_n=10, by='in_degree')
for node in popular:
print(f"{node.url}: {node.metadata.get('in_degree', 0)} incoming links")
get_edge_statistics()¶
edge_stats = graph.get_edge_statistics()
print(edge_stats)
# {
# 'total_edges': 487,
# 'internal_edges': 450,
# 'external_edges': 37,
# 'avg_out_degree': 3.2,
# 'avg_in_degree': 3.2,
# 'max_out_degree': 15,
# 'max_in_degree': 25,
# }
find_cycles()¶
# Знайти цикли в графі
cycles = graph.find_cycles(max_cycles=10)
print(f"Found {len(cycles)} cycles:")
for cycle in cycles:
print(f" {' -> '.join(cycle[:5])}...")
Аналіз структури¶
Orphan Pages (без вхідних посилань)¶
from collections import defaultdict
def find_orphans(graph):
"""Знайти сторінки без вхідних посилань."""
in_degree = defaultdict(int)
for edge in graph.edges:
in_degree[edge.target_node_id] += 1
orphans = []
for node in graph:
if node.depth > 0 and in_degree[node.node_id] == 0:
orphans.append(node)
return orphans
orphans = find_orphans(graph)
print(f"Orphan pages: {len(orphans)}")
Dead Ends (без вихідних посилань)¶
def find_dead_ends(graph):
"""Знайти сторінки без вихідних посилань."""
out_degree = defaultdict(int)
for edge in graph.edges:
out_degree[edge.source_node_id] += 1
dead_ends = []
for node in graph:
if node.scanned and out_degree[node.node_id] == 0:
dead_ends.append(node)
return dead_ends
dead_ends = find_dead_ends(graph)
print(f"Dead ends: {len(dead_ends)}")
Глибина сайту¶
from collections import Counter
def analyze_depth(graph):
"""Аналіз розподілу сторінок по глибині."""
distribution = Counter(node.depth for node in graph)
print("Depth distribution:")
for depth in sorted(distribution.keys()):
count = distribution[depth]
bar = '█' * (count // 5)
print(f" Level {depth}: {count:4} {bar}")
return distribution
analyze_depth(graph)
Експорт для візуалізації¶
Graphviz (DOT)¶
graph.export_edges("site.dot", format="dot")
# Візуалізація
import os
os.system("dot -Tpng site.dot -o site.png")
os.system("dot -Tsvg site.dot -o site.svg")
NetworkX (Python)¶
import networkx as nx
import matplotlib.pyplot as plt
def to_networkx(graph):
"""Конвертація в NetworkX граф."""
G = nx.DiGraph()
for node in graph:
G.add_node(node.node_id, url=node.url, title=node.get_title())
for edge in graph.edges:
G.add_edge(edge.source_node_id, edge.target_node_id)
return G
G = to_networkx(graph)
# Візуалізація
plt.figure(figsize=(20, 20))
nx.draw(G, with_labels=False, node_size=50, alpha=0.7)
plt.savefig("graph.png", dpi=150)