Skip to content

pyroid.warc

warcio 1.8.2's WARC/ARC reader and datatrove 0.10.1's WARC reading loop, with their API and results. Guide: pyroid.warc.

A drop-in for warcio 1.8.2's WARC/ARC reader, and datatrove 0.10.1's WARC loop, in Rust.

import pyroid.warc as pw

with open("CC-MAIN-...warc.gz", "rb") as f:
    for record in pw.ArchiveIterator(f):          # as warcio.ArchiveIterator
        if record.rec_type == "response":
            html = record.content_stream().read()

for record in pw.ArchiveIterator("x.warc.gz"):    # a path: read with the GIL released
    ...

for ri, doc in pw.iter_documents("x.warc.gz"):    # datatrove's WarcReader.read_file loop
    doc["text"], doc["id"], doc["url"], doc["date"]

import pyarrow as pa
table = pa.Table.from_batches(pa.record_batch(b) for b in pw.iter_batches("x.warc.gz"))

The reader is warcio 1.8.2's, ported statement by statement: the same records, headers (StatusAndHeaders), payloads (content_stream(): chunked, gzip, deflate, deflate_alt and, with brotlicffi >= 1.2.1.0 installed, br), offsets (get_record_offset/get_record_length), digest checks (check_digests), exceptions with the same messages (ArchiveLoadFailed, EOFError, zlib.error, brotlicffi.error, ...), the same sys.stderr writes and warcio.recordloader log warnings, and the same read(n) and tell() calls on the file object. Gzip members are inflated with zlib-rs; parsing and decoding run in Rust.

A file object is read with the GIL held, one fh.read(block_size) per block as warcio does. A path (str or os.PathLike, a pyroid extension) is opened and read in Rust with the GIL released. An iterator may be used from several threads (its calls run one at a time), including on free-threaded Python; a file object whose read() calls back into its own iterator gets a RuntimeError.

Records are pyroid's ports of warcio's classes. pyroid.patch("warcio") makes warcio's own ArchiveIterator this one, with warcio's StatusAndHeaders, ArchiveLoadFailed and an ArcWarcRecord subclass: ArchiveIterator takes every class it creates, and its logger, from class attributes (record_cls, headers_cls, digest_checker_cls, archive_load_failed, parser_exception, logger).

Differences from warcio:

  • iter(it) returns the native iterator (warcio returns a generator), and the arguments are read once: changing it.no_record_parse later has no effect, and known_format is read-only. A block_size that is not an int raises TypeError when the iterator is made, not at the first read.
  • check_digests knows md5, sha1, sha224, sha256, sha384 and sha512, under the names hashlib.new takes for them (sha1, SHA-1, sha-256, ...). Other algorithms (sha3_256, blake2b, ...) report "unknown hash algorithm name" where hashlib would hash.
  • content_stream() needs the record's raw_stream to be pyroid's.
  • Brotli payloads: a damaged stream whose command runs past the end of a metablock that is not the last one decodes further than libbrotli (more output, or another error later), where brotlicffi raises brotlicffi.error.

WARCIO_VERSION module-attribute

WARCIO_VERSION: Final = '1.8.2'

BUFF_SIZE module-attribute

BUFF_SIZE: Final[int]

ArchiveLoadFailed

ArchiveLoadFailed(reason: object)

Bases: Exception

The archive could not be read (warcio's warcio.exceptions.ArchiveLoadFailed).

Raised for an unknown format ("Unknown archive format, first line: ..."), a WARC or ARC record that does not start as one, a gzip file that is not one member per record, and, with check_digests="raise", a digest mismatch.

msg instance-attribute

msg: str

StatusAndHeadersParserException

StatusAndHeadersParserException(msg: str, statusline: Any)

Bases: Exception

An HTTP status line that does not start with a known protocol (verify_http=True).

Attributes:

statusline instance-attribute

statusline: Any

StatusAndHeaders

StatusAndHeaders(
    statusline: str,
    headers: Any,
    protocol: str = "",
    total_len: int = 0,
    is_http_request: bool = False,
)

A status line and an ordered list of (name, value) headers.

warcio's StatusAndHeaders: rec_headers (the WARC or ARC headers) and http_headers of a record. Lookups by name ignore case; headers[...], get, in and item assignment act like a case-insensitive dict.

Parameters:

  • statusline (str) –

    The status line after the protocol ("200 OK"; empty for WARC headers).

  • headers (Any) –

    (name, value) pairs (or a mapping); bytes are decoded as latin-1.

  • protocol (str, default: '' ) –

    "WARC/1.0", "HTTP/1.1", "GET", ...

  • total_len (int, default: 0 ) –

    The characters read for the block, line ends included.

  • is_http_request (bool, default: False ) –

    Split the protocol (the verb) off statusline.

ENCODE_HEADER_RX class-attribute

ENCODE_HEADER_RX: Pattern[str]

statusline instance-attribute

statusline: str

headers instance-attribute

headers: list[tuple[str, str]]

protocol instance-attribute

protocol: str

total_len instance-attribute

total_len: int

headers_buff instance-attribute

headers_buff: bytes | None

get_header

get_header(name: str, default_value: Any = None) -> Any

Return the value of the first header named name (any case), or default_value.

add_header

add_header(name: str, value: str) -> None

Append a header.

replace_header

replace_header(name: str, value: str) -> str | None

Set the last header named name (any case) to value, or append it.

Returns:

  • str | None –

    The old value, or None if the header was added.

remove_header

remove_header(name: str) -> bool

Remove the last header named name (any case).

Returns:

  • bool –

    Whether there was one.

get_statuscode

get_statuscode() -> str

Return the status code: the status line up to the first space.

validate_statusline

validate_statusline(valid_statusline: str) -> bool

Check that the status line starts with a positive integer; else replace it.

Returns:

  • bool –

    Whether it did.

add_range

add_range(
    start: int, part_len: int, total_len: int
) -> StatusAndHeaders

Make this a 206 Partial Content response for part_len bytes at start.

compute_headers_buffer

compute_headers_buffer(
    header_filter: Callable[..., Any] | None = None,
) -> None

Set headers_buff to to_ascii_bytes.

to_str

to_str(
    filter_func: Callable[..., Any] | None = None,
) -> str

Return the status line and headers, each line ending in \r\n.

Parameters:

  • filter_func (Callable[..., Any] | None, default: None ) –

    Called with each (name, value); returns the pair to write, or a false value to drop it.

to_bytes

to_bytes(
    filter_func: Callable[..., Any] | None = None,
    encoding: str = "utf-8",
) -> bytes

Return to_str encoded, followed by the blank line.

to_ascii_bytes

to_ascii_bytes(
    filter_func: Callable[..., Any] | None = None,
) -> bytes

Return to_bytes in ASCII, %-encoding non-ASCII values first (RFC 8187).

percent_encode_non_ascii_headers

percent_encode_non_ascii_headers(
    encoding: str = "UTF-8",
) -> None

%-encode the header values that are not ASCII (RFC 8187, RFC 5987).

get

get(name: str, default_value: Any = None) -> Any

Return the value of the first header named name (any case), or default_value.

DigestChecker

DigestChecker(kind: Any = None)

The digest checks of one record (warcio's DigestChecker).

record.digest_checker is kept up to date as the record's stream is read.

Parameters:

  • kind (Any, default: None ) –

    ArchiveIterator's check_digests: "raise" raises ArchiveLoadFailed at the first problem, "log" also writes each problem to sys.stderr.

kind instance-attribute

kind: Any

passed property writable

passed: bool | None

Whether every digest checked so far matched (None: none checked).

problems property

problems: list[str]

The problems found so far, such as "payload digest failed: sha1:...".

problem

problem(value: str, passed: bool = False) -> None

Record a problem (and raise or log it, as kind says).

RawStream

A record's bytes after its headers (record.raw_stream).

warcio's LimitReader over the iterator's reader: it stops at the record's Content-Length. Reading an old record reads the file where the iterator is now, as in warcio.

limit property

limit: int | None

The bytes left in the record (None when it has no Content-Length).

read

read(length: int | None = None) -> bytes

Read up to length bytes (the rest of the record when None).

readline

readline(length: int | None = None) -> bytes

Read up to the next \n (at most length bytes).

tell

tell() -> int

Return the bytes of the record read so far.

close

close() -> None

Close the file object, then the iterator's decompressor (as warcio).

ContentStream

A record's payload, de-chunked and decompressed (record.content_stream()).

read

read(length: int | None = None) -> bytes

Read up to length decoded bytes (all of them when None).

readline

readline(length: int | None = None) -> bytes

Read up to the next \n (at most length bytes).

tell

tell() -> int

Return the decoded bytes produced so far.

close

close() -> None

Close the underlying stream and the file object, then the decompressor.

ArcWarcRecord

ArcWarcRecord(*args: Any, **kwargs: Any)

A WARC or ARC record (warcio's ArcWarcRecord).

Attributes:

format instance-attribute

format: str

rec_type instance-attribute

rec_type: str | None

rec_headers instance-attribute

rec_headers: StatusAndHeaders

raw_stream instance-attribute

raw_stream: RawStream

http_headers instance-attribute

http_headers: StatusAndHeaders | None

content_type instance-attribute

content_type: str | None

length instance-attribute

length: int | None

payload_length instance-attribute

payload_length: int

digest_checker instance-attribute

digest_checker: DigestChecker

content_stream

content_stream() -> RawStream | ContentStream

Return the payload, decoded per the HTTP headers.

Transfer-Encoding: chunked is de-chunked, and a Content-Encoding of gzip, x-gzip, deflate, deflate_alt or (with brotlicffi >= 1.2.1.0) br decompressed. Returns raw_stream itself when there is nothing to decode, else a new decoder over it on each call, as warcio.

ArchiveIterator

ArchiveIterator(
    fileobj: SupportsRead[bytes] | StrPath,
    no_record_parse: bool = False,
    verify_http: bool = False,
    arc2warc: bool = False,
    ensure_http_headers: bool = False,
    block_size: int = 16384,
    check_digests: bool | Literal["log", "raise"] = False,
)

Iterate over the records of a WARC or ARC file (warcio 1.8.2's ArchiveIterator).

The file may be uncompressed or gzip-compressed with one member per record, as warcio reads them.

Parameters:

  • fileobj (SupportsRead[bytes] | StrPath) –

    A binary file object, or a path to open (read with the GIL released).

  • no_record_parse (bool, default: False ) –

    Do not parse the HTTP headers.

  • verify_http (bool, default: False ) –

    Raise StatusAndHeadersParserException for an HTTP status line that does not start with a known protocol (or verb, for requests).

  • arc2warc (bool, default: False ) –

    Present ARC records as WARC records.

  • ensure_http_headers (bool, default: False ) –

    Give records without HTTP headers a 200 OK set.

  • block_size (int, default: 16384 ) –

    The bytes asked for in each fh.read().

  • check_digests (bool | Literal['log', 'raise'], default: False ) –

    Check WARC-Block-Digest and WARC-Payload-Digest: True records the results in record.digest_checker, "log" also writes problems to stderr, "raise" raises ArchiveLoadFailed at the first.

Raises:

  • ArchiveLoadFailed –

    The file is not a WARC or ARC file, a record is malformed, or a gzip member holds several records.

  • EOFError –

    A gzip member or the file ends inside a record's headers.

Subclass to change the classes it creates (what pyroid.patch("warcio") does): the class attributes record_cls (called as warcio's ArcWarcRecord(format, rec_type, rec_headers, raw_stream, http_headers, content_type, length, payload_length=..., digest_checker=...)), headers_cls (as StatusAndHeaders(statusline=..., headers=..., protocol=..., total_len=...)), digest_checker_cls, archive_load_failed, parser_exception and logger (warcio.recordloader's by default).

GZIP_ERR_MSG class-attribute

GZIP_ERR_MSG: str

INC_RECORD class-attribute

INC_RECORD: str

record_cls class-attribute

record_cls: Any

headers_cls class-attribute

headers_cls: Any

digest_checker_cls class-attribute

digest_checker_cls: Any

archive_load_failed class-attribute

archive_load_failed: Any

parser_exception class-attribute

parser_exception: Any

logger class-attribute

logger: Logger

no_record_parse instance-attribute

no_record_parse: bool

ensure_http_headers instance-attribute

ensure_http_headers: bool

mixed_arc_warc instance-attribute

mixed_arc_warc: bool

check_digests instance-attribute

check_digests: Any

the_iter instance-attribute

offset property

offset: int

The offset where the next record starts, once the current one has been read.

err_count property

err_count: int

Records so far not followed by a blank line (each also warned about on stderr).

member_info property

member_info: tuple[int, int] | None

(offset, length) of the current record once it has been read to its end.

known_format property

known_format: Literal['warc', 'arc'] | None

The format of the records read so far (the first record decides).

record property

record: ArcWarcRecord | None

The current record (None before the first and after the end).

fh property

fh: Any

The file object read (None for a path).

close

close() -> None

Drop the current record and flush the decompressor (the file stays open).

Raises:

  • error –

    The last gzip member was corrupt (CPython 3.13 and later).

read_to_end

read_to_end(record: object = None) -> None

Read the rest of the current record and the blank lines after it.

get_record_offset

get_record_offset() -> int

Return the offset of the current record in the file (reads it to its end).

get_record_length

get_record_length() -> int

Return the bytes of the current record in the file (reads it to its end).

Compressed: its gzip member. Uncompressed: up to the blank lines after it.

WARCIterator

WARCIterator(
    fileobj: SupportsRead[bytes] | StrPath,
    no_record_parse: bool = False,
    verify_http: bool = False,
    arc2warc: bool = False,
    ensure_http_headers: bool = False,
    block_size: int = 16384,
    check_digests: bool | Literal["log", "raise"] = False,
)

Bases: ArchiveIterator

ArchiveIterator that reads WARC records only.

ARCIterator

ARCIterator(
    fileobj: SupportsRead[bytes] | StrPath,
    no_record_parse: bool = False,
    verify_http: bool = False,
    arc2warc: bool = False,
    ensure_http_headers: bool = False,
    block_size: int = 16384,
    check_digests: bool | Literal["log", "raise"] = False,
)

Bases: ArchiveIterator

ArchiveIterator that reads ARC records only.

Document

Bases: TypedDict

A document from iter_documents (datatrove's process_record dict).

text instance-attribute

text: str

id instance-attribute

id: str | None

url instance-attribute

url: str

date instance-attribute

date: str

Batch

Documents of a WARC file as an Arrow record batch (no Arrow library needed).

Columns: id (WARC-Record-ID), url, date (WARC-Target-URI and WARC-Date, with datatrove's ARC fallbacks), mime_type (WARC-Identified-Payload-Type), content_type (the HTTP Content-Type), all large_string; html (large_binary: the decoded payload); and text (large_string: datatrove's decoding of it).

pyarrow.record_batch(b), pyarrow.table(b) and polars.DataFrame(b) take it without a copy, through the Arrow PyCapsule interface.

num_rows property

num_rows: int

Rows in the batch.

num_columns property

num_columns: int

Columns in the batch.

column_names property

column_names: list[str]

The column names, in order.

column

column(key: str | int) -> Column[Any]

Return a column by name or position, as a pyroid.Column (no copy).

Raises:

to_pydict

to_pydict() -> dict[str, list[Any]]

Return {name: [values]} (str, bytes for html, None for nulls).

iter_documents

iter_documents(
    source: SupportsRead[bytes] | StrPath,
    *,
    compression: Literal["infer", "gzip"] | None = "infer",
    iterator_cls: type[ArchiveIterator] = ...,
) -> Iterator[tuple[int, Document]]

Read the documents datatrove 0.10.1's WarcReader reads from a WARC file.

This is for ri, record in enumerate(ArchiveIterator(f)): doc = process_record(record); if doc: yield ri, doc, where f is what datatrove opens: response and conversion records whose WARC-Identified-Payload-Type is text/html or application/xhtml+xml (or text/plain for conversion), or missing and then detected by magic.from_buffer; the payload decoded as UTF-8, else as cchardet guesses (records that decode neither way are skipped). The same documents, the same exceptions at the same record (KeyError for a record without a URL or date), and the same messages.

ri is the record's index among all records of the file. cchardet (faust-cchardet) and magic (python-magic) are imported at the first next(), as datatrove imports them at its first record; if either import fails, datatrove's own loop runs on ArchiveIterator over CPython's gzip.GzipFile, so the ImportError comes at the first record. The file is opened at the first next(); up to 64 documents (16 MiB of payload) are read ahead, and messages and errors are delivered at the document they belong to.

Parameters:

  • source (SupportsRead[bytes] | StrPath) –

    A path, or a binary file object.

  • compression (Literal['infer', 'gzip'] | None, default: 'infer' ) –

    For a path, "infer" reads names ending in .gz (any case) as gzip and others as uncompressed, as fsspec does; "gzip" and None say which. A gzip file is read through an emulation of CPython's gzip.GzipFile (the bytes warcio sees, and the EOFError / gzip.BadGzipFile / zlib.error at the same point, as on this CPython version). A file object is read as is unless compression="gzip".

  • iterator_cls (type[ArchiveIterator], default: ... ) –

    Where the archive_load_failed exception and the logger come from (and the iterator when an import fails), such as warcio's patched class.

Raises:

  • ValueError –

    compression is not "infer", "gzip" or None, or the path's extension names another compression (.zst, .bz2, .xz, ...).

iter_batches

iter_batches(
    source: SupportsRead[bytes] | StrPath,
    *,
    batch_size: int = 1000,
    compression: Literal["infer", "gzip"] | None = "infer",
    iterator_cls: type[ArchiveIterator] = ...,
) -> Iterator[Batch]

Read a WARC file's documents as Arrow record batches of up to batch_size rows.

The rows are the records iter_documents looks at: response and conversion records whose WARC-Identified-Payload-Type is one datatrove keeps or is missing. text is what datatrove's process_record decodes, and null where it skips the record (magic.from_buffer finds another type, or the payload decodes neither as UTF-8 nor as cchardet guesses); url and date are null where datatrove raises KeyError. magic and cchardet are imported only when a row needs them. source, compression and iterator_cls are as for iter_documents; the messages of a batch are emitted before it is yielded.

Raises: