Newsletter
TechAnV Blog
Get updates on security engineering, Rust, eBPF, and DevSecOps. No spam, unsubscribe anytime.
Check your inbox and click the confirmation link to complete your subscription.
Combining substr and instr to extract text#
Derek Willis has a Datasette instance full of political campaign emails running on Heroku.
Matt Hodges pointed out that a lot of these emails include refcode= codes, which are used by ActBlue campaigns to track clicks.
They look like this:
...c-email?refcode=220210_FR_midmonth1kavin_plain] Jessica Mason......hmp-footer?refcode=2021_footer&amount=25&a...
I thought it would be fun to extract just the codes.
The datasette-rure plugin adds regular expression support which can be used for this kind of thing, but in the absence of a plugin like that the only way to do it is with the SQLite instr() and substr() functions.
Here’s the query I figured out:
1with snippets as (2 select3 substr(body, instr(body, 'refcode=') + 8, 128) as snippet4 from5 emails6 where7 body LIKE '%refcode%'8),9refcodes as (10 select11 snippet,12 substr(13 snippet,14 0,15 min(16 case17 when instr(snippet, '&') > 0 then instr(snippet, '&')18 else 12819 end,20 case21 when instr(snippet, ']') > 0 then instr(snippet, ']')22 else 12823 end,24 case25 when instr(snippet, ' ') > 0 then instr(snippet, ' ')26 else 12827 end,28 case29 when instr(snippet, '.') > 0 then instr(snippet, '.')30 else 12831 end32 )33 ) as refcode34 from35 snippets36)37select38 refcode,39 count(*) as n40from41 refcodes42group by43 refcode44order by45 n descI started by pulling out just the 128 characters following each refcode= - I picked 128 characters at random just to make the data easier to look at:
1 substr(body, instr(body, 'refcode=') + 8, 128) as snippetinstr(body, 'refcode=') + 8 gives the character after the = sign, because refcode= is 8 characters long.
Next I needed to find the first character following the refcode that was either a &, a ], a space or a.`. That’s what this bit does:
1 substr(2 snippet,3 0,4 min(5 case6 when instr(snippet, '&') > 0 then instr(snippet, '&')7 else 1288 end,9 case10 when instr(snippet, ']') > 0 then instr(snippet, ']')11 else 12812 end,13 case14 when instr(snippet, ' ') > 0 then instr(snippet, ' ')15 else 12816 end,17 case18 when instr(snippet, '.') > 0 then instr(snippet, '.')19 else 12820 end21 )22 ) as refcodeI’m trying to find the first instance of any of those characters - so I use instr to find them, but ignore any results where that returns 0 for “character not found” - in those cases I use the number 128 picked earlier. I can then grab the minimum of those scores.
Then finally I do a group-by/count to find the most common refcodes:
1select2 refcode,3 count(*) as n4from5 refcodes6group by7 refcode8order by9 n descTop results were:
| refcode | n |
|---|---|
| email_footer | 527 |
| em_pt | 352 |
| em_fr_2020 | 285 |
| pt | 254 |
| emfooter | 242 |
| em_fr_2021 | 242 |
| footer-bio | 198 |
| footer_button | 192 |
| em_footer | 173 |
| email-footer | 168 |
| 168 | |
| footer | 164 |
| em2021 | 135 |
| em_jc_fr_footer_link | 107 |
| em_fr_2019 | 107 |
| em-footer | 104 |
| em_fr_2018 | 84 |
| ABD_EM_FR_2021 | 67 |