惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

量子位
Vercel News
Vercel News
Google DeepMind News
Google DeepMind News
罗磊的独立博客
WordPress大学
WordPress大学
The Cloudflare Blog
GbyAI
GbyAI
The Register - Security
The Register - Security
L
LangChain Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
Microsoft Security Blog
Microsoft Security Blog
MyScale Blog
MyScale Blog
A
About on SuperTechFans
U
Unit 42
T
The Blog of Author Tim Ferriss
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
V
Visual Studio Blog
云风的 BLOG
云风的 BLOG
Stack Overflow Blog
Stack Overflow Blog
博客园 - 三生石上(FineUI控件)
博客园 - 司徒正美
Blog — PlanetScale
Blog — PlanetScale
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
M
MIT News - Artificial intelligence
C
CERT Recently Published Vulnerability Notes
T
The Exploit Database - CXSecurity.com
T
Tor Project blog
A
Arctic Wolf
H
Hacker News: Front Page
NISL@THU
NISL@THU
F
Full Disclosure
雷峰网
雷峰网
L
LINUX DO - 热门话题
Recent Announcements
Recent Announcements
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Apple Machine Learning Research
Apple Machine Learning Research
Google Online Security Blog
Google Online Security Blog
I
InfoQ
Webroot Blog
Webroot Blog
S
Security Affairs
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
爱范儿
爱范儿
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
N
News | PayPal Newsroom
Forbes - Security
Forbes - Security
博客园 - Franky
V
Vulnerabilities – Threatpost
博客园 - 【当耐特】
Latest news
Latest news

alexwlchan

Preventing line breaks in <code> elements A Git hook to prevent committing directly to main Describing all my photos I don’t want to repeat repeat myself Rebuilding the computer room What can wonky APIs tell us about the web? Using the Screen Capture API to record a browser window Using Pytester to test my Playwright fixtures Rendering a chat thread in CSS and JavaScript Waiting for website changes in the browser Watching for file changes on macOS Using Playwright to test my static sites Building a basic cache with SQLite HTTP GET requests with the Python standard library Auditing my local Python packages Quietly quantum-resistant blogging Creating a personalised bin calendar Monki Gras 2026 “Prepping Craft” The selfish case for public libraries Dreaming of a ten-year computer Gumdrop, a silly app for messing with my webcam The bare minimum for syncing Git repos Creating Caddyfiles with Cog Swapping gems for tiles Parody posters for made-up movies The Good, the Bad, and the Gutters Using perceptual distance to create better headers The passwords I actually memorise Where I store my multi-factor recovery codes Quick-and-dirty print debugging in Go My favourite books from 2025 Drawing Truchet tiles in SVG Adding a README to S3 buckets with Terraform The palm tree that led to Palmyra
Fixing a bug with byte order marks
2026-07-18 · via alexwlchan

Recently I’ve been tidying up the subtitles in my local media library. There are two popular file formats for subtitles: SRT (SubRip Subtitle) and WebVTT (Web Video Text Tracks).

I’ve been standardising on WebVTT because it works with the HTML5 <video> element, and I play all my videos through the <video> element embedded in static websites. However, lots of subtitles are only available as SRT, so I wrote a Python function to convert SRT files to WebVTT. The formats looked simple and the conversion seemed straightforward. Famous last words!

When I spot checked the converted subtitles, I noticed a bug in my handling of byte order marks (BOM), and it took several steps to fix.

Failing to look for the UTF‑8 BOM

A byte order mark is a special use of the zero width no-break space character U+FEFF at the beginning of a text file, which tella a program reading the file about how the text is encoded. It depends on the exact sequence of bytes used to encode the character. Here are a few examples:

  • EF BB BF – the file is UTF‑8 text. UTF‑8 always has the same byte order, so it’s just telling us about the encoding.
  • FE FF – the file is UTF‑16 text, with big-endian byte order (UTF‑16BE).
  • FF FE – the file is UTF‑16 text, with little-endian byte order (UTF‑16LE).
  • 00 00 FE FF – the file is UTF‑32 text, with big-endian byte order.

All of my SRT input files were UTF‑8 encoded, and some of them had the UTF‑8 byte order mark, and I wasn’t handling it correctly. For example, suppose I had this input SRT file:

<U+FEFF>1
00:00:01,001 --> 00:00:10,010
You have grown, Keyne.

2
00:02:00,002 --> 00:20:00,020
Soon you’ll be needing another name.

When I convert to WebVTT, I want to add the WEBVTT header, remove the sequence numbers, and change the timestamp format.

To remove sequence numbers, I was checking if a line was all digits. Because the BOM is on the same line as the first sequence number, the line isn’t all digits, so I didn’t remove it. Instead, I copied the entire line into the middle of the WebVTT file, BOM and all:

WEBVTT

<U+FEFF>1
00:00:01.001 --> 00:00:10.010
You have grown, Keyne.

00:02:00.002 --> 00:20:00.020
Soon you’ll be needing another name.

The correct conversion would remove both the byte order mark and that first sequence number:

WEBVTT

00:00:01.001 --> 00:00:10.010
You have grown, Keyne.

00:02:00.002 --> 00:20:00.020
Soon you’ll be needing another name.

In my local media library, I can assume everything is UTF‑8. I can safely remove the byte order marks, and my web browser will still decode my subtitles correctly.

Fixing the converter with encoding="utf-8-sig"

In my first fix, I tried to handle the BOM manually. I wrote code that looked for U+FEFF and stripped it from the file, trying to detect it and re-insert it into the converted WebVTT file. (This was before I realised I could just remove it entirely.) It was a bit messy, because I was mixing low-level text encoding code with my high-level subtitle conversion steps.

As I was researching this article, I realised there’s a more elegant solution: if I open the SRT file with encoding="utf-8-sig", Python will automatically detect and skip the optional UTF‑8 encoded BOM at the start of the file. The rest of my code doesn’t know or care that it’s there.

I fixed the bug in my function, which means future conversions will work correctly – but what about the broken files I’ve already generated?

Detecting the UTF‑8 BOM with ripgrep

Initially I tried searching for U+FEFF with TextMate, but it crashed consistently with that search, so I turned to command-line tools.

I use ripgrep for searching text. By default it does “BOM sniffing” on files – when it reads a file, it looks at the first few bytes, transcodes the file from its actual encoding to UTF‑8, then executes the search on the transcoded version. This is exactly how the BOM is meant to be used, but it’s less helpful if the BOM itself is what you’re searching for!

Instead, we can disable ripgrep’s Unicode support and search raw bytes by using (?-u:…) in the regular expression. (This flag comes from Rust’s regex crate.) The following command looks for lines that start with the UTF‑8 BOM:

$ rg '^(?-u:\xEF\xBB\xBF)'

If you were only looking for the BOM at the start of the file, you’d also want the --multiline flag. That changes the caret ^ to anchor to the start of the file, not the start of any line. But since I’m looking for BOMs which are in the middle of the file, omitting --multiline is correct.

This search threw up dozens of files with a broken BOM. Initially I opened the broken files in TextMate and edited them manually:

$ rg --files-with-matches --null '^(?-u:\xEF\xBB\xBF)' | xargs -0 mate

But I quickly realised this was too slow, so I wrote a Python script to clean up all the files at once:

#!/usr/bin/env python3

import glob

for filepath in glob.glob("**/*.vtt", recursive=True):
    with open(filepath, "rb") as f:
        content = f.read()
        
    if b"\xef\xbb\xbf" in content:
        content = content.replace(b"\xef\xbb\xbf", b"")
        with open(filepath, "wb") as f:
            f.write(content)
        print(filepath)

Once I’d run this script, I used my ripgrep command to check it was correct – and indeed, all the erronous BOMs had been stripped from my media collection. I also track my subtitle files in a Git repo, so I could confirm the script didn’t introduce other changes.

Before this bug, I’d only vaguely heard of byte order marks, and I’d never had to tackle them in anger. This sort of lesson is exactly why I love managing my local media archives as hand-built static websites – the lo-fi approach gives me lots of opportunities to explore low-level ideas and learn how things actually work on my computer.