/* LanguageTool, a natural language style checker * Copyright (C) 2015 Daniel Naber (http://www.danielnaber.de) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 * USA */ package org.languagetool.chunking; import edu.washington.cs.knowitall.regex.Match; import edu.washington.cs.knowitall.regex.RegularExpression; import org.languagetool.AnalyzedTokenReadings; import java.util.*; import java.util.regex.Pattern; import static org.languagetool.chunking.GermanChunker.PhraseType.*; /** * A rule-based German chunker for noun phrases. Please note that this chunker * has not been evaluated as a stand-alone chunker, it has only been used and tested * in the context of LanguageTool's error detection rules. * @since 2.9 */ public class GermanChunker implements Chunker { private static final Set<String> FILTER_TAGS = new HashSet<>(Arrays.asList("PP", "NPP", "NPS")); private static final TokenExpressionFactory FACTORY = new TokenExpressionFactory(false); private static final Map<String,String> SYNTAX_EXPANSION = new HashMap<>(); static { SYNTAX_EXPANSION.put("<NP>", "<chunk=B-NP> <chunk=I-NP>*"); SYNTAX_EXPANSION.put("&prozent;", "Prozent|Kilo|Kilogramm|Gramm|Euro|Pfund"); } enum PhraseType { NP, // "noun phrase", will be assigned as B-NP for the first token and I-NP for following tokens (like OpenNLP) NPS, // "noun phrase singular" NPP, // "noun phrase plural" PP // "prepositional phrase" and similar } /** @deprecated for internal use only */ public static void setDebug(boolean debugMode) { debug = debugMode; } /** @deprecated for internal use only */ public static boolean isDebug() { return debug; } private static boolean debug = false; /* * REGEXES1 and REGEXES2 are OpenRegex (https:*github.com/knowitall/openregex) expressions. * REGEXES1 roughly emulates the behavior of the OpenNLP chunker by tagging the first * token of a noun phrase with B-NP and the remaining ones with I-NP. * REGEXES2 builds on those annotations to find complex noun phrases. * * Syntax: * <string|regex|regexCS|chunk|pos|posregex|posre=value> * string: matches the token itself * regex: matches the token against a regular expression * regexCS: is like regex but case-sensitive * chunk: matches the token's chunk * pos: matches the token's POS tags * posregex: matches the token's POS tags against a regular expression * posre: is a synonym for posregex * <foo> is a short form of <string=foo> * <pos=X> will match tokens with POS tags that contain X as a substring * * Example to combine two conditions via logical AND: * <pos=ADJ & chunk=B-NP> * Example: Quote a regular expression so OpenRegex doesn't get confused: * <posre='.*(NOM|AKK).*'> * * See SYNTAX_EXPANSION for strings that get expanded before interpreted by OpenRegex. * The chunks are added to the existing chunks, unless the last argument of build() is * true, in which case existing chunks get overwritten. */ private static final List<RegularExpressionWithPhraseType> REGEXES1 = Arrays.asList( // "das Auto", "das schöne Auto", "das sehr schöne Auto", "die Pariser Innenstadt": build("(<posre=^ART.*>|<pos=PRO>)? <pos=ADV>* <pos=PA2>* <pos=ADJ>* <pos=SUB>+", NP), // "Mythen und Sagen": build("<pos=SUB> (<und|oder>|(<bzw> <.>)) <pos=SUB>", NP), // "ältesten und bekanntesten Maßnahmen": build("<pos=ADJ> (<und|oder>|(<bzw> <.>)) <pos=PA2> <pos=SUB>", NP), // "räumliche und zeitliche Abstände": build("<pos=ADJ> (<und|oder>|(<bzw> <.>)) <pos=ADJ> <pos=SUB>", NP), // "eine leckere Lasagne": build("<posre=^ART.*> <pos=ADV>* <pos=ADJ>* <regexCS=[A-ZÖÄÜ][a-zöäü]+>", NP), // Lexikon kennt nicht alle Nomen, also so... //build("<posre=^ART.*>? <pos=PRO>? <pos=ZAL> <pos=SUB>"), // "zwei Wochen" build("<pos=PRO>? <pos=ZAL> <pos=SUB>", NP), // "zwei Wochen", "[eines] ihrer drei Autos" build("<Herr|Herrn|Frau> <pos=EIG>+", NP), build("<Herr|Herrn|Frau> <regexCS=[A-ZÖÄÜ][a-zöäü-]+>+", NP), // für seltene Nachnamen, die nicht im Lexikon sind build("<der>", NP) // simulate OpenNLP?! ); private static final List<RegularExpressionWithPhraseType> REGEXES2 = Arrays.asList( // ===== plural and singular noun phrases, based on OpenNLP chunker output =============== // "In christlichen, islamischen und jüdischen Traditionen": build("<pos=ADJ> <,> <chunk=B-NP> <chunk=I-NP>* <und|sowie> <NP>", NPP), // "ein Hund und eine Katze": build("<chunk=B-NP & !regex=jede[rs]?> <chunk=I-NP>* <und|sowie> <NP>", NPP), // "größte und erfolgreichste Erfindung" (fixes mistagging introduced above): build("<pos=ADJ> <und|sowie> <chunk=B-NP & !pos=PLU> <chunk=I-NP>*", NPS, true), // "deren Bestimmung und Funktion" (fixes mistagging introduced above): build("<deren> <chunk=B-NP & !pos=PLU> <und|sowie> <chunk=B-NP>*", NPS, true), // "Julia und Karsten": build("<pos=EIG> <und> <pos=EIG>", NPP), // "die älteste und bekannteste Maßnahme" - OpenNLP won't detect that as one NP: build("<pos=ART> <pos=ADJ> <und|sowie> (<pos=ADJ>|<pos=PA2>) <chunk=I-NP & !pos=PLU>+", NPS, true), // "eine Masseeinheit und keine Gewichtseinheit": build("<chunk=B-NP & !pos=PLU> <chunk=I-NP>* <und|sowie> <keine> <chunk=I-NP>+", NPS, true), // "Der See und das anliegende Marschland": build("<NP> <und|sowie> <pos=ART> <pos=PA1> <pos=SUB>", NPP, true), // "eins ihrer drei Autos": build("(<eins>|<eines>) <chunk=B-NP> <chunk=I-NP>+", NPS), // "er und seine Schwester": build("<ich|du|er|sie|es|wir|ihr|sie> <und|oder|sowie> <NP>", NPP), // "sowohl sein Vater als auch seine Mutter": build("<sowohl> <NP> <als> <auch> <NP>", NPP), // "sowohl Tom als auch Maria": build("<sowohl> <pos=EIG> <als> <auch> <pos=EIG>", NPP), // "sowohl er als auch seine Schwester": build("<sowohl> <ich|du|er|sie|es|wir|ihr|sie> <als> <auch> <NP>", NPP), // "Rekonstruktionen oder der Wiederaufbau", aber nicht "Isolation und ihre Überwindung": build("<pos=SUB> <und|oder|sowie> <chunk=B-NP & !ihre> <chunk=I-NP>*", NPP), // "Weder Gerechtigkeit noch Freiheit": build("<weder> <pos=SUB> <noch> <pos=SUB>", NPP), // "drei Katzen" - needed as ZAL cannot be unified, as it has no features: build("(<zwei|drei|vier|fünf|sechs|sieben|acht|neun|zehn|elf|zwölf>) <chunk=I-NP>", NPP), // "der von der Regierung geprüfte Hund ist grün": build("<chunk=B-NP> <pos=PRP> <NP> <chunk=B-NP & pos=SIN> <chunk=I-NP>*", NPS), build("<chunk=B-NP> <pos=PRP> <NP> <chunk=B-NP & pos=PLU> <chunk=I-NP>*", NPP), // "der von der Regierung geprüfte Hund": build("<chunk=B-NP> <pos=PRP> <NP> <pos=PA2> <chunk=B-NP & !pos=PLU> <chunk=I-NP>*", NPS), build("<chunk=B-NP> <pos=PRP> <NP> <pos=PA2> <chunk=B-NP & !pos=SIN> <chunk=I-NP>*", NPP), // "Herr und Frau Schröder": build("<Herr|Frau> <und> <Herr|Frau> <pos=EIG>*", NPP), // "ein Hund", aber nicht: "[kaum mehr als] vier Prozent": build("<chunk=B-NP & !pos=ZAL & !pos=PLU & !chunk=NPP & !einige & !(regex=&prozent;)> <chunk=I-NP & !pos=PLU & !und>*", NPS), //"!und": OpenNLP packt "Bob und Tom" in eine NP // "die Hunde": build("<chunk=B-NP & !pos=SIN & !chunk=NPS & !Ellen> <chunk=I-NP & !pos=SIN>*", NPP), // "die hohe Zahl dieser relativ kleinen Verwaltungseinheiten": build("<chunk=NPS> <pos=PRO> <pos=ADJ> <pos=ADJ> <NP>", NPS), // "eine der am meisten verbreiteten Krankheiten": build("<regex=eine[rs]?> <der> <am> <pos=ADJ> <pos=PA2> <NP>", NPS), // "einer der beiden Höfe": build("<regex=eine[rs]?> <der> <beiden> <pos=ADJ>* <pos=SUB>", NPS), // "Einer seiner bedeutendsten Kämpfe": build("<regex=eine[rs]?> <seiner|ihrer> <pos=PA1> <pos=SUB>", NPS), // "xy Prozent" - beide Varianten okay (zumindest umgangssprachlich): // siehe http://www.canoo.net/services/OnlineGrammar/Wort/Verb/Numerus-Person/ProblemNum.html#Anchor-Mengenangabe-49575 build("<regex=[\\d,.]+> <&prozent;>", NPS), build("<regex=[\\d,.]+> <&prozent;>", NPP), // "[alle Arbeitsplätze so umzugestalten,] dass sie wie ein Spiel": build("<dass> <sie> <wie> <NP>", NPP), // "[so dass Knochenbrüche und] Platzwunden die Regel [sind]" build("<pos=PLU> <die> <Regel>", NPP), // "Veranstaltung, die immer wieder ein kultureller Höhepunkt", aber nicht "... in der Geschichte des Museums, die Sammlung ist seit 2011 zugänglich.": build("<chunk=B-NP & pos=SIN> <chunk=I-NP & pos=SIN>* <,> <die> <pos=ADV>+ <chunk=NPS>+", NPS), // "Die Nauheimer Musiktage, die immer wieder ein kultureller Höhepunkt sind": build("<chunk=B-NP & pos=PLU> <chunk=I-NP & pos=PLU>* <,> <die> <pos=ADV>+ <chunk=NPS>+", NPP), // ===== genitive phrases and similar ==================================================== // "Das letzte der teilnehmenden Länder": build("<der|die|das> <pos=ADJ> <der> <pos=PA1> <pos=SUB>", NPS), // "Ursachen der vorliegenden Durchblutungsstörung": build("<pos=SUB & pos=PLU> <der> <pos=PA1> <pos=SUB>", NPP), // "die ältere der beiden Töchter": build("<der|die|das> <pos=ADJ> <der> <pos=PRO>? <pos=SUB>", NPS), // "Synthese organischer Verbindungen", "die Anordnung der vier Achsen", aber nicht "Einige der Inhaltsstoffe": build("<chunk=NPS & !einige> <chunk=NPP & (pos=GEN |pos=ZAL)>+", NPS, true), // "die Kenntnisse der Sprache": build("<chunk=NPP> <chunk=NPS & pos=GEN>+", NPP, true), // "die Pyramide des Friedens und der Eintracht": build("<chunk=NPS>+ <und> <chunk=NP[SP] & (pos=GEN | pos=ADV)>+", NPS, true), // "Teil der dort ausgestellten Bestände": build("<chunk=NPS>+ <der> <pos=ADV> <pos=PA2> <chunk=I-NP>", NPS, true), // "Autor der ersten beiden Bücher": build("<chunk=NPS>+ <der> (<pos=ADJ>|<pos=ZAL>) <NP>", NPS, true), // "Autor der beiden Bücher": build("<chunk=NPS>+ <der> <NP>", NPS, true), // "Teil der umfangreichen dort ausgestellten Bestände": build("<chunk=NPS>+ <der> <pos=ADJ> <pos=ADV> <pos=PA2> <NP>", NPS, true), // "die Krankheit unserer heutigen Städte und Siedlungen": build("<chunk=NPS>+ <pos=PRO:POS> <pos=ADJ> <NP>", NPS, true), // "der letzte der vier großen Flüsse": build("<der|das> <pos=ADJ> <der> <pos=ZAL> <NP>", NPS, true), // "Elemente eines axiomatischen Systems": -- führt zu Fehlalarm anderswo //build("<chunk=B-NP & pos=PLU> <chunk=I-NP>* <chunk=B-NP & pos=GEN> <chunk=I-NP>*", NPP), // "eine Menge englischer Wörter [sind aus dem Lateinischen abgeleitet]": // NPP stimmt aber nicht für Mathematik, dort NPS: "Eine Menge ist ein Konzept der Mathematik." build("<eine> <menge> <NP>+", NPP, true), // "dass [sie und sein Sohn ein Paar] sind": build("<er|sie|es> <und> <NP> <NP>", NPP), // ===== prepositional phrases =========================================================== // "laut den meisten Quellen": build("<laut> <regex=.*>{0,3} <Quellen>", PP, true), // "bei den sehr niedrigen Oberflächentemperaturen" (OpenNLP doesn't find this) build("<pos=PRP> <pos=ART:> <pos=ADV>* <pos=ADJ> <NP>", PP, true), // "in den alten Religionen, Mythen und Sagen": build("<pos=PRP> <chunk=NPP>+ <,> <NP>", PP, true), // "für die Stadtteile und selbständigen Ortsteile": build("<pos=PRP> <chunk=NPP>+", PP, true), // "Das Bündnis zwischen der Sowjetunion und Kuba": build("<pos=PRP> <der> <chunk=NPP>+", PP), // "in chemischen Komplexverbindungen", "für die Fische": build("<pos=PRP> <NP>", PP), // "einschließlich der biologischen und sozialen Grundlagen": // with OpenNLP: build("<pos=PRP> <NP> <pos=ADJ> (<und>|<oder>|<bzw.>) <pos=ADJ> <NP>", PP), build("<pos=PRP> <NP> <pos=ADJ> (<und>|<oder>|<bzw.>) <NP>", PP), // "für Ärzte und Ärztinnen festgestellte Risikoprofil", "der als Befestigung gedachte östliche Teil der Burg": build("<pos=PRP> (<NP>)+", PP), // "in den darauf folgenden Wochen": build("<pos=PRP> <chunk=B-NP> <pos=ADV> <NP>", PP), // "in nur zwei Wochen": build("<pos=PRP> <pos=ADV> <pos=ZAL> <chunk=B-NP>", PP), // "in deren deutschen Installationen": build("<pos=PRP> <pos=PRO> <NP>", PP), // "nach sachlichen und militärischen Kriterien" - we need to help OpenNLP a bit with this one: // with OpenNLP: build("<pos=PRP> <pos=ADJ> (<und|oder|sowie>) <pos=ADJ> <chunk=B-NP>", PP), build("<pos=PRP> <pos=ADJ> (<und|oder|sowie>) <NP>", PP), // "mit über 1000 Handschriften": build("<pos=PRP> <pos=ADV> <regex=\\d+> <NP>", PP), // "über laufende Sanierungsmaßnahmen": build("<pos=PRP> <pos=PA1> <NP>", PP), // "Aufgrund stark schwankender Absatzmärkte war die GEFA-Flug..." build("<pos=PRP> <pos=ADJ> <pos=PA1> <NP>", PP), // "durch Einsatz größerer Maschinen und bessere Kapazitätsplanung": // with OpenNLP: build("<pos=PRP> <NP> <pos=ADJ> <NP> (<und|oder>) <NP>", PP), build("<pos=PRP> <NP> <NP> (<und|oder>) <NP>", PP), // "bei sehr guten Beobachtungsbedingungen": build("<pos=PRP> <pos=ADV> <pos=ADJ> <NP>", PP), // "[Von ursprünglich drei Almhütten] ist noch eine erhalten": build("<pos=PRP> <pos=ADJ:PRD:GRU> <pos=ZAL> <NP>", PP), // "die darauffolgenden Jahre" -> eigentlich "in den darauffolgenden Jahren": build("<die> <pos=ADJ> <Sekunden|Minuten|Stunden|Tage|Wochen|Monate|Jahre|Jahrzehnte|Jahrhunderte> (<NP>)?", PP), // "die letzten zwei Monate" -> eigentlich "in den letzten zwei Monaten": build("<die> <pos=ADJ> <pos=ZAL> <Sekunden|Minuten|Stunden|Tage|Wochen|Monate|Jahre|Jahrzehnte|Jahrhunderte> (<NP>)?", PP), // "letztes Jahr": build("<regex=(vor)?letzte[sn]?> <Woche|Monat|Jahr|Jahrzehnt|Jahrhundert>", PP), // "Für in Österreich lebende Afrikaner und Afrikanerinnen": build("<für> <in> <pos=EIG> <pos=PA1> <pos=SUB> <und> <pos=SUB>", PP, true), // "die Beziehungen zwischen Kanada und dem Iran": build("<chunk=NPP> <zwischen> <pos=EIG> <und|sowie> <NP>", NPP), // ", die die hauptsächliche Beute der Eisbären", ", welche der Urstoff aller Körper": build("<,> <die|welche> <NP> <chunk=NPS & pos=GEN>+", NPP), // "Kommentare, Korrekturen, Kritik": build("<NP> <,> <NP> <,> <NP>", NPP), // "Details, Dialoge, wie auch die Typologie der Charaktere": build("<NP> <,> <NP> <,> <wie> <auch> <chunk=NPS>+", NPP) ); private static RegularExpressionWithPhraseType build(String expr, PhraseType phraseType) { return build(expr, phraseType, false); } private static RegularExpressionWithPhraseType build(String expr, PhraseType phraseType, boolean overwrite) { String expandedExpr = expr; for (Map.Entry<String, String> entry : SYNTAX_EXPANSION.entrySet()) { expandedExpr = expandedExpr.replace(entry.getKey(), entry.getValue()); } RegularExpression<ChunkTaggedToken> expression = RegularExpression.compile(expandedExpr, FACTORY); return new RegularExpressionWithPhraseType(expression, phraseType, overwrite); } public GermanChunker() { } @Override public void addChunkTags(List<AnalyzedTokenReadings> tokenReadings) { List<ChunkTaggedToken> chunkTaggedTokens = getBasicChunks(tokenReadings); for (RegularExpressionWithPhraseType regex : REGEXES2) { apply(regex, chunkTaggedTokens); } assignChunksToReadings(chunkTaggedTokens); } List<ChunkTaggedToken> getBasicChunks(List<AnalyzedTokenReadings> tokenReadings) { List<ChunkTaggedToken> chunkTaggedTokens = new ArrayList<>(); for (AnalyzedTokenReadings tokenReading : tokenReadings) { if (!tokenReading.isWhitespace()) { List<ChunkTag> chunkTags = Collections.singletonList(new ChunkTag("O")); ChunkTaggedToken chunkTaggedToken = new ChunkTaggedToken(tokenReading.getToken(), chunkTags, tokenReading); chunkTaggedTokens.add(chunkTaggedToken); } } if (debug) { System.out.println("=============== CHUNKER INPUT ==============="); System.out.println(getDebugString(chunkTaggedTokens)); } for (RegularExpressionWithPhraseType regex : REGEXES1) { apply(regex, chunkTaggedTokens); } return chunkTaggedTokens; } private void apply(RegularExpressionWithPhraseType regex, List<ChunkTaggedToken> tokens) { String prevDebug = getDebugString(tokens); try { AffectedSpans affectedSpans = doApplyRegex(regex, tokens); String debug = getDebugString(tokens); if (!debug.equals(prevDebug)) { printDebugInfo(regex, affectedSpans, debug); } } catch (Exception e) { throw new RuntimeException("Could not apply chunk regexp '" + regex + "' to tokens: " + tokens, e); } } private void assignChunksToReadings(List<ChunkTaggedToken> chunkTaggedTokens) { for (ChunkTaggedToken taggedToken : chunkTaggedTokens) { AnalyzedTokenReadings readings = taggedToken.getReadings(); if (readings != null) { readings.setChunkTags(taggedToken.getChunkTags()); } } } private AffectedSpans doApplyRegex(RegularExpressionWithPhraseType regex, List<ChunkTaggedToken> tokens) { List<Match<ChunkTaggedToken>> matches = regex.expression.findAll(tokens); List<Span> affectedSpans = new ArrayList<>(); for (Match<ChunkTaggedToken> match : matches) { affectedSpans.add(new Span(match.startIndex(), match.endIndex())); for (int i = match.startIndex(); i < match.endIndex(); i++) { ChunkTaggedToken token = tokens.get(i); List<ChunkTag> newChunkTags = new ArrayList<>(); newChunkTags.addAll(token.getChunkTags()); if (regex.overwrite) { List<ChunkTag> filtered = new ArrayList<>(); for (ChunkTag newChunkTag : newChunkTags) { if (!FILTER_TAGS.contains(newChunkTag.getChunkTag())) { filtered.add(newChunkTag); } } newChunkTags = filtered; } ChunkTag newTag = getChunkTag(regex, match, i); if (!newChunkTags.contains(newTag)) { newChunkTags.add(newTag); newChunkTags.remove(new ChunkTag("O")); } tokens.set(i, new ChunkTaggedToken(token.getToken(), newChunkTags, token.getReadings())); } } return new AffectedSpans(affectedSpans); } private ChunkTag getChunkTag(RegularExpressionWithPhraseType regex, Match<ChunkTaggedToken> match, int i) { ChunkTag newTag; if (regex.phraseType == NP) { // we assign the same tags as the OpenNLP chunker if (i == match.startIndex()) { newTag = new ChunkTag("B-NP"); } else { newTag = new ChunkTag("I-NP"); } } else { newTag = new ChunkTag(regex.phraseType.name()); } return newTag; } private void printDebugInfo(RegularExpressionWithPhraseType regex, AffectedSpans affectedSpans, String debug) { System.out.println("=== Applied " + regex + " ==="); if (regex.overwrite) { System.out.println("Note: overwrite mode, replacing old " + FILTER_TAGS + " tags"); } String[] debugLines = debug.split("\n"); int i = 0; for (String debugLine : debugLines) { if (affectedSpans.isAffected(i)) { System.out.println(debugLine.replaceFirst("^ ", " *")); } else { System.out.println(debugLine); } i++; } System.out.println(); } private String getDebugString(List<ChunkTaggedToken> tokens) { if (!debug) { return ""; } StringBuilder sb = new StringBuilder(); for (ChunkTaggedToken token : tokens) { String tokenReadingStr = token.getReadings().toString().replaceFirst(Pattern.quote(token.getToken()) + "\\[", "["); sb.append(" ").append(token).append(" -- ").append(tokenReadingStr).append("\n"); } return sb.toString(); } private static class Span { final int startIndex; final int endIndex; Span(int startIndex, int endIndex) { this.startIndex = startIndex; this.endIndex = endIndex; } } private static class AffectedSpans { final List<Span> spans; AffectedSpans(List<Span> spans) { this.spans = spans; } boolean isAffected(int pos) { for (Span span : spans) { if (pos >= span.startIndex && pos < span.endIndex) { return true; } } return false; } } private static class RegularExpressionWithPhraseType { final RegularExpression<ChunkTaggedToken> expression; final PhraseType phraseType; final boolean overwrite; RegularExpressionWithPhraseType(RegularExpression<ChunkTaggedToken> expression, PhraseType phraseType, boolean overwrite) { this.expression = expression; this.phraseType = phraseType; this.overwrite = overwrite; } @Override public String toString() { return phraseType + " <= " + expression + " (overwrite: " + overwrite + ")"; } } }