<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: GunsNPrices</title>
    <description>The latest articles on DEV Community by GunsNPrices (@gunsnprices).</description>
    <link>https://dev.to/gunsnprices</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4181907%2Ff3827b52-e1b1-4818-a383-87bdb4865cf4.png</url>
      <title>DEV Community: GunsNPrices</title>
      <link>https://dev.to/gunsnprices</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gunsnprices"/>
    <language>en</language>
    <item>
      <title>What 64,000 gun store listings taught us about parsing product titles</title>
      <dc:creator>GunsNPrices</dc:creator>
      <pubDate>Tue, 13 Oct 2026 11:37:54 +0000</pubDate>
      <link>https://dev.to/gunsnprices/what-64000-gun-store-listings-taught-us-about-parsing-product-titles-27dk</link>
      <guid>https://dev.to/gunsnprices/what-64000-gun-store-listings-taught-us-about-parsing-product-titles-27dk</guid>
      <description>&lt;p&gt;If you compare prices across stores, sooner or later you have to answer a boring question: are these two listings the&lt;br&gt;
same product? For guns and ammunition the answer lives almost entirely in the title, and store titles are a mess.&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://gunsnprices.com" rel="noopener noreferrer"&gt;GunsNPrices&lt;/a&gt; we track listings from 40+ US gun and ammo stores. We recently open-sourced the&lt;br&gt;
parser we use to read those titles, as a Python package, a JavaScript package and an MCP server for AI assistants. This&lt;br&gt;
post is about what we learned from running it on 63,755 real listings.&lt;/p&gt;

&lt;h2&gt;
  
  
  What store titles look like
&lt;/h2&gt;

&lt;p&gt;A few real examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;G19 GEN3 COMPACT 9MM 4" 2/15RD Thrasher&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Glock 10 + 1 Round Double Action Only 9mm W/fixed Sights &amp;amp; O&lt;/code&gt;, with the part number &lt;code&gt;PI1957201&lt;/code&gt; in a separate field&lt;/li&gt;
&lt;li&gt;&lt;code&gt;202AE WHITE O/U 12/28 BL/WD 3"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Federal 308 150gr Fusion MSR Ammunition 20rds - F308MSR1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Kimber CDS9 9mm Luger 3.18in 13rd/15rd Black&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Barnaul 7.62x39 123gr FMJ 20/25&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of them is readable to a person who knows guns, and every one hides a trap for a regex.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;O/U 12/28 BL/WD 3"&lt;/code&gt; is a 12 gauge over/under shotgun with a 28" barrel and a 3" chamber. &lt;code&gt;BL/WD&lt;/code&gt; means blued metal and&lt;br&gt;
walnut wood. The &lt;code&gt;3"&lt;/code&gt; is not the barrel length. &lt;code&gt;20/25&lt;/code&gt; in an ammo title means 20 rounds per box and 25 boxes per case,&lt;br&gt;
not a caliber. &lt;code&gt;13rd/15rd&lt;/code&gt; means the pistol ships with two magazines, and you probably want 15. &lt;code&gt;10/22&lt;/code&gt; is a Ruger rifle,&lt;br&gt;
not 10 gauge with a 22" barrel. And &lt;code&gt;G19 GEN3 COMPACT 9MM 4" 2/15RD&lt;/code&gt;, given the brand from the store's feed, should come&lt;br&gt;
out as a Glock 19 Gen 3 in 9mm with a 4" barrel and 15-round magazines (two of them).&lt;/p&gt;

&lt;h2&gt;
  
  
  The approach: dictionaries, not one big model
&lt;/h2&gt;

&lt;p&gt;We tried the obvious things first. A general NER model doesn't know that &lt;code&gt;G19&lt;/code&gt; is a Glock and &lt;code&gt;P365&lt;/code&gt; is a Sig. An LLM&lt;br&gt;
knows, but it costs money on every listing, it is slow, and it gives slightly different answers for the same title.&lt;/p&gt;

&lt;p&gt;What works is boring and explicit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One dictionary per brand.&lt;/strong&gt; Each brand (50 so far: Glock, Sig Sauer, Smith &amp;amp; Wesson, Ruger, Springfield, Beretta,
Taurus...) has an ordered list of model patterns, generations, configurations and features. The first matching
pattern wins, so &lt;code&gt;P365 XL&lt;/code&gt; is listed before &lt;code&gt;P365&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A numeric mode for brands like Glock&lt;/strong&gt;, where the model is a number with suffixes (&lt;code&gt;19&lt;/code&gt;, &lt;code&gt;19X&lt;/code&gt;, &lt;code&gt;43X&lt;/code&gt;, &lt;code&gt;20SF&lt;/code&gt;) and
the part number encodes the model and generation. &lt;code&gt;PI1957201&lt;/code&gt; decodes to Glock 19 Gen 3, which is the only way to
identify the &lt;code&gt;Glock 10 + 1 Round Double Action Only 9mm&lt;/code&gt; listing above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A ranking step when several dictionaries match.&lt;/strong&gt; The listing's own brand wins, then a brand at the start of the
title, then the brand mentioned first. This matters for titles like &lt;code&gt;Kel tec Sub 2000 Defender 9mm 15rd Fits Glock Magazines&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An accessory filter.&lt;/strong&gt; &lt;code&gt;Glock 19 holster&lt;/code&gt; and &lt;code&gt;Magpul PMAG for Glock 19&lt;/code&gt; must not become pistols, but
&lt;code&gt;Glock 19 Gen 5 9mm 15rd w/ 3 magazines&lt;/code&gt; must stay one. The filter only fires when the title has no gun words and the
accessory isn't "included". On our firearm listings it rejects 4 real guns out of 32,316.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These are the same dictionaries the site uses to group listings, so the library returns the model names you see on&lt;br&gt;
the site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calibers: spellings come from the stores
&lt;/h2&gt;

&lt;p&gt;Caliber names are the other half of the problem. &lt;code&gt;.300 AAC Blackout&lt;/code&gt;, &lt;code&gt;300 BLK&lt;/code&gt;, &lt;code&gt;300 Blackout&lt;/code&gt;, &lt;code&gt;300BO&lt;/code&gt; and &lt;code&gt;300 AAC&lt;/code&gt;&lt;br&gt;
are one cartridge. &lt;code&gt;6.5 CM&lt;/code&gt;, &lt;code&gt;6.5 Creed&lt;/code&gt;, &lt;code&gt;6.5CR&lt;/code&gt; and &lt;code&gt;6.5 Creedmoor&lt;/code&gt; are another.&lt;/p&gt;

&lt;p&gt;Instead of writing synonyms by hand, we generated them from what stores actually write: every caliber spelling seen in&lt;br&gt;
store caliber fields, expanded with common abbreviations (&lt;code&gt;win&lt;/code&gt;/&lt;code&gt;winchester&lt;/code&gt;, &lt;code&gt;mag&lt;/code&gt;/&lt;code&gt;magnum&lt;/code&gt;, &lt;code&gt;spl&lt;/code&gt;/&lt;code&gt;special&lt;/code&gt;). Two rules&lt;br&gt;
kept precision high:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ambiguous spellings are resolved by popularity.&lt;/strong&gt; If one spelling maps to two calibers, we keep it only when one has
10x more listings than the other. Otherwise the spelling is dropped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bare numbers are banned by default.&lt;/strong&gt; &lt;code&gt;20&lt;/code&gt;, &lt;code&gt;307&lt;/code&gt; or &lt;code&gt;6.5mm&lt;/code&gt; in a title is usually something else. A short
allowlist (&lt;code&gt;9mm&lt;/code&gt;, &lt;code&gt;380&lt;/code&gt;, &lt;code&gt;.308&lt;/code&gt;, &lt;code&gt;243&lt;/code&gt;, &lt;code&gt;5.56&lt;/code&gt;...) made it in only after each number proved right in at least 97% of the
listings where it appeared.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Context rules handle the rest: &lt;code&gt;12g&lt;/code&gt; next to &lt;code&gt;CO2&lt;/code&gt; is a 12-gram CO2 cartridge, not 12 gauge.&lt;/p&gt;

&lt;h2&gt;
  
  
  How accurate is it?
&lt;/h2&gt;

&lt;p&gt;We measured against the attributes stored on GunsNPrices for 50,202 firearm and 13,555 ammo listings:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Found in the title&lt;/th&gt;
&lt;th&gt;Right when found&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model (32,317 listings with a known model)&lt;/td&gt;
&lt;td&gt;99.97%&lt;/td&gt;
&lt;td&gt;99.8% same name as the site&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Caliber, firearms&lt;/td&gt;
&lt;td&gt;81%&lt;/td&gt;
&lt;td&gt;97.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Caliber, ammo&lt;/td&gt;
&lt;td&gt;91%&lt;/td&gt;
&lt;td&gt;94.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Magazine capacity&lt;/td&gt;
&lt;td&gt;46%&lt;/td&gt;
&lt;td&gt;95.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Barrel length&lt;/td&gt;
&lt;td&gt;58%&lt;/td&gt;
&lt;td&gt;94.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rounds per box&lt;/td&gt;
&lt;td&gt;80%&lt;/td&gt;
&lt;td&gt;96.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The "found" column is limited by the titles themselves: plenty of stores never put the caliber or barrel length in the&lt;br&gt;
title.&lt;/p&gt;

&lt;p&gt;The most interesting part was the "wrong" column. When we checked mismatches by hand, most of them were errors in the&lt;br&gt;
store data, not in the parser. Accuracy International rifles titled &lt;code&gt;AXSA 6 Creedmoor&lt;/code&gt; were stored as 6.5 Creedmoor. A &lt;code&gt;1000 Rounds&lt;/code&gt; case was&lt;br&gt;
stored as a box of 20. A &lt;code&gt;16ga&lt;/code&gt; shotgun was labeled 12 gauge. The parser, which only reads the title, was right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping Python and JavaScript identical
&lt;/h2&gt;

&lt;p&gt;We ship the parser twice: &lt;a href="https://pypi.org/project/gunsnprices/" rel="noopener noreferrer"&gt;Python on PyPI&lt;/a&gt; and&lt;br&gt;
&lt;a href="https://www.npmjs.com/package/gunsnprices" rel="noopener noreferrer"&gt;JavaScript on npm&lt;/a&gt;. Both run a shared file of 1,150 recorded titles and&lt;br&gt;
answers in their test suites, and on the full 63,755 titles they return exactly the same result.&lt;/p&gt;

&lt;p&gt;Getting there took three small fixes worth knowing about if you ever port text-processing code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript objects reorder integer-like keys.&lt;/strong&gt; A dictionary of configurations like &lt;code&gt;{"MK4": ..., "100": ...}&lt;/code&gt; comes
back with &lt;code&gt;"100"&lt;/code&gt; first. When the order of rules matters, store them as arrays of pairs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;html.unescape&lt;/code&gt; is not just &lt;code&gt;&amp;amp;amp;&lt;/code&gt; and &lt;code&gt;&amp;amp;quot;&lt;/code&gt;.&lt;/strong&gt; Python accepts legacy entities without a semicolon and maps
&lt;code&gt;&amp;amp;#148;&lt;/code&gt; to a curly quote. We exported Python's entity table to get identical behavior.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rounding ties.&lt;/strong&gt; Python's &lt;code&gt;round(0.53125, 4)&lt;/code&gt; gives &lt;code&gt;0.5312&lt;/code&gt; (ties go to even); &lt;code&gt;toFixed(4)&lt;/code&gt; in JavaScript gives
&lt;code&gt;0.5313&lt;/code&gt;. One listing out of 63,755 hit this.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Using it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;gunsnprices&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;parse_title&lt;/span&gt;

&lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Federal American Eagle 5.56x45mm 55gr FMJ 1000 Rounds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;449.99&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;            &lt;span class="c1"&gt;# 'ammo'
&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;caliber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;    &lt;span class="c1"&gt;# '5.56×45mm NATO'
&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rounds&lt;/span&gt;          &lt;span class="c1"&gt;# 1000
&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cost_per_round&lt;/span&gt;  &lt;span class="c1"&gt;# 0.45
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;parseTitle&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gunsnprices&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;parseTitle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Glock 19 Gen 5 MOS 9mm 4.02in 15rd Pistol&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 'Glock 19 Gen 5 MOS'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is also an &lt;a href="https://github.com/gunsnprices/gunsnprices-mcp" rel="noopener noreferrer"&gt;MCP server&lt;/a&gt; (&lt;code&gt;npx -y gunsnprices-mcp&lt;/code&gt;) that gives AI&lt;br&gt;
assistants like Claude and Cursor the same tools, plus links to the GunsNPrices pages where current prices are.&lt;/p&gt;

&lt;p&gt;The model dictionaries and the caliber list are published as open datasets under CC BY 4.0 on&lt;br&gt;
&lt;a href="https://huggingface.co/gunsnprices" rel="noopener noreferrer"&gt;Hugging Face&lt;/a&gt;, &lt;a href="https://www.kaggle.com/gunsnprices" rel="noopener noreferrer"&gt;Kaggle&lt;/a&gt; and&lt;br&gt;
&lt;a href="https://github.com/gunsnprices/firearm-datasets" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;More brands, a price index of ammo cost per round by caliber built from daily prices, and a public API if there is&lt;br&gt;
interest. Issues and corrections are welcome on &lt;a href="https://github.com/gunsnprices" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. If you just want to know where&lt;br&gt;
a gun or a box of ammo is cheapest today, that's what &lt;a href="https://gunsnprices.com" rel="noopener noreferrer"&gt;gunsnprices.com&lt;/a&gt; is for.&lt;/p&gt;

</description>
      <category>python</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>ecommerce</category>
    </item>
  </channel>
</rss>
