Oga: an XML and HTML parser for Ruby with no system library to install
Oga is an XML/HTML parser written in Ruby.
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.
Star history
- yorickpeterse/oga1,169 ★ · +0
| Repository | 2026-10-21 | 2026-11-07 |
|---|---|---|
| yorickpeterse/oga | 1.2k | 1.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
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.
[](https://hysenlabs.com/projects/yorickpeterse-oga)Star history chart for your README
[](https://hysenlabs.com/projects/yorickpeterse-oga)Was this useful?