Open-source project
yorickpeterse/oga avatar
yorickpeterse/oga

Oga: an XML and HTML parser for Ruby with no system library to install

Oga is an XML/HTML parser written in Ruby.

1,169 stars45 forksRubyMPL-2.0

At a glance

What is it?
Oga is a Ruby parser for XML and HTML that ships its own small native extension instead of binding a system library, so it installs cleanly on any platform, and it offers DOM, pull and SAX parsing with XPath 1.0 and CSS3 selectors over the result. It suits a project that wants to parse markup without a libxml dependency, and it is a pre-1.0 library whose author says openly that his time on it is limited.
Who is it for?
Adopt Oga if you are parsing XML or HTML in Ruby and the deployment target makes a system library awkward, because Oga needs no libxml and ships a small extension it builds itself. Do not adopt it if you need HTML5 parsing as complete as the browser engine's, since the parser is its own rather than a browser engine, and remember that before version 1.0 the API can change.
Can I use it commercially?
Yes, with conditions. MPL-2.0 is a weak copyleft licence: you can use it inside commercial and closed-source software, but if you distribute changes to its own files, you must publish those changes under the same licence.
Is it still maintained?
No. The owners have archived the repository on GitHub, so it is read-only and no longer receives changes.
What is it written in?
Mainly Ruby, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 7, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • yorickpeterse/oga1,169 ★ · +0
Star history
Repository2026-10-212026-11-07
yorickpeterse/oga1.2k1.2k

0 Stars in the last 17 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

What Oga is, and the pun in the name

Oga is an XML and HTML parser written in Ruby, and it provides an easy to use API for parsing, modifying and querying documents using XPath expressions.

The two design decisions in the readme's opening paragraph are the ones to understand. Oga does not require system libraries such as libxml, which makes it easier and faster to install on various platforms. And to achieve better performance it uses a small native extension, written in C for MRI and Rubinius and in Java for JRuby.

That combination is unusual and it is the point. The obvious alternative in Ruby is to bind an existing C library, which gives you a mature parser and a dependency on the platform. Oga instead writes the parser itself and only reaches for native code in the small hot spots, so the install is a gem with an extension and nothing else.

The name explains the rest. From Wikipedia, an oga is a large two-person saw used for ripping large boards in the days before power saws, with one person standing on a raised platform and the other underneath. The name is a pun on Nokogiri, and the pun is the two person saw, which is the image of a parser where one part is the platform and the other is the blade.

The API is also described as safe to use for parsing and querying documents in a multi-threaded environment, without having to worry about your applications blowing up, and the thread safety section comes back to that in more detail later.

The licence is MPL-2.0, and the last push to the repository was on 2026-06-10.

Three ways to parse, and when each one fits

The feature list names three parsing modes, and the examples show each of them.

The DOM parser is the default, and it is the simplest. A string goes in and a document comes out.

Oga.parse_xml('<people><person>Alice</person></people>')

There is a strict mode, and it is worth knowing about because it changes the error behaviour. Parsing with the strict option disables automatic tag insertion, so a well formed document parses and a truncated one raises an error rather than being silently repaired.

Oga.parse_xml('<people>foo</people>', :strict => true) # works fine Oga.parse_xml('<people>foo', :strict => true) # throws an error

The pull parser is the streaming form. You get a parser object and a block, and the parser calls you for each node, with a text event that gives you the character data.

parser = Oga::XML::PullParser.new(handle)

parser.parse do |node| parser.on(:text) do puts node.text end end

The SAX parser is the event driven form, and the example shows the shape you would expect, a handler object with a method per event that collects what it needs.

Oga.sax_parse_xml(handler, '<foo><bar></bar></foo>')

And there is a fourth option that is the most interesting one, which is parsing straight from an enumerator. The example builds an enumerator that fetches a large XML document over HTTP in chunks and yields each one, and then hands that to the parser, so a document that is too large to hold in memory can be parsed as it arrives.

enum = Enumerator.new do |yielder| HTTPClient.get('http://some-website.com/some-big-file.xml') do |chunk| yielder << chunk end end

document = Oga.parse_xml(enum)

That is the feature that justifies the low memory footprint claim, because a document is not required to exist all at once.

Querying and modifying

The query API is the part people actually use, and it comes in two flavours that return the same kind of object.

The xpath method returns an enumerable of nodes, and the at_xpath method returns a single node from a set, which the documentation explains is the same as taking the first element of the xpath result.

document.xpath('people/person').each do |person| puts person.get('id') # => "1"

puts person.at_xpath('name').text # => "Alice" end

The css method does the same job with CSS3 selectors, with at_css as the single node version, and the examples are identical in shape.

document.css('people person').each do |person| puts person.get('id') # => "1"

puts person.at_css('name').text # => "Alice" end

Modification is direct, and the document serialises back out to XML.

document = Oga.parse_xml('<people><person>Alice</person></people>') name = document.at_xpath('people/person[1]/text()')

name.text = 'Bob'

document.to_xml # => "<people><person>Bob</person></people>"

So the document is a tree you can walk, query and edit, and then write back, which is the model most people picture when they say they want to parse HTML.

The supported standard is XPath 1.0, and that is worth noting, because XPath 2.0 and 3.0 exist and Oga does not implement them.

Namespaces, and the case that trips everyone up

Oga fully supports parsing and registering XML namespaces as well as querying them using XPath, and the documentation spends a paragraph on why that is not the same as namespaces being easy.

The example is a document with a default namespace declared on the root element.

<root xmlns="http://example.com"> <bar>bar</bar> </root>

The point is that a query for the bar element using the plain path root and bar returns an empty node set, because the root element defines an alternative default namespace. The documented workaround is to use a local name test, with a path that selects on the local name rather than on the qualified name.

*[local-name() = "root"]/*[local-name() = "bar"]

And the documentation offers an alternative for anyone who does not care where the bar element is.

This is standard XML behaviour rather than a defect, and every XPath implementation has it. What is useful here is that the readme calls it out, because it is the first thing that surprises people when they move from CSS selectors, where namespace handling is looser, to XPath.

The namespace objects are first class in the API as well, since the example shows the namespace of a selected element being returned as a named value with a name and a URI.

Thread safety, and the caching caveat

The thread safety section is short and has two halves, and the second half is the one that catches people.

The first half is the good news. Oga does not use an unsynchronized global mutable state, and as a result you can parse or create documents concurrently without any problems.

The second half is the constraint. Modifying documents concurrently can lead to bugs, as those operations are not synchronized. And some querying operations will cache data in instance variables, without synchronization, with the example given being the namespace method on an element, which caches the element's namespace after the first call.

So the accurate summary is that parsing and creating documents are safe in parallel, and that a document shared between threads is not something to modify from more than one of them. The general recommendation in the readme is not to use the same document in multiple threads at the same time.

That is a narrower promise than a fully thread safe library, and it is a more honest one than a library that claims safety and delivers it only for the operations that happen to be safe. A document per thread, with the results merged afterwards, is the pattern this design supports.

Installation, the runtime matrix, and the versioning policy

Two practical sections close the readme, and both of them are things to check before you adopt the library.

The requirements table lists the Ruby implementations and their versions. MRI requires 2.3.0 or later and recommends 2.6.0 or later. JRuby requires 1.7 and recommends 1.7.12. And Rubinius, Maglev, Topaz and mruby are all listed as not supported, with a note explaining why for three of them, that Maglev and Topaz lack a C API, as far as the author knows, and lack active development, and that mruby is a very different implementation altogether.

Building is straightforward on MRI and Rubinius, where you need a working compiler such as gcc or clang for the C extension. JRuby needs no compiler, because the native extension is compiled during the gem building process and bundled inside the gem.

The versioning policy is unusually explicit for a pre-1.0 library. Oga uses the format MAJOR.MINOR, an increase of the major version indicates backwards incompatible changes were introduced, and the minor version is only increased when changes are backwards compatible, whether those changes are bug fixes or new features. Up until version 1.0 the code should be considered unstable, meaning it can change and break at any given moment. And APIs explicitly tagged as private, whether with Ruby's private keyword or with the documentation generator's api private tag, are not covered by these rules.

That last clause is the one that matters in practice, because it tells you that the public surface is the contract and everything else is fair game, which is exactly the arrangement a library needs when it is pre-1.0.

The repository layout matches the design, with the lib directory for the Ruby code, an ext directory for the native extension, the spec directory for the tests, a benchmark directory, a profile directory, a checksum directory, a task directory and the gemspec and Rakefile at the root.

Editorial conclusion

Adopt Oga if you are parsing XML or HTML in Ruby and the deployment target makes a system library awkward, because Oga needs no libxml and ships a small extension it builds itself. Do not adopt it if you need HTML5 parsing as complete as the browser engine's, since the parser is its own rather than a browser engine, and remember that before version 1.0 the API can change. Before you depend on it, read the thread safety section, because parsing documents in several threads is safe while sharing one document across threads is not.

Frequently asked questions

What is Oga?

It is an XML and HTML parser written in Ruby that provides an API for parsing, modifying and querying documents using XPath expressions. It does not require system libraries such as libxml, and it uses a small native extension, written in C for MRI and Rubinius and in Java for JRuby, to get better performance.

Which parsing modes does Oga support?

DOM parsing, stream or pull parsing with the pull parser class, and SAX parsing through a handler object. It can also parse from an enumerator, as the readme shows with a chunked HTTP download, so a large document does not have to exist in memory all at once.

Is Oga thread safe?

Partly. Oga does not use unsynchronized global mutable state, so parsing or creating documents concurrently is safe. Modifying documents concurrently is not, and some querying operations cache data in instance variables without synchronization, so the readme recommends not using the same document in multiple threads at the same time.

What is Oga's versioning policy?

The version format is major and minor, where a major increase means backwards incompatible changes were introduced and the minor version only increases for backwards compatible changes, whether bug fixes or new features. Until version 1.0 the code should be considered unstable, and APIs explicitly tagged as private are not covered by the rules.

Official sources

  1. Issues
  2. License: MPL-2.0
  3. README
  4. Releases
  5. yorickpeterse/oga on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/yorickpeterse-oga.svg)](https://hysenlabs.com/projects/yorickpeterse-oga)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/yorickpeterse-oga.svg)](https://hysenlabs.com/projects/yorickpeterse-oga)

Was this useful?