Streaming Parser with Captures
Extract named values from large files without loading them into memory. Combines streaming parser efficiency with capture extraction.
Key Concepts
- streaming with captures
- bounded memory
- chunk-based extraction
- capture persistence
- incremental processing
Why Parse This Format?
Combine the memory efficiency of streaming with the convenience of named captures. Process GB-scale files while extracting specific fields.
Use Cases
- Log file analysis (extract IPs, status codes from 10GB+ logs)
- CSV export processing (extract emails from 500MB+ files)
- Real-time feed parsing (stock tickers, event streams)
- Database dump processing (extract specific fields)
Input Format
Large files processed in fixed-size chunks.
Any format with captures: CSV, logs, JSON lines, etc.Examples
192.168.1.1 - - [10/Oct/2000:13:55:36] "GET /path" 200 2326
...VALIDApache access log (millions of lines)
email,name,age
user1@example.com,Alice,30
...VALIDCSV export (millions of rows)
Output Format
Captures available after streaming parse completes.
Structure
capture_stateCaptureState with all extracted captures
bytes_processedTotal bytes processed
chunks_processedNumber of chunks used
How the Code Works
This section explains how the parsing rules map to the format semantics.
Basic Setup
use parsanol::portable::streaming::{StreamingParser, ChunkConfig};
let grammar = GrammarBuilder::new()
.rule("email", capture("email", re(r"[^@\s]+@[^@\s]+")))
.build();
let config = ChunkConfig {
chunk_size: 64 * 1024, // 64KB chunks
window_size: 2,
};
let mut parser = StreamingParser::new(&grammar, config);
let mut arena = AstArena::for_input(64 * 1024);
let result = parser.parse_from_reader(&mut file, &mut arena)?;Create a grammar with captures, then use the streaming parser:
Accessing Captures
if let Some(captures) = &result.capture_state {
for name in captures.names() {
if let Some(value) = captures.get(&name) {
let text = value.get_text(input);
println!("{} = {:?}", name, text);
}
}
}After parsing, access captures from the result:
Log File Analysis
let grammar = GrammarBuilder::new()
.rule("log_line", sequence([
capture("ip", re(r"\d+\.\d+\.\d+\.\d+")),
str(" - - ["),
capture("timestamp", re(r"[^\]]+")),
str("] \""),
capture("method", re(r"[A-Z]+")),
str(" "),
capture("path", re(r"[^\s]+")),
re(r" [^\"]+\" "),
capture("status", re(r"\d+")),
]))
.build();Extract fields from Apache-style logs: Process 10GB log files with ~2MB memory.
CSV Processing
let grammar = GrammarBuilder::new()
.rule("row", sequence([
re(r"[^,]*"), // id
str(","),
capture("name", re(r"[^,]*")),
str(","),
capture("email", re(r"[^,@]+@[^,]+")),
str("\n"),
]))
.build();Extract specific columns:
Chunk Configuration
let config = ChunkConfig {
chunk_size: 1024 * 1024, // 1MB chunks
window_size: 2, // 2-chunk sliding window
};- `chunk_size`: Bytes per chunk (larger = fewer syscalls) - `window_size`: Chunks kept in memory (larger = more backtracking)
Memory Management
// Option 1: Process incrementally
if let Some(captures) = &result.capture_state {
if captures.names().len() > 100_000 {
process_captures(captures);
parser.reset(); // Clear captures
}
}
// Option 2: Use scopes to limit capture accumulation
let grammar = GrammarBuilder::new()
.rule("section", sequence([
capture("section_name", re(r"[a-z]+")),
scope(repeat("item", 0, None)), // Items don't accumulate
]))
.build();For very large captures:
StreamingResult Fields
pub struct StreamingResult {
pub ast: AstNode, // Parse tree
pub bytes_processed: usize, // Total bytes read
pub chunks_processed: usize, // Number of chunks used
pub peak_memory: usize, // Maximum memory used
pub cache_stats: (u64, u64, f64), // (hits, misses, hit_rate)
pub capture_state: Option<CaptureState>, // Extracted captures
}StreamingResult Fields
pub struct StreamingResult {
pub ast: AstNode, // Parse tree
pub bytes_processed: usize, // Total bytes read
pub chunks_processed: usize, // Number of chunks used
pub peak_memory: usize, // Maximum memory used
pub cache_stats: (u64, u64, f64), // (hits, misses, hit_rate)
pub capture_state: Option<CaptureState>, // Extracted captures
}Source Code
//! Streaming Parser with Captures Example
//!
//! Demonstrates the streaming parser API for memory-efficient parsing of large inputs.
//! Shows chunk-based parsing configuration and capture extraction.
//!
//! Run with: cargo run --example streaming-captures --no-default-features
#![allow(clippy::print_literal)]
use parsanol::portable::{
arena::AstArena,
parser_dsl::{capture, dynamic, re, GrammarBuilder},
streaming::{ChunkConfig, StreamingParser},
};
use std::io::Cursor;
// =========================================================================
// Example 1: Basic Streaming with Captures
// =========================================================================
fn example_basic_streaming() -> Result<(), Box<dyn std::error::Error>> {
println!("--- Example 1: Basic Streaming with Captures ---\n");
// Simple grammar: match an identifier and capture it
let grammar = GrammarBuilder::new()
.rule("word", capture("word", dynamic(re(r"[a-zA-Z]+"))))
.build();
// Configure streaming with small chunks for demo
let config = ChunkConfig {
chunk_size: 64,
window_size: 2,
};
let input = "hello";
let mut parser = StreamingParser::new(&grammar, config);
let mut arena = AstArena::for_input(64);
let mut cursor = Cursor::new(input.as_bytes());
let result = parser.parse_from_reader(&mut cursor, &mut arena)?;
println!(" Input: {:?}", input);
println!(" Bytes processed: {}", result.bytes_processed);
println!(" Chunks processed: {}", result.chunks_processed);
println!(" Peak memory: {} bytes", result.peak_memory);
if let Some(captures) = &result.capture_state {
println!(" Captures found:");
for name in captures.names() {
if let Some(value) = captures.get(name) {
println!(" {} = {:?}", name, value.get_text(input));
}
}
}
Ok(())
}
// =========================================================================
// Example 2: Chunk Configuration Options
// =========================================================================
fn example_chunk_config() {
println!("\n--- Example 2: Chunk Configuration Options ---\n");
println!(" ChunkConfig {{");
println!(" chunk_size: 65536, // 64 KB - size of each chunk");
println!(" window_size: 2, // Number of chunks to keep in memory");
println!(" }}");
println!();
println!(" Preset configurations:");
println!(" * ChunkConfig::small() - 16 KB chunks, window=2");
println!(" * ChunkConfig::medium() - 64 KB chunks, window=3 (default)");
println!(" * ChunkConfig::large() - 256 KB chunks, window=4");
println!(" * ChunkConfig::huge() - 1 MB chunks, window=5");
}
// =========================================================================
// Example 3: Chunk Size Selection Guide
// =========================================================================
fn example_chunk_selection() {
println!("\n--- Example 3: Chunk Size Selection Guide ---\n");
println!(" | Use Case | Chunk Size | Reason |");
println!(" |----------------------|--------------|--------|");
println!(" | Real-time feeds | 4-16 KB | Low latency |");
println!(" | Log files | 256 KB - 1 MB | Throughput |");
println!(" | Network streams | 8-64 KB | Balance |");
println!(" | Large files | 1-4 MB | Fewer syscalls |");
println!("\n Window size guidelines:");
println!(" | Grammar type | Window | Reason |");
println!(" |----------------------|--------|--------|");
println!(" | Sequential | 1-2 | Minimal backtracking |");
println!(" | Moderate backtracking| 2-3 | Default |");
println!(" | Heavy backtracking | 4-5 | Complex grammars |");
println!("\n Memory formula: memory = chunk_size * window_size + capture_state");
}
// =========================================================================
// Example 4: StreamingResult Structure
// =========================================================================
fn example_result_structure() {
println!("\n--- Example 4: StreamingResult Structure ---\n");
println!(" pub struct StreamingResult {{");
println!(" pub ast: AstNode, // Parse tree");
println!(" pub bytes_processed: usize, // Total bytes read");
println!(" pub chunks_processed: usize, // Number of chunks used");
println!(" pub peak_memory: usize, // Maximum memory used");
println!(" pub cache_stats: (u64, u64, f64), // (hits, misses, hit_rate)");
println!(" pub capture_state: Option<CaptureState>, // Extracted captures");
println!(" }}");
}
// =========================================================================
// Example 5: CaptureState API
// =========================================================================
fn example_capture_api() {
println!("\n--- Example 5: CaptureState API ---\n");
println!(" impl CaptureState {{");
println!(" /// Get all capture names");
println!(" pub fn names(&self) -> impl Iterator<Item = &String>;");
println!();
println!(" /// Get a capture by name");
println!(" pub fn get(&self, name: &str) -> Option<CaptureValue>;");
println!();
println!(" /// Check if capture exists");
println!(" pub fn contains(&self, name: &str) -> bool;");
println!(" }}");
println!();
println!(" impl CaptureValue {{");
println!(" /// Get text from original input (zero-copy)");
println!(" pub fn get_text<'a>(&self, input: &'a str) -> &'a str;");
println!();
println!(" /// Get offset in input");
println!(" pub fn offset(&self) -> usize;");
println!();
println!(" /// Get length of captured text");
println!(" pub fn len(&self) -> usize;");
println!(" }}");
}
// =========================================================================
// Main
// =========================================================================
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Streaming Parser with Captures Example");
println!("======================================\n");
example_basic_streaming()?;
example_chunk_config();
example_chunk_selection();
example_result_structure();
example_capture_api();
// =========================================================================
// Summary
// =========================================================================
println!("\n--- Benefits of Streaming with Captures ---");
println!("* Process files larger than available RAM");
println!("* Captures persist across streaming parse operations");
println!("* Memory bounded by chunk_size * window_size");
println!("* Single pass through data");
println!("* Extract specific fields without loading entire file");
println!("\n--- Performance Notes ---");
println!("* Memory: O(chunk_size * window_size)");
println!("* Captures: Accumulate during parse, available at end");
println!("* For very large captures: process incrementally with reset()");
println!("\n--- API Summary ---");
println!(" let config = ChunkConfig {{ chunk_size: 65536, window_size: 2 }};");
println!(" let mut parser = StreamingParser::new(&grammar, config);");
println!(" let result = parser.parse_from_reader(&mut reader, &mut arena)?;");
println!(" if let Some(captures) = result.capture_state {{");
println!(" captures.get(\"name\").map(|v| v.get_text(input))");
println!(" }}");
Ok(())
}
# frozen_string_literal: true
# Streaming Parser with Captures Example
#
# Demonstrates how to extract named values from large files without loading
# them into memory. Combines streaming parser efficiency with capture extraction.
$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib"
require "parsanol/parslet"
require "stringio"
puts "Streaming Parser with Captures Example"
puts "======================================\n"
# ===========================================================================
# Example 1: Basic Streaming with Captures
# ===========================================================================
puts "--- Example 1: Basic Streaming with Captures ---\n"
# Grammar: Extract email addresses
email_parser = match('[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}').capture(:email)
# Configure streaming with small chunks for demo
config = { chunk_size: 64, window_size: 2 }
input = "Contact us at user@example.com or support@test.org for help."
puts " Input: #{input.inspect}"
puts " Chunk size: #{config[:chunk_size]}"
puts " Window size: #{config[:window_size]}"
# NOTE: Requires native extension for full streaming functionality
if defined?(Parsanol::Native) && Parsanol::Native.respond_to?(:is_available) && Parsanol::Native.is_available
require "parsanol/streaming_parser"
streaming_parser = Parsanol::StreamingParser.new(email_parser, **config)
io = StringIO.new(input)
results = streaming_parser.parse_stream(io)
puts " Results: #{results.length} items parsed"
results.each do |result|
puts " #{result.inspect}"
end
else
puts " (Streaming parser requires native extension)"
puts " Falling back to regular parse:"
result = email_parser.parse(input)
puts " Captured email: #{result[:email].inspect}"
end
# ===========================================================================
# Example 2: Log File Analysis
# ===========================================================================
puts "\n--- Example 2: Log File Analysis ---\n"
# Grammar: Parse Apache-style log lines
# Pattern: IP - - [timestamp] "METHOD path ..." status size
ip_parser = match('\d+\.\d+\.\d+\.\d+').capture(:ip)
timestamp_parser = match('[^\]]+').capture(:timestamp)
method_parser = match("[A-Z]+").capture(:method)
path_parser = match('[^\s"]+').capture(:path)
log_parser = ip_parser >> str(" - - [") >> timestamp_parser >> str('] "') >>
method_parser >> str(" ") >> path_parser >> match('[^"]*') >>
str('" ') >> match('\d+').capture(:status) >> str(" ") >> match('\d+').capture(:size)
sample_log = <<~LOG
192.168.1.1 - - [10/Oct/2000:13:55:36 -0700] "GET /index.html HTTP/1.0" 200 2326
10.0.0.1 - - [10/Oct/2000:13:55:37 -0700] "POST /api/users HTTP/1.0" 201 512
172.16.0.1 - - [10/Oct/2000:13:55:38 -0700] "GET /favicon.ico HTTP/1.0" 404 128
LOG
puts " Processing log file..."
puts " Sample input (#{sample_log.lines.count} lines):"
sample_log.lines.first(2).each { |line| puts " #{line.strip}" }
if defined?(Parsanol::Native) && Parsanol::Native.respond_to?(:is_available) && Parsanol::Native.is_available
streaming_parser = Parsanol::StreamingParser.new(log_parser, chunk_size: 128)
io = StringIO.new(sample_log)
results = streaming_parser.parse_stream(io)
puts " Parsed #{results.length} log lines"
else
puts " (Streaming requires native extension)"
puts " Parsing first line with regular parser:"
result = log_parser.parse(sample_log.lines.first)
puts " IP: #{result[:ip].inspect}"
puts " Method: #{result[:method].inspect}"
puts " Status: #{result[:status].inspect}"
end
# ===========================================================================
# Example 3: Memory-Bounded Processing
# ===========================================================================
puts "\n--- Example 3: Memory-Bounded Processing ---\n"
word_parser = match("[a-z]+").capture(:word)
input = "apple banana cherry date elderberry fig grape"
puts " Input: #{input}"
puts " Testing different chunk sizes:"
[16, 32, 64].each do |chunk_size|
if defined?(Parsanol::Native) && Parsanol::Native.respond_to?(:is_available) && Parsanol::Native.is_available
streaming_parser = Parsanol::StreamingParser.new(word_parser,
chunk_size: chunk_size)
io = StringIO.new(input)
results = streaming_parser.parse_stream(io)
puts " Chunk size #{chunk_size}: #{results.length} results"
else
puts " Chunk size #{chunk_size}: (requires native extension)"
end
end
puts "\n Memory usage is bounded by chunk_size * window_size"
# ===========================================================================
# Example 4: Chunk Size Selection Guide
# ===========================================================================
puts "\n--- Example 4: Chunk Size Selection Guide ---\n"
puts " | Use Case | Chunk Size | Reason |"
puts " |----------------------|--------------|--------|"
puts " | Real-time feeds | 4-16 KB | Low latency |"
puts " | Log files | 256 KB - 1 MB | Throughput |"
puts " | Network streams | 8-64 KB | Balance |"
puts " | Large files | 1-4 MB | Fewer syscalls |"
puts "\n Window size guidelines:"
puts " | Grammar type | Window | Reason |"
puts " |----------------------|--------|--------|"
puts " | Sequential | 1-2 | Minimal backtracking |"
puts " | Moderate backtracking| 2-3 | Default |"
puts " | Heavy backtracking | 4-5 | Complex grammars |"
puts "\n Memory formula: memory = chunk_size * window_size + capture_state"
# ===========================================================================
# Example 5: StreamingResult Structure
# ===========================================================================
puts "\n--- Example 5: StreamingResult Structure ---\n"
puts " StreamingParser#parse_stream returns:"
puts " ["
puts " {"
puts " ast: ..., # Parse tree"
puts " bytes_processed: N, # Bytes read"
puts " captures: { ... }, # Extracted captures"
puts " },"
puts " ..."
puts " ]"
# ===========================================================================
# Summary
# ===========================================================================
puts "\n--- Benefits of Streaming with Captures ---"
puts "* Process files larger than available RAM"
puts "* Captures persist across streaming parse operations"
puts "* Memory bounded by chunk_size * window_size"
puts "* Single pass through data"
puts "* Extract specific fields without loading entire file"
puts "\n--- Performance Notes ---"
puts "* Memory: O(chunk_size * window_size)"
puts "* Captures: Accumulate during parse, available at end"
puts "* For very large captures: process incrementally with reset()"
puts "\n--- API Summary ---"
puts " parser = StreamingParser.new(grammar, chunk_size: 65536)"
puts " results = parser.parse_stream(io)"
puts " results.each { |r| r[:capture_name] }"