Every few months someone asks me for a "list of Chinese surnames". The answer is always more annoying than it should be.
The lists that exist fall into three camps. The classic Hundred Family Surnames ordering — 赵钱孙李 — is a Song dynasty poem, not a frequency table, so sorting by it means nothing. Scraped CSVs have thousands of rows, no tone marks, and 欧阳 split into two separate characters somewhere in the middle. Genealogy tables have the detail but usually no licence, or a licence that says "contact us".
So while building a Chinese naming site I extracted the structured parts of my own surname pages and released them: 109 surnames, 13 fields per record, JSON and CSV, CC BY 4.0.
- JSON:
https://naming-cn.pages.dev/data/surnames.json(78 KB) - CSV:
https://naming-cn.pages.dev/data/surnames.csv(18 KB, UTF-8 with BOM) - Docs:
https://naming-cn.pages.dev/en/chinese-surnames-dataset
Both files are served with Access-Control-Allow-Origin: *, so this works from the browser:
const rows = await fetch('https://naming-cn.pages.dev/data/surnames.json') .then(r => r.json()); rows.filter(r => r.rank).length; // 30 rows.find(r => r.surname === '李'); // full record No key, no signup. That was the whole point.
What's actually in a record
{ "surname": "李", "slug": "li3", "pinyin": "lǐ", "era": "商末", "ancestor": "李利贞(血缘始祖皋陶)", "origin": "河南鹿邑", "junwang": ["陇西", "赵郡"], "tanghao": [ { "name": "陇西堂", "note": "以郡望立堂,出自陇西一系" }, { "name": "青莲堂", "note": "李白自号青莲居士,后世一支取为堂号" } ], "rank": "全国第 2 位", "population": "约 1.01 亿人", "notables": ["李耳(老子)", "李广", "李世民", "李白", "李时珍"], "origin_note": "嬴姓。舜时皋陶任大理(掌刑狱),子孙以官为氏称「理」。……遂改「理」为「李」。", "url": "https://naming-cn.pages.dev/xing/li3" } Four decisions in there were less obvious than they look.
1. Compound surnames are one record, not two characters
30 of the 109 are two-character surnames — 欧阳, 司马, 诸葛, 尉迟, 万俟. If you split on characters you get garbage: 欧阳 is not "a surname 欧 followed by 阳", and 万俟 is read mò qí, not wàn sì. So surname holds one or two characters, and slug is the tone-numbered pinyin (ouyang2, moqi2) which doubles as the URL key.
That slug convention is also why lookups don't need tone marks. Want everything starting with li? /^li/ on the slug catches 李 (li3), 梁 (liang2), 廖 (liao4), 林 (lin2), 刘 (liu2), 卢 (lu2), 陆 (lu4), 罗 (luo2), 吕 (lv3) — without you having to type lǚ.
2. Empty means unknown, not zero
Only 30 of the 109 have a population rank. That's not laziness. The Ministry of Public Security's 2020 national name report — the only official Chinese surname statistics at national scale — publishes ranks and sizes for the top 100 surnames only. Below that, there is no official number, and every figure you find online for surname #147 is somebody's estimate presented as fact.
So rank and population are empty strings for the other 79, and the docs say plainly: empty means unknown, not zero. If your code does int(r['rank']), it will crash — deliberately. I would rather crash than silently aggregate 79 fake zeros.
3. Arrays are pipe-separated in the CSV
junwang, tanghao and notables are arrays. CSV has no array type, so they're 陇西|赵郡 — a delimiter choice, not a schema. Two consequences:
- The CSV flattens
tanghaofrom[{name, note}]to names only, because the note is a sentence and sentences with commas inside a CSV cell are a support ticket waiting to happen. If you need the derivations, use the JSON. - The BOM is there on purpose. Without it, Excel on a Chinese Windows machine opens the file as GBK and every surname is mojibake. The BOM costs you three bytes at the start of the file and one
encoding='utf-8-sig'if you're reading it back in Python.
4. Commandery and hall name are not the same field, and merging them loses the point
A junwang (郡望) is a region where a branch of the clan became prominent — the coordinate that determined a family's social rank from the Wei–Jin period through the Tang. A tanghao (堂号) is the inscription on that branch's ancestral hall. Many hall names just repeat the commandery (陇西堂 for the 陇西 branch of 李). Others commemorate an anecdote: 李's 青莲堂 comes from Li Bai calling himself the "Lotus Dweller", and the name carries a claim about poetry and wine rather than about geography.
Flatten both into one "origin" string and you lose exactly the part that's interesting.
The wart I haven't fixed
rank is a display string, "全国第 2 位", not a number. That was fine for rendering the pages and wrong for anyone who wants to sort. The fix is a rank_num field, and it's on the list. For now:
import pandas as pd df = pd.read_csv('https://naming-cn.pages.dev/data/surnames.csv') ranked = df[df['rank'].notna()] ranked['rank_num'] = ranked['rank'].str.extract(r'(\d+)').astype(int) ranked.sort_values('rank_num').head() Free-text Chinese in a machine-readable field is the kind of thing you only notice when someone else tries to use your data. Which is an argument for publishing the data in the first place.
Where the numbers come from
Population ranks and sizes: the 2020 MPS report. Commanderies: Tang and Song surname books — the Genealogical Tables of the Chancellors in the New Book of Tang, the Yuanhe Surname Compilation. Hall names: clan genealogies and surviving ancestral halls. The synthesis into one record per surname with a readable origin note is my own work, and errors in it are mine.
Where the sources disagree, the record keeps both readings instead of picking one. ancestor: "白公胜、公子白" is not a typo — it's two documented traditions for the same surname. If you're doing anything analytical with this, filter on origin_note containing 异说 or treat ancestor as potentially multi-valued.
Using it
CC BY 4.0 — commercial use, modification, redistribution all fine; attribution plus a licence link is the only condition.
Chinese Naming (2026). Chinese Surnames Open Dataset. https://naming-cn.pages.dev/en/chinese-surnames-dataset (CC BY 4.0) Corrections are genuinely welcome, especially from anyone who knows the genealogy literature better than I do. If a commandery is wrong, I'd rather be told than be cited.


