Skip to contents

cjk_segmenters() lists the engines cjk_segment() can dispatch to, and register_cjk_segmenter() adds one.

Usage

cjk_segmenters()

register_cjk_segmenter(name, fn)

Arguments

name

Name of the engine, a single string.

fn

A function of (x, ...) returning a list of character vectors.

Value

cjk_segmenters() returns a character vector of engine names. register_cjk_segmenter() is called for its side effect and returns name invisibly.

Details

Where a word begins and ends in CJK text is a fact about a language, not about Unicode, so it cannot be derived the way everything else in this package is. It needs a dictionary and a statistical model, and which one is right depends on the language and the corpus. tidycjk therefore dispatches on a name rather than committing to one.

Two engines ship with the package.

"icu" is a real word segmenter. ICU carries dictionary-based break iterators for Chinese and Japanese, and stringi carries ICU, so this costs no dependency you have not already installed: a six-character Chinese sentence comes back as its four words rather than as six characters.

It returns surface forms only, with no part of speech, lemma or user dictionary, and its models are lighter than MeCab's or jieba's tuned ones. It is the right starting point and not the last word.

locale is accepted and forwarded, but it does not select the dictionary. ICU applies a single combined Chinese-Japanese word list to Han and kana runs, chosen by the script of the text, so Chinese and Japanese segment the same way under "zh", "ja" or the session default.

"character" needs nothing at all: every CJK character becomes its own token and runs of non-CJK text are split on whitespace. Whitespace is never a token, the ideographic space U+3000 included, even though has_cjk() counts it as CJK. It is character tokenisation rather than word segmentation, and for Chinese it will cut two-character words in half. It is a baseline, not an answer.

The two engines do not tokenise punctuation alike

This matters when comparing counts, so it is worth stating rather than leaving to be discovered. "icu" drops punctuation and symbols along with whitespace; "character" keeps CJK punctuation as tokens, because those code points are in blocks cjk_blocks() lists, and keeps a run of non-CJK text whole up to the next space.

The upshot is that the same sentence yields different token counts, and the gap is punctuation rather than a disagreement about where words end. An emoji is dropped by "icu" and kept by "character" for the same reason. Filter or compare accordingly.

Zero-width characters divide the same way, in three groups. "character" keeps all of them wherever they sit. "icu" drops U+200B everywhere, because ICU classifies the zero-width space as "none" in every position and this engine asks for "none" to be skipped; it drops U+200D, U+00AD, U+2060, a combining mark and a variation selector only when one begins the string, which is the one position ICU calls "none" for those. A byte-order mark survives both engines, and for a different reason worth keeping straight: stringi removes a leading U+FEFF before ICU is called at all, so that one is data loss rather than policy, and both engines put it back.

Registering another segmenter

For Japanese, gibasa binds MeCab and is on CRAN; it gives part of speech and lemma, which "icu" does not.

jiebaR, which binds cppjieba, is the usual choice for Chinese. It was archived from CRAN on 2025-05-01, so it cannot be a dependency of a CRAN package and install.packages() will not find it; install it from source with remotes::install_github("qinwf/jiebaR"). Once you have it, four lines make it an engine:

register_cjk_segmenter("jiebar", function(x, ...) {
  worker <- jiebaR::worker(...)
  lapply(x, function(s) {
    if (is.na(s)) return(NA_character_)
    if (!nzchar(s)) return(character(0))
    as.character(jiebaR::segment(s, worker))
  })
})

The same shape works for any segmenter you can call from R.

The engine contract

An engine is any function taking (x, ...) – a character vector and the dots from cjk_segment() – and returning a list the same length as x, each element a character vector of tokens. NA input should give NA_character_ and the empty string should give character(0); cjk_segment() checks the shape and complains if an engine breaks the contract. A plain list is required: a data frame is a list too, but length() on one counts columns rather than elements, so it is refused rather than quietly mistaken for a list of tokens.

A leading byte-order mark is preserved by both built-in engines, and by neither of the stringi calls underneath them: stri_split_charclass() and stri_split_boundaries() each read a leading U+FEFF as a byte-order mark and drop it. Both engines count it off and put it back, so a column read from a BOM-carrying CSV tokenises without losing a character. An engine you register yourself gets no such treatment – if it calls stringi, it needs the same guard.

The elements are checked too, for the same reason one level down. An atomic element is coerced with as.character(), so an engine returning integers or a factor works; a list element is refused, because as.character() deparses a list rather than coercing it and list(c(1, 2)) would otherwise arrive as the single token "c(1, 2)" – R code spelled out as data.

Passing arguments to an engine

Anything in ... goes to the engine, which is how you configure one. Name those arguments so that they are not a prefix of an argument of the verb itself: ... comes after engine in cjk_segment(), and after data and col in cjk_tokens(), so R's partial matching claims a prefix of one of those before the dots ever see it.

It is worth knowing because the result does not look like an argument-matching problem. cjk_tokens(df, text, "mine", c = 1) matches c to col, which pushes the bare text into engine, where it resolves to graphics::text() – a function, so it is accepted as an engine – and the error you get is about plotting. Single letters and short prefixes are the risk: c, co, d, da, e, en, eng. A longer name, or a closure that captures the setting instead of passing it, avoids the question:

register_cjk_segmenter("mine", function(x, ...) my_segmenter(x, cutoff = 1))

What registering does, and does not, undo

A registration lasts for the rest of the session and there is no function to remove one. Registering the same name again replaces it, which is the way to correct an engine you got wrong.

A name that matches a built-in shadows it. That is deliberate – it is how you substitute your own tokeniser for "character" without this package getting a say – but it is worth knowing that "character" is a natural name for an engine and taking it hides the built-in for the session, with nothing in cjk_segmenters() to show that anything changed. Pick a distinct name unless shadowing is what you meant.

Examples

cjk_segmenters()
#> [1] "character" "icu"      

# an engine that splits on an explicit marker
register_cjk_segmenter("pipe", function(x, ...) strsplit(x, "|",
                                                         fixed = TRUE))
cjk_segment("\u4e2d\u6587|\u5f88\u597d", engine = "pipe")
#> [[1]]
#> [1] "中文" "很好"
#>