Dinamai menangkap grup di JavaScript regex?

208

Sejauh yang saya tahu tidak ada yang namanya grup penangkap bernama dalam JavaScript. Apa cara alternatif untuk mendapatkan fungsionalitas serupa?

mmierin
sumber
1
Grup tangkap dalam javascript adalah dengan angka .. $ 1 adalah grup yang pertama ditangkap, $ 2, $ 3 ... hingga $ 99 tetapi sepertinya Anda menginginkan sesuatu yang lain - yang tidak ada
Erik
24
@Erik Anda berbicara tentang grup penangkap bernomor , OP berbicara tentang kelompok penangkap bernama . Mereka ada, tetapi kami ingin tahu apakah ada dukungan untuk mereka di JS.
Alba Mendez
4
Ada proposal untuk membawa regex yang dinamai ke dalam JavaScript , tetapi mungkin bertahun-tahun sebelum kita melihatnya, jika kita melakukannya.
fregante
Firefox menghukum saya karena mencoba menggunakan kelompok tangkapan bernama di situs web ... salahku sendiri. stackoverflow.com/a/58221254/782034
Nick Grealy

Jawaban:

134

ECMAScript 2018 memperkenalkan kelompok penangkap bernama ke dalam regex JavaScript.

Contoh:

  const auth = 'Bearer AUTHORIZATION_TOKEN'
  const { groups: { token } } = /Bearer (?<token>[^ $]*)/.exec(auth)
  console.log(token) // "Prints AUTHORIZATION_TOKEN"

Jika Anda perlu mendukung browser lama, Anda dapat melakukan semuanya dengan grup penangkap normal (bernomor) yang dapat Anda lakukan dengan grup penangkap bernama, Anda hanya perlu melacak angka - yang mungkin rumit jika urutan grup penangkap dalam Anda perubahan regex.

Hanya ada dua keuntungan "struktural" dari kelompok penangkap bernama yang dapat saya pikirkan:

  1. Dalam beberapa rasa regex (.NET dan JGSoft, sejauh yang saya tahu), Anda dapat menggunakan nama yang sama untuk grup yang berbeda di regex Anda ( lihat di sini untuk contoh di mana ini penting ). Tetapi sebagian besar rasa regex tidak mendukung fungsi ini.

  2. Jika Anda perlu merujuk ke grup penangkap bernomor dalam situasi di mana mereka dikelilingi oleh angka, Anda bisa mendapatkan masalah. Katakanlah Anda ingin menambahkan nol ke digit dan karenanya ingin mengganti (\d)dengan $10. Dalam JavaScript, ini akan berfungsi (selama Anda memiliki kurang dari 10 grup yang menangkap di regex Anda), tetapi Perl akan berpikir Anda sedang mencari nomor referensi 10bukan angka 1, diikuti oleh a 0. Di Perl, Anda dapat menggunakan ${1}0dalam hal ini.

Selain itu, kelompok penangkap bernama hanya "gula sintaksis". Ini membantu untuk menggunakan grup menangkap hanya ketika Anda benar-benar membutuhkannya dan menggunakan grup yang tidak menangkap (?:...)dalam semua keadaan lain.

Masalah yang lebih besar (menurut saya) dengan JavaScript adalah bahwa ia tidak mendukung regex verbose yang akan membuat pembuatan ekspresi reguler yang mudah dibaca dan kompleks menjadi jauh lebih mudah.

Pustaka XRegExp Steve Levithan memecahkan masalah ini.

Tim Pietzcker
sumber
5
Banyak rasa memungkinkan menggunakan nama grup pengambilan yang sama beberapa kali dalam regex. Tetapi hanya .NET dan Perl 5.10+ yang membuat ini sangat berguna dengan menjaga nilai yang ditangkap oleh kelompok terakhir dari nama yang berpartisipasi dalam pertandingan.
slevithan
103
Keuntungan besar adalah: Anda hanya dapat mengubah RegExp Anda, tidak ada pemetaan jumlah ke variabel. Grup yang tidak menangkap menyelesaikan masalah ini, kecuali pada satu kasus: bagaimana jika urutan grup berubah? Juga, sangat sulit untuk menempatkan karakter ekstra ini pada kelompok lain ...
Alba Mendez
55
Gula sintaksis yang disebut tidak membantu mempermanis pembacaan kode!
Mrchief
1
Saya pikir ada alasan lain untuk menamai kelompok yang benar-benar berharga. Misalnya, jika Anda ingin menggunakan regex untuk mem-parsing tanggal dari sebuah string, Anda bisa menulis fungsi fleksibel yang mengambil nilai dan regex. Selama regex telah menamai tangkapan untuk tahun, bulan dan tanggal Anda bisa menjalankan melalui array ekspresi reguler dengan kode minimal.
Dewey Vozel
4
Pada Oktober 2019, Firefox, IE 11 dan Microsoft Edge (pre-Chromium) tidak mendukung tangkapan kelompok yang diberi nama. Sebagian besar browser lain (bahkan Opera dan ponsel Samsung) melakukannya. caniuse.com/...
JDB masih mengingat Monica
63

Anda dapat menggunakan XRegExp , implementasi lintas-reguler dari ekspresi reguler yang diperbesar, dapat diperluas, lintas-browser, termasuk dukungan untuk sintaks tambahan, flag, dan metode:

  • Menambahkan sintaks regex dan teks pengganti baru, termasuk dukungan komprehensif untuk tangkapan bernama .
  • Menambahkan dua flag regex baru s:, untuk membuat dot cocok dengan semua karakter (alias mode dotall atau singleline), dan x, untuk spasi bebas dan komentar (alias mode diperluas).
  • Menyediakan serangkaian fungsi dan metode yang membuat pemrosesan regex yang kompleks menjadi mudah.
  • Secara otomatis memperbaiki inkonsistensi lintas-browser yang paling sering ditemui dalam perilaku regex dan sintaksis.
  • Memungkinkan Anda dengan mudah membuat dan menggunakan plugin yang menambahkan sintaks dan flag baru ke bahasa ekspresi reguler XRegExp.
Yunga Palatino
sumber
60

Solusi lain yang mungkin: buat objek yang berisi nama dan indeks grup.

var regex = new RegExp("(.*) (.*)");
var regexGroups = { FirstName: 1, LastName: 2 };

Kemudian, gunakan tombol objek untuk referensi grup:

var m = regex.exec("John Smith");
var f = m[regexGroups.FirstName];

Ini meningkatkan keterbacaan / kualitas kode menggunakan hasil dari regex, tetapi bukan keterbacaan dari regex itu sendiri.

Tuan TA
sumber
58

Di ES6 Anda dapat menggunakan array restrukturisasi untuk menangkap grup Anda:

let text = '27 months';
let regex = /(\d+)\s*(days?|months?|years?)/;
let [, count, unit] = regex.exec(text) || [];

// count === '27'
// unit === 'months'

Memperhatikan:

  • koma pertama pada yang terakhir letmelewatkan nilai pertama dari array yang dihasilkan, yang merupakan keseluruhan string yang cocok
  • yang || []setelah .exec()akan mencegah kesalahan destrukturisasi ketika tidak ada pertandingan (karena .exec()akan kembali null)
fregante
sumber
1
Koma pertama adalah karena elemen pertama array yang dikembalikan oleh kecocokan adalah ekspresi input, kan?
Emilio Grisolía
1
String.prototype.matchmengembalikan array dengan: seluruh string yang cocok di posisi 0, lalu grup apa pun setelah itu. Koma pertama mengatakan "lewati elemen di posisi 0"
fregante
2
Jawaban favorit saya di sini untuk orang-orang dengan target pengalihan atau ES6 +. Ini tidak serta merta mencegah kesalahan inkonsistensi dan juga indeks yang dinamai jika misalnya regex yang digunakan kembali berubah, tapi saya pikir keringkasan di sini dengan mudah menggantikannya. Saya sudah memilih untuk RegExp.prototype.execlebih String.prototype.matchdi tempat-tempat di mana string mungkin nullatau undefined.
Mike Hill
22

Pembaruan: Akhirnya berhasil masuk ke dalam JavaScript (ECMAScript 2018)!


Grup penangkap yang dinamai dapat membuatnya menjadi JavaScript segera.
Proposal untuk itu sudah pada tahap 3.

Grup tangkap dapat diberi nama di dalam kurung sudut menggunakan (?<name>...)sintaks, untuk nama pengidentifikasi apa pun. Ekspresi reguler untuk kencan kemudian dapat ditulis sebagai /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u. Setiap nama harus unik dan mengikuti tata bahasa untuk ECMAScript IdentifierName .

Grup yang diberi nama dapat diakses dari properti properti grup dari hasil ekspresi reguler. Referensi bernomor untuk grup juga dibuat, seperti halnya untuk grup yang tidak disebutkan namanya. Sebagai contoh:

let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
let result = re.exec('2015-01-02');
// result.groups.year === '2015';
// result.groups.month === '01';
// result.groups.day === '02';

// result[0] === '2015-01-02';
// result[1] === '2015';
// result[2] === '01';
// result[3] === '02';
Forivin
sumber
Ini proposal tahap 4 saat ini.
GOTO 0
Jika Anda menggunakan '18, mungkin lebih baik menghancurkannya; let {year, month, day} = ((result) => ((result) ? result.groups : {}))(re.exec('2015-01-02'));
Hashbrown
6

Memberi nama grup yang ditangkap memberikan satu hal: lebih sedikit kebingungan dengan ekspresi reguler yang kompleks.

Ini benar-benar tergantung pada kasus penggunaan Anda, tetapi mungkin pencetakan cukup regex Anda bisa membantu.

Atau Anda bisa mencoba dan mendefinisikan konstanta untuk merujuk ke grup yang Anda tangkap.

Komentar mungkin juga membantu menunjukkan kepada orang lain yang membaca kode Anda, apa yang telah Anda lakukan.

Selebihnya saya harus setuju dengan jawaban Tims.

Yashima
sumber
5

Ada perpustakaan node.js bernama bernama-regexp yang bisa Anda gunakan dalam proyek node.js Anda (aktif di browser dengan mengemas perpustakaan dengan browserify atau skrip pengemasan lainnya). Namun, perpustakaan tidak dapat digunakan dengan ekspresi reguler yang berisi grup penangkap tanpa nama.

Jika Anda menghitung kawat penangkap pembuka dalam ekspresi reguler Anda, Anda dapat membuat pemetaan antara grup penangkap yang diberi nama dan grup penangkap yang diberi nomor di regex Anda dan dapat mencampur dan mencocokkan secara bebas. Anda hanya perlu menghapus nama grup sebelum menggunakan regex. Saya telah menulis tiga fungsi yang menunjukkan itu. Lihat inti ini: https://gist.github.com/gbirke/2cc2370135b665eee3ef

Chiborg
sumber
Itu mengejutkan ringan, saya akan mencobanya
gratis
Apakah ini berfungsi dengan grup yang diberi nama bersarang di dalam grup reguler dalam ekspresi reguler yang kompleks?
ElSajko
Itu tidak sempurna. Bug ketika: getMap ("((a | b (: <foo> c)))"); foo harus menjadi kelompok ketiga, bukan yang kedua. /((a|b(c)))/g.exec("bc "); ["bc", "bc", "bc", "c"]
ElSajko
3

Seperti yang dikatakan Tim Pietzcker , ECMAScript 2018 memperkenalkan kelompok penangkap bernama ke dalam regex JavaScript. Tetapi apa yang saya tidak temukan dalam jawaban di atas adalah bagaimana menggunakan kelompok yang ditangkap nama di regex itu sendiri.

Anda dapat menggunakan nama kelompok yang diambil dengan sintaks ini: \k<name>. sebagai contoh

var regexObj = /(?<year>\d{4})-(?<day>\d{2})-(?<month>\d{2}) year is \k<year>/

dan seperti yang dikatakan Forivin , Anda dapat menggunakan grup yang ditangkap di hasil objek sebagai berikut:

let result = regexObj.exec('2019-28-06 year is 2019');
// result.groups.year === '2019';
// result.groups.month === '06';
// result.groups.day === '28';

  var regexObj = /(?<year>\d{4})-(?<day>\d{2})-(?<month>\d{2}) year is \k<year>/mgi;

function check(){
    var inp = document.getElementById("tinput").value;
    let result = regexObj.exec(inp);
    document.getElementById("year").innerHTML = result.groups.year;
    document.getElementById("month").innerHTML = result.groups.month;
    document.getElementById("day").innerHTML = result.groups.day;
}
td, th{
  border: solid 2px #ccc;
}
<input id="tinput" type="text" value="2019-28-06 year is 2019"/>
<br/>
<br/>
<span>Pattern: "(?<year>\d{4})-(?<day>\d{2})-(?<month>\d{2}) year is \k<year>";
<br/>
<br/>
<button onclick="check()">Check!</button>
<br/>
<br/>
<table>
  <thead>
    <tr>
      <th>
        <span>Year</span>
      </th>
      <th>
        <span>Month</span>
      </th>
      <th>
        <span>Day</span>
      </th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>
        <span id="year"></span>
      </td>
      <td>
        <span id="month"></span>
      </td>
      <td>
        <span id="day"></span>
      </td>
    </tr>
  </tbody>
</table>

Hamed Mahdizadeh
sumber
2

Meskipun Anda tidak dapat melakukan ini dengan JavaScript vanilla, mungkin Anda dapat menggunakan beberapa Array.prototypefungsi seperti Array.prototype.reducemengubah kecocokan yang diindeks menjadi yang bernama menggunakan beberapa sihir .

Jelas, solusi berikut akan membutuhkan pencocokan agar:

// @text Contains the text to match
// @regex A regular expression object (f.e. /.+/)
// @matchNames An array of literal strings where each item
//             is the name of each group
function namedRegexMatch(text, regex, matchNames) {
  var matches = regex.exec(text);

  return matches.reduce(function(result, match, index) {
    if (index > 0)
      // This substraction is required because we count 
      // match indexes from 1, because 0 is the entire matched string
      result[matchNames[index - 1]] = match;

    return result;
  }, {});
}

var myString = "Hello Alex, I am John";

var namedMatches = namedRegexMatch(
  myString,
  /Hello ([a-z]+), I am ([a-z]+)/i, 
  ["firstPersonName", "secondPersonName"]
);

alert(JSON.stringify(namedMatches));

Fidemraizer Matías
sumber
Itu keren sekali. Saya hanya berpikir .. apakah tidak mungkin membuat fungsi regex yang menerima kustom regex? Agar Anda bisa sepertivar assocArray = Regex("hello alex, I am dennis", "hello ({hisName}.+), I am ({yourName}.+)");
Forivin
@ Forivin Jelas Anda dapat melangkah lebih jauh dan mengembangkan fitur ini. Ini tidak akan sulit untuk membuatnya bekerja: D
Matías Fidemraizer
Anda dapat memperluas RegExpobjek dengan menambahkan fungsi ke prototipe-nya.
Tn. TA
@ Mr.TA AFAIK, tidak disarankan untuk memperluas objek
bawaan
0

Tidak punya ECMAScript 2018?

Tujuan saya adalah membuatnya bekerja semirip mungkin dengan yang biasa kami lakukan dengan grup yang disebutkan. Sedangkan dalam ECMAScript 2018 Anda dapat menempatkan ?<groupname>di dalam grup untuk menunjukkan grup bernama, dalam solusi saya untuk javascript yang lebih lama, Anda dapat menempatkan (?!=<groupname>)di dalam grup untuk melakukan hal yang sama. Jadi ini adalah seperangkat kurung tambahan dan tambahan !=. Cukup dekat!

Saya membungkus semuanya menjadi fungsi prototipe string

fitur

  • bekerja dengan javascript yang lebih lama
  • tidak ada kode tambahan
  • cukup mudah digunakan
  • Regex masih berfungsi
  • kelompok didokumentasikan dalam regex itu sendiri
  • nama grup dapat memiliki spasi
  • mengembalikan objek dengan hasil

Instruksi

  • tempatkan (?!={groupname})di dalam setiap grup yang ingin Anda beri nama
  • ingatlah untuk menghilangkan kelompok yang tidak menangkap ()dengan menempatkan ?:di awal kelompok itu. Ini tidak akan disebutkan namanya.

arrays.js

// @@pattern - includes injections of (?!={groupname}) for each group
// @@returns - an object with a property for each group having the group's match as the value 
String.prototype.matchWithGroups = function (pattern) {
  var matches = this.match(pattern);
  return pattern
  // get the pattern as a string
  .toString()
  // suss out the groups
  .match(/<(.+?)>/g)
  // remove the braces
  .map(function(group) {
    return group.match(/<(.+)>/)[1];
  })
  // create an object with a property for each group having the group's match as the value 
  .reduce(function(acc, curr, index, arr) {
    acc[curr] = matches[index + 1];
    return acc;
  }, {});
};    

pemakaian

function testRegGroups() {
  var s = '123 Main St';
  var pattern = /((?!=<house number>)\d+)\s((?!=<street name>)\w+)\s((?!=<street type>)\w+)/;
  var o = s.matchWithGroups(pattern); // {'house number':"123", 'street name':"Main", 'street type':"St"}
  var j = JSON.stringify(o);
  var housenum = o['house number']; // 123
}

hasil o

{
  "house number": "123",
  "street name": "Main",
  "street type": "St"
}
toddmo
sumber