Skip to content
← Back to Examples

Streaming Parser with Captures

RustRubyconceptualadvanced

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 ...VALID

Apache access log (millions of lines)

email,name,age user1@example.com,Alice,30 ...VALID

CSV export (millions of rows)

Output Format

Captures available after streaming parse completes.

Structure

capture_state

CaptureState with all extracted captures

bytes_processed

Total bytes processed

chunks_processed

Number 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

Rust
//! 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(())
}
Ruby
# 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] }"